/ 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  }