feat(canvas-chat): cross-device sync + free multi-send + new-session + auto-grow (core#2697) #2700
Reference in New Issue
Block a user
Delete Branch "feat/2697-canvas-chat-ux"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements the 4 CTO-requested canvas chat UX improvements as a single molecule-core PR (Go backend + React/TypeScript frontend). Closes #2697.
Features
1. Server-authoritative cross-device sync + id-based dedup
EventUserMessage/EventSessionResetadded to the event taxonomy.broadcastUserMessageFromA2ABodyhelper fans out aUSER_MESSAGEWS event afterpersistUserMessageAtIngestdurably commits the user message. Payload mirrorsAGENT_MESSAGEshape (message_id, content, attachments, workspace_id).appendMessageDedupedById(canvas) dedupes by id regardless of timing — origin device's optimistic add collapses the WS echo (same id), other devices append fresh.useChatSocketnow subscribes toUSER_MESSAGEandSESSION_RESET.2. Free multi-send (no sending-gated input lock)
sendingfrom the textarea / send-button / attach-button disabled guards inChatTab.tsx. The hook still trackssendingfor the 'thinking' indicator; distinct messages no longer block each other. Uploading stays gated (file upload is sequential at the wire).handleSenddrops thesendingguard so two messages can fire in quick succession. The hook'ssendInFlightRef/sendTokenRefstill prevent double-fires for the SAME message.3. New-session soft boundary (non-destructive, CTO decision)
20260613010000:workspaces.chat_session_started_at(nullable, idempotentADD COLUMN IF NOT EXISTS). NULL = no boundary; pre-deploy workspaces read history unchanged.POST /workspaces/:id/chat-session/newrotates the marker tonow()and broadcastsSESSION_RESETso other devices clear in lockstep.chat-historyendpoint reads the marker;ListOptionsgainsHasSessionStarted/SessionStartedAt;queryActivityRowscomposes the filter with the existingbefore_tscursor.4. Auto-expanding textarea
scrollHeight, capped at ~6 lines, internal scroll past cap). Reset to single-row on send. Preserves IME-safe Enter / Shift+Enter / paste handling.Tests
chat_session_test.go(7 cases): NewSession 400 on bad UUID; broadcast helper emits correct payload (text+attachments); no broadcast on empty/malformed body; no panic on nil broadcaster; no broadcast on empty messageId; SESSION_RESET payload shape.postgres_store_test.go(3 cases):SessionFilter_NoMarkerNoFilter(HasSessionStarted=false → no filter);SessionFilter_AppliesToQuery(marker binds as $N);SessionFilter_ComposesWithBeforeCursor(both filters, 3-arg bind).Verified
go build ./...clean.go vet ./...clean.feat/2697-canvas-chat-ux.Out of scope (deferred follow-ups)
useChatSendin-flight counter refactor — the dispatch suggests tracking pending sends as a count so the thinking indicator survives multiple in-flight messages. The current booleansendingis sufficient for the visible UX (one or more messages in flight → indicator up); a follow-up can land the counter if/when a real bug is reported.appendMessageDedupedtime-windowed for agent messages,appendMessageDedupedByIdfor the new USER_MESSAGE path) so the agent triple-delivery contract is unchanged.Reviewer notes
LogActivity(the row is the source of truth; a phantom-free contract).Tagging CTO for review per the dispatch.
COMMENT on head
b9a2e19fcb.I’m not approving yet because two review inputs are still unresolved:
Required CI is not complete:
CI / Canvas (Next.js)is still pending on this head. Go/API/handlers/E2E Chat/Staging Canvas are green so far.The requested Playwright coverage is not actually present for the full feature set. Current e2e coverage appears to include only the existing chat flows plus a new mobile textarea auto-grow assertion. I do not see genuine Playwright coverage for cross-device USER_MESSAGE id-dedup, free multi-send, or the New session / SESSION_RESET flow. The PR body also still says Playwright e2e coverage for those items is deferred, which conflicts with the request to confirm it is genuine.
One implementation question before approval:
persistUserMessageAtIngestcallsLogActivity(...)and then unconditionally callsbroadcastUserMessageFromA2ABody(...). The nearby comment says this should fire only on successful persist to avoid phantom USER_MESSAGE events, but this call site does not observe a LogActivity success/failure signal. If LogActivity can swallow INSERT errors as documented, the broadcast can still happen without durable history. Please either wire a success signal before broadcasting or confirm LogActivity cannot fail silently on this path.The core design looks aligned: nullable/idempotent migration, session filter guarded by HasSessionStarted, SESSION_RESET endpoint, id-based client dedup, and free multi-send UI gates all match the requested shape. Holding approval pending the above.
Code re-review on head
a2f5db6e6d: the #11302 concerns are addressed. LogActivityWithResult gates both ACTIVITY_LOGGED and USER_MESSAGE behind the durable activity_logs insert, so I do not see a remaining phantom USER_MESSAGE path. The appendMessageDedupedById unit tests cover the load-bearing cases: origin WS echo collapse by id, fresh broadcast append, empty-id fallback, and same-content/different-id multi-send. I am OK with deferring the Playwright e2e given that focused unit coverage.Not approving yet because current required CI is not green: CI / Canvas (Next.js) is failing on this head and CI / all-required is skipped. Please fix or rerun Canvas to green, then I can approve + /sop-ack.
RCA tick: #2700 current-head Canvas red plus non-code gate reds.
MECHANISM: current head
a2f5db6e6dc808601b3f74342d4bb0699d955dc2has one real Canvas unit-test failure incanvas/src/components/tabs/chat/__tests__/types.test.ts:176-183. The testdoes NOT collapse entries that share content but not idcallsvi.advanceTimersByTime(50)at line 180, but this test file has not enabled fake timers for that test scope, so Vitest fails before it exercisesappendMessageDedupedById. Separately, the repeated qa/security/reserved-path/gate-check reds are governance waits for non-author approvals/SOP acks, not product-code failures.EVIDENCE: run 356869/job 484502 reports
types.test.ts > appendMessageDedupedByIdand the log excerpttimers APIs are not mocked. The source at current head still hasvi.advanceTimersByTime(50)atcanvas/src/components/tabs/chat/__tests__/types.test.ts:180. Current combined status also showsqa-review,security-review,reserved-path-review, andsop-checklistfailures; their logs explicitly say awaiting non-author approval or missing SOP ack. Advisory run 356878/job 484522 is a separate environment/lifecycle flake: Docker name conflict followed by boot-register 401s and stub DNS errors.RECOMMENDED FIX SHAPE: in
canvas/src/components/tabs/chat/__tests__/types.test.ts, either remove the unnecessary timer advancement from this dedupe-by-id test or wrap the relevant test/describe invi.useFakeTimers()/vi.useRealTimers()with cleanup. Keep the production dedupe code unchanged unless the fixed test then exposes a behavioral mismatch. After that, rerun Canvas; approvals/SOP can be handled as governance, and the advisory lifecycle failure should be tracked separately only if it blocks a required gate.APPROVED on head
ca1ea8976d.Final pass after the Canvas CI fix: the appendMessageDedupedById test block now uses fake timers and a pinned system time, matching the sibling appendMessageDeduped setup, so the same-content/different-id case no longer relies on vi.advanceTimersByTime as a no-op. Prior code review remains clean: LogActivityWithResult gates both ACTIVITY_LOGGED and USER_MESSAGE behind durable activity_logs insert success, the id-based dedup unit tests cover the load-bearing cross-device cases, and the Playwright e2e deferral is acceptable given that focused unit coverage. CI / Canvas (Next.js), CI / Platform (Go), E2E API Smoke, and CI / all-required are green; PR is mergeable.
/sop-ack