composite-dom-render.test.tsxfix: composite playbook renders empty due to invalid parsed content The composite playbook editor loaded blank even though the server data was intact. Root cause: empty markdown leaves (e.g. an empty ``` fence in a cross-cutting block) parse to a codeBlock holding an empty text node ({type:'text',text:''}), which ProseMirror forbids — so setContent throws "Empty text nodes are not allowed" and the whole doc fails to render. Strip empty text nodes at the source (parsePlaybookXmlToJson) and again in mergeComposite. Also hardens the open→render→save path so this class of failure can't recur silently or destroy data: - always fetch the playbook fresh (blacklist from IndexedDB persistence + staleTime 0 / refetchOnMount 'always') so a stale/empty cached copy is never rendered without a network call - defer setContent to a microtask so TipTap's React NodeViews render (React refuses flushSync inside a lifecycle) - re-seed on fresh data without clobbering unsaved edits; surface a visible error instead of a silent blank on hydration failure - refuse to overwrite a populated playbook with an empty doc (server guard in saveCompositePlaybook + client guard when unhydrated) - getChatAgentPlaybook fetches its documentId fresh too Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jul 6, 2026, 9:32 PMcomposite-hydration.test.tsxfix: composite playbook renders empty due to invalid parsed content The composite playbook editor loaded blank even though the server data was intact. Root cause: empty markdown leaves (e.g. an empty ``` fence in a cross-cutting block) parse to a codeBlock holding an empty text node ({type:'text',text:''}), which ProseMirror forbids — so setContent throws "Empty text nodes are not allowed" and the whole doc fails to render. Strip empty text nodes at the source (parsePlaybookXmlToJson) and again in mergeComposite. Also hardens the open→render→save path so this class of failure can't recur silently or destroy data: - always fetch the playbook fresh (blacklist from IndexedDB persistence + staleTime 0 / refetchOnMount 'always') so a stale/empty cached copy is never rendered without a network call - defer setContent to a microtask so TipTap's React NodeViews render (React refuses flushSync inside a lifecycle) - re-seed on fresh data without clobbering unsaved edits; surface a visible error instead of a silent blank on hydration failure - refuse to overwrite a populated playbook with an empty doc (server guard in saveCompositePlaybook + client guard when unhydrated) - getChatAgentPlaybook fetches its documentId fresh too Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jul 6, 2026, 9:32 PMcomposite-merge.test.tsfix: composite playbook renders empty due to invalid parsed content The composite playbook editor loaded blank even though the server data was intact. Root cause: empty markdown leaves (e.g. an empty ``` fence in a cross-cutting block) parse to a codeBlock holding an empty text node ({type:'text',text:''}), which ProseMirror forbids — so setContent throws "Empty text nodes are not allowed" and the whole doc fails to render. Strip empty text nodes at the source (parsePlaybookXmlToJson) and again in mergeComposite. Also hardens the open→render→save path so this class of failure can't recur silently or destroy data: - always fetch the playbook fresh (blacklist from IndexedDB persistence + staleTime 0 / refetchOnMount 'always') so a stale/empty cached copy is never rendered without a network call - defer setContent to a microtask so TipTap's React NodeViews render (React refuses flushSync inside a lifecycle) - re-seed on fresh data without clobbering unsaved edits; surface a visible error instead of a silent blank on hydration failure - refuse to overwrite a populated playbook with an empty doc (server guard in saveCompositePlaybook + client guard when unhydrated) - getChatAgentPlaybook fetches its documentId fresh too Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jul 6, 2026, 9:32 PMplaybook-document-owner.test.tsxfix(lint): oxlint reaches zero, without a single underscore CI runs oxlint with --deny-warnings, and this branch carried nine. Every one is in a test, and all nine are the same two shapes. Two unused imports and a dead type alias are deletions, which is the rule. The other six are `vi.fn(async (...args: unknown[]) => …)` — a rest parameter nobody reads, but not dead code either: it is what lets the mock accept the arguments its call site spreads into it, so deleting the binding turns every `spy(...a)` into "expected 0 arguments". The signature moves into `vi.fn`'s type parameter instead, where it goes on saying the same thing to the compiler with nothing left over for the linter to find. Underscoring them was the other option and the repo bans it, for the reason this case illustrates: `_args` would have hidden a parameter that is load-bearing behind a convention that means "ignore me". Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 10, 2026, 8:42 PMpost-api-node.test.tsxfeat(files): the agent workspace's own rows join the kit (design: unified-files-ui phase 4) The Output tab's tree already came from the conversation tree, but its two flat lists did not: "Touched elsewhere" drew a rounded-square tile with a mono path under the name, and "In conversations" drew a round medallion with an emoji. Three chromes, one tab. Both now render `FileListRow`, so the columns line up with the tree above them. The path keeps its place as provenance — in the meta slot, one line, truncated, full value on hover — rather than as a second line that would make every row twice as tall as the tree's. Editor identity for both lists comes from ONE batched `documents.lastEditors` over the union of their ids, the shape `files.unseenAgentEdits` established, because the alternative is a request per row on every render. Two fixes that were in the way: - the shared tRPC test mock resolved queries to `undefined`, which TanStack rejects — every component firing an unstubbed query buried its own test output in "Query data cannot be undefined". - the playbook slash-menu test still expected nine entries after Board, Table and Document were added to the menu in dfc2197fe. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 9:57 PMtrigger-dispatch.test.tsxfix(agents): drop the building glyph, green the calendar, unlabel the instruction box THE GLYPH ROW. `external_crm` loses its mark entirely rather than gaining a better one. Email and Slack wear real vendor logos and a meeting is a calendar, but "the external CRM" is whichever of HubSpot, Salesforce, Attio or Copper this user connected — so any single glyph either names the wrong vendor or names nothing. A building says "a company" and invites the question "which one?", to which there is no answer; sync arrows say "refresh", which is an action rather than an event. An honest gap beats a mark that needs explaining. So `Icon` is now optional on TriggerEventType, and the absence is the documented point. `external_crm` stays a first-class event everywhere it matters — the picker offers it, the row's accessible name lists it, the `?` explainer names it in prose. Only the compact glyph row skips it, and that row was always a summary rather than a census. The trigger badge falls back to the same generic mark `any` wears. The meeting calendar is green, matching what the conversation timeline gives a meeting. No tinted discs: the two vendor logos hardcode their brand fills, so a text colour cannot reach them, and the calendar was the one grey mark in a row of colour — it was the only one that needed anything. THE INSTRUCTION BOX. "Instructions" as a visible label made every source row read as a section heading over a box; stacked six deep that is six headings for one idea. The label is gone and the sentence under the box does the naming instead — which it had to do anyway, because "Instructions" alone never said WHICH instructions or how they differ from the agent's own: "Trigger-specific instructions (optional): if you want special instructions for only this trigger to be given to the agent." That needed a real kit change, not a workaround: `Field` now accepts `label={null}` and renders no label element. It is `null` rather than an omitted prop because a label is the default and dropping one should be a decision somebody typed — and the control still carries an `aria-label`, since "obvious from context" is only true for the reader who can see the context. 330 agent + playbook tests green, including new ones pinning that external_crm is named but not drawn, and that only the meeting carries a tint. Co-Authored-By: Claude Opus 5 <<email>>Sep 8, 2026, 11:59 AMtrigger-ref-panel.test.tsxfix(playbook): the four things wrong with the trigger UI 1. TWO GLYPHS THAT SAID NOTHING. The meeting glyph was invisible. It asked `connectionLogo('meetings')`, which resolves to the hand-drawn icons.tsx Calendar — an `<svg fill="none">` whose path sets no fill of its own, so it inherits `none` and paints NOTHING unless the call site passes a `fill-*` class. That row passes only a size. Fixed at both ends: the glyph row now uses lucide's Calendar (which carries its own stroke), and the shared icon gets `fill="currentColor"` on its path so every other call site that passes only a size renders too. It is a presentation attribute, which a CSS class beats, so date-picker-dialog's `fill-[#9D9D9D]` is unaffected. The external-CRM glyph was sync arrows, which read as "refresh" — an action, not an event — and named neither the event nor the system. Both marks now come from the conversation timeline (Calendar, Building2), which is where a user has already learned what these events look like. The trigger badge follows suit. 2. THE MEMBER PICKER MOVES INTO THE BREADCRUMB ROW. It answers the same question the crumbs do — WHOSE playbook is this — so on its own row underneath it read as a filter over the content rather than as part of the address, and cost a row of height on every load for a control most sessions never touch. Only the picker moves: the banner and the permission notice stay in AdministeredUserBar, because they are full-width messages and squeezing either into a title row would truncate it or shove the crumbs sideways as it appears. The bar now collapses to nothing when it holds neither. 3. A REF WITH NO INSTRUCTION HAD NO WAY TO GET ONE. Every ref authored before this feature is self-closing, so it parses to the inline chip — and the chip had nowhere to put an instruction. That is not a migration to wait out; it is every ref in every playbook today. The chip now carries a second half, `+ Instructions`, which converts it into the panel with the caret already in the instruction area. Offered ONLY inside a trigger: a ref in <always-loaded> is a document the agent reads, not an agent a trigger fires, so there is no trigger for it to have an instruction for. The shared FileLinkNode does not learn what a trigger is — it asks its host "does anyone want a segment on this chip", and the playbook is the only host that answers. The conversion carries `section`/`when` across (dropping them would turn "add an instruction" into "quietly widen this ref's scope") and has two shapes: a chip alone in its paragraph replaces the paragraph, leaving no empty one behind; a chip among prose leaves the prose as the block's own note. 4. THE DISPATCH EXPLAINER MOVES INTO A `?`. It was a line under the pill. But it is REFERENCE, not status — identical for every trigger of a kind, never changing — and a sentence that never changes on a row you read daily stops being read within a week while still costing a line of height on every block in the document. Behind a `?` it is one click away on the day you need it and invisible on the days you do not. The popover answers two things in the order they are needed: what wakes it (with the four event glyphs when the trigger is `any`, since "every event" is the one label that names no thing), and what happens then — whether an orchestrator reads this block and CHOOSES, or every agent in it simply runs. 4,103 mail tests green; tsc -b clean. Co-Authored-By: Claude Opus 5 <<email>>Sep 7, 2026, 11:00 PMwebhook-trigger-panel.test.tsxfeat: webhook trigger editor UX (design: playbook-webhooks-and-post-api phase 3) - WebhookTriggerPanel: mints the webhook row on first pick, shows a copyable POST URL + curl example, enable toggle, and rotate (pure View + tRPC container). - TriggerNode renders the panel for webhook-type blocks; TriggerSection hides conversation field filters for webhook (conversationless). - Restrict the webhook pill from the legacy [on:] PlaybookTriggerNode (XML path only); AgentsEditor already scopes its trigger pills. - RTL test for the panel view (URL shown, rotate + enable handlers fire). Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jun 25, 2026, 3:08 PM