@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.
- package/README.md +25 -12
- package/dist/app-sidecar.js +244 -74
- package/dist/app-sidecar.js.map +1 -1
- package/dist/chat-agents/general.d.ts +7 -0
- package/dist/chat-agents/general.d.ts.map +1 -0
- package/dist/chat-agents/general.js +16 -0
- package/dist/chat-agents/general.js.map +1 -0
- package/dist/chat-agents/general.test.d.ts +2 -0
- package/dist/chat-agents/general.test.d.ts.map +1 -0
- package/dist/chat-agents/general.test.js +47 -0
- package/dist/chat-agents/general.test.js.map +1 -0
- package/dist/chat-agents/index.d.ts +11 -0
- package/dist/chat-agents/index.d.ts.map +1 -0
- package/dist/chat-agents/index.js +89 -0
- package/dist/chat-agents/index.js.map +1 -0
- package/dist/chat-agents/memory.d.ts +53 -0
- package/dist/chat-agents/memory.d.ts.map +1 -0
- package/dist/chat-agents/memory.js +362 -0
- package/dist/chat-agents/memory.js.map +1 -0
- package/dist/chat-agents/memory.test.d.ts +2 -0
- package/dist/chat-agents/memory.test.d.ts.map +1 -0
- package/dist/chat-agents/memory.test.js +155 -0
- package/dist/chat-agents/memory.test.js.map +1 -0
- package/dist/chat-agents/research.d.ts +7 -0
- package/dist/chat-agents/research.d.ts.map +1 -0
- package/dist/chat-agents/research.js +18 -0
- package/dist/chat-agents/research.js.map +1 -0
- package/dist/chat-agents/shared.d.ts +11 -0
- package/dist/chat-agents/shared.d.ts.map +1 -0
- package/dist/chat-agents/shared.js +48 -0
- package/dist/chat-agents/shared.js.map +1 -0
- package/dist/chat-agents/specialists.test.d.ts +2 -0
- package/dist/chat-agents/specialists.test.d.ts.map +1 -0
- package/dist/chat-agents/specialists.test.js +97 -0
- package/dist/chat-agents/specialists.test.js.map +1 -0
- package/dist/chat-agents/therapist.d.ts +7 -0
- package/dist/chat-agents/therapist.d.ts.map +1 -0
- package/dist/chat-agents/therapist.js +20 -0
- package/dist/chat-agents/therapist.js.map +1 -0
- package/dist/chat-agents/types.d.ts +4 -0
- package/dist/chat-agents/types.d.ts.map +1 -0
- package/dist/chat-agents/types.js +7 -0
- package/dist/chat-agents/types.js.map +1 -0
- package/dist/cli/auth.js +1 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +72 -22
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session-memory-tail.test.d.ts +2 -0
- package/dist/core/agent-session-memory-tail.test.d.ts.map +1 -0
- package/dist/core/agent-session-memory-tail.test.js +122 -0
- package/dist/core/agent-session-memory-tail.test.js.map +1 -0
- package/dist/core/agent-session-subagent-policy.test.d.ts +2 -0
- package/dist/core/agent-session-subagent-policy.test.d.ts.map +1 -0
- package/dist/core/agent-session-subagent-policy.test.js +36 -0
- package/dist/core/agent-session-subagent-policy.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +37 -8
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +168 -109
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agents.d.ts +1 -1
- package/dist/core/agents.d.ts.map +1 -1
- package/dist/core/agents.js +4 -2
- package/dist/core/agents.js.map +1 -1
- package/dist/core/auth-providers.js +5 -5
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts +7 -0
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +73 -9
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +31 -4
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/event-bus.d.ts +2 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/mcp/defaults.d.ts +1 -1
- package/dist/core/mcp/defaults.js +1 -1
- package/dist/core/mcp/store.d.ts +2 -2
- package/dist/core/mcp/store.test.js +1 -1
- package/dist/core/mcp/store.test.js.map +1 -1
- package/dist/core/progress/rebuild.d.ts +3 -3
- package/dist/core/progress/rebuild.d.ts.map +1 -1
- package/dist/core/progress/rebuild.js +33 -27
- package/dist/core/progress/rebuild.js.map +1 -1
- package/dist/core/progress/rebuild.test.js +15 -0
- package/dist/core/progress/rebuild.test.js.map +1 -1
- package/dist/core/project-discovery.d.ts +1 -1
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +5 -6
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.js +14 -1
- package/dist/core/project-discovery.test.js.map +1 -1
- package/dist/core/radio.d.ts +3 -0
- package/dist/core/radio.d.ts.map +1 -1
- package/dist/core/radio.js +150 -21
- package/dist/core/radio.js.map +1 -1
- package/dist/core/radio.test.d.ts +2 -0
- package/dist/core/radio.test.d.ts.map +1 -0
- package/dist/core/radio.test.js +101 -0
- package/dist/core/radio.test.js.map +1 -0
- package/dist/core/session-history.test.js +1 -1
- package/dist/core/session-history.test.js.map +1 -1
- package/dist/core/session-manager.d.ts +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +2 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +18 -0
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +73 -0
- package/dist/core/subagent-manager.d.ts.map +1 -0
- package/dist/core/subagent-manager.js +412 -0
- package/dist/core/subagent-manager.js.map +1 -0
- package/dist/core/subagent-manager.test.d.ts +2 -0
- package/dist/core/subagent-manager.test.d.ts.map +1 -0
- package/dist/core/subagent-manager.test.js +103 -0
- package/dist/core/subagent-manager.test.js.map +1 -0
- package/dist/core/subagent-policy.d.ts +4 -0
- package/dist/core/subagent-policy.d.ts.map +1 -0
- package/dist/core/subagent-policy.js +19 -0
- package/dist/core/subagent-policy.js.map +1 -0
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +3 -4
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts +14 -0
- package/dist/modes/subagent-worker-mode.d.ts.map +1 -0
- package/dist/modes/subagent-worker-mode.js +191 -0
- package/dist/modes/subagent-worker-mode.js.map +1 -0
- package/dist/tools/generate-image.d.ts.map +1 -1
- package/dist/tools/generate-image.js +12 -5
- package/dist/tools/generate-image.js.map +1 -1
- package/dist/tools/generate-image.test.js +17 -0
- package/dist/tools/generate-image.test.js.map +1 -1
- package/dist/tools/goals.d.ts +3 -3
- package/dist/tools/index.d.ts +10 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +17 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/subagent-control.d.ts +6 -0
- package/dist/tools/subagent-control.d.ts.map +1 -0
- package/dist/tools/subagent-control.js +95 -0
- package/dist/tools/subagent-control.js.map +1 -0
- package/dist/tools/subagent-control.test.d.ts +2 -0
- package/dist/tools/subagent-control.test.d.ts.map +1 -0
- package/dist/tools/subagent-control.test.js +40 -0
- package/dist/tools/subagent-control.test.js.map +1 -0
- package/dist/tools/subagent-shared.d.ts +24 -0
- package/dist/tools/subagent-shared.d.ts.map +1 -0
- package/dist/tools/subagent-shared.js +53 -0
- package/dist/tools/subagent-shared.js.map +1 -0
- package/dist/tools/subagent-shared.test.d.ts +2 -0
- package/dist/tools/subagent-shared.test.d.ts.map +1 -0
- package/dist/tools/subagent-shared.test.js +29 -0
- package/dist/tools/subagent-shared.test.js.map +1 -0
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +21 -64
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/subagent.test.js +27 -1
- package/dist/tools/subagent.test.js.map +1 -1
- package/dist/tools/web-fetch.d.ts +1 -0
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +112 -35
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-fetch.test.js +146 -1
- package/dist/tools/web-fetch.test.js.map +1 -1
- package/dist/tools/web-search.d.ts +10 -0
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +236 -28
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools/web-search.test.js +77 -6
- package/dist/tools/web-search.test.js.map +1 -1
- package/dist/ui/App.d.ts +2 -0
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +61 -2
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/hooks/useModeState.d.ts +4 -2
- package/dist/ui/hooks/useModeState.d.ts.map +1 -1
- package/dist/ui/hooks/useModeState.js +14 -3
- package/dist/ui/hooks/useModeState.js.map +1 -1
- package/dist/ui/login.d.ts.map +1 -1
- package/dist/ui/login.js +13 -5
- package/dist/ui/login.js.map +1 -1
- package/dist/ui/render.d.ts +14 -0
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +2 -0
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/render.test.js +17 -0
- package/dist/ui/render.test.js.map +1 -1
- 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
|
|
155
|
-
|
|
156
|
-
| `bash`
|
|
157
|
-
| `read`
|
|
158
|
-
| `write`
|
|
159
|
-
| `edit`
|
|
160
|
-
| `grep`
|
|
161
|
-
| `find`
|
|
162
|
-
| `ls`
|
|
163
|
-
| `web_fetch`
|
|
164
|
-
| `screenshot`
|
|
165
|
-
| `subagent`
|
|
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
|
package/dist/app-sidecar.js
CHANGED
|
@@ -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", {
|
|
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
|
|
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 →
|
|
749
|
-
|
|
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 ~/.
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
-
|
|
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
|
|
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", "
|
|
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
|
-
|
|
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:
|
|
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
|
-
// .
|
|
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, {
|
|
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
|
|
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
|
|
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", "
|
|
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,
|