diff --git a/README.md b/README.md index 13a377e6..3437440a 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,93 @@ To make your version of a tool usable with a one-line `npx` command: your project directory 3. Now you can run it with `npx yourpackagename` +# Workflow queue + +The wizard executes agent work through a queue-backed runner. Instead of one monolithic prompt, each workflow step is a separate continued query. + +## How it works + +1. **Bootstrap** runs first as a standalone query — installs the skill and emits the skill ID. +2. The runner reads `SKILL.md` from the installed skill and parses the `workflow` array from its YAML frontmatter to discover the step list. +3. A `WizardWorkflowQueue` is seeded from those steps plus an `env-vars` step at the end. +4. The runner pops items from the queue and issues one continued query per item, preserving the conversation across steps. + +## SKILL.md frontmatter format + +The skill generator in `context-mill` writes a `workflow` array into each integration skill's frontmatter: + +```yaml +--- +name: integration-nextjs-app-router +workflow: + - step_id: 1.0-begin + reference: basic-integration-1.0-begin.md + title: PostHog Setup - Begin + next: + - basic-integration-1.1-edit.md + - step_id: 1.1-edit + reference: basic-integration-1.1-edit.md + title: PostHog Setup - Edit + next: + - basic-integration-1.2-revise.md + # ... +--- +``` + +- `step_id` — unique identifier for the step +- `reference` — filename in the skill's `references/` directory +- `title` — human-readable label shown in the TUI progress list +- `next` — array of next step references (for future parallelization) + +## Queue item types + +```typescript +type WizardWorkflowQueueItem = + | { id: 'bootstrap'; kind: 'bootstrap'; label: string } + | { id: string; kind: 'workflow'; referenceFilename: string; label: string } + | { id: 'env-vars'; kind: 'env-vars'; label: string }; +``` + +## Enqueueing work dynamically + +The queue is exposed to the UI via `store.workQueue`. To add work during a run: + +```typescript +// Insert at front of queue (runs next) +store.workQueue.enqueueNext({ + id: 'my-task', + kind: 'workflow', + referenceFilename: 'my-reference.md', + label: 'My custom step', +}); + +// Append to end of queue +store.workQueue.enqueue({ + id: 'my-task', + kind: 'workflow', + referenceFilename: 'my-reference.md', + label: 'My custom step', +}); +``` + +The queue is reactive — mutations trigger UI re-renders. Items enqueued while the runner loop is active will be picked up when the current step finishes. + +## TUI progress display + +The RunScreen shows a stage-grouped progress list: + +``` +☑ PostHog Setup - Begin +▶ PostHog Setup - Edit + ☑ Add PostHog to auth.ts + ▶ Add PostHog to checkout.ts +○ PostHog Setup - Revise +○ PostHog Setup - Conclusion +○ Environment variables +``` + +Stage headers come from queue item labels. Nested tasks come from the agent's `TodoWrite` calls. Tasks reset when the runner advances to a new stage. + # Health checks `src/lib/health-checks/` checks external status pages and PostHog-owned diff --git a/src/lib/__tests__/workflow-queue.test.ts b/src/lib/__tests__/workflow-queue.test.ts index d2f1d1c1..547af0c7 100644 --- a/src/lib/__tests__/workflow-queue.test.ts +++ b/src/lib/__tests__/workflow-queue.test.ts @@ -7,15 +7,25 @@ import { } from '../workflow-queue'; const BASIC_INTEGRATION_STEPS: WorkflowStepSeed[] = [ - { stepId: '1.0-begin', referenceFilename: 'basic-integration-1.0-begin.md' }, - { stepId: '1.1-edit', referenceFilename: 'basic-integration-1.1-edit.md' }, + { + stepId: '1.0-begin', + referenceFilename: 'basic-integration-1.0-begin.md', + title: 'PostHog Setup - Begin', + }, + { + stepId: '1.1-edit', + referenceFilename: 'basic-integration-1.1-edit.md', + title: 'PostHog Setup - Edit', + }, { stepId: '1.2-revise', referenceFilename: 'basic-integration-1.2-revise.md', + title: 'PostHog Setup - Revise', }, { stepId: '1.3-conclude', referenceFilename: 'basic-integration-1.3-conclude.md', + title: 'PostHog Setup - Conclusion', }, ]; @@ -24,51 +34,65 @@ describe('WizardWorkflowQueue', () => { const queue = createInitialWizardWorkflowQueue(BASIC_INTEGRATION_STEPS); expect(queue.toArray()).toEqual([ - { id: 'bootstrap', kind: 'bootstrap' }, + { id: 'bootstrap', kind: 'bootstrap', label: 'Preparing integration' }, { id: 'workflow:1.0-begin', kind: 'workflow', referenceFilename: 'basic-integration-1.0-begin.md', + label: 'PostHog Setup - Begin', }, { id: 'workflow:1.1-edit', kind: 'workflow', referenceFilename: 'basic-integration-1.1-edit.md', + label: 'PostHog Setup - Edit', }, { id: 'workflow:1.2-revise', kind: 'workflow', referenceFilename: 'basic-integration-1.2-revise.md', + label: 'PostHog Setup - Revise', }, { id: 'workflow:1.3-conclude', kind: 'workflow', referenceFilename: 'basic-integration-1.3-conclude.md', + label: 'PostHog Setup - Conclusion', }, - { id: 'env-vars', kind: 'env-vars' }, + { id: 'env-vars', kind: 'env-vars', label: 'Environment variables' }, ]); }); it('builds a queue from arbitrary steps, not just basic-integration', () => { const customSteps: WorkflowStepSeed[] = [ - { stepId: 'setup', referenceFilename: 'feature-flags-setup.md' }, - { stepId: 'verify', referenceFilename: 'feature-flags-verify.md' }, + { + stepId: 'setup', + referenceFilename: 'feature-flags-setup.md', + title: 'Setup', + }, + { + stepId: 'verify', + referenceFilename: 'feature-flags-verify.md', + title: 'Verify', + }, ]; const queue = createInitialWizardWorkflowQueue(customSteps); expect(queue.toArray()).toEqual([ - { id: 'bootstrap', kind: 'bootstrap' }, + { id: 'bootstrap', kind: 'bootstrap', label: 'Preparing integration' }, { id: 'workflow:setup', kind: 'workflow', referenceFilename: 'feature-flags-setup.md', + label: 'Setup', }, { id: 'workflow:verify', kind: 'workflow', referenceFilename: 'feature-flags-verify.md', + label: 'Verify', }, - { id: 'env-vars', kind: 'env-vars' }, + { id: 'env-vars', kind: 'env-vars', label: 'Environment variables' }, ]); }); @@ -80,10 +104,12 @@ describe('WizardWorkflowQueue', () => { id: 'workflow:1.0-begin', kind: 'workflow', referenceFilename: 'basic-integration-1.0-begin.md', + label: 'PostHog Setup - Begin', }); expect(items[items.length - 1]).toEqual({ id: 'env-vars', kind: 'env-vars', + label: 'Environment variables', }); expect(items.find((i) => i.id === 'bootstrap')).toBeUndefined(); }); @@ -91,20 +117,30 @@ describe('WizardWorkflowQueue', () => { it('supports enqueue and dequeue operations', () => { const queue = new WizardWorkflowQueue(); - queue.enqueue({ id: 'bootstrap', kind: 'bootstrap' }); + queue.enqueue({ id: 'bootstrap', kind: 'bootstrap', label: 'Bootstrap' }); queue.enqueue({ id: 'workflow:1.0-begin', kind: 'workflow', referenceFilename: 'basic-integration-1.0-begin.md', + label: 'Begin', }); - expect(queue.peek()).toEqual({ id: 'bootstrap', kind: 'bootstrap' }); - expect(queue.dequeue()).toEqual({ id: 'bootstrap', kind: 'bootstrap' }); + expect(queue.peek()).toEqual({ + id: 'bootstrap', + kind: 'bootstrap', + label: 'Bootstrap', + }); + expect(queue.dequeue()).toEqual({ + id: 'bootstrap', + kind: 'bootstrap', + label: 'Bootstrap', + }); expect(queue).toHaveLength(1); expect(queue.dequeue()).toEqual({ id: 'workflow:1.0-begin', kind: 'workflow', referenceFilename: 'basic-integration-1.0-begin.md', + label: 'Begin', }); expect(queue).toHaveLength(0); }); @@ -149,18 +185,22 @@ workflow: { stepId: '1.0-begin', referenceFilename: 'basic-integration-1.0-begin.md', + title: 'PostHog Setup - Begin', }, { stepId: '1.1-edit', referenceFilename: 'basic-integration-1.1-edit.md', + title: 'PostHog Setup - Edit', }, { stepId: '1.2-revise', referenceFilename: 'basic-integration-1.2-revise.md', + title: 'PostHog Setup - Revise', }, { stepId: '1.3-conclude', referenceFilename: 'basic-integration-1.3-conclude.md', + title: 'PostHog Setup - Conclusion', }, ]); }); diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts index 4f0cb847..c6a4b9bc 100644 --- a/src/lib/agent-runner.ts +++ b/src/lib/agent-runner.ts @@ -342,9 +342,13 @@ export async function runAgentWizard( // ── Step 3: Execute workflow steps + env-vars from the queue ── const queue = createPostBootstrapQueue(workflowSteps); + getUI().setWorkQueue(queue); while (queue.length > 0) { const queueItem = queue.dequeue()!; + + getUI().setCurrentQueueItem({ id: queueItem.id, label: queueItem.label }); + const prompt = buildQueuedPrompt( queueItem, config, @@ -373,10 +377,13 @@ export async function runAgentWizard( middleware, ); + getUI().completeQueueItem({ id: queueItem.id, label: queueItem.label }); + if (agentResult.error) { break; } } + getUI().setCurrentQueueItem(null); } // Handle error cases detected in agent output diff --git a/src/lib/stage.ts b/src/lib/stage.ts new file mode 100644 index 00000000..69ceda65 --- /dev/null +++ b/src/lib/stage.ts @@ -0,0 +1,66 @@ +import type { WizardSession } from './wizard-session'; + +/** + * A workflow step is the primary unit of the wizard's execution model. + * + * It can own: + * - a screen in the TUI (optional — some steps are headless) + * - agent work via a workflow reference (optional — some steps are UI-only) + * - local state needs (selectors it depends on) + * - completion and visibility predicates + * + * The current PostHog integration flow is one ordered list of steps. + * Future flows (e.g. feature-flag builder) register a different step list. + */ +export interface WorkflowStep { + /** Unique identifier for this step */ + id: string; + + /** + * TUI screen this step owns, if any. + * Matches the Screen enum values (e.g. 'intro', 'run', 'outro'). + */ + screen?: string; + + /** + * Whether this step should be visible in the current flow. + * If omitted, the step is always visible. + */ + show?: (session: WizardSession) => boolean; + + /** + * Whether this step is complete. + * The flow engine advances past complete steps. + */ + isComplete?: (session: WizardSession) => boolean; + + /** + * Workflow reference filename this step executes, if any. + * When set, the runner issues a continued query for this reference. + * e.g. "basic-integration-1.0-begin.md" + */ + workflowReference?: string; + + /** + * Whether this step blocks downstream code via a gate promise. + * e.g. "setup" and "health-check" gate bin.ts before runWizard(). + */ + gate?: 'setup' | 'health'; + + /** + * Hook called when the step becomes active. + */ + onEnter?: () => void; + + /** + * Hook called when the step completes. + */ + onComplete?: () => void; +} + +/** + * An ordered list of workflow steps that defines a wizard flow. + * The first flow is the current PostHog integration. + * Future flows register different step lists. + */ +export type Workflow = WorkflowStep[]; diff --git a/src/lib/workflow-queue.ts b/src/lib/workflow-queue.ts index c8ab586e..e3b47e29 100644 --- a/src/lib/workflow-queue.ts +++ b/src/lib/workflow-queue.ts @@ -2,30 +2,48 @@ export type WizardWorkflowQueueItem = | { id: 'bootstrap'; kind: 'bootstrap'; + label: string; } | { id: string; kind: 'workflow'; referenceFilename: string; + label: string; } | { id: 'env-vars'; kind: 'env-vars'; + label: string; }; export class WizardWorkflowQueue { private items: WizardWorkflowQueueItem[]; + private onChange?: () => void; constructor(items: WizardWorkflowQueueItem[] = []) { this.items = [...items]; } + /** Register a listener that fires on any queue mutation. */ + setOnChange(fn: () => void): void { + this.onChange = fn; + } + enqueue(item: WizardWorkflowQueueItem): void { this.items.push(item); + this.onChange?.(); + } + + /** Insert an item at the front of the queue (next to run). */ + enqueueNext(item: WizardWorkflowQueueItem): void { + this.items.unshift(item); + this.onChange?.(); } dequeue(): WizardWorkflowQueueItem | undefined { - return this.items.shift(); + const item = this.items.shift(); + this.onChange?.(); + return item; } peek(): WizardWorkflowQueueItem | undefined { @@ -50,12 +68,14 @@ export interface WorkflowStepSeed { stepId: string; /** Filename inside the skill's references/ dir, e.g. "basic-integration-1.0-begin.md" */ referenceFilename: string; + /** Human-readable title from SKILL.md frontmatter, e.g. "PostHog Setup - Edit" */ + title: string; } /** * Parse workflow steps from SKILL.md content. * - * Extracts `step_id` and `reference` from the YAML frontmatter's + * Extracts `step_id`, `reference`, and `title` from the YAML frontmatter's * `workflow` array. Uses simple regex — no YAML library needed * since we control the output format in skill-generator. */ @@ -67,12 +87,13 @@ export function parseWorkflowStepsFromSkillMd( const frontmatter = fmMatch[1]; const steps: WorkflowStepSeed[] = []; - const entryRegex = /step_id:\s*(.+)\n\s*reference:\s*(.+)/g; + const entryRegex = /step_id:\s*(.+)\n\s*reference:\s*(.+)\n\s*title:\s*(.+)/g; let match; while ((match = entryRegex.exec(frontmatter)) !== null) { steps.push({ stepId: match[1].trim(), referenceFilename: match[2].trim(), + title: match[3].trim(), }); } return steps; @@ -86,15 +107,16 @@ export function createInitialWizardWorkflowQueue( steps: WorkflowStepSeed[], ): WizardWorkflowQueue { const items: WizardWorkflowQueueItem[] = [ - { id: 'bootstrap', kind: 'bootstrap' }, + { id: 'bootstrap', kind: 'bootstrap', label: 'Preparing integration' }, ...steps.map( (step): WizardWorkflowQueueItem => ({ id: `workflow:${step.stepId}`, kind: 'workflow', referenceFilename: step.referenceFilename, + label: step.title, }), ), - { id: 'env-vars', kind: 'env-vars' }, + { id: 'env-vars', kind: 'env-vars', label: 'Environment variables' }, ]; return new WizardWorkflowQueue(items); } @@ -112,9 +134,10 @@ export function createPostBootstrapQueue( id: `workflow:${step.stepId}`, kind: 'workflow', referenceFilename: step.referenceFilename, + label: step.title, }), ), - { id: 'env-vars', kind: 'env-vars' }, + { id: 'env-vars', kind: 'env-vars', label: 'Environment variables' }, ]; return new WizardWorkflowQueue(items); } diff --git a/src/ui/logging-ui.ts b/src/ui/logging-ui.ts index b5a816f0..574c66f5 100644 --- a/src/ui/logging-ui.ts +++ b/src/ui/logging-ui.ts @@ -6,6 +6,7 @@ import { TaskStatus, type WizardUI, type SpinnerHandle } from './wizard-ui'; import type { SettingsConflict } from '../lib/agent-interface'; +import type { WizardWorkflowQueue } from '../lib/workflow-queue'; export class LoggingUI implements WizardUI { intro(message: string): void { @@ -154,4 +155,16 @@ export class LoggingUI implements WizardUI { setEventPlan(_events: Array<{ name: string; description: string }>): void { // No-op in CI mode } + + setWorkQueue(_queue: WizardWorkflowQueue): void { + // No-op in CI mode + } + + setCurrentQueueItem(_item: { id: string; label: string } | null): void { + // No-op in CI mode + } + + completeQueueItem(_item: { id: string; label: string }): void { + // No-op in CI mode + } } diff --git a/src/ui/tui/ink-ui.ts b/src/ui/tui/ink-ui.ts index 305e5b33..33e51b06 100644 --- a/src/ui/tui/ink-ui.ts +++ b/src/ui/tui/ink-ui.ts @@ -9,6 +9,7 @@ import type { WizardUI, SpinnerHandle } from '../wizard-ui.js'; import type { WizardStore } from './store.js'; import type { SettingsConflict } from '../../lib/agent-interface.js'; +import type { WizardWorkflowQueue } from '../../lib/workflow-queue.js'; import { RunPhase, OutroKind } from '../../lib/wizard-session.js'; // Strip ANSI escape codes (chalk formatting) from strings @@ -156,4 +157,16 @@ export class InkUI implements WizardUI { setEventPlan(events: Array<{ name: string; description: string }>): void { this.store.setEventPlan(events); } + + setWorkQueue(queue: WizardWorkflowQueue): void { + this.store.workQueue = queue; + } + + setCurrentQueueItem(item: { id: string; label: string } | null): void { + this.store.setCurrentQueueItem(item); + } + + completeQueueItem(item: { id: string; label: string }): void { + this.store.completeQueueItem(item); + } } diff --git a/src/ui/tui/playground/demos/ProgressDemo.tsx b/src/ui/tui/playground/demos/ProgressDemo.tsx index f6513576..065450e0 100644 --- a/src/ui/tui/playground/demos/ProgressDemo.tsx +++ b/src/ui/tui/playground/demos/ProgressDemo.tsx @@ -7,30 +7,35 @@ import { Box, Text } from 'ink'; import { useState, useEffect } from 'react'; import { ProgressList, LoadingBox } from '../../primitives/index.js'; import type { ProgressItem } from '../../primitives/index.js'; +import { TaskStatus } from '../../../wizard-ui.js'; import { Colors } from '../../styles.js'; const INITIAL_ITEMS: ProgressItem[] = [ { label: 'Detect framework', activeForm: 'Detecting framework', - status: 'pending', + status: TaskStatus.Pending, }, { label: 'Install dependencies', activeForm: 'Installing dependencies', - status: 'pending', + status: TaskStatus.Pending, }, { label: 'Configure PostHog', activeForm: 'Configuring PostHog', - status: 'pending', + status: TaskStatus.Pending, }, { label: 'Add analytics provider', activeForm: 'Adding analytics provider', - status: 'pending', + status: TaskStatus.Pending, + }, + { + label: 'Verify setup', + activeForm: 'Verifying setup', + status: TaskStatus.Pending, }, - { label: 'Verify setup', activeForm: 'Verifying setup', status: 'pending' }, ]; export const ProgressDemo = () => { @@ -52,9 +57,9 @@ export const ProgressDemo = () => { setItems( INITIAL_ITEMS.map((item, i) => { - if (i < cycle) return { ...item, status: 'completed' as const }; - if (i === cycle) return { ...item, status: 'in_progress' as const }; - return { ...item, status: 'pending' as const }; + if (i < cycle) return { ...item, status: TaskStatus.Completed }; + if (i === cycle) return { ...item, status: TaskStatus.InProgress }; + return { ...item, status: TaskStatus.Pending }; }), ); }, [tick]); diff --git a/src/ui/tui/primitives/ProgressList.tsx b/src/ui/tui/primitives/ProgressList.tsx index f7d28d46..40355d2c 100644 --- a/src/ui/tui/primitives/ProgressList.tsx +++ b/src/ui/tui/primitives/ProgressList.tsx @@ -7,11 +7,14 @@ import { Box, Text } from 'ink'; import { Spinner } from '@inkjs/ui'; import { Colors, Icons } from '../styles.js'; import { LoadingBox } from './LoadingBox.js'; +import { TaskStatus } from '../../wizard-ui.js'; export interface ProgressItem { label: string; activeForm?: string; - status: 'pending' | 'in_progress' | 'completed'; + status: TaskStatus; + /** Nesting depth — 0 = top-level, 1 = nested under a stage, etc. */ + indent?: number; } interface ProgressListProps { @@ -34,26 +37,29 @@ export const ProgressList = ({ items, title }: ProgressListProps) => { {items.length === 0 && } {items.map((item, i) => { const icon = - item.status === 'completed' + item.status === TaskStatus.Completed ? Icons.squareFilled - : item.status === 'in_progress' + : item.status === TaskStatus.InProgress ? Icons.triangleRight : Icons.squareOpen; const color = - item.status === 'completed' + item.status === TaskStatus.Completed ? Colors.success - : item.status === 'in_progress' + : item.status === TaskStatus.InProgress ? Colors.primary : Colors.muted; const label = - item.status === 'in_progress' && item.activeForm + item.status === TaskStatus.InProgress && item.activeForm ? item.activeForm : item.label; + const pad = item.indent ? ' '.repeat(item.indent) : ''; + return ( + {pad} {icon} - {label} + {label} ); })} diff --git a/src/ui/tui/screens/RunScreen.tsx b/src/ui/tui/screens/RunScreen.tsx index 7ba6ff5c..b0811dfd 100644 --- a/src/ui/tui/screens/RunScreen.tsx +++ b/src/ui/tui/screens/RunScreen.tsx @@ -21,6 +21,7 @@ import { HNViewer, } from '../primitives/index.js'; import type { ProgressItem } from '../primitives/index.js'; +import { TaskStatus } from '../../wizard-ui.js'; import { ADDITIONAL_FEATURE_LABELS } from '../../../lib/wizard-session.js'; import { LearnCard } from '../components/LearnCard.js'; import { TipsCard } from '../components/TipsCard.js'; @@ -40,23 +41,53 @@ export const RunScreen = ({ store }: RunScreenProps) => { const [columns] = useStdoutDimensions(); - const progressItems: ProgressItem[] = store.tasks.map((t) => ({ - label: t.label, - activeForm: t.activeForm, - status: t.status, - })); + // Build stage-grouped progress items + const progressItems: ProgressItem[] = []; + const current = store.currentQueueItem; + const completed = store.completedQueueItems; + const pendingQueue = store.workQueue?.toArray() ?? []; - // When all tasks are done but the queue has features, show a transitional item - const queue = store.session.additionalFeatureQueue; - const allDone = - progressItems.length > 0 && - progressItems.every((t) => t.status === 'completed'); - if (allDone && queue.length > 0) { - const nextLabel = ADDITIONAL_FEATURE_LABELS[queue[0]]; + // Completed stages + for (const item of completed) { + progressItems.push({ + label: item.label, + status: TaskStatus.Completed, + }); + } + + // Current stage header + nested agent tasks + if (current) { + progressItems.push({ + label: current.label, + activeForm: current.label, + status: TaskStatus.InProgress, + }); + // Nest agent tasks under current stage + for (const t of store.tasks) { + progressItems.push({ + label: t.label, + activeForm: t.activeForm, + status: t.status, + indent: 1, + }); + } + } + + // Pending queue items + for (const item of pendingQueue) { + progressItems.push({ + label: item.label, + status: TaskStatus.Pending, + }); + } + + // Additional features waiting + const featureQueue = store.session.additionalFeatureQueue; + for (const feature of featureQueue) { + const nextLabel = ADDITIONAL_FEATURE_LABELS[feature]; progressItems.push({ label: `Set up ${nextLabel}`, - activeForm: `Setting up ${nextLabel}...`, - status: 'in_progress', + status: TaskStatus.Pending, }); } diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts index 26c9fd75..d5d95584 100644 --- a/src/ui/tui/store.ts +++ b/src/ui/tui/store.ts @@ -34,6 +34,7 @@ import { evaluateWizardReadiness, WizardReadiness, } from '../../lib/health-checks/readiness.js'; +import type { WizardWorkflowQueue } from '../../lib/workflow-queue.js'; export { TaskStatus, Screen, Overlay, Flow, RunPhase, McpOutcome }; export type { ScreenName, OutroData, WizardSession }; @@ -58,6 +59,8 @@ export class WizardStore { private $statusExpanded = atom(false); private $tasks = atom([]); private $eventPlan = atom([]); + private $currentQueueItem = atom<{ id: string; label: string } | null>(null); + private $completedQueueItems = atom<{ id: string; label: string }[]>([]); private $learnCardBlockIdx = atom(0); private $learnCardComplete = atom(false); private $version = atom(0); @@ -70,6 +73,22 @@ export class WizardStore { version = ''; + /** + * Live reference to the workflow queue. + * Set by the agent runner so the UI can dynamically enqueue work. + */ + private _workQueue: WizardWorkflowQueue | null = null; + + get workQueue() { + return this._workQueue; + } + + set workQueue(queue: WizardWorkflowQueue | null) { + this._workQueue = queue; + // Re-render when the queue changes (enqueue/dequeue) + queue?.setOnChange(() => this.emitChange()); + } + /** Navigation router — resolves active screen from session state. */ readonly router: WizardRouter; @@ -154,6 +173,30 @@ export class WizardStore { return this.$eventPlan.get(); } + get currentQueueItem(): { id: string; label: string } | null { + return this.$currentQueueItem.get(); + } + + get completedQueueItems(): { id: string; label: string }[] { + return this.$completedQueueItems.get(); + } + + setCurrentQueueItem(item: { id: string; label: string } | null): void { + this.$currentQueueItem.set(item); + // Clear agent tasks when transitioning to a new stage — + // each stage gets a fresh task list from TodoWrite + this.$tasks.set([]); + this.emitChange(); + } + + completeQueueItem(item: { id: string; label: string }): void { + const completed = this.$completedQueueItems.get(); + if (!completed.some((c) => c.id === item.id)) { + this.$completedQueueItems.set([...completed, item]); + } + this.emitChange(); + } + get statusExpanded(): boolean { return this.$statusExpanded.get(); } @@ -520,6 +563,7 @@ export class WizardStore { const incomingLabels = new Set(incoming.map((t) => t.label)); + // Keep completed tasks that aren't being updated by the incoming list const retained = this.$tasks .get() .filter((t) => t.done && !incomingLabels.has(t.label)); diff --git a/src/ui/wizard-ui.ts b/src/ui/wizard-ui.ts index 09bfb564..8a8beafc 100644 --- a/src/ui/wizard-ui.ts +++ b/src/ui/wizard-ui.ts @@ -9,6 +9,7 @@ */ import type { SettingsConflict } from '../lib/agent-interface'; +import type { WizardWorkflowQueue } from '../lib/workflow-queue'; export enum TaskStatus { Pending = 'pending', @@ -96,4 +97,11 @@ export interface WizardUI { // ── Event plan from .posthog-events.json ──────────────────── setEventPlan(events: Array<{ name: string; description: string }>): void; + + // ── Work queue (for dynamic task enqueue from UI) ────────── + setWorkQueue(queue: WizardWorkflowQueue): void; + + // ── Queue stage tracking ────────────────────────────────── + setCurrentQueueItem(item: { id: string; label: string } | null): void; + completeQueueItem(item: { id: string; label: string }): void; }