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