返回 DeepSeek-Reasonix
useComposerInsertCommands.ts
根目录 / desktop / frontend / src / app-runtime / useComposerInsertCommands.ts
1 import { useEffect, useRef, useState } from "react";
2 import { useCommittedCommand } from "../lib/useCommittedCommand";
3 import { formatSelectionReference, type SelectedTextInsertRequest } from "../lib/selectedTextContext";
4 import type { ComposerInsertRequest } from "../lib/types";
5 import type { Translator } from "../lib/i18n";
6 import type { useSessionOperations } from "./useSessionOperations";
7
8 export type WorkspaceInsertTarget = "composer" | "planRevision";
9
10 export type ComposerInsertCommandsInput = {
11 activeTabId: string | undefined;
12 sessionKey: string;
13 browserSessionPath?: string;
14 approval: { id: string; tool: string } | undefined | null;
15 operations: ReturnType<typeof useSessionOperations>;
16 t: Translator;
17 showToast: (message: string, kind: "info" | "warn" | "error") => void;
18 ports: {
19 terminalOutput(tabId: string, sessionId: string): Promise<string>;
20 };
21 };
22
23 /**
24 * Owns every composer-bound insertion channel: per-tab composer insert
25 * requests, selected-text/code requests, the plan-revision insert and the
26 * workspace insert target that routes between them, plus terminal-output
27 * insertion through the session operations authority. The plan-revision
28 * input is plain text and only consumes request.text, so structured
29 * references land there in their fenced rendering.
30 */
31 export function useComposerInsertCommands(input: ComposerInsertCommandsInput) {
32 const { activeTabId, approval, t, showToast, ports } = input;
33 const [composerInsertRequestsByTab, setComposerInsertRequestsByTab] = useState<Record<string, ComposerInsertRequest>>({});
34 const [selectedTextRequestsByTab, setSelectedTextRequestsByTab] = useState<Record<string, SelectedTextInsertRequest>>({});
35 const selectedTextRequestIdRef = useRef(0);
36 const browserRequestKey = JSON.stringify([activeTabId, input.sessionKey]);
37 useEffect(() => {
38 if (!activeTabId || !input.browserSessionPath) return;
39 let closed = false, off: (() => void) | undefined;
40 void import("./browserComposerBridge").then(module => {
41 if (!closed) off = module.attachBrowserComposer(activeTabId, input.browserSessionPath!, text => {
42 const id = ++selectedTextRequestIdRef.current;
43 setSelectedTextRequestsByTab(current => ({ ...current, [browserRequestKey]: { id, text, source: "browser" } }));
44 });
45 });
46 return () => { closed = true; off?.(); };
47 }, [activeTabId, input.browserSessionPath, browserRequestKey]);
48 const [planRevisionInsertRequest, setPlanRevisionInsertRequest] = useState<{
49 tabId: string;
50 approvalId: string;
51 request: ComposerInsertRequest;
52 } | null>(null);
53 const [workspaceInsertTarget, setWorkspaceInsertTarget] = useState<WorkspaceInsertTarget>("composer");
54
55 const activePlanRevisionInsertRequest =
56 planRevisionInsertRequest &&
57 planRevisionInsertRequest.tabId === activeTabId &&
58 planRevisionInsertRequest.approvalId === approval?.id
59 ? planRevisionInsertRequest.request
60 : null;
61 const composerInsertRequest = activeTabId ? composerInsertRequestsByTab[activeTabId] ?? null : null;
62 const browserRequest = selectedTextRequestsByTab[browserRequestKey];
63 const otherRequest = activeTabId ? selectedTextRequestsByTab[activeTabId] : undefined;
64 const selectedTextRequest = activeTabId ? ((browserRequest?.id ?? 0) > (otherRequest?.id ?? 0) ? browserRequest : otherRequest) ?? null : null;
65
66 const setInsertTarget = useCommittedCommand((target: WorkspaceInsertTarget) => setWorkspaceInsertTarget(target));
67 const handleRevisionActiveChange = useCommittedCommand((active: boolean) => {
68 setWorkspaceInsertTarget(active ? "planRevision" : "composer");
69 });
70
71 const replaceComposerInsert = useCommittedCommand((tabId: string, text: string) => {
72 setComposerInsertRequestsByTab((current) => ({ ...current, [tabId]: { id: Date.now(), text, mode: "replace" } }));
73 });
74 const prefillSubagentCommand = useCommittedCommand((command: string) => {
75 if (!activeTabId) return;
76 setComposerInsertRequestsByTab((current) => ({
77 ...current,
78 [activeTabId]: { id: Date.now(), text: command, mode: "prefix" },
79 }));
80 });
81
82 const addWorkspaceTextToComposer = useCommittedCommand((text: string) => {
83 if (activeTabId && workspaceInsertTarget === "planRevision" && approval?.tool === "exit_plan_mode") {
84 setPlanRevisionInsertRequest({
85 tabId: activeTabId,
86 approvalId: approval.id,
87 request: { id: Date.now(), text },
88 });
89 return;
90 }
91 if (activeTabId) {
92 setComposerInsertRequestsByTab((current) => ({
93 ...current,
94 [activeTabId]: { id: Date.now(), text },
95 }));
96 }
97 });
98
99 const addTerminalOutputToComposer = useCommittedCommand(async (sessionId: string) => {
100 if (!activeTabId) return;
101 const target = { tabId: activeTabId, sessionKey: input.sessionKey };
102 const outcome = await input.operations(target, `terminal-output:${sessionId}`, {}, async (_operationInput, authority) => {
103 const [runtime, formatter] = await Promise.all([import("./sessionRuntimeOwner"), import("../lib/terminalOutput")]);
104 return runtime.executeTerminalOutputInsertion(target, sessionId, {
105 read: (tabId, terminalSessionId) => ports.terminalOutput(tabId, terminalSessionId),
106 apply: addWorkspaceTextToComposer,
107 }, formatter.formatTerminalOutputForComposer, authority);
108 },
109 );
110 if (outcome.status === "completed" && !outcome.value) showToast(t("terminal.noOutput"), "info");
111 if (outcome.status === "failed") showToast(outcome.error instanceof Error ? outcome.error.message : String(outcome.error), "error");
112 });
113
114 const addSelectedTextToComposer = useCommittedCommand((text: string, source?: SelectedTextInsertRequest["source"]) => {
115 const selected = text.trim();
116 if (!activeTabId || !selected) return;
117 selectedTextRequestIdRef.current += 1;
118 setSelectedTextRequestsByTab((current) => ({
119 ...current,
120 [activeTabId]: { id: selectedTextRequestIdRef.current, text: selected, ...(source ? { source } : {}) },
121 }));
122 });
123
124 const addTerminalSelectionToComposer = useCommittedCommand((text: string) => addSelectedTextToComposer(text, "terminal"));
125 const addWorkspaceCodeToComposer = useCommittedCommand((path: string, code: string) => {
126 if (!activeTabId || !code.trim()) return;
127 if (workspaceInsertTarget === "planRevision" && approval?.tool === "exit_plan_mode") {
128 setPlanRevisionInsertRequest({
129 tabId: activeTabId,
130 approvalId: approval.id,
131 request: { id: Date.now(), text: formatSelectionReference(path, code) },
132 });
133 return;
134 }
135 selectedTextRequestIdRef.current += 1;
136 setSelectedTextRequestsByTab((current) => ({
137 ...current,
138 [activeTabId]: { id: selectedTextRequestIdRef.current, text: code, path },
139 }));
140 });
141
142 return {
143 composerInsertRequest,
144 selectedTextRequest,
145 activePlanRevisionInsertRequest,
146 setInsertTarget,
147 handleRevisionActiveChange,
148 replaceComposerInsert,
149 prefillSubagentCommand,
150 addWorkspaceTextToComposer,
151 addTerminalOutputToComposer,
152 addSelectedTextToComposer,
153 addTerminalSelectionToComposer,
154 addWorkspaceCodeToComposer,
155 };
156 }
157
157 lines TYPESCRIPT