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
+1 -1
View File
@@ -38,7 +38,7 @@ Examples:
- `useFeatureFlagsStore.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
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 { 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 ContextPanelMode = 'diff' | 'walkthrough' | 'file' | 'context' | 'plan' | 'chat' | 'browser' | 'git' | 'pr' | 'notes' | 'terminal';
export type MermaidRenderingMode = 'svg' | 'ascii';
@@ -77,7 +83,9 @@ type PendingFileNavigation = {
column: number;
};
export type MainTabGuard = (nextTab: MainTab) => boolean;
export type WorkspaceSurfaceGuard = (nextSurface: WorkspaceSurface) => boolean;
/** @deprecated Use WorkspaceSurfaceGuard. */
export type MainTabGuard = WorkspaceSurfaceGuard;
export type EventStreamStatus =
| 'idle'
| 'connecting'
@@ -129,7 +137,7 @@ const CONTEXT_PANEL_MAX_WIDTH = 1400;
const CONTEXT_PANEL_MAX_TABS = 12;
const CONTEXT_PANEL_MAX_LABEL_LENGTH = 120;
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. */
let browserTabSequence = 0;
@@ -648,8 +656,12 @@ interface UIStore {
workStatusHiddenSections: string[];
isSessionSwitcherOpen: boolean;
isSessionDropdownOpen: boolean;
activeMainTab: MainTab;
mainTabGuard: MainTabGuard | null;
activeSurface: WorkspaceSurface;
surfaceGuard: WorkspaceSurfaceGuard | null;
/** @deprecated Use activeSurface. */
activeMainTab: WorkspaceSurface;
/** @deprecated Use surfaceGuard. */
mainTabGuard: WorkspaceSurfaceGuard | null;
sidebarOpenBeforeFullscreenTab: boolean | null;
pendingDiffFile: string | null;
pendingDiffStaged: boolean;
@@ -834,10 +846,14 @@ interface UIStore {
setWorkStatusHiddenSections: (sectionIds: string[]) => void;
setSessionSwitcherOpen: (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;
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;
setPendingDiagramFile: (filePath: string | null) => void;
setPendingFileNavigation: (navigation: PendingFileNavigation | null) => void;
@@ -1011,6 +1027,8 @@ export const useUIStore = create<UIStore>()(
workStatusHiddenSections: [],
isSessionSwitcherOpen: false,
isSessionDropdownOpen: false,
activeSurface: 'chat',
surfaceGuard: null,
activeMainTab: 'chat',
mainTabGuard: null,
sidebarOpenBeforeFullscreenTab: null,
@@ -1625,29 +1643,33 @@ export const useUIStore = create<UIStore>()(
set({ isSessionDropdownOpen: open });
},
setMainTabGuard: (guard) => {
if (get().mainTabGuard === guard) {
setSurfaceGuard: (guard) => {
if (get().surfaceGuard === guard) {
return;
}
set({ mainTabGuard: guard });
set({ surfaceGuard: guard, mainTabGuard: guard });
},
setActiveMainTab: (tab) => {
const guard = get().mainTabGuard;
if (guard && !guard(tab)) {
setMainTabGuard: (guard) => get().setSurfaceGuard(guard),
setActiveSurface: (surface) => {
const guard = get().surfaceGuard;
if (guard && !guard(surface)) {
return;
}
activeMainTabByRuntime.set(runtimeMemoryKey(), tab);
set({ activeMainTab: tab });
activeSurfaceByRuntime.set(runtimeMemoryKey(), surface);
set({ activeSurface: surface, activeMainTab: surface });
},
setActiveMainTab: (surface) => get().setActiveSurface(surface),
prepareForRuntimeSwitch: (runtimeKey?: string | null) => {
activeMainTabByRuntime.set(runtimeMemoryKey(runtimeKey), get().activeMainTab);
activeSurfaceByRuntime.set(runtimeMemoryKey(runtimeKey), get().activeSurface);
},
restoreForRuntimeSwitch: (runtimeKey?: string | null) => {
const restored = activeMainTabByRuntime.get(runtimeMemoryKey(runtimeKey)) ?? 'chat';
set({ activeMainTab: restored });
const restored = activeSurfaceByRuntime.get(runtimeMemoryKey(runtimeKey)) ?? 'chat';
set({ activeSurface: restored, activeMainTab: restored });
},
setPendingDiffFile: (filePath, staged = false, scope = null) => {
@@ -1671,11 +1693,11 @@ export const useUIStore = create<UIStore>()(
},
navigateToDiff: (filePath, staged = false, scope = null) => {
const guard = get().mainTabGuard;
const guard = get().surfaceGuard;
if (guard && !guard('diff')) {
return;
}
set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeMainTab: 'diff' });
set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeSurface: 'diff', activeMainTab: 'diff' });
},
consumePendingDiffFile: () => {
@@ -1687,11 +1709,11 @@ export const useUIStore = create<UIStore>()(
},
navigateToDiagram: (filePath) => {
const guard = get().mainTabGuard;
const guard = get().surfaceGuard;
if (guard && !guard('diagram')) {
return;
}
set({ pendingDiagramFile: filePath, activeMainTab: 'diagram' });
set({ pendingDiagramFile: filePath, activeSurface: 'diagram', activeMainTab: 'diagram' });
},
consumePendingDiagramFile: () => {
@@ -2462,13 +2484,20 @@ export const useUIStore = create<UIStore>()(
{
name: 'ui-store',
storage: createDeferredSafeJSONStorage(),
version: 14,
version: 15,
migrate: (persistedState, version) => {
if (!persistedState || typeof persistedState !== 'object') {
return persistedState;
}
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'.
// Stored preview tabs keep their URL and become browser tabs; their
// id encodes the mode, so it is rebuilt rather than left dangling.
@@ -2674,7 +2703,9 @@ export const useUIStore = create<UIStore>()(
workStatusPanelEnabled: state.workStatusPanelEnabled,
workStatusHiddenSections: state.workStatusHiddenSections,
isSessionSwitcherOpen: state.isSessionSwitcherOpen,
activeMainTab: state.activeMainTab,
activeSurface: state.activeSurface,
// Keep the deprecated mirror synchronized while consumers migrate.
activeMainTab: state.activeSurface,
sidebarSection: state.sidebarSection,
settingsPage: state.settingsPage,
settingsHasOpenedOnce: state.settingsHasOpenedOnce,