From c6047688a153f007582badb30b60c79969d6ef35 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Feb 2026 01:29:27 -0800 Subject: [PATCH 1/3] Keep composer focused after toolbar and mode changes - 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 actions --- apps/web/src/components/BranchToolbar.tsx | 12 +++++++++- apps/web/src/components/ChatView.tsx | 27 +++++++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index f5b670cecf0..1e6dd9e923c 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -25,9 +25,15 @@ interface BranchToolbarProps { envMode: "local" | "worktree"; onEnvModeChange: (mode: "local" | "worktree") => void; envLocked: boolean; + onComposerFocusRequest?: () => void; } -export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: BranchToolbarProps) { +export default function BranchToolbar({ + envMode, + onEnvModeChange, + envLocked, + onComposerFocusRequest, +}: BranchToolbarProps) { const { state, dispatch } = useStore(); const api = useNativeApi(); const queryClient = useQueryClient(); @@ -123,6 +129,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B setThreadError(null); setThreadBranch(branch.name, null); setIsBranchMenuOpen(false); + onComposerFocusRequest?.(); return; } @@ -135,6 +142,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B // Secondary worktree → point the thread at that worktree path. setThreadBranch(branch.name, isMainWorktree ? null : branch.worktreePath); setIsBranchMenuOpen(false); + onComposerFocusRequest?.(); return; } @@ -143,6 +151,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B setThreadError(null); setThreadBranch(branch.name, activeWorktreePath); setIsBranchMenuOpen(false); + onComposerFocusRequest?.(); }, }); }; @@ -156,6 +165,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B setThreadBranch(name, activeWorktreePath); setBranchQuery(""); setIsBranchMenuOpen(false); + onComposerFocusRequest?.(); }, }); }; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 806d26fdc44..553f029ec36 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -273,6 +273,11 @@ export default function ChatView() { const cursor = textarea.value.length; textarea.setSelectionRange(cursor, cursor); }, []); + const scheduleComposerFocus = useCallback(() => { + window.requestAnimationFrame(() => { + focusComposer(); + }); + }, [focusComposer]); const toggleTerminalVisibility = useCallback(() => { if (!activeThreadId) return; dispatch({ @@ -344,6 +349,7 @@ export default function ChatView() { const handleRuntimeModeChange = async (mode: "approval-required" | "full-access") => { if (mode === state.runtimeMode) return; dispatch({ type: "SET_RUNTIME_MODE", mode }); + scheduleComposerFocus(); if (!api) return; const sessionIds = state.threads @@ -813,7 +819,19 @@ export default function ChatView() { threadId: activeThread.id, model: resolveModelSlug(model), }); + scheduleComposerFocus(); + }; + const onEffortSelect = (effort: ReasoningEffort) => { + setSelectedEffort(effort); + scheduleComposerFocus(); }; + const onEnvModeChange = useCallback( + (mode: "local" | "worktree") => { + setEnvMode(mode); + scheduleComposerFocus(); + }, + [scheduleComposerFocus], + ); const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { @@ -1213,7 +1231,7 @@ export default function ChatView() { {/* Reasoning effort */} - + {/* Divider */} @@ -1316,7 +1334,12 @@ export default function ChatView() { {isGitRepo && ( - + )} {activeThread.terminalOpen && api && activeProject && ( From 4d2f16be47ad730af252ffce3b7d3d351bf83dee Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Feb 2026 16:22:00 -0800 Subject: [PATCH 2/3] Simplify env mode change handler in ChatView - Remove unnecessary `useCallback` wrapping from `onEnvModeChange` - Keep env mode update and composer refocus behavior unchanged --- apps/web/src/components/ChatView.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 553f029ec36..f3c6619fa78 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -825,13 +825,10 @@ export default function ChatView() { setSelectedEffort(effort); scheduleComposerFocus(); }; - const onEnvModeChange = useCallback( - (mode: "local" | "worktree") => { - setEnvMode(mode); - scheduleComposerFocus(); - }, - [scheduleComposerFocus], - ); + const onEnvModeChange = (mode: "local" | "worktree") => { + setEnvMode(mode); + scheduleComposerFocus(); + }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { From c9fae23efe5ca6512ef2c84d99f1f0b46f9917f0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Feb 2026 16:23:11 -0800 Subject: [PATCH 3/3] Memoize ChatView selection handlers with useCallback - Wrap model, effort, and env mode handlers in `useCallback` - Stabilize callback identities while preserving composer focus behavior --- apps/web/src/components/ChatView.tsx | 43 +++++++++++++++++----------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f3c6619fa78..24aee68cb9c 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -812,23 +812,32 @@ export default function ChatView() { } }; - const onModelSelect = (model: ModelSlug) => { - if (!activeThread) return; - dispatch({ - type: "SET_THREAD_MODEL", - threadId: activeThread.id, - model: resolveModelSlug(model), - }); - scheduleComposerFocus(); - }; - const onEffortSelect = (effort: ReasoningEffort) => { - setSelectedEffort(effort); - scheduleComposerFocus(); - }; - const onEnvModeChange = (mode: "local" | "worktree") => { - setEnvMode(mode); - scheduleComposerFocus(); - }; + const onModelSelect = useCallback( + (model: ModelSlug) => { + if (!activeThread) return; + dispatch({ + type: "SET_THREAD_MODEL", + threadId: activeThread.id, + model: resolveModelSlug(model), + }); + scheduleComposerFocus(); + }, + [activeThread, dispatch, scheduleComposerFocus], + ); + const onEffortSelect = useCallback( + (effort: ReasoningEffort) => { + setSelectedEffort(effort); + scheduleComposerFocus(); + }, + [scheduleComposerFocus], + ); + const onEnvModeChange = useCallback( + (mode: "local" | "worktree") => { + setEnvMode(mode); + scheduleComposerFocus(); + }, + [scheduleComposerFocus], + ); const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) {