| author | |
| committer | |
| log | dd23bd73d94dd711df16fea5d380ddd7a194cf0e |
| tree | ef73878d51462452b3ff07e7e2292490e727fe6d |
| parent | 8dd107429e92dfcb52d8afb46e2a01517840fabc |
- Add a shared composer-focus scheduler in `ChatView`
- Refocus composer after runtime mode, model, effort, and env mode selections
- Let `BranchToolbar` request composer focus after branch/worktree actions2 files changed, 36 insertions(+), 3 deletions(-)
apps/web/src/components/BranchToolbar.tsx+11-1| ... | @@ -25,9 +25,15 @@ interface BranchToolbarProps { | ... | @@ -25,9 +25,15 @@ interface BranchToolbarProps { |
| 25 | envMode: "local" | "worktree"; | 25 | envMode: "local" | "worktree"; |
| 26 | onEnvModeChange: (mode: "local" | "worktree") => void; | 26 | onEnvModeChange: (mode: "local" | "worktree") => void; |
| 27 | envLocked: boolean; | 27 | envLocked: boolean; |
| 28 | onComposerFocusRequest?: () => void; | ||
| 28 | } | 29 | } |
| 29 | 30 | ||
| 30 | export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: BranchToolbarProps) { | 31 | export default function BranchToolbar({ |
| 32 | envMode, | ||
| 33 | onEnvModeChange, | ||
| 34 | envLocked, | ||
| 35 | onComposerFocusRequest, | ||
| 36 | }: BranchToolbarProps) { | ||
| 31 | const { state, dispatch } = useStore(); | 37 | const { state, dispatch } = useStore(); |
| 32 | const api = useNativeApi(); | 38 | const api = useNativeApi(); |
| 33 | const queryClient = useQueryClient(); | 39 | const queryClient = useQueryClient(); |
| ... | @@ -123,6 +129,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B | ... | @@ -123,6 +129,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 123 | setThreadError(null); | 129 | setThreadError(null); |
| 124 | setThreadBranch(branch.name, null); | 130 | setThreadBranch(branch.name, null); |
| 125 | setIsBranchMenuOpen(false); | 131 | setIsBranchMenuOpen(false); |
| 132 | onComposerFocusRequest?.(); | ||
| 126 | return; | 133 | return; |
| 127 | } | 134 | } |
| 128 | 135 | ||
| ... | @@ -135,6 +142,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B | ... | @@ -135,6 +142,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 135 | // Secondary worktree → point the thread at that worktree path. | 142 | // Secondary worktree → point the thread at that worktree path. |
| 136 | setThreadBranch(branch.name, isMainWorktree ? null : branch.worktreePath); | 143 | setThreadBranch(branch.name, isMainWorktree ? null : branch.worktreePath); |
| 137 | setIsBranchMenuOpen(false); | 144 | setIsBranchMenuOpen(false); |
| 145 | onComposerFocusRequest?.(); | ||
| 138 | return; | 146 | return; |
| 139 | } | 147 | } |
| 140 | 148 | ||
| ... | @@ -143,6 +151,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B | ... | @@ -143,6 +151,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 143 | setThreadError(null); | 151 | setThreadError(null); |
| 144 | setThreadBranch(branch.name, activeWorktreePath); | 152 | setThreadBranch(branch.name, activeWorktreePath); |
| 145 | setIsBranchMenuOpen(false); | 153 | setIsBranchMenuOpen(false); |
| 154 | onComposerFocusRequest?.(); | ||
| 146 | }, | 155 | }, |
| 147 | }); | 156 | }); |
| 148 | }; | 157 | }; |
| ... | @@ -156,6 +165,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B | ... | @@ -156,6 +165,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 156 | setThreadBranch(name, activeWorktreePath); | 165 | setThreadBranch(name, activeWorktreePath); |
| 157 | setBranchQuery(""); | 166 | setBranchQuery(""); |
| 158 | setIsBranchMenuOpen(false); | 167 | setIsBranchMenuOpen(false); |
| 168 | onComposerFocusRequest?.(); | ||
| 159 | }, | 169 | }, |
| 160 | }); | 170 | }); |
| 161 | }; | 171 | }; |
apps/web/src/components/ChatView.tsx+25-2| ... | @@ -298,6 +298,11 @@ export default function ChatView() { | ... | @@ -298,6 +298,11 @@ export default function ChatView() { |
| 298 | const cursor = textarea.value.length; | 298 | const cursor = textarea.value.length; |
| 299 | textarea.setSelectionRange(cursor, cursor); | 299 | textarea.setSelectionRange(cursor, cursor); |
| 300 | }, []); | 300 | }, []); |
| 301 | const scheduleComposerFocus = useCallback(() => { | ||
| 302 | window.requestAnimationFrame(() => { | ||
| 303 | focusComposer(); | ||
| 304 | }); | ||
| 305 | }, [focusComposer]); | ||
| 301 | const toggleTerminalVisibility = useCallback(() => { | 306 | const toggleTerminalVisibility = useCallback(() => { |
| 302 | if (!activeThreadId) return; | 307 | if (!activeThreadId) return; |
| 303 | dispatch({ | 308 | dispatch({ |
| ... | @@ -369,6 +374,7 @@ export default function ChatView() { | ... | @@ -369,6 +374,7 @@ export default function ChatView() { |
| 369 | const handleRuntimeModeChange = async (mode: "approval-required" | "full-access") => { | 374 | const handleRuntimeModeChange = async (mode: "approval-required" | "full-access") => { |
| 370 | if (mode === state.runtimeMode) return; | 375 | if (mode === state.runtimeMode) return; |
| 371 | dispatch({ type: "SET_RUNTIME_MODE", mode }); | 376 | dispatch({ type: "SET_RUNTIME_MODE", mode }); |
| 377 | scheduleComposerFocus(); | ||
| 372 | if (!api) return; | 378 | if (!api) return; |
| 373 | 379 | ||
| 374 | const sessionIds = state.threads | 380 | const sessionIds = state.threads |
| ... | @@ -898,7 +904,19 @@ export default function ChatView() { | ... | @@ -898,7 +904,19 @@ export default function ChatView() { |
| 898 | threadId: activeThread.id, | 904 | threadId: activeThread.id, |
| 899 | model: resolveModelSlug(model), | 905 | model: resolveModelSlug(model), |
| 900 | }); | 906 | }); |
| 907 | scheduleComposerFocus(); | ||
| 908 | }; | ||
| 909 | const onEffortSelect = (effort: ReasoningEffort) => { | ||
| 910 | setSelectedEffort(effort); | ||
| 911 | scheduleComposerFocus(); | ||
| 901 | }; | 912 | }; |
| 913 | const onEnvModeChange = useCallback( | ||
| 914 | (mode: "local" | "worktree") => { | ||
| 915 | setEnvMode(mode); | ||
| 916 | scheduleComposerFocus(); | ||
| 917 | }, | ||
| 918 | [scheduleComposerFocus], | ||
| 919 | ); | ||
| 902 | 920 | ||
| 903 | const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => { | 921 | const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => { |
| 904 | if (e.key === "Enter" && !e.shiftKey) { | 922 | if (e.key === "Enter" && !e.shiftKey) { |
| ... | @@ -1302,7 +1320,7 @@ export default function ChatView() { | ... | @@ -1302,7 +1320,7 @@ export default function ChatView() { |
| 1302 | <Separator orientation="vertical" className="mx-0.5 h-4" /> | 1320 | <Separator orientation="vertical" className="mx-0.5 h-4" /> |
| 1303 | 1321 | ||
| 1304 | {/* Reasoning effort */} | 1322 | {/* Reasoning effort */} |
| 1305 | <ReasoningEffortPicker effort={selectedEffort} onEffortChange={setSelectedEffort} /> | 1323 | <ReasoningEffortPicker effort={selectedEffort} onEffortChange={onEffortSelect} /> |
| 1306 | 1324 | ||
| 1307 | {/* Divider */} | 1325 | {/* Divider */} |
| 1308 | <Separator orientation="vertical" className="mx-0.5 h-4" /> | 1326 | <Separator orientation="vertical" className="mx-0.5 h-4" /> |
| ... | @@ -1405,7 +1423,12 @@ export default function ChatView() { | ... | @@ -1405,7 +1423,12 @@ export default function ChatView() { |
| 1405 | </div> | 1423 | </div> |
| 1406 | 1424 | ||
| 1407 | {isGitRepo && ( | 1425 | {isGitRepo && ( |
| 1408 | <BranchToolbar envMode={envMode} onEnvModeChange={setEnvMode} envLocked={envLocked} /> | 1426 | <BranchToolbar |
| 1427 | envMode={envMode} | ||
| 1428 | onEnvModeChange={onEnvModeChange} | ||
| 1429 | envLocked={envLocked} | ||
| 1430 | onComposerFocusRequest={scheduleComposerFocus} | ||
| 1431 | /> | ||
| 1409 | )} | 1432 | )} |
| 1410 | 1433 | ||
| 1411 | {activeThread.terminalOpen && api && activeProject && ( | 1434 | {activeThread.terminalOpen && api && activeProject && ( |