agent-yes 1.251.0 → 1.252.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.
Files changed (32) hide show
  1. package/dist/{SUPPORTED_CLIS-CwSokgZr.js → SUPPORTED_CLIS-ZDqi9LbP.js} +3 -3
  2. package/dist/{SUPPORTED_CLIS-BXJSjxkF.js → SUPPORTED_CLIS-kkxFgTKh.js} +2 -2
  3. package/dist/{agentShare-BbLkcQm4.js → agentShare-BXLfevNX.js} +3 -3
  4. package/dist/{callback-8RWpxE4I.js → callback-D63YERtT.js} +3 -3
  5. package/dist/{callback-C4O66DTj.js → callback-D_gfdf2c.js} +3 -3
  6. package/dist/cli.js +7 -7
  7. package/dist/index.js +2 -2
  8. package/dist/{notifyDaemon-B1IAqhdC.js → notifyDaemon-FhMvFDm8.js} +3 -3
  9. package/dist/{remotes-DNLZlk9U.js → remotes-DzPup1dS.js} +1 -1
  10. package/dist/{remotes-BxQnNxQv.js → remotes-a0bGDae9.js} +2 -2
  11. package/dist/{rustBinary-ByzIf2js.js → rustBinary-BBOpYC-y.js} +21 -3
  12. package/dist/{schedule-CcBm_AJi.js → schedule-B5w4JNpg.js} +4 -4
  13. package/dist/{serve-BXvJ0BVz.js → serve-H8y95HWl.js} +83 -18
  14. package/dist/{setup-DN7g-GlT.js → setup-CRJSyz9Z.js} +2 -2
  15. package/dist/{subcommands-CIIal0U8.js → subcommands-BGrZZAZ2.js} +2 -2
  16. package/dist/{subcommands-DeEoYwPl.js → subcommands-DeUbXdTK.js} +21 -16
  17. package/dist/{terminal-lMeA3l5c.js → terminal-BMHnUWFm.js} +2 -2
  18. package/dist/{todoCli-DhD3G8Et.js → todoCli-CVYCQrmX.js} +1 -1
  19. package/dist/{tray-DpswPpBI.js → tray-LxRESrQs.js} +1 -1
  20. package/dist/trayApp-DELeG-wq.js +5 -0
  21. package/dist/trayApp-DEmn5LW0.js +100 -0
  22. package/dist/{ts-D-lKlowc.js → ts-1rQZA3oo.js} +2 -2
  23. package/dist/{versionChecker-UbAydyGS.js → versionChecker-hMS7Hkpn.js} +2 -2
  24. package/dist/{webrtcRemote-D9ENKmzx.js → webrtcRemote-DgoZGUr9.js} +1 -1
  25. package/dist/{widget-w6D-Sn_o.js → widget-D55SUI6o.js} +3 -3
  26. package/dist/{ws-Ba4q33_l.js → ws-GVmilTGt.js} +3 -3
  27. package/package.json +1 -1
  28. package/ts/rustBinary.ts +22 -0
  29. package/ts/serve.ts +86 -3
  30. package/ts/subcommands.ts +5 -0
  31. package/ts/trayApp.spec.ts +282 -0
  32. package/ts/trayApp.ts +112 -0
@@ -98,7 +98,7 @@ async function cmdTermMint(args) {
98
98
  if (flags.ro && flags.interactive) throw new Error("--ro and --interactive are mutually exclusive");
99
99
  const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
100
100
  const canSend = flags.interactive === true;
101
- const { mintScopedTermToken } = await import("./serve-BXvJ0BVz.js");
101
+ const { mintScopedTermToken } = await import("./serve-H8y95HWl.js");
102
102
  const r = await mintScopedTermToken(pid, {
103
103
  ttlSec,
104
104
  canSend
@@ -135,4 +135,4 @@ async function cmdTerm(args) {
135
135
 
136
136
  //#endregion
137
137
  export { cmdTerm };
138
- //# sourceMappingURL=terminal-lMeA3l5c.js.map
138
+ //# sourceMappingURL=terminal-BMHnUWFm.js.map
@@ -456,4 +456,4 @@ async function runTodoSubcommand(rest0) {
456
456
 
457
457
  //#endregion
458
458
  export { runTodoSubcommand };
459
- //# sourceMappingURL=todoCli-DhD3G8Et.js.map
459
+ //# sourceMappingURL=todoCli-CVYCQrmX.js.map
@@ -187,4 +187,4 @@ async function startTray() {
187
187
 
188
188
  //#endregion
189
189
  export { ensureTray, startTray };
190
- //# sourceMappingURL=tray-DpswPpBI.js.map
190
+ //# sourceMappingURL=tray-LxRESrQs.js.map
@@ -0,0 +1,5 @@
1
+ import "./versionChecker-hMS7Hkpn.js";
2
+ import "./rustBinary-BBOpYC-y.js";
3
+ import { a as trayHiddenMarker, i as launchTray, n as hasDesktop, r as isTrayHidden, t as cmdTray } from "./trayApp-DEmn5LW0.js";
4
+
5
+ export { cmdTray };
@@ -0,0 +1,100 @@
1
+ import { n as findTrayLauncher } from "./rustBinary-BBOpYC-y.js";
2
+ import { t as agentYesHome } from "./agentYesHome-CtmsY6I-.js";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { unlink, writeFile } from "node:fs/promises";
6
+
7
+ //#region ts/trayApp.ts
8
+ /** Marker whose PRESENCE means the user has hidden the tray. */
9
+ function trayHiddenMarker() {
10
+ return path.join(agentYesHome(), "tray.hidden");
11
+ }
12
+ function isTrayHidden() {
13
+ return existsSync(trayHiddenMarker());
14
+ }
15
+ /** A desktop session we can show a tray on. Windows/macOS always; Linux needs a
16
+ * display server. Headless servers → no tray. */
17
+ function hasDesktop() {
18
+ if (process.platform === "win32" || process.platform === "darwin") return true;
19
+ return !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
20
+ }
21
+ /**
22
+ * Launch the native tray (detached, window-less) if it makes sense to: a desktop
23
+ * session, the tray binary exists, and the user hasn't hidden it. No-op otherwise.
24
+ * The tray also self-gates on `ay serve status`, so this is best-effort. Returns
25
+ * true if a process was spawned.
26
+ */
27
+ function launchTray() {
28
+ if (!hasDesktop() || isTrayHidden()) return false;
29
+ const bin = findTrayLauncher();
30
+ if (!bin) return false;
31
+ try {
32
+ Bun.spawn([bin], {
33
+ detached: true,
34
+ stdio: [
35
+ "ignore",
36
+ "ignore",
37
+ "ignore"
38
+ ]
39
+ }).unref();
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ /** Stop any running native tray process(es). */
46
+ async function stopTray() {
47
+ const argv = process.platform === "win32" ? [
48
+ "taskkill",
49
+ "/IM",
50
+ "agent-yes-tray.exe",
51
+ "/F"
52
+ ] : [
53
+ "pkill",
54
+ "-f",
55
+ "agent-yes-tray"
56
+ ];
57
+ try {
58
+ await Bun.spawn(argv, { stdio: [
59
+ "ignore",
60
+ "ignore",
61
+ "ignore"
62
+ ] }).exited;
63
+ } catch {}
64
+ }
65
+ async function cmdTray(args) {
66
+ const sub = args[0] ?? "status";
67
+ const w = (s = "") => process.stdout.write(s + "\n");
68
+ if (sub === "show") {
69
+ await unlink(trayHiddenMarker()).catch(() => {});
70
+ if (!hasDesktop()) {
71
+ process.stderr.write("ay tray show: no desktop session detected (headless) — nothing to show\n");
72
+ return 1;
73
+ }
74
+ if (!findTrayLauncher()) {
75
+ process.stderr.write("ay tray show: tray binary not found — build it with\n cargo build --release --manifest-path rs-tray/Cargo.toml\n");
76
+ return 1;
77
+ }
78
+ w(launchTray() ? "tray shown." : "tray not launched (already running, or serve not installed).");
79
+ w("hint: the tray auto-shows on desktop sessions whenever `ay serve` is installed.");
80
+ return 0;
81
+ }
82
+ if (sub === "hide") {
83
+ await writeFile(trayHiddenMarker(), "hidden by `ay tray hide`\n").catch(() => {});
84
+ await stopTray();
85
+ w("tray hidden — it will stay hidden until `ay tray show`.");
86
+ return 0;
87
+ }
88
+ if (sub === "status") {
89
+ w(`hidden: ${isTrayHidden() ? "yes (run `ay tray show` to re-enable)" : "no"}`);
90
+ w(`desktop: ${hasDesktop() ? "yes" : "no (headless)"}`);
91
+ w(`binary: ${findTrayLauncher() ?? "(not found — not built for this platform)"}`);
92
+ return 0;
93
+ }
94
+ process.stderr.write(`ay tray: unknown subcommand '${sub}' (use show | hide | status)\n`);
95
+ return 1;
96
+ }
97
+
98
+ //#endregion
99
+ export { trayHiddenMarker as a, launchTray as i, hasDesktop as n, isTrayHidden as r, cmdTray as t };
100
+ //# sourceMappingURL=trayApp-DEmn5LW0.js.map
@@ -1,6 +1,6 @@
1
1
  import { n as __esmMin, r as __exportAll } from "./chunk-cZBX9HZv.js";
2
2
  import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
3
- import { r as getInstalledPackage } from "./versionChecker-UbAydyGS.js";
3
+ import { r as getInstalledPackage } from "./versionChecker-hMS7Hkpn.js";
4
4
  import { t as agentYesHome } from "./agentYesHome-CtmsY6I-.js";
5
5
  import { d as openStore, p as LIFECYCLES, x as removeControlCharacters } from "./todoAutomation-Buo457VO.js";
6
6
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-DBoONoM-.js";
@@ -2155,4 +2155,4 @@ function sleep(ms) {
2155
2155
 
2156
2156
  //#endregion
2157
2157
  export { hasTodoStore as a, permissionBadge as c, answerAsk as i, AgentContext as l, agentYes as n, listAsks as o, config as r, listAsksForProject as s, CLIS_CONFIG as t, CLAUDE_SESSION_PIN_ENV as u };
2158
- //# sourceMappingURL=ts-D-lKlowc.js.map
2158
+ //# sourceMappingURL=ts-1rQZA3oo.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "agent-yes";
10
- var version = "1.251.0";
10
+ var version = "1.252.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -215,4 +215,4 @@ async function displayVersion() {
215
215
 
216
216
  //#endregion
217
217
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
218
- //# sourceMappingURL=versionChecker-UbAydyGS.js.map
218
+ //# sourceMappingURL=versionChecker-hMS7Hkpn.js.map
@@ -256,4 +256,4 @@ async function startWebrtcBridge(link) {
256
256
 
257
257
  //#endregion
258
258
  export { startWebrtcBridge };
259
- //# sourceMappingURL=webrtcRemote-D9ENKmzx.js.map
259
+ //# sourceMappingURL=webrtcRemote-DgoZGUr9.js.map
@@ -28,7 +28,7 @@ function parseFlags(args, known) {
28
28
  }
29
29
  /** Resolve the local daemon base URL + token (flags override discovery). */
30
30
  async function daemonTarget(flags) {
31
- const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-BXvJ0BVz.js");
31
+ const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-H8y95HWl.js");
32
32
  const base = typeof flags.base === "string" ? flags.base : await resolveDaemonHttpBase();
33
33
  if (!base) throw new Error("no running ay serve daemon found — start `ay serve` or pass --base <url> (e.g. http://127.0.0.1:PORT)");
34
34
  const token = typeof flags.token === "string" ? flags.token : await loadTokenReadOnly() ?? "";
@@ -186,7 +186,7 @@ async function cmdMint(args) {
186
186
  const bad = caps.filter((c) => !KNOWN_CAPS.has(c));
187
187
  if (bad.length) throw new Error(`unknown cap(s): ${bad.join(", ")} (valid: ${[...KNOWN_CAPS].join(", ")})`);
188
188
  const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
189
- const { mintScopedTermToken } = await import("./serve-BXvJ0BVz.js");
189
+ const { mintScopedTermToken } = await import("./serve-H8y95HWl.js");
190
190
  const r = await mintScopedTermToken(target, {
191
191
  ttlSec,
192
192
  caps
@@ -202,4 +202,4 @@ async function cmdMint(args) {
202
202
 
203
203
  //#endregion
204
204
  export { cmdMint, cmdWidget };
205
- //# sourceMappingURL=widget-w6D-Sn_o.js.map
205
+ //# sourceMappingURL=widget-D55SUI6o.js.map
@@ -3,9 +3,9 @@ import "./globalPidIndex-D9DOSd-c.js";
3
3
  import "./messageLog-C-YIzhRj.js";
4
4
  import "./e2e-CEIdJGZ7.js";
5
5
  import "./configShared-IBbnn9b8.js";
6
- import { C as listRecords } from "./subcommands-DeEoYwPl.js";
6
+ import { C as listRecords } from "./subcommands-DeUbXdTK.js";
7
7
  import "./webrtcLink-BMYneHbu.js";
8
- import "./remotes-BxQnNxQv.js";
8
+ import "./remotes-a0bGDae9.js";
9
9
  import { a as getProvisionRoot } from "./workspaceConfig-CpilQzMY.js";
10
10
  import { existsSync } from "fs";
11
11
  import { lstat, opendir } from "fs/promises";
@@ -418,4 +418,4 @@ async function cmdWs(args) {
418
418
 
419
419
  //#endregion
420
420
  export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, workspaceStatus };
421
- //# sourceMappingURL=ws-Ba4q33_l.js.map
421
+ //# sourceMappingURL=ws-GVmilTGt.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.251.0",
3
+ "version": "1.252.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
package/ts/rustBinary.ts CHANGED
@@ -131,6 +131,28 @@ export function findSpawnHiddenLauncher(): string | undefined {
131
131
  return undefined;
132
132
  }
133
133
 
134
+ /**
135
+ * Locate the `agent-yes-tray` binary (the system-tray companion). Returns
136
+ * undefined when it isn't present (not built / not shipped for this platform —
137
+ * Windows-first today). Unlike the spawn-hidden launcher this is NOT gated to
138
+ * Windows, so macOS/Linux builds are found once they exist. Lives in its OWN
139
+ * crate (rs-tray), so it sits under rs-tray/target rather than rs/target.
140
+ */
141
+ export function findTrayLauncher(): string | undefined {
142
+ const exe = process.platform === "win32" ? "agent-yes-tray.exe" : "agent-yes-tray";
143
+ const dir = import.meta.dirname ?? import.meta.dir;
144
+ const searchPaths = [
145
+ path.resolve(dir, `../rs-tray/target/release/${exe}`),
146
+ path.resolve(dir, `../rs-tray/target/debug/${exe}`),
147
+ path.join(getBinDir(), exe),
148
+ Bun.which("agent-yes-tray") ?? "",
149
+ ];
150
+ for (const p of searchPaths) {
151
+ if (p && existsSync(p)) return p;
152
+ }
153
+ return undefined;
154
+ }
155
+
134
156
  /**
135
157
  * Get GitHub release download URL for the binary
136
158
  */
package/ts/serve.ts CHANGED
@@ -53,6 +53,7 @@ import { type MailParty, recordInbox } from "./messageLog.ts";
53
53
  import { updateGlobalPidStatus } from "./globalPidIndex.ts";
54
54
  import { spawnRejectionReason } from "./spawnGate.ts";
55
55
  import { findSpawnHiddenLauncher } from "./rustBinary.ts";
56
+ import { launchTray } from "./trayApp.ts";
56
57
  import { pgidForWrapper } from "./reaper.ts";
57
58
  import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
58
59
  import { getInstalledPackage } from "./versionChecker.ts";
@@ -959,6 +960,32 @@ async function readDaemonServeArgs(mgr: DaemonManager): Promise<string[] | null>
959
960
  }
960
961
  }
961
962
 
963
+ // Whether the daemon PROCESS is currently online per the manager. Distinct from
964
+ // `readDaemonServeArgs` (registered?) and from the HTTP probe (reachable?) — a
965
+ // webrtc-only daemon opens no port, so the manager's own view is the only way to
966
+ // know it's up. oxmgr: `status <name>` → "Status: running"; pm2: jlist status.
967
+ async function readDaemonRunning(mgr: DaemonManager): Promise<boolean> {
968
+ try {
969
+ if (mgr.id === "oxmgr") {
970
+ const p = Bun.spawn([mgr.bin, "status", DAEMON_NAME], {
971
+ stdout: "pipe",
972
+ stderr: "ignore",
973
+ env: liveEnv(),
974
+ });
975
+ const out = await new Response(p.stdout).text();
976
+ if ((await p.exited) !== 0) return false;
977
+ return /^\s*Status:\s*running\b/im.test(out);
978
+ }
979
+ const p = Bun.spawn([mgr.bin, "jlist"], { stdout: "pipe", stderr: "ignore", env: liveEnv() });
980
+ const out = await new Response(p.stdout).text();
981
+ if ((await p.exited) !== 0) return false;
982
+ const list = JSON.parse(out) as Array<{ name?: string; pm2_env?: { status?: string } }>;
983
+ return list.find((x) => x.name === DAEMON_NAME)?.pm2_env?.status === "online";
984
+ } catch {
985
+ return false;
986
+ }
987
+ }
988
+
962
989
  function portFromArgs(args: string[]): number | null {
963
990
  const m = /--port[=\s](\d+)/.exec(args.join(" "));
964
991
  return m ? Number(m[1]) : null;
@@ -1064,6 +1091,38 @@ async function fetchDaemonVersion(port: number | null, token: string): Promise<s
1064
1091
  }
1065
1092
  }
1066
1093
 
1094
+ // ay serve start | stop — control the ALREADY-INSTALLED daemon's run state via
1095
+ // the manager. Used by the tray's toggle and available on the CLI. Starting an
1096
+ // unregistered daemon is a no-op with a hint to `install`.
1097
+ async function cmdServeStartStop(sub: "start" | "stop"): Promise<number> {
1098
+ const mgr = resolveDaemonManager();
1099
+ if (!mgr) {
1100
+ process.stderr.write("ay serve " + sub + ": no process manager found (need pm2 or oxmgr)\n");
1101
+ return 1;
1102
+ }
1103
+ if ((await readDaemonServeArgs(mgr)) === null) {
1104
+ process.stderr.write(
1105
+ `ay serve ${sub}: '${DAEMON_NAME}' is not installed — run 'ay serve install' first\n`,
1106
+ );
1107
+ return 1;
1108
+ }
1109
+ // pm2 restarts a stopped named proc with `start <name>`; oxmgr's `start` takes a
1110
+ // command, so an existing app is (re)started via `restart <name>`.
1111
+ const argv =
1112
+ sub === "stop"
1113
+ ? [mgr.bin, "stop", DAEMON_NAME]
1114
+ : mgr.id === "oxmgr"
1115
+ ? [mgr.bin, "restart", DAEMON_NAME]
1116
+ : [mgr.bin, "start", DAEMON_NAME];
1117
+ const code = await spawnExit(argv);
1118
+ process.stdout.write(
1119
+ code === 0
1120
+ ? `${sub === "stop" ? "stopped" : "started"} '${DAEMON_NAME}' (via ${mgr.id})\n`
1121
+ : `ay serve ${sub}: manager command failed (exit ${code})\n`,
1122
+ );
1123
+ return code;
1124
+ }
1125
+
1067
1126
  async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
1068
1127
  // Uninstall first, before any manager resolution/bootstrap: it must sweep
1069
1128
  // EVERY present manager, not just the currently-preferred one. After a
@@ -1293,6 +1352,10 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
1293
1352
  const code = await proc.exited;
1294
1353
  if (code === 0) {
1295
1354
  const onBoot = await ensureBootAutostart(mgr);
1355
+ // Desktop companion: bring up the tray icon (best-effort; no-op on headless
1356
+ // sessions, when the tray binary is absent, or when the user ran `ay tray
1357
+ // hide`). The tray self-gates on serve being installed.
1358
+ const trayShown = launchTray();
1296
1359
  const port = argsUsePortless(effArgs) ? await portlessAppPort() : portFromArgs(effArgs);
1297
1360
  const localUrl = argsUsePortless(effArgs) ? await resolvedPortlessConsoleUrl(token) : null;
1298
1361
  // Mirror cmdServe's mode resolution: webrtc-only daemons open no HTTP port.
@@ -1302,6 +1365,7 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
1302
1365
  process.stdout.write(
1303
1366
  `\n${priorArgs !== null ? `rolled '${DAEMON_NAME}' forward to` : `installed '${DAEMON_NAME}' as a daemon via ${mgr.id} —`} v${current}\n`,
1304
1367
  );
1368
+ if (trayShown) process.stdout.write(`tray: shown (hide it with \`ay tray hide\`)\n`);
1305
1369
  if (mgr.id === "oxmgr" && process.platform === "win32")
1306
1370
  process.stdout.write(
1307
1371
  onBoot
@@ -1369,6 +1433,8 @@ async function cmdServeStatus(args: string[]): Promise<number> {
1369
1433
  // A non-null arg list means the daemon is registered with the manager.
1370
1434
  const daemonArgs = mgr ? await readDaemonServeArgs(mgr) : null;
1371
1435
  const installed = daemonArgs !== null;
1436
+ // Manager-reported process state — the only up/down signal for webrtc daemons.
1437
+ const running = installed ? await readDaemonRunning(mgr!) : false;
1372
1438
  const a = daemonArgs ?? [];
1373
1439
  const local = argsUsePortless(a);
1374
1440
  const port = local ? await portlessAppPort() : portFromArgs(a);
@@ -1392,6 +1458,7 @@ async function cmdServeStatus(args: string[]): Promise<number> {
1392
1458
  {
1393
1459
  manager: mgr?.id ?? null,
1394
1460
  installed,
1461
+ running,
1395
1462
  mode,
1396
1463
  port: httpish ? port : null,
1397
1464
  localUrl: httpish ? localUrl : null,
@@ -1415,6 +1482,7 @@ async function cmdServeStatus(args: string[]): Promise<number> {
1415
1482
  w(`manager: ${mgr ? mgr.id : "none — install pm2 or oxmgr to daemonize"}`);
1416
1483
  if (installed) {
1417
1484
  w(`installed: yes (via ${mgr!.id})`);
1485
+ w(`running: ${running ? "yes" : "no (stopped)"}`);
1418
1486
  w(`mode: ${mode}${httpish ? (local ? ` (${localUrl})` : ` (port ${port})`) : ""}`);
1419
1487
  if (a.length) w(`args: ${a.join(" ")}`);
1420
1488
  } else {
@@ -1489,6 +1557,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
1489
1557
  // Daemon subcommands
1490
1558
  const sub = rest[0];
1491
1559
  if (sub === "status") return cmdServeStatus(rest.slice(1));
1560
+ if (sub === "start" || sub === "stop") return cmdServeStartStop(sub);
1492
1561
  if (sub === "healthcheck") {
1493
1562
  // oxmgr --health-cmd liveness probe. Exit non-zero only when the heartbeat is
1494
1563
  // demonstrably stale (event loop wedged), so the manager restarts us. A
@@ -3562,7 +3631,12 @@ export async function cmdServe(rest: string[]): Promise<number> {
3562
3631
  process.platform === "win32" && ayBin.toLowerCase().endsWith(".exe")
3563
3632
  ? [ayBin]
3564
3633
  : [process.execPath, ayBin];
3565
- const child = Bun.spawn([...ayCmd, ...args], {
3634
+ // On Windows, interpose the window-less launcher so this detached spawn
3635
+ // doesn't flash a console window that steals focus. Same shim the daemon
3636
+ // install uses; undefined off Windows / when not installed → raw command.
3637
+ const spawnHidden = findSpawnHiddenLauncher();
3638
+ const restartArgv = [...ayCmd, ...args];
3639
+ const child = Bun.spawn(spawnHidden ? [spawnHidden, ...restartArgv] : restartArgv, {
3566
3640
  cwd: record.cwd,
3567
3641
  detached: true,
3568
3642
  stdio: ["ignore", "ignore", "ignore"],
@@ -4003,6 +4077,14 @@ export async function cmdServe(rest: string[]): Promise<number> {
4003
4077
  // subagents don't inherit and collide. The caller can then address the agent
4004
4078
  // immediately: `ay <verb> <remote>:<agentId>`.
4005
4079
  const agentId = randomBytes(6).toString("hex");
4080
+ // On Windows, interpose the window-less launcher (GUI-subsystem shim) so the
4081
+ // detached agent spawn below doesn't flash a console window that grabs focus
4082
+ // — the exact annoyance when spawning an agent from the web console. Mirrors
4083
+ // the daemon-install path; undefined off Windows / when the launcher isn't
4084
+ // installed → fall back to the raw argv. NOTE: when interposed, `child.pid`
4085
+ // is the launcher's pid (a faithful shim that lives exactly as long as the
4086
+ // agent); `agentId` remains the stable handle the console addresses by.
4087
+ const spawnHidden = findSpawnHiddenLauncher();
4006
4088
  // Detach the agent into its OWN session (setsid). When `ay serve` runs WITH a
4007
4089
  // controlling terminal (started in a shell rather than as a headless daemon),
4008
4090
  // an undetached child inherits the daemon's session + controlling tty and lands
@@ -4029,7 +4111,8 @@ export async function cmdServe(rest: string[]): Promise<number> {
4029
4111
  );
4030
4112
  // File-backed stderr never blocks the child (a pipe we stop draining
4031
4113
  // after exec would), and bounds our read to the first few KB.
4032
- const child = Bun.spawn([shell, "-c", script, "ay-spawn", ...agentArgv], {
4114
+ const hookArgv = [shell, "-c", script, "ay-spawn", ...agentArgv];
4115
+ const child = Bun.spawn(spawnHidden ? [spawnHidden, ...hookArgv] : hookArgv, {
4033
4116
  cwd,
4034
4117
  detached: true,
4035
4118
  env: {
@@ -4081,7 +4164,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
4081
4164
  ...(provisioned ? { provisioned } : {}),
4082
4165
  });
4083
4166
  }
4084
- const child = Bun.spawn(agentArgv, {
4167
+ const child = Bun.spawn(spawnHidden ? [spawnHidden, ...agentArgv] : agentArgv, {
4085
4168
  cwd,
4086
4169
  detached: true,
4087
4170
  env: { ...agentEnv, AGENT_YES_AGENT_ID: agentId },
package/ts/subcommands.ts CHANGED
@@ -370,6 +370,7 @@ const SUBCOMMANDS = new Set([
370
370
  "widget",
371
371
  "mint",
372
372
  "serve",
373
+ "tray",
373
374
  "schedule",
374
375
  "remote",
375
376
  "expose",
@@ -525,6 +526,10 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
525
526
  const { cmdServe } = await import("./serve.ts");
526
527
  return cmdServe(rest);
527
528
  }
529
+ case "tray": {
530
+ const { cmdTray } = await import("./trayApp.ts");
531
+ return cmdTray(rest);
532
+ }
528
533
  case "setup": {
529
534
  const { cmdSetup } = await import("./setup.ts");
530
535
  return cmdSetup(rest);