From 9db0a1d29fb645aec0055307eeddfe3dc8724813 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 10:51:54 +0000 Subject: [PATCH 1/6] test(web): reproduce thread navigation shortcuts swallowed by terminal and composer These tests reproduce #26407. In a focused terminal, Ctrl+Shift+[ and ] (thread.previous/next) are encoded as terminal input instead of reaching the app's window keydown listener. On macOS, a user binding of Command-Up or Command-Down to thread navigation is consumed by the composer's document-boundary handling. Co-Authored-By: Claude Opus 5.5 --- ...rPromptEditorTiptap.input.browser.test.tsx | 39 +++++++++++++ .../ThreadTerminalDrawer.browser.test.tsx | 57 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.input.browser.test.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.input.browser.test.tsx index c59724cccf693..855a3273eedb0 100644 --- a/apps/web/src/components/ComposerPromptEditorTiptap.input.browser.test.tsx +++ b/apps/web/src/components/ComposerPromptEditorTiptap.input.browser.test.tsx @@ -23,6 +23,22 @@ import { type ComposerPromptEditorProps, } from "./ComposerPromptEditorTiptap"; +vi.mock(import("../state/server"), async (original) => { + const { Atom } = await import("effect/reactivity"); + const { compileResolvedKeybindingsConfig, DEFAULT_KEYBINDINGS } = + await import("@t3tools/shared/keybindings"); + return { + ...(await original()), + primaryServerKeybindingsAtom: Atom.make( + compileResolvedKeybindingsConfig([ + ...DEFAULT_KEYBINDINGS, + { key: "mod+arrowup", command: "thread.previous" }, + { key: "mod+arrowdown", command: "thread.next" }, + ]), + ), + }; +}); + const babysit: ServerProviderSkill = { name: "babysit", path: "/skills/babysit/SKILL.md", @@ -168,6 +184,29 @@ it("bolds with Mod+B only when text is selected", async () => { await vi.waitFor(() => expect(textWithMark(editor, "bold")).toEqual(["ship"])); }); +// The mocked server keybindings bind Command-Up and Command-Down to thread navigation. +it("leaves Command-arrow thread navigation bindings to the app on macOS", async () => { + using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("MacIntel"); + await using screen = await mountEditor("first\nsecond"); + const { editor } = screen; + await act(async () => { + editor.commands.focus("end"); + }); + await vi.waitFor(() => expect(document.activeElement).toBe(editor.view.dom)); + const reachedApp: string[] = []; + const onWindowKeyDown = (event: KeyboardEvent) => { + if (!event.defaultPrevented && event.key.startsWith("Arrow")) reachedApp.push(event.key); + }; + window.addEventListener("keydown", onWindowKeyDown); + using _listener = { + [Symbol.dispose]: () => window.removeEventListener("keydown", onWindowKeyDown), + }; + + await userEvent.keyboard("{Meta>}{ArrowUp}{ArrowDown}{/Meta}"); + + expect(reachedApp).toEqual(["ArrowUp", "ArrowDown"]); +}); + it.each([false, true])( "composes an umlaut without wrapping the preedit text (rich text: %s)", async (richTextEnabled) => { diff --git a/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx b/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx new file mode 100644 index 0000000000000..17684334b3d04 --- /dev/null +++ b/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx @@ -0,0 +1,57 @@ +import { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings"; +import { expect, it, vi } from "vite-plus/test"; +import { userEvent } from "vite-plus/test/browser"; + +import { render } from "../../test/render"; +import { TerminalViewport } from "./ThreadTerminalDrawer"; + +vi.mock("../terminal/ghostty/vendor/ghostty-vt.wasm?url", async () => ({ + default: (await import("../terminal/ghostty/vendor/ghostty-vt.wasm?inline")).default, +})); +vi.mock("../terminal/ghostty/vendor/ghostty-write-pty.wasm?url&no-inline", async () => ({ + default: (await import("../terminal/ghostty/vendor/ghostty-write-pty.wasm?inline")).default, +})); + +// Thread navigation defaults have no `!terminalFocus`, so they apply in a focused terminal. +it("hands thread navigation shortcuts from a focused terminal back to the app", async () => { + using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64"); + const threadId = ThreadId.make("thread"); + const container = document.createElement("div"); + container.style.cssText = "position:relative;width:600px;height:300px"; + await using screen = await render( + {}} + visible + keybindings={DEFAULT_RESOLVED_KEYBINDINGS} + />, + { container }, + ); + // The drawer marks the input once the surface has loaded and handles keys. + const input = await vi.waitFor(() => { + const element = screen.container.querySelector( + ".t3-ghostty-input[data-workspace-focus-default]", + ); + if (!element) throw new Error("Terminal surface did not load"); + return element; + }); + input.focus(); + const reachedApp: string[] = []; + const onWindowKeyDown = (event: KeyboardEvent) => { + if (!event.defaultPrevented && event.code.startsWith("Bracket")) reachedApp.push(event.code); + }; + window.addEventListener("keydown", onWindowKeyDown); + using _listener = { + [Symbol.dispose]: () => window.removeEventListener("keydown", onWindowKeyDown), + }; + + await userEvent.keyboard("{Control>}{Shift>}[BracketLeft][BracketRight]{/Shift}{/Control}"); + + expect(reachedApp).toEqual(["BracketLeft", "BracketRight"]); +}); From 93b440dc0e4910c5ba48667104fe129e805da01e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 10:56:59 +0000 Subject: [PATCH 2/6] fix(web): thread navigation shortcuts work from the terminal and macOS composer A focused terminal encoded thread.previous, thread.next and thread.jump.N as shell input, and on macOS the composer consumed Command-arrow chords for caret moves even when a user bound them to thread navigation. Both now hand a chord to the app's window listener when it resolves, under their focus context, to a thread navigation command. A user binding that excludes terminalFocus still keeps its key in the terminal. Fixes pingdotgg/t3code#26407 Co-Authored-By: Claude Opus 5.5 --- .../components/ComposerPromptEditorTiptap.tsx | 10 +++++++ .../src/components/ThreadTerminalDrawer.tsx | 4 ++- apps/web/src/keybindings.test.ts | 27 +++++++++++++++++++ apps/web/src/keybindings.ts | 14 +++++++++- 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.tsx index abc9549ee3d00..47645765fe65c 100644 --- a/apps/web/src/components/ComposerPromptEditorTiptap.tsx +++ b/apps/web/src/components/ComposerPromptEditorTiptap.tsx @@ -104,6 +104,9 @@ import { import { collectInlineContextIds } from "~/lib/composerContextReferences"; import { resolveDiffThemeName } from "~/lib/diffRendering"; import { cn, isMacPlatform } from "~/lib/utils"; +import { isThreadNavigationShortcut } from "~/keybindings"; +import { appAtomRegistry } from "~/rpc/atomRegistry"; +import { primaryServerKeybindingsAtom } from "~/state/server"; import { basenameOfPath } from "~/pierre-icons"; import { WorkspaceEntryTooltip } from "./chat/WorkspaceEntryIcon"; import { FileTagChipContent } from "./chat/FileTagChip"; @@ -1127,6 +1130,13 @@ function ComposerPromptEditorTiptapInner( !event.ctrlKey && !event.isComposing ) { + if ( + isThreadNavigationShortcut(event, appAtomRegistry.get(primaryServerKeybindingsAtom), { + context: { editableFocus: true }, + }) + ) { + return false; + } const selection = window.getSelection(); if ( selection?.anchorNode && diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 295c553827f7c..e701af3add7e1 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -67,6 +67,7 @@ import { isTerminalSplitShortcut, isTerminalSplitVerticalShortcut, isTerminalToggleShortcut, + isThreadNavigationShortcut, terminalDeleteShortcutData, terminalNavigationShortcutData, } from "../keybindings"; @@ -947,7 +948,8 @@ function TerminalSessionViewport({ isTerminalSplitShortcut(event, currentKeybindings, options) || isTerminalSplitVerticalShortcut(event, currentKeybindings, options) || isTerminalNewShortcut(event, currentKeybindings, options) || - isDiffToggleShortcut(event, currentKeybindings, options) + isDiffToggleShortcut(event, currentKeybindings, options) || + isThreadNavigationShortcut(event, currentKeybindings, options) ) { return false; } diff --git a/apps/web/src/keybindings.test.ts b/apps/web/src/keybindings.test.ts index a0ba7fa7ee043..4810da3079ff1 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -15,6 +15,7 @@ import { effectiveShortcutsForCommand, formatShortcutLabel, isRichTextBoldShortcut, + isThreadNavigationShortcut, isTerminalNewShortcut, isTerminalSplitShortcut, resolveChatShortcutCommand, @@ -373,6 +374,32 @@ describe("thread navigation helpers", () => { }); }); +describe("isThreadNavigationShortcut", () => { + it("leaves a key to a focused terminal when the user's binding excludes it", () => { + const next = event({ key: "}", code: "BracketRight", ctrlKey: true, shiftKey: true }); + const terminal = { platform: "Linux x86_64", context: { terminalFocus: true } }; + assert.isTrue(isThreadNavigationShortcut(next, DEFAULT_RESOLVED_KEYBINDINGS, terminal)); + assert.isFalse( + isThreadNavigationShortcut( + next, + mergeWithDefaultKeybindings( + compileResolvedKeybindingsConfig([ + { key: "mod+shift+]", command: "thread.next", when: "!terminalFocus" }, + ]), + ), + terminal, + ), + ); + assert.isFalse( + isThreadNavigationShortcut( + event({ key: "o", ctrlKey: true }), + DEFAULT_RESOLVED_KEYBINDINGS, + terminal, + ), + ); + }); +}); + describe("model picker navigation helpers", () => { it("keeps default model jumps off the web even while the picker is open", () => { const input = event({ key: "3", metaKey: true }); diff --git a/apps/web/src/keybindings.ts b/apps/web/src/keybindings.ts index d3d3de58d3676..ec38087d541c1 100644 --- a/apps/web/src/keybindings.ts +++ b/apps/web/src/keybindings.ts @@ -272,12 +272,24 @@ export function threadTraversalDirectionFromCommand( return null; } +export function isThreadNavigationShortcut( + event: ShortcutEventLike, + keybindings: ResolvedKeybindingsConfig, + options?: ShortcutMatchOptions, +): boolean { + const command = resolveShortcutCommand(event, keybindings, options); + return ( + threadTraversalDirectionFromCommand(command) !== null || + threadJumpIndexFromCommand(command ?? "") !== null + ); +} + export function shouldShowThreadJumpHintsForModifiers( modifiers: ShortcutModifierStateLike, keybindings: ResolvedKeybindingsConfig, options?: ShortcutMatchOptions, ): boolean { - // Ghostty can write keys to the shell before window shortcuts run, regardless of the when clause. + // Holding a modifier is routine in a shell (Ctrl+C, Ctrl+R), so don't flash hints over it. if (resolveContext(options).terminalFocus) { return false; } From 3029894bf02394cdcf3c4415ca7e963c2885f51c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 00:39:07 +0000 Subject: [PATCH 3/6] style(web): format terminal drawer browser test AI-assisted; verified with the tests and tools listed above. --- .../src/components/ThreadTerminalDrawer.browser.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx b/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx index 6c63ed9dca631..bb7034485183b 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx @@ -187,9 +187,9 @@ it("hands thread navigation shortcuts from a focused terminal back to the app", fixture.pendingSurfaces.shift()?.(); }); const terminalHandles = (key: string, code: string) => - fixture.surfaceOptions.at(-1)!.beforeKey( - new KeyboardEvent("keydown", { key, code, ctrlKey: true, shiftKey: true }), - ); + fixture.surfaceOptions + .at(-1)! + .beforeKey(new KeyboardEvent("keydown", { key, code, ctrlKey: true, shiftKey: true })); expect([terminalHandles("{", "BracketLeft"), terminalHandles("}", "BracketRight")]).toEqual([ false, From 39a8c5753075f7d7ca48c604c4f2de0fcf8d6d02 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 00:55:01 +0000 Subject: [PATCH 4/6] fix(web): terminal passes thread shortcuts only when Sidebar will accept them The pass-through check resolved previewOpen as false while Sidebar reads the thread's right panel, so a binding gated on !previewOpen skipped the shell and was then rejected by Sidebar. AI-assisted; verified with the tests and tools listed above. --- .../ThreadTerminalDrawer.browser.test.tsx | 19 ++++++++++++++----- .../src/components/ThreadTerminalDrawer.tsx | 13 ++++++++++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx b/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx index bb7034485183b..a3f33dd0b60d2 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx @@ -4,11 +4,13 @@ import { ThreadId, type ScopedThreadRef, type FilesystemEntryMetadata, + type ResolvedKeybindingsConfig, } from "@t3tools/contracts"; -import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings"; +import { compileResolvedKeybindingsConfig, DEFAULT_KEYBINDINGS } from "@t3tools/shared/keybindings"; import { Atom, AsyncResult } from "effect/reactivity"; import type { GhosttyTerminalSurfaceOptions } from "~/terminal/ghostty/surface"; import { AppAtomRegistryProvider, appAtomRegistry } from "../rpc/atomRegistry"; +import { useRightPanelStore } from "../rightPanelStore"; import { extractTerminalLinks } from "../terminal-links"; import { expect, it, vi } from "vite-plus/test"; import { act, fakeTimers, render } from "../../test/render"; @@ -111,7 +113,7 @@ function Drawer(props: { threadRef: ScopedThreadRef; cwd?: string; onCloseTerminal: (terminalId: string, threadRef?: ScopedThreadRef) => void; - keybindings?: typeof DEFAULT_RESOLVED_KEYBINDINGS; + keybindings?: ResolvedKeybindingsConfig; }) { return ( { const { other: threadRef } = setup(); using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64"); + useRightPanelStore.getState().openBrowser(threadRef, null); await using _screen = await render( {}} - keybindings={DEFAULT_RESOLVED_KEYBINDINGS} + keybindings={compileResolvedKeybindingsConfig([ + ...DEFAULT_KEYBINDINGS, + { key: "ctrl+r", command: "thread.next", when: "!previewOpen" }, + ])} />, ); await act(async () => { fixture.pendingSurfaces.shift()?.(); }); - const terminalHandles = (key: string, code: string) => + const terminalHandles = (key: string, code: string, shiftKey = true) => fixture.surfaceOptions .at(-1)! - .beforeKey(new KeyboardEvent("keydown", { key, code, ctrlKey: true, shiftKey: true })); + .beforeKey(new KeyboardEvent("keydown", { key, code, ctrlKey: true, shiftKey })); expect([terminalHandles("{", "BracketLeft"), terminalHandles("}", "BracketRight")]).toEqual([ false, false, ]); expect(terminalHandles("C", "KeyC")).toBe(true); + // Sidebar sees the open preview and rejects this binding, so the shell keeps the key. + expect(terminalHandles("r", "KeyR", false)).toBe(true); }); it("only activates terminal paths confirmed to exist on their environment", async () => { diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 310d7eaf3b14d..e843112d93b7f 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -97,6 +97,7 @@ import { openTerminalLinkInPreview } from "./preview/openTerminalLinkInPreview"; import { useAtomCommand } from "../state/use-atom-command"; import { preventTerminalCloseShortcut } from "../lib/terminalCloseShortcut"; import { observeResize } from "../lib/observeResize"; +import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; import { consumePendingTerminalFocus, focusTerminal, @@ -590,6 +591,10 @@ function TerminalSessionViewport({ const selectionActionRequestIdRef = useRef(0); const openSelectionMenuRequestIdRef = useRef(null); const readKeybindings = useEffectEvent(() => keybindings); + const readPreviewOpen = useEffectEvent( + () => + selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, threadRef) === "preview", + ); const handleSessionExited = useEffectEvent(() => { if (hasTerminalWriteAccess()) onSessionExited(); }); @@ -1012,7 +1017,13 @@ function TerminalSessionViewport({ isTerminalSplitVerticalShortcut(event, currentKeybindings, options) || isTerminalNewShortcut(event, currentKeybindings, options) || isDiffToggleShortcut(event, currentKeybindings, options) || - isThreadNavigationShortcut(event, currentKeybindings, options) + // Match Sidebar's context, or a key it will reject skips the shell. + isThreadNavigationShortcut(event, currentKeybindings, { + context: { + ...options.context, + previewOpen: readPreviewOpen(), + }, + }) ) { return false; } From 668fdc8058bd989349eedcf3cdfba90693fa066a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 01:11:23 +0000 Subject: [PATCH 5/6] fix(web): terminal reads the drawer's open state for thread shortcuts A right-panel terminal inherited terminalOpen: true while Sidebar reads the drawer store, so a binding gated on terminalOpen skipped the shell and was then rejected by Sidebar. AI-assisted; verified with the tests and tools listed above. --- .../ThreadTerminalDrawer.browser.test.tsx | 10 ++++++++-- apps/web/src/components/ThreadTerminalDrawer.tsx | 16 +++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx b/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx index a3f33dd0b60d2..850b9cb8174f5 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx @@ -11,6 +11,7 @@ import { Atom, AsyncResult } from "effect/reactivity"; import type { GhosttyTerminalSurfaceOptions } from "~/terminal/ghostty/surface"; import { AppAtomRegistryProvider, appAtomRegistry } from "../rpc/atomRegistry"; import { useRightPanelStore } from "../rightPanelStore"; +import { useTerminalUiStateStore } from "../terminalUiStateStore"; import { extractTerminalLinks } from "../terminal-links"; import { expect, it, vi } from "vite-plus/test"; import { act, fakeTimers, render } from "../../test/render"; @@ -154,6 +155,7 @@ function setup() { fixture.surfaceOptions = []; fixture.metadata.clear(); useRightPanelStore.setState(useRightPanelStore.getInitialState()); + useTerminalUiStateStore.setState(useTerminalUiStateStore.getInitialState()); return { original, other }; } @@ -187,6 +189,7 @@ it("hands thread navigation shortcuts from a focused terminal back to the app", keybindings={compileResolvedKeybindingsConfig([ ...DEFAULT_KEYBINDINGS, { key: "ctrl+r", command: "thread.next", when: "!previewOpen" }, + { key: "ctrl+g", command: "thread.previous", when: "terminalOpen" }, ])} />, ); @@ -203,8 +206,11 @@ it("hands thread navigation shortcuts from a focused terminal back to the app", false, ]); expect(terminalHandles("C", "KeyC")).toBe(true); - // Sidebar sees the open preview and rejects this binding, so the shell keeps the key. - expect(terminalHandles("r", "KeyR", false)).toBe(true); + // Sidebar sees the open preview and the closed drawer and rejects these, so the shell keeps them. + expect([terminalHandles("r", "KeyR", false), terminalHandles("g", "KeyG", false)]).toEqual([ + true, + true, + ]); }); it("only activates terminal paths confirmed to exist on their environment", async () => { diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index e843112d93b7f..e2b2f78c0c276 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -98,6 +98,7 @@ import { useAtomCommand } from "../state/use-atom-command"; import { preventTerminalCloseShortcut } from "../lib/terminalCloseShortcut"; import { observeResize } from "../lib/observeResize"; import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; +import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; import { consumePendingTerminalFocus, focusTerminal, @@ -591,10 +592,14 @@ function TerminalSessionViewport({ const selectionActionRequestIdRef = useRef(0); const openSelectionMenuRequestIdRef = useRef(null); const readKeybindings = useEffectEvent(() => keybindings); - const readPreviewOpen = useEffectEvent( - () => + const readThreadNavigationContext = useEffectEvent(() => ({ + terminalOpen: selectThreadTerminalUiState( + useTerminalUiStateStore.getState().terminalUiStateByThreadKey, + threadRef, + ).terminalOpen, + previewOpen: selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, threadRef) === "preview", - ); + })); const handleSessionExited = useEffectEvent(() => { if (hasTerminalWriteAccess()) onSessionExited(); }); @@ -1019,10 +1024,7 @@ function TerminalSessionViewport({ isDiffToggleShortcut(event, currentKeybindings, options) || // Match Sidebar's context, or a key it will reject skips the shell. isThreadNavigationShortcut(event, currentKeybindings, { - context: { - ...options.context, - previewOpen: readPreviewOpen(), - }, + context: { ...options.context, ...readThreadNavigationContext() }, }) ) { return false; From de963b105d0500d4ce2e157de0ce59cfda160ca1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 13 Nov 2026 02:54:45 +0000 Subject: [PATCH 6/6] refactor(web): terminal reuses Sidebar's thread shortcut context The terminal copied only terminalOpen and previewOpen, so a thread binding gated on isDesktop, isWeb or modelPickerOpen could be judged differently by the terminal and by Sidebar. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/ThreadTerminalDrawer.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index cf783fe959a41..de2dc970466d9 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -92,8 +92,7 @@ import { openTerminalLinkInPreview } from "./preview/openTerminalLinkInPreview"; import { useAtomCommand } from "../state/use-atom-command"; import { preventTerminalCloseShortcut } from "../lib/terminalCloseShortcut"; import { observeResize } from "../lib/observeResize"; -import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; -import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; +import { threadShortcutContext } from "./Sidebar.logic"; import { consumePendingTerminalFocus, focusTerminal, @@ -578,14 +577,7 @@ function TerminalSessionViewport({ const hasHandledExitRef = useRef(false); const selectionActionRequestIdRef = useRef(0); const readKeybindings = useEffectEvent(() => keybindings); - const readThreadNavigationContext = useEffectEvent(() => ({ - terminalOpen: selectThreadTerminalUiState( - useTerminalUiStateStore.getState().terminalUiStateByThreadKey, - threadRef, - ).terminalOpen, - previewOpen: - selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, threadRef) === "preview", - })); + const readThreadShortcutContext = useEffectEvent(() => threadShortcutContext(threadRef)); const handleSessionExited = useEffectEvent(() => { if (hasTerminalWriteAccess()) onSessionExited(); }); @@ -952,7 +944,7 @@ function TerminalSessionViewport({ isDiffToggleShortcut(event, currentKeybindings, options) || // Match Sidebar's context, or a key it will reject skips the shell. isThreadNavigationShortcut(event, currentKeybindings, { - context: { ...options.context, ...readThreadNavigationContext() }, + context: { ...readThreadShortcutContext(), terminalFocus: true }, }) ) { return false;