singularity-forge/web/lib/command-surface-contract.ts
Andrew 815be0a698 feat: managed RTK integration with opt-in preference and web UI toggle (#2620)
* feat: integrate managed RTK across shell workflows

* fix(rtk): unify managed fallback and live savings wiring

* fix(rtk): improve TUI status visibility

* fix(tests): make portability tests independent of pi-coding-agent dist build

The CI portability test runs don't guarantee that
packages/pi-coding-agent has been compiled. Any test that
imported files pulling in @gsd/pi-coding-agent (resource-loader,
preferences-skills, async-bash-tool, etc.) crashed with
ERR_MODULE_NOT_FOUND pointing at dist/index.js.

Two changes to dist-redirect.mjs (the Node ESM loader hook used by
all unit tests):
- Redirect the bare @gsd/pi-coding-agent specifier to the workspace
  source entrypoint (src/index.ts) so no dist/ artifact is needed.
- Extend the load() hook to transpile *.ts files under
  packages/pi-coding-agent/src/ through TypeScript's transpileModule.
  Node's --experimental-strip-types can't handle parameter properties
  and similar syntax present in that package's source; full transpilation
  avoids the ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX crash.

Also fix the dashboard.tsx responsive grid:
- xl:grid-cols-5 → xl:grid-cols-4 2xl:grid-cols-5
  (5 metric cards no longer fit at xl without overflow; test contract
  expected xl:grid-cols-4)
- Keep loading-skeletons.tsx in sync with the same breakpoints.

Add src/tests/resolve-ts-loader.test.ts to guard the loader behaviour:
- bare @gsd/pi-coding-agent redirect points to workspace source
- direct source-entry rewrite (.js → .ts)
- transpilation removes TS parameter property syntax that strip-only
  mode cannot parse

* fix(tests): redirect all workspace package imports to source in portability tests

The previous fix only redirected @gsd/pi-coding-agent to its
source entrypoint. In CI, pi-coding-agent/src itself imports
@gsd/pi-ai (and other workspace packages) which were still pointing
at dist/. Since no workspace dist is built during the portability
test run, any transitive resolution hit the same ERR_MODULE_NOT_FOUND.

Changes to dist-redirect.mjs:
- Redirect @gsd/pi-ai, @gsd/pi-ai/oauth, @gsd/pi-agent-core, and
  @gsd/pi-tui bare imports to their workspace src/ entrypoints.
- Broaden the load() transpilation condition from
  '/packages/pi-coding-agent/src/' to '/packages/*/src/' so that
  all workspace source files are run through TypeScript's
  transpileModule, handling parameter properties and other syntax
  that Node's strip-only mode rejects.

Verified by hiding all four workspace dist/ directories locally and
running the failing test set — 96/96 pass.

* fix(tests): redirect @gsd/native sub-paths; fix Windows .cmd spawnSync

Two more portability failures after the previous fix:

1. @gsd/native sub-path imports (@gsd/native/fd, @gsd/native/text, etc.)
   were not redirected — the loader only handled the bare specifier.
   Added a prefix-match redirect for @gsd/native/* → packages/native/src/<sub>/index.ts.

2. Windows RTK tests failed because createFakeRtk produces a .cmd wrapper
   on Windows, and spawnSync(binaryPath, [...]) without shell:true silently
   returns non-zero when the binary is a .cmd file.
   Added shell: /\.(cmd|bat)$/i.test(binaryPath) to the spawnSync calls in:
   - src/resources/extensions/shared/rtk.ts (rewriteCommandWithRtk)
   - src/resources/extensions/shared/rtk-session-stats.ts (readCurrentRtkGainSummary)
   - packages/pi-coding-agent/src/utils/rtk.ts (rewriteCommandForGsd)
   Production use of rtk.exe is unaffected; the shell flag is only true for
   .cmd/.bat paths.

Verified: all 93 portability tests pass with all workspace dist/ directories
removed (simulating CI portability environment).

* fix(tests): Windows portability fixes — HOME env, managed RTK path, perf threshold

Four Windows-specific failures fixed:

1. app-smoke.test.ts: process.env.HOME is undefined on Windows (uses
   USERPROFILE instead). Changed to homedir() from node:os which works
   cross-platform.

2. Managed RTK path tests on Windows: tests placed a fake RTK as rtk.exe
   (by copying a .cmd script into a .exe filename), which Windows cannot
   execute. Two-part fix:
   - resolveRtkBinaryPath() in both rtk.ts files now falls back to rtk.cmd
     in the managed dir on Windows when rtk.exe is absent.
   - withManagedFakeRtk and equivalent patterns in rtk.test.ts,
     rtk-session-stats.test.ts, rtk-execution-seams.test.ts changed to
     place the fake at rtk.cmd instead of rtk.exe on Windows.

3. bg_shell RTK test on Windows: requires bash (for shell sessions), which
   is not available on the blacksmith-4vcpu-windows-2025 runner without
   Git Bash installed. Test now skips on win32.

4. derive-state-db perf assertion: 10ms threshold was too tight for Windows
   CI runners (measured 12ms under load). Raised to 25ms — still catches
   real regressions (baseline is 3ms locally and ~12ms on stressed runners).

* fix(tests): fix managed RTK path fallback on Windows in src/rtk.ts + fix copyable fake

Two remaining Windows failures:

1. src/rtk.ts was never patched with the rtk.cmd managed-dir fallback
   (only the shared/rtk.ts and pi-coding-agent/src/utils/rtk.ts were updated).
   Added the same rtk.cmd fallback and shell:.cmd detection to src/rtk.ts,
   which is what rtk.test.ts imports from.

2. createFakeRtk on Windows wrote '%~dp0\fake-rtk.js' in the .cmd content —
   this resolves relative to the .cmd file's own directory. When the test
   copies rtk.cmd to a different managed dir, %~dp0 resolves to the copy
   destination where fake-rtk.js does not exist. Fixed by embedding the
   absolute path to fake-rtk.js directly in the .cmd content so the fake
   works correctly regardless of where the .cmd is copied.

* feat(experimental): add RTK opt-in preference with web UI toggle

- Add `experimental` category to GSDPreferences with `rtk: boolean` (default: false)
- RTK is now opt-in: disabled by default for all projects unless explicitly enabled
- Validate experimental.* keys; unknown experimental keys produce warnings

Web UI:
- Add ExperimentalPanel component with animated toggle switch per flag
- Add /api/experimental route (GET/PATCH) to read/write flags in preferences.md
- Add 'Experimental' tab to settings dialog sidebar nav (FlaskConical icon)
- Include ExperimentalPanel at bottom of gsd-prefs mega-scroll
- Fix toggle disabled state: trigger loadSettingsData for 'experimental' section
  and self-fetch on mount when data is absent

Dashboard:
- Gate RTK Saved metric card on rtkEnabled from live auto state (web)
- Gate TUI dashboard RTK savings row on rtkEnabled
- Gate TUI footer RTK status updates on experimental.rtk preference
- Propagate rtkEnabled through AutoDashboardData → bridge-service → store

Build:
- Add scripts/build-if-stale.cjs: incremental build driver that skips each
  step (packages, root tsc, copy-resources, web) when output is newer than
  source; replaces full rebuild chain in gsd:web
- Add scripts/web-stop.cjs: robust stop with registry + legacy PID + orphan
  sweep via pgrep; handles crash/restart orphaned next-server processes
- gsd:web now uses build-if-stale.cjs (fast cold starts, instant when unchanged)
- gsd:web:stop / gsd:web:stop:all use web-stop.cjs directly

Fix: correct import path in rtk-status.ts (./preferences.js not ../preferences.js)

* fix: restore em-dash encoding in package.json to match upstream

* refactor(rtk): move command rewrite out of pi-coding-agent into GSD extension

Per review feedback from igouss: pi-coding-agent should not be modified to add
GSD-specific logic. Instead, add a proper extension point and wire RTK through it.

Changes to packages/pi-coding-agent (extension API only — no RTK logic):
- Add BashTransformEvent + BashTransformEventResult types to extension API
- Add on('bash_transform') overload to ExtensionAPI interface
- Add emitBashTransform() to ExtensionRunner (chains all handlers in order)
- Call emitBashTransform() in wrapToolWithExtensions before bash tool execution
- Export new types from extensions/index.ts and package index.ts
- Revert all RTK-specific changes from bash-executor.ts, tools/bash.ts
- Remove packages/pi-coding-agent/src/utils/rtk.ts entirely

Changes to GSD extension:
- Register bash_transform handler in register-hooks.ts that calls
  rewriteCommandWithRtk() from the existing shared/rtk.ts module
- Handler is a no-op when RTK is disabled or not installed

* fix: correct import path for shared/rtk.js in register-hooks

* fix(tests): remove deleted pi-coding-agent/utils/rtk imports from execution seams test

The RTK rewrite logic was moved out of pi-coding-agent into the GSD
extension (bash_transform hook). Tests that directly imported the
deleted utils/rtk.ts are removed; remaining tests verify the shared
RTK module and GSD-layer surfaces that still call rewriteCommandWithRtk.
2026-03-26 09:33:07 -06:00

1108 lines
33 KiB
TypeScript

import type { BrowserSlashCommandDispatchResult, BrowserSlashCommandSurface } from "./browser-slash-command-dispatch"
import type { DoctorFixResult, DoctorReport, ForensicReport, SkillHealthReport } from "./diagnostics-types"
import type { KnowledgeData, CapturesData, CaptureResolveResult } from "./knowledge-captures-types"
import type { SettingsData } from "./settings-types"
import type {
HistoryData,
InspectData,
HooksData,
ExportResult,
UndoInfo,
CleanupData,
SteerData,
} from "./remaining-command-types"
import type { GitSummaryResponse } from "./git-summary-contract"
import type {
SessionBrowserNameFilter,
SessionBrowserSession,
SessionBrowserSortMode,
} from "./session-browser-contract"
export const COMMAND_SURFACE_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const
export type CommandSurfaceThinkingLevel = (typeof COMMAND_SURFACE_THINKING_LEVELS)[number]
export type CommandSurfaceSection =
| "general"
| "model"
| "thinking"
| "queue"
| "compaction"
| "retry"
| "session-behavior"
| "recovery"
| "auth"
| "admin"
| "git"
| "resume"
| "name"
| "fork"
| "session"
| "compact"
| "workspace"
| "integrations"
| "experimental"
// GSD subcommand surfaces (S02)
| "gsd-status"
| "gsd-visualize"
| "gsd-forensics"
| "gsd-doctor"
| "gsd-skill-health"
| "gsd-knowledge"
| "gsd-capture"
| "gsd-triage"
| "gsd-quick"
| "gsd-history"
| "gsd-undo"
| "gsd-inspect"
| "gsd-prefs"
| "gsd-config"
| "gsd-hooks"
| "gsd-mode"
| "gsd-steer"
| "gsd-export"
| "gsd-cleanup"
| "gsd-queue"
export type CommandSurfaceSource = "slash" | "sidebar" | "surface"
export type CommandSurfacePendingAction =
| "loading_models"
| "set_model"
| "set_thinking_level"
| "set_steering_mode"
| "set_follow_up_mode"
| "set_auto_compaction"
| "set_auto_retry"
| "abort_retry"
| "load_git_summary"
| "load_recovery_diagnostics"
| "load_session_browser"
| "rename_session"
| "save_api_key"
| "start_provider_flow"
| "submit_provider_flow_input"
| "cancel_provider_flow"
| "logout_provider"
| "switch_session"
| "load_fork_messages"
| "fork_session"
| "load_session_stats"
| "export_html"
| "compact_session"
export interface CommandSurfaceModelOption {
provider: string
modelId: string
name?: string
reasoning: boolean
isCurrent: boolean
}
export interface CommandSurfaceForkMessage {
entryId: string
text: string
}
export interface CommandSurfaceSessionStats {
sessionFile: string | undefined
sessionId: string
userMessages: number
assistantMessages: number
toolCalls: number
toolResults: number
totalMessages: number
tokens: {
input: number
output: number
cacheRead: number
cacheWrite: number
total: number
}
cost: number
}
export interface CommandSurfaceCompactionResult {
summary: string
firstKeptEntryId: string
tokensBefore: number
details?: unknown
}
export interface CommandSurfaceResumableSession {
id: string
path: string
name?: string
isActive: boolean
}
export interface CommandSurfaceSessionBrowserState {
scope: "current_project" | null
projectCwd: string | null
projectSessionsDir: string | null
activeSessionPath: string | null
query: string
sortMode: SessionBrowserSortMode
nameFilter: SessionBrowserNameFilter
totalSessions: number
returnedSessions: number
sessions: SessionBrowserSession[]
loaded: boolean
error: string | null
}
export interface CommandSurfaceSessionMutationState {
pending: boolean
sessionPath: string | null
result: string | null
error: string | null
}
export interface CommandSurfaceSettingMutationState {
pending: boolean
result: string | null
error: string | null
}
export interface CommandSurfaceSettingsMutationState {
steeringMode: CommandSurfaceSettingMutationState
followUpMode: CommandSurfaceSettingMutationState
autoCompaction: CommandSurfaceSettingMutationState
autoRetry: CommandSurfaceSettingMutationState
abortRetry: CommandSurfaceSettingMutationState
}
export interface CommandSurfaceGitSummaryState {
pending: boolean
loaded: boolean
result: GitSummaryResponse | null
error: string | null
}
export type WorkspaceRecoverySummaryTone = "healthy" | "warning" | "danger"
export type WorkspaceRecoveryDiagnosticsStatus = "ready" | "unavailable"
export type WorkspaceRecoveryBrowserActionId =
| "refresh_diagnostics"
| "refresh_workspace"
| "open_retry_controls"
| "open_resume_controls"
| "open_auth_controls"
export type CommandSurfaceRecoveryPhase = "idle" | "loading" | "ready" | "unavailable" | "error"
export interface WorkspaceRecoveryBrowserAction {
id: WorkspaceRecoveryBrowserActionId
label: string
detail: string
emphasis?: "primary" | "secondary" | "danger"
}
export interface WorkspaceRecoveryCommandSuggestion {
label: string
command: string
}
export interface WorkspaceRecoveryCodeSummary {
code: string
count: number
label: string
severity: "info" | "warning" | "error"
}
export interface WorkspaceRecoveryIssueDigest {
code: string
severity: "info" | "warning" | "error"
scope: string
message: string
file?: string
suggestion?: string
unitId?: string
}
export interface WorkspaceRecoveryDiagnostics {
status: WorkspaceRecoveryDiagnosticsStatus
loadedAt: string
project: {
cwd: string
activeScope: string | null
activeSessionPath: string | null
activeSessionId: string | null
}
summary: {
tone: WorkspaceRecoverySummaryTone
label: string
detail: string
validationCount: number
doctorIssueCount: number
lastFailurePhase: string | null
currentUnitId: string | null
retryAttempt: number
retryInProgress: boolean
compactionActive: boolean
}
bridge: {
phase: string
retry: {
enabled: boolean
inProgress: boolean
attempt: number
label: string
}
compaction: {
active: boolean
label: string
}
lastFailure: {
message: string
phase: string
at: string
commandType: string | null
afterSessionAttachment: boolean
} | null
authRefresh: {
phase: string
error: string | null
label: string
}
}
validation: {
total: number
bySeverity: {
errors: number
warnings: number
infos: number
}
codes: WorkspaceRecoveryCodeSummary[]
topIssues: WorkspaceRecoveryIssueDigest[]
}
doctor: {
scope: string | null
total: number
errors: number
warnings: number
infos: number
fixable: number
codes: Array<{ code: string; count: number }>
topIssues: WorkspaceRecoveryIssueDigest[]
}
interruptedRun: {
available: boolean
detected: boolean
label: string
detail: string
unit: {
type: string
id: string
} | null
counts: {
toolCalls: number
filesWritten: number
commandsRun: number
errors: number
}
gitChangesDetected: boolean
lastError: string | null
}
actions: {
browser: WorkspaceRecoveryBrowserAction[]
commands: WorkspaceRecoveryCommandSuggestion[]
}
}
export interface CommandSurfaceRecoveryState {
phase: CommandSurfaceRecoveryPhase
pending: boolean
loaded: boolean
stale: boolean
diagnostics: WorkspaceRecoveryDiagnostics | null
error: string | null
lastLoadedAt: string | null
lastInvalidatedAt: string | null
lastFailureAt: string | null
}
export interface WorkspaceRecoverySummary {
visible: boolean
tone: WorkspaceRecoverySummaryTone
label: string
detail: string
validationCount: number
retryInProgress: boolean
retryAttempt: number
autoRetryEnabled: boolean
isCompacting: boolean
currentUnitId: string | null
freshness: "idle" | "fresh" | "stale" | "error"
entrypointLabel: string
lastError: {
message: string
phase: string
at: string
} | null
}
export type CommandSurfaceTarget =
| { kind: "settings"; section: CommandSurfaceSection }
| { kind: "model"; provider?: string; modelId?: string; query?: string }
| { kind: "thinking"; level: CommandSurfaceThinkingLevel }
| { kind: "auth"; providerId?: string; intent: "login" | "logout" | "manage" }
| { kind: "resume"; sessionPath?: string }
| { kind: "name"; sessionPath?: string; name: string }
| { kind: "fork"; entryId?: string }
| { kind: "session"; outputPath?: string }
| { kind: "compact"; customInstructions: string }
| { kind: "gsd"; surface: string; subcommand: string; args: string }
// ─── Diagnostics panel state ──────────────────────────────────────────────────
export type CommandSurfaceDiagnosticsPhase = "idle" | "loading" | "loaded" | "error"
export interface CommandSurfaceDiagnosticsPhaseState<T> {
phase: CommandSurfaceDiagnosticsPhase
data: T | null
error: string | null
lastLoadedAt: string | null
}
export interface CommandSurfaceDoctorState extends CommandSurfaceDiagnosticsPhaseState<DoctorReport> {
fixPending: boolean
lastFixResult: DoctorFixResult | null
lastFixError: string | null
}
export interface CommandSurfaceDiagnosticsState {
forensics: CommandSurfaceDiagnosticsPhaseState<ForensicReport>
doctor: CommandSurfaceDoctorState
skillHealth: CommandSurfaceDiagnosticsPhaseState<SkillHealthReport>
}
export function createInitialDiagnosticsPhaseState<T>(): CommandSurfaceDiagnosticsPhaseState<T> {
return { phase: "idle", data: null, error: null, lastLoadedAt: null }
}
export function createInitialDoctorState(): CommandSurfaceDoctorState {
return { phase: "idle", data: null, error: null, lastLoadedAt: null, fixPending: false, lastFixResult: null, lastFixError: null }
}
export function createInitialDiagnosticsState(): CommandSurfaceDiagnosticsState {
return {
forensics: createInitialDiagnosticsPhaseState<ForensicReport>(),
doctor: createInitialDoctorState(),
skillHealth: createInitialDiagnosticsPhaseState<SkillHealthReport>(),
}
}
// ─── Knowledge/Captures panel state ──────────────────────────────────────────
export interface CommandSurfaceKnowledgeCapturesResolveState {
pending: boolean
lastError: string | null
lastResult: CaptureResolveResult | null
}
export interface CommandSurfaceKnowledgeCapturesState {
knowledge: CommandSurfaceDiagnosticsPhaseState<KnowledgeData>
captures: CommandSurfaceDiagnosticsPhaseState<CapturesData>
resolveRequest: CommandSurfaceKnowledgeCapturesResolveState
}
export function createInitialKnowledgeCapturesState(): CommandSurfaceKnowledgeCapturesState {
return {
knowledge: createInitialDiagnosticsPhaseState<KnowledgeData>(),
captures: createInitialDiagnosticsPhaseState<CapturesData>(),
resolveRequest: { pending: false, lastError: null, lastResult: null },
}
}
// ─── Settings panel state ────────────────────────────────────────────────────
export type CommandSurfaceSettingsState = CommandSurfaceDiagnosticsPhaseState<SettingsData>
export function createInitialSettingsState(): CommandSurfaceSettingsState {
return createInitialDiagnosticsPhaseState<SettingsData>()
}
// ─── Remaining command surfaces state ────────────────────────────────────────
export interface CommandSurfaceRemainingState {
history: CommandSurfaceDiagnosticsPhaseState<HistoryData>
inspect: CommandSurfaceDiagnosticsPhaseState<InspectData>
hooks: CommandSurfaceDiagnosticsPhaseState<HooksData>
exportData: CommandSurfaceDiagnosticsPhaseState<ExportResult>
undo: CommandSurfaceDiagnosticsPhaseState<UndoInfo>
cleanup: CommandSurfaceDiagnosticsPhaseState<CleanupData>
steer: CommandSurfaceDiagnosticsPhaseState<SteerData>
}
export function createInitialRemainingState(): CommandSurfaceRemainingState {
return {
history: createInitialDiagnosticsPhaseState<HistoryData>(),
inspect: createInitialDiagnosticsPhaseState<InspectData>(),
hooks: createInitialDiagnosticsPhaseState<HooksData>(),
exportData: createInitialDiagnosticsPhaseState<ExportResult>(),
undo: createInitialDiagnosticsPhaseState<UndoInfo>(),
cleanup: createInitialDiagnosticsPhaseState<CleanupData>(),
steer: createInitialDiagnosticsPhaseState<SteerData>(),
}
}
export interface WorkspaceCommandSurfaceState {
open: boolean
activeSurface: BrowserSlashCommandSurface | null
source: CommandSurfaceSource | null
section: CommandSurfaceSection | null
args: string
pendingAction: CommandSurfacePendingAction | null
selectedTarget: CommandSurfaceTarget | null
lastError: string | null
lastResult: string | null
availableModels: CommandSurfaceModelOption[]
forkMessages: CommandSurfaceForkMessage[]
sessionStats: CommandSurfaceSessionStats | null
lastCompaction: CommandSurfaceCompactionResult | null
gitSummary: CommandSurfaceGitSummaryState
recovery: CommandSurfaceRecoveryState
diagnostics: CommandSurfaceDiagnosticsState
knowledgeCaptures: CommandSurfaceKnowledgeCapturesState
settingsData: CommandSurfaceSettingsState
remainingCommands: CommandSurfaceRemainingState
sessionBrowser: CommandSurfaceSessionBrowserState
resumeRequest: CommandSurfaceSessionMutationState
renameRequest: CommandSurfaceSessionMutationState
settingsRequests: CommandSurfaceSettingsMutationState
}
export interface CommandSurfaceOpenContext {
onboardingLocked?: boolean
currentModel?: { provider?: string; modelId?: string } | null
currentThinkingLevel?: string | null
preferredProviderId?: string | null
resumableSessions?: CommandSurfaceResumableSession[]
currentSessionPath?: string | null
currentSessionName?: string | null
projectCwd?: string | null
projectSessionsDir?: string | null
}
export interface CommandSurfaceOpenRequest extends CommandSurfaceOpenContext {
surface: BrowserSlashCommandSurface
source: CommandSurfaceSource
args?: string
selectedTarget?: CommandSurfaceTarget | null
}
export interface CommandSurfaceActionResult {
action: CommandSurfacePendingAction
success: boolean
message: string
selectedTarget?: CommandSurfaceTarget | null
availableModels?: CommandSurfaceModelOption[]
forkMessages?: CommandSurfaceForkMessage[]
sessionStats?: CommandSurfaceSessionStats | null
lastCompaction?: CommandSurfaceCompactionResult | null
gitSummary?: CommandSurfaceGitSummaryState
recovery?: CommandSurfaceRecoveryState
sessionBrowser?: CommandSurfaceSessionBrowserState
}
const AUTH_SURFACE_COMMANDS = new Set<BrowserSlashCommandSurface>(["settings", "login", "logout"])
const SETTINGS_MUTATION_ACTION_TO_REQUEST: Partial<
Record<CommandSurfacePendingAction, keyof CommandSurfaceSettingsMutationState>
> = {
set_steering_mode: "steeringMode",
set_follow_up_mode: "followUpMode",
set_auto_compaction: "autoCompaction",
set_auto_retry: "autoRetry",
abort_retry: "abortRetry",
}
function matchingSessionPath(
sessions: CommandSurfaceResumableSession[] | undefined,
query: string | undefined,
): string | undefined {
if (!sessions?.length) return undefined
const normalizedQuery = query?.trim().toLowerCase()
if (!normalizedQuery) {
return sessions.find((session) => !session.isActive)?.path ?? sessions[0]?.path
}
const exactMatch = sessions.find((session) => {
const values = [session.id, session.name, session.path].filter(Boolean).map((value) => value!.toLowerCase())
return values.includes(normalizedQuery)
})
if (exactMatch) return exactMatch.path
return sessions.find((session) => {
const values = [session.id, session.name, session.path].filter(Boolean).map((value) => value!.toLowerCase())
return values.some((value) => value.includes(normalizedQuery))
})?.path
}
function createInitialCommandSurfaceSessionBrowserState(
overrides: Partial<CommandSurfaceSessionBrowserState> = {},
): CommandSurfaceSessionBrowserState {
return {
scope: null,
projectCwd: null,
projectSessionsDir: null,
activeSessionPath: null,
query: "",
sortMode: "threaded",
nameFilter: "all",
totalSessions: 0,
returnedSessions: 0,
sessions: [],
loaded: false,
error: null,
...overrides,
}
}
function createInitialCommandSurfaceSessionMutationState(): CommandSurfaceSessionMutationState {
return {
pending: false,
sessionPath: null,
result: null,
error: null,
}
}
function createInitialCommandSurfaceSettingMutationState(): CommandSurfaceSettingMutationState {
return {
pending: false,
result: null,
error: null,
}
}
function createInitialCommandSurfaceSettingsMutationState(): CommandSurfaceSettingsMutationState {
return {
steeringMode: createInitialCommandSurfaceSettingMutationState(),
followUpMode: createInitialCommandSurfaceSettingMutationState(),
autoCompaction: createInitialCommandSurfaceSettingMutationState(),
autoRetry: createInitialCommandSurfaceSettingMutationState(),
abortRetry: createInitialCommandSurfaceSettingMutationState(),
}
}
function createInitialCommandSurfaceGitSummaryState(): CommandSurfaceGitSummaryState {
return {
pending: false,
loaded: false,
result: null,
error: null,
}
}
export function createInitialCommandSurfaceRecoveryState(): CommandSurfaceRecoveryState {
return {
phase: "idle",
pending: false,
loaded: false,
stale: false,
diagnostics: null,
error: null,
lastLoadedAt: null,
lastInvalidatedAt: null,
lastFailureAt: null,
}
}
function buildInitialSessionBrowserState(request: CommandSurfaceOpenRequest): CommandSurfaceSessionBrowserState {
const initialQuery = request.surface === "resume" ? request.args?.trim() ?? "" : ""
return createInitialCommandSurfaceSessionBrowserState({
activeSessionPath: request.currentSessionPath ?? null,
projectCwd: request.projectCwd ?? null,
projectSessionsDir: request.projectSessionsDir ?? null,
query: initialQuery,
sortMode: initialQuery ? "relevance" : "threaded",
})
}
export function isCommandSurfaceThinkingLevel(value: string | null | undefined): value is CommandSurfaceThinkingLevel {
return COMMAND_SURFACE_THINKING_LEVELS.includes((value ?? "") as CommandSurfaceThinkingLevel)
}
export function createInitialCommandSurfaceState(): WorkspaceCommandSurfaceState {
return {
open: false,
activeSurface: null,
source: null,
section: null,
args: "",
pendingAction: null,
selectedTarget: null,
lastError: null,
lastResult: null,
availableModels: [],
forkMessages: [],
sessionStats: null,
lastCompaction: null,
gitSummary: createInitialCommandSurfaceGitSummaryState(),
recovery: createInitialCommandSurfaceRecoveryState(),
diagnostics: createInitialDiagnosticsState(),
knowledgeCaptures: createInitialKnowledgeCapturesState(),
settingsData: createInitialSettingsState(),
remainingCommands: createInitialRemainingState(),
sessionBrowser: createInitialCommandSurfaceSessionBrowserState(),
resumeRequest: createInitialCommandSurfaceSessionMutationState(),
renameRequest: createInitialCommandSurfaceSessionMutationState(),
settingsRequests: createInitialCommandSurfaceSettingsMutationState(),
}
}
export function commandSurfaceSectionForRequest(request: CommandSurfaceOpenRequest): CommandSurfaceSection | null {
switch (request.surface) {
case "model":
return "model"
case "thinking":
return "thinking"
case "settings":
return request.onboardingLocked ? "auth" : "general"
case "git":
return "git"
case "login":
case "logout":
return "auth"
case "resume":
return "resume"
case "name":
return "name"
case "fork":
return "fork"
case "session":
case "export":
return "session"
case "compact":
return "compact"
// GSD subcommand surfaces (S02)
case "gsd-status": return "gsd-status"
case "gsd-visualize": return "gsd-visualize"
case "gsd-forensics": return "gsd-forensics"
case "gsd-doctor": return "gsd-doctor"
case "gsd-skill-health": return "gsd-skill-health"
case "gsd-knowledge": return "gsd-knowledge"
case "gsd-capture": return "gsd-capture"
case "gsd-triage": return "gsd-triage"
case "gsd-quick": return "gsd-quick"
case "gsd-history": return "gsd-history"
case "gsd-undo": return "gsd-undo"
case "gsd-inspect": return "gsd-inspect"
case "gsd-prefs": return "gsd-prefs"
case "gsd-config": return "gsd-config"
case "gsd-hooks": return "gsd-hooks"
case "gsd-mode": return "gsd-mode"
case "gsd-steer": return "gsd-steer"
case "gsd-export": return "gsd-export"
case "gsd-cleanup": return "gsd-cleanup"
case "gsd-queue": return "gsd-queue"
default:
return null
}
}
function buildSettingsTarget(section: CommandSurfaceSection): CommandSurfaceTarget {
return { kind: "settings", section }
}
function buildModelTarget(request: CommandSurfaceOpenRequest): CommandSurfaceTarget {
const query = request.args?.trim() || undefined
return {
kind: "model",
provider: request.currentModel?.provider,
modelId: request.currentModel?.modelId,
query,
}
}
function buildThinkingTarget(request: CommandSurfaceOpenRequest): CommandSurfaceTarget {
const requestedLevel = request.args?.trim().toLowerCase() || ""
const level = isCommandSurfaceThinkingLevel(requestedLevel)
? requestedLevel
: isCommandSurfaceThinkingLevel(request.currentThinkingLevel)
? request.currentThinkingLevel
: "off"
return {
kind: "thinking",
level,
}
}
function buildAuthTarget(request: CommandSurfaceOpenRequest): CommandSurfaceTarget {
const requestedProviderId = request.args?.trim() || undefined
return {
kind: "auth",
providerId: requestedProviderId ?? request.preferredProviderId ?? undefined,
intent: request.surface === "login" ? "login" : request.surface === "logout" ? "logout" : "manage",
}
}
function buildResumeTarget(request: CommandSurfaceOpenRequest): Extract<CommandSurfaceTarget, { kind: "resume" }> {
const selectedPath = matchingSessionPath(request.resumableSessions, request.args)
return {
kind: "resume",
sessionPath: selectedPath,
}
}
function buildNameTarget(request: CommandSurfaceOpenRequest): CommandSurfaceTarget {
const providedName = request.args?.trim()
return {
kind: "name",
sessionPath: request.currentSessionPath ?? undefined,
name: providedName !== undefined && providedName.length > 0 ? providedName : request.currentSessionName?.trim() ?? "",
}
}
function buildForkTarget(request: CommandSurfaceOpenRequest): CommandSurfaceTarget {
const entryId = request.args?.trim() || undefined
return {
kind: "fork",
entryId,
}
}
function buildSessionTarget(request: CommandSurfaceOpenRequest): CommandSurfaceTarget {
const outputPath = request.args?.trim() || undefined
return {
kind: "session",
outputPath,
}
}
function buildCompactTarget(request: CommandSurfaceOpenRequest): CommandSurfaceTarget {
return {
kind: "compact",
customInstructions: request.args?.trim() ?? "",
}
}
export function buildCommandSurfaceTarget(request: CommandSurfaceOpenRequest): CommandSurfaceTarget | null {
if (request.selectedTarget !== undefined) {
return request.selectedTarget
}
const section = commandSurfaceSectionForRequest(request)
if (!section) return null
if (request.surface === "settings") {
return buildSettingsTarget(section)
}
if (request.surface === "model") {
return buildModelTarget(request)
}
if (request.surface === "thinking") {
return buildThinkingTarget(request)
}
if (AUTH_SURFACE_COMMANDS.has(request.surface)) {
return buildAuthTarget(request)
}
if (request.surface === "resume") {
return buildResumeTarget(request)
}
if (request.surface === "name") {
return buildNameTarget(request)
}
if (request.surface === "fork") {
return buildForkTarget(request)
}
if (request.surface === "session" || request.surface === "export") {
return buildSessionTarget(request)
}
if (request.surface === "compact") {
return buildCompactTarget(request)
}
// GSD subcommand surfaces — generic target (S02)
if (request.surface?.startsWith("gsd-")) {
const subcommand = request.surface.slice(4) // "gsd-forensics" -> "forensics"
return { kind: "gsd", surface: request.surface, subcommand, args: request.args ?? "" }
}
return buildSettingsTarget(section)
}
export function openCommandSurfaceState(
current: WorkspaceCommandSurfaceState,
request: CommandSurfaceOpenRequest,
): WorkspaceCommandSurfaceState {
const section = commandSurfaceSectionForRequest(request)
return {
...current,
open: true,
activeSurface: request.surface,
source: request.source,
section,
args: request.args?.trim() ?? "",
pendingAction: null,
selectedTarget: buildCommandSurfaceTarget(request),
lastError: null,
lastResult: null,
sessionStats: null,
forkMessages: [],
lastCompaction: null,
gitSummary: createInitialCommandSurfaceGitSummaryState(),
recovery: createInitialCommandSurfaceRecoveryState(),
diagnostics: createInitialDiagnosticsState(),
knowledgeCaptures: createInitialKnowledgeCapturesState(),
settingsData: createInitialSettingsState(),
remainingCommands: createInitialRemainingState(),
sessionBrowser: buildInitialSessionBrowserState(request),
resumeRequest: createInitialCommandSurfaceSessionMutationState(),
renameRequest: createInitialCommandSurfaceSessionMutationState(),
settingsRequests: createInitialCommandSurfaceSettingsMutationState(),
}
}
export function closeCommandSurfaceState(current: WorkspaceCommandSurfaceState): WorkspaceCommandSurfaceState {
return {
...current,
open: false,
pendingAction: null,
}
}
export function setCommandSurfaceSection(
current: WorkspaceCommandSurfaceState,
section: CommandSurfaceSection,
context: CommandSurfaceOpenContext = {},
): WorkspaceCommandSurfaceState {
const request: CommandSurfaceOpenRequest = {
surface: current.activeSurface ?? "settings",
source: current.source ?? "surface",
args: current.args,
...context,
}
const currentSessionPath =
current.selectedTarget?.kind === "resume"
? current.selectedTarget.sessionPath
: current.selectedTarget?.kind === "name"
? current.selectedTarget.sessionPath
: undefined
const currentDraftName = current.selectedTarget?.kind === "name" ? current.selectedTarget.name : undefined
let selectedTarget: CommandSurfaceTarget | null = current.selectedTarget
if (section === "model") {
selectedTarget = buildModelTarget(request)
} else if (section === "thinking") {
selectedTarget = buildThinkingTarget(request)
} else if (section === "general" || section === "session-behavior" || section === "queue" || section === "compaction" || section === "retry" || section === "recovery" || section === "git" || section === "admin") {
selectedTarget = buildSettingsTarget(section)
} else if (section === "auth") {
selectedTarget = buildAuthTarget({
...request,
surface:
current.activeSurface === "logout"
? "logout"
: current.activeSurface === "login"
? "login"
: "settings",
})
} else if (section === "resume") {
selectedTarget = { kind: "resume", sessionPath: currentSessionPath ?? buildResumeTarget(request).sessionPath }
} else if (section === "name") {
selectedTarget = {
kind: "name",
sessionPath: currentSessionPath ?? request.currentSessionPath ?? undefined,
name: currentDraftName ?? request.currentSessionName?.trim() ?? "",
}
} else if (section === "fork") {
selectedTarget = buildForkTarget(request)
} else if (section === "session") {
selectedTarget = buildSessionTarget(request)
} else if (section === "compact") {
selectedTarget = buildCompactTarget(request)
}
return {
...current,
section,
selectedTarget,
}
}
export function selectCommandSurfaceStateTarget(
current: WorkspaceCommandSurfaceState,
target: CommandSurfaceTarget,
): WorkspaceCommandSurfaceState {
const nextSection =
target.kind === "settings"
? target.section
: target.kind === "model"
? "model"
: target.kind === "thinking"
? "thinking"
: target.kind === "auth"
? "auth"
: target.kind === "resume"
? "resume"
: target.kind === "name"
? "name"
: target.kind === "fork"
? "fork"
: target.kind === "session"
? "session"
: "compact"
return {
...current,
section: nextSection,
selectedTarget: target,
lastError: null,
lastResult: null,
}
}
export function setCommandSurfacePending(
current: WorkspaceCommandSurfaceState,
action: CommandSurfacePendingAction,
selectedTarget: CommandSurfaceTarget | null = current.selectedTarget,
): WorkspaceCommandSurfaceState {
const nextResumeRequest =
action === "switch_session"
? {
pending: true,
sessionPath: selectedTarget?.kind === "resume" ? selectedTarget.sessionPath ?? null : null,
result: null,
error: null,
}
: current.resumeRequest
const nextRenameRequest =
action === "rename_session"
? {
pending: true,
sessionPath: selectedTarget?.kind === "name" ? selectedTarget.sessionPath ?? null : null,
result: null,
error: null,
}
: current.renameRequest
const settingsRequestKey = SETTINGS_MUTATION_ACTION_TO_REQUEST[action]
const nextSettingsRequests = settingsRequestKey
? {
...current.settingsRequests,
[settingsRequestKey]: {
pending: true,
result: null,
error: null,
},
}
: current.settingsRequests
return {
...current,
pendingAction: action,
selectedTarget,
lastError: null,
lastResult: null,
gitSummary:
action === "load_git_summary"
? {
...current.gitSummary,
pending: true,
error: null,
}
: current.gitSummary,
recovery:
action === "load_recovery_diagnostics"
? {
...current.recovery,
pending: true,
error: null,
phase: current.recovery.loaded ? current.recovery.phase : "loading",
}
: current.recovery,
sessionBrowser:
action === "load_session_browser"
? {
...current.sessionBrowser,
error: null,
}
: current.sessionBrowser,
resumeRequest: nextResumeRequest,
renameRequest: nextRenameRequest,
settingsRequests: nextSettingsRequests,
}
}
export function applyCommandSurfaceActionResult(
current: WorkspaceCommandSurfaceState,
result: CommandSurfaceActionResult,
): WorkspaceCommandSurfaceState {
const nextSelectedTarget = result.selectedTarget === undefined ? current.selectedTarget : result.selectedTarget
const resumeSessionPath =
(nextSelectedTarget?.kind === "resume" ? nextSelectedTarget.sessionPath : undefined) ?? current.resumeRequest.sessionPath
const renameSessionPath =
(nextSelectedTarget?.kind === "name" ? nextSelectedTarget.sessionPath : undefined) ?? current.renameRequest.sessionPath
const settingsRequestKey = SETTINGS_MUTATION_ACTION_TO_REQUEST[result.action]
const nextSettingsRequests = settingsRequestKey
? {
...current.settingsRequests,
[settingsRequestKey]: {
pending: false,
result: result.success ? result.message : null,
error: result.success ? null : result.message,
},
}
: current.settingsRequests
return {
...current,
pendingAction: null,
selectedTarget: nextSelectedTarget,
availableModels: result.availableModels ?? current.availableModels,
forkMessages: result.forkMessages ?? current.forkMessages,
sessionStats: result.sessionStats === undefined ? current.sessionStats : result.sessionStats,
lastCompaction: result.lastCompaction === undefined ? current.lastCompaction : result.lastCompaction,
gitSummary:
result.gitSummary === undefined
? current.gitSummary
: {
...result.gitSummary,
pending: false,
loaded: result.gitSummary.loaded || result.success,
},
recovery: result.recovery ?? current.recovery,
sessionBrowser: result.sessionBrowser ?? current.sessionBrowser,
resumeRequest:
result.action === "switch_session"
? {
pending: false,
sessionPath: resumeSessionPath ?? null,
result: result.success ? result.message : null,
error: result.success ? null : result.message,
}
: current.resumeRequest,
renameRequest:
result.action === "rename_session"
? {
pending: false,
sessionPath: renameSessionPath ?? null,
result: result.success ? result.message : null,
error: result.success ? null : result.message,
}
: current.renameRequest,
settingsRequests: nextSettingsRequests,
lastError: result.success ? null : result.message,
lastResult: result.success ? result.message : null,
}
}
export function surfaceOutcomeToOpenRequest(
outcome: Extract<BrowserSlashCommandDispatchResult, { kind: "surface" }>,
context: CommandSurfaceOpenContext = {},
): CommandSurfaceOpenRequest {
return {
surface: outcome.surface,
source: "slash",
args: outcome.args,
...context,
}
}