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