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:
committed by
brooklyn!
parent
e02d1e41fc
commit
a9a4a04070
@@ -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
|
||||
|
||||
@@ -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: 'إعادة تحميل النافذة',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1288,6 +1288,7 @@ export const ja = defineLocale({
|
||||
gatewayStopped: 'メッセージングゲートウェイが停止中',
|
||||
hermesActiveSessions: (version, count) => `Hermes ${version} · アクティブセッション ${count}`,
|
||||
restartGateway: 'ゲートウェイを再起動',
|
||||
openBrowser: 'ブラウザを開く',
|
||||
gatewayRestartFailed: 'ゲートウェイの再起動に失敗しました。',
|
||||
updateHermes: 'Hermes を更新',
|
||||
reloadWindow: 'ウィンドウを再読み込み',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1250,6 +1250,7 @@ export const zhHant = defineLocale({
|
||||
gatewayStopped: '訊息閘道已停止',
|
||||
hermesActiveSessions: (version, count) => `Hermes ${version} · 活躍工作階段 ${count}`,
|
||||
restartGateway: '重新啟動閘道',
|
||||
openBrowser: '開啟瀏覽器',
|
||||
gatewayRestartFailed: '閘道重新啟動失敗。',
|
||||
updateHermes: '更新 Hermes',
|
||||
reloadWindow: '重新載入視窗',
|
||||
|
||||
@@ -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: '重新载入窗口',
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user