Uh oh!
There was an error while loading. Please reload this page.
Split chat send state into worktree prep and turn-send phases - #97
Conversation
WalkthroughReplaces ChatView's Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Send phase not set before first await
- Added
setSendPhase("sending-turn")immediately after clearing the composer state and before any awaits, ensuringisSendBusyis true during the auto-title await gap for non-worktree first messages.
- Added
- ✅ Fixed: Error recovery restores content to wrong thread
- Added an
activeThreadIdRefthat tracks the current thread and a guard conditionactiveThreadIdRef.current === threadIdForSendin the catch block to prevent restoring old thread content into a different thread's composer.
- Added an
Or push these changes by commenting:
@cursor push 7970e70d02
Preview (7970e70d02)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx--- a/apps/web/src/components/ChatView.tsx+++ b/apps/web/src/components/ChatView.tsx@@ -364,6 +364,7 @@
const composerCommandInputRef = useRef<HTMLInputElement>(null);
const composerImagesRef = useRef<ComposerImageAttachment[]>([]);
const dragDepthRef = useRef(0);
+ const activeThreadIdRef = useRef<ThreadId | undefined>(threadId);
const terminalOpenByThreadRef = useRef<Record<string, boolean>>({});
const activeThread = state.threads.find((t) => t.id === threadId);
@@ -1008,6 +1009,7 @@
}, [composerImages]);
useEffect(() => {
+ activeThreadIdRef.current = activeThread?.id;
setComposerImages((existing) => {
revokePreviewUrls(existing);
return [];
@@ -1353,6 +1355,7 @@
setComposerImages([]);
setComposerCursor(0);
setComposerHighlightedItemId(null);
+ setSendPhase("sending-turn");
let attemptedTurnStart = false;
try {
@@ -1436,6 +1439,7 @@
} catch (err) {
if (
!attemptedTurnStart &&
+ activeThreadIdRef.current === threadIdForSend &&
promptRef.current.length === 0 &&
composerImagesRef.current.length === 0
) {| setIsSending(true); | ||
| try { | ||
| setSendPhase("sending-turn"); |
There was a problem hiding this comment.
Send phase not set before first await
Medium Severity
For non-worktree first messages, the composer is cleared (lines 1350–1355) but setSendPhase is not called until line 1401, after the auto-title await. During this gap, sendPhase remains "idle", so isSendBusy is false. Because the textarea is now always enabled during sends (disabled={isConnecting}), a user could type a new message and submit it before setSendPhase("sending-turn") fires, bypassing the isSendBusy guard and causing concurrent turn dispatches.
Additional Locations (1)
Uh oh!
There was an error while loading. Please reload this page.
6ff4c9d to
7506881Compare- Replace boolean `isSending` with `sendPhase` state - Show "Preparing worktree..." status before first-turn dispatch - Keep composer editable while preventing duplicate sends and restore input on pre-send failures
7506881 to
8edbf4bCompare- Resolve `T3CODE_STATE_DIR` override/env values with `path.resolve` - Prevent path breakage when Turbo changes the working directory
- Append pending user messages to the timeline before server echo arrives - Reuse message ID/timestamp for optimistic and sent payloads to avoid duplicates - Remove optimistic entries on sync or send failure
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Optimistic message persists as ghost after failed dispatch
- Moved the setOptimisticUserMessages filter call outside the !attemptedTurnStart guard so the optimistic message is always removed on error, preventing ghost messages when the dispatch fails after attemptedTurnStart is set to true.
Or push these changes by commenting:
@cursor push 80c2dac816
Preview (80c2dac816)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx--- a/apps/web/src/components/ChatView.tsx+++ b/apps/web/src/components/ChatView.tsx@@ -1509,14 +1509,14 @@
createdAt: messageCreatedAt,
});
} catch (err) {
+ setOptimisticUserMessages((existing) =>+ existing.filter((message) => message.id !== messageIdForSend),+ );
if (
!attemptedTurnStart &&
promptRef.current.length === 0 &&
composerImagesRef.current.length === 0
) {
- setOptimisticUserMessages((existing) =>- existing.filter((message) => message.id !== messageIdForSend),- );
promptRef.current = trimmed;
setPrompt(trimmed);
setComposerImages(composerImagesSnapshot);| setPrompt(trimmed); | ||
| setComposerImages(composerImagesSnapshot); | ||
| setComposerCursor(trimmed.length); | ||
| } |
There was a problem hiding this comment.
Optimistic message persists as ghost after failed dispatch
Medium Severity
When attemptedTurnStart is true but thread.turn.start dispatch fails (e.g., network error), the optimistic message is never removed from optimisticUserMessages. The catch block only cleans up when !attemptedTurnStart, and the cleanup effect at line 1045 early-returns when activeThread.messages.length === 0 (first-message case), so the ghost message persists in timelineMessages indefinitely until the user switches threads. For follow-up messages, the cleanup effect also can't remove it since the server never received the message ID. This results in a permanently visible "sent" message that was never actually delivered.
Additional Locations (1)
Constraint: Jarvis PRs pingdotgg#97-pingdotgg#104 expose parent_chat_id for project threads and this task required a tight sidebar/conversation-only diff.\nRejected: Rewriting sidebar project grouping or composer flows | outside P3 ownership and likely to conflict with parallel agents.\nConfidence: high\nScope-risk: narrow\nDirective: Keep contracts schema-only and keep future child-chat creation separate from this render-only tree work.\nTested: pnpm exec vp test apps/web/src/components/chatTree.logic.test.ts; pnpm exec vp check; pnpm exec vp run typecheck\nNot-tested: Manual browser dogfood; task explicitly said not to run dev servers.



Summary
isSendingwith explicit send phases inChatView(idle,preparing-worktree,sending-turn) to separate worktree setup from turn dispatch.onSendflow to snapshot thread state, clear composer early, and restore prompt/attachments if send fails beforethread.turn.start.Preparing worktree..., update buttonaria-labelstates, and gate actions usingisSendBusy..plans/17-claude-code.md.Testing
Note
Medium Risk
Touches core
ChatViewsend and timeline rendering logic, including optimistic message handling and new phase gating; mistakes could cause duplicated/missing messages or stuck UI states, but changes are localized to the web client.Overview
Refactors
ChatView’s send flow to replace booleanisSendingwith explicitsendPhasestates (idle,preparing-worktree,sending-turn), separating first-message worktree creation from turn dispatch and tightening busy-state gating (e.g., checkpoint revert + send button).Adds optimistic user message rendering by merging pending user messages into
timelineMessages, auto-scroll/empty-state checks against that merged list, and cleanup to drop optimistic entries once the server message arrives.Improves composer UX by clearing input/attachments immediately on send, showing a “Preparing worktree...” indicator, and restoring prompt/attachments (and removing the optimistic message) if failure occurs before
thread.turn.start;scripts/dev-runner.mjsnow resolvesT3CODE_STATE_DIRto an absolute path so relative values survive turbo directory changes.Written by Cursor Bugbot for commit 5121dad. This will update automatically on new commits. Configure here.
Note
Split ChatView send lifecycle into
SendPhasewith worktree preparation and turn sending, and update UI to show optimistic user messages and phase-specific controls in ChatView.tsxIntroduce
SendPhase(idle|preparing-worktree|sending-turn), add optimistic user message insertion, derive timeline from optimistic + server messages, block reverts during any send phase, and adjust send button/labels while keeping the textarea editable; resolveT3CODE_STATE_DIRto absolute in dev-runner.mjs.📍Where to Start
Start with the
ChatViewcomponent state andonSendhandler in ChatView.tsx.Macroscope summarized 5121dad.
Summary by CodeRabbit
New Features
Bug Fixes
Chores