🤖 fix: preserve creation prompt caret on workspace updates #1284
+106
−11
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes an annoying UX bug on the ProjectPage (workspace creation screen): when the workspace tree updates (e.g. a new workspace/subworkspace is created elsewhere), we were re-running ChatInput’s
onReadywiring and re-focusing the textarea, which forces the caret to the end.Changes:
onReadyeffect on unrelated prop changes.Validation:
make static-check📋 Implementation Plan
Fix: prompt textarea caret jumps to end when other workspaces appear
What’s happening (and why it feels like the cursor “jumps to the bottom”)
While you’re typing in the workspace creation prompt (the screen in your screenshot), the app re-renders when any workspace/subworkspace is created elsewhere. That re-render unintentionally re-runs the “ready” callback for the chat input, which re-focuses the textarea and forcibly moves the caret to the end.
Concrete chain (with code pointers):
ProjectPage→ChatInput(variant="creation") →VimTextArea.src/browser/components/ProjectPage.tsxsrc/browser/components/ChatInput/index.tsxChatInputexposes an API to parents via anonReadyeffect.props(the entire props object), so it re-runs whenever any prop identity changes.src/browser/components/ChatInput/index.tsx(useEffect“Provide API to parent via callback”)App.tsxpasses an inlineonWorkspaceCreated={(metadata) => { … }}function intoProjectPage, so every App re-render creates a new function identity.workspaceMetadataupdates →Appre-renders → newonWorkspaceCreatedprop identity.src/browser/App.tsx(rendering<ProjectPage … onWorkspaceCreated={(metadata) => { … }} />)ChatInput’sonReadyeffect re-runs,ProjectPage.handleChatReadyruns again and callsapi.focus().src/browser/components/ProjectPage.tsx(handleChatReady)ChatInput.focusMessageInput()always does:element.focus()selectionStart/selectionEnd = element.value.lengthsrc/browser/components/ChatInput/index.tsx(focusMessageInput)Recommended fix (Approach A — minimal, targeted)
Net product LoC estimate: ~+10 / -2
onReadyon unrelated re-renderssrc/browser/components/ChatInput/index.tsx, update theuseEffectthat callsprops.onReady(…).propsfrom the dependency array; depend only on the specific values used (props.onReady, and the API callbacks).onReadybehave like “component mounted / API changed” instead of “any parent re-render”.ProjectPage’s initial autofocus idempotentsrc/browser/components/ProjectPage.tsx, guardhandleChatReadyso it only callsapi.focus()once per mount.const didAutoFocusRef = useRef(false);and only focus when false.document.activeElementis not already the textarea / not inside the chat input.onReadyis called again for some legitimate reason in the future, we won’t steal the caret.Optional hardening (Approach B — improve focus behavior)
Net product LoC estimate: ~+10–20
focusMessageInput(src/browser/components/ChatInput/index.tsx), detect whendocument.activeElement === inputRef.current.selectionStart/selectionEnd = value.lengthstep.Why this is useful:
Alternative (more robust, more code): preserve selection across temporary focus loss
If you want the caret to return to the exact prior position even after focus moves away (e.g. opening a model picker), store
selectionStart/Endin a ref viaonSelect/onKeyUp, then restore it on re-focus. This is more invasive and should be done only if Approach A+B still leaves edge cases.Tests / regression coverage
Net product LoC estimate: 0
Net test LoC estimate: ~+40–80
Add a regression test that fails with today’s behavior:
ChatInputinvariant="creation"(or renderProjectPageif that’s easier for setup).setSelectionRange).onWorkspaceCreatedfunction viarerender).selectionStart/Enddid not change.Good locations:
src/browser/components/ChatInput/ChatInputCaret.test.tsxsrc/browser/components/ChatInput/.Manual QA checklist
Notes / non-goals
ChatInputto a stable position inApp.tsx). Those would also solve remount/selection issues but are higher-risk and higher-LoC.Generated with
mux• Model:openai:gpt-5.2• Thinking:xhigh