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() + } +})