返回 DeepSeek-Reasonix
bridge.ts
根目录 / desktop / frontend / src / lib / bridge.ts
1 import { makeMockSessionExportBindings, type SessionExportBindings } from "./sessionExportBridge";
2 import type { AttachmentBindings } from "./attachmentBindings";
3 import { makeMockSessionLifecycleBindings, type SessionLifecycleBindings } from "./sessionLifecycleBindings";
4 import { makeMockModelSettingsBindings, type ModelSettingsBindings } from "./modelSettingsBridge";
5 import { mockProviderTemplate, mockPreset, mockBundlePreset, mockKimiAPIModels, mockLongCatModels, mockTokenRhythmModels, mockTokenRhythmModelOverrides, mockMiMoV25Models, mockMiniMaxModels, mockGLMAPIModels, mockGLMCodingModels, mockGLMAnthropicModels, mockQwenAPIModels, mockQwenPlanModels, mockQwenPlanVisionModels, mockStepFunModels, mockOpenCodeGoModels, mockNovitaModels, mockGMIModels, mockVercelModels, mockOllamaCloudModels } from "./mockProviderTemplates";
6 // The Electron host and the browser mock share this React-to-Go contract.
7 import type {
8 CancelReceipt,
9 ChatFileReferenceRequest,
10 ChatFileReferenceResult,
11 DesktopCommandName,
12 HistoryWindowPage,
13 HistoryWindowRequest,
14 LegacyEmptySessionCleanupStatus,
15 MarkdownSVGView,
16 MessageFieldPage,
17 MessageHistoryPage,
18 MessageLocation,
19 Ref as SessionContentRef,
20 SearchHistoryPage,
21 SessionArchitectureDiagnostics,
22 SessionCreationResult,
23 SessionDraftContextView,
24 SessionDraftSaveRequest,
25 SessionDraftSaveResult,
26 SessionDraftSubmissionRequest,
27 SessionDraftSubmissionView,
28 SessionDraftSummary,
29 SessionDraftView,
30 SessionHistoryContentChunk,
31 SessionMutationResult,
32 SessionRef,
33 SessionSelector,
34 ComposerTarget,
35 WorkspaceSessionPage,
36 WorkspaceSnapshot,
37 } from "../generated/desktopContract.generated";
38 import type { ExactInteractionBindings } from "./exactInteractionBindings";
39 import type { InvocationRequest } from "./invocationDisplay";
40 import type { FollowupBindings } from "./pendingFollowup";
41 import { addBreadcrumb } from "./breadcrumbs";
42 import { maybeShare } from "./queryCoalesce";
43 import { makeMockSessionCatalogBindings } from "./sessionCatalogBridge";
44 import { makeMockHistoryCatalogBindings, type HistoryCatalogBindings } from "./historyCatalogBridge";
45 import { makeMockTaskCatalogBindings, type TaskCatalogBindings } from "./taskCatalogBridge";
46 import { makeMockBlankProjectBindings, type BlankProjectBindings } from "./blankProjectBridge";
47 import { makeMockQualityFloorBindings, type QualityFloorBindings } from "./deliveryFloorBridge";
48 import { t } from "./i18n";
49 import { makeMockForkBindings, withMockHistoryIds } from "./mockForkWorktree";
50 import { makeMockWorktreeMergeBindings } from "./worktreeMergeMock";
51 import { providerIsConfigured, providerRequiresKey, removeProviderAccessesForMock } from "./providerModels";
52 import { DEFAULT_STATUS_BAR_ITEMS } from "./statusBarItems";
53 import { registerTrustedThemeBackgroundURLs } from "./themePack";
54 import { modeHasAutoApproveTools, modeWithAutoApproveTools, modeWithPlan, normalizeCollaborationMode, normalizeMode, normalizeToolApprovalMode } from "./types";
55 import { makeMockProjectTreeOrganizationBindings, subscribeMockProjectTreeChanged, notifyMockProjectTreeChanged } from "./mockProjectTreeOrganization";
56 import { decisionSurfaceMockFromInput, isLongDecisionOptionsMockInput } from "./decisionSurfaceMock";
57 import { mockWorkspaceFile } from "./mockWorkspaceFile";
58 import { mockAIRenameTarget, mockSessionTitleTarget, type SessionTitleBindings } from "./mockSessionTitle";
59 import { sessionTitleTarget } from "./sessionTitleOperation";
60 import { mockHistoryContentField, mockHistorySlice, mockTopicHistory as topicHistoryFixture } from "./bridgeHistoryFixtures";
61 import { createMockModelScopePreset, type MockProviderPresetTemplate } from "./mockModelScopePreset";
62 import { createMockRemoteProjects } from "./mockRemoteProjects";
63 import { createBrowserMockInteractionIdentity, mockSessionMeta, withMockSessionIdentity } from "./browserMockInteractionIdentity";
64 import { mockRemoteHostView } from "./mockRemoteHosts";
65 import type { RemoteProjectBindings } from "./remoteProjectBridge";
66 import type { ForkTargetsBindings } from "./forkTargets";
67 import type { ToolRecoveryBindings } from "./toolRecovery";
68 import type { ScrollDiagnosticBindings } from "./scrollDiagnosticBridge";
69 import type { TranscriptProtocolBindings } from "./transcriptProtocol";
70 import { makeMockSessionReaderBindings, publishMockTranscriptEvent, type SessionReaderBindings } from "./sessionReaderBridge";
71 import { makeMockMCPAppBindings, type MCPAppBindings } from "./mcpAppBridge";
72 import { makeMockPinnedContextBindings, type PinnedContextBindings } from "./pinnedContextBridge";
73 import { createDesktopPreferencesMock } from "./desktopPreferencesMock";
74 import type {
75 TurnChanges,
76 TurnFileChange,
77 RemoteHostView,
78 RemoteHostInput,
79 RemoteConnectionStatus,
80 RemoteDirEntry,
81 RemoteFilePreview,
82 RemoteWriteResult,
83 RemoteForwardInput,
84 RemoteForwardView,
85 RemoteServerView,
86 RemoteForwardsEvent,
87 RemoteLegacyWorkbenchData,
88 BalanceInfo,
89 UsageStatsRange,
90 UsageStatsRequest,
91 BotConnectionDiagnostic,
92 BotInstallPollResult,
93 BotInstallStartResult,
94 BotRuntimeStatusView,
95 BotSettingsView,
96 CapabilitiesView,
97 CapabilityDiagnosticsReport,
98 CredentialDiagnosticReport,
99 RuntimeDoctorReport,
100 CheckpointMeta,
101 CommandInfo,
102 ControlResult,
103 ContextInfo,
104 ContextPanelInfo,
105 DirEntry,
106 DesktopStartupSettingsView,
107 DeliveryWorktreeAvailability,
108 DeliveryWorktreeOpenResult,
109 WorktreeMergeInspection,
110 WorktreeMergeRequest,
111 WorktreeMergeResult,
112 WorktreeCleanupRequest,
113 WorktreeCleanupResult,
114 CloseMergedWorktreeTabRequest,
115 CloseMergedWorktreeTabResult,
116 DroppedItem,
117 EffortInfo,
118 ExtensionActionView,
119 FilePreview,
120 ExternalOpenersView,
121 HistoryMessage,
122 HistoryPage,
123 HistorySearchPage,
124 HistoryContentChunk,
125 HistoryContentRef,
126 HistorySlice,
127 HistorySliceRequest,
128 TabMetaRefreshEvent,
129 TopicActivationEvent,
130 TopicActivationRequest,
131 TopicActivationTicket,
132 HookConfigView,
133 HooksSettingsView,
134 JobView,
135 ActiveWorkView,
136 BackgroundRuntimeView,
137 JobCancelBatchView,
138 WorkspaceConflictView,
139 MCPMarketplaceEntry,
140 MCPServerInput,
141 MCPInstallResult,
142 MCPMarketplaceView,
143 MCPToolView,
144 MemoryFact,
145 MemorySuggestion,
146 MemorySuggestionsView,
147 MemoryView,
148 Meta,
149 Mode,
150 ModelInfo,
151 NetworkView,
152 PluginInstallOptions,
153 PluginView,
154 PermissionSnapshot,
155 ProjectNode,
156 ProjectTreeOrganizationBindings,
157 RecoveryLineageView,
158 RecoveryCleanupRequest,
159 RecoveryCleanupResult,
160 SessionCatalogBindings,
161 PromptHistoryEntry,
162 PromptHistoryResult,
163 ProviderModelCapabilityView,
164 ProviderModelCatalogUpdate,
165 ProviderPresetView,
166 ProviderView,
167 QuestionAnswer,
168 ServerView,
169 SessionMeta,
170 SessionRecoveryFailedEvent,
171 SessionRecoveryEvent,
172 SettingsView,
173 ShellInstallResult,
174 SkillsSettingsView,
175 SkillRootView,
176 SkillSuggestion,
177 SkillView,
178 TaskEvent,
179 TaskSnapshot,
180 SlashArgsResult,
181 SubagentProfileInput,
182 TabMeta,
183 TerminalSessionView,
184 TerminalWorkspaceView,
185 TopicMeta,
186 ToolApprovalMode,
187 TurnEventReplayView,
188 UpdateInfo,
189 UpdateProgress,
190 WireEvent,
191 WorkspaceChangeDetailView,
192 WorkspaceChangesView,
193 WorkspaceRevisions,
194 GitCommitView,
195 GitCommitDetailView,
196 WorkspaceView,
197 SessionClearResult,
198 } from "./types";
199 import { editMockGoalTab } from "./mockGoalLifecycle";
200 import { browserPreviewShellSupport } from "./shellSupportPreview";
201 import { desktopHost } from "./desktopHost";
202 export * from "./remoteTabEvents";
203 export const COMPACT_RATIO_MIN_PERCENT = 30, COMPACT_RATIO_MAX_PERCENT = 85;
204
205 export interface DesktopShellStatusView {
206 trayState: "probing" | "ready" | "unavailable";
207 backgroundCloseAvailable: boolean;
208 reason?: string;
209 }
210 import type { MarkdownImageView } from "./markdownImage";
211 const GLOBAL_PROJECT_ORDER_KEY = "__global__";
212
213 function stripLegacyGoalBudgetFlags(arg: string): string {
214 const parts = arg.trim().split(/\s+/).filter(Boolean);
215 while (parts.length > 0) {
216 const flag = parts[0].toLowerCase();
217 if (flag !== "--research" && flag !== "--auto-research" && flag !== "--deep" && flag !== "--simple" && flag !== "--no-research") break;
218 parts.shift();
219 }
220 return parts.join(" ");
221 }
222
223 // AppBindings is checked against the generated desktop host contract (see
224 // src/generated/desktopContract.generated.ts), so the compiler catches drift
225 // between the Go binding surface and the frontend mock. After adding or
226 // renaming a bound method on App, run `cd desktop && go run . -emit-contract
227 // frontend/src/generated`, then `pnpm typecheck` to verify the mock still
228 // satisfies the contract.
229 //
230 // Types for native-feel bindings, used only by AppBindings and the dev mock.
231 interface NativeConfirmRequest {
232 title: string;
233 message: string;
234 detail: string;
235 confirmLabel: string;
236 cancelLabel: string;
237 destructive: boolean;
238 }
239 interface DesktopWindowState {
240 width: number;
241 height: number;
242 x: number;
243 y: number;
244 maximised: boolean;
245 }
246 // AppBindings is the hand-written React-to-Go contract. _CheckGeneratedBindings
247 // catches generated methods missing here; update this interface and typecheck.
248 export interface AppBindings extends AttachmentBindings, SessionExportBindings, SessionLifecycleBindings, ForkTargetsBindings, ToolRecoveryBindings, ModelSettingsBindings, SessionCatalogBindings, ProjectTreeOrganizationBindings, HistoryCatalogBindings, TaskCatalogBindings, BlankProjectBindings, QualityFloorBindings, SessionTitleBindings, ScrollDiagnosticBindings, RemoteProjectBindings, MCPAppBindings, PinnedContextBindings, FollowupBindings, TranscriptProtocolBindings, SessionReaderBindings, ExactInteractionBindings {
249 GetLegacyEmptySessionCleanupStatus(): Promise<LegacyEmptySessionCleanupStatus>;
250 RetryLegacyEmptySessionCleanup(): Promise<LegacyEmptySessionCleanupStatus>;
251 OpenSessionDraft(workspaceId: string): Promise<SessionDraftView>;
252 OpenSessionDraftForTarget(scope: string, workspaceRoot: string): Promise<SessionDraftView>;
253 RestoreSessionDraft(): Promise<SessionDraftView | null>;
254 SaveSessionDraft(request: SessionDraftSaveRequest): Promise<SessionDraftSaveResult>;
255 ListSessionDraftSummaries(): Promise<SessionDraftSummary[]>;
256 DiscardSessionDraft(draftId: string, revision: number): Promise<void>;
257 DismissSessionDraft(draftId: string): Promise<void>;
258 SetSessionDraftRestoreTarget(draftId: string): Promise<void>;
259 GetSessionDraft(draftId: string): Promise<SessionDraftView>;
260 GetSessionDraftState(draftId: string): Promise<import("../generated/desktopContract.generated").SessionDraftState>;
261 ResumeDraftSubmission(operationId: string, revision: number): Promise<SessionDraftSubmissionView>;
262 GetDraftContext(draftId: string): Promise<SessionDraftContextView>;
263 BeginDraftSubmission(request: SessionDraftSubmissionRequest): Promise<SessionDraftSubmissionView>;
264 GetDraftSubmission(operationId: string): Promise<SessionDraftSubmissionView>;
265 CancelDraftSubmission(operationId: string): Promise<SessionDraftSubmissionView>;
266 SavePastedImageForComposerTarget(target: ComposerTarget, dataUrl: string): Promise<string>;
267 SavePastedFileForComposerTarget(target: ComposerTarget, name: string, dataUrl: string): Promise<string>;
268 SaveClipboardImageForComposerTarget(target: ComposerTarget): Promise<string>;
269 AttachDroppedForComposerTarget(target: ComposerTarget, path: string): Promise<DroppedItem>;
270 ListDirForTarget(target: ComposerTarget, rel: string): Promise<DirEntry[]>;
271 SearchFileRefsForTarget(target: ComposerTarget, query: string): Promise<DirEntry[]>;
272 AttachmentDataURLForComposerTarget(target: ComposerTarget, path: string): Promise<string>;
273 GetSessionActivityBaseline(selector: SessionSelector): Promise<import("../generated/desktopContract.generated").SessionActivityBaseline>;
274 GetWorkspaceSnapshot(): Promise<WorkspaceSnapshot>;
275 CreateSession(workspaceId: string): Promise<SessionRef>;
276 ForkSession(ref: SessionRef, turnBoundary: string): Promise<SessionRef>;
277 ForkSessionTarget(selector: SessionSelector, turnBoundary: string): Promise<SessionRef>;
278 CopySessionTarget(selector: SessionSelector, operationId: string): Promise<SessionCreationResult>;
279 HistorySliceForTarget(selector: SessionSelector, request: HistorySliceRequest): Promise<HistorySlice>;
280 HistoryContentForTarget(selector: SessionSelector, ref: HistoryContentRef, chunkIndex: number): Promise<HistoryContentChunk>;
281 SearchHistoryContentForTarget(selector: SessionSelector, query: string, cursor: string, limit: number): Promise<HistorySearchPage>;
282 SessionHistoryPageForTarget(selector: SessionSelector, cursor: string, limit: number): Promise<MessageHistoryPage>;
283 SessionHistoryContentForTarget(selector: SessionSelector, ref: SessionContentRef, offset: number): Promise<SessionHistoryContentChunk>;
284 LocateSessionMessageForTarget(selector: SessionSelector, messageId: string, snapshot: number): Promise<MessageLocation>;
285 SessionMessageFieldForTarget(selector: SessionSelector, messageId: string, version: number, field: string, offset: number, length: number): Promise<MessageFieldPage>;
286 SearchSessionHistoryForTarget(selector: SessionSelector, query: string, cursor: string, limit: number): Promise<SearchHistoryPage>;
287 SessionHistoryWindowForTarget(selector: SessionSelector, request: HistoryWindowRequest): Promise<HistoryWindowPage>;
288 ListWorkspaceSessions(workspaceId: string, query: string, cursor: string, limit: number, includeArchived: boolean): Promise<WorkspaceSessionPage>;
289 OpenSession(ref: SessionRef): Promise<HistoryPage>;
290 ReadSessionHistory(ref: SessionRef, cursor: string, limit: number): Promise<HistoryPage>;
291 RenameCanonicalSession(ref: SessionRef, title: string): Promise<void>;
292 SetSessionPinned(selector: SessionSelector, pinned: boolean): Promise<void>;
293 ArchiveCanonicalSession(ref: SessionRef): Promise<void>;
294 ArchiveSessionTarget(selector: SessionSelector): Promise<SessionMutationResult>;
295 RestoreCanonicalSession(ref: SessionRef): Promise<void>;
296 RestoreSessionTarget(selector: SessionSelector): Promise<SessionMutationResult>;
297 DeleteSessionTarget(selector: SessionSelector): Promise<SessionMutationResult>;
298 MoveWorkspaceSession(workspaceId: string, sessionId: string, beforeSessionId: string): Promise<void>;
299 MoveSessionTarget(selector: SessionSelector, workspaceId: string, beforeSessionId: string): Promise<SessionMutationResult>;
300 RenameWorkspace(workspaceId: string, title: string): Promise<void>;
301 SetWorkspaceVisible(workspaceId: string, visible: boolean): Promise<void>;
302 MoveWorkspace(workspaceId: string, beforeWorkspaceId: string): Promise<void>;
303 GetSessionArchitectureDiagnostics(): Promise<SessionArchitectureDiagnostics>;
304 Platform(): Promise<string>;
305 MinimiseMainWindow(): Promise<void>;
306 ToggleMaximiseMainWindow(): Promise<void>;
307 IsMainWindowMaximised(): Promise<boolean>;
308 CloseMainWindow(): Promise<void>;
309 // ── Heartbeat ──
310 HeartbeatListTasks(): Promise<unknown>;
311 HeartbeatReloadTasks(): Promise<unknown>;
312 HeartbeatSaveTasks(tasks: unknown): Promise<void>;
313 HeartbeatReloadConfig(): Promise<unknown>;
314 HeartbeatSaveConfig(update: unknown): Promise<unknown>;
315 HeartbeatTriggerNow(id: string): Promise<void>;
316 HeartbeatGenerateID(): Promise<string>;
317 Submit(input: string): Promise<void>;
318 SubmitToTab(tabID: string, input: string): Promise<void>;
319 SubmitToTabWithID(tabID: string, input: string, submissionID: string): Promise<void>;
320 StartTurnForTab?(tabID: string, input: string, submissionID: string): Promise<{ turnId: string; status: string; disposition?: "turn_started" | "management_handled"; runtimeEpoch?: string; submissionId?: string }>;
321 SubmitDisplay(display: string, input: string): Promise<void>;
322 SubmitDisplayToTab(tabID: string, display: string, input: string): Promise<void>;
323 SubmitDisplayToTabWithID(tabID: string, display: string, input: string, submissionID: string): Promise<void>;
324 SubmitDeliveryRecoveryToTab(tabID: string, display: string, input: string): Promise<void>;
325 SubmitDeliveryRecoveryToTabWithID(tabID: string, display: string, input: string, submissionID: string): Promise<void>;
326 SubmitInvocationsToTab(tabID: string, display: string, input: string, invocations: InvocationRequest[]): Promise<void>;
327 SubmitInvocationsToTabWithID(tabID: string, display: string, input: string, invocations: InvocationRequest[], submissionID: string): Promise<void>;
328 SubmitInitialGoalToTab(
329 tabID: string,
330 goal: string,
331 display: string,
332 input: string,
333 invocations: InvocationRequest[],
334 collaborationMode: string,
335 toolApprovalMode: string,
336 ): Promise<string[]>;
337 SubmitInitialGoalToTabWithID(tabID: string, goal: string, display: string, input: string, invocations: InvocationRequest[], collaborationMode: string, toolApprovalMode: string, submissionID: string): Promise<string[]>;
338 SubmitEditedDisplayToTab(tabID: string, display: string, input: string, original: string): Promise<void>;
339 SubmitEditedDisplayToTabWithID(tabID: string, display: string, input: string, original: string, submissionID: string): Promise<void>;
340 RunShellForTab(tabID: string, command: string): Promise<void>;
341 Steer(text: string): Promise<void>;
342 SteerForTab(tabID: string, text: string): Promise<void>;
343 InboxSnapshot(tabID: string): Promise<{
344 revision: number;
345 paused: boolean;
346 recovered: boolean;
347 recoveredCount?: number; sessionPath?: string;
348 items: Array<{
349 id: string;
350 intent: string;
351 state: string;
352 preview: string;
353 byteSize: number;
354 source?: string;
355 position: number;
356 blockReason?: string;
357 }>;
358 itemsCount: number;
359 bytes: number;
360 maxItems: number;
361 maxBytes: number;
362 }>;
363 EnqueueInboxSteer(tabID: string, display: string, submit: string, idempotency: string): Promise<{ itemId: string; disposition: string; position: number; paused: boolean; idempotent?: boolean; error?: string }>;
364 EnqueueInboxSteerForTurn?(tabID: string, turnID: string, display: string, submit: string, idempotency: string): Promise<{ itemId: string; disposition: string; position: number; paused: boolean; idempotent?: boolean; error?: string }>;
365 SteerInboxItem(tabID: string, itemID: string): Promise<{
366 itemId: string; disposition: string; position: number; paused: boolean; idempotent?: boolean; error?: string;
367 }>;
368 SteerInboxItemForTurn?(tabID: string, turnID: string, itemID: string): Promise<{
369 itemId: string; disposition: string; position: number; paused: boolean; idempotent?: boolean; error?: string;
370 }>;
371 ReadInboxItem(tabID: string, id: string): Promise<{ id: string; displayText: string; rawText: string; submitText: string }>;
372 UpdateInboxItem(tabID: string, id: string, display: string, submit: string): Promise<void>;
373 DeleteInboxItem(tabID: string, id: string): Promise<void>;
374 MoveInboxItem(tabID: string, id: string, toIndex: number): Promise<void>;
375 SetInboxPaused(tabID: string, paused: boolean): Promise<void>;
376 RetryInboxItem(tabID: string, id: string): Promise<void>;
377 RefreshInboxItem(tabID: string, id: string): Promise<void>;
378 InboxHasItems(tabID: string): Promise<boolean>;
379 Cancel(): Promise<void>;
380 CancelTab(tabID: string): Promise<void>;
381 CancelSessionForTab?(tabID: string): Promise<CancelReceipt>;
382 CancelTabWithInboxItems(tabID: string, itemIDs: string[]): Promise<void>;
383 CancelTabWithInboxItemsResult?(tabID: string, itemIDs: string[]): Promise<{ discardedItemIds: string[]; warning?: string }>;
384 InterruptTurnForTab?(tabID: string, turnID: string): Promise<void>;
385 InterruptTurnWithInboxItemsForTab?(tabID: string, turnID: string, itemIDs: string[]): Promise<{ discardedItemIds: string[]; warning?: string }>;
386 TurnEventsForTab?(tabID: string, afterSeq: number): Promise<TurnEventReplayView>;
387 Approve(id: string, allow: boolean, session: boolean, persist: boolean): Promise<void>;
388 ApproveTab(tabID: string, id: string, allow: boolean, session: boolean, persist: boolean): Promise<void>;
389 ApproveTabForTurn?(tabID: string, turnID: string, runtimeEpoch: string, id: string, allow: boolean, session: boolean, persist: boolean): Promise<void>;
390 ResolvePlanDecision(id: string, action: "start_execution" | "revise_plan" | "exit_plan"): Promise<void>;
391 ResolvePlanDecisionTab(tabID: string, id: string, action: "start_execution" | "revise_plan" | "exit_plan"): Promise<void>;
392 ResolvePlanDecisionTabForTurn?(tabID: string, turnID: string, runtimeEpoch: string, id: string, action: "start_execution" | "revise_plan" | "exit_plan"): Promise<void>;
393 ResolveRecovery(id: string, action: string, feedback: string): Promise<void>;
394 ResolveRecoveryTab(tabID: string, id: string, action: string, feedback: string): Promise<void>;
395 ResolveRecoveryTabForTurn?(tabID: string, turnID: string, runtimeEpoch: string, id: string, action: string, feedback: string): Promise<void>;
396 SetRecoveryCheckpointEnabled(enabled: boolean): Promise<void>;
397 SetRecoveryCheckpointEnabledTab(tabID: string, enabled: boolean): Promise<void>;
398 RecoveryCheckpointEnabled(): Promise<boolean>;
399 RecoveryCheckpointEnabledTab(tabID: string): Promise<boolean>;
400 AnswerQuestion(id: string, answers: QuestionAnswer[]): Promise<void>;
401 AnswerQuestionForTab(tabID: string, id: string, answers: QuestionAnswer[]): Promise<void>;
402 AnswerMCPInteractionForTab(
403 tabID: string,
404 id: string,
405 action: "accept" | "decline" | "cancel",
406 content: Record<string, unknown> | null,
407 ): Promise<void>;
408 AnswerMCPInteractionForTurn?(tabID: string, turnID: string, runtimeEpoch: string, id: string, action: "accept" | "decline" | "cancel", content: Record<string, unknown> | null): Promise<void>;
409 AnswerPromptForTab?(tabID: string, turnID: string, id: string, answers: QuestionAnswer[]): Promise<void>;
410 ResolvePromptForTab?(tabID: string, promptID: string, turnID: string, runtimeEpoch: string, kind: string, answer: {
411 questions?: QuestionAnswer[]; allow?: boolean; session?: boolean; persist?: boolean;
412 action?: string; feedback?: string; content?: Record<string, unknown> | null;
413 generation?: number; permissionRevision?: number;
414 }): Promise<void>;
415 PendingPromptIdentitiesForTab?(tabID: string): Promise<Array<{ promptId: string; turnId: string; runtimeEpoch?: string; kind: string }>>;
416 ReplayPendingPromptIdentitiesForTab?(tabID: string): Promise<Array<{ promptId: string; turnId: string; runtimeEpoch?: string; kind: string }>>;
417 ReplayPendingPrompts(): Promise<void>;
418 ReplayPendingPromptsForTab(tabID: string): Promise<void>;
419 SetPlanMode(on: boolean): Promise<void>;
420 SetMode(mode: string): Promise<void>;
421 // Returns auto-allowed prompt ids; unlisted prompts remain pending (#6432).
422 SetModeForTab(tabID: string, mode: string): Promise<string[] | void>;
423 SetAutoApproveTools(on: boolean): Promise<void>;
424 SetCollaborationModeForTab(tabID: string, mode: string): Promise<void>;
425 SetToolApprovalMode(mode: string): Promise<void>;
426 // Same drained-prompt-id contract as SetModeForTab.
427 SetToolApprovalModeForTab(tabID: string, mode: string): Promise<string[] | void>;
428 PermissionSnapshotForTab(tabID: string): Promise<PermissionSnapshot>;
429 SetPermissionPresetForTab(tabID: string, preset: string, expectedRevision: number): Promise<PermissionSnapshot>;
430 RevokePermissionGrantForTab(tabID: string, scope: string, target: string, expectedRevision: number): Promise<PermissionSnapshot>;
431 // Atomically applies the controller-facing composer profile and reports any
432 // approval prompts drained by the resulting tool-approval posture.
433 SetComposerProfileForTab(tabID: string, collaborationMode: string, toolApprovalMode: string, goal: string): Promise<string[] | void>;
434 SetGoal(goal: string): Promise<void>;
435 SetGoalForTab(tabID: string, goal: string): Promise<void>;
436 EditGoalForTab(tabID: string, objective: string, maxGoalRounds: number | null): Promise<void>;
437 ResumeGoalForTab(tabID: string): Promise<boolean>;
438 PauseGoalForTab(tabID: string): Promise<boolean>;
439 ClearGoalForTab(tabID: string): Promise<void>;
440 Compact(): Promise<void>;
441 CompactForTab(tabID: string): Promise<void>;
442 NewSession(): Promise<void>;
443 NewSessionForTab(tabID: string): Promise<void>;
444 ClearSession(): Promise<SessionClearResult>;
445 ClearSessionForTab(tabID: string): Promise<SessionClearResult>;
446 History(): Promise<HistoryMessage[]>;
447 HistoryForTab(tabID: string): Promise<HistoryMessage[]>;
448 HistoryPage(beforeTurn: number, limit: number): Promise<HistoryPage>;
449 HistoryPageForTab(tabID: string, beforeTurn: number, limit: number): Promise<HistoryPage>;
450 // Windowed history paging (supersedes HistoryPageForTab for tab history).
451 HistorySliceForTab(tabID: string, req: HistorySliceRequest): Promise<HistorySlice>;
452 HistoryContentForTab(tabID: string, ref: HistoryContentRef, chunkIndex: number): Promise<HistoryContentChunk>;
453 HistoryCheckpointTurnsForTab(tabID: string): Promise<number[]>;
454 Checkpoints(): Promise<CheckpointMeta[]>;
455 CheckpointsForTab(tabID: string): Promise<CheckpointMeta[]>;
456 Rewind(turn: number, scope: string): Promise<void>;
457 RewindForTab(tabID: string, turn: number, scope: string): Promise<void>;
458 PreviewRewindForTab(tabID: string, turn: number, scope: string): Promise<import("./types").RewindPlanView>;
459 CommitRewindForTab(tabID: string, planID: string, turn: number, scope: string): Promise<import("./types").RewindResultView>;
460 UndoRewindForTab(tabID: string, transactionID: string): Promise<import("./types").RewindResultView>;
461 PreviewWorkspaceFileRevertForTab(tabID: string, path: string): Promise<import("./types").RewindPlanView>;
462 CommitWorkspaceFileRevertForTab(tabID: string, planID: string, resolution: string): Promise<import("./types").RewindResultView>;
463 Fork(turn: number): Promise<TabMeta>;
464 ForkForTab(tabID: string, turn: number): Promise<TabMeta>;
465 ForkWorktreeForTab(tabID: string, turn: number): Promise<import("./forkWorktree").ForkWorktreeResultView>;
466 SummarizeFrom(turn: number): Promise<void>;
467 SummarizeFromForTab(tabID: string, turn: number): Promise<void>;
468 SummarizeUpTo(turn: number): Promise<void>;
469 SummarizeUpToForTab(tabID: string, turn: number): Promise<void>;
470 ListSessions(): Promise<SessionMeta[]>;
471 ListSessionsForTab(tabID: string): Promise<SessionMeta[]>;
472 ListTrashedSessions(): Promise<SessionMeta[]>;
473 ResumeSession(path: string): Promise<HistoryMessage[]>;
474 ResumeSessionForTab(tabID: string, path: string): Promise<HistoryMessage[]>;
475 ResumeSessionPage(path: string, limit: number): Promise<HistoryPage>;
476 ResumeSessionPageForTab(tabID: string, path: string, limit: number): Promise<HistoryPage>;
477 OpenChannelSessionForTab(tabID: string, path: string): Promise<HistoryMessage[]>;
478 OpenChannelSessionPageForTab(tabID: string, path: string, limit: number): Promise<HistoryPage>;
479 PreviewSession(path: string): Promise<HistoryMessage[]>;
480 QuerySessionTakeover(tabId: string): Promise<import("./types").SessionTakeoverView | null>;
481 TakeoverSession(tabId: string, mode: "wait" | "interrupt"): Promise<void>;
482 DeleteSession(path: string): Promise<void>;
483 DeleteRecoveryCopy(path: string): Promise<void>;
484 GetRecoveryLineage(key: { scope: string; workspaceRoot?: string; topicId: string; path?: string; recordClassification?: boolean }): Promise<RecoveryLineageView>;
485 GetSessionVersionState(key: { scope: string; workspaceRoot?: string; topicId: string; path?: string; recordClassification?: boolean }): Promise<import("./types").SessionVersionStateView>;
486 SetActiveSessionVersion(request: import("./types").RecoveryPreferenceRequest): Promise<void>;
487 RetrySessionRecovery(request: import("./types").RecoveryPreferenceRequest): Promise<void>;
488 ReconcileRecoveryVersions(key: { scope: string; workspaceRoot?: string; topicId: string; path?: string }): Promise<void>;
489 ChooseRecoveryBranch(request: import("./types").RecoveryPreferenceRequest): Promise<void>;
490 CleanRecoveryLineage(request: RecoveryCleanupRequest): Promise<RecoveryCleanupResult>;
491 RestoreSession(path: string): Promise<void>;
492 PurgeTrashedSession(path: string): Promise<void>;
493 PurgeRecoveryCopy(path: string): Promise<void>;
494 RenameSession(path: string, title: string): Promise<void>;
495 RenameSessionHead(path: string, headId: string, name: string): Promise<void>;
496 ScanPromptHistory(nonce: string): Promise<PromptHistoryResult>;
497 ListWorkspaces(): Promise<WorkspaceView[]>;
498 PickWorkspace(): Promise<string>;
499 SwitchWorkspace(path: string): Promise<string>;
500 RemoveWorkspace(path: string): Promise<void>;
501 ContextUsageForTab(tabID: string): Promise<ContextInfo>;
502 Balance(): Promise<BalanceInfo>;
503 BalanceForTab(tabID: string): Promise<BalanceInfo>;
504 UsageStats(req: UsageStatsRequest): Promise<UsageStatsRange>;
505 Jobs(): Promise<JobView[]>;
506 ListTasks(): Promise<TaskSnapshot[]>;
507 CurrentTaskSessionID(): Promise<string>;
508 ListTasksForSession(sessionID: string): Promise<TaskSnapshot[]>;
509 GetTask(taskID: string): Promise<TaskSnapshot | null>;
510 StopTask(taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>;
511 CancelTask(taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>;
512 ListTasksForTab(tabID: string): Promise<TaskSnapshot[]>;
513 ListTaskEventsForTab(tabID: string, taskID: string, afterSequence: number): Promise<TaskEvent[]>;
514 StopTaskForTab(tabID: string, taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>;
515 CancelTaskForTab(tabID: string, taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>;
516 RequeueTaskForTab(tabID: string, taskID: string, expectedVersion: number, idemKey: string): Promise<ControlResult>;
517 OpenTaskSessionForTab(tabID: string, taskID: string): Promise<ControlResult>;
518 JobsForTab(tabID: string): Promise<JobView[]>;
519 CancelJob(jobID: string): Promise<boolean>;
520 CancelJobForTab(tabID: string, jobID: string): Promise<boolean>;
521 CancelJobsForTab(tabID: string, jobIDs: string[]): Promise<JobCancelBatchView>;
522 ActiveWorkForTab(tabID: string): Promise<ActiveWorkView>;
523 BackgroundRuntimes(): Promise<BackgroundRuntimeView[]>;
524 RevealBackgroundRuntime(tabID: string): Promise<TabMeta>;
525 WorkspaceConflictForTab(tabID: string): Promise<WorkspaceConflictView>;
526 RevealWorkspaceWriterForTab(tabID: string): Promise<TabMeta>;
527 CloseTabWithPolicy(tabID: string, policy: "keep_running" | "stop_and_close"): Promise<void>;
528 ToolResultForTab(tabID: string, toolID: string): Promise<{ name?: string; args: string; output: string; execution?: import("./types").WireShellExecution; mcpApp?: import("./types").MCPAppPresentation; presentedFiles?: import("./types").PresentedFile[] } | null>;
529 Meta(): Promise<Meta>;
530 MetaForTab(tabID: string): Promise<Meta>;
531 Commands(): Promise<CommandInfo[]>;
532 Capabilities(): Promise<CapabilitiesView>;
533 MCPServers(): Promise<ServerView[]>;
534 MCPCapabilityMatrix(): Promise<{
535 views: Array<{ id: string; layer: string; state: string; negotiated: boolean; detail: string }>;
536 hostProfile: string;
537 }>;
538 MCPMarketplace(query: string): Promise<MCPMarketplaceView>;
539 MCPMarketplaceResolve(registryName: string): Promise<MCPMarketplaceEntry>;
540 SkillsSettings(): Promise<SkillsSettingsView>;
541 CapabilityDiagnostics(includeSessionRuntime: boolean): Promise<CapabilityDiagnosticsReport>;
542 CredentialDiagnostics(probe: boolean): Promise<CredentialDiagnosticReport>;
543 RepairCredentials(dryRun: boolean): Promise<CredentialDiagnosticReport>;
544 RetryAuthenticationForTab(tabId: string): Promise<NonNullable<TabMeta["authentication"]>>;
545 RuntimeDoctor(): Promise<RuntimeDoctorReport>;
546 Plugins(): Promise<PluginView[]>;
547 PlanPluginInstall(source: string, options: PluginInstallOptions): Promise<string>;
548 InstallPlugin(source: string, options: PluginInstallOptions): Promise<string>;
549 RemovePlugin(name: string): Promise<void>;
550 SetPluginEnabled(name: string, enabled: boolean): Promise<void>;
551 UpdatePlugin(name: string): Promise<string>;
552 PluginDoctor(name: string): Promise<PluginView>;
553 // Extension UI (stage 8b2): enumerate handshake-declared extension actions
554 // for the command palette, invoke one, and deliver a form surface's values
555 // (or {cancelled: true} on dismissal) back to the owning sidecar.
556 ExtensionActions(tabID: string): Promise<ExtensionActionView[]>;
557 InvokeExtensionAction(tabID: string, name: string, args: Record<string, string>): Promise<string>;
558 SubmitExtensionForm(tabID: string, pluginID: string, surfaceID: string, values: Record<string, unknown>): Promise<void>;
559 AddMCPServer(input: MCPServerInput): Promise<number>;
560 InstallMCPServer(input: MCPServerInput): Promise<MCPInstallResult>;
561 UpdateMCPServer(name: string, input: MCPServerInput): Promise<void>;
562 RemoveMCPServer(name: string): Promise<void>;
563 AuthorizeAndConnectMCPServer(name: string): Promise<void>;
564 AuthenticateMCPServer(name: string): Promise<void>;
565 ReconnectMCPServer(name: string): Promise<void>;
566 ClearMCPServerAuthentication(name: string): Promise<void>;
567 PickSkillFolder(): Promise<string>;
568 PickPluginFolder(): Promise<string>;
569 AddSkillPath(path: string): Promise<void>;
570 RemoveSkillPath(path: string): Promise<void>;
571 SetSkillPathEnabled(path: string, enabled: boolean): Promise<void>;
572 RefreshSkills(): Promise<void>;
573 ReloadCommands(): Promise<void>;
574 SetSkillEnabled(name: string, enabled: boolean): Promise<void>;
575 SetSkillImplicitInvocation(enabled: boolean): Promise<void>;
576 AvailableSubagentTools(): Promise<MCPToolView[]>;
577 CreateSubagentProfile(input: SubagentProfileInput): Promise<string>;
578 UpdateSubagentProfile(name: string, scope: string, input: SubagentProfileInput): Promise<void>;
579 DeleteSubagentProfile(name: string, scope: string): Promise<void>;
580 SetSubagentProfileModel(name: string, ref: string): Promise<void>;
581 SetSubagentProfileEffort(name: string, level: string): Promise<void>;
582 TrySubagentProfile(input: SubagentProfileInput, task: string): Promise<string>;
583 CancelTrySubagentProfile(): Promise<void>;
584 SetMCPServerEnabled(name: string, enabled: boolean): Promise<void>;
585 SetMCPServerTier(name: string, tier: string): Promise<void>;
586 SlashArgs(input: string): Promise<SlashArgsResult>;
587 ListDir(rel: string): Promise<DirEntry[]>;
588 ListDirForTab(tabID: string, rel: string): Promise<DirEntry[]>;
589 SearchFileRefs(query: string): Promise<DirEntry[]>;
590 SearchFileRefsForTab(tabID: string, query: string): Promise<DirEntry[]>;
591 ReadFile(rel: string): Promise<FilePreview>;
592 ReadFileForTab(tabID: string, rel: string): Promise<FilePreview>;
593 ReadPresentedFileForTab(tabID: string, toolCallID: string, path: string): Promise<FilePreview>;
594 ReadPresentedFileSourceForTab(tabID: string, toolCallID: string, path: string): Promise<FilePreview>;
595 ReadPresentedTextPageForTab(tabID: string, toolCallID: string, path: string, offset: number, expectedVersion: string): Promise<import("./types").PresentedTextPage>;
596 ResolveChatFileReferencesForTab(tabID: string, turnKey: string, candidates: ChatFileReferenceRequest[]): Promise<ChatFileReferenceResult>;
597 ReadReferenceFileForTab(tabID: string, path: string): Promise<FilePreview>;
598 ReadReferenceFileSourceForTab(tabID: string, path: string): Promise<FilePreview>;
599 SanitizeMarkdownSVG(content: string): Promise<MarkdownSVGView>;
600 CreateWorkspaceBrowserPreviewForTab(tabID: string, rel: string): Promise<string>;
601 CreatePresentedBrowserPreviewForTab(tabID: string, toolCallID: string, path: string): Promise<string>;
602 RevokeWorkspaceBrowserPreview(url: string): Promise<void>;
603 RevokeWorkspaceMediaPreview(url: string): Promise<void>;
604 ResolveMarkdownImageForTab(tabID: string, source: string): Promise<MarkdownImageView>;
605 WorkspaceRevisionForTab(tabID: string): Promise<{ revisions: WorkspaceRevisions; watchState: "active" | "degraded" | "unavailable" }>;
606 WorkspaceChanges(tabID: string): Promise<WorkspaceChangesView>;
607 WorkspaceChangeDetail(tabID: string, path: string): Promise<WorkspaceChangeDetailView>;
608 WorkspaceTurnChanges(tabID: string, sessionPath: string, turn: number, resultID: string): Promise<TurnChanges>;
609 WorkspaceTurnChangeDetail(tabID: string, sessionPath: string, turn: number, resultID: string, path: string): Promise<TurnFileChange | null>;
610 TurnCheckLog(tabID: string, sessionPath: string, toolID: string, resultID: string): Promise<{ output: string; truncated: boolean } | null>;
611 GitBranches(): Promise<string[]>;
612 GitCheckout(branch: string): Promise<void>;
613 GitCreateBranch(name: string): Promise<void>;
614 GitBranchesForTab(tabID: string, workspaceRoot: string): Promise<string[]>;
615 GitCheckoutForTab(tabID: string, workspaceRoot: string, branch: string): Promise<void>;
616 GitCreateBranchForTab(tabID: string, workspaceRoot: string, name: string): Promise<void>;
617 WorkspaceGitStatsForTab(tabID: string, workspaceRoot: string): Promise<WorkspaceChangesView>;
618 WorkspaceGitHistory(tabID: string, path: string): Promise<GitCommitView[]>;
619 WorkspaceGitCommitDetail(tabID: string, hash: string, path: string): Promise<GitCommitDetailView>;
620 OpenWorkspacePathForTab(tabID: string, rel: string): Promise<void>;
621 OpenPresentedPathForTab(tabID: string, toolCallID: string, path: string): Promise<void>;
622 OpenReferencePathForTab(tabID: string, path: string): Promise<void>;
623 ResolvePresentedPathForTab(tabID: string, toolCallID: string, path: string): Promise<string>;
624 ResolveReferencePathForTab(tabID: string, path: string): Promise<string>;
625 ResolveWorkspacePathForTab(tabID: string, rel: string): Promise<string>;
626 ExternalOpeners(): Promise<ExternalOpenersView>; ExternalOpenersForTab(tabID: string): Promise<ExternalOpenersView>;
627 SetPreferredExternalOpener(id: string): Promise<void>;
628 OpenWorkspaceInExternalOpenerForTab(tabID: string, id: string): Promise<void>; OpenLocalPathInExternalOpener(path: string, id: string): Promise<void>; SaveLocalPathAs(path: string): Promise<string>;
629 RevealWorkspacePathForTab(tabID: string, rel: string): Promise<void>;
630 RevealPresentedPathForTab(tabID: string, toolCallID: string, path: string): Promise<void>;
631 RevealReferencePathForTab(tabID: string, path: string): Promise<void>;
632 SaveWorkspacePathAsForTab(tabID: string, rel: string): Promise<string>;
633 SavePresentedPathAsForTab(tabID: string, toolCallID: string, path: string): Promise<string>;
634 SaveReferencePathAsForTab(tabID: string, path: string): Promise<string>;
635 SaveRemoteFileAs(hostID: string, remotePath: string): Promise<string>;
636 SaveRemotePresentedFileAs(tabID: string, hostID: string, toolCallID: string, remotePath: string): Promise<string>;
637 ResolveRemotePresentedPathForTab(tabID: string, hostID: string, toolCallID: string, remotePath: string): Promise<string>;
638 ResolveRemoteWorkspacePathForTab(tabID: string, hostID: string, toolCallID: string, remotePath: string): Promise<string>;
639 RevealPath(path: string): Promise<void>;
640 OpenLocalPath(path: string): Promise<void>;
641 SavePastedImage(dataUrl: string): Promise<string>;
642 SaveClipboardImage(): Promise<string>;
643 SavePastedFile(name: string, dataUrl: string): Promise<string>;
644 PickExportFile(defaultFilename: string, mimeType: string): Promise<string>;
645 ExportGoalDiagnostics(): Promise<string>;
646 SaveExportFile(path: string, payload: string, base64Encoded: boolean): Promise<void>;
647 SaveSessionMarkdownForTab(tabID: string, path: string, title: string): Promise<void>;
648 SaveExportImageFiles(path: string, payloads: string[]): Promise<void>;
649 AttachDropped(path: string): Promise<DroppedItem>;
650 AttachmentDataURL(path: string): Promise<string>;
651 Models(): Promise<ModelInfo[]>;
652 SetModel(name: string): Promise<void>;
653 ModelsForTab(tabID: string): Promise<ModelInfo[]>;
654 ModelsForDraft(draftID: string): Promise<ModelInfo[]>;
655 SetModelForTab(tabID: string, name: string): Promise<void>;
656 Effort(): Promise<EffortInfo>;
657 SetEffort(level: string): Promise<void>;
658 EffortForTab(tabID: string): Promise<EffortInfo>;
659 SetEffortForTab(tabID: string, level: string): Promise<void>;
660 // ReloadRuntime rebuilds the tab's agent runtime in place (tools, skills,
661 // commands, hooks, providers, MCP servers) via boot.Rebuild, keeping the
662 // session. Busy tabs queue one reload for when they go idle.
663 ReloadRuntime(tabID: string): Promise<void>;
664 Memory(): Promise<MemoryView>;
665 MemorySuggestions(): Promise<MemorySuggestionsView>;
666 AcceptMemorySuggestion(suggestion: MemorySuggestion): Promise<string>;
667 AcceptSkillSuggestion(suggestion: SkillSuggestion): Promise<string>;
668 MemoryForTab(tabID: string): Promise<MemoryView>;
669 MemoryRevisions(ref: string): Promise<MemoryFact[]>;
670 MemoryRevisionsForTab(tabID: string, ref: string): Promise<MemoryFact[]>;
671 RestoreMemoryRevision(ref: string, revision: number): Promise<MemoryFact>;
672 RestoreMemoryRevisionForTab(tabID: string, ref: string, revision: number): Promise<MemoryFact>;
673 MemorySuggestionsForTab(tabID: string): Promise<MemorySuggestionsView>;
674 AcceptMemorySuggestionForTab(tabID: string, suggestion: MemorySuggestion): Promise<string>;
675 AcceptSkillSuggestionForTab(tabID: string, suggestion: SkillSuggestion): Promise<string>;
676 Remember(scope: string, note: string): Promise<string>;
677 RememberForTab(tabID: string, scope: string, note: string): Promise<string>;
678 Forget(name: string): Promise<void>;
679 ForgetForTab(tabID: string, name: string): Promise<void>;
680 RestoreArchivedMemory(archivePath: string): Promise<MemoryFact>;
681 RestoreArchivedMemoryForTab(tabID: string, archivePath: string): Promise<MemoryFact>;
682 SaveDoc(path: string, body: string): Promise<string>;
683 SaveDocForTab(tabID: string, path: string, body: string): Promise<string>;
684 DesktopStartupSettings(): Promise<DesktopStartupSettingsView>;
685 HooksSettings(scope: string): Promise<HooksSettingsView>;
686 SaveHooksSettings(scope: string, hooks: HookConfigView[]): Promise<void>;
687 SaveHooksSettingsForRoot(scope: string, projectRoot: string, hooks: HookConfigView[]): Promise<void>;
688 TrustProjectHooks(): Promise<void>;
689 TrustProjectHooksForRoot(projectRoot: string): Promise<void>;
690 SetDefaultModel(ref: string): Promise<void>;
691 SetPlannerModel(ref: string): Promise<void>;
692 SetVisionModel(ref: string): Promise<void>;
693 SetWebSearchModel(ref: string): Promise<void>;
694 SetSubagentModel(ref: string): Promise<void>;
695 SetSubagentEffort(level: string): Promise<void>;
696 SetMaxSubagentDepth(depth: number): Promise<void>;
697 SetMaxSubagentConcurrency(n: number): Promise<void>;
698 SetMaxParallelWriters(n: number): Promise<void>;
699 SetAutoPlan(mode: string): Promise<void>;
700 SetDefaultToolApprovalMode(mode: string): Promise<void>;
701 SetDefaultAutoRecoveryCheckpoint(enabled: boolean): Promise<void>;
702 RenameProviderConnections(names: string[], displayName: string): Promise<void>;
703 SaveProvider(p: ProviderView): Promise<void>;
704 SetProviderWebSearch(names: string[], enabled: boolean): Promise<void>;
705 SaveProviderModelCatalogs(updates: ProviderModelCatalogUpdate[]): Promise<string[]>;
706 SaveProviderWithKey(p: ProviderView, key: string): Promise<string>;
707 AddOfficialProviderAccess(kind: string, key: string): Promise<string>;
708 UpgradeDeepSeekProviderAccess(name: string): Promise<string>;
709 AddProviderPresetAccess(id: string, key: string): Promise<string>;
710 ResetProviderPresetAccess(id: string): Promise<void>;
711 FetchProviderModels(p: ProviderView): Promise<string[]>;
712 FetchProviderModelCatalog(p: ProviderView): Promise<ProviderModelCapabilityView[]>;
713 FetchProviderModelCatalogDraft(p: ProviderView, key: string): Promise<ProviderModelCapabilityView[]>;
714 TestProviderModel(p: ProviderView, model: string, key: string): Promise<void>;
715 FetchAllProviderModelCatalogs(providers: ProviderView[]): Promise<Record<string, ProviderModelCapabilityView[]>>;
716 FetchAllProviderModels(providers: ProviderView[]): Promise<Record<string, string[]>>;
717 DeleteProvider(name: string): Promise<void>;
718 RemoveProviderAccess(name: string): Promise<void>;
719 RemoveProviderAccesses(names: string[]): Promise<void>;
720 SaveProviderKey(apiKeyEnv: string, value: string): Promise<string>;
721 SetConnectionKey(name: string, value: string): Promise<string>;
722 AddProviderConnectionWithOptions(presetID: string, sourceName: string, key: string, baseURL: string, kind: string): Promise<string>;
723 AddProviderConnectionWithURL(presetID: string, sourceName: string, key: string, baseURL: string): Promise<string>;
724 AddProviderConnection(presetID: string, sourceName: string, key: string): Promise<string>;
725 SetProviderKey(apiKeyEnv: string, value: string): Promise<string>;
726 ClearProviderKey(apiKeyEnv: string): Promise<void>;
727 SetPermissionMode(mode: string): Promise<void>;
728 AddPermissionRule(list: string, rule: string): Promise<void>;
729 RemovePermissionRule(list: string, rule: string): Promise<void>;
730 ReloadSettings(): Promise<void>;
731 SetShellPreference(prefer: string): Promise<void>;
732 InstallShellSupport(id: string): Promise<ShellInstallResult>;
733 CancelShellInstall(): Promise<void>;
734 SetSandbox(bash: string, network: boolean, workspaceRoot: string, allowWrite: string[], shell: string): Promise<void>;
735 SetNetwork(n: NetworkView): Promise<void>;
736 SetBotSettings(b: BotSettingsView): Promise<void>;
737 SetBotConnectionToolApprovalMode(connID: string, mode: string): Promise<void>;
738 SetBotDingtalkToolApprovalMode(mode: string): Promise<void>;
739 SetBotSecret(envName: string, value: string): Promise<void>;
740 ClearBotSecret(envName: string): Promise<void>;
741 StartBotConnectionInstall(provider: string, domain: string): Promise<BotInstallStartResult>;
742 PollBotConnectionInstall(installID: string): Promise<BotInstallPollResult>;
743 BotRuntimeStatus(): Promise<BotRuntimeStatusView>;
744 DiagnoseBotConnection(id: string): Promise<BotConnectionDiagnostic>;
745 TestBotConnection(id: string, target?: string): Promise<BotConnectionDiagnostic>;
746 TestDingtalkBot(): Promise<BotConnectionDiagnostic>;
747 SetCloseBehavior(mode: string): Promise<void>;
748 SetSessionExperience(mode: "standard" | "deep"): Promise<void>;
749 SetDisplayMode(mode: string): Promise<void>;
750 SetStatusBarStyle(style: string): Promise<void>;
751 SetStatusBarItems(items: string[]): Promise<void>; SetReasoningDisplayMode(mode: "hidden" | "summary" | "auto" | "expanded"): Promise<void>;
752 SetDesktopLanguage(lang: string): Promise<void>;
753 SetDesktopCurrency(currency: string): Promise<void>;
754 SetDesktopAppearance(theme: string, style: string): Promise<void>;
755 SetDesktopTerminalTheme(theme: string): Promise<void>;
756 ListThemePacks(): Promise<import("./themePack").ThemePackView[]>;
757 GetActiveThemePack(): Promise<import("./themePack").ThemeActiveView>;
758 GetThemeExperience(): Promise<import("./themeExperience").ThemeExperienceView>;
759 ActivateThemePack(id: string): Promise<void>;
760 ActivateBaseStyle(style: string): Promise<void>;
761 DisableThemePack(): Promise<void>;
762 RestoreGraphiteAppearance(): Promise<void>;
763 ResetThemePack(): Promise<void>;
764 SaveThemePack(input: import("./themePack").ThemeSaveInput): Promise<import("./themePack").ThemePackView>;
765 DeleteThemePack(id: string): Promise<void>;
766 CopyThemePack(sourceID: string, newID: string, newName: string): Promise<import("./themePack").ThemePackView>;
767 ImportThemePack(sourcePath: string, replace: boolean): Promise<import("./themePack").ThemeImportResult>;
768 ExportThemePack(id: string, destPath: string): Promise<string>;
769 PickThemeBackground(): Promise<string>;
770 SetDesktopLayoutStyle(style: string): Promise<void>;
771 SetDesktopZoomFactor(factor: number): Promise<void>;
772 GetDesktopZoomFactor(): Promise<number>;
773 RestartApplication(): Promise<void>;
774 ReportDesktopWebViewReady(): Promise<void>;
775 GetDesktopShellStatus(): Promise<DesktopShellStatusView>;
776 SetDesktopCheckUpdates(enabled: boolean): Promise<void>;
777 SetDesktopUpdateChannel(channel: string): Promise<void>;
778 SetDesktopTelemetry(enabled: boolean): Promise<void>;
779 SetDesktopMetrics(enabled: boolean): Promise<void>;
780 SetExpandThinking(on: boolean): Promise<void>;
781 SetDesktopConversationWidth(width: string): Promise<void>;
782 MigrateDesktopPreferences(language: string, theme: string, style: string): Promise<void>;
783 SetAgentParams(temperature: number, maxSteps: number, plannerMaxSteps: number, systemPrompt: string): Promise<void>;
784 SetCompactRatio(ratio: number): Promise<void>;
785 SetReasoningLanguage(lang: string): Promise<void>;
786 SetTrayLocale(locale: "en" | "zh" | "zh-TW"): Promise<void>;
787 // SetBypass is the legacy desktop name for YOLO/full-access tool auto-approval
788 // (ask questions and plan approvals still wait; deny rules still apply).
789 // Runtime-only.
790 SetBypass(on: boolean): Promise<void>;
791 Version(): Promise<string>;
792 CheckUpdate(channel: string): Promise<UpdateInfo | null>;
793 /** v1.20+ single-action update: download, verify, install, relaunch. */
794 ApplyUpdateRequest(channel: string, expectedVersion: string, requestId: string): Promise<void>;
795 /** Discard a stuck previous update transaction so the next install can proceed. */
796 AbandonPendingUpdate?(): Promise<void>;
797 OpenDownloadPage(): Promise<void>;
798 OpenUserConfigPath?(): Promise<void>;
799 ReloadUserConfig?(): Promise<{ configWarnings?: string[]; configWarningsRevision?: number; configPath?: string } | null>;
800 StorageSettings(): Promise<{ defaultWorkspace: string; statePath: string; cachePath: string; extensionsPath: string }>;
801 NeedsOnboarding(): Promise<boolean>;
802 ConnectKey(apiKey: string): Promise<string>;
803 // Crash overlay "Send report" (desktop/crash_app.go): scrubs user paths, attaches
804 // version/os/arch, POSTs to the collection endpoint. Only ever sent on user click.
805 ReportCrash(kind: string, detail: string): Promise<void>;
806 RecordUIPerf(signals: Record<string, string>): Promise<void>;
807 ListTabs(): Promise<TabMeta[]>;
808 OpenProjectTab(workspaceRoot: string, topicID: string): Promise<TabMeta>;
809 IsolatedWorktreeAvailability(workspaceRoot: string): Promise<DeliveryWorktreeAvailability>;
810 CreateIsolatedWorktree(workspaceRoot: string): Promise<DeliveryWorktreeOpenResult>;
811 InspectWorktreeMerge(tabID: string): Promise<WorktreeMergeInspection>;
812 GetWorktreeStatus(tabID: string): Promise<WorktreeMergeInspection>;
813 PrepareWorktreeMerge(tabID: string): Promise<WorktreeMergeInspection>;
814 MergeWorktreeBack(request: WorktreeMergeRequest): Promise<WorktreeMergeResult>;
815 RegisterNavigationIntent(token: string): Promise<void>;
816 CloseMergedWorktreeTab(request: CloseMergedWorktreeTabRequest): Promise<CloseMergedWorktreeTabResult>;
817 FinalizeWorktreeMerge(request: WorktreeCleanupRequest): Promise<WorktreeCleanupResult>;
818 // Deprecated one-version aliases kept bound for older desktop clients.
819 DeliveryWorktreeAvailability(workspaceRoot: string): Promise<DeliveryWorktreeAvailability>;
820 CreateDeliveryWorktree(workspaceRoot: string): Promise<DeliveryWorktreeOpenResult>;
821 SetAgentPreset(preset: string): Promise<void>;
822 SetAgentPresetForTab(tabID: string, preset: string): Promise<void>;
823 SetTokenMode(mode: string): Promise<void>;
824 SetTokenModeForTab(tabID: string, mode: string): Promise<void>;
825 OpenGlobalTab(topicID: string): Promise<TabMeta>;
826 OpenTopicSession(scope: string, workspaceRoot: string, topicID: string, sessionPath: string): Promise<TabMeta>;
827 EnsureBlankTab(scope: string, workspaceRoot: string): Promise<TabMeta>;
828 ActivateTopic(scope: string, workspaceRoot: string, topicID: string, sessionPath: string): Promise<TabMeta>;
829 // Two-phase ticketed topic activation (supersedes ActivateTopic for topic
830 // navigation): returns a ticket after the surface switch; completion lands
831 // on the "topic:activation" channel.
832 StartTopicActivation(req: TopicActivationRequest): Promise<TopicActivationTicket>;
833 EnsureBlankSurface(scope: string, workspaceRoot: string): Promise<TabMeta>;
834 SetActiveTab(tabID: string): Promise<void>;
835 ReorderTabs(tabIDs: string[]): Promise<void>;
836 CloseTab(tabID: string): Promise<void>;
837 TerminalWorkspaceForTab(tabID: string): Promise<TerminalWorkspaceView>;
838 TerminalOutputForTab(tabID: string, sessionID: string): Promise<string>;
839 CreateTerminalForTab(tabID: string, relativePath: string, shellID: string): Promise<TerminalSessionView>;
840 WriteTerminalForTab(tabID: string, sessionID: string, data: string): Promise<void>;
841 ResizeTerminalForTab(tabID: string, sessionID: string, cols: number, rows: number): Promise<void>;
842 CloseTerminalForTab(tabID: string, sessionID: string): Promise<void>;
843 RenameTerminalForTab(tabID: string, sessionID: string, title: string): Promise<void>;
844 ListProjectTree(): Promise<ProjectNode[]>;
845 RenameProject(workspaceRoot: string, title: string): Promise<void>;
846 SetProjectColor(workspaceRoot: string, color: string): Promise<void>;
847 SetProjectPinned(workspaceRoot: string, pinned: boolean): Promise<void>;
848 ReorderProjects(workspaceRoots: string[]): Promise<void>;
849 CreateTopic(scope: string, workspaceRoot: string, title: string): Promise<TopicMeta>;
850 RenameTopic(topicID: string, title: string): Promise<void>;
851 DeleteTopic(topicID: string): Promise<void>;
852 TrashTopic(topicID: string): Promise<void>;
853 SetTopicPinned(topicID: string, pinned: boolean): Promise<void>;
854 ContextPanel(tabID: string): Promise<ContextPanelInfo>;
855 // New native-feel bindings (added with the desktop native-feel plan).
856 ConfirmAction(req: NativeConfirmRequest): Promise<boolean>;
857 SaveWindowState(state: DesktopWindowState): Promise<void>;
858 // ── Remote (SSH) ──
859 RemoteHosts(): Promise<RemoteHostView[]>;
860 AddRemoteHost(input: RemoteHostInput): Promise<RemoteHostView>;
861 UpdateRemoteHost(id: string, input: RemoteHostInput): Promise<RemoteHostView>;
862 RemoveRemoteHost(id: string): Promise<void>;
863 ScanSSHConfig(): Promise<RemoteHostInput[]>;
864 ConnectRemoteHost(id: string): Promise<void>;
865 DisconnectRemoteHost(id: string): Promise<void>;
866 RemoteConnectionStatuses(): Promise<RemoteConnectionStatus[]>;
867 ConfirmRemoteHostKey(hostId: string, accept: boolean): Promise<void>;
868 ConfirmRemoteSecret(hostId: string, promptId: string, secret: string, accept: boolean): Promise<void>;
869 ListRemoteDir(hostId: string, path: string): Promise<RemoteDirEntry[]>;
870 ReadRemoteFile(hostId: string, path: string): Promise<RemoteFilePreview>;
871 WriteRemoteFile(hostId: string, path: string, body: string, expectMtimeUnix: number): Promise<RemoteWriteResult>;
872 MkdirRemote(hostId: string, path: string): Promise<void>;
873 RenameRemotePath(hostId: string, oldPath: string, newPath: string): Promise<void>;
874 DeleteRemotePath(hostId: string, path: string, recursive: boolean): Promise<void>;
875 RemoteForwards(hostId: string): Promise<RemoteForwardView[]>;
876 AddRemoteForward(hostId: string, input: RemoteForwardInput): Promise<RemoteForwardView>;
877 RemoveRemoteForward(hostId: string, forwardId: string): Promise<void>;
878 OpenRemoteWorkspace(hostId: string, workspace: string): Promise<void>;
879 PickRemoteIdentityFile(): Promise<string>;
880 CheckRemotePlatform(hostId: string): Promise<void>;
881 StopRemoteServer(hostId: string, workspace: string): Promise<void>;
882 RemoteServerStatus(hostId: string, workspace: string): Promise<RemoteServerView>;
883 RemoteServerLogs(hostId: string, workspace: string, tailLines: number): Promise<string>;
884 RemoteLastWorkspace(hostId: string): Promise<string>;
885 ScanRemoteLegacyWorkbenchData(): Promise<RemoteLegacyWorkbenchData>;
886 CleanRemoteLegacyWorkbenchData(target: "mirrors" | "trust"): Promise<void>;
887 }
888 // Compile-time drift check. Exclude<A, B> extracts keys in A that are missing
889 // from B. If that set is non-empty, AssertNever<non-never> fails with
890 // "Type 'X' does not satisfy the constraint 'never'".
891 // _CheckGenToApp errors mean a generated Go method has no TS counterpart.
892 // This compares method *names* only: the generated contract uses positional
893 // arg0/arg1 parameters and its own DTO interfaces, so full signature
894 // assignability would false-positive. Parameter drift is caught at the call
895 // sites by tsc when components invoke app.<method>(...).
896 type AssertNever<T extends never> = T;
897 export type _CheckGenToApp = AssertNever<Exclude<DesktopCommandName, keyof AppBindings>>;
898 // Must match desktop/app.go's eventChannel constant.
899 const EVENT_CHANNEL = "agent:event";
900
901 function hostEvents(name: string, cb: (...args: unknown[]) => void): (() => void) | null {
902 const host = desktopHost();
903 return host.kind === "none" ? null : host.events.on(name, cb);
904 }
905 type LazyMockAttachmentCommand = keyof AttachmentBindings
906 | "SavePastedImage"
907 | "SavePastedImageForComposerTarget"
908 | "SaveClipboardImage"
909 | "SaveClipboardImageForComposerTarget"
910 | "SavePastedFile"
911 | "SavePastedFileForComposerTarget"
912 | "AttachDropped"
913 | "AttachDroppedForComposerTarget"
914 | "AttachmentDataURL"
915 | "AttachmentDataURLForComposerTarget";
916 type MockAppBindings = Omit<AppBindings, LazyMockAttachmentCommand>;
917
918 let mockSingleton: AppBindings | null = null;
919 function getMock(): AppBindings {
920 if (!mockSingleton) mockSingleton = makeMockApp() as AppBindings;
921 return mockSingleton;
922 }
923
924 // onEvent subscribes to the agent's typed event stream; returns an unsubscribe.
925 export function onEvent(cb: (e: WireEvent) => void): () => void {
926 return hostEvents(EVENT_CHANNEL, (payload) => cb(payload as WireEvent)) ?? mockSubscribe(cb);
927 }
928
929 export interface TerminalOutputEvent {
930 id: string;
931 data: string;
932 }
933
934 export interface TerminalExitEvent {
935 id: string;
936 exitCode: number;
937 removed?: boolean;
938 }
939
940 function terminalEventPayload<T>(payload: unknown): T | null {
941 if (!payload || typeof payload !== "object") return null;
942 return payload as T;
943 }
944
945 export function onTerminalOutput(cb: (event: TerminalOutputEvent) => void): () => void {
946 const off = hostEvents("terminal:output", (payload) => {
947 const event = terminalEventPayload<TerminalOutputEvent>(payload);
948 if (event?.id && typeof event.data === "string") cb(event);
949 });
950 if (off) return off;
951 mockTerminalOutputListeners.add(cb);
952 return () => mockTerminalOutputListeners.delete(cb);
953 }
954
955 export function onTerminalExit(cb: (event: TerminalExitEvent) => void): () => void {
956 const off = hostEvents("terminal:exit", (payload) => {
957 const event = terminalEventPayload<TerminalExitEvent>(payload);
958 if (event?.id && typeof event.exitCode === "number") cb(event);
959 });
960 if (off) return off;
961 mockTerminalExitListeners.add(cb);
962 return () => mockTerminalExitListeners.delete(cb);
963 }
964
965 const mockTerminalOutputListeners = new Set<(event: TerminalOutputEvent) => void>();
966 const mockTerminalExitListeners = new Set<(event: TerminalExitEvent) => void>();
967
968 export function __emitMockTerminalOutput(event: TerminalOutputEvent): void {
969 mockTerminalOutputListeners.forEach((listener) => listener(event));
970 }
971
972 export function __emitMockTerminalExit(event: TerminalExitEvent): void {
973 mockTerminalExitListeners.forEach((listener) => listener(event));
974 }
975
976 // onUpdaterProgress subscribes to the auto-updater's progress events (a separate
977 // channel from the agent stream); returns an unsubscribe. Must match the event
978 // name emitted in desktop/updater_app.go.
979 export function onUpdaterProgress(cb: (p: UpdateProgress) => void): () => void {
980 const off = hostEvents("updater:progress", (p) => cb(p as UpdateProgress));
981 if (off) return off;
982 updaterListeners.add(cb);
983 return () => {
984 updaterListeners.delete(cb);
985 };
986 }
987
988 // onFilesDropped subscribes to native OS file drops landing on the composer's
989 // drop target; the callback gets the dropped files' absolute paths. No-op in
990 // the browser dev mock.
991 export function onFilesDropped(cb: (paths: string[]) => void): () => void {
992 return desktopHost().native.onFilesDropped(cb);
993 }
994
995 // onReady subscribes to the agent:ready event fired when boot.Build completes.
996 // The frontend re-fetches Meta/Context/History when this lands.
997 // onRuntimeRebuilt fires when a tab's controller is replaced in place
998 // (model/effort/token-mode switch, clear-while-running). The rebuilt
999 // controller restarts prompt ids, so per-tab id-keyed state must reset.
1000 export function onRuntimeRebuilt(cb: (tabId?: string, runtimeEpoch?: string) => void): () => void {
1001 return hostEvents("runtime:rebuilt", (tabId?: unknown, runtimeEpoch?: unknown) =>
1002 cb(
1003 typeof tabId === "string" ? tabId : undefined,
1004 typeof runtimeEpoch === "string" ? runtimeEpoch : undefined,
1005 )
1006 ) ?? (() => {});
1007 }
1008
1009 export function onReady(cb: (tabId?: string) => void): () => void {
1010 const off = hostEvents("agent:ready", (tabId?: unknown) => cb(typeof tabId === "string" ? tabId : undefined));
1011 if (off) return off;
1012 // The browser mock has no native event bridge, but SessionRef navigation
1013 // still needs the same reload semantics as the desktop host.
1014 mockReadyListeners.add(cb);
1015 cb();
1016 return () => mockReadyListeners.delete(cb);
1017 }
1018
1019 export function onProjectTreeChanged(cb: () => void): () => void {
1020 return hostEvents("project-tree:changed", (payload?: unknown) => (payload as { reason?: unknown } | undefined)?.reason !== "runtime" && (payload as { reason?: unknown } | undefined)?.reason !== "catalog-v2" && cb()) ?? subscribeMockProjectTreeChanged(cb);
1021 }
1022
1023 export function onLegacyEmptySessionCleanupChanged(cb: (status: LegacyEmptySessionCleanupStatus) => void): () => void {
1024 return hostEvents("legacy-empty-session-cleanup:changed", (payload?: unknown) => {
1025 if (payload && typeof payload === "object") cb(payload as LegacyEmptySessionCleanupStatus);
1026 }) ?? (() => {});
1027 }
1028
1029 // onTopicActivation subscribes to the "topic:activation" channel carrying the
1030 // lifecycle of ticketed StartTopicActivation requests (starting/ready/failed/
1031 // cancelled). Returns an unsubscribe.
1032 export function onTopicActivation(cb: (event: TopicActivationEvent) => void): () => void {
1033 const off = hostEvents("topic:activation", (payload?: unknown) => {
1034 if (payload && typeof payload === "object") cb(payload as TopicActivationEvent);
1035 });
1036 if (off) return off;
1037 mockTopicActivationListeners.add(cb);
1038 return () => mockTopicActivationListeners.delete(cb);
1039 }
1040
1041 // onTabMeta subscribes to the "tab:meta" channel: a full refreshed Meta pushed
1042 // after the backend recomputes the expensive MetaForTab fields (git branch,
1043 // image-input capability) in the background.
1044 export function onTabMeta(cb: (event: TabMetaRefreshEvent) => void): () => void {
1045 const off = hostEvents("tab:meta", (payload?: unknown) => {
1046 if (payload && typeof payload === "object") cb(payload as TabMetaRefreshEvent);
1047 });
1048 if (off) return off;
1049 mockTabMetaListeners.add(cb);
1050 return () => mockTabMetaListeners.delete(cb);
1051 }
1052
1053 const mockTopicActivationListeners = new Set<(event: TopicActivationEvent) => void>();
1054 const mockTabMetaListeners = new Set<(event: TabMetaRefreshEvent) => void>();
1055 const mockReadyListeners = new Set<(tabId?: string) => void>();
1056
1057 function emitMockReady(tabId?: string): void {
1058 mockReadyListeners.forEach((listener) => listener(tabId));
1059 }
1060
1061 export function __emitMockTopicActivation(event: TopicActivationEvent): void {
1062 mockTopicActivationListeners.forEach((listener) => listener(event));
1063 }
1064
1065 export function __emitMockTabMeta(event: TabMetaRefreshEvent): void {
1066 mockTabMetaListeners.forEach((listener) => listener(event));
1067 }
1068
1069 export function onSessionRecovered(cb: (payload: SessionRecoveryEvent) => void): () => void {
1070 return hostEvents("session:recovered", (payload?: unknown) => cb((payload ?? {}) as SessionRecoveryEvent)) ?? (() => {});
1071 }
1072
1073 export function onSessionActiveVersionChanged(cb: (payload: SessionRecoveryEvent) => void): () => void {
1074 return hostEvents("session:active-version-changed", (payload?: unknown) => cb((payload ?? {}) as SessionRecoveryEvent)) ?? (() => {});
1075 }
1076
1077 export function onSessionRecoveryFailed(cb: (payload: SessionRecoveryFailedEvent) => void): () => void {
1078 return hostEvents("session:recovery-failed", (payload?: unknown) => cb((payload ?? {}) as SessionRecoveryFailedEvent)) ?? (() => {});
1079 }
1080
1081 export function onRemoteStatus(cb: (s: RemoteConnectionStatus) => void): () => void {
1082 return hostEvents("remote:status", (payload?: unknown) => cb((payload ?? {}) as RemoteConnectionStatus))
1083 ?? registerMockRemoteListener("status", cb as (v: unknown) => void);
1084 }
1085
1086 export function onRemoteForwards(cb: (e: RemoteForwardsEvent) => void): () => void {
1087 return hostEvents("remote:forwards", (payload?: unknown) => cb((payload ?? {}) as RemoteForwardsEvent))
1088 ?? registerMockRemoteListener("forwards", cb as (v: unknown) => void);
1089 }
1090
1091 export function onRemoteServer(cb: (s: RemoteServerView) => void): () => void {
1092 return hostEvents("remote:server", (payload?: unknown) => cb((payload ?? {}) as RemoteServerView))
1093 ?? registerMockRemoteListener("server", cb as (v: unknown) => void);
1094 }
1095
1096 // Mock event fan-out so browser-dev and tsx tests can drive remote:* events
1097 // without a shell runtime.
1098 type MockRemoteChannel = "status" | "forwards" | "server";
1099 const mockRemoteListeners: Record<MockRemoteChannel, Set<(v: unknown) => void>> = {
1100 status: new Set(),
1101 forwards: new Set(),
1102 server: new Set(),
1103 };
1104 function registerMockRemoteListener(ch: MockRemoteChannel, cb: (v: unknown) => void): () => void {
1105 mockRemoteListeners[ch].add(cb);
1106 return () => mockRemoteListeners[ch].delete(cb);
1107 }
1108 export function __emitMockRemote(ch: MockRemoteChannel, payload: unknown): void {
1109 for (const cb of mockRemoteListeners[ch]) cb(payload);
1110 }
1111
1112 // app proxies each call to the live binding (or the dev mock only when truly
1113 // outside the shell), resolving the host at call time so a shell that attaches
1114 // late is picked up transparently.
1115 function bridgeBreadcrumb(method: string): string {
1116 if (method === "ReportCrash" || method === "RecordUIPerf") return "";
1117 if (/^(Submit|SubmitDisplay|RunShell|Steer|Cancel|Approve|AnswerQuestion|ReplayPendingPrompts)/.test(method))
1118 return `turn ${method}`;
1119 if (/^(SetModel|SetEffort|SetDefaultModel|SetPlannerModel|SetVisionModel|SetWebSearchModel|SetSubagentModel|SetSubagentEffort|SetMaxSubagentDepth|SetMaxSubagentConcurrency|SetMaxParallelWriters)/.test(method))
1120 return `model ${method}`;
1121 if (/^(SetDesktop|SetCloseBehavior|SetDisplayMode|SetStatusBar|SetReasoningDisplayMode|SetExpandThinking|SetAutoPlan|SetDefaultToolApprovalMode|SetCompactRatio|SetReasoningLanguage)/.test(method))
1122 return `settings ${method}`;
1123 if (/^(SetConnectionKey|AddProviderConnection|RenameProviderConnections|SaveProvider|SetProviderWebSearch|SaveProviderModelCatalogs|AddOfficialProviderAccess|UpgradeDeepSeekProviderAccess|AddProviderPresetAccess|ResetProviderPresetAccess|RemoveProviderAccess|RemoveProviderAccesses|DeleteProvider|SaveProviderKey|SetProviderKey|ClearProviderKey|TestProviderModel|FetchProviderModelCatalog|FetchAllProviderModelCatalogs|FetchProviderModels|FetchAllProviderModels|ConnectKey)/.test(method))
1124 return `provider ${method}`;
1125 if (/^(CheckUpdate|ApplyUpdateRequest|OpenDownloadPage|OpenUserConfigPath|ReloadUserConfig)/.test(method)) return `update ${method}`;
1126 if (/^(AddMCPServer|InstallMCPServer|UpdateMCPServer|RemoveMCPServer|AuthorizeAndConnectMCPServer|AuthenticateMCPServer|ReconnectMCPServer|ClearMCPServerAuthentication|SetMCPServer)/.test(method))
1127 return `mcp ${method}`;
1128 if (/^(AddSkillPath|RemoveSkillPath|SetSkillPathEnabled|RefreshSkills|SetSkillEnabled|SetSkillImplicitInvocation|AcceptSkillSuggestion|AvailableSubagentTools|CreateSubagentProfile|UpdateSubagentProfile|DeleteSubagentProfile|SetSubagentProfileModel|SetSubagentProfileEffort|TrySubagentProfile|CancelTrySubagentProfile)/.test(method))
1129 return `skill ${method}`;
1130 if (/^(MinimiseMainWindow|ToggleMaximiseMainWindow|IsMainWindowMaximised|CloseMainWindow)$/.test(method)) return `window ${method}`;
1131 if (/^(OpenProjectTab|OpenGlobalTab|OpenTopicSession|EnsureBlankTab|ActivateTopic|StartTopicActivation|EnsureBlankSurface|SetActiveTab|CloseTab|RegisterNavigationIntent|CloseMergedWorktreeTab|ReorderTabs|CreateTopic|RenameTopic|DeleteTopic|TrashTopic|RenameProject|RemoveWorkspace|SwitchWorkspace|PickWorkspace|IsolatedWorktreeAvailability|CreateIsolatedWorktree|InspectWorktreeMerge|GetWorktreeStatus|PrepareWorktreeMerge|ReconcileRecoveryVersions|MergeWorktreeBack|FinalizeWorktreeMerge|DeliveryWorktreeAvailability|CreateDeliveryWorktree)/.test(method))
1132 return `nav ${method}`;
1133 return "";
1134 }
1135
1136 function elapsedMs(startedAt: number): number {
1137 const now = typeof performance !== "undefined" ? performance.now() : Date.now();
1138 return Math.max(0, Math.round(now - startedAt));
1139 }
1140
1141 export const app: AppBindings = new Proxy({} as AppBindings, {
1142 get(_t, prop) {
1143 const host = desktopHost().app, target = host ?? getMock();
1144 let v = (target as unknown as Record<string, unknown>)[String(prop)];
1145 if (!host && v === undefined && typeof prop === "string") v = (...args: unknown[]) => import("./attachmentBindings").then(
1146 module => module.callMockAttachment(target, prop as keyof AttachmentBindings, args));
1147 if (typeof v !== "function") return v;
1148 return (...args: unknown[]) => {
1149 const method = String(prop), crumb = bridgeBreadcrumb(method);
1150 const startedAt = crumb ? (typeof performance !== "undefined" ? performance.now() : Date.now()) : 0;
1151 if (crumb) addBreadcrumb("bridge", crumb);
1152 try {
1153 const result = maybeShare(method, args, () => (v as (...a: unknown[]) => unknown).apply(target, args));
1154 if (result && typeof (result as Promise<unknown>).then === "function") {
1155 return (result as Promise<unknown>).then(
1156 (value) => {
1157 if (crumb) addBreadcrumb("bridge", `${crumb} done ms=${elapsedMs(startedAt)}`);
1158 return value;
1159 },
1160 (err) => {
1161 if (crumb) addBreadcrumb("bridge.error", `${method} ms=${elapsedMs(startedAt)}`);
1162 throw err;
1163 },
1164 );
1165 }
1166 if (crumb) addBreadcrumb("bridge", `${crumb} done ms=${elapsedMs(startedAt)}`);
1167 return result;
1168 } catch (err) {
1169 if (crumb) addBreadcrumb("bridge.error", `${method} ms=${elapsedMs(startedAt)}`);
1170 throw err;
1171 }
1172 };
1173 },
1174 });
1175
1176 // openExternal opens a URL in the system browser (so links in rendered markdown
1177 // don't navigate the webview away from the app). Falls back to window.open in the
1178 // browser dev mock.
1179 export function openExternal(url: string): void {
1180 desktopHost().native.openExternal(url);
1181 }
1182
1183 // --- browser dev mock --------------------------------------------------------
1184
1185 const listeners = new Set<(e: WireEvent) => void>();
1186 let mockScopedTabId: string | undefined;
1187 let mockPendingTopicActivation: { requestId: string; tabId: string } | undefined;
1188 let mockTopicActivationCounter = 0;
1189
1190 function mockSubscribe(cb: (e: WireEvent) => void): () => void {
1191 listeners.add(cb);
1192 return () => {
1193 listeners.delete(cb);
1194 };
1195 }
1196
1197 function emit(e: WireEvent) {
1198 const event = mockScopedTabId && !e.tabId ? { ...e, tabId: mockScopedTabId } : e;
1199 publishMockTranscriptEvent(event);
1200 listeners.forEach((l) => l(event));
1201 }
1202
1203 export function mockToolApprovalModeAfterModeChange(current: string | undefined, nextMode: Mode): ToolApprovalMode {
1204 if (modeHasAutoApproveTools(nextMode)) return "workspace-write";
1205 return normalizeToolApprovalMode(current);
1206 }
1207
1208 async function withMockTabScope<T>(tabId: string, fn: () => Promise<T>): Promise<T> {
1209 const previous = mockScopedTabId;
1210 mockScopedTabId = tabId || previous;
1211 try {
1212 return await fn();
1213 } finally {
1214 mockScopedTabId = previous;
1215 }
1216 }
1217
1218 // Updater progress has its own listener set so the browser dev mock can stream a
1219 // fake download/install flow through onUpdaterProgress.
1220 const updaterListeners = new Set<(p: UpdateProgress) => void>();
1221
1222 function emitUpdater(p: UpdateProgress) {
1223 updaterListeners.forEach((l) => l(p));
1224 }
1225
1226 // Test seam for the browser-dev updater state machine. The desktop shell
1227 // receives the same payloads through the host event stream.
1228 export function __emitMockUpdater(p: UpdateProgress): void {
1229 emitUpdater(p);
1230 }
1231
1232 function delay(ms: number): Promise<void> {
1233 return new Promise((r) => setTimeout(r, ms));
1234 }
1235
1236 function baseName(path: string): string {
1237 return path.replace(/[/\\]+$/, "").split(/[/\\]/).filter(Boolean).pop() ?? path;
1238 }
1239
1240 function browserPlatformOverride(): "darwin" | "windows" | "linux" | "" {
1241 if (typeof window === "undefined" || desktopHost().kind !== "none") return "";
1242 const value = new URLSearchParams(window.location.search).get("platform");
1243 return value === "darwin" || value === "windows" || value === "linux" ? value : "";
1244 }
1245
1246 function browserPreviewBashSandboxMode(): "enforce" | "off" {
1247 return browserPlatformOverride() === "windows" ? "off" : "enforce";
1248 }
1249
1250 function browserPreviewEffectiveShell(prefer = "auto"): "bash" | "git-bash" | "powershell" | "pwsh" {
1251 const normalized = prefer.trim().toLowerCase();
1252 if (normalized === "powershell" || normalized === "pwsh") return normalized;
1253 return browserPlatformOverride() === "windows" ? (normalized === "bash" ? "git-bash" : "pwsh") : "bash";
1254 }
1255
1256 function mockScenario(): "demo" | "fresh" | "running" | "guidance" | "recovery" | "sandbox_escape" | "notice" | "deepseek_upgrade" | "bench" {
1257 if (typeof window === "undefined") return "demo";
1258 const value = new URLSearchParams(window.location.search).get("mock")?.trim().toLowerCase();
1259 if (value === "fresh" || value === "empty" || value === "first-run") return "fresh";
1260 if (typeof import.meta.env !== "undefined" && import.meta.env.DEV && (value === "recovery" || value === "inbox-recovery")) return "recovery"; if (value === "guidance" || value === "guide" || value === "steer") return "guidance";
1261 if (value === "running" || value === "busy" || value === "streaming") return "running";
1262 if (value === "sandbox_escape" || value === "sandbox-escape" || value === "sandboxescape") return "sandbox_escape";
1263 if (value === "notice" || value === "notices" || value === "notice-preview") return "notice";
1264 if (value === "deepseek_upgrade" || value === "deepseek-upgrade") return "deepseek_upgrade";
1265 if (value === "bench" || value === "benchmark" || value === "perf") return "bench";
1266 return "demo";
1267 }
1268
1269 const mockProviderPresetTemplates: MockProviderPresetTemplate[] = [
1270 mockBundlePreset(
1271 "opencode-go-recommended",
1272 "OpenCode Go (Recommended)",
1273 "One key, three safe routes: Chat, Anthropic Messages, and Responses. Defaults use a low-cost effort and a 32K output cap.",
1274 "OPENCODE_GO_API_KEY",
1275 [
1276 mockProviderTemplate({ name: "opencode-go", kind: "openai", baseUrl: "https://opencode.ai/zen/go/v1", models: mockOpenCodeGoModels, visionModels: ["kimi-k3"], default: "glm-5.3", apiKeyEnv: "OPENCODE_GO_API_KEY", contextWindow: 128000, modelOverrides: [{ model: "glm-5.3", reasoningProtocol: "openai", supportedEfforts: ["low", "high", "max"], defaultEffort: "low", maxOutputTokens: 32768 }, { model: "kimi-k3", reasoningProtocol: "openai", supportedEfforts: ["high", "max"], defaultEffort: "max", contextWindow: 1048576, maxOutputTokens: 32768 }] }),
1277 mockProviderTemplate({ name: "opencode-go-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen/go", models: ["qwen3.8-max", "qwen3.7-plus", "qwen3.7-max", "qwen3.6-plus", "minimax-m3", "minimax-m2.7", "minimax-m2.5"], visionModels: ["qwen3.8-max", "qwen3.7-plus", "qwen3.6-plus"], default: "qwen3.7-plus", apiKeyEnv: "OPENCODE_GO_API_KEY", thinking: "adaptive", contextWindow: 262144 }),
1278 mockProviderTemplate({ name: "opencode-go-responses", kind: "responses", baseUrl: "https://opencode.ai/zen/go/v1", models: ["grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor"], visionModels: ["grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor"], default: "grok-4.5", apiKeyEnv: "OPENCODE_GO_API_KEY", contextWindow: 500000, supportedEfforts: ["low", "medium", "high"], defaultEffort: "low" }),
1279 ],
1280 { recommended: true, billingMode: "subscription_equivalent", displayGroup: "opencode", displaySection: "go", displayTier: "primary", routeKind: "bundle", displayOrder: 0 },
1281 ),
1282 mockPreset("deepseek-responses", "DeepSeek Official Responses API", "Official stateless DeepSeek Responses API for Flash and Pro with server-side web search.", "DEEPSEEK_API_KEY", mockProviderTemplate({ name: "deepseek-responses", kind: "responses", baseUrl: "https://api.deepseek.com", models: ["deepseek-v4-flash", "deepseek-v4-pro"], default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", balanceUrl: "https://api.deepseek.com/user/balance", webSearch: true, serverWebSearchCapability: 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" }] })),
1283 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" })),
1284 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" })),
1285 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 })),
1286 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 }] })),
1287 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 }] })),
1288 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 })),
1289 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 })),
1290 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 })),
1291 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 })),
1292 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 })),
1293 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 })),
1294 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 })),
1295 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 })),
1296 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 })),
1297 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" })),
1298 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" })),
1299 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" })),
1300 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" })),
1301 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" })),
1302 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" })),
1303 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" })),
1304 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 })),
1305 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" })),
1306 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 })),
1307 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: "glm-5.3", reasoningProtocol: "openai", supportedEfforts: ["low", "high", "max"], defaultEffort: "high" }, { model: "glm-5.2", reasoningProtocol: "openai", supportedEfforts: ["high", "max"], defaultEffort: "high" }, { model: "kimi-k3", reasoningProtocol: "openai", supportedEfforts: ["high", "max"], defaultEffort: "max", contextWindow: 1048576 }, { model: "hy3", reasoningProtocol: "openai", supportedEfforts: ["none", "low", "high"], defaultEffort: "high" }] }), { displayGroup: "opencode", displaySection: "go", displayTier: "compatibility", routeKind: "chat", displayOrder: 90 }),
1308 mockPreset("opencode-go-anthropic", "OpenCode Go Anthropic", "OpenCode Go subscription Anthropic-compatible route for Qwen and MiniMax.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen/go", models: ["qwen3.8-max", "qwen3.7-plus", "qwen3.7-max", "qwen3.6-plus", "minimax-m3", "minimax-m2.7", "minimax-m2.5"], visionModels: ["qwen3.8-max", "qwen3.7-plus", "qwen3.6-plus"], default: "qwen3.7-plus", apiKeyEnv: "OPENCODE_GO_API_KEY", thinking: "adaptive", contextWindow: 262144 }), { displayGroup: "opencode", displaySection: "go", displayTier: "advanced", routeKind: "anthropic", displayOrder: 20 }),
1309 mockPreset("opencode-go-responses", "OpenCode Go Responses", "OpenCode Go Responses API route for Grok 4.5, GPT 5.6 Luna, and Muse Spark.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-responses", kind: "responses", baseUrl: "https://opencode.ai/zen/go/v1", models: ["grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor"], visionModels: ["grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor"], default: "grok-4.5", apiKeyEnv: "OPENCODE_GO_API_KEY", contextWindow: 500000, supportedEfforts: ["low", "medium", "high"], defaultEffort: "high", modelOverrides: [{ model: "grok-4.5", reasoningProtocol: "", supportedEfforts: ["low", "medium", "high"], defaultEffort: "high", contextWindow: 500000 }, { model: "gpt-5.6-luna", reasoningProtocol: "", supportedEfforts: ["none", "low", "medium", "high", "xhigh", "max"], defaultEffort: "high", contextWindow: 1050000 }, { model: "muse-spark-1.2-contributor", reasoningProtocol: "", supportedEfforts: ["minimal", "low", "medium", "high", "xhigh"], defaultEffort: "high", contextWindow: 1048576 }] }), { displayGroup: "opencode", displaySection: "go", displayTier: "advanced", routeKind: "responses", displayOrder: 30 }),
1310 mockPreset("opencode-go-deepseek-anthropic", "OpenCode Go DeepSeek Anthropic", "OpenCode Go Anthropic Messages route for DeepSeek Flash with server-side web search.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-deepseek-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen/go", models: ["deepseek-v4-flash"], default: "deepseek-v4-flash", apiKeyEnv: "OPENCODE_GO_API_KEY", thinking: "adaptive", webSearch: true, serverWebSearchCapability: true, contextWindow: 262144, supportedEfforts: ["disabled", "high", "max"], defaultEffort: "high" }), { displayGroup: "opencode", displaySection: "go", displayTier: "advanced", routeKind: "search-anthropic", optional: true, displayOrder: 40 }),
1311 mockPreset("opencode-go-deepseek-responses", "OpenCode Go DeepSeek Responses", "OpenCode Go stateless Responses API route for DeepSeek Flash with server-side web search.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-deepseek-responses", kind: "responses", baseUrl: "https://opencode.ai/zen/go/v1", models: ["deepseek-v4-flash"], default: "deepseek-v4-flash", apiKeyEnv: "OPENCODE_GO_API_KEY", webSearch: true, serverWebSearchCapability: true, contextWindow: 1000000, supportedEfforts: ["disabled", "high", "max"], defaultEffort: "high" }), { displayGroup: "opencode", displaySection: "go", displayTier: "advanced", routeKind: "search-responses", optional: true, displayOrder: 50 }),
1312 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: ["claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5", "qwen3.6-plus", "qwen3.5-plus", "qwen3.6-plus-free"], visionModels: ["claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5"], default: "claude-sonnet-4-6", apiKeyEnv: "OPENCODE_API_KEY", contextWindow: 262144 }), { displayGroup: "opencode", displaySection: "zen", displayTier: "primary", routeKind: "zen-anthropic", displayOrder: 100 }),
1313 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" })),
1314 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" })),
1315 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" })),
1316 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" })),
1317 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" })),
1318 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" })),
1319 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" })),
1320 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" })),
1321 mockPreset("stepfun-responses", "StepFun Responses API", "StepFun Responses API endpoint with reasoning effort and tool calls (step-3.7-flash).", "STEPFUN_API_KEY", mockProviderTemplate({ name: "stepfun-responses", kind: "responses", baseUrl: "https://api.stepfun.com/v1", models: ["step-3.7-flash"], default: "step-3.7-flash", apiKeyEnv: "STEPFUN_API_KEY", supportedEfforts: ["low", "medium", "high"], defaultEffort: "medium" })),
1322 mockPreset("stepfun-api", "StepFun API Pay-as-you-go", "StepFun pay-as-you-go OpenAI-compatible API with vision on step-3.7-flash.", "STEPFUN_API_KEY", mockProviderTemplate({ name: "stepfun-api", kind: "openai", baseUrl: "https://api.stepfun.com/v1", models: mockStepFunModels, visionModels: ["step-3.7-flash"], default: "step-3.7-flash", apiKeyEnv: "STEPFUN_API_KEY", supportedEfforts: ["low", "medium", "high"], defaultEffort: "medium" })),
1323 mockPreset("stepfun-api-anthropic", "StepFun API Anthropic Pay-as-you-go", "StepFun pay-as-you-go Anthropic-compatible Messages API with automatic prefix caching.", "STEPFUN_API_KEY", mockProviderTemplate({ name: "stepfun-api-anthropic", kind: "anthropic", baseUrl: "https://api.stepfun.com", models: mockStepFunModels, default: "step-3.7-flash", apiKeyEnv: "STEPFUN_API_KEY", thinking: "adaptive", supportedEfforts: ["low", "medium", "high"], defaultEffort: "medium" })),
1324 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" })),
1325 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" } })),
1326 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 })),
1327 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" })),
1328 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" })),
1329 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" })),
1330 createMockModelScopePreset(mockProviderTemplate, mockPreset),
1331 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" })),
1332 mockPreset("scnet", "SCNet", "SCNet (National Supercomputing Internet) OpenAI-compatible token-plan API.", "SCNET_API_KEY", mockProviderTemplate({ name: "scnet", kind: "openai", baseUrl: "https://api.scnet.cn/api/llm/v1", modelsUrl: "https://api.scnet.cn/api/llm/v1/models", models: ["GLM-5.2", "GLM-5", "GLM-5.1", "Kimi-K3", "Kimi-K2.7-Code", "Kimi-K2.6", "Kimi-K2.5", "DeepSeek-V4-Flash", "DeepSeek-V3.2", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "MiMo-V2.5-Pro"], visionModels: ["Kimi-K2.6", "Kimi-K2.5"], default: "MiniMax-M2.5", apiKeyEnv: "SCNET_API_KEY", modelOverrides: [{ model: "DeepSeek-V4-Flash", reasoningProtocol: "openai", supportedEfforts: ["high", "max"], defaultEffort: "high" }] })),
1333 mockPreset("scnet-anthropic", "SCNet Anthropic", "SCNet (National Supercomputing Internet) Anthropic-compatible token-plan endpoint with Bearer auth.", "SCNET_API_KEY", mockProviderTemplate({ name: "scnet-anthropic", kind: "anthropic", baseUrl: "https://api.scnet.cn/api/llm/anthropic", models: ["GLM-5.2", "GLM-5", "GLM-5.1", "Kimi-K3", "Kimi-K2.7-Code", "Kimi-K2.6", "Kimi-K2.5", "DeepSeek-V4-Flash", "DeepSeek-V3.2", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "MiMo-V2.5-Pro"], visionModels: ["Kimi-K2.6", "Kimi-K2.5"], default: "MiniMax-M2.5", apiKeyEnv: "SCNET_API_KEY", authHeader: true })),
1334 ];
1335
1336 let mockCatalogLoad: Promise<void> | undefined;
1337 let mockCatalogs: Record<string, NonNullable<ProviderPresetView["catalog"]>> = {};
1338 function loadMockProviderCatalog(): Promise<void> {
1339 return mockCatalogLoad ??= import("./providerCatalog.generated.json").then(({default: data}) => {
1340 mockCatalogs = data.catalogs;
1341 for (const template of data.templates) {
1342 if (!mockProviderPresetTemplates.some(p => p.id === template.id)) {
1343 mockProviderPresetTemplates.push(mockPreset(template.id, template.label, template.description, template.keyEnv, mockProviderTemplate(template.provider)));
1344 }
1345 }
1346 });
1347 }
1348
1349 function mockProviderPresetViews(): ProviderPresetView[] {
1350 return [...mockProviderPresetTemplates].sort((a, b) => mockProviderPresetDisplayRank(a.id) - mockProviderPresetDisplayRank(b.id)).map((template) => ({
1351 catalog: mockCatalogs[template.id],
1352 id: template.id,
1353 label: template.label,
1354 description: template.description,
1355 keyEnv: template.keyEnv,
1356 recommended: Boolean(template.recommended),
1357 billingMode: template.billingMode,
1358 displayGroup: template.displayGroup,
1359 displaySection: template.displaySection,
1360 displayTier: template.displayTier,
1361 routeKind: template.routeKind,
1362 optional: Boolean(template.optional),
1363 displayOrder: template.displayOrder,
1364 providerNames: [...(template.providers ?? [template.provider]).map((provider) => provider.name)],
1365 models: [...(template.providers ?? [template.provider]).flatMap((provider) => provider.models)],
1366 added: false,
1367 status: "available",
1368 statusProviderNames: [],
1369 keySet: false,
1370 requiresKey: true,
1371 configured: false,
1372 }));
1373 }
1374
1375 function mockProviderPresetDisplayRank(id: string): number {
1376 if (id === "opencode-go-recommended") return -3;
1377 if (id === "deepseek-responses") return -2;
1378 if (id === "glm-cn" || id === "zai-global" || id.startsWith("glm-coding-plan-") || id.startsWith("zai-coding-plan-")) return 0;
1379 if (id.startsWith("longcat-")) return 1;
1380 if (id === "token-rhythm") return 1;
1381 if (id.startsWith("kimi-")) return 2;
1382 if (id.startsWith("minimax-")) return 3;
1383 return 4;
1384 }
1385
1386 function cloneMockProviderTemplates(id: string, key: string): ProviderView[] | undefined {
1387 const template = mockProviderPresetTemplates.find((candidate) => candidate.id === id);
1388 if (!template) return undefined;
1389 return (template.providers ?? [template.provider]).map((provider) => ({
1390 ...JSON.parse(JSON.stringify(provider)) as ProviderView,
1391 keySet: Boolean(key.trim()),
1392 }));
1393 }
1394
1395 function mockExternalOpenerIconDataURL(color: string, label: string): string {
1396 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>`;
1397 return `data:image/svg+xml,${encodeURIComponent(svg)}`;
1398 }
1399 function makeMockApp(): MockAppBindings {
1400 const credentialDiagnostics = (actions: string[] = []): CredentialDiagnosticReport => ({
1401 home: "/mock/.reasonix",
1402 credentialPath: "/mock/.reasonix/.env",
1403 pendingTransactions: 0,
1404 checks: [],
1405 actions,
1406 });
1407 const scenario = mockScenario();
1408 // Both bridge families publish into the same catalog, as ListTabs does in
1409 // the desktop backend. A remote event is not a second source of tab state.
1410 const remoteProjects = createMockRemoteProjects({
1411 get: id => { const tab = mockTabs.find(item => item.id === id); return tab && { ...tab }; },
1412 publish: tab => {
1413 const existing = mockTabs.some(item => item.id === tab.id);
1414 mockTabs = mockTabs.map(item => item.id === tab.id ? { ...tab } : tab.active ? { ...item, active: false } : item);
1415 if (!existing) mockTabs.push({ ...tab });
1416 },
1417 remove: id => {
1418 if (!mockTabs.some(tab => tab.id === id)) return;
1419 if (mockTabs.length === 1) throw new Error("cannot close the last tab");
1420 const index = mockTabs.findIndex(tab => tab.id === id), active = mockTabs[index].active;
1421 mockTabs = mockTabs.filter(tab => tab.id !== id);
1422 if (active) setMockActiveTab(mockTabs[Math.min(index, mockTabs.length - 1)].id);
1423 },
1424 });
1425 const freshMock = scenario === "fresh";
1426 const guidanceMock = scenario === "guidance", recoveryMock = typeof import.meta.env !== "undefined" && import.meta.env.DEV && scenario === "recovery";
1427 const runningMock = scenario === "running" || guidanceMock;
1428 const sandboxEscapeMock = scenario === "sandbox_escape";
1429 const noticePreviewMock = scenario === "notice";
1430 const deepSeekUpgradeMock = scenario === "deepseek_upgrade";
1431 const benchMock = scenario === "bench";
1432 const mockDrafts = new Map<string, SessionDraftView>();
1433 const mockDraftOperations = new Map<string, SessionDraftSubmissionView>();
1434 const mockDraftForTarget = (scope: string, workspaceRoot: string): SessionDraftView => {
1435 const normalizedScope = scope === "project" && workspaceRoot ? "project" : "global";
1436 const normalizedRoot = normalizedScope === "project" ? workspaceRoot : "";
1437 const workspaceId = normalizedScope === "global" ? "global" : `project-${normalizedRoot}`;
1438 const prior = [...mockDrafts.values()].find((draft) => draft.workspaceId === workspaceId && draft.status === "active");
1439 if (prior) return prior;
1440 const now = Date.now();
1441 const draft: SessionDraftView = {
1442 id: `draft-mock-${mockDrafts.size + 1}`,
1443 workspaceId,
1444 scope: normalizedScope,
1445 workspaceRoot: normalizedRoot,
1446 revision: 1,
1447 contentJson: "{}",
1448 settings: { model: "deepseek/deepseek-v4-flash", modelSource: "default", mode: "default", toolApprovalMode: "ask", disabledMcp: {}, mcpOrder: [] },
1449 status: "active",
1450 updatedAt: now,
1451 };
1452 mockDrafts.set(draft.id, draft);
1453 return draft;
1454 };
1455 let cancelled = false;
1456 let pendingAskPreview = false, pendingApprovalPreview = false;
1457 // Mirrors the last emitted approval preview so mode switches can mirror the
1458 // backend drain contract: only non-fresh tools auto-allow; plan/sandbox
1459 // escape prompts stay pending and visible.
1460 let pendingApprovalPreviewPrompt: { id: string; tool: string } | undefined;
1461 const globalWorkspaceRoot = "~/Library/Application Support/reasonix/global-workspace";
1462 let cwd = freshMock ? globalWorkspaceRoot : "~/projects/joyquant-db"; // mutable so PickWorkspace is visible in dev
1463 let workspaces = freshMock ? [] : ["~/projects/joyquant-db", "~/projects/joyquant-sys", "~/projects/reasonix", "~/projects/blade"];
1464 let mockEffort = "auto";
1465 let mockDesktopZoomFactor = 1.0;
1466 let mockActiveThemeId = "";
1467 let mockBaseStyle = "graphite";
1468 let mockThemeMode: "auto" | "light" | "dark" = "dark";
1469 let mockHeartbeatRevision = 0;
1470 let mockHeartbeatTasks: unknown[] = [];
1471 // Vite rewrites these literal asset URLs in both dev and production builds.
1472 // Keeping them on the browser mock makes local visual acceptance match the
1473 // desktop bridge, whose ListThemePacks response carries the same two URLs.
1474 const mockOfficialThemeAssets = {
1475 "official-rose-dawn": {
1476 previewUrl: new URL("../../../themes/official/official-rose-dawn/preview.webp", import.meta.url).href,
1477 backgroundUrl: new URL("../../../themes/official/official-rose-dawn/background.webp", import.meta.url).href,
1478 },
1479 "official-fortune-forge": {
1480 previewUrl: new URL("../../../themes/official/official-fortune-forge/preview.webp", import.meta.url).href,
1481 backgroundUrl: new URL("../../../themes/official/official-fortune-forge/background.webp", import.meta.url).href,
1482 },
1483 "official-crimson-horizon": {
1484 previewUrl: new URL("../../../themes/official/official-crimson-horizon/preview.webp", import.meta.url).href,
1485 backgroundUrl: new URL("../../../themes/official/official-crimson-horizon/background.webp", import.meta.url).href,
1486 },
1487 "official-sage-breeze": {
1488 previewUrl: new URL("../../../themes/official/official-sage-breeze/preview.webp", import.meta.url).href,
1489 backgroundUrl: new URL("../../../themes/official/official-sage-breeze/background.webp", import.meta.url).href,
1490 },
1491 "official-spark-notebook": {
1492 previewUrl: new URL("../../../themes/official/official-spark-notebook/preview.webp", import.meta.url).href,
1493 backgroundUrl: new URL("../../../themes/official/official-spark-notebook/background.webp", import.meta.url).href,
1494 },
1495 "official-violet-starlight": {
1496 previewUrl: new URL("../../../themes/official/official-violet-starlight/preview.webp", import.meta.url).href,
1497 backgroundUrl: new URL("../../../themes/official/official-violet-starlight/background.webp", import.meta.url).href,
1498 },
1499 "official-cyan-stage": {
1500 previewUrl: new URL("../../../themes/official/official-cyan-stage/preview.webp", import.meta.url).href,
1501 backgroundUrl: new URL("../../../themes/official/official-cyan-stage/background.webp", import.meta.url).href,
1502 },
1503 "official-noir-gold": {
1504 previewUrl: new URL("../../../themes/official/official-noir-gold/preview.webp", import.meta.url).href,
1505 backgroundUrl: new URL("../../../themes/official/official-noir-gold/background.webp", import.meta.url).href,
1506 },
1507 } as const;
1508 registerTrustedThemeBackgroundURLs(Object.values(mockOfficialThemeAssets).map((asset) => asset.backgroundUrl));
1509 let mockThemePacks: import("./themePack").ThemePackView[] = [
1510 { id: "graphite", name: "Graphite", author: "Reasonix", baseStyle: "graphite", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1511 { id: "aurora", name: "Aurora", author: "Reasonix", baseStyle: "aurora", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1512 { id: "slate", name: "Slate", author: "Reasonix", baseStyle: "slate", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1513 { id: "carbon", name: "Carbon", author: "Reasonix", baseStyle: "carbon", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1514 { id: "nocturne", name: "Nocturne", author: "Reasonix", baseStyle: "nocturne", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1515 { id: "amber", name: "Amber", author: "Reasonix", baseStyle: "amber", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } },
1516 { ...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 } },
1517 { ...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 } },
1518 { ...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 } },
1519 { ...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 } },
1520 { ...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 } },
1521 { ...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 } },
1522 { ...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 } },
1523 { ...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 } },
1524 ];
1525 const day = 86_400_000;
1526 const t0 = Date.now();
1527 // Mutable so MCP add/remove/retry are observable in browser dev.
1528 let capServers: ServerView[] = [
1529 {
1530 name: "project-knowledge",
1531 transport: "http",
1532 status: "connected",
1533 configured: true,
1534 autoStart: true,
1535 tier: "background",
1536 source: "project",
1537 configSource: "reasonix.toml",
1538 url: "https://mcp.example.test/project",
1539 tools: 3,
1540 prompts: 0,
1541 resources: 1,
1542 toolList: [
1543 { name: "search_knowledge", description: "Search the project knowledge base.", readOnlyHint: true },
1544 { name: "get_document", description: "Read a knowledge-base document.", readOnlyHint: true },
1545 { name: "list_topics", description: "List available knowledge topics.", readOnlyHint: true },
1546 ],
1547 },
1548 {
1549 name: "github",
1550 transport: "stdio",
1551 status: "connected",
1552 configured: true,
1553 autoStart: true,
1554 tier: "background",
1555 command: "npx",
1556 args: ["-y", "@modelcontextprotocol/server-github"],
1557 tools: 4,
1558 prompts: 2,
1559 resources: 0,
1560 toolList: [
1561 { name: "issue_read", description: "Read GitHub issue details and comments.", readOnlyHint: true },
1562 { name: "pull_request_read", description: "Read pull request metadata, files, and review threads.", readOnlyHint: true },
1563 { name: "search_issues", description: "Search issues and pull requests.", readOnlyHint: true },
1564 { name: "issue_write", description: "Create or update GitHub issues." },
1565 ],
1566 },
1567 {
1568 name: "linear",
1569 transport: "http",
1570 status: "initializing",
1571 configured: true,
1572 autoStart: true,
1573 tier: "background",
1574 url: "https://mcp.linear.app/mcp",
1575 authStatus: "possible",
1576 authUrl: "https://mcp.linear.app/mcp",
1577 tools: 8,
1578 prompts: 0,
1579 resources: 0,
1580 toolList: [
1581 { name: "list_issues", description: "List and filter Linear issues." },
1582 { name: "get_issue", description: "Fetch a Linear issue by id or key." },
1583 { name: "create_issue", description: "Create a Linear issue." },
1584 { name: "update_issue", description: "Update status, assignee, priority, or labels." },
1585 { name: "list_projects", description: "List Linear projects." },
1586 { name: "get_project", description: "Fetch project details." },
1587 { name: "list_teams", description: "List Linear teams." },
1588 { name: "search", description: "Search Linear workspace objects." },
1589 ],
1590 },
1591 { 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" },
1592 ];
1593 const capSkills: SkillView[] = [
1594 {
1595 name: "explore", description: "Investigate the codebase in an isolated subagent", scope: "builtin", runAs: "subagent", enabled: true,
1596 allowedTools: ["read_file", "ls", "glob", "grep", "code_index"], invocation: "/explore", invocationMode: "auto",
1597 configuredModel: "deepseek/deepseek-v4-pro", configuredEffort: "high",
1598 },
1599 { 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" },
1600 { name: "review", description: "Review the staged diff", scope: "project", sourceDir: "~/projects/reasonix/.reasonix/skills", runAs: "inline", enabled: false, invocation: "/review" },
1601 { name: "init", description: "Scaffold a REASONIX.md for this repo", scope: "builtin", runAs: "inline", enabled: true, invocation: "/init" },
1602 {
1603 name: "my-formatter", description: "Formats code the way I like it", scope: "global", sourceDir: "~/.reasonix/skills", runAs: "subagent", enabled: true,
1604 model: "deepseek-pro", effort: "high", allowedTools: ["read_file", "edit_file"], color: "amber", invocation: "/my-formatter", invocationMode: "manual",
1605 body: "You are a code formatting assistant. Reformat the given file to match project style without changing behavior.",
1606 },
1607 ];
1608 let capSkillRoots: SkillRootView[] = [
1609 { dir: "~/projects/reasonix/.reasonix/skills", scope: "project", priority: 1, status: "missing", enabled: true, configured: false, removable: true, skills: 0 },
1610 {
1611 dir: "~/my-skills",
1612 scope: "custom",
1613 priority: 5,
1614 status: "ok",
1615 enabled: true,
1616 configured: true,
1617 removable: true,
1618 skills: 1,
1619 skillItems: [{ name: "review", description: "Review the staged diff", scope: "custom", runAs: "inline" }],
1620 },
1621 {
1622 dir: "~/.reasonix/skills",
1623 scope: "global",
1624 priority: 6,
1625 status: "ok",
1626 enabled: true,
1627 configured: false,
1628 removable: true,
1629 skills: 2,
1630 skillItems: [
1631 { name: "explore", description: "Investigate the codebase in an isolated subagent", scope: "global", runAs: "subagent" },
1632 { name: "init", description: "Scaffold a REASONIX.md for this repo", scope: "global", runAs: "inline" },
1633 ],
1634 },
1635 ];
1636 let capPlugins: PluginView[] = [];
1637 const mockSwitchWorkspace = async (path: string) => {
1638 cwd = path || "~";
1639 workspaces = [cwd, ...workspaces.filter((p) => p !== cwd)].slice(0, 12);
1640 if (!mockProjectTree.some((node) => node.kind === "project" && node.root === cwd)) {
1641 mockProjectTree.unshift({
1642 key: `project_${cwd}`,
1643 kind: "project",
1644 label: baseName(cwd),
1645 root: cwd,
1646 children: [],
1647 });
1648 }
1649 return cwd;
1650 };
1651 // Mutable so delete/rename are observable in browser dev.
1652 const sessions: SessionMeta[] = [
1653 { 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 },
1654 { 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 },
1655 { 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 },
1656 { 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 },
1657 ];
1658 const trashedSessions: SessionMeta[] = [
1659 {
1660 path: "/mock/sessions/.trash/trash-dev-standard.jsonl",
1661 title: t("mock.trashDevStandardTitle"),
1662 preview: t("mock.trashDevStandardPreview"),
1663 turns: 4,
1664 createdAt: t0 - 8 * day,
1665 lastActivityAt: t0 - 7 * day,
1666 modTime: t0 - 7 * day,
1667 deletedAt: t0 - 20 * 60_000,
1668 current: false,
1669 open: false,
1670 scope: "project",
1671 workspaceRoot: "~/projects/joyquant-db",
1672 topicId: "topic_dev_standard",
1673 topicTitle: t("mock.trashDevStandardTitle"),
1674 },
1675 {
1676 path: "/mock/sessions/.trash/trash-p3a-review.jsonl",
1677 title: t("mock.trashP3aTitle"),
1678 preview: t("mock.trashP3aPreview"),
1679 turns: 7,
1680 createdAt: t0 - 6 * day,
1681 lastActivityAt: t0 - 5 * day,
1682 modTime: t0 - 5 * day,
1683 deletedAt: t0 - 2 * 3_600_000,
1684 current: false,
1685 open: false,
1686 scope: "project",
1687 workspaceRoot: "~/projects/joyquant-sys",
1688 topicId: "topic_p3a_pd",
1689 topicTitle: t("mock.trashP3aTitle"),
1690 },
1691 {
1692 path: "/mock/sessions/.trash/trash-global-product.jsonl",
1693 title: t("mock.trashGlobalProductTitle"),
1694 preview: t("mock.trashGlobalProductPreview"),
1695 turns: 2,
1696 createdAt: t0 - 4 * day,
1697 lastActivityAt: t0 - 3 * day,
1698 modTime: t0 - 3 * day,
1699 deletedAt: t0 - day,
1700 current: false,
1701 open: false,
1702 scope: "global",
1703 topicId: "topic_product",
1704 topicTitle: t("mock.trashGlobalProductTitle"),
1705 recovered: true,
1706 recoveryCopy: true,
1707 },
1708 ];
1709 if (freshMock) {
1710 sessions.splice(0);
1711 trashedSessions.splice(0);
1712 }
1713 // Mutable settings so the Settings panel's edits are observable in browser dev.
1714 const settings: SettingsView = {
1715 modelSettingsFingerprint: "mock-model-settings-0",
1716 defaultModel: "deepseek",
1717 plannerModel: "",
1718 visionModel: "",
1719 webSearchModel: "auto",
1720 webSearchModels: ["deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro"],
1721 webSearchModelStatus: "ready",
1722 subagentModel: "",
1723 subagentEffort: "",
1724 autoPlan: "off",
1725 providers: [
1726 { name: "deepseek", builtIn: true, added: deepSeekUpgradeMock, kind: "openai", baseUrl: "https://api.deepseek.com", modelsUrl: "", models: ["deepseek-v4-flash"], visionModels: [], visionModelsConfigured: false, default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", headers: deepSeekUpgradeMock ? { "X-Route": "official-custom" } : undefined, keySet: true, balanceUrl: "https://api.deepseek.com/user/balance", contextWindow: 1_000_000, reasoningProtocol: "", thinking: "enabled", webSearch: true, serverWebSearchCapability: true, supportedEfforts: ["disabled", "low", "high", "max"], defaultEffort: "high", recommendedUpgradeAvailable: false },
1727 ],
1728 officialProviders: [
1729 { 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: "enabled", webSearch: true, serverWebSearchCapability: true, supportedEfforts: ["disabled", "low", "high", "max"], defaultEffort: "high" },
1730 ],
1731 providerPresets: mockProviderPresetViews(),
1732 permissions: { mode: "ask", allow: ["ls", "read_file"], ask: [], deny: ["Bash(rm:*)"] },
1733 sandbox: { bash: browserPreviewBashSandboxMode(), network: true, workspaceRoot: "", allowWrite: [], effectiveWorkspaceRoot: cwd, effectiveWriteRoots: [cwd], shell: "auto", effectiveShell: browserPreviewEffectiveShell("auto"), resolvedShell: browserPreviewEffectiveShell("auto"), shellReloadRequired: false, ...browserPreviewShellSupport(browserPlatformOverride()) },
1734 network: {
1735 proxyMode: "auto",
1736 proxyUrl: "",
1737 noProxy: "",
1738 proxy: { type: "socks5", server: "127.0.0.1", port: 7890, username: "", password: "" },
1739 },
1740 agent: { temperature: 0.2, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, maxSubagentConcurrency: 6, maxParallelWriters: 3, systemPrompt: "You are Reasonix, a coding agent.", reasoningLanguage: "auto", compactRatio: 0.8 },
1741 bot: {
1742 enabled: !freshMock,
1743 model: "",
1744 toolApprovalMode: "ask",
1745 maxSteps: 0,
1746 debounceMs: 1500,
1747 queueMode: "steer",
1748 queueCap: 20,
1749 queueDrop: "summarize",
1750 ignoreSelfMessages: true,
1751 selfUserIds: {
1752 qq: [],
1753 feishu: [],
1754 weixin: [],
1755 dingtalk: [],
1756 },
1757 control: {
1758 enabled: false,
1759 addr: "127.0.0.1:37913",
1760 tokenEnv: "REASONIX_BOT_CONTROL_TOKEN",
1761 },
1762 pairing: {
1763 enabled: true,
1764 requestTtlMinutes: 60,
1765 maxPendingPerPlatform: 3,
1766 },
1767 routes: [],
1768 allowlist: {
1769 enabled: true,
1770 allowAll: false,
1771 qqUsers: [],
1772 feishuUsers: freshMock ? [] : ["ou_mock_user_001"],
1773 weixinUsers: freshMock ? [] : ["wxid_mock_user_001"],
1774 qqApprovers: [],
1775 feishuApprovers: [],
1776 weixinApprovers: [],
1777 qqAdmins: [],
1778 feishuAdmins: [],
1779 weixinAdmins: [],
1780 qqGroups: [],
1781 feishuGroups: [],
1782 weixinGroups: [],
1783 dingtalkUsers: [],
1784 dingtalkApprovers: [],
1785 dingtalkAdmins: [],
1786 dingtalkGroups: [],
1787 },
1788 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: [] } },
1789 feishu: {
1790 enabled: false,
1791 domain: "feishu",
1792 appId: "",
1793 appSecretEnv: "FEISHU_BOT_APP_SECRET",
1794 secretSet: false,
1795 verificationToken: "",
1796 mode: "webhook",
1797 webhookPort: 8080,
1798 requireMention: true,
1799 },
1800 weixin: {
1801 enabled: false,
1802 accountId: "default",
1803 tokenEnv: "WEIXIN_BOT_TOKEN",
1804 tokenSet: false,
1805 apiBase: "https://ilinkai.weixin.qq.com",
1806 },
1807 dingtalk: {
1808 enabled: false,
1809 clientId: "",
1810 clientSecretEnv: "DINGTALK_CLIENT_SECRET",
1811 secretSet: false,
1812 botName: "",
1813 requireMention: true,
1814 model: "",
1815 toolApprovalMode: "",
1816 workspaceRoot: "",
1817 access: { enabled: true, allowAll: false, pairingEnabled: true, users: [], groups: [], approvers: [], admins: [] },
1818 },
1819 connections: freshMock ? [] : [
1820 {
1821 id: "mock-lark-kun",
1822 provider: "feishu",
1823 domain: "lark",
1824 label: "kun",
1825 enabled: true,
1826 status: "connected",
1827 model: "",
1828 toolApprovalMode: "",
1829 workspaceRoot: "",
1830 access: { enabled: true, allowAll: false, pairingEnabled: true, users: ["ou_mock_user_001"], groups: [], approvers: [], admins: [] },
1831 credential: {
1832 appId: "cli_mock_lark",
1833 appSecretEnv: "FEISHU_BOT_APP_SECRET",
1834 accountId: "",
1835 tokenEnv: "",
1836 secretSet: true,
1837 },
1838 sessionMappings: [
1839 {
1840 remoteId: "ou_mock_user_001",
1841 sessionId: "topic:topic_product",
1842 sessionSource: "",
1843 chatType: "",
1844 userId: "",
1845 threadId: "",
1846 scope: "global",
1847 workspaceRoot: "",
1848 updatedAt: new Date(Date.now() - 4 * 60_000).toISOString(),
1849 },
1850 ],
1851 lastError: "",
1852 createdAt: new Date(Date.now() - 86_400_000).toISOString(),
1853 updatedAt: new Date(Date.now() - 4 * 60_000).toISOString(),
1854 },
1855 {
1856 id: "mock-weixin-kun",
1857 provider: "weixin",
1858 domain: "weixin",
1859 label: "kun",
1860 enabled: true,
1861 status: "connected",
1862 model: "",
1863 toolApprovalMode: "",
1864 workspaceRoot: "",
1865 access: { enabled: true, allowAll: false, pairingEnabled: true, users: ["wxid_mock_user_001"], groups: [], approvers: [], admins: [] },
1866 credential: {
1867 appId: "",
1868 appSecretEnv: "",
1869 accountId: "default",
1870 tokenEnv: "WEIXIN_BOT_TOKEN",
1871 secretSet: true,
1872 },
1873 sessionMappings: [
1874 {
1875 remoteId: "wxid_mock_user_001",
1876 sessionId: "topic:topic_ai",
1877 sessionSource: "",
1878 chatType: "",
1879 userId: "",
1880 threadId: "",
1881 scope: "global",
1882 workspaceRoot: "",
1883 updatedAt: new Date(Date.now() - 12 * 60_000).toISOString(),
1884 },
1885 ],
1886 lastError: "",
1887 createdAt: new Date(Date.now() - 86_400_000).toISOString(),
1888 updatedAt: new Date(Date.now() - 12 * 60_000).toISOString(),
1889 },
1890 ],
1891 },
1892 desktopLanguage: "",
1893 desktopCurrency: "",
1894 desktopTheme: "auto",
1895 desktopThemeStyle: "graphite",
1896 desktopTerminalTheme: "auto",
1897 conversationWidth: "standard",
1898 closeBehavior: "background",
1899 displayMode: "standard", sessionExperience: "standard", reasoningDisplayMode: "auto", reasoningDisplayModeExplicit: false,
1900 statusBarStyle: "icon",
1901 statusBarItems: [...DEFAULT_STATUS_BAR_ITEMS],
1902 defaultToolApprovalMode: "workspace-write",
1903 checkUpdates: true,
1904 updaterEnabled: true,
1905 updateChannel: "stable",
1906 telemetry: true,
1907 metrics: true,
1908 configPath: "~/.reasonix/config.toml",
1909 shadowedByPath: "~/projects/reasonix/reasonix.toml",
1910 providerKinds: ["openai", "anthropic", "responses"],
1911 autoApproveTools: false,
1912 bypass: false,
1913 };
1914 const hookEvents = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "PostLLMCall", "SessionStart", "SessionEnd", "SubagentStop", "Notification", "PreCompact"];
1915 const hookSettings: Record<string, HooksSettingsView> = {
1916 global: {
1917 scope: "global",
1918 path: "~/.reasonix/settings.json",
1919 projectRoot: "",
1920 trusted: true,
1921 events: hookEvents,
1922 hooks: [
1923 { event: "Stop", command: "echo turn done", description: "Notify after each turn" },
1924 ],
1925 },
1926 project: {
1927 scope: "project",
1928 path: "./.reasonix/settings.json",
1929 projectRoot: "/mock/project",
1930 trusted: false,
1931 events: hookEvents,
1932 hooks: [],
1933 },
1934 };
1935 settings.providers = settings.providers.map((provider) =>
1936 provider.apiKeyEnv === "DEEPSEEK_API_KEY" ? { ...provider, keySet: !freshMock } : provider,
1937 );
1938 if (freshMock) {
1939 settings.configPath = "~/.reasonix/config.toml";
1940 settings.shadowedByPath = "";
1941 }
1942 const mockNow = Date.now();
1943 const mockProjectTree: ProjectNode[] = freshMock ? [] : benchMock ? [
1944 {
1945 key: "project_~/projects/reasonix",
1946 kind: "project",
1947 label: "reasonix",
1948 root: "~/projects/reasonix",
1949 projectColor: "purple",
1950 children: [
1951 { key: "topic_bench_markdown", kind: "topic", label: "● bench:markdown-46t", root: "~/projects/reasonix", topicId: "topic_bench_markdown", projectColor: "purple", turns: 46, lastActivityAt: mockNow - 60_000, open: true },
1952 { key: "topic_bench_tools", kind: "topic", label: "● bench:tools-38t", root: "~/projects/reasonix", topicId: "topic_bench_tools", projectColor: "blue", turns: 38, lastActivityAt: mockNow - 120_000, open: true },
1953 { key: "topic_bench_small", kind: "topic", label: "bench:small-6t", root: "~/projects/reasonix", topicId: "topic_bench_small", projectColor: "green", turns: 6, lastActivityAt: mockNow - 180_000 },
1954 { key: "topic_bench_giant_turn", kind: "topic", label: "bench:giant-turn", root: "~/projects/reasonix", topicId: "topic_bench_giant_turn", projectColor: "amber", turns: 1, lastActivityAt: mockNow - 240_000 },
1955 { key: "topic_bench_windowed", kind: "topic", label: "bench:windowed-1000t", root: "~/projects/reasonix", topicId: "topic_bench_windowed", projectColor: "blue", turns: 1000, lastActivityAt: mockNow - 270_000 },
1956 { key: "topic_bench_reported_long_turn", kind: "topic", label: "bench:reported-long-turn", root: "~/projects/reasonix", topicId: "topic_bench_reported_long_turn", projectColor: "amber", turns: 1, lastActivityAt: mockNow - 300_000 },
1957 { key: "topic_bench_geometry_contract", kind: "topic", label: "bench:geometry-blocks", root: "~/projects/reasonix", topicId: "topic_bench_geometry_contract", projectColor: "amber", turns: 1, lastActivityAt: mockNow - 330_000 },
1958 { key: "topic_bench_storm", kind: "topic", label: "bench:storm-40t", root: "~/projects/reasonix", topicId: "topic_bench_storm", projectColor: "red", turns: 40, lastActivityAt: mockNow - 360_000 },
1959 { key: "topic_bench_selection_table", kind: "topic", label: "bench:selection-table", root: "~/projects/reasonix", topicId: "topic_bench_selection_table" },
1960 ],
1961 },
1962 ] : [
1963 {
1964 key: "project_~/projects/joyquant-db",
1965 kind: "project",
1966 label: t("mock.projectJoyquantDb"),
1967 root: "~/projects/joyquant-db",
1968 projectColor: "blue",
1969 children: [
1970 { key: "topic_dev_standard", kind: "topic", label: `● ${t("mock.topicDevStandard")}`, root: "~/projects/joyquant-db", topicId: "topic_dev_standard", projectColor: "blue", turns: 18, createdAt: mockNow - 3 * 24 * 60 * 60_000, lastActivityAt: mockNow - 8 * 60_000, open: true, running: runningMock },
1971 { key: "topic_db_maint", kind: "topic", label: t("mock.topicDbMaint"), root: "~/projects/joyquant-db", topicId: "topic_db_maint", projectColor: "blue", turns: 7, createdAt: mockNow - 2 * 24 * 60 * 60_000, lastActivityAt: mockNow - 2 * 60 * 60_000 },
1972 { key: "topic_env", kind: "topic", label: t("mock.topicEnv"), root: "~/projects/joyquant-db", topicId: "topic_env", projectColor: "blue", turns: 3, createdAt: mockNow - 24 * 60 * 60_000, lastActivityAt: mockNow - 26 * 60 * 60_000 },
1973 ],
1974 },
1975 {
1976 key: "project_~/projects/joyquant-sys",
1977 kind: "project",
1978 label: t("mock.projectJoyquantSys"),
1979 root: "~/projects/joyquant-sys",
1980 projectColor: "purple",
1981 children: [
1982 { 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 },
1983 { 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 },
1984 { 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 },
1985 { 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 },
1986 { 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" },
1987 { 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" },
1988 ],
1989 },
1990 {
1991 key: "global_folder",
1992 kind: "global_folder",
1993 label: "Global",
1994 root: globalWorkspaceRoot,
1995 children: [
1996 { key: "global_topic_product", kind: "global_topic", label: t("mock.topicProduct"), topicId: "topic_product", turns: 5, lastActivityAt: mockNow - 8 * 24 * 60 * 60_000 },
1997 { key: "global_topic_ai", kind: "global_topic", label: t("mock.topicAi"), topicId: "topic_ai", turns: 8, lastActivityAt: mockNow - 10 * 24 * 60 * 60_000 },
1998 { key: "global_topic_lab", kind: "global_topic", label: t("mock.topicLab"), topicId: "topic_lab", turns: 2, lastActivityAt: mockNow - 12 * 24 * 60 * 60_000 },
1999 ],
2000 },
2001 ];
2002 const ensureMockGlobalFolder = (): ProjectNode => {
2003 let node = mockProjectTree.find((item) => item.kind === "global_folder");
2004 if (!node) {
2005 node = {
2006 key: "global_folder",
2007 kind: "global_folder",
2008 label: "Global",
2009 root: globalWorkspaceRoot,
2010 children: [],
2011 };
2012 mockProjectTree.push(node);
2013 }
2014 return node;
2015 };
2016 const mockProjectTreeForDisplay = () => {
2017 const pinnedProjects = mockProjectTree.filter((node) => node.kind === "project" && node.pinned);
2018 if (pinnedProjects.length === 0) return mockProjectTree;
2019 const rest = mockProjectTree.filter((node) => !(node.kind === "project" && node.pinned));
2020 return [...pinnedProjects, ...rest];
2021 };
2022 const cloneProjectTree = () => {
2023 if (mockProjectTree.length === 0) ensureMockGlobalFolder();
2024 return remoteProjects.appendToTree(JSON.parse(JSON.stringify(mockProjectTreeForDisplay())) as ProjectNode[]);
2025 };
2026 const projectChildren = (node: ProjectNode): ProjectNode[] => Array.isArray(node.children) ? node.children : [];
2027 const findMockTopic = (topicId: string): ProjectNode | null => {
2028 for (const parent of mockProjectTree) {
2029 const found = projectChildren(parent).find((child) => child.topicId === topicId);
2030 if (found) return found;
2031 }
2032 return null;
2033 };
2034 const setMockTopicPinned = (topicId: string, pinned: boolean) => {
2035 for (const parent of mockProjectTree) {
2036 const children = projectChildren(parent);
2037 const index = children.findIndex((child) => child.topicId === topicId);
2038 if (index < 0) continue;
2039 const topic = { ...children[index], pinned: pinned || undefined };
2040 if (!pinned) {
2041 parent.children = children.map((child, i) => (i === index ? topic : child));
2042 return;
2043 }
2044 const remaining = children.filter((_, i) => i !== index);
2045 parent.children = [topic, ...remaining];
2046 return;
2047 }
2048 };
2049 const setMockProjectPinned = (workspaceRoot: string, pinned: boolean) => {
2050 const index = mockProjectTree.findIndex((node) => node.kind === "project" && node.root === workspaceRoot);
2051 if (index < 0) return;
2052 mockProjectTree[index] = { ...mockProjectTree[index], pinned: pinned || undefined };
2053 };
2054 const deleteMockTopic = (topicId: string) => {
2055 for (const parent of mockProjectTree) {
2056 parent.children = projectChildren(parent).filter((child) => child.topicId !== topicId);
2057 }
2058 };
2059 const topicLabel = (topicId: string, fallback: string) => (findMockTopic(topicId)?.label || fallback).replace(/^●\s*/, "");
2060 const mockTopicStatus = (topicId: string) => findMockTopic(topicId)?.status ?? "";
2061 const mockTopicIsRunning = (topicId: string) => {
2062 const status = mockTopicStatus(topicId);
2063 return status === "streaming" || status === "thinking" || status === "waiting_confirmation";
2064 };
2065 const mockTopicIsBlank = (topicId: string) => {
2066 const topic = findMockTopic(topicId);
2067 return Boolean(topic && topic.label === t("mock.newSession") && !topic.turns && !topic.lastActivityAt && !topic.status);
2068 };
2069 const mockTopicRunsInScenario = (topicId: string) => runningMock && mockTopicIsRunning(topicId);
2070 // Benchmark fixtures (?mock=bench) live in a lazily imported module: the
2071 // generators build ~1MiB of mock content and must stay out of the eager
2072 // bundle (initial-chunk gzip budget). See bridgeBenchFixtures.ts.
2073 const benchFixturesPromise = benchMock ? import("./bridgeBenchFixtures") : null;
2074 const mockHistoryPage = (messages: HistoryMessage[], beforeTurn = 0, limit = 60): HistoryPage => {
2075 const totalTurns = messages.reduce((count, message) => count + (message.role === "user" ? 1 : 0), 0);
2076 const safeLimit = Math.max(1, Math.min(200, Math.floor(limit || 60)));
2077 const endTurn = beforeTurn > 0 && beforeTurn <= totalTurns ? beforeTurn : totalTurns;
2078 const startTurn = Math.max(0, endTurn - safeLimit);
2079 let turn = -1;
2080 const pageMessages = messages.filter((message) => {
2081 if (message.role === "user") turn += 1;
2082 if (turn < 0) return startTurn === 0;
2083 return turn >= startTurn && turn < endTurn;
2084 });
2085 return { messages: pageMessages, startTurn, endTurn, totalTurns, hasOlder: startTurn > 0 };
2086 };
2087 const mockRuntimeInjected = new Set<string>();
2088 const queueMockTopicRuntime = (tab: TabMeta) => {
2089 if (!runningMock) return;
2090 const status = mockTopicStatus(tab.topicId);
2091 if (status !== "streaming" && status !== "thinking" && status !== "waiting_confirmation") return;
2092 const key = `${tab.id}:${tab.topicId}:${status}`;
2093 if (mockRuntimeInjected.has(key)) return;
2094 mockRuntimeInjected.add(key);
2095 window.setTimeout(() => {
2096 void withMockTabScope(tab.id, async () => {
2097 emitMockTurnStarted();
2098 await delay(120);
2099 if (tab.topicId === "topic_p3b_pd") {
2100 const text = "我会先把范围拆成三层:目标、依赖、风险。当前已经确认 p3b 的交付边界,接下来补充每个模块的验收口径...";
2101 for (const ch of text) {
2102 emit({ kind: "text", text: ch });
2103 await delay(5);
2104 }
2105 return;
2106 }
2107 if (tab.topicId === "topic_p3a_pd") {
2108 emit({ kind: "reasoning", text: "我正在对比 p3a 和 p3b 的差异:先看约束,再看变更风险,最后判断是否需要拆成独立任务。\n\n" });
2109 await delay(220);
2110 emit({ kind: "reasoning", text: "当前倾向:先保留 p3a 的兼容路径,不急于删除旧逻辑。" });
2111 return;
2112 }
2113 if (tab.topicId === "topic_hotfix") {
2114 const id = "mock-hotfix-shell";
2115 emit({ kind: "tool_dispatch", tool: { id, name: "bash", args: JSON.stringify({ command: "git status --short && npm test" }), readOnly: true } });
2116 await delay(180);
2117 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" } });
2118 return;
2119 }
2120 if (tab.topicId === "topic_sys_coord") {
2121 pendingApprovalPreview = true;
2122 pendingApprovalPreviewPrompt = { id: "mock-sys-confirm", tool: "bash" };
2123 emit({ kind: "reasoning", text: "我已经准备好执行同步脚本,但这个操作会影响本地 workspace,需要用户确认。" });
2124 await delay(160);
2125 emitMockPrompt({
2126 kind: "approval_request",
2127 approval: {
2128 id: "mock-sys-confirm",
2129 tool: "bash",
2130 subject: "npm run sync:joyquant-sys\n\n该命令会同步 SYS 项目配置并刷新本地缓存。",
2131 },
2132 });
2133 }
2134 });
2135 }, 180);
2136 };
2137 const setMockActiveTab = (tabId: string) => {
2138 mockTabs = mockTabs.map((tab) => ({ ...tab, active: tab.id === tabId }));
2139 };
2140 // Single-surface prunes stash removed tabs so a later re-activation restores
2141 // the SAME tab identity (the real backend "opens or reuses" the topic's
2142 // tab), keeping the transcript store's (tabId, sessionPath) cache warm.
2143 const mockPrunedTabs = new Map<string, TabMeta>();
2144 const mockTabGraveyardKey = (tab: Pick<TabMeta, "scope" | "workspaceRoot" | "topicId">) =>
2145 `${tab.scope}:${tab.workspaceRoot}:${tab.topicId}`;
2146 const pruneMockTabsTo = (keepTabId: string) => {
2147 for (const tab of mockTabs) {
2148 if (tab.id !== keepTabId) mockPrunedTabs.set(mockTabGraveyardKey(tab), tab);
2149 }
2150 mockTabs = mockTabs.filter((item) => item.id === keepTabId).map((item) => ({ ...item, active: true }));
2151 };
2152 const restoreMockPrunedTab = (scope: string, workspaceRoot: string, topicId: string): TabMeta | undefined => {
2153 const key = mockTabGraveyardKey({ scope, workspaceRoot, topicId });
2154 const tab = mockPrunedTabs.get(key);
2155 if (!tab) return undefined;
2156 mockPrunedTabs.delete(key);
2157 return tab;
2158 };
2159 const currentMockTurnTabId = () => mockScopedTabId || mockTabs.find((tab) => tab.active)?.id;
2160 const setMockTabRunning = (tabId: string | undefined, running: boolean) => {
2161 if (!tabId) return;
2162 mockTabs = mockTabs.map((tab) => (tab.id === tabId ? { ...tab, running } : tab));
2163 };
2164 const mockInteractions = createBrowserMockInteractionIdentity({ emit, currentTabId: currentMockTurnTabId, setRunning: setMockTabRunning });
2165 const { emitPrompt: emitMockPrompt, turnStarted: emitMockTurnStarted, turnDone: emitMockTurnDone } = mockInteractions;
2166 // Fresh user decisions never auto-allow on a posture switch (mirrors the
2167 // backend's requiresFreshApprovalTool set).
2168 const mockFreshApprovalTools = new Set(["exit_plan_mode", "sandbox_escape", "memory_remember", "memory_forget", "managed_config_write"]);
2169 // Mirrors the backend drain contract for the mode-switch bindings: returns
2170 // the prompt ids the new posture auto-allowed; fresh prompts stay pending.
2171 const drainMockApprovalPreviews = (toolApprovalMode: string): string[] => {
2172 const preset = normalizeToolApprovalMode(toolApprovalMode);
2173 if (preset !== "workspace-write" && preset !== "danger-full-access") return [];
2174 const prompt = pendingApprovalPreviewPrompt;
2175 if (!pendingApprovalPreview || !prompt || mockFreshApprovalTools.has(prompt.tool)) return [];
2176 pendingApprovalPreview = false;
2177 pendingApprovalPreviewPrompt = undefined;
2178 emit({ kind: "message", text: `approval preview auto-allowed (${toolApprovalMode})` });
2179 emitMockTurnDone();
2180 return [prompt.id];
2181 };
2182 let mockTabs: TabMeta[] = benchMock ? [
2183 // Phase F benchmark scenario: the two heaviest sessions pre-opened, the
2184 // markdown-heavy one active (cold open renders the 500KiB answer).
2185 {
2186 id: "tab_bench_markdown",
2187 scope: "project",
2188 workspaceRoot: "~/projects/reasonix",
2189 workspaceName: "reasonix",
2190 workspacePath: "~/projects/reasonix",
2191 gitBranch: "bench",
2192 topicId: "topic_bench_markdown",
2193 topicTitle: "bench:markdown-46t",
2194 projectColor: "purple",
2195 label: "DeepSeek-R1",
2196 ready: true,
2197 running: false,
2198 mode: "normal",
2199 collaborationMode: "normal",
2200 toolApprovalMode: "ask",
2201 tokenMode: "full",
2202 active: true,
2203 cwd: "~/projects/reasonix",
2204 },
2205 {
2206 id: "tab_bench_tools",
2207 scope: "project",
2208 workspaceRoot: "~/projects/reasonix",
2209 workspaceName: "reasonix",
2210 workspacePath: "~/projects/reasonix",
2211 gitBranch: "bench",
2212 topicId: "topic_bench_tools",
2213 topicTitle: "bench:tools-38t",
2214 projectColor: "blue",
2215 label: "DeepSeek-R1",
2216 ready: true,
2217 running: false,
2218 mode: "normal",
2219 collaborationMode: "normal",
2220 toolApprovalMode: "ask",
2221 tokenMode: "full",
2222 active: false,
2223 cwd: "~/projects/reasonix",
2224 },
2225 ] : noticePreviewMock ? [
2226 {
2227 id: "tab_notice_preview",
2228 scope: "project",
2229 workspaceRoot: "~/projects/reasonix",
2230 workspaceName: "reasonix",
2231 workspacePath: "~/projects/reasonix",
2232 gitBranch: "codex/compact-chat-notices-i18n",
2233 topicId: "topic_notice_preview",
2234 topicTitle: "Compact notice preview",
2235 projectColor: "green",
2236 label: "DeepSeek-R1",
2237 ready: true,
2238 running: false,
2239 mode: "normal",
2240 collaborationMode: "normal",
2241 toolApprovalMode: "ask",
2242 tokenMode: "full",
2243 active: true,
2244 cwd: "~/projects/reasonix",
2245 },
2246 ] : freshMock ? [
2247 {
2248 id: "tab_global",
2249 scope: "global",
2250 workspaceRoot: globalWorkspaceRoot,
2251 workspaceName: "Global",
2252 workspacePath: globalWorkspaceRoot,
2253 topicId: "",
2254 topicTitle: "Global",
2255 label: "DeepSeek-R1",
2256 ready: true,
2257 running: false,
2258 mode: "normal",
2259 collaborationMode: "normal",
2260 toolApprovalMode: "ask",
2261 tokenMode: "full",
2262 active: true,
2263 cwd: globalWorkspaceRoot,
2264 },
2265 ] : [
2266 {
2267 id: "tab_joyquant_db",
2268 scope: "project",
2269 workspaceRoot: "~/projects/joyquant-db",
2270 workspaceName: "joyquant-db",
2271 workspacePath: "~/projects/joyquant-db",
2272 gitBranch: "main",
2273 topicId: "topic_dev_standard",
2274 topicTitle: t("mock.trashDevStandardTitle"),
2275 projectColor: "blue",
2276 label: "DeepSeek-R1",
2277 ready: true,
2278 running: false,
2279 mode: "normal",
2280 collaborationMode: "normal",
2281 toolApprovalMode: "ask",
2282 tokenMode: "full",
2283 active: !guidanceMock,
2284 cwd: "~/projects/joyquant-db",
2285 },
2286 {
2287 id: "tab_joyquant_sys",
2288 scope: "project",
2289 workspaceRoot: "~/projects/joyquant-sys",
2290 workspaceName: "joyquant-sys",
2291 workspacePath: "~/projects/joyquant-sys",
2292 gitBranch: "feature/p3b",
2293 topicId: "topic_p3b_pd",
2294 topicTitle: "p3b P&D",
2295 projectColor: "purple",
2296 label: "DeepSeek-R1",
2297 ready: true,
2298 running: runningMock && mockTopicIsRunning("topic_p3b_pd"),
2299 mode: "normal",
2300 collaborationMode: "normal",
2301 toolApprovalMode: "ask",
2302 tokenMode: "full",
2303 active: guidanceMock,
2304 cwd: "~/projects/joyquant-sys",
2305 },
2306 {
2307 id: "tab_global",
2308 scope: "global",
2309 workspaceRoot: globalWorkspaceRoot,
2310 workspaceName: "Global",
2311 workspacePath: globalWorkspaceRoot,
2312 topicId: "topic_global",
2313 topicTitle: "Global",
2314 label: "DeepSeek-R1",
2315 ready: true,
2316 running: false,
2317 mode: "normal",
2318 collaborationMode: "normal",
2319 toolApprovalMode: "ask",
2320 tokenMode: "full",
2321 active: false,
2322 cwd: globalWorkspaceRoot,
2323 },
2324 ];
2325 if (sandboxEscapeMock) {
2326 window.setTimeout(() => {
2327 if (pendingApprovalPreview) return;
2328 pendingApprovalPreview = true;
2329 pendingApprovalPreviewPrompt = { id: "mock-sandbox-escape-preview", tool: "sandbox_escape" };
2330 emitMockTurnStarted();
2331 emit({ kind: "reasoning", text: t("mock.sandboxEscapeReasoning") });
2332 emitMockPrompt({
2333 kind: "approval_request",
2334 approval: {
2335 id: "mock-sandbox-escape-preview",
2336 tool: "sandbox_escape",
2337 subject: t("mock.sandboxEscapeSubject"),
2338 reason: t("mock.sandboxEscapeReason"),
2339 },
2340 });
2341 }, 800);
2342 }
2343 const mockModelCatalog = [
2344 { ref: "deepseek/deepseek-v4-flash", provider: "deepseek", model: "deepseek-v4-flash" },
2345 { ref: "deepseek/deepseek-v4-pro", provider: "deepseek", model: "deepseek-v4-pro" },
2346 ];
2347 const defaultMockModelRef = mockModelCatalog[0].ref;
2348 const mockModelRef = (name: string): string => {
2349 const trimmed = name.trim();
2350 if (!trimmed || trimmed === "DeepSeek-R1") return defaultMockModelRef;
2351 const exact = mockModelCatalog.find((model) => model.ref === trimmed);
2352 if (exact) return exact.ref;
2353 const byModel = mockModelCatalog.find((model) => model.model === trimmed);
2354 return byModel?.ref ?? trimmed;
2355 };
2356 const mockModelLabel = (ref: string): string => mockModelCatalog.find((model) => model.ref === mockModelRef(ref))?.model ?? ref.split("/").pop() ?? ref;
2357 const mockTabModelRef = (tab?: TabMeta): string => mockModelRef(tab?.label ?? "");
2358 let mockTerminalSessions: TerminalSessionView[] = [];
2359 const mockTerminalOutput = new Map<string, string>();
2360 const mockTerminalTabIDs = new Map<string, string>();
2361 const mockTerminalBytes = (text: string): string => {
2362 if (typeof btoa === "function") return btoa(unescape(encodeURIComponent(text)));
2363 return "";
2364 };
2365 const setMockTabModel = (tabID: string | undefined, name: string) => {
2366 const ref = mockModelRef(name);
2367 const label = mockModelLabel(ref);
2368 let applied = false;
2369 mockTabs = mockTabs.map((tab) => {
2370 const match = tabID ? tab.id === tabID : tab.active;
2371 if (!match) return tab;
2372 applied = true;
2373 return { ...tab, label };
2374 });
2375 if (!applied && mockTabs.length > 0) {
2376 mockTabs = mockTabs.map((tab, index) => (index === 0 ? { ...tab, label } : tab));
2377 }
2378 };
2379 const mockArchivedSessionIDs = new Set<string>();
2380 const mockPurgedSessionIDs = new Set<string>();
2381 const mockSessionIDForNode = (node: ProjectNode) => (node.topicId || node.key || "mock-session").replace(/[^a-zA-Z0-9._-]/g, "-");
2382 const mockWorkspaceID = (node: ProjectNode) => node.kind === "global_folder" ? "global" : `project-${(node.root || node.key).replace(/[^a-zA-Z0-9._-]/g, "-")}`;
2383 const mockWorkspaceSnapshot = (): WorkspaceSnapshot => ({
2384 generation: 1,
2385 workspaces: mockProjectTreeForDisplay().filter((node) => node.kind === "project" || node.kind === "global_folder").map((node) => ({
2386 id: mockWorkspaceID(node), root: node.root || "", title: node.label,
2387 sessionIds: projectChildren(node).map(mockSessionIDForNode), visible: true, createdAt: 0, updatedAt: 0,
2388 })),
2389 archivedSessionIds: [...mockArchivedSessionIDs], pendingCreates: [],
2390 });
2391 return { ...({} as AttachmentBindings),
2392 async OpenSessionDraft(workspaceId: string) {
2393 const prior = [...mockDrafts.values()].find((draft) => draft.workspaceId === workspaceId && draft.status === "active");
2394 if (prior) return structuredClone(prior);
2395 return structuredClone(mockDraftForTarget(workspaceId === "global" ? "global" : "project", workspaceId === "global" ? "" : workspaceId));
2396 },
2397 async OpenSessionDraftForTarget(scope: string, workspaceRoot: string) {
2398 return structuredClone(mockDraftForTarget(scope, workspaceRoot));
2399 },
2400 async RestoreSessionDraft() {
2401 return structuredClone([...mockDrafts.values()].find((draft) => draft.status === "active") ?? null);
2402 },
2403 async SaveSessionDraft(request: SessionDraftSaveRequest) {
2404 const current = mockDrafts.get(request.draftId);
2405 if (!current || current.status !== "active") throw new Error("session draft not found");
2406 if (current.revision !== request.revision) return { draft: structuredClone(current), conflict: true, outcome: "conflict" };
2407 const next = { ...current, revision: current.revision + 1, contentJson: request.contentJson, settings: structuredClone(request.settings), updatedAt: Date.now() };
2408 mockDrafts.set(next.id, next);
2409 return { draft: structuredClone(next), conflict: false, outcome: "saved" };
2410 },
2411 async ListSessionDraftSummaries() {
2412 return [...mockDrafts.values()].filter((draft) => draft.status === "active").map((draft) => ({
2413 id: draft.id, workspaceId: draft.workspaceId, scope: draft.scope, workspaceRoot: draft.workspaceRoot,
2414 revision: draft.revision, hasContent: draft.contentJson !== "{}", state: "saved", updatedAt: draft.updatedAt,
2415 }));
2416 },
2417 async DiscardSessionDraft(draftId: string, revision: number) {
2418 const current = mockDrafts.get(draftId);
2419 if (!current || current.revision !== revision) throw new Error("session draft revision conflict");
2420 mockDrafts.set(draftId, { ...current, status: "discarded", revision: current.revision + 1 });
2421 },
2422 async DismissSessionDraft(_draftId: string) {},
2423 async SetSessionDraftRestoreTarget(_draftId: string) {},
2424 async GetSessionDraft(draftId: string) {
2425 const draft = mockDrafts.get(draftId);
2426 if (!draft) throw new Error("session draft not found");
2427 return structuredClone(draft);
2428 },
2429 async GetDraftContext(draftId: string) {
2430 const draft = mockDrafts.get(draftId);
2431 if (!draft) throw new Error("session draft not found");
2432 return { draft: structuredClone(draft), commands: [], servers: [] };
2433 },
2434 async GetSessionDraftState(draftId: string) {
2435 const draft = mockDrafts.get(draftId);
2436 if (!draft) throw new Error("session draft not found");
2437 const operation = [...mockDraftOperations.values()].reverse().find(item => item.draftId === draftId);
2438 return structuredClone({ draft, operation });
2439 },
2440 async ResumeDraftSubmission(operationId: string, revision: number) {
2441 const operation = mockDraftOperations.get(operationId);
2442 if (!operation || operation.revision !== revision) throw new Error("operation revision conflict");
2443 return structuredClone(operation);
2444 },
2445 async BeginDraftSubmission(request: SessionDraftSubmissionRequest) {
2446 const draft = mockDrafts.get(request.draftId);
2447 if (!draft || draft.revision !== request.revision) throw new Error("session draft revision conflict");
2448 const operationId = `draft-op-mock-${mockDraftOperations.size + 1}`;
2449 const sessionId = `draft-session-mock-${mockDraftOperations.size + 1}`;
2450 const view: SessionDraftSubmissionView = { operationId, draftId: draft.id, requestId: request.requestId, revision: 1, canResume: false, canEdit: false, canCancel: false, canDiscard: false, phase: "accepted", submissionId: `draft-submit-mock-${mockDraftOperations.size + 1}`, session: { hostId: "local", sessionId }, updatedAt: Date.now() };
2451 mockDraftOperations.set(operationId, view);
2452 mockDrafts.set(draft.id, { ...draft, status: "converted", revision: draft.revision + 1 });
2453 return structuredClone(view);
2454 },
2455 async GetDraftSubmission(operationId: string) {
2456 const operation = mockDraftOperations.get(operationId);
2457 if (!operation) throw new Error("session draft submission not found");
2458 return structuredClone(operation);
2459 },
2460 async CancelDraftSubmission(operationId: string) {
2461 const operation = mockDraftOperations.get(operationId);
2462 if (!operation) throw new Error("session draft submission not found");
2463 const next = { ...operation, phase: operation.phase === "accepted" ? "accepted" : "cancelled", updatedAt: Date.now() };
2464 mockDraftOperations.set(operationId, next);
2465 return structuredClone(next);
2466 },
2467 ...makeMockSessionExportBindings(),
2468 ...makeMockSessionCatalogBindings(cloneProjectTree),
2469 ...makeMockBlankProjectBindings(),
2470 async GetWorkspaceSnapshot() { return mockWorkspaceSnapshot(); },
2471 async GetLegacyEmptySessionCleanupStatus() {
2472 return { version: 1, state: "complete", removed: 0, pending: 0, busy: 0, unknown: 0, protected: 0, hasContent: 0, items: [] };
2473 },
2474 async RetryLegacyEmptySessionCleanup() {
2475 return { version: 1, state: "complete", removed: 0, pending: 0, busy: 0, unknown: 0, protected: 0, hasContent: 0, items: [] };
2476 },
2477 ...makeMockSessionLifecycleBindings(mockWorkspaceSnapshot, mockArchivedSessionIDs, mockPurgedSessionIDs, notifyMockProjectTreeChanged),
2478 async CreateSession(_workspaceId: string) { return { hostId: "local", sessionId: `mock-${Date.now()}` }; },
2479 async ForkSession(_ref: SessionRef, _turnBoundary: string) { return { hostId: "local", sessionId: `mock-fork-${Date.now()}` }; },
2480 async ForkSessionTarget(_selector: SessionSelector, _turnBoundary: string) { return { hostId: "local", sessionId: `mock-fork-${Date.now()}` }; },
2481 async GetSessionActivityBaseline(selector: SessionSelector) {
2482 return { ref: selector.ref ?? { hostId: "local", sessionId: "" }, resultSequence: 0, eventVersion: "", lifecycleGeneration: 0, complete: false };
2483 },
2484 async CopySessionTarget(_selector: SessionSelector, operationId: string): Promise<SessionCreationResult> {
2485 return {
2486 ref: { hostId: "local", sessionId: `mock-copy-${operationId || Date.now()}` },
2487 operationId: operationId || `mock-copy-operation-${Date.now()}`,
2488 committed: true,
2489 };
2490 },
2491 async HistorySliceForTarget(_selector: SessionSelector, _request: HistorySliceRequest): Promise<HistorySlice> {
2492 return { entries: [], nextCursor: "", hasOlder: false, totalTurns: 0, startTurn: 0, endTurn: 0, stale: false, revision: 0 };
2493 },
2494 async HistoryContentForTarget(_selector: SessionSelector, ref: HistoryContentRef, chunkIndex: number): Promise<HistoryContentChunk> {
2495 return {
2496 entryId: ref.entryId, field: ref.field, chunk: Math.max(0, chunkIndex),
2497 chunks: 1, data: "", done: true, stale: false,
2498 };
2499 },
2500 async SearchHistoryContentForTarget(_selector: SessionSelector, _query: string, _cursor: string, _limit: number): Promise<HistorySearchPage> {
2501 return {
2502 items: [], nextCursor: "", revision: 0, partial: false, staleCursor: false,
2503 status: { state: "ready", mode: "incremental", total: 0, indexed: 0, pending: 0, failed: 0, revision: 0 },
2504 };
2505 },
2506 async SessionHistoryPageForTarget(_selector: SessionSelector, _cursor: string, _limit: number): Promise<MessageHistoryPage> {
2507 return { messages: [], snapshotSequence: 0, coverageSequence: 0, status: "ready", totalTurns: 0, generation: "mock", hasMore: false };
2508 },
2509 async SessionHistoryContentForTarget(_selector: SessionSelector, ref: SessionContentRef, offset: number): Promise<SessionHistoryContentChunk> {
2510 return { data: "", nextOffset: Math.min(Math.max(0, offset), ref.bytes), done: offset >= ref.bytes };
2511 },
2512 async LocateSessionMessageForTarget(_selector: SessionSelector, messageId: string, snapshot: number): Promise<MessageLocation> {
2513 return { messageId, snapshotSequence: snapshot, coverageSequence: snapshot, status: "ready", position: 0, visibleTurn: 0 };
2514 },
2515 async SessionMessageFieldForTarget(
2516 _selector: SessionSelector,
2517 messageId: string,
2518 version: number,
2519 field: string,
2520 offset: number,
2521 _length: number,
2522 ): Promise<MessageFieldPage> {
2523 return { status: "ready", messageId, version, field, totalBytes: 0, offset, data: "", nextOffset: offset, encoding: "utf8" };
2524 },
2525 async SearchSessionHistoryForTarget(_selector: SessionSelector, _query: string, _cursor: string, _limit: number): Promise<SearchHistoryPage> {
2526 return { hits: [], snapshotSequence: 0, coverageSequence: 0, status: "ready", hasMore: false };
2527 },
2528 async SessionHistoryWindowForTarget(_selector: SessionSelector, _request: HistoryWindowRequest): Promise<HistoryWindowPage> {
2529 return { messages: [], status: "ready", snapshotSequence: 0, coverageSequence: 0, totalTurns: 0, hasOlder: false, hasNewer: false };
2530 },
2531 async ListWorkspaceSessions(workspaceId: string, query: string, _cursor: string, limit: number, includeArchived: boolean) {
2532 const parent = mockProjectTreeForDisplay().find((node) => mockWorkspaceID(node) === workspaceId);
2533 const needle = query.trim().toLowerCase();
2534 const sessions = projectChildren(parent ?? { key: "", kind: "global_folder", label: "", children: [] }).map((node) => {
2535 const sessionId = mockSessionIDForNode(node), isArchived = mockArchivedSessionIDs.has(sessionId);
2536 return {
2537 ref: { hostId: "local", sessionId }, workspaceId, title: node.label, preview: node.preview || "", turns: node.turns || 0,
2538 createdAt: node.createdAt || 0, updatedAt: node.lastActivityAt || 0, blank: !node.turns && !node.preview,
2539 archived: isArchived, running: Boolean(node.running), metadataStatus: "ready", health: "healthy",
2540 };
2541 }).filter((row) => !mockPurgedSessionIDs.has(row.ref.sessionId) && (includeArchived || !row.archived) && (!needle || `${row.title}\n${row.preview}`.toLowerCase().includes(needle))).slice(0, limit);
2542 return { sessions, registryGeneration: 1 };
2543 },
2544 async OpenSession(ref: SessionRef) {
2545 if (ref.hostId !== "local") throw new Error(`unsupported mock session host: ${ref.hostId}`);
2546 const { rebindMockSessionTab } = await import("./bridgeMockSessionNavigation");
2547 const rebound = rebindMockSessionTab(ref, mockTabs.find((candidate) => candidate.active) ?? mockTabs[0], mockProjectTreeForDisplay(), globalWorkspaceRoot, mockTopicRunsInScenario);
2548 // SessionRef navigation rebinds the existing surface; legacy Topic
2549 // activation swaps tab ids and makes its ready event correctly stale.
2550 pruneMockTabsTo(rebound.id);
2551 mockTabs = [rebound];
2552 emitMockReady(rebound.id);
2553 return this.HistoryPageForTab(rebound.id, 0, 60);
2554 },
2555 async ReadSessionHistory(_ref: SessionRef, _cursor: string, _limit: number) { return { messages: [], startTurn: 0, endTurn: 0, totalTurns: 0, hasOlder: false }; },
2556 async RenameCanonicalSession(_ref: SessionRef, _title: string) {},
2557 async SetSessionPinned(_selector: SessionSelector, _pinned: boolean) { notifyMockProjectTreeChanged(); },
2558 async ArchiveCanonicalSession(ref: SessionRef) { mockArchivedSessionIDs.add(ref.sessionId); notifyMockProjectTreeChanged(); },
2559 async ArchiveSessionTarget(selector: SessionSelector): Promise<SessionMutationResult> {
2560 const node = mockSessionTitleTarget(mockProjectTree, selector);
2561 const targetKey = node ? sessionTitleTarget(node) : selector.sessionPath?.trim() || selector.topicId?.trim() || selector.ref?.sessionId || "";
2562 if (selector.ref?.sessionId) mockArchivedSessionIDs.add(selector.ref.sessionId);
2563 if (selector.sessionPath) await this.DeleteSession(selector.sessionPath);
2564 notifyMockProjectTreeChanged();
2565 return { targetKey, operationId: `mock-archive-${Date.now()}`, committed: true, lifecycleGeneration: 2 };
2566 },
2567 async RestoreCanonicalSession(ref: SessionRef) { mockArchivedSessionIDs.delete(ref.sessionId); notifyMockProjectTreeChanged(); },
2568 async RestoreSessionTarget(selector: SessionSelector): Promise<SessionMutationResult> {
2569 const node = mockSessionTitleTarget(mockProjectTree, selector);
2570 const targetKey = node ? sessionTitleTarget(node) : selector.sessionPath?.trim() || selector.topicId?.trim() || selector.ref?.sessionId || "";
2571 if (selector.ref?.sessionId) mockArchivedSessionIDs.delete(selector.ref.sessionId);
2572 notifyMockProjectTreeChanged();
2573 return { targetKey, operationId: `mock-restore-${Date.now()}`, committed: true, lifecycleGeneration: 3 };
2574 },
2575 async DeleteSessionTarget(selector: SessionSelector): Promise<SessionMutationResult> {
2576 const node = mockSessionTitleTarget(mockProjectTree, selector);
2577 const sessionId = selector.ref?.sessionId || (node ? mockSessionIDForNode(node) : "");
2578 if (sessionId) mockPurgedSessionIDs.add(sessionId);
2579 notifyMockProjectTreeChanged();
2580 return { targetKey: node ? sessionTitleTarget(node) : sessionId, operationId: `mock-delete-${Date.now()}`, committed: true, lifecycleGeneration: 4 };
2581 },
2582 async MoveWorkspaceSession(_workspaceId: string, _sessionId: string, _beforeSessionId: string) {},
2583 async MoveSessionTarget(selector: SessionSelector, _workspaceId: string, _beforeSessionId: string): Promise<SessionMutationResult> {
2584 const node = mockSessionTitleTarget(mockProjectTree, selector);
2585 return { targetKey: node ? sessionTitleTarget(node) : "", operationId: `mock-move-${Date.now()}`, committed: true, lifecycleGeneration: 1 };
2586 },
2587 async RenameWorkspace(_workspaceId: string, _title: string) {},
2588 async SetWorkspaceVisible(_workspaceId: string, _visible: boolean) {},
2589 async MoveWorkspace(_workspaceId: string, _beforeWorkspaceId: string) {},
2590 async GetSessionArchitectureDiagnostics() {
2591 return {
2592 pending_operations: 0, missing_members: 0, identity_mismatches: 0,
2593 source_conflicts: 0, recovery_entries: 0,
2594 session_headers_total: 0, workspace_members_total: 0, unassigned_sessions: 0,
2595 migration_pending: 0, migration_failed: 0, migration_completed: 0,
2596 projection_pending: 0, projection_failed: 0, pending_create_recovered: 0,
2597 prune_blocked_persistence: 0,
2598 };
2599 },
2600 async MinimiseMainWindow() {
2601 console.info("mock MinimiseMainWindow");
2602 },
2603 async ToggleMaximiseMainWindow() {
2604 console.info("mock ToggleMaximiseMainWindow");
2605 },
2606 async IsMainWindowMaximised() {
2607 return false;
2608 },
2609 async CloseMainWindow() {
2610 console.info("mock CloseMainWindow");
2611 },
2612 async Platform() {
2613 const override = browserPlatformOverride();
2614 if (override) return override;
2615 // Mirror the OS the browser dev mock runs on.
2616 const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
2617 if (/Win/i.test(ua)) return "windows";
2618 if (/Mac/i.test(ua)) return "darwin";
2619 return "linux";
2620 },
2621 async Submit(input, submissionID?: string) {
2622 cancelled = false;
2623 emitMockTurnStarted(submissionID);
2624 const trimmedInput = input.trim().toLowerCase();
2625 // Deterministic browser QA for the real recovery/source components.
2626 if (trimmedInput === "/mock-protocol-recovery") {
2627 setMockTabRunning(currentMockTurnTabId(), false);
2628 emit({kind:"turn_done",err:"上游拒绝了请求,但未提供具体原因",protocolRecovery:{id:"mock-protocol-once"}});
2629 return;
2630 }
2631 if (trimmedInput === "/recover-context mock-protocol-once") {
2632 await delay(1500);
2633 if (cancelled) return;
2634 emit({kind:"tool_dispatch",tool:{id:"mock-search",name:"web_search",args:'{"query":"OpenCode documentation"}',readOnly:true}});
2635 emit({kind:"tool_result",tool:{id:"mock-search",name:"web_search",args:'{"query":"OpenCode documentation"}',readOnly:true,output:JSON.stringify({summary:"搜索摘要仍然可读。",sources:[],sources_status:"not_provided"})}});
2636 emit({kind:"notice",level:"info",code:"search_sources_not_provided",text:"搜索已完成,供应商未提供可用的结构化来源"});
2637 emitMockTurnDone(submissionID);
2638 return;
2639 }
2640 const decisionSurfaceMock = decisionSurfaceMockFromInput(trimmedInput);
2641 const goalMatch = /^\/goal(?:\s+([\s\S]*))?$/.exec(input.trim());
2642 if (goalMatch) {
2643 const arg = stripLegacyGoalBudgetFlags((goalMatch[1] ?? "").trim());
2644 const lowered = arg.toLowerCase();
2645 const active = mockTabs.find((tab) => tab.active);
2646 if (!arg || lowered === "status") {
2647 emit({ kind: "notice", level: "info", text: active?.goal ? `goal: ${active.goal}` : "goal: none" });
2648 emitMockTurnDone(submissionID);
2649 return;
2650 }
2651 if (["clear", "off", "stop", "done"].includes(lowered)) {
2652 mockTabs = mockTabs.map((tab) => (tab.active ? { ...tab, goal: "", goalStatus: "stopped", collaborationMode: "normal" } : tab));
2653 emit({ kind: "notice", level: "info", text: "goal cleared" });
2654 emitMockTurnDone(submissionID);
2655 return;
2656 }
2657 mockTabs = mockTabs.map((tab) => (tab.active ? { ...tab, goal: arg, goalStatus: "running", collaborationMode: "goal" } : tab));
2658 emit({ kind: "notice", level: "info", text: `goal set: ${arg}` });
2659 await delay(350);
2660 if (cancelled) return;
2661 const reply = `Autonomous goal run started for: **${arg}**\n\nMock run completed.\n\n[goal:complete]`;
2662 emit({ kind: "message", text: reply });
2663 mockTabs = mockTabs.map((tab) => (tab.active ? { ...tab, goal: "", goalStatus: "complete", collaborationMode: "normal" } : tab));
2664 emit({ kind: "notice", level: "info", text: "goal complete" });
2665 emitMockTurnDone(submissionID);
2666 return;
2667 }
2668 if (decisionSurfaceMock === "mcp_interaction") return (await import("./mockMCPInteraction")).showMockMCPInteraction(delay, () => cancelled, emitMockPrompt);
2669 if (decisionSurfaceMock === "tool_approval") {
2670 pendingApprovalPreview = true;
2671 pendingApprovalPreviewPrompt = { id: "mock-approval-preview", tool: "bash" };
2672 await delay(250);
2673 if (cancelled) return;
2674 emitMockPrompt({
2675 kind: "approval_request",
2676 approval: {
2677 id: "mock-approval-preview",
2678 tool: "bash",
2679 subject: t("mock.approvalSubject"),
2680 },
2681 });
2682 return;
2683 }
2684 if (trimmedInput === "/recovery-preview" || trimmedInput === "recovery preview" || trimmedInput === "恢复预览") {
2685 pendingApprovalPreview = true;
2686 pendingApprovalPreviewPrompt = { id: "mock-recovery-preview", tool: "write_file" };
2687 await delay(250);
2688 if (cancelled) return;
2689 emitMockPrompt({
2690 kind: "approval_request",
2691 approval: {
2692 id: "mock-recovery-preview",
2693 tool: "write_file",
2694 subject: "internal/recovery/gate.go",
2695 reason: "The proposed recovery changes the implementation method after a failing verification.",
2696 fresh: true,
2697 kind: "recovery",
2698 recovery: {
2699 source_agent: "root",
2700 failed_tool: "bash",
2701 failed_summary: "go test ./internal/recovery failed",
2702 diagnosis: "The failure is isolated to recovery state persistence.",
2703 next_tool: "write_file",
2704 next_action: "Update internal/recovery/gate.go",
2705 change_kind: "strategy",
2706 change_rationale: "The proposed edit changes the recovery method and needs a fresh decision.",
2707 plan_before: [
2708 "1. Keep the existing Auto execution path [in_progress]",
2709 "2. Add execution-risk approval prompts [pending]",
2710 "3. Run the recovery regression suite [pending]",
2711 ].join("\n"),
2712 plan_after: [
2713 "1. Keep the existing Auto execution path [in_progress]",
2714 "2. Ask only when strategy or scope changes [pending]",
2715 "3. Show the old and proposed plan before deciding [pending]",
2716 "4. Run the recovery regression suite [pending]",
2717 ].join("\n"),
2718 },
2719 },
2720 });
2721 return;
2722 }
2723 if (
2724 trimmedInput === "/sandbox-escape-preview" ||
2725 trimmedInput === "sandbox escape preview" ||
2726 trimmedInput === "sandbox_escape preview" ||
2727 trimmedInput === "sandbox escape预览"
2728 ) {
2729 pendingApprovalPreview = true;
2730 pendingApprovalPreviewPrompt = { id: "mock-sandbox-escape-preview", tool: "sandbox_escape" };
2731 await delay(250);
2732 if (cancelled) return;
2733 emitMockPrompt({
2734 kind: "approval_request",
2735 approval: {
2736 id: "mock-sandbox-escape-preview",
2737 tool: "sandbox_escape",
2738 subject: t("mock.sandboxEscapeSubject"),
2739 reason: t("mock.sandboxEscapeReason"),
2740 },
2741 });
2742 return;
2743 }
2744 if (decisionSurfaceMock === "plan_approval") {
2745 pendingApprovalPreview = true;
2746 pendingApprovalPreviewPrompt = { id: "mock-plan-approval-preview", tool: "exit_plan_mode" };
2747 await delay(250);
2748 if (cancelled) return;
2749 emitMockPrompt({
2750 kind: "approval_request",
2751 approval: {
2752 id: "mock-plan-approval-preview",
2753 tool: "exit_plan_mode",
2754 subject: "",
2755 },
2756 });
2757 return;
2758 }
2759 if (isLongDecisionOptionsMockInput(trimmedInput)) {
2760 pendingAskPreview = true;
2761 await delay(250);
2762 if (cancelled) return;
2763
2764 const longDescription = (...parts: string[]) => [...parts, ...parts, ...parts].join(" ");
2765 const longLabel = (...parts: string[]) => [...parts, ...parts, ...parts].join(" · ");
2766 const q1Option1Description = t("mock.askQ1Opt1Desc");
2767 const q1Option2Description = t("mock.askQ1Opt2Desc");
2768 const q1Option3Description = t("mock.askQ1Opt3Desc");
2769 const q2Option1Description = t("mock.askQ2Opt1Desc");
2770 const q2Option2Description = t("mock.askQ2Opt2Desc");
2771 const q2Option3Description = t("mock.askQ2Opt3Desc");
2772 const allowOnceDescription = t("approval.allowOnceDesc");
2773 const denyDescription = t("approval.denyDesc");
2774
2775 emitMockPrompt({
2776 kind: "ask_request",
2777 ask: {
2778 id: "mock-long-options",
2779 questions: [
2780 {
2781 id: "long-options",
2782 header: `${t("mock.askQ1Header")} · QA`,
2783 prompt: `${t("mock.askQ1Prompt")} ${t("mock.askQ2Prompt")}`,
2784 options: [
2785 {
2786 label: t("mock.askQ1Opt1Label"),
2787 description: longDescription(q1Option1Description, q2Option1Description, allowOnceDescription),
2788 },
2789 {
2790 label: t("mock.askQ1Opt2Label"),
2791 description: longDescription(q1Option2Description, q2Option2Description, denyDescription),
2792 },
2793 {
2794 label: t("mock.askQ1Opt3Label"),
2795 description: longDescription(q1Option3Description, q2Option3Description, allowOnceDescription),
2796 },
2797 {
2798 // Deliberately omit description here: this exercises the
2799 // legacy/malformed payload fallback where the complete
2800 // decision was placed in label instead of split into a
2801 // short label plus supporting description.
2802 label: longLabel(
2803 t("mock.askQ2Opt1Label"),
2804 t("mock.askQ1Opt3Label"),
2805 t("mock.askQ2Opt3Label"),
2806 t("mock.askQ1Opt1Label"),
2807 ),
2808 },
2809 {
2810 label: t("mock.askQ2Opt2Label"),
2811 description: longDescription(
2812 q2Option2Description,
2813 "DecisionSurfacePreviewWithAnExtremelyLongUnbrokenIdentifierForOverflowVerification0123456789",
2814 q1Option1Description,
2815 ),
2816 },
2817 {
2818 label: t("mock.askQ2Opt3Label"),
2819 description: longDescription(q2Option3Description, q1Option1Description, q2Option2Description),
2820 },
2821 {
2822 label: t("approval.deny"),
2823 description: longDescription(denyDescription, q1Option2Description, q2Option1Description),
2824 },
2825 ],
2826 },
2827 ],
2828 },
2829 });
2830 return;
2831 }
2832 if (decisionSurfaceMock === "ask") {
2833 pendingAskPreview = true;
2834 await delay(250);
2835 if (cancelled) return;
2836 emitMockPrompt({
2837 kind: "ask_request",
2838 ask: {
2839 id: `mock-ask-preview-${Date.now()}`,
2840 questions: [
2841 {
2842 id: "q1",
2843 header: t("mock.askQ1Header"),
2844 prompt: t("mock.askQ1Prompt"),
2845 options: [
2846 { label: t("mock.askQ1Opt1Label"), description: t("mock.askQ1Opt1Desc") },
2847 { label: t("mock.askQ1Opt2Label"), description: t("mock.askQ1Opt2Desc") },
2848 { label: t("mock.askQ1Opt3Label"), description: t("mock.askQ1Opt3Desc") },
2849 ],
2850 },
2851 {
2852 id: "q2",
2853 header: t("mock.askQ2Header"),
2854 prompt: t("mock.askQ2Prompt"),
2855 options: [
2856 { label: t("mock.askQ2Opt1Label"), description: t("mock.askQ2Opt1Desc") },
2857 { label: t("mock.askQ2Opt2Label"), description: t("mock.askQ2Opt2Desc") },
2858 { label: t("mock.askQ2Opt3Label"), description: t("mock.askQ2Opt3Desc") },
2859 ],
2860 },
2861 ],
2862 },
2863 });
2864 return;
2865 }
2866 if (trimmedInput === "/todo-preview" || trimmedInput === "todo preview" || trimmedInput === "todo预览") {
2867 await delay(250);
2868 if (cancelled) return;
2869 emit({
2870 kind: "tool_dispatch",
2871 tool: {
2872 id: "mock-todo-preview",
2873 name: "todo_write",
2874 args: JSON.stringify({
2875 todos: [
2876 { content: t("mock.todo1"), status: "completed" },
2877 { content: t("mock.todo2"), status: "in_progress" },
2878 { content: t("mock.todo3"), status: "pending" },
2879 ],
2880 }),
2881 readOnly: false,
2882 },
2883 });
2884 await delay(150);
2885 emit({
2886 kind: "tool_result",
2887 tool: {
2888 id: "mock-todo-preview",
2889 name: "todo_write",
2890 args: JSON.stringify({
2891 todos: [
2892 { content: t("mock.todo1"), status: "completed" },
2893 { content: t("mock.todo2"), status: "in_progress" },
2894 { content: t("mock.todo3"), status: "pending" },
2895 ],
2896 }),
2897 output: "todo list updated",
2898 readOnly: false,
2899 durationMs: 150,
2900 },
2901 });
2902 emitMockTurnDone(submissionID);
2903 return;
2904 }
2905 if (trimmedInput === "/process-preview" || trimmedInput === "process preview" || trimmedInput === "过程预览") {
2906 await delay(200);
2907 if (cancelled) return;
2908 emit({ kind: "phase", text: "Preparing context" });
2909 await delay(120);
2910 emit({ kind: "notice", level: "info", text: "Loaded project instructions from AGENTS.md." });
2911 await delay(120);
2912 emit({ kind: "notice", level: "warn", text: "Network access is enabled; external results may change over time." });
2913 await delay(120);
2914 emit({ kind: "compaction_started", compaction: { trigger: "manual" } });
2915 await delay(320);
2916 emit({
2917 kind: "compaction_done",
2918 compaction: {
2919 trigger: "manual",
2920 messages: 6,
2921 summary: "Preserved the active task, relevant files, and UI decisions while trimming earlier exploratory context.",
2922 },
2923 });
2924 emit({ kind: "message", text: "Process card preview complete." });
2925 emitMockTurnDone(submissionID);
2926 return;
2927 }
2928 if (trimmedInput === "/nested-preview" || trimmedInput === "nested preview" || trimmedInput === "嵌套预览") {
2929 const parentId = "mock-nested-explore";
2930 await delay(180);
2931 if (cancelled) return;
2932 emit({
2933 kind: "reasoning",
2934 text: "我先快速探索相关文件,再整理这个工具行的视觉层级。",
2935 });
2936 emit({
2937 kind: "message",
2938 text: "",
2939 reasoning: "我先快速探索相关文件,再整理这个工具行的视觉层级。",
2940 });
2941 emit({
2942 kind: "tool_dispatch",
2943 tool: {
2944 id: parentId,
2945 name: "explore",
2946 args: JSON.stringify({ task: "在 Reasonix 前端中检查工具调用图标和嵌套调用展示" }),
2947 readOnly: true,
2948 profile: { model: "mock-reasonix", effort: "high" },
2949 },
2950 });
2951 for (let i = 1; i <= 30; i += 1) {
2952 if (cancelled) return;
2953 const id = `mock-nested-${i}`;
2954 const isSearch = i % 3 === 0;
2955 const name = isSearch ? "grep" : "read_file";
2956 const args = isSearch
2957 ? { pattern: i % 2 === 0 ? "tool__nested-count" : "explore", path: "desktop/frontend/src" }
2958 : { path: `desktop/frontend/src/${i % 2 === 0 ? "components/ToolCard.tsx" : "styles.css"}`, offset: i * 10, limit: 40 };
2959 emit({ kind: "tool_dispatch", tool: { id, name, args: JSON.stringify(args), readOnly: true, parentId } });
2960 emit({
2961 kind: "tool_result",
2962 tool: {
2963 id,
2964 name,
2965 readOnly: true,
2966 output: isSearch ? "3 matches" : "read 40 lines",
2967 durationMs: 24 + i,
2968 },
2969 });
2970 await delay(18);
2971 }
2972 emit({
2973 kind: "tool_result",
2974 tool: {
2975 id: parentId,
2976 name: "explore",
2977 readOnly: true,
2978 output: "已读 20 个文件 · 搜索 10 个文件",
2979 durationMs: 61510,
2980 },
2981 });
2982 emit({
2983 kind: "message",
2984 text: "Mock nested tool preview complete. The explore row now shows the compass count marker.",
2985 });
2986 emitMockTurnDone(submissionID);
2987 return;
2988 }
2989 // Simulate the server's pre-first-token latency so the deferred user bubble
2990 // and the "un-send on Esc before any reply" path are observable in browser
2991 // dev. Bail if cancelled during the wait — nothing was streamed yet.
2992 await delay(700);
2993 if (cancelled) return;
2994 const reasoningChunks = [
2995 "我先判断这是浏览器预览环境,所以不会调用真实 kernel。\n",
2996 "接着模拟 provider 的 reasoning delta:先展示思考过程,再切到正式回复。\n",
2997 "完成后前端应该把过程区折叠成“已工作 N 秒”。\n",
2998 ];
2999 for (const chunk of reasoningChunks) {
3000 if (cancelled) return;
3001 emit({ kind: "reasoning", reasoning: chunk });
3002 await delay(520);
3003 }
3004 if (cancelled) return;
3005 await delay(260);
3006 const reply =
3007 `You said: **${input}**\n\n` +
3008 "This is the browser dev mock — the real reply comes from the kernel " +
3009 "inside the desktop shell. Here's a fenced block to exercise the editor seam:\n\n" +
3010 "```go\nfunc main() {\n println(\"hello from the mock\")\n}\n```\n";
3011 for (const ch of reply) {
3012 if (cancelled) break;
3013 emit({ kind: "text", text: ch });
3014 await delay(6);
3015 }
3016 emit({ kind: "message", text: reply });
3017 emit({
3018 kind: "tool_dispatch",
3019 tool: {
3020 id: "t1",
3021 name: "edit_file",
3022 args: '{"path":"main.go","old_string":"println(\\"hi\\")","new_string":"println(\\"hello\\")"}',
3023 readOnly: false,
3024 },
3025 });
3026 await delay(350);
3027 emit({
3028 kind: "tool_result",
3029 tool: { id: "t1", name: "edit_file", output: "edited main.go", readOnly: false, durationMs: 350 },
3030 });
3031 emit({
3032 kind: "usage",
3033 usage: {
3034 promptTokens: 1280,
3035 completionTokens: 64,
3036 totalTokens: 1344,
3037 cacheHitTokens: 1024,
3038 cacheMissTokens: 256,
3039 sessionCacheHitTokens: 1024,
3040 sessionCacheMissTokens: 256,
3041 cost: 0.0064,
3042 currency: "¥",
3043 currencyCode: "CNY",
3044 costComplete: true,
3045 displayComplete: true,
3046 displayStatus: "matched",
3047 costQuote: {
3048 original: { amount: "0.0064", currency: "CNY" },
3049 selected: { amount: "0.0064", currency: "CNY" },
3050 estimated: true,
3051 costComplete: true,
3052 displayComplete: true,
3053 complete: true,
3054 displayStatus: "matched",
3055 aggregateMode: "single_currency",
3056 modelRef: "deepseek-official/deepseek-v4-flash",
3057 rateBand: "off_peak",
3058 ratedAt: "2026-08-17T00:30:00Z",
3059 },
3060 },
3061 });
3062 emitMockTurnDone(submissionID);
3063 },
3064 async SubmitToTab(_tabID, input) { await withMockTabScope(_tabID, () => this.Submit(input)); },
3065 async SubmitToTabWithID(_tabID, input, submissionID) { const submit = this.Submit as (value: string, id?: string) => Promise<void>; await withMockTabScope(_tabID, () => submit(input, submissionID)); },
3066 async StartTurnForTab(_tabID, input, submissionID) {
3067 const turnId = `mock-turn-${submissionID}`;
3068 const submit = this.Submit as (value: string, id?: string) => Promise<void>;
3069 void withMockTabScope(_tabID, () => submit(input, submissionID)).catch((error) => {
3070 console.error("mock turn failed", error);
3071 });
3072 return { turnId, status: "started", disposition: "turn_started", runtimeEpoch: "mock", submissionId: submissionID };
3073 },
3074 async SubmitDisplay(_display, input) { await this.Submit(input); },
3075 async SubmitDisplayToTab(_tabID, display, input) { await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input)); },
3076 async SubmitDisplayToTabWithID(_tabID, _display, input, submissionID) { await this.SubmitToTabWithID(_tabID, input, submissionID); },
3077 async SubmitDeliveryRecoveryToTab(_tabID, display, input) { await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input)); },
3078 async SubmitDeliveryRecoveryToTabWithID(_tabID, _display, input, submissionID) { await this.SubmitToTabWithID(_tabID, input, submissionID); },
3079 async SubmitInvocationsToTab(_tabID, display, input, _invocations) { await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input)); },
3080 async SubmitInvocationsToTabWithID(_tabID, _display, input, _invocations, submissionID) { await this.SubmitToTabWithID(_tabID, input, submissionID); },
3081 async SubmitInitialGoalToTab(
3082 _tabID,
3083 goal,
3084 display,
3085 input,
3086 invocations,
3087 _collaborationMode,
3088 _toolApprovalMode,
3089 ) {
3090 return await withMockTabScope(_tabID, async () => {
3091 await this.SetGoalForTab(_tabID, goal);
3092 if (invocations.length > 0) {
3093 await this.SubmitInvocationsToTab(_tabID, display, input, invocations);
3094 return [];
3095 }
3096 await this.SubmitDisplayToTab(_tabID, display, input);
3097 return [];
3098 });
3099 },
3100 async SubmitInitialGoalToTabWithID(_tabID, goal, display, input, invocations, _collaborationMode, _toolApprovalMode, submissionID) { await this.SetGoalForTab(_tabID, goal); if (invocations.length > 0) await this.SubmitInvocationsToTabWithID(_tabID, display, input, invocations, submissionID); else await this.SubmitDisplayToTabWithID(_tabID, display, input, submissionID); return []; },
3101 async SubmitEditedDisplayToTab(_tabID, display, input, _original) { await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input)); },
3102 async SubmitEditedDisplayToTabWithID(_tabID, display, input, _original, submissionID) { await this.SubmitDisplayToTabWithID(_tabID, display, input, submissionID); },
3103 async RunShellForTab(_tabID, command) {
3104 await withMockTabScope(_tabID, async () => {
3105 cancelled = false;
3106 emitMockTurnStarted();
3107 await delay(100);
3108 if (cancelled) return;
3109 const id = `shell-${command.slice(0, 32)}`;
3110 emit({ kind: "tool_dispatch", tool: { id, name: "bash", args: JSON.stringify({ command }), readOnly: false } });
3111 await delay(200);
3112 if (cancelled) return;
3113 emit({ kind: "tool_progress", tool: { id, name: "bash", output: `$ ${command}\n(mock output)\n`, readOnly: false } });
3114 await delay(100);
3115 if (cancelled) return;
3116 emit({ kind: "tool_result", tool: { id, name: "bash", output: `$ ${command}\n(mock output)\n`, readOnly: false, durationMs: 300 } });
3117 emitMockTurnDone();
3118 });
3119 },
3120 async Steer(_text) {
3121 // Mock: emit a steer event as confirmation in the transcript.
3122 emit({ kind: "steer", text: _text });
3123 },
3124 async SteerForTab(_tabID, _text) {
3125 await this.Steer(_text);
3126 },
3127 async InboxSnapshot(_tabID) { if (recoveryMock) return (await import("./inboxRecoveryPreview")).inboxRecoveryPreviewSnapshot();
3128 return {
3129 revision: 0,
3130 paused: false,
3131 recovered: false, sessionPath: "",
3132 items: [],
3133 itemsCount: 0,
3134 bytes: 0,
3135 maxItems: 64,
3136 maxBytes: 64 * 1024 * 1024,
3137 };
3138 },
3139 async EnqueueInboxFollowup(_tabID, _display, _submit, _idempotency) {
3140 return { itemId: `mock-${Date.now()}`, disposition: "queued_followup", position: 1, paused: false };
3141 },
3142 async EnqueueInboxFollowupWithInvocations(_tabID, _display, _submit, _invocations, _idempotency) {
3143 return { itemId: `mock-invocation-${Date.now()}`, disposition: "queued_followup", position: 1, paused: false };
3144 },
3145 async EnqueueInboxSteer(_tabID, display, submit, _idempotency) {
3146 const itemId = `mock-steer-${Date.now()}`;
3147 emit({ kind: "steer", text: submit || display, itemId });
3148 return { itemId, disposition: "steer_accepted", position: 1, paused: false };
3149 },
3150 async SteerInboxItem(_tabID, itemID) {
3151 emit({ kind: "steer", itemId: itemID });
3152 return { itemId: itemID, disposition: "steer_accepted", position: 1, paused: false };
3153 },
3154 async ReadInboxItem(_tabID, id) {
3155 return { id, displayText: "", rawText: "", submitText: "" };
3156 },
3157 async UpdateInboxItem() {},
3158 async DeleteInboxItem() {},
3159 async MoveInboxItem() {},
3160 async SetInboxPaused(_tabID, paused) { if (recoveryMock) (await import("./inboxRecoveryPreview")).setInboxRecoveryPreviewPaused(paused); },
3161 async RetryInboxItem() {},
3162 async RefreshInboxItem() {},
3163 async InboxHasItems() { return recoveryMock; },
3164 async Cancel() {
3165 cancelled = true;
3166 emitMockTurnDone();
3167 },
3168 async CancelTab(_tabID) {
3169 await withMockTabScope(_tabID, () => this.Cancel());
3170 },
3171 async CancelSessionForTab(_tabID) {
3172 await withMockTabScope(_tabID, () => this.Cancel());
3173 return { sessionRef: "", headId: "", runtimeEpoch: "mock", accepted: true, alreadyIdle: false, recoveryRequired: false };
3174 },
3175 async CancelTabWithInboxItems(_tabID, _itemIDs) {
3176 await withMockTabScope(_tabID, () => this.Cancel());
3177 },
3178 async CancelTabWithInboxItemsResult(_tabID, itemIDs) { await withMockTabScope(_tabID, () => this.Cancel()); return { discardedItemIds: [...itemIDs] }; },
3179 async Approve(_id, allow, session, persist) {
3180 if (!pendingApprovalPreview) return;
3181 pendingApprovalPreview = false;
3182 pendingApprovalPreviewPrompt = undefined;
3183 const suffix = persist ? "grant saved" : session ? "grant active this session" : "allowed once";
3184 emit({
3185 kind: "message",
3186 text: `approval preview answered: ${allow ? suffix : "denied"}`,
3187 });
3188 emitMockTurnDone();
3189 },
3190 async ApproveTab(_tabID, id, allow, session, persist) {
3191 await withMockTabScope(_tabID, () => this.Approve(id, allow, session, persist));
3192 },
3193 async ResolvePlanDecision(id, action) {
3194 const active = mockTabs.find((tab) => tab.active);
3195 await this.ResolvePlanDecisionTab(active?.id ?? "", id, action);
3196 },
3197 async ResolvePlanDecisionTab(_tabID, id, action) {
3198 await withMockTabScope(_tabID, async () => {
3199 void id;
3200 pendingApprovalPreview = false;
3201 pendingApprovalPreviewPrompt = undefined;
3202 emit({
3203 kind: "message",
3204 text: `plan preview answered: ${action}`,
3205 });
3206 emitMockTurnDone();
3207 });
3208 },
3209 async ResolveRecovery(id, action, feedback) {
3210 const active = mockTabs.find((tab) => tab.active);
3211 await this.ResolveRecoveryTab(active?.id ?? "", id, action, feedback);
3212 },
3213 async ResolveRecoveryTab(_tabID, id, action, feedback) {
3214 void id;
3215 void feedback;
3216 pendingApprovalPreview = false;
3217 pendingApprovalPreviewPrompt = undefined;
3218 emit({
3219 kind: "message",
3220 text: `recovery preview answered: ${action}`,
3221 });
3222 emitMockTurnDone();
3223 },
3224 async SetRecoveryCheckpointEnabled(_enabled) {},
3225 async SetRecoveryCheckpointEnabledTab(_tabID, _enabled) {},
3226 async RecoveryCheckpointEnabled() {
3227 return true;
3228 },
3229 async RecoveryCheckpointEnabledTab(_tabID) {
3230 return true;
3231 },
3232 async AnswerQuestion(_id, answers) {
3233 if (!pendingAskPreview) return;
3234 pendingAskPreview = false;
3235 const summary = answers
3236 .map((answer) => `${answer.questionId}: ${(answer.selected ?? []).join(", ") || "(no answer)"}`)
3237 .join("\n");
3238 emit({ kind: "message", text: `ask preview answered:\n\n${summary}` });
3239 emitMockTurnDone();
3240 },
3241 async AnswerMCPInteractionForTab(_tabID, id, _action, _content) {
3242 if (!cancelled && (await import("./mockMCPInteraction")).consumeMockMCPInteraction(id)) await withMockTabScope(_tabID, async () => { emit({ kind: "prompt_answered", itemId: id }); emitMockTurnDone(); });
3243 },
3244 ...makeMockMCPAppBindings(), ...makeMockPinnedContextBindings(),
3245 async AnswerQuestionForTab(_tabID, id, answers) {
3246 await withMockTabScope(_tabID, () => this.AnswerQuestion(id, answers));
3247 },
3248 async ResolvePromptForTab(tabID, promptID, _turnID, _runtimeEpoch, kind, answer) {
3249 await withMockTabScope(tabID, async () => {
3250 if (kind === "ask") return this.AnswerQuestion(promptID, answer.questions ?? []);
3251 if (kind === "approval") return this.Approve(promptID, Boolean(answer.allow), Boolean(answer.session), Boolean(answer.persist));
3252 if (kind === "plan") return this.ResolvePlanDecisionTab(tabID, promptID, (answer.action ?? "exit_plan") as "start_execution" | "revise_plan" | "exit_plan");
3253 if (kind === "recovery") return this.ResolveRecoveryTab(tabID, promptID, answer.action ?? "revise", answer.feedback ?? "");
3254 if (kind === "mcp") return this.AnswerMCPInteractionForTab(tabID, promptID, (answer.action ?? "cancel") as "accept" | "decline" | "cancel", answer.content ?? null);
3255 throw new Error(`unsupported prompt kind: ${kind}`);
3256 });
3257 },
3258 async ResolvePromptForSession(target, answer) { await this.ResolvePromptForTab?.(target.tabId, target.promptId, target.turnId, target.runtimeEpoch, target.kind, answer); },
3259 async ReplayPendingPrompts() {},
3260 async ReplayPendingPromptsForTab(_tabID) {},
3261 async ConfirmAction(req) {
3262 void req;
3263 return false;
3264 },
3265 async SetPlanMode(on) {
3266 const active = mockTabs.find((tab) => tab.active);
3267 if (active) await this.SetModeForTab(active.id, modeWithPlan(normalizeMode(active.mode), on));
3268 },
3269 async SetMode(mode) {
3270 const active = mockTabs.find((tab) => tab.active);
3271 if (active) await this.SetModeForTab(active.id, mode);
3272 },
3273 async SetModeForTab(tabID, mode) {
3274 const nextMode = normalizeMode(mode);
3275 let nextToolApprovalMode: ToolApprovalMode | "" = "";
3276 mockTabs = mockTabs.map((tab) => {
3277 if (tab.id !== tabID) return tab;
3278 nextToolApprovalMode = mockToolApprovalModeAfterModeChange(tab.toolApprovalMode, nextMode);
3279 return {
3280 ...tab,
3281 mode: nextMode,
3282 collaborationMode: normalizeCollaborationMode(undefined, tab.goal, nextMode),
3283 toolApprovalMode: nextToolApprovalMode,
3284 };
3285 });
3286 return drainMockApprovalPreviews(nextToolApprovalMode);
3287 },
3288 async SetCollaborationModeForTab(tabID, mode) {
3289 const next = normalizeCollaborationMode(mode);
3290 mockTabs = mockTabs.map((tab) => {
3291 if (tab.id !== tabID) return tab;
3292 const toolMode = normalizeToolApprovalMode(tab.toolApprovalMode, normalizeMode(tab.mode));
3293 return {
3294 ...tab,
3295 collaborationMode: next,
3296 goal: next === "normal" || next === "plan" ? "" : tab.goal,
3297 mode: modeWithPlan(modeWithAutoApproveTools(normalizeMode(tab.mode), toolMode === "danger-full-access"), next === "plan"),
3298 };
3299 });
3300 },
3301 async SetToolApprovalMode(mode) {
3302 const active = mockTabs.find((tab) => tab.active);
3303 if (active) await this.SetToolApprovalModeForTab(active.id, mode);
3304 },
3305 async SetToolApprovalModeForTab(tabID, mode) {
3306 const next = normalizeToolApprovalMode(mode);
3307 settings.autoApproveTools = next === "danger-full-access";
3308 settings.bypass = next === "danger-full-access";
3309 mockTabs = mockTabs.map((tab) =>
3310 tab.id === tabID
3311 ? {
3312 ...tab,
3313 toolApprovalMode: next,
3314 mode: modeWithAutoApproveTools(normalizeMode(tab.mode), next === "danger-full-access"),
3315 }
3316 : tab,
3317 );
3318 return drainMockApprovalPreviews(next);
3319 },
3320 async PermissionSnapshotForTab(tabID) {
3321 const tab = mockTabs.find((candidate) => candidate.id === tabID);
3322 if (!tab) throw new Error("tab not found");
3323 return {
3324 sessionId: tab.sessionPath || tab.id,
3325 generation: 1,
3326 revision: Number((tab as TabMeta & { permissionRevision?: number }).permissionRevision ?? 1),
3327 preset: normalizeToolApprovalMode(tab.toolApprovalMode),
3328 workspaceRoot: tab.workspaceRoot || tab.cwd || "",
3329 grants: [],
3330 capabilities: {
3331 backend: "mock",
3332 enforcement: "full",
3333 supportedPresets: ["read-only", "workspace-write", "danger-full-access"],
3334 },
3335 };
3336 },
3337 async SetPermissionPresetForTab(tabID, preset, expectedRevision) {
3338 const current = await this.PermissionSnapshotForTab(tabID);
3339 if (current.revision !== expectedRevision) throw new Error("permission revision changed");
3340 await this.SetToolApprovalModeForTab(tabID, preset);
3341 mockTabs = mockTabs.map((tab) => tab.id === tabID ? { ...tab, permissionRevision: current.revision + 1 } as TabMeta : tab);
3342 return { ...current, revision: current.revision + 1, preset: normalizeToolApprovalMode(preset) };
3343 },
3344 async RevokePermissionGrantForTab(tabID, _scope, _target, expectedRevision) {
3345 const current = await this.PermissionSnapshotForTab(tabID);
3346 if (current.revision !== expectedRevision) throw new Error("permission revision changed");
3347 return { ...current, revision: current.revision + 1, grants: [] };
3348 },
3349 async SetComposerProfileForTab(tabID, collaborationMode, toolApprovalMode, goal) {
3350 const nextCollaboration = normalizeCollaborationMode(collaborationMode);
3351 const nextToolApproval = normalizeToolApprovalMode(toolApprovalMode);
3352 const nextGoal = goal.trim();
3353 settings.autoApproveTools = nextToolApproval === "danger-full-access";
3354 settings.bypass = nextToolApproval === "danger-full-access";
3355 mockTabs = mockTabs.map((tab) => {
3356 if (tab.id !== tabID) return tab;
3357 const plan = !nextGoal && nextCollaboration === "plan";
3358 return {
3359 ...tab,
3360 collaborationMode: nextGoal ? "goal" : plan ? "plan" : "normal",
3361 toolApprovalMode: nextToolApproval,
3362 goal: nextGoal,
3363 goalStatus: nextGoal ? "running" : "stopped",
3364 mode: modeWithAutoApproveTools(modeWithPlan(normalizeMode(tab.mode), plan), nextToolApproval === "danger-full-access"),
3365 };
3366 });
3367 return drainMockApprovalPreviews(nextToolApproval);
3368 },
3369 async SetGoal(goal) { const active = mockTabs.find((tab) => tab.active); if (active) await this.SetGoalForTab(active.id, goal); },
3370 async SetGoalForTab(tabID, goal) {
3371 const nextGoal = goal.trim();
3372 mockTabs = mockTabs.map((tab) =>
3373 tab.id === tabID
3374 ? {
3375 ...tab,
3376 goal: nextGoal,
3377 goalStatus: nextGoal ? "running" : "stopped",
3378 collaborationMode: nextGoal ? "goal" : "normal",
3379 mode: modeWithPlan(normalizeMode(tab.mode), false),
3380 }
3381 : tab,
3382 );
3383 },
3384 async EditGoalForTab(tabID, objective, maxGoalRounds) { mockTabs = mockTabs.map((tab) => editMockGoalTab(tab, tabID, objective, maxGoalRounds)); },
3385 async ResumeGoalForTab(tabID) {
3386 let resumed = false;
3387 mockTabs = mockTabs.map((tab) => {
3388 if (tab.id !== tabID || !tab.goal || tab.goalStatus === "complete") return tab;
3389 resumed = true;
3390 return { ...tab, goalStatus: "running", collaborationMode: "goal", goalRuntime: undefined };
3391 });
3392 return resumed;
3393 },
3394 async PauseGoalForTab(tabID) {
3395 let paused = false;
3396 mockTabs = mockTabs.map((tab) => {
3397 if (tab.id !== tabID || !tab.goal || tab.goalStatus !== "running") return tab;
3398 paused = true;
3399 return { ...tab, goalStatus: "blocked", goalRuntime: undefined };
3400 });
3401 return paused;
3402 },
3403 async ClearGoalForTab(tabID) { await this.SetGoalForTab(tabID, ""); },
3404 async Compact() {},
3405 async CompactForTab() {},
3406 async NewSession() {},
3407 async NewSessionForTab() {},
3408 async ClearSession() { return { sessionPath: "", sessionGeneration: 0 }; },
3409 async ClearSessionForTab() { return { sessionPath: "", sessionGeneration: 0 }; },
3410 async Checkpoints() {
3411 return [
3412 { turn: 0, prompt: "你好呀", files: ["src/App.tsx"], fileCount: 1, turnFileCount: 1, time: Date.now() - 30_000, canCode: true, canConversation: true },
3413 ];
3414 },
3415 async CheckpointsForTab() { return this.Checkpoints(); },
3416 async Rewind() {},
3417 async RewindForTab() {},
3418 async PreviewRewindForTab() {
3419 return { ok: true, canFiles: true, canConversation: true, planId: "mock", fileCount: 0 };
3420 },
3421 async CommitRewindForTab() {
3422 return { ok: true, undoAvailable: true, transactionId: "mock-tx" };
3423 },
3424 async UndoRewindForTab() {
3425 return { ok: true, undoAvailable: false };
3426 },
3427 async PreviewWorkspaceFileRevertForTab(_tabID, path) {
3428 return { ok: true, canFiles: true, path, planId: "mock-file" };
3429 },
3430 async CommitWorkspaceFileRevertForTab() {
3431 return { ok: true, undoAvailable: true, transactionId: "mock-file-tx" };
3432 },
3433 ...makeMockForkBindings(
3434 () => mockTabs,
3435 (tabs) => { mockTabs = tabs; },
3436 t("rewind.fork"),
3437 (tabID) => getMock().HistoryForTab(tabID),
3438 ),
3439 async SummarizeFrom() {},
3440 async SummarizeFromForTab() {},
3441 async SummarizeUpTo() {},
3442 async SummarizeUpToForTab() {},
3443 async History() {
3444 return [];
3445 },
3446 async HistoryForTab(tabID?: string) {
3447 const tab = mockTabs.find((item) => item.id === tabID) ?? mockTabs.find((item) => item.active);
3448 if (tab?.topicId) {
3449 queueMockTopicRuntime(tab);
3450 if (benchFixturesPromise) {
3451 const fixtures = await benchFixturesPromise;
3452 const history = fixtures.benchTopicHistory(tab.topicId);
3453 if (history) return history;
3454 }
3455 return withMockHistoryIds(tab.id, topicHistoryFixture(t0, tab.topicId));
3456 }
3457 return withMockHistoryIds(tabID ?? "mock", await this.History());
3458 },
3459 async HistoryPage(beforeTurn = 0, limit = 60) {
3460 return mockHistoryPage(await this.History(), beforeTurn, limit);
3461 },
3462 async HistoryPageForTab(tabID: string, beforeTurn = 0, limit = 60) {
3463 return mockHistoryPage(await this.HistoryForTab(tabID), beforeTurn, limit);
3464 },
3465 async HistoryCheckpointTurnsForTab(tabID: string) {
3466 const turns: number[] = [];
3467 for (const message of await this.HistoryForTab(tabID)) {
3468 if (message.role !== "user") continue;
3469 turns.push(message.checkpointTurn ?? turns.length);
3470 }
3471 return turns;
3472 },
3473 async HistorySliceForTab(tabID: string, req: HistorySliceRequest) {
3474 return mockHistorySlice(tabID, await this.HistoryForTab(tabID), req, benchMock);
3475 },
3476 async HistoryContentForTab(tabID: string, ref: HistoryContentRef, chunkIndex: number): Promise<HistoryContentChunk> {
3477 const out: HistoryContentChunk = { entryId: ref.entryId, field: ref.field, chunk: Math.max(0, chunkIndex), chunks: 1, data: "", done: true, stale: false };
3478 const match = /:m(\d+):o\d+$/.exec(ref.entryId);
3479 if (!match) return out;
3480 const messages = await this.HistoryForTab(tabID);
3481 const message = messages[Number(match[1])];
3482 if (benchMock && message?.content?.includes("ASYNC LAYOUT EXPANSION COMPLETE")) await delay((await benchFixturesPromise)!.benchHydrationDelay());
3483 // Storm fixture: pace ref resolutions deterministically by entry
3484 // index so opening the session produces a seconds-long patch storm
3485 // instead of a single burst (#8657).
3486 if (benchMock && (message?.content?.includes("BENCH STORM HYDRATION RESOLVED") || message?.reasoning?.includes("BENCH STORM HYDRATION RESOLVED"))) {
3487 await delay(50 + (Number(match[1]) % 24) * 120);
3488 }
3489 if (!message) return { ...out, stale: true };
3490 out.data = mockHistoryContentField(message, ref);
3491 out.chunks = 1;
3492 return out;
3493 },
3494 ...makeMockSessionReaderBindings(),
3495 async ListSessions() {
3496 return sessions.map((s) => ({ ...s }));
3497 },
3498 async ListSessionsForTab() {
3499 return sessions.map((s) => ({ ...s }));
3500 },
3501 ...makeMockHistoryCatalogBindings(sessions),
3502 async ListTrashedSessions() {
3503 return trashedSessions.map((s) => ({ ...s }));
3504 },
3505 async ResumeSession(path: string) {
3506 sessions.forEach((s) => {
3507 s.current = s.path === path;
3508 s.open = s.open || s.path === path;
3509 });
3510 return [
3511 { role: "user", content: `(mock) resumed ${path}` },
3512 { role: "assistant", content: "This is a mock resumed transcript — the real one comes from the kernel." },
3513 ];
3514 },
3515 async ResumeSessionForTab(_tabID: string, path: string) {
3516 return this.ResumeSession(path);
3517 },
3518 async ResumeSessionPage(path: string, limit = 60) {
3519 return mockHistoryPage(await this.ResumeSession(path), 0, limit);
3520 },
3521 async ResumeSessionPageForTab(_tabID: string, path: string, limit = 60) {
3522 return this.ResumeSessionPage(path, limit);
3523 },
3524 async OpenChannelSessionForTab(tabID: string, path: string) {
3525 mockTabs = mockTabs.map((tab) => tab.id === tabID ? { ...tab, sessionPath: path, readOnly: true } : tab);
3526 return this.ResumeSession(path);
3527 },
3528 async OpenChannelSessionPageForTab(tabID: string, path: string, limit = 60) {
3529 return mockHistoryPage(await this.OpenChannelSessionForTab(tabID, path), 0, limit);
3530 },
3531 async QuerySessionTakeover(_tabId: string) {
3532 return { available: false, reason: "mock" } as import("./types").SessionTakeoverView;
3533 },
3534 async TakeoverSession(_tabId: string, _mode: "wait" | "interrupt") {},
3535 async PreviewSession(path: string) {
3536 const s = sessions.find((x) => x.path === path) ?? trashedSessions.find((x) => x.path === path);
3537 return [
3538 { role: "user", content: s?.preview || `(mock) preview ${path}` },
3539 { role: "phase", content: "Preparing read-only preview" },
3540 {
3541 role: "assistant",
3542 content: "This is a read-only mock preview. The active conversation is unchanged.",
3543 reasoning: "Preview reads the saved session without resuming it.",
3544 },
3545 { role: "notice", level: "info", content: "Preview mode keeps the active conversation untouched." },
3546 { role: "compaction", content: "", trigger: "manual", messages: 3, summary: "Mock preview preserved the latest task, tool result, and answer summary." },
3547 ];
3548 },
3549 async DeleteSession(path: string) {
3550 const i = sessions.findIndex((s) => s.path === path);
3551 if (i >= 0) {
3552 const [s] = sessions.splice(i, 1);
3553 trashedSessions.unshift({
3554 ...s,
3555 current: false,
3556 open: false,
3557 path: s.path.replace("/mock/sessions/", "/mock/sessions/.trash/"),
3558 deletedAt: Date.now(),
3559 });
3560 }
3561 },
3562 async DeleteRecoveryCopy(path: string) {
3563 return this.DeleteSession(path);
3564 },
3565 async GetRecoveryLineage(key) {
3566 const topic = findMockTopic(key.topicId);
3567 return {
3568 groupId: key.topicId,
3569 state: topic?.recoveryState ?? "normal",
3570 branchCount: topic?.recoveryBranchCount ?? 0,
3571 unresolved: topic?.recoveryUnresolvedCount ?? 0,
3572 cleanupEligible: topic?.recoveryCleanupEligibleCount ?? 0,
3573 members: [],
3574 };
3575 },
3576 async GetSessionVersionState(key) {
3577 const lineage = await this.GetRecoveryLineage(key);
3578 return { conversationId: key.topicId, canContinue: true, requiresChoice: lineage.state === "diverged" && lineage.unresolved > 0, lineage };
3579 },
3580 async SetActiveSessionVersion() {},
3581 async RetrySessionRecovery() {},
3582 async ReconcileRecoveryVersions() {},
3583 async ChooseRecoveryBranch() {},
3584 async RenameSessionHead() {},
3585 async CleanRecoveryLineage(request) {
3586 const topic = findMockTopic(request.topicId);
3587 const eligible = topic?.recoveryCleanupEligibleCount ?? 0;
3588 if (request.apply && topic) topic.recoveryCleanupEligibleCount = 0;
3589 return { eligible, moved: request.apply ? eligible : 0, busy: 0, kept: 0, dryRun: !request.apply, items: [] };
3590 },
3591 async RestoreSession(path: string) {
3592 const i = trashedSessions.findIndex((s) => s.path === path);
3593 if (i >= 0) {
3594 const [s] = trashedSessions.splice(i, 1);
3595 sessions.unshift({
3596 ...s,
3597 path: s.path.replace("/mock/sessions/.trash/", "/mock/sessions/"),
3598 deletedAt: undefined,
3599 });
3600 }
3601 },
3602 async PurgeTrashedSession(path: string) {
3603 const i = trashedSessions.findIndex((s) => s.path === path);
3604 if (i >= 0) trashedSessions.splice(i, 1);
3605 },
3606 async PurgeRecoveryCopy(path: string) {
3607 return this.PurgeTrashedSession(path);
3608 },
3609 async RenameSession(path: string, title: string) {
3610 const s = sessions.find((x) => x.path === path);
3611 if (s) s.title = title.trim() || undefined;
3612 },
3613 async ScanPromptHistory(nonce: string) {
3614 // Dev mock returns a static set of sample prompts for UI development.
3615 const entries: PromptHistoryEntry[] = [
3616 { text: "Explain the architecture of this project", at: Date.now() - 60000, sessionPath: "/mock/sessions/arch.jsonl", turn: 0 },
3617 { text: "Fix the login button styling", at: Date.now() - 120000, sessionPath: "/mock/sessions/arch.jsonl", turn: 1 },
3618 { text: "What is the capital of France?", at: Date.now() - 300000, sessionPath: "/mock/sessions/general.jsonl", turn: 0 },
3619 ];
3620 return { entries, nonce: "mock-" + nonce, olderCursor: "", hasOlder: false };
3621 },
3622 async ListWorkspaces() {
3623 return mockProjectTree
3624 .filter((node) => node.kind === "project" && node.root)
3625 .map((node) => ({
3626 path: node.root!,
3627 name: node.label || baseName(node.root!),
3628 current: node.root === cwd,
3629 }));
3630 },
3631 async PickWorkspace() {
3632 // Browser dev has no native dialog; simulate picking a folder and re-root so
3633 // the topbar folder chip visibly changes.
3634 return mockSwitchWorkspace(cwd.endsWith("another-project") ? "~/projects/reasonix" : "~/projects/another-project");
3635 },
3636 async SwitchWorkspace(path: string) {
3637 return mockSwitchWorkspace(path);
3638 },
3639 async RemoveWorkspace(path: string) {
3640 workspaces = workspaces.filter((p) => p !== path);
3641 const index = mockProjectTree.findIndex((node) => node.root === path);
3642 if (index >= 0) mockProjectTree.splice(index, 1);
3643 },
3644 async ContextUsageForTab() {
3645 return {
3646 used: 42124,
3647 window: 128000,
3648 sessionTokens: 34479,
3649 compactRatio: 0.8,
3650 sessionCost: 0.1287,
3651 sessionCurrency: "CNY",
3652 sessionCostQuote: {
3653 original: { amount: "0.1287", currency: "CNY" },
3654 selected: { amount: "0.1287", currency: "CNY" },
3655 estimated: true,
3656 costComplete: true,
3657 displayComplete: true,
3658 complete: true,
3659 displayStatus: "matched",
3660 aggregateMode: "single_currency",
3661 rateBand: "mixed",
3662 },
3663 };
3664 },
3665 async Balance() {
3666 // Mirror the active mock provider: deepseek-flash carries a balance_url.
3667 const p = settings.providers.find((x) => x.name === settings.defaultModel);
3668 if (!p?.balanceUrl) return { available: false, display: "" };
3669 return { available: true, display: "¥128.50" };
3670 },
3671 async BalanceForTab() {
3672 return this.Balance();
3673 },
3674 async UsageStats() {
3675 // Browser dev mock has no stats files; the panel does not consume
3676 // provider aggregates, so keep this initial-bundle fallback lean.
3677 return { from: "", to: "", tokens: 0, requests: 0, turns: 0, cacheHit: 0, cacheMiss: 0, activeDays: 0, topModel: "", daily: [], models: [] } as unknown as UsageStatsRange;
3678 },
3679 async Jobs() {
3680 return []; // browser dev mock has no background jobs
3681 },
3682 async JobsForTab() {
3683 return this.Jobs();
3684 },
3685 async CancelJob() {
3686 return false;
3687 },
3688 async CancelJobForTab(_tabID, jobID) {
3689 return this.CancelJob(jobID);
3690 },
3691 async CancelJobsForTab(_tabID, jobIDs) {
3692 return { cancelled: [], notRunning: [...jobIDs] };
3693 },
3694 async ActiveWorkForTab() {
3695 return { running: false, pendingPrompt: false, cancellable: false, jobs: [] };
3696 },
3697 async BackgroundRuntimes() {
3698 return [];
3699 },
3700 async RevealBackgroundRuntime() {
3701 throw new Error("background runtime is unavailable in browser preview");
3702 },
3703 async WorkspaceConflictForTab() {
3704 return {
3705 state: "none", ownerWork: { running: false, pendingPrompt: false, cancellable: false, jobs: [] },
3706 canReveal: false, canCreateWorktree: false,
3707 };
3708 },
3709 async RevealWorkspaceWriterForTab() {
3710 throw new Error("workspace writer is unavailable in browser preview");
3711 },
3712 async CloseTabWithPolicy(tabID) {
3713 return this.CloseTab(tabID);
3714 },
3715 async ToolResultForTab() {
3716 return null;
3717 },
3718 async Meta() {
3719 const active = withMockSessionIdentity(mockTabs.find((tab) => tab.active) ?? mockTabs[0]);
3720 const toolApprovalMode = normalizeToolApprovalMode(active?.toolApprovalMode, active ? normalizeMode(active.mode) : "normal", settings.autoApproveTools);
3721 const autoApproveTools = toolApprovalMode === "danger-full-access";
3722 const collaborationMode = normalizeCollaborationMode(active?.collaborationMode, active?.goal, active ? normalizeMode(active.mode) : "normal");
3723 const workspacePath = active?.workspacePath || active?.workspaceRoot || active?.cwd || cwd;
3724 return {
3725 label: active?.label ?? "DeepSeek-R1",
3726 ready: active?.ready ?? true,
3727 eventChannel: EVENT_CHANNEL,
3728 cwd: active?.cwd || cwd,
3729 workspaceRoot: active?.workspaceRoot || workspacePath,
3730 workspaceName: active?.workspaceName,
3731 workspacePath,
3732 ...mockSessionMeta(active),
3733 sandboxPath: settings.sandbox.workspaceRoot,
3734 gitBranch: active?.gitBranch || (active?.scope === "project" ? "main" : ""),
3735 imageInputEnabled: true,
3736 autoApproveTools,
3737 bypass: autoApproveTools,
3738 collaborationMode,
3739 toolApprovalMode,
3740 goal: active?.goal ?? "",
3741 goalStatus: active?.goalStatus ?? (active?.goal ? "running" : "stopped"),
3742 };
3743 },
3744 async MetaForTab(tabID) {
3745 const tab = withMockSessionIdentity(mockTabs.find((item) => item.id === tabID) ?? mockTabs.find((item) => item.active) ?? mockTabs[0]);
3746 const toolApprovalMode = normalizeToolApprovalMode(tab?.toolApprovalMode, tab ? normalizeMode(tab.mode) : "normal", settings.autoApproveTools);
3747 const autoApproveTools = toolApprovalMode === "danger-full-access";
3748 const collaborationMode = normalizeCollaborationMode(tab?.collaborationMode, tab?.goal, tab ? normalizeMode(tab.mode) : "normal");
3749 const workspacePath = tab?.workspacePath || tab?.workspaceRoot || tab?.cwd || cwd;
3750 return {
3751 label: tab?.label ?? "DeepSeek-R1",
3752 ready: tab?.ready ?? true,
3753 eventChannel: EVENT_CHANNEL,
3754 cwd: tab?.cwd || cwd,
3755 workspaceRoot: tab?.workspaceRoot || workspacePath,
3756 workspaceName: tab?.workspaceName,
3757 workspacePath,
3758 ...mockSessionMeta(tab),
3759 sandboxPath: settings.sandbox.workspaceRoot,
3760 gitBranch: tab?.gitBranch || (tab?.scope === "project" ? "main" : ""),
3761 autoApproveTools,
3762 bypass: autoApproveTools,
3763 collaborationMode,
3764 toolApprovalMode,
3765 goal: tab?.goal ?? "",
3766 goalStatus: tab?.goalStatus ?? (tab?.goal ? "running" : "stopped"),
3767 };
3768 },
3769 async Commands() {
3770 const commands: CommandInfo[] = [
3771 { name: "new", description: "start new session; save transcript", kind: "builtin" as const, group: "actions" },
3772 { name: "clear", description: "discard current context", kind: "builtin" as const, group: "actions" },
3773 { name: "compact", description: "Summarize older history to free up context", kind: "builtin" as const, group: "actions" },
3774 { name: "model", description: "Switch model", kind: "builtin" as const, group: "actions" },
3775 { name: "effort", description: "Set reasoning effort", kind: "builtin" as const, group: "actions" },
3776 { name: "skill", description: "List skills", kind: "builtin" as const, group: "skills" },
3777 { name: "mcp", description: "Manage MCP servers", kind: "builtin" as const, group: "integrations" },
3778 { name: "plugins", description: "Manage plugin packages", kind: "builtin" as const, group: "integrations" },
3779 { name: "review", description: "Review the staged diff", hint: "[focus]", kind: "custom" as const, group: "skills" },
3780 ];
3781 const seen = new Set(commands.map((command) => command.name));
3782 for (const skill of capSkills) {
3783 if (skill.enabled === false) continue;
3784 const name = (skill.invocation || `/${skill.name}`).replace(/^\/+/, "");
3785 if (!name || seen.has(name)) continue;
3786 seen.add(name);
3787 commands.push({
3788 name,
3789 description: skill.description,
3790 kind: skill.runAs === "subagent" ? "subagent" : "skill",
3791 group: skill.runAs === "subagent" ? "subagents" : "skills",
3792 color: skill.color,
3793 });
3794 }
3795 return commands;
3796 },
3797 async Capabilities() {
3798 return {
3799 servers: capServers.map((s) => ({ ...s })),
3800 skills: capSkills.map((s) => ({ ...s })),
3801 skillRoots: capSkillRoots.map((s) => ({ ...s })),
3802 plugins: capPlugins.map((p) => ({ ...p })),
3803 };
3804 },
3805 async MCPServers() {
3806 return capServers.map((s) => ({ ...s }));
3807 },
3808 async MCPCapabilityMatrix() {
3809 return { views: [], hostProfile: "desktop-apps-2026-01-26-v1" };
3810 },
3811 async MCPMarketplace(query: string) {
3812 const servers = [
3813 {
3814 name: "io.modelcontextprotocol/server-filesystem",
3815 suggestedName: "server-filesystem",
3816 title: "Filesystem",
3817 description: "Secure file operations through MCP.",
3818 version: "1.0.0",
3819 installable: true,
3820 transport: "stdio",
3821 command: "npx",
3822 args: ["-y", "@modelcontextprotocol/server-filesystem@1.0.0"],
3823 },
3824 {
3825 name: "io.example/manual",
3826 suggestedName: "manual",
3827 title: "Manual setup example",
3828 description: "Requires an API key before installation.",
3829 version: "1.0.0",
3830 installable: false,
3831 unavailableReason: "package requires environment variables or arguments",
3832 args: [],
3833 },
3834 ];
3835 const normalized = query.trim().toLowerCase();
3836 return {
3837 servers: normalized ? servers.filter((entry) => [entry.name, entry.title, entry.description].join(" ").toLowerCase().includes(normalized)) : servers,
3838 cached: false,
3839 } as MCPMarketplaceView;
3840 },
3841 async MCPMarketplaceResolve(registryName: string) {
3842 const result = await this.MCPMarketplace(registryName);
3843 const entry = result.servers.find((candidate) => candidate.name.toLowerCase() === registryName.trim().toLowerCase());
3844 if (!entry) throw new Error(`MCP Registry has no server named ${JSON.stringify(registryName)}`);
3845 return entry;
3846 },
3847 async SkillsSettings() {
3848 return {
3849 skills: capSkills.map((s) => ({ ...s })),
3850 skillRoots: capSkillRoots.map((s) => ({ ...s })),
3851 allowImplicitInvocation: true,
3852 };
3853 },
3854 async RuntimeDoctor() {
3855 return {
3856 text: "runtime status: mock\nrecoverability: clean=true irreversible=false\nresume: allow=true cleanRollback=true\n",
3857 publishedGeneration: 0,
3858 allowResume: true,
3859 cleanRollback: true,
3860 hasIrreversible: false,
3861 noOpRebuilds: 0,
3862 fullRebuilds: 0,
3863 subgraphRebuilds: 0,
3864 staleDrops: 0,
3865 admissionRejected: 0, runtimeOwnerFallbacks: 0,
3866 };
3867 },
3868 async CapabilityDiagnostics(includeSessionRuntime: boolean) {
3869 const report: CapabilityDiagnosticsReport = {
3870 schema_version: 1,
3871 root: "<workspace>",
3872 live: false,
3873 summary: {
3874 errors: 0,
3875 warnings: 1,
3876 infos: includeSessionRuntime ? 1 : 0,
3877 instructions: 1,
3878 skills: capSkills.length,
3879 commands: 0,
3880 hooks: 0,
3881 plugins: capPlugins.length,
3882 mcp_servers: capServers.length,
3883 },
3884 instructions: { docs: [{ path: "<workspace>/AGENTS.md", scope: "project", directory: "<workspace>", depth: 0, order: 1 }] },
3885 skills: {
3886 roots: [{ path: "<workspace>/.reasonix/skills", scope: "project", status: "ok" }],
3887 entries: capSkills.map((s) => ({
3888 name: s.name,
3889 description: s.description,
3890 scope: s.scope,
3891 path: "(mock)",
3892 status: "winner",
3893 run_as: s.runAs,
3894 })),
3895 winners: capSkills.length,
3896 shadowed: 0,
3897 },
3898 commands: { roots: [], entries: [], winners: 0, shadowed: 0 },
3899 hooks: { trusted_project: true, project_defines_hooks: false, sources: [], entries: [] },
3900 plugins: {
3901 packages: capPlugins.map((p) => ({
3902 name: p.name,
3903 enabled: p.enabled,
3904 root: p.root || "<external>/plugin",
3905 skills: p.skills ?? 0,
3906 commands: 0,
3907 hooks: p.hooks ?? 0,
3908 mcp_servers: p.mcpServers ?? 0,
3909 status: p.enabled ? "ok" : "disabled",
3910 })),
3911 },
3912 mcp: {
3913 servers: capServers.map((s) => ({
3914 name: s.name,
3915 transport: s.transport || "stdio",
3916 start_intent: s.startIntent === "off" ? "off" : "automatic",
3917 source: "toml",
3918 runtime_status: includeSessionRuntime ? s.status || "connected" : undefined,
3919 tool_count: s.tools,
3920 env_keys: s.envKeys ?? [],
3921 header_keys: s.headerKeys ?? [],
3922 })),
3923 },
3924 issues: [
3925 {
3926 severity: "warning",
3927 code: "skill.missing_description",
3928 subsystem: "skills",
3929 name: "example",
3930 message: "mock warning for browser harness",
3931 remediation: "Add a description frontmatter field",
3932 settings_tab: "skills",
3933 },
3934 ...(includeSessionRuntime
3935 ? [{
3936 severity: "info" as const,
3937 code: "mcp.runtime_unavailable",
3938 subsystem: "mcp",
3939 message: "browser mock has no live Host; runtime fields are synthetic",
3940 settings_tab: "mcp",
3941 }]
3942 : []),
3943 ],
3944 };
3945 return JSON.parse(JSON.stringify(report)) as CapabilityDiagnosticsReport;
3946 },
3947 async CredentialDiagnostics(_probe: boolean) {
3948 return credentialDiagnostics();
3949 },
3950 async RepairCredentials(dryRun: boolean) {
3951 return credentialDiagnostics([dryRun ? "would repair credential access" : "repaired credential access"]);
3952 },
3953 async RetryAuthenticationForTab(_tabId: string) {
3954 return { status: "ready" };
3955 },
3956 async Plugins() {
3957 return capPlugins.map((p) => ({ ...p }));
3958 },
3959 async PlanPluginInstall(source: string, options: PluginInstallOptions) {
3960 const name = options.name || source.split("/").filter(Boolean).pop()?.replace(/\.git$/, "") || "plugin";
3961 return JSON.stringify({
3962 ok: true,
3963 status: "planned",
3964 kind: "plugin",
3965 actions: [{ kind: "plugin", action: "install_plugin_package", name, source, status: "planned" }],
3966 });
3967 },
3968 async InstallPlugin(source: string, options: PluginInstallOptions) {
3969 const name = options.name || source.split("/").filter(Boolean).pop()?.replace(/\.git$/, "") || "plugin";
3970 const existing = capPlugins.findIndex((p) => p.name === name);
3971 const view: PluginView = {
3972 name,
3973 version: "dev",
3974 description: "Mock plugin",
3975 source,
3976 root: `~/.reasonix/plugins/${name}`,
3977 manifestKind: "reasonix",
3978 enabled: true,
3979 skills: 1,
3980 hooks: 0,
3981 mcpServers: 0,
3982 skillDetails: [{ name: "plan", description: "Plan work before implementation", invocation: "/plan", runAs: "inline" }],
3983 };
3984 if (existing >= 0) capPlugins[existing] = view;
3985 else capPlugins.push(view);
3986 return JSON.stringify({ ok: true, status: "done", kind: "plugin", actions: [{ kind: "plugin", name }] });
3987 },
3988 async RemovePlugin(name: string) {
3989 capPlugins = capPlugins.filter((p) => p.name !== name);
3990 },
3991 async SetPluginEnabled(name: string, enabled: boolean) {
3992 capPlugins = capPlugins.map((p) => p.name === name ? { ...p, enabled } : p);
3993 },
3994 async UpdatePlugin(name: string) {
3995 capPlugins = capPlugins.map((p) => p.name === name ? { ...p, version: p.version || "dev" } : p);
3996 return JSON.stringify({ ok: true, status: "done", kind: "plugin", name });
3997 },
3998 async PluginDoctor(name: string) {
3999 return capPlugins.find((p) => p.name === name) || {
4000 name,
4001 root: "",
4002 enabled: false,
4003 skills: 0,
4004 hooks: 0,
4005 mcpServers: 0,
4006 error: "plugin is not installed",
4007 };
4008 },
4009 async AddMCPServer(input: MCPServerInput) {
4010 const tools = input.transport === "stdio" ? 3 : 5;
4011 capServers.push({
4012 name: input.name,
4013 transport: input.transport,
4014 status: "connected",
4015 configured: true,
4016 autoStart: true,
4017 tier: "background",
4018 command: input.command,
4019 args: input.args,
4020 url: input.url,
4021 envKeys: input.env ? Object.keys(input.env).sort() : undefined,
4022 headerKeys: input.headers ? Object.keys(input.headers).sort() : undefined,
4023 tools,
4024 prompts: 0,
4025 resources: 0,
4026 toolList: Array.from({ length: tools }, (_, i) => ({
4027 name: `${input.name}_tool_${i + 1}`,
4028 description: `Mock tool ${i + 1} exposed by ${input.name}.`,
4029 })),
4030 });
4031 return tools;
4032 },
4033 async InstallMCPServer(input: MCPServerInput) {
4034 const tools = await this.AddMCPServer(input);
4035 return { name: input.name, state: "ready" as const, toolCount: tools, action: "none" as const, message: `${input.name} is ready` };
4036 },
4037 async UpdateMCPServer(name: string, input: MCPServerInput) {
4038 capServers = capServers.map((s) => {
4039 if (s.name !== name) return s;
4040 const connected = s.status === "connected" || s.status === "failed" || s.autoStart !== false;
4041 const nextStatus = s.status === "disabled" ? "disabled" : connected ? "connected" : "deferred";
4042 const nextTools = nextStatus === "connected" ? s.tools || (input.transport === "stdio" ? 3 : 5) : 0;
4043 return {
4044 ...s,
4045 transport: input.transport,
4046 status: nextStatus,
4047 command: input.transport === "stdio" ? input.command : "",
4048 args: input.transport === "stdio" ? input.args : [],
4049 url: input.transport === "stdio" ? "" : input.url,
4050 envKeys: input.env ? Object.keys(input.env).sort() : s.envKeys,
4051 headerKeys: input.headers ? Object.keys(input.headers).sort() : s.headerKeys,
4052 tools: nextTools,
4053 error: undefined,
4054 authStatus: nextStatus !== "connected" && input.transport !== "stdio" ? "possible" : undefined,
4055 authUrl: nextStatus !== "connected" && input.transport !== "stdio" ? input.url : undefined,
4056 };
4057 });
4058 },
4059 async RemoveMCPServer(name: string) {
4060 capServers = capServers.filter((s) => s.name !== name);
4061 },
4062 async AuthorizeAndConnectMCPServer(name: string) {
4063 capServers = capServers.map((s) => s.name === name
4064 ? { ...s, status: "connected", runtimeState: "ready", tools: s.tools || 4, error: undefined, requiresLaunchApproval: false }
4065 : s);
4066 },
4067 async AuthenticateMCPServer(name: string) {
4068 capServers = capServers.map((s) => s.name === name
4069 ? { ...s, status: "connected", runtimeState: "ready", tools: s.tools || 4, error: undefined, authStatus: "none", authUrl: undefined }
4070 : s);
4071 },
4072 async ReconnectMCPServer(name: string) {
4073 capServers = capServers.map((s) =>
4074 s.name === name
4075 ? { ...s, status: "initializing", error: undefined, authStatus: undefined, authUrl: undefined }
4076 : s,
4077 );
4078 await new Promise((r) => setTimeout(r, 400));
4079 capServers = capServers.map((s) =>
4080 s.name === name ? { ...s, status: "connected", tools: s.tools || 4 } : s,
4081 );
4082 },
4083 async ClearMCPServerAuthentication(name: string) {
4084 capServers = capServers.map((s) =>
4085 s.name === name
4086 ? {
4087 ...s,
4088 status: s.autoStart === false ? "disabled" : "initializing",
4089 tools: 0,
4090 error: undefined,
4091 authStatus: s.transport !== "stdio" ? "possible" : undefined,
4092 authUrl: s.transport !== "stdio" ? s.url : undefined,
4093 authConfigured: undefined,
4094 }
4095 : s,
4096 );
4097 },
4098 async PickSkillFolder() {
4099 return "~/my-skills";
4100 },
4101 async PickPluginFolder() {
4102 return "~/plugins/superpowers";
4103 },
4104 async AddSkillPath(path: string) {
4105 const dir = path.trim() || "~/my-skills";
4106 if (!capSkillRoots.some((r) => r.scope === "custom" && r.dir === dir)) {
4107 capSkillRoots.push({
4108 dir,
4109 scope: "custom",
4110 priority: capSkillRoots.length + 1,
4111 status: "ok",
4112 enabled: true,
4113 configured: true,
4114 removable: true,
4115 skills: 1,
4116 skillItems: [{ name: "local-dev", description: "Local custom development workflow", scope: "custom", runAs: "inline" }],
4117 });
4118 }
4119 if (!capSkills.some((s) => s.name === "local-dev")) {
4120 capSkills.push({ name: "local-dev", description: "Local custom development workflow", scope: "custom", runAs: "inline", enabled: true });
4121 }
4122 },
4123 async RemoveSkillPath(path: string) {
4124 capSkillRoots = capSkillRoots.filter((r) => r.dir !== path);
4125 if (!capSkillRoots.some((r) => r.scope === "custom")) {
4126 const idx = capSkills.findIndex((s) => s.name === "local-dev");
4127 if (idx >= 0) capSkills.splice(idx, 1);
4128 }
4129 },
4130 async SetSkillPathEnabled(path: string, enabled: boolean) {
4131 const root = capSkillRoots.find((r) => r.dir === path);
4132 if (root) {
4133 root.enabled = enabled;
4134 root.status = enabled ? "ok" : "disabled";
4135 root.skills = enabled ? (root.skillItems?.length ?? 0) : 0;
4136 }
4137 },
4138 async RefreshSkills() {},
4139 async ReloadCommands() {},
4140 async SetSkillEnabled(name: string, enabled: boolean) {
4141 const skill = capSkills.find((s) => s.name === name);
4142 if (skill) skill.enabled = enabled;
4143 },
4144 async SetSkillImplicitInvocation(_enabled: boolean) {},
4145 async AvailableSubagentTools() {
4146 return [
4147 { name: "read_file", description: "Read a file's contents", readOnlyHint: true },
4148 { name: "ls", description: "List a directory", readOnlyHint: true },
4149 { name: "glob", description: "Find files by name pattern", readOnlyHint: true },
4150 { name: "grep", description: "Search file contents", readOnlyHint: true },
4151 { name: "code_index", description: "Look up symbol definitions and file outlines", readOnlyHint: true },
4152 { name: "edit_file", description: "Edit an existing file" },
4153 { name: "write_file", description: "Write a new file" },
4154 { name: "bash", description: "Run a shell command" },
4155 { name: "web_fetch", description: "Fetch a URL" },
4156 ];
4157 },
4158 async CreateSubagentProfile(input: SubagentProfileInput) {
4159 const name = input.name.trim();
4160 const builtinNames = ["init", "explore", "research", "install-capability", "review", "security-review", "test"];
4161 if (builtinNames.includes(name)) throw new Error(`"${name}" is a built-in subagent name and cannot be reused`);
4162 if (capSkills.some((s) => s.name === name)) throw new Error(`"${name}" already exists`);
4163 capSkills.push({
4164 name, description: input.description, scope: input.scope === "project" ? "project" : "global",
4165 runAs: "subagent", enabled: true, model: input.model, effort: input.effort,
4166 allowedTools: input.allowedTools, color: input.color, invocation: `/${name}`, invocationMode: "manual",
4167 });
4168 return `~/.reasonix/skills/${name}/SKILL.md`;
4169 },
4170 async UpdateSubagentProfile(name: string, scope: string, input: SubagentProfileInput) {
4171 const skill = capSkills.find((s) => s.name === name && s.scope === scope);
4172 if (!skill) throw new Error(`"${name}" resolves at a different scope — refusing to update`);
4173 skill.description = input.description;
4174 skill.color = input.color;
4175 skill.model = input.model;
4176 skill.effort = input.effort;
4177 skill.allowedTools = input.allowedTools;
4178 },
4179 async DeleteSubagentProfile(name: string, scope: string) {
4180 const idx = capSkills.findIndex((s) => s.name === name && s.scope === scope);
4181 if (idx < 0) throw new Error(`"${name}" resolves at a different scope — refusing to delete`);
4182 capSkills.splice(idx, 1);
4183 },
4184 async SetSubagentProfileModel(name: string, ref: string) {
4185 const skill = capSkills.find((s) => s.name === name);
4186 if (skill) skill.configuredModel = ref || undefined;
4187 },
4188 async SetSubagentProfileEffort(name: string, level: string) {
4189 const skill = capSkills.find((s) => s.name === name);
4190 if (skill) skill.configuredEffort = level || undefined;
4191 },
4192 async CancelTrySubagentProfile() {},
4193 async TrySubagentProfile(input: SubagentProfileInput, task: string) {
4194 if (!task.trim()) throw new Error("task is required");
4195 if (!input.systemPrompt.trim()) throw new Error("system prompt is required");
4196 await new Promise((resolve) => setTimeout(resolve, 400));
4197 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.)`;
4198 },
4199 async SetMCPServerEnabled(name: string, enabled: boolean) {
4200 capServers = capServers.map((s) =>
4201 s.name === name
4202 ? {
4203 ...s,
4204 status: enabled ? "connected" : "disabled",
4205 autoStart: s.builtIn ? enabled : s.autoStart,
4206 tools: enabled ? s.tools || 4 : 0,
4207 error: undefined,
4208 authStatus: !enabled && s.transport !== "stdio" ? "possible" : undefined,
4209 authUrl: !enabled && s.transport !== "stdio" ? s.url : undefined,
4210 }
4211 : s,
4212 );
4213 },
4214 async SetMCPServerTier(name: string, tier: string) {
4215 capServers = capServers.map((s) => {
4216 if (s.name !== name) return s;
4217 const tools = s.tools || (s.transport === "stdio" ? 3 : 5);
4218 return { ...s, tier, autoStart: true, status: "connected", tools, error: undefined, authStatus: undefined, authUrl: undefined };
4219 });
4220 },
4221 async SlashArgs(input: string) {
4222 // Mirror a slice of the real arg hints so the menu is exercisable in browser dev.
4223 const from = input.lastIndexOf(" ") + 1;
4224 const cur = input.slice(from);
4225 const cmd = input.slice(0, input.indexOf(" ") < 0 ? input.length : input.indexOf(" "));
4226 const subs: Record<string, { label: string; insert: string; hint: string; descend?: boolean }[]> = {
4227 "/skill": [
4228 { label: "list", insert: "list", hint: "list skills" },
4229 { label: "show", insert: "show ", hint: "show a skill's body", descend: true },
4230 { label: "enable", insert: "enable ", hint: "enable a disabled skill", descend: true },
4231 { label: "disable", insert: "disable ", hint: "disable an enabled skill", descend: true },
4232 { label: "new", insert: "new ", hint: "scaffold a new skill" },
4233 { label: "paths", insert: "paths", hint: "show discovery paths" },
4234 ],
4235 "/hooks": [
4236 { label: "list", insert: "list", hint: "list active hooks" },
4237 ],
4238 "/model": [
4239 { label: "deepseek/deepseek-v4-flash", insert: "deepseek/deepseek-v4-flash", hint: "current" },
4240 { label: "deepseek/deepseek-v4-pro", insert: "deepseek/deepseek-v4-pro", hint: "" },
4241 ],
4242 "/effort": [
4243 { label: "auto", insert: "auto", hint: "use the model default" },
4244 { label: "high", insert: "high", hint: "deeper reasoning" },
4245 { label: "max", insert: "max", hint: "maximum reasoning" },
4246 ],
4247 };
4248 const items = (subs[cmd] ?? [])
4249 .filter((it) => it.label.toLowerCase().startsWith(cur.toLowerCase()))
4250 .map((it) => ({ label: it.label, insert: it.insert, hint: it.hint, descend: it.descend ?? false }));
4251 return { items, from };
4252 },
4253 async ListDir(rel: string) {
4254 // A tiny fake tree so the @ menu is navigable in browser dev.
4255 if (rel === "" || rel === "./") {
4256 return [
4257 { name: "internal", isDir: true },
4258 { name: "desktop", isDir: true },
4259 { name: "README.md", isDir: false },
4260 { name: "go.mod", isDir: false },
4261 ];
4262 }
4263 if (rel === "internal/") {
4264 return [
4265 { name: "control", isDir: true },
4266 { name: "boot", isDir: true },
4267 { name: "event.go", isDir: false },
4268 ];
4269 }
4270 return [{ name: "file.go", isDir: false }];
4271 },
4272 async ListDirForTab(_tabID: string, rel: string) {
4273 return this.ListDir(rel);
4274 },
4275 async ListDirForTarget(_target: ComposerTarget, rel: string) {
4276 return this.ListDir(rel);
4277 },
4278 async SearchFileRefs(query: string) {
4279 const q = query.toLowerCase();
4280 return ["desktop/frontend/src/lib/bridge.ts", "desktop/frontend/src/main.tsx", "internal/control/refs.go"]
4281 .filter((path) => path.split("/").pop()?.toLowerCase().includes(q))
4282 .map((name) => ({ name, isDir: false }));
4283 },
4284 async SearchFileRefsForTab(_tabID: string, query: string) {
4285 return this.SearchFileRefs(query);
4286 },
4287 async SearchFileRefsForTarget(_target: ComposerTarget, query: string) {
4288 return this.SearchFileRefs(query);
4289 },
4290 async ReadFile(rel: string) {
4291 return mockWorkspaceFile(rel);
4292 },
4293 async ReadFileForTab(_tabID: string, rel: string) {
4294 return this.ReadFile(rel);
4295 },
4296 async CreateWorkspaceBrowserPreviewForTab(_tabID: string, rel: string) {
4297 return `http://127.0.0.1:0/__reasonix_workspace_media/mock/${encodeURIComponent(rel)}`;
4298 },
4299 async ReadPresentedFileForTab(tabID: string, _toolCallID: string, path: string) {
4300 return this.ReadFileForTab(tabID, path);
4301 },
4302 async ReadPresentedFileSourceForTab(tabID: string, _toolCallID: string, path: string) {
4303 return this.ReadFileForTab(tabID, path);
4304 },
4305 async ReadPresentedTextPageForTab(_tabID: string, _toolCallID: string, path: string, offset: number, expectedVersion: string) {
4306 return { path, body: "", offset, nextOffset: offset, size: offset, hasMore: false, version: expectedVersion };
4307 },
4308 async ResolveChatFileReferencesForTab(_tabID: string, turnKey: string, candidates: ChatFileReferenceRequest[]): Promise<ChatFileReferenceResult> {
4309 return {
4310 turnKey,
4311 references: candidates.map(candidate => ({
4312 key: candidate.key,
4313 path: candidate.path,
4314 status: "unavailable" as const,
4315 actions: [],
4316 reason: "not-found" as const,
4317 })),
4318 };
4319 },
4320 async ReadReferenceFileForTab(tabID: string, path: string) {
4321 return this.ReadFileForTab(tabID, path);
4322 },
4323 async ReadReferenceFileSourceForTab(tabID: string, path: string) {
4324 return this.ReadFileForTab(tabID, path);
4325 },
4326 async SanitizeMarkdownSVG(): Promise<MarkdownSVGView> {
4327 return { ok: false, reason: "unsupported" };
4328 },
4329 async CreatePresentedBrowserPreviewForTab(tabID: string, _toolCallID: string, path: string) {
4330 return this.CreateWorkspaceBrowserPreviewForTab(tabID, path);
4331 },
4332 async RevokeWorkspaceBrowserPreview() {},
4333 async RevokeWorkspaceMediaPreview() {},
4334 async SaveRemoteFileAs(_hostID: string, remotePath: string) {
4335 return remotePath;
4336 },
4337 async SaveRemotePresentedFileAs(_tabID: string, hostID: string, _toolCallID: string, remotePath: string) {
4338 return this.SaveRemoteFileAs(hostID, remotePath);
4339 },
4340 async ResolveRemotePresentedPathForTab(_tabID: string, _hostID: string, _toolCallID: string, remotePath: string) {
4341 return remotePath.startsWith("/") ? remotePath : `/remote/workspace/${remotePath}`;
4342 },
4343 async ResolveRemoteWorkspacePathForTab(_tabID: string, _hostID: string, _toolCallID: string, remotePath: string) {
4344 return remotePath.startsWith("/") ? remotePath : `/remote/workspace/${remotePath}`;
4345 },
4346 async ResolveMarkdownImageForTab(_tabID: string, source: string) {
4347 return { url: source, openHref: source };
4348 },
4349 async WorkspaceRevisionForTab(_tabID: string) {
4350 return { revisions: { content: 0, tree: 0, workingTree: 0, gitMeta: 0, session: 0 }, watchState: "active" as const };
4351 },
4352 async WorkspaceChanges(_tabID: string) {
4353 return {
4354 gitAvailable: true,
4355 gitBranch: "main",
4356 files: [
4357 {
4358 path: "desktop/frontend/src/components/WorkspacePanel.tsx",
4359 sources: ["session", "git"],
4360 gitStatus: "M",
4361 turns: [0, 2],
4362 latestPrompt: "Mock session edited the workspace panel.",
4363 latestTime: Date.now() - 60_000,
4364 },
4365 { path: "README.md", sources: ["git"], gitStatus: "??" },
4366 { path: "internal/control/controller.go", sources: ["session"], turns: [1], latestTime: Date.now() - 120_000 },
4367 ],
4368 };
4369 },
4370 async WorkspaceTurnChanges(_tabID: string, _sessionPath: string, turn: number, _resultID: string) {
4371 return { turn, coverage: "unknown" as const, files: [], reasons: [], added: 0, removed: 0 };
4372 },
4373 async WorkspaceTurnChangeDetail() { return null; },
4374 async TurnCheckLog() { return null; },
4375 async WorkspaceChangeDetail(_tabID: string, path: string) {
4376 return {
4377 source: "git" as const,
4378 added: 2,
4379 removed: 1,
4380 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`,
4381 };
4382 },
4383 async GitBranches() {
4384 return ["main", "dev", "feature/branch-switcher"];
4385 },
4386 async GitBranchesForTab(_tabID: string, _workspaceRoot: string) { return this.GitBranches(); },
4387 async GitCheckoutForTab(_tabID: string, _workspaceRoot: string, branch: string) { await this.GitCheckout(branch); },
4388 async GitCreateBranchForTab(_tabID: string, _workspaceRoot: string, name: string) { await this.GitCreateBranch(name); },
4389 async WorkspaceGitStatsForTab(tabID: string, _workspaceRoot: string) { return this.WorkspaceChanges(tabID); },
4390 async GitCheckout(_branch: string) {
4391 console.info("mock GitCheckout", _branch);
4392 },
4393 async GitCreateBranch(_name: string) {
4394 console.info("mock GitCreateBranch", _name);
4395 },
4396 async WorkspaceGitHistory(_tabID: string, path: string) {
4397 return [
4398 { hash: "abcdef123456", author: "Mock Author", date: new Date().toISOString(), message: "Mock commit message for " + path },
4399 ];
4400 },
4401 async WorkspaceGitCommitDetail(_tabID: string, _hash: string, path: string) {
4402 if (path) {
4403 return { diff: "--- a/mock\n+++ b/mock\n@@ -1,1 +1,1 @@\n-mock\n+mock diff" };
4404 }
4405 return { files: ["mock_file_1.ts", "mock_file_2.ts"] };
4406 },
4407 async OpenLocalPath(path: string) {
4408 console.info("mock OpenLocalPath", path);
4409 },
4410 async OpenWorkspacePathForTab(_tabID: string, rel: string) {
4411 console.info("mock OpenWorkspacePath", rel);
4412 },
4413 async OpenPresentedPathForTab(tabID: string, _toolCallID: string, path: string) {
4414 return this.OpenWorkspacePathForTab(tabID, path);
4415 },
4416 async OpenReferencePathForTab(_tabID: string, path: string) {
4417 console.info("mock OpenReferencePathForTab", path);
4418 },
4419 async ResolvePresentedPathForTab(tabID: string, _toolCallID: string, path: string) {
4420 return this.ResolveWorkspacePathForTab(tabID, path);
4421 },
4422 async ResolveReferencePathForTab(_tabID: string, path: string) {
4423 return path;
4424 },
4425 async ResolveWorkspacePathForTab(_tabID: string, rel: string) { return `${cwd.replace(/[\\/]+$/, "")}/${rel.replace(/^[/\\]+/, "").replace(/[\\/]+$/, "")}`; },
4426 async ExternalOpeners() {
4427 return {
4428 openers: [
4429 { id: "vscode", name: "VS Code", kind: "editor", iconDataUrl: mockExternalOpenerIconDataURL("#1684d6", "V") },
4430 { id: "cursor", name: "Cursor", kind: "editor", iconDataUrl: mockExternalOpenerIconDataURL("#25262a", "C") },
4431 { id: "finder", name: "Finder", kind: "file-manager", iconDataUrl: mockExternalOpenerIconDataURL("#36aaf4", "F") },
4432 { id: "ghostty", name: "Ghostty", kind: "terminal", iconDataUrl: mockExternalOpenerIconDataURL("#264db6", ">") },
4433 ],
4434 preferred: "vscode",
4435 } as ExternalOpenersView;
4436 }, async ExternalOpenersForTab(_tabID: string) { return { ...(await this.ExternalOpeners()), workspaceOpenable: true }; },
4437 async SetPreferredExternalOpener(_id: string) {},
4438 async OpenWorkspaceInExternalOpenerForTab(_tabID: string, _id: string) {}, async OpenLocalPathInExternalOpener(path: string, id: string) { console.info("mock OpenLocalPathInExternalOpener", path, id); }, async SaveLocalPathAs(path: string) { console.info("mock SaveLocalPathAs", path); return path; },
4439 async RevealWorkspacePathForTab(_tabID: string, rel: string) {
4440 console.info("mock RevealWorkspacePath", rel);
4441 },
4442 async RevealPresentedPathForTab(tabID: string, _toolCallID: string, path: string) {
4443 return this.RevealWorkspacePathForTab(tabID, path);
4444 },
4445 async RevealReferencePathForTab(_tabID: string, path: string) {
4446 console.info("mock RevealReferencePathForTab", path);
4447 },
4448 async SaveWorkspacePathAsForTab(_tabID: string, rel: string) {
4449 console.info("mock SaveWorkspacePathAsForTab", rel);
4450 return rel;
4451 },
4452 async SavePresentedPathAsForTab(tabID: string, _toolCallID: string, path: string) {
4453 return this.SaveWorkspacePathAsForTab(tabID, path);
4454 },
4455 async SaveReferencePathAsForTab(_tabID: string, path: string) {
4456 return path;
4457 },
4458 async RevealPath(path: string) {
4459 console.info("mock RevealPath", path);
4460 },
4461 async PickExportFile(defaultFilename: string, _mimeType: string) {
4462 return defaultFilename;
4463 },
4464 async ExportGoalDiagnostics() {
4465 return "goal-diagnostics.json";
4466 },
4467 async SaveExportFile(path: string, payload: string, base64Encoded: boolean) {
4468 const a = document.createElement("a");
4469 let url = "";
4470 if (base64Encoded) {
4471 url = `data:application/octet-stream;base64,${payload}`;
4472 } else {
4473 url = URL.createObjectURL(new Blob([payload], { type: "text/plain;charset=utf-8" }));
4474 }
4475 a.href = url;
4476 a.download = path;
4477 document.body.appendChild(a);
4478 a.click();
4479 a.remove();
4480 if (!base64Encoded) URL.revokeObjectURL(url);
4481 },
4482 async SaveSessionMarkdownForTab(_tabID: string, path: string, title: string) {
4483 await this.SaveExportFile(path, `# ${title || "Reasonix session"}\n`, false);
4484 },
4485 async SaveExportImageFiles(path: string, payloads: string[]) {
4486 if (payloads.length === 0) throw new Error("No image payloads to export");
4487 const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
4488 const dot = path.lastIndexOf(".");
4489 const extensionStart = dot > slash ? dot : path.length;
4490 const stem = path.slice(0, extensionStart);
4491 const extension = path.slice(extensionStart);
4492 for (let index = 0; index < payloads.length; index++) {
4493 const partPath = payloads.length > 1
4494 ? `${stem}-${index + 1}-of-${payloads.length}${extension}`
4495 : path;
4496 await this.SaveExportFile(partPath, payloads[index], true);
4497 }
4498 },
4499 async Models() {
4500 const active = mockTabs.find((tab) => tab.active) ?? mockTabs[0];
4501 const current = mockTabModelRef(active);
4502 return mockModelCatalog.map((model) => ({ ...model, current: model.ref === current }));
4503 },
4504 async ModelsForTab(tabID) {
4505 const tab = mockTabs.find((item) => item.id === tabID) ?? mockTabs.find((item) => item.active) ?? mockTabs[0];
4506 const current = mockTabModelRef(tab);
4507 return mockModelCatalog.map((model) => ({ ...model, current: model.ref === current }));
4508 },
4509 async ModelsForDraft(draftID) {
4510 const current = mockDrafts.get(draftID)?.settings.model ?? "";
4511 return mockModelCatalog.map((model) => ({ ...model, current: model.ref === current }));
4512 },
4513 async SetModel(name) {
4514 setMockTabModel(undefined, name);
4515 },
4516 async SetModelForTab(tabID, name) {
4517 setMockTabModel(tabID, name);
4518 },
4519 async Effort() {
4520 return { supported: true, current: mockEffort, default: "high", levels: ["auto", "high", "max"] };
4521 },
4522 async EffortForTab() {
4523 return this.Effort();
4524 },
4525 async SetEffort(level: string) {
4526 mockEffort = level || "auto";
4527 },
4528 async SetEffortForTab(_tabID, level) {
4529 await this.SetEffort(level);
4530 },
4531 async ReloadRuntime(_tabID) {},
4532 async Memory() {
4533 return {
4534 available: true,
4535 storeDir: "~/.reasonix/projects/-mock/memory",
4536 storeGlobalDir: "~/.reasonix/memory/global",
4537 docs: [
4538 {
4539 path: "REASONIX.md",
4540 scope: "project",
4541 directory: ".",
4542 body: "# Reasonix project memory\n\nMock doc shown in the browser dev seam.\n\n## Notes\n\n- prefers concise replies",
4543 imports: [],
4544 depth: 0,
4545 order: 0,
4546 precedence: 0,
4547 },
4548 {
4549 path: "~/.reasonix/REASONIX.md",
4550 scope: "user",
4551 body: t("mock.memoryBody"),
4552 imports: [],
4553 depth: -1,
4554 order: 1,
4555 precedence: 1,
4556 },
4557 ],
4558 instructionDiagnostics: [],
4559 facts: [
4560 {
4561 name: "prefers-tabs",
4562 description: "User prefers tabs",
4563 type: "user",
4564 scope: "project",
4565 body: "Indent with tabs.",
4566 freshness: "fresh",
4567 },
4568 ],
4569 archives: [
4570 {
4571 name: "old-plan",
4572 description: "Superseded planning note",
4573 type: "project",
4574 scope: "project",
4575 body: "This plan was archived after the implementation changed.",
4576 path: "~/.reasonix/projects/-mock/memory/.archive/20260612-021500.000-old-plan.md",
4577 archivedAt: "2026-06-12T02:15:00Z",
4578 freshness: "current",
4579 },
4580 ],
4581 scopes: [
4582 { scope: "user", path: "~/.reasonix/REASONIX.md" },
4583 { scope: "project", path: "REASONIX.md" },
4584 { scope: "local", path: "REASONIX.local.md" },
4585 ],
4586 conflicts: [],
4587 lastRecall: {
4588 query: "",
4589 hits: [],
4590 omitted: 0,
4591 charBudget: 2400,
4592 usedChars: 0,
4593 suppressed: "no user turn yet",
4594 },
4595 };
4596 },
4597 async MemorySuggestions() {
4598 return {
4599 memories: [
4600 {
4601 id: "memory-prefers-concise-replies",
4602 name: "prefers-concise-replies",
4603 title: "Prefers concise replies",
4604 description: "User prefers concise replies unless detail is requested.",
4605 type: "user",
4606 scope: "project",
4607 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.",
4608 reason: "future-facing preference",
4609 evidence: ["mock-session: always keep replies concise"],
4610 },
4611 ],
4612 skills: [
4613 {
4614 id: "skill-reasonix-pr-followup",
4615 name: "reasonix-pr-followup",
4616 description: "Review or update a Reasonix GitHub PR, address feedback, verify, and publish safely.",
4617 scope: "project",
4618 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",
4619 reason: "recent history repeatedly touched PR workflows",
4620 evidence: ["mock-pr-session: 提交到pr,并更新内容", "mock-review-session: 解决该pr下机器人提出来的问题"],
4621 },
4622 ],
4623 generatedAt: new Date().toISOString(),
4624 available: true,
4625 source: "mock",
4626 };
4627 },
4628 async AcceptMemorySuggestion(suggestion: MemorySuggestion) {
4629 emit({ kind: "notice", level: "info", text: `saved suggested memory → ${suggestion.name}` });
4630 return `${suggestion.name}.md`;
4631 },
4632 async AcceptSkillSuggestion(suggestion: SkillSuggestion) {
4633 emit({ kind: "notice", level: "info", text: `created suggested skill → ${suggestion.name}` });
4634 return `.reasonix/skills/${suggestion.name}/SKILL.md`;
4635 },
4636 async MemorySuggestionsForTab(_tabID: string) {
4637 return this.MemorySuggestions();
4638 },
4639 async AcceptMemorySuggestionForTab(_tabID: string, suggestion: MemorySuggestion) {
4640 return this.AcceptMemorySuggestion(suggestion);
4641 },
4642 async AcceptSkillSuggestionForTab(_tabID: string, suggestion: SkillSuggestion) {
4643 return this.AcceptSkillSuggestion(suggestion);
4644 },
4645 async MemoryForTab(_tabID: string) {
4646 return this.Memory();
4647 },
4648 async MemoryRevisions(_ref: string) {
4649 return [];
4650 },
4651 async MemoryRevisionsForTab(_tabID: string, ref: string) {
4652 return this.MemoryRevisions(ref);
4653 },
4654 async RestoreMemoryRevision(ref: string, revision: number) {
4655 emit({ kind: "notice", level: "info", text: `restored revision → ${ref}@${revision}` });
4656 return {
4657 id: ref,
4658 revision: revision + 1,
4659 name: ref,
4660 description: "Restored memory revision",
4661 type: "project",
4662 scope: "project",
4663 body: "Restored guidance.",
4664 freshness: "fresh",
4665 };
4666 },
4667 async RestoreMemoryRevisionForTab(_tabID: string, ref: string, revision: number) {
4668 return this.RestoreMemoryRevision(ref, revision);
4669 },
4670 async Remember(_scope: string, _note: string) {
4671 emit({ kind: "notice", level: "info", text: `remembered → ${_scope}` });
4672 return `${_scope} REASONIX.md (mock): ${_note}`;
4673 },
4674 async RememberForTab(_tabID: string, scope: string, note: string) {
4675 return this.Remember(scope, note);
4676 },
4677 async Forget(_name: string) {
4678 emit({ kind: "notice", level: "info", text: `forgot → ${_name}` });
4679 },
4680 async ForgetForTab(_tabID: string, name: string) {
4681 return this.Forget(name);
4682 },
4683 async RestoreArchivedMemory(archivePath: string) {
4684 emit({ kind: "notice", level: "info", text: `restored → ${archivePath}` });
4685 return {
4686 id: "mock-restored-memory",
4687 revision: 2,
4688 name: "restored-memory",
4689 description: "Recovered archived memory",
4690 type: "project",
4691 scope: "project",
4692 body: "Recovered guidance.",
4693 freshness: "fresh",
4694 };
4695 },
4696 async RestoreArchivedMemoryForTab(_tabID: string, archivePath: string) {
4697 return this.RestoreArchivedMemory(archivePath);
4698 },
4699 async SaveDoc(_path: string, _body: string) {
4700 emit({ kind: "notice", level: "info", text: `saved → ${_path}` });
4701 return _path;
4702 },
4703 async SaveDocForTab(_tabID: string, path: string, body: string) {
4704 return this.SaveDoc(path, body);
4705 },
4706 async DesktopStartupSettings() {
4707 const { bot, desktopLanguage, desktopTheme, desktopThemeStyle, desktopTerminalTheme, displayMode, sessionExperience, reasoningDisplayMode, reasoningDisplayModeExplicit, statusBarStyle, statusBarItems, checkUpdates, updaterEnabled, conversationWidth } = settings;
4708 return JSON.parse(JSON.stringify({
4709 bot,
4710 desktopLanguage,
4711 desktopTheme,
4712 desktopThemeStyle,
4713 desktopTerminalTheme,
4714 displayMode, sessionExperience, reasoningDisplayMode, reasoningDisplayModeExplicit,
4715 statusBarStyle,
4716 statusBarItems,
4717 checkUpdates,
4718 updaterEnabled,
4719 conversationWidth,
4720 })) as DesktopStartupSettingsView;
4721 },
4722 ...makeMockModelSettingsBindings(settings, loadMockProviderCatalog, mockProviderPresetViews),
4723 async StorageSettings() { return { defaultWorkspace: cwd, statePath: `${cwd}/.reasonix`, cachePath: `${cwd}/.reasonix/cache`, extensionsPath: `${cwd}/.reasonix/plugins` }; },
4724 async HooksSettings(scope: string) {
4725 const key = scope === "project" ? "project" : "global";
4726 return JSON.parse(JSON.stringify(hookSettings[key])) as HooksSettingsView;
4727 },
4728 async SaveHooksSettings(scope: string, hooks: HookConfigView[]) {
4729 const key = scope === "project" ? "project" : "global";
4730 hookSettings[key].hooks = JSON.parse(JSON.stringify(hooks)) as HookConfigView[];
4731 },
4732 async SaveHooksSettingsForRoot(scope: string, _projectRoot: string, hooks: HookConfigView[]) {
4733 const key = scope === "project" ? "project" : "global";
4734 hookSettings[key].hooks = JSON.parse(JSON.stringify(hooks)) as HookConfigView[];
4735 },
4736 async TrustProjectHooks() {
4737 // Compatibility no-op: project hooks are enabled automatically.
4738 },
4739 async TrustProjectHooksForRoot(_projectRoot: string) {
4740 // Compatibility no-op: project hooks are enabled automatically.
4741 },
4742 async SetDefaultModel(ref: string) {
4743 settings.defaultModel = ref;
4744 },
4745 async SetPlannerModel(ref: string) {
4746 settings.plannerModel = ref;
4747 },
4748 async SetWebSearchModel(ref: string) {
4749 if (ref && ref !== "auto" && !(settings.webSearchModels ?? []).includes(ref)) throw new Error("Search model unavailable");
4750 settings.webSearchModel = ref || "auto";
4751 settings.effectiveWebSearchModel = settings.webSearchModel;
4752 settings.webSearchModelStatus = "ready";
4753 settings.webSearchModelReason = "";
4754 },
4755 async SetVisionModel(ref: string) {
4756 settings.visionModel = ref;
4757 },
4758 async SetSubagentModel(ref: string) {
4759 settings.subagentModel = ref;
4760 },
4761 async SetSubagentEffort(level: string) {
4762 settings.subagentEffort = level;
4763 },
4764 async SetMaxSubagentDepth(depth: number) {
4765 settings.agent = { ...settings.agent, maxSubagentDepth: depth <= 1 ? 1 : 2 };
4766 },
4767 async SetMaxSubagentConcurrency(n: number) {
4768 const total = Math.max(1, Math.min(32, Math.floor(n) || 6));
4769 const writers = Math.min(total, Math.max(1, settings.agent.maxParallelWriters || 3));
4770 settings.agent = { ...settings.agent, maxSubagentConcurrency: total, maxParallelWriters: writers };
4771 },
4772 async SetMaxParallelWriters(n: number) {
4773 const total = Math.max(1, Math.min(32, settings.agent.maxSubagentConcurrency || 6));
4774 const writers = Math.max(1, Math.min(total, Math.floor(n) || 3));
4775 settings.agent = { ...settings.agent, maxParallelWriters: writers };
4776 },
4777 async SetAutoPlan(mode: string) {
4778 if (mode !== "off") throw new Error("Automatic plan mode has been retired; use Plan Mode explicitly.");
4779 settings.autoPlan = "off";
4780 },
4781 async SetDefaultToolApprovalMode(mode: string) {
4782 settings.defaultToolApprovalMode = normalizeToolApprovalMode(mode);
4783 },
4784 async SetDefaultAutoRecoveryCheckpoint(_enabled: boolean) {
4785 // Legacy no-op; Auto Guard is retired.
4786 },
4787 async SetConnectionKey(name: string, value: string) {
4788 const p = settings.providers.find(p => p.name === name);
4789 if (!p) throw new Error("Connection not found");
4790 p.apiKeyEnv = `REASONIX_CONNECTION_${crypto.randomUUID().replaceAll("-", "")}_KEY`;
4791 p.keySet = Boolean(value.trim());
4792 return "";
4793 },
4794 async AddProviderConnectionWithOptions(_presetID: string, sourceName: string, key: string, baseURL: string, kind: string) {
4795 const source = settings.providers.find(p => p.name === sourceName);
4796 if (!source) throw new Error("Connection template unavailable in preview");
4797 settings.providers.push({...source, kind:kind || source.kind, baseUrl:baseURL || source.baseUrl, requestUrl:"", chatUrl:"", modelsUrl:"", name:`${source.name}-${crypto.randomUUID()}`, builtIn:false, added:true, apiKeyEnv:`REASONIX_CONNECTION_${crypto.randomUUID().replaceAll("-", "")}_KEY`, keySet:Boolean(key.trim())});
4798 return "";
4799 },
4800 async AddProviderConnectionWithURL(_presetID: string, sourceName: string, key: string, baseURL: string) {
4801 const source = settings.providers.find(p => p.name === sourceName);
4802 if (!source) throw new Error("Connection template unavailable in preview");
4803 settings.providers.push({...source, baseUrl:baseURL, requestUrl:"", chatUrl:"", modelsUrl:"", name:`${source.name}-${crypto.randomUUID()}`, builtIn:false, added:true, apiKeyEnv:`REASONIX_CONNECTION_${crypto.randomUUID().replaceAll("-", "")}_KEY`, keySet:Boolean(key.trim())});
4804 return "";
4805 },
4806 async AddProviderConnection(_presetID: string, sourceName: string, key: string) {
4807 const source = settings.providers.find(p => p.name === sourceName);
4808 if (!source) throw new Error("Connection template unavailable in preview");
4809 settings.providers.push({...source, name:`${source.name}-${crypto.randomUUID()}`, builtIn:false, added:true, apiKeyEnv:`REASONIX_CONNECTION_${crypto.randomUUID().replaceAll("-", "")}_KEY`, keySet:Boolean(key.trim())});
4810 return "";
4811 },
4812 async RenameProviderConnections(names: string[], displayName: string) {
4813 for (const name of names) if (!settings.providers.some(p => p.name === name)) throw new Error(`Provider ${name} not found`);
4814 settings.providers = settings.providers.map(p => names.includes(p.name) ? {...p, displayName: displayName.trim()} : p);
4815 },
4816 async SaveProvider(p: ProviderView) {
4817 p.added = true;
4818 const i = settings.providers.findIndex((x) => x.name === p.name);
4819 if (i >= 0) settings.providers[i] = { ...settings.providers[i], ...p };
4820 else settings.providers.push(p);
4821 },
4822 async SetProviderWebSearch(names: string[], enabled: boolean) {
4823 const requested = new Set(names);
4824 settings.providers = settings.providers.map((provider) => (
4825 requested.has(provider.name) ? { ...provider, webSearch: enabled } : provider
4826 ));
4827 },
4828 async SaveProviderModelCatalogs(updates: ProviderModelCatalogUpdate[]) {
4829 const applied: string[] = [];
4830 for (const update of updates) {
4831 const i = settings.providers.findIndex((provider) => provider.name === update.name);
4832 if (i < 0) continue;
4833 const current = settings.providers[i];
4834 if (!update.expectedFingerprint || current.modelCatalogFingerprint !== update.expectedFingerprint) continue;
4835 settings.providers[i] = {
4836 ...current,
4837 models: [...update.models],
4838 default: update.default,
4839 visionModels: [...update.visionModels],
4840 modelCatalogFingerprint: `${update.expectedFingerprint}:updated`,
4841 };
4842 applied.push(update.name);
4843 }
4844 return applied;
4845 },
4846 async SaveProviderWithKey(p: ProviderView, key: string) {
4847 p.added = true;
4848 p.keySet = Boolean(key.trim()) || p.keySet;
4849 const i = settings.providers.findIndex((x) => x.name === p.name);
4850 if (i >= 0) settings.providers[i] = p;
4851 else settings.providers.push(p);
4852 return "";
4853 },
4854 async AddOfficialProviderAccess(kind: string, key: string) {
4855 const templates: Record<string, ProviderView> = {
4856 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: "enabled", webSearch: true, serverWebSearchCapability: true, supportedEfforts: [], defaultEffort: "", modelOverrides: [{ model: "deepseek-v4-flash", reasoningProtocol: "", supportedEfforts: ["disabled", "low", "high", "max"], defaultEffort: "high" }, { model: "deepseek-v4-pro", reasoningProtocol: "", supportedEfforts: ["disabled", "high", "max"], defaultEffort: "high" }] },
4857 };
4858 const next = templates[kind];
4859 if (!next) throw new Error(`unknown official provider template ${kind}`);
4860 const i = settings.providers.findIndex((x) => x.name === next.name);
4861 if (i >= 0) settings.providers[i] = { ...settings.providers[i], ...next, keySet: next.keySet || settings.providers[i].keySet };
4862 else settings.providers.push(next);
4863 return "";
4864 },
4865 async UpgradeDeepSeekProviderAccess(name: string) {
4866 const family = new Set(["deepseek", "deepseek-flash", "deepseek-pro"]);
4867 let changed = false;
4868 settings.providers = settings.providers.map((provider) => {
4869 if ((name === "deepseek" ? family.has(provider.name) : provider.name === name) && provider.recommendedUpgradeAvailable) {
4870 changed = true;
4871 return {
4872 ...provider,
4873 kind: "anthropic",
4874 baseUrl: "https://api.deepseek.com/anthropic",
4875 thinking: provider.thinking || "enabled",
4876 webSearch: provider.webSearch ?? true,
4877 serverWebSearchCapability: true,
4878 recommendedUpgradeAvailable: false,
4879 };
4880 }
4881 return provider;
4882 });
4883 if (!changed) throw new Error(`DeepSeek provider ${name} is not eligible for upgrade`);
4884 return "";
4885 },
4886 async AddProviderPresetAccess(id: string, key: string) {
4887 const preset = settings.providerPresets.find((p) => p.id === id);
4888 if (!preset) throw new Error(`unknown provider preset ${id}`);
4889 const next = cloneMockProviderTemplates(id, key);
4890 if (!next) throw new Error(`unknown provider preset ${id}`);
4891 for (const provider of next) {
4892 const i = settings.providers.findIndex((x) => x.name === provider.name);
4893 if (i >= 0) settings.providers[i] = { ...settings.providers[i], ...provider, keySet: provider.keySet || settings.providers[i].keySet };
4894 else settings.providers.push(provider);
4895 }
4896 preset.added = true;
4897 preset.status = "installed";
4898 preset.statusProviderNames = [...preset.providerNames];
4899 preset.keySet = preset.keySet || !!key.trim();
4900 preset.configured = !preset.requiresKey || preset.keySet;
4901 return "";
4902 },
4903 async ResetProviderPresetAccess(id: string) {
4904 const preset = settings.providerPresets.find((p) => p.id === id);
4905 if (!preset) throw new Error(`unknown provider preset ${id}`);
4906 const next = cloneMockProviderTemplates(id, "");
4907 if (!next) throw new Error(`unknown provider preset ${id}`);
4908 for (const provider of next) {
4909 const i = settings.providers.findIndex((x) => x.name === provider.name);
4910 if (i < 0) throw new Error(`provider preset ${id} cannot be reset because no same-name provider exists`);
4911 const existing = settings.providers[i];
4912 settings.providers[i] = {
4913 ...provider,
4914 added: true,
4915 keySet: existing.apiKeyEnv === provider.apiKeyEnv ? existing.keySet : provider.keySet,
4916 };
4917 }
4918 preset.added = true;
4919 preset.status = "installed";
4920 preset.statusProviderNames = [...preset.providerNames];
4921 preset.keySet = preset.keySet || next.every((provider) => settings.providers.some((item) => item.name === provider.name && item.keySet));
4922 preset.configured = !preset.requiresKey || preset.keySet;
4923 },
4924 async FetchProviderModels(p: ProviderView) {
4925 if (!p.baseUrl.trim()) throw new Error(t("settings.fetchModelsMissingBaseUrl"));
4926 if (providerRequiresKey(p) && !p.apiKeyEnv.trim()) throw new Error(t("settings.fetchModelsMissingKeyEnv"));
4927 await delay(350);
4928 if (p.baseUrl.includes("deepseek")) return ["deepseek-v4-flash", "deepseek-v4-pro"];
4929 if (p.baseUrl.includes("token-plan")) return ["mimo-v2.5", "mimo-v2.5-pro"];
4930 if (p.baseUrl.includes("xiaomimimo")) return ["mimo-v2.5-pro", "mimo-v2.5"];
4931 return ["gpt-5", "gpt-5-mini", "qwen3-coder"];
4932 },
4933 async FetchProviderModelCatalogDraft(p: ProviderView, _key: string) {
4934 return this.FetchProviderModelCatalog(p);
4935 },
4936 async TestProviderModel(_p: ProviderView, _model: string, _key: string) {},
4937 async FetchProviderModelCatalog(p: ProviderView) {
4938 const models = await this.FetchProviderModels(p);
4939 return models.map((model) => ({
4940 model,
4941 inputModalities: p.modelCapabilities?.find((item) => item.model === model)?.inputModalities ?? [],
4942 state: p.modelCapabilities?.find((item) => item.model === model)?.state ?? "unknown",
4943 source: "adapter",
4944 }));
4945 },
4946 async FetchAllProviderModels(providers: ProviderView[]) {
4947 const out: Record<string, string[]> = {};
4948 for (const p of providers) {
4949 try {
4950 out[p.name] = await this.FetchProviderModels(p);
4951 } catch {
4952 out[p.name] = [];
4953 }
4954 }
4955 return out;
4956 },
4957 async FetchAllProviderModelCatalogs(providers: ProviderView[]) {
4958 const out: Record<string, ProviderModelCapabilityView[]> = {};
4959 for (const p of providers) {
4960 try {
4961 out[p.name] = await this.FetchProviderModelCatalog(p);
4962 } catch {
4963 out[p.name] = [];
4964 }
4965 }
4966 return out;
4967 },
4968 async DeleteProvider(name: string) {
4969 settings.providers = settings.providers.filter((p) => p.name !== name);
4970 },
4971 async RemoveProviderAccess(name: string) { settings.providers = removeProviderAccessesForMock(settings.providers, [name]); },
4972 async RemoveProviderAccesses(names: string[]) { settings.providers = removeProviderAccessesForMock(settings.providers, names); },
4973 async SaveProviderKey(apiKeyEnv: string, _value: string) {
4974 settings.providers.forEach((p) => {
4975 if (p.apiKeyEnv === apiKeyEnv) p.keySet = true;
4976 });
4977 return "";
4978 },
4979 async SetProviderKey(apiKeyEnv: string, _value: string) {
4980 settings.providers.forEach((p) => {
4981 if (p.apiKeyEnv === apiKeyEnv) p.keySet = true;
4982 });
4983 return "";
4984 },
4985 async ClearProviderKey(apiKeyEnv: string) {
4986 settings.providers.forEach((p) => {
4987 if (p.apiKeyEnv === apiKeyEnv) p.keySet = false;
4988 });
4989 },
4990 async SetPermissionMode(mode: string) {
4991 settings.permissions.mode = mode;
4992 },
4993 async AddPermissionRule(list: string, rule: string) {
4994 const k = list as "allow" | "ask" | "deny";
4995 if (settings.permissions[k] && !settings.permissions[k].includes(rule)) settings.permissions[k].push(rule);
4996 },
4997 async RemovePermissionRule(list: string, rule: string) {
4998 const k = list as "allow" | "ask" | "deny";
4999 settings.permissions[k] = settings.permissions[k].filter((r) => r !== rule);
5000 },
5001 async ReloadSettings() {},
5002 async SetShellPreference(prefer: string) {
5003 const sb = settings.sandbox;
5004 if (!sb) return;
5005 sb.shell = prefer;
5006 sb.resolvedShell = browserPreviewEffectiveShell(prefer);
5007 sb.shellReloadRequired = sb.resolvedShell !== sb.effectiveShell;
5008 },
5009 async InstallShellSupport(id: string): Promise<ShellInstallResult> {
5010 if (id !== "git-for-windows") throw new Error(`unknown shell support action ${id}`);
5011 if (browserPlatformOverride() !== "windows") return { status: "unsupported_platform", reason: "shell helper install is only available on Windows" };
5012 return {
5013 status: "manual_required",
5014 reason: "automatic installation is disabled because Git for Windows cannot reliably honor user scope",
5015 manualUrl: "https://git-scm.com/download/win",
5016 };
5017 },
5018 async CancelShellInstall() {},
5019 async SetSandbox(bash: string, network: boolean, workspaceRoot: string, allowWrite: string[], shell: string) {
5020 const effectiveWorkspaceRoot = workspaceRoot.trim() || cwd;
5021 const prev = settings.sandbox;
5022 const effectiveShell = browserPreviewEffectiveShell(shell);
5023 const shellSupport = browserPreviewShellSupport(browserPlatformOverride());
5024 settings.sandbox = { bash, network, workspaceRoot, allowWrite, effectiveWorkspaceRoot, effectiveWriteRoots: [effectiveWorkspaceRoot, ...allowWrite], shell, effectiveShell,
5025 resolvedShell: effectiveShell, shellReloadRequired: false,
5026 shellCapabilities: prev?.shellCapabilities ?? shellSupport.shellCapabilities, shellInstallAction: prev?.shellInstallAction ?? shellSupport.shellInstallAction,
5027 shellRepairGuidance: prev?.shellRepairGuidance ?? shellSupport.shellRepairGuidance,
5028 gitCapability: prev?.gitCapability ?? shellSupport.gitCapability, gitRepairGuidance: prev?.gitRepairGuidance ?? shellSupport.gitRepairGuidance };
5029 },
5030 async SetNetwork(n: NetworkView) {
5031 settings.network = n;
5032 },
5033 async SetBotSettings(b: BotSettingsView) {
5034 settings.bot = JSON.parse(JSON.stringify(b)) as BotSettingsView;
5035 },
5036 async SetBotConnectionToolApprovalMode(connID, mode) {
5037 const conn = settings.bot.connections.find((c) => c.id === connID);
5038 if (conn) conn.toolApprovalMode = mode as any;
5039 },
5040 async SetBotDingtalkToolApprovalMode(mode) {
5041 settings.bot.dingtalk.toolApprovalMode = mode as any;
5042 },
5043 async SetBotSecret(envName: string, _value: string) {
5044 const name = envName.trim();
5045 if (settings.bot.qq.appSecretEnv === name) settings.bot.qq.secretSet = true;
5046 if (settings.bot.feishu.appSecretEnv === name) settings.bot.feishu.secretSet = true;
5047 if (settings.bot.weixin.tokenEnv === name) settings.bot.weixin.tokenSet = true;
5048 settings.bot.connections = settings.bot.connections.map((connection) => ({
5049 ...connection,
5050 credential: connection.credential.appSecretEnv === name || connection.credential.tokenEnv === name
5051 ? { ...connection.credential, secretSet: true }
5052 : connection.credential,
5053 }));
5054 },
5055 async ClearBotSecret(envName: string) {
5056 const name = envName.trim();
5057 if (settings.bot.qq.appSecretEnv === name) settings.bot.qq.secretSet = false;
5058 if (settings.bot.feishu.appSecretEnv === name) settings.bot.feishu.secretSet = false;
5059 if (settings.bot.weixin.tokenEnv === name) settings.bot.weixin.tokenSet = false;
5060 settings.bot.connections = settings.bot.connections.map((connection) => ({
5061 ...connection,
5062 credential: connection.credential.appSecretEnv === name || connection.credential.tokenEnv === name
5063 ? { ...connection.credential, secretSet: false }
5064 : connection.credential,
5065 }));
5066 },
5067 async BotRuntimeStatus() {
5068 const qqRunning = settings.bot.qq.enabled && settings.bot.qq.appId.trim() && settings.bot.qq.secretSet;
5069 const runningConnections = (qqRunning ? 1 : 0) + settings.bot.connections.filter((connection) => connection.enabled && connection.status === "connected").length;
5070 const dingtalkRunning = Boolean(settings.bot.dingtalk.enabled && settings.bot.dingtalk.clientId.trim() && settings.bot.dingtalk.secretSet);
5071 const running = settings.bot.enabled && runningConnections > 0;
5072 return {
5073 running,
5074 status: running ? "running" : "stopped",
5075 message: running ? `${runningConnections} bot connection(s) running` : "bot runtime is not started",
5076 connections: runningConnections,
5077 startedAt: running ? new Date(t0).toISOString() : "",
5078 platforms: {
5079 dingtalk: dingtalkRunning ? "running" : settings.bot.dingtalk.clientId.trim() ? "configured" : "",
5080 qq: qqRunning ? "running" : settings.bot.qq.appId.trim() ? "configured" : "",
5081 ...Object.fromEntries(settings.bot.connections
5082 .filter((connection) => connection.provider !== "qq")
5083 .map((connection) => [connection.provider, connection.enabled && connection.status === "connected" ? "running" : connection.credential.secretSet ? "configured" : ""])),
5084 },
5085 };
5086 },
5087 async StartBotConnectionInstall(provider: string, domain: string) {
5088 const normalizedProvider = provider === "weixin" ? "weixin" : "feishu";
5089 const normalizedDomain = normalizedProvider === "weixin" ? "weixin" : domain === "lark" ? "lark" : "feishu";
5090 return {
5091 ok: true,
5092 provider: normalizedProvider,
5093 domain: normalizedDomain,
5094 installId: `mock-${normalizedProvider}-${normalizedDomain}`,
5095 url: "https://example.com/reasonix-bot-qr",
5096 deviceCode: "MOCKDEVICE",
5097 userCode: normalizedProvider === "weixin" ? "" : "MOCK-CODE",
5098 interval: 3,
5099 expireIn: 300,
5100 message: "",
5101 };
5102 },
5103 async PollBotConnectionInstall(installID: string) {
5104 const isWeixin = installID.includes("weixin");
5105 const domain = installID.includes("lark") ? "lark" : isWeixin ? "weixin" : "feishu";
5106 const provider = isWeixin ? "weixin" : "feishu";
5107 const connection = {
5108 id: `${provider}-${domain}`,
5109 provider,
5110 domain,
5111 label: domain === "lark" ? "Lark" : domain === "weixin" ? "微信" : "飞书",
5112 enabled: true,
5113 status: "connected",
5114 model: "",
5115 toolApprovalMode: "",
5116 workspaceRoot: "",
5117 access: { enabled: true, allowAll: false, pairingEnabled: true, users: [provider === "weixin" ? "wxid_mock_user_001" : "ou_mock_user_001"], groups: [], approvers: [], admins: [] },
5118 credential: {
5119 appId: provider === "feishu" ? "cli_mock" : "",
5120 appSecretEnv: provider === "feishu" ? (domain === "lark" ? "LARK_BOT_APP_SECRET" : "FEISHU_BOT_APP_SECRET") : "",
5121 accountId: provider === "weixin" ? "mock-account" : "",
5122 tokenEnv: provider === "weixin" ? "WEIXIN_BOT_TOKEN" : "",
5123 secretSet: true,
5124 },
5125 sessionMappings: [],
5126 lastError: "",
5127 createdAt: new Date().toISOString(),
5128 updatedAt: new Date().toISOString(),
5129 };
5130 settings.bot.connections = [...settings.bot.connections.filter((c) => c.id !== connection.id), connection];
5131 return { done: true, connection, status: "connected", message: "connected", error: "" };
5132 },
5133 async DiagnoseBotConnection(id: string) {
5134 const connection = settings.bot.connections.find((c) => c.id === id);
5135 const occurredAt = new Date().toISOString();
5136 return connection
5137 ? { 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 }
5138 : { 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 };
5139 },
5140 async TestBotConnection(id: string, target?: string) {
5141 const diag = await this.DiagnoseBotConnection(id);
5142 if (target?.trim()) return { ...diag, message: `Mock test sent to ${target.trim()}`, messageId: "mock-message-id" };
5143 return diag;
5144 },
5145 async TestDingtalkBot() {
5146 const occurredAt = new Date().toISOString();
5147 return { id: "dingtalk", label: "DingTalk", status: "ok", message: "Mock dingtalk test sent", messageId: "mock-dingtalk-id", phase: "send", code: "dingtalk_test_send_ok", reportKind: "", reportDetail: "", occurredAt };
5148 },
5149 ...createDesktopPreferencesMock(settings),
5150 async SetDesktopAppearance(theme: string, style: string) {
5151 settings.desktopTheme = theme === "auto" || theme === "light" ? theme : "dark";
5152 settings.desktopThemeStyle = style;
5153 mockThemeMode = settings.desktopTheme as "auto" | "light" | "dark";
5154 if (["graphite","aurora","slate","carbon","nocturne","amber"].includes(style)) {
5155 mockBaseStyle = style;
5156 }
5157 },
5158 async SetDesktopTerminalTheme(theme: string) {
5159 settings.desktopTerminalTheme = theme === "dark" || theme === "light" ? theme : "auto";
5160 },
5161 async ListThemePacks() {
5162 const baseActive = !mockActiveThemeId;
5163 return mockThemePacks.map((p) => {
5164 const kind = p.kind || (p.builtin ? "base" : "user");
5165 let active = false;
5166 if (kind === "base") active = baseActive && p.id === mockBaseStyle;
5167 else active = p.id === mockActiveThemeId;
5168 return { ...p, active, tokens: { light: { ...(p.tokens.light || {}) }, dark: { ...(p.tokens.dark || {}) } }, recipes: { ...p.recipes } };
5169 });
5170 },
5171 async GetActiveThemePack() {
5172 // Base style ids are never active packs in the redesigned model.
5173 const pack = mockActiveThemeId && !["graphite","aurora","slate","carbon","nocturne","amber"].includes(mockActiveThemeId)
5174 ? mockThemePacks.find((p) => p.id === mockActiveThemeId)
5175 : null;
5176 return { activeThemeId: pack ? mockActiveThemeId : "", pack: pack ? { ...pack, active: true } : null };
5177 },
5178 async GetThemeExperience() {
5179 const pack = mockActiveThemeId && !["graphite","aurora","slate","carbon","nocturne","amber"].includes(mockActiveThemeId)
5180 ? mockThemePacks.find((p) => p.id === mockActiveThemeId)
5181 : null;
5182 return {
5183 themeMode: mockThemeMode,
5184 baseStyle: mockBaseStyle,
5185 effectiveStyle: pack?.baseStyle || mockBaseStyle,
5186 activeThemeId: pack ? mockActiveThemeId : "",
5187 activePack: pack ? { ...pack, active: true } : null,
5188 };
5189 },
5190 async ActivateThemePack(id: string) {
5191 const next = String(id || "").trim();
5192 if (["graphite","aurora","slate","carbon","nocturne","amber"].includes(next)) {
5193 throw new Error(`base style ${next} is not a theme pack; use ActivateBaseStyle`);
5194 }
5195 mockActiveThemeId = next;
5196 },
5197 async ActivateBaseStyle(style: string) {
5198 const s = String(style || "").trim().toLowerCase();
5199 if (!["graphite","aurora","slate","carbon","nocturne","amber"].includes(s)) {
5200 throw new Error(`unknown base style ${s}`);
5201 }
5202 mockBaseStyle = s;
5203 mockActiveThemeId = "";
5204 },
5205 async DisableThemePack() {
5206 mockActiveThemeId = "";
5207 },
5208 async RestoreGraphiteAppearance() {
5209 mockBaseStyle = "graphite";
5210 mockActiveThemeId = "";
5211 },
5212 async ResetThemePack() {
5213 mockActiveThemeId = "";
5214 },
5215 async SaveThemePack(input: import("./themePack").ThemeSaveInput) {
5216 const pack: import("./themePack").ThemePackView = {
5217 id: input.id,
5218 name: input.name,
5219 author: input.author,
5220 description: input.description,
5221 license: input.license,
5222 baseStyle: input.baseStyle,
5223 builtin: false,
5224 kind: "user",
5225 active: Boolean(input.activate),
5226 hasBackground: Boolean(
5227 (input.background && (input.backgroundDataUrl || input.background.image)) ||
5228 (input.taskBackground && (input.taskBackgroundDataUrl || input.taskBackground.image)),
5229 ),
5230 backgroundUrl: input.backgroundDataUrl || "",
5231 taskBackgroundUrl: input.taskBackgroundDataUrl || "",
5232 tokens: input.tokens || {},
5233 recipes: input.recipes || { density: "comfortable", corners: "soft" },
5234 background: input.background ?? undefined,
5235 taskBackground: input.taskBackground ?? undefined,
5236 };
5237 const idx = mockThemePacks.findIndex((p) => p.id === pack.id);
5238 if (idx >= 0) mockThemePacks[idx] = pack;
5239 else mockThemePacks.push(pack);
5240 if (input.activate) mockActiveThemeId = pack.id;
5241 return pack;
5242 },
5243 async DeleteThemePack(id: string) {
5244 mockThemePacks = mockThemePacks.filter((p) => p.id !== id || p.builtin);
5245 if (mockActiveThemeId === id) mockActiveThemeId = "";
5246 },
5247 async CopyThemePack(sourceID: string, newID: string, newName: string) {
5248 const src = mockThemePacks.find((p) => p.id === sourceID);
5249 if (!src) throw new Error("source theme not found");
5250 const pack: import("./themePack").ThemePackView = {
5251 ...src,
5252 id: newID,
5253 name: newName || `${src.name} Copy`,
5254 builtin: false,
5255 kind: "user",
5256 nameKey: undefined,
5257 descriptionKey: undefined,
5258 active: false,
5259 };
5260 mockThemePacks.push(pack);
5261 return pack;
5262 },
5263 async ImportThemePack(_sourcePath: string, replace: boolean) {
5264 if (replace) {
5265 return { pack: mockThemePacks[0], replaced: true };
5266 }
5267 // Simulate conflict path without re-prompting for a file on confirm.
5268 return { pack: mockThemePacks[0], replaced: false, needsReplace: true, pendingId: "pending-mock" };
5269 },
5270 async ExportThemePack(_id: string, _destPath: string) {
5271 return "";
5272 },
5273 async PickThemeBackground() {
5274 return "";
5275 },
5276 // The layout-style preference was removed from the UI; the binding
5277 // stays because the generated host contract still declares it.
5278 async SetDesktopLayoutStyle(_style: string) {},
5279 async SetDesktopZoomFactor(factor: number) {
5280 mockDesktopZoomFactor = Math.min(2.0, Math.max(0.5, Number.isFinite(factor) ? factor : 1.0));
5281 },
5282 async GetDesktopZoomFactor() {
5283 return mockDesktopZoomFactor;
5284 },
5285 async RestartApplication() {
5286 // no-op in mock
5287 },
5288 async ReportDesktopWebViewReady() {
5289 // no-op in mock
5290 },
5291 async GetDesktopShellStatus() {
5292 return { trayState: "ready", backgroundCloseAvailable: true } as DesktopShellStatusView;
5293 },
5294 async SetAgentParams(temperature: number, maxSteps: number, plannerMaxSteps: number, systemPrompt: string) {
5295 settings.agent = { ...settings.agent, temperature, maxSteps, plannerMaxSteps, systemPrompt };
5296 },
5297 async SetCompactRatio(ratio: number) {
5298 if (!Number.isFinite(ratio)
5299 || ratio < COMPACT_RATIO_MIN_PERCENT / 100
5300 || ratio > COMPACT_RATIO_MAX_PERCENT / 100) {
5301 throw new Error(`compact ratio must be between ${COMPACT_RATIO_MIN_PERCENT / 100} and ${COMPACT_RATIO_MAX_PERCENT / 100}`);
5302 }
5303 settings.agent = { ...settings.agent, compactRatio: ratio };
5304 },
5305 async SetReasoningLanguage(lang: string) {
5306 const normalized = lang === "zh" || lang === "en" ? lang : "auto";
5307 settings.agent = { ...settings.agent, reasoningLanguage: normalized };
5308 },
5309 // ── Heartbeat mock ──
5310 async HeartbeatListTasks() { return mockHeartbeatTasks; },
5311 async HeartbeatReloadTasks() { return mockHeartbeatTasks; },
5312 async HeartbeatSaveTasks(tasks: unknown) { mockHeartbeatTasks = Array.isArray(tasks) ? tasks : []; },
5313 async HeartbeatReloadConfig() { return { revision: mockHeartbeatRevision, etag: `mock-${mockHeartbeatRevision}`, tasks: mockHeartbeatTasks }; },
5314 async HeartbeatSaveConfig(update: unknown) {
5315 const tasks = (update as { tasks?: unknown[] } | null)?.tasks;
5316 mockHeartbeatTasks = Array.isArray(tasks) ? tasks : [];
5317 mockHeartbeatRevision += 1;
5318 return { revision: mockHeartbeatRevision, etag: `mock-${mockHeartbeatRevision}`, tasks: mockHeartbeatTasks };
5319 },
5320 async HeartbeatTriggerNow(_id: string) {},
5321 async HeartbeatGenerateID() { return "mock-" + Date.now().toString(36); },
5322 async ListTasks() { return []; },
5323 async CurrentTaskSessionID() { return ""; },
5324 async ListTasksForSession() { return []; },
5325 async GetTask() { return null; },
5326 async StopTask() { return { schema_version: 1, command: "stop", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
5327 async CancelTask() { return { schema_version: 1, command: "cancel", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
5328 async ListTasksForTab() { return []; },
5329 ...makeMockTaskCatalogBindings(),
5330 async ListTaskEventsForTab() { return []; },
5331 async StopTaskForTab() { return { schema_version: 1, command: "stop", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
5332 async CancelTaskForTab() { return { schema_version: 1, command: "cancel", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
5333 async RequeueTaskForTab() { return { schema_version: 1, command: "requeue", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
5334 async OpenTaskSessionForTab() { return { schema_version: 1, command: "open_session", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; },
5335 async SetTrayLocale(_locale: "en" | "zh" | "zh-TW") {},
5336 async SetAutoApproveTools(_on: boolean) {
5337 await this.SetToolApprovalMode("workspace-write");
5338 },
5339 async SetBypass(on: boolean) {
5340 await this.SetAutoApproveTools(on);
5341 },
5342 async Version() {
5343 return "v1.0.0 (browser dev)";
5344 },
5345 async CheckUpdate(channel: string) {
5346 void channel;
5347 // Keep the default browser preview focused on the primary product surface.
5348 // Updater methods remain mocked for explicit updater-flow tests.
5349 return {
5350 available: false,
5351 current: "v1.0.0",
5352 latest: "v1.0.0",
5353 notes: "",
5354 channel: "stable",
5355 canSelfUpdate: false,
5356 manualOnly: true,
5357 installMode: "manual",
5358 manualReason: "browser preview",
5359 downloaded: false,
5360 downloadUrl: "",
5361 assetSize: 0,
5362 };
5363 },
5364 async ApplyUpdateRequest(channel: string, expectedVersion: string, requestId: string) {
5365 void channel;
5366 const selectedChannel = "stable";
5367 const total = 12_345_678;
5368 for (let r = 0; r <= total; r += 1_800_000) {
5369 emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "downloading", received: Math.min(r, total), total });
5370 await delay(120);
5371 }
5372 emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "verifying", received: total, total });
5373 await delay(300);
5374 emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "installing", received: total, total });
5375 await delay(300);
5376 emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "relaunching", received: 0, total: 0 });
5377 },
5378 async AbandonPendingUpdate() {},
5379 async OpenDownloadPage() {
5380 if (typeof window !== "undefined") {
5381 window.open("https://reasonix.io/?download=desktop#start", "_blank", "noopener");
5382 }
5383 },
5384 async OpenUserConfigPath() {},
5385 async ReloadUserConfig() {
5386 return { configWarnings: [], configWarningsRevision: 0, configPath: "" };
5387 },
5388 // Dev seam: match the backend's provider-agnostic onboarding predicate.
5389 async NeedsOnboarding() {
5390 return !settings.providers.some((p) => p.models.length > 0 && providerIsConfigured(p));
5391 },
5392 async ConnectKey(apiKey: string) {
5393 if (!apiKey.trim()) throw new Error("key is required");
5394 // Match the production onboarding path: saving a DeepSeek key also
5395 // restores the current official provider template instead of merely
5396 // marking a possibly stale legacy entry as configured.
5397 await this.AddOfficialProviderAccess("deepseek", apiKey);
5398 await delay(300);
5399 return "";
5400 },
5401 async ReportCrash() { await delay(300); },
5402 async RecordUIPerf() {},
5403 // Tab management mocks.
5404 async ListTabs() {
5405 return mockTabs.map((tab) => ({ ...tab }));
5406 },
5407 async OpenProjectTab(workspaceRoot: string, _topicID: string) {
5408 const existing = mockTabs.find((tab) => tab.scope === "project" && tab.workspaceRoot === workspaceRoot && tab.topicId === _topicID);
5409 if (existing) {
5410 const active = { ...existing, active: true, running: mockTopicRunsInScenario(_topicID) };
5411 mockTabs = mockTabs.map((tab) => (tab.id === existing.id ? active : { ...tab, active: false }));
5412 return { ...active };
5413 }
5414 const pruned = restoreMockPrunedTab("project", workspaceRoot, _topicID);
5415 if (pruned) {
5416 const restored = { ...pruned, active: true, running: mockTopicRunsInScenario(_topicID) };
5417 mockTabs = [...mockTabs.map((item) => ({ ...item, active: false })), restored];
5418 return { ...restored };
5419 }
5420 const defaultToolApprovalMode = normalizeToolApprovalMode(settings.defaultToolApprovalMode);
5421 const tab: TabMeta = {
5422 id: "tab_" + Date.now(),
5423 scope: "project",
5424 workspaceRoot,
5425 workspaceName: workspaceRoot.split("/").filter(Boolean).pop() ?? workspaceRoot,
5426 workspacePath: workspaceRoot,
5427 gitBranch: "main",
5428 topicId: _topicID,
5429 topicTitle: topicLabel(_topicID, t("mock.newSession")),
5430 sessionPath: `/mock/sessions/${_topicID}.jsonl`,
5431 projectColor: mockProjectTree.find((node) => node.root === workspaceRoot)?.projectColor,
5432 label: mockModelLabel(settings.defaultModel),
5433 ready: true,
5434 running: mockTopicRunsInScenario(_topicID),
5435 mode: modeWithAutoApproveTools("normal", defaultToolApprovalMode === "danger-full-access"),
5436 collaborationMode: "normal",
5437 toolApprovalMode: defaultToolApprovalMode,
5438 tokenMode: "full",
5439 active: true,
5440 cwd: workspaceRoot,
5441 };
5442 mockTabs = [...mockTabs.map((item) => ({ ...item, active: false })), tab];
5443 return { ...tab };
5444 },
5445 async IsolatedWorktreeAvailability(workspaceRoot: string) {
5446 return workspaceRoot
5447 ? { available: true, repoRoot: workspaceRoot, branch: "main", sourceDirty: false }
5448 : { available: false, reason: "project folder is required" };
5449 },
5450 async DeliveryWorktreeAvailability(workspaceRoot: string) {
5451 return this.IsolatedWorktreeAvailability(workspaceRoot);
5452 },
5453 ...makeMockQualityFloorBindings(() => mockTabs, (next) => { mockTabs = next; }),
5454 async SetAgentPreset(_preset: string) {},
5455 async SetAgentPresetForTab(_tabID: string, _preset: string) {},
5456 async SetTokenMode(_mode: string) {},
5457 async SetTokenModeForTab(_tabID: string, _mode: string) {},
5458 async CreateIsolatedWorktree(workspaceRoot: string) {
5459 if (!workspaceRoot) throw new Error("project folder is required");
5460 const suffix = Date.now().toString(36);
5461 const isolatedRoot = `/mock/reasonix-worktrees/${suffix}/${workspaceRoot.split("/").filter(Boolean).pop() ?? "project"}`;
5462 const topicID = `topic_worktree_${suffix}`;
5463 const tab = await this.OpenProjectTab(isolatedRoot, topicID);
5464 tab.isolatedWorktree = true;
5465 tab.gitBranch = `reasonix/isolated-${suffix}`;
5466 mockTabs = mockTabs.map((candidate) => candidate.id === tab.id ? { ...tab } : candidate);
5467 return {
5468 workspaceRoot: isolatedRoot,
5469 worktreeRoot: isolatedRoot,
5470 sourceRoot: workspaceRoot,
5471 branch: tab.gitBranch,
5472 sourceDirty: false,
5473 tab,
5474 };
5475 },
5476 async CreateDeliveryWorktree(workspaceRoot: string) {
5477 return this.CreateIsolatedWorktree(workspaceRoot);
5478 },
5479 ...makeMockWorktreeMergeBindings(() => mockTabs, (next) => { mockTabs = next; }),
5480 async OpenGlobalTab(_topicID: string) {
5481 const existing = mockTabs.find((tab) => tab.scope === "global" && tab.topicId === _topicID);
5482 if (existing) {
5483 setMockActiveTab(existing.id);
5484 return { ...existing, active: true };
5485 }
5486 const defaultToolApprovalMode = normalizeToolApprovalMode(settings.defaultToolApprovalMode);
5487 const tab: TabMeta = {
5488 id: "tab_" + Date.now(),
5489 scope: "global",
5490 workspaceRoot: globalWorkspaceRoot,
5491 workspaceName: "Global",
5492 workspacePath: globalWorkspaceRoot,
5493 topicId: _topicID,
5494 topicTitle: topicLabel(_topicID, "Global"),
5495 sessionPath: `/mock/sessions/${_topicID}.jsonl`,
5496 label: mockModelLabel(settings.defaultModel),
5497 ready: true,
5498 running: false,
5499 mode: modeWithAutoApproveTools("normal", defaultToolApprovalMode === "danger-full-access"),
5500 collaborationMode: "normal",
5501 toolApprovalMode: defaultToolApprovalMode,
5502 tokenMode: "full",
5503 active: true,
5504 cwd: globalWorkspaceRoot,
5505 };
5506 mockTabs = [...mockTabs.map((item) => ({ ...item, active: false })), tab];
5507 return { ...tab };
5508 },
5509 async OpenTopicSession(scope: string, workspaceRoot: string, topicID: string, sessionPath: string) {
5510 const tab = scope === "project"
5511 ? await this.OpenProjectTab(workspaceRoot, topicID)
5512 : await this.OpenGlobalTab(topicID);
5513 const active = { ...tab, sessionPath };
5514 mockTabs = mockTabs.map((item) => (item.id === tab.id ? active : item));
5515 return { ...active };
5516 },
5517 async EnsureBlankTab(scope: string, workspaceRoot: string) {
5518 const targetScope = scope === "project" && workspaceRoot ? "project" : "global";
5519 const targetRoot = targetScope === "project" ? workspaceRoot : "";
5520 const existing = mockTabs.find((tab) =>
5521 tab.scope === targetScope &&
5522 (targetScope === "global" || tab.workspaceRoot === targetRoot) &&
5523 !tab.running &&
5524 mockTopicIsBlank(tab.topicId)
5525 );
5526 if (existing) {
5527 setMockActiveTab(existing.id);
5528 return { ...existing, active: true };
5529 }
5530 const topic = await this.CreateTopic(targetScope, targetRoot, "");
5531 return targetScope === "global" ? this.OpenGlobalTab(topic.id) : this.OpenProjectTab(targetRoot, topic.id);
5532 },
5533 async ActivateTopic(scope: string, workspaceRoot: string, topicID: string, sessionPath: string) {
5534 const tab = sessionPath
5535 ? await this.OpenTopicSession(scope, workspaceRoot, topicID, sessionPath)
5536 : scope === "project"
5537 ? await this.OpenProjectTab(workspaceRoot, topicID)
5538 : await this.OpenGlobalTab(topicID);
5539 pruneMockTabsTo(tab.id);
5540 return { ...mockTabs[0] };
5541 },
5542 async StartTopicActivation(req: TopicActivationRequest): Promise<TopicActivationTicket> {
5543 // Publish identity first; terminal runtime events arrive asynchronously.
5544 const tab = req.sessionPath
5545 ? await this.OpenTopicSession(req.scope, req.workspaceRoot, req.topicId, req.sessionPath)
5546 : req.scope === "project"
5547 ? await this.OpenProjectTab(req.workspaceRoot, req.topicId)
5548 : await this.OpenGlobalTab(req.topicId);
5549 pruneMockTabsTo(tab.id);
5550 const requestId = req.requestId?.trim() || `mock-act-${++mockTopicActivationCounter}`;
5551 const previous = mockPendingTopicActivation;
5552 mockPendingTopicActivation = { requestId, tabId: tab.id };
5553 if (previous && previous.requestId !== requestId) {
5554 __emitMockTopicActivation({ requestId: previous.requestId, tabId: previous.tabId, phase: "cancelled" });
5555 }
5556 __emitMockTopicActivation({ requestId, tabId: tab.id, phase: "starting" });
5557 const meta = { ...mockTabs[0] };
5558 window.setTimeout(() => {
5559 if (mockPendingTopicActivation?.requestId !== requestId) return;
5560 mockPendingTopicActivation = undefined;
5561 __emitMockTopicActivation({ requestId, tabId: tab.id, phase: "ready" });
5562 }, 0);
5563 return { requestId, tabId: tab.id, meta };
5564 },
5565 async EnsureBlankSurface(scope: string, workspaceRoot: string) {
5566 const tab = await this.EnsureBlankTab(scope, workspaceRoot);
5567 pruneMockTabsTo(tab.id);
5568 return { ...mockTabs[0] };
5569 },
5570 async SetActiveTab(_tabID: string) {
5571 const tab = mockTabs.find((item) => item.id === _tabID);
5572 if (!tab) throw new Error(`tab ${_tabID} not found`);
5573 setMockActiveTab(_tabID);
5574 if (!tab.remote) queueMockTopicRuntime(tab);
5575 },
5576 async ReorderTabs(_tabIDs: string[]) {
5577 const byId = new Map(mockTabs.map((tab) => [tab.id, tab]));
5578 const ordered = _tabIDs.map((id) => byId.get(id)).filter((tab): tab is TabMeta => Boolean(tab));
5579 if (ordered.length === mockTabs.length) mockTabs = ordered;
5580 },
5581 async CloseTab(_tabID: string) {
5582 if (mockTabs.length <= 1) return;
5583 const terminalIDs = mockTerminalSessions
5584 .filter((session) => mockTerminalTabIDs.get(session.id) === _tabID)
5585 .map((session) => session.id);
5586 mockTerminalSessions = mockTerminalSessions.filter((session) => !terminalIDs.includes(session.id));
5587 terminalIDs.forEach((id) => {
5588 mockTerminalOutput.delete(id);
5589 mockTerminalTabIDs.delete(id);
5590 __emitMockTerminalExit({ id, exitCode: 0, removed: true });
5591 });
5592 const wasActive = mockTabs.some((tab) => tab.id === _tabID && tab.active);
5593 mockTabs = mockTabs.filter((tab) => tab.id !== _tabID);
5594 if (wasActive && mockTabs.length > 0 && !mockTabs.some((tab) => tab.active)) {
5595 mockTabs[mockTabs.length - 1] = { ...mockTabs[mockTabs.length - 1], active: true };
5596 }
5597 },
5598 async TerminalWorkspaceForTab(tabID: string) {
5599 const tab = mockTabs.find((candidate) => candidate.id === tabID) ?? mockTabs.find((candidate) => candidate.active);
5600 const sessions = tab ? mockTerminalSessions.filter((session) => mockTerminalTabIDs.get(session.id) === tab.id) : [];
5601 return {
5602 available: true,
5603 readOnly: Boolean(tab?.readOnly),
5604 sessions: sessions.map((session) => ({ ...session })),
5605 shells: [
5606 { id: "default", label: "Default shell" },
5607 { id: "bash", label: "bash" },
5608 { id: "zsh", label: "zsh" },
5609 ],
5610 };
5611 },
5612 async TerminalOutputForTab(tabID: string, sessionID: string) {
5613 if (mockTerminalTabIDs.get(sessionID) !== tabID) return "";
5614 return mockTerminalOutput.get(sessionID) ?? "";
5615 },
5616 async CreateTerminalForTab(tabID: string, relativePath: string, shellID: string) {
5617 const tab = mockTabs.find((candidate) => candidate.id === tabID) ?? mockTabs.find((candidate) => candidate.active);
5618 if (tab?.readOnly) throw new Error("channel session is read-only");
5619 const id = `term-mock-${Date.now()}-${Math.random().toString(16).slice(2)}`;
5620 const session: TerminalSessionView = {
5621 id,
5622 title: shellID || "Default shell",
5623 shell: shellID || "default",
5624 cwd: `${tab?.cwd || cwd}/${relativePath || "."}`.replace(/\/\.\/?$/, ""),
5625 createdAt: Date.now(),
5626 running: true,
5627 };
5628 mockTerminalSessions = [...mockTerminalSessions, session];
5629 mockTerminalOutput.set(id, "Reasonix terminal ready\r\n");
5630 mockTerminalTabIDs.set(id, tabID);
5631 window.setTimeout(() => __emitMockTerminalOutput({ id, data: mockTerminalBytes("Reasonix terminal ready\r\n") }), 0);
5632 return { ...session };
5633 },
5634 async WriteTerminalForTab(_tabID: string, sessionID: string, data: string) {
5635 const session = mockTerminalSessions.find((candidate) => candidate.id === sessionID);
5636 if (!session?.running) throw new Error("terminal session has exited");
5637 mockTerminalOutput.set(sessionID, `${mockTerminalOutput.get(sessionID) ?? ""}${data}`);
5638 window.setTimeout(() => __emitMockTerminalOutput({ id: sessionID, data: mockTerminalBytes(data) }), 0);
5639 },
5640 async ResizeTerminalForTab() {},
5641 async CloseTerminalForTab(_tabID: string, sessionID: string) {
5642 mockTerminalSessions = mockTerminalSessions.filter((session) => session.id !== sessionID);
5643 mockTerminalOutput.delete(sessionID);
5644 mockTerminalTabIDs.delete(sessionID);
5645 __emitMockTerminalExit({ id: sessionID, exitCode: 0, removed: true });
5646 },
5647 async RenameTerminalForTab(_tabID: string, sessionID: string, title: string) {
5648 mockTerminalSessions = mockTerminalSessions.map((session) => session.id === sessionID ? { ...session, title } : session);
5649 },
5650 async ListProjectTree() {
5651 return cloneProjectTree();
5652 },
5653 async RenameProject(workspaceRoot: string, title: string) {
5654 const node = workspaceRoot
5655 ? mockProjectTree.find((item) => item.root === workspaceRoot)
5656 : mockProjectTree.find((item) => item.kind === "global_folder");
5657 if (node) node.label = title.trim() || (node.kind === "global_folder" ? "Global" : node.label);
5658 },
5659 async SetProjectColor(workspaceRoot: string, color: string) {
5660 const node = workspaceRoot
5661 ? mockProjectTree.find((item) => item.root === workspaceRoot)
5662 : mockProjectTree.find((item) => item.kind === "global_folder");
5663 if (!node) return;
5664 node.projectColor = color || undefined;
5665 for (const child of projectChildren(node)) child.projectColor = node.projectColor;
5666 mockTabs = mockTabs.map((tab) =>
5667 (workspaceRoot ? tab.workspaceRoot === workspaceRoot : tab.scope === "global")
5668 ? { ...tab, projectColor: node.projectColor }
5669 : tab,
5670 );
5671 },
5672 async SetProjectPinned(workspaceRoot: string, pinned: boolean) {
5673 setMockProjectPinned(workspaceRoot, pinned);
5674 },
5675 async ReorderProjects(workspaceRoots: string[]) {
5676 const projects = mockProjectTree.filter((node) => node.kind === "project");
5677 const globals = mockProjectTree.filter((node) => node.kind === "global_folder");
5678 if (!workspaceRoots.includes(GLOBAL_PROJECT_ORDER_KEY)) {
5679 if (workspaceRoots.length !== projects.length) return;
5680 const byRoot = new Map(projects.map((node) => [node.root, node]));
5681 const ordered = workspaceRoots.map((root) => byRoot.get(root)).filter((node): node is ProjectNode => Boolean(node));
5682 if (ordered.length !== projects.length) return;
5683 mockProjectTree.splice(0, mockProjectTree.length, ...globals, ...ordered);
5684 return;
5685 }
5686 const byKey = new Map<string, ProjectNode>();
5687 for (const node of projects) {
5688 if (node.root) byKey.set(node.root, node);
5689 }
5690 for (const node of globals) byKey.set(GLOBAL_PROJECT_ORDER_KEY, node);
5691 const seen = new Set<string>();
5692 const ordered: ProjectNode[] = [];
5693 for (const key of workspaceRoots) {
5694 if (seen.has(key)) return;
5695 const node = byKey.get(key);
5696 if (!node) return;
5697 seen.add(key);
5698 ordered.push(node);
5699 }
5700 if (ordered.length !== projects.length + globals.length) return;
5701 mockProjectTree.splice(0, mockProjectTree.length, ...ordered);
5702 },
5703 async CreateTopic(_scope: string, _workspaceRoot: string, title: string) {
5704 const now = Date.now();
5705 const id = "topic_" + now;
5706 const topicTitle = title.trim() || t("mock.newSession");
5707 const parent = _scope === "global"
5708 ? ensureMockGlobalFolder()
5709 : mockProjectTree.find((node) => node.root === _workspaceRoot);
5710 if (parent) {
5711 const global = parent.kind === "global_folder";
5712 parent.children = [{
5713 key: parent.kind === "global_folder" ? "global_topic_" + id : "topic_" + id,
5714 kind: global ? "global_topic" : "topic",
5715 label: topicTitle,
5716 root: parent.root,
5717 topicId: id,
5718 projectColor: parent.projectColor,
5719 createdAt: now,
5720 }, ...projectChildren(parent)];
5721 }
5722 return { id, title: topicTitle, createdAt: now };
5723 },
5724 async RenameTopic(topicID: string, title: string) {
5725 const topic = findMockTopic(topicID);
5726 const nextTitle = title.trim();
5727 if (!topic || !nextTitle) return;
5728 const activePrefix = topic.label?.startsWith("● ") ? "● " : "";
5729 topic.label = `${activePrefix}${nextTitle}`;
5730 mockTabs = mockTabs.map((tab) =>
5731 tab.topicId === topicID ? { ...tab, topicTitle: nextTitle } : tab,
5732 );
5733 },
5734 async AIRenameSession(topicID: string) { return mockAIRenameTarget(mockProjectTree, topicID); },
5735 async AIRenameSessionTarget(selector: SessionSelector): Promise<SessionMutationResult> {
5736 const node = mockSessionTitleTarget(mockProjectTree, selector);
5737 const title = node ? mockAIRenameTarget(mockProjectTree, sessionTitleTarget(node)) : "";
5738 return { targetKey: sessionTitleTarget(node ?? { key: "", kind: "topic", label: "" }), operationId: `mock-title-${Date.now()}`, committed: true, title, lifecycleGeneration: 1 };
5739 },
5740 async RenameSessionTarget(selector: SessionSelector, title: string): Promise<SessionMutationResult> {
5741 const node = mockSessionTitleTarget(mockProjectTree, selector);
5742 const nextTitle = title.trim();
5743 if (node && nextTitle) {
5744 const activePrefix = node.label?.startsWith("● ") ? "● " : "";
5745 node.label = `${activePrefix}${nextTitle}`;
5746 }
5747 return { targetKey: node ? sessionTitleTarget(node) : "", operationId: `mock-title-${Date.now()}`, committed: Boolean(node), title: nextTitle, lifecycleGeneration: 1 };
5748 },
5749 async DeleteTopic(topicID: string) {
5750 deleteMockTopic(topicID);
5751 },
5752 async TrashTopic(topicID: string) {
5753 deleteMockTopic(topicID);
5754 },
5755 async SetTopicPinned(topicID: string, pinned: boolean) {
5756 setMockTopicPinned(topicID, pinned);
5757 },
5758 ...makeMockProjectTreeOrganizationBindings(mockProjectTree),
5759 async SaveWindowState(_state) {
5760 // no-op in browser dev — no real window geometry to persist
5761 },
5762 async ContextPanel(_tabID: string) {
5763 const now = Date.now();
5764 const currency = "¥";
5765 const cost = (usd: number) => currency === "¥" ? Number((usd * 7.15).toFixed(4)) : usd;
5766 return {
5767 usedTokens: 42124,
5768 windowTokens: 128000,
5769 promptTokens: 22134,
5770 completionTokens: 12345,
5771 totalTokens: 34479,
5772 reasoningTokens: 7521,
5773 cacheHitTokens: 87000,
5774 cacheMissTokens: 13000,
5775 sessionCacheHitTokens: 87000,
5776 sessionCacheMissTokens: 13000,
5777 sessionCompletionTokens: 12345,
5778 requestCount: 10,
5779 elapsedMs: 33 * 60 * 1000,
5780 sessionCost: cost(0.018),
5781 sessionCurrency: currency,
5782 sessionCostUsd: cost(0.018),
5783 sessionCostQuote: {
5784 original: { amount: String(cost(0.018)), currency: "CNY" },
5785 selected: { amount: String(cost(0.018)), currency: "CNY" },
5786 estimated: true,
5787 costComplete: true,
5788 displayComplete: true,
5789 complete: true,
5790 displayStatus: "matched",
5791 aggregateMode: "single_currency",
5792 rateBand: "mixed",
5793 },
5794 sources: {
5795 executor: {
5796 promptTokens: 24100,
5797 completionTokens: 8300,
5798 totalTokens: 32400,
5799 reasoningTokens: 5200,
5800 cacheHitTokens: 76000,
5801 cacheMissTokens: 9000,
5802 requestCount: 4,
5803 sessionCost: cost(0.0124),
5804 sessionCurrency: currency,
5805 sessionCostUsd: cost(0.0124),
5806 },
5807 planner: {
5808 promptTokens: 1800,
5809 completionTokens: 600,
5810 totalTokens: 2400,
5811 reasoningTokens: 420,
5812 cacheHitTokens: 3400,
5813 cacheMissTokens: 700,
5814 requestCount: 1,
5815 sessionCost: cost(0.0011),
5816 sessionCurrency: currency,
5817 sessionCostUsd: cost(0.0011),
5818 },
5819 subagent: {
5820 promptTokens: 4200,
5821 completionTokens: 2100,
5822 totalTokens: 6300,
5823 reasoningTokens: 1500,
5824 cacheHitTokens: 6100,
5825 cacheMissTokens: 2100,
5826 requestCount: 2,
5827 sessionCost: cost(0.0032),
5828 sessionCurrency: currency,
5829 sessionCostUsd: cost(0.0032),
5830 },
5831 compaction: {
5832 promptTokens: 2600,
5833 completionTokens: 700,
5834 totalTokens: 3300,
5835 reasoningTokens: 260,
5836 cacheHitTokens: 1100,
5837 cacheMissTokens: 900,
5838 requestCount: 1,
5839 sessionCost: cost(0.0009),
5840 sessionCurrency: currency,
5841 sessionCostUsd: cost(0.0009),
5842 },
5843 classifier: {
5844 promptTokens: 900,
5845 completionTokens: 120,
5846 totalTokens: 1020,
5847 reasoningTokens: 70,
5848 cacheHitTokens: 300,
5849 cacheMissTokens: 250,
5850 requestCount: 1,
5851 sessionCost: cost(0.0003),
5852 sessionCurrency: currency,
5853 sessionCostUsd: cost(0.0003),
5854 },
5855 title: {
5856 promptTokens: 420,
5857 completionTokens: 80,
5858 totalTokens: 500,
5859 reasoningTokens: 20,
5860 cacheHitTokens: 100,
5861 cacheMissTokens: 50,
5862 requestCount: 1,
5863 sessionCost: cost(0.0001),
5864 sessionCurrency: currency,
5865 sessionCostUsd: cost(0.0001),
5866 },
5867 },
5868 mock: true,
5869 readFiles: [
5870 { path: "README.md", turn: 2, time: now - 34 * 60 * 1000 },
5871 { path: "go.mod", turn: 3, time: now - 30 * 60 * 1000 },
5872 { path: "desktop/file.go", turn: 5, time: now - 13 * 60 * 1000, offset: 0, limit: 180 },
5873 { path: "internal/event.go", turn: 6, time: now - 4 * 60 * 1000, offset: 120, limit: 80, truncated: true },
5874 ],
5875 changedFiles: [
5876 { path: t("mock.changedFile1Path"), sources: ["session"], gitStatus: "modified", turns: [5, 6], latestPrompt: t("mock.changedFile1Prompt"), latestTime: now - 2 * 60 * 1000 },
5877 { path: t("mock.changedFile2Path"), sources: ["session"], gitStatus: "added", turns: [6], latestPrompt: t("mock.changedFile2Prompt"), latestTime: now - 60 * 1000 },
5878 ],
5879 };
5880 },
5881 // ── Remote (SSH) mock ──
5882 async RemoteHosts() {
5883 return mockRemoteHosts.slice();
5884 },
5885 async AddRemoteHost(input) {
5886 const view = mockRemoteHostView(input.label, input);
5887 mockRemoteHosts = [...mockRemoteHosts.filter((h) => h.id !== view.id), view];
5888 return view;
5889 },
5890 async UpdateRemoteHost(id, input) {
5891 const previous = mockRemoteHosts.find((h) => h.id === id);
5892 const view = mockRemoteHostView(id, input, previous);
5893 mockRemoteHosts = mockRemoteHosts.map((h) => (h.id === id ? view : h));
5894 return view;
5895 },
5896 async RemoveRemoteHost(id) {
5897 mockRemoteHosts = mockRemoteHosts.filter((h) => h.id !== id);
5898 delete mockRemoteConn[id];
5899 },
5900 async ScanSSHConfig() {
5901 return [
5902 { label: "gpu-box", host: "gpu-box", port: 0, user: "", identityFile: "", proxyJump: "", defaultWorkspace: "", serveInstall: "auto", credentialMode: "remote", useSSHConfig: true, preserveExistingSettings: true },
5903 ];
5904 },
5905 async ConnectRemoteHost(id) {
5906 mockRemoteConn[id] = "connecting";
5907 __emitMockRemote("status", { hostId: id, state: "connecting" });
5908 setTimeout(() => {
5909 mockRemoteConn[id] = "connected";
5910 __emitMockRemote("status", { hostId: id, state: "connected" });
5911 }, 300);
5912 },
5913 async DisconnectRemoteHost(id) {
5914 mockRemoteConn[id] = "stopped";
5915 __emitMockRemote("status", { hostId: id, state: "stopped" });
5916 },
5917 async RemoteConnectionStatuses() {
5918 return Object.entries(mockRemoteConn).map(([hostId, state]) => ({ hostId, state: state as RemoteConnectionStatus["state"] }));
5919 },
5920 async ConfirmRemoteHostKey(hostId, accept) {
5921 mockRemoteConn[hostId] = accept ? "connected" : "stopped";
5922 __emitMockRemote("status", { hostId, state: mockRemoteConn[hostId] });
5923 },
5924 async ConfirmRemoteSecret(hostId, _promptId, _secret, accept) {
5925 mockRemoteConn[hostId] = accept ? "connected" : "stopped";
5926 __emitMockRemote("status", { hostId, state: mockRemoteConn[hostId] });
5927 },
5928 async ListRemoteDir(_hostId, path) {
5929 const base = path.replace(/\/$/, "");
5930 return [
5931 { name: "src", path: `${base}/src`, isDir: true, size: 0, mtimeUnix: 1_700_000_000, symlink: false },
5932 { name: "README.md", path: `${base}/README.md`, isDir: false, size: 1024, mtimeUnix: 1_700_000_500, symlink: false },
5933 ];
5934 },
5935 async ReadRemoteFile(_hostId, path) {
5936 return { path, body: `# Mock remote file\n${path}\n`, size: 40, mtimeUnix: 1_700_000_500, truncated: false, binary: false };
5937 },
5938 async WriteRemoteFile(_hostId, _path, _body, _expectMtimeUnix) {
5939 return { ok: true, conflict: false, newMtimeUnix: 1_700_000_900 };
5940 },
5941 async MkdirRemote() {},
5942 async RenameRemotePath() {},
5943 async DeleteRemotePath() {},
5944 async RemoteForwards(hostId) {
5945 return mockRemoteForwards[hostId] ?? [];
5946 },
5947 async AddRemoteForward(hostId, input) {
5948 const view: RemoteForwardView = { id: `L:${input.localPort}`, hostId, ...input, state: "active" };
5949 mockRemoteForwards[hostId] = [...(mockRemoteForwards[hostId] ?? []), view];
5950 __emitMockRemote("forwards", { hostId, forwards: mockRemoteForwards[hostId] });
5951 return view;
5952 },
5953 async RemoveRemoteForward(hostId, forwardId) {
5954 mockRemoteForwards[hostId] = (mockRemoteForwards[hostId] ?? []).filter((f) => f.id !== forwardId);
5955 __emitMockRemote("forwards", { hostId, forwards: mockRemoteForwards[hostId] });
5956 },
5957 async OpenRemoteWorkspace() {},
5958 async PickRemoteIdentityFile() { return "~/.ssh/id_ed25519"; },
5959 async CheckRemotePlatform() {},
5960 async StopRemoteServer(hostId, workspace) {
5961 __emitMockRemote("server", { hostId, workspace, state: "stopped" });
5962 },
5963 async RemoteServerStatus(hostId, workspace) {
5964 return { hostId, workspace, state: "stopped" };
5965 },
5966 async RemoteServerLogs() {
5967 return "mock serve log line 1\nmock serve log line 2\n";
5968 },
5969 async RemoteLastWorkspace() {
5970 return "~/app";
5971 },
5972 async ScanRemoteLegacyWorkbenchData() {
5973 return { mirrorCount: 0, mirrorBytes: 0, trustFile: false };
5974 },
5975 async ExtensionActions() {
5976 return [];
5977 },
5978 async InvokeExtensionAction() {
5979 return "";
5980 },
5981 async SubmitExtensionForm() {},
5982 async SubmitExtensionFormExact() {}, async ResolveRemoteTabPromptExact() {}, async SubmitRemoteTabExtensionFormExact() {},
5983 ...remoteProjects.bindings,
5984 async CleanRemoteLegacyWorkbenchData() {},
5985 };
5986 }
5987 let mockRemoteHosts: RemoteHostView[] = [
5988 { id: "demo", label: "demo", host: "192.168.1.10", port: 22, user: "dev", identityFile: "", proxyJump: "", defaultWorkspace: "~/app", serveInstall: "auto", credentialMode: "remote", useSSHConfig: false },
5989 ];
5990 const mockRemoteConn: Record<string, RemoteConnectionStatus["state"]> = { demo: "connected" };
5991 const mockRemoteForwards: Record<string, RemoteForwardView[]> = {};
5992
5992 lines TYPESCRIPT