feat(macos): implement comprehensive native menu system

This commit is contained in:
Bohdan Triapitsyn
2025-12-19 17:57:50 +02:00
parent 08ac58aa8d
commit 560db1c5fa
10 changed files with 582 additions and 54 deletions
@@ -18,7 +18,7 @@ pub async fn fetch_desktop_logs() -> Result<DesktopLogFile, String> {
let file_name = path
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("desktop.log")
.unwrap_or("openchamber.log")
.to_string();
Ok(DesktopLogFile { file_name, content })
+1 -1
View File
@@ -15,6 +15,6 @@ pub fn log_directory() -> Option<PathBuf> {
pub fn log_file_path() -> Option<PathBuf> {
let mut dir = log_directory()?;
dir.push("desktop.log");
dir.push("openchamber.log");
Some(dir)
}
+287 -8
View File
@@ -88,6 +88,44 @@ const MENU_ITEM_REPORT_BUG_ID: &str = "openchamber_report_bug";
#[cfg(target_os = "macos")]
const MENU_ITEM_REQUEST_FEATURE_ID: &str = "openchamber_request_feature";
// App menu
#[cfg(target_os = "macos")]
const MENU_ITEM_SETTINGS_ID: &str = "openchamber_settings";
#[cfg(target_os = "macos")]
const MENU_ITEM_COMMAND_PALETTE_ID: &str = "openchamber_command_palette";
// File menu
#[cfg(target_os = "macos")]
const MENU_ITEM_NEW_SESSION_ID: &str = "openchamber_new_session";
#[cfg(target_os = "macos")]
const MENU_ITEM_WORKTREE_CREATOR_ID: &str = "openchamber_worktree_creator";
#[cfg(target_os = "macos")]
const MENU_ITEM_CHANGE_WORKSPACE_ID: &str = "openchamber_change_workspace";
// View menu
#[cfg(target_os = "macos")]
const MENU_ITEM_OPEN_GIT_TAB_ID: &str = "openchamber_open_git_tab";
#[cfg(target_os = "macos")]
const MENU_ITEM_OPEN_DIFF_TAB_ID: &str = "openchamber_open_diff_tab";
#[cfg(target_os = "macos")]
const MENU_ITEM_OPEN_TERMINAL_TAB_ID: &str = "openchamber_open_terminal_tab";
#[cfg(target_os = "macos")]
const MENU_ITEM_THEME_LIGHT_ID: &str = "openchamber_theme_light";
#[cfg(target_os = "macos")]
const MENU_ITEM_THEME_DARK_ID: &str = "openchamber_theme_dark";
#[cfg(target_os = "macos")]
const MENU_ITEM_THEME_SYSTEM_ID: &str = "openchamber_theme_system";
#[cfg(target_os = "macos")]
const MENU_ITEM_TOGGLE_SIDEBAR_ID: &str = "openchamber_toggle_sidebar";
#[cfg(target_os = "macos")]
const MENU_ITEM_TOGGLE_MEMORY_DEBUG_ID: &str = "openchamber_toggle_memory_debug";
// Help menu
#[cfg(target_os = "macos")]
const MENU_ITEM_HELP_DIALOG_ID: &str = "openchamber_help_dialog";
#[cfg(target_os = "macos")]
const MENU_ITEM_DOWNLOAD_LOGS_ID: &str = "openchamber_download_logs";
const GITHUB_BUG_REPORT_URL: &str = "https://github.com/btriapitsyn/openchamber/issues/new?template=bug_report.yml";
const GITHUB_FEATURE_REQUEST_URL: &str = "https://github.com/btriapitsyn/openchamber/issues/new?template=feature_request.yml";
@@ -306,11 +344,135 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
let check_for_updates = MenuItem::with_id(
app,
MENU_ITEM_CHECK_FOR_UPDATES_ID,
"Check for Updates",
"Check for Updates",
true,
None::<&str>,
)?;
// App menu items
let settings = MenuItem::with_id(
app,
MENU_ITEM_SETTINGS_ID,
"Settings",
true,
Some("Cmd+,"),
)?;
let command_palette = MenuItem::with_id(
app,
MENU_ITEM_COMMAND_PALETTE_ID,
"Command Palette",
true,
Some("Ctrl+X"),
)?;
// File menu items
let new_session = MenuItem::with_id(
app,
MENU_ITEM_NEW_SESSION_ID,
"New Session",
true,
Some("Ctrl+N"),
)?;
let worktree_creator = MenuItem::with_id(
app,
MENU_ITEM_WORKTREE_CREATOR_ID,
"New Worktree…",
true,
Some("Ctrl+Shift+N"),
)?;
let change_workspace = MenuItem::with_id(
app,
MENU_ITEM_CHANGE_WORKSPACE_ID,
"Change Workspace…",
true,
None::<&str>,
)?;
// View menu items
let open_git_tab = MenuItem::with_id(
app,
MENU_ITEM_OPEN_GIT_TAB_ID,
"Git",
true,
Some("Ctrl+G"),
)?;
let open_diff_tab = MenuItem::with_id(
app,
MENU_ITEM_OPEN_DIFF_TAB_ID,
"Diff",
true,
Some("Ctrl+E"),
)?;
let open_terminal_tab = MenuItem::with_id(
app,
MENU_ITEM_OPEN_TERMINAL_TAB_ID,
"Terminal",
true,
Some("Ctrl+T"),
)?;
let theme_light = MenuItem::with_id(
app,
MENU_ITEM_THEME_LIGHT_ID,
"Light Theme",
true,
None::<&str>,
)?;
let theme_dark = MenuItem::with_id(
app,
MENU_ITEM_THEME_DARK_ID,
"Dark Theme",
true,
None::<&str>,
)?;
let theme_system = MenuItem::with_id(
app,
MENU_ITEM_THEME_SYSTEM_ID,
"System Theme",
true,
None::<&str>,
)?;
let toggle_sidebar = MenuItem::with_id(
app,
MENU_ITEM_TOGGLE_SIDEBAR_ID,
"Toggle Session Sidebar",
true,
Some("Ctrl+L"),
)?;
let toggle_memory_debug = MenuItem::with_id(
app,
MENU_ITEM_TOGGLE_MEMORY_DEBUG_ID,
"Toggle Memory Debug",
true,
Some("Cmd+Shift+M"),
)?;
// Help menu items
let help_dialog = MenuItem::with_id(
app,
MENU_ITEM_HELP_DIALOG_ID,
"Keyboard Shortcuts",
true,
Some("Ctrl+H"),
)?;
let download_logs = MenuItem::with_id(
app,
MENU_ITEM_DOWNLOAD_LOGS_ID,
"Download Logs",
true,
Some("Ctrl+Shift+L"),
)?;
let report_bug = MenuItem::with_id(
app,
MENU_ITEM_REPORT_BUG_ID,
@@ -327,6 +489,13 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
None::<&str>,
)?;
let theme_submenu = Submenu::with_items(
app,
"Theme",
true,
&[&theme_light, &theme_dark, &theme_system],
)?;
let window_menu = Submenu::with_id_and_items(
app,
WINDOW_SUBMENU_ID,
@@ -345,7 +514,13 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
HELP_SUBMENU_ID,
"Help",
true,
&[&report_bug, &request_feature],
&[
&help_dialog,
&download_logs,
&PredefinedMenuItem::separator(app)?,
&report_bug,
&request_feature,
],
)?;
Menu::with_items(
@@ -359,6 +534,9 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
&PredefinedMenuItem::about(app, None, Some(about_metadata))?,
&check_for_updates,
&PredefinedMenuItem::separator(app)?,
&settings,
&command_palette,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::services(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::hide(app, None)?,
@@ -371,7 +549,14 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
app,
"File",
true,
&[&PredefinedMenuItem::close_window(app, None)?],
&[
&new_session,
&worktree_creator,
&PredefinedMenuItem::separator(app)?,
&change_workspace,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::close_window(app, None)?,
],
)?,
&Submenu::with_items(
app,
@@ -391,7 +576,18 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
app,
"View",
true,
&[&PredefinedMenuItem::fullscreen(app, None)?],
&[
&open_git_tab,
&open_diff_tab,
&open_terminal_tab,
&PredefinedMenuItem::separator(app)?,
&theme_submenu,
&PredefinedMenuItem::separator(app)?,
&toggle_sidebar,
&toggle_memory_debug,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::fullscreen(app, None)?,
],
)?,
&window_menu,
&help_menu,
@@ -409,7 +605,7 @@ fn main() {
if let Some(dir) = logging::log_directory() {
log_builder = log_builder.target(Target::new(TargetKind::Folder {
path: dir,
file_name: Some("desktop".into()),
file_name: Some("openchamber".into()),
}));
}
@@ -646,12 +842,16 @@ fn main() {
.on_menu_event(|app, event| {
#[cfg(target_os = "macos")]
{
if event.id() == MENU_ITEM_CHECK_FOR_UPDATES_ID {
let event_id = event.id().as_ref();
// Check for updates
if event_id == MENU_ITEM_CHECK_FOR_UPDATES_ID {
let _ = app.emit(CHECK_FOR_UPDATES_EVENT, ());
return;
}
if event.id() == MENU_ITEM_REPORT_BUG_ID {
// External links
if event_id == MENU_ITEM_REPORT_BUG_ID {
use tauri_plugin_shell::ShellExt;
#[allow(deprecated)]
{
@@ -660,7 +860,7 @@ fn main() {
return;
}
if event.id() == MENU_ITEM_REQUEST_FEATURE_ID {
if event_id == MENU_ITEM_REQUEST_FEATURE_ID {
use tauri_plugin_shell::ShellExt;
#[allow(deprecated)]
{
@@ -668,6 +868,85 @@ fn main() {
}
return;
}
// App menu actions
if event_id == MENU_ITEM_SETTINGS_ID {
let _ = app.emit("openchamber:menu-action", "settings");
return;
}
if event_id == MENU_ITEM_COMMAND_PALETTE_ID {
let _ = app.emit("openchamber:menu-action", "command-palette");
return;
}
// File menu actions
if event_id == MENU_ITEM_NEW_SESSION_ID {
let _ = app.emit("openchamber:menu-action", "new-session");
return;
}
if event_id == MENU_ITEM_WORKTREE_CREATOR_ID {
let _ = app.emit("openchamber:menu-action", "worktree-creator");
return;
}
if event_id == MENU_ITEM_CHANGE_WORKSPACE_ID {
let _ = app.emit("openchamber:menu-action", "change-workspace");
return;
}
// View menu actions
if event_id == MENU_ITEM_OPEN_GIT_TAB_ID {
let _ = app.emit("openchamber:menu-action", "open-git-tab");
return;
}
if event_id == MENU_ITEM_OPEN_DIFF_TAB_ID {
let _ = app.emit("openchamber:menu-action", "open-diff-tab");
return;
}
if event_id == MENU_ITEM_OPEN_TERMINAL_TAB_ID {
let _ = app.emit("openchamber:menu-action", "open-terminal-tab");
return;
}
if event_id == MENU_ITEM_THEME_LIGHT_ID {
let _ = app.emit("openchamber:menu-action", "theme-light");
return;
}
if event_id == MENU_ITEM_THEME_DARK_ID {
let _ = app.emit("openchamber:menu-action", "theme-dark");
return;
}
if event_id == MENU_ITEM_THEME_SYSTEM_ID {
let _ = app.emit("openchamber:menu-action", "theme-system");
return;
}
if event_id == MENU_ITEM_TOGGLE_SIDEBAR_ID {
let _ = app.emit("openchamber:menu-action", "toggle-sidebar");
return;
}
if event_id == MENU_ITEM_TOGGLE_MEMORY_DEBUG_ID {
let _ = app.emit("openchamber:menu-action", "toggle-memory-debug");
return;
}
// Help menu actions
if event_id == MENU_ITEM_HELP_DIALOG_ID {
let _ = app.emit("openchamber:menu-action", "help-dialog");
return;
}
if event_id == MENU_ITEM_DOWNLOAD_LOGS_ID {
let _ = app.emit("openchamber:menu-action", "download-logs");
return;
}
}
})
.on_window_event(|window, event| {
+1 -1
View File
@@ -7,7 +7,7 @@ type LogResponse = {
};
const normalizePayload = (payload: LogResponse): { fileName: string; content: string } => ({
fileName: typeof payload.fileName === 'string' && payload.fileName.trim().length > 0 ? payload.fileName : 'desktop.log',
fileName: typeof payload.fileName === 'string' && payload.fileName.trim().length > 0 ? payload.fileName : 'openchamber.log',
content: typeof payload.content === 'string' ? payload.content : '',
});
+25 -8
View File
@@ -46,6 +46,7 @@ declare global {
}
const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates';
const MENU_ACTION_EVENT = 'openchamber:menu-action';
const cleanupFunctions: Array<() => void | Promise<void>> = [];
@@ -62,6 +63,11 @@ try {
});
cleanupFunctions.push(() => updateCheckUnlisten());
const menuActionUnlisten = await listen<string>(MENU_ACTION_EVENT, (event) => {
window.dispatchEvent(new CustomEvent(MENU_ACTION_EVENT, { detail: event.payload }));
});
cleanupFunctions.push(() => menuActionUnlisten());
requestInitialNotificationPermission().catch(err => {
console.error('[main] Failed to request notification permission:', err);
});
@@ -117,14 +123,25 @@ if (homeDirectory) {
window.opencodeDesktop = {
homeDirectory,
async getServerInfo() {
const server = window.__OPENCHAMBER_DESKTOP_SERVER__;
return {
webPort: server?.origin ? parseInt(server.origin.split(':')[2] || '0', 10) : null,
openCodePort: server?.opencodePort ?? null,
host: '127.0.0.1',
ready: true,
cliAvailable: server?.cliAvailable ?? false,
};
try {
const info = await invoke<ServerInfo>('desktop_server_info');
return {
webPort: info.server_port,
openCodePort: info.opencode_port ?? null,
host: '127.0.0.1',
ready: info.opencode_port !== null,
cliAvailable: info.cli_available ?? false,
};
} catch {
const server = window.__OPENCHAMBER_DESKTOP_SERVER__;
return {
webPort: server?.origin ? parseInt(server.origin.split(':')[2] || '0', 10) : null,
openCodePort: server?.opencodePort ?? null,
host: '127.0.0.1',
ready: false,
cliAvailable: server?.cliAvailable ?? false,
};
}
},
async getSettings(): Promise<DesktopSettings> {
try {
+7
View File
@@ -7,6 +7,7 @@ import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel';
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
import { useEventStream } from '@/hooks/useEventStream';
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
import { useMenuActions } from '@/hooks/useMenuActions';
import { useMessageSync } from '@/hooks/useMessageSync';
import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap';
import { GitPollingProvider } from '@/hooks/useGitPolling';
@@ -130,6 +131,12 @@ function App({ apis }: AppProps) {
useKeyboardShortcuts();
const handleToggleMemoryDebug = React.useCallback(() => {
setShowMemoryDebug(prev => !prev);
}, []);
useMenuActions(handleToggleMemoryDebug);
useMessageSync();
useSessionStatusBootstrap();
@@ -1,5 +1,6 @@
import React from 'react';
import { RiDownloadLine, RiSettings3Line } from '@remixicon/react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import { ErrorBoundary } from '../ui/ErrorBoundary';
import { useUIStore } from '@/stores/useUIStore';
@@ -84,6 +85,10 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
if (available || downloaded) {
setUpdateDialogOpen(true);
} else {
toast.success('No updates available', {
description: 'You are running the latest version.',
});
}
pendingMenuUpdateCheckRef.current = false;
}, [available, downloaded, checking]);
@@ -2,6 +2,7 @@ import React from 'react';
import { useSessionStore, MEMORY_LIMITS } from '@/stores/useSessionStore';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
import { RiCloseLine, RiDatabase2Line, RiDeleteBinLine, RiPulseLine } from '@remixicon/react';
import { useDesktopServerInfo } from '@/hooks/useDesktopServerInfo';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
@@ -153,40 +154,65 @@ export const MemoryDebugPanel: React.FC<MemoryDebugPanelProps> = ({ onClose }) =
{}
<div className="flex gap-2 pt-2 border-t">
<Button
size="sm"
variant="outline"
className="typography-meta"
onClick={() => {
if (currentSessionId) {
trimToViewportWindow(currentSessionId, 10);
}
}}
>
<RiDeleteBinLine className="h-3 w-3 mr-1" />
Force Trim (10)
</Button>
<Button
size="sm"
variant="outline"
className="typography-meta"
onClick={() => {
evictLeastRecentlyUsed();
}}
>
Evict LRU
</Button>
<Button
size="sm"
variant="outline"
className="typography-meta"
onClick={() => {
}}
>
Log State
</Button>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
className="typography-meta"
onClick={() => {
if (currentSessionId) {
trimToViewportWindow(currentSessionId, 10);
}
}}
>
<RiDeleteBinLine className="h-3 w-3 mr-1" />
Force Trim (10)
</Button>
</TooltipTrigger>
<TooltipContent side="top">
Trim current session to only 10 most recent messages
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
className="typography-meta"
onClick={() => {
evictLeastRecentlyUsed();
}}
>
Evict LRU
</Button>
</TooltipTrigger>
<TooltipContent side="top">
Remove least recently used sessions from memory cache
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
className="typography-meta"
onClick={() => {
console.log('[MemoryDebug] Session store state:', {
sessions: sessions.map(s => ({ id: s.id, title: s.title })),
currentSessionId,
cachedSessions: Array.from(messages.keys()),
memoryStates: Object.fromEntries(sessionMemoryState),
});
}}
>
Log State
</Button>
</TooltipTrigger>
<TooltipContent side="top">
Log current memory state to browser console
</TooltipContent>
</Tooltip>
</div>
</div>
</Card>
@@ -57,11 +57,12 @@ export const useKeyboardShortcuts = () => {
diagnostics
.downloadLogs()
.then(({ fileName, content }) => {
const finalFileName = fileName || 'openchamber.log';
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = fileName || 'desktop.log';
anchor.download = finalFileName;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
+193
View File
@@ -0,0 +1,193 @@
import React from 'react';
import { toast } from 'sonner';
import { useSessionStore } from '@/stores/useSessionStore';
import { useUIStore } from '@/stores/useUIStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { sessionEvents } from '@/lib/sessionEvents';
import { isDesktopRuntime } from '@/lib/desktop';
const MENU_ACTION_EVENT = 'openchamber:menu-action';
type MenuAction =
| 'settings'
| 'command-palette'
| 'new-session'
| 'worktree-creator'
| 'change-workspace'
| 'open-git-tab'
| 'open-diff-tab'
| 'open-terminal-tab'
| 'theme-light'
| 'theme-dark'
| 'theme-system'
| 'toggle-sidebar'
| 'toggle-memory-debug'
| 'help-dialog'
| 'download-logs';
export const useMenuActions = (
onToggleMemoryDebug?: () => void
) => {
const { createSession, initializeNewOpenChamberSession } = useSessionStore();
const {
toggleCommandPalette,
toggleHelpDialog,
toggleSidebar,
setSessionCreateDialogOpen,
setActiveMainTab,
setSettingsDialogOpen,
} = useUIStore();
const { agents } = useConfigStore();
const { setDirectory } = useDirectoryStore();
const { setThemeMode } = useThemeSystem();
const isDownloadingLogsRef = React.useRef(false);
const handleChangeWorkspace = React.useCallback(() => {
if (isDesktopRuntime() && window.opencodeDesktop?.requestDirectoryAccess) {
window.opencodeDesktop
.requestDirectoryAccess('')
.then((result) => {
if (result.success && result.path) {
setDirectory(result.path, { showOverlay: true });
} else if (result.error && result.error !== 'Directory selection cancelled') {
toast.error('Failed to select directory', {
description: result.error,
});
}
})
.catch((error) => {
console.error('Desktop: Error selecting directory:', error);
toast.error('Failed to select directory');
});
} else {
sessionEvents.requestDirectoryDialog();
}
}, [setDirectory]);
React.useEffect(() => {
const handleMenuAction = (event: Event) => {
const action = (event as CustomEvent<MenuAction>).detail;
switch (action) {
case 'settings':
setSettingsDialogOpen(true);
break;
case 'command-palette':
toggleCommandPalette();
break;
case 'new-session':
createSession().then(session => {
if (session) {
initializeNewOpenChamberSession(session.id, agents);
}
});
break;
case 'worktree-creator':
setSessionCreateDialogOpen(true);
break;
case 'change-workspace':
handleChangeWorkspace();
break;
case 'open-git-tab': {
const { activeMainTab } = useUIStore.getState();
setActiveMainTab(activeMainTab === 'git' ? 'chat' : 'git');
break;
}
case 'open-diff-tab': {
const { activeMainTab } = useUIStore.getState();
setActiveMainTab(activeMainTab === 'diff' ? 'chat' : 'diff');
break;
}
case 'open-terminal-tab': {
const { activeMainTab } = useUIStore.getState();
setActiveMainTab(activeMainTab === 'terminal' ? 'chat' : 'terminal');
break;
}
case 'theme-light':
setThemeMode('light');
break;
case 'theme-dark':
setThemeMode('dark');
break;
case 'theme-system':
setThemeMode('system');
break;
case 'toggle-sidebar':
toggleSidebar();
break;
case 'toggle-memory-debug':
onToggleMemoryDebug?.();
break;
case 'help-dialog':
toggleHelpDialog();
break;
case 'download-logs': {
const runtimeAPIs = getRegisteredRuntimeAPIs();
const diagnostics = runtimeAPIs?.diagnostics;
if (!diagnostics || isDownloadingLogsRef.current) {
break;
}
isDownloadingLogsRef.current = true;
diagnostics
.downloadLogs()
.then(({ fileName, content }) => {
const finalFileName = fileName || 'openchamber.log';
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = finalFileName;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
toast.success('Logs saved', {
description: `Downloaded to ~/Downloads/${finalFileName}`,
});
})
.catch(() => {
toast.error('Failed to download logs');
})
.finally(() => {
isDownloadingLogsRef.current = false;
});
break;
}
}
};
window.addEventListener(MENU_ACTION_EVENT, handleMenuAction);
return () => window.removeEventListener(MENU_ACTION_EVENT, handleMenuAction);
}, [
createSession,
initializeNewOpenChamberSession,
toggleCommandPalette,
toggleHelpDialog,
toggleSidebar,
setSessionCreateDialogOpen,
setActiveMainTab,
setSettingsDialogOpen,
setThemeMode,
agents,
onToggleMemoryDebug,
handleChangeWorkspace,
]);
};