diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx index abbd2446669..c0f1760f38c 100644 --- a/src/renderer/src/components/Terminal.tsx +++ b/src/renderer/src/components/Terminal.tsx @@ -44,6 +44,11 @@ export default function Terminal(): React.JSX.Element | null { const tabs = activeWorktreeId ? (tabsByWorktree[activeWorktreeId] ?? []) : [] const allWorktrees = Object.values(worktreesByRepo).flat() + // Filter editor files to only show those belonging to the active worktree + const worktreeFiles = activeWorktreeId + ? openFiles.filter((f) => f.worktreeId === activeWorktreeId) + : [] + // Save confirmation dialog state const [saveDialogFileId, setSaveDialogFileId] = useState(null) const saveDialogFile = saveDialogFileId ? openFiles.find((f) => f.id === saveDialogFileId) : null @@ -140,7 +145,7 @@ export default function Terminal(): React.JSX.Element | null { createTab(activeWorktreeId) }, [workspaceSessionReady, activeWorktreeId, tabs.length, createTab]) - const totalTabs = tabs.length + openFiles.length + const totalTabs = tabs.length + worktreeFiles.length const handleNewTab = useCallback(() => { if (!activeWorktreeId) { @@ -281,7 +286,9 @@ export default function Terminal(): React.JSX.Element | null { if (e.metaKey && e.shiftKey && (e.key === ']' || e.key === '[') && !e.repeat) { const state = useAppStore.getState() const currentTerminalTabs = state.tabsByWorktree[activeWorktreeId] ?? [] - const currentEditorFiles = state.openFiles + const currentEditorFiles = activeWorktreeId + ? state.openFiles.filter((f) => f.worktreeId === activeWorktreeId) + : [] // Build unified tab list: terminal tabs then editor tabs const allTabIds: { type: 'terminal' | 'editor'; id: string }[] = [ @@ -347,7 +354,7 @@ export default function Terminal(): React.JSX.Element | null { onSetTabColor={setTabColor} expandedPaneByTabId={expandedPaneByTabId} onTogglePaneExpand={handleTogglePaneExpand} - editorFiles={openFiles} + editorFiles={worktreeFiles} activeFileId={activeFileId} activeTabType={activeTabType} onActivateFile={(fileId) => { @@ -363,7 +370,7 @@ export default function Terminal(): React.JSX.Element | null { {/* Terminal panes container - hidden when editor tab active */}
0 ? 'hidden' : ''}`} + className={`relative flex-1 min-h-0 overflow-hidden ${activeTabType === 'editor' && worktreeFiles.length > 0 ? 'hidden' : ''}`} > {allWorktrees .filter((wt) => mountedWorktreeIdsRef.current.has(wt.id)) @@ -393,7 +400,7 @@ export default function Terminal(): React.JSX.Element | null {
{/* Editor panel - shown when editor tab is active */} - {activeWorktreeId && activeTabType === 'editor' && openFiles.length > 0 && ( + {activeWorktreeId && activeTabType === 'editor' && worktreeFiles.length > 0 && ( diff --git a/src/renderer/src/store/slices/editor.ts b/src/renderer/src/store/slices/editor.ts index 2e8e8e5e59a..016e518ef9f 100644 --- a/src/renderer/src/store/slices/editor.ts +++ b/src/renderer/src/store/slices/editor.ts @@ -2,7 +2,7 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' import type { GitStatusEntry } from '../../../../shared/types' -export interface OpenFile { +export type OpenFile = { id: string // use filePath as unique key filePath: string // absolute path relativePath: string // relative to worktree root @@ -15,7 +15,7 @@ export interface OpenFile { export type RightSidebarTab = 'explorer' | 'source-control' -export interface EditorSlice { +export type EditorSlice = { // Right sidebar rightSidebarOpen: boolean rightSidebarWidth: number @@ -32,6 +32,8 @@ export interface EditorSlice { // Open files / editor tabs openFiles: OpenFile[] activeFileId: string | null + activeFileIdByWorktree: Record // worktreeId -> last active file + activeTabTypeByWorktree: Record // worktreeId -> last active tab type activeTabType: 'terminal' | 'editor' setActiveTabType: (type: 'terminal' | 'editor') => void openFile: (file: Omit) => void @@ -80,57 +82,138 @@ export const createEditorSlice: StateCreator = (s // Open files openFiles: [], activeFileId: null, + activeFileIdByWorktree: {}, + activeTabTypeByWorktree: {}, activeTabType: 'terminal', - setActiveTabType: (type) => set({ activeTabType: type }), + setActiveTabType: (type) => + set((s) => { + const worktreeId = s.activeWorktreeId + return { + activeTabType: type, + activeTabTypeByWorktree: worktreeId + ? { ...s.activeTabTypeByWorktree, [worktreeId]: type } + : s.activeTabTypeByWorktree + } + }), openFile: (file) => set((s) => { const id = file.filePath const existing = s.openFiles.find((f) => f.id === id) + const worktreeId = file.worktreeId if (existing) { - // If it's already open, just activate it (and update mode if needed) if (existing.mode === file.mode && existing.diffStaged === file.diffStaged) { - return { activeFileId: id, activeTabType: 'editor' } + return { + activeFileId: id, + activeTabType: 'editor', + activeFileIdByWorktree: { ...s.activeFileIdByWorktree, [worktreeId]: id }, + activeTabTypeByWorktree: { ...s.activeTabTypeByWorktree, [worktreeId]: 'editor' } + } } - // Update the existing file entry return { openFiles: s.openFiles.map((f) => f.id === id ? { ...f, mode: file.mode, diffStaged: file.diffStaged } : f ), activeFileId: id, - activeTabType: 'editor' + activeTabType: 'editor', + activeFileIdByWorktree: { ...s.activeFileIdByWorktree, [worktreeId]: id }, + activeTabTypeByWorktree: { ...s.activeTabTypeByWorktree, [worktreeId]: 'editor' } } } return { openFiles: [...s.openFiles, { ...file, id, isDirty: false }], activeFileId: id, - activeTabType: 'editor' + activeTabType: 'editor', + activeFileIdByWorktree: { ...s.activeFileIdByWorktree, [worktreeId]: id }, + activeTabTypeByWorktree: { ...s.activeTabTypeByWorktree, [worktreeId]: 'editor' } } }), closeFile: (fileId) => set((s) => { + const closedFile = s.openFiles.find((f) => f.id === fileId) const idx = s.openFiles.findIndex((f) => f.id === fileId) const newFiles = s.openFiles.filter((f) => f.id !== fileId) let newActiveId = s.activeFileId + const newActiveFileIdByWorktree = { ...s.activeFileIdByWorktree } + if (s.activeFileId === fileId) { - // Activate adjacent tab - if (newFiles.length === 0) { + // Find next file within the same worktree + const worktreeId = closedFile?.worktreeId + const worktreeFiles = worktreeId + ? newFiles.filter((f) => f.worktreeId === worktreeId) + : newFiles + if (worktreeFiles.length === 0) { newActiveId = null - } else if (idx >= newFiles.length) { - newActiveId = newFiles[newFiles.length - 1].id } else { - newActiveId = newFiles[idx].id + // Pick adjacent file from same worktree + const closedWorktreeIdx = worktreeId + ? s.openFiles + .filter((f) => f.worktreeId === worktreeId) + .findIndex((f) => f.id === fileId) + : idx + newActiveId = + closedWorktreeIdx >= worktreeFiles.length + ? worktreeFiles.at(-1)!.id + : worktreeFiles[closedWorktreeIdx].id + } + if (worktreeId) { + newActiveFileIdByWorktree[worktreeId] = newActiveId } } - // When last editor file is closed, switch back to terminal - const newActiveTabType = newFiles.length === 0 ? 'terminal' : s.activeTabType - return { openFiles: newFiles, activeFileId: newActiveId, activeTabType: newActiveTabType } + + // When last editor file for current worktree is closed, switch back to terminal + const activeWorktreeId = s.activeWorktreeId + const remainingForWorktree = activeWorktreeId + ? newFiles.filter((f) => f.worktreeId === activeWorktreeId) + : newFiles + const newActiveTabType = remainingForWorktree.length === 0 ? 'terminal' : s.activeTabType + const newActiveTabTypeByWorktree = { ...s.activeTabTypeByWorktree } + if (activeWorktreeId && remainingForWorktree.length === 0) { + newActiveTabTypeByWorktree[activeWorktreeId] = 'terminal' + } + + return { + openFiles: newFiles, + activeFileId: newActiveId, + activeTabType: newActiveTabType, + activeFileIdByWorktree: newActiveFileIdByWorktree, + activeTabTypeByWorktree: newActiveTabTypeByWorktree + } }), - closeAllFiles: () => set({ openFiles: [], activeFileId: null, activeTabType: 'terminal' }), + closeAllFiles: () => + set((s) => { + const activeWorktreeId = s.activeWorktreeId + if (!activeWorktreeId) { + return { openFiles: [], activeFileId: null, activeTabType: 'terminal' } + } + // Only close files for the current worktree + const newFiles = s.openFiles.filter((f) => f.worktreeId !== activeWorktreeId) + const newActiveFileIdByWorktree = { ...s.activeFileIdByWorktree } + delete newActiveFileIdByWorktree[activeWorktreeId] + const newActiveTabTypeByWorktree = { ...s.activeTabTypeByWorktree } + newActiveTabTypeByWorktree[activeWorktreeId] = 'terminal' + return { + openFiles: newFiles, + activeFileId: null, + activeTabType: 'terminal', + activeFileIdByWorktree: newActiveFileIdByWorktree, + activeTabTypeByWorktree: newActiveTabTypeByWorktree + } + }), - setActiveFile: (fileId) => set({ activeFileId: fileId }), + setActiveFile: (fileId) => + set((s) => { + const file = s.openFiles.find((f) => f.id === fileId) + const worktreeId = file?.worktreeId + return { + activeFileId: fileId, + activeFileIdByWorktree: worktreeId + ? { ...s.activeFileIdByWorktree, [worktreeId]: fileId } + : s.activeFileIdByWorktree + } + }), markFileDirty: (fileId, dirty) => set((s) => ({ @@ -139,11 +222,15 @@ export const createEditorSlice: StateCreator = (s openDiff: (worktreeId, filePath, relativePath, language, staged) => set((s) => { - // Use a unique ID that includes staging state to allow both staged and unstaged diffs const id = `${filePath}${staged ? '::staged' : ''}` const existing = s.openFiles.find((f) => f.id === id) if (existing) { - return { activeFileId: id, activeTabType: 'editor' } + return { + activeFileId: id, + activeTabType: 'editor', + activeFileIdByWorktree: { ...s.activeFileIdByWorktree, [worktreeId]: id }, + activeTabTypeByWorktree: { ...s.activeTabTypeByWorktree, [worktreeId]: 'editor' } + } } const newFile: OpenFile = { id, @@ -158,7 +245,9 @@ export const createEditorSlice: StateCreator = (s return { openFiles: [...s.openFiles, newFile], activeFileId: id, - activeTabType: 'editor' + activeTabType: 'editor', + activeFileIdByWorktree: { ...s.activeFileIdByWorktree, [worktreeId]: id }, + activeTabTypeByWorktree: { ...s.activeTabTypeByWorktree, [worktreeId]: 'editor' } } }), @@ -167,7 +256,12 @@ export const createEditorSlice: StateCreator = (s const id = `${worktreeId}::all-diffs` const existing = s.openFiles.find((f) => f.id === id) if (existing) { - return { activeFileId: id, activeTabType: 'editor' } + return { + activeFileId: id, + activeTabType: 'editor', + activeFileIdByWorktree: { ...s.activeFileIdByWorktree, [worktreeId]: id }, + activeTabTypeByWorktree: { ...s.activeTabTypeByWorktree, [worktreeId]: 'editor' } + } } const newFile: OpenFile = { id, @@ -182,7 +276,9 @@ export const createEditorSlice: StateCreator = (s return { openFiles: [...s.openFiles, newFile], activeFileId: id, - activeTabType: 'editor' + activeTabType: 'editor', + activeFileIdByWorktree: { ...s.activeFileIdByWorktree, [worktreeId]: id }, + activeTabTypeByWorktree: { ...s.activeTabTypeByWorktree, [worktreeId]: 'editor' } } }), diff --git a/src/renderer/src/store/slices/worktrees.ts b/src/renderer/src/store/slices/worktrees.ts index a03ef933f03..02754e7c8f8 100644 --- a/src/renderer/src/store/slices/worktrees.ts +++ b/src/renderer/src/store/slices/worktrees.ts @@ -96,6 +96,16 @@ export const createWorktreeSlice: StateCreator } const nextDeleteState = { ...s.deleteStateByWorktreeId } delete nextDeleteState[worktreeId] + // Clean up editor files belonging to this worktree + const newOpenFiles = s.openFiles.filter((f) => f.worktreeId !== worktreeId) + const nextActiveFileIdByWorktree = { ...s.activeFileIdByWorktree } + delete nextActiveFileIdByWorktree[worktreeId] + const nextActiveTabTypeByWorktree = { ...s.activeTabTypeByWorktree } + delete nextActiveTabTypeByWorktree[worktreeId] + // If the active file belonged to the removed worktree, clear it + const activeFileCleared = s.activeFileId + ? s.openFiles.some((f) => f.id === s.activeFileId && f.worktreeId === worktreeId) + : false return { worktreesByRepo: next, tabsByWorktree: nextTabs, @@ -103,7 +113,12 @@ export const createWorktreeSlice: StateCreator terminalLayoutsByTabId: nextLayouts, deleteStateByWorktreeId: nextDeleteState, activeWorktreeId: s.activeWorktreeId === worktreeId ? null : s.activeWorktreeId, - activeTabId: s.activeTabId && tabIds.has(s.activeTabId) ? null : s.activeTabId + activeTabId: s.activeTabId && tabIds.has(s.activeTabId) ? null : s.activeTabId, + openFiles: newOpenFiles, + activeFileIdByWorktree: nextActiveFileIdByWorktree, + activeTabTypeByWorktree: nextActiveTabTypeByWorktree, + activeFileId: activeFileCleared ? null : s.activeFileId, + activeTabType: activeFileCleared ? 'terminal' : s.activeTabType } }) return { ok: true as const } @@ -189,8 +204,31 @@ export const createWorktreeSlice: StateCreator const worktree = findWorktreeById(s.worktreesByRepo, worktreeId) shouldClearUnread = Boolean(worktree?.isUnread) + + // Restore per-worktree editor state + const restoredFileId = s.activeFileIdByWorktree[worktreeId] ?? null + const restoredTabType = s.activeTabTypeByWorktree[worktreeId] ?? 'terminal' + // Verify the restored file still exists in openFiles + const fileStillOpen = restoredFileId + ? s.openFiles.some((f) => f.id === restoredFileId) + : false + + // If restored file is gone, fall back to another open file for this worktree + let activeFileId: string | null + let activeTabType: 'terminal' | 'editor' + if (fileStillOpen) { + activeFileId = restoredFileId + activeTabType = restoredTabType + } else { + const fallbackFile = s.openFiles.find((f) => f.worktreeId === worktreeId) + activeFileId = fallbackFile?.id ?? null + activeTabType = fallbackFile ? 'editor' : 'terminal' + } + return { activeWorktreeId: worktreeId, + activeFileId, + activeTabType, worktreesByRepo: shouldClearUnread ? applyWorktreeUpdates(s.worktreesByRepo, worktreeId, { isUnread: false }) : s.worktreesByRepo