/ extensions / iterm-tab-color.ts
iterm-tab-color.ts
1 /** 2 * iTerm2 tab color with two-state behavior: 3 * - runningColor: while Pi agent is running 4 * - notRunningColor: when Pi agent is not running 5 * 6 * Uses agent_start/agent_end (same lifecycle as titlebar-spinner), not turn events 7 */ 8 import type { 9 AgentEndEvent, 10 AgentStartEvent, 11 ExtensionAPI, 12 ExtensionContext, 13 SessionShutdownEvent, 14 SessionStartEvent, 15 } from "@mariozechner/pi-coding-agent"; 16 17 const OSC = "\x1b]"; 18 const BEL = "\x07"; 19 20 const CONFIG = { 21 runningColor: "71c1e3", 22 notRunningColor: "6f5e7d", 23 } as const; 24 25 const normalizeHexColor = (value: string): string => { 26 const normalized = value.trim().replace(/^#/, "").toUpperCase(); 27 if (!/^[0-9A-F]{6}$/.test(normalized)) { 28 throw new Error(`Invalid hex color: ${value}`); 29 } 30 return normalized; 31 }; 32 33 const parseChannel = (hexColor: string, start: number): number => 34 Number.parseInt(hexColor.slice(start, start + 2), 16); 35 36 const buildSetTabColorSequence = (hexColor: string): string => { 37 const red = parseChannel(hexColor, 0); 38 const green = parseChannel(hexColor, 2); 39 const blue = parseChannel(hexColor, 4); 40 41 return [ 42 `${OSC}6;1;bg;red;brightness;${red}${BEL}`, 43 `${OSC}6;1;bg;green;brightness;${green}${BEL}`, 44 `${OSC}6;1;bg;blue;brightness;${blue}${BEL}`, 45 ].join(""); 46 }; 47 48 const isIterm2 = (): boolean => process.env.TERM_PROGRAM === "iTerm.app"; 49 50 const writeOsc = (sequence: string): void => { 51 process.stdout.write(sequence); 52 }; 53 54 export default function (pi: ExtensionAPI) { 55 const runningColor = normalizeHexColor(CONFIG.runningColor); 56 const notRunningColor = normalizeHexColor(CONFIG.notRunningColor); 57 58 const setColor = (ctx: ExtensionContext, color: string): void => { 59 if (!ctx.hasUI || !isIterm2()) return; 60 writeOsc(buildSetTabColorSequence(color)); 61 }; 62 63 const setRunning = (ctx: ExtensionContext): void => { 64 setColor(ctx, runningColor); 65 }; 66 67 const setNotRunning = (ctx: ExtensionContext): void => { 68 setColor(ctx, notRunningColor); 69 }; 70 71 pi.on("session_start", async (_event: SessionStartEvent, ctx: ExtensionContext) => { 72 setNotRunning(ctx); 73 }); 74 75 pi.on("agent_start", async (_event: AgentStartEvent, ctx: ExtensionContext) => { 76 setRunning(ctx); 77 }); 78 79 pi.on("agent_end", async (_event: AgentEndEvent, ctx: ExtensionContext) => { 80 setNotRunning(ctx); 81 }); 82 83 pi.on("session_shutdown", async (_event: SessionShutdownEvent, ctx: ExtensionContext) => { 84 setNotRunning(ctx); 85 }); 86 }