refactor(ui): name workspace surfaces

This commit is contained in:
Bohdan Triapitsyn
2026-08-22 01:10:19 +03:00
parent be234548fa
commit 70a7a77ded
10 changed files with 99 additions and 70 deletions
@@ -12,7 +12,6 @@ import { useI18n } from '@/lib/i18n';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { getRuntimeKey } from '@/lib/runtime-switch'; import { getRuntimeKey } from '@/lib/runtime-switch';
import { focusChatInput } from '@/components/chat/composer/editor/dom'; import { focusChatInput } from '@/components/chat/composer/editor/dom';
import { useUIStore } from '@/stores/useUIStore';
type LineRangeBase = { type LineRangeBase = {
start: number; start: number;
@@ -164,7 +163,6 @@ export function useInlineCommentController<TRange extends LineRangeBase>(
reset(); reset();
if (isNewComment) { if (isNewComment) {
useUIStore.getState().setActiveMainTab('chat');
requestAnimationFrame(focusChatInput); requestAnimationFrame(focusChatInput);
} }
}, [addDraft, editingDraftId, fileLabel, getCodeForRange, language, reset, selection, source, t, target, toStoreRange, updateDraft]); }, [addDraft, editingDraftId, fileLabel, getCodeForRange, language, reset, selection, source, t, target, toStoreRange, updateDraft]);
@@ -45,7 +45,7 @@ const SettingsWindow = lazyWithChunkRecovery(() => import('@/components/views/Se
export const MainLayout: React.FC = () => { export const MainLayout: React.FC = () => {
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen); const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
const activeMainTab = useUIStore((state) => state.activeMainTab); const activeSurface = useUIStore((state) => state.activeSurface);
const setIsMobile = useUIStore((state) => state.setIsMobile); const setIsMobile = useUIStore((state) => state.setIsMobile);
const isSessionSwitcherOpen = useUIStore((state) => state.isSessionSwitcherOpen); const isSessionSwitcherOpen = useUIStore((state) => state.isSessionSwitcherOpen);
const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen); const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen);
@@ -83,7 +83,7 @@ export const MainLayout: React.FC = () => {
if (sessionSelected || draftOpened) closeSurfacePages(); if (sessionSelected || draftOpened) closeSurfacePages();
}); });
const unsubscribeTab = useUIStore.subscribe((state, prev) => { const unsubscribeTab = useUIStore.subscribe((state, prev) => {
if (state.activeMainTab !== prev.activeMainTab) closeSurfacePages(); if (state.activeSurface !== prev.activeSurface) closeSurfacePages();
}); });
return () => { return () => {
unsubscribeSession(); unsubscribeSession();
@@ -195,7 +195,7 @@ export const MainLayout: React.FC = () => {
}, [isMobile, setMobileSessionPanelOpen]); }, [isMobile, setMobileSessionPanelOpen]);
useEffect(() => { useEffect(() => {
if (!isMobile || activeMainTab !== 'chat' || mobileLeftDrawerOpen || mobileRightSidebarOpen || isSettingsDialogOpen) { if (!isMobile || activeSurface !== 'chat' || mobileLeftDrawerOpen || mobileRightSidebarOpen || isSettingsDialogOpen) {
return; return;
} }
@@ -231,7 +231,7 @@ export const MainLayout: React.FC = () => {
window.clearTimeout(timeoutId); window.clearTimeout(timeoutId);
} }
}; };
}, [activeMainTab, isMobile, isSettingsDialogOpen, mobileLeftDrawerOpen, mobileRightSidebarOpen]); }, [activeSurface, isMobile, isSettingsDialogOpen, mobileLeftDrawerOpen, mobileRightSidebarOpen]);
// Ensure mobile drawers are closed when opening full-screen settings // Ensure mobile drawers are closed when opening full-screen settings
useEffect(() => { useEffect(() => {
@@ -263,10 +263,10 @@ export const MainLayout: React.FC = () => {
// Desktop surfaces live in the context panel; the only full-view // Desktop surfaces live in the context panel; the only full-view
// overlays left there are the terminal (promoted by project actions) // overlays left there are the terminal (promoted by project actions)
// and the diagram viewer. Mobile keeps the full tab set. // and the diagram viewer. Mobile keeps the full tab set.
if (!isMobile && activeMainTab !== 'terminal' && activeMainTab !== 'diagram') { if (!isMobile && activeSurface !== 'terminal' && activeSurface !== 'diagram') {
return null; return null;
} }
switch (activeMainTab) { switch (activeSurface) {
case 'plan': case 'plan':
return <React.Suspense fallback={null}><PlanView /></React.Suspense>; return <React.Suspense fallback={null}><PlanView /></React.Suspense>;
case 'git': case 'git':
@@ -284,9 +284,9 @@ export const MainLayout: React.FC = () => {
default: default:
return null; return null;
} }
}, [activeMainTab, isMobile, mobileRightSidebarOpen]); }, [activeSurface, isMobile, mobileRightSidebarOpen]);
const isChatActive = activeMainTab === 'chat'; const isChatActive = activeSurface === 'chat';
return ( return (
<DiffWorkerProvider> <DiffWorkerProvider>
@@ -147,8 +147,8 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
terminalControllerRef.current?.focus(); terminalControllerRef.current?.focus();
}, [useTouchTerminalInput]); }, [useTouchTerminalInput]);
const activeMainTab = useUIStore((state) => state.activeMainTab); const activeSurface = useUIStore((state) => state.activeSurface);
const isTerminalActive = activeMainTab === 'terminal'; const isTerminalActive = activeSurface === 'terminal';
const isTerminalVisible = visible ?? isTerminalActive; const isTerminalVisible = visible ?? isTerminalActive;
const [hasOpenedTerminalViewport, setHasOpenedTerminalViewport] = React.useState(isTerminalVisible); const [hasOpenedTerminalViewport, setHasOpenedTerminalViewport] = React.useState(isTerminalVisible);
+19 -19
View File
@@ -3,7 +3,7 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { parseRoute, updateBrowserURL, hasRouteParams } from '@/lib/router'; import { parseRoute, updateBrowserURL, hasRouteParams } from '@/lib/router';
import type { RouteState, AppRouteState } from '@/lib/router'; import type { RouteState, AppRouteState } from '@/lib/router';
import type { MainTab } from '@/stores/useUIStore'; import type { WorkspaceSurface } from '@/stores/useUIStore';
import { resolveSettingsSlug } from '@/lib/settings/metadata'; import { resolveSettingsSlug } from '@/lib/settings/metadata';
import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat'; import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
@@ -49,7 +49,7 @@ export function useRouter(): void {
// Get store actions (stable references) // Get store actions (stable references)
const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession);
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen); const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
const setSettingsPage = useUIStore((state) => state.setSettingsPage); const setSettingsPage = useUIStore((state) => state.setSettingsPage);
const navigateToDiff = useUIStore((state) => state.navigateToDiff); const navigateToDiff = useUIStore((state) => state.navigateToDiff);
@@ -75,11 +75,11 @@ export function useRouter(): void {
} }
} }
// 2. Handle settings (takes precedence over tabs - it's a full-screen overlay) // 2. Handle settings first because it is a full-screen overlay.
if (route.settingsPath) { if (route.settingsPath) {
setSettingsPage(resolveSettingsSlug(route.settingsPath)); setSettingsPage(resolveSettingsSlug(route.settingsPath));
setSettingsDialogOpen(true); setSettingsDialogOpen(true);
// Don't process tab when settings is open // Do not process a route view while settings is open.
return; return;
} }
@@ -88,9 +88,9 @@ export function useRouter(): void {
setSettingsDialogOpen(false); setSettingsDialogOpen(false);
} }
// 3. Apply tab // 3. Apply the view selected by the legacy URL parameter.
if (route.tab) { if (route.tab) {
setActiveMainTab(route.tab); setActiveSurface(route.tab);
} }
// 4. Apply diff file (only if going to diff tab) // 4. Apply diff file (only if going to diff tab)
@@ -101,7 +101,7 @@ export function useRouter(): void {
isApplyingRouteRef.current = false; isApplyingRouteRef.current = false;
} }
}, },
[setCurrentSession, setActiveMainTab, setSettingsDialogOpen, setSettingsPage, navigateToDiff] [setCurrentSession, setActiveSurface, setSettingsDialogOpen, setSettingsPage, navigateToDiff]
); );
/** /**
@@ -113,7 +113,7 @@ export function useRouter(): void {
return { return {
sessionId: sessionState.currentSessionId, sessionId: sessionState.currentSessionId,
tab: uiState.activeMainTab, tab: uiState.activeSurface,
isSettingsOpen: uiState.isSettingsDialogOpen, isSettingsOpen: uiState.isSettingsDialogOpen,
settingsPath: uiState.settingsPage, settingsPath: uiState.settingsPage,
diffFile: uiState.pendingDiffFile, diffFile: uiState.pendingDiffFile,
@@ -162,7 +162,7 @@ export function useRouter(): void {
updateBrowserURL({ updateBrowserURL({
...getCurrentAppState(), ...getCurrentAppState(),
sessionId: route.sessionId ?? useSessionUIStore.getState().currentSessionId, sessionId: route.sessionId ?? useSessionUIStore.getState().currentSessionId,
tab: route.tab ?? useUIStore.getState().activeMainTab, tab: route.tab ?? useUIStore.getState().activeSurface,
settingsPath: route.settingsPath ?? useUIStore.getState().settingsPage, settingsPath: route.settingsPath ?? useUIStore.getState().settingsPage,
diffFile: route.diffFile ?? useUIStore.getState().pendingDiffFile, diffFile: route.diffFile ?? useUIStore.getState().pendingDiffFile,
}, { replace: true, force: true }); }, { replace: true, force: true });
@@ -195,13 +195,13 @@ export function useRouter(): void {
return unsubscribe; return unsubscribe;
}, [isVSCode, isEmbeddedChat, syncURLFromState]); }, [isVSCode, isEmbeddedChat, syncURLFromState]);
// Subscribe to UI store changes (tab, settings) // Subscribe to UI store changes (view, settings)
React.useEffect(() => { React.useEffect(() => {
if (isVSCode || isEmbeddedChat) { if (isVSCode || isEmbeddedChat) {
return; return;
} }
let prevTab: MainTab = useUIStore.getState().activeMainTab; let prevSurface: WorkspaceSurface = useUIStore.getState().activeSurface;
let prevSettingsOpen: boolean = useUIStore.getState().isSettingsDialogOpen; let prevSettingsOpen: boolean = useUIStore.getState().isSettingsDialogOpen;
let prevSettingsPath: string = useUIStore.getState().settingsPage; let prevSettingsPath: string = useUIStore.getState().settingsPage;
let prevDiffFile: string | null = useUIStore.getState().pendingDiffFile; let prevDiffFile: string | null = useUIStore.getState().pendingDiffFile;
@@ -212,19 +212,19 @@ export function useRouter(): void {
return; return;
} }
const tabChanged = state.activeMainTab !== prevTab; const surfaceChanged = state.activeSurface !== prevSurface;
const settingsOpenChanged = state.isSettingsDialogOpen !== prevSettingsOpen; const settingsOpenChanged = state.isSettingsDialogOpen !== prevSettingsOpen;
const settingsPathChanged = state.settingsPage !== prevSettingsPath; const settingsPathChanged = state.settingsPage !== prevSettingsPath;
const diffFileChanged = state.pendingDiffFile !== prevDiffFile && state.activeMainTab === 'diff'; const diffFileChanged = state.pendingDiffFile !== prevDiffFile && state.activeSurface === 'diff';
// Update tracking vars // Update tracking vars
prevTab = state.activeMainTab; prevSurface = state.activeSurface;
prevSettingsOpen = state.isSettingsDialogOpen; prevSettingsOpen = state.isSettingsDialogOpen;
prevSettingsPath = state.settingsPage; prevSettingsPath = state.settingsPage;
prevDiffFile = state.pendingDiffFile; prevDiffFile = state.pendingDiffFile;
// Only sync if something relevant changed // Only sync if something relevant changed
if (tabChanged || settingsOpenChanged || settingsPathChanged || diffFileChanged) { if (surfaceChanged || settingsOpenChanged || settingsPathChanged || diffFileChanged) {
syncURLFromState(); syncURLFromState();
} }
}); });
@@ -252,9 +252,9 @@ export function useRouter(): void {
if (uiState.isSettingsDialogOpen) { if (uiState.isSettingsDialogOpen) {
setSettingsDialogOpen(false); setSettingsDialogOpen(false);
} }
// Reset to chat tab if not already there // Reset to chat when no route view is specified.
if (uiState.activeMainTab !== 'chat') { if (uiState.activeSurface !== 'chat') {
setActiveMainTab('chat'); setActiveSurface('chat');
} }
} }
}; };
@@ -264,5 +264,5 @@ export function useRouter(): void {
return () => { return () => {
window.removeEventListener('popstate', handlePopState); window.removeEventListener('popstate', handlePopState);
}; };
}, [applyRoute, isVSCode, isEmbeddedChat, setActiveMainTab, setSettingsDialogOpen]); }, [applyRoute, isVSCode, isEmbeddedChat, setActiveSurface, setSettingsDialogOpen]);
} }
+3 -3
View File
@@ -6,15 +6,15 @@
* *
* URL Schema: * URL Schema:
* - `?session=<id>` - Navigate to specific session * - `?session=<id>` - Navigate to specific session
* - `?tab=<chat|git|diff|terminal|files>` - Active main tab * - `?tab=<chat|git|diff|terminal|files>` - Legacy URL name for the active workspace surface
* - `?settings=<section>` - Open settings to specific section * - `?settings=<section>` - Open settings to specific section
* - `?file=<path>` - Diff view with file selected * - `?file=<path>` - Diff view with file selected
* *
* Examples: * Examples:
* - `/?session=abc123` - Open session abc123 * - `/?session=abc123` - Open session abc123
* - `/?tab=git` - Open git tab * - `/?tab=git` - Open the Git surface
* - `/?settings=providers` - Open settings to providers section * - `/?settings=providers` - Open settings to providers section
* - `/?tab=diff&file=src/main.ts` - Open diff view with file * - `/?tab=diff&file=src/main.ts` - Open the Diff surface with a file
*/ */
export type { RouteState } from './types'; export type { RouteState } from './types';
+3 -3
View File
@@ -1,4 +1,4 @@
import type { MainTab } from '@/stores/useUIStore'; import type { WorkspaceSurface } from '@/stores/useUIStore';
import { import {
type RouteState, type RouteState,
VALID_TABS, VALID_TABS,
@@ -52,13 +52,13 @@ function parseSessionId(params: URLSearchParams): string | null {
* Parse main tab from URL parameters. * Parse main tab from URL parameters.
* Returns null if missing or invalid. * Returns null if missing or invalid.
*/ */
function parseTab(params: URLSearchParams): MainTab | null { function parseTab(params: URLSearchParams): WorkspaceSurface | null {
const value = params.get(ROUTE_PARAMS.TAB); const value = params.get(ROUTE_PARAMS.TAB);
if (!value) { if (!value) {
return null; return null;
} }
const normalized = value.toLowerCase().trim() as MainTab; const normalized = value.toLowerCase().trim() as WorkspaceSurface;
if (VALID_TABS.includes(normalized)) { if (VALID_TABS.includes(normalized)) {
return normalized; return normalized;
} }
+3 -3
View File
@@ -1,4 +1,4 @@
import type { MainTab } from '@/stores/useUIStore'; import type { WorkspaceSurface } from '@/stores/useUIStore';
import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat'; import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
import { ROUTE_PARAMS } from './types'; import { ROUTE_PARAMS } from './types';
@@ -7,7 +7,7 @@ import { ROUTE_PARAMS } from './types';
*/ */
export interface AppRouteState { export interface AppRouteState {
sessionId: string | null; sessionId: string | null;
tab: MainTab; tab: WorkspaceSurface;
isSettingsOpen: boolean; isSettingsOpen: boolean;
settingsPath: string; settingsPath: string;
diffFile: string | null; diffFile: string | null;
@@ -16,7 +16,7 @@ export interface AppRouteState {
/** /**
* Default tab when none is specified. * Default tab when none is specified.
*/ */
const DEFAULT_TAB: MainTab = 'chat'; const DEFAULT_TAB: WorkspaceSurface = 'chat';
/** /**
* Serialize application state to URL search parameters. * Serialize application state to URL search parameters.
+5 -5
View File
@@ -1,5 +1,5 @@
import type { SidebarSection } from '@/constants/sidebar'; import type { SidebarSection } from '@/constants/sidebar';
import type { MainTab } from '@/stores/useUIStore'; import type { WorkspaceSurface } from '@/stores/useUIStore';
/** /**
* Represents the current route state derived from URL parameters. * Represents the current route state derived from URL parameters.
@@ -8,8 +8,8 @@ import type { MainTab } from '@/stores/useUIStore';
export interface RouteState { export interface RouteState {
/** Session ID to navigate to */ /** Session ID to navigate to */
sessionId: string | null; sessionId: string | null;
/** Main tab to display (chat, git, diff, terminal, files) */ /** View selected through the legacy `tab` URL parameter. */
tab: MainTab | null; tab: WorkspaceSurface | null;
/** Settings section - when non-null, settings dialog should be open */ /** Settings section - when non-null, settings dialog should be open */
settingsPath: string | null; settingsPath: string | null;
/** File path for diff view */ /** File path for diff view */
@@ -17,9 +17,9 @@ export interface RouteState {
} }
/** /**
* Valid main tab values for URL routing. * Valid values for the legacy `tab` URL parameter.
*/ */
export const VALID_TABS: readonly MainTab[] = ['chat', 'git', 'diff', 'terminal', 'files', 'diagram'] as const; export const VALID_TABS: readonly WorkspaceSurface[] = ['chat', 'git', 'diff', 'terminal', 'files', 'diagram'] as const;
/** /**
* Valid settings section values for URL routing. * Valid settings section values for URL routing.
+1 -1
View File
@@ -38,7 +38,7 @@ Examples:
- `useFeatureFlagsStore.ts` - `useFeatureFlagsStore.ts`
- `useUpdateStore.ts` - `useUpdateStore.ts`
These stores coordinate visible app state, navigation, selected tabs, dialogs, and lightweight feature flags. These stores coordinate visible app state, navigation, selected context-panel tabs, dialogs, and lightweight feature flags. `useUIStore.activeSurface` selects the primary mobile view and the few desktop views that are promoted out of the context panel. It is not a desktop tab selection.
Context-panel session chats mount only the active chat iframe. After installing Context-panel session chats mount only the active chat iframe. After installing
its message listener, the iframe requests its authoritative visibility from the its message listener, the iframe requests its authoritative visibility from the
+55 -24
View File
@@ -13,7 +13,13 @@ import { useFilesViewTabsStore } from './useFilesViewTabsStore';
import { isWindowsArm64 } from '@/lib/platform'; import { isWindowsArm64 } from '@/lib/platform';
import { isVSCodeRuntime } from '@/lib/desktop'; import { isVSCodeRuntime } from '@/lib/desktop';
export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files' | 'context' | 'diagram'; /**
* The primary view on mobile and the desktop's promoted full-screen view.
* Desktop context-panel content is not represented here.
*/
export type WorkspaceSurface = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files' | 'context' | 'diagram';
/** @deprecated Use WorkspaceSurface. */
export type MainTab = WorkspaceSurface;
export type PendingDiffScope = 'working' | 'staged' | 'turn'; export type PendingDiffScope = 'working' | 'staged' | 'turn';
export type ContextPanelMode = 'diff' | 'walkthrough' | 'file' | 'context' | 'plan' | 'chat' | 'browser' | 'git' | 'pr' | 'notes' | 'terminal'; export type ContextPanelMode = 'diff' | 'walkthrough' | 'file' | 'context' | 'plan' | 'chat' | 'browser' | 'git' | 'pr' | 'notes' | 'terminal';
export type MermaidRenderingMode = 'svg' | 'ascii'; export type MermaidRenderingMode = 'svg' | 'ascii';
@@ -77,7 +83,9 @@ type PendingFileNavigation = {
column: number; column: number;
}; };
export type MainTabGuard = (nextTab: MainTab) => boolean; export type WorkspaceSurfaceGuard = (nextSurface: WorkspaceSurface) => boolean;
/** @deprecated Use WorkspaceSurfaceGuard. */
export type MainTabGuard = WorkspaceSurfaceGuard;
export type EventStreamStatus = export type EventStreamStatus =
| 'idle' | 'idle'
| 'connecting' | 'connecting'
@@ -129,7 +137,7 @@ const CONTEXT_PANEL_MAX_WIDTH = 1400;
const CONTEXT_PANEL_MAX_TABS = 12; const CONTEXT_PANEL_MAX_TABS = 12;
const CONTEXT_PANEL_MAX_LABEL_LENGTH = 120; const CONTEXT_PANEL_MAX_LABEL_LENGTH = 120;
const LEFT_SIDEBAR_MIN_WIDTH = 280; const LEFT_SIDEBAR_MIN_WIDTH = 280;
const activeMainTabByRuntime = new Map<string, MainTab>(); const activeSurfaceByRuntime = new Map<string, WorkspaceSurface>();
/** Separates browser tabs opened in the same millisecond. */ /** Separates browser tabs opened in the same millisecond. */
let browserTabSequence = 0; let browserTabSequence = 0;
@@ -648,8 +656,12 @@ interface UIStore {
workStatusHiddenSections: string[]; workStatusHiddenSections: string[];
isSessionSwitcherOpen: boolean; isSessionSwitcherOpen: boolean;
isSessionDropdownOpen: boolean; isSessionDropdownOpen: boolean;
activeMainTab: MainTab; activeSurface: WorkspaceSurface;
mainTabGuard: MainTabGuard | null; surfaceGuard: WorkspaceSurfaceGuard | null;
/** @deprecated Use activeSurface. */
activeMainTab: WorkspaceSurface;
/** @deprecated Use surfaceGuard. */
mainTabGuard: WorkspaceSurfaceGuard | null;
sidebarOpenBeforeFullscreenTab: boolean | null; sidebarOpenBeforeFullscreenTab: boolean | null;
pendingDiffFile: string | null; pendingDiffFile: string | null;
pendingDiffStaged: boolean; pendingDiffStaged: boolean;
@@ -834,10 +846,14 @@ interface UIStore {
setWorkStatusHiddenSections: (sectionIds: string[]) => void; setWorkStatusHiddenSections: (sectionIds: string[]) => void;
setSessionSwitcherOpen: (open: boolean) => void; setSessionSwitcherOpen: (open: boolean) => void;
setSessionDropdownOpen: (open: boolean) => void; setSessionDropdownOpen: (open: boolean) => void;
setActiveMainTab: (tab: MainTab) => void; setActiveSurface: (surface: WorkspaceSurface) => void;
/** @deprecated Use setActiveSurface. */
setActiveMainTab: (surface: WorkspaceSurface) => void;
prepareForRuntimeSwitch: (runtimeKey?: string | null) => void; prepareForRuntimeSwitch: (runtimeKey?: string | null) => void;
restoreForRuntimeSwitch: (runtimeKey?: string | null) => void; restoreForRuntimeSwitch: (runtimeKey?: string | null) => void;
setMainTabGuard: (guard: MainTabGuard | null) => void; setSurfaceGuard: (guard: WorkspaceSurfaceGuard | null) => void;
/** @deprecated Use setSurfaceGuard. */
setMainTabGuard: (guard: WorkspaceSurfaceGuard | null) => void;
setPendingDiffFile: (filePath: string | null, staged?: boolean, scope?: PendingDiffScope | null) => void; setPendingDiffFile: (filePath: string | null, staged?: boolean, scope?: PendingDiffScope | null) => void;
setPendingDiagramFile: (filePath: string | null) => void; setPendingDiagramFile: (filePath: string | null) => void;
setPendingFileNavigation: (navigation: PendingFileNavigation | null) => void; setPendingFileNavigation: (navigation: PendingFileNavigation | null) => void;
@@ -1011,6 +1027,8 @@ export const useUIStore = create<UIStore>()(
workStatusHiddenSections: [], workStatusHiddenSections: [],
isSessionSwitcherOpen: false, isSessionSwitcherOpen: false,
isSessionDropdownOpen: false, isSessionDropdownOpen: false,
activeSurface: 'chat',
surfaceGuard: null,
activeMainTab: 'chat', activeMainTab: 'chat',
mainTabGuard: null, mainTabGuard: null,
sidebarOpenBeforeFullscreenTab: null, sidebarOpenBeforeFullscreenTab: null,
@@ -1625,29 +1643,33 @@ export const useUIStore = create<UIStore>()(
set({ isSessionDropdownOpen: open }); set({ isSessionDropdownOpen: open });
}, },
setMainTabGuard: (guard) => { setSurfaceGuard: (guard) => {
if (get().mainTabGuard === guard) { if (get().surfaceGuard === guard) {
return; return;
} }
set({ mainTabGuard: guard }); set({ surfaceGuard: guard, mainTabGuard: guard });
}, },
setActiveMainTab: (tab) => { setMainTabGuard: (guard) => get().setSurfaceGuard(guard),
const guard = get().mainTabGuard;
if (guard && !guard(tab)) { setActiveSurface: (surface) => {
const guard = get().surfaceGuard;
if (guard && !guard(surface)) {
return; return;
} }
activeMainTabByRuntime.set(runtimeMemoryKey(), tab); activeSurfaceByRuntime.set(runtimeMemoryKey(), surface);
set({ activeMainTab: tab }); set({ activeSurface: surface, activeMainTab: surface });
}, },
setActiveMainTab: (surface) => get().setActiveSurface(surface),
prepareForRuntimeSwitch: (runtimeKey?: string | null) => { prepareForRuntimeSwitch: (runtimeKey?: string | null) => {
activeMainTabByRuntime.set(runtimeMemoryKey(runtimeKey), get().activeMainTab); activeSurfaceByRuntime.set(runtimeMemoryKey(runtimeKey), get().activeSurface);
}, },
restoreForRuntimeSwitch: (runtimeKey?: string | null) => { restoreForRuntimeSwitch: (runtimeKey?: string | null) => {
const restored = activeMainTabByRuntime.get(runtimeMemoryKey(runtimeKey)) ?? 'chat'; const restored = activeSurfaceByRuntime.get(runtimeMemoryKey(runtimeKey)) ?? 'chat';
set({ activeMainTab: restored }); set({ activeSurface: restored, activeMainTab: restored });
}, },
setPendingDiffFile: (filePath, staged = false, scope = null) => { setPendingDiffFile: (filePath, staged = false, scope = null) => {
@@ -1671,11 +1693,11 @@ export const useUIStore = create<UIStore>()(
}, },
navigateToDiff: (filePath, staged = false, scope = null) => { navigateToDiff: (filePath, staged = false, scope = null) => {
const guard = get().mainTabGuard; const guard = get().surfaceGuard;
if (guard && !guard('diff')) { if (guard && !guard('diff')) {
return; return;
} }
set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeMainTab: 'diff' }); set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeSurface: 'diff', activeMainTab: 'diff' });
}, },
consumePendingDiffFile: () => { consumePendingDiffFile: () => {
@@ -1687,11 +1709,11 @@ export const useUIStore = create<UIStore>()(
}, },
navigateToDiagram: (filePath) => { navigateToDiagram: (filePath) => {
const guard = get().mainTabGuard; const guard = get().surfaceGuard;
if (guard && !guard('diagram')) { if (guard && !guard('diagram')) {
return; return;
} }
set({ pendingDiagramFile: filePath, activeMainTab: 'diagram' }); set({ pendingDiagramFile: filePath, activeSurface: 'diagram', activeMainTab: 'diagram' });
}, },
consumePendingDiagramFile: () => { consumePendingDiagramFile: () => {
@@ -2462,13 +2484,20 @@ export const useUIStore = create<UIStore>()(
{ {
name: 'ui-store', name: 'ui-store',
storage: createDeferredSafeJSONStorage(), storage: createDeferredSafeJSONStorage(),
version: 14, version: 15,
migrate: (persistedState, version) => { migrate: (persistedState, version) => {
if (!persistedState || typeof persistedState !== 'object') { if (!persistedState || typeof persistedState !== 'object') {
return persistedState; return persistedState;
} }
const state = persistedState as Record<string, unknown>; const state = persistedState as Record<string, unknown>;
// v14 -> v15: rename the historic main-tab field. The selected
// mobile or promoted desktop view remains unchanged.
if (version < 15) {
state.activeSurface = state.activeMainTab;
state.activeMainTab = state.activeSurface;
}
// v13 -> v14: the separate 'preview' surface merged into 'browser'. // v13 -> v14: the separate 'preview' surface merged into 'browser'.
// Stored preview tabs keep their URL and become browser tabs; their // Stored preview tabs keep their URL and become browser tabs; their
// id encodes the mode, so it is rebuilt rather than left dangling. // id encodes the mode, so it is rebuilt rather than left dangling.
@@ -2674,7 +2703,9 @@ export const useUIStore = create<UIStore>()(
workStatusPanelEnabled: state.workStatusPanelEnabled, workStatusPanelEnabled: state.workStatusPanelEnabled,
workStatusHiddenSections: state.workStatusHiddenSections, workStatusHiddenSections: state.workStatusHiddenSections,
isSessionSwitcherOpen: state.isSessionSwitcherOpen, isSessionSwitcherOpen: state.isSessionSwitcherOpen,
activeMainTab: state.activeMainTab, activeSurface: state.activeSurface,
// Keep the deprecated mirror synchronized while consumers migrate.
activeMainTab: state.activeSurface,
sidebarSection: state.sidebarSection, sidebarSection: state.sidebarSection,
settingsPage: state.settingsPage, settingsPage: state.settingsPage,
settingsHasOpenedOnce: state.settingsHasOpenedOnce, settingsHasOpenedOnce: state.settingsHasOpenedOnce,