|
10 | 10 |
|
11 | 11 | import { existsSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, readFileSync } from 'fs'; |
12 | 12 | import { join } from 'path'; |
13 | | -import { execSync } from 'child_process'; |
| 13 | +import { execFileSync } from 'child_process'; |
14 | 14 | import { TAB_COLORS, PHASE_TAB_CONFIG, ACTIVE_TAB_BG, ACTIVE_TAB_FG, INACTIVE_TAB_FG, type TabState, type AlgorithmTabPhase } from './tab-constants'; |
15 | 15 | import { paiPath } from './paths'; |
16 | 16 |
|
@@ -121,12 +121,19 @@ function cleanupStaleStateFiles(): void { |
121 | 121 | const defaultSocket = `/tmp/kitty-${process.env.USER}`; |
122 | 122 | const socketPath = process.env.KITTY_LISTEN_ON || (existsSync(defaultSocket) ? `unix:${defaultSocket}` : null); |
123 | 123 | if (!socketPath) return; // No socket — skip cleanup to avoid escape sequence IPC |
124 | | - const liveOutput = execSync(`kitten @ --to="${socketPath}" ls 2>/dev/null | jq -r ".[].tabs[].windows[].id" 2>/dev/null`, { |
| 124 | + // Validate socket path before passing to execFileSync (prevent injection via env var) |
| 125 | + if (!/^[a-zA-Z0-9_\-]+:[a-zA-Z0-9/_\-.]+$/.test(socketPath)) return; |
| 126 | + const rawLs = execFileSync('kitten', ['@', `--to=${socketPath}`, 'ls'], { |
125 | 127 | encoding: 'utf-8', timeout: 2000, |
126 | 128 | }).trim(); |
127 | | - if (!liveOutput) return; |
| 129 | + if (!rawLs) return; |
128 | 130 |
|
129 | | - const liveIds = new Set(liveOutput.split('\n').map(id => id.trim())); |
| 131 | + const liveIds = new Set<string>(); |
| 132 | + try { |
| 133 | + const osWindows = JSON.parse(rawLs) as Array<{ tabs: Array<{ windows: Array<{ id: number }> }> }>; |
| 134 | + for (const os of osWindows) for (const tab of os.tabs) for (const win of tab.windows) liveIds.add(String(win.id)); |
| 135 | + } catch { return; } |
| 136 | + if (liveIds.size === 0) return; |
130 | 137 |
|
131 | 138 | for (const file of files) { |
132 | 139 | const winId = file.replace('.json', ''); |
@@ -156,27 +163,20 @@ export function setTabState(opts: SetTabOptions): void { |
156 | 163 | return; |
157 | 164 | } |
158 | 165 |
|
159 | | - const escaped = title.replace(/"/g, '\\"'); |
160 | 166 | // Set BOTH tab title AND window title. Kitty's tab_title_template uses |
161 | 167 | // {active_window.title} (the window title). OSC escape codes from Claude Code |
162 | 168 | // reset set-tab-title overrides, so the template falls back to window title. |
163 | 169 | // By setting both, our title survives OSC resets. |
164 | | - const toFlag = `--to="${kittyEnv.listenOn}"`; |
165 | | - console.error(`[tab-setter] Setting tab: "${escaped}" with toFlag: ${toFlag}`); |
166 | | - execSync(`kitten @ ${toFlag} set-tab-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 }); |
167 | | - execSync(`kitten @ ${toFlag} set-window-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 }); |
| 170 | + const toArg = `--to=${kittyEnv.listenOn}`; |
| 171 | + console.error(`[tab-setter] Setting tab: "${title}" via ${toArg}`); |
| 172 | + execFileSync('kitten', ['@', toArg, 'set-tab-title', title], { stdio: 'ignore', timeout: 2000 }); |
| 173 | + execFileSync('kitten', ['@', toArg, 'set-window-title', title], { stdio: 'ignore', timeout: 2000 }); |
168 | 174 |
|
169 | 175 | // For idle state, reset ALL colors to Kitty defaults (no lingering backgrounds) |
170 | 176 | if (state === 'idle') { |
171 | | - execSync( |
172 | | - `kitten @ ${toFlag} set-tab-color --self active_bg=none active_fg=none inactive_bg=none inactive_fg=none`, |
173 | | - { stdio: 'ignore', timeout: 2000 } |
174 | | - ); |
| 177 | + execFileSync('kitten', ['@', toArg, 'set-tab-color', '--self', 'active_bg=none', 'active_fg=none', 'inactive_bg=none', 'inactive_fg=none'], { stdio: 'ignore', timeout: 2000 }); |
175 | 178 | } else { |
176 | | - execSync( |
177 | | - `kitten @ ${toFlag} set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TAB_FG} inactive_bg=${colors.inactiveBg} inactive_fg=${INACTIVE_TAB_FG}`, |
178 | | - { stdio: 'ignore', timeout: 2000 } |
179 | | - ); |
| 179 | + execFileSync('kitten', ['@', toArg, 'set-tab-color', '--self', `active_bg=${ACTIVE_TAB_BG}`, `active_fg=${ACTIVE_TAB_FG}`, `inactive_bg=${colors.inactiveBg}`, `inactive_fg=${INACTIVE_TAB_FG}`], { stdio: 'ignore', timeout: 2000 }); |
180 | 180 | } |
181 | 181 | console.error(`[tab-setter] Tab commands completed successfully`); |
182 | 182 | } catch (err) { |
@@ -323,24 +323,17 @@ export function setPhaseTab(phase: AlgorithmTabPhase, sessionId: string, summary |
323 | 323 | return; |
324 | 324 | } |
325 | 325 |
|
326 | | - const escaped = title.replace(/"/g, '\\"'); |
327 | | - const toFlag = `--to="${kittyEnv.listenOn}"`; |
| 326 | + const toArg = `--to=${kittyEnv.listenOn}`; |
328 | 327 |
|
329 | | - execSync(`kitten @ ${toFlag} set-tab-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 }); |
330 | | - execSync(`kitten @ ${toFlag} set-window-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 }); |
| 328 | + execFileSync('kitten', ['@', toArg, 'set-tab-title', title], { stdio: 'ignore', timeout: 2000 }); |
| 329 | + execFileSync('kitten', ['@', toArg, 'set-window-title', title], { stdio: 'ignore', timeout: 2000 }); |
331 | 330 |
|
332 | 331 | if (phase === 'IDLE') { |
333 | | - execSync( |
334 | | - `kitten @ ${toFlag} set-tab-color --self active_bg=none active_fg=none inactive_bg=none inactive_fg=none`, |
335 | | - { stdio: 'ignore', timeout: 2000 } |
336 | | - ); |
| 332 | + execFileSync('kitten', ['@', toArg, 'set-tab-color', '--self', 'active_bg=none', 'active_fg=none', 'inactive_bg=none', 'inactive_fg=none'], { stdio: 'ignore', timeout: 2000 }); |
337 | 333 | } else { |
338 | | - execSync( |
339 | | - `kitten @ ${toFlag} set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TAB_FG} inactive_bg=${config.inactiveBg} inactive_fg=${INACTIVE_TAB_FG}`, |
340 | | - { stdio: 'ignore', timeout: 2000 } |
341 | | - ); |
| 334 | + execFileSync('kitten', ['@', toArg, 'set-tab-color', '--self', `active_bg=${ACTIVE_TAB_BG}`, `active_fg=${ACTIVE_TAB_FG}`, `inactive_bg=${config.inactiveBg}`, `inactive_fg=${INACTIVE_TAB_FG}`], { stdio: 'ignore', timeout: 2000 }); |
342 | 335 | } |
343 | | - console.error(`[tab-setter] Phase tab: "${escaped}" (${phase}, bg=${config.inactiveBg})`); |
| 336 | + console.error(`[tab-setter] Phase tab: "${title}" (${phase}, bg=${config.inactiveBg})`); |
344 | 337 | } catch (err) { |
345 | 338 | console.error(`[tab-setter] Error setting phase tab:`, err); |
346 | 339 | } |
|
0 commit comments