mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
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:
parent
0ac56a6b11
commit
3077f1569d
@ -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')
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1619,4 +1619,6 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'tabs.openTerminal': '打开终端',
|
||||
'tabs.showWorkspace': '显示工作区',
|
||||
'tabs.hideWorkspace': '隐藏工作区',
|
||||
'tabs.showBrowser': '显示浏览器',
|
||||
'tabs.hideBrowser': '隐藏浏览器',
|
||||
}
|
||||
|
||||
@ -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} />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user