返回 DeepSeek-Reasonix
usePaletteCommands.tsx
根目录 / desktop / frontend / src / app-runtime / usePaletteCommands.tsx
1 import { useMemo } from "react";
2 import { app } from "../lib/bridge";
3 import { useCommittedCommand } from "../lib/useCommittedCommand";
4 import { useGlobalShortcut } from "../lib/keyboardShortcuts";
5 import { clearThemePack } from "../lib/themePack";
6 import { paletteSessionDisplayTitle, paletteSessionHint, paletteSessionKeywords, sessionActivityTime } from "../lib/session";
7 import { useOverlayStore } from "../store/overlays";
8 import { useAppNavigationStore } from "../store/appNavigation";
9 import { useRemoteStore } from "../store/remote";
10 import { activeTabMirror } from "./activeTabMirror";
11 import type { RemoteHostView, SessionMeta } from "../lib/types";
12 import type { Translator } from "../lib/i18n";
13 import type { PaletteItem } from "../components/CommandPalette";
14
15 export type PaletteCommandsInput = {
16 managementActive: boolean;
17 activeTabId: string | undefined;
18 remoteSurfaceActive: boolean;
19 t: Translator;
20 notice(message: string, kind?: "info" | "warn" | "error"): void;
21 showToast(message: string, level: "info" | "warn" | "error", options?: { durationMs?: number }): void;
22 ports: {
23 handleNewTab(): void;
24 listSessions(): Promise<SessionMeta[]>;
25 openTrash(): void;
26 onResumeSession(session: SessionMeta): Promise<void>;
27 openRemoteWorkspaceFromStatus(host: RemoteHostView): void;
28 connectAndOpenRemoteWorkspace(host: RemoteHostView): void;
29 toggleTerminalPanel(): void;
30 setTasksOpen(open: false | "session" | "all"): void;
31 handleTabClose(id: string): void;
32 toggleSidebar(): void;
33 returnToWorkspace(): void;
34 };
35 };
36
37 /**
38 * Owns the command palette: its open action (snapshotting sessions and
39 * extension actions), its items and the global command shortcuts that open it
40 * or the new-session/settings/tab-close/shortcuts/sidebar surfaces. Session,
41 * extension, remote-host and navigation targets come from their stores.
42 */
43 export function usePaletteCommands(input: PaletteCommandsInput) {
44 const { managementActive, activeTabId, remoteSurfaceActive, t, notice, showToast, ports } = input;
45 const setPaletteOpen = useOverlayStore((state) => state.setPaletteOpen);
46 const paletteSessions = useOverlayStore((state) => state.paletteSessions);
47 const setPaletteSessions = useOverlayStore((state) => state.setPaletteSessions);
48 const paletteExtensionActions = useOverlayStore((state) => state.paletteExtensionActions);
49 const setPaletteExtensionActions = useOverlayStore((state) => state.setPaletteExtensionActions);
50 const setShortcutsOpen = useOverlayStore((state) => state.setShortcutsOpen);
51 const setTransientOverlayDismissSignal = useOverlayStore((state) => state.setTransientOverlayDismissSignal);
52 const remoteHosts = useRemoteStore((state) => state.hosts);
53 const remoteStatuses = useRemoteStore((state) => state.statuses);
54
55 const closeTransientOverlays = useCommittedCommand(() => {
56 setTransientOverlayDismissSignal((signal) => signal + 1);
57 });
58
59 const openPalette = useCommittedCommand(async () => {
60 closeTransientOverlays();
61 setPaletteOpen(true);
62 setPaletteSessions(await ports.listSessions().catch(() => []));
63 setPaletteExtensionActions(await app.ExtensionActions(activeTabMirror().current ?? "").catch(() => []));
64 });
65
66 useGlobalShortcut("commandPalette.open", () => {
67 setPaletteOpen((current) => {
68 if (!current) void openPalette();
69 return !current; // toggle the state so the palette actually opens/closes
70 });
71 }, [openPalette]);
72 useGlobalShortcut("app.newSession", () => void ports.handleNewTab(), [ports.handleNewTab]);
73 useGlobalShortcut("settings.open", () => {
74 closeTransientOverlays();
75 useAppNavigationStore.getState().setSettingsTarget(useAppNavigationStore.getState().lastSettingsTarget);
76 }, [closeTransientOverlays]);
77 useGlobalShortcut("tab.close", () => {
78 if (managementActive) ports.returnToWorkspace();
79 else if (activeTabId) void ports.handleTabClose(activeTabId);
80 }, [activeTabId, managementActive, ports.handleTabClose, ports.returnToWorkspace], managementActive || Boolean(activeTabId));
81 useGlobalShortcut("shortcuts.show", () => setShortcutsOpen(true));
82 useGlobalShortcut("sidebar.toggle", ports.toggleSidebar, [ports.toggleSidebar], !managementActive);
83
84 const paletteItems = useMemo<PaletteItem[]>(() => {
85 const navigation = useAppNavigationStore.getState();
86 const cmds: PaletteItem[] = [
87 { id: "cmd-new", group: t("palette.group.commands"), title: t("palette.cmd.newSession"), icon: "new", compact: true, keywords: ["new", "新建"], run: () => void ports.handleNewTab() },
88 { id: "cmd-automation", group: t("palette.group.commands"), title: t("sidebar.automation"), icon: "automation", compact: true, keywords: ["automation", "自动化"], run: () => navigation.openPage({ kind: "automation" }) },
89 { id: "cmd-trash", group: t("palette.group.commands"), title: t("palette.cmd.trash"), icon: "trash", compact: true, keywords: ["trash", "回收站"], run: () => void ports.openTrash() },
90 { id: "cmd-settings", group: t("palette.group.commands"), title: t("palette.cmd.settings"), icon: "settings", compact: true, keywords: ["settings", "设置"], run: () => navigation.setSettingsTarget(navigation.lastSettingsTarget) },
91 { id: "cmd-appearance", group: t("palette.group.commands"), title: t("palette.cmd.appearance"), icon: "appearance", compact: true, keywords: ["theme", "appearance", "外观", "主题"], run: () => navigation.setSettingsTarget("appearance") },
92 {
93 id: "cmd-theme-reset",
94 group: t("palette.group.commands"),
95 title: t("settings.themeLibrary.reset"),
96 icon: "appearance",
97 compact: true,
98 keywords: ["theme", "reset", "default", "恢复默认", "主题"],
99 run: () => {
100 void app.ResetThemePack()
101 .then(() => {
102 clearThemePack();
103 notice(t("settings.themeReset"));
104 })
105 .catch((err) => showToast(err instanceof Error ? err.message : String(err), "error"));
106 },
107 },
108 { id: "cmd-memory", group: t("palette.group.commands"), title: t("palette.cmd.memory"), icon: "memory", compact: true, keywords: ["memory", "记忆"], run: () => navigation.setSettingsTarget("memory") },
109 { id: "cmd-models", group: t("palette.group.commands"), title: t("palette.cmd.models"), icon: "models", compact: true, keywords: ["model", "模型"], run: () => navigation.setSettingsTarget("models") },
110 {
111 id: "cmd-usage-stats",
112 group: t("palette.group.commands"),
113 title: t("palette.cmd.usageStats"),
114 icon: "usage",
115 compact: true,
116 keywords: ["usage", "stats", "statistics", "用量", "统计"],
117 run: () => {
118 navigation.setSettingsFocus((current) => ({
119 target: "model-stats",
120 requestId: (current?.requestId ?? 0) + 1,
121 }));
122 navigation.setSettingsTarget("models");
123 },
124 },
125 { id: "cmd-task-center", group: t("palette.group.commands"), title: t("palette.cmd.taskCenter"), icon: "tasks", compact: true, keywords: ["task", "tasks", "center", "任务", "任务中心"], run: () => ports.setTasksOpen("all") },
126 { id: "cmd-terminal", group: t("palette.group.commands"), title: t("rightDock.terminal"), icon: "terminal", compact: true, keywords: ["terminal", "shell", "终端"], run: () => ports.toggleTerminalPanel() },
127 {
128 id: "cmd-reload-runtime",
129 group: t("palette.group.commands"),
130 title: t("palette.cmd.reloadRuntime"),
131 icon: "reload",
132 compact: true,
133 keywords: ["reload", "runtime", "重载", "运行时"],
134 run: () => {
135 const tabID = activeTabId;
136 if (!tabID) return;
137 // Success/queued feedback arrives as a tab notice; only hard failures need a toast.
138 void app.ReloadRuntime(tabID).catch((err) => showToast(err instanceof Error ? err.message : String(err), "error"));
139 },
140 },
141 ];
142 const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
143 const dayLabel = (ms: number) => {
144 const days = Math.round((startOfDay(new Date()) - startOfDay(new Date(ms))) / 86_400_000);
145 if (days <= 0) return t("history.today");
146 if (days === 1) return t("history.yesterday");
147 return new Date(ms).toLocaleDateString();
148 };
149 const sessionItems: PaletteItem[] = paletteSessions.slice(0, 12).map((s) => ({
150 id: `sess-${s.path}`,
151 group: t("palette.group.sessions"),
152 title: paletteSessionDisplayTitle(s, t("history.emptySession")),
153 hint: paletteSessionHint(s),
154 keywords: paletteSessionKeywords(s),
155 meta: dayLabel(sessionActivityTime(s)),
156 badge: t(s.turns === 1 ? "history.turnOne" : "history.turnOther", { n: s.turns }),
157 run: () => void ports.onResumeSession(s),
158 }));
159 const remoteItems: PaletteItem[] = remoteHosts.map((host) => {
160 const status = remoteStatuses[host.id];
161 const connected = status?.state === "connected" || status?.state === "degraded";
162 const target = `${host.user ? `${host.user}@` : ""}${host.host}${host.port && host.port !== 22 ? `:${host.port}` : ""}`;
163 return {
164 id: `remote-${host.id}`,
165 group: t("palette.group.remote"),
166 title: connected
167 ? t("palette.remote.open", { host: host.label })
168 : t("palette.remote.connect", { host: host.label }),
169 hint: host.defaultWorkspace || target,
170 icon: "remote",
171 keywords: ["ssh", "remote", "远程", "连接", host.label, host.host],
172 run: () => {
173 if (connected) ports.openRemoteWorkspaceFromStatus(host);
174 else ports.connectAndOpenRemoteWorkspace(host);
175 },
176 };
177 });
178 const extensionItems: PaletteItem[] = paletteExtensionActions.map((action) => ({
179 id: `ext-${action.slash}`,
180 group: t("palette.group.extensions"),
181 title: action.description || action.slash,
182 hint: action.slash,
183 icon: "extension",
184 keywords: ["extension", "扩展", action.plugin, action.action, action.slash],
185 run: () => {
186 const tabID = activeTabId;
187 if (!tabID) return;
188 // The extension's result message is user-facing feedback; only hard
189 // failures need an error toast.
190 void app.InvokeExtensionAction(tabID, action.slash, {})
191 .then((message) => {
192 if (message) showToast(message, "info");
193 })
194 .catch((err) => showToast(err instanceof Error ? err.message : String(err), "error"));
195 },
196 }));
197 return [...(remoteSurfaceActive ? cmds.filter((item) => item.id !== "cmd-terminal" && item.id !== "cmd-reload-runtime") : cmds), ...extensionItems, ...remoteItems, ...sessionItems];
198 }, [t, paletteSessions, paletteExtensionActions, remoteHosts, remoteStatuses, activeTabId, remoteSurfaceActive, ports, showToast, notice]);
199
200 return { openPalette, paletteItems };
201 }
202
202 lines Plain Text