Compare commits

...

1 Commits

Author SHA1 Message Date
Luke Parker cc289a93f3 fix(app): suppress middle-click tab auxclick
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-29 01:02:10 +00:00
3 changed files with 65 additions and 2 deletions
@@ -1,5 +1,11 @@
import { describe, expect, test } from "bun:test"
import { canOpenTabRename, canStartTabDrag, forwardTabRef, isTabCloseTarget } from "./titlebar-tab-gesture"
import {
canOpenTabRename,
canStartTabDrag,
forwardTabRef,
isTabCloseTarget,
suppressNextMiddleAuxClick,
} from "./titlebar-tab-gesture"
describe("titlebar tab gestures", () => {
test("excludes close controls from tab gestures", () => {
@@ -20,6 +26,22 @@ describe("titlebar tab gestures", () => {
expect(received).toBe(element)
})
test("suppresses the auxclick following a middle-click close", () => {
const link = document.createElement("a")
let reachedLink = false
document.body.append(link)
link.addEventListener("auxclick", () => (reachedLink = true))
suppressNextMiddleAuxClick(document)
const event = new MouseEvent("auxclick", { button: 1, bubbles: true, cancelable: true })
const dispatched = link.dispatchEvent(event)
expect(dispatched).toBe(false)
expect(event.defaultPrevented).toBe(true)
expect(reachedLink).toBe(false)
link.remove()
})
test("does not reopen rename while a save is pending", () => {
expect(canOpenTabRename(false, false, false)).toBe(true)
expect(canOpenTabRename(false, false, true)).toBe(false)
@@ -8,6 +8,35 @@ export function canStartTabDrag(pointerType: string) {
return pointerType !== "touch"
}
let cancelMiddleAuxClick: (() => void) | undefined
export function suppressNextMiddleAuxClick(doc: Document = document) {
// Closing on mousedown can move another tab link under the pointer before auxclick fires.
// Catch that one follow-up event at the document level so Electron does not open it in a new window.
cancelMiddleAuxClick?.()
const controller = new AbortController()
const win = doc.defaultView ?? window
let timeout: number | undefined
const cleanup = () => {
if (timeout !== undefined) win.clearTimeout(timeout)
controller.abort()
if (cancelMiddleAuxClick === cleanup) cancelMiddleAuxClick = undefined
}
const suppress = (event: MouseEvent) => {
if (event.button !== 1) return
event.preventDefault()
event.stopPropagation()
cleanup()
}
timeout = win.setTimeout(cleanup, 1000)
cancelMiddleAuxClick = cleanup
doc.addEventListener("auxclick", suppress, { capture: true, signal: controller.signal })
}
export function forwardTabRef(ref: Ref<HTMLDivElement> | undefined, element: HTMLDivElement) {
if (typeof ref === "function") ref(element)
}
@@ -10,7 +10,7 @@ import { projectForSession } from "@/pages/layout/helpers"
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
import { showToast } from "@/utils/toast"
import type { Session } from "@opencode-ai/sdk/v2"
import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture"
import { canOpenTabRename, forwardTabRef, suppressNextMiddleAuxClick } from "./titlebar-tab-gesture"
import "./titlebar-tab-nav.css"
export function TabNavItem(props: {
@@ -41,6 +41,7 @@ export function TabNavItem(props: {
const closeTab = (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
if (event.button === 1) suppressNextMiddleAuxClick()
props.onClose()
}
const global = useGlobal()
@@ -187,6 +188,11 @@ export function TabNavItem(props: {
if (event.button !== 1) return
closeTab(event)
}}
onAuxClick={(event) => {
if (event.button !== 1) return
event.preventDefault()
event.stopPropagation()
}}
>
<Show when={props.session()}>
{(session) => {
@@ -289,6 +295,7 @@ export function DraftTabItem(props: {
const closeTab = (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
if (event.button === 1) suppressNextMiddleAuxClick()
props.onClose()
}
return (
@@ -305,6 +312,11 @@ export function DraftTabItem(props: {
if (event.button !== 1) return
closeTab(event)
}}
onAuxClick={(event) => {
if (event.button !== 1) return
event.preventDefault()
event.stopPropagation()
}}
>
<a
data-slot="tab-link"