From f5a97ac21c9f42e786822ab3ec67dcaa9f87d27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 15 Jun 2026 19:41:57 +0800 Subject: [PATCH] fix(desktop): expand workbench panels into tabs (#730) Tested: cd desktop && bun run test -- --run src/components/workbench/WorkbenchPanel.test.tsx Tested: cd desktop && bun run test -- --run src/components/layout/ContentRouter.test.tsx Tested: cd desktop && bun run test -- --run src/stores/tabStore.test.ts Tested: bun run check:desktop Confidence: high Scope-risk: moderate --- .../components/layout/ContentRouter.test.tsx | 26 +++++++++- .../src/components/layout/ContentRouter.tsx | 8 ++- desktop/src/components/layout/TabBar.tsx | 7 ++- .../workbench/WorkbenchPanel.test.tsx | 44 +++++++++++++++- .../components/workbench/WorkbenchPanel.tsx | 51 +++++++++++++++---- .../src/components/workbench/WorkbenchTab.tsx | 30 +++++++++++ .../components/workspace/WorkspacePanel.tsx | 22 +++++--- desktop/src/i18n/locales/en.ts | 2 + desktop/src/i18n/locales/jp.ts | 2 + desktop/src/i18n/locales/kr.ts | 2 + desktop/src/i18n/locales/zh-TW.ts | 2 + desktop/src/i18n/locales/zh.ts | 2 + desktop/src/pages/ActiveSession.tsx | 4 +- desktop/src/stores/tabStore.test.ts | 22 ++++++++ desktop/src/stores/tabStore.ts | 34 ++++++++++++- 15 files changed, 231 insertions(+), 27 deletions(-) create mode 100644 desktop/src/components/workbench/WorkbenchTab.tsx diff --git a/desktop/src/components/layout/ContentRouter.test.tsx b/desktop/src/components/layout/ContentRouter.test.tsx index c31ac291..3bfc96bf 100644 --- a/desktop/src/components/layout/ContentRouter.test.tsx +++ b/desktop/src/components/layout/ContentRouter.test.tsx @@ -42,10 +42,16 @@ vi.mock('../../pages/TraceList', () => ({ TraceList: () =>
, })) +vi.mock('../workbench/WorkbenchTab', () => ({ + WorkbenchTab: ({ sessionId, tabId }: { sessionId: string; tabId: string }) => ( +
workbench:{sessionId}:{tabId}
+ ), +})) + import { ContentRouter } from './ContentRouter' import { useTabStore } from '../../stores/tabStore' -describe('ContentRouter terminal tabs', () => { +describe('ContentRouter tab surfaces', () => { afterEach(() => { cleanup() previewBridgeMock.close.mockClear() @@ -148,6 +154,24 @@ describe('ContentRouter terminal tabs', () => { expect(screen.queryByTestId('active-session')).not.toBeInTheDocument() }) + it('renders workbench tabs as main content instead of mounting the chat session surface', () => { + useTabStore.setState({ + tabs: [{ + sessionId: '__workbench__session-1', + title: 'Workbench', + type: 'workbench', + status: 'idle', + workbenchSessionId: 'session-1', + }], + activeTabId: '__workbench__session-1', + }) + + render() + + expect(screen.getByTestId('workbench-tab')).toHaveTextContent('workbench:session-1:__workbench__session-1') + expect(screen.queryByTestId('active-session')).not.toBeInTheDocument() + }) + it('closes the native preview when switching from a chat session to settings', async () => { useTabStore.setState({ tabs: [ diff --git a/desktop/src/components/layout/ContentRouter.tsx b/desktop/src/components/layout/ContentRouter.tsx index 7fbf5321..2e4845f0 100644 --- a/desktop/src/components/layout/ContentRouter.tsx +++ b/desktop/src/components/layout/ContentRouter.tsx @@ -7,6 +7,7 @@ import { Settings } from '../../pages/Settings' import { TerminalSettings } from '../../pages/TerminalSettings' import { TraceList } from '../../pages/TraceList' import { TraceSession } from '../../pages/TraceSession' +import { WorkbenchTab } from '../workbench/WorkbenchTab' import { previewBridge } from '../../lib/previewBridge' export function ContentRouter() { @@ -16,7 +17,7 @@ export function ContentRouter() { const terminalTabs = tabs.filter((tab) => tab.type === 'terminal') useEffect(() => { - if (activeTabType === 'session') return + if (activeTabType === 'session' || activeTabType === 'workbench') return void previewBridge.close() }, [activeTabType]) @@ -32,6 +33,11 @@ export function ContentRouter() { page = traceSessionId ? : } else if (activeTabType === 'traces') { page = + } else if (activeTabType === 'workbench') { + const workbenchTab = tabs.find((t) => t.sessionId === activeTabId) + page = workbenchTab?.workbenchSessionId + ? + : } else if (activeTabType !== 'terminal') { page = } diff --git a/desktop/src/components/layout/TabBar.tsx b/desktop/src/components/layout/TabBar.tsx index c7282f60..75669a5c 100644 --- a/desktop/src/components/layout/TabBar.tsx +++ b/desktop/src/components/layout/TabBar.tsx @@ -6,6 +6,7 @@ import { TERMINAL_TAB_PREFIX, TRACE_LIST_TAB_ID, TRACE_TAB_PREFIX, + WORKBENCH_TAB_PREFIX, useTabStore, type Tab, } from '../../stores/tabStore' @@ -44,7 +45,8 @@ function isSessionTabId(tabId: string | null) { tabId !== SCHEDULED_TAB_ID && tabId !== TRACE_LIST_TAB_ID && !tabId.startsWith(TERMINAL_TAB_PREFIX) && - !tabId.startsWith(TRACE_TAB_PREFIX) + !tabId.startsWith(TRACE_TAB_PREFIX) && + !tabId.startsWith(WORKBENCH_TAB_PREFIX) } export function TabBar() { @@ -549,6 +551,9 @@ const TabItem = forwardRefterminal )} + {tab.type === 'workbench' && ( + view_sidebar + )} {tab.title || 'Untitled'} diff --git a/desktop/src/components/workbench/WorkbenchPanel.test.tsx b/desktop/src/components/workbench/WorkbenchPanel.test.tsx index a3695da3..20fef1b7 100644 --- a/desktop/src/components/workbench/WorkbenchPanel.test.tsx +++ b/desktop/src/components/workbench/WorkbenchPanel.test.tsx @@ -1,10 +1,16 @@ +// @vitest-environment jsdom + import '@testing-library/jest-dom' import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('../workspace/WorkspacePanel', () => ({ - WorkspacePanel: ({ sessionId, embedded }: { sessionId: string; embedded?: boolean }) => ( -
+ 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 ( +
+ useTabStore.getState().closeTab(tabId)} + /> +
+ ) +} diff --git a/desktop/src/components/workspace/WorkspacePanel.tsx b/desktop/src/components/workspace/WorkspacePanel.tsx index d2b74313..454d7423 100644 --- a/desktop/src/components/workspace/WorkspacePanel.tsx +++ b/desktop/src/components/workspace/WorkspacePanel.tsx @@ -38,6 +38,11 @@ type WorkspacePanelProps = { * button so the panel header doesn't render a duplicate close control. */ embedded?: boolean + /** + * Main-content workbench tabs reuse the same workspace preview UI without + * depending on the right-side panel's open bit. + */ + forceVisible?: boolean } type TreeNodeProps = { @@ -912,7 +917,7 @@ function TreeNode({ ) } -export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelProps) { +export function WorkspacePanel({ sessionId, embedded = false, forceVisible = false }: WorkspacePanelProps) { const t = useTranslation() const addToast = useUIStore((state) => state.addToast) const [filterQuery, setFilterQuery] = useState('') @@ -946,6 +951,7 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr const closePanel = useWorkspacePanelStore((state) => state.closePanel) const addWorkspaceReference = useWorkspaceChatContextStore((state) => state.addReference) const chatState = useChatStore((state) => state.sessions[sessionId]?.chatState ?? 'idle') + const shouldRender = forceVisible || isOpen const refreshLifecycleRef = useRef({ sessionId, isOpen: false, @@ -986,25 +992,25 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr useEffect(() => { const previous = refreshLifecycleRef.current const sessionChanged = previous.sessionId !== sessionId - const opened = isOpen && (sessionChanged || !previous.isOpen) + const opened = shouldRender && (sessionChanged || !previous.isOpen) const completedTurn = - isOpen && + shouldRender && !sessionChanged && previous.chatState !== 'idle' && chatState === 'idle' - refreshLifecycleRef.current = { sessionId, isOpen, chatState } + refreshLifecycleRef.current = { sessionId, isOpen: shouldRender, chatState } const shouldRefreshOnOpen = opened const shouldRefreshAfterCompletedTurn = completedTurn && chatState === 'idle' if ((!shouldRefreshOnOpen && !shouldRefreshAfterCompletedTurn) || statusLoading) return void loadStatus(sessionId) - }, [chatState, isOpen, loadStatus, sessionId, statusLoading]) + }, [chatState, loadStatus, sessionId, shouldRender, statusLoading]) useEffect(() => { - if (!isOpen || !isNavigatorVisible || activeView !== 'all' || rootTree || rootTreeLoading || rootTreeError) return + if (!shouldRender || !isNavigatorVisible || activeView !== 'all' || rootTree || rootTreeLoading || rootTreeError) return void loadTree(sessionId, '') - }, [activeView, isNavigatorVisible, isOpen, loadTree, rootTree, rootTreeError, rootTreeLoading, sessionId]) + }, [activeView, isNavigatorVisible, loadTree, rootTree, rootTreeError, rootTreeLoading, sessionId, shouldRender]) useEffect(() => { if (!previewTabContextMenu && !fileContextMenu) return @@ -1028,7 +1034,7 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr } }, [isNavigatorVisible]) - if (!isOpen) return null + if (!shouldRender) return null const panelWidth = hasPreviewTabs ? width : Math.min(width, 520) const panelMaxWidth = hasPreviewTabs ? 'min(62%, calc(100% - 328px))' : '36%' diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index a0cc0b3e..9297e1be 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -141,7 +141,9 @@ export const en = { 'workbench.modeSwitch': 'Workbench mode', 'workbench.modeWorkspace': 'Files', 'workbench.modeBrowser': 'Browser', + 'workbench.expand': 'Expand panel', 'workbench.close': 'Close', + 'workbench.tabTitle': 'Workbench', 'workspace.closeTab': 'Close tab', 'workspace.preview': 'Preview', 'workspace.previewEmpty': 'Select a file to preview.', diff --git a/desktop/src/i18n/locales/jp.ts b/desktop/src/i18n/locales/jp.ts index 0cbcef39..d7f7cc15 100644 --- a/desktop/src/i18n/locales/jp.ts +++ b/desktop/src/i18n/locales/jp.ts @@ -143,7 +143,9 @@ export const jp: Record = { 'workbench.modeSwitch': 'ワークベンチモード', 'workbench.modeWorkspace': 'ファイル', 'workbench.modeBrowser': 'ブラウザ', + 'workbench.expand': 'パネルを展開', 'workbench.close': '閉じる', + 'workbench.tabTitle': 'ワークベンチ', 'workspace.closeTab': 'タブを閉じる', 'workspace.preview': 'プレビュー', 'workspace.previewEmpty': 'プレビューするファイルを選択してください。', diff --git a/desktop/src/i18n/locales/kr.ts b/desktop/src/i18n/locales/kr.ts index b7618a08..0f96fa86 100644 --- a/desktop/src/i18n/locales/kr.ts +++ b/desktop/src/i18n/locales/kr.ts @@ -143,7 +143,9 @@ export const kr: Record = { 'workbench.modeSwitch': '워크벤치 모드', 'workbench.modeWorkspace': '파일', 'workbench.modeBrowser': '브라우저', + 'workbench.expand': '패널 펼치기', 'workbench.close': '닫기', + 'workbench.tabTitle': '워크벤치', 'workspace.closeTab': '탭 닫기', 'workspace.preview': '미리 보기', 'workspace.previewEmpty': '미리 볼 파일을 선택하세요.', diff --git a/desktop/src/i18n/locales/zh-TW.ts b/desktop/src/i18n/locales/zh-TW.ts index ba4b5327..10dea4c8 100644 --- a/desktop/src/i18n/locales/zh-TW.ts +++ b/desktop/src/i18n/locales/zh-TW.ts @@ -143,7 +143,9 @@ export const zh: Record = { 'workbench.modeSwitch': '工作臺模式', 'workbench.modeWorkspace': '檔案', 'workbench.modeBrowser': '瀏覽器', + 'workbench.expand': '展開面板', 'workbench.close': '關閉', + 'workbench.tabTitle': '工作臺', 'workspace.closeTab': '關閉標籤', 'workspace.preview': '預覽', 'workspace.previewEmpty': '選擇一個檔案進行預覽。', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 68475197..0f695df6 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -143,7 +143,9 @@ export const zh: Record = { 'workbench.modeSwitch': '工作台模式', 'workbench.modeWorkspace': '文件', 'workbench.modeBrowser': '浏览器', + 'workbench.expand': '展开面板', 'workbench.close': '关闭', + 'workbench.tabTitle': '工作台', 'workspace.closeTab': '关闭标签', 'workspace.preview': '预览', 'workspace.previewEmpty': '选择一个文件进行预览。', diff --git a/desktop/src/pages/ActiveSession.tsx b/desktop/src/pages/ActiveSession.tsx index 45f63d32..64f8e5ae 100644 --- a/desktop/src/pages/ActiveSession.tsx +++ b/desktop/src/pages/ActiveSession.tsx @@ -5,6 +5,7 @@ import { SETTINGS_TAB_ID, TERMINAL_TAB_PREFIX, TRACE_TAB_PREFIX, + WORKBENCH_TAB_PREFIX, useTabStore, type TabType, } from '../stores/tabStore' @@ -47,7 +48,8 @@ function isSessionTabState(activeTabId: string | null, activeTabType: TabType | return activeTabId !== SETTINGS_TAB_ID && activeTabId !== SCHEDULED_TAB_ID && !activeTabId.startsWith(TERMINAL_TAB_PREFIX) && - !activeTabId.startsWith(TRACE_TAB_PREFIX) + !activeTabId.startsWith(TRACE_TAB_PREFIX) && + !activeTabId.startsWith(WORKBENCH_TAB_PREFIX) } function getSessionTerminalCwd(session: SessionListItem | undefined) { diff --git a/desktop/src/stores/tabStore.test.ts b/desktop/src/stores/tabStore.test.ts index 63649b12..64135b52 100644 --- a/desktop/src/stores/tabStore.test.ts +++ b/desktop/src/stores/tabStore.test.ts @@ -44,6 +44,28 @@ describe('tabStore', () => { expect(useTabStore.getState().activeTabId).toBe(tabId) }) + it('opens one ephemeral workbench tab per source session', () => { + const firstTabId = useTabStore.getState().openWorkbenchTab('session-1', 'Workbench') + const secondTabId = useTabStore.getState().openWorkbenchTab('session-1', 'Workbench') + + expect(firstTabId).toBe('__workbench__session-1') + expect(secondTabId).toBe(firstTabId) + expect(useTabStore.getState().tabs).toEqual([ + { + sessionId: '__workbench__session-1', + title: 'Workbench', + type: 'workbench', + status: 'idle', + workbenchSessionId: 'session-1', + }, + ]) + expect(useTabStore.getState().activeTabId).toBe('__workbench__session-1') + expect(localStorage.getItem('cc-haha-open-tabs')).toBe(JSON.stringify({ + openTabs: [], + activeTabId: null, + })) + }) + it('does not let async tab restore overwrite tabs opened while restore is in flight', async () => { let resolveSessions: (value: unknown) => void = () => {} vi.mocked(sessionsApi.list).mockReturnValueOnce(new Promise((resolve) => { diff --git a/desktop/src/stores/tabStore.ts b/desktop/src/stores/tabStore.ts index 2ab3e6b5..30e15586 100644 --- a/desktop/src/stores/tabStore.ts +++ b/desktop/src/stores/tabStore.ts @@ -10,8 +10,9 @@ export const SCHEDULED_TAB_ID = '__scheduled__' export const TRACE_LIST_TAB_ID = '__traces__' export const TERMINAL_TAB_PREFIX = '__terminal__' export const TRACE_TAB_PREFIX = '__trace__' +export const WORKBENCH_TAB_PREFIX = '__workbench__' -export type TabType = 'session' | 'settings' | 'scheduled' | 'terminal' | 'trace' | 'traces' +export type TabType = 'session' | 'settings' | 'scheduled' | 'terminal' | 'trace' | 'traces' | 'workbench' export type Tab = { sessionId: string @@ -21,6 +22,7 @@ export type Tab = { terminalCwd?: string terminalRuntimeId?: string traceSessionId?: string + workbenchSessionId?: string } type TabPersistence = { @@ -36,6 +38,7 @@ type TabStore = { openTracesTab: (title?: string) => string openTraceTab: (sessionId: string, title?: string) => string openTerminalTab: (cwd?: string, terminalRuntimeId?: string) => string + openWorkbenchTab: (sessionId: string, title?: string) => string closeTab: (sessionId: string) => void setActiveTab: (sessionId: string) => void updateTabTitle: (sessionId: string, title: string) => void @@ -141,6 +144,33 @@ export const useTabStore = create((set, get) => ({ return sessionId }, + openWorkbenchTab: (sessionId, title = 'Workbench') => { + const tabId = `${WORKBENCH_TAB_PREFIX}${sessionId}` + const { tabs } = get() + const existing = tabs.find((tab) => tab.sessionId === tabId) + const tab: Tab = { + sessionId: tabId, + title, + type: 'workbench', + status: 'idle', + workbenchSessionId: sessionId, + } + + if (existing) { + set({ + tabs: tabs.map((current) => current.sessionId === tabId ? tab : current), + activeTabId: tabId, + }) + } else { + set({ + tabs: [...tabs, tab], + activeTabId: tabId, + }) + } + get().saveTabs() + return tabId + }, + closeTab: (sessionId) => { const { tabs, activeTabId } = get() const index = tabs.findIndex((t) => t.sessionId === sessionId) @@ -210,7 +240,7 @@ export const useTabStore = create((set, get) => ({ saveTabs: () => { const { tabs, activeTabId } = get() - const persistableTabs = tabs.filter((tab) => tab.type !== 'terminal') + const persistableTabs = tabs.filter((tab) => tab.type !== 'terminal' && tab.type !== 'workbench') const data: TabPersistence = { openTabs: persistableTabs.map((t) => ({ sessionId: t.sessionId,