From 1cf27010558959ce401f719ebd503c61ffa0f401 Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Mon, 2 Nov 2026 13:06:34 +0530 Subject: [PATCH 1/3] fix(web): restore composer focus when returning to a thread --- apps/web/src/components/ChatView.tsx | 16 +- .../src/components/ThreadTerminalDrawer.tsx | 1 + ...FocusComposerOnThreadOpen.browser.test.tsx | 17 +- .../chat/useFocusComposerOnThreadOpen.ts | 39 +++-- .../web/src/lib/terminalFocus.browser.test.ts | 162 ++++++++++++++++++ apps/web/src/lib/terminalFocus.ts | 62 ++++++- 6 files changed, 263 insertions(+), 34 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index fdbb91d9073ff..f53fc542902ee 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -216,7 +216,7 @@ import { usePanelAnimationSettings, usePanelPresence } from "../panelAnimations" import { useNewThreadHandler } from "../hooks/useHandleNewThread"; import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl"; import { isPreviewFocused } from "../lib/previewFocus"; -import { focusTerminalOnThreadEnter, getTerminalFocusOwner } from "../lib/terminalFocus"; +import { getTerminalFocusOwner } from "../lib/terminalFocus"; import { deriveLogicalProjectKeyFromSettings, selectProjectGroupingSettings, @@ -2138,8 +2138,10 @@ export default function ChatView(props: ChatViewProps) { }); useFocusComposerOnThreadOpen( - activeThread?.id, - !embedded && !terminalUiState.terminalOpen, + activeThreadRef, + workspaceLayoutElement, + !embedded, + terminalUiState.terminalOpen, focusComposerStable, ); @@ -2676,14 +2678,6 @@ export default function ChatView(props: ChatViewProps) { setTerminalUiLaunchContext(null); } - // Entering a thread with its drawer open focuses the drawer terminal; opening - // it from a handler requests focus there. A closed drawer leaves focus to the - // composer effect above. - // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- focuses the open drawer terminal when entering a thread - useEffect(() => { - if (activeThreadRef && !embedded) return focusTerminalOnThreadEnter(activeThreadRef); - }, [activeThreadRef, embedded]); - const getShortcutContext = (eventTarget: EventTarget | null = document.activeElement) => ({ terminalFocus: getTerminalFocusOwner() !== null, terminalOpen: Boolean(terminalUiState.terminalOpen), diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 049589d0b671a..98604724cc118 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -1206,6 +1206,7 @@ function TerminalSessionViewport({ const unregister = registerTerminalFocusTarget({ threadRef, terminalId, + element: mount, focus, }); return () => { diff --git a/apps/web/src/components/chat/useFocusComposerOnThreadOpen.browser.test.tsx b/apps/web/src/components/chat/useFocusComposerOnThreadOpen.browser.test.tsx index 864f4c7079497..8324e96133e73 100644 --- a/apps/web/src/components/chat/useFocusComposerOnThreadOpen.browser.test.tsx +++ b/apps/web/src/components/chat/useFocusComposerOnThreadOpen.browser.test.tsx @@ -1,4 +1,6 @@ -import { useRef } from "react"; +import { scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { useRef, useState } from "react"; import { expect, it, vi } from "vite-plus/test"; import { act, fakeTimers, render } from "../../../test/render"; @@ -6,14 +8,21 @@ import { useFocusComposerOnThreadOpen } from "./useFocusComposerOnThreadOpen"; function ThreadProbe(props: { threadId: string }) { const composerRef = useRef(null); - useFocusComposerOnThreadOpen(props.threadId, true, () => composerRef.current?.focus()); + const [root, setRoot] = useState(null); + useFocusComposerOnThreadOpen( + scopeThreadRef(EnvironmentId.make("rename"), ThreadId.make(props.threadId)), + root, + true, + false, + () => composerRef.current?.focus(), + ); return ( - <> +