feat(desktop): open the browser with a hotkey instead of only through chat

The Browser tab could only be summoned by asking the agent or clicking a
link. Adds Cmd+Shift+L and a command-palette row.

L is the location-bar chord every browser shares; plain Cmd+L is
already the terminal's selection shortcut, hence the shift. Pressing it
re-fronts whatever page is loaded rather than resetting it, so it reads
as 'show me the browser' and not 'start over'.
This commit is contained in:
Brooklyn Nicholson
2026-08-18 07:46:37 -05:00
committed by brooklyn!
parent e02d1e41fc
commit a9a4a04070
11 changed files with 83 additions and 0 deletions
@@ -68,6 +68,7 @@ import {
import { $bindings, bindingsFor } from '@/store/keybinds'
import { $dismissedAutoProjectIds, filterVisibleProjects } from '@/store/layout'
import { openPetGenerate } from '@/store/pet-generate'
import { openBrowserTab } from '@/store/preview'
import { $projectTree, goToProject, openFolderAsProject, requestStartWorkSession } from '@/store/projects'
import { $connection } from '@/store/session'
import { runGatewayRestart } from '@/store/system-actions'
@@ -902,6 +903,14 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
keywords: ['reload', 'window', 'refresh', 'restart', 'ui', 'stuck'],
label: cc.reloadWindow,
run: () => window.location.reload()
},
{
action: 'view.showBrowser',
icon: codiconIcon('globe'),
id: 'cc-open-browser',
keywords: ['browser', 'web', 'url', 'address', 'open', 'navigate', 'internet', 'site'],
label: cc.openBrowser,
run: () => openBrowserTab()
}
]
},
@@ -35,6 +35,7 @@ import {
togglePanesFlipped,
toggleSidebarOpen
} from '@/store/layout'
import { openBrowserTab } from '@/store/preview'
import {
$newChatProfile,
cycleProfile,
@@ -235,6 +236,7 @@ export function useKeybinds(deps: KeybindRuntimeDeps): void {
'view.toggleReview': toggleReview,
'view.toggleStatusbar': toggleStatusbarVisible,
'view.showFiles': showFiles,
'view.showBrowser': openBrowserTab,
'view.toggleHud': () => toggleHud(hudTargetSessionId()),
'view.showTerminal': () => togglePaneVisible('terminal'),
// Create first so the pane's open-effect ensure sees a non-empty set and
+2
View File
@@ -240,6 +240,7 @@ export const ar = defineLocale({
'view.toggleRightSidebar': 'تبديل متصفح الملفات',
'view.toggleReview': 'تبديل لوحة المراجعة',
'view.showFiles': 'إظهار متصفح الملفات',
'view.showBrowser': 'فتح المتصفح',
'view.showTerminal': 'إظهار الطرفية',
'view.closeTab': 'إغلاق علامة التبويب',
'view.reopenTab': 'إعادة فتح علامة التبويب المغلقة',
@@ -1131,6 +1132,7 @@ export const ar = defineLocale({
gatewayStopped: 'البوابة متوقفة',
hermesActiveSessions: (version, count) => `Hermes ${version} لديه ${count} جلسة نشطة`,
restartGateway: 'إعادة تشغيل البوابة',
openBrowser: 'فتح المتصفح',
gatewayRestartFailed: 'فشل إعادة تشغيل البوابة.',
updateHermes: 'تحديث Hermes',
reloadWindow: 'إعادة تحميل النافذة',
+2
View File
@@ -273,6 +273,7 @@ export const en: Translations = {
'view.toggleReview': 'Toggle review pane',
'view.toggleStatusbar': 'Toggle status bar',
'view.showFiles': 'Show file browser',
'view.showBrowser': 'Open browser',
'view.toggleHud': 'Toggle HUD mode',
'hud.snapToPointer': 'Move HUD to pointer (global, while HUD is open)',
'view.showTerminal': 'Toggle terminal',
@@ -1413,6 +1414,7 @@ export const en: Translations = {
gatewayStopped: 'Messaging gateway stopped',
hermesActiveSessions: (version, count) => `Hermes ${version} · Active sessions ${count}`,
restartGateway: 'Restart gateway',
openBrowser: 'Open browser',
gatewayRestartFailed: 'Gateway restart failed.',
updateHermes: 'Update Hermes',
reloadWindow: 'Reload window',
+1
View File
@@ -1288,6 +1288,7 @@ export const ja = defineLocale({
gatewayStopped: 'メッセージングゲートウェイが停止中',
hermesActiveSessions: (version, count) => `Hermes ${version} · アクティブセッション ${count}`,
restartGateway: 'ゲートウェイを再起動',
openBrowser: 'ブラウザを開く',
gatewayRestartFailed: 'ゲートウェイの再起動に失敗しました。',
updateHermes: 'Hermes を更新',
reloadWindow: 'ウィンドウを再読み込み',
+1
View File
@@ -1239,6 +1239,7 @@ export interface Translations {
gatewayStopped: string
hermesActiveSessions: (version: string, count: number) => string
restartGateway: string
openBrowser: string
gatewayRestartFailed: string
updateHermes: string
reloadWindow: string
+1
View File
@@ -1250,6 +1250,7 @@ export const zhHant = defineLocale({
gatewayStopped: '訊息閘道已停止',
hermesActiveSessions: (version, count) => `Hermes ${version} · 活躍工作階段 ${count}`,
restartGateway: '重新啟動閘道',
openBrowser: '開啟瀏覽器',
gatewayRestartFailed: '閘道重新啟動失敗。',
updateHermes: '更新 Hermes',
reloadWindow: '重新載入視窗',
+2
View File
@@ -268,6 +268,7 @@ export const zh: Translations = {
'view.toggleReview': '切换审查面板',
'view.toggleStatusbar': '切换状态栏',
'view.showFiles': '显示文件浏览器',
'view.showBrowser': '打开浏览器',
'view.showTerminal': '显示终端',
'view.terminalSelection': '将终端选区发送到输入框',
'view.terminalCopy': '复制终端选区',
@@ -1602,6 +1603,7 @@ export const zh: Translations = {
gatewayStopped: '消息网关已停止',
hermesActiveSessions: (version, count) => `Hermes ${version} · 活跃会话 ${count}`,
restartGateway: '重启网关',
openBrowser: '打开浏览器',
gatewayRestartFailed: '网关重启失败。',
updateHermes: '更新 Hermes',
reloadWindow: '重新载入窗口',
+3
View File
@@ -125,6 +125,9 @@ export const KEYBIND_ACTIONS: readonly KeybindActionMeta[] = [
// ⌘G — "g" for git; the review pane is the source-control view.
{ id: 'view.toggleReview', category: 'view', defaults: ['mod+g'] },
{ id: 'view.showFiles', category: 'view', defaults: [] },
// ⌘⇧L — "L" for location, the address-bar chord every browser shares. Plain
// ⌘L is the terminal's selection shortcut, hence the shift.
{ id: 'view.showBrowser', category: 'view', defaults: ['mod+shift+l'] },
// ⌘⇧H — "h" for HUD. Enters/leaves the chrome-free floating chat: the app
// window steps aside and a composer + live reply float over whatever the
// user is working in. Ships bound because the whole point is leaving the app
@@ -0,0 +1,51 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { $previewTabs, closeRightRail, openBrowserTab, openPreview } from './preview'
beforeEach(() => {
closeRightRail()
})
describe('openBrowserTab', () => {
it('opens a blank browser when there is no browser tab yet', () => {
openBrowserTab()
const tabs = $previewTabs.get()
expect(tabs).toHaveLength(1)
// about:blank, so the pane lands on its empty state inviting an address
// rather than a white void.
expect(tabs[0]?.target.url).toBe('about:blank')
})
// The hotkey is "show me the browser", not "reset the browser" — pressing it
// while a page is loaded must not throw that page away.
it('re-fronts the existing page instead of blanking it', () => {
openPreview({ kind: 'url', label: 'Example', source: 'https://example.com', url: 'https://example.com' })
openBrowserTab()
const tabs = $previewTabs.get()
expect(tabs).toHaveLength(1)
expect(tabs[0]?.target.url).toBe('https://example.com')
})
it('reuses the one Browser tab rather than stacking duplicates', () => {
openBrowserTab()
openBrowserTab()
openBrowserTab()
expect($previewTabs.get()).toHaveLength(1)
})
// A file tab is keyed by identity; only the web surface is a singleton.
it('leaves other preview tabs alone', () => {
openPreview({ kind: 'file', label: 'notes.md', source: '/work/notes.md', url: 'file:///work/notes.md' })
openBrowserTab()
const tabs = $previewTabs.get()
expect(tabs).toHaveLength(2)
expect(tabs.map(tab => tab.target.url)).toContain('file:///work/notes.md')
})
})
+9
View File
@@ -226,6 +226,15 @@ export function openPreview(target: PreviewTarget, source: PreviewRecordSource =
selectRightRailTab(id)
}
/** Open the Browser tab — the surface, not a page. Keeps whatever it was last
* showing so the hotkey re-fronts your page instead of wiping it; a fresh tab
* lands on `about:blank`, where the pane's empty state invites an address. */
export function openBrowserTab() {
const existing = $previewTabs.get().find(tab => tab.id === BROWSER_TAB_ID)
openPreview(existing?.target ?? { kind: 'url', label: 'Browser', source: 'about:blank', url: 'about:blank' })
}
export function closeRightRailTab(tabId: string) {
const current = $previewTabs.get()
const index = current.findIndex(tab => tab.id === tabId)