Skip to content

fix(app): scope tab shortcuts to the focused panel and calm the side tab strip - #57935

Merged
kitlangton merged 3 commits into
betafrom
side-panel-focus
Oct 21, 2026
Merged

kitlangton merged 3 commits into
betafrom
side-panel-focus

Conversation

@kitlangton

Copy link
Copy Markdown
Contributor

From Kit's Scrawl of the side panel ("something crazy is happening up here", "if I'm focused here and I hit ⌘T, it takes me to a new session").

The "crazy" tab strip

The page in his browser tab was stuck in a redirect loop: a misrouted dev server answered /tasks/controls.html with an index page that runs location.replace("controls.html"), which loads the same address again. Every time a navigation committed, Chromium titled the untitled document after its URL until <title> parsed. So the tab label switched between knobs and 127.0.0.1:5291/tasks/c… several times a second. Because the tab sizes to its label, the tab and the + jumped sideways each time.

chromium.ts now keeps the shown title through navigations on the same site until the new document names itself or stops loading. A new site still shows its URL until it has a title, the same way its icon resets.

Headless repro (the real createBrowserPage in Electron, a fictional Field notes page that replaces itself every 40 ms):

label changes in 3 s labels
before 34 Field notes ↔ 127.0.0.1:…/field/notes.html?…
after 2 New tab → URL (first commit) → Field notes, then steady

Leaving for another page still shows that page's title (Quiet page) in both runs.

⌘T / ⌘W follow focus

  • Why ⌘T opened a session: while a page loads, the pane shows its own loading view where the native page goes. Clicking there focused the side panel around #browser-panel, not the pane itself, so the browser's scoped ⌘T didn't match and the shell's new session ran. #browser-panel is now focusable (tabIndex=-1), so a click on the pane's surface keeps its shortcuts.
  • Side panel owns ⌘T and ⌘W: with focus anywhere in the side panel, ⌘T opens the side panel's new tab view in the focused pane and ⌘W closes that view, then the selected tab. Outside the side panel both chords go back to session tabs. Before this, ⌘W closed the side tab from anywhere while the panel was open. A panel that binds the key in its own scope still answers first: the browser's ⌘T opens a browser tab and the terminal's ⌘W closes a terminal. This is decided per key (extensionClaims), so a scope that binds other keys no longer blocks the host.
  • Native page focus: the page's focus is reported over the pane IPC (focused), and the pane turns it into a focusin. The app's single side-focus tracker, now owned by the region, sees it like any other focus.
  • New tab view state moved from PaneTabs into the region (openNewTab / closeNewTab / newTab), so the shortcut and the + button share one owner.

Focus indicator

While the keyboard is in the main column, the side panel's selected tab label dims to the strip's muted colour; its fill still marks the selection. With focus in the side panel (including a page) it's full contrast. 150 ms colour fade, no rings.

New tab page shortcuts

  • Browser is now bound to ⌃⌘B (Ctrl+Alt+B elsewhere), matching Review ⌃⌘R and Open file ⌃⌘P. Before, it showed ⌘T only while a browser pane was on screen. I found no conflicts in the app, the desktop menu, or the page's forwarded keys.
  • Open file (⌃⌘P) vs ⌘P: I kept them separate. ⌘P is the palette's quick open: type, pick a file, done. ⌃⌘P opens a side-panel tab with the project tree and a filter, which you can keep open to browse. Making them one action would lose one of the two. The overlap is really just the name, and renaming the launcher (e.g. "Browse files") would be a separate copy change.

Tests

  • New component stories in browser-pane-restore.spec.ts: ⌘T/⌘W follow focus (main column → session; side panel surface → new tab view, then close; page focus marks the panel focused), and ⌘T after a click on the pane's loading surface opens a browser tab rather than a session.
  • bun typecheck in app and gui-extensions, targeted unit tests, lint:changed origin/beta.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach looks sound: the new tab view state moves into the region, the per-key extensionClaims check means a panel's own binding wins without blocking the host, and the same-site title handling in chromium.ts is reasonable. Two component stories fail, though, so this needs another pass before it's ready.

How I checked: I couldn't exercise the real desktop behaviour (Electron, native page focus) here, so I ran the browser-pane-restore.spec.ts stories in headless Chromium. All 11 pass on the base. On this branch, 11 of 13 pass:

  • The existing story "opens a browser tab with ⌘T inside the pane and a session everywhere else" now fails at line 345. The + menu shows Ctrl+Alt+B where none is expected, because of the new always-on browser.open keybind.
  • The new "⌘T and ⌘W follow focus" story fails every time (3 of 3 runs) at line 423: #review-panel never gets data-focused after editor.click().

Typecheck passes in app and gui-extensions, and connection.test.ts passes.

Public API: unchanged. Two internal additions: a focused pane IPC event and a default ⌃⌘B / Ctrl+Alt+B binding for browser.open, which users can rebind.

See the inline notes on the failing stories and on the fixture copying the session commands.

icon: "globe",
// 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,
keybind: "browser.open",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The + menu now always shows this binding, so the existing story "opens a browser tab with ⌘T inside the pane and a session everywhere else" in browser-pane-restore.spec.ts fails at line 345. It still expects add-shortcut to be none while a file tab is shown, and it now gets Ctrl+Alt+B. It passes on the base. Please update that story to match the new behaviour.

})

// As the session's commands: ⌘T and ⌘W inside the side region, unless the focused panel binds the key there.
const sideKey = (event: KeyboardEvent) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This copies the sideKey / file.close / file.tab.new logic from use-session-commands.tsx into the fixture, so the stories test the copy, not the commands the app ships. If the real when or closeTab changes, these stories will still pass. Could the fixture reuse the real command definitions, or could the stories be cut down to the region and focus behaviour they actually exercise?

// The page's focus, which reaches the app only as the pane's report, counts as the side panel's.
await outside.focus()
await expect(panel).not.toHaveAttribute("data-focused")
await editor.click()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This step fails for me every time in headless Chromium (3 of 3 runs): after editor.click(), #review-panel never gets data-focused. My guess is that the parent document's own focusin on the <iframe> (which sits outside #review-panel in the fixture) resets the tracker around the fixture's synthetic focused report. In Electron there's no such parent focusin, so this may only affect the fixture, but the story needs to pass reliably.

@kitlangton
kitlangton merged commit 84f73b6 into beta Oct 21, 2026
9 of 17 checks passed
@kitlangton
kitlangton deleted the side-panel-focus branch October 21, 2026 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant