diff --git a/README.md b/README.md index af86c33e..f738e00d 100644 --- a/README.md +++ b/README.md @@ -198,212 +198,71 @@ 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` -# Wizard execution flow +# Execution flow -## Full lifecycle - -When a user runs `npx @posthog/wizard`, here's what happens end-to-end: - -### 1. CLI parsing and framework detection (`bin.ts` → `src/run.ts`) - -`bin.ts` parses CLI args, checks Node version, and calls `runWizard()` in `src/run.ts`. The run function detects the project framework (Next.js, React, etc.) by inspecting `package.json` and project structure, then loads the matching `FrameworkConfig` from `src/frameworks/`. - -### 2. TUI startup and UI flow (`src/ui/tui/start-tui.ts`) - -The TUI renders and the user progresses through screens. Screen order is driven by a `Workflow` — an ordered list of `WorkflowStep` objects defined in `src/lib/workflows/posthog-integration.ts`. Each step declares which screen it owns and when that screen is complete. - -The workflow is converted to `FlowEntry[]` via `workflowToFlowEntries()` and fed to the router. The router walks the entries, skipping completed/hidden screens, and returns the first incomplete one. This is reactive — every session mutation re-resolves the active screen. - -**Gate steps** block downstream code. The `intro` step has `gate: 'setup'` — `bin.ts` awaits `store.setupComplete` before proceeding. The `health-check` step has `gate: 'health'` — `bin.ts` awaits `store.healthGateComplete`. - -### 3. Agent runner (`src/lib/agent-runner.ts`) - -Once gates resolve, `runAgentWizard()` runs. This is where the queue takes over: - -**Bootstrap query** — A standalone query tells the agent to load the skill menu, pick and install a skill, read SKILL.md, and emit the installed skill ID via `[WIZARD-SKILL-ID] `. The model does NOT know about the queue — it just prepares the skill. - -**SKILL.md parsing** — After bootstrap, the runner reads `.claude/skills//SKILL.md` from disk and parses the `workflow` array from its YAML frontmatter using `parseWorkflowStepsFromSkillMd()`. This produces a `WorkflowStepSeed[]` with step ids, reference filenames, and display titles. - -**Queue seeding** — `createPostBootstrapQueue(steps)` builds a `WizardWorkflowQueue` from the parsed steps plus an `env-vars` step at the end. The queue is set on the store via `getUI().setWorkQueue(queue)` so the TUI can display it and dynamically enqueue new work. - -**Execution loop** — The runner pops items from the queue one at a time: ``` -while (queue.length > 0) { - dequeue → setCurrentQueueItem → build prompt → runAgent → completeQueueItem -} +bin.ts → detect framework → start TUI → await gates → runAgentWizard() + │ + wizard-queued-workflow flag? + │ │ + ON OFF + │ │ + queued-workflow-runner legacy/single-query-runner + │ │ + bootstrap query one big prompt + parse SKILL.md (does everything) + queue loop + per-step prompts ``` -Each `runAgent` call continues the same conversation via `resumeSessionId`. The model sees one prompt per step — either "read and follow this reference file" (for workflow items) or "set up environment variables" (for env-vars). The stop hook only fires the remark/feature-queue on the last item. +## 1. CLI + framework detection -### 4. TUI progress tracking +`bin.ts` → `src/run.ts` → detects framework → loads `FrameworkConfig`. -During the run, the RunScreen displays a stage-grouped progress list. Stage headers come from queue item labels (which come from SKILL.md frontmatter titles). Nested tasks come from the agent's `TodoWrite` tool calls. When the runner advances to a new queue item, `setCurrentQueueItem()` fires, the store clears the task list, and the previous item moves to the completed list. +## 2. TUI screens -The queue is reactive on the store — `enqueue()` and `dequeue()` trigger `emitChange()` which re-renders the UI immediately. +Screen flow is a `Workflow` (`WorkflowStep[]`) defined in `src/lib/workflows/posthog-integration.ts`. Converted to `FlowEntry[]` via `workflowToFlowEntries()` for the router. Gate steps (`setup`, `health`) block the agent runner until the user completes them. -### 5. Post-run (`agent-runner.ts` after loop) +## 3. Agent runner -After the queue drains: error handling, env var upload to hosting providers, outro data construction, analytics shutdown. +`agent-runner.ts` initializes the agent, then forks on the `wizard-queued-workflow` feature flag. -## Data flow diagram +**Flag OFF** → `legacy/single-query-runner.ts` — one prompt, one `runAgent` call. Skills downloaded as v1 (continuation links in reference bodies). -``` -bin.ts - │ - ├─ Framework detection → FrameworkConfig - ├─ TUI startup → WizardStore + Router - │ │ - │ └─ Workflow (WorkflowStep[]) - │ │ - │ └─ workflowToFlowEntries() → FlowEntry[] → Router (screen resolution) - │ - ├─ await setupComplete (gate) - ├─ await healthGateComplete (gate) - │ - └─ runAgentWizard() - │ - ├─ Bootstrap query → skill installed → [WIZARD-SKILL-ID] - │ - ├─ Read SKILL.md → parseWorkflowStepsFromSkillMd() → WorkflowStepSeed[] - │ - ├─ createPostBootstrapQueue(steps) → WizardWorkflowQueue - │ │ - │ └─ setWorkQueue(queue) → store (reactive, UI can enqueue) - │ - └─ while (queue.length > 0) - │ - ├─ dequeue → setCurrentQueueItem - ├─ buildWorkflowStepPrompt / buildEnvVarPrompt - ├─ runAgent (continued conversation) - └─ completeQueueItem -``` +**Flag ON** → `queued-workflow-runner.ts` — bootstrap installs the skill, SKILL.md frontmatter is parsed into a queue, then each step runs as a continued conversation. Skills downloaded as v2 (workflow metadata in frontmatter, no continuation links). -# Workflow queue - -## 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 - # ... ---- -``` +## 4. Queued workflow detail -- `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) +1. **Bootstrap** — installs skill, emits `[WIZARD-SKILL-ID] ` +2. **Parse SKILL.md** — `parseWorkflowStepsFromSkillMd()` extracts `workflow[]` from frontmatter +3. **Seed queue** — `createPostBootstrapQueue(steps)` builds a `WizardWorkflowQueue` +4. **Execute** — dequeue → build prompt → `runAgent` with `resumeSessionId` → repeat -## Queue item types +The queue is reactive on the store — UI or business logic can `enqueue()` / `enqueueNext()` during the run. -```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 }; -``` +## 5. v1 vs v2 skills -## 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', -}); -``` +Controlled by the `wizard-queued-workflow` flag. The `install_skill` MCP tool checks `useV2Skills` and picks the right download URL. -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. +| | v1 (legacy) | v2 (queued) | +|---|---|---| +| Source files | `llm-prompts/basic-integration/` | `llm-prompts/basic-integration-v2/` | +| Step ordering | Continuation links in body | `workflow[]` in SKILL.md frontmatter | +| `next` defined in | Computed by generator | Workflow file frontmatter | +| Served from | `dist/skills/` | `dist/basic-integration-v2/` | -## 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. - -## Defining a workflow - -A workflow is an ordered list of `WorkflowStep` objects. Each step can own a screen, agent work, or both. - -```typescript -// src/lib/workflow-step.ts -interface WorkflowStep { - id: string; // unique step id - label: string; // shown in progress list - screen?: string; // TUI screen (e.g. 'intro', 'run') - show?: (session: WizardSession) => boolean; // visibility predicate - isComplete?: (session: WizardSession) => boolean; // completion predicate - gate?: 'setup' | 'health'; // blocks downstream code -} -``` - -The current PostHog integration workflow is defined in `src/lib/workflows/posthog-integration.ts`: - -```typescript -export const POSTHOG_INTEGRATION_WORKFLOW: Workflow = [ - { id: 'intro', label: 'Welcome', screen: 'intro', gate: 'setup', isComplete: ... }, - { id: 'health', label: 'Health check', screen: 'health-check', gate: 'health', ... }, - { id: 'setup', label: 'Setup', screen: 'setup', show: needsSetup, ... }, - { id: 'auth', label: 'Authentication', screen: 'auth', isComplete: ... }, - { id: 'run', label: 'Integration', screen: 'run', isComplete: ... }, - { id: 'mcp', label: 'MCP servers', screen: 'mcp', isComplete: ... }, - { id: 'outro', label: 'Done', screen: 'outro', isComplete: ... }, - { id: 'skills', label: 'Skills', screen: 'skills' }, -]; -``` - -### Creating a new workflow - -1. Create a new file in `src/lib/workflows/` (e.g. `feature-flags.ts`) -2. Export a `Workflow` array with your steps -3. Each step with a `screen` field needs a matching component in the screen registry -4. The flow engine converts your workflow to `FlowEntry[]` via `workflowToFlowEntries()` — the existing router handles the rest -5. Agent work steps are seeded from SKILL.md frontmatter at runtime, not from the workflow definition - -### How the pieces connect - -``` -WorkflowStep[] ──workflowToFlowEntries()──> FlowEntry[] ──> Router (screen resolution) - │ -SKILL.md frontmatter ──parseWorkflowStepsFromSkillMd()──> Queue ──> Agent runner (per-step queries) -``` +## 6. Key files -The workflow definition owns the UI flow. The SKILL.md frontmatter owns the agent work sequence. Both run during the same wizard session. +| File | Purpose | +|---|---| +| `src/lib/agent-runner.ts` | Shared setup, flag fork, error handling | +| `src/lib/queued-workflow-runner.ts` | New flow: bootstrap → queue → per-step prompts | +| `src/lib/legacy/single-query-runner.ts` | Old flow: one prompt, one call | +| `src/lib/legacy/integration-prompt.ts` | Old flow: prompt builder | +| `src/lib/workflow-queue.ts` | Queue, parser, seed functions | +| `src/lib/workflow-step.ts` | `WorkflowStep` interface, `workflowToFlowEntries()` | +| `src/lib/workflows/posthog-integration.ts` | TUI screen flow as `WorkflowStep[]` | +| `src/lib/wizard-tools.ts` | MCP tools including `install_skill` (v1/v2) | # Health checks diff --git a/src/lib/__tests__/workflow-queue.test.ts b/src/lib/__tests__/workflow-queue.test.ts index 547af0c7..1becad06 100644 --- a/src/lib/__tests__/workflow-queue.test.ts +++ b/src/lib/__tests__/workflow-queue.test.ts @@ -1,6 +1,5 @@ import { WizardWorkflowQueue, - createInitialWizardWorkflowQueue, createPostBootstrapQueue, parseWorkflowStepsFromSkillMd, type WorkflowStepSeed, @@ -30,73 +29,7 @@ const BASIC_INTEGRATION_STEPS: WorkflowStepSeed[] = [ ]; describe('WizardWorkflowQueue', () => { - it('seeds a queue from workflow steps in the expected order', () => { - const queue = createInitialWizardWorkflowQueue(BASIC_INTEGRATION_STEPS); - - expect(queue.toArray()).toEqual([ - { 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', label: 'Environment variables' }, - ]); - }); - - it('builds a queue from arbitrary steps, not just basic-integration', () => { - const customSteps: WorkflowStepSeed[] = [ - { - 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', 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', label: 'Environment variables' }, - ]); - }); - - it('createPostBootstrapQueue omits bootstrap', () => { + it('createPostBootstrapQueue builds queue without bootstrap', () => { const queue = createPostBootstrapQueue(BASIC_INTEGRATION_STEPS); const items = queue.toArray(); diff --git a/src/lib/agent-interface.ts b/src/lib/agent-interface.ts index 84d1249f..45bc8344 100644 --- a/src/lib/agent-interface.ts +++ b/src/lib/agent-interface.ts @@ -651,6 +651,7 @@ export async function initializeAgent( workingDirectory: config.workingDirectory, detectPackageManager: config.detectPackageManager, skillsBaseUrl: config.skillsBaseUrl, + useV2Skills: config.wizardFlags?.['wizard-queued-workflow'] === 'true', }); mcpServers['wizard-tools'] = wizardToolsServer; diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts index c6a4b9bc..5e120842 100644 --- a/src/lib/agent-runner.ts +++ b/src/lib/agent-runner.ts @@ -1,9 +1,4 @@ -import fs from 'fs'; -import path from 'path'; -import { - DEFAULT_PACKAGE_INSTALLATION, - type FrameworkConfig, -} from './framework-config'; +import { type FrameworkConfig } from './framework-config'; import { type WizardSession, OutroKind } from './wizard-session'; import { tryGetPackageJson, @@ -18,8 +13,8 @@ import { getUI } from '../ui'; import { initializeAgent, runAgent, - AgentSignals, AgentErrorType, + AgentSignals, buildWizardMetadata, checkAllSettingsConflicts, backupAndFixClaudeSettings, @@ -40,13 +35,8 @@ import { registerCleanup, } from '../utils/wizard-abort'; import { formatScanReport, writeScanReport } from './yara-hooks'; -import { - createPostBootstrapQueue, - parseWorkflowStepsFromSkillMd, - type WizardWorkflowQueueItem, -} from './workflow-queue'; - -const WIZARD_SKILL_ID_SIGNAL = '[WIZARD-SKILL-ID]'; +import { runSingleQueryFlow } from './legacy/single-query-runner'; +import { runQueuedWorkflow } from './queued-workflow-runner'; /** * Build a WizardOptions bag from a WizardSession (for code that still expects WizardOptions). @@ -276,114 +266,35 @@ export async function runAgentWizard( ? createBenchmarkPipeline(spinner, sessionToOptions(session)) : undefined; - // ── Step 1: Bootstrap — install the skill and get its ID ── - - let agentResult = await runAgent( - agent, - buildBootstrapPrompt(config, promptContext, frameworkContext), - sessionToOptions(session), - spinner, - { - estimatedDurationMinutes: config.ui.estimatedDurationMinutes, - spinnerMessage: 'Preparing integration...', - successMessage: 'Integration prepared', - errorMessage: 'Integration failed during bootstrap', - additionalFeatureQueue: [], - requestRemark: false, - captureOutputText: true, - captureSessionId: true, - finalizeMiddleware: false, - }, - middleware, - ); - - const queuedSessionId = agentResult.sessionId; - const installedSkillId = - extractInstalledSkillId(agentResult.outputText ?? '') ?? undefined; + // ── Feature flag: queued workflow vs old single-query flow ── + const useQueuedWorkflow = wizardFlags['wizard-queued-workflow'] === 'true'; + logToFile(`[agent-runner] wizard-queued-workflow=${useQueuedWorkflow}`); - if (!installedSkillId) { - await wizardAbort({ - message: - 'The wizard could not determine which integration skill was installed during bootstrap.', - error: new WizardError('Bootstrap step did not emit installed skill id'), - }); - } - - // ── Step 2: Read SKILL.md and seed the queue from its frontmatter ── - - if (!agentResult.error && installedSkillId) { - const skillMdPath = path.join( - session.installDir, - '.claude', - 'skills', - installedSkillId, - 'SKILL.md', - ); - const skillMdContent = fs.readFileSync(skillMdPath, 'utf-8'); - const workflowSteps = parseWorkflowStepsFromSkillMd(skillMdContent); + let agentResult: Awaited>; - if (workflowSteps.length === 0) { - logToFile( - '[agent-runner] No workflow steps found in SKILL.md frontmatter, aborting', - ); - await wizardAbort({ - message: - 'The installed skill does not contain workflow steps in its metadata.', - error: new WizardError('No workflow steps in SKILL.md frontmatter'), - }); - } - - logToFile( - `[agent-runner] Seeded queue from SKILL.md: ${workflowSteps - .map((s) => s.stepId) - .join(', ')}`, + if (useQueuedWorkflow) { + agentResult = await runQueuedWorkflow( + agent, + config, + session, + sessionToOptions(session), + promptContext, + frameworkContext, + spinner, + middleware, ); - - // ── 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, - promptContext, - installedSkillId, - ); - - agentResult = await runAgent( - agent, - prompt, - sessionToOptions(session), - spinner, - { - estimatedDurationMinutes: config.ui.estimatedDurationMinutes, - spinnerMessage: getQueueSpinnerMessage(queueItem), - successMessage: getQueueSuccessMessage(queueItem, config), - errorMessage: `Integration failed during ${queueItem.id}`, - additionalFeatureQueue: - queueItem.id === 'env-vars' ? session.additionalFeatureQueue : [], - resumeSessionId: queuedSessionId, - requestRemark: queueItem.id === 'env-vars', - captureOutputText: false, - captureSessionId: false, - finalizeMiddleware: queue.length === 0, - }, - middleware, - ); - - getUI().completeQueueItem({ id: queueItem.id, label: queueItem.label }); - - if (agentResult.error) { - break; - } - } - getUI().setCurrentQueueItem(null); + } else { + // OLD FLOW — single monolithic prompt (see legacy/ folder) + agentResult = await runSingleQueryFlow({ + agent, + config, + session, + options: sessionToOptions(session), + spinner, + promptContext, + frameworkContext, + middleware, + }); } // Handle error cases detected in agent output @@ -493,184 +404,5 @@ export async function runAgentWizard( await analytics.shutdown('success'); } -/** - * Build the integration prompt for the agent. - */ -function buildQueuedPrompt( - queueItem: WizardWorkflowQueueItem, - config: FrameworkConfig, - context: { - frameworkVersion: string; - typescript: boolean; - projectApiKey: string; - host: string; - projectId: number; - }, - installedSkillId: string, -): string { - if (queueItem.kind === 'workflow') { - return buildWorkflowStepPrompt( - queueItem.referenceFilename, - installedSkillId, - ); - } - - return buildEnvVarPrompt(config, context); -} - -function buildProjectContextBlock( - config: FrameworkConfig, - context: { - frameworkVersion: string; - typescript: boolean; - projectApiKey: string; - host: string; - projectId: number; - }, - frameworkContext: Record, -): string { - const additionalLines = config.prompts.getAdditionalContextLines - ? config.prompts.getAdditionalContextLines(frameworkContext) - : []; - - const additionalContext = - additionalLines.length > 0 - ? '\n' + additionalLines.map((line) => `- ${line}`).join('\n') - : ''; - - return `Project context: -- PostHog Project ID: ${context.projectId} -- Framework: ${config.metadata.name} ${context.frameworkVersion} -- TypeScript: ${context.typescript ? 'Yes' : 'No'} -- PostHog public token: ${context.projectApiKey} -- PostHog Host: ${context.host} -- Project type: ${config.prompts.projectTypeDetection} -- Package installation: ${ - config.prompts.packageInstallation ?? DEFAULT_PACKAGE_INSTALLATION - }${additionalContext}`; -} - -function buildBootstrapPrompt( - config: FrameworkConfig, - context: { - frameworkVersion: string; - typescript: boolean; - projectApiKey: string; - host: string; - projectId: number; - }, - frameworkContext: Record, -): string { - return `You have access to the PostHog MCP server which provides skills to integrate PostHog into this ${ - config.metadata.name - } project. - -${buildProjectContextBlock(config, context, frameworkContext)} - -STEP 1: Call load_skill_menu (from the wizard-tools MCP server) to see available skills. - If the tool fails, emit: ${ - AgentSignals.ERROR_MCP_MISSING - } Could not load skill menu and halt. - - Choose a skill from the \`integration\` category that matches this project's framework. Do NOT pick skills from other categories (llm-analytics, error-tracking, feature-flags, omnibus, etc.) — those are handled separately. - If no suitable integration skill is found, emit: ${ - AgentSignals.ERROR_RESOURCE_MISSING - } Could not find a suitable skill for this project. - -STEP 2: Call install_skill (from the wizard-tools MCP server) with the chosen skill ID (e.g., "integration-nextjs-app-router"). - Do NOT run any shell commands to install skills. - -STEP 3: Load the installed skill's SKILL.md file to understand what references are available. - -STEP 4: When preparation is complete, emit exactly one line in this format: -${WIZARD_SKILL_ID_SIGNAL} - -Important: -- Do NOT execute any of the workflow reference files yet. -- Do NOT set up environment variables yet. -- Stop after preparation is complete. -- Use the detect_package_manager tool (from the wizard-tools MCP server) to determine which package manager the project uses. Do not manually search for lockfiles or config files. Always install packages as a background task. Don't await completion; proceed with other work immediately after starting the installation. You must read a file immediately before attempting to write it, even if you have previously read it; failure to do so will cause a tool failure. - -`; -} - -function buildWorkflowStepPrompt( - referenceFilename: string, - installedSkillId: string, -): string { - return `Continue the existing conversation. - -Read and follow this workflow reference: -\`.claude/skills/${installedSkillId}/references/${referenceFilename}\` - -Important: -- Complete only this workflow step. -- Do NOT continue to any other workflow file. -- Do NOT set up environment variables yet. -- Stop when this step is complete.`; -} - -function buildEnvVarPrompt( - config: FrameworkConfig, - context: { - frameworkVersion: string; - typescript: boolean; - projectApiKey: string; - host: string; - projectId: number; - }, -): string { - return `Continue the existing conversation. - -Execute the final queued environment-variable setup step for this ${ - config.metadata.name - } project. - -${buildProjectContextBlock(config, context, {})} - -Set up environment variables for PostHog using the wizard-tools MCP server (this runs locally — secret values never leave the machine): -- Use check_env_keys to see which keys already exist in the project's .env file (e.g. .env.local or .env). -- Use set_env_values to create or update the PostHog public token and host, using the appropriate environment variable naming convention for ${ - config.metadata.name - }, which you'll find in example code. The tool will also ensure .gitignore coverage. Don't assume the presence of keys means the value is up to date. Write the correct value each time. -- Reference these environment variables in the code files you create instead of hardcoding the public token and host. - -Stop after the environment-variable setup step is complete.`; -} - -function getQueueSpinnerMessage(queueItem: WizardWorkflowQueueItem): string { - switch (queueItem.kind) { - case 'bootstrap': - return 'Preparing integration...'; - case 'workflow': - return `Running step ${queueItem.id.replace('workflow:', '')}...`; - case 'env-vars': - return 'Finalizing environment variables...'; - } -} - -function getQueueSuccessMessage( - queueItem: WizardWorkflowQueueItem, - config: FrameworkConfig, -): string { - switch (queueItem.kind) { - case 'bootstrap': - return 'Integration prepared'; - case 'workflow': - return `Step ${queueItem.id.replace('workflow:', '')} complete`; - case 'env-vars': - return config.ui.successMessage; - } -} - -export function extractInstalledSkillId(outputText: string): string | null { - const match = outputText.match( - new RegExp( - `${WIZARD_SKILL_ID_SIGNAL.replace( - /[.*+?^${}()|[\]\\]/g, - '\\$&', - )}\\s+([A-Za-z0-9._-]+)`, - ), - ); - return match?.[1] ?? null; -} +// Re-export for tests +export { extractInstalledSkillId } from './queued-workflow-runner'; diff --git a/src/lib/legacy/integration-prompt.ts b/src/lib/legacy/integration-prompt.ts new file mode 100644 index 00000000..058c0563 --- /dev/null +++ b/src/lib/legacy/integration-prompt.ts @@ -0,0 +1,82 @@ +/** + * OLD FLOW: Single monolithic integration prompt. + * + * Used when the `wizard-queued-workflow` feature flag is OFF. + * This prompt tells the agent to do everything in one shot: + * load skill menu → install skill → follow workflow files → set env vars. + * + * Delete this file once the queued workflow is the only path. + */ + +import { + DEFAULT_PACKAGE_INSTALLATION, + type FrameworkConfig, +} from '../framework-config.js'; +import { AgentSignals } from '../agent-interface.js'; + +export function buildIntegrationPrompt( + config: FrameworkConfig, + context: { + frameworkVersion: string; + typescript: boolean; + projectApiKey: string; + host: string; + projectId: number; + }, + frameworkContext: Record, +): string { + const additionalLines = config.prompts.getAdditionalContextLines + ? config.prompts.getAdditionalContextLines(frameworkContext) + : []; + + const additionalContext = + additionalLines.length > 0 + ? '\n' + additionalLines.map((line) => `- ${line}`).join('\n') + : ''; + + return `You have access to the PostHog MCP server which provides skills to integrate PostHog into this ${ + config.metadata.name + } project. + +Project context: +- PostHog Project ID: ${context.projectId} +- Framework: ${config.metadata.name} ${context.frameworkVersion} +- TypeScript: ${context.typescript ? 'Yes' : 'No'} +- PostHog public token: ${context.projectApiKey} +- PostHog Host: ${context.host} +- Project type: ${config.prompts.projectTypeDetection} +- Package installation: ${ + config.prompts.packageInstallation ?? DEFAULT_PACKAGE_INSTALLATION + }${additionalContext} + +Instructions (follow these steps IN ORDER - do not skip or reorder): + +STEP 1: Call load_skill_menu (from the wizard-tools MCP server) to see available skills. + If the tool fails, emit: ${ + AgentSignals.ERROR_MCP_MISSING + } Could not load skill menu and halt. + + Choose a skill from the \`integration\` category that matches this project's framework. Do NOT pick skills from other categories (llm-analytics, error-tracking, feature-flags, omnibus, etc.) — those are handled separately. + If no suitable integration skill is found, emit: ${ + AgentSignals.ERROR_RESOURCE_MISSING + } Could not find a suitable skill for this project. + +STEP 2: Call install_skill (from the wizard-tools MCP server) with the chosen skill ID (e.g., "integration-nextjs-app-router"). + Do NOT run any shell commands to install skills. + +STEP 3: Load the installed skill's SKILL.md file to understand what references are available. + +STEP 4: Follow the skill's workflow files in sequence. Look for numbered workflow files in the references (e.g., files with patterns like "1.0-", "1.1-", "1.2-"). Start with the first one and proceed through each step until completion. Each workflow file will tell you what to do and which file comes next. Never directly write PostHog tokens directly to code files; always use environment variables. + +STEP 5: Set up environment variables for PostHog using the wizard-tools MCP server (this runs locally — secret values never leave the machine): + - Use check_env_keys to see which keys already exist in the project's .env file (e.g. .env.local or .env). + - Use set_env_values to create or update the PostHog public token and host, using the appropriate environment variable naming convention for ${ + config.metadata.name + }, which you'll find in example code. The tool will also ensure .gitignore coverage. Don't assume the presence of keys means the value is up to date. Write the correct value each time. + - Reference these environment variables in the code files you create instead of hardcoding the public token and host. + +Important: Use the detect_package_manager tool (from the wizard-tools MCP server) to determine which package manager the project uses. Do not manually search for lockfiles or config files. Always install packages as a background task. Don't await completion; proceed with other work immediately after starting the installation. You must read a file immediately before attempting to write it, even if you have previously read it; failure to do so will cause a tool failure. + + +`; +} diff --git a/src/lib/legacy/single-query-runner.ts b/src/lib/legacy/single-query-runner.ts new file mode 100644 index 00000000..e271e1e6 --- /dev/null +++ b/src/lib/legacy/single-query-runner.ts @@ -0,0 +1,73 @@ +/** + * OLD FLOW: Single-query agent runner. + * + * Used when the `wizard-queued-workflow` feature flag is OFF. + * Sends one monolithic prompt that does everything in a single agent conversation. + * + * Delete this file once the queued workflow is the only path. + */ + +import { SPINNER_MESSAGE, type FrameworkConfig } from '../framework-config.js'; +import type { WizardSession } from '../wizard-session.js'; +import { runAgent } from '../agent-interface.js'; +import type { SpinnerHandle } from '../../ui/wizard-ui.js'; +import type { WizardOptions } from '../../utils/types.js'; +import { getUI } from '../../ui/index.js'; +import { buildIntegrationPrompt } from './integration-prompt'; + +/** + * OLD FLOW: Run the entire integration in a single agent query. + * + * Enqueues a single "Integration" queue item so the RunScreen + * displays tasks under it like the queued workflow does. + * + * Returns the agent result so the caller can handle errors uniformly. + */ +export async function runSingleQueryFlow(args: { + agent: Awaited< + ReturnType + >; + config: FrameworkConfig; + session: WizardSession; + options: WizardOptions; + spinner: SpinnerHandle; + promptContext: { + frameworkVersion: string; + typescript: boolean; + projectApiKey: string; + host: string; + projectId: number; + }; + frameworkContext: Record; + middleware?: Parameters[5]; +}) { + const integrationPrompt = buildIntegrationPrompt( + args.config, + args.promptContext, + args.frameworkContext, + ); + + // Set a single queue item so the RunScreen shows tasks nested under it + const queueItem = { id: 'integration', label: 'Integration' }; + getUI().setCurrentQueueItem(queueItem); + + const result = await runAgent( + args.agent, + integrationPrompt, + args.options, + args.spinner, + { + estimatedDurationMinutes: args.config.ui.estimatedDurationMinutes, + spinnerMessage: SPINNER_MESSAGE, + successMessage: args.config.ui.successMessage, + errorMessage: 'Integration failed', + additionalFeatureQueue: args.session.additionalFeatureQueue, + }, + args.middleware, + ); + + getUI().completeQueueItem(queueItem); + getUI().setCurrentQueueItem(null); + + return result; +} diff --git a/src/lib/queued-workflow-runner.ts b/src/lib/queued-workflow-runner.ts new file mode 100644 index 00000000..1150eb37 --- /dev/null +++ b/src/lib/queued-workflow-runner.ts @@ -0,0 +1,315 @@ +/** + * NEW FLOW: Queued workflow runner. + * + * Used when the `wizard-queued-workflow` feature flag is ON. + * Bootstrap → parse SKILL.md frontmatter → per-step continued queries. + * + * Delete the legacy/ folder once this is the only path. + */ + +import fs from 'fs'; +import path from 'path'; +import { + DEFAULT_PACKAGE_INSTALLATION, + type FrameworkConfig, +} from './framework-config'; +import type { WizardSession } from './wizard-session'; +import type { WizardOptions } from '../utils/types'; +import { getUI, type SpinnerHandle } from '../ui'; +import { initializeAgent, runAgent, AgentSignals } from './agent-interface'; +import { logToFile } from '../utils/debug'; +import { wizardAbort, WizardError } from '../utils/wizard-abort'; +import { + createPostBootstrapQueue, + parseWorkflowStepsFromSkillMd, + type WizardWorkflowQueueItem, +} from './workflow-queue'; + +const WIZARD_SKILL_ID_SIGNAL = '[WIZARD-SKILL-ID]'; + +export type PromptContext = { + frameworkVersion: string; + typescript: boolean; + projectApiKey: string; + host: string; + projectId: number; +}; + +export async function runQueuedWorkflow( + agent: Awaited>, + config: FrameworkConfig, + session: WizardSession, + options: WizardOptions, + promptContext: PromptContext, + frameworkContext: Record, + spinner: SpinnerHandle, + middleware?: Parameters[5], +): Promise>> { + // Step 1: Bootstrap — install the skill and get its ID + let agentResult = await runAgent( + agent, + buildBootstrapPrompt(config, promptContext, frameworkContext), + options, + spinner, + { + estimatedDurationMinutes: config.ui.estimatedDurationMinutes, + spinnerMessage: 'Preparing integration...', + successMessage: 'Integration prepared', + errorMessage: 'Integration failed during bootstrap', + additionalFeatureQueue: [], + requestRemark: false, + captureOutputText: true, + captureSessionId: true, + finalizeMiddleware: false, + }, + middleware, + ); + + const queuedSessionId = agentResult.sessionId; + const installedSkillId = + extractInstalledSkillId(agentResult.outputText ?? '') ?? undefined; + + if (!installedSkillId) { + await wizardAbort({ + message: + 'The wizard could not determine which integration skill was installed during bootstrap.', + error: new WizardError('Bootstrap step did not emit installed skill id'), + }); + } + + // Step 2: Read SKILL.md and seed the queue from its frontmatter + if (!agentResult.error && installedSkillId) { + const skillMdPath = path.join( + session.installDir, + '.claude', + 'skills', + installedSkillId, + 'SKILL.md', + ); + const skillMdContent = fs.readFileSync(skillMdPath, 'utf-8'); + const workflowSteps = parseWorkflowStepsFromSkillMd(skillMdContent); + + if (workflowSteps.length === 0) { + logToFile( + '[agent-runner] No workflow steps found in SKILL.md frontmatter, aborting', + ); + await wizardAbort({ + message: + 'The installed skill does not contain workflow steps in its metadata.', + error: new WizardError('No workflow steps in SKILL.md frontmatter'), + }); + } + + logToFile( + `[agent-runner] Seeded queue from SKILL.md: ${workflowSteps + .map((s) => s.stepId) + .join(', ')}`, + ); + + // 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, + promptContext, + installedSkillId, + ); + + agentResult = await runAgent( + agent, + prompt, + options, + spinner, + { + estimatedDurationMinutes: config.ui.estimatedDurationMinutes, + spinnerMessage: getQueueSpinnerMessage(queueItem), + successMessage: getQueueSuccessMessage(queueItem, config), + errorMessage: `Integration failed during ${queueItem.id}`, + additionalFeatureQueue: + queueItem.id === 'env-vars' ? session.additionalFeatureQueue : [], + resumeSessionId: queuedSessionId, + requestRemark: queueItem.id === 'env-vars', + captureOutputText: false, + captureSessionId: false, + finalizeMiddleware: queue.length === 0, + }, + middleware, + ); + + getUI().completeQueueItem({ id: queueItem.id, label: queueItem.label }); + + if (agentResult.error) { + break; + } + } + getUI().setCurrentQueueItem(null); + } + + return agentResult; +} + +export function extractInstalledSkillId(outputText: string): string | null { + const match = outputText.match( + new RegExp( + `${WIZARD_SKILL_ID_SIGNAL.replace( + /[.*+?^${}()|[\]\\]/g, + '\\$&', + )}\\s+([A-Za-z0-9._-]+)`, + ), + ); + return match?.[1] ?? null; +} + +// ── Prompt builders ───────────────────────────────────────────────── + +function buildQueuedPrompt( + queueItem: WizardWorkflowQueueItem, + config: FrameworkConfig, + context: PromptContext, + installedSkillId: string, +): string { + if (queueItem.kind === 'workflow') { + return buildWorkflowStepPrompt( + queueItem.referenceFilename, + installedSkillId, + ); + } + + return buildEnvVarPrompt(config, context); +} + +function buildProjectContextBlock( + config: FrameworkConfig, + context: PromptContext, + frameworkContext: Record, +): string { + const additionalLines = config.prompts.getAdditionalContextLines + ? config.prompts.getAdditionalContextLines(frameworkContext) + : []; + + const additionalContext = + additionalLines.length > 0 + ? '\n' + additionalLines.map((line) => `- ${line}`).join('\n') + : ''; + + return `Project context: +- PostHog Project ID: ${context.projectId} +- Framework: ${config.metadata.name} ${context.frameworkVersion} +- TypeScript: ${context.typescript ? 'Yes' : 'No'} +- PostHog public token: ${context.projectApiKey} +- PostHog Host: ${context.host} +- Project type: ${config.prompts.projectTypeDetection} +- Package installation: ${ + config.prompts.packageInstallation ?? DEFAULT_PACKAGE_INSTALLATION + }${additionalContext}`; +} + +function buildBootstrapPrompt( + config: FrameworkConfig, + context: PromptContext, + frameworkContext: Record, +): string { + return `You have access to the PostHog MCP server which provides skills to integrate PostHog into this ${ + config.metadata.name + } project. + +${buildProjectContextBlock(config, context, frameworkContext)} + +STEP 1: Call load_skill_menu (from the wizard-tools MCP server) to see available skills. + If the tool fails, emit: ${ + AgentSignals.ERROR_MCP_MISSING + } Could not load skill menu and halt. + + Choose a skill from the \`integration\` category that matches this project's framework. Do NOT pick skills from other categories (llm-analytics, error-tracking, feature-flags, omnibus, etc.) — those are handled separately. + If no suitable integration skill is found, emit: ${ + AgentSignals.ERROR_RESOURCE_MISSING + } Could not find a suitable skill for this project. + +STEP 2: Call install_skill (from the wizard-tools MCP server) with the chosen skill ID (e.g., "integration-nextjs-app-router"). + Do NOT run any shell commands to install skills. + +STEP 3: Load the installed skill's SKILL.md file to understand what references are available. + +STEP 4: When preparation is complete, emit exactly one line in this format: +${WIZARD_SKILL_ID_SIGNAL} + +Important: +- Do NOT execute any of the workflow reference files yet. +- Do NOT set up environment variables yet. +- Stop after preparation is complete. +- Use the detect_package_manager tool (from the wizard-tools MCP server) to determine which package manager the project uses. Do not manually search for lockfiles or config files. Always install packages as a background task. Don't await completion; proceed with other work immediately after starting the installation. You must read a file immediately before attempting to write it, even if you have previously read it; failure to do so will cause a tool failure. + +`; +} + +function buildWorkflowStepPrompt( + referenceFilename: string, + installedSkillId: string, +): string { + return `Continue the existing conversation. + +Read and follow this workflow reference: +\`.claude/skills/${installedSkillId}/references/${referenceFilename}\` + +Before starting work, use TodoWrite to create your task plan. Update it as you complete each task. + +Important: +- Complete only this workflow step. +- Do NOT continue to any other workflow file. +- Do NOT set up environment variables yet. +- Stop when this step is complete.`; +} + +function buildEnvVarPrompt( + config: FrameworkConfig, + context: PromptContext, +): string { + return `Continue the existing conversation. + +Execute the final queued environment-variable setup step for this ${ + config.metadata.name + } project. + +${buildProjectContextBlock(config, context, {})} + +Set up environment variables for PostHog using the wizard-tools MCP server (this runs locally — secret values never leave the machine): +- Use check_env_keys to see which keys already exist in the project's .env file (e.g. .env.local or .env). +- Use set_env_values to create or update the PostHog public token and host, using the appropriate environment variable naming convention for ${ + config.metadata.name + }, which you'll find in example code. The tool will also ensure .gitignore coverage. Don't assume the presence of keys means the value is up to date. Write the correct value each time. +- Reference these environment variables in the code files you create instead of hardcoding the public token and host. + +Stop after the environment-variable setup step is complete.`; +} + +function getQueueSpinnerMessage(queueItem: WizardWorkflowQueueItem): string { + switch (queueItem.kind) { + case 'bootstrap': + return 'Preparing integration...'; + case 'workflow': + return `Running step ${queueItem.id.replace('workflow:', '')}...`; + case 'env-vars': + return 'Finalizing environment variables...'; + } +} + +function getQueueSuccessMessage( + queueItem: WizardWorkflowQueueItem, + config: FrameworkConfig, +): string { + switch (queueItem.kind) { + case 'bootstrap': + return 'Integration prepared'; + case 'workflow': + return `Step ${queueItem.id.replace('workflow:', '')} complete`; + case 'env-vars': + return config.ui.successMessage; + } +} diff --git a/src/lib/wizard-tools.ts b/src/lib/wizard-tools.ts index 206c0183..e62ef2ad 100644 --- a/src/lib/wizard-tools.ts +++ b/src/lib/wizard-tools.ts @@ -30,7 +30,12 @@ async function getSDKModule(): Promise { // Skill types // --------------------------------------------------------------------------- -export type SkillEntry = { id: string; name: string; downloadUrl: string }; +export type SkillEntry = { + id: string; + name: string; + downloadUrl: string; + downloadUrlV2?: string; +}; export interface SkillMenu { categories: Record; @@ -70,22 +75,27 @@ export async function fetchSkillMenu( /** * Download and extract a skill. - * By default installs to `/.claude/skills//`. - * Pass `skillsRoot` to override the base directory (e.g. `.posthog/skills`). + * Installs to `/.claude/skills//` by default, or `///` if specified. + * When `useV2` is true, downloads the v2 variant (workflow frontmatter) if available. */ export function downloadSkill( skillEntry: SkillEntry, installDir: string, skillsRoot?: string, + useV2 = false, ): { success: boolean; error?: string } { const skillDir = skillsRoot ? path.join(installDir, skillsRoot, skillEntry.id) : path.join(installDir, '.claude', 'skills', skillEntry.id); const tmpFile = `/tmp/posthog-skill-${skillEntry.id}.zip`; + const url = + useV2 && skillEntry.downloadUrlV2 + ? skillEntry.downloadUrlV2 + : skillEntry.downloadUrl; try { fs.mkdirSync(skillDir, { recursive: true }); - execFileSync('curl', ['-sL', skillEntry.downloadUrl, '-o', tmpFile], { + execFileSync('curl', ['-sL', url, '-o', tmpFile], { timeout: 30000, }); execFileSync('unzip', ['-o', tmpFile, '-d', skillDir], { @@ -97,9 +107,7 @@ export function downloadSkill( /* ignore cleanup errors */ } - logToFile( - `downloadSkill: installed ${skillEntry.id} from ${skillEntry.downloadUrl}`, - ); + logToFile(`downloadSkill: installed ${skillEntry.id} from ${url}`); return { success: true }; } catch (err: any) { logToFile(`downloadSkill: error: ${err.message}`); @@ -120,6 +128,9 @@ export interface WizardToolsOptions { /** Base URL for the skills server (e.g. http://localhost:8765 or GitHub releases URL) */ skillsBaseUrl: string; + + /** When true, download v2 skills (workflow frontmatter) instead of v1 (continuation links). */ + useV2Skills?: boolean; } // --------------------------------------------------------------------------- @@ -229,7 +240,12 @@ const SERVER_NAME = 'wizard-tools'; * Must be called asynchronously because the SDK is an ESM module loaded via dynamic import. */ export async function createWizardToolsServer(options: WizardToolsOptions) { - const { workingDirectory, detectPackageManager, skillsBaseUrl } = options; + const { + workingDirectory, + detectPackageManager, + skillsBaseUrl, + useV2Skills = false, + } = options; const sdk = await getSDKModule(); const { tool, createSdkMcpServer } = sdk; @@ -448,7 +464,12 @@ export async function createWizardToolsServer(options: WizardToolsOptions) { }; } - const result = downloadSkill(skill, workingDirectory); + const result = downloadSkill( + skill, + workingDirectory, + undefined, + useV2Skills, + ); if (result.success) { return { content: [ diff --git a/src/lib/workflow-queue.ts b/src/lib/workflow-queue.ts index e3b47e29..a453065a 100644 --- a/src/lib/workflow-queue.ts +++ b/src/lib/workflow-queue.ts @@ -99,28 +99,6 @@ export function parseWorkflowStepsFromSkillMd( return steps; } -/** - * Build the initial queue from an ordered list of workflow steps. - * The queue is always: bootstrap → workflow steps → env-vars. - */ -export function createInitialWizardWorkflowQueue( - steps: WorkflowStepSeed[], -): WizardWorkflowQueue { - const items: WizardWorkflowQueueItem[] = [ - { 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', label: 'Environment variables' }, - ]; - return new WizardWorkflowQueue(items); -} - /** * Build a queue with only workflow steps + env-vars (no bootstrap). * Used after bootstrap has already run and SKILL.md has been parsed. @@ -130,7 +108,7 @@ export function createPostBootstrapQueue( ): WizardWorkflowQueue { const items: WizardWorkflowQueueItem[] = [ ...steps.map( - (step): WizardWorkflowQueueItem => ({ + (step, _): WizardWorkflowQueueItem => ({ id: `workflow:${step.stepId}`, kind: 'workflow', referenceFilename: step.referenceFilename, diff --git a/src/ui/logging-ui.ts b/src/ui/logging-ui.ts index 574c66f5..b7aa746f 100644 --- a/src/ui/logging-ui.ts +++ b/src/ui/logging-ui.ts @@ -4,9 +4,9 @@ * No prompts, no TUI, no interactivity. Just console output. */ -import { TaskStatus, type WizardUI, type SpinnerHandle } from './wizard-ui'; -import type { SettingsConflict } from '../lib/agent-interface'; -import type { WizardWorkflowQueue } from '../lib/workflow-queue'; +import { TaskStatus, type WizardUI, type SpinnerHandle } from './wizard-ui.js'; +import type { SettingsConflict } from '../lib/agent-interface.js'; +import type { WizardWorkflowQueue } from '../lib/workflow-queue.js'; export class LoggingUI implements WizardUI { intro(message: string): void { diff --git a/src/ui/tui/primitives/ProgressList.tsx b/src/ui/tui/primitives/ProgressList.tsx index 40355d2c..dfe30d89 100644 --- a/src/ui/tui/primitives/ProgressList.tsx +++ b/src/ui/tui/primitives/ProgressList.tsx @@ -23,8 +23,12 @@ interface ProgressListProps { } export const ProgressList = ({ items, title }: ProgressListProps) => { - const completed = items.filter((t) => t.status === 'completed').length; - const total = items.length; + // Only count top-level items (not indented sub-tasks) for progress + const topLevel = items.filter((t) => !t.indent); + const completed = topLevel.filter( + (t) => t.status === TaskStatus.Completed, + ).length; + const total = topLevel.length; return ( @@ -69,7 +73,7 @@ export const ProgressList = ({ items, title }: ProgressListProps) => { {completed < total ? `Progress: ${completed}/${total} completed` - : 'Cleaning up...'} + : 'Thinking about next steps...'} )} diff --git a/src/ui/tui/screens/RunScreen.tsx b/src/ui/tui/screens/RunScreen.tsx index b0811dfd..2410ce3d 100644 --- a/src/ui/tui/screens/RunScreen.tsx +++ b/src/ui/tui/screens/RunScreen.tsx @@ -41,53 +41,23 @@ export const RunScreen = ({ store }: RunScreenProps) => { const [columns] = useStdoutDimensions(); - // Build stage-grouped progress items - const progressItems: ProgressItem[] = []; - const current = store.currentQueueItem; - const completed = store.completedQueueItems; - const pendingQueue = store.workQueue?.toArray() ?? []; + const progressItems: ProgressItem[] = store.tasks.map((t) => ({ + label: t.label, + activeForm: t.activeForm, + status: t.status, + })); - // 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 + // When all tasks are done but the queue has features, show a transitional item const featureQueue = store.session.additionalFeatureQueue; - for (const feature of featureQueue) { - const nextLabel = ADDITIONAL_FEATURE_LABELS[feature]; + const allDone = + progressItems.length > 0 && + progressItems.every((t) => t.status === TaskStatus.Completed); + if (allDone && featureQueue.length > 0) { + const nextLabel = ADDITIONAL_FEATURE_LABELS[featureQueue[0]]; progressItems.push({ label: `Set up ${nextLabel}`, - status: TaskStatus.Pending, + activeForm: `Setting up ${nextLabel}...`, + status: TaskStatus.InProgress, }); } @@ -99,7 +69,9 @@ export const RunScreen = ({ store }: RunScreenProps) => { ) : ( store.setLearnCardComplete()} /> ); - const progressList = ; + const progressList = ( + + ); // On narrow terminals, drop the learn pane and show only progress const statusComponent = diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts index d5d95584..6f690d8d 100644 --- a/src/ui/tui/store.ts +++ b/src/ui/tui/store.ts @@ -183,9 +183,6 @@ export class WizardStore { 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(); } diff --git a/src/ui/wizard-ui.ts b/src/ui/wizard-ui.ts index 8a8beafc..85ea6c49 100644 --- a/src/ui/wizard-ui.ts +++ b/src/ui/wizard-ui.ts @@ -8,8 +8,8 @@ * Session-mutating methods trigger reactive screen resolution in the TUI. */ -import type { SettingsConflict } from '../lib/agent-interface'; -import type { WizardWorkflowQueue } from '../lib/workflow-queue'; +import type { SettingsConflict } from '../lib/agent-interface.js'; +import type { WizardWorkflowQueue } from '../lib/workflow-queue.js'; export enum TaskStatus { Pending = 'pending',