Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 23 additions & 30 deletions Releases/v4.0.3/.claude/hooks/lib/tab-setter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import { existsSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, readFileSync } from 'fs';
import { join } from 'path';
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import { TAB_COLORS, PHASE_TAB_CONFIG, ACTIVE_TAB_BG, ACTIVE_TAB_FG, INACTIVE_TAB_FG, type TabState, type AlgorithmTabPhase } from './tab-constants';
import { paiPath } from './paths';

Expand Down Expand Up @@ -121,12 +121,19 @@ function cleanupStaleStateFiles(): void {
const defaultSocket = `/tmp/kitty-${process.env.USER}`;
const socketPath = process.env.KITTY_LISTEN_ON || (existsSync(defaultSocket) ? `unix:${defaultSocket}` : null);
if (!socketPath) return; // No socket — skip cleanup to avoid escape sequence IPC
const liveOutput = execSync(`kitten @ --to="${socketPath}" ls 2>/dev/null | jq -r ".[].tabs[].windows[].id" 2>/dev/null`, {
// Validate socket path before passing to execFileSync (prevent injection via env var)
if (!/^[a-zA-Z0-9_\-]+:[a-zA-Z0-9/_\-.]+$/.test(socketPath)) return;
const rawLs = execFileSync('kitten', ['@', `--to=${socketPath}`, 'ls'], {
encoding: 'utf-8', timeout: 2000,
}).trim();
if (!liveOutput) return;
if (!rawLs) return;

const liveIds = new Set(liveOutput.split('\n').map(id => id.trim()));
const liveIds = new Set<string>();
try {
const osWindows = JSON.parse(rawLs) as Array<{ tabs: Array<{ windows: Array<{ id: number }> }> }>;
for (const os of osWindows) for (const tab of os.tabs) for (const win of tab.windows) liveIds.add(String(win.id));
} catch { return; }
if (liveIds.size === 0) return;

for (const file of files) {
const winId = file.replace('.json', '');
Expand Down Expand Up @@ -156,27 +163,20 @@ export function setTabState(opts: SetTabOptions): void {
return;
}

const escaped = title.replace(/"/g, '\\"');
// Set BOTH tab title AND window title. Kitty's tab_title_template uses
// {active_window.title} (the window title). OSC escape codes from Claude Code
// reset set-tab-title overrides, so the template falls back to window title.
// By setting both, our title survives OSC resets.
const toFlag = `--to="${kittyEnv.listenOn}"`;
console.error(`[tab-setter] Setting tab: "${escaped}" with toFlag: ${toFlag}`);
execSync(`kitten @ ${toFlag} set-tab-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 });
execSync(`kitten @ ${toFlag} set-window-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 });
const toArg = `--to=${kittyEnv.listenOn}`;
console.error(`[tab-setter] Setting tab: "${title}" via ${toArg}`);
execFileSync('kitten', ['@', toArg, 'set-tab-title', title], { stdio: 'ignore', timeout: 2000 });
execFileSync('kitten', ['@', toArg, 'set-window-title', title], { stdio: 'ignore', timeout: 2000 });

// For idle state, reset ALL colors to Kitty defaults (no lingering backgrounds)
if (state === 'idle') {
execSync(
`kitten @ ${toFlag} set-tab-color --self active_bg=none active_fg=none inactive_bg=none inactive_fg=none`,
{ stdio: 'ignore', timeout: 2000 }
);
execFileSync('kitten', ['@', toArg, 'set-tab-color', '--self', 'active_bg=none', 'active_fg=none', 'inactive_bg=none', 'inactive_fg=none'], { stdio: 'ignore', timeout: 2000 });
} else {
execSync(
`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}`,
{ stdio: 'ignore', timeout: 2000 }
);
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 });
}
console.error(`[tab-setter] Tab commands completed successfully`);
} catch (err) {
Expand Down Expand Up @@ -323,24 +323,17 @@ export function setPhaseTab(phase: AlgorithmTabPhase, sessionId: string, summary
return;
}

const escaped = title.replace(/"/g, '\\"');
const toFlag = `--to="${kittyEnv.listenOn}"`;
const toArg = `--to=${kittyEnv.listenOn}`;

execSync(`kitten @ ${toFlag} set-tab-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 });
execSync(`kitten @ ${toFlag} set-window-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 });
execFileSync('kitten', ['@', toArg, 'set-tab-title', title], { stdio: 'ignore', timeout: 2000 });
execFileSync('kitten', ['@', toArg, 'set-window-title', title], { stdio: 'ignore', timeout: 2000 });

if (phase === 'IDLE') {
execSync(
`kitten @ ${toFlag} set-tab-color --self active_bg=none active_fg=none inactive_bg=none inactive_fg=none`,
{ stdio: 'ignore', timeout: 2000 }
);
execFileSync('kitten', ['@', toArg, 'set-tab-color', '--self', 'active_bg=none', 'active_fg=none', 'inactive_bg=none', 'inactive_fg=none'], { stdio: 'ignore', timeout: 2000 });
} else {
execSync(
`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}`,
{ stdio: 'ignore', timeout: 2000 }
);
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 });
}
console.error(`[tab-setter] Phase tab: "${escaped}" (${phase}, bg=${config.inactiveBg})`);
console.error(`[tab-setter] Phase tab: "${title}" (${phase}, bg=${config.inactiveBg})`);
} catch (err) {
console.error(`[tab-setter] Error setting phase tab:`, err);
}
Expand Down