diff --git a/.claude/skills/ink-tui/SKILL.md b/.claude/skills/ink-tui/SKILL.md index d8e900b27..aac75ba5d 100644 --- a/.claude/skills/ink-tui/SKILL.md +++ b/.claude/skills/ink-tui/SKILL.md @@ -51,7 +51,7 @@ service wiring depends on the screen's needs; `App` remains the shared shell. Business logic calls [WizardUI](../../../src/store/ui/wizard-ui.ts) through [getUI](../../../src/store/ui/index.ts). Screens use -[WizardStore](../../../src/store/state/store.ts) setters for reactive changes. +[FlowStore](../../../src/store/state/store.ts) setters for reactive changes. The router resolves program screens from session predicates; overlays interrupt that resolution. Local state is appropriate for presentation details such as tab selection, not wizard progression. diff --git a/.claude/skills/ink-tui/references/ARCHITECTURE.md b/.claude/skills/ink-tui/references/ARCHITECTURE.md index d07484ae0..fae6fd95f 100644 --- a/.claude/skills/ink-tui/references/ARCHITECTURE.md +++ b/.claude/skills/ink-tui/references/ARCHITECTURE.md @@ -8,7 +8,7 @@ | [ProgramStep](../../../../src/store/programs/program-step.ts) | Screens, visibility/completion predicates, gates, and initialization hooks | | [screen-sequences.ts](../../../../src/tui/screen-sequences.ts) | `ScreenId`, `Screen`, `Sequence`, and the derived `PROGRAM_SEQUENCES` | | [WizardRouter](../../../../src/tui/router.ts) | Resolution and the `Overlay` stack | -| [WizardStore](../../../../src/store/state/store.ts) | Reactive state, gate promises, display observations, and pending interactions | +| [FlowStore](../../../../src/store/state/store.ts) | Reactive state, gate promises, display observations, and pending interactions | | [WizardUI](../../../../src/store/ui/wizard-ui.ts) | Typed operations available to business logic | ## Program screens diff --git a/e2e-harness/ARCHITECTURE.md b/e2e-harness/ARCHITECTURE.md index cecd792d9..b59759677 100644 --- a/e2e-harness/ARCHITECTURE.md +++ b/e2e-harness/ARCHITECTURE.md @@ -42,11 +42,11 @@ scripts/ wizard-ci-explore.no-jest.ts open an app, confirm setup, print one frame ``` -The server reads and mutates the **real** `WizardStore` the TUI renders from. -The router resolves the active screen from session state, every action goes -through a store setter, and the render is a pure projection of that state. So a -commit over the socket makes the real TUI react, and the parent never touches -the TUI's input. +The server reads and mutates the **real** `FlowStore` the TUI renders from. The +router resolves the active screen from session state, every action goes through +a store setter, and the render is a pure projection of that state. So a commit +over the socket makes the real TUI react, and the parent never touches the TUI's +input. ## Launching a controlled wizard @@ -83,7 +83,7 @@ program), 404 (unknown route), 409 (a run is in flight), 413 (body over 64 KB), | `POST /credentials` | Resolves the API key into project credentials and commits them, advancing `auth` | | `POST /run` | TUI surface: `requestRun` on the store, releasing the runner's agent start. Idempotent | | `POST /detect` body `{ programId?, installDir? }` | Headless surface: runs detection through the store's setters | -| `POST /runs` body `{ programId, installDir?, frameworkContext?, skillId? }` | Headless surface: one independent agent run; 409 while one runs | +| `POST /runs` body `{ programId, installDir?, frameworkContext?, skillId? }` | Headless surface: one independent agent run on a fresh `RunStore`; 409 while one runs | | `GET /runs` | The run ledger: `runId`, `programId`, `installDir`, `status`, `error`, timestamps, `result` | | `POST /shutdown` | Flushes and exits. Idempotent | @@ -96,9 +96,10 @@ follow-up flags, `runRequested`, `runPhase`, `pendingQuestion`, `taskNotice`, redaction (`secret:` refs become `[secret-ref]`, secret-looking keys become `[redacted]`); credentials reduce to `hasCredentials` and `projectId`. `session.runPhase` is the run status and `session.outroData` carries a failed -run's reason. No access token, API key, user record, or answer value is ever -projected. A ledger record's `result` is this state as it read when the run -ended. +run's reason. `dashboardUrl` and `notebookUrl` are artefacts of the session: a +later run inherits them. No access token, API key, user record, or answer value +is ever projected. A ledger record's `result` is this state as it read when the +run ended. The socket is created mode 0600 in a directory the caller owns. A stale socket file is replaced; a live one is refused. The wizard unlinks it on exit. diff --git a/e2e-harness/__tests__/control-driver.test.ts b/e2e-harness/__tests__/control-driver.test.ts index 644552a57..517f88488 100644 --- a/e2e-harness/__tests__/control-driver.test.ts +++ b/e2e-harness/__tests__/control-driver.test.ts @@ -1,5 +1,5 @@ /** - * Control-plane test: drive a REAL WizardStore through the full integration + * Control-plane test: drive a REAL FlowStore through the full integration * screen sequence using only the ControlDriver — proving read_state is a * truthful projection of router-resolved state and that perform_action commits * cause the same transitions the interactive UI would. @@ -9,7 +9,7 @@ * through the driver. */ -import { WizardStore } from '@store/state/store'; +import { FlowStore } from '@store/state/store'; import { StoreUI } from '@store/ui/store-ui'; import { setUI } from '@store/ui'; import { @@ -28,8 +28,8 @@ import { SOURCE_MAPS_CONTEXT_KEYS } from '@store/programs/error-tracking-upload- import { OutroKind } from '@store/session/wizard-session'; import { flowFor } from '@store/programs/flow-for'; -function freshStore(): WizardStore { - const store = new WizardStore(flowFor(Program.PostHogIntegration).flow); +function freshStore(): FlowStore { + const store = new FlowStore(flowFor(Program.PostHogIntegration).flow); // Headless: a real store + StoreUI (which only forwards to the store), no Ink // render. setUI so any getUI() path the store touches resolves. setUI(new StoreUI(store)); @@ -205,8 +205,8 @@ describe('ControlDriver — wizard_ask overlay', () => { }); describe('ControlDriver — self-driving integration check', () => { - function selfDrivingStore(): WizardStore { - const store = new WizardStore(flowFor(Program.SelfDriving).flow); + function selfDrivingStore(): FlowStore { + const store = new FlowStore(flowFor(Program.SelfDriving).flow); setUI(new StoreUI(store)); store.session = buildSession({ installDir: '/tmp/ci-driver-sd', ci: true }); return store; @@ -245,8 +245,8 @@ describe('ControlDriver — self-driving integration check', () => { }); describe('ControlDriver — source-maps project pick', () => { - function sourceMapsStore(): WizardStore { - const store = new WizardStore( + function sourceMapsStore(): FlowStore { + const store = new FlowStore( flowFor(Program.ErrorTrackingUploadSourceMaps).flow, ); setUI(new StoreUI(store)); @@ -254,7 +254,7 @@ describe('ControlDriver — source-maps project pick', () => { return store; } - function toDetectScreen(store: WizardStore): void { + function toDetectScreen(store: FlowStore): void { // Intro → auth → detect. store.completeSetup(); store.setCredentials({ diff --git a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts index 16d400999..fdb466437 100644 --- a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts +++ b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts @@ -13,7 +13,7 @@ * Update goldens with `jest -u` after an intentional flow change. */ -import { WizardStore } from '@store/state/store'; +import { FlowStore } from '@store/state/store'; import { StoreUI } from '@store/ui/store-ui'; import { setUI } from '@store/ui'; import { buildSession, RunPhase } from '@store/session/wizard-session'; @@ -51,7 +51,7 @@ function traceFlow( action: string; params?: Record; }> { - const store = new WizardStore(flowFor(program).flow); + const store = new FlowStore(flowFor(program).flow); setUI(new StoreUI(store)); const session = buildSession({ installDir: '/tmp/e2e-snap', ci: true }); if (integration) { diff --git a/e2e-harness/__tests__/keyboard-equivalence.test.tsx b/e2e-harness/__tests__/keyboard-equivalence.test.tsx index 3db83fc8c..18e7b8cc3 100644 --- a/e2e-harness/__tests__/keyboard-equivalence.test.tsx +++ b/e2e-harness/__tests__/keyboard-equivalence.test.tsx @@ -6,7 +6,7 @@ import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { render, cleanup } from 'ink-testing-library'; import { - WizardStore, + FlowStore, Program, RunPhase, McpOutcome, @@ -90,11 +90,11 @@ const approved = (ok: boolean) => organization: { is_ai_data_processing_approved: ok }, } as unknown as WizardSession['apiUser']); -const confirmed = (s: WizardStore) => { +const confirmed = (s: FlowStore) => { s.completeSetup(); s.setReadinessResult(clean); }; -const authed = (s: WizardStore) => { +const authed = (s: FlowStore) => { s.setCredentials({ accessToken: 'phx_test', projectApiKey: 'phc_test', @@ -103,7 +103,7 @@ const authed = (s: WizardStore) => { }); s.setApiUser(approved(true)); }; -const ran = (s: WizardStore) => { +const ran = (s: FlowStore) => { s.setRunPhase(RunPhase.Running); s.setOutroData({ kind: OutroKind.Success, message: 'done' }); s.setRunPhase(RunPhase.Completed); @@ -123,7 +123,7 @@ interface Pair { program: ProgramId; integration?: Integration; screen: string; - arrange: (s: WizardStore) => void; + arrange: (s: FlowStore) => void; keys: string[]; action: string; params?: Record; @@ -358,8 +358,8 @@ const PAIRS: Pair[] = [ }, ]; -function makeStore(pair: Pair): WizardStore { - const store = new WizardStore(flowFor(pair.program).flow); +function makeStore(pair: Pair): FlowStore { + const store = new FlowStore(flowFor(pair.program).flow); store.version = '0.0.0-test'; setUI(new StoreUI(store)); const session = buildSession({ installDir: '/app', ci: false }); @@ -378,7 +378,7 @@ interface Snap { session: Record; } -function snap(store: WizardStore): Snap { +function snap(store: FlowStore): Snap { const session: Record = {}; for (const [k, v] of Object.entries(store.session)) { session[k] = k === 'frameworkConfig' ? (v ? '[config]' : null) : v; diff --git a/scripts/__fixtures__/chunk-manifest.ci.json b/scripts/__fixtures__/chunk-manifest.ci.json index 1f1d937b5..37b53247f 100644 --- a/scripts/__fixtures__/chunk-manifest.ci.json +++ b/scripts/__fixtures__/chunk-manifest.ci.json @@ -23,9 +23,9 @@ ], "imports": [ "src/agent/runner/sequence/orchestrator/queue-tools.ts", + "src/store/agent-protocol/agent-env-isolation.ts", "src/store/agent-protocol/agent-signals.ts", - "src/store/auth-session-state.ts", - "src/store/services/steps/add-mcp-server-to-clients/MCPClient.ts" + "src/store/auth-session-state.ts" ] }, "src/agent/agent-prompt-loader.ts": { @@ -78,10 +78,10 @@ "src/agent/agent-prompt-loader.ts", "src/agent/aio-capture.ts", "src/agent/runner/sequence/orchestrator/queue-tools.ts", + "src/store/agent-protocol/agent-env-isolation.ts", "src/store/agent-protocol/agent-signals.ts", "src/store/auth-session-state.ts", - "src/store/programs/ai-opt-in-gate.ts", - "src/store/services/steps/add-mcp-server-to-clients/MCPClient.ts" + "src/store/programs/ai-opt-in-gate.ts" ] }, "src/agent/aio-capture.ts": { @@ -98,9 +98,8 @@ "src/agent/runner/harness/pi/task.ts", "src/agent/runner/harness/pi/tasks.ts", "src/agent/runner/harness/pi/tools.ts", - "src/store/agent-protocol/agent-signals.ts", - "src/store/auth-session-state.ts", - "src/store/services/steps/add-mcp-server-to-clients/MCPClient.ts" + "src/store/agent-protocol/agent-env-isolation.ts", + "src/store/agent-protocol/agent-signals.ts" ] }, "src/agent/runner/harness/pi/mcp.ts": { @@ -145,9 +144,8 @@ "src/agent/runner/harness/pi/security.ts", "src/agent/runner/harness/pi/tools.ts", "src/agent/runner/sequence/orchestrator/queue-tools.ts", - "src/store/agent-protocol/agent-signals.ts", - "src/store/auth-session-state.ts", - "src/store/services/steps/add-mcp-server-to-clients/MCPClient.ts" + "src/store/agent-protocol/agent-env-isolation.ts", + "src/store/agent-protocol/agent-signals.ts" ] }, "src/agent/runner/harness/pi/tasks.ts": { @@ -162,9 +160,8 @@ ], "imports": [ "src/agent/aio-capture.ts", - "src/store/agent-protocol/agent-signals.ts", - "src/store/auth-session-state.ts", - "src/store/services/steps/add-mcp-server-to-clients/MCPClient.ts" + "src/store/agent-protocol/agent-env-isolation.ts", + "src/store/auth-session-state.ts" ] }, "src/agent/runner/sequence/orchestrator/queue-tools.ts": { @@ -219,11 +216,10 @@ "src/cli/commands/basic-integration/non-interactive.ts", "src/cli/commands/basic-integration/playground.ts", "src/cli/control-hooks.ts", + "src/store/agent-protocol/agent-env-isolation.ts", "src/store/agent-protocol/agent-signals.ts", "src/store/auth-session-state.ts", - "src/store/services/steps/add-mcp-server-to-clients/MCPClient.ts", - "src/tui/App.tsx", - "src/tui/console/headless-ui.ts" + "src/tui/App.tsx" ] }, "src/cli/commands/basic-integration/ci-install.ts": { @@ -233,8 +229,7 @@ "imports": [ "src/cli/control-hooks.ts", "src/store/agent-protocol/agent-signals.ts", - "src/store/auth-session-state.ts", - "src/tui/console/headless-ui.ts" + "src/store/auth-session-state.ts" ] }, "src/cli/commands/basic-integration/interactive.ts": { @@ -273,13 +268,50 @@ ], "imports": [ "src/agent/agent-prompt.ts", + "src/store/agent-protocol/agent-env-isolation.ts", "src/store/agent-protocol/agent-signals.ts", "src/store/auth-session-state.ts", "src/store/control/actions.ts", "src/store/programs/ai-opt-in-gate.ts", - "src/store/services/steps/add-mcp-server-to-clients/MCPClient.ts", - "src/tui/App.tsx", - "src/tui/console/headless-ui.ts" + "src/tui/App.tsx" + ] + }, + "src/store/agent-protocol/agent-env-isolation.ts": { + "sources": [ + "src/store/agent-protocol/agent-env-isolation.ts", + "src/store/agent-protocol/agent-phase.ts", + "src/store/agent-protocol/mcp-prompt.ts", + "src/store/agent-protocol/token-pricing.ts", + "src/store/file-watcher.ts", + "src/store/mcp-project-profile.ts", + "src/store/mcp-role-prompts.copy.json", + "src/store/mcp-role-prompts.ts", + "src/store/mcp-seed-events.ts", + "src/store/safe-tools.ts", + "src/store/services/claude-settings.ts", + "src/store/services/steps/add-mcp-server-to-clients/browser-client.ts", + "src/store/services/steps/add-mcp-server-to-clients/login-client.ts", + "src/store/session/secret-vault.ts", + "src/store/shared/clipboard.ts", + "src/store/shared/custom-headers.ts", + "src/store/shared/env-api-key.ts", + "src/store/shared/environment.ts", + "src/store/shared/terminal-bell.ts", + "src/store/state/run-store.ts", + "src/store/state/store.ts", + "src/store/task-stream/audit-areas.ts", + "src/store/task-stream/destinations/file.ts", + "src/store/task-stream/destinations/posthog.ts", + "src/store/task-stream/event-plan-watcher.ts", + "src/store/task-stream/task-stream-push.ts", + "src/store/tools/handoff.ts", + "src/store/ui/store-ui.ts", + "src/store/ui/wizard-ui.ts", + "src/store/wizard-spellbook.ts" + ], + "imports": [ + "src/store/agent-protocol/agent-signals.ts", + "src/store/auth-session-state.ts" ] }, "src/store/agent-protocol/agent-signals.ts": { @@ -470,9 +502,9 @@ "src/store/programs/run-config.ts" ], "imports": [ + "src/store/agent-protocol/agent-env-isolation.ts", "src/store/agent-protocol/agent-signals.ts", - "src/store/auth-session-state.ts", - "src/store/services/steps/add-mcp-server-to-clients/MCPClient.ts" + "src/store/auth-session-state.ts" ] }, "src/store/services/steps/add-mcp-server-to-clients/MCPClient.ts": { @@ -687,19 +719,11 @@ "src/tui/ui-store.ts" ], "imports": [ + "src/store/agent-protocol/agent-env-isolation.ts", "src/store/agent-protocol/agent-signals.ts", "src/store/auth-session-state.ts", "src/store/programs/ai-opt-in-gate.ts", "src/store/services/steps/add-mcp-server-to-clients/MCPClient.ts" ] - }, - "src/tui/console/headless-ui.ts": { - "sources": [ - "src/tui/console/headless-ui.ts", - "src/tui/console/logging-ui.ts" - ], - "imports": [ - "src/store/auth-session-state.ts" - ] } } diff --git a/scripts/__fixtures__/chunk-manifest.prod.json b/scripts/__fixtures__/chunk-manifest.prod.json index 262963df6..672cbb271 100644 --- a/scripts/__fixtures__/chunk-manifest.prod.json +++ b/scripts/__fixtures__/chunk-manifest.prod.json @@ -300,6 +300,7 @@ "src/store/shared/env-api-key.ts", "src/store/shared/environment.ts", "src/store/shared/terminal-bell.ts", + "src/store/state/run-store.ts", "src/store/state/store.ts", "src/store/task-stream/audit-areas.ts", "src/store/task-stream/destinations/file.ts", diff --git a/src/__tests__/architecture/__snapshots__/boundary-contracts.test.ts.snap b/src/__tests__/architecture/__snapshots__/boundary-contracts.test.ts.snap index 21fb952fb..48cd24c65 100644 --- a/src/__tests__/architecture/__snapshots__/boundary-contracts.test.ts.snap +++ b/src/__tests__/architecture/__snapshots__/boundary-contracts.test.ts.snap @@ -199,6 +199,7 @@ exports[`boundary contracts > cli consumes exactly the recorded names from other "ApiError", "CLI_STEERING_TARGETS", "ErrorCodes", + "FlowStore", "Harness", "OutroKind", "POSTHOG_DOCS_URL", @@ -211,7 +212,6 @@ exports[`boundary contracts > cli consumes exactly the recorded names from other "VERSION", "WIZARD_TOOL_NAMES", "WizardError", - "WizardStore", "addMCPServerToClientsStep", "analytics", "authenticate", @@ -289,6 +289,7 @@ exports[`boundary contracts > cli consumes exactly the recorded names from other "ControlServerHandle", "DetectRequest", "ErrorCode", + "FlowStore", "Harness", "OutroData", "ProgramConfig", @@ -296,10 +297,10 @@ exports[`boundary contracts > cli consumes exactly the recorded names from other "ProvisioningResult", "RunPhase", "RunRequest", + "RunStore", "Sequence", "TaskStreamPush", "WizardSession", - "WizardStore", ], "@tui": [ "createFamilyPickerDefault", @@ -331,6 +332,7 @@ exports[`boundary contracts > tui consumes exactly the recorded names from other "ErrorCodes", "FOLLOW_UP_EXIT_SENTINEL", "FRAMEWORK_REGISTRY", + "FlowStore", "GITHUB_SKILLS_BASE_URL", "HostResolution", "Integration", @@ -356,7 +358,6 @@ exports[`boundary contracts > tui consumes exactly the recorded names from other "TaskStatus", "WIZARD_LOG_FILE", "WizardReadiness", - "WizardStore", "analytics", "assembleProfile", "copyToClipboard", @@ -461,6 +462,7 @@ exports[`boundary contracts > tui consumes exactly the recorded names from other "ErrorTrackingProject", "FileWatcherHandle", "FileWatcherOptions", + "FlowStore", "HealthCheckKey", "HealthIssue", "HealthIssueSeverity", @@ -489,7 +491,6 @@ exports[`boundary contracts > tui consumes exactly the recorded names from other "WizardReadinessResult", "WizardSession", "WizardSpellbook", - "WizardStore", "WizardUI", ], } diff --git a/src/__tests__/architecture/store-boundary-members.test.ts b/src/__tests__/architecture/store-boundary-members.test.ts index cf6f74cef..9c4abc280 100644 --- a/src/__tests__/architecture/store-boundary-members.test.ts +++ b/src/__tests__/architecture/store-boundary-members.test.ts @@ -7,7 +7,7 @@ const HERE = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(HERE, '../../..'); const CONSUMER = /\/(src\/(tui|cli)|e2e-harness|scripts)\/|\/bin\.ts$/; -/** Every WizardStore member the other surfaces reach, from the type checker. */ +/** Every FlowStore member the other surfaces reach, from the type checker. */ function usedMembers(): string[] { const cfg = ts.getParsedCommandLineOfConfigFile( path.join(REPO_ROOT, 'tsconfig.json'), @@ -31,7 +31,7 @@ function usedMembers(): string[] { const isStore = (node: ts.Node): boolean => { const type = checker.getTypeAtLocation(node); const parts = type.isUnion() ? type.types : [type]; - return parts.some((t) => t.getSymbol()?.getName() === 'WizardStore'); + return parts.some((t) => t.getSymbol()?.getName() === 'FlowStore'); }; const visit = (node: ts.Node): void => { if (ts.isPropertyAccessExpression(node) && isStore(node.expression)) { @@ -55,7 +55,7 @@ function usedMembers(): string[] { return [...used].sort(); } -describe('WizardStore boundary', () => { +describe('FlowStore boundary', () => { it('STORE_BOUNDARY_MEMBERS is exactly what tui, cli, and the harness use', () => { expect(usedMembers()).toEqual([...STORE_BOUNDARY_MEMBERS].sort()); }, 120_000); diff --git a/src/cli/__tests__/cli.test.ts b/src/cli/__tests__/cli.test.ts index e3316ba2a..383792c3d 100644 --- a/src/cli/__tests__/cli.test.ts +++ b/src/cli/__tests__/cli.test.ts @@ -8,7 +8,7 @@ const { mockBuildSessionCli, mockProvisionNewAccountCli } = vi.hoisted(() => ({ })); // Headless-only machinery, stubbed so the headless path doesn't construct a -// real WizardStore (which would re-call the mocked buildSession) or open a real +// real FlowStore (which would re-call the mocked buildSession) or open a real // network stream. The spies assert the stream is wired in headless and not CI. const { mockStreamAttach, mockStreamShutdown, mockStreamDestinations } = vi.hoisted(() => ({ @@ -48,11 +48,22 @@ vi.mock('@store/task-stream/destinations/file', () => ({ })); vi.mock('@store/state/store', async (importOriginal) => ({ ...(await importOriginal()), - WizardStore: class { + FlowStore: class { session: unknown; setRunPhase = vi.fn(); setOutroData = vi.fn(); syncTodos = vi.fn(); + // The run store shares the flow's spies, so phase assertions read either. + startRun = () => ({ + session: this.session, + setRunPhase: this.setRunPhase, + setOutroData: this.setOutroData, + tasks: [], + eventPlan: [], + handoffText: null, + subscribe: vi.fn(() => () => undefined), + setEventPlan: vi.fn(), + }); }, })); diff --git a/src/cli/__tests__/control-hooks.test.ts b/src/cli/__tests__/control-hooks.test.ts index 640017e69..15eddc737 100644 --- a/src/cli/__tests__/control-hooks.test.ts +++ b/src/cli/__tests__/control-hooks.test.ts @@ -180,7 +180,9 @@ describe('one independent run', () => { expect(stream.shutdown).toHaveBeenCalledWith(2000); } expect(cleanups).toHaveBeenCalledTimes(2); - expect(store.session.dashboardUrl).toBeNull(); + expect(store.session.dashboardUrl).toBe( + 'https://us.posthog.com/project/1/dashboard/9', + ); expect(store.tasks).toEqual([]); expect(store.session.runPhase).toBe(RunPhase.Completed); }); diff --git a/src/cli/control-hooks.ts b/src/cli/control-hooks.ts index dd9e0cbc8..e1ccb46c5 100644 --- a/src/cli/control-hooks.ts +++ b/src/cli/control-hooks.ts @@ -14,23 +14,24 @@ import type { ProgramConfig, ProgramId, RunRequest, + RunStore, WizardSession, - WizardStore, + FlowStore, } from '@store/types'; -/** The task stream one independent run publishes to; a new session per run. */ +/** The task stream one run's store publishes to. */ export interface RunStream { attach(): void; shutdown(timeoutMs: number): Promise; } export interface ControlHookDeps { - store: WizardStore; + store: FlowStore; /** The program this process launched with. */ programId: ProgramId; runAgent: RunAgent; - /** Builds the stream a run publishes to; absent means the run publishes nothing. */ - runStream?: (config: ProgramConfig, session: WizardSession) => RunStream; + /** Builds the stream over one run's store; absent means the run publishes nothing. */ + runStream?: (config: ProgramConfig, run: RunStore) => RunStream; /** Flush and exit; the runner owns the exact steps. */ shutdown: () => Promise; } @@ -111,26 +112,27 @@ export function createControlHooks(deps: ControlHookDeps): ControlHooks { programLabel: config.id, }; logToFile(`[control] run ${config.id} in ${runSession.installDir}`); - // One session per run: a clean run state and its own stream; credentials and context persist. - store.resetRunState(); - store.setRunPhase(RunPhase.Running); - const stream = deps.runStream?.(config, runSession); + // One run, one store: a fresh RunStore on the flow; credentials and context carry over. + const run = store.startRun(runSession); + // In flight from here on: pollers read the phase, not the ledger. + run.setRunPhase(RunPhase.Running); + const stream = deps.runStream?.(config, run); stream?.attach(); try { - await deps.runAgent(runConfigFor(config), runSession, { + await deps.runAgent(runConfigFor(config), run.session, { composed: true, }); // Headless renderers never flip the phase; settle it so the ledger records a completed run. - if (store.session.runPhase === RunPhase.Running) { - store.setRunPhase(RunPhase.Completed); + if (run.session.runPhase === RunPhase.Running) { + run.setRunPhase(RunPhase.Completed); } } catch (err) { - if (store.session.runPhase !== RunPhase.Error) { - store.setOutroData({ + if (run.session.runPhase !== RunPhase.Error) { + run.setOutroData({ kind: OutroKind.Error, message: err instanceof Error ? err.message : String(err), }); - store.setRunPhase(RunPhase.Error); + run.setRunPhase(RunPhase.Error); } throw err; } finally { diff --git a/src/cli/runners/__tests__/mint-recovery.test.ts b/src/cli/runners/__tests__/mint-recovery.test.ts index be4ffc62c..eaf1efe45 100644 --- a/src/cli/runners/__tests__/mint-recovery.test.ts +++ b/src/cli/runners/__tests__/mint-recovery.test.ts @@ -2,7 +2,7 @@ import { vi, it, expect, afterEach } from 'vitest'; import { runWizard } from '../run-wizard.js'; import { runAgent } from '@agent'; import { startTUI } from '@tui'; -import { WizardStore } from '@store/state/store'; +import { FlowStore } from '@store/state/store'; import { StoreUI } from '@store/ui/store-ui'; import { setUI } from '@store/ui'; import { posthogIntegrationConfig } from '@store/programs/posthog-integration'; @@ -49,7 +49,7 @@ afterEach(() => { it.each(['continue', 'exit'] as const)( 'catches a failed run, shows the handoff screen, and exits 1 after %s', async (action) => { - const store = new WizardStore(flowFor(Program.PostHogIntegration).flow); + const store = new FlowStore(flowFor(Program.PostHogIntegration).flow); setUI(new StoreUI(store)); vi.spyOn(store, 'runReadyHooks').mockResolvedValue(undefined); vi.spyOn(store, 'getGate').mockResolvedValue(undefined); diff --git a/src/cli/runners/run-non-interactive.ts b/src/cli/runners/run-non-interactive.ts index 80ea2c19b..59c704ce5 100644 --- a/src/cli/runners/run-non-interactive.ts +++ b/src/cli/runners/run-non-interactive.ts @@ -16,8 +16,8 @@ import type { Sequence, CloudRegion, ProgramConfig, - WizardSession, - WizardStore, + FlowStore, + RunStore, TaskStreamPush, OutroData, RunPhase as RunPhaseT, @@ -177,18 +177,22 @@ export function runNonInteractive( } // Headless streams run state to the PostHog backend so the web app can show - // live progress. Reuses the interactive TaskStreamPush + WizardStore (no Ink + // live progress. Reuses the interactive TaskStreamPush + FlowStore (no Ink // render): HeadlessUI keeps LoggingUI's output and feeds task updates into // the store; this runner drives the phase transitions. Headless pushes to // PostHog (the web app is that run's only UI); `--ci` is synthetic, so it // dumps locally and pushes nothing. Telemetry consent gates the push only. - let store: WizardStore | null = null; + let store: FlowStore | null = null; let taskStream: TaskStreamPush | null = null; let runStream: - | ((config: ProgramConfig, runSession: WizardSession) => TaskStreamPush) + | ((config: ProgramConfig, run: RunStore) => TaskStreamPush) | null = null; + // Starts this process's one run on the flow and streams it; assigned once the store exists. + let beginRun: () => RunStore = () => { + throw new Error('the run store is not configured yet'); + }; { - const { WizardStore } = await import('@store'); + const { FlowStore } = await import('@store'); const { HeadlessUI } = await import('@tui/console'); const { TaskStreamPush, PostHogDestination, createFileDestination } = await import('@store'); @@ -211,7 +215,7 @@ export function runNonInteractive( ...(fileDestination ? [fileDestination] : []), ]; - const headlessStore = new WizardStore(flowFor(config.id).flow); + const headlessStore = new FlowStore(flowFor(config.id).flow); store = headlessStore; // A controlled run answers the agent's questions over the socket, so the // ask bridge stays wired despite `ci`. @@ -225,15 +229,15 @@ export function runNonInteractive( } const streamFor = ( runConfig: ProgramConfig, - runSession: WizardSession, + run: RunStore, ): TaskStreamPush => new TaskStreamPush({ - store: headlessStore, + store: run, programId: runConfig.streamWorkflowId ?? runConfig.id, - skillId: runSession.skillId ?? undefined, + skillId: run.session.skillId ?? undefined, destinations, eventPlanPath: runConfig.eventPlanFile - ? join(runSession.installDir, runConfig.eventPlanFile) + ? join(run.session.installDir, runConfig.eventPlanFile) : undefined, auditChecks: runConfig.auditLedgerFile ? () => getAuditChecks(headlessStore.session) @@ -241,12 +245,15 @@ export function runNonInteractive( enabled: destinations.length > 0, }); if (options.controlSocket) { - // Every POST /runs is one independent run with its own stream session. + // Every POST /runs is one independent run with its own store and stream. runStream = streamFor; } else { - taskStream = streamFor(config, session); - taskStream.attach(); - headlessStore.setRunPhase(RunPhase.Running); + beginRun = () => { + const run = headlessStore.startRun(session); + taskStream = streamFor(config, run); + taskStream.attach(); + return run; + }; } if (fileDestination) { logToFile(`[task-stream] ${mode} dump: ${fileDestination.path}`); @@ -259,7 +266,10 @@ export function runNonInteractive( phase: RunPhaseT, outroData?: OutroData, ): Promise => { - if (!store || !taskStream) return; + if (!store || options.controlSocket) return; + // An abort before the run starts still reports through a run of its own. + if (!taskStream) beginRun(); + if (!taskStream) return; if (outroData) store.setOutroData(outroData); store.setRunPhase(phase); await taskStream.shutdown(2000); @@ -406,7 +416,9 @@ export function runNonInteractive( } const { runAgent } = await import('@agent'); - await runAgent(runConfigFor(config), session); + const run = beginRun(); + run.setRunPhase(RunPhase.Running); + await runAgent(runConfigFor(config), run.session); await settleStream(RunPhase.Completed); } catch (error) { const errorMessage = diff --git a/src/cli/runners/run-wizard.ts b/src/cli/runners/run-wizard.ts index 032e22616..6ba523075 100644 --- a/src/cli/runners/run-wizard.ts +++ b/src/cli/runners/run-wizard.ts @@ -23,12 +23,12 @@ import type { ProgramConfig, Harness, Sequence, - WizardStore, + FlowStore, WizardSession, TaskStreamPush as TaskStreamPushClass, } from '@store/types'; import type { TuiHandle } from '@tui/types'; -import type { ControlServerHandle, CloudRegion } from '@store/types'; +import type { ControlServerHandle, CloudRegion, RunStore } from '@store/types'; import { IS_PRODUCTION_BUILD } from '@env'; import { createControlHooks } from '../control-hooks.js'; import { resolveNoTelemetry } from './resolve-no-telemetry.js'; @@ -64,26 +64,26 @@ async function prepareRunSession( * the user to satisfy `isComplete`. */ async function advanceStep( step: Step, - store: WizardStore, + store: FlowStore, config: ProgramConfig, + beginRun: (runConfig: ProgramConfig, session: WizardSession) => RunStore, ): Promise { if (step.screenId === 'auth') { await authenticate(store.session, config.id); maybeStampAiSdkDetected(store.session); } else if (step.run) { const { runAgent } = await import('@agent'); - await runAgent( - runConfigFor(getProgramConfig(step.run.programId)), + const runConfig = getProgramConfig(step.run.programId); + const run = beginRun( + runConfig, await prepareRunSession(step, store.session), - { composed: true }, ); + await runAgent(runConfigFor(runConfig), run.session, { composed: true }); store.completeRunStep(step.id); } else if (step.screenId === 'run') { const { runAgent } = await import('@agent'); - await runAgent( - runConfigFor(config), - await prepareRunSession(step, store.session), - ); + const run = beginRun(config, await prepareRunSession(step, store.session)); + await runAgent(runConfigFor(config), run.session); } else if (step.isComplete) { await store.waitUntil(step.isComplete); } @@ -101,6 +101,8 @@ export function runWizard( let tui: TuiHandle | null = null; let control: ControlServerHandle | null = null; let taskStream: TaskStreamPushClass | null = null; + // Assigned inside beginRun, which narrowing does not see. + const currentStream = (): TaskStreamPushClass | null => taskStream; let onSignal: (() => void) | null = null; let exitInProgress = false; @@ -251,11 +253,6 @@ export function runWizard( config = getProgramConfig(active); } - // After the switch loop, not before: the stream bakes its program id, - // session id, and event-plan path in at construction, so a stream built - // for the launch program would report the whole run under a program the - // user left on the intro screen. Nothing before this point produces a - // task to push. // Consent gates the push, not the dump: `--no-telemetry` still logs. const fileDestination = createFileDestination(options.taskStreamLog); const destinations = [ @@ -269,21 +266,31 @@ export function runWizard( ]), ...(fileDestination ? [fileDestination] : []), ]; - const taskStreamEnabled = destinations.length > 0; - const activeStream = new TaskStreamPush({ - store: activeTui.store, - programId: config.streamWorkflowId ?? config.id, - destinations, - eventPlanPath: config.eventPlanFile - ? join(session.installDir, config.eventPlanFile) - : undefined, - auditChecks: config.auditLedgerFile - ? () => getAuditChecks(activeTui.store.session) - : undefined, - enabled: taskStreamEnabled, - }); - taskStream = activeStream; - activeStream.attach(); + // One run, one RunStore, one stream session: the stream bakes the program, + // the session id, and the event-plan path in at construction, so it is + // built when a run starts, over that run's store. + const beginRun = ( + runConfig: ProgramConfig, + runSession: WizardSession, + ): RunStore => { + const run = activeTui.store.startRun(runSession); + const stream = new TaskStreamPush({ + store: run, + programId: runConfig.streamWorkflowId ?? runConfig.id, + skillId: run.session.skillId ?? undefined, + destinations, + eventPlanPath: runConfig.eventPlanFile + ? join(run.session.installDir, runConfig.eventPlanFile) + : undefined, + auditChecks: runConfig.auditLedgerFile + ? () => getAuditChecks(activeTui.store.session) + : undefined, + enabled: destinations.length > 0, + }); + taskStream = stream; + stream.attach(); + return run; + }; await activeTui.store.getGate('integration-check'); await activeTui.store.getGate('health-check'); @@ -307,7 +314,9 @@ export function runWizard( // Walk the list once, advancing each step to completion. for (const step of config.steps) { if (step.screenId === 'outro') break; // run-completion wait owns it - if (shown(step)) await advanceStep(step, activeTui.store, config); + if (shown(step)) { + await advanceStep(step, activeTui.store, config, beginRun); + } } } else if (skipAgent) { const { getOrAskForProjectData } = await import('@store'); @@ -329,7 +338,8 @@ export function runWizard( } else { try { const { runAgent } = await import('@agent'); - await runAgent(runConfigFor(config), activeTui.store.session); + const run = beginRun(config, activeTui.store.session); + await runAgent(runConfigFor(config), run.session); } catch (error) { // The run threw before its own error handling rendered an outro. // Show the handoff screen and let the user's agent take over. @@ -356,7 +366,7 @@ export function runWizard( }); exitInProgress = true; - await activeStream.shutdown(2000); + await currentStream()?.shutdown(2000); await control?.close(); process.off('SIGINT', onSignal); process.off('SIGTERM', onSignal); @@ -376,9 +386,10 @@ export function runWizard( process.off('SIGINT', onSignal); process.off('SIGTERM', onSignal); } - if (taskStream) { + const stream = currentStream(); + if (stream) { try { - await taskStream.shutdown(2000); + await stream.shutdown(2000); } catch { // ignore } diff --git a/src/cli/testing/fake-surfaces.ts b/src/cli/testing/fake-surfaces.ts index 062e26b56..db80c3526 100644 --- a/src/cli/testing/fake-surfaces.ts +++ b/src/cli/testing/fake-surfaces.ts @@ -1,5 +1,5 @@ import type { RunAgent } from '@agent/types'; -import { StoreUI, WizardStore, setUI } from '@store'; +import { StoreUI, FlowStore, setUI } from '@store'; import { flowFor } from '@store/programs'; import type { ProgramId } from '@store/types'; import type { TuiHandle } from '@tui/types'; @@ -30,7 +30,7 @@ export function fakeRunAgent(): { runAgent: RunAgent; calls: RunAgentCall[] } { /** A real store behind StoreUI, no Ink: what startTUI hands the runner. */ export function fakeStartTUI(programId: ProgramId): TuiHandle { - const store = new WizardStore(flowFor(programId).flow); + const store = new FlowStore(flowFor(programId).flow); setUI(new StoreUI(store)); return { store, diff --git a/src/store/README.md b/src/store/README.md index 75c5d6171..64c108689 100644 --- a/src/store/README.md +++ b/src/store/README.md @@ -4,7 +4,16 @@ Render-agnostic state and the contract between the agent and whatever renders. ## Owns -- `state/`: `WizardStore`, flows, interrupts, screen resolution, run failure. +- `state/`: `RunStore`, `FlowStore`, flows, interrupts, screen resolution, run + failure. A `RunStore` is the state of one agent run: its copy of the session, + tasks, phase, outro, and the questions the agent asks. `FlowStore` owns the + flow, gates, interrupts, and the session every run inherits; it chains runs + through `startRun(session)`, mirrors the active run in its `session`, and + re-emits the run's commits. The agent writes through `StoreUI` to the + `FlowStore`, which routes run state to the active `RunStore`; the task stream + reads one `RunStore`; screens and the control API read the `FlowStore`. The + dashboard and notebook a run creates are session artefacts and stay on the + flow, so a later run and the outro still link them. - `session/`: `WizardSession`, ask policy, ask bridge, secret vault. - `ui/`: the `WizardUI` interface, `getUI`/`setUI`, `StoreUI`, `NullUI`. - `agent-protocol/`: run configs, agent signals, token pricing, subprocess env. @@ -50,7 +59,7 @@ line. throws. `testing/` holds `createTestStore`; shipped code never imports it. `__tests__/contract.test.ts` pins the boundary: `StoreUI` and `NullUI` implement `WizardUI`, `ProgramConfig` extends `ProgramRunConfig`, `runConfigFor` emits -nothing beyond the run contract, and `WizardStore` satisfies `WizardStoreApi` +nothing beyond the run contract, and `FlowStore` satisfies `FlowStoreApi` (`state/store-api.ts`, whose member list the architecture suite derives from real tui, cli, and harness usage). Goldens live under `**/__tests__/__snapshots__` and must stay byte identical across refactors. diff --git a/src/store/__tests__/contract.test.ts b/src/store/__tests__/contract.test.ts index 7c6c3e829..56e3b06f8 100644 --- a/src/store/__tests__/contract.test.ts +++ b/src/store/__tests__/contract.test.ts @@ -7,8 +7,8 @@ import { PROGRAM_REGISTRY, } from '../programs/program-registry.js'; import { runConfigFor } from '../programs/run-config.js'; -import type { WizardStoreApi } from '../state/store-api.js'; -import type { WizardStore } from '../state/store.js'; +import type { FlowStoreApi } from '../state/store-api.js'; +import type { FlowStore } from '../state/store.js'; import { createTestStore } from '../testing/index.js'; import { NullUI } from '../ui/null-ui.js'; import { StoreUI } from '../ui/store-ui.js'; @@ -41,7 +41,7 @@ describe('store contract', () => { }); it('the store satisfies the API other surfaces see', () => { - expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); }); it('every ProgramConfig is a ProgramRunConfig', () => { diff --git a/src/store/agent-protocol/token-pricing.ts b/src/store/agent-protocol/token-pricing.ts index 38fd4842b..085444246 100644 --- a/src/store/agent-protocol/token-pricing.ts +++ b/src/store/agent-protocol/token-pricing.ts @@ -143,7 +143,7 @@ export function pricePerMtokForModel( * being priced at $0. * * Takes the same shape as `TokenUsageDelta` (`@ui/wizard-ui`) so a caller - * that already has one — `WizardStore.addTokenUsage` — can pass it straight + * that already has one — `FlowStore.addTokenUsage` — can pass it straight * through instead of re-listing its fields in a fixed positional order. */ export function computeTokenCostUsd(usage: { diff --git a/src/store/control/__tests__/actions.test.ts b/src/store/control/__tests__/actions.test.ts index 71650ebe9..b19416938 100644 --- a/src/store/control/__tests__/actions.test.ts +++ b/src/store/control/__tests__/actions.test.ts @@ -10,7 +10,7 @@ import { } from '../../session/wizard-session.js'; import type { Flow } from '../../state/flow.js'; import { Interrupt } from '../../state/interrupts.js'; -import { WizardStore } from '../../state/store.js'; +import { FlowStore } from '../../state/store.js'; import { createControlledStore, createTestStore } from '../../testing/index.js'; import { setUI } from '../../ui/index.js'; import { StoreUI } from '../../ui/store-ui.js'; @@ -23,7 +23,7 @@ import { import { BadParamError, MissingParamError } from '../params.js'; import { ControlDriver } from '../driver.js'; -function storeFor(program = Program.PostHogIntegration): WizardStore { +function storeFor(program = Program.PostHogIntegration): FlowStore { const store = createTestStore(program); setUI(new StoreUI(store)); store.session = buildSession({ @@ -33,7 +33,7 @@ function storeFor(program = Program.PostHogIntegration): WizardStore { return store; } -function apply(store: WizardStore, screen: string, id: string, params = {}) { +function apply(store: FlowStore, screen: string, id: string, params = {}) { const action = actionsFor(store.flow, screen).find((a) => a.id === id); if (!action) throw new Error(`no ${id} on ${screen}`); action.apply(store, params); @@ -53,49 +53,44 @@ describe('generic actions', () => { 'health-check', 'dismiss_outage', {}, - (s: WizardStore) => s.session.outageDismissed === true, + (s: FlowStore) => s.session.outageDismissed === true, ], [ 'setup', 'choose', { key: 'router', value: 'app' }, - (s: WizardStore) => s.session.frameworkContext.router === 'app', - ], - [ - 'outro', - 'dismiss_outro', - {}, - (s: WizardStore) => s.session.outroDismissed, + (s: FlowStore) => s.session.frameworkContext.router === 'app', ], + ['outro', 'dismiss_outro', {}, (s: FlowStore) => s.session.outroDismissed], [ 'audit-outro', 'dismiss_outro', {}, - (s: WizardStore) => s.session.outroDismissed, + (s: FlowStore) => s.session.outroDismissed, ], [ 'source-maps-outro', 'dismiss_outro', {}, - (s: WizardStore) => s.session.outroDismissed, + (s: FlowStore) => s.session.outroDismissed, ], [ 'mint-failure', 'continue_setup', {}, - (s: WizardStore) => s.session.mintHandoff === 'continue', + (s: FlowStore) => s.session.mintHandoff === 'continue', ], [ 'mint-failure', 'dismiss_outro', {}, - (s: WizardStore) => s.session.mintHandoff === 'exit', + (s: FlowStore) => s.session.mintHandoff === 'exit', ], [ 'mcp', 'set_mcp_outcome', { outcome: 'installed', clients: ['cursor'] }, - (s: WizardStore) => + (s: FlowStore) => s.session.mcpComplete && s.session.mcpOutcome === McpOutcome.Installed && s.session.mcpInstalledClients[0] === 'cursor', @@ -104,37 +99,37 @@ describe('generic actions', () => { 'mcp-add', 'set_mcp_outcome', { outcome: 'skipped' }, - (s: WizardStore) => s.session.mcpOutcome === McpOutcome.Skipped, + (s: FlowStore) => s.session.mcpOutcome === McpOutcome.Skipped, ], [ 'mcp-remove', 'set_mcp_outcome', {}, - (s: WizardStore) => s.session.mcpOutcome === McpOutcome.Skipped, + (s: FlowStore) => s.session.mcpOutcome === McpOutcome.Skipped, ], [ 'mcp-suggested-prompts', 'dismiss', {}, - (s: WizardStore) => s.session.mcpSuggestedPromptsDismissed, + (s: FlowStore) => s.session.mcpSuggestedPromptsDismissed, ], [ 'slack-connect', 'dismiss_slack', {}, - (s: WizardStore) => s.session.slackStepDismissed, + (s: FlowStore) => s.session.slackStepDismissed, ], [ 'slack-connect', 'set_slack_connected', { connected: true }, - (s: WizardStore) => s.session.slackConnected === true, + (s: FlowStore) => s.session.slackConnected === true, ], [ 'keep-skills', 'keep_skills', { kept: false }, - (s: WizardStore) => s.session.skillsComplete, + (s: FlowStore) => s.session.skillsComplete, ], ] as const)( '%s / %s commits through its setter', diff --git a/src/store/control/driver.ts b/src/store/control/driver.ts index 689f87fbc..afc629de0 100644 --- a/src/store/control/driver.ts +++ b/src/store/control/driver.ts @@ -1,11 +1,11 @@ -import type { WizardStore } from '../state/store.js'; +import type { FlowStore } from '../state/store.js'; import { actionsFor, UnknownActionError } from './actions.js'; import { projectState } from './state.js'; import type { ControlState } from './types.js'; /** Reads the committed store and acts through the setter the screen's key handler would call. */ export class ControlDriver { - constructor(private readonly store: WizardStore) {} + constructor(private readonly store: FlowStore) {} readState(): ControlState { return projectState(this.store); diff --git a/src/store/control/server.ts b/src/store/control/server.ts index 690dc9f45..edbe80051 100644 --- a/src/store/control/server.ts +++ b/src/store/control/server.ts @@ -8,7 +8,7 @@ import { import { RunPhase } from '../session/wizard-session.js'; import { logToFile } from '../shared/debug.js'; import { resolveInstallDir } from '../shared/paths.js'; -import type { WizardStore } from '../state/store.js'; +import type { FlowStore } from '../state/store.js'; import { UnknownActionError } from './actions.js'; import { ControlDriver } from './driver.js'; import { CONTROL_SERVER_MARKER } from './marker.js'; @@ -181,7 +181,7 @@ function actionId(pathname: string): string | null { /** Serve the control API for one store over a unix socket: HTTP/1.1, JSON in and out. */ export async function attachControlServer( - store: WizardStore, + store: FlowStore, options: ControlServerOptions, ): Promise { const { socketPath, surface, hooks } = options; diff --git a/src/store/control/state.ts b/src/store/control/state.ts index 2daf143d3..8befd7bc0 100644 --- a/src/store/control/state.ts +++ b/src/store/control/state.ts @@ -1,6 +1,6 @@ import type { WizardSession } from '../session/wizard-session.js'; import { sanitizeErrorDetail } from '../shared/errors/sanitize.js'; -import type { WizardStore } from '../state/store.js'; +import type { FlowStore } from '../state/store.js'; import { actionsFor, toActionView } from './actions.js'; import type { ControlSession, ControlState } from './types.js'; @@ -90,7 +90,7 @@ function projectSession(s: WizardSession): ControlSession { } /** Project the committed store for a controlling parent. */ -export function projectState(store: WizardStore): ControlState { +export function projectState(store: FlowStore): ControlState { const s = store.session; const screen = store.currentScreen; const questions = s.frameworkConfig?.metadata.setup?.questions ?? []; diff --git a/src/store/control/types.ts b/src/store/control/types.ts index edee5e0fa..0d3e18c68 100644 --- a/src/store/control/types.ts +++ b/src/store/control/types.ts @@ -1,7 +1,7 @@ import type { SetupQuestion } from '../framework-config.js'; import type { ProgramId } from '../programs/program-registry.js'; import type { WizardSession } from '../session/wizard-session.js'; -import type { PlannedEvent, TaskItem, WizardStore } from '../state/store.js'; +import type { PlannedEvent, TaskItem, FlowStore } from '../state/store.js'; import type { CONTROL_SESSION_KEYS } from './state.js'; /** One commit a controlling parent may make on a screen. */ @@ -12,7 +12,7 @@ export interface DriverAction { /** Parameter name to a human/type hint. Absent means no params. */ params?: Record; /** Apply the commit through exactly one store setter or resolver. */ - apply: (store: WizardStore, params: Record) => void; + apply: (store: FlowStore, params: Record) => void; } /** An action as the wire carries it: no closure. */ diff --git a/src/store/index.ts b/src/store/index.ts index ad459dafc..465275f5c 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -240,7 +240,8 @@ export { isRunFailure } from './state/run-failure.js'; export { STORE_BOUNDARY_MEMBERS } from './state/store-api.js'; export { MAX_STATUS_MESSAGES, - WizardStore, + FlowStore, + RunStore, totalTokenCount, } from './state/store.js'; export { createFileDestination } from './task-stream/destinations/file.js'; diff --git a/src/store/programs/__tests__/flow-traces.test.ts b/src/store/programs/__tests__/flow-traces.test.ts index 40b2ab949..c879d2b57 100644 --- a/src/store/programs/__tests__/flow-traces.test.ts +++ b/src/store/programs/__tests__/flow-traces.test.ts @@ -3,7 +3,7 @@ * by walking each program's steps through the store with a generic advance per * screen. Baseline for the surface split: must stay byte identical. */ -import { WizardStore, RunPhase, McpOutcome } from '../../state/store.js'; +import { FlowStore, RunPhase, McpOutcome } from '../../state/store.js'; import { StoreUI } from '@store/ui/store-ui'; import { setUI } from '../../ui/index.js'; import { @@ -58,7 +58,7 @@ function screenEvents(): ScreenEvent[] { const NODE = FRAMEWORK_REGISTRY[Integration.javascriptNode]; function createStore(program: ProgramId, integration: Integration | null) { - const store = new WizardStore(flowFor(program).flow); + const store = new FlowStore(flowFor(program).flow); setUI(new StoreUI(store)); const session = buildSession({ installDir: '/app', ci: false }); if (integration) { @@ -75,7 +75,7 @@ const approved = (ok: boolean) => } as unknown as WizardSession['apiUser']); /** Commit what a user, the runner, or the agent would commit on this screen. */ -function advance(store: WizardStore, screen: string): boolean { +function advance(store: FlowStore, screen: string): boolean { const s = store.session; if (screen === 'intro' || screen.endsWith('-intro')) { store.completeSetup(); @@ -216,7 +216,7 @@ describe('headless walk analytics', () => { for (const program of ['posthog-integration', 'audit'] as ProgramId[]) { it(`${program}: run phases without a TUI`, () => { wizardCapture.mockClear(); - const store = new WizardStore(flowFor(program).flow); + const store = new FlowStore(flowFor(program).flow); setUI(new StoreUI(store)); store.session = buildSession({ installDir: '/app', ci: true }); store.setRunPhase(RunPhase.Running); diff --git a/src/store/programs/__tests__/self-driving-detect.test.ts b/src/store/programs/__tests__/self-driving-detect.test.ts index 325fff0c8..93abbeaa3 100644 --- a/src/store/programs/__tests__/self-driving-detect.test.ts +++ b/src/store/programs/__tests__/self-driving-detect.test.ts @@ -14,7 +14,7 @@ import { getSelfDrivingDetectedTools, } from '../self-driving/detect.js'; import { getDetectedWarehouseSources } from '../warehouse-source/detect.js'; -import { WizardStore } from '../../state/store.js'; +import { FlowStore } from '../../state/store.js'; import { SOURCE_DETECTORS } from '../../services/warehouse-sources/registry.js'; import type { DetectedSource } from '../../services/warehouse-sources/types.js'; import { toIntegrationReport } from '../self-driving/detect-agentic.js'; @@ -132,7 +132,7 @@ describe('the detect step does not leak into the composed integration run', () = afterEach(() => cleanup(tmpDir)); it('stashes under its own key and leaves the warehouse key untouched', async () => { - const store = new WizardStore(flowFor('self-driving').flow); + const store = new FlowStore(flowFor('self-driving').flow); store.session = buildSession({ installDir: tmpDir }); await store.runReadyHooks(); diff --git a/src/store/programs/posthog-integration/detect.ts b/src/store/programs/posthog-integration/detect.ts index bd12750f5..9017fda92 100644 --- a/src/store/programs/posthog-integration/detect.ts +++ b/src/store/programs/posthog-integration/detect.ts @@ -187,7 +187,7 @@ export function maybeStampAiSdkDetected(session: WizardSession): void { // (latched only from TUI-only consent screens), this runs from // `authenticate()`, which also fires in `--ci` mode, where the session is a // plain object with no nanostore — see run-non-interactive.ts. A setter - // routed through WizardStore would silently never latch there. + // routed through FlowStore would silently never latch there. // // Depends on consent resolving before auth, same as every program's step // list orders 'intro' before 'auth' today; a program that reversed that @@ -201,7 +201,7 @@ export function maybeStampAiSdkDetected(session: WizardSession): void { /** * The single place scan results become telemetry. Called from - * `WizardStore.completeSetup()`, the point consent becomes final — the privacy + * `FlowStore.completeSetup()`, the point consent becomes final — the privacy * panel's choice is reversible until then, so nothing may report earlier. * The org stamp is a separate concern: see `maybeStampAiSdkDetected`, which * runs post-auth rather than at consent resolution. diff --git a/src/store/state/__tests__/invariants.test.ts b/src/store/state/__tests__/invariants.test.ts index a003f0cc5..38a64604c 100644 --- a/src/store/state/__tests__/invariants.test.ts +++ b/src/store/state/__tests__/invariants.test.ts @@ -1,10 +1,10 @@ /** - * Behaviour baseline for WizardStore and flow resolution, taken before a refactor. + * Behaviour baseline for FlowStore and flow resolution, taken before a refactor. * Every expectation here pins what the code does today, exceptions included. */ import { - WizardStore, + FlowStore, TaskStatus, Program, type ProgramId, @@ -107,8 +107,8 @@ const ANSWERS: AskAnswers = { a: 'yes' }; const aiUser = (approved: boolean): WizardSession['apiUser'] => ({ organization: { is_ai_data_processing_approved: approved } } as never); -function createStore(program?: ProgramId): WizardStore { - return new WizardStore(flowFor(program ?? Program.PostHogIntegration).flow); +function createStore(program?: ProgramId): FlowStore { + return new FlowStore(flowFor(program ?? Program.PostHogIntegration).flow); } async function flushMicrotasks(): Promise { @@ -124,7 +124,7 @@ function tracked(promise: Promise): { resolved: boolean } { return state; } -function countEmissions(store: WizardStore, act: () => void): number { +function countEmissions(store: FlowStore, act: () => void): number { let count = 0; const unsubscribe = store.subscribe(() => { count += 1; @@ -137,8 +137,8 @@ function countEmissions(store: WizardStore, act: () => void): number { interface MutationCase { name: string; /** Untracked setup — emissions it fires are excluded from the count. */ - prepare?: (store: WizardStore) => void; - invoke: (store: WizardStore) => void; + prepare?: (store: FlowStore) => void; + invoke: (store: FlowStore) => void; emits: number; } @@ -397,7 +397,7 @@ const MUTATIONS: MutationCase[] = [ emits: 1, }, { name: 'requestRun', invoke: (s) => s.requestRun(), emits: 1 }, - { name: 'resetRunState', invoke: (s) => s.resetRunState(), emits: 1 }, + { name: 'startRun', invoke: (s) => s.startRun(s.session), emits: 1 }, { name: 'setOutroDismissed', invoke: (s) => s.setOutroDismissed(), emits: 1 }, { name: 'setOutroData', @@ -505,10 +505,10 @@ describe('store invariants', () => { describe('one notification per mutation', () => { it('enumerates every public method on the class', () => { - const methods = Object.getOwnPropertyNames(WizardStore.prototype).filter( + const methods = Object.getOwnPropertyNames(FlowStore.prototype).filter( (name) => { const descriptor = Object.getOwnPropertyDescriptor( - WizardStore.prototype, + FlowStore.prototype, name, ); return ( diff --git a/src/store/state/__tests__/reset-run-state.test.ts b/src/store/state/__tests__/start-run.test.ts similarity index 50% rename from src/store/state/__tests__/reset-run-state.test.ts rename to src/store/state/__tests__/start-run.test.ts index a5d91e92b..04d2c88b9 100644 --- a/src/store/state/__tests__/reset-run-state.test.ts +++ b/src/store/state/__tests__/start-run.test.ts @@ -2,8 +2,8 @@ import { HostResolution } from '../../host-resolution.js'; import { OutroKind, RunPhase } from '../../session/wizard-session.js'; import { createControlledStore } from '../../testing/index.js'; -describe('resetRunState', () => { - it('clears exactly what one run produced and keeps what the next run needs', async () => { +describe('startRun', () => { + it('starts a fresh run store and keeps what the next run needs on the flow', async () => { const store = createControlledStore(); store.setCredentials({ accessToken: 't', @@ -37,9 +37,15 @@ describe('resetRunState', () => { prompt: 'p', }); + const previous = store.run; const version = store.getVersion(); - store.resetRunState(); + const run = store.startRun({ ...store.session, installDir: '/tmp/scoped' }); + expect(run).not.toBe(previous); + expect(store.run).toBe(run); + // The run's own copy is scoped; the flow keeps its own install dir. + expect(run.session.installDir).toBe('/tmp/scoped'); + expect(store.session.installDir).toBe('/tmp/controlled-store'); expect(store.tasks).toEqual([]); expect(store.statusMessages).toEqual([]); expect(store.eventPlan).toEqual([]); @@ -49,17 +55,51 @@ describe('resetRunState', () => { runPhase: RunPhase.Idle, outroData: null, outroDismissed: false, - dashboardUrl: null, - notebookUrl: null, pendingQuestion: null, taskNotice: null, }); + expect(store.hasInterrupt).toBe(false); await expect(question).resolves.toEqual({ a: '__cancelled__' }); await expect(notice).resolves.toBe(false); expect(store.session.credentials?.projectId).toBe(3); + expect(run.session.credentials?.projectId).toBe(3); expect(store.session.frameworkContext).toEqual({ router: 'app' }); expect(store.session.completedRuns).toContain('earlier'); + // Artefacts a run created for the project outlive it: the flow's outro links them. + expect(store.session.dashboardUrl).toBe('https://us.posthog.com/d/1'); + expect(run.session.dashboardUrl).toBe('https://us.posthog.com/d/1'); expect(store.getVersion()).toBeGreaterThan(version); }); + + it('mirrors the active run and only the active run', () => { + const store = createControlledStore(); + const run = store.startRun(store.session); + const before = store.getVersion(); + run.setRunPhase(RunPhase.Running); + run.pushStatus('working'); + expect(store.getVersion()).toBeGreaterThan(before); + expect(store.session.runPhase).toBe(RunPhase.Running); + expect(store.statusMessages).toEqual(['working']); + + const stale = run; + store.startRun(store.session); + const after = store.getVersion(); + stale.pushStatus('from the old run'); + stale.setRunPhase(RunPhase.Error); + expect(store.getVersion()).toBe(after); + expect(store.statusMessages).toEqual([]); + expect(store.session.runPhase).toBe(RunPhase.Idle); + }); + + it('writes context and skill to the flow and to the run the agent is in', () => { + const store = createControlledStore(); + const run = store.startRun(store.session); + store.setFrameworkContext('picked', 'yes'); + store.setSkillId('nextjs'); + expect(run.session.frameworkContext.picked).toBe('yes'); + expect(store.session.frameworkContext.picked).toBe('yes'); + expect(run.session.skillId).toBe('nextjs'); + expect(store.session.skillId).toBe('nextjs'); + }); }); diff --git a/src/store/state/__tests__/store.test.ts b/src/store/state/__tests__/store.test.ts index 6990f66fe..408845c92 100644 --- a/src/store/state/__tests__/store.test.ts +++ b/src/store/state/__tests__/store.test.ts @@ -1,5 +1,5 @@ import { - WizardStore, + FlowStore, TaskStatus, Program, type ProgramId, @@ -49,8 +49,8 @@ vi.mock('@store/health-checks/readiness', () => ({ getBlockingServiceKeys: vi.fn(() => []), })); -function createStore(program?: ProgramId): WizardStore { - return new WizardStore(flowFor(program ?? Program.PostHogIntegration).flow); +function createStore(program?: ProgramId): FlowStore { + return new FlowStore(flowFor(program ?? Program.PostHogIntegration).flow); } const wizardCaptureMock = analytics.wizardCapture as Mock; @@ -63,7 +63,7 @@ async function flushMicrotasks(): Promise { await Promise.resolve(); } -describe('WizardStore', () => { +describe('FlowStore', () => { beforeEach(() => { vi.clearAllMocks(); evaluateWizardReadinessMock.mockResolvedValue({ @@ -389,7 +389,7 @@ describe('WizardStore', () => { const integration = Integration.nextjs; const config = { metadata: { name: 'Next.js' }, - } as WizardStore['session']['frameworkConfig']; + } as FlowStore['session']['frameworkConfig']; store.setFrameworkConfig(integration, config); diff --git a/src/store/state/run-store.ts b/src/store/state/run-store.ts new file mode 100644 index 000000000..7302a5979 --- /dev/null +++ b/src/store/state/run-store.ts @@ -0,0 +1,395 @@ +/** + * RunStore — the state of one agent run: the run's session copy, its tasks, + * status, phase, outro, and the questions the agent asks. The agent writes + * here through WizardUI; a FlowStore chains runs on top of it. + */ +import { atom, map } from 'nanostores'; +import { computeTokenCostUsd } from '../agent-protocol/token-pricing.js'; +import { + RunPhase, + type AskAnswers, + type OutroData, + type PendingQuestion, + type TaskNotice, + type WizardSession, +} from '../session/wizard-session.js'; +import type { SettingsConflict } from '../services/claude-settings.js'; +import { analytics } from '../shared/analytics.js'; +import { logToFile } from '../shared/debug.js'; +import { + TaskStatus, + isTaskStatus, + type TokenUsageDelta, +} from '../ui/wizard-ui.js'; + +export interface TaskItem { + label: string; + activeForm?: string; + status: TaskStatus; + /** Legacy compat */ + done: boolean; +} + +export interface PlannedEvent { + name: string; + description: string; +} + +/** Running token/cost estimate for the Ctrl+T HUD; `costIsFinal` flips once the run's total is known. */ +export interface TokenUsageSnapshot { + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheCreationTokens: number; + costUsd: number; + costIsFinal: boolean; +} + +export const EMPTY_TOKEN_USAGE: TokenUsageSnapshot = { + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheCreationTokens: 0, + costUsd: 0, + costIsFinal: false, +}; + +/** Total tokens across all counters; the HUD and the exit line use it to detect "no agent turns yet". */ +export function totalTokenCount(usage: TokenUsageSnapshot): number { + return ( + usage.inputTokens + + usage.outputTokens + + usage.cacheReadTokens + + usage.cacheCreationTokens + ); +} + +/** FIFO cap on retained status lines; the status bar's expanded window. */ +export const MAX_STATUS_MESSAGES = 10; + +/** Session fields that belong to one agent run; the flow reads them from its active run. */ +export const RUN_SESSION_KEYS = [ + 'runPhase', + 'outroData', + 'outroDismissed', + 'pendingQuestion', + 'taskNotice', + 'mintHandoff', + 'settingsConflicts', + 'settingsOverrideKeys', +] as const satisfies readonly (keyof WizardSession)[]; + +export type RunSessionKey = (typeof RUN_SESSION_KEYS)[number]; + +/** What a fresh run starts from, whatever the session it was seeded with held. */ +export const RUN_SESSION_DEFAULTS: Pick = { + runPhase: RunPhase.Idle, + outroData: null, + outroDismissed: false, + pendingQuestion: null, + taskNotice: null, + mintHandoff: null, + settingsConflicts: null, + settingsOverrideKeys: null, +}; + +export function pickRunSession( + session: WizardSession, +): Pick { + return Object.fromEntries( + RUN_SESSION_KEYS.map((key) => [key, session[key]]), + ) as Pick; +} + +export class RunStore { + private $session: ReturnType>; + private $statusMessages = atom([]); + private $tasks = atom([]); + private $eventPlan = atom([]); + private $handoffText = atom(null); + private $currentStage = atom<{ stage: string; startedAt: number } | null>( + null, + ); + private $tokenUsage = atom(EMPTY_TOKEN_USAGE); + private $version = atom(0); + + private _onTasksChanged: (() => void) | null = null; + private _resolvePendingQuestion: ((answers: AskAnswers) => void) | null = + null; + private _resolveTaskNotice: ((keep: boolean) => void) | null = null; + private _resolveSettingsOverride: (() => void) | null = null; + private _backupAndFixSettings: (() => boolean) | null = null; + + constructor(session: WizardSession) { + this.$session = map(session); + } + + // ── Session ────────────────────────────────────────────────────── + + get session(): WizardSession { + return this.$session.get(); + } + + set session(value: WizardSession) { + this.$session.set(value); + this.emitChange(); + } + + /** Overwrite some session keys; `emit: false` lets an owner batch the change with its own commit. */ + patchSession(partial: Partial, emit = true): void { + this.$session.set({ ...this.$session.get(), ...partial }); + if (emit) this.emitChange(); + } + + private setKey( + key: K, + value: WizardSession[K], + emit = true, + ): void { + this.$session.setKey(key, value); + if (emit) this.emitChange(); + } + + // ── Run atoms ──────────────────────────────────────────────────── + + get statusMessages(): string[] { + return this.$statusMessages.get(); + } + + get tasks(): TaskItem[] { + return this.$tasks.get(); + } + + get eventPlan(): PlannedEvent[] { + return this.$eventPlan.get(); + } + + get handoffText(): string | null { + return this.$handoffText.get(); + } + + get currentStage(): { stage: string; startedAt: number } | null { + return this.$currentStage.get(); + } + + get tokenUsage(): TokenUsageSnapshot { + return this.$tokenUsage.get(); + } + + /** No-op when the stage hasn't changed, so `startedAt` measures real stage time. */ + setCurrentStage(stage: string): void { + const cur = this.$currentStage.get(); + if (cur?.stage === stage) return; + this.$currentStage.set({ stage, startedAt: Date.now() }); + this.emitChange(); + } + + setRunPhase(phase: RunPhase): void { + this.$session.setKey('runPhase', phase); + analytics.setTag('run_phase', phase); + this.emitChange(); + } + + setOutroData(data: OutroData | null, emit = true): void { + this.setKey('outroData', data, emit); + } + + setOutroDismissed(dismissed = true): void { + this.setKey('outroDismissed', dismissed); + } + + setMintHandoff( + action: NonNullable, + emit = true, + ): void { + this.setKey('mintHandoff', action, emit); + } + + setSkillId(skillId: string | null, emit = true): void { + this.setKey('skillId', skillId, emit); + } + + setFrameworkContext(key: string, value: unknown, emit = true): void { + const ctx = { ...this.$session.get().frameworkContext, [key]: value }; + this.setKey('frameworkContext', ctx, emit); + } + + pushStatus(message: string): void { + const msgs = this.$statusMessages.get(); + // Skip consecutive duplicate messages (no allocation on the hot path) + if (msgs.length > 0 && msgs[msgs.length - 1] === message) return; + const next = + msgs.length >= MAX_STATUS_MESSAGES + ? [...msgs.slice(msgs.length - MAX_STATUS_MESSAGES + 1), message] + : [...msgs, message]; + this.$statusMessages.set(next); + this.emitChange(); + } + + /** Approximate by design: a live indicator, corrected by `setFinalTokenCostUsd`. */ + addTokenUsage(delta: TokenUsageDelta): void { + const cur = this.$tokenUsage.get(); + if (cur.costIsFinal) return; + const deltaCostUsd = computeTokenCostUsd(delta); + this.$tokenUsage.set({ + inputTokens: cur.inputTokens + delta.inputTokens, + outputTokens: cur.outputTokens + delta.outputTokens, + cacheReadTokens: cur.cacheReadTokens + delta.cacheReadTokens, + cacheCreationTokens: cur.cacheCreationTokens + delta.cacheCreationTokens, + costUsd: cur.costUsd + deltaCostUsd, + costIsFinal: false, + }); + this.emitChange(); + } + + setFinalTokenCostUsd(costUsd: number): void { + const cur = this.$tokenUsage.get(); + this.$tokenUsage.set({ ...cur, costUsd, costIsFinal: true }); + this.emitChange(); + } + + setTasks(tasks: TaskItem[], emit = true): void { + this.$tasks.set(tasks); + if (emit) this.emitChange(); + } + + updateTask(index: number, done: boolean): void { + const tasks = this.$tasks.get(); + if (tasks[index]) { + const updated = [...tasks]; + updated[index] = { + ...updated[index], + done, + status: done ? TaskStatus.Completed : TaskStatus.Pending, + }; + this.$tasks.set(updated); + this.emitChange(); + } + } + + setEventPlan(events: PlannedEvent[]): void { + this.$eventPlan.set(events); + this.emitChange(); + } + + /** No-op on identical text: an emit here means a network push downstream. */ + setHandoffText(text: string): void { + if (this.$handoffText.get() === text) return; + logToFile(`store.setHandoffText: ${text.length} chars`); + this.$handoffText.set(text); + this.emitChange(); + } + + syncTodos( + todos: Array<{ content: string; status: string; activeForm?: string }>, + ): void { + const incoming = todos.map((t) => { + const status = isTaskStatus(t.status) ? t.status : TaskStatus.Pending; + return { + label: t.content, + activeForm: t.activeForm, + status, + done: status === TaskStatus.Completed, + }; + }); + const incomingLabels = new Set(incoming.map((t) => t.label)); + const retained = this.$tasks + .get() + .filter((t) => t.done && !incomingLabels.has(t.label)); + this.$tasks.set([...retained, ...incoming]); + this.emitChange(); + this._onTasksChanged?.(); + } + + /** Register a listener for task state changes (e.g. task stream push). */ + set onTasksChanged(fn: () => void) { + this._onTasksChanged = fn; + } + + // ── What the agent waits on: questions, notices, settings ──────── + + /** Record a wizard_ask request; the owner raises the interrupt and resolves it. */ + requestQuestion(question: PendingQuestion): Promise { + if (this._resolvePendingQuestion) { + throw new Error( + 'requestQuestion called while another wizard_ask request is pending', + ); + } + this.setKey('pendingQuestion', question, false); + return new Promise((resolve) => { + this._resolvePendingQuestion = resolve; + }); + } + + /** Clear the request and hand the answers to the waiting agent. */ + resolvePendingQuestion(answers: AskAnswers): boolean { + const resolve = this._resolvePendingQuestion; + this._resolvePendingQuestion = null; + this.setKey('pendingQuestion', null, false); + resolve?.(answers); + return resolve !== null; + } + + showTaskNotice(notice: TaskNotice): Promise { + this.setKey('taskNotice', notice, false); + return new Promise((resolve) => { + this._resolveTaskNotice = resolve; + }); + } + + resolveTaskNotice(keep: boolean): void { + this.setKey('taskNotice', null, false); + this._resolveTaskNotice?.(keep); + this._resolveTaskNotice = null; + } + + showSettingsOverride( + conflicts: SettingsConflict[], + backupAndFix: () => boolean, + ): Promise { + this.patchSession( + { + settingsOverrideKeys: conflicts.flatMap((c) => c.keys), + settingsConflicts: conflicts, + }, + false, + ); + this._backupAndFixSettings = backupAndFix; + return new Promise((resolve) => { + this._resolveSettingsOverride = resolve; + }); + } + + /** Back up .claude/settings.json; true means the overlay may close. */ + backupAndFixSettingsOverride(): boolean { + const ok = this._backupAndFixSettings?.() ?? false; + if (ok) { + this.patchSession( + { settingsOverrideKeys: null, settingsConflicts: null }, + false, + ); + this._resolveSettingsOverride?.(); + this._resolveSettingsOverride = null; + this._backupAndFixSettings = null; + } + return ok; + } + + // ── Change notification ────────────────────────────────────────── + + getVersion(): number { + return this.$version.get(); + } + + emitChange(): void { + this.$version.set(this.$version.get() + 1); + } + + subscribe(callback: () => void): () => void { + return this.$version.listen(() => callback()); + } + + getSnapshot(): number { + return this.$version.get(); + } +} diff --git a/src/store/state/store-api.ts b/src/store/state/store-api.ts index 86344565f..dc073c14e 100644 --- a/src/store/state/store-api.ts +++ b/src/store/state/store-api.ts @@ -1,7 +1,7 @@ -import type { WizardStore } from './store.js'; +import type { FlowStore } from './store.js'; /** - * Every WizardStore member the tui, the cli, and the e2e harness may touch. + * Every FlowStore member the tui, the cli, and the e2e harness may touch. * The architecture suite derives the real usage and fails when the two drift, * so widening the boundary is an explicit edit here. */ @@ -27,7 +27,6 @@ export const STORE_BOUNDARY_MEMBERS = [ 'grantSharing', 'interruptDepth', 'pushStatus', - 'resetRunState', 'resolvePendingQuestion', 'resolvePortConflict', 'resolveTaskNotice', @@ -59,6 +58,7 @@ export const STORE_BOUNDARY_MEMBERS = [ 'setSlackStepDismissed', 'setSpellbook', 'setTasks', + 'startRun', 'showManualAuthCode', 'statusMessages', 'submitManualAuthCode', @@ -74,4 +74,4 @@ export const STORE_BOUNDARY_MEMBERS = [ export type StoreBoundaryMember = (typeof STORE_BOUNDARY_MEMBERS)[number]; /** The store as other surfaces see it. */ -export type WizardStoreApi = Pick; +export type FlowStoreApi = Pick; diff --git a/src/store/state/store.ts b/src/store/state/store.ts index a8f316d48..7619f5e76 100644 --- a/src/store/state/store.ts +++ b/src/store/state/store.ts @@ -1,23 +1,14 @@ /** - * WizardStore — Nanostore-backed reactive store for the TUI. - * React components subscribe via useSyncExternalStore. - * - * The active screen is derived from session state: flow resolution walks - * the flow and shows the first step whose `isComplete` is still false. - * - * Define a step `gate` if your screen needs to await user interactions. - * bin.ts calls `await store.getGate(stepId)` to pause until the gate - * predicate becomes true. - * - * All session mutations that affect screen resolution go through - * explicit setters so emitChange() is always called. + * FlowStore — the state a program flow spans across agent runs: the flow and + * its gates, the interrupts, the session every run inherits, and the active + * RunStore. Screens read it; agents write to the run through WizardUI. The + * active screen is derived from state; setters always emitChange(). */ import { atom, map } from 'nanostores'; import { logToFile } from '../shared/debug.js'; import { TaskStatus, - isTaskStatus, type AuthErrorDetail, type TokenUsageDelta, } from '../ui/wizard-ui.js'; @@ -48,59 +39,29 @@ import { Program, type ProgramId } from '../programs/program-registry.js'; import { analytics, sessionProperties } from '../shared/analytics.js'; import type { StoreInitContext, ProgramReadyContext } from './flow.js'; import { reportWarehouseSourcesDetected } from '../programs/posthog-integration/detect.js'; -import { computeTokenCostUsd } from '../agent-protocol/token-pricing.js'; +import { + RUN_SESSION_DEFAULTS, + RunStore, + pickRunSession, + type PlannedEvent, + type TaskItem, + type TokenUsageSnapshot, +} from './run-store.js'; export { TaskStatus, Program, RunPhase, McpOutcome }; export type { OutroData, WizardSession, ProgramId }; - -export interface TaskItem { - label: string; - activeForm?: string; - status: TaskStatus; - /** Legacy compat */ - done: boolean; -} - -export interface PlannedEvent { - name: string; - description: string; -} - -/** - * Running token/cost estimate for the hidden Ctrl+T HUD. Accumulated live - * from each assistant turn's usage (see `agent-interface.ts`), then - * reconciled to the SDK's authoritative `total_cost_usd` once the run - * completes — `costIsFinal` flips so the HUD can show the number as exact - * rather than a running estimate. - */ -export interface TokenUsageSnapshot { - inputTokens: number; - outputTokens: number; - cacheReadTokens: number; - cacheCreationTokens: number; - costUsd: number; - costIsFinal: boolean; -} - -const EMPTY_TOKEN_USAGE: TokenUsageSnapshot = { - inputTokens: 0, - outputTokens: 0, - cacheReadTokens: 0, - cacheCreationTokens: 0, - costUsd: 0, - costIsFinal: false, -}; - -/** Total tokens across all counters in a `TokenUsageSnapshot` — used by - * both `TokenCostHud` and `exit-line.ts` to detect "no agent turns yet". */ -export function totalTokenCount(usage: TokenUsageSnapshot): number { - return ( - usage.inputTokens + - usage.outputTokens + - usage.cacheReadTokens + - usage.cacheCreationTokens - ); -} +export { + EMPTY_TOKEN_USAGE, + MAX_STATUS_MESSAGES, + RUN_SESSION_KEYS, + RunStore, + totalTokenCount, +} from './run-store.js'; +export type { + PlannedEvent, + TaskItem, + TokenUsageSnapshot, +} from './run-store.js'; interface GateEntry { predicate: (session: WizardSession) => boolean; @@ -109,9 +70,6 @@ interface GateEntry { resolved: boolean; } -/** FIFO cap on retained status lines; the status bar's expanded window. */ -export const MAX_STATUS_MESSAGES = 10; - // Capture blocked skill downloads once per readiness result. function captureHealthCheckBlocked(result: WizardReadinessResult): void { try { @@ -134,20 +92,14 @@ function captureHealthCheckBlocked(result: WizardReadinessResult): void { } } -export class WizardStore { - // ── Internal nanostore atoms ───────────────────────────────────── +export class FlowStore { + /** The session every run inherits; run-scoped keys are read from the active run instead. */ private $session = map(buildSession({})); - private $statusMessages = atom([]); - private $tasks = atom([]); - private $eventPlan = atom([]); - private $handoffText = atom(null); private $version = atom(0); - private $currentStage = atom<{ stage: string; startedAt: number } | null>( - null, - ); - private $tokenUsage = atom(EMPTY_TOKEN_USAGE); - private _onTasksChanged: (() => void) | null = null; + private _run: RunStore; + private _unsubscribeRun: () => void; + /** Last screen seen — used to detect screen transitions for analytics. */ private _lastScreen: string | null = null; @@ -164,25 +116,17 @@ export class WizardStore { /** Interrupts take over the active screen until dismissed, last on top. */ private _interrupts: Interrupt[] = []; - /** Blocks agent execution until the settings-override overlay is dismissed. */ - private _resolveSettingsOverride: (() => void) | null = null; - private _backupAndFixSettings: (() => boolean) | null = null; - - /** Blocks the run until an optional step's notice is answered. */ - private _resolveTaskNotice: ((keep: boolean) => void) | null = null; /** Blocks OAuth flow until the port-conflict overlay is dismissed. */ private _resolvePortConflict: (() => void) | null = null; /** Resolves the OAuth flow with a manually-entered authorization code. */ private _resolveManualAuthCode: ((code: string) => void) | null = null; - /** Resolves the in-flight wizard_ask request. */ - private _resolvePendingQuestion: ((answers: AskAnswers) => void) | null = - null; - constructor(flow: Flow) { this._flow = flow; this._initGates(flow); + this._run = new RunStore(this.$session.get()); + this._unsubscribeRun = this._run.subscribe(() => this.emitChange()); } /** Create one gate promise per step that declares a `gate` predicate. */ @@ -203,11 +147,52 @@ export class WizardStore { } } + // ── Runs ──────────────────────────────────────────────────────── + + /** The store of the active (or last) agent run. */ + get run(): RunStore { + return this._run; + } + /** - * Run the program steps' onInit callbacks. startTUI calls this once - * the screens are actually rendering — constructing a store alone - * (tests, playground) must not fire init work like the health-check - * pre-flight, whose probes belong only to flows that show its screen. + * Begin one independent agent run: a fresh RunStore seeded from `session` + * with a clean run state. The previous run's open question or notice is + * cancelled; credentials, detection, and setup live here and carry over. + */ + startRun(session: WizardSession): RunStore { + this.cancelPendingQuestion(); + if (this._run.session.taskNotice) this.resolveTaskNotice(false); + this._unsubscribeRun(); + this._run = new RunStore({ ...session, ...RUN_SESSION_DEFAULTS }); + this._unsubscribeRun = this._run.subscribe(() => this.emitChange()); + this.emitChange(); + return this._run; + } + + /** + * Mark a composed run step complete (e.g. self-driving's `integrate-run`). + * Records the step id so its `isComplete` predicate holds, clears the task + * list, and resets run phase to Idle so the next run step starts fresh. + */ + completeRunStep(stepId: string): void { + const done = this.$session.get().completedRuns; + if (!done.includes(stepId)) { + this.$session.setKey('completedRuns', [...done, stepId]); + } + this._run.setTasks([], false); + this._run.setRunPhase(RunPhase.Idle); + } + + /** A controlled run's parent released the agent; the runner waits on this. */ + requestRun(): void { + this.$session.setKey('runRequested', true); + this.emitChange(); + } + + /** + * Run the program steps' onInit callbacks. startTUI calls this once the + * screens are actually rendering — constructing a store alone (tests, + * playground) must not fire init work like the health-check pre-flight. */ runInitHooks(): void { const steps = this._flow.steps; @@ -226,10 +211,9 @@ export class WizardStore { } /** - * Run all `onReady` hooks declared by the current flow's steps, in - * order. Must be called after `store.session = session` so hooks see - * the real installDir. bin.ts calls this generically — it doesn't - * need to know which program has which pre-flow work. + * Run all `onReady` hooks declared by the current flow's steps, in order. + * Must be called after `store.session = session` so hooks see the real + * installDir. */ async runReadyHooks(): Promise { const steps = this._flow.steps; @@ -254,29 +238,14 @@ export class WizardStore { // ── Gate API ──────────────────────────────────────────────────── /** - * Get a gate promise by step ID — the primary blocking checkpoint API - * for bin.ts. `await store.getGate('...')` parks the caller until the - * corresponding program step's gate predicate flips to true (if the - * predicate stays false, the caller stays parked indefinitely — the - * TUI keeps rendering so the user can resolve whatever is blocking). - * - * If the program doesn't define a step with this ID, or the step - * has no `gate` predicate, this returns an already-resolved promise - * so bin.ts flows straight through. This lets programs opt in to - * gates on a per-step basis without bin.ts needing to know which - * gates exist in which flow. + * The blocking checkpoint for a runner: parks until the step's gate + * predicate flips to true. A step without a gate resolves at once. */ getGate(stepId: string): Promise { return this._gates.get(stepId)?.promise ?? Promise.resolve(); } - /** - * Resolve once `predicate(session)` is true. Unlike a gate, this is created - * at the await point and evaluated live against the current session, so it - * never latches on a startup value — the orchestrator uses it to wait for a - * decision (a project picked, a handoff acknowledged) without the "true while - * undecided" trap that latched gate predicates have. - */ + /** Resolve once `predicate(session)` is true, evaluated live; never latches on a startup value. */ waitUntil(predicate: (session: WizardSession) => boolean): Promise { if (predicate(this.session)) return Promise.resolve(); return new Promise((resolve) => { @@ -289,14 +258,7 @@ export class WizardStore { }); } - /** - * Re-evaluate every gate predicate against the current session and - * resolve any whose predicate now returns true. Called after every - * emitChange(), so gates unblock as soon as the session mutation - * that satisfies them lands. Gates only resolve once — a predicate - * that goes true → false → true will NOT re-block a caller that - * already awaited through. - */ + /** Gates resolve once; a predicate that goes true, false, true does not re-block. */ private _checkGates(): void { for (const [, gate] of this._gates) { if (!gate.resolved && gate.predicate(this.session)) { @@ -306,44 +268,45 @@ export class WizardStore { } } - // ── State accessors (read from atoms) ──────────────────────────── + // ── State accessors ───────────────────────────────────────────── + /** The session as screens read it: the flow's, with the active run's own keys on top. */ get session(): WizardSession { - return this.$session.get(); + return { ...this.$session.get(), ...pickRunSession(this._run.session) }; } set session(value: WizardSession) { this.$session.set(value); + this._run.patchSession(pickRunSession(value), false); this.emitChange(); } get statusMessages(): string[] { - return this.$statusMessages.get(); + return this._run.statusMessages; } get tasks(): TaskItem[] { - return this.$tasks.get(); + return this._run.tasks; } get eventPlan(): PlannedEvent[] { - return this.$eventPlan.get(); + return this._run.eventPlan; } get handoffText(): string | null { - return this.$handoffText.get(); + return this._run.handoffText; } get currentStage(): { stage: string; startedAt: number } | null { - return this.$currentStage.get(); + return this._run.currentStage; + } + + get tokenUsage(): TokenUsageSnapshot { + return this._run.tokenUsage; } - /** No-op when the stage hasn't changed, so `startedAt` survives across - * re-renders and tab switches and measures real stage time. */ setCurrentStage(stage: string): void { - const cur = this.$currentStage.get(); - if (cur?.stage === stage) return; - this.$currentStage.set({ stage, startedAt: Date.now() }); - this.emitChange(); + this._run.setCurrentStage(stage); } // ── Session setters ───────────────────────────────────────────── @@ -354,44 +317,25 @@ export class WizardStore { completeSetup(): void { this.$session.setKey('setupConfirmed', true); // Reports first: analytics merges tags into an event as it is sent, so - // `setup confirmed` only carries the warehouse tags if they are already - // set. On main they were, because reporting happened back in detect. + // `setup confirmed` only carries the warehouse tags if they are already set. this._markWarehouseSourcesReportedIfNeeded(); analytics.wizardCapture('setup confirmed', sessionProperties(this.session)); this.emitChange(); } - /** - * Sharing is on: either the user turned it back on in the panel, or they - * pressed Continue without ever touching it. Both are reversible until - * completeSetup() resolves the intro gate and reports. - */ + /** Sharing is on; reversible until completeSetup() reports. */ grantSharing(): void { this.$session.setKey('scanConsent', ScanConsent.Granted); this.emitChange(); } - /** - * Sharing is off. Suppresses reporting only — local detection still ran and - * the results stay in the session, so the outro suggestion and the warehouse - * task are unaffected; see `scanConsent` on `WizardSession`. - * - * Deliberately does not report. The panel's toggle can come back here, so - * marking the run reported would strand a user who turns sharing off and - * then on again. completeSetup() owns the single report. - */ + /** Sharing is off: suppresses reporting only; detection results stay. */ declineSharing(): void { this.$session.setKey('scanConsent', ScanConsent.Declined); this.emitChange(); } - /** - * reportWarehouseSourcesDetected() is the single place scan results turn - * into telemetry; this just supplies its idempotency flag via the normal - * setter path (never mutate session directly). A no-op once - * `warehouseSourcesReported` is set, or for any program that never - * populated a warehouse-scan result in the first place. - */ + /** completeSetup() owns the single warehouse-sources report; this supplies its idempotency flag. */ private _markWarehouseSourcesReportedIfNeeded(): void { if (reportWarehouseSourcesDetected(this.session)) { this.$session.setKey('warehouseSourcesReported', true); @@ -399,9 +343,7 @@ export class WizardStore { } setRunPhase(phase: RunPhase): void { - this.$session.setKey('runPhase', phase); - analytics.setTag('run_phase', phase); - this.emitChange(); + this._run.setRunPhase(phase); } setCredentials(credentials: WizardSession['credentials']): void { @@ -466,13 +408,14 @@ export class WizardStore { setMintHandoff(action: NonNullable): void { // The parked agent may still hold a question or notice open. this.cancelPendingQuestion(); - if (this.session.taskNotice) this.resolveTaskNotice(false); - this.$session.setKey('mintHandoff', action); - this.emitChange(); + if (this._run.session.taskNotice) this.resolveTaskNotice(false); + this._run.setMintHandoff(action); } + /** The skill lives with the flow and with the run the agent is in. */ setSkillId(skillId: string | null): void { this.$session.setKey('skillId', skillId); + this._run.setSkillId(skillId, false); this.emitChange(); } @@ -510,35 +453,20 @@ export class WizardStore { this.emitChange(); } - /** - * Push the settings-override overlay and return a promise that blocks - * until the user dismisses it via backupAndFixSettingsOverride(). - */ + /** Push the settings-override overlay; resolves when backupAndFixSettingsOverride() succeeds. */ showSettingsOverride( conflicts: SettingsConflict[], backupAndFix: () => boolean, ): Promise { - const allKeys = conflicts.flatMap((c) => c.keys); - this.$session.setKey('settingsOverrideKeys', allKeys); - this.$session.setKey('settingsConflicts', conflicts); - this._backupAndFixSettings = backupAndFix; - + const pending = this._run.showSettingsOverride(conflicts, backupAndFix); const hasReadOnly = conflicts.some((c) => !c.writable); - if (hasReadOnly) { - this.pushInterrupt(Interrupt.ManagedSettings); - } else { - this.pushInterrupt(Interrupt.SettingsOverride); - } - - return new Promise((resolve) => { - this._resolveSettingsOverride = resolve; - }); + this.pushInterrupt( + hasReadOnly ? Interrupt.ManagedSettings : Interrupt.SettingsOverride, + ); + return pending; } - /** - * Push the port-conflict overlay and return a promise that blocks - * until the user frees the ports and retries, or exits. - */ + /** Push the port-conflict overlay; resolves when the user frees the ports and retries. */ showPortConflict(processInfo: { command: string; pid: string; @@ -560,31 +488,20 @@ export class WizardStore { this._resolvePortConflict = null; } - /** - * Show an optional step's notice and return whether to keep that step. - * Asked before the step runs, so nobody is surprised by a prompt mid-run. - */ + /** Show an optional step's notice before it runs; resolves with whether to keep the step. */ showTaskNotice(notice: TaskNotice): Promise { - this.$session.setKey('taskNotice', notice); + const pending = this._run.showTaskNotice(notice); this.pushInterrupt(Interrupt.TaskNotice); - return new Promise((resolve) => { - this._resolveTaskNotice = resolve; - }); + return pending; } /** Dismiss the notice, keeping (`true`) or skipping (`false`) the step. */ resolveTaskNotice(keep: boolean): void { - this.$session.setKey('taskNotice', null); + this._run.resolveTaskNotice(keep); this.popInterrupt(); - this._resolveTaskNotice?.(keep); - this._resolveTaskNotice = null; } - /** - * Return a promise that resolves when the user submits a manually-entered - * OAuth code via the paste modal. The OAuth flow races this against the - * local callback server — see `performOAuthFlow`. - */ + /** Resolves when the user submits a manually-entered OAuth code; raced against the callback server. */ waitForManualAuthCode(): Promise { return new Promise((resolve) => { this._resolveManualAuthCode = resolve; @@ -601,59 +518,34 @@ export class WizardStore { this.popInterrupt(); } - /** - * Submit a manually-entered authorization code: dismiss the overlay and - * resolve the in-flight OAuth flow so it can exchange the code for a token. - */ + /** Submit a manually-entered authorization code: dismiss the overlay and resolve the OAuth flow. */ submitManualAuthCode(code: string): void { this.popInterrupt(); this._resolveManualAuthCode?.(code); this._resolveManualAuthCode = null; } - /** - * Open the WizardAsk overlay with a set of questions and return a promise - * that resolves once the user submits answers (or the request is cancelled). - * - * Only one request is in flight at a time — calling this while a request - * is already pending throws. - */ + /** Open the WizardAsk overlay; resolves with the answers, or the cancel sentinels. One request at a time. */ requestQuestion(question: PendingQuestion): Promise { - if (this._resolvePendingQuestion) { - throw new Error( - 'requestQuestion called while another wizard_ask request is pending', - ); - } - this.$session.setKey('pendingQuestion', question); + const pending = this._run.requestQuestion(question); this.pushInterrupt(Interrupt.WizardAsk); analytics.wizardCapture('wizard_ask shown', { source: question.source, question_count: question.questions.length, kinds: question.questions.map((q) => q.kind), }); - return new Promise((resolve) => { - this._resolvePendingQuestion = resolve; - }); + return pending; } - /** - * Resolve the in-flight wizard_ask request with the user's answers and - * dismiss the overlay. Answers flow back to the agent as the tool result. - */ + /** Resolve the in-flight wizard_ask request and dismiss the overlay. */ resolvePendingQuestion(answers: AskAnswers): void { - const resolve = this._resolvePendingQuestion; - this._resolvePendingQuestion = null; - this.$session.setKey('pendingQuestion', null); + this._run.resolvePendingQuestion(answers); this.popInterrupt(); - resolve?.(answers); } - /** - * Cancel the in-flight wizard_ask request — the bridge sends a sentinel - * answer ("__cancelled__") so the skill can decide how to handle it. - */ + /** Cancel the in-flight wizard_ask request with the `__cancelled__` sentinel per question. */ cancelPendingQuestion(): void { - const pending = this.session.pendingQuestion; + const pending = this._run.session.pendingQuestion; if (!pending) return; const cancelled: AskAnswers = {}; for (const q of pending.questions) { @@ -662,19 +554,10 @@ export class WizardStore { this.resolvePendingQuestion(cancelled); } - /** - * Back up .claude/settings.json. Dismisses the overlay on success. - */ + /** Back up .claude/settings.json. Dismisses the overlay on success. */ backupAndFixSettingsOverride(): boolean { - const ok = this._backupAndFixSettings?.() ?? false; - if (ok) { - this.$session.setKey('settingsOverrideKeys', null); - this.$session.setKey('settingsConflicts', null); - this.popInterrupt(); - this._resolveSettingsOverride?.(); - this._resolveSettingsOverride = null; - this._backupAndFixSettings = null; - } + const ok = this._run.backupAndFixSettingsOverride(); + if (ok) this.popInterrupt(); return ok; } @@ -690,29 +573,24 @@ export class WizardStore { } addDiscoveredFeature(feature: DiscoveredFeature): void { - if (!this.session.discoveredFeatures.includes(feature)) { - this.session.discoveredFeatures.push(feature); + const features = this.$session.get().discoveredFeatures; + if (!features.includes(feature)) { + this.$session.setKey('discoveredFeatures', [...features, feature]); this.emitChange(); } } - /** - * Enable an additional feature: enqueue it for the stop hook - * and set any feature-specific session flags. - */ + /** Enable an additional feature: enqueue it for the stop hook and set its session flags. */ enableFeature(feature: AdditionalFeature): void { - if (!this.session.additionalFeatureQueue.includes(feature)) { - this.session.additionalFeatureQueue.push(feature); - // Distinct key from `sessionProperties()`'s array-valued - // `additional_features` — see the note in posthog-integration/detect.ts. - analytics.setTag( - 'additional_feature_kinds', - this.session.additionalFeatureQueue.join(','), - ); + const queue = this.$session.get().additionalFeatureQueue; + if (!queue.includes(feature)) { + const next = [...queue, feature]; + this.$session.setKey('additionalFeatureQueue', next); + // Distinct key from `sessionProperties()`'s array-valued `additional_features`. + analytics.setTag('additional_feature_kinds', next.join(',')); } - // Feature-specific flags if (feature === AdditionalFeature.LLM) { - this.session.llmOptIn = true; + this.$session.setKey('llmOptIn', true); } analytics.wizardCapture('feature enabled', { feature }); this.emitChange(); @@ -770,22 +648,14 @@ export class WizardStore { this.emitChange(); } - /** - * Self-driving GitHub gate declined. Carries the outro the user lands on, - * since declining ends the flow before the agent runs and there is no abort - * case to render one. - */ + /** Self-driving GitHub gate declined: carries the outro the user lands on, since no run renders one. */ declineGithub(outroData: OutroData): void { this.$session.setKey('githubDeclined', true); - this.$session.setKey('outroData', outroData); + this._run.setOutroData(outroData, false); this.emitChange(); } - /** - * Self-driving integration-check answer. `true` → integrate the SDK as part - * of this run; `false` → PostHog is already set up, go straight to - * Self-driving. Resolves `session.integrate` from null. - */ + /** Self-driving integration-check answer; resolves `session.integrate` from null. */ setIntegrate( integrate: boolean, extra?: { via?: string; path?: string }, @@ -800,15 +670,7 @@ export class WizardStore { this.emitChange(); } - /** - * Self-driving "no PostHog account" branch of the integration check. The - * project has no SDK, so we always integrate (`integrate = true`); and since - * the user has no account, we flip `signup` and record the `email` / `region` - * collected on the screen so `authenticate` → `getOrAskForProjectData` takes - * the provisioning path (create account + email a login link) instead of - * OAuth. The "yes, I have an account" branch uses `setIntegrate(true)` and - * leaves `signup` false so auth runs the normal OAuth login. - */ + /** Self-driving "no PostHog account" branch: integrate, and provision an account at auth. */ chooseProvisionAccount(email: string, region: CloudRegion): void { this.$session.setKey('signup', true); this.$session.setKey('email', email); @@ -823,63 +685,21 @@ export class WizardStore { this.emitChange(); } - /** - * Self-driving handoff confirmed — the user acknowledged the post-integration - * screen, so the Self-driving run can begin. Gate resolves via _checkGates(). - */ + /** Self-driving handoff confirmed; the Self-driving run can begin. */ confirmSelfDrivingHandoff(): void { this.$session.setKey('selfDrivingHandoffConfirmed', true); this.emitChange(); } - /** - * Mark a composed run step complete (e.g. self-driving's `integrate-run`). - * Records the step id so its `isComplete` predicate holds, clears the task - * list, and resets run phase to Idle so the next run step starts fresh. - */ - completeRunStep(stepId: string): void { - const done = this.session.completedRuns; - if (!done.includes(stepId)) { - this.$session.setKey('completedRuns', [...done, stepId]); - } - this.$tasks.set([]); - this.setRunPhase(RunPhase.Idle); - } - - /** Clear what one agent run leaves behind, so the next independent run starts clean. */ - resetRunState(): void { - this.cancelPendingQuestion(); - if (this.session.taskNotice) this.resolveTaskNotice(false); - this.$tasks.set([]); - this.$statusMessages.set([]); - this.$eventPlan.set([]); - this.$handoffText.set(null); - this.$tokenUsage.set(EMPTY_TOKEN_USAGE); - this.$currentStage.set(null); - this.$session.setKey('runPhase', RunPhase.Idle); - this.$session.setKey('outroData', null); - this.$session.setKey('outroDismissed', false); - this.$session.setKey('dashboardUrl', null); - this.$session.setKey('notebookUrl', null); - this.emitChange(); - } - - /** A controlled run's parent released the agent; the runner waits on this. */ - requestRun(): void { - this.$session.setKey('runRequested', true); - this.emitChange(); - } - setOutroDismissed(dismissed = true): void { - this.$session.setKey('outroDismissed', dismissed); - this.emitChange(); + this._run.setOutroDismissed(dismissed); } setOutroData(data: OutroData): void { - this.$session.setKey('outroData', data); - this.emitChange(); + this._run.setOutroData(data); } + /** Artefacts a run created for the project outlive it: later runs and the outro link them. */ setDashboardUrl(url: string): void { logToFile(`store.setDashboardUrl: ${url}`); this.$session.setKey('dashboardUrl', url); @@ -892,9 +712,11 @@ export class WizardStore { this.emitChange(); } + /** Context lives with the flow and with the run the agent is in. */ setFrameworkContext(key: string, value: unknown): void { const ctx = { ...this.$session.get().frameworkContext, [key]: value }; this.$session.setKey('frameworkContext', ctx); + this._run.setFrameworkContext(key, value, false); this.emitChange(); } @@ -915,6 +737,7 @@ export class WizardStore { this.$session.setKey('setupConfirmed', false); this.$session.setKey('programLabel', flow.programId); this.$session.setKey('skillId', flow.skillId); + this._run.setSkillId(flow.skillId, false); this.emitChange(); } @@ -948,11 +771,7 @@ export class WizardStore { return this.$version.get(); } - /** - * Notify React that state has changed. - * The active screen re-resolves on next render. - * Gate predicates are checked and resolved if ready. - */ + /** Bump the version, resolve gates that came true, and record a screen transition. */ emitChange(): void { this.$version.set(this.$version.get() + 1); this._checkGates(); @@ -973,45 +792,32 @@ export class WizardStore { this._detectTransition(); } - // ── ScreenId transition analytics ───────────────────────────────── + // ── Screen transition analytics ─────────────────────────────────── - /** - * Register a callback to run when transitioning onto the given screen. - * Fires after every transition that lands on this screen. - */ + /** Register a callback to run after every transition that lands on `screen`. */ onEnterScreen(screen: string, fn: () => void): void { const list = this._enterScreenHooks.get(screen) ?? []; list.push(fn); this._enterScreenHooks.set(screen, list); } - /** - * The program `screen` reports under — its step's `reportsAsProgramId` if it - * claims one, else the running program (also the fallback for overlays and - * screens with no owning step). - */ + /** The program `screen` reports under: its step's `reportsAsProgramId`, else the running program. */ private _programIdForScreen(screen: string): ProgramId { const program = this._flow.programId; const step = this._flow.steps.find((s) => s.screenId === screen); return step?.reportsAsProgramId ?? program; } - /** The program the visible screen reports under; screens stamp this on their - * own events rather than relying on the run-level `program_id` tag. */ + /** The program the visible screen reports under. */ get analyticsProgramId(): ProgramId { return this._programIdForScreen(this.currentScreen); } - /** - * Detect screen transitions, run enter-screen hooks, and fire analytics. - * Called at the end of emitChange/pushInterrupt/popInterrupt. - */ private _detectTransition(): void { const next = this.currentScreen; const prev = this._lastScreen; if (next !== prev) { - // Every event carries the active TUI screen, filling the - // "URL / Screen" column in PostHog. + // Every event carries the active TUI screen, filling the "URL / Screen" column. analytics.setTag('$screen_name', next); } if (prev !== null && next !== prev) { @@ -1028,117 +834,45 @@ export class WizardStore { this._lastScreen = next; } - // ── Agent observation state ───────────────────────────────────── + // ── Agent observation state, delegated to the active run ──────── pushStatus(message: string): void { - const msgs = this.$statusMessages.get(); - // Skip consecutive duplicate messages (no allocation on the hot path) - if (msgs.length > 0 && msgs[msgs.length - 1] === message) return; - // Nanostore detects change by reference equality, so a new array is - // required. At the cap, allocate exactly once at the final size (dropping - // the oldest entry) rather than push-then-truncate. - const next = - msgs.length >= MAX_STATUS_MESSAGES - ? [...msgs.slice(msgs.length - MAX_STATUS_MESSAGES + 1), message] - : [...msgs, message]; - this.$statusMessages.set(next); - this.emitChange(); + this._run.pushStatus(message); } - get tokenUsage(): TokenUsageSnapshot { - return this.$tokenUsage.get(); - } - - /** - * Accumulate one assistant turn's token usage into the running estimate. - * Approximate by design (no dedup for SDK-retried/replayed turns, unlike - * the benchmark middleware's TurnCounterPlugin) — it's a live indicator - * for a hidden debug HUD, not a billing record, and `setFinalTokenCostUsd` - * corrects the total once the run's authoritative cost is known. - */ addTokenUsage(delta: TokenUsageDelta): void { - const cur = this.$tokenUsage.get(); - if (cur.costIsFinal) return; - const deltaCostUsd = computeTokenCostUsd(delta); - this.$tokenUsage.set({ - inputTokens: cur.inputTokens + delta.inputTokens, - outputTokens: cur.outputTokens + delta.outputTokens, - cacheReadTokens: cur.cacheReadTokens + delta.cacheReadTokens, - cacheCreationTokens: cur.cacheCreationTokens + delta.cacheCreationTokens, - costUsd: cur.costUsd + deltaCostUsd, - costIsFinal: false, - }); - this.emitChange(); + this._run.addTokenUsage(delta); } - /** Reconcile the running cost estimate to the SDK's authoritative total - * once the agent run completes — same trick the benchmark's - * CostTrackerPlugin.onFinalize uses to correct any per-turn drift. */ setFinalTokenCostUsd(costUsd: number): void { - const cur = this.$tokenUsage.get(); - this.$tokenUsage.set({ ...cur, costUsd, costIsFinal: true }); - this.emitChange(); + this._run.setFinalTokenCostUsd(costUsd); } setTasks(tasks: TaskItem[]): void { - this.$tasks.set(tasks); - this.emitChange(); + this._run.setTasks(tasks); } updateTask(index: number, done: boolean): void { - const tasks = this.$tasks.get(); - if (tasks[index]) { - const updated = [...tasks]; - updated[index] = { - ...updated[index], - done, - status: done ? TaskStatus.Completed : TaskStatus.Pending, - }; - this.$tasks.set(updated); - this.emitChange(); - } + this._run.updateTask(index, done); } setEventPlan(events: PlannedEvent[]): void { - this.$eventPlan.set(events); - this.emitChange(); + this._run.setEventPlan(events); } - /** No-op on identical text: an emit here means a network push downstream. */ setHandoffText(text: string): void { - if (this.$handoffText.get() === text) return; - logToFile(`store.setHandoffText: ${text.length} chars`); - this.$handoffText.set(text); - this.emitChange(); + this._run.setHandoffText(text); } syncTodos( todos: Array<{ content: string; status: string; activeForm?: string }>, ): void { - const incoming = todos.map((t) => { - const status = isTaskStatus(t.status) ? t.status : TaskStatus.Pending; - return { - label: t.content, - activeForm: t.activeForm, - status, - done: status === TaskStatus.Completed, - }; - }); - - const incomingLabels = new Set(incoming.map((t) => t.label)); - - const retained = this.$tasks - .get() - .filter((t) => t.done && !incomingLabels.has(t.label)); - - this.$tasks.set([...retained, ...incoming]); - this.emitChange(); - this._onTasksChanged?.(); + this._run.syncTodos(todos); } - /** Register a listener for task state changes (e.g. task stream push). */ + /** Register a listener for task state changes on the active run. */ set onTasksChanged(fn: () => void) { - this._onTasksChanged = fn; + this._run.onTasksChanged = fn; } // ── React integration ─────────────────────────────────────────── diff --git a/src/store/task-stream/__tests__/event-plan-watcher.test.ts b/src/store/task-stream/__tests__/event-plan-watcher.test.ts index 594813f70..8dbb9cf4c 100644 --- a/src/store/task-stream/__tests__/event-plan-watcher.test.ts +++ b/src/store/task-stream/__tests__/event-plan-watcher.test.ts @@ -9,7 +9,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { EventPlanWatcher, normalizeEventPlan } from '../event-plan-watcher.js'; import { EVENT_PLAN_FILE } from '../../programs/posthog-integration/constants.js'; -import type { PlannedEvent, WizardStore } from '../../state/store.js'; +import type { PlannedEvent, FlowStore } from '../../state/store.js'; const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -23,7 +23,7 @@ function createStore(installDir: string) { setEventPlan(events: PlannedEvent[]) { eventPlan = events; }, - } as WizardStore; + } as FlowStore; } describe('EventPlanWatcher', () => { diff --git a/src/store/task-stream/__tests__/task-stream-push.test.ts b/src/store/task-stream/__tests__/task-stream-push.test.ts index 71cea2020..d67369767 100644 --- a/src/store/task-stream/__tests__/task-stream-push.test.ts +++ b/src/store/task-stream/__tests__/task-stream-push.test.ts @@ -1,7 +1,7 @@ import { TaskStreamPush } from '../task-stream-push.js'; import { StreamEvent, StreamTaskStatus } from '../types.js'; import type { TaskStreamDestination, TaskStreamUpdate } from '../types.js'; -import type { WizardStore, TaskItem } from '../../state/store.js'; +import type { FlowStore, TaskItem } from '../../state/store.js'; import { TaskStatus } from '../../ui/wizard-ui.js'; import { RunPhase, @@ -91,7 +91,7 @@ function createMockStore(overrides: Partial = {}) { }, }; - return store as typeof store & WizardStore; + return store as typeof store & FlowStore; } function createMockDestination(name = 'test'): TaskStreamDestination & { diff --git a/src/store/task-stream/event-plan-watcher.ts b/src/store/task-stream/event-plan-watcher.ts index e30beee03..27aacdb18 100644 --- a/src/store/task-stream/event-plan-watcher.ts +++ b/src/store/task-stream/event-plan-watcher.ts @@ -1,4 +1,5 @@ -import type { PlannedEvent, WizardStore } from '../state/store.js'; +import type { PlannedEvent } from '../state/store.js'; +import type { RunStateSource } from './types.js'; import { startFileWatcher, type FileWatcherHandle, @@ -45,7 +46,7 @@ export class EventPlanWatcher { private captured = false; constructor( - private readonly store: WizardStore, + private readonly store: RunStateSource, private readonly path: string, private readonly options: FileWatcherOptions = {}, ) {} diff --git a/src/store/task-stream/task-stream-push.ts b/src/store/task-stream/task-stream-push.ts index 7b1ee84ec..c40179711 100644 --- a/src/store/task-stream/task-stream-push.ts +++ b/src/store/task-stream/task-stream-push.ts @@ -1,5 +1,5 @@ /** - * Task-stream push — subscribes to WizardStore, builds payloads, + * Task-stream push — subscribes to one RunStore, builds payloads, * and fans out async to all registered destinations. * * Behaviour: @@ -16,7 +16,7 @@ * latest state once the current one settles. */ -import type { WizardStore, TaskItem } from '../state/store.js'; +import type { TaskItem } from '../state/store.js'; import { TaskStatus } from '../ui/wizard-ui.js'; import { RunPhase, @@ -30,6 +30,7 @@ import { type StreamTask, type TaskStreamError, type StreamPendingInput, + type RunStateSource, StreamTaskStatus, StreamEvent, } from './types.js'; @@ -111,7 +112,7 @@ function buildPendingInput( } export interface TaskStreamPushOptions { - store: WizardStore; + store: RunStateSource; programId: string; destinations: TaskStreamDestination[]; /** Optional absolute event-plan path to load into the store once. */ @@ -125,7 +126,7 @@ export interface TaskStreamPushOptions { } export class TaskStreamPush { - private readonly store: WizardStore; + private readonly store: RunStateSource; private readonly destinations: TaskStreamDestination[]; private readonly startedAt: string; private readonly programId: string; @@ -171,7 +172,7 @@ export class TaskStreamPush { * remains disabled when `enabled === false`, but the plan still populates the * store for local and headless consumers. */ - attach(store?: WizardStore): void { + attach(store?: RunStateSource): void { this.eventPlanWatcher?.start(); if (!this.enabled) return; if (this.unsubscribe) return; diff --git a/src/store/task-stream/types.ts b/src/store/task-stream/types.ts index a5cd80ed2..6cbb5bcbb 100644 --- a/src/store/task-stream/types.ts +++ b/src/store/task-stream/types.ts @@ -105,3 +105,16 @@ export interface TaskStreamDestination { readonly name: string; send(event: StreamEvent, payload: TaskStreamUpdate): Promise; } + +import type { RunStore } from '../state/run-store.js'; + +/** What a task stream reads from the run it publishes: a RunStore, or a flow store standing in for one. */ +export type RunStateSource = Pick< + RunStore, + | 'session' + | 'tasks' + | 'eventPlan' + | 'handoffText' + | 'subscribe' + | 'setEventPlan' +>; diff --git a/src/store/testing/index.ts b/src/store/testing/index.ts index bc6ea4c16..de238ac84 100644 --- a/src/store/testing/index.ts +++ b/src/store/testing/index.ts @@ -1,22 +1,22 @@ import { flowFor } from '../programs/flow-for.js'; import { Program, type ProgramId } from '../programs/program-registry.js'; import { buildSession } from '../session/wizard-session.js'; -import { WizardStore } from '../state/store.js'; +import { FlowStore } from '../state/store.js'; import { setUI } from '../ui/index.js'; import { StoreUI } from '../ui/store-ui.js'; /** A real store on a real program flow; tests fake nothing below it. */ export function createTestStore( programId: ProgramId = Program.PostHogIntegration, -): WizardStore { - return new WizardStore(flowFor(programId).flow); +): FlowStore { + return new FlowStore(flowFor(programId).flow); } /** A store behind `StoreUI` with a non-interactive session: what a controlled run drives. */ export function createControlledStore( programId: ProgramId = Program.PostHogIntegration, session: Partial[0]> = {}, -): WizardStore { +): FlowStore { const store = createTestStore(programId); setUI(new StoreUI(store)); store.session = buildSession({ diff --git a/src/store/types.ts b/src/store/types.ts index 2b950493a..bc77406f0 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -78,7 +78,8 @@ export type { CloudRegion, WizardRunOptions } from './shared/types.js'; export type { PlannedEvent, TokenUsageSnapshot, - WizardStore, + FlowStore, + RunStore, } from './state/store.js'; export type { TaskStreamPush } from './task-stream/task-stream-push.js'; export type { @@ -93,7 +94,7 @@ export type { WizardUI, } from './ui/wizard-ui.js'; export type { WizardSpellbook } from './wizard-spellbook.js'; -export type { StoreBoundaryMember, WizardStoreApi } from './state/store-api.js'; +export type { StoreBoundaryMember, FlowStoreApi } from './state/store-api.js'; export type { ActionView, ControlHooks, diff --git a/src/store/ui/store-ui.ts b/src/store/ui/store-ui.ts index e230d8dfc..fd9008b6b 100644 --- a/src/store/ui/store-ui.ts +++ b/src/store/ui/store-ui.ts @@ -6,7 +6,7 @@ import type { AuthErrorDetail, TokenUsageDelta, } from './wizard-ui.js'; -import type { WizardStore } from '../state/store.js'; +import type { FlowStore } from '../state/store.js'; import type { SettingsConflict } from '../services/claude-settings.js'; import type { WizardReadinessResult } from '../health-checks/readiness.js'; import type { ApiUser } from '../api.js'; @@ -28,7 +28,7 @@ function stripAnsi(s: string): string { export class StoreUI implements WizardUI { readonly interactive = true; - constructor(private store: WizardStore) {} + constructor(private store: FlowStore) {} intro(message: string): void { this.store.pushStatus(message); diff --git a/src/tui/App.tsx b/src/tui/App.tsx index c879a2967..5d5c717fc 100644 --- a/src/tui/App.tsx +++ b/src/tui/App.tsx @@ -1,11 +1,11 @@ import { useMemo } from 'react'; import { ScreenContainer } from './primitives/index.js'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import type { UiStore } from './ui-store.js'; import { createScreens, createServices } from './screen-registry.js'; interface AppProps { - store: WizardStore; + store: FlowStore; ui: UiStore; } diff --git a/src/tui/README.md b/src/tui/README.md index 89f827cb2..4c59e9850 100644 --- a/src/tui/README.md +++ b/src/tui/README.md @@ -5,7 +5,7 @@ All rendering: Ink screens, primitives, presentation state, console renderers. ## Owns - `App.tsx`, `start-tui.ts`, screens, primitives, components, hooks, playground. -- `ui-store.ts`: presentation state that watches the injected `WizardStore`. +- `ui-store.ts`: presentation state that watches the injected `FlowStore`. - `programs/`: content decks and tips keyed by program id. - `console/`: `LoggingUI` and `HeadlessUI`. Ink free; headless builds ship them. - Agent work the TUI needs arrives through the store's `getMcpPromptRunner`. diff --git a/src/tui/__tests__/MintFailureScreen.test.tsx b/src/tui/__tests__/MintFailureScreen.test.tsx index 24ef6cc69..e8e40b1cb 100644 --- a/src/tui/__tests__/MintFailureScreen.test.tsx +++ b/src/tui/__tests__/MintFailureScreen.test.tsx @@ -1,6 +1,6 @@ import { vi, it, expect, afterEach } from 'vitest'; import { render, cleanup } from 'ink-testing-library'; -import { WizardStore } from '@store/state/store'; +import { FlowStore } from '@store/state/store'; import { MintFailureScreen, type MintFailureServices, @@ -27,7 +27,7 @@ const saved = { const delay = () => new Promise((resolve) => setTimeout(resolve, 30)); function setup() { - const store = new WizardStore(flowFor(Program.PostHogIntegration).flow); + const store = new FlowStore(flowFor(Program.PostHogIntegration).flow); store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', diff --git a/src/tui/__tests__/WizardAskScreen.test.ts b/src/tui/__tests__/WizardAskScreen.test.ts index 30ddfdb1b..ae5fba067 100644 --- a/src/tui/__tests__/WizardAskScreen.test.ts +++ b/src/tui/__tests__/WizardAskScreen.test.ts @@ -19,7 +19,7 @@ vi.mock('@store/shared/analytics', () => ({ sessionProperties: vi.fn(() => ({})), })); -import { WizardStore } from '@store/state/store'; +import { FlowStore } from '@store/state/store'; import { askEscapeHint, handleAskKey, @@ -53,7 +53,7 @@ describe('handleAskKey', () => { }); it('declines the whole request end-to-end so the task can fall back', async () => { - const store = new WizardStore(flowFor(Program.PostHogIntegration).flow); + const store = new FlowStore(flowFor(Program.PostHogIntegration).flow); const answers = store.requestQuestion(pending); handleAskKey({ escape: true }, store); diff --git a/src/tui/__tests__/contract.test.ts b/src/tui/__tests__/contract.test.ts index a5b6c4383..e7d6478ac 100644 --- a/src/tui/__tests__/contract.test.ts +++ b/src/tui/__tests__/contract.test.ts @@ -1,5 +1,5 @@ import { expectTypeOf } from 'vitest'; -import { WizardStore } from '@store'; +import { FlowStore } from '@store'; import { flowFor, Program, PROGRAM_REGISTRY } from '@store/programs'; import type { WizardUI } from '@store/types'; import { HeadlessUI } from '../console/headless-ui.js'; @@ -13,7 +13,7 @@ describe('tui contract', () => { expectTypeOf().toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); expect(new LoggingUI().interactive).toBe(false); - const store = new WizardStore(flowFor(Program.PostHogIntegration).flow); + const store = new FlowStore(flowFor(Program.PostHogIntegration).flow); expect(new HeadlessUI(store).interactive).toBe(false); }); diff --git a/src/tui/__tests__/exit-line.test.ts b/src/tui/__tests__/exit-line.test.ts index 044aa3571..ae69cee88 100644 --- a/src/tui/__tests__/exit-line.test.ts +++ b/src/tui/__tests__/exit-line.test.ts @@ -1,5 +1,5 @@ import { getExitLine } from '../exit-line.js'; -import { WizardStore, Program } from '@store/state/store'; +import { FlowStore, Program } from '@store/state/store'; import { OutroKind } from '@store/session/wizard-session'; import { HostResolution } from '@store/host-resolution'; import { flowFor } from '@store/programs/flow-for'; @@ -20,15 +20,15 @@ vi.mock('@store/shared/analytics', () => ({ const stripAnsi = (s: string): string => s.replace(/\x1b\[[0-9;]*m/g, ''); function storeWithOutro( - data: Parameters[0], -): WizardStore { - const store = new WizardStore(flowFor(Program.PostHogIntegration).flow); + data: Parameters[0], +): FlowStore { + const store = new FlowStore(flowFor(Program.PostHogIntegration).flow); store.setOutroData(data); return store; } -const uiFor = new WeakMap(); -function uiOf(store: WizardStore): UiStore { +const uiFor = new WeakMap(); +function uiOf(store: FlowStore): UiStore { let ui = uiFor.get(store); if (!ui) { ui = new UiStore(store); @@ -36,10 +36,10 @@ function uiOf(store: WizardStore): UiStore { } return ui; } -const exitLine = (store: WizardStore) => getExitLine(store, uiOf(store)); +const exitLine = (store: FlowStore) => getExitLine(store, uiOf(store)); /** Force `tokenHudVisible` to `visible`, regardless of its IS_DEV default. */ -function setHudVisible(store: WizardStore, visible: boolean): void { +function setHudVisible(store: FlowStore, visible: boolean): void { if (uiOf(store).tokenHudVisible !== visible) uiOf(store).toggleTokenHud(); } diff --git a/src/tui/__tests__/frames.test.tsx b/src/tui/__tests__/frames.test.tsx index 0bdc70e5e..5eaa02ddb 100644 --- a/src/tui/__tests__/frames.test.tsx +++ b/src/tui/__tests__/frames.test.tsx @@ -97,7 +97,7 @@ vi.mock('@store/programs/posthog-doctor/fetch', () => ({ ]), })); -import { WizardStore, TaskStatus } from '@store/state/store'; +import { FlowStore, TaskStatus } from '@store/state/store'; import type { ScreenName } from '../router.js'; import { StoreUI } from '@store/ui/store-ui'; import { setUI } from '@store/ui'; @@ -204,15 +204,15 @@ const inertPromptsServices = { seedDemoEvents: pending, } as unknown as McpSuggestedPromptsServices; -function makeStore(program: ProgramId): WizardStore { - const store = new WizardStore(flowFor(program).flow); +function makeStore(program: ProgramId): FlowStore { + const store = new FlowStore(flowFor(program).flow); setUI(new StoreUI(store)); store.version = '0.0.0-test'; store.session = buildSession({ installDir: '/app' }); return store; } -function makeServices(store: WizardStore): ScreenServices { +function makeServices(store: FlowStore): ScreenServices { return { ...createServices(store), mcpInstaller: fakeInstaller, @@ -220,20 +220,20 @@ function makeServices(store: WizardStore): ScreenServices { }; } -function authed(store: WizardStore): void { +function authed(store: FlowStore): void { store.completeSetup(); store.setReadinessResult(HEALTHY); store.setCredentials(CREDENTIALS); } -function ranSuccessfully(store: WizardStore): void { +function ranSuccessfully(store: FlowStore): void { store.setRunPhase(RunPhase.Completed); store.setOutroData(SUCCESS_OUTRO); } interface Fixture { program: ProgramId; - arrange?: (store: WizardStore) => void; + arrange?: (store: FlowStore) => void; } const FIXTURES: Record = { @@ -546,7 +546,7 @@ const FIXTURES: Record = { }; /** Only the org's AI consent and membership level drive the gate screen. */ -function apiUser(approved: boolean): WizardStore['session']['apiUser'] { +function apiUser(approved: boolean): FlowStore['session']['apiUser'] { return { distinct_id: 'user-1', team: { id: 42, organization: '00000000-0000-0000-0000-000000000000' }, @@ -556,7 +556,7 @@ function apiUser(approved: boolean): WizardStore['session']['apiUser'] { membership_level: 8, is_ai_data_processing_approved: approved, }, - } as unknown as WizardStore['session']['apiUser']; + } as unknown as FlowStore['session']['apiUser']; } beforeAll(() => { diff --git a/src/tui/__tests__/headless-ui.test.ts b/src/tui/__tests__/headless-ui.test.ts index fcd656601..2a12e104b 100644 --- a/src/tui/__tests__/headless-ui.test.ts +++ b/src/tui/__tests__/headless-ui.test.ts @@ -1,11 +1,11 @@ import { HeadlessUI } from '../console/headless-ui.js'; import { TaskStatus } from '@store/ui/wizard-ui'; -import type { WizardStore } from '@store/state/store'; +import type { FlowStore } from '@store/state/store'; describe('HeadlessUI', () => { it('forwards task updates to the store and still logs to the console', () => { const syncTodos = vi.fn(); - const store = { syncTodos } as unknown as WizardStore; + const store = { syncTodos } as unknown as FlowStore; const ui = new HeadlessUI(store); const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); diff --git a/src/tui/__tests__/helpers/render-screen.no-jest.tsx b/src/tui/__tests__/helpers/render-screen.no-jest.tsx index 2d5e942a3..cba7aba91 100644 --- a/src/tui/__tests__/helpers/render-screen.no-jest.tsx +++ b/src/tui/__tests__/helpers/render-screen.no-jest.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from 'react'; import { vi } from 'vitest'; import { ScreenContainer } from '../../primitives/ScreenContainer.js'; import { createScreens, type ScreenServices } from '../../screen-registry.js'; -import type { WizardStore } from '@store/state/store'; +import type { FlowStore } from '@store/state/store'; import { UiStore } from '../../ui-store.js'; export interface TerminalSize { @@ -36,7 +36,7 @@ export async function flushInk(ms = 50): Promise { /** The shell App.tsx builds: router-resolved screen inside the full chrome. */ export function screenShell( - store: WizardStore, + store: FlowStore, services: ScreenServices, ): ReactNode { return ( @@ -49,7 +49,7 @@ export function screenShell( } export async function renderScreen( - store: WizardStore, + store: FlowStore, element: ReactNode, { columns, rows }: TerminalSize, ): Promise { diff --git a/src/tui/__tests__/task-notice.test.ts b/src/tui/__tests__/task-notice.test.ts index a57ed8c2a..18b1482bd 100644 --- a/src/tui/__tests__/task-notice.test.ts +++ b/src/tui/__tests__/task-notice.test.ts @@ -15,7 +15,7 @@ vi.mock('@store/shared/analytics', () => ({ sessionProperties: vi.fn(() => ({})), })); -import { WizardStore } from '@store/state/store'; +import { FlowStore } from '@store/state/store'; import { Overlay } from '../router.js'; import { flowFor } from '@store/programs/flow-for'; import { Program } from '@store/programs/program-registry'; @@ -36,7 +36,7 @@ const NOTICE: TaskNotice = { describe('task notice', () => { it('resolves true when kept and false when skipped, closing the overlay', async () => { - const store = new WizardStore(flowFor(Program.PostHogIntegration).flow); + const store = new FlowStore(flowFor(Program.PostHogIntegration).flow); const kept = store.showTaskNotice(NOTICE); expect(store.currentScreen).toBe(Overlay.TaskNotice); @@ -51,7 +51,7 @@ describe('task notice', () => { }); it('leaves no notice behind for the next step to inherit', () => { - const store = new WizardStore(flowFor(Program.PostHogIntegration).flow); + const store = new FlowStore(flowFor(Program.PostHogIntegration).flow); expect(store.session.taskNotice).toBeNull(); }); }); diff --git a/src/tui/__tests__/ui-store.test.ts b/src/tui/__tests__/ui-store.test.ts index 9973d90d1..05d1492b5 100644 --- a/src/tui/__tests__/ui-store.test.ts +++ b/src/tui/__tests__/ui-store.test.ts @@ -1,4 +1,4 @@ -import { WizardStore } from '@store/state/store'; +import { FlowStore } from '@store/state/store'; import { UiStore } from '../ui-store.js'; import { Overlay } from '../router.js'; import { flowFor } from '@store/programs/flow-for'; @@ -15,7 +15,7 @@ vi.mock('@store/shared/analytics', () => ({ })); function pair() { - const store = new WizardStore(flowFor(Program.PostHogIntegration).flow); + const store = new FlowStore(flowFor(Program.PostHogIntegration).flow); return { store, ui: new UiStore(store) }; } diff --git a/src/tui/components/LearnCard.tsx b/src/tui/components/LearnCard.tsx index a6753db0e..5882979d9 100644 --- a/src/tui/components/LearnCard.tsx +++ b/src/tui/components/LearnCard.tsx @@ -9,7 +9,7 @@ import { Box, Text } from 'ink'; import { Colors } from '../styles.js'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { ContentSequencer, TextRevealMode } from '../primitives/index.js'; import type { ContentBlock } from '../primitives/index.js'; import { useStdoutDimensions } from '../hooks/useStdoutDimensions.js'; @@ -22,7 +22,7 @@ const HEADER_ROWS = 2; // title + spacer const MIN_CONTENT_ROWS = 6; interface LearnCardProps { - store?: WizardStore; + store?: FlowStore; /** The script to play. Program-owned; see programs//content/. */ blocks: ContentBlock[]; onComplete?: () => void; diff --git a/src/tui/components/PhaseVisuals.tsx b/src/tui/components/PhaseVisuals.tsx index 1476adba1..2292cb06d 100644 --- a/src/tui/components/PhaseVisuals.tsx +++ b/src/tui/components/PhaseVisuals.tsx @@ -10,7 +10,7 @@ import { Box, Text, measureElement, type DOMElement } from 'ink'; import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { useTick } from '../hooks/useTick.js'; import { AgentPhase } from '@store'; import { MATRIX_FADE } from './visualizer/panel.js'; @@ -36,7 +36,7 @@ const PHASE_LABELS: Record = { /** Reads the active phase from the store. The agent loop pushes it in via * `getUI().setStage(...)` whenever a new tool fires. */ -export function useAgentPhase(store: WizardStore): AgentPhase { +export function useAgentPhase(store: FlowStore): AgentPhase { useSyncExternalStore( (cb) => store.subscribe(cb), () => store.getSnapshot(), @@ -45,7 +45,7 @@ export function useAgentPhase(store: WizardStore): AgentPhase { } interface PhaseVisualProps { - store: WizardStore; + store: FlowStore; width: number; height: number; } @@ -64,7 +64,7 @@ export const PhaseVisual = ({ store, width, height }: PhaseVisualProps) => { * short terminals. When height runs out, chrome rows drop in order: * transport bar first, then track title, then the NOW PLAYING header. */ -export const VisualizerTab = ({ store }: { store: WizardStore }) => { +export const VisualizerTab = ({ store }: { store: FlowStore }) => { const phase = useAgentPhase(store); const containerRef = useRef(null); const [size, setSize] = useState<{ width: number; height: number }>({ diff --git a/src/tui/components/StatusPeekTrigger.tsx b/src/tui/components/StatusPeekTrigger.tsx index 3a13eb080..11417aa5d 100644 --- a/src/tui/components/StatusPeekTrigger.tsx +++ b/src/tui/components/StatusPeekTrigger.tsx @@ -7,14 +7,14 @@ import { Text } from 'ink'; import { useEffect } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { useUiStore } from '../hooks/useUiStore.js'; let peekedOnce = false; interface StatusPeekTriggerProps { /** Accepted for the content decks that pass it; presentation reads the UiStore. */ - store?: WizardStore; + store?: FlowStore; /** How long the status bar stays expanded, in ms. */ duration?: number; } diff --git a/src/tui/components/TipsCard.tsx b/src/tui/components/TipsCard.tsx index 42f8ef13f..1d0cd3996 100644 --- a/src/tui/components/TipsCard.tsx +++ b/src/tui/components/TipsCard.tsx @@ -5,7 +5,7 @@ */ import { Box, Text, useInput } from 'ink'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { Colors, Icons } from '../styles.js'; import { DiscoveredFeature, AdditionalFeature } from '@store'; @@ -20,7 +20,7 @@ export interface Tip { /** Optional URL shown after the description */ url?: string; /** When provided, the tip is only shown if this returns true */ - visible?: (store: WizardStore) => boolean; + visible?: (store: FlowStore) => boolean; /** Optional key binding that toggles an AdditionalFeature */ toggle?: { /** The key the user presses (lowercase) */ @@ -32,7 +32,7 @@ export interface Tip { /** Prompt shown when not yet toggled */ prompt: string; /** Returns true if already toggled */ - isEnabled: (store: WizardStore) => boolean; + isEnabled: (store: FlowStore) => boolean; }; } @@ -93,7 +93,7 @@ export const TipsCard = ({ store, tips = DEFAULT_TIPS, }: { - store: WizardStore; + store: FlowStore; tips?: Tip[]; }) => { useInput((input) => { diff --git a/src/tui/components/TokenCostHud.tsx b/src/tui/components/TokenCostHud.tsx index bdde57638..a56921bca 100644 --- a/src/tui/components/TokenCostHud.tsx +++ b/src/tui/components/TokenCostHud.tsx @@ -3,7 +3,7 @@ * token/cost estimate for this wizard run. Toggled from `ScreenContainer`; * deliberately not registered via `useKeyBindings`, so the shortcut never * shows in the `KeyboardHintsBar` in production. Defaults visible for - * local/dev/test runs (`WizardStore`'s `$tokenHudVisible` initial value), + * local/dev/test runs (`FlowStore`'s `$tokenHudVisible` initial value), * so contributors see it without needing to know the shortcut; defaults * hidden in the published build. Once shown, the panel itself names the * shortcut, so it's self-documenting either way. diff --git a/src/tui/console/headless-ui.ts b/src/tui/console/headless-ui.ts index 46be06dc9..c656be737 100644 --- a/src/tui/console/headless-ui.ts +++ b/src/tui/console/headless-ui.ts @@ -1,8 +1,8 @@ import { LoggingUI } from './logging-ui.js'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; /** - * `LoggingUI` plus it feeds run state into a `WizardStore` so the background + * `LoggingUI` plus it feeds run state into a `FlowStore` so the background * wizard-session sync (`TaskStreamPush`) can observe a headless run. We extend * `LoggingUI` (not `InkUI`) because its blocking/gate methods would wait on a * TUI that never renders; the runner drives phase transitions on the store @@ -10,7 +10,7 @@ import type { WizardStore } from '@store/types'; * ledger arrives through `setFrameworkContext`, the seam every UI implements. */ export class HeadlessUI extends LoggingUI { - constructor(private readonly store: WizardStore) { + constructor(private readonly store: FlowStore) { super(); } diff --git a/src/tui/exit-line.ts b/src/tui/exit-line.ts index d34089326..1c573ca14 100644 --- a/src/tui/exit-line.ts +++ b/src/tui/exit-line.ts @@ -20,7 +20,7 @@ import { formatCostUsd, getLogFilePath, } from '@store'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { isRunFailure } from '@store'; import { MINT_FAILURE_CONTACT } from './mint-failure.js'; import type { UiStore } from './ui-store.js'; @@ -40,7 +40,7 @@ const DIM = '\x1b[2m'; * HUD is hidden, or the run never produced any usage (e.g. non-agent * programs). */ -function tokenCostLine(store: WizardStore, ui: UiStore): string | null { +function tokenCostLine(store: FlowStore, ui: UiStore): string | null { if (!ui.tokenHudVisible) return null; const usage = store.tokenUsage; if (totalTokenCount(usage) === 0) return null; @@ -62,7 +62,7 @@ function tokenCostLine(store: WizardStore, ui: UiStore): string | null { * Echoed into scrollback like the handoff prompt — command on its own plain * line so a terminal can triple-click-select it. */ -function mcpLoginBlock(store: WizardStore): string | null { +function mcpLoginBlock(store: FlowStore): string | null { const commands = store.session.mcpLoginCommands; if (!commands || commands.length === 0) return null; return ( @@ -71,7 +71,7 @@ function mcpLoginBlock(store: WizardStore): string | null { ); } -export function getExitLine(store: WizardStore, ui: UiStore): string { +export function getExitLine(store: FlowStore, ui: UiStore): string { const outro = store.session.outroData; const label = store.session.programLabel ?? 'Wizard'; const costLine = tokenCostLine(store, ui); diff --git a/src/tui/hooks/useGithubConnection.ts b/src/tui/hooks/useGithubConnection.ts index ac94c3e15..b49286670 100644 --- a/src/tui/hooks/useGithubConnection.ts +++ b/src/tui/hooks/useGithubConnection.ts @@ -9,7 +9,7 @@ import { useEffect } from 'react'; -import type { WizardStore, WizardSession } from '@store/types'; +import type { FlowStore, WizardSession } from '@store/types'; import { fetchGithubConnected, requestDeepLink, analytics } from '@store'; const POLL_INTERVAL_MS = 3000; @@ -26,7 +26,7 @@ export async function fetchLoginUrl( return deepLink ?? `${session.credentials.host.appHost}/login`; } -export function useGithubConnection(store: WizardStore): void { +export function useGithubConnection(store: FlowStore): void { const credentials = store.session.credentials; const connected = store.session.githubConnected === true; diff --git a/src/tui/playground/PlaygroundApp.tsx b/src/tui/playground/PlaygroundApp.tsx index 03be5a099..08dc91841 100644 --- a/src/tui/playground/PlaygroundApp.tsx +++ b/src/tui/playground/PlaygroundApp.tsx @@ -6,7 +6,7 @@ */ import { ScreenContainer, TabContainer } from '../primitives/index.js'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import type { UiStore } from '../ui-store.js'; import { WelcomeDemo } from './demos/WelcomeDemo.js'; import { LayoutDemo } from './demos/LayoutDemo.js'; @@ -28,7 +28,7 @@ import { AskModalDemo } from './demos/AskModalDemo.js'; import { ViewportGuardDemo } from './demos/ViewportGuardDemo.js'; interface PlaygroundAppProps { - store: WizardStore; + store: FlowStore; ui: UiStore; } diff --git a/src/tui/playground/demos/AiOptInDemo.tsx b/src/tui/playground/demos/AiOptInDemo.tsx index f20652520..47e1a4811 100644 --- a/src/tui/playground/demos/AiOptInDemo.tsx +++ b/src/tui/playground/demos/AiOptInDemo.tsx @@ -14,7 +14,7 @@ import { useEffect, useState } from 'react'; import { Box, Text } from 'ink'; -import { WizardStore, HostResolution } from '@store'; +import { FlowStore, HostResolution } from '@store'; import { AiOptInRequiredScreen } from '../../screens/AiOptInRequiredScreen.js'; import { flowFor, Program } from '@store/programs'; @@ -26,7 +26,7 @@ interface AiOptInDemoProps { export const AiOptInDemo = ({ variant }: AiOptInDemoProps) => { const [store] = useState(() => { - const s = new WizardStore(flowFor(Program.PostHogIntegration).flow); + const s = new FlowStore(flowFor(Program.PostHogIntegration).flow); s.setCredentials({ accessToken: 'demo-fake-token', projectApiKey: 'demo-fake-project-key', @@ -37,7 +37,7 @@ export const AiOptInDemo = ({ variant }: AiOptInDemoProps) => { }); useEffect(() => { - store.session.region = 'us'; + store.session = { ...store.session, region: 'us' }; store.setApiUser({ distinct_id: 'demo-distinct-id', email: 'sarah@example.com', diff --git a/src/tui/playground/demos/EndScreensDemo.tsx b/src/tui/playground/demos/EndScreensDemo.tsx index 7ec0ab4b3..40a06ca1b 100644 --- a/src/tui/playground/demos/EndScreensDemo.tsx +++ b/src/tui/playground/demos/EndScreensDemo.tsx @@ -22,7 +22,7 @@ import { Box, Text, useInput } from 'ink'; import { useEffect, useState, useSyncExternalStore } from 'react'; -import type { WizardStore, OutroData } from '@store/types'; +import type { FlowStore, OutroData } from '@store/types'; import { SlackConnectScreen } from '../../screens/SlackConnectScreen.js'; import { OutroScreen } from '../../screens/OutroScreen.js'; import { Colors } from '../../styles.js'; @@ -60,7 +60,7 @@ const OUTRO_FIXTURES: Record = { }; interface EndScreensDemoProps { - store: WizardStore; + store: FlowStore; } export const EndScreensDemo = ({ store }: EndScreensDemoProps) => { diff --git a/src/tui/playground/demos/LearnDeckDemo.tsx b/src/tui/playground/demos/LearnDeckDemo.tsx index 3c3fab332..51f07071f 100644 --- a/src/tui/playground/demos/LearnDeckDemo.tsx +++ b/src/tui/playground/demos/LearnDeckDemo.tsx @@ -27,7 +27,7 @@ import { } from '../../primitives/index.js'; import type { ContentBlock, ProgressItem } from '../../primitives/index.js'; import { Colors } from '../../styles.js'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { PROGRAM_REGISTRY } from '@store/programs'; import { PROGRAM_PRESENTATION } from '../../programs/presentation.js'; import { AUDIT_AREA_SLIDES } from '../../screens/audit/slides/index.js'; @@ -83,7 +83,7 @@ const MOCK_TASKS: ProgressItem[] = [ ]; interface LearnDeckDemoProps { - store: WizardStore; + store: FlowStore; } export const LearnDeckDemo = ({ store }: LearnDeckDemoProps) => { @@ -197,10 +197,10 @@ export const LearnDeckDemo = ({ store }: LearnDeckDemoProps) => { * crash at render time. */ function withSessionOverride( - store: WizardStore, - patch: Partial, -): WizardStore { - const stub = Object.create(Object.getPrototypeOf(store)) as WizardStore; + store: FlowStore, + patch: Partial, +): FlowStore { + const stub = Object.create(Object.getPrototypeOf(store)) as FlowStore; Object.assign(stub, store); Object.defineProperty(stub, 'session', { value: { ...store.session, ...patch }, diff --git a/src/tui/playground/demos/McpDemo.tsx b/src/tui/playground/demos/McpDemo.tsx index 2ed03ede4..45cd284d9 100644 --- a/src/tui/playground/demos/McpDemo.tsx +++ b/src/tui/playground/demos/McpDemo.tsx @@ -5,7 +5,7 @@ * a short install delay, and a successful result. */ -import { WizardStore, McpClientStatus } from '@store'; +import { FlowStore, McpClientStatus } from '@store'; import { McpScreen } from '../../screens/McpScreen.js'; import type { McpInstaller, @@ -66,7 +66,7 @@ function createMockInstaller(): McpInstaller { } interface McpDemoProps { - store: WizardStore; + store: FlowStore; } export const McpDemo = ({ store }: McpDemoProps) => { diff --git a/src/tui/playground/demos/McpSuggestedPromptsDemo.tsx b/src/tui/playground/demos/McpSuggestedPromptsDemo.tsx index dc3005136..5172af62a 100644 --- a/src/tui/playground/demos/McpSuggestedPromptsDemo.tsx +++ b/src/tui/playground/demos/McpSuggestedPromptsDemo.tsx @@ -33,7 +33,7 @@ import { Box, Text, useInput } from 'ink'; import { useEffect, useMemo, useRef, useState } from 'react'; -import type { WizardStore, ProjectDataProfile } from '@store/types'; +import type { FlowStore, ProjectDataProfile } from '@store/types'; import { McpSuggestedPromptsScreen } from '../../screens/McpSuggestedPromptsScreen.js'; import { Colors } from '../../styles.js'; import { @@ -185,7 +185,7 @@ interface MockConfig { } interface McpSuggestedPromptsDemoProps { - store: WizardStore; + store: FlowStore; } const delay = (ms: number): Promise => @@ -197,7 +197,7 @@ const delay = (ms: number): Promise => * the *next* invocation without remounting the screen. */ function createMockServices( - store: WizardStore, + store: FlowStore, configRef: { current: MockConfig }, ): McpSuggestedPromptsServices { return { diff --git a/src/tui/playground/demos/RunScreenDemo.tsx b/src/tui/playground/demos/RunScreenDemo.tsx index 4fd6acd93..a5b913c8a 100644 --- a/src/tui/playground/demos/RunScreenDemo.tsx +++ b/src/tui/playground/demos/RunScreenDemo.tsx @@ -22,7 +22,7 @@ import { } from 'react'; import { Box, Text, useInput } from 'ink'; import { - WizardStore, + FlowStore, TaskStatus, DiscoveredFeature, AgentPhase, @@ -145,7 +145,7 @@ const MOCK_EVENTS = [ ]; interface RunScreenDemoProps { - store: WizardStore; + store: FlowStore; } export const RunScreenDemo = ({ store }: RunScreenDemoProps) => { diff --git a/src/tui/playground/demos/WelcomeDemo.tsx b/src/tui/playground/demos/WelcomeDemo.tsx index df05d0712..4e297dfb7 100644 --- a/src/tui/playground/demos/WelcomeDemo.tsx +++ b/src/tui/playground/demos/WelcomeDemo.tsx @@ -3,11 +3,11 @@ */ import { Box, Text, useInput } from 'ink'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { Colors, Icons } from '../../styles.js'; interface WelcomeDemoProps { - store: WizardStore; + store: FlowStore; } export const WelcomeDemo = ({ store }: WelcomeDemoProps) => { diff --git a/src/tui/playground/start-playground.ts b/src/tui/playground/start-playground.ts index 81922392e..3c12eb0a3 100644 --- a/src/tui/playground/start-playground.ts +++ b/src/tui/playground/start-playground.ts @@ -4,7 +4,7 @@ import { render } from 'ink'; import { createElement } from 'react'; -import { WizardStore, HostResolution, WizardReadiness } from '@store'; +import { FlowStore, HostResolution, WizardReadiness } from '@store'; import { UiStore } from '../ui-store.js'; import { PlaygroundApp } from './PlaygroundApp.js'; import { enterDarkTerminal, releaseTerminal } from '../terminal.js'; @@ -13,7 +13,7 @@ import { flowFor, Program } from '@store/programs'; export function startPlayground(version: string): void { enterDarkTerminal(); - const store = new WizardStore(flowFor(Program.PostHogIntegration).flow); + const store = new FlowStore(flowFor(Program.PostHogIntegration).flow); store.version = version; // Pre-fill session so the router skips health-check, auth, and setup, diff --git a/src/tui/primitives/ScreenContainer.tsx b/src/tui/primitives/ScreenContainer.tsx index d0923fe2f..1dad5ed18 100644 --- a/src/tui/primitives/ScreenContainer.tsx +++ b/src/tui/primitives/ScreenContainer.tsx @@ -23,7 +23,7 @@ import { DissolveTransition } from './DissolveTransition.js'; import { KeyboardHintsBar } from './KeyboardHintsBar.js'; import { ScreenErrorBoundary } from './ScreenErrorBoundary.js'; import { ViewportTooSmall, isViewportTooSmall } from './ViewportTooSmall.js'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import type { UiStore } from '../ui-store.js'; import { UiStoreContext } from '../hooks/useUiStore.js'; @@ -37,7 +37,7 @@ function getContentWidth(terminalColumns: number): number { } interface ScreenContainerProps { - store: WizardStore; + store: FlowStore; ui: UiStore; screens: Record; } diff --git a/src/tui/primitives/ScreenErrorBoundary.tsx b/src/tui/primitives/ScreenErrorBoundary.tsx index fe91a8e07..d6b15b49c 100644 --- a/src/tui/primitives/ScreenErrorBoundary.tsx +++ b/src/tui/primitives/ScreenErrorBoundary.tsx @@ -7,11 +7,11 @@ import { Box, Text } from 'ink'; import { Component, type ReactNode } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { OutroKind, RunPhase, logToFile } from '@store'; interface Props { - store: WizardStore; + store: FlowStore; children: ReactNode; } diff --git a/src/tui/primitives/TabContainer.tsx b/src/tui/primitives/TabContainer.tsx index e2f5638ed..eccb01b2c 100644 --- a/src/tui/primitives/TabContainer.tsx +++ b/src/tui/primitives/TabContainer.tsx @@ -14,7 +14,7 @@ import { KeyMatch, type KeyBinding, } from '../hooks/useKeyBindings.js'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { COLLAPSED_COUNT, EXPANDED_COUNT } from '../constants.js'; import { useUiStore } from '../hooks/useUiStore.js'; @@ -33,7 +33,7 @@ interface TabContainerProps { /** Enable expand/collapse on the status box via 's' key */ expandableStatus?: boolean; /** Store reference — required when expandableStatus is true so status state is shared. */ - store?: WizardStore; + store?: FlowStore; } export const TabContainer = ({ diff --git a/src/tui/programs/agent-skill/content/index.tsx b/src/tui/programs/agent-skill/content/index.tsx index 4b3c319d5..a73a51a84 100644 --- a/src/tui/programs/agent-skill/content/index.tsx +++ b/src/tui/programs/agent-skill/content/index.tsx @@ -5,11 +5,11 @@ */ import { Text } from 'ink'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { TextRevealMode } from '../../../primitives/TextBlock.js'; import type { ContentBlock } from '../../../primitives/content-types.js'; -export const getContentBlocks = (store?: WizardStore): ContentBlock[] => { +export const getContentBlocks = (store?: FlowStore): ContentBlock[] => { const skillId = store?.session.skillId ?? 'unknown'; return [ { diff --git a/src/tui/programs/error-tracking-upload-source-maps/content/index.tsx b/src/tui/programs/error-tracking-upload-source-maps/content/index.tsx index f58426d6f..372580ab4 100644 --- a/src/tui/programs/error-tracking-upload-source-maps/content/index.tsx +++ b/src/tui/programs/error-tracking-upload-source-maps/content/index.tsx @@ -13,7 +13,7 @@ import { Text } from 'ink'; import { Colors } from '../../../styles.js'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { TextRevealMode } from '../../../primitives/TextBlock.js'; import { isClearBlock, @@ -128,7 +128,7 @@ export type SourceMapsDeckCopy = { intro: string; wiring: string }; /** The source-maps narrative. The error-tracking program reuses it with its own copy. */ export const buildSourceMapsDeck = ( - store: WizardStore | undefined, + store: FlowStore | undefined, copy: SourceMapsDeckCopy, ): ContentBlock[] => pace([ @@ -274,7 +274,7 @@ export const buildSourceMapsDeck = ( }, ]); -export const getContentBlocks = (store?: WizardStore): ContentBlock[] => +export const getContentBlocks = (store?: FlowStore): ContentBlock[] => buildSourceMapsDeck(store, { intro: "I'm wiring PostHog Error Tracking into your build.", wiring: diff --git a/src/tui/programs/error-tracking/content/index.tsx b/src/tui/programs/error-tracking/content/index.tsx index cf982ad74..0ac01ab77 100644 --- a/src/tui/programs/error-tracking/content/index.tsx +++ b/src/tui/programs/error-tracking/content/index.tsx @@ -1,10 +1,10 @@ /** Error-tracking learn-deck: the source-maps narrative, worded to also fit platforms that upload nothing. */ -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import type { ContentBlock } from '../../../primitives/content-types.js'; import { buildSourceMapsDeck } from '../../error-tracking-upload-source-maps/content/index.js'; -export const getContentBlocks = (store?: WizardStore): ContentBlock[] => +export const getContentBlocks = (store?: FlowStore): ContentBlock[] => buildSourceMapsDeck(store, { intro: "I'm wiring PostHog Error Tracking into your project.", wiring: diff --git a/src/tui/programs/migration/content/index.tsx b/src/tui/programs/migration/content/index.tsx index 2f28673c4..8e5feefe5 100644 --- a/src/tui/programs/migration/content/index.tsx +++ b/src/tui/programs/migration/content/index.tsx @@ -16,7 +16,7 @@ */ import { Text } from 'ink'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { Colors } from '../../../styles.js'; import { TextRevealMode } from '../../../primitives/TextBlock.js'; import type { ContentBlock } from '../../../primitives/content-types.js'; @@ -28,7 +28,7 @@ import { VENDOR_STACK_BLOCK } from './vendor-stack.js'; import { FREE_TIER_BLOCK } from './free-tier.js'; import { PRICING_STRUCTURE_BLOCK } from './pricing-structure.js'; -export const getContentBlocks = (store?: WizardStore): ContentBlock[] => [ +export const getContentBlocks = (store?: FlowStore): ContentBlock[] => [ // ── Welcome ──────────────────────────────────────────────────────────── { content: 'Hello.', diff --git a/src/tui/programs/posthog-integration/content/index.tsx b/src/tui/programs/posthog-integration/content/index.tsx index 2da915d7d..c134dc686 100644 --- a/src/tui/programs/posthog-integration/content/index.tsx +++ b/src/tui/programs/posthog-integration/content/index.tsx @@ -6,7 +6,7 @@ import { Text } from 'ink'; import { Colors } from '../../../styles.js'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { TextRevealMode } from '../../../primitives/TextBlock.js'; import type { ContentBlock } from '../../../primitives/content-types.js'; import { StatusPeekTrigger } from '../../../components/StatusPeekTrigger.js'; @@ -15,7 +15,7 @@ import { PRODUCT_SUITE_BLOCK } from './product-suite.js'; import { LINE_CHART_BLOCK } from './line-chart.js'; import { FUNNEL_BLOCK } from './funnel.js'; -export const getContentBlocks = (store?: WizardStore): ContentBlock[] => [ +export const getContentBlocks = (store?: FlowStore): ContentBlock[] => [ { // Name comes from the login profile; falls back when absent (CI keys). content: store?.session.apiUser?.first_name diff --git a/src/tui/programs/presentation.ts b/src/tui/programs/presentation.ts index 347f46a3e..9aa43e839 100644 --- a/src/tui/programs/presentation.ts +++ b/src/tui/programs/presentation.ts @@ -7,7 +7,7 @@ * the generic skill deck and `DEFAULT_TIPS`. */ -import type { ProgramId, WizardStore } from '@store/types'; +import type { ProgramId, FlowStore } from '@store/types'; import type { ContentBlock } from '../primitives/index.js'; import type { Tip } from '../components/TipsCard.js'; import { getContentBlocks as agentSkillDeck } from './agent-skill/content/index.js'; @@ -22,8 +22,8 @@ import { getTips as selfDrivingTips } from './self-driving/content/tips.js'; import { getContentBlocks as warehouseSourceDeck } from './warehouse-source/content/index.js'; export interface ProgramPresentation { - getContentBlocks?: (store?: WizardStore) => ContentBlock[]; - getTips?: (store?: WizardStore) => Tip[]; + getContentBlocks?: (store?: FlowStore) => ContentBlock[]; + getTips?: (store?: FlowStore) => Tip[]; } export const PROGRAM_PRESENTATION: Partial< diff --git a/src/tui/programs/self-driving/content/index.tsx b/src/tui/programs/self-driving/content/index.tsx index cbc2c54b4..890316d9e 100644 --- a/src/tui/programs/self-driving/content/index.tsx +++ b/src/tui/programs/self-driving/content/index.tsx @@ -13,7 +13,7 @@ import { NO_DEFAULT_LIMIT, PRICING_LONG } from '@store/programs'; import { Text } from 'ink'; import { Colors } from '../../../styles.js'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { TextRevealMode } from '../../../primitives/TextBlock.js'; import type { ContentBlock } from '../../../primitives/content-types.js'; import { StatusPeekTrigger } from '../../../components/StatusPeekTrigger.js'; @@ -21,7 +21,7 @@ import { PIPELINE_BLOCK } from './pipeline-diagram.js'; const CLEAR: ContentBlock = { type: 'clear', pause: 2000 }; -export const getContentBlocks = (store?: WizardStore): ContentBlock[] => { +export const getContentBlocks = (store?: FlowStore): ContentBlock[] => { return [ // Scene 1 — orient { diff --git a/src/tui/router.ts b/src/tui/router.ts index 0cc6b6897..70cc1db49 100644 --- a/src/tui/router.ts +++ b/src/tui/router.ts @@ -1,6 +1,6 @@ /** * Screen name vocabulary for the TUI. Resolution of the active screen lives in - * the store (`WizardStore.currentScreen` over `state/flow-resolution.ts`). + * the store (`FlowStore.currentScreen` over `state/flow-resolution.ts`). */ import { ScreenId } from './screen-sequences.js'; diff --git a/src/tui/screen-registry.tsx b/src/tui/screen-registry.tsx index d769dfa4e..d76fb5916 100644 --- a/src/tui/screen-registry.tsx +++ b/src/tui/screen-registry.tsx @@ -11,7 +11,7 @@ import type { ReactNode } from 'react'; import path from 'node:path'; import { getLogFilePath, writeWizardSpellbook } from '@store'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { ScreenId, Overlay, type ScreenName } from './router.js'; import { HealthCheckScreen } from './screens/health/HealthCheckScreen.js'; @@ -69,7 +69,7 @@ export interface ScreenServices extends MintFailureServices { mcpSuggestedPromptsServices: McpSuggestedPromptsServices; } -export function createServices(store: WizardStore): ScreenServices { +export function createServices(store: FlowStore): ScreenServices { return { get logPath() { return path.resolve(getLogFilePath()); @@ -87,7 +87,7 @@ export function createServices(store: WizardStore): ScreenServices { } export function createScreens( - store: WizardStore, + store: FlowStore, services: ScreenServices, ): Record { return { diff --git a/src/tui/screens/AgentSkillIntroScreen.tsx b/src/tui/screens/AgentSkillIntroScreen.tsx index 882fdde3f..a7b0e80fc 100644 --- a/src/tui/screens/AgentSkillIntroScreen.tsx +++ b/src/tui/screens/AgentSkillIntroScreen.tsx @@ -8,12 +8,12 @@ import { Box, Text } from 'ink'; import type { ReactNode } from 'react'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { IntroScreenLayout } from './IntroScreenLayout.js'; import { SkillSourceInfo, useSkillEntry } from './SkillSourceInfo.js'; interface AgentSkillIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const AgentSkillIntroScreen = ({ diff --git a/src/tui/screens/AiObservabilityIntroScreen.tsx b/src/tui/screens/AiObservabilityIntroScreen.tsx index a32028e0d..dd5aea10c 100644 --- a/src/tui/screens/AiObservabilityIntroScreen.tsx +++ b/src/tui/screens/AiObservabilityIntroScreen.tsx @@ -1,11 +1,11 @@ import { Box, Text } from 'ink'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { IntroScreenLayout } from './IntroScreenLayout.js'; import { SkillSourceInfo, useSkillEntry } from './SkillSourceInfo.js'; interface AiObservabilityIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const AiObservabilityIntroScreen = ({ diff --git a/src/tui/screens/AiOptInRequiredScreen.tsx b/src/tui/screens/AiOptInRequiredScreen.tsx index 1129a44bc..82ced82f2 100644 --- a/src/tui/screens/AiOptInRequiredScreen.tsx +++ b/src/tui/screens/AiOptInRequiredScreen.tsx @@ -16,7 +16,7 @@ import opn from 'opn'; import { Box, Text } from 'ink'; import { useEffect, useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { useKeyBindings } from '../hooks/useKeyBindings.js'; import { Colors } from '../styles.js'; import { useSkillEntry } from './SkillSourceInfo.js'; @@ -33,7 +33,7 @@ const SETTINGS_PATH = 'settings/organization-details'; const SETTINGS_ANCHOR = '#organization-ai-consent'; interface AiOptInRequiredScreenProps { - store: WizardStore; + store: FlowStore; } export const AiOptInRequiredScreen = ({ diff --git a/src/tui/screens/AuthErrorScreen.tsx b/src/tui/screens/AuthErrorScreen.tsx index 6eb2f5457..1b9bbd2d8 100644 --- a/src/tui/screens/AuthErrorScreen.tsx +++ b/src/tui/screens/AuthErrorScreen.tsx @@ -13,12 +13,12 @@ import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { Colors } from '../styles.js'; import { useDismissOnAnyKey } from '../hooks/useDismissOnAnyKey.js'; interface AuthErrorScreenProps { - store: WizardStore; + store: FlowStore; } export const AuthErrorScreen = ({ store }: AuthErrorScreenProps) => { diff --git a/src/tui/screens/AuthScreen.tsx b/src/tui/screens/AuthScreen.tsx index 629e924c5..80dbc696d 100644 --- a/src/tui/screens/AuthScreen.tsx +++ b/src/tui/screens/AuthScreen.tsx @@ -12,7 +12,7 @@ import { Box, Text } from 'ink'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { LoadingBox } from '../primitives/index.js'; import { MAX_WIDTH } from '../primitives/ScreenContainer.js'; import { @@ -26,7 +26,7 @@ import { Colors, Icons } from '../styles.js'; import { copyToClipboard } from '@store'; interface AuthScreenProps { - store: WizardStore; + store: FlowStore; } export const AuthScreen = ({ store }: AuthScreenProps) => { diff --git a/src/tui/screens/ErrorTrackingDetectScreen.tsx b/src/tui/screens/ErrorTrackingDetectScreen.tsx index e8962bb09..5c21b1424 100644 --- a/src/tui/screens/ErrorTrackingDetectScreen.tsx +++ b/src/tui/screens/ErrorTrackingDetectScreen.tsx @@ -7,7 +7,7 @@ import { Box, Text } from 'ink'; import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; import type { - WizardStore, + FlowStore, ErrorTrackingDetectionReport, ErrorTrackingProject, } from '@store/types'; @@ -20,7 +20,7 @@ import { } from '@store/programs'; interface ErrorTrackingDetectScreenProps { - store: WizardStore; + store: FlowStore; } type DetectState = diff --git a/src/tui/screens/ErrorTrackingIntroScreen.tsx b/src/tui/screens/ErrorTrackingIntroScreen.tsx index bffcfe827..7ab6ec96b 100644 --- a/src/tui/screens/ErrorTrackingIntroScreen.tsx +++ b/src/tui/screens/ErrorTrackingIntroScreen.tsx @@ -1,11 +1,11 @@ import { Box, Text } from 'ink'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { IntroScreenLayout } from './IntroScreenLayout.js'; import { SkillSourceInfo, useSkillEntry } from './SkillSourceInfo.js'; interface ErrorTrackingIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const ErrorTrackingIntroScreen = ({ diff --git a/src/tui/screens/ExitScreen.tsx b/src/tui/screens/ExitScreen.tsx index 739f8dfd4..31c5497c8 100644 --- a/src/tui/screens/ExitScreen.tsx +++ b/src/tui/screens/ExitScreen.tsx @@ -6,9 +6,9 @@ */ import { useEffect } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; -export const ExitScreen = ({ store }: { store?: WizardStore }) => { +export const ExitScreen = ({ store }: { store?: FlowStore }) => { useEffect(() => { // After a mint failure run-wizard owns the exit (status 1, analytics). if (!store?.session.mintHandoff) process.exit(0); diff --git a/src/tui/screens/KeepSkillsScreen.tsx b/src/tui/screens/KeepSkillsScreen.tsx index 67de7884f..778cbcdf9 100644 --- a/src/tui/screens/KeepSkillsScreen.tsx +++ b/src/tui/screens/KeepSkillsScreen.tsx @@ -14,13 +14,13 @@ import { readdir, rm, access } from 'node:fs/promises'; import { join } from 'node:path'; const WIZARD_MARKER = '.posthog-wizard'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { ConfirmationInput } from '../primitives/index.js'; import { Colors } from '../styles.js'; import { CONTEXT_MILL_URL } from '@store'; interface KeepSkillsScreenProps { - store: WizardStore; + store: FlowStore; } interface SkillEntry { diff --git a/src/tui/screens/ManagedSettingsScreen.tsx b/src/tui/screens/ManagedSettingsScreen.tsx index 5474f26bd..1ce7948e4 100644 --- a/src/tui/screens/ManagedSettingsScreen.tsx +++ b/src/tui/screens/ManagedSettingsScreen.tsx @@ -11,7 +11,7 @@ import { Box, Text } from 'ink'; import { useEffect, useSyncExternalStore } from 'react'; -import type { WizardStore, SettingsConflict } from '@store/types'; +import type { FlowStore, SettingsConflict } from '@store/types'; import { ConfirmationInput, ModalOverlay } from '../primitives/index.js'; import { Icons } from '../styles.js'; import { analytics } from '@store'; @@ -30,7 +30,7 @@ function sourceLabel(source: SettingsConflict['source']): string { } interface ManagedSettingsScreenProps { - store: WizardStore; + store: FlowStore; } export const ManagedSettingsScreen = ({ diff --git a/src/tui/screens/ManualAuthCodeScreen.tsx b/src/tui/screens/ManualAuthCodeScreen.tsx index c8c7b63f5..a8332ed4d 100644 --- a/src/tui/screens/ManualAuthCodeScreen.tsx +++ b/src/tui/screens/ManualAuthCodeScreen.tsx @@ -15,12 +15,12 @@ import { Box, Text, useInput } from 'ink'; import { TextInput } from '@inkjs/ui'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { Colors, Icons } from '../styles.js'; import { extractOAuthCode } from '@store'; interface ManualAuthCodeScreenProps { - store: WizardStore; + store: FlowStore; } export const ManualAuthCodeScreen = ({ store }: ManualAuthCodeScreenProps) => { diff --git a/src/tui/screens/McpScreen.tsx b/src/tui/screens/McpScreen.tsx index c77db915f..edaef5e07 100644 --- a/src/tui/screens/McpScreen.tsx +++ b/src/tui/screens/McpScreen.tsx @@ -14,7 +14,7 @@ import { Box, Text, useInput } from 'ink'; import { useState, useEffect, useRef } from 'react'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { McpOutcome, McpClientStatus, @@ -40,7 +40,7 @@ import type { export type McpMode = 'install' | 'remove'; interface McpScreenProps { - store: WizardStore; + store: FlowStore; installer: McpInstaller; mode?: McpMode; } @@ -57,7 +57,7 @@ enum Phase { } const markDone = ( - store: WizardStore, + store: FlowStore, outcome: McpOutcome, clients: string[] = [], featuresSelected?: 'all' | string[], diff --git a/src/tui/screens/McpSuggestedPromptsScreen.tsx b/src/tui/screens/McpSuggestedPromptsScreen.tsx index 95638dc70..b7af43894 100644 --- a/src/tui/screens/McpSuggestedPromptsScreen.tsx +++ b/src/tui/screens/McpSuggestedPromptsScreen.tsx @@ -43,7 +43,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useSyncExternalStore } from 'react'; import type { - WizardStore, + FlowStore, PromptOption, RoleGreeting, ProjectDataProfile, @@ -78,7 +78,7 @@ import type { } from '../services/mcp-suggested-prompts-services.js'; interface McpSuggestedPromptsScreenProps { - store: WizardStore; + store: FlowStore; services: McpSuggestedPromptsServices; } diff --git a/src/tui/screens/MetricsIntroScreen.tsx b/src/tui/screens/MetricsIntroScreen.tsx index 33e69c94e..ea79e2195 100644 --- a/src/tui/screens/MetricsIntroScreen.tsx +++ b/src/tui/screens/MetricsIntroScreen.tsx @@ -1,11 +1,11 @@ import { Box, Text } from 'ink'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { IntroScreenLayout } from './IntroScreenLayout.js'; import { SkillSourceInfo, useSkillEntry } from './SkillSourceInfo.js'; interface MetricsIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const MetricsIntroScreen = ({ store }: MetricsIntroScreenProps) => { diff --git a/src/tui/screens/MigrationIntroScreen.tsx b/src/tui/screens/MigrationIntroScreen.tsx index fedc3bb14..16856a398 100644 --- a/src/tui/screens/MigrationIntroScreen.tsx +++ b/src/tui/screens/MigrationIntroScreen.tsx @@ -1,10 +1,10 @@ import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { IntroScreenLayout } from './IntroScreenLayout.js'; interface MigrationIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const MigrationIntroScreen = ({ store }: MigrationIntroScreenProps) => { diff --git a/src/tui/screens/MintFailureScreen.tsx b/src/tui/screens/MintFailureScreen.tsx index 102f839f8..a810e6626 100644 --- a/src/tui/screens/MintFailureScreen.tsx +++ b/src/tui/screens/MintFailureScreen.tsx @@ -1,6 +1,6 @@ import { Box, Text } from 'ink'; import { useRef, useState, useSyncExternalStore } from 'react'; -import type { WizardStore, WizardSpellbook } from '@store/types'; +import type { FlowStore, WizardSpellbook } from '@store/types'; import { PickerMenu } from '../primitives/index.js'; import { useStdoutDimensions } from '../hooks/useStdoutDimensions.js'; import { Colors } from '../styles.js'; @@ -31,7 +31,7 @@ export function MintFailureScreen({ store, services, }: { - store: WizardStore; + store: FlowStore; services: MintFailureServices; }) { const [columns] = useStdoutDimensions(); diff --git a/src/tui/screens/OutroScreen.tsx b/src/tui/screens/OutroScreen.tsx index 18be2a1d9..85d3dcd6e 100644 --- a/src/tui/screens/OutroScreen.tsx +++ b/src/tui/screens/OutroScreen.tsx @@ -8,14 +8,14 @@ import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { OutroKind, withUtm } from '@store'; import { Colors } from '../styles.js'; import { LinkText } from '../primitives/LinkText.js'; import { useDismissOnAnyKey } from '../hooks/useDismissOnAnyKey.js'; interface OutroScreenProps { - store: WizardStore; + store: FlowStore; } export const OutroScreen = ({ store }: OutroScreenProps) => { diff --git a/src/tui/screens/PortConflictScreen.tsx b/src/tui/screens/PortConflictScreen.tsx index d3c7644db..396f45231 100644 --- a/src/tui/screens/PortConflictScreen.tsx +++ b/src/tui/screens/PortConflictScreen.tsx @@ -6,12 +6,12 @@ import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { OAUTH_PORTS } from '@store'; import { ConfirmationInput, ModalOverlay } from '../primitives/index.js'; interface PortConflictScreenProps { - store: WizardStore; + store: FlowStore; } export const PortConflictScreen = ({ store }: PortConflictScreenProps) => { diff --git a/src/tui/screens/PostHogIntegrationIntroScreen.tsx b/src/tui/screens/PostHogIntegrationIntroScreen.tsx index decd765e9..a9fe6c051 100644 --- a/src/tui/screens/PostHogIntegrationIntroScreen.tsx +++ b/src/tui/screens/PostHogIntegrationIntroScreen.tsx @@ -11,7 +11,7 @@ import { Box, Text } from 'ink'; import type { ReactNode } from 'react'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { Integration, ScanConsent, analytics } from '@store'; import { getCommandPath, @@ -85,7 +85,7 @@ const FrameworkPicker = ({ store, onComplete, }: { - store: WizardStore; + store: FlowStore; onComplete?: () => void; }) => { const options = Object.values(Integration).map((value) => ({ @@ -113,7 +113,7 @@ const FrameworkPicker = ({ }; interface PostHogIntegrationIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const PostHogIntegrationIntroScreen = ({ diff --git a/src/tui/screens/RevenueIntroScreen.tsx b/src/tui/screens/RevenueIntroScreen.tsx index 87706ece9..3fd0527ef 100644 --- a/src/tui/screens/RevenueIntroScreen.tsx +++ b/src/tui/screens/RevenueIntroScreen.tsx @@ -11,13 +11,13 @@ import { Box, Text } from 'ink'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore, RevenueDetectError } from '@store/types'; +import type { FlowStore, RevenueDetectError } from '@store/types'; import { PickerMenu } from '../primitives/index.js'; import { IntroScreenLayout, type DetectionRow } from './IntroScreenLayout.js'; import { POSTHOG_SDKS, STRIPE_SDKS } from '@store/programs'; interface RevenueIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const RevenueIntroScreen = ({ store }: RevenueIntroScreenProps) => { diff --git a/src/tui/screens/RunScreen.tsx b/src/tui/screens/RunScreen.tsx index 8134b49c1..1bff5dfd2 100644 --- a/src/tui/screens/RunScreen.tsx +++ b/src/tui/screens/RunScreen.tsx @@ -8,7 +8,7 @@ import { useMemo, useSyncExternalStore } from 'react'; import { Box } from 'ink'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { TabContainer, SplitView, @@ -30,7 +30,7 @@ import { getContentBlocks as getSkillContentBlocks } from '../programs/agent-ski import { useUiStore } from '../hooks/useUiStore.js'; interface RunScreenProps { - store: WizardStore; + store: FlowStore; } export const RunScreen = ({ store }: RunScreenProps) => { diff --git a/src/tui/screens/SelfDrivingGitHubScreen.tsx b/src/tui/screens/SelfDrivingGitHubScreen.tsx index 1422ef6f6..fe695daa5 100644 --- a/src/tui/screens/SelfDrivingGitHubScreen.tsx +++ b/src/tui/screens/SelfDrivingGitHubScreen.tsx @@ -19,7 +19,7 @@ import { Box, Text } from 'ink'; import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { Colors, Icons } from '../styles.js'; import { PickerMenu, LoadingBox } from '../primitives/index.js'; import { useKeyBindings, KeyMatch } from '../hooks/useKeyBindings.js'; @@ -36,7 +36,7 @@ import { import { GITHUB_REQUIRED_BODY, GITHUB_REQUIRED_MESSAGE } from '@store/programs'; interface SelfDrivingGitHubScreenProps { - store: WizardStore; + store: FlowStore; } enum ChoiceValue { diff --git a/src/tui/screens/SelfDrivingHandoffScreen.tsx b/src/tui/screens/SelfDrivingHandoffScreen.tsx index fbdeaceb4..857cbcd27 100644 --- a/src/tui/screens/SelfDrivingHandoffScreen.tsx +++ b/src/tui/screens/SelfDrivingHandoffScreen.tsx @@ -9,7 +9,7 @@ import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { PickerMenu } from '../primitives/index.js'; import { Colors } from '../styles.js'; import { @@ -18,7 +18,7 @@ import { } from '@store/programs'; interface SelfDrivingHandoffScreenProps { - store: WizardStore; + store: FlowStore; } export const SelfDrivingHandoffScreen = ({ diff --git a/src/tui/screens/SelfDrivingIntegrationCheckScreen.tsx b/src/tui/screens/SelfDrivingIntegrationCheckScreen.tsx index e40b35516..f9277a600 100644 --- a/src/tui/screens/SelfDrivingIntegrationCheckScreen.tsx +++ b/src/tui/screens/SelfDrivingIntegrationCheckScreen.tsx @@ -18,7 +18,7 @@ import { Box, Text, useInput } from 'ink'; import { TextInput } from '@inkjs/ui'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore, CloudRegion } from '@store/types'; +import type { FlowStore, CloudRegion } from '@store/types'; import { PickerMenu } from '../primitives/index.js'; import { PrivacyPanel, @@ -29,7 +29,7 @@ import { POSTHOG_PRIVACY_URL, POSTHOG_TERMS_URL } from '@store'; import { Colors } from '../styles.js'; interface SelfDrivingIntegrationCheckScreenProps { - store: WizardStore; + store: FlowStore; } /** Multi-step screen state: pick account status → email → region. */ diff --git a/src/tui/screens/SelfDrivingIntegrationDetectScreen.tsx b/src/tui/screens/SelfDrivingIntegrationDetectScreen.tsx index 8decf7d6c..8bd48f272 100644 --- a/src/tui/screens/SelfDrivingIntegrationDetectScreen.tsx +++ b/src/tui/screens/SelfDrivingIntegrationDetectScreen.tsx @@ -12,7 +12,7 @@ import { Box, Text } from 'ink'; import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; import type { - WizardStore, + FlowStore, IntegrationProject, IntegrationDetectionReport, } from '@store/types'; @@ -25,7 +25,7 @@ import { } from '@store/programs'; interface SelfDrivingIntegrationDetectScreenProps { - store: WizardStore; + store: FlowStore; } type DetectState = diff --git a/src/tui/screens/SelfDrivingIntroScreen.tsx b/src/tui/screens/SelfDrivingIntroScreen.tsx index a544301a4..52d1a16a1 100644 --- a/src/tui/screens/SelfDrivingIntroScreen.tsx +++ b/src/tui/screens/SelfDrivingIntroScreen.tsx @@ -11,13 +11,13 @@ import { Box, Text } from 'ink'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore, SelfDrivingDetectError } from '@store/types'; +import type { FlowStore, SelfDrivingDetectError } from '@store/types'; import { PickerMenu } from '../primitives/index.js'; import { IntroScreenLayout } from './IntroScreenLayout.js'; import { NO_DEFAULT_LIMIT, PRICING_LONG, PRICING_SHORT } from '@store/programs'; interface SelfDrivingIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const SelfDrivingIntroScreen = ({ diff --git a/src/tui/screens/SessionTimeoutScreen.tsx b/src/tui/screens/SessionTimeoutScreen.tsx index 3e3f0a93d..cf659866b 100644 --- a/src/tui/screens/SessionTimeoutScreen.tsx +++ b/src/tui/screens/SessionTimeoutScreen.tsx @@ -9,13 +9,13 @@ import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { Colors } from '../styles.js'; import { OAUTH_TIMEOUT_MS } from '@store'; import { useDismissOnAnyKey } from '../hooks/useDismissOnAnyKey.js'; interface SessionTimeoutScreenProps { - store: WizardStore; + store: FlowStore; } const TIMEOUT_MINUTES = Math.round(OAUTH_TIMEOUT_MS / 60_000); diff --git a/src/tui/screens/SettingsOverrideScreen.tsx b/src/tui/screens/SettingsOverrideScreen.tsx index 8426428eb..deac27c4d 100644 --- a/src/tui/screens/SettingsOverrideScreen.tsx +++ b/src/tui/screens/SettingsOverrideScreen.tsx @@ -1,12 +1,12 @@ import { Box, Text } from 'ink'; import { useEffect, useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { ConfirmationInput, ModalOverlay } from '../primitives/index.js'; import { Icons } from '../styles.js'; import { analytics } from '@store'; interface SettingsOverrideScreenProps { - store: WizardStore; + store: FlowStore; } export const SettingsOverrideScreen = ({ diff --git a/src/tui/screens/SetupScreen.tsx b/src/tui/screens/SetupScreen.tsx index d0d6de0e2..4a46c70f9 100644 --- a/src/tui/screens/SetupScreen.tsx +++ b/src/tui/screens/SetupScreen.tsx @@ -9,12 +9,12 @@ import { Box, Text } from 'ink'; import { useState, useEffect } from 'react'; import { useSyncExternalStore } from 'react'; -import type { WizardStore, SetupQuestion } from '@store/types'; +import type { FlowStore, SetupQuestion } from '@store/types'; import { PickerMenu } from '../primitives/index.js'; import { Colors } from '../styles.js'; interface SetupScreenProps { - store: WizardStore; + store: FlowStore; } export const SetupScreen = ({ store }: SetupScreenProps) => { diff --git a/src/tui/screens/SlackConnectScreen.tsx b/src/tui/screens/SlackConnectScreen.tsx index f029d48b5..bb596741c 100644 --- a/src/tui/screens/SlackConnectScreen.tsx +++ b/src/tui/screens/SlackConnectScreen.tsx @@ -26,7 +26,7 @@ import { Box, Text } from 'ink'; import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { Colors, Icons } from '../styles.js'; import { PickerMenu, LoadingBox } from '../primitives/index.js'; import { useKeyBindings, KeyMatch } from '../hooks/useKeyBindings.js'; @@ -42,7 +42,7 @@ import { import { Program } from '@store/programs'; interface SlackConnectScreenProps { - store: WizardStore; + store: FlowStore; } enum ChoiceValue { diff --git a/src/tui/screens/SourceMapsDetectScreen.tsx b/src/tui/screens/SourceMapsDetectScreen.tsx index 6bc03c9e9..da04a6791 100644 --- a/src/tui/screens/SourceMapsDetectScreen.tsx +++ b/src/tui/screens/SourceMapsDetectScreen.tsx @@ -9,11 +9,7 @@ import { Box, Text } from 'ink'; import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; -import type { - WizardStore, - DetectedProject, - DetectionReport, -} from '@store/types'; +import type { FlowStore, DetectedProject, DetectionReport } from '@store/types'; import { LoadingBox, PickerMenu } from '../primitives/index.js'; import { Colors, Icons } from '../styles.js'; import { @@ -24,7 +20,7 @@ import { } from '@store/programs'; interface SourceMapsDetectScreenProps { - store: WizardStore; + store: FlowStore; } type DetectState = diff --git a/src/tui/screens/SourceMapsIntroScreen.tsx b/src/tui/screens/SourceMapsIntroScreen.tsx index e98620417..5e1cbd440 100644 --- a/src/tui/screens/SourceMapsIntroScreen.tsx +++ b/src/tui/screens/SourceMapsIntroScreen.tsx @@ -8,13 +8,13 @@ import { Box, Text } from 'ink'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { IntroScreenLayout } from './IntroScreenLayout.js'; type View = 'default' | 'more-info'; interface SourceMapsIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const SourceMapsIntroScreen = ({ diff --git a/src/tui/screens/SourceMapsOutroScreen.tsx b/src/tui/screens/SourceMapsOutroScreen.tsx index 07201ce79..287832907 100644 --- a/src/tui/screens/SourceMapsOutroScreen.tsx +++ b/src/tui/screens/SourceMapsOutroScreen.tsx @@ -13,13 +13,13 @@ import { join } from 'node:path'; import type { ReactNode } from 'react'; import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { OutroKind } from '@store'; import { Colors } from '../styles.js'; import { useDismissOnAnyKey } from '../hooks/useDismissOnAnyKey.js'; interface SourceMapsOutroScreenProps { - store: WizardStore; + store: FlowStore; } export const SourceMapsOutroScreen = ({ diff --git a/src/tui/screens/TaskNoticeScreen.tsx b/src/tui/screens/TaskNoticeScreen.tsx index fedad7d68..da9ee4a44 100644 --- a/src/tui/screens/TaskNoticeScreen.tsx +++ b/src/tui/screens/TaskNoticeScreen.tsx @@ -8,12 +8,12 @@ import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { Colors } from '../styles.js'; import { ConfirmationInput, ModalOverlay } from '../primitives/index.js'; interface TaskNoticeScreenProps { - store: WizardStore; + store: FlowStore; } export const TaskNoticeScreen = ({ store }: TaskNoticeScreenProps) => { diff --git a/src/tui/screens/WarehouseIntroScreen.tsx b/src/tui/screens/WarehouseIntroScreen.tsx index 802879a13..066cd02eb 100644 --- a/src/tui/screens/WarehouseIntroScreen.tsx +++ b/src/tui/screens/WarehouseIntroScreen.tsx @@ -12,13 +12,13 @@ import { Box, Text } from 'ink'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore, WarehouseDetectError } from '@store/types'; +import type { FlowStore, WarehouseDetectError } from '@store/types'; import { PickerMenu } from '../primitives/index.js'; import { IntroScreenLayout } from './IntroScreenLayout.js'; import { getDetectedWarehouseSources } from '@store/programs'; interface WarehouseIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const WarehouseIntroScreen = ({ store }: WarehouseIntroScreenProps) => { diff --git a/src/tui/screens/WizardAskScreen.tsx b/src/tui/screens/WizardAskScreen.tsx index 9c45df150..bb7553224 100644 --- a/src/tui/screens/WizardAskScreen.tsx +++ b/src/tui/screens/WizardAskScreen.tsx @@ -9,7 +9,7 @@ import { Box, Text, useInput } from 'ink'; import { PasswordInput, TextInput } from '@inkjs/ui'; import { useEffect, useState, useSyncExternalStore } from 'react'; -import type { WizardStore, AskAnswers, AskQuestion } from '@store/types'; +import type { FlowStore, AskAnswers, AskQuestion } from '@store/types'; import { LinkText, ModalOverlay, @@ -21,7 +21,7 @@ import { copyToClipboard, openInBrowser } from '@store'; import { useKeyBindings } from '../hooks/useKeyBindings.js'; interface WizardAskScreenProps { - store: WizardStore; + store: FlowStore; } /** @@ -31,7 +31,7 @@ interface WizardAskScreenProps { */ export function handleAskKey( key: { escape?: boolean }, - store: Pick, + store: Pick, ): void { if (key.escape) store.cancelPendingQuestion(); } @@ -39,7 +39,7 @@ export function handleAskKey( /** * What pressing Esc actually does, phrased for the footer hint. * - * Esc declines the *whole* request — {@link WizardStore.cancelPendingQuestion} + * Esc declines the *whole* request — {@link FlowStore.cancelPendingQuestion} * builds a cancelled answer for every question, so the ones already typed are * discarded too. The footer used to label that "skip", which on a multi-question * request reads as "skip this field": the warehouse task walks a source's diff --git a/src/tui/screens/audit/AuditIntroScreen.tsx b/src/tui/screens/audit/AuditIntroScreen.tsx index db41e6782..b086b888e 100644 --- a/src/tui/screens/audit/AuditIntroScreen.tsx +++ b/src/tui/screens/audit/AuditIntroScreen.tsx @@ -1,11 +1,11 @@ import { Box, Text } from 'ink'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { IntroScreenLayout } from '../IntroScreenLayout.js'; import { SkillSourceInfo, useSkillEntry } from '../SkillSourceInfo.js'; interface AuditIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const AuditIntroScreen = ({ store }: AuditIntroScreenProps) => { diff --git a/src/tui/screens/audit/AuditOutroScreen.tsx b/src/tui/screens/audit/AuditOutroScreen.tsx index 81632c181..82a31b1d4 100644 --- a/src/tui/screens/audit/AuditOutroScreen.tsx +++ b/src/tui/screens/audit/AuditOutroScreen.tsx @@ -8,7 +8,7 @@ import { join } from 'node:path'; import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { OutroKind } from '@store'; import { Colors } from '../../styles.js'; import { getAuditChecks } from '@store/programs'; @@ -16,7 +16,7 @@ import { AuditChecksOutroSection } from './AuditChecksOutroSection.js'; import { useDismissOnAnyKey } from '../../hooks/useDismissOnAnyKey.js'; interface AuditOutroScreenProps { - store: WizardStore; + store: FlowStore; } export const AuditOutroScreen = ({ store }: AuditOutroScreenProps) => { diff --git a/src/tui/screens/audit/AuditRunScreen.tsx b/src/tui/screens/audit/AuditRunScreen.tsx index c4d16abdf..e9bf77527 100644 --- a/src/tui/screens/audit/AuditRunScreen.tsx +++ b/src/tui/screens/audit/AuditRunScreen.tsx @@ -1,6 +1,6 @@ import { useSyncExternalStore } from 'react'; import { Box } from 'ink'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { TabContainer, SplitView, @@ -21,7 +21,7 @@ import { import { WIZARD_LOG_FILE } from '@store'; interface AuditRunScreenProps { - store: WizardStore; + store: FlowStore; } export const AuditRunScreen = ({ store }: AuditRunScreenProps) => { diff --git a/src/tui/screens/doctor/DoctorIntroScreen.tsx b/src/tui/screens/doctor/DoctorIntroScreen.tsx index 904727976..452556de2 100644 --- a/src/tui/screens/doctor/DoctorIntroScreen.tsx +++ b/src/tui/screens/doctor/DoctorIntroScreen.tsx @@ -1,11 +1,11 @@ import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { PickerMenu } from '../../primitives/index.js'; import { Colors, Icons } from '../../styles.js'; interface DoctorIntroScreenProps { - store: WizardStore; + store: FlowStore; } export const DoctorIntroScreen = ({ store }: DoctorIntroScreenProps) => { diff --git a/src/tui/screens/doctor/DoctorReportScreen.tsx b/src/tui/screens/doctor/DoctorReportScreen.tsx index 80ce2a8e9..ffefdfc32 100644 --- a/src/tui/screens/doctor/DoctorReportScreen.tsx +++ b/src/tui/screens/doctor/DoctorReportScreen.tsx @@ -1,6 +1,6 @@ import { Box, Text } from 'ink'; import { useEffect, useState, useSyncExternalStore } from 'react'; -import type { WizardStore, HealthIssue } from '@store/types'; +import type { FlowStore, HealthIssue } from '@store/types'; import { LoadingBox, PickerMenu } from '../../primitives/index.js'; import { Colors, Icons } from '../../styles.js'; import { fetchHealthIssues } from '@store/programs'; @@ -8,7 +8,7 @@ import { OutroKind, ApiError, POSTHOG_DOCS_URL } from '@store'; import { IssueTable, SEVERITY_LABEL, SEVERITY_ORDER } from './IssueTable.js'; interface DoctorReportScreenProps { - store: WizardStore; + store: FlowStore; } type FetchState = diff --git a/src/tui/screens/health/HealthCheckScreen.tsx b/src/tui/screens/health/HealthCheckScreen.tsx index 3d7a161eb..ced611c5d 100644 --- a/src/tui/screens/health/HealthCheckScreen.tsx +++ b/src/tui/screens/health/HealthCheckScreen.tsx @@ -9,7 +9,7 @@ import { Box, Text } from 'ink'; import { useState, useSyncExternalStore } from 'react'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; import { ConfirmationInput, LoadingBox, @@ -30,7 +30,7 @@ import { import { useDismissOnAnyKey } from '../../hooks/useDismissOnAnyKey.js'; interface HealthCheckScreenProps { - store: WizardStore; + store: FlowStore; } const EXAMPLE_PROMPT = diff --git a/src/tui/services/mcp-suggested-prompts-services.ts b/src/tui/services/mcp-suggested-prompts-services.ts index af1654f2e..106f47b4c 100644 --- a/src/tui/services/mcp-suggested-prompts-services.ts +++ b/src/tui/services/mcp-suggested-prompts-services.ts @@ -14,7 +14,7 @@ import type { Credentials, AgentChunk, McpPromptRequest, - WizardStore, + FlowStore, ApiUser, ProjectDataProfile, } from '@store/types'; @@ -98,7 +98,7 @@ export interface McpSuggestedPromptsServices { * actually invoked. */ export function createMcpSuggestedPromptsServices( - store: WizardStore, + store: FlowStore, ): McpSuggestedPromptsServices { return { performLogin: async () => { diff --git a/src/tui/start-tui.ts b/src/tui/start-tui.ts index 065eff20a..a54bbd985 100644 --- a/src/tui/start-tui.ts +++ b/src/tui/start-tui.ts @@ -8,7 +8,7 @@ import { render } from 'ink'; import { createElement } from 'react'; -import { WizardStore, setUI, analytics, logToFile, StoreUI } from '@store'; +import { FlowStore, setUI, analytics, logToFile, StoreUI } from '@store'; import { Program, flowFor } from '@store/programs'; import type { ProgramId } from '@store/types'; import { UiStore } from './ui-store.js'; @@ -23,7 +23,7 @@ export const TUI_ENTRY_MARKER = 'wizard-tui-entry'; export interface TuiHandle { unmount: () => void; - store: WizardStore; + store: FlowStore; waitForSetup: () => Promise; } @@ -34,7 +34,7 @@ export function startTUI( enterDarkTerminal(); logToFile(`[${TUI_ENTRY_MARKER}] ${program}`); - const store = new WizardStore(flowFor(program).flow); + const store = new FlowStore(flowFor(program).flow); store.version = version; const ui = new UiStore(store); diff --git a/src/tui/ui-store.ts b/src/tui/ui-store.ts index 61ef17221..f2d3100ac 100644 --- a/src/tui/ui-store.ts +++ b/src/tui/ui-store.ts @@ -1,5 +1,5 @@ /** - * UiStore — presentation state the TUI keeps beside the WizardStore: status + * UiStore — presentation state the TUI keeps beside the FlowStore: status * bar expansion, learn card progress, the token HUD toggle, and the direction * hint for screen transitions. It watches the store and re-emits every store * commit, so React subscribes here once for both. @@ -7,7 +7,7 @@ import { atom } from 'nanostores'; import { IS_DEV } from '@env'; -import type { WizardStore } from '@store/types'; +import type { FlowStore } from '@store/types'; export class UiStore { private $version = atom(0); @@ -20,7 +20,7 @@ export class UiStore { private _lastDirection: 'push' | 'pop' | null = null; private _lastDepth: number; - constructor(readonly store: WizardStore) { + constructor(readonly store: FlowStore) { this._lastDepth = store.interruptDepth; store.subscribe(() => this._onStoreChange()); }