1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import type { EngineInterface, Register } from "claude-code";
import {
	done,
	isPrimaryAgent,
	permissionRequest,
	questionRequest,
	recordArguments,
	stateScript,
	working,
	type StateRecord,
} from "./state.ts";

const sketchybarTrigger = [
	"sketchybar_bin=$(command -v sketchybar) || sketchybar_bin=/opt/homebrew/opt/sketchybar/bin/sketchybar",
	'[ -x "$sketchybar_bin" ] && "$sketchybar_bin" --trigger agent_state_change >/dev/null 2>&1 || true',
].join("; ");

async function cwd($: EngineInterface): Promise<string> {
	return $.session.cwd();
}

async function triggerSketchybar($: EngineInterface): Promise<void> {
	await $.process.run(["/bin/sh", "-c", sketchybarTrigger], { timeoutMs: 5000 });
}

async function publish($: EngineInterface, record: StateRecord): Promise<void> {
	const [pane, home] = await Promise.all([$.env.get("TMUX_PANE"), $.env.get("HOME")]);
	if (pane === undefined || pane === "" || home === undefined || home === "") return;

	try {
		await $.process.run([stateScript(home), ...recordArguments(record)], { timeoutMs: 5000 });
		await triggerSketchybar($);
	} catch {
		// State tracking must not disrupt the Claude session.
	}
}

async function clear($: EngineInterface): Promise<void> {
	const [pane, home] = await Promise.all([$.env.get("TMUX_PANE"), $.env.get("HOME")]);
	if (pane === undefined || pane === "" || home === undefined || home === "") return;

	try {
		await $.process.run([stateScript(home), "clear"], { timeoutMs: 5000 });
		await triggerSketchybar($);
	} catch {
		// State tracking must not disrupt the Claude session.
	}
}

export const register: Register = on => {
	let writes: Promise<void> = Promise.resolve();
	const enqueue = (operation: () => Promise<void>) => {
		writes = writes.then(operation, operation).catch(() => undefined);
		return writes;
	};
	// No event fires when a prompt is answered, so a blocked session goes back
	// to working once the tool call it was blocked on resolves.
	let blocked = false;

	on("turn.start", async ($, event, next) => {
		if (isPrimaryAgent(event.agentId)) {
			blocked = false;
			await enqueue(async () => publish($, working(await cwd($))));
		}
		return next(event);
	});

	on("turn.complete", async ($, event, next) => {
		if (isPrimaryAgent(event.agentId)) {
			blocked = false;
			await enqueue(async () => publish($, done(await cwd($))));
		}
		return next(event);
	});

	on("tool.call", async ($, event, next) => {
		if (!isPrimaryAgent(event.agentId)) return next(event);

		if (event.tool === "AskUserQuestion") {
			blocked = true;
			await enqueue(async () => publish($, questionRequest(await cwd($))));
		}
		const result = await next(event);
		if (blocked) {
			blocked = false;
			await enqueue(async () => publish($, working(await cwd($))));
		}
		return result;
	}).catch(($, event, next) => next(event));

	on("classic.PermissionRequest", async ($, event, next) => {
		if (isPrimaryAgent(event.agent_id)) {
			blocked = true;
			await enqueue(async () => publish($, permissionRequest(event.tool_name, await cwd($))));
		}
		return next(event);
	}).catch(($, event, next) => next(event));

	on("classic.PermissionDenied", async ($, event, next) => {
		if (isPrimaryAgent(event.agent_id)) await enqueue(async () => publish($, working(await cwd($))));
		return next(event);
	}).catch(($, event, next) => next(event));

	on("session.end", async ($, event, next) => {
		await enqueue(() => clear($));
		return next(event);
	});
};