diff --git a/docs/superpowers/plans/2026-07-05-partial-markdown-streaming-body-rows.md b/docs/superpowers/plans/2026-07-05-partial-markdown-streaming-body-rows.md new file mode 100644 index 000000000..bbd281357 --- /dev/null +++ b/docs/superpowers/plans/2026-07-05-partial-markdown-streaming-body-rows.md @@ -0,0 +1,367 @@ +# Streaming Table Body Rows 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:** An in-progress table body row renders as a streaming row inside the table's `
` (growing cell-by-cell) instead of flickering as a raw-pipe paragraph or a spurious second table. + +**Architecture:** One `optimisticBlock`-gated branch in `handleBlockLine` (`block.ts`), placed before the `mode === 'table'` close: a partial open-line row (leading pipe, no trailing pipe yet) appends via the existing `appendTableRow` builder on the throwaway projection state. Committed parse untouched. Ships as `@cacheplane/partial-markdown` 0.5.3, consumed by `libs/chat`; a Chrome MCP live-stream capture is a required verification gate. + +**Tech Stack:** TypeScript, Vitest, tsup (cacheplane); Angular/Vitest (ngaf `libs/chat`); Chrome MCP for the live gate. + +**Repo note:** Tasks 1–3 run in **`~/repos/cacheplane`** (branch off main). Tasks 4–6 run in **`~/repos/angular-agent-framework`**. cacheplane main is PROTECTED (3 required checks) — release goes PR → squash-merge → annotated tag on the merged commit. + +**Spec:** [docs/superpowers/specs/2026-07-05-partial-markdown-streaming-body-rows-design.md](../specs/2026-07-05-partial-markdown-streaming-body-rows-design.md) + +--- + +## File Structure + +**`~/repos/cacheplane/packages/partial-markdown`:** +- `src/handlers/block.ts` — `OPEN_TABLE_ROW_RE` (next to `TABLE_HEADER_INPROGRESS_RE`, ~line 682) + the new branch in `handleBlockLine` (before the table close at ~line 97). +- `src/__tests__/streaming-table.test.ts` — new `describe` block for body rows (appended; reuses the existing `blocks()` helper). +- `CHANGELOG.md`, `package.json` — 0.5.3. + +**`~/repos/angular-agent-framework`:** +- `libs/chat/package.json`, `package-lock.json` — `^0.5.2 → ^0.5.3` (surgical lockfile edit; never regenerate). +- `libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts` — body-row anti-flicker test appended. + +--- + +## Task 1: Body-row projection branch (TDD) + +**Repo:** `~/repos/cacheplane` — first create the branch: `git checkout main && git pull --ff-only && git checkout -b feat/streaming-table-body-rows` + +**Files:** +- Modify: `packages/partial-markdown/src/handlers/block.ts` (~line 97 branch; ~line 682 regex) +- Test: `packages/partial-markdown/src/__tests__/streaming-table.test.ts` (append) + +- [ ] **Step 1: Write the failing tests** + +Append to `packages/partial-markdown/src/__tests__/streaming-table.test.ts` (the file already imports `createPartialMarkdownParser`/`materialize` and defines `blocks()`): + +```ts +describe('streaming table body rows — open-line projection', () => { + // Committed header + delimiter, so the table is active (mode 'table'). + const HEADER = '| A | B |\n| - | - |\n'; + + it('projects a bare "|" as an empty in-progress row (no paragraph)', () => { + const p = createPartialMarkdownParser(); + p.push(HEADER); + p.push('|'); + expect(blocks(p)).toEqual([{ type: 'table', status: 'streaming' }]); + const doc = materialize(p.root) as any; + expect(doc.children[0].children).toHaveLength(2); // header + empty in-progress row + }); + + it('projects a partial row into the SAME table (no spurious second table)', () => { + const p = createPartialMarkdownParser(); + p.push(HEADER); + p.push('| x1 | y'); // two pipes, no trailing pipe — used to spawn a 2nd optimistic table + expect(blocks(p)).toEqual([{ type: 'table', status: 'streaming' }]); + const doc = materialize(p.root) as any; + const rows = doc.children[0].children; + expect(rows).toHaveLength(2); // header + in-progress body row + const lastRow = JSON.stringify(rows[1]); + expect(lastRow).toContain('x1'); + expect(lastRow).toContain('y'); + }); + + it('grows the in-progress row cell-by-cell, padded to header width', () => { + const p = createPartialMarkdownParser(); + p.push(HEADER); + p.push('| x1'); + let doc = materialize(p.root) as any; + expect(doc.children).toHaveLength(1); + expect(doc.children[0].children[1].children).toHaveLength(2); // padded to 2 cols + p.push(' | y1'); + doc = materialize(p.root) as any; + expect(JSON.stringify(doc.children[0].children[1])).toContain('y1'); + }); + + it('still appends a COMPLETE open-line row (regression guard)', () => { + const p = createPartialMarkdownParser(); + p.push(HEADER); + p.push('| x1 | y1 |'); // trailing pipe — pre-existing mid-table branch + const doc = materialize(p.root) as any; + expect(doc.children).toHaveLength(1); + expect(doc.children[0].children).toHaveLength(2); + }); + + it('leaves the committed parse unchanged (newline-terminated + finish)', () => { + const p = createPartialMarkdownParser(); + p.push('| A | B |\n| - | - |\n| x1 | y1 |\n| x2 | y2 |\n'); + p.finish(); + const doc = materialize(p.root) as any; + expect(doc.children).toHaveLength(1); + expect(doc.children[0].type).toBe('table'); + expect(doc.children[0].status).toBe('complete'); + expect(doc.children[0].children).toHaveLength(3); // header + 2 body rows + }); + + it('a blank open line still closes nothing prematurely (table stays, no extra row)', () => { + const p = createPartialMarkdownParser(); + p.push(HEADER); + // No open line at all: committed table only. + expect(blocks(p)).toEqual([{ type: 'table', status: 'streaming' }]); + const doc = materialize(p.root) as any; + expect(doc.children[0].children).toHaveLength(1); // header only + }); +}); +``` + +- [ ] **Step 2: Run to verify the new tests fail** + +Run: `pnpm -C packages/partial-markdown test -- streaming-table` +Expected: FAIL — bare `|` yields `[table, paragraph]`, `| x1 | y` yields `[table, table]` (the 0.5.2 artifacts). The complete-row, committed-unchanged, and header-only tests already pass. + +- [ ] **Step 3: Add the regex + branch** + +In `packages/partial-markdown/src/handlers/block.ts`: + +(a) Next to the existing header regex (~line 682, immediately above `const TABLE_HEADER_INPROGRESS_RE`): + +```ts +// A line that has STARTED a table row but not necessarily finished it: up to +// three spaces of indent then a leading pipe. Prefix-consistent with +// TABLE_ROW_RE (which additionally requires the trailing pipe). +const OPEN_TABLE_ROW_RE = /^\s{0,3}\|/; +``` + +(b) In `handleBlockLine`, find (~line 96): + +```ts + // Close open table if current line is not a table row. + if (s.mode === 'table' && !TABLE_ROW_RE.test(line)) { + s = closeOpenTable(s); + } +``` + +Insert **immediately before** it: + +```ts + // Optimistic projection only: while a table is active, an open line that + // begins a new row (leading pipe, row not yet complete) projects as an + // in-progress body row appended to the active table — instead of closing the + // table and rendering the partial row as a paragraph or a spurious second + // optimistic header table. A bare "|" projects as an empty in-progress row. + if (s.optimisticBlock && s.mode === 'table' && OPEN_TABLE_ROW_RE.test(line) && !TABLE_ROW_RE.test(line)) { + return appendTableRow(s, line); + } + +``` + +Note: the local variable at this point is `s` (declared just above as `{ ...state, line: state.line + 1, lineBuffer: '' }`) — verify by reading the surrounding code. + +- [ ] **Step 4: Run the streaming-table suite to verify green** + +Run: `pnpm -C packages/partial-markdown test -- streaming-table` +Expected: PASS (all, including the 12 pre-existing 0.5.2 tests). + +- [ ] **Step 5: Run the full package suite (incl. chunk-fuzz)** + +Run: `pnpm -C packages/partial-markdown test` +Expected: PASS — the chunk-invariance property (table corpus entry) re-verifies the new branch never changes committed output under any chunk split. + +- [ ] **Step 6: Commit** + +```bash +cd ~/repos/cacheplane +git add packages/partial-markdown/src/handlers/block.ts packages/partial-markdown/src/__tests__/streaming-table.test.ts +git commit -m "feat(partial-markdown): project in-progress table body rows on the open line" +``` + +--- + +## Task 2: Lint, typecheck, CHANGELOG, version 0.5.3 + +**Repo:** `~/repos/cacheplane` + +**Files:** +- Modify: `packages/partial-markdown/CHANGELOG.md`, `packages/partial-markdown/package.json` + +- [ ] **Step 1: Lint + typecheck** + +Run: `pnpm -C packages/partial-markdown lint && pnpm -C packages/partial-markdown typecheck` +Expected: PASS, no errors. Fix any issues before continuing. + +- [ ] **Step 2: Prepend the CHANGELOG entry** (above `## 0.5.2 — 2026-06-27`): + +```markdown +## 0.5.3 — 2026-07-05 + +### Added + +- **Streaming table body rows.** While a table is active, an in-progress body + row on the open line now renders as a streaming row inside the table — + growing cell-by-cell, padded to header width — instead of momentarily + closing the table and flashing as a raw-pipe paragraph (bare `|` / one pipe) + or a spurious second header-only table (two+ pipes). A bare `|` projects as + an empty in-progress row. Projection-only: the committed parse is unchanged. + Completes the streaming-table work begun in 0.5.2. +``` + +- [ ] **Step 3: Bump version** — `packages/partial-markdown/package.json`: `"version": "0.5.2"` → `"version": "0.5.3"`. + +- [ ] **Step 4: Build** — Run: `pnpm -C packages/partial-markdown build` — Expected: tsup regenerates `dist/` cleanly. + +- [ ] **Step 5: Commit** + +```bash +cd ~/repos/cacheplane +git add packages/partial-markdown/CHANGELOG.md packages/partial-markdown/package.json +git commit -m "chore(partial-markdown): 0.5.3 — streaming table body rows" +``` + +--- + +## Task 3: Release 0.5.3 (PR → tag → npm) + +**Repo:** `~/repos/cacheplane`. Main is protected (3 required checks: Workspace install+lint+typecheck, Package partial-json, Package partial-markdown) — direct pushes are rejected. + +- [ ] **Step 1: Dry-run the publish** + +```bash +cd ~/repos/cacheplane/packages/partial-markdown && npm publish --dry-run; cd - +``` +Expected: would publish `@cacheplane/partial-markdown@0.5.3`, `dist/` files included. + +- [ ] **Step 2: Push branch + open PR** + +```bash +cd ~/repos/cacheplane +git push -u origin feat/streaming-table-body-rows +gh pr create --fill --title "feat(partial-markdown): streaming table body rows (0.5.3)" +``` + +- [ ] **Step 3: Merge on green (3 checks), then tag the merged main commit** + +```bash +gh pr checks` and the `'| x1 | y'` step renders 2 tables.
+
+- [ ] **Step 4: Bump dep + surgical lockfile edit** (values from Task 3). Refresh the on-disk copy WITHOUT touching the lockfile: `cd /tmp && npm pack @cacheplane/partial-markdown@0.5.3 && tar -xzf cacheplane-partial-markdown-0.5.3.tgz && rm -rf ~/repos/angular-agent-framework/node_modules/@cacheplane/partial-markdown && cp -r package ~/repos/angular-agent-framework/node_modules/@cacheplane/partial-markdown`. Verify: `cd ~/repos/angular-agent-framework && npm ls @cacheplane/partial-markdown` → `0.5.3`, no invalid/missing.
+
+- [ ] **Step 5: Verify the test passes + gates**
+
+Run: `npx nx test chat --skip-nx-cache -- streaming-markdown.table-stream` → PASS.
+Run: `npx nx lint chat && npx nx test chat && npx nx build chat` → all PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add libs/chat/package.json package-lock.json libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts
+git commit -m "build(chat): consume partial-markdown 0.5.3 — streaming table body rows
+
+Co-Authored-By: Claude Opus 4.8 (1M context) ` frames; the #743 baseline measured 2 such frames — must now be 0),
+ - final DOM: one table, 4 `thead th`, 12 `tbody tr`.
+ If any assertion fails: STOP, root-cause (systematic-debugging), do not proceed to Task 6.
+
+- [ ] **Step 4: Shut down the stack** — kill the serve processes by PID (`lsof -ti :4200 :2024`); confirm both ports free. (The nx process tree respawns children — kill by PID, verify, repeat if needed.)
+
+---
+
+## Task 6: PR + merge on green
+
+**Repo:** `~/repos/angular-agent-framework`
+
+- [ ] **Step 1: Push + PR**
+
+```bash
+git push -u origin feat/chat-consume-pm-0.5.3
+gh pr create --title "build(chat): consume partial-markdown 0.5.3 — streaming table body rows" --fill
+```
+Include in the body: root cause recap (0.5.2 deferred body rows; partial open-line row closed the table → paragraph / 2nd-table flicker), the fix (projection-only body-row branch), test evidence (cacheplane suite, consumer red→green), and the Chrome MCP capture numbers from Task 5.
+
+- [ ] **Step 2: Merge on green**
+
+```bash
+gh pr checks ` siblings at every step.
+
+**Live verification (required gate, Chrome MCP):**
+- Run examples/chat locally (`nx run examples-chat:serve`), drive it via Chrome MCP: send a 10+ row table prompt, sample the last `chat-streaming-md` DOM every ~16ms during the stream, and assert:
+ - `tables === 1` in every frame after the header appears (no second table),
+ - `pipeParagraphs === 0` in every frame (no raw-pipe paragraph),
+ - final table has the full row count.
+- This is the same harness used to verify #743; the pre-fix baseline there measured `framesTableWithPipe = 2` — post-fix it must be 0.
+
+## Release & consume
+
+1. cacheplane: bump `0.5.2 → 0.5.3`, CHANGELOG, PR to protected main (3 required checks), squash-merge, then annotated tag `partial-markdown-v0.5.3` on the merged commit → OIDC publish; verify on npm (CDN may lag ~1 min).
+2. ngaf: bump `libs/chat` dep `^0.5.2 → ^0.5.3`, surgical lockfile edit (never regenerate on macOS), chat lint/test/build, the Chrome MCP live gate above, PR, merge on green Vercel.
+
+## Out of scope
+
+- Setext headings; non-pipe-fenced tables; live alignment during the delimiter stream (unchanged from 0.5.2's out-of-scope list).
+- Any chat-side rendering changes — none needed.
diff --git a/libs/chat/package.json b/libs/chat/package.json
index 428e579cc..c8c6ea73e 100644
--- a/libs/chat/package.json
+++ b/libs/chat/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
"@cacheplane/partial-json": ">=0.1.1 <0.3.0",
- "@cacheplane/partial-markdown": "^0.5.2"
+ "@cacheplane/partial-markdown": "^0.5.3"
},
"peerDependencies": {
"zod": "^3.25.0",
diff --git a/libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts b/libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts
index e8e0b4f09..cb1282a5b 100644
--- a/libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts
+++ b/libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts
@@ -97,4 +97,17 @@ describe('ChatStreamingMdComponent — streaming table rendering', () => {
vi.useRealTimers();
}
});
+
+ it('streams body rows inside the table — no paragraph, no second table (0.5.3)', () => {
+ host.streaming.set(true);
+ grow('| A | B |\n| - | - |\n');
+ for (const c of ['|', '| x1', '| x1 | y', '| x1 | y1 |', '| x1 | y1 |\n']) {
+ grow('| A | B |\n| - | - |\n' + c);
+ expect(el.querySelectorAll('table').length, `one table at ${JSON.stringify(c)}`).toBe(1);
+ expect(
+ [...el.querySelectorAll('p')].some((p) => (p.textContent || '').includes('|')),
+ `no raw-pipe paragraph at ${JSON.stringify(c)}`,
+ ).toBe(false);
+ }
+ });
});
diff --git a/package-lock.json b/package-lock.json
index 1b0d13b37..dc93f494a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,7 +29,7 @@
"@angular/platform-browser": "~21.1.0",
"@angular/router": "~21.1.0",
"@cacheplane/partial-json": "^0.1.1",
- "@cacheplane/partial-markdown": "^0.5.2",
+ "@cacheplane/partial-markdown": "^0.5.3",
"@langchain/core": "^1.1.33",
"@langchain/langgraph-sdk": "^1.7.4",
"@neondatabase/serverless": "^0.10.0",
@@ -207,7 +207,7 @@
"license": "PolyForm-Noncommercial-1.0.0 OR LicenseRef-Threadplane-Commercial",
"dependencies": {
"@cacheplane/partial-json": ">=0.1.1 <0.3.0",
- "@cacheplane/partial-markdown": "^0.5.2"
+ "@cacheplane/partial-markdown": "^0.5.3"
},
"peerDependencies": {
"@angular/common": "^20.0.0 || ^21.0.0",
@@ -7272,9 +7272,9 @@
"license": "MIT"
},
"node_modules/@cacheplane/partial-markdown": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/@cacheplane/partial-markdown/-/partial-markdown-0.5.2.tgz",
- "integrity": "sha512-8ItJlOjihgvIBEZ269XfZHoU+yZ7TDhqNyxopYeJiyaa3ROISwAY2Q/2CAgUHFLDXZtk+9xn1qElntIkcOJjlw==",
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@cacheplane/partial-markdown/-/partial-markdown-0.5.3.tgz",
+ "integrity": "sha512-XlvjGVsEY7tA3Q4eSY0EiP3FmKkzv5VwFWCmmqvnvf1/dYy+YjQXmDg1mqtFWk4iNyjMMd7aBtGiY+lEuZPmLQ==",
"license": "MIT",
"engines": {
"node": ">=20"