From 885d1cfbf80eb5528db8af3ca81210ddb228631f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 19 Oct 2026 22:13:25 -0700 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20=E2=8C=98T=20opens=20a=20browse?= =?UTF-8?q?r=20tab=20inside=20the=20browser=20pane=20(#57156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../browser-pane-restore.spec.ts | 74 ++++++++++++++++++- .../gui-extensions/src/browser/i18n/en.ts | 1 + .../src/browser/panel.fixture.tsx | 22 +++++- .../gui-extensions/src/browser/renderer.tsx | 28 ++++++- 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/packages/app/component-tests/browser-pane-restore.spec.ts b/packages/app/component-tests/browser-pane-restore.spec.ts index c7673f204fb..c48f7ae55b5 100644 --- a/packages/app/component-tests/browser-pane-restore.spec.ts +++ b/packages/app/component-tests/browser-pane-restore.spec.ts @@ -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) => 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") diff --git a/packages/gui-extensions/src/browser/i18n/en.ts b/packages/gui-extensions/src/browser/i18n/en.ts index 39967cc6413..14a5b26a3f6 100644 --- a/packages/gui-extensions/src/browser/i18n/en.ts +++ b/packages/gui-extensions/src/browser/i18n/en.ts @@ -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", diff --git a/packages/gui-extensions/src/browser/panel.fixture.tsx b/packages/gui-extensions/src/browser/panel.fixture.tsx index dc98fb5239c..a34311a5811 100644 --- a/packages/gui-extensions/src/browser/panel.fixture.tsx +++ b/packages/gui-extensions/src/browser/panel.fixture.tsx @@ -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) => 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() + const [shortcut, setShortcut] = createSignal>(() => "") 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) { setStore("closed", true)} review={() => setStore("reviews", (count) => count + 1)} + newSession={() => setStore("sessions", (count) => count + 1)} + shortcut={(next) => setShortcut(() => next)} />

Restored side strip