fix: sync run button state with terminal process via centralized exit tracking

Centralize terminal process lifecycle in terminal-context as single
source of truth. TerminalView reports exit/failure via callback,
context maintains exitedTerminals set, command-dropdown reacts to it.

Removes redundant polling and per-component exit event subscriptions
that raced with deferred spawn introduced in b2d10fa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-02 19:02:01 +08:00
parent ef94687c87
commit e7bd12e16f
4 changed files with 94 additions and 116 deletions

View File

@@ -35,6 +35,8 @@ interface TerminalContextValue {
setHeight: (h: number) => void
tabs: TerminalTab[]
activeTabId: string | null
exitedTerminals: Set<string>
markTerminalExited: (id: string) => void
createTerminal: () => Promise<void>
createTerminalInDirectory: (
workingDir: string,
@@ -69,6 +71,7 @@ export function TerminalProvider({ children }: { children: ReactNode }) {
const [tabs, setTabs] = useState<TerminalTab[]>([])
const [activeTabId, setActiveTabId] = useState<string | null>(null)
const tabCounterRef = useRef(0)
const [exitedTerminals, setExitedTerminals] = useState<Set<string>>(new Set())
const lastMouseActivityInTerminalRef = useRef(false)
// Keep a ref of tabs for cleanup on unmount (effect [] captures stale state)
const tabsRef = useRef(tabs)
@@ -78,6 +81,27 @@ export function TerminalProvider({ children }: { children: ReactNode }) {
const folderPath = folder?.path ?? ""
const markTerminalExited = useCallback((id: string) => {
setExitedTerminals((prev) => {
if (prev.has(id)) return prev
const next = new Set(prev)
next.add(id)
return next
})
}, [])
const removeExitedTerminals = useCallback((ids: string[]) => {
setExitedTerminals((prev) => {
if (prev.size === 0) return prev
let changed = false
const next = new Set(prev)
for (const id of ids) {
if (next.delete(id)) changed = true
}
return changed ? next : prev
})
}, [])
const killTerminalTabs = useCallback((targetTabs: TerminalTab[]) => {
targetTabs.forEach((tab) => {
terminalKill(tab.id).catch(() => {})
@@ -158,43 +182,51 @@ export function TerminalProvider({ children }: { children: ReactNode }) {
setHeightState(Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, h)))
}, [])
const closeTerminal = useCallback((id: string) => {
terminalKill(id).catch(() => {})
setTabs((prev) => {
const next = prev.filter((t) => t.id !== id)
if (next.length === 0) {
tabCounterRef.current = 0
setIsOpen(false)
setActiveTabId(null)
} else {
setActiveTabId((prevActive) =>
prevActive === id ? next[next.length - 1].id : prevActive
)
}
return next
})
}, [])
const closeTerminal = useCallback(
(id: string) => {
markTerminalExited(id)
removeExitedTerminals([id])
terminalKill(id).catch(() => {})
setTabs((prev) => {
const next = prev.filter((t) => t.id !== id)
if (next.length === 0) {
tabCounterRef.current = 0
setIsOpen(false)
setActiveTabId(null)
} else {
setActiveTabId((prevActive) =>
prevActive === id ? next[next.length - 1].id : prevActive
)
}
return next
})
},
[markTerminalExited, removeExitedTerminals]
)
const closeOtherTerminals = useCallback(
(id: string) => {
setTabs((prev) => {
killTerminalTabs(prev.filter((t) => t.id !== id))
const closed = prev.filter((t) => t.id !== id)
killTerminalTabs(closed)
removeExitedTerminals(closed.map((t) => t.id))
return prev.filter((t) => t.id === id)
})
setActiveTabId(id)
},
[killTerminalTabs]
[killTerminalTabs, removeExitedTerminals]
)
const closeAllTerminals = useCallback(() => {
setTabs((prev) => {
killTerminalTabs(prev)
removeExitedTerminals(prev.map((t) => t.id))
return []
})
tabCounterRef.current = 0
setActiveTabId(null)
setIsOpen(false)
}, [killTerminalTabs])
}, [killTerminalTabs, removeExitedTerminals])
const renameTerminal = useCallback((id: string, title: string) => {
setTabs((prev) => prev.map((t) => (t.id === id ? { ...t, title } : t)))
@@ -304,6 +336,8 @@ export function TerminalProvider({ children }: { children: ReactNode }) {
setHeight,
tabs,
activeTabId,
exitedTerminals,
markTerminalExited,
createTerminal,
createTerminalInDirectory,
createTerminalWithCommand,
@@ -320,6 +354,8 @@ export function TerminalProvider({ children }: { children: ReactNode }) {
setHeight,
tabs,
activeTabId,
exitedTerminals,
markTerminalExited,
createTerminal,
createTerminalInDirectory,
createTerminalWithCommand,