agent-yes 1.70.1 → 1.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ts/tray.ts ADDED
@@ -0,0 +1,205 @@
1
+ import { existsSync } from "fs";
2
+ import { mkdir, readFile, writeFile, unlink } from "fs/promises";
3
+ import { homedir } from "os";
4
+ import path from "path";
5
+ import { getRunningAgentCount, type Task } from "./runningLock.ts";
6
+
7
+ const POLL_INTERVAL = 2000;
8
+
9
+ const getTrayDir = () => path.join(process.env.CLAUDE_YES_HOME || homedir(), ".claude-yes");
10
+ const getTrayPidFile = () => path.join(getTrayDir(), "tray.pid");
11
+
12
+ // Minimal 16x16 white circle PNG as base64 (used as tray icon)
13
+ const ICON_BASE64 =
14
+ "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA" +
15
+ "jklEQVQ4T2NkoBAwUqifgWoGMDIyNjAyMv5nYGBYQMgVjMgC" +
16
+ "QM0LGBkZHYDYAY8BDUBxByB2wGcAyAUOQOwAxPYMDAyOeCzA" +
17
+ "bwBIMyMjowNQsz0ely8ACjng8wJeA0CaGRgY7IHYAZ8hQHEH" +
18
+ "fF7AawBYMwODPZABRHsBpwEgzUDN9kDsgM8lQHEHfC4gJhwA" +
19
+ "AM3hMBGq3cNNAAAAAElFTkSuQmCC";
20
+
21
+ function buildMenuItems(tasks: Task[]) {
22
+ const items = [];
23
+
24
+ if (tasks.length === 0) {
25
+ items.push({ title: "No running agents", tooltip: "", enabled: false });
26
+ } else {
27
+ items.push({
28
+ title: `Running agents: ${tasks.length}`,
29
+ tooltip: "",
30
+ enabled: false,
31
+ });
32
+ items.push({ title: "---", tooltip: "", enabled: false });
33
+ for (const task of tasks) {
34
+ const dir = task.cwd.replace(/^.*[/\\]/, "");
35
+ const desc = task.task ? ` - ${task.task.slice(0, 40)}` : "";
36
+ items.push({
37
+ title: `[${task.pid}] ${dir}${desc}`,
38
+ tooltip: task.cwd,
39
+ enabled: false,
40
+ });
41
+ }
42
+ }
43
+
44
+ items.push({ title: "---", tooltip: "", enabled: false });
45
+ items.push({ title: "Quit Tray", tooltip: "Exit tray icon", enabled: true });
46
+
47
+ return items;
48
+ }
49
+
50
+ function isDesktopOS(): boolean {
51
+ return process.platform === "darwin" || process.platform === "win32";
52
+ }
53
+
54
+ function isTrayProcessRunning(pid: number): boolean {
55
+ try {
56
+ process.kill(pid, 0);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Write the current process PID to the tray PID file
65
+ */
66
+ async function writeTrayPid(): Promise<void> {
67
+ const dir = getTrayDir();
68
+ await mkdir(dir, { recursive: true });
69
+ await writeFile(getTrayPidFile(), String(process.pid), "utf8");
70
+ }
71
+
72
+ /**
73
+ * Remove the tray PID file
74
+ */
75
+ async function removeTrayPid(): Promise<void> {
76
+ try {
77
+ await unlink(getTrayPidFile());
78
+ } catch {
79
+ // ignore
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Check if a tray process is already running
85
+ */
86
+ export async function isTrayRunning(): Promise<boolean> {
87
+ try {
88
+ const pidFile = getTrayPidFile();
89
+ if (!existsSync(pidFile)) return false;
90
+ const pid = parseInt(await readFile(pidFile, "utf8"), 10);
91
+ if (isNaN(pid)) return false;
92
+ return isTrayProcessRunning(pid);
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Auto-spawn a tray process in the background if not already running.
100
+ * Only spawns on desktop OS (macOS/Windows).
101
+ * Silently does nothing if systray2 is not installed or on non-desktop OS.
102
+ */
103
+ export async function ensureTray(): Promise<void> {
104
+ if (!isDesktopOS()) return;
105
+ if (await isTrayRunning()) return;
106
+
107
+ try {
108
+ // Resolve the CLI entry point (dist/cli.js or ts/cli.ts)
109
+ const cliPath = new URL("./cli.ts", import.meta.url).pathname;
110
+ const { spawn } = await import("child_process");
111
+
112
+ // Spawn detached tray process
113
+ const child = spawn(process.execPath, [cliPath, "--tray", "--no-rust"], {
114
+ detached: true,
115
+ stdio: "ignore",
116
+ env: { ...process.env },
117
+ });
118
+ child.unref();
119
+ } catch {
120
+ // Silently fail — tray is best-effort
121
+ }
122
+ }
123
+
124
+ export async function startTray(): Promise<void> {
125
+ if (!isDesktopOS()) {
126
+ console.error("Tray icon is only supported on macOS and Windows.");
127
+ return;
128
+ }
129
+
130
+ // Check if another tray is already running
131
+ if (await isTrayRunning()) {
132
+ console.error("Tray is already running.");
133
+ return;
134
+ }
135
+
136
+ // Register our PID
137
+ await writeTrayPid();
138
+
139
+ let SysTray: typeof import("systray2").default;
140
+ try {
141
+ SysTray = (await import("systray2")).default;
142
+ } catch {
143
+ await removeTrayPid();
144
+ console.error("systray2 is not installed. Install it with: npm install systray2");
145
+ return;
146
+ }
147
+
148
+ const { count, tasks } = await getRunningAgentCount();
149
+
150
+ const systray = new SysTray({
151
+ menu: {
152
+ icon: ICON_BASE64,
153
+ title: `AY: ${count}`,
154
+ tooltip: `agent-yes: ${count} running`,
155
+ items: buildMenuItems(tasks),
156
+ },
157
+ debug: false,
158
+ copyDir: false,
159
+ });
160
+
161
+ await systray.ready();
162
+ console.log(`Tray started. Watching ${count} running agent(s).`);
163
+
164
+ // Handle quit
165
+ systray.onClick((action) => {
166
+ if (action.item.title === "Quit Tray") {
167
+ systray.kill(false);
168
+ removeTrayPid().finally(() => process.exit(0));
169
+ }
170
+ });
171
+
172
+ // Poll and update
173
+ let lastCount = count;
174
+ const interval = setInterval(async () => {
175
+ try {
176
+ const { count: newCount, tasks: newTasks } = await getRunningAgentCount();
177
+
178
+ if (newCount !== lastCount) {
179
+ lastCount = newCount;
180
+
181
+ // Update title and tooltip
182
+ systray.sendAction({
183
+ type: "update-menu",
184
+ menu: {
185
+ icon: ICON_BASE64,
186
+ title: `AY: ${newCount}`,
187
+ tooltip: `agent-yes: ${newCount} running`,
188
+ items: buildMenuItems(newTasks),
189
+ },
190
+ });
191
+ }
192
+ } catch {
193
+ // Ignore polling errors
194
+ }
195
+ }, POLL_INTERVAL);
196
+
197
+ // Cleanup on exit
198
+ const cleanup = () => {
199
+ clearInterval(interval);
200
+ systray.kill(false);
201
+ removeTrayPid().finally(() => process.exit(0));
202
+ };
203
+ process.on("SIGINT", cleanup);
204
+ process.on("SIGTERM", cleanup);
205
+ }