feat(ui): redesign workspace shell with context panel, tabbed sidebars, and faster diff UX (#433)
* feat: tabbed right sidebar, context panel, floating diff comments * fix: auto-close left sidebar when context panel opens - Increase default context panel width from 520 to 600 pixels - Increase sidebar minimum width from 200 to 300 pixels - Replace collapsible component with custom button in diff view * refactoring: rework sidebars, tabs, and file tree layout - Rewrite AnimatedTabs as segment-style with sliding indicator - Upgrade SidebarFilesTree to match FilesView features (context menus, git status, file icons, CRUD dialogs, fuzzy search ranking) - Restructure FilesView header: tabs row + actions row, remove breadcrumbs - Show relative path in context panel header, track active tab - Allow left sidebar to stay open alongside context panel - Hide diff/files tabs from header on desktop (mobile-only) - Move chevron after group name in session sidebar - Compact tab heights in right sidebar and git view - Size PreviewToggleButton to match other action buttons - Remove directory loading spinner from folder icons * feat: add project icon and color customization - Enable users to assign custom icons to projects - Allow users to choose accent colors for projects - Stabilize repo status UI during project switching * feat: add scroll fade indicators to editor tabs * style: reduce spacing and icon sizes in header * style: adjust tab component padding from uniform to vertical-horizontal * feat: Add session state indicators to project tabs * feat: Enhance session status handling and improve UI responsiveness * fix: preserve upstream tracking on branch rename * fix: improve initial remote selection for pull requests - Uses saved remote name from previous session when available - Selects remote based on tracking branch when possible - Falls back to origin or first available remote * perf(diff): faster highlight, stable stacked scroll - split/unified Pierre worker pools; prefer shiki-wasm - align diff CSS line-height; disable scroll anchoring; drop WebKit compositing hacks - harden stacked pin/align (cancel on user scroll/input); prevent overscroll - make overlay scrollbar MutationObserver optional; disable for diff container * feat: handle binary files in diff view * fix: adjust project tabs layout and drag regions * style: update drag overlay visual styling * feat: enable number keys to switch projects in the sidebar * fix: recognize octet-stream as text-based MIME type * feat: add keyboard navigation to context panel * feat: add session pinning to sidebar - Pin important sessions to keep them at the top - Pinned sessions persist across browser sessions * refactor: move context usage display from chat input to header
This commit is contained in:
committed by
GitHub
parent
12606b9e53
commit
47c943b487
@@ -25,7 +25,7 @@ interface DirectoryGitState {
|
||||
branches: GitBranch | null;
|
||||
log: GitLogResponse | null;
|
||||
identity: GitIdentitySummary | null;
|
||||
diffCache: Map<string, { original: string; modified: string; fetchedAt: number }>;
|
||||
diffCache: Map<string, { original: string; modified: string; fetchedAt: number; isBinary?: boolean }>;
|
||||
lastStatusFetch: number;
|
||||
lastStatusChange: number;
|
||||
lastLogFetch: number;
|
||||
@@ -55,8 +55,8 @@ interface GitStore {
|
||||
fetchIdentity: (directory: string, git: GitAPI) => Promise<void>;
|
||||
fetchAll: (directory: string, git: GitAPI, options?: { force?: boolean }) => Promise<void>;
|
||||
|
||||
getDiff: (directory: string, filePath: string) => { original: string; modified: string; fetchedAt: number } | null;
|
||||
setDiff: (directory: string, filePath: string, diff: { original: string; modified: string }) => void;
|
||||
getDiff: (directory: string, filePath: string) => { original: string; modified: string; fetchedAt: number; isBinary?: boolean } | null;
|
||||
setDiff: (directory: string, filePath: string, diff: { original: string; modified: string; isBinary?: boolean }) => void;
|
||||
clearDiffCache: (directory: string) => void;
|
||||
fetchAllDiffs: (directory: string, git: GitAPI) => Promise<void>;
|
||||
|
||||
@@ -72,6 +72,7 @@ interface GitFileDiffResponse {
|
||||
original: string;
|
||||
modified: string;
|
||||
path: string;
|
||||
isBinary?: boolean;
|
||||
}
|
||||
|
||||
interface GitAPI {
|
||||
@@ -98,10 +99,10 @@ const createEmptyDirectoryState = (): DirectoryGitState => ({
|
||||
|
||||
// LRU eviction helper for diff cache
|
||||
const evictDiffCacheIfNeeded = (
|
||||
diffCache: Map<string, { original: string; modified: string; fetchedAt: number }>,
|
||||
diffCache: Map<string, { original: string; modified: string; fetchedAt: number; isBinary?: boolean }>,
|
||||
maxEntries: number = DIFF_CACHE_MAX_ENTRIES,
|
||||
maxTotalSize: number = DIFF_CACHE_MAX_TOTAL_SIZE_BYTES
|
||||
): Map<string, { original: string; modified: string; fetchedAt: number }> => {
|
||||
): Map<string, { original: string; modified: string; fetchedAt: number; isBinary?: boolean }> => {
|
||||
// Calculate total size
|
||||
let totalSize = 0;
|
||||
for (const entry of diffCache.values()) {
|
||||
@@ -117,7 +118,7 @@ const evictDiffCacheIfNeeded = (
|
||||
const entries = Array.from(diffCache.entries())
|
||||
.sort((a, b) => a[1].fetchedAt - b[1].fetchedAt);
|
||||
|
||||
const newCache = new Map<string, { original: string; modified: string; fetchedAt: number }>();
|
||||
const newCache = new Map<string, { original: string; modified: string; fetchedAt: number; isBinary?: boolean }>();
|
||||
let newTotalSize = 0;
|
||||
|
||||
// Keep entries from newest to oldest until limits are reached
|
||||
@@ -441,6 +442,7 @@ export const useGitStore = create<GitStore>()(
|
||||
|
||||
// Pre-fetch all diffs so they're ready when user opens Diff tab
|
||||
void get().fetchAllDiffs(directory, git);
|
||||
|
||||
},
|
||||
|
||||
getDiff: (directory, filePath) => {
|
||||
@@ -481,7 +483,7 @@ export const useGitStore = create<GitStore>()(
|
||||
if (limitedFilesToFetch.length === 0) return;
|
||||
|
||||
let nextIndex = 0;
|
||||
const results: Array<{ path: string; diff: { original: string; modified: string } }> = [];
|
||||
const results: Array<{ path: string; diff: { original: string; modified: string; isBinary?: boolean } }> = [];
|
||||
|
||||
const takeNext = () => {
|
||||
const current = nextIndex;
|
||||
@@ -497,7 +499,7 @@ export const useGitStore = create<GitStore>()(
|
||||
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
||||
return {
|
||||
path: filePath,
|
||||
diff: { original: response.original ?? '', modified: response.modified ?? '' },
|
||||
diff: { original: response.original ?? '', modified: response.modified ?? '', isBinary: response.isBinary },
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ interface ProjectsStore {
|
||||
setActiveProject: (id: string) => void;
|
||||
setActiveProjectIdOnly: (id: string) => void;
|
||||
renameProject: (id: string, label: string) => void;
|
||||
updateProjectMeta: (id: string, meta: { label?: string; icon?: string | null; color?: string | null }) => void;
|
||||
reorderProjects: (fromIndex: number, toIndex: number) => void;
|
||||
validateProjectPath: (path: string) => ProjectPathValidationResult;
|
||||
synchronizeFromSettings: (settings: DesktopSettings) => void;
|
||||
@@ -72,7 +73,8 @@ const deriveProjectLabel = (path: string): string => {
|
||||
return 'Root';
|
||||
}
|
||||
const segments = normalized.split('/').filter(Boolean);
|
||||
return segments[segments.length - 1] || normalized;
|
||||
const raw = segments[segments.length - 1] || normalized;
|
||||
return raw.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
};
|
||||
|
||||
const createProjectId = (): string => {
|
||||
@@ -377,6 +379,26 @@ export const useProjectsStore = create<ProjectsStore>()(
|
||||
persistProjects(nextProjects, activeProjectId);
|
||||
},
|
||||
|
||||
updateProjectMeta: (id: string, meta: { label?: string; icon?: string | null; color?: string | null }) => {
|
||||
if (vscodeWorkspace) {
|
||||
return;
|
||||
}
|
||||
const { projects, activeProjectId } = get();
|
||||
const nextProjects = projects.map((project) => {
|
||||
if (project.id !== id) return project;
|
||||
const updated = { ...project };
|
||||
if (meta.label !== undefined) {
|
||||
const trimmed = meta.label.trim();
|
||||
if (trimmed) updated.label = trimmed;
|
||||
}
|
||||
if (meta.icon !== undefined) updated.icon = meta.icon;
|
||||
if (meta.color !== undefined) updated.color = meta.color;
|
||||
return updated;
|
||||
});
|
||||
set({ projects: nextProjects });
|
||||
persistProjects(nextProjects, activeProjectId);
|
||||
},
|
||||
|
||||
reorderProjects: (fromIndex: number, toIndex: number) => {
|
||||
if (vscodeWorkspace) {
|
||||
return;
|
||||
|
||||
@@ -5,6 +5,17 @@ import { getSafeStorage } from './utils/safeStorage';
|
||||
import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey } from '@/lib/typography';
|
||||
|
||||
export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files';
|
||||
export type RightSidebarTab = 'git' | 'files';
|
||||
export type ContextPanelMode = 'diff' | 'file';
|
||||
|
||||
type ContextPanelDirectoryState = {
|
||||
isOpen: boolean;
|
||||
expanded: boolean;
|
||||
mode: ContextPanelMode | null;
|
||||
targetPath: string | null;
|
||||
width: number;
|
||||
touchedAt: number;
|
||||
};
|
||||
|
||||
export type MainTabGuard = (nextTab: MainTab) => boolean;
|
||||
export type EventStreamStatus =
|
||||
@@ -51,6 +62,71 @@ const isLegacyDefaultTemplates = (value: unknown): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
const CONTEXT_PANEL_DEFAULT_WIDTH = 600;
|
||||
const CONTEXT_PANEL_MIN_WIDTH = 360;
|
||||
const CONTEXT_PANEL_MAX_WIDTH = 1400;
|
||||
const LEFT_SIDEBAR_MIN_WIDTH = 300;
|
||||
const RIGHT_SIDEBAR_MIN_WIDTH = 400;
|
||||
|
||||
const normalizeDirectoryPath = (value: string): string => {
|
||||
if (!value) return '';
|
||||
|
||||
const raw = value.replace(/\\/g, '/');
|
||||
const hadUncPrefix = raw.startsWith('//');
|
||||
let normalized = raw.replace(/\/+$/g, '');
|
||||
normalized = normalized.replace(/\/+/g, '/');
|
||||
|
||||
if (hadUncPrefix && !normalized.startsWith('//')) {
|
||||
normalized = `/${normalized}`;
|
||||
}
|
||||
|
||||
if (normalized === '') {
|
||||
return raw.startsWith('/') ? '/' : '';
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const clampContextPanelWidth = (width: number): number => {
|
||||
if (!Number.isFinite(width)) {
|
||||
return CONTEXT_PANEL_DEFAULT_WIDTH;
|
||||
}
|
||||
|
||||
return Math.min(CONTEXT_PANEL_MAX_WIDTH, Math.max(CONTEXT_PANEL_MIN_WIDTH, Math.round(width)));
|
||||
};
|
||||
|
||||
const touchContextPanelState = (prev?: ContextPanelDirectoryState): ContextPanelDirectoryState => {
|
||||
if (prev) {
|
||||
return { ...prev, touchedAt: Date.now() };
|
||||
}
|
||||
|
||||
return {
|
||||
isOpen: false,
|
||||
expanded: false,
|
||||
mode: null,
|
||||
targetPath: null,
|
||||
width: CONTEXT_PANEL_DEFAULT_WIDTH,
|
||||
touchedAt: Date.now(),
|
||||
};
|
||||
};
|
||||
|
||||
const clampContextPanelRoots = (
|
||||
byDirectory: Record<string, ContextPanelDirectoryState>,
|
||||
maxRoots: number
|
||||
): Record<string, ContextPanelDirectoryState> => {
|
||||
const entries = Object.entries(byDirectory);
|
||||
if (entries.length <= maxRoots) {
|
||||
return byDirectory;
|
||||
}
|
||||
|
||||
entries.sort((a, b) => (b[1]?.touchedAt ?? 0) - (a[1]?.touchedAt ?? 0));
|
||||
const next: Record<string, ContextPanelDirectoryState> = {};
|
||||
for (const [directory, state] of entries.slice(0, maxRoots)) {
|
||||
next[directory] = state;
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
interface UIStore {
|
||||
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
@@ -62,7 +138,10 @@ interface UIStore {
|
||||
isRightSidebarOpen: boolean;
|
||||
rightSidebarWidth: number;
|
||||
hasManuallyResizedRightSidebar: boolean;
|
||||
rightSidebarTab: RightSidebarTab;
|
||||
contextPanelByDirectory: Record<string, ContextPanelDirectoryState>;
|
||||
isBottomTerminalOpen: boolean;
|
||||
isBottomTerminalExpanded: boolean;
|
||||
bottomTerminalHeight: number;
|
||||
hasManuallyResizedBottomTerminal: boolean;
|
||||
isSessionSwitcherOpen: boolean;
|
||||
@@ -142,8 +221,15 @@ interface UIStore {
|
||||
toggleRightSidebar: () => void;
|
||||
setRightSidebarOpen: (open: boolean) => void;
|
||||
setRightSidebarWidth: (width: number) => void;
|
||||
setRightSidebarTab: (tab: RightSidebarTab) => void;
|
||||
openContextDiff: (directory: string, filePath: string) => void;
|
||||
openContextFile: (directory: string, filePath: string) => void;
|
||||
closeContextPanel: (directory: string) => void;
|
||||
toggleContextPanelExpanded: (directory: string) => void;
|
||||
setContextPanelWidth: (directory: string, width: number) => void;
|
||||
toggleBottomTerminal: () => void;
|
||||
setBottomTerminalOpen: (open: boolean) => void;
|
||||
setBottomTerminalExpanded: (expanded: boolean) => void;
|
||||
setBottomTerminalHeight: (height: number) => void;
|
||||
setSessionSwitcherOpen: (open: boolean) => void;
|
||||
setActiveMainTab: (tab: MainTab) => void;
|
||||
@@ -221,12 +307,15 @@ export const useUIStore = create<UIStore>()(
|
||||
isMultiRunLauncherOpen: false,
|
||||
multiRunLauncherPrefillPrompt: '',
|
||||
isSidebarOpen: true,
|
||||
sidebarWidth: 264,
|
||||
sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH,
|
||||
hasManuallyResizedLeftSidebar: false,
|
||||
isRightSidebarOpen: false,
|
||||
rightSidebarWidth: 420,
|
||||
rightSidebarWidth: RIGHT_SIDEBAR_MIN_WIDTH,
|
||||
hasManuallyResizedRightSidebar: false,
|
||||
rightSidebarTab: 'git',
|
||||
contextPanelByDirectory: {},
|
||||
isBottomTerminalOpen: false,
|
||||
isBottomTerminalExpanded: false,
|
||||
bottomTerminalHeight: 300,
|
||||
hasManuallyResizedBottomTerminal: false,
|
||||
isSessionSwitcherOpen: false,
|
||||
@@ -303,12 +392,10 @@ export const useUIStore = create<UIStore>()(
|
||||
set((state) => {
|
||||
const newOpen = !state.isSidebarOpen;
|
||||
|
||||
if (newOpen && typeof window !== 'undefined') {
|
||||
const proportionalWidth = Math.floor(window.innerWidth * 0.2);
|
||||
if (newOpen && !state.hasManuallyResizedLeftSidebar) {
|
||||
return {
|
||||
isSidebarOpen: newOpen,
|
||||
sidebarWidth: proportionalWidth,
|
||||
hasManuallyResizedLeftSidebar: false
|
||||
sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH,
|
||||
};
|
||||
}
|
||||
return { isSidebarOpen: newOpen };
|
||||
@@ -316,14 +403,11 @@ export const useUIStore = create<UIStore>()(
|
||||
},
|
||||
|
||||
setSidebarOpen: (open) => {
|
||||
set(() => {
|
||||
|
||||
if (open && typeof window !== 'undefined') {
|
||||
const proportionalWidth = Math.floor(window.innerWidth * 0.2);
|
||||
set((state) => {
|
||||
if (open && !state.hasManuallyResizedLeftSidebar) {
|
||||
return {
|
||||
isSidebarOpen: open,
|
||||
sidebarWidth: proportionalWidth,
|
||||
hasManuallyResizedLeftSidebar: false
|
||||
sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH,
|
||||
};
|
||||
}
|
||||
return { isSidebarOpen: open };
|
||||
@@ -338,12 +422,10 @@ export const useUIStore = create<UIStore>()(
|
||||
set((state) => {
|
||||
const newOpen = !state.isRightSidebarOpen;
|
||||
|
||||
if (newOpen && typeof window !== 'undefined') {
|
||||
const proportionalWidth = Math.floor(window.innerWidth * 0.28);
|
||||
if (newOpen && !state.hasManuallyResizedRightSidebar) {
|
||||
return {
|
||||
isRightSidebarOpen: newOpen,
|
||||
rightSidebarWidth: proportionalWidth,
|
||||
hasManuallyResizedRightSidebar: false,
|
||||
rightSidebarWidth: RIGHT_SIDEBAR_MIN_WIDTH,
|
||||
};
|
||||
}
|
||||
return { isRightSidebarOpen: newOpen };
|
||||
@@ -351,13 +433,11 @@ export const useUIStore = create<UIStore>()(
|
||||
},
|
||||
|
||||
setRightSidebarOpen: (open) => {
|
||||
set(() => {
|
||||
if (open && typeof window !== 'undefined') {
|
||||
const proportionalWidth = Math.floor(window.innerWidth * 0.28);
|
||||
set((state) => {
|
||||
if (open && !state.hasManuallyResizedRightSidebar) {
|
||||
return {
|
||||
isRightSidebarOpen: open,
|
||||
rightSidebarWidth: proportionalWidth,
|
||||
hasManuallyResizedRightSidebar: false,
|
||||
rightSidebarWidth: RIGHT_SIDEBAR_MIN_WIDTH,
|
||||
};
|
||||
}
|
||||
return { isRightSidebarOpen: open };
|
||||
@@ -368,6 +448,125 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ rightSidebarWidth: width, hasManuallyResizedRightSidebar: true });
|
||||
},
|
||||
|
||||
setRightSidebarTab: (tab) => {
|
||||
set({ rightSidebarTab: tab });
|
||||
},
|
||||
|
||||
openContextDiff: (directory, filePath) => {
|
||||
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||
const normalizedFilePath = (filePath || '').trim();
|
||||
if (!normalizedDirectory || !normalizedFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const prev = state.contextPanelByDirectory[normalizedDirectory];
|
||||
const current = touchContextPanelState(prev);
|
||||
const byDirectory = {
|
||||
...state.contextPanelByDirectory,
|
||||
[normalizedDirectory]: {
|
||||
...current,
|
||||
isOpen: true,
|
||||
mode: 'diff' as const,
|
||||
targetPath: normalizedFilePath,
|
||||
},
|
||||
};
|
||||
|
||||
return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) };
|
||||
});
|
||||
get().setPendingDiffFile(normalizedFilePath);
|
||||
},
|
||||
|
||||
openContextFile: (directory, filePath) => {
|
||||
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||
const normalizedFilePath = (filePath || '').trim();
|
||||
if (!normalizedDirectory || !normalizedFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const prev = state.contextPanelByDirectory[normalizedDirectory];
|
||||
const current = touchContextPanelState(prev);
|
||||
const byDirectory = {
|
||||
...state.contextPanelByDirectory,
|
||||
[normalizedDirectory]: {
|
||||
...current,
|
||||
isOpen: true,
|
||||
mode: 'file' as const,
|
||||
targetPath: normalizedFilePath,
|
||||
},
|
||||
};
|
||||
|
||||
return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) };
|
||||
});
|
||||
},
|
||||
|
||||
closeContextPanel: (directory) => {
|
||||
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||
if (!normalizedDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const prev = state.contextPanelByDirectory[normalizedDirectory];
|
||||
if (!prev || !prev.isOpen) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const byDirectory = {
|
||||
...state.contextPanelByDirectory,
|
||||
[normalizedDirectory]: {
|
||||
...touchContextPanelState(prev),
|
||||
isOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) };
|
||||
});
|
||||
},
|
||||
|
||||
toggleContextPanelExpanded: (directory) => {
|
||||
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||
if (!normalizedDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const prev = state.contextPanelByDirectory[normalizedDirectory];
|
||||
const current = touchContextPanelState(prev);
|
||||
const byDirectory = {
|
||||
...state.contextPanelByDirectory,
|
||||
[normalizedDirectory]: {
|
||||
...current,
|
||||
expanded: !current.expanded,
|
||||
},
|
||||
};
|
||||
|
||||
return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) };
|
||||
});
|
||||
},
|
||||
|
||||
setContextPanelWidth: (directory, width) => {
|
||||
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||
if (!normalizedDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const prev = state.contextPanelByDirectory[normalizedDirectory];
|
||||
const current = touchContextPanelState(prev);
|
||||
const byDirectory = {
|
||||
...state.contextPanelByDirectory,
|
||||
[normalizedDirectory]: {
|
||||
...current,
|
||||
width: clampContextPanelWidth(width),
|
||||
},
|
||||
};
|
||||
|
||||
return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) };
|
||||
});
|
||||
},
|
||||
|
||||
toggleBottomTerminal: () => {
|
||||
set((state) => {
|
||||
const newOpen = !state.isBottomTerminalOpen;
|
||||
@@ -400,6 +599,10 @@ export const useUIStore = create<UIStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
setBottomTerminalExpanded: (expanded) => {
|
||||
set({ isBottomTerminalExpanded: expanded });
|
||||
},
|
||||
|
||||
setBottomTerminalHeight: (height) => {
|
||||
set({ bottomTerminalHeight: height, hasManuallyResizedBottomTerminal: true });
|
||||
},
|
||||
@@ -420,36 +623,7 @@ export const useUIStore = create<UIStore>()(
|
||||
if (guard && !guard(tab)) {
|
||||
return;
|
||||
}
|
||||
const state = get();
|
||||
const currentTab = state.activeMainTab;
|
||||
const fullscreenTabs: MainTab[] = ['files', 'diff'];
|
||||
const isEnteringFullscreen = fullscreenTabs.includes(tab) && !fullscreenTabs.includes(currentTab);
|
||||
const isLeavingFullscreen = !fullscreenTabs.includes(tab) && fullscreenTabs.includes(currentTab);
|
||||
|
||||
if (isEnteringFullscreen) {
|
||||
// Save current sidebar state and close it
|
||||
set({
|
||||
activeMainTab: tab,
|
||||
sidebarOpenBeforeFullscreenTab: state.isSidebarOpen,
|
||||
isSidebarOpen: false,
|
||||
});
|
||||
} else if (isLeavingFullscreen) {
|
||||
// Restore sidebar state if it was open before
|
||||
const shouldRestore = state.sidebarOpenBeforeFullscreenTab === true;
|
||||
set({
|
||||
activeMainTab: tab,
|
||||
sidebarOpenBeforeFullscreenTab: null,
|
||||
...(shouldRestore && typeof window !== 'undefined'
|
||||
? {
|
||||
isSidebarOpen: true,
|
||||
sidebarWidth: Math.floor(window.innerWidth * 0.2),
|
||||
hasManuallyResizedLeftSidebar: false,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
} else {
|
||||
set({ activeMainTab: tab });
|
||||
}
|
||||
set({ activeMainTab: tab });
|
||||
},
|
||||
|
||||
setPendingDiffFile: (filePath) => {
|
||||
@@ -461,21 +635,7 @@ export const useUIStore = create<UIStore>()(
|
||||
if (guard && !guard('diff')) {
|
||||
return;
|
||||
}
|
||||
const state = get();
|
||||
const currentTab = state.activeMainTab;
|
||||
const fullscreenTabs: MainTab[] = ['files', 'diff'];
|
||||
const isEnteringFullscreen = !fullscreenTabs.includes(currentTab);
|
||||
|
||||
if (isEnteringFullscreen) {
|
||||
set({
|
||||
pendingDiffFile: filePath,
|
||||
activeMainTab: 'diff',
|
||||
sidebarOpenBeforeFullscreenTab: state.isSidebarOpen,
|
||||
isSidebarOpen: false,
|
||||
});
|
||||
} else {
|
||||
set({ pendingDiffFile: filePath, activeMainTab: 'diff' });
|
||||
}
|
||||
set({ pendingDiffFile: filePath, activeMainTab: 'diff' });
|
||||
},
|
||||
|
||||
consumePendingDiffFile: () => {
|
||||
@@ -770,14 +930,6 @@ export const useUIStore = create<UIStore>()(
|
||||
set((state) => {
|
||||
const updates: Partial<UIStore> = {};
|
||||
|
||||
if (state.isSidebarOpen && !state.hasManuallyResizedLeftSidebar) {
|
||||
updates.sidebarWidth = Math.floor(window.innerWidth * 0.2);
|
||||
}
|
||||
|
||||
if (state.isRightSidebarOpen && !state.hasManuallyResizedRightSidebar) {
|
||||
updates.rightSidebarWidth = Math.floor(window.innerWidth * 0.28);
|
||||
}
|
||||
|
||||
if (state.isBottomTerminalOpen && !state.hasManuallyResizedBottomTerminal) {
|
||||
updates.bottomTerminalHeight = Math.floor(window.innerHeight * 0.32);
|
||||
}
|
||||
@@ -865,7 +1017,7 @@ export const useUIStore = create<UIStore>()(
|
||||
{
|
||||
name: 'ui-store',
|
||||
storage: createJSONStorage(() => getSafeStorage()),
|
||||
version: 3,
|
||||
version: 4,
|
||||
migrate: (persistedState, version) => {
|
||||
if (!persistedState || typeof persistedState !== 'object') {
|
||||
return persistedState;
|
||||
@@ -905,6 +1057,14 @@ export const useUIStore = create<UIStore>()(
|
||||
delete state.memoryLimitActiveSession;
|
||||
}
|
||||
|
||||
if (typeof state.rightSidebarTab !== 'string' || (state.rightSidebarTab !== 'git' && state.rightSidebarTab !== 'files')) {
|
||||
state.rightSidebarTab = 'git';
|
||||
}
|
||||
|
||||
if (!state.contextPanelByDirectory || typeof state.contextPanelByDirectory !== 'object') {
|
||||
state.contextPanelByDirectory = {};
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
partialize: (state) => ({
|
||||
@@ -913,7 +1073,10 @@ export const useUIStore = create<UIStore>()(
|
||||
sidebarWidth: state.sidebarWidth,
|
||||
isRightSidebarOpen: state.isRightSidebarOpen,
|
||||
rightSidebarWidth: state.rightSidebarWidth,
|
||||
rightSidebarTab: state.rightSidebarTab,
|
||||
contextPanelByDirectory: state.contextPanelByDirectory,
|
||||
isBottomTerminalOpen: state.isBottomTerminalOpen,
|
||||
isBottomTerminalExpanded: state.isBottomTerminalExpanded,
|
||||
bottomTerminalHeight: state.bottomTerminalHeight,
|
||||
isSessionSwitcherOpen: state.isSessionSwitcherOpen,
|
||||
activeMainTab: state.activeMainTab,
|
||||
|
||||
Reference in New Issue
Block a user