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
|
let file_name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|value| value.to_str())
|
.and_then(|value| value.to_str())
|
||||||
.unwrap_or("desktop.log")
|
.unwrap_or("openchamber.log")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
Ok(DesktopLogFile { file_name, content })
|
Ok(DesktopLogFile { file_name, content })
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ pub fn log_directory() -> Option<PathBuf> {
|
|||||||
|
|
||||||
pub fn log_file_path() -> Option<PathBuf> {
|
pub fn log_file_path() -> Option<PathBuf> {
|
||||||
let mut dir = log_directory()?;
|
let mut dir = log_directory()?;
|
||||||
dir.push("desktop.log");
|
dir.push("openchamber.log");
|
||||||
Some(dir)
|
Some(dir)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,44 @@ const MENU_ITEM_REPORT_BUG_ID: &str = "openchamber_report_bug";
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
const MENU_ITEM_REQUEST_FEATURE_ID: &str = "openchamber_request_feature";
|
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_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";
|
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(
|
let check_for_updates = MenuItem::with_id(
|
||||||
app,
|
app,
|
||||||
MENU_ITEM_CHECK_FOR_UPDATES_ID,
|
MENU_ITEM_CHECK_FOR_UPDATES_ID,
|
||||||
"Check for Updates…",
|
"Check for Updates",
|
||||||
true,
|
true,
|
||||||
None::<&str>,
|
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(
|
let report_bug = MenuItem::with_id(
|
||||||
app,
|
app,
|
||||||
MENU_ITEM_REPORT_BUG_ID,
|
MENU_ITEM_REPORT_BUG_ID,
|
||||||
@@ -327,6 +489,13 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
|
|||||||
None::<&str>,
|
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(
|
let window_menu = Submenu::with_id_and_items(
|
||||||
app,
|
app,
|
||||||
WINDOW_SUBMENU_ID,
|
WINDOW_SUBMENU_ID,
|
||||||
@@ -345,7 +514,13 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
|
|||||||
HELP_SUBMENU_ID,
|
HELP_SUBMENU_ID,
|
||||||
"Help",
|
"Help",
|
||||||
true,
|
true,
|
||||||
&[&report_bug, &request_feature],
|
&[
|
||||||
|
&help_dialog,
|
||||||
|
&download_logs,
|
||||||
|
&PredefinedMenuItem::separator(app)?,
|
||||||
|
&report_bug,
|
||||||
|
&request_feature,
|
||||||
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Menu::with_items(
|
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))?,
|
&PredefinedMenuItem::about(app, None, Some(about_metadata))?,
|
||||||
&check_for_updates,
|
&check_for_updates,
|
||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
|
&settings,
|
||||||
|
&command_palette,
|
||||||
|
&PredefinedMenuItem::separator(app)?,
|
||||||
&PredefinedMenuItem::services(app, None)?,
|
&PredefinedMenuItem::services(app, None)?,
|
||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
&PredefinedMenuItem::hide(app, None)?,
|
&PredefinedMenuItem::hide(app, None)?,
|
||||||
@@ -371,7 +549,14 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
|
|||||||
app,
|
app,
|
||||||
"File",
|
"File",
|
||||||
true,
|
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(
|
&Submenu::with_items(
|
||||||
app,
|
app,
|
||||||
@@ -391,7 +576,18 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
|
|||||||
app,
|
app,
|
||||||
"View",
|
"View",
|
||||||
true,
|
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,
|
&window_menu,
|
||||||
&help_menu,
|
&help_menu,
|
||||||
@@ -409,7 +605,7 @@ fn main() {
|
|||||||
if let Some(dir) = logging::log_directory() {
|
if let Some(dir) = logging::log_directory() {
|
||||||
log_builder = log_builder.target(Target::new(TargetKind::Folder {
|
log_builder = log_builder.target(Target::new(TargetKind::Folder {
|
||||||
path: dir,
|
path: dir,
|
||||||
file_name: Some("desktop".into()),
|
file_name: Some("openchamber".into()),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,12 +842,16 @@ fn main() {
|
|||||||
.on_menu_event(|app, event| {
|
.on_menu_event(|app, event| {
|
||||||
#[cfg(target_os = "macos")]
|
#[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, ());
|
let _ = app.emit(CHECK_FOR_UPDATES_EVENT, ());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.id() == MENU_ITEM_REPORT_BUG_ID {
|
// External links
|
||||||
|
if event_id == MENU_ITEM_REPORT_BUG_ID {
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
{
|
{
|
||||||
@@ -660,7 +860,7 @@ fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.id() == MENU_ITEM_REQUEST_FEATURE_ID {
|
if event_id == MENU_ITEM_REQUEST_FEATURE_ID {
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
{
|
{
|
||||||
@@ -668,6 +868,85 @@ fn main() {
|
|||||||
}
|
}
|
||||||
return;
|
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| {
|
.on_window_event(|window, event| {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type LogResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const normalizePayload = (payload: LogResponse): { fileName: string; content: string } => ({
|
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 : '',
|
content: typeof payload.content === 'string' ? payload.content : '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates';
|
const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates';
|
||||||
|
const MENU_ACTION_EVENT = 'openchamber:menu-action';
|
||||||
|
|
||||||
const cleanupFunctions: Array<() => void | Promise<void>> = [];
|
const cleanupFunctions: Array<() => void | Promise<void>> = [];
|
||||||
|
|
||||||
@@ -62,6 +63,11 @@ try {
|
|||||||
});
|
});
|
||||||
cleanupFunctions.push(() => updateCheckUnlisten());
|
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 => {
|
requestInitialNotificationPermission().catch(err => {
|
||||||
console.error('[main] Failed to request notification permission:', err);
|
console.error('[main] Failed to request notification permission:', err);
|
||||||
});
|
});
|
||||||
@@ -117,14 +123,25 @@ if (homeDirectory) {
|
|||||||
window.opencodeDesktop = {
|
window.opencodeDesktop = {
|
||||||
homeDirectory,
|
homeDirectory,
|
||||||
async getServerInfo() {
|
async getServerInfo() {
|
||||||
const server = window.__OPENCHAMBER_DESKTOP_SERVER__;
|
try {
|
||||||
return {
|
const info = await invoke<ServerInfo>('desktop_server_info');
|
||||||
webPort: server?.origin ? parseInt(server.origin.split(':')[2] || '0', 10) : null,
|
return {
|
||||||
openCodePort: server?.opencodePort ?? null,
|
webPort: info.server_port,
|
||||||
host: '127.0.0.1',
|
openCodePort: info.opencode_port ?? null,
|
||||||
ready: true,
|
host: '127.0.0.1',
|
||||||
cliAvailable: server?.cliAvailable ?? false,
|
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> {
|
async getSettings(): Promise<DesktopSettings> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel';
|
|||||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||||
import { useEventStream } from '@/hooks/useEventStream';
|
import { useEventStream } from '@/hooks/useEventStream';
|
||||||
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
||||||
|
import { useMenuActions } from '@/hooks/useMenuActions';
|
||||||
import { useMessageSync } from '@/hooks/useMessageSync';
|
import { useMessageSync } from '@/hooks/useMessageSync';
|
||||||
import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap';
|
import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap';
|
||||||
import { GitPollingProvider } from '@/hooks/useGitPolling';
|
import { GitPollingProvider } from '@/hooks/useGitPolling';
|
||||||
@@ -130,6 +131,12 @@ function App({ apis }: AppProps) {
|
|||||||
|
|
||||||
useKeyboardShortcuts();
|
useKeyboardShortcuts();
|
||||||
|
|
||||||
|
const handleToggleMemoryDebug = React.useCallback(() => {
|
||||||
|
setShowMemoryDebug(prev => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useMenuActions(handleToggleMemoryDebug);
|
||||||
|
|
||||||
useMessageSync();
|
useMessageSync();
|
||||||
|
|
||||||
useSessionStatusBootstrap();
|
useSessionStatusBootstrap();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RiDownloadLine, RiSettings3Line } from '@remixicon/react';
|
import { RiDownloadLine, RiSettings3Line } from '@remixicon/react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
@@ -84,6 +85,10 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
|||||||
|
|
||||||
if (available || downloaded) {
|
if (available || downloaded) {
|
||||||
setUpdateDialogOpen(true);
|
setUpdateDialogOpen(true);
|
||||||
|
} else {
|
||||||
|
toast.success('No updates available', {
|
||||||
|
description: 'You are running the latest version.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
pendingMenuUpdateCheckRef.current = false;
|
pendingMenuUpdateCheckRef.current = false;
|
||||||
}, [available, downloaded, checking]);
|
}, [available, downloaded, checking]);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { useSessionStore, MEMORY_LIMITS } from '@/stores/useSessionStore';
|
import { useSessionStore, MEMORY_LIMITS } from '@/stores/useSessionStore';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||||||
import { RiCloseLine, RiDatabase2Line, RiDeleteBinLine, RiPulseLine } from '@remixicon/react';
|
import { RiCloseLine, RiDatabase2Line, RiDeleteBinLine, RiPulseLine } from '@remixicon/react';
|
||||||
import { useDesktopServerInfo } from '@/hooks/useDesktopServerInfo';
|
import { useDesktopServerInfo } from '@/hooks/useDesktopServerInfo';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
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">
|
<div className="flex gap-2 pt-2 border-t">
|
||||||
<Button
|
<Tooltip delayDuration={1000}>
|
||||||
size="sm"
|
<TooltipTrigger asChild>
|
||||||
variant="outline"
|
<Button
|
||||||
className="typography-meta"
|
size="sm"
|
||||||
onClick={() => {
|
variant="outline"
|
||||||
if (currentSessionId) {
|
className="typography-meta"
|
||||||
trimToViewportWindow(currentSessionId, 10);
|
onClick={() => {
|
||||||
}
|
if (currentSessionId) {
|
||||||
}}
|
trimToViewportWindow(currentSessionId, 10);
|
||||||
>
|
}
|
||||||
<RiDeleteBinLine className="h-3 w-3 mr-1" />
|
}}
|
||||||
Force Trim (10)
|
>
|
||||||
</Button>
|
<RiDeleteBinLine className="h-3 w-3 mr-1" />
|
||||||
<Button
|
Force Trim (10)
|
||||||
size="sm"
|
</Button>
|
||||||
variant="outline"
|
</TooltipTrigger>
|
||||||
className="typography-meta"
|
<TooltipContent side="top">
|
||||||
onClick={() => {
|
Trim current session to only 10 most recent messages
|
||||||
evictLeastRecentlyUsed();
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
}}
|
<Tooltip delayDuration={1000}>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
Evict LRU
|
<Button
|
||||||
</Button>
|
size="sm"
|
||||||
<Button
|
variant="outline"
|
||||||
size="sm"
|
className="typography-meta"
|
||||||
variant="outline"
|
onClick={() => {
|
||||||
className="typography-meta"
|
evictLeastRecentlyUsed();
|
||||||
onClick={() => {
|
}}
|
||||||
|
>
|
||||||
}}
|
Evict LRU
|
||||||
>
|
</Button>
|
||||||
Log State
|
</TooltipTrigger>
|
||||||
</Button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -57,11 +57,12 @@ export const useKeyboardShortcuts = () => {
|
|||||||
diagnostics
|
diagnostics
|
||||||
.downloadLogs()
|
.downloadLogs()
|
||||||
.then(({ fileName, content }) => {
|
.then(({ fileName, content }) => {
|
||||||
|
const finalFileName = fileName || 'openchamber.log';
|
||||||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const anchor = document.createElement('a');
|
const anchor = document.createElement('a');
|
||||||
anchor.href = url;
|
anchor.href = url;
|
||||||
anchor.download = fileName || 'desktop.log';
|
anchor.download = finalFileName;
|
||||||
document.body.appendChild(anchor);
|
document.body.appendChild(anchor);
|
||||||
anchor.click();
|
anchor.click();
|
||||||
document.body.removeChild(anchor);
|
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