feat(desktop): mount BrowserSurface in right panel with TabBar toggle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-29 12:06:00 +08:00
parent 0ac56a6b11
commit 3077f1569d
5 changed files with 116 additions and 12 deletions

View File

@ -64,6 +64,8 @@ vi.mock('../../i18n', () => ({
'tabs.openTerminal': 'Open Terminal',
'tabs.showWorkspace': 'Show Workspace',
'tabs.hideWorkspace': 'Hide Workspace',
'tabs.showBrowser': 'Show Browser',
'tabs.hideBrowser': 'Hide Browser',
'openProject.openProject': 'Open project',
'openProject.openIn': 'Open in {target}',
'openProject.openFailed': 'Could not open project',
@ -137,6 +139,7 @@ describe('TabBar', () => {
const { useSessionStore } = await import('../../stores/sessionStore')
const { useWorkspacePanelStore } = await import('../../stores/workspacePanelStore')
const { useTerminalPanelStore } = await import('../../stores/terminalPanelStore')
const { useBrowserPanelStore } = await import('../../stores/browserPanelStore')
useTabStore.setState({ tabs: [], activeTabId: null })
useChatStore.setState({
@ -152,6 +155,7 @@ describe('TabBar', () => {
} as Partial<ReturnType<typeof useSessionStore.getState>>)
useWorkspacePanelStore.setState(useWorkspacePanelStore.getInitialState(), true)
useTerminalPanelStore.setState(useTerminalPanelStore.getInitialState(), true)
useBrowserPanelStore.setState(useBrowserPanelStore.getInitialState(), true)
delete (window as typeof window & { __TAURI__?: unknown }).__TAURI__
})
@ -780,6 +784,63 @@ describe('TabBar', () => {
expect(useWorkspacePanelStore.getState().isPanelOpen('tab-1')).toBe(false)
})
it('toggles the browser panel for the active session from the toolbar', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useBrowserPanelStore } = await import('../../stores/browserPanelStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
fireEvent.click(screen.getByRole('button', { name: 'Show Browser' }))
expect(useBrowserPanelStore.getState().bySession['tab-1']?.isOpen).toBe(true)
fireEvent.click(screen.getByRole('button', { name: 'Hide Browser' }))
expect(useBrowserPanelStore.getState().bySession['tab-1']?.isOpen).toBe(false)
})
it('hides the browser toolbar button for non-session tabs', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: '__terminal__1', title: 'Terminal 1', type: 'terminal', status: 'idle' },
{ sessionId: '__settings__', title: 'Settings', type: 'settings', status: 'idle' },
],
activeTabId: '__terminal__1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
const { rerender } = render(<TabBar />)
expect(screen.queryByRole('button', { name: 'Show Browser' })).not.toBeInTheDocument()
await act(async () => {
useTabStore.getState().setActiveTab('__settings__')
})
rerender(<TabBar />)
expect(screen.queryByRole('button', { name: 'Show Browser' })).not.toBeInTheDocument()
})
it('hides the workspace toolbar button for non-session tabs', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')

View File

@ -11,10 +11,11 @@ import { useChatStore } from '../../stores/chatStore'
import { useSessionStore } from '../../stores/sessionStore'
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
import { useTerminalPanelStore } from '../../stores/terminalPanelStore'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
import { useTranslation } from '../../i18n'
import { WindowControls, showWindowControls } from './WindowControls'
import { OpenProjectMenu } from './OpenProjectMenu'
import { Folder, FolderOpen, SquareTerminal } from 'lucide-react'
import { Folder, FolderOpen, Globe, SquareTerminal } from 'lucide-react'
const TAB_WIDTH = 180
const DRAG_START_THRESHOLD = 4
@ -67,6 +68,9 @@ export function TabBar() {
const isTerminalPanelOpen = useTerminalPanelStore((state) =>
activeTabId && isActiveSessionTab ? state.isPanelOpen(activeTabId) : false,
)
const isBrowserPanelOpen = useBrowserPanelStore((state) =>
activeTabId && isActiveSessionTab ? Boolean(state.bySession[activeTabId]?.isOpen) : false,
)
const moveTab = useTabStore((s) => s.moveTab)
const scrollRef = useRef<HTMLDivElement>(null)
@ -388,6 +392,21 @@ export function TabBar() {
active={isWorkspacePanelOpen}
/>
)}
{isActiveSessionTab && activeTabId && (
<ToolbarIconButton
icon={<Globe size={17} strokeWidth={1.9} />}
label={t(isBrowserPanelOpen ? 'tabs.hideBrowser' : 'tabs.showBrowser')}
onClick={() => {
const browser = useBrowserPanelStore.getState()
if (browser.bySession[activeTabId]?.isOpen) {
browser.close(activeTabId)
} else {
browser.open(activeTabId, 'http://localhost:5173/')
}
}}
active={isBrowserPanelOpen}
/>
)}
</div>
{isTauri && (

View File

@ -1617,6 +1617,8 @@ export const en = {
'tabs.openTerminal': 'Open Terminal',
'tabs.showWorkspace': 'Show Workspace',
'tabs.hideWorkspace': 'Hide Workspace',
'tabs.showBrowser': 'Show Browser',
'tabs.hideBrowser': 'Hide Browser',
} as const
export type TranslationKey = keyof typeof en

View File

@ -1619,4 +1619,6 @@ export const zh: Record<TranslationKey, string> = {
'tabs.openTerminal': '打开终端',
'tabs.showWorkspace': '显示工作区',
'tabs.hideWorkspace': '隐藏工作区',
'tabs.showBrowser': '显示浏览器',
'tabs.hideBrowser': '隐藏浏览器',
}

View File

@ -12,6 +12,7 @@ import { useChatStore } from '../stores/chatStore'
import { useCLITaskStore } from '../stores/cliTaskStore'
import { useTeamStore } from '../stores/teamStore'
import { useWorkspacePanelStore } from '../stores/workspacePanelStore'
import { useBrowserPanelStore } from '../stores/browserPanelStore'
import {
TERMINAL_PANEL_DEFAULT_HEIGHT,
TERMINAL_PANEL_MAX_HEIGHT,
@ -24,6 +25,7 @@ import { ChatInput } from '../components/chat/ChatInput'
import { ComputerUsePermissionModal } from '../components/chat/ComputerUsePermissionModal'
import { SessionTaskBar } from '../components/chat/SessionTaskBar'
import { WorkspacePanel } from '../components/workspace/WorkspacePanel'
import { BrowserSurface } from '../components/browser/BrowserSurface'
import { TeamStatusBar } from '../components/teams/TeamStatusBar'
import { TerminalSettings } from './TerminalSettings'
import type { SessionListItem } from '../types/session'
@ -281,6 +283,13 @@ export function ActiveSession() {
? state.isPanelOpen(activeTabId)
: false,
)
const showBrowserPanel = useBrowserPanelStore((state) =>
activeTabId && isSessionTabState(activeTabId, activeTabType) && !isMemberSession && !isMobileLayout
? Boolean(state.bySession[activeTabId]?.isOpen)
: false,
)
const showRightPanel = showWorkspacePanel || showBrowserPanel
const rightPanelWidth = useWorkspacePanelStore((state) => state.width)
const showTerminalPanel = useTerminalPanelStore((state) =>
activeTabId && isSessionTabState(activeTabId, activeTabType) && !isMemberSession && !isMobileLayout
? state.isPanelOpen(activeTabId)
@ -352,7 +361,7 @@ export function ActiveSession() {
<div data-testid="active-session-content-row" className="flex min-h-0 min-w-0 flex-1">
<div
data-testid="active-session-chat-column"
className={`flex flex-col ${showWorkspacePanel ? CHAT_COLUMN_WITH_WORKSPACE_CLASS : isMobileLayout ? 'min-w-0 flex-1' : 'min-w-[360px] flex-1'}`}
className={`flex flex-col ${showRightPanel ? CHAT_COLUMN_WITH_WORKSPACE_CLASS : isMobileLayout ? 'min-w-0 flex-1' : 'min-w-[360px] flex-1'}`}
>
{isMemberSession && (
<div className="shrink-0 border-b border-[var(--color-border)] bg-[var(--color-surface-container)]">
@ -429,15 +438,15 @@ export function ActiveSession() {
{!isMemberSession && !isMobileLayout && (
<div
className={
showWorkspacePanel
showRightPanel
? 'flex w-full items-center border-b border-[var(--color-border)]/70 px-4 py-3'
: 'w-full border-b border-outline-variant/10 px-4 py-3'
}
>
<div className={showWorkspacePanel ? 'min-w-0 flex-1' : 'mx-auto w-full max-w-[860px] min-w-0'}>
<div className={showRightPanel ? 'min-w-0 flex-1' : 'mx-auto w-full max-w-[860px] min-w-0'}>
<h1
className={
showWorkspacePanel
showRightPanel
? 'truncate text-[15px] font-bold font-headline leading-tight text-on-surface'
: 'text-lg font-bold font-headline text-on-surface leading-tight'
}
@ -446,7 +455,7 @@ export function ActiveSession() {
</h1>
<div
className={
showWorkspacePanel
showRightPanel
? 'mt-1 flex min-w-0 items-center gap-1.5 overflow-hidden whitespace-nowrap text-[10px] font-medium text-outline'
: 'flex items-center gap-2 text-[10px] text-outline font-medium mt-1'
}
@ -469,7 +478,7 @@ export function ActiveSession() {
<span className="truncate">{t('session.lastUpdated', { time: lastUpdated })}</span>
</>
)}
{!showWorkspacePanel && visibleMessageCount > 0 && (
{!showRightPanel && visibleMessageCount > 0 && (
<>
<span className="text-[var(--color-outline)]">·</span>
<span>{t('session.messages', { count: visibleMessageCount })}</span>
@ -487,13 +496,13 @@ export function ActiveSession() {
<ActiveGoalStrip
goal={activeGoal}
isRunning={isActive}
compact={showWorkspacePanel}
compact={showRightPanel}
/>
</div>
</div>
)}
<MessageList compact={showWorkspacePanel} />
<MessageList compact={showRightPanel} />
</>
)}
@ -502,8 +511,8 @@ export function ActiveSession() {
<TeamStatusBar />
<ChatInput
variant={isEmpty && !isMemberSession && !showWorkspacePanel ? 'hero' : 'default'}
compact={showWorkspacePanel}
variant={isEmpty && !isMemberSession && !showRightPanel ? 'hero' : 'default'}
compact={showRightPanel}
/>
{terminalPanelRuntimeId && activeTabId ? (
@ -534,7 +543,18 @@ export function ActiveSession() {
) : null}
</div>
{showWorkspacePanel ? (
{showBrowserPanel ? (
<>
<WorkspaceResizeHandle />
<aside
data-testid="browser-panel"
className="flex h-full shrink-0 flex-col border-l border-[var(--color-border)] bg-[var(--color-surface)]"
style={{ width: rightPanelWidth, maxWidth: '62%', minWidth: 'min(420px, 54%)' }}
>
<BrowserSurface sessionId={activeTabId} />
</aside>
</>
) : showWorkspacePanel ? (
<>
<WorkspaceResizeHandle />
<WorkspacePanel sessionId={activeTabId} />