From 415ae35dce85a55c190becb6a700c9645645f271 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:00:10 -0700 Subject: [PATCH] Fix shutdown clearing active worktree/tab state from wrong layer (#44) Move activeWorktreeId/activeTabId clearing out of shutdownWorktreeTerminals (a terminal-focused store action) and into the UI component that triggers shutdown. The delete flow already handles this independently in worktrees.ts. Also fix pre-existing lint issues and extract terminal helpers to separate file. Co-authored-by: Claude Opus 4.6 --- .../sidebar/WorktreeContextMenu.tsx | 13 ++-- .../src/store/slices/terminal-helpers.ts | 23 ++++++++ src/renderer/src/store/slices/terminals.ts | 59 +++++++------------ 3 files changed, 53 insertions(+), 42 deletions(-) create mode 100644 src/renderer/src/store/slices/terminal-helpers.ts 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 -}