diff --git a/packages/desktop/src-tauri/src/commands/logs.rs b/packages/desktop/src-tauri/src/commands/logs.rs index 8482b3eb..1a23ac40 100644 --- a/packages/desktop/src-tauri/src/commands/logs.rs +++ b/packages/desktop/src-tauri/src/commands/logs.rs @@ -18,7 +18,7 @@ pub async fn fetch_desktop_logs() -> Result { 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 }) diff --git a/packages/desktop/src-tauri/src/logging.rs b/packages/desktop/src-tauri/src/logging.rs index dbc2dba9..f9ffa69f 100644 --- a/packages/desktop/src-tauri/src/logging.rs +++ b/packages/desktop/src-tauri/src/logging.rs @@ -15,6 +15,6 @@ pub fn log_directory() -> Option { pub fn log_file_path() -> Option { let mut dir = log_directory()?; - dir.push("desktop.log"); + dir.push("openchamber.log"); Some(dir) } diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs index cb62c112..6dac9e5b 100644 --- a/packages/desktop/src-tauri/src/main.rs +++ b/packages/desktop/src-tauri/src/main.rs @@ -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(app: &tauri::AppHandle) -> 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(app: &tauri::AppHandle) -> 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(app: &tauri::AppHandle) -> 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(app: &tauri::AppHandle) -> 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(app: &tauri::AppHandle) -> 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(app: &tauri::AppHandle) -> 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| { diff --git a/packages/desktop/src/api/diagnostics.ts b/packages/desktop/src/api/diagnostics.ts index 169df787..0d2afd24 100644 --- a/packages/desktop/src/api/diagnostics.ts +++ b/packages/desktop/src/api/diagnostics.ts @@ -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 : '', }); diff --git a/packages/desktop/src/main.tsx b/packages/desktop/src/main.tsx index 1c4b8251..0b8156c5 100644 --- a/packages/desktop/src/main.tsx +++ b/packages/desktop/src/main.tsx @@ -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> = []; @@ -62,6 +63,11 @@ try { }); cleanupFunctions.push(() => updateCheckUnlisten()); + const menuActionUnlisten = await listen(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('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 { try { diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 25ffc9f7..024298a8 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -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(); diff --git a/packages/ui/src/components/layout/Sidebar.tsx b/packages/ui/src/components/layout/Sidebar.tsx index 1387120b..fcda76b0 100644 --- a/packages/ui/src/components/layout/Sidebar.tsx +++ b/packages/ui/src/components/layout/Sidebar.tsx @@ -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 = ({ 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]); diff --git a/packages/ui/src/components/ui/MemoryDebugPanel.tsx b/packages/ui/src/components/ui/MemoryDebugPanel.tsx index 3dc05e33..65068dfa 100644 --- a/packages/ui/src/components/ui/MemoryDebugPanel.tsx +++ b/packages/ui/src/components/ui/MemoryDebugPanel.tsx @@ -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 = ({ onClose }) = {}
- - - + + + + + + Trim current session to only 10 most recent messages + + + + + + + + Remove least recently used sessions from memory cache + + + + + + + + Log current memory state to browser console + +
diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts index 222cb37d..7aed46b8 100644 --- a/packages/ui/src/hooks/useKeyboardShortcuts.ts +++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts @@ -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); diff --git a/packages/ui/src/hooks/useMenuActions.ts b/packages/ui/src/hooks/useMenuActions.ts new file mode 100644 index 00000000..d5957173 --- /dev/null +++ b/packages/ui/src/hooks/useMenuActions.ts @@ -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).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, + ]); +};