diff --git a/packages/tui/src/component/session-tabs.tsx b/packages/tui/src/component/session-tabs.tsx
index 10a19a8842c..43c887e6a2f 100644
--- a/packages/tui/src/component/session-tabs.tsx
+++ b/packages/tui/src/component/session-tabs.tsx
@@ -345,6 +345,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
let rail: { screenX: number; screenY: number } | undefined
let scroll: ScrollBoxRenderable | undefined
let didDrag = false
+ let addPressed = false
// A captured drag ends with a synthetic up on its drop target; do not turn that into a click.
let suppressClick = false
@@ -760,7 +761,8 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
onMouseDown={(event: MouseEvent) => {
didDrag = false
setDragging(undefined)
- if (event.button !== RIGHT_MOUSE_BUTTON) return
+ addPressed = event.button !== RIGHT_MOUSE_BUTTON
+ if (addPressed) return
if (!rail) return
setContextMenu({ x: event.x, y: event.y })
event.preventDefault()
@@ -769,8 +771,11 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
onMouseUp={(event: MouseEvent) => {
if (event.button === RIGHT_MOUSE_BUTTON) return
if (suppressClick) return
+ if (!addPressed) return
+ addPressed = false
if (!newTab()) tabs.add?.()
}}
+ onMouseDragEnd={() => (addPressed = false)}
>
()
let strip: { screenX: number; screenY: number } | undefined
let didDrag = false
+ let addPressed = false
// A captured drag ends with a synthetic up on its drop target; do not turn that into a click.
let suppressClick = false
const hueStep = () => (mode() === "light" ? 800 : 200)
@@ -1203,7 +1209,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
onMouseDown={(event) => {
didDrag = false
setDragging(undefined)
- if (event.button !== RIGHT_MOUSE_BUTTON) return
+ addPressed = event.button !== RIGHT_MOUSE_BUTTON
+ if (addPressed) return
setContextMenu({ x: event.x, y: event.y })
event.preventDefault()
event.stopPropagation()
@@ -1211,8 +1218,11 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
onMouseUp={(event) => {
if (event.button === RIGHT_MOUSE_BUTTON) return
if (suppressClick) return
+ if (!addPressed) return
+ addPressed = false
tabs.add?.()
}}
+ onMouseDragEnd={() => (addPressed = false)}
>
{" + "}
diff --git a/packages/tui/test/component/session-tabs-mouse.test.tsx b/packages/tui/test/component/session-tabs-mouse.test.tsx
new file mode 100644
index 00000000000..4c275e23a4d
--- /dev/null
+++ b/packages/tui/test/component/session-tabs-mouse.test.tsx
@@ -0,0 +1,62 @@
+/** @jsxImportSource @opentui/solid */
+import { testRender } from "@opentui/solid"
+import { expect, test } from "bun:test"
+import { createSignal } from "solid-js"
+import { ConfigProvider } from "../../src/config"
+import { EMPTY_SESSION_TAB_STATUS, SessionTabs, type SessionTabsController } from "../../src/component/session-tabs"
+import { ThemeProvider } from "../../src/context/theme"
+import { emptyThemeSource } from "../fixture/fixture"
+import { TestTuiContexts } from "../fixture/tui-environment"
+import { createTuiResolvedConfig } from "../fixture/tui-runtime"
+
+test("releasing a transcript selection over tab controls does not activate them", async () => {
+ const [active, setActive] = createSignal("first")
+ const [added, setAdded] = createSignal(0)
+ const controller = {
+ tabs: () => [
+ { sessionID: "first", title: "First" },
+ { sessionID: "second", title: "Second" },
+ ],
+ current: active,
+ select: setActive,
+ close() {},
+ move() {},
+ add: () => setAdded((value) => value + 1),
+ status: () => EMPTY_SESSION_TAB_STATUS,
+ } satisfies SessionTabsController
+ const app = await testRender(
+ () => (
+
+
+
+
+
+ selectable transcript text
+
+
+
+
+ ),
+ { width: 60, height: 3 },
+ )
+
+ try {
+ app.renderer.start()
+ await app.waitForFrame((frame) => frame.includes("Second"))
+ await app.mockMouse.pressDown(5, 1)
+ await app.mockMouse.release(40, 0)
+ expect(active()).toBe("first")
+
+ await app.mockMouse.click(40, 0)
+ expect(active()).toBe("second")
+
+ await app.mockMouse.pressDown(5, 1)
+ await app.mockMouse.release(58, 0)
+ expect(added()).toBe(0)
+
+ await app.mockMouse.click(58, 0)
+ expect(added()).toBe(1)
+ } finally {
+ app.renderer.destroy()
+ }
+})