返回 DeepSeek-Reasonix
bridge.ts
根目录 / desktop / frontend / src / lib / bridge.ts
1 // bridge is the single seam between the React app and the Go kernel. In the Wails
2 // shell it calls the bound App methods (window.go.main.App.*) and subscribes to
3 // the runtime event stream (window.runtime.EventsOn). In a plain browser (`pnpm
4 // dev` outside the shell) those globals are absent, so it falls back to a mock
5 // that streams a canned turn through the same contract — letting the whole UI be
6 // developed and laid out without rebuilding the Go side.
7
8 // @ts-ignore `wails generate module` creates this locally; fresh checkouts keep
9 // typecheck green by falling back to a disabled drift check below.
10 import type * as GeneratedApp from "../../wailsjs/go/main/App";
11 import type { InvocationRequest } from "./invocationDisplay";
12
13 import { addBreadcrumb } from "./breadcrumbs";
14 import { t } from "./i18n";
15 import { providerIsConfigured, providerRequiresKey } from "./providerModels";
16 import { DEFAULT_STATUS_BAR_ITEMS, normalizeStatusBarItems } from "./statusBarItems";
17 import { registerTrustedThemeBackgroundURLs } from "./themePack";
18 import { modeHasAutoApproveTools, modeWithAutoApproveTools, modeWithPlan, normalizeCollaborationMode, normalizeMode, normalizeTokenMode, normalizeToolApprovalMode } from "./types";
19 import { decisionSurfaceMockFromInput, isLongDecisionOptionsMockInput } from "./decisionSurfaceMock";
20
21 import type {
22 AutoResearchFindingView,
23 AutoResearchEvidenceView,
24 AutoResearchStatusView,
25 RemoteHostView,
26 RemoteHostInput,
27 RemoteConnectionStatus,
28 RemoteDirEntry,
29 RemoteFilePreview,
30 RemoteWriteResult,
31 RemoteForwardInput,
32 RemoteForwardView,
33 RemoteServerView,
34 RemoteForwardsEvent,
35 RemoteLegacyWorkbenchData,
36 BalanceInfo,
37 UsageStatsRange,
38 UsageStatsRequest,
39 BotConnectionDiagnostic,
40 BotInstallPollResult,
41 BotInstallStartResult,
42 BotRuntimeStatusView,
43 BotSettingsView,
44 CapabilitiesView,
45 CapabilityDiagnosticsReport,
46 CheckpointMeta,
47 CommandInfo,
48 ControlResult,
49 ContextInfo,
50 ContextPanelInfo,
51 DirEntry,
52 DesktopStartupSettingsView,
53 DeliveryWorktreeAvailability,
54 DeliveryWorktreeOpenResult,
55 DroppedItem,
56 EffortInfo,
57 ExtensionActionView,
58 FilePreview,
59 ExternalOpenersView,
60 HistoryMessage,
61 HistoryPage,
62 HookConfigView,
63 HooksSettingsView,
64 JobView,
65 ActiveWorkView,
66 BackgroundRuntimeView,
67 JobCancelBatchView,
68 WorkspaceConflictView,
69 MCPMarketplaceEntry,
70 MCPServerInput,
71 MCPInstallResult,
72 MCPMarketplaceView,
73 MCPToolView,
74 MemoryFact,
75 MemorySuggestion,
76 MemorySuggestionsView,
77 MemoryView,
78 Meta,
79 Mode,
80 ModelInfo,
81 NetworkView,
82 PluginInstallOptions,
83 PluginView,
84 ProjectNode,
85 PromptHistoryEntry,
86 PromptHistoryResult,
87 ProviderModelCatalogUpdate,
88 ProviderPresetView,
89 ProviderView,
90 QuestionAnswer,
91 ServerView,
92 SessionMeta,
93 SessionRecoveryFailedEvent,
94 SessionRecoveryEvent,
95 SettingsView,
96 SkillsSettingsView,
97 SkillRootView,
98 SkillSuggestion,
99 SkillView,
100 TaskEvent,
101 TaskSnapshot,
102 SlashArgsResult,
103 SubagentProfileInput,
104 TabMeta,
105 TerminalSessionView,
106 TerminalWorkspaceView,
107 TopicMeta,
108 ToolApprovalMode,
109 UpdateInfo,
110 UpdateProgress,
111 WireEvent,
112 WorkspaceChangeDetailView,
113 WorkspaceChangesView,
114 GitCommitView,
115 GitCommitDetailView,
116 WorkspaceView,
117 } from "./types";
118
119 const GLOBAL_PROJECT_ORDER_KEY = "__global__";
120
121 function stripGoalResearchFlags(arg: string): string {
122 const parts = arg.trim().split(/\s+/).filter(Boolean);
123 while (parts.length > 0) {
124 const flag = parts[0].toLowerCase();
125 if (flag !== "--research" && flag !== "--auto-research" && flag !== "--deep" && flag !== "--simple" && flag !== "--no-research") break;
126 parts.shift();
127 }
128 return parts.join(" ");
129 }
130
131 // AppBindings is derived from the Wails-generated Go → TS method signatures, so
132 // the compiler catches drift between the Go binding surface and the frontend mock.
133 // Run `wails generate module` after adding/renaming a bound method on App, then
134 // `pnpm typecheck` to verify the mock still satisfies the contract.
135 //
136 // Types for the new native-feel bindings — kept inline since they are
137 // bridge-specific and only used in AppBindings / the dev mock.
138 interface NativeConfirmRequest {
139 title: string;
140 message: string;
141 detail: string;
142 confirmLabel: string;
143 cancelLabel: string;
144 destructive: boolean;
145 }
146
147 interface DesktopWindowState {
148 width: number;
149 height: number;
150 x: number;
151 y: number;
152 maximised: boolean;
153 }
154
155 // AppBindings is the hand-written contract between the React app and the Go
156 // kernel. It uses local types (types.ts) so components don't import generated
157 // model classes. _CheckGeneratedBindings catches drift: when a Go method is
158 // added or renamed, the generated types shift, and a key present in GeneratedApp
159 // but missing from AppBindings causes a type error here. Fix: add the new method
160 // to AppBindings, then run `pnpm typecheck` to verify.
161 export interface AppBindings {
162 Platform(): Promise<string>;
163 MinimiseMainWindow(): Promise<void>;
164 ToggleMaximiseMainWindow(): Promise<void>;
165 IsMainWindowMaximised(): Promise<boolean>;
166 CloseMainWindow(): Promise<void>;
167 // ── Heartbeat ──
168 HeartbeatListTasks(): Promise<unknown>;
169 HeartbeatReloadTasks(): Promise<unknown>;
170 HeartbeatSaveTasks(tasks: unknown): Promise<void>;
171 HeartbeatTriggerNow(id: string): Promise<void>;
172 HeartbeatGenerateID(): Promise<string>;
173 Submit(input: string): Promise<void>;
174 SubmitToTab(tabID: string, input: string): Promise<void>;
175 SubmitDisplay(display: string, input: string): Promise<void>;
176 SubmitDisplayToTab(tabID: string, display: string, input: string): Promise<void>;
177 SubmitDeliveryRecoveryToTab(tabID: string, display: string, input: string): Promise<void>;
178 SubmitInvocationsToTab(tabID: string, display: string, input: string, invocations: InvocationRequest[]): Promise<void>;
179 SubmitInitialGoalToTab(
180 tabID: string,
181 goal: string,
182 display: string,
183 input: string,
184 invocations: InvocationRequest[],
185 collaborationMode: string,
186 toolApprovalMode: string,
187 ): Promise<string[]>;
188 SubmitEditedDisplayToTab(tabID: string, display: string, input: string, original: string): Promise<void>;
189 RunShell(command: string): Promise<void>;
190 RunShellForTab(tabID: string, command: string): Promise<void>;
191 Steer(text: string): Promise<void>;
192 SteerForTab(tabID: string, text: string): Promise<void>;
193 Cancel(): Promise<void>;
194 CancelTab(tabID: string): Promise<void>;
195 Approve(id: string, allow: boolean, session: boolean, persist: boolean): Promise<void>;
196 ApproveTab(tabID: string, id: string, allow: boolean, session: boolean, persist: boolean): Promise<void>;
197 ResolvePlanDecision(id: string, action: "start_execution" | "revise_plan" | "exit_plan"): Promise<void>;
198 ResolvePlanDecisionTab(tabID: string, id: string, action: "start_execution" | "revise_plan" | "exit_plan"): Promise<void>;
199 ResolveRecovery(id: string, action: string, feedback: string): Promise<void>;
200 ResolveRecoveryTab(tabID: string, id: string, action: string, feedback: string): Promise<void>;
201 // Legacy no-ops: Auto Guard is always built into Auto.
202 SetRecoveryCheckpointEnabled(enabled: boolean): Promise<void>;
203 SetRecoveryCheckpointEnabledTab(tabID: string, enabled: boolean): Promise<void>;
204 RecoveryCheckpointEnabled(): Promise<boolean>;
205 RecoveryCheckpointEnabledTab(tabID: string): Promise<boolean>;
206 AnswerQuestion(id: string, answers: QuestionAnswer[]): Promise<void>;
207 AnswerQuestionForTab(tabID: string, id: string, answers: QuestionAnswer[]): Promise<void>;
208 ReplayPendingPrompts(): Promise<void>;
209 SetPlanMode(on: boolean): Promise<void>;
210 SetMode(mode: string): Promise<void>;
211 // Resolves with the pending approval prompt ids the switch auto-allowed
212 // (drained); prompts not listed are still pending backend-side (#6432).
213 SetModeForTab(tabID: string, mode: string): Promise<string[] | void>;
214 SetAutoApproveTools(on: boolean): Promise<void>;
215 SetCollaborationMode(mode: string): Promise<void>;
216 SetCollaborationModeForTab(tabID: string, mode: string): Promise<void>;
217 SetToolApprovalMode(mode: string): Promise<void>;
218 // Same drained-prompt-id contract as SetModeForTab.
219 SetToolApprovalModeForTab(tabID: string, mode: string): Promise<string[] | void>;
220 // Atomically applies the controller-facing composer profile and reports any
221 // approval prompts drained by the resulting tool-approval posture.
222 SetComposerProfileForTab(tabID: string, collaborationMode: string, toolApprovalMode: string, goal: string): Promise<string[] | void>;
223 SetGoal(goal: string): Promise<void>;
224 SetGoalForTab(tabID: string, goal: string): Promise<void>;
225 ResumeGoalForTab(tabID: string): Promise<boolean>;
226 PauseGoalForTab(tabID: string): Promise<boolean>;
227 ClearGoal(): Promise<void>;
228 ClearGoalForTab(tabID: string): Promise<void>;
229 Compact(): Promise<void>;
230 CompactForTab(tabID: string): Promise<void>;
231 NewSession(): Promise<void>;
232 NewSessionForTab(tabID: string): Promise<void>;
233 ClearSession(): Promise<void>;
234 ClearSessionForTab(tabID: string): Promise<void>;
235 History(): Promise<HistoryMessage[]>;
236 HistoryForTab(tabID: string): Promise<HistoryMessage[]>;
237 HistoryPage(beforeTurn: number, limit: number): Promise<HistoryPage>;
238 HistoryPageForTab(tabID: string, beforeTurn: number, limit: number): Promise<HistoryPage>;
239 HistoryCheckpointTurnsForTab(tabID: string): Promise<number[]>;
240 Checkpoints(): Promise<CheckpointMeta[]>;
241 CheckpointsForTab(tabID: string): Promise<CheckpointMeta[]>;
242 Rewind(turn: number, scope: string): Promise<void>;
243 RewindForTab(tabID: string, turn: number, scope: string): Promise<void>;
244 PreviewRewindForTab(tabID: string, turn: number, scope: string): Promise<import("./types").RewindPlanView>;
245 CommitRewindForTab(tabID: string, planID: string, turn: number, scope: string): Promise<import("./types").RewindResultView>;
246 UndoRewindForTab(tabID: string, transactionID: string): Promise<import("./types").RewindResultView>;
247 PreviewWorkspaceFileRevertForTab(tabID: string, path: string): Promise<import("./types").RewindPlanView>;
248 CommitWorkspaceFileRevertForTab(tabID: string, planID: string, resolution: string): Promise<import("./types").RewindResultView>;
249 Fork(turn: number): Promise<TabMeta>;
250 ForkForTab(tabID: string, turn: number): Promise<TabMeta>;
251 SummarizeFrom(turn: number): Promise<void>;
252 SummarizeFromForTab(tabID: string, turn: number): Promise<void>;
253 SummarizeUpTo(turn: number): Promise<void>;
254 SummarizeUpToForTab(tabID: string, turn: number): Promise<void>;
255 ListSessions(): Promise<SessionMeta[]>;
256 ListSessionsForTab(tabID: string): Promise<SessionMeta[]>;
257 ListTrashedSessions(): Promise<SessionMeta[]>;
258 ResumeSession(path: string): Promise<HistoryMessage[]>;
259 ResumeSessionForTab(tabID: string, path: string): Promise<HistoryMessage[]>;
260 ResumeSessionPage(path: string, limit: number): Promise<HistoryPage>;
261 ResumeSessionPageForTab(tabID: string, path: string, limit: number): Promise<HistoryPage>;
262 OpenChannelSessionForTab(tabID: string, path: string): Promise<HistoryMessage[]>;
263 OpenChannelSessionPageForTab(tabID: string, path: string, limit: number): Promise<HistoryPage>;
264 PreviewSession(path: string): Promise<HistoryMessage[]>;
265 DeleteSession(path: string): Promise<void>;
266 DeleteRecoveryCopy(path: string): Promise<void>;
267 RestoreSession(path: string): Promise<void>;
268 PurgeTrashedSession(path: string): Promise<void>;
269 PurgeRecoveryCopy(path: string): Promise<void>;
270 RenameSession(path: string, title: string): Promise<void>;
271 ScanPromptHistory(nonce: string): Promise<PromptHistoryResult>;
272 ListWorkspaces(): Promise<WorkspaceView[]>;
273 PickWorkspace(): Promise<string>;
274 SwitchWorkspace(path: string): Promise<string>;
275 RemoveWorkspace(path: string): Promise<void>;
276 ContextUsage(): Promise<ContextInfo>;
277 ContextUsageForTab(tabID: string): Promise<ContextInfo>;
278 Balance(): Promise<BalanceInfo>;
279 BalanceForTab(tabID: string): Promise<BalanceInfo>;
280 UsageStats(req: UsageStatsRequest): Promise<UsageStatsRange>;
281 Jobs(): Promise<JobView[]>;
282 ListTasks(): Promise<TaskSnapshot[]>;
283 CurrentTaskSessionID(): Promise<string>;
284 ListTasksForSession(sessionID: string): Promise<TaskSnapshot[]>;
285 GetTask(taskID: string): Promise<TaskSnapshot | null>;
286 ListTaskEvents(taskID: string, afterSequence: number): Promise<TaskEvent[]>;
287 StopTask(taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>;
288 CancelTask(taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>;
289 RequeueTask(taskID: string, expectedVersion: number, idemKey: string): Promise<ControlResult>;
290 OpenTaskSession(taskID: string): Promise<ControlResult>;
291 ListTasksForTab(tabID: string): Promise<TaskSnapshot[]>;
292 ListTaskEventsForTab(tabID: string, taskID: string, afterSequence: number): Promise<TaskEvent[]>;
293 StopTaskForTab(tabID: string, taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>;
294 CancelTaskForTab(tabID: string, taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>;
295 RequeueTaskForTab(tabID: string, taskID: string, expectedVersion: number, idemKey: string): Promise<ControlResult>;
296 OpenTaskSessionForTab(tabID: string, taskID: string): Promise<ControlResult>;
297 JobsForTab(tabID: string): Promise<JobView[]>;
298 CancelJob(jobID: string): Promise<boolean>;
299 CancelJobForTab(tabID: string, jobID: string): Promise<boolean>;
300 CancelJobsForTab(tabID: string, jobIDs: string[]): Promise<JobCancelBatchView>;
301 ActiveWorkForTab(tabID: string): Promise<ActiveWorkView>;
302 BackgroundRuntimes(): Promise<BackgroundRuntimeView[]>;
303 RevealBackgroundRuntime(tabID: string): Promise<TabMeta>;
304 WorkspaceConflictForTab(tabID: string): Promise<WorkspaceConflictView>;
305 RevealWorkspaceWriterForTab(tabID: string): Promise<TabMeta>;
306 CloseTabWithPolicy(tabID: string, policy: "keep_running" | "stop_and_close"): Promise<void>;
307 ToolResultForTab(tabID: string, toolID: string): Promise<{ args: string; output: string; execution?: import("./types").WireShellExecution } | null>;
308 Meta(): Promise<Meta>;
309 MetaForTab(tabID: string): Promise<Meta>;
310 AutoResearchCurrent(): Promise<AutoResearchStatusView>;
311 AutoResearchStatus(tabID: string): Promise<AutoResearchStatusView>;
312 AutoResearchList(tabID: string): Promise<AutoResearchStatusView[]>;
313 AutoResearchFindings(tabID: string, limit: number): Promise<AutoResearchFindingView[]>;
314 AutoResearchOpenTask(tabID: string): Promise<void>;
315 AutoResearchRecordEvidence(tabID: string, criterionID: string, input: AutoResearchEvidenceView): Promise<void>;
316 Commands(): Promise<CommandInfo[]>;
317 Capabilities(): Promise<CapabilitiesView>;
318 MCPServers(): Promise<ServerView[]>;
319 MCPMarketplace(query: string): Promise<MCPMarketplaceView>;
320 MCPMarketplaceResolve(registryName: string): Promise<MCPMarketplaceEntry>;
321 SkillsSettings(): Promise<SkillsSettingsView>;
322 CapabilityDiagnostics(includeSessionRuntime: boolean): Promise<CapabilityDiagnosticsReport>;
323 Plugins(): Promise<PluginView[]>;
324 PlanPluginInstall(source: string, options: PluginInstallOptions): Promise<string>;
325 InstallPlugin(source: string, options: PluginInstallOptions): Promise<string>;
326 RemovePlugin(name: string): Promise<void>;
327 SetPluginEnabled(name: string, enabled: boolean): Promise<void>;
328 UpdatePlugin(name: string): Promise<string>;
329 PluginDoctor(name: string): Promise<PluginView>;
330 // Extension UI (stage 8b2): enumerate handshake-declared extension actions
331 // for the command palette, invoke one, and deliver a form surface's values
332 // (or {cancelled: true} on dismissal) back to the owning sidecar.
333 ExtensionActions(tabID: string): Promise<ExtensionActionView[]>;
334 InvokeExtensionAction(tabID: string, name: string, args: Record<string, string>): Promise<string>;
335 SubmitExtensionForm(tabID: string, pluginID: string, surfaceID: string, values: Record<string, unknown>): Promise<void>;
336 AddMCPServer(input: MCPServerInput): Promise<number>;
337 InstallMCPServer(input: MCPServerInput): Promise<MCPInstallResult>;
338 UpdateMCPServer(name: string, input: MCPServerInput): Promise<void>;
339 RemoveMCPServer(name: string): Promise<void>;
340 AuthorizeAndConnectMCPServer(name: string): Promise<void>;
341 ReconnectMCPServer(name: string): Promise<void>;
342 ClearMCPServerAuthentication(name: string): Promise<void>;
343 PickSkillFolder(): Promise<string>;
344 PickPluginFolder(): Promise<string>;
345 AddSkillPath(path: string): Promise<void>;
346 RemoveSkillPath(path: string): Promise<void>;
347 RefreshSkills(): Promise<void>;
348 ReloadCommands(): Promise<void>;
349 SetSkillEnabled(name: string, enabled: boolean): Promise<void>;
350 AvailableSubagentTools(): Promise<MCPToolView[]>;
351 CreateSubagentProfile(input: SubagentProfileInput): Promise<string>;
352 UpdateSubagentProfile(name: string, scope: string, input: SubagentProfileInput): Promise<void>;
353 DeleteSubagentProfile(name: string, scope: string): Promise<void>;
354 SetSubagentProfileModel(name: string, ref: string): Promise<void>;
355 SetSubagentProfileEffort(name: string, level: string): Promise<void>;
356 TrySubagentProfile(input: SubagentProfileInput, task: string): Promise<string>;
357 CancelTrySubagentProfile(): Promise<void>;
358 SetMCPServerEnabled(name: string, enabled: boolean): Promise<void>;
359 SetMCPServerTier(name: string, tier: string): Promise<void>;
360 SlashArgs(input: string): Promise<SlashArgsResult>;
361 ListDir(rel: string): Promise<DirEntry[]>;
362 ListDirForTab(tabID: string, rel: string): Promise<DirEntry[]>;
363 SearchFileRefs(query: string): Promise<DirEntry[]>;
364 SearchFileRefsForTab(tabID: string, query: string): Promise<DirEntry[]>;
365 ReadFile(rel: string): Promise<FilePreview>;
366 ReadFileForTab(tabID: string, rel: string): Promise<FilePreview>;
367 WorkspaceChanges(tabID: string): Promise<WorkspaceChangesView>;
368 WorkspaceChangeDetail(tabID: string, path: string): Promise<WorkspaceChangeDetailView>;
369 GitBranches(): Promise<string[]>;
370 GitCheckout(branch: string): Promise<void>;
371 WorkspaceGitHistory(tabID: string, path: string): Promise<GitCommitView[]>;
372 WorkspaceGitCommitDetail(tabID: string, hash: string, path: string): Promise<GitCommitDetailView>;
373 OpenWorkspacePath(rel: string): Promise<void>;
374 OpenWorkspacePathForTab(tabID: string, rel: string): Promise<void>;
375 ExternalOpeners(): Promise<ExternalOpenersView>;
376 SetPreferredExternalOpener(id: string): Promise<void>;
377 OpenWorkspaceInExternalOpener(id: string): Promise<void>;
378 OpenWorkspaceInExternalOpenerForTab(tabID: string, id: string): Promise<void>;
379 RevealWorkspacePath(rel: string): Promise<void>;
380 RevealWorkspacePathForTab(tabID: string, rel: string): Promise<void>;
381 RevealPath(path: string): Promise<void>;
382 OpenLocalPath(path: string): Promise<void>;
383 SavePastedImage(dataUrl: string): Promise<string>;
384 SaveClipboardImage(): Promise<string>;
385 SavePastedFile(name: string, dataUrl: string): Promise<string>;
386 PickExportFile(defaultFilename: string, mimeType: string): Promise<string>;
387 SaveExportFile(path: string, payload: string, base64Encoded: boolean): Promise<void>;
388 SaveExportImageFiles(path: string, payloads: string[]): Promise<void>;
389 AttachDropped(path: string): Promise<DroppedItem>;
390 AttachmentDataURL(path: string): Promise<string>;
391 Models(): Promise<ModelInfo[]>;
392 SetModel(name: string): Promise<void>;
393 ModelsForTab(tabID: string): Promise<ModelInfo[]>;
394 SetModelForTab(tabID: string, name: string): Promise<void>;
395 Effort(): Promise<EffortInfo>;
396 SetEffort(level: string): Promise<void>;
397 EffortForTab(tabID: string): Promise<EffortInfo>;
398 SetEffortForTab(tabID: string, level: string): Promise<void>;
399 SetTokenMode(mode: string): Promise<void>;
400 SetTokenModeForTab(tabID: string, mode: string): Promise<void>;
401 // ReloadRuntime rebuilds the tab's agent runtime in place (tools, skills,
402 // commands, hooks, providers, MCP servers) via boot.Rebuild, keeping the
403 // session. Busy tabs queue one reload for when they go idle.
404 ReloadRuntime(tabID: string): Promise<void>;
405 Memory(): Promise<MemoryView>;
406 MemorySuggestions(): Promise<MemorySuggestionsView>;
407 AcceptMemorySuggestion(suggestion: MemorySuggestion): Promise<string>;
408 AcceptSkillSuggestion(suggestion: SkillSuggestion): Promise<string>;
409 MemoryForTab(tabID: string): Promise<MemoryView>;
410 MemoryRevisions(ref: string): Promise<MemoryFact[]>;
411 MemoryRevisionsForTab(tabID: string, ref: string): Promise<MemoryFact[]>;
412 RestoreMemoryRevision(ref: string, revision: number): Promise<MemoryFact>;
413 RestoreMemoryRevisionForTab(tabID: string, ref: string, revision: number): Promise<MemoryFact>;
414 MemorySuggestionsForTab(tabID: string): Promise<MemorySuggestionsView>;
415 AcceptMemorySuggestionForTab(tabID: string, suggestion: MemorySuggestion): Promise<string>;
416 AcceptSkillSuggestionForTab(tabID: string, suggestion: SkillSuggestion): Promise<string>;
417 Remember(scope: string, note: string): Promise<string>;
418 RememberForTab(tabID: string, scope: string, note: string): Promise<string>;
419 Forget(name: string): Promise<void>;
420 ForgetForTab(tabID: string, name: string): Promise<void>;
421 RestoreArchivedMemory(archivePath: string): Promise<MemoryFact>;
422 RestoreArchivedMemoryForTab(tabID: string, archivePath: string): Promise<MemoryFact>;
423 SaveDoc(path: string, body: string): Promise<string>;
424 SaveDocForTab(tabID: string, path: string, body: string): Promise<string>;
425 DesktopStartupSettings(): Promise<DesktopStartupSettingsView>;
426 Settings(): Promise<SettingsView>;
427 HooksSettings(scope: string): Promise<HooksSettingsView>;
428 SaveHooksSettings(scope: string, hooks: HookConfigView[]): Promise<void>;
429 SaveHooksSettingsForRoot(scope: string, projectRoot: string, hooks: HookConfigView[]): Promise<void>;
430 TrustProjectHooks(): Promise<void>;
431 TrustProjectHooksForRoot(projectRoot: string): Promise<void>;
432 SetDefaultModel(ref: string): Promise<void>;
433 SetPlannerModel(ref: string): Promise<void>;
434 SetSubagentModel(ref: string): Promise<void>;
435 SetSubagentEffort(level: string): Promise<void>;
436 SetMaxSubagentDepth(depth: number): Promise<void>;
437 SetMaxSubagentConcurrency(n: number): Promise<void>;
438 SetMaxParallelWriters(n: number): Promise<void>;
439 SetAutoPlan(mode: string): Promise<void>;
440 SetDefaultToolApprovalMode(mode: string): Promise<void>;
441 SetDefaultAutoRecoveryCheckpoint(enabled: boolean): Promise<void>;
442
443 SaveProvider(p: ProviderView): Promise<void>;
444 SaveProviderModelCatalogs(updates: ProviderModelCatalogUpdate[]): Promise<string[]>;
445 SaveProviderWithKey(p: ProviderView, key: string): Promise<string>;
446 AddOfficialProviderAccess(kind: string, key: string): Promise<string>;
447 AddProviderPresetAccess(id: string, key: string): Promise<string>;
448 ResetProviderPresetAccess(id: string): Promise<void>;
449 FetchProviderModels(p: ProviderView): Promise<string[]>;
450 FetchAllProviderModels(providers: ProviderView[]): Promise<Record<string, string[]>>;
451 DeleteProvider(name: string): Promise<void>;
452 RemoveProviderAccess(name: string): Promise<void>;
453 SaveProviderKey(apiKeyEnv: string, value: string): Promise<string>;
454 SetProviderKey(apiKeyEnv: string, value: string): Promise<string>;
455 ClearProviderKey(apiKeyEnv: string): Promise<void>;
456 SetPermissionMode(mode: string): Promise<void>;
457 AddPermissionRule(list: string, rule: string): Promise<void>;
458 RemovePermissionRule(list: string, rule: string): Promise<void>;
459 ReloadSettings(): Promise<void>;
460 SetSandbox(bash: string, network: boolean, workspaceRoot: string, allowWrite: string[], shell: string): Promise<void>;
461 SetNetwork(n: NetworkView): Promise<void>;
462 SetBotSettings(b: BotSettingsView): Promise<void>;
463 SetBotConnectionToolApprovalMode(connID: string, mode: string): Promise<void>;
464 SetBotSecret(envName: string, value: string): Promise<void>;
465 ClearBotSecret(envName: string): Promise<void>;
466 StartBotConnectionInstall(provider: string, domain: string): Promise<BotInstallStartResult>;
467 PollBotConnectionInstall(installID: string): Promise<BotInstallPollResult>;
468 BotRuntimeStatus(): Promise<BotRuntimeStatusView>;
469 DiagnoseBotConnection(id: string): Promise<BotConnectionDiagnostic>;
470 TestBotConnection(id: string, target?: string): Promise<BotConnectionDiagnostic>;
471 SetCloseBehavior(mode: string): Promise<void>;
472 SetDisplayMode(mode: string): Promise<void>;
473 SetStatusBarStyle(style: string): Promise<void>;
474 SetStatusBarItems(items: string[]): Promise<void>;
475 SetDesktopLanguage(lang: string): Promise<void>;
476 SetDesktopCurrency(currency: string): Promise<void>;
477 SetDesktopAppearance(theme: string, style: string): Promise<void>;
478 SetDesktopTerminalTheme(theme: string): Promise<void>;
479 ListThemePacks(): Promise<import("./themePack").ThemePackView[]>;
480 GetActiveThemePack(): Promise<import("./themePack").ThemeActiveView>;
481 GetThemeExperience(): Promise<import("./themeExperience").ThemeExperienceView>;
482 ActivateThemePack(id: string): Promise<void>;
483 ActivateBaseStyle(style: string): Promise<void>;
484 DisableThemePack(): Promise<void>;
485 RestoreGraphiteAppearance(): Promise<void>;
486 ResetThemePack(): Promise<void>;
487 SaveThemePack(input: import("./themePack").ThemeSaveInput): Promise<import("./themePack").ThemePackView>;
488 DeleteThemePack(id: string): Promise<void>;
489 CopyThemePack(sourceID: string, newID: string, newName: string): Promise<import("./themePack").ThemePackView>;
490 ImportThemePack(sourcePath: string, replace: boolean): Promise<import("./themePack").ThemeImportResult>;
491 ExportThemePack(id: string, destPath: string): Promise<string>;
492 PickThemeBackground(): Promise<string>;
493 SetDesktopLayoutStyle(style: string): Promise<void>;
494 SetDesktopZoomFactor(factor: number): Promise<void>;
495 GetDesktopZoomFactor(): Promise<number>;
496 RestartApplication(): Promise<void>;
497 SetDesktopCheckUpdates(enabled: boolean): Promise<void>;
498 SetDesktopUpdateChannel(channel: string): Promise<void>;
499 SetDesktopTelemetry(enabled: boolean): Promise<void>;
500 SetDesktopMetrics(enabled: boolean): Promise<void>;
501 SetExpandThinking(on: boolean): Promise<void>;
502 SetDesktopConversationWidth(width: string): Promise<void>;
503 MigrateDesktopPreferences(language: string, theme: string, style: string): Promise<void>;
504 SetAgentParams(temperature: number, maxSteps: number, plannerMaxSteps: number, systemPrompt: string): Promise<void>;
505 SetColdResumePrune(enabled: boolean): Promise<void>;
506 SetCompactRatio(ratio: number): Promise<void>;
507 SetReasoningLanguage(lang: string): Promise<void>;
508 SetTrayLocale(locale: "en" | "zh" | "zh-TW"): Promise<void>;
509 // SetBypass is the legacy Wails name for YOLO/full-access tool auto-approval
510 // (ask questions and plan approvals still wait; deny rules still apply).
511 // Runtime-only.
512 SetBypass(on: boolean): Promise<void>;
513 Version(): Promise<string>;
514 CheckUpdate(channel: string): Promise<UpdateInfo | null>;
515 /** v1.20+ single-action update: download, verify, install, relaunch. */
516 ApplyUpdateRequest(channel: string, expectedVersion: string, requestId: string): Promise<void>;
517 OpenDownloadPage(): Promise<void>;
518 OpenUserConfigPath?(): Promise<void>;
519 ReloadUserConfig?(): Promise<{ configWarnings?: string[]; configPath?: string } | null>;
520 NeedsOnboarding(): Promise<boolean>;
521 ConnectKey(apiKey: string): Promise<string>;
522 // Crash overlay "Send report" (desktop/crash_app.go): scrubs user paths, attaches
523 // version/os/arch, POSTs to the collection endpoint. Only ever sent on user click.
524 ReportCrash(kind: string, detail: string): Promise<void>;
525 ListTabs(): Promise<TabMeta[]>;
526 OpenProjectTab(workspaceRoot: string, topicID: string): Promise<TabMeta>;
527 DeliveryWorktreeAvailability(workspaceRoot: string): Promise<DeliveryWorktreeAvailability>;
528 CreateDeliveryWorktree(workspaceRoot: string): Promise<DeliveryWorktreeOpenResult>;
529 OpenGlobalTab(topicID: string): Promise<TabMeta>;
530 OpenTopicSession(scope: string, workspaceRoot: string, topicID: string, sessionPath: string): Promise<TabMeta>;
531 EnsureBlankTab(scope: string, workspaceRoot: string): Promise<TabMeta>;
532 ActivateTopic(scope: string, workspaceRoot: string, topicID: string, sessionPath: string): Promise<TabMeta>;
533 EnsureBlankSurface(scope: string, workspaceRoot: string): Promise<TabMeta>;
534 SetActiveTab(tabID: string): Promise<void>;
535 ReorderTabs(tabIDs: string[]): Promise<void>;
536 CloseTab(tabID: string): Promise<void>;
537 TerminalWorkspaceForTab(tabID: string): Promise<TerminalWorkspaceView>;
538 TerminalOutputForTab(tabID: string, sessionID: string): Promise<string>;
539 CreateTerminalForTab(tabID: string, relativePath: string, shellID: string): Promise<TerminalSessionView>;
540 WriteTerminalForTab(tabID: string, sessionID: string, data: string): Promise<void>;
541 ResizeTerminalForTab(tabID: string, sessionID: string, cols: number, rows: number): Promise<void>;
542 CloseTerminalForTab(tabID: string, sessionID: string): Promise<void>;
543 RenameTerminalForTab(tabID: string, sessionID: string, title: string): Promise<void>;
544 ListProjectTree(): Promise<ProjectNode[]>;
545 RenameProject(workspaceRoot: string, title: string): Promise<void>;
546 SetProjectColor(workspaceRoot: string, color: string): Promise<void>;
547 SetProjectPinned(workspaceRoot: string, pinned: boolean): Promise<void>;
548 ReorderProjects(workspaceRoots: string[]): Promise<void>;
549 CreateTopic(scope: string, workspaceRoot: string, title: string): Promise<TopicMeta>;
550 RenameTopic(topicID: string, title: string): Promise<void>;
551 DeleteTopic(topicID: string): Promise<void>;
552 TrashTopic(topicID: string): Promise<void>;
553 SetTopicPinned(topicID: string, pinned: boolean): Promise<void>;
554 ContextPanel(tabID: string): Promise<ContextPanelInfo>;
555 // New native-feel bindings (added with the desktop native-feel plan).
556 ConfirmAction(req: NativeConfirmRequest): Promise<boolean>;
557 SaveWindowState(state: DesktopWindowState): Promise<void>;
558 // ── Remote (SSH) ──
559 RemoteHosts(): Promise<RemoteHostView[]>;
560 AddRemoteHost(input: RemoteHostInput): Promise<RemoteHostView>;
561 UpdateRemoteHost(id: string, input: RemoteHostInput): Promise<RemoteHostView>;
562 RemoveRemoteHost(id: string): Promise<void>;
563 ScanSSHConfig(): Promise<RemoteHostInput[]>;
564 ConnectRemoteHost(id: string): Promise<void>;
565 DisconnectRemoteHost(id: string): Promise<void>;
566 RemoteConnectionStatuses(): Promise<RemoteConnectionStatus[]>;
567 ConfirmRemoteHostKey(hostId: string, accept: boolean): Promise<void>;
568 ConfirmRemoteSecret(hostId: string, promptId: string, secret: string, accept: boolean): Promise<void>;
569 ListRemoteDir(hostId: string, path: string): Promise<RemoteDirEntry[]>;
570 ReadRemoteFile(hostId: string, path: string): Promise<RemoteFilePreview>;
571 WriteRemoteFile(hostId: string, path: string, body: string, expectMtimeUnix: number): Promise<RemoteWriteResult>;
572 MkdirRemote(hostId: string, path: string): Promise<void>;
573 RenameRemotePath(hostId: string, oldPath: string, newPath: string): Promise<void>;
574 DeleteRemotePath(hostId: string, path: string, recursive: boolean): Promise<void>;
575 RemoteForwards(hostId: string): Promise<RemoteForwardView[]>;
576 AddRemoteForward(hostId: string, input: RemoteForwardInput): Promise<RemoteForwardView>;
577 RemoveRemoteForward(hostId: string, forwardId: string): Promise<void>;
578 OpenRemoteWorkspace(hostId: string, workspace: string): Promise<void>;
579 StopRemoteServer(hostId: string): Promise<void>;
580 RemoteServerStatus(hostId: string): Promise<RemoteServerView>;
581 RemoteServerLogs(hostId: string, tailLines: number): Promise<string>;
582 RemoteLastWorkspace(hostId: string): Promise<string>;
583 ScanRemoteLegacyWorkbenchData(): Promise<RemoteLegacyWorkbenchData>;
584 CleanRemoteLegacyWorkbenchData(target: "mirrors" | "trust"): Promise<void>;
585 }
586
587 // Compile-time drift check. Exclude<A, B> extracts keys in A that are missing
588 // from B. If that set is non-empty, AssertNever<non-never> fails with
589 // "Type 'X' does not satisfy the constraint 'never'".
590 // _CheckGenToApp errors mean a generated Go method has no TS counterpart.
591 // These compare method *names* only; full signature checking isn't possible here
592 // because local types (types.ts) use plain interfaces while generated types
593 // (models.ts) use classes with a convertValues prototype method. The structural
594 // mismatch would produce false positives. Method-arity and parameter-order drift
595 // are caught at the call sites by tsc when components invoke app.<method>(...).
596 type AssertNever<T extends never> = T;
597 type GeneratedAppKeys = keyof typeof GeneratedApp;
598 type GeneratedAppMissing =
599 string extends GeneratedAppKeys ? true :
600 number extends GeneratedAppKeys ? true :
601 symbol extends GeneratedAppKeys ? true :
602 false;
603 export type _CheckGenToApp = AssertNever<
604 GeneratedAppMissing extends true ? never : Exclude<GeneratedAppKeys, keyof AppBindings>
605 >;
606
607 interface WailsRuntime {
608 EventsOn(name: string, cb: (...data: unknown[]) => void): () => void;
609 BrowserOpenURL(url: string): void;
610 WindowSetSystemDefaultTheme?(): void;
611 WindowSetLightTheme?(): void;
612 WindowSetDarkTheme?(): void;
613 WindowSetBackgroundColour?(r: number, g: number, b: number, a: number): void;
614 WindowGetSize?(): Promise<{ w: number; h: number }>;
615 WindowGetPosition?(): Promise<{ x: number; y: number }>;
616 WindowIsMaximised?(): Promise<boolean>;
617 ClipboardSetText?(text: string): Promise<boolean>;
618 // Native OS file drop (desktop only); useDropTarget gates delivery to elements
619 // carrying the --wails-drop-target CSS property. Absent in the browser dev mock.
620 OnFileDrop?(cb: (x: number, y: number, paths: string[]) => void, useDropTarget: boolean): void;
621 OnFileDropOff?(): void;
622 }
623
624 declare global {
625 interface Window {
626 runtime?: WailsRuntime;
627 go?: { main?: { App?: AppBindings } };
628 }
629 }
630
631 // Must match desktop/app.go's eventChannel constant.
632 const EVENT_CHANNEL = "agent:event";
633 const RECENT_NATIVE_FILE_DRAG_MS = 2000;
634 const WAILS_NON_FILE_DRAG_MESSAGE = "additional File object is not a file on the disk";
635 const UNCAUGHT_ERROR_PREFIX_RE = /^Uncaught(?:\s+\(in promise\))?(?:\s+\w*Error)?:\s*/i;
636 const WAILS_IPC_CONNECTING_RE = /Failed to execute 'send' on 'WebSocket': Still in CONNECTING state/i;
637 const WAILS_IPC_NULL_SEND_RE = /Cannot read properties of null \(reading 'send'\)/i;
638
639 // Resolve the Wails binding at CALL time, not module-load time: in dev the Wails
640 // runtime can inject window.go AFTER this module first evaluates, so snapshotting
641 // once would pin the browser mock for the whole session (and show fake data — the
642 // dev mock's model list leaking into the real app was exactly this bug).
643 function realApp(): AppBindings | undefined {
644 return typeof window !== "undefined" ? window.go?.main?.App : undefined;
645 }
646
647 let mockSingleton: AppBindings | null = null;
648 function getMock(): AppBindings {
649 if (!mockSingleton) mockSingleton = makeMockApp();
650 return mockSingleton;
651 }
652
653 // onEvent subscribes to the agent's typed event stream; returns an unsubscribe.
654 export function onEvent(cb: (e: WireEvent) => void): () => void {
655 if (realApp() && typeof window !== "undefined" && window.runtime) {
656 return window.runtime.EventsOn(EVENT_CHANNEL, (payload) => cb(payload as WireEvent));
657 }
658 return mockSubscribe(cb);
659 }
660
661 export interface TerminalOutputEvent {
662 id: string;
663 data: string;
664 }
665
666 export interface TerminalExitEvent {
667 id: string;
668 exitCode: number;
669 removed?: boolean;
670 }
671
672 function terminalEventPayload<T>(payload: unknown): T | null {
673 if (!payload || typeof payload !== "object") return null;
674 return payload as T;
675 }
676
677 export function onTerminalOutput(cb: (event: TerminalOutputEvent) => void): () => void {
678 if (realApp() && typeof window !== "undefined" && window.runtime) {
679 return window.runtime.EventsOn("terminal:output", (payload) => {
680 const event = terminalEventPayload<TerminalOutputEvent>(payload);
681 if (event?.id && typeof event.data === "string") cb(event);
682 });
683 }
684 mockTerminalOutputListeners.add(cb);
685 return () => mockTerminalOutputListeners.delete(cb);
686 }
687
688 export function onTerminalExit(cb: (event: TerminalExitEvent) => void): () => void {
689 if (realApp() && typeof window !== "undefined" && window.runtime) {
690 return window.runtime.EventsOn("terminal:exit", (payload) => {
691 const event = terminalEventPayload<TerminalExitEvent>(payload);
692 if (event?.id && typeof event.exitCode === "number") cb(event);
693 });
694 }
695 mockTerminalExitListeners.add(cb);
696 return () => mockTerminalExitListeners.delete(cb);
697 }
698
699 const mockTerminalOutputListeners = new Set<(event: TerminalOutputEvent) => void>();
700 const mockTerminalExitListeners = new Set<(event: TerminalExitEvent) => void>();
701
702 export function __emitMockTerminalOutput(event: TerminalOutputEvent): void {
703 mockTerminalOutputListeners.forEach((listener) => listener(event));
704 }
705
706 export function __emitMockTerminalExit(event: TerminalExitEvent): void {
707 mockTerminalExitListeners.forEach((listener) => listener(event));
708 }
709
710 // onUpdaterProgress subscribes to the auto-updater's progress events (a separate
711 // channel from the agent stream); returns an unsubscribe. Must match the event
712 // name emitted in desktop/updater_app.go.
713 export function onUpdaterProgress(cb: (p: UpdateProgress) => void): () => void {
714 if (realApp() && typeof window !== "undefined" && window.runtime) {
715 return window.runtime.EventsOn("updater:progress", (p) => cb(p as UpdateProgress));
716 }
717 updaterListeners.add(cb);
718 return () => {
719 updaterListeners.delete(cb);
720 };
721 }
722
723 function errorMessage(err: unknown): string {
724 if (err && typeof err === "object" && "message" in err) {
725 const msg = (err as { message?: unknown }).message;
726 if (typeof msg === "string") return msg;
727 }
728 return String(err);
729 }
730
731 export function isWailsNonFileDragError(err: unknown, recentNativeFileDrag = false): boolean {
732 const msg = errorMessage(err).trim().replace(UNCAUGHT_ERROR_PREFIX_RE, "");
733 if (msg.includes(WAILS_NON_FILE_DRAG_MESSAGE)) return true;
734 return recentNativeFileDrag && msg.toLowerCase() === "invalid argument";
735 }
736
737 export function isWailsNonFileDragErrorEvent(
738 event: Pick<ErrorEvent, "error" | "message">,
739 recentNativeFileDrag = false,
740 ): boolean {
741 if (isWailsNonFileDragError(event.error ?? event.message, recentNativeFileDrag)) return true;
742 return event.error != null && isWailsNonFileDragError(event.message, recentNativeFileDrag);
743 }
744
745 export function isTransientWailsIPCError(err: unknown): boolean {
746 const msg = errorMessage(err).trim().replace(UNCAUGHT_ERROR_PREFIX_RE, "");
747 return WAILS_IPC_CONNECTING_RE.test(msg) || WAILS_IPC_NULL_SEND_RE.test(msg);
748 }
749
750 function dataTransferLooksLikeFileDrag(dt: DataTransfer | null): boolean {
751 if (!dt) return false;
752 if (dt.files?.length > 0) return true;
753 return Array.from(dt.types ?? []).includes("Files");
754 }
755
756 let wailsDragSuppressionRefs = 0;
757 let wailsDragSuppressionUninstall: (() => void) | null = null;
758 let lastNativeFileDragAt = 0;
759
760 export function installWailsNonFileDragErrorSuppression(): () => void {
761 if (typeof window === "undefined") return () => {};
762
763 wailsDragSuppressionRefs += 1;
764 if (!wailsDragSuppressionUninstall) {
765 const markNativeFileDrag = (e: DragEvent) => {
766 if (dataTransferLooksLikeFileDrag(e.dataTransfer)) lastNativeFileDragAt = Date.now();
767 };
768 const hasRecentNativeFileDrag = () => Date.now() - lastNativeFileDragAt <= RECENT_NATIVE_FILE_DRAG_MS;
769 const suppressNonFileDragError = (e: ErrorEvent) => {
770 if (isWailsNonFileDragErrorEvent(e, hasRecentNativeFileDrag()) || isTransientWailsIPCError(e.error ?? e.message)) {
771 e.preventDefault();
772 }
773 };
774 const suppressNonFileDragRejection = (e: PromiseRejectionEvent) => {
775 if (isWailsNonFileDragError(e.reason, hasRecentNativeFileDrag()) || isTransientWailsIPCError(e.reason)) {
776 e.preventDefault();
777 }
778 };
779
780 window.addEventListener("dragenter", markNativeFileDrag, true);
781 window.addEventListener("dragover", markNativeFileDrag, true);
782 window.addEventListener("drop", markNativeFileDrag, true);
783 window.addEventListener("error", suppressNonFileDragError);
784 window.addEventListener("unhandledrejection", suppressNonFileDragRejection);
785 wailsDragSuppressionUninstall = () => {
786 window.removeEventListener("dragenter", markNativeFileDrag, true);
787 window.removeEventListener("dragover", markNativeFileDrag, true);
788 window.removeEventListener("drop", markNativeFileDrag, true);
789 window.removeEventListener("error", suppressNonFileDragError);
790 window.removeEventListener("unhandledrejection", suppressNonFileDragRejection);
791 lastNativeFileDragAt = 0;
792 };
793 }
794
795 let disposed = false;
796 return () => {
797 if (disposed) return;
798 disposed = true;
799 wailsDragSuppressionRefs = Math.max(0, wailsDragSuppressionRefs - 1);
800 if (wailsDragSuppressionRefs === 0 && wailsDragSuppressionUninstall) {
801 wailsDragSuppressionUninstall();
802 wailsDragSuppressionUninstall = null;
803 }
804 };
805 }
806
807 // onFilesDropped subscribes to native OS file drops landing on the composer (the
808 // --wails-drop-target element); the callback gets the dropped files' absolute
809 // paths. No-op in the browser dev mock, where the runtime is absent.
810 export function onFilesDropped(cb: (paths: string[]) => void): () => void {
811 const rt = typeof window !== "undefined" ? window.runtime : undefined;
812 if (!rt?.OnFileDrop) return () => {};
813
814 // Wails' internal ResolveFilePaths throws when a non-file object (e.g. the
815 // window icon) is dragged onto the webview. The error is uncaught and crashes
816 // the app. Intercept it here so only real file drops reach the callback.
817 const uninstallDragSuppression = installWailsNonFileDragErrorSuppression();
818
819 rt.OnFileDrop((_x, _y, paths) => {
820 if (Array.isArray(paths) && paths.length > 0) cb(paths);
821 }, true);
822 return () => {
823 rt.OnFileDropOff?.();
824 uninstallDragSuppression();
825 };
826 }
827
828 // onReady subscribes to the agent:ready event fired when boot.Build completes.
829 // The frontend re-fetches Meta/Context/History when this lands.
830 // onRuntimeRebuilt fires when a tab's controller is replaced in place
831 // (model/effort/token-mode switch, clear-while-running). The rebuilt
832 // controller restarts prompt ids, so per-tab id-keyed state must reset.
833 export function onRuntimeRebuilt(cb: (tabId?: string, runtimeEpoch?: string) => void): () => void {
834 if (realApp() && typeof window !== "undefined" && window.runtime) {
835 return window.runtime.EventsOn("runtime:rebuilt", (tabId?: unknown, runtimeEpoch?: unknown) =>
836 cb(
837 typeof tabId === "string" ? tabId : undefined,
838 typeof runtimeEpoch === "string" ? runtimeEpoch : undefined,
839 )
840 );
841 }
842 return () => {};
843 }
844
845 export function onReady(cb: (tabId?: string) => void): () => void {
846 if (realApp() && typeof window !== "undefined" && window.runtime) {
847 return window.runtime.EventsOn("agent:ready", (tabId?: unknown) => cb(typeof tabId === "string" ? tabId : undefined));
848 }
849 // In dev mock, fire immediately since there's no real boot sequence.
850 cb();
851 return () => {};
852 }
853
854 export function onProjectTreeChanged(cb: () => void): () => void {
855 if (realApp() && typeof window !== "undefined" && window.runtime) {
856 return window.runtime.EventsOn("project-tree:changed", () => cb());
857 }
858 return () => {};
859 }
860
861 export function onSessionRecovered(cb: (payload: SessionRecoveryEvent) => void): () => void {
862 if (realApp() && typeof window !== "undefined" && window.runtime) {
863 return window.runtime.EventsOn("session:recovered", (payload?: unknown) => cb((payload ?? {}) as SessionRecoveryEvent));
864 }
865 return () => {};
866 }
867
868 export function onSessionRecoveryFailed(cb: (payload: SessionRecoveryFailedEvent) => void): () => void {
869 if (realApp() && typeof window !== "undefined" && window.runtime) {
870 return window.runtime.EventsOn("session:recovery-failed", (payload?: unknown) => cb((payload ?? {}) as SessionRecoveryFailedEvent));
871 }
872 return () => {};
873 }
874
875 export function onRemoteStatus(cb: (s: RemoteConnectionStatus) => void): () => void {
876 if (realApp() && typeof window !== "undefined" && window.runtime) {
877 return window.runtime.EventsOn("remote:status", (payload?: unknown) => cb((payload ?? {}) as RemoteConnectionStatus));
878 }
879 return registerMockRemoteListener("status", cb as (v: unknown) => void);
880 }
881
882 export function onRemoteForwards(cb: (e: RemoteForwardsEvent) => void): () => void {
883 if (realApp() && typeof window !== "undefined" && window.runtime) {
884 return window.runtime.EventsOn("remote:forwards", (payload?: unknown) => cb((payload ?? {}) as RemoteForwardsEvent));
885 }
886 return registerMockRemoteListener("forwards", cb as (v: unknown) => void);
887 }
888
889 export function onRemoteServer(cb: (s: RemoteServerView) => void): () => void {
890 if (realApp() && typeof window !== "undefined" && window.runtime) {
891 return window.runtime.EventsOn("remote:server", (payload?: unknown) => cb((payload ?? {}) as RemoteServerView));
892 }
893 return registerMockRemoteListener("server", cb as (v: unknown) => void);
894 }
895
896
897
898 // Mock event fan-out so browser-dev and tsx tests can drive remote:* events
899 // without a Wails runtime.
900 type MockRemoteChannel = "status" | "forwards" | "server";
901 const mockRemoteListeners: Record<MockRemoteChannel, Set<(v: unknown) => void>> = {
902 status: new Set(),
903 forwards: new Set(),
904 server: new Set(),
905 };
906 function registerMockRemoteListener(ch: MockRemoteChannel, cb: (v: unknown) => void): () => void {
907 mockRemoteListeners[ch].add(cb);
908 return () => mockRemoteListeners[ch].delete(cb);
909 }
910 export function __emitMockRemote(ch: MockRemoteChannel, payload: unknown): void {
911 for (const cb of mockRemoteListeners[ch]) cb(payload);
912 }
913
914 // app proxies each call to the live binding (or the dev mock only when truly
915 // outside the shell), so a late-injected window.go is picked up transparently.
916 function bridgeBreadcrumb(method: string): string {
917 if (method === "ReportCrash") return "";
918 if (/^(Submit|SubmitDisplay|RunShell|Steer|Cancel|Approve|AnswerQuestion|ReplayPendingPrompts)/.test(method))
919 return `turn ${method}`;
920 if (/^(SetModel|SetEffort|SetTokenMode|SetDefaultModel|SetPlannerModel|SetSubagentModel|SetSubagentEffort|SetMaxSubagentDepth|SetMaxSubagentConcurrency|SetMaxParallelWriters)/.test(method))
921 return `model ${method}`;
922 if (/^(SetDesktop|SetCloseBehavior|SetDisplayMode|SetStatusBar|SetExpandThinking|SetAutoPlan|SetDefaultToolApprovalMode|SetCompactRatio|SetReasoningLanguage)/.test(method))
923 return `settings ${method}`;
924 if (/^(SaveProvider|SaveProviderModelCatalogs|AddOfficialProviderAccess|AddProviderPresetAccess|ResetProviderPresetAccess|RemoveProviderAccess|DeleteProvider|SaveProviderKey|SetProviderKey|ClearProviderKey|FetchProviderModels|FetchAllProviderModels|ConnectKey)/.test(method))
925 return `provider ${method}`;
926 if (/^(CheckUpdate|ApplyUpdateRequest|OpenDownloadPage|OpenUserConfigPath|ReloadUserConfig)/.test(method)) return `update ${method}`;
927 if (/^(AddMCPServer|InstallMCPServer|UpdateMCPServer|RemoveMCPServer|AuthorizeAndConnectMCPServer|ReconnectMCPServer|ClearMCPServerAuthentication|SetMCPServer)/.test(method))
928 return `mcp ${method}`;
929 if (/^(AddSkillPath|RemoveSkillPath|RefreshSkills|SetSkillEnabled|AcceptSkillSuggestion|AvailableSubagentTools|CreateSubagentProfile|UpdateSubagentProfile|DeleteSubagentProfile|SetSubagentProfileModel|SetSubagentProfileEffort|TrySubagentProfile|CancelTrySubagentProfile)/.test(method))
930 return `skill ${method}`;
931 if (/^(MinimiseMainWindow|ToggleMaximiseMainWindow|IsMainWindowMaximised|CloseMainWindow)$/.test(method)) return `window ${method}`;
932 if (/^(OpenProjectTab|OpenGlobalTab|OpenTopicSession|EnsureBlankTab|ActivateTopic|EnsureBlankSurface|SetActiveTab|CloseTab|ReorderTabs|CreateTopic|RenameTopic|DeleteTopic|TrashTopic|RenameProject|RemoveWorkspace|SwitchWorkspace|PickWorkspace|DeliveryWorktreeAvailability|CreateDeliveryWorktree)/.test(method))
933 return `nav ${method}`;
934 return "";
935 }
936
937 function elapsedMs(startedAt: number): number {
938 const now = typeof performance !== "undefined" ? performance.now() : Date.now();
939 return Math.max(0, Math.round(now - startedAt));
940 }
941
942 export const app: AppBindings = new Proxy({} as AppBindings, {
943 get(_t, prop) {
944 const target = realApp() ?? getMock();
945 const v = (target as unknown as Record<string, unknown>)[String(prop)];
946 if (typeof v !== "function") return v;
947 return (...args: unknown[]) => {
948 const method = String(prop);
949 const crumb = bridgeBreadcrumb(method);
950 const startedAt = crumb ? (typeof performance !== "undefined" ? performance.now() : Date.now()) : 0;
951 if (crumb) addBreadcrumb("bridge", crumb);
952 try {
953 const result = (v as (...a: unknown[]) => unknown).apply(target, args);
954 if (result && typeof (result as Promise<unknown>).then === "function") {
955 return (result as Promise<unknown>).then(
956 (value) => {
957 if (crumb) addBreadcrumb("bridge", `${crumb} done ms=${elapsedMs(startedAt)}`);
958 return value;
959 },
960 (err) => {
961 if (crumb) addBreadcrumb("bridge.error", `${method} ms=${elapsedMs(startedAt)}`);
962 throw err;
963 },
964 );
965 }
966 if (crumb) addBreadcrumb("bridge", `${crumb} done ms=${elapsedMs(startedAt)}`);
967 return result;
968 } catch (err) {
969 if (crumb) addBreadcrumb("bridge.error", `${method} ms=${elapsedMs(startedAt)}`);
970 throw err;
971 }
972 };
973 },
974 });
975
976 // openExternal opens a URL in the system browser (so links in rendered markdown
977 // don't navigate the webview away from the app). Falls back to window.open in the
978 // browser dev mock.
979 export function openExternal(url: string): void {
980 if (typeof window !== "undefined" && window.runtime?.BrowserOpenURL) {
981 window.runtime.BrowserOpenURL(url);
982 } else if (typeof window !== "undefined") {
983 window.open(url, "_blank", "noopener");
984 }
985 }
986
987 // --- browser dev mock --------------------------------------------------------
988
989 const listeners = new Set<(e: WireEvent) => void>();
990 let mockScopedTabId: string | undefined;
991
992 function mockSubscribe(cb: (e: WireEvent) => void): () => void {
993 listeners.add(cb);
994 return () => {
995 listeners.delete(cb);
996 };
997 }
998
999 function emit(e: WireEvent) {
1000 const event = mockScopedTabId && !e.tabId ? { ...e, tabId: mockScopedTabId } : e;
1001 listeners.forEach((l) => l(event));
1002 }
1003
1004 export function mockToolApprovalModeAfterModeChange(current: string | undefined, nextMode: Mode): ToolApprovalMode {
1005 if (modeHasAutoApproveTools(nextMode)) return "yolo";
1006 const currentMode = normalizeToolApprovalMode(current);
1007 return currentMode === "yolo" ? "ask" : currentMode;
1008 }
1009
1010 async function withMockTabScope<T>(tabId: string, fn: () => Promise<T>): Promise<T> {
1011 const previous = mockScopedTabId;
1012 mockScopedTabId = tabId || previous;
1013 try {
1014 return await fn();
1015 } finally {
1016 mockScopedTabId = previous;
1017 }
1018 }
1019
1020 // Updater progress has its own listener set so the browser dev mock can stream a
1021 // fake download/install flow through onUpdaterProgress.
1022 const updaterListeners = new Set<(p: UpdateProgress) => void>();
1023
1024 function emitUpdater(p: UpdateProgress) {
1025 updaterListeners.forEach((l) => l(p));
1026 }
1027
1028 // Test seam for the browser-dev updater state machine. Production Wails builds
1029 // receive the same payloads through runtime.EventsOn("updater:progress").
1030 export function __emitMockUpdater(p: UpdateProgress): void {
1031 emitUpdater(p);
1032 }
1033
1034 function delay(ms: number): Promise<void> {
1035 return new Promise((r) => setTimeout(r, ms));
1036 }
1037
1038 function baseName(path: string): string {
1039 return path.replace(/[/\\]+$/, "").split(/[/\\]/).filter(Boolean).pop() ?? path;
1040 }
1041
1042 function browserPlatformOverride(): "darwin" | "windows" | "linux" | "" {
1043 if (typeof window === "undefined" || window.runtime) return "";
1044 const value = new URLSearchParams(window.location.search).get("platform");
1045 return value === "darwin" || value === "windows" || value === "linux" ? value : "";
1046 }
1047
1048 function browserPreviewBashSandboxMode(): "enforce" | "off" {
1049 return browserPlatformOverride() === "windows" ? "off" : "enforce";
1050 }
1051
1052 function browserPreviewEffectiveShell(prefer = "auto"): "bash" | "git-bash" | "powershell" | "pwsh" {
1053 const normalized = prefer.trim().toLowerCase();
1054 if (normalized === "powershell" || normalized === "pwsh") return normalized;
1055 return browserPlatformOverride() === "windows" ? "git-bash" : "bash";
1056 }
1057
1058 function mockScenario(): "demo" | "fresh" | "running" | "guidance" | "sandbox_escape" | "notice" {
1059 if (typeof window === "undefined") return "demo";
1060 const value = new URLSearchParams(window.location.search).get("mock")?.trim().toLowerCase();
1061 if (value === "fresh" || value === "empty" || value === "first-run") return "fresh";
1062 if (value === "guidance" || value === "guide" || value === "steer") return "guidance";
1063 if (value === "running" || value === "busy" || value === "streaming") return "running";
1064 if (value === "sandbox_escape" || value === "sandbox-escape" || value === "sandboxescape") return "sandbox_escape";
1065 if (value === "notice" || value === "notices" || value === "notice-preview") return "notice";
1066 return "demo";
1067 }
1068
1069 type MockProviderPresetTemplate = {
1070 id: string;
1071 label: string;
1072 description: string;
1073 keyEnv: string;
1074 provider: ProviderView;
1075 };
1076
1077 function mockProviderTemplate(p: Pick<ProviderView, "name" | "kind" | "baseUrl" | "models" | "default" | "apiKeyEnv"> & Partial<ProviderView>): ProviderView {
1078 return {
1079 name: p.name,
1080 builtIn: false,
1081 added: true,
1082 kind: p.kind,
1083 baseUrl: p.baseUrl,
1084 modelsUrl: p.modelsUrl ?? "",
1085 models: p.models,
1086 visionModels: p.visionModels ?? [],
1087 visionModelsConfigured: Boolean(p.visionModelsConfigured ?? ((p.visionModels ?? []).length > 0)),
1088 default: p.default,
1089 apiKeyEnv: p.apiKeyEnv,
1090 headers: p.headers,
1091 extraBody: p.extraBody,
1092 authHeader: p.authHeader,
1093 keySet: Boolean(p.keySet),
1094 balanceUrl: p.balanceUrl ?? "",
1095 contextWindow: p.contextWindow ?? 0,
1096 reasoningProtocol: p.reasoningProtocol ?? "",
1097 thinking: p.thinking ?? "",
1098 webSearch: Boolean(p.webSearch),
1099 supportedEfforts: p.supportedEfforts ?? [],
1100 defaultEffort: p.defaultEffort ?? "",
1101 modelOverrides: p.modelOverrides,
1102 };
1103 }
1104
1105 function mockPreset(id: string, label: string, description: string, keyEnv: string, provider: ProviderView): MockProviderPresetTemplate {
1106 return { id, label, description, keyEnv, provider };
1107 }
1108
1109 const mockKimiAPIModels = ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"];
1110 const mockLongCatModels = ["LongCat-2.0"];
1111 const mockTokenRhythmModels = ["deepseek-v4-flash", "deepseek-v4-pro", "glm-5", "glm-5.1", "minimax-m2.7", "kimi-k2.5", "kimi-k2.6", "minimax-m2.5", "mimo-v2.5-pro", "qwen3.7-max", "kimi-k2.7-code", "glm-5.2", "qwen3.8-max", "deepseek-v4-flash-0731"];
1112 const mockTokenRhythmModelOverrides = mockTokenRhythmModels.flatMap((model) => {
1113 if (model.startsWith("glm-")) return [{ model, reasoningProtocol: "glm", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" }];
1114 if (model.startsWith("deepseek-")) return [{ model, reasoningProtocol: "deepseek", supportedEfforts: model === "deepseek-v4-pro" ? ["disabled", "high", "max"] : ["disabled", "low", "high", "max"], defaultEffort: "high" }];
1115 return [];
1116 });
1117 const mockMiMoV25Models = ["mimo-v2.5-pro", "mimo-v2.5"];
1118 const mockMiniMaxModels = ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed"];
1119 const mockGLMAPIModels = ["glm-5.2", "glm-5.1", "glm-5", "glm-5-turbo", "glm-5v-turbo", "glm-4.7", "glm-4.7-flash", "glm-4.7-flashx", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4.5-flash"];
1120 const mockGLMCodingModels = ["glm-5.2", "glm-5.1", "glm-5", "glm-4.7"];
1121 const mockGLMAnthropicModels = ["glm-5.2[1m]", "glm-5.2", "glm-5.1", "glm-5", "glm-4.7", "glm-4.5-air"];
1122 const mockQwenAPIModels = ["qwen3.7-plus", "qwen3.7-max", "qwen3.6-plus", "qwen3.5-plus", "qwen3-max-2026-01-23", "qwen3-coder-next", "qwen3-coder-plus", "MiniMax-M2.5", "glm-5", "glm-4.7", "kimi-k2.5"];
1123 const mockQwenPlanModels = ["qwen3.7-plus", "qwen3.6-plus", "kimi-k2.5", "glm-5", "MiniMax-M2.5", "qwen3.5-plus", "qwen3-max-2026-01-23", "qwen3-coder-next", "qwen3-coder-plus", "glm-4.7"];
1124 const mockQwenPlanVisionModels = ["qwen3.7-plus", "qwen3.6-plus", "qwen3.5-plus", "kimi-k2.5"];
1125 const mockStepFunModels = ["step-3.7-flash", "step-3.5-flash", "step-3.5-flash-2603"];
1126 const mockOpenCodeGoModels = ["glm-5.2", "glm-5.1", "kimi-k3", "kimi-k2.7-code", "kimi-k2.6", "deepseek-v4-pro", "deepseek-v4-flash", "mimo-v2.5-pro", "mimo-v2.5"];
1127 const mockOpenCodeGoAnthropicModels = ["qwen3.7-plus", "qwen3.7-max", "qwen3.6-plus", "minimax-m3", "minimax-m2.7", "minimax-m2.5"];
1128 const mockOpenCodeZenAnthropicModels = ["claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5", "qwen3.6-plus", "qwen3.5-plus", "qwen3.6-plus-free"];
1129 const mockNovitaModels = ["zai-org/glm-5.2", "moonshotai/kimi-k2.7-code", "minimax/minimax-m3", "deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash", "qwen/qwen3.7-max", "qwen/qwen3.6-plus", "zai-org/glm-5v-turbo"];
1130 const mockGMIModels = ["zai-org/GLM-5.2-FP8", "deepseek-ai/DeepSeek-V4-Pro", "deepseek-ai/DeepSeek-V4-Flash", "moonshotai/Kimi-K2.7-Code", "anthropic/claude-sonnet-4.6", "openai/gpt-5.5"];
1131 const mockVercelModels = ["anthropic/claude-sonnet-4.6", "anthropic/claude-opus-4.8", "openai/gpt-5.4", "openai/gpt-5.4-pro", "moonshotai/kimi-k2.7-code", "zai/glm-5.2", "deepseek/deepseek-v4-pro"];
1132 const mockOllamaCloudModels = ["glm-5.2", "kimi-k2.7-code", "deepseek-v4-pro", "deepseek-v4-flash", "minimax-m3", "nemotron-3-nano:30b", "qwen3-coder-next"];
1133
1134 const mockProviderPresetTemplates: MockProviderPresetTemplate[] = [
1135 mockPreset("deepseek-responses", "DeepSeek Responses API", "DeepSeek official stateless Responses API for deepseek-v4-flash with server-side web search; search may add charges.", "DEEPSEEK_API_KEY", mockProviderTemplate({ name: "deepseek-responses", kind: "responses", baseUrl: "https://api.deepseek.com", models: ["deepseek-v4-flash"], default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", balanceUrl: "https://api.deepseek.com/user/balance", webSearch: true, contextWindow: 1000000, supportedEfforts: ["low", "high", "max"], defaultEffort: "high" })),
1136 mockPreset("deepseek-anthropic", "DeepSeek Anthropic", "Official DeepSeek Anthropic-compatible endpoint for Flash and Pro with server-side web search; search may add charges.", "DEEPSEEK_API_KEY", mockProviderTemplate({ name: "deepseek-anthropic", kind: "anthropic", baseUrl: "https://api.deepseek.com/anthropic", models: ["deepseek-v4-flash", "deepseek-v4-pro"], default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", balanceUrl: "https://api.deepseek.com/user/balance", thinking: "enabled", webSearch: true, contextWindow: 1000000, modelOverrides: [{ model: "deepseek-v4-flash", reasoningProtocol: "", supportedEfforts: ["disabled", "low", "high", "max"], defaultEffort: "high" }, { model: "deepseek-v4-pro", reasoningProtocol: "", supportedEfforts: ["disabled", "high", "max"], defaultEffort: "high" }] })),
1137 mockPreset("longcat-openai", "LongCat OpenAI", "LongCat Platform OpenAI-compatible endpoint for LongCat-2.0.", "LONGCAT_API_KEY", mockProviderTemplate({ name: "longcat-openai", kind: "openai", baseUrl: "https://api.longcat.chat/openai/v1", modelsUrl: "https://api.longcat.chat/openai/v1/models", models: mockLongCatModels, default: "LongCat-2.0", apiKeyEnv: "LONGCAT_API_KEY", contextWindow: 131072, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })),
1138 mockPreset("longcat-anthropic", "LongCat Anthropic", "LongCat Platform Anthropic-compatible Messages endpoint for LongCat-2.0.", "LONGCAT_API_KEY", mockProviderTemplate({ name: "longcat-anthropic", kind: "anthropic", baseUrl: "https://api.longcat.chat/anthropic", modelsUrl: "https://api.longcat.chat/anthropic/v1/models", models: mockLongCatModels, default: "LongCat-2.0", apiKeyEnv: "LONGCAT_API_KEY", authHeader: true, contextWindow: 131072, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })),
1139 mockPreset("token-rhythm", "Token Rhythm", "Token Rhythm (基元律动) multi-model OpenAI-compatible gateway.", "TOKEN_RHYTHM_API_KEY", mockProviderTemplate({ name: "token-rhythm", kind: "openai", baseUrl: "https://tokenrhythm.studio/v1", modelsUrl: "https://tokenrhythm.studio/v1/models", models: mockTokenRhythmModels, visionModels: ["kimi-k2.5", "kimi-k2.6", "kimi-k2.7-code"], default: "deepseek-v4-flash", apiKeyEnv: "TOKEN_RHYTHM_API_KEY", contextWindow: 1000000, modelOverrides: mockTokenRhythmModelOverrides })),
1140 mockPreset("kimi-cn", "Kimi CN API", "Moonshot Kimi China OpenAI-compatible API.", "KIMI_API_KEY", mockProviderTemplate({ name: "kimi-cn", kind: "openai", baseUrl: "https://api.moonshot.cn/v1", models: mockKimiAPIModels, visionModels: mockKimiAPIModels, default: "kimi-k2.7-code", apiKeyEnv: "KIMI_API_KEY", balanceUrl: "https://api.moonshot.cn/v1/users/me/balance", contextWindow: 262144, reasoningProtocol: "none", modelOverrides: [{ model: "kimi-k3", reasoningProtocol: "openai", supportedEfforts: ["low", "high", "max"], defaultEffort: "max", contextWindow: 1048576 }] })),
1141 mockPreset("kimi-global", "Kimi Global API", "Moonshot Kimi international OpenAI-compatible API.", "MOONSHOT_API_KEY", mockProviderTemplate({ name: "kimi-global", kind: "openai", baseUrl: "https://api.moonshot.ai/v1", models: mockKimiAPIModels, visionModels: mockKimiAPIModels, default: "kimi-k2.7-code", apiKeyEnv: "MOONSHOT_API_KEY", balanceUrl: "https://api.moonshot.ai/v1/users/me/balance", contextWindow: 262144, reasoningProtocol: "none", modelOverrides: [{ model: "kimi-k3", reasoningProtocol: "openai", supportedEfforts: ["low", "high", "max"], defaultEffort: "max", contextWindow: 1048576 }] })),
1142 mockPreset("kimi-coding-plan", "Kimi Coding Plan", "Kimi Coding Plan via its dedicated Anthropic-compatible endpoint.", "KIMI_CODING_API_KEY", mockProviderTemplate({ name: "kimi-coding-plan", kind: "anthropic", baseUrl: "https://api.kimi.com/coding/", models: ["kimi-for-coding"], visionModels: ["kimi-for-coding"], default: "kimi-for-coding", apiKeyEnv: "KIMI_CODING_API_KEY", headers: { "User-Agent": "claude-code/0.1.0" }, thinking: "adaptive", contextWindow: 262144 })),
1143 mockPreset("mimo-api", "MiMo API", "Xiaomi MiMo direct API with text and vision-capable models.", "MIMO_API_KEY", mockProviderTemplate({ name: "mimo-api", kind: "openai", baseUrl: "https://api.xiaomimimo.com/v1", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_API_KEY", contextWindow: 1048576 })),
1144 mockPreset("mimo-anthropic", "MiMo Anthropic", "Xiaomi MiMo direct Anthropic-compatible endpoint.", "MIMO_API_KEY", mockProviderTemplate({ name: "mimo-anthropic", kind: "anthropic", baseUrl: "https://api.xiaomimimo.com/anthropic", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_API_KEY", thinking: "adaptive", contextWindow: 1048576 })),
1145 mockPreset("mimo-token-plan-cn", "MiMo Token Plan CN", "Xiaomi MiMo token-plan China endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-cn", kind: "openai", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", contextWindow: 1048576 })),
1146 mockPreset("mimo-token-plan-cn-anthropic", "MiMo Token Plan CN Anthropic", "Xiaomi MiMo token-plan China Anthropic-compatible endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-cn-anthropic", kind: "anthropic", baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", thinking: "adaptive", contextWindow: 1048576 })),
1147 mockPreset("mimo-token-plan-sgp", "MiMo Token Plan SGP", "Xiaomi MiMo token-plan Singapore endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-sgp", kind: "openai", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", contextWindow: 1048576 })),
1148 mockPreset("mimo-token-plan-sgp-anthropic", "MiMo Token Plan SGP Anthropic", "Xiaomi MiMo token-plan Singapore Anthropic-compatible endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-sgp-anthropic", kind: "anthropic", baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", thinking: "adaptive", contextWindow: 1048576 })),
1149 mockPreset("mimo-token-plan-ams", "MiMo Token Plan AMS", "Xiaomi MiMo token-plan Amsterdam endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-ams", kind: "openai", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", contextWindow: 1048576 })),
1150 mockPreset("mimo-token-plan-ams-anthropic", "MiMo Token Plan AMS Anthropic", "Xiaomi MiMo token-plan Amsterdam Anthropic-compatible endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-ams-anthropic", kind: "anthropic", baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", thinking: "adaptive", contextWindow: 1048576 })),
1151 mockPreset("minimax-cn-api", "MiniMax CN API", "MiniMax China OpenAI-compatible M-series API endpoint.", "MINIMAX_API_KEY", mockProviderTemplate({ name: "minimax-cn-api", kind: "openai", baseUrl: "https://api.minimaxi.com/v1", models: mockMiniMaxModels, visionModels: ["MiniMax-M3"], default: "MiniMax-M3", apiKeyEnv: "MINIMAX_API_KEY", extraBody: { reasoning_split: true }, contextWindow: 1048576, thinking: "adaptive", supportedEfforts: ["disabled", "adaptive"], defaultEffort: "adaptive" })),
1152 mockPreset("minimax-global-api", "MiniMax Global API", "MiniMax international OpenAI-compatible M-series API endpoint.", "MINIMAX_API_KEY", mockProviderTemplate({ name: "minimax-global-api", kind: "openai", baseUrl: "https://api.minimax.io/v1", models: mockMiniMaxModels, visionModels: ["MiniMax-M3"], default: "MiniMax-M3", apiKeyEnv: "MINIMAX_API_KEY", extraBody: { reasoning_split: true }, contextWindow: 1048576, thinking: "adaptive", supportedEfforts: ["disabled", "adaptive"], defaultEffort: "adaptive" })),
1153 mockPreset("minimax-cn-anthropic", "MiniMax CN Anthropic", "MiniMax China Anthropic-compatible M-series endpoint.", "MINIMAX_PLAN_API_KEY", mockProviderTemplate({ name: "minimax-cn-anthropic", kind: "anthropic", baseUrl: "https://api.minimaxi.com/anthropic", models: mockMiniMaxModels, visionModels: ["MiniMax-M3"], default: "MiniMax-M3", apiKeyEnv: "MINIMAX_PLAN_API_KEY", authHeader: true, contextWindow: 1048576, thinking: "adaptive", supportedEfforts: ["disabled", "adaptive"], defaultEffort: "adaptive" })),
1154 mockPreset("minimax-global-anthropic", "MiniMax Global Anthropic", "MiniMax international Anthropic-compatible endpoint with Bearer auth.", "MINIMAX_API_KEY", mockProviderTemplate({ name: "minimax-global-anthropic", kind: "anthropic", baseUrl: "https://api.minimax.io/anthropic", models: mockMiniMaxModels, visionModels: ["MiniMax-M3"], default: "MiniMax-M3", apiKeyEnv: "MINIMAX_API_KEY", authHeader: true, contextWindow: 1048576, thinking: "adaptive", supportedEfforts: ["disabled", "adaptive"], defaultEffort: "adaptive" })),
1155 mockPreset("glm-cn", "GLM CN API", "Zhipu GLM China OpenAI-compatible API with thinking controls.", "GLM_API_KEY", mockProviderTemplate({ name: "glm-cn", kind: "openai", baseUrl: "https://open.bigmodel.cn/api/paas/v4", models: mockGLMAPIModels, visionModels: ["glm-5v-turbo"], default: "glm-5.2", apiKeyEnv: "GLM_API_KEY", contextWindow: 1000000, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })),
1156 mockPreset("zai-global", "Z.AI Global API", "Z.AI international OpenAI-compatible GLM API.", "ZAI_API_KEY", mockProviderTemplate({ name: "zai-global", kind: "openai", baseUrl: "https://api.z.ai/api/paas/v4", models: mockGLMAPIModels, visionModels: ["glm-5v-turbo"], default: "glm-5.2", apiKeyEnv: "ZAI_API_KEY", contextWindow: 1000000, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })),
1157 mockPreset("glm-coding-plan-cn", "GLM Coding Plan CN", "Zhipu GLM China coding-plan endpoint.", "GLM_PLAN_API_KEY", mockProviderTemplate({ name: "glm-coding-plan-cn", kind: "openai", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", models: mockGLMCodingModels, default: "glm-5.2", apiKeyEnv: "GLM_PLAN_API_KEY", contextWindow: 1000000, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })),
1158 mockPreset("glm-coding-plan-cn-anthropic", "GLM Coding Plan CN Anthropic", "Zhipu GLM China coding-plan Anthropic-compatible endpoint.", "GLM_PLAN_API_KEY", mockProviderTemplate({ name: "glm-coding-plan-cn-anthropic", kind: "anthropic", baseUrl: "https://open.bigmodel.cn/api/anthropic", models: mockGLMAnthropicModels, default: "glm-5.2", apiKeyEnv: "GLM_PLAN_API_KEY", authHeader: true, thinking: "adaptive", contextWindow: 1000000 })),
1159 mockPreset("zai-coding-plan-global", "Z.AI Coding Plan Global", "Z.AI international coding-plan endpoint.", "ZAI_CODING_API_KEY", mockProviderTemplate({ name: "zai-coding-plan-global", kind: "openai", baseUrl: "https://api.z.ai/api/coding/paas/v4", models: mockGLMCodingModels, default: "glm-5.2", apiKeyEnv: "ZAI_CODING_API_KEY", contextWindow: 1000000, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })),
1160 mockPreset("zai-coding-plan-global-anthropic", "Z.AI Coding Plan Global Anthropic", "Z.AI international coding-plan Anthropic-compatible endpoint.", "ZAI_CODING_API_KEY", mockProviderTemplate({ name: "zai-coding-plan-global-anthropic", kind: "anthropic", baseUrl: "https://api.z.ai/api/anthropic", models: mockGLMAnthropicModels, default: "glm-5.2", apiKeyEnv: "ZAI_CODING_API_KEY", authHeader: true, thinking: "adaptive", contextWindow: 1000000 })),
1161 mockPreset("opencode-go", "OpenCode Go", "OpenCode Go relay with per-model capability overrides.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go", kind: "openai", baseUrl: "https://opencode.ai/zen/go/v1", models: mockOpenCodeGoModels, visionModels: ["kimi-k3"], default: "glm-5.2", apiKeyEnv: "OPENCODE_GO_API_KEY", contextWindow: 128000, modelOverrides: [{ model: "kimi-k3", reasoningProtocol: "openai", supportedEfforts: ["high", "max"], defaultEffort: "max", contextWindow: 1048576 }] })),
1162 mockPreset("opencode-go-anthropic", "OpenCode Go Anthropic", "OpenCode Go subscription Anthropic-compatible route for Qwen and MiniMax models.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen/go", models: mockOpenCodeGoAnthropicModels, visionModels: ["qwen3.7-plus", "qwen3.6-plus"], default: "qwen3.7-plus", apiKeyEnv: "OPENCODE_GO_API_KEY", thinking: "adaptive", contextWindow: 262144 })),
1163 mockPreset("opencode-zen-anthropic", "OpenCode Zen Anthropic", "OpenCode Zen Anthropic-compatible route for Claude and Qwen models.", "OPENCODE_API_KEY", mockProviderTemplate({ name: "opencode-zen-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen", models: mockOpenCodeZenAnthropicModels, visionModels: ["claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5"], default: "claude-sonnet-4-6", apiKeyEnv: "OPENCODE_API_KEY", contextWindow: 262144 })),
1164 mockPreset("qwen-cn", "Qwen CN API", "Alibaba DashScope China standard OpenAI-compatible endpoint.", "QWEN_API_KEY", mockProviderTemplate({ name: "qwen-cn", kind: "openai", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", models: mockQwenAPIModels, visionModels: ["qwen3.7-plus", "qwen3.6-plus", "qwen3.5-plus", "kimi-k2.5"], default: "qwen3.7-plus", apiKeyEnv: "QWEN_API_KEY" })),
1165 mockPreset("qwen-global", "Qwen Global API", "Alibaba DashScope international standard OpenAI-compatible endpoint.", "QWEN_API_KEY", mockProviderTemplate({ name: "qwen-global", kind: "openai", baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", models: mockQwenAPIModels, visionModels: ["qwen3.7-plus", "qwen3.6-plus", "qwen3.5-plus", "kimi-k2.5"], default: "qwen3.7-plus", apiKeyEnv: "QWEN_API_KEY" })),
1166 mockPreset("qwen-coding-plan-cn", "Qwen Coding Plan CN", "Alibaba Cloud Qwen Coding Plan China endpoint.", "QWEN_CODING_API_KEY", mockProviderTemplate({ name: "qwen-coding-plan-cn", kind: "openai", baseUrl: "https://coding.dashscope.aliyuncs.com/v1", models: mockQwenPlanModels, visionModels: mockQwenPlanVisionModels, default: "qwen3.7-plus", apiKeyEnv: "QWEN_CODING_API_KEY" })),
1167 mockPreset("qwen-coding-plan-cn-anthropic", "Qwen Coding Plan CN Anthropic", "Alibaba Cloud Qwen Coding Plan China Anthropic-compatible endpoint.", "QWEN_CODING_API_KEY", mockProviderTemplate({ name: "qwen-coding-plan-cn-anthropic", kind: "anthropic", baseUrl: "https://coding.dashscope.aliyuncs.com/apps/anthropic", models: mockQwenPlanModels, visionModels: mockQwenPlanVisionModels, default: "qwen3.7-plus", apiKeyEnv: "QWEN_CODING_API_KEY", thinking: "adaptive" })),
1168 mockPreset("qwen-coding-plan-global", "Qwen Coding Plan Global", "Alibaba Cloud Qwen Coding Plan international endpoint.", "QWEN_CODING_API_KEY", mockProviderTemplate({ name: "qwen-coding-plan-global", kind: "openai", baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", models: mockQwenPlanModels, visionModels: mockQwenPlanVisionModels, default: "qwen3.7-plus", apiKeyEnv: "QWEN_CODING_API_KEY" })),
1169 mockPreset("qwen-coding-plan-global-anthropic", "Qwen Coding Plan Global Anthropic", "Alibaba Cloud Qwen Coding Plan international Anthropic-compatible endpoint.", "QWEN_CODING_API_KEY", mockProviderTemplate({ name: "qwen-coding-plan-global-anthropic", kind: "anthropic", baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic", models: mockQwenPlanModels, visionModels: mockQwenPlanVisionModels, default: "qwen3.7-plus", apiKeyEnv: "QWEN_CODING_API_KEY", thinking: "adaptive" })),
1170 mockPreset("stepfun", "StepFun", "StepFun coding-plan OpenAI-compatible endpoint.", "STEPFUN_API_KEY", mockProviderTemplate({ name: "stepfun", kind: "openai", baseUrl: "https://api.stepfun.com/step_plan/v1", models: mockStepFunModels, default: "step-3.7-flash", apiKeyEnv: "STEPFUN_API_KEY", supportedEfforts: ["low", "medium", "high"], defaultEffort: "medium" })),
1171 mockPreset("stepfun-anthropic", "StepFun Anthropic", "StepFun coding-plan Anthropic-compatible endpoint.", "STEPFUN_API_KEY", mockProviderTemplate({ name: "stepfun-anthropic", kind: "anthropic", baseUrl: "https://api.stepfun.com/step_plan", models: mockStepFunModels, default: "step-3.7-flash", apiKeyEnv: "STEPFUN_API_KEY", thinking: "adaptive", supportedEfforts: ["low", "medium", "high"], defaultEffort: "medium" })),
1172 mockPreset("novita", "NovitaAI", "NovitaAI OpenAI-compatible multi-model gateway.", "NOVITA_API_KEY", mockProviderTemplate({ name: "novita", kind: "openai", baseUrl: "https://api.novita.ai/openai/v1", models: mockNovitaModels, default: "zai-org/glm-5.2", apiKeyEnv: "NOVITA_API_KEY" })),
1173 mockPreset("gmi", "GMI Cloud", "GMI Cloud direct multi-model OpenAI-compatible gateway.", "GMI_API_KEY", mockProviderTemplate({ name: "gmi", kind: "openai", baseUrl: "https://api.gmi-serving.com/v1", models: mockGMIModels, default: "zai-org/GLM-5.2-FP8", apiKeyEnv: "GMI_API_KEY", headers: { "User-Agent": "Reasonix" } })),
1174 mockPreset("vercel-ai-gateway", "Vercel AI Gateway", "Vercel AI Gateway via Anthropic-compatible Messages API.", "AI_GATEWAY_API_KEY", mockProviderTemplate({ name: "vercel-ai-gateway", kind: "anthropic", baseUrl: "https://ai-gateway.vercel.sh", models: mockVercelModels, visionModels: ["anthropic/claude-sonnet-4.6", "anthropic/claude-opus-4.8", "openai/gpt-5.4", "openai/gpt-5.4-pro", "moonshotai/kimi-k2.7-code"], default: "anthropic/claude-sonnet-4.6", apiKeyEnv: "AI_GATEWAY_API_KEY", authHeader: true, contextWindow: 1000000 })),
1175 mockPreset("huggingface", "HuggingFace Router", "HuggingFace Inference Router OpenAI-compatible endpoint.", "HF_TOKEN", mockProviderTemplate({ name: "huggingface", kind: "openai", baseUrl: "https://router.huggingface.co/v1", models: ["zai-org/GLM-5.2", "deepseek-ai/DeepSeek-V3.2", "Qwen/Qwen3.5-72B-Instruct"], default: "zai-org/GLM-5.2", apiKeyEnv: "HF_TOKEN" })),
1176 mockPreset("nvidia", "NVIDIA NIM", "NVIDIA NIM OpenAI-compatible accelerated inference endpoint.", "NVIDIA_API_KEY", mockProviderTemplate({ name: "nvidia", kind: "openai", baseUrl: "https://integrate.api.nvidia.com/v1", models: ["nvidia/nemotron-3-nano-30b-a3b", "nvidia/nemotron-3-super-120b-a12b", "nvidia/nemotron-3-ultra-550b-a55b", "deepseek-ai/deepseek-v4-pro", "qwen/qwen3.5-397b-a17b"], default: "nvidia/nemotron-3-nano-30b-a3b", apiKeyEnv: "NVIDIA_API_KEY" })),
1177 mockPreset("kilocode", "KiloCode", "Kilo Code gateway OpenAI-compatible endpoint.", "KILOCODE_API_KEY", mockProviderTemplate({ name: "kilocode", kind: "openai", baseUrl: "https://api.kilo.ai/api/gateway", models: ["kilo/auto"], default: "kilo/auto", apiKeyEnv: "KILOCODE_API_KEY" })),
1178 mockPreset("ollama-cloud", "Ollama Cloud", "Hosted Ollama Cloud OpenAI-compatible endpoint with max reasoning effort.", "OLLAMA_API_KEY", mockProviderTemplate({ name: "ollama-cloud", kind: "openai", baseUrl: "https://ollama.com/v1", models: mockOllamaCloudModels, default: "glm-5.2", apiKeyEnv: "OLLAMA_API_KEY" })),
1179 ];
1180
1181 function mockProviderPresetViews(): ProviderPresetView[] {
1182 return [...mockProviderPresetTemplates].sort((a, b) => mockProviderPresetDisplayRank(a.id) - mockProviderPresetDisplayRank(b.id)).map((template) => ({
1183 id: template.id,
1184 label: template.label,
1185 description: template.description,
1186 keyEnv: template.keyEnv,
1187 providerNames: [template.provider.name],
1188 models: [...template.provider.models],
1189 added: false,
1190 status: "available",
1191 statusProviderNames: [],
1192 keySet: false,
1193 requiresKey: true,
1194 configured: false,
1195 }));
1196 }
1197
1198 function mockProviderPresetDisplayRank(id: string): number {
1199 if (id === "deepseek-responses") return -1;
1200 if (id === "deepseek-anthropic") return 0;
1201 if (id === "glm-cn" || id === "zai-global" || id.startsWith("glm-coding-plan-") || id.startsWith("zai-coding-plan-")) return 0;
1202 if (id.startsWith("longcat-")) return 1;
1203 if (id === "token-rhythm") return 1;
1204 if (id.startsWith("kimi-")) return 2;
1205 if (id.startsWith("minimax-")) return 3;
1206 return 4;
1207 }
1208
1209 function cloneMockProviderTemplate(id: string, key: string): ProviderView | undefined {
1210 const template = mockProviderPresetTemplates.find((candidate) => candidate.id === id);
1211 if (!template) return undefined;
1212 return {
1213 ...JSON.parse(JSON.stringify(template.provider)) as ProviderView,
1214 keySet: Boolean(key.trim()),
1215 };
1216 }
1217
1218 const mockPreviewImageDataURL =
1219 "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='120' viewBox='0 0 160 120'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23f97316'/%3E%3Cstop offset='1' stop-color='%232563eb'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='160' height='120' rx='14' fill='url(%23g)'/%3E%3Ccircle cx='44' cy='38' r='16' fill='%23fff7ed' opacity='.9'/%3E%3Cpath d='M18 96 62 58l24 22 18-16 38 32z' fill='%23ffffff' opacity='.9'/%3E%3C/svg%3E";
1220
1221 function mockExternalOpenerIconDataURL(color: string, label: string): string {
1222 const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="${color}"/><text x="32" y="40" text-anchor="middle" font-family="system-ui" font-size="25" font-weight="700" fill="white">${label}</text></svg>`;
1223 return `data:image/svg+xml,${encodeURIComponent(svg)}`;
1224 }
1225
1226 function makeMockApp(): AppBindings {
1227 const scenario = mockScenario();
1228 const freshMock = scenario === "fresh";
1229 const guidanceMock = scenario === "guidance";
1230 const runningMock = scenario === "running" || guidanceMock;
1231 const sandboxEscapeMock = scenario === "sandbox_escape";
1232 const noticePreviewMock = scenario === "notice";
1233 const mockAttachmentDataURLs = new Map<string, string>();
1234 let cancelled = false;
1235 let pendingAskPreview = false;
1236 let pendingApprovalPreview = false;
1237 // Mirrors the last emitted approval preview so mode switches can mirror the
1238 // backend drain contract: only non-fresh tools auto-allow; plan/sandbox
1239 // escape prompts stay pending and visible.
1240 let pendingApprovalPreviewPrompt: { id: string; tool: string } | undefined;
1241 const globalWorkspaceRoot = "~/Library/Application Support/reasonix/global-workspace";
1242 let cwd = freshMock ? globalWorkspaceRoot : "~/projects/joyquant-db"; // mutable so PickWorkspace is visible in dev
1243 let workspaces = freshMock ? [] : ["~/projects/joyquant-db", "~/projects/joyquant-sys", "~/projects/reasonix", "~/projects/blade"];
1244 let mockEffort = "auto";
1245 let mockDesktopZoomFactor = 1.0;
1246 let mockActiveThemeId = "";
1247 let mockBaseStyle = "graphite";
1248 let mockThemeMode: "auto" | "light" | "dark" = "dark";
1249 // Vite rewrites these literal asset URLs in both dev and production builds.
1250 // Keeping them on the browser mock makes local visual acceptance match the
1251 // Wails bridge, whose ListThemePacks response carries the same two URLs.
1252 const mockOfficialThemeAssets = {
1253 "official-rose-dawn": {
1254 previewUrl: new URL("../../../themes/official/official-rose-dawn/preview.webp", import.meta.url).href,
1255 backgroundUrl: new URL("../../../themes/official/official-rose-dawn/background.webp", import.meta.url).href,
1256 },
1257 "official-fortune-forge": {
1258 previewUrl: new URL("../../../themes/official/official-fortune-forge/preview.webp", import.meta.url).href,
1259 backgroundUrl: new URL("../../../themes/official/official-fortune-forge/background.webp", import.meta.url).href,
1260 },
1261 "official-crimson-horizon": {
1262 previewUrl: new URL("../../../themes/official/official-crimson-horizon/preview.webp", import.meta.url).href,
1263 backgroundUrl: new URL("../../../themes/official/official-crimson-horizon/background.webp", import.meta.url).href,
1264 },
1265 "official-sage-breeze": {
1266 previewUrl: new URL("../../../themes/official/official-sage-breeze/preview.webp", import.meta.url).href,
1267 backgroundUrl: new URL("../../../themes/official/official-sage-breeze/background.webp", import.meta.url).href,
1268 },
1269 "official-spark-notebook": {
1270 previewUrl: new URL("../../../themes/official/official-spark-notebook/preview.webp", import.meta.url).href,
1271 backgroundUrl: new URL("../../../themes/official/official-spark-notebook/background.webp", import.meta.url).href,
1272 },
1273 "official-violet-starlight": {
1274 previewUrl: new URL("../../../themes/official/official-violet-starlight/preview.webp", import.meta.url).href,
1275 backgroundUrl: new URL("../../../themes/official/official-violet-starlight/background.webp", import.meta.url).href,
1276 },
1277 "official-cyan-stage": {
1278 previewUrl: new URL("../../../themes/official/official-cyan-stage/preview.webp", import.meta.url).href,
1279 backgroundUrl: new URL("../../../themes/official/official-cyan-stage/background.webp", import.meta.url).href,
1280 },
1281 "official-noir-gold": {
1282 previewUrl: new URL("../../../themes/official/official-noir-gold/preview.webp", import.meta.url).href,
1283 backgroundUrl: new URL("../../../themes/official/official-noir-gold/background.webp", import.meta.url).href,
1284 },
1285 } as const;
1286 registerTrustedThemeBackgroundURLs(Object.values(mockOfficialThemeAssets).map((asset) => asset.backgroundUrl));
1287 let mockThemePacks: import("./themePack").ThemePackView[] = [
1288 { id: "graphite", name: "Graphite", author: "Reasonix", baseStyle: "graphite", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1289 { id: "aurora", name: "Aurora", author: "Reasonix", baseStyle: "aurora", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1290 { id: "slate", name: "Slate", author: "Reasonix", baseStyle: "slate", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1291 { id: "carbon", name: "Carbon", author: "Reasonix", baseStyle: "carbon", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1292 { id: "nocturne", name: "Nocturne", author: "Reasonix", baseStyle: "nocturne", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1293 { id: "amber", name: "Amber", author: "Reasonix", baseStyle: "amber", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1294 { ...mockOfficialThemeAssets["official-rose-dawn"], id: "official-rose-dawn", name: "Rose Dawn", author: "Reasonix Contributors", license: "MIT", baseStyle: "graphite", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-rose-dawn.name", descriptionKey: "settings.themes.official.official-rose-dawn.description", tokens: { light: { bg: "#FFF7F8", fg: "#3A252C", accent: "#B43F65" }, dark: { bg: "#1E1419", fg: "#FFF3F6", accent: "#E26D91" } }, recipes: { density: "comfortable", corners: "round" }, background: { focusX: 0.72, focusY: 0.43, safeArea: "left", homeOpacity: 1, taskOpacity: 0.2, overlayStrength: 0.68, paneOpacity: 0.50 } },
1295 { ...mockOfficialThemeAssets["official-fortune-forge"], id: "official-fortune-forge", name: "Fortune Forge", author: "Reasonix Contributors", license: "MIT", baseStyle: "amber", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-fortune-forge.name", descriptionKey: "settings.themes.official.official-fortune-forge.description", tokens: { light: { bg: "#FFF8E8", fg: "#382116", accent: "#A92D22" }, dark: { bg: "#1D140D", fg: "#FFF2D1", accent: "#E8AD38" } }, recipes: { density: "comfortable", corners: "soft" }, background: { focusX: 0.74, focusY: 0.44, safeArea: "left", homeOpacity: 1, taskOpacity: 0.2, overlayStrength: 0.7, paneOpacity: 0.50 } },
1296 { ...mockOfficialThemeAssets["official-crimson-horizon"], id: "official-crimson-horizon", name: "Crimson Horizon", author: "Reasonix Contributors", license: "MIT", baseStyle: "graphite", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-crimson-horizon.name", descriptionKey: "settings.themes.official.official-crimson-horizon.description", tokens: { light: { bg: "#FFF8F7", fg: "#301D1D", accent: "#B92B38" }, dark: { bg: "#190D11", fg: "#FFF1F2", accent: "#FF6772" } }, recipes: { density: "comfortable", corners: "soft" }, background: { focusX: 0.75, focusY: 0.45, safeArea: "left", homeOpacity: 0.98, taskOpacity: 0.22, overlayStrength: 0.66, paneOpacity: 0.50 } },
1297 { ...mockOfficialThemeAssets["official-sage-breeze"], id: "official-sage-breeze", name: "Sage Breeze", author: "Reasonix Contributors", license: "MIT", baseStyle: "slate", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-sage-breeze.name", descriptionKey: "settings.themes.official.official-sage-breeze.description", tokens: { light: { bg: "#F7F7EF", fg: "#26332D", accent: "#47735F" }, dark: { bg: "#101814", fg: "#EEF6F0", accent: "#84CBA7" } }, recipes: { density: "comfortable", corners: "soft" }, background: { focusX: 0.73, focusY: 0.44, safeArea: "left", homeOpacity: 1, taskOpacity: 0.2, overlayStrength: 0.68, paneOpacity: 0.50 } },
1298 { ...mockOfficialThemeAssets["official-spark-notebook"], id: "official-spark-notebook", name: "Spark Notebook", author: "Reasonix Contributors", license: "MIT", baseStyle: "aurora", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-spark-notebook.name", descriptionKey: "settings.themes.official.official-spark-notebook.description", tokens: { light: { bg: "#FFF9ED", fg: "#2B2F35", accent: "#007B78" }, dark: { bg: "#14171A", fg: "#F8F5E9", accent: "#42D1C6" } }, recipes: { density: "comfortable", corners: "round" }, background: { focusX: 0.74, focusY: 0.46, safeArea: "left", homeOpacity: 0.98, taskOpacity: 0.2, overlayStrength: 0.68, paneOpacity: 0.50 } },
1299 { ...mockOfficialThemeAssets["official-violet-starlight"], id: "official-violet-starlight", name: "Violet Starlight", author: "Reasonix Contributors", license: "MIT", baseStyle: "nocturne", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-violet-starlight.name", descriptionKey: "settings.themes.official.official-violet-starlight.description", tokens: { light: { bg: "#F7F4FF", fg: "#251F3C", accent: "#6242C7" }, dark: { bg: "#0C1022", fg: "#F4F2FF", accent: "#9B86FF" } }, recipes: { density: "comfortable", corners: "round" }, background: { focusX: 0.73, focusY: 0.44, safeArea: "left", homeOpacity: 0.96, taskOpacity: 0.18, overlayStrength: 0.72, paneOpacity: 0.50 } },
1300 { ...mockOfficialThemeAssets["official-cyan-stage"], id: "official-cyan-stage", name: "Cyan Stage", author: "Reasonix Contributors", license: "MIT", baseStyle: "carbon", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-cyan-stage.name", descriptionKey: "settings.themes.official.official-cyan-stage.description", tokens: { light: { bg: "#F1FCFD", fg: "#173238", accent: "#007C92" }, dark: { bg: "#07181D", fg: "#E9FCFF", accent: "#37D7E4" } }, recipes: { density: "comfortable", corners: "round" }, background: { focusX: 0.74, focusY: 0.45, safeArea: "left", homeOpacity: 0.96, taskOpacity: 0.18, overlayStrength: 0.72, paneOpacity: 0.50 } },
1301 { ...mockOfficialThemeAssets["official-noir-gold"], id: "official-noir-gold", name: "Noir Gold", author: "Reasonix Contributors", license: "MIT", baseStyle: "carbon", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-noir-gold.name", descriptionKey: "settings.themes.official.official-noir-gold.description", tokens: { light: { bg: "#FCF8EE", fg: "#2A241B", accent: "#7A5A16" }, dark: { bg: "#0D0B09", fg: "#F8F1DF", accent: "#D9B45B" } }, recipes: { density: "comfortable", corners: "soft" }, background: { focusX: 0.73, focusY: 0.43, safeArea: "left", homeOpacity: 0.94, taskOpacity: 0.18, overlayStrength: 0.74, paneOpacity: 0.50 } },
1302 ];
1303 const day = 86_400_000;
1304 const t0 = Date.now();
1305 // Mutable so MCP add/remove/retry are observable in browser dev.
1306 let capServers: ServerView[] = [
1307 {
1308 name: "project-knowledge",
1309 transport: "http",
1310 status: "connected",
1311 configured: true,
1312 autoStart: true,
1313 tier: "background",
1314 source: "project",
1315 configSource: "reasonix.toml",
1316 url: "https://mcp.example.test/project",
1317 tools: 3,
1318 prompts: 0,
1319 resources: 1,
1320 toolList: [
1321 { name: "search_knowledge", description: "Search the project knowledge base.", readOnlyHint: true },
1322 { name: "get_document", description: "Read a knowledge-base document.", readOnlyHint: true },
1323 { name: "list_topics", description: "List available knowledge topics.", readOnlyHint: true },
1324 ],
1325 },
1326 {
1327 name: "github",
1328 transport: "stdio",
1329 status: "connected",
1330 configured: true,
1331 autoStart: true,
1332 tier: "background",
1333 command: "npx",
1334 args: ["-y", "@modelcontextprotocol/server-github"],
1335 tools: 4,
1336 prompts: 2,
1337 resources: 0,
1338 toolList: [
1339 { name: "issue_read", description: "Read GitHub issue details and comments.", readOnlyHint: true },
1340 { name: "pull_request_read", description: "Read pull request metadata, files, and review threads.", readOnlyHint: true },
1341 { name: "search_issues", description: "Search issues and pull requests.", readOnlyHint: true },
1342 { name: "issue_write", description: "Create or update GitHub issues." },
1343 ],
1344 },
1345 {
1346 name: "linear",
1347 transport: "http",
1348 status: "initializing",
1349 configured: true,
1350 autoStart: true,
1351 tier: "background",
1352 url: "https://mcp.linear.app/mcp",
1353 authStatus: "possible",
1354 authUrl: "https://mcp.linear.app/mcp",
1355 tools: 8,
1356 prompts: 0,
1357 resources: 0,
1358 toolList: [
1359 { name: "list_issues", description: "List and filter Linear issues." },
1360 { name: "get_issue", description: "Fetch a Linear issue by id or key." },
1361 { name: "create_issue", description: "Create a Linear issue." },
1362 { name: "update_issue", description: "Update status, assignee, priority, or labels." },
1363 { name: "list_projects", description: "List Linear projects." },
1364 { name: "get_project", description: "Fetch project details." },
1365 { name: "list_teams", description: "List Linear teams." },
1366 { name: "search", description: "Search Linear workspace objects." },
1367 ],
1368 },
1369 { name: "figma", transport: "http", status: "failed", configured: true, autoStart: true, tier: "background", url: "https://mcp.figma.com/mcp", authStatus: "required", authUrl: "https://mcp.figma.com/mcp", tools: 0, prompts: 0, resources: 0, error: "connect: 401 unauthorized" },
1370 ];
1371 const capSkills: SkillView[] = [
1372 {
1373 name: "explore", description: "Investigate the codebase in an isolated subagent", scope: "builtin", runAs: "subagent", enabled: true,
1374 allowedTools: ["read_file", "ls", "glob", "grep", "code_index"], invocation: "/explore", invocationMode: "auto",
1375 configuredModel: "deepseek/deepseek-v4-pro", configuredEffort: "high",
1376 },
1377 { name: "research", description: "Combine web_fetch + code reading in an isolated subagent", scope: "builtin", runAs: "subagent", enabled: true, allowedTools: ["read_file", "ls", "glob", "grep", "code_index", "web_fetch"], invocation: "/research", invocationMode: "auto" },
1378 { name: "review", description: "Review the staged diff", scope: "project", runAs: "inline", enabled: false, invocation: "/review" },
1379 { name: "init", description: "Scaffold a REASONIX.md for this repo", scope: "builtin", runAs: "inline", enabled: true, invocation: "/init" },
1380 {
1381 name: "my-formatter", description: "Formats code the way I like it", scope: "global", runAs: "subagent", enabled: true,
1382 model: "deepseek-pro", effort: "high", allowedTools: ["read_file", "edit_file"], color: "amber", invocation: "/my-formatter", invocationMode: "manual",
1383 body: "You are a code formatting assistant. Reformat the given file to match project style without changing behavior.",
1384 },
1385 ];
1386 let capSkillRoots: SkillRootView[] = [
1387 { dir: "~/projects/reasonix/.reasonix/skills", scope: "project", priority: 1, status: "missing", configured: false, removable: true, skills: 0 },
1388 {
1389 dir: "~/my-skills",
1390 scope: "custom",
1391 priority: 5,
1392 status: "ok",
1393 configured: true,
1394 removable: true,
1395 skills: 1,
1396 skillItems: [{ name: "review", description: "Review the staged diff", scope: "custom", runAs: "inline" }],
1397 },
1398 {
1399 dir: "~/.reasonix/skills",
1400 scope: "global",
1401 priority: 6,
1402 status: "ok",
1403 configured: false,
1404 removable: true,
1405 skills: 2,
1406 skillItems: [
1407 { name: "explore", description: "Investigate the codebase in an isolated subagent", scope: "global", runAs: "subagent" },
1408 { name: "init", description: "Scaffold a REASONIX.md for this repo", scope: "global", runAs: "inline" },
1409 ],
1410 },
1411 ];
1412 let capPlugins: PluginView[] = [];
1413 const mockSwitchWorkspace = async (path: string) => {
1414 cwd = path || "~";
1415 workspaces = [cwd, ...workspaces.filter((p) => p !== cwd)].slice(0, 12);
1416 if (!mockProjectTree.some((node) => node.kind === "project" && node.root === cwd)) {
1417 mockProjectTree.unshift({
1418 key: `project_${cwd}`,
1419 kind: "project",
1420 label: baseName(cwd),
1421 root: cwd,
1422 children: [],
1423 });
1424 }
1425 return cwd;
1426 };
1427 // Mutable so delete/rename are observable in browser dev.
1428 const sessions: SessionMeta[] = [
1429 { path: "/mock/sessions/a.jsonl", preview: "fix the login bug in auth.go", turns: 12, createdAt: t0 - 2 * day, lastActivityAt: t0 - 3_600_000, modTime: t0 - 3_600_000, current: true, open: true },
1430 { path: "/mock/sessions/b-recovery-0123456789abcdef.jsonl", preview: "refactor the payment module", turns: 5, createdAt: t0 - 3 * day, lastActivityAt: t0 - 6 * 3_600_000, modTime: t0 - 6 * 3_600_000, current: false, open: true, recovered: true, recoveryCopy: true },
1431 { path: "/mock/sessions/c.jsonl", preview: "write the README and badges", turns: 8, createdAt: t0 - 4 * day, lastActivityAt: t0 - day - 3_600_000, modTime: t0 - day - 3_600_000, current: false, open: false },
1432 { path: "/mock/sessions/d.jsonl", preview: "explain the plugin host design", turns: 3, createdAt: t0 - 5 * day, lastActivityAt: t0 - 4 * day, modTime: t0 - 4 * day, current: false, open: false },
1433 ];
1434 const trashedSessions: SessionMeta[] = [
1435 {
1436 path: "/mock/sessions/.trash/trash-dev-standard.jsonl",
1437 title: t("mock.trashDevStandardTitle"),
1438 preview: t("mock.trashDevStandardPreview"),
1439 turns: 4,
1440 createdAt: t0 - 8 * day,
1441 lastActivityAt: t0 - 7 * day,
1442 modTime: t0 - 7 * day,
1443 deletedAt: t0 - 20 * 60_000,
1444 current: false,
1445 open: false,
1446 scope: "project",
1447 workspaceRoot: "~/projects/joyquant-db",
1448 topicId: "topic_dev_standard",
1449 topicTitle: t("mock.trashDevStandardTitle"),
1450 },
1451 {
1452 path: "/mock/sessions/.trash/trash-p3a-review.jsonl",
1453 title: t("mock.trashP3aTitle"),
1454 preview: t("mock.trashP3aPreview"),
1455 turns: 7,
1456 createdAt: t0 - 6 * day,
1457 lastActivityAt: t0 - 5 * day,
1458 modTime: t0 - 5 * day,
1459 deletedAt: t0 - 2 * 3_600_000,
1460 current: false,
1461 open: false,
1462 scope: "project",
1463 workspaceRoot: "~/projects/joyquant-sys",
1464 topicId: "topic_p3a_pd",
1465 topicTitle: t("mock.trashP3aTitle"),
1466 },
1467 {
1468 path: "/mock/sessions/.trash/trash-global-product.jsonl",
1469 title: t("mock.trashGlobalProductTitle"),
1470 preview: t("mock.trashGlobalProductPreview"),
1471 turns: 2,
1472 createdAt: t0 - 4 * day,
1473 lastActivityAt: t0 - 3 * day,
1474 modTime: t0 - 3 * day,
1475 deletedAt: t0 - day,
1476 current: false,
1477 open: false,
1478 scope: "global",
1479 topicId: "topic_product",
1480 topicTitle: t("mock.trashGlobalProductTitle"),
1481 recovered: true,
1482 recoveryCopy: true,
1483 },
1484 ];
1485 if (freshMock) {
1486 sessions.splice(0);
1487 trashedSessions.splice(0);
1488 }
1489 // Mutable settings so the Settings panel's edits are observable in browser dev.
1490 const settings: SettingsView = {
1491 defaultModel: "deepseek",
1492 plannerModel: "",
1493 subagentModel: "",
1494 subagentEffort: "",
1495 autoPlan: "off",
1496 providers: [
1497 { name: "deepseek", builtIn: true, added: false, kind: "openai", baseUrl: "https://api.deepseek.com", modelsUrl: "", models: ["deepseek-v4-flash"], visionModels: [], visionModelsConfigured: false, default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", keySet: true, balanceUrl: "https://api.deepseek.com/user/balance", contextWindow: 1_000_000, reasoningProtocol: "", thinking: "", supportedEfforts: [], defaultEffort: "" },
1498 ],
1499 officialProviders: [
1500 { name: "deepseek", builtIn: true, added: false, kind: "openai", baseUrl: "https://api.deepseek.com", modelsUrl: "", models: ["deepseek-v4-flash", "deepseek-v4-pro"], visionModels: [], visionModelsConfigured: false, default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", keySet: true, balanceUrl: "https://api.deepseek.com/user/balance", contextWindow: 1_000_000, reasoningProtocol: "", thinking: "", supportedEfforts: [], defaultEffort: "" },
1501 ],
1502 providerPresets: mockProviderPresetViews(),
1503 permissions: { mode: "ask", allow: ["ls", "read_file"], ask: [], deny: ["Bash(rm:*)"] },
1504 sandbox: { bash: browserPreviewBashSandboxMode(), network: true, workspaceRoot: "", allowWrite: [], effectiveWorkspaceRoot: cwd, effectiveWriteRoots: [cwd], shell: "auto", effectiveShell: browserPreviewEffectiveShell("auto") },
1505 network: {
1506 proxyMode: "auto",
1507 proxyUrl: "",
1508 noProxy: "",
1509 proxy: { type: "socks5", server: "127.0.0.1", port: 7890, username: "", password: "" },
1510 },
1511 agent: { temperature: 0.2, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, maxSubagentConcurrency: 6, maxParallelWriters: 3, systemPrompt: "You are Reasonix, a coding agent.", coldResumePrune: true, reasoningLanguage: "auto", compactRatio: 0.8 },
1512 bot: {
1513 enabled: !freshMock,
1514 model: "",
1515 toolApprovalMode: "ask",
1516 maxSteps: 25,
1517 debounceMs: 1500,
1518 queueMode: "steer",
1519 queueCap: 20,
1520 queueDrop: "summarize",
1521 ignoreSelfMessages: true,
1522 selfUserIds: {
1523 qq: [],
1524 feishu: [],
1525 weixin: [],
1526 },
1527 control: {
1528 enabled: false,
1529 addr: "127.0.0.1:37913",
1530 tokenEnv: "REASONIX_BOT_CONTROL_TOKEN",
1531 },
1532 pairing: {
1533 enabled: true,
1534 requestTtlMinutes: 60,
1535 maxPendingPerPlatform: 3,
1536 },
1537 routes: [],
1538 allowlist: {
1539 enabled: true,
1540 allowAll: false,
1541 qqUsers: [],
1542 feishuUsers: freshMock ? [] : ["ou_mock_user_001"],
1543 weixinUsers: freshMock ? [] : ["wxid_mock_user_001"],
1544 qqApprovers: [],
1545 feishuApprovers: [],
1546 weixinApprovers: [],
1547 qqAdmins: [],
1548 feishuAdmins: [],
1549 weixinAdmins: [],
1550 qqGroups: [],
1551 feishuGroups: [],
1552 weixinGroups: [],
1553 },
1554 qq: { enabled: false, appId: "", appSecretEnv: "QQ_BOT_APP_SECRET", secretSet: false, sandbox: false, model: "", toolApprovalMode: "ask", workspaceRoot: "", access: { enabled: true, allowAll: false, pairingEnabled: true, users: [], groups: [], approvers: [], admins: [] } },
1555 feishu: {
1556 enabled: false,
1557 domain: "feishu",
1558 appId: "",
1559 appSecretEnv: "FEISHU_BOT_APP_SECRET",
1560 secretSet: false,
1561 verificationToken: "",
1562 mode: "webhook",
1563 webhookPort: 8080,
1564 requireMention: true,
1565 },
1566 weixin: {
1567 enabled: false,
1568 accountId: "default",
1569 tokenEnv: "WEIXIN_BOT_TOKEN",
1570 tokenSet: false,
1571 apiBase: "https://ilinkai.weixin.qq.com",
1572 },
1573 connections: freshMock ? [] : [
1574 {
1575 id: "mock-lark-kun",
1576 provider: "feishu",
1577 domain: "lark",
1578 label: "kun",
1579 enabled: true,
1580 status: "connected",
1581 model: "",
1582 toolApprovalMode: "",
1583 workspaceRoot: "",
1584 access: { enabled: true, allowAll: false, pairingEnabled: true, users: ["ou_mock_user_001"], groups: [], approvers: [], admins: [] },
1585 credential: {
1586 appId: "cli_mock_lark",
1587 appSecretEnv: "FEISHU_BOT_APP_SECRET",
1588 accountId: "",
1589 tokenEnv: "",
1590 secretSet: true,
1591 },
1592 sessionMappings: [
1593 {
1594 remoteId: "ou_mock_user_001",
1595 sessionId: "topic:topic_product",
1596 sessionSource: "",
1597 chatType: "",
1598 userId: "",
1599 threadId: "",
1600 scope: "global",
1601 workspaceRoot: "",
1602 updatedAt: new Date(Date.now() - 4 * 60_000).toISOString(),
1603 },
1604 ],
1605 lastError: "",
1606 createdAt: new Date(Date.now() - 86_400_000).toISOString(),
1607 updatedAt: new Date(Date.now() - 4 * 60_000).toISOString(),
1608 },
1609 {
1610 id: "mock-weixin-kun",
1611 provider: "weixin",
1612 domain: "weixin",
1613 label: "kun",
1614 enabled: true,
1615 status: "connected",
1616 model: "",
1617 toolApprovalMode: "",
1618 workspaceRoot: "",
1619 access: { enabled: true, allowAll: false, pairingEnabled: true, users: ["wxid_mock_user_001"], groups: [], approvers: [], admins: [] },
1620 credential: {
1621 appId: "",
1622 appSecretEnv: "",
1623 accountId: "default",
1624 tokenEnv: "WEIXIN_BOT_TOKEN",
1625 secretSet: true,
1626 },
1627 sessionMappings: [
1628 {
1629 remoteId: "wxid_mock_user_001",
1630 sessionId: "topic:topic_ai",
1631 sessionSource: "",
1632 chatType: "",
1633 userId: "",
1634 threadId: "",
1635 scope: "global",
1636 workspaceRoot: "",
1637 updatedAt: new Date(Date.now() - 12 * 60_000).toISOString(),
1638 },
1639 ],
1640 lastError: "",
1641 createdAt: new Date(Date.now() - 86_400_000).toISOString(),
1642 updatedAt: new Date(Date.now() - 12 * 60_000).toISOString(),
1643 },
1644 ],
1645 },
1646 desktopLanguage: "",
1647 desktopCurrency: "",
1648 desktopLayoutStyle: "workbench",
1649 desktopTheme: "auto",
1650 desktopThemeStyle: "graphite",
1651 desktopTerminalTheme: "auto",
1652 conversationWidth: "standard",
1653 closeBehavior: "background",
1654 displayMode: "compact",
1655 statusBarStyle: "text",
1656 statusBarItems: [...DEFAULT_STATUS_BAR_ITEMS],
1657 defaultToolApprovalMode: "auto",
1658 checkUpdates: true,
1659 updateChannel: "stable",
1660 telemetry: true,
1661 metrics: true,
1662 configPath: "~/.reasonix/config.toml",
1663 shadowedByPath: "~/projects/reasonix/reasonix.toml",
1664 providerKinds: ["openai", "anthropic"],
1665 autoApproveTools: false,
1666 bypass: false,
1667 };
1668 const hookEvents = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "PostLLMCall", "SessionStart", "SessionEnd", "SubagentStop", "Notification", "PreCompact"];
1669 const hookSettings: Record<string, HooksSettingsView> = {
1670 global: {
1671 scope: "global",
1672 path: "~/.reasonix/settings.json",
1673 projectRoot: "",
1674 trusted: true,
1675 events: hookEvents,
1676 hooks: [
1677 { event: "Stop", command: "echo turn done", description: "Notify after each turn" },
1678 ],
1679 },
1680 project: {
1681 scope: "project",
1682 path: "./.reasonix/settings.json",
1683 projectRoot: "/mock/project",
1684 trusted: false,
1685 events: hookEvents,
1686 hooks: [],
1687 },
1688 };
1689 settings.providers = settings.providers.map((provider) =>
1690 provider.apiKeyEnv === "DEEPSEEK_API_KEY" ? { ...provider, keySet: !freshMock } : provider,
1691 );
1692 if (freshMock) {
1693 settings.configPath = "~/.reasonix/config.toml";
1694 settings.shadowedByPath = "";
1695 }
1696 const mockNow = Date.now();
1697 const mockProjectTree: ProjectNode[] = freshMock ? [] : [
1698 {
1699 key: "project_~/projects/joyquant-db",
1700 kind: "project",
1701 label: t("mock.projectJoyquantDb"),
1702 root: "~/projects/joyquant-db",
1703 projectColor: "blue",
1704 children: [
1705 { key: "topic_dev_standard", kind: "topic", label: `● ${t("mock.topicDevStandard")}`, root: "~/projects/joyquant-db", topicId: "topic_dev_standard", projectColor: "blue", turns: 18, lastActivityAt: mockNow - 8 * 60_000, open: true, running: runningMock },
1706 { key: "topic_db_maint", kind: "topic", label: t("mock.topicDbMaint"), root: "~/projects/joyquant-db", topicId: "topic_db_maint", projectColor: "blue", turns: 7, lastActivityAt: mockNow - 2 * 60 * 60_000 },
1707 { key: "topic_env", kind: "topic", label: t("mock.topicEnv"), root: "~/projects/joyquant-db", topicId: "topic_env", projectColor: "blue", turns: 3, lastActivityAt: mockNow - 26 * 60 * 60_000 },
1708 ],
1709 },
1710 {
1711 key: "project_~/projects/joyquant-sys",
1712 kind: "project",
1713 label: t("mock.projectJoyquantSys"),
1714 root: "~/projects/joyquant-sys",
1715 projectColor: "purple",
1716 children: [
1717 { key: "topic_p3b_pd", kind: "topic", label: `● ${t("mock.topicP3b")}`, root: "~/projects/joyquant-sys", topicId: "topic_p3b_pd", projectColor: "purple", turns: 11, lastActivityAt: mockNow - 3 * 24 * 60 * 60_000, status: runningMock ? "streaming" : undefined },
1718 { key: "topic_p3a_pd", kind: "topic", label: t("mock.topicP3a"), root: "~/projects/joyquant-sys", topicId: "topic_p3a_pd", projectColor: "purple", turns: 9, lastActivityAt: mockNow - 4 * 24 * 60 * 60_000, status: runningMock ? "thinking" : undefined },
1719 { key: "topic_hotfix", kind: "topic", label: t("mock.topicHotfix"), root: "~/projects/joyquant-sys", topicId: "topic_hotfix", projectColor: "purple", turns: 4, lastActivityAt: mockNow - 5 * 24 * 60 * 60_000, status: runningMock ? "thinking" : undefined },
1720 { key: "topic_sys_coord", kind: "topic", label: t("mock.topicSysCoord"), root: "~/projects/joyquant-sys", topicId: "topic_sys_coord", projectColor: "purple", turns: 14, lastActivityAt: mockNow - 6 * 24 * 60 * 60_000, status: runningMock ? "waiting_confirmation" : undefined },
1721 { key: "topic_sys_standard", kind: "topic", label: t("mock.topicSysStandard"), root: "~/projects/joyquant-sys", topicId: "topic_sys_standard", projectColor: "purple", turns: 6, lastActivityAt: mockNow - 7 * 24 * 60 * 60_000, status: "paused" },
1722 { key: "topic_sys_exception", kind: "topic", label: t("mock.topicSysException"), root: "~/projects/joyquant-sys", topicId: "topic_sys_exception", projectColor: "purple", turns: 2, lastActivityAt: mockNow - 8 * 24 * 60 * 60_000, status: "error" },
1723 ],
1724 },
1725 {
1726 key: "global_folder",
1727 kind: "global_folder",
1728 label: "Global",
1729 root: globalWorkspaceRoot,
1730 children: [
1731 { key: "global_topic_product", kind: "global_topic", label: t("mock.topicProduct"), topicId: "topic_product", turns: 5, lastActivityAt: mockNow - 8 * 24 * 60 * 60_000 },
1732 { key: "global_topic_ai", kind: "global_topic", label: t("mock.topicAi"), topicId: "topic_ai", turns: 8, lastActivityAt: mockNow - 10 * 24 * 60 * 60_000 },
1733 { key: "global_topic_lab", kind: "global_topic", label: t("mock.topicLab"), topicId: "topic_lab", turns: 2, lastActivityAt: mockNow - 12 * 24 * 60 * 60_000 },
1734 ],
1735 },
1736 ];
1737 const ensureMockGlobalFolder = (): ProjectNode => {
1738 let node = mockProjectTree.find((item) => item.kind === "global_folder");
1739 if (!node) {
1740 node = {
1741 key: "global_folder",
1742 kind: "global_folder",
1743 label: "Global",
1744 root: globalWorkspaceRoot,
1745 children: [],
1746 };
1747 mockProjectTree.push(node);
1748 }
1749 return node;
1750 };
1751 const mockProjectTreeForDisplay = () => {
1752 const pinnedProjects = mockProjectTree.filter((node) => node.kind === "project" && node.pinned);
1753 if (pinnedProjects.length === 0) return mockProjectTree;
1754 const rest = mockProjectTree.filter((node) => !(node.kind === "project" && node.pinned));
1755 return [...pinnedProjects, ...rest];
1756 };
1757 const cloneProjectTree = () => {
1758 if (mockProjectTree.length === 0) ensureMockGlobalFolder();
1759 return JSON.parse(JSON.stringify(mockProjectTreeForDisplay())) as ProjectNode[];
1760 };
1761 const projectChildren = (node: ProjectNode): ProjectNode[] => Array.isArray(node.children) ? node.children : [];
1762 const findMockTopic = (topicId: string): ProjectNode | null => {
1763 for (const parent of mockProjectTree) {
1764 const found = projectChildren(parent).find((child) => child.topicId === topicId);
1765 if (found) return found;
1766 }
1767 return null;
1768 };
1769 const setMockTopicPinned = (topicId: string, pinned: boolean) => {
1770 for (const parent of mockProjectTree) {
1771 const children = projectChildren(parent);
1772 const index = children.findIndex((child) => child.topicId === topicId);
1773 if (index < 0) continue;
1774 const topic = { ...children[index], pinned: pinned || undefined };
1775 if (!pinned) {
1776 parent.children = children.map((child, i) => (i === index ? topic : child));
1777 return;
1778 }
1779 const remaining = children.filter((_, i) => i !== index);
1780 parent.children = [topic, ...remaining];
1781 return;
1782 }
1783 };
1784 const setMockProjectPinned = (workspaceRoot: string, pinned: boolean) => {
1785 const index = mockProjectTree.findIndex((node) => node.kind === "project" && node.root === workspaceRoot);
1786 if (index < 0) return;
1787 mockProjectTree[index] = { ...mockProjectTree[index], pinned: pinned || undefined };
1788 };
1789 const deleteMockTopic = (topicId: string) => {
1790 for (const parent of mockProjectTree) {
1791 parent.children = projectChildren(parent).filter((child) => child.topicId !== topicId);
1792 }
1793 };
1794 const topicLabel = (topicId: string, fallback: string) => (findMockTopic(topicId)?.label || fallback).replace(/^●\s*/, "");
1795 const mockTopicStatus = (topicId: string) => findMockTopic(topicId)?.status ?? "";
1796 const mockTopicIsRunning = (topicId: string) => {
1797 const status = mockTopicStatus(topicId);
1798 return status === "streaming" || status === "thinking" || status === "waiting_confirmation";
1799 };
1800 const mockTopicIsBlank = (topicId: string) => {
1801 const topic = findMockTopic(topicId);
1802 return Boolean(topic && topic.label === t("mock.newSession") && !topic.turns && !topic.lastActivityAt && !topic.status);
1803 };
1804 const mockTopicRunsInScenario = (topicId: string) => runningMock && mockTopicIsRunning(topicId);
1805 const mockLongTranscriptHistory = (): HistoryMessage[] => {
1806 const out: HistoryMessage[] = [];
1807 for (let i = 1; i <= 18; i++) {
1808 out.push({
1809 role: "user",
1810 content: `第 ${i} 轮:检查聊天滚动定位,切换会话后应该自动停在最新消息底部。`,
1811 createdAt: t0 - (19 - i) * 15 * 60_000,
1812 });
1813 if (i === 4) {
1814 out.push({ role: "phase", content: "复现切换会话后的滚动位置" });
1815 }
1816 if (i === 8) {
1817 const toolID = "mock-scroll-layout-check";
1818 out.push({
1819 role: "assistant",
1820 content: "我会先读取滚动容器尺寸,再确认是否存在动态高度变化导致的底部偏移。",
1821 reasoning: "旧实现只重置 stick 标志,没有主动等待布局稳定;AskCard、Approval、Todo 这类卡片可能在下一帧改变高度。",
1822 toolCalls: [{ id: toolID, name: "bash", arguments: JSON.stringify({ command: "npm run check:css && pnpm typecheck" }) }],
1823 });
1824 out.push({
1825 role: "tool",
1826 toolCallId: toolID,
1827 toolName: "bash",
1828 content: "CSS syntax check passed\nz-index token check passed\ntsc --noEmit passed\n",
1829 });
1830 continue;
1831 }
1832 if (i === 13) {
1833 out.push({ role: "notice", level: "info", content: "模拟提示:用户向上查看历史后,右下角应出现跳到底部按钮。" });
1834 }
1835 out.push({
1836 role: "assistant",
1837 content: [
1838 `第 ${i} 轮结果:当前滚动契约会在切换会话或 reveal 信号到达后执行强制贴底。`,
1839 "它会先立即设置 scrollTop 到 scrollHeight,再连续几个 animation frame 复查,避免动态内容把底部再次推走。",
1840 "如果用户主动向上滚动,普通 streaming 不会强行拉回;只有点击跳到底部按钮或显式切换会话才会重新贴底。",
1841 ].join("\n\n"),
1842 });
1843 }
1844 out.push({
1845 role: "compaction",
1846 content: "",
1847 trigger: "manual",
1848 messages: 36,
1849 summary: "Mock 长会话用于验证桌面端 Transcript 自动贴底、多帧布局修正和跳到底部按钮。",
1850 archive: "mock-scroll-preview",
1851 });
1852 out.push({
1853 role: "assistant",
1854 content: "最终状态:这条消息应该位于真实底部。向上滚动后,右下角会显示跳到底部按钮;点击按钮后应回到这里。",
1855 });
1856 return out;
1857 };
1858 const mockTopicHistory = (topicId: string): HistoryMessage[] => {
1859 switch (topicId) {
1860 case "topic_product":
1861 return [
1862 {
1863 role: "user",
1864 content: [
1865 "[[reasonix-im]]",
1866 "provider=lark",
1867 "label=Feishu / Lark",
1868 "sender=ou_mock_user_001",
1869 "chat=p2p 会话",
1870 "[[/reasonix-im]]",
1871 "你可以做什么",
1872 ].join("\n"),
1873 },
1874 {
1875 role: "assistant",
1876 content: "这是 Global 范围下的 IM 会话。我可以先处理不依赖项目文件的问答、计划和信息整理;需要进入项目时,再由桌面端显式绑定或迁移到项目话题。",
1877 },
1878 ];
1879 case "topic_ai":
1880 return [
1881 {
1882 role: "user",
1883 content: [
1884 "[[reasonix-im]]",
1885 "provider=weixin",
1886 "label=微信",
1887 "sender=wxid_mock_user_001",
1888 "chat=单聊",
1889 "[[/reasonix-im]]",
1890 "帮我整理一下今天要做的事",
1891 ].join("\n"),
1892 },
1893 {
1894 role: "assistant",
1895 content: "可以。我会先在 Global 范围里整理任务清单;如果某条任务需要读取项目文件,再切到你授权的项目话题处理。",
1896 },
1897 ];
1898 case "topic_dev_standard":
1899 return mockLongTranscriptHistory();
1900 case "topic_p3b_pd":
1901 return [
1902 { role: "user", content: "把 p3b P&D 的范围和风险重新整理成可执行计划。" },
1903 { role: "phase", content: "分析需求范围" },
1904 ];
1905 case "topic_p3a_pd":
1906 return [
1907 { role: "user", content: "复盘 p3a 的技术方案,先不要写文件,先说明你的判断。" },
1908 ];
1909 case "topic_hotfix":
1910 return [
1911 { role: "user", content: "检查 post-p3-hotfix 的回归风险,重点看最近的 shell 输出和 git 改动。" },
1912 { role: "assistant", content: "", reasoning: "我先定位最近一次 hotfix 的上下文,然后用只读命令检查状态;左侧保持“思考中”,工具细节在这里展开。" },
1913 ];
1914 case "topic_sys_coord":
1915 return [
1916 { role: "user", content: "准备执行 joyquant-sys 的同步脚本,但需要我确认后再运行。" },
1917 { role: "assistant", content: "", reasoning: "这个动作会运行脚本并可能刷新本地缓存,所以需要先等用户确认。" },
1918 ];
1919 case "topic_sys_standard":
1920 return [
1921 { role: "user", content: "继续制定 SYS 项目开发规范,先停在当前检查点。" },
1922 { role: "assistant", content: "已暂停在规范整理阶段。当前保留了目录约定、分支策略和待确认的发布检查项;继续时可以从这里恢复。" },
1923 { role: "notice", level: "info", content: "会话已暂停:未继续执行命令,等待用户恢复或切换任务。" },
1924 ];
1925 case "topic_sys_exception":
1926 return [
1927 { role: "user", content: "演练异常处理流程,看看失败时界面怎么提示。" },
1928 { role: "assistant", content: "我尝试校验恢复脚本时遇到异常,已停止继续执行。" },
1929 { role: "notice", level: "warn", content: "运行异常:恢复脚本缺少必要环境变量 JOYQUANT_SYS_TOKEN。请补齐配置后重试。" },
1930 ];
1931 default:
1932 return [];
1933 }
1934 };
1935 const mockHistoryPage = (messages: HistoryMessage[], beforeTurn = 0, limit = 60): HistoryPage => {
1936 const totalTurns = messages.reduce((count, message) => count + (message.role === "user" ? 1 : 0), 0);
1937 const safeLimit = Math.max(1, Math.min(200, Math.floor(limit || 60)));
1938 const endTurn = beforeTurn > 0 && beforeTurn <= totalTurns ? beforeTurn : totalTurns;
1939 const startTurn = Math.max(0, endTurn - safeLimit);
1940 let turn = -1;
1941 const pageMessages = messages.filter((message) => {
1942 if (message.role === "user") turn += 1;
1943 if (turn < 0) return startTurn === 0;
1944 return turn >= startTurn && turn < endTurn;
1945 });
1946 return { messages: pageMessages, startTurn, endTurn, totalTurns, hasOlder: startTurn > 0 };
1947 };
1948 const mockRuntimeInjected = new Set<string>();
1949 const queueMockTopicRuntime = (tab: TabMeta) => {
1950 if (!runningMock) return;
1951 const status = mockTopicStatus(tab.topicId);
1952 if (status !== "streaming" && status !== "thinking" && status !== "waiting_confirmation") return;
1953 const key = `${tab.id}:${tab.topicId}:${status}`;
1954 if (mockRuntimeInjected.has(key)) return;
1955 mockRuntimeInjected.add(key);
1956 window.setTimeout(() => {
1957 void withMockTabScope(tab.id, async () => {
1958 emitMockTurnStarted();
1959 await delay(120);
1960 if (tab.topicId === "topic_p3b_pd") {
1961 const text = "我会先把范围拆成三层:目标、依赖、风险。当前已经确认 p3b 的交付边界,接下来补充每个模块的验收口径...";
1962 for (const ch of text) {
1963 emit({ kind: "text", text: ch });
1964 await delay(5);
1965 }
1966 return;
1967 }
1968 if (tab.topicId === "topic_p3a_pd") {
1969 emit({ kind: "reasoning", text: "我正在对比 p3a 和 p3b 的差异:先看约束,再看变更风险,最后判断是否需要拆成独立任务。\n\n" });
1970 await delay(220);
1971 emit({ kind: "reasoning", text: "当前倾向:先保留 p3a 的兼容路径,不急于删除旧逻辑。" });
1972 return;
1973 }
1974 if (tab.topicId === "topic_hotfix") {
1975 const id = "mock-hotfix-shell";
1976 emit({ kind: "tool_dispatch", tool: { id, name: "bash", args: JSON.stringify({ command: "git status --short && npm test" }), readOnly: true } });
1977 await delay(180);
1978 emit({ kind: "tool_progress", tool: { id, name: "bash", readOnly: true, output: "$ git status --short\n M internal/sys/runner.go\n\n$ npm test\nrunning targeted regression tests...\n" } });
1979 return;
1980 }
1981 if (tab.topicId === "topic_sys_coord") {
1982 pendingApprovalPreview = true;
1983 pendingApprovalPreviewPrompt = { id: "mock-sys-confirm", tool: "bash" };
1984 emit({ kind: "reasoning", text: "我已经准备好执行同步脚本,但这个操作会影响本地 workspace,需要用户确认。" });
1985 await delay(160);
1986 emit({
1987 kind: "approval_request",
1988 approval: {
1989 id: "mock-sys-confirm",
1990 tool: "bash",
1991 subject: "npm run sync:joyquant-sys\n\n该命令会同步 SYS 项目配置并刷新本地缓存。",
1992 },
1993 });
1994 }
1995 });
1996 }, 180);
1997 };
1998 const setMockActiveTab = (tabId: string) => {
1999 mockTabs = mockTabs.map((tab) => ({ ...tab, active: tab.id === tabId }));
2000 };
2001 const currentMockTurnTabId = () => mockScopedTabId || mockTabs.find((tab) => tab.active)?.id;
2002 const setMockTabRunning = (tabId: string | undefined, running: boolean) => {
2003 if (!tabId) return;
2004 mockTabs = mockTabs.map((tab) => (tab.id === tabId ? { ...tab, running } : tab));
2005 };
2006 const emitMockTurnStarted = () => {
2007 setMockTabRunning(currentMockTurnTabId(), true);
2008 emit({ kind: "turn_started" });
2009 };
2010 const emitMockTurnDone = () => {
2011 setMockTabRunning(currentMockTurnTabId(), false);
2012 emit({ kind: "turn_done" });
2013 };
2014 // Fresh user decisions never auto-allow on a posture switch (mirrors the
2015 // backend's requiresFreshApprovalTool set).
2016 const mockFreshApprovalTools = new Set(["exit_plan_mode", "sandbox_escape", "memory_remember", "memory_forget", "managed_config_write"]);
2017 // Mirrors the backend drain contract for the mode-switch bindings: returns
2018 // the prompt ids the new posture auto-allowed; fresh prompts stay pending.
2019 const drainMockApprovalPreviews = (toolApprovalMode: string): string[] => {
2020 if (toolApprovalMode !== "auto" && toolApprovalMode !== "yolo") return [];
2021 const prompt = pendingApprovalPreviewPrompt;
2022 if (!pendingApprovalPreview || !prompt || mockFreshApprovalTools.has(prompt.tool)) return [];
2023 pendingApprovalPreview = false;
2024 pendingApprovalPreviewPrompt = undefined;
2025 emit({ kind: "message", text: `approval preview auto-allowed (${toolApprovalMode})` });
2026 emitMockTurnDone();
2027 return [prompt.id];
2028 };
2029 let mockTabs: TabMeta[] = noticePreviewMock ? [
2030 {
2031 id: "tab_notice_preview",
2032 scope: "project",
2033 workspaceRoot: "~/projects/reasonix",
2034 workspaceName: "reasonix",
2035 workspacePath: "~/projects/reasonix",
2036 gitBranch: "codex/compact-chat-notices-i18n",
2037 topicId: "topic_notice_preview",
2038 topicTitle: "Compact notice preview",
2039 projectColor: "green",
2040 label: "DeepSeek-R1",
2041 ready: true,
2042 running: false,
2043 mode: "normal",
2044 collaborationMode: "normal",
2045 toolApprovalMode: "ask",
2046 tokenMode: "full",
2047 active: true,
2048 cwd: "~/projects/reasonix",
2049 },
2050 ] : freshMock ? [
2051 {
2052 id: "tab_global",
2053 scope: "global",
2054 workspaceRoot: globalWorkspaceRoot,
2055 workspaceName: "Global",
2056 workspacePath: globalWorkspaceRoot,
2057 topicId: "",
2058 topicTitle: "Global",
2059 label: "DeepSeek-R1",
2060 ready: true,
2061 running: false,
2062 mode: "normal",
2063 collaborationMode: "normal",
2064 toolApprovalMode: "ask",
2065 tokenMode: "full",
2066 active: true,
2067 cwd: globalWorkspaceRoot,
2068 },
2069 ] : [
2070 {
2071 id: "tab_joyquant_db",
2072 scope: "project",
2073 workspaceRoot: "~/projects/joyquant-db",
2074 workspaceName: "joyquant-db",
2075 workspacePath: "~/projects/joyquant-db",
2076 gitBranch: "main",
2077 topicId: "topic_dev_standard",
2078 topicTitle: t("mock.trashDevStandardTitle"),
2079 projectColor: "blue",
2080 label: "DeepSeek-R1",
2081 ready: true,
2082 running: false,
2083 mode: "normal",
2084 collaborationMode: "normal",
2085 toolApprovalMode: "ask",
2086 tokenMode: "full",
2087 active: !guidanceMock,
2088 cwd: "~/projects/joyquant-db",
2089 },
2090 {
2091 id: "tab_joyquant_sys",
2092 scope: "project",
2093 workspaceRoot: "~/projects/joyquant-sys",
2094 workspaceName: "joyquant-sys",
2095 workspacePath: "~/projects/joyquant-sys",
2096 gitBranch: "feature/p3b",
2097 topicId: "topic_p3b_pd",
2098 topicTitle: "p3b P&D",
2099 projectColor: "purple",
2100 label: "DeepSeek-R1",
2101 ready: true,
2102 running: runningMock && mockTopicIsRunning("topic_p3b_pd"),
2103 mode: "normal",
2104 collaborationMode: "normal",
2105 toolApprovalMode: "ask",
2106 tokenMode: "full",
2107 active: guidanceMock,
2108 cwd: "~/projects/joyquant-sys",
2109 },
2110 {
2111 id: "tab_global",
2112 scope: "global",
2113 workspaceRoot: "",
2114 workspaceName: "Global",
2115 workspacePath: "~/projects/joyquant-db",
2116 topicId: "topic_global",
2117 topicTitle: "Global",
2118 label: "DeepSeek-R1",
2119 ready: true,
2120 running: false,
2121 mode: "normal",
2122 collaborationMode: "normal",
2123 toolApprovalMode: "ask",
2124 tokenMode: "full",
2125 active: false,
2126 cwd: "~/projects/joyquant-db",
2127 },
2128 ];
2129 if (sandboxEscapeMock) {
2130 window.setTimeout(() => {
2131 if (pendingApprovalPreview) return;
2132 pendingApprovalPreview = true;
2133 pendingApprovalPreviewPrompt = { id: "mock-sandbox-escape-preview", tool: "sandbox_escape" };
2134 emitMockTurnStarted();
2135 emit({ kind: "reasoning", text: t("mock.sandboxEscapeReasoning") });
2136 emit({
2137 kind: "approval_request",
2138 approval: {
2139 id: "mock-sandbox-escape-preview",
2140 tool: "sandbox_escape",
2141 subject: t("mock.sandboxEscapeSubject"),
2142 reason: t("mock.sandboxEscapeReason"),
2143 },
2144 });
2145 }, 800);
2146 }
2147 const mockModelCatalog = [
2148 { ref: "deepseek/deepseek-v4-flash", provider: "deepseek", model: "deepseek-v4-flash" },
2149 { ref: "deepseek/deepseek-v4-pro", provider: "deepseek", model: "deepseek-v4-pro" },
2150 ];
2151 const defaultMockModelRef = mockModelCatalog[0].ref;
2152 const mockModelRef = (name: string): string => {
2153 const trimmed = name.trim();
2154 if (!trimmed || trimmed === "DeepSeek-R1") return defaultMockModelRef;
2155 const exact = mockModelCatalog.find((model) => model.ref === trimmed);
2156 if (exact) return exact.ref;
2157 const byModel = mockModelCatalog.find((model) => model.model === trimmed);
2158 return byModel?.ref ?? trimmed;
2159 };
2160 const mockModelLabel = (ref: string): string => mockModelCatalog.find((model) => model.ref === mockModelRef(ref))?.model ?? ref.split("/").pop() ?? ref;
2161 const mockTabModelRef = (tab?: TabMeta): string => mockModelRef(tab?.label ?? "");
2162 let mockTerminalSessions: TerminalSessionView[] = [];
2163 const mockTerminalOutput = new Map<string, string>();
2164 const mockTerminalTabIDs = new Map<string, string>();
2165 const mockTerminalBytes = (text: string): string => {
2166 if (typeof btoa === "function") return btoa(unescape(encodeURIComponent(text)));
2167 return "";
2168 };
2169 const setMockTabModel = (tabID: string | undefined, name: string) => {
2170 const ref = mockModelRef(name);
2171 const label = mockModelLabel(ref);
2172 let applied = false;
2173 mockTabs = mockTabs.map((tab) => {
2174 const match = tabID ? tab.id === tabID : tab.active;
2175 if (!match) return tab;
2176 applied = true;
2177 return { ...tab, label };
2178 });
2179 if (!applied && mockTabs.length > 0) {
2180 mockTabs = mockTabs.map((tab, index) => (index === 0 ? { ...tab, label } : tab));
2181 }
2182 };
2183 return {
2184 async MinimiseMainWindow() {
2185 console.info("mock MinimiseMainWindow");
2186 },
2187 async ToggleMaximiseMainWindow() {
2188 console.info("mock ToggleMaximiseMainWindow");
2189 },
2190 async IsMainWindowMaximised() {
2191 return false;
2192 },
2193 async CloseMainWindow() {
2194 console.info("mock CloseMainWindow");
2195 },
2196 async Platform() {
2197 const override = browserPlatformOverride();
2198 if (override) return override;
2199 // Mirror the OS the browser dev mock runs on.
2200 const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
2201 if (/Win/i.test(ua)) return "windows";
2202 if (/Mac/i.test(ua)) return "darwin";
2203 return "linux";
2204 },
2205 async Submit(input) {
2206 cancelled = false;
2207 emitMockTurnStarted();
2208 const trimmedInput = input.trim().toLowerCase();
2209 const decisionSurfaceMock = decisionSurfaceMockFromInput(trimmedInput);
2210 const goalMatch = /^\/goal(?:\s+([\s\S]*))?$/.exec(input.trim());
2211 if (goalMatch) {
2212 const arg = stripGoalResearchFlags((goalMatch[1] ?? "").trim());
2213 const lowered = arg.toLowerCase();
2214 const active = mockTabs.find((tab) => tab.active);
2215 if (!arg || lowered === "status") {
2216 emit({ kind: "notice", level: "info", text: active?.goal ? `goal: ${active.goal}` : "goal: none" });
2217 emitMockTurnDone();
2218 return;
2219 }
2220 if (["clear", "off", "stop", "done"].includes(lowered)) {
2221 mockTabs = mockTabs.map((tab) => (tab.active ? { ...tab, goal: "", goalStatus: "stopped", collaborationMode: "normal" } : tab));
2222 emit({ kind: "notice", level: "info", text: "goal cleared" });
2223 emitMockTurnDone();
2224 return;
2225 }
2226 mockTabs = mockTabs.map((tab) => (tab.active ? { ...tab, goal: arg, goalStatus: "running", collaborationMode: "goal" } : tab));
2227 emit({ kind: "notice", level: "info", text: `goal set: ${arg}` });
2228 await delay(350);
2229 if (cancelled) return;
2230 const reply = `Autonomous goal run started for: **${arg}**\n\nMock run completed.\n\n[goal:complete]`;
2231 emit({ kind: "message", text: reply });
2232 mockTabs = mockTabs.map((tab) => (tab.active ? { ...tab, goal: "", goalStatus: "complete", collaborationMode: "normal" } : tab));
2233 emit({ kind: "notice", level: "info", text: "goal complete" });
2234 emitMockTurnDone();
2235 return;
2236 }
2237 if (decisionSurfaceMock === "tool_approval") {
2238 pendingApprovalPreview = true;
2239 pendingApprovalPreviewPrompt = { id: "mock-approval-preview", tool: "bash" };
2240 await delay(250);
2241 if (cancelled) return;
2242 emit({
2243 kind: "approval_request",
2244 approval: {
2245 id: "mock-approval-preview",
2246 tool: "bash",
2247 subject: t("mock.approvalSubject"),
2248 },
2249 });
2250 return;
2251 }
2252 if (trimmedInput === "/recovery-preview" || trimmedInput === "recovery preview" || trimmedInput === "恢复预览") {
2253 pendingApprovalPreview = true;
2254 pendingApprovalPreviewPrompt = { id: "mock-recovery-preview", tool: "write_file" };
2255 await delay(250);
2256 if (cancelled) return;
2257 emit({
2258 kind: "approval_request",
2259 approval: {
2260 id: "mock-recovery-preview",
2261 tool: "write_file",
2262 subject: "internal/recovery/gate.go",
2263 reason: "The proposed recovery changes the implementation method after a failing verification.",
2264 fresh: true,
2265 kind: "recovery",
2266 recovery: {
2267 source_agent: "root",
2268 failed_tool: "bash",
2269 failed_summary: "go test ./internal/recovery failed",
2270 diagnosis: "The failure is isolated to recovery state persistence.",
2271 next_tool: "write_file",
2272 next_action: "Update internal/recovery/gate.go",
2273 change_kind: "strategy",
2274 change_rationale: "The proposed edit changes the recovery method and needs a fresh decision.",
2275 plan_before: [
2276 "1. Keep the existing Auto execution path [in_progress]",
2277 "2. Add execution-risk approval prompts [pending]",
2278 "3. Run the recovery regression suite [pending]",
2279 ].join("\n"),
2280 plan_after: [
2281 "1. Keep the existing Auto execution path [in_progress]",
2282 "2. Ask only when strategy or scope changes [pending]",
2283 "3. Show the old and proposed plan before deciding [pending]",
2284 "4. Run the recovery regression suite [pending]",
2285 ].join("\n"),
2286 },
2287 },
2288 });
2289 return;
2290 }
2291 if (
2292 trimmedInput === "/sandbox-escape-preview" ||
2293 trimmedInput === "sandbox escape preview" ||
2294 trimmedInput === "sandbox_escape preview" ||
2295 trimmedInput === "sandbox escape预览"
2296 ) {
2297 pendingApprovalPreview = true;
2298 pendingApprovalPreviewPrompt = { id: "mock-sandbox-escape-preview", tool: "sandbox_escape" };
2299 await delay(250);
2300 if (cancelled) return;
2301 emit({
2302 kind: "approval_request",
2303 approval: {
2304 id: "mock-sandbox-escape-preview",
2305 tool: "sandbox_escape",
2306 subject: t("mock.sandboxEscapeSubject"),
2307 reason: t("mock.sandboxEscapeReason"),
2308 },
2309 });
2310 return;
2311 }
2312 if (decisionSurfaceMock === "plan_approval") {
2313 pendingApprovalPreview = true;
2314 pendingApprovalPreviewPrompt = { id: "mock-plan-approval-preview", tool: "exit_plan_mode" };
2315 await delay(250);
2316 if (cancelled) return;
2317 emit({
2318 kind: "approval_request",
2319 approval: {
2320 id: "mock-plan-approval-preview",
2321 tool: "exit_plan_mode",
2322 subject: "",
2323 },
2324 });
2325 return;
2326 }
2327 if (isLongDecisionOptionsMockInput(trimmedInput)) {
2328 pendingAskPreview = true;
2329 await delay(250);
2330 if (cancelled) return;
2331
2332 const longDescription = (...parts: string[]) => [...parts, ...parts, ...parts].join(" ");
2333 const longLabel = (...parts: string[]) => [...parts, ...parts, ...parts].join(" · ");
2334 const q1Option1Description = t("mock.askQ1Opt1Desc");
2335 const q1Option2Description = t("mock.askQ1Opt2Desc");
2336 const q1Option3Description = t("mock.askQ1Opt3Desc");
2337 const q2Option1Description = t("mock.askQ2Opt1Desc");
2338 const q2Option2Description = t("mock.askQ2Opt2Desc");
2339 const q2Option3Description = t("mock.askQ2Opt3Desc");
2340 const allowOnceDescription = t("approval.allowOnceDesc");
2341 const denyDescription = t("approval.denyDesc");
2342
2343 emit({
2344 kind: "ask_request",
2345 ask: {
2346 id: "mock-long-options",
2347 questions: [
2348 {
2349 id: "long-options",
2350 header: `${t("mock.askQ1Header")} · QA`,
2351 prompt: `${t("mock.askQ1Prompt")} ${t("mock.askQ2Prompt")}`,
2352 options: [
2353 {
2354 label: t("mock.askQ1Opt1Label"),
2355 description: longDescription(q1Option1Description, q2Option1Description, allowOnceDescription),
2356 },
2357 {
2358 label: t("mock.askQ1Opt2Label"),
2359 description: longDescription(q1Option2Description, q2Option2Description, denyDescription),
2360 },
2361 {
2362 label: t("mock.askQ1Opt3Label"),
2363 description: longDescription(q1Option3Description, q2Option3Description, allowOnceDescription),
2364 },
2365 {
2366 // Deliberately omit description here: this exercises the
2367 // legacy/malformed payload fallback where the complete
2368 // decision was placed in label instead of split into a
2369 // short label plus supporting description.
2370 label: longLabel(
2371 t("mock.askQ2Opt1Label"),
2372 t("mock.askQ1Opt3Label"),
2373 t("mock.askQ2Opt3Label"),
2374 t("mock.askQ1Opt1Label"),
2375 ),
2376 },
2377 {
2378 label: t("mock.askQ2Opt2Label"),
2379 description: longDescription(
2380 q2Option2Description,
2381 "DecisionSurfacePreviewWithAnExtremelyLongUnbrokenIdentifierForOverflowVerification0123456789",
2382 q1Option1Description,
2383 ),
2384 },
2385 {
2386 label: t("mock.askQ2Opt3Label"),
2387 description: longDescription(q2Option3Description, q1Option1Description, q2Option2Description),
2388 },
2389 {
2390 label: t("approval.deny"),
2391 description: longDescription(denyDescription, q1Option2Description, q2Option1Description),
2392 },
2393 ],
2394 },
2395 ],
2396 },
2397 });
2398 return;
2399 }
2400 if (decisionSurfaceMock === "ask") {
2401 pendingAskPreview = true;
2402 await delay(250);
2403 if (cancelled) return;
2404 emit({
2405 kind: "ask_request",
2406 ask: {
2407 id: "mock-ask-preview",
2408 questions: [
2409 {
2410 id: "q1",
2411 header: t("mock.askQ1Header"),
2412 prompt: t("mock.askQ1Prompt"),
2413 options: [
2414 { label: t("mock.askQ1Opt1Label"), description: t("mock.askQ1Opt1Desc") },
2415 { label: t("mock.askQ1Opt2Label"), description: t("mock.askQ1Opt2Desc") },
2416 { label: t("mock.askQ1Opt3Label"), description: t("mock.askQ1Opt3Desc") },
2417 ],
2418 },
2419 {
2420 id: "q2",
2421 header: t("mock.askQ2Header"),
2422 prompt: t("mock.askQ2Prompt"),
2423 options: [
2424 { label: t("mock.askQ2Opt1Label"), description: t("mock.askQ2Opt1Desc") },
2425 { label: t("mock.askQ2Opt2Label"), description: t("mock.askQ2Opt2Desc") },
2426 { label: t("mock.askQ2Opt3Label"), description: t("mock.askQ2Opt3Desc") },
2427 ],
2428 },
2429 ],
2430 },
2431 });
2432 return;
2433 }
2434 if (trimmedInput === "/todo-preview" || trimmedInput === "todo preview" || trimmedInput === "todo预览") {
2435 await delay(250);
2436 if (cancelled) return;
2437 emit({
2438 kind: "tool_dispatch",
2439 tool: {
2440 id: "mock-todo-preview",
2441 name: "todo_write",
2442 args: JSON.stringify({
2443 todos: [
2444 { content: t("mock.todo1"), status: "completed" },
2445 { content: t("mock.todo2"), activeForm: t("mock.todo2ActiveForm"), status: "in_progress" },
2446 { content: t("mock.todo3"), status: "pending" },
2447 ],
2448 }),
2449 readOnly: false,
2450 },
2451 });
2452 await delay(150);
2453 emit({
2454 kind: "tool_result",
2455 tool: {
2456 id: "mock-todo-preview",
2457 name: "todo_write",
2458 args: JSON.stringify({
2459 todos: [
2460 { content: t("mock.todo1"), status: "completed" },
2461 { content: t("mock.todo2"), activeForm: t("mock.todo2ActiveForm"), status: "in_progress" },
2462 { content: t("mock.todo3"), status: "pending" },
2463 ],
2464 }),
2465 output: "todo list updated",
2466 readOnly: false,
2467 durationMs: 150,
2468 },
2469 });
2470 emitMockTurnDone();
2471 return;
2472 }
2473 if (trimmedInput === "/process-preview" || trimmedInput === "process preview" || trimmedInput === "过程预览") {
2474 await delay(200);
2475 if (cancelled) return;
2476 emit({ kind: "phase", text: "Preparing context" });
2477 await delay(120);
2478 emit({ kind: "notice", level: "info", text: "Loaded project instructions from AGENTS.md." });
2479 await delay(120);
2480 emit({ kind: "notice", level: "warn", text: "Network access is enabled; external results may change over time." });
2481 await delay(120);
2482 emit({ kind: "compaction_started", compaction: { trigger: "manual" } });
2483 await delay(320);
2484 emit({
2485 kind: "compaction_done",
2486 compaction: {
2487 trigger: "manual",
2488 messages: 6,
2489 summary: "Preserved the active task, relevant files, and UI decisions while trimming earlier exploratory context.",
2490 },
2491 });
2492 emit({ kind: "message", text: "Process card preview complete." });
2493 emitMockTurnDone();
2494 return;
2495 }
2496 if (trimmedInput === "/nested-preview" || trimmedInput === "nested preview" || trimmedInput === "嵌套预览") {
2497 const parentId = "mock-nested-explore";
2498 await delay(180);
2499 if (cancelled) return;
2500 emit({
2501 kind: "reasoning",
2502 text: "我先快速探索相关文件,再整理这个工具行的视觉层级。",
2503 });
2504 emit({
2505 kind: "message",
2506 text: "",
2507 reasoning: "我先快速探索相关文件,再整理这个工具行的视觉层级。",
2508 });
2509 emit({
2510 kind: "tool_dispatch",
2511 tool: {
2512 id: parentId,
2513 name: "explore",
2514 args: JSON.stringify({ task: "在 Reasonix 前端中检查工具调用图标和嵌套调用展示" }),
2515 readOnly: true,
2516 profile: { model: "mock-reasonix", effort: "high" },
2517 },
2518 });
2519 for (let i = 1; i <= 30; i += 1) {
2520 if (cancelled) return;
2521 const id = `mock-nested-${i}`;
2522 const isSearch = i % 3 === 0;
2523 const name = isSearch ? "grep" : "read_file";
2524 const args = isSearch
2525 ? { pattern: i % 2 === 0 ? "tool__nested-count" : "explore", path: "desktop/frontend/src" }
2526 : { path: `desktop/frontend/src/${i % 2 === 0 ? "components/ToolCard.tsx" : "styles.css"}`, offset: i * 10, limit: 40 };
2527 emit({ kind: "tool_dispatch", tool: { id, name, args: JSON.stringify(args), readOnly: true, parentId } });
2528 emit({
2529 kind: "tool_result",
2530 tool: {
2531 id,
2532 name,
2533 readOnly: true,
2534 output: isSearch ? "3 matches" : "read 40 lines",
2535 durationMs: 24 + i,
2536 },
2537 });
2538 await delay(18);
2539 }
2540 emit({
2541 kind: "tool_result",
2542 tool: {
2543 id: parentId,
2544 name: "explore",
2545 readOnly: true,
2546 output: "已读 20 个文件 · 搜索 10 个文件",
2547 durationMs: 61510,
2548 },
2549 });
2550 emit({
2551 kind: "message",
2552 text: "Mock nested tool preview complete. The explore row now shows the compass count marker.",
2553 });
2554 emitMockTurnDone();
2555 return;
2556 }
2557 // Simulate the server's pre-first-token latency so the deferred user bubble
2558 // and the "un-send on Esc before any reply" path are observable in browser
2559 // dev. Bail if cancelled during the wait — nothing was streamed yet.
2560 await delay(700);
2561 if (cancelled) return;
2562 const reasoningChunks = [
2563 "我先判断这是浏览器预览环境,所以不会调用真实 kernel。\n",
2564 "接着模拟 provider 的 reasoning delta:先展示思考过程,再切到正式回复。\n",
2565 "完成后前端应该把过程区折叠成“已工作 N 秒”。\n",
2566 ];
2567 for (const chunk of reasoningChunks) {
2568 if (cancelled) return;
2569 emit({ kind: "reasoning", reasoning: chunk });
2570 await delay(520);
2571 }
2572 if (cancelled) return;
2573 await delay(260);
2574 const reply =
2575 `You said: **${input}**\n\n` +
2576 "This is the browser dev mock — the real reply comes from the kernel " +
2577 "inside the Wails shell. Here's a fenced block to exercise the editor seam:\n\n" +
2578 "```go\nfunc main() {\n println(\"hello from the mock\")\n}\n```\n";
2579 for (const ch of reply) {
2580 if (cancelled) break;
2581 emit({ kind: "text", text: ch });
2582 await delay(6);
2583 }
2584 emit({ kind: "message", text: reply });
2585 emit({
2586 kind: "tool_dispatch",
2587 tool: {
2588 id: "t1",
2589 name: "edit_file",
2590 args: '{"path":"main.go","old_string":"println(\\"hi\\")","new_string":"println(\\"hello\\")"}',
2591 readOnly: false,
2592 },
2593 });
2594 await delay(350);
2595 emit({
2596 kind: "tool_result",
2597 tool: { id: "t1", name: "edit_file", output: "edited main.go", readOnly: false, durationMs: 350 },
2598 });
2599 emit({
2600 kind: "usage",
2601 usage: {
2602 promptTokens: 1280,
2603 completionTokens: 64,
2604 totalTokens: 1344,
2605 cacheHitTokens: 1024,
2606 cacheMissTokens: 256,
2607 sessionCacheHitTokens: 1024,
2608 sessionCacheMissTokens: 256,
2609 },
2610 });
2611 emitMockTurnDone();
2612 },
2613 async SubmitToTab(_tabID, input) {
2614 await withMockTabScope(_tabID, () => this.Submit(input));
2615 },
2616 async SubmitDisplay(_display, input) {
2617 await this.Submit(input);
2618 },
2619 async SubmitDisplayToTab(_tabID, display, input) {
2620 await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input));
2621 },
2622 async SubmitDeliveryRecoveryToTab(_tabID, display, input) {
2623 await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input));
2624 },
2625 async SubmitInvocationsToTab(_tabID, display, input, _invocations) {
2626 await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input));
2627 },
2628 async SubmitInitialGoalToTab(
2629 _tabID,
2630 goal,
2631 display,
2632 input,
2633 invocations,
2634 _collaborationMode,
2635 _toolApprovalMode,
2636 ) {
2637 return await withMockTabScope(_tabID, async () => {
2638 await this.SetGoalForTab(_tabID, goal);
2639 if (invocations.length > 0) {
2640 await this.SubmitInvocationsToTab(_tabID, display, input, invocations);
2641 return [];
2642 }
2643 await this.SubmitDisplayToTab(_tabID, display, input);
2644 return [];
2645 });
2646 },
2647 async SubmitEditedDisplayToTab(_tabID, display, input, _original) {
2648 await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input));
2649 },
2650 async RunShell(command) {
2651 cancelled = false;
2652 emitMockTurnStarted();
2653 await delay(100);
2654 if (cancelled) return;
2655 const id = `shell-${command.slice(0, 32)}`;
2656 emit({ kind: "tool_dispatch", tool: { id, name: "bash", args: JSON.stringify({ command }), readOnly: false } });
2657 await delay(200);
2658 if (cancelled) return;
2659 emit({ kind: "tool_progress", tool: { id, name: "bash", output: `$ ${command}\n(mock output)\n`, readOnly: false } });
2660 await delay(100);
2661 if (cancelled) return;
2662 emit({ kind: "tool_result", tool: { id, name: "bash", output: `$ ${command}\n(mock output)\n`, readOnly: false, durationMs: 300 } });
2663 emitMockTurnDone();
2664 },
2665 async RunShellForTab(_tabID, command) {
2666 await withMockTabScope(_tabID, () => this.RunShell(command));
2667 },
2668 async Steer(_text) {
2669 // Mock: emit a steer event as confirmation in the transcript.
2670 emit({ kind: "steer", text: _text });
2671 },
2672 async SteerForTab(_tabID, _text) {
2673 await this.Steer(_text);
2674 },
2675 async Cancel() {
2676 cancelled = true;
2677 emitMockTurnDone();
2678 },
2679 async CancelTab(_tabID) {
2680 await withMockTabScope(_tabID, () => this.Cancel());
2681 },
2682 async Approve(_id, allow, session, persist) {
2683 if (!pendingApprovalPreview) return;
2684 pendingApprovalPreview = false;
2685 pendingApprovalPreviewPrompt = undefined;
2686 const suffix = persist ? "grant saved" : session ? "grant active this session" : "allowed once";
2687 emit({
2688 kind: "message",
2689 text: `approval preview answered: ${allow ? suffix : "denied"}`,
2690 });
2691 emitMockTurnDone();
2692 },
2693 async ApproveTab(_tabID, id, allow, session, persist) {
2694 await withMockTabScope(_tabID, () => this.Approve(id, allow, session, persist));
2695 },
2696 async ResolvePlanDecision(id, action) {
2697 const active = mockTabs.find((tab) => tab.active);
2698 await this.ResolvePlanDecisionTab(active?.id ?? "", id, action);
2699 },
2700 async ResolvePlanDecisionTab(_tabID, id, action) {
2701 await withMockTabScope(_tabID, async () => {
2702 void id;
2703 pendingApprovalPreview = false;
2704 pendingApprovalPreviewPrompt = undefined;
2705 emit({
2706 kind: "message",
2707 text: `plan preview answered: ${action}`,
2708 });
2709 emitMockTurnDone();
2710 });
2711 },
2712 async ResolveRecovery(id, action, feedback) {
2713 const active = mockTabs.find((tab) => tab.active);
2714 await this.ResolveRecoveryTab(active?.id ?? "", id, action, feedback);
2715 },
2716 async ResolveRecoveryTab(_tabID, id, action, feedback) {
2717 void id;
2718 void feedback;
2719 pendingApprovalPreview = false;
2720 pendingApprovalPreviewPrompt = undefined;
2721 emit({
2722 kind: "message",
2723 text: `recovery preview answered: ${action}`,
2724 });
2725 emitMockTurnDone();
2726 },
2727 async SetRecoveryCheckpointEnabled(_enabled) {},
2728 async SetRecoveryCheckpointEnabledTab(_tabID, _enabled) {},
2729 async RecoveryCheckpointEnabled() {
2730 return true;
2731 },
2732 async RecoveryCheckpointEnabledTab(_tabID) {
2733 return true;
2734 },
2735 async AnswerQuestion(_id, answers) {
2736 if (!pendingAskPreview) return;
2737 pendingAskPreview = false;
2738 const summary = answers
2739 .map((answer) => `${answer.questionId}: ${(answer.selected ?? []).join(", ") || "(no answer)"}`)
2740 .join("\n");
2741 emit({ kind: "message", text: `ask preview answered:\n\n${summary}` });
2742 emitMockTurnDone();
2743 },
2744 async AnswerQuestionForTab(_tabID, id, answers) {
2745 await withMockTabScope(_tabID, () => this.AnswerQuestion(id, answers));
2746 },
2747 async ReplayPendingPrompts() {},
2748 async ConfirmAction(req) {
2749 void req;
2750 return false;
2751 },
2752 async SetPlanMode(on) {
2753 const active = mockTabs.find((tab) => tab.active);
2754 if (active) await this.SetModeForTab(active.id, modeWithPlan(normalizeMode(active.mode), on));
2755 },
2756 async SetMode(mode) {
2757 const active = mockTabs.find((tab) => tab.active);
2758 if (active) await this.SetModeForTab(active.id, mode);
2759 },
2760 async SetModeForTab(tabID, mode) {
2761 const nextMode = normalizeMode(mode);
2762 let nextToolApprovalMode: ToolApprovalMode | "" = "";
2763 mockTabs = mockTabs.map((tab) => {
2764 if (tab.id !== tabID) return tab;
2765 nextToolApprovalMode = mockToolApprovalModeAfterModeChange(tab.toolApprovalMode, nextMode);
2766 return {
2767 ...tab,
2768 mode: nextMode,
2769 collaborationMode: normalizeCollaborationMode(undefined, tab.goal, nextMode),
2770 toolApprovalMode: nextToolApprovalMode,
2771 };
2772 });
2773 return drainMockApprovalPreviews(nextToolApprovalMode);
2774 },
2775 async SetCollaborationMode(mode) {
2776 const active = mockTabs.find((tab) => tab.active);
2777 if (active) await this.SetCollaborationModeForTab(active.id, mode);
2778 },
2779 async SetCollaborationModeForTab(tabID, mode) {
2780 const next = normalizeCollaborationMode(mode);
2781 mockTabs = mockTabs.map((tab) => {
2782 if (tab.id !== tabID) return tab;
2783 const toolMode = normalizeToolApprovalMode(tab.toolApprovalMode, normalizeMode(tab.mode));
2784 return {
2785 ...tab,
2786 collaborationMode: next,
2787 goal: next === "normal" || next === "plan" ? "" : tab.goal,
2788 mode: modeWithPlan(modeWithAutoApproveTools(normalizeMode(tab.mode), toolMode === "yolo"), next === "plan"),
2789 };
2790 });
2791 },
2792 async SetToolApprovalMode(mode) {
2793 const active = mockTabs.find((tab) => tab.active);
2794 if (active) await this.SetToolApprovalModeForTab(active.id, mode);
2795 },
2796 async SetToolApprovalModeForTab(tabID, mode) {
2797 const next = normalizeToolApprovalMode(mode);
2798 settings.autoApproveTools = next === "yolo";
2799 settings.bypass = next === "yolo";
2800 mockTabs = mockTabs.map((tab) =>
2801 tab.id === tabID
2802 ? {
2803 ...tab,
2804 toolApprovalMode: next,
2805 mode: modeWithAutoApproveTools(normalizeMode(tab.mode), next === "yolo"),
2806 }
2807 : tab,
2808 );
2809 return drainMockApprovalPreviews(next);
2810 },
2811 async SetComposerProfileForTab(tabID, collaborationMode, toolApprovalMode, goal) {
2812 const nextCollaboration = normalizeCollaborationMode(collaborationMode);
2813 const nextToolApproval = normalizeToolApprovalMode(toolApprovalMode);
2814 const nextGoal = goal.trim();
2815 settings.autoApproveTools = nextToolApproval === "yolo";
2816 settings.bypass = nextToolApproval === "yolo";
2817 mockTabs = mockTabs.map((tab) => {
2818 if (tab.id !== tabID) return tab;
2819 const plan = !nextGoal && nextCollaboration === "plan";
2820 return {
2821 ...tab,
2822 collaborationMode: nextGoal ? "goal" : plan ? "plan" : "normal",
2823 toolApprovalMode: nextToolApproval,
2824 goal: nextGoal,
2825 goalStatus: nextGoal ? "running" : "stopped",
2826 mode: modeWithAutoApproveTools(modeWithPlan(normalizeMode(tab.mode), plan), nextToolApproval === "yolo"),
2827 };
2828 });
2829 return drainMockApprovalPreviews(nextToolApproval);
2830 },
2831 async SetGoal(goal) {
2832 const active = mockTabs.find((tab) => tab.active);
2833 if (active) await this.SetGoalForTab(active.id, goal);
2834 },
2835 async SetGoalForTab(tabID, goal) {
2836 const nextGoal = goal.trim();
2837 mockTabs = mockTabs.map((tab) =>
2838 tab.id === tabID
2839 ? {
2840 ...tab,
2841 goal: nextGoal,
2842 goalStatus: nextGoal ? "running" : "stopped",
2843 collaborationMode: nextGoal ? "goal" : "normal",
2844 mode: modeWithPlan(normalizeMode(tab.mode), false),
2845 }
2846 : tab,
2847 );
2848 },
2849 async ResumeGoalForTab(tabID) {
2850 let resumed = false;
2851 mockTabs = mockTabs.map((tab) => {
2852 if (tab.id !== tabID || !tab.goal || tab.goalStatus === "complete") return tab;
2853 resumed = true;
2854 return { ...tab, goalStatus: "running", collaborationMode: "goal", goalRuntime: undefined };
2855 });
2856 return resumed;
2857 },
2858 async PauseGoalForTab(tabID) {
2859 let paused = false;
2860 mockTabs = mockTabs.map((tab) => {
2861 if (tab.id !== tabID || !tab.goal || tab.goalStatus !== "running") return tab;
2862 paused = true;
2863 return { ...tab, goalStatus: "blocked", goalRuntime: undefined };
2864 });
2865 return paused;
2866 },
2867 async ClearGoal() {
2868 await this.SetGoal("");
2869 },
2870 async ClearGoalForTab(tabID) {
2871 await this.SetGoalForTab(tabID, "");
2872 },
2873 async Compact() {},
2874 async CompactForTab() {},
2875 async NewSession() {},
2876 async NewSessionForTab() {},
2877 async ClearSession() {},
2878 async ClearSessionForTab() {},
2879 async Checkpoints() {
2880 return [
2881 { turn: 0, prompt: "你好呀", files: ["src/App.tsx"], fileCount: 1, turnFileCount: 1, time: Date.now() - 30_000, canCode: true, canConversation: true },
2882 ];
2883 },
2884 async CheckpointsForTab() {
2885 return this.Checkpoints();
2886 },
2887 async Rewind() {},
2888 async RewindForTab() {},
2889 async PreviewRewindForTab() {
2890 return { ok: true, canFiles: true, canConversation: true, planId: "mock", fileCount: 0 };
2891 },
2892 async CommitRewindForTab() {
2893 return { ok: true, undoAvailable: true, transactionId: "mock-tx" };
2894 },
2895 async UndoRewindForTab() {
2896 return { ok: true, undoAvailable: false };
2897 },
2898 async PreviewWorkspaceFileRevertForTab(_tabID, path) {
2899 return { ok: true, canFiles: true, path, planId: "mock-file" };
2900 },
2901 async CommitWorkspaceFileRevertForTab() {
2902 return { ok: true, undoAvailable: true, transactionId: "mock-file-tx" };
2903 },
2904 async Fork() {
2905 const active = mockTabs.find((tab) => tab.active) ?? mockTabs[0];
2906 const tab: TabMeta = {
2907 ...active,
2908 id: "tab_fork_" + Date.now(),
2909 topicId: "topic_fork_" + Date.now(),
2910 topicTitle: `${active.topicTitle || t("rewind.fork")} · fork`,
2911 active: true,
2912 running: false,
2913 };
2914 mockTabs = [...mockTabs.map((item) => ({ ...item, active: false })), tab];
2915 return { ...tab };
2916 },
2917 async ForkForTab(tabID, turn) {
2918 mockTabs = mockTabs.map((tab) => ({ ...tab, active: tab.id === tabID }));
2919 return this.Fork(turn);
2920 },
2921 async SummarizeFrom() {},
2922 async SummarizeFromForTab() {},
2923 async SummarizeUpTo() {},
2924 async SummarizeUpToForTab() {},
2925 async History() {
2926 return [];
2927 },
2928 async HistoryForTab(tabID?: string) {
2929 const tab = mockTabs.find((item) => item.id === tabID) ?? mockTabs.find((item) => item.active);
2930 if (tab?.topicId) {
2931 queueMockTopicRuntime(tab);
2932 return mockTopicHistory(tab.topicId);
2933 }
2934 return this.History();
2935 },
2936 async HistoryPage(beforeTurn = 0, limit = 60) {
2937 return mockHistoryPage(await this.History(), beforeTurn, limit);
2938 },
2939 async HistoryPageForTab(tabID: string, beforeTurn = 0, limit = 60) {
2940 return mockHistoryPage(await this.HistoryForTab(tabID), beforeTurn, limit);
2941 },
2942 async HistoryCheckpointTurnsForTab(tabID: string) {
2943 const turns: number[] = [];
2944 for (const message of await this.HistoryForTab(tabID)) {
2945 if (message.role !== "user") continue;
2946 turns.push(message.checkpointTurn ?? turns.length);
2947 }
2948 return turns;
2949 },
2950 async ListSessions() {
2951 return sessions.map((s) => ({ ...s }));
2952 },
2953 async ListSessionsForTab() {
2954 return sessions.map((s) => ({ ...s }));
2955 },
2956 async ListTrashedSessions() {
2957 return trashedSessions.map((s) => ({ ...s }));
2958 },
2959 async ResumeSession(path: string) {
2960 sessions.forEach((s) => {
2961 s.current = s.path === path;
2962 s.open = s.open || s.path === path;
2963 });
2964 return [
2965 { role: "user", content: `(mock) resumed ${path}` },
2966 { role: "assistant", content: "This is a mock resumed transcript — the real one comes from the kernel." },
2967 ];
2968 },
2969 async ResumeSessionForTab(_tabID: string, path: string) {
2970 return this.ResumeSession(path);
2971 },
2972 async ResumeSessionPage(path: string, limit = 60) {
2973 return mockHistoryPage(await this.ResumeSession(path), 0, limit);
2974 },
2975 async ResumeSessionPageForTab(_tabID: string, path: string, limit = 60) {
2976 return this.ResumeSessionPage(path, limit);
2977 },
2978 async OpenChannelSessionForTab(tabID: string, path: string) {
2979 mockTabs = mockTabs.map((tab) => tab.id === tabID ? { ...tab, sessionPath: path, readOnly: true } : tab);
2980 return this.ResumeSession(path);
2981 },
2982 async OpenChannelSessionPageForTab(tabID: string, path: string, limit = 60) {
2983 return mockHistoryPage(await this.OpenChannelSessionForTab(tabID, path), 0, limit);
2984 },
2985 async PreviewSession(path: string) {
2986 const s = sessions.find((x) => x.path === path) ?? trashedSessions.find((x) => x.path === path);
2987 return [
2988 { role: "user", content: s?.preview || `(mock) preview ${path}` },
2989 { role: "phase", content: "Preparing read-only preview" },
2990 {
2991 role: "assistant",
2992 content: "This is a read-only mock preview. The active conversation is unchanged.",
2993 reasoning: "Preview reads the saved session without resuming it.",
2994 },
2995 { role: "notice", level: "info", content: "Preview mode keeps the active conversation untouched." },
2996 { role: "compaction", content: "", trigger: "manual", messages: 3, summary: "Mock preview preserved the latest task, tool result, and answer summary." },
2997 ];
2998 },
2999 async DeleteSession(path: string) {
3000 const i = sessions.findIndex((s) => s.path === path);
3001 if (i >= 0) {
3002 const [s] = sessions.splice(i, 1);
3003 trashedSessions.unshift({
3004 ...s,
3005 current: false,
3006 open: false,
3007 path: s.path.replace("/mock/sessions/", "/mock/sessions/.trash/"),
3008 deletedAt: Date.now(),
3009 });
3010 }
3011 },
3012 async DeleteRecoveryCopy(path: string) {
3013 return this.DeleteSession(path);
3014 },
3015 async RestoreSession(path: string) {
3016 const i = trashedSessions.findIndex((s) => s.path === path);
3017 if (i >= 0) {
3018 const [s] = trashedSessions.splice(i, 1);
3019 sessions.unshift({
3020 ...s,
3021 path: s.path.replace("/mock/sessions/.trash/", "/mock/sessions/"),
3022 deletedAt: undefined,
3023 });
3024 }
3025 },
3026 async PurgeTrashedSession(path: string) {
3027 const i = trashedSessions.findIndex((s) => s.path === path);
3028 if (i >= 0) trashedSessions.splice(i, 1);
3029 },
3030 async PurgeRecoveryCopy(path: string) {
3031 return this.PurgeTrashedSession(path);
3032 },
3033 async RenameSession(path: string, title: string) {
3034 const s = sessions.find((x) => x.path === path);
3035 if (s) s.title = title.trim() || undefined;
3036 },
3037 async ScanPromptHistory(nonce: string) {
3038 // Dev mock returns a static set of sample prompts for UI development.
3039 const entries: PromptHistoryEntry[] = [
3040 { text: "Explain the architecture of this project", at: Date.now() - 60000, sessionPath: "/mock/sessions/arch.jsonl", turn: 0 },
3041 { text: "Fix the login button styling", at: Date.now() - 120000, sessionPath: "/mock/sessions/arch.jsonl", turn: 1 },
3042 { text: "What is the capital of France?", at: Date.now() - 300000, sessionPath: "/mock/sessions/general.jsonl", turn: 0 },
3043 ];
3044 return { entries, nonce: "mock-" + nonce, olderCursor: "", hasOlder: false };
3045 },
3046 async ListWorkspaces() {
3047 return mockProjectTree
3048 .filter((node) => node.kind === "project" && node.root)
3049 .map((node) => ({
3050 path: node.root!,
3051 name: node.label || baseName(node.root!),
3052 current: node.root === cwd,
3053 }));
3054 },
3055 async PickWorkspace() {
3056 // Browser dev has no native dialog; simulate picking a folder and re-root so
3057 // the topbar folder chip visibly changes.
3058 return mockSwitchWorkspace(cwd.endsWith("another-project") ? "~/projects/reasonix" : "~/projects/another-project");
3059 },
3060 async SwitchWorkspace(path: string) {
3061 return mockSwitchWorkspace(path);
3062 },
3063 async RemoveWorkspace(path: string) {
3064 workspaces = workspaces.filter((p) => p !== path);
3065 const index = mockProjectTree.findIndex((node) => node.root === path);
3066 if (index >= 0) mockProjectTree.splice(index, 1);
3067 },
3068 async ContextUsage() {
3069 return { used: 42124, window: 128000, sessionTokens: 34479, compactRatio: 0.8 };
3070 },
3071 async ContextUsageForTab() {
3072 return this.ContextUsage();
3073 },
3074 async Balance() {
3075 // Mirror the active mock provider: deepseek-flash carries a balance_url.
3076 const p = settings.providers.find((x) => x.name === settings.defaultModel);
3077 if (!p?.balanceUrl) return { available: false, display: "" };
3078 return { available: true, display: "¥128.50" };
3079 },
3080 async BalanceForTab() {
3081 return this.Balance();
3082 },
3083 async UsageStats() {
3084 // Browser dev mock has no stats files; the panel does not consume
3085 // provider aggregates, so keep this initial-bundle fallback lean.
3086 return { from: "", to: "", tokens: 0, requests: 0, turns: 0, cacheHit: 0, cacheMiss: 0, activeDays: 0, topModel: "", daily: [], models: [] } as unknown as UsageStatsRange;
3087 },
3088 async Jobs() {
3089 return []; // browser dev mock has no background jobs
3090 },
3091 async JobsForTab() {
3092 return this.Jobs();
3093 },
3094 async CancelJob() {
3095 return false;
3096 },
3097 async CancelJobForTab(_tabID, jobID) {
3098 return this.CancelJob(jobID);
3099 },
3100 async CancelJobsForTab(_tabID, jobIDs) {
3101 return { cancelled: [], notRunning: [...jobIDs] };
3102 },
3103 async ActiveWorkForTab() {
3104 return { running: false, pendingPrompt: false, cancellable: false, jobs: [] };
3105 },
3106 async BackgroundRuntimes() {
3107 return [];
3108 },
3109 async RevealBackgroundRuntime() {
3110 throw new Error("background runtime is unavailable in browser preview");
3111 },
3112 async WorkspaceConflictForTab() {
3113 return {
3114 state: "none", ownerWork: { running: false, pendingPrompt: false, cancellable: false, jobs: [] },
3115 canReveal: false, canCreateWorktree: false,
3116 };
3117 },
3118 async RevealWorkspaceWriterForTab() {
3119 throw new Error("workspace writer is unavailable in browser preview");
3120 },
3121 async CloseTabWithPolicy(tabID) {
3122 return this.CloseTab(tabID);
3123 },
3124 async ToolResultForTab() {
3125 return null;
3126 },
3127 async Meta() {
3128 const active = mockTabs.find((tab) => tab.active) ?? mockTabs[0];
3129 const toolApprovalMode = normalizeToolApprovalMode(active?.toolApprovalMode, active ? normalizeMode(active.mode) : "normal", settings.autoApproveTools);
3130 const autoApproveTools = toolApprovalMode === "yolo";
3131 const collaborationMode = normalizeCollaborationMode(active?.collaborationMode, active?.goal, active ? normalizeMode(active.mode) : "normal");
3132 const workspacePath = active?.workspacePath || active?.workspaceRoot || active?.cwd || cwd;
3133 return {
3134 label: active?.label ?? "DeepSeek-R1",
3135 ready: active?.ready ?? true,
3136 eventChannel: EVENT_CHANNEL,
3137 cwd: active?.cwd || cwd,
3138 workspaceRoot: active?.workspaceRoot || workspacePath,
3139 workspaceName: active?.workspaceName,
3140 workspacePath,
3141 sandboxPath: settings.sandbox.workspaceRoot,
3142 gitBranch: active?.gitBranch || (active?.scope === "project" ? "main" : ""),
3143 imageInputEnabled: true,
3144 autoApproveTools,
3145 bypass: autoApproveTools,
3146 collaborationMode,
3147 toolApprovalMode,
3148 tokenMode: normalizeTokenMode(active?.tokenMode),
3149 goal: active?.goal ?? "",
3150 goalStatus: active?.goalStatus ?? (active?.goal ? "running" : "stopped"),
3151 autoResearch: active?.goal ? { taskId: "mock-autoresearch", status: "running", iteration: 4, pivotRequired: false, staleCount: 0 } : undefined,
3152 };
3153 },
3154 async MetaForTab(tabID) {
3155 const tab = mockTabs.find((item) => item.id === tabID) ?? mockTabs.find((item) => item.active) ?? mockTabs[0];
3156 const toolApprovalMode = normalizeToolApprovalMode(tab?.toolApprovalMode, tab ? normalizeMode(tab.mode) : "normal", settings.autoApproveTools);
3157 const autoApproveTools = toolApprovalMode === "yolo";
3158 const collaborationMode = normalizeCollaborationMode(tab?.collaborationMode, tab?.goal, tab ? normalizeMode(tab.mode) : "normal");
3159 const workspacePath = tab?.workspacePath || tab?.workspaceRoot || tab?.cwd || cwd;
3160 return {
3161 label: tab?.label ?? "DeepSeek-R1",
3162 ready: tab?.ready ?? true,
3163 eventChannel: EVENT_CHANNEL,
3164 cwd: tab?.cwd || cwd,
3165 workspaceRoot: tab?.workspaceRoot || workspacePath,
3166 workspaceName: tab?.workspaceName,
3167 workspacePath,
3168 sandboxPath: settings.sandbox.workspaceRoot,
3169 gitBranch: tab?.gitBranch || (tab?.scope === "project" ? "main" : ""),
3170 autoApproveTools,
3171 bypass: autoApproveTools,
3172 collaborationMode,
3173 toolApprovalMode,
3174 tokenMode: normalizeTokenMode(tab?.tokenMode),
3175 goal: tab?.goal ?? "",
3176 goalStatus: tab?.goalStatus ?? (tab?.goal ? "running" : "stopped"),
3177 autoResearch: tab?.goal ? { taskId: "mock-autoresearch", status: "running", iteration: 4, pivotRequired: false, staleCount: 0 } : undefined,
3178 };
3179 },
3180 async AutoResearchCurrent() {
3181 return {
3182 taskId: "mock-autoresearch",
3183 goal: "Mock long-running research",
3184 status: "running",
3185 iteration: 4,
3186 currentDirection: "Inspect status chip",
3187 staleCount: 0,
3188 pivotCount: 0,
3189 pivotRequired: false,
3190 lastHeartbeatAt: "2026-06-29T00:00:00Z",
3191 findingCount: 1,
3192 openCriteria: [],
3193 blocker: "",
3194 taskPath: "/tmp/mock/.reasonix/autoresearch/mock-autoresearch",
3195 nextRequiredAction: "continue with the next evidence-producing step",
3196 };
3197 },
3198 async AutoResearchStatus(_tabID) {
3199 return {
3200 taskId: "mock-autoresearch",
3201 goal: "Mock long-running research",
3202 status: "running",
3203 iteration: 4,
3204 currentDirection: "Inspect status chip",
3205 staleCount: 0,
3206 pivotCount: 0,
3207 pivotRequired: false,
3208 lastHeartbeatAt: "2026-06-29T00:00:00Z",
3209 findingCount: 1,
3210 openCriteria: [],
3211 blocker: "",
3212 taskPath: "/tmp/mock/.reasonix/autoresearch/mock-autoresearch",
3213 nextRequiredAction: "continue with the next evidence-producing step",
3214 };
3215 },
3216 async AutoResearchList(_tabID) {
3217 return [{
3218 taskId: "mock-autoresearch",
3219 goal: "Mock long-running research",
3220 status: "running",
3221 iteration: 4,
3222 currentDirection: "Inspect status chip",
3223 staleCount: 0,
3224 pivotCount: 0,
3225 pivotRequired: false,
3226 lastHeartbeatAt: "2026-06-29T00:00:00Z",
3227 findingCount: 1,
3228 openCriteria: [],
3229 blocker: "",
3230 taskPath: "/tmp/mock/.reasonix/autoresearch/mock-autoresearch",
3231 nextRequiredAction: "continue with the next evidence-producing step",
3232 }];
3233 },
3234 async AutoResearchFindings(_tabID, limit) {
3235 return [{
3236 id: "f1",
3237 kind: "test",
3238 summary: "Mock accepted finding",
3239 source: "command",
3240 command: "go test ./...",
3241 accepted: true,
3242 createdAt: "2026-06-29T00:00:00Z",
3243 }].slice(0, Math.max(0, limit || 1));
3244 },
3245 async AutoResearchOpenTask(_tabID) {
3246 console.info("mock AutoResearchOpenTask");
3247 },
3248 async AutoResearchRecordEvidence(_tabID, _criterionID, _input) {
3249 console.info("mock AutoResearchRecordEvidence");
3250 },
3251 async Commands() {
3252 const commands: CommandInfo[] = [
3253 { name: "new", description: "start new session; save transcript", kind: "builtin" as const, group: "actions" },
3254 { name: "clear", description: "discard current context", kind: "builtin" as const, group: "actions" },
3255 { name: "compact", description: "Summarize older history to free up context", kind: "builtin" as const, group: "actions" },
3256 { name: "model", description: "Switch model", kind: "builtin" as const, group: "actions" },
3257 { name: "effort", description: "Set reasoning effort", kind: "builtin" as const, group: "actions" },
3258 { name: "skill", description: "List skills", kind: "builtin" as const, group: "skills" },
3259 { name: "mcp", description: "Manage MCP servers", kind: "builtin" as const, group: "integrations" },
3260 { name: "plugins", description: "Manage plugin packages", kind: "builtin" as const, group: "integrations" },
3261 { name: "review", description: "Review the staged diff", hint: "[focus]", kind: "custom" as const, group: "skills" },
3262 ];
3263 const seen = new Set(commands.map((command) => command.name));
3264 for (const skill of capSkills) {
3265 if (skill.enabled === false) continue;
3266 const name = (skill.invocation || `/${skill.name}`).replace(/^\/+/, "");
3267 if (!name || seen.has(name)) continue;
3268 seen.add(name);
3269 commands.push({
3270 name,
3271 description: skill.description,
3272 kind: skill.runAs === "subagent" ? "subagent" : "skill",
3273 group: skill.runAs === "subagent" ? "subagents" : "skills",
3274 color: skill.color,
3275 });
3276 }
3277 return commands;
3278 },
3279 async Capabilities() {
3280 return {
3281 servers: capServers.map((s) => ({ ...s })),
3282 skills: capSkills.map((s) => ({ ...s })),
3283 skillRoots: capSkillRoots.map((s) => ({ ...s })),
3284 plugins: capPlugins.map((p) => ({ ...p })),
3285 };
3286 },
3287 async MCPServers() {
3288 return capServers.map((s) => ({ ...s }));
3289 },
3290 async MCPMarketplace(query: string) {
3291 const servers = [
3292 {
3293 name: "io.modelcontextprotocol/server-filesystem",
3294 suggestedName: "server-filesystem",
3295 title: "Filesystem",
3296 description: "Secure file operations through MCP.",
3297 version: "1.0.0",
3298 installable: true,
3299 transport: "stdio",
3300 command: "npx",
3301 args: ["-y", "@modelcontextprotocol/server-filesystem@1.0.0"],
3302 },
3303 {
3304 name: "io.example/manual",
3305 suggestedName: "manual",
3306 title: "Manual setup example",
3307 description: "Requires an API key before installation.",
3308 version: "1.0.0",
3309 installable: false,
3310 unavailableReason: "package requires environment variables or arguments",
3311 args: [],
3312 },
3313 ];
3314 const normalized = query.trim().toLowerCase();
3315 return {
3316 servers: normalized ? servers.filter((entry) => [entry.name, entry.title, entry.description].join(" ").toLowerCase().includes(normalized)) : servers,
3317 cached: false,
3318 } as MCPMarketplaceView;
3319 },
3320 async MCPMarketplaceResolve(registryName: string) {
3321 const result = await this.MCPMarketplace(registryName);
3322 const entry = result.servers.find((candidate) => candidate.name.toLowerCase() === registryName.trim().toLowerCase());
3323 if (!entry) throw new Error(`MCP Registry has no server named ${JSON.stringify(registryName)}`);
3324 return entry;
3325 },
3326 async SkillsSettings() {
3327 return {
3328 skills: capSkills.map((s) => ({ ...s })),
3329 skillRoots: capSkillRoots.map((s) => ({ ...s })),
3330 };
3331 },
3332 async CapabilityDiagnostics(includeSessionRuntime: boolean) {
3333 const report: CapabilityDiagnosticsReport = {
3334 schema_version: 1,
3335 root: "<workspace>",
3336 live: false,
3337 summary: {
3338 errors: 0,
3339 warnings: 1,
3340 infos: includeSessionRuntime ? 1 : 0,
3341 instructions: 1,
3342 skills: capSkills.length,
3343 commands: 0,
3344 hooks: 0,
3345 plugins: capPlugins.length,
3346 mcp_servers: capServers.length,
3347 },
3348 instructions: { docs: [{ path: "<workspace>/AGENTS.md", scope: "project", directory: "<workspace>", depth: 0, order: 1 }] },
3349 skills: {
3350 roots: [{ path: "<workspace>/.reasonix/skills", scope: "project", status: "ok" }],
3351 entries: capSkills.map((s) => ({
3352 name: s.name,
3353 description: s.description,
3354 scope: s.scope,
3355 path: "(mock)",
3356 status: "winner",
3357 run_as: s.runAs,
3358 })),
3359 winners: capSkills.length,
3360 shadowed: 0,
3361 },
3362 commands: { roots: [], entries: [], winners: 0, shadowed: 0 },
3363 hooks: { trusted_project: true, project_defines_hooks: false, sources: [], entries: [] },
3364 plugins: {
3365 packages: capPlugins.map((p) => ({
3366 name: p.name,
3367 enabled: p.enabled,
3368 root: p.root || "<external>/plugin",
3369 skills: p.skills ?? 0,
3370 commands: 0,
3371 hooks: p.hooks ?? 0,
3372 mcp_servers: p.mcpServers ?? 0,
3373 status: p.enabled ? "ok" : "disabled",
3374 })),
3375 },
3376 mcp: {
3377 servers: capServers.map((s) => ({
3378 name: s.name,
3379 transport: s.transport || "stdio",
3380 start_intent: s.startIntent === "off" ? "off" : "automatic",
3381 source: "toml",
3382 runtime_status: includeSessionRuntime ? s.status || "connected" : undefined,
3383 tool_count: s.tools,
3384 env_keys: s.envKeys ?? [],
3385 header_keys: s.headerKeys ?? [],
3386 })),
3387 },
3388 issues: [
3389 {
3390 severity: "warning",
3391 code: "skill.missing_description",
3392 subsystem: "skills",
3393 name: "example",
3394 message: "mock warning for browser harness",
3395 remediation: "Add a description frontmatter field",
3396 settings_tab: "skills",
3397 },
3398 ...(includeSessionRuntime
3399 ? [{
3400 severity: "info" as const,
3401 code: "mcp.runtime_unavailable",
3402 subsystem: "mcp",
3403 message: "browser mock has no live Host; runtime fields are synthetic",
3404 settings_tab: "mcp",
3405 }]
3406 : []),
3407 ],
3408 };
3409 return JSON.parse(JSON.stringify(report)) as CapabilityDiagnosticsReport;
3410 },
3411 async Plugins() {
3412 return capPlugins.map((p) => ({ ...p }));
3413 },
3414 async PlanPluginInstall(source: string, options: PluginInstallOptions) {
3415 const name = options.name || source.split("/").filter(Boolean).pop()?.replace(/\.git$/, "") || "plugin";
3416 return JSON.stringify({
3417 ok: true,
3418 status: "planned",
3419 kind: "plugin",
3420 actions: [{ kind: "plugin", action: "install_plugin_package", name, source, status: "planned" }],
3421 });
3422 },
3423 async InstallPlugin(source: string, options: PluginInstallOptions) {
3424 const name = options.name || source.split("/").filter(Boolean).pop()?.replace(/\.git$/, "") || "plugin";
3425 const existing = capPlugins.findIndex((p) => p.name === name);
3426 const view: PluginView = {
3427 name,
3428 version: "dev",
3429 description: "Mock plugin",
3430 source,
3431 root: `~/.reasonix/plugins/${name}`,
3432 manifestKind: "reasonix",
3433 enabled: true,
3434 skills: 1,
3435 hooks: 0,
3436 mcpServers: 0,
3437 skillDetails: [{ name: "plan", description: "Plan work before implementation", invocation: "/plan", runAs: "inline" }],
3438 };
3439 if (existing >= 0) capPlugins[existing] = view;
3440 else capPlugins.push(view);
3441 return JSON.stringify({ ok: true, status: "done", kind: "plugin", actions: [{ kind: "plugin", name }] });
3442 },
3443 async RemovePlugin(name: string) {
3444 capPlugins = capPlugins.filter((p) => p.name !== name);
3445 },
3446 async SetPluginEnabled(name: string, enabled: boolean) {
3447 capPlugins = capPlugins.map((p) => p.name === name ? { ...p, enabled } : p);
3448 },
3449 async UpdatePlugin(name: string) {
3450 capPlugins = capPlugins.map((p) => p.name === name ? { ...p, version: p.version || "dev" } : p);
3451 return JSON.stringify({ ok: true, status: "done", kind: "plugin", name });
3452 },
3453 async PluginDoctor(name: string) {
3454 return capPlugins.find((p) => p.name === name) || {
3455 name,
3456 root: "",
3457 enabled: false,
3458 skills: 0,
3459 hooks: 0,
3460 mcpServers: 0,
3461 error: "plugin is not installed",
3462 };
3463 },
3464 async AddMCPServer(input: MCPServerInput) {
3465 const tools = input.transport === "stdio" ? 3 : 5;
3466 capServers.push({
3467 name: input.name,
3468 transport: input.transport,
3469 status: "connected",
3470 configured: true,
3471 autoStart: true,
3472 tier: "background",
3473 command: input.command,
3474 args: input.args,
3475 url: input.url,
3476 envKeys: input.env ? Object.keys(input.env).sort() : undefined,
3477 headerKeys: input.headers ? Object.keys(input.headers).sort() : undefined,
3478 tools,
3479 prompts: 0,
3480 resources: 0,
3481 toolList: Array.from({ length: tools }, (_, i) => ({
3482 name: `${input.name}_tool_${i + 1}`,
3483 description: `Mock tool ${i + 1} exposed by ${input.name}.`,
3484 })),
3485 });
3486 return tools;
3487 },
3488 async InstallMCPServer(input: MCPServerInput) {
3489 const tools = await this.AddMCPServer(input);
3490 return { name: input.name, state: "ready" as const, toolCount: tools, action: "none" as const, message: `${input.name} is ready` };
3491 },
3492 async UpdateMCPServer(name: string, input: MCPServerInput) {
3493 capServers = capServers.map((s) => {
3494 if (s.name !== name) return s;
3495 const connected = s.status === "connected" || s.status === "failed" || s.autoStart !== false;
3496 const nextStatus = s.status === "disabled" ? "disabled" : connected ? "connected" : "deferred";
3497 const nextTools = nextStatus === "connected" ? s.tools || (input.transport === "stdio" ? 3 : 5) : 0;
3498 return {
3499 ...s,
3500 transport: input.transport,
3501 status: nextStatus,
3502 command: input.transport === "stdio" ? input.command : "",
3503 args: input.transport === "stdio" ? input.args : [],
3504 url: input.transport === "stdio" ? "" : input.url,
3505 envKeys: input.env ? Object.keys(input.env).sort() : s.envKeys,
3506 headerKeys: input.headers ? Object.keys(input.headers).sort() : s.headerKeys,
3507 tools: nextTools,
3508 error: undefined,
3509 authStatus: nextStatus !== "connected" && input.transport !== "stdio" ? "possible" : undefined,
3510 authUrl: nextStatus !== "connected" && input.transport !== "stdio" ? input.url : undefined,
3511 };
3512 });
3513 },
3514 async RemoveMCPServer(name: string) {
3515 capServers = capServers.filter((s) => s.name !== name);
3516 },
3517 async AuthorizeAndConnectMCPServer(name: string) {
3518 capServers = capServers.map((s) =>
3519 s.name === name
3520 ? {
3521 ...s,
3522 status: "connected",
3523 runtimeState: "ready",
3524 tools: s.tools || 4,
3525 error: undefined,
3526 requiresLaunchApproval: false,
3527 }
3528 : s,
3529 );
3530 },
3531 async ReconnectMCPServer(name: string) {
3532 capServers = capServers.map((s) =>
3533 s.name === name
3534 ? { ...s, status: "initializing", error: undefined, authStatus: undefined, authUrl: undefined }
3535 : s,
3536 );
3537 await new Promise((r) => setTimeout(r, 400));
3538 capServers = capServers.map((s) =>
3539 s.name === name ? { ...s, status: "connected", tools: s.tools || 4 } : s,
3540 );
3541 },
3542 async ClearMCPServerAuthentication(name: string) {
3543 capServers = capServers.map((s) =>
3544 s.name === name
3545 ? {
3546 ...s,
3547 status: s.autoStart === false ? "disabled" : "initializing",
3548 tools: 0,
3549 error: undefined,
3550 authStatus: s.transport !== "stdio" ? "possible" : undefined,
3551 authUrl: s.transport !== "stdio" ? s.url : undefined,
3552 authConfigured: undefined,
3553 }
3554 : s,
3555 );
3556 },
3557 async PickSkillFolder() {
3558 return "~/my-skills";
3559 },
3560 async PickPluginFolder() {
3561 return "~/plugins/superpowers";
3562 },
3563 async AddSkillPath(path: string) {
3564 const dir = path.trim() || "~/my-skills";
3565 if (!capSkillRoots.some((r) => r.scope === "custom" && r.dir === dir)) {
3566 capSkillRoots.push({
3567 dir,
3568 scope: "custom",
3569 priority: capSkillRoots.length + 1,
3570 status: "ok",
3571 configured: true,
3572 removable: true,
3573 skills: 1,
3574 skillItems: [{ name: "local-dev", description: "Local custom development workflow", scope: "custom", runAs: "inline" }],
3575 });
3576 }
3577 if (!capSkills.some((s) => s.name === "local-dev")) {
3578 capSkills.push({ name: "local-dev", description: "Local custom development workflow", scope: "custom", runAs: "inline", enabled: true });
3579 }
3580 },
3581 async RemoveSkillPath(path: string) {
3582 capSkillRoots = capSkillRoots.filter((r) => r.dir !== path);
3583 if (!capSkillRoots.some((r) => r.scope === "custom")) {
3584 const idx = capSkills.findIndex((s) => s.name === "local-dev");
3585 if (idx >= 0) capSkills.splice(idx, 1);
3586 }
3587 },
3588 async RefreshSkills() {},
3589 async ReloadCommands() {},
3590 async SetSkillEnabled(name: string, enabled: boolean) {
3591 const skill = capSkills.find((s) => s.name === name);
3592 if (skill) skill.enabled = enabled;
3593 },
3594 async AvailableSubagentTools() {
3595 return [
3596 { name: "read_file", description: "Read a file's contents", readOnlyHint: true },
3597 { name: "ls", description: "List a directory", readOnlyHint: true },
3598 { name: "glob", description: "Find files by name pattern", readOnlyHint: true },
3599 { name: "grep", description: "Search file contents", readOnlyHint: true },
3600 { name: "code_index", description: "Look up symbol definitions and file outlines", readOnlyHint: true },
3601 { name: "edit_file", description: "Edit an existing file" },
3602 { name: "write_file", description: "Write a new file" },
3603 { name: "bash", description: "Run a shell command" },
3604 { name: "web_fetch", description: "Fetch a URL" },
3605 ];
3606 },
3607 async CreateSubagentProfile(input: SubagentProfileInput) {
3608 const name = input.name.trim();
3609 const builtinNames = ["init", "explore", "research", "install-capability", "review", "security-review", "test"];
3610 if (builtinNames.includes(name)) throw new Error(`"${name}" is a built-in subagent name and cannot be reused`);
3611 if (capSkills.some((s) => s.name === name)) throw new Error(`"${name}" already exists`);
3612 capSkills.push({
3613 name, description: input.description, scope: input.scope === "project" ? "project" : "global",
3614 runAs: "subagent", enabled: true, model: input.model, effort: input.effort,
3615 allowedTools: input.allowedTools, color: input.color, invocation: `/${name}`, invocationMode: "manual",
3616 });
3617 return `~/.reasonix/skills/${name}/SKILL.md`;
3618 },
3619 async UpdateSubagentProfile(name: string, scope: string, input: SubagentProfileInput) {
3620 const skill = capSkills.find((s) => s.name === name && s.scope === scope);
3621 if (!skill) throw new Error(`"${name}" resolves at a different scope — refusing to update`);
3622 skill.description = input.description;
3623 skill.color = input.color;
3624 skill.model = input.model;
3625 skill.effort = input.effort;
3626 skill.allowedTools = input.allowedTools;
3627 },
3628 async DeleteSubagentProfile(name: string, scope: string) {
3629 const idx = capSkills.findIndex((s) => s.name === name && s.scope === scope);
3630 if (idx < 0) throw new Error(`"${name}" resolves at a different scope — refusing to delete`);
3631 capSkills.splice(idx, 1);
3632 },
3633 async SetSubagentProfileModel(name: string, ref: string) {
3634 const skill = capSkills.find((s) => s.name === name);
3635 if (skill) skill.configuredModel = ref || undefined;
3636 },
3637 async SetSubagentProfileEffort(name: string, level: string) {
3638 const skill = capSkills.find((s) => s.name === name);
3639 if (skill) skill.configuredEffort = level || undefined;
3640 },
3641 async CancelTrySubagentProfile() {},
3642 async TrySubagentProfile(input: SubagentProfileInput, task: string) {
3643 if (!task.trim()) throw new Error("task is required");
3644 if (!input.systemPrompt.trim()) throw new Error("system prompt is required");
3645 await new Promise((resolve) => setTimeout(resolve, 400));
3646 return `[mock run of "${input.name || "draft"}"]\n\nTask: ${task}\n\n(This is a dev-mode mock response — the real backend runs an isolated subagent loop against your configured model.)`;
3647 },
3648 async SetMCPServerEnabled(name: string, enabled: boolean) {
3649 capServers = capServers.map((s) =>
3650 s.name === name
3651 ? {
3652 ...s,
3653 status: enabled ? "connected" : "disabled",
3654 autoStart: s.builtIn ? enabled : s.autoStart,
3655 tools: enabled ? s.tools || 4 : 0,
3656 error: undefined,
3657 authStatus: !enabled && s.transport !== "stdio" ? "possible" : undefined,
3658 authUrl: !enabled && s.transport !== "stdio" ? s.url : undefined,
3659 }
3660 : s,
3661 );
3662 },
3663 async SetMCPServerTier(name: string, tier: string) {
3664 capServers = capServers.map((s) => {
3665 if (s.name !== name) return s;
3666 const tools = s.tools || (s.transport === "stdio" ? 3 : 5);
3667 return { ...s, tier, autoStart: true, status: "connected", tools, error: undefined, authStatus: undefined, authUrl: undefined };
3668 });
3669 },
3670 async SlashArgs(input: string) {
3671 // Mirror a slice of the real arg hints so the menu is exercisable in browser dev.
3672 const from = input.lastIndexOf(" ") + 1;
3673 const cur = input.slice(from);
3674 const cmd = input.slice(0, input.indexOf(" ") < 0 ? input.length : input.indexOf(" "));
3675 const subs: Record<string, { label: string; insert: string; hint: string; descend?: boolean }[]> = {
3676 "/skill": [
3677 { label: "list", insert: "list", hint: "list skills" },
3678 { label: "show", insert: "show ", hint: "show a skill's body", descend: true },
3679 { label: "enable", insert: "enable ", hint: "enable a disabled skill", descend: true },
3680 { label: "disable", insert: "disable ", hint: "disable an enabled skill", descend: true },
3681 { label: "new", insert: "new ", hint: "scaffold a new skill" },
3682 { label: "paths", insert: "paths", hint: "show discovery paths" },
3683 ],
3684 "/hooks": [
3685 { label: "list", insert: "list", hint: "list active hooks" },
3686 ],
3687 "/model": [
3688 { label: "deepseek/deepseek-v4-flash", insert: "deepseek/deepseek-v4-flash", hint: "current" },
3689 { label: "deepseek/deepseek-v4-pro", insert: "deepseek/deepseek-v4-pro", hint: "" },
3690 ],
3691 "/effort": [
3692 { label: "auto", insert: "auto", hint: "use the model default" },
3693 { label: "high", insert: "high", hint: "deeper reasoning" },
3694 { label: "max", insert: "max", hint: "maximum reasoning" },
3695 ],
3696 };
3697 const items = (subs[cmd] ?? [])
3698 .filter((it) => it.label.toLowerCase().startsWith(cur.toLowerCase()))
3699 .map((it) => ({ label: it.label, insert: it.insert, hint: it.hint, descend: it.descend ?? false }));
3700 return { items, from };
3701 },
3702 async ListDir(rel: string) {
3703 // A tiny fake tree so the @ menu is navigable in browser dev.
3704 if (rel === "" || rel === "./") {
3705 return [
3706 { name: "internal", isDir: true },
3707 { name: "desktop", isDir: true },
3708 { name: "README.md", isDir: false },
3709 { name: "go.mod", isDir: false },
3710 ];
3711 }
3712 if (rel === "internal/") {
3713 return [
3714 { name: "control", isDir: true },
3715 { name: "boot", isDir: true },
3716 { name: "event.go", isDir: false },
3717 ];
3718 }
3719 return [{ name: "file.go", isDir: false }];
3720 },
3721 async ListDirForTab(_tabID: string, rel: string) {
3722 return this.ListDir(rel);
3723 },
3724 async SearchFileRefs(query: string) {
3725 const q = query.toLowerCase();
3726 return ["desktop/frontend/src/lib/bridge.ts", "frontend/wailsjs/runtime/runtime.js", "internal/control/refs.go"]
3727 .filter((path) => path.split("/").pop()?.toLowerCase().includes(q))
3728 .map((name) => ({ name, isDir: false }));
3729 },
3730 async SearchFileRefsForTab(_tabID: string, query: string) {
3731 return this.SearchFileRefs(query);
3732 },
3733 async ReadFile(rel: string) {
3734 const samples: Record<string, string> = {
3735 "README.md": "# Reasonix\n\nBrowser-dev workspace preview.\n\n- Chat in the center\n- Browse files on the right\n- Keep sessions on the left\n",
3736 "go.mod": "module reasonix\n\ngo 1.23\n",
3737 "desktop/file.go": "package desktop\n\nfunc main() {\n\tprintln(\"workspace preview\")\n}\n",
3738 "internal/event.go": "package internal\n\n// mock file used by the browser dev seam\n",
3739 };
3740 return {
3741 path: rel,
3742 body: samples[rel] ?? `// ${rel}\n\nMock file body from browser dev.`,
3743 size: samples[rel]?.length ?? 42,
3744 truncated: false,
3745 binary: false,
3746 };
3747 },
3748 async ReadFileForTab(_tabID: string, rel: string) {
3749 return this.ReadFile(rel);
3750 },
3751 async WorkspaceChanges(_tabID: string) {
3752 return {
3753 gitAvailable: true,
3754 gitBranch: "main",
3755 files: [
3756 {
3757 path: "desktop/frontend/src/components/WorkspacePanel.tsx",
3758 sources: ["session", "git"],
3759 gitStatus: "M",
3760 turns: [0, 2],
3761 latestPrompt: "Mock session edited the workspace panel.",
3762 latestTime: Date.now() - 60_000,
3763 },
3764 { path: "README.md", sources: ["git"], gitStatus: "??" },
3765 { path: "internal/control/controller.go", sources: ["session"], turns: [1], latestTime: Date.now() - 120_000 },
3766 ],
3767 };
3768 },
3769 async WorkspaceChangeDetail(_tabID: string, path: string) {
3770 return {
3771 source: "git" as const,
3772 added: 2,
3773 removed: 1,
3774 diff: `diff --git a/${path} b/${path}\n--- a/${path}\n+++ b/${path}\n@@ -1,2 +1,3 @@\n-old line\n+new line\n context\n+another line`,
3775 };
3776 },
3777 async GitBranches() {
3778 return ["main", "dev", "feature/branch-switcher"];
3779 },
3780 async GitCheckout(_branch: string) {
3781 console.info("mock GitCheckout", _branch);
3782 },
3783 async WorkspaceGitHistory(_tabID: string, path: string) {
3784 return [
3785 { hash: "abcdef123456", author: "Mock Author", date: new Date().toISOString(), message: "Mock commit message for " + path },
3786 ];
3787 },
3788 async WorkspaceGitCommitDetail(_tabID: string, _hash: string, path: string) {
3789 if (path) {
3790 return { diff: "--- a/mock\n+++ b/mock\n@@ -1,1 +1,1 @@\n-mock\n+mock diff" };
3791 }
3792 return { files: ["mock_file_1.ts", "mock_file_2.ts"] };
3793 },
3794 async OpenWorkspacePath(rel: string) {
3795 console.info("mock OpenWorkspacePath", rel);
3796 },
3797 async OpenLocalPath(path: string) {
3798 console.info("mock OpenLocalPath", path);
3799 },
3800 async OpenWorkspacePathForTab(_tabID: string, rel: string) {
3801 await this.OpenWorkspacePath(rel);
3802 },
3803 async ExternalOpeners() {
3804 return {
3805 openers: [
3806 { id: "vscode", name: "VS Code", kind: "editor", iconDataUrl: mockExternalOpenerIconDataURL("#1684d6", "V") },
3807 { id: "cursor", name: "Cursor", kind: "editor", iconDataUrl: mockExternalOpenerIconDataURL("#25262a", "C") },
3808 { id: "finder", name: "Finder", kind: "file-manager", iconDataUrl: mockExternalOpenerIconDataURL("#36aaf4", "F") },
3809 { id: "ghostty", name: "Ghostty", kind: "terminal", iconDataUrl: mockExternalOpenerIconDataURL("#264db6", ">") },
3810 ],
3811 preferred: "vscode",
3812 } as ExternalOpenersView;
3813 },
3814 async SetPreferredExternalOpener(_id: string) {},
3815 async OpenWorkspaceInExternalOpener(_id: string) {},
3816 async OpenWorkspaceInExternalOpenerForTab(_tabID: string, id: string) {
3817 await this.OpenWorkspaceInExternalOpener(id);
3818 },
3819 async RevealWorkspacePath(rel: string) {
3820 console.info("mock RevealWorkspacePath", rel);
3821 },
3822 async RevealWorkspacePathForTab(_tabID: string, rel: string) {
3823 await this.RevealWorkspacePath(rel);
3824 },
3825 async RevealPath(path: string) {
3826 console.info("mock RevealPath", path);
3827 },
3828 async SavePastedImage(dataUrl: string) {
3829 const path = `.reasonix/attachments/mock-${mockAttachmentDataURLs.size + 1}.png`;
3830 mockAttachmentDataURLs.set(path, dataUrl);
3831 return path;
3832 },
3833 async SaveClipboardImage() {
3834 const path = `.reasonix/attachments/mock-clipboard-${mockAttachmentDataURLs.size + 1}.png`;
3835 mockAttachmentDataURLs.set(path, mockPreviewImageDataURL);
3836 return path;
3837 },
3838 async SavePastedFile(name: string, dataUrl: string) {
3839 const path = `.reasonix/attachments/mock-${name}`;
3840 mockAttachmentDataURLs.set(path, dataUrl);
3841 return path;
3842 },
3843 async PickExportFile(defaultFilename: string, _mimeType: string) {
3844 return defaultFilename;
3845 },
3846 async SaveExportFile(path: string, payload: string, base64Encoded: boolean) {
3847 const a = document.createElement("a");
3848 let url = "";
3849 if (base64Encoded) {
3850 url = `data:application/octet-stream;base64,${payload}`;
3851 } else {
3852 url = URL.createObjectURL(new Blob([payload], { type: "text/plain;charset=utf-8" }));
3853 }
3854 a.href = url;
3855 a.download = path;
3856 document.body.appendChild(a);
3857 a.click();
3858 a.remove();
3859 if (!base64Encoded) URL.revokeObjectURL(url);
3860 },
3861 async SaveExportImageFiles(path: string, payloads: string[]) {
3862 if (payloads.length === 0) throw new Error("No image payloads to export");
3863 const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
3864 const dot = path.lastIndexOf(".");
3865 const extensionStart = dot > slash ? dot : path.length;
3866 const stem = path.slice(0, extensionStart);
3867 const extension = path.slice(extensionStart);
3868 for (let index = 0; index < payloads.length; index++) {
3869 const partPath = payloads.length > 1
3870 ? `${stem}-${index + 1}-of-${payloads.length}${extension}`
3871 : path;
3872 await this.SaveExportFile(partPath, payloads[index], true);
3873 }
3874 },
3875 async AttachDropped(path: string) {
3876 const name = path.split(/[/\\]/).filter(Boolean).pop() ?? path;
3877 const hasExt = /\.\w{1,6}$/i.test(name);
3878 if (!hasExt) {
3879 const tokenName = name.replace(/[^\w.-]+/g, "-") || "folder";
3880 return { kind: "workspace" as const, path: `__reasonix_external_folder/mock/${tokenName}`, isDir: true, displayPath: path };
3881 }
3882 const attachmentPath = `.reasonix/attachments/mock-${name}`;
3883 mockAttachmentDataURLs.set(attachmentPath, mockPreviewImageDataURL);
3884 return { kind: "attachment" as const, path: attachmentPath };
3885 },
3886 async AttachmentDataURL(path: string) {
3887 return mockAttachmentDataURLs.get(path) ?? mockPreviewImageDataURL;
3888 },
3889 async Models() {
3890 const active = mockTabs.find((tab) => tab.active) ?? mockTabs[0];
3891 const current = mockTabModelRef(active);
3892 return mockModelCatalog.map((model) => ({ ...model, current: model.ref === current }));
3893 },
3894 async ModelsForTab(tabID) {
3895 const tab = mockTabs.find((item) => item.id === tabID) ?? mockTabs.find((item) => item.active) ?? mockTabs[0];
3896 const current = mockTabModelRef(tab);
3897 return mockModelCatalog.map((model) => ({ ...model, current: model.ref === current }));
3898 },
3899 async SetModel(name) {
3900 setMockTabModel(undefined, name);
3901 },
3902 async SetModelForTab(tabID, name) {
3903 setMockTabModel(tabID, name);
3904 },
3905 async Effort() {
3906 return { supported: true, current: mockEffort, default: "high", levels: ["auto", "high", "max"] };
3907 },
3908 async EffortForTab() {
3909 return this.Effort();
3910 },
3911 async SetEffort(level: string) {
3912 mockEffort = level || "auto";
3913 },
3914 async SetEffortForTab(_tabID, level) {
3915 await this.SetEffort(level);
3916 },
3917 async SetTokenMode(mode: string) {
3918 const active = mockTabs.find((tab) => tab.active);
3919 if (active) await this.SetTokenModeForTab(active.id, mode);
3920 },
3921 async SetTokenModeForTab(tabID, mode) {
3922 const tokenMode = normalizeTokenMode(mode);
3923 mockTabs = mockTabs.map((tab) => (tab.id === tabID ? { ...tab, tokenMode } : tab));
3924 },
3925 async ReloadRuntime(_tabID) {},
3926 async Memory() {
3927 return {
3928 available: true,
3929 storeDir: "~/.reasonix/projects/-mock/memory",
3930 storeGlobalDir: "~/.reasonix/memory/global",
3931 docs: [
3932 {
3933 path: "REASONIX.md",
3934 scope: "project",
3935 directory: ".",
3936 body: "# Reasonix project memory\n\nMock doc shown in the browser dev seam.\n\n## Notes\n\n- prefers concise replies",
3937 imports: [],
3938 depth: 0,
3939 order: 0,
3940 precedence: 0,
3941 },
3942 {
3943 path: "~/.reasonix/REASONIX.md",
3944 scope: "user",
3945 body: t("mock.memoryBody"),
3946 imports: [],
3947 depth: -1,
3948 order: 1,
3949 precedence: 1,
3950 },
3951 ],
3952 instructionDiagnostics: [],
3953 facts: [
3954 {
3955 name: "prefers-tabs",
3956 description: "User prefers tabs",
3957 type: "user",
3958 scope: "project",
3959 body: "Indent with tabs.",
3960 freshness: "fresh",
3961 },
3962 ],
3963 archives: [
3964 {
3965 name: "old-plan",
3966 description: "Superseded planning note",
3967 type: "project",
3968 scope: "project",
3969 body: "This plan was archived after the implementation changed.",
3970 path: "~/.reasonix/projects/-mock/memory/.archive/20260612-021500.000-old-plan.md",
3971 archivedAt: "2026-06-12T02:15:00Z",
3972 freshness: "current",
3973 },
3974 ],
3975 scopes: [
3976 { scope: "user", path: "~/.reasonix/REASONIX.md" },
3977 { scope: "project", path: "REASONIX.md" },
3978 { scope: "local", path: "REASONIX.local.md" },
3979 ],
3980 conflicts: [],
3981 lastRecall: {
3982 query: "",
3983 hits: [],
3984 omitted: 0,
3985 charBudget: 2400,
3986 usedChars: 0,
3987 suppressed: "no user turn yet",
3988 },
3989 };
3990 },
3991 async MemorySuggestions() {
3992 return {
3993 memories: [
3994 {
3995 id: "memory-prefers-concise-replies",
3996 name: "prefers-concise-replies",
3997 title: "Prefers concise replies",
3998 description: "User prefers concise replies unless detail is requested.",
3999 type: "user",
4000 scope: "project",
4001 body: "User prefers concise replies unless detail is requested.\n\n**Why:** Suggested from recent local history.\n**How to apply:** Keep answers brief by default.",
4002 reason: "future-facing preference",
4003 evidence: ["mock-session: always keep replies concise"],
4004 },
4005 ],
4006 skills: [
4007 {
4008 id: "skill-reasonix-pr-followup",
4009 name: "reasonix-pr-followup",
4010 description: "Review or update a Reasonix GitHub PR, address feedback, verify, and publish safely.",
4011 scope: "project",
4012 body: "# Reasonix PR Followup\n\nUse this skill for repeated Reasonix PR work.\n\n## Workflow\n\n1. Confirm branch and PR state.\n2. Inspect the diff.\n3. Fix actionable feedback.\n4. Verify and update the PR.\n",
4013 reason: "recent history repeatedly touched PR workflows",
4014 evidence: ["mock-pr-session: 提交到pr,并更新内容", "mock-review-session: 解决该pr下机器人提出来的问题"],
4015 },
4016 ],
4017 generatedAt: new Date().toISOString(),
4018 available: true,
4019 source: "mock",
4020 };
4021 },
4022 async AcceptMemorySuggestion(suggestion: MemorySuggestion) {
4023 emit({ kind: "notice", level: "info", text: `saved suggested memory → ${suggestion.name}` });
4024 return `${suggestion.name}.md`;
4025 },
4026 async AcceptSkillSuggestion(suggestion: SkillSuggestion) {
4027 emit({ kind: "notice", level: "info", text: `created suggested skill → ${suggestion.name}` });
4028 return `.reasonix/skills/${suggestion.name}/SKILL.md`;
4029 },
4030 async MemorySuggestionsForTab(_tabID: string) {
4031 return this.MemorySuggestions();
4032 },
4033 async AcceptMemorySuggestionForTab(_tabID: string, suggestion: MemorySuggestion) {
4034 return this.AcceptMemorySuggestion(suggestion);
4035 },
4036 async AcceptSkillSuggestionForTab(_tabID: string, suggestion: SkillSuggestion) {
4037 return this.AcceptSkillSuggestion(suggestion);
4038 },
4039 async MemoryForTab(_tabID: string) {
4040 return this.Memory();
4041 },
4042 async MemoryRevisions(_ref: string) {
4043 return [];
4044 },
4045 async MemoryRevisionsForTab(_tabID: string, ref: string) {
4046 return this.MemoryRevisions(ref);
4047 },
4048 async RestoreMemoryRevision(ref: string, revision: number) {
4049 emit({ kind: "notice", level: "info", text: `restored revision → ${ref}@${revision}` });
4050 return {
4051 id: ref,
4052 revision: revision + 1,
4053 name: ref,
4054 description: "Restored memory revision",
4055 type: "project",
4056 scope: "project",
4057 body: "Restored guidance.",
4058 freshness: "fresh",
4059 };
4060 },
4061 async RestoreMemoryRevisionForTab(_tabID: string, ref: string, revision: number) {
4062 return this.RestoreMemoryRevision(ref, revision);
4063 },
4064 async Remember(_scope: string, _note: string) {
4065 emit({ kind: "notice", level: "info", text: `remembered → ${_scope}` });
4066 return `${_scope} REASONIX.md (mock): ${_note}`;
4067 },
4068 async RememberForTab(_tabID: string, scope: string, note: string) {
4069 return this.Remember(scope, note);
4070 },
4071 async Forget(_name: string) {
4072 emit({ kind: "notice", level: "info", text: `forgot → ${_name}` });
4073 },
4074 async ForgetForTab(_tabID: string, name: string) {
4075 return this.Forget(name);
4076 },
4077 async RestoreArchivedMemory(archivePath: string) {
4078 emit({ kind: "notice", level: "info", text: `restored → ${archivePath}` });
4079 return {
4080 id: "mock-restored-memory",
4081 revision: 2,
4082 name: "restored-memory",
4083 description: "Recovered archived memory",
4084 type: "project",
4085 scope: "project",
4086 body: "Recovered guidance.",
4087 freshness: "fresh",
4088 };
4089 },
4090 async RestoreArchivedMemoryForTab(_tabID: string, archivePath: string) {
4091 return this.RestoreArchivedMemory(archivePath);
4092 },
4093 async SaveDoc(_path: string, _body: string) {
4094 emit({ kind: "notice", level: "info", text: `saved → ${_path}` });
4095 return _path;
4096 },
4097 async SaveDocForTab(_tabID: string, path: string, body: string) {
4098 return this.SaveDoc(path, body);
4099 },
4100 async DesktopStartupSettings() {
4101 const { bot, desktopLanguage, desktopLayoutStyle, desktopTheme, desktopThemeStyle, desktopTerminalTheme, displayMode, statusBarStyle, statusBarItems, checkUpdates, conversationWidth } = settings;
4102 return JSON.parse(JSON.stringify({
4103 bot,
4104 desktopLanguage,
4105 desktopLayoutStyle,
4106 desktopTheme,
4107 desktopThemeStyle,
4108 desktopTerminalTheme,
4109 displayMode,
4110 statusBarStyle,
4111 statusBarItems,
4112 checkUpdates,
4113 conversationWidth,
4114 })) as DesktopStartupSettingsView;
4115 },
4116 async Settings() {
4117 return JSON.parse(JSON.stringify(settings)) as SettingsView;
4118 },
4119 async HooksSettings(scope: string) {
4120 const key = scope === "project" ? "project" : "global";
4121 return JSON.parse(JSON.stringify(hookSettings[key])) as HooksSettingsView;
4122 },
4123 async SaveHooksSettings(scope: string, hooks: HookConfigView[]) {
4124 const key = scope === "project" ? "project" : "global";
4125 hookSettings[key].hooks = JSON.parse(JSON.stringify(hooks)) as HookConfigView[];
4126 },
4127 async SaveHooksSettingsForRoot(scope: string, _projectRoot: string, hooks: HookConfigView[]) {
4128 const key = scope === "project" ? "project" : "global";
4129 hookSettings[key].hooks = JSON.parse(JSON.stringify(hooks)) as HookConfigView[];
4130 },
4131 async TrustProjectHooks() {
4132 // Compatibility no-op: project hooks are enabled automatically.
4133 },
4134 async TrustProjectHooksForRoot(_projectRoot: string) {
4135 // Compatibility no-op: project hooks are enabled automatically.
4136 },
4137 async SetDefaultModel(ref: string) {
4138 settings.defaultModel = ref;
4139 },
4140 async SetPlannerModel(ref: string) {
4141 settings.plannerModel = ref;
4142 },
4143 async SetSubagentModel(ref: string) {
4144 settings.subagentModel = ref;
4145 },
4146 async SetSubagentEffort(level: string) {
4147 settings.subagentEffort = level;
4148 },
4149 async SetMaxSubagentDepth(depth: number) {
4150 settings.agent = { ...settings.agent, maxSubagentDepth: depth <= 1 ? 1 : 2 };
4151 },
4152 async SetMaxSubagentConcurrency(n: number) {
4153 const total = Math.max(1, Math.min(32, Math.floor(n) || 6));
4154 const writers = Math.min(total, Math.max(1, settings.agent.maxParallelWriters || 3));
4155 settings.agent = { ...settings.agent, maxSubagentConcurrency: total, maxParallelWriters: writers };
4156 },
4157 async SetMaxParallelWriters(n: number) {
4158 const total = Math.max(1, Math.min(32, settings.agent.maxSubagentConcurrency || 6));
4159 const writers = Math.max(1, Math.min(total, Math.floor(n) || 3));
4160 settings.agent = { ...settings.agent, maxParallelWriters: writers };
4161 },
4162 async SetAutoPlan(mode: string) {
4163 if (mode !== "off") throw new Error("Automatic plan mode has been retired; use Plan Mode explicitly.");
4164 settings.autoPlan = "off";
4165 },
4166 async SetDefaultToolApprovalMode(mode: string) {
4167 settings.defaultToolApprovalMode = normalizeToolApprovalMode(mode);
4168 },
4169 async SetDefaultAutoRecoveryCheckpoint(_enabled: boolean) {
4170 // Legacy no-op; Auto Guard is always built into Auto.
4171 },
4172 async SaveProvider(p: ProviderView) {
4173 p.added = true;
4174 const i = settings.providers.findIndex((x) => x.name === p.name);
4175 if (i >= 0) settings.providers[i] = p;
4176 else settings.providers.push(p);
4177 },
4178 async SaveProviderModelCatalogs(updates: ProviderModelCatalogUpdate[]) {
4179 const applied: string[] = [];
4180 for (const update of updates) {
4181 const i = settings.providers.findIndex((provider) => provider.name === update.name);
4182 if (i < 0) continue;
4183 const current = settings.providers[i];
4184 if (!update.expectedFingerprint || current.modelCatalogFingerprint !== update.expectedFingerprint) continue;
4185 settings.providers[i] = {
4186 ...current,
4187 models: [...update.models],
4188 default: update.default,
4189 visionModels: [...update.visionModels],
4190 modelCatalogFingerprint: `${update.expectedFingerprint}:updated`,
4191 };
4192 applied.push(update.name);
4193 }
4194 return applied;
4195 },
4196 async SaveProviderWithKey(p: ProviderView, key: string) {
4197 p.added = true;
4198 p.keySet = Boolean(key.trim()) || p.keySet;
4199 const i = settings.providers.findIndex((x) => x.name === p.name);
4200 if (i >= 0) settings.providers[i] = p;
4201 else settings.providers.push(p);
4202 return "";
4203 },
4204 async AddOfficialProviderAccess(kind: string, key: string) {
4205 const templates: Record<string, ProviderView> = {
4206 deepseek: { name: "deepseek", builtIn: true, added: true, kind: "openai", baseUrl: "https://api.deepseek.com", modelsUrl: "", models: ["deepseek-v4-flash", "deepseek-v4-pro"], visionModels: [], visionModelsConfigured: false, default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", keySet: !!key.trim(), balanceUrl: "https://api.deepseek.com/user/balance", contextWindow: 1_000_000, reasoningProtocol: "", thinking: "", supportedEfforts: [], defaultEffort: "" },
4207 };
4208 const next = templates[kind];
4209 if (!next) throw new Error(`unknown official provider template ${kind}`);
4210 const i = settings.providers.findIndex((x) => x.name === next.name);
4211 if (i >= 0) settings.providers[i] = { ...settings.providers[i], ...next, keySet: next.keySet || settings.providers[i].keySet };
4212 else settings.providers.push(next);
4213 return "";
4214 },
4215 async AddProviderPresetAccess(id: string, key: string) {
4216 const preset = settings.providerPresets.find((p) => p.id === id);
4217 if (!preset) throw new Error(`unknown provider preset ${id}`);
4218 const next = cloneMockProviderTemplate(id, key);
4219 if (!next) throw new Error(`unknown provider preset ${id}`);
4220 const i = settings.providers.findIndex((x) => x.name === next.name);
4221 if (i >= 0) settings.providers[i] = { ...settings.providers[i], ...next, keySet: next.keySet || settings.providers[i].keySet };
4222 else settings.providers.push(next);
4223 preset.added = true;
4224 preset.status = "installed";
4225 preset.statusProviderNames = [...preset.providerNames];
4226 preset.keySet = preset.keySet || !!key.trim();
4227 preset.configured = !preset.requiresKey || preset.keySet;
4228 return "";
4229 },
4230 async ResetProviderPresetAccess(id: string) {
4231 const preset = settings.providerPresets.find((p) => p.id === id);
4232 if (!preset) throw new Error(`unknown provider preset ${id}`);
4233 const next = cloneMockProviderTemplate(id, "");
4234 if (!next) throw new Error(`unknown provider preset ${id}`);
4235 const i = settings.providers.findIndex((x) => x.name === next.name);
4236 if (i < 0) throw new Error(`provider preset ${id} cannot be reset because no same-name provider exists`);
4237 const existing = settings.providers[i];
4238 settings.providers[i] = {
4239 ...next,
4240 added: true,
4241 keySet: existing.apiKeyEnv === next.apiKeyEnv ? existing.keySet : next.keySet,
4242 };
4243 preset.added = true;
4244 preset.status = "installed";
4245 preset.statusProviderNames = [...preset.providerNames];
4246 preset.keySet = preset.keySet || settings.providers[i].keySet;
4247 preset.configured = !preset.requiresKey || preset.keySet;
4248 },
4249 async FetchProviderModels(p: ProviderView) {
4250 if (!p.baseUrl.trim()) throw new Error(t("settings.fetchModelsMissingBaseUrl"));
4251 if (providerRequiresKey(p) && !p.apiKeyEnv.trim()) throw new Error(t("settings.fetchModelsMissingKeyEnv"));
4252 await delay(350);
4253 if (p.baseUrl.includes("deepseek")) return ["deepseek-v4-flash", "deepseek-v4-pro"];
4254 if (p.baseUrl.includes("token-plan")) return ["mimo-v2.5", "mimo-v2.5-pro"];
4255 if (p.baseUrl.includes("xiaomimimo")) return ["mimo-v2.5-pro", "mimo-v2.5"];
4256 return ["gpt-5", "gpt-5-mini", "qwen3-coder"];
4257 },
4258 async FetchAllProviderModels(providers: ProviderView[]) {
4259 const out: Record<string, string[]> = {};
4260 for (const p of providers) {
4261 try {
4262 out[p.name] = await this.FetchProviderModels(p);
4263 } catch {
4264 out[p.name] = [];
4265 }
4266 }
4267 return out;
4268 },
4269 async DeleteProvider(name: string) {
4270 settings.providers = settings.providers.filter((p) => p.name !== name);
4271 },
4272 async RemoveProviderAccess(name: string) {
4273 const p = settings.providers.find((x) => x.name === name);
4274 if (p?.builtIn) p.added = false;
4275 else settings.providers = settings.providers.filter((x) => x.name !== name);
4276 },
4277 async SaveProviderKey(apiKeyEnv: string, _value: string) {
4278 settings.providers.forEach((p) => {
4279 if (p.apiKeyEnv === apiKeyEnv) p.keySet = true;
4280 });
4281 return "";
4282 },
4283 async SetProviderKey(apiKeyEnv: string, _value: string) {
4284 settings.providers.forEach((p) => {
4285 if (p.apiKeyEnv === apiKeyEnv) p.keySet = true;
4286 });
4287 return "";
4288 },
4289 async ClearProviderKey(apiKeyEnv: string) {
4290 settings.providers.forEach((p) => {
4291 if (p.apiKeyEnv === apiKeyEnv) p.keySet = false;
4292 });
4293 },
4294 async SetPermissionMode(mode: string) {
4295 settings.permissions.mode = mode;
4296 },
4297 async AddPermissionRule(list: string, rule: string) {
4298 const k = list as "allow" | "ask" | "deny";
4299 if (settings.permissions[k] && !settings.permissions[k].includes(rule)) settings.permissions[k].push(rule);
4300 },
4301 async RemovePermissionRule(list: string, rule: string) {
4302 const k = list as "allow" | "ask" | "deny";
4303 settings.permissions[k] = settings.permissions[k].filter((r) => r !== rule);
4304 },
4305 async ReloadSettings() {},
4306 async SetSandbox(bash: string, network: boolean, workspaceRoot: string, allowWrite: string[], shell: string) {
4307 const effectiveWorkspaceRoot = workspaceRoot.trim() || cwd;
4308 settings.sandbox = { bash, network, workspaceRoot, allowWrite, effectiveWorkspaceRoot, effectiveWriteRoots: [effectiveWorkspaceRoot, ...allowWrite], shell, effectiveShell: browserPreviewEffectiveShell(shell) };
4309 },
4310 async SetNetwork(n: NetworkView) {
4311 settings.network = n;
4312 },
4313 async SetBotSettings(b: BotSettingsView) {
4314 settings.bot = JSON.parse(JSON.stringify(b)) as BotSettingsView;
4315 },
4316 async SetBotConnectionToolApprovalMode(connID, mode) {
4317 const conn = settings.bot.connections.find((c) => c.id === connID);
4318 if (conn) conn.toolApprovalMode = mode as any;
4319 },
4320 async SetBotSecret(envName: string, _value: string) {
4321 const name = envName.trim();
4322 if (settings.bot.qq.appSecretEnv === name) settings.bot.qq.secretSet = true;
4323 if (settings.bot.feishu.appSecretEnv === name) settings.bot.feishu.secretSet = true;
4324 if (settings.bot.weixin.tokenEnv === name) settings.bot.weixin.tokenSet = true;
4325 settings.bot.connections = settings.bot.connections.map((connection) => ({
4326 ...connection,
4327 credential: connection.credential.appSecretEnv === name || connection.credential.tokenEnv === name
4328 ? { ...connection.credential, secretSet: true }
4329 : connection.credential,
4330 }));
4331 },
4332 async ClearBotSecret(envName: string) {
4333 const name = envName.trim();
4334 if (settings.bot.qq.appSecretEnv === name) settings.bot.qq.secretSet = false;
4335 if (settings.bot.feishu.appSecretEnv === name) settings.bot.feishu.secretSet = false;
4336 if (settings.bot.weixin.tokenEnv === name) settings.bot.weixin.tokenSet = false;
4337 settings.bot.connections = settings.bot.connections.map((connection) => ({
4338 ...connection,
4339 credential: connection.credential.appSecretEnv === name || connection.credential.tokenEnv === name
4340 ? { ...connection.credential, secretSet: false }
4341 : connection.credential,
4342 }));
4343 },
4344 async BotRuntimeStatus() {
4345 const qqRunning = settings.bot.qq.enabled && settings.bot.qq.appId.trim() && settings.bot.qq.secretSet;
4346 const runningConnections = (qqRunning ? 1 : 0) + settings.bot.connections.filter((connection) => connection.enabled && connection.status === "connected").length;
4347 return {
4348 running: settings.bot.enabled && runningConnections > 0,
4349 status: settings.bot.enabled && runningConnections > 0 ? "running" : "stopped",
4350 message: settings.bot.enabled && runningConnections > 0 ? `${runningConnections} bot connection(s) running` : "bot runtime is not started",
4351 connections: runningConnections,
4352 startedAt: settings.bot.enabled && runningConnections > 0 ? new Date(t0).toISOString() : "",
4353 };
4354 },
4355 async StartBotConnectionInstall(provider: string, domain: string) {
4356 const normalizedProvider = provider === "weixin" ? "weixin" : "feishu";
4357 const normalizedDomain = normalizedProvider === "weixin" ? "weixin" : domain === "lark" ? "lark" : "feishu";
4358 return {
4359 ok: true,
4360 provider: normalizedProvider,
4361 domain: normalizedDomain,
4362 installId: `mock-${normalizedProvider}-${normalizedDomain}`,
4363 url: "https://example.com/reasonix-bot-qr",
4364 deviceCode: "MOCKDEVICE",
4365 userCode: normalizedProvider === "weixin" ? "" : "MOCK-CODE",
4366 interval: 3,
4367 expireIn: 300,
4368 message: "",
4369 };
4370 },
4371 async PollBotConnectionInstall(installID: string) {
4372 const isWeixin = installID.includes("weixin");
4373 const domain = installID.includes("lark") ? "lark" : isWeixin ? "weixin" : "feishu";
4374 const provider = isWeixin ? "weixin" : "feishu";
4375 const connection = {
4376 id: `${provider}-${domain}`,
4377 provider,
4378 domain,
4379 label: domain === "lark" ? "Lark" : domain === "weixin" ? "微信" : "飞书",
4380 enabled: true,
4381 status: "connected",
4382 model: "",
4383 toolApprovalMode: "",
4384 workspaceRoot: "",
4385 access: { enabled: true, allowAll: false, pairingEnabled: true, users: [provider === "weixin" ? "wxid_mock_user_001" : "ou_mock_user_001"], groups: [], approvers: [], admins: [] },
4386 credential: {
4387 appId: provider === "feishu" ? "cli_mock" : "",
4388 appSecretEnv: provider === "feishu" ? (domain === "lark" ? "LARK_BOT_APP_SECRET" : "FEISHU_BOT_APP_SECRET") : "",
4389 accountId: provider === "weixin" ? "mock-account" : "",
4390 tokenEnv: provider === "weixin" ? "WEIXIN_BOT_TOKEN" : "",
4391 secretSet: true,
4392 },
4393 sessionMappings: [],
4394 lastError: "",
4395 createdAt: new Date().toISOString(),
4396 updatedAt: new Date().toISOString(),
4397 };
4398 settings.bot.connections = [...settings.bot.connections.filter((c) => c.id !== connection.id), connection];
4399 return { done: true, connection, status: "connected", message: "connected", error: "" };
4400 },
4401 async DiagnoseBotConnection(id: string) {
4402 const connection = settings.bot.connections.find((c) => c.id === id);
4403 const occurredAt = new Date().toISOString();
4404 return connection
4405 ? { id, label: connection.label, status: connection.enabled ? "ok" : "disabled", message: connection.enabled ? "连接配置已保存。" : "连接已保存但未启用。", messageId: "", phase: "config", code: connection.enabled ? "config_ok" : "connection_disabled", reportKind: "", reportDetail: "", occurredAt }
4406 : { id, label: "", status: "missing", message: "未找到连接。", messageId: "", phase: "config", code: "connection_missing", reportKind: "bot", reportDetail: JSON.stringify({ schemaVersion: 2, kind: "bot", source: "bot.runtime", label: "bot.mock.config", message: "mock missing bot connection", errorType: "BotConnectionDiagnostic", errorMessage: "bot connection record was not found", topFrame: "bot.config", occurredAt }), occurredAt };
4407 },
4408 async TestBotConnection(id: string, target?: string) {
4409 const diag = await this.DiagnoseBotConnection(id);
4410 if (target?.trim()) return { ...diag, message: `Mock test sent to ${target.trim()}`, messageId: "mock-message-id" };
4411 return diag;
4412 },
4413 async SetCloseBehavior(mode: string) {
4414 settings.closeBehavior = mode === "quit" ? "quit" : "background";
4415 },
4416 async SetDisplayMode(mode: string) {
4417 settings.displayMode = mode;
4418 },
4419 async SetStatusBarStyle(style: string) {
4420 settings.statusBarStyle = style === "text" ? "text" : "icon";
4421 },
4422 async SetStatusBarItems(items: string[]) {
4423 settings.statusBarItems = normalizeStatusBarItems(items);
4424 },
4425 async SetDesktopLanguage(lang: string) {
4426 settings.desktopLanguage = lang === "en" || lang === "zh" ? lang : "";
4427 },
4428 async SetDesktopCurrency(currency: string) {
4429 settings.desktopCurrency = currency === "CNY" || currency === "USD" ? currency : "";
4430 },
4431 async SetDesktopAppearance(theme: string, style: string) {
4432 settings.desktopTheme = theme === "auto" || theme === "light" ? theme : "dark";
4433 settings.desktopThemeStyle = style;
4434 mockThemeMode = settings.desktopTheme as "auto" | "light" | "dark";
4435 if (["graphite","aurora","slate","carbon","nocturne","amber"].includes(style)) {
4436 mockBaseStyle = style;
4437 }
4438 },
4439 async SetDesktopTerminalTheme(theme: string) {
4440 settings.desktopTerminalTheme = theme === "dark" || theme === "light" ? theme : "auto";
4441 },
4442 async ListThemePacks() {
4443 const baseActive = !mockActiveThemeId;
4444 return mockThemePacks.map((p) => {
4445 const kind = p.kind || (p.builtin ? "base" : "user");
4446 let active = false;
4447 if (kind === "base") active = baseActive && p.id === mockBaseStyle;
4448 else active = p.id === mockActiveThemeId;
4449 return { ...p, active, tokens: { light: { ...(p.tokens.light || {}) }, dark: { ...(p.tokens.dark || {}) } }, recipes: { ...p.recipes } };
4450 });
4451 },
4452 async GetActiveThemePack() {
4453 // Base style ids are never active packs in the redesigned model.
4454 const pack = mockActiveThemeId && !["graphite","aurora","slate","carbon","nocturne","amber"].includes(mockActiveThemeId)
4455 ? mockThemePacks.find((p) => p.id === mockActiveThemeId)
4456 : null;
4457 return { activeThemeId: pack ? mockActiveThemeId : "", pack: pack ? { ...pack, active: true } : null };
4458 },
4459 async GetThemeExperience() {
4460 const pack = mockActiveThemeId && !["graphite","aurora","slate","carbon","nocturne","amber"].includes(mockActiveThemeId)
4461 ? mockThemePacks.find((p) => p.id === mockActiveThemeId)
4462 : null;
4463 return {
4464 themeMode: mockThemeMode,
4465 baseStyle: mockBaseStyle,
4466 effectiveStyle: pack?.baseStyle || mockBaseStyle,
4467 activeThemeId: pack ? mockActiveThemeId : "",
4468 activePack: pack ? { ...pack, active: true } : null,
4469 };
4470 },
4471 async ActivateThemePack(id: string) {
4472 const next = String(id || "").trim();
4473 if (["graphite","aurora","slate","carbon","nocturne","amber"].includes(next)) {
4474 throw new Error(`base style ${next} is not a theme pack; use ActivateBaseStyle`);
4475 }
4476 mockActiveThemeId = next;
4477 },
4478 async ActivateBaseStyle(style: string) {
4479 const s = String(style || "").trim().toLowerCase();
4480 if (!["graphite","aurora","slate","carbon","nocturne","amber"].includes(s)) {
4481 throw new Error(`unknown base style ${s}`);
4482 }
4483 mockBaseStyle = s;
4484 mockActiveThemeId = "";
4485 },
4486 async DisableThemePack() {
4487 mockActiveThemeId = "";
4488 },
4489 async RestoreGraphiteAppearance() {
4490 mockBaseStyle = "graphite";
4491 mockActiveThemeId = "";
4492 },
4493 async ResetThemePack() {
4494 mockActiveThemeId = "";
4495 },
4496 async SaveThemePack(input: import("./themePack").ThemeSaveInput) {
4497 const pack: import("./themePack").ThemePackView = {
4498 id: input.id,
4499 name: input.name,
4500 author: input.author,
4501 description: input.description,
4502 license: input.license,
4503 baseStyle: input.baseStyle,
4504 builtin: false,
4505 kind: "user",
4506 active: Boolean(input.activate),
4507 hasBackground: Boolean(
4508 (input.background && (input.backgroundDataUrl || input.background.image)) ||
4509 (input.taskBackground && (input.taskBackgroundDataUrl || input.taskBackground.image)),
4510 ),
4511 backgroundUrl: input.backgroundDataUrl || "",
4512 taskBackgroundUrl: input.taskBackgroundDataUrl || "",
4513 tokens: input.tokens || {},
4514 recipes: input.recipes || { density: "comfortable", corners: "soft" },
4515 background: input.background ?? undefined,
4516 taskBackground: input.taskBackground ?? undefined,
4517 };
4518 const idx = mockThemePacks.findIndex((p) => p.id === pack.id);
4519 if (idx >= 0) mockThemePacks[idx] = pack;
4520 else mockThemePacks.push(pack);
4521 if (input.activate) mockActiveThemeId = pack.id;
4522 return pack;
4523 },
4524 async DeleteThemePack(id: string) {
4525 mockThemePacks = mockThemePacks.filter((p) => p.id !== id || p.builtin);
4526 if (mockActiveThemeId === id) mockActiveThemeId = "";
4527 },
4528 async CopyThemePack(sourceID: string, newID: string, newName: string) {
4529 const src = mockThemePacks.find((p) => p.id === sourceID);
4530 if (!src) throw new Error("source theme not found");
4531 const pack: import("./themePack").ThemePackView = {
4532 ...src,
4533 id: newID,
4534 name: newName || `${src.name} Copy`,
4535 builtin: false,
4536 kind: "user",
4537 nameKey: undefined,
4538 descriptionKey: undefined,
4539 active: false,
4540 };
4541 mockThemePacks.push(pack);
4542 return pack;
4543 },
4544 async ImportThemePack(_sourcePath: string, replace: boolean) {
4545 if (replace) {
4546 return { pack: mockThemePacks[0], replaced: true };
4547 }
4548 // Simulate conflict path without re-prompting for a file on confirm.
4549 return { pack: mockThemePacks[0], replaced: false, needsReplace: true, pendingId: "pending-mock" };
4550 },
4551 async ExportThemePack(_id: string, _destPath: string) {
4552 return "";
4553 },
4554 async PickThemeBackground() {
4555 return "";
4556 },
4557 async SetDesktopLayoutStyle(style: string) {
4558 settings.desktopLayoutStyle = style === "workbench" || style === "creation" ? style : "classic";
4559 },
4560 async SetDesktopZoomFactor(factor: number) {
4561 mockDesktopZoomFactor = Math.min(2.0, Math.max(0.5, Number.isFinite(factor) ? factor : 1.0));
4562 },
4563 async GetDesktopZoomFactor() {
4564 return mockDesktopZoomFactor;
4565 },
4566 async RestartApplication() {
4567 // no-op in mock
4568 },
4569 async SetDesktopCheckUpdates(enabled: boolean) {
4570 settings.checkUpdates = enabled;
4571 },
4572 async SetDesktopUpdateChannel(channel: string) {
4573 void channel;
4574 settings.updateChannel = "stable";
4575 },
4576 async SetDesktopTelemetry(enabled: boolean) {
4577 settings.telemetry = enabled;
4578 },
4579 async SetDesktopMetrics(enabled: boolean) {
4580 settings.metrics = enabled;
4581 },
4582 async SetDesktopConversationWidth(width: string) {
4583 settings.conversationWidth = width;
4584 },
4585 async SetExpandThinking(_on: boolean) {},
4586 async MigrateDesktopPreferences(language: string, theme: string, style: string) {
4587 if (!settings.desktopLanguage) settings.desktopLanguage = language === "en" || language === "zh" || language === "zh-TW" ? language : "";
4588 if (!settings.desktopTheme && !settings.desktopThemeStyle) {
4589 settings.desktopTheme = theme === "auto" || theme === "light" ? theme : "dark";
4590 settings.desktopThemeStyle = style;
4591 }
4592 },
4593 async SetAgentParams(temperature: number, maxSteps: number, plannerMaxSteps: number, systemPrompt: string) {
4594 settings.agent = { ...settings.agent, temperature, maxSteps, plannerMaxSteps, systemPrompt };
4595 },
4596 async SetColdResumePrune(enabled: boolean) {
4597 settings.agent = { ...settings.agent, coldResumePrune: enabled };
4598 },
4599 async SetCompactRatio(ratio: number) {
4600 if (!Number.isFinite(ratio) || ratio < 0.65 || ratio > 0.85) throw new Error("compact ratio must be between 0.65 and 0.85");
4601 settings.agent = { ...settings.agent, compactRatio: ratio };
4602 },
4603 async SetReasoningLanguage(lang: string) {
4604 const normalized = lang === "zh" || lang === "en" ? lang : "auto";
4605 settings.agent = { ...settings.agent, reasoningLanguage: normalized };
4606 },
4607 // ── Heartbeat mock ──
4608 async HeartbeatListTasks() { return []; },
4609 async HeartbeatReloadTasks() { return []; },
4610 async HeartbeatSaveTasks(_tasks: unknown) {},
4611 async HeartbeatTriggerNow(_id: string) {},
4612 async HeartbeatGenerateID() { return "mock-" + Date.now().toString(36); },
4613 async ListTasks() { return []; },
4614 async CurrentTaskSessionID() { return ""; },
4615 async ListTasksForSession() { return []; },
4616 async GetTask() { return null; },
4617 async ListTaskEvents() { return []; },
4618 async StopTask() { return { schema_version: 1, command: "stop", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
4619 async CancelTask() { return { schema_version: 1, command: "cancel", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
4620 async RequeueTask() { return { schema_version: 1, command: "requeue", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
4621 async OpenTaskSession() { return { schema_version: 1, command: "open_session", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
4622 async ListTasksForTab() { return []; },
4623 async ListTaskEventsForTab() { return []; },
4624 async StopTaskForTab() { return { schema_version: 1, command: "stop", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
4625 async CancelTaskForTab() { return { schema_version: 1, command: "cancel", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
4626 async RequeueTaskForTab() { return { schema_version: 1, command: "requeue", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
4627 async OpenTaskSessionForTab() { return { schema_version: 1, command: "open_session", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
4628 async SetTrayLocale(_locale: "en" | "zh" | "zh-TW") {},
4629 async SetAutoApproveTools(on: boolean) {
4630 await this.SetToolApprovalMode(on ? "yolo" : "ask");
4631 },
4632 async SetBypass(on: boolean) {
4633 await this.SetAutoApproveTools(on);
4634 },
4635 async Version() {
4636 return "v1.0.0 (browser dev)";
4637 },
4638 async CheckUpdate(channel: string) {
4639 void channel;
4640 // Keep the default browser preview focused on the primary product surface.
4641 // Updater methods remain mocked for explicit updater-flow tests.
4642 return {
4643 available: false,
4644 current: "v1.0.0",
4645 latest: "v1.0.0",
4646 notes: "",
4647 channel: "stable",
4648 canSelfUpdate: false,
4649 manualOnly: true,
4650 installMode: "manual",
4651 manualReason: "browser preview",
4652 downloaded: false,
4653 downloadUrl: "",
4654 assetSize: 0,
4655 };
4656 },
4657 async ApplyUpdateRequest(channel: string, expectedVersion: string, requestId: string) {
4658 void channel;
4659 const selectedChannel = "stable";
4660 const total = 12_345_678;
4661 for (let r = 0; r <= total; r += 1_800_000) {
4662 emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "downloading", received: Math.min(r, total), total });
4663 await delay(120);
4664 }
4665 emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "verifying", received: total, total });
4666 await delay(300);
4667 emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "installing", received: total, total });
4668 await delay(300);
4669 emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "relaunching", received: 0, total: 0 });
4670 },
4671 async OpenDownloadPage() {
4672 if (typeof window !== "undefined") {
4673 window.open("https://reasonix.io/?download=desktop#start", "_blank", "noopener");
4674 }
4675 },
4676 async OpenUserConfigPath() {},
4677 async ReloadUserConfig() {
4678 return { configWarnings: [], configPath: "" };
4679 },
4680 // Dev seam: match the backend's provider-agnostic onboarding predicate.
4681 async NeedsOnboarding() {
4682 return !settings.providers.some((p) => p.models.length > 0 && providerIsConfigured(p));
4683 },
4684 async ConnectKey(apiKey: string) {
4685 if (!apiKey.trim()) throw new Error("key is required");
4686 settings.providers.forEach((p) => {
4687 if (p.apiKeyEnv === "DEEPSEEK_API_KEY") {
4688 p.added = true;
4689 p.keySet = true;
4690 p.configured = true;
4691 }
4692 });
4693 await delay(300);
4694 return "";
4695 },
4696 async ReportCrash() {
4697 await delay(300);
4698 },
4699 // Tab management mocks.
4700 async ListTabs() {
4701 return mockTabs.map((tab) => ({ ...tab }));
4702 },
4703 async OpenProjectTab(workspaceRoot: string, _topicID: string) {
4704 const existing = mockTabs.find((tab) => tab.scope === "project" && tab.workspaceRoot === workspaceRoot && tab.topicId === _topicID);
4705 if (existing) {
4706 const active = { ...existing, active: true, running: mockTopicRunsInScenario(_topicID) };
4707 mockTabs = mockTabs.map((tab) => (tab.id === existing.id ? active : { ...tab, active: false }));
4708 return { ...active };
4709 }
4710 const defaultToolApprovalMode = normalizeToolApprovalMode(settings.defaultToolApprovalMode);
4711 const tab: TabMeta = {
4712 id: "tab_" + Date.now(),
4713 scope: "project",
4714 workspaceRoot,
4715 workspaceName: workspaceRoot.split("/").filter(Boolean).pop() ?? workspaceRoot,
4716 workspacePath: workspaceRoot,
4717 gitBranch: "main",
4718 topicId: _topicID,
4719 topicTitle: topicLabel(_topicID, t("mock.newSession")),
4720 sessionPath: `/mock/sessions/${_topicID}.jsonl`,
4721 projectColor: mockProjectTree.find((node) => node.root === workspaceRoot)?.projectColor,
4722 label: mockModelLabel(settings.defaultModel),
4723 ready: true,
4724 running: mockTopicRunsInScenario(_topicID),
4725 mode: modeWithAutoApproveTools("normal", defaultToolApprovalMode === "yolo"),
4726 collaborationMode: "normal",
4727 toolApprovalMode: defaultToolApprovalMode,
4728 tokenMode: "full",
4729 active: true,
4730 cwd: workspaceRoot,
4731 };
4732 mockTabs = [...mockTabs.map((item) => ({ ...item, active: false })), tab];
4733 return { ...tab };
4734 },
4735 async DeliveryWorktreeAvailability(workspaceRoot: string) {
4736 return workspaceRoot
4737 ? { available: true, repoRoot: workspaceRoot, branch: "main", sourceDirty: false }
4738 : { available: false, reason: "project folder is required" };
4739 },
4740 async CreateDeliveryWorktree(workspaceRoot: string) {
4741 if (!workspaceRoot) throw new Error("project folder is required");
4742 const suffix = Date.now().toString(36);
4743 const isolatedRoot = `/mock/reasonix-worktrees/${suffix}/${workspaceRoot.split("/").filter(Boolean).pop() ?? "project"}`;
4744 const topicID = `topic_worktree_${suffix}`;
4745 const tab = await this.OpenProjectTab(isolatedRoot, topicID);
4746 tab.isolatedWorktree = true;
4747 tab.gitBranch = `reasonix/delivery-${suffix}`;
4748 mockTabs = mockTabs.map((candidate) => candidate.id === tab.id ? { ...tab } : candidate);
4749 return {
4750 workspaceRoot: isolatedRoot,
4751 worktreeRoot: isolatedRoot,
4752 sourceRoot: workspaceRoot,
4753 branch: tab.gitBranch,
4754 sourceDirty: false,
4755 tab,
4756 };
4757 },
4758 async OpenGlobalTab(_topicID: string) {
4759 const existing = mockTabs.find((tab) => tab.scope === "global" && tab.topicId === _topicID);
4760 if (existing) {
4761 setMockActiveTab(existing.id);
4762 return { ...existing, active: true };
4763 }
4764 const defaultToolApprovalMode = normalizeToolApprovalMode(settings.defaultToolApprovalMode);
4765 const tab: TabMeta = {
4766 id: "tab_" + Date.now(),
4767 scope: "global",
4768 workspaceRoot: "",
4769 workspaceName: "Global",
4770 workspacePath: cwd,
4771 topicId: _topicID,
4772 topicTitle: topicLabel(_topicID, "Global"),
4773 sessionPath: `/mock/sessions/${_topicID}.jsonl`,
4774 label: mockModelLabel(settings.defaultModel),
4775 ready: true,
4776 running: false,
4777 mode: modeWithAutoApproveTools("normal", defaultToolApprovalMode === "yolo"),
4778 collaborationMode: "normal",
4779 toolApprovalMode: defaultToolApprovalMode,
4780 tokenMode: "full",
4781 active: true,
4782 cwd: "",
4783 };
4784 mockTabs = [...mockTabs.map((item) => ({ ...item, active: false })), tab];
4785 return { ...tab };
4786 },
4787 async OpenTopicSession(scope: string, workspaceRoot: string, topicID: string, sessionPath: string) {
4788 const tab = scope === "project"
4789 ? await this.OpenProjectTab(workspaceRoot, topicID)
4790 : await this.OpenGlobalTab(topicID);
4791 const active = { ...tab, sessionPath };
4792 mockTabs = mockTabs.map((item) => (item.id === tab.id ? active : item));
4793 return { ...active };
4794 },
4795 async EnsureBlankTab(scope: string, workspaceRoot: string) {
4796 const targetScope = scope === "project" && workspaceRoot ? "project" : "global";
4797 const targetRoot = targetScope === "project" ? workspaceRoot : "";
4798 const existing = mockTabs.find((tab) =>
4799 tab.scope === targetScope &&
4800 (targetScope === "global" || tab.workspaceRoot === targetRoot) &&
4801 !tab.running &&
4802 mockTopicIsBlank(tab.topicId)
4803 );
4804 if (existing) {
4805 setMockActiveTab(existing.id);
4806 return { ...existing, active: true };
4807 }
4808 const topic = await this.CreateTopic(targetScope, targetRoot, "");
4809 return targetScope === "global" ? this.OpenGlobalTab(topic.id) : this.OpenProjectTab(targetRoot, topic.id);
4810 },
4811 async ActivateTopic(scope: string, workspaceRoot: string, topicID: string, sessionPath: string) {
4812 const tab = sessionPath
4813 ? await this.OpenTopicSession(scope, workspaceRoot, topicID, sessionPath)
4814 : scope === "project"
4815 ? await this.OpenProjectTab(workspaceRoot, topicID)
4816 : await this.OpenGlobalTab(topicID);
4817 mockTabs = mockTabs.filter((item) => item.id === tab.id).map((item) => ({ ...item, active: true }));
4818 return { ...mockTabs[0] };
4819 },
4820 async EnsureBlankSurface(scope: string, workspaceRoot: string) {
4821 const tab = await this.EnsureBlankTab(scope, workspaceRoot);
4822 mockTabs = mockTabs.filter((item) => item.id === tab.id).map((item) => ({ ...item, active: true }));
4823 return { ...mockTabs[0] };
4824 },
4825 async SetActiveTab(_tabID: string) {
4826 setMockActiveTab(_tabID);
4827 const tab = mockTabs.find((item) => item.id === _tabID);
4828 if (tab) queueMockTopicRuntime(tab);
4829 },
4830 async ReorderTabs(_tabIDs: string[]) {
4831 const byId = new Map(mockTabs.map((tab) => [tab.id, tab]));
4832 const ordered = _tabIDs.map((id) => byId.get(id)).filter((tab): tab is TabMeta => Boolean(tab));
4833 if (ordered.length === mockTabs.length) mockTabs = ordered;
4834 },
4835 async CloseTab(_tabID: string) {
4836 if (mockTabs.length <= 1) return;
4837 const terminalIDs = mockTerminalSessions
4838 .filter((session) => mockTerminalTabIDs.get(session.id) === _tabID)
4839 .map((session) => session.id);
4840 mockTerminalSessions = mockTerminalSessions.filter((session) => !terminalIDs.includes(session.id));
4841 terminalIDs.forEach((id) => {
4842 mockTerminalOutput.delete(id);
4843 mockTerminalTabIDs.delete(id);
4844 __emitMockTerminalExit({ id, exitCode: 0, removed: true });
4845 });
4846 const wasActive = mockTabs.some((tab) => tab.id === _tabID && tab.active);
4847 mockTabs = mockTabs.filter((tab) => tab.id !== _tabID);
4848 if (wasActive && mockTabs.length > 0 && !mockTabs.some((tab) => tab.active)) {
4849 mockTabs[mockTabs.length - 1] = { ...mockTabs[mockTabs.length - 1], active: true };
4850 }
4851 },
4852 async TerminalWorkspaceForTab(tabID: string) {
4853 const tab = mockTabs.find((candidate) => candidate.id === tabID) ?? mockTabs.find((candidate) => candidate.active);
4854 const sessions = tab ? mockTerminalSessions.filter((session) => mockTerminalTabIDs.get(session.id) === tab.id) : [];
4855 return {
4856 available: true,
4857 readOnly: Boolean(tab?.readOnly),
4858 sessions: sessions.map((session) => ({ ...session })),
4859 shells: [
4860 { id: "default", label: "Default shell" },
4861 { id: "bash", label: "bash" },
4862 { id: "zsh", label: "zsh" },
4863 ],
4864 };
4865 },
4866 async TerminalOutputForTab(tabID: string, sessionID: string) {
4867 if (mockTerminalTabIDs.get(sessionID) !== tabID) return "";
4868 return mockTerminalOutput.get(sessionID) ?? "";
4869 },
4870 async CreateTerminalForTab(tabID: string, relativePath: string, shellID: string) {
4871 const tab = mockTabs.find((candidate) => candidate.id === tabID) ?? mockTabs.find((candidate) => candidate.active);
4872 if (tab?.readOnly) throw new Error("channel session is read-only");
4873 const id = `term-mock-${Date.now()}-${Math.random().toString(16).slice(2)}`;
4874 const session: TerminalSessionView = {
4875 id,
4876 title: shellID || "Default shell",
4877 shell: shellID || "default",
4878 cwd: `${tab?.cwd || cwd}/${relativePath || "."}`.replace(/\/\.\/?$/, ""),
4879 createdAt: Date.now(),
4880 running: true,
4881 };
4882 mockTerminalSessions = [...mockTerminalSessions, session];
4883 mockTerminalOutput.set(id, "Reasonix terminal ready\r\n");
4884 mockTerminalTabIDs.set(id, tabID);
4885 window.setTimeout(() => __emitMockTerminalOutput({ id, data: mockTerminalBytes("Reasonix terminal ready\r\n") }), 0);
4886 return { ...session };
4887 },
4888 async WriteTerminalForTab(_tabID: string, sessionID: string, data: string) {
4889 const session = mockTerminalSessions.find((candidate) => candidate.id === sessionID);
4890 if (!session?.running) throw new Error("terminal session has exited");
4891 mockTerminalOutput.set(sessionID, `${mockTerminalOutput.get(sessionID) ?? ""}${data}`);
4892 window.setTimeout(() => __emitMockTerminalOutput({ id: sessionID, data: mockTerminalBytes(data) }), 0);
4893 },
4894 async ResizeTerminalForTab() {},
4895 async CloseTerminalForTab(_tabID: string, sessionID: string) {
4896 mockTerminalSessions = mockTerminalSessions.filter((session) => session.id !== sessionID);
4897 mockTerminalOutput.delete(sessionID);
4898 mockTerminalTabIDs.delete(sessionID);
4899 __emitMockTerminalExit({ id: sessionID, exitCode: 0, removed: true });
4900 },
4901 async RenameTerminalForTab(_tabID: string, sessionID: string, title: string) {
4902 mockTerminalSessions = mockTerminalSessions.map((session) => session.id === sessionID ? { ...session, title } : session);
4903 },
4904 async ListProjectTree() {
4905 return cloneProjectTree();
4906 },
4907 async RenameProject(workspaceRoot: string, title: string) {
4908 const node = workspaceRoot
4909 ? mockProjectTree.find((item) => item.root === workspaceRoot)
4910 : mockProjectTree.find((item) => item.kind === "global_folder");
4911 if (node) node.label = title.trim() || (node.kind === "global_folder" ? "Global" : node.label);
4912 },
4913 async SetProjectColor(workspaceRoot: string, color: string) {
4914 const node = workspaceRoot
4915 ? mockProjectTree.find((item) => item.root === workspaceRoot)
4916 : mockProjectTree.find((item) => item.kind === "global_folder");
4917 if (!node) return;
4918 node.projectColor = color || undefined;
4919 for (const child of projectChildren(node)) child.projectColor = node.projectColor;
4920 mockTabs = mockTabs.map((tab) =>
4921 (workspaceRoot ? tab.workspaceRoot === workspaceRoot : tab.scope === "global")
4922 ? { ...tab, projectColor: node.projectColor }
4923 : tab,
4924 );
4925 },
4926 async SetProjectPinned(workspaceRoot: string, pinned: boolean) {
4927 setMockProjectPinned(workspaceRoot, pinned);
4928 },
4929 async ReorderProjects(workspaceRoots: string[]) {
4930 const projects = mockProjectTree.filter((node) => node.kind === "project");
4931 const globals = mockProjectTree.filter((node) => node.kind === "global_folder");
4932 if (!workspaceRoots.includes(GLOBAL_PROJECT_ORDER_KEY)) {
4933 if (workspaceRoots.length !== projects.length) return;
4934 const byRoot = new Map(projects.map((node) => [node.root, node]));
4935 const ordered = workspaceRoots.map((root) => byRoot.get(root)).filter((node): node is ProjectNode => Boolean(node));
4936 if (ordered.length !== projects.length) return;
4937 mockProjectTree.splice(0, mockProjectTree.length, ...globals, ...ordered);
4938 return;
4939 }
4940 const byKey = new Map<string, ProjectNode>();
4941 for (const node of projects) {
4942 if (node.root) byKey.set(node.root, node);
4943 }
4944 for (const node of globals) byKey.set(GLOBAL_PROJECT_ORDER_KEY, node);
4945 const seen = new Set<string>();
4946 const ordered: ProjectNode[] = [];
4947 for (const key of workspaceRoots) {
4948 if (seen.has(key)) return;
4949 const node = byKey.get(key);
4950 if (!node) return;
4951 seen.add(key);
4952 ordered.push(node);
4953 }
4954 if (ordered.length !== projects.length + globals.length) return;
4955 mockProjectTree.splice(0, mockProjectTree.length, ...ordered);
4956 },
4957 async CreateTopic(_scope: string, _workspaceRoot: string, title: string) {
4958 const now = Date.now();
4959 const id = "topic_" + now;
4960 const topicTitle = title.trim() || t("mock.newSession");
4961 const parent = _scope === "global"
4962 ? ensureMockGlobalFolder()
4963 : mockProjectTree.find((node) => node.root === _workspaceRoot);
4964 if (parent) {
4965 const global = parent.kind === "global_folder";
4966 parent.children = [{
4967 key: parent.kind === "global_folder" ? "global_topic_" + id : "topic_" + id,
4968 kind: global ? "global_topic" : "topic",
4969 label: topicTitle,
4970 root: parent.root,
4971 topicId: id,
4972 projectColor: parent.projectColor,
4973 createdAt: now,
4974 }, ...projectChildren(parent)];
4975 }
4976 return { id, title: topicTitle, createdAt: now };
4977 },
4978 async RenameTopic(topicID: string, title: string) {
4979 const topic = findMockTopic(topicID);
4980 const nextTitle = title.trim();
4981 if (!topic || !nextTitle) return;
4982 const activePrefix = topic.label?.startsWith("● ") ? "● " : "";
4983 topic.label = `${activePrefix}${nextTitle}`;
4984 mockTabs = mockTabs.map((tab) =>
4985 tab.topicId === topicID ? { ...tab, topicTitle: nextTitle } : tab,
4986 );
4987 },
4988 async DeleteTopic(topicID: string) {
4989 deleteMockTopic(topicID);
4990 },
4991 async TrashTopic(topicID: string) {
4992 deleteMockTopic(topicID);
4993 },
4994 async SetTopicPinned(topicID: string, pinned: boolean) {
4995 setMockTopicPinned(topicID, pinned);
4996 },
4997 async SaveWindowState(_state) {
4998 // no-op in browser dev — no real window geometry to persist
4999 },
5000 async ContextPanel(_tabID: string) {
5001 const now = Date.now();
5002 const currency = "¥";
5003 const cost = (usd: number) => currency === "¥" ? Number((usd * 7.15).toFixed(4)) : usd;
5004 return {
5005 usedTokens: 42124,
5006 windowTokens: 128000,
5007 promptTokens: 22134,
5008 completionTokens: 12345,
5009 totalTokens: 34479,
5010 reasoningTokens: 7521,
5011 cacheHitTokens: 87000,
5012 cacheMissTokens: 13000,
5013 sessionCacheHitTokens: 87000,
5014 sessionCacheMissTokens: 13000,
5015 sessionCompletionTokens: 12345,
5016 requestCount: 10,
5017 elapsedMs: 33 * 60 * 1000,
5018 sessionCost: cost(0.018),
5019 sessionCurrency: currency,
5020 sessionCostUsd: cost(0.018),
5021 sources: {
5022 executor: {
5023 promptTokens: 24100,
5024 completionTokens: 8300,
5025 totalTokens: 32400,
5026 reasoningTokens: 5200,
5027 cacheHitTokens: 76000,
5028 cacheMissTokens: 9000,
5029 requestCount: 4,
5030 sessionCost: cost(0.0124),
5031 sessionCurrency: currency,
5032 sessionCostUsd: cost(0.0124),
5033 },
5034 planner: {
5035 promptTokens: 1800,
5036 completionTokens: 600,
5037 totalTokens: 2400,
5038 reasoningTokens: 420,
5039 cacheHitTokens: 3400,
5040 cacheMissTokens: 700,
5041 requestCount: 1,
5042 sessionCost: cost(0.0011),
5043 sessionCurrency: currency,
5044 sessionCostUsd: cost(0.0011),
5045 },
5046 subagent: {
5047 promptTokens: 4200,
5048 completionTokens: 2100,
5049 totalTokens: 6300,
5050 reasoningTokens: 1500,
5051 cacheHitTokens: 6100,
5052 cacheMissTokens: 2100,
5053 requestCount: 2,
5054 sessionCost: cost(0.0032),
5055 sessionCurrency: currency,
5056 sessionCostUsd: cost(0.0032),
5057 },
5058 compaction: {
5059 promptTokens: 2600,
5060 completionTokens: 700,
5061 totalTokens: 3300,
5062 reasoningTokens: 260,
5063 cacheHitTokens: 1100,
5064 cacheMissTokens: 900,
5065 requestCount: 1,
5066 sessionCost: cost(0.0009),
5067 sessionCurrency: currency,
5068 sessionCostUsd: cost(0.0009),
5069 },
5070 classifier: {
5071 promptTokens: 900,
5072 completionTokens: 120,
5073 totalTokens: 1020,
5074 reasoningTokens: 70,
5075 cacheHitTokens: 300,
5076 cacheMissTokens: 250,
5077 requestCount: 1,
5078 sessionCost: cost(0.0003),
5079 sessionCurrency: currency,
5080 sessionCostUsd: cost(0.0003),
5081 },
5082 title: {
5083 promptTokens: 420,
5084 completionTokens: 80,
5085 totalTokens: 500,
5086 reasoningTokens: 20,
5087 cacheHitTokens: 100,
5088 cacheMissTokens: 50,
5089 requestCount: 1,
5090 sessionCost: cost(0.0001),
5091 sessionCurrency: currency,
5092 sessionCostUsd: cost(0.0001),
5093 },
5094 },
5095 mock: true,
5096 readFiles: [
5097 { path: "README.md", turn: 2, time: now - 34 * 60 * 1000 },
5098 { path: "go.mod", turn: 3, time: now - 30 * 60 * 1000 },
5099 { path: "desktop/file.go", turn: 5, time: now - 13 * 60 * 1000, offset: 0, limit: 180 },
5100 { path: "internal/event.go", turn: 6, time: now - 4 * 60 * 1000, offset: 120, limit: 80, truncated: true },
5101 ],
5102 changedFiles: [
5103 { path: t("mock.changedFile1Path"), sources: ["session"], gitStatus: "modified", turns: [5, 6], latestPrompt: t("mock.changedFile1Prompt"), latestTime: now - 2 * 60 * 1000 },
5104 { path: t("mock.changedFile2Path"), sources: ["session"], gitStatus: "added", turns: [6], latestPrompt: t("mock.changedFile2Prompt"), latestTime: now - 60 * 1000 },
5105 ],
5106 };
5107 },
5108
5109 // ── Remote (SSH) mock ──
5110 async RemoteHosts() {
5111 return mockRemoteHosts.slice();
5112 },
5113 async AddRemoteHost(input) {
5114 const view = mockRemoteHostView(input.label, input);
5115 mockRemoteHosts = [...mockRemoteHosts.filter((h) => h.id !== view.id), view];
5116 return view;
5117 },
5118 async UpdateRemoteHost(id, input) {
5119 const previous = mockRemoteHosts.find((h) => h.id === id);
5120 const view = mockRemoteHostView(id, input, previous);
5121 mockRemoteHosts = mockRemoteHosts.map((h) => (h.id === id ? view : h));
5122 return view;
5123 },
5124 async RemoveRemoteHost(id) {
5125 mockRemoteHosts = mockRemoteHosts.filter((h) => h.id !== id);
5126 delete mockRemoteConn[id];
5127 },
5128 async ScanSSHConfig() {
5129 return [
5130 { label: "gpu-box", host: "gpu-box", port: 0, user: "", identityFile: "", proxyJump: "", defaultWorkspace: "", serveInstall: "auto", useSSHConfig: true, preserveExistingSettings: true },
5131 ];
5132 },
5133 async ConnectRemoteHost(id) {
5134 mockRemoteConn[id] = "connecting";
5135 __emitMockRemote("status", { hostId: id, state: "connecting" });
5136 setTimeout(() => {
5137 mockRemoteConn[id] = "connected";
5138 __emitMockRemote("status", { hostId: id, state: "connected" });
5139 }, 300);
5140 },
5141 async DisconnectRemoteHost(id) {
5142 mockRemoteConn[id] = "stopped";
5143 __emitMockRemote("status", { hostId: id, state: "stopped" });
5144 },
5145 async RemoteConnectionStatuses() {
5146 return Object.entries(mockRemoteConn).map(([hostId, state]) => ({ hostId, state: state as RemoteConnectionStatus["state"] }));
5147 },
5148 async ConfirmRemoteHostKey(hostId, accept) {
5149 mockRemoteConn[hostId] = accept ? "connected" : "stopped";
5150 __emitMockRemote("status", { hostId, state: mockRemoteConn[hostId] });
5151 },
5152 async ConfirmRemoteSecret(hostId, _promptId, _secret, accept) {
5153 mockRemoteConn[hostId] = accept ? "connected" : "stopped";
5154 __emitMockRemote("status", { hostId, state: mockRemoteConn[hostId] });
5155 },
5156 async ListRemoteDir(_hostId, path) {
5157 const base = path.replace(/\/$/, "");
5158 return [
5159 { name: "src", path: `${base}/src`, isDir: true, size: 0, mtimeUnix: 1_700_000_000, symlink: false },
5160 { name: "README.md", path: `${base}/README.md`, isDir: false, size: 1024, mtimeUnix: 1_700_000_500, symlink: false },
5161 ];
5162 },
5163 async ReadRemoteFile(_hostId, path) {
5164 return { path, body: `# Mock remote file\n${path}\n`, size: 40, mtimeUnix: 1_700_000_500, truncated: false, binary: false };
5165 },
5166 async WriteRemoteFile(_hostId, _path, _body, _expectMtimeUnix) {
5167 return { ok: true, conflict: false, newMtimeUnix: 1_700_000_900 };
5168 },
5169 async MkdirRemote() {},
5170 async RenameRemotePath() {},
5171 async DeleteRemotePath() {},
5172 async RemoteForwards(hostId) {
5173 return mockRemoteForwards[hostId] ?? [];
5174 },
5175 async AddRemoteForward(hostId, input) {
5176 const view: RemoteForwardView = { id: `L:${input.localPort}`, hostId, ...input, state: "active" };
5177 mockRemoteForwards[hostId] = [...(mockRemoteForwards[hostId] ?? []), view];
5178 __emitMockRemote("forwards", { hostId, forwards: mockRemoteForwards[hostId] });
5179 return view;
5180 },
5181 async RemoveRemoteForward(hostId, forwardId) {
5182 mockRemoteForwards[hostId] = (mockRemoteForwards[hostId] ?? []).filter((f) => f.id !== forwardId);
5183 __emitMockRemote("forwards", { hostId, forwards: mockRemoteForwards[hostId] });
5184 },
5185 async OpenRemoteWorkspace() {},
5186 async StopRemoteServer(hostId) {
5187 __emitMockRemote("server", { hostId, workspace: "", state: "stopped" });
5188 },
5189 async RemoteServerStatus(hostId) {
5190 return { hostId, workspace: "~/app", state: "stopped" };
5191 },
5192 async RemoteServerLogs() {
5193 return "mock serve log line 1\nmock serve log line 2\n";
5194 },
5195 async RemoteLastWorkspace() {
5196 return "~/app";
5197 },
5198 async ScanRemoteLegacyWorkbenchData() {
5199 return { mirrorCount: 0, mirrorBytes: 0, trustFile: false };
5200 },
5201 async ExtensionActions() {
5202 return [];
5203 },
5204 async InvokeExtensionAction() {
5205 return "";
5206 },
5207 async SubmitExtensionForm() {},
5208 async CleanRemoteLegacyWorkbenchData() {},
5209 };
5210 }
5211
5212 // Mock remote state, module-scoped so it survives across mock method calls.
5213 function mockRemoteHostView(id: string, input: RemoteHostInput, previous?: RemoteHostView): RemoteHostView {
5214 return {
5215 id,
5216 label: input.label,
5217 host: input.host,
5218 port: input.port,
5219 user: input.user,
5220 identityFile: input.identityFile,
5221 proxyJump: input.proxyJump,
5222 defaultWorkspace: input.defaultWorkspace,
5223 serveInstall: input.serveInstall,
5224 useSSHConfig: input.useSSHConfig,
5225 passwordSet: input.password ? true : input.clearPassword ? false : previous?.passwordSet,
5226 keyPassphraseSet: input.keyPassphrase ? true : input.clearPassphrase ? false : previous?.keyPassphraseSet,
5227 };
5228 }
5229
5230 let mockRemoteHosts: RemoteHostView[] = [
5231 { id: "demo", label: "demo", host: "192.168.1.10", port: 22, user: "dev", identityFile: "", proxyJump: "", defaultWorkspace: "~/app", serveInstall: "auto", useSSHConfig: false },
5232 ];
5233 const mockRemoteConn: Record<string, RemoteConnectionStatus["state"]> = {};
5234 const mockRemoteForwards: Record<string, RemoteForwardView[]> = {};
5235
5235 lines TYPESCRIPT