singularity-forge/web/lib/sf-workspace-store.tsx
ace-pm 421fccd898 refactor: rebrand gsd_ tool names and references to sf_ namespace
Updates workflow tool names, documentation references, and internal naming
conventions across MCP server, CLI, tests, and web components to complete
the singularity-forge rebrand from gsd to sf.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:51:38 +02:00

5406 lines
184 KiB
TypeScript

"use client"
import {
createContext,
useContext,
useEffect,
useState,
useSyncExternalStore,
type ReactNode,
} from "react"
import {
dispatchBrowserSlashCommand,
getBrowserSlashCommandTerminalNotice,
SF_HELP_TEXT,
type BrowserSlashCommandDispatchResult,
type BrowserSlashCommandSurface,
} from "./browser-slash-command-dispatch"
import {
applyCommandSurfaceActionResult,
closeCommandSurfaceState,
createInitialCommandSurfaceState,
openCommandSurfaceState,
selectCommandSurfaceStateTarget,
setCommandSurfacePending,
setCommandSurfaceSection,
type CommandSurfaceCompactionResult,
type CommandSurfaceDiagnosticsPhaseState,
type CommandSurfaceDoctorState,
type CommandSurfaceForkMessage,
type CommandSurfaceGitSummaryState,
type CommandSurfaceModelOption,
type CommandSurfaceRecoveryState,
type CommandSurfaceSection,
type CommandSurfaceSessionBrowserState,
type CommandSurfaceSessionStats,
type CommandSurfaceTarget,
type CommandSurfaceThinkingLevel,
type CommandSurfaceKnowledgeCapturesState,
type WorkspaceCommandSurfaceState,
type WorkspaceRecoveryDiagnostics,
type WorkspaceRecoverySummary,
} from "./command-surface-contract"
import type { DoctorFixResult, DoctorReport, ForensicReport, SkillHealthReport } from "./diagnostics-types"
import type { KnowledgeData, CapturesData, CaptureResolveRequest, CaptureResolveResult } from "./knowledge-captures-types"
import type { SettingsData } from "./settings-types"
import type {
HistoryData,
InspectData,
HooksData,
ExportResult,
UndoInfo,
UndoResult,
CleanupData,
CleanupResult,
SteerData,
} from "./remaining-command-types"
import { isGitSummaryResponse, type GitSummaryResponse } from "./git-summary-contract"
import type { PendingImage } from "./image-utils"
import type { ChatMessage } from "./pty-chat-parser"
import type {
SessionBrowserNameFilter,
SessionBrowserResponse,
SessionBrowserSession,
SessionBrowserSortMode,
SessionManageResponse,
} from "./session-browser-contract"
import { authFetch, appendAuthParam } from "./auth"
import { ContextualTips } from "../../packages/pi-coding-agent/src/core/contextual-tips.ts"
export type WorkspaceStatus = "idle" | "loading" | "ready" | "error" | "unauthenticated"
export type WorkspaceConnectionState =
| "idle"
| "connecting"
| "connected"
| "reconnecting"
| "disconnected"
| "error"
export type TerminalLineType = "input" | "output" | "system" | "success" | "error"
export type BridgePhase = "idle" | "starting" | "ready" | "failed"
export type WorkspaceStatusTone = "muted" | "info" | "success" | "warning" | "danger"
export interface WorkspaceModelRef {
id?: string
provider?: string
providerId?: string
}
export interface BridgeLastError {
message: string
at: string
phase: BridgePhase
afterSessionAttachment: boolean
commandType?: string
}
export interface WorkspaceSessionState {
model?: WorkspaceModelRef
thinkingLevel: string
isStreaming: boolean
isCompacting: boolean
steeringMode: "all" | "one-at-a-time"
followUpMode: "all" | "one-at-a-time"
sessionFile?: string
sessionId: string
sessionName?: string
autoCompactionEnabled: boolean
autoRetryEnabled: boolean
retryInProgress: boolean
retryAttempt: number
messageCount: number
pendingMessageCount: number
}
export interface BridgeRuntimeSnapshot {
phase: BridgePhase
projectCwd: string
projectSessionsDir: string
packageRoot: string
startedAt: string | null
updatedAt: string
connectionCount: number
lastCommandType: string | null
activeSessionId: string | null
activeSessionFile: string | null
sessionState: WorkspaceSessionState | null
lastError: BridgeLastError | null
}
export type { WorkspaceTaskTarget, RiskLevel, WorkspaceSliceTarget, WorkspaceMilestoneTarget } from "./workspace-types.js"
export interface WorkspaceScopeTarget {
scope: string
label: string
kind: "project" | "milestone" | "slice" | "task"
}
export interface WorkspaceValidationIssue {
message?: string
[key: string]: unknown
}
export interface WorkspaceIndex {
milestones: WorkspaceMilestoneTarget[]
active: {
milestoneId?: string
sliceId?: string
taskId?: string
phase: string
}
scopes: WorkspaceScopeTarget[]
validationIssues: WorkspaceValidationIssue[]
}
export interface RtkSessionSavings {
commands: number
inputTokens: number
outputTokens: number
savedTokens: number
savingsPct: number
totalTimeMs: number
avgTimeMs: number
updatedAt: string
}
export interface AutoDashboardData {
active: boolean
paused: boolean
stepMode: boolean
startTime: number
elapsed: number
currentUnit: { type: string; id: string; startedAt: number } | null
completedUnits: { type: string; id: string; startedAt: number; finishedAt: number }[]
basePath: string
totalCost: number
totalTokens: number
rtkSavings?: RtkSessionSavings | null
/** Whether RTK is enabled via experimental.rtk preference. False when not opted in. */
rtkEnabled?: boolean
}
export interface BootResumableSession {
id: string
path: string
cwd: string
name?: string
createdAt: string
modifiedAt: string
messageCount: number
isActive: boolean
}
export interface WorkspaceOnboardingProviderState {
id: string
label: string
required: true
recommended: boolean
configured: boolean
configuredVia: "auth_file" | "environment" | "runtime" | null
supports: {
apiKey: boolean
oauth: boolean
oauthAvailable: boolean
usesCallbackServer: boolean
}
}
export interface WorkspaceOnboardingOptionalSectionState {
id: string
label: string
blocking: false
skippable: true
configured: boolean
configuredItems: string[]
}
export interface WorkspaceOnboardingValidationResult {
status: "succeeded" | "failed"
providerId: string
method: "api_key" | "oauth"
checkedAt: string
message: string
persisted: boolean
}
export interface WorkspaceOnboardingFlowState {
flowId: string
providerId: string
providerLabel: string
status: "idle" | "running" | "awaiting_browser_auth" | "awaiting_input" | "succeeded" | "failed" | "cancelled"
updatedAt: string
auth: {
url: string
instructions?: string
} | null
prompt: {
kind: "text" | "manual_code"
message: string
placeholder?: string
allowEmpty?: boolean
} | null
progress: string[]
error: string | null
}
export interface WorkspaceOnboardingBridgeAuthRefreshState {
phase: "idle" | "pending" | "succeeded" | "failed"
strategy: "restart" | null
startedAt: string | null
completedAt: string | null
error: string | null
}
export interface WorkspaceOnboardingState {
status: "blocked" | "ready"
locked: boolean
lockReason: "required_setup" | "bridge_refresh_pending" | "bridge_refresh_failed" | null
required: {
blocking: true
skippable: false
satisfied: boolean
satisfiedBy: { providerId: string; source: "auth_file" | "environment" | "runtime" } | null
providers: WorkspaceOnboardingProviderState[]
}
optional: {
blocking: false
skippable: true
sections: WorkspaceOnboardingOptionalSectionState[]
}
lastValidation: WorkspaceOnboardingValidationResult | null
activeFlow: WorkspaceOnboardingFlowState | null
bridgeAuthRefresh: WorkspaceOnboardingBridgeAuthRefreshState
}
// ─── Project Detection ──────────────────────────────────────────────────────
export type ProjectDetectionKind =
| "active-sf"
| "empty-sf"
| "v1-legacy"
| "brownfield"
| "blank"
export interface ProjectDetectionSignals {
hasGsdFolder: boolean
hasPlanningFolder: boolean
hasGitRepo: boolean
hasPackageJson: boolean
isMonorepo?: boolean
fileCount: number
}
export interface ProjectDetection {
kind: ProjectDetectionKind
signals: ProjectDetectionSignals
}
// ─── Boot Payload ───────────────────────────────────────────────────────────
export interface WorkspaceBootPayload {
project: {
cwd: string
sessionsDir: string
packageRoot: string
}
workspace: WorkspaceIndex
auto: AutoDashboardData
onboarding: WorkspaceOnboardingState
onboardingNeeded: boolean
resumableSessions: BootResumableSession[]
bridge: BridgeRuntimeSnapshot
projectDetection?: ProjectDetection
}
export interface BridgeStatusEvent {
type: "bridge_status"
bridge: BridgeRuntimeSnapshot
}
export type LiveStateInvalidationDomain = "auto" | "workspace" | "recovery" | "resumable_sessions"
export type LiveStateInvalidationSource = "bridge_event" | "rpc_command" | "session_manage"
export type LiveStateInvalidationReason =
| "agent_end"
| "turn_end"
| "auto_retry_start"
| "auto_retry_end"
| "auto_compaction_start"
| "auto_compaction_end"
| "new_session"
| "switch_session"
| "fork"
| "set_session_name"
export interface LiveStateInvalidationEvent {
type: "live_state_invalidation"
at: string
reason: LiveStateInvalidationReason
source: LiveStateInvalidationSource
domains: LiveStateInvalidationDomain[]
workspaceIndexCacheInvalidated: boolean
}
export type WorkspaceFreshnessStatus = "idle" | "fresh" | "refreshing" | "stale" | "error"
export interface WorkspaceFreshnessBucket {
status: WorkspaceFreshnessStatus
stale: boolean
reloadCount: number
lastRequestedAt: string | null
lastSuccessAt: string | null
lastFailureAt: string | null
lastFailure: string | null
invalidatedAt: string | null
invalidationReason: LiveStateInvalidationReason | null
invalidationSource: LiveStateInvalidationSource | null
}
export interface WorkspaceLiveFreshnessState {
auto: WorkspaceFreshnessBucket
workspace: WorkspaceFreshnessBucket
recovery: WorkspaceFreshnessBucket
resumableSessions: WorkspaceFreshnessBucket
gitSummary: WorkspaceFreshnessBucket
sessionBrowser: WorkspaceFreshnessBucket
sessionStats: WorkspaceFreshnessBucket
}
export interface WorkspaceLiveState {
auto: AutoDashboardData | null
workspace: WorkspaceIndex | null
resumableSessions: BootResumableSession[]
recoverySummary: WorkspaceRecoverySummary
freshness: WorkspaceLiveFreshnessState
softBootRefreshCount: number
targetedRefreshCount: number
}
// Discriminated union for extension UI requests — matches the authoritative
// RpcExtensionUIRequest from rpc-types.ts. Blocking methods queue in pendingUiRequests;
// fire-and-forget methods update state maps directly.
export type ExtensionUiRequestEvent =
| { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number; allowMultiple?: boolean }
| { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string; timeout?: number }
| { type: "extension_ui_request"; id: string; method: "input"; title: string; placeholder?: string; timeout?: number }
| { type: "extension_ui_request"; id: string; method: "editor"; title: string; prefill?: string }
| { type: "extension_ui_request"; id: string; method: "notify"; message: string; notifyType?: "info" | "warning" | "error" }
| { type: "extension_ui_request"; id: string; method: "setStatus"; statusKey: string; statusText: string | undefined }
| { type: "extension_ui_request"; id: string; method: "setWidget"; widgetKey: string; widgetLines: string[] | undefined; widgetPlacement?: "aboveEditor" | "belowEditor" }
| { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }
| { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string }
export interface ExtensionErrorEvent {
type: "extension_error"
extensionPath?: string
event?: string
error: string
}
export interface MessageUpdateEvent {
type: "message_update"
assistantMessageEvent?: {
type: string
delta?: string
[key: string]: unknown
}
[key: string]: unknown
}
export interface ToolExecutionStartEvent {
type: "tool_execution_start"
toolCallId: string
toolName: string
[key: string]: unknown
}
export interface ToolExecutionUpdateEvent {
type: "tool_execution_update"
toolCallId: string
toolName: string
partialResult?: {
content?: Array<{ type: string; text?: string }>
details?: Record<string, unknown>
isError?: boolean
}
[key: string]: unknown
}
export interface ToolExecutionEndEvent {
type: "tool_execution_end"
toolCallId: string
toolName: string
isError?: boolean
[key: string]: unknown
}
export interface AgentEndEvent {
type: "agent_end"
[key: string]: unknown
}
export interface TurnEndEvent {
type: "turn_end"
[key: string]: unknown
}
export type WorkspaceEvent =
| BridgeStatusEvent
| LiveStateInvalidationEvent
| ExtensionUiRequestEvent
| ExtensionErrorEvent
| MessageUpdateEvent
| ToolExecutionStartEvent
| ToolExecutionUpdateEvent
| ToolExecutionEndEvent
| AgentEndEvent
| TurnEndEvent
| ({ type: Exclude<string, "bridge_status" | "live_state_invalidation" | "extension_ui_request" | "extension_error" | "message_update" | "tool_execution_start" | "tool_execution_update" | "tool_execution_end" | "agent_end" | "turn_end">; [key: string]: unknown } & Record<string, unknown>)
export function isWorkspaceEvent(value: unknown): value is WorkspaceEvent {
return value !== null && typeof value === "object" && typeof (value as Record<string, unknown>).type === "string"
}
export interface WorkspaceCommandResponse {
type: "response"
command: string
success: boolean
error?: string
data?: unknown
id?: string
code?: string
details?: {
reason?: "required_setup" | "bridge_refresh_pending" | "bridge_refresh_failed"
onboarding?: Partial<WorkspaceOnboardingState>
}
}
export interface WorkspaceBridgeCommand {
type: string
[key: string]: unknown
}
export interface WorkspaceTerminalLine {
id: string
type: TerminalLineType
content: string
timestamp: string
}
export type WorkspaceOnboardingRequestState =
| "idle"
| "refreshing"
| "saving_api_key"
| "starting_provider_flow"
| "submitting_provider_flow_input"
| "cancelling_provider_flow"
| "logging_out_provider"
// A blocking UI request that needs user response before the agent can continue.
// The `method` field discriminates the payload shape.
export type PendingUiRequest = Extract<
ExtensionUiRequestEvent,
{ method: "select" | "confirm" | "input" | "editor" }
>
export interface ActiveToolExecution {
id: string
name: string
args?: Record<string, unknown>
result?: {
content?: Array<{ type: string; text?: string }>
details?: Record<string, unknown>
isError?: boolean
}
}
/** Completed tool execution with result — kept for chat rendering */
export interface CompletedToolExecution {
id: string
name: string
args: Record<string, unknown>
result?: {
content?: Array<{ type: string; text?: string }>
details?: Record<string, unknown>
isError?: boolean
}
}
/**
* A chronologically-ordered segment within a single assistant turn.
* The sequence `thinking → text → tool → thinking → text → tool …`
* is captured as separate segments so the chat UI can render them
* in the correct interleaved order.
*/
export type TurnSegment =
| { kind: "thinking"; content: string }
| { kind: "text"; content: string }
| { kind: "tool"; tool: CompletedToolExecution }
export interface WidgetContent {
lines: string[] | undefined
placement?: "aboveEditor" | "belowEditor"
}
export interface WorkspaceStoreState {
bootStatus: WorkspaceStatus
connectionState: WorkspaceConnectionState
boot: WorkspaceBootPayload | null
live: WorkspaceLiveState
terminalLines: WorkspaceTerminalLine[]
lastClientError: string | null
lastBridgeError: BridgeLastError | null
sessionAttached: boolean
lastEventType: string | null
commandInFlight: string | null
lastSlashCommandOutcome: BrowserSlashCommandDispatchResult | null
commandSurface: WorkspaceCommandSurfaceState
onboardingRequestState: WorkspaceOnboardingRequestState
onboardingRequestProviderId: string | null
// Live interaction state
pendingUiRequests: PendingUiRequest[]
streamingAssistantText: string
streamingThinkingText: string
liveTranscript: string[]
/** Thinking text for each liveTranscript block (parallel array — same length) */
liveThinkingTranscript: string[]
completedToolExecutions: CompletedToolExecution[]
activeToolExecution: ActiveToolExecution | null
/**
* Ordered segments within the current streaming turn.
* Captures the chronological sequence: thinking → text → tool → thinking → text → ...
* Flushed to `completedTurnSegments` on turn boundary.
*/
currentTurnSegments: TurnSegment[]
/**
* Segment history for completed turns. Each entry is a full turn's segments.
* Parallel to `liveTranscript` (same index = same turn).
*/
completedTurnSegments: TurnSegment[][]
/** User messages in chat — persisted in store so they survive component unmount/remount */
chatUserMessages: ChatMessage[]
statusTexts: Record<string, string>
widgetContents: Record<string, WidgetContent>
titleOverride: string | null
editorTextBuffer: string | null
}
const MAX_TERMINAL_LINES = 250
export const MAX_TRANSCRIPT_BLOCKS = 100
export const COMMAND_TIMEOUT_MS = 90_000
export const VISIBILITY_REFRESH_THRESHOLD_MS = 30_000
const IMPLEMENTED_BROWSER_COMMAND_SURFACES = new Set<BrowserSlashCommandSurface>([
"settings",
"model",
"thinking",
"git",
"resume",
"name",
"fork",
"compact",
"login",
"logout",
"session",
"export",
// SF subcommand surfaces (S02)
"sf-status",
"sf-visualize",
"sf-forensics",
"sf-doctor",
"sf-skill-health",
"sf-knowledge",
"sf-capture",
"sf-triage",
"sf-quick",
"sf-history",
"sf-undo",
"sf-inspect",
"sf-prefs",
"sf-config",
"sf-hooks",
"sf-mode",
"sf-steer",
"sf-export",
"sf-cleanup",
"sf-queue",
])
function timestampLabel(date = new Date()): string {
return date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
}
function createTerminalLine(type: TerminalLineType, content: string): WorkspaceTerminalLine {
return {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
type,
content,
timestamp: timestampLabel(),
}
}
function withTerminalLine(lines: WorkspaceTerminalLine[], line: WorkspaceTerminalLine): WorkspaceTerminalLine[] {
return [...lines, line].slice(-MAX_TERMINAL_LINES)
}
function hasAttachedSession(bridge: BridgeRuntimeSnapshot | null | undefined): boolean {
return Boolean(bridge?.activeSessionId || bridge?.sessionState?.sessionId)
}
function normalizeClientError(error: unknown): string {
if (error instanceof Error) return error.message
return String(error)
}
function getCommandInputLabel(command: WorkspaceBridgeCommand): string {
return typeof command.message === "string" ? command.message : `/${command.type}`
}
function summarizeBridgeStatus(bridge: BridgeRuntimeSnapshot): { type: TerminalLineType; message: string } {
if (bridge.phase === "failed") {
return {
type: "error",
message: `Bridge failed${bridge.lastError?.message ? `${bridge.lastError.message}` : ""}`,
}
}
if (bridge.phase === "starting") {
return {
type: "system",
message: "Bridge starting for the current project…",
}
}
if (bridge.phase === "ready") {
const sessionLabel = getSessionLabelFromBridge(bridge)
return {
type: "success",
message: sessionLabel
? `Live bridge ready — attached to ${sessionLabel}`
: "Live bridge ready — session attachment pending",
}
}
return {
type: "system",
message: "Bridge idle",
}
}
function summarizeEvent(event: WorkspaceEvent): { type: TerminalLineType; message: string } | null {
switch (event.type) {
case "bridge_status":
return summarizeBridgeStatus((event as BridgeStatusEvent).bridge)
case "live_state_invalidation":
return {
type: "system",
message: `[Live] Refreshing ${Array.isArray(event.domains) ? event.domains.join(", ") : "state"} after ${String(event.reason).replaceAll("_", " ")}`,
}
case "agent_start":
return { type: "system", message: "[Agent] Run started" }
case "agent_end":
return { type: "success", message: "[Agent] Run finished" }
case "turn_start":
return { type: "system", message: "[Agent] Turn started" }
case "turn_end":
return { type: "success", message: "[Agent] Turn complete" }
case "tool_execution_start":
return {
type: "output",
message: `[Tool] ${typeof event.toolName === "string" ? event.toolName : "tool"} started`,
}
case "tool_execution_update":
return null
case "tool_execution_end":
return {
type: event.isError ? "error" : "success",
message: `[Tool] ${typeof event.toolName === "string" ? event.toolName : "tool"} ${event.isError ? "failed" : "completed"}`,
}
case "auto_compaction_start":
return { type: "system", message: "[Auto] Compaction started" }
case "auto_compaction_end":
return {
type: event.aborted ? "error" : "success",
message: event.aborted ? "[Auto] Compaction aborted" : "[Auto] Compaction finished",
}
case "auto_retry_start":
return {
type: "system",
message: `[Auto] Retry ${String(event.attempt)}/${String(event.maxAttempts)} scheduled`,
}
case "auto_retry_end":
return {
type: event.success ? "success" : "error",
message: event.success ? "[Auto] Retry recovered the run" : "[Auto] Retry exhausted",
}
case "extension_ui_request": {
const uiEvent = event as ExtensionUiRequestEvent
const detail =
"title" in uiEvent && typeof uiEvent.title === "string" && uiEvent.title.trim().length > 0
? uiEvent.title
: "message" in uiEvent && typeof uiEvent.message === "string" && uiEvent.message.trim().length > 0
? uiEvent.message
: uiEvent.method
return {
type: ("notifyType" in uiEvent && uiEvent.notifyType === "error") ? "error" : "system",
message: `[UI] ${detail}`,
}
}
case "extension_error":
return { type: "error", message: `[Extension] ${event.error}` }
default:
return null
}
}
type OnboardingApiPayload = {
onboarding?: WorkspaceOnboardingState
error?: string
}
const ACTIVE_ONBOARDING_FLOW_STATUSES = new Set<WorkspaceOnboardingFlowState["status"]>([
"running",
"awaiting_browser_auth",
"awaiting_input",
])
const TERMINAL_ONBOARDING_FLOW_STATUSES = new Set<WorkspaceOnboardingFlowState["status"]>([
"succeeded",
"failed",
"cancelled",
])
function findOnboardingProviderLabel(onboarding: WorkspaceOnboardingState, providerId: string): string {
return onboarding.required.providers.find((provider) => provider.id === providerId)?.label ?? providerId
}
function mergeOnboardingState(
current: WorkspaceOnboardingState,
patch: Partial<WorkspaceOnboardingState>,
): WorkspaceOnboardingState {
return {
...current,
...patch,
required: {
...current.required,
...(patch.required ?? {}),
providers: patch.required?.providers ?? current.required.providers,
},
optional: {
...current.optional,
...(patch.optional ?? {}),
sections: patch.optional?.sections ?? current.optional.sections,
},
bridgeAuthRefresh: {
...current.bridgeAuthRefresh,
...(patch.bridgeAuthRefresh ?? {}),
},
}
}
function cloneBootWithBridge(
boot: WorkspaceBootPayload | null,
bridge: BridgeRuntimeSnapshot,
): WorkspaceBootPayload | null {
if (!boot) return null
const nextBoot = {
...boot,
bridge,
}
return {
...nextBoot,
resumableSessions: overlayLiveBridgeSessionState(nextBoot.resumableSessions, nextBoot),
}
}
function patchBootSessionState(
boot: WorkspaceBootPayload | null,
patch: Partial<WorkspaceSessionState>,
): WorkspaceBootPayload | null {
if (!boot?.bridge.sessionState) return boot
return cloneBootWithBridge(boot, {
...boot.bridge,
sessionState: {
...boot.bridge.sessionState,
...patch,
},
})
}
function patchBootSessionName(
boot: WorkspaceBootPayload | null,
sessionPath: string,
name: string,
): WorkspaceBootPayload | null {
if (!boot) return null
const isActiveSession = getLiveActiveSessionPath(boot) === sessionPath
const nextBridge =
isActiveSession && boot.bridge.sessionState
? {
...boot.bridge,
sessionState: {
...boot.bridge.sessionState,
sessionName: name,
},
}
: boot.bridge
const nextBoot = {
...boot,
bridge: nextBridge,
}
return {
...nextBoot,
resumableSessions: overlayLiveBridgeSessionState(
nextBoot.resumableSessions.map((session) =>
session.path === sessionPath
? {
...session,
name,
}
: session,
),
nextBoot,
),
}
}
function patchBootActiveSession(
boot: WorkspaceBootPayload | null,
sessionPath: string,
sessionName?: string,
): WorkspaceBootPayload | null {
if (!boot) return null
const selectedSession = boot.resumableSessions.find((session) => session.path === sessionPath)
const nextBridge = {
...boot.bridge,
activeSessionFile: sessionPath,
activeSessionId: selectedSession?.id ?? boot.bridge.activeSessionId,
sessionState: boot.bridge.sessionState
? {
...boot.bridge.sessionState,
sessionFile: sessionPath,
sessionId: selectedSession?.id ?? boot.bridge.sessionState.sessionId,
sessionName: sessionName ?? selectedSession?.name ?? boot.bridge.sessionState.sessionName,
}
: boot.bridge.sessionState,
}
const nextBoot = {
...boot,
bridge: nextBridge,
}
return {
...nextBoot,
resumableSessions: overlayLiveBridgeSessionState(
nextBoot.resumableSessions.map((session) => ({
...session,
isActive: session.path === sessionPath,
})),
nextBoot,
),
}
}
function cloneBootWithOnboarding(
boot: WorkspaceBootPayload | null,
onboarding: WorkspaceOnboardingState,
): WorkspaceBootPayload | null {
if (!boot) return null
return {
...boot,
onboarding,
onboardingNeeded: onboarding.locked,
}
}
function cloneBootWithPartialOnboarding(
boot: WorkspaceBootPayload | null,
onboarding: Partial<WorkspaceOnboardingState>,
): WorkspaceBootPayload | null {
if (!boot) return null
return cloneBootWithOnboarding(boot, mergeOnboardingState(boot.onboarding, onboarding))
}
function summarizeOnboardingState(onboarding: WorkspaceOnboardingState): { type: TerminalLineType; message: string } | null {
if (onboarding.bridgeAuthRefresh.phase === "failed") {
return {
type: "error",
message: onboarding.bridgeAuthRefresh.error
? `Bridge auth refresh failed — ${onboarding.bridgeAuthRefresh.error}`
: "Bridge auth refresh failed after setup",
}
}
if (onboarding.bridgeAuthRefresh.phase === "pending") {
return {
type: "system",
message: "Credentials saved — refreshing bridge auth before the workspace unlocks…",
}
}
if (onboarding.lastValidation?.status === "failed") {
return {
type: "error",
message: `Credential validation failed — ${onboarding.lastValidation.message}`,
}
}
if (!onboarding.locked && onboarding.lastValidation?.status === "succeeded") {
return {
type: "success",
message: `${findOnboardingProviderLabel(onboarding, onboarding.lastValidation.providerId)} is ready — workspace unlocked`,
}
}
if (onboarding.activeFlow?.status === "awaiting_browser_auth") {
return {
type: "system",
message: `${onboarding.activeFlow.providerLabel} sign-in is waiting for browser confirmation`,
}
}
if (onboarding.activeFlow?.status === "awaiting_input") {
return {
type: "system",
message: `${onboarding.activeFlow.providerLabel} sign-in needs one more input step`,
}
}
if (onboarding.activeFlow?.status === "cancelled") {
return {
type: "system",
message: `${onboarding.activeFlow.providerLabel} sign-in was cancelled`,
}
}
if (onboarding.activeFlow?.status === "failed") {
return {
type: "error",
message: onboarding.activeFlow.error
? `${onboarding.activeFlow.providerLabel} sign-in failed — ${onboarding.activeFlow.error}`
: `${onboarding.activeFlow.providerLabel} sign-in failed`,
}
}
if (onboarding.lockReason === "required_setup") {
return {
type: "system",
message: "Onboarding is still required before model-backed prompts will run",
}
}
return null
}
function bootSeedLines(boot: WorkspaceBootPayload): WorkspaceTerminalLine[] {
const lines = [
createTerminalLine("system", `SF web workspace attached to ${boot.project.cwd}`),
createTerminalLine("system", `Workspace scope: ${getCurrentScopeLabel(boot.workspace)}`),
]
const bridgeSummary = summarizeBridgeStatus(boot.bridge)
lines.push(createTerminalLine(bridgeSummary.type, bridgeSummary.message))
if (boot.bridge.lastError) {
lines.push(createTerminalLine("error", `Bridge error: ${boot.bridge.lastError.message}`))
}
const onboardingSummary = summarizeOnboardingState(boot.onboarding)
if (onboardingSummary) {
lines.push(createTerminalLine(onboardingSummary.type, onboardingSummary.message))
}
return lines
}
function responseToLine(response: WorkspaceCommandResponse): WorkspaceTerminalLine {
if (!response.success) {
return createTerminalLine("error", `Command failed (${response.command}) — ${response.error ?? "unknown error"}`)
}
switch (response.command) {
case "get_state":
return createTerminalLine("success", "Session state refreshed")
case "new_session":
return createTerminalLine("success", "Started a new session")
case "prompt":
return createTerminalLine("success", "Prompt accepted by the live bridge")
case "follow_up":
return createTerminalLine("success", "Follow-up queued on the live bridge")
default:
return createTerminalLine("success", `Command accepted (${response.command})`)
}
}
export function shortenPath(path: string | undefined, segmentCount = 3): string {
if (!path) return "—"
const parts = path.split(/[\\/]/).filter(Boolean)
if (parts.length <= segmentCount) {
return path.startsWith("/") ? `/${parts.join("/")}` : parts.join("/")
}
const tail = parts.slice(-segmentCount).join("/")
return `…/${tail}`
}
export function getProjectDisplayName(path: string | undefined): string {
if (!path) return "Current project"
const parts = path.split(/[\\/]/).filter(Boolean)
return parts.at(-1) || path
}
export function formatDuration(ms: number): string {
if (!ms || ms < 1000) return "0m"
const totalMinutes = Math.floor(ms / 60_000)
const hours = Math.floor(totalMinutes / 60)
const minutes = totalMinutes % 60
if (hours > 0) return `${hours}h ${minutes}m`
return `${minutes}m`
}
export function formatTokens(tokens: number): string {
if (!Number.isFinite(tokens) || tokens <= 0) return "0"
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`
if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}K`
return String(Math.round(tokens))
}
export function formatCost(cost: number): string {
if (!Number.isFinite(cost) || cost <= 0) return "$0.00"
return `$${cost.toFixed(2)}`
}
export function getCurrentScopeLabel(workspace: WorkspaceIndex | null | undefined): string {
if (!workspace) return "Project scope pending"
const scope = [workspace.active.milestoneId, workspace.active.sliceId, workspace.active.taskId]
.filter(Boolean)
.join("/")
return scope ? `${scope}${workspace.active.phase}` : `project — ${workspace.active.phase}`
}
export function getCurrentBranch(workspace: WorkspaceIndex | null | undefined): string | null {
if (!workspace?.active.milestoneId || !workspace.active.sliceId) {
return null
}
const milestone = workspace.milestones.find((entry) => entry.id === workspace.active.milestoneId)
const slice = milestone?.slices.find((entry) => entry.id === workspace.active.sliceId)
return slice?.branch ?? null
}
export function getCurrentSlice(workspace: WorkspaceIndex | null | undefined): WorkspaceSliceTarget | null {
if (!workspace?.active.milestoneId || !workspace.active.sliceId) return null
const milestone = workspace.milestones.find((entry) => entry.id === workspace.active.milestoneId)
return milestone?.slices.find((entry) => entry.id === workspace.active.sliceId) ?? null
}
export function getSessionLabelFromBridge(bridge: BridgeRuntimeSnapshot | null | undefined): string | null {
if (!bridge?.sessionState && !bridge?.activeSessionId) return null
const sessionName = bridge.sessionState?.sessionName?.trim()
if (sessionName) return sessionName
if (bridge.activeSessionId) return `session ${bridge.activeSessionId}`
return bridge.sessionState?.sessionId ?? null
}
export function getModelLabel(bridge: BridgeRuntimeSnapshot | null | undefined): string {
const model = bridge?.sessionState?.model
if (!model) return "model pending"
return model.id || model.providerId || model.provider || "model pending"
}
function getCurrentModelSelection(
bridge: BridgeRuntimeSnapshot | null | undefined,
): { provider?: string; modelId?: string } | null {
const model = bridge?.sessionState?.model
if (!model) return null
return {
provider: model.provider ?? model.providerId,
modelId: model.id,
}
}
function getPreferredOnboardingProviderId(onboarding: WorkspaceOnboardingState | null | undefined): string | null {
if (!onboarding) return null
if (onboarding.required.satisfiedBy?.providerId) {
return onboarding.required.satisfiedBy.providerId
}
const recommended = onboarding.required.providers.find((provider) => !provider.configured && provider.recommended)
if (recommended) return recommended.id
const firstUnconfigured = onboarding.required.providers.find((provider) => !provider.configured)
if (firstUnconfigured) return firstUnconfigured.id
return onboarding.required.providers[0]?.id ?? null
}
function normalizeAvailableModels(
payload: unknown,
currentModel: { provider?: string; modelId?: string } | null,
): CommandSurfaceModelOption[] {
const models =
payload &&
typeof payload === "object" &&
"models" in payload &&
Array.isArray((payload as { models?: unknown[] }).models)
? (payload as { models: Array<Record<string, unknown>> }).models
: []
const results: CommandSurfaceModelOption[] = []
for (const model of models) {
const provider =
typeof model.provider === "string"
? model.provider
: typeof model.providerId === "string"
? model.providerId
: undefined
const modelId = typeof model.id === "string" ? model.id : undefined
if (!provider || !modelId) continue
results.push({
provider,
modelId,
name: typeof model.name === "string" ? model.name : undefined,
reasoning: Boolean(model.reasoning),
isCurrent: provider === currentModel?.provider && modelId === currentModel?.modelId,
})
}
return results
.sort((left, right) => Number(right.isCurrent) - Number(left.isCurrent) || left.provider.localeCompare(right.provider) || left.modelId.localeCompare(right.modelId))
}
function normalizeSessionStats(payload: unknown): CommandSurfaceSessionStats | null {
if (!payload || typeof payload !== "object") return null
const stats = payload as Partial<CommandSurfaceSessionStats>
if (typeof stats.sessionId !== "string") return null
return {
sessionFile: typeof stats.sessionFile === "string" ? stats.sessionFile : undefined,
sessionId: stats.sessionId,
userMessages: Number(stats.userMessages ?? 0),
assistantMessages: Number(stats.assistantMessages ?? 0),
toolCalls: Number(stats.toolCalls ?? 0),
toolResults: Number(stats.toolResults ?? 0),
totalMessages: Number(stats.totalMessages ?? 0),
tokens: {
input: Number(stats.tokens?.input ?? 0),
output: Number(stats.tokens?.output ?? 0),
cacheRead: Number(stats.tokens?.cacheRead ?? 0),
cacheWrite: Number(stats.tokens?.cacheWrite ?? 0),
total: Number(stats.tokens?.total ?? 0),
},
cost: Number(stats.cost ?? 0),
}
}
function normalizeForkMessages(payload: unknown): CommandSurfaceForkMessage[] {
const messages =
payload &&
typeof payload === "object" &&
"messages" in payload &&
Array.isArray((payload as { messages?: unknown[] }).messages)
? (payload as { messages: Array<Record<string, unknown>> }).messages
: []
return messages
.map((message) => {
const entryId = typeof message.entryId === "string" ? message.entryId : undefined
const text = typeof message.text === "string" ? message.text : undefined
if (!entryId || !text) return null
return { entryId, text } satisfies CommandSurfaceForkMessage
})
.filter((message): message is CommandSurfaceForkMessage => message !== null)
}
function normalizeCompactionResult(payload: unknown): CommandSurfaceCompactionResult | null {
if (!payload || typeof payload !== "object") return null
const result = payload as Partial<CommandSurfaceCompactionResult>
if (typeof result.summary !== "string" || typeof result.firstKeptEntryId !== "string") return null
return {
summary: result.summary,
firstKeptEntryId: result.firstKeptEntryId,
tokensBefore: Number(result.tokensBefore ?? 0),
details: result.details,
}
}
function normalizeGitSummaryPayload(payload: unknown): GitSummaryResponse | null {
return isGitSummaryResponse(payload) ? payload : null
}
function normalizeGitSummaryError(
current: CommandSurfaceGitSummaryState,
message: string,
): CommandSurfaceGitSummaryState {
return {
...current,
pending: false,
loaded: false,
error: message,
}
}
function normalizeRecoveryDiagnosticsPayload(payload: unknown): WorkspaceRecoveryDiagnostics | null {
if (!payload || typeof payload !== "object") return null
const candidate = payload as Partial<WorkspaceRecoveryDiagnostics>
if (candidate.status !== "ready" && candidate.status !== "unavailable") return null
if (typeof candidate.loadedAt !== "string") return null
if (!candidate.project || typeof candidate.project.cwd !== "string") return null
if (!candidate.summary || typeof candidate.summary.label !== "string" || typeof candidate.summary.detail !== "string") return null
if (!candidate.bridge || typeof candidate.bridge.phase !== "string") return null
if (!candidate.validation || typeof candidate.validation.total !== "number") return null
if (!candidate.doctor || typeof candidate.doctor.total !== "number") return null
if (!candidate.interruptedRun || typeof candidate.interruptedRun.available !== "boolean") return null
if (!candidate.actions || !Array.isArray(candidate.actions.browser) || !Array.isArray(candidate.actions.commands)) return null
return candidate as WorkspaceRecoveryDiagnostics
}
function createRecoveryStateFromDiagnostics(diagnostics: WorkspaceRecoveryDiagnostics): CommandSurfaceRecoveryState {
return {
phase: diagnostics.status === "ready" ? "ready" : "unavailable",
pending: false,
loaded: true,
stale: false,
diagnostics,
error: null,
lastLoadedAt: diagnostics.loadedAt,
lastInvalidatedAt: null,
lastFailureAt: null,
}
}
function markRecoveryStatePending(current: CommandSurfaceRecoveryState): CommandSurfaceRecoveryState {
return {
...current,
pending: true,
error: null,
phase: current.loaded ? current.phase : "loading",
}
}
function markRecoveryStateInvalidated(current: CommandSurfaceRecoveryState): CommandSurfaceRecoveryState {
if (!current.loaded && !current.error) return current
return {
...current,
stale: true,
lastInvalidatedAt: new Date().toISOString(),
}
}
function markRecoveryStateFailure(current: CommandSurfaceRecoveryState, message: string): CommandSurfaceRecoveryState {
return {
...current,
phase: "error",
pending: false,
stale: true,
error: message,
lastFailureAt: new Date().toISOString(),
}
}
function normalizeSessionBrowserPayload(payload: unknown): CommandSurfaceSessionBrowserState | null {
if (!payload || typeof payload !== "object") return null
const response = payload as Partial<SessionBrowserResponse>
const project = response.project
const query = response.query
if (!project || !query || !Array.isArray(response.sessions)) return null
if (project.scope !== "current_project") return null
if (typeof project.cwd !== "string" || typeof project.sessionsDir !== "string") return null
if (typeof query.query !== "string" || typeof query.sortMode !== "string" || typeof query.nameFilter !== "string") return null
const sessions = response.sessions.filter((session): session is SessionBrowserSession => {
return (
typeof session?.id === "string" &&
typeof session?.path === "string" &&
typeof session?.cwd === "string" &&
typeof session?.createdAt === "string" &&
typeof session?.modifiedAt === "string" &&
typeof session?.messageCount === "number" &&
typeof session?.firstMessage === "string" &&
typeof session?.isActive === "boolean" &&
typeof session?.depth === "number" &&
typeof session?.isLastInThread === "boolean" &&
Array.isArray(session?.ancestorHasNextSibling)
)
})
return {
scope: project.scope,
projectCwd: project.cwd,
projectSessionsDir: project.sessionsDir,
activeSessionPath: typeof project.activeSessionPath === "string" ? project.activeSessionPath : null,
query: query.query,
sortMode: query.sortMode as SessionBrowserSortMode,
nameFilter: query.nameFilter as SessionBrowserNameFilter,
totalSessions: Number(response.totalSessions ?? sessions.length),
returnedSessions: Number(response.returnedSessions ?? sessions.length),
sessions,
loaded: true,
error: null,
}
}
function getLiveActiveSessionPath(boot: WorkspaceBootPayload | null): string | null {
return boot?.bridge.activeSessionFile ?? boot?.bridge.sessionState?.sessionFile ?? null
}
function getLiveActiveSessionName(boot: WorkspaceBootPayload | null): string | undefined {
const value = boot?.bridge.sessionState?.sessionName?.trim()
return value ? value : undefined
}
function overlayLiveBridgeSessionState<T extends { path: string; isActive: boolean; name?: string }>(
sessions: T[],
boot: WorkspaceBootPayload | null,
): T[] {
const activeSessionPath = getLiveActiveSessionPath(boot)
const activeSessionName = getLiveActiveSessionName(boot)
return sessions.map((session) => {
const isActive = activeSessionPath ? session.path === activeSessionPath : session.isActive
return {
...session,
isActive,
...(isActive && activeSessionName ? { name: activeSessionName } : {}),
}
})
}
function syncSessionBrowserStateWithBridge(
sessionBrowser: CommandSurfaceSessionBrowserState,
boot: WorkspaceBootPayload | null,
): CommandSurfaceSessionBrowserState {
return {
...sessionBrowser,
activeSessionPath: getLiveActiveSessionPath(boot),
sessions: overlayLiveBridgeSessionState(sessionBrowser.sessions, boot),
}
}
function patchSessionBrowserSession(
sessionBrowser: CommandSurfaceSessionBrowserState,
sessionPath: string,
patch: Partial<Pick<SessionBrowserSession, "name" | "isActive">>,
): CommandSurfaceSessionBrowserState {
return {
...sessionBrowser,
activeSessionPath: patch.isActive ? sessionPath : sessionBrowser.activeSessionPath,
sessions: sessionBrowser.sessions.map((session) =>
session.path === sessionPath
? {
...session,
...patch,
}
: patch.isActive
? {
...session,
isActive: false,
}
: session,
),
}
}
function describeSessionPath(sessionPath: string, boot: WorkspaceBootPayload | null): string {
const knownSession = boot?.resumableSessions.find((session) => session.path === sessionPath)
if (knownSession?.name?.trim()) return knownSession.name.trim()
if (knownSession?.id) return knownSession.id
return shortenPath(sessionPath)
}
export interface WorkspaceOnboardingPresentation {
phase:
| "loading"
| "locked"
| "validating"
| "running_flow"
| "awaiting_browser_auth"
| "awaiting_input"
| "refreshing"
| "failure"
| "ready"
label: string
detail: string
tone: WorkspaceStatusTone
}
export function getOnboardingPresentation(
state: Pick<WorkspaceStoreState, "bootStatus" | "boot" | "onboardingRequestState">,
): WorkspaceOnboardingPresentation {
if (state.bootStatus === "loading" || !state.boot) {
return {
phase: "loading",
label: "Loading setup state",
detail: "Resolving the current project, bridge, and onboarding contract…",
tone: "info",
}
}
const onboarding = state.boot.onboarding
if (onboarding.activeFlow?.status === "awaiting_browser_auth") {
return {
phase: "awaiting_browser_auth",
label: "Continue sign-in in your browser",
detail: `${onboarding.activeFlow.providerLabel} is waiting for browser confirmation before the workspace can unlock.`,
tone: "info",
}
}
if (onboarding.activeFlow?.status === "awaiting_input") {
return {
phase: "awaiting_input",
label: "One more sign-in step is required",
detail: onboarding.activeFlow.prompt?.message ?? `${onboarding.activeFlow.providerLabel} needs one more input step.`,
tone: "info",
}
}
if (onboarding.lockReason === "bridge_refresh_pending") {
return {
phase: "refreshing",
label: "Refreshing bridge auth",
detail: "Credentials validated. The live bridge is restarting onto the new auth view before the shell unlocks.",
tone: "info",
}
}
if (onboarding.lockReason === "bridge_refresh_failed") {
return {
phase: "failure",
label: "Setup completed, but the shell is still locked",
detail: onboarding.bridgeAuthRefresh.error ?? "The bridge could not reload auth after setup.",
tone: "danger",
}
}
if (onboarding.lastValidation?.status === "failed") {
return {
phase: "failure",
label: "Credential validation failed",
detail: onboarding.lastValidation.message,
tone: "danger",
}
}
if (state.onboardingRequestState === "saving_api_key") {
return {
phase: "validating",
label: "Validating credentials",
detail: "Checking the provider key and saving it only if validation succeeds.",
tone: "info",
}
}
if (state.onboardingRequestState === "starting_provider_flow" || state.onboardingRequestState === "submitting_provider_flow_input") {
return {
phase: "running_flow",
label: "Advancing provider sign-in",
detail: "The onboarding flow is running and will update here as soon as the next step is ready.",
tone: "info",
}
}
if (onboarding.locked) {
return {
phase: "locked",
label: "Required setup needed",
detail: "Choose a required provider, validate it here, and the workspace will unlock without restarting the host.",
tone: "warning",
}
}
return {
phase: "ready",
label: "Workspace unlocked",
detail:
onboarding.lastValidation?.status === "succeeded"
? `${findOnboardingProviderLabel(onboarding, onboarding.lastValidation.providerId)} is ready and the workspace is live.`
: "Required setup is satisfied and the shell is ready for live commands.",
tone: "success",
}
}
export function getVisibleWorkspaceError(
state: Pick<WorkspaceStoreState, "boot" | "lastBridgeError" | "lastClientError">,
): string | null {
const onboarding = state.boot?.onboarding
if (onboarding?.bridgeAuthRefresh.phase === "failed" && onboarding.bridgeAuthRefresh.error) {
return onboarding.bridgeAuthRefresh.error
}
if (onboarding?.lastValidation?.status === "failed") {
return onboarding.lastValidation.message
}
return state.lastBridgeError?.message ?? state.lastClientError
}
export function getStatusPresentation(
state: Pick<WorkspaceStoreState, "bootStatus" | "connectionState" | "boot" | "onboardingRequestState">,
): {
label: string
tone: WorkspaceStatusTone
} {
if (state.bootStatus === "loading") {
return { label: "Loading workspace", tone: "info" }
}
if (state.bootStatus === "error") {
return { label: "Boot failed", tone: "danger" }
}
const onboardingPresentation = getOnboardingPresentation(state)
if (onboardingPresentation.phase !== "ready") {
return {
label: onboardingPresentation.label,
tone: onboardingPresentation.tone,
}
}
if (state.boot?.bridge.phase === "failed") {
return { label: "Bridge failed", tone: "danger" }
}
switch (state.connectionState) {
case "connected":
return { label: "Bridge connected", tone: "success" }
case "connecting":
return { label: "Connecting stream", tone: "info" }
case "reconnecting":
return { label: "Reconnecting stream", tone: "warning" }
case "disconnected":
return { label: "Stream disconnected", tone: "warning" }
case "error":
return { label: "Stream error", tone: "danger" }
default:
return { label: "Workspace idle", tone: "muted" }
}
}
function createFreshnessBucket(): WorkspaceFreshnessBucket {
return {
status: "idle",
stale: false,
reloadCount: 0,
lastRequestedAt: null,
lastSuccessAt: null,
lastFailureAt: null,
lastFailure: null,
invalidatedAt: null,
invalidationReason: null,
invalidationSource: null,
}
}
function createInitialRecoverySummary(): WorkspaceRecoverySummary {
return {
visible: false,
tone: "healthy",
label: "Recovery summary pending",
detail: "Waiting for the first live workspace snapshot.",
validationCount: 0,
retryInProgress: false,
retryAttempt: 0,
autoRetryEnabled: false,
isCompacting: false,
currentUnitId: null,
freshness: "idle",
entrypointLabel: "Inspect recovery",
lastError: null,
}
}
function createInitialWorkspaceLiveFreshnessState(): WorkspaceLiveFreshnessState {
return {
auto: createFreshnessBucket(),
workspace: createFreshnessBucket(),
recovery: createFreshnessBucket(),
resumableSessions: createFreshnessBucket(),
gitSummary: createFreshnessBucket(),
sessionBrowser: createFreshnessBucket(),
sessionStats: createFreshnessBucket(),
}
}
function createInitialWorkspaceLiveState(): WorkspaceLiveState {
return {
auto: null,
workspace: null,
resumableSessions: [],
recoverySummary: createInitialRecoverySummary(),
freshness: createInitialWorkspaceLiveFreshnessState(),
softBootRefreshCount: 0,
targetedRefreshCount: 0,
}
}
function withFreshnessRequested(bucket: WorkspaceFreshnessBucket): WorkspaceFreshnessBucket {
return {
...bucket,
status: "refreshing",
lastRequestedAt: new Date().toISOString(),
lastFailure: null,
}
}
function withFreshnessInvalidated(
bucket: WorkspaceFreshnessBucket,
reason: LiveStateInvalidationReason,
source: LiveStateInvalidationSource,
): WorkspaceFreshnessBucket {
return {
...bucket,
status: bucket.lastSuccessAt ? "stale" : bucket.status,
stale: true,
invalidatedAt: new Date().toISOString(),
invalidationReason: reason,
invalidationSource: source,
}
}
function withFreshnessSucceeded(bucket: WorkspaceFreshnessBucket): WorkspaceFreshnessBucket {
return {
...bucket,
status: "fresh",
stale: false,
reloadCount: bucket.reloadCount + 1,
lastSuccessAt: new Date().toISOString(),
lastFailureAt: null,
lastFailure: null,
}
}
function withFreshnessFailed(bucket: WorkspaceFreshnessBucket, error: string): WorkspaceFreshnessBucket {
return {
...bucket,
status: "error",
stale: true,
lastFailureAt: new Date().toISOString(),
lastFailure: error,
}
}
export function getLiveWorkspaceIndex(
state: Pick<WorkspaceStoreState, "boot" | "live">,
): WorkspaceIndex | null {
return state.live.workspace ?? state.boot?.workspace ?? null
}
export function getLiveAutoDashboard(
state: Pick<WorkspaceStoreState, "boot" | "live">,
): AutoDashboardData | null {
return state.live.auto ?? state.boot?.auto ?? null
}
export function getLiveResumableSessions(
state: Pick<WorkspaceStoreState, "boot" | "live">,
): BootResumableSession[] {
return state.live.resumableSessions.length > 0 ? state.live.resumableSessions : state.boot?.resumableSessions ?? []
}
export function createWorkspaceRecoverySummary(state: Pick<WorkspaceStoreState, "boot" | "live">): WorkspaceRecoverySummary {
const bridge = state.boot?.bridge ?? null
const workspace = getLiveWorkspaceIndex(state)
const auto = getLiveAutoDashboard(state)
const validationCount = workspace?.validationIssues.length ?? 0
const retryInProgress = Boolean(bridge?.sessionState?.retryInProgress)
const retryAttempt = bridge?.sessionState?.retryAttempt ?? 0
const autoRetryEnabled = Boolean(bridge?.sessionState?.autoRetryEnabled)
const isCompacting = Boolean(bridge?.sessionState?.isCompacting)
const freshnessBucket = state.live.freshness.recovery
const freshness =
freshnessBucket.status === "error"
? "error"
: freshnessBucket.stale
? "stale"
: freshnessBucket.lastSuccessAt
? "fresh"
: "idle"
const lastError = bridge?.lastError
? {
message: bridge.lastError.message,
phase: bridge.lastError.phase,
at: bridge.lastError.at,
}
: null
let tone: WorkspaceRecoverySummary["tone"] = "healthy"
let label = "Recovery summary healthy"
let detail = "No retry, compaction, bridge, or validation recovery signals are active."
if (!workspace && !auto && !bridge) {
return createInitialRecoverySummary()
}
if (lastError || freshness === "error") {
tone = "danger"
label = "Recovery attention required"
detail = lastError?.message ?? freshnessBucket.lastFailure ?? "A targeted live refresh failed."
} else if (validationCount > 0) {
tone = "warning"
label = `Recovery summary: ${validationCount} validation issue${validationCount === 1 ? "" : "s"}`
detail = "Workspace validation surfaced issues that may need doctor or audit follow-up."
} else if (retryInProgress) {
tone = "warning"
label = `Recovery retry active (attempt ${Math.max(1, retryAttempt)})`
detail = "The live bridge is retrying the current unit after a transient failure."
} else if (isCompacting) {
tone = "warning"
label = "Recovery compaction active"
detail = "The live session is compacting context before continuing."
} else if (freshness === "stale") {
tone = "warning"
label = "Recovery summary stale"
detail = freshnessBucket.invalidationReason
? `Waiting for a targeted refresh after ${freshnessBucket.invalidationReason.replaceAll("_", " ")}.`
: "Waiting for the next targeted refresh."
}
return {
visible: true,
tone,
label,
detail,
validationCount,
retryInProgress,
retryAttempt,
autoRetryEnabled,
isCompacting,
currentUnitId: auto?.currentUnit?.id ?? null,
freshness,
entrypointLabel: tone === "danger" || tone === "warning" ? "Inspect recovery" : "Review recovery",
lastError,
}
}
function applyBootToLiveState(
current: WorkspaceLiveState,
boot: WorkspaceBootPayload,
options: { soft?: boolean } = {},
): WorkspaceLiveState {
const next: WorkspaceLiveState = {
...current,
auto: boot.auto,
workspace: boot.workspace,
resumableSessions: boot.resumableSessions,
freshness: {
...current.freshness,
auto: withFreshnessSucceeded(current.freshness.auto),
workspace: withFreshnessSucceeded(current.freshness.workspace),
recovery: withFreshnessSucceeded(current.freshness.recovery),
resumableSessions: withFreshnessSucceeded(current.freshness.resumableSessions),
},
softBootRefreshCount: current.softBootRefreshCount + (options.soft ? 1 : 0),
}
next.recoverySummary = createWorkspaceRecoverySummary({ boot, live: next })
return next
}
function createInitialState(): WorkspaceStoreState {
return {
bootStatus: "idle",
connectionState: "idle",
boot: null,
live: createInitialWorkspaceLiveState(),
terminalLines: [createTerminalLine("system", "Preparing the live SF workspace…")],
lastClientError: null,
lastBridgeError: null,
sessionAttached: false,
lastEventType: null,
commandInFlight: null,
lastSlashCommandOutcome: null,
commandSurface: createInitialCommandSurfaceState(),
onboardingRequestState: "idle",
onboardingRequestProviderId: null,
// Live interaction state
pendingUiRequests: [],
streamingAssistantText: "",
streamingThinkingText: "",
liveTranscript: [],
liveThinkingTranscript: [],
completedToolExecutions: [],
activeToolExecution: null,
currentTurnSegments: [],
completedTurnSegments: [],
chatUserMessages: [],
statusTexts: {},
widgetContents: {},
titleOverride: null,
editorTextBuffer: null,
}
}
export function buildProjectUrl(path: string, projectCwd?: string): string {
if (!projectCwd) return path
const url = new URL(path, "http://localhost")
url.searchParams.set("project", projectCwd)
return url.pathname + url.search
}
export class SFWorkspaceStore {
constructor(private readonly projectCwd?: string) {}
private buildUrl(path: string): string {
return buildProjectUrl(path, this.projectCwd)
}
private state = createInitialState()
private readonly listeners = new Set<() => void>()
private readonly contextualTips = new ContextualTips()
private bootPromise: Promise<void> | null = null
private eventSource: EventSource | null = null
private onboardingPollTimer: ReturnType<typeof setInterval> | null = null
private started = false
private disposed = false
private lastBridgeDigest: string | null = null
private lastStreamState: WorkspaceConnectionState = "idle"
private commandTimeoutTimer: ReturnType<typeof setTimeout> | null = null
private lastBootRefreshAt = 0
private visibilityHandler: (() => void) | null = null
subscribe = (listener: () => void): (() => void) => {
this.listeners.add(listener)
return () => {
this.listeners.delete(listener)
}
}
getSnapshot = (): WorkspaceStoreState => this.state
start = (): void => {
if (this.started || this.disposed) return
this.started = true
if (typeof document !== "undefined") {
this.visibilityHandler = () => {
if (document.visibilityState === "visible" && Date.now() - this.lastBootRefreshAt >= VISIBILITY_REFRESH_THRESHOLD_MS) {
void this.refreshBoot({ soft: true })
}
}
document.addEventListener("visibilitychange", this.visibilityHandler)
}
void this.refreshBoot()
}
dispose = (): void => {
this.disposed = true
this.started = false
this.stopOnboardingPoller()
this.closeEventStream()
this.clearCommandTimeout()
if (this.visibilityHandler && typeof document !== "undefined") {
document.removeEventListener("visibilitychange", this.visibilityHandler)
this.visibilityHandler = null
}
}
disconnectSSE = (): void => {
this.closeEventStream()
}
reconnectSSE = (): void => {
if (this.disposed) return
this.ensureEventStream()
void this.refreshBoot({ soft: true })
}
clearTerminalLines = (): void => {
const replacement = this.state.boot ? bootSeedLines(this.state.boot) : [createTerminalLine("system", "Terminal cleared")]
this.patchState({ terminalLines: replacement })
}
consumeEditorTextBuffer = (): string | null => {
const next = this.state.editorTextBuffer
if (next !== null) {
this.patchState({ editorTextBuffer: null })
}
return next
}
openCommandSurface = (
surface: BrowserSlashCommandSurface,
options: { source?: "slash" | "sidebar" | "surface"; args?: string; selectedTarget?: CommandSurfaceTarget | null } = {},
): void => {
const resumableSessions = getLiveResumableSessions(this.state)
this.patchState({
commandSurface: openCommandSurfaceState(this.state.commandSurface, {
surface,
source: options.source ?? "surface",
args: options.args ?? "",
selectedTarget: options.selectedTarget,
onboardingLocked: this.state.boot?.onboarding.locked,
currentModel: getCurrentModelSelection(this.state.boot?.bridge),
currentThinkingLevel: this.state.boot?.bridge.sessionState?.thinkingLevel ?? null,
preferredProviderId: getPreferredOnboardingProviderId(this.state.boot?.onboarding),
resumableSessions: resumableSessions.map((session) => ({
id: session.id,
path: session.path,
name: session.name,
isActive: session.isActive,
})),
currentSessionPath: this.state.boot?.bridge.activeSessionFile ?? this.state.boot?.bridge.sessionState?.sessionFile ?? null,
currentSessionName: this.state.boot?.bridge.sessionState?.sessionName ?? null,
projectCwd: this.state.boot?.project.cwd ?? null,
projectSessionsDir: this.state.boot?.project.sessionsDir ?? null,
}),
})
}
closeCommandSurface = (): void => {
this.patchState({
commandSurface: closeCommandSurfaceState(this.state.commandSurface),
})
}
setCommandSurfaceSection = (section: CommandSurfaceSection): void => {
const resumableSessions = getLiveResumableSessions(this.state)
this.patchState({
commandSurface: setCommandSurfaceSection(this.state.commandSurface, section, {
onboardingLocked: this.state.boot?.onboarding.locked,
currentModel: getCurrentModelSelection(this.state.boot?.bridge),
currentThinkingLevel: this.state.boot?.bridge.sessionState?.thinkingLevel ?? null,
preferredProviderId: getPreferredOnboardingProviderId(this.state.boot?.onboarding),
resumableSessions: resumableSessions.map((session) => ({
id: session.id,
path: session.path,
name: session.name,
isActive: session.isActive,
})),
currentSessionPath: this.state.boot?.bridge.activeSessionFile ?? this.state.boot?.bridge.sessionState?.sessionFile ?? null,
currentSessionName: this.state.boot?.bridge.sessionState?.sessionName ?? null,
projectCwd: this.state.boot?.project.cwd ?? null,
projectSessionsDir: this.state.boot?.project.sessionsDir ?? null,
}),
})
}
selectCommandSurfaceTarget = (target: CommandSurfaceTarget): void => {
this.patchState({
commandSurface: selectCommandSurfaceStateTarget(this.state.commandSurface, target),
})
}
loadGitSummary = async (): Promise<GitSummaryResponse | null> => {
const requestedGitSummary: CommandSurfaceGitSummaryState = {
...this.state.commandSurface.gitSummary,
pending: true,
error: null,
}
const requestedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
gitSummary: withFreshnessRequested(this.state.live.freshness.gitSummary),
},
}
this.patchState({
live: {
...requestedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: requestedLive }),
},
commandSurface: setCommandSurfacePending(
{
...this.state.commandSurface,
gitSummary: requestedGitSummary,
},
"load_git_summary",
),
})
try {
const response = await authFetch(this.buildUrl("/api/git"), {
method: "GET",
cache: "no-store",
headers: {
Accept: "application/json",
},
})
const payload = await response.json().catch(() => null)
const normalizedGitSummary = normalizeGitSummaryPayload(payload)
if (!response.ok || !normalizedGitSummary) {
const message =
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
? payload.error
: `Current-project git summary failed with ${response.status}`
const failedGitSummary = normalizeGitSummaryError(requestedGitSummary, message)
const failedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
gitSummary: withFreshnessFailed(this.state.live.freshness.gitSummary, message),
},
}
this.patchState({
live: {
...failedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: failedLive }),
},
commandSurface: applyCommandSurfaceActionResult(
{
...this.state.commandSurface,
gitSummary: failedGitSummary,
},
{
action: "load_git_summary",
success: false,
message,
gitSummary: failedGitSummary,
},
),
})
return null
}
const gitSummary: CommandSurfaceGitSummaryState = {
pending: false,
loaded: true,
result: normalizedGitSummary,
error: null,
}
const nextLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
gitSummary: withFreshnessSucceeded(this.state.live.freshness.gitSummary),
},
}
this.patchState({
live: {
...nextLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: nextLive }),
},
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "load_git_summary",
success: true,
message: "",
gitSummary,
}),
})
return normalizedGitSummary
} catch (error) {
const message = normalizeClientError(error)
const failedGitSummary = normalizeGitSummaryError(requestedGitSummary, message)
const failedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
gitSummary: withFreshnessFailed(this.state.live.freshness.gitSummary, message),
},
}
this.patchState({
live: {
...failedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: failedLive }),
},
commandSurface: applyCommandSurfaceActionResult(
{
...this.state.commandSurface,
gitSummary: failedGitSummary,
},
{
action: "load_git_summary",
success: false,
message,
gitSummary: failedGitSummary,
},
),
})
return null
}
}
loadRecoveryDiagnostics = async (): Promise<WorkspaceRecoveryDiagnostics | null> => {
const requestedRecovery = markRecoveryStatePending(this.state.commandSurface.recovery)
const requestedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
recovery: withFreshnessRequested(this.state.live.freshness.recovery),
},
}
this.patchState({
live: {
...requestedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: requestedLive }),
},
commandSurface: setCommandSurfacePending(
{
...this.state.commandSurface,
recovery: requestedRecovery,
},
"load_recovery_diagnostics",
),
})
try {
const response = await authFetch(this.buildUrl("/api/recovery"), {
method: "GET",
cache: "no-store",
headers: {
Accept: "application/json",
},
})
const payload = await response.json().catch(() => null)
const diagnostics = normalizeRecoveryDiagnosticsPayload(payload)
if (!response.ok || !diagnostics) {
const message =
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
? payload.error
: `Recovery diagnostics failed with ${response.status}`
const failedRecovery = markRecoveryStateFailure(requestedRecovery, message)
const failedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
recovery: withFreshnessFailed(this.state.live.freshness.recovery, message),
},
}
this.patchState({
lastClientError: message,
live: {
...failedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: failedLive }),
},
commandSurface: applyCommandSurfaceActionResult(
{
...this.state.commandSurface,
recovery: failedRecovery,
},
{
action: "load_recovery_diagnostics",
success: false,
message,
recovery: failedRecovery,
},
),
})
return null
}
const recovery = {
...createRecoveryStateFromDiagnostics(diagnostics),
lastInvalidatedAt: this.state.commandSurface.recovery.lastInvalidatedAt,
}
const nextLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
recovery: withFreshnessSucceeded(this.state.live.freshness.recovery),
},
}
this.patchState({
lastClientError: null,
live: {
...nextLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: nextLive }),
},
commandSurface: applyCommandSurfaceActionResult(
{
...this.state.commandSurface,
recovery,
},
{
action: "load_recovery_diagnostics",
success: true,
message:
diagnostics.status === "ready"
? "Recovery diagnostics refreshed"
: "Recovery diagnostics are currently unavailable",
recovery,
},
),
})
return diagnostics
} catch (error) {
const message = normalizeClientError(error)
const failedRecovery = markRecoveryStateFailure(requestedRecovery, message)
const failedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
recovery: withFreshnessFailed(this.state.live.freshness.recovery, message),
},
}
this.patchState({
lastClientError: message,
live: {
...failedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: failedLive }),
},
commandSurface: applyCommandSurfaceActionResult(
{
...this.state.commandSurface,
recovery: failedRecovery,
},
{
action: "load_recovery_diagnostics",
success: false,
message,
recovery: failedRecovery,
},
),
})
return null
}
}
// ─── Diagnostics panel fetch methods ────────────────────────────────────────
private patchDiagnosticsPhaseState<K extends "forensics" | "skillHealth">(
key: K,
patch: Partial<CommandSurfaceDiagnosticsPhaseState<K extends "forensics" ? ForensicReport : SkillHealthReport>>,
): void {
this.patchState({
commandSurface: {
...this.state.commandSurface,
diagnostics: {
...this.state.commandSurface.diagnostics,
[key]: { ...this.state.commandSurface.diagnostics[key], ...patch },
},
},
})
}
private patchDoctorState(patch: Partial<CommandSurfaceDoctorState>): void {
this.patchState({
commandSurface: {
...this.state.commandSurface,
diagnostics: {
...this.state.commandSurface.diagnostics,
doctor: { ...this.state.commandSurface.diagnostics.doctor, ...patch },
},
},
})
}
private patchKnowledgeCapturesState(patch: Partial<CommandSurfaceKnowledgeCapturesState>): void {
this.patchState({
commandSurface: {
...this.state.commandSurface,
knowledgeCaptures: { ...this.state.commandSurface.knowledgeCaptures, ...patch },
},
})
}
private patchKnowledgeCapturesPhaseState<K extends "knowledge" | "captures">(
key: K,
patch: Partial<CommandSurfaceDiagnosticsPhaseState<K extends "knowledge" ? KnowledgeData : CapturesData>>,
): void {
this.patchState({
commandSurface: {
...this.state.commandSurface,
knowledgeCaptures: {
...this.state.commandSurface.knowledgeCaptures,
[key]: { ...this.state.commandSurface.knowledgeCaptures[key], ...patch },
},
},
})
}
private patchSettingsPhaseState(patch: Partial<CommandSurfaceDiagnosticsPhaseState<SettingsData>>): void {
this.patchState({
commandSurface: {
...this.state.commandSurface,
settingsData: { ...this.state.commandSurface.settingsData, ...patch },
},
})
}
private patchRemainingCommandsPhaseState<
K extends keyof import("./command-surface-contract").CommandSurfaceRemainingState,
>(
key: K,
patch: Partial<CommandSurfaceDiagnosticsPhaseState<import("./command-surface-contract").CommandSurfaceRemainingState[K] extends CommandSurfaceDiagnosticsPhaseState<infer T> ? T : never>>,
): void {
this.patchState({
commandSurface: {
...this.state.commandSurface,
remainingCommands: {
...this.state.commandSurface.remainingCommands,
[key]: { ...this.state.commandSurface.remainingCommands[key], ...patch },
},
},
})
}
loadForensicsDiagnostics = async (): Promise<ForensicReport | null> => {
this.patchDiagnosticsPhaseState("forensics", { phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/forensics"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Forensics request failed with ${response.status}`
this.patchDiagnosticsPhaseState("forensics", { phase: "error", error: message })
return null
}
this.patchDiagnosticsPhaseState("forensics", { phase: "loaded", data: payload as ForensicReport, lastLoadedAt: new Date().toISOString() })
return payload as ForensicReport
} catch (error) {
const message = normalizeClientError(error)
this.patchDiagnosticsPhaseState("forensics", { phase: "error", error: message })
return null
}
}
loadDoctorDiagnostics = async (scope?: string): Promise<DoctorReport | null> => {
this.patchDoctorState({ phase: "loading", error: null })
try {
const url = scope ? `/api/doctor?scope=${encodeURIComponent(scope)}` : "/api/doctor"
const response = await authFetch(url, { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Doctor request failed with ${response.status}`
this.patchDoctorState({ phase: "error", error: message })
return null
}
this.patchDoctorState({ phase: "loaded", data: payload as DoctorReport, lastLoadedAt: new Date().toISOString() })
return payload as DoctorReport
} catch (error) {
const message = normalizeClientError(error)
this.patchDoctorState({ phase: "error", error: message })
return null
}
}
applyDoctorFixes = async (scope?: string): Promise<DoctorFixResult | null> => {
this.patchDoctorState({ fixPending: true, lastFixError: null, lastFixResult: null })
try {
const response = await authFetch(this.buildUrl("/api/doctor"), {
method: "POST",
cache: "no-store",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(scope ? { scope } : {}),
})
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Doctor fix request failed with ${response.status}`
this.patchDoctorState({ fixPending: false, lastFixError: message })
return null
}
const fixResult = payload as DoctorFixResult
this.patchDoctorState({ fixPending: false, lastFixResult: fixResult })
// Reload doctor data after applying fixes so the issue list refreshes
void this.loadDoctorDiagnostics(scope)
return fixResult
} catch (error) {
const message = normalizeClientError(error)
this.patchDoctorState({ fixPending: false, lastFixError: message })
return null
}
}
loadSkillHealthDiagnostics = async (): Promise<SkillHealthReport | null> => {
this.patchDiagnosticsPhaseState("skillHealth", { phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/skill-health"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Skill health request failed with ${response.status}`
this.patchDiagnosticsPhaseState("skillHealth", { phase: "error", error: message })
return null
}
this.patchDiagnosticsPhaseState("skillHealth", { phase: "loaded", data: payload as SkillHealthReport, lastLoadedAt: new Date().toISOString() })
return payload as SkillHealthReport
} catch (error) {
const message = normalizeClientError(error)
this.patchDiagnosticsPhaseState("skillHealth", { phase: "error", error: message })
return null
}
}
loadKnowledgeData = async (): Promise<KnowledgeData | null> => {
this.patchKnowledgeCapturesPhaseState("knowledge", { phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/knowledge"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Knowledge request failed with ${response.status}`
this.patchKnowledgeCapturesPhaseState("knowledge", { phase: "error", error: message })
return null
}
this.patchKnowledgeCapturesPhaseState("knowledge", { phase: "loaded", data: payload as KnowledgeData, lastLoadedAt: new Date().toISOString() })
return payload as KnowledgeData
} catch (error) {
const message = normalizeClientError(error)
this.patchKnowledgeCapturesPhaseState("knowledge", { phase: "error", error: message })
return null
}
}
loadCapturesData = async (): Promise<CapturesData | null> => {
this.patchKnowledgeCapturesPhaseState("captures", { phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/captures"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Captures request failed with ${response.status}`
this.patchKnowledgeCapturesPhaseState("captures", { phase: "error", error: message })
return null
}
this.patchKnowledgeCapturesPhaseState("captures", { phase: "loaded", data: payload as CapturesData, lastLoadedAt: new Date().toISOString() })
return payload as CapturesData
} catch (error) {
const message = normalizeClientError(error)
this.patchKnowledgeCapturesPhaseState("captures", { phase: "error", error: message })
return null
}
}
loadSettingsData = async (): Promise<SettingsData | null> => {
this.patchSettingsPhaseState({ phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/settings-data"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Settings request failed with ${response.status}`
this.patchSettingsPhaseState({ phase: "error", error: message })
return null
}
this.patchSettingsPhaseState({ phase: "loaded", data: payload as SettingsData, lastLoadedAt: new Date().toISOString() })
return payload as SettingsData
} catch (error) {
const message = normalizeClientError(error)
this.patchSettingsPhaseState({ phase: "error", error: message })
return null
}
}
// ─── Remaining command surface load/mutation methods ──────────────────────────
loadHistoryData = async (): Promise<HistoryData | null> => {
this.patchRemainingCommandsPhaseState("history", { phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/history"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `History request failed with ${response.status}`
this.patchRemainingCommandsPhaseState("history", { phase: "error", error: message })
return null
}
this.patchRemainingCommandsPhaseState("history", { phase: "loaded", data: payload as HistoryData, lastLoadedAt: new Date().toISOString() })
return payload as HistoryData
} catch (error) {
const message = normalizeClientError(error)
this.patchRemainingCommandsPhaseState("history", { phase: "error", error: message })
return null
}
}
loadInspectData = async (): Promise<InspectData | null> => {
this.patchRemainingCommandsPhaseState("inspect", { phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/inspect"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Inspect request failed with ${response.status}`
this.patchRemainingCommandsPhaseState("inspect", { phase: "error", error: message })
return null
}
this.patchRemainingCommandsPhaseState("inspect", { phase: "loaded", data: payload as InspectData, lastLoadedAt: new Date().toISOString() })
return payload as InspectData
} catch (error) {
const message = normalizeClientError(error)
this.patchRemainingCommandsPhaseState("inspect", { phase: "error", error: message })
return null
}
}
loadHooksData = async (): Promise<HooksData | null> => {
this.patchRemainingCommandsPhaseState("hooks", { phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/hooks"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Hooks request failed with ${response.status}`
this.patchRemainingCommandsPhaseState("hooks", { phase: "error", error: message })
return null
}
this.patchRemainingCommandsPhaseState("hooks", { phase: "loaded", data: payload as HooksData, lastLoadedAt: new Date().toISOString() })
return payload as HooksData
} catch (error) {
const message = normalizeClientError(error)
this.patchRemainingCommandsPhaseState("hooks", { phase: "error", error: message })
return null
}
}
loadExportData = async (format?: "markdown" | "json"): Promise<ExportResult | null> => {
this.patchRemainingCommandsPhaseState("exportData", { phase: "loading", error: null })
try {
const url = format ? `/api/export-data?format=${encodeURIComponent(format)}` : "/api/export-data"
const response = await authFetch(url, { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Export request failed with ${response.status}`
this.patchRemainingCommandsPhaseState("exportData", { phase: "error", error: message })
return null
}
this.patchRemainingCommandsPhaseState("exportData", { phase: "loaded", data: payload as ExportResult, lastLoadedAt: new Date().toISOString() })
return payload as ExportResult
} catch (error) {
const message = normalizeClientError(error)
this.patchRemainingCommandsPhaseState("exportData", { phase: "error", error: message })
return null
}
}
loadUndoInfo = async (): Promise<UndoInfo | null> => {
this.patchRemainingCommandsPhaseState("undo", { phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/undo"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Undo info request failed with ${response.status}`
this.patchRemainingCommandsPhaseState("undo", { phase: "error", error: message })
return null
}
this.patchRemainingCommandsPhaseState("undo", { phase: "loaded", data: payload as UndoInfo, lastLoadedAt: new Date().toISOString() })
return payload as UndoInfo
} catch (error) {
const message = normalizeClientError(error)
this.patchRemainingCommandsPhaseState("undo", { phase: "error", error: message })
return null
}
}
loadCleanupData = async (): Promise<CleanupData | null> => {
this.patchRemainingCommandsPhaseState("cleanup", { phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/cleanup"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Cleanup data request failed with ${response.status}`
this.patchRemainingCommandsPhaseState("cleanup", { phase: "error", error: message })
return null
}
this.patchRemainingCommandsPhaseState("cleanup", { phase: "loaded", data: payload as CleanupData, lastLoadedAt: new Date().toISOString() })
return payload as CleanupData
} catch (error) {
const message = normalizeClientError(error)
this.patchRemainingCommandsPhaseState("cleanup", { phase: "error", error: message })
return null
}
}
loadSteerData = async (): Promise<SteerData | null> => {
this.patchRemainingCommandsPhaseState("steer", { phase: "loading", error: null })
try {
const response = await authFetch(this.buildUrl("/api/steer"), { method: "GET", cache: "no-store", headers: { Accept: "application/json" } })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Steer data request failed with ${response.status}`
this.patchRemainingCommandsPhaseState("steer", { phase: "error", error: message })
return null
}
this.patchRemainingCommandsPhaseState("steer", { phase: "loaded", data: payload as SteerData, lastLoadedAt: new Date().toISOString() })
return payload as SteerData
} catch (error) {
const message = normalizeClientError(error)
this.patchRemainingCommandsPhaseState("steer", { phase: "error", error: message })
return null
}
}
executeUndoAction = async (): Promise<UndoResult | null> => {
try {
const response = await authFetch(this.buildUrl("/api/undo"), {
method: "POST",
cache: "no-store",
headers: { Accept: "application/json" },
})
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Undo action failed with ${response.status}`
return { success: false, message }
}
// Reload undo info after executing
void this.loadUndoInfo()
return payload as UndoResult
} catch (error) {
const message = normalizeClientError(error)
return { success: false, message }
}
}
executeCleanupAction = async (branches: string[], snapshots: string[]): Promise<CleanupResult | null> => {
try {
const response = await authFetch(this.buildUrl("/api/cleanup"), {
method: "POST",
cache: "no-store",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ branches, snapshots }),
})
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Cleanup action failed with ${response.status}`
return { deletedBranches: 0, prunedSnapshots: 0, message }
}
// Reload cleanup data after executing
void this.loadCleanupData()
return payload as CleanupResult
} catch (error) {
const message = normalizeClientError(error)
return { deletedBranches: 0, prunedSnapshots: 0, message }
}
}
resolveCaptureAction = async (request: CaptureResolveRequest): Promise<CaptureResolveResult | null> => {
this.patchKnowledgeCapturesState({ resolveRequest: { pending: true, lastError: null, lastResult: null } })
try {
const response = await authFetch(this.buildUrl("/api/captures"), {
method: "POST",
cache: "no-store",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(request),
})
const payload = await response.json().catch(() => null)
if (!response.ok || !payload) {
const message = payload?.error ?? `Capture resolve failed with ${response.status}`
this.patchKnowledgeCapturesState({ resolveRequest: { pending: false, lastError: message, lastResult: null } })
return null
}
const result = payload as CaptureResolveResult
this.patchKnowledgeCapturesState({ resolveRequest: { pending: false, lastError: null, lastResult: result } })
// Auto-reload captures after successful resolve
void this.loadCapturesData()
return result
} catch (error) {
const message = normalizeClientError(error)
this.patchKnowledgeCapturesState({ resolveRequest: { pending: false, lastError: message, lastResult: null } })
return null
}
}
updateSessionBrowserState = (
patch: Partial<Pick<CommandSurfaceSessionBrowserState, "query" | "sortMode" | "nameFilter">>,
): void => {
this.patchState({
commandSurface: {
...this.state.commandSurface,
sessionBrowser: {
...this.state.commandSurface.sessionBrowser,
...patch,
error: null,
},
lastError: null,
lastResult: null,
},
})
}
loadSessionBrowser = async (
overrides: Partial<Pick<CommandSurfaceSessionBrowserState, "query" | "sortMode" | "nameFilter">> = {},
): Promise<CommandSurfaceSessionBrowserState | null> => {
const requestedSessionBrowser = {
...this.state.commandSurface.sessionBrowser,
...overrides,
error: null,
}
const requestedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
sessionBrowser: withFreshnessRequested(this.state.live.freshness.sessionBrowser),
},
}
this.patchState({
live: {
...requestedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: requestedLive }),
},
commandSurface: setCommandSurfacePending(
{
...this.state.commandSurface,
sessionBrowser: requestedSessionBrowser,
},
"load_session_browser",
),
})
const params = new URLSearchParams()
if (requestedSessionBrowser.query.trim()) {
params.set("query", requestedSessionBrowser.query.trim())
}
params.set("sortMode", requestedSessionBrowser.sortMode)
params.set("nameFilter", requestedSessionBrowser.nameFilter)
try {
const response = await authFetch(this.buildUrl(`/api/session/browser?${params.toString()}`), {
method: "GET",
cache: "no-store",
headers: {
Accept: "application/json",
},
})
const payload = await response.json().catch(() => null)
const normalizedSessionBrowser = normalizeSessionBrowserPayload(payload)
if (!response.ok || !normalizedSessionBrowser) {
const message =
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
? payload.error
: `Current-project session browser failed with ${response.status}`
const failedSessionBrowser = {
...requestedSessionBrowser,
error: message,
}
const failedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
sessionBrowser: withFreshnessFailed(this.state.live.freshness.sessionBrowser, message),
},
}
this.patchState({
live: {
...failedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: failedLive }),
},
commandSurface: applyCommandSurfaceActionResult(
{
...this.state.commandSurface,
sessionBrowser: failedSessionBrowser,
},
{
action: "load_session_browser",
success: false,
message,
sessionBrowser: failedSessionBrowser,
},
),
})
return null
}
const sessionBrowser = syncSessionBrowserStateWithBridge(normalizedSessionBrowser, this.state.boot)
const currentTarget = this.state.commandSurface.selectedTarget
const defaultResumePath = sessionBrowser.sessions.find((session) => !session.isActive)?.path ?? sessionBrowser.sessions[0]?.path
const defaultRenameSession =
sessionBrowser.sessions.find((session) => session.path === sessionBrowser.activeSessionPath) ?? sessionBrowser.sessions[0]
let selectedTarget = currentTarget
if (currentTarget?.kind === "resume" || this.state.commandSurface.section === "resume") {
const visiblePath =
currentTarget?.kind === "resume" && currentTarget.sessionPath && sessionBrowser.sessions.some((session) => session.path === currentTarget.sessionPath)
? currentTarget.sessionPath
: defaultResumePath
selectedTarget = { kind: "resume", sessionPath: visiblePath }
} else if (currentTarget?.kind === "name" || this.state.commandSurface.section === "name") {
const visibleSession =
currentTarget?.kind === "name" && currentTarget.sessionPath
? sessionBrowser.sessions.find((session) => session.path === currentTarget.sessionPath) ?? defaultRenameSession
: defaultRenameSession
selectedTarget = {
kind: "name",
sessionPath: visibleSession?.path,
name:
currentTarget?.kind === "name" && currentTarget.sessionPath === visibleSession?.path
? currentTarget.name
: visibleSession?.name ?? "",
}
}
const nextLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
sessionBrowser: withFreshnessSucceeded(this.state.live.freshness.sessionBrowser),
},
}
this.patchState({
live: {
...nextLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: nextLive }),
},
commandSurface: applyCommandSurfaceActionResult(
{
...this.state.commandSurface,
sessionBrowser,
},
{
action: "load_session_browser",
success: true,
message: "",
selectedTarget,
sessionBrowser,
},
),
})
return sessionBrowser
} catch (error) {
const message = normalizeClientError(error)
const failedSessionBrowser = {
...requestedSessionBrowser,
error: message,
}
const failedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
sessionBrowser: withFreshnessFailed(this.state.live.freshness.sessionBrowser, message),
},
}
this.patchState({
live: {
...failedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: failedLive }),
},
commandSurface: applyCommandSurfaceActionResult(
{
...this.state.commandSurface,
sessionBrowser: failedSessionBrowser,
},
{
action: "load_session_browser",
success: false,
message,
sessionBrowser: failedSessionBrowser,
},
),
})
return null
}
}
renameSessionFromSurface = async (sessionPath: string, name?: string): Promise<SessionManageResponse | null> => {
const currentTarget = this.state.commandSurface.selectedTarget
const requestedName = name ?? (currentTarget?.kind === "name" ? currentTarget.name : "")
const trimmedName = requestedName.trim()
const selectedTarget: CommandSurfaceTarget = { kind: "name", sessionPath, name: requestedName }
if (!trimmedName) {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "rename_session",
success: false,
message: "Session name cannot be empty",
selectedTarget,
}),
})
return null
}
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "rename_session", selectedTarget),
})
try {
const response = await authFetch(this.buildUrl("/api/session/manage"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
action: "rename",
sessionPath,
name: trimmedName,
}),
})
const payload = await response.json().catch(() => null)
if (!response.ok || !payload || typeof payload !== "object" || payload.success !== true) {
const message =
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
? payload.error
: `Session rename failed with ${response.status}`
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "rename_session",
success: false,
message,
selectedTarget,
}),
})
return null
}
const result = payload as SessionManageResponse & { success: true }
const nextBoot = patchBootSessionName(this.state.boot, result.sessionPath, result.name)
const nextSessionBrowser = syncSessionBrowserStateWithBridge(
patchSessionBrowserSession(this.state.commandSurface.sessionBrowser, result.sessionPath, {
name: result.name,
...(result.isActiveSession ? { isActive: true } : {}),
}),
nextBoot,
)
const nextSelectedTarget: CommandSurfaceTarget = {
kind: "name",
sessionPath: result.sessionPath,
name: result.name,
}
const nextLiveBase: WorkspaceLiveState = {
...this.state.live,
resumableSessions: overlayLiveBridgeSessionState(
getLiveResumableSessions(this.state).map((session) =>
session.path === result.sessionPath
? {
...session,
name: result.name,
}
: session,
),
nextBoot,
),
}
this.patchState({
...(nextBoot ? { boot: nextBoot } : {}),
live: {
...nextLiveBase,
recoverySummary: createWorkspaceRecoverySummary({ boot: nextBoot, live: nextLiveBase }),
},
commandSurface: applyCommandSurfaceActionResult(
{
...this.state.commandSurface,
sessionBrowser: nextSessionBrowser,
},
{
action: "rename_session",
success: true,
message: `Session name set: ${result.name}`,
selectedTarget: nextSelectedTarget,
sessionBrowser: nextSessionBrowser,
},
),
})
return result
} catch (error) {
const message = normalizeClientError(error)
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "rename_session",
success: false,
message,
selectedTarget,
}),
})
return null
}
}
loadAvailableModels = async (): Promise<CommandSurfaceModelOption[]> => {
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "loading_models"),
})
const response = await this.sendCommand(
{ type: "get_available_models" },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "loading_models",
success: false,
message: `Couldn't load models — ${message}`,
}),
})
return []
}
const availableModels = normalizeAvailableModels(response.data, getCurrentModelSelection(this.state.boot?.bridge))
const currentTarget = this.state.commandSurface.selectedTarget
const selectedTarget =
currentTarget?.kind === "model"
? currentTarget
: availableModels[0]
? { kind: "model" as const, provider: availableModels[0].provider, modelId: availableModels[0].modelId }
: currentTarget
this.patchState({
commandSurface: {
...this.state.commandSurface,
pendingAction: null,
lastError: null,
availableModels,
selectedTarget: selectedTarget ?? null,
},
})
return availableModels
}
applyModelSelection = async (provider: string, modelId: string): Promise<WorkspaceCommandResponse | null> => {
const selectedTarget: CommandSurfaceTarget = { kind: "model", provider, modelId }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "set_model", selectedTarget),
})
const response = await this.sendCommand(
{ type: "set_model", provider, modelId },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_model",
success: false,
message,
selectedTarget,
}),
})
return response
}
const nextBridge = this.state.boot?.bridge.sessionState
? {
...this.state.boot.bridge,
sessionState: {
...this.state.boot.bridge.sessionState,
model: response.data as WorkspaceModelRef,
},
}
: null
const nextAvailableModels = this.state.commandSurface.availableModels.map((model) => ({
...model,
isCurrent: model.provider === provider && model.modelId === modelId,
}))
this.patchState({
...(nextBridge && this.state.boot ? { boot: cloneBootWithBridge(this.state.boot, nextBridge) } : {}),
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_model",
success: true,
message: `Model set to ${provider}/${modelId}`,
selectedTarget,
availableModels: nextAvailableModels,
}),
})
return response
}
applyThinkingLevel = async (level: CommandSurfaceThinkingLevel): Promise<WorkspaceCommandResponse | null> => {
const selectedTarget: CommandSurfaceTarget = { kind: "thinking", level }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "set_thinking_level", selectedTarget),
})
const response = await this.sendCommand(
{ type: "set_thinking_level", level },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_thinking_level",
success: false,
message,
selectedTarget,
}),
})
return response
}
const nextBridge = this.state.boot?.bridge.sessionState
? {
...this.state.boot.bridge,
sessionState: {
...this.state.boot.bridge.sessionState,
thinkingLevel: level,
},
}
: null
this.patchState({
...(nextBridge && this.state.boot ? { boot: cloneBootWithBridge(this.state.boot, nextBridge) } : {}),
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_thinking_level",
success: true,
message: `Thinking level set to ${level}`,
selectedTarget,
}),
})
return response
}
setSteeringModeFromSurface = async (
mode: WorkspaceSessionState["steeringMode"],
): Promise<WorkspaceCommandResponse | null> => {
const selectedTarget = this.state.commandSurface.selectedTarget
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "set_steering_mode", selectedTarget),
})
const response = await this.sendCommand(
{ type: "set_steering_mode", mode },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_steering_mode",
success: false,
message,
selectedTarget,
}),
})
return response
}
const nextBoot = patchBootSessionState(this.state.boot, { steeringMode: mode })
this.patchState({
...(nextBoot ? { boot: nextBoot } : {}),
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_steering_mode",
success: true,
message: `Steering mode set to ${mode}`,
selectedTarget,
}),
})
return response
}
setFollowUpModeFromSurface = async (
mode: WorkspaceSessionState["followUpMode"],
): Promise<WorkspaceCommandResponse | null> => {
const selectedTarget = this.state.commandSurface.selectedTarget
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "set_follow_up_mode", selectedTarget),
})
const response = await this.sendCommand(
{ type: "set_follow_up_mode", mode },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_follow_up_mode",
success: false,
message,
selectedTarget,
}),
})
return response
}
const nextBoot = patchBootSessionState(this.state.boot, { followUpMode: mode })
this.patchState({
...(nextBoot ? { boot: nextBoot } : {}),
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_follow_up_mode",
success: true,
message: `Follow-up mode set to ${mode}`,
selectedTarget,
}),
})
return response
}
setAutoCompactionFromSurface = async (enabled: boolean): Promise<WorkspaceCommandResponse | null> => {
const selectedTarget = this.state.commandSurface.selectedTarget
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "set_auto_compaction", selectedTarget),
})
const response = await this.sendCommand(
{ type: "set_auto_compaction", enabled },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_auto_compaction",
success: false,
message,
selectedTarget,
}),
})
return response
}
const nextBoot = patchBootSessionState(this.state.boot, { autoCompactionEnabled: enabled })
this.patchState({
...(nextBoot ? { boot: nextBoot } : {}),
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_auto_compaction",
success: true,
message: `Auto-compaction ${enabled ? "enabled" : "disabled"}`,
selectedTarget,
}),
})
return response
}
setAutoRetryFromSurface = async (enabled: boolean): Promise<WorkspaceCommandResponse | null> => {
const selectedTarget = this.state.commandSurface.selectedTarget
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "set_auto_retry", selectedTarget),
})
const response = await this.sendCommand(
{ type: "set_auto_retry", enabled },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_auto_retry",
success: false,
message,
selectedTarget,
}),
})
return response
}
const nextBoot = patchBootSessionState(this.state.boot, { autoRetryEnabled: enabled })
this.patchState({
...(nextBoot ? { boot: nextBoot } : {}),
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "set_auto_retry",
success: true,
message: `Auto-retry ${enabled ? "enabled" : "disabled"}`,
selectedTarget,
}),
})
return response
}
abortRetryFromSurface = async (): Promise<WorkspaceCommandResponse | null> => {
const selectedTarget = this.state.commandSurface.selectedTarget
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "abort_retry", selectedTarget),
})
const response = await this.sendCommand(
{ type: "abort_retry" },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "abort_retry",
success: false,
message,
selectedTarget,
}),
})
return response
}
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "abort_retry",
success: true,
message: "Retry cancellation requested. Live retry state will update when the bridge confirms the abort.",
selectedTarget,
}),
})
return response
}
switchSessionFromSurface = async (sessionPath: string): Promise<WorkspaceCommandResponse | null> => {
const selectedTarget: CommandSurfaceTarget = { kind: "resume", sessionPath }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "switch_session", selectedTarget),
})
const response = await this.sendCommand(
{ type: "switch_session", sessionPath },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "switch_session",
success: false,
message,
selectedTarget,
}),
})
return response
}
if (response.data && typeof response.data === "object" && "cancelled" in response.data && response.data.cancelled) {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "switch_session",
success: false,
message: "Session switch was cancelled before the browser changed sessions.",
selectedTarget,
}),
})
return response
}
const nextSessionName =
this.state.commandSurface.sessionBrowser.sessions.find((session) => session.path === sessionPath)?.name ??
this.state.boot?.resumableSessions.find((session) => session.path === sessionPath)?.name
const nextBoot = patchBootActiveSession(this.state.boot, sessionPath, nextSessionName)
const nextSessionBrowser = syncSessionBrowserStateWithBridge(
patchSessionBrowserSession(this.state.commandSurface.sessionBrowser, sessionPath, {
isActive: true,
...(nextSessionName ? { name: nextSessionName } : {}),
}),
nextBoot,
)
const nextLiveBase: WorkspaceLiveState = {
...this.state.live,
resumableSessions: overlayLiveBridgeSessionState(
getLiveResumableSessions(this.state).map((session) => ({
...session,
isActive: session.path === sessionPath,
...(session.path === sessionPath && nextSessionName ? { name: nextSessionName } : {}),
})),
nextBoot,
),
}
this.patchState({
...(nextBoot ? { boot: nextBoot } : {}),
live: {
...nextLiveBase,
recoverySummary: createWorkspaceRecoverySummary({ boot: nextBoot, live: nextLiveBase }),
},
commandSurface: applyCommandSurfaceActionResult(
{
...this.state.commandSurface,
sessionBrowser: nextSessionBrowser,
},
{
action: "switch_session",
success: true,
message: `Switched to ${describeSessionPath(sessionPath, nextBoot ?? this.state.boot)}`,
selectedTarget,
sessionBrowser: nextSessionBrowser,
},
),
})
return response
}
loadSessionStats = async (): Promise<CommandSurfaceSessionStats | null> => {
const requestedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
sessionStats: withFreshnessRequested(this.state.live.freshness.sessionStats),
},
}
this.patchState({
live: {
...requestedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: requestedLive }),
},
commandSurface: setCommandSurfacePending(this.state.commandSurface, "load_session_stats"),
})
const response = await this.sendCommand(
{ type: "get_session_stats" },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
const failedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
sessionStats: withFreshnessFailed(this.state.live.freshness.sessionStats, message),
},
}
this.patchState({
live: {
...failedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: failedLive }),
},
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "load_session_stats",
success: false,
message: `Couldn't load session details — ${message}`,
sessionStats: null,
}),
})
return null
}
const sessionStats = normalizeSessionStats(response.data)
if (!sessionStats) {
const message = "Session details response was missing the expected fields."
const failedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
sessionStats: withFreshnessFailed(this.state.live.freshness.sessionStats, message),
},
}
this.patchState({
live: {
...failedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: failedLive }),
},
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "load_session_stats",
success: false,
message,
sessionStats: null,
}),
})
return null
}
const nextLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
sessionStats: withFreshnessSucceeded(this.state.live.freshness.sessionStats),
},
}
this.patchState({
live: {
...nextLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: nextLive }),
},
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "load_session_stats",
success: true,
message: `Loaded session details for ${sessionStats.sessionId}`,
sessionStats,
}),
})
return sessionStats
}
exportSessionFromSurface = async (outputPath?: string): Promise<WorkspaceCommandResponse | null> => {
const normalizedOutputPath = outputPath?.trim() || undefined
const selectedTarget: CommandSurfaceTarget = { kind: "session", outputPath: normalizedOutputPath }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "export_html", selectedTarget),
})
const response = await this.sendCommand(
normalizedOutputPath ? { type: "export_html", outputPath: normalizedOutputPath } : { type: "export_html" },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "export_html",
success: false,
message: `Couldn't export this session — ${message}`,
selectedTarget,
}),
})
return response
}
const exportedPath =
response.data && typeof response.data === "object" && "path" in response.data && typeof response.data.path === "string"
? response.data.path
: "the generated file"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "export_html",
success: true,
message: `Session exported to ${exportedPath}`,
selectedTarget,
}),
})
return response
}
loadForkMessages = async (): Promise<CommandSurfaceForkMessage[]> => {
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "load_fork_messages"),
})
const response = await this.sendCommand(
{ type: "get_fork_messages" },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "load_fork_messages",
success: false,
message: `Couldn't load fork points — ${message}`,
forkMessages: [],
}),
})
return []
}
const forkMessages = normalizeForkMessages(response.data)
const currentTarget = this.state.commandSurface.selectedTarget
const selectedTarget =
currentTarget?.kind === "fork" && currentTarget.entryId
? currentTarget
: forkMessages[0]
? { kind: "fork" as const, entryId: forkMessages[0].entryId }
: currentTarget
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "load_fork_messages",
success: true,
message: forkMessages.length > 0 ? `Loaded ${forkMessages.length} fork points.` : "No fork points are available yet.",
selectedTarget: selectedTarget ?? null,
forkMessages,
}),
})
return forkMessages
}
forkSessionFromSurface = async (entryId: string): Promise<WorkspaceCommandResponse | null> => {
const selectedTarget: CommandSurfaceTarget = { kind: "fork", entryId }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "fork_session", selectedTarget),
})
const response = await this.sendCommand(
{ type: "fork", entryId },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "fork_session",
success: false,
message: `Couldn't create a fork — ${message}`,
selectedTarget,
}),
})
return response
}
if (response.data && typeof response.data === "object" && "cancelled" in response.data && response.data.cancelled) {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "fork_session",
success: false,
message: "Fork creation was cancelled before a new session was created.",
selectedTarget,
}),
})
return response
}
const sourceText =
response.data && typeof response.data === "object" && "text" in response.data && typeof response.data.text === "string"
? response.data.text.trim()
: ""
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "fork_session",
success: true,
message: sourceText ? `Forked from “${sourceText.slice(0, 120)}${sourceText.length > 120 ? "…" : ""}` : "Created a forked session.",
selectedTarget,
}),
})
return response
}
compactSessionFromSurface = async (customInstructions?: string): Promise<WorkspaceCommandResponse | null> => {
const normalizedInstructions = customInstructions?.trim() ?? ""
const selectedTarget: CommandSurfaceTarget = { kind: "compact", customInstructions: normalizedInstructions }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "compact_session", selectedTarget),
})
const response = await this.sendCommand(
normalizedInstructions ? { type: "compact", customInstructions: normalizedInstructions } : { type: "compact" },
{ appendInputLine: false, appendResponseLine: false },
)
if (!response || response.success === false) {
const message = response?.error ?? this.state.lastClientError ?? "Unknown error"
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "compact_session",
success: false,
message: `Couldn't compact the session — ${message}`,
selectedTarget,
lastCompaction: null,
}),
})
return response
}
const compactionResult = normalizeCompactionResult(response.data)
if (!compactionResult) {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "compact_session",
success: false,
message: "Compaction finished but the browser could not read the compaction result.",
selectedTarget,
lastCompaction: null,
}),
})
return response
}
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "compact_session",
success: true,
message: `Compacted ${compactionResult.tokensBefore.toLocaleString()} tokens into a fresh summary${normalizedInstructions ? " with custom instructions" : ""}.`,
selectedTarget,
lastCompaction: compactionResult,
}),
})
return response
}
saveApiKeyFromSurface = async (providerId: string, apiKey: string): Promise<WorkspaceOnboardingState | null> => {
const selectedTarget: CommandSurfaceTarget = { kind: "auth", providerId, intent: "manage" }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "save_api_key", selectedTarget),
})
const onboarding = await this.saveApiKey(providerId, apiKey)
const providerLabel = onboarding ? findOnboardingProviderLabel(onboarding, providerId) : providerId
if (!onboarding) {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "save_api_key",
success: false,
message: this.state.lastClientError ?? `${providerLabel} setup failed`,
selectedTarget,
}),
})
return null
}
if (onboarding.lastValidation?.status === "failed") {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "save_api_key",
success: false,
message: onboarding.lastValidation.message,
selectedTarget,
}),
})
return onboarding
}
if (onboarding.bridgeAuthRefresh.phase === "failed") {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "save_api_key",
success: false,
message: onboarding.bridgeAuthRefresh.error ?? `${providerLabel} credentials validated but bridge auth refresh failed`,
selectedTarget,
}),
})
return onboarding
}
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "save_api_key",
success: true,
message: `${providerLabel} credentials validated and saved.`,
selectedTarget,
}),
})
return onboarding
}
startProviderFlowFromSurface = async (providerId: string): Promise<WorkspaceOnboardingState | null> => {
const selectedTarget: CommandSurfaceTarget = { kind: "auth", providerId, intent: "login" }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "start_provider_flow", selectedTarget),
})
const onboarding = await this.startProviderFlow(providerId)
const providerLabel = onboarding ? findOnboardingProviderLabel(onboarding, providerId) : providerId
if (!onboarding) {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "start_provider_flow",
success: false,
message: this.state.lastClientError ?? `${providerLabel} sign-in failed to start`,
selectedTarget,
}),
})
return null
}
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "start_provider_flow",
success: true,
message: `${providerLabel} sign-in started. Continue in the auth section.`,
selectedTarget,
}),
})
return onboarding
}
submitProviderFlowInputFromSurface = async (flowId: string, input: string): Promise<WorkspaceOnboardingState | null> => {
const providerId = this.state.boot?.onboarding.activeFlow?.providerId ?? undefined
const selectedTarget: CommandSurfaceTarget = { kind: "auth", providerId, intent: "login" }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "submit_provider_flow_input", selectedTarget),
})
const onboarding = await this.submitProviderFlowInput(flowId, input)
const providerLabel =
onboarding?.activeFlow?.providerLabel ??
(providerId && onboarding ? findOnboardingProviderLabel(onboarding, providerId) : providerId) ??
"Provider"
if (!onboarding) {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "submit_provider_flow_input",
success: false,
message: this.state.lastClientError ?? `${providerLabel} sign-in failed`,
selectedTarget,
}),
})
return null
}
if (onboarding.activeFlow?.status === "failed") {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "submit_provider_flow_input",
success: false,
message: onboarding.activeFlow.error ?? `${providerLabel} sign-in failed`,
selectedTarget,
}),
})
return onboarding
}
if (onboarding.bridgeAuthRefresh.phase === "failed") {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "submit_provider_flow_input",
success: false,
message: onboarding.bridgeAuthRefresh.error ?? `${providerLabel} sign-in completed but bridge auth refresh failed`,
selectedTarget,
}),
})
return onboarding
}
const successMessage =
onboarding.activeFlow && ["running", "awaiting_browser_auth", "awaiting_input"].includes(onboarding.activeFlow.status)
? `${providerLabel} sign-in advanced. Complete the remaining step in this panel.`
: `${providerLabel} sign-in complete.`
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "submit_provider_flow_input",
success: true,
message: successMessage,
selectedTarget,
}),
})
return onboarding
}
cancelProviderFlowFromSurface = async (flowId: string): Promise<WorkspaceOnboardingState | null> => {
const providerId = this.state.boot?.onboarding.activeFlow?.providerId ?? undefined
const selectedTarget: CommandSurfaceTarget = { kind: "auth", providerId, intent: "login" }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "cancel_provider_flow", selectedTarget),
})
const onboarding = await this.cancelProviderFlow(flowId)
const providerLabel =
onboarding?.activeFlow?.providerLabel ??
(providerId && onboarding ? findOnboardingProviderLabel(onboarding, providerId) : providerId) ??
"Provider"
if (!onboarding) {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "cancel_provider_flow",
success: false,
message: this.state.lastClientError ?? `${providerLabel} sign-in cancellation failed`,
selectedTarget,
}),
})
return null
}
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "cancel_provider_flow",
success: true,
message: `${providerLabel} sign-in cancelled.`,
selectedTarget,
}),
})
return onboarding
}
logoutProviderFromSurface = async (providerId: string): Promise<WorkspaceOnboardingState | null> => {
const selectedTarget: CommandSurfaceTarget = { kind: "auth", providerId, intent: "logout" }
this.patchState({
commandSurface: setCommandSurfacePending(this.state.commandSurface, "logout_provider", selectedTarget),
})
const onboarding = await this.logoutProvider(providerId)
const providerLabel = onboarding ? findOnboardingProviderLabel(onboarding, providerId) : providerId
if (!onboarding) {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "logout_provider",
success: false,
message: this.state.lastClientError ?? `${providerLabel} logout failed`,
selectedTarget,
}),
})
return null
}
if (onboarding.bridgeAuthRefresh.phase === "failed") {
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "logout_provider",
success: false,
message: onboarding.bridgeAuthRefresh.error ?? `${providerLabel} logout completed but bridge auth refresh failed`,
selectedTarget,
}),
})
return onboarding
}
const providerState = onboarding.required.providers.find((provider) => provider.id === providerId)
const resultMessage = providerState?.configured
? `${providerLabel} saved credentials were removed, but ${providerState.configuredVia} auth still keeps the provider available.`
: onboarding.locked
? `${providerLabel} logged out — required setup is needed again.`
: `${providerLabel} logged out.`
this.patchState({
commandSurface: applyCommandSurfaceActionResult(this.state.commandSurface, {
action: "logout_provider",
success: true,
message: resultMessage,
selectedTarget,
}),
})
return onboarding
}
respondToUiRequest = async (id: string, response: Record<string, unknown>): Promise<void> => {
this.patchState({ commandInFlight: "extension_ui_response" })
try {
const result = await authFetch(this.buildUrl("/api/session/command"), {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ type: "extension_ui_response", id, ...response }),
})
if (!result.ok) {
const body = await result.json().catch(() => ({ error: `HTTP ${result.status}` })) as { error?: string }
throw new Error(body.error ?? `extension_ui_response failed with ${result.status}`)
}
this.patchState({
pendingUiRequests: this.state.pendingUiRequests.filter((r) => r.id !== id),
})
} catch (error) {
const message = normalizeClientError(error)
this.patchState({
lastClientError: message,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `UI response failed — ${message}`)),
})
} finally {
this.patchState({ commandInFlight: null })
}
}
dismissUiRequest = async (id: string): Promise<void> => {
this.patchState({ commandInFlight: "extension_ui_response" })
try {
const result = await authFetch(this.buildUrl("/api/session/command"), {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ type: "extension_ui_response", id, cancelled: true }),
})
if (!result.ok) {
const body = await result.json().catch(() => ({ error: `HTTP ${result.status}` })) as { error?: string }
throw new Error(body.error ?? `extension_ui_response cancel failed with ${result.status}`)
}
this.patchState({
pendingUiRequests: this.state.pendingUiRequests.filter((r) => r.id !== id),
})
} catch (error) {
const message = normalizeClientError(error)
this.patchState({
lastClientError: message,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `UI dismiss failed — ${message}`)),
})
} finally {
this.patchState({ commandInFlight: null })
}
}
sendSteer = async (message: string): Promise<void> => {
await this.sendCommand({ type: "steer", message })
}
sendAbort = async (): Promise<void> => {
await this.sendCommand({ type: "abort" })
}
pushChatUserMessage = (msg: ChatMessage) => {
this.patchState({ chatUserMessages: [...this.state.chatUserMessages, msg] })
}
submitInput = async (input: string, images?: PendingImage[]): Promise<BrowserSlashCommandDispatchResult | null> => {
const trimmed = input.trim()
if (!trimmed) return null
const outcome = dispatchBrowserSlashCommand(trimmed, {
isStreaming: this.state.boot?.bridge.sessionState?.isStreaming,
})
this.patchState({
lastSlashCommandOutcome: trimmed.startsWith("/") ? outcome : null,
})
// Evaluate contextual tips before sending to agent
if (outcome.kind === "prompt") {
const sessionState = this.state.boot?.bridge.sessionState
const tip = this.contextualTips.evaluate({
input: trimmed,
isStreaming: Boolean(sessionState?.isStreaming),
thinkingLevel: sessionState?.thinkingLevel,
// contextPercent not available in web — compaction nudge won't fire here
contextPercent: undefined,
})
if (tip) {
this.patchState({
terminalLines: withTerminalLine(
this.state.terminalLines,
createTerminalLine("system", `💡 ${tip}`),
),
})
}
}
switch (outcome.kind) {
case "prompt":
case "rpc": {
const imagePayload = images?.map((i) => ({ type: "image" as const, data: i.data, mimeType: i.mimeType }))
const command = imagePayload && imagePayload.length > 0
? { ...outcome.command, images: imagePayload }
: outcome.command
await this.sendCommand(command, { displayInput: trimmed })
return outcome
}
case "local":
if (outcome.action === "clear_terminal") {
this.clearTerminalLines()
return outcome
}
if (outcome.action === "refresh_workspace") {
await this.refreshBoot()
return outcome
}
if (outcome.action === "sf_help") {
this.patchState({
terminalLines: withTerminalLine(
withTerminalLine(this.state.terminalLines, createTerminalLine("input", trimmed)),
createTerminalLine("system", SF_HELP_TEXT),
),
})
return outcome
}
return outcome
case "surface": {
if (IMPLEMENTED_BROWSER_COMMAND_SURFACES.has(outcome.surface)) {
this.patchState({
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("input", trimmed)),
})
this.openCommandSurface(outcome.surface, { source: "slash", args: outcome.args })
return outcome
}
const notice = getBrowserSlashCommandTerminalNotice(outcome)
let nextLines = withTerminalLine(this.state.terminalLines, createTerminalLine("input", trimmed))
if (notice) {
nextLines = withTerminalLine(nextLines, createTerminalLine(notice.type, notice.message))
}
this.patchState({ terminalLines: nextLines })
return outcome
}
case "reject": {
const notice = getBrowserSlashCommandTerminalNotice(outcome)
let nextLines = withTerminalLine(this.state.terminalLines, createTerminalLine("input", trimmed))
if (notice) {
nextLines = withTerminalLine(nextLines, createTerminalLine(notice.type, notice.message))
}
this.patchState({ terminalLines: nextLines })
return outcome
}
case "view-navigate": {
this.patchState({
terminalLines: withTerminalLine(
this.state.terminalLines,
createTerminalLine("system", `Navigating to ${outcome.view} view`),
),
})
window.dispatchEvent(
new CustomEvent("sf:navigate-view", { detail: { view: outcome.view } }),
)
return outcome
}
}
}
refreshBoot = async (options: { soft?: boolean } = {}): Promise<void> => {
if (this.bootPromise) return await this.bootPromise
this.lastBootRefreshAt = Date.now()
const softRefresh = Boolean(options.soft && this.state.boot)
this.bootPromise = (async () => {
if (!softRefresh) {
this.patchState({
bootStatus: "loading",
connectionState: this.state.connectionState === "connected" ? "connected" : "connecting",
lastClientError: null,
})
} else {
this.patchState({
lastClientError: null,
})
}
try {
const response = await authFetch(this.buildUrl("/api/boot"), {
method: "GET",
cache: "no-store",
headers: {
Accept: "application/json",
},
})
if (!response.ok) {
if (response.status === 401) {
this.patchState({
bootStatus: "unauthenticated",
connectionState: "error",
})
return
}
throw new Error(`Boot request failed with ${response.status}`)
}
const bootPayload = (await response.json()) as WorkspaceBootPayload
const boot = cloneBootWithBridge(bootPayload, bootPayload.bridge) ?? bootPayload
const live = applyBootToLiveState(this.state.live, boot, { soft: softRefresh })
this.lastBridgeDigest = null
this.lastBridgeDigest = [boot.bridge.phase, boot.bridge.activeSessionId, boot.bridge.lastError?.at, boot.bridge.lastError?.message].join("::")
this.patchState({
bootStatus: "ready",
boot,
live,
connectionState: boot.onboarding.locked
? "idle"
: this.eventSource
? this.state.connectionState
: "connecting",
lastBridgeError: boot.bridge.lastError,
sessionAttached: hasAttachedSession(boot.bridge),
lastClientError: null,
...(softRefresh ? {} : { terminalLines: bootSeedLines(boot) }),
})
if (boot.onboarding.locked) {
this.closeEventStream()
} else {
this.ensureEventStream()
}
} catch (error) {
const message = normalizeClientError(error)
if (softRefresh) {
this.patchState({
lastClientError: message,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `Workspace refresh failed — ${message}`)),
})
return
}
this.patchState({
bootStatus: "error",
connectionState: "error",
lastClientError: message,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `Boot failed — ${message}`)),
})
}
})().finally(() => {
this.bootPromise = null
})
await this.bootPromise
}
private async refreshBootAfterCurrentSettles(options: { soft?: boolean } = {}): Promise<void> {
if (this.bootPromise) {
try {
await this.bootPromise
} catch {
// Preserve the original boot failure surface, then issue a fresh refresh.
}
}
await this.refreshBoot(options)
}
private invalidateLiveFreshness(
domains: LiveStateInvalidationDomain[],
reason: LiveStateInvalidationReason,
source: LiveStateInvalidationSource,
): WorkspaceLiveState {
const nextFreshness = { ...this.state.live.freshness }
if (domains.includes("auto")) {
nextFreshness.auto = withFreshnessInvalidated(nextFreshness.auto, reason, source)
}
if (domains.includes("workspace")) {
nextFreshness.workspace = withFreshnessInvalidated(nextFreshness.workspace, reason, source)
nextFreshness.gitSummary = withFreshnessInvalidated(nextFreshness.gitSummary, reason, source)
}
if (domains.includes("recovery")) {
nextFreshness.recovery = withFreshnessInvalidated(nextFreshness.recovery, reason, source)
nextFreshness.sessionStats = withFreshnessInvalidated(nextFreshness.sessionStats, reason, source)
}
if (domains.includes("resumable_sessions")) {
nextFreshness.resumableSessions = withFreshnessInvalidated(nextFreshness.resumableSessions, reason, source)
nextFreshness.sessionBrowser = withFreshnessInvalidated(nextFreshness.sessionBrowser, reason, source)
nextFreshness.sessionStats = withFreshnessInvalidated(nextFreshness.sessionStats, reason, source)
}
const nextLive = {
...this.state.live,
freshness: nextFreshness,
}
return {
...nextLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: nextLive }),
}
}
private refreshOpenCommandSurfacesForInvalidation(event: LiveStateInvalidationEvent): void {
if (event.domains.includes("workspace") && this.state.commandSurface.open && this.state.commandSurface.section === "git") {
if (this.state.commandSurface.pendingAction !== "load_git_summary") {
void this.loadGitSummary()
}
}
if (event.domains.includes("recovery") && this.state.commandSurface.open && this.state.commandSurface.section === "recovery") {
if (this.state.commandSurface.pendingAction !== "load_recovery_diagnostics") {
void this.loadRecoveryDiagnostics()
}
}
if (event.domains.includes("resumable_sessions")) {
if (
this.state.commandSurface.open &&
(this.state.commandSurface.section === "resume" || this.state.commandSurface.section === "name") &&
this.state.commandSurface.pendingAction !== "load_session_browser"
) {
void this.loadSessionBrowser()
}
if (this.state.commandSurface.open && this.state.commandSurface.section === "session") {
const activeSessionPath = this.state.boot?.bridge.activeSessionFile ?? this.state.boot?.bridge.sessionState?.sessionFile ?? null
this.patchState({
commandSurface: {
...this.state.commandSurface,
sessionStats:
this.state.commandSurface.sessionStats && this.state.commandSurface.sessionStats.sessionFile === activeSessionPath
? this.state.commandSurface.sessionStats
: null,
},
})
if (this.state.commandSurface.pendingAction !== "load_session_stats") {
void this.loadSessionStats()
}
}
}
}
private async reloadLiveState(
domains: LiveStateInvalidationDomain[],
reason: LiveStateInvalidationReason,
): Promise<void> {
const requestedDomains = domains.filter((domain) => domain === "auto" || domain === "workspace" || domain === "resumable_sessions")
if (requestedDomains.length === 0) {
const nextLive = {
...this.state.live,
freshness: {
...this.state.live.freshness,
recovery: withFreshnessSucceeded(this.state.live.freshness.recovery),
},
}
this.patchState({
live: {
...nextLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: nextLive }),
},
})
return
}
const nextFreshness = { ...this.state.live.freshness }
if (requestedDomains.includes("auto")) {
nextFreshness.auto = withFreshnessRequested(nextFreshness.auto)
}
if (requestedDomains.includes("workspace")) {
nextFreshness.workspace = withFreshnessRequested(nextFreshness.workspace)
}
if (requestedDomains.includes("resumable_sessions")) {
nextFreshness.resumableSessions = withFreshnessRequested(nextFreshness.resumableSessions)
}
nextFreshness.recovery = withFreshnessRequested(nextFreshness.recovery)
const requestedLive = {
...this.state.live,
freshness: nextFreshness,
targetedRefreshCount: this.state.live.targetedRefreshCount + 1,
}
this.patchState({
live: {
...requestedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: requestedLive }),
},
})
const params = new URLSearchParams()
for (const domain of requestedDomains) {
params.append("domain", domain)
}
try {
const response = await authFetch(this.buildUrl(`/api/live-state?${params.toString()}`), {
method: "GET",
cache: "no-store",
headers: {
Accept: "application/json",
},
})
const payload = await response.json().catch(() => null) as {
auto?: AutoDashboardData
workspace?: WorkspaceIndex
resumableSessions?: BootResumableSession[]
error?: string
} | null
if (!response.ok || !payload) {
throw new Error(payload?.error ?? `Live state request failed with ${response.status}`)
}
let nextBoot = this.state.boot
const nextLive: WorkspaceLiveState = {
...this.state.live,
freshness: { ...this.state.live.freshness },
}
if (requestedDomains.includes("auto") && payload.auto) {
nextLive.auto = payload.auto
nextLive.freshness.auto = withFreshnessSucceeded(nextLive.freshness.auto)
nextBoot = nextBoot
? {
...nextBoot,
auto: payload.auto,
}
: nextBoot
}
if (requestedDomains.includes("workspace") && payload.workspace) {
nextLive.workspace = payload.workspace
nextLive.freshness.workspace = withFreshnessSucceeded(nextLive.freshness.workspace)
nextBoot = nextBoot
? {
...nextBoot,
workspace: payload.workspace,
}
: nextBoot
}
if (requestedDomains.includes("resumable_sessions") && payload.resumableSessions) {
const nextSessions = overlayLiveBridgeSessionState(payload.resumableSessions, nextBoot)
nextLive.resumableSessions = nextSessions
nextLive.freshness.resumableSessions = withFreshnessSucceeded(nextLive.freshness.resumableSessions)
nextBoot = nextBoot
? {
...nextBoot,
resumableSessions: nextSessions,
}
: nextBoot
}
nextLive.freshness.recovery = withFreshnessSucceeded(nextLive.freshness.recovery)
nextLive.recoverySummary = createWorkspaceRecoverySummary({ boot: nextBoot, live: nextLive })
this.patchState({
...(nextBoot ? { boot: nextBoot } : {}),
live: nextLive,
})
} catch (error) {
const message = normalizeClientError(error)
const failedLive: WorkspaceLiveState = {
...this.state.live,
freshness: {
...this.state.live.freshness,
auto:
requestedDomains.includes("auto")
? withFreshnessFailed(this.state.live.freshness.auto, message)
: this.state.live.freshness.auto,
workspace:
requestedDomains.includes("workspace")
? withFreshnessFailed(this.state.live.freshness.workspace, message)
: this.state.live.freshness.workspace,
resumableSessions:
requestedDomains.includes("resumable_sessions")
? withFreshnessFailed(this.state.live.freshness.resumableSessions, message)
: this.state.live.freshness.resumableSessions,
recovery: withFreshnessFailed(this.state.live.freshness.recovery, message),
},
}
this.patchState({
lastClientError: message,
live: {
...failedLive,
recoverySummary: createWorkspaceRecoverySummary({ boot: this.state.boot, live: failedLive }),
},
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `Live refresh failed (${reason}) — ${message}`)),
})
}
}
private handleLiveStateInvalidation(event: LiveStateInvalidationEvent): void {
this.patchState({
live: this.invalidateLiveFreshness(event.domains, event.reason, event.source),
commandSurface: event.domains.includes("recovery")
? {
...this.state.commandSurface,
recovery: markRecoveryStateInvalidated(this.state.commandSurface.recovery),
}
: this.state.commandSurface,
})
this.refreshOpenCommandSurfacesForInvalidation(event)
void this.reloadLiveState(event.domains, event.reason)
}
refreshOnboarding = async (): Promise<WorkspaceOnboardingState | null> => {
this.patchState({
onboardingRequestState: "refreshing",
onboardingRequestProviderId: null,
lastClientError: null,
})
try {
return await this.fetchOnboardingState()
} catch (error) {
const message = normalizeClientError(error)
this.patchState({
lastClientError: message,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `Onboarding refresh failed — ${message}`)),
})
return null
} finally {
this.patchState({
onboardingRequestState: "idle",
onboardingRequestProviderId: null,
})
}
}
saveApiKey = async (providerId: string, apiKey: string): Promise<WorkspaceOnboardingState | null> => {
this.patchState({
onboardingRequestState: "saving_api_key",
onboardingRequestProviderId: providerId,
lastClientError: null,
})
try {
const onboarding = await this.postOnboardingAction({
action: "save_api_key",
providerId,
apiKey,
})
await this.syncAfterOnboardingMutation(onboarding)
return onboarding
} catch (error) {
const message = normalizeClientError(error)
this.patchState({
lastClientError: message,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `Credential setup failed — ${message}`)),
})
return null
} finally {
this.patchState({
onboardingRequestState: "idle",
onboardingRequestProviderId: null,
})
}
}
startProviderFlow = async (providerId: string): Promise<WorkspaceOnboardingState | null> => {
this.patchState({
onboardingRequestState: "starting_provider_flow",
onboardingRequestProviderId: providerId,
lastClientError: null,
})
try {
const onboarding = await this.postOnboardingAction({
action: "start_provider_flow",
providerId,
})
await this.syncAfterOnboardingMutation(onboarding)
return onboarding
} catch (error) {
const message = normalizeClientError(error)
this.patchState({
lastClientError: message,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `Provider sign-in failed to start — ${message}`)),
})
return null
} finally {
this.patchState({
onboardingRequestState: "idle",
onboardingRequestProviderId: null,
})
}
}
submitProviderFlowInput = async (flowId: string, input: string): Promise<WorkspaceOnboardingState | null> => {
this.patchState({
onboardingRequestState: "submitting_provider_flow_input",
onboardingRequestProviderId: this.state.boot?.onboarding.activeFlow?.providerId ?? null,
lastClientError: null,
})
try {
const onboarding = await this.postOnboardingAction({
action: "continue_provider_flow",
flowId,
input,
})
await this.syncAfterOnboardingMutation(onboarding)
return onboarding
} catch (error) {
const message = normalizeClientError(error)
this.patchState({
lastClientError: message,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `Provider sign-in input failed — ${message}`)),
})
return null
} finally {
this.patchState({
onboardingRequestState: "idle",
onboardingRequestProviderId: null,
})
}
}
cancelProviderFlow = async (flowId: string): Promise<WorkspaceOnboardingState | null> => {
this.patchState({
onboardingRequestState: "cancelling_provider_flow",
onboardingRequestProviderId: this.state.boot?.onboarding.activeFlow?.providerId ?? null,
lastClientError: null,
})
try {
const onboarding = await this.postOnboardingAction({
action: "cancel_provider_flow",
flowId,
})
await this.syncAfterOnboardingMutation(onboarding)
return onboarding
} catch (error) {
const message = normalizeClientError(error)
this.patchState({
lastClientError: message,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `Provider sign-in cancellation failed — ${message}`)),
})
return null
} finally {
this.patchState({
onboardingRequestState: "idle",
onboardingRequestProviderId: null,
})
}
}
logoutProvider = async (providerId: string): Promise<WorkspaceOnboardingState | null> => {
this.patchState({
onboardingRequestState: "logging_out_provider",
onboardingRequestProviderId: providerId,
lastClientError: null,
})
try {
const onboarding = await this.postOnboardingAction({
action: "logout_provider",
providerId,
})
await this.syncAfterOnboardingMutation(onboarding)
return onboarding
} catch (error) {
const message = normalizeClientError(error)
this.patchState({
lastClientError: message,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `Provider logout failed — ${message}`)),
})
return null
} finally {
this.patchState({
onboardingRequestState: "idle",
onboardingRequestProviderId: null,
})
}
}
sendCommand = async (
command: WorkspaceBridgeCommand,
options: { displayInput?: string; appendInputLine?: boolean; appendResponseLine?: boolean } = {},
): Promise<WorkspaceCommandResponse | null> => {
this.clearCommandTimeout()
const nextPatch: Partial<WorkspaceStoreState> = {
commandInFlight: command.type,
}
if (options.appendInputLine !== false) {
nextPatch.terminalLines = withTerminalLine(
this.state.terminalLines,
createTerminalLine("input", options.displayInput ?? getCommandInputLabel(command)),
)
}
this.patchState(nextPatch)
this.commandTimeoutTimer = setTimeout(() => {
if (this.state.commandInFlight) {
this.patchState({
commandInFlight: null,
lastClientError: "Command timed out — controls re-enabled",
terminalLines: withTerminalLine(
this.state.terminalLines,
createTerminalLine("error", "Command timed out — controls re-enabled"),
),
})
}
}, COMMAND_TIMEOUT_MS)
try {
const response = await authFetch(this.buildUrl("/api/session/command"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(command),
})
const payload = (await response.json()) as WorkspaceCommandResponse | { ok: true }
if ("ok" in payload) {
return null
}
if (payload.command === "get_state" && payload.success && this.state.boot) {
const nextBridge = {
...this.state.boot.bridge,
sessionState: payload.data as WorkspaceSessionState,
activeSessionId: (payload.data as WorkspaceSessionState).sessionId,
activeSessionFile: (payload.data as WorkspaceSessionState).sessionFile ?? this.state.boot.bridge.activeSessionFile,
lastCommandType: "get_state",
updatedAt: new Date().toISOString(),
}
this.patchState({
boot: cloneBootWithBridge(this.state.boot, nextBridge),
lastBridgeError: nextBridge.lastError,
sessionAttached: hasAttachedSession(nextBridge),
})
}
// Reset contextual tips on new session
if (payload.command === "new_session" && payload.success) {
this.contextualTips.reset()
}
if (payload.code === "onboarding_locked" && payload.details?.onboarding && this.state.boot) {
this.patchState({
boot: cloneBootWithPartialOnboarding(this.state.boot, payload.details.onboarding),
})
}
this.patchState({
...(options.appendResponseLine === false
? {}
: { terminalLines: withTerminalLine(this.state.terminalLines, responseToLine(payload)) }),
lastBridgeError: payload.success ? this.state.lastBridgeError : this.state.boot?.bridge.lastError ?? this.state.lastBridgeError,
})
return payload
} catch (error) {
const message = normalizeClientError(error)
this.patchState({
lastClientError: message,
terminalLines: withTerminalLine(
this.state.terminalLines,
createTerminalLine("error", `Command failed (${command.type}) — ${message}`),
),
})
return {
type: "response",
command: command.type,
success: false,
error: message,
}
} finally {
this.clearCommandTimeout()
this.patchState({ commandInFlight: null })
}
}
private clearCommandTimeout(): void {
if (this.commandTimeoutTimer) {
clearTimeout(this.commandTimeoutTimer)
this.commandTimeoutTimer = null
}
}
private async fetchOnboardingState(silent = false): Promise<WorkspaceOnboardingState> {
const previousFlowStatus = this.state.boot?.onboarding.activeFlow?.status ?? null
const response = await authFetch(this.buildUrl("/api/onboarding"), {
method: "GET",
cache: "no-store",
headers: {
Accept: "application/json",
},
})
const payload = (await response.json()) as OnboardingApiPayload
if (!response.ok || !payload.onboarding) {
throw new Error(payload.error ?? `Onboarding request failed with ${response.status}`)
}
this.applyOnboardingState(payload.onboarding)
if (
previousFlowStatus &&
ACTIVE_ONBOARDING_FLOW_STATUSES.has(previousFlowStatus) &&
payload.onboarding.activeFlow &&
TERMINAL_ONBOARDING_FLOW_STATUSES.has(payload.onboarding.activeFlow.status)
) {
await this.syncAfterOnboardingMutation(payload.onboarding)
} else if (!silent) {
this.appendOnboardingSummaryLine(payload.onboarding)
}
return payload.onboarding
}
private async postOnboardingAction(body: Record<string, unknown>): Promise<WorkspaceOnboardingState> {
const response = await authFetch(this.buildUrl("/api/onboarding"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(body),
})
const payload = (await response.json()) as OnboardingApiPayload
if (!payload.onboarding) {
throw new Error(payload.error ?? `Onboarding action failed with ${response.status}`)
}
this.applyOnboardingState(payload.onboarding)
return payload.onboarding
}
private applyOnboardingState(onboarding: WorkspaceOnboardingState): void {
if (!this.state.boot) return
this.patchState({
boot: cloneBootWithOnboarding(this.state.boot, onboarding),
})
}
private async syncAfterOnboardingMutation(onboarding: WorkspaceOnboardingState): Promise<void> {
this.applyOnboardingState(onboarding)
this.appendOnboardingSummaryLine(onboarding)
if (onboarding.lastValidation?.status === "succeeded" || onboarding.bridgeAuthRefresh.phase !== "idle") {
void this.refreshBootAfterCurrentSettles({ soft: true })
}
}
private appendOnboardingSummaryLine(onboarding: WorkspaceOnboardingState): void {
const summary = summarizeOnboardingState(onboarding)
if (!summary) return
const lastLine = this.state.terminalLines.at(-1)
if (lastLine?.type === summary.type && lastLine.content === summary.message) {
return
}
this.patchState({
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine(summary.type, summary.message)),
})
}
private emit(): void {
for (const listener of this.listeners) {
listener()
}
}
private patchState(patch: Partial<WorkspaceStoreState>): void {
this.state = { ...this.state, ...patch }
this.syncOnboardingPoller()
this.emit()
}
private syncOnboardingPoller(): void {
if (this.disposed) {
this.stopOnboardingPoller()
return
}
const flowStatus = this.state.boot?.onboarding.activeFlow?.status
const shouldPoll = Boolean(flowStatus && ACTIVE_ONBOARDING_FLOW_STATUSES.has(flowStatus))
if (shouldPoll && !this.onboardingPollTimer) {
this.onboardingPollTimer = setInterval(() => {
if (this.state.onboardingRequestState !== "idle") return
void this.fetchOnboardingState(true).catch((error) => {
const message = normalizeClientError(error)
this.patchState({
lastClientError: message,
})
})
}, 1500)
return
}
if (!shouldPoll) {
this.stopOnboardingPoller()
}
}
private stopOnboardingPoller(): void {
if (!this.onboardingPollTimer) return
clearInterval(this.onboardingPollTimer)
this.onboardingPollTimer = null
}
private ensureEventStream(): void {
if (this.eventSource || this.disposed || this.state.boot?.onboarding.locked) return
const stream = new EventSource(appendAuthParam(this.buildUrl("/api/session/events")))
this.eventSource = stream
stream.onopen = () => {
const previousState = this.lastStreamState
const wasDisconnected = previousState === "reconnecting" || previousState === "disconnected" || previousState === "error"
if (wasDisconnected) {
this.patchState({
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("success", "Live event stream reconnected")),
})
}
this.lastStreamState = "connected"
this.patchState({ connectionState: "connected", lastClientError: null })
if (wasDisconnected) {
void this.refreshBoot({ soft: true })
}
}
stream.onmessage = (message) => {
try {
const parsed: unknown = JSON.parse(message.data)
if (!isWorkspaceEvent(parsed)) {
this.patchState({
lastClientError: "Malformed event received from stream",
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", "Malformed event received from stream")),
})
return
}
this.handleEvent(parsed)
} catch (error) {
const text = normalizeClientError(error)
this.patchState({
lastClientError: text,
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", `Failed to parse stream event — ${text}`)),
})
}
}
stream.onerror = () => {
const nextConnectionState = this.lastStreamState === "connected" ? "reconnecting" : "error"
if (nextConnectionState !== this.lastStreamState) {
this.patchState({
connectionState: nextConnectionState,
terminalLines: withTerminalLine(
this.state.terminalLines,
createTerminalLine(
nextConnectionState === "reconnecting" ? "system" : "error",
nextConnectionState === "reconnecting"
? "Live event stream disconnected — retrying…"
: "Live event stream failed before connection was established",
),
),
})
} else {
this.patchState({ connectionState: nextConnectionState })
}
this.lastStreamState = nextConnectionState
}
}
private closeEventStream(): void {
this.eventSource?.close()
this.eventSource = null
}
private handleEvent(event: WorkspaceEvent): void {
this.patchState({ lastEventType: event.type })
if (event.type === "bridge_status") {
this.recordBridgeStatus((event as BridgeStatusEvent).bridge)
return
}
if (event.type === "live_state_invalidation") {
this.handleLiveStateInvalidation(event as LiveStateInvalidationEvent)
}
// Route into structured live-interaction state (additive — summary lines still produced below)
this.routeLiveInteractionEvent(event)
const summary = summarizeEvent(event)
if (!summary) return
this.patchState({
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine(summary.type, summary.message)),
})
}
private routeLiveInteractionEvent(event: WorkspaceEvent): void {
switch (event.type) {
case "extension_ui_request":
this.handleExtensionUiRequest(event as ExtensionUiRequestEvent)
break
case "message_update":
this.handleMessageUpdate(event as MessageUpdateEvent)
break
case "agent_end":
case "turn_end":
this.handleTurnBoundary()
break
case "tool_execution_start":
this.handleToolExecutionStart(event as ToolExecutionStartEvent)
break
case "tool_execution_update":
this.handleToolExecutionUpdate(event as ToolExecutionUpdateEvent)
break
case "tool_execution_end":
this.handleToolExecutionEnd(event as ToolExecutionEndEvent)
break
case "bridge_status":
// Handled upstream in handleEvent with early return — never reaches here
break
case "live_state_invalidation":
// Handled upstream in handleEvent via handleLiveStateInvalidation — no live interaction state update needed
break
case "extension_error":
// Terminal line produced by summarizeEvent — no live interaction state update needed
break
}
}
private handleExtensionUiRequest(event: ExtensionUiRequestEvent): void {
const method = event.method
switch (method) {
// Blocking methods → queue in pendingUiRequests
case "select":
case "confirm":
case "input":
case "editor":
this.patchState({
pendingUiRequests: [...this.state.pendingUiRequests, event as PendingUiRequest],
})
break
// Fire-and-forget methods → update state maps
case "notify":
// notify still produces a terminal line (via summarizeEvent), but we don't store it in pendingUiRequests
break
case "setStatus":
if (event.method === "setStatus") {
const next = { ...this.state.statusTexts }
if (event.statusText === undefined) {
delete next[event.statusKey]
} else {
next[event.statusKey] = event.statusText
}
this.patchState({ statusTexts: next })
}
break
case "setWidget":
if (event.method === "setWidget") {
const next = { ...this.state.widgetContents }
if (event.widgetLines === undefined) {
delete next[event.widgetKey]
} else {
next[event.widgetKey] = { lines: event.widgetLines, placement: event.widgetPlacement }
}
this.patchState({ widgetContents: next })
}
break
case "setTitle":
if (event.method === "setTitle") {
const nextTitle = event.title.trim()
this.patchState({ titleOverride: nextTitle ? nextTitle : null })
}
break
case "set_editor_text":
if (event.method === "set_editor_text") {
this.patchState({ editorTextBuffer: event.text })
}
break
}
}
private handleMessageUpdate(event: MessageUpdateEvent): void {
const assistantEvent = event.assistantMessageEvent
if (!assistantEvent) return
if (assistantEvent.type === "text_delta" && typeof assistantEvent.delta === "string") {
// If we were accumulating thinking and now text arrives, finalize the thinking segment
if (this.state.streamingThinkingText.length > 0) {
this.patchState({
currentTurnSegments: [...this.state.currentTurnSegments, { kind: "thinking", content: this.state.streamingThinkingText }],
streamingThinkingText: "",
})
}
this.patchState({
streamingAssistantText: this.state.streamingAssistantText + assistantEvent.delta,
})
} else if (assistantEvent.type === "thinking_delta" && typeof assistantEvent.delta === "string") {
// If we were accumulating text and now thinking arrives, finalize the text segment
if (this.state.streamingAssistantText.length > 0) {
this.patchState({
currentTurnSegments: [...this.state.currentTurnSegments, { kind: "text", content: this.state.streamingAssistantText }],
streamingAssistantText: "",
})
}
this.patchState({
streamingThinkingText: this.state.streamingThinkingText + assistantEvent.delta,
})
} else if (assistantEvent.type === "thinking_end") {
// Finalize thinking segment
if (this.state.streamingThinkingText.length > 0) {
this.patchState({
currentTurnSegments: [...this.state.currentTurnSegments, { kind: "thinking", content: this.state.streamingThinkingText }],
streamingThinkingText: "",
})
}
}
}
private handleTurnBoundary(): void {
// Finalize any remaining streaming content into segments
const pendingSegments: TurnSegment[] = []
if (this.state.streamingThinkingText.length > 0) {
pendingSegments.push({ kind: "thinking", content: this.state.streamingThinkingText })
}
if (this.state.streamingAssistantText.length > 0) {
pendingSegments.push({ kind: "text", content: this.state.streamingAssistantText })
}
const finalSegments = pendingSegments.length > 0
? [...this.state.currentTurnSegments, ...pendingSegments]
: this.state.currentTurnSegments
// Build the flat transcript text (backward-compat for terminal.tsx / files-view.tsx)
const fullText = finalSegments
.filter((s): s is TurnSegment & { kind: "text" } => s.kind === "text")
.map((s) => s.content)
.join("")
if (fullText.length > 0 || finalSegments.length > 0) {
const nextTranscript = [...this.state.liveTranscript, fullText]
const nextThinking = [...this.state.liveThinkingTranscript, ""]
const nextSegments = [...this.state.completedTurnSegments, finalSegments]
const overflow = nextTranscript.length > MAX_TRANSCRIPT_BLOCKS ? nextTranscript.length - MAX_TRANSCRIPT_BLOCKS : 0
// When overflow trims the front of parallel arrays, also trim
// chatUserMessages to keep index-based interleaving aligned (#2707).
const trimmedUserMsgs = overflow > 0
? this.state.chatUserMessages.slice(overflow)
: undefined
this.patchState({
liveTranscript: overflow > 0 ? nextTranscript.slice(overflow) : nextTranscript,
liveThinkingTranscript: overflow > 0 ? nextThinking.slice(overflow) : nextThinking,
completedTurnSegments: overflow > 0 ? nextSegments.slice(overflow) : nextSegments,
...(trimmedUserMsgs !== undefined ? { chatUserMessages: trimmedUserMsgs } : {}),
streamingAssistantText: "",
streamingThinkingText: "",
currentTurnSegments: [],
completedToolExecutions: [],
})
} else if (this.state.streamingThinkingText.length > 0) {
// Turn ended with only thinking, no visible text — clear
this.patchState({
streamingThinkingText: "",
currentTurnSegments: [],
completedToolExecutions: [],
})
} else {
// Empty turn — just reset
this.patchState({
currentTurnSegments: [],
completedToolExecutions: [],
})
}
}
private handleToolExecutionStart(event: ToolExecutionStartEvent): void {
this.patchState({
activeToolExecution: {
id: event.toolCallId,
name: event.toolName,
args: (event as Record<string, unknown>).args as Record<string, unknown> | undefined,
},
// Treat pre-tool streaming text as ephemeral. Claude Code can emit
// provisional assistant text before a tool call, then replace it with
// the real final text after the tool completes. If we finalize that
// interim text here, the chat timeline shows stale text above the tool.
streamingAssistantText: "",
streamingThinkingText: "",
})
}
private handleToolExecutionUpdate(event: ToolExecutionUpdateEvent): void {
const active = this.state.activeToolExecution
if (!active || active.id !== event.toolCallId) return
this.patchState({
activeToolExecution: {
...active,
result: event.partialResult
? {
content: event.partialResult.content,
details: event.partialResult.details,
isError: Boolean(event.partialResult.isError),
}
: active.result,
},
})
}
private handleToolExecutionEnd(event: ToolExecutionEndEvent): void {
const active = this.state.activeToolExecution
if (active) {
const completed: CompletedToolExecution = {
id: active.id,
name: active.name,
args: active.args ?? {},
result: {
content: ((event as Record<string, unknown>).result as NonNullable<CompletedToolExecution["result"]> | undefined)?.content,
details: ((event as Record<string, unknown>).result as NonNullable<CompletedToolExecution["result"]> | undefined)?.details,
isError: event.isError,
},
}
const next = [...this.state.completedToolExecutions, completed]
this.patchState({
activeToolExecution: null,
completedToolExecutions: next.length > 50 ? next.slice(next.length - 50) : next,
// Also push tool segment into chronological order
currentTurnSegments: [...this.state.currentTurnSegments, { kind: "tool", tool: completed }],
})
} else {
this.patchState({ activeToolExecution: null })
}
}
private recordBridgeStatus(bridge: BridgeRuntimeSnapshot): void {
const digest = [bridge.phase, bridge.activeSessionId, bridge.lastError?.at, bridge.lastError?.message].join("::")
const shouldEmitLine = digest !== this.lastBridgeDigest
this.lastBridgeDigest = digest
const nextBoot = cloneBootWithBridge(this.state.boot, bridge)
const nextLiveBase: WorkspaceLiveState = {
...this.state.live,
resumableSessions: overlayLiveBridgeSessionState(this.state.live.resumableSessions, nextBoot),
}
const nextLive = {
...nextLiveBase,
recoverySummary: createWorkspaceRecoverySummary({ boot: nextBoot, live: nextLiveBase }),
}
const nextPatch: Partial<WorkspaceStoreState> = {
boot: nextBoot,
live: nextLive,
lastBridgeError: bridge.lastError,
sessionAttached: hasAttachedSession(bridge),
commandSurface: {
...this.state.commandSurface,
sessionBrowser: syncSessionBrowserStateWithBridge(this.state.commandSurface.sessionBrowser, nextBoot),
},
}
if (shouldEmitLine) {
const summary = summarizeBridgeStatus(bridge)
nextPatch.terminalLines = withTerminalLine(this.state.terminalLines, createTerminalLine(summary.type, summary.message))
}
this.patchState(nextPatch)
}
}
const WorkspaceStoreContext = createContext<SFWorkspaceStore | null>(null)
export function SFWorkspaceProvider({ children, store: externalStore }: { children: ReactNode; store?: SFWorkspaceStore }) {
const [internalStore] = useState(() => new SFWorkspaceStore())
const store = externalStore ?? internalStore
useEffect(() => {
// Only start/dispose if using internal store (not externally managed)
if (!externalStore) {
store.start()
return () => store.dispose()
}
}, [store, externalStore])
return <WorkspaceStoreContext.Provider value={store}>{children}</WorkspaceStoreContext.Provider>
}
function useWorkspaceStore(): SFWorkspaceStore {
const store = useContext(WorkspaceStoreContext)
if (!store) {
throw new Error("useWorkspaceStore must be used within SFWorkspaceProvider")
}
return store
}
export function useSFWorkspaceState(): WorkspaceStoreState {
const store = useWorkspaceStore()
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)
}
export function useSFWorkspaceActions(): Pick<
SFWorkspaceStore,
| "sendCommand"
| "submitInput"
| "clearTerminalLines"
| "consumeEditorTextBuffer"
| "refreshBoot"
| "refreshOnboarding"
| "openCommandSurface"
| "closeCommandSurface"
| "setCommandSurfaceSection"
| "selectCommandSurfaceTarget"
| "loadGitSummary"
| "loadRecoveryDiagnostics"
| "loadForensicsDiagnostics"
| "loadDoctorDiagnostics"
| "applyDoctorFixes"
| "loadSkillHealthDiagnostics"
| "loadKnowledgeData"
| "loadCapturesData"
| "loadSettingsData"
| "loadHistoryData"
| "loadInspectData"
| "loadHooksData"
| "loadExportData"
| "loadUndoInfo"
| "loadCleanupData"
| "loadSteerData"
| "executeUndoAction"
| "executeCleanupAction"
| "resolveCaptureAction"
| "updateSessionBrowserState"
| "loadSessionBrowser"
| "renameSessionFromSurface"
| "loadAvailableModels"
| "applyModelSelection"
| "applyThinkingLevel"
| "setSteeringModeFromSurface"
| "setFollowUpModeFromSurface"
| "setAutoCompactionFromSurface"
| "setAutoRetryFromSurface"
| "abortRetryFromSurface"
| "switchSessionFromSurface"
| "loadSessionStats"
| "exportSessionFromSurface"
| "loadForkMessages"
| "forkSessionFromSurface"
| "compactSessionFromSurface"
| "saveApiKey"
| "saveApiKeyFromSurface"
| "startProviderFlow"
| "startProviderFlowFromSurface"
| "submitProviderFlowInput"
| "submitProviderFlowInputFromSurface"
| "cancelProviderFlow"
| "cancelProviderFlowFromSurface"
| "logoutProvider"
| "logoutProviderFromSurface"
| "respondToUiRequest"
| "dismissUiRequest"
| "sendSteer"
| "sendAbort"
| "pushChatUserMessage"
> {
const store = useWorkspaceStore()
return {
sendCommand: store.sendCommand,
submitInput: store.submitInput,
clearTerminalLines: store.clearTerminalLines,
consumeEditorTextBuffer: store.consumeEditorTextBuffer,
refreshBoot: store.refreshBoot,
refreshOnboarding: store.refreshOnboarding,
openCommandSurface: store.openCommandSurface,
closeCommandSurface: store.closeCommandSurface,
setCommandSurfaceSection: store.setCommandSurfaceSection,
selectCommandSurfaceTarget: store.selectCommandSurfaceTarget,
loadGitSummary: store.loadGitSummary,
loadRecoveryDiagnostics: store.loadRecoveryDiagnostics,
loadForensicsDiagnostics: store.loadForensicsDiagnostics,
loadDoctorDiagnostics: store.loadDoctorDiagnostics,
applyDoctorFixes: store.applyDoctorFixes,
loadSkillHealthDiagnostics: store.loadSkillHealthDiagnostics,
loadKnowledgeData: store.loadKnowledgeData,
loadCapturesData: store.loadCapturesData,
loadSettingsData: store.loadSettingsData,
loadHistoryData: store.loadHistoryData,
loadInspectData: store.loadInspectData,
loadHooksData: store.loadHooksData,
loadExportData: store.loadExportData,
loadUndoInfo: store.loadUndoInfo,
loadCleanupData: store.loadCleanupData,
loadSteerData: store.loadSteerData,
executeUndoAction: store.executeUndoAction,
executeCleanupAction: store.executeCleanupAction,
resolveCaptureAction: store.resolveCaptureAction,
updateSessionBrowserState: store.updateSessionBrowserState,
loadSessionBrowser: store.loadSessionBrowser,
renameSessionFromSurface: store.renameSessionFromSurface,
loadAvailableModels: store.loadAvailableModels,
applyModelSelection: store.applyModelSelection,
applyThinkingLevel: store.applyThinkingLevel,
setSteeringModeFromSurface: store.setSteeringModeFromSurface,
setFollowUpModeFromSurface: store.setFollowUpModeFromSurface,
setAutoCompactionFromSurface: store.setAutoCompactionFromSurface,
setAutoRetryFromSurface: store.setAutoRetryFromSurface,
abortRetryFromSurface: store.abortRetryFromSurface,
switchSessionFromSurface: store.switchSessionFromSurface,
loadSessionStats: store.loadSessionStats,
exportSessionFromSurface: store.exportSessionFromSurface,
loadForkMessages: store.loadForkMessages,
forkSessionFromSurface: store.forkSessionFromSurface,
compactSessionFromSurface: store.compactSessionFromSurface,
saveApiKey: store.saveApiKey,
saveApiKeyFromSurface: store.saveApiKeyFromSurface,
startProviderFlow: store.startProviderFlow,
startProviderFlowFromSurface: store.startProviderFlowFromSurface,
submitProviderFlowInput: store.submitProviderFlowInput,
submitProviderFlowInputFromSurface: store.submitProviderFlowInputFromSurface,
cancelProviderFlow: store.cancelProviderFlow,
cancelProviderFlowFromSurface: store.cancelProviderFlowFromSurface,
logoutProvider: store.logoutProvider,
logoutProviderFromSurface: store.logoutProviderFromSurface,
respondToUiRequest: store.respondToUiRequest,
dismissUiRequest: store.dismissUiRequest,
sendSteer: store.sendSteer,
sendAbort: store.sendAbort,
pushChatUserMessage: store.pushChatUserMessage,
}
}
export function buildPromptCommand(
input: string,
bridge: BridgeRuntimeSnapshot | null | undefined,
): WorkspaceBridgeCommand {
const outcome = dispatchBrowserSlashCommand(input, {
isStreaming: bridge?.sessionState?.isStreaming,
})
if (outcome.kind === "prompt" || outcome.kind === "rpc") {
return outcome.command
}
throw new Error(
`buildPromptCommand cannot serialize ${outcome.input || input} because browser dispatch resolved it to ${outcome.kind}; use submitInput() instead.`,
)
}