authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 01:29:27-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-15 16:27:25-08:00
logdd23bd73d94dd711df16fea5d380ddd7a194cf0e
treeef73878d51462452b3ff07e7e2292490e727fe6d
parent8dd107429e92dfcb52d8afb46e2a01517840fabc

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

2 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}
2930
30export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: BranchToolbarProps) {31export 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 }
128135
...@@ -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 }
140148
...@@ -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;
373379
374 const sessionIds = state.threads380 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 );
902920
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" />
13031321
1304 {/* Reasoning effort */}1322 {/* Reasoning effort */}
1305 <ReasoningEffortPicker effort={selectedEffort} onEffortChange={setSelectedEffort} />1323 <ReasoningEffortPicker effort={selectedEffort} onEffortChange={onEffortSelect} />
13061324
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>
14061424
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 )}
14101433
1411 {activeThread.terminalOpen && api && activeProject && (1434 {activeThread.terminalOpen && api && activeProject && (