mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(desktop): ⌘T opens a browser tab inside the browser pane (#57156)
This commit is contained in:
committed by
GitHub
Unverified
parent
55bbfe6b1f
commit
885d1cfbf8
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user