FileLinkChip.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 PMFileLinkMenu.tsxfeat: typed cells, live ref chips, and tables as first-class files (design: table-documents phases 6, 7)
Phase 6 — a cell chip and a prose chip are literally the same component. ConversationNode
and EventNode were split the way FileLinkChip already was (Content + the NodeViewProps
wrapper that mounts it), so behaviour stays identical by construction: same data hooks,
same click handler, same setSelectedArtifact path. The only PM-specific parts were
NodeViewWrapper and reading node.attrs, which is why the extraction is small.
Editing mounts a one-line TipTap instance for the focused cell ONLY, configured with the
EXISTING suggestion extensions — so typing `@` in a table cell runs the same extension,
the same search, and inserts the same node as typing `@` in a prose document, rather than
a parallel implementation that drifts. Exactly one editor is alive at a time, so the
descriptor-tree costs that rule out a ProseMirror grid never apply here.
Phase 7 — "New table" in the file-creation menu, a table graph-node kind, "N rows × M
columns" from the metadata the server hook already writes, the grid in the artifact panel
(discriminated inside FileArtifact on the FETCHED documentType, since the outer switch is
on ContextKind where a table is just a `file`), a read-only grid on the public share page,
resolveOpenDoc, and Download as Excel/CSV wired to documents.exportTable.
Also completes Phase 10's agent-facing half: the `table` tool's `create` now takes a
`from` clause, so an agent materializes rows from a real query instead of transcribing
them. The query runs BEFORE the document is provisioned, so an over-cap or malformed
filter leaves no half-built table behind, and truncation plus any binding error is
surfaced as a tool warning rather than silently yielding a short or empty column.
Three fixes found by running the suites rather than by reading:
- The tool description had grown to 2134 chars, past the 2048 limit Claude Code truncates
at — text past the cutoff is silently dropped for every caller. Trimmed to 2030 while
ADDING the `from` guidance, which is the part most worth having early.
- tableTool.test.ts enumerated its mock of the documents barrel, so every new import in
the tool broke the file with a mock error rather than a real failure. It now spreads the
real module.
- A cell-ref offset expectation was hand-counted one short (`[[conversation: 9f2e]]` is 22
chars, so end is 27). The implementation was right; the test now asserts by slicing the
value, since these offsets exist precisely so the grid can interleave text and chips.
Tests: 1787 server, 55 mail across the table/home/files suites. Both apps typecheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 10, 2026, 5:04 PMFileLinkNode.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 PMFileLinkSuggestion.tsfeat(playbook): the agent panel — a ref stops being a chip (design: per-trigger-instructions phase 6)
A ref inside a trigger was a 12px FileText glyph and a document title at py-0.5 —
the smallest thing in the block, indistinguishable from a link to a resource doc,
and the most important thing in the block. It becomes a block-level panel: the
real AgentAvatar and the agent's name, then its instruction underneath.
The instruction area is ALWAYS rendered. No toggle, no disclosure. An instruction
you have to click to see is one nobody audits, and an empty slot that shows itself
is what teaches the feature exists.
The empty state is a CSS ::before, never seeded text. Seeded text would round-trip
into the XML and every ref would ship with "Instructions for this trigger…" as its
real instruction, delivered to the agent. The test mutation-checks this: seeding
the placeholder as content makes both assertions fail.
Two keyboard rules the panel needs and a plain block node does not:
- Enter inserts a hardBreak (which the serializer writes as \n) rather than
splitting the panel into two refs pointing at the same agent.
- isolating, plus Backspace-deletes-empty-panel: without isolating, an
instructed panel merges its text upward and takes its documentId with it.
`triggerRef` is registered in ID_NODE_TYPES. Omitting it makes the schema strip the
node's nodeId on every save and re-seed the whole Y.Doc — the bug documented in
that file.
Also fixes a PRE-EXISTING round-trip bug found on the way: compile-playbook reads
`section`/`when` off a ref, but the editor emitted and parsed only `id`, so any
playbook edited in the UI silently lost them. Both ref shapes now carry them, in
both directions — fixing only one shape would have relocated the loss rather than
removed it.
@-insertion could not produce a panel as the design assumed: the @ menu is
PATH-addressed and a <ref> needs an id. ReferenceOption gained an optional
documentId, so an option that knows its document produces a panel and the system
tokens (@crm-updater, @next-steps), which have no document behind them, correctly
stay chips.
8 new panel tests + 5 attribute round-trip cases; 55 playbook and 190
document-saving tests green.
Co-Authored-By: Claude Opus 5 <<email>>Sep 7, 2026, 10:25 PMindex.tschore: snapshot in-progress working tree before layout-slice refactor
Pre-existing WIP across chat input, file-link mentions, nav buttons, agenda,
and server file-system, committed to clear the runway for the unified layout
slice implementation (design: layout-slice).
Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jul 11, 2026, 8:20 PMmarkdown-bridge.tsrefactor: consolidate document code into documents/ + add {{ event mention
Frontend: move the editor primitive, file-link/, mention/, yjs/, and history/
under apps/mail/modules/documents/ so every doc-editor concern lives in one
place. Rename AgendaConversationMention -> ConversationMention and generalize
it to take an onSelect callback so the agenda keeps its task-promotion
dispatch while regular docs insert a plain conversationNode chip.
Backend: rename services/document-store/ -> services/documents/ and absorb
services/sse/doc-event-bus.ts + services/sse/doc-events-delta.ts alongside
their tests.
New: {{ event mention in documents. Custom Tiptap suggestion matcher requires
the literal {{ prefix; picker shows a type-icon, title, conversation context,
and relative time; selecting inserts an eventNode chip. New tRPC route
crm.searchEventsForMention runs a global search across the user's crm_events
(emails, slack, meetings, calls, notes, custom) joined to company info.
crm_conversation_updates lives in its own table and stays excluded by design.
Co-Authored-By: Claude Opus 4.7 (1M context) <<email>>May 26, 2026, 9:30 AMremarkFileLinksPlugin.tsrefactor: consolidate document code into documents/ + add {{ event mention
Frontend: move the editor primitive, file-link/, mention/, yjs/, and history/
under apps/mail/modules/documents/ so every doc-editor concern lives in one
place. Rename AgendaConversationMention -> ConversationMention and generalize
it to take an onSelect callback so the agenda keeps its task-promotion
dispatch while regular docs insert a plain conversationNode chip.
Backend: rename services/document-store/ -> services/documents/ and absorb
services/sse/doc-event-bus.ts + services/sse/doc-events-delta.ts alongside
their tests.
New: {{ event mention in documents. Custom Tiptap suggestion matcher requires
the literal {{ prefix; picker shows a type-icon, title, conversation context,
and relative time; selecting inserts an eventNode chip. New tRPC route
crm.searchEventsForMention runs a global search across the user's crm_events
(emails, slack, meetings, calls, notes, custom) joined to company info.
crm_conversation_updates lives in its own table and stays excluded by design.
Co-Authored-By: Claude Opus 4.7 (1M context) <<email>>May 26, 2026, 9:30 AMuseCachedFileLinkSearch.tschore: snapshot in-progress working tree before layout-slice refactor
Pre-existing WIP across chat input, file-link mentions, nav buttons, agenda,
and server file-system, committed to clear the runway for the unified layout
slice implementation (design: layout-slice).
Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jul 11, 2026, 8:20 PM