ConversationEmailRow.tsxfeat: v0 smart & conversation inboxApr 21, 2026, 11:30 PMConversationGroup.tsxfeat: v0 smart & conversation inboxApr 21, 2026, 11:30 PMConversationInboxLayout.tsxfix(artifacts): open the chat instead of reciting its id, and never leave a blank screen
A LinkedIn chat opened from a context chip drew a card with the counterpart's
name over the raw chat id — a screen with no message on it and no way forward.
Nothing outside the inbox route could turn a chat id into the feed row the chat
view renders from, so the panel had only the chip's kind and id, and drew those.
`inbox.channelItem` reads one container by the provider's key and maps it through
the feed's own normalizers, with none of the feed's list filters: done, snoozed
and quarantined are rules about what belongs in a LIST, and you have already
clicked this chat. One resolver — feed row, container read, then whatever opened
it left behind — now backs every surface, so Slack, LinkedIn and WhatsApp are
openable from all of them or from none. The label card is gone.
The same audit turned up three more dead ends. The overlay hosts hide the whole
page behind an artifact flag, so a branch that resolves to null is a white screen
with nothing to click: `isThreadOpen` is derived from the artifact while the
branch read `selectedThreadId`, which every chat-side writer leaves untouched.
ActiveViewDisplay now takes each id off the artifact itself, and ArtifactDeadEnd
is the floor under all of it. `isSlackThreadOpen` becomes `isChannelChatOpen` and
covers all three channels — a LinkedIn artifact used to flip no flag at all, so
/tasks and /inbox rendered straight past it. And the home panel says so when its
one query fails, rather than staying blank for the session.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 26, 2026, 11:36 PMConversationTaskRow.tsxfix(tasks): a tick on an already-resolved task stops looping (design: zach-sept10-bugs phase 1)
Zach, 2026-09-10: "i keep checking this off and it comes back."
It was a closed loop. The Overview renders Due Tasks off the crm.getConversation
cache and filters status === 'todo', so a row the server has already resolved can
still be on screen. Ticking it reached completeTask, which refused anything not
`todo`; the tRPC route THREW that refusal; the client's onError rolled the row
back — restoring the very row the user was clearing. Every attempt re-rendered
it, so the tick could never converge.
Measured: tasks ad0fd8a4 and 5fbee194 went status='deleted' at 17:07:32-34 UTC,
and the screenshot 8 seconds later still lists both as open.
Two halves, because they fail independently:
- completeTask treats `deleted`/`agent_deleted` as a terminal SUCCESS, the way
`done` always was, and reports which state via `alreadyResolved`. The status is
never rewritten — a deleted task must not come back as done — and the draft
cleanup does not re-run, since deleteTask already tidied it.
- The client refetches before it restores, so a row that is genuinely gone stays
gone. `staleTime: 0` on that refetch is load-bearing: this same function has
already optimistically written `done` into that cache, so a fetch allowed to
serve cached data would read back its own write and drop tasks that are still
open. A test caught exactly that.
ConversationTaskRow refreshed only conversationInbox.list, leaving the other
three userTasks-carrying reads stale; it now invalidates all four like the main
path, and refetches on error.
Headless proof, all five statuses against the real service:
pnpm --filter @zero/server task-complete-probe
PASS todo → completed | done → done | deleted → deleted |
agent_deleted → agent_deleted | recommended → refused
Two pre-existing tests asserted the old contract; the deleted one was pinning the
bug itself, and is rewritten to keep the half that still holds (no draft cleanup).
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 10, 2026, 8:18 PM