@prestyj/cli 5.4.1 → 5.5.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 (192) hide show
  1. package/README.md +25 -12
  2. package/dist/app-sidecar.js +244 -74
  3. package/dist/app-sidecar.js.map +1 -1
  4. package/dist/chat-agents/general.d.ts +7 -0
  5. package/dist/chat-agents/general.d.ts.map +1 -0
  6. package/dist/chat-agents/general.js +16 -0
  7. package/dist/chat-agents/general.js.map +1 -0
  8. package/dist/chat-agents/general.test.d.ts +2 -0
  9. package/dist/chat-agents/general.test.d.ts.map +1 -0
  10. package/dist/chat-agents/general.test.js +47 -0
  11. package/dist/chat-agents/general.test.js.map +1 -0
  12. package/dist/chat-agents/index.d.ts +11 -0
  13. package/dist/chat-agents/index.d.ts.map +1 -0
  14. package/dist/chat-agents/index.js +89 -0
  15. package/dist/chat-agents/index.js.map +1 -0
  16. package/dist/chat-agents/memory.d.ts +53 -0
  17. package/dist/chat-agents/memory.d.ts.map +1 -0
  18. package/dist/chat-agents/memory.js +362 -0
  19. package/dist/chat-agents/memory.js.map +1 -0
  20. package/dist/chat-agents/memory.test.d.ts +2 -0
  21. package/dist/chat-agents/memory.test.d.ts.map +1 -0
  22. package/dist/chat-agents/memory.test.js +155 -0
  23. package/dist/chat-agents/memory.test.js.map +1 -0
  24. package/dist/chat-agents/research.d.ts +7 -0
  25. package/dist/chat-agents/research.d.ts.map +1 -0
  26. package/dist/chat-agents/research.js +18 -0
  27. package/dist/chat-agents/research.js.map +1 -0
  28. package/dist/chat-agents/shared.d.ts +11 -0
  29. package/dist/chat-agents/shared.d.ts.map +1 -0
  30. package/dist/chat-agents/shared.js +48 -0
  31. package/dist/chat-agents/shared.js.map +1 -0
  32. package/dist/chat-agents/specialists.test.d.ts +2 -0
  33. package/dist/chat-agents/specialists.test.d.ts.map +1 -0
  34. package/dist/chat-agents/specialists.test.js +97 -0
  35. package/dist/chat-agents/specialists.test.js.map +1 -0
  36. package/dist/chat-agents/therapist.d.ts +7 -0
  37. package/dist/chat-agents/therapist.d.ts.map +1 -0
  38. package/dist/chat-agents/therapist.js +20 -0
  39. package/dist/chat-agents/therapist.js.map +1 -0
  40. package/dist/chat-agents/types.d.ts +4 -0
  41. package/dist/chat-agents/types.d.ts.map +1 -0
  42. package/dist/chat-agents/types.js +7 -0
  43. package/dist/chat-agents/types.js.map +1 -0
  44. package/dist/cli/auth.js +1 -1
  45. package/dist/cli/auth.js.map +1 -1
  46. package/dist/cli.d.ts.map +1 -1
  47. package/dist/cli.js +72 -22
  48. package/dist/cli.js.map +1 -1
  49. package/dist/core/agent-session-memory-tail.test.d.ts +2 -0
  50. package/dist/core/agent-session-memory-tail.test.d.ts.map +1 -0
  51. package/dist/core/agent-session-memory-tail.test.js +122 -0
  52. package/dist/core/agent-session-memory-tail.test.js.map +1 -0
  53. package/dist/core/agent-session-subagent-policy.test.d.ts +2 -0
  54. package/dist/core/agent-session-subagent-policy.test.d.ts.map +1 -0
  55. package/dist/core/agent-session-subagent-policy.test.js +36 -0
  56. package/dist/core/agent-session-subagent-policy.test.js.map +1 -0
  57. package/dist/core/agent-session.d.ts +37 -8
  58. package/dist/core/agent-session.d.ts.map +1 -1
  59. package/dist/core/agent-session.js +168 -109
  60. package/dist/core/agent-session.js.map +1 -1
  61. package/dist/core/agents.d.ts +1 -1
  62. package/dist/core/agents.d.ts.map +1 -1
  63. package/dist/core/agents.js +4 -2
  64. package/dist/core/agents.js.map +1 -1
  65. package/dist/core/auth-providers.js +5 -5
  66. package/dist/core/auth-providers.js.map +1 -1
  67. package/dist/core/compaction/compactor.d.ts +7 -0
  68. package/dist/core/compaction/compactor.d.ts.map +1 -1
  69. package/dist/core/compaction/compactor.js +73 -9
  70. package/dist/core/compaction/compactor.js.map +1 -1
  71. package/dist/core/compaction/compactor.test.js +31 -4
  72. package/dist/core/compaction/compactor.test.js.map +1 -1
  73. package/dist/core/event-bus.d.ts +2 -0
  74. package/dist/core/event-bus.d.ts.map +1 -1
  75. package/dist/core/event-bus.js.map +1 -1
  76. package/dist/core/mcp/defaults.d.ts +1 -1
  77. package/dist/core/mcp/defaults.js +1 -1
  78. package/dist/core/mcp/store.d.ts +2 -2
  79. package/dist/core/mcp/store.test.js +1 -1
  80. package/dist/core/mcp/store.test.js.map +1 -1
  81. package/dist/core/progress/rebuild.d.ts +3 -3
  82. package/dist/core/progress/rebuild.d.ts.map +1 -1
  83. package/dist/core/progress/rebuild.js +33 -27
  84. package/dist/core/progress/rebuild.js.map +1 -1
  85. package/dist/core/progress/rebuild.test.js +15 -0
  86. package/dist/core/progress/rebuild.test.js.map +1 -1
  87. package/dist/core/project-discovery.d.ts +1 -1
  88. package/dist/core/project-discovery.d.ts.map +1 -1
  89. package/dist/core/project-discovery.js +5 -6
  90. package/dist/core/project-discovery.js.map +1 -1
  91. package/dist/core/project-discovery.test.js +14 -1
  92. package/dist/core/project-discovery.test.js.map +1 -1
  93. package/dist/core/radio.d.ts +3 -0
  94. package/dist/core/radio.d.ts.map +1 -1
  95. package/dist/core/radio.js +150 -21
  96. package/dist/core/radio.js.map +1 -1
  97. package/dist/core/radio.test.d.ts +2 -0
  98. package/dist/core/radio.test.d.ts.map +1 -0
  99. package/dist/core/radio.test.js +101 -0
  100. package/dist/core/radio.test.js.map +1 -0
  101. package/dist/core/session-history.test.js +1 -1
  102. package/dist/core/session-history.test.js.map +1 -1
  103. package/dist/core/session-manager.d.ts +1 -1
  104. package/dist/core/session-manager.d.ts.map +1 -1
  105. package/dist/core/session-manager.js +2 -1
  106. package/dist/core/session-manager.js.map +1 -1
  107. package/dist/core/session-manager.test.js +18 -0
  108. package/dist/core/session-manager.test.js.map +1 -1
  109. package/dist/core/settings-manager.d.ts +1 -0
  110. package/dist/core/settings-manager.d.ts.map +1 -1
  111. package/dist/core/settings-manager.js +1 -1
  112. package/dist/core/settings-manager.js.map +1 -1
  113. package/dist/core/subagent-manager.d.ts +73 -0
  114. package/dist/core/subagent-manager.d.ts.map +1 -0
  115. package/dist/core/subagent-manager.js +412 -0
  116. package/dist/core/subagent-manager.js.map +1 -0
  117. package/dist/core/subagent-manager.test.d.ts +2 -0
  118. package/dist/core/subagent-manager.test.d.ts.map +1 -0
  119. package/dist/core/subagent-manager.test.js +103 -0
  120. package/dist/core/subagent-manager.test.js.map +1 -0
  121. package/dist/core/subagent-policy.d.ts +4 -0
  122. package/dist/core/subagent-policy.d.ts.map +1 -0
  123. package/dist/core/subagent-policy.js +19 -0
  124. package/dist/core/subagent-policy.js.map +1 -0
  125. package/dist/modes/json-mode.d.ts.map +1 -1
  126. package/dist/modes/json-mode.js +3 -4
  127. package/dist/modes/json-mode.js.map +1 -1
  128. package/dist/modes/subagent-worker-mode.d.ts +14 -0
  129. package/dist/modes/subagent-worker-mode.d.ts.map +1 -0
  130. package/dist/modes/subagent-worker-mode.js +191 -0
  131. package/dist/modes/subagent-worker-mode.js.map +1 -0
  132. package/dist/tools/generate-image.d.ts.map +1 -1
  133. package/dist/tools/generate-image.js +12 -5
  134. package/dist/tools/generate-image.js.map +1 -1
  135. package/dist/tools/generate-image.test.js +17 -0
  136. package/dist/tools/generate-image.test.js.map +1 -1
  137. package/dist/tools/goals.d.ts +3 -3
  138. package/dist/tools/index.d.ts +10 -2
  139. package/dist/tools/index.d.ts.map +1 -1
  140. package/dist/tools/index.js +17 -1
  141. package/dist/tools/index.js.map +1 -1
  142. package/dist/tools/subagent-control.d.ts +6 -0
  143. package/dist/tools/subagent-control.d.ts.map +1 -0
  144. package/dist/tools/subagent-control.js +95 -0
  145. package/dist/tools/subagent-control.js.map +1 -0
  146. package/dist/tools/subagent-control.test.d.ts +2 -0
  147. package/dist/tools/subagent-control.test.d.ts.map +1 -0
  148. package/dist/tools/subagent-control.test.js +40 -0
  149. package/dist/tools/subagent-control.test.js.map +1 -0
  150. package/dist/tools/subagent-shared.d.ts +24 -0
  151. package/dist/tools/subagent-shared.d.ts.map +1 -0
  152. package/dist/tools/subagent-shared.js +53 -0
  153. package/dist/tools/subagent-shared.js.map +1 -0
  154. package/dist/tools/subagent-shared.test.d.ts +2 -0
  155. package/dist/tools/subagent-shared.test.d.ts.map +1 -0
  156. package/dist/tools/subagent-shared.test.js +29 -0
  157. package/dist/tools/subagent-shared.test.js.map +1 -0
  158. package/dist/tools/subagent.d.ts.map +1 -1
  159. package/dist/tools/subagent.js +21 -64
  160. package/dist/tools/subagent.js.map +1 -1
  161. package/dist/tools/subagent.test.js +27 -1
  162. package/dist/tools/subagent.test.js.map +1 -1
  163. package/dist/tools/web-fetch.d.ts +1 -0
  164. package/dist/tools/web-fetch.d.ts.map +1 -1
  165. package/dist/tools/web-fetch.js +112 -35
  166. package/dist/tools/web-fetch.js.map +1 -1
  167. package/dist/tools/web-fetch.test.js +146 -1
  168. package/dist/tools/web-fetch.test.js.map +1 -1
  169. package/dist/tools/web-search.d.ts +10 -0
  170. package/dist/tools/web-search.d.ts.map +1 -1
  171. package/dist/tools/web-search.js +236 -28
  172. package/dist/tools/web-search.js.map +1 -1
  173. package/dist/tools/web-search.test.js +77 -6
  174. package/dist/tools/web-search.test.js.map +1 -1
  175. package/dist/ui/App.d.ts +2 -0
  176. package/dist/ui/App.d.ts.map +1 -1
  177. package/dist/ui/App.js +61 -2
  178. package/dist/ui/App.js.map +1 -1
  179. package/dist/ui/hooks/useModeState.d.ts +4 -2
  180. package/dist/ui/hooks/useModeState.d.ts.map +1 -1
  181. package/dist/ui/hooks/useModeState.js +14 -3
  182. package/dist/ui/hooks/useModeState.js.map +1 -1
  183. package/dist/ui/login.d.ts.map +1 -1
  184. package/dist/ui/login.js +13 -5
  185. package/dist/ui/login.js.map +1 -1
  186. package/dist/ui/render.d.ts +14 -0
  187. package/dist/ui/render.d.ts.map +1 -1
  188. package/dist/ui/render.js +2 -0
  189. package/dist/ui/render.js.map +1 -1
  190. package/dist/ui/render.test.js +17 -0
  191. package/dist/ui/render.test.js.map +1 -1
  192. package/package.json +4 -4
package/README.md CHANGED
@@ -151,23 +151,36 @@ Plus built-in workflows that ship with the binary:
151
151
 
152
152
  EZ Coder comes with a focused set of tools. Each one is small, well-described, and earns its place in the prompt.
153
153
 
154
- | Tool | What it does |
155
- |---|---|
156
- | `bash` | Run shell commands |
157
- | `read` | Read file contents |
158
- | `write` | Write files |
159
- | `edit` | Surgical string replacements |
160
- | `grep` | Search file contents (regex) |
161
- | `find` | Find files by glob pattern |
162
- | `ls` | List directory contents |
163
- | `web_fetch` | Fetch URL content |
164
- | `screenshot` | Open a URL / dev server in a headless browser and capture a PNG so the agent can see the rendered page |
165
- | `subagent` | Spawn parallel sub-agents |
154
+ | Tool | What it does |
155
+ | --------------------------------- | ------------------------------------------------------------------------------------------------------ |
156
+ | `bash` | Run shell commands |
157
+ | `read` | Read file contents |
158
+ | `write` | Write files |
159
+ | `edit` | Surgical string replacements |
160
+ | `grep` | Search file contents (regex) |
161
+ | `find` | Find files by glob pattern |
162
+ | `ls` | List directory contents |
163
+ | `web_fetch` | Fetch URL content |
164
+ | `screenshot` | Open a URL / dev server in a headless browser and capture a PNG so the agent can see the rendered page |
165
+ | `subagent` | Run one blocking, isolated child task (backward-compatible) |
166
+ | `spawn_agent` / `wait_agent` | Launch persistent child turns concurrently and collect results |
167
+ | `send_message` / `followup_task` | Steer a running child or reuse an idle child's context |
168
+ | `list_agents` / `interrupt_agent` | Inspect or interrupt persistent children |
166
169
 
167
170
  The `screenshot` tool needs the optional `playwright` dependency plus a one-time `npx playwright install chromium`. Without it the tool returns an install hint instead of failing the turn. Captured images render inline in graphics-capable terminals (kitty, Ghostty, WezTerm, iTerm2); other terminals show a text line.
168
171
 
169
172
  Plus the [Grep MCP](https://grep.dev) for searching across 1M+ public GitHub repos. Add your own MCPs in settings if you need more — but start lean.
170
173
 
174
+ ### Async subagent lifecycle
175
+
176
+ `subagent` remains blocking. The async suite launches persistent NDJSON worker processes, so a parent can start up to four active child turns, keep working, steer them, and wait for any or all results. Up to eight idle workers remain available for follow-up; bounded snapshots retain the latest 20 agents.
177
+
178
+ Only GPT-5.6 Sol/Terra at **Ultra** delegates proactively. Lower Sol/Terra levels use async agents only when the user or project/skill instructions request delegation; other models receive no proactive policy.
179
+
180
+ Children share the parent working directory, not isolated worktrees. Parallel writes must target disjoint files or subsystems. Async fan-out is one level deep, child output is bounded, idle workers reap after 10 minutes, and workers are not resumable after a CLI/app restart.
181
+
182
+ Parent cancellation interrupts active children. Session disposal shuts down every worker process alongside background commands, LSP servers, and MCP connections.
183
+
171
184
  ---
172
185
 
173
186
  ## 🪄 Custom commands
@@ -20,7 +20,10 @@ import { randomUUID } from "node:crypto";
20
20
  import { parseArgs } from "node:util";
21
21
  import { formatError } from "@prestyj/ai";
22
22
  import { runJsonMode } from "./modes/json-mode.js";
23
+ import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
23
24
  import { AgentSession } from "./core/agent-session.js";
25
+ import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, sessionsDirForChatAgent, switchChatAgent, } from "./chat-agents/index.js";
26
+ import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
24
27
  import { buildNolanSystemPrompt, buildNolanAutopilotSystemPrompt } from "./core/nolan-prompt.js";
25
28
  import { buildNolanDigest, buildNolanAutopilotContext, buildNolanAutopilotPlanContext, } from "./core/nolan-context.js";
26
29
  import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
@@ -46,7 +49,7 @@ import { loadCustomCommands } from "./core/custom-commands.js";
46
49
  import { discoverProjects, listRecentSessions } from "./core/project-discovery.js";
47
50
  import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
48
51
  import { initLogger, log } from "./core/logger.js";
49
- import { RADIO_STATIONS, getCurrentStation, playRadio, stopRadio } from "./core/radio.js";
52
+ import { RADIO_STATIONS, getCurrentStation, getRadioVolume, playRadio, setRadioVolume, stopRadio, } from "./core/radio.js";
50
53
  import { enrichProcessPath } from "./core/shell-path.js";
51
54
  import { downscaleForPreview, validateVisionImage } from "./utils/image.js";
52
55
  import { startServeMode } from "./modes/serve-mode.js";
@@ -457,6 +460,11 @@ function daemonJson(res, status, body) {
457
460
  res.end(JSON.stringify(body));
458
461
  }
459
462
  async function main() {
463
+ // Hidden persistent-worker dispatch must win before strict JSON/server parsing.
464
+ if (process.argv.includes("--subagent-worker")) {
465
+ await runSubagentWorkerMode();
466
+ return;
467
+ }
460
468
  // Sub-agent JSON-mode dispatch must win before any sidecar/server setup.
461
469
  if (await runJsonModeIfRequested())
462
470
  return;
@@ -502,6 +510,13 @@ async function main() {
502
510
  // request to its window's session via the `x-gg-session` header (and the
503
511
  // `?session=` query for the SSE /events stream).
504
512
  const sessions = new Map();
513
+ const memoryStore = new MemoryStore({
514
+ onChange: ({ memories }) => {
515
+ for (const ctx of sessions.values()) {
516
+ ctx.broadcast("memory_change", { count: memories.length });
517
+ }
518
+ },
519
+ });
505
520
  // XP/rank progress — loaded once per daemon; awards fan out to every window.
506
521
  // Each frame is tagged `origin: true` only for the session that earned the
507
522
  // XP, so that window alone plays sounds/chips while the rest just re-render.
@@ -600,7 +615,7 @@ async function main() {
600
615
  return;
601
616
  }
602
617
  // ── Daemon-level routes (session lifecycle) ──────────────────────────
603
- // Create a session for a window: { cwd, sessionPath? } → { sessionId }.
618
+ // Create a session for a window: { mode?, cwd, sessionPath? } → { sessionId }.
604
619
  if (method === "POST" && url === "/session") {
605
620
  void daemonReadBody(req).then(async (raw) => {
606
621
  let body = {};
@@ -610,15 +625,22 @@ async function main() {
610
625
  catch {
611
626
  /* empty/invalid body → defaults below */
612
627
  }
628
+ const mode = body.mode === "chat" ? "chat" : "code";
629
+ const chatAgent = parseChatAgentId(body.chatAgent);
613
630
  const sessionCwd = typeof body.cwd === "string" && body.cwd
614
631
  ? body.cwd
615
632
  : (process.env.GG_APP_CWD ?? process.cwd());
616
633
  const sessionPath = typeof body.sessionPath === "string" && body.sessionPath ? body.sessionPath : undefined;
617
634
  const id = randomUUID();
618
635
  try {
619
- const ctx = await createSession({ auth, paths, progress }, { id, cwd: sessionCwd, sessionPath });
636
+ const ctx = await createSession({ auth, paths, progress, memoryStore }, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
620
637
  sessions.set(id, ctx);
621
- log("INFO", "app-sidecar", "session created", { id, cwd: sessionCwd });
638
+ log("INFO", "app-sidecar", "session created", {
639
+ id,
640
+ mode,
641
+ chatAgent,
642
+ cwd: sessionCwd,
643
+ });
622
644
  daemonJson(res, 200, { sessionId: id });
623
645
  }
624
646
  catch (err) {
@@ -681,16 +703,40 @@ async function main() {
681
703
  process.stdout.write(`GG_APP_LISTENING ${addr.port}\n`);
682
704
  log("INFO", "app-sidecar", "daemon listening", { port: String(addr.port), host });
683
705
  });
684
- const shutdown = async () => {
706
+ const shellPid = process.ppid;
707
+ let shuttingDown = false;
708
+ async function shutdown() {
709
+ if (shuttingDown)
710
+ return;
711
+ shuttingDown = true;
712
+ clearInterval(parentWatch);
685
713
  // Radio playback is app-wide (one stream across all windows), so it stops
686
714
  // at the daemon level, not per session.
687
715
  stopRadio();
688
716
  await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
689
717
  server.close();
690
718
  process.exit(0);
691
- };
719
+ }
692
720
  process.on("SIGINT", () => void shutdown());
693
721
  process.on("SIGTERM", () => void shutdown());
722
+ process.once("exit", stopRadio);
723
+ // Tauri can disappear without delivering a signal (force-quit, dev runner
724
+ // teardown, crash). Detect reparenting or a dead shell so the daemon and its
725
+ // radio player do not survive as audible orphans.
726
+ const parentWatch = setInterval(() => {
727
+ let parentAlive = process.ppid === shellPid;
728
+ if (parentAlive && shellPid > 1) {
729
+ try {
730
+ process.kill(shellPid, 0);
731
+ }
732
+ catch (error) {
733
+ parentAlive = error.code === "EPERM";
734
+ }
735
+ }
736
+ if (!parentAlive)
737
+ void shutdown();
738
+ }, 1_000);
739
+ parentWatch.unref?.();
694
740
  }
695
741
  /** Nolan's read-only tool allow-list. Excludes every mutating tool (write/edit/
696
742
  * bash/tasks/subagent/generate_image/enter_plan/exit_plan/task_*) so the mentor
@@ -745,8 +791,14 @@ function buildNolanContext(buildSession, cwd, gitBranch, question, workflowComma
745
791
  });
746
792
  }
747
793
  async function createProgressManager(agentDir, broadcastAll) {
748
- // Boot: recovery chain main → backup → one-time session-history rebuild → empty.
749
- let file = await loadProgress({ rebuild: () => rebuildFromSessions() });
794
+ // Boot: recovery chain main → backup → coding + chat session rebuild → empty.
795
+ const coderSessionsDir = path.join(agentDir, "sessions");
796
+ let file = await loadProgress({
797
+ rebuild: () => rebuildFromSessions([
798
+ coderSessionsDir,
799
+ ...CHAT_AGENT_IDS.map((agentId) => chatAgentSessionsDir(coderSessionsDir, agentId)),
800
+ ]),
801
+ });
750
802
  // Don't re-celebrate an old levelUp event on boot.
751
803
  let lastSeenNonce = file.lastEvent?.nonce ?? null;
752
804
  log("INFO", "app-sidecar", "progress loaded", {
@@ -794,7 +846,7 @@ async function createProgressManager(agentDir, broadcastAll) {
794
846
  });
795
847
  }
796
848
  }
797
- // Watch ~/.gg (dir watch survives the atomic tmp+rename) for progress.json
849
+ // Watch ~/.ezcoder (dir watch survives the atomic tmp+rename) for progress.json
798
850
  // writes from other daemon processes; debounce, reload read-only, dedupe by nonce.
799
851
  let watchDebounce = null;
800
852
  try {
@@ -834,8 +886,10 @@ async function createProgressManager(agentDir, broadcastAll) {
834
886
  * logger, PATH, shared auth file, and radio live at the daemon level.
835
887
  */
836
888
  async function createSession(deps, opts) {
837
- const { auth, progress } = deps;
889
+ const { auth, progress, memoryStore } = deps;
838
890
  const paths = deps.paths;
891
+ const mode = opts.mode;
892
+ let chatAgent = opts.chatAgent;
839
893
  const cwd = opts.cwd;
840
894
  // Base host for parsing request-URL query params (value is irrelevant to
841
895
  // parsing); the daemon owns the real listen host.
@@ -938,66 +992,77 @@ async function createSession(deps, opts) {
938
992
  // empty/unset starts a fresh session.
939
993
  const resumeSessionPath = opts.sessionPath;
940
994
  let abort = new AbortController();
941
- const session = new AgentSession({
995
+ const baseSessionOptions = {
942
996
  provider,
943
997
  model,
944
998
  cwd,
945
999
  thinkingLevel,
946
1000
  sessionId: resumeSessionPath,
947
1001
  signal: abort.signal,
948
- // The shell gates window readiness on the GG_APP_LISTENING handshake, which
949
- // can't fire until initialize() resolves. Connect MCP in the background so a
950
- // slow or hanging stdio server (e.g. a first-run `npx -y @playwright/mcp`
951
- // download) can't delay the sidecar past the webview's startup timeout
952
- // ("sidecar did not start in time"). Tools attach when the servers come up.
1002
+ // Keep MCP startup off the readiness path in both modes.
953
1003
  backgroundMcpConnect: true,
954
- // Plan mode: the agent's enter_plan/exit_plan tools drive these. We flip
955
- // session plan state (rebuilds the system prompt + enforces read-only
956
- // tools) and surface the transition to the webview.
957
- onEnterPlan: async (reason) => {
958
- // During a task run there is no human present to approve a plan, and a
959
- // submitted plan would strand the task in-progress while the run-all loop
960
- // advances to the next one. Decline plan mode so the agent implements the
961
- // task directly — mirrors the TUI's isUnattendedRun guard (App.tsx). The
962
- // enter_plan tool turns a false return into an "implement directly" hint.
963
- if (taskRunActive)
964
- return false;
965
- await session.setPlanMode(true);
966
- broadcast("plan_enter", { reason: reason ?? "" });
967
- // Persist the plan-mode banner so a resumed session still shows it.
968
- void session.persistAppMarker("plan", { reason: reason ?? "" }).catch(() => { });
969
- },
970
- onExitPlan: async (planPath) => {
971
- // A model can call exit_plan even when enter_plan was declined. During a
972
- // task run, never route the plan into review (no approver) — that would
973
- // hang the task and stall run-all. Leave plan mode and tell it to build.
974
- if (taskRunActive) {
1004
+ };
1005
+ let session;
1006
+ if (mode === "chat") {
1007
+ session = createChatAgent(chatAgent, {
1008
+ ...baseSessionOptions,
1009
+ sessionsDir: paths.sessionsDir,
1010
+ additionalTools: buildMemoryTools(memoryStore),
1011
+ getSystemPromptTail: () => memoryStore.renderForPrompt(),
1012
+ onAgentChange: async (nextAgent) => {
1013
+ chatAgent = nextAgent;
1014
+ broadcast("chat_agent_change", { chatAgent: nextAgent });
1015
+ await session.persistAppMarker("agent_handoff", { chatAgent: nextAgent }).catch((error) => {
1016
+ log("WARN", "app-sidecar", "agent handoff marker persist failed", {
1017
+ message: error instanceof Error ? error.message : String(error),
1018
+ });
1019
+ });
1020
+ },
1021
+ });
1022
+ }
1023
+ else {
1024
+ session = new AgentSession({
1025
+ ...baseSessionOptions,
1026
+ // Plan mode belongs only to the coding agent.
1027
+ onEnterPlan: async (reason) => {
1028
+ // Unattended task runs cannot approve plans; implement directly instead.
1029
+ if (taskRunActive)
1030
+ return false;
1031
+ await session.setPlanMode(true);
1032
+ broadcast("plan_enter", { reason: reason ?? "" });
1033
+ void session.persistAppMarker("plan", { reason: reason ?? "" }).catch(() => { });
1034
+ },
1035
+ onExitPlan: async (planPath) => {
1036
+ if (taskRunActive) {
1037
+ await session.setPlanMode(false);
1038
+ return ("Plan review is unavailable during a task run. Skip review and implement " +
1039
+ "the plan directly now: make the changes, verify them, then mark the task done.");
1040
+ }
975
1041
  await session.setPlanMode(false);
976
- return ("Plan review is unavailable during a task run. Skip review and implement " +
977
- "the plan directly now: make the changes, verify them, then mark the task done.");
978
- }
979
- await session.setPlanMode(false);
980
- // Surface the plan's path + markdown so the webview can show the review
981
- // modal (Accept / Feedback / Reject). Best-effort content read.
982
- let content;
983
- try {
984
- content = await fs.readFile(planPath, "utf-8");
985
- }
986
- catch {
987
- content = "";
988
- }
989
- // Record the submitted plan so the autopilot gate can route this turn
990
- // into a PLAN review instead of a stale work review (plan mode is
991
- // already false here, so the gate's planMode check alone never catches
992
- // a submission). setPendingPlan bumps planGeneration, which invalidates
993
- // any in-flight Nolan plan review racing a user action.
994
- setPendingPlan(planPath, content);
995
- broadcast("plan_exit", { planPath, content });
996
- return "Plan submitted for user review. Wait for the user to approve, reject, or dismiss it before implementing.";
997
- },
998
- });
1042
+ let content;
1043
+ try {
1044
+ content = await fs.readFile(planPath, "utf-8");
1045
+ }
1046
+ catch {
1047
+ content = "";
1048
+ }
1049
+ setPendingPlan(planPath, content);
1050
+ broadcast("plan_exit", { planPath, content });
1051
+ return "Plan submitted for user review. Wait for the user to approve, reject, or dismiss it before implementing.";
1052
+ },
1053
+ });
1054
+ }
999
1055
  await session.initialize();
1000
- log("INFO", "app-sidecar", "session ready", { provider, model, cwd });
1056
+ if (mode === "chat") {
1057
+ const restoredAgent = [...session.getAppMarkers()]
1058
+ .reverse()
1059
+ .find((marker) => marker.kind === "agent_handoff")?.data.chatAgent;
1060
+ if (typeof restoredAgent === "string") {
1061
+ chatAgent = parseChatAgentId(restoredAgent);
1062
+ await switchChatAgent(session, chatAgent, false);
1063
+ }
1064
+ }
1065
+ log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
1001
1066
  // Footer extras (context window, git branch, background tasks). The git
1002
1067
  // branch is resolved once at startup and refreshed lazily; the context
1003
1068
  // window follows the active model.
@@ -1060,6 +1125,7 @@ async function createSession(deps, opts) {
1060
1125
  });
1061
1126
  session.eventBus.on("model_change", (d) => broadcast("model_change", d));
1062
1127
  session.eventBus.on("hook", (d) => broadcast("hook", d));
1128
+ session.eventBus.on("subagent_state", (d) => broadcast("subagent_state", d));
1063
1129
  session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
1064
1130
  session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
1065
1131
  let running = false;
@@ -1071,7 +1137,7 @@ async function createSession(deps, opts) {
1071
1137
  // ezcoder-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
1072
1138
  // runAutopilotCycle after the user's turn settles — Nolan auto-reviews the work
1073
1139
  // and drives the review→prompt→review loop.
1074
- let autopilot = await loadAutopilot(cwd);
1140
+ let autopilot = mode === "code" && (await loadAutopilot(cwd));
1075
1141
  // True while an autopilot review is in flight (used to defer nolanAuto model
1076
1142
  // switches, like nolanRunning does for chat Nolan, and to drive the spinner).
1077
1143
  let autopilotReviewing = false;
@@ -1139,7 +1205,7 @@ async function createSession(deps, opts) {
1139
1205
  let nolanRunning = false;
1140
1206
  let pendingNolanModel = null;
1141
1207
  const nolanToolCallNames = new Map();
1142
- // Nolan's per-project model override. null → Nolan (chat + autopilot) follows GG
1208
+ // Nolan's per-project model override. null → Nolan (chat + autopilot) follows EZ
1143
1209
  // Coder's model, including live switches (the historical behavior). Set → Nolan
1144
1210
  // is pinned to his own model and EZ Coder switches no longer touch him. A
1145
1211
  // stale persisted pin (model dropped from the registry / provider logged
@@ -1149,7 +1215,7 @@ async function createSession(deps, opts) {
1149
1215
  providerConnected: () => true, // async auth checked below
1150
1216
  });
1151
1217
  if (nolanModelOverride && !(await auth.hasProviderAuth(nolanModelOverride.provider))) {
1152
- log("WARN", "app-sidecar", "ken model override provider not connected — following GG", {
1218
+ log("WARN", "app-sidecar", "Nolan model override provider not connected — following EZ Coder", {
1153
1219
  provider: nolanModelOverride.provider,
1154
1220
  model: nolanModelOverride.model,
1155
1221
  });
@@ -1764,6 +1830,8 @@ async function createSession(deps, opts) {
1764
1830
  const st = session.getState();
1765
1831
  json(res, 200, {
1766
1832
  ...st,
1833
+ mode,
1834
+ chatAgent,
1767
1835
  running,
1768
1836
  ready: true,
1769
1837
  thinkingLevel: session.getThinkingLevel() ?? null,
@@ -1779,6 +1847,26 @@ async function createSession(deps, opts) {
1779
1847
  json(res, 200, progress.snapshot());
1780
1848
  return;
1781
1849
  }
1850
+ if (method === "GET" && url === "/memories") {
1851
+ void memoryStore
1852
+ .snapshot()
1853
+ .then((snapshot) => json(res, 200, snapshot))
1854
+ .catch((error) => json(res, 500, { error: error instanceof Error ? error.message : String(error) }));
1855
+ return;
1856
+ }
1857
+ if (method === "DELETE" && url.startsWith("/memories/")) {
1858
+ const id = decodeURIComponent(url.slice("/memories/".length));
1859
+ if (!id) {
1860
+ json(res, 400, { error: "memory id is required" });
1861
+ return;
1862
+ }
1863
+ void memoryStore
1864
+ .forget(id)
1865
+ .then(() => memoryStore.snapshot())
1866
+ .then((snapshot) => json(res, 200, snapshot))
1867
+ .catch((error) => json(res, 500, { error: error instanceof Error ? error.message : String(error) }));
1868
+ return;
1869
+ }
1782
1870
  if (method === "GET" && (url === "/events" || url.startsWith("/events?"))) {
1783
1871
  res.writeHead(200, {
1784
1872
  "content-type": "text/event-stream",
@@ -1794,6 +1882,8 @@ async function createSession(deps, opts) {
1794
1882
  type: "ready",
1795
1883
  data: {
1796
1884
  ...st,
1885
+ mode,
1886
+ chatAgent,
1797
1887
  running,
1798
1888
  thinkingLevel: session.getThinkingLevel() ?? null,
1799
1889
  supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
@@ -1909,7 +1999,36 @@ async function createSession(deps, opts) {
1909
1999
  json(res, 400, { error: "missing cwd query param" });
1910
2000
  return;
1911
2001
  }
1912
- void listRecentSessions(target, 5)
2002
+ const requestedAgent = new URL(url, `http://${host}`).searchParams.get("chatAgent");
2003
+ if (requestedAgent === "all") {
2004
+ void Promise.all(CHAT_AGENT_IDS.map(async (agentId) => {
2005
+ const agentSessions = await listRecentSessions(target, 5, chatAgentSessionsDir(paths.sessionsDir, agentId));
2006
+ return agentSessions.map((item) => ({ ...item, chatAgent: agentId }));
2007
+ }))
2008
+ .then(async (groups) => {
2009
+ const dated = await Promise.all(groups.flat().map(async (item) => ({
2010
+ item,
2011
+ mtime: await fs
2012
+ .stat(item.path)
2013
+ .then((stat) => stat.mtimeMs)
2014
+ .catch(() => 0),
2015
+ })));
2016
+ const recent = dated
2017
+ .sort((left, right) => right.mtime - left.mtime)
2018
+ .slice(0, 5)
2019
+ .map(({ item }) => item);
2020
+ json(res, 200, { sessions: recent });
2021
+ })
2022
+ .catch(() => json(res, 200, { sessions: [] }));
2023
+ return;
2024
+ }
2025
+ // The picker may be served by a sidecar currently running in Chat mode.
2026
+ // An omitted chatAgent always means coding history; chat callers identify
2027
+ // their namespace explicitly (one agent or "all").
2028
+ const sessionsDir = requestedAgent
2029
+ ? sessionsDirForChatAgent(paths.sessionsDir, requestedAgent)
2030
+ : paths.sessionsDir;
2031
+ void listRecentSessions(target, 5, sessionsDir)
1913
2032
  .then((sessions) => json(res, 200, { sessions }))
1914
2033
  .catch(() => json(res, 200, { sessions: [] }));
1915
2034
  return;
@@ -2174,12 +2293,17 @@ async function createSession(deps, opts) {
2174
2293
  }
2175
2294
  // Assistant tool_call blocks: detect sub-agent delegations.
2176
2295
  if (msg.role === "assistant" && typeof msg.content !== "string") {
2177
- const subagentCalls = msg.content.filter((c) => c.type === "tool_call" && c.name === "subagent");
2296
+ const subagentCalls = msg.content.filter((c) => c.type === "tool_call" && (c.name === "subagent" || c.name === "spawn_agent"));
2178
2297
  if (subagentCalls.length > 0) {
2179
2298
  const agents = subagentCalls.map((c) => {
2180
2299
  const result = toolResultMap.get(c.id);
2181
2300
  return {
2182
- agentName: typeof c.args?.agent === "string" ? c.args.agent : undefined,
2301
+ agentName: c.name === "spawn_agent" && typeof c.args?.task_name === "string"
2302
+ ? c.args.task_name
2303
+ : typeof c.args?.agent === "string"
2304
+ ? c.args.agent
2305
+ : undefined,
2306
+ // Async workers are intentionally non-resumable; restored rows are historical.
2183
2307
  status: result?.isError ? "error" : "done",
2184
2308
  toolUseCount: 0,
2185
2309
  };
@@ -2211,6 +2335,10 @@ async function createSession(deps, opts) {
2211
2335
  return;
2212
2336
  }
2213
2337
  if (method === "GET" && url === "/commands") {
2338
+ if (mode === "chat") {
2339
+ json(res, 200, { commands: [] });
2340
+ return;
2341
+ }
2214
2342
  // Workflow commands with agent functionality: built-in prompt templates +
2215
2343
  // the user's own `.ezcoder/commands/*.md`. UI commands (model/quit/etc.) are
2216
2344
  // handled webview-side and intentionally excluded.
@@ -2258,7 +2386,7 @@ async function createSession(deps, opts) {
2258
2386
  // an autopilot cycle is active but between injected runs (build idle,
2259
2387
  // Nolan reviewing) so the message never starts a run that collides with
2260
2388
  // an injected one on the same session. Attachments are persisted to
2261
- // .gg/uploads first so the queued media rides the same native-block
2389
+ // .ezcoder/uploads first so the queued media rides the same native-block
2262
2390
  // path as a non-queued attachment prompt when it drains.
2263
2391
  const prepared = attachments.length > 0 ? await prepareAttachments(cwd, attachments) : [];
2264
2392
  const count = session.queueMessage(text, prepared);
@@ -2351,6 +2479,10 @@ async function createSession(deps, opts) {
2351
2479
  // webview keeps the bubbles separate. The context digest is assembled fresh
2352
2480
  // from the BUILD session's transcript each turn (one-way mirror).
2353
2481
  if (method === "POST" && url === "/ken/prompt") {
2482
+ if (mode === "chat") {
2483
+ json(res, 404, { error: "Nolan is not available in EZ Chat." });
2484
+ return;
2485
+ }
2354
2486
  void readBody(req).then(async (raw) => {
2355
2487
  let text;
2356
2488
  try {
@@ -2406,6 +2538,10 @@ async function createSession(deps, opts) {
2406
2538
  return;
2407
2539
  }
2408
2540
  if (method === "POST" && url === "/autopilot") {
2541
+ if (mode === "chat") {
2542
+ json(res, 404, { error: "Autopilot is not available in EZ Chat." });
2543
+ return;
2544
+ }
2409
2545
  void readBody(req).then(async (raw) => {
2410
2546
  let enabled;
2411
2547
  try {
@@ -2464,7 +2600,34 @@ async function createSession(deps, opts) {
2464
2600
  // duplicate audio across windows (the original per-window goal), now for
2465
2601
  // free. (To restore per-window radio, key playback by sessionId.)
2466
2602
  if (method === "GET" && url === "/radio") {
2467
- json(res, 200, { stations: RADIO_STATIONS, current: getCurrentStation() });
2603
+ json(res, 200, {
2604
+ stations: RADIO_STATIONS,
2605
+ current: getCurrentStation(),
2606
+ volume: getRadioVolume(),
2607
+ });
2608
+ return;
2609
+ }
2610
+ if (method === "POST" && url === "/radio/volume") {
2611
+ void readBody(req).then((raw) => {
2612
+ let volume;
2613
+ try {
2614
+ volume = Number(JSON.parse(raw).volume);
2615
+ }
2616
+ catch {
2617
+ json(res, 400, { error: "invalid JSON body" });
2618
+ return;
2619
+ }
2620
+ if (!Number.isFinite(volume)) {
2621
+ json(res, 400, { error: "volume must be a number" });
2622
+ return;
2623
+ }
2624
+ const result = setRadioVolume(volume);
2625
+ if (!result.ok) {
2626
+ json(res, 400, { error: result.error ?? "Radio volume failed to update." });
2627
+ return;
2628
+ }
2629
+ json(res, 200, { current: getCurrentStation(), volume: getRadioVolume() });
2630
+ });
2468
2631
  return;
2469
2632
  }
2470
2633
  if (method === "POST" && url === "/radio") {
@@ -2657,7 +2820,7 @@ async function createSession(deps, opts) {
2657
2820
  await auth.reload();
2658
2821
  await session.switchModel(target.provider, target.id);
2659
2822
  // Nolan follows EZ Coder's model only while un-pinned; a user-set Nolan
2660
- // override survives GG model switches untouched.
2823
+ // override survives EZ Coder model switches untouched.
2661
2824
  if (!nolanModelOverride) {
2662
2825
  await syncNolanModel(target.provider, target.id);
2663
2826
  await syncNolanAutoModel(target.provider, target.id);
@@ -2690,8 +2853,8 @@ async function createSession(deps, opts) {
2690
2853
  broadcast("thinking_change", payload);
2691
2854
  // Un-pinned Nolan just followed the switch — update his footer chip too.
2692
2855
  // When Nolan is pinned, his effective model did not change, so skip the
2693
- // no-op event (keeps footer/event tests from treating a GG switch as a
2694
- // Nolan switch).
2856
+ // no-op event (keeps footer/event tests from treating an EZ Coder
2857
+ // switch as a Nolan switch).
2695
2858
  if (!nolanModelOverride)
2696
2859
  broadcast("ken_model_change", nolanStatePayload());
2697
2860
  // The new model usually has a different context window — push extras so
@@ -2723,7 +2886,7 @@ async function createSession(deps, opts) {
2723
2886
  const st = session.getState();
2724
2887
  await syncNolanModel(st.provider, st.model);
2725
2888
  await syncNolanAutoModel(st.provider, st.model);
2726
- log("INFO", "app-sidecar", "ken model pin cleared — following GG", {
2889
+ log("INFO", "app-sidecar", "Nolan model pin cleared — following EZ Coder", {
2727
2890
  provider: st.provider,
2728
2891
  model: st.model,
2729
2892
  });
@@ -2821,7 +2984,10 @@ async function createSession(deps, opts) {
2821
2984
  }
2822
2985
  void session
2823
2986
  .newSession()
2824
- .then(() => {
2987
+ .then(async () => {
2988
+ if (mode === "chat") {
2989
+ await session.persistAppMarker("agent_handoff", { chatAgent });
2990
+ }
2825
2991
  injectedAutopilotPrompts = [];
2826
2992
  clearPendingPlan();
2827
2993
  broadcast("session_reset", {});
@@ -3320,6 +3486,10 @@ async function createSession(deps, opts) {
3320
3486
  }
3321
3487
  return {
3322
3488
  id: opts.id,
3489
+ mode,
3490
+ get chatAgent() {
3491
+ return chatAgent;
3492
+ },
3323
3493
  cwd,
3324
3494
  sessionPath: opts.sessionPath,
3325
3495
  session,