refactor(ui): name workspace surfaces
This commit is contained in:
@@ -12,7 +12,6 @@ import { useI18n } from '@/lib/i18n';
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||
import { focusChatInput } from '@/components/chat/composer/editor/dom';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
|
||||
type LineRangeBase = {
|
||||
start: number;
|
||||
@@ -164,7 +163,6 @@ export function useInlineCommentController<TRange extends LineRangeBase>(
|
||||
|
||||
reset();
|
||||
if (isNewComment) {
|
||||
useUIStore.getState().setActiveMainTab('chat');
|
||||
requestAnimationFrame(focusChatInput);
|
||||
}
|
||||
}, [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 = () => {
|
||||
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 isSessionSwitcherOpen = useUIStore((state) => state.isSessionSwitcherOpen);
|
||||
const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen);
|
||||
@@ -83,7 +83,7 @@ export const MainLayout: React.FC = () => {
|
||||
if (sessionSelected || draftOpened) closeSurfacePages();
|
||||
});
|
||||
const unsubscribeTab = useUIStore.subscribe((state, prev) => {
|
||||
if (state.activeMainTab !== prev.activeMainTab) closeSurfacePages();
|
||||
if (state.activeSurface !== prev.activeSurface) closeSurfacePages();
|
||||
});
|
||||
return () => {
|
||||
unsubscribeSession();
|
||||
@@ -195,7 +195,7 @@ export const MainLayout: React.FC = () => {
|
||||
}, [isMobile, setMobileSessionPanelOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobile || activeMainTab !== 'chat' || mobileLeftDrawerOpen || mobileRightSidebarOpen || isSettingsDialogOpen) {
|
||||
if (!isMobile || activeSurface !== 'chat' || mobileLeftDrawerOpen || mobileRightSidebarOpen || isSettingsDialogOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ export const MainLayout: React.FC = () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [activeMainTab, isMobile, isSettingsDialogOpen, mobileLeftDrawerOpen, mobileRightSidebarOpen]);
|
||||
}, [activeSurface, isMobile, isSettingsDialogOpen, mobileLeftDrawerOpen, mobileRightSidebarOpen]);
|
||||
|
||||
// Ensure mobile drawers are closed when opening full-screen settings
|
||||
useEffect(() => {
|
||||
@@ -263,10 +263,10 @@ export const MainLayout: React.FC = () => {
|
||||
// Desktop surfaces live in the context panel; the only full-view
|
||||
// overlays left there are the terminal (promoted by project actions)
|
||||
// and the diagram viewer. Mobile keeps the full tab set.
|
||||
if (!isMobile && activeMainTab !== 'terminal' && activeMainTab !== 'diagram') {
|
||||
if (!isMobile && activeSurface !== 'terminal' && activeSurface !== 'diagram') {
|
||||
return null;
|
||||
}
|
||||
switch (activeMainTab) {
|
||||
switch (activeSurface) {
|
||||
case 'plan':
|
||||
return <React.Suspense fallback={null}><PlanView /></React.Suspense>;
|
||||
case 'git':
|
||||
@@ -284,9 +284,9 @@ export const MainLayout: React.FC = () => {
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [activeMainTab, isMobile, mobileRightSidebarOpen]);
|
||||
}, [activeSurface, isMobile, mobileRightSidebarOpen]);
|
||||
|
||||
const isChatActive = activeMainTab === 'chat';
|
||||
const isChatActive = activeSurface === 'chat';
|
||||
|
||||
return (
|
||||
<DiffWorkerProvider>
|
||||
|
||||
@@ -147,8 +147,8 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
|
||||
terminalControllerRef.current?.focus();
|
||||
}, [useTouchTerminalInput]);
|
||||
|
||||
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
||||
const isTerminalActive = activeMainTab === 'terminal';
|
||||
const activeSurface = useUIStore((state) => state.activeSurface);
|
||||
const isTerminalActive = activeSurface === 'terminal';
|
||||
const isTerminalVisible = visible ?? isTerminalActive;
|
||||
const [hasOpenedTerminalViewport, setHasOpenedTerminalViewport] = React.useState(isTerminalVisible);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { parseRoute, updateBrowserURL, hasRouteParams } 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 { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
|
||||
|
||||
@@ -49,7 +49,7 @@ export function useRouter(): void {
|
||||
|
||||
// Get store actions (stable references)
|
||||
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 setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
||||
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) {
|
||||
setSettingsPage(resolveSettingsSlug(route.settingsPath));
|
||||
setSettingsDialogOpen(true);
|
||||
// Don't process tab when settings is open
|
||||
// Do not process a route view while settings is open.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,9 +88,9 @@ export function useRouter(): void {
|
||||
setSettingsDialogOpen(false);
|
||||
}
|
||||
|
||||
// 3. Apply tab
|
||||
// 3. Apply the view selected by the legacy URL parameter.
|
||||
if (route.tab) {
|
||||
setActiveMainTab(route.tab);
|
||||
setActiveSurface(route.tab);
|
||||
}
|
||||
|
||||
// 4. Apply diff file (only if going to diff tab)
|
||||
@@ -101,7 +101,7 @@ export function useRouter(): void {
|
||||
isApplyingRouteRef.current = false;
|
||||
}
|
||||
},
|
||||
[setCurrentSession, setActiveMainTab, setSettingsDialogOpen, setSettingsPage, navigateToDiff]
|
||||
[setCurrentSession, setActiveSurface, setSettingsDialogOpen, setSettingsPage, navigateToDiff]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -113,7 +113,7 @@ export function useRouter(): void {
|
||||
|
||||
return {
|
||||
sessionId: sessionState.currentSessionId,
|
||||
tab: uiState.activeMainTab,
|
||||
tab: uiState.activeSurface,
|
||||
isSettingsOpen: uiState.isSettingsDialogOpen,
|
||||
settingsPath: uiState.settingsPage,
|
||||
diffFile: uiState.pendingDiffFile,
|
||||
@@ -162,7 +162,7 @@ export function useRouter(): void {
|
||||
updateBrowserURL({
|
||||
...getCurrentAppState(),
|
||||
sessionId: route.sessionId ?? useSessionUIStore.getState().currentSessionId,
|
||||
tab: route.tab ?? useUIStore.getState().activeMainTab,
|
||||
tab: route.tab ?? useUIStore.getState().activeSurface,
|
||||
settingsPath: route.settingsPath ?? useUIStore.getState().settingsPage,
|
||||
diffFile: route.diffFile ?? useUIStore.getState().pendingDiffFile,
|
||||
}, { replace: true, force: true });
|
||||
@@ -195,13 +195,13 @@ export function useRouter(): void {
|
||||
return unsubscribe;
|
||||
}, [isVSCode, isEmbeddedChat, syncURLFromState]);
|
||||
|
||||
// Subscribe to UI store changes (tab, settings)
|
||||
// Subscribe to UI store changes (view, settings)
|
||||
React.useEffect(() => {
|
||||
if (isVSCode || isEmbeddedChat) {
|
||||
return;
|
||||
}
|
||||
|
||||
let prevTab: MainTab = useUIStore.getState().activeMainTab;
|
||||
let prevSurface: WorkspaceSurface = useUIStore.getState().activeSurface;
|
||||
let prevSettingsOpen: boolean = useUIStore.getState().isSettingsDialogOpen;
|
||||
let prevSettingsPath: string = useUIStore.getState().settingsPage;
|
||||
let prevDiffFile: string | null = useUIStore.getState().pendingDiffFile;
|
||||
@@ -212,19 +212,19 @@ export function useRouter(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabChanged = state.activeMainTab !== prevTab;
|
||||
const surfaceChanged = state.activeSurface !== prevSurface;
|
||||
const settingsOpenChanged = state.isSettingsDialogOpen !== prevSettingsOpen;
|
||||
const settingsPathChanged = state.settingsPage !== prevSettingsPath;
|
||||
const diffFileChanged = state.pendingDiffFile !== prevDiffFile && state.activeMainTab === 'diff';
|
||||
const diffFileChanged = state.pendingDiffFile !== prevDiffFile && state.activeSurface === 'diff';
|
||||
|
||||
// Update tracking vars
|
||||
prevTab = state.activeMainTab;
|
||||
prevSurface = state.activeSurface;
|
||||
prevSettingsOpen = state.isSettingsDialogOpen;
|
||||
prevSettingsPath = state.settingsPage;
|
||||
prevDiffFile = state.pendingDiffFile;
|
||||
|
||||
// Only sync if something relevant changed
|
||||
if (tabChanged || settingsOpenChanged || settingsPathChanged || diffFileChanged) {
|
||||
if (surfaceChanged || settingsOpenChanged || settingsPathChanged || diffFileChanged) {
|
||||
syncURLFromState();
|
||||
}
|
||||
});
|
||||
@@ -252,9 +252,9 @@ export function useRouter(): void {
|
||||
if (uiState.isSettingsDialogOpen) {
|
||||
setSettingsDialogOpen(false);
|
||||
}
|
||||
// Reset to chat tab if not already there
|
||||
if (uiState.activeMainTab !== 'chat') {
|
||||
setActiveMainTab('chat');
|
||||
// Reset to chat when no route view is specified.
|
||||
if (uiState.activeSurface !== 'chat') {
|
||||
setActiveSurface('chat');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -264,5 +264,5 @@ export function useRouter(): void {
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handlePopState);
|
||||
};
|
||||
}, [applyRoute, isVSCode, isEmbeddedChat, setActiveMainTab, setSettingsDialogOpen]);
|
||||
}, [applyRoute, isVSCode, isEmbeddedChat, setActiveSurface, setSettingsDialogOpen]);
|
||||
}
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
*
|
||||
* URL Schema:
|
||||
* - `?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
|
||||
* - `?file=<path>` - Diff view with file selected
|
||||
*
|
||||
* Examples:
|
||||
* - `/?session=abc123` - Open session abc123
|
||||
* - `/?tab=git` - Open git tab
|
||||
* - `/?tab=git` - Open the Git surface
|
||||
* - `/?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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MainTab } from '@/stores/useUIStore';
|
||||
import type { WorkspaceSurface } from '@/stores/useUIStore';
|
||||
import {
|
||||
type RouteState,
|
||||
VALID_TABS,
|
||||
@@ -52,13 +52,13 @@ function parseSessionId(params: URLSearchParams): string | null {
|
||||
* Parse main tab from URL parameters.
|
||||
* 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);
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = value.toLowerCase().trim() as MainTab;
|
||||
const normalized = value.toLowerCase().trim() as WorkspaceSurface;
|
||||
if (VALID_TABS.includes(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MainTab } from '@/stores/useUIStore';
|
||||
import type { WorkspaceSurface } from '@/stores/useUIStore';
|
||||
import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
|
||||
import { ROUTE_PARAMS } from './types';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ROUTE_PARAMS } from './types';
|
||||
*/
|
||||
export interface AppRouteState {
|
||||
sessionId: string | null;
|
||||
tab: MainTab;
|
||||
tab: WorkspaceSurface;
|
||||
isSettingsOpen: boolean;
|
||||
settingsPath: string;
|
||||
diffFile: string | null;
|
||||
@@ -16,7 +16,7 @@ export interface AppRouteState {
|
||||
/**
|
||||
* Default tab when none is specified.
|
||||
*/
|
||||
const DEFAULT_TAB: MainTab = 'chat';
|
||||
const DEFAULT_TAB: WorkspaceSurface = 'chat';
|
||||
|
||||
/**
|
||||
* Serialize application state to URL search parameters.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.
|
||||
@@ -8,8 +8,8 @@ import type { MainTab } from '@/stores/useUIStore';
|
||||
export interface RouteState {
|
||||
/** Session ID to navigate to */
|
||||
sessionId: string | null;
|
||||
/** Main tab to display (chat, git, diff, terminal, files) */
|
||||
tab: MainTab | null;
|
||||
/** View selected through the legacy `tab` URL parameter. */
|
||||
tab: WorkspaceSurface | null;
|
||||
/** Settings section - when non-null, settings dialog should be open */
|
||||
settingsPath: string | null;
|
||||
/** 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user