From 301e0c3a33dda4cff85ff4f78edc0f79a18bc870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 13 Apr 2026 20:30:57 +0800 Subject: [PATCH] feat(desktop): route macOS native About menu to in-app Settings page Replace the default macOS about dialog with a custom menu that emits a Tauri event, navigating to the Settings > About tab within the React app. Co-Authored-By: Claude Opus 4.6 (1M context) --- desktop/src-tauri/src/lib.rs | 62 +++++++++++++++++++++- desktop/src/components/layout/AppShell.tsx | 21 +++++++- desktop/src/pages/Settings.tsx | 10 +++- desktop/src/stores/uiStore.ts | 6 +++ 4 files changed, 95 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index e774dabe..cf642a19 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -6,7 +6,10 @@ use std::{ time::{Duration, Instant}, }; -use tauri::{AppHandle, Manager, RunEvent, State}; +use tauri::{ + menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder}, + AppHandle, Emitter, Manager, RunEvent, State, +}; use tauri_plugin_shell::{ process::{CommandChild, CommandEvent}, ShellExt, @@ -302,6 +305,63 @@ pub fn run() { get_server_url, restart_adapters_sidecar ]) + .menu(|app| { + let about_item = MenuItemBuilder::with_id("nav_about", "About Claude Code Haha") + .build(app)?; + let settings_item = MenuItemBuilder::with_id("nav_settings", "Settings...") + .accelerator("CmdOrCtrl+,") + .build(app)?; + + let app_submenu = SubmenuBuilder::new(app, "Claude Code Haha") + .item(&about_item) + .separator() + .item(&settings_item) + .separator() + .services() + .separator() + .hide() + .hide_others() + .show_all() + .separator() + .quit() + .build()?; + + let edit_submenu = SubmenuBuilder::new(app, "Edit") + .undo() + .redo() + .separator() + .cut() + .copy() + .paste() + .select_all() + .build()?; + + let view_submenu = SubmenuBuilder::new(app, "View") + .fullscreen() + .build()?; + + let window_submenu = SubmenuBuilder::new(app, "Window") + .minimize() + .maximize() + .close_window() + .build()?; + + MenuBuilder::new(app) + .item(&app_submenu) + .item(&edit_submenu) + .item(&view_submenu) + .item(&window_submenu) + .build() + }) + .on_menu_event(|app, event| match event.id().as_ref() { + "nav_about" => { + let _ = app.emit("native-menu-navigate", "about"); + } + "nav_settings" => { + let _ = app.emit("native-menu-navigate", "settings"); + } + _ => {} + }) .setup(|app| { let state = app.state::(); let mut guard = state diff --git a/desktop/src/components/layout/AppShell.tsx b/desktop/src/components/layout/AppShell.tsx index 221d8029..eab928e0 100644 --- a/desktop/src/components/layout/AppShell.tsx +++ b/desktop/src/components/layout/AppShell.tsx @@ -4,10 +4,11 @@ import { ContentRouter } from './ContentRouter' import { ToastContainer } from '../shared/Toast' import { UpdateChecker } from '../shared/UpdateChecker' import { useSettingsStore } from '../../stores/settingsStore' +import { useUIStore, type SettingsTab } from '../../stores/uiStore' import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts' import { initializeDesktopServerUrl } from '../../lib/desktopRuntime' import { TabBar } from './TabBar' -import { useTabStore } from '../../stores/tabStore' +import { useTabStore, SETTINGS_TAB_ID } from '../../stores/tabStore' import { useChatStore } from '../../stores/chatStore' export function AppShell() { @@ -46,6 +47,24 @@ export function AppShell() { } }, [fetchSettings]) + // Listen for macOS native menu navigation events (About / Settings) + useEffect(() => { + let unlisten: (() => void) | undefined + import(/* @vite-ignore */ '@tauri-apps/api/event') + .then(({ listen }) => + listen('native-menu-navigate', (event) => { + const target = event.payload as SettingsTab | 'settings' + if (target === 'about') { + useUIStore.getState().setPendingSettingsTab('about') + } + useTabStore.getState().openTab(SETTINGS_TAB_ID, 'Settings', 'settings') + }), + ) + .then((fn) => { unlisten = fn }) + .catch(() => {}) + return () => { unlisten?.() } + }, []) + useKeyboardShortcuts() if (startupError) { diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index e37a6876..32344e98 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -18,13 +18,19 @@ import { MarkdownRenderer } from '../components/markdown/MarkdownRenderer' import { useSkillStore } from '../stores/skillStore' import { SkillList } from '../components/skills/SkillList' import { SkillDetail } from '../components/skills/SkillDetail' - -type SettingsTab = 'providers' | 'permissions' | 'general' | 'adapters' | 'agents' | 'skills' | 'about' +import { useUIStore, type SettingsTab } from '../stores/uiStore' export function Settings() { const [activeTab, setActiveTab] = useState('providers') + const pendingSettingsTab = useUIStore((s) => s.pendingSettingsTab) const t = useTranslation() + useEffect(() => { + if (!pendingSettingsTab) return + setActiveTab(pendingSettingsTab) + useUIStore.getState().setPendingSettingsTab(null) + }, [pendingSettingsTab]) + return (
diff --git a/desktop/src/stores/uiStore.ts b/desktop/src/stores/uiStore.ts index bfd43982..2682f50e 100644 --- a/desktop/src/stores/uiStore.ts +++ b/desktop/src/stores/uiStore.ts @@ -7,12 +7,15 @@ export type Toast = { duration?: number } +export type SettingsTab = 'providers' | 'permissions' | 'general' | 'adapters' | 'agents' | 'skills' | 'about' + type ActiveView = 'code' | 'scheduled' | 'terminal' | 'history' | 'settings' type UIStore = { theme: 'light' | 'dark' sidebarOpen: boolean activeView: ActiveView + pendingSettingsTab: SettingsTab | null activeModal: string | null toasts: Toast[] @@ -21,6 +24,7 @@ type UIStore = { toggleSidebar: () => void setSidebarOpen: (open: boolean) => void setActiveView: (view: ActiveView) => void + setPendingSettingsTab: (tab: SettingsTab | null) => void openModal: (id: string) => void closeModal: () => void addToast: (toast: Omit) => void @@ -33,6 +37,7 @@ export const useUIStore = create((set) => ({ theme: 'light', sidebarOpen: true, activeView: 'code', + pendingSettingsTab: null, activeModal: null, toasts: [], @@ -52,6 +57,7 @@ export const useUIStore = create((set) => ({ toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })), setSidebarOpen: (open) => set({ sidebarOpen: open }), setActiveView: (view) => set({ activeView: view }), + setPendingSettingsTab: (tab) => set({ pendingSettingsTab: tab }), openModal: (id) => set({ activeModal: id }), closeModal: () => set({ activeModal: null }),