Kanban card detail and board dialogs
Surfaces
CardDetailRail({ snapshot, cardId, onClose }) draws one card in the shared
WorkspaceDetailRail: the right-hand drawer on a desktop, full screen on a phone through
the shared overlay rule. It reads the card through useCard and keeps the board snapshot
for board-wide facts: column order, the cards a move lands behind and the viewer's rights.
A refused read shows its own state inside the rail; not_found reads as a missing card even when a
successful earlier read remains in the host cache. A transient refetch failure keeps the cached editor;
only the terminal not_found response replaces it with the missing-card state.
The body is Settings content in the drawer presentation: title, properties (column,
assignee including Chetty, due day, priority, labels), the RichTextEditor description,
checklist, attachments, comments and a folded history. The header carries the autosave
state, the handoff control and the card menu (archive or restore, and permanent deletion
when board.permissions.deleteCards). A confirmed deletion disables the card's useCard
read until it settles: the mutation's settling invalidation runs before the drawer closes, and a
still-enabled read would ask for the deleted card and log a 404. A refused deletion enables the
read again, which refetches the card once.
A card without agentSessionId offers "Hand to Chetty", which posts the handoff route and
predicts nothing. A handoff refused before its conversation was prepared leaves no session, so
the control stays available. Once a conversation exists, including after a refusal that came
after it was prepared, the control opens it through utils.openBrainSessionWindow; there is no
second handoff. agentStatus is shown as stored; a non-null agentStatusReason is a stored
message ID, shown through kanbanMessage in the reader's language.
BoardDialogs({ dialog, snapshot, onClose }) renders the BoardDialog union of
boardModel.ts: create, rename, members, Project link, labels, archived cards, delete, and
the card menu's due day. DueDateDialog and LabelPickerDialog are presentational and
reused by the detail. Column rename, role and deletion stay inline on the board.
Writes and autosave
Every write goes through useKanbanMutation from hooks.ts, with optimistic patches of the
board snapshot and the card detail. A refusal rolls both back and is toasted once by that
hook; a dialog therefore only closes on success and draws no second error line.
Title and description share one useAutoSaveStatus controller. Only fields that differ from
the stored baseline are sent, and only sent fields advance it. The editor's first normalized
report of its seed becomes the baseline, not an edit. A server change is adopted only while
nothing is unsaved. Closing the rail flushes first and stays open on a halted save.
Checklist reorder uses the shared SortableList and useSortableItem (UI API 74). The new
position comes from positionInOrder in boardModel.ts, with the server's collision rule.
Attachments
Uploads are JSON contentBase64. Files over 5 MiB are refused before reading;
ATTACHMENT_LIMIT_BYTES restates MAX_ATTACHMENT_BYTES so the bundle does not carry the
server validators, and a test holds the two equal. At that decoded cap, base64 content
and bounded metadata fit within the host's unchanged 8 MiB JSON request limit. The
localized attachmentTooLarge message takes its {limit} from formatBytes rather
than embedding a file-size literal. Downloads are links to the download
route composed by routeUrl. Only PNG, JPEG, GIF, WebP and PDF preview in place: images
are fetched and wrapped in a blob of the allow-listed declared type for ImageLightbox, PDFs
open in PdfPreview. Object URLs are revoked when the preview closes.
People and mentions
Assignees and mentions come from the board's members. The mention picker writes
@username into the comment and a TokenList chip; the chips that name a member are sent as
mentions, and a chip naming anyone else blocks sending.
Members add people from the board's own listMemberCandidates read
(boards/:boardId/member-candidates): the human accounts that are not members yet, for every
admitted member, not the host's administrator directory. A whole-team board shows no picker.
Any member edits the label catalog. The archived-cards dialog asks listCards for
archived: 'only', so the server pages and counts archived cards alone. A new board sends
its four starting column names in the reader's language as initialColumnNames.