From 00729ec1fcf19dff5168d9ce90c3828a19c7002e Mon Sep 17 00:00:00 2001 From: mutdogus Date: Mon, 26 Jan 2026 19:18:27 +0900 Subject: [PATCH] fix(hooks): use socket-based Kitty remote control to prevent escape sequence leaks When PAI hooks use `kitty @` commands without specifying a socket, Kitty communicates via escape sequences. In subprocess contexts (how hooks run), these escape sequences cannot be properly consumed, causing them to leak into the terminal output as visible "P@kitty-cmd" garbage text. This affects: - UpdateTabTitle.hook.ts - SetQuestionTab.hook.ts - QuestionAnswered.hook.ts - handlers/tab-state.ts The fix adds a `getKittySocket()` helper that checks for: 1. KITTY_LISTEN_ON environment variable 2. Default socket at /tmp/kitty-$USER All kitty/kitten commands now use `--to ` flag for socket-based communication, which doesn't suffer from escape sequence leaks. Users need to configure Kitty with: ``` allow_remote_control socket-only listen_on unix:/tmp/kitty-$USER ``` Fixes terminal artifacts like: P@kitty-cmd{"ok": false, "error": "Remote control is disabled"} Co-Authored-By: Claude Opus 4.5 --- .../src/hooks/QuestionAnswered.hook.ts | 30 +++++++++++- .../src/hooks/SetQuestionTab.hook.ts | 30 +++++++++++- .../src/hooks/UpdateTabTitle.hook.ts | 47 +++++++++++++------ .../src/hooks/handlers/tab-state.ts | 31 +++++++++++- 4 files changed, 118 insertions(+), 20 deletions(-) diff --git a/Packs/pai-hook-system/src/hooks/QuestionAnswered.hook.ts b/Packs/pai-hook-system/src/hooks/QuestionAnswered.hook.ts index ffb6bdb80..6809e71ab 100755 --- a/Packs/pai-hook-system/src/hooks/QuestionAnswered.hook.ts +++ b/Packs/pai-hook-system/src/hooks/QuestionAnswered.hook.ts @@ -36,18 +36,44 @@ * - Kitty unavailable: Silent failure */ +import { existsSync } from 'fs'; + const TAB_WORKING_BG = '#804000'; // Dark orange - actively working const ACTIVE_TAB_BG = '#002B80'; // Dark blue - active tab always const ACTIVE_TEXT = '#FFFFFF'; const INACTIVE_TEXT = '#A0A0A0'; +/** + * Get kitty socket path - required for socket-only remote control. + * Using socket-based control prevents escape sequence leaks (P@kitty-cmd artifacts). + */ +function getKittySocket(): string | null { + if (process.env.KITTY_LISTEN_ON) { + return process.env.KITTY_LISTEN_ON; + } + const defaultSocket = `/tmp/kitty-${process.env.USER}`; + try { + if (existsSync(defaultSocket)) { + return `unix:${defaultSocket}`; + } + } catch {} + return null; +} + async function main() { try { + const socket = getKittySocket(); + + if (!socket) { + console.error('[QuestionAnswered] No kitty socket available, skipping'); + process.exit(0); + } + // Set tab color: active stays dark blue, inactive shows orange - await Bun.$`kitten @ set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TEXT} inactive_bg=${TAB_WORKING_BG} inactive_fg=${INACTIVE_TEXT}`.quiet(); + await Bun.$`kitten @ --to ${socket} set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TEXT} inactive_bg=${TAB_WORKING_BG} inactive_fg=${INACTIVE_TEXT}`.quiet(); // Set working title - await Bun.$`kitty @ set-tab-title "⚙️Processing answer…"`.quiet(); + await Bun.$`kitty @ --to ${socket} set-tab-title "⚙️Processing answer…"`.quiet(); console.error('[QuestionAnswered] Tab reset to working state (orange on inactive only)'); } catch (error) { diff --git a/Packs/pai-hook-system/src/hooks/SetQuestionTab.hook.ts b/Packs/pai-hook-system/src/hooks/SetQuestionTab.hook.ts index 3370214e8..1cf62de2d 100755 --- a/Packs/pai-hook-system/src/hooks/SetQuestionTab.hook.ts +++ b/Packs/pai-hook-system/src/hooks/SetQuestionTab.hook.ts @@ -41,6 +41,8 @@ * - Typical execution: <50ms */ +import { existsSync } from 'fs'; + const TAB_AWAITING_BG = '#085050'; // Dark teal - waiting for user input const ACTIVE_TAB_BG = '#002B80'; // Dark blue - active tab always const TAB_TEXT = '#FFFFFF'; @@ -49,13 +51,37 @@ const INACTIVE_TEXT = '#A0A0A0'; // Simple question indicator - teal background does the work const QUESTION_TITLE = '❓ Question'; +/** + * Get kitty socket path - required for socket-only remote control. + * Using socket-based control prevents escape sequence leaks (P@kitty-cmd artifacts). + */ +function getKittySocket(): string | null { + if (process.env.KITTY_LISTEN_ON) { + return process.env.KITTY_LISTEN_ON; + } + const defaultSocket = `/tmp/kitty-${process.env.USER}`; + try { + if (existsSync(defaultSocket)) { + return `unix:${defaultSocket}`; + } + } catch {} + return null; +} + async function main() { try { + const socket = getKittySocket(); + + if (!socket) { + console.error('[SetQuestionTab] No kitty socket available, skipping'); + process.exit(0); + } + // Set tab color: active stays dark blue, inactive shows teal - await Bun.$`kitten @ set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${TAB_TEXT} inactive_bg=${TAB_AWAITING_BG} inactive_fg=${INACTIVE_TEXT}`; + await Bun.$`kitten @ --to ${socket} set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${TAB_TEXT} inactive_bg=${TAB_AWAITING_BG} inactive_fg=${INACTIVE_TEXT}`; // Set simple question title - teal background provides visual distinction - await Bun.$`kitty @ set-tab-title ${QUESTION_TITLE}`; + await Bun.$`kitty @ --to ${socket} set-tab-title ${QUESTION_TITLE}`; console.error('[SetQuestionTab] Tab set to teal with question indicator'); } catch (error) { diff --git a/Packs/pai-hook-system/src/hooks/UpdateTabTitle.hook.ts b/Packs/pai-hook-system/src/hooks/UpdateTabTitle.hook.ts index bfc0d2af6..2c7b609d6 100755 --- a/Packs/pai-hook-system/src/hooks/UpdateTabTitle.hook.ts +++ b/Packs/pai-hook-system/src/hooks/UpdateTabTitle.hook.ts @@ -61,10 +61,28 @@ */ import { execSync } from 'child_process'; -import { readFileSync } from 'fs'; +import { readFileSync, existsSync } from 'fs'; import { inference } from '../skills/CORE/Tools/Inference'; import { isValidTabSummary, getTabFallback } from './lib/response-format'; +/** + * Get kitty socket path - required for socket-only remote control. + * Using socket-based control prevents escape sequence leaks (P@kitty-cmd artifacts) + * that occur when hooks run as subprocesses. + */ +function getKittySocket(): string | null { + if (process.env.KITTY_LISTEN_ON) { + return process.env.KITTY_LISTEN_ON; + } + const defaultSocket = `/tmp/kitty-${process.env.USER}`; + try { + if (existsSync(defaultSocket)) { + return `unix:${defaultSocket}`; + } + } catch {} + return null; +} + // Tab colors - different states const TAB_WORKING_BG = '#804000'; // Dark orange - actively working const TAB_INFERENCE_BG = '#1E0A3C'; // Very dark purple - inference/AI thinking @@ -218,9 +236,9 @@ ${prompt.slice(0, 800)}`; type TabState = 'normal' | 'working' | 'inference'; /** - * Set terminal tab title and color based on state - * Uses Kitty remote control if available (hooks run without TTY), - * falls back to escape codes for other terminals + * Set terminal tab title and color based on state. + * Uses Kitty socket-based remote control to avoid escape sequence leaks. + * Falls back to escape codes for non-Kitty terminals. */ function setTabTitle(title: string, state: TabState = 'normal'): void { try { @@ -229,31 +247,32 @@ function setTabTitle(title: string, state: TabState = 'normal'): void { const truncated = titleWithSuffix.length > 50 ? titleWithSuffix.slice(0, 47) + '…' : titleWithSuffix; const escaped = truncated.replace(/'/g, "'\\''"); - // Check if we're in Kitty (TERM=xterm-kitty or KITTY_LISTEN_ON set) - const isKitty = process.env.TERM === 'xterm-kitty' || process.env.KITTY_LISTEN_ON; + // Get kitty socket for socket-based remote control + const socket = getKittySocket(); - if (isKitty) { - // Use Kitty remote control - works even without TTY - execSync(`kitty @ set-tab-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 }); + if (socket) { + // Use socket-based remote control - prevents escape sequence leaks + execSync(`kitty @ --to ${socket} set-tab-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 }); // Set color based on state if (state === 'inference') { - // Purple for inference/AI thinking - active tab stays dark blue, inactive shows purple execSync( - `kitten @ set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TEXT} inactive_bg=${TAB_INFERENCE_BG} inactive_fg=${INACTIVE_TEXT}`, + `kitten @ --to ${socket} set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TEXT} inactive_bg=${TAB_INFERENCE_BG} inactive_fg=${INACTIVE_TEXT}`, { stdio: 'ignore', timeout: 2000 } ); console.error('[UpdateTabTitle] Set inference color (purple on inactive only)'); } else if (state === 'working') { - // Orange for actively working - active tab stays dark blue, inactive shows orange execSync( - `kitten @ set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TEXT} inactive_bg=${TAB_WORKING_BG} inactive_fg=${INACTIVE_TEXT}`, + `kitten @ --to ${socket} set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TEXT} inactive_bg=${TAB_WORKING_BG} inactive_fg=${INACTIVE_TEXT}`, { stdio: 'ignore', timeout: 2000 } ); console.error('[UpdateTabTitle] Set working color (orange on inactive only)'); } - console.error('[UpdateTabTitle] Set via Kitty remote control'); + console.error('[UpdateTabTitle] Set via Kitty socket'); + } else if (process.env.TERM === 'xterm-kitty') { + // Kitty detected but no socket - skip to avoid escape sequence leaks + console.error('[UpdateTabTitle] Kitty detected but no socket available, skipping'); } else { // Fallback to escape codes for other terminals execSync(`printf '\\033]0;${escaped}\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] }); diff --git a/Packs/pai-hook-system/src/hooks/handlers/tab-state.ts b/Packs/pai-hook-system/src/hooks/handlers/tab-state.ts index 5e6b5ddde..d48f1548c 100755 --- a/Packs/pai-hook-system/src/hooks/handlers/tab-state.ts +++ b/Packs/pai-hook-system/src/hooks/handlers/tab-state.ts @@ -3,8 +3,10 @@ * * Pure handler: receives pre-parsed transcript data, updates Kitty tab. * No I/O for transcript reading - that's done by orchestrator. + * Uses socket-based remote control to prevent escape sequence leaks. */ +import { existsSync } from 'fs'; import { isValidVoiceCompletion, getTabFallback } from '../lib/response-format'; import type { ParsedTranscript, ResponseState } from '../../skills/CORE/Tools/TranscriptParser'; @@ -25,6 +27,23 @@ const ACTIVE_TAB_COLOR = '#002B80'; // Dark blue const ACTIVE_TEXT_COLOR = '#FFFFFF'; const INACTIVE_TEXT_COLOR = '#A0A0A0'; +/** + * Get kitty socket path - required for socket-only remote control. + * Using socket-based control prevents escape sequence leaks (P@kitty-cmd artifacts). + */ +function getKittySocket(): string | null { + if (process.env.KITTY_LISTEN_ON) { + return process.env.KITTY_LISTEN_ON; + } + const defaultSocket = `/tmp/kitty-${process.env.USER}`; + try { + if (existsSync(defaultSocket)) { + return `unix:${defaultSocket}`; + } + } catch {} + return null; +} + /** * Handle tab state update with pre-parsed transcript data. */ @@ -56,11 +75,19 @@ export async function handleTabState(parsed: ParsedTranscript): Promise { console.error(`[TabState] State: ${state}, Color: ${stateColor}, Suffix: "${suffix}"`); + // Get socket for kitty remote control + const socket = getKittySocket(); + + if (!socket) { + console.error('[TabState] No kitty socket available, skipping tab update'); + return; + } + // Set tab colors: active tab always dark blue, inactive shows state color - await Bun.$`kitten @ set-tab-color --self active_bg=${ACTIVE_TAB_COLOR} active_fg=${ACTIVE_TEXT_COLOR} inactive_bg=${stateColor} inactive_fg=${INACTIVE_TEXT_COLOR}`; + await Bun.$`kitten @ --to ${socket} set-tab-color --self active_bg=${ACTIVE_TAB_COLOR} active_fg=${ACTIVE_TEXT_COLOR} inactive_bg=${stateColor} inactive_fg=${INACTIVE_TEXT_COLOR}`; // Set tab title - await Bun.$`kitty @ set-tab-title ${tabTitle}`; + await Bun.$`kitty @ --to ${socket} set-tab-title ${tabTitle}`; } catch (error) { console.error('[TabState] Failed to update Kitty tab:', error); }