返回 DeepSeek-Reasonix
useRemoteComposerIntegration.ts
根目录 / desktop / frontend / src / lib / useRemoteComposerIntegration.ts
1 import { useEffect, useRef, type Dispatch, type SetStateAction } from "react";
2 import { useCommittedCommand } from "./useCommittedCommand";
3 import { executeRemoteSend, executeComposerRuntime } from "../app-runtime/remoteComposerOwner";
4 import type { SessionResource, useSessionOperations } from "../app-runtime/useSessionOperations";
5 import type { RemoteNavigationCommand } from "./remoteNavigationCommands";
6 import { reconcileComposerProfile, type ComposerProfile, type ComposerProfilesByTab } from "./composerProfile";
7 import type { GoalAction } from "./goalAction";
8 import type { CollaborationMode, QualityFloor, RemoteTabRefView, ToolApprovalMode } from "./types";
9 import type { RemoteSessionApi } from "./useRemoteSession";
10
11 type RemoteProfile = RemoteSessionApi["composerProfile"];
12
13 export function remoteRuntimeCommand(input: string):
14 | { method: "setModel" | "setEffort"; value: string }
15 | { method: "newSession" | "clearSession" }
16 | { method: "compact"; value: string }
17 | { method: "runManagementCommand"; rehydrate?: boolean }
18 | undefined {
19 const trimmed = input.trim();
20 if (trimmed === "/new") return { method: "newSession" };
21 if (trimmed === "/clear") return { method: "clearSession" };
22 const match = /^\/(model|effort)\s+(\S+)$/.exec(trimmed);
23 if (match) return { method: match[1] === "model" ? "setModel" : "setEffort", value: match[2] };
24 const verb = /^\/([^\s]+)/.exec(trimmed)?.[1]?.toLowerCase();
25 if (verb === "compact") return { method: "compact", value: trimmed.slice("/compact".length).trim() };
26 if (verb === "goal" && remoteGoalCommandStartsTurn(trimmed)) return undefined;
27 // These controller verbs are synchronous management operations: they emit
28 // notices or mutate session metadata but do not admit a conversational
29 // turn. Custom commands, skills, docs queries, and MCP prompts deliberately
30 // remain on the ordinary submit path because they can start model work.
31 const management = new Set([
32 "context", "goal", "memory", "remember", "migrate", "migration",
33 "skill", "skills", "plugin", "plugins", "reload-cmd", "hooks", "mcp",
34 "provider", "tree", "branch", "switch", "rewind",
35 ]);
36 if (!verb || !management.has(verb)) return undefined;
37 return { method: "runManagementCommand", rehydrate: verb === "branch" || verb === "switch" || verb === "rewind" };
38 }
39
40 function remoteGoalCommandStartsTurn(input: string): boolean {
41 const args = input.trim().slice("/goal".length).trim().split(/\s+/).filter(Boolean);
42 const flags = new Set(["--strict", "--research", "--auto-research", "--deep", "--simple", "--no-research"]);
43 while (args.length > 0 && flags.has(args[0].toLowerCase())) args.shift();
44 if (args.length === 0) return false;
45 const action = args.join(" ").toLowerCase();
46 return !new Set(["status", "clear", "off", "stop", "done", "pause", "resume"]).has(action);
47 }
48
49 export function useRemoteComposerSend(
50 activeRemote: RemoteTabRefView | undefined,
51 activeTabId: string | undefined,
52 collaborationMode: CollaborationMode,
53 goal: string,
54 session: RemoteSessionApi,
55 send: (displayText: string, submitText?: string) => Promise<void>,
56 applyGoal: (tabId: string, goal: string) => Promise<unknown>,
57 requestClear: () => void,
58 ownership: { target: SessionResource; operations: ReturnType<typeof useSessionOperations>; navigateRemote: RemoteNavigationCommand },
59 ) {
60 const ports = { compact: session.compact, runManagementCommand: session.runManagementCommand,
61 setModel: session.setModel, setEffort: session.setEffort,
62 send, applyGoal, requestClear, newSession: ownership.navigateRemote };
63 return useCommittedCommand(async (displayText: string, submitText = displayText): Promise<void> => {
64 const trimmed = (submitText || displayText).trim();
65 const outcome = await ownership.operations(ownership.target, "send", {
66 tabId: activeTabId ?? "", remote: activeRemote, display: displayText, submit: submitText, commandText: trimmed,
67 command: remoteRuntimeCommand(trimmed), activateGoal: collaborationMode === "goal" && !goal.trim() && Boolean(trimmed), ports,
68 }, executeRemoteSend);
69 if (outcome.status === "failed") throw outcome.error;
70 });
71 }
72
73 export function useRemoteComposerProfileSync(options: {
74 activeTabId?: string;
75 sessionRoute?: string;
76 remote: boolean;
77 remoteProfile: RemoteProfile;
78 collaborationMode: CollaborationMode;
79 toolApprovalMode: ToolApprovalMode;
80 goal: string;
81 qualityFloor: QualityFloor;
82 pending: ComposerProfile["pending"];
83 setProfiles: Dispatch<SetStateAction<ComposerProfilesByTab>>;
84 }): boolean {
85 const { activeTabId, sessionRoute, remote, remoteProfile, collaborationMode, toolApprovalMode, goal, qualityFloor, pending, setProfiles } = options;
86 // The last synced session route per tab. A tab switch A -> B -> A is not a
87 // route change for A; only A's own session rotation is, so the memory must
88 // be keyed by tab rather than hold the most recently active tab alone.
89 const routesRef = useRef(new Map<string, string>());
90 useEffect(() => {
91 if (!activeTabId || !remote || !remoteProfile) return;
92 // One remote tab rotates through many sessions; a cached profile belongs
93 // to the session it was read from, so a route change adopts the backend
94 // snapshot instead of reconciling over the previous session's overrides.
95 const routes = routesRef.current;
96 const route = sessionRoute ?? "";
97 const routeChanged = routes.has(activeTabId) && routes.get(activeTabId) !== route;
98 routes.set(activeTabId, route);
99 setProfiles((current) => {
100 // The profile table drops closed tabs on hydration; forget their routes
101 // at the next sync so the memory stays bounded to live tabs. Deleting
102 // absent ids is idempotent, which keeps this updater pure enough for a
103 // double invocation.
104 for (const id of routes.keys()) if (id !== activeTabId && !(id in current)) routes.delete(id);
105 const existing = routeChanged ? undefined : current[activeTabId];
106 const backend: ComposerProfile = {
107 collaborationMode: remoteProfile.collaborationMode,
108 goalDraftMode: false,
109 toolApprovalMode: remoteProfile.toolApprovalMode,
110 goal: remoteProfile.goal,
111 qualityFloor: remoteProfile.qualityFloor,
112 pending: {},
113 };
114 const next = reconcileComposerProfile(existing, backend);
115 return existing === next ? current : { ...current, [activeTabId]: next };
116 });
117 }, [activeTabId, sessionRoute, remote, remoteProfile, setProfiles]);
118
119 return !remote || Boolean(remoteProfile
120 && (pending.collaborationMode || collaborationMode === remoteProfile.collaborationMode)
121 && (pending.toolApprovalMode || toolApprovalMode === remoteProfile.toolApprovalMode)
122 && (pending.goal || goal === remoteProfile.goal)
123 && (pending.qualityFloor || qualityFloor === remoteProfile.qualityFloor));
124 }
125
126 export function useRemoteComposerRuntimeActions(options: {
127 target: SessionResource;
128 operations: ReturnType<typeof useSessionOperations>;
129 remote: boolean;
130 session: RemoteSessionApi;
131 runGoalAction: (action: GoalAction) => void;
132 pauseLocal: (tabId: string) => Promise<unknown>;
133 resumeLocal: (tabId: string) => Promise<unknown>;
134 setLocalEffort: (tabId: string, level: string) => Promise<void>;
135 showError: (message: string) => void;
136 }) {
137 const { target, operations, remote, session, runGoalAction, pauseLocal, resumeLocal, setLocalEffort, showError } = options;
138 const ports = { pauseGoal: session.pauseGoal, resumeGoal: session.resumeGoal, setEffort: session.setEffort,
139 pauseLocal, resumeLocal, effortLocal: setLocalEffort };
140 const execute = useCommittedCommand(async (action: "pause" | "resume" | "effort", level?: string) => {
141 const outcome = await operations(target, action === "effort" ? "effort" : "goal-lifecycle",
142 { tabId: target.tabId, remote, action, level, ports }, executeComposerRuntime);
143 if (outcome.status === "failed") throw outcome.error;
144 });
145 const pauseGoal = useCommittedCommand(() => runGoalAction(() => execute("pause")));
146 const resumeGoal = useCommittedCommand(() => runGoalAction(() => execute("resume")));
147 const report = useCommittedCommand((error: unknown) => showError(error instanceof Error ? error.message : String(error)));
148 const setEffort = useCommittedCommand((level: string) => { void execute("effort", level).catch(report); });
149 return { pauseGoal, resumeGoal, setEffort };
150 }
151
151 lines TYPESCRIPT