From 8fb534d03e34321633a3eda3b325c10bc82c572e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:40:21 -0400 Subject: [PATCH] feat(tui): close tabs with middle click (#43497) Co-authored-by: kitlangton <7587245+kitlangton@users.noreply.github.com> Co-authored-by: Kit Langton --- packages/tui/src/component/session-tabs.tsx | 17 ++++++++ .../component/session-tabs-mouse.test.tsx | 39 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/packages/tui/src/component/session-tabs.tsx b/packages/tui/src/component/session-tabs.tsx index 7f796a455cd..53b2fec0018 100644 --- a/packages/tui/src/component/session-tabs.tsx +++ b/packages/tui/src/component/session-tabs.tsx @@ -46,6 +46,7 @@ const ADD_TAB_WIDTH = 3 const MARQUEE_DELAY = 600 const MARQUEE_INTERVAL = 80 const CONTEXT_MENU_WIDTH = 16 +const MIDDLE_MOUSE_BUTTON = 1 const RIGHT_MOUSE_BUTTON = 2 type TabContextMenuState = { @@ -569,6 +570,14 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat onMouseOver={() => marquee.enter(tab.sessionID, title(), hoveredTitleWidth())} onMouseOut={() => marquee.leave(tab.sessionID)} onMouseDown={(event) => { + if (event.button === MIDDLE_MOUSE_BUTTON) { + didDrag = false + setDragging(undefined) + tabs.close(tab.sessionID) + event.preventDefault() + event.stopPropagation() + return + } if (event.button === RIGHT_MOUSE_BUTTON) { didDrag = false setDragging(undefined) @@ -1108,6 +1117,14 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim onMouseOver={() => marquee.enter(tab.sessionID, title(), hoveredTitleWidth())} onMouseOut={() => marquee.leave(tab.sessionID)} onMouseDown={(event) => { + if (event.button === MIDDLE_MOUSE_BUTTON) { + didDrag = false + setDragging(undefined) + tabs.close(tab === NEW_SESSION_TAB ? undefined : tab.sessionID) + event.preventDefault() + event.stopPropagation() + return + } if (event.button === RIGHT_MOUSE_BUTTON) { didDrag = false setDragging(undefined) diff --git a/packages/tui/test/component/session-tabs-mouse.test.tsx b/packages/tui/test/component/session-tabs-mouse.test.tsx index 4c275e23a4d..cc425207cf2 100644 --- a/packages/tui/test/component/session-tabs-mouse.test.tsx +++ b/packages/tui/test/component/session-tabs-mouse.test.tsx @@ -1,5 +1,6 @@ /** @jsxImportSource @opentui/solid */ import { testRender } from "@opentui/solid" +import { MouseButton } from "@opentui/core" import { expect, test } from "bun:test" import { createSignal } from "solid-js" import { ConfigProvider } from "../../src/config" @@ -60,3 +61,41 @@ test("releasing a transcript selection over tab controls does not activate them" app.renderer.destroy() } }) + +test("middle-click closes a session tab without selecting it", async () => { + const [active, setActive] = createSignal("first") + const closed: Array = [] + const controller = { + tabs: () => [ + { sessionID: "first", title: "First" }, + { sessionID: "second", title: "Second" }, + ], + current: active, + select: setActive, + close: (sessionID?: string) => closed.push(sessionID), + move() {}, + status: () => EMPTY_SESSION_TAB_STATUS, + } satisfies SessionTabsController + const app = await testRender( + () => ( + + + + + + + + ), + { width: 60, height: 8 }, + ) + + try { + app.renderer.start() + await app.waitForFrame((frame) => frame.includes("Second")) + await app.mockMouse.click(40, 0, MouseButton.MIDDLE) + expect(closed).toEqual(["second"]) + expect(active()).toBe("first") + } finally { + app.renderer.destroy() + } +})