Skip to content

Commit daf84ba

Browse files
committed
fix: guard cliAvailability setState against unmount
Both SpawnAgentDialog and TerminalPane lacked a cancelled flag in the CLI availability useEffect, allowing setCliAvailability to fire on an unmounted component if the user closed the pane before the IPC round trip completed. Pattern matches the existing loadPersonas/loadBurnSummaries guards in the same files. https://claude.ai/code/session_01KXU1uAUwx3L82TMLnAmU4z
1 parent ed6285c commit daf84ba

3 files changed

Lines changed: 31 additions & 2 deletions

File tree

check_types.mjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Check if Object.fromEntries result is assignable to Partial<Record<SpawnAgentCli, boolean>>
2+
3+
// According to TypeScript:
4+
// Partial<Record<SpawnAgentCli, boolean>> expands to:
5+
// { claude?: boolean; codex?: boolean; opencode?: boolean }
6+
7+
// Object.fromEntries returns: { [k: string]: any }
8+
// In the context of:
9+
// const results: Array<[SpawnAgentCli, boolean]> = [['claude', true], ...]
10+
// Object.fromEntries(results) is typed as: { [k: string]: boolean }
11+
12+
// The question: is { [k: string]: boolean } assignable to
13+
// { claude?: boolean; codex?: boolean; opencode?: boolean }?
14+
15+
// Answer: YES, because:
16+
// 1. { [k: string]: boolean } is an index signature that matches ANY string key
17+
// 2. { claude?: boolean; codex?: boolean; opencode?: boolean } only uses specific keys
18+
// 3. Any value with an index signature { [k: string]: T } satisfies a type with specific optional properties
19+
20+
console.log("Type check analysis:");
21+
console.log("Object.fromEntries(results) type: { [k: string]: boolean }");
22+
console.log("setCliAvailability expects: Partial<Record<SpawnAgentCli, boolean>>");
23+
console.log("Which expands to: { claude?: boolean; codex?: boolean; opencode?: boolean }");
24+
console.log("");
25+
console.log("TypeScript assignability: YES - index signature types are assignable to property-specific types");

src/renderer/src/components/sidebar/SpawnAgentDialog.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export function SpawnAgentDialog(): React.ReactNode {
4848
}, [project, root?.id, selectedRootId])
4949

5050
useEffect(() => {
51+
let cancelled = false
5152
const clis: SpawnAgentCli[] = ['claude', 'codex', 'opencode']
5253
void Promise.all(clis.map(async (cli) => {
5354
const available = await pear.broker.checkCliAvailable(cli).catch(() => false)
5455
return [cli, available] as const
5556
})).then((results) => {
56-
setCliAvailability(Object.fromEntries(results))
57+
if (!cancelled) setCliAvailability(Object.fromEntries(results))
5758
})
59+
return () => { cancelled = true }
5860
}, [])
5961

6062
useEffect(() => {

src/renderer/src/components/terminal/TerminalPane.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,13 +569,15 @@ export function TerminalPane(): React.ReactNode {
569569
}
570570

571571
useEffect(() => {
572+
let cancelled = false
572573
const clis: SpawnAgentCli[] = ['claude', 'codex', 'opencode']
573574
void Promise.all(clis.map(async (cli) => {
574575
const available = await pear.broker.checkCliAvailable(cli).catch(() => false)
575576
return [cli, available] as const
576577
})).then((results) => {
577-
setCliAvailability(Object.fromEntries(results))
578+
if (!cancelled) setCliAvailability(Object.fromEntries(results))
578579
})
580+
return () => { cancelled = true }
579581
}, [])
580582

581583
useEffect(() => {

0 commit comments

Comments
 (0)