# lda Report Demo Panel Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a working console demo panel that operates an already-prepared `lda_report_case_study.default` deployment through start, typed interrupt review, resume, trace, and final output inspection. **Architecture:** The Python JSON-RPC server already exposes `workflow.runs.start` and `workflow.runs.resume`; this plan only brings those methods into the web RPC package and adds a React panel that uses the public console backend. The panel does not create artifacts, save deployments, or run hidden setup commands. If the prepared deployment is missing, it shows explicit setup instructions and stops. **Tech Stack:** TypeScript, React 19, Valibot, Effect RPC schemas in `@lda/workflow-rpc`, existing console `/api/rpc` operation bridge, existing Python `wf-rpc-server`. ## Global Constraints - Do not add workflow authoring/setup buttons to the UI in this slice. - The prepared deployment id is exactly `lda_report_case_study.default`. - The demo start input is the checked-in example shape: selected documents plus `board_path: "issue-board.json"`. - The expected interrupt kind is exactly `issue_review`. - The resume form supports the two real outcomes: `submitted` and `cancelled`. - Keep all demo-specific UI under `web/apps/console/src/demo/`. - Preserve raw request/response evidence by going through `callOperation()` only; do not call `fetch()` directly from the demo panel. - Before implementation, keep or commit the current graph-console fixes; this plan assumes the console already has a readable graph view. --- ## File Structure - Modify `web/packages/rpc/src/rpcs.ts` - Add schemas/RPC definitions for `workflow.runs.start` and `workflow.runs.resume`. - Extend the run interrupt schema with `typed`, `request_schema`, and `resume_schema`. - Modify `web/packages/rpc/src/service.ts` - Allow and dispatch the two new run operations. - Modify `web/packages/rpc/src/method-registry.ts` - Add metadata, CLI strings, and interpreted DTOs for start/resume. - Modify `web/packages/rpc/src/index.ts` - Export the new schemas/RPC definitions if needed by tests. - Modify `web/packages/rpc/src/service.test.ts` - Add operation cases for start/resume and assert typed interrupt fields survive interpretation. - Modify `web/apps/console/src/connection/contracts.ts` - Add the two operation literals so the browser can call them. - Create `web/apps/console/src/demo/ldaReportDemoConfig.ts` - Constants for deployment id, default input, setup commands, and expected interrupt kind. - Create `web/apps/console/src/demo/ldaReportDemoModels.ts` - Valibot decoders for the demo-specific interrupt payload and final output. - Create `web/apps/console/src/demo/useLdaReportDemo.ts` - Hook that drives deployment discovery, run start, resume, inspect, and trace. - Create `web/apps/console/src/demo/LdaReportDemoPanel.tsx` - Working UI for demo status, start button, interrupt payload, resume form, final output, trace summary, and missing-demo instructions. - Create `web/apps/console/src/demo/useLdaReportDemo.test.tsx` - Hook tests for missing deployment, interrupted start, submitted resume, and cancelled resume. - Create `web/apps/console/src/demo/LdaReportDemoPanel.test.tsx` - Component tests for the main user paths. - Modify `web/apps/console/src/app/App.tsx` - Mount the demo panel after connection, above or beside the generic lifecycle explorer. - Modify `web/apps/console/src/app/App.test.tsx` - Assert the demo panel mounts after connect and does not break existing source/lifecycle loading. - Modify `web/apps/console/src/styles/global.css` - Add minimal demo panel styles; prioritize clarity over visual polish. - Modify `web/README.md` - Add the manual demo run instructions. - Modify `docs/current_roadmap.md` - Mark this demo panel as completed only after implementation and verification. --- ### Task 1: Add Web RPC Support For Run Start And Resume **Files:** - Modify: `web/packages/rpc/src/rpcs.ts` - Modify: `web/packages/rpc/src/service.ts` - Modify: `web/packages/rpc/src/method-registry.ts` - Modify: `web/packages/rpc/src/index.ts` - Modify: `web/packages/rpc/src/service.test.ts` - Modify: `web/apps/console/src/connection/contracts.ts` **Interfaces:** - Consumes: Python JSON-RPC methods `workflow.runs.start` and `workflow.runs.resume`. - Produces: - Operation names accepted by `callOperation()`. - Interpreted start/resume results shaped like existing `RunDetail`, with camelCase `nextActions`, interrupt contract fields, and no trace frames in inspect/start/resume payloads. - [ ] **Step 1: Add failing service tests for start and resume** In `web/packages/rpc/src/service.test.ts`, extend `lifecycleCases` with these two cases: ```ts { operation: "workflow.runs.start" as const, params: { deployment_id: "lda_report_case_study.default", workflow_input: { selected_documents: [ "project-brief.md", "architecture-notes.md", "evaluation-findings.md", "risk-register.md", "roadmap.md", ], board_path: "issue-board.json", }, trace_range: { start: 0, limit: 50 }, }, result: { run_id: "run_demo", deployment_id: "lda_report_case_study.default", artifact_id: "lda_report_case_study", artifact_version: 1, status: "interrupted", resume_readiness: "ready", interrupt: { kind: "issue_review", payload: { report_markdown: "# lda.chat Thesis And Project Readiness Report", proposed_issues: [ { id: "demo-issue-1", title: "Prepare demo script", body: "Write the defense walkthrough.", severity: "medium", }, ], }, outcomes: ["submitted", "cancelled"], request_schema: { type: "object", required: ["report_markdown", "proposed_issues"], }, resume_schema: { type: "object", required: ["approved", "selected_issue_ids"], }, typed: true, }, outcome: null, error: null, output: null, diagnostics: [], trace_count: 1, next_actions: { can_continue: true, can_save_now: null, recommended_next_tool: "wf.workflow.resume_run", reason: "run is interrupted", patch_examples: [], warnings: [], }, }, }, { operation: "workflow.runs.resume" as const, params: { run_id: "run_demo", resume_payload: { approved: true, selected_issue_ids: ["demo-issue-1"], comment: "Create selected issues.", }, resume_outcome: "submitted", trace_range: { start: 0, limit: 50 }, }, result: { run_id: "run_demo", deployment_id: "lda_report_case_study.default", artifact_id: "lda_report_case_study", artifact_version: 1, status: "completed", resume_readiness: "not_applicable", interrupt: null, outcome: "completed", error: null, output: { approved: true, markdown: "# lda.chat Thesis And Project Readiness Report", created_issues: [ { id: "ISSUE-001", title: "Prepare demo script", url: "local://issues/ISSUE-001", }, ], selected_issue_ids: ["demo-issue-1"], comment: "Create selected issues.", }, diagnostics: [], trace_count: 4, next_actions: { can_continue: false, can_save_now: null, recommended_next_tool: null, reason: "Run completed.", patch_examples: [], warnings: [], }, }, } ``` Add this assertion after the generic lifecycle loop: ```ts it("interprets typed interrupt contracts from run start", async () => { const startCase = lifecycleCases.find( (testCase) => testCase.operation === "workflow.runs.start", ); expect(startCase).toBeDefined(); if (!startCase) return; const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: startCase.result, }); }; const exchange = await runOperation( { fetch }, startCase.operation as "workflow.health" | "workflow.sources.list", startCase.params, ); expect(exchange.interpreted).toMatchObject({ runId: "run_demo", status: "interrupted", interrupt: { kind: "issue_review", typed: true, outcomes: ["submitted", "cancelled"], }, nextActions: { canContinue: true, }, }); }); ``` - [ ] **Step 2: Run the failing tests** Run: ```powershell pnpm --dir web --filter @lda/workflow-rpc test -- src/service.test.ts ``` Expected: FAIL because `workflow.runs.start` and `workflow.runs.resume` are not accepted operation names in the web package. - [ ] **Step 3: Add Effect RPC schemas** In `web/packages/rpc/src/rpcs.ts`, add: ```ts export const WorkflowRunsStartPayloadSchema = Schema.Struct({ deployment_id: Schema.String, workflow_input: JsonObjectSchema, trace_range: Schema.optional(Schema.NullOr(TraceRangeSchema)), }); export const WorkflowRunsResumePayloadSchema = Schema.Struct({ run_id: Schema.String, resume_payload: JsonObjectSchema, resume_outcome: Schema.optional(Schema.String), trace_range: Schema.optional(Schema.NullOr(TraceRangeSchema)), }); ``` Replace `RunInterruptSchema` with: ```ts const RunInterruptSchema = Schema.Struct({ kind: Schema.String, payload: JsonObjectSchema, outcomes: Schema.Array(Schema.String), request_schema: Schema.optional(JsonObjectSchema), resume_schema: Schema.optional(JsonObjectSchema), typed: Schema.optional(Schema.Boolean), }); ``` After `WorkflowRunsInspect`, add: ```ts export const WorkflowRunsStart = Rpc.make("workflow.runs.start", { payload: WorkflowRunsStartPayloadSchema, success: WorkflowRunsInspectResultSchema, error: Schema.Never, }); export const WorkflowRunsResume = Rpc.make("workflow.runs.resume", { payload: WorkflowRunsResumePayloadSchema, success: WorkflowRunsInspectResultSchema, error: Schema.Never, }); ``` Add both RPCs to `WorkflowRpcs`. - [ ] **Step 4: Dispatch the operations** In `web/packages/rpc/src/service.ts`, extend the operation union and `isKnownOperation`: ```ts | "workflow.runs.start" | "workflow.runs.resume" ``` Import the two new payload schemas and add switch cases: ```ts case "workflow.runs.start": { const payload = yield* decodeParams( WorkflowRunsStartPayloadSchema, params, ); return yield* client.workflow["runs.start"](payload); } case "workflow.runs.resume": { const payload = yield* decodeParams( WorkflowRunsResumePayloadSchema, params, ); return yield* client.workflow["runs.resume"](payload); } ``` - [ ] **Step 5: Add operation metadata** In `web/packages/rpc/src/method-registry.ts`, add a helper: ```ts const interpretRunDetail = (decoded: { readonly run_id: string; readonly deployment_id: string; readonly artifact_id: string; readonly artifact_version: number; readonly status: string; readonly resume_readiness: string; readonly interrupt: unknown; readonly outcome: string | null; readonly error: string | null; readonly output: Record | null; readonly diagnostics: ReadonlyArray; readonly trace_count: number; readonly next_actions: Parameters[0]; }) => ({ runId: decoded.run_id, deploymentId: decoded.deployment_id, artifactId: decoded.artifact_id, artifactVersion: decoded.artifact_version, status: decoded.status, resumeReadiness: decoded.resume_readiness, interrupt: decoded.interrupt, outcome: decoded.outcome, error: decoded.error, output: decoded.output, diagnostics: decoded.diagnostics, traceCount: decoded.trace_count, nextActions: interpretNextActions(decoded.next_actions), }); ``` Use it for existing `workflow.runs.inspect`, then add entries: ```ts { method: "workflow.runs.start", label: "Start run", explanation: "Start a workflow deployment run", idempotency: "write", equivalentCli: (params) => { const p = Schema.decodeUnknownSync(WorkflowRunsStartPayloadSchema)( params, { onExcessProperty: "error" }, ); return `uv run wf run start ${p.deployment_id} --input ''`; }, interpret: (result) => { const decoded = Schema.decodeUnknownSync(WorkflowRunsInspectResultSchema)( result, { onExcessProperty: "ignore" }, ); return interpretRunDetail(decoded); }, }, { method: "workflow.runs.resume", label: "Resume run", explanation: "Resume an interrupted workflow run", idempotency: "write", equivalentCli: (params) => { const p = Schema.decodeUnknownSync(WorkflowRunsResumePayloadSchema)( params, { onExcessProperty: "error" }, ); return `uv run wf run resume ${p.run_id} --payload ''`; }, interpret: (result) => { const decoded = Schema.decodeUnknownSync(WorkflowRunsInspectResultSchema)( result, { onExcessProperty: "ignore" }, ); return interpretRunDetail(decoded); }, }, ``` - [ ] **Step 6: Add browser operation literals** In `web/apps/console/src/connection/contracts.ts`, add: ```ts v.literal("workflow.runs.start"), v.literal("workflow.runs.resume"), ``` - [ ] **Step 7: Run tests** Run: ```powershell pnpm --dir web --filter @lda/workflow-rpc test -- src/service.test.ts pnpm --dir web --filter @lda/workflow-rpc typecheck pnpm --dir web --filter @lda/console typecheck ``` Expected: all pass. - [ ] **Step 8: Commit** ```powershell git add web/packages/rpc/src/rpcs.ts web/packages/rpc/src/service.ts web/packages/rpc/src/method-registry.ts web/packages/rpc/src/index.ts web/packages/rpc/src/service.test.ts web/apps/console/src/connection/contracts.ts git commit -m "feat: expose run start and resume in console rpc" ``` --- ### Task 2: Add Demo Hook And Models **Files:** - Create: `web/apps/console/src/demo/ldaReportDemoConfig.ts` - Create: `web/apps/console/src/demo/ldaReportDemoModels.ts` - Create: `web/apps/console/src/demo/useLdaReportDemo.ts` - Create: `web/apps/console/src/demo/useLdaReportDemo.test.tsx` **Interfaces:** - Consumes: `callOperation(operation, target, params)` and Task 1 operation names. - Produces: - `useLdaReportDemo(target, recordEvidence)` hook. - Controller with `refresh`, `startRun`, `submitSelectedIssues`, and `cancelReview`. - [ ] **Step 1: Create demo constants** Create `web/apps/console/src/demo/ldaReportDemoConfig.ts`: ```ts export const LDA_REPORT_DEPLOYMENT_ID = "lda_report_case_study.default"; export const LDA_REPORT_INTERRUPT_KIND = "issue_review"; export const ldaReportDemoInput = { selected_documents: [ "project-brief.md", "architecture-notes.md", "evaluation-findings.md", "risk-register.md", "roadmap.md", ], board_path: "issue-board.json", } as const; export const ldaReportSetupCommands = [ "uv run wf-rpc-server --config examples/lda_report_workflow/wf.config.json --host 127.0.0.1 --port 8765", "uv run wf --config examples/lda_report_workflow/wf.config.json --local artifact create-from-plan examples/lda_report_workflow/workflow.plan.json --artifact lda_report_case_study --version 1 --title \"lda.chat Report Case Study\" --outcome completed --outcome cancelled --binding local.lda_docs=local.lda_docs --binding local.lda_report=local.lda_report --binding local.issue_board=local.issue_board", "uv run wf --config examples/lda_report_workflow/wf.config.json --local deploy save lda_report_case_study.default --artifact lda_report_case_study --version 1 --binding local.lda_docs=local.lda_docs --binding local.lda_report=local.lda_report --binding local.issue_board=local.issue_board", ] as const; ``` - [ ] **Step 2: Create demo-specific decoders** Create `web/apps/console/src/demo/ldaReportDemoModels.ts`: ```ts import * as v from "valibot"; export const ProposedIssueSchema = v.object({ id: v.string(), title: v.string(), body: v.string(), severity: v.optional(v.string(), "medium"), }); export const CreatedIssueSchema = v.object({ id: v.string(), title: v.string(), url: v.string(), }); export const LdaReportInterruptPayloadSchema = v.object({ report_markdown: v.string(), proposed_issues: v.array(ProposedIssueSchema), }); export const LdaReportOutputSchema = v.object({ approved: v.boolean(), markdown: v.string(), created_issues: v.array(CreatedIssueSchema), selected_issue_ids: v.array(v.string()), comment: v.nullish(v.string(), null), }); export type ProposedIssue = v.InferOutput; export type LdaReportInterruptPayload = v.InferOutput; export type LdaReportOutput = v.InferOutput; export const parseLdaReportInterruptPayload = (value: unknown): LdaReportInterruptPayload => v.parse(LdaReportInterruptPayloadSchema, value); export const parseLdaReportOutput = (value: unknown): LdaReportOutput => v.parse(LdaReportOutputSchema, value); ``` - [ ] **Step 3: Write hook tests first** Create `web/apps/console/src/demo/useLdaReportDemo.test.tsx`. Test the missing deployment path: ```ts import { renderHook, act, waitFor } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { useLdaReportDemo } from "./useLdaReportDemo.js"; import { callOperation } from "../connection/api.js"; vi.mock("../connection/api.js", () => ({ callOperation: vi.fn(), })); const mockedCallOperation = vi.mocked(callOperation); beforeEach(() => { mockedCallOperation.mockReset(); }); describe("useLdaReportDemo", () => { it("reports missing deployment without trying to create it", async () => { mockedCallOperation.mockResolvedValue({ ok: false, error: { code: "rpc_remote_error", message: "not found" }, exchange: { request: {}, response: {} }, }); const { result } = renderHook(() => useLdaReportDemo("http://127.0.0.1:8765/rpc", vi.fn()), ); await waitFor(() => { expect(result.current.state.phase).toBe("missing"); }); expect(mockedCallOperation).toHaveBeenCalledWith( "workflow.deployments.inspect", "http://127.0.0.1:8765/rpc", { deployment_id: "lda_report_case_study.default" }, ); expect(mockedCallOperation).not.toHaveBeenCalledWith( "workflow.runs.start", expect.anything(), expect.anything(), ); }); }); ``` Add tests for start/resume after the hook implementation in Step 5. - [ ] **Step 4: Run the failing hook test** Run: ```powershell pnpm --dir web --filter @lda/console test -- src/demo/useLdaReportDemo.test.tsx ``` Expected: FAIL because `useLdaReportDemo.ts` does not exist. - [ ] **Step 5: Implement the hook** Create `web/apps/console/src/demo/useLdaReportDemo.ts`: ```ts import { useCallback, useEffect, useReducer } from "react"; import { callOperation } from "../connection/api.js"; import type { EvidenceRecord } from "../app/state.js"; import { LDA_REPORT_DEPLOYMENT_ID, LDA_REPORT_INTERRUPT_KIND, ldaReportDemoInput, } from "./ldaReportDemoConfig.js"; import { parseLdaReportInterruptPayload, parseLdaReportOutput, type LdaReportInterruptPayload, type LdaReportOutput, } from "./ldaReportDemoModels.js"; import { decodeRunDetail, decodeTracePage, type TracePage } from "../lifecycle/models.js"; type DemoPhase = | "idle" | "checking" | "missing" | "ready" | "starting" | "interrupted" | "resuming" | "completed" | "error"; type DemoState = { readonly phase: DemoPhase; readonly message: string | null; readonly runId: string | null; readonly interruptPayload: LdaReportInterruptPayload | null; readonly output: LdaReportOutput | null; readonly trace: TracePage | null; }; const initialState: DemoState = { phase: "idle", message: null, runId: null, interruptPayload: null, output: null, trace: null, }; type DemoAction = | { readonly type: "checking" } | { readonly type: "missing"; readonly message: string } | { readonly type: "ready" } | { readonly type: "starting" } | { readonly type: "interrupted"; readonly runId: string; readonly payload: LdaReportInterruptPayload } | { readonly type: "resuming" } | { readonly type: "completed"; readonly output: LdaReportOutput; readonly trace: TracePage | null } | { readonly type: "error"; readonly message: string }; const reducer = (state: DemoState, action: DemoAction): DemoState => { switch (action.type) { case "checking": return { ...initialState, phase: "checking" }; case "missing": return { ...initialState, phase: "missing", message: action.message }; case "ready": return { ...initialState, phase: "ready" }; case "starting": return { ...state, phase: "starting", message: null }; case "interrupted": return { ...state, phase: "interrupted", runId: action.runId, interruptPayload: action.payload, output: null, trace: null, }; case "resuming": return { ...state, phase: "resuming", message: null }; case "completed": return { ...state, phase: "completed", output: action.output, trace: action.trace }; case "error": return { ...state, phase: "error", message: action.message }; default: return state; } }; type EvidenceRecorder = (record: EvidenceRecord) => void; const recordOperationEvidence = ( recordEvidence: EvidenceRecorder, result: Awaited>, ) => { if (!result.ok) return; recordEvidence({ id: `demo-${result.operation}-${Date.now()}`, operation: result.operation, label: result.label, equivalentCli: result.equivalentCli, request: result.exchange.request, response: result.exchange.response, durationMs: result.durationMs, }); }; export const useLdaReportDemo = ( target: string | null, recordEvidence: EvidenceRecorder, ) => { const [state, dispatch] = useReducer(reducer, initialState); const refresh = useCallback(async () => { if (!target) return; dispatch({ type: "checking" }); const result = await callOperation( "workflow.deployments.inspect", target, { deployment_id: LDA_REPORT_DEPLOYMENT_ID }, ); recordOperationEvidence(recordEvidence, result); if (!result.ok) { dispatch({ type: "missing", message: result.error.message }); return; } dispatch({ type: "ready" }); }, [target, recordEvidence]); useEffect(() => { void refresh(); }, [refresh]); const startRun = useCallback(async () => { if (!target) return; dispatch({ type: "starting" }); const result = await callOperation( "workflow.runs.start", target, { deployment_id: LDA_REPORT_DEPLOYMENT_ID, workflow_input: ldaReportDemoInput, trace_range: { start: 0, limit: 50 }, }, ); recordOperationEvidence(recordEvidence, result); if (!result.ok) { dispatch({ type: "error", message: result.error.message }); return; } const detail = decodeRunDetail(result.interpreted); if (detail.status !== "interrupted" || detail.interrupt?.kind !== LDA_REPORT_INTERRUPT_KIND) { dispatch({ type: "error", message: "Demo run did not stop at issue_review interrupt." }); return; } dispatch({ type: "interrupted", runId: detail.runId, payload: parseLdaReportInterruptPayload(detail.interrupt.payload), }); }, [target, recordEvidence]); const resume = useCallback(async ( resumePayload: { approved: boolean; selected_issue_ids: string[]; comment: string }, resumeOutcome: "submitted" | "cancelled", ) => { if (!target || !state.runId) return; dispatch({ type: "resuming" }); const result = await callOperation( "workflow.runs.resume", target, { run_id: state.runId, resume_payload: resumePayload, resume_outcome: resumeOutcome, trace_range: { start: 0, limit: 50 }, }, ); recordOperationEvidence(recordEvidence, result); if (!result.ok) { dispatch({ type: "error", message: result.error.message }); return; } const detail = decodeRunDetail(result.interpreted); const traceResult = await callOperation( "workflow.runs.trace", target, { run_id: detail.runId, trace_range: { start: 0, limit: 50 } }, ); recordOperationEvidence(recordEvidence, traceResult); const trace = traceResult.ok ? decodeTracePage(traceResult.interpreted) : null; dispatch({ type: "completed", output: parseLdaReportOutput(detail.output), trace, }); }, [target, state.runId, recordEvidence]); return { state, refresh, startRun, submitSelectedIssues: (selectedIssueIds: string[], comment: string) => resume( { approved: true, selected_issue_ids: selectedIssueIds, comment }, "submitted", ), cancelReview: (comment: string) => resume( { approved: false, selected_issue_ids: [], comment }, "cancelled", ), }; }; ``` - [ ] **Step 6: Add hook tests for interrupted start and resume** In `useLdaReportDemo.test.tsx`, add tests that mock: - `workflow.deployments.inspect` -> ok. - `workflow.runs.start` -> interrupted result with issue payload. - `workflow.runs.resume` -> completed result with final output. - `workflow.runs.trace` -> trace page. Assert: ```ts expect(result.current.state.phase).toBe("interrupted"); expect(result.current.state.interruptPayload?.proposed_issues[0]?.id).toBe("demo-issue-1"); ``` After `submitSelectedIssues(["demo-issue-1"], "Create it.")`, assert: ```ts expect(result.current.state.phase).toBe("completed"); expect(result.current.state.output?.created_issues[0]?.id).toBe("ISSUE-001"); expect(result.current.state.trace?.frames.length).toBeGreaterThan(0); ``` - [ ] **Step 7: Run hook tests** Run: ```powershell pnpm --dir web --filter @lda/console test -- src/demo/useLdaReportDemo.test.tsx ``` Expected: all pass. - [ ] **Step 8: Commit** ```powershell git add web/apps/console/src/demo/ldaReportDemoConfig.ts web/apps/console/src/demo/ldaReportDemoModels.ts web/apps/console/src/demo/useLdaReportDemo.ts web/apps/console/src/demo/useLdaReportDemo.test.tsx git commit -m "feat: add lda report demo workflow controller" ``` --- ### Task 3: Add Working Demo Panel UI **Files:** - Create: `web/apps/console/src/demo/LdaReportDemoPanel.tsx` - Create: `web/apps/console/src/demo/LdaReportDemoPanel.test.tsx` - Modify: `web/apps/console/src/app/App.tsx` - Modify: `web/apps/console/src/app/App.test.tsx` - Modify: `web/apps/console/src/styles/global.css` **Interfaces:** - Consumes: `useLdaReportDemo(target, recordEvidence)`. - Produces: visible operator panel for prepared report workflow demo. - [ ] **Step 1: Write panel tests first** Create `web/apps/console/src/demo/LdaReportDemoPanel.test.tsx` with mocked hook state cases: ```ts import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi } from "vitest"; import { LdaReportDemoPanel } from "./LdaReportDemoPanel.js"; describe("LdaReportDemoPanel", () => { it("shows setup commands when the prepared deployment is missing", () => { render( , ); expect(screen.getByText(/prepared demo deployment is missing/i)).toBeInTheDocument(); expect(screen.getByText(/wf-rpc-server --config examples\\/lda_report_workflow\\/wf.config.json/i)).toBeInTheDocument(); }); it("starts the demo when ready", async () => { const startRun = vi.fn(); render( , ); await userEvent.click(screen.getByRole("button", { name: /start demo run/i })); expect(startRun).toHaveBeenCalledOnce(); }); }); ``` - [ ] **Step 2: Run failing panel tests** Run: ```powershell pnpm --dir web --filter @lda/console test -- src/demo/LdaReportDemoPanel.test.tsx ``` Expected: FAIL because the component does not exist. - [ ] **Step 3: Implement the panel** Create `web/apps/console/src/demo/LdaReportDemoPanel.tsx`: ```tsx import { useMemo, useState } from "react"; import { ldaReportSetupCommands } from "./ldaReportDemoConfig.js"; import type { useLdaReportDemo } from "./useLdaReportDemo.js"; type Controller = ReturnType; export const LdaReportDemoPanel = ({ controller }: { readonly controller: Controller }) => { const { state } = controller; const [selectedIds, setSelectedIds] = useState>(new Set()); const [comment, setComment] = useState("Create selected issues before the defense."); const proposedIssues = state.interruptPayload?.proposed_issues ?? []; const selectedIssueIds = useMemo(() => [...selectedIds], [selectedIds]); return (

lda report workflow demo

Prepared workflow: start run, stop at typed issue review, resume, then inspect trace and generated issues.

{state.phase === "missing" && (

Prepared demo deployment is missing

Run the example RPC server/store setup outside the UI, then refresh.

{ldaReportSetupCommands.join("\\n")}
)} {(state.phase === "ready" || state.phase === "checking") && ( )} {(state.phase === "starting" || state.phase === "resuming") && (

Demo workflow is {state.phase}.

)} {state.phase === "interrupted" && state.interruptPayload && (

Typed interrupt: issue_review

Run id: {state.runId}

Generated report preview

{state.interruptPayload.report_markdown}
Select issues to create {proposedIssues.map((issue) => ( ))}