feat(macos): implement comprehensive native menu system
This commit is contained in:
@@ -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 })
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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 : '',
|
||||
});
|
||||
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
};
|
||||
Reference in New Issue
Block a user