From 95c35ff43ecff88891ce46837c97ab33e8158618 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Sat, 19 Sep 2026 19:44:04 -0400 Subject: [PATCH 1/2] refactor(store): one RunStore per agent run, FlowStore chains them A RunStore holds the state of one agent run. FlowStore owns the flow, gates, interrupts, and the session every run inherits, starts runs with startRun, and mirrors the active run. Runners and control hooks build the task stream per run. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .claude/skills/ink-tui/SKILL.md | 2 +- .../skills/ink-tui/references/ARCHITECTURE.md | 2 +- e2e-harness/ARCHITECTURE.md | 19 +- e2e-harness/__tests__/control-driver.test.ts | 18 +- .../__tests__/e2e-flow-snapshot.test.ts | 4 +- .../__tests__/keyboard-equivalence.test.tsx | 16 +- .../boundary-contracts.test.ts.snap | 9 +- .../store-boundary-members.test.ts | 6 +- src/cli/__tests__/cli.test.ts | 15 +- src/cli/__tests__/control-hooks.test.ts | 4 +- src/cli/control-hooks.ts | 32 +- .../runners/__tests__/mint-recovery.test.ts | 4 +- src/cli/runners/run-non-interactive.ts | 46 +- src/cli/runners/run-wizard.ts | 81 ++- src/cli/testing/fake-surfaces.ts | 4 +- src/store/README.md | 13 +- src/store/__tests__/contract.test.ts | 6 +- src/store/agent-protocol/token-pricing.ts | 2 +- src/store/control/__tests__/actions.test.ts | 39 +- src/store/control/driver.ts | 4 +- src/store/control/server.ts | 4 +- src/store/control/state.ts | 4 +- src/store/control/types.ts | 4 +- src/store/index.ts | 3 +- .../programs/__tests__/flow-traces.test.ts | 8 +- .../__tests__/self-driving-detect.test.ts | 4 +- .../programs/posthog-integration/detect.ts | 4 +- src/store/state/__tests__/invariants.test.ts | 20 +- ...et-run-state.test.ts => start-run.test.ts} | 50 +- src/store/state/__tests__/store.test.ts | 10 +- src/store/state/run-store.ts | 395 ++++++++++++ src/store/state/store-api.ts | 8 +- src/store/state/store.ts | 602 +++++------------- .../__tests__/event-plan-watcher.test.ts | 4 +- .../__tests__/task-stream-push.test.ts | 4 +- src/store/task-stream/event-plan-watcher.ts | 5 +- src/store/task-stream/task-stream-push.ts | 11 +- src/store/task-stream/types.ts | 13 + src/store/testing/index.ts | 8 +- src/store/types.ts | 5 +- src/store/ui/store-ui.ts | 4 +- src/tui/App.tsx | 4 +- src/tui/README.md | 2 +- src/tui/__tests__/MintFailureScreen.test.tsx | 4 +- src/tui/__tests__/WizardAskScreen.test.ts | 4 +- src/tui/__tests__/contract.test.ts | 4 +- src/tui/__tests__/exit-line.test.ts | 16 +- src/tui/__tests__/frames.test.tsx | 18 +- src/tui/__tests__/headless-ui.test.ts | 4 +- .../helpers/render-screen.no-jest.tsx | 6 +- src/tui/__tests__/task-notice.test.ts | 6 +- src/tui/__tests__/ui-store.test.ts | 4 +- src/tui/components/LearnCard.tsx | 4 +- src/tui/components/PhaseVisuals.tsx | 8 +- src/tui/components/StatusPeekTrigger.tsx | 4 +- src/tui/components/TipsCard.tsx | 8 +- src/tui/components/TokenCostHud.tsx | 2 +- src/tui/console/headless-ui.ts | 6 +- src/tui/exit-line.ts | 8 +- src/tui/hooks/useGithubConnection.ts | 4 +- src/tui/playground/PlaygroundApp.tsx | 4 +- src/tui/playground/demos/AiOptInDemo.tsx | 6 +- src/tui/playground/demos/EndScreensDemo.tsx | 4 +- src/tui/playground/demos/LearnDeckDemo.tsx | 12 +- src/tui/playground/demos/McpDemo.tsx | 4 +- .../demos/McpSuggestedPromptsDemo.tsx | 6 +- src/tui/playground/demos/RunScreenDemo.tsx | 4 +- src/tui/playground/demos/WelcomeDemo.tsx | 4 +- src/tui/playground/start-playground.ts | 4 +- src/tui/primitives/ScreenContainer.tsx | 4 +- src/tui/primitives/ScreenErrorBoundary.tsx | 4 +- src/tui/primitives/TabContainer.tsx | 4 +- .../programs/agent-skill/content/index.tsx | 4 +- .../content/index.tsx | 6 +- .../programs/error-tracking/content/index.tsx | 4 +- src/tui/programs/migration/content/index.tsx | 4 +- .../posthog-integration/content/index.tsx | 4 +- src/tui/programs/presentation.ts | 6 +- .../programs/self-driving/content/index.tsx | 4 +- src/tui/router.ts | 2 +- src/tui/screen-registry.tsx | 6 +- src/tui/screens/AgentSkillIntroScreen.tsx | 4 +- .../screens/AiObservabilityIntroScreen.tsx | 4 +- src/tui/screens/AiOptInRequiredScreen.tsx | 4 +- src/tui/screens/AuthErrorScreen.tsx | 4 +- src/tui/screens/AuthScreen.tsx | 4 +- src/tui/screens/ErrorTrackingDetectScreen.tsx | 4 +- src/tui/screens/ErrorTrackingIntroScreen.tsx | 4 +- src/tui/screens/ExitScreen.tsx | 4 +- src/tui/screens/KeepSkillsScreen.tsx | 4 +- src/tui/screens/ManagedSettingsScreen.tsx | 4 +- src/tui/screens/ManualAuthCodeScreen.tsx | 4 +- src/tui/screens/McpScreen.tsx | 6 +- src/tui/screens/McpSuggestedPromptsScreen.tsx | 4 +- src/tui/screens/MetricsIntroScreen.tsx | 4 +- src/tui/screens/MigrationIntroScreen.tsx | 4 +- src/tui/screens/MintFailureScreen.tsx | 4 +- src/tui/screens/OutroScreen.tsx | 4 +- src/tui/screens/PortConflictScreen.tsx | 4 +- .../screens/PostHogIntegrationIntroScreen.tsx | 6 +- src/tui/screens/RevenueIntroScreen.tsx | 4 +- src/tui/screens/RunScreen.tsx | 4 +- src/tui/screens/SelfDrivingGitHubScreen.tsx | 4 +- src/tui/screens/SelfDrivingHandoffScreen.tsx | 4 +- .../SelfDrivingIntegrationCheckScreen.tsx | 4 +- .../SelfDrivingIntegrationDetectScreen.tsx | 4 +- src/tui/screens/SelfDrivingIntroScreen.tsx | 4 +- src/tui/screens/SessionTimeoutScreen.tsx | 4 +- src/tui/screens/SettingsOverrideScreen.tsx | 4 +- src/tui/screens/SetupScreen.tsx | 4 +- src/tui/screens/SlackConnectScreen.tsx | 4 +- src/tui/screens/SourceMapsDetectScreen.tsx | 8 +- src/tui/screens/SourceMapsIntroScreen.tsx | 4 +- src/tui/screens/SourceMapsOutroScreen.tsx | 4 +- src/tui/screens/TaskNoticeScreen.tsx | 4 +- src/tui/screens/WarehouseIntroScreen.tsx | 4 +- src/tui/screens/WizardAskScreen.tsx | 8 +- src/tui/screens/audit/AuditIntroScreen.tsx | 4 +- src/tui/screens/audit/AuditOutroScreen.tsx | 4 +- src/tui/screens/audit/AuditRunScreen.tsx | 4 +- src/tui/screens/doctor/DoctorIntroScreen.tsx | 4 +- src/tui/screens/doctor/DoctorReportScreen.tsx | 4 +- src/tui/screens/health/HealthCheckScreen.tsx | 4 +- .../mcp-suggested-prompts-services.ts | 4 +- src/tui/start-tui.ts | 6 +- src/tui/ui-store.ts | 6 +- 126 files changed, 1067 insertions(+), 841 deletions(-) rename src/store/state/__tests__/{reset-run-state.test.ts => start-run.test.ts} (50%) create mode 100644 src/store/state/run-store.ts 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/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()); } From 63cd355e1e602471714af90986c8e4a05971f74d Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Sun, 20 Sep 2026 23:03:52 -0400 Subject: [PATCH 2/2] chore(build): refresh the chunk manifest fixtures The bundle audit compares these with the manifests CI builds; the new modules and the regrouped chunks belong in them. A failed comparison now uploads its manifests and reports both files. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- scripts/__fixtures__/chunk-manifest.ci.json | 88 ++++++++++++------- scripts/__fixtures__/chunk-manifest.prod.json | 1 + 2 files changed, 57 insertions(+), 32 deletions(-) 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",