2025-12-19 17:57:50 +02:00
|
|
|
import React from 'react';
|
2026-01-19 02:50:40 +02:00
|
|
|
import { toast } from '@/components/ui';
|
2025-12-19 17:57:50 +02:00
|
|
|
import { useSessionStore } from '@/stores/useSessionStore';
|
|
|
|
|
import { useUIStore } from '@/stores/useUIStore';
|
2026-01-06 21:31:04 +02:00
|
|
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
2026-03-04 01:41:01 +02:00
|
|
|
import { useUpdateStore } from '@/stores/useUpdateStore';
|
2025-12-19 17:57:50 +02:00
|
|
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
|
|
|
|
import { sessionEvents } from '@/lib/sessionEvents';
|
2026-02-05 01:59:49 +02:00
|
|
|
import { isTauriShell } from '@/lib/desktop';
|
2026-01-06 21:31:04 +02:00
|
|
|
import { useFileSystemAccess } from '@/hooks/useFileSystemAccess';
|
2026-01-07 22:06:28 +02:00
|
|
|
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
|
2026-02-05 01:59:49 +02:00
|
|
|
import { showOpenCodeStatus } from '@/lib/openCodeStatus';
|
2025-12-19 17:57:50 +02:00
|
|
|
|
|
|
|
|
const MENU_ACTION_EVENT = 'openchamber:menu-action';
|
2026-02-05 01:59:49 +02:00
|
|
|
const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates';
|
|
|
|
|
|
|
|
|
|
type TauriEventApi = {
|
|
|
|
|
listen?: (
|
|
|
|
|
event: string,
|
|
|
|
|
handler: (evt: { payload?: unknown }) => void
|
|
|
|
|
) => Promise<() => void>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type TauriGlobal = {
|
|
|
|
|
event?: TauriEventApi;
|
|
|
|
|
};
|
2025-12-19 17:57:50 +02:00
|
|
|
|
|
|
|
|
type MenuAction =
|
2025-12-19 18:59:08 +02:00
|
|
|
| 'about'
|
2025-12-19 17:57:50 +02:00
|
|
|
| 'settings'
|
|
|
|
|
| 'command-palette'
|
|
|
|
|
| 'new-session'
|
2026-01-07 22:06:28 +02:00
|
|
|
| 'new-worktree-session'
|
2025-12-19 17:57:50 +02:00
|
|
|
| 'change-workspace'
|
|
|
|
|
| 'open-git-tab'
|
|
|
|
|
| 'open-diff-tab'
|
2026-02-05 01:59:49 +02:00
|
|
|
| 'open-files-tab'
|
2025-12-19 17:57:50 +02:00
|
|
|
| 'open-terminal-tab'
|
2026-02-20 13:23:01 +02:00
|
|
|
| 'copy'
|
2025-12-19 17:57:50 +02:00
|
|
|
| 'theme-light'
|
|
|
|
|
| 'theme-dark'
|
|
|
|
|
| 'theme-system'
|
|
|
|
|
| 'toggle-sidebar'
|
|
|
|
|
| 'toggle-memory-debug'
|
|
|
|
|
| 'help-dialog'
|
|
|
|
|
| 'download-logs';
|
|
|
|
|
|
|
|
|
|
export const useMenuActions = (
|
|
|
|
|
onToggleMemoryDebug?: () => void
|
|
|
|
|
) => {
|
2025-12-21 20:18:51 +02:00
|
|
|
const { openNewSessionDraft } = useSessionStore();
|
2025-12-19 17:57:50 +02:00
|
|
|
const {
|
|
|
|
|
toggleCommandPalette,
|
|
|
|
|
toggleHelpDialog,
|
|
|
|
|
toggleSidebar,
|
2025-12-21 20:18:51 +02:00
|
|
|
setSessionSwitcherOpen,
|
2025-12-19 17:57:50 +02:00
|
|
|
setActiveMainTab,
|
|
|
|
|
setSettingsDialogOpen,
|
2025-12-19 18:59:08 +02:00
|
|
|
setAboutDialogOpen,
|
2025-12-19 17:57:50 +02:00
|
|
|
} = useUIStore();
|
2026-01-06 21:31:04 +02:00
|
|
|
const { addProject } = useProjectsStore();
|
2026-03-04 01:41:01 +02:00
|
|
|
const checkForUpdates = useUpdateStore((state) => state.checkForUpdates);
|
2026-01-06 21:31:04 +02:00
|
|
|
const { requestAccess, startAccessing } = useFileSystemAccess();
|
2025-12-19 17:57:50 +02:00
|
|
|
const { setThemeMode } = useThemeSystem();
|
2026-03-04 01:41:01 +02:00
|
|
|
const checkUpdatesInFlightRef = React.useRef(false);
|
|
|
|
|
|
|
|
|
|
const handleCheckForUpdates = React.useCallback(() => {
|
|
|
|
|
if (checkUpdatesInFlightRef.current) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
checkUpdatesInFlightRef.current = true;
|
|
|
|
|
|
|
|
|
|
void checkForUpdates()
|
|
|
|
|
.then(() => {
|
|
|
|
|
const { available, error } = useUpdateStore.getState();
|
|
|
|
|
if (error) {
|
|
|
|
|
toast.error('Failed to check for updates', {
|
|
|
|
|
description: error,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!available) {
|
|
|
|
|
toast.success('You are on the latest version');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
checkUpdatesInFlightRef.current = false;
|
|
|
|
|
});
|
|
|
|
|
}, [checkForUpdates]);
|
2025-12-19 17:57:50 +02:00
|
|
|
|
|
|
|
|
const handleChangeWorkspace = React.useCallback(() => {
|
2026-02-05 01:59:49 +02:00
|
|
|
if (isTauriShell()) {
|
2026-01-06 21:31:04 +02:00
|
|
|
requestAccess('')
|
|
|
|
|
.then(async (result) => {
|
|
|
|
|
if (!result.success || !result.path) {
|
|
|
|
|
if (result.error && result.error !== 'Directory selection cancelled') {
|
|
|
|
|
toast.error('Failed to select directory', {
|
|
|
|
|
description: result.error,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const accessResult = await startAccessing(result.path);
|
|
|
|
|
if (!accessResult.success) {
|
|
|
|
|
toast.error('Failed to open directory', {
|
|
|
|
|
description: accessResult.error || 'Desktop could not grant file access.',
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const added = addProject(result.path, { id: result.projectId });
|
|
|
|
|
if (!added) {
|
|
|
|
|
toast.error('Failed to add project', {
|
|
|
|
|
description: 'Please select a valid directory path.',
|
2025-12-19 17:57:50 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error('Desktop: Error selecting directory:', error);
|
|
|
|
|
toast.error('Failed to select directory');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 01:59:49 +02:00
|
|
|
sessionEvents.requestDirectoryDialog();
|
|
|
|
|
}, [addProject, requestAccess, startAccessing]);
|
2025-12-19 17:57:50 +02:00
|
|
|
|
2026-02-05 01:59:49 +02:00
|
|
|
const handleAction = React.useCallback(
|
|
|
|
|
(action: MenuAction) => {
|
2025-12-19 17:57:50 +02:00
|
|
|
switch (action) {
|
2025-12-19 18:59:08 +02:00
|
|
|
case 'about':
|
|
|
|
|
setAboutDialogOpen(true);
|
|
|
|
|
break;
|
|
|
|
|
|
2025-12-19 17:57:50 +02:00
|
|
|
case 'settings':
|
|
|
|
|
setSettingsDialogOpen(true);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'command-palette':
|
|
|
|
|
toggleCommandPalette();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'new-session':
|
2025-12-21 20:18:51 +02:00
|
|
|
setActiveMainTab('chat');
|
|
|
|
|
setSessionSwitcherOpen(false);
|
|
|
|
|
openNewSessionDraft();
|
2025-12-19 17:57:50 +02:00
|
|
|
break;
|
|
|
|
|
|
2026-01-07 22:06:28 +02:00
|
|
|
case 'new-worktree-session':
|
|
|
|
|
setActiveMainTab('chat');
|
|
|
|
|
setSessionSwitcherOpen(false);
|
|
|
|
|
createWorktreeSession();
|
2025-12-19 17:57:50 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 01:59:49 +02:00
|
|
|
case 'open-files-tab': {
|
|
|
|
|
const { activeMainTab } = useUIStore.getState();
|
|
|
|
|
setActiveMainTab(activeMainTab === 'files' ? 'chat' : 'files');
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 17:57:50 +02:00
|
|
|
case 'open-terminal-tab': {
|
|
|
|
|
const { activeMainTab } = useUIStore.getState();
|
|
|
|
|
setActiveMainTab(activeMainTab === 'terminal' ? 'chat' : 'terminal');
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 13:23:01 +02:00
|
|
|
case 'copy': {
|
|
|
|
|
const copyEvent = new Event('openchamber:copy', { cancelable: true });
|
|
|
|
|
const wasHandled = !window.dispatchEvent(copyEvent);
|
|
|
|
|
if (!wasHandled) {
|
|
|
|
|
document.execCommand('copy');
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 17:57:50 +02:00
|
|
|
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': {
|
2026-02-05 01:59:49 +02:00
|
|
|
void showOpenCodeStatus().catch(() => {
|
|
|
|
|
toast.error('Failed to collect OpenCode status');
|
|
|
|
|
});
|
2025-12-19 17:57:50 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 01:59:49 +02:00
|
|
|
},
|
|
|
|
|
[
|
|
|
|
|
handleChangeWorkspace,
|
|
|
|
|
onToggleMemoryDebug,
|
|
|
|
|
openNewSessionDraft,
|
|
|
|
|
setAboutDialogOpen,
|
|
|
|
|
setActiveMainTab,
|
|
|
|
|
setSessionSwitcherOpen,
|
|
|
|
|
setSettingsDialogOpen,
|
|
|
|
|
setThemeMode,
|
|
|
|
|
toggleCommandPalette,
|
|
|
|
|
toggleHelpDialog,
|
|
|
|
|
toggleSidebar,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const handleMenuAction = (event: Event) => {
|
|
|
|
|
const action = (event as CustomEvent<MenuAction>).detail;
|
|
|
|
|
if (!action) return;
|
|
|
|
|
handleAction(action);
|
2025-12-19 17:57:50 +02:00
|
|
|
};
|
|
|
|
|
|
2026-03-04 01:41:01 +02:00
|
|
|
const handleCheckForUpdatesEvent = () => {
|
|
|
|
|
handleCheckForUpdates();
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-19 17:57:50 +02:00
|
|
|
window.addEventListener(MENU_ACTION_EVENT, handleMenuAction);
|
2026-03-04 01:41:01 +02:00
|
|
|
window.addEventListener(CHECK_FOR_UPDATES_EVENT, handleCheckForUpdatesEvent);
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener(MENU_ACTION_EVENT, handleMenuAction);
|
|
|
|
|
window.removeEventListener(CHECK_FOR_UPDATES_EVENT, handleCheckForUpdatesEvent);
|
|
|
|
|
};
|
|
|
|
|
}, [handleAction, handleCheckForUpdates]);
|
2026-02-05 01:59:49 +02:00
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (typeof window === 'undefined') return;
|
|
|
|
|
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
|
|
|
|
const listen = tauri?.event?.listen;
|
|
|
|
|
if (typeof listen !== 'function') return;
|
|
|
|
|
|
|
|
|
|
let unlistenMenu: null | (() => void | Promise<void>) = null;
|
|
|
|
|
let unlistenUpdate: null | (() => void | Promise<void>) = null;
|
|
|
|
|
|
|
|
|
|
listen('openchamber:menu-action', (evt) => {
|
|
|
|
|
const action = evt?.payload;
|
|
|
|
|
if (typeof action !== 'string') return;
|
|
|
|
|
handleAction(action as MenuAction);
|
|
|
|
|
})
|
|
|
|
|
.then((fn) => {
|
|
|
|
|
unlistenMenu = fn;
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
// ignore
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
listen('openchamber:check-for-updates', () => {
|
|
|
|
|
window.dispatchEvent(new Event(CHECK_FOR_UPDATES_EVENT));
|
|
|
|
|
})
|
|
|
|
|
.then((fn) => {
|
|
|
|
|
unlistenUpdate = fn;
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
// ignore
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
const cleanup = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const a = unlistenMenu?.();
|
|
|
|
|
if (a instanceof Promise) await a;
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const b = unlistenUpdate?.();
|
|
|
|
|
if (b instanceof Promise) await b;
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
void cleanup();
|
|
|
|
|
};
|
|
|
|
|
}, [handleAction]);
|
2025-12-19 17:57:50 +02:00
|
|
|
};
|