chatfeat(boards): a board born with columns, and a schema you edit as a ticket
A new board arrived as a blank rectangle — the one thing it failed to teach was that it
was a kanban. It is now created on a starter axis (`Column 1/2/3`), placeholder vocabulary
that reads as "rename me" rather than Cedar deciding the board is a work ladder.
`emptyBoardSchema` stays what a board READS as, so a failed parse still invents nothing.
The 20rem schema dropdown becomes two controls, the same split as /tasks/kanban:
- Schema, a modal shaped like an open ticket — title/description beside a rail of typed
attributes, the geometry a person already knows from `CardDocumentView`. Identity
writes to `documents` columns (`boards.setInfo`); shape writes to the Y.Doc, so an
agent's edit merges rather than clobbers and shows live while the dialog is open.
- Display, the `view` half alone — which attribute draws the columns, which show on the
card face. Regrouping writes no card row, which is the whole reason it is a view.
Renaming a column renames the value on every card in it (`boards.renameColumnValue`);
renaming only the option drew a new empty column beside the old one, still full.
Boards and cards also render in /brain now — a board's content is not its prosemirror
fragment, so the prose branch drew an empty editor that wrote prose INTO the board. Both
own their full surface, and the back gutter steps aside: at wide widths that invisible
~460px strip lay over the first kanban column, eating clicks and blocking drags.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 7, 2026, 3:28 PMcomponentsfeat: the transcript names what it did, and the table it made is reachable
A chat that spent twenty calls building a LinkedIn tracker recorded almost
none of it: four different people got connection requests under four
identical "Executing linkedin-write" lines, the table itself never became a
context item, and re-entering the chat reopened the deal instead of the
table. Traced end to end; this is the set of causes.
The transcript:
- Tool labels resolve from the ARGUMENTS, not just the name. Every tool is an
action family now, so the name alone collapses a dozen verbs into one row.
Rows expand to who / where / whether a message went with it.
- A select column's declared option order IS its sort ladder; the tool
description now says so, and the transcript shows the ladder.
LinkedIn caps:
- A send's provider-id lookup gets its own budget (connect/dm/inmail_lookup),
mirroring the send it serves. Charged to profile_view, a day of browsing
refused invitations that had 80/week of connect headroom left.
- Connection requests go out with NO note unless the user asked for one —
enforced at execute time, not just described.
Getting back to what the agent made:
- The table tool files a context chip on create/import, as write-document has
always done; delete pulls it back off.
- A thread remembers what it was displaying (chat_threads.context), so
re-entry restores the table rather than re-deriving the deal.
- A fan-out opens a conversation-scoped table inside its deal's Files tab.
- 'table' is a valid write type on the document family — the tool was telling
the agent to pass a value its own schema rejected, which is what drove a
delete-and-rebuild that orphaned the open document.
The grid:
- Title, then description, then the commands — the order the questions arrive
in; a URL or address in a cell is a real link; a deleted table says so
instead of rendering on from the local Y.Doc cache.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 20, 2026, 11:43 PMgraphfix(brain): an agent's own namespace stops filling the workspace it isn't part of
`user|organisation/agent-…` folders are machine-managed — an agent's output/config/memory
head, and the routing surface's instruction docs. There is one per agent, so twenty rows
reading "Agent 4008f724 71dc…" sat above the first folder the user actually made.
One rule (`isBrainWorkspaceDoc`) now gates all three readers — the tree's store, the graph,
and both halves of the search — so a row one of them hides cannot walk back in through
another. The server still lists those folders: the agent workspace walks the same route.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 7, 2026, 3:28 PMhooksfeat(files): every file surface renders one row (design: unified-files-ui phases 3-6)
/brain, a conversation's Files tab and an agent's Output/Memory tabs were ten
row renderers that had each re-decided indentation, icon chrome, type size,
selected colour and separators. The same file looked like a different object
depending on where you found it. They now all render FileListRow.
Shape is Google Drive's, reworked from review:
- the column header IS the top row; no count strip above it
- the right edge is one fixed cell holding a ⋮ menu, so a row with four
affordances and a row with one still end at the same x
- no "Edited by" column — the actor rides inside Modified ("3 days ago by
Jesse Li", "by you" for the viewer). The two read as one fact, and splitting
them spent a column's width to say something the reader reassembles anyway
- a folder's count sits beside its title, "Archives (50)", not as a lone
number at the far right where it reads as a column only some rows have
- "New file"/"New folder" live in the header's + and in each folder's own ⋮,
never a strip above the list that pushes the files down the page
- a scope root is a ROW, not a section header. It is a folder — you open it,
drop into it, create inside it — so it gets the folder's row, just a
prominent one
Selection and keyboard, matching the thread list where it exists and diverging
only where the divergence is the point:
- hovering cross-fades the tile into a square checkbox in the same footprint;
pure CSS, because a tree re-rendering every row on mouse-move is unusable
- hover reaches the keyboard by CustomEvent into a ref, never state
- keys act on hover FIRST, then selection — the cursor is the more specific
statement of intent
- the letters are the task list's (w delete, x select, ⇧X range), but w
CONFIRMS here: mail can delete instantly because its undo stack reverses a
label change, and a folder takes its whole subtree with it
Two bugs the migration surfaced, both from Radix restoring focus to a menu
trigger a tick after close: a popover opened from a ⋮ item dismissed itself,
and Rename's autofocused input blurred and cancelled in the same frame.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 11:21 PMstorefix(documents): stop the save badge promising a retry that never comes
A refused save was emitted as the same `flush-error` as a dropped request, so
`SaveStatusBadge` rendered "Save failed — retrying" on one — while the provider
deliberately schedules nothing, because re-POSTing identical bytes would be
refused identically. The user waits for a retry that only their own next edit
can trigger.
`FlushError` now carries `refused`, the doc-save log carries it through, and the
badge splits the two: a transport failure keeps the amber "retrying", a refusal
gets a red "Not saved" with the server's reason on the tooltip.
Raised by greptile on #2690.
Co-Authored-By: Claude Opus 5 <<email>>
Claude-Session: https://claude.ai/code/session_01NVHnt2pEEjhVwSyhkcaxoCSep 6, 2026, 9:16 AMuploadfix(types): close 205 typecheck errors by repairing real client/server drift
apps/mail typechecked against a stale apps/server/dist d.ts (the server's
package.json points "types" at dist), so a chunk of the reported drift was
phantom. Rebuilt it and fixed the four script errors that blocked `tsc -b`
in server, which is what regenerates it.
The rest was genuine contract drift, fixed at the source rather than cast
away — routes that dropped fields their callers still read (agentExecutions,
getUpcomingCalendarEvents' conversationId, createCanvas' description),
schema splits the UI never followed (conversationUsers vs
conversationContacts), and enums that grew server-side but not client-side
(TASK_TYPES, ActionStatus, DateFilterOperator).
Also deletes code that was already dead: an unreferenced sort popover whose
store API is gone, a panel importing a deleted module, the system-skill
metadata UI whose mutation was removed deliberately, and a test asserting a
store method that no longer exists.
apps/server: 4 -> 0. apps/mail: 228 -> 23, all remaining errors being
dependency resolution (zod v3/v4, react-router dev/runtime skew, tiptap
v2 via novel) rather than code.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 2, 2026, 7:45 PMutilsMerge remote-tracking branch 'origin/staging' into refactor/agent-files-sections
# Conflicts:
# apps/mail/modules/agents/components/AgentConversationFilesFolder.tsx
# apps/mail/modules/conversations/components/files/ConversationFileTree.tsx
# apps/mail/modules/files/components/list/FileListColumns.tsx
# apps/mail/modules/files/components/list/index.ts
# apps/mail/modules/files/components/list/types.ts
# apps/mail/tests/modules/files/brain-explorer.test.tsx
# apps/mail/tests/modules/files/list/FileListRow.test.tsx
# apps/server/src/mastra/tools/debug/debugTool.ts
# apps/server/src/services/playbook/playbook-execution-triggers.ts
# apps/server/src/services/playbook/playbook-renderers.ts
# apps/server/src/services/playbook/resolve-playbook-context.ts
# apps/server/src/services/playbook/write-document-guarded.ts
# apps/server/src/trpc/routes/documents.tsSep 9, 2026, 9:26 AMfileViews.tsfix: six review findings — tenant leaks, wrong seat, lost writes
The LinkedIn counterpart cache was GLOBAL. Keyed on `provider_urn` alone, one row served every
customer who had met that member: the second org's refresh overwrote the first org's row and its
`person_id` (a pointer into that org's own crm_person), and either org's read could hand back the
other's cache. The key is now (organization_id, provider_urn), and the read and the upsert both
say so. DDL applied as a targeted, idempotent ALTER — the old single-column PK is dropped only
where it is still the one in place, and the org index goes with it, being redundant the moment the
key leads with the org.
The refresh no longer takes a seat from its caller. A profile view is billed to one seat's 50/day
and reaches LinkedIn as that seat's identity, and the client was naming the org's first connected
account — so in an org with several seats the panel spent the wrong rep's budget under the wrong
rep's name, with nothing checking the seat had any relationship to the chat. `linkedin_chats`
already records which seat holds a chat, so that is where the answer comes from.
A remembered `budget` skip now expires. The client suppresses its auto-refresh entirely while one
is reported, so an unexpiring reason was a permanently cold panel: a chat opened once while the
day's views were down to the send reserve never refreshed on its own again, however many days
later it was reopened. `last_skip_at` stamps the reason and the read honours it only inside the
window it was measured in — windows read off the governor's own caps, not a number repeated here.
A range or paste over two linked cells of ONE deal now writes them in order. `crm.updateConversation`
holds an optimistic lock on the conversation's `updated_at`; fired together, both calls read the
same timestamp, the first commits and the second matches no row and comes back CONFLICT — so half
the gesture reverted with "please retry" on an edit nobody was competing for. Same deal, one at a
time; different deals still in parallel.
"Shared with me" is gone. It listed every row in the org owned by another PERSON, which is not a
sharing relationship — there is none between users in this product; `share.ts` is public token
links. `user/` is the one genuinely private scope and `documents.get` refuses to open another
person's row at all, so the view could only ever show teammates' private files (10,037 rows live,
titles, paths and a 300-character content preview included) and then fail to open them. A menu row
that cannot be implemented honestly is worse than no menu row.
The Deleted view now lists exactly what Restore accepts. It listed every org-scoped tombstone,
which put 52 live rows (`#agendas/…`, `organisation/wiki/…`, `organisation/tables/…`) on screen
wearing a button that `callerOwnsDocument` always refuses. The set predicate lives beside that row
check so the two cannot drift apart. One deliberate gap: a `thread/` row with no owner is left out
rather than listed, because its ownership lives on the thread and cannot be asked in the query.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 10, 2026, 12:03 PM