diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 5c9e8806..c406536a 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -626,7 +626,7 @@ function App({ apis }: AppProps) { const directory = typeof detail?.directory === 'string' && detail.directory.trim().length > 0 ? detail.directory.trim() : null; - useUIStore.getState().setActiveMainTab('chat'); + useUIStore.getState().setActiveSurface('chat'); void useSessionUIStore.getState().setCurrentSession(sessionId, directory); }; @@ -675,7 +675,7 @@ function App({ apis }: AppProps) { ? detail.projectId.trim() : null; const hasProjectTarget = Boolean(directory || projectId); - useUIStore.getState().setActiveMainTab('chat'); + useUIStore.getState().setActiveSurface('chat'); useUIStore.getState().setSessionSwitcherOpen(false); useSessionUIStore.getState().openNewSessionDraft({ target: hasProjectTarget ? 'project' : 'chat', diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx index a38e313a..e8c46c4b 100644 --- a/packages/ui/src/components/chat/ChatContainer.tsx +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -1006,8 +1006,8 @@ export const ChatContainer: React.FC = ({ return; } - const { activeMainTab } = useUIStore.getState(); - if (activeMainTab !== 'chat' || hasBlockingChatOverlay()) { + const { activeSurface } = useUIStore.getState(); + if (activeSurface !== 'chat' || hasBlockingChatOverlay()) { return; } diff --git a/packages/ui/src/components/chat/FileAttachment.tsx b/packages/ui/src/components/chat/FileAttachment.tsx index 158a78f8..7f1d4f26 100644 --- a/packages/ui/src/components/chat/FileAttachment.tsx +++ b/packages/ui/src/components/chat/FileAttachment.tsx @@ -2,6 +2,7 @@ import React, { useRef, memo } from 'react'; import { useInputStore } from '@/sync/input-store'; import type { AttachedFile } from '@/sync/session-ui-store'; import { useUIStore } from '@/stores/useUIStore'; +import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { toast } from '@/components/ui'; import { cn } from '@/lib/utils'; import { openExternalUrl } from '@/lib/url'; @@ -833,7 +834,10 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false } - - -
- -
- - ); -} diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 05ca0323..a00ce036 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -939,7 +939,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const [confirmDiscardOpen, setConfirmDiscardOpen] = React.useState(false); const pendingSelectFileRef = React.useRef(null); - const pendingTabRef = React.useRef(null); + const pendingTabRef = React.useRef(null); const pendingClosePathRef = React.useRef(null); const skipDirtyOnceRef = React.useRef(false); const copiedContentTimeoutRef = React.useRef(null); @@ -1029,7 +1029,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const [isDragging, setIsDragging] = React.useState(false); // Session/config for sending comments - const setMainTabGuard = useUIStore((state) => state.setMainTabGuard); + const setSurfaceGuard = useUIStore((state) => state.setSurfaceGuard); const pendingFileNavigation = useUIStore((state) => state.pendingFileNavigation); const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation); const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath); @@ -1098,10 +1098,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { React.useEffect(() => { setLineSelection(null); reset(); - setMainTabGuard(null); + setSurfaceGuard(null); setDraftContent(''); setIsSaving(false); - }, [selectedFile?.path, reset, setMainTabGuard]); + }, [selectedFile?.path, reset, setSurfaceGuard]); React.useEffect(() => { setCommentSelection(lineSelection); @@ -1713,11 +1713,11 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { React.useEffect(() => { if (!isDirty) { - setMainTabGuard(null); + setSurfaceGuard(null); return; } - const guard = (_nextTab: import('@/stores/useUIStore').MainTab) => { + const guard = (_nextTab: import('@/stores/useUIStore').WorkspaceSurface) => { if (skipDirtyOnceRef.current) { skipDirtyOnceRef.current = false; return true; @@ -1727,15 +1727,15 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return false; }; - setMainTabGuard(guard); + setSurfaceGuard(guard); return () => { - const currentGuard = useUIStore.getState().mainTabGuard; + const currentGuard = useUIStore.getState().surfaceGuard; if (currentGuard === guard) { - setMainTabGuard(null); + setSurfaceGuard(null); } }; - }, [isDirty, setMainTabGuard]); + }, [isDirty, setSurfaceGuard]); React.useEffect(() => { if (autoSaveEnabled) { @@ -2180,10 +2180,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { } if (nextTab) { - setMainTabGuard(null); - useUIStore.getState().setActiveMainTab(nextTab); + setSurfaceGuard(null); + 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 nextFile = pendingSelectFileRef.current; @@ -2234,10 +2234,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { } if (nextTab) { - setMainTabGuard(null); - useUIStore.getState().setActiveMainTab(nextTab); + setSurfaceGuard(null); + 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 isActive = selectedFile?.path === path; diff --git a/packages/ui/src/components/views/PlanView.tsx b/packages/ui/src/components/views/PlanView.tsx index 6efac6d0..a1ca3111 100644 --- a/packages/ui/src/components/views/PlanView.tsx +++ b/packages/ui/src/components/views/PlanView.tsx @@ -168,7 +168,7 @@ export const PlanView: React.FC = ({ targetPath = null, projectPl const activeProjectId = useProjectsStore((state) => state.activeProjectId); const gitDirectories = useGitStore((state) => state.directories); const effectiveDirectory = useEffectiveDirectory() ?? ''; - const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); + const setActiveSurface = useUIStore((state) => state.setActiveSurface); const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const runtimeApis = useRuntimeAPIs(); const { isMobile } = useDeviceInfo(); @@ -579,10 +579,10 @@ export const PlanView: React.FC = ({ targetPath = null, projectPl }, []); const routeToChat = React.useCallback(() => { - setActiveMainTab('chat'); + setActiveSurface('chat'); setSessionSwitcherOpen(false); onNavigatedToChat?.(); - }, [onNavigatedToChat, setActiveMainTab, setSessionSwitcherOpen]); + }, [onNavigatedToChat, setActiveSurface, setSessionSwitcherOpen]); const handleConfirmPlanSend = React.useCallback( async (execution: TodoSendExecution) => { diff --git a/packages/ui/src/components/views/git/ConflictDialog.tsx b/packages/ui/src/components/views/git/ConflictDialog.tsx index bd75bc4e..3df26e6a 100644 --- a/packages/ui/src/components/views/git/ConflictDialog.tsx +++ b/packages/ui/src/components/views/git/ConflictDialog.tsx @@ -41,7 +41,7 @@ export const ConflictDialog: React.FC = ({ const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const setPendingInputText = useInputStore((state) => state.setPendingInputText); 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 [conflictDetails, setConflictDetails] = React.useState(null); @@ -137,7 +137,7 @@ export const ConflictDialog: React.FC = ({ { text: context.payloadText, synthetic: true }, ]); - setActiveMainTab('chat'); + setActiveSurface('chat'); onClearState?.(); onOpenChange(false); }; @@ -159,7 +159,7 @@ export const ConflictDialog: React.FC = ({ ], }); // Navigate to chat tab so user sees the new session - setActiveMainTab('chat'); + setActiveSurface('chat'); onClearState?.(); onOpenChange(false); }; diff --git a/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx b/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx index fa750251..2d8db8ea 100644 --- a/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx +++ b/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx @@ -65,7 +65,7 @@ export const IntegrateCommitsSection: React.FC<{ }) => { const { t } = useI18n(); 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 [branchSearch, setBranchSearch] = React.useState(''); const searchInputRef = React.useRef(null); @@ -236,7 +236,7 @@ export const IntegrateCommitsSection: React.FC<{ ], }); // Navigate to chat tab so user sees the new session - setActiveMainTab('chat'); + setActiveSurface('chat'); return; } @@ -251,8 +251,8 @@ export const IntegrateCommitsSection: React.FC<{ { text: context.instructionsText, synthetic: true }, { text: context.payloadText, synthetic: true }, ]); - setActiveMainTab('chat'); - }, [currentSessionId, setActiveMainTab, buildConflictContext, openNewSessionDraft, setPendingInputText, setPendingSyntheticParts, t]); + setActiveSurface('chat'); + }, [currentSessionId, setActiveSurface, buildConflictContext, openNewSessionDraft, setPendingInputText, setPendingSyntheticParts, t]); const handleMove = React.useCallback(async () => { if (ui.kind !== 'ready') return; diff --git a/packages/ui/src/components/views/git/PullRequestSection.tsx b/packages/ui/src/components/views/git/PullRequestSection.tsx index 3a258526..cb0dad8f 100644 --- a/packages/ui/src/components/views/git/PullRequestSection.tsx +++ b/packages/ui/src/components/views/git/PullRequestSection.tsx @@ -327,7 +327,7 @@ export const PullRequestSection: React.FC<{ const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked); const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen); 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 newSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open)); const { isMobile, hasTouchInput, screenWidth } = useDeviceInfo(); @@ -986,14 +986,14 @@ export const PullRequestSection: React.FC<{ text: '', }); } - setActiveMainTab('chat'); + setActiveSurface('chat'); } catch (e) { const message = e instanceof Error ? e.message : String(e); toast.error(t('gitView.pr.toast.loadChecksFailed'), { description: message }); } finally { 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 () => { if (!github?.prContext) { @@ -1021,14 +1021,14 @@ export const PullRequestSection: React.FC<{ for (const comment of timelineComments) { attachCommentDraft(target, comment); } - setActiveMainTab('chat'); + setActiveSurface('chat'); } catch (e) { const message = e instanceof Error ? e.message : String(e); toast.error(t('gitView.pr.toast.loadPrCommentsFailed'), { description: message }); } finally { 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 target = resolveDraftTarget(); @@ -1037,8 +1037,8 @@ export const PullRequestSection: React.FC<{ } attachCommentDraft(target, comment); - setActiveMainTab('chat'); - }, [attachCommentDraft, resolveDraftTarget, setActiveMainTab]); + setActiveSurface('chat'); + }, [attachCommentDraft, resolveDraftTarget, setActiveSurface]); const refresh = React.useCallback(async (options?: { force?: boolean; onlyExistingPr?: boolean; silent?: boolean; markInitialResolved?: boolean }) => { await refreshPrStatus(prStatusKey, options); diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts index 114bc0b4..f66b66ff 100644 --- a/packages/ui/src/hooks/useKeyboardShortcuts.ts +++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts @@ -59,7 +59,7 @@ export const useKeyboardShortcuts = () => { }, [currentShortcutDirectory]); const isMobile = useUIStore((s) => s.isMobile); 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 setModelSelectorOpen = useUIStore((s) => s.setModelSelectorOpen); const setTimelineDialogOpen = useUIStore((s) => s.setTimelineDialogOpen); @@ -154,7 +154,7 @@ export const useKeyboardShortcuts = () => { isAboutDialogOpen, isMultiRunLauncherOpen, isImagePreviewOpen, - activeMainTab, + activeSurface, isPromptNavigatorPanelOpen, } = useUIStore.getState(); @@ -183,7 +183,7 @@ export const useKeyboardShortcuts = () => { } const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen || isMultiRunLauncherOpen || isImagePreviewOpen; - const isChatActive = activeMainTab === 'chat'; + const isChatActive = activeSurface === 'chat'; if (hasOverlay || !isChatActive) { resetAbortPriming(); @@ -245,7 +245,7 @@ export const useKeyboardShortcuts = () => { if (eventMatchesShortcut(e, combo('toggle_prompt_navigator'))) { const { - activeMainTab, + activeSurface, promptNavigatorEnabled, isSettingsDialogOpen, isCommandPaletteOpen, @@ -257,7 +257,7 @@ export const useKeyboardShortcuts = () => { isImagePreviewOpen, } = useUIStore.getState(); - if (!promptNavigatorEnabled || isMobile || isVSCodeRuntime() || activeMainTab !== 'chat') { + if (!promptNavigatorEnabled || isMobile || isVSCodeRuntime() || activeSurface !== 'chat') { return; } @@ -308,7 +308,7 @@ export const useKeyboardShortcuts = () => { if (matchedNewSessionShortcut || matchedWorktreeShortcut) { e.preventDefault(); - setActiveMainTab('chat'); + setActiveSurface('chat'); setSessionSwitcherOpen(false); if (!isVSCodeRuntime() && matchedWorktreeShortcut) { @@ -394,11 +394,11 @@ export const useKeyboardShortcuts = () => { isHelpDialogOpen, isSessionSwitcherOpen, isAboutDialogOpen, - activeMainTab, + activeSurface, } = useUIStore.getState(); const hasOverlay = isSettingsDialogOpen || isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen; - if (hasOverlay || activeMainTab !== 'chat' || !isChatInputTarget(e.target)) { + if (hasOverlay || activeSurface !== 'chat' || !isChatInputTarget(e.target)) { return; } @@ -525,7 +525,7 @@ export const useKeyboardShortcuts = () => { isHelpDialogOpen, isSessionSwitcherOpen, isAboutDialogOpen, - activeMainTab, + activeSurface, isModelSelectorOpen, } = useUIStore.getState(); @@ -536,7 +536,7 @@ export const useKeyboardShortcuts = () => { // Skip if any overlay open or not on chat tab const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen; - const isChatActive = activeMainTab === 'chat'; + const isChatActive = activeSurface === 'chat'; if (hasOverlay || !isChatActive) { return; @@ -555,7 +555,7 @@ export const useKeyboardShortcuts = () => { isHelpDialogOpen, isSessionSwitcherOpen, isAboutDialogOpen, - activeMainTab, + activeSurface, } = useUIStore.getState(); if (isSettingsDialogOpen) { @@ -563,7 +563,7 @@ export const useKeyboardShortcuts = () => { } const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen; - const isChatActive = activeMainTab === 'chat'; + const isChatActive = activeSurface === 'chat'; if (hasOverlay || !isChatActive) { return; @@ -602,7 +602,7 @@ export const useKeyboardShortcuts = () => { isHelpDialogOpen, isSessionSwitcherOpen, isAboutDialogOpen, - activeMainTab, + activeSurface, favoriteModels, addRecentModel, } = useUIStore.getState(); @@ -612,7 +612,7 @@ export const useKeyboardShortcuts = () => { } const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen; - const isChatActive = activeMainTab === 'chat'; + const isChatActive = activeSurface === 'chat'; if (hasOverlay || !isChatActive || favoriteModels.length === 0) { return; @@ -644,8 +644,8 @@ export const useKeyboardShortcuts = () => { } if (eventMatchesShortcut(e, combo('toggle_dictation'))) { - const { activeMainTab, isCommandPaletteOpen, isHelpDialogOpen, isSessionSwitcherOpen, isSettingsDialogOpen } = useUIStore.getState(); - if (activeMainTab !== 'chat' || isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isSettingsDialogOpen) { + const { activeSurface, isCommandPaletteOpen, isHelpDialogOpen, isSessionSwitcherOpen, isSettingsDialogOpen } = useUIStore.getState(); + if (activeSurface !== 'chat' || isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isSettingsDialogOpen) { return; } e.preventDefault(); @@ -695,7 +695,7 @@ export const useKeyboardShortcuts = () => { toggleTerminalSurfaceExpanded, isMobile, setSessionSwitcherOpen, - setActiveMainTab, + setActiveSurface, setSettingsDialogOpen, setModelSelectorOpen, setTimelineDialogOpen, diff --git a/packages/ui/src/hooks/useMenuActions.ts b/packages/ui/src/hooks/useMenuActions.ts index 48b4093b..03f16abf 100644 --- a/packages/ui/src/hooks/useMenuActions.ts +++ b/packages/ui/src/hooks/useMenuActions.ts @@ -102,7 +102,7 @@ export const useMenuActions = ( const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog); const toggleSidebar = useUIStore((s) => s.toggleSidebar); 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 setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen); const checkForUpdates = useUpdateStore((state) => state.checkForUpdates); @@ -151,10 +151,10 @@ export const useMenuActions = ( const nextSession = sessions[nextIndex]; if (!nextSession) return; - setActiveMainTab('chat'); + setActiveSurface('chat'); setSessionSwitcherOpen(false); useSessionUIStore.getState().setCurrentSession(nextSession.id); - }, [setActiveMainTab, setSessionSwitcherOpen]); + }, [setActiveSurface, setSessionSwitcherOpen]); const navigateProject = React.useCallback((direction: -1 | 1) => { const { activeProjectId, projects, setActiveProject } = useProjectsStore.getState(); @@ -191,7 +191,7 @@ export const useMenuActions = ( break; case 'new-session': - setActiveMainTab('chat'); + setActiveSurface('chat'); setSessionSwitcherOpen(false); { const sessionState = useSessionUIStore.getState(); @@ -203,7 +203,7 @@ export const useMenuActions = ( break; case 'new-worktree-session': - setActiveMainTab('chat'); + setActiveSurface('chat'); setSessionSwitcherOpen(false); createWorktreeSession(); break; @@ -341,7 +341,7 @@ export const useMenuActions = ( onToggleMemoryDebug, openNewSessionDraft, setAboutDialogOpen, - setActiveMainTab, + setActiveSurface, setSessionSwitcherOpen, setCommandPaletteOpen, setSettingsDialogOpen, diff --git a/packages/ui/src/hooks/useRouter.ts b/packages/ui/src/hooks/useRouter.ts index 1e34e765..a1d3d5ca 100644 --- a/packages/ui/src/hooks/useRouter.ts +++ b/packages/ui/src/hooks/useRouter.ts @@ -1,11 +1,12 @@ import React from 'react'; 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 type { RouteState, AppRouteState } from '@/lib/router'; import type { WorkspaceSurface } from '@/stores/useUIStore'; import { resolveSettingsSlug } from '@/lib/settings/metadata'; import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat'; +import { useDirectoryStore } from '@/stores/useDirectoryStore'; /** * Check if running in VS Code webview context. @@ -88,9 +89,16 @@ export function useRouter(): void { setSettingsDialogOpen(false); } - // 3. Apply the view selected by the legacy URL parameter. - if (route.tab) { - setActiveSurface(route.tab); + // 3. Apply the view selected by the legacy URL parameter. Desktop + // surfaces live in the context panel, so a non-chat tab deep link + // 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) diff --git a/packages/ui/src/lib/addSelectionToChat.test.ts b/packages/ui/src/lib/addSelectionToChat.test.ts index 6acc225a..65a50d33 100644 --- a/packages/ui/src/lib/addSelectionToChat.test.ts +++ b/packages/ui/src/lib/addSelectionToChat.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'; const focusChatInputCalls: number[] = []; const pendingInputCalls: Array<{ text: string | null; mode?: string }> = []; -const activeMainTabCalls: string[] = []; +const activeSurfaceCalls: string[] = []; const sessionSwitcherCalls: boolean[] = []; const codeMirrorDispatches: Array<{ selection: { anchor: number } }> = []; @@ -41,8 +41,8 @@ mock.module('@/sync/input-store', () => ({ mock.module('@/stores/useUIStore', () => ({ useUIStore: { getState: () => ({ - setActiveMainTab: (tab: string) => { - activeMainTabCalls.push(tab); + setActiveSurface: (tab: string) => { + activeSurfaceCalls.push(tab); }, setSessionSwitcherOpen: (open: boolean) => { sessionSwitcherCalls.push(open); @@ -86,7 +86,7 @@ const installSelectionEnvironment = (options: { const clearCalls = () => { focusChatInputCalls.length = 0; pendingInputCalls.length = 0; - activeMainTabCalls.length = 0; + activeSurfaceCalls.length = 0; sessionSwitcherCalls.length = 0; codeMirrorDispatches.length = 0; codeMirrorView = null; @@ -262,7 +262,7 @@ describe('addSelectionToChat', () => { installSelectionEnvironment({ activeElement: textarea }); expect(addSelectionToChat()).toBe(true); - expect(activeMainTabCalls).toEqual(['chat']); + expect(activeSurfaceCalls).toEqual(['chat']); expect(sessionSwitcherCalls).toEqual([false]); expect(pendingInputCalls).toEqual([{ text: '```md\nselected\n```', mode: 'append' }]); @@ -290,7 +290,7 @@ describe('addSelectionToChat', () => { expect(addSelectionToChat()).toBe(false); expect(pendingInputCalls).toEqual([]); - expect(activeMainTabCalls).toEqual(['chat']); + expect(activeSurfaceCalls).toEqual(['chat']); await Promise.resolve(); expect(focusChatInputCalls.length).toBe(1); diff --git a/packages/ui/src/lib/addSelectionToChat.ts b/packages/ui/src/lib/addSelectionToChat.ts index ce296569..a35e892b 100644 --- a/packages/ui/src/lib/addSelectionToChat.ts +++ b/packages/ui/src/lib/addSelectionToChat.ts @@ -151,7 +151,7 @@ export const captureSelectionMarkdownForChat = (): string | null => { export const addSelectionToChat = (): boolean => { const markdown = captureSelectionMarkdownForChat(); - useUIStore.getState().setActiveMainTab('chat'); + useUIStore.getState().setActiveSurface('chat'); useUIStore.getState().setSessionSwitcherOpen(false); if (markdown) { diff --git a/packages/ui/src/lib/router/types.ts b/packages/ui/src/lib/router/types.ts index 9c5801e2..25c838a6 100644 --- a/packages/ui/src/lib/router/types.ts +++ b/packages/ui/src/lib/router/types.ts @@ -19,7 +19,7 @@ export interface RouteState { /** * 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. diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index 85f4ad02..2a1ca610 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -17,9 +17,7 @@ import { isVSCodeRuntime } from '@/lib/desktop'; * The primary view on mobile and the desktop's promoted full-screen view. * Desktop context-panel content is not represented here. */ -export type WorkspaceSurface = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files' | 'context' | 'diagram'; -/** @deprecated Use WorkspaceSurface. */ -export type MainTab = WorkspaceSurface; +export type WorkspaceSurface = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files' | 'context'; export type PendingDiffScope = 'working' | 'staged' | 'turn' | 'branch'; export type ContextPanelMode = 'diff' | 'walkthrough' | 'file' | 'context' | 'plan' | 'chat' | 'browser' | 'git' | 'pr' | 'notes' | 'terminal'; export type MermaidRenderingMode = 'svg' | 'ascii'; @@ -84,8 +82,6 @@ type PendingFileNavigation = { }; export type WorkspaceSurfaceGuard = (nextSurface: WorkspaceSurface) => boolean; -/** @deprecated Use WorkspaceSurfaceGuard. */ -export type MainTabGuard = WorkspaceSurfaceGuard; export type EventStreamStatus = | 'idle' | 'connecting' @@ -658,15 +654,10 @@ interface UIStore { isSessionDropdownOpen: boolean; activeSurface: WorkspaceSurface; surfaceGuard: WorkspaceSurfaceGuard | null; - /** @deprecated Use activeSurface. */ - activeMainTab: WorkspaceSurface; - /** @deprecated Use surfaceGuard. */ - mainTabGuard: WorkspaceSurfaceGuard | null; sidebarOpenBeforeFullscreenTab: boolean | null; pendingDiffFile: string | null; pendingDiffStaged: boolean; pendingDiffScope: PendingDiffScope | null; - pendingDiagramFile: string | null; pendingFileNavigation: PendingFileNavigation | null; pendingFileFocusPath: string | null; isMobile: boolean; @@ -854,21 +845,14 @@ interface UIStore { setSessionSwitcherOpen: (open: boolean) => void; setSessionDropdownOpen: (open: boolean) => void; setActiveSurface: (surface: WorkspaceSurface) => void; - /** @deprecated Use setActiveSurface. */ - setActiveMainTab: (surface: WorkspaceSurface) => void; prepareForRuntimeSwitch: (runtimeKey?: string | null) => void; restoreForRuntimeSwitch: (runtimeKey?: string | null) => void; setSurfaceGuard: (guard: WorkspaceSurfaceGuard | null) => void; - /** @deprecated Use setSurfaceGuard. */ - setMainTabGuard: (guard: WorkspaceSurfaceGuard | null) => void; setPendingDiffFile: (filePath: string | null, staged?: boolean, scope?: PendingDiffScope | null) => void; - setPendingDiagramFile: (filePath: string | null) => void; setPendingFileNavigation: (navigation: PendingFileNavigation | null) => void; setPendingFileFocusPath: (path: string | null) => void; navigateToDiff: (filePath: string, staged?: boolean, scope?: PendingDiffScope | null) => void; consumePendingDiffFile: () => string | null; - navigateToDiagram: (filePath: string) => void; - consumePendingDiagramFile: () => string | null; setIsMobile: (isMobile: boolean) => void; toggleCommandPalette: () => void; setCommandPaletteOpen: (open: boolean) => void; @@ -1037,13 +1021,10 @@ export const useUIStore = create()( isSessionDropdownOpen: false, activeSurface: 'chat', surfaceGuard: null, - activeMainTab: 'chat', - mainTabGuard: null, sidebarOpenBeforeFullscreenTab: null, pendingDiffFile: null, pendingDiffStaged: false, pendingDiffScope: null, - pendingDiagramFile: null, pendingFileNavigation: null, pendingFileFocusPath: null, isMobile: false, @@ -1656,29 +1637,25 @@ export const useUIStore = create()( if (get().surfaceGuard === guard) { return; } - set({ surfaceGuard: guard, mainTabGuard: guard }); + set({ surfaceGuard: guard }); }, - setMainTabGuard: (guard) => get().setSurfaceGuard(guard), - setActiveSurface: (surface) => { const guard = get().surfaceGuard; if (guard && !guard(surface)) { return; } activeSurfaceByRuntime.set(runtimeMemoryKey(), surface); - set({ activeSurface: surface, activeMainTab: surface }); + set({ activeSurface: surface }); }, - setActiveMainTab: (surface) => get().setActiveSurface(surface), - prepareForRuntimeSwitch: (runtimeKey?: string | null) => { activeSurfaceByRuntime.set(runtimeMemoryKey(runtimeKey), get().activeSurface); }, restoreForRuntimeSwitch: (runtimeKey?: string | null) => { const restored = activeSurfaceByRuntime.get(runtimeMemoryKey(runtimeKey)) ?? 'chat'; - set({ activeSurface: restored, activeMainTab: restored }); + set({ activeSurface: restored }); }, setPendingDiffFile: (filePath, staged = false, scope = null) => { @@ -1689,10 +1666,6 @@ export const useUIStore = create()( }); }, - setPendingDiagramFile: (filePath) => { - set({ pendingDiagramFile: filePath }); - }, - setPendingFileNavigation: (navigation) => { set({ pendingFileNavigation: navigation }); }, @@ -1706,7 +1679,7 @@ export const useUIStore = create()( if (guard && !guard('diff')) { return; } - set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeSurface: 'diff', activeMainTab: 'diff' }); + set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeSurface: 'diff' }); }, consumePendingDiffFile: () => { @@ -1717,22 +1690,6 @@ export const useUIStore = create()( 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) => { set({ isMobile }); }, @@ -2498,18 +2455,20 @@ export const useUIStore = create()( { name: 'ui-store', storage: createDeferredSafeJSONStorage(), - version: 15, + version: 16, migrate: (persistedState, version) => { if (!persistedState || typeof persistedState !== 'object') { return persistedState; } const state = persistedState as Record; - // v14 -> v15: rename the historic main-tab field. The selected - // mobile or promoted desktop view remains unchanged. - if (version < 15) { - state.activeSurface = state.activeMainTab; - state.activeMainTab = state.activeSurface; + // v15 -> v16: the main-area surface concept is gone from persistence + // (the chat always owns the desktop main area; panel surfaces have + // their own state). Drop the historic fields so a stored non-chat + // value cannot rehydrate into a blank main area. + if (version < 16) { + delete state.activeMainTab; + delete state.activeSurface; } // v13 -> v14: the separate 'preview' surface merged into 'browser'. @@ -2717,9 +2676,6 @@ export const useUIStore = create()( workStatusPanelEnabled: state.workStatusPanelEnabled, workStatusHiddenSections: state.workStatusHiddenSections, isSessionSwitcherOpen: state.isSessionSwitcherOpen, - activeSurface: state.activeSurface, - // Keep the deprecated mirror synchronized while consumers migrate. - activeMainTab: state.activeSurface, sidebarSection: state.sidebarSection, settingsPage: state.settingsPage, settingsHasOpenedOnce: state.settingsHasOpenedOnce,