| 1 | import { makeMockSessionExportBindings, type SessionExportBindings } from "./sessionExportBridge"; |
| 2 | import type { AttachmentBindings } from "./attachmentBindings"; |
| 3 | import { makeMockSessionLifecycleBindings, type SessionLifecycleBindings } from "./sessionLifecycleBindings"; |
| 4 | import { makeMockModelSettingsBindings, type ModelSettingsBindings } from "./modelSettingsBridge"; |
| 5 | import { mockProviderTemplate, mockPreset, mockBundlePreset, mockKimiAPIModels, mockLongCatModels, mockTokenRhythmModels, mockTokenRhythmModelOverrides, mockMiMoV25Models, mockMiniMaxModels, mockGLMAPIModels, mockGLMCodingModels, mockGLMAnthropicModels, mockQwenAPIModels, mockQwenPlanModels, mockQwenPlanVisionModels, mockStepFunModels, mockOpenCodeGoModels, mockNovitaModels, mockGMIModels, mockVercelModels, mockOllamaCloudModels } from "./mockProviderTemplates"; |
| 6 | // The Electron host and the browser mock share this React-to-Go contract. |
| 7 | import type { |
| 8 | CancelReceipt, |
| 9 | ChatFileReferenceRequest, |
| 10 | ChatFileReferenceResult, |
| 11 | DesktopCommandName, |
| 12 | HistoryWindowPage, |
| 13 | HistoryWindowRequest, |
| 14 | LegacyEmptySessionCleanupStatus, |
| 15 | MarkdownSVGView, |
| 16 | MessageFieldPage, |
| 17 | MessageHistoryPage, |
| 18 | MessageLocation, |
| 19 | Ref as SessionContentRef, |
| 20 | SearchHistoryPage, |
| 21 | SessionArchitectureDiagnostics, |
| 22 | SessionCreationResult, |
| 23 | SessionDraftContextView, |
| 24 | SessionDraftSaveRequest, |
| 25 | SessionDraftSaveResult, |
| 26 | SessionDraftSubmissionRequest, |
| 27 | SessionDraftSubmissionView, |
| 28 | SessionDraftSummary, |
| 29 | SessionDraftView, |
| 30 | SessionHistoryContentChunk, |
| 31 | SessionMutationResult, |
| 32 | SessionRef, |
| 33 | SessionSelector, |
| 34 | ComposerTarget, |
| 35 | WorkspaceSessionPage, |
| 36 | WorkspaceSnapshot, |
| 37 | } from "../generated/desktopContract.generated"; |
| 38 | import type { ExactInteractionBindings } from "./exactInteractionBindings"; |
| 39 | import type { InvocationRequest } from "./invocationDisplay"; |
| 40 | import type { FollowupBindings } from "./pendingFollowup"; |
| 41 | import { addBreadcrumb } from "./breadcrumbs"; |
| 42 | import { maybeShare } from "./queryCoalesce"; |
| 43 | import { makeMockSessionCatalogBindings } from "./sessionCatalogBridge"; |
| 44 | import { makeMockHistoryCatalogBindings, type HistoryCatalogBindings } from "./historyCatalogBridge"; |
| 45 | import { makeMockTaskCatalogBindings, type TaskCatalogBindings } from "./taskCatalogBridge"; |
| 46 | import { makeMockBlankProjectBindings, type BlankProjectBindings } from "./blankProjectBridge"; |
| 47 | import { makeMockQualityFloorBindings, type QualityFloorBindings } from "./deliveryFloorBridge"; |
| 48 | import { t } from "./i18n"; |
| 49 | import { makeMockForkBindings, withMockHistoryIds } from "./mockForkWorktree"; |
| 50 | import { makeMockWorktreeMergeBindings } from "./worktreeMergeMock"; |
| 51 | import { providerIsConfigured, providerRequiresKey, removeProviderAccessesForMock } from "./providerModels"; |
| 52 | import { DEFAULT_STATUS_BAR_ITEMS } from "./statusBarItems"; |
| 53 | import { registerTrustedThemeBackgroundURLs } from "./themePack"; |
| 54 | import { modeHasAutoApproveTools, modeWithAutoApproveTools, modeWithPlan, normalizeCollaborationMode, normalizeMode, normalizeToolApprovalMode } from "./types"; |
| 55 | import { makeMockProjectTreeOrganizationBindings, subscribeMockProjectTreeChanged, notifyMockProjectTreeChanged } from "./mockProjectTreeOrganization"; |
| 56 | import { decisionSurfaceMockFromInput, isLongDecisionOptionsMockInput } from "./decisionSurfaceMock"; |
| 57 | import { mockWorkspaceFile } from "./mockWorkspaceFile"; |
| 58 | import { mockAIRenameTarget, mockSessionTitleTarget, type SessionTitleBindings } from "./mockSessionTitle"; |
| 59 | import { sessionTitleTarget } from "./sessionTitleOperation"; |
| 60 | import { mockHistoryContentField, mockHistorySlice, mockTopicHistory as topicHistoryFixture } from "./bridgeHistoryFixtures"; |
| 61 | import { createMockModelScopePreset, type MockProviderPresetTemplate } from "./mockModelScopePreset"; |
| 62 | import { createMockRemoteProjects } from "./mockRemoteProjects"; |
| 63 | import { createBrowserMockInteractionIdentity, mockSessionMeta, withMockSessionIdentity } from "./browserMockInteractionIdentity"; |
| 64 | import { mockRemoteHostView } from "./mockRemoteHosts"; |
| 65 | import type { RemoteProjectBindings } from "./remoteProjectBridge"; |
| 66 | import type { ForkTargetsBindings } from "./forkTargets"; |
| 67 | import type { ToolRecoveryBindings } from "./toolRecovery"; |
| 68 | import type { ScrollDiagnosticBindings } from "./scrollDiagnosticBridge"; |
| 69 | import type { TranscriptProtocolBindings } from "./transcriptProtocol"; |
| 70 | import { makeMockSessionReaderBindings, publishMockTranscriptEvent, type SessionReaderBindings } from "./sessionReaderBridge"; |
| 71 | import { makeMockMCPAppBindings, type MCPAppBindings } from "./mcpAppBridge"; |
| 72 | import { makeMockPinnedContextBindings, type PinnedContextBindings } from "./pinnedContextBridge"; |
| 73 | import { createDesktopPreferencesMock } from "./desktopPreferencesMock"; |
| 74 | import type { |
| 75 | TurnChanges, |
| 76 | TurnFileChange, |
| 77 | RemoteHostView, |
| 78 | RemoteHostInput, |
| 79 | RemoteConnectionStatus, |
| 80 | RemoteDirEntry, |
| 81 | RemoteFilePreview, |
| 82 | RemoteWriteResult, |
| 83 | RemoteForwardInput, |
| 84 | RemoteForwardView, |
| 85 | RemoteServerView, |
| 86 | RemoteForwardsEvent, |
| 87 | RemoteLegacyWorkbenchData, |
| 88 | BalanceInfo, |
| 89 | UsageStatsRange, |
| 90 | UsageStatsRequest, |
| 91 | BotConnectionDiagnostic, |
| 92 | BotInstallPollResult, |
| 93 | BotInstallStartResult, |
| 94 | BotRuntimeStatusView, |
| 95 | BotSettingsView, |
| 96 | CapabilitiesView, |
| 97 | CapabilityDiagnosticsReport, |
| 98 | CredentialDiagnosticReport, |
| 99 | RuntimeDoctorReport, |
| 100 | CheckpointMeta, |
| 101 | CommandInfo, |
| 102 | ControlResult, |
| 103 | ContextInfo, |
| 104 | ContextPanelInfo, |
| 105 | DirEntry, |
| 106 | DesktopStartupSettingsView, |
| 107 | DeliveryWorktreeAvailability, |
| 108 | DeliveryWorktreeOpenResult, |
| 109 | WorktreeMergeInspection, |
| 110 | WorktreeMergeRequest, |
| 111 | WorktreeMergeResult, |
| 112 | WorktreeCleanupRequest, |
| 113 | WorktreeCleanupResult, |
| 114 | CloseMergedWorktreeTabRequest, |
| 115 | CloseMergedWorktreeTabResult, |
| 116 | DroppedItem, |
| 117 | EffortInfo, |
| 118 | ExtensionActionView, |
| 119 | FilePreview, |
| 120 | ExternalOpenersView, |
| 121 | HistoryMessage, |
| 122 | HistoryPage, |
| 123 | HistorySearchPage, |
| 124 | HistoryContentChunk, |
| 125 | HistoryContentRef, |
| 126 | HistorySlice, |
| 127 | HistorySliceRequest, |
| 128 | TabMetaRefreshEvent, |
| 129 | TopicActivationEvent, |
| 130 | TopicActivationRequest, |
| 131 | TopicActivationTicket, |
| 132 | HookConfigView, |
| 133 | HooksSettingsView, |
| 134 | JobView, |
| 135 | ActiveWorkView, |
| 136 | BackgroundRuntimeView, |
| 137 | JobCancelBatchView, |
| 138 | WorkspaceConflictView, |
| 139 | MCPMarketplaceEntry, |
| 140 | MCPServerInput, |
| 141 | MCPInstallResult, |
| 142 | MCPMarketplaceView, |
| 143 | MCPToolView, |
| 144 | MemoryFact, |
| 145 | MemorySuggestion, |
| 146 | MemorySuggestionsView, |
| 147 | MemoryView, |
| 148 | Meta, |
| 149 | Mode, |
| 150 | ModelInfo, |
| 151 | NetworkView, |
| 152 | PluginInstallOptions, |
| 153 | PluginView, |
| 154 | PermissionSnapshot, |
| 155 | ProjectNode, |
| 156 | ProjectTreeOrganizationBindings, |
| 157 | RecoveryLineageView, |
| 158 | RecoveryCleanupRequest, |
| 159 | RecoveryCleanupResult, |
| 160 | SessionCatalogBindings, |
| 161 | PromptHistoryEntry, |
| 162 | PromptHistoryResult, |
| 163 | ProviderModelCapabilityView, |
| 164 | ProviderModelCatalogUpdate, |
| 165 | ProviderPresetView, |
| 166 | ProviderView, |
| 167 | QuestionAnswer, |
| 168 | ServerView, |
| 169 | SessionMeta, |
| 170 | SessionRecoveryFailedEvent, |
| 171 | SessionRecoveryEvent, |
| 172 | SettingsView, |
| 173 | ShellInstallResult, |
| 174 | SkillsSettingsView, |
| 175 | SkillRootView, |
| 176 | SkillSuggestion, |
| 177 | SkillView, |
| 178 | TaskEvent, |
| 179 | TaskSnapshot, |
| 180 | SlashArgsResult, |
| 181 | SubagentProfileInput, |
| 182 | TabMeta, |
| 183 | TerminalSessionView, |
| 184 | TerminalWorkspaceView, |
| 185 | TopicMeta, |
| 186 | ToolApprovalMode, |
| 187 | TurnEventReplayView, |
| 188 | UpdateInfo, |
| 189 | UpdateProgress, |
| 190 | WireEvent, |
| 191 | WorkspaceChangeDetailView, |
| 192 | WorkspaceChangesView, |
| 193 | WorkspaceRevisions, |
| 194 | GitCommitView, |
| 195 | GitCommitDetailView, |
| 196 | WorkspaceView, |
| 197 | SessionClearResult, |
| 198 | } from "./types"; |
| 199 | import { editMockGoalTab } from "./mockGoalLifecycle"; |
| 200 | import { browserPreviewShellSupport } from "./shellSupportPreview"; |
| 201 | import { desktopHost } from "./desktopHost"; |
| 202 | export * from "./remoteTabEvents"; |
| 203 | export const COMPACT_RATIO_MIN_PERCENT = 30, COMPACT_RATIO_MAX_PERCENT = 85; |
| 204 | |
| 205 | export interface DesktopShellStatusView { |
| 206 | trayState: "probing" | "ready" | "unavailable"; |
| 207 | backgroundCloseAvailable: boolean; |
| 208 | reason?: string; |
| 209 | } |
| 210 | import type { MarkdownImageView } from "./markdownImage"; |
| 211 | const GLOBAL_PROJECT_ORDER_KEY = "__global__"; |
| 212 | |
| 213 | function stripLegacyGoalBudgetFlags(arg: string): string { |
| 214 | const parts = arg.trim().split(/\s+/).filter(Boolean); |
| 215 | while (parts.length > 0) { |
| 216 | const flag = parts[0].toLowerCase(); |
| 217 | if (flag !== "--research" && flag !== "--auto-research" && flag !== "--deep" && flag !== "--simple" && flag !== "--no-research") break; |
| 218 | parts.shift(); |
| 219 | } |
| 220 | return parts.join(" "); |
| 221 | } |
| 222 | |
| 223 | // AppBindings is checked against the generated desktop host contract (see |
| 224 | // src/generated/desktopContract.generated.ts), so the compiler catches drift |
| 225 | // between the Go binding surface and the frontend mock. After adding or |
| 226 | // renaming a bound method on App, run `cd desktop && go run . -emit-contract |
| 227 | // frontend/src/generated`, then `pnpm typecheck` to verify the mock still |
| 228 | // satisfies the contract. |
| 229 | // |
| 230 | // Types for native-feel bindings, used only by AppBindings and the dev mock. |
| 231 | interface NativeConfirmRequest { |
| 232 | title: string; |
| 233 | message: string; |
| 234 | detail: string; |
| 235 | confirmLabel: string; |
| 236 | cancelLabel: string; |
| 237 | destructive: boolean; |
| 238 | } |
| 239 | interface DesktopWindowState { |
| 240 | width: number; |
| 241 | height: number; |
| 242 | x: number; |
| 243 | y: number; |
| 244 | maximised: boolean; |
| 245 | } |
| 246 | // AppBindings is the hand-written React-to-Go contract. _CheckGeneratedBindings |
| 247 | // catches generated methods missing here; update this interface and typecheck. |
| 248 | export interface AppBindings extends AttachmentBindings, SessionExportBindings, SessionLifecycleBindings, ForkTargetsBindings, ToolRecoveryBindings, ModelSettingsBindings, SessionCatalogBindings, ProjectTreeOrganizationBindings, HistoryCatalogBindings, TaskCatalogBindings, BlankProjectBindings, QualityFloorBindings, SessionTitleBindings, ScrollDiagnosticBindings, RemoteProjectBindings, MCPAppBindings, PinnedContextBindings, FollowupBindings, TranscriptProtocolBindings, SessionReaderBindings, ExactInteractionBindings { |
| 249 | GetLegacyEmptySessionCleanupStatus(): Promise<LegacyEmptySessionCleanupStatus>; |
| 250 | RetryLegacyEmptySessionCleanup(): Promise<LegacyEmptySessionCleanupStatus>; |
| 251 | OpenSessionDraft(workspaceId: string): Promise<SessionDraftView>; |
| 252 | OpenSessionDraftForTarget(scope: string, workspaceRoot: string): Promise<SessionDraftView>; |
| 253 | RestoreSessionDraft(): Promise<SessionDraftView | null>; |
| 254 | SaveSessionDraft(request: SessionDraftSaveRequest): Promise<SessionDraftSaveResult>; |
| 255 | ListSessionDraftSummaries(): Promise<SessionDraftSummary[]>; |
| 256 | DiscardSessionDraft(draftId: string, revision: number): Promise<void>; |
| 257 | DismissSessionDraft(draftId: string): Promise<void>; |
| 258 | SetSessionDraftRestoreTarget(draftId: string): Promise<void>; |
| 259 | GetSessionDraft(draftId: string): Promise<SessionDraftView>; |
| 260 | GetSessionDraftState(draftId: string): Promise<import("../generated/desktopContract.generated").SessionDraftState>; |
| 261 | ResumeDraftSubmission(operationId: string, revision: number): Promise<SessionDraftSubmissionView>; |
| 262 | GetDraftContext(draftId: string): Promise<SessionDraftContextView>; |
| 263 | BeginDraftSubmission(request: SessionDraftSubmissionRequest): Promise<SessionDraftSubmissionView>; |
| 264 | GetDraftSubmission(operationId: string): Promise<SessionDraftSubmissionView>; |
| 265 | CancelDraftSubmission(operationId: string): Promise<SessionDraftSubmissionView>; |
| 266 | SavePastedImageForComposerTarget(target: ComposerTarget, dataUrl: string): Promise<string>; |
| 267 | SavePastedFileForComposerTarget(target: ComposerTarget, name: string, dataUrl: string): Promise<string>; |
| 268 | SaveClipboardImageForComposerTarget(target: ComposerTarget): Promise<string>; |
| 269 | AttachDroppedForComposerTarget(target: ComposerTarget, path: string): Promise<DroppedItem>; |
| 270 | ListDirForTarget(target: ComposerTarget, rel: string): Promise<DirEntry[]>; |
| 271 | SearchFileRefsForTarget(target: ComposerTarget, query: string): Promise<DirEntry[]>; |
| 272 | AttachmentDataURLForComposerTarget(target: ComposerTarget, path: string): Promise<string>; |
| 273 | GetSessionActivityBaseline(selector: SessionSelector): Promise<import("../generated/desktopContract.generated").SessionActivityBaseline>; |
| 274 | GetWorkspaceSnapshot(): Promise<WorkspaceSnapshot>; |
| 275 | CreateSession(workspaceId: string): Promise<SessionRef>; |
| 276 | ForkSession(ref: SessionRef, turnBoundary: string): Promise<SessionRef>; |
| 277 | ForkSessionTarget(selector: SessionSelector, turnBoundary: string): Promise<SessionRef>; |
| 278 | CopySessionTarget(selector: SessionSelector, operationId: string): Promise<SessionCreationResult>; |
| 279 | HistorySliceForTarget(selector: SessionSelector, request: HistorySliceRequest): Promise<HistorySlice>; |
| 280 | HistoryContentForTarget(selector: SessionSelector, ref: HistoryContentRef, chunkIndex: number): Promise<HistoryContentChunk>; |
| 281 | SearchHistoryContentForTarget(selector: SessionSelector, query: string, cursor: string, limit: number): Promise<HistorySearchPage>; |
| 282 | SessionHistoryPageForTarget(selector: SessionSelector, cursor: string, limit: number): Promise<MessageHistoryPage>; |
| 283 | SessionHistoryContentForTarget(selector: SessionSelector, ref: SessionContentRef, offset: number): Promise<SessionHistoryContentChunk>; |
| 284 | LocateSessionMessageForTarget(selector: SessionSelector, messageId: string, snapshot: number): Promise<MessageLocation>; |
| 285 | SessionMessageFieldForTarget(selector: SessionSelector, messageId: string, version: number, field: string, offset: number, length: number): Promise<MessageFieldPage>; |
| 286 | SearchSessionHistoryForTarget(selector: SessionSelector, query: string, cursor: string, limit: number): Promise<SearchHistoryPage>; |
| 287 | SessionHistoryWindowForTarget(selector: SessionSelector, request: HistoryWindowRequest): Promise<HistoryWindowPage>; |
| 288 | ListWorkspaceSessions(workspaceId: string, query: string, cursor: string, limit: number, includeArchived: boolean): Promise<WorkspaceSessionPage>; |
| 289 | OpenSession(ref: SessionRef): Promise<HistoryPage>; |
| 290 | ReadSessionHistory(ref: SessionRef, cursor: string, limit: number): Promise<HistoryPage>; |
| 291 | RenameCanonicalSession(ref: SessionRef, title: string): Promise<void>; |
| 292 | SetSessionPinned(selector: SessionSelector, pinned: boolean): Promise<void>; |
| 293 | ArchiveCanonicalSession(ref: SessionRef): Promise<void>; |
| 294 | ArchiveSessionTarget(selector: SessionSelector): Promise<SessionMutationResult>; |
| 295 | RestoreCanonicalSession(ref: SessionRef): Promise<void>; |
| 296 | RestoreSessionTarget(selector: SessionSelector): Promise<SessionMutationResult>; |
| 297 | DeleteSessionTarget(selector: SessionSelector): Promise<SessionMutationResult>; |
| 298 | MoveWorkspaceSession(workspaceId: string, sessionId: string, beforeSessionId: string): Promise<void>; |
| 299 | MoveSessionTarget(selector: SessionSelector, workspaceId: string, beforeSessionId: string): Promise<SessionMutationResult>; |
| 300 | RenameWorkspace(workspaceId: string, title: string): Promise<void>; |
| 301 | SetWorkspaceVisible(workspaceId: string, visible: boolean): Promise<void>; |
| 302 | MoveWorkspace(workspaceId: string, beforeWorkspaceId: string): Promise<void>; |
| 303 | GetSessionArchitectureDiagnostics(): Promise<SessionArchitectureDiagnostics>; |
| 304 | Platform(): Promise<string>; |
| 305 | MinimiseMainWindow(): Promise<void>; |
| 306 | ToggleMaximiseMainWindow(): Promise<void>; |
| 307 | IsMainWindowMaximised(): Promise<boolean>; |
| 308 | CloseMainWindow(): Promise<void>; |
| 309 | // ── Heartbeat ── |
| 310 | HeartbeatListTasks(): Promise<unknown>; |
| 311 | HeartbeatReloadTasks(): Promise<unknown>; |
| 312 | HeartbeatSaveTasks(tasks: unknown): Promise<void>; |
| 313 | HeartbeatReloadConfig(): Promise<unknown>; |
| 314 | HeartbeatSaveConfig(update: unknown): Promise<unknown>; |
| 315 | HeartbeatTriggerNow(id: string): Promise<void>; |
| 316 | HeartbeatGenerateID(): Promise<string>; |
| 317 | Submit(input: string): Promise<void>; |
| 318 | SubmitToTab(tabID: string, input: string): Promise<void>; |
| 319 | SubmitToTabWithID(tabID: string, input: string, submissionID: string): Promise<void>; |
| 320 | StartTurnForTab?(tabID: string, input: string, submissionID: string): Promise<{ turnId: string; status: string; disposition?: "turn_started" | "management_handled"; runtimeEpoch?: string; submissionId?: string }>; |
| 321 | SubmitDisplay(display: string, input: string): Promise<void>; |
| 322 | SubmitDisplayToTab(tabID: string, display: string, input: string): Promise<void>; |
| 323 | SubmitDisplayToTabWithID(tabID: string, display: string, input: string, submissionID: string): Promise<void>; |
| 324 | SubmitDeliveryRecoveryToTab(tabID: string, display: string, input: string): Promise<void>; |
| 325 | SubmitDeliveryRecoveryToTabWithID(tabID: string, display: string, input: string, submissionID: string): Promise<void>; |
| 326 | SubmitInvocationsToTab(tabID: string, display: string, input: string, invocations: InvocationRequest[]): Promise<void>; |
| 327 | SubmitInvocationsToTabWithID(tabID: string, display: string, input: string, invocations: InvocationRequest[], submissionID: string): Promise<void>; |
| 328 | SubmitInitialGoalToTab( |
| 329 | tabID: string, |
| 330 | goal: string, |
| 331 | display: string, |
| 332 | input: string, |
| 333 | invocations: InvocationRequest[], |
| 334 | collaborationMode: string, |
| 335 | toolApprovalMode: string, |
| 336 | ): Promise<string[]>; |
| 337 | SubmitInitialGoalToTabWithID(tabID: string, goal: string, display: string, input: string, invocations: InvocationRequest[], collaborationMode: string, toolApprovalMode: string, submissionID: string): Promise<string[]>; |
| 338 | SubmitEditedDisplayToTab(tabID: string, display: string, input: string, original: string): Promise<void>; |
| 339 | SubmitEditedDisplayToTabWithID(tabID: string, display: string, input: string, original: string, submissionID: string): Promise<void>; |
| 340 | RunShellForTab(tabID: string, command: string): Promise<void>; |
| 341 | Steer(text: string): Promise<void>; |
| 342 | SteerForTab(tabID: string, text: string): Promise<void>; |
| 343 | InboxSnapshot(tabID: string): Promise<{ |
| 344 | revision: number; |
| 345 | paused: boolean; |
| 346 | recovered: boolean; |
| 347 | recoveredCount?: number; sessionPath?: string; |
| 348 | items: Array<{ |
| 349 | id: string; |
| 350 | intent: string; |
| 351 | state: string; |
| 352 | preview: string; |
| 353 | byteSize: number; |
| 354 | source?: string; |
| 355 | position: number; |
| 356 | blockReason?: string; |
| 357 | }>; |
| 358 | itemsCount: number; |
| 359 | bytes: number; |
| 360 | maxItems: number; |
| 361 | maxBytes: number; |
| 362 | }>; |
| 363 | EnqueueInboxSteer(tabID: string, display: string, submit: string, idempotency: string): Promise<{ itemId: string; disposition: string; position: number; paused: boolean; idempotent?: boolean; error?: string }>; |
| 364 | EnqueueInboxSteerForTurn?(tabID: string, turnID: string, display: string, submit: string, idempotency: string): Promise<{ itemId: string; disposition: string; position: number; paused: boolean; idempotent?: boolean; error?: string }>; |
| 365 | SteerInboxItem(tabID: string, itemID: string): Promise<{ |
| 366 | itemId: string; disposition: string; position: number; paused: boolean; idempotent?: boolean; error?: string; |
| 367 | }>; |
| 368 | SteerInboxItemForTurn?(tabID: string, turnID: string, itemID: string): Promise<{ |
| 369 | itemId: string; disposition: string; position: number; paused: boolean; idempotent?: boolean; error?: string; |
| 370 | }>; |
| 371 | ReadInboxItem(tabID: string, id: string): Promise<{ id: string; displayText: string; rawText: string; submitText: string }>; |
| 372 | UpdateInboxItem(tabID: string, id: string, display: string, submit: string): Promise<void>; |
| 373 | DeleteInboxItem(tabID: string, id: string): Promise<void>; |
| 374 | MoveInboxItem(tabID: string, id: string, toIndex: number): Promise<void>; |
| 375 | SetInboxPaused(tabID: string, paused: boolean): Promise<void>; |
| 376 | RetryInboxItem(tabID: string, id: string): Promise<void>; |
| 377 | RefreshInboxItem(tabID: string, id: string): Promise<void>; |
| 378 | InboxHasItems(tabID: string): Promise<boolean>; |
| 379 | Cancel(): Promise<void>; |
| 380 | CancelTab(tabID: string): Promise<void>; |
| 381 | CancelSessionForTab?(tabID: string): Promise<CancelReceipt>; |
| 382 | CancelTabWithInboxItems(tabID: string, itemIDs: string[]): Promise<void>; |
| 383 | CancelTabWithInboxItemsResult?(tabID: string, itemIDs: string[]): Promise<{ discardedItemIds: string[]; warning?: string }>; |
| 384 | InterruptTurnForTab?(tabID: string, turnID: string): Promise<void>; |
| 385 | InterruptTurnWithInboxItemsForTab?(tabID: string, turnID: string, itemIDs: string[]): Promise<{ discardedItemIds: string[]; warning?: string }>; |
| 386 | TurnEventsForTab?(tabID: string, afterSeq: number): Promise<TurnEventReplayView>; |
| 387 | Approve(id: string, allow: boolean, session: boolean, persist: boolean): Promise<void>; |
| 388 | ApproveTab(tabID: string, id: string, allow: boolean, session: boolean, persist: boolean): Promise<void>; |
| 389 | ApproveTabForTurn?(tabID: string, turnID: string, runtimeEpoch: string, id: string, allow: boolean, session: boolean, persist: boolean): Promise<void>; |
| 390 | ResolvePlanDecision(id: string, action: "start_execution" | "revise_plan" | "exit_plan"): Promise<void>; |
| 391 | ResolvePlanDecisionTab(tabID: string, id: string, action: "start_execution" | "revise_plan" | "exit_plan"): Promise<void>; |
| 392 | ResolvePlanDecisionTabForTurn?(tabID: string, turnID: string, runtimeEpoch: string, id: string, action: "start_execution" | "revise_plan" | "exit_plan"): Promise<void>; |
| 393 | ResolveRecovery(id: string, action: string, feedback: string): Promise<void>; |
| 394 | ResolveRecoveryTab(tabID: string, id: string, action: string, feedback: string): Promise<void>; |
| 395 | ResolveRecoveryTabForTurn?(tabID: string, turnID: string, runtimeEpoch: string, id: string, action: string, feedback: string): Promise<void>; |
| 396 | SetRecoveryCheckpointEnabled(enabled: boolean): Promise<void>; |
| 397 | SetRecoveryCheckpointEnabledTab(tabID: string, enabled: boolean): Promise<void>; |
| 398 | RecoveryCheckpointEnabled(): Promise<boolean>; |
| 399 | RecoveryCheckpointEnabledTab(tabID: string): Promise<boolean>; |
| 400 | AnswerQuestion(id: string, answers: QuestionAnswer[]): Promise<void>; |
| 401 | AnswerQuestionForTab(tabID: string, id: string, answers: QuestionAnswer[]): Promise<void>; |
| 402 | AnswerMCPInteractionForTab( |
| 403 | tabID: string, |
| 404 | id: string, |
| 405 | action: "accept" | "decline" | "cancel", |
| 406 | content: Record<string, unknown> | null, |
| 407 | ): Promise<void>; |
| 408 | AnswerMCPInteractionForTurn?(tabID: string, turnID: string, runtimeEpoch: string, id: string, action: "accept" | "decline" | "cancel", content: Record<string, unknown> | null): Promise<void>; |
| 409 | AnswerPromptForTab?(tabID: string, turnID: string, id: string, answers: QuestionAnswer[]): Promise<void>; |
| 410 | ResolvePromptForTab?(tabID: string, promptID: string, turnID: string, runtimeEpoch: string, kind: string, answer: { |
| 411 | questions?: QuestionAnswer[]; allow?: boolean; session?: boolean; persist?: boolean; |
| 412 | action?: string; feedback?: string; content?: Record<string, unknown> | null; |
| 413 | generation?: number; permissionRevision?: number; |
| 414 | }): Promise<void>; |
| 415 | PendingPromptIdentitiesForTab?(tabID: string): Promise<Array<{ promptId: string; turnId: string; runtimeEpoch?: string; kind: string }>>; |
| 416 | ReplayPendingPromptIdentitiesForTab?(tabID: string): Promise<Array<{ promptId: string; turnId: string; runtimeEpoch?: string; kind: string }>>; |
| 417 | ReplayPendingPrompts(): Promise<void>; |
| 418 | ReplayPendingPromptsForTab(tabID: string): Promise<void>; |
| 419 | SetPlanMode(on: boolean): Promise<void>; |
| 420 | SetMode(mode: string): Promise<void>; |
| 421 | // Returns auto-allowed prompt ids; unlisted prompts remain pending (#6432). |
| 422 | SetModeForTab(tabID: string, mode: string): Promise<string[] | void>; |
| 423 | SetAutoApproveTools(on: boolean): Promise<void>; |
| 424 | SetCollaborationModeForTab(tabID: string, mode: string): Promise<void>; |
| 425 | SetToolApprovalMode(mode: string): Promise<void>; |
| 426 | // Same drained-prompt-id contract as SetModeForTab. |
| 427 | SetToolApprovalModeForTab(tabID: string, mode: string): Promise<string[] | void>; |
| 428 | PermissionSnapshotForTab(tabID: string): Promise<PermissionSnapshot>; |
| 429 | SetPermissionPresetForTab(tabID: string, preset: string, expectedRevision: number): Promise<PermissionSnapshot>; |
| 430 | RevokePermissionGrantForTab(tabID: string, scope: string, target: string, expectedRevision: number): Promise<PermissionSnapshot>; |
| 431 | // Atomically applies the controller-facing composer profile and reports any |
| 432 | // approval prompts drained by the resulting tool-approval posture. |
| 433 | SetComposerProfileForTab(tabID: string, collaborationMode: string, toolApprovalMode: string, goal: string): Promise<string[] | void>; |
| 434 | SetGoal(goal: string): Promise<void>; |
| 435 | SetGoalForTab(tabID: string, goal: string): Promise<void>; |
| 436 | EditGoalForTab(tabID: string, objective: string, maxGoalRounds: number | null): Promise<void>; |
| 437 | ResumeGoalForTab(tabID: string): Promise<boolean>; |
| 438 | PauseGoalForTab(tabID: string): Promise<boolean>; |
| 439 | ClearGoalForTab(tabID: string): Promise<void>; |
| 440 | Compact(): Promise<void>; |
| 441 | CompactForTab(tabID: string): Promise<void>; |
| 442 | NewSession(): Promise<void>; |
| 443 | NewSessionForTab(tabID: string): Promise<void>; |
| 444 | ClearSession(): Promise<SessionClearResult>; |
| 445 | ClearSessionForTab(tabID: string): Promise<SessionClearResult>; |
| 446 | History(): Promise<HistoryMessage[]>; |
| 447 | HistoryForTab(tabID: string): Promise<HistoryMessage[]>; |
| 448 | HistoryPage(beforeTurn: number, limit: number): Promise<HistoryPage>; |
| 449 | HistoryPageForTab(tabID: string, beforeTurn: number, limit: number): Promise<HistoryPage>; |
| 450 | // Windowed history paging (supersedes HistoryPageForTab for tab history). |
| 451 | HistorySliceForTab(tabID: string, req: HistorySliceRequest): Promise<HistorySlice>; |
| 452 | HistoryContentForTab(tabID: string, ref: HistoryContentRef, chunkIndex: number): Promise<HistoryContentChunk>; |
| 453 | HistoryCheckpointTurnsForTab(tabID: string): Promise<number[]>; |
| 454 | Checkpoints(): Promise<CheckpointMeta[]>; |
| 455 | CheckpointsForTab(tabID: string): Promise<CheckpointMeta[]>; |
| 456 | Rewind(turn: number, scope: string): Promise<void>; |
| 457 | RewindForTab(tabID: string, turn: number, scope: string): Promise<void>; |
| 458 | PreviewRewindForTab(tabID: string, turn: number, scope: string): Promise<import("./types").RewindPlanView>; |
| 459 | CommitRewindForTab(tabID: string, planID: string, turn: number, scope: string): Promise<import("./types").RewindResultView>; |
| 460 | UndoRewindForTab(tabID: string, transactionID: string): Promise<import("./types").RewindResultView>; |
| 461 | PreviewWorkspaceFileRevertForTab(tabID: string, path: string): Promise<import("./types").RewindPlanView>; |
| 462 | CommitWorkspaceFileRevertForTab(tabID: string, planID: string, resolution: string): Promise<import("./types").RewindResultView>; |
| 463 | Fork(turn: number): Promise<TabMeta>; |
| 464 | ForkForTab(tabID: string, turn: number): Promise<TabMeta>; |
| 465 | ForkWorktreeForTab(tabID: string, turn: number): Promise<import("./forkWorktree").ForkWorktreeResultView>; |
| 466 | SummarizeFrom(turn: number): Promise<void>; |
| 467 | SummarizeFromForTab(tabID: string, turn: number): Promise<void>; |
| 468 | SummarizeUpTo(turn: number): Promise<void>; |
| 469 | SummarizeUpToForTab(tabID: string, turn: number): Promise<void>; |
| 470 | ListSessions(): Promise<SessionMeta[]>; |
| 471 | ListSessionsForTab(tabID: string): Promise<SessionMeta[]>; |
| 472 | ListTrashedSessions(): Promise<SessionMeta[]>; |
| 473 | ResumeSession(path: string): Promise<HistoryMessage[]>; |
| 474 | ResumeSessionForTab(tabID: string, path: string): Promise<HistoryMessage[]>; |
| 475 | ResumeSessionPage(path: string, limit: number): Promise<HistoryPage>; |
| 476 | ResumeSessionPageForTab(tabID: string, path: string, limit: number): Promise<HistoryPage>; |
| 477 | OpenChannelSessionForTab(tabID: string, path: string): Promise<HistoryMessage[]>; |
| 478 | OpenChannelSessionPageForTab(tabID: string, path: string, limit: number): Promise<HistoryPage>; |
| 479 | PreviewSession(path: string): Promise<HistoryMessage[]>; |
| 480 | QuerySessionTakeover(tabId: string): Promise<import("./types").SessionTakeoverView | null>; |
| 481 | TakeoverSession(tabId: string, mode: "wait" | "interrupt"): Promise<void>; |
| 482 | DeleteSession(path: string): Promise<void>; |
| 483 | DeleteRecoveryCopy(path: string): Promise<void>; |
| 484 | GetRecoveryLineage(key: { scope: string; workspaceRoot?: string; topicId: string; path?: string; recordClassification?: boolean }): Promise<RecoveryLineageView>; |
| 485 | GetSessionVersionState(key: { scope: string; workspaceRoot?: string; topicId: string; path?: string; recordClassification?: boolean }): Promise<import("./types").SessionVersionStateView>; |
| 486 | SetActiveSessionVersion(request: import("./types").RecoveryPreferenceRequest): Promise<void>; |
| 487 | RetrySessionRecovery(request: import("./types").RecoveryPreferenceRequest): Promise<void>; |
| 488 | ReconcileRecoveryVersions(key: { scope: string; workspaceRoot?: string; topicId: string; path?: string }): Promise<void>; |
| 489 | ChooseRecoveryBranch(request: import("./types").RecoveryPreferenceRequest): Promise<void>; |
| 490 | CleanRecoveryLineage(request: RecoveryCleanupRequest): Promise<RecoveryCleanupResult>; |
| 491 | RestoreSession(path: string): Promise<void>; |
| 492 | PurgeTrashedSession(path: string): Promise<void>; |
| 493 | PurgeRecoveryCopy(path: string): Promise<void>; |
| 494 | RenameSession(path: string, title: string): Promise<void>; |
| 495 | RenameSessionHead(path: string, headId: string, name: string): Promise<void>; |
| 496 | ScanPromptHistory(nonce: string): Promise<PromptHistoryResult>; |
| 497 | ListWorkspaces(): Promise<WorkspaceView[]>; |
| 498 | PickWorkspace(): Promise<string>; |
| 499 | SwitchWorkspace(path: string): Promise<string>; |
| 500 | RemoveWorkspace(path: string): Promise<void>; |
| 501 | ContextUsageForTab(tabID: string): Promise<ContextInfo>; |
| 502 | Balance(): Promise<BalanceInfo>; |
| 503 | BalanceForTab(tabID: string): Promise<BalanceInfo>; |
| 504 | UsageStats(req: UsageStatsRequest): Promise<UsageStatsRange>; |
| 505 | Jobs(): Promise<JobView[]>; |
| 506 | ListTasks(): Promise<TaskSnapshot[]>; |
| 507 | CurrentTaskSessionID(): Promise<string>; |
| 508 | ListTasksForSession(sessionID: string): Promise<TaskSnapshot[]>; |
| 509 | GetTask(taskID: string): Promise<TaskSnapshot | null>; |
| 510 | StopTask(taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>; |
| 511 | CancelTask(taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>; |
| 512 | ListTasksForTab(tabID: string): Promise<TaskSnapshot[]>; |
| 513 | ListTaskEventsForTab(tabID: string, taskID: string, afterSequence: number): Promise<TaskEvent[]>; |
| 514 | StopTaskForTab(tabID: string, taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>; |
| 515 | CancelTaskForTab(tabID: string, taskID: string, expectedVersion: number, reason: string, idemKey: string): Promise<ControlResult>; |
| 516 | RequeueTaskForTab(tabID: string, taskID: string, expectedVersion: number, idemKey: string): Promise<ControlResult>; |
| 517 | OpenTaskSessionForTab(tabID: string, taskID: string): Promise<ControlResult>; |
| 518 | JobsForTab(tabID: string): Promise<JobView[]>; |
| 519 | CancelJob(jobID: string): Promise<boolean>; |
| 520 | CancelJobForTab(tabID: string, jobID: string): Promise<boolean>; |
| 521 | CancelJobsForTab(tabID: string, jobIDs: string[]): Promise<JobCancelBatchView>; |
| 522 | ActiveWorkForTab(tabID: string): Promise<ActiveWorkView>; |
| 523 | BackgroundRuntimes(): Promise<BackgroundRuntimeView[]>; |
| 524 | RevealBackgroundRuntime(tabID: string): Promise<TabMeta>; |
| 525 | WorkspaceConflictForTab(tabID: string): Promise<WorkspaceConflictView>; |
| 526 | RevealWorkspaceWriterForTab(tabID: string): Promise<TabMeta>; |
| 527 | CloseTabWithPolicy(tabID: string, policy: "keep_running" | "stop_and_close"): Promise<void>; |
| 528 | ToolResultForTab(tabID: string, toolID: string): Promise<{ name?: string; args: string; output: string; execution?: import("./types").WireShellExecution; mcpApp?: import("./types").MCPAppPresentation; presentedFiles?: import("./types").PresentedFile[] } | null>; |
| 529 | Meta(): Promise<Meta>; |
| 530 | MetaForTab(tabID: string): Promise<Meta>; |
| 531 | Commands(): Promise<CommandInfo[]>; |
| 532 | Capabilities(): Promise<CapabilitiesView>; |
| 533 | MCPServers(): Promise<ServerView[]>; |
| 534 | MCPCapabilityMatrix(): Promise<{ |
| 535 | views: Array<{ id: string; layer: string; state: string; negotiated: boolean; detail: string }>; |
| 536 | hostProfile: string; |
| 537 | }>; |
| 538 | MCPMarketplace(query: string): Promise<MCPMarketplaceView>; |
| 539 | MCPMarketplaceResolve(registryName: string): Promise<MCPMarketplaceEntry>; |
| 540 | SkillsSettings(): Promise<SkillsSettingsView>; |
| 541 | CapabilityDiagnostics(includeSessionRuntime: boolean): Promise<CapabilityDiagnosticsReport>; |
| 542 | CredentialDiagnostics(probe: boolean): Promise<CredentialDiagnosticReport>; |
| 543 | RepairCredentials(dryRun: boolean): Promise<CredentialDiagnosticReport>; |
| 544 | RetryAuthenticationForTab(tabId: string): Promise<NonNullable<TabMeta["authentication"]>>; |
| 545 | RuntimeDoctor(): Promise<RuntimeDoctorReport>; |
| 546 | Plugins(): Promise<PluginView[]>; |
| 547 | PlanPluginInstall(source: string, options: PluginInstallOptions): Promise<string>; |
| 548 | InstallPlugin(source: string, options: PluginInstallOptions): Promise<string>; |
| 549 | RemovePlugin(name: string): Promise<void>; |
| 550 | SetPluginEnabled(name: string, enabled: boolean): Promise<void>; |
| 551 | UpdatePlugin(name: string): Promise<string>; |
| 552 | PluginDoctor(name: string): Promise<PluginView>; |
| 553 | // Extension UI (stage 8b2): enumerate handshake-declared extension actions |
| 554 | // for the command palette, invoke one, and deliver a form surface's values |
| 555 | // (or {cancelled: true} on dismissal) back to the owning sidecar. |
| 556 | ExtensionActions(tabID: string): Promise<ExtensionActionView[]>; |
| 557 | InvokeExtensionAction(tabID: string, name: string, args: Record<string, string>): Promise<string>; |
| 558 | SubmitExtensionForm(tabID: string, pluginID: string, surfaceID: string, values: Record<string, unknown>): Promise<void>; |
| 559 | AddMCPServer(input: MCPServerInput): Promise<number>; |
| 560 | InstallMCPServer(input: MCPServerInput): Promise<MCPInstallResult>; |
| 561 | UpdateMCPServer(name: string, input: MCPServerInput): Promise<void>; |
| 562 | RemoveMCPServer(name: string): Promise<void>; |
| 563 | AuthorizeAndConnectMCPServer(name: string): Promise<void>; |
| 564 | AuthenticateMCPServer(name: string): Promise<void>; |
| 565 | ReconnectMCPServer(name: string): Promise<void>; |
| 566 | ClearMCPServerAuthentication(name: string): Promise<void>; |
| 567 | PickSkillFolder(): Promise<string>; |
| 568 | PickPluginFolder(): Promise<string>; |
| 569 | AddSkillPath(path: string): Promise<void>; |
| 570 | RemoveSkillPath(path: string): Promise<void>; |
| 571 | SetSkillPathEnabled(path: string, enabled: boolean): Promise<void>; |
| 572 | RefreshSkills(): Promise<void>; |
| 573 | ReloadCommands(): Promise<void>; |
| 574 | SetSkillEnabled(name: string, enabled: boolean): Promise<void>; |
| 575 | SetSkillImplicitInvocation(enabled: boolean): Promise<void>; |
| 576 | AvailableSubagentTools(): Promise<MCPToolView[]>; |
| 577 | CreateSubagentProfile(input: SubagentProfileInput): Promise<string>; |
| 578 | UpdateSubagentProfile(name: string, scope: string, input: SubagentProfileInput): Promise<void>; |
| 579 | DeleteSubagentProfile(name: string, scope: string): Promise<void>; |
| 580 | SetSubagentProfileModel(name: string, ref: string): Promise<void>; |
| 581 | SetSubagentProfileEffort(name: string, level: string): Promise<void>; |
| 582 | TrySubagentProfile(input: SubagentProfileInput, task: string): Promise<string>; |
| 583 | CancelTrySubagentProfile(): Promise<void>; |
| 584 | SetMCPServerEnabled(name: string, enabled: boolean): Promise<void>; |
| 585 | SetMCPServerTier(name: string, tier: string): Promise<void>; |
| 586 | SlashArgs(input: string): Promise<SlashArgsResult>; |
| 587 | ListDir(rel: string): Promise<DirEntry[]>; |
| 588 | ListDirForTab(tabID: string, rel: string): Promise<DirEntry[]>; |
| 589 | SearchFileRefs(query: string): Promise<DirEntry[]>; |
| 590 | SearchFileRefsForTab(tabID: string, query: string): Promise<DirEntry[]>; |
| 591 | ReadFile(rel: string): Promise<FilePreview>; |
| 592 | ReadFileForTab(tabID: string, rel: string): Promise<FilePreview>; |
| 593 | ReadPresentedFileForTab(tabID: string, toolCallID: string, path: string): Promise<FilePreview>; |
| 594 | ReadPresentedFileSourceForTab(tabID: string, toolCallID: string, path: string): Promise<FilePreview>; |
| 595 | ReadPresentedTextPageForTab(tabID: string, toolCallID: string, path: string, offset: number, expectedVersion: string): Promise<import("./types").PresentedTextPage>; |
| 596 | ResolveChatFileReferencesForTab(tabID: string, turnKey: string, candidates: ChatFileReferenceRequest[]): Promise<ChatFileReferenceResult>; |
| 597 | ReadReferenceFileForTab(tabID: string, path: string): Promise<FilePreview>; |
| 598 | ReadReferenceFileSourceForTab(tabID: string, path: string): Promise<FilePreview>; |
| 599 | SanitizeMarkdownSVG(content: string): Promise<MarkdownSVGView>; |
| 600 | CreateWorkspaceBrowserPreviewForTab(tabID: string, rel: string): Promise<string>; |
| 601 | CreatePresentedBrowserPreviewForTab(tabID: string, toolCallID: string, path: string): Promise<string>; |
| 602 | RevokeWorkspaceBrowserPreview(url: string): Promise<void>; |
| 603 | RevokeWorkspaceMediaPreview(url: string): Promise<void>; |
| 604 | ResolveMarkdownImageForTab(tabID: string, source: string): Promise<MarkdownImageView>; |
| 605 | WorkspaceRevisionForTab(tabID: string): Promise<{ revisions: WorkspaceRevisions; watchState: "active" | "degraded" | "unavailable" }>; |
| 606 | WorkspaceChanges(tabID: string): Promise<WorkspaceChangesView>; |
| 607 | WorkspaceChangeDetail(tabID: string, path: string): Promise<WorkspaceChangeDetailView>; |
| 608 | WorkspaceTurnChanges(tabID: string, sessionPath: string, turn: number, resultID: string): Promise<TurnChanges>; |
| 609 | WorkspaceTurnChangeDetail(tabID: string, sessionPath: string, turn: number, resultID: string, path: string): Promise<TurnFileChange | null>; |
| 610 | TurnCheckLog(tabID: string, sessionPath: string, toolID: string, resultID: string): Promise<{ output: string; truncated: boolean } | null>; |
| 611 | GitBranches(): Promise<string[]>; |
| 612 | GitCheckout(branch: string): Promise<void>; |
| 613 | GitCreateBranch(name: string): Promise<void>; |
| 614 | GitBranchesForTab(tabID: string, workspaceRoot: string): Promise<string[]>; |
| 615 | GitCheckoutForTab(tabID: string, workspaceRoot: string, branch: string): Promise<void>; |
| 616 | GitCreateBranchForTab(tabID: string, workspaceRoot: string, name: string): Promise<void>; |
| 617 | WorkspaceGitStatsForTab(tabID: string, workspaceRoot: string): Promise<WorkspaceChangesView>; |
| 618 | WorkspaceGitHistory(tabID: string, path: string): Promise<GitCommitView[]>; |
| 619 | WorkspaceGitCommitDetail(tabID: string, hash: string, path: string): Promise<GitCommitDetailView>; |
| 620 | OpenWorkspacePathForTab(tabID: string, rel: string): Promise<void>; |
| 621 | OpenPresentedPathForTab(tabID: string, toolCallID: string, path: string): Promise<void>; |
| 622 | OpenReferencePathForTab(tabID: string, path: string): Promise<void>; |
| 623 | ResolvePresentedPathForTab(tabID: string, toolCallID: string, path: string): Promise<string>; |
| 624 | ResolveReferencePathForTab(tabID: string, path: string): Promise<string>; |
| 625 | ResolveWorkspacePathForTab(tabID: string, rel: string): Promise<string>; |
| 626 | ExternalOpeners(): Promise<ExternalOpenersView>; ExternalOpenersForTab(tabID: string): Promise<ExternalOpenersView>; |
| 627 | SetPreferredExternalOpener(id: string): Promise<void>; |
| 628 | OpenWorkspaceInExternalOpenerForTab(tabID: string, id: string): Promise<void>; OpenLocalPathInExternalOpener(path: string, id: string): Promise<void>; SaveLocalPathAs(path: string): Promise<string>; |
| 629 | RevealWorkspacePathForTab(tabID: string, rel: string): Promise<void>; |
| 630 | RevealPresentedPathForTab(tabID: string, toolCallID: string, path: string): Promise<void>; |
| 631 | RevealReferencePathForTab(tabID: string, path: string): Promise<void>; |
| 632 | SaveWorkspacePathAsForTab(tabID: string, rel: string): Promise<string>; |
| 633 | SavePresentedPathAsForTab(tabID: string, toolCallID: string, path: string): Promise<string>; |
| 634 | SaveReferencePathAsForTab(tabID: string, path: string): Promise<string>; |
| 635 | SaveRemoteFileAs(hostID: string, remotePath: string): Promise<string>; |
| 636 | SaveRemotePresentedFileAs(tabID: string, hostID: string, toolCallID: string, remotePath: string): Promise<string>; |
| 637 | ResolveRemotePresentedPathForTab(tabID: string, hostID: string, toolCallID: string, remotePath: string): Promise<string>; |
| 638 | ResolveRemoteWorkspacePathForTab(tabID: string, hostID: string, toolCallID: string, remotePath: string): Promise<string>; |
| 639 | RevealPath(path: string): Promise<void>; |
| 640 | OpenLocalPath(path: string): Promise<void>; |
| 641 | SavePastedImage(dataUrl: string): Promise<string>; |
| 642 | SaveClipboardImage(): Promise<string>; |
| 643 | SavePastedFile(name: string, dataUrl: string): Promise<string>; |
| 644 | PickExportFile(defaultFilename: string, mimeType: string): Promise<string>; |
| 645 | ExportGoalDiagnostics(): Promise<string>; |
| 646 | SaveExportFile(path: string, payload: string, base64Encoded: boolean): Promise<void>; |
| 647 | SaveSessionMarkdownForTab(tabID: string, path: string, title: string): Promise<void>; |
| 648 | SaveExportImageFiles(path: string, payloads: string[]): Promise<void>; |
| 649 | AttachDropped(path: string): Promise<DroppedItem>; |
| 650 | AttachmentDataURL(path: string): Promise<string>; |
| 651 | Models(): Promise<ModelInfo[]>; |
| 652 | SetModel(name: string): Promise<void>; |
| 653 | ModelsForTab(tabID: string): Promise<ModelInfo[]>; |
| 654 | ModelsForDraft(draftID: string): Promise<ModelInfo[]>; |
| 655 | SetModelForTab(tabID: string, name: string): Promise<void>; |
| 656 | Effort(): Promise<EffortInfo>; |
| 657 | SetEffort(level: string): Promise<void>; |
| 658 | EffortForTab(tabID: string): Promise<EffortInfo>; |
| 659 | SetEffortForTab(tabID: string, level: string): Promise<void>; |
| 660 | // ReloadRuntime rebuilds the tab's agent runtime in place (tools, skills, |
| 661 | // commands, hooks, providers, MCP servers) via boot.Rebuild, keeping the |
| 662 | // session. Busy tabs queue one reload for when they go idle. |
| 663 | ReloadRuntime(tabID: string): Promise<void>; |
| 664 | Memory(): Promise<MemoryView>; |
| 665 | MemorySuggestions(): Promise<MemorySuggestionsView>; |
| 666 | AcceptMemorySuggestion(suggestion: MemorySuggestion): Promise<string>; |
| 667 | AcceptSkillSuggestion(suggestion: SkillSuggestion): Promise<string>; |
| 668 | MemoryForTab(tabID: string): Promise<MemoryView>; |
| 669 | MemoryRevisions(ref: string): Promise<MemoryFact[]>; |
| 670 | MemoryRevisionsForTab(tabID: string, ref: string): Promise<MemoryFact[]>; |
| 671 | RestoreMemoryRevision(ref: string, revision: number): Promise<MemoryFact>; |
| 672 | RestoreMemoryRevisionForTab(tabID: string, ref: string, revision: number): Promise<MemoryFact>; |
| 673 | MemorySuggestionsForTab(tabID: string): Promise<MemorySuggestionsView>; |
| 674 | AcceptMemorySuggestionForTab(tabID: string, suggestion: MemorySuggestion): Promise<string>; |
| 675 | AcceptSkillSuggestionForTab(tabID: string, suggestion: SkillSuggestion): Promise<string>; |
| 676 | Remember(scope: string, note: string): Promise<string>; |
| 677 | RememberForTab(tabID: string, scope: string, note: string): Promise<string>; |
| 678 | Forget(name: string): Promise<void>; |
| 679 | ForgetForTab(tabID: string, name: string): Promise<void>; |
| 680 | RestoreArchivedMemory(archivePath: string): Promise<MemoryFact>; |
| 681 | RestoreArchivedMemoryForTab(tabID: string, archivePath: string): Promise<MemoryFact>; |
| 682 | SaveDoc(path: string, body: string): Promise<string>; |
| 683 | SaveDocForTab(tabID: string, path: string, body: string): Promise<string>; |
| 684 | DesktopStartupSettings(): Promise<DesktopStartupSettingsView>; |
| 685 | HooksSettings(scope: string): Promise<HooksSettingsView>; |
| 686 | SaveHooksSettings(scope: string, hooks: HookConfigView[]): Promise<void>; |
| 687 | SaveHooksSettingsForRoot(scope: string, projectRoot: string, hooks: HookConfigView[]): Promise<void>; |
| 688 | TrustProjectHooks(): Promise<void>; |
| 689 | TrustProjectHooksForRoot(projectRoot: string): Promise<void>; |
| 690 | SetDefaultModel(ref: string): Promise<void>; |
| 691 | SetPlannerModel(ref: string): Promise<void>; |
| 692 | SetVisionModel(ref: string): Promise<void>; |
| 693 | SetWebSearchModel(ref: string): Promise<void>; |
| 694 | SetSubagentModel(ref: string): Promise<void>; |
| 695 | SetSubagentEffort(level: string): Promise<void>; |
| 696 | SetMaxSubagentDepth(depth: number): Promise<void>; |
| 697 | SetMaxSubagentConcurrency(n: number): Promise<void>; |
| 698 | SetMaxParallelWriters(n: number): Promise<void>; |
| 699 | SetAutoPlan(mode: string): Promise<void>; |
| 700 | SetDefaultToolApprovalMode(mode: string): Promise<void>; |
| 701 | SetDefaultAutoRecoveryCheckpoint(enabled: boolean): Promise<void>; |
| 702 | RenameProviderConnections(names: string[], displayName: string): Promise<void>; |
| 703 | SaveProvider(p: ProviderView): Promise<void>; |
| 704 | SetProviderWebSearch(names: string[], enabled: boolean): Promise<void>; |
| 705 | SaveProviderModelCatalogs(updates: ProviderModelCatalogUpdate[]): Promise<string[]>; |
| 706 | SaveProviderWithKey(p: ProviderView, key: string): Promise<string>; |
| 707 | AddOfficialProviderAccess(kind: string, key: string): Promise<string>; |
| 708 | UpgradeDeepSeekProviderAccess(name: string): Promise<string>; |
| 709 | AddProviderPresetAccess(id: string, key: string): Promise<string>; |
| 710 | ResetProviderPresetAccess(id: string): Promise<void>; |
| 711 | FetchProviderModels(p: ProviderView): Promise<string[]>; |
| 712 | FetchProviderModelCatalog(p: ProviderView): Promise<ProviderModelCapabilityView[]>; |
| 713 | FetchProviderModelCatalogDraft(p: ProviderView, key: string): Promise<ProviderModelCapabilityView[]>; |
| 714 | TestProviderModel(p: ProviderView, model: string, key: string): Promise<void>; |
| 715 | FetchAllProviderModelCatalogs(providers: ProviderView[]): Promise<Record<string, ProviderModelCapabilityView[]>>; |
| 716 | FetchAllProviderModels(providers: ProviderView[]): Promise<Record<string, string[]>>; |
| 717 | DeleteProvider(name: string): Promise<void>; |
| 718 | RemoveProviderAccess(name: string): Promise<void>; |
| 719 | RemoveProviderAccesses(names: string[]): Promise<void>; |
| 720 | SaveProviderKey(apiKeyEnv: string, value: string): Promise<string>; |
| 721 | SetConnectionKey(name: string, value: string): Promise<string>; |
| 722 | AddProviderConnectionWithOptions(presetID: string, sourceName: string, key: string, baseURL: string, kind: string): Promise<string>; |
| 723 | AddProviderConnectionWithURL(presetID: string, sourceName: string, key: string, baseURL: string): Promise<string>; |
| 724 | AddProviderConnection(presetID: string, sourceName: string, key: string): Promise<string>; |
| 725 | SetProviderKey(apiKeyEnv: string, value: string): Promise<string>; |
| 726 | ClearProviderKey(apiKeyEnv: string): Promise<void>; |
| 727 | SetPermissionMode(mode: string): Promise<void>; |
| 728 | AddPermissionRule(list: string, rule: string): Promise<void>; |
| 729 | RemovePermissionRule(list: string, rule: string): Promise<void>; |
| 730 | ReloadSettings(): Promise<void>; |
| 731 | SetShellPreference(prefer: string): Promise<void>; |
| 732 | InstallShellSupport(id: string): Promise<ShellInstallResult>; |
| 733 | CancelShellInstall(): Promise<void>; |
| 734 | SetSandbox(bash: string, network: boolean, workspaceRoot: string, allowWrite: string[], shell: string): Promise<void>; |
| 735 | SetNetwork(n: NetworkView): Promise<void>; |
| 736 | SetBotSettings(b: BotSettingsView): Promise<void>; |
| 737 | SetBotConnectionToolApprovalMode(connID: string, mode: string): Promise<void>; |
| 738 | SetBotDingtalkToolApprovalMode(mode: string): Promise<void>; |
| 739 | SetBotSecret(envName: string, value: string): Promise<void>; |
| 740 | ClearBotSecret(envName: string): Promise<void>; |
| 741 | StartBotConnectionInstall(provider: string, domain: string): Promise<BotInstallStartResult>; |
| 742 | PollBotConnectionInstall(installID: string): Promise<BotInstallPollResult>; |
| 743 | BotRuntimeStatus(): Promise<BotRuntimeStatusView>; |
| 744 | DiagnoseBotConnection(id: string): Promise<BotConnectionDiagnostic>; |
| 745 | TestBotConnection(id: string, target?: string): Promise<BotConnectionDiagnostic>; |
| 746 | TestDingtalkBot(): Promise<BotConnectionDiagnostic>; |
| 747 | SetCloseBehavior(mode: string): Promise<void>; |
| 748 | SetSessionExperience(mode: "standard" | "deep"): Promise<void>; |
| 749 | SetDisplayMode(mode: string): Promise<void>; |
| 750 | SetStatusBarStyle(style: string): Promise<void>; |
| 751 | SetStatusBarItems(items: string[]): Promise<void>; SetReasoningDisplayMode(mode: "hidden" | "summary" | "auto" | "expanded"): Promise<void>; |
| 752 | SetDesktopLanguage(lang: string): Promise<void>; |
| 753 | SetDesktopCurrency(currency: string): Promise<void>; |
| 754 | SetDesktopAppearance(theme: string, style: string): Promise<void>; |
| 755 | SetDesktopTerminalTheme(theme: string): Promise<void>; |
| 756 | ListThemePacks(): Promise<import("./themePack").ThemePackView[]>; |
| 757 | GetActiveThemePack(): Promise<import("./themePack").ThemeActiveView>; |
| 758 | GetThemeExperience(): Promise<import("./themeExperience").ThemeExperienceView>; |
| 759 | ActivateThemePack(id: string): Promise<void>; |
| 760 | ActivateBaseStyle(style: string): Promise<void>; |
| 761 | DisableThemePack(): Promise<void>; |
| 762 | RestoreGraphiteAppearance(): Promise<void>; |
| 763 | ResetThemePack(): Promise<void>; |
| 764 | SaveThemePack(input: import("./themePack").ThemeSaveInput): Promise<import("./themePack").ThemePackView>; |
| 765 | DeleteThemePack(id: string): Promise<void>; |
| 766 | CopyThemePack(sourceID: string, newID: string, newName: string): Promise<import("./themePack").ThemePackView>; |
| 767 | ImportThemePack(sourcePath: string, replace: boolean): Promise<import("./themePack").ThemeImportResult>; |
| 768 | ExportThemePack(id: string, destPath: string): Promise<string>; |
| 769 | PickThemeBackground(): Promise<string>; |
| 770 | SetDesktopLayoutStyle(style: string): Promise<void>; |
| 771 | SetDesktopZoomFactor(factor: number): Promise<void>; |
| 772 | GetDesktopZoomFactor(): Promise<number>; |
| 773 | RestartApplication(): Promise<void>; |
| 774 | ReportDesktopWebViewReady(): Promise<void>; |
| 775 | GetDesktopShellStatus(): Promise<DesktopShellStatusView>; |
| 776 | SetDesktopCheckUpdates(enabled: boolean): Promise<void>; |
| 777 | SetDesktopUpdateChannel(channel: string): Promise<void>; |
| 778 | SetDesktopTelemetry(enabled: boolean): Promise<void>; |
| 779 | SetDesktopMetrics(enabled: boolean): Promise<void>; |
| 780 | SetExpandThinking(on: boolean): Promise<void>; |
| 781 | SetDesktopConversationWidth(width: string): Promise<void>; |
| 782 | MigrateDesktopPreferences(language: string, theme: string, style: string): Promise<void>; |
| 783 | SetAgentParams(temperature: number, maxSteps: number, plannerMaxSteps: number, systemPrompt: string): Promise<void>; |
| 784 | SetCompactRatio(ratio: number): Promise<void>; |
| 785 | SetReasoningLanguage(lang: string): Promise<void>; |
| 786 | SetTrayLocale(locale: "en" | "zh" | "zh-TW"): Promise<void>; |
| 787 | // SetBypass is the legacy desktop name for YOLO/full-access tool auto-approval |
| 788 | // (ask questions and plan approvals still wait; deny rules still apply). |
| 789 | // Runtime-only. |
| 790 | SetBypass(on: boolean): Promise<void>; |
| 791 | Version(): Promise<string>; |
| 792 | CheckUpdate(channel: string): Promise<UpdateInfo | null>; |
| 793 | /** v1.20+ single-action update: download, verify, install, relaunch. */ |
| 794 | ApplyUpdateRequest(channel: string, expectedVersion: string, requestId: string): Promise<void>; |
| 795 | /** Discard a stuck previous update transaction so the next install can proceed. */ |
| 796 | AbandonPendingUpdate?(): Promise<void>; |
| 797 | OpenDownloadPage(): Promise<void>; |
| 798 | OpenUserConfigPath?(): Promise<void>; |
| 799 | ReloadUserConfig?(): Promise<{ configWarnings?: string[]; configWarningsRevision?: number; configPath?: string } | null>; |
| 800 | StorageSettings(): Promise<{ defaultWorkspace: string; statePath: string; cachePath: string; extensionsPath: string }>; |
| 801 | NeedsOnboarding(): Promise<boolean>; |
| 802 | ConnectKey(apiKey: string): Promise<string>; |
| 803 | // Crash overlay "Send report" (desktop/crash_app.go): scrubs user paths, attaches |
| 804 | // version/os/arch, POSTs to the collection endpoint. Only ever sent on user click. |
| 805 | ReportCrash(kind: string, detail: string): Promise<void>; |
| 806 | RecordUIPerf(signals: Record<string, string>): Promise<void>; |
| 807 | ListTabs(): Promise<TabMeta[]>; |
| 808 | OpenProjectTab(workspaceRoot: string, topicID: string): Promise<TabMeta>; |
| 809 | IsolatedWorktreeAvailability(workspaceRoot: string): Promise<DeliveryWorktreeAvailability>; |
| 810 | CreateIsolatedWorktree(workspaceRoot: string): Promise<DeliveryWorktreeOpenResult>; |
| 811 | InspectWorktreeMerge(tabID: string): Promise<WorktreeMergeInspection>; |
| 812 | GetWorktreeStatus(tabID: string): Promise<WorktreeMergeInspection>; |
| 813 | PrepareWorktreeMerge(tabID: string): Promise<WorktreeMergeInspection>; |
| 814 | MergeWorktreeBack(request: WorktreeMergeRequest): Promise<WorktreeMergeResult>; |
| 815 | RegisterNavigationIntent(token: string): Promise<void>; |
| 816 | CloseMergedWorktreeTab(request: CloseMergedWorktreeTabRequest): Promise<CloseMergedWorktreeTabResult>; |
| 817 | FinalizeWorktreeMerge(request: WorktreeCleanupRequest): Promise<WorktreeCleanupResult>; |
| 818 | // Deprecated one-version aliases kept bound for older desktop clients. |
| 819 | DeliveryWorktreeAvailability(workspaceRoot: string): Promise<DeliveryWorktreeAvailability>; |
| 820 | CreateDeliveryWorktree(workspaceRoot: string): Promise<DeliveryWorktreeOpenResult>; |
| 821 | SetAgentPreset(preset: string): Promise<void>; |
| 822 | SetAgentPresetForTab(tabID: string, preset: string): Promise<void>; |
| 823 | SetTokenMode(mode: string): Promise<void>; |
| 824 | SetTokenModeForTab(tabID: string, mode: string): Promise<void>; |
| 825 | OpenGlobalTab(topicID: string): Promise<TabMeta>; |
| 826 | OpenTopicSession(scope: string, workspaceRoot: string, topicID: string, sessionPath: string): Promise<TabMeta>; |
| 827 | EnsureBlankTab(scope: string, workspaceRoot: string): Promise<TabMeta>; |
| 828 | ActivateTopic(scope: string, workspaceRoot: string, topicID: string, sessionPath: string): Promise<TabMeta>; |
| 829 | // Two-phase ticketed topic activation (supersedes ActivateTopic for topic |
| 830 | // navigation): returns a ticket after the surface switch; completion lands |
| 831 | // on the "topic:activation" channel. |
| 832 | StartTopicActivation(req: TopicActivationRequest): Promise<TopicActivationTicket>; |
| 833 | EnsureBlankSurface(scope: string, workspaceRoot: string): Promise<TabMeta>; |
| 834 | SetActiveTab(tabID: string): Promise<void>; |
| 835 | ReorderTabs(tabIDs: string[]): Promise<void>; |
| 836 | CloseTab(tabID: string): Promise<void>; |
| 837 | TerminalWorkspaceForTab(tabID: string): Promise<TerminalWorkspaceView>; |
| 838 | TerminalOutputForTab(tabID: string, sessionID: string): Promise<string>; |
| 839 | CreateTerminalForTab(tabID: string, relativePath: string, shellID: string): Promise<TerminalSessionView>; |
| 840 | WriteTerminalForTab(tabID: string, sessionID: string, data: string): Promise<void>; |
| 841 | ResizeTerminalForTab(tabID: string, sessionID: string, cols: number, rows: number): Promise<void>; |
| 842 | CloseTerminalForTab(tabID: string, sessionID: string): Promise<void>; |
| 843 | RenameTerminalForTab(tabID: string, sessionID: string, title: string): Promise<void>; |
| 844 | ListProjectTree(): Promise<ProjectNode[]>; |
| 845 | RenameProject(workspaceRoot: string, title: string): Promise<void>; |
| 846 | SetProjectColor(workspaceRoot: string, color: string): Promise<void>; |
| 847 | SetProjectPinned(workspaceRoot: string, pinned: boolean): Promise<void>; |
| 848 | ReorderProjects(workspaceRoots: string[]): Promise<void>; |
| 849 | CreateTopic(scope: string, workspaceRoot: string, title: string): Promise<TopicMeta>; |
| 850 | RenameTopic(topicID: string, title: string): Promise<void>; |
| 851 | DeleteTopic(topicID: string): Promise<void>; |
| 852 | TrashTopic(topicID: string): Promise<void>; |
| 853 | SetTopicPinned(topicID: string, pinned: boolean): Promise<void>; |
| 854 | ContextPanel(tabID: string): Promise<ContextPanelInfo>; |
| 855 | // New native-feel bindings (added with the desktop native-feel plan). |
| 856 | ConfirmAction(req: NativeConfirmRequest): Promise<boolean>; |
| 857 | SaveWindowState(state: DesktopWindowState): Promise<void>; |
| 858 | // ── Remote (SSH) ── |
| 859 | RemoteHosts(): Promise<RemoteHostView[]>; |
| 860 | AddRemoteHost(input: RemoteHostInput): Promise<RemoteHostView>; |
| 861 | UpdateRemoteHost(id: string, input: RemoteHostInput): Promise<RemoteHostView>; |
| 862 | RemoveRemoteHost(id: string): Promise<void>; |
| 863 | ScanSSHConfig(): Promise<RemoteHostInput[]>; |
| 864 | ConnectRemoteHost(id: string): Promise<void>; |
| 865 | DisconnectRemoteHost(id: string): Promise<void>; |
| 866 | RemoteConnectionStatuses(): Promise<RemoteConnectionStatus[]>; |
| 867 | ConfirmRemoteHostKey(hostId: string, accept: boolean): Promise<void>; |
| 868 | ConfirmRemoteSecret(hostId: string, promptId: string, secret: string, accept: boolean): Promise<void>; |
| 869 | ListRemoteDir(hostId: string, path: string): Promise<RemoteDirEntry[]>; |
| 870 | ReadRemoteFile(hostId: string, path: string): Promise<RemoteFilePreview>; |
| 871 | WriteRemoteFile(hostId: string, path: string, body: string, expectMtimeUnix: number): Promise<RemoteWriteResult>; |
| 872 | MkdirRemote(hostId: string, path: string): Promise<void>; |
| 873 | RenameRemotePath(hostId: string, oldPath: string, newPath: string): Promise<void>; |
| 874 | DeleteRemotePath(hostId: string, path: string, recursive: boolean): Promise<void>; |
| 875 | RemoteForwards(hostId: string): Promise<RemoteForwardView[]>; |
| 876 | AddRemoteForward(hostId: string, input: RemoteForwardInput): Promise<RemoteForwardView>; |
| 877 | RemoveRemoteForward(hostId: string, forwardId: string): Promise<void>; |
| 878 | OpenRemoteWorkspace(hostId: string, workspace: string): Promise<void>; |
| 879 | PickRemoteIdentityFile(): Promise<string>; |
| 880 | CheckRemotePlatform(hostId: string): Promise<void>; |
| 881 | StopRemoteServer(hostId: string, workspace: string): Promise<void>; |
| 882 | RemoteServerStatus(hostId: string, workspace: string): Promise<RemoteServerView>; |
| 883 | RemoteServerLogs(hostId: string, workspace: string, tailLines: number): Promise<string>; |
| 884 | RemoteLastWorkspace(hostId: string): Promise<string>; |
| 885 | ScanRemoteLegacyWorkbenchData(): Promise<RemoteLegacyWorkbenchData>; |
| 886 | CleanRemoteLegacyWorkbenchData(target: "mirrors" | "trust"): Promise<void>; |
| 887 | } |
| 888 | // Compile-time drift check. Exclude<A, B> extracts keys in A that are missing |
| 889 | // from B. If that set is non-empty, AssertNever<non-never> fails with |
| 890 | // "Type 'X' does not satisfy the constraint 'never'". |
| 891 | // _CheckGenToApp errors mean a generated Go method has no TS counterpart. |
| 892 | // This compares method *names* only: the generated contract uses positional |
| 893 | // arg0/arg1 parameters and its own DTO interfaces, so full signature |
| 894 | // assignability would false-positive. Parameter drift is caught at the call |
| 895 | // sites by tsc when components invoke app.<method>(...). |
| 896 | type AssertNever<T extends never> = T; |
| 897 | export type _CheckGenToApp = AssertNever<Exclude<DesktopCommandName, keyof AppBindings>>; |
| 898 | // Must match desktop/app.go's eventChannel constant. |
| 899 | const EVENT_CHANNEL = "agent:event"; |
| 900 | |
| 901 | function hostEvents(name: string, cb: (...args: unknown[]) => void): (() => void) | null { |
| 902 | const host = desktopHost(); |
| 903 | return host.kind === "none" ? null : host.events.on(name, cb); |
| 904 | } |
| 905 | type LazyMockAttachmentCommand = keyof AttachmentBindings |
| 906 | | "SavePastedImage" |
| 907 | | "SavePastedImageForComposerTarget" |
| 908 | | "SaveClipboardImage" |
| 909 | | "SaveClipboardImageForComposerTarget" |
| 910 | | "SavePastedFile" |
| 911 | | "SavePastedFileForComposerTarget" |
| 912 | | "AttachDropped" |
| 913 | | "AttachDroppedForComposerTarget" |
| 914 | | "AttachmentDataURL" |
| 915 | | "AttachmentDataURLForComposerTarget"; |
| 916 | type MockAppBindings = Omit<AppBindings, LazyMockAttachmentCommand>; |
| 917 | |
| 918 | let mockSingleton: AppBindings | null = null; |
| 919 | function getMock(): AppBindings { |
| 920 | if (!mockSingleton) mockSingleton = makeMockApp() as AppBindings; |
| 921 | return mockSingleton; |
| 922 | } |
| 923 | |
| 924 | // onEvent subscribes to the agent's typed event stream; returns an unsubscribe. |
| 925 | export function onEvent(cb: (e: WireEvent) => void): () => void { |
| 926 | return hostEvents(EVENT_CHANNEL, (payload) => cb(payload as WireEvent)) ?? mockSubscribe(cb); |
| 927 | } |
| 928 | |
| 929 | export interface TerminalOutputEvent { |
| 930 | id: string; |
| 931 | data: string; |
| 932 | } |
| 933 | |
| 934 | export interface TerminalExitEvent { |
| 935 | id: string; |
| 936 | exitCode: number; |
| 937 | removed?: boolean; |
| 938 | } |
| 939 | |
| 940 | function terminalEventPayload<T>(payload: unknown): T | null { |
| 941 | if (!payload || typeof payload !== "object") return null; |
| 942 | return payload as T; |
| 943 | } |
| 944 | |
| 945 | export function onTerminalOutput(cb: (event: TerminalOutputEvent) => void): () => void { |
| 946 | const off = hostEvents("terminal:output", (payload) => { |
| 947 | const event = terminalEventPayload<TerminalOutputEvent>(payload); |
| 948 | if (event?.id && typeof event.data === "string") cb(event); |
| 949 | }); |
| 950 | if (off) return off; |
| 951 | mockTerminalOutputListeners.add(cb); |
| 952 | return () => mockTerminalOutputListeners.delete(cb); |
| 953 | } |
| 954 | |
| 955 | export function onTerminalExit(cb: (event: TerminalExitEvent) => void): () => void { |
| 956 | const off = hostEvents("terminal:exit", (payload) => { |
| 957 | const event = terminalEventPayload<TerminalExitEvent>(payload); |
| 958 | if (event?.id && typeof event.exitCode === "number") cb(event); |
| 959 | }); |
| 960 | if (off) return off; |
| 961 | mockTerminalExitListeners.add(cb); |
| 962 | return () => mockTerminalExitListeners.delete(cb); |
| 963 | } |
| 964 | |
| 965 | const mockTerminalOutputListeners = new Set<(event: TerminalOutputEvent) => void>(); |
| 966 | const mockTerminalExitListeners = new Set<(event: TerminalExitEvent) => void>(); |
| 967 | |
| 968 | export function __emitMockTerminalOutput(event: TerminalOutputEvent): void { |
| 969 | mockTerminalOutputListeners.forEach((listener) => listener(event)); |
| 970 | } |
| 971 | |
| 972 | export function __emitMockTerminalExit(event: TerminalExitEvent): void { |
| 973 | mockTerminalExitListeners.forEach((listener) => listener(event)); |
| 974 | } |
| 975 | |
| 976 | // onUpdaterProgress subscribes to the auto-updater's progress events (a separate |
| 977 | // channel from the agent stream); returns an unsubscribe. Must match the event |
| 978 | // name emitted in desktop/updater_app.go. |
| 979 | export function onUpdaterProgress(cb: (p: UpdateProgress) => void): () => void { |
| 980 | const off = hostEvents("updater:progress", (p) => cb(p as UpdateProgress)); |
| 981 | if (off) return off; |
| 982 | updaterListeners.add(cb); |
| 983 | return () => { |
| 984 | updaterListeners.delete(cb); |
| 985 | }; |
| 986 | } |
| 987 | |
| 988 | // onFilesDropped subscribes to native OS file drops landing on the composer's |
| 989 | // drop target; the callback gets the dropped files' absolute paths. No-op in |
| 990 | // the browser dev mock. |
| 991 | export function onFilesDropped(cb: (paths: string[]) => void): () => void { |
| 992 | return desktopHost().native.onFilesDropped(cb); |
| 993 | } |
| 994 | |
| 995 | // onReady subscribes to the agent:ready event fired when boot.Build completes. |
| 996 | // The frontend re-fetches Meta/Context/History when this lands. |
| 997 | // onRuntimeRebuilt fires when a tab's controller is replaced in place |
| 998 | // (model/effort/token-mode switch, clear-while-running). The rebuilt |
| 999 | // controller restarts prompt ids, so per-tab id-keyed state must reset. |
| 1000 | export function onRuntimeRebuilt(cb: (tabId?: string, runtimeEpoch?: string) => void): () => void { |
| 1001 | return hostEvents("runtime:rebuilt", (tabId?: unknown, runtimeEpoch?: unknown) => |
| 1002 | cb( |
| 1003 | typeof tabId === "string" ? tabId : undefined, |
| 1004 | typeof runtimeEpoch === "string" ? runtimeEpoch : undefined, |
| 1005 | ) |
| 1006 | ) ?? (() => {}); |
| 1007 | } |
| 1008 | |
| 1009 | export function onReady(cb: (tabId?: string) => void): () => void { |
| 1010 | const off = hostEvents("agent:ready", (tabId?: unknown) => cb(typeof tabId === "string" ? tabId : undefined)); |
| 1011 | if (off) return off; |
| 1012 | // The browser mock has no native event bridge, but SessionRef navigation |
| 1013 | // still needs the same reload semantics as the desktop host. |
| 1014 | mockReadyListeners.add(cb); |
| 1015 | cb(); |
| 1016 | return () => mockReadyListeners.delete(cb); |
| 1017 | } |
| 1018 | |
| 1019 | export function onProjectTreeChanged(cb: () => void): () => void { |
| 1020 | return hostEvents("project-tree:changed", (payload?: unknown) => (payload as { reason?: unknown } | undefined)?.reason !== "runtime" && (payload as { reason?: unknown } | undefined)?.reason !== "catalog-v2" && cb()) ?? subscribeMockProjectTreeChanged(cb); |
| 1021 | } |
| 1022 | |
| 1023 | export function onLegacyEmptySessionCleanupChanged(cb: (status: LegacyEmptySessionCleanupStatus) => void): () => void { |
| 1024 | return hostEvents("legacy-empty-session-cleanup:changed", (payload?: unknown) => { |
| 1025 | if (payload && typeof payload === "object") cb(payload as LegacyEmptySessionCleanupStatus); |
| 1026 | }) ?? (() => {}); |
| 1027 | } |
| 1028 | |
| 1029 | // onTopicActivation subscribes to the "topic:activation" channel carrying the |
| 1030 | // lifecycle of ticketed StartTopicActivation requests (starting/ready/failed/ |
| 1031 | // cancelled). Returns an unsubscribe. |
| 1032 | export function onTopicActivation(cb: (event: TopicActivationEvent) => void): () => void { |
| 1033 | const off = hostEvents("topic:activation", (payload?: unknown) => { |
| 1034 | if (payload && typeof payload === "object") cb(payload as TopicActivationEvent); |
| 1035 | }); |
| 1036 | if (off) return off; |
| 1037 | mockTopicActivationListeners.add(cb); |
| 1038 | return () => mockTopicActivationListeners.delete(cb); |
| 1039 | } |
| 1040 | |
| 1041 | // onTabMeta subscribes to the "tab:meta" channel: a full refreshed Meta pushed |
| 1042 | // after the backend recomputes the expensive MetaForTab fields (git branch, |
| 1043 | // image-input capability) in the background. |
| 1044 | export function onTabMeta(cb: (event: TabMetaRefreshEvent) => void): () => void { |
| 1045 | const off = hostEvents("tab:meta", (payload?: unknown) => { |
| 1046 | if (payload && typeof payload === "object") cb(payload as TabMetaRefreshEvent); |
| 1047 | }); |
| 1048 | if (off) return off; |
| 1049 | mockTabMetaListeners.add(cb); |
| 1050 | return () => mockTabMetaListeners.delete(cb); |
| 1051 | } |
| 1052 | |
| 1053 | const mockTopicActivationListeners = new Set<(event: TopicActivationEvent) => void>(); |
| 1054 | const mockTabMetaListeners = new Set<(event: TabMetaRefreshEvent) => void>(); |
| 1055 | const mockReadyListeners = new Set<(tabId?: string) => void>(); |
| 1056 | |
| 1057 | function emitMockReady(tabId?: string): void { |
| 1058 | mockReadyListeners.forEach((listener) => listener(tabId)); |
| 1059 | } |
| 1060 | |
| 1061 | export function __emitMockTopicActivation(event: TopicActivationEvent): void { |
| 1062 | mockTopicActivationListeners.forEach((listener) => listener(event)); |
| 1063 | } |
| 1064 | |
| 1065 | export function __emitMockTabMeta(event: TabMetaRefreshEvent): void { |
| 1066 | mockTabMetaListeners.forEach((listener) => listener(event)); |
| 1067 | } |
| 1068 | |
| 1069 | export function onSessionRecovered(cb: (payload: SessionRecoveryEvent) => void): () => void { |
| 1070 | return hostEvents("session:recovered", (payload?: unknown) => cb((payload ?? {}) as SessionRecoveryEvent)) ?? (() => {}); |
| 1071 | } |
| 1072 | |
| 1073 | export function onSessionActiveVersionChanged(cb: (payload: SessionRecoveryEvent) => void): () => void { |
| 1074 | return hostEvents("session:active-version-changed", (payload?: unknown) => cb((payload ?? {}) as SessionRecoveryEvent)) ?? (() => {}); |
| 1075 | } |
| 1076 | |
| 1077 | export function onSessionRecoveryFailed(cb: (payload: SessionRecoveryFailedEvent) => void): () => void { |
| 1078 | return hostEvents("session:recovery-failed", (payload?: unknown) => cb((payload ?? {}) as SessionRecoveryFailedEvent)) ?? (() => {}); |
| 1079 | } |
| 1080 | |
| 1081 | export function onRemoteStatus(cb: (s: RemoteConnectionStatus) => void): () => void { |
| 1082 | return hostEvents("remote:status", (payload?: unknown) => cb((payload ?? {}) as RemoteConnectionStatus)) |
| 1083 | ?? registerMockRemoteListener("status", cb as (v: unknown) => void); |
| 1084 | } |
| 1085 | |
| 1086 | export function onRemoteForwards(cb: (e: RemoteForwardsEvent) => void): () => void { |
| 1087 | return hostEvents("remote:forwards", (payload?: unknown) => cb((payload ?? {}) as RemoteForwardsEvent)) |
| 1088 | ?? registerMockRemoteListener("forwards", cb as (v: unknown) => void); |
| 1089 | } |
| 1090 | |
| 1091 | export function onRemoteServer(cb: (s: RemoteServerView) => void): () => void { |
| 1092 | return hostEvents("remote:server", (payload?: unknown) => cb((payload ?? {}) as RemoteServerView)) |
| 1093 | ?? registerMockRemoteListener("server", cb as (v: unknown) => void); |
| 1094 | } |
| 1095 | |
| 1096 | // Mock event fan-out so browser-dev and tsx tests can drive remote:* events |
| 1097 | // without a shell runtime. |
| 1098 | type MockRemoteChannel = "status" | "forwards" | "server"; |
| 1099 | const mockRemoteListeners: Record<MockRemoteChannel, Set<(v: unknown) => void>> = { |
| 1100 | status: new Set(), |
| 1101 | forwards: new Set(), |
| 1102 | server: new Set(), |
| 1103 | }; |
| 1104 | function registerMockRemoteListener(ch: MockRemoteChannel, cb: (v: unknown) => void): () => void { |
| 1105 | mockRemoteListeners[ch].add(cb); |
| 1106 | return () => mockRemoteListeners[ch].delete(cb); |
| 1107 | } |
| 1108 | export function __emitMockRemote(ch: MockRemoteChannel, payload: unknown): void { |
| 1109 | for (const cb of mockRemoteListeners[ch]) cb(payload); |
| 1110 | } |
| 1111 | |
| 1112 | // app proxies each call to the live binding (or the dev mock only when truly |
| 1113 | // outside the shell), resolving the host at call time so a shell that attaches |
| 1114 | // late is picked up transparently. |
| 1115 | function bridgeBreadcrumb(method: string): string { |
| 1116 | if (method === "ReportCrash" || method === "RecordUIPerf") return ""; |
| 1117 | if (/^(Submit|SubmitDisplay|RunShell|Steer|Cancel|Approve|AnswerQuestion|ReplayPendingPrompts)/.test(method)) |
| 1118 | return `turn ${method}`; |
| 1119 | if (/^(SetModel|SetEffort|SetDefaultModel|SetPlannerModel|SetVisionModel|SetWebSearchModel|SetSubagentModel|SetSubagentEffort|SetMaxSubagentDepth|SetMaxSubagentConcurrency|SetMaxParallelWriters)/.test(method)) |
| 1120 | return `model ${method}`; |
| 1121 | if (/^(SetDesktop|SetCloseBehavior|SetDisplayMode|SetStatusBar|SetReasoningDisplayMode|SetExpandThinking|SetAutoPlan|SetDefaultToolApprovalMode|SetCompactRatio|SetReasoningLanguage)/.test(method)) |
| 1122 | return `settings ${method}`; |
| 1123 | if (/^(SetConnectionKey|AddProviderConnection|RenameProviderConnections|SaveProvider|SetProviderWebSearch|SaveProviderModelCatalogs|AddOfficialProviderAccess|UpgradeDeepSeekProviderAccess|AddProviderPresetAccess|ResetProviderPresetAccess|RemoveProviderAccess|RemoveProviderAccesses|DeleteProvider|SaveProviderKey|SetProviderKey|ClearProviderKey|TestProviderModel|FetchProviderModelCatalog|FetchAllProviderModelCatalogs|FetchProviderModels|FetchAllProviderModels|ConnectKey)/.test(method)) |
| 1124 | return `provider ${method}`; |
| 1125 | if (/^(CheckUpdate|ApplyUpdateRequest|OpenDownloadPage|OpenUserConfigPath|ReloadUserConfig)/.test(method)) return `update ${method}`; |
| 1126 | if (/^(AddMCPServer|InstallMCPServer|UpdateMCPServer|RemoveMCPServer|AuthorizeAndConnectMCPServer|AuthenticateMCPServer|ReconnectMCPServer|ClearMCPServerAuthentication|SetMCPServer)/.test(method)) |
| 1127 | return `mcp ${method}`; |
| 1128 | if (/^(AddSkillPath|RemoveSkillPath|SetSkillPathEnabled|RefreshSkills|SetSkillEnabled|SetSkillImplicitInvocation|AcceptSkillSuggestion|AvailableSubagentTools|CreateSubagentProfile|UpdateSubagentProfile|DeleteSubagentProfile|SetSubagentProfileModel|SetSubagentProfileEffort|TrySubagentProfile|CancelTrySubagentProfile)/.test(method)) |
| 1129 | return `skill ${method}`; |
| 1130 | if (/^(MinimiseMainWindow|ToggleMaximiseMainWindow|IsMainWindowMaximised|CloseMainWindow)$/.test(method)) return `window ${method}`; |
| 1131 | if (/^(OpenProjectTab|OpenGlobalTab|OpenTopicSession|EnsureBlankTab|ActivateTopic|StartTopicActivation|EnsureBlankSurface|SetActiveTab|CloseTab|RegisterNavigationIntent|CloseMergedWorktreeTab|ReorderTabs|CreateTopic|RenameTopic|DeleteTopic|TrashTopic|RenameProject|RemoveWorkspace|SwitchWorkspace|PickWorkspace|IsolatedWorktreeAvailability|CreateIsolatedWorktree|InspectWorktreeMerge|GetWorktreeStatus|PrepareWorktreeMerge|ReconcileRecoveryVersions|MergeWorktreeBack|FinalizeWorktreeMerge|DeliveryWorktreeAvailability|CreateDeliveryWorktree)/.test(method)) |
| 1132 | return `nav ${method}`; |
| 1133 | return ""; |
| 1134 | } |
| 1135 | |
| 1136 | function elapsedMs(startedAt: number): number { |
| 1137 | const now = typeof performance !== "undefined" ? performance.now() : Date.now(); |
| 1138 | return Math.max(0, Math.round(now - startedAt)); |
| 1139 | } |
| 1140 | |
| 1141 | export const app: AppBindings = new Proxy({} as AppBindings, { |
| 1142 | get(_t, prop) { |
| 1143 | const host = desktopHost().app, target = host ?? getMock(); |
| 1144 | let v = (target as unknown as Record<string, unknown>)[String(prop)]; |
| 1145 | if (!host && v === undefined && typeof prop === "string") v = (...args: unknown[]) => import("./attachmentBindings").then( |
| 1146 | module => module.callMockAttachment(target, prop as keyof AttachmentBindings, args)); |
| 1147 | if (typeof v !== "function") return v; |
| 1148 | return (...args: unknown[]) => { |
| 1149 | const method = String(prop), crumb = bridgeBreadcrumb(method); |
| 1150 | const startedAt = crumb ? (typeof performance !== "undefined" ? performance.now() : Date.now()) : 0; |
| 1151 | if (crumb) addBreadcrumb("bridge", crumb); |
| 1152 | try { |
| 1153 | const result = maybeShare(method, args, () => (v as (...a: unknown[]) => unknown).apply(target, args)); |
| 1154 | if (result && typeof (result as Promise<unknown>).then === "function") { |
| 1155 | return (result as Promise<unknown>).then( |
| 1156 | (value) => { |
| 1157 | if (crumb) addBreadcrumb("bridge", `${crumb} done ms=${elapsedMs(startedAt)}`); |
| 1158 | return value; |
| 1159 | }, |
| 1160 | (err) => { |
| 1161 | if (crumb) addBreadcrumb("bridge.error", `${method} ms=${elapsedMs(startedAt)}`); |
| 1162 | throw err; |
| 1163 | }, |
| 1164 | ); |
| 1165 | } |
| 1166 | if (crumb) addBreadcrumb("bridge", `${crumb} done ms=${elapsedMs(startedAt)}`); |
| 1167 | return result; |
| 1168 | } catch (err) { |
| 1169 | if (crumb) addBreadcrumb("bridge.error", `${method} ms=${elapsedMs(startedAt)}`); |
| 1170 | throw err; |
| 1171 | } |
| 1172 | }; |
| 1173 | }, |
| 1174 | }); |
| 1175 | |
| 1176 | // openExternal opens a URL in the system browser (so links in rendered markdown |
| 1177 | // don't navigate the webview away from the app). Falls back to window.open in the |
| 1178 | // browser dev mock. |
| 1179 | export function openExternal(url: string): void { |
| 1180 | desktopHost().native.openExternal(url); |
| 1181 | } |
| 1182 | |
| 1183 | // --- browser dev mock -------------------------------------------------------- |
| 1184 | |
| 1185 | const listeners = new Set<(e: WireEvent) => void>(); |
| 1186 | let mockScopedTabId: string | undefined; |
| 1187 | let mockPendingTopicActivation: { requestId: string; tabId: string } | undefined; |
| 1188 | let mockTopicActivationCounter = 0; |
| 1189 | |
| 1190 | function mockSubscribe(cb: (e: WireEvent) => void): () => void { |
| 1191 | listeners.add(cb); |
| 1192 | return () => { |
| 1193 | listeners.delete(cb); |
| 1194 | }; |
| 1195 | } |
| 1196 | |
| 1197 | function emit(e: WireEvent) { |
| 1198 | const event = mockScopedTabId && !e.tabId ? { ...e, tabId: mockScopedTabId } : e; |
| 1199 | publishMockTranscriptEvent(event); |
| 1200 | listeners.forEach((l) => l(event)); |
| 1201 | } |
| 1202 | |
| 1203 | export function mockToolApprovalModeAfterModeChange(current: string | undefined, nextMode: Mode): ToolApprovalMode { |
| 1204 | if (modeHasAutoApproveTools(nextMode)) return "workspace-write"; |
| 1205 | return normalizeToolApprovalMode(current); |
| 1206 | } |
| 1207 | |
| 1208 | async function withMockTabScope<T>(tabId: string, fn: () => Promise<T>): Promise<T> { |
| 1209 | const previous = mockScopedTabId; |
| 1210 | mockScopedTabId = tabId || previous; |
| 1211 | try { |
| 1212 | return await fn(); |
| 1213 | } finally { |
| 1214 | mockScopedTabId = previous; |
| 1215 | } |
| 1216 | } |
| 1217 | |
| 1218 | // Updater progress has its own listener set so the browser dev mock can stream a |
| 1219 | // fake download/install flow through onUpdaterProgress. |
| 1220 | const updaterListeners = new Set<(p: UpdateProgress) => void>(); |
| 1221 | |
| 1222 | function emitUpdater(p: UpdateProgress) { |
| 1223 | updaterListeners.forEach((l) => l(p)); |
| 1224 | } |
| 1225 | |
| 1226 | // Test seam for the browser-dev updater state machine. The desktop shell |
| 1227 | // receives the same payloads through the host event stream. |
| 1228 | export function __emitMockUpdater(p: UpdateProgress): void { |
| 1229 | emitUpdater(p); |
| 1230 | } |
| 1231 | |
| 1232 | function delay(ms: number): Promise<void> { |
| 1233 | return new Promise((r) => setTimeout(r, ms)); |
| 1234 | } |
| 1235 | |
| 1236 | function baseName(path: string): string { |
| 1237 | return path.replace(/[/\\]+$/, "").split(/[/\\]/).filter(Boolean).pop() ?? path; |
| 1238 | } |
| 1239 | |
| 1240 | function browserPlatformOverride(): "darwin" | "windows" | "linux" | "" { |
| 1241 | if (typeof window === "undefined" || desktopHost().kind !== "none") return ""; |
| 1242 | const value = new URLSearchParams(window.location.search).get("platform"); |
| 1243 | return value === "darwin" || value === "windows" || value === "linux" ? value : ""; |
| 1244 | } |
| 1245 | |
| 1246 | function browserPreviewBashSandboxMode(): "enforce" | "off" { |
| 1247 | return browserPlatformOverride() === "windows" ? "off" : "enforce"; |
| 1248 | } |
| 1249 | |
| 1250 | function browserPreviewEffectiveShell(prefer = "auto"): "bash" | "git-bash" | "powershell" | "pwsh" { |
| 1251 | const normalized = prefer.trim().toLowerCase(); |
| 1252 | if (normalized === "powershell" || normalized === "pwsh") return normalized; |
| 1253 | return browserPlatformOverride() === "windows" ? (normalized === "bash" ? "git-bash" : "pwsh") : "bash"; |
| 1254 | } |
| 1255 | |
| 1256 | function mockScenario(): "demo" | "fresh" | "running" | "guidance" | "recovery" | "sandbox_escape" | "notice" | "deepseek_upgrade" | "bench" { |
| 1257 | if (typeof window === "undefined") return "demo"; |
| 1258 | const value = new URLSearchParams(window.location.search).get("mock")?.trim().toLowerCase(); |
| 1259 | if (value === "fresh" || value === "empty" || value === "first-run") return "fresh"; |
| 1260 | if (typeof import.meta.env !== "undefined" && import.meta.env.DEV && (value === "recovery" || value === "inbox-recovery")) return "recovery"; if (value === "guidance" || value === "guide" || value === "steer") return "guidance"; |
| 1261 | if (value === "running" || value === "busy" || value === "streaming") return "running"; |
| 1262 | if (value === "sandbox_escape" || value === "sandbox-escape" || value === "sandboxescape") return "sandbox_escape"; |
| 1263 | if (value === "notice" || value === "notices" || value === "notice-preview") return "notice"; |
| 1264 | if (value === "deepseek_upgrade" || value === "deepseek-upgrade") return "deepseek_upgrade"; |
| 1265 | if (value === "bench" || value === "benchmark" || value === "perf") return "bench"; |
| 1266 | return "demo"; |
| 1267 | } |
| 1268 | |
| 1269 | const mockProviderPresetTemplates: MockProviderPresetTemplate[] = [ |
| 1270 | mockBundlePreset( |
| 1271 | "opencode-go-recommended", |
| 1272 | "OpenCode Go (Recommended)", |
| 1273 | "One key, three safe routes: Chat, Anthropic Messages, and Responses. Defaults use a low-cost effort and a 32K output cap.", |
| 1274 | "OPENCODE_GO_API_KEY", |
| 1275 | [ |
| 1276 | mockProviderTemplate({ name: "opencode-go", kind: "openai", baseUrl: "https://opencode.ai/zen/go/v1", models: mockOpenCodeGoModels, visionModels: ["kimi-k3"], default: "glm-5.3", apiKeyEnv: "OPENCODE_GO_API_KEY", contextWindow: 128000, modelOverrides: [{ model: "glm-5.3", reasoningProtocol: "openai", supportedEfforts: ["low", "high", "max"], defaultEffort: "low", maxOutputTokens: 32768 }, { model: "kimi-k3", reasoningProtocol: "openai", supportedEfforts: ["high", "max"], defaultEffort: "max", contextWindow: 1048576, maxOutputTokens: 32768 }] }), |
| 1277 | mockProviderTemplate({ name: "opencode-go-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen/go", models: ["qwen3.8-max", "qwen3.7-plus", "qwen3.7-max", "qwen3.6-plus", "minimax-m3", "minimax-m2.7", "minimax-m2.5"], visionModels: ["qwen3.8-max", "qwen3.7-plus", "qwen3.6-plus"], default: "qwen3.7-plus", apiKeyEnv: "OPENCODE_GO_API_KEY", thinking: "adaptive", contextWindow: 262144 }), |
| 1278 | mockProviderTemplate({ name: "opencode-go-responses", kind: "responses", baseUrl: "https://opencode.ai/zen/go/v1", models: ["grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor"], visionModels: ["grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor"], default: "grok-4.5", apiKeyEnv: "OPENCODE_GO_API_KEY", contextWindow: 500000, supportedEfforts: ["low", "medium", "high"], defaultEffort: "low" }), |
| 1279 | ], |
| 1280 | { recommended: true, billingMode: "subscription_equivalent", displayGroup: "opencode", displaySection: "go", displayTier: "primary", routeKind: "bundle", displayOrder: 0 }, |
| 1281 | ), |
| 1282 | mockPreset("deepseek-responses", "DeepSeek Official Responses API", "Official stateless DeepSeek Responses API for Flash and Pro with server-side web search.", "DEEPSEEK_API_KEY", mockProviderTemplate({ name: "deepseek-responses", kind: "responses", baseUrl: "https://api.deepseek.com", models: ["deepseek-v4-flash", "deepseek-v4-pro"], default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", balanceUrl: "https://api.deepseek.com/user/balance", webSearch: true, serverWebSearchCapability: true, contextWindow: 1000000, modelOverrides: [{ model: "deepseek-v4-flash", reasoningProtocol: "", supportedEfforts: ["disabled", "low", "high", "max"], defaultEffort: "high" }, { model: "deepseek-v4-pro", reasoningProtocol: "", supportedEfforts: ["disabled", "high", "max"], defaultEffort: "high" }] })), |
| 1283 | mockPreset("longcat-openai", "LongCat OpenAI", "LongCat Platform OpenAI-compatible endpoint for LongCat-2.0.", "LONGCAT_API_KEY", mockProviderTemplate({ name: "longcat-openai", kind: "openai", baseUrl: "https://api.longcat.chat/openai/v1", modelsUrl: "https://api.longcat.chat/openai/v1/models", models: mockLongCatModels, default: "LongCat-2.0", apiKeyEnv: "LONGCAT_API_KEY", contextWindow: 131072, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })), |
| 1284 | mockPreset("longcat-anthropic", "LongCat Anthropic", "LongCat Platform Anthropic-compatible Messages endpoint for LongCat-2.0.", "LONGCAT_API_KEY", mockProviderTemplate({ name: "longcat-anthropic", kind: "anthropic", baseUrl: "https://api.longcat.chat/anthropic", modelsUrl: "https://api.longcat.chat/anthropic/v1/models", models: mockLongCatModels, default: "LongCat-2.0", apiKeyEnv: "LONGCAT_API_KEY", authHeader: true, contextWindow: 131072, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })), |
| 1285 | mockPreset("token-rhythm", "Token Rhythm", "Token Rhythm (基元律动) multi-model OpenAI-compatible gateway.", "TOKEN_RHYTHM_API_KEY", mockProviderTemplate({ name: "token-rhythm", kind: "openai", baseUrl: "https://tokenrhythm.studio/v1", modelsUrl: "https://tokenrhythm.studio/v1/models", models: mockTokenRhythmModels, visionModels: ["kimi-k2.5", "kimi-k2.6", "kimi-k2.7-code"], default: "deepseek-v4-flash", apiKeyEnv: "TOKEN_RHYTHM_API_KEY", contextWindow: 1000000, modelOverrides: mockTokenRhythmModelOverrides })), |
| 1286 | mockPreset("kimi-cn", "Kimi CN API", "Moonshot Kimi China OpenAI-compatible API.", "KIMI_API_KEY", mockProviderTemplate({ name: "kimi-cn", kind: "openai", baseUrl: "https://api.moonshot.cn/v1", models: mockKimiAPIModels, visionModels: mockKimiAPIModels, default: "kimi-k2.7-code", apiKeyEnv: "KIMI_API_KEY", balanceUrl: "https://api.moonshot.cn/v1/users/me/balance", contextWindow: 262144, reasoningProtocol: "none", modelOverrides: [{ model: "kimi-k3", reasoningProtocol: "openai", supportedEfforts: ["low", "high", "max"], defaultEffort: "max", contextWindow: 1048576 }] })), |
| 1287 | mockPreset("kimi-global", "Kimi Global API", "Moonshot Kimi international OpenAI-compatible API.", "MOONSHOT_API_KEY", mockProviderTemplate({ name: "kimi-global", kind: "openai", baseUrl: "https://api.moonshot.ai/v1", models: mockKimiAPIModels, visionModels: mockKimiAPIModels, default: "kimi-k2.7-code", apiKeyEnv: "MOONSHOT_API_KEY", balanceUrl: "https://api.moonshot.ai/v1/users/me/balance", contextWindow: 262144, reasoningProtocol: "none", modelOverrides: [{ model: "kimi-k3", reasoningProtocol: "openai", supportedEfforts: ["low", "high", "max"], defaultEffort: "max", contextWindow: 1048576 }] })), |
| 1288 | mockPreset("kimi-coding-plan", "Kimi Coding Plan", "Kimi Coding Plan via its dedicated Anthropic-compatible endpoint.", "KIMI_CODING_API_KEY", mockProviderTemplate({ name: "kimi-coding-plan", kind: "anthropic", baseUrl: "https://api.kimi.com/coding/", models: ["kimi-for-coding"], visionModels: ["kimi-for-coding"], default: "kimi-for-coding", apiKeyEnv: "KIMI_CODING_API_KEY", headers: { "User-Agent": "claude-code/0.1.0" }, thinking: "adaptive", contextWindow: 262144 })), |
| 1289 | mockPreset("mimo-api", "MiMo API", "Xiaomi MiMo direct API with text and vision-capable models.", "MIMO_API_KEY", mockProviderTemplate({ name: "mimo-api", kind: "openai", baseUrl: "https://api.xiaomimimo.com/v1", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_API_KEY", contextWindow: 1048576 })), |
| 1290 | mockPreset("mimo-anthropic", "MiMo Anthropic", "Xiaomi MiMo direct Anthropic-compatible endpoint.", "MIMO_API_KEY", mockProviderTemplate({ name: "mimo-anthropic", kind: "anthropic", baseUrl: "https://api.xiaomimimo.com/anthropic", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_API_KEY", thinking: "adaptive", contextWindow: 1048576 })), |
| 1291 | mockPreset("mimo-token-plan-cn", "MiMo Token Plan CN", "Xiaomi MiMo token-plan China endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-cn", kind: "openai", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", contextWindow: 1048576 })), |
| 1292 | mockPreset("mimo-token-plan-cn-anthropic", "MiMo Token Plan CN Anthropic", "Xiaomi MiMo token-plan China Anthropic-compatible endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-cn-anthropic", kind: "anthropic", baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", thinking: "adaptive", contextWindow: 1048576 })), |
| 1293 | mockPreset("mimo-token-plan-sgp", "MiMo Token Plan SGP", "Xiaomi MiMo token-plan Singapore endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-sgp", kind: "openai", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", contextWindow: 1048576 })), |
| 1294 | mockPreset("mimo-token-plan-sgp-anthropic", "MiMo Token Plan SGP Anthropic", "Xiaomi MiMo token-plan Singapore Anthropic-compatible endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-sgp-anthropic", kind: "anthropic", baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", thinking: "adaptive", contextWindow: 1048576 })), |
| 1295 | mockPreset("mimo-token-plan-ams", "MiMo Token Plan AMS", "Xiaomi MiMo token-plan Amsterdam endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-ams", kind: "openai", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", contextWindow: 1048576 })), |
| 1296 | mockPreset("mimo-token-plan-ams-anthropic", "MiMo Token Plan AMS Anthropic", "Xiaomi MiMo token-plan Amsterdam Anthropic-compatible endpoint.", "MIMO_TOKEN_PLAN_API_KEY", mockProviderTemplate({ name: "mimo-token-plan-ams-anthropic", kind: "anthropic", baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", models: mockMiMoV25Models, visionModels: ["mimo-v2.5"], default: "mimo-v2.5-pro", apiKeyEnv: "MIMO_TOKEN_PLAN_API_KEY", thinking: "adaptive", contextWindow: 1048576 })), |
| 1297 | mockPreset("minimax-cn-api", "MiniMax CN API", "MiniMax China OpenAI-compatible M-series API endpoint.", "MINIMAX_API_KEY", mockProviderTemplate({ name: "minimax-cn-api", kind: "openai", baseUrl: "https://api.minimaxi.com/v1", models: mockMiniMaxModels, visionModels: ["MiniMax-M3"], default: "MiniMax-M3", apiKeyEnv: "MINIMAX_API_KEY", extraBody: { reasoning_split: true }, contextWindow: 1048576, thinking: "adaptive", supportedEfforts: ["disabled", "adaptive"], defaultEffort: "adaptive" })), |
| 1298 | mockPreset("minimax-global-api", "MiniMax Global API", "MiniMax international OpenAI-compatible M-series API endpoint.", "MINIMAX_API_KEY", mockProviderTemplate({ name: "minimax-global-api", kind: "openai", baseUrl: "https://api.minimax.io/v1", models: mockMiniMaxModels, visionModels: ["MiniMax-M3"], default: "MiniMax-M3", apiKeyEnv: "MINIMAX_API_KEY", extraBody: { reasoning_split: true }, contextWindow: 1048576, thinking: "adaptive", supportedEfforts: ["disabled", "adaptive"], defaultEffort: "adaptive" })), |
| 1299 | mockPreset("minimax-cn-anthropic", "MiniMax CN Anthropic", "MiniMax China Anthropic-compatible M-series endpoint.", "MINIMAX_PLAN_API_KEY", mockProviderTemplate({ name: "minimax-cn-anthropic", kind: "anthropic", baseUrl: "https://api.minimaxi.com/anthropic", models: mockMiniMaxModels, visionModels: ["MiniMax-M3"], default: "MiniMax-M3", apiKeyEnv: "MINIMAX_PLAN_API_KEY", authHeader: true, contextWindow: 1048576, thinking: "adaptive", supportedEfforts: ["disabled", "adaptive"], defaultEffort: "adaptive" })), |
| 1300 | mockPreset("minimax-global-anthropic", "MiniMax Global Anthropic", "MiniMax international Anthropic-compatible endpoint with Bearer auth.", "MINIMAX_API_KEY", mockProviderTemplate({ name: "minimax-global-anthropic", kind: "anthropic", baseUrl: "https://api.minimax.io/anthropic", models: mockMiniMaxModels, visionModels: ["MiniMax-M3"], default: "MiniMax-M3", apiKeyEnv: "MINIMAX_API_KEY", authHeader: true, contextWindow: 1048576, thinking: "adaptive", supportedEfforts: ["disabled", "adaptive"], defaultEffort: "adaptive" })), |
| 1301 | mockPreset("glm-cn", "GLM CN API", "Zhipu GLM China OpenAI-compatible API with thinking controls.", "GLM_API_KEY", mockProviderTemplate({ name: "glm-cn", kind: "openai", baseUrl: "https://open.bigmodel.cn/api/paas/v4", models: mockGLMAPIModels, visionModels: ["glm-5v-turbo"], default: "glm-5.2", apiKeyEnv: "GLM_API_KEY", contextWindow: 1000000, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })), |
| 1302 | mockPreset("zai-global", "Z.AI Global API", "Z.AI international OpenAI-compatible GLM API.", "ZAI_API_KEY", mockProviderTemplate({ name: "zai-global", kind: "openai", baseUrl: "https://api.z.ai/api/paas/v4", models: mockGLMAPIModels, visionModels: ["glm-5v-turbo"], default: "glm-5.2", apiKeyEnv: "ZAI_API_KEY", contextWindow: 1000000, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })), |
| 1303 | mockPreset("glm-coding-plan-cn", "GLM Coding Plan CN", "Zhipu GLM China coding-plan endpoint.", "GLM_PLAN_API_KEY", mockProviderTemplate({ name: "glm-coding-plan-cn", kind: "openai", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", models: mockGLMCodingModels, default: "glm-5.2", apiKeyEnv: "GLM_PLAN_API_KEY", contextWindow: 1000000, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })), |
| 1304 | mockPreset("glm-coding-plan-cn-anthropic", "GLM Coding Plan CN Anthropic", "Zhipu GLM China coding-plan Anthropic-compatible endpoint.", "GLM_PLAN_API_KEY", mockProviderTemplate({ name: "glm-coding-plan-cn-anthropic", kind: "anthropic", baseUrl: "https://open.bigmodel.cn/api/anthropic", models: mockGLMAnthropicModels, default: "glm-5.2", apiKeyEnv: "GLM_PLAN_API_KEY", authHeader: true, thinking: "adaptive", contextWindow: 1000000 })), |
| 1305 | mockPreset("zai-coding-plan-global", "Z.AI Coding Plan Global", "Z.AI international coding-plan endpoint.", "ZAI_CODING_API_KEY", mockProviderTemplate({ name: "zai-coding-plan-global", kind: "openai", baseUrl: "https://api.z.ai/api/coding/paas/v4", models: mockGLMCodingModels, default: "glm-5.2", apiKeyEnv: "ZAI_CODING_API_KEY", contextWindow: 1000000, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })), |
| 1306 | mockPreset("zai-coding-plan-global-anthropic", "Z.AI Coding Plan Global Anthropic", "Z.AI international coding-plan Anthropic-compatible endpoint.", "ZAI_CODING_API_KEY", mockProviderTemplate({ name: "zai-coding-plan-global-anthropic", kind: "anthropic", baseUrl: "https://api.z.ai/api/anthropic", models: mockGLMAnthropicModels, default: "glm-5.2", apiKeyEnv: "ZAI_CODING_API_KEY", authHeader: true, thinking: "adaptive", contextWindow: 1000000 })), |
| 1307 | mockPreset("opencode-go", "OpenCode Go", "OpenCode Go relay with per-model capability overrides.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go", kind: "openai", baseUrl: "https://opencode.ai/zen/go/v1", models: mockOpenCodeGoModels, visionModels: ["kimi-k3"], default: "glm-5.2", apiKeyEnv: "OPENCODE_GO_API_KEY", contextWindow: 128000, modelOverrides: [{ model: "glm-5.3", reasoningProtocol: "openai", supportedEfforts: ["low", "high", "max"], defaultEffort: "high" }, { model: "glm-5.2", reasoningProtocol: "openai", supportedEfforts: ["high", "max"], defaultEffort: "high" }, { model: "kimi-k3", reasoningProtocol: "openai", supportedEfforts: ["high", "max"], defaultEffort: "max", contextWindow: 1048576 }, { model: "hy3", reasoningProtocol: "openai", supportedEfforts: ["none", "low", "high"], defaultEffort: "high" }] }), { displayGroup: "opencode", displaySection: "go", displayTier: "compatibility", routeKind: "chat", displayOrder: 90 }), |
| 1308 | mockPreset("opencode-go-anthropic", "OpenCode Go Anthropic", "OpenCode Go subscription Anthropic-compatible route for Qwen and MiniMax.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen/go", models: ["qwen3.8-max", "qwen3.7-plus", "qwen3.7-max", "qwen3.6-plus", "minimax-m3", "minimax-m2.7", "minimax-m2.5"], visionModels: ["qwen3.8-max", "qwen3.7-plus", "qwen3.6-plus"], default: "qwen3.7-plus", apiKeyEnv: "OPENCODE_GO_API_KEY", thinking: "adaptive", contextWindow: 262144 }), { displayGroup: "opencode", displaySection: "go", displayTier: "advanced", routeKind: "anthropic", displayOrder: 20 }), |
| 1309 | mockPreset("opencode-go-responses", "OpenCode Go Responses", "OpenCode Go Responses API route for Grok 4.5, GPT 5.6 Luna, and Muse Spark.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-responses", kind: "responses", baseUrl: "https://opencode.ai/zen/go/v1", models: ["grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor"], visionModels: ["grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor"], default: "grok-4.5", apiKeyEnv: "OPENCODE_GO_API_KEY", contextWindow: 500000, supportedEfforts: ["low", "medium", "high"], defaultEffort: "high", modelOverrides: [{ model: "grok-4.5", reasoningProtocol: "", supportedEfforts: ["low", "medium", "high"], defaultEffort: "high", contextWindow: 500000 }, { model: "gpt-5.6-luna", reasoningProtocol: "", supportedEfforts: ["none", "low", "medium", "high", "xhigh", "max"], defaultEffort: "high", contextWindow: 1050000 }, { model: "muse-spark-1.2-contributor", reasoningProtocol: "", supportedEfforts: ["minimal", "low", "medium", "high", "xhigh"], defaultEffort: "high", contextWindow: 1048576 }] }), { displayGroup: "opencode", displaySection: "go", displayTier: "advanced", routeKind: "responses", displayOrder: 30 }), |
| 1310 | mockPreset("opencode-go-deepseek-anthropic", "OpenCode Go DeepSeek Anthropic", "OpenCode Go Anthropic Messages route for DeepSeek Flash with server-side web search.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-deepseek-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen/go", models: ["deepseek-v4-flash"], default: "deepseek-v4-flash", apiKeyEnv: "OPENCODE_GO_API_KEY", thinking: "adaptive", webSearch: true, serverWebSearchCapability: true, contextWindow: 262144, supportedEfforts: ["disabled", "high", "max"], defaultEffort: "high" }), { displayGroup: "opencode", displaySection: "go", displayTier: "advanced", routeKind: "search-anthropic", optional: true, displayOrder: 40 }), |
| 1311 | mockPreset("opencode-go-deepseek-responses", "OpenCode Go DeepSeek Responses", "OpenCode Go stateless Responses API route for DeepSeek Flash with server-side web search.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-deepseek-responses", kind: "responses", baseUrl: "https://opencode.ai/zen/go/v1", models: ["deepseek-v4-flash"], default: "deepseek-v4-flash", apiKeyEnv: "OPENCODE_GO_API_KEY", webSearch: true, serverWebSearchCapability: true, contextWindow: 1000000, supportedEfforts: ["disabled", "high", "max"], defaultEffort: "high" }), { displayGroup: "opencode", displaySection: "go", displayTier: "advanced", routeKind: "search-responses", optional: true, displayOrder: 50 }), |
| 1312 | mockPreset("opencode-zen-anthropic", "OpenCode Zen Anthropic", "OpenCode Zen Anthropic-compatible route for Claude and Qwen models.", "OPENCODE_API_KEY", mockProviderTemplate({ name: "opencode-zen-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen", models: ["claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5", "qwen3.6-plus", "qwen3.5-plus", "qwen3.6-plus-free"], visionModels: ["claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5"], default: "claude-sonnet-4-6", apiKeyEnv: "OPENCODE_API_KEY", contextWindow: 262144 }), { displayGroup: "opencode", displaySection: "zen", displayTier: "primary", routeKind: "zen-anthropic", displayOrder: 100 }), |
| 1313 | mockPreset("qwen-cn", "Qwen CN API", "Alibaba DashScope China standard OpenAI-compatible endpoint.", "QWEN_API_KEY", mockProviderTemplate({ name: "qwen-cn", kind: "openai", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", models: mockQwenAPIModels, visionModels: ["qwen3.7-plus", "qwen3.6-plus", "qwen3.5-plus", "kimi-k2.5"], default: "qwen3.7-plus", apiKeyEnv: "QWEN_API_KEY" })), |
| 1314 | mockPreset("qwen-global", "Qwen Global API", "Alibaba DashScope international standard OpenAI-compatible endpoint.", "QWEN_API_KEY", mockProviderTemplate({ name: "qwen-global", kind: "openai", baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", models: mockQwenAPIModels, visionModels: ["qwen3.7-plus", "qwen3.6-plus", "qwen3.5-plus", "kimi-k2.5"], default: "qwen3.7-plus", apiKeyEnv: "QWEN_API_KEY" })), |
| 1315 | mockPreset("qwen-coding-plan-cn", "Qwen Coding Plan CN", "Alibaba Cloud Qwen Coding Plan China endpoint.", "QWEN_CODING_API_KEY", mockProviderTemplate({ name: "qwen-coding-plan-cn", kind: "openai", baseUrl: "https://coding.dashscope.aliyuncs.com/v1", models: mockQwenPlanModels, visionModels: mockQwenPlanVisionModels, default: "qwen3.7-plus", apiKeyEnv: "QWEN_CODING_API_KEY" })), |
| 1316 | mockPreset("qwen-coding-plan-cn-anthropic", "Qwen Coding Plan CN Anthropic", "Alibaba Cloud Qwen Coding Plan China Anthropic-compatible endpoint.", "QWEN_CODING_API_KEY", mockProviderTemplate({ name: "qwen-coding-plan-cn-anthropic", kind: "anthropic", baseUrl: "https://coding.dashscope.aliyuncs.com/apps/anthropic", models: mockQwenPlanModels, visionModels: mockQwenPlanVisionModels, default: "qwen3.7-plus", apiKeyEnv: "QWEN_CODING_API_KEY", thinking: "adaptive" })), |
| 1317 | mockPreset("qwen-coding-plan-global", "Qwen Coding Plan Global", "Alibaba Cloud Qwen Coding Plan international endpoint.", "QWEN_CODING_API_KEY", mockProviderTemplate({ name: "qwen-coding-plan-global", kind: "openai", baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", models: mockQwenPlanModels, visionModels: mockQwenPlanVisionModels, default: "qwen3.7-plus", apiKeyEnv: "QWEN_CODING_API_KEY" })), |
| 1318 | mockPreset("qwen-coding-plan-global-anthropic", "Qwen Coding Plan Global Anthropic", "Alibaba Cloud Qwen Coding Plan international Anthropic-compatible endpoint.", "QWEN_CODING_API_KEY", mockProviderTemplate({ name: "qwen-coding-plan-global-anthropic", kind: "anthropic", baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic", models: mockQwenPlanModels, visionModels: mockQwenPlanVisionModels, default: "qwen3.7-plus", apiKeyEnv: "QWEN_CODING_API_KEY", thinking: "adaptive" })), |
| 1319 | mockPreset("stepfun", "StepFun", "StepFun coding-plan OpenAI-compatible endpoint.", "STEPFUN_API_KEY", mockProviderTemplate({ name: "stepfun", kind: "openai", baseUrl: "https://api.stepfun.com/step_plan/v1", models: mockStepFunModels, default: "step-3.7-flash", apiKeyEnv: "STEPFUN_API_KEY", supportedEfforts: ["low", "medium", "high"], defaultEffort: "medium" })), |
| 1320 | mockPreset("stepfun-anthropic", "StepFun Anthropic", "StepFun coding-plan Anthropic-compatible endpoint.", "STEPFUN_API_KEY", mockProviderTemplate({ name: "stepfun-anthropic", kind: "anthropic", baseUrl: "https://api.stepfun.com/step_plan", models: mockStepFunModels, default: "step-3.7-flash", apiKeyEnv: "STEPFUN_API_KEY", thinking: "adaptive", supportedEfforts: ["low", "medium", "high"], defaultEffort: "medium" })), |
| 1321 | mockPreset("stepfun-responses", "StepFun Responses API", "StepFun Responses API endpoint with reasoning effort and tool calls (step-3.7-flash).", "STEPFUN_API_KEY", mockProviderTemplate({ name: "stepfun-responses", kind: "responses", baseUrl: "https://api.stepfun.com/v1", models: ["step-3.7-flash"], default: "step-3.7-flash", apiKeyEnv: "STEPFUN_API_KEY", supportedEfforts: ["low", "medium", "high"], defaultEffort: "medium" })), |
| 1322 | mockPreset("stepfun-api", "StepFun API Pay-as-you-go", "StepFun pay-as-you-go OpenAI-compatible API with vision on step-3.7-flash.", "STEPFUN_API_KEY", mockProviderTemplate({ name: "stepfun-api", kind: "openai", baseUrl: "https://api.stepfun.com/v1", models: mockStepFunModels, visionModels: ["step-3.7-flash"], default: "step-3.7-flash", apiKeyEnv: "STEPFUN_API_KEY", supportedEfforts: ["low", "medium", "high"], defaultEffort: "medium" })), |
| 1323 | mockPreset("stepfun-api-anthropic", "StepFun API Anthropic Pay-as-you-go", "StepFun pay-as-you-go Anthropic-compatible Messages API with automatic prefix caching.", "STEPFUN_API_KEY", mockProviderTemplate({ name: "stepfun-api-anthropic", kind: "anthropic", baseUrl: "https://api.stepfun.com", models: mockStepFunModels, default: "step-3.7-flash", apiKeyEnv: "STEPFUN_API_KEY", thinking: "adaptive", supportedEfforts: ["low", "medium", "high"], defaultEffort: "medium" })), |
| 1324 | mockPreset("novita", "NovitaAI", "NovitaAI OpenAI-compatible multi-model gateway.", "NOVITA_API_KEY", mockProviderTemplate({ name: "novita", kind: "openai", baseUrl: "https://api.novita.ai/openai/v1", models: mockNovitaModels, default: "zai-org/glm-5.2", apiKeyEnv: "NOVITA_API_KEY" })), |
| 1325 | mockPreset("gmi", "GMI Cloud", "GMI Cloud direct multi-model OpenAI-compatible gateway.", "GMI_API_KEY", mockProviderTemplate({ name: "gmi", kind: "openai", baseUrl: "https://api.gmi-serving.com/v1", models: mockGMIModels, default: "zai-org/GLM-5.2-FP8", apiKeyEnv: "GMI_API_KEY", headers: { "User-Agent": "Reasonix" } })), |
| 1326 | mockPreset("vercel-ai-gateway", "Vercel AI Gateway", "Vercel AI Gateway via Anthropic-compatible Messages API.", "AI_GATEWAY_API_KEY", mockProviderTemplate({ name: "vercel-ai-gateway", kind: "anthropic", baseUrl: "https://ai-gateway.vercel.sh", models: mockVercelModels, visionModels: ["anthropic/claude-sonnet-4.6", "anthropic/claude-opus-4.8", "openai/gpt-5.4", "openai/gpt-5.4-pro", "moonshotai/kimi-k2.7-code"], default: "anthropic/claude-sonnet-4.6", apiKeyEnv: "AI_GATEWAY_API_KEY", authHeader: true, contextWindow: 1000000 })), |
| 1327 | mockPreset("huggingface", "HuggingFace Router", "HuggingFace Inference Router OpenAI-compatible endpoint.", "HF_TOKEN", mockProviderTemplate({ name: "huggingface", kind: "openai", baseUrl: "https://router.huggingface.co/v1", models: ["zai-org/GLM-5.2", "deepseek-ai/DeepSeek-V3.2", "Qwen/Qwen3.5-72B-Instruct"], default: "zai-org/GLM-5.2", apiKeyEnv: "HF_TOKEN" })), |
| 1328 | mockPreset("nvidia", "NVIDIA NIM", "NVIDIA NIM OpenAI-compatible accelerated inference endpoint.", "NVIDIA_API_KEY", mockProviderTemplate({ name: "nvidia", kind: "openai", baseUrl: "https://integrate.api.nvidia.com/v1", models: ["nvidia/nemotron-3-nano-30b-a3b", "nvidia/nemotron-3-super-120b-a12b", "nvidia/nemotron-3-ultra-550b-a55b", "deepseek-ai/deepseek-v4-pro", "qwen/qwen3.5-397b-a17b"], default: "nvidia/nemotron-3-nano-30b-a3b", apiKeyEnv: "NVIDIA_API_KEY" })), |
| 1329 | mockPreset("kilocode", "KiloCode", "Kilo Code gateway OpenAI-compatible endpoint.", "KILOCODE_API_KEY", mockProviderTemplate({ name: "kilocode", kind: "openai", baseUrl: "https://api.kilo.ai/api/gateway", models: ["kilo/auto"], default: "kilo/auto", apiKeyEnv: "KILOCODE_API_KEY" })), |
| 1330 | createMockModelScopePreset(mockProviderTemplate, mockPreset), |
| 1331 | mockPreset("ollama-cloud", "Ollama Cloud", "Hosted Ollama Cloud OpenAI-compatible endpoint with max reasoning effort.", "OLLAMA_API_KEY", mockProviderTemplate({ name: "ollama-cloud", kind: "openai", baseUrl: "https://ollama.com/v1", models: mockOllamaCloudModels, default: "glm-5.2", apiKeyEnv: "OLLAMA_API_KEY" })), |
| 1332 | mockPreset("scnet", "SCNet", "SCNet (National Supercomputing Internet) OpenAI-compatible token-plan API.", "SCNET_API_KEY", mockProviderTemplate({ name: "scnet", kind: "openai", baseUrl: "https://api.scnet.cn/api/llm/v1", modelsUrl: "https://api.scnet.cn/api/llm/v1/models", models: ["GLM-5.2", "GLM-5", "GLM-5.1", "Kimi-K3", "Kimi-K2.7-Code", "Kimi-K2.6", "Kimi-K2.5", "DeepSeek-V4-Flash", "DeepSeek-V3.2", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "MiMo-V2.5-Pro"], visionModels: ["Kimi-K2.6", "Kimi-K2.5"], default: "MiniMax-M2.5", apiKeyEnv: "SCNET_API_KEY", modelOverrides: [{ model: "DeepSeek-V4-Flash", reasoningProtocol: "openai", supportedEfforts: ["high", "max"], defaultEffort: "high" }] })), |
| 1333 | mockPreset("scnet-anthropic", "SCNet Anthropic", "SCNet (National Supercomputing Internet) Anthropic-compatible token-plan endpoint with Bearer auth.", "SCNET_API_KEY", mockProviderTemplate({ name: "scnet-anthropic", kind: "anthropic", baseUrl: "https://api.scnet.cn/api/llm/anthropic", models: ["GLM-5.2", "GLM-5", "GLM-5.1", "Kimi-K3", "Kimi-K2.7-Code", "Kimi-K2.6", "Kimi-K2.5", "DeepSeek-V4-Flash", "DeepSeek-V3.2", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "MiMo-V2.5-Pro"], visionModels: ["Kimi-K2.6", "Kimi-K2.5"], default: "MiniMax-M2.5", apiKeyEnv: "SCNET_API_KEY", authHeader: true })), |
| 1334 | ]; |
| 1335 | |
| 1336 | let mockCatalogLoad: Promise<void> | undefined; |
| 1337 | let mockCatalogs: Record<string, NonNullable<ProviderPresetView["catalog"]>> = {}; |
| 1338 | function loadMockProviderCatalog(): Promise<void> { |
| 1339 | return mockCatalogLoad ??= import("./providerCatalog.generated.json").then(({default: data}) => { |
| 1340 | mockCatalogs = data.catalogs; |
| 1341 | for (const template of data.templates) { |
| 1342 | if (!mockProviderPresetTemplates.some(p => p.id === template.id)) { |
| 1343 | mockProviderPresetTemplates.push(mockPreset(template.id, template.label, template.description, template.keyEnv, mockProviderTemplate(template.provider))); |
| 1344 | } |
| 1345 | } |
| 1346 | }); |
| 1347 | } |
| 1348 | |
| 1349 | function mockProviderPresetViews(): ProviderPresetView[] { |
| 1350 | return [...mockProviderPresetTemplates].sort((a, b) => mockProviderPresetDisplayRank(a.id) - mockProviderPresetDisplayRank(b.id)).map((template) => ({ |
| 1351 | catalog: mockCatalogs[template.id], |
| 1352 | id: template.id, |
| 1353 | label: template.label, |
| 1354 | description: template.description, |
| 1355 | keyEnv: template.keyEnv, |
| 1356 | recommended: Boolean(template.recommended), |
| 1357 | billingMode: template.billingMode, |
| 1358 | displayGroup: template.displayGroup, |
| 1359 | displaySection: template.displaySection, |
| 1360 | displayTier: template.displayTier, |
| 1361 | routeKind: template.routeKind, |
| 1362 | optional: Boolean(template.optional), |
| 1363 | displayOrder: template.displayOrder, |
| 1364 | providerNames: [...(template.providers ?? [template.provider]).map((provider) => provider.name)], |
| 1365 | models: [...(template.providers ?? [template.provider]).flatMap((provider) => provider.models)], |
| 1366 | added: false, |
| 1367 | status: "available", |
| 1368 | statusProviderNames: [], |
| 1369 | keySet: false, |
| 1370 | requiresKey: true, |
| 1371 | configured: false, |
| 1372 | })); |
| 1373 | } |
| 1374 | |
| 1375 | function mockProviderPresetDisplayRank(id: string): number { |
| 1376 | if (id === "opencode-go-recommended") return -3; |
| 1377 | if (id === "deepseek-responses") return -2; |
| 1378 | if (id === "glm-cn" || id === "zai-global" || id.startsWith("glm-coding-plan-") || id.startsWith("zai-coding-plan-")) return 0; |
| 1379 | if (id.startsWith("longcat-")) return 1; |
| 1380 | if (id === "token-rhythm") return 1; |
| 1381 | if (id.startsWith("kimi-")) return 2; |
| 1382 | if (id.startsWith("minimax-")) return 3; |
| 1383 | return 4; |
| 1384 | } |
| 1385 | |
| 1386 | function cloneMockProviderTemplates(id: string, key: string): ProviderView[] | undefined { |
| 1387 | const template = mockProviderPresetTemplates.find((candidate) => candidate.id === id); |
| 1388 | if (!template) return undefined; |
| 1389 | return (template.providers ?? [template.provider]).map((provider) => ({ |
| 1390 | ...JSON.parse(JSON.stringify(provider)) as ProviderView, |
| 1391 | keySet: Boolean(key.trim()), |
| 1392 | })); |
| 1393 | } |
| 1394 | |
| 1395 | function mockExternalOpenerIconDataURL(color: string, label: string): string { |
| 1396 | const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="${color}"/><text x="32" y="40" text-anchor="middle" font-family="system-ui" font-size="25" font-weight="700" fill="white">${label}</text></svg>`; |
| 1397 | return `data:image/svg+xml,${encodeURIComponent(svg)}`; |
| 1398 | } |
| 1399 | function makeMockApp(): MockAppBindings { |
| 1400 | const credentialDiagnostics = (actions: string[] = []): CredentialDiagnosticReport => ({ |
| 1401 | home: "/mock/.reasonix", |
| 1402 | credentialPath: "/mock/.reasonix/.env", |
| 1403 | pendingTransactions: 0, |
| 1404 | checks: [], |
| 1405 | actions, |
| 1406 | }); |
| 1407 | const scenario = mockScenario(); |
| 1408 | // Both bridge families publish into the same catalog, as ListTabs does in |
| 1409 | // the desktop backend. A remote event is not a second source of tab state. |
| 1410 | const remoteProjects = createMockRemoteProjects({ |
| 1411 | get: id => { const tab = mockTabs.find(item => item.id === id); return tab && { ...tab }; }, |
| 1412 | publish: tab => { |
| 1413 | const existing = mockTabs.some(item => item.id === tab.id); |
| 1414 | mockTabs = mockTabs.map(item => item.id === tab.id ? { ...tab } : tab.active ? { ...item, active: false } : item); |
| 1415 | if (!existing) mockTabs.push({ ...tab }); |
| 1416 | }, |
| 1417 | remove: id => { |
| 1418 | if (!mockTabs.some(tab => tab.id === id)) return; |
| 1419 | if (mockTabs.length === 1) throw new Error("cannot close the last tab"); |
| 1420 | const index = mockTabs.findIndex(tab => tab.id === id), active = mockTabs[index].active; |
| 1421 | mockTabs = mockTabs.filter(tab => tab.id !== id); |
| 1422 | if (active) setMockActiveTab(mockTabs[Math.min(index, mockTabs.length - 1)].id); |
| 1423 | }, |
| 1424 | }); |
| 1425 | const freshMock = scenario === "fresh"; |
| 1426 | const guidanceMock = scenario === "guidance", recoveryMock = typeof import.meta.env !== "undefined" && import.meta.env.DEV && scenario === "recovery"; |
| 1427 | const runningMock = scenario === "running" || guidanceMock; |
| 1428 | const sandboxEscapeMock = scenario === "sandbox_escape"; |
| 1429 | const noticePreviewMock = scenario === "notice"; |
| 1430 | const deepSeekUpgradeMock = scenario === "deepseek_upgrade"; |
| 1431 | const benchMock = scenario === "bench"; |
| 1432 | const mockDrafts = new Map<string, SessionDraftView>(); |
| 1433 | const mockDraftOperations = new Map<string, SessionDraftSubmissionView>(); |
| 1434 | const mockDraftForTarget = (scope: string, workspaceRoot: string): SessionDraftView => { |
| 1435 | const normalizedScope = scope === "project" && workspaceRoot ? "project" : "global"; |
| 1436 | const normalizedRoot = normalizedScope === "project" ? workspaceRoot : ""; |
| 1437 | const workspaceId = normalizedScope === "global" ? "global" : `project-${normalizedRoot}`; |
| 1438 | const prior = [...mockDrafts.values()].find((draft) => draft.workspaceId === workspaceId && draft.status === "active"); |
| 1439 | if (prior) return prior; |
| 1440 | const now = Date.now(); |
| 1441 | const draft: SessionDraftView = { |
| 1442 | id: `draft-mock-${mockDrafts.size + 1}`, |
| 1443 | workspaceId, |
| 1444 | scope: normalizedScope, |
| 1445 | workspaceRoot: normalizedRoot, |
| 1446 | revision: 1, |
| 1447 | contentJson: "{}", |
| 1448 | settings: { model: "deepseek/deepseek-v4-flash", modelSource: "default", mode: "default", toolApprovalMode: "ask", disabledMcp: {}, mcpOrder: [] }, |
| 1449 | status: "active", |
| 1450 | updatedAt: now, |
| 1451 | }; |
| 1452 | mockDrafts.set(draft.id, draft); |
| 1453 | return draft; |
| 1454 | }; |
| 1455 | let cancelled = false; |
| 1456 | let pendingAskPreview = false, pendingApprovalPreview = false; |
| 1457 | // Mirrors the last emitted approval preview so mode switches can mirror the |
| 1458 | // backend drain contract: only non-fresh tools auto-allow; plan/sandbox |
| 1459 | // escape prompts stay pending and visible. |
| 1460 | let pendingApprovalPreviewPrompt: { id: string; tool: string } | undefined; |
| 1461 | const globalWorkspaceRoot = "~/Library/Application Support/reasonix/global-workspace"; |
| 1462 | let cwd = freshMock ? globalWorkspaceRoot : "~/projects/joyquant-db"; // mutable so PickWorkspace is visible in dev |
| 1463 | let workspaces = freshMock ? [] : ["~/projects/joyquant-db", "~/projects/joyquant-sys", "~/projects/reasonix", "~/projects/blade"]; |
| 1464 | let mockEffort = "auto"; |
| 1465 | let mockDesktopZoomFactor = 1.0; |
| 1466 | let mockActiveThemeId = ""; |
| 1467 | let mockBaseStyle = "graphite"; |
| 1468 | let mockThemeMode: "auto" | "light" | "dark" = "dark"; |
| 1469 | let mockHeartbeatRevision = 0; |
| 1470 | let mockHeartbeatTasks: unknown[] = []; |
| 1471 | // Vite rewrites these literal asset URLs in both dev and production builds. |
| 1472 | // Keeping them on the browser mock makes local visual acceptance match the |
| 1473 | // desktop bridge, whose ListThemePacks response carries the same two URLs. |
| 1474 | const mockOfficialThemeAssets = { |
| 1475 | "official-rose-dawn": { |
| 1476 | previewUrl: new URL("../../../themes/official/official-rose-dawn/preview.webp", import.meta.url).href, |
| 1477 | backgroundUrl: new URL("../../../themes/official/official-rose-dawn/background.webp", import.meta.url).href, |
| 1478 | }, |
| 1479 | "official-fortune-forge": { |
| 1480 | previewUrl: new URL("../../../themes/official/official-fortune-forge/preview.webp", import.meta.url).href, |
| 1481 | backgroundUrl: new URL("../../../themes/official/official-fortune-forge/background.webp", import.meta.url).href, |
| 1482 | }, |
| 1483 | "official-crimson-horizon": { |
| 1484 | previewUrl: new URL("../../../themes/official/official-crimson-horizon/preview.webp", import.meta.url).href, |
| 1485 | backgroundUrl: new URL("../../../themes/official/official-crimson-horizon/background.webp", import.meta.url).href, |
| 1486 | }, |
| 1487 | "official-sage-breeze": { |
| 1488 | previewUrl: new URL("../../../themes/official/official-sage-breeze/preview.webp", import.meta.url).href, |
| 1489 | backgroundUrl: new URL("../../../themes/official/official-sage-breeze/background.webp", import.meta.url).href, |
| 1490 | }, |
| 1491 | "official-spark-notebook": { |
| 1492 | previewUrl: new URL("../../../themes/official/official-spark-notebook/preview.webp", import.meta.url).href, |
| 1493 | backgroundUrl: new URL("../../../themes/official/official-spark-notebook/background.webp", import.meta.url).href, |
| 1494 | }, |
| 1495 | "official-violet-starlight": { |
| 1496 | previewUrl: new URL("../../../themes/official/official-violet-starlight/preview.webp", import.meta.url).href, |
| 1497 | backgroundUrl: new URL("../../../themes/official/official-violet-starlight/background.webp", import.meta.url).href, |
| 1498 | }, |
| 1499 | "official-cyan-stage": { |
| 1500 | previewUrl: new URL("../../../themes/official/official-cyan-stage/preview.webp", import.meta.url).href, |
| 1501 | backgroundUrl: new URL("../../../themes/official/official-cyan-stage/background.webp", import.meta.url).href, |
| 1502 | }, |
| 1503 | "official-noir-gold": { |
| 1504 | previewUrl: new URL("../../../themes/official/official-noir-gold/preview.webp", import.meta.url).href, |
| 1505 | backgroundUrl: new URL("../../../themes/official/official-noir-gold/background.webp", import.meta.url).href, |
| 1506 | }, |
| 1507 | } as const; |
| 1508 | registerTrustedThemeBackgroundURLs(Object.values(mockOfficialThemeAssets).map((asset) => asset.backgroundUrl)); |
| 1509 | let mockThemePacks: import("./themePack").ThemePackView[] = [ |
| 1510 | { id: "graphite", name: "Graphite", author: "Reasonix", baseStyle: "graphite", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } }, |
| 1511 | { id: "aurora", name: "Aurora", author: "Reasonix", baseStyle: "aurora", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } }, |
| 1512 | { id: "slate", name: "Slate", author: "Reasonix", baseStyle: "slate", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } }, |
| 1513 | { id: "carbon", name: "Carbon", author: "Reasonix", baseStyle: "carbon", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } }, |
| 1514 | { id: "nocturne", name: "Nocturne", author: "Reasonix", baseStyle: "nocturne", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } }, |
| 1515 | { id: "amber", name: "Amber", author: "Reasonix", baseStyle: "amber", builtin: true, kind: "base", active: false, hasBackground: false, tokens: {}, recipes: { density: "comfortable", corners: "soft" } }, |
| 1516 | { ...mockOfficialThemeAssets["official-rose-dawn"], id: "official-rose-dawn", name: "Rose Dawn", author: "Reasonix Contributors", license: "MIT", baseStyle: "graphite", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-rose-dawn.name", descriptionKey: "settings.themes.official.official-rose-dawn.description", tokens: { light: { bg: "#FFF7F8", fg: "#3A252C", accent: "#B43F65" }, dark: { bg: "#1E1419", fg: "#FFF3F6", accent: "#E26D91" } }, recipes: { density: "comfortable", corners: "round" }, background: { focusX: 0.72, focusY: 0.43, safeArea: "left", homeOpacity: 1, taskOpacity: 0.2, overlayStrength: 0.68, paneOpacity: 0.50 } }, |
| 1517 | { ...mockOfficialThemeAssets["official-fortune-forge"], id: "official-fortune-forge", name: "Fortune Forge", author: "Reasonix Contributors", license: "MIT", baseStyle: "amber", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-fortune-forge.name", descriptionKey: "settings.themes.official.official-fortune-forge.description", tokens: { light: { bg: "#FFF8E8", fg: "#382116", accent: "#A92D22" }, dark: { bg: "#1D140D", fg: "#FFF2D1", accent: "#E8AD38" } }, recipes: { density: "comfortable", corners: "soft" }, background: { focusX: 0.74, focusY: 0.44, safeArea: "left", homeOpacity: 1, taskOpacity: 0.2, overlayStrength: 0.7, paneOpacity: 0.50 } }, |
| 1518 | { ...mockOfficialThemeAssets["official-crimson-horizon"], id: "official-crimson-horizon", name: "Crimson Horizon", author: "Reasonix Contributors", license: "MIT", baseStyle: "graphite", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-crimson-horizon.name", descriptionKey: "settings.themes.official.official-crimson-horizon.description", tokens: { light: { bg: "#FFF8F7", fg: "#301D1D", accent: "#B92B38" }, dark: { bg: "#190D11", fg: "#FFF1F2", accent: "#FF6772" } }, recipes: { density: "comfortable", corners: "soft" }, background: { focusX: 0.75, focusY: 0.45, safeArea: "left", homeOpacity: 0.98, taskOpacity: 0.22, overlayStrength: 0.66, paneOpacity: 0.50 } }, |
| 1519 | { ...mockOfficialThemeAssets["official-sage-breeze"], id: "official-sage-breeze", name: "Sage Breeze", author: "Reasonix Contributors", license: "MIT", baseStyle: "slate", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-sage-breeze.name", descriptionKey: "settings.themes.official.official-sage-breeze.description", tokens: { light: { bg: "#F7F7EF", fg: "#26332D", accent: "#47735F" }, dark: { bg: "#101814", fg: "#EEF6F0", accent: "#84CBA7" } }, recipes: { density: "comfortable", corners: "soft" }, background: { focusX: 0.73, focusY: 0.44, safeArea: "left", homeOpacity: 1, taskOpacity: 0.2, overlayStrength: 0.68, paneOpacity: 0.50 } }, |
| 1520 | { ...mockOfficialThemeAssets["official-spark-notebook"], id: "official-spark-notebook", name: "Spark Notebook", author: "Reasonix Contributors", license: "MIT", baseStyle: "aurora", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-spark-notebook.name", descriptionKey: "settings.themes.official.official-spark-notebook.description", tokens: { light: { bg: "#FFF9ED", fg: "#2B2F35", accent: "#007B78" }, dark: { bg: "#14171A", fg: "#F8F5E9", accent: "#42D1C6" } }, recipes: { density: "comfortable", corners: "round" }, background: { focusX: 0.74, focusY: 0.46, safeArea: "left", homeOpacity: 0.98, taskOpacity: 0.2, overlayStrength: 0.68, paneOpacity: 0.50 } }, |
| 1521 | { ...mockOfficialThemeAssets["official-violet-starlight"], id: "official-violet-starlight", name: "Violet Starlight", author: "Reasonix Contributors", license: "MIT", baseStyle: "nocturne", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-violet-starlight.name", descriptionKey: "settings.themes.official.official-violet-starlight.description", tokens: { light: { bg: "#F7F4FF", fg: "#251F3C", accent: "#6242C7" }, dark: { bg: "#0C1022", fg: "#F4F2FF", accent: "#9B86FF" } }, recipes: { density: "comfortable", corners: "round" }, background: { focusX: 0.73, focusY: 0.44, safeArea: "left", homeOpacity: 0.96, taskOpacity: 0.18, overlayStrength: 0.72, paneOpacity: 0.50 } }, |
| 1522 | { ...mockOfficialThemeAssets["official-cyan-stage"], id: "official-cyan-stage", name: "Cyan Stage", author: "Reasonix Contributors", license: "MIT", baseStyle: "carbon", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-cyan-stage.name", descriptionKey: "settings.themes.official.official-cyan-stage.description", tokens: { light: { bg: "#F1FCFD", fg: "#173238", accent: "#007C92" }, dark: { bg: "#07181D", fg: "#E9FCFF", accent: "#37D7E4" } }, recipes: { density: "comfortable", corners: "round" }, background: { focusX: 0.74, focusY: 0.45, safeArea: "left", homeOpacity: 0.96, taskOpacity: 0.18, overlayStrength: 0.72, paneOpacity: 0.50 } }, |
| 1523 | { ...mockOfficialThemeAssets["official-noir-gold"], id: "official-noir-gold", name: "Noir Gold", author: "Reasonix Contributors", license: "MIT", baseStyle: "carbon", builtin: true, kind: "official", active: false, hasBackground: true, nameKey: "settings.themes.official.official-noir-gold.name", descriptionKey: "settings.themes.official.official-noir-gold.description", tokens: { light: { bg: "#FCF8EE", fg: "#2A241B", accent: "#7A5A16" }, dark: { bg: "#0D0B09", fg: "#F8F1DF", accent: "#D9B45B" } }, recipes: { density: "comfortable", corners: "soft" }, background: { focusX: 0.73, focusY: 0.43, safeArea: "left", homeOpacity: 0.94, taskOpacity: 0.18, overlayStrength: 0.74, paneOpacity: 0.50 } }, |
| 1524 | ]; |
| 1525 | const day = 86_400_000; |
| 1526 | const t0 = Date.now(); |
| 1527 | // Mutable so MCP add/remove/retry are observable in browser dev. |
| 1528 | let capServers: ServerView[] = [ |
| 1529 | { |
| 1530 | name: "project-knowledge", |
| 1531 | transport: "http", |
| 1532 | status: "connected", |
| 1533 | configured: true, |
| 1534 | autoStart: true, |
| 1535 | tier: "background", |
| 1536 | source: "project", |
| 1537 | configSource: "reasonix.toml", |
| 1538 | url: "https://mcp.example.test/project", |
| 1539 | tools: 3, |
| 1540 | prompts: 0, |
| 1541 | resources: 1, |
| 1542 | toolList: [ |
| 1543 | { name: "search_knowledge", description: "Search the project knowledge base.", readOnlyHint: true }, |
| 1544 | { name: "get_document", description: "Read a knowledge-base document.", readOnlyHint: true }, |
| 1545 | { name: "list_topics", description: "List available knowledge topics.", readOnlyHint: true }, |
| 1546 | ], |
| 1547 | }, |
| 1548 | { |
| 1549 | name: "github", |
| 1550 | transport: "stdio", |
| 1551 | status: "connected", |
| 1552 | configured: true, |
| 1553 | autoStart: true, |
| 1554 | tier: "background", |
| 1555 | command: "npx", |
| 1556 | args: ["-y", "@modelcontextprotocol/server-github"], |
| 1557 | tools: 4, |
| 1558 | prompts: 2, |
| 1559 | resources: 0, |
| 1560 | toolList: [ |
| 1561 | { name: "issue_read", description: "Read GitHub issue details and comments.", readOnlyHint: true }, |
| 1562 | { name: "pull_request_read", description: "Read pull request metadata, files, and review threads.", readOnlyHint: true }, |
| 1563 | { name: "search_issues", description: "Search issues and pull requests.", readOnlyHint: true }, |
| 1564 | { name: "issue_write", description: "Create or update GitHub issues." }, |
| 1565 | ], |
| 1566 | }, |
| 1567 | { |
| 1568 | name: "linear", |
| 1569 | transport: "http", |
| 1570 | status: "initializing", |
| 1571 | configured: true, |
| 1572 | autoStart: true, |
| 1573 | tier: "background", |
| 1574 | url: "https://mcp.linear.app/mcp", |
| 1575 | authStatus: "possible", |
| 1576 | authUrl: "https://mcp.linear.app/mcp", |
| 1577 | tools: 8, |
| 1578 | prompts: 0, |
| 1579 | resources: 0, |
| 1580 | toolList: [ |
| 1581 | { name: "list_issues", description: "List and filter Linear issues." }, |
| 1582 | { name: "get_issue", description: "Fetch a Linear issue by id or key." }, |
| 1583 | { name: "create_issue", description: "Create a Linear issue." }, |
| 1584 | { name: "update_issue", description: "Update status, assignee, priority, or labels." }, |
| 1585 | { name: "list_projects", description: "List Linear projects." }, |
| 1586 | { name: "get_project", description: "Fetch project details." }, |
| 1587 | { name: "list_teams", description: "List Linear teams." }, |
| 1588 | { name: "search", description: "Search Linear workspace objects." }, |
| 1589 | ], |
| 1590 | }, |
| 1591 | { name: "figma", transport: "http", status: "failed", configured: true, autoStart: true, tier: "background", url: "https://mcp.figma.com/mcp", authStatus: "required", authUrl: "https://mcp.figma.com/mcp", tools: 0, prompts: 0, resources: 0, error: "connect: 401 unauthorized" }, |
| 1592 | ]; |
| 1593 | const capSkills: SkillView[] = [ |
| 1594 | { |
| 1595 | name: "explore", description: "Investigate the codebase in an isolated subagent", scope: "builtin", runAs: "subagent", enabled: true, |
| 1596 | allowedTools: ["read_file", "ls", "glob", "grep", "code_index"], invocation: "/explore", invocationMode: "auto", |
| 1597 | configuredModel: "deepseek/deepseek-v4-pro", configuredEffort: "high", |
| 1598 | }, |
| 1599 | { name: "research", description: "Combine web_fetch + code reading in an isolated subagent", scope: "builtin", runAs: "subagent", enabled: true, allowedTools: ["read_file", "ls", "glob", "grep", "code_index", "web_fetch"], invocation: "/research", invocationMode: "auto" }, |
| 1600 | { name: "review", description: "Review the staged diff", scope: "project", sourceDir: "~/projects/reasonix/.reasonix/skills", runAs: "inline", enabled: false, invocation: "/review" }, |
| 1601 | { name: "init", description: "Scaffold a REASONIX.md for this repo", scope: "builtin", runAs: "inline", enabled: true, invocation: "/init" }, |
| 1602 | { |
| 1603 | name: "my-formatter", description: "Formats code the way I like it", scope: "global", sourceDir: "~/.reasonix/skills", runAs: "subagent", enabled: true, |
| 1604 | model: "deepseek-pro", effort: "high", allowedTools: ["read_file", "edit_file"], color: "amber", invocation: "/my-formatter", invocationMode: "manual", |
| 1605 | body: "You are a code formatting assistant. Reformat the given file to match project style without changing behavior.", |
| 1606 | }, |
| 1607 | ]; |
| 1608 | let capSkillRoots: SkillRootView[] = [ |
| 1609 | { dir: "~/projects/reasonix/.reasonix/skills", scope: "project", priority: 1, status: "missing", enabled: true, configured: false, removable: true, skills: 0 }, |
| 1610 | { |
| 1611 | dir: "~/my-skills", |
| 1612 | scope: "custom", |
| 1613 | priority: 5, |
| 1614 | status: "ok", |
| 1615 | enabled: true, |
| 1616 | configured: true, |
| 1617 | removable: true, |
| 1618 | skills: 1, |
| 1619 | skillItems: [{ name: "review", description: "Review the staged diff", scope: "custom", runAs: "inline" }], |
| 1620 | }, |
| 1621 | { |
| 1622 | dir: "~/.reasonix/skills", |
| 1623 | scope: "global", |
| 1624 | priority: 6, |
| 1625 | status: "ok", |
| 1626 | enabled: true, |
| 1627 | configured: false, |
| 1628 | removable: true, |
| 1629 | skills: 2, |
| 1630 | skillItems: [ |
| 1631 | { name: "explore", description: "Investigate the codebase in an isolated subagent", scope: "global", runAs: "subagent" }, |
| 1632 | { name: "init", description: "Scaffold a REASONIX.md for this repo", scope: "global", runAs: "inline" }, |
| 1633 | ], |
| 1634 | }, |
| 1635 | ]; |
| 1636 | let capPlugins: PluginView[] = []; |
| 1637 | const mockSwitchWorkspace = async (path: string) => { |
| 1638 | cwd = path || "~"; |
| 1639 | workspaces = [cwd, ...workspaces.filter((p) => p !== cwd)].slice(0, 12); |
| 1640 | if (!mockProjectTree.some((node) => node.kind === "project" && node.root === cwd)) { |
| 1641 | mockProjectTree.unshift({ |
| 1642 | key: `project_${cwd}`, |
| 1643 | kind: "project", |
| 1644 | label: baseName(cwd), |
| 1645 | root: cwd, |
| 1646 | children: [], |
| 1647 | }); |
| 1648 | } |
| 1649 | return cwd; |
| 1650 | }; |
| 1651 | // Mutable so delete/rename are observable in browser dev. |
| 1652 | const sessions: SessionMeta[] = [ |
| 1653 | { path: "/mock/sessions/a.jsonl", preview: "fix the login bug in auth.go", turns: 12, createdAt: t0 - 2 * day, lastActivityAt: t0 - 3_600_000, modTime: t0 - 3_600_000, current: true, open: true }, |
| 1654 | { path: "/mock/sessions/b-recovery-0123456789abcdef.jsonl", preview: "refactor the payment module", turns: 5, createdAt: t0 - 3 * day, lastActivityAt: t0 - 6 * 3_600_000, modTime: t0 - 6 * 3_600_000, current: false, open: true, recovered: true, recoveryCopy: true }, |
| 1655 | { path: "/mock/sessions/c.jsonl", preview: "write the README and badges", turns: 8, createdAt: t0 - 4 * day, lastActivityAt: t0 - day - 3_600_000, modTime: t0 - day - 3_600_000, current: false, open: false }, |
| 1656 | { path: "/mock/sessions/d.jsonl", preview: "explain the plugin host design", turns: 3, createdAt: t0 - 5 * day, lastActivityAt: t0 - 4 * day, modTime: t0 - 4 * day, current: false, open: false }, |
| 1657 | ]; |
| 1658 | const trashedSessions: SessionMeta[] = [ |
| 1659 | { |
| 1660 | path: "/mock/sessions/.trash/trash-dev-standard.jsonl", |
| 1661 | title: t("mock.trashDevStandardTitle"), |
| 1662 | preview: t("mock.trashDevStandardPreview"), |
| 1663 | turns: 4, |
| 1664 | createdAt: t0 - 8 * day, |
| 1665 | lastActivityAt: t0 - 7 * day, |
| 1666 | modTime: t0 - 7 * day, |
| 1667 | deletedAt: t0 - 20 * 60_000, |
| 1668 | current: false, |
| 1669 | open: false, |
| 1670 | scope: "project", |
| 1671 | workspaceRoot: "~/projects/joyquant-db", |
| 1672 | topicId: "topic_dev_standard", |
| 1673 | topicTitle: t("mock.trashDevStandardTitle"), |
| 1674 | }, |
| 1675 | { |
| 1676 | path: "/mock/sessions/.trash/trash-p3a-review.jsonl", |
| 1677 | title: t("mock.trashP3aTitle"), |
| 1678 | preview: t("mock.trashP3aPreview"), |
| 1679 | turns: 7, |
| 1680 | createdAt: t0 - 6 * day, |
| 1681 | lastActivityAt: t0 - 5 * day, |
| 1682 | modTime: t0 - 5 * day, |
| 1683 | deletedAt: t0 - 2 * 3_600_000, |
| 1684 | current: false, |
| 1685 | open: false, |
| 1686 | scope: "project", |
| 1687 | workspaceRoot: "~/projects/joyquant-sys", |
| 1688 | topicId: "topic_p3a_pd", |
| 1689 | topicTitle: t("mock.trashP3aTitle"), |
| 1690 | }, |
| 1691 | { |
| 1692 | path: "/mock/sessions/.trash/trash-global-product.jsonl", |
| 1693 | title: t("mock.trashGlobalProductTitle"), |
| 1694 | preview: t("mock.trashGlobalProductPreview"), |
| 1695 | turns: 2, |
| 1696 | createdAt: t0 - 4 * day, |
| 1697 | lastActivityAt: t0 - 3 * day, |
| 1698 | modTime: t0 - 3 * day, |
| 1699 | deletedAt: t0 - day, |
| 1700 | current: false, |
| 1701 | open: false, |
| 1702 | scope: "global", |
| 1703 | topicId: "topic_product", |
| 1704 | topicTitle: t("mock.trashGlobalProductTitle"), |
| 1705 | recovered: true, |
| 1706 | recoveryCopy: true, |
| 1707 | }, |
| 1708 | ]; |
| 1709 | if (freshMock) { |
| 1710 | sessions.splice(0); |
| 1711 | trashedSessions.splice(0); |
| 1712 | } |
| 1713 | // Mutable settings so the Settings panel's edits are observable in browser dev. |
| 1714 | const settings: SettingsView = { |
| 1715 | modelSettingsFingerprint: "mock-model-settings-0", |
| 1716 | defaultModel: "deepseek", |
| 1717 | plannerModel: "", |
| 1718 | visionModel: "", |
| 1719 | webSearchModel: "auto", |
| 1720 | webSearchModels: ["deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro"], |
| 1721 | webSearchModelStatus: "ready", |
| 1722 | subagentModel: "", |
| 1723 | subagentEffort: "", |
| 1724 | autoPlan: "off", |
| 1725 | providers: [ |
| 1726 | { name: "deepseek", builtIn: true, added: deepSeekUpgradeMock, kind: "openai", baseUrl: "https://api.deepseek.com", modelsUrl: "", models: ["deepseek-v4-flash"], visionModels: [], visionModelsConfigured: false, default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", headers: deepSeekUpgradeMock ? { "X-Route": "official-custom" } : undefined, keySet: true, balanceUrl: "https://api.deepseek.com/user/balance", contextWindow: 1_000_000, reasoningProtocol: "", thinking: "enabled", webSearch: true, serverWebSearchCapability: true, supportedEfforts: ["disabled", "low", "high", "max"], defaultEffort: "high", recommendedUpgradeAvailable: false }, |
| 1727 | ], |
| 1728 | officialProviders: [ |
| 1729 | { name: "deepseek", builtIn: true, added: false, kind: "openai", baseUrl: "https://api.deepseek.com", modelsUrl: "", models: ["deepseek-v4-flash", "deepseek-v4-pro"], visionModels: [], visionModelsConfigured: false, default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", keySet: true, balanceUrl: "https://api.deepseek.com/user/balance", contextWindow: 1_000_000, reasoningProtocol: "", thinking: "enabled", webSearch: true, serverWebSearchCapability: true, supportedEfforts: ["disabled", "low", "high", "max"], defaultEffort: "high" }, |
| 1730 | ], |
| 1731 | providerPresets: mockProviderPresetViews(), |
| 1732 | permissions: { mode: "ask", allow: ["ls", "read_file"], ask: [], deny: ["Bash(rm:*)"] }, |
| 1733 | sandbox: { bash: browserPreviewBashSandboxMode(), network: true, workspaceRoot: "", allowWrite: [], effectiveWorkspaceRoot: cwd, effectiveWriteRoots: [cwd], shell: "auto", effectiveShell: browserPreviewEffectiveShell("auto"), resolvedShell: browserPreviewEffectiveShell("auto"), shellReloadRequired: false, ...browserPreviewShellSupport(browserPlatformOverride()) }, |
| 1734 | network: { |
| 1735 | proxyMode: "auto", |
| 1736 | proxyUrl: "", |
| 1737 | noProxy: "", |
| 1738 | proxy: { type: "socks5", server: "127.0.0.1", port: 7890, username: "", password: "" }, |
| 1739 | }, |
| 1740 | agent: { temperature: 0.2, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, maxSubagentConcurrency: 6, maxParallelWriters: 3, systemPrompt: "You are Reasonix, a coding agent.", reasoningLanguage: "auto", compactRatio: 0.8 }, |
| 1741 | bot: { |
| 1742 | enabled: !freshMock, |
| 1743 | model: "", |
| 1744 | toolApprovalMode: "ask", |
| 1745 | maxSteps: 0, |
| 1746 | debounceMs: 1500, |
| 1747 | queueMode: "steer", |
| 1748 | queueCap: 20, |
| 1749 | queueDrop: "summarize", |
| 1750 | ignoreSelfMessages: true, |
| 1751 | selfUserIds: { |
| 1752 | qq: [], |
| 1753 | feishu: [], |
| 1754 | weixin: [], |
| 1755 | dingtalk: [], |
| 1756 | }, |
| 1757 | control: { |
| 1758 | enabled: false, |
| 1759 | addr: "127.0.0.1:37913", |
| 1760 | tokenEnv: "REASONIX_BOT_CONTROL_TOKEN", |
| 1761 | }, |
| 1762 | pairing: { |
| 1763 | enabled: true, |
| 1764 | requestTtlMinutes: 60, |
| 1765 | maxPendingPerPlatform: 3, |
| 1766 | }, |
| 1767 | routes: [], |
| 1768 | allowlist: { |
| 1769 | enabled: true, |
| 1770 | allowAll: false, |
| 1771 | qqUsers: [], |
| 1772 | feishuUsers: freshMock ? [] : ["ou_mock_user_001"], |
| 1773 | weixinUsers: freshMock ? [] : ["wxid_mock_user_001"], |
| 1774 | qqApprovers: [], |
| 1775 | feishuApprovers: [], |
| 1776 | weixinApprovers: [], |
| 1777 | qqAdmins: [], |
| 1778 | feishuAdmins: [], |
| 1779 | weixinAdmins: [], |
| 1780 | qqGroups: [], |
| 1781 | feishuGroups: [], |
| 1782 | weixinGroups: [], |
| 1783 | dingtalkUsers: [], |
| 1784 | dingtalkApprovers: [], |
| 1785 | dingtalkAdmins: [], |
| 1786 | dingtalkGroups: [], |
| 1787 | }, |
| 1788 | qq: { enabled: false, appId: "", appSecretEnv: "QQ_BOT_APP_SECRET", secretSet: false, sandbox: false, model: "", toolApprovalMode: "ask", workspaceRoot: "", access: { enabled: true, allowAll: false, pairingEnabled: true, users: [], groups: [], approvers: [], admins: [] } }, |
| 1789 | feishu: { |
| 1790 | enabled: false, |
| 1791 | domain: "feishu", |
| 1792 | appId: "", |
| 1793 | appSecretEnv: "FEISHU_BOT_APP_SECRET", |
| 1794 | secretSet: false, |
| 1795 | verificationToken: "", |
| 1796 | mode: "webhook", |
| 1797 | webhookPort: 8080, |
| 1798 | requireMention: true, |
| 1799 | }, |
| 1800 | weixin: { |
| 1801 | enabled: false, |
| 1802 | accountId: "default", |
| 1803 | tokenEnv: "WEIXIN_BOT_TOKEN", |
| 1804 | tokenSet: false, |
| 1805 | apiBase: "https://ilinkai.weixin.qq.com", |
| 1806 | }, |
| 1807 | dingtalk: { |
| 1808 | enabled: false, |
| 1809 | clientId: "", |
| 1810 | clientSecretEnv: "DINGTALK_CLIENT_SECRET", |
| 1811 | secretSet: false, |
| 1812 | botName: "", |
| 1813 | requireMention: true, |
| 1814 | model: "", |
| 1815 | toolApprovalMode: "", |
| 1816 | workspaceRoot: "", |
| 1817 | access: { enabled: true, allowAll: false, pairingEnabled: true, users: [], groups: [], approvers: [], admins: [] }, |
| 1818 | }, |
| 1819 | connections: freshMock ? [] : [ |
| 1820 | { |
| 1821 | id: "mock-lark-kun", |
| 1822 | provider: "feishu", |
| 1823 | domain: "lark", |
| 1824 | label: "kun", |
| 1825 | enabled: true, |
| 1826 | status: "connected", |
| 1827 | model: "", |
| 1828 | toolApprovalMode: "", |
| 1829 | workspaceRoot: "", |
| 1830 | access: { enabled: true, allowAll: false, pairingEnabled: true, users: ["ou_mock_user_001"], groups: [], approvers: [], admins: [] }, |
| 1831 | credential: { |
| 1832 | appId: "cli_mock_lark", |
| 1833 | appSecretEnv: "FEISHU_BOT_APP_SECRET", |
| 1834 | accountId: "", |
| 1835 | tokenEnv: "", |
| 1836 | secretSet: true, |
| 1837 | }, |
| 1838 | sessionMappings: [ |
| 1839 | { |
| 1840 | remoteId: "ou_mock_user_001", |
| 1841 | sessionId: "topic:topic_product", |
| 1842 | sessionSource: "", |
| 1843 | chatType: "", |
| 1844 | userId: "", |
| 1845 | threadId: "", |
| 1846 | scope: "global", |
| 1847 | workspaceRoot: "", |
| 1848 | updatedAt: new Date(Date.now() - 4 * 60_000).toISOString(), |
| 1849 | }, |
| 1850 | ], |
| 1851 | lastError: "", |
| 1852 | createdAt: new Date(Date.now() - 86_400_000).toISOString(), |
| 1853 | updatedAt: new Date(Date.now() - 4 * 60_000).toISOString(), |
| 1854 | }, |
| 1855 | { |
| 1856 | id: "mock-weixin-kun", |
| 1857 | provider: "weixin", |
| 1858 | domain: "weixin", |
| 1859 | label: "kun", |
| 1860 | enabled: true, |
| 1861 | status: "connected", |
| 1862 | model: "", |
| 1863 | toolApprovalMode: "", |
| 1864 | workspaceRoot: "", |
| 1865 | access: { enabled: true, allowAll: false, pairingEnabled: true, users: ["wxid_mock_user_001"], groups: [], approvers: [], admins: [] }, |
| 1866 | credential: { |
| 1867 | appId: "", |
| 1868 | appSecretEnv: "", |
| 1869 | accountId: "default", |
| 1870 | tokenEnv: "WEIXIN_BOT_TOKEN", |
| 1871 | secretSet: true, |
| 1872 | }, |
| 1873 | sessionMappings: [ |
| 1874 | { |
| 1875 | remoteId: "wxid_mock_user_001", |
| 1876 | sessionId: "topic:topic_ai", |
| 1877 | sessionSource: "", |
| 1878 | chatType: "", |
| 1879 | userId: "", |
| 1880 | threadId: "", |
| 1881 | scope: "global", |
| 1882 | workspaceRoot: "", |
| 1883 | updatedAt: new Date(Date.now() - 12 * 60_000).toISOString(), |
| 1884 | }, |
| 1885 | ], |
| 1886 | lastError: "", |
| 1887 | createdAt: new Date(Date.now() - 86_400_000).toISOString(), |
| 1888 | updatedAt: new Date(Date.now() - 12 * 60_000).toISOString(), |
| 1889 | }, |
| 1890 | ], |
| 1891 | }, |
| 1892 | desktopLanguage: "", |
| 1893 | desktopCurrency: "", |
| 1894 | desktopTheme: "auto", |
| 1895 | desktopThemeStyle: "graphite", |
| 1896 | desktopTerminalTheme: "auto", |
| 1897 | conversationWidth: "standard", |
| 1898 | closeBehavior: "background", |
| 1899 | displayMode: "standard", sessionExperience: "standard", reasoningDisplayMode: "auto", reasoningDisplayModeExplicit: false, |
| 1900 | statusBarStyle: "icon", |
| 1901 | statusBarItems: [...DEFAULT_STATUS_BAR_ITEMS], |
| 1902 | defaultToolApprovalMode: "workspace-write", |
| 1903 | checkUpdates: true, |
| 1904 | updaterEnabled: true, |
| 1905 | updateChannel: "stable", |
| 1906 | telemetry: true, |
| 1907 | metrics: true, |
| 1908 | configPath: "~/.reasonix/config.toml", |
| 1909 | shadowedByPath: "~/projects/reasonix/reasonix.toml", |
| 1910 | providerKinds: ["openai", "anthropic", "responses"], |
| 1911 | autoApproveTools: false, |
| 1912 | bypass: false, |
| 1913 | }; |
| 1914 | const hookEvents = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "PostLLMCall", "SessionStart", "SessionEnd", "SubagentStop", "Notification", "PreCompact"]; |
| 1915 | const hookSettings: Record<string, HooksSettingsView> = { |
| 1916 | global: { |
| 1917 | scope: "global", |
| 1918 | path: "~/.reasonix/settings.json", |
| 1919 | projectRoot: "", |
| 1920 | trusted: true, |
| 1921 | events: hookEvents, |
| 1922 | hooks: [ |
| 1923 | { event: "Stop", command: "echo turn done", description: "Notify after each turn" }, |
| 1924 | ], |
| 1925 | }, |
| 1926 | project: { |
| 1927 | scope: "project", |
| 1928 | path: "./.reasonix/settings.json", |
| 1929 | projectRoot: "/mock/project", |
| 1930 | trusted: false, |
| 1931 | events: hookEvents, |
| 1932 | hooks: [], |
| 1933 | }, |
| 1934 | }; |
| 1935 | settings.providers = settings.providers.map((provider) => |
| 1936 | provider.apiKeyEnv === "DEEPSEEK_API_KEY" ? { ...provider, keySet: !freshMock } : provider, |
| 1937 | ); |
| 1938 | if (freshMock) { |
| 1939 | settings.configPath = "~/.reasonix/config.toml"; |
| 1940 | settings.shadowedByPath = ""; |
| 1941 | } |
| 1942 | const mockNow = Date.now(); |
| 1943 | const mockProjectTree: ProjectNode[] = freshMock ? [] : benchMock ? [ |
| 1944 | { |
| 1945 | key: "project_~/projects/reasonix", |
| 1946 | kind: "project", |
| 1947 | label: "reasonix", |
| 1948 | root: "~/projects/reasonix", |
| 1949 | projectColor: "purple", |
| 1950 | children: [ |
| 1951 | { key: "topic_bench_markdown", kind: "topic", label: "● bench:markdown-46t", root: "~/projects/reasonix", topicId: "topic_bench_markdown", projectColor: "purple", turns: 46, lastActivityAt: mockNow - 60_000, open: true }, |
| 1952 | { key: "topic_bench_tools", kind: "topic", label: "● bench:tools-38t", root: "~/projects/reasonix", topicId: "topic_bench_tools", projectColor: "blue", turns: 38, lastActivityAt: mockNow - 120_000, open: true }, |
| 1953 | { key: "topic_bench_small", kind: "topic", label: "bench:small-6t", root: "~/projects/reasonix", topicId: "topic_bench_small", projectColor: "green", turns: 6, lastActivityAt: mockNow - 180_000 }, |
| 1954 | { key: "topic_bench_giant_turn", kind: "topic", label: "bench:giant-turn", root: "~/projects/reasonix", topicId: "topic_bench_giant_turn", projectColor: "amber", turns: 1, lastActivityAt: mockNow - 240_000 }, |
| 1955 | { key: "topic_bench_windowed", kind: "topic", label: "bench:windowed-1000t", root: "~/projects/reasonix", topicId: "topic_bench_windowed", projectColor: "blue", turns: 1000, lastActivityAt: mockNow - 270_000 }, |
| 1956 | { key: "topic_bench_reported_long_turn", kind: "topic", label: "bench:reported-long-turn", root: "~/projects/reasonix", topicId: "topic_bench_reported_long_turn", projectColor: "amber", turns: 1, lastActivityAt: mockNow - 300_000 }, |
| 1957 | { key: "topic_bench_geometry_contract", kind: "topic", label: "bench:geometry-blocks", root: "~/projects/reasonix", topicId: "topic_bench_geometry_contract", projectColor: "amber", turns: 1, lastActivityAt: mockNow - 330_000 }, |
| 1958 | { key: "topic_bench_storm", kind: "topic", label: "bench:storm-40t", root: "~/projects/reasonix", topicId: "topic_bench_storm", projectColor: "red", turns: 40, lastActivityAt: mockNow - 360_000 }, |
| 1959 | { key: "topic_bench_selection_table", kind: "topic", label: "bench:selection-table", root: "~/projects/reasonix", topicId: "topic_bench_selection_table" }, |
| 1960 | ], |
| 1961 | }, |
| 1962 | ] : [ |
| 1963 | { |
| 1964 | key: "project_~/projects/joyquant-db", |
| 1965 | kind: "project", |
| 1966 | label: t("mock.projectJoyquantDb"), |
| 1967 | root: "~/projects/joyquant-db", |
| 1968 | projectColor: "blue", |
| 1969 | children: [ |
| 1970 | { key: "topic_dev_standard", kind: "topic", label: `● ${t("mock.topicDevStandard")}`, root: "~/projects/joyquant-db", topicId: "topic_dev_standard", projectColor: "blue", turns: 18, createdAt: mockNow - 3 * 24 * 60 * 60_000, lastActivityAt: mockNow - 8 * 60_000, open: true, running: runningMock }, |
| 1971 | { key: "topic_db_maint", kind: "topic", label: t("mock.topicDbMaint"), root: "~/projects/joyquant-db", topicId: "topic_db_maint", projectColor: "blue", turns: 7, createdAt: mockNow - 2 * 24 * 60 * 60_000, lastActivityAt: mockNow - 2 * 60 * 60_000 }, |
| 1972 | { key: "topic_env", kind: "topic", label: t("mock.topicEnv"), root: "~/projects/joyquant-db", topicId: "topic_env", projectColor: "blue", turns: 3, createdAt: mockNow - 24 * 60 * 60_000, lastActivityAt: mockNow - 26 * 60 * 60_000 }, |
| 1973 | ], |
| 1974 | }, |
| 1975 | { |
| 1976 | key: "project_~/projects/joyquant-sys", |
| 1977 | kind: "project", |
| 1978 | label: t("mock.projectJoyquantSys"), |
| 1979 | root: "~/projects/joyquant-sys", |
| 1980 | projectColor: "purple", |
| 1981 | children: [ |
| 1982 | { key: "topic_p3b_pd", kind: "topic", label: `● ${t("mock.topicP3b")}`, root: "~/projects/joyquant-sys", topicId: "topic_p3b_pd", projectColor: "purple", turns: 11, lastActivityAt: mockNow - 3 * 24 * 60 * 60_000, status: runningMock ? "streaming" : undefined }, |
| 1983 | { key: "topic_p3a_pd", kind: "topic", label: t("mock.topicP3a"), root: "~/projects/joyquant-sys", topicId: "topic_p3a_pd", projectColor: "purple", turns: 9, lastActivityAt: mockNow - 4 * 24 * 60 * 60_000, status: runningMock ? "thinking" : undefined }, |
| 1984 | { key: "topic_hotfix", kind: "topic", label: t("mock.topicHotfix"), root: "~/projects/joyquant-sys", topicId: "topic_hotfix", projectColor: "purple", turns: 4, lastActivityAt: mockNow - 5 * 24 * 60 * 60_000, status: runningMock ? "thinking" : undefined }, |
| 1985 | { key: "topic_sys_coord", kind: "topic", label: t("mock.topicSysCoord"), root: "~/projects/joyquant-sys", topicId: "topic_sys_coord", projectColor: "purple", turns: 14, lastActivityAt: mockNow - 6 * 24 * 60 * 60_000, status: runningMock ? "waiting_confirmation" : undefined }, |
| 1986 | { key: "topic_sys_standard", kind: "topic", label: t("mock.topicSysStandard"), root: "~/projects/joyquant-sys", topicId: "topic_sys_standard", projectColor: "purple", turns: 6, lastActivityAt: mockNow - 7 * 24 * 60 * 60_000, status: "paused" }, |
| 1987 | { key: "topic_sys_exception", kind: "topic", label: t("mock.topicSysException"), root: "~/projects/joyquant-sys", topicId: "topic_sys_exception", projectColor: "purple", turns: 2, lastActivityAt: mockNow - 8 * 24 * 60 * 60_000, status: "error" }, |
| 1988 | ], |
| 1989 | }, |
| 1990 | { |
| 1991 | key: "global_folder", |
| 1992 | kind: "global_folder", |
| 1993 | label: "Global", |
| 1994 | root: globalWorkspaceRoot, |
| 1995 | children: [ |
| 1996 | { key: "global_topic_product", kind: "global_topic", label: t("mock.topicProduct"), topicId: "topic_product", turns: 5, lastActivityAt: mockNow - 8 * 24 * 60 * 60_000 }, |
| 1997 | { key: "global_topic_ai", kind: "global_topic", label: t("mock.topicAi"), topicId: "topic_ai", turns: 8, lastActivityAt: mockNow - 10 * 24 * 60 * 60_000 }, |
| 1998 | { key: "global_topic_lab", kind: "global_topic", label: t("mock.topicLab"), topicId: "topic_lab", turns: 2, lastActivityAt: mockNow - 12 * 24 * 60 * 60_000 }, |
| 1999 | ], |
| 2000 | }, |
| 2001 | ]; |
| 2002 | const ensureMockGlobalFolder = (): ProjectNode => { |
| 2003 | let node = mockProjectTree.find((item) => item.kind === "global_folder"); |
| 2004 | if (!node) { |
| 2005 | node = { |
| 2006 | key: "global_folder", |
| 2007 | kind: "global_folder", |
| 2008 | label: "Global", |
| 2009 | root: globalWorkspaceRoot, |
| 2010 | children: [], |
| 2011 | }; |
| 2012 | mockProjectTree.push(node); |
| 2013 | } |
| 2014 | return node; |
| 2015 | }; |
| 2016 | const mockProjectTreeForDisplay = () => { |
| 2017 | const pinnedProjects = mockProjectTree.filter((node) => node.kind === "project" && node.pinned); |
| 2018 | if (pinnedProjects.length === 0) return mockProjectTree; |
| 2019 | const rest = mockProjectTree.filter((node) => !(node.kind === "project" && node.pinned)); |
| 2020 | return [...pinnedProjects, ...rest]; |
| 2021 | }; |
| 2022 | const cloneProjectTree = () => { |
| 2023 | if (mockProjectTree.length === 0) ensureMockGlobalFolder(); |
| 2024 | return remoteProjects.appendToTree(JSON.parse(JSON.stringify(mockProjectTreeForDisplay())) as ProjectNode[]); |
| 2025 | }; |
| 2026 | const projectChildren = (node: ProjectNode): ProjectNode[] => Array.isArray(node.children) ? node.children : []; |
| 2027 | const findMockTopic = (topicId: string): ProjectNode | null => { |
| 2028 | for (const parent of mockProjectTree) { |
| 2029 | const found = projectChildren(parent).find((child) => child.topicId === topicId); |
| 2030 | if (found) return found; |
| 2031 | } |
| 2032 | return null; |
| 2033 | }; |
| 2034 | const setMockTopicPinned = (topicId: string, pinned: boolean) => { |
| 2035 | for (const parent of mockProjectTree) { |
| 2036 | const children = projectChildren(parent); |
| 2037 | const index = children.findIndex((child) => child.topicId === topicId); |
| 2038 | if (index < 0) continue; |
| 2039 | const topic = { ...children[index], pinned: pinned || undefined }; |
| 2040 | if (!pinned) { |
| 2041 | parent.children = children.map((child, i) => (i === index ? topic : child)); |
| 2042 | return; |
| 2043 | } |
| 2044 | const remaining = children.filter((_, i) => i !== index); |
| 2045 | parent.children = [topic, ...remaining]; |
| 2046 | return; |
| 2047 | } |
| 2048 | }; |
| 2049 | const setMockProjectPinned = (workspaceRoot: string, pinned: boolean) => { |
| 2050 | const index = mockProjectTree.findIndex((node) => node.kind === "project" && node.root === workspaceRoot); |
| 2051 | if (index < 0) return; |
| 2052 | mockProjectTree[index] = { ...mockProjectTree[index], pinned: pinned || undefined }; |
| 2053 | }; |
| 2054 | const deleteMockTopic = (topicId: string) => { |
| 2055 | for (const parent of mockProjectTree) { |
| 2056 | parent.children = projectChildren(parent).filter((child) => child.topicId !== topicId); |
| 2057 | } |
| 2058 | }; |
| 2059 | const topicLabel = (topicId: string, fallback: string) => (findMockTopic(topicId)?.label || fallback).replace(/^●\s*/, ""); |
| 2060 | const mockTopicStatus = (topicId: string) => findMockTopic(topicId)?.status ?? ""; |
| 2061 | const mockTopicIsRunning = (topicId: string) => { |
| 2062 | const status = mockTopicStatus(topicId); |
| 2063 | return status === "streaming" || status === "thinking" || status === "waiting_confirmation"; |
| 2064 | }; |
| 2065 | const mockTopicIsBlank = (topicId: string) => { |
| 2066 | const topic = findMockTopic(topicId); |
| 2067 | return Boolean(topic && topic.label === t("mock.newSession") && !topic.turns && !topic.lastActivityAt && !topic.status); |
| 2068 | }; |
| 2069 | const mockTopicRunsInScenario = (topicId: string) => runningMock && mockTopicIsRunning(topicId); |
| 2070 | // Benchmark fixtures (?mock=bench) live in a lazily imported module: the |
| 2071 | // generators build ~1MiB of mock content and must stay out of the eager |
| 2072 | // bundle (initial-chunk gzip budget). See bridgeBenchFixtures.ts. |
| 2073 | const benchFixturesPromise = benchMock ? import("./bridgeBenchFixtures") : null; |
| 2074 | const mockHistoryPage = (messages: HistoryMessage[], beforeTurn = 0, limit = 60): HistoryPage => { |
| 2075 | const totalTurns = messages.reduce((count, message) => count + (message.role === "user" ? 1 : 0), 0); |
| 2076 | const safeLimit = Math.max(1, Math.min(200, Math.floor(limit || 60))); |
| 2077 | const endTurn = beforeTurn > 0 && beforeTurn <= totalTurns ? beforeTurn : totalTurns; |
| 2078 | const startTurn = Math.max(0, endTurn - safeLimit); |
| 2079 | let turn = -1; |
| 2080 | const pageMessages = messages.filter((message) => { |
| 2081 | if (message.role === "user") turn += 1; |
| 2082 | if (turn < 0) return startTurn === 0; |
| 2083 | return turn >= startTurn && turn < endTurn; |
| 2084 | }); |
| 2085 | return { messages: pageMessages, startTurn, endTurn, totalTurns, hasOlder: startTurn > 0 }; |
| 2086 | }; |
| 2087 | const mockRuntimeInjected = new Set<string>(); |
| 2088 | const queueMockTopicRuntime = (tab: TabMeta) => { |
| 2089 | if (!runningMock) return; |
| 2090 | const status = mockTopicStatus(tab.topicId); |
| 2091 | if (status !== "streaming" && status !== "thinking" && status !== "waiting_confirmation") return; |
| 2092 | const key = `${tab.id}:${tab.topicId}:${status}`; |
| 2093 | if (mockRuntimeInjected.has(key)) return; |
| 2094 | mockRuntimeInjected.add(key); |
| 2095 | window.setTimeout(() => { |
| 2096 | void withMockTabScope(tab.id, async () => { |
| 2097 | emitMockTurnStarted(); |
| 2098 | await delay(120); |
| 2099 | if (tab.topicId === "topic_p3b_pd") { |
| 2100 | const text = "我会先把范围拆成三层:目标、依赖、风险。当前已经确认 p3b 的交付边界,接下来补充每个模块的验收口径..."; |
| 2101 | for (const ch of text) { |
| 2102 | emit({ kind: "text", text: ch }); |
| 2103 | await delay(5); |
| 2104 | } |
| 2105 | return; |
| 2106 | } |
| 2107 | if (tab.topicId === "topic_p3a_pd") { |
| 2108 | emit({ kind: "reasoning", text: "我正在对比 p3a 和 p3b 的差异:先看约束,再看变更风险,最后判断是否需要拆成独立任务。\n\n" }); |
| 2109 | await delay(220); |
| 2110 | emit({ kind: "reasoning", text: "当前倾向:先保留 p3a 的兼容路径,不急于删除旧逻辑。" }); |
| 2111 | return; |
| 2112 | } |
| 2113 | if (tab.topicId === "topic_hotfix") { |
| 2114 | const id = "mock-hotfix-shell"; |
| 2115 | emit({ kind: "tool_dispatch", tool: { id, name: "bash", args: JSON.stringify({ command: "git status --short && npm test" }), readOnly: true } }); |
| 2116 | await delay(180); |
| 2117 | emit({ kind: "tool_progress", tool: { id, name: "bash", readOnly: true, output: "$ git status --short\n M internal/sys/runner.go\n\n$ npm test\nrunning targeted regression tests...\n" } }); |
| 2118 | return; |
| 2119 | } |
| 2120 | if (tab.topicId === "topic_sys_coord") { |
| 2121 | pendingApprovalPreview = true; |
| 2122 | pendingApprovalPreviewPrompt = { id: "mock-sys-confirm", tool: "bash" }; |
| 2123 | emit({ kind: "reasoning", text: "我已经准备好执行同步脚本,但这个操作会影响本地 workspace,需要用户确认。" }); |
| 2124 | await delay(160); |
| 2125 | emitMockPrompt({ |
| 2126 | kind: "approval_request", |
| 2127 | approval: { |
| 2128 | id: "mock-sys-confirm", |
| 2129 | tool: "bash", |
| 2130 | subject: "npm run sync:joyquant-sys\n\n该命令会同步 SYS 项目配置并刷新本地缓存。", |
| 2131 | }, |
| 2132 | }); |
| 2133 | } |
| 2134 | }); |
| 2135 | }, 180); |
| 2136 | }; |
| 2137 | const setMockActiveTab = (tabId: string) => { |
| 2138 | mockTabs = mockTabs.map((tab) => ({ ...tab, active: tab.id === tabId })); |
| 2139 | }; |
| 2140 | // Single-surface prunes stash removed tabs so a later re-activation restores |
| 2141 | // the SAME tab identity (the real backend "opens or reuses" the topic's |
| 2142 | // tab), keeping the transcript store's (tabId, sessionPath) cache warm. |
| 2143 | const mockPrunedTabs = new Map<string, TabMeta>(); |
| 2144 | const mockTabGraveyardKey = (tab: Pick<TabMeta, "scope" | "workspaceRoot" | "topicId">) => |
| 2145 | `${tab.scope}:${tab.workspaceRoot}:${tab.topicId}`; |
| 2146 | const pruneMockTabsTo = (keepTabId: string) => { |
| 2147 | for (const tab of mockTabs) { |
| 2148 | if (tab.id !== keepTabId) mockPrunedTabs.set(mockTabGraveyardKey(tab), tab); |
| 2149 | } |
| 2150 | mockTabs = mockTabs.filter((item) => item.id === keepTabId).map((item) => ({ ...item, active: true })); |
| 2151 | }; |
| 2152 | const restoreMockPrunedTab = (scope: string, workspaceRoot: string, topicId: string): TabMeta | undefined => { |
| 2153 | const key = mockTabGraveyardKey({ scope, workspaceRoot, topicId }); |
| 2154 | const tab = mockPrunedTabs.get(key); |
| 2155 | if (!tab) return undefined; |
| 2156 | mockPrunedTabs.delete(key); |
| 2157 | return tab; |
| 2158 | }; |
| 2159 | const currentMockTurnTabId = () => mockScopedTabId || mockTabs.find((tab) => tab.active)?.id; |
| 2160 | const setMockTabRunning = (tabId: string | undefined, running: boolean) => { |
| 2161 | if (!tabId) return; |
| 2162 | mockTabs = mockTabs.map((tab) => (tab.id === tabId ? { ...tab, running } : tab)); |
| 2163 | }; |
| 2164 | const mockInteractions = createBrowserMockInteractionIdentity({ emit, currentTabId: currentMockTurnTabId, setRunning: setMockTabRunning }); |
| 2165 | const { emitPrompt: emitMockPrompt, turnStarted: emitMockTurnStarted, turnDone: emitMockTurnDone } = mockInteractions; |
| 2166 | // Fresh user decisions never auto-allow on a posture switch (mirrors the |
| 2167 | // backend's requiresFreshApprovalTool set). |
| 2168 | const mockFreshApprovalTools = new Set(["exit_plan_mode", "sandbox_escape", "memory_remember", "memory_forget", "managed_config_write"]); |
| 2169 | // Mirrors the backend drain contract for the mode-switch bindings: returns |
| 2170 | // the prompt ids the new posture auto-allowed; fresh prompts stay pending. |
| 2171 | const drainMockApprovalPreviews = (toolApprovalMode: string): string[] => { |
| 2172 | const preset = normalizeToolApprovalMode(toolApprovalMode); |
| 2173 | if (preset !== "workspace-write" && preset !== "danger-full-access") return []; |
| 2174 | const prompt = pendingApprovalPreviewPrompt; |
| 2175 | if (!pendingApprovalPreview || !prompt || mockFreshApprovalTools.has(prompt.tool)) return []; |
| 2176 | pendingApprovalPreview = false; |
| 2177 | pendingApprovalPreviewPrompt = undefined; |
| 2178 | emit({ kind: "message", text: `approval preview auto-allowed (${toolApprovalMode})` }); |
| 2179 | emitMockTurnDone(); |
| 2180 | return [prompt.id]; |
| 2181 | }; |
| 2182 | let mockTabs: TabMeta[] = benchMock ? [ |
| 2183 | // Phase F benchmark scenario: the two heaviest sessions pre-opened, the |
| 2184 | // markdown-heavy one active (cold open renders the 500KiB answer). |
| 2185 | { |
| 2186 | id: "tab_bench_markdown", |
| 2187 | scope: "project", |
| 2188 | workspaceRoot: "~/projects/reasonix", |
| 2189 | workspaceName: "reasonix", |
| 2190 | workspacePath: "~/projects/reasonix", |
| 2191 | gitBranch: "bench", |
| 2192 | topicId: "topic_bench_markdown", |
| 2193 | topicTitle: "bench:markdown-46t", |
| 2194 | projectColor: "purple", |
| 2195 | label: "DeepSeek-R1", |
| 2196 | ready: true, |
| 2197 | running: false, |
| 2198 | mode: "normal", |
| 2199 | collaborationMode: "normal", |
| 2200 | toolApprovalMode: "ask", |
| 2201 | tokenMode: "full", |
| 2202 | active: true, |
| 2203 | cwd: "~/projects/reasonix", |
| 2204 | }, |
| 2205 | { |
| 2206 | id: "tab_bench_tools", |
| 2207 | scope: "project", |
| 2208 | workspaceRoot: "~/projects/reasonix", |
| 2209 | workspaceName: "reasonix", |
| 2210 | workspacePath: "~/projects/reasonix", |
| 2211 | gitBranch: "bench", |
| 2212 | topicId: "topic_bench_tools", |
| 2213 | topicTitle: "bench:tools-38t", |
| 2214 | projectColor: "blue", |
| 2215 | label: "DeepSeek-R1", |
| 2216 | ready: true, |
| 2217 | running: false, |
| 2218 | mode: "normal", |
| 2219 | collaborationMode: "normal", |
| 2220 | toolApprovalMode: "ask", |
| 2221 | tokenMode: "full", |
| 2222 | active: false, |
| 2223 | cwd: "~/projects/reasonix", |
| 2224 | }, |
| 2225 | ] : noticePreviewMock ? [ |
| 2226 | { |
| 2227 | id: "tab_notice_preview", |
| 2228 | scope: "project", |
| 2229 | workspaceRoot: "~/projects/reasonix", |
| 2230 | workspaceName: "reasonix", |
| 2231 | workspacePath: "~/projects/reasonix", |
| 2232 | gitBranch: "codex/compact-chat-notices-i18n", |
| 2233 | topicId: "topic_notice_preview", |
| 2234 | topicTitle: "Compact notice preview", |
| 2235 | projectColor: "green", |
| 2236 | label: "DeepSeek-R1", |
| 2237 | ready: true, |
| 2238 | running: false, |
| 2239 | mode: "normal", |
| 2240 | collaborationMode: "normal", |
| 2241 | toolApprovalMode: "ask", |
| 2242 | tokenMode: "full", |
| 2243 | active: true, |
| 2244 | cwd: "~/projects/reasonix", |
| 2245 | }, |
| 2246 | ] : freshMock ? [ |
| 2247 | { |
| 2248 | id: "tab_global", |
| 2249 | scope: "global", |
| 2250 | workspaceRoot: globalWorkspaceRoot, |
| 2251 | workspaceName: "Global", |
| 2252 | workspacePath: globalWorkspaceRoot, |
| 2253 | topicId: "", |
| 2254 | topicTitle: "Global", |
| 2255 | label: "DeepSeek-R1", |
| 2256 | ready: true, |
| 2257 | running: false, |
| 2258 | mode: "normal", |
| 2259 | collaborationMode: "normal", |
| 2260 | toolApprovalMode: "ask", |
| 2261 | tokenMode: "full", |
| 2262 | active: true, |
| 2263 | cwd: globalWorkspaceRoot, |
| 2264 | }, |
| 2265 | ] : [ |
| 2266 | { |
| 2267 | id: "tab_joyquant_db", |
| 2268 | scope: "project", |
| 2269 | workspaceRoot: "~/projects/joyquant-db", |
| 2270 | workspaceName: "joyquant-db", |
| 2271 | workspacePath: "~/projects/joyquant-db", |
| 2272 | gitBranch: "main", |
| 2273 | topicId: "topic_dev_standard", |
| 2274 | topicTitle: t("mock.trashDevStandardTitle"), |
| 2275 | projectColor: "blue", |
| 2276 | label: "DeepSeek-R1", |
| 2277 | ready: true, |
| 2278 | running: false, |
| 2279 | mode: "normal", |
| 2280 | collaborationMode: "normal", |
| 2281 | toolApprovalMode: "ask", |
| 2282 | tokenMode: "full", |
| 2283 | active: !guidanceMock, |
| 2284 | cwd: "~/projects/joyquant-db", |
| 2285 | }, |
| 2286 | { |
| 2287 | id: "tab_joyquant_sys", |
| 2288 | scope: "project", |
| 2289 | workspaceRoot: "~/projects/joyquant-sys", |
| 2290 | workspaceName: "joyquant-sys", |
| 2291 | workspacePath: "~/projects/joyquant-sys", |
| 2292 | gitBranch: "feature/p3b", |
| 2293 | topicId: "topic_p3b_pd", |
| 2294 | topicTitle: "p3b P&D", |
| 2295 | projectColor: "purple", |
| 2296 | label: "DeepSeek-R1", |
| 2297 | ready: true, |
| 2298 | running: runningMock && mockTopicIsRunning("topic_p3b_pd"), |
| 2299 | mode: "normal", |
| 2300 | collaborationMode: "normal", |
| 2301 | toolApprovalMode: "ask", |
| 2302 | tokenMode: "full", |
| 2303 | active: guidanceMock, |
| 2304 | cwd: "~/projects/joyquant-sys", |
| 2305 | }, |
| 2306 | { |
| 2307 | id: "tab_global", |
| 2308 | scope: "global", |
| 2309 | workspaceRoot: globalWorkspaceRoot, |
| 2310 | workspaceName: "Global", |
| 2311 | workspacePath: globalWorkspaceRoot, |
| 2312 | topicId: "topic_global", |
| 2313 | topicTitle: "Global", |
| 2314 | label: "DeepSeek-R1", |
| 2315 | ready: true, |
| 2316 | running: false, |
| 2317 | mode: "normal", |
| 2318 | collaborationMode: "normal", |
| 2319 | toolApprovalMode: "ask", |
| 2320 | tokenMode: "full", |
| 2321 | active: false, |
| 2322 | cwd: globalWorkspaceRoot, |
| 2323 | }, |
| 2324 | ]; |
| 2325 | if (sandboxEscapeMock) { |
| 2326 | window.setTimeout(() => { |
| 2327 | if (pendingApprovalPreview) return; |
| 2328 | pendingApprovalPreview = true; |
| 2329 | pendingApprovalPreviewPrompt = { id: "mock-sandbox-escape-preview", tool: "sandbox_escape" }; |
| 2330 | emitMockTurnStarted(); |
| 2331 | emit({ kind: "reasoning", text: t("mock.sandboxEscapeReasoning") }); |
| 2332 | emitMockPrompt({ |
| 2333 | kind: "approval_request", |
| 2334 | approval: { |
| 2335 | id: "mock-sandbox-escape-preview", |
| 2336 | tool: "sandbox_escape", |
| 2337 | subject: t("mock.sandboxEscapeSubject"), |
| 2338 | reason: t("mock.sandboxEscapeReason"), |
| 2339 | }, |
| 2340 | }); |
| 2341 | }, 800); |
| 2342 | } |
| 2343 | const mockModelCatalog = [ |
| 2344 | { ref: "deepseek/deepseek-v4-flash", provider: "deepseek", model: "deepseek-v4-flash" }, |
| 2345 | { ref: "deepseek/deepseek-v4-pro", provider: "deepseek", model: "deepseek-v4-pro" }, |
| 2346 | ]; |
| 2347 | const defaultMockModelRef = mockModelCatalog[0].ref; |
| 2348 | const mockModelRef = (name: string): string => { |
| 2349 | const trimmed = name.trim(); |
| 2350 | if (!trimmed || trimmed === "DeepSeek-R1") return defaultMockModelRef; |
| 2351 | const exact = mockModelCatalog.find((model) => model.ref === trimmed); |
| 2352 | if (exact) return exact.ref; |
| 2353 | const byModel = mockModelCatalog.find((model) => model.model === trimmed); |
| 2354 | return byModel?.ref ?? trimmed; |
| 2355 | }; |
| 2356 | const mockModelLabel = (ref: string): string => mockModelCatalog.find((model) => model.ref === mockModelRef(ref))?.model ?? ref.split("/").pop() ?? ref; |
| 2357 | const mockTabModelRef = (tab?: TabMeta): string => mockModelRef(tab?.label ?? ""); |
| 2358 | let mockTerminalSessions: TerminalSessionView[] = []; |
| 2359 | const mockTerminalOutput = new Map<string, string>(); |
| 2360 | const mockTerminalTabIDs = new Map<string, string>(); |
| 2361 | const mockTerminalBytes = (text: string): string => { |
| 2362 | if (typeof btoa === "function") return btoa(unescape(encodeURIComponent(text))); |
| 2363 | return ""; |
| 2364 | }; |
| 2365 | const setMockTabModel = (tabID: string | undefined, name: string) => { |
| 2366 | const ref = mockModelRef(name); |
| 2367 | const label = mockModelLabel(ref); |
| 2368 | let applied = false; |
| 2369 | mockTabs = mockTabs.map((tab) => { |
| 2370 | const match = tabID ? tab.id === tabID : tab.active; |
| 2371 | if (!match) return tab; |
| 2372 | applied = true; |
| 2373 | return { ...tab, label }; |
| 2374 | }); |
| 2375 | if (!applied && mockTabs.length > 0) { |
| 2376 | mockTabs = mockTabs.map((tab, index) => (index === 0 ? { ...tab, label } : tab)); |
| 2377 | } |
| 2378 | }; |
| 2379 | const mockArchivedSessionIDs = new Set<string>(); |
| 2380 | const mockPurgedSessionIDs = new Set<string>(); |
| 2381 | const mockSessionIDForNode = (node: ProjectNode) => (node.topicId || node.key || "mock-session").replace(/[^a-zA-Z0-9._-]/g, "-"); |
| 2382 | const mockWorkspaceID = (node: ProjectNode) => node.kind === "global_folder" ? "global" : `project-${(node.root || node.key).replace(/[^a-zA-Z0-9._-]/g, "-")}`; |
| 2383 | const mockWorkspaceSnapshot = (): WorkspaceSnapshot => ({ |
| 2384 | generation: 1, |
| 2385 | workspaces: mockProjectTreeForDisplay().filter((node) => node.kind === "project" || node.kind === "global_folder").map((node) => ({ |
| 2386 | id: mockWorkspaceID(node), root: node.root || "", title: node.label, |
| 2387 | sessionIds: projectChildren(node).map(mockSessionIDForNode), visible: true, createdAt: 0, updatedAt: 0, |
| 2388 | })), |
| 2389 | archivedSessionIds: [...mockArchivedSessionIDs], pendingCreates: [], |
| 2390 | }); |
| 2391 | return { ...({} as AttachmentBindings), |
| 2392 | async OpenSessionDraft(workspaceId: string) { |
| 2393 | const prior = [...mockDrafts.values()].find((draft) => draft.workspaceId === workspaceId && draft.status === "active"); |
| 2394 | if (prior) return structuredClone(prior); |
| 2395 | return structuredClone(mockDraftForTarget(workspaceId === "global" ? "global" : "project", workspaceId === "global" ? "" : workspaceId)); |
| 2396 | }, |
| 2397 | async OpenSessionDraftForTarget(scope: string, workspaceRoot: string) { |
| 2398 | return structuredClone(mockDraftForTarget(scope, workspaceRoot)); |
| 2399 | }, |
| 2400 | async RestoreSessionDraft() { |
| 2401 | return structuredClone([...mockDrafts.values()].find((draft) => draft.status === "active") ?? null); |
| 2402 | }, |
| 2403 | async SaveSessionDraft(request: SessionDraftSaveRequest) { |
| 2404 | const current = mockDrafts.get(request.draftId); |
| 2405 | if (!current || current.status !== "active") throw new Error("session draft not found"); |
| 2406 | if (current.revision !== request.revision) return { draft: structuredClone(current), conflict: true, outcome: "conflict" }; |
| 2407 | const next = { ...current, revision: current.revision + 1, contentJson: request.contentJson, settings: structuredClone(request.settings), updatedAt: Date.now() }; |
| 2408 | mockDrafts.set(next.id, next); |
| 2409 | return { draft: structuredClone(next), conflict: false, outcome: "saved" }; |
| 2410 | }, |
| 2411 | async ListSessionDraftSummaries() { |
| 2412 | return [...mockDrafts.values()].filter((draft) => draft.status === "active").map((draft) => ({ |
| 2413 | id: draft.id, workspaceId: draft.workspaceId, scope: draft.scope, workspaceRoot: draft.workspaceRoot, |
| 2414 | revision: draft.revision, hasContent: draft.contentJson !== "{}", state: "saved", updatedAt: draft.updatedAt, |
| 2415 | })); |
| 2416 | }, |
| 2417 | async DiscardSessionDraft(draftId: string, revision: number) { |
| 2418 | const current = mockDrafts.get(draftId); |
| 2419 | if (!current || current.revision !== revision) throw new Error("session draft revision conflict"); |
| 2420 | mockDrafts.set(draftId, { ...current, status: "discarded", revision: current.revision + 1 }); |
| 2421 | }, |
| 2422 | async DismissSessionDraft(_draftId: string) {}, |
| 2423 | async SetSessionDraftRestoreTarget(_draftId: string) {}, |
| 2424 | async GetSessionDraft(draftId: string) { |
| 2425 | const draft = mockDrafts.get(draftId); |
| 2426 | if (!draft) throw new Error("session draft not found"); |
| 2427 | return structuredClone(draft); |
| 2428 | }, |
| 2429 | async GetDraftContext(draftId: string) { |
| 2430 | const draft = mockDrafts.get(draftId); |
| 2431 | if (!draft) throw new Error("session draft not found"); |
| 2432 | return { draft: structuredClone(draft), commands: [], servers: [] }; |
| 2433 | }, |
| 2434 | async GetSessionDraftState(draftId: string) { |
| 2435 | const draft = mockDrafts.get(draftId); |
| 2436 | if (!draft) throw new Error("session draft not found"); |
| 2437 | const operation = [...mockDraftOperations.values()].reverse().find(item => item.draftId === draftId); |
| 2438 | return structuredClone({ draft, operation }); |
| 2439 | }, |
| 2440 | async ResumeDraftSubmission(operationId: string, revision: number) { |
| 2441 | const operation = mockDraftOperations.get(operationId); |
| 2442 | if (!operation || operation.revision !== revision) throw new Error("operation revision conflict"); |
| 2443 | return structuredClone(operation); |
| 2444 | }, |
| 2445 | async BeginDraftSubmission(request: SessionDraftSubmissionRequest) { |
| 2446 | const draft = mockDrafts.get(request.draftId); |
| 2447 | if (!draft || draft.revision !== request.revision) throw new Error("session draft revision conflict"); |
| 2448 | const operationId = `draft-op-mock-${mockDraftOperations.size + 1}`; |
| 2449 | const sessionId = `draft-session-mock-${mockDraftOperations.size + 1}`; |
| 2450 | const view: SessionDraftSubmissionView = { operationId, draftId: draft.id, requestId: request.requestId, revision: 1, canResume: false, canEdit: false, canCancel: false, canDiscard: false, phase: "accepted", submissionId: `draft-submit-mock-${mockDraftOperations.size + 1}`, session: { hostId: "local", sessionId }, updatedAt: Date.now() }; |
| 2451 | mockDraftOperations.set(operationId, view); |
| 2452 | mockDrafts.set(draft.id, { ...draft, status: "converted", revision: draft.revision + 1 }); |
| 2453 | return structuredClone(view); |
| 2454 | }, |
| 2455 | async GetDraftSubmission(operationId: string) { |
| 2456 | const operation = mockDraftOperations.get(operationId); |
| 2457 | if (!operation) throw new Error("session draft submission not found"); |
| 2458 | return structuredClone(operation); |
| 2459 | }, |
| 2460 | async CancelDraftSubmission(operationId: string) { |
| 2461 | const operation = mockDraftOperations.get(operationId); |
| 2462 | if (!operation) throw new Error("session draft submission not found"); |
| 2463 | const next = { ...operation, phase: operation.phase === "accepted" ? "accepted" : "cancelled", updatedAt: Date.now() }; |
| 2464 | mockDraftOperations.set(operationId, next); |
| 2465 | return structuredClone(next); |
| 2466 | }, |
| 2467 | ...makeMockSessionExportBindings(), |
| 2468 | ...makeMockSessionCatalogBindings(cloneProjectTree), |
| 2469 | ...makeMockBlankProjectBindings(), |
| 2470 | async GetWorkspaceSnapshot() { return mockWorkspaceSnapshot(); }, |
| 2471 | async GetLegacyEmptySessionCleanupStatus() { |
| 2472 | return { version: 1, state: "complete", removed: 0, pending: 0, busy: 0, unknown: 0, protected: 0, hasContent: 0, items: [] }; |
| 2473 | }, |
| 2474 | async RetryLegacyEmptySessionCleanup() { |
| 2475 | return { version: 1, state: "complete", removed: 0, pending: 0, busy: 0, unknown: 0, protected: 0, hasContent: 0, items: [] }; |
| 2476 | }, |
| 2477 | ...makeMockSessionLifecycleBindings(mockWorkspaceSnapshot, mockArchivedSessionIDs, mockPurgedSessionIDs, notifyMockProjectTreeChanged), |
| 2478 | async CreateSession(_workspaceId: string) { return { hostId: "local", sessionId: `mock-${Date.now()}` }; }, |
| 2479 | async ForkSession(_ref: SessionRef, _turnBoundary: string) { return { hostId: "local", sessionId: `mock-fork-${Date.now()}` }; }, |
| 2480 | async ForkSessionTarget(_selector: SessionSelector, _turnBoundary: string) { return { hostId: "local", sessionId: `mock-fork-${Date.now()}` }; }, |
| 2481 | async GetSessionActivityBaseline(selector: SessionSelector) { |
| 2482 | return { ref: selector.ref ?? { hostId: "local", sessionId: "" }, resultSequence: 0, eventVersion: "", lifecycleGeneration: 0, complete: false }; |
| 2483 | }, |
| 2484 | async CopySessionTarget(_selector: SessionSelector, operationId: string): Promise<SessionCreationResult> { |
| 2485 | return { |
| 2486 | ref: { hostId: "local", sessionId: `mock-copy-${operationId || Date.now()}` }, |
| 2487 | operationId: operationId || `mock-copy-operation-${Date.now()}`, |
| 2488 | committed: true, |
| 2489 | }; |
| 2490 | }, |
| 2491 | async HistorySliceForTarget(_selector: SessionSelector, _request: HistorySliceRequest): Promise<HistorySlice> { |
| 2492 | return { entries: [], nextCursor: "", hasOlder: false, totalTurns: 0, startTurn: 0, endTurn: 0, stale: false, revision: 0 }; |
| 2493 | }, |
| 2494 | async HistoryContentForTarget(_selector: SessionSelector, ref: HistoryContentRef, chunkIndex: number): Promise<HistoryContentChunk> { |
| 2495 | return { |
| 2496 | entryId: ref.entryId, field: ref.field, chunk: Math.max(0, chunkIndex), |
| 2497 | chunks: 1, data: "", done: true, stale: false, |
| 2498 | }; |
| 2499 | }, |
| 2500 | async SearchHistoryContentForTarget(_selector: SessionSelector, _query: string, _cursor: string, _limit: number): Promise<HistorySearchPage> { |
| 2501 | return { |
| 2502 | items: [], nextCursor: "", revision: 0, partial: false, staleCursor: false, |
| 2503 | status: { state: "ready", mode: "incremental", total: 0, indexed: 0, pending: 0, failed: 0, revision: 0 }, |
| 2504 | }; |
| 2505 | }, |
| 2506 | async SessionHistoryPageForTarget(_selector: SessionSelector, _cursor: string, _limit: number): Promise<MessageHistoryPage> { |
| 2507 | return { messages: [], snapshotSequence: 0, coverageSequence: 0, status: "ready", totalTurns: 0, generation: "mock", hasMore: false }; |
| 2508 | }, |
| 2509 | async SessionHistoryContentForTarget(_selector: SessionSelector, ref: SessionContentRef, offset: number): Promise<SessionHistoryContentChunk> { |
| 2510 | return { data: "", nextOffset: Math.min(Math.max(0, offset), ref.bytes), done: offset >= ref.bytes }; |
| 2511 | }, |
| 2512 | async LocateSessionMessageForTarget(_selector: SessionSelector, messageId: string, snapshot: number): Promise<MessageLocation> { |
| 2513 | return { messageId, snapshotSequence: snapshot, coverageSequence: snapshot, status: "ready", position: 0, visibleTurn: 0 }; |
| 2514 | }, |
| 2515 | async SessionMessageFieldForTarget( |
| 2516 | _selector: SessionSelector, |
| 2517 | messageId: string, |
| 2518 | version: number, |
| 2519 | field: string, |
| 2520 | offset: number, |
| 2521 | _length: number, |
| 2522 | ): Promise<MessageFieldPage> { |
| 2523 | return { status: "ready", messageId, version, field, totalBytes: 0, offset, data: "", nextOffset: offset, encoding: "utf8" }; |
| 2524 | }, |
| 2525 | async SearchSessionHistoryForTarget(_selector: SessionSelector, _query: string, _cursor: string, _limit: number): Promise<SearchHistoryPage> { |
| 2526 | return { hits: [], snapshotSequence: 0, coverageSequence: 0, status: "ready", hasMore: false }; |
| 2527 | }, |
| 2528 | async SessionHistoryWindowForTarget(_selector: SessionSelector, _request: HistoryWindowRequest): Promise<HistoryWindowPage> { |
| 2529 | return { messages: [], status: "ready", snapshotSequence: 0, coverageSequence: 0, totalTurns: 0, hasOlder: false, hasNewer: false }; |
| 2530 | }, |
| 2531 | async ListWorkspaceSessions(workspaceId: string, query: string, _cursor: string, limit: number, includeArchived: boolean) { |
| 2532 | const parent = mockProjectTreeForDisplay().find((node) => mockWorkspaceID(node) === workspaceId); |
| 2533 | const needle = query.trim().toLowerCase(); |
| 2534 | const sessions = projectChildren(parent ?? { key: "", kind: "global_folder", label: "", children: [] }).map((node) => { |
| 2535 | const sessionId = mockSessionIDForNode(node), isArchived = mockArchivedSessionIDs.has(sessionId); |
| 2536 | return { |
| 2537 | ref: { hostId: "local", sessionId }, workspaceId, title: node.label, preview: node.preview || "", turns: node.turns || 0, |
| 2538 | createdAt: node.createdAt || 0, updatedAt: node.lastActivityAt || 0, blank: !node.turns && !node.preview, |
| 2539 | archived: isArchived, running: Boolean(node.running), metadataStatus: "ready", health: "healthy", |
| 2540 | }; |
| 2541 | }).filter((row) => !mockPurgedSessionIDs.has(row.ref.sessionId) && (includeArchived || !row.archived) && (!needle || `${row.title}\n${row.preview}`.toLowerCase().includes(needle))).slice(0, limit); |
| 2542 | return { sessions, registryGeneration: 1 }; |
| 2543 | }, |
| 2544 | async OpenSession(ref: SessionRef) { |
| 2545 | if (ref.hostId !== "local") throw new Error(`unsupported mock session host: ${ref.hostId}`); |
| 2546 | const { rebindMockSessionTab } = await import("./bridgeMockSessionNavigation"); |
| 2547 | const rebound = rebindMockSessionTab(ref, mockTabs.find((candidate) => candidate.active) ?? mockTabs[0], mockProjectTreeForDisplay(), globalWorkspaceRoot, mockTopicRunsInScenario); |
| 2548 | // SessionRef navigation rebinds the existing surface; legacy Topic |
| 2549 | // activation swaps tab ids and makes its ready event correctly stale. |
| 2550 | pruneMockTabsTo(rebound.id); |
| 2551 | mockTabs = [rebound]; |
| 2552 | emitMockReady(rebound.id); |
| 2553 | return this.HistoryPageForTab(rebound.id, 0, 60); |
| 2554 | }, |
| 2555 | async ReadSessionHistory(_ref: SessionRef, _cursor: string, _limit: number) { return { messages: [], startTurn: 0, endTurn: 0, totalTurns: 0, hasOlder: false }; }, |
| 2556 | async RenameCanonicalSession(_ref: SessionRef, _title: string) {}, |
| 2557 | async SetSessionPinned(_selector: SessionSelector, _pinned: boolean) { notifyMockProjectTreeChanged(); }, |
| 2558 | async ArchiveCanonicalSession(ref: SessionRef) { mockArchivedSessionIDs.add(ref.sessionId); notifyMockProjectTreeChanged(); }, |
| 2559 | async ArchiveSessionTarget(selector: SessionSelector): Promise<SessionMutationResult> { |
| 2560 | const node = mockSessionTitleTarget(mockProjectTree, selector); |
| 2561 | const targetKey = node ? sessionTitleTarget(node) : selector.sessionPath?.trim() || selector.topicId?.trim() || selector.ref?.sessionId || ""; |
| 2562 | if (selector.ref?.sessionId) mockArchivedSessionIDs.add(selector.ref.sessionId); |
| 2563 | if (selector.sessionPath) await this.DeleteSession(selector.sessionPath); |
| 2564 | notifyMockProjectTreeChanged(); |
| 2565 | return { targetKey, operationId: `mock-archive-${Date.now()}`, committed: true, lifecycleGeneration: 2 }; |
| 2566 | }, |
| 2567 | async RestoreCanonicalSession(ref: SessionRef) { mockArchivedSessionIDs.delete(ref.sessionId); notifyMockProjectTreeChanged(); }, |
| 2568 | async RestoreSessionTarget(selector: SessionSelector): Promise<SessionMutationResult> { |
| 2569 | const node = mockSessionTitleTarget(mockProjectTree, selector); |
| 2570 | const targetKey = node ? sessionTitleTarget(node) : selector.sessionPath?.trim() || selector.topicId?.trim() || selector.ref?.sessionId || ""; |
| 2571 | if (selector.ref?.sessionId) mockArchivedSessionIDs.delete(selector.ref.sessionId); |
| 2572 | notifyMockProjectTreeChanged(); |
| 2573 | return { targetKey, operationId: `mock-restore-${Date.now()}`, committed: true, lifecycleGeneration: 3 }; |
| 2574 | }, |
| 2575 | async DeleteSessionTarget(selector: SessionSelector): Promise<SessionMutationResult> { |
| 2576 | const node = mockSessionTitleTarget(mockProjectTree, selector); |
| 2577 | const sessionId = selector.ref?.sessionId || (node ? mockSessionIDForNode(node) : ""); |
| 2578 | if (sessionId) mockPurgedSessionIDs.add(sessionId); |
| 2579 | notifyMockProjectTreeChanged(); |
| 2580 | return { targetKey: node ? sessionTitleTarget(node) : sessionId, operationId: `mock-delete-${Date.now()}`, committed: true, lifecycleGeneration: 4 }; |
| 2581 | }, |
| 2582 | async MoveWorkspaceSession(_workspaceId: string, _sessionId: string, _beforeSessionId: string) {}, |
| 2583 | async MoveSessionTarget(selector: SessionSelector, _workspaceId: string, _beforeSessionId: string): Promise<SessionMutationResult> { |
| 2584 | const node = mockSessionTitleTarget(mockProjectTree, selector); |
| 2585 | return { targetKey: node ? sessionTitleTarget(node) : "", operationId: `mock-move-${Date.now()}`, committed: true, lifecycleGeneration: 1 }; |
| 2586 | }, |
| 2587 | async RenameWorkspace(_workspaceId: string, _title: string) {}, |
| 2588 | async SetWorkspaceVisible(_workspaceId: string, _visible: boolean) {}, |
| 2589 | async MoveWorkspace(_workspaceId: string, _beforeWorkspaceId: string) {}, |
| 2590 | async GetSessionArchitectureDiagnostics() { |
| 2591 | return { |
| 2592 | pending_operations: 0, missing_members: 0, identity_mismatches: 0, |
| 2593 | source_conflicts: 0, recovery_entries: 0, |
| 2594 | session_headers_total: 0, workspace_members_total: 0, unassigned_sessions: 0, |
| 2595 | migration_pending: 0, migration_failed: 0, migration_completed: 0, |
| 2596 | projection_pending: 0, projection_failed: 0, pending_create_recovered: 0, |
| 2597 | prune_blocked_persistence: 0, |
| 2598 | }; |
| 2599 | }, |
| 2600 | async MinimiseMainWindow() { |
| 2601 | console.info("mock MinimiseMainWindow"); |
| 2602 | }, |
| 2603 | async ToggleMaximiseMainWindow() { |
| 2604 | console.info("mock ToggleMaximiseMainWindow"); |
| 2605 | }, |
| 2606 | async IsMainWindowMaximised() { |
| 2607 | return false; |
| 2608 | }, |
| 2609 | async CloseMainWindow() { |
| 2610 | console.info("mock CloseMainWindow"); |
| 2611 | }, |
| 2612 | async Platform() { |
| 2613 | const override = browserPlatformOverride(); |
| 2614 | if (override) return override; |
| 2615 | // Mirror the OS the browser dev mock runs on. |
| 2616 | const ua = typeof navigator !== "undefined" ? navigator.userAgent : ""; |
| 2617 | if (/Win/i.test(ua)) return "windows"; |
| 2618 | if (/Mac/i.test(ua)) return "darwin"; |
| 2619 | return "linux"; |
| 2620 | }, |
| 2621 | async Submit(input, submissionID?: string) { |
| 2622 | cancelled = false; |
| 2623 | emitMockTurnStarted(submissionID); |
| 2624 | const trimmedInput = input.trim().toLowerCase(); |
| 2625 | // Deterministic browser QA for the real recovery/source components. |
| 2626 | if (trimmedInput === "/mock-protocol-recovery") { |
| 2627 | setMockTabRunning(currentMockTurnTabId(), false); |
| 2628 | emit({kind:"turn_done",err:"上游拒绝了请求,但未提供具体原因",protocolRecovery:{id:"mock-protocol-once"}}); |
| 2629 | return; |
| 2630 | } |
| 2631 | if (trimmedInput === "/recover-context mock-protocol-once") { |
| 2632 | await delay(1500); |
| 2633 | if (cancelled) return; |
| 2634 | emit({kind:"tool_dispatch",tool:{id:"mock-search",name:"web_search",args:'{"query":"OpenCode documentation"}',readOnly:true}}); |
| 2635 | emit({kind:"tool_result",tool:{id:"mock-search",name:"web_search",args:'{"query":"OpenCode documentation"}',readOnly:true,output:JSON.stringify({summary:"搜索摘要仍然可读。",sources:[],sources_status:"not_provided"})}}); |
| 2636 | emit({kind:"notice",level:"info",code:"search_sources_not_provided",text:"搜索已完成,供应商未提供可用的结构化来源"}); |
| 2637 | emitMockTurnDone(submissionID); |
| 2638 | return; |
| 2639 | } |
| 2640 | const decisionSurfaceMock = decisionSurfaceMockFromInput(trimmedInput); |
| 2641 | const goalMatch = /^\/goal(?:\s+([\s\S]*))?$/.exec(input.trim()); |
| 2642 | if (goalMatch) { |
| 2643 | const arg = stripLegacyGoalBudgetFlags((goalMatch[1] ?? "").trim()); |
| 2644 | const lowered = arg.toLowerCase(); |
| 2645 | const active = mockTabs.find((tab) => tab.active); |
| 2646 | if (!arg || lowered === "status") { |
| 2647 | emit({ kind: "notice", level: "info", text: active?.goal ? `goal: ${active.goal}` : "goal: none" }); |
| 2648 | emitMockTurnDone(submissionID); |
| 2649 | return; |
| 2650 | } |
| 2651 | if (["clear", "off", "stop", "done"].includes(lowered)) { |
| 2652 | mockTabs = mockTabs.map((tab) => (tab.active ? { ...tab, goal: "", goalStatus: "stopped", collaborationMode: "normal" } : tab)); |
| 2653 | emit({ kind: "notice", level: "info", text: "goal cleared" }); |
| 2654 | emitMockTurnDone(submissionID); |
| 2655 | return; |
| 2656 | } |
| 2657 | mockTabs = mockTabs.map((tab) => (tab.active ? { ...tab, goal: arg, goalStatus: "running", collaborationMode: "goal" } : tab)); |
| 2658 | emit({ kind: "notice", level: "info", text: `goal set: ${arg}` }); |
| 2659 | await delay(350); |
| 2660 | if (cancelled) return; |
| 2661 | const reply = `Autonomous goal run started for: **${arg}**\n\nMock run completed.\n\n[goal:complete]`; |
| 2662 | emit({ kind: "message", text: reply }); |
| 2663 | mockTabs = mockTabs.map((tab) => (tab.active ? { ...tab, goal: "", goalStatus: "complete", collaborationMode: "normal" } : tab)); |
| 2664 | emit({ kind: "notice", level: "info", text: "goal complete" }); |
| 2665 | emitMockTurnDone(submissionID); |
| 2666 | return; |
| 2667 | } |
| 2668 | if (decisionSurfaceMock === "mcp_interaction") return (await import("./mockMCPInteraction")).showMockMCPInteraction(delay, () => cancelled, emitMockPrompt); |
| 2669 | if (decisionSurfaceMock === "tool_approval") { |
| 2670 | pendingApprovalPreview = true; |
| 2671 | pendingApprovalPreviewPrompt = { id: "mock-approval-preview", tool: "bash" }; |
| 2672 | await delay(250); |
| 2673 | if (cancelled) return; |
| 2674 | emitMockPrompt({ |
| 2675 | kind: "approval_request", |
| 2676 | approval: { |
| 2677 | id: "mock-approval-preview", |
| 2678 | tool: "bash", |
| 2679 | subject: t("mock.approvalSubject"), |
| 2680 | }, |
| 2681 | }); |
| 2682 | return; |
| 2683 | } |
| 2684 | if (trimmedInput === "/recovery-preview" || trimmedInput === "recovery preview" || trimmedInput === "恢复预览") { |
| 2685 | pendingApprovalPreview = true; |
| 2686 | pendingApprovalPreviewPrompt = { id: "mock-recovery-preview", tool: "write_file" }; |
| 2687 | await delay(250); |
| 2688 | if (cancelled) return; |
| 2689 | emitMockPrompt({ |
| 2690 | kind: "approval_request", |
| 2691 | approval: { |
| 2692 | id: "mock-recovery-preview", |
| 2693 | tool: "write_file", |
| 2694 | subject: "internal/recovery/gate.go", |
| 2695 | reason: "The proposed recovery changes the implementation method after a failing verification.", |
| 2696 | fresh: true, |
| 2697 | kind: "recovery", |
| 2698 | recovery: { |
| 2699 | source_agent: "root", |
| 2700 | failed_tool: "bash", |
| 2701 | failed_summary: "go test ./internal/recovery failed", |
| 2702 | diagnosis: "The failure is isolated to recovery state persistence.", |
| 2703 | next_tool: "write_file", |
| 2704 | next_action: "Update internal/recovery/gate.go", |
| 2705 | change_kind: "strategy", |
| 2706 | change_rationale: "The proposed edit changes the recovery method and needs a fresh decision.", |
| 2707 | plan_before: [ |
| 2708 | "1. Keep the existing Auto execution path [in_progress]", |
| 2709 | "2. Add execution-risk approval prompts [pending]", |
| 2710 | "3. Run the recovery regression suite [pending]", |
| 2711 | ].join("\n"), |
| 2712 | plan_after: [ |
| 2713 | "1. Keep the existing Auto execution path [in_progress]", |
| 2714 | "2. Ask only when strategy or scope changes [pending]", |
| 2715 | "3. Show the old and proposed plan before deciding [pending]", |
| 2716 | "4. Run the recovery regression suite [pending]", |
| 2717 | ].join("\n"), |
| 2718 | }, |
| 2719 | }, |
| 2720 | }); |
| 2721 | return; |
| 2722 | } |
| 2723 | if ( |
| 2724 | trimmedInput === "/sandbox-escape-preview" || |
| 2725 | trimmedInput === "sandbox escape preview" || |
| 2726 | trimmedInput === "sandbox_escape preview" || |
| 2727 | trimmedInput === "sandbox escape预览" |
| 2728 | ) { |
| 2729 | pendingApprovalPreview = true; |
| 2730 | pendingApprovalPreviewPrompt = { id: "mock-sandbox-escape-preview", tool: "sandbox_escape" }; |
| 2731 | await delay(250); |
| 2732 | if (cancelled) return; |
| 2733 | emitMockPrompt({ |
| 2734 | kind: "approval_request", |
| 2735 | approval: { |
| 2736 | id: "mock-sandbox-escape-preview", |
| 2737 | tool: "sandbox_escape", |
| 2738 | subject: t("mock.sandboxEscapeSubject"), |
| 2739 | reason: t("mock.sandboxEscapeReason"), |
| 2740 | }, |
| 2741 | }); |
| 2742 | return; |
| 2743 | } |
| 2744 | if (decisionSurfaceMock === "plan_approval") { |
| 2745 | pendingApprovalPreview = true; |
| 2746 | pendingApprovalPreviewPrompt = { id: "mock-plan-approval-preview", tool: "exit_plan_mode" }; |
| 2747 | await delay(250); |
| 2748 | if (cancelled) return; |
| 2749 | emitMockPrompt({ |
| 2750 | kind: "approval_request", |
| 2751 | approval: { |
| 2752 | id: "mock-plan-approval-preview", |
| 2753 | tool: "exit_plan_mode", |
| 2754 | subject: "", |
| 2755 | }, |
| 2756 | }); |
| 2757 | return; |
| 2758 | } |
| 2759 | if (isLongDecisionOptionsMockInput(trimmedInput)) { |
| 2760 | pendingAskPreview = true; |
| 2761 | await delay(250); |
| 2762 | if (cancelled) return; |
| 2763 | |
| 2764 | const longDescription = (...parts: string[]) => [...parts, ...parts, ...parts].join(" "); |
| 2765 | const longLabel = (...parts: string[]) => [...parts, ...parts, ...parts].join(" · "); |
| 2766 | const q1Option1Description = t("mock.askQ1Opt1Desc"); |
| 2767 | const q1Option2Description = t("mock.askQ1Opt2Desc"); |
| 2768 | const q1Option3Description = t("mock.askQ1Opt3Desc"); |
| 2769 | const q2Option1Description = t("mock.askQ2Opt1Desc"); |
| 2770 | const q2Option2Description = t("mock.askQ2Opt2Desc"); |
| 2771 | const q2Option3Description = t("mock.askQ2Opt3Desc"); |
| 2772 | const allowOnceDescription = t("approval.allowOnceDesc"); |
| 2773 | const denyDescription = t("approval.denyDesc"); |
| 2774 | |
| 2775 | emitMockPrompt({ |
| 2776 | kind: "ask_request", |
| 2777 | ask: { |
| 2778 | id: "mock-long-options", |
| 2779 | questions: [ |
| 2780 | { |
| 2781 | id: "long-options", |
| 2782 | header: `${t("mock.askQ1Header")} · QA`, |
| 2783 | prompt: `${t("mock.askQ1Prompt")} ${t("mock.askQ2Prompt")}`, |
| 2784 | options: [ |
| 2785 | { |
| 2786 | label: t("mock.askQ1Opt1Label"), |
| 2787 | description: longDescription(q1Option1Description, q2Option1Description, allowOnceDescription), |
| 2788 | }, |
| 2789 | { |
| 2790 | label: t("mock.askQ1Opt2Label"), |
| 2791 | description: longDescription(q1Option2Description, q2Option2Description, denyDescription), |
| 2792 | }, |
| 2793 | { |
| 2794 | label: t("mock.askQ1Opt3Label"), |
| 2795 | description: longDescription(q1Option3Description, q2Option3Description, allowOnceDescription), |
| 2796 | }, |
| 2797 | { |
| 2798 | // Deliberately omit description here: this exercises the |
| 2799 | // legacy/malformed payload fallback where the complete |
| 2800 | // decision was placed in label instead of split into a |
| 2801 | // short label plus supporting description. |
| 2802 | label: longLabel( |
| 2803 | t("mock.askQ2Opt1Label"), |
| 2804 | t("mock.askQ1Opt3Label"), |
| 2805 | t("mock.askQ2Opt3Label"), |
| 2806 | t("mock.askQ1Opt1Label"), |
| 2807 | ), |
| 2808 | }, |
| 2809 | { |
| 2810 | label: t("mock.askQ2Opt2Label"), |
| 2811 | description: longDescription( |
| 2812 | q2Option2Description, |
| 2813 | "DecisionSurfacePreviewWithAnExtremelyLongUnbrokenIdentifierForOverflowVerification0123456789", |
| 2814 | q1Option1Description, |
| 2815 | ), |
| 2816 | }, |
| 2817 | { |
| 2818 | label: t("mock.askQ2Opt3Label"), |
| 2819 | description: longDescription(q2Option3Description, q1Option1Description, q2Option2Description), |
| 2820 | }, |
| 2821 | { |
| 2822 | label: t("approval.deny"), |
| 2823 | description: longDescription(denyDescription, q1Option2Description, q2Option1Description), |
| 2824 | }, |
| 2825 | ], |
| 2826 | }, |
| 2827 | ], |
| 2828 | }, |
| 2829 | }); |
| 2830 | return; |
| 2831 | } |
| 2832 | if (decisionSurfaceMock === "ask") { |
| 2833 | pendingAskPreview = true; |
| 2834 | await delay(250); |
| 2835 | if (cancelled) return; |
| 2836 | emitMockPrompt({ |
| 2837 | kind: "ask_request", |
| 2838 | ask: { |
| 2839 | id: `mock-ask-preview-${Date.now()}`, |
| 2840 | questions: [ |
| 2841 | { |
| 2842 | id: "q1", |
| 2843 | header: t("mock.askQ1Header"), |
| 2844 | prompt: t("mock.askQ1Prompt"), |
| 2845 | options: [ |
| 2846 | { label: t("mock.askQ1Opt1Label"), description: t("mock.askQ1Opt1Desc") }, |
| 2847 | { label: t("mock.askQ1Opt2Label"), description: t("mock.askQ1Opt2Desc") }, |
| 2848 | { label: t("mock.askQ1Opt3Label"), description: t("mock.askQ1Opt3Desc") }, |
| 2849 | ], |
| 2850 | }, |
| 2851 | { |
| 2852 | id: "q2", |
| 2853 | header: t("mock.askQ2Header"), |
| 2854 | prompt: t("mock.askQ2Prompt"), |
| 2855 | options: [ |
| 2856 | { label: t("mock.askQ2Opt1Label"), description: t("mock.askQ2Opt1Desc") }, |
| 2857 | { label: t("mock.askQ2Opt2Label"), description: t("mock.askQ2Opt2Desc") }, |
| 2858 | { label: t("mock.askQ2Opt3Label"), description: t("mock.askQ2Opt3Desc") }, |
| 2859 | ], |
| 2860 | }, |
| 2861 | ], |
| 2862 | }, |
| 2863 | }); |
| 2864 | return; |
| 2865 | } |
| 2866 | if (trimmedInput === "/todo-preview" || trimmedInput === "todo preview" || trimmedInput === "todo预览") { |
| 2867 | await delay(250); |
| 2868 | if (cancelled) return; |
| 2869 | emit({ |
| 2870 | kind: "tool_dispatch", |
| 2871 | tool: { |
| 2872 | id: "mock-todo-preview", |
| 2873 | name: "todo_write", |
| 2874 | args: JSON.stringify({ |
| 2875 | todos: [ |
| 2876 | { content: t("mock.todo1"), status: "completed" }, |
| 2877 | { content: t("mock.todo2"), status: "in_progress" }, |
| 2878 | { content: t("mock.todo3"), status: "pending" }, |
| 2879 | ], |
| 2880 | }), |
| 2881 | readOnly: false, |
| 2882 | }, |
| 2883 | }); |
| 2884 | await delay(150); |
| 2885 | emit({ |
| 2886 | kind: "tool_result", |
| 2887 | tool: { |
| 2888 | id: "mock-todo-preview", |
| 2889 | name: "todo_write", |
| 2890 | args: JSON.stringify({ |
| 2891 | todos: [ |
| 2892 | { content: t("mock.todo1"), status: "completed" }, |
| 2893 | { content: t("mock.todo2"), status: "in_progress" }, |
| 2894 | { content: t("mock.todo3"), status: "pending" }, |
| 2895 | ], |
| 2896 | }), |
| 2897 | output: "todo list updated", |
| 2898 | readOnly: false, |
| 2899 | durationMs: 150, |
| 2900 | }, |
| 2901 | }); |
| 2902 | emitMockTurnDone(submissionID); |
| 2903 | return; |
| 2904 | } |
| 2905 | if (trimmedInput === "/process-preview" || trimmedInput === "process preview" || trimmedInput === "过程预览") { |
| 2906 | await delay(200); |
| 2907 | if (cancelled) return; |
| 2908 | emit({ kind: "phase", text: "Preparing context" }); |
| 2909 | await delay(120); |
| 2910 | emit({ kind: "notice", level: "info", text: "Loaded project instructions from AGENTS.md." }); |
| 2911 | await delay(120); |
| 2912 | emit({ kind: "notice", level: "warn", text: "Network access is enabled; external results may change over time." }); |
| 2913 | await delay(120); |
| 2914 | emit({ kind: "compaction_started", compaction: { trigger: "manual" } }); |
| 2915 | await delay(320); |
| 2916 | emit({ |
| 2917 | kind: "compaction_done", |
| 2918 | compaction: { |
| 2919 | trigger: "manual", |
| 2920 | messages: 6, |
| 2921 | summary: "Preserved the active task, relevant files, and UI decisions while trimming earlier exploratory context.", |
| 2922 | }, |
| 2923 | }); |
| 2924 | emit({ kind: "message", text: "Process card preview complete." }); |
| 2925 | emitMockTurnDone(submissionID); |
| 2926 | return; |
| 2927 | } |
| 2928 | if (trimmedInput === "/nested-preview" || trimmedInput === "nested preview" || trimmedInput === "嵌套预览") { |
| 2929 | const parentId = "mock-nested-explore"; |
| 2930 | await delay(180); |
| 2931 | if (cancelled) return; |
| 2932 | emit({ |
| 2933 | kind: "reasoning", |
| 2934 | text: "我先快速探索相关文件,再整理这个工具行的视觉层级。", |
| 2935 | }); |
| 2936 | emit({ |
| 2937 | kind: "message", |
| 2938 | text: "", |
| 2939 | reasoning: "我先快速探索相关文件,再整理这个工具行的视觉层级。", |
| 2940 | }); |
| 2941 | emit({ |
| 2942 | kind: "tool_dispatch", |
| 2943 | tool: { |
| 2944 | id: parentId, |
| 2945 | name: "explore", |
| 2946 | args: JSON.stringify({ task: "在 Reasonix 前端中检查工具调用图标和嵌套调用展示" }), |
| 2947 | readOnly: true, |
| 2948 | profile: { model: "mock-reasonix", effort: "high" }, |
| 2949 | }, |
| 2950 | }); |
| 2951 | for (let i = 1; i <= 30; i += 1) { |
| 2952 | if (cancelled) return; |
| 2953 | const id = `mock-nested-${i}`; |
| 2954 | const isSearch = i % 3 === 0; |
| 2955 | const name = isSearch ? "grep" : "read_file"; |
| 2956 | const args = isSearch |
| 2957 | ? { pattern: i % 2 === 0 ? "tool__nested-count" : "explore", path: "desktop/frontend/src" } |
| 2958 | : { path: `desktop/frontend/src/${i % 2 === 0 ? "components/ToolCard.tsx" : "styles.css"}`, offset: i * 10, limit: 40 }; |
| 2959 | emit({ kind: "tool_dispatch", tool: { id, name, args: JSON.stringify(args), readOnly: true, parentId } }); |
| 2960 | emit({ |
| 2961 | kind: "tool_result", |
| 2962 | tool: { |
| 2963 | id, |
| 2964 | name, |
| 2965 | readOnly: true, |
| 2966 | output: isSearch ? "3 matches" : "read 40 lines", |
| 2967 | durationMs: 24 + i, |
| 2968 | }, |
| 2969 | }); |
| 2970 | await delay(18); |
| 2971 | } |
| 2972 | emit({ |
| 2973 | kind: "tool_result", |
| 2974 | tool: { |
| 2975 | id: parentId, |
| 2976 | name: "explore", |
| 2977 | readOnly: true, |
| 2978 | output: "已读 20 个文件 · 搜索 10 个文件", |
| 2979 | durationMs: 61510, |
| 2980 | }, |
| 2981 | }); |
| 2982 | emit({ |
| 2983 | kind: "message", |
| 2984 | text: "Mock nested tool preview complete. The explore row now shows the compass count marker.", |
| 2985 | }); |
| 2986 | emitMockTurnDone(submissionID); |
| 2987 | return; |
| 2988 | } |
| 2989 | // Simulate the server's pre-first-token latency so the deferred user bubble |
| 2990 | // and the "un-send on Esc before any reply" path are observable in browser |
| 2991 | // dev. Bail if cancelled during the wait — nothing was streamed yet. |
| 2992 | await delay(700); |
| 2993 | if (cancelled) return; |
| 2994 | const reasoningChunks = [ |
| 2995 | "我先判断这是浏览器预览环境,所以不会调用真实 kernel。\n", |
| 2996 | "接着模拟 provider 的 reasoning delta:先展示思考过程,再切到正式回复。\n", |
| 2997 | "完成后前端应该把过程区折叠成“已工作 N 秒”。\n", |
| 2998 | ]; |
| 2999 | for (const chunk of reasoningChunks) { |
| 3000 | if (cancelled) return; |
| 3001 | emit({ kind: "reasoning", reasoning: chunk }); |
| 3002 | await delay(520); |
| 3003 | } |
| 3004 | if (cancelled) return; |
| 3005 | await delay(260); |
| 3006 | const reply = |
| 3007 | `You said: **${input}**\n\n` + |
| 3008 | "This is the browser dev mock — the real reply comes from the kernel " + |
| 3009 | "inside the desktop shell. Here's a fenced block to exercise the editor seam:\n\n" + |
| 3010 | "```go\nfunc main() {\n println(\"hello from the mock\")\n}\n```\n"; |
| 3011 | for (const ch of reply) { |
| 3012 | if (cancelled) break; |
| 3013 | emit({ kind: "text", text: ch }); |
| 3014 | await delay(6); |
| 3015 | } |
| 3016 | emit({ kind: "message", text: reply }); |
| 3017 | emit({ |
| 3018 | kind: "tool_dispatch", |
| 3019 | tool: { |
| 3020 | id: "t1", |
| 3021 | name: "edit_file", |
| 3022 | args: '{"path":"main.go","old_string":"println(\\"hi\\")","new_string":"println(\\"hello\\")"}', |
| 3023 | readOnly: false, |
| 3024 | }, |
| 3025 | }); |
| 3026 | await delay(350); |
| 3027 | emit({ |
| 3028 | kind: "tool_result", |
| 3029 | tool: { id: "t1", name: "edit_file", output: "edited main.go", readOnly: false, durationMs: 350 }, |
| 3030 | }); |
| 3031 | emit({ |
| 3032 | kind: "usage", |
| 3033 | usage: { |
| 3034 | promptTokens: 1280, |
| 3035 | completionTokens: 64, |
| 3036 | totalTokens: 1344, |
| 3037 | cacheHitTokens: 1024, |
| 3038 | cacheMissTokens: 256, |
| 3039 | sessionCacheHitTokens: 1024, |
| 3040 | sessionCacheMissTokens: 256, |
| 3041 | cost: 0.0064, |
| 3042 | currency: "¥", |
| 3043 | currencyCode: "CNY", |
| 3044 | costComplete: true, |
| 3045 | displayComplete: true, |
| 3046 | displayStatus: "matched", |
| 3047 | costQuote: { |
| 3048 | original: { amount: "0.0064", currency: "CNY" }, |
| 3049 | selected: { amount: "0.0064", currency: "CNY" }, |
| 3050 | estimated: true, |
| 3051 | costComplete: true, |
| 3052 | displayComplete: true, |
| 3053 | complete: true, |
| 3054 | displayStatus: "matched", |
| 3055 | aggregateMode: "single_currency", |
| 3056 | modelRef: "deepseek-official/deepseek-v4-flash", |
| 3057 | rateBand: "off_peak", |
| 3058 | ratedAt: "2026-08-17T00:30:00Z", |
| 3059 | }, |
| 3060 | }, |
| 3061 | }); |
| 3062 | emitMockTurnDone(submissionID); |
| 3063 | }, |
| 3064 | async SubmitToTab(_tabID, input) { await withMockTabScope(_tabID, () => this.Submit(input)); }, |
| 3065 | async SubmitToTabWithID(_tabID, input, submissionID) { const submit = this.Submit as (value: string, id?: string) => Promise<void>; await withMockTabScope(_tabID, () => submit(input, submissionID)); }, |
| 3066 | async StartTurnForTab(_tabID, input, submissionID) { |
| 3067 | const turnId = `mock-turn-${submissionID}`; |
| 3068 | const submit = this.Submit as (value: string, id?: string) => Promise<void>; |
| 3069 | void withMockTabScope(_tabID, () => submit(input, submissionID)).catch((error) => { |
| 3070 | console.error("mock turn failed", error); |
| 3071 | }); |
| 3072 | return { turnId, status: "started", disposition: "turn_started", runtimeEpoch: "mock", submissionId: submissionID }; |
| 3073 | }, |
| 3074 | async SubmitDisplay(_display, input) { await this.Submit(input); }, |
| 3075 | async SubmitDisplayToTab(_tabID, display, input) { await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input)); }, |
| 3076 | async SubmitDisplayToTabWithID(_tabID, _display, input, submissionID) { await this.SubmitToTabWithID(_tabID, input, submissionID); }, |
| 3077 | async SubmitDeliveryRecoveryToTab(_tabID, display, input) { await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input)); }, |
| 3078 | async SubmitDeliveryRecoveryToTabWithID(_tabID, _display, input, submissionID) { await this.SubmitToTabWithID(_tabID, input, submissionID); }, |
| 3079 | async SubmitInvocationsToTab(_tabID, display, input, _invocations) { await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input)); }, |
| 3080 | async SubmitInvocationsToTabWithID(_tabID, _display, input, _invocations, submissionID) { await this.SubmitToTabWithID(_tabID, input, submissionID); }, |
| 3081 | async SubmitInitialGoalToTab( |
| 3082 | _tabID, |
| 3083 | goal, |
| 3084 | display, |
| 3085 | input, |
| 3086 | invocations, |
| 3087 | _collaborationMode, |
| 3088 | _toolApprovalMode, |
| 3089 | ) { |
| 3090 | return await withMockTabScope(_tabID, async () => { |
| 3091 | await this.SetGoalForTab(_tabID, goal); |
| 3092 | if (invocations.length > 0) { |
| 3093 | await this.SubmitInvocationsToTab(_tabID, display, input, invocations); |
| 3094 | return []; |
| 3095 | } |
| 3096 | await this.SubmitDisplayToTab(_tabID, display, input); |
| 3097 | return []; |
| 3098 | }); |
| 3099 | }, |
| 3100 | async SubmitInitialGoalToTabWithID(_tabID, goal, display, input, invocations, _collaborationMode, _toolApprovalMode, submissionID) { await this.SetGoalForTab(_tabID, goal); if (invocations.length > 0) await this.SubmitInvocationsToTabWithID(_tabID, display, input, invocations, submissionID); else await this.SubmitDisplayToTabWithID(_tabID, display, input, submissionID); return []; }, |
| 3101 | async SubmitEditedDisplayToTab(_tabID, display, input, _original) { await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input)); }, |
| 3102 | async SubmitEditedDisplayToTabWithID(_tabID, display, input, _original, submissionID) { await this.SubmitDisplayToTabWithID(_tabID, display, input, submissionID); }, |
| 3103 | async RunShellForTab(_tabID, command) { |
| 3104 | await withMockTabScope(_tabID, async () => { |
| 3105 | cancelled = false; |
| 3106 | emitMockTurnStarted(); |
| 3107 | await delay(100); |
| 3108 | if (cancelled) return; |
| 3109 | const id = `shell-${command.slice(0, 32)}`; |
| 3110 | emit({ kind: "tool_dispatch", tool: { id, name: "bash", args: JSON.stringify({ command }), readOnly: false } }); |
| 3111 | await delay(200); |
| 3112 | if (cancelled) return; |
| 3113 | emit({ kind: "tool_progress", tool: { id, name: "bash", output: `$ ${command}\n(mock output)\n`, readOnly: false } }); |
| 3114 | await delay(100); |
| 3115 | if (cancelled) return; |
| 3116 | emit({ kind: "tool_result", tool: { id, name: "bash", output: `$ ${command}\n(mock output)\n`, readOnly: false, durationMs: 300 } }); |
| 3117 | emitMockTurnDone(); |
| 3118 | }); |
| 3119 | }, |
| 3120 | async Steer(_text) { |
| 3121 | // Mock: emit a steer event as confirmation in the transcript. |
| 3122 | emit({ kind: "steer", text: _text }); |
| 3123 | }, |
| 3124 | async SteerForTab(_tabID, _text) { |
| 3125 | await this.Steer(_text); |
| 3126 | }, |
| 3127 | async InboxSnapshot(_tabID) { if (recoveryMock) return (await import("./inboxRecoveryPreview")).inboxRecoveryPreviewSnapshot(); |
| 3128 | return { |
| 3129 | revision: 0, |
| 3130 | paused: false, |
| 3131 | recovered: false, sessionPath: "", |
| 3132 | items: [], |
| 3133 | itemsCount: 0, |
| 3134 | bytes: 0, |
| 3135 | maxItems: 64, |
| 3136 | maxBytes: 64 * 1024 * 1024, |
| 3137 | }; |
| 3138 | }, |
| 3139 | async EnqueueInboxFollowup(_tabID, _display, _submit, _idempotency) { |
| 3140 | return { itemId: `mock-${Date.now()}`, disposition: "queued_followup", position: 1, paused: false }; |
| 3141 | }, |
| 3142 | async EnqueueInboxFollowupWithInvocations(_tabID, _display, _submit, _invocations, _idempotency) { |
| 3143 | return { itemId: `mock-invocation-${Date.now()}`, disposition: "queued_followup", position: 1, paused: false }; |
| 3144 | }, |
| 3145 | async EnqueueInboxSteer(_tabID, display, submit, _idempotency) { |
| 3146 | const itemId = `mock-steer-${Date.now()}`; |
| 3147 | emit({ kind: "steer", text: submit || display, itemId }); |
| 3148 | return { itemId, disposition: "steer_accepted", position: 1, paused: false }; |
| 3149 | }, |
| 3150 | async SteerInboxItem(_tabID, itemID) { |
| 3151 | emit({ kind: "steer", itemId: itemID }); |
| 3152 | return { itemId: itemID, disposition: "steer_accepted", position: 1, paused: false }; |
| 3153 | }, |
| 3154 | async ReadInboxItem(_tabID, id) { |
| 3155 | return { id, displayText: "", rawText: "", submitText: "" }; |
| 3156 | }, |
| 3157 | async UpdateInboxItem() {}, |
| 3158 | async DeleteInboxItem() {}, |
| 3159 | async MoveInboxItem() {}, |
| 3160 | async SetInboxPaused(_tabID, paused) { if (recoveryMock) (await import("./inboxRecoveryPreview")).setInboxRecoveryPreviewPaused(paused); }, |
| 3161 | async RetryInboxItem() {}, |
| 3162 | async RefreshInboxItem() {}, |
| 3163 | async InboxHasItems() { return recoveryMock; }, |
| 3164 | async Cancel() { |
| 3165 | cancelled = true; |
| 3166 | emitMockTurnDone(); |
| 3167 | }, |
| 3168 | async CancelTab(_tabID) { |
| 3169 | await withMockTabScope(_tabID, () => this.Cancel()); |
| 3170 | }, |
| 3171 | async CancelSessionForTab(_tabID) { |
| 3172 | await withMockTabScope(_tabID, () => this.Cancel()); |
| 3173 | return { sessionRef: "", headId: "", runtimeEpoch: "mock", accepted: true, alreadyIdle: false, recoveryRequired: false }; |
| 3174 | }, |
| 3175 | async CancelTabWithInboxItems(_tabID, _itemIDs) { |
| 3176 | await withMockTabScope(_tabID, () => this.Cancel()); |
| 3177 | }, |
| 3178 | async CancelTabWithInboxItemsResult(_tabID, itemIDs) { await withMockTabScope(_tabID, () => this.Cancel()); return { discardedItemIds: [...itemIDs] }; }, |
| 3179 | async Approve(_id, allow, session, persist) { |
| 3180 | if (!pendingApprovalPreview) return; |
| 3181 | pendingApprovalPreview = false; |
| 3182 | pendingApprovalPreviewPrompt = undefined; |
| 3183 | const suffix = persist ? "grant saved" : session ? "grant active this session" : "allowed once"; |
| 3184 | emit({ |
| 3185 | kind: "message", |
| 3186 | text: `approval preview answered: ${allow ? suffix : "denied"}`, |
| 3187 | }); |
| 3188 | emitMockTurnDone(); |
| 3189 | }, |
| 3190 | async ApproveTab(_tabID, id, allow, session, persist) { |
| 3191 | await withMockTabScope(_tabID, () => this.Approve(id, allow, session, persist)); |
| 3192 | }, |
| 3193 | async ResolvePlanDecision(id, action) { |
| 3194 | const active = mockTabs.find((tab) => tab.active); |
| 3195 | await this.ResolvePlanDecisionTab(active?.id ?? "", id, action); |
| 3196 | }, |
| 3197 | async ResolvePlanDecisionTab(_tabID, id, action) { |
| 3198 | await withMockTabScope(_tabID, async () => { |
| 3199 | void id; |
| 3200 | pendingApprovalPreview = false; |
| 3201 | pendingApprovalPreviewPrompt = undefined; |
| 3202 | emit({ |
| 3203 | kind: "message", |
| 3204 | text: `plan preview answered: ${action}`, |
| 3205 | }); |
| 3206 | emitMockTurnDone(); |
| 3207 | }); |
| 3208 | }, |
| 3209 | async ResolveRecovery(id, action, feedback) { |
| 3210 | const active = mockTabs.find((tab) => tab.active); |
| 3211 | await this.ResolveRecoveryTab(active?.id ?? "", id, action, feedback); |
| 3212 | }, |
| 3213 | async ResolveRecoveryTab(_tabID, id, action, feedback) { |
| 3214 | void id; |
| 3215 | void feedback; |
| 3216 | pendingApprovalPreview = false; |
| 3217 | pendingApprovalPreviewPrompt = undefined; |
| 3218 | emit({ |
| 3219 | kind: "message", |
| 3220 | text: `recovery preview answered: ${action}`, |
| 3221 | }); |
| 3222 | emitMockTurnDone(); |
| 3223 | }, |
| 3224 | async SetRecoveryCheckpointEnabled(_enabled) {}, |
| 3225 | async SetRecoveryCheckpointEnabledTab(_tabID, _enabled) {}, |
| 3226 | async RecoveryCheckpointEnabled() { |
| 3227 | return true; |
| 3228 | }, |
| 3229 | async RecoveryCheckpointEnabledTab(_tabID) { |
| 3230 | return true; |
| 3231 | }, |
| 3232 | async AnswerQuestion(_id, answers) { |
| 3233 | if (!pendingAskPreview) return; |
| 3234 | pendingAskPreview = false; |
| 3235 | const summary = answers |
| 3236 | .map((answer) => `${answer.questionId}: ${(answer.selected ?? []).join(", ") || "(no answer)"}`) |
| 3237 | .join("\n"); |
| 3238 | emit({ kind: "message", text: `ask preview answered:\n\n${summary}` }); |
| 3239 | emitMockTurnDone(); |
| 3240 | }, |
| 3241 | async AnswerMCPInteractionForTab(_tabID, id, _action, _content) { |
| 3242 | if (!cancelled && (await import("./mockMCPInteraction")).consumeMockMCPInteraction(id)) await withMockTabScope(_tabID, async () => { emit({ kind: "prompt_answered", itemId: id }); emitMockTurnDone(); }); |
| 3243 | }, |
| 3244 | ...makeMockMCPAppBindings(), ...makeMockPinnedContextBindings(), |
| 3245 | async AnswerQuestionForTab(_tabID, id, answers) { |
| 3246 | await withMockTabScope(_tabID, () => this.AnswerQuestion(id, answers)); |
| 3247 | }, |
| 3248 | async ResolvePromptForTab(tabID, promptID, _turnID, _runtimeEpoch, kind, answer) { |
| 3249 | await withMockTabScope(tabID, async () => { |
| 3250 | if (kind === "ask") return this.AnswerQuestion(promptID, answer.questions ?? []); |
| 3251 | if (kind === "approval") return this.Approve(promptID, Boolean(answer.allow), Boolean(answer.session), Boolean(answer.persist)); |
| 3252 | if (kind === "plan") return this.ResolvePlanDecisionTab(tabID, promptID, (answer.action ?? "exit_plan") as "start_execution" | "revise_plan" | "exit_plan"); |
| 3253 | if (kind === "recovery") return this.ResolveRecoveryTab(tabID, promptID, answer.action ?? "revise", answer.feedback ?? ""); |
| 3254 | if (kind === "mcp") return this.AnswerMCPInteractionForTab(tabID, promptID, (answer.action ?? "cancel") as "accept" | "decline" | "cancel", answer.content ?? null); |
| 3255 | throw new Error(`unsupported prompt kind: ${kind}`); |
| 3256 | }); |
| 3257 | }, |
| 3258 | async ResolvePromptForSession(target, answer) { await this.ResolvePromptForTab?.(target.tabId, target.promptId, target.turnId, target.runtimeEpoch, target.kind, answer); }, |
| 3259 | async ReplayPendingPrompts() {}, |
| 3260 | async ReplayPendingPromptsForTab(_tabID) {}, |
| 3261 | async ConfirmAction(req) { |
| 3262 | void req; |
| 3263 | return false; |
| 3264 | }, |
| 3265 | async SetPlanMode(on) { |
| 3266 | const active = mockTabs.find((tab) => tab.active); |
| 3267 | if (active) await this.SetModeForTab(active.id, modeWithPlan(normalizeMode(active.mode), on)); |
| 3268 | }, |
| 3269 | async SetMode(mode) { |
| 3270 | const active = mockTabs.find((tab) => tab.active); |
| 3271 | if (active) await this.SetModeForTab(active.id, mode); |
| 3272 | }, |
| 3273 | async SetModeForTab(tabID, mode) { |
| 3274 | const nextMode = normalizeMode(mode); |
| 3275 | let nextToolApprovalMode: ToolApprovalMode | "" = ""; |
| 3276 | mockTabs = mockTabs.map((tab) => { |
| 3277 | if (tab.id !== tabID) return tab; |
| 3278 | nextToolApprovalMode = mockToolApprovalModeAfterModeChange(tab.toolApprovalMode, nextMode); |
| 3279 | return { |
| 3280 | ...tab, |
| 3281 | mode: nextMode, |
| 3282 | collaborationMode: normalizeCollaborationMode(undefined, tab.goal, nextMode), |
| 3283 | toolApprovalMode: nextToolApprovalMode, |
| 3284 | }; |
| 3285 | }); |
| 3286 | return drainMockApprovalPreviews(nextToolApprovalMode); |
| 3287 | }, |
| 3288 | async SetCollaborationModeForTab(tabID, mode) { |
| 3289 | const next = normalizeCollaborationMode(mode); |
| 3290 | mockTabs = mockTabs.map((tab) => { |
| 3291 | if (tab.id !== tabID) return tab; |
| 3292 | const toolMode = normalizeToolApprovalMode(tab.toolApprovalMode, normalizeMode(tab.mode)); |
| 3293 | return { |
| 3294 | ...tab, |
| 3295 | collaborationMode: next, |
| 3296 | goal: next === "normal" || next === "plan" ? "" : tab.goal, |
| 3297 | mode: modeWithPlan(modeWithAutoApproveTools(normalizeMode(tab.mode), toolMode === "danger-full-access"), next === "plan"), |
| 3298 | }; |
| 3299 | }); |
| 3300 | }, |
| 3301 | async SetToolApprovalMode(mode) { |
| 3302 | const active = mockTabs.find((tab) => tab.active); |
| 3303 | if (active) await this.SetToolApprovalModeForTab(active.id, mode); |
| 3304 | }, |
| 3305 | async SetToolApprovalModeForTab(tabID, mode) { |
| 3306 | const next = normalizeToolApprovalMode(mode); |
| 3307 | settings.autoApproveTools = next === "danger-full-access"; |
| 3308 | settings.bypass = next === "danger-full-access"; |
| 3309 | mockTabs = mockTabs.map((tab) => |
| 3310 | tab.id === tabID |
| 3311 | ? { |
| 3312 | ...tab, |
| 3313 | toolApprovalMode: next, |
| 3314 | mode: modeWithAutoApproveTools(normalizeMode(tab.mode), next === "danger-full-access"), |
| 3315 | } |
| 3316 | : tab, |
| 3317 | ); |
| 3318 | return drainMockApprovalPreviews(next); |
| 3319 | }, |
| 3320 | async PermissionSnapshotForTab(tabID) { |
| 3321 | const tab = mockTabs.find((candidate) => candidate.id === tabID); |
| 3322 | if (!tab) throw new Error("tab not found"); |
| 3323 | return { |
| 3324 | sessionId: tab.sessionPath || tab.id, |
| 3325 | generation: 1, |
| 3326 | revision: Number((tab as TabMeta & { permissionRevision?: number }).permissionRevision ?? 1), |
| 3327 | preset: normalizeToolApprovalMode(tab.toolApprovalMode), |
| 3328 | workspaceRoot: tab.workspaceRoot || tab.cwd || "", |
| 3329 | grants: [], |
| 3330 | capabilities: { |
| 3331 | backend: "mock", |
| 3332 | enforcement: "full", |
| 3333 | supportedPresets: ["read-only", "workspace-write", "danger-full-access"], |
| 3334 | }, |
| 3335 | }; |
| 3336 | }, |
| 3337 | async SetPermissionPresetForTab(tabID, preset, expectedRevision) { |
| 3338 | const current = await this.PermissionSnapshotForTab(tabID); |
| 3339 | if (current.revision !== expectedRevision) throw new Error("permission revision changed"); |
| 3340 | await this.SetToolApprovalModeForTab(tabID, preset); |
| 3341 | mockTabs = mockTabs.map((tab) => tab.id === tabID ? { ...tab, permissionRevision: current.revision + 1 } as TabMeta : tab); |
| 3342 | return { ...current, revision: current.revision + 1, preset: normalizeToolApprovalMode(preset) }; |
| 3343 | }, |
| 3344 | async RevokePermissionGrantForTab(tabID, _scope, _target, expectedRevision) { |
| 3345 | const current = await this.PermissionSnapshotForTab(tabID); |
| 3346 | if (current.revision !== expectedRevision) throw new Error("permission revision changed"); |
| 3347 | return { ...current, revision: current.revision + 1, grants: [] }; |
| 3348 | }, |
| 3349 | async SetComposerProfileForTab(tabID, collaborationMode, toolApprovalMode, goal) { |
| 3350 | const nextCollaboration = normalizeCollaborationMode(collaborationMode); |
| 3351 | const nextToolApproval = normalizeToolApprovalMode(toolApprovalMode); |
| 3352 | const nextGoal = goal.trim(); |
| 3353 | settings.autoApproveTools = nextToolApproval === "danger-full-access"; |
| 3354 | settings.bypass = nextToolApproval === "danger-full-access"; |
| 3355 | mockTabs = mockTabs.map((tab) => { |
| 3356 | if (tab.id !== tabID) return tab; |
| 3357 | const plan = !nextGoal && nextCollaboration === "plan"; |
| 3358 | return { |
| 3359 | ...tab, |
| 3360 | collaborationMode: nextGoal ? "goal" : plan ? "plan" : "normal", |
| 3361 | toolApprovalMode: nextToolApproval, |
| 3362 | goal: nextGoal, |
| 3363 | goalStatus: nextGoal ? "running" : "stopped", |
| 3364 | mode: modeWithAutoApproveTools(modeWithPlan(normalizeMode(tab.mode), plan), nextToolApproval === "danger-full-access"), |
| 3365 | }; |
| 3366 | }); |
| 3367 | return drainMockApprovalPreviews(nextToolApproval); |
| 3368 | }, |
| 3369 | async SetGoal(goal) { const active = mockTabs.find((tab) => tab.active); if (active) await this.SetGoalForTab(active.id, goal); }, |
| 3370 | async SetGoalForTab(tabID, goal) { |
| 3371 | const nextGoal = goal.trim(); |
| 3372 | mockTabs = mockTabs.map((tab) => |
| 3373 | tab.id === tabID |
| 3374 | ? { |
| 3375 | ...tab, |
| 3376 | goal: nextGoal, |
| 3377 | goalStatus: nextGoal ? "running" : "stopped", |
| 3378 | collaborationMode: nextGoal ? "goal" : "normal", |
| 3379 | mode: modeWithPlan(normalizeMode(tab.mode), false), |
| 3380 | } |
| 3381 | : tab, |
| 3382 | ); |
| 3383 | }, |
| 3384 | async EditGoalForTab(tabID, objective, maxGoalRounds) { mockTabs = mockTabs.map((tab) => editMockGoalTab(tab, tabID, objective, maxGoalRounds)); }, |
| 3385 | async ResumeGoalForTab(tabID) { |
| 3386 | let resumed = false; |
| 3387 | mockTabs = mockTabs.map((tab) => { |
| 3388 | if (tab.id !== tabID || !tab.goal || tab.goalStatus === "complete") return tab; |
| 3389 | resumed = true; |
| 3390 | return { ...tab, goalStatus: "running", collaborationMode: "goal", goalRuntime: undefined }; |
| 3391 | }); |
| 3392 | return resumed; |
| 3393 | }, |
| 3394 | async PauseGoalForTab(tabID) { |
| 3395 | let paused = false; |
| 3396 | mockTabs = mockTabs.map((tab) => { |
| 3397 | if (tab.id !== tabID || !tab.goal || tab.goalStatus !== "running") return tab; |
| 3398 | paused = true; |
| 3399 | return { ...tab, goalStatus: "blocked", goalRuntime: undefined }; |
| 3400 | }); |
| 3401 | return paused; |
| 3402 | }, |
| 3403 | async ClearGoalForTab(tabID) { await this.SetGoalForTab(tabID, ""); }, |
| 3404 | async Compact() {}, |
| 3405 | async CompactForTab() {}, |
| 3406 | async NewSession() {}, |
| 3407 | async NewSessionForTab() {}, |
| 3408 | async ClearSession() { return { sessionPath: "", sessionGeneration: 0 }; }, |
| 3409 | async ClearSessionForTab() { return { sessionPath: "", sessionGeneration: 0 }; }, |
| 3410 | async Checkpoints() { |
| 3411 | return [ |
| 3412 | { turn: 0, prompt: "你好呀", files: ["src/App.tsx"], fileCount: 1, turnFileCount: 1, time: Date.now() - 30_000, canCode: true, canConversation: true }, |
| 3413 | ]; |
| 3414 | }, |
| 3415 | async CheckpointsForTab() { return this.Checkpoints(); }, |
| 3416 | async Rewind() {}, |
| 3417 | async RewindForTab() {}, |
| 3418 | async PreviewRewindForTab() { |
| 3419 | return { ok: true, canFiles: true, canConversation: true, planId: "mock", fileCount: 0 }; |
| 3420 | }, |
| 3421 | async CommitRewindForTab() { |
| 3422 | return { ok: true, undoAvailable: true, transactionId: "mock-tx" }; |
| 3423 | }, |
| 3424 | async UndoRewindForTab() { |
| 3425 | return { ok: true, undoAvailable: false }; |
| 3426 | }, |
| 3427 | async PreviewWorkspaceFileRevertForTab(_tabID, path) { |
| 3428 | return { ok: true, canFiles: true, path, planId: "mock-file" }; |
| 3429 | }, |
| 3430 | async CommitWorkspaceFileRevertForTab() { |
| 3431 | return { ok: true, undoAvailable: true, transactionId: "mock-file-tx" }; |
| 3432 | }, |
| 3433 | ...makeMockForkBindings( |
| 3434 | () => mockTabs, |
| 3435 | (tabs) => { mockTabs = tabs; }, |
| 3436 | t("rewind.fork"), |
| 3437 | (tabID) => getMock().HistoryForTab(tabID), |
| 3438 | ), |
| 3439 | async SummarizeFrom() {}, |
| 3440 | async SummarizeFromForTab() {}, |
| 3441 | async SummarizeUpTo() {}, |
| 3442 | async SummarizeUpToForTab() {}, |
| 3443 | async History() { |
| 3444 | return []; |
| 3445 | }, |
| 3446 | async HistoryForTab(tabID?: string) { |
| 3447 | const tab = mockTabs.find((item) => item.id === tabID) ?? mockTabs.find((item) => item.active); |
| 3448 | if (tab?.topicId) { |
| 3449 | queueMockTopicRuntime(tab); |
| 3450 | if (benchFixturesPromise) { |
| 3451 | const fixtures = await benchFixturesPromise; |
| 3452 | const history = fixtures.benchTopicHistory(tab.topicId); |
| 3453 | if (history) return history; |
| 3454 | } |
| 3455 | return withMockHistoryIds(tab.id, topicHistoryFixture(t0, tab.topicId)); |
| 3456 | } |
| 3457 | return withMockHistoryIds(tabID ?? "mock", await this.History()); |
| 3458 | }, |
| 3459 | async HistoryPage(beforeTurn = 0, limit = 60) { |
| 3460 | return mockHistoryPage(await this.History(), beforeTurn, limit); |
| 3461 | }, |
| 3462 | async HistoryPageForTab(tabID: string, beforeTurn = 0, limit = 60) { |
| 3463 | return mockHistoryPage(await this.HistoryForTab(tabID), beforeTurn, limit); |
| 3464 | }, |
| 3465 | async HistoryCheckpointTurnsForTab(tabID: string) { |
| 3466 | const turns: number[] = []; |
| 3467 | for (const message of await this.HistoryForTab(tabID)) { |
| 3468 | if (message.role !== "user") continue; |
| 3469 | turns.push(message.checkpointTurn ?? turns.length); |
| 3470 | } |
| 3471 | return turns; |
| 3472 | }, |
| 3473 | async HistorySliceForTab(tabID: string, req: HistorySliceRequest) { |
| 3474 | return mockHistorySlice(tabID, await this.HistoryForTab(tabID), req, benchMock); |
| 3475 | }, |
| 3476 | async HistoryContentForTab(tabID: string, ref: HistoryContentRef, chunkIndex: number): Promise<HistoryContentChunk> { |
| 3477 | const out: HistoryContentChunk = { entryId: ref.entryId, field: ref.field, chunk: Math.max(0, chunkIndex), chunks: 1, data: "", done: true, stale: false }; |
| 3478 | const match = /:m(\d+):o\d+$/.exec(ref.entryId); |
| 3479 | if (!match) return out; |
| 3480 | const messages = await this.HistoryForTab(tabID); |
| 3481 | const message = messages[Number(match[1])]; |
| 3482 | if (benchMock && message?.content?.includes("ASYNC LAYOUT EXPANSION COMPLETE")) await delay((await benchFixturesPromise)!.benchHydrationDelay()); |
| 3483 | // Storm fixture: pace ref resolutions deterministically by entry |
| 3484 | // index so opening the session produces a seconds-long patch storm |
| 3485 | // instead of a single burst (#8657). |
| 3486 | if (benchMock && (message?.content?.includes("BENCH STORM HYDRATION RESOLVED") || message?.reasoning?.includes("BENCH STORM HYDRATION RESOLVED"))) { |
| 3487 | await delay(50 + (Number(match[1]) % 24) * 120); |
| 3488 | } |
| 3489 | if (!message) return { ...out, stale: true }; |
| 3490 | out.data = mockHistoryContentField(message, ref); |
| 3491 | out.chunks = 1; |
| 3492 | return out; |
| 3493 | }, |
| 3494 | ...makeMockSessionReaderBindings(), |
| 3495 | async ListSessions() { |
| 3496 | return sessions.map((s) => ({ ...s })); |
| 3497 | }, |
| 3498 | async ListSessionsForTab() { |
| 3499 | return sessions.map((s) => ({ ...s })); |
| 3500 | }, |
| 3501 | ...makeMockHistoryCatalogBindings(sessions), |
| 3502 | async ListTrashedSessions() { |
| 3503 | return trashedSessions.map((s) => ({ ...s })); |
| 3504 | }, |
| 3505 | async ResumeSession(path: string) { |
| 3506 | sessions.forEach((s) => { |
| 3507 | s.current = s.path === path; |
| 3508 | s.open = s.open || s.path === path; |
| 3509 | }); |
| 3510 | return [ |
| 3511 | { role: "user", content: `(mock) resumed ${path}` }, |
| 3512 | { role: "assistant", content: "This is a mock resumed transcript — the real one comes from the kernel." }, |
| 3513 | ]; |
| 3514 | }, |
| 3515 | async ResumeSessionForTab(_tabID: string, path: string) { |
| 3516 | return this.ResumeSession(path); |
| 3517 | }, |
| 3518 | async ResumeSessionPage(path: string, limit = 60) { |
| 3519 | return mockHistoryPage(await this.ResumeSession(path), 0, limit); |
| 3520 | }, |
| 3521 | async ResumeSessionPageForTab(_tabID: string, path: string, limit = 60) { |
| 3522 | return this.ResumeSessionPage(path, limit); |
| 3523 | }, |
| 3524 | async OpenChannelSessionForTab(tabID: string, path: string) { |
| 3525 | mockTabs = mockTabs.map((tab) => tab.id === tabID ? { ...tab, sessionPath: path, readOnly: true } : tab); |
| 3526 | return this.ResumeSession(path); |
| 3527 | }, |
| 3528 | async OpenChannelSessionPageForTab(tabID: string, path: string, limit = 60) { |
| 3529 | return mockHistoryPage(await this.OpenChannelSessionForTab(tabID, path), 0, limit); |
| 3530 | }, |
| 3531 | async QuerySessionTakeover(_tabId: string) { |
| 3532 | return { available: false, reason: "mock" } as import("./types").SessionTakeoverView; |
| 3533 | }, |
| 3534 | async TakeoverSession(_tabId: string, _mode: "wait" | "interrupt") {}, |
| 3535 | async PreviewSession(path: string) { |
| 3536 | const s = sessions.find((x) => x.path === path) ?? trashedSessions.find((x) => x.path === path); |
| 3537 | return [ |
| 3538 | { role: "user", content: s?.preview || `(mock) preview ${path}` }, |
| 3539 | { role: "phase", content: "Preparing read-only preview" }, |
| 3540 | { |
| 3541 | role: "assistant", |
| 3542 | content: "This is a read-only mock preview. The active conversation is unchanged.", |
| 3543 | reasoning: "Preview reads the saved session without resuming it.", |
| 3544 | }, |
| 3545 | { role: "notice", level: "info", content: "Preview mode keeps the active conversation untouched." }, |
| 3546 | { role: "compaction", content: "", trigger: "manual", messages: 3, summary: "Mock preview preserved the latest task, tool result, and answer summary." }, |
| 3547 | ]; |
| 3548 | }, |
| 3549 | async DeleteSession(path: string) { |
| 3550 | const i = sessions.findIndex((s) => s.path === path); |
| 3551 | if (i >= 0) { |
| 3552 | const [s] = sessions.splice(i, 1); |
| 3553 | trashedSessions.unshift({ |
| 3554 | ...s, |
| 3555 | current: false, |
| 3556 | open: false, |
| 3557 | path: s.path.replace("/mock/sessions/", "/mock/sessions/.trash/"), |
| 3558 | deletedAt: Date.now(), |
| 3559 | }); |
| 3560 | } |
| 3561 | }, |
| 3562 | async DeleteRecoveryCopy(path: string) { |
| 3563 | return this.DeleteSession(path); |
| 3564 | }, |
| 3565 | async GetRecoveryLineage(key) { |
| 3566 | const topic = findMockTopic(key.topicId); |
| 3567 | return { |
| 3568 | groupId: key.topicId, |
| 3569 | state: topic?.recoveryState ?? "normal", |
| 3570 | branchCount: topic?.recoveryBranchCount ?? 0, |
| 3571 | unresolved: topic?.recoveryUnresolvedCount ?? 0, |
| 3572 | cleanupEligible: topic?.recoveryCleanupEligibleCount ?? 0, |
| 3573 | members: [], |
| 3574 | }; |
| 3575 | }, |
| 3576 | async GetSessionVersionState(key) { |
| 3577 | const lineage = await this.GetRecoveryLineage(key); |
| 3578 | return { conversationId: key.topicId, canContinue: true, requiresChoice: lineage.state === "diverged" && lineage.unresolved > 0, lineage }; |
| 3579 | }, |
| 3580 | async SetActiveSessionVersion() {}, |
| 3581 | async RetrySessionRecovery() {}, |
| 3582 | async ReconcileRecoveryVersions() {}, |
| 3583 | async ChooseRecoveryBranch() {}, |
| 3584 | async RenameSessionHead() {}, |
| 3585 | async CleanRecoveryLineage(request) { |
| 3586 | const topic = findMockTopic(request.topicId); |
| 3587 | const eligible = topic?.recoveryCleanupEligibleCount ?? 0; |
| 3588 | if (request.apply && topic) topic.recoveryCleanupEligibleCount = 0; |
| 3589 | return { eligible, moved: request.apply ? eligible : 0, busy: 0, kept: 0, dryRun: !request.apply, items: [] }; |
| 3590 | }, |
| 3591 | async RestoreSession(path: string) { |
| 3592 | const i = trashedSessions.findIndex((s) => s.path === path); |
| 3593 | if (i >= 0) { |
| 3594 | const [s] = trashedSessions.splice(i, 1); |
| 3595 | sessions.unshift({ |
| 3596 | ...s, |
| 3597 | path: s.path.replace("/mock/sessions/.trash/", "/mock/sessions/"), |
| 3598 | deletedAt: undefined, |
| 3599 | }); |
| 3600 | } |
| 3601 | }, |
| 3602 | async PurgeTrashedSession(path: string) { |
| 3603 | const i = trashedSessions.findIndex((s) => s.path === path); |
| 3604 | if (i >= 0) trashedSessions.splice(i, 1); |
| 3605 | }, |
| 3606 | async PurgeRecoveryCopy(path: string) { |
| 3607 | return this.PurgeTrashedSession(path); |
| 3608 | }, |
| 3609 | async RenameSession(path: string, title: string) { |
| 3610 | const s = sessions.find((x) => x.path === path); |
| 3611 | if (s) s.title = title.trim() || undefined; |
| 3612 | }, |
| 3613 | async ScanPromptHistory(nonce: string) { |
| 3614 | // Dev mock returns a static set of sample prompts for UI development. |
| 3615 | const entries: PromptHistoryEntry[] = [ |
| 3616 | { text: "Explain the architecture of this project", at: Date.now() - 60000, sessionPath: "/mock/sessions/arch.jsonl", turn: 0 }, |
| 3617 | { text: "Fix the login button styling", at: Date.now() - 120000, sessionPath: "/mock/sessions/arch.jsonl", turn: 1 }, |
| 3618 | { text: "What is the capital of France?", at: Date.now() - 300000, sessionPath: "/mock/sessions/general.jsonl", turn: 0 }, |
| 3619 | ]; |
| 3620 | return { entries, nonce: "mock-" + nonce, olderCursor: "", hasOlder: false }; |
| 3621 | }, |
| 3622 | async ListWorkspaces() { |
| 3623 | return mockProjectTree |
| 3624 | .filter((node) => node.kind === "project" && node.root) |
| 3625 | .map((node) => ({ |
| 3626 | path: node.root!, |
| 3627 | name: node.label || baseName(node.root!), |
| 3628 | current: node.root === cwd, |
| 3629 | })); |
| 3630 | }, |
| 3631 | async PickWorkspace() { |
| 3632 | // Browser dev has no native dialog; simulate picking a folder and re-root so |
| 3633 | // the topbar folder chip visibly changes. |
| 3634 | return mockSwitchWorkspace(cwd.endsWith("another-project") ? "~/projects/reasonix" : "~/projects/another-project"); |
| 3635 | }, |
| 3636 | async SwitchWorkspace(path: string) { |
| 3637 | return mockSwitchWorkspace(path); |
| 3638 | }, |
| 3639 | async RemoveWorkspace(path: string) { |
| 3640 | workspaces = workspaces.filter((p) => p !== path); |
| 3641 | const index = mockProjectTree.findIndex((node) => node.root === path); |
| 3642 | if (index >= 0) mockProjectTree.splice(index, 1); |
| 3643 | }, |
| 3644 | async ContextUsageForTab() { |
| 3645 | return { |
| 3646 | used: 42124, |
| 3647 | window: 128000, |
| 3648 | sessionTokens: 34479, |
| 3649 | compactRatio: 0.8, |
| 3650 | sessionCost: 0.1287, |
| 3651 | sessionCurrency: "CNY", |
| 3652 | sessionCostQuote: { |
| 3653 | original: { amount: "0.1287", currency: "CNY" }, |
| 3654 | selected: { amount: "0.1287", currency: "CNY" }, |
| 3655 | estimated: true, |
| 3656 | costComplete: true, |
| 3657 | displayComplete: true, |
| 3658 | complete: true, |
| 3659 | displayStatus: "matched", |
| 3660 | aggregateMode: "single_currency", |
| 3661 | rateBand: "mixed", |
| 3662 | }, |
| 3663 | }; |
| 3664 | }, |
| 3665 | async Balance() { |
| 3666 | // Mirror the active mock provider: deepseek-flash carries a balance_url. |
| 3667 | const p = settings.providers.find((x) => x.name === settings.defaultModel); |
| 3668 | if (!p?.balanceUrl) return { available: false, display: "" }; |
| 3669 | return { available: true, display: "¥128.50" }; |
| 3670 | }, |
| 3671 | async BalanceForTab() { |
| 3672 | return this.Balance(); |
| 3673 | }, |
| 3674 | async UsageStats() { |
| 3675 | // Browser dev mock has no stats files; the panel does not consume |
| 3676 | // provider aggregates, so keep this initial-bundle fallback lean. |
| 3677 | return { from: "", to: "", tokens: 0, requests: 0, turns: 0, cacheHit: 0, cacheMiss: 0, activeDays: 0, topModel: "", daily: [], models: [] } as unknown as UsageStatsRange; |
| 3678 | }, |
| 3679 | async Jobs() { |
| 3680 | return []; // browser dev mock has no background jobs |
| 3681 | }, |
| 3682 | async JobsForTab() { |
| 3683 | return this.Jobs(); |
| 3684 | }, |
| 3685 | async CancelJob() { |
| 3686 | return false; |
| 3687 | }, |
| 3688 | async CancelJobForTab(_tabID, jobID) { |
| 3689 | return this.CancelJob(jobID); |
| 3690 | }, |
| 3691 | async CancelJobsForTab(_tabID, jobIDs) { |
| 3692 | return { cancelled: [], notRunning: [...jobIDs] }; |
| 3693 | }, |
| 3694 | async ActiveWorkForTab() { |
| 3695 | return { running: false, pendingPrompt: false, cancellable: false, jobs: [] }; |
| 3696 | }, |
| 3697 | async BackgroundRuntimes() { |
| 3698 | return []; |
| 3699 | }, |
| 3700 | async RevealBackgroundRuntime() { |
| 3701 | throw new Error("background runtime is unavailable in browser preview"); |
| 3702 | }, |
| 3703 | async WorkspaceConflictForTab() { |
| 3704 | return { |
| 3705 | state: "none", ownerWork: { running: false, pendingPrompt: false, cancellable: false, jobs: [] }, |
| 3706 | canReveal: false, canCreateWorktree: false, |
| 3707 | }; |
| 3708 | }, |
| 3709 | async RevealWorkspaceWriterForTab() { |
| 3710 | throw new Error("workspace writer is unavailable in browser preview"); |
| 3711 | }, |
| 3712 | async CloseTabWithPolicy(tabID) { |
| 3713 | return this.CloseTab(tabID); |
| 3714 | }, |
| 3715 | async ToolResultForTab() { |
| 3716 | return null; |
| 3717 | }, |
| 3718 | async Meta() { |
| 3719 | const active = withMockSessionIdentity(mockTabs.find((tab) => tab.active) ?? mockTabs[0]); |
| 3720 | const toolApprovalMode = normalizeToolApprovalMode(active?.toolApprovalMode, active ? normalizeMode(active.mode) : "normal", settings.autoApproveTools); |
| 3721 | const autoApproveTools = toolApprovalMode === "danger-full-access"; |
| 3722 | const collaborationMode = normalizeCollaborationMode(active?.collaborationMode, active?.goal, active ? normalizeMode(active.mode) : "normal"); |
| 3723 | const workspacePath = active?.workspacePath || active?.workspaceRoot || active?.cwd || cwd; |
| 3724 | return { |
| 3725 | label: active?.label ?? "DeepSeek-R1", |
| 3726 | ready: active?.ready ?? true, |
| 3727 | eventChannel: EVENT_CHANNEL, |
| 3728 | cwd: active?.cwd || cwd, |
| 3729 | workspaceRoot: active?.workspaceRoot || workspacePath, |
| 3730 | workspaceName: active?.workspaceName, |
| 3731 | workspacePath, |
| 3732 | ...mockSessionMeta(active), |
| 3733 | sandboxPath: settings.sandbox.workspaceRoot, |
| 3734 | gitBranch: active?.gitBranch || (active?.scope === "project" ? "main" : ""), |
| 3735 | imageInputEnabled: true, |
| 3736 | autoApproveTools, |
| 3737 | bypass: autoApproveTools, |
| 3738 | collaborationMode, |
| 3739 | toolApprovalMode, |
| 3740 | goal: active?.goal ?? "", |
| 3741 | goalStatus: active?.goalStatus ?? (active?.goal ? "running" : "stopped"), |
| 3742 | }; |
| 3743 | }, |
| 3744 | async MetaForTab(tabID) { |
| 3745 | const tab = withMockSessionIdentity(mockTabs.find((item) => item.id === tabID) ?? mockTabs.find((item) => item.active) ?? mockTabs[0]); |
| 3746 | const toolApprovalMode = normalizeToolApprovalMode(tab?.toolApprovalMode, tab ? normalizeMode(tab.mode) : "normal", settings.autoApproveTools); |
| 3747 | const autoApproveTools = toolApprovalMode === "danger-full-access"; |
| 3748 | const collaborationMode = normalizeCollaborationMode(tab?.collaborationMode, tab?.goal, tab ? normalizeMode(tab.mode) : "normal"); |
| 3749 | const workspacePath = tab?.workspacePath || tab?.workspaceRoot || tab?.cwd || cwd; |
| 3750 | return { |
| 3751 | label: tab?.label ?? "DeepSeek-R1", |
| 3752 | ready: tab?.ready ?? true, |
| 3753 | eventChannel: EVENT_CHANNEL, |
| 3754 | cwd: tab?.cwd || cwd, |
| 3755 | workspaceRoot: tab?.workspaceRoot || workspacePath, |
| 3756 | workspaceName: tab?.workspaceName, |
| 3757 | workspacePath, |
| 3758 | ...mockSessionMeta(tab), |
| 3759 | sandboxPath: settings.sandbox.workspaceRoot, |
| 3760 | gitBranch: tab?.gitBranch || (tab?.scope === "project" ? "main" : ""), |
| 3761 | autoApproveTools, |
| 3762 | bypass: autoApproveTools, |
| 3763 | collaborationMode, |
| 3764 | toolApprovalMode, |
| 3765 | goal: tab?.goal ?? "", |
| 3766 | goalStatus: tab?.goalStatus ?? (tab?.goal ? "running" : "stopped"), |
| 3767 | }; |
| 3768 | }, |
| 3769 | async Commands() { |
| 3770 | const commands: CommandInfo[] = [ |
| 3771 | { name: "new", description: "start new session; save transcript", kind: "builtin" as const, group: "actions" }, |
| 3772 | { name: "clear", description: "discard current context", kind: "builtin" as const, group: "actions" }, |
| 3773 | { name: "compact", description: "Summarize older history to free up context", kind: "builtin" as const, group: "actions" }, |
| 3774 | { name: "model", description: "Switch model", kind: "builtin" as const, group: "actions" }, |
| 3775 | { name: "effort", description: "Set reasoning effort", kind: "builtin" as const, group: "actions" }, |
| 3776 | { name: "skill", description: "List skills", kind: "builtin" as const, group: "skills" }, |
| 3777 | { name: "mcp", description: "Manage MCP servers", kind: "builtin" as const, group: "integrations" }, |
| 3778 | { name: "plugins", description: "Manage plugin packages", kind: "builtin" as const, group: "integrations" }, |
| 3779 | { name: "review", description: "Review the staged diff", hint: "[focus]", kind: "custom" as const, group: "skills" }, |
| 3780 | ]; |
| 3781 | const seen = new Set(commands.map((command) => command.name)); |
| 3782 | for (const skill of capSkills) { |
| 3783 | if (skill.enabled === false) continue; |
| 3784 | const name = (skill.invocation || `/${skill.name}`).replace(/^\/+/, ""); |
| 3785 | if (!name || seen.has(name)) continue; |
| 3786 | seen.add(name); |
| 3787 | commands.push({ |
| 3788 | name, |
| 3789 | description: skill.description, |
| 3790 | kind: skill.runAs === "subagent" ? "subagent" : "skill", |
| 3791 | group: skill.runAs === "subagent" ? "subagents" : "skills", |
| 3792 | color: skill.color, |
| 3793 | }); |
| 3794 | } |
| 3795 | return commands; |
| 3796 | }, |
| 3797 | async Capabilities() { |
| 3798 | return { |
| 3799 | servers: capServers.map((s) => ({ ...s })), |
| 3800 | skills: capSkills.map((s) => ({ ...s })), |
| 3801 | skillRoots: capSkillRoots.map((s) => ({ ...s })), |
| 3802 | plugins: capPlugins.map((p) => ({ ...p })), |
| 3803 | }; |
| 3804 | }, |
| 3805 | async MCPServers() { |
| 3806 | return capServers.map((s) => ({ ...s })); |
| 3807 | }, |
| 3808 | async MCPCapabilityMatrix() { |
| 3809 | return { views: [], hostProfile: "desktop-apps-2026-01-26-v1" }; |
| 3810 | }, |
| 3811 | async MCPMarketplace(query: string) { |
| 3812 | const servers = [ |
| 3813 | { |
| 3814 | name: "io.modelcontextprotocol/server-filesystem", |
| 3815 | suggestedName: "server-filesystem", |
| 3816 | title: "Filesystem", |
| 3817 | description: "Secure file operations through MCP.", |
| 3818 | version: "1.0.0", |
| 3819 | installable: true, |
| 3820 | transport: "stdio", |
| 3821 | command: "npx", |
| 3822 | args: ["-y", "@modelcontextprotocol/server-filesystem@1.0.0"], |
| 3823 | }, |
| 3824 | { |
| 3825 | name: "io.example/manual", |
| 3826 | suggestedName: "manual", |
| 3827 | title: "Manual setup example", |
| 3828 | description: "Requires an API key before installation.", |
| 3829 | version: "1.0.0", |
| 3830 | installable: false, |
| 3831 | unavailableReason: "package requires environment variables or arguments", |
| 3832 | args: [], |
| 3833 | }, |
| 3834 | ]; |
| 3835 | const normalized = query.trim().toLowerCase(); |
| 3836 | return { |
| 3837 | servers: normalized ? servers.filter((entry) => [entry.name, entry.title, entry.description].join(" ").toLowerCase().includes(normalized)) : servers, |
| 3838 | cached: false, |
| 3839 | } as MCPMarketplaceView; |
| 3840 | }, |
| 3841 | async MCPMarketplaceResolve(registryName: string) { |
| 3842 | const result = await this.MCPMarketplace(registryName); |
| 3843 | const entry = result.servers.find((candidate) => candidate.name.toLowerCase() === registryName.trim().toLowerCase()); |
| 3844 | if (!entry) throw new Error(`MCP Registry has no server named ${JSON.stringify(registryName)}`); |
| 3845 | return entry; |
| 3846 | }, |
| 3847 | async SkillsSettings() { |
| 3848 | return { |
| 3849 | skills: capSkills.map((s) => ({ ...s })), |
| 3850 | skillRoots: capSkillRoots.map((s) => ({ ...s })), |
| 3851 | allowImplicitInvocation: true, |
| 3852 | }; |
| 3853 | }, |
| 3854 | async RuntimeDoctor() { |
| 3855 | return { |
| 3856 | text: "runtime status: mock\nrecoverability: clean=true irreversible=false\nresume: allow=true cleanRollback=true\n", |
| 3857 | publishedGeneration: 0, |
| 3858 | allowResume: true, |
| 3859 | cleanRollback: true, |
| 3860 | hasIrreversible: false, |
| 3861 | noOpRebuilds: 0, |
| 3862 | fullRebuilds: 0, |
| 3863 | subgraphRebuilds: 0, |
| 3864 | staleDrops: 0, |
| 3865 | admissionRejected: 0, runtimeOwnerFallbacks: 0, |
| 3866 | }; |
| 3867 | }, |
| 3868 | async CapabilityDiagnostics(includeSessionRuntime: boolean) { |
| 3869 | const report: CapabilityDiagnosticsReport = { |
| 3870 | schema_version: 1, |
| 3871 | root: "<workspace>", |
| 3872 | live: false, |
| 3873 | summary: { |
| 3874 | errors: 0, |
| 3875 | warnings: 1, |
| 3876 | infos: includeSessionRuntime ? 1 : 0, |
| 3877 | instructions: 1, |
| 3878 | skills: capSkills.length, |
| 3879 | commands: 0, |
| 3880 | hooks: 0, |
| 3881 | plugins: capPlugins.length, |
| 3882 | mcp_servers: capServers.length, |
| 3883 | }, |
| 3884 | instructions: { docs: [{ path: "<workspace>/AGENTS.md", scope: "project", directory: "<workspace>", depth: 0, order: 1 }] }, |
| 3885 | skills: { |
| 3886 | roots: [{ path: "<workspace>/.reasonix/skills", scope: "project", status: "ok" }], |
| 3887 | entries: capSkills.map((s) => ({ |
| 3888 | name: s.name, |
| 3889 | description: s.description, |
| 3890 | scope: s.scope, |
| 3891 | path: "(mock)", |
| 3892 | status: "winner", |
| 3893 | run_as: s.runAs, |
| 3894 | })), |
| 3895 | winners: capSkills.length, |
| 3896 | shadowed: 0, |
| 3897 | }, |
| 3898 | commands: { roots: [], entries: [], winners: 0, shadowed: 0 }, |
| 3899 | hooks: { trusted_project: true, project_defines_hooks: false, sources: [], entries: [] }, |
| 3900 | plugins: { |
| 3901 | packages: capPlugins.map((p) => ({ |
| 3902 | name: p.name, |
| 3903 | enabled: p.enabled, |
| 3904 | root: p.root || "<external>/plugin", |
| 3905 | skills: p.skills ?? 0, |
| 3906 | commands: 0, |
| 3907 | hooks: p.hooks ?? 0, |
| 3908 | mcp_servers: p.mcpServers ?? 0, |
| 3909 | status: p.enabled ? "ok" : "disabled", |
| 3910 | })), |
| 3911 | }, |
| 3912 | mcp: { |
| 3913 | servers: capServers.map((s) => ({ |
| 3914 | name: s.name, |
| 3915 | transport: s.transport || "stdio", |
| 3916 | start_intent: s.startIntent === "off" ? "off" : "automatic", |
| 3917 | source: "toml", |
| 3918 | runtime_status: includeSessionRuntime ? s.status || "connected" : undefined, |
| 3919 | tool_count: s.tools, |
| 3920 | env_keys: s.envKeys ?? [], |
| 3921 | header_keys: s.headerKeys ?? [], |
| 3922 | })), |
| 3923 | }, |
| 3924 | issues: [ |
| 3925 | { |
| 3926 | severity: "warning", |
| 3927 | code: "skill.missing_description", |
| 3928 | subsystem: "skills", |
| 3929 | name: "example", |
| 3930 | message: "mock warning for browser harness", |
| 3931 | remediation: "Add a description frontmatter field", |
| 3932 | settings_tab: "skills", |
| 3933 | }, |
| 3934 | ...(includeSessionRuntime |
| 3935 | ? [{ |
| 3936 | severity: "info" as const, |
| 3937 | code: "mcp.runtime_unavailable", |
| 3938 | subsystem: "mcp", |
| 3939 | message: "browser mock has no live Host; runtime fields are synthetic", |
| 3940 | settings_tab: "mcp", |
| 3941 | }] |
| 3942 | : []), |
| 3943 | ], |
| 3944 | }; |
| 3945 | return JSON.parse(JSON.stringify(report)) as CapabilityDiagnosticsReport; |
| 3946 | }, |
| 3947 | async CredentialDiagnostics(_probe: boolean) { |
| 3948 | return credentialDiagnostics(); |
| 3949 | }, |
| 3950 | async RepairCredentials(dryRun: boolean) { |
| 3951 | return credentialDiagnostics([dryRun ? "would repair credential access" : "repaired credential access"]); |
| 3952 | }, |
| 3953 | async RetryAuthenticationForTab(_tabId: string) { |
| 3954 | return { status: "ready" }; |
| 3955 | }, |
| 3956 | async Plugins() { |
| 3957 | return capPlugins.map((p) => ({ ...p })); |
| 3958 | }, |
| 3959 | async PlanPluginInstall(source: string, options: PluginInstallOptions) { |
| 3960 | const name = options.name || source.split("/").filter(Boolean).pop()?.replace(/\.git$/, "") || "plugin"; |
| 3961 | return JSON.stringify({ |
| 3962 | ok: true, |
| 3963 | status: "planned", |
| 3964 | kind: "plugin", |
| 3965 | actions: [{ kind: "plugin", action: "install_plugin_package", name, source, status: "planned" }], |
| 3966 | }); |
| 3967 | }, |
| 3968 | async InstallPlugin(source: string, options: PluginInstallOptions) { |
| 3969 | const name = options.name || source.split("/").filter(Boolean).pop()?.replace(/\.git$/, "") || "plugin"; |
| 3970 | const existing = capPlugins.findIndex((p) => p.name === name); |
| 3971 | const view: PluginView = { |
| 3972 | name, |
| 3973 | version: "dev", |
| 3974 | description: "Mock plugin", |
| 3975 | source, |
| 3976 | root: `~/.reasonix/plugins/${name}`, |
| 3977 | manifestKind: "reasonix", |
| 3978 | enabled: true, |
| 3979 | skills: 1, |
| 3980 | hooks: 0, |
| 3981 | mcpServers: 0, |
| 3982 | skillDetails: [{ name: "plan", description: "Plan work before implementation", invocation: "/plan", runAs: "inline" }], |
| 3983 | }; |
| 3984 | if (existing >= 0) capPlugins[existing] = view; |
| 3985 | else capPlugins.push(view); |
| 3986 | return JSON.stringify({ ok: true, status: "done", kind: "plugin", actions: [{ kind: "plugin", name }] }); |
| 3987 | }, |
| 3988 | async RemovePlugin(name: string) { |
| 3989 | capPlugins = capPlugins.filter((p) => p.name !== name); |
| 3990 | }, |
| 3991 | async SetPluginEnabled(name: string, enabled: boolean) { |
| 3992 | capPlugins = capPlugins.map((p) => p.name === name ? { ...p, enabled } : p); |
| 3993 | }, |
| 3994 | async UpdatePlugin(name: string) { |
| 3995 | capPlugins = capPlugins.map((p) => p.name === name ? { ...p, version: p.version || "dev" } : p); |
| 3996 | return JSON.stringify({ ok: true, status: "done", kind: "plugin", name }); |
| 3997 | }, |
| 3998 | async PluginDoctor(name: string) { |
| 3999 | return capPlugins.find((p) => p.name === name) || { |
| 4000 | name, |
| 4001 | root: "", |
| 4002 | enabled: false, |
| 4003 | skills: 0, |
| 4004 | hooks: 0, |
| 4005 | mcpServers: 0, |
| 4006 | error: "plugin is not installed", |
| 4007 | }; |
| 4008 | }, |
| 4009 | async AddMCPServer(input: MCPServerInput) { |
| 4010 | const tools = input.transport === "stdio" ? 3 : 5; |
| 4011 | capServers.push({ |
| 4012 | name: input.name, |
| 4013 | transport: input.transport, |
| 4014 | status: "connected", |
| 4015 | configured: true, |
| 4016 | autoStart: true, |
| 4017 | tier: "background", |
| 4018 | command: input.command, |
| 4019 | args: input.args, |
| 4020 | url: input.url, |
| 4021 | envKeys: input.env ? Object.keys(input.env).sort() : undefined, |
| 4022 | headerKeys: input.headers ? Object.keys(input.headers).sort() : undefined, |
| 4023 | tools, |
| 4024 | prompts: 0, |
| 4025 | resources: 0, |
| 4026 | toolList: Array.from({ length: tools }, (_, i) => ({ |
| 4027 | name: `${input.name}_tool_${i + 1}`, |
| 4028 | description: `Mock tool ${i + 1} exposed by ${input.name}.`, |
| 4029 | })), |
| 4030 | }); |
| 4031 | return tools; |
| 4032 | }, |
| 4033 | async InstallMCPServer(input: MCPServerInput) { |
| 4034 | const tools = await this.AddMCPServer(input); |
| 4035 | return { name: input.name, state: "ready" as const, toolCount: tools, action: "none" as const, message: `${input.name} is ready` }; |
| 4036 | }, |
| 4037 | async UpdateMCPServer(name: string, input: MCPServerInput) { |
| 4038 | capServers = capServers.map((s) => { |
| 4039 | if (s.name !== name) return s; |
| 4040 | const connected = s.status === "connected" || s.status === "failed" || s.autoStart !== false; |
| 4041 | const nextStatus = s.status === "disabled" ? "disabled" : connected ? "connected" : "deferred"; |
| 4042 | const nextTools = nextStatus === "connected" ? s.tools || (input.transport === "stdio" ? 3 : 5) : 0; |
| 4043 | return { |
| 4044 | ...s, |
| 4045 | transport: input.transport, |
| 4046 | status: nextStatus, |
| 4047 | command: input.transport === "stdio" ? input.command : "", |
| 4048 | args: input.transport === "stdio" ? input.args : [], |
| 4049 | url: input.transport === "stdio" ? "" : input.url, |
| 4050 | envKeys: input.env ? Object.keys(input.env).sort() : s.envKeys, |
| 4051 | headerKeys: input.headers ? Object.keys(input.headers).sort() : s.headerKeys, |
| 4052 | tools: nextTools, |
| 4053 | error: undefined, |
| 4054 | authStatus: nextStatus !== "connected" && input.transport !== "stdio" ? "possible" : undefined, |
| 4055 | authUrl: nextStatus !== "connected" && input.transport !== "stdio" ? input.url : undefined, |
| 4056 | }; |
| 4057 | }); |
| 4058 | }, |
| 4059 | async RemoveMCPServer(name: string) { |
| 4060 | capServers = capServers.filter((s) => s.name !== name); |
| 4061 | }, |
| 4062 | async AuthorizeAndConnectMCPServer(name: string) { |
| 4063 | capServers = capServers.map((s) => s.name === name |
| 4064 | ? { ...s, status: "connected", runtimeState: "ready", tools: s.tools || 4, error: undefined, requiresLaunchApproval: false } |
| 4065 | : s); |
| 4066 | }, |
| 4067 | async AuthenticateMCPServer(name: string) { |
| 4068 | capServers = capServers.map((s) => s.name === name |
| 4069 | ? { ...s, status: "connected", runtimeState: "ready", tools: s.tools || 4, error: undefined, authStatus: "none", authUrl: undefined } |
| 4070 | : s); |
| 4071 | }, |
| 4072 | async ReconnectMCPServer(name: string) { |
| 4073 | capServers = capServers.map((s) => |
| 4074 | s.name === name |
| 4075 | ? { ...s, status: "initializing", error: undefined, authStatus: undefined, authUrl: undefined } |
| 4076 | : s, |
| 4077 | ); |
| 4078 | await new Promise((r) => setTimeout(r, 400)); |
| 4079 | capServers = capServers.map((s) => |
| 4080 | s.name === name ? { ...s, status: "connected", tools: s.tools || 4 } : s, |
| 4081 | ); |
| 4082 | }, |
| 4083 | async ClearMCPServerAuthentication(name: string) { |
| 4084 | capServers = capServers.map((s) => |
| 4085 | s.name === name |
| 4086 | ? { |
| 4087 | ...s, |
| 4088 | status: s.autoStart === false ? "disabled" : "initializing", |
| 4089 | tools: 0, |
| 4090 | error: undefined, |
| 4091 | authStatus: s.transport !== "stdio" ? "possible" : undefined, |
| 4092 | authUrl: s.transport !== "stdio" ? s.url : undefined, |
| 4093 | authConfigured: undefined, |
| 4094 | } |
| 4095 | : s, |
| 4096 | ); |
| 4097 | }, |
| 4098 | async PickSkillFolder() { |
| 4099 | return "~/my-skills"; |
| 4100 | }, |
| 4101 | async PickPluginFolder() { |
| 4102 | return "~/plugins/superpowers"; |
| 4103 | }, |
| 4104 | async AddSkillPath(path: string) { |
| 4105 | const dir = path.trim() || "~/my-skills"; |
| 4106 | if (!capSkillRoots.some((r) => r.scope === "custom" && r.dir === dir)) { |
| 4107 | capSkillRoots.push({ |
| 4108 | dir, |
| 4109 | scope: "custom", |
| 4110 | priority: capSkillRoots.length + 1, |
| 4111 | status: "ok", |
| 4112 | enabled: true, |
| 4113 | configured: true, |
| 4114 | removable: true, |
| 4115 | skills: 1, |
| 4116 | skillItems: [{ name: "local-dev", description: "Local custom development workflow", scope: "custom", runAs: "inline" }], |
| 4117 | }); |
| 4118 | } |
| 4119 | if (!capSkills.some((s) => s.name === "local-dev")) { |
| 4120 | capSkills.push({ name: "local-dev", description: "Local custom development workflow", scope: "custom", runAs: "inline", enabled: true }); |
| 4121 | } |
| 4122 | }, |
| 4123 | async RemoveSkillPath(path: string) { |
| 4124 | capSkillRoots = capSkillRoots.filter((r) => r.dir !== path); |
| 4125 | if (!capSkillRoots.some((r) => r.scope === "custom")) { |
| 4126 | const idx = capSkills.findIndex((s) => s.name === "local-dev"); |
| 4127 | if (idx >= 0) capSkills.splice(idx, 1); |
| 4128 | } |
| 4129 | }, |
| 4130 | async SetSkillPathEnabled(path: string, enabled: boolean) { |
| 4131 | const root = capSkillRoots.find((r) => r.dir === path); |
| 4132 | if (root) { |
| 4133 | root.enabled = enabled; |
| 4134 | root.status = enabled ? "ok" : "disabled"; |
| 4135 | root.skills = enabled ? (root.skillItems?.length ?? 0) : 0; |
| 4136 | } |
| 4137 | }, |
| 4138 | async RefreshSkills() {}, |
| 4139 | async ReloadCommands() {}, |
| 4140 | async SetSkillEnabled(name: string, enabled: boolean) { |
| 4141 | const skill = capSkills.find((s) => s.name === name); |
| 4142 | if (skill) skill.enabled = enabled; |
| 4143 | }, |
| 4144 | async SetSkillImplicitInvocation(_enabled: boolean) {}, |
| 4145 | async AvailableSubagentTools() { |
| 4146 | return [ |
| 4147 | { name: "read_file", description: "Read a file's contents", readOnlyHint: true }, |
| 4148 | { name: "ls", description: "List a directory", readOnlyHint: true }, |
| 4149 | { name: "glob", description: "Find files by name pattern", readOnlyHint: true }, |
| 4150 | { name: "grep", description: "Search file contents", readOnlyHint: true }, |
| 4151 | { name: "code_index", description: "Look up symbol definitions and file outlines", readOnlyHint: true }, |
| 4152 | { name: "edit_file", description: "Edit an existing file" }, |
| 4153 | { name: "write_file", description: "Write a new file" }, |
| 4154 | { name: "bash", description: "Run a shell command" }, |
| 4155 | { name: "web_fetch", description: "Fetch a URL" }, |
| 4156 | ]; |
| 4157 | }, |
| 4158 | async CreateSubagentProfile(input: SubagentProfileInput) { |
| 4159 | const name = input.name.trim(); |
| 4160 | const builtinNames = ["init", "explore", "research", "install-capability", "review", "security-review", "test"]; |
| 4161 | if (builtinNames.includes(name)) throw new Error(`"${name}" is a built-in subagent name and cannot be reused`); |
| 4162 | if (capSkills.some((s) => s.name === name)) throw new Error(`"${name}" already exists`); |
| 4163 | capSkills.push({ |
| 4164 | name, description: input.description, scope: input.scope === "project" ? "project" : "global", |
| 4165 | runAs: "subagent", enabled: true, model: input.model, effort: input.effort, |
| 4166 | allowedTools: input.allowedTools, color: input.color, invocation: `/${name}`, invocationMode: "manual", |
| 4167 | }); |
| 4168 | return `~/.reasonix/skills/${name}/SKILL.md`; |
| 4169 | }, |
| 4170 | async UpdateSubagentProfile(name: string, scope: string, input: SubagentProfileInput) { |
| 4171 | const skill = capSkills.find((s) => s.name === name && s.scope === scope); |
| 4172 | if (!skill) throw new Error(`"${name}" resolves at a different scope — refusing to update`); |
| 4173 | skill.description = input.description; |
| 4174 | skill.color = input.color; |
| 4175 | skill.model = input.model; |
| 4176 | skill.effort = input.effort; |
| 4177 | skill.allowedTools = input.allowedTools; |
| 4178 | }, |
| 4179 | async DeleteSubagentProfile(name: string, scope: string) { |
| 4180 | const idx = capSkills.findIndex((s) => s.name === name && s.scope === scope); |
| 4181 | if (idx < 0) throw new Error(`"${name}" resolves at a different scope — refusing to delete`); |
| 4182 | capSkills.splice(idx, 1); |
| 4183 | }, |
| 4184 | async SetSubagentProfileModel(name: string, ref: string) { |
| 4185 | const skill = capSkills.find((s) => s.name === name); |
| 4186 | if (skill) skill.configuredModel = ref || undefined; |
| 4187 | }, |
| 4188 | async SetSubagentProfileEffort(name: string, level: string) { |
| 4189 | const skill = capSkills.find((s) => s.name === name); |
| 4190 | if (skill) skill.configuredEffort = level || undefined; |
| 4191 | }, |
| 4192 | async CancelTrySubagentProfile() {}, |
| 4193 | async TrySubagentProfile(input: SubagentProfileInput, task: string) { |
| 4194 | if (!task.trim()) throw new Error("task is required"); |
| 4195 | if (!input.systemPrompt.trim()) throw new Error("system prompt is required"); |
| 4196 | await new Promise((resolve) => setTimeout(resolve, 400)); |
| 4197 | return `[mock run of "${input.name || "draft"}"]\n\nTask: ${task}\n\n(This is a dev-mode mock response — the real backend runs an isolated subagent loop against your configured model.)`; |
| 4198 | }, |
| 4199 | async SetMCPServerEnabled(name: string, enabled: boolean) { |
| 4200 | capServers = capServers.map((s) => |
| 4201 | s.name === name |
| 4202 | ? { |
| 4203 | ...s, |
| 4204 | status: enabled ? "connected" : "disabled", |
| 4205 | autoStart: s.builtIn ? enabled : s.autoStart, |
| 4206 | tools: enabled ? s.tools || 4 : 0, |
| 4207 | error: undefined, |
| 4208 | authStatus: !enabled && s.transport !== "stdio" ? "possible" : undefined, |
| 4209 | authUrl: !enabled && s.transport !== "stdio" ? s.url : undefined, |
| 4210 | } |
| 4211 | : s, |
| 4212 | ); |
| 4213 | }, |
| 4214 | async SetMCPServerTier(name: string, tier: string) { |
| 4215 | capServers = capServers.map((s) => { |
| 4216 | if (s.name !== name) return s; |
| 4217 | const tools = s.tools || (s.transport === "stdio" ? 3 : 5); |
| 4218 | return { ...s, tier, autoStart: true, status: "connected", tools, error: undefined, authStatus: undefined, authUrl: undefined }; |
| 4219 | }); |
| 4220 | }, |
| 4221 | async SlashArgs(input: string) { |
| 4222 | // Mirror a slice of the real arg hints so the menu is exercisable in browser dev. |
| 4223 | const from = input.lastIndexOf(" ") + 1; |
| 4224 | const cur = input.slice(from); |
| 4225 | const cmd = input.slice(0, input.indexOf(" ") < 0 ? input.length : input.indexOf(" ")); |
| 4226 | const subs: Record<string, { label: string; insert: string; hint: string; descend?: boolean }[]> = { |
| 4227 | "/skill": [ |
| 4228 | { label: "list", insert: "list", hint: "list skills" }, |
| 4229 | { label: "show", insert: "show ", hint: "show a skill's body", descend: true }, |
| 4230 | { label: "enable", insert: "enable ", hint: "enable a disabled skill", descend: true }, |
| 4231 | { label: "disable", insert: "disable ", hint: "disable an enabled skill", descend: true }, |
| 4232 | { label: "new", insert: "new ", hint: "scaffold a new skill" }, |
| 4233 | { label: "paths", insert: "paths", hint: "show discovery paths" }, |
| 4234 | ], |
| 4235 | "/hooks": [ |
| 4236 | { label: "list", insert: "list", hint: "list active hooks" }, |
| 4237 | ], |
| 4238 | "/model": [ |
| 4239 | { label: "deepseek/deepseek-v4-flash", insert: "deepseek/deepseek-v4-flash", hint: "current" }, |
| 4240 | { label: "deepseek/deepseek-v4-pro", insert: "deepseek/deepseek-v4-pro", hint: "" }, |
| 4241 | ], |
| 4242 | "/effort": [ |
| 4243 | { label: "auto", insert: "auto", hint: "use the model default" }, |
| 4244 | { label: "high", insert: "high", hint: "deeper reasoning" }, |
| 4245 | { label: "max", insert: "max", hint: "maximum reasoning" }, |
| 4246 | ], |
| 4247 | }; |
| 4248 | const items = (subs[cmd] ?? []) |
| 4249 | .filter((it) => it.label.toLowerCase().startsWith(cur.toLowerCase())) |
| 4250 | .map((it) => ({ label: it.label, insert: it.insert, hint: it.hint, descend: it.descend ?? false })); |
| 4251 | return { items, from }; |
| 4252 | }, |
| 4253 | async ListDir(rel: string) { |
| 4254 | // A tiny fake tree so the @ menu is navigable in browser dev. |
| 4255 | if (rel === "" || rel === "./") { |
| 4256 | return [ |
| 4257 | { name: "internal", isDir: true }, |
| 4258 | { name: "desktop", isDir: true }, |
| 4259 | { name: "README.md", isDir: false }, |
| 4260 | { name: "go.mod", isDir: false }, |
| 4261 | ]; |
| 4262 | } |
| 4263 | if (rel === "internal/") { |
| 4264 | return [ |
| 4265 | { name: "control", isDir: true }, |
| 4266 | { name: "boot", isDir: true }, |
| 4267 | { name: "event.go", isDir: false }, |
| 4268 | ]; |
| 4269 | } |
| 4270 | return [{ name: "file.go", isDir: false }]; |
| 4271 | }, |
| 4272 | async ListDirForTab(_tabID: string, rel: string) { |
| 4273 | return this.ListDir(rel); |
| 4274 | }, |
| 4275 | async ListDirForTarget(_target: ComposerTarget, rel: string) { |
| 4276 | return this.ListDir(rel); |
| 4277 | }, |
| 4278 | async SearchFileRefs(query: string) { |
| 4279 | const q = query.toLowerCase(); |
| 4280 | return ["desktop/frontend/src/lib/bridge.ts", "desktop/frontend/src/main.tsx", "internal/control/refs.go"] |
| 4281 | .filter((path) => path.split("/").pop()?.toLowerCase().includes(q)) |
| 4282 | .map((name) => ({ name, isDir: false })); |
| 4283 | }, |
| 4284 | async SearchFileRefsForTab(_tabID: string, query: string) { |
| 4285 | return this.SearchFileRefs(query); |
| 4286 | }, |
| 4287 | async SearchFileRefsForTarget(_target: ComposerTarget, query: string) { |
| 4288 | return this.SearchFileRefs(query); |
| 4289 | }, |
| 4290 | async ReadFile(rel: string) { |
| 4291 | return mockWorkspaceFile(rel); |
| 4292 | }, |
| 4293 | async ReadFileForTab(_tabID: string, rel: string) { |
| 4294 | return this.ReadFile(rel); |
| 4295 | }, |
| 4296 | async CreateWorkspaceBrowserPreviewForTab(_tabID: string, rel: string) { |
| 4297 | return `http://127.0.0.1:0/__reasonix_workspace_media/mock/${encodeURIComponent(rel)}`; |
| 4298 | }, |
| 4299 | async ReadPresentedFileForTab(tabID: string, _toolCallID: string, path: string) { |
| 4300 | return this.ReadFileForTab(tabID, path); |
| 4301 | }, |
| 4302 | async ReadPresentedFileSourceForTab(tabID: string, _toolCallID: string, path: string) { |
| 4303 | return this.ReadFileForTab(tabID, path); |
| 4304 | }, |
| 4305 | async ReadPresentedTextPageForTab(_tabID: string, _toolCallID: string, path: string, offset: number, expectedVersion: string) { |
| 4306 | return { path, body: "", offset, nextOffset: offset, size: offset, hasMore: false, version: expectedVersion }; |
| 4307 | }, |
| 4308 | async ResolveChatFileReferencesForTab(_tabID: string, turnKey: string, candidates: ChatFileReferenceRequest[]): Promise<ChatFileReferenceResult> { |
| 4309 | return { |
| 4310 | turnKey, |
| 4311 | references: candidates.map(candidate => ({ |
| 4312 | key: candidate.key, |
| 4313 | path: candidate.path, |
| 4314 | status: "unavailable" as const, |
| 4315 | actions: [], |
| 4316 | reason: "not-found" as const, |
| 4317 | })), |
| 4318 | }; |
| 4319 | }, |
| 4320 | async ReadReferenceFileForTab(tabID: string, path: string) { |
| 4321 | return this.ReadFileForTab(tabID, path); |
| 4322 | }, |
| 4323 | async ReadReferenceFileSourceForTab(tabID: string, path: string) { |
| 4324 | return this.ReadFileForTab(tabID, path); |
| 4325 | }, |
| 4326 | async SanitizeMarkdownSVG(): Promise<MarkdownSVGView> { |
| 4327 | return { ok: false, reason: "unsupported" }; |
| 4328 | }, |
| 4329 | async CreatePresentedBrowserPreviewForTab(tabID: string, _toolCallID: string, path: string) { |
| 4330 | return this.CreateWorkspaceBrowserPreviewForTab(tabID, path); |
| 4331 | }, |
| 4332 | async RevokeWorkspaceBrowserPreview() {}, |
| 4333 | async RevokeWorkspaceMediaPreview() {}, |
| 4334 | async SaveRemoteFileAs(_hostID: string, remotePath: string) { |
| 4335 | return remotePath; |
| 4336 | }, |
| 4337 | async SaveRemotePresentedFileAs(_tabID: string, hostID: string, _toolCallID: string, remotePath: string) { |
| 4338 | return this.SaveRemoteFileAs(hostID, remotePath); |
| 4339 | }, |
| 4340 | async ResolveRemotePresentedPathForTab(_tabID: string, _hostID: string, _toolCallID: string, remotePath: string) { |
| 4341 | return remotePath.startsWith("/") ? remotePath : `/remote/workspace/${remotePath}`; |
| 4342 | }, |
| 4343 | async ResolveRemoteWorkspacePathForTab(_tabID: string, _hostID: string, _toolCallID: string, remotePath: string) { |
| 4344 | return remotePath.startsWith("/") ? remotePath : `/remote/workspace/${remotePath}`; |
| 4345 | }, |
| 4346 | async ResolveMarkdownImageForTab(_tabID: string, source: string) { |
| 4347 | return { url: source, openHref: source }; |
| 4348 | }, |
| 4349 | async WorkspaceRevisionForTab(_tabID: string) { |
| 4350 | return { revisions: { content: 0, tree: 0, workingTree: 0, gitMeta: 0, session: 0 }, watchState: "active" as const }; |
| 4351 | }, |
| 4352 | async WorkspaceChanges(_tabID: string) { |
| 4353 | return { |
| 4354 | gitAvailable: true, |
| 4355 | gitBranch: "main", |
| 4356 | files: [ |
| 4357 | { |
| 4358 | path: "desktop/frontend/src/components/WorkspacePanel.tsx", |
| 4359 | sources: ["session", "git"], |
| 4360 | gitStatus: "M", |
| 4361 | turns: [0, 2], |
| 4362 | latestPrompt: "Mock session edited the workspace panel.", |
| 4363 | latestTime: Date.now() - 60_000, |
| 4364 | }, |
| 4365 | { path: "README.md", sources: ["git"], gitStatus: "??" }, |
| 4366 | { path: "internal/control/controller.go", sources: ["session"], turns: [1], latestTime: Date.now() - 120_000 }, |
| 4367 | ], |
| 4368 | }; |
| 4369 | }, |
| 4370 | async WorkspaceTurnChanges(_tabID: string, _sessionPath: string, turn: number, _resultID: string) { |
| 4371 | return { turn, coverage: "unknown" as const, files: [], reasons: [], added: 0, removed: 0 }; |
| 4372 | }, |
| 4373 | async WorkspaceTurnChangeDetail() { return null; }, |
| 4374 | async TurnCheckLog() { return null; }, |
| 4375 | async WorkspaceChangeDetail(_tabID: string, path: string) { |
| 4376 | return { |
| 4377 | source: "git" as const, |
| 4378 | added: 2, |
| 4379 | removed: 1, |
| 4380 | diff: `diff --git a/${path} b/${path}\n--- a/${path}\n+++ b/${path}\n@@ -1,2 +1,3 @@\n-old line\n+new line\n context\n+another line`, |
| 4381 | }; |
| 4382 | }, |
| 4383 | async GitBranches() { |
| 4384 | return ["main", "dev", "feature/branch-switcher"]; |
| 4385 | }, |
| 4386 | async GitBranchesForTab(_tabID: string, _workspaceRoot: string) { return this.GitBranches(); }, |
| 4387 | async GitCheckoutForTab(_tabID: string, _workspaceRoot: string, branch: string) { await this.GitCheckout(branch); }, |
| 4388 | async GitCreateBranchForTab(_tabID: string, _workspaceRoot: string, name: string) { await this.GitCreateBranch(name); }, |
| 4389 | async WorkspaceGitStatsForTab(tabID: string, _workspaceRoot: string) { return this.WorkspaceChanges(tabID); }, |
| 4390 | async GitCheckout(_branch: string) { |
| 4391 | console.info("mock GitCheckout", _branch); |
| 4392 | }, |
| 4393 | async GitCreateBranch(_name: string) { |
| 4394 | console.info("mock GitCreateBranch", _name); |
| 4395 | }, |
| 4396 | async WorkspaceGitHistory(_tabID: string, path: string) { |
| 4397 | return [ |
| 4398 | { hash: "abcdef123456", author: "Mock Author", date: new Date().toISOString(), message: "Mock commit message for " + path }, |
| 4399 | ]; |
| 4400 | }, |
| 4401 | async WorkspaceGitCommitDetail(_tabID: string, _hash: string, path: string) { |
| 4402 | if (path) { |
| 4403 | return { diff: "--- a/mock\n+++ b/mock\n@@ -1,1 +1,1 @@\n-mock\n+mock diff" }; |
| 4404 | } |
| 4405 | return { files: ["mock_file_1.ts", "mock_file_2.ts"] }; |
| 4406 | }, |
| 4407 | async OpenLocalPath(path: string) { |
| 4408 | console.info("mock OpenLocalPath", path); |
| 4409 | }, |
| 4410 | async OpenWorkspacePathForTab(_tabID: string, rel: string) { |
| 4411 | console.info("mock OpenWorkspacePath", rel); |
| 4412 | }, |
| 4413 | async OpenPresentedPathForTab(tabID: string, _toolCallID: string, path: string) { |
| 4414 | return this.OpenWorkspacePathForTab(tabID, path); |
| 4415 | }, |
| 4416 | async OpenReferencePathForTab(_tabID: string, path: string) { |
| 4417 | console.info("mock OpenReferencePathForTab", path); |
| 4418 | }, |
| 4419 | async ResolvePresentedPathForTab(tabID: string, _toolCallID: string, path: string) { |
| 4420 | return this.ResolveWorkspacePathForTab(tabID, path); |
| 4421 | }, |
| 4422 | async ResolveReferencePathForTab(_tabID: string, path: string) { |
| 4423 | return path; |
| 4424 | }, |
| 4425 | async ResolveWorkspacePathForTab(_tabID: string, rel: string) { return `${cwd.replace(/[\\/]+$/, "")}/${rel.replace(/^[/\\]+/, "").replace(/[\\/]+$/, "")}`; }, |
| 4426 | async ExternalOpeners() { |
| 4427 | return { |
| 4428 | openers: [ |
| 4429 | { id: "vscode", name: "VS Code", kind: "editor", iconDataUrl: mockExternalOpenerIconDataURL("#1684d6", "V") }, |
| 4430 | { id: "cursor", name: "Cursor", kind: "editor", iconDataUrl: mockExternalOpenerIconDataURL("#25262a", "C") }, |
| 4431 | { id: "finder", name: "Finder", kind: "file-manager", iconDataUrl: mockExternalOpenerIconDataURL("#36aaf4", "F") }, |
| 4432 | { id: "ghostty", name: "Ghostty", kind: "terminal", iconDataUrl: mockExternalOpenerIconDataURL("#264db6", ">") }, |
| 4433 | ], |
| 4434 | preferred: "vscode", |
| 4435 | } as ExternalOpenersView; |
| 4436 | }, async ExternalOpenersForTab(_tabID: string) { return { ...(await this.ExternalOpeners()), workspaceOpenable: true }; }, |
| 4437 | async SetPreferredExternalOpener(_id: string) {}, |
| 4438 | async OpenWorkspaceInExternalOpenerForTab(_tabID: string, _id: string) {}, async OpenLocalPathInExternalOpener(path: string, id: string) { console.info("mock OpenLocalPathInExternalOpener", path, id); }, async SaveLocalPathAs(path: string) { console.info("mock SaveLocalPathAs", path); return path; }, |
| 4439 | async RevealWorkspacePathForTab(_tabID: string, rel: string) { |
| 4440 | console.info("mock RevealWorkspacePath", rel); |
| 4441 | }, |
| 4442 | async RevealPresentedPathForTab(tabID: string, _toolCallID: string, path: string) { |
| 4443 | return this.RevealWorkspacePathForTab(tabID, path); |
| 4444 | }, |
| 4445 | async RevealReferencePathForTab(_tabID: string, path: string) { |
| 4446 | console.info("mock RevealReferencePathForTab", path); |
| 4447 | }, |
| 4448 | async SaveWorkspacePathAsForTab(_tabID: string, rel: string) { |
| 4449 | console.info("mock SaveWorkspacePathAsForTab", rel); |
| 4450 | return rel; |
| 4451 | }, |
| 4452 | async SavePresentedPathAsForTab(tabID: string, _toolCallID: string, path: string) { |
| 4453 | return this.SaveWorkspacePathAsForTab(tabID, path); |
| 4454 | }, |
| 4455 | async SaveReferencePathAsForTab(_tabID: string, path: string) { |
| 4456 | return path; |
| 4457 | }, |
| 4458 | async RevealPath(path: string) { |
| 4459 | console.info("mock RevealPath", path); |
| 4460 | }, |
| 4461 | async PickExportFile(defaultFilename: string, _mimeType: string) { |
| 4462 | return defaultFilename; |
| 4463 | }, |
| 4464 | async ExportGoalDiagnostics() { |
| 4465 | return "goal-diagnostics.json"; |
| 4466 | }, |
| 4467 | async SaveExportFile(path: string, payload: string, base64Encoded: boolean) { |
| 4468 | const a = document.createElement("a"); |
| 4469 | let url = ""; |
| 4470 | if (base64Encoded) { |
| 4471 | url = `data:application/octet-stream;base64,${payload}`; |
| 4472 | } else { |
| 4473 | url = URL.createObjectURL(new Blob([payload], { type: "text/plain;charset=utf-8" })); |
| 4474 | } |
| 4475 | a.href = url; |
| 4476 | a.download = path; |
| 4477 | document.body.appendChild(a); |
| 4478 | a.click(); |
| 4479 | a.remove(); |
| 4480 | if (!base64Encoded) URL.revokeObjectURL(url); |
| 4481 | }, |
| 4482 | async SaveSessionMarkdownForTab(_tabID: string, path: string, title: string) { |
| 4483 | await this.SaveExportFile(path, `# ${title || "Reasonix session"}\n`, false); |
| 4484 | }, |
| 4485 | async SaveExportImageFiles(path: string, payloads: string[]) { |
| 4486 | if (payloads.length === 0) throw new Error("No image payloads to export"); |
| 4487 | const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")); |
| 4488 | const dot = path.lastIndexOf("."); |
| 4489 | const extensionStart = dot > slash ? dot : path.length; |
| 4490 | const stem = path.slice(0, extensionStart); |
| 4491 | const extension = path.slice(extensionStart); |
| 4492 | for (let index = 0; index < payloads.length; index++) { |
| 4493 | const partPath = payloads.length > 1 |
| 4494 | ? `${stem}-${index + 1}-of-${payloads.length}${extension}` |
| 4495 | : path; |
| 4496 | await this.SaveExportFile(partPath, payloads[index], true); |
| 4497 | } |
| 4498 | }, |
| 4499 | async Models() { |
| 4500 | const active = mockTabs.find((tab) => tab.active) ?? mockTabs[0]; |
| 4501 | const current = mockTabModelRef(active); |
| 4502 | return mockModelCatalog.map((model) => ({ ...model, current: model.ref === current })); |
| 4503 | }, |
| 4504 | async ModelsForTab(tabID) { |
| 4505 | const tab = mockTabs.find((item) => item.id === tabID) ?? mockTabs.find((item) => item.active) ?? mockTabs[0]; |
| 4506 | const current = mockTabModelRef(tab); |
| 4507 | return mockModelCatalog.map((model) => ({ ...model, current: model.ref === current })); |
| 4508 | }, |
| 4509 | async ModelsForDraft(draftID) { |
| 4510 | const current = mockDrafts.get(draftID)?.settings.model ?? ""; |
| 4511 | return mockModelCatalog.map((model) => ({ ...model, current: model.ref === current })); |
| 4512 | }, |
| 4513 | async SetModel(name) { |
| 4514 | setMockTabModel(undefined, name); |
| 4515 | }, |
| 4516 | async SetModelForTab(tabID, name) { |
| 4517 | setMockTabModel(tabID, name); |
| 4518 | }, |
| 4519 | async Effort() { |
| 4520 | return { supported: true, current: mockEffort, default: "high", levels: ["auto", "high", "max"] }; |
| 4521 | }, |
| 4522 | async EffortForTab() { |
| 4523 | return this.Effort(); |
| 4524 | }, |
| 4525 | async SetEffort(level: string) { |
| 4526 | mockEffort = level || "auto"; |
| 4527 | }, |
| 4528 | async SetEffortForTab(_tabID, level) { |
| 4529 | await this.SetEffort(level); |
| 4530 | }, |
| 4531 | async ReloadRuntime(_tabID) {}, |
| 4532 | async Memory() { |
| 4533 | return { |
| 4534 | available: true, |
| 4535 | storeDir: "~/.reasonix/projects/-mock/memory", |
| 4536 | storeGlobalDir: "~/.reasonix/memory/global", |
| 4537 | docs: [ |
| 4538 | { |
| 4539 | path: "REASONIX.md", |
| 4540 | scope: "project", |
| 4541 | directory: ".", |
| 4542 | body: "# Reasonix project memory\n\nMock doc shown in the browser dev seam.\n\n## Notes\n\n- prefers concise replies", |
| 4543 | imports: [], |
| 4544 | depth: 0, |
| 4545 | order: 0, |
| 4546 | precedence: 0, |
| 4547 | }, |
| 4548 | { |
| 4549 | path: "~/.reasonix/REASONIX.md", |
| 4550 | scope: "user", |
| 4551 | body: t("mock.memoryBody"), |
| 4552 | imports: [], |
| 4553 | depth: -1, |
| 4554 | order: 1, |
| 4555 | precedence: 1, |
| 4556 | }, |
| 4557 | ], |
| 4558 | instructionDiagnostics: [], |
| 4559 | facts: [ |
| 4560 | { |
| 4561 | name: "prefers-tabs", |
| 4562 | description: "User prefers tabs", |
| 4563 | type: "user", |
| 4564 | scope: "project", |
| 4565 | body: "Indent with tabs.", |
| 4566 | freshness: "fresh", |
| 4567 | }, |
| 4568 | ], |
| 4569 | archives: [ |
| 4570 | { |
| 4571 | name: "old-plan", |
| 4572 | description: "Superseded planning note", |
| 4573 | type: "project", |
| 4574 | scope: "project", |
| 4575 | body: "This plan was archived after the implementation changed.", |
| 4576 | path: "~/.reasonix/projects/-mock/memory/.archive/20260612-021500.000-old-plan.md", |
| 4577 | archivedAt: "2026-06-12T02:15:00Z", |
| 4578 | freshness: "current", |
| 4579 | }, |
| 4580 | ], |
| 4581 | scopes: [ |
| 4582 | { scope: "user", path: "~/.reasonix/REASONIX.md" }, |
| 4583 | { scope: "project", path: "REASONIX.md" }, |
| 4584 | { scope: "local", path: "REASONIX.local.md" }, |
| 4585 | ], |
| 4586 | conflicts: [], |
| 4587 | lastRecall: { |
| 4588 | query: "", |
| 4589 | hits: [], |
| 4590 | omitted: 0, |
| 4591 | charBudget: 2400, |
| 4592 | usedChars: 0, |
| 4593 | suppressed: "no user turn yet", |
| 4594 | }, |
| 4595 | }; |
| 4596 | }, |
| 4597 | async MemorySuggestions() { |
| 4598 | return { |
| 4599 | memories: [ |
| 4600 | { |
| 4601 | id: "memory-prefers-concise-replies", |
| 4602 | name: "prefers-concise-replies", |
| 4603 | title: "Prefers concise replies", |
| 4604 | description: "User prefers concise replies unless detail is requested.", |
| 4605 | type: "user", |
| 4606 | scope: "project", |
| 4607 | body: "User prefers concise replies unless detail is requested.\n\n**Why:** Suggested from recent local history.\n**How to apply:** Keep answers brief by default.", |
| 4608 | reason: "future-facing preference", |
| 4609 | evidence: ["mock-session: always keep replies concise"], |
| 4610 | }, |
| 4611 | ], |
| 4612 | skills: [ |
| 4613 | { |
| 4614 | id: "skill-reasonix-pr-followup", |
| 4615 | name: "reasonix-pr-followup", |
| 4616 | description: "Review or update a Reasonix GitHub PR, address feedback, verify, and publish safely.", |
| 4617 | scope: "project", |
| 4618 | body: "# Reasonix PR Followup\n\nUse this skill for repeated Reasonix PR work.\n\n## Workflow\n\n1. Confirm branch and PR state.\n2. Inspect the diff.\n3. Fix actionable feedback.\n4. Verify and update the PR.\n", |
| 4619 | reason: "recent history repeatedly touched PR workflows", |
| 4620 | evidence: ["mock-pr-session: 提交到pr,并更新内容", "mock-review-session: 解决该pr下机器人提出来的问题"], |
| 4621 | }, |
| 4622 | ], |
| 4623 | generatedAt: new Date().toISOString(), |
| 4624 | available: true, |
| 4625 | source: "mock", |
| 4626 | }; |
| 4627 | }, |
| 4628 | async AcceptMemorySuggestion(suggestion: MemorySuggestion) { |
| 4629 | emit({ kind: "notice", level: "info", text: `saved suggested memory → ${suggestion.name}` }); |
| 4630 | return `${suggestion.name}.md`; |
| 4631 | }, |
| 4632 | async AcceptSkillSuggestion(suggestion: SkillSuggestion) { |
| 4633 | emit({ kind: "notice", level: "info", text: `created suggested skill → ${suggestion.name}` }); |
| 4634 | return `.reasonix/skills/${suggestion.name}/SKILL.md`; |
| 4635 | }, |
| 4636 | async MemorySuggestionsForTab(_tabID: string) { |
| 4637 | return this.MemorySuggestions(); |
| 4638 | }, |
| 4639 | async AcceptMemorySuggestionForTab(_tabID: string, suggestion: MemorySuggestion) { |
| 4640 | return this.AcceptMemorySuggestion(suggestion); |
| 4641 | }, |
| 4642 | async AcceptSkillSuggestionForTab(_tabID: string, suggestion: SkillSuggestion) { |
| 4643 | return this.AcceptSkillSuggestion(suggestion); |
| 4644 | }, |
| 4645 | async MemoryForTab(_tabID: string) { |
| 4646 | return this.Memory(); |
| 4647 | }, |
| 4648 | async MemoryRevisions(_ref: string) { |
| 4649 | return []; |
| 4650 | }, |
| 4651 | async MemoryRevisionsForTab(_tabID: string, ref: string) { |
| 4652 | return this.MemoryRevisions(ref); |
| 4653 | }, |
| 4654 | async RestoreMemoryRevision(ref: string, revision: number) { |
| 4655 | emit({ kind: "notice", level: "info", text: `restored revision → ${ref}@${revision}` }); |
| 4656 | return { |
| 4657 | id: ref, |
| 4658 | revision: revision + 1, |
| 4659 | name: ref, |
| 4660 | description: "Restored memory revision", |
| 4661 | type: "project", |
| 4662 | scope: "project", |
| 4663 | body: "Restored guidance.", |
| 4664 | freshness: "fresh", |
| 4665 | }; |
| 4666 | }, |
| 4667 | async RestoreMemoryRevisionForTab(_tabID: string, ref: string, revision: number) { |
| 4668 | return this.RestoreMemoryRevision(ref, revision); |
| 4669 | }, |
| 4670 | async Remember(_scope: string, _note: string) { |
| 4671 | emit({ kind: "notice", level: "info", text: `remembered → ${_scope}` }); |
| 4672 | return `${_scope} REASONIX.md (mock): ${_note}`; |
| 4673 | }, |
| 4674 | async RememberForTab(_tabID: string, scope: string, note: string) { |
| 4675 | return this.Remember(scope, note); |
| 4676 | }, |
| 4677 | async Forget(_name: string) { |
| 4678 | emit({ kind: "notice", level: "info", text: `forgot → ${_name}` }); |
| 4679 | }, |
| 4680 | async ForgetForTab(_tabID: string, name: string) { |
| 4681 | return this.Forget(name); |
| 4682 | }, |
| 4683 | async RestoreArchivedMemory(archivePath: string) { |
| 4684 | emit({ kind: "notice", level: "info", text: `restored → ${archivePath}` }); |
| 4685 | return { |
| 4686 | id: "mock-restored-memory", |
| 4687 | revision: 2, |
| 4688 | name: "restored-memory", |
| 4689 | description: "Recovered archived memory", |
| 4690 | type: "project", |
| 4691 | scope: "project", |
| 4692 | body: "Recovered guidance.", |
| 4693 | freshness: "fresh", |
| 4694 | }; |
| 4695 | }, |
| 4696 | async RestoreArchivedMemoryForTab(_tabID: string, archivePath: string) { |
| 4697 | return this.RestoreArchivedMemory(archivePath); |
| 4698 | }, |
| 4699 | async SaveDoc(_path: string, _body: string) { |
| 4700 | emit({ kind: "notice", level: "info", text: `saved → ${_path}` }); |
| 4701 | return _path; |
| 4702 | }, |
| 4703 | async SaveDocForTab(_tabID: string, path: string, body: string) { |
| 4704 | return this.SaveDoc(path, body); |
| 4705 | }, |
| 4706 | async DesktopStartupSettings() { |
| 4707 | const { bot, desktopLanguage, desktopTheme, desktopThemeStyle, desktopTerminalTheme, displayMode, sessionExperience, reasoningDisplayMode, reasoningDisplayModeExplicit, statusBarStyle, statusBarItems, checkUpdates, updaterEnabled, conversationWidth } = settings; |
| 4708 | return JSON.parse(JSON.stringify({ |
| 4709 | bot, |
| 4710 | desktopLanguage, |
| 4711 | desktopTheme, |
| 4712 | desktopThemeStyle, |
| 4713 | desktopTerminalTheme, |
| 4714 | displayMode, sessionExperience, reasoningDisplayMode, reasoningDisplayModeExplicit, |
| 4715 | statusBarStyle, |
| 4716 | statusBarItems, |
| 4717 | checkUpdates, |
| 4718 | updaterEnabled, |
| 4719 | conversationWidth, |
| 4720 | })) as DesktopStartupSettingsView; |
| 4721 | }, |
| 4722 | ...makeMockModelSettingsBindings(settings, loadMockProviderCatalog, mockProviderPresetViews), |
| 4723 | async StorageSettings() { return { defaultWorkspace: cwd, statePath: `${cwd}/.reasonix`, cachePath: `${cwd}/.reasonix/cache`, extensionsPath: `${cwd}/.reasonix/plugins` }; }, |
| 4724 | async HooksSettings(scope: string) { |
| 4725 | const key = scope === "project" ? "project" : "global"; |
| 4726 | return JSON.parse(JSON.stringify(hookSettings[key])) as HooksSettingsView; |
| 4727 | }, |
| 4728 | async SaveHooksSettings(scope: string, hooks: HookConfigView[]) { |
| 4729 | const key = scope === "project" ? "project" : "global"; |
| 4730 | hookSettings[key].hooks = JSON.parse(JSON.stringify(hooks)) as HookConfigView[]; |
| 4731 | }, |
| 4732 | async SaveHooksSettingsForRoot(scope: string, _projectRoot: string, hooks: HookConfigView[]) { |
| 4733 | const key = scope === "project" ? "project" : "global"; |
| 4734 | hookSettings[key].hooks = JSON.parse(JSON.stringify(hooks)) as HookConfigView[]; |
| 4735 | }, |
| 4736 | async TrustProjectHooks() { |
| 4737 | // Compatibility no-op: project hooks are enabled automatically. |
| 4738 | }, |
| 4739 | async TrustProjectHooksForRoot(_projectRoot: string) { |
| 4740 | // Compatibility no-op: project hooks are enabled automatically. |
| 4741 | }, |
| 4742 | async SetDefaultModel(ref: string) { |
| 4743 | settings.defaultModel = ref; |
| 4744 | }, |
| 4745 | async SetPlannerModel(ref: string) { |
| 4746 | settings.plannerModel = ref; |
| 4747 | }, |
| 4748 | async SetWebSearchModel(ref: string) { |
| 4749 | if (ref && ref !== "auto" && !(settings.webSearchModels ?? []).includes(ref)) throw new Error("Search model unavailable"); |
| 4750 | settings.webSearchModel = ref || "auto"; |
| 4751 | settings.effectiveWebSearchModel = settings.webSearchModel; |
| 4752 | settings.webSearchModelStatus = "ready"; |
| 4753 | settings.webSearchModelReason = ""; |
| 4754 | }, |
| 4755 | async SetVisionModel(ref: string) { |
| 4756 | settings.visionModel = ref; |
| 4757 | }, |
| 4758 | async SetSubagentModel(ref: string) { |
| 4759 | settings.subagentModel = ref; |
| 4760 | }, |
| 4761 | async SetSubagentEffort(level: string) { |
| 4762 | settings.subagentEffort = level; |
| 4763 | }, |
| 4764 | async SetMaxSubagentDepth(depth: number) { |
| 4765 | settings.agent = { ...settings.agent, maxSubagentDepth: depth <= 1 ? 1 : 2 }; |
| 4766 | }, |
| 4767 | async SetMaxSubagentConcurrency(n: number) { |
| 4768 | const total = Math.max(1, Math.min(32, Math.floor(n) || 6)); |
| 4769 | const writers = Math.min(total, Math.max(1, settings.agent.maxParallelWriters || 3)); |
| 4770 | settings.agent = { ...settings.agent, maxSubagentConcurrency: total, maxParallelWriters: writers }; |
| 4771 | }, |
| 4772 | async SetMaxParallelWriters(n: number) { |
| 4773 | const total = Math.max(1, Math.min(32, settings.agent.maxSubagentConcurrency || 6)); |
| 4774 | const writers = Math.max(1, Math.min(total, Math.floor(n) || 3)); |
| 4775 | settings.agent = { ...settings.agent, maxParallelWriters: writers }; |
| 4776 | }, |
| 4777 | async SetAutoPlan(mode: string) { |
| 4778 | if (mode !== "off") throw new Error("Automatic plan mode has been retired; use Plan Mode explicitly."); |
| 4779 | settings.autoPlan = "off"; |
| 4780 | }, |
| 4781 | async SetDefaultToolApprovalMode(mode: string) { |
| 4782 | settings.defaultToolApprovalMode = normalizeToolApprovalMode(mode); |
| 4783 | }, |
| 4784 | async SetDefaultAutoRecoveryCheckpoint(_enabled: boolean) { |
| 4785 | // Legacy no-op; Auto Guard is retired. |
| 4786 | }, |
| 4787 | async SetConnectionKey(name: string, value: string) { |
| 4788 | const p = settings.providers.find(p => p.name === name); |
| 4789 | if (!p) throw new Error("Connection not found"); |
| 4790 | p.apiKeyEnv = `REASONIX_CONNECTION_${crypto.randomUUID().replaceAll("-", "")}_KEY`; |
| 4791 | p.keySet = Boolean(value.trim()); |
| 4792 | return ""; |
| 4793 | }, |
| 4794 | async AddProviderConnectionWithOptions(_presetID: string, sourceName: string, key: string, baseURL: string, kind: string) { |
| 4795 | const source = settings.providers.find(p => p.name === sourceName); |
| 4796 | if (!source) throw new Error("Connection template unavailable in preview"); |
| 4797 | settings.providers.push({...source, kind:kind || source.kind, baseUrl:baseURL || source.baseUrl, requestUrl:"", chatUrl:"", modelsUrl:"", name:`${source.name}-${crypto.randomUUID()}`, builtIn:false, added:true, apiKeyEnv:`REASONIX_CONNECTION_${crypto.randomUUID().replaceAll("-", "")}_KEY`, keySet:Boolean(key.trim())}); |
| 4798 | return ""; |
| 4799 | }, |
| 4800 | async AddProviderConnectionWithURL(_presetID: string, sourceName: string, key: string, baseURL: string) { |
| 4801 | const source = settings.providers.find(p => p.name === sourceName); |
| 4802 | if (!source) throw new Error("Connection template unavailable in preview"); |
| 4803 | settings.providers.push({...source, baseUrl:baseURL, requestUrl:"", chatUrl:"", modelsUrl:"", name:`${source.name}-${crypto.randomUUID()}`, builtIn:false, added:true, apiKeyEnv:`REASONIX_CONNECTION_${crypto.randomUUID().replaceAll("-", "")}_KEY`, keySet:Boolean(key.trim())}); |
| 4804 | return ""; |
| 4805 | }, |
| 4806 | async AddProviderConnection(_presetID: string, sourceName: string, key: string) { |
| 4807 | const source = settings.providers.find(p => p.name === sourceName); |
| 4808 | if (!source) throw new Error("Connection template unavailable in preview"); |
| 4809 | settings.providers.push({...source, name:`${source.name}-${crypto.randomUUID()}`, builtIn:false, added:true, apiKeyEnv:`REASONIX_CONNECTION_${crypto.randomUUID().replaceAll("-", "")}_KEY`, keySet:Boolean(key.trim())}); |
| 4810 | return ""; |
| 4811 | }, |
| 4812 | async RenameProviderConnections(names: string[], displayName: string) { |
| 4813 | for (const name of names) if (!settings.providers.some(p => p.name === name)) throw new Error(`Provider ${name} not found`); |
| 4814 | settings.providers = settings.providers.map(p => names.includes(p.name) ? {...p, displayName: displayName.trim()} : p); |
| 4815 | }, |
| 4816 | async SaveProvider(p: ProviderView) { |
| 4817 | p.added = true; |
| 4818 | const i = settings.providers.findIndex((x) => x.name === p.name); |
| 4819 | if (i >= 0) settings.providers[i] = { ...settings.providers[i], ...p }; |
| 4820 | else settings.providers.push(p); |
| 4821 | }, |
| 4822 | async SetProviderWebSearch(names: string[], enabled: boolean) { |
| 4823 | const requested = new Set(names); |
| 4824 | settings.providers = settings.providers.map((provider) => ( |
| 4825 | requested.has(provider.name) ? { ...provider, webSearch: enabled } : provider |
| 4826 | )); |
| 4827 | }, |
| 4828 | async SaveProviderModelCatalogs(updates: ProviderModelCatalogUpdate[]) { |
| 4829 | const applied: string[] = []; |
| 4830 | for (const update of updates) { |
| 4831 | const i = settings.providers.findIndex((provider) => provider.name === update.name); |
| 4832 | if (i < 0) continue; |
| 4833 | const current = settings.providers[i]; |
| 4834 | if (!update.expectedFingerprint || current.modelCatalogFingerprint !== update.expectedFingerprint) continue; |
| 4835 | settings.providers[i] = { |
| 4836 | ...current, |
| 4837 | models: [...update.models], |
| 4838 | default: update.default, |
| 4839 | visionModels: [...update.visionModels], |
| 4840 | modelCatalogFingerprint: `${update.expectedFingerprint}:updated`, |
| 4841 | }; |
| 4842 | applied.push(update.name); |
| 4843 | } |
| 4844 | return applied; |
| 4845 | }, |
| 4846 | async SaveProviderWithKey(p: ProviderView, key: string) { |
| 4847 | p.added = true; |
| 4848 | p.keySet = Boolean(key.trim()) || p.keySet; |
| 4849 | const i = settings.providers.findIndex((x) => x.name === p.name); |
| 4850 | if (i >= 0) settings.providers[i] = p; |
| 4851 | else settings.providers.push(p); |
| 4852 | return ""; |
| 4853 | }, |
| 4854 | async AddOfficialProviderAccess(kind: string, key: string) { |
| 4855 | const templates: Record<string, ProviderView> = { |
| 4856 | deepseek: { name: "deepseek", builtIn: true, added: true, kind: "openai", baseUrl: "https://api.deepseek.com", modelsUrl: "", models: ["deepseek-v4-flash", "deepseek-v4-pro"], visionModels: [], visionModelsConfigured: false, default: "deepseek-v4-flash", apiKeyEnv: "DEEPSEEK_API_KEY", keySet: !!key.trim(), balanceUrl: "https://api.deepseek.com/user/balance", contextWindow: 1_000_000, reasoningProtocol: "", thinking: "enabled", webSearch: true, serverWebSearchCapability: true, supportedEfforts: [], defaultEffort: "", modelOverrides: [{ model: "deepseek-v4-flash", reasoningProtocol: "", supportedEfforts: ["disabled", "low", "high", "max"], defaultEffort: "high" }, { model: "deepseek-v4-pro", reasoningProtocol: "", supportedEfforts: ["disabled", "high", "max"], defaultEffort: "high" }] }, |
| 4857 | }; |
| 4858 | const next = templates[kind]; |
| 4859 | if (!next) throw new Error(`unknown official provider template ${kind}`); |
| 4860 | const i = settings.providers.findIndex((x) => x.name === next.name); |
| 4861 | if (i >= 0) settings.providers[i] = { ...settings.providers[i], ...next, keySet: next.keySet || settings.providers[i].keySet }; |
| 4862 | else settings.providers.push(next); |
| 4863 | return ""; |
| 4864 | }, |
| 4865 | async UpgradeDeepSeekProviderAccess(name: string) { |
| 4866 | const family = new Set(["deepseek", "deepseek-flash", "deepseek-pro"]); |
| 4867 | let changed = false; |
| 4868 | settings.providers = settings.providers.map((provider) => { |
| 4869 | if ((name === "deepseek" ? family.has(provider.name) : provider.name === name) && provider.recommendedUpgradeAvailable) { |
| 4870 | changed = true; |
| 4871 | return { |
| 4872 | ...provider, |
| 4873 | kind: "anthropic", |
| 4874 | baseUrl: "https://api.deepseek.com/anthropic", |
| 4875 | thinking: provider.thinking || "enabled", |
| 4876 | webSearch: provider.webSearch ?? true, |
| 4877 | serverWebSearchCapability: true, |
| 4878 | recommendedUpgradeAvailable: false, |
| 4879 | }; |
| 4880 | } |
| 4881 | return provider; |
| 4882 | }); |
| 4883 | if (!changed) throw new Error(`DeepSeek provider ${name} is not eligible for upgrade`); |
| 4884 | return ""; |
| 4885 | }, |
| 4886 | async AddProviderPresetAccess(id: string, key: string) { |
| 4887 | const preset = settings.providerPresets.find((p) => p.id === id); |
| 4888 | if (!preset) throw new Error(`unknown provider preset ${id}`); |
| 4889 | const next = cloneMockProviderTemplates(id, key); |
| 4890 | if (!next) throw new Error(`unknown provider preset ${id}`); |
| 4891 | for (const provider of next) { |
| 4892 | const i = settings.providers.findIndex((x) => x.name === provider.name); |
| 4893 | if (i >= 0) settings.providers[i] = { ...settings.providers[i], ...provider, keySet: provider.keySet || settings.providers[i].keySet }; |
| 4894 | else settings.providers.push(provider); |
| 4895 | } |
| 4896 | preset.added = true; |
| 4897 | preset.status = "installed"; |
| 4898 | preset.statusProviderNames = [...preset.providerNames]; |
| 4899 | preset.keySet = preset.keySet || !!key.trim(); |
| 4900 | preset.configured = !preset.requiresKey || preset.keySet; |
| 4901 | return ""; |
| 4902 | }, |
| 4903 | async ResetProviderPresetAccess(id: string) { |
| 4904 | const preset = settings.providerPresets.find((p) => p.id === id); |
| 4905 | if (!preset) throw new Error(`unknown provider preset ${id}`); |
| 4906 | const next = cloneMockProviderTemplates(id, ""); |
| 4907 | if (!next) throw new Error(`unknown provider preset ${id}`); |
| 4908 | for (const provider of next) { |
| 4909 | const i = settings.providers.findIndex((x) => x.name === provider.name); |
| 4910 | if (i < 0) throw new Error(`provider preset ${id} cannot be reset because no same-name provider exists`); |
| 4911 | const existing = settings.providers[i]; |
| 4912 | settings.providers[i] = { |
| 4913 | ...provider, |
| 4914 | added: true, |
| 4915 | keySet: existing.apiKeyEnv === provider.apiKeyEnv ? existing.keySet : provider.keySet, |
| 4916 | }; |
| 4917 | } |
| 4918 | preset.added = true; |
| 4919 | preset.status = "installed"; |
| 4920 | preset.statusProviderNames = [...preset.providerNames]; |
| 4921 | preset.keySet = preset.keySet || next.every((provider) => settings.providers.some((item) => item.name === provider.name && item.keySet)); |
| 4922 | preset.configured = !preset.requiresKey || preset.keySet; |
| 4923 | }, |
| 4924 | async FetchProviderModels(p: ProviderView) { |
| 4925 | if (!p.baseUrl.trim()) throw new Error(t("settings.fetchModelsMissingBaseUrl")); |
| 4926 | if (providerRequiresKey(p) && !p.apiKeyEnv.trim()) throw new Error(t("settings.fetchModelsMissingKeyEnv")); |
| 4927 | await delay(350); |
| 4928 | if (p.baseUrl.includes("deepseek")) return ["deepseek-v4-flash", "deepseek-v4-pro"]; |
| 4929 | if (p.baseUrl.includes("token-plan")) return ["mimo-v2.5", "mimo-v2.5-pro"]; |
| 4930 | if (p.baseUrl.includes("xiaomimimo")) return ["mimo-v2.5-pro", "mimo-v2.5"]; |
| 4931 | return ["gpt-5", "gpt-5-mini", "qwen3-coder"]; |
| 4932 | }, |
| 4933 | async FetchProviderModelCatalogDraft(p: ProviderView, _key: string) { |
| 4934 | return this.FetchProviderModelCatalog(p); |
| 4935 | }, |
| 4936 | async TestProviderModel(_p: ProviderView, _model: string, _key: string) {}, |
| 4937 | async FetchProviderModelCatalog(p: ProviderView) { |
| 4938 | const models = await this.FetchProviderModels(p); |
| 4939 | return models.map((model) => ({ |
| 4940 | model, |
| 4941 | inputModalities: p.modelCapabilities?.find((item) => item.model === model)?.inputModalities ?? [], |
| 4942 | state: p.modelCapabilities?.find((item) => item.model === model)?.state ?? "unknown", |
| 4943 | source: "adapter", |
| 4944 | })); |
| 4945 | }, |
| 4946 | async FetchAllProviderModels(providers: ProviderView[]) { |
| 4947 | const out: Record<string, string[]> = {}; |
| 4948 | for (const p of providers) { |
| 4949 | try { |
| 4950 | out[p.name] = await this.FetchProviderModels(p); |
| 4951 | } catch { |
| 4952 | out[p.name] = []; |
| 4953 | } |
| 4954 | } |
| 4955 | return out; |
| 4956 | }, |
| 4957 | async FetchAllProviderModelCatalogs(providers: ProviderView[]) { |
| 4958 | const out: Record<string, ProviderModelCapabilityView[]> = {}; |
| 4959 | for (const p of providers) { |
| 4960 | try { |
| 4961 | out[p.name] = await this.FetchProviderModelCatalog(p); |
| 4962 | } catch { |
| 4963 | out[p.name] = []; |
| 4964 | } |
| 4965 | } |
| 4966 | return out; |
| 4967 | }, |
| 4968 | async DeleteProvider(name: string) { |
| 4969 | settings.providers = settings.providers.filter((p) => p.name !== name); |
| 4970 | }, |
| 4971 | async RemoveProviderAccess(name: string) { settings.providers = removeProviderAccessesForMock(settings.providers, [name]); }, |
| 4972 | async RemoveProviderAccesses(names: string[]) { settings.providers = removeProviderAccessesForMock(settings.providers, names); }, |
| 4973 | async SaveProviderKey(apiKeyEnv: string, _value: string) { |
| 4974 | settings.providers.forEach((p) => { |
| 4975 | if (p.apiKeyEnv === apiKeyEnv) p.keySet = true; |
| 4976 | }); |
| 4977 | return ""; |
| 4978 | }, |
| 4979 | async SetProviderKey(apiKeyEnv: string, _value: string) { |
| 4980 | settings.providers.forEach((p) => { |
| 4981 | if (p.apiKeyEnv === apiKeyEnv) p.keySet = true; |
| 4982 | }); |
| 4983 | return ""; |
| 4984 | }, |
| 4985 | async ClearProviderKey(apiKeyEnv: string) { |
| 4986 | settings.providers.forEach((p) => { |
| 4987 | if (p.apiKeyEnv === apiKeyEnv) p.keySet = false; |
| 4988 | }); |
| 4989 | }, |
| 4990 | async SetPermissionMode(mode: string) { |
| 4991 | settings.permissions.mode = mode; |
| 4992 | }, |
| 4993 | async AddPermissionRule(list: string, rule: string) { |
| 4994 | const k = list as "allow" | "ask" | "deny"; |
| 4995 | if (settings.permissions[k] && !settings.permissions[k].includes(rule)) settings.permissions[k].push(rule); |
| 4996 | }, |
| 4997 | async RemovePermissionRule(list: string, rule: string) { |
| 4998 | const k = list as "allow" | "ask" | "deny"; |
| 4999 | settings.permissions[k] = settings.permissions[k].filter((r) => r !== rule); |
| 5000 | }, |
| 5001 | async ReloadSettings() {}, |
| 5002 | async SetShellPreference(prefer: string) { |
| 5003 | const sb = settings.sandbox; |
| 5004 | if (!sb) return; |
| 5005 | sb.shell = prefer; |
| 5006 | sb.resolvedShell = browserPreviewEffectiveShell(prefer); |
| 5007 | sb.shellReloadRequired = sb.resolvedShell !== sb.effectiveShell; |
| 5008 | }, |
| 5009 | async InstallShellSupport(id: string): Promise<ShellInstallResult> { |
| 5010 | if (id !== "git-for-windows") throw new Error(`unknown shell support action ${id}`); |
| 5011 | if (browserPlatformOverride() !== "windows") return { status: "unsupported_platform", reason: "shell helper install is only available on Windows" }; |
| 5012 | return { |
| 5013 | status: "manual_required", |
| 5014 | reason: "automatic installation is disabled because Git for Windows cannot reliably honor user scope", |
| 5015 | manualUrl: "https://git-scm.com/download/win", |
| 5016 | }; |
| 5017 | }, |
| 5018 | async CancelShellInstall() {}, |
| 5019 | async SetSandbox(bash: string, network: boolean, workspaceRoot: string, allowWrite: string[], shell: string) { |
| 5020 | const effectiveWorkspaceRoot = workspaceRoot.trim() || cwd; |
| 5021 | const prev = settings.sandbox; |
| 5022 | const effectiveShell = browserPreviewEffectiveShell(shell); |
| 5023 | const shellSupport = browserPreviewShellSupport(browserPlatformOverride()); |
| 5024 | settings.sandbox = { bash, network, workspaceRoot, allowWrite, effectiveWorkspaceRoot, effectiveWriteRoots: [effectiveWorkspaceRoot, ...allowWrite], shell, effectiveShell, |
| 5025 | resolvedShell: effectiveShell, shellReloadRequired: false, |
| 5026 | shellCapabilities: prev?.shellCapabilities ?? shellSupport.shellCapabilities, shellInstallAction: prev?.shellInstallAction ?? shellSupport.shellInstallAction, |
| 5027 | shellRepairGuidance: prev?.shellRepairGuidance ?? shellSupport.shellRepairGuidance, |
| 5028 | gitCapability: prev?.gitCapability ?? shellSupport.gitCapability, gitRepairGuidance: prev?.gitRepairGuidance ?? shellSupport.gitRepairGuidance }; |
| 5029 | }, |
| 5030 | async SetNetwork(n: NetworkView) { |
| 5031 | settings.network = n; |
| 5032 | }, |
| 5033 | async SetBotSettings(b: BotSettingsView) { |
| 5034 | settings.bot = JSON.parse(JSON.stringify(b)) as BotSettingsView; |
| 5035 | }, |
| 5036 | async SetBotConnectionToolApprovalMode(connID, mode) { |
| 5037 | const conn = settings.bot.connections.find((c) => c.id === connID); |
| 5038 | if (conn) conn.toolApprovalMode = mode as any; |
| 5039 | }, |
| 5040 | async SetBotDingtalkToolApprovalMode(mode) { |
| 5041 | settings.bot.dingtalk.toolApprovalMode = mode as any; |
| 5042 | }, |
| 5043 | async SetBotSecret(envName: string, _value: string) { |
| 5044 | const name = envName.trim(); |
| 5045 | if (settings.bot.qq.appSecretEnv === name) settings.bot.qq.secretSet = true; |
| 5046 | if (settings.bot.feishu.appSecretEnv === name) settings.bot.feishu.secretSet = true; |
| 5047 | if (settings.bot.weixin.tokenEnv === name) settings.bot.weixin.tokenSet = true; |
| 5048 | settings.bot.connections = settings.bot.connections.map((connection) => ({ |
| 5049 | ...connection, |
| 5050 | credential: connection.credential.appSecretEnv === name || connection.credential.tokenEnv === name |
| 5051 | ? { ...connection.credential, secretSet: true } |
| 5052 | : connection.credential, |
| 5053 | })); |
| 5054 | }, |
| 5055 | async ClearBotSecret(envName: string) { |
| 5056 | const name = envName.trim(); |
| 5057 | if (settings.bot.qq.appSecretEnv === name) settings.bot.qq.secretSet = false; |
| 5058 | if (settings.bot.feishu.appSecretEnv === name) settings.bot.feishu.secretSet = false; |
| 5059 | if (settings.bot.weixin.tokenEnv === name) settings.bot.weixin.tokenSet = false; |
| 5060 | settings.bot.connections = settings.bot.connections.map((connection) => ({ |
| 5061 | ...connection, |
| 5062 | credential: connection.credential.appSecretEnv === name || connection.credential.tokenEnv === name |
| 5063 | ? { ...connection.credential, secretSet: false } |
| 5064 | : connection.credential, |
| 5065 | })); |
| 5066 | }, |
| 5067 | async BotRuntimeStatus() { |
| 5068 | const qqRunning = settings.bot.qq.enabled && settings.bot.qq.appId.trim() && settings.bot.qq.secretSet; |
| 5069 | const runningConnections = (qqRunning ? 1 : 0) + settings.bot.connections.filter((connection) => connection.enabled && connection.status === "connected").length; |
| 5070 | const dingtalkRunning = Boolean(settings.bot.dingtalk.enabled && settings.bot.dingtalk.clientId.trim() && settings.bot.dingtalk.secretSet); |
| 5071 | const running = settings.bot.enabled && runningConnections > 0; |
| 5072 | return { |
| 5073 | running, |
| 5074 | status: running ? "running" : "stopped", |
| 5075 | message: running ? `${runningConnections} bot connection(s) running` : "bot runtime is not started", |
| 5076 | connections: runningConnections, |
| 5077 | startedAt: running ? new Date(t0).toISOString() : "", |
| 5078 | platforms: { |
| 5079 | dingtalk: dingtalkRunning ? "running" : settings.bot.dingtalk.clientId.trim() ? "configured" : "", |
| 5080 | qq: qqRunning ? "running" : settings.bot.qq.appId.trim() ? "configured" : "", |
| 5081 | ...Object.fromEntries(settings.bot.connections |
| 5082 | .filter((connection) => connection.provider !== "qq") |
| 5083 | .map((connection) => [connection.provider, connection.enabled && connection.status === "connected" ? "running" : connection.credential.secretSet ? "configured" : ""])), |
| 5084 | }, |
| 5085 | }; |
| 5086 | }, |
| 5087 | async StartBotConnectionInstall(provider: string, domain: string) { |
| 5088 | const normalizedProvider = provider === "weixin" ? "weixin" : "feishu"; |
| 5089 | const normalizedDomain = normalizedProvider === "weixin" ? "weixin" : domain === "lark" ? "lark" : "feishu"; |
| 5090 | return { |
| 5091 | ok: true, |
| 5092 | provider: normalizedProvider, |
| 5093 | domain: normalizedDomain, |
| 5094 | installId: `mock-${normalizedProvider}-${normalizedDomain}`, |
| 5095 | url: "https://example.com/reasonix-bot-qr", |
| 5096 | deviceCode: "MOCKDEVICE", |
| 5097 | userCode: normalizedProvider === "weixin" ? "" : "MOCK-CODE", |
| 5098 | interval: 3, |
| 5099 | expireIn: 300, |
| 5100 | message: "", |
| 5101 | }; |
| 5102 | }, |
| 5103 | async PollBotConnectionInstall(installID: string) { |
| 5104 | const isWeixin = installID.includes("weixin"); |
| 5105 | const domain = installID.includes("lark") ? "lark" : isWeixin ? "weixin" : "feishu"; |
| 5106 | const provider = isWeixin ? "weixin" : "feishu"; |
| 5107 | const connection = { |
| 5108 | id: `${provider}-${domain}`, |
| 5109 | provider, |
| 5110 | domain, |
| 5111 | label: domain === "lark" ? "Lark" : domain === "weixin" ? "微信" : "飞书", |
| 5112 | enabled: true, |
| 5113 | status: "connected", |
| 5114 | model: "", |
| 5115 | toolApprovalMode: "", |
| 5116 | workspaceRoot: "", |
| 5117 | access: { enabled: true, allowAll: false, pairingEnabled: true, users: [provider === "weixin" ? "wxid_mock_user_001" : "ou_mock_user_001"], groups: [], approvers: [], admins: [] }, |
| 5118 | credential: { |
| 5119 | appId: provider === "feishu" ? "cli_mock" : "", |
| 5120 | appSecretEnv: provider === "feishu" ? (domain === "lark" ? "LARK_BOT_APP_SECRET" : "FEISHU_BOT_APP_SECRET") : "", |
| 5121 | accountId: provider === "weixin" ? "mock-account" : "", |
| 5122 | tokenEnv: provider === "weixin" ? "WEIXIN_BOT_TOKEN" : "", |
| 5123 | secretSet: true, |
| 5124 | }, |
| 5125 | sessionMappings: [], |
| 5126 | lastError: "", |
| 5127 | createdAt: new Date().toISOString(), |
| 5128 | updatedAt: new Date().toISOString(), |
| 5129 | }; |
| 5130 | settings.bot.connections = [...settings.bot.connections.filter((c) => c.id !== connection.id), connection]; |
| 5131 | return { done: true, connection, status: "connected", message: "connected", error: "" }; |
| 5132 | }, |
| 5133 | async DiagnoseBotConnection(id: string) { |
| 5134 | const connection = settings.bot.connections.find((c) => c.id === id); |
| 5135 | const occurredAt = new Date().toISOString(); |
| 5136 | return connection |
| 5137 | ? { id, label: connection.label, status: connection.enabled ? "ok" : "disabled", message: connection.enabled ? "连接配置已保存。" : "连接已保存但未启用。", messageId: "", phase: "config", code: connection.enabled ? "config_ok" : "connection_disabled", reportKind: "", reportDetail: "", occurredAt } |
| 5138 | : { id, label: "", status: "missing", message: "未找到连接。", messageId: "", phase: "config", code: "connection_missing", reportKind: "bot", reportDetail: JSON.stringify({ schemaVersion: 2, kind: "bot", source: "bot.runtime", label: "bot.mock.config", message: "mock missing bot connection", errorType: "BotConnectionDiagnostic", errorMessage: "bot connection record was not found", topFrame: "bot.config", occurredAt }), occurredAt }; |
| 5139 | }, |
| 5140 | async TestBotConnection(id: string, target?: string) { |
| 5141 | const diag = await this.DiagnoseBotConnection(id); |
| 5142 | if (target?.trim()) return { ...diag, message: `Mock test sent to ${target.trim()}`, messageId: "mock-message-id" }; |
| 5143 | return diag; |
| 5144 | }, |
| 5145 | async TestDingtalkBot() { |
| 5146 | const occurredAt = new Date().toISOString(); |
| 5147 | return { id: "dingtalk", label: "DingTalk", status: "ok", message: "Mock dingtalk test sent", messageId: "mock-dingtalk-id", phase: "send", code: "dingtalk_test_send_ok", reportKind: "", reportDetail: "", occurredAt }; |
| 5148 | }, |
| 5149 | ...createDesktopPreferencesMock(settings), |
| 5150 | async SetDesktopAppearance(theme: string, style: string) { |
| 5151 | settings.desktopTheme = theme === "auto" || theme === "light" ? theme : "dark"; |
| 5152 | settings.desktopThemeStyle = style; |
| 5153 | mockThemeMode = settings.desktopTheme as "auto" | "light" | "dark"; |
| 5154 | if (["graphite","aurora","slate","carbon","nocturne","amber"].includes(style)) { |
| 5155 | mockBaseStyle = style; |
| 5156 | } |
| 5157 | }, |
| 5158 | async SetDesktopTerminalTheme(theme: string) { |
| 5159 | settings.desktopTerminalTheme = theme === "dark" || theme === "light" ? theme : "auto"; |
| 5160 | }, |
| 5161 | async ListThemePacks() { |
| 5162 | const baseActive = !mockActiveThemeId; |
| 5163 | return mockThemePacks.map((p) => { |
| 5164 | const kind = p.kind || (p.builtin ? "base" : "user"); |
| 5165 | let active = false; |
| 5166 | if (kind === "base") active = baseActive && p.id === mockBaseStyle; |
| 5167 | else active = p.id === mockActiveThemeId; |
| 5168 | return { ...p, active, tokens: { light: { ...(p.tokens.light || {}) }, dark: { ...(p.tokens.dark || {}) } }, recipes: { ...p.recipes } }; |
| 5169 | }); |
| 5170 | }, |
| 5171 | async GetActiveThemePack() { |
| 5172 | // Base style ids are never active packs in the redesigned model. |
| 5173 | const pack = mockActiveThemeId && !["graphite","aurora","slate","carbon","nocturne","amber"].includes(mockActiveThemeId) |
| 5174 | ? mockThemePacks.find((p) => p.id === mockActiveThemeId) |
| 5175 | : null; |
| 5176 | return { activeThemeId: pack ? mockActiveThemeId : "", pack: pack ? { ...pack, active: true } : null }; |
| 5177 | }, |
| 5178 | async GetThemeExperience() { |
| 5179 | const pack = mockActiveThemeId && !["graphite","aurora","slate","carbon","nocturne","amber"].includes(mockActiveThemeId) |
| 5180 | ? mockThemePacks.find((p) => p.id === mockActiveThemeId) |
| 5181 | : null; |
| 5182 | return { |
| 5183 | themeMode: mockThemeMode, |
| 5184 | baseStyle: mockBaseStyle, |
| 5185 | effectiveStyle: pack?.baseStyle || mockBaseStyle, |
| 5186 | activeThemeId: pack ? mockActiveThemeId : "", |
| 5187 | activePack: pack ? { ...pack, active: true } : null, |
| 5188 | }; |
| 5189 | }, |
| 5190 | async ActivateThemePack(id: string) { |
| 5191 | const next = String(id || "").trim(); |
| 5192 | if (["graphite","aurora","slate","carbon","nocturne","amber"].includes(next)) { |
| 5193 | throw new Error(`base style ${next} is not a theme pack; use ActivateBaseStyle`); |
| 5194 | } |
| 5195 | mockActiveThemeId = next; |
| 5196 | }, |
| 5197 | async ActivateBaseStyle(style: string) { |
| 5198 | const s = String(style || "").trim().toLowerCase(); |
| 5199 | if (!["graphite","aurora","slate","carbon","nocturne","amber"].includes(s)) { |
| 5200 | throw new Error(`unknown base style ${s}`); |
| 5201 | } |
| 5202 | mockBaseStyle = s; |
| 5203 | mockActiveThemeId = ""; |
| 5204 | }, |
| 5205 | async DisableThemePack() { |
| 5206 | mockActiveThemeId = ""; |
| 5207 | }, |
| 5208 | async RestoreGraphiteAppearance() { |
| 5209 | mockBaseStyle = "graphite"; |
| 5210 | mockActiveThemeId = ""; |
| 5211 | }, |
| 5212 | async ResetThemePack() { |
| 5213 | mockActiveThemeId = ""; |
| 5214 | }, |
| 5215 | async SaveThemePack(input: import("./themePack").ThemeSaveInput) { |
| 5216 | const pack: import("./themePack").ThemePackView = { |
| 5217 | id: input.id, |
| 5218 | name: input.name, |
| 5219 | author: input.author, |
| 5220 | description: input.description, |
| 5221 | license: input.license, |
| 5222 | baseStyle: input.baseStyle, |
| 5223 | builtin: false, |
| 5224 | kind: "user", |
| 5225 | active: Boolean(input.activate), |
| 5226 | hasBackground: Boolean( |
| 5227 | (input.background && (input.backgroundDataUrl || input.background.image)) || |
| 5228 | (input.taskBackground && (input.taskBackgroundDataUrl || input.taskBackground.image)), |
| 5229 | ), |
| 5230 | backgroundUrl: input.backgroundDataUrl || "", |
| 5231 | taskBackgroundUrl: input.taskBackgroundDataUrl || "", |
| 5232 | tokens: input.tokens || {}, |
| 5233 | recipes: input.recipes || { density: "comfortable", corners: "soft" }, |
| 5234 | background: input.background ?? undefined, |
| 5235 | taskBackground: input.taskBackground ?? undefined, |
| 5236 | }; |
| 5237 | const idx = mockThemePacks.findIndex((p) => p.id === pack.id); |
| 5238 | if (idx >= 0) mockThemePacks[idx] = pack; |
| 5239 | else mockThemePacks.push(pack); |
| 5240 | if (input.activate) mockActiveThemeId = pack.id; |
| 5241 | return pack; |
| 5242 | }, |
| 5243 | async DeleteThemePack(id: string) { |
| 5244 | mockThemePacks = mockThemePacks.filter((p) => p.id !== id || p.builtin); |
| 5245 | if (mockActiveThemeId === id) mockActiveThemeId = ""; |
| 5246 | }, |
| 5247 | async CopyThemePack(sourceID: string, newID: string, newName: string) { |
| 5248 | const src = mockThemePacks.find((p) => p.id === sourceID); |
| 5249 | if (!src) throw new Error("source theme not found"); |
| 5250 | const pack: import("./themePack").ThemePackView = { |
| 5251 | ...src, |
| 5252 | id: newID, |
| 5253 | name: newName || `${src.name} Copy`, |
| 5254 | builtin: false, |
| 5255 | kind: "user", |
| 5256 | nameKey: undefined, |
| 5257 | descriptionKey: undefined, |
| 5258 | active: false, |
| 5259 | }; |
| 5260 | mockThemePacks.push(pack); |
| 5261 | return pack; |
| 5262 | }, |
| 5263 | async ImportThemePack(_sourcePath: string, replace: boolean) { |
| 5264 | if (replace) { |
| 5265 | return { pack: mockThemePacks[0], replaced: true }; |
| 5266 | } |
| 5267 | // Simulate conflict path without re-prompting for a file on confirm. |
| 5268 | return { pack: mockThemePacks[0], replaced: false, needsReplace: true, pendingId: "pending-mock" }; |
| 5269 | }, |
| 5270 | async ExportThemePack(_id: string, _destPath: string) { |
| 5271 | return ""; |
| 5272 | }, |
| 5273 | async PickThemeBackground() { |
| 5274 | return ""; |
| 5275 | }, |
| 5276 | // The layout-style preference was removed from the UI; the binding |
| 5277 | // stays because the generated host contract still declares it. |
| 5278 | async SetDesktopLayoutStyle(_style: string) {}, |
| 5279 | async SetDesktopZoomFactor(factor: number) { |
| 5280 | mockDesktopZoomFactor = Math.min(2.0, Math.max(0.5, Number.isFinite(factor) ? factor : 1.0)); |
| 5281 | }, |
| 5282 | async GetDesktopZoomFactor() { |
| 5283 | return mockDesktopZoomFactor; |
| 5284 | }, |
| 5285 | async RestartApplication() { |
| 5286 | // no-op in mock |
| 5287 | }, |
| 5288 | async ReportDesktopWebViewReady() { |
| 5289 | // no-op in mock |
| 5290 | }, |
| 5291 | async GetDesktopShellStatus() { |
| 5292 | return { trayState: "ready", backgroundCloseAvailable: true } as DesktopShellStatusView; |
| 5293 | }, |
| 5294 | async SetAgentParams(temperature: number, maxSteps: number, plannerMaxSteps: number, systemPrompt: string) { |
| 5295 | settings.agent = { ...settings.agent, temperature, maxSteps, plannerMaxSteps, systemPrompt }; |
| 5296 | }, |
| 5297 | async SetCompactRatio(ratio: number) { |
| 5298 | if (!Number.isFinite(ratio) |
| 5299 | || ratio < COMPACT_RATIO_MIN_PERCENT / 100 |
| 5300 | || ratio > COMPACT_RATIO_MAX_PERCENT / 100) { |
| 5301 | throw new Error(`compact ratio must be between ${COMPACT_RATIO_MIN_PERCENT / 100} and ${COMPACT_RATIO_MAX_PERCENT / 100}`); |
| 5302 | } |
| 5303 | settings.agent = { ...settings.agent, compactRatio: ratio }; |
| 5304 | }, |
| 5305 | async SetReasoningLanguage(lang: string) { |
| 5306 | const normalized = lang === "zh" || lang === "en" ? lang : "auto"; |
| 5307 | settings.agent = { ...settings.agent, reasoningLanguage: normalized }; |
| 5308 | }, |
| 5309 | // ── Heartbeat mock ── |
| 5310 | async HeartbeatListTasks() { return mockHeartbeatTasks; }, |
| 5311 | async HeartbeatReloadTasks() { return mockHeartbeatTasks; }, |
| 5312 | async HeartbeatSaveTasks(tasks: unknown) { mockHeartbeatTasks = Array.isArray(tasks) ? tasks : []; }, |
| 5313 | async HeartbeatReloadConfig() { return { revision: mockHeartbeatRevision, etag: `mock-${mockHeartbeatRevision}`, tasks: mockHeartbeatTasks }; }, |
| 5314 | async HeartbeatSaveConfig(update: unknown) { |
| 5315 | const tasks = (update as { tasks?: unknown[] } | null)?.tasks; |
| 5316 | mockHeartbeatTasks = Array.isArray(tasks) ? tasks : []; |
| 5317 | mockHeartbeatRevision += 1; |
| 5318 | return { revision: mockHeartbeatRevision, etag: `mock-${mockHeartbeatRevision}`, tasks: mockHeartbeatTasks }; |
| 5319 | }, |
| 5320 | async HeartbeatTriggerNow(_id: string) {}, |
| 5321 | async HeartbeatGenerateID() { return "mock-" + Date.now().toString(36); }, |
| 5322 | async ListTasks() { return []; }, |
| 5323 | async CurrentTaskSessionID() { return ""; }, |
| 5324 | async ListTasksForSession() { return []; }, |
| 5325 | async GetTask() { return null; }, |
| 5326 | async StopTask() { return { schema_version: 1, command: "stop", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; }, |
| 5327 | async CancelTask() { return { schema_version: 1, command: "cancel", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; }, |
| 5328 | async ListTasksForTab() { return []; }, |
| 5329 | ...makeMockTaskCatalogBindings(), |
| 5330 | async ListTaskEventsForTab() { return []; }, |
| 5331 | async StopTaskForTab() { return { schema_version: 1, command: "stop", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; }, |
| 5332 | async CancelTaskForTab() { return { schema_version: 1, command: "cancel", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; }, |
| 5333 | async RequeueTaskForTab() { return { schema_version: 1, command: "requeue", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; }, |
| 5334 | async OpenTaskSessionForTab() { return { schema_version: 1, command: "open_session", task_id: "", accepted: false, idempotent: false, error: { code: "mock", message: "not available in browser mock" } }; }, |
| 5335 | async SetTrayLocale(_locale: "en" | "zh" | "zh-TW") {}, |
| 5336 | async SetAutoApproveTools(_on: boolean) { |
| 5337 | await this.SetToolApprovalMode("workspace-write"); |
| 5338 | }, |
| 5339 | async SetBypass(on: boolean) { |
| 5340 | await this.SetAutoApproveTools(on); |
| 5341 | }, |
| 5342 | async Version() { |
| 5343 | return "v1.0.0 (browser dev)"; |
| 5344 | }, |
| 5345 | async CheckUpdate(channel: string) { |
| 5346 | void channel; |
| 5347 | // Keep the default browser preview focused on the primary product surface. |
| 5348 | // Updater methods remain mocked for explicit updater-flow tests. |
| 5349 | return { |
| 5350 | available: false, |
| 5351 | current: "v1.0.0", |
| 5352 | latest: "v1.0.0", |
| 5353 | notes: "", |
| 5354 | channel: "stable", |
| 5355 | canSelfUpdate: false, |
| 5356 | manualOnly: true, |
| 5357 | installMode: "manual", |
| 5358 | manualReason: "browser preview", |
| 5359 | downloaded: false, |
| 5360 | downloadUrl: "", |
| 5361 | assetSize: 0, |
| 5362 | }; |
| 5363 | }, |
| 5364 | async ApplyUpdateRequest(channel: string, expectedVersion: string, requestId: string) { |
| 5365 | void channel; |
| 5366 | const selectedChannel = "stable"; |
| 5367 | const total = 12_345_678; |
| 5368 | for (let r = 0; r <= total; r += 1_800_000) { |
| 5369 | emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "downloading", received: Math.min(r, total), total }); |
| 5370 | await delay(120); |
| 5371 | } |
| 5372 | emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "verifying", received: total, total }); |
| 5373 | await delay(300); |
| 5374 | emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "installing", received: total, total }); |
| 5375 | await delay(300); |
| 5376 | emitUpdater({ requestId, version: expectedVersion, channel: selectedChannel, phase: "relaunching", received: 0, total: 0 }); |
| 5377 | }, |
| 5378 | async AbandonPendingUpdate() {}, |
| 5379 | async OpenDownloadPage() { |
| 5380 | if (typeof window !== "undefined") { |
| 5381 | window.open("https://reasonix.io/?download=desktop#start", "_blank", "noopener"); |
| 5382 | } |
| 5383 | }, |
| 5384 | async OpenUserConfigPath() {}, |
| 5385 | async ReloadUserConfig() { |
| 5386 | return { configWarnings: [], configWarningsRevision: 0, configPath: "" }; |
| 5387 | }, |
| 5388 | // Dev seam: match the backend's provider-agnostic onboarding predicate. |
| 5389 | async NeedsOnboarding() { |
| 5390 | return !settings.providers.some((p) => p.models.length > 0 && providerIsConfigured(p)); |
| 5391 | }, |
| 5392 | async ConnectKey(apiKey: string) { |
| 5393 | if (!apiKey.trim()) throw new Error("key is required"); |
| 5394 | // Match the production onboarding path: saving a DeepSeek key also |
| 5395 | // restores the current official provider template instead of merely |
| 5396 | // marking a possibly stale legacy entry as configured. |
| 5397 | await this.AddOfficialProviderAccess("deepseek", apiKey); |
| 5398 | await delay(300); |
| 5399 | return ""; |
| 5400 | }, |
| 5401 | async ReportCrash() { await delay(300); }, |
| 5402 | async RecordUIPerf() {}, |
| 5403 | // Tab management mocks. |
| 5404 | async ListTabs() { |
| 5405 | return mockTabs.map((tab) => ({ ...tab })); |
| 5406 | }, |
| 5407 | async OpenProjectTab(workspaceRoot: string, _topicID: string) { |
| 5408 | const existing = mockTabs.find((tab) => tab.scope === "project" && tab.workspaceRoot === workspaceRoot && tab.topicId === _topicID); |
| 5409 | if (existing) { |
| 5410 | const active = { ...existing, active: true, running: mockTopicRunsInScenario(_topicID) }; |
| 5411 | mockTabs = mockTabs.map((tab) => (tab.id === existing.id ? active : { ...tab, active: false })); |
| 5412 | return { ...active }; |
| 5413 | } |
| 5414 | const pruned = restoreMockPrunedTab("project", workspaceRoot, _topicID); |
| 5415 | if (pruned) { |
| 5416 | const restored = { ...pruned, active: true, running: mockTopicRunsInScenario(_topicID) }; |
| 5417 | mockTabs = [...mockTabs.map((item) => ({ ...item, active: false })), restored]; |
| 5418 | return { ...restored }; |
| 5419 | } |
| 5420 | const defaultToolApprovalMode = normalizeToolApprovalMode(settings.defaultToolApprovalMode); |
| 5421 | const tab: TabMeta = { |
| 5422 | id: "tab_" + Date.now(), |
| 5423 | scope: "project", |
| 5424 | workspaceRoot, |
| 5425 | workspaceName: workspaceRoot.split("/").filter(Boolean).pop() ?? workspaceRoot, |
| 5426 | workspacePath: workspaceRoot, |
| 5427 | gitBranch: "main", |
| 5428 | topicId: _topicID, |
| 5429 | topicTitle: topicLabel(_topicID, t("mock.newSession")), |
| 5430 | sessionPath: `/mock/sessions/${_topicID}.jsonl`, |
| 5431 | projectColor: mockProjectTree.find((node) => node.root === workspaceRoot)?.projectColor, |
| 5432 | label: mockModelLabel(settings.defaultModel), |
| 5433 | ready: true, |
| 5434 | running: mockTopicRunsInScenario(_topicID), |
| 5435 | mode: modeWithAutoApproveTools("normal", defaultToolApprovalMode === "danger-full-access"), |
| 5436 | collaborationMode: "normal", |
| 5437 | toolApprovalMode: defaultToolApprovalMode, |
| 5438 | tokenMode: "full", |
| 5439 | active: true, |
| 5440 | cwd: workspaceRoot, |
| 5441 | }; |
| 5442 | mockTabs = [...mockTabs.map((item) => ({ ...item, active: false })), tab]; |
| 5443 | return { ...tab }; |
| 5444 | }, |
| 5445 | async IsolatedWorktreeAvailability(workspaceRoot: string) { |
| 5446 | return workspaceRoot |
| 5447 | ? { available: true, repoRoot: workspaceRoot, branch: "main", sourceDirty: false } |
| 5448 | : { available: false, reason: "project folder is required" }; |
| 5449 | }, |
| 5450 | async DeliveryWorktreeAvailability(workspaceRoot: string) { |
| 5451 | return this.IsolatedWorktreeAvailability(workspaceRoot); |
| 5452 | }, |
| 5453 | ...makeMockQualityFloorBindings(() => mockTabs, (next) => { mockTabs = next; }), |
| 5454 | async SetAgentPreset(_preset: string) {}, |
| 5455 | async SetAgentPresetForTab(_tabID: string, _preset: string) {}, |
| 5456 | async SetTokenMode(_mode: string) {}, |
| 5457 | async SetTokenModeForTab(_tabID: string, _mode: string) {}, |
| 5458 | async CreateIsolatedWorktree(workspaceRoot: string) { |
| 5459 | if (!workspaceRoot) throw new Error("project folder is required"); |
| 5460 | const suffix = Date.now().toString(36); |
| 5461 | const isolatedRoot = `/mock/reasonix-worktrees/${suffix}/${workspaceRoot.split("/").filter(Boolean).pop() ?? "project"}`; |
| 5462 | const topicID = `topic_worktree_${suffix}`; |
| 5463 | const tab = await this.OpenProjectTab(isolatedRoot, topicID); |
| 5464 | tab.isolatedWorktree = true; |
| 5465 | tab.gitBranch = `reasonix/isolated-${suffix}`; |
| 5466 | mockTabs = mockTabs.map((candidate) => candidate.id === tab.id ? { ...tab } : candidate); |
| 5467 | return { |
| 5468 | workspaceRoot: isolatedRoot, |
| 5469 | worktreeRoot: isolatedRoot, |
| 5470 | sourceRoot: workspaceRoot, |
| 5471 | branch: tab.gitBranch, |
| 5472 | sourceDirty: false, |
| 5473 | tab, |
| 5474 | }; |
| 5475 | }, |
| 5476 | async CreateDeliveryWorktree(workspaceRoot: string) { |
| 5477 | return this.CreateIsolatedWorktree(workspaceRoot); |
| 5478 | }, |
| 5479 | ...makeMockWorktreeMergeBindings(() => mockTabs, (next) => { mockTabs = next; }), |
| 5480 | async OpenGlobalTab(_topicID: string) { |
| 5481 | const existing = mockTabs.find((tab) => tab.scope === "global" && tab.topicId === _topicID); |
| 5482 | if (existing) { |
| 5483 | setMockActiveTab(existing.id); |
| 5484 | return { ...existing, active: true }; |
| 5485 | } |
| 5486 | const defaultToolApprovalMode = normalizeToolApprovalMode(settings.defaultToolApprovalMode); |
| 5487 | const tab: TabMeta = { |
| 5488 | id: "tab_" + Date.now(), |
| 5489 | scope: "global", |
| 5490 | workspaceRoot: globalWorkspaceRoot, |
| 5491 | workspaceName: "Global", |
| 5492 | workspacePath: globalWorkspaceRoot, |
| 5493 | topicId: _topicID, |
| 5494 | topicTitle: topicLabel(_topicID, "Global"), |
| 5495 | sessionPath: `/mock/sessions/${_topicID}.jsonl`, |
| 5496 | label: mockModelLabel(settings.defaultModel), |
| 5497 | ready: true, |
| 5498 | running: false, |
| 5499 | mode: modeWithAutoApproveTools("normal", defaultToolApprovalMode === "danger-full-access"), |
| 5500 | collaborationMode: "normal", |
| 5501 | toolApprovalMode: defaultToolApprovalMode, |
| 5502 | tokenMode: "full", |
| 5503 | active: true, |
| 5504 | cwd: globalWorkspaceRoot, |
| 5505 | }; |
| 5506 | mockTabs = [...mockTabs.map((item) => ({ ...item, active: false })), tab]; |
| 5507 | return { ...tab }; |
| 5508 | }, |
| 5509 | async OpenTopicSession(scope: string, workspaceRoot: string, topicID: string, sessionPath: string) { |
| 5510 | const tab = scope === "project" |
| 5511 | ? await this.OpenProjectTab(workspaceRoot, topicID) |
| 5512 | : await this.OpenGlobalTab(topicID); |
| 5513 | const active = { ...tab, sessionPath }; |
| 5514 | mockTabs = mockTabs.map((item) => (item.id === tab.id ? active : item)); |
| 5515 | return { ...active }; |
| 5516 | }, |
| 5517 | async EnsureBlankTab(scope: string, workspaceRoot: string) { |
| 5518 | const targetScope = scope === "project" && workspaceRoot ? "project" : "global"; |
| 5519 | const targetRoot = targetScope === "project" ? workspaceRoot : ""; |
| 5520 | const existing = mockTabs.find((tab) => |
| 5521 | tab.scope === targetScope && |
| 5522 | (targetScope === "global" || tab.workspaceRoot === targetRoot) && |
| 5523 | !tab.running && |
| 5524 | mockTopicIsBlank(tab.topicId) |
| 5525 | ); |
| 5526 | if (existing) { |
| 5527 | setMockActiveTab(existing.id); |
| 5528 | return { ...existing, active: true }; |
| 5529 | } |
| 5530 | const topic = await this.CreateTopic(targetScope, targetRoot, ""); |
| 5531 | return targetScope === "global" ? this.OpenGlobalTab(topic.id) : this.OpenProjectTab(targetRoot, topic.id); |
| 5532 | }, |
| 5533 | async ActivateTopic(scope: string, workspaceRoot: string, topicID: string, sessionPath: string) { |
| 5534 | const tab = sessionPath |
| 5535 | ? await this.OpenTopicSession(scope, workspaceRoot, topicID, sessionPath) |
| 5536 | : scope === "project" |
| 5537 | ? await this.OpenProjectTab(workspaceRoot, topicID) |
| 5538 | : await this.OpenGlobalTab(topicID); |
| 5539 | pruneMockTabsTo(tab.id); |
| 5540 | return { ...mockTabs[0] }; |
| 5541 | }, |
| 5542 | async StartTopicActivation(req: TopicActivationRequest): Promise<TopicActivationTicket> { |
| 5543 | // Publish identity first; terminal runtime events arrive asynchronously. |
| 5544 | const tab = req.sessionPath |
| 5545 | ? await this.OpenTopicSession(req.scope, req.workspaceRoot, req.topicId, req.sessionPath) |
| 5546 | : req.scope === "project" |
| 5547 | ? await this.OpenProjectTab(req.workspaceRoot, req.topicId) |
| 5548 | : await this.OpenGlobalTab(req.topicId); |
| 5549 | pruneMockTabsTo(tab.id); |
| 5550 | const requestId = req.requestId?.trim() || `mock-act-${++mockTopicActivationCounter}`; |
| 5551 | const previous = mockPendingTopicActivation; |
| 5552 | mockPendingTopicActivation = { requestId, tabId: tab.id }; |
| 5553 | if (previous && previous.requestId !== requestId) { |
| 5554 | __emitMockTopicActivation({ requestId: previous.requestId, tabId: previous.tabId, phase: "cancelled" }); |
| 5555 | } |
| 5556 | __emitMockTopicActivation({ requestId, tabId: tab.id, phase: "starting" }); |
| 5557 | const meta = { ...mockTabs[0] }; |
| 5558 | window.setTimeout(() => { |
| 5559 | if (mockPendingTopicActivation?.requestId !== requestId) return; |
| 5560 | mockPendingTopicActivation = undefined; |
| 5561 | __emitMockTopicActivation({ requestId, tabId: tab.id, phase: "ready" }); |
| 5562 | }, 0); |
| 5563 | return { requestId, tabId: tab.id, meta }; |
| 5564 | }, |
| 5565 | async EnsureBlankSurface(scope: string, workspaceRoot: string) { |
| 5566 | const tab = await this.EnsureBlankTab(scope, workspaceRoot); |
| 5567 | pruneMockTabsTo(tab.id); |
| 5568 | return { ...mockTabs[0] }; |
| 5569 | }, |
| 5570 | async SetActiveTab(_tabID: string) { |
| 5571 | const tab = mockTabs.find((item) => item.id === _tabID); |
| 5572 | if (!tab) throw new Error(`tab ${_tabID} not found`); |
| 5573 | setMockActiveTab(_tabID); |
| 5574 | if (!tab.remote) queueMockTopicRuntime(tab); |
| 5575 | }, |
| 5576 | async ReorderTabs(_tabIDs: string[]) { |
| 5577 | const byId = new Map(mockTabs.map((tab) => [tab.id, tab])); |
| 5578 | const ordered = _tabIDs.map((id) => byId.get(id)).filter((tab): tab is TabMeta => Boolean(tab)); |
| 5579 | if (ordered.length === mockTabs.length) mockTabs = ordered; |
| 5580 | }, |
| 5581 | async CloseTab(_tabID: string) { |
| 5582 | if (mockTabs.length <= 1) return; |
| 5583 | const terminalIDs = mockTerminalSessions |
| 5584 | .filter((session) => mockTerminalTabIDs.get(session.id) === _tabID) |
| 5585 | .map((session) => session.id); |
| 5586 | mockTerminalSessions = mockTerminalSessions.filter((session) => !terminalIDs.includes(session.id)); |
| 5587 | terminalIDs.forEach((id) => { |
| 5588 | mockTerminalOutput.delete(id); |
| 5589 | mockTerminalTabIDs.delete(id); |
| 5590 | __emitMockTerminalExit({ id, exitCode: 0, removed: true }); |
| 5591 | }); |
| 5592 | const wasActive = mockTabs.some((tab) => tab.id === _tabID && tab.active); |
| 5593 | mockTabs = mockTabs.filter((tab) => tab.id !== _tabID); |
| 5594 | if (wasActive && mockTabs.length > 0 && !mockTabs.some((tab) => tab.active)) { |
| 5595 | mockTabs[mockTabs.length - 1] = { ...mockTabs[mockTabs.length - 1], active: true }; |
| 5596 | } |
| 5597 | }, |
| 5598 | async TerminalWorkspaceForTab(tabID: string) { |
| 5599 | const tab = mockTabs.find((candidate) => candidate.id === tabID) ?? mockTabs.find((candidate) => candidate.active); |
| 5600 | const sessions = tab ? mockTerminalSessions.filter((session) => mockTerminalTabIDs.get(session.id) === tab.id) : []; |
| 5601 | return { |
| 5602 | available: true, |
| 5603 | readOnly: Boolean(tab?.readOnly), |
| 5604 | sessions: sessions.map((session) => ({ ...session })), |
| 5605 | shells: [ |
| 5606 | { id: "default", label: "Default shell" }, |
| 5607 | { id: "bash", label: "bash" }, |
| 5608 | { id: "zsh", label: "zsh" }, |
| 5609 | ], |
| 5610 | }; |
| 5611 | }, |
| 5612 | async TerminalOutputForTab(tabID: string, sessionID: string) { |
| 5613 | if (mockTerminalTabIDs.get(sessionID) !== tabID) return ""; |
| 5614 | return mockTerminalOutput.get(sessionID) ?? ""; |
| 5615 | }, |
| 5616 | async CreateTerminalForTab(tabID: string, relativePath: string, shellID: string) { |
| 5617 | const tab = mockTabs.find((candidate) => candidate.id === tabID) ?? mockTabs.find((candidate) => candidate.active); |
| 5618 | if (tab?.readOnly) throw new Error("channel session is read-only"); |
| 5619 | const id = `term-mock-${Date.now()}-${Math.random().toString(16).slice(2)}`; |
| 5620 | const session: TerminalSessionView = { |
| 5621 | id, |
| 5622 | title: shellID || "Default shell", |
| 5623 | shell: shellID || "default", |
| 5624 | cwd: `${tab?.cwd || cwd}/${relativePath || "."}`.replace(/\/\.\/?$/, ""), |
| 5625 | createdAt: Date.now(), |
| 5626 | running: true, |
| 5627 | }; |
| 5628 | mockTerminalSessions = [...mockTerminalSessions, session]; |
| 5629 | mockTerminalOutput.set(id, "Reasonix terminal ready\r\n"); |
| 5630 | mockTerminalTabIDs.set(id, tabID); |
| 5631 | window.setTimeout(() => __emitMockTerminalOutput({ id, data: mockTerminalBytes("Reasonix terminal ready\r\n") }), 0); |
| 5632 | return { ...session }; |
| 5633 | }, |
| 5634 | async WriteTerminalForTab(_tabID: string, sessionID: string, data: string) { |
| 5635 | const session = mockTerminalSessions.find((candidate) => candidate.id === sessionID); |
| 5636 | if (!session?.running) throw new Error("terminal session has exited"); |
| 5637 | mockTerminalOutput.set(sessionID, `${mockTerminalOutput.get(sessionID) ?? ""}${data}`); |
| 5638 | window.setTimeout(() => __emitMockTerminalOutput({ id: sessionID, data: mockTerminalBytes(data) }), 0); |
| 5639 | }, |
| 5640 | async ResizeTerminalForTab() {}, |
| 5641 | async CloseTerminalForTab(_tabID: string, sessionID: string) { |
| 5642 | mockTerminalSessions = mockTerminalSessions.filter((session) => session.id !== sessionID); |
| 5643 | mockTerminalOutput.delete(sessionID); |
| 5644 | mockTerminalTabIDs.delete(sessionID); |
| 5645 | __emitMockTerminalExit({ id: sessionID, exitCode: 0, removed: true }); |
| 5646 | }, |
| 5647 | async RenameTerminalForTab(_tabID: string, sessionID: string, title: string) { |
| 5648 | mockTerminalSessions = mockTerminalSessions.map((session) => session.id === sessionID ? { ...session, title } : session); |
| 5649 | }, |
| 5650 | async ListProjectTree() { |
| 5651 | return cloneProjectTree(); |
| 5652 | }, |
| 5653 | async RenameProject(workspaceRoot: string, title: string) { |
| 5654 | const node = workspaceRoot |
| 5655 | ? mockProjectTree.find((item) => item.root === workspaceRoot) |
| 5656 | : mockProjectTree.find((item) => item.kind === "global_folder"); |
| 5657 | if (node) node.label = title.trim() || (node.kind === "global_folder" ? "Global" : node.label); |
| 5658 | }, |
| 5659 | async SetProjectColor(workspaceRoot: string, color: string) { |
| 5660 | const node = workspaceRoot |
| 5661 | ? mockProjectTree.find((item) => item.root === workspaceRoot) |
| 5662 | : mockProjectTree.find((item) => item.kind === "global_folder"); |
| 5663 | if (!node) return; |
| 5664 | node.projectColor = color || undefined; |
| 5665 | for (const child of projectChildren(node)) child.projectColor = node.projectColor; |
| 5666 | mockTabs = mockTabs.map((tab) => |
| 5667 | (workspaceRoot ? tab.workspaceRoot === workspaceRoot : tab.scope === "global") |
| 5668 | ? { ...tab, projectColor: node.projectColor } |
| 5669 | : tab, |
| 5670 | ); |
| 5671 | }, |
| 5672 | async SetProjectPinned(workspaceRoot: string, pinned: boolean) { |
| 5673 | setMockProjectPinned(workspaceRoot, pinned); |
| 5674 | }, |
| 5675 | async ReorderProjects(workspaceRoots: string[]) { |
| 5676 | const projects = mockProjectTree.filter((node) => node.kind === "project"); |
| 5677 | const globals = mockProjectTree.filter((node) => node.kind === "global_folder"); |
| 5678 | if (!workspaceRoots.includes(GLOBAL_PROJECT_ORDER_KEY)) { |
| 5679 | if (workspaceRoots.length !== projects.length) return; |
| 5680 | const byRoot = new Map(projects.map((node) => [node.root, node])); |
| 5681 | const ordered = workspaceRoots.map((root) => byRoot.get(root)).filter((node): node is ProjectNode => Boolean(node)); |
| 5682 | if (ordered.length !== projects.length) return; |
| 5683 | mockProjectTree.splice(0, mockProjectTree.length, ...globals, ...ordered); |
| 5684 | return; |
| 5685 | } |
| 5686 | const byKey = new Map<string, ProjectNode>(); |
| 5687 | for (const node of projects) { |
| 5688 | if (node.root) byKey.set(node.root, node); |
| 5689 | } |
| 5690 | for (const node of globals) byKey.set(GLOBAL_PROJECT_ORDER_KEY, node); |
| 5691 | const seen = new Set<string>(); |
| 5692 | const ordered: ProjectNode[] = []; |
| 5693 | for (const key of workspaceRoots) { |
| 5694 | if (seen.has(key)) return; |
| 5695 | const node = byKey.get(key); |
| 5696 | if (!node) return; |
| 5697 | seen.add(key); |
| 5698 | ordered.push(node); |
| 5699 | } |
| 5700 | if (ordered.length !== projects.length + globals.length) return; |
| 5701 | mockProjectTree.splice(0, mockProjectTree.length, ...ordered); |
| 5702 | }, |
| 5703 | async CreateTopic(_scope: string, _workspaceRoot: string, title: string) { |
| 5704 | const now = Date.now(); |
| 5705 | const id = "topic_" + now; |
| 5706 | const topicTitle = title.trim() || t("mock.newSession"); |
| 5707 | const parent = _scope === "global" |
| 5708 | ? ensureMockGlobalFolder() |
| 5709 | : mockProjectTree.find((node) => node.root === _workspaceRoot); |
| 5710 | if (parent) { |
| 5711 | const global = parent.kind === "global_folder"; |
| 5712 | parent.children = [{ |
| 5713 | key: parent.kind === "global_folder" ? "global_topic_" + id : "topic_" + id, |
| 5714 | kind: global ? "global_topic" : "topic", |
| 5715 | label: topicTitle, |
| 5716 | root: parent.root, |
| 5717 | topicId: id, |
| 5718 | projectColor: parent.projectColor, |
| 5719 | createdAt: now, |
| 5720 | }, ...projectChildren(parent)]; |
| 5721 | } |
| 5722 | return { id, title: topicTitle, createdAt: now }; |
| 5723 | }, |
| 5724 | async RenameTopic(topicID: string, title: string) { |
| 5725 | const topic = findMockTopic(topicID); |
| 5726 | const nextTitle = title.trim(); |
| 5727 | if (!topic || !nextTitle) return; |
| 5728 | const activePrefix = topic.label?.startsWith("● ") ? "● " : ""; |
| 5729 | topic.label = `${activePrefix}${nextTitle}`; |
| 5730 | mockTabs = mockTabs.map((tab) => |
| 5731 | tab.topicId === topicID ? { ...tab, topicTitle: nextTitle } : tab, |
| 5732 | ); |
| 5733 | }, |
| 5734 | async AIRenameSession(topicID: string) { return mockAIRenameTarget(mockProjectTree, topicID); }, |
| 5735 | async AIRenameSessionTarget(selector: SessionSelector): Promise<SessionMutationResult> { |
| 5736 | const node = mockSessionTitleTarget(mockProjectTree, selector); |
| 5737 | const title = node ? mockAIRenameTarget(mockProjectTree, sessionTitleTarget(node)) : ""; |
| 5738 | return { targetKey: sessionTitleTarget(node ?? { key: "", kind: "topic", label: "" }), operationId: `mock-title-${Date.now()}`, committed: true, title, lifecycleGeneration: 1 }; |
| 5739 | }, |
| 5740 | async RenameSessionTarget(selector: SessionSelector, title: string): Promise<SessionMutationResult> { |
| 5741 | const node = mockSessionTitleTarget(mockProjectTree, selector); |
| 5742 | const nextTitle = title.trim(); |
| 5743 | if (node && nextTitle) { |
| 5744 | const activePrefix = node.label?.startsWith("● ") ? "● " : ""; |
| 5745 | node.label = `${activePrefix}${nextTitle}`; |
| 5746 | } |
| 5747 | return { targetKey: node ? sessionTitleTarget(node) : "", operationId: `mock-title-${Date.now()}`, committed: Boolean(node), title: nextTitle, lifecycleGeneration: 1 }; |
| 5748 | }, |
| 5749 | async DeleteTopic(topicID: string) { |
| 5750 | deleteMockTopic(topicID); |
| 5751 | }, |
| 5752 | async TrashTopic(topicID: string) { |
| 5753 | deleteMockTopic(topicID); |
| 5754 | }, |
| 5755 | async SetTopicPinned(topicID: string, pinned: boolean) { |
| 5756 | setMockTopicPinned(topicID, pinned); |
| 5757 | }, |
| 5758 | ...makeMockProjectTreeOrganizationBindings(mockProjectTree), |
| 5759 | async SaveWindowState(_state) { |
| 5760 | // no-op in browser dev — no real window geometry to persist |
| 5761 | }, |
| 5762 | async ContextPanel(_tabID: string) { |
| 5763 | const now = Date.now(); |
| 5764 | const currency = "¥"; |
| 5765 | const cost = (usd: number) => currency === "¥" ? Number((usd * 7.15).toFixed(4)) : usd; |
| 5766 | return { |
| 5767 | usedTokens: 42124, |
| 5768 | windowTokens: 128000, |
| 5769 | promptTokens: 22134, |
| 5770 | completionTokens: 12345, |
| 5771 | totalTokens: 34479, |
| 5772 | reasoningTokens: 7521, |
| 5773 | cacheHitTokens: 87000, |
| 5774 | cacheMissTokens: 13000, |
| 5775 | sessionCacheHitTokens: 87000, |
| 5776 | sessionCacheMissTokens: 13000, |
| 5777 | sessionCompletionTokens: 12345, |
| 5778 | requestCount: 10, |
| 5779 | elapsedMs: 33 * 60 * 1000, |
| 5780 | sessionCost: cost(0.018), |
| 5781 | sessionCurrency: currency, |
| 5782 | sessionCostUsd: cost(0.018), |
| 5783 | sessionCostQuote: { |
| 5784 | original: { amount: String(cost(0.018)), currency: "CNY" }, |
| 5785 | selected: { amount: String(cost(0.018)), currency: "CNY" }, |
| 5786 | estimated: true, |
| 5787 | costComplete: true, |
| 5788 | displayComplete: true, |
| 5789 | complete: true, |
| 5790 | displayStatus: "matched", |
| 5791 | aggregateMode: "single_currency", |
| 5792 | rateBand: "mixed", |
| 5793 | }, |
| 5794 | sources: { |
| 5795 | executor: { |
| 5796 | promptTokens: 24100, |
| 5797 | completionTokens: 8300, |
| 5798 | totalTokens: 32400, |
| 5799 | reasoningTokens: 5200, |
| 5800 | cacheHitTokens: 76000, |
| 5801 | cacheMissTokens: 9000, |
| 5802 | requestCount: 4, |
| 5803 | sessionCost: cost(0.0124), |
| 5804 | sessionCurrency: currency, |
| 5805 | sessionCostUsd: cost(0.0124), |
| 5806 | }, |
| 5807 | planner: { |
| 5808 | promptTokens: 1800, |
| 5809 | completionTokens: 600, |
| 5810 | totalTokens: 2400, |
| 5811 | reasoningTokens: 420, |
| 5812 | cacheHitTokens: 3400, |
| 5813 | cacheMissTokens: 700, |
| 5814 | requestCount: 1, |
| 5815 | sessionCost: cost(0.0011), |
| 5816 | sessionCurrency: currency, |
| 5817 | sessionCostUsd: cost(0.0011), |
| 5818 | }, |
| 5819 | subagent: { |
| 5820 | promptTokens: 4200, |
| 5821 | completionTokens: 2100, |
| 5822 | totalTokens: 6300, |
| 5823 | reasoningTokens: 1500, |
| 5824 | cacheHitTokens: 6100, |
| 5825 | cacheMissTokens: 2100, |
| 5826 | requestCount: 2, |
| 5827 | sessionCost: cost(0.0032), |
| 5828 | sessionCurrency: currency, |
| 5829 | sessionCostUsd: cost(0.0032), |
| 5830 | }, |
| 5831 | compaction: { |
| 5832 | promptTokens: 2600, |
| 5833 | completionTokens: 700, |
| 5834 | totalTokens: 3300, |
| 5835 | reasoningTokens: 260, |
| 5836 | cacheHitTokens: 1100, |
| 5837 | cacheMissTokens: 900, |
| 5838 | requestCount: 1, |
| 5839 | sessionCost: cost(0.0009), |
| 5840 | sessionCurrency: currency, |
| 5841 | sessionCostUsd: cost(0.0009), |
| 5842 | }, |
| 5843 | classifier: { |
| 5844 | promptTokens: 900, |
| 5845 | completionTokens: 120, |
| 5846 | totalTokens: 1020, |
| 5847 | reasoningTokens: 70, |
| 5848 | cacheHitTokens: 300, |
| 5849 | cacheMissTokens: 250, |
| 5850 | requestCount: 1, |
| 5851 | sessionCost: cost(0.0003), |
| 5852 | sessionCurrency: currency, |
| 5853 | sessionCostUsd: cost(0.0003), |
| 5854 | }, |
| 5855 | title: { |
| 5856 | promptTokens: 420, |
| 5857 | completionTokens: 80, |
| 5858 | totalTokens: 500, |
| 5859 | reasoningTokens: 20, |
| 5860 | cacheHitTokens: 100, |
| 5861 | cacheMissTokens: 50, |
| 5862 | requestCount: 1, |
| 5863 | sessionCost: cost(0.0001), |
| 5864 | sessionCurrency: currency, |
| 5865 | sessionCostUsd: cost(0.0001), |
| 5866 | }, |
| 5867 | }, |
| 5868 | mock: true, |
| 5869 | readFiles: [ |
| 5870 | { path: "README.md", turn: 2, time: now - 34 * 60 * 1000 }, |
| 5871 | { path: "go.mod", turn: 3, time: now - 30 * 60 * 1000 }, |
| 5872 | { path: "desktop/file.go", turn: 5, time: now - 13 * 60 * 1000, offset: 0, limit: 180 }, |
| 5873 | { path: "internal/event.go", turn: 6, time: now - 4 * 60 * 1000, offset: 120, limit: 80, truncated: true }, |
| 5874 | ], |
| 5875 | changedFiles: [ |
| 5876 | { path: t("mock.changedFile1Path"), sources: ["session"], gitStatus: "modified", turns: [5, 6], latestPrompt: t("mock.changedFile1Prompt"), latestTime: now - 2 * 60 * 1000 }, |
| 5877 | { path: t("mock.changedFile2Path"), sources: ["session"], gitStatus: "added", turns: [6], latestPrompt: t("mock.changedFile2Prompt"), latestTime: now - 60 * 1000 }, |
| 5878 | ], |
| 5879 | }; |
| 5880 | }, |
| 5881 | // ── Remote (SSH) mock ── |
| 5882 | async RemoteHosts() { |
| 5883 | return mockRemoteHosts.slice(); |
| 5884 | }, |
| 5885 | async AddRemoteHost(input) { |
| 5886 | const view = mockRemoteHostView(input.label, input); |
| 5887 | mockRemoteHosts = [...mockRemoteHosts.filter((h) => h.id !== view.id), view]; |
| 5888 | return view; |
| 5889 | }, |
| 5890 | async UpdateRemoteHost(id, input) { |
| 5891 | const previous = mockRemoteHosts.find((h) => h.id === id); |
| 5892 | const view = mockRemoteHostView(id, input, previous); |
| 5893 | mockRemoteHosts = mockRemoteHosts.map((h) => (h.id === id ? view : h)); |
| 5894 | return view; |
| 5895 | }, |
| 5896 | async RemoveRemoteHost(id) { |
| 5897 | mockRemoteHosts = mockRemoteHosts.filter((h) => h.id !== id); |
| 5898 | delete mockRemoteConn[id]; |
| 5899 | }, |
| 5900 | async ScanSSHConfig() { |
| 5901 | return [ |
| 5902 | { label: "gpu-box", host: "gpu-box", port: 0, user: "", identityFile: "", proxyJump: "", defaultWorkspace: "", serveInstall: "auto", credentialMode: "remote", useSSHConfig: true, preserveExistingSettings: true }, |
| 5903 | ]; |
| 5904 | }, |
| 5905 | async ConnectRemoteHost(id) { |
| 5906 | mockRemoteConn[id] = "connecting"; |
| 5907 | __emitMockRemote("status", { hostId: id, state: "connecting" }); |
| 5908 | setTimeout(() => { |
| 5909 | mockRemoteConn[id] = "connected"; |
| 5910 | __emitMockRemote("status", { hostId: id, state: "connected" }); |
| 5911 | }, 300); |
| 5912 | }, |
| 5913 | async DisconnectRemoteHost(id) { |
| 5914 | mockRemoteConn[id] = "stopped"; |
| 5915 | __emitMockRemote("status", { hostId: id, state: "stopped" }); |
| 5916 | }, |
| 5917 | async RemoteConnectionStatuses() { |
| 5918 | return Object.entries(mockRemoteConn).map(([hostId, state]) => ({ hostId, state: state as RemoteConnectionStatus["state"] })); |
| 5919 | }, |
| 5920 | async ConfirmRemoteHostKey(hostId, accept) { |
| 5921 | mockRemoteConn[hostId] = accept ? "connected" : "stopped"; |
| 5922 | __emitMockRemote("status", { hostId, state: mockRemoteConn[hostId] }); |
| 5923 | }, |
| 5924 | async ConfirmRemoteSecret(hostId, _promptId, _secret, accept) { |
| 5925 | mockRemoteConn[hostId] = accept ? "connected" : "stopped"; |
| 5926 | __emitMockRemote("status", { hostId, state: mockRemoteConn[hostId] }); |
| 5927 | }, |
| 5928 | async ListRemoteDir(_hostId, path) { |
| 5929 | const base = path.replace(/\/$/, ""); |
| 5930 | return [ |
| 5931 | { name: "src", path: `${base}/src`, isDir: true, size: 0, mtimeUnix: 1_700_000_000, symlink: false }, |
| 5932 | { name: "README.md", path: `${base}/README.md`, isDir: false, size: 1024, mtimeUnix: 1_700_000_500, symlink: false }, |
| 5933 | ]; |
| 5934 | }, |
| 5935 | async ReadRemoteFile(_hostId, path) { |
| 5936 | return { path, body: `# Mock remote file\n${path}\n`, size: 40, mtimeUnix: 1_700_000_500, truncated: false, binary: false }; |
| 5937 | }, |
| 5938 | async WriteRemoteFile(_hostId, _path, _body, _expectMtimeUnix) { |
| 5939 | return { ok: true, conflict: false, newMtimeUnix: 1_700_000_900 }; |
| 5940 | }, |
| 5941 | async MkdirRemote() {}, |
| 5942 | async RenameRemotePath() {}, |
| 5943 | async DeleteRemotePath() {}, |
| 5944 | async RemoteForwards(hostId) { |
| 5945 | return mockRemoteForwards[hostId] ?? []; |
| 5946 | }, |
| 5947 | async AddRemoteForward(hostId, input) { |
| 5948 | const view: RemoteForwardView = { id: `L:${input.localPort}`, hostId, ...input, state: "active" }; |
| 5949 | mockRemoteForwards[hostId] = [...(mockRemoteForwards[hostId] ?? []), view]; |
| 5950 | __emitMockRemote("forwards", { hostId, forwards: mockRemoteForwards[hostId] }); |
| 5951 | return view; |
| 5952 | }, |
| 5953 | async RemoveRemoteForward(hostId, forwardId) { |
| 5954 | mockRemoteForwards[hostId] = (mockRemoteForwards[hostId] ?? []).filter((f) => f.id !== forwardId); |
| 5955 | __emitMockRemote("forwards", { hostId, forwards: mockRemoteForwards[hostId] }); |
| 5956 | }, |
| 5957 | async OpenRemoteWorkspace() {}, |
| 5958 | async PickRemoteIdentityFile() { return "~/.ssh/id_ed25519"; }, |
| 5959 | async CheckRemotePlatform() {}, |
| 5960 | async StopRemoteServer(hostId, workspace) { |
| 5961 | __emitMockRemote("server", { hostId, workspace, state: "stopped" }); |
| 5962 | }, |
| 5963 | async RemoteServerStatus(hostId, workspace) { |
| 5964 | return { hostId, workspace, state: "stopped" }; |
| 5965 | }, |
| 5966 | async RemoteServerLogs() { |
| 5967 | return "mock serve log line 1\nmock serve log line 2\n"; |
| 5968 | }, |
| 5969 | async RemoteLastWorkspace() { |
| 5970 | return "~/app"; |
| 5971 | }, |
| 5972 | async ScanRemoteLegacyWorkbenchData() { |
| 5973 | return { mirrorCount: 0, mirrorBytes: 0, trustFile: false }; |
| 5974 | }, |
| 5975 | async ExtensionActions() { |
| 5976 | return []; |
| 5977 | }, |
| 5978 | async InvokeExtensionAction() { |
| 5979 | return ""; |
| 5980 | }, |
| 5981 | async SubmitExtensionForm() {}, |
| 5982 | async SubmitExtensionFormExact() {}, async ResolveRemoteTabPromptExact() {}, async SubmitRemoteTabExtensionFormExact() {}, |
| 5983 | ...remoteProjects.bindings, |
| 5984 | async CleanRemoteLegacyWorkbenchData() {}, |
| 5985 | }; |
| 5986 | } |
| 5987 | let mockRemoteHosts: RemoteHostView[] = [ |
| 5988 | { id: "demo", label: "demo", host: "192.168.1.10", port: 22, user: "dev", identityFile: "", proxyJump: "", defaultWorkspace: "~/app", serveInstall: "auto", credentialMode: "remote", useSSHConfig: false }, |
| 5989 | ]; |
| 5990 | const mockRemoteConn: Record<string, RemoteConnectionStatus["state"]> = { demo: "connected" }; |
| 5991 | const mockRemoteForwards: Record<string, RemoteForwardView[]> = {}; |
| 5992 |