From fde4ab0b261eca640fdbe3f10889e41a2ea9d0fa Mon Sep 17 00:00:00 2001 From: hshum Date: Tue, 14 Jul 2026 05:46:31 -0700 Subject: [PATCH 1/2] docs(ai-elements): reconcile migration governance --- CHANGELOG/README.md | 27 +- CHANGELOG/TEMPLATE.md | 19 +- CHANGELOG/build/vite.md | 15 + CHANGELOG/components/fast-agent-panel.md | 65 +++++ CHANGELOG/integrations/ai-elements.md | 34 +++ docs/architecture/AI_ELEMENTS_MIGRATION.md | 122 ++++---- .../AI_ELEMENTS_MIGRATION_HANDOFF.md | 244 +++++++++------- docs/design/UI_CONTRACT.md | 273 ++++++++++-------- docs/design/ui-contract/README.md | 116 +++++--- src/design/designSystem.test.ts | 29 ++ src/design/designSystem.ts | 88 +++++- 11 files changed, 691 insertions(+), 341 deletions(-) create mode 100644 CHANGELOG/build/vite.md create mode 100644 CHANGELOG/components/fast-agent-panel.md create mode 100644 CHANGELOG/integrations/ai-elements.md diff --git a/CHANGELOG/README.md b/CHANGELOG/README.md index d4874c4b..13c8d9de 100644 --- a/CHANGELOG/README.md +++ b/CHANGELOG/README.md @@ -7,7 +7,32 @@ This directory contains append-only per-surface changelog lanes. Each lane recor ### Pages and prototypes - [`pages/proto-home-v5.md`](pages/proto-home-v5.md) — ScratchNode live-event prototype at `public/proto/home-v5.html`. +- [`pages/convex-events.md`](pages/convex-events.md) — ScratchNode live-event backend surface. +- [`pages/linkedin-daily-brief.md`](pages/linkedin-daily-brief.md) — LinkedIn daily-brief pipeline. +- [`pages/scratchnode-nodebench-bridge.md`](pages/scratchnode-nodebench-bridge.md) — ScratchNode-to-NodeBench bridge. + +### Components + +- [`components/fast-agent-panel.md`](components/fast-agent-panel.md) — FastAgentPanel AI Elements cutovers and preserved behavior seams. + +### Integrations + +- [`integrations/ai-elements.md`](integrations/ai-elements.md) — Vercel AI Elements scaffold, adapter, and governance integration. + +### Build and bundling + +- [`build/vite.md`](build/vite.md) — Vite/PWA chunking rules that affect runtime delivery. + +### Scripts and operating systems + +- [`scripts/improvement-loop.md`](scripts/improvement-loop.md) — self-improvement loop tooling. +- [`goals.md`](goals.md) — goal-driven development operating system. ## Format -Use the template in [`TEMPLATE.md`](TEMPLATE.md). Prepend new entries at the top of the relevant lane. +Use the template in [`TEMPLATE.md`](TEMPLATE.md). Prepend new entries at the top +of the relevant lane. Cross-link every affected lane for a multi-surface change. +Use the canonical squash SHA from `origin/main`; if it does not exist yet, use +the template's explicit pending marker and replace it before the final release +commit. Source, checks, visual proof, preview, and production-live state are +separate claims. diff --git a/CHANGELOG/TEMPLATE.md b/CHANGELOG/TEMPLATE.md index 35a3fa62..ad841999 100644 --- a/CHANGELOG/TEMPLATE.md +++ b/CHANGELOG/TEMPLATE.md @@ -6,8 +6,23 @@ Each lane is append-only. Add the newest entry directly below the title and desc ## YYYY-MM-DD — Short imperative title What changed and why in 1–3 sentences. Mention the user-visible effect and any important verification. -**Commit**: ``. **Author**: Homen Shum + Augment Agent. -**Touches**: `other/lane.md` if this is part of a multi-surface change. +**PR / canonical main commit**: `#NNN` / ``. +Use `PENDING #NNN MAIN SHA / FINAL QA` only while the merge or exact-revision +verification is genuinely pending; replace it before the final release commit. + +**Evidence state**: +- Source: `` +- Checks: `` +- Visual proof: `` +- Preview: `` +- Production live: `` + +**Author**: Homen Shum + ``. +**Touches**: `other/lane.md` if this is part of a multi-surface change; use a +real relative Markdown link in the completed entry. ``` Skip entries for generated files, pure formatting, lockfile churn, or changes that would not help a future maintainer understand the surface. +Never fabricate screenshots, QA receipts, findings, Agentic UI Bar scores, test +counts, preview results, or production-live claims. A green build proves only +the build that ran; it does not prove a deployment. diff --git a/CHANGELOG/build/vite.md b/CHANGELOG/build/vite.md new file mode 100644 index 00000000..c7a2453b --- /dev/null +++ b/CHANGELOG/build/vite.md @@ -0,0 +1,15 @@ +# Vite and PWA bundling + +Append-only lane for Vite/Rolldown chunking and service-worker precache rules. +Newest entries first. + +## 2026-07-14 — Preserve Shiki's lazy grammar splitting + +Removed `@shikijs/*` from the shared editor manual chunk, routed emitted +grammar/theme chunks under `assets/shiki/`, and excluded that directory from +PWA precache. The rule preserves lazy language loading and avoids collapsing +the grammar set into an oversized eager/precache asset. + +**PR / canonical main commit**: #516 / `c83a41c8`. +**Evidence state**: source merged. Build and chunk-size claims must be measured on the exact candidate revision. +**Touches**: [`../integrations/ai-elements.md`](../integrations/ai-elements.md), [`../components/fast-agent-panel.md`](../components/fast-agent-panel.md). diff --git a/CHANGELOG/components/fast-agent-panel.md b/CHANGELOG/components/fast-agent-panel.md new file mode 100644 index 00000000..4b46438e --- /dev/null +++ b/CHANGELOG/components/fast-agent-panel.md @@ -0,0 +1,65 @@ +# FastAgentPanel + +Append-only lane for behavior-preserving AI Elements adoption inside +`src/features/agents/components/FastAgentPanel/`. Newest entries first. + +## 2026-07-14 — Migrate LiveEventCard onto the shared tool shell + +LiveEventCard now composes the tool/task vocabulary, while live-event derivation +is shared with FastAgentPanel and the panel header is isolated behind its own +component contract. This records source merge, not visual-proof-complete or a +direct production DOM claim. + +**PR / canonical main commit**: #527 / `28d704b2`. +**Evidence state**: source merged; later evidence states not recorded here. +**Touches**: [`../integrations/ai-elements.md`](../integrations/ai-elements.md). + +## 2026-07-14 — Wrap the live composer without changing send semantics + +InputBar now composes the `prompt-input` shell while the explicit send-contract +seam preserves send, stop, spawn, voice, attachment, model, and enhancement +behavior. This entry records source state only; visual and production proof are +tracked separately. + +**PR / canonical main commit**: #526 / `3cc7cd06`. +**Evidence state**: source merged; later evidence states not recorded here. +**Touches**: [`../integrations/ai-elements.md`](../integrations/ai-elements.md). + +## 2026-07-14 — Wrap the live message bubble around generic parts + +UIMessageBubble now uses the message/reasoning/tool/source vocabulary while the +identity-preserving adapter routes domain cards through the existing custom +renderers and leaves live Convex hooks authoritative. + +**PR / canonical main commit**: #525 / `165ecec2`. +**Evidence state**: source merged; later evidence states not recorded here. +**Touches**: [`../integrations/ai-elements.md`](../integrations/ai-elements.md). + +## 2026-07-14 — Migrate tool-call transparency + +ToolCallTransparency maps running, success, and error states onto the AI +Elements tool shell without changing the existing arguments/results disclosure. + +**PR / canonical main commit**: #524 / `64203ded`. +**Evidence state**: source merged; later evidence states not recorded here. +**Touches**: [`../integrations/ai-elements.md`](../integrations/ai-elements.md). + +## 2026-07-14 — Wrap the agent-progress disclosure + +CollapsibleAgentProgress composes task, reasoning, and tool primitives while +preserving its exported ToolUIPart contract. This is a source-migration record, +not a claim that the component rendered on a live production path. + +**PR / canonical main commit**: #523 / `a4fe5ee3`. +**Evidence state**: source merged; live-render evidence not claimed. +**Touches**: [`../integrations/ai-elements.md`](../integrations/ai-elements.md). + +## 2026-07-14 — Adopt primitives in six low-risk leaves + +TypingIndicator, ThoughtBubble, QuickCommandChips, LazySyntaxHighlighter, +AgentHierarchy, and SourceCard moved onto the themed primitive layer with their +public component contracts retained. + +**PR / canonical main commit**: #516 / `c83a41c8`. +**Evidence state**: source merged; later evidence states not recorded here. +**Touches**: [`../integrations/ai-elements.md`](../integrations/ai-elements.md), [`../build/vite.md`](../build/vite.md). diff --git a/CHANGELOG/integrations/ai-elements.md b/CHANGELOG/integrations/ai-elements.md new file mode 100644 index 00000000..76b56785 --- /dev/null +++ b/CHANGELOG/integrations/ai-elements.md @@ -0,0 +1,34 @@ +# Vercel AI Elements integration + +Append-only lane for the vendored primitive layer, shared adapters, design +governance, and integration-wide contracts. Newest entries first. + +## 2026-07-14 — Add the identity-preserving Convex parts adapter + +Added `convexToUIParts` as the shared parser for UIMessage parts. It retains +source part identity, separates generic parts from domain passthrough, and does +not take ownership of persistent text streams. + +**PR / canonical main commit**: #521 / `30688119`. +**Evidence state**: source merged; adapter test evidence belongs to the exact tested revision. +**Touches**: [`../components/fast-agent-panel.md`](../components/fast-agent-panel.md). + +## 2026-07-14 — Prevent code-token cache collisions + +Changed the code-block token cache key to include the complete source so equal- +length snippets with the same prefix and suffix cannot display each other's +cached middle content. + +**PR / canonical main commit**: #517 / `988a3f56`. +**Evidence state**: source merged; later evidence states not recorded here. +**Touches**: [`../components/fast-agent-panel.md`](../components/fast-agent-panel.md). + +## 2026-07-14 — Establish the AI Elements primitive and governance layer + +Vendored the themed AI Elements and isolated shadcn base primitives, added the +thin consumer layer, migrated six leaves, and created the design manifest, +56-file migration matrix, and visual-proof protocol. + +**PR / canonical main commit**: #516 / `c83a41c8`. +**Evidence state**: source merged; no dated visual-proof folder is claimed by this entry. +**Touches**: [`../components/fast-agent-panel.md`](../components/fast-agent-panel.md), [`../build/vite.md`](../build/vite.md). diff --git a/docs/architecture/AI_ELEMENTS_MIGRATION.md b/docs/architecture/AI_ELEMENTS_MIGRATION.md index b84373f5..498a4bc4 100644 --- a/docs/architecture/AI_ELEMENTS_MIGRATION.md +++ b/docs/architecture/AI_ELEMENTS_MIGRATION.md @@ -3,7 +3,8 @@ Scaffolding NodeBench's AI-chat UI onto [Vercel AI Elements](https://elements.ai-sdk.dev/) primitives so we maintain thin wrappers instead of ~80 hand-rolled components. -_Last updated: 2026-07-13_ +_Last updated: 2026-07-14. Canonical source: `origin/main` through PR #527 +(`28d704b2`)._ ## Why @@ -12,7 +13,7 @@ streaming, input, reasoning, tool calls, sources, loaders, code blocks). AI Elem ships these as owned, upgradeable source built on shadcn/ui — so "our components" become thin adapters over canonical primitives, not full custom implementations to self-maintain. -## Scaffold status — DONE (verified) +## Scaffold status — MERGED - **24 AI Elements primitives** installed at `src/components/ai-elements/`: message, conversation, prompt-input, reasoning, chain-of-thought, sources, @@ -30,9 +31,11 @@ thin adapters over canonical primitives, not full custom implementations to self `--background`/`--card`/`--muted`/`--border` are the warm neutrals with a `.dark` block. Every primitive (`bg-primary`, `text-muted-foreground`, `border`, `ring`) renders on-brand automatically. Only optional later polish: glass treatment on `bg-card`. -- **`tsc --noEmit` clean** across the whole project after the scaffold. +- The scaffold landed on `origin/main` in PR #516 (`c83a41c8`). Verification + claims for later revisions belong to the exact revision that was tested; a + historical green command is not reused as proof for current main. -## Consumer layer — SHIPPED (verified `tsc` green) +## Consumer layer — MERGED `src/features/agents/components/ai/` — our thin components built on the primitives, consuming the app's real `UIMessage` shape directly: @@ -70,29 +73,45 @@ Modeled on NodeRoom (`src/design/designSystem.ts` + `docs/design/ui-contract/`): extra inlined patterns vs. `defaultSpec.ts` — the audit here uses the canonical `defaultSpec.ts`; reconciling the two is a tracked follow-up. -## Batch 1 — SHIPPED + verified (2026-07-14, commit `776c4868`) - -6 low-risk leaf components migrated (internals-only, export APIs byte-for-byte -preserved): TypingIndicator→`shimmer`, ThoughtBubble→`reasoning`, -QuickCommandChips→`suggestion`, LazySyntaxHighlighter→`code-block`, -AgentHierarchy→`task`, SourceCard→`sources`+`inline-citation`. -Gate: `tsc` 0 errors · FastAgentPanel suite **89 passed / 0 failed** · `build` exit 0. - -**Build regression fixed (this batch introduced it):** LazySyntaxHighlighter -pulled Shiki into a live path for the first time; `vite.config.ts` force-grouped -all `@shikijs/*` into one `editor-vendor` chunk → a single 10.2 MB blob that -overflowed the PWA 2 MiB precache limit and broke the build. Fix (config-only, -primitive pristine): (1) unlump `@shikijs` so Shiki self-splits per grammar -(`editor-vendor` 10.2 MB → 782 kB), (2) route the 278 grammar chunks to -`assets/shiki/` and `globIgnore` them from precache (they lazy-load on demand). -Curating Shiki to a language allowlist is blocked — shiki 4.x `exports` has no -`./langs/*` wildcard, so fine-grained imports aren't Vite-resolvable. +## Current delivery status — 11/26 complete, migration ongoing + +The runbook scoreboard is **11/26 component decisions complete**. It does not +close the broader 56-file component migration. + +| Milestone | Canonical main source | State | +|---|---|---| +| TypingIndicator, ThoughtBubble, QuickCommandChips, LazySyntaxHighlighter, AgentHierarchy, SourceCard | PR #516 · `c83a41c8` | migrated/wrapped | +| Full-source code-token cache identity | PR #517 · `988a3f56` | compatibility fix | +| `convexToUIParts` shared adapter | PR #521 · `30688119` | merged adapter | +| CollapsibleAgentProgress | PR #523 · `a4fe5ee3` | wrapped source; **not a live-render claim** | +| ToolCallTransparency | PR #524 · `64203ded` | migrated | +| UIMessageBubble | PR #525 · `165ecec2` | wrapped | +| InputBar + explicit send-contract seam | PR #526 · `3cc7cd06` | wrapped | +| LiveEventCard + shared live-event derivation | PR #527 · `28d704b2` | migrated | + +The scoreboard counts 11 completed component decisions in the matrix. The +shared adapter is required foundation but is tracked separately from the +numerator. Its denominator is the original 26 candidate rows: 8 `migrate`, 17 +`wrap`, and the transitional HumanRequestCard `wrap→likely keep` review. The +other 30 rows are explicit `keep_custom`. HumanRequestCard remains operationally +keep-custom unless a future contract review preserves every HITL semantic. + +**Shiki build guard:** LazySyntaxHighlighter made Shiki reachable. The PR #516 +Vite change leaves `@shikijs/*` out of `manualChunks`, routes grammar/theme +chunks to `assets/shiki/`, and excludes that directory from service-worker +precache. Curating Shiki to a language allowlist remains blocked by the package's +export map. Do not infer current bundle sizes from the historical migration run; +measure the exact build being released. ## Migration matrix (from the mapping workflow — 56 files) -**8 migrate / 18 wrap / 30 keep_custom.** The message-render vertical (live path) and the -input composer carry all the high-risk Convex seams; everything domain-shaped (entity -pickers, verification reports, media galleries, memory cards) stays custom. +**Original 56 rows: 8 migrate / 17 wrap / 30 keep_custom / 1 transitional +`wrap→likely keep` review (HumanRequestCard).** The program denominator of 26 +counts that transitional row with the migration candidates. Operationally, +HumanRequestCard stays inside the keep-custom boundary unless a future contract +review proves full parity. This is a planning matrix, not a completion claim. +Completed units are listed above; all other rows remain ongoing until their +exact source and verification evidence land. ### message-render @@ -185,28 +204,30 @@ pickers, verification reports, media galleries, memory cards) stays custom. | ArbitrageReportCard | Verification report | keep_custom | — | data parsed from tool-result; depends on StatusBadge from VisualCitation | low | | LiveEventCard | Single live agent-event card | migrate | `tool` + task connector | ORPHANED (FAP inlines its own); feed from liveEvents useMemo, never fixtures | med | -## Sequenced implementation plan (value-per-risk) - -- **Phase 0 — Shared adapter + de-risk on dead code (low risk).** Build `convexToUIParts` - once; prove primitives on orphaned files first (ThoughtBubble→reasoning, TypingIndicator→shimmer, - CollapsibleAgentProgress, MessageStream/UIMessageStream) — they can't break live flows. -- **Phase 1 — Message-render leaf renderers (med).** LazySyntaxHighlighter→code-block (−130KB); - StreamingMessage→MessageResponse (keep `useStream`); MessageBubble→message+reasoning. -- **Phase 2 — Live UIMessageBubble wrap (HIGH — crown jewel).** Wrap generic sub-parts only; - domain cards render unchanged inside `MessageContent`. Guard with `MessageBubble.streaming.test`. -- **Phase 3 — Input composer (HIGH).** InputBar→prompt-input (delegate onSubmit→onSend); - FileUpload→attachments; QuickCommandChips→suggestion. -- **Phase 4 — Reasoning + tools/tasks shells (mixed).** LiveThinking, ToolCallTransparency, - StepTimeline, AgentTasksTab/AgentHierarchy, ToolResultPopover. -- **Phase 5 — Cards + domain wraps (low-med, opportunistic).** SourceCard, FileViewer (security!), - TokenUsageBadge, GoalCard, DocumentActionCard, EditProgressCard. Re-evaluate HumanRequestCard - (likely keep_custom). - -## Shared adapter (build once) - -`src/features/agents/components/FastAgentPanel/adapters/convexToUIParts.ts` — funnel all -message-render migrations through one parts-parser so "is this a tool-result part?" isn't -re-derived four times (UIMessageBubble, StepTimeline, streamingPhases, FusedSearchResults): +## Sequenced implementation plan + +- **Completed foundation:** scaffold + six leaves (#516), shared adapter (#521), + CollapsibleAgentProgress (#523), ToolCallTransparency (#524), UIMessageBubble + (#525), InputBar (#526), and LiveEventCard (#527). +- **Next message/input leaves:** MessageBubble, MessageStream, UIMessageStream, + StreamingMessage, FileUpload, and LiveThinking. Confirm reachability before + work and retain `useStream` for persistent text. +- **Then tool/task shells:** ToolResultPopover, StepTimeline, and AgentTasksTab. + Preserve live queries, timeline ingestion, and entity callbacks. +- **Then bounded cards:** FileViewer, TokenUsageBadge, GoalCard, + DocumentActionCard, and EditProgressCard. FileViewer sandbox policy and edit + mutations are security/behavior seams, not presentation details. +- **Keep custom:** HumanRequestCard and every domain/proof surface named in the + keep-custom matrix. Revisit only with an explicit product-contract review. + +Each slice is source-complete only after its tests pass. Visual-proof-complete, +preview-verified, and production-live-verified remain separate later states. + +## Shared adapter (merged in #521) + +`src/features/agents/components/FastAgentPanel/adapters/convexToUIParts.ts` +funnels message-render migrations through one parts parser so part identity is +not re-derived independently in every renderer: ``` convexToUIParts(message: UIMessage): { @@ -216,8 +237,7 @@ convexToUIParts(message: UIMessage): { } ``` -`domainParts` is a **pass-through, not a transform** — it routes domain parts to keep_custom -components, never flattens them into a primitive. That boundary is what keeps the honesty -contract intact. Do NOT make the adapter emit persistent-text-streaming bodies — that path -(`useStream`) is a live subscription; StreamingMessage keeps its own hook and plugs into the -same `MessageResponse` target. +`domainParts` is a **pass-through, not a transform**. It routes domain parts to +keep-custom components and never flattens them into a primitive. The adapter +must not emit persistent-text-streaming bodies: that path remains a live +`useStream` subscription owned by StreamingMessage. diff --git a/docs/architecture/AI_ELEMENTS_MIGRATION_HANDOFF.md b/docs/architecture/AI_ELEMENTS_MIGRATION_HANDOFF.md index 7024ce0a..6aaea9ec 100644 --- a/docs/architecture/AI_ELEMENTS_MIGRATION_HANDOFF.md +++ b/docs/architecture/AI_ELEMENTS_MIGRATION_HANDOFF.md @@ -1,119 +1,141 @@ # AI Elements Migration — Codex Handoff -**Owner of record:** Claude (batch 1 + governance). **Next owner:** Codex agent -(`gpt-5.6-sol`, deepswe #1 — this is the multi-file, live-Convex-seam work it's for). -**Date:** 2026-07-14. **Branch to base on:** `origin/main` (batch 1 merged as of this handoff). - -> Goal in one line: **retire the ~56 hand-rolled FastAgentPanel AI components by -> making them thin adapters over Vercel AI Elements primitives — WITHOUT breaking a -> single live Convex stream or changing any export contract.** - -## Read these first (do not skip) - -1. [`docs/design/UI_CONTRACT.md`](../design/UI_CONTRACT.md) — the visual + behavior contract, DNA tokens, migration rule. -2. [`docs/architecture/AI_ELEMENTS_MIGRATION.md`](AI_ELEMENTS_MIGRATION.md) — the full 56-file matrix (8 migrate / 18 wrap / 30 keep_custom), the phased plan, and the `convexToUIParts` adapter design. -3. [`src/design/designSystem.ts`](../../src/design/designSystem.ts) — machine-readable per-primitive `must`/`avoid`. `npm run test:design` must stay green. -4. `.claude/rules/scratchpad_first.md` + the honesty contract below. +**Date:** 2026-07-14. **Canonical source:** `origin/main` through PR #527, +commit `28d704b2`. **Program state:** **11/26 component decisions complete**; +the broader 56-file migrate/wrap/keep-custom migration remains ongoing. + +> Goal: retire hand-rolled generic AI UI behind thin Vercel AI Elements +> adapters without changing a live Convex stream, product callback, export +> contract, navigation contract, or NodeBench-specific proof surface. + +## Release-train ledger + +These are squash commits on `origin/main`, not pre-merge branch SHAs. + +| PR | Canonical main SHA | Slice | +|---|---|---| +| #516 | `c83a41c8` | AI Elements scaffold, six leaf cutovers, design governance, Shiki chunk routing | +| #517 | `988a3f56` | Full-source code-token cache key | +| #519 | `00e5594d` | Fail-closed post-deploy verification | +| #520 | `35a7b85d` | Workspace live-smoke parity | +| #521 | `30688119` | Identity-preserving `convexToUIParts` adapter | +| #522 | `ad2b26c6` | Protected Tier B preview authentication | +| #523 | `a4fe5ee3` | `CollapsibleAgentProgress` source migration; **not a live-render claim** | +| #524 | `64203ded` | `ToolCallTransparency` migration | +| #525 | `165ecec2` | Live `UIMessageBubble` wrapper | +| #526 | `3cc7cd06` | Live `InputBar` / send-contract wrapper | +| #527 | `28d704b2` | `LiveEventCard` migration plus shared live-event derivation and panel header extraction | + +## Read first + +1. [`docs/design/UI_CONTRACT.md`](../design/UI_CONTRACT.md) — behavior, + visual DNA, evidence states, navigation, and keep-custom boundary. +2. [`docs/architecture/AI_ELEMENTS_MIGRATION.md`](AI_ELEMENTS_MIGRATION.md) — + the complete 56-file decision matrix and remaining sequence. +3. [`src/design/designSystem.ts`](../../src/design/designSystem.ts) — + machine-readable primitive rules and 11/26 scoreboard. +4. [`docs/design/ui-contract/README.md`](../design/ui-contract/README.md) — + proof protocol. It forbids placeholder screenshots and unsupported QA scores. ## Non-negotiable safety contract -These are the invariants that make this migration safe. **Every PR must uphold all of them.** - -1. **Honesty contract — no primitive drives a stream.** The live Convex hooks - (`useUIMessages(stream:true)`, `useSmoothText`, `useStream` persistent-text-streaming) - remain the data source. Primitives are the presentation layer fed by hook output. - **Never** feed a primitive a static string where a live stream belonged, and never - replace a live part with a fixture. This breaks the `message-live-data` / - `MessageBubble.streaming` test invariants and the ScratchNode honesty contract. -2. **Export API is byte-for-byte preserved.** A migration rewrites a component's - *internals only*; its exported signature, prop interface, and barrel re-exports do - not change. Call sites and `index.ts` / `index.enhanced.ts` stay untouched. -3. **Domain cards pass through, never flatten.** Selection cards, arbitrage reports, - media galleries, memory cards, GoalCard, verification receipts stay custom and render - unchanged inside `MessageContent` via the `renderCustomPart` passthrough. The adapter - *routes* domain parts; it does not transform them into a primitive. -4. **Reduced motion.** motion/react-driven primitives (`Shimmer`, `Reasoning`) bypass the - CSS `prefers-reduced-motion` override — add an explicit `useReducedMotion()` guard. -5. **Shiki bundle guard (already in `vite.config.ts` — do not regress).** Never - force-group `@shikijs/*` into one `manualChunk` (collapses ~200 grammars into a 10 MB - blob → PWA precache overflow → build fails). Grammar chunks route to `assets/shiki/` - and are `globIgnore`d from precache. If you touch `vite.config.ts`, re-run `npm run build`. -6. **Terracotta, not default Tailwind.** Primitives resolve to the shadcn token bridge. - `npm run lint:design` reports no NEW high-severity drift on the AI surface. - -## Verification floor (every PR, in order) - -``` -npx tsc --noEmit --pretty false # 0 errors -npx vitest run src/features/agents/components/FastAgentPanel/__tests__/ # 89 baseline, 0 fail -npx vitest run src/design/designSystem.test.ts # 11 pass -npm run lint:design # no NEW high-severity on AI surface -npm run build # exit 0, no "Assets exceeding" precache error -# For any LIVE-path change, also run the streaming guard explicitly: +1. **No primitive drives a stream.** `useUIMessages(stream:true)`, + `useSmoothText`, and persistent `useStream` subscriptions remain the data + source. Primitives only present their output. Never replace a live part with + fixture data. +2. **Preserve exports and callbacks.** Migration work changes internals only. + Existing public props, barrel exports, send/stop/spawn/voice callbacks, edit + and feedback handlers, and model semantics remain load-bearing. +3. **Domain cards pass through.** Selection cards, arbitrage reports, media, + memory, GoalCard, human requests, verification receipts, and document/edit + workflows remain custom. The adapter routes them without flattening them. +4. **Keep the web navigation contract:** `Home - Reports - Chat - Inbox - Me`. + Workspace remains a separate deployed surface, never a sixth web tab. +5. **Reduced motion is explicit.** Motion-driven primitives use an explicit + reduced-motion guard; the global CSS rule is insufficient. +6. **Keep the Shiki bundle guard.** Never force-group `@shikijs/*` into one + manual chunk. Grammar/theme chunks stay under `assets/shiki/` and outside + the service-worker precache. +7. **Terracotta, not default Tailwind.** Selection/focus/provenance uses the + NodeBench token bridge. Success green remains reserved for completed state. + +## Evidence vocabulary + +These states are independent and must never be collapsed: + +- **Source merged:** the canonical SHA is on `origin/main`. +- **Checks verified:** named commands were run for that exact source state. +- **Visual proof complete:** real before/after files and a valid manifest exist. +- **Preview verified:** a normal product path was browser-driven on a preview. +- **Production live verified:** the production deployment and rendered bundle + were checked directly after the merge. + +A green build does not prove preview or production state. A screenshot does not +prove production state. This handoff creates no screenshot, Agentic UI Bar +score, Gemini receipt, or production-live claim. + +## Verification floor + +Run from a clean worktree based on current `origin/main`: + +```powershell +npx tsc --noEmit --pretty false +npx vitest run src/features/agents/components/FastAgentPanel +npx vitest run src/design/designSystem.test.ts +npm run lint:design +npm run build npx vitest run src/features/agents/components/FastAgentPanel/__tests__/MessageBubble.streaming.test.tsx -# Before claiming "live": vite preview + drive the Chat surface, 0 console errors (live_dom_verification). +git diff --check ``` -## Done so far (do not redo) - -Batch 1 — 6 leaf components migrated, verified, shipped (this branch): -TypingIndicator→`shimmer`, ThoughtBubble→`reasoning`, QuickCommandChips→`suggestion`, -LazySyntaxHighlighter→`code-block`, AgentHierarchy→`task`, SourceCard→`sources`+`inline-citation`. -Plus the design dir + UI-contract dir + the Shiki build fix. - -## The remaining work — sequenced by risk (LOW → HIGH) - -### Step 1 — CollapsibleAgentProgress (DEAD-safe, do first) -- **File:** `src/features/agents/components/FastAgentPanel/CollapsibleAgentProgress.tsx` (137 lines). -- **Consumer:** only `index.enhanced.ts` (a barrel re-export) — **dead**, cannot break a live flow. -- **Target:** `task` / `chain-of-thought` + `reasoning` + `tool` shell. -- **Preserve:** the `toolParts: ToolUIPart[]` prop feed + the collapsible answer/process shell shape. -- **Risk:** low. This is the batch-1 pattern — internals-only, export preserved. - -### Step 2 — ToolCallTransparency + LiveEventCard (LIVE, guard each) -- **ToolCallTransparency** (`ToolCallTransparency.tsx`, 250 lines) → `tool`. - **Consumer: the LIVE `FastAgentPanel.UIMessageBubble.tsx`.** Map the status enum - running/success/error → `input-available`/`output-available`/`output-error`. Preserve - the MCP tool-call timeline semantics exactly. Guard with `MessageBubble.streaming.test` - and a live Chat-surface render check. -- **LiveEventCard** (`LiveEventCard.tsx`, 255 lines) → `tool` + task connector. - Consumer: `FastAgentPanel.tsx` (matrix says orphaned — FAP inlines its own; **confirm - render reachability first**). Feed from the `liveEvents` `useMemo`, never fixtures. -- **Risk:** medium. Both are single-component, self-contained. Preserve exports + behavior. - -### Step 3 — Crown jewels (HIGH — build the adapter first) -- **Build the shared adapter** `src/features/agents/components/FastAgentPanel/adapters/convexToUIParts.ts` - per the matrix spec (§ "Shared adapter"). It funnels message-render migrations through - one parts-parser. `domainParts` is a **pass-through, not a transform**. Do NOT make it - emit persistent-text-streaming bodies — `useStream` stays a live subscription. -- **UIMessageBubble.tsx (140 KB, THE live bubble)** → wrap with `message`+`reasoning`+`tool`+`sources`; - keep selection/arbitrage/media/GoalCard custom. Preserve `useUIMessages(stream:true)`, - `useSmoothText` gated on status, `useMessageHandlers()`, all `on{Company,Person,Event,News,Doc,Regen,Delete,Edit,Feedback}` - callbacks, and StreamingStatus. **Guard: `MessageBubble.streaming.test` + full FastAgentPanel suite + live render.** -- **InputBar.tsx** → wrap with `prompt-input` + `context` + `model-selector`. `PromptInput.onSubmit` - MUST delegate to `onSend`/`onStop`/`onSpawn`/`onVoiceIntent` — never swallow. Preserve - `useAction(enhancePrompt)`; slash / @mentions / voice / drag-drop stay custom. -- **Risk:** high — live Convex seams + the largest files. One component per PR. Cut over - only after e2e content assertions pass and the change is live-verified. - -## Keep-custom boundary (do NOT migrate — the matrix says keep) -VirtualizedMessageList (perf), StreamingStatus, VisualCitation, ParallelTaskTimeline, -SwarmLanesView, HumanRequestCard (Confirmation lacks textarea/multi-option), FusedSearchResults, -ResourceLinkCard, MermaidDiagram (XSS-hardened), MediaGallery, and all domain selection cards. -See the matrix for the full 30-file keep_custom list + rationale. - -## Definition of done (per step) -1. Verification floor green (above). -2. Export APIs unchanged; no barrel/call-site edits. -3. Domain cards + live streams intact (honesty contract). -4. `docs/architecture/AI_ELEMENTS_MIGRATION.md` adoption status updated (scaffolded→migrated/live). -5. For live-path steps: before/after proof captured under `docs/design/ui-contract/YYYYMMDD-/` - (`npm run dogfood:full:local`) + live-verified per `live_dom_verification`. -6. `docs/design/UI_CONTRACT.md` "Migration Status" appended with the shipped slice + commit. - -## Tracked follow-ups (not blockers) -- The `.mjs` design linter has drifted extra inlined patterns vs. canonical `defaultSpec.ts` - (only 1 high pattern: `uppercase tracking-widest`). Reconcile so `lint:design` and - `auditAiSurfaceDesign` agree. -- Curating Shiki to a language allowlist is blocked (shiki 4.x `exports` has no `./langs/*` - wildcard). Revisit if shiki adds fine-grained subpath exports. +For a live-path change, add browser assertions against the normal Chat path. +Only create a proof folder when the referenced images and receipts actually +exist. Only claim production live after post-merge production verification. + +## Completed component decisions — 11/26 + +- Six leaf components in #516: TypingIndicator, ThoughtBubble, + QuickCommandChips, LazySyntaxHighlighter, AgentHierarchy, and SourceCard. +- Shared `convexToUIParts` adapter in #521 is required foundation and is tracked + separately from the 11/26 component numerator. +- `CollapsibleAgentProgress` in #523. This is source-complete but must not be + presented as a proven live production surface. +- `ToolCallTransparency` in #524. +- `UIMessageBubble` in #525. +- `InputBar` and its explicit send-contract seam in #526. +- `LiveEventCard` plus the shared live-event derivation in #527. + +The 11/26 number counts completed component decisions in the 56-file matrix. +The denominator is the original 26 candidate rows: 8 migrate, 17 wrap, and the +HumanRequestCard wrap-to-keep re-evaluation row. HumanRequestCard remains inside +the operational keep-custom boundary. The broader matrix is not complete. + +## Remaining sequence + +1. Capture final visual/browser proof for reachable changed surfaces. Do not + invent proof for dead or unreachable exports. +2. Continue the medium-risk generic shells: MessageBubble, MessageStream, + UIMessageStream, StreamingMessage, FileUpload, LiveThinking, + ToolResultPopover, StepTimeline, AgentTasksTab, FileViewer, GoalCard, + TokenUsageBadge, DocumentActionCard, and EditProgressCard. Reconfirm + reachability before each slice. +3. Keep HumanRequestCard custom unless a future primitive preserves its + textarea, multi-option, cancel, and decision-recording semantics. + +## Keep-custom boundary + +Do not migrate VirtualizedMessageList, StreamingStatus, VisualCitation, +ParallelTaskTimeline, SwarmLanesView, HumanRequestCard, FusedSearchResults, +ResourceLinkCard, MermaidDiagram, MediaGallery, domain selection cards, memory +cards, or verification reports merely to increase adoption count. See the full +matrix for every rationale and live seam. + +## Definition of done for the next slice + +1. The verification floor passes for the exact candidate revision. +2. Exports, callbacks, live hooks, domain cards, nav, and Workspace separation + remain intact. +3. The 56-file matrix and machine-readable manifest are updated together. +4. Changelog entries use canonical main SHAs. A pending marker is allowed only + before merge and must be replaced before the final release commit. +5. Visual and live claims cite artifacts or direct checks that actually exist. diff --git a/docs/design/UI_CONTRACT.md b/docs/design/UI_CONTRACT.md index 74a24903..7d5a495b 100644 --- a/docs/design/UI_CONTRACT.md +++ b/docs/design/UI_CONTRACT.md @@ -1,138 +1,163 @@ # NodeBench UI Contract — AI Surface -Captured: 2026-07-14 - -This contract governs a **behavior-preserving** migration of NodeBench's agent -chat surface onto [Vercel AI Elements](https://elements.ai-sdk.dev) primitives. -Product behavior remains sourced from the live prod-parity app. AI Elements are -the component vocabulary, not a new authority over product logic: they render -generic surfaces (markdown, reasoning, tool headers, citations, code); every -domain, proof, and live-data affordance stays intact. - -The machine-readable companion to this doc is -[`src/design/designSystem.ts`](../../src/design/designSystem.ts) — the same -tokens, principles, and per-primitive rules, readable by tests and agents. -Enforcement is [`src/design-governance/`](../../src/design-governance/) via +Captured: 2026-07-14. Canonical source: `origin/main` through PR #527 +(`28d704b2`). + +This contract governs a behavior-preserving migration of NodeBench's agent chat +surface onto [Vercel AI Elements](https://elements.ai-sdk.dev) primitives. +Product behavior remains sourced from the prod-parity app. AI Elements provide +generic presentation vocabulary; NodeBench domain, proof, navigation, and +live-data behavior remain authoritative. + +The machine-readable companion is +[`src/design/designSystem.ts`](../../src/design/designSystem.ts). Enforcement is +[`src/design-governance/`](../../src/design-governance/) via `npm run lint:design`. -## Visual Authority Order +## Visual authority order -1. **Latest prod-parity app is the behavioral source of truth.** (`origin/main`; - clean worktree per `CLAUDE.md`.) Never restore behavior from a design artifact. -2. **`src/design/designSystem.ts` + this contract** are the token/primitive +1. `origin/main` in a clean prod-parity worktree is the behavioral source of + truth. Never restore behavior from a design packet or stale screenshot. +2. `src/design/designSystem.ts` and this contract are the token and primitive authority for the AI surface. -3. AI Elements' own default Tailwind palette is **reference only** — it is - re-themed to NodeBench tokens (terracotta `#d97757`), never shipped raw. -4. CSS cascade accidents and stale snapshots are not design decisions. +3. The latest UI kit packet is a design target, not an implementation branch. +4. AI Elements' default palette is reference only and must resolve through the + NodeBench token bridge. +5. CSS accidents, fixtures, and stale screenshots are not product decisions. -## Design DNA (tokens) +## Product invariants -Grounded in `src/index.css` (`:root` light + `.dark`). The shadcn token bridge -maps these onto every primitive automatically (`bg-primary`, `ring`, `border`). +- Web navigation remains exactly `Home - Reports - Chat - Inbox - Me`. +- Workspace remains a separate deployed surface, not a sixth web tab. +- Live Convex-backed flows never silently fall back to fixtures. +- Export signatures, send/stop/spawn/voice callbacks, model behavior, and + message action handlers remain load-bearing. +- Domain and proof cards pass through unchanged unless their own contract is + explicitly approved for migration. + +## Design DNA + +Grounded in `src/index.css` (`:root` light and `.dark`). The shadcn token bridge +maps these values onto the vendored primitives. | Token area | Contract | |---|---| -| Accent | Terracotta selection/focus/CTA/provenance `#d97757`; hover `#c76648`; secondary `#e59579`. shadcn `--primary` = `18 62% 60%` (light) / `18 60% 55%` (dark). | -| Semantic | Success green **only** for completed/healthy — never selection. Amber for held/review. Red for error/failure. | -| Surfaces | Glass DNA: hairline `border-white/[0.06]` (`rgba(255,255,255,.10)` dark / `rgba(0,0,0,.06)` light) over faint fill `bg-white/[0.02]`. App canvas `--bg-primary` `#FFFFFF` / `#111418`. | -| Fonts | UI: Inter → Manrope fallback. Mono (code/trace): JetBrains Mono. | -| Type scale (px) | 11, 12, 13, 14, 16, 18, 20, 24, 30, 36. Eyebrows: 11px uppercase `tracking-[0.15em]`. | -| Radius (px) | 4, 6, 8, 12, 16, 24, pill. `--radius` base 8px; panels 12–16px. | -| Motion | Respect `prefers-reduced-motion`. **motion/react-driven primitives (Shimmer, Reasoning) bypass the CSS override — each needs an explicit `useReducedMotion()` guard.** | - -## Honesty Contract (non-negotiable) - -**No primitive drives a stream.** The live Convex hooks -(`useUIMessages(stream:true)`, `useSmoothText`, `useStream`) remain the data -source; primitives are the presentation layer fed by hook output. A migration -that replaces a live part with a fixture violates the ScratchNode honesty -contract and the `message-live-data` / `MessageBubble.streaming` test invariants. - -## Migration Rule - -Every changed file must be classifiable as exactly one of: - -- **Presentation only** — tokens, class names, layout wrappers, visual - primitives, focus/hover styling. -- **Adapter only** — a component wraps existing props/callbacks/state without - changing behavior. Export API preserved **byte-for-byte**. -- **Tiny compatibility fix** — only if a compile/type error directly requires it - (e.g. the Shiki chunking fix in `vite.config.ts`). - -Any change to API calls, state shape, routing, persistence, auth/session, -Convex functions, agent runtime, or tool behavior is **outside** this migration -unless explicitly approved. - -## AI Elements Adoption - -`migrated` = internals rewritten onto the primitive, export API preserved, tests -green. `wrapped` = primitive wraps existing props/callbacks, domain logic -untouched. `scaffolded` = themed + render-verified, not cut into a live path. -`keep_custom` = intentionally NOT adopted (domain/proof-shaped). Full mapping: -[`docs/architecture/AI_ELEMENTS_MIGRATION.md`](../architecture/AI_ELEMENTS_MIGRATION.md) -(56 files: 8 migrate / 18 wrap / 30 keep_custom). - -| Primitive | Consumer / replaced file | Status | KEEP / REFINE | Notes | +| Accent | Terracotta selection/focus/CTA/provenance `#d97757`; hover `#c76648`; secondary `#e59579`. | +| Semantic | Success green only for completed/healthy. Amber for held/review. Red for error/failure. | +| Surfaces | Warm canvas plus glass hairline/faint fill; avoid saturated cards. | +| Fonts | UI: Inter with Manrope fallback. Code/trace: JetBrains Mono. | +| Type scale | 11, 12, 13, 14, 16, 18, 20, 24, 30, 36px. Eyebrows: 11px uppercase with controlled tracking. | +| Radius | 4, 6, 8, 12, 16, 24px and pill. Base `--radius` is 8px. | +| Motion | Respect `prefers-reduced-motion`; motion-driven primitives need explicit `useReducedMotion()` handling. | + +## Honesty contract + +**No primitive drives a stream.** `useUIMessages(stream:true)`, `useSmoothText`, +and persistent `useStream` subscriptions remain the data sources. Primitives +present hook output. Replacing a live part with a static string or fixture +violates the message-live-data and streaming invariants. + +## Migration rule + +Every change must be one of: + +- **Presentation only:** tokens, classes, layout, focus/hover, or primitive + composition. +- **Adapter only:** wraps existing props, callbacks, and state without changing + product behavior or exports. +- **Tiny compatibility fix:** the minimum compile/build correction directly + required by the migration. + +API calls, state shape, routing, navigation, persistence, auth/session, Convex +functions, agent runtime, and tool behavior are out of scope unless separately +approved. + +## Program status + +The delivery scoreboard is **11/26 complete**. The broader **56-file migration +is ongoing**. The denominator is the original 26 candidate rows: 8 migrate, 17 +wrap, and the HumanRequestCard wrap-to-keep re-evaluation row. HumanRequestCard +remains operationally keep-custom. The shared `convexToUIParts` adapter is +required foundation but tracked separately; the number is not a claim that all +11 components are production-live. + +`migrated` means internals use the primitive. `wrapped` means the primitive +wraps existing behavior. `scaffolded` means available and themed but not cut +into the target. `keep_custom` is an intentional boundary. + +| Primitive | Consumer | Adoption | Canonical source | Evidence boundary | |---|---|---|---|---| -| `shimmer` | TypingIndicator | **migrated** | KEEP | `useReducedMotion()` guard added — motion/react bypasses the CSS reduced-motion rule. | -| `reasoning` | ThoughtBubble | **migrated** | KEEP | Collapsible "Thought for…" disclosure; static trigger under reduced motion. | -| `suggestion` | QuickCommandChips | **migrated** | KEEP | `cmd.navigate → window.location.assign` branch preserved via `commandsById` lookup. | -| `code-block` | LazySyntaxHighlighter | **migrated** | REFINE | Shiki, lazy; grammars routed to `assets/shiki/` + globIgnored from precache (see below). | -| `task` | AgentHierarchy | **migrated** | KEEP | `startedAt/completedAt` timing preserved. | -| `sources` + `inline-citation` | SourceCard | **migrated** | KEEP | Rich preview extras stay in wrapper. | -| `message`+`reasoning`+`tool`+`sources` | UIMessageBubble (live, 140KB) | wrapped (planned) | REFINE | **Crown jewel.** Wrap generic sub-parts only; domain cards pass through `renderCustomPart`. Guard with `MessageBubble.streaming.test`. | -| `prompt-input`+`context`+`model-selector` | InputBar (live) | wrapped (planned) | REFINE | `onSubmit` delegates to `onSend/onStop/onSpawn`; slash/mentions/voice/drag-drop stay custom. | -| `checkpoint` | (restore/version-jump) | scaffolded | KEEP | The primitive named in the original ask; maps to restore-checkpoint. | -| `plan` / `artifact` / `web-preview` / `context` | GoalCard / DocumentActionCard / FileViewer / TokenUsageBadge | scaffolded | REFINE | Evaluate per matrix; `web-preview` keeps per-fileType sandbox (security). | -| `confirmation` | HumanRequestCard | **keep_custom** | — | No textarea/multi-option; HITL approval + CAS semantics stay custom. | - -### Shiki bundle guard (code-block) - -`code-block` uses Shiki (~200 TextMate grammars). Two build invariants, both in +| `shimmer` | TypingIndicator | migrated | #516 · `c83a41c8` | source merged | +| `reasoning` | ThoughtBubble | migrated | #516 · `c83a41c8` | source merged | +| `suggestion` | QuickCommandChips | migrated | #516 · `c83a41c8` | source merged | +| `code-block` | LazySyntaxHighlighter | migrated | #516 · `c83a41c8`; cache fix #517 · `988a3f56` | source merged; Shiki build guard applies | +| `task` | AgentHierarchy | wrapped | #516 · `c83a41c8` | source merged | +| `sources` + `inline-citation` | SourceCard | wrapped | #516 · `c83a41c8` | source merged | +| adapter | `convexToUIParts` | merged | #521 · `30688119` | identity-preserving data seam; not a visual surface | +| `task` + `chain-of-thought` + `reasoning` + `tool` | CollapsibleAgentProgress | wrapped | #523 · `a4fe5ee3` | source migration only; **no live-render claim** | +| `tool` | ToolCallTransparency | migrated | #524 · `64203ded` | source merged | +| `message` + `reasoning` + `tool` + `sources` | UIMessageBubble | wrapped | #525 · `165ecec2` | source merged; production-live proof tracked separately | +| `prompt-input` + `context` + `model-selector` | InputBar | wrapped | #526 · `3cc7cd06` | source merged; production-live proof tracked separately | +| `tool` + task connector | LiveEventCard | migrated | #527 · `28d704b2` | source merged; visual and production-live proof tracked separately | +| `checkpoint` | restore/version jump | scaffolded | #516 · `c83a41c8` | not cut into a target | +| `plan` / `artifact` / `web-preview` / `context` | GoalCard / DocumentActionCard / FileViewer / TokenUsageBadge | scaffolded | #516 · `c83a41c8` | evaluate per 56-file matrix | +| `confirmation` | HumanRequestCard | keep_custom | n/a | textarea, multi-option, cancel, and decision semantics remain custom | + +Full mapping and remaining sequence: +[`docs/architecture/AI_ELEMENTS_MIGRATION.md`](../architecture/AI_ELEMENTS_MIGRATION.md). + +## Keep-custom boundary + +Keep VirtualizedMessageList, StreamingStatus, VisualCitation, +ParallelTaskTimeline, SwarmLanesView, HumanRequestCard, FusedSearchResults, +ResourceLinkCard, MermaidDiagram, MediaGallery, all domain selection cards, +memory cards, and verification reports custom. A generic primitive may frame a +surface only when every domain callback, proof affordance, security rule, and +live subscription survives unchanged. + +## Shiki bundle guard + +`code-block` uses Shiki's lazy grammar/theme chunks. Two invariants live in `vite.config.ts`: -1. **Never force-group `@shikijs/*` into one `manualChunk`** — that collapses - Shiki's per-language dynamic splitting into a single ~10 MB blob that - overflows the PWA 2 MiB precache limit and **fails the build**. Leave it - unmatched so each grammar is its own lazy chunk. -2. **Route grammar chunks to `assets/shiki/` and `globIgnore` them from - precache** — they lazy-load on demand (graceful plaintext fallback offline), - keeping ~7.4 MB of rarely-used grammars out of the first-visit SW precache. - -_Known follow-up:_ curating Shiki to a language allowlist would shrink dist -further, but is blocked at the package level — shiki 4.x `exports` has no -`./langs/*` wildcard, so fine-grained per-language imports are not -Vite-resolvable. The chunking + globIgnore guard above is the shipped fix. - -## Proof - -The `ui-contract/` sibling directory holds dated visual-contract evidence -(before/after screenshots + capture manifests + Gemini QA receipts), following -the NodeRoom pattern. See [`ui-contract/README.md`](ui-contract/README.md) for -the capture protocol and manifest schema. - -Design parity for a slice is not complete until all are true: - -- Each migrated region has before/after screenshots under a dated `ui-contract/` folder. -- `npx tsc --noEmit`, the touched Vitest suites, and `npm run build` pass (or - failures are documented as pre-existing baseline). -- `npm run lint:design` reports no NEW high-severity drift on the AI surface. -- Browser verification uses normal product paths — not static screenshots alone. - -## Migration Status - -**2026-07-14 — Batch 1 (6 leaf components) shipped + verified:** - -- Migrated: TypingIndicator→shimmer, ThoughtBubble→reasoning, QuickCommandChips→suggestion, - LazySyntaxHighlighter→code-block, AgentHierarchy→task, SourceCard→sources+inline-citation. -- Verified: `tsc` 0 errors · FastAgentPanel suite 89 passed / 0 failed (incl. SourceCard's 15) · `npm run build` exit 0. -- Fixed a build regression the batch introduced (Shiki bundle — see guard above). -- **Live-render verified** on the production bundle (`vite preview`): landing + Chat - surface render on-brand (terracotta, glass DNA) with **zero console errors**; the - migrated `reasoning` disclosure and `suggestion` chips render in the live Chat - surface. Durable before/after PNG capture via `npm run dogfood:full:local` → - `ui-contract/20260714-ai-elements-batch1/` is the pending proof-folder step. -- Commit `776c4868` (batch + fix) · `f0b0094e` (design/contract dirs). - -Next: the live `UIMessageBubble` + `InputBar` wraps (Phases 2–3), each guarded by -the streaming tests and cut over only after e2e content assertions + live verification. +1. Never force-group `@shikijs/*` into one `manualChunk`; doing so destroys + per-language splitting and can exceed the PWA precache limit. +2. Route grammar/theme chunks to `assets/shiki/` and exclude that directory + from precache. They remain runtime-lazy with plaintext fallback behavior. + +Measure the exact candidate build. Historical chunk sizes are context, not +evidence for the current revision. + +## Evidence states + +Use the following labels literally: + +- **source merged** — canonical SHA is on `origin/main`; +- **checks verified** — named commands passed on that exact SHA; +- **visual proof complete** — real files plus a schema-valid manifest exist; +- **preview verified** — a normal product path was browser-driven on preview; +- **production live verified** — the post-merge production deployment and + rendered bundle were checked directly. + +Do not infer one state from another. Never create placeholder PNGs, QA receipts, +findings, Agentic UI Bar scores, or live claims. As of this source revision, +[`docs/design/ui-contract/`](ui-contract/) contains the protocol and schema but +no dated proof folder, so visual-proof-complete is not claimed here. + +## Migration ledger + +- #516 `c83a41c8`: scaffold, six leaf cutovers, governance, and Vite Shiki guard. +- #517 `988a3f56`: prevent equal-length code-token cache collisions by keying on + the full source. +- #521 `30688119`: add the shared identity-preserving message-parts adapter. +- #523 `a4fe5ee3`: wrap CollapsibleAgentProgress; source state only. +- #524 `64203ded`: migrate ToolCallTransparency. +- #525 `165ecec2`: wrap UIMessageBubble while retaining live hooks and domain + passthrough. +- #526 `3cc7cd06`: wrap InputBar and make send-contract forwarding explicit. +- #527 `28d704b2`: migrate LiveEventCard, centralize live-event derivation, and + extract the panel header without changing the 11/26 keep-custom boundary. + +Release guard support also landed in #519 (`00e5594d`), #520 (`35a7b85d`), and +#522 (`ad2b26c6`). Final visual and production evidence must be recorded against +the exact revision it verifies; this governance update makes no such claim. diff --git a/docs/design/ui-contract/README.md b/docs/design/ui-contract/README.md index af304f16..b26b77c1 100644 --- a/docs/design/ui-contract/README.md +++ b/docs/design/ui-contract/README.md @@ -1,52 +1,77 @@ # UI Contract — Visual Evidence -Dated, append-only visual-contract evidence for NodeBench's UI migrations -(currently the AI Elements adoption). Governed by -[`../UI_CONTRACT.md`](../UI_CONTRACT.md); machine-readable companion is -[`src/design/designSystem.ts`](../../../src/design/designSystem.ts). +This directory is the append-only evidence store for NodeBench UI migrations. +It is governed by [`../UI_CONTRACT.md`](../UI_CONTRACT.md); the machine-readable +companion is [`src/design/designSystem.ts`](../../../src/design/designSystem.ts). -Pattern borrowed from NodeRoom's `docs/design/ui-contract/` — each capture is a -dated folder holding screenshots + a `manifest.json` describing what was shot, -plus (for migrations) before/after pairs and Gemini QA receipts. +At the current `origin/main` boundary through PR #527, this directory contains +the protocol and schema only. No dated proof folder is present, so this document +does not claim screenshots, QA findings, an Agentic UI Bar score, preview +verification, or production-live verification. + +## Evidence states + +Record each state independently: + +| State | Minimum evidence | +|---|---| +| source merged | canonical main SHA and PR | +| checks verified | exact command, result, and tested SHA | +| visual proof complete | real before/after files plus schema-valid manifest | +| preview verified | normal product route, preview URL/build, browser assertions | +| production live verified | post-merge production URL, deployed revision/bundle, browser assertions | + +Never promote one state based on another. A build is not a live check; a +screenshot is not a deployment check; a branch SHA is not the canonical main +SHA after squash merge. ## Folder convention -``` +```text ui-contract/ - README.md ← this file (the protocol) - manifest.schema.json ← the manifest shape every capture folder follows - YYYYMMDD-/ ← one folder per capture session - manifest.json ← required: what/where/how it was captured - -.png ← source-design or "after" screenshots - before--.png ← (migration proof) the pre-change baseline - after--.png ← (migration proof) the post-change result - -qa.json ← (optional) Gemini QA receipt for that shot + README.md + manifest.schema.json + YYYYMMDD-/ + manifest.json + before--.png + after--.png + -qa.json # optional, only when actually produced ``` -Slug examples: `20260714-ai-elements-batch1`, `20260714-flagship-conversation`. -Viewports: desktop `1456x940` / `1280x900`, mobile `390x844`. Dark + light both -where theming changed. +Useful viewports include desktop `1456x940` or `1280x900` and mobile `390x844`. +Capture light and dark only where those states are actually exercised. ## Capture protocol -1. Build + serve the real app (never a static mock): +1. Start from a clean worktree at the exact baseline or candidate revision. +2. Build and serve the real app: `npm run build && npx vite preview --host 127.0.0.1 --port 4173`. -2. Drive **normal product paths** to the migrated surface (the FastAgentPanel - agent chat), in dark and light, desktop and mobile 390px. -3. Screenshot before (from `origin/main` or the pre-batch commit) and after. - The repo's dogfood tooling already automates capture + publish: - `npm run dogfood:full:local` (or `scripts/ui/recordDogfoodWalkthrough.mjs`). -4. (Optional but preferred) score with the Gemini QA loop - (`scripts/ui/runDogfoodGeminiQa.mjs`) and drop the JSON receipt beside the shot. -5. Write `manifest.json` (schema below) and reference the folder from - `UI_CONTRACT.md` → Migration Status. - -## Proof requirement - -A migration slice is not "shipped" until its `ui-contract/YYYYMMDD-*` folder -exists with before/after screenshots and a manifest, AND `UI_CONTRACT.md` links -it. Per `live_dom_verification`, screenshots alone do not prove "live" — pair -them with a live-path browser verification. +3. Drive the normal product path to the reachable changed region. Preserve web + navigation as `Home - Reports - Chat - Inbox - Me`; Workspace remains a + separate deployed surface. +4. Capture before and after from the stated revisions. Do not substitute a + static mock, story, or unrelated route for a live product seam. +5. If a scorer is run, store its actual receipt. If it is not run, omit the + receipt and score; never invent a score or finding. +6. Write `manifest.json`, validate it against `manifest.schema.json`, and ensure + every referenced file exists before linking the folder from UI_CONTRACT. +7. If claiming preview or production state, separately record the URL, deployed + revision/bundle evidence, browser assertions, and console result. + +Dead or unreachable exports may have source/test evidence, but must not be +described as live. In particular, a CollapsibleAgentProgress unit render or +successful build is not proof that the component rendered in production. + +## Proof integrity rules + +- Never create placeholder PNGs, empty receipts, synthetic manifests, or copied + screenshots solely to satisfy the folder convention. +- Never cite a path that does not exist. +- Never reuse a result from a different SHA without labeling it historical. +- Keep domain and proof cards visible in any chat capture; do not replace live + Convex data with fixtures to make a screenshot easier. +- Redact secrets and private content without changing the functional state being + verified. ## Manifest shape @@ -56,23 +81,22 @@ See [`manifest.schema.json`](manifest.schema.json). Minimal example: { "schema": "nodebench-ui-contract-v1", "capturedAt": "2026-07-14T00:00:00.000Z", - "slice": "ai-elements-batch1", + "slice": "ai-elements-example", "app": "nodebench-ai", "localServer": "http://127.0.0.1:4173", - "policy": "before/after proof for a behavior-preserving migration; production parity verified via normal product paths separately", - "commitBefore": "776c4868~1", - "commitAfter": "776c4868", + "policy": "before/after proof for a behavior-preserving migration", + "commitBefore": "", + "commitAfter": "", "pages": [ { - "id": "fast-agent-panel-typing", - "label": "Agent chat — streaming", - "purpose": "shimmer + reasoning primitives during a live stream", + "id": "fast-agent-panel-chat", + "label": "Agent chat", + "purpose": "reachable migrated region under normal product navigation", "route": "/?surface=ask", "viewport": { "width": 1456, "height": 940 }, "theme": "dark", - "before": "before-fast-agent-panel-typing-1456x940.png", - "after": "after-fast-agent-panel-typing-1456x940.png", - "qa": "fast-agent-panel-typing-qa.json" + "before": "before-fast-agent-panel-chat-1456x940.png", + "after": "after-fast-agent-panel-chat-1456x940.png" } ] } diff --git a/src/design/designSystem.test.ts b/src/design/designSystem.test.ts index 21085cf3..d22da599 100644 --- a/src/design/designSystem.test.ts +++ b/src/design/designSystem.test.ts @@ -60,6 +60,35 @@ describe("Scenario: a coding agent reads the manifest before migrating", () => { it("scopes governance to the AI surface, not the whole app", () => { expect(aiSurfaceRoots).toContain("src/components/ai-elements"); expect(aiSurfaceRoots).toContain("src/features/agents/components/ai"); + expect(aiSurfaceRoots).toContain("src/features/agents/components/FastAgentPanel"); + }); + + it("records the current migration scoreboard without closing the 56-file program", () => { + expect(m.data.migration).toMatchObject({ + matrixFiles: 56, + totalMilestones: 26, + completedMilestones: 11, + status: "ongoing", + canonicalMainThrough: { pullRequest: 527, commit: "28d704b2" }, + }); + expect(m.data.migration.completedUnits).toHaveLength(11); + expect(m.data.migration.completedUnits).toContain("UIMessageBubble"); + expect(m.data.migration.completedUnits).toContain("InputBar"); + expect(m.data.migration.completedUnits).toContain("LiveEventCard"); + expect(m.data.migration.countingRule).toContain("8 migrate, 17 wrap"); + expect(m.data.migration.countingRule).toContain("HumanRequestCard"); + }); + + it("marks the merged live adapters and the non-live progress shell honestly", () => { + const byPrimitive = new Map(m.data.primitives.map((rule) => [rule.primitive, rule])); + expect(byPrimitive.get("message + reasoning + tool + sources")?.adoption).toBe("wrapped"); + expect(byPrimitive.get("prompt-input + context + model-selector")?.adoption).toBe("wrapped"); + expect(byPrimitive.get("tool")?.adoption).toBe("migrated"); + expect(byPrimitive.get("tool + task connector")?.adoption).toBe("migrated"); + + const progress = byPrimitive.get("task + chain-of-thought + reasoning + tool"); + expect(progress?.adoption).toBe("wrapped"); + expect(progress?.must.join(" ").toLowerCase()).toContain("not a live-surface claim"); }); }); diff --git a/src/design/designSystem.ts b/src/design/designSystem.ts index 06f7f182..c83c6bc6 100644 --- a/src/design/designSystem.ts +++ b/src/design/designSystem.ts @@ -91,6 +91,7 @@ export const nbRadiusScalePx = [4, 6, 8, 12, 16, 24, 9999] as const; export const aiSurfaceRoots = [ "src/components/ai-elements", // 24 vendored AI Elements primitives (we own them) "src/features/agents/components/ai", // thin consumers: AiMessage, AiConversation, AiPromptInput + "src/features/agents/components/FastAgentPanel", // live adapters + preserved domain renderers ] as const; /* ─── Manifest types ─────────────────────────────────────────────────────── */ @@ -98,7 +99,7 @@ export const aiSurfaceRoots = [ export type AiPrimitiveAdoption = | "migrated" // internals rewritten onto the primitive; export API preserved | "wrapped" // primitive wraps existing props/callbacks; domain logic untouched - | "scaffolded" // themed + render-verified, not yet cut into a live path + | "scaffolded" // themed + available, not yet cut into the target path | "planned" // decided target, not yet built | "keep_custom_boundary"; // primitive intentionally NOT adopted here; custom stays @@ -122,6 +123,15 @@ export type AiDesignManifest = { inspiration: Array<{ source: string; appliedAs: string }>; principles: string[]; tokens: Array<{ name: string; role: string; value: string }>; + migration: { + matrixFiles: 56; + totalMilestones: 26; + completedMilestones: 11; + status: "ongoing"; + canonicalMainThrough: { pullRequest: 527; commit: "28d704b2" }; + completedUnits: string[]; + countingRule: string; + }; primitives: AiPrimitiveRule[]; auditChecks: string[]; }; @@ -170,6 +180,28 @@ export function getNodeBenchAiDesignManifest(): AiDesignManifest { { name: "--success", role: "completed / healthy only — never selection", value: nbSemantic.success }, { name: "--radius", role: "default container radius (8px)", value: "0.5rem" }, ], + migration: { + matrixFiles: 56, + totalMilestones: 26, + completedMilestones: 11, + status: "ongoing", + canonicalMainThrough: { pullRequest: 527, commit: "28d704b2" }, + completedUnits: [ + "TypingIndicator", + "ThoughtBubble", + "QuickCommandChips", + "LazySyntaxHighlighter", + "AgentHierarchy", + "SourceCard", + "CollapsibleAgentProgress", + "ToolCallTransparency", + "UIMessageBubble", + "InputBar", + "LiveEventCard", + ], + countingRule: + "The 11/26 scoreboard uses the original 26 candidate rows: 8 migrate, 17 wrap, and the HumanRequestCard wrap-to-keep re-evaluation row. The other 30 rows are explicit keep_custom. HumanRequestCard remains operationally keep-custom unless a future contract review preserves all HITL semantics. The shared convexToUIParts adapter is required foundation but is tracked outside the fraction.", + }, primitives: [ { primitive: "shimmer", @@ -184,8 +216,11 @@ export function getNodeBenchAiDesignManifest(): AiDesignManifest { }, { primitive: "reasoning", - consumers: ["src/features/agents/components/FastAgentPanel/FastAgentPanel.ThoughtBubble.tsx"], - role: "Collapsible 'Thought for…' disclosure; auto-opens while streaming.", + consumers: [ + "src/features/agents/components/FastAgentPanel/FastAgentPanel.ThoughtBubble.tsx", + "src/features/agents/components/FastAgentPanel/CollapsibleAgentProgress.tsx", + ], + role: "Reasoning disclosures, including the migrated thought leaf and wrapped agent-progress shell.", adoption: "migrated", must: ["Wire isStreaming through to .", "Pass a static trigger message under reduced motion."], avoid: ["Duplicating Reasoning and ChainOfThought in the same turn."], @@ -229,8 +264,11 @@ export function getNodeBenchAiDesignManifest(): AiDesignManifest { }, { primitive: "message + reasoning + tool + sources", - consumers: ["src/features/agents/components/ai/AiMessage.tsx (consumer)", "UIMessageBubble (live, wrap target)"], - role: "The live UIMessage bubble: parts → text / reasoning / tool / sources, with domain cards passed through.", + consumers: [ + "src/features/agents/components/ai/AiMessage.tsx", + "src/features/agents/components/FastAgentPanel/FastAgentPanel.UIMessageBubble.tsx", + ], + role: "The active-path UIMessage bubble: parts → text / reasoning / tool / sources, with domain cards passed through.", adoption: "wrapped", must: [ "Wrap generic sub-parts ONLY; domain cards (selection, arbitrage, media, GoalCard) render unchanged inside MessageContent via a renderCustomPart passthrough.", @@ -241,7 +279,10 @@ export function getNodeBenchAiDesignManifest(): AiDesignManifest { }, { primitive: "prompt-input + context + model-selector", - consumers: ["src/features/agents/components/ai/AiPromptInput.tsx (consumer)", "InputBar (live, wrap target)"], + consumers: [ + "src/features/agents/components/ai/AiPromptInput.tsx", + "src/features/agents/components/FastAgentPanel/FastAgentPanel.InputBar.tsx", + ], role: "Composer: autoresize, send/stop, attachments, model, slash/@mentions/voice.", adoption: "wrapped", must: [ @@ -250,6 +291,39 @@ export function getNodeBenchAiDesignManifest(): AiDesignManifest { ], avoid: ["Replacing the live send path with the primitive's own state."], }, + { + primitive: "task + chain-of-thought + reasoning + tool", + consumers: ["src/features/agents/components/FastAgentPanel/CollapsibleAgentProgress.tsx"], + role: "Agent-progress answer/process disclosure while preserving the ToolUIPart feed.", + adoption: "wrapped", + must: [ + "Preserve the exported component contract and toolParts: ToolUIPart[] feed.", + "Describe this as a source migration, not a live-surface claim; the component is not a proven production render path.", + ], + avoid: ["Claiming live verification from unit coverage or a successful build."], + }, + { + primitive: "tool", + consumers: ["src/features/agents/components/FastAgentPanel/ToolCallTransparency.tsx"], + role: "MCP tool-call disclosure with input, output, and error states.", + adoption: "migrated", + must: [ + "Preserve running/success/error semantics when mapping to input-available/output-available/output-error.", + "Keep tool arguments and results available to the existing transparency surface.", + ], + avoid: ["Treating an error result as a completed or success state."], + }, + { + primitive: "tool + task connector", + consumers: ["src/features/agents/components/FastAgentPanel/LiveEventCard.tsx"], + role: "Live agent-event cards fed by the panel's shared live-event derivation.", + adoption: "migrated", + must: [ + "Feed the card from derived panel events; never introduce fixture fallback in the production path.", + "Preserve event status, summary, timing, and task-connector semantics.", + ], + avoid: ["Treating source merge or post-deploy health as visual-proof-complete."], + }, { primitive: "confirmation", consumers: ["HumanRequestCard (keep_custom candidate)"], @@ -275,6 +349,8 @@ export function getNodeBenchAiDesignManifest(): AiDesignManifest { "code-block stays lazy and @shikijs/* is never force-grouped into one chunk", "no primitive drives a live stream — Convex hooks remain the data source (honesty contract)", "success-green is reserved for completed state and never used for selection", + "source-merged, visual-proof-complete, and production-live-verified are distinct evidence states", + "web navigation remains Home - Reports - Chat - Inbox - Me; Workspace remains a separate deployed surface", ], }, }; From d882354c2f0708cab7c59e329806b0d989dfa6d8 Mon Sep 17 00:00:00 2001 From: hshum Date: Tue, 14 Jul 2026 06:17:12 -0700 Subject: [PATCH 2/2] fix(agents): project strict query summaries --- AGENT_COORDINATION.md | 21 +- .../__tests__/agentPlanning.listPlans.test.ts | 233 ++++++++++++++++++ convex/domains/agents/agentMemory.ts | 12 +- convex/domains/agents/agentMemorySummary.ts | 49 ++++ convex/domains/agents/agentPlanSummary.ts | 27 ++ convex/domains/agents/agentPlanning.ts | 5 +- .../FastAgentPanel/FastAgentPanel.tsx | 46 ---- 7 files changed, 327 insertions(+), 66 deletions(-) create mode 100644 convex/__tests__/agentPlanning.listPlans.test.ts create mode 100644 convex/domains/agents/agentMemorySummary.ts create mode 100644 convex/domains/agents/agentPlanSummary.ts diff --git a/AGENT_COORDINATION.md b/AGENT_COORDINATION.md index 5102ea13..0bc608f5 100644 --- a/AGENT_COORDINATION.md +++ b/AGENT_COORDINATION.md @@ -57,21 +57,12 @@ Server Error) for an unknown slug. ## Active claims (who is editing what RIGHT NOW) -- **2026-07-14 · Codex release captain →** `.github/workflows/tier-b-preview.yml`, - `.github/workflows/post-deploy-verify.yml`, `scripts/post-deploy-verify.mjs`, - `tests/e2e/live-smoke.spec.ts`, release runbooks, and final migration governance/proof · - repair fail-closed release truth and own the serial merge/deploy train · branch - `fix/release-verification-fail-closed`. -- **2026-07-14 · Codex message lane →** - `src/features/agents/components/FastAgentPanel/adapters/*`, direct message contract tests, - and `FastAgentPanel.UIMessageBubble.tsx` · build the identity-preserving Convex-to-AI-parts - adapter and the live bubble wrap · branch `codex/ai-elements-message`. -- **2026-07-14 · Codex tools lane →** `CollapsibleAgentProgress.tsx`, - `ToolCallTransparency.tsx`, `LiveEventCard.tsx`, and their dedicated tests · migrate the - reasoning/tool leaves with honest reachability · branch `codex/ai-elements-tools`. -- **2026-07-14 · Codex composer lane →** `FastAgentPanel.InputBar.tsx`, its direct tests, - and the minimal `FastAgentPanel.tsx` send-forwarding seam · preserve every composer - callback while wrapping the live input · branch `codex/ai-elements-composer`. +- **2026-07-14 · Codex release captain →** + `convex/domains/agents/agentPlanning.ts`, `agentMemory.ts`, their exact public-summary + projectors/tests, `FastAgentPanel.tsx#unused-ambient-subscriptions`, and final AI Elements + governance/proof · repair the signed-in production query-validator crash discovered by + live dogfood, then own the serial P0 merge/deploy/evidence train · branch + `fix/agent-query-summary-contracts`. - **2026-06-03 · Claude →** `convex/*#handoff-token`, `src/.../ScratchnodePrivateBridge`, `src/App.tsx#events-private-route`, `public/proto/home-v5.html#private-handoff` · diff --git a/convex/__tests__/agentPlanning.listPlans.test.ts b/convex/__tests__/agentPlanning.listPlans.test.ts new file mode 100644 index 00000000..1dad4e5f --- /dev/null +++ b/convex/__tests__/agentPlanning.listPlans.test.ts @@ -0,0 +1,233 @@ +import { describe, expect, it } from "vitest"; +import { + getEpisodicByRunId, + listMemory, + readMemory, +} from "../domains/agents/agentMemory"; +import { projectAgentPlanSummary } from "../domains/agents/agentPlanSummary"; +import { + projectAgentEpisodicSummary, + projectAgentMemorySummary, +} from "../domains/agents/agentMemorySummary"; +import { getPlan, listPlans } from "../domains/agents/agentPlanning"; + +function authenticatedQueryContext(rows: unknown[]) { + return { + auth: { + getUserIdentity: async () => ({ subject: "user-qa|session-qa" }), + }, + db: { + query: () => ({ + withIndex: () => ({ + order: () => ({ + take: async () => rows, + }), + }), + }), + }, + }; +} + +function authenticatedGetContext(row: unknown) { + return { + auth: { + getUserIdentity: async () => ({ subject: "user-qa|session-qa" }), + }, + db: { + get: async () => row, + }, + }; +} + +function authenticatedFirstContext(row: unknown) { + return { + auth: { + getUserIdentity: async () => ({ subject: "user-qa|session-qa" }), + }, + db: { + query: () => ({ + withIndex: () => ({ + first: async () => row, + }), + }), + }, + }; +} + +describe("agentPlanning.listPlans summary projection", () => { + it("strips initializer-only and database metadata fields", () => { + const initializerPlan = { + _id: "plan-qa", + _creationTime: 3, + userId: "user-qa", + agentThreadId: "thread-qa", + goal: "Verify the production plan subscription", + steps: [{ description: "Search the source", status: "pending" as const }], + features: [ + { + name: "search", + status: "pending", + testCriteria: "A source is returned", + }, + ], + progressLog: [{ ts: 1, status: "info", message: "seeded" }], + createdAt: 1, + updatedAt: 2, + }; + const summary = projectAgentPlanSummary(initializerPlan); + + expect(summary).toEqual({ + _id: "plan-qa", + userId: "user-qa", + goal: "Verify the production plan subscription", + steps: [{ description: "Search the source", status: "pending" }], + createdAt: 1, + updatedAt: 2, + }); + expect(Object.keys(summary).sort()).toEqual([ + "_id", + "createdAt", + "goal", + "steps", + "updatedAt", + "userId", + ]); + }); + + it("projects rows in the registered listPlans handler", async () => { + const initializerPlan = { + _id: "plan-qa", + _creationTime: 3, + userId: "user-qa", + agentThreadId: "thread-qa", + goal: "Verify the production plan subscription", + steps: [{ description: "Search the source", status: "pending" as const }], + features: [], + progressLog: [], + createdAt: 1, + updatedAt: 2, + }; + + const result = await (listPlans as any)._handler( + authenticatedQueryContext([initializerPlan]), + { limit: 5 }, + ); + + expect(result).toEqual([projectAgentPlanSummary(initializerPlan)]); + }); + + it("projects a row in the registered getPlan handler", async () => { + const initializerPlan = { + _id: "plan-qa", + _creationTime: 3, + userId: "user-qa", + agentThreadId: "thread-qa", + goal: "Verify the production plan subscription", + steps: [{ description: "Search the source", status: "pending" as const }], + features: [], + progressLog: [], + createdAt: 1, + updatedAt: 2, + }; + + const result = await (getPlan as any)._handler( + authenticatedGetContext(initializerPlan), + { planId: "plan-qa" }, + ); + + expect(result).toEqual(projectAgentPlanSummary(initializerPlan)); + }); +}); + +describe("agentMemory public summary projection", () => { + it("strips database metadata while preserving optional metadata", () => { + const storedMemory = { + _id: "memory-qa", + _creationTime: 3, + userId: "user-qa", + key: "constraint:region", + content: "US only", + metadata: { source: "qa" }, + createdAt: 1, + updatedAt: 2, + }; + const summary = projectAgentMemorySummary(storedMemory); + + expect(summary).toEqual({ + _id: "memory-qa", + userId: "user-qa", + key: "constraint:region", + content: "US only", + metadata: { source: "qa" }, + createdAt: 1, + updatedAt: 2, + }); + expect(summary).not.toHaveProperty("_creationTime"); + }); + + it("projects rows in the registered listMemory handler", async () => { + const storedMemory = { + _id: "memory-qa", + _creationTime: 3, + userId: "user-qa", + key: "scratchpad", + content: "Remember this", + createdAt: 1, + updatedAt: 2, + }; + + const result = await (listMemory as any)._handler( + authenticatedQueryContext([storedMemory]), + { limit: 10 }, + ); + + expect(result).toEqual([projectAgentMemorySummary(storedMemory)]); + }); + + it("projects a row in the registered readMemory handler", async () => { + const storedMemory = { + _id: "memory-qa", + _creationTime: 3, + userId: "user-qa", + key: "scratchpad", + content: "Remember this", + metadata: { source: "qa" }, + createdAt: 1, + updatedAt: 2, + }; + + const result = await (readMemory as any)._handler( + authenticatedFirstContext(storedMemory), + { key: "scratchpad" }, + ); + + expect(result).toEqual(projectAgentMemorySummary(storedMemory)); + }); +}); + +describe("agentMemory episodic summary projection", () => { + it("projects registered getEpisodicByRunId rows onto its strict return contract", async () => { + const ownEntry = { + _id: "episode-own", + _creationTime: 3, + runId: "run-qa", + userId: "user-qa", + ts: 2, + tags: ["verification"], + data: { passed: true }, + }; + const otherEntry = { + ...ownEntry, + _id: "episode-other", + userId: "other-user", + }; + + const result = await (getEpisodicByRunId as any)._handler( + authenticatedQueryContext([ownEntry, otherEntry]), + { runId: "run-qa", limit: 10 }, + ); + + expect(result).toEqual([projectAgentEpisodicSummary(ownEntry)]); + expect(result[0]).not.toHaveProperty("_creationTime"); + }); +}); diff --git a/convex/domains/agents/agentMemory.ts b/convex/domains/agents/agentMemory.ts index 0f25bb66..f4422c28 100644 --- a/convex/domains/agents/agentMemory.ts +++ b/convex/domains/agents/agentMemory.ts @@ -31,6 +31,10 @@ import { } from "../../_generated/server"; import type { Doc } from "../../_generated/dataModel"; import { getAuthUserId } from "@convex-dev/auth/server"; +import { + projectAgentEpisodicSummary, + projectAgentMemorySummary, +} from "./agentMemorySummary"; // Avoid TS2589 "excessively deep" instantiations in large-schema projects. const mutation = mutationBase as any; @@ -115,7 +119,7 @@ export const readMemory = query({ .withIndex("by_user_key", (q) => q.eq("userId", userId).eq("key", args.key)) .first(); - return memory || null; + return memory ? projectAgentMemorySummary(memory) : null; }, }); @@ -150,7 +154,7 @@ export const listMemory = query({ ? await query.take(args.limit) : await query.take(50); - return memories; + return memories.map(projectAgentMemorySummary); }, }); @@ -327,7 +331,9 @@ export const getEpisodicByRunId = query({ .order("desc") .take(limit); - return items.filter((e) => e.userId === userId); + return items + .filter((e) => e.userId === userId) + .map(projectAgentEpisodicSummary); }, }); diff --git a/convex/domains/agents/agentMemorySummary.ts b/convex/domains/agents/agentMemorySummary.ts new file mode 100644 index 00000000..11a54aa9 --- /dev/null +++ b/convex/domains/agents/agentMemorySummary.ts @@ -0,0 +1,49 @@ +type AgentMemorySummarySource = { + _id: unknown; + userId: unknown; + key: string; + content: string; + metadata?: unknown; + createdAt: number; + updatedAt: number; +}; + +/** Project stored memory documents onto the strict public query contract. */ +export function projectAgentMemorySummary(memory: T) { + const summary = { + _id: memory._id, + userId: memory.userId, + key: memory.key, + content: memory.content, + createdAt: memory.createdAt, + updatedAt: memory.updatedAt, + }; + + return memory.metadata === undefined + ? summary + : { ...summary, metadata: memory.metadata }; +} + +type AgentEpisodicSummarySource = { + _id: unknown; + runId: string; + userId: unknown; + ts: number; + tags?: string[]; + data: unknown; +}; + +/** Keep episodic reads from leaking database-only `_creationTime`. */ +export function projectAgentEpisodicSummary(entry: T) { + const summary = { + _id: entry._id, + runId: entry.runId, + userId: entry.userId, + ts: entry.ts, + data: entry.data, + }; + + return entry.tags === undefined + ? summary + : { ...summary, tags: entry.tags }; +} diff --git a/convex/domains/agents/agentPlanSummary.ts b/convex/domains/agents/agentPlanSummary.ts new file mode 100644 index 00000000..2e33a7a9 --- /dev/null +++ b/convex/domains/agents/agentPlanSummary.ts @@ -0,0 +1,27 @@ +type AgentPlanSummarySource = { + _id: unknown; + userId: unknown; + goal: string; + steps: Array<{ + description: string; + status: "pending" | "in_progress" | "completed"; + }>; + createdAt: number; + updatedAt: number; +}; + +/** + * Keep public plan reads aligned with their strict Convex return validator. + * Database documents also contain `_creationTime` and may contain initializer + * metadata; returning the raw document makes authenticated subscriptions throw. + */ +export function projectAgentPlanSummary(plan: T) { + return { + _id: plan._id, + userId: plan.userId, + goal: plan.goal, + steps: plan.steps, + createdAt: plan.createdAt, + updatedAt: plan.updatedAt, + }; +} diff --git a/convex/domains/agents/agentPlanning.ts b/convex/domains/agents/agentPlanning.ts index fdfb465f..8c067567 100644 --- a/convex/domains/agents/agentPlanning.ts +++ b/convex/domains/agents/agentPlanning.ts @@ -9,6 +9,7 @@ import { v } from "convex/values"; import { mutation, query } from "../../_generated/server"; import type { Doc } from "../../_generated/dataModel"; import { getAuthUserId } from "@convex-dev/auth/server"; +import { projectAgentPlanSummary } from "./agentPlanSummary"; // Step type for type inference type PlanStep = { @@ -144,7 +145,7 @@ export const getPlan = query({ return null; } - return plan; + return projectAgentPlanSummary(plan); }, }); @@ -185,7 +186,7 @@ export const listPlans = query({ ? await query.take(args.limit) : await query.take(20); - return plans; + return plans.map(projectAgentPlanSummary); }, }); diff --git a/src/features/agents/components/FastAgentPanel/FastAgentPanel.tsx b/src/features/agents/components/FastAgentPanel/FastAgentPanel.tsx index 557baf5b..32855641 100644 --- a/src/features/agents/components/FastAgentPanel/FastAgentPanel.tsx +++ b/src/features/agents/components/FastAgentPanel/FastAgentPanel.tsx @@ -22,8 +22,6 @@ import { FastAgentUIMessageBubble } from './FastAgentPanel.UIMessageBubble'; import { MessageHandlersProvider } from './MessageHandlersContext'; import { VirtualizedMessageList, useMessageVirtualization } from './VirtualizedMessageList'; import { useSwarmByThread, useSwarmActions, parseSpawnCommand, isSpawnCommand } from '@/hooks/useSwarm'; -import { MemoryStatusHeader, type PlanItem } from './MemoryStatusHeader'; -import { ContextBar, type ContextConstraint } from './ContextBar'; import { SwarmQuickActions } from './SwarmQuickActions'; import { QuickCommandChips } from './QuickCommandChips'; import { QuotePopover } from '@/features/chat/components/QuotePopover'; @@ -1054,50 +1052,6 @@ export const FastAgentPanel = memo(function FastAgentPanel({ (isAuthenticated && activeThreadId) ? { threadId: activeThreadId } : 'skip' ); - // Query for agent planning data (ambient memory - plan progress) - const agentPlans = useQuery( - api.domains.agents.agentPlanning.listPlans, - isAuthenticated ? { limit: 5 } : 'skip' - ); - - // Query for agent memory (context constraints / scratchpad) - const agentMemory = useQuery( - api.domains.agents.agentMemory.listMemory, - isAuthenticated ? { limit: 10 } : 'skip' - ); - - // Transform plans into PlanItem format for MemoryStatusHeader - const planItems = useMemo((): PlanItem[] => { - if (!agentPlans || agentPlans.length === 0) return []; - - // Get the most recent plan - const latestPlan = agentPlans[0]; - if (!latestPlan?.steps) return []; - - return latestPlan.steps.map((step: any, idx: number) => ({ - id: `${latestPlan._id}-${idx}`, - name: step.name, - status: step.status === 'completed' ? 'done' - : step.status === 'in_progress' ? 'active' - : 'queued', - })); - }, [agentPlans]); - - // Transform memory into constraints for ContextBar - const contextConstraints = useMemo((): ContextConstraint[] => { - if (!agentMemory) return []; - - // Filter for constraint-type memory entries (e.g., keys like 'constraint:*' or 'context:*') - return agentMemory - .filter((m: any) => m.key.startsWith('constraint:') || m.key.startsWith('context:') || m.key === 'scratchpad') - .map((m: any) => ({ - id: m._id, - label: m.key.replace(/^(constraint:|context:)/, ''), - value: m.content.length > 30 ? m.content.slice(0, 30) + '...' : m.content, - type: m.key.startsWith('constraint:') ? 'rule' as const : 'custom' as const, - })); - }, [agentMemory]); - const processedDocMessageIdsRef = useRef>(new Set()); // Update active thread when initialThreadId changes (for external navigation)