feat(tui): close tabs with middle click (#43497)

Co-authored-by: kitlangton <7587245+kitlangton@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
This commit is contained in:
opencode-agent[bot]
2026-08-19 16:40:21 -04:00
committed by GitHub
Unverified
parent d4a0f163a9
commit 8fb534d03e
2 changed files with 56 additions and 0 deletions
@@ -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)
@@ -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<string | undefined> = []
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(
() => (
<TestTuiContexts>
<ConfigProvider config={createTuiResolvedConfig({ tabs: { enabled: true } })}>
<ThemeProvider mode="dark" source={emptyThemeSource}>
<SessionTabs controller={controller} animations={false} />
</ThemeProvider>
</ConfigProvider>
</TestTuiContexts>
),
{ 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()
}
})