Split chat send state into worktree prep and turn-send phases - #97

Merged
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e
Feb 27, 2026
Merged

Split chat send state into worktree prep and turn-send phases#97
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace isSending with explicit send phases in ChatView (idle, preparing-worktree, sending-turn) to separate worktree setup from turn dispatch.
  • Refactor onSend flow to snapshot thread state, clear composer early, and restore prompt/attachments if send fails before thread.turn.start.
  • Improve UI/UX during send lifecycle: show Preparing worktree..., update button aria-label states, and gate actions using isSendBusy.
  • Remove obsolete planning document .plans/17-claude-code.md.

Testing

  • Not run (no test or lint output was provided in this change context).

Note

Medium Risk
Touches core ChatView send 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 boolean isSending with explicit sendPhase states (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.mjs now resolves T3CODE_STATE_DIR to 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 SendPhase with worktree preparation and turn sending, and update UI to show optimistic user messages and phase-specific controls in ChatView.tsx

Introduce 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; resolve T3CODE_STATE_DIR to absolute in dev-runner.mjs.

📍Where to Start

Start with the ChatView component state and onSend handler in ChatView.tsx.

Macroscope summarized 5121dad.

Summary by CodeRabbit

  • New Features

    • Shows "Preparing worktree…" and updates send button labels/ARIA for clearer send-state feedback.
    • Displays optimistic user messages immediately and merges them into the timeline for smoother rendering and auto-scroll.
  • Bug Fixes

    • Preserves user input and attachments on send failures for easier retry.
    • More reliable worktree initialization for first messages and steadier send flow.
  • Chores

    • Resolves configured state-directory paths to absolute paths to avoid relative-path issues.

@coderabbitai

coderabbitaiBot commented Feb 27, 2026

Copy link
Copy Markdown

Walkthrough

Replaces ChatView's isSending boolean with a three-state SendPhase ("idle", "preparing-worktree", "sending-turn"); adds conditional worktree preparation for first messages, uses threadIdForSend for dispatching, updates error paths and UI indicators to reflect the new send phases. (50 words)

Changes

Cohort / File(s)Summary
Send state, worktree & dispatch flow
apps/web/src/components/ChatView.tsx
Replaces isSending with SendPhase + derived isSendBusy/isPreparingWorktree; adds baseBranch/worktree creation flow for first-message sends and updates thread meta with worktree path; introduces threadIdForSend for subsequent dispatches; updates guards, error paths, UI labels, and a "Preparing worktree…" indicator; resets phase on completion or reset.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant ChatView
participant WorktreeService
participant ThreadMetaStore
participant Dispatcher
participant SetupScriptRunner

User->>ChatView: Click Send
ChatView->>ChatView: compute baseBranchForWorktree, set SendPhase = "preparing-worktree"
ChatView->>WorktreeService: create worktree (if needed)
WorktreeService-->>ChatView: worktree path / response
ChatView->>ThreadMetaStore: update thread.meta with worktree path (threadIdForSend)
ThreadMetaStore-->>ChatView: ack
ChatView->>SetupScriptRunner: optionally run setup script
SetupScriptRunner-->>ChatView: done
ChatView->>ChatView: set SendPhase = "sending-turn"
ChatView->>Dispatcher: dispatch send-turn command (uses threadIdForSend)
Dispatcher-->>ChatView: send result / error
ChatView->>ChatView: set SendPhase = "idle" / update UI

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'Split chat send state into worktree prep and turn-send phases' directly and clearly describes the main change: replacing boolean isSending with a SendPhase type that separates worktree preparation from turn dispatch.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/1a01c92e

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, ensuring isSendBusy is true during the auto-title await gap for non-worktree first messages.
  • ✅ Fixed: Error recovery restores content to wrong thread
    • Added an activeThreadIdRef that tracks the current thread and a guard condition activeThreadIdRef.current === threadIdForSend in the catch block to prevent restoring old thread content into a different thread's composer.

Create PR

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

Comment threadapps/web/src/components/ChatView.tsx
- 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
- 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
@juliusmarminge
juliusmarminge merged commit c7a029a into mainFeb 27, 2026
3 of 4 checks passed

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

roughcoder added a commit to roughcoder/jarvis-cockpit that referenced this pull request Jul 7, 2026
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Split chat send state into worktree prep and turn-send phases - #97

Merged
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e
Feb 27, 2026
Merged

Split chat send state into worktree prep and turn-send phases#97
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace isSending with explicit send phases in ChatView (idle, preparing-worktree, sending-turn) to separate worktree setup from turn dispatch.
  • Refactor onSend flow to snapshot thread state, clear composer early, and restore prompt/attachments if send fails before thread.turn.start.
  • Improve UI/UX during send lifecycle: show Preparing worktree..., update button aria-label states, and gate actions using isSendBusy.
  • Remove obsolete planning document .plans/17-claude-code.md.

Testing

  • Not run (no test or lint output was provided in this change context).

Note

Medium Risk
Touches core ChatView send 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 boolean isSending with explicit sendPhase states (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.mjs now resolves T3CODE_STATE_DIR to 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 SendPhase with worktree preparation and turn sending, and update UI to show optimistic user messages and phase-specific controls in ChatView.tsx

Introduce 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; resolve T3CODE_STATE_DIR to absolute in dev-runner.mjs.

📍Where to Start

Start with the ChatView component state and onSend handler in ChatView.tsx.

Macroscope summarized 5121dad.

Summary by CodeRabbit

  • New Features

    • Shows "Preparing worktree…" and updates send button labels/ARIA for clearer send-state feedback.
    • Displays optimistic user messages immediately and merges them into the timeline for smoother rendering and auto-scroll.
  • Bug Fixes

    • Preserves user input and attachments on send failures for easier retry.
    • More reliable worktree initialization for first messages and steadier send flow.
  • Chores

    • Resolves configured state-directory paths to absolute paths to avoid relative-path issues.

@coderabbitai

coderabbitaiBot commented Feb 27, 2026

Copy link
Copy Markdown

Walkthrough

Replaces ChatView's isSending boolean with a three-state SendPhase ("idle", "preparing-worktree", "sending-turn"); adds conditional worktree preparation for first messages, uses threadIdForSend for dispatching, updates error paths and UI indicators to reflect the new send phases. (50 words)

Changes

Cohort / File(s)Summary
Send state, worktree & dispatch flow
apps/web/src/components/ChatView.tsx
Replaces isSending with SendPhase + derived isSendBusy/isPreparingWorktree; adds baseBranch/worktree creation flow for first-message sends and updates thread meta with worktree path; introduces threadIdForSend for subsequent dispatches; updates guards, error paths, UI labels, and a "Preparing worktree…" indicator; resets phase on completion or reset.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant ChatView
participant WorktreeService
participant ThreadMetaStore
participant Dispatcher
participant SetupScriptRunner

User->>ChatView: Click Send
ChatView->>ChatView: compute baseBranchForWorktree, set SendPhase = "preparing-worktree"
ChatView->>WorktreeService: create worktree (if needed)
WorktreeService-->>ChatView: worktree path / response
ChatView->>ThreadMetaStore: update thread.meta with worktree path (threadIdForSend)
ThreadMetaStore-->>ChatView: ack
ChatView->>SetupScriptRunner: optionally run setup script
SetupScriptRunner-->>ChatView: done
ChatView->>ChatView: set SendPhase = "sending-turn"
ChatView->>Dispatcher: dispatch send-turn command (uses threadIdForSend)
Dispatcher-->>ChatView: send result / error
ChatView->>ChatView: set SendPhase = "idle" / update UI

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'Split chat send state into worktree prep and turn-send phases' directly and clearly describes the main change: replacing boolean isSending with a SendPhase type that separates worktree preparation from turn dispatch.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/1a01c92e

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, ensuring isSendBusy is true during the auto-title await gap for non-worktree first messages.
  • ✅ Fixed: Error recovery restores content to wrong thread
    • Added an activeThreadIdRef that tracks the current thread and a guard condition activeThreadIdRef.current === threadIdForSend in the catch block to prevent restoring old thread content into a different thread's composer.

Create PR

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

Comment threadapps/web/src/components/ChatView.tsx
- 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
- 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
@juliusmarminge
juliusmarminge merged commit c7a029a into mainFeb 27, 2026
3 of 4 checks passed

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

roughcoder added a commit to roughcoder/jarvis-cockpit that referenced this pull request Jul 7, 2026
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Split chat send state into worktree prep and turn-send phases - #97

Merged
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e
Feb 27, 2026
Merged

Split chat send state into worktree prep and turn-send phases#97
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace isSending with explicit send phases in ChatView (idle, preparing-worktree, sending-turn) to separate worktree setup from turn dispatch.
  • Refactor onSend flow to snapshot thread state, clear composer early, and restore prompt/attachments if send fails before thread.turn.start.
  • Improve UI/UX during send lifecycle: show Preparing worktree..., update button aria-label states, and gate actions using isSendBusy.
  • Remove obsolete planning document .plans/17-claude-code.md.

Testing

  • Not run (no test or lint output was provided in this change context).

Note

Medium Risk
Touches core ChatView send 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 boolean isSending with explicit sendPhase states (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.mjs now resolves T3CODE_STATE_DIR to 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 SendPhase with worktree preparation and turn sending, and update UI to show optimistic user messages and phase-specific controls in ChatView.tsx

Introduce 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; resolve T3CODE_STATE_DIR to absolute in dev-runner.mjs.

📍Where to Start

Start with the ChatView component state and onSend handler in ChatView.tsx.

Macroscope summarized 5121dad.

Summary by CodeRabbit

  • New Features

    • Shows "Preparing worktree…" and updates send button labels/ARIA for clearer send-state feedback.
    • Displays optimistic user messages immediately and merges them into the timeline for smoother rendering and auto-scroll.
  • Bug Fixes

    • Preserves user input and attachments on send failures for easier retry.
    • More reliable worktree initialization for first messages and steadier send flow.
  • Chores

    • Resolves configured state-directory paths to absolute paths to avoid relative-path issues.

@coderabbitai

coderabbitaiBot commented Feb 27, 2026

Copy link
Copy Markdown

Walkthrough

Replaces ChatView's isSending boolean with a three-state SendPhase ("idle", "preparing-worktree", "sending-turn"); adds conditional worktree preparation for first messages, uses threadIdForSend for dispatching, updates error paths and UI indicators to reflect the new send phases. (50 words)

Changes

Cohort / File(s)Summary
Send state, worktree & dispatch flow
apps/web/src/components/ChatView.tsx
Replaces isSending with SendPhase + derived isSendBusy/isPreparingWorktree; adds baseBranch/worktree creation flow for first-message sends and updates thread meta with worktree path; introduces threadIdForSend for subsequent dispatches; updates guards, error paths, UI labels, and a "Preparing worktree…" indicator; resets phase on completion or reset.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant ChatView
participant WorktreeService
participant ThreadMetaStore
participant Dispatcher
participant SetupScriptRunner

User->>ChatView: Click Send
ChatView->>ChatView: compute baseBranchForWorktree, set SendPhase = "preparing-worktree"
ChatView->>WorktreeService: create worktree (if needed)
WorktreeService-->>ChatView: worktree path / response
ChatView->>ThreadMetaStore: update thread.meta with worktree path (threadIdForSend)
ThreadMetaStore-->>ChatView: ack
ChatView->>SetupScriptRunner: optionally run setup script
SetupScriptRunner-->>ChatView: done
ChatView->>ChatView: set SendPhase = "sending-turn"
ChatView->>Dispatcher: dispatch send-turn command (uses threadIdForSend)
Dispatcher-->>ChatView: send result / error
ChatView->>ChatView: set SendPhase = "idle" / update UI

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'Split chat send state into worktree prep and turn-send phases' directly and clearly describes the main change: replacing boolean isSending with a SendPhase type that separates worktree preparation from turn dispatch.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/1a01c92e

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, ensuring isSendBusy is true during the auto-title await gap for non-worktree first messages.
  • ✅ Fixed: Error recovery restores content to wrong thread
    • Added an activeThreadIdRef that tracks the current thread and a guard condition activeThreadIdRef.current === threadIdForSend in the catch block to prevent restoring old thread content into a different thread's composer.

Create PR

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

Comment threadapps/web/src/components/ChatView.tsx
- 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
- 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
@juliusmarminge
juliusmarminge merged commit c7a029a into mainFeb 27, 2026
3 of 4 checks passed

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

roughcoder added a commit to roughcoder/jarvis-cockpit that referenced this pull request Jul 7, 2026
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Split chat send state into worktree prep and turn-send phases - #97

Merged
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e
Feb 27, 2026
Merged

Split chat send state into worktree prep and turn-send phases#97
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace isSending with explicit send phases in ChatView (idle, preparing-worktree, sending-turn) to separate worktree setup from turn dispatch.
  • Refactor onSend flow to snapshot thread state, clear composer early, and restore prompt/attachments if send fails before thread.turn.start.
  • Improve UI/UX during send lifecycle: show Preparing worktree..., update button aria-label states, and gate actions using isSendBusy.
  • Remove obsolete planning document .plans/17-claude-code.md.

Testing

  • Not run (no test or lint output was provided in this change context).

Note

Medium Risk
Touches core ChatView send 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 boolean isSending with explicit sendPhase states (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.mjs now resolves T3CODE_STATE_DIR to 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 SendPhase with worktree preparation and turn sending, and update UI to show optimistic user messages and phase-specific controls in ChatView.tsx

Introduce 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; resolve T3CODE_STATE_DIR to absolute in dev-runner.mjs.

📍Where to Start

Start with the ChatView component state and onSend handler in ChatView.tsx.

Macroscope summarized 5121dad.

Summary by CodeRabbit

  • New Features

    • Shows "Preparing worktree…" and updates send button labels/ARIA for clearer send-state feedback.
    • Displays optimistic user messages immediately and merges them into the timeline for smoother rendering and auto-scroll.
  • Bug Fixes

    • Preserves user input and attachments on send failures for easier retry.
    • More reliable worktree initialization for first messages and steadier send flow.
  • Chores

    • Resolves configured state-directory paths to absolute paths to avoid relative-path issues.

@coderabbitai

coderabbitaiBot commented Feb 27, 2026

Copy link
Copy Markdown

Walkthrough

Replaces ChatView's isSending boolean with a three-state SendPhase ("idle", "preparing-worktree", "sending-turn"); adds conditional worktree preparation for first messages, uses threadIdForSend for dispatching, updates error paths and UI indicators to reflect the new send phases. (50 words)

Changes

Cohort / File(s)Summary
Send state, worktree & dispatch flow
apps/web/src/components/ChatView.tsx
Replaces isSending with SendPhase + derived isSendBusy/isPreparingWorktree; adds baseBranch/worktree creation flow for first-message sends and updates thread meta with worktree path; introduces threadIdForSend for subsequent dispatches; updates guards, error paths, UI labels, and a "Preparing worktree…" indicator; resets phase on completion or reset.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant ChatView
participant WorktreeService
participant ThreadMetaStore
participant Dispatcher
participant SetupScriptRunner

User->>ChatView: Click Send
ChatView->>ChatView: compute baseBranchForWorktree, set SendPhase = "preparing-worktree"
ChatView->>WorktreeService: create worktree (if needed)
WorktreeService-->>ChatView: worktree path / response
ChatView->>ThreadMetaStore: update thread.meta with worktree path (threadIdForSend)
ThreadMetaStore-->>ChatView: ack
ChatView->>SetupScriptRunner: optionally run setup script
SetupScriptRunner-->>ChatView: done
ChatView->>ChatView: set SendPhase = "sending-turn"
ChatView->>Dispatcher: dispatch send-turn command (uses threadIdForSend)
Dispatcher-->>ChatView: send result / error
ChatView->>ChatView: set SendPhase = "idle" / update UI

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'Split chat send state into worktree prep and turn-send phases' directly and clearly describes the main change: replacing boolean isSending with a SendPhase type that separates worktree preparation from turn dispatch.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/1a01c92e

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, ensuring isSendBusy is true during the auto-title await gap for non-worktree first messages.
  • ✅ Fixed: Error recovery restores content to wrong thread
    • Added an activeThreadIdRef that tracks the current thread and a guard condition activeThreadIdRef.current === threadIdForSend in the catch block to prevent restoring old thread content into a different thread's composer.

Create PR

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

Comment threadapps/web/src/components/ChatView.tsx
- 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
- 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
@juliusmarminge
juliusmarminge merged commit c7a029a into mainFeb 27, 2026
3 of 4 checks passed

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

roughcoder added a commit to roughcoder/jarvis-cockpit that referenced this pull request Jul 7, 2026
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Split chat send state into worktree prep and turn-send phases - #97

Merged
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e
Feb 27, 2026
Merged

Split chat send state into worktree prep and turn-send phases#97
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace isSending with explicit send phases in ChatView (idle, preparing-worktree, sending-turn) to separate worktree setup from turn dispatch.
  • Refactor onSend flow to snapshot thread state, clear composer early, and restore prompt/attachments if send fails before thread.turn.start.
  • Improve UI/UX during send lifecycle: show Preparing worktree..., update button aria-label states, and gate actions using isSendBusy.
  • Remove obsolete planning document .plans/17-claude-code.md.

Testing

  • Not run (no test or lint output was provided in this change context).

Note

Medium Risk
Touches core ChatView send 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 boolean isSending with explicit sendPhase states (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.mjs now resolves T3CODE_STATE_DIR to 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 SendPhase with worktree preparation and turn sending, and update UI to show optimistic user messages and phase-specific controls in ChatView.tsx

Introduce 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; resolve T3CODE_STATE_DIR to absolute in dev-runner.mjs.

📍Where to Start

Start with the ChatView component state and onSend handler in ChatView.tsx.

Macroscope summarized 5121dad.

Summary by CodeRabbit

  • New Features

    • Shows "Preparing worktree…" and updates send button labels/ARIA for clearer send-state feedback.
    • Displays optimistic user messages immediately and merges them into the timeline for smoother rendering and auto-scroll.
  • Bug Fixes

    • Preserves user input and attachments on send failures for easier retry.
    • More reliable worktree initialization for first messages and steadier send flow.
  • Chores

    • Resolves configured state-directory paths to absolute paths to avoid relative-path issues.

@coderabbitai

coderabbitaiBot commented Feb 27, 2026

Copy link
Copy Markdown

Walkthrough

Replaces ChatView's isSending boolean with a three-state SendPhase ("idle", "preparing-worktree", "sending-turn"); adds conditional worktree preparation for first messages, uses threadIdForSend for dispatching, updates error paths and UI indicators to reflect the new send phases. (50 words)

Changes

Cohort / File(s)Summary
Send state, worktree & dispatch flow
apps/web/src/components/ChatView.tsx
Replaces isSending with SendPhase + derived isSendBusy/isPreparingWorktree; adds baseBranch/worktree creation flow for first-message sends and updates thread meta with worktree path; introduces threadIdForSend for subsequent dispatches; updates guards, error paths, UI labels, and a "Preparing worktree…" indicator; resets phase on completion or reset.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant ChatView
participant WorktreeService
participant ThreadMetaStore
participant Dispatcher
participant SetupScriptRunner

User->>ChatView: Click Send
ChatView->>ChatView: compute baseBranchForWorktree, set SendPhase = "preparing-worktree"
ChatView->>WorktreeService: create worktree (if needed)
WorktreeService-->>ChatView: worktree path / response
ChatView->>ThreadMetaStore: update thread.meta with worktree path (threadIdForSend)
ThreadMetaStore-->>ChatView: ack
ChatView->>SetupScriptRunner: optionally run setup script
SetupScriptRunner-->>ChatView: done
ChatView->>ChatView: set SendPhase = "sending-turn"
ChatView->>Dispatcher: dispatch send-turn command (uses threadIdForSend)
Dispatcher-->>ChatView: send result / error
ChatView->>ChatView: set SendPhase = "idle" / update UI

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'Split chat send state into worktree prep and turn-send phases' directly and clearly describes the main change: replacing boolean isSending with a SendPhase type that separates worktree preparation from turn dispatch.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/1a01c92e

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, ensuring isSendBusy is true during the auto-title await gap for non-worktree first messages.
  • ✅ Fixed: Error recovery restores content to wrong thread
    • Added an activeThreadIdRef that tracks the current thread and a guard condition activeThreadIdRef.current === threadIdForSend in the catch block to prevent restoring old thread content into a different thread's composer.

Create PR

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

Comment threadapps/web/src/components/ChatView.tsx
- 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
- 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
@juliusmarminge
juliusmarminge merged commit c7a029a into mainFeb 27, 2026
3 of 4 checks passed

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

roughcoder added a commit to roughcoder/jarvis-cockpit that referenced this pull request Jul 7, 2026
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Split chat send state into worktree prep and turn-send phases - #97

Merged
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e
Feb 27, 2026
Merged

Split chat send state into worktree prep and turn-send phases#97
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace isSending with explicit send phases in ChatView (idle, preparing-worktree, sending-turn) to separate worktree setup from turn dispatch.
  • Refactor onSend flow to snapshot thread state, clear composer early, and restore prompt/attachments if send fails before thread.turn.start.
  • Improve UI/UX during send lifecycle: show Preparing worktree..., update button aria-label states, and gate actions using isSendBusy.
  • Remove obsolete planning document .plans/17-claude-code.md.

Testing

  • Not run (no test or lint output was provided in this change context).

Note

Medium Risk
Touches core ChatView send 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 boolean isSending with explicit sendPhase states (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.mjs now resolves T3CODE_STATE_DIR to 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 SendPhase with worktree preparation and turn sending, and update UI to show optimistic user messages and phase-specific controls in ChatView.tsx

Introduce 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; resolve T3CODE_STATE_DIR to absolute in dev-runner.mjs.

📍Where to Start

Start with the ChatView component state and onSend handler in ChatView.tsx.

Macroscope summarized 5121dad.

Summary by CodeRabbit

  • New Features

    • Shows "Preparing worktree…" and updates send button labels/ARIA for clearer send-state feedback.
    • Displays optimistic user messages immediately and merges them into the timeline for smoother rendering and auto-scroll.
  • Bug Fixes

    • Preserves user input and attachments on send failures for easier retry.
    • More reliable worktree initialization for first messages and steadier send flow.
  • Chores

    • Resolves configured state-directory paths to absolute paths to avoid relative-path issues.

@coderabbitai

coderabbitaiBot commented Feb 27, 2026

Copy link
Copy Markdown

Walkthrough

Replaces ChatView's isSending boolean with a three-state SendPhase ("idle", "preparing-worktree", "sending-turn"); adds conditional worktree preparation for first messages, uses threadIdForSend for dispatching, updates error paths and UI indicators to reflect the new send phases. (50 words)

Changes

Cohort / File(s)Summary
Send state, worktree & dispatch flow
apps/web/src/components/ChatView.tsx
Replaces isSending with SendPhase + derived isSendBusy/isPreparingWorktree; adds baseBranch/worktree creation flow for first-message sends and updates thread meta with worktree path; introduces threadIdForSend for subsequent dispatches; updates guards, error paths, UI labels, and a "Preparing worktree…" indicator; resets phase on completion or reset.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant ChatView
participant WorktreeService
participant ThreadMetaStore
participant Dispatcher
participant SetupScriptRunner

User->>ChatView: Click Send
ChatView->>ChatView: compute baseBranchForWorktree, set SendPhase = "preparing-worktree"
ChatView->>WorktreeService: create worktree (if needed)
WorktreeService-->>ChatView: worktree path / response
ChatView->>ThreadMetaStore: update thread.meta with worktree path (threadIdForSend)
ThreadMetaStore-->>ChatView: ack
ChatView->>SetupScriptRunner: optionally run setup script
SetupScriptRunner-->>ChatView: done
ChatView->>ChatView: set SendPhase = "sending-turn"
ChatView->>Dispatcher: dispatch send-turn command (uses threadIdForSend)
Dispatcher-->>ChatView: send result / error
ChatView->>ChatView: set SendPhase = "idle" / update UI

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'Split chat send state into worktree prep and turn-send phases' directly and clearly describes the main change: replacing boolean isSending with a SendPhase type that separates worktree preparation from turn dispatch.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/1a01c92e

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, ensuring isSendBusy is true during the auto-title await gap for non-worktree first messages.
  • ✅ Fixed: Error recovery restores content to wrong thread
    • Added an activeThreadIdRef that tracks the current thread and a guard condition activeThreadIdRef.current === threadIdForSend in the catch block to prevent restoring old thread content into a different thread's composer.

Create PR

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

Comment threadapps/web/src/components/ChatView.tsx
- 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
- 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
@juliusmarminge
juliusmarminge merged commit c7a029a into mainFeb 27, 2026
3 of 4 checks passed

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

roughcoder added a commit to roughcoder/jarvis-cockpit that referenced this pull request Jul 7, 2026
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Split chat send state into worktree prep and turn-send phases - #97

Merged
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e
Feb 27, 2026
Merged

Split chat send state into worktree prep and turn-send phases#97
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace isSending with explicit send phases in ChatView (idle, preparing-worktree, sending-turn) to separate worktree setup from turn dispatch.
  • Refactor onSend flow to snapshot thread state, clear composer early, and restore prompt/attachments if send fails before thread.turn.start.
  • Improve UI/UX during send lifecycle: show Preparing worktree..., update button aria-label states, and gate actions using isSendBusy.
  • Remove obsolete planning document .plans/17-claude-code.md.

Testing

  • Not run (no test or lint output was provided in this change context).

Note

Medium Risk
Touches core ChatView send 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 boolean isSending with explicit sendPhase states (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.mjs now resolves T3CODE_STATE_DIR to 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 SendPhase with worktree preparation and turn sending, and update UI to show optimistic user messages and phase-specific controls in ChatView.tsx

Introduce 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; resolve T3CODE_STATE_DIR to absolute in dev-runner.mjs.

📍Where to Start

Start with the ChatView component state and onSend handler in ChatView.tsx.

Macroscope summarized 5121dad.

Summary by CodeRabbit

  • New Features

    • Shows "Preparing worktree…" and updates send button labels/ARIA for clearer send-state feedback.
    • Displays optimistic user messages immediately and merges them into the timeline for smoother rendering and auto-scroll.
  • Bug Fixes

    • Preserves user input and attachments on send failures for easier retry.
    • More reliable worktree initialization for first messages and steadier send flow.
  • Chores

    • Resolves configured state-directory paths to absolute paths to avoid relative-path issues.

@coderabbitai

coderabbitaiBot commented Feb 27, 2026

Copy link
Copy Markdown

Walkthrough

Replaces ChatView's isSending boolean with a three-state SendPhase ("idle", "preparing-worktree", "sending-turn"); adds conditional worktree preparation for first messages, uses threadIdForSend for dispatching, updates error paths and UI indicators to reflect the new send phases. (50 words)

Changes

Cohort / File(s)Summary
Send state, worktree & dispatch flow
apps/web/src/components/ChatView.tsx
Replaces isSending with SendPhase + derived isSendBusy/isPreparingWorktree; adds baseBranch/worktree creation flow for first-message sends and updates thread meta with worktree path; introduces threadIdForSend for subsequent dispatches; updates guards, error paths, UI labels, and a "Preparing worktree…" indicator; resets phase on completion or reset.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant ChatView
participant WorktreeService
participant ThreadMetaStore
participant Dispatcher
participant SetupScriptRunner

User->>ChatView: Click Send
ChatView->>ChatView: compute baseBranchForWorktree, set SendPhase = "preparing-worktree"
ChatView->>WorktreeService: create worktree (if needed)
WorktreeService-->>ChatView: worktree path / response
ChatView->>ThreadMetaStore: update thread.meta with worktree path (threadIdForSend)
ThreadMetaStore-->>ChatView: ack
ChatView->>SetupScriptRunner: optionally run setup script
SetupScriptRunner-->>ChatView: done
ChatView->>ChatView: set SendPhase = "sending-turn"
ChatView->>Dispatcher: dispatch send-turn command (uses threadIdForSend)
Dispatcher-->>ChatView: send result / error
ChatView->>ChatView: set SendPhase = "idle" / update UI

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'Split chat send state into worktree prep and turn-send phases' directly and clearly describes the main change: replacing boolean isSending with a SendPhase type that separates worktree preparation from turn dispatch.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/1a01c92e

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, ensuring isSendBusy is true during the auto-title await gap for non-worktree first messages.
  • ✅ Fixed: Error recovery restores content to wrong thread
    • Added an activeThreadIdRef that tracks the current thread and a guard condition activeThreadIdRef.current === threadIdForSend in the catch block to prevent restoring old thread content into a different thread's composer.

Create PR

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

Comment threadapps/web/src/components/ChatView.tsx
- 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
- 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
@juliusmarminge
juliusmarminge merged commit c7a029a into mainFeb 27, 2026
3 of 4 checks passed

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

roughcoder added a commit to roughcoder/jarvis-cockpit that referenced this pull request Jul 7, 2026
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Split chat send state into worktree prep and turn-send phases - #97

Merged
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e
Feb 27, 2026
Merged

Split chat send state into worktree prep and turn-send phases#97
juliusmarminge merged 3 commits into
mainfrom
codething/1a01c92e

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace isSending with explicit send phases in ChatView (idle, preparing-worktree, sending-turn) to separate worktree setup from turn dispatch.
  • Refactor onSend flow to snapshot thread state, clear composer early, and restore prompt/attachments if send fails before thread.turn.start.
  • Improve UI/UX during send lifecycle: show Preparing worktree..., update button aria-label states, and gate actions using isSendBusy.
  • Remove obsolete planning document .plans/17-claude-code.md.

Testing

  • Not run (no test or lint output was provided in this change context).

Note

Medium Risk
Touches core ChatView send 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 boolean isSending with explicit sendPhase states (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.mjs now resolves T3CODE_STATE_DIR to 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 SendPhase with worktree preparation and turn sending, and update UI to show optimistic user messages and phase-specific controls in ChatView.tsx

Introduce 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; resolve T3CODE_STATE_DIR to absolute in dev-runner.mjs.

📍Where to Start

Start with the ChatView component state and onSend handler in ChatView.tsx.

Macroscope summarized 5121dad.

Summary by CodeRabbit

  • New Features

    • Shows "Preparing worktree…" and updates send button labels/ARIA for clearer send-state feedback.
    • Displays optimistic user messages immediately and merges them into the timeline for smoother rendering and auto-scroll.
  • Bug Fixes

    • Preserves user input and attachments on send failures for easier retry.
    • More reliable worktree initialization for first messages and steadier send flow.
  • Chores

    • Resolves configured state-directory paths to absolute paths to avoid relative-path issues.

@coderabbitai

coderabbitaiBot commented Feb 27, 2026

Copy link
Copy Markdown

Walkthrough

Replaces ChatView's isSending boolean with a three-state SendPhase ("idle", "preparing-worktree", "sending-turn"); adds conditional worktree preparation for first messages, uses threadIdForSend for dispatching, updates error paths and UI indicators to reflect the new send phases. (50 words)

Changes

Cohort / File(s)Summary
Send state, worktree & dispatch flow
apps/web/src/components/ChatView.tsx
Replaces isSending with SendPhase + derived isSendBusy/isPreparingWorktree; adds baseBranch/worktree creation flow for first-message sends and updates thread meta with worktree path; introduces threadIdForSend for subsequent dispatches; updates guards, error paths, UI labels, and a "Preparing worktree…" indicator; resets phase on completion or reset.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant ChatView
participant WorktreeService
participant ThreadMetaStore
participant Dispatcher
participant SetupScriptRunner

User->>ChatView: Click Send
ChatView->>ChatView: compute baseBranchForWorktree, set SendPhase = "preparing-worktree"
ChatView->>WorktreeService: create worktree (if needed)
WorktreeService-->>ChatView: worktree path / response
ChatView->>ThreadMetaStore: update thread.meta with worktree path (threadIdForSend)
ThreadMetaStore-->>ChatView: ack
ChatView->>SetupScriptRunner: optionally run setup script
SetupScriptRunner-->>ChatView: done
ChatView->>ChatView: set SendPhase = "sending-turn"
ChatView->>Dispatcher: dispatch send-turn command (uses threadIdForSend)
Dispatcher-->>ChatView: send result / error
ChatView->>ChatView: set SendPhase = "idle" / update UI

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'Split chat send state into worktree prep and turn-send phases' directly and clearly describes the main change: replacing boolean isSending with a SendPhase type that separates worktree preparation from turn dispatch.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/1a01c92e

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, ensuring isSendBusy is true during the auto-title await gap for non-worktree first messages.
  • ✅ Fixed: Error recovery restores content to wrong thread
    • Added an activeThreadIdRef that tracks the current thread and a guard condition activeThreadIdRef.current === threadIdForSend in the catch block to prevent restoring old thread content into a different thread's composer.

Create PR

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

Comment threadapps/web/src/components/ChatView.tsx
- 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
- 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
@juliusmarminge
juliusmarminge merged commit c7a029a into mainFeb 27, 2026
3 of 4 checks passed

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in CursorFix in Web

roughcoder added a commit to roughcoder/jarvis-cockpit that referenced this pull request Jul 7, 2026
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge