diff --git a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx index 07c945574ec..165705e5066 100644 --- a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx +++ b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx @@ -10,7 +10,7 @@ import { FolderOpen, Copy, Bell, BellOff, Link, MessageSquare, XCircle, Trash2 } import { useAppStore } from '@/store' import type { Worktree } from '../../../../shared/types' -interface Props { +type Props = { worktree: Worktree children: React.ReactNode } @@ -21,6 +21,8 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ worktree, const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta) const openModal = useAppStore((s) => s.openModal) const shutdownWorktreeTerminals = useAppStore((s) => s.shutdownWorktreeTerminals) + const activeWorktreeId = useAppStore((s) => s.activeWorktreeId) + const setActiveWorktree = useAppStore((s) => s.setActiveWorktree) const clearWorktreeDeleteState = useAppStore((s) => s.clearWorktreeDeleteState) const deleteState = useAppStore((s) => s.deleteStateByWorktreeId[worktree.id]) const [menuOpen, setMenuOpen] = useState(false) @@ -53,9 +55,12 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ worktree, openModal('edit-comment', { worktreeId: worktree.id, currentComment: worktree.comment }) }, [worktree.id, worktree.comment, openModal]) - const handleCloseTerminals = useCallback(() => { - shutdownWorktreeTerminals(worktree.id) - }, [worktree.id, shutdownWorktreeTerminals]) + const handleCloseTerminals = useCallback(async () => { + await shutdownWorktreeTerminals(worktree.id) + if (activeWorktreeId === worktree.id) { + setActiveWorktree(null) + } + }, [worktree.id, shutdownWorktreeTerminals, activeWorktreeId, setActiveWorktree]) const handleDelete = useCallback(() => { clearWorktreeDeleteState(worktree.id) diff --git a/src/renderer/src/store/slices/terminal-helpers.ts b/src/renderer/src/store/slices/terminal-helpers.ts new file mode 100644 index 00000000000..477bf6dd831 --- /dev/null +++ b/src/renderer/src/store/slices/terminal-helpers.ts @@ -0,0 +1,23 @@ +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import { detectAgentStatusFromTitle } from '@/lib/agent-status' + +export function emptyLayoutSnapshot(): TerminalLayoutSnapshot { + return { + root: null, + activeLeafId: null, + expandedLeafId: null + } +} + +export function clearTransientTerminalState(tab: TerminalTab, index: number): TerminalTab { + return { + ...tab, + ptyId: null, + title: getResetTitle(tab, index) + } +} + +function getResetTitle(tab: TerminalTab, index: number): string { + const fallbackTitle = tab.customTitle?.trim() || `Terminal ${index + 1}` + return detectAgentStatusFromTitle(tab.title) ? fallbackTitle : tab.title +} diff --git a/src/renderer/src/store/slices/terminals.ts b/src/renderer/src/store/slices/terminals.ts index de533ea0a80..5f7b845d215 100644 --- a/src/renderer/src/store/slices/terminals.ts +++ b/src/renderer/src/store/slices/terminals.ts @@ -5,9 +5,9 @@ import type { TerminalTab, WorkspaceSessionState } from '../../../../shared/types' -import { detectAgentStatusFromTitle } from '@/lib/agent-status' +import { clearTransientTerminalState, emptyLayoutSnapshot } from './terminal-helpers' -export interface TerminalSlice { +export type TerminalSlice = { tabsByWorktree: Record activeTabId: string | null ptyIdsByTabId: Record @@ -170,26 +170,25 @@ export const createTerminalSlice: StateCreator const next = { ...s.tabsByWorktree } for (const wId of Object.keys(next)) { next[wId] = next[wId].map((t) => { - if (t.id !== tabId) return t + if (t.id !== tabId) { + return t + } const remainingPtyIds = ptyId ? (s.ptyIdsByTabId[tabId] ?? []).filter((id) => id !== ptyId) : [] - return { ...t, ptyId: remainingPtyIds[remainingPtyIds.length - 1] ?? null } + return { ...t, ptyId: remainingPtyIds.at(-1) ?? null } }) } const nextPtyIdsByTabId = { ...s.ptyIdsByTabId } - if (ptyId) { - nextPtyIdsByTabId[tabId] = (nextPtyIdsByTabId[tabId] ?? []).filter((id) => id !== ptyId) - } else { - nextPtyIdsByTabId[tabId] = [] - } + nextPtyIdsByTabId[tabId] = ptyId + ? (nextPtyIdsByTabId[tabId] ?? []).filter((id) => id !== ptyId) + : [] return { tabsByWorktree: next, ptyIdsByTabId: nextPtyIdsByTabId } }) }, shutdownWorktreeTerminals: async (worktreeId) => { const tabs = get().tabsByWorktree[worktreeId] ?? [] - const tabIds = new Set(tabs.map((tab) => tab.id)) const ptyIds = tabs.flatMap((tab) => get().ptyIdsByTabId[tab.id] ?? []) set((s) => { @@ -211,13 +210,13 @@ export const createTerminalSlice: StateCreator return { tabsByWorktree: nextTabsByWorktree, ptyIdsByTabId: nextPtyIdsByTabId, - suppressedPtyExitIds: nextSuppressedPtyExitIds, - activeWorktreeId: s.activeWorktreeId === worktreeId ? null : s.activeWorktreeId, - activeTabId: s.activeTabId && tabIds.has(s.activeTabId) ? null : s.activeTabId + suppressedPtyExitIds: nextSuppressedPtyExitIds } }) - if (ptyIds.length === 0) return + if (ptyIds.length === 0) { + return + } await Promise.allSettled(ptyIds.map((ptyId) => window.api.pty.kill(ptyId))) }, @@ -225,7 +224,9 @@ export const createTerminalSlice: StateCreator consumeSuppressedPtyExit: (ptyId) => { let wasSuppressed = false set((s) => { - if (!s.suppressedPtyExitIds[ptyId]) return {} + if (!s.suppressedPtyExitIds[ptyId]) { + return {} + } wasSuppressed = true const next = { ...s.suppressedPtyExitIds } delete next[ptyId] @@ -249,8 +250,11 @@ export const createTerminalSlice: StateCreator setTabLayout: (tabId, layout) => { set((s) => { const next = { ...s.terminalLayoutsByTabId } - if (layout) next[tabId] = layout - else delete next[tabId] + if (layout) { + next[tabId] = layout + } else { + delete next[tabId] + } return { terminalLayoutsByTabId: next } }) }, @@ -311,24 +315,3 @@ export const createTerminalSlice: StateCreator }) } }) - -function emptyLayoutSnapshot(): TerminalLayoutSnapshot { - return { - root: null, - activeLeafId: null, - expandedLeafId: null - } -} - -function clearTransientTerminalState(tab: TerminalTab, index: number): TerminalTab { - return { - ...tab, - ptyId: null, - title: getResetTitle(tab, index) - } -} - -function getResetTitle(tab: TerminalTab, index: number): string { - const fallbackTitle = tab.customTitle?.trim() || `Terminal ${index + 1}` - return detectAgentStatusFromTitle(tab.title) ? fallbackTitle : tab.title -}