diff --git a/desktop/src/components/layout/TabBar.test.tsx b/desktop/src/components/layout/TabBar.test.tsx index 6553b41d..d735517c 100644 --- a/desktop/src/components/layout/TabBar.test.tsx +++ b/desktop/src/components/layout/TabBar.test.tsx @@ -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>) 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>) + + await act(async () => { + render() + }) + + 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>) + + const { rerender } = render() + + expect(screen.queryByRole('button', { name: 'Show Browser' })).not.toBeInTheDocument() + + await act(async () => { + useTabStore.getState().setActiveTab('__settings__') + }) + rerender() + + 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') diff --git a/desktop/src/components/layout/TabBar.tsx b/desktop/src/components/layout/TabBar.tsx index eec97b5c..2fdfc547 100644 --- a/desktop/src/components/layout/TabBar.tsx +++ b/desktop/src/components/layout/TabBar.tsx @@ -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(null) @@ -388,6 +392,21 @@ export function TabBar() { active={isWorkspacePanelOpen} /> )} + {isActiveSessionTab && activeTabId && ( + } + 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} + /> + )} {isTauri && ( diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 5c12a29c..f0948667 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -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 diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 41479fad..7b2f39f6 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -1619,4 +1619,6 @@ export const zh: Record = { 'tabs.openTerminal': '打开终端', 'tabs.showWorkspace': '显示工作区', 'tabs.hideWorkspace': '隐藏工作区', + 'tabs.showBrowser': '显示浏览器', + 'tabs.hideBrowser': '隐藏浏览器', } diff --git a/desktop/src/pages/ActiveSession.tsx b/desktop/src/pages/ActiveSession.tsx index 5652ed03..545404fe 100644 --- a/desktop/src/pages/ActiveSession.tsx +++ b/desktop/src/pages/ActiveSession.tsx @@ -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() {
{isMemberSession && (
@@ -429,15 +438,15 @@ export function ActiveSession() { {!isMemberSession && !isMobileLayout && (
-
+

{t('session.lastUpdated', { time: lastUpdated })} )} - {!showWorkspacePanel && visibleMessageCount > 0 && ( + {!showRightPanel && visibleMessageCount > 0 && ( <> · {t('session.messages', { count: visibleMessageCount })} @@ -487,13 +496,13 @@ export function ActiveSession() {

)} - + )} @@ -502,8 +511,8 @@ export function ActiveSession() { {terminalPanelRuntimeId && activeTabId ? ( @@ -534,7 +543,18 @@ export function ActiveSession() { ) : null}
- {showWorkspacePanel ? ( + {showBrowserPanel ? ( + <> + + + + ) : showWorkspacePanel ? ( <>