AnalysisReviewMode.tsxfeat: refactorApr 23, 2026, 5:49 PMCanvasMetricsBar.tsxfix: fall back to REST when the TCP pool is capped, and scope aggregates to pinned ids
Three fixes from the review of #2496.
The listener path decided on `streamTransportEnabled()` alone, but a connection is
only available while the process is under MAX_BLOCKING_TCP_CLIENTS. Past the cap
`createBlockingTcpClient` returns null and `listenStreamFrames` ends having yielded
nothing — a clean SSE close, which the browser reads as a finished, empty reply. The
caller now claims the connection itself and falls back to the polling tail when it
can't, which is what the generator's own contract always assumed. `tcp_capped` on the
transport log separates "TCP is off" from "TCP is on but saturated".
A canvas pinned to an explicit id set passes those ids to the row query but not to the
aggregate, so the metrics bar described the whole unpinned filter set while the table
below it showed the pinned rows — the exact drift the hook exists to prevent.
`useCanvasAggregates` now takes the same pinned list.
Also drops the `_`-prefixed throwaway bindings used to strip the paging keys; the repo
rule is to remove unused bindings, not rename them.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 15, 2026, 2:11 PMCanvasReviewMode.tsxfeat: UI reorganization, score centering, and lint fixes
Co-Authored-By: Claude Sonnet 4.6 (1M context) <<email>>Apr 24, 2026, 11:22 AMColumnConfigurationItem.tsxfix(mail): stop a list load selecting a deal, and rename the Status label to Stage
A fresh /agent chat came up bound to whatever deal sorted first. setCurrentConversationList
auto-selected conversations[0] into activeConversationId, which on the chat surfaces is not a
row highlight but the CHAT'S CONTEXT — useRouteChatThread writes it onto the thread's
selectedArtifact and the attach effect commits it as primaryConversation. So the Top Deals tile
finishing its load put a deal nobody opened into a brand-new chat's chip row and into what the
agent was told. Loading a list now selects nothing; j/k is unaffected (focusedIndex starts null,
so the first press still resolves to row 0 — and no longer skips it).
CardListCanvasView also claimed the global currentConversationList and read it back to render.
useCanvasConversations gains syncCurrentList and returns the ordered list, so a tile that merely
displays deals no longer repoints the app's shared selection state. That leaves nothing writing
that list on /agent.
Rename the deal status field's LABEL to "Stage" everywhere it shows (the filter builder, table
header, group-by, kanban, profile, onboarding preview). Reported on Vooma's onboarding: "these
are stages, not statuses — status is like open and closed." Column id stays `status`;
statusOverview keeps its name, being a prose summary rather than the stage enum.
Add mod+N for a new chat, sharing one implementation with the chat header's button so the two
cannot drift, plus a row in the hotkey bar. The desktop File menu had CmdOrCtrl+N on Compose,
which the main process consumes before the page sees it — moved to CmdOrCtrl+Shift+N. A plain
browser tab reserves the key and will never dispatch it.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 17, 2026, 11:06 PMConversationCanvas.tsxrefactor(crm): use absolute imports for the new canvas modules
CLAUDE.md requires absolute imports; the canvas-metrics work introduced six
relative ones, which is what Greptile flagged. Only the specifiers this PR
added are rewritten — pre-existing relative imports in these files are left
alone so the diff stays about the finding.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 15, 2026, 5:32 PMConversationItem.tsxperf(crm): stop shipping list rows what they cannot render
Follows the event projection with the other two thirds of the payload. On the
same 50-row page: 2,809 KB -> 1,173 KB.
userTasks was 58% of it. The list had no status filter at all, so every task
ever attached to a conversation went over the wire — 650 deleted, 329
agent_deleted and 268 done against 59 todo on one page; 341 renderable out of
6,935 org-wide. Every list surface filters to todo before drawing (the tasks
cell, the current/future action columns, the kanban card), so the rest was
never rendered. Filtering to todo + recommended takes it 1,621 KB -> 175 KB.
The blobs on a task are small (~350 bytes of task_output on a todo task), so
this is deliberately a row filter and not a narrower field projection: the
execution ids and chat_thread_id / task_group_id drive execution mode when a
task is opened from a card, and trimming them would risk that for a few KB.
customFields was 522 KB. Two things, neither of them the values themselves:
54% of entries carry no value at all (4,028 of 7,506), and an absent entry
renders exactly what an empty one does — every reader is `field?.value || null`
— so they are skipped unless they still have a signal dot to draw. And 31% of
the bytes were envelope (id, conversationId, agentExecutionId, editedBy,
lastEdited, createdAt) that no list surface reads; every lookup goes through the
field name. updatedAt stays, because getCustomFieldsRevision keys the store's
change detection on it. 522 KB -> 332 KB, now 84% real user content.
Also deletes `siblingTasks`, which existed to carry the full task list to
TimelineTaskItem — where it was declared as a prop and never destructured. Five
call sites were threading a conversation's entire task history into a component
that ignored it.
Left alone deliberately: primaryCompany (75 KB across ~50 enrichment columns —
trimming it needs a store guard so a list refetch cannot blank the panel's
company card, which is a poor trade for 6%), and the event cap, which is a
product dial rather than waste.
Co-Authored-By: Claude Opus 5 <<email>>Sep 2, 2026, 11:42 PMCreateConversationDialog.tsxfix(mail): stop a list load selecting a deal, and rename the Status label to Stage
A fresh /agent chat came up bound to whatever deal sorted first. setCurrentConversationList
auto-selected conversations[0] into activeConversationId, which on the chat surfaces is not a
row highlight but the CHAT'S CONTEXT — useRouteChatThread writes it onto the thread's
selectedArtifact and the attach effect commits it as primaryConversation. So the Top Deals tile
finishing its load put a deal nobody opened into a brand-new chat's chip row and into what the
agent was told. Loading a list now selects nothing; j/k is unaffected (focusedIndex starts null,
so the first press still resolves to row 0 — and no longer skips it).
CardListCanvasView also claimed the global currentConversationList and read it back to render.
useCanvasConversations gains syncCurrentList and returns the ordered list, so a tile that merely
displays deals no longer repoints the app's shared selection state. That leaves nothing writing
that list on /agent.
Rename the deal status field's LABEL to "Stage" everywhere it shows (the filter builder, table
header, group-by, kanban, profile, onboarding preview). Reported on Vooma's onboarding: "these
are stages, not statuses — status is like open and closed." Column id stays `status`;
statusOverview keeps its name, being a prose summary rather than the stage enum.
Add mod+N for a new chat, sharing one implementation with the chat header's button so the two
cannot drift, plus a row in the hotkey bar. The desktop File menu had CmdOrCtrl+N on Compose,
which the main process consumes before the page sees it — moved to CmdOrCtrl+Shift+N. A plain
browser tab reserves the key and will never dispatch it.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 17, 2026, 11:06 PMCumulativeFieldsSection.tsxfix: collapse the Cumulative tile picker behind a toggle
Seven always-visible checkboxes pushed the presets — the part of the Fields popover people
actually reach for — below the fold, to pay for a decision that gets made about once per
canvas. It is now a collapsed row that expands on click, with a count badge so the current
state is still readable without opening it.
Co-Authored-By: Claude Opus 5 <<email>>Aug 14, 2026, 8:23 PMDraftReviewPill.tsxcapitalize review drafts for draft review pillFeb 19, 2026, 3:29 PMfilter-sort-adapters.tsfeat(mail): build an inbox's CRM rule with the real conversation filter UI (design: pipeline-inbox-crm-stage-filter phase 5)
Adds a "Linked deals" section to the inbox editor: pick AOP(s), then filter on
that AOP's own stages and custom fields. The column set is derived from the
selection, so Customer Success offers its stages and Deals offers discovery →
closed won, with no hard-coded enum anywhere.
This reuses FilterSortPopoverContent rather than growing a second filter UI. The
new `{ kind: 'inbox' }` scope is draft-local — the inbox does not exist yet while
the form is open — so it carries its own config + setter instead of a store slice.
Sharp edge found while wiring it: FilterSortPopoverContent only uses the adapter
for add/remove/clear. The six leaf column popovers resolve their write target from
a `canvasId` prop, falling back to the GLOBAL CRM slice when absent — so every
click in the inbox builder would have silently re-filtered the user's /crm page
and never touched the draft. They now consult a ColumnFilterOverride context
first; a test asserts the value lands on the draft AND that the global slice stays
untouched. The pre-existing 'sidebar' scope has the same latent bug (noted, not
changed here).
Also seeds docs/wiki entries for the mail read path and this subsystem, and
comments the deleted_at sharp edge at all three SQL builders.
Co-Authored-By: Claude Opus 5 <<email>>Aug 4, 2026, 7:28 PMfilter-sort-scope.tsfeat(mail): build an inbox's CRM rule with the real conversation filter UI (design: pipeline-inbox-crm-stage-filter phase 5)
Adds a "Linked deals" section to the inbox editor: pick AOP(s), then filter on
that AOP's own stages and custom fields. The column set is derived from the
selection, so Customer Success offers its stages and Deals offers discovery →
closed won, with no hard-coded enum anywhere.
This reuses FilterSortPopoverContent rather than growing a second filter UI. The
new `{ kind: 'inbox' }` scope is draft-local — the inbox does not exist yet while
the form is open — so it carries its own config + setter instead of a store slice.
Sharp edge found while wiring it: FilterSortPopoverContent only uses the adapter
for add/remove/clear. The six leaf column popovers resolve their write target from
a `canvasId` prop, falling back to the GLOBAL CRM slice when absent — so every
click in the inbox builder would have silently re-filtered the user's /crm page
and never touched the draft. They now consult a ColumnFilterOverride context
first; a test asserts the value lands on the draft AND that the global slice stays
untouched. The pre-existing 'sidebar' scope has the same latent bug (noted, not
changed here).
Also seeds docs/wiki entries for the mail read path and this subsystem, and
comments the deleted_at sharp edge at all three SQL builders.
Co-Authored-By: Claude Opus 5 <<email>>Aug 4, 2026, 7:28 PMFilterSortConfigurationRow.tsxfix(search): "All Org" was quietly searching only your own deals
The Cmd+K default moved from "me" to org-wide in the same change that stopped
hiding non-shared AOPs. That exposed a gap in the org-wide expansion: it widens
an AOP filter by its org-level parent, and an AOP whose `org_aop_id` was never
set expands to nothing and falls back to the requester's own id. The pill said
"All Org"; the query said "just mine". Four of the six Deals AOPs on our own org
are unlinked, and 48 of 236 across prod. Match those by name within the org
instead, and union the requested ids back in so an expansion can never come back
narrower than what was asked for.
The remembered deal type had the same shape of problem from the other side:
`selectedAopId` is persisted and goes straight into `aopIds`, but the pill reads
its label off the AOP list. Once that AOP is deleted the label falls back to
"All types" while the search keeps filtering on a dead id — nothing on screen,
no results, forever. Reconcile it against the list once the list has answered.
And picking the default back is not a choice worth storing: it is what "Clear
filters" means, so it returns the facet to unset rather than pinning it.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 11:13 PMGroupedConversationCanvas.tsxfix(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 PMindex.tsfeat(crm): split filters and sorting into separate popovers, sort from a header wins
Filtering and sorting were one popover with one horizontal row of badges, where
badge order silently encoded sort priority. Order now only means something in the
sort list, which is the only list you can drag.
Sorting a column from its header takes priority 0 and pushes the rest down, rather
than landing last where it has no visible effect. Shared by all seven column
popovers plus toggleColumnSort, so every entry point behaves the same.
Column popovers opened from a list are scoped to that list's concern and anchor
beside it like a nested menu; a "Set filter" / "Set sort" button hands the column
to the other list. Enum fields keep one list for both — drag is the sort order,
click is the filter — so their affordances split by mode instead.
Drops ~750 lines of dead code (FilterSortBadgeRow, the deprecated inline row,
OwnerUserFilter). The drag-reorder test now runs against the real function instead
of a hand-copied duplicate.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 2, 2026, 4:29 PMNewConversationCanvas.tsxfix(tests): unbreak 153 server suites and repair the CRM mock's positional inserts
`email-processor.ts` moved to a default import of `@barkleapp/css-sanitizer` to fix
tsx scripts, but Vite leaves `default` undefined for a CJS module with no
`__esModule` marker — so every suite whose import graph reaches google.ts died at
collection. A namespace import with a `.default` fallback is the one shape that
works under tsx and vitest both.
`pnpm types:test` — a task this branch adds — was red on its own new test file and
on `result.company` assertions the nullable-company widening invalidated.
Also repairs failures this branch inherited rather than caused:
- The CRM mock DB pinned an exact POSITIONAL insert sequence, which the
crm_company / crm_account_domains / crm_person dual-write shifted by three.
Replaced with a shared `mockInsertsByTable()` so insert-order changes can no
longer mis-wire it silently.
- `sanitizeContext` deliberately keeps `message`/`data` for failed-send
debugging, and `get()` deliberately emits label IDs without a per-thread
labels.list call. Both tests still asserted the pre-change contract.
- email-composer-send timed out on all 42 tests: `flushPromises` scheduled on a
FAKED setTimeout, react-query mocks returning fresh identities each render,
four missing store selectors, and a `useForm` override leaking `to: []` into
every test after it.
Account deletion: order the blocking-FK set by dependency like the unreferenced
set already was — inside a transaction a violation aborts everything, so the
retry loop cannot rescue it — and classify blocking columns instead of assuming
'delete'. NO ACTION is Postgres's default, so the next attribution column added
with a plain `references(user.id)` would have deleted rows belonging to whoever
the record is actually about.
pnpm types, types:test, deps:check, 166/166 mail suites and 461 server suites
(5124 tests) all green.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 2, 2026, 11:29 PMPresetNavigationBar.tsxrefactor: field signal replaces strength/completeness (design: strategic-overview phase 1)
Add CustomFieldSignalConfig + linkedDocPath to CustomFieldDefinition, a
strategicOverview DisplayConfig layout, and a shared SignalDot. Field values
now carry a red/yellow/green signal + signalReasoning in place of the two
numeric scores, across server (schema, field-values service, update tool,
crm routes, context formatting) and the mail CRM canvas.
Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jul 12, 2026, 3:05 PMReviewModeBottomBar.tsxfeat: refactorApr 23, 2026, 5:49 PMSexyAnalysisReviewPill.tsxclear button ui improvsFeb 24, 2026, 2:27 PMtoolbar-pill.tsfix(search): the Cmd+K filters were three different controls in three different places
The deal type was a tinted badge on the input's right edge, the mode and owner
were chips stacked above it, and the filter button opened a fourth thing. They
are now one row of one badge under the input, and the menus are the tasks
Filter menu — a submenu per facet, checkable rows, all of the user's AOPs
rather than the shared ones.
Owner scope and deal type persist across opens and reloads, defaulting to
org-wide, so the bar stops forgetting how you set it up.
The menus were non-modal, which Radix dismisses on focus-outside — the dialog's
own focus trap closed them the instant they mounted, so the pills looked dead.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 10:48 PM