CanvasHeader.tsxfix(pipeline,inbox): filters that survive a refresh, and a lost deal that leaves the board
Three reported bugs, two of them the same mechanism seen from different ends.
A rejected viewConfig save was invisible. Every filter control fires
`void saveCanvasViewConfig(id)`, so a rejection had no observer at all: no toast,
no retry, and a local config the user believed was stored. `dirtyCanvasIds` kept
the edit on screen — but that Set was never persisted while `canvasesById` was,
so a reload came back with the change still in localStorage and nothing marking
it unsaved. The server's pre-save row then won rehydration, which is why this
read as "my pipeline filters don't survive a refresh" rather than as a write that
failed. The save now reports failure and resolves false instead of rejecting, so
all ~20 `void` call sites are covered without touching them; the dirty set
persists, so the guard already in upsertCanvas holds across the reload it was
being thrown away by; and CanvasHeader stops flashing "Saved" over a failure.
Saves are also chained per canvas. Working through a filter popover fired several
updateCanvas mutations at one row concurrently, so the last RESPONSE to land, not
the last edit made, decided what the server kept.
Closed Lost deals stayed in the pipeline because status is filtered SERVER-side:
a page's membership is fixed when it is fetched, and marking a deal Closed Lost
writes to the store, never to that page. The status/priority change now
invalidates listConversations — it was the only successful-update path that never
did — and the fetched rows are re-checked against the optimistic value, so the row
leaves in the same frame as the click and the refetch merely agrees. That check is
one shared predicate now; the kanban had its own copy, which is why two views of
one pipeline disagreed about what a stage filter admits.
Escape clears the inbox filters, in a second stage. Selection first, filters only
once nothing is selected — a single stage would throw a filter set away on every
ordinary deselect.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 4:06 PMCanvasRenderer.tsxfeat(inbox): unibox keyboard parity, undo-send that puts the draft back, sharper chat context
Unibox now navigates like the email list — j/k, Enter, Escape all walk whichever
list is on screen via a new `activeListSource` on the thread slice, and Escape
clears the full selection (focused row, bulk checkboxes, ambient conversation)
instead of only the keyboard cursor.
Undo send stops round-tripping through localStorage and a URL rewrite: the send
records where the draft came from and undo reopens that surface, restoring the
un-quoted body onto the draft row (recreating it when the send removed it).
Chat context: an auto-committed primary conversation is provisional while the
chat is empty and drops when you move on, the context cards follow the deal
you're actually on, threads with no conversation get a link prompt, and the
conversation surface shows a keyboard legend rather than restating cards already
on screen.
Also: agenda Trash removes the row outright, Next steps renders as one card with
its booked calls beneath, and channel chats focus the composer on open.Jul 30, 2026, 11:54 PMCardListCanvasView.tsxfix(calendar,ui): a Zoom connect dialog, and four things the review found
The Zoom work lands its last piece: picking Zoom on an unconnected account used
to open a bare browser window with no explanation and no way back. ZoomConnectDialog
owns everything around the consent screen — why Cedar is asking, the wait, and the
four different reasons a popup ends without a connection — and resolves back to the
composer so one click on "Change to Zoom" still ends with Zoom on the event. The
OAuth callback URL now falls back to the server's own origin, so an unset env var
is no longer the difference between a working integration and a message telling a
user to edit a file they have never heard of.
Agent avatars move to components/icons/agent-avatar.tsx as one component, and the
conversation/agent reading column narrows from 100ch to 80ch across all four places
that hold the measure.
Review fixes:
- Breadcrumb decided "you are here" positionally, so it rendered the last crumb as
an inert span and silently DISCARDED any href it was handed. A task's bar is
`Tasks / <lane>` above the task's own title — the h1 below is the page, and the
lane is somewhere to go — so its link had become dead markup. The current crumb is
now the last one with nowhere to go, which every other call site already is.
- modifyCalendarEventTool passed a start time but no duration to Zoom, which
silently books 60 minutes: an AI-updated 25-minute event held an hour on the
host's own account.
- schema.ts declared two_factor_required without .notNull(), drifting from the
NOT NULL the migration actually applied.
- HomeAgentsWidget cast a tRPC result that was already typed — the redundant-cast
shape that took down the home rail once before.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 11:54 PMConversationCanvasView.tsxfix: lastContacted popover + filters working with individual canvasesFeb 27, 2026, 3:38 AMConversationCard.tsxfeat(tasks): one plain-text task line everywhere, no cards, no agenda groups
A task's description is now two parts — `<headline> — <detail>` — and every
surface paints the same split: headline bold, deal as an inline badge, detail
muted. `parseTaskLine` derives it from the string, so descriptions that already
carried a dash render in the new shape with no migration.
The agenda's conversation card is gone (rows are plain text; the deal is a badge
in the sentence, deferring to an `@[id]` chip the row already carries), and so
are its task-group sections: grouping and ordering belong to the daily-agenda
agent alone. Legacy `taskGroupSection` wrappers are unwrapped on read — deep
copied by hand, since `Y.XmlElement.clone()` drops non-string attrs and would
have unticked every completed row.
The agents write only the description now: no `reason`, no `flags`, neither of
which rendered anywhere. The digest email reads the detail half instead.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 26, 2026, 8:31 PMKanbanConversationView.tsxfeat: v0 kanban viewMar 29, 2026, 4:19 PMReportCanvasView.tsxfix(meetings): a Circleback recording that plays, on open, with a link that goes to Circleback
Three things stood between a Circleback meeting and its recording.
The recording could not be fetched at all. credentials-encryption derives its
AES key from ORG_CREDENTIALS_ENCRYPTION_KEY || BETTER_AUTH_SECRET, so which
secret a credential was written under is a property of the environment at WRITE
time. Every Circleback MCP token set was written under BETTER_AUTH_SECRET; once
the org key was set, every decrypt died with "Cipher job failed", the call sites
swallowed it into "no credentials", and the player said "No playable recording
for this meeting" with the real cause visible nowhere. Decryption now tries each
configured secret; encryption still writes with the first.
The meeting dialog waited for a press before fetching. That poster exists so a
coaching document with ten moments does not fire ten mints on open against a
Fathom budget of 30/min — an argument that does not apply to one recording the
reader opened to watch. `autoLoad` skips the press without autoplaying.
"Open in Circleback" went to storage.googleapis.com. What ingest stored is a
signed GCS object whose signature dies 24h later; unsigned it answers 403
forever. The page is built from the meeting id instead, and no surface will link
to a raw media file any more.
Webhook-era rows (numeric external_id, all before 2026-07-29) still cannot mint
or link — neither ReadMeetings nor the page route takes a numeric id.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 5, 2026, 12:21 PMStatisticsCanvasView.tsxfeat(forms): the settings row layout, and a select that admits it opens
Every field is one row now — icon and name left, the sentence under it,
the control right — because a form with eight fields laid out
label-over-control gives the eye nothing to run down. Fields read the
layout from context (`FieldRows`), so a form declares it once instead of
per field.
The select rendered no chevron at all, and no call site had noticed: it
was a bordered box with a word in it. It now owns its chevron, comes in
two sizes instead of the four that had accumulated across 62 call sites,
and has a `ghost` variant for the ones that sit inside a sentence.
Two bugs found by reading computed styles rather than screenshots:
FormControl is a Slot that hardcoded `bg-popover`, so it was overriding
the background of every control in every react-hook-form in the app; and
the menu's `shadow-[...]` compiled to three fully transparent layers.
Geometry is measured off Linear's settings surface; the colours are
Cedar's own tokens, since our light theme is warm and theirs is grey.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>
Claude-Session: https://claude.ai/code/session_01DFkAfWRuLsdTkpxrFhTKYWAug 30, 2026, 11:37 PMTableCanvasView.tsxfeat: new leftSidebar homeviewApr 6, 2026, 11:06 PM