fix(desktop): ⌘T opens a browser tab inside the browser pane (#57156)

This commit is contained in:
Kit Langton
2026-10-19 22:13:25 -07:00
committed by GitHub
Unverified
parent 55bbfe6b1f
commit 885d1cfbf8
4 changed files with 118 additions and 7 deletions
@@ -1,6 +1,6 @@
import { expect, sourceURL, story } from "../../storybook/playwright/story"
import type { ParentProps } from "solid-js"
import type { Command } from "@opencode/gui-extensions/sdk"
import type { Accessor, ParentProps } from "solid-js"
import type { Command, MenuItem } from "@opencode/gui-extensions/sdk"
import type { Item } from "../src/runtime/extension/host"
const source = (path: string) => sourceURL(new URL(path, import.meta.url))
@@ -81,9 +81,20 @@ story.beforeEach(async ({ mount, page }) => {
})
},
}),
Commands: (props: { close: () => void; review: () => void }) => {
Commands: (props: {
close: () => void
review: () => void
newSession: () => void
shortcut: (shortcut: Accessor<string>) => void
}) => {
const command = commands.useCommand()
const extensions = host.useExtensionHost()
// What the side strip's + shows beside the browser item, through the host's own formatter.
props.shortcut(() => {
const item = extensions.list(sdk.MenuItem).find((value: MenuItem) => value.menu === "session.panel")
return item?.menu === "session.panel" && item.keybind ? command.keybind(item.keybind) : ""
})
command.register("fixture-session", () => [
{
id: "tab.close",
@@ -91,6 +102,12 @@ story.beforeEach(async ({ mount, page }) => {
keybind: "mod+w",
onSelect: props.close,
},
{
id: "tab.new",
title: "New session",
keybind: "mod+t,mod+n",
onSelect: props.newSession,
},
{
id: "review.toggle",
title: "Toggle review",
@@ -220,6 +237,57 @@ story("keeps in-page shortcuts like ⌘K in the page while forwarding ⌘⇧R an
await expect(root.getByTestId("session-state")).toHaveText("open")
})
story("opens a browser tab with ⌘T inside the pane and a session everywhere else", async ({ page }) => {
const root = page.getByTestId("browser-region-fixture")
const mac = await page.evaluate(() => /Mac|iPhone|iPad|iPod/.test(navigator.platform))
const mod = mac ? "Meta" : "Control"
const outside = root.getByRole("button", { name: "Beta", exact: true })
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
const editor = page.frameLocator('iframe[title="Browser page"]').getByRole("textbox", { name: "Editor page input" })
const opens = root.getByTestId("browser-opens")
const sessions = root.getByTestId("new-sessions")
await expect(root.getByText("Registrations: 1", { exact: true })).toBeVisible()
// Alpha shows a file tab, so the + advertises no browser shortcut.
await expect(root.getByTestId("add-shortcut")).toHaveText("none")
await outside.click()
await expect(root.getByText("Registrations: 2", { exact: true })).toBeVisible()
await root.getByRole("button", { name: "First inventory", exact: true }).click()
await expect(root.getByRole("tab", { name: "Preview", exact: true })).toHaveAttribute("aria-selected", "true")
await expect(root.getByTestId("add-shortcut")).toHaveText(mac ? "⌘T" : "Ctrl+T")
// Outside the pane the shell keeps ⌘T.
await outside.focus()
await outside.press(`${mod}+t`)
await expect(sessions).toHaveText("1")
await expect(opens).toHaveText("0")
// Inside the pane it opens a browser tab instead.
await address.focus()
await address.press(`${mod}+t`)
await expect(opens).toHaveText("1")
// Dvorak's T sits on the physical K key: the shortcut follows the character, not the key position.
await address.evaluate((element, meta) => {
element.dispatchEvent(
new KeyboardEvent("keydown", {
key: "t",
code: "KeyK",
metaKey: meta,
ctrlKey: !meta,
bubbles: true,
cancelable: true,
}),
)
}, mac)
await expect(opens).toHaveText("2")
// A focused page hands the reserved chord to the pane before it sees it.
await editor.focus()
await editor.press(`${mod}+t`)
await expect(opens).toHaveText("3")
await expect(sessions).toHaveText("1")
})
story("keeps a restored browser tab selected and undrawn until the desktop's first inventory", async ({ page }) => {
const root = page.getByTestId("browser-region-fixture")
const tabs = root.getByRole("tab")
@@ -4,6 +4,7 @@ export default {
"command.inspect": "Select an element in the browser page",
"command.address": "Focus browser address",
"command.close": "Close browser tab",
"command.newTab": "New browser tab",
"tab.title": "Browser",
"tab.new": "New tab",
"address.label": "Browser address",
@@ -603,7 +603,13 @@ type RegionHost = {
/** Real shell command providers for shortcut cases. */
Shell: ParentComponent
/** Shell fallback and the real extension command adapter. */
Commands: Component<{ close: () => void; review: () => void }>
Commands: Component<{
close: () => void
review: () => void
newSession: () => void
/** The shortcut the browser's + menu item shows, as the host formats it; empty when it shows none. */
shortcut: (shortcut: Accessor<string>) => void
}>
/** The real host wrapper that renders an extension in its context. */
Contribution: Component<{ extension: string; children: () => JSX.Element }>
}
@@ -619,6 +625,10 @@ type RegionFixtureState = {
closed: boolean
/** How many times the review side panel toggled from a shortcut. */
reviews: number
/** How many times the shell's new session shortcut ran. */
sessions: number
/** How many browser tabs the pane asked main to open. */
opens: number
/** The page main reports, blank or loaded. */
blank: boolean
/** Native browser tab close requests, by tab id. */
@@ -651,6 +661,8 @@ export function mountBrowserRegion(input: RegionHost) {
away: false,
closed: false,
reviews: 0,
sessions: 0,
opens: 0,
blank: false,
closes: [],
inspects: [],
@@ -658,6 +670,7 @@ export function mountBrowserRegion(input: RegionHost) {
// The file tree's state, Changes or All files, as the file extension stores it.
const [tree, setTree] = createSignal<object>()
const [shortcut, setShortcut] = createSignal<Accessor<string>>(() => "")
const strip = (session: string) => store.strips[session] ?? { all: [] }
const setAll = (session: string, all: string[]) => setStore("strips", session, "all", all)
@@ -814,6 +827,8 @@ export function mountBrowserRegion(input: RegionHost) {
},
load: async () => undefined,
command: async (value) => {
if (value.command.type === "tabs.open") setStore("opens", (count) => count + 1)
if (value.command.type !== "tabs.close") return
const tabID = value.command.tabID
@@ -1014,6 +1029,8 @@ export function mountBrowserRegion(input: RegionHost) {
<input.Commands
close={() => setStore("closed", true)}
review={() => setStore("reviews", (count) => count + 1)}
newSession={() => setStore("sessions", (count) => count + 1)}
shortcut={(next) => setShortcut(() => next)}
/>
<h1 style={{ "font-size": "24px", "margin-bottom": "16px" }}>Restored side strip</h1>
<nav style={{ display: "flex", gap: "12px", margin: "16px 0" }}>
@@ -1048,6 +1065,9 @@ export function mountBrowserRegion(input: RegionHost) {
<p data-testid="tree">{JSON.stringify(tree())}</p>
<p data-testid="session-state">{store.closed ? "closed" : "open"}</p>
<p data-testid="review-toggles">{store.reviews}</p>
<p data-testid="new-sessions">{store.sessions}</p>
<p data-testid="browser-opens">{store.opens}</p>
<p data-testid="add-shortcut">{shortcut()() || "none"}</p>
<p data-testid="browser-closes">{JSON.stringify(store.closes)}</p>
<p data-testid="browser-inspects">{JSON.stringify(store.inspects)}</p>
<Ready>
@@ -11,7 +11,8 @@ import tabStyles from "./tabs.css?inline"
const setup: Setup<typeof definition> = (ctx) => {
const sessions = ctx.sessions
const [model, setModel] = createSignal<Model>()
// Settings > Shortcuts lists the command on every platform; it stays disabled until the pane can open.
// Settings > Shortcuts lists the command on every platform; it stays disabled until the pane can open. Unbound by
// default: the sidebar owns mod+shift+b, and mod+t opens a tab from inside the pane.
ctx.add(Command, (): Command | undefined => {
const view = sessions.current()
@@ -22,7 +23,6 @@ const setup: Setup<typeof definition> = (ctx) => {
id: "open",
title: ctx.t("command.open"),
group: ctx.t("command.category.view"),
bind: "mod+shift+b",
enabled: !!value?.available(view),
run: () => value?.open(view),
}
@@ -107,6 +107,27 @@ const setup: Setup<typeof definition> = (ctx) => {
}
})
// As in a browser, mod+t opens a tab inside the pane; everywhere else it keeps the app's new session.
ctx.add(Command, (): Command | undefined => {
const view = sessions.current()
const value = model()
const pane = value?.pane()
if (!view || !value || !pane) return undefined
return {
id: "newTab",
title: ctx.t("command.newTab"),
group: ctx.t("command.category.view"),
bind: "mod+t",
scope: "#browser-panel",
hidden: true,
editable: true,
enabled: pane.visible() && value.available(view),
run: () => value.open(view),
}
})
// Chromium's element picker chord. While the page has focus, the page claims it itself; here it covers focus elsewhere
// in the pane. Ctrl+Shift+C copies in the terminal outside the pane.
ctx.add(Command, (): Command | undefined => {
@@ -139,7 +160,8 @@ const setup: Setup<typeof definition> = (ctx) => {
id: "open",
title: ctx.t("tab.new"),
icon: "globe",
keybind: "browser.open",
// mod+t opens a browser tab only inside the pane, so advertise it only while the pane is on screen.
keybind: value.pane()?.visible() ? "browser.newTab" : undefined,
order: 40,
primary: true,
run: () => value.open(view),