Skip to content

Commit 2b36854

Browse files
committed
fix(process): improve process cleanup by killing entire process group on Unix
Windows or no PID cases fall back to child.kill(). Also removes token display from sub-agent summary UI as it's no longer needed.
1 parent f002617 commit 2b36854

2 files changed

Lines changed: 33 additions & 26 deletions

File tree

‎src/cli/tui/routes/workflow/components/timeline/sub-agent-summary.tsx‎

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import { Show } from "solid-js"
1010
import { useTheme } from "@tui/shared/context/theme"
1111
import type { SubAgentState } from "../../state/types"
12-
import { formatTokens } from "../../state/formatters"
1312

1413
export interface SubAgentSummaryProps {
1514
subAgents: SubAgentState[]
@@ -27,12 +26,12 @@ export function SubAgentSummary(props: SubAgentSummaryProps) {
2726
const arrow = () => (props.isExpanded ? "▼" : "▶")
2827
const showArrow = () => props.isSelected
2928

30-
// Count by status
29+
// Count by status (sub-agents can only be running, completed, or failed)
3130
const statusStr = () => {
3231
const counts = {
3332
completed: 0,
3433
running: 0,
35-
pending: 0,
34+
failed: 0,
3635
}
3736

3837
for (const agent of props.subAgents) {
@@ -42,26 +41,13 @@ export function SubAgentSummary(props: SubAgentSummaryProps) {
4241
}
4342

4443
const parts: string[] = []
45-
if (counts.completed > 0) parts.push(`${counts.completed} completed`)
44+
if (counts.completed > 0) parts.push(`${counts.completed} done`)
4645
if (counts.running > 0) parts.push(`${counts.running} running`)
47-
if (counts.pending > 0) parts.push(`${counts.pending} pending`)
46+
if (counts.failed > 0) parts.push(`${counts.failed} failed`)
4847

4948
return parts.join(", ")
5049
}
5150

52-
// Aggregate tokens
53-
const tokenStr = () => {
54-
let totalIn = 0
55-
let totalOut = 0
56-
57-
for (const agent of props.subAgents) {
58-
totalIn += agent.telemetry.tokensIn
59-
totalOut += agent.telemetry.tokensOut
60-
}
61-
62-
return totalIn > 0 || totalOut > 0 ? formatTokens(totalIn, totalOut) : ""
63-
}
64-
6551
// Selection indicator
6652
const selectionPrefix = () => (showArrow() ? "> " : " ")
6753

@@ -71,7 +57,6 @@ export function SubAgentSummary(props: SubAgentSummaryProps) {
7157
<text fg={themeCtx.theme.text}>{selectionPrefix()}</text>
7258
<text fg={themeCtx.theme.textMuted}>
7359
{arrow()} Sub-agents: {statusStr()}
74-
<Show when={tokenStr()}> • {tokenStr()}</Show>
7560
</text>
7661
</box>
7762
</Show>

‎src/infra/process/spawn.ts‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,39 @@ function resolveCommandExecutable(command: string): string {
5959
export function killAllActiveProcesses(): void {
6060
for (const child of activeProcesses) {
6161
try {
62-
child.kill('SIGTERM');
63-
// Force kill after 1 second if still running
64-
setTimeout(() => {
62+
// Kill process group on Unix (for wrapper scripts like CCR/Claude that spawn children)
63+
// Using negative PID kills the entire process group, not just the direct child
64+
if (process.platform !== 'win32' && child.pid) {
6565
try {
66+
process.kill(-child.pid, 'SIGTERM');
67+
// Force kill after 1 second if still running
68+
setTimeout(() => {
69+
try {
70+
process.kill(-child.pid!, 'SIGKILL');
71+
} catch {
72+
// Process group already dead
73+
}
74+
}, 1000);
75+
} catch {
76+
// Fallback to killing just the child if process group kill fails
6677
if (!child.killed) {
67-
child.kill('SIGKILL');
78+
child.kill('SIGTERM');
6879
}
69-
} catch {
70-
// Process already dead
7180
}
72-
}, 1000);
81+
} else {
82+
// Windows or no PID: use child.kill()
83+
child.kill('SIGTERM');
84+
// Force kill after 1 second if still running
85+
setTimeout(() => {
86+
try {
87+
if (!child.killed) {
88+
child.kill('SIGKILL');
89+
}
90+
} catch {
91+
// Process already dead
92+
}
93+
}, 1000);
94+
}
7395
} catch {
7496
// Ignore errors during cleanup
7597
}

0 commit comments

Comments
 (0)