table-documents-handoff.md9.4 KBView on GitHub # Table documents — handoff for frontend testing
**Branch:** `feat/table-documents` (pushed to origin, 31 commits ahead of `staging`)
**Design doc:** `apps/mail/docs/table-documents.md` — the full spec, with every phase ticked and
the two deliberate omissions recorded
**Architecture wiki:** `apps/server/docs/wiki/table-documents.md` (server) and
`apps/mail/docs/wiki/table-grid.md` (client)
This document is for whoever picks the branch up to **test it in the frontend**. It is not a
summary of the diff — it is what you need to know to exercise it and to judge what you see.
---
## 1. What this feature actually is
A `table` document type: a Y.js-backed spreadsheet that renders as a real data grid.
The important part is *why* it exists, because it changes what "working" looks like. A table is
Cedar's **parallel-agent-work primitive**. When a task has N independent units — "draft a
follow-up for each of these 40 deals" — the old shape was one agent looping 40 times and
finishing with a chat message summarising work you cannot inspect. Instead you materialize the
work list as rows and fan out one subagent per row. Materializing the list IS the plan, filling it
IS the execution, reading it afterwards IS the audit trail. Rows fill in live in front of you.
So a table that renders correctly but cannot be filled by a fan-out is half the feature.
## 2. The one architectural decision worth knowing
Rows live in **sibling top-level Y types** (`ydoc.getMap('tableSchema')`,
`ydoc.getArray('tableRows')`) *beside* the `prosemirror` XmlFragment — not inside it.
This works because TipTap's `Collaboration` binds only `field: 'prosemirror'`, so every server
path (applyUpdate, the advisory lock, `document_updates`/`document_snapshots`, `docEventBus`,
share tokens) and every client transport (`CedarYjsProvider`, IndexedDB, `useDocEvents`) keeps
working untouched, while rendering is free to be a grid instead of a ProseMirror node view.
Measured payoff: a one-cell `set` is a single `Y.Map.set` — a **71-byte delta at 100, 500 and
1,000 rows** (flat). That is the property the whole design rests on.
## 3. Testing it — the click paths
### Create a table by hand (no agent needed)
1. `/brain` → **Files** tab
2. Hover a scope-root or folder row → click the **`+`** button
*(it is `opacity-0 group-hover/row:opacity-100`, so it only appears on hover — this is the
pre-existing pattern for all file creation, not table-specific)*
3. Click the **Table** tab in the popover (`CompanyExplorer.tsx:340`), name it, press Enter
You get a grid with three starter columns (Name / Status / Notes). Then exercise:
- click a cell → a TipTap editor mounts for that one cell; type `@` and a suggestion popover
should open, identical to prose
- Enter commits and moves down; Tab commits and moves right; Escape reverts and returns focus to
the grid (arrow keys / Enter from there put you back in a cell)
- Add row, Add column, the column menu (type changes, rename, hide, delete)
- Export → Download as Excel / CSV
- 🔧 → Compact table
- Print → should render a real `<table>`, not pipe syntax
### Convert a spreadsheet
Upload an `.xlsx`/`.csv` in the file explorer → click the attachment → **Convert to table**
(`CompanyExplorer.tsx:2110`).
### The agent path — READ THIS BEFORE CONCLUDING IT IS BROKEN
Ask chat something like *"make a table of my open deals and research each one"*.
The `table` tool lives in `FAMILY_TOOLS`, which the claude-agent-sdk harness exposes. The other
branch of that router is the **legacy Mastra `chatAgent`** — which carries 9 static tools, has no
`table`, and which `chat-workflow.ts` describes as "the one no deployed traffic exercises."
So in staging and prod this is not a concern: the harness is the path. Locally, PostHog cannot
answer, so chat falls onto the legacy branch unless `USE_CHAT_AGENT_SDK="true"` is set — and
`.env.example` already sets it. **Check your local `.env` has that line**; that is the whole of
it, not a feature gate to reason about.
### Sharing
Share a table document and open the link — it renders a read-only preview from the markdown
mirror (no Y.js provider or session on that page, which is correct).
## 4. Deploy prerequisites: none
Verified, not assumed:
| | |
|---|---|
| DB migration | **None.** `documents.document_type` is `text(...)`, not a PG enum, so `'table'` is already a legal value. Zero schema files touched, zero `.sql` added. |
| New npm deps | **None.** `exceljs` already existed in both apps. The only `package.json` change is two subpath exports (`./table`, `./table/ydoc`). No lockfile change. |
| Infra / CDK | **None.** `git diff origin/staging...HEAD -- aws/` is empty. |
| Env vars | One optional kill switch: `TABLE_STREAM_DISABLED`, default `'false'` (streaming on). Nothing to set. |
| Frontend flag | **None.** The create tab and the grid are unconditional. |
## 5. What to be suspicious of
This branch was reviewed twice — once over the feature, once over the fixes — and the second pass
found a regression the first round of fixes had introduced. The failure mode that kept recurring is
worth carrying while you test:
> **Bugs here produce plausible nothing, not errors.** An empty markdown mirror, a stripped zod
> field, a dropped filter clause, a blank version preview, a restore that reports success and
> changes nothing. Nothing throws.
So when testing, prefer checking that something *specific* happened over checking that nothing
broke. Concretely, the things most worth poking:
- **Concurrent edit vs agent write.** Click into a cell during a fan-out and click away without
typing. The agent's value must survive. (This was a real bug: the commit guard compared the
editor's serialized value against the *current* cell value, so it wrote the stale text back
precisely when someone else had changed it.)
- **Round-trip a weird cell.** A value with a leading space, a tab, a `|`, a newline, a Windows
path, an emoji. Type it, reload, export to Excel, re-import. The markdown mirror is
authoritative on rehydrate, so anything it cannot represent is silently replaced on the next
save.
- **Version history.** Open History on a table — versions should preview with content, and
Restore should actually change the grid.
- **A column labelled `_status`.** Should be refused. (It used to shift every following column
and fabricate fan-out state out of ordinary text.)
## 6. Known gaps — deliberate, do not report as bugs
- **No scheduled fills.** Removed on purpose (commit `e8c2129eb`). A schedule needed a cron pass,
a KV registration and drain, a run lease, two stop conditions, ten procedures and a polling
toolbar — all to save the user from re-prompting. Long-lived rows still work: the row's own
`status` column holds where it is, and "advance the ones that are connected" tomorrow runs the
same `where`-filtered fill. Rationale is in the design doc under Phase 12.
- **`fill` over external MCP returns briefs instead of spawning.** Not a gap — the design. Check
`fill.mode`: `spawned` means Cedar ran the fan-out (in-app), `delegate` means the caller runs it
from `fill.briefs`, one ready-to-use task per row, with `fill.howToRun` spelling out the
contract. Claude Code already has subagent spawning and its subagents inherit its own
permissions, so this is strictly safer than Cedar spawning a row agent whose static tool
allowlist the caller's scopes do not bound — which is what made the verb in-app-only before.
- **LinkedIn steps do not go through the outbound engine.** Design decision, recorded in the doc.
- **The bloat-alert Axiom monitor is not created.** The signal is emitted
(`[tableStats] compaction due` at `warn` with `bloatRatio`); creating the monitor needs an owner
and a notification target.
## 7. Verification state
- Both typechecks clean (`@zero/server`, `@zero/mail`)
- `pnpm deps:check` clean — no circular imports
- **eslint clean on every file this branch touches** (remaining errors under those paths are in
code from `staging`, verified by `git blame`)
- Server: 725 tests pass across the touched surface; mail: 1,252 tests pass
- Two load-sensitive flakes seen and understood: `table-compaction` (fixed — it was doing 2.5×
the work its assertion needed) and `calendar-events-sync` (pre-existing; its subject module is
fully mocked, so nothing here can reach it)
**Not verified: a real click-through in a running app.** Everything above is code-reading,
reproduction scripts and tests. That is exactly the gap this handoff exists to close.
## 8. Where things live
```
apps/server/src/services/documents/table/ the layout, markdown codec, sources, bindings,
fan-out, compaction, escaping (25 modules)
apps/server/src/services/document-saving/ writeTableAsYjs.ts — the row-granular writer
apps/server/src/mastra/tools/document/ tableTool.ts — the agent surface; table-row-agent.ts
apps/mail/modules/documents/table/ the grid (22 modules) — useYTable.ts is the Y binding
apps/server/src/trpc/routes/ documents.ts (user), admin-tables.ts (staff)
apps/server/src/table-admin/cli.ts `pnpm cedar-cli table …` — the headless driver
```
Headless smoke test, if you want to prove the backend without the UI:
```bash
pnpm cedar-cli table create --user <id> --path user/tables/smoke --schema-file s.json
pnpm cedar-cli table read --user <id> --path user/tables/smoke
pnpm cedar-cli table destroy --user <id> --path user/tables/smoke --confirm
```