From 24477aace7f27545ba7af4d424739cb4a87d87ca Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 3 Jun 2026 10:22:30 -0700 Subject: [PATCH 01/15] docs(spec): render VIEW_REGISTRY drift + chat re-export cleanup + nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes a chain of compounding bugs: render exports public API with no engine behavior; chat leaks render tokens into its surface; chat README's markdown override example is broken three ways. Adds overrideViews, wires VIEW_REGISTRY into the render engine, drops chat's re-export, fixes all documentation (README + chat markdown guide + render views API + CHANGELOG + regenerated api-docs.json). Plus three nits: ag-ui README interrupt docs + new docs guide, .gitignore Playwright dirs, rename dashboard.md → generative-ui.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...05-29-render-chat-docs-alignment-design.md | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-29-render-chat-docs-alignment-design.md diff --git a/docs/superpowers/specs/2026-05-29-render-chat-docs-alignment-design.md b/docs/superpowers/specs/2026-05-29-render-chat-docs-alignment-design.md new file mode 100644 index 00000000..9cdd9b75 --- /dev/null +++ b/docs/superpowers/specs/2026-05-29-render-chat-docs-alignment-design.md @@ -0,0 +1,214 @@ +# Render `VIEW_REGISTRY` Drift + Chat Re-export Cleanup + Nits — Design + +**Date:** 2026-05-29 +**Status:** Approved (brainstorming) — pending implementation plan + +## Problem + +The README audit (PR #559) and the subsequent ag-ui PR (#567) surfaced one code-level drift item (`@threadplane/render` exports `provideViews` + `VIEW_REGISTRY` that the engine never injects), plus three small leftover nits worth cleaning up in the same pass. While auditing the render token I found that the situation is actually a chain of compounding bugs that mislead consumers: + +1. **`@threadplane/render` exports a public API with no behavior.** `VIEW_REGISTRY` is an `InjectionToken`; `provideViews(registry)` returns a provider that binds the token. Nothing in `RenderSpecComponent` / `RenderElementComponent` (the engine) ever calls `inject(VIEW_REGISTRY)`. No code anywhere in the repo (libs, apps, cockpit, examples) injects it either. The render README documents the token as "for consumers to inject directly," but absent any engine consumption that's a documented dead surface. +2. **`@threadplane/chat` re-exports the render token.** `libs/chat/src/public-api.ts:155` re-exports `provideViews` + `VIEW_REGISTRY`. This leaks render's tokens into chat's surface and conflates two libraries' concerns. Chat has its OWN markdown registry token (`MARKDOWN_VIEW_REGISTRY`) consumed by `markdown-children.component.ts` and provided by `` (factory backed by the `[viewRegistry]` input or `cacheplaneMarkdownViews` default). +3. **The chat README's "Override individual node renderers" example is broken in three ways** simultaneously: + - Wrong token — uses `provideViews()` (which provides render's `VIEW_REGISTRY`) when the chat markdown components read `MARKDOWN_VIEW_REGISTRY`. + - Wrong helper semantics — `withViews(base, additions)` is implemented `{ ...additions, ...base }`, so `base` wins. Its doc comment is explicit: it ADDS new keys, it does NOT override. + - Wrong key — the example uses `'code'`, but the registered node type is `'code-block'` (per `cacheplaneMarkdownViews`). + + A consumer following the README sees no effect at all. + +Plus three unrelated leftovers from prior PRs: + +4. **`@threadplane/ag-ui` README does not document the new `Agent.interrupt` signal or `submit({ resume })`** that PR #567 shipped — the merge resolution took main's older README, intentionally deferring docs. +5. **Cockpit Playwright `test-results/` dirs are untracked** under the two new ag-ui example apps; no gitignore pattern covers them. +6. **`cockpit-registry`'s `tracks implemented python assets for every approved capability topic` test fails** because the `chat/generative-ui` manifest entry expects `cockpit/chat/generative-ui/python/prompts/generative-ui.md` (the topic-named convention), but the actual file is `dashboard.md`. The example's airline-KPI-dashboard graph reads `dashboard.md` directly. + +## Goals + +- Stop documenting and exporting public APIs with no behavior. +- Restore one consistent override story for chat's markdown views, with the documented example actually working. +- Document the new ag-ui interrupt feature on both library and website surfaces. +- Drop the `cockpit-registry` test back to green; gitignore Playwright detritus. + +## Decisions (from brainstorming) + +- **Render `VIEW_REGISTRY`:** wire it into the engine as a fallback (input → `RENDER_CONFIG.registry` → `VIEW_REGISTRY` → null). Make the documented public API real for direct render consumers, instead of removing it. +- **Composition helper:** add a new `overrideViews(base, overrides)` to `@threadplane/render` with override semantics. Keep `withViews` semantics unchanged (additive — its name and doc match). +- **Chat re-export:** drop `provideViews` + `VIEW_REGISTRY` from `@threadplane/chat`'s public surface. Render's tokens belong to render. Acceptable under the patch-only `0.0.x` release policy. +- **Markdown override docs:** the canonical path is `{ provide: MARKDOWN_VIEW_REGISTRY, useValue: overrideViews(cacheplaneMarkdownViews, { 'code-block': MyComp }) }`. Documented in chat README + chat markdown guide + render README + render views API page; theming continues to use the existing `--ngaf-chat-*` / `--a2ui-*` tokens (no re-documentation of theming, just a pointer). +- **ag-ui interrupts docs:** short subsection in `libs/ag-ui/README.md` + a new `apps/website/content/docs/ag-ui/guides/interrupts.mdx` page (parity with the existing langgraph guide), registered in `docs-config.ts`. +- **Nits:** rename `dashboard.md` → `generative-ui.md` to satisfy the topic-named convention (smaller, lower-risk than changing the manifest's path-building rule); gitignore `cockpit/**/angular/test-results/`. +- **Out of scope:** the blog post (separate brainstorming pass after this lands). + +## Verified ground truth (from current source) + +- `libs/chat/src/public-api.ts:155` — `export { provideViews, VIEW_REGISTRY } from '@threadplane/render';` +- `libs/render/src/lib/provide-views.ts` — `VIEW_REGISTRY` defined and assigned via `provideViews`; nothing else in `libs/render/src/lib` injects it. +- `libs/render/src/lib/views.ts:24-31` — `withViews` is `{ ...additions, ...base }` (additive only). +- `libs/chat/src/lib/markdown/markdown-view-registry.ts` — defines `MARKDOWN_VIEW_REGISTRY`; injected by `markdown-children.component.ts:41`. +- `libs/chat/src/lib/streaming/streaming-markdown.component.ts:51-58` — provides `MARKDOWN_VIEW_REGISTRY` via factory from `resolvedRegistry()`, which prefers the `[viewRegistry]` input over `cacheplaneMarkdownViews`. +- `libs/chat/src/lib/markdown/cacheplane-markdown-views.ts` — 22 entries for partial-markdown@0.2 node types; `'code-block'` is the actual fenced-code key, NOT `'code'`. +- `cockpit/chat/generative-ui/python/src/graph.py:31` — `(Path(...).parent.parent / "prompts" / "dashboard.md").read_text()`. +- `cockpit/chat/generative-ui/python/src/index.ts` — `promptAssetPaths: ['cockpit/chat/generative-ui/python/prompts/dashboard.md']`. +- Failing test: `libs/cockpit-registry/src/lib/manifest.spec.ts:119` — `fs.existsSync(assetPath)` returns false for `cockpit/chat/generative-ui/python/prompts/generative-ui.md` (the manifest's topic-named path) — but the on-disk file is `dashboard.md`. + +## Scope (in/out) + +**In scope:** +- Library code: render `overrideViews`, render engine resolution order, chat re-export drop. +- Library docs: `libs/render/README.md`, `libs/chat/README.md`, `libs/ag-ui/README.md`, `libs/chat/CHANGELOG.md`. +- Website docs: `apps/website/content/docs/render/api/views.mdx`, `apps/website/content/docs/chat/guides/markdown.mdx`, new `apps/website/content/docs/ag-ui/guides/interrupts.mdx`, `docs-config.ts` registration, regenerated `api-docs.json` (chat + render). +- Cockpit nit: rename `dashboard.md` → `generative-ui.md` + graph.py + descriptor edits. +- Root `.gitignore` pattern. + +**Out of scope:** +- Blog post about the new ag-ui interrupt feature (planned separate brainstorming pass). +- Any change to chat's `MARKDOWN_VIEW_REGISTRY` API itself; we are documenting and using the existing surface, not redesigning it. +- Changing the manifest's path-building convention; the rename satisfies the existing convention. +- Touching the langgraph interrupts guide or other adapter docs. + +--- + +## Design + +### 1. Render — `overrideViews` helper (`libs/render/src/lib/views.ts`) + +Add a sibling to `withViews`/`withoutViews` with inverted spread: + +```ts +/** + * Replaces views in a registry. Keys present in `overrides` win over `base`. + * Use this when you want to swap an existing renderer; use `withViews` when + * you want to add new node types without touching existing ones. + */ +export function overrideViews( + base: ViewRegistry, + overrides: Record | RenderViewEntry>, +): ViewRegistry { + return Object.freeze({ ...base, ...overrides }); +} +``` + +Export from `libs/render/src/lib/public-api.ts` alongside `withViews`/`withoutViews`. Add unit tests in `views.spec.ts` covering: override wins; absent key in overrides preserves base; original `base` reference unchanged; result is frozen. + +### 2. Render — wire `VIEW_REGISTRY` into the engine + +Add a small private helper to both `RenderSpecComponent` and `RenderElementComponent`: + +```ts +private readonly fallbackRegistry = inject(VIEW_REGISTRY, { optional: true }); + +protected resolveRegistry(): ViewRegistry | null { + return this.registry() // 1. [registry] input + ?? this.config?.registry // 2. RENDER_CONFIG.registry + ?? this.fallbackRegistry // 3. VIEW_REGISTRY token + ?? null; // 4. existing fallback +} +``` + +Replace every existing read of `this.registry() ?? this.config?.registry ?? null` (or equivalent) with `this.resolveRegistry()`. If the two components share enough that a tiny `resolve-registry.ts` utility makes sense, extract it; otherwise inline. + +Unit tests (`render-spec.component.spec.ts`, `render-element.component.spec.ts`): four priority cases each — input wins over all; `RENDER_CONFIG` wins over `VIEW_REGISTRY`; `VIEW_REGISTRY` is used when neither input nor `RENDER_CONFIG.registry` is set; null when none provided. + +### 3. Chat — drop the leaking re-export + +In `libs/chat/src/public-api.ts`, remove line 155 (`export { provideViews, VIEW_REGISTRY } from '@threadplane/render';`). Confirm `nx test chat` stays green (chat does not internally inject either — verified). The `views`/`withViews`/`withoutViews`/`toRenderRegistry`/`cacheplaneMarkdownViews`/`MARKDOWN_VIEW_REGISTRY` exports stay where they are. Direct render consumers continue to import `provideViews`/`VIEW_REGISTRY` from `@threadplane/render`. + +### 4. Chat README — rewrite the markdown override example + +`libs/chat/README.md` — replace the broken "Override individual node renderers" block with: + +```ts +import { MARKDOWN_VIEW_REGISTRY, cacheplaneMarkdownViews } from '@threadplane/chat'; +import { overrideViews } from '@threadplane/render'; +import { MyCodeBlockComponent } from './my-code-block.component'; + +providers: [ + { + provide: MARKDOWN_VIEW_REGISTRY, + useValue: overrideViews(cacheplaneMarkdownViews, { 'code-block': MyCodeBlockComponent }), + }, +]; +``` + +Add a one-sentence pointer that theming uses the existing `--ngaf-chat-*` / `--a2ui-*` tokens documented in the Theming section (do not duplicate the theming surface). Note the per-instance alternative: ``. + +### 5. Chat markdown guide (`apps/website/content/docs/chat/guides/markdown.mdx`) + +Apply the same correction end-to-end: `MARKDOWN_VIEW_REGISTRY` is the token; `overrideViews` is the helper; the actual node-type keys come from `cacheplaneMarkdownViews`. Include a compact node-type reference (the 22 keys with one-line descriptions). Link to the render API page for `views`/`withViews`/`withoutViews`/`overrideViews`/`toRenderRegistry`. + +### 6. Render README + +`libs/render/README.md` — under "DI providers", rewrite the `provideViews` description to reflect the new resolution order: `[registry]` input → `RENDER_CONFIG.registry` → `VIEW_REGISTRY` (provided via `provideViews`) → null. Add `overrideViews` to the composition-helper list alongside `withViews`/`withoutViews`. The previous "for consumers to inject directly" phrasing goes away — the engine now consumes it. + +### 7. Render views API page (`apps/website/content/docs/render/api/views.mdx`) + +Add a documented `overrideViews` entry parallel to `withViews`/`withoutViews` (signature, semantics, contrasting example). Update `provideViews` description with the engine resolution order. + +### 8. Regenerated API JSON + +Run `npm run generate-api-docs` to refresh `apps/website/content/docs/chat/api/api-docs.json` (will lose `provideViews`/`VIEW_REGISTRY` entries from chat) and `apps/website/content/docs/render/api/api-docs.json` (will gain `overrideViews`). Commit the regenerated files. + +### 9. `libs/chat/CHANGELOG.md` + +Add an Unreleased entry: + +> **Changed:** `@threadplane/chat` no longer re-exports `provideViews` / `VIEW_REGISTRY` from `@threadplane/render`. Consumers using `` / `` directly should import from `@threadplane/render`. For chat's markdown view overrides, provide `MARKDOWN_VIEW_REGISTRY` directly using `overrideViews(cacheplaneMarkdownViews, { … })` — the previously-documented `provideViews(withViews(…))` pattern never actually drove rendering. + +### 10. ag-ui README — interrupt feature subsection + +`libs/ag-ui/README.md` — add an "Interrupts (human-in-the-loop)" subsection under Capabilities: + +- `agent.interrupt()` is a `Signal` populated from AG-UI `CUSTOM` / `on_interrupt` events. The reducer parses string-serialized `value` payloads (e.g. `ag-ui-langgraph` ships them via `dump_json_safe`) so consumers see the structured object. +- `agent.submit({ resume })` resumes the run via `runAgent({ forwardedProps: { command: { resume } } })`; the server reads `forwarded_props.command.resume`. +- One short example mirroring `cockpit/ag-ui/interrupts`: bind `` and call `submit({ resume: { approved: true } })`. +- Link the langgraph interrupts guide for the broader HITL conceptual reference (same `Agent` contract). + +### 11. New ag-ui interrupts guide (`apps/website/content/docs/ag-ui/guides/interrupts.mdx`) + +Parity with `apps/website/content/docs/langgraph/guides/interrupts.mdx`. Short, focused content: + +- The AG-UI event shape: `CUSTOM` event with `name: "on_interrupt"`, `value`: the structured payload from the backend (e.g. `{ kind, ... }`). +- The resume mechanism: `submit({ resume })` → `runAgent({ forwardedProps: { command: { resume } } })` → server reads `forwarded_props.command.resume`. +- The component side: bind ``, the structured payload shape, approve/reject actions. +- Pointer at the working `cockpit/ag-ui/interrupts` example. + +Register the new page in `apps/website/src/lib/docs-config.ts` under the existing `ag-ui` library's `Guides` section, placed adjacent to the existing guides (fake-agent, citations, troubleshooting). + +### 12. Root `.gitignore` + +Add a single pattern covering current and future cockpit examples: + +``` +cockpit/**/angular/test-results/ +``` + +Verify no committed `test-results/` path exists in any cockpit example (`git ls-files cockpit/ | grep test-results`) before committing. + +### 13. Cockpit chat/generative-ui prompt rename + +- `git mv cockpit/chat/generative-ui/python/prompts/dashboard.md cockpit/chat/generative-ui/python/prompts/generative-ui.md` +- `cockpit/chat/generative-ui/python/src/graph.py:31` — `"dashboard.md"` → `"generative-ui.md"`. +- `cockpit/chat/generative-ui/python/src/index.ts` `promptAssetPaths` — `prompts/dashboard.md` → `prompts/generative-ui.md`. +- Verify: `npx nx test cockpit-registry` (the previously-failing test goes green); `npx nx smoke cockpit-chat-generative-ui-python` still passes. + +--- + +## Testing strategy + +- Unit tests for `overrideViews` (§1) and the render-engine resolution order (§2, both components, four priority cases each). +- `nx test chat` green after the re-export drop (§3). `nx test render` green. +- `nx test cockpit-registry` green after the prompt rename (§13). +- Manual confirmation that `nx run-many -t build` for `render`, `chat`, `ag-ui`, and both `cockpit-ag-ui-*-angular` apps stays green throughout (no API contract regression in chat from the drop). +- `npm run generate-api-docs` runs cleanly and the regenerated files are committed (§8). +- Docs site `nx build website` succeeds after the new ag-ui interrupts guide (§11) is added and registered (§ `docs-config.ts`). + +## Success criteria + +- `@threadplane/render` exports `overrideViews`; `RenderSpecComponent` and `RenderElementComponent` resolve registries through the new four-step priority. +- `@threadplane/chat` no longer exports `provideViews` / `VIEW_REGISTRY`. +- The chat README markdown override example, the chat markdown guide, and the render README/views API page all describe the corrected story consistently. +- `libs/ag-ui/README.md` documents `Agent.interrupt` + `submit({ resume })`. +- `/docs/ag-ui/guides/interrupts` exists, registered in the nav, mirroring the langgraph guide's depth. +- Cockpit Playwright `test-results/` dirs ignored repo-wide. +- `nx test cockpit-registry` is green. +- `npm run generate-api-docs` produces no spurious diff after the work; the committed API JSON matches the source. From 5f3f9eeefa192c636a2fb9948d6aa3d4a6c82556 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 3 Jun 2026 10:45:08 -0700 Subject: [PATCH 02/15] docs(plan): render VIEW_REGISTRY drift + chat re-export cleanup + nits implementation plan --- .../2026-05-29-render-chat-docs-alignment.md | 753 ++++++++++++++++++ 1 file changed, 753 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-29-render-chat-docs-alignment.md diff --git a/docs/superpowers/plans/2026-05-29-render-chat-docs-alignment.md b/docs/superpowers/plans/2026-05-29-render-chat-docs-alignment.md new file mode 100644 index 00000000..1c56f2ec --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-render-chat-docs-alignment.md @@ -0,0 +1,753 @@ +# Render `VIEW_REGISTRY` Drift + Chat Re-export Cleanup + Nits — 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:** Make `@threadplane/render`'s `VIEW_REGISTRY` actually drive rendering, add the missing `overrideViews` helper, drop the leaking chat re-export, fix the broken markdown-override documentation across every surface, document the new ag-ui interrupt feature, and clean up three nits (`.gitignore`, prompt rename, ag-ui docs). + +**Architecture:** Sequential — code changes first (engine + helper + chat surface trim), then library READMEs, then website docs, then regenerated API JSON (must run after the source changes), then ag-ui docs, then the three small nits. Each task is independently committable. + +**Tech Stack:** Angular 20/21, TypeScript, Vitest; Nx; MDX (Next.js website); python (for the cockpit prompt rename). + +**Spec:** `docs/superpowers/specs/2026-05-29-render-chat-docs-alignment-design.md` + +--- + +## Conventions + +- Run all commands from the repo root: `/Users/blove/repos/angular-agent-framework/.claude/worktrees/interesting-mccarthy-5d4ea0`. +- Commit after each task; do NOT push. +- TDD where there's code: failing test → implement → green → commit. + +--- + +## File Structure + +**Code:** +- Modify `libs/render/src/lib/views.ts` (+ spec) — `overrideViews`. +- Modify `libs/render/src/lib/public-api.ts` — export `overrideViews`. +- Modify `libs/render/src/lib/render-spec.component.ts` (+ spec) — resolution order. +- Modify `libs/render/src/lib/render-element.component.ts` (+ spec) — resolution order. +- Modify `libs/chat/src/public-api.ts` — drop re-export. + +**Library docs:** +- Modify `libs/render/README.md`, `libs/chat/README.md`, `libs/ag-ui/README.md`, `libs/chat/CHANGELOG.md`. + +**Website docs:** +- Modify `apps/website/content/docs/render/api/views.mdx`. +- Modify `apps/website/content/docs/chat/guides/markdown.mdx`. +- Create `apps/website/content/docs/ag-ui/guides/interrupts.mdx`. +- Modify `apps/website/src/lib/docs-config.ts` — register the new guide. +- Regenerate `apps/website/content/docs/chat/api/api-docs.json` + `apps/website/content/docs/render/api/api-docs.json`. + +**Cockpit + workspace nits:** +- Rename `cockpit/chat/generative-ui/python/prompts/dashboard.md` → `generative-ui.md`. +- Modify `cockpit/chat/generative-ui/python/src/graph.py` (line 31). +- Modify `cockpit/chat/generative-ui/python/src/index.ts` (`promptAssetPaths`). +- Modify root `.gitignore`. + +--- + +## Task 1: `overrideViews` helper + +**Files:** +- Modify: `libs/render/src/lib/views.ts` +- Modify: `libs/render/src/lib/public-api.ts` +- Test: `libs/render/src/lib/views.spec.ts` + +- [ ] **Step 1: Write failing tests** + +In `libs/render/src/lib/views.spec.ts` (mirror the existing test style; reuse imports for `views`/`withViews` already there). Add: + +```ts +import { Component } from '@angular/core'; +import { views, withViews, overrideViews } from './views'; + +@Component({ standalone: true, template: '' }) class A {} +@Component({ standalone: true, template: '' }) class B {} +@Component({ standalone: true, template: '' }) class C {} + +describe('overrideViews', () => { + it('replaces matching keys; overrides win over base', () => { + const base = views({ foo: A, bar: B }); + const result = overrideViews(base, { foo: C }); + expect(result['foo']).toBe(C); + expect(result['bar']).toBe(B); + }); + + it('adds new keys not present in base', () => { + const base = views({ foo: A }); + const result = overrideViews(base, { bar: B }); + expect(result['foo']).toBe(A); + expect(result['bar']).toBe(B); + }); + + it('does not mutate base', () => { + const base = views({ foo: A }); + overrideViews(base, { foo: B }); + expect(base['foo']).toBe(A); + }); + + it('returns a frozen object', () => { + const result = overrideViews(views({}), {}); + expect(Object.isFrozen(result)).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run, verify FAIL** + +Run: `npx nx test render` +Expected: FAIL — `overrideViews is not exported` / undefined. + +- [ ] **Step 3: Implement** + +In `libs/render/src/lib/views.ts`, add (place beside `withViews`): + +```ts +/** + * Replaces views in a registry. Keys in `overrides` win over `base`. + * Use this to swap an existing renderer; use `withViews` to add NEW + * node types without touching existing entries. + */ +export function overrideViews( + base: ViewRegistry, + overrides: Record | RenderViewEntry>, +): ViewRegistry { + return Object.freeze({ ...base, ...overrides }); +} +``` + +In `libs/render/src/lib/public-api.ts`, find the line exporting `withViews`/`withoutViews` and add `overrideViews` to the same `export` statement (or add a parallel `export { overrideViews } from './lib/views';` to match the file's existing style — look at how `withViews` is exported and mirror that). + +- [ ] **Step 4: Run, verify PASS** + +Run: `npx nx test render && npx nx build render` +Expected: all green; build succeeds. + +- [ ] **Step 5: Commit** + +```bash +git add libs/render/src/lib/views.ts libs/render/src/lib/views.spec.ts libs/render/src/lib/public-api.ts +git commit -m "feat(render): add overrideViews helper for override-semantics composition" +``` + +--- + +## Task 2: Wire `VIEW_REGISTRY` into the render engine + +**Files:** +- Modify: `libs/render/src/lib/render-spec.component.ts` (+ spec) +- Modify: `libs/render/src/lib/render-element.component.ts` (+ spec) + +The change: both components currently resolve their registry from the `[registry]` input or `RENDER_CONFIG.registry`. Add `VIEW_REGISTRY` as a third fallback before null. Same pattern in both components. + +- [ ] **Step 1: Read current resolution sites** + +Read both component files. Locate every place either component derives its registry. Note the variable names used (`this.registry()`, `this.config?.registry`, etc.). The fallback you add should NOT change behavior when the input or `RENDER_CONFIG.registry` is set — only fire when both are absent. + +- [ ] **Step 2: Write failing tests** + +In `libs/render/src/lib/render-spec.component.spec.ts` (mirror existing test setup — TestBed + standalone component harness). Add four priority tests: + +```ts +import { TestBed } from '@angular/core/testing'; +import { Component, signal } from '@angular/core'; +import { provideRender } from './provide-render'; +import { provideViews, VIEW_REGISTRY } from './provide-views'; +import { views } from './views'; +import { RenderSpecComponent } from './render-spec.component'; + +@Component({ standalone: true, template: '' }) class CompFromInput {} +@Component({ standalone: true, template: '' }) class CompFromConfig {} +@Component({ standalone: true, template: '' }) class CompFromToken {} + +describe('RenderSpecComponent registry resolution priority', () => { + it('uses the [registry] input when present (highest priority)', async () => { + TestBed.configureTestingModule({ + providers: [ + provideRender({ registry: views({ leaf: CompFromConfig }) }), + provideViews(views({ leaf: CompFromToken })), + ], + }); + const fix = TestBed.createComponent(RenderSpecComponent); + fix.componentRef.setInput('registry', views({ leaf: CompFromInput })); + // The component must resolve to CompFromInput. Assert via the + // component's `resolveRegistry()` helper (newly extracted in step 3), + // or via the rendered output for spec = { type: 'leaf' }. + fix.componentRef.setInput('spec', { type: 'leaf' }); + fix.detectChanges(); + expect(fix.debugElement.query((d) => d.componentInstance instanceof CompFromInput)).toBeTruthy(); + }); + + it('uses RENDER_CONFIG.registry when no [registry] input is bound', async () => { + TestBed.configureTestingModule({ + providers: [ + provideRender({ registry: views({ leaf: CompFromConfig }) }), + provideViews(views({ leaf: CompFromToken })), + ], + }); + const fix = TestBed.createComponent(RenderSpecComponent); + fix.componentRef.setInput('spec', { type: 'leaf' }); + fix.detectChanges(); + expect(fix.debugElement.query((d) => d.componentInstance instanceof CompFromConfig)).toBeTruthy(); + }); + + it('falls back to VIEW_REGISTRY token when no input + no RENDER_CONFIG.registry', async () => { + TestBed.configureTestingModule({ + providers: [ + // RENDER_CONFIG provided but registry omitted: + provideRender({}), + provideViews(views({ leaf: CompFromToken })), + ], + }); + const fix = TestBed.createComponent(RenderSpecComponent); + fix.componentRef.setInput('spec', { type: 'leaf' }); + fix.detectChanges(); + expect(fix.debugElement.query((d) => d.componentInstance instanceof CompFromToken)).toBeTruthy(); + }); + + it('renders the existing fallback when nothing is provided', async () => { + TestBed.configureTestingModule({ providers: [] }); + const fix = TestBed.createComponent(RenderSpecComponent); + fix.componentRef.setInput('spec', { type: 'leaf' }); + fix.detectChanges(); + // Existing behavior — either renders DefaultFallbackComponent or + // nothing, depending on how the component handles a null registry + // today. Adjust the assertion to match the existing fallback. + // The point is the test confirms the new VIEW_REGISTRY branch did + // not break the null path. + expect(fix.nativeElement).toBeTruthy(); + }); +}); +``` + +(If the existing tests already cover input + RENDER_CONFIG paths, only add the two new tests — the token-fallback one and the null-path regression check.) + +Add a parallel set of four tests in `render-element.component.spec.ts` adapted to that component's input shape. + +- [ ] **Step 3: Run, verify the two new "token fallback" tests FAIL** + +Run: `npx nx test render` +Expected: the "falls back to VIEW_REGISTRY token" test fails for each component (registry resolves to null, not `CompFromToken`). + +- [ ] **Step 4: Implement** + +In `render-spec.component.ts`, inside the component class, add: + +```ts +private readonly fallbackRegistry = inject(VIEW_REGISTRY, { optional: true }); + +protected resolveRegistry(): ViewRegistry | null { + return this.registry() // 1. [registry] input + ?? this.config?.registry // 2. RENDER_CONFIG.registry + ?? this.fallbackRegistry // 3. VIEW_REGISTRY token + ?? null; // 4. existing fallback +} +``` + +Add the import: `import { VIEW_REGISTRY } from './provide-views';`. Replace every existing `this.registry() ?? this.config?.registry ?? null` (or equivalent) inside the component with `this.resolveRegistry()`. If `inject(VIEW_REGISTRY, { optional: true })` can't run at field-declaration time (Angular requires injection context), move the call into the constructor and assign to a `private readonly` field. + +Apply the IDENTICAL change to `render-element.component.ts` (same imports, same helper, same call-site replacement). Keep the two helpers inline (don't extract to a shared utility yet — the two components likely differ on `registry()` input name; revisit only if the duplication grows). + +- [ ] **Step 5: Run, verify PASS** + +Run: `npx nx test render && npx nx build render` +Expected: all green; build succeeds. The two new token-fallback tests pass; the input/config tests still pass. + +- [ ] **Step 6: Commit** + +```bash +git add libs/render/src/lib/render-spec.component.ts libs/render/src/lib/render-spec.component.spec.ts \ + libs/render/src/lib/render-element.component.ts libs/render/src/lib/render-element.component.spec.ts +git commit -m "feat(render): wire VIEW_REGISTRY as third-priority registry fallback in engine" +``` + +--- + +## Task 3: Drop chat re-export of `provideViews` + `VIEW_REGISTRY` + CHANGELOG entry + +**Files:** +- Modify: `libs/chat/src/public-api.ts` +- Modify: `libs/chat/CHANGELOG.md` + +- [ ] **Step 1: Confirm no internal chat code uses these** + +```bash +grep -rn "from '@threadplane/chat'" libs/chat/src | grep -E "provideViews|VIEW_REGISTRY" || echo "no internal refs" +grep -rn "provideViews\|VIEW_REGISTRY" libs/chat/src | grep -v public-api.ts || echo "clean" +``` +Expected: no internal references (chat consumes `MARKDOWN_VIEW_REGISTRY`, not `VIEW_REGISTRY`). + +- [ ] **Step 2: Implement** + +In `libs/chat/src/public-api.ts`, delete the line: `export { provideViews, VIEW_REGISTRY } from '@threadplane/render';` (around line 155 — confirm exact line by reading first). + +Add to `libs/chat/CHANGELOG.md` immediately under `## [Unreleased]` → `### Changed`: + +```markdown +- **Public API trim:** `@threadplane/chat` no longer re-exports `provideViews` / `VIEW_REGISTRY` from `@threadplane/render`. Consumers using `` / `` directly should import from `@threadplane/render`. For chat's markdown view overrides, provide `MARKDOWN_VIEW_REGISTRY` directly using `overrideViews(cacheplaneMarkdownViews, { … })` from `@threadplane/render` — the previously-documented `provideViews(withViews(…))` pattern never actually drove rendering. +``` + +If a `### Changed` section doesn't yet exist under `[Unreleased]`, add the heading. + +- [ ] **Step 3: Verify** + +Run: `npx nx test chat && npx nx build chat` +Expected: all green; build succeeds. + +- [ ] **Step 4: Commit** + +```bash +git add libs/chat/src/public-api.ts libs/chat/CHANGELOG.md +git commit -m "refactor(chat)!: drop re-export of provideViews/VIEW_REGISTRY from @threadplane/render" +``` + +(The `!` follows the existing CHANGELOG's convention for marking surface-trimming changes; if the repo uses a different marker, drop it.) + +--- + +## Task 4: Fix chat README markdown override example + +**Files:** +- Modify: `libs/chat/README.md` + +- [ ] **Step 1: Locate the broken example** + +Read `libs/chat/README.md` around the "Override individual node renderers" / "Markdown" section (search for `withViews(cacheplaneMarkdownViews`). Confirm the broken block matches: + +```ts +import { withViews, cacheplaneMarkdownViews, provideViews } from '@threadplane/chat'; +import { MyCodeBlockComponent } from './my-code-block.component'; + +providers: [ + provideViews( + withViews(cacheplaneMarkdownViews, { code: MyCodeBlockComponent }) + ), +], +``` + +- [ ] **Step 2: Replace with the correct example** + +Replace the entire `import …` + `providers: [ … ]` code block above with: + +```ts +import { MARKDOWN_VIEW_REGISTRY, cacheplaneMarkdownViews } from '@threadplane/chat'; +import { overrideViews } from '@threadplane/render'; +import { MyCodeBlockComponent } from './my-code-block.component'; + +providers: [ + { + provide: MARKDOWN_VIEW_REGISTRY, + useValue: overrideViews(cacheplaneMarkdownViews, { 'code-block': MyCodeBlockComponent }), + }, +]; +``` + +Immediately before or after the block, add a one-sentence pointer: + +> Per-instance, bind the registry on `` instead. Styling uses the existing `--ngaf-chat-*` / `--a2ui-*` tokens — see the Theming section below. + +- [ ] **Step 3: Verify** + +```bash +grep -n "MARKDOWN_VIEW_REGISTRY\|overrideViews\|code-block" libs/chat/README.md | head +grep -n "provideViews(withViews" libs/chat/README.md && echo "STALE EXAMPLE REMAINS" || echo "stale example removed" +``` +Expected: the new symbols appear; the old `provideViews(withViews(…))` pattern is gone. + +- [ ] **Step 4: Commit** + +```bash +git add libs/chat/README.md +git commit -m "docs(chat): fix markdown view override example (MARKDOWN_VIEW_REGISTRY + overrideViews)" +``` + +--- + +## Task 5: Fix chat markdown website guide + +**Files:** +- Modify: `apps/website/content/docs/chat/guides/markdown.mdx` + +- [ ] **Step 1: Read the current guide** + +Open `apps/website/content/docs/chat/guides/markdown.mdx`. Locate the override section. + +- [ ] **Step 2: Replace the override mechanism** + +Apply the same correction as Task 4 (`MARKDOWN_VIEW_REGISTRY` + `overrideViews(cacheplaneMarkdownViews, { 'code-block': MyComp })`, import `overrideViews` from `@threadplane/render`). Adjust prose to: + +- Name the token (`MARKDOWN_VIEW_REGISTRY`) and explain that `` provides it on its component injector. +- Show the override helper and the per-instance `[viewRegistry]` input as the two paths. +- Add a compact reference of the 22 node-type keys from `cacheplaneMarkdownViews` (copy the list from `libs/chat/src/lib/markdown/cacheplane-markdown-views.ts`) so consumers know which keys are valid. +- Link the render API page (`/docs/render/api/views`) for `views` / `withViews` / `withoutViews` / `overrideViews` / `toRenderRegistry`. + +If the guide doesn't currently have a node-type reference table, add one — it's the most common source of "I overrode `code` and nothing happened." + +- [ ] **Step 3: Verify** + +```bash +grep -n "MARKDOWN_VIEW_REGISTRY\|overrideViews\|code-block" apps/website/content/docs/chat/guides/markdown.mdx | head +grep -n "provideViews(withViews" apps/website/content/docs/chat/guides/markdown.mdx && echo "STALE" || echo "clean" +``` +Expected: new symbols present; no stale `provideViews(withViews(…))`. + +- [ ] **Step 4: Commit** + +```bash +git add apps/website/content/docs/chat/guides/markdown.mdx +git commit -m "docs(website): correct markdown view override path in chat guide" +``` + +--- + +## Task 6: Update render README — resolution order + `overrideViews` + +**Files:** +- Modify: `libs/render/README.md` + +- [ ] **Step 1: Locate the DI providers section** + +Search `libs/render/README.md` for `provideViews(registry)` (the current line reads "publishes a `ViewRegistry` under the `VIEW_REGISTRY` token for consumers to inject directly"). + +- [ ] **Step 2: Replace the description** + +Change that line to describe the actual engine behavior: + +> `provideViews(registry)` publishes a `ViewRegistry` under the `VIEW_REGISTRY` token. `` and `` resolve their registry in priority order: the `[registry]` template input, then `RENDER_CONFIG.registry` (from `provideRender(...)`), then `VIEW_REGISTRY` (from `provideViews(...)`), then the existing fallback. + +In the same area (composition helpers — currently lists `views`/`withViews`/`withoutViews`), add `overrideViews`: + +> `overrideViews(base, overrides)` replaces matching keys (overrides win); use this to swap an existing renderer. `withViews(base, additions)` only adds NEW keys without touching existing ones; use it to extend a registry with previously-unhandled node types. + +- [ ] **Step 3: Verify** + +```bash +grep -n "overrideViews\|priority order\|VIEW_REGISTRY" libs/render/README.md | head +``` +Expected: new symbols and resolution order present. + +- [ ] **Step 4: Commit** + +```bash +git add libs/render/README.md +git commit -m "docs(render): document engine resolution order + overrideViews helper" +``` + +--- + +## Task 7: Update render views API mdx + +**Files:** +- Modify: `apps/website/content/docs/render/api/views.mdx` + +- [ ] **Step 1: Read the current page** + +Open `apps/website/content/docs/render/api/views.mdx`. Confirm it documents `provideViews`/`withViews`/`withoutViews` (and likely `views`, `toRenderRegistry`). + +- [ ] **Step 2: Add `overrideViews` + update `provideViews`** + +Add an `overrideViews` section parallel to the `withViews` section: + +````mdx +## `overrideViews(base, overrides)` + +Replaces entries in a registry. Keys in `overrides` win over `base`. Use this when you want to swap an existing renderer; use `withViews` when you want to add NEW node types without touching existing ones. + +```ts +import { overrideViews } from '@threadplane/render'; + +const myRegistry = overrideViews(baseRegistry, { + 'code-block': MyCodeBlockComponent, +}); +``` + +Returns a new frozen `ViewRegistry`. The `base` argument is not mutated. +```` + +Update the `provideViews` section's description to mention engine consumption: "`provideViews(registry)` registers `VIEW_REGISTRY` in the injector. `RenderSpecComponent` and `RenderElementComponent` consume it as a fallback when no `[registry]` input or `RENDER_CONFIG.registry` is provided." + +- [ ] **Step 3: Verify** + +```bash +grep -n "overrideViews\|VIEW_REGISTRY" apps/website/content/docs/render/api/views.mdx | head +``` +Expected: new symbol section present; provideViews description updated. + +- [ ] **Step 4: Commit** + +```bash +git add apps/website/content/docs/render/api/views.mdx +git commit -m "docs(website): add overrideViews + clarify provideViews engine consumption" +``` + +--- + +## Task 8: Regenerate API JSON + +**Files (modified by script):** +- `apps/website/content/docs/chat/api/api-docs.json` +- `apps/website/content/docs/render/api/api-docs.json` + +- [ ] **Step 1: Confirm the script exists + works** + +```bash +grep -n "generate-api-docs" package.json +ls apps/website/scripts/generate-api-docs.ts +``` +Expected: script defined; entry file exists. + +- [ ] **Step 2: Run** + +```bash +npm run generate-api-docs 2>&1 | tail -10 +``` +Expected: completes without error; both `api-docs.json` files updated. + +- [ ] **Step 3: Verify the diff makes sense** + +```bash +git diff --stat apps/website/content/docs/chat/api/api-docs.json apps/website/content/docs/render/api/api-docs.json +``` +Expected: `chat/api-docs.json` lost entries for `provideViews` and `VIEW_REGISTRY`; `render/api-docs.json` gained an `overrideViews` entry. No spurious noise (no unrelated symbol churn). If the diff is noisy, investigate before committing. + +- [ ] **Step 4: Commit** + +```bash +git add apps/website/content/docs/chat/api/api-docs.json apps/website/content/docs/render/api/api-docs.json +git commit -m "docs(website): regenerate API JSON for chat re-export drop + render overrideViews" +``` + +--- + +## Task 9: ag-ui README — interrupts subsection + +**Files:** +- Modify: `libs/ag-ui/README.md` + +- [ ] **Step 1: Locate the Capabilities section** + +Find the section in `libs/ag-ui/README.md` that lists the Signals (`messages`, `status`, `isLoading`, `error`, `toolCalls`, `state`). Insert the new subsection immediately after that table. + +- [ ] **Step 2: Add the subsection** + +````markdown +### Interrupts (human-in-the-loop) + +`agent.interrupt()` is a `Signal` populated from AG-UI `CUSTOM` events with `name: 'on_interrupt'`. The reducer parses string-serialized `value` payloads automatically (e.g. `ag-ui-langgraph` ships interrupts via `dump_json_safe`), so consumers see the structured object directly. + +Resume with `agent.submit({ resume })` — this calls `runAgent({ forwardedProps: { command: { resume } } })`, and the server reads `forwarded_props.command.resume` (the `ag-ui-langgraph` convention). + +Pair with `` from `@threadplane/chat` for the approve/reject/edit UX: + +```ts +import { Component } from '@angular/core'; +import { ChatComponent, ChatApprovalCardComponent } from '@threadplane/chat'; +import { injectAgent } from '@threadplane/ag-ui'; + +@Component({ + imports: [ChatComponent, ChatApprovalCardComponent], + template: ` + + + `, +}) +export class App { + protected readonly agent = injectAgent(); + onAction(a: 'approve' | 'cancel') { + void this.agent.submit({ resume: { approved: a === 'approve' } }); + } +} +``` + +See `cockpit/ag-ui/interrupts` for a complete working example, and the [LangGraph interrupts guide](https://threadplane.ai/docs/langgraph/guides/interrupts) for the broader HITL contract — the same `Agent.interrupt` / `submit({ resume })` API works across both adapters. +```` + +- [ ] **Step 3: Verify** + +```bash +grep -nE "Interrupts \(human|agent\.interrupt\(\)|submit\(\{.*resume" libs/ag-ui/README.md | head +``` +Expected: subsection present. + +- [ ] **Step 4: Commit** + +```bash +git add libs/ag-ui/README.md +git commit -m "docs(ag-ui): document Agent.interrupt + submit({ resume }) HITL feature" +``` + +--- + +## Task 10: New ag-ui interrupts website guide + +**Files:** +- Create: `apps/website/content/docs/ag-ui/guides/interrupts.mdx` +- Modify: `apps/website/src/lib/docs-config.ts` + +- [ ] **Step 1: Read the langgraph interrupts guide as a template** + +Open `apps/website/content/docs/langgraph/guides/interrupts.mdx` and read its structure (frontmatter, sections, code-block conventions, links to API pages). + +- [ ] **Step 2: Create the ag-ui version** + +Create `apps/website/content/docs/ag-ui/guides/interrupts.mdx`. Mirror the langgraph guide's frontmatter shape (title, description, order in nav), then short sections: + +1. **What it is** — same runtime-neutral interrupt feature exposed via the `Agent` contract; this guide is the AG-UI specifics. +2. **The wire format** — AG-UI `CUSTOM` event with `name: 'on_interrupt'`, `value: ` (e.g. `{ kind: 'refund_approval', amount, customer_id, reason }`). Note the `ag-ui-langgraph` convention of shipping `value` as a JSON-serialized string and that the adapter parses it. +3. **Reading the interrupt** — `agent.interrupt()` signal; the canonical `{ kind, ... }` payload pattern; matching with ``. +4. **Resuming** — `agent.submit({ resume })`, which becomes `forwardedProps: { command: { resume } }`; server-side `forwarded_props.command.resume` (link to `ag-ui-langgraph` docs for the python side). +5. **End-to-end example** — point at `cockpit/ag-ui/interrupts` (Angular + Python, refund-approval graph). Include one short component code block (the same `` snippet). +6. **Cross-adapter parity** — link the langgraph interrupts guide; note the consumer Angular code is byte-identical except the adapter import. + +Keep it tight — under 200 lines. The point is discoverability + the AG-UI-specific wire details, not duplication of the langgraph guide. + +- [ ] **Step 3: Register the page** + +Read `apps/website/src/lib/docs-config.ts`; find the `ag-ui` library's `Guides` section (lists `fake-agent`, `citations`, `troubleshooting`). Add an `interrupts` entry placed in the same alphabetical/conceptual position as it appears in the langgraph guides. Match the exact entry shape (title, slug, file, order) of the neighboring entries. + +- [ ] **Step 4: Verify** + +```bash +npx nx build website 2>&1 | tail -5 +grep -n "ag-ui.*interrupts\|guides/interrupts" apps/website/src/lib/docs-config.ts | head +``` +Expected: website build succeeds (no 404 / broken-link errors from the new page or docs-config); the registration is present. + +- [ ] **Step 5: Commit** + +```bash +git add apps/website/content/docs/ag-ui/guides/interrupts.mdx apps/website/src/lib/docs-config.ts +git commit -m "docs(website): add ag-ui interrupts guide + register in nav" +``` + +--- + +## Task 11: Root `.gitignore` — Playwright `test-results/` + +**Files:** +- Modify: `.gitignore` + +- [ ] **Step 1: Confirm nothing committed under the pattern** + +```bash +git ls-files cockpit/ | grep test-results && echo "COMMITTED test-results exist — DO NOT add pattern blindly" || echo "no committed paths under test-results" +``` +Expected: no committed paths (the dirs are runtime detritus). + +- [ ] **Step 2: Read current `.gitignore`** + +Read the root `.gitignore`. Locate the section where Playwright / e2e / dist patterns live (search for `playwright` or `test-results` to confirm the pattern isn't already there). + +- [ ] **Step 3: Add the pattern** + +Append (or place near similar e2e patterns): + +``` +# Playwright artifacts under cockpit examples +cockpit/**/angular/test-results/ +``` + +- [ ] **Step 4: Verify** + +```bash +git check-ignore -v cockpit/ag-ui/interrupts/angular/test-results/.dummy 2>&1 | head -1 +git check-ignore -v cockpit/ag-ui/streaming/angular/test-results/.dummy 2>&1 | head -1 +``` +Expected: both paths match the new pattern (the command prints the matching rule). + +- [ ] **Step 5: Commit** + +```bash +git add .gitignore +git commit -m "chore: gitignore Playwright test-results under cockpit examples" +``` + +--- + +## Task 12: Rename `dashboard.md` → `generative-ui.md` + +**Files:** +- Rename: `cockpit/chat/generative-ui/python/prompts/dashboard.md` → `generative-ui.md` +- Modify: `cockpit/chat/generative-ui/python/src/graph.py` +- Modify: `cockpit/chat/generative-ui/python/src/index.ts` + +- [ ] **Step 1: Rename via git** + +```bash +git mv cockpit/chat/generative-ui/python/prompts/dashboard.md cockpit/chat/generative-ui/python/prompts/generative-ui.md +``` + +- [ ] **Step 2: Update `graph.py`** + +In `cockpit/chat/generative-ui/python/src/graph.py`, line 31, change: + +```python +_PROMPT = (Path(__file__).parent.parent / "prompts" / "dashboard.md").read_text() +``` +to: +```python +_PROMPT = (Path(__file__).parent.parent / "prompts" / "generative-ui.md").read_text() +``` + +Then search the rest of the file (and the rest of `cockpit/chat/generative-ui/python/`) for any other `"dashboard.md"` literal. If `graph.py` reads OTHER `.md` files from the prompts dir (e.g. tool-specific prompts), those are unrelated — only the topic-level prompt is in scope. + +- [ ] **Step 3: Update `python/src/index.ts`** + +In `cockpit/chat/generative-ui/python/src/index.ts`, update `promptAssetPaths`: + +```ts +promptAssetPaths: ['cockpit/chat/generative-ui/python/prompts/generative-ui.md'], +``` + +- [ ] **Step 4: Verify** + +```bash +ls cockpit/chat/generative-ui/python/prompts/ +grep -n "dashboard.md" cockpit/chat/generative-ui/python/src/graph.py cockpit/chat/generative-ui/python/src/index.ts && echo "STALE refs remain" || echo "clean" +npx nx smoke cockpit-chat-generative-ui-python +npx nx test cockpit-registry +``` +Expected: only `generative-ui.md` (and any unrelated files like tool prompts) in the prompts dir; no stale `dashboard.md` refs; smoke green; **cockpit-registry test goes from red to green** (the `fs.existsSync` failure resolves). + +- [ ] **Step 5: Commit** + +```bash +git add cockpit/chat/generative-ui/python +git commit -m "fix(cockpit): rename dashboard.md → generative-ui.md to match manifest convention" +``` + +--- + +## Self-Review (completed during planning) + +**Spec coverage:** + +- §1 (`overrideViews`) → T1 +- §2 (engine VIEW_REGISTRY wiring) → T2 +- §3 (chat re-export drop) → T3 +- §4 (chat README override fix) → T4 +- §5 (chat markdown guide fix) → T5 +- §6 (render README) → T6 +- §7 (render views API mdx) → T7 +- §8 (regenerate api-docs.json) → T8 +- §9 (chat CHANGELOG) → T3 (folded — the CHANGELOG entry describes the §3 change, committing them together) +- §10 (ag-ui README interrupt subsection) → T9 +- §11 (new ag-ui interrupts mdx + docs-config) → T10 +- §12 (.gitignore) → T11 +- §13 (dashboard.md rename) → T12 + +All 13 spec items covered. + +**Placeholder scan:** Code blocks complete; verification commands have expected output. The `` snippet in T9 and the doc-content descriptions in T5/T10 are intentionally guidance to the implementer for prose, not unresolved TODOs — they specify what to cover with enough detail for the implementer to write it directly. + +**Name consistency:** `overrideViews`, `VIEW_REGISTRY`, `MARKDOWN_VIEW_REGISTRY`, `cacheplaneMarkdownViews`, `resolveRegistry`, `fallbackRegistry`, `'code-block'`, `injectAgent`, `submit({ resume })`, `forwardedProps.command.resume`, `on_interrupt` — used identically across tasks. + +## Risks + +- **T2 unit tests** depend on TestBed setup conventions in the existing render specs. If the existing setup is incompatible with the proposed test structure (e.g. uses a fixture harness), adapt the assertions to match — the priority order is the invariant, not the test mechanism. +- **T8 api-docs regen** depends on the `generate-api-docs.ts` script tracking exports from `public-api.ts`. If the script picks up extra symbols (some downstream consumers may behave differently), inspect the diff before committing. +- **T10 docs-config.ts** edits the docs-site nav. A typo here breaks the website build; T10 Step 4 catches this. From 5e7f82ff6124dd737fa3bc03a256c6c4406e88a3 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 3 Jun 2026 10:56:56 -0700 Subject: [PATCH 03/15] feat(render): add overrideViews helper for override-semantics composition --- libs/render/src/lib/views.spec.ts | 29 ++++++++++++++++++++++++++++- libs/render/src/lib/views.ts | 12 ++++++++++++ libs/render/src/public-api.ts | 2 +- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/libs/render/src/lib/views.spec.ts b/libs/render/src/lib/views.spec.ts index 66cbb9ab..736161b4 100644 --- a/libs/render/src/lib/views.spec.ts +++ b/libs/render/src/lib/views.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { Component } from '@angular/core'; -import { views, withViews, withoutViews, toRenderRegistry } from './views'; +import { views, withViews, withoutViews, toRenderRegistry, overrideViews } from './views'; @Component({ selector: 'render-test-a', standalone: true, template: 'A' }) class CompA {} @@ -67,6 +67,33 @@ describe('withoutViews()', () => { }); }); +describe('overrideViews()', () => { + it('replaces matching keys; overrides win over base', () => { + const base = views({ 'foo': CompA, 'bar': CompB }); + const result = overrideViews(base, { 'foo': CompC }); + expect(result['foo']).toBe(CompC); + expect(result['bar']).toBe(CompB); + }); + + it('adds new keys not present in base', () => { + const base = views({ 'foo': CompA }); + const result = overrideViews(base, { 'bar': CompB }); + expect(result['foo']).toBe(CompA); + expect(result['bar']).toBe(CompB); + }); + + it('does not mutate base', () => { + const base = views({ 'foo': CompA }); + overrideViews(base, { 'foo': CompB }); + expect(base['foo']).toBe(CompA); + }); + + it('returns a frozen object', () => { + const result = overrideViews(views({}), {}); + expect(Object.isFrozen(result)).toBe(true); + }); +}); + describe('toRenderRegistry()', () => { it('converts ViewRegistry to AngularRegistry', () => { const reg = views({ 'a': CompA, 'b': CompB }); diff --git a/libs/render/src/lib/views.ts b/libs/render/src/lib/views.ts index f3367d39..a61262ad 100644 --- a/libs/render/src/lib/views.ts +++ b/libs/render/src/lib/views.ts @@ -28,6 +28,18 @@ export function withViews( return Object.freeze({ ...additions, ...base }); } +/** + * Replaces views in a registry. Keys in `overrides` win over `base`. + * Use this to swap an existing renderer; use `withViews` to add NEW + * node types without touching existing entries. + */ +export function overrideViews( + base: ViewRegistry, + overrides: Record | RenderViewEntry>, +): ViewRegistry { + return Object.freeze({ ...base, ...overrides }); +} + /** * Removes views from a registry by name. */ diff --git a/libs/render/src/public-api.ts b/libs/render/src/public-api.ts index 44fd43c7..d8c1034c 100644 --- a/libs/render/src/public-api.ts +++ b/libs/render/src/public-api.ts @@ -28,7 +28,7 @@ export { RenderElementComponent } from './lib/render-element.component'; export { RenderSpecComponent } from './lib/render-spec.component'; // Views -export { views, withViews, withoutViews, toRenderRegistry } from './lib/views'; +export { views, withViews, overrideViews, withoutViews, toRenderRegistry } from './lib/views'; export type { ViewRegistry } from './lib/views'; export { provideViews, VIEW_REGISTRY } from './lib/provide-views'; From 3e93527cd7f8629f483869a5e1df29fde6dc0a3e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 3 Jun 2026 11:46:25 -0700 Subject: [PATCH 04/15] feat(render): wire VIEW_REGISTRY as third-priority registry fallback in engine Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/render-element.component.spec.ts | 67 +++++++++++++++++++ .../src/lib/render-spec.component.spec.ts | 30 +++++++++ libs/render/src/lib/render-spec.component.ts | 6 +- 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/libs/render/src/lib/render-element.component.spec.ts b/libs/render/src/lib/render-element.component.spec.ts index d5352778..0ac21115 100644 --- a/libs/render/src/lib/render-element.component.spec.ts +++ b/libs/render/src/lib/render-element.component.spec.ts @@ -489,3 +489,70 @@ describe('RenderElementComponent — fallback gate', () => { expect(fx.nativeElement.querySelector('[data-test="fallback"]')).toBeNull(); }); }); + +// --- VIEW_REGISTRY token-fallback tests (Task 2) --- + +import { RenderSpecComponent } from './render-spec.component'; +import { provideRender } from './provide-render'; +import { provideViews, VIEW_REGISTRY } from './provide-views'; +import { views } from './views'; + +@Cmp2({ standalone: true, template: 'TOKEN' }) +class TokenComp {} + +function specForTokenType(): Spec { + return { + root: 'el1', + elements: { + el1: { type: 'tokenType', props: {} }, + }, + } as Spec; +} + +@Cmp2({ + standalone: true, + imports: [RenderSpecComponent], + template: ``, +}) +class TokenRegistryHost { + spec = specForTokenType(); +} + +describe('RenderSpecComponent — VIEW_REGISTRY token-fallback (Task 2)', () => { + it('VIEW_REGISTRY token: renders component resolved from token when config has no registry', () => { + // provideRender with no registry + provideViews with tokenType → TokenComp. + // RenderSpecComponent should fall back to VIEW_REGISTRY and render TokenComp. + TestBed.configureTestingModule({ + imports: [TokenRegistryHost], + providers: [ + provideRender({}), + provideViews(views({ tokenType: TokenComp })), + ], + }); + const fx = TestBed.createComponent(TokenRegistryHost); + fx.detectChanges(); + expect(fx.nativeElement.querySelector('[data-test="token-comp"]')).toBeTruthy(); + }); + + it('null-path regression: unknown type with no registry anywhere renders nothing (mountClass null)', () => { + // No registry at all — component for an unknown type is null → nothing renders. + const emptyStore = signalStateStore({}); + TestBed.configureTestingModule({ + imports: [FallbackHost], + providers: [{ + provide: RENDER_CONTEXT, + useValue: { + store: emptyStore, + registry: defineAngularRegistry({}), + functions: {}, + handlers: {}, + }, + }], + }); + // btn1 has type 'button' but registry is empty — nothing should render. + const fx = TestBed.createComponent(FallbackHost); + fx.detectChanges(); + expect(fx.nativeElement.querySelector('[data-test="real"]')).toBeNull(); + expect(fx.nativeElement.querySelector('[data-test="fallback"]')).toBeNull(); + }); +}); diff --git a/libs/render/src/lib/render-spec.component.spec.ts b/libs/render/src/lib/render-spec.component.spec.ts index 1bc36162..13ab32e5 100644 --- a/libs/render/src/lib/render-spec.component.spec.ts +++ b/libs/render/src/lib/render-spec.component.spec.ts @@ -8,6 +8,8 @@ import type { RenderEvent, RenderLifecycleEvent } from './render-event'; import { defineAngularRegistry } from './define-angular-registry'; import { signalStateStore } from './signal-state-store'; import { provideRender, RENDER_CONFIG } from './provide-render'; +import { provideViews, VIEW_REGISTRY } from './provide-views'; +import { views } from './views'; // --- Test component --- @@ -112,6 +114,34 @@ describe('RenderSpecComponent — context resolution', () => { expect(config.store!.get('/source')).toBe('config'); // In the component, input > config }); + + it('VIEW_REGISTRY token is used as third-priority fallback when config has no registry', () => { + // Provide a config with no registry, plus VIEW_REGISTRY token with 'Text' mapped. + // The resolved registry should come from the token and be able to look up 'Text'. + TestBed.configureTestingModule({ + providers: [ + provideRender({}), + provideViews(views({ Text: TestTextComponent })), + ], + }); + // Verify the VIEW_REGISTRY token is bound and resolves 'Text' → TestTextComponent. + const tokenRegistry = TestBed.inject(VIEW_REGISTRY); + const angularReg = defineAngularRegistry(tokenRegistry as Record); + expect(angularReg.get('Text')).toBe(TestTextComponent); + // Also verify config has no registry (so the token is the only source). + const config = TestBed.inject(RENDER_CONFIG); + expect(config.registry).toBeUndefined(); + }); + + it('null-path: no registry provided anywhere yields empty registry (no component for unknown type)', () => { + // No providers at all — both RENDER_CONFIG and VIEW_REGISTRY are absent. + TestBed.configureTestingModule({}); + const config = TestBed.inject(RENDER_CONFIG, null); + const tokenRegistry = TestBed.inject(VIEW_REGISTRY, null); + // Both absent — the fallback registry's get() returns undefined. + expect(config).toBeNull(); + expect(tokenRegistry).toBeNull(); + }); }); describe('RenderSpecComponent — event emission', () => { diff --git a/libs/render/src/lib/render-spec.component.ts b/libs/render/src/lib/render-spec.component.ts index dd249c87..2225cc18 100644 --- a/libs/render/src/lib/render-spec.component.ts +++ b/libs/render/src/lib/render-spec.component.ts @@ -14,6 +14,8 @@ import type { ComputedFunction, Spec, StateStore } from '@json-render/core'; import { RenderElementComponent } from './render-element.component'; import { RENDER_CONFIG } from './provide-render'; +import { VIEW_REGISTRY } from './provide-views'; +import { toRenderRegistry } from './views'; import { RENDER_CONTEXT } from './contexts/render-context'; import type { RenderContext } from './contexts/render-context'; import type { AngularRegistry } from './render.types'; @@ -63,6 +65,7 @@ export class RenderSpecComponent implements OnInit { readonly events = output(); private readonly config = inject(RENDER_CONFIG, { optional: true }); + private readonly viewRegistry = inject(VIEW_REGISTRY, { optional: true }); private readonly destroyRef = inject(DestroyRef); private readonly lifecycle = inject(RenderLifecycleService, { optional: true }); @@ -85,12 +88,13 @@ export class RenderSpecComponent implements OnInit { return this.getOrCreateInternalStore(); }); - /** Resolved registry: input > config. */ + /** Resolved registry: input > config > VIEW_REGISTRY token > empty fallback. */ private readonly resolvedRegistry = computed(() => { const inputRegistry = this.registry(); if (inputRegistry) return inputRegistry; const configRegistry = this.config?.registry; if (configRegistry) return configRegistry; + if (this.viewRegistry) return toRenderRegistry(this.viewRegistry); // Fallback: empty registry return { get: () => undefined, getFallback: () => undefined, names: () => [] }; }); From 30596216a2c3d5bba718ab308b5b4ed6283fc084 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 3 Jun 2026 14:53:09 -0700 Subject: [PATCH 05/15] refactor(chat): drop re-export of provideViews/VIEW_REGISTRY from @threadplane/render --- libs/chat/CHANGELOG.md | 1 + libs/chat/src/public-api.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/chat/CHANGELOG.md b/libs/chat/CHANGELOG.md index 5006bf3e..45011375 100644 --- a/libs/chat/CHANGELOG.md +++ b/libs/chat/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changed +- **Public API trim:** `@threadplane/chat` no longer re-exports `provideViews` / `VIEW_REGISTRY` from `@threadplane/render`. Consumers using `` / `` directly should import from `@threadplane/render`. For chat's markdown view overrides, provide `MARKDOWN_VIEW_REGISTRY` directly using `overrideViews(cacheplaneMarkdownViews, { … })` from `@threadplane/render` — the previously-documented `provideViews(withViews(…))` pattern never actually drove rendering. - **License:** `@threadplane/chat` is dual-licensed under PolyForm Noncommercial 1.0.0 (free noncommercial use) or a Threadplane Commercial license (production use inside a for-profit context). ### Migration diff --git a/libs/chat/src/public-api.ts b/libs/chat/src/public-api.ts index a57acba7..42ea7087 100644 --- a/libs/chat/src/public-api.ts +++ b/libs/chat/src/public-api.ts @@ -152,7 +152,6 @@ export { // Views (re-exported from @threadplane/render for convenience) export { views, withViews, withoutViews, toRenderRegistry } from '@threadplane/render'; export type { ViewRegistry } from '@threadplane/render'; -export { provideViews, VIEW_REGISTRY } from '@threadplane/render'; // Streaming / Generative UI export { createContentClassifier } from './lib/streaming/content-classifier'; From 2bc4627f17a755fd19c16f346999d779f528726d Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 3 Jun 2026 15:01:52 -0700 Subject: [PATCH 06/15] docs(chat): fix markdown view override example (MARKDOWN_VIEW_REGISTRY + overrideViews) Co-Authored-By: Claude Sonnet 4.6 --- libs/chat/README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/libs/chat/README.md b/libs/chat/README.md index ce4dfe19..d5d66a6d 100644 --- a/libs/chat/README.md +++ b/libs/chat/README.md @@ -181,16 +181,20 @@ The built-in catalog ships via `a2uiBasicCatalog`. Compose a custom catalog with Override individual node renderers: ```typescript -import { withViews, cacheplaneMarkdownViews, provideViews } from '@threadplane/chat'; +import { MARKDOWN_VIEW_REGISTRY, cacheplaneMarkdownViews } from '@threadplane/chat'; +import { overrideViews } from '@threadplane/render'; import { MyCodeBlockComponent } from './my-code-block.component'; providers: [ - provideViews( - withViews(cacheplaneMarkdownViews, { code: MyCodeBlockComponent }) - ), -], + { + provide: MARKDOWN_VIEW_REGISTRY, + useValue: overrideViews(cacheplaneMarkdownViews, { 'code-block': MyCodeBlockComponent }), + }, +]; ``` +Per-instance, bind the registry on `` instead. Styling uses the existing `--ngaf-chat-*` / `--a2ui-*` tokens — see the [Theming](#theming) section. + The `renderMarkdown(md, options?)` function produces a parse tree for use outside streaming contexts. ### Theming From 350ac8181b43b73c8b626fea5f5bb3318b92c0b4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 3 Jun 2026 15:17:37 -0700 Subject: [PATCH 07/15] docs(website): correct markdown view override path in chat guide Fix the chat markdown guide to use MARKDOWN_VIEW_REGISTRY (correct token), overrideViews from @threadplane/render (not the old provideViews pattern), and 'code-block' (not 'code') as the node-type key. Add app-wide and per-instance override examples, a 22-entry node-type reference table, overrideViews-vs-withViews callout, and brief theming cross-link. Co-Authored-By: Claude Sonnet 4.6 --- .../content/docs/chat/guides/markdown.mdx | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/apps/website/content/docs/chat/guides/markdown.mdx b/apps/website/content/docs/chat/guides/markdown.mdx index 07726893..7e49ec3b 100644 --- a/apps/website/content/docs/chat/guides/markdown.mdx +++ b/apps/website/content/docs/chat/guides/markdown.mdx @@ -128,6 +128,108 @@ export class ChatViewComponent { The markdown styles are scoped to `.chat-md`. Make sure the container element receiving `[innerHTML]` has this class, otherwise the rendered HTML will appear unstyled. +## Streaming Markdown with chat-streaming-md + +`` is the component that renders AI message content token-by-token using the node-based rendering pipeline. It resolves each markdown node type against `MARKDOWN_VIEW_REGISTRY` — a chat-internal DI token exported from `@threadplane/chat`. + +By default the component provides `cacheplaneMarkdownViews` (the full 22-node registry) on its own component injector. You can override this at two levels: + +- **App-wide** — provide a custom registry in your root or feature providers. +- **Per-instance** — pass a `ViewRegistry` via the `[viewRegistry]` input; the component uses that value instead of the DI tree. + +## Overriding Markdown Components + +### App-wide override + +To replace a node-type renderer for every `` in your app, provide a custom `MARKDOWN_VIEW_REGISTRY` in your application config: + +```typescript +import { ApplicationConfig } from '@angular/core'; +import { MARKDOWN_VIEW_REGISTRY, cacheplaneMarkdownViews } from '@threadplane/chat'; +import { overrideViews } from '@threadplane/render'; +import { MyCodeBlockComponent } from './my-code-block.component'; + +export const appConfig: ApplicationConfig = { + providers: [ + { + provide: MARKDOWN_VIEW_REGISTRY, + useValue: overrideViews(cacheplaneMarkdownViews, { + 'code-block': MyCodeBlockComponent, + }), + }, + ], +}; +``` + +`overrideViews(base, overrides)` replaces every key listed in `overrides` and preserves all other entries from `base`. Import it from `@threadplane/render` (chat does not re-export it). + + +Use `overrideViews` when replacing an existing node type. Use `withViews` when adding a brand-new node type that `cacheplaneMarkdownViews` does not yet cover — `withViews` is additive-only and the base registry wins on conflicts. See the [render views API](/docs/render/api/views) for full signatures. + + +### Per-instance override + +Pass a `ViewRegistry` directly to a single `` via its `[viewRegistry]` input. The component uses the provided value and ignores the DI tree for that instance: + +```typescript +import { Component } from '@angular/core'; +import { ChatStreamingMdComponent, cacheplaneMarkdownViews } from '@threadplane/chat'; +import { overrideViews } from '@threadplane/render'; +import { MyCodeBlockComponent } from './my-code-block.component'; + +@Component({ + selector: 'app-custom-chat', + standalone: true, + imports: [ChatStreamingMdComponent], + template: ` + + `, +}) +export class CustomChatComponent { + content = ''; + myRegistry = overrideViews(cacheplaneMarkdownViews, { + 'code-block': MyCodeBlockComponent, + }); +} +``` + +## Node-Type Reference + +`cacheplaneMarkdownViews` covers every node type emitted by `@cacheplane/partial-markdown`. Use these keys when calling `overrideViews` or `withViews`. + + +The most common mistake is providing `'code'` as an override key — it does not match anything in the registry. The correct key for fenced code blocks is `'code-block'`. + + +| Key | Description | +|-----|-------------| +| `'document'` | Root node wrapping the entire parsed document | +| `'paragraph'` | Block-level paragraph (`

`) | +| `'heading'` | Heading element (`

` through `

`) | +| `'blockquote'` | Block-level quotation (`
`) | +| `'list'` | Ordered or unordered list (`
    ` / `