返回 DeepSeek-Reasonix
mockRemoteProjects.ts
根目录 / desktop / frontend / src / lib / mockRemoteProjects.ts
1 import type { ProjectNode, RemoteProjectView, RemoteSessionView, TabMeta } from "./types";
2 import type { RemoteProjectBindings } from "./remoteProjectBridge";
3 import { desktopHost } from "./desktopHost";
4 import { __emitMockRemoteTab, __emitMockRemoteTabOpened } from "./remoteTabEvents";
5
6 export type MockRemoteTabCatalog = {
7 get(id: string): TabMeta | undefined;
8 publish(tab: TabMeta): void;
9 remove(id: string): void;
10 };
11
12 export function createMockRemoteProjects(tabs: MockRemoteTabCatalog): {
13 bindings: RemoteProjectBindings;
14 appendToTree: (tree: ProjectNode[]) => ProjectNode[];
15 } {
16 // Browser-only fault fixtures exercise the real remote hook and recovery commands.
17 const recoveryScenario = typeof window !== "undefined" && desktopHost().kind === "none"
18 ? new URLSearchParams(window.location.search).get("session-recovery") : null;
19 let recoveryInjected = false;
20 let disconnectedTab = "";
21 let historyFailures = 0;
22 let projects: RemoteProjectView[] = [{ hostId: "demo", workspace: "~/app" }];
23 const sessions: Record<string, RemoteSessionView[]> = {
24 "demo\u0000~/app": [
25 { name: "intro", title: "远程会话演示 / Remote demo session", turns: 18, current: true, lastActivityAt: Date.now() - 45 * 60 * 1000 },
26 ],
27 };
28 const key = (hostId: string, workspace: string) => `${hostId}\u0000${workspace}`;
29 const tabIdFor = (hostId: string, workspace: string) => `remote-mock-${hostId}-${workspace}`.replace(/[^a-z0-9-]/gi, "_");
30 const status = (tabId: string) => ({
31 sessionPath: tabs.get(tabId)?.sessionPath,
32 label: tabs.get(tabId)?.label ?? "", running: false, pendingPrompt: false,
33 backgroundJobs: 0, plan: false, toolApprovalMode: "workspace-write", goal: "",
34 });
35
36 const bindings: RemoteProjectBindings = {
37 async AddRemoteProject(hostId, workspace) {
38 const existing = projects.find((project) => project.hostId === hostId && project.workspace === workspace);
39 if (existing) return { ...existing, merged: true };
40 const project = { hostId, workspace, title: `${hostId}:${workspace}` };
41 projects = [...projects, project];
42 return project;
43 },
44 async RemoveRemoteProject(hostId, workspace) {
45 projects = projects.filter((project) => project.hostId !== hostId || project.workspace !== workspace);
46 },
47 async ListRemoteProjects() {
48 return projects.slice();
49 },
50 async OpenRemoteProjectTab(hostId, workspace, opts) {
51 const id = tabIdFor(hostId, workspace);
52 if (!recoveryInjected && (recoveryScenario === "connection" || recoveryScenario === "history")) {
53 recoveryInjected = true;
54 if (recoveryScenario === "connection") disconnectedTab = id;
55 else historyFailures = 3;
56 } else if (disconnectedTab === id) disconnectedTab = "";
57 let tab = tabs.get(id);
58 if (!tab) {
59 const workspaceName = workspace.split("/").filter(Boolean).pop() || workspace;
60 tab = {
61 id,
62 scope: "project",
63 workspaceRoot: workspace,
64 sessionPath: `${workspace}/sessions/${opts?.sessionName || "intro"}.jsonl`,
65 workspaceName,
66 topicId: "",
67 topicTitle: workspaceName,
68 label: "remote/mock",
69 ready: true,
70 running: false,
71 mode: "normal",
72 active: true,
73 cwd: workspace,
74 remote: { hostId, workspace },
75 remoteState: "ready",
76 };
77 }
78 if (opts?.newSession) {
79 tab.topicTitle = "New session";
80 tab.sessionPath = `${workspace}/sessions/${crypto.randomUUID()}.jsonl`;
81 }
82 // Canonical rows are addressed by their immutable session id and may
83 // carry no legacy basename or path at all; legacy rows keep the
84 // basename route this mock has always synthesized. Mirrors the Serve
85 // contract, where opts.SessionID outranks SessionPath/SessionName and
86 // the opened session becomes the current one (desktop/remote_projects.go).
87 const requestedSessionID = opts?.sessionId?.trim() ?? "";
88 const requestedName = opts?.sessionName?.trim() ?? "";
89 if (requestedSessionID || requestedName) {
90 const rows = sessions[key(hostId, workspace)] ?? [];
91 const identity = requestedSessionID || requestedName;
92 const row = rows.find((candidate) => (candidate.sessionId || candidate.name) === identity);
93 tab.sessionId = requestedSessionID || row?.sessionId;
94 tab.sessionPath = requestedSessionID
95 ? row?.path ?? opts?.sessionPath ?? undefined
96 : `${workspace}/sessions/${requestedName}.jsonl`;
97 tab.topicTitle = row?.title || tab.workspaceName;
98 for (const candidate of rows) candidate.current = (candidate.sessionId || candidate.name) === identity;
99 }
100 tabs.publish(tab);
101 __emitMockRemoteTab(id, "state", { state: "ready" });
102 __emitMockRemoteTabOpened({ ...tab });
103 return { ...tab };
104 },
105 async RemoteProjectSessions(hostId, workspace) {
106 return (sessions[key(hostId, workspace)] ?? []).map((row) => ({ ...row }));
107 },
108 async EnsureRemoteProjectSessions(hostId, workspace) {
109 return (sessions[key(hostId, workspace)] ?? []).map((row) => ({ ...row }));
110 },
111 async SetRemoteSessionPinned(hostId, workspace, name, pinned) {
112 const row = (sessions[key(hostId, workspace)] ?? []).find((item) => (item.sessionId || item.name) === name);
113 if (row) row.pinned = pinned;
114 },
115 async SetRemoteProjectTitle(hostId, workspace, title) {
116 const project = projects.find((item) => item.hostId === hostId && item.workspace === workspace);
117 if (project) project.title = title.trim() || undefined;
118 },
119 async RenameRemoteProjectSession(hostId, workspace, name, title) {
120 const row = (sessions[key(hostId, workspace)] ?? []).find((item) => (item.sessionId || item.name) === name);
121 if (row) row.title = title.trim();
122 },
123 async DeleteRemoteProjectSession(hostId, workspace, name) {
124 sessions[key(hostId, workspace)] = (sessions[key(hostId, workspace)] ?? []).filter((item) => (item.sessionId || item.name) !== name);
125 },
126 async CloseRemoteTab(tabId) { tabs.remove(tabId); },
127 async SubmitRemoteTab(tabId, text) {
128 __emitMockRemoteTab(tabId, "event", { kind: "turn_started" });
129 __emitMockRemoteTab(tabId, "event", { kind: "message", text: `Mock remote reply: ${text}` });
130 __emitMockRemoteTab(tabId, "event", { kind: "turn_done" });
131 },
132 async ClearRemoteTabSession(tabId) {
133 __emitMockRemoteTab(tabId, "state", { state: "ready" });
134 },
135 async CancelRemoteTab(tabId) { __emitMockRemoteTab(tabId, "event", { kind: "turn_done" }); },
136 async ApproveRemoteTab() {},
137 async ResolveRemoteTabPlanDecision() {},
138 async SetRemoteTabQualityFloor() {},
139 async AnswerRemoteTab() {},
140 async SubmitRemoteTabExtensionForm() {},
141 async ReclaimRemoteTabSession() {},
142 async SetRemoteTabModel(tabId, ref) {
143 const tab = tabs.get(tabId);
144 if (tab) {
145 tab.label = ref;
146 tabs.publish(tab);
147 __emitMockRemoteTabOpened({ ...tab });
148 }
149 },
150 async RewindRemoteTab() {},
151 async SetRemoteTabGoal() {},
152 async EditRemoteTabGoal() {},
153 async RemoteTabSnapshot(tabId) {
154 if (disconnectedTab === tabId) {
155 __emitMockRemoteTab(tabId, "state", { state: "serve_down", error: "Mock remote tunnel closed" });
156 throw new Error("Mock remote tunnel closed");
157 }
158 if (historyFailures > 0) { historyFailures--; throw new Error("Mock remote history unavailable"); }
159 return { history: [], status: status(tabId) };
160 },
161 async RemoteTabStatus(tabId) { return status(tabId); },
162 async SetRemoteTabEffort() {},
163 async PauseRemoteTabGoal() {},
164 async ResumeRemoteTabGoal() {},
165 async CancelRemoteTabJobs() {},
166 async SteerRemoteTab() {},
167 async SetRemoteTabComposerProfile() { return []; },
168 async SetRemoteTabToolApprovalMode() {},
169 async SetRemoteTabPlanMode() {},
170 async CompactRemoteTab() {},
171 async ReplayRemoteTabPrompts() { return []; },
172 async ForkRemoteTab() {},
173 // The dev mock stands in for a serve that advertises the create-only fork
174 // capability: a target list that is always present, verifiable, and opens.
175 async ForkTargetsRemoteTab(tabId) {
176 if (!tabId) return { targets: [], verifiable: false };
177 return {
178 sourceSessionId: tabId, sessionGeneration: 1,
179 targets: [{ sourceSessionId: tabId, sessionGeneration: 1, turnId: `${tabId}:turn-1`, boundarySequence: 3, turnNumber: 1, status: "committed", available: true }],
180 verifiable: true,
181 };
182 },
183 async CreateForkRemoteTab(tabId, anchor) {
184 if (!tabId || !anchor.turnId) return { opened: false };
185 return { sessionId: `mock-remote-fork-${anchor.turnId}`, operationId: crypto.randomUUID(), opened: true };
186 },
187 async SummarizeRemoteTab() {},
188 async ForgetRemoteTab() {},
189 async RemoteTabBranches() { return []; },
190 async RemoteTabSkills() { return []; },
191 };
192
193 return {
194 bindings,
195 appendToTree(tree) {
196 for (const project of projects) {
197 if (tree.some((node) => node.key === `project_remote_${project.hostId}_${project.workspace}`)) continue;
198 tree.push({
199 key: `project_remote_${project.hostId}_${project.workspace}`,
200 kind: "project",
201 label: project.workspace.split("/").filter(Boolean).pop() || project.workspace,
202 root: project.workspace,
203 remote: { hostId: project.hostId, workspace: project.workspace },
204 });
205 }
206 return tree;
207 },
208 };
209 }
210
210 lines TYPESCRIPT