__tests__feat(agenda): the instructions button goes to the agent, not a copy of its prose
Clicking "Agenda Agent Instructions" opened a dialog that rendered the prompt
read-only. But the instructions ARE the agent's config — every question the prose
raises (what cadence, which AOP, what it is allowed to touch, when it runs) is
answered by controls the dialog did not have, so it left the reader one click
short of all of them. It now navigates to `/agents/{agentId}?tab=config`, landing
on the config tab rather than the workspace's default.
`getAgendaInstructions` returns the agent id to make that possible. It is
resolved with `resolveSubagentAgentId` over the row's metadata AND frontmatter —
the one place that knows which of the two wins — so the link cannot point
somewhere other than what the cron actually dispatches. The button hides itself
when there is no agent rather than linking nowhere.
`AgendaInstructionsDialog` is deleted; nothing else referenced it.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 10:41 PMcomponentsfeat(mail): the graph canvas (design: graph-documents phase 5)
A graph document renders as an editable node/edge canvas whose nodes open as
the documents they point at.
ONE tRPC read returns the schema, the nodes, the edges and the layout, because
all four live in one Y.Doc and come out of one decode. That is the payoff of
keeping nodes inside the document: a board reads its schema from the Y.Doc and
its cards from SQL and the two can disagree; a graph cannot.
ONE node component, for every graph there will ever be. It renders from the
SCHEMA — one row per `displayOnCard` field, typed by the same `TableColumnType`
the board and table use, drawn by the same `CardFieldDisplay`. There is no node
component per domain, and the day there is, "an org chart is configuration"
stops being true.
Three things the tests pin that are easy to undo:
- The layout runs over ONLY the edges matching `view.layoutEdgeKind`, so which
relation is the skeleton is a VIEW property: point it at `influences` and the
same people re-rank with nothing written. Adding a non-layout edge moves
nobody.
- `hydrated ?? derived ?? fields`, in that order. An authored value shows as
authored, a bound value shows live once hydration lands, and the cache only
fills the gap between open and arrival — so the three can disagree in exactly
one direction, toward the document.
- Only TWO gestures write: a drag is one `graphLayout` key, a drawn connection
is one `connect`. There is deliberately no whole-canvas save, because that is
how a derived annotation becomes a persisted field and how one open tab
overwrites another's concurrent edit.
A computed layout is persisted ONCE, so the next open is stable and any later
re-layout is a deliberate act rather than something that happens because someone
opened the file.
`graph` is registered in all five places a document type has to be — the
full-bleed set, FileBrowser, FileArtifactPanel, FilesTab and CompanyExplorer —
plus its icon and label.
36 graph tests; 4,588 passing across apps/mail, no regressions.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 14, 2026, 10:04 PMhooksfeat(home): the meetings widget owns one day, and the pipeline widget knows which one it counts
Two widgets in the home rail were quietly lying, in different ways.
The pipeline tile counted every crm_conversation the user owns. computePipelineStats
only filters by AOP when an aopId is passed, and nothing passed one — so on a real
account it read 788 "deals" against an actual pipeline of 124, with cold inbound,
vendors, recruiting and spam as stages. Worse, those carry no deal_value, so each
added 1 to the count and 0 to the ACV. It now defaults to the fuzzy-matched deals AOP
— the same match /statistics already used, extracted to modules/aop/utils/deals-aop
so the two surfaces cannot drift onto different pipelines — and a modal behind the
header's pencil carries the rest of the filters (which pipeline, whose, which stages).
The scope is resolved at the rail so it still feeds ONE getOverview call. An unscoped
count stays available, but only under a label that says what it is.
The meetings tile was a maze of its own making: when today ran out it drew TOMORROW
while the arrows still pointed at today, so Next went to tomorrow again (identical,
minus the now-line) and Back went to yesterday. The list now only ever renders the day
it is handed; it reports that today is spent and the widget moves the day, once,
ref-guarded so Back onto a spent today does not bounce straight forward again. It also
waits for the last meeting to END rather than to start, so the day cannot move out
from under the meeting you are in.
The rest is the shape those two changes exposed: the four stat tiles collapse into one
Statistics table (four bordered cards to say four numbers), meeting rows put the time
in a left-aligned gutter outside the colour rule so the times, the header icon and the
now-line dot all land on one edge, and per-tile remove gives way to one Edit widgets
button — adding and removing are the same act.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 3:45 PMwidgetsfeat(agents): a face that blinks, glances, and gets to work
`AgentAvatar` gains two moods. `idle` blinks and glances; `thinking` bobs, and blinks on
a quicker cadence — a busy face is a more awake one. The timings are measured frame by
frame off a reference clip rather than eyeballed, because what makes that motion read as
alive is the intervals, not the poses: the body holds perfectly still, the eye pair is
rigid and orbits about the point between the eyes, a blink is a ~70ms vertical collapse
that reopens ~18% too tall before settling, and every move is followed by real stillness.
Amplitudes are sized for the smallest place they run — the 16px rail icon — where a
tasteful 2° sway is a third of a pixel and therefore not an animation at all.
The sidebar's chat rows drop `TabBotIcon` for the agent's own face, so a row wears the
thing it is about; the finished-dot overlays whichever icon the row is wearing.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 7, 2026, 3:28 PM