From d132f1adf55ae8068bd5b29a6888c5c19042cbda Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 10 Nov 2026 02:27:14 +0800 Subject: [PATCH] fix(terminal): keep session and search controls visible (#2379) In a narrow Terminal tool window the search field's options and match count pushed the navigation and close buttons out of view (24px overflow), and session tab controls were misaligned. Search now lays out on a control-width grid (match count and navigation on their own row when narrow) and session controls align with the tool window header. Verified on macOS with tabs, new sessions, splits, search, light/dark and narrow panes. --- e2e/terminal-ui.spec.ts | 61 +++++++++++++++++++ src/features/terminal/FindBar.tsx | 5 +- src/features/terminal/Terminal.module.css | 15 +++++ src/features/terminal/TerminalView.tsx | 2 +- src/ui/expui/general/chevronDownLarge.svg | 4 ++ .../expui/general/chevronDownLarge_dark.svg | 4 ++ src/ui/icons.tsx | 1 + 7 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 e2e/terminal-ui.spec.ts create mode 100644 src/ui/expui/general/chevronDownLarge.svg create mode 100644 src/ui/expui/general/chevronDownLarge_dark.svg diff --git a/e2e/terminal-ui.spec.ts b/e2e/terminal-ui.spec.ts new file mode 100644 index 000000000..3f2d0d04f --- /dev/null +++ b/e2e/terminal-ui.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from "./fixtures"; + +for (const { width, split } of [{ width: 220 }, { width: 280 }, { width: 520 }, { width: 520, split: true }]) { + test(`terminal search fits a ${width}px ${split ? "split" : "panel"} and its buttons work from the keyboard`, async ({ + workbench: page, + }) => { + await page.evaluate(async (width) => { + const path = "/src/workbench/toolwindows/store.ts"; + const layout = await import(path); + layout.moveToolWindow("terminal", "right"); + layout.setToolWindowSize("right", width); + layout.showToolWindow("terminal", true); + }, width); + const terminal = page.getByRole("region", { name: "Terminal", exact: true }); + await expect(terminal.locator(".xterm-helper-textarea")).toBeFocused(); + if (split) { + await page.evaluate(async () => { + const path = "/src/platform/actions.ts"; + const { executeAction } = await import(path); + await executeAction("terminal.splitRight"); + }); + await expect(terminal.getByRole("tablist", { name: "Terminal sessions" })).toHaveCount(2); + await expect(terminal.locator(".xterm-helper-textarea:focus")).toHaveCount(1); + } + await page.evaluate(async () => { + const path = "/src/features/terminal/runtime.ts"; + const { activeSession } = await import(path); + await new Promise((resolve) => activeSession().term.write("\r\nalpha ALPHA alpha\r\n", resolve)); + }); + await page.keyboard.press("Meta+f"); + const search = terminal.getByRole("search", { name: "Find in terminal" }); + const input = search.getByRole("textbox", { name: "Find in terminal" }); + await input.fill("alpha"); + await expect(search.locator("[aria-live=polite]")).toHaveText(/\d+\/3/); + if (process.env.OPENIDEA_TERMINAL_SHOTS) + await terminal.screenshot({ + path: `${process.env.OPENIDEA_TERMINAL_SHOTS}/terminal-${width}${split ? "-split" : ""}.png`, + }); + + const bounds = (await search.boundingBox())!; + for (const control of await search.locator("input, button, [aria-live]").all()) { + const rect = (await control.boundingBox())!; + expect(rect.x).toBeGreaterThanOrEqual(bounds.x); + expect(rect.x + rect.width).toBeLessThanOrEqual(bounds.x + bounds.width + 1); + } + expect((await input.boundingBox())!.width).toBeGreaterThan(40); + + const matchCase = search.getByRole("button", { name: "Match Case", exact: true }); + await matchCase.focus(); + await page.keyboard.press("Enter"); + await expect(matchCase).toHaveAttribute("aria-pressed", "true"); + await expect(search.locator("[aria-live=polite]")).toHaveText(/\d+\/2/); + await input.fill("missing"); + await expect(search.locator("[aria-live=polite]")).toHaveText("0 results"); + await expect(search.getByRole("button", { name: "Next Occurrence", exact: true })).toBeDisabled(); + await search.getByRole("button", { name: "Close", exact: true }).focus(); + await page.keyboard.press("Enter"); + await expect(search).toBeHidden(); + await expect(terminal.locator(".xterm-helper-textarea:focus")).toHaveCount(1); + }); +} diff --git a/src/features/terminal/FindBar.tsx b/src/features/terminal/FindBar.tsx index fc22a2445..431121fe4 100644 --- a/src/features/terminal/FindBar.tsx +++ b/src/features/terminal/FindBar.tsx @@ -87,6 +87,7 @@ export function TerminalFindBar({ sessionKey }: { sessionKey: string | null }) { e.stopPropagation(); closeTerminalFind(); } else if (e.key === "Enter") { + if ((e.target as Element).closest("button")) return; e.preventDefault(); find(e.shiftKey ? "previous" : "next"); } else if (e.key === "F3" || (hasPrimaryModifier(e) && e.key.toLowerCase() === "g")) { @@ -155,7 +156,7 @@ export function TerminalFindBar({ sessionKey }: { sessionKey: string | null }) { title="Previous Occurrence" shortcut={formatBinding("Shift+Enter")} size="small" - disabled={!query} + disabled={!query || !result?.count} onClick={() => find("previous")} /> find("next")} /> diff --git a/src/features/terminal/Terminal.module.css b/src/features/terminal/Terminal.module.css index acc77a358..369c79c09 100644 --- a/src/features/terminal/Terminal.module.css +++ b/src/features/terminal/Terminal.module.css @@ -27,6 +27,7 @@ /* A tab group: its header (while split) over its tabs' terminals. */ .group { + container: terminal-group / inline-size; flex: 1; display: flex; flex-direction: column; @@ -156,3 +157,17 @@ .findSpacer { flex: 1; } + +@container terminal-group (max-width: 360px) { + .find { + display: grid; + grid-template-columns: minmax(0, 1fr) repeat(3, var(--h-control-small)); + } + .findField { + grid-column: 1 / -1; + min-width: 0; + } + .findSpacer { + display: none; + } +} diff --git a/src/features/terminal/TerminalView.tsx b/src/features/terminal/TerminalView.tsx index efc1964cb..8d91a4011 100644 --- a/src/features/terminal/TerminalView.tsx +++ b/src/features/terminal/TerminalView.tsx @@ -459,7 +459,7 @@ function GroupTabs({ group }: { group: string }) { /> + + + diff --git a/src/ui/expui/general/chevronDownLarge_dark.svg b/src/ui/expui/general/chevronDownLarge_dark.svg new file mode 100644 index 000000000..70a43560d --- /dev/null +++ b/src/ui/expui/general/chevronDownLarge_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/ui/icons.tsx b/src/ui/icons.tsx index 4ec3e57ca..3d1d95336 100644 --- a/src/ui/icons.tsx +++ b/src/ui/icons.tsx @@ -224,6 +224,7 @@ const ICONS = { // General UI "chevron-right": "general/chevronRight", "chevron-down": "general/chevronDown", + "toolbar-expand": "general/chevronDownLarge", "chevron-up": "general/chevronUp", "chevron-left": "general/chevronLeft", close: "general/close",