__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