refactor(surface): drop the deprecated main-tab aliases and dead diagram surface

MainTab/activeMainTab/setActiveMainTab/setMainTabGuard were deprecated
mirrors of the surface names — every call site now uses
activeSurface/setActiveSurface/setSurfaceGuard directly and the aliases
are gone, including the persisted mirror field.

The 'diagram' surface had no way to open it (navigateToDiagram had no
callers except a .drawio attachment click that navigated to a surface
nothing rendered); the surface, DiagramView, and its store plumbing are
removed, and a .drawio attachment now opens in the file panel.

?tab= deep links map to the matching context-panel surface instead of
setting a main-area surface nothing renders, and a persisted non-chat
surface can no longer rehydrate into a blank main area.
This commit is contained in:
Bohdan Triapitsyn
2026-08-24 16:21:05 +03:00
parent 8b7d7803de
commit 4da7604d7d
28 changed files with 151 additions and 285 deletions
+2 -2
View File
@@ -626,7 +626,7 @@ function App({ apis }: AppProps) {
const directory = typeof detail?.directory === 'string' && detail.directory.trim().length > 0 const directory = typeof detail?.directory === 'string' && detail.directory.trim().length > 0
? detail.directory.trim() ? detail.directory.trim()
: null; : null;
useUIStore.getState().setActiveMainTab('chat'); useUIStore.getState().setActiveSurface('chat');
void useSessionUIStore.getState().setCurrentSession(sessionId, directory); void useSessionUIStore.getState().setCurrentSession(sessionId, directory);
}; };
@@ -675,7 +675,7 @@ function App({ apis }: AppProps) {
? detail.projectId.trim() ? detail.projectId.trim()
: null; : null;
const hasProjectTarget = Boolean(directory || projectId); const hasProjectTarget = Boolean(directory || projectId);
useUIStore.getState().setActiveMainTab('chat'); useUIStore.getState().setActiveSurface('chat');
useUIStore.getState().setSessionSwitcherOpen(false); useUIStore.getState().setSessionSwitcherOpen(false);
useSessionUIStore.getState().openNewSessionDraft({ useSessionUIStore.getState().openNewSessionDraft({
target: hasProjectTarget ? 'project' : 'chat', target: hasProjectTarget ? 'project' : 'chat',
@@ -1006,8 +1006,8 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
return; return;
} }
const { activeMainTab } = useUIStore.getState(); const { activeSurface } = useUIStore.getState();
if (activeMainTab !== 'chat' || hasBlockingChatOverlay()) { if (activeSurface !== 'chat' || hasBlockingChatOverlay()) {
return; return;
} }
@@ -2,6 +2,7 @@ import React, { useRef, memo } from 'react';
import { useInputStore } from '@/sync/input-store'; import { useInputStore } from '@/sync/input-store';
import type { AttachedFile } from '@/sync/session-ui-store'; import type { AttachedFile } from '@/sync/session-ui-store';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { toast } from '@/components/ui'; import { toast } from '@/components/ui';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { openExternalUrl } from '@/lib/url'; import { openExternalUrl } from '@/lib/url';
@@ -833,7 +834,10 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
useUIStore.getState().navigateToDiagram(filePath); const directory = useDirectoryStore.getState().currentDirectory;
if (directory) {
useUIStore.getState().openContextFile(directory, filePath);
}
}} }}
className={cn( className={cn(
"flex items-center gap-2 p-2 rounded-lg border border-border/40 bg-muted/10 hover:bg-muted/20 transition-colors text-left cursor-pointer", "flex items-center gap-2 p-2 rounded-lg border border-border/40 bg-muted/10 hover:bg-muted/20 transition-colors text-left cursor-pointer",
+7 -7
View File
@@ -434,7 +434,7 @@ export const Header: React.FC = () => {
const openContextOverview = useUIStore((state) => state.openContextOverview); const openContextOverview = useUIStore((state) => state.openContextOverview);
const openContextPlan = useUIStore((state) => state.openContextPlan); const openContextPlan = useUIStore((state) => state.openContextPlan);
const closeContextPanel = useUIStore((state) => state.closeContextPanel); const closeContextPanel = useUIStore((state) => state.closeContextPanel);
const activeMainTab = useUIStore((state) => state.activeMainTab); const activeSurface = useUIStore((state) => state.activeSurface);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const getCurrentModel = useConfigStore((state) => state.getCurrentModel); const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
@@ -612,7 +612,7 @@ export const Header: React.FC = () => {
}, [setWorkStatusOverlayOpen, setWorkStatusPanelEnabled, workStatusOverlayOpen, workStatusPanelEnabled, workStatusPanelFits]); }, [setWorkStatusOverlayOpen, setWorkStatusPanelEnabled, workStatusOverlayOpen, workStatusPanelEnabled, workStatusPanelFits]);
const showDesktopHeaderContextUsage = !isVSCode const showDesktopHeaderContextUsage = !isVSCode
&& !workStatusPanelVisible && !workStatusPanelVisible
&& activeMainTab === 'chat' && activeSurface === 'chat'
&& !!stableDesktopContextUsage && !!stableDesktopContextUsage
&& stableDesktopContextUsage.totalTokens > 0; && stableDesktopContextUsage.totalTokens > 0;
const desktopHeaderDisplayPercentage = stableDesktopContextUsage && stableDesktopContextUsage.contextLimit > 0 const desktopHeaderDisplayPercentage = stableDesktopContextUsage && stableDesktopContextUsage.contextLimit > 0
@@ -1153,8 +1153,8 @@ export const Header: React.FC = () => {
// Reset plan tab availability when session changes // Reset plan tab availability when session changes
React.useEffect(() => { React.useEffect(() => {
if (!planModeEnabled) { if (!planModeEnabled) {
if (useUIStore.getState().activeMainTab === 'plan') { if (useUIStore.getState().activeSurface === 'plan') {
useUIStore.getState().setActiveMainTab('chat'); useUIStore.getState().setActiveSurface('chat');
} }
return; return;
} }
@@ -1167,8 +1167,8 @@ export const Header: React.FC = () => {
} }
// If plan is not available but user is on plan tab, switch them back to chat // If plan is not available but user is on plan tab, switch them back to chat
if (!planTabAvailable && useUIStore.getState().activeMainTab === 'plan') { if (!planTabAvailable && useUIStore.getState().activeSurface === 'plan') {
useUIStore.getState().setActiveMainTab('chat'); useUIStore.getState().setActiveSurface('chat');
} }
}, [ }, [
planModeEnabled, planModeEnabled,
@@ -1759,7 +1759,7 @@ export const Header: React.FC = () => {
className={cn(desktopHeaderIconButtonClass, 'mr-1')} className={cn(desktopHeaderIconButtonClass, 'mr-1')}
Icon={'picture-in-picture-2'} Icon={'picture-in-picture-2'}
/> />
{activeMainTab === 'chat' && !isVSCode ? ( {activeSurface === 'chat' && !isVSCode ? (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
@@ -148,7 +148,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
const homeDirectory = useDirectoryStore((s) => s.homeDirectory); const homeDirectory = useDirectoryStore((s) => s.homeDirectory);
const projects = useProjectsStore((s) => s.projects); const projects = useProjectsStore((s) => s.projects);
const addProject = useProjectsStore((s) => s.addProject); const addProject = useProjectsStore((s) => s.addProject);
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab); const setActiveSurface = useUIStore((s) => s.setActiveSurface);
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen); const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft); const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
const gitIdentityProfiles = useGitIdentitiesStore((s) => s.profiles); const gitIdentityProfiles = useGitIdentitiesStore((s) => s.profiles);
@@ -411,11 +411,11 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
}, [onOpenChange]); }, [onOpenChange]);
const openProjectDraft = React.useCallback((projectId: string, projectPath: string) => { const openProjectDraft = React.useCallback((projectId: string, projectPath: string) => {
setActiveMainTab('chat'); setActiveSurface('chat');
if (isMobile) setSessionSwitcherOpen(false); if (isMobile) setSessionSwitcherOpen(false);
openNewSessionDraft({ selectedProjectId: projectId, directoryOverride: projectPath }); openNewSessionDraft({ selectedProjectId: projectId, directoryOverride: projectPath });
handleClose(); handleClose();
}, [handleClose, isMobile, openNewSessionDraft, setActiveMainTab, setSessionSwitcherOpen]); }, [handleClose, isMobile, openNewSessionDraft, setActiveSurface, setSessionSwitcherOpen]);
const handleQuickAdd = React.useCallback((event: React.MouseEvent, path: string) => { const handleQuickAdd = React.useCallback((event: React.MouseEvent, path: string) => {
event.stopPropagation(); event.stopPropagation();
@@ -370,7 +370,7 @@ export function ScheduledTasksDialog() {
setOpen(false); setOpen(false);
if (isMobile) { if (isMobile) {
useFilesViewTabsStore.getState().setSelectedPath(selectedProject.path, task.loopFile, { allowOutsideRoot: true }); useFilesViewTabsStore.getState().setSelectedPath(selectedProject.path, task.loopFile, { allowOutsideRoot: true });
useUIStore.getState().setActiveMainTab('files'); useUIStore.getState().setActiveSurface('files');
return; return;
} }
useUIStore.getState().openContextFile(selectedProject.path, task.loopFile); useUIStore.getState().openContextFile(selectedProject.path, task.loopFile);
@@ -397,7 +397,7 @@ export function ScheduledTasksDialog() {
// this surface (MainLayout closes surfaces on session selection). // this surface (MainLayout closes surfaces on session selection).
const project = projects.find((entry) => entry.id === selectedProjectID); const project = projects.find((entry) => entry.id === selectedProjectID);
useSessionUIStore.getState().setCurrentSession(sessionId, project?.path ?? null); useSessionUIStore.getState().setCurrentSession(sessionId, project?.path ?? null);
useUIStore.getState().setActiveMainTab('chat'); useUIStore.getState().setActiveSurface('chat');
} }
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : t('sessions.scheduledTasks.dialog.toast.runFailed')); toast.error(error instanceof Error ? error.message : t('sessions.scheduledTasks.dialog.toast.runFailed'));
@@ -391,7 +391,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
const updateProjectMeta = useProjectsStore((state) => state.updateProjectMeta); const updateProjectMeta = useProjectsStore((state) => state.updateProjectMeta);
const reorderProjects = useProjectsStore((state) => state.reorderProjects); const reorderProjects = useProjectsStore((state) => state.reorderProjects);
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const openContextPanelTab = useUIStore((state) => state.openContextPanelTab); const openContextPanelTab = useUIStore((state) => state.openContextPanelTab);
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen); const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog); const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog);
@@ -848,7 +848,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
sessionSearchQuery, sessionSearchQuery,
setSessionSearchQuery, setSessionSearchQuery,
setIsSessionSearchOpen, setIsSessionSearchOpen,
setActiveMainTab, setActiveSurface,
setSessionSwitcherOpen, setSessionSwitcherOpen,
setCurrentSession, setCurrentSession,
updateSessionTitle, updateSessionTitle,
@@ -1698,7 +1698,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
alwaysShowActions={alwaysShowSidebarActions} alwaysShowActions={alwaysShowSidebarActions}
activeProjectId={activeProjectId} activeProjectId={activeProjectId}
setActiveProjectIdOnly={setActiveProjectIdOnly} setActiveProjectIdOnly={setActiveProjectIdOnly}
setActiveMainTab={setActiveMainTab} setActiveSurface={setActiveSurface}
setSessionSwitcherOpen={setSessionSwitcherOpen} setSessionSwitcherOpen={setSessionSwitcherOpen}
openNewSessionDraft={openNewSessionDraftFromTree} openNewSessionDraft={openNewSessionDraftFromTree}
addSessionToFolder={addSessionToFolder} addSessionToFolder={addSessionToFolder}
@@ -1741,7 +1741,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
alwaysShowSidebarActions, alwaysShowSidebarActions,
activeProjectId, activeProjectId,
setActiveProjectIdOnly, setActiveProjectIdOnly,
setActiveMainTab, setActiveSurface,
setSessionSwitcherOpen, setSessionSwitcherOpen,
openNewSessionDraftFromTree, openNewSessionDraftFromTree,
addSessionToFolder, addSessionToFolder,
@@ -1763,12 +1763,12 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
const handleOpenNewSessionDraftFromHeader = React.useCallback(() => { const handleOpenNewSessionDraftFromHeader = React.useCallback(() => {
useUIStore.getState().closeMainSurfaces(); useUIStore.getState().closeMainSurfaces();
setActiveMainTab('chat'); setActiveSurface('chat');
if (mobileVariant) { if (mobileVariant) {
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
} }
openNewSessionDraft(); openNewSessionDraft();
}, [mobileVariant, openNewSessionDraft, setActiveMainTab, setSessionSwitcherOpen]); }, [mobileVariant, openNewSessionDraft, setActiveSurface, setSessionSwitcherOpen]);
const renderChatsSection = React.useCallback((items: ActivityItem[]) => { const renderChatsSection = React.useCallback((items: ActivityItem[]) => {
const chatsRoot = getChatsRootForHome(homeDirectory) const chatsRoot = getChatsRootForHome(homeDirectory)
@@ -1850,12 +1850,12 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
setBulkDeleteConfirm, setBulkDeleteConfirm,
}); });
const handleOpenMultiRunFromHeader = React.useCallback(() => { const handleOpenMultiRunFromHeader = React.useCallback(() => {
setActiveMainTab('chat'); setActiveSurface('chat');
if (mobileVariant) { if (mobileVariant) {
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
} }
openMultiRunLauncher(); openMultiRunLauncher();
}, [mobileVariant, openMultiRunLauncher, setActiveMainTab, setSessionSwitcherOpen]); }, [mobileVariant, openMultiRunLauncher, setActiveSurface, setSessionSwitcherOpen]);
return ( return (
// One shared tooltip provider for the whole sidebar: session tooltips open // One shared tooltip provider for the whole sidebar: session tooltips open
@@ -1889,7 +1889,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
handleSessionSelect={stableHandleSessionSelect} handleSessionSelect={stableHandleSessionSelect}
mobileVariant={mobileVariant} mobileVariant={mobileVariant}
openNewSessionDraft={openNewSessionDraft} openNewSessionDraft={openNewSessionDraft}
setActiveMainTab={setActiveMainTab} setActiveSurface={setActiveSurface}
setSessionSwitcherOpen={setSessionSwitcherOpen} setSessionSwitcherOpen={setSessionSwitcherOpen}
sessionOwnerBySessionId={sessionOwnership.bySessionId} sessionOwnerBySessionId={sessionOwnership.bySessionId}
/> />
@@ -1959,7 +1959,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
alwaysShowActions={alwaysShowSidebarActions} alwaysShowActions={alwaysShowSidebarActions}
toggleProject={toggleProject} toggleProject={toggleProject}
setActiveProjectIdOnly={setActiveProjectIdOnly} setActiveProjectIdOnly={setActiveProjectIdOnly}
setActiveMainTab={setActiveMainTab} setActiveSurface={setActiveSurface}
setSessionSwitcherOpen={setSessionSwitcherOpen} setSessionSwitcherOpen={setSessionSwitcherOpen}
openNewSessionDraft={openNewSessionDraftFromTree} openNewSessionDraft={openNewSessionDraftFromTree}
openNewWorktreeDialog={openNewWorktreeDialog} openNewWorktreeDialog={openNewWorktreeDialog}
@@ -2033,7 +2033,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
open={newWorktreeDialogOpen} open={newWorktreeDialogOpen}
onOpenChange={setNewWorktreeDialogOpen} onOpenChange={setNewWorktreeDialogOpen}
onWorktreeCreated={(worktreePath, options) => { onWorktreeCreated={(worktreePath, options) => {
setActiveMainTab('chat'); setActiveSurface('chat');
if (mobileVariant) { if (mobileVariant) {
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
} }
@@ -71,14 +71,14 @@ type SwitcherContentProps = {
function SwitcherContent({ onSelect, variant, scopeProjectId }: SwitcherContentProps): React.ReactElement { function SwitcherContent({ onSelect, variant, scopeProjectId }: SwitcherContentProps): React.ReactElement {
const items = useSwitcherItems(true, { scopeProjectId }); const items = useSwitcherItems(true, { scopeProjectId });
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft); const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const { t } = useI18n(); const { t } = useI18n();
const handleNewSession = React.useCallback(() => { const handleNewSession = React.useCallback(() => {
setActiveMainTab('chat'); setActiveSurface('chat');
onSelect(); onSelect();
openNewSessionDraft(); openNewSessionDraft();
}, [onSelect, openNewSessionDraft, setActiveMainTab]); }, [onSelect, openNewSessionDraft, setActiveSurface]);
const [expandedParents, setExpandedParents] = React.useState<Set<string>>(new Set()); const [expandedParents, setExpandedParents] = React.useState<Set<string>>(new Set());
const toggleParent = React.useCallback((sessionId: string) => { const toggleParent = React.useCallback((sessionId: string) => {
@@ -44,13 +44,13 @@ export const useProjectTodoSend = (options: {
const sendMessage = useSessionUIStore((state) => state.sendMessage); const sendMessage = useSessionUIStore((state) => state.sendMessage);
const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession);
const setPendingInputText = useInputStore((state) => state.setPendingInputText); const setPendingInputText = useInputStore((state) => state.setPendingInputText);
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
const routeToChat = React.useCallback(() => { const routeToChat = React.useCallback(() => {
setActiveMainTab('chat'); setActiveSurface('chat');
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
}, [setActiveMainTab, setSessionSwitcherOpen]); }, [setActiveSurface, setSessionSwitcherOpen]);
const sendToCurrentSession = React.useCallback( const sendToCurrentSession = React.useCallback(
(todoText: string) => { (todoText: string) => {
@@ -14,7 +14,7 @@ import { Button } from '@/components/ui/button';
import { Icon } from "@/components/icon/Icon"; import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { sessionEvents } from '@/lib/sessionEvents'; import { sessionEvents } from '@/lib/sessionEvents';
import type { MainTab } from '@/stores/useUIStore'; import type { WorkspaceSurface } from '@/stores/useUIStore';
import { SessionFolderItem } from '../SessionFolderItem'; import { SessionFolderItem } from '../SessionFolderItem';
import type { SortableDragHandleProps } from './sortableItems'; import type { SortableDragHandleProps } from './sortableItems';
import { DroppableFolderWrapper, SessionFolderDndScope } from './sessionFolderDnd'; import { DroppableFolderWrapper, SessionFolderDndScope } from './sessionFolderDnd';
@@ -84,7 +84,7 @@ type Props = {
alwaysShowActions: boolean; alwaysShowActions: boolean;
activeProjectId: string | null; activeProjectId: string | null;
setActiveProjectIdOnly: (id: string) => void; setActiveProjectIdOnly: (id: string) => void;
setActiveMainTab: (tab: MainTab) => void; setActiveSurface: (tab: WorkspaceSurface) => void;
setSessionSwitcherOpen: (open: boolean) => void; setSessionSwitcherOpen: (open: boolean) => void;
openNewSessionDraft: (options?: { selectedProjectId?: string | null; directoryOverride?: string | null; targetFolderId?: string; target?: 'chat' | 'project' }) => void; openNewSessionDraft: (options?: { selectedProjectId?: string | null; directoryOverride?: string | null; targetFolderId?: string; target?: 'chat' | 'project' }) => void;
addSessionToFolder: (scopeKey: string, folderId: string, sessionId: string) => void; addSessionToFolder: (scopeKey: string, folderId: string, sessionId: string) => void;
@@ -265,7 +265,7 @@ const areGroupPropsEqual = (prev: Props, next: Props): boolean => {
&& prev.alwaysShowActions === next.alwaysShowActions && prev.alwaysShowActions === next.alwaysShowActions
&& prev.activeProjectId === next.activeProjectId && prev.activeProjectId === next.activeProjectId
&& prev.setActiveProjectIdOnly === next.setActiveProjectIdOnly && prev.setActiveProjectIdOnly === next.setActiveProjectIdOnly
&& prev.setActiveMainTab === next.setActiveMainTab && prev.setActiveSurface === next.setActiveSurface
&& prev.setSessionSwitcherOpen === next.setSessionSwitcherOpen && prev.setSessionSwitcherOpen === next.setSessionSwitcherOpen
&& prev.openNewSessionDraft === next.openNewSessionDraft && prev.openNewSessionDraft === next.openNewSessionDraft
&& prev.addSessionToFolder === next.addSessionToFolder && prev.addSessionToFolder === next.addSessionToFolder
@@ -307,7 +307,7 @@ function SessionGroupSectionBase(props: Props): React.ReactNode {
alwaysShowActions, alwaysShowActions,
activeProjectId, activeProjectId,
setActiveProjectIdOnly, setActiveProjectIdOnly,
setActiveMainTab, setActiveSurface,
setSessionSwitcherOpen, setSessionSwitcherOpen,
openNewSessionDraft, openNewSessionDraft,
addSessionToFolder, addSessionToFolder,
@@ -880,7 +880,7 @@ function SessionGroupSectionBase(props: Props): React.ReactNode {
depth={0} depth={0}
onNewSession={() => { onNewSession={() => {
if (projectId && projectId !== activeProjectId) setActiveProjectIdOnly(projectId); if (projectId && projectId !== activeProjectId) setActiveProjectIdOnly(projectId);
setActiveMainTab('chat'); setActiveSurface('chat');
if (mobileVariant) setSessionSwitcherOpen(false); if (mobileVariant) setSessionSwitcherOpen(false);
openNewSessionDraft({ openNewSessionDraft({
selectedProjectId: projectId, selectedProjectId: projectId,
@@ -1248,7 +1248,7 @@ function SessionGroupSectionBase(props: Props): React.ReactNode {
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
if (projectId && projectId !== activeProjectId) setActiveProjectIdOnly(projectId); if (projectId && projectId !== activeProjectId) setActiveProjectIdOnly(projectId);
setActiveMainTab('chat'); setActiveSurface('chat');
if (mobileVariant) setSessionSwitcherOpen(false); if (mobileVariant) setSessionSwitcherOpen(false);
openNewSessionDraft({ selectedProjectId: projectId, directoryOverride: group.directory }); openNewSessionDraft({ selectedProjectId: projectId, directoryOverride: group.directory });
}} }}
@@ -16,7 +16,7 @@ import type { SortableDragHandleProps } from './sortableItems';
import { ProjectHeaderIdentity, SortableGroupItem, SortableProjectItem } from './sortableItems'; import { ProjectHeaderIdentity, SortableGroupItem, SortableProjectItem } from './sortableItems';
import { formatProjectLabel } from './utils'; import { formatProjectLabel } from './utils';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import type { MainTab } from '@/stores/useUIStore'; import type { WorkspaceSurface } from '@/stores/useUIStore';
import type { ProjectSortOrder } from '@/stores/useSessionDisplayStore'; import type { ProjectSortOrder } from '@/stores/useSessionDisplayStore';
import { streamPerfCount } from '@/stores/utils/streamDebug'; import { streamPerfCount } from '@/stores/utils/streamDebug';
import { Icon } from '@/components/icon/Icon'; import { Icon } from '@/components/icon/Icon';
@@ -91,7 +91,7 @@ type Props = {
alwaysShowActions: boolean; alwaysShowActions: boolean;
toggleProject: (id: string) => void; toggleProject: (id: string) => void;
setActiveProjectIdOnly: (id: string) => void; setActiveProjectIdOnly: (id: string) => void;
setActiveMainTab: (tab: MainTab) => void; setActiveSurface: (tab: WorkspaceSurface) => void;
setSessionSwitcherOpen: (open: boolean) => void; setSessionSwitcherOpen: (open: boolean) => void;
openNewSessionDraft: (options?: { selectedProjectId?: string | null; directoryOverride?: string | null }) => void; openNewSessionDraft: (options?: { selectedProjectId?: string | null; directoryOverride?: string | null }) => void;
openNewWorktreeDialog: () => void; openNewWorktreeDialog: () => void;
@@ -362,7 +362,7 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode {
}} }}
onNewSession={() => { onNewSession={() => {
if (projectKey !== props.activeProjectId) props.setActiveProjectIdOnly(projectKey); if (projectKey !== props.activeProjectId) props.setActiveProjectIdOnly(projectKey);
props.setActiveMainTab('chat'); props.setActiveSurface('chat');
if (props.mobileVariant) props.setSessionSwitcherOpen(false); if (props.mobileVariant) props.setSessionSwitcherOpen(false);
props.openNewSessionDraft({ props.openNewSessionDraft({
selectedProjectId: projectKey, selectedProjectId: projectKey,
@@ -371,7 +371,7 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode {
}} }}
onNewWorktreeSession={() => { onNewWorktreeSession={() => {
if (projectKey !== props.activeProjectId) props.setActiveProjectIdOnly(projectKey); if (projectKey !== props.activeProjectId) props.setActiveProjectIdOnly(projectKey);
props.setActiveMainTab('chat'); props.setActiveSurface('chat');
props.openNewWorktreeDialog(); props.openNewWorktreeDialog();
}} }}
onManageWorktrees={() => props.openWorktreesPage(projectKey)} onManageWorktrees={() => props.openWorktreesPage(projectKey)}
@@ -2,7 +2,7 @@ import React from 'react';
import type { Session } from '@opencode-ai/sdk/v2'; import type { Session } from '@opencode-ai/sdk/v2';
import type { SessionGroup, SessionNode } from '../types'; import type { SessionGroup, SessionNode } from '../types';
import { normalizePath } from '../utils'; import { normalizePath } from '../utils';
import type { MainTab } from '@/stores/useUIStore'; import type { WorkspaceSurface } from '@/stores/useUIStore';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
@@ -22,7 +22,7 @@ type Args = {
newSessionDraftOpen: boolean; newSessionDraftOpen: boolean;
mobileVariant: boolean; mobileVariant: boolean;
openNewSessionDraft: (options?: { selectedProjectId?: string | null; directoryOverride?: string | null }) => void; openNewSessionDraft: (options?: { selectedProjectId?: string | null; directoryOverride?: string | null }) => void;
setActiveMainTab: (tab: MainTab) => void; setActiveSurface: (tab: WorkspaceSurface) => void;
setSessionSwitcherOpen: (open: boolean) => void; setSessionSwitcherOpen: (open: boolean) => void;
}; };
@@ -101,7 +101,7 @@ export const useProjectSessionSelection = (args: Args): void => {
newSessionDraftOpen, newSessionDraftOpen,
mobileVariant, mobileVariant,
openNewSessionDraft, openNewSessionDraft,
setActiveMainTab, setActiveSurface,
setSessionSwitcherOpen, setSessionSwitcherOpen,
} = args; } = args;
@@ -205,7 +205,7 @@ export const useProjectSessionSelection = (args: Args): void => {
previousActiveProjectRef.current = activeProjectId; previousActiveProjectRef.current = activeProjectId;
if (selection.kind === 'open-draft') { if (selection.kind === 'open-draft') {
setActiveMainTab('chat'); setActiveSurface('chat');
if (mobileVariant) { if (mobileVariant) {
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
} }
@@ -232,7 +232,7 @@ export const useProjectSessionSelection = (args: Args): void => {
openNewSessionDraft, openNewSessionDraft,
projectSections, projectSections,
projectSessionMeta, projectSessionMeta,
setActiveMainTab, setActiveSurface,
setSessionSwitcherOpen, setSessionSwitcherOpen,
setActiveSessionByProject, setActiveSessionByProject,
]); ]);
@@ -3,7 +3,7 @@ import type { Session } from '@opencode-ai/sdk/v2';
import { toast } from '@/components/ui'; import { toast } from '@/components/ui';
import { copyTextToClipboard } from '@/lib/clipboard'; import { copyTextToClipboard } from '@/lib/clipboard';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import type { MainTab } from '@/stores/useUIStore'; import type { WorkspaceSurface } from '@/stores/useUIStore';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { streamPerfMark } from '@/stores/utils/streamDebug'; import { streamPerfMark } from '@/stores/utils/streamDebug';
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
@@ -30,7 +30,7 @@ type Args = {
sessionSearchQuery: string; sessionSearchQuery: string;
setSessionSearchQuery: (value: string) => void; setSessionSearchQuery: (value: string) => void;
setIsSessionSearchOpen: (open: boolean) => void; setIsSessionSearchOpen: (open: boolean) => void;
setActiveMainTab: (tab: MainTab) => void; setActiveSurface: (tab: WorkspaceSurface) => void;
setSessionSwitcherOpen: (open: boolean) => void; setSessionSwitcherOpen: (open: boolean) => void;
setCurrentSession: (sessionId: string | null, directoryHint?: string | null) => void; setCurrentSession: (sessionId: string | null, directoryHint?: string | null) => void;
updateSessionTitle: (id: string, title: string) => Promise<void>; updateSessionTitle: (id: string, title: string) => Promise<void>;
@@ -79,7 +79,7 @@ export const useSessionActions = (args: Args) => {
}; };
if (args.mobileVariant) { if (args.mobileVariant) {
args.setActiveMainTab('chat'); args.setActiveSurface('chat');
args.setSessionSwitcherOpen(false); args.setSessionSwitcherOpen(false);
} }
@@ -81,7 +81,7 @@ export const CommandPalette: React.FC = () => {
const isCommandPaletteOpen = useUIStore((s) => s.isCommandPaletteOpen); const isCommandPaletteOpen = useUIStore((s) => s.isCommandPaletteOpen);
const setCommandPaletteOpen = useUIStore((s) => s.setCommandPaletteOpen); const setCommandPaletteOpen = useUIStore((s) => s.setCommandPaletteOpen);
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab); const setActiveSurface = useUIStore((s) => s.setActiveSurface);
const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen); const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen);
const setSettingsPage = useUIStore((s) => s.setSettingsPage); const setSettingsPage = useUIStore((s) => s.setSettingsPage);
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen); const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
@@ -170,7 +170,7 @@ export const CommandPalette: React.FC = () => {
shortcutId: 'new_chat', shortcutId: 'new_chat',
searchText: t('commandPalette.item.newSession'), searchText: t('commandPalette.item.newSession'),
onSelect: run(() => { onSelect: run(() => {
setActiveMainTab('chat'); setActiveSurface('chat');
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
openNewSessionDraft(); openNewSessionDraft();
}), }),
@@ -263,7 +263,7 @@ export const CommandPalette: React.FC = () => {
t, t,
run, run,
isMobile, isMobile,
setActiveMainTab, setActiveSurface,
setSessionSwitcherOpen, setSessionSwitcherOpen,
openNewSessionDraft, openNewSessionDraft,
toggleSidebar, toggleSidebar,
@@ -28,7 +28,7 @@ export function ArchiveView(): React.ReactNode {
const { t } = useI18n(); const { t } = useI18n();
const open = useUIStore((state) => state.isArchivePageOpen); const open = useUIStore((state) => state.isArchivePageOpen);
const setOpen = useUIStore((state) => state.setArchivePageOpen); const setOpen = useUIStore((state) => state.setArchivePageOpen);
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession);
const unarchiveSession = useSessionUIStore((state) => state.unarchiveSession); const unarchiveSession = useSessionUIStore((state) => state.unarchiveSession);
const homeDirectory = useDirectoryStore((state) => state.homeDirectory); const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
@@ -86,9 +86,9 @@ export function ArchiveView(): React.ReactNode {
const openSession = React.useCallback((session: Session) => { const openSession = React.useCallback((session: Session) => {
const directory = normalizePath(resolveGlobalSessionDirectory(session)); const directory = normalizePath(resolveGlobalSessionDirectory(session));
setCurrentSession(session.id, directory ?? undefined); setCurrentSession(session.id, directory ?? undefined);
setActiveMainTab('chat'); setActiveSurface('chat');
setOpen(false); setOpen(false);
}, [setActiveMainTab, setCurrentSession, setOpen]); }, [setActiveSurface, setCurrentSession, setOpen]);
const restoreSession = React.useCallback((session: Session) => { const restoreSession = React.useCallback((session: Session) => {
void unarchiveSession(session.id).then((success) => { void unarchiveSession(session.id).then((success) => {
@@ -1,102 +0,0 @@
import React from 'react';
import { useUIStore } from '@/stores/useUIStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useI18n } from '@/lib/i18n';
import { DiagramEditor, type DiagramEditorHandle } from '@/components/diagram';
import { Icon } from '@/components/icon/Icon';
export function DiagramView() {
const { t } = useI18n();
const { files } = useRuntimeAPIs();
const [filePath, setFilePath] = React.useState<string | null>(null);
const [xml, setXml] = React.useState('');
const [loading, setLoading] = React.useState(true);
const editorRef = React.useRef<DiagramEditorHandle>(null);
const pendingDiagramFile = useUIStore((state) => state.pendingDiagramFile);
const loadFile = React.useCallback(async (path: string) => {
setLoading(true);
setFilePath(path);
try {
const result = await files?.readFile?.(path);
if (result) {
setXml(result.content);
}
} catch {
setXml('');
} finally {
setLoading(false);
}
}, [files]);
React.useEffect(() => {
if (!pendingDiagramFile) {
return;
}
const pending = useUIStore.getState().consumePendingDiagramFile();
if (pending) {
void loadFile(pending);
}
}, [loadFile, pendingDiagramFile]);
const saveDiagram = React.useCallback(async () => {
const newXml = editorRef.current?.getXml();
if (filePath && files?.writeFile && newXml && newXml !== xml) {
await files.writeFile(filePath, newXml);
setXml(newXml);
}
}, [filePath, files, xml]);
const fileName = filePath ? filePath.split('/').pop() || filePath : '';
if (!filePath) {
return (
<div className="flex h-full items-center justify-center p-3">
<div className="typography-ui text-muted-foreground">
{t('filesView.editor.pickFileFromTree')}
</div>
</div>
);
}
if (loading) {
return (
<div className="flex h-full items-center justify-center p-3">
<Icon name="loader-4" className="size-4 animate-spin" />
</div>
);
}
return (
<div className="flex h-full flex-col">
<div className="flex items-center gap-2 border-b border-border/30 px-3 py-1.5">
<Icon name="file" className="size-4 shrink-0 text-muted-foreground" />
<span className="typography-ui text-muted-foreground truncate flex-1">{fileName}</span>
<button
type="button"
onClick={() => void saveDiagram()}
className="size-6 flex items-center justify-center rounded-md text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
title={t('filesView.diagram.saveDiagram')}
>
<Icon name="save-3" className="size-4" />
</button>
<button
type="button"
onClick={() => useUIStore.getState().setActiveMainTab('chat')}
className="size-6 flex items-center justify-center rounded-md text-muted-foreground hover:bg-interactive-hover/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
title={t('filesView.diagram.closeDiagramView')}
>
<Icon name="close" className="size-4" />
</button>
</div>
<div className="flex-1 min-h-0">
<DiagramEditor
ref={editorRef}
xml={xml}
className="h-full"
/>
</div>
</div>
);
}
+16 -16
View File
@@ -939,7 +939,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const [confirmDiscardOpen, setConfirmDiscardOpen] = React.useState(false); const [confirmDiscardOpen, setConfirmDiscardOpen] = React.useState(false);
const pendingSelectFileRef = React.useRef<FileNode | null>(null); const pendingSelectFileRef = React.useRef<FileNode | null>(null);
const pendingTabRef = React.useRef<import('@/stores/useUIStore').MainTab | null>(null); const pendingTabRef = React.useRef<import('@/stores/useUIStore').WorkspaceSurface | null>(null);
const pendingClosePathRef = React.useRef<string | null>(null); const pendingClosePathRef = React.useRef<string | null>(null);
const skipDirtyOnceRef = React.useRef(false); const skipDirtyOnceRef = React.useRef(false);
const copiedContentTimeoutRef = React.useRef<number | null>(null); const copiedContentTimeoutRef = React.useRef<number | null>(null);
@@ -1029,7 +1029,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const [isDragging, setIsDragging] = React.useState(false); const [isDragging, setIsDragging] = React.useState(false);
// Session/config for sending comments // Session/config for sending comments
const setMainTabGuard = useUIStore((state) => state.setMainTabGuard); const setSurfaceGuard = useUIStore((state) => state.setSurfaceGuard);
const pendingFileNavigation = useUIStore((state) => state.pendingFileNavigation); const pendingFileNavigation = useUIStore((state) => state.pendingFileNavigation);
const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation); const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation);
const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath); const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath);
@@ -1098,10 +1098,10 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
React.useEffect(() => { React.useEffect(() => {
setLineSelection(null); setLineSelection(null);
reset(); reset();
setMainTabGuard(null); setSurfaceGuard(null);
setDraftContent(''); setDraftContent('');
setIsSaving(false); setIsSaving(false);
}, [selectedFile?.path, reset, setMainTabGuard]); }, [selectedFile?.path, reset, setSurfaceGuard]);
React.useEffect(() => { React.useEffect(() => {
setCommentSelection(lineSelection); setCommentSelection(lineSelection);
@@ -1713,11 +1713,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
React.useEffect(() => { React.useEffect(() => {
if (!isDirty) { if (!isDirty) {
setMainTabGuard(null); setSurfaceGuard(null);
return; return;
} }
const guard = (_nextTab: import('@/stores/useUIStore').MainTab) => { const guard = (_nextTab: import('@/stores/useUIStore').WorkspaceSurface) => {
if (skipDirtyOnceRef.current) { if (skipDirtyOnceRef.current) {
skipDirtyOnceRef.current = false; skipDirtyOnceRef.current = false;
return true; return true;
@@ -1727,15 +1727,15 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
return false; return false;
}; };
setMainTabGuard(guard); setSurfaceGuard(guard);
return () => { return () => {
const currentGuard = useUIStore.getState().mainTabGuard; const currentGuard = useUIStore.getState().surfaceGuard;
if (currentGuard === guard) { if (currentGuard === guard) {
setMainTabGuard(null); setSurfaceGuard(null);
} }
}; };
}, [isDirty, setMainTabGuard]); }, [isDirty, setSurfaceGuard]);
React.useEffect(() => { React.useEffect(() => {
if (autoSaveEnabled) { if (autoSaveEnabled) {
@@ -2180,10 +2180,10 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
} }
if (nextTab) { if (nextTab) {
setMainTabGuard(null); setSurfaceGuard(null);
useUIStore.getState().setActiveMainTab(nextTab); useUIStore.getState().setActiveSurface(nextTab);
} }
}, [displayedContent, handleSelectFile, isMobile, removeOpenPath, root, selectedFile?.path, setMainTabGuard, setSelectedPath]); }, [displayedContent, handleSelectFile, isMobile, removeOpenPath, root, selectedFile?.path, setSurfaceGuard, setSelectedPath]);
const saveAndContinue = React.useCallback(async () => { const saveAndContinue = React.useCallback(async () => {
const nextFile = pendingSelectFileRef.current; const nextFile = pendingSelectFileRef.current;
@@ -2234,10 +2234,10 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
} }
if (nextTab) { if (nextTab) {
setMainTabGuard(null); setSurfaceGuard(null);
useUIStore.getState().setActiveMainTab(nextTab); useUIStore.getState().setActiveSurface(nextTab);
} }
}, [handleSelectFile, isMobile, removeOpenPath, root, saveDraft, selectedFile?.path, setMainTabGuard, setSelectedPath]); }, [handleSelectFile, isMobile, removeOpenPath, root, saveDraft, selectedFile?.path, setSurfaceGuard, setSelectedPath]);
const handleCloseFile = React.useCallback((path: string) => { const handleCloseFile = React.useCallback((path: string) => {
const isActive = selectedFile?.path === path; const isActive = selectedFile?.path === path;
@@ -168,7 +168,7 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null, projectPl
const activeProjectId = useProjectsStore((state) => state.activeProjectId); const activeProjectId = useProjectsStore((state) => state.activeProjectId);
const gitDirectories = useGitStore((state) => state.directories); const gitDirectories = useGitStore((state) => state.directories);
const effectiveDirectory = useEffectiveDirectory() ?? ''; const effectiveDirectory = useEffectiveDirectory() ?? '';
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
const runtimeApis = useRuntimeAPIs(); const runtimeApis = useRuntimeAPIs();
const { isMobile } = useDeviceInfo(); const { isMobile } = useDeviceInfo();
@@ -579,10 +579,10 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null, projectPl
}, []); }, []);
const routeToChat = React.useCallback(() => { const routeToChat = React.useCallback(() => {
setActiveMainTab('chat'); setActiveSurface('chat');
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
onNavigatedToChat?.(); onNavigatedToChat?.();
}, [onNavigatedToChat, setActiveMainTab, setSessionSwitcherOpen]); }, [onNavigatedToChat, setActiveSurface, setSessionSwitcherOpen]);
const handleConfirmPlanSend = React.useCallback( const handleConfirmPlanSend = React.useCallback(
async (execution: TodoSendExecution) => { async (execution: TodoSendExecution) => {
@@ -41,7 +41,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const setPendingInputText = useInputStore((state) => state.setPendingInputText); const setPendingInputText = useInputStore((state) => state.setPendingInputText);
const setPendingSyntheticParts = useInputStore((state) => state.setPendingSyntheticParts); const setPendingSyntheticParts = useInputStore((state) => state.setPendingSyntheticParts);
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [conflictDetails, setConflictDetails] = React.useState<MergeConflictDetails | null>(null); const [conflictDetails, setConflictDetails] = React.useState<MergeConflictDetails | null>(null);
@@ -137,7 +137,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
{ text: context.payloadText, synthetic: true }, { text: context.payloadText, synthetic: true },
]); ]);
setActiveMainTab('chat'); setActiveSurface('chat');
onClearState?.(); onClearState?.();
onOpenChange(false); onOpenChange(false);
}; };
@@ -159,7 +159,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
], ],
}); });
// Navigate to chat tab so user sees the new session // Navigate to chat tab so user sees the new session
setActiveMainTab('chat'); setActiveSurface('chat');
onClearState?.(); onClearState?.();
onOpenChange(false); onOpenChange(false);
}; };
@@ -65,7 +65,7 @@ export const IntegrateCommitsSection: React.FC<{
}) => { }) => {
const { t } = useI18n(); const { t } = useI18n();
const currentSessionId = useSessionUIStore((s) => s.currentSessionId); const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab); const setActiveSurface = useUIStore((s) => s.setActiveSurface);
const [branchDropdownOpen, setBranchDropdownOpen] = React.useState(false); const [branchDropdownOpen, setBranchDropdownOpen] = React.useState(false);
const [branchSearch, setBranchSearch] = React.useState(''); const [branchSearch, setBranchSearch] = React.useState('');
const searchInputRef = React.useRef<HTMLInputElement>(null); const searchInputRef = React.useRef<HTMLInputElement>(null);
@@ -236,7 +236,7 @@ export const IntegrateCommitsSection: React.FC<{
], ],
}); });
// Navigate to chat tab so user sees the new session // Navigate to chat tab so user sees the new session
setActiveMainTab('chat'); setActiveSurface('chat');
return; return;
} }
@@ -251,8 +251,8 @@ export const IntegrateCommitsSection: React.FC<{
{ text: context.instructionsText, synthetic: true }, { text: context.instructionsText, synthetic: true },
{ text: context.payloadText, synthetic: true }, { text: context.payloadText, synthetic: true },
]); ]);
setActiveMainTab('chat'); setActiveSurface('chat');
}, [currentSessionId, setActiveMainTab, buildConflictContext, openNewSessionDraft, setPendingInputText, setPendingSyntheticParts, t]); }, [currentSessionId, setActiveSurface, buildConflictContext, openNewSessionDraft, setPendingInputText, setPendingSyntheticParts, t]);
const handleMove = React.useCallback(async () => { const handleMove = React.useCallback(async () => {
if (ui.kind !== 'ready') return; if (ui.kind !== 'ready') return;
@@ -327,7 +327,7 @@ export const PullRequestSection: React.FC<{
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked); const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
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 setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const newSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open)); const newSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open));
const { isMobile, hasTouchInput, screenWidth } = useDeviceInfo(); const { isMobile, hasTouchInput, screenWidth } = useDeviceInfo();
@@ -986,14 +986,14 @@ export const PullRequestSection: React.FC<{
text: '', text: '',
}); });
} }
setActiveMainTab('chat'); setActiveSurface('chat');
} catch (e) { } catch (e) {
const message = e instanceof Error ? e.message : String(e); const message = e instanceof Error ? e.message : String(e);
toast.error(t('gitView.pr.toast.loadChecksFailed'), { description: message }); toast.error(t('gitView.pr.toast.loadChecksFailed'), { description: message });
} finally { } finally {
setIsAttachingChecks(false); setIsAttachingChecks(false);
} }
}, [directory, ensurePrContext, github, pr, resolveDraftTarget, setActiveMainTab, status?.repo, t]); }, [directory, ensurePrContext, github, pr, resolveDraftTarget, setActiveSurface, status?.repo, t]);
const sendCommentsToChat = React.useCallback(async () => { const sendCommentsToChat = React.useCallback(async () => {
if (!github?.prContext) { if (!github?.prContext) {
@@ -1021,14 +1021,14 @@ export const PullRequestSection: React.FC<{
for (const comment of timelineComments) { for (const comment of timelineComments) {
attachCommentDraft(target, comment); attachCommentDraft(target, comment);
} }
setActiveMainTab('chat'); setActiveSurface('chat');
} catch (e) { } catch (e) {
const message = e instanceof Error ? e.message : String(e); const message = e instanceof Error ? e.message : String(e);
toast.error(t('gitView.pr.toast.loadPrCommentsFailed'), { description: message }); toast.error(t('gitView.pr.toast.loadPrCommentsFailed'), { description: message });
} finally { } finally {
setIsAttachingComments(false); setIsAttachingComments(false);
} }
}, [attachCommentDraft, directory, ensurePrContext, github, pr, resolveDraftTarget, setActiveMainTab, status?.repo, t, timelineComments]); }, [attachCommentDraft, directory, ensurePrContext, github, pr, resolveDraftTarget, setActiveSurface, status?.repo, t, timelineComments]);
const sendSingleCommentToChat = React.useCallback(async (comment: TimelineCommentItem) => { const sendSingleCommentToChat = React.useCallback(async (comment: TimelineCommentItem) => {
const target = resolveDraftTarget(); const target = resolveDraftTarget();
@@ -1037,8 +1037,8 @@ export const PullRequestSection: React.FC<{
} }
attachCommentDraft(target, comment); attachCommentDraft(target, comment);
setActiveMainTab('chat'); setActiveSurface('chat');
}, [attachCommentDraft, resolveDraftTarget, setActiveMainTab]); }, [attachCommentDraft, resolveDraftTarget, setActiveSurface]);
const refresh = React.useCallback(async (options?: { force?: boolean; onlyExistingPr?: boolean; silent?: boolean; markInitialResolved?: boolean }) => { const refresh = React.useCallback(async (options?: { force?: boolean; onlyExistingPr?: boolean; silent?: boolean; markInitialResolved?: boolean }) => {
await refreshPrStatus(prStatusKey, options); await refreshPrStatus(prStatusKey, options);
+17 -17
View File
@@ -59,7 +59,7 @@ export const useKeyboardShortcuts = () => {
}, [currentShortcutDirectory]); }, [currentShortcutDirectory]);
const isMobile = useUIStore((s) => s.isMobile); const isMobile = useUIStore((s) => s.isMobile);
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen); const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab); const setActiveSurface = useUIStore((s) => s.setActiveSurface);
const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen); const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen);
const setModelSelectorOpen = useUIStore((s) => s.setModelSelectorOpen); const setModelSelectorOpen = useUIStore((s) => s.setModelSelectorOpen);
const setTimelineDialogOpen = useUIStore((s) => s.setTimelineDialogOpen); const setTimelineDialogOpen = useUIStore((s) => s.setTimelineDialogOpen);
@@ -154,7 +154,7 @@ export const useKeyboardShortcuts = () => {
isAboutDialogOpen, isAboutDialogOpen,
isMultiRunLauncherOpen, isMultiRunLauncherOpen,
isImagePreviewOpen, isImagePreviewOpen,
activeMainTab, activeSurface,
isPromptNavigatorPanelOpen, isPromptNavigatorPanelOpen,
} = useUIStore.getState(); } = useUIStore.getState();
@@ -183,7 +183,7 @@ export const useKeyboardShortcuts = () => {
} }
const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen || isMultiRunLauncherOpen || isImagePreviewOpen; const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen || isMultiRunLauncherOpen || isImagePreviewOpen;
const isChatActive = activeMainTab === 'chat'; const isChatActive = activeSurface === 'chat';
if (hasOverlay || !isChatActive) { if (hasOverlay || !isChatActive) {
resetAbortPriming(); resetAbortPriming();
@@ -245,7 +245,7 @@ export const useKeyboardShortcuts = () => {
if (eventMatchesShortcut(e, combo('toggle_prompt_navigator'))) { if (eventMatchesShortcut(e, combo('toggle_prompt_navigator'))) {
const { const {
activeMainTab, activeSurface,
promptNavigatorEnabled, promptNavigatorEnabled,
isSettingsDialogOpen, isSettingsDialogOpen,
isCommandPaletteOpen, isCommandPaletteOpen,
@@ -257,7 +257,7 @@ export const useKeyboardShortcuts = () => {
isImagePreviewOpen, isImagePreviewOpen,
} = useUIStore.getState(); } = useUIStore.getState();
if (!promptNavigatorEnabled || isMobile || isVSCodeRuntime() || activeMainTab !== 'chat') { if (!promptNavigatorEnabled || isMobile || isVSCodeRuntime() || activeSurface !== 'chat') {
return; return;
} }
@@ -308,7 +308,7 @@ export const useKeyboardShortcuts = () => {
if (matchedNewSessionShortcut || matchedWorktreeShortcut) { if (matchedNewSessionShortcut || matchedWorktreeShortcut) {
e.preventDefault(); e.preventDefault();
setActiveMainTab('chat'); setActiveSurface('chat');
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
if (!isVSCodeRuntime() && matchedWorktreeShortcut) { if (!isVSCodeRuntime() && matchedWorktreeShortcut) {
@@ -394,11 +394,11 @@ export const useKeyboardShortcuts = () => {
isHelpDialogOpen, isHelpDialogOpen,
isSessionSwitcherOpen, isSessionSwitcherOpen,
isAboutDialogOpen, isAboutDialogOpen,
activeMainTab, activeSurface,
} = useUIStore.getState(); } = useUIStore.getState();
const hasOverlay = isSettingsDialogOpen || isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen; const hasOverlay = isSettingsDialogOpen || isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen;
if (hasOverlay || activeMainTab !== 'chat' || !isChatInputTarget(e.target)) { if (hasOverlay || activeSurface !== 'chat' || !isChatInputTarget(e.target)) {
return; return;
} }
@@ -525,7 +525,7 @@ export const useKeyboardShortcuts = () => {
isHelpDialogOpen, isHelpDialogOpen,
isSessionSwitcherOpen, isSessionSwitcherOpen,
isAboutDialogOpen, isAboutDialogOpen,
activeMainTab, activeSurface,
isModelSelectorOpen, isModelSelectorOpen,
} = useUIStore.getState(); } = useUIStore.getState();
@@ -536,7 +536,7 @@ export const useKeyboardShortcuts = () => {
// Skip if any overlay open or not on chat tab // Skip if any overlay open or not on chat tab
const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen; const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen;
const isChatActive = activeMainTab === 'chat'; const isChatActive = activeSurface === 'chat';
if (hasOverlay || !isChatActive) { if (hasOverlay || !isChatActive) {
return; return;
@@ -555,7 +555,7 @@ export const useKeyboardShortcuts = () => {
isHelpDialogOpen, isHelpDialogOpen,
isSessionSwitcherOpen, isSessionSwitcherOpen,
isAboutDialogOpen, isAboutDialogOpen,
activeMainTab, activeSurface,
} = useUIStore.getState(); } = useUIStore.getState();
if (isSettingsDialogOpen) { if (isSettingsDialogOpen) {
@@ -563,7 +563,7 @@ export const useKeyboardShortcuts = () => {
} }
const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen; const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen;
const isChatActive = activeMainTab === 'chat'; const isChatActive = activeSurface === 'chat';
if (hasOverlay || !isChatActive) { if (hasOverlay || !isChatActive) {
return; return;
@@ -602,7 +602,7 @@ export const useKeyboardShortcuts = () => {
isHelpDialogOpen, isHelpDialogOpen,
isSessionSwitcherOpen, isSessionSwitcherOpen,
isAboutDialogOpen, isAboutDialogOpen,
activeMainTab, activeSurface,
favoriteModels, favoriteModels,
addRecentModel, addRecentModel,
} = useUIStore.getState(); } = useUIStore.getState();
@@ -612,7 +612,7 @@ export const useKeyboardShortcuts = () => {
} }
const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen; const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen;
const isChatActive = activeMainTab === 'chat'; const isChatActive = activeSurface === 'chat';
if (hasOverlay || !isChatActive || favoriteModels.length === 0) { if (hasOverlay || !isChatActive || favoriteModels.length === 0) {
return; return;
@@ -644,8 +644,8 @@ export const useKeyboardShortcuts = () => {
} }
if (eventMatchesShortcut(e, combo('toggle_dictation'))) { if (eventMatchesShortcut(e, combo('toggle_dictation'))) {
const { activeMainTab, isCommandPaletteOpen, isHelpDialogOpen, isSessionSwitcherOpen, isSettingsDialogOpen } = useUIStore.getState(); const { activeSurface, isCommandPaletteOpen, isHelpDialogOpen, isSessionSwitcherOpen, isSettingsDialogOpen } = useUIStore.getState();
if (activeMainTab !== 'chat' || isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isSettingsDialogOpen) { if (activeSurface !== 'chat' || isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isSettingsDialogOpen) {
return; return;
} }
e.preventDefault(); e.preventDefault();
@@ -695,7 +695,7 @@ export const useKeyboardShortcuts = () => {
toggleTerminalSurfaceExpanded, toggleTerminalSurfaceExpanded,
isMobile, isMobile,
setSessionSwitcherOpen, setSessionSwitcherOpen,
setActiveMainTab, setActiveSurface,
setSettingsDialogOpen, setSettingsDialogOpen,
setModelSelectorOpen, setModelSelectorOpen,
setTimelineDialogOpen, setTimelineDialogOpen,
+6 -6
View File
@@ -102,7 +102,7 @@ export const useMenuActions = (
const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog); const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog);
const toggleSidebar = useUIStore((s) => s.toggleSidebar); const toggleSidebar = useUIStore((s) => s.toggleSidebar);
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen); const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab); const setActiveSurface = useUIStore((s) => s.setActiveSurface);
const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen); const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen);
const setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen); const setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen);
const checkForUpdates = useUpdateStore((state) => state.checkForUpdates); const checkForUpdates = useUpdateStore((state) => state.checkForUpdates);
@@ -151,10 +151,10 @@ export const useMenuActions = (
const nextSession = sessions[nextIndex]; const nextSession = sessions[nextIndex];
if (!nextSession) return; if (!nextSession) return;
setActiveMainTab('chat'); setActiveSurface('chat');
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
useSessionUIStore.getState().setCurrentSession(nextSession.id); useSessionUIStore.getState().setCurrentSession(nextSession.id);
}, [setActiveMainTab, setSessionSwitcherOpen]); }, [setActiveSurface, setSessionSwitcherOpen]);
const navigateProject = React.useCallback((direction: -1 | 1) => { const navigateProject = React.useCallback((direction: -1 | 1) => {
const { activeProjectId, projects, setActiveProject } = useProjectsStore.getState(); const { activeProjectId, projects, setActiveProject } = useProjectsStore.getState();
@@ -191,7 +191,7 @@ export const useMenuActions = (
break; break;
case 'new-session': case 'new-session':
setActiveMainTab('chat'); setActiveSurface('chat');
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
{ {
const sessionState = useSessionUIStore.getState(); const sessionState = useSessionUIStore.getState();
@@ -203,7 +203,7 @@ export const useMenuActions = (
break; break;
case 'new-worktree-session': case 'new-worktree-session':
setActiveMainTab('chat'); setActiveSurface('chat');
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
createWorktreeSession(); createWorktreeSession();
break; break;
@@ -341,7 +341,7 @@ export const useMenuActions = (
onToggleMemoryDebug, onToggleMemoryDebug,
openNewSessionDraft, openNewSessionDraft,
setAboutDialogOpen, setAboutDialogOpen,
setActiveMainTab, setActiveSurface,
setSessionSwitcherOpen, setSessionSwitcherOpen,
setCommandPaletteOpen, setCommandPaletteOpen,
setSettingsDialogOpen, setSettingsDialogOpen,
+12 -4
View File
@@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore, type ContextPanelMode } 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 { WorkspaceSurface } 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';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
/** /**
* Check if running in VS Code webview context. * Check if running in VS Code webview context.
@@ -88,9 +89,16 @@ export function useRouter(): void {
setSettingsDialogOpen(false); setSettingsDialogOpen(false);
} }
// 3. Apply the view selected by the legacy URL parameter. // 3. Apply the view selected by the legacy URL parameter. Desktop
if (route.tab) { // surfaces live in the context panel, so a non-chat tab deep link
setActiveSurface(route.tab); // opens the matching panel surface; activeSurface itself stays 'chat'
// (nothing renders non-chat surfaces in the main area).
if (route.tab && route.tab !== 'chat') {
const directory = useDirectoryStore.getState().currentDirectory;
if (directory) {
const mode: ContextPanelMode = route.tab === 'files' ? 'file' : route.tab;
useUIStore.getState().openContextSurface(directory, mode);
}
} }
// 4. Apply diff file (only if going to diff tab) // 4. Apply diff file (only if going to diff tab)
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
const focusChatInputCalls: number[] = []; const focusChatInputCalls: number[] = [];
const pendingInputCalls: Array<{ text: string | null; mode?: string }> = []; const pendingInputCalls: Array<{ text: string | null; mode?: string }> = [];
const activeMainTabCalls: string[] = []; const activeSurfaceCalls: string[] = [];
const sessionSwitcherCalls: boolean[] = []; const sessionSwitcherCalls: boolean[] = [];
const codeMirrorDispatches: Array<{ selection: { anchor: number } }> = []; const codeMirrorDispatches: Array<{ selection: { anchor: number } }> = [];
@@ -41,8 +41,8 @@ mock.module('@/sync/input-store', () => ({
mock.module('@/stores/useUIStore', () => ({ mock.module('@/stores/useUIStore', () => ({
useUIStore: { useUIStore: {
getState: () => ({ getState: () => ({
setActiveMainTab: (tab: string) => { setActiveSurface: (tab: string) => {
activeMainTabCalls.push(tab); activeSurfaceCalls.push(tab);
}, },
setSessionSwitcherOpen: (open: boolean) => { setSessionSwitcherOpen: (open: boolean) => {
sessionSwitcherCalls.push(open); sessionSwitcherCalls.push(open);
@@ -86,7 +86,7 @@ const installSelectionEnvironment = (options: {
const clearCalls = () => { const clearCalls = () => {
focusChatInputCalls.length = 0; focusChatInputCalls.length = 0;
pendingInputCalls.length = 0; pendingInputCalls.length = 0;
activeMainTabCalls.length = 0; activeSurfaceCalls.length = 0;
sessionSwitcherCalls.length = 0; sessionSwitcherCalls.length = 0;
codeMirrorDispatches.length = 0; codeMirrorDispatches.length = 0;
codeMirrorView = null; codeMirrorView = null;
@@ -262,7 +262,7 @@ describe('addSelectionToChat', () => {
installSelectionEnvironment({ activeElement: textarea }); installSelectionEnvironment({ activeElement: textarea });
expect(addSelectionToChat()).toBe(true); expect(addSelectionToChat()).toBe(true);
expect(activeMainTabCalls).toEqual(['chat']); expect(activeSurfaceCalls).toEqual(['chat']);
expect(sessionSwitcherCalls).toEqual([false]); expect(sessionSwitcherCalls).toEqual([false]);
expect(pendingInputCalls).toEqual([{ text: '```md\nselected\n```', mode: 'append' }]); expect(pendingInputCalls).toEqual([{ text: '```md\nselected\n```', mode: 'append' }]);
@@ -290,7 +290,7 @@ describe('addSelectionToChat', () => {
expect(addSelectionToChat()).toBe(false); expect(addSelectionToChat()).toBe(false);
expect(pendingInputCalls).toEqual([]); expect(pendingInputCalls).toEqual([]);
expect(activeMainTabCalls).toEqual(['chat']); expect(activeSurfaceCalls).toEqual(['chat']);
await Promise.resolve(); await Promise.resolve();
expect(focusChatInputCalls.length).toBe(1); expect(focusChatInputCalls.length).toBe(1);
+1 -1
View File
@@ -151,7 +151,7 @@ export const captureSelectionMarkdownForChat = (): string | null => {
export const addSelectionToChat = (): boolean => { export const addSelectionToChat = (): boolean => {
const markdown = captureSelectionMarkdownForChat(); const markdown = captureSelectionMarkdownForChat();
useUIStore.getState().setActiveMainTab('chat'); useUIStore.getState().setActiveSurface('chat');
useUIStore.getState().setSessionSwitcherOpen(false); useUIStore.getState().setSessionSwitcherOpen(false);
if (markdown) { if (markdown) {
+1 -1
View File
@@ -19,7 +19,7 @@ export interface RouteState {
/** /**
* Valid values for the legacy `tab` URL parameter. * Valid values for the legacy `tab` URL parameter.
*/ */
export const VALID_TABS: readonly WorkspaceSurface[] = ['chat', 'git', 'diff', 'terminal', 'files', 'diagram'] as const; export const VALID_TABS: readonly WorkspaceSurface[] = ['chat', 'git', 'diff', 'terminal', 'files'] as const;
/** /**
* Valid settings section values for URL routing. * Valid settings section values for URL routing.
+13 -57
View File
@@ -17,9 +17,7 @@ import { isVSCodeRuntime } from '@/lib/desktop';
* The primary view on mobile and the desktop's promoted full-screen view. * The primary view on mobile and the desktop's promoted full-screen view.
* Desktop context-panel content is not represented here. * Desktop context-panel content is not represented here.
*/ */
export type WorkspaceSurface = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files' | 'context' | 'diagram'; export type WorkspaceSurface = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files' | 'context';
/** @deprecated Use WorkspaceSurface. */
export type MainTab = WorkspaceSurface;
export type PendingDiffScope = 'working' | 'staged' | 'turn' | 'branch'; export type PendingDiffScope = 'working' | 'staged' | 'turn' | 'branch';
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';
@@ -84,8 +82,6 @@ type PendingFileNavigation = {
}; };
export type WorkspaceSurfaceGuard = (nextSurface: WorkspaceSurface) => boolean; export type WorkspaceSurfaceGuard = (nextSurface: WorkspaceSurface) => boolean;
/** @deprecated Use WorkspaceSurfaceGuard. */
export type MainTabGuard = WorkspaceSurfaceGuard;
export type EventStreamStatus = export type EventStreamStatus =
| 'idle' | 'idle'
| 'connecting' | 'connecting'
@@ -658,15 +654,10 @@ interface UIStore {
isSessionDropdownOpen: boolean; isSessionDropdownOpen: boolean;
activeSurface: WorkspaceSurface; activeSurface: WorkspaceSurface;
surfaceGuard: WorkspaceSurfaceGuard | 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;
pendingDiffScope: PendingDiffScope | null; pendingDiffScope: PendingDiffScope | null;
pendingDiagramFile: string | null;
pendingFileNavigation: PendingFileNavigation | null; pendingFileNavigation: PendingFileNavigation | null;
pendingFileFocusPath: string | null; pendingFileFocusPath: string | null;
isMobile: boolean; isMobile: boolean;
@@ -854,21 +845,14 @@ interface UIStore {
setSessionSwitcherOpen: (open: boolean) => void; setSessionSwitcherOpen: (open: boolean) => void;
setSessionDropdownOpen: (open: boolean) => void; setSessionDropdownOpen: (open: boolean) => void;
setActiveSurface: (surface: WorkspaceSurface) => 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;
setSurfaceGuard: (guard: WorkspaceSurfaceGuard | 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;
setPendingFileNavigation: (navigation: PendingFileNavigation | null) => void; setPendingFileNavigation: (navigation: PendingFileNavigation | null) => void;
setPendingFileFocusPath: (path: string | null) => void; setPendingFileFocusPath: (path: string | null) => void;
navigateToDiff: (filePath: string, staged?: boolean, scope?: PendingDiffScope | null) => void; navigateToDiff: (filePath: string, staged?: boolean, scope?: PendingDiffScope | null) => void;
consumePendingDiffFile: () => string | null; consumePendingDiffFile: () => string | null;
navigateToDiagram: (filePath: string) => void;
consumePendingDiagramFile: () => string | null;
setIsMobile: (isMobile: boolean) => void; setIsMobile: (isMobile: boolean) => void;
toggleCommandPalette: () => void; toggleCommandPalette: () => void;
setCommandPaletteOpen: (open: boolean) => void; setCommandPaletteOpen: (open: boolean) => void;
@@ -1037,13 +1021,10 @@ export const useUIStore = create<UIStore>()(
isSessionDropdownOpen: false, isSessionDropdownOpen: false,
activeSurface: 'chat', activeSurface: 'chat',
surfaceGuard: null, surfaceGuard: null,
activeMainTab: 'chat',
mainTabGuard: null,
sidebarOpenBeforeFullscreenTab: null, sidebarOpenBeforeFullscreenTab: null,
pendingDiffFile: null, pendingDiffFile: null,
pendingDiffStaged: false, pendingDiffStaged: false,
pendingDiffScope: null, pendingDiffScope: null,
pendingDiagramFile: null,
pendingFileNavigation: null, pendingFileNavigation: null,
pendingFileFocusPath: null, pendingFileFocusPath: null,
isMobile: false, isMobile: false,
@@ -1656,29 +1637,25 @@ export const useUIStore = create<UIStore>()(
if (get().surfaceGuard === guard) { if (get().surfaceGuard === guard) {
return; return;
} }
set({ surfaceGuard: guard, mainTabGuard: guard }); set({ surfaceGuard: guard });
}, },
setMainTabGuard: (guard) => get().setSurfaceGuard(guard),
setActiveSurface: (surface) => { setActiveSurface: (surface) => {
const guard = get().surfaceGuard; const guard = get().surfaceGuard;
if (guard && !guard(surface)) { if (guard && !guard(surface)) {
return; return;
} }
activeSurfaceByRuntime.set(runtimeMemoryKey(), surface); activeSurfaceByRuntime.set(runtimeMemoryKey(), surface);
set({ activeSurface: surface, activeMainTab: surface }); set({ activeSurface: surface });
}, },
setActiveMainTab: (surface) => get().setActiveSurface(surface),
prepareForRuntimeSwitch: (runtimeKey?: string | null) => { prepareForRuntimeSwitch: (runtimeKey?: string | null) => {
activeSurfaceByRuntime.set(runtimeMemoryKey(runtimeKey), get().activeSurface); activeSurfaceByRuntime.set(runtimeMemoryKey(runtimeKey), get().activeSurface);
}, },
restoreForRuntimeSwitch: (runtimeKey?: string | null) => { restoreForRuntimeSwitch: (runtimeKey?: string | null) => {
const restored = activeSurfaceByRuntime.get(runtimeMemoryKey(runtimeKey)) ?? 'chat'; const restored = activeSurfaceByRuntime.get(runtimeMemoryKey(runtimeKey)) ?? 'chat';
set({ activeSurface: restored, activeMainTab: restored }); set({ activeSurface: restored });
}, },
setPendingDiffFile: (filePath, staged = false, scope = null) => { setPendingDiffFile: (filePath, staged = false, scope = null) => {
@@ -1689,10 +1666,6 @@ export const useUIStore = create<UIStore>()(
}); });
}, },
setPendingDiagramFile: (filePath) => {
set({ pendingDiagramFile: filePath });
},
setPendingFileNavigation: (navigation) => { setPendingFileNavigation: (navigation) => {
set({ pendingFileNavigation: navigation }); set({ pendingFileNavigation: navigation });
}, },
@@ -1706,7 +1679,7 @@ export const useUIStore = create<UIStore>()(
if (guard && !guard('diff')) { if (guard && !guard('diff')) {
return; return;
} }
set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeSurface: 'diff', activeMainTab: 'diff' }); set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeSurface: 'diff' });
}, },
consumePendingDiffFile: () => { consumePendingDiffFile: () => {
@@ -1717,22 +1690,6 @@ export const useUIStore = create<UIStore>()(
return pendingDiffFile; return pendingDiffFile;
}, },
navigateToDiagram: (filePath) => {
const guard = get().surfaceGuard;
if (guard && !guard('diagram')) {
return;
}
set({ pendingDiagramFile: filePath, activeSurface: 'diagram', activeMainTab: 'diagram' });
},
consumePendingDiagramFile: () => {
const { pendingDiagramFile } = get();
if (pendingDiagramFile) {
set({ pendingDiagramFile: null });
}
return pendingDiagramFile;
},
setIsMobile: (isMobile) => { setIsMobile: (isMobile) => {
set({ isMobile }); set({ isMobile });
}, },
@@ -2498,18 +2455,20 @@ export const useUIStore = create<UIStore>()(
{ {
name: 'ui-store', name: 'ui-store',
storage: createDeferredSafeJSONStorage(), storage: createDeferredSafeJSONStorage(),
version: 15, version: 16,
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 // v15 -> v16: the main-area surface concept is gone from persistence
// mobile or promoted desktop view remains unchanged. // (the chat always owns the desktop main area; panel surfaces have
if (version < 15) { // their own state). Drop the historic fields so a stored non-chat
state.activeSurface = state.activeMainTab; // value cannot rehydrate into a blank main area.
state.activeMainTab = state.activeSurface; if (version < 16) {
delete state.activeMainTab;
delete state.activeSurface;
} }
// v13 -> v14: the separate 'preview' surface merged into 'browser'. // v13 -> v14: the separate 'preview' surface merged into 'browser'.
@@ -2717,9 +2676,6 @@ export const useUIStore = create<UIStore>()(
workStatusPanelEnabled: state.workStatusPanelEnabled, workStatusPanelEnabled: state.workStatusPanelEnabled,
workStatusHiddenSections: state.workStatusHiddenSections, workStatusHiddenSections: state.workStatusHiddenSections,
isSessionSwitcherOpen: state.isSessionSwitcherOpen, isSessionSwitcherOpen: state.isSessionSwitcherOpen,
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,