# Presentation AI Chat Surface 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:** Replace the custom `OperatorChat` markup with source-owned AI Elements-style chat primitives that make messages, tool calls, and approval requests look and behave like a standard AI app while preserving the existing deterministic timeline agent.
**Architecture:** Keep `AgentMessage`, `AgentMessagePart`, `TimelineAgentController`, and `SchemaApprovalSurface` as the behavioral seam. Add app-local, source-owned chat primitives with AI Elements-compatible concepts (`Conversation`, `Message`, `Tool`, prompt/action row) under `src/presentation/chat/`; `OperatorChat` becomes a thin adapter from existing agent messages to those primitives. Do not add a live LLM driver in this slice.
**Tech Stack:** React 19, TypeScript, Vitest, Testing Library, existing CSS modules in `presentation.css`; AI Elements reference model from `/vercel/ai-elements` docs, source-owned locally instead of installed through shadcn CLI because this app is not currently configured with shadcn/ui.
## Global Constraints
- Do not replace `useDemoTimeline`, `useTimelineAgent`, or the live/replay truth model.
- Do not introduce an AI SDK network driver or provider key requirement.
- Do not add shadcn/ui, Radix, or lucide unless a task explicitly proves the dependency is already needed.
- Preserve approval behavior: schema approval `Submit` calls `timelineAgent.submitSelectedIssues` when a timeline agent exists; `Cancel` calls `timelineAgent.cancelReview`.
- Preserve route behavior: `/present#scene/interrupt-evidence/approval` must still show the approval surface, and the chat/footer live status must stay synchronized.
- Add comments around intentional source-owned AI Elements compatibility. Future agents should know this is a component seam, not a fake package install.
- Scope test runs to affected files first, then run console typecheck/build.
---
## File Structure
Create:
- `web/apps/console/src/presentation/chat/ChatPrimitives.tsx` — source-owned AI Elements-style primitives for conversation, message, tool, and prompt action surfaces.
- `web/apps/console/src/presentation/chat/ChatPrimitives.test.tsx` — primitive semantics, collapsed tool behavior, action row tests.
- `web/apps/console/src/presentation/chat/agentChatProjection.ts` — pure projection from `AgentMessagePart` to renderable chat rows.
- `web/apps/console/src/presentation/chat/agentChatProjection.test.ts` — projection tests for text, workflow handoff, tool result, presentation action, approval request, and errors.
Modify:
- `web/apps/console/src/presentation/OperatorChat.tsx` — replace custom part rendering with primitives and projection.
- `web/apps/console/src/presentation/OperatorChat.test.tsx` — update assertions for the new primitive structure and collapsed tool call behavior.
- `web/apps/console/src/presentation/presentation.css` — replace `.chat-message` / `.chat-tool-part` rules with `.ai-chat-*` rules scoped under `.operator-chat`.
- `docs/current_roadmap.md` — mark the chat primitive slice complete after implementation.
Do not modify:
- `web/apps/console/src/demo/agent/events.ts` unless TypeScript proves a missing field is needed.
- `web/apps/console/src/demo/agent/timelineAgent.ts` unless an existing test fails because of a real integration issue.
- `web/apps/console/src/presentation/approval/SchemaApprovalSurface.tsx`.
---
### Task 1: Add Source-Owned Chat Primitives
**Files:**
- Create: `web/apps/console/src/presentation/chat/ChatPrimitives.tsx`
- Create: `web/apps/console/src/presentation/chat/ChatPrimitives.test.tsx`
- Modify: `web/apps/console/src/presentation/presentation.css`
**Interfaces:**
- Produces:
- `Conversation({ children, mode })`
- `ConversationContent({ children })`
- `Message({ from, children })`
- `MessageContent({ children })`
- `MessageResponse({ children })`
- `Tool({ label, name, state, defaultOpen, children })`
- `ToolInput({ input })`
- `ToolOutput({ status, output })`
- `PromptAction({ label, disabled, onClick })`
- Consumes: React children only; no agent-specific types.
- [ ] **Step 1: Write primitive tests**
Create `web/apps/console/src/presentation/chat/ChatPrimitives.test.tsx`:
```tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import {
Conversation,
ConversationContent,
Message,
MessageContent,
MessageResponse,
PromptAction,
Tool,
ToolInput,
ToolOutput,
} from "./ChatPrimitives.js";
describe("ChatPrimitives", () => {
it("renders conversation and message landmarks", () => {
render(
Live target is ready.,
);
expect(screen.getByRole("log", { name: "operator conversation" })).toHaveAttribute("data-mode", "dock");
expect(screen.getByText("Live target is ready.")).toBeInTheDocument();
});
it("keeps tool details collapsed by default and expands on click", async () => {
const user = userEvent.setup();
render(
,
);
const toggle = screen.getByRole("button", { name: /workflow operation workflow\.runs\.start success/i });
expect(screen.queryByText(/deployment_id/)).not.toBeInTheDocument();
await user.click(toggle);
expect(screen.getByText(/deployment_id/)).toBeInTheDocument();
expect(screen.getByText(/run_123/)).toBeInTheDocument();
});
it("supports default-open tools for currently relevant operations", () => {
render(
,
);
expect(screen.getByText("waiting for operator")).toBeInTheDocument();
});
it("renders prompt action buttons", async () => {
const user = userEvent.setup();
const run = vi.fn();
render();
await user.click(screen.getByRole("button", { name: "Run prepared workflow" }));
expect(run).toHaveBeenCalledOnce();
});
});
```
- [ ] **Step 2: Run tests to verify failure**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/chat/ChatPrimitives.test.tsx
```
Expected: FAIL because `ChatPrimitives.tsx` does not exist.
- [ ] **Step 3: Implement primitives**
Create `web/apps/console/src/presentation/chat/ChatPrimitives.tsx`:
```tsx
import { useId, useState, type ReactNode } from "react";
export type ConversationMode = "hidden" | "rail" | "dock";
export type MessageFrom = "user" | "assistant" | "system";
export type ToolState = "pending" | "success" | "error";
type ChildrenProps = {
readonly children: ReactNode;
};
const formatJson = (value: unknown): string => {
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
};
export const Conversation = ({ mode, children }: ChildrenProps & { readonly mode: ConversationMode }) => (
{children}
);
export const ConversationContent = ({ children }: ChildrenProps) => (
)}
);
case "error":
return {part.message};
}
};
```
Top-level render:
```tsx
```
- [ ] **Step 4: Remove obsolete CSS rules**
In `presentation.css`, remove or stop relying on:
- `.chat-message`
- `.chat-tool-part`
- `.chat-error`
Keep `.operator-chat`, `.operator-chat__action` only if still needed for layout. Prefer `.ai-chat-prompt-action` for the run button.
- [ ] **Step 5: Run focused tests and commit**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/OperatorChat.test.tsx src/presentation/chat/ChatPrimitives.test.tsx src/presentation/chat/agentChatProjection.test.ts
```
Expected: PASS.
Commit:
```bash
git add web/apps/console/src/presentation/OperatorChat.tsx web/apps/console/src/presentation/OperatorChat.test.tsx web/apps/console/src/presentation/presentation.css
git commit -m "refactor: render operator chat with AI primitives"
```
---
### Task 4: Preserve Route-Level Live/Replay And Approval Behavior
**Files:**
- Modify: `web/apps/console/src/presentation/PresentationRoute.test.tsx`
- Modify only if tests expose a real bug: `web/apps/console/src/presentation/OperatorChat.tsx`
**Interfaces:**
- Consumes: unchanged `OperatorChat` props.
- Produces: route-level proof that the new chat primitives did not break the presentation.
- [ ] **Step 1: Add route-level primitive assertions**
In `PresentationRoute.test.tsx`, add:
```tsx
it("renders the live target status through the AI chat surface", async () => {
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
const { PresentationRoute } = await import("./PresentationRoute.js");
render();
expect(await screen.findByRole("log", { name: "operator conversation" })).toBeInTheDocument();
expect(await screen.findByText(/Live target is ready/i)).toBeInTheDocument();
expect(screen.getByLabelText("presentation evidence mode")).toHaveAttribute("data-status", "ready");
});
it("keeps Scene 10 approval submit and cancel wired through chat primitives", async () => {
const user = userEvent.setup();
setReplayMode();
window.location.hash = "#scene/interrupt-evidence/approval";
const { PresentationRoute } = await import("./PresentationRoute.js");
render();
const submitButton = await screen.findByRole("button", { name: "Submit" });
await waitFor(() => expect(submitButton).toBeEnabled(), { timeout: 10000 });
await act(async () => {
await user.click(submitButton);
});
expect(window.location.hash).toBe("#scene/interrupt-evidence/resume");
});
```
If equivalent tests already exist after prior slices, update their expectations to include `role="log"` rather than duplicating whole scenarios.
- [ ] **Step 2: Run route tests**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/PresentationRoute.test.tsx
```
Expected: PASS.
- [ ] **Step 3: Run presentation focused tests**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation src/demo/agent
```
Expected: PASS.
- [ ] **Step 4: Commit**
Commit only test updates or integration fixes:
```bash
git add web/apps/console/src/presentation/PresentationRoute.test.tsx web/apps/console/src/presentation/OperatorChat.tsx
git commit -m "test: preserve presentation chat route behavior"
```
---
### Task 5: Docs, Roadmap, And Visual Smoke
**Files:**
- Modify: `docs/current_roadmap.md`
- Modify: `web/README.md`
- Move: `docs/superpowers/plans/2026-07-09-presentation-ai-chat-surface.md` -> `docs/historical/superpowers/plans/2026-07-09-presentation-ai-chat-surface.md`
**Interfaces:**
- Produces: documented current behavior and archived plan.
- [ ] **Step 1: Update roadmap**
In `docs/current_roadmap.md`, change item 14 from:
```md
14. Then: adopt source-owned AI Elements chat primitives against existing
`AgentMessagePart` / `AgentDriver` contracts.
```
to:
```md
14. Completed: presentation chat uses source-owned AI Elements-style
conversation, message, tool, and prompt-action primitives against existing
`AgentMessagePart` / `TimelineAgent` contracts. Live AI SDK driver remains
deferred; the current chat runs the deterministic timeline agent.
Implementation:
[`presentation AI chat surface`](historical/superpowers/plans/2026-07-09-presentation-ai-chat-surface.md).
```
- [ ] **Step 2: Update web README**
In `web/README.md`, add a short paragraph under Presentation Mode:
```md
The presentation chat surface is source-owned and follows the AI Elements
conversation/message/tool/prompt-action model. It currently renders the
prepared timeline agent and approval flow; a future AI SDK driver should target
`AgentMessagePart` / `TimelineAgent`-compatible events instead of replacing the
presentation timeline.
```
- [ ] **Step 3: Archive the plan**
Run:
```bash
git mv docs/superpowers/plans/2026-07-09-presentation-ai-chat-surface.md docs/historical/superpowers/plans/2026-07-09-presentation-ai-chat-surface.md
```
- [ ] **Step 4: Run verification**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation src/demo/agent
pnpm --dir web --filter @lda/console typecheck
pnpm --dir web --filter @lda/console build
git diff --check
```
Expected:
- Tests pass.
- Typecheck passes.
- Build passes with only the known Vite chunk-size warning.
- `git diff --check` reports no whitespace errors; Windows CRLF warnings are acceptable.
- [ ] **Step 5: Browser smoke**
Use an already running dev server or start one manually with:
```bash
pnpm --dir web --filter @lda/console dev
```
Smoke these routes:
- `http://127.0.0.1:5173/present#scene/interrupt-evidence/approval`
- `http://127.0.0.1:5173/present#scene/interrupt-evidence/trace`
- `http://127.0.0.1:5173/present#scene/agent-handoff/request`
Expected:
- Chat uses the new conversation/message surface.
- Tool calls are collapsed by default except workflow handoff / approval where explicitly default-open.
- Submit/Cancel still work on Scene 10 approval.
- Footer truth badge and chat intro agree about live/replay status.
- [ ] **Step 6: Commit docs and archive**
```bash
git add docs/current_roadmap.md web/README.md docs/historical/superpowers/plans/2026-07-09-presentation-ai-chat-surface.md
git add -u docs/superpowers/plans/2026-07-09-presentation-ai-chat-surface.md
git commit -m "docs: complete presentation AI chat surface"
```
---
## Self-Review
- Spec coverage: This plan covers source-owned AI Elements-style primitives, existing message/tool/approval contracts, route-level live/replay truth, docs, and visual smoke. It intentionally excludes a live AI SDK driver and provider key handling.
- Placeholder scan: No `TBD`, `TODO`, "similar to", or unspecified error-handling steps remain.
- Type consistency: `ProjectedChatPart`, primitive component names, and `OperatorChat` usage are defined before use. `ToolState` values match the planned primitive implementation.
- Risk: The plan uses local AI Elements-style primitives instead of running `npx ai-elements@latest add ...` because the console app does not have a shadcn/ui component registry. This should be explicit in the report so reviewers do not mistake it for a package install.