From 9c0f8a34de79a09d892dbbc27eb62e293a52548c Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 11:42:54 -0700 Subject: [PATCH] Cancel composer focus frames from root cleanup (#3452) --- .../components/NewWorkspaceComposerCard.tsx | 18 ++++++++++++++++-- .../components/NewWorkspaceComposerModal.tsx | 10 +++++++++- src/renderer/src/hooks/useComposerState.ts | 13 ++++++++++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.tsx index f610c2785c6..b7546889ba8 100644 --- a/src/renderer/src/components/NewWorkspaceComposerCard.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerCard.tsx @@ -41,6 +41,7 @@ type RepoOption = React.ComponentProps['repos'][number] type NewWorkspaceComposerCardProps = { containerClassName?: string composerRef?: React.RefObject + onComposerNodeChange?: (node: HTMLDivElement | null) => void nameInputRef?: React.RefObject quickAgent: TuiAgent | null onQuickAgentChange: (agent: TuiAgent | null) => void @@ -205,6 +206,7 @@ function useComposerFileDragOver(): { export default function NewWorkspaceComposerCard({ containerClassName, composerRef, + onComposerNodeChange, nameInputRef, quickAgent, onQuickAgentChange, @@ -281,7 +283,19 @@ export default function NewWorkspaceComposerCard({ nameInputFocusFrameRef.current = null }, []) - React.useEffect(() => cancelNameInputFocusFrame, [cancelNameInputFocusFrame]) + const setComposerNode = React.useCallback( + (node: HTMLDivElement | null): void => { + // Why: the queued repo-picker focus is only valid while this composer exists. + if (!node) { + cancelNameInputFocusFrame() + } + if (composerRef) { + composerRef.current = node + } + onComposerNodeChange?.(node) + }, + [cancelNameInputFocusFrame, composerRef, onComposerNodeChange] + ) const focusNameInput = React.useCallback(() => { // Why: after the repo picker commits a choice, moving focus to the name @@ -313,7 +327,7 @@ export default function NewWorkspaceComposerCard({ return (
s.settings) - const { cardProps, composerRef, nameInputRef, submitQuick, createDisabled } = useComposerState({ + const { + cardProps, + composerRef, + onComposerNodeChange, + nameInputRef, + submitQuick, + createDisabled + } = useComposerState({ initialName: modalData.prefilledName ?? '', // Why: the modal is quick-create only now, so prompt-prefill state is // intentionally ignored even if older callers still send it. @@ -219,6 +226,7 @@ function QuickTabBody({ + onComposerNodeChange: (node: HTMLDivElement | null) => void promptTextareaRef: React.RefObject nameInputRef: React.RefObject submit: () => Promise @@ -514,7 +515,16 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS promptCaretFrameRef.current = null }, []) - useEffect(() => cancelPromptCaretFrame, [cancelPromptCaretFrame]) + const handleComposerNodeChange = useCallback( + (node: HTMLDivElement | null): void => { + // Why: the queued caret restoration targets composer descendants and + // must be canceled as soon as the composer root leaves the DOM. + if (!node) { + cancelPromptCaretFrame() + } + }, + [cancelPromptCaretFrame] + ) const hookCheckRef = useRef<{ key: string @@ -2329,6 +2339,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS return { cardProps, composerRef, + onComposerNodeChange: handleComposerNodeChange, promptTextareaRef, nameInputRef, submit,