+ WorkspacePanel: ({ sessionId, embedded, forceVisible }: { sessionId: string; embedded?: boolean; forceVisible?: boolean }) => (
+
workspace:{sessionId}
),
@@ -20,12 +26,14 @@ import { WorkbenchPanel } from './WorkbenchPanel'
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
import { useSettingsStore } from '../../stores/settingsStore'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
+import { useTabStore } from '../../stores/tabStore'
const SESSION_ID = 'workbench-session'
beforeEach(() => {
useWorkspacePanelStore.setState(useWorkspacePanelStore.getInitialState(), true)
useBrowserPanelStore.setState(useBrowserPanelStore.getInitialState(), true)
+ useTabStore.setState(useTabStore.getInitialState(), true)
useSettingsStore.setState({ locale: 'en' })
useWorkspacePanelStore.getState().openPanel(SESSION_ID)
})
@@ -34,6 +42,7 @@ afterEach(() => {
cleanup()
useWorkspacePanelStore.setState(useWorkspacePanelStore.getInitialState(), true)
useBrowserPanelStore.setState(useBrowserPanelStore.getInitialState(), true)
+ useTabStore.setState(useTabStore.getInitialState(), true)
})
describe('WorkbenchPanel', () => {
@@ -95,4 +104,35 @@ describe('WorkbenchPanel', () => {
expect(useWorkspacePanelStore.getState().isPanelOpen(SESSION_ID)).toBe(false)
})
+
+ it('the expand button promotes the current workbench into a main content tab', () => {
+ useWorkspacePanelStore.getState().setMode(SESSION_ID, 'browser')
+ render(
)
+
+ fireEvent.click(screen.getByRole('button', { name: 'Expand panel' }))
+
+ expect(useTabStore.getState().activeTabId).toBe(`__workbench__${SESSION_ID}`)
+ expect(useTabStore.getState().tabs).toEqual([
+ {
+ sessionId: `__workbench__${SESSION_ID}`,
+ title: 'Workbench',
+ type: 'workbench',
+ status: 'idle',
+ workbenchSessionId: SESSION_ID,
+ },
+ ])
+ })
+
+ it('renders the tab variant without a nested expand action', () => {
+ const handleClose = vi.fn()
+ render(
)
+
+ expect(screen.queryByRole('button', { name: 'Expand panel' })).not.toBeInTheDocument()
+ expect(screen.getByTestId('workspace-panel')).toHaveAttribute('data-force-visible', 'true')
+
+ fireEvent.click(screen.getByRole('button', { name: 'Close' }))
+
+ expect(handleClose).toHaveBeenCalledTimes(1)
+ expect(useWorkspacePanelStore.getState().isPanelOpen(SESSION_ID)).toBe(true)
+ })
})
diff --git a/desktop/src/components/workbench/WorkbenchPanel.tsx b/desktop/src/components/workbench/WorkbenchPanel.tsx
index c348ed86..d39ad7e6 100644
--- a/desktop/src/components/workbench/WorkbenchPanel.tsx
+++ b/desktop/src/components/workbench/WorkbenchPanel.tsx
@@ -1,15 +1,18 @@
-import { FolderOpen, Globe, X } from 'lucide-react'
+import { FolderOpen, Globe, Maximize2, X } from 'lucide-react'
import { useTranslation } from '../../i18n'
import {
useWorkspacePanelStore,
type WorkbenchMode,
} from '../../stores/workspacePanelStore'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
+import { useTabStore } from '../../stores/tabStore'
import { WorkspacePanel } from '../workspace/WorkspacePanel'
import { BrowserSurface } from '../browser/BrowserSurface'
type WorkbenchPanelProps = {
sessionId: string
+ variant?: 'panel' | 'tab'
+ onClose?: () => void
}
const MODE_ITEMS: ReadonlyArray<{
@@ -26,12 +29,13 @@ const MODE_ITEMS: ReadonlyArray<{
* browser surface behind a single per-session mode switch (file ↔ browser),
* sharing the panel's open state and width via {@link useWorkspacePanelStore}.
*/
-export function WorkbenchPanel({ sessionId }: WorkbenchPanelProps) {
+export function WorkbenchPanel({ sessionId, variant = 'panel', onClose }: WorkbenchPanelProps) {
const t = useTranslation()
const mode = useWorkspacePanelStore((state) => state.getMode(sessionId))
const setMode = useWorkspacePanelStore((state) => state.setMode)
const closePanel = useWorkspacePanelStore((state) => state.closePanel)
const ensureBlankBrowser = useBrowserPanelStore((state) => state.ensureBlank)
+ const isTabVariant = variant === 'tab'
const handleModeSelect = (nextMode: WorkbenchMode) => {
if (nextMode === 'browser') {
@@ -40,6 +44,18 @@ export function WorkbenchPanel({ sessionId }: WorkbenchPanelProps) {
setMode(sessionId, nextMode)
}
+ const handleExpand = () => {
+ useTabStore.getState().openWorkbenchTab(sessionId, t('workbench.tabTitle'))
+ }
+
+ const handleClose = () => {
+ if (onClose) {
+ onClose()
+ return
+ }
+ closePanel(sessionId)
+ }
+
return (
@@ -70,21 +86,34 @@ export function WorkbenchPanel({ sessionId }: WorkbenchPanelProps) {
})}
-
+
+ {!isTabVariant && (
+
+ )}
+
+
{mode === 'browser' ? (
) : (
-
+
)}
diff --git a/desktop/src/components/workbench/WorkbenchTab.tsx b/desktop/src/components/workbench/WorkbenchTab.tsx
new file mode 100644
index 00000000..b7b437f8
--- /dev/null
+++ b/desktop/src/components/workbench/WorkbenchTab.tsx
@@ -0,0 +1,30 @@
+import { useEffect } from 'react'
+import { useBrowserPanelStore } from '../../stores/browserPanelStore'
+import { useTabStore } from '../../stores/tabStore'
+import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
+import { WorkbenchPanel } from './WorkbenchPanel'
+
+type WorkbenchTabProps = {
+ tabId: string
+ sessionId: string
+}
+
+export function WorkbenchTab({ tabId, sessionId }: WorkbenchTabProps) {
+ const mode = useWorkspacePanelStore((state) => state.getMode(sessionId))
+
+ useEffect(() => {
+ if (mode === 'browser') {
+ useBrowserPanelStore.getState().ensureBlank(sessionId)
+ }
+ }, [mode, sessionId])
+
+ return (
+