返回 DeepSeek-Reasonix
useComposerRouter.ts
根目录 / desktop / frontend / src / app-runtime / useComposerRouter.ts
1 import { app } from "../lib/bridge";
2 import { desktopHost } from "../lib/desktopHost";
3 import { useCommittedCommand } from "../lib/useCommittedCommand";
4 import { clearThemePack } from "../lib/themePack";
5 import { applyTheme, getTheme, getThemeStyle, isThemeStyle } from "../lib/theme";
6 import { decisionSurfaceMockFromInput } from "../lib/decisionSurfaceMock";
7 import { activeTabMirror } from "./activeTabMirror";
8 import type { SettingsTab } from "../lib/types";
9 import type { StructuredInvocationSubmit } from "../lib/invocationDisplay";
10 import type { Translator } from "../lib/i18n";
11
12 type MockWorkView = {
13 running: true;
14 pendingPrompt: false;
15 cancellable: true;
16 jobs: { id: string; kind: string; label: string; status: string; startedAt: number }[];
17 };
18
19 export type ComposerRouterInput = {
20 activeTabId: string | undefined;
21 goalDraftActive: boolean;
22 t: Translator;
23 notice(message: string, kind?: "info" | "warn" | "error"): void;
24 showToast(message: string, level: "info" | "warn" | "error", options?: { durationMs?: number }): void;
25 ports: {
26 runShellForTab(tabId: string, cmd: string): Promise<void>;
27 switchModel(name: string, tabId: string): Promise<unknown>;
28 newSession(): Promise<unknown>;
29 setSettingsTarget(tab: SettingsTab): void;
30 setClearContextPending(pending: boolean): void;
31 clearWorkspaceConflict(): void;
32 setWorkspaceConflict(value: { state: "local"; ownerTabId: string; ownerTitle: string; ownerWork: MockWorkView; canReveal: true; canCreateWorktree: true } | null): void;
33 setPendingClose(value: { tabId: string; work: MockWorkView; stopping: boolean } | null): void;
34 submitComposerTurn(tabId: string, display: string, submit?: string, structured?: StructuredInvocationSubmit): Promise<void>;
35 steerForTab(tabId: string, text: string): Promise<void>;
36 isRemoteTab(tabId: string): boolean;
37 };
38 };
39
40 function isThemeMode(value: string): value is "auto" | "light" | "dark" {
41 return value === "auto" || value === "light" || value === "dark";
42 }
43
44 /**
45 * Routes a composer submit to its desktop-native action: shell commands,
46 * model/memory/clear/new commands, the browser decision-surface mock seeds,
47 * Goal activation or ordinary submission, theme commands and remote steer.
48 * Only the routes that need a desktop-native UI action are reserved here.
49 */
50 export function useComposerRouter(input: ComposerRouterInput) {
51 const { activeTabId, goalDraftActive, t, notice, showToast, ports } = input;
52
53 const handleSend = useCommittedCommand(async (displayText: string, submitText = displayText, requestedTabId = activeTabId, structured?: StructuredInvocationSubmit) => {
54 const sourceTabId = requestedTabId || activeTabId;
55 if (!sourceTabId) throw new Error(t("composer.workspaceStarting"));
56 const trimmed = displayText.trim();
57 // "!<cmd>" runs a shell command directly, bypassing the model.
58 if (trimmed.startsWith("!")) {
59 const cmd = trimmed.slice(1).trim();
60 if (!cmd) {
61 notice("usage: !<command> (e.g. !ls -la)");
62 return;
63 }
64 await ports.runShellForTab(sourceTabId, cmd);
65 return;
66 }
67 const model = /^\/model\s+(\S+)$/.exec(trimmed);
68 if (model) {
69 await ports.switchModel(model[1], sourceTabId);
70 return;
71 }
72 if (trimmed === "/memory") {
73 if (activeTabMirror().current !== sourceTabId) return;
74 ports.setSettingsTarget("memory");
75 return;
76 }
77 if (trimmed === "/clear") {
78 if (activeTabMirror().current !== sourceTabId) return;
79 ports.setClearContextPending(true);
80 return;
81 }
82 if (trimmed === "/new") {
83 if (activeTabMirror().current !== sourceTabId) return;
84 await ports.newSession();
85 return;
86 }
87 const decisionMock = typeof window !== "undefined" && desktopHost().kind === "none"
88 ? decisionSurfaceMockFromInput(trimmed)
89 : null;
90 if (decisionMock === "workspace_conflict" || decisionMock === "mode_jobs" || decisionMock === "close_active" || decisionMock === "clear_context") {
91 if (activeTabMirror().current !== sourceTabId) return;
92 ports.clearWorkspaceConflict();
93 ports.setPendingClose(null);
94 ports.setClearContextPending(false);
95 const mockWork: MockWorkView = {
96 running: true,
97 pendingPrompt: false,
98 cancellable: true,
99 jobs: [
100 { id: "mock-decision-build", kind: "bash", label: "pnpm build", status: "running", startedAt: Date.now() - 42_000 },
101 { id: "mock-decision-test", kind: "bash", label: "go test ./...", status: "running", startedAt: Date.now() - 18_000 },
102 ],
103 };
104 if (decisionMock === "workspace_conflict") {
105 ports.setWorkspaceConflict({
106 state: "local",
107 ownerTabId: "mock-workspace-writer",
108 ownerTitle: t("mock.topicDevStandard"),
109 ownerWork: mockWork,
110 canReveal: true,
111 canCreateWorktree: true,
112 });
113 } else if (decisionMock === "close_active") {
114 ports.setPendingClose({ tabId: sourceTabId, work: mockWork, stopping: false });
115 } else {
116 ports.setClearContextPending(true);
117 }
118 return;
119 }
120 if (goalDraftActive) {
121 await ports.submitComposerTurn(sourceTabId, displayText, submitText, structured);
122 return;
123 }
124 const theme = /^\/theme(?:\s+(\S+))?$/.exec(trimmed);
125 if (theme) {
126 const arg = theme[1]?.toLowerCase();
127 if (!arg) {
128 const cur = getTheme();
129 notice(t("settings.themeCurrent", { theme: cur, style: getThemeStyle(cur) }));
130 return;
131 }
132 if (arg === "reset" || arg === "default" || arg === "clear") {
133 try {
134 await app.ResetThemePack();
135 clearThemePack();
136 notice(t("settings.themeReset"));
137 } catch (err) {
138 showToast(err instanceof Error ? err.message : String(err), "error");
139 }
140 return;
141 }
142 if (isThemeMode(arg)) {
143 const next = arg;
144 const style = getThemeStyle(next);
145 try {
146 await app.SetDesktopAppearance(next, style);
147 applyTheme(next, style);
148 notice(t("settings.themeChanged", { theme: next, style }));
149 } catch (err) {
150 showToast(err instanceof Error ? err.message : String(err), "error");
151 }
152 return;
153 }
154 if (isThemeStyle(arg)) {
155 const cur = getTheme();
156 try {
157 await app.SetDesktopAppearance(cur, arg);
158 applyTheme(cur, arg);
159 notice(t("settings.themeChanged", { theme: cur, style: arg }));
160 } catch (err) {
161 showToast(err instanceof Error ? err.message : String(err), "error");
162 }
163 return;
164 }
165 notice(t("settings.themeUnknown", { name: arg }), "warn");
166 return;
167 }
168 await ports.submitComposerTurn(sourceTabId, displayText, submitText, structured);
169 });
170
171 const handleSteer = useCommittedCommand(async (text: string, requestedTabId = activeTabId) => {
172 const sourceTabId = requestedTabId || activeTabId;
173 if (!sourceTabId) throw new Error(t("composer.workspaceStarting"));
174 if (ports.isRemoteTab(sourceTabId)) {
175 await app.SteerRemoteTab(sourceTabId, text.trim());
176 return;
177 }
178 await ports.steerForTab(sourceTabId, text.trim());
179 });
180
181 return { handleSend, handleSteer };
182 }
183
183 lines TYPESCRIPT