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

@@ -5,7 +5,7 @@ import { TerminalTabBar } from "./terminal-tab-bar"
import { TerminalView } from "./terminal-view"
export function TerminalPanel() {
const { isOpen, tabs, activeTabId } = useTerminalContext()
const { isOpen, tabs, activeTabId, markTerminalExited } = useTerminalContext()
return (
<section
@@ -22,6 +22,7 @@ export function TerminalPanel() {
initialCommand={tab.initialCommand}
isActive={tab.id === activeTabId}
isVisible={isOpen}
onProcessExited={markTerminalExited}
/>
))}
</div>

View File

@@ -95,6 +95,7 @@ interface TerminalViewProps {
initialCommand?: string
isActive: boolean
isVisible: boolean
onProcessExited?: (terminalId: string) => void
}
export function TerminalView({
@@ -103,6 +104,7 @@ export function TerminalView({
initialCommand,
isActive,
isVisible,
onProcessExited,
}: TerminalViewProps) {
const containerRef = useRef<HTMLDivElement>(null)
const fitAddonRef = useRef<{ fit: () => void } | null>(null)
@@ -110,6 +112,7 @@ export function TerminalView({
const lastResizeRef = useRef<{ cols: number; rows: number } | null>(null)
const isActiveRef = useRef(isActive)
const isVisibleRef = useRef(isVisible)
const onProcessExitedRef = useRef(onProcessExited)
const [loading, setLoading] = useState(true)
useEffect(() => {
@@ -117,6 +120,10 @@ export function TerminalView({
isVisibleRef.current = isVisible
}, [isActive, isVisible])
useEffect(() => {
onProcessExitedRef.current = onProcessExited
}, [onProcessExited])
useEffect(() => {
let cancelled = false
let cleanup: (() => void) | undefined
@@ -188,6 +195,7 @@ export function TerminalView({
const unlistenExit = await subscribe<TerminalEvent>(
`terminal://exit/${terminalId}`,
() => {
onProcessExitedRef.current?.(terminalId)
term.write("\r\n\x1b[90m[Process exited]\x1b[0m\r\n")
}
)
@@ -206,6 +214,7 @@ export function TerminalView({
try {
await terminalSpawn(workingDir, initialCommand, terminalId)
} catch (err) {
onProcessExitedRef.current?.(terminalId)
term.write(`\r\n\x1b[31m[Failed to start terminal: ${err}]\x1b[0m\r\n`)
} finally {
if (!cancelled) setLoading(false)