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) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-04-13 20:30:57 +08:00
parent bd604429ec
commit 301e0c3a33
4 changed files with 95 additions and 4 deletions

View File

@ -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::<ServerState>();
let mut guard = state

View File

@ -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<string>('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) {

View File

@ -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<SettingsTab>('providers')
const pendingSettingsTab = useUIStore((s) => s.pendingSettingsTab)
const t = useTranslation()
useEffect(() => {
if (!pendingSettingsTab) return
setActiveTab(pendingSettingsTab)
useUIStore.getState().setPendingSettingsTab(null)
}, [pendingSettingsTab])
return (
<div className="flex-1 flex flex-col overflow-hidden bg-[var(--color-surface)]">
<div className="flex-1 flex overflow-hidden">

View File

@ -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<Toast, 'id'>) => void
@ -33,6 +37,7 @@ export const useUIStore = create<UIStore>((set) => ({
theme: 'light',
sidebarOpen: true,
activeView: 'code',
pendingSettingsTab: null,
activeModal: null,
toasts: [],
@ -52,6 +57,7 @@ export const useUIStore = create<UIStore>((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 }),