@prestyj/cli 5.1.1 → 5.2.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 +2 -2
- package/dist/app-sidecar.js +259 -26
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +19 -2
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +80 -27
- package/dist/cli.js.map +1 -1
- package/dist/config.js +32 -30
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-compaction.test.js +1 -1
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session.d.ts +17 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +61 -12
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tools-allowlist.test.d.ts +2 -0
- package/dist/core/agent-tools-allowlist.test.d.ts.map +1 -0
- package/dist/core/agent-tools-allowlist.test.js +72 -0
- package/dist/core/agent-tools-allowlist.test.js.map +1 -0
- package/dist/core/auth-providers.d.ts +21 -0
- package/dist/core/auth-providers.d.ts.map +1 -1
- package/dist/core/auth-providers.js +15 -2
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/autopilot-verdict.d.ts +32 -0
- package/dist/core/autopilot-verdict.d.ts.map +1 -0
- package/dist/core/autopilot-verdict.js +93 -0
- package/dist/core/autopilot-verdict.js.map +1 -0
- package/dist/core/autopilot-verdict.test.d.ts +2 -0
- package/dist/core/autopilot-verdict.test.d.ts.map +1 -0
- package/dist/core/autopilot-verdict.test.js +80 -0
- package/dist/core/autopilot-verdict.test.js.map +1 -0
- package/dist/core/compaction/compactor.test.js +2 -2
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/token-estimator.test.js +3 -3
- package/dist/core/compaction/token-estimator.test.js.map +1 -1
- package/dist/core/event-bus.d.ts +4 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +6 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/nolan-context.d.ts +18 -0
- package/dist/core/nolan-context.d.ts.map +1 -1
- package/dist/core/nolan-context.js +19 -0
- package/dist/core/nolan-context.js.map +1 -1
- package/dist/core/nolan-context.test.js +23 -1
- package/dist/core/nolan-context.test.js.map +1 -1
- package/dist/core/nolan-prompt.d.ts +10 -0
- package/dist/core/nolan-prompt.d.ts.map +1 -1
- package/dist/core/nolan-prompt.js +48 -0
- package/dist/core/nolan-prompt.js.map +1 -1
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +17 -3
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +27 -1
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/session-manager.test.js +3 -1
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/task-store.d.ts +7 -0
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js +13 -0
- package/dist/core/task-store.js.map +1 -1
- package/dist/modes/json-mode.d.ts +7 -0
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +4 -0
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/system-prompt.test.js +4 -4
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/plan-mode.test.js +1 -1
- package/dist/tools/plan-mode.test.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +4 -0
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +24 -2
- package/dist/tools/subagent.js.map +1 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +21 -5
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +1 -0
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/Footer.js +1 -1
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useContextCompaction.d.ts.map +1 -1
- package/dist/ui/hooks/useContextCompaction.js +4 -2
- package/dist/ui/hooks/useContextCompaction.js.map +1 -1
- package/dist/ui/login.d.ts.map +1 -1
- package/dist/ui/login.js +6 -2
- package/dist/ui/login.js.map +1 -1
- package/dist/ui/thinking-level-cycle.test.js +1 -1
- package/dist/ui/thinking-level-cycle.test.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -69,12 +69,12 @@ Switch mid-conversation with `/model`. Not locked to anyone.
|
|
|
69
69
|
|
|
70
70
|
| Provider | Models | Auth |
|
|
71
71
|
|---|---|---|
|
|
72
|
-
| **Anthropic** | Claude
|
|
72
|
+
| **Anthropic** | Claude Opus 4.8, Sonnet 5, Haiku 4.5 | OAuth |
|
|
73
73
|
| **OpenAI** | GPT-5.5, GPT-5.5 Pro, GPT-5.4, GPT-5.3 Codex | OAuth |
|
|
74
74
|
| **Moonshot** | Kimi K2.6 | API key |
|
|
75
75
|
| **Z.AI (GLM)** | GLM-5.1, GLM-4.7, GLM-4.7 Flash | API key |
|
|
76
76
|
| **MiniMax** | MiniMax M3 (image + video) | API key |
|
|
77
|
-
| **Xiaomi (MiMo)** | MiMo-V2.5-Pro, MiMo-V2.5 (image + video) | API key |
|
|
77
|
+
| **Xiaomi (MiMo)** | MiMo-V2.5-Pro, MiMo-V2.5-Pro-UltraSpeed, MiMo-V2.5 (image + video) | API key |
|
|
78
78
|
| **DeepSeek** | DeepSeek V4 Pro, V4 Flash | API key |
|
|
79
79
|
| **OpenRouter** | Qwen3.6-Plus + multi-provider gateway | API key |
|
|
80
80
|
|
package/dist/app-sidecar.js
CHANGED
|
@@ -17,13 +17,15 @@ import os from "node:os";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
19
|
import { parseArgs } from "node:util";
|
|
20
|
+
import { formatError } from "@prestyj/ai";
|
|
20
21
|
import { runJsonMode } from "./modes/json-mode.js";
|
|
21
22
|
import { AgentSession } from "./core/agent-session.js";
|
|
22
|
-
import { buildNolanSystemPrompt } from "./core/nolan-prompt.js";
|
|
23
|
-
import { buildNolanDigest } from "./core/nolan-context.js";
|
|
23
|
+
import { buildNolanSystemPrompt, buildNolanAutopilotSystemPrompt } from "./core/nolan-prompt.js";
|
|
24
|
+
import { buildNolanDigest, buildNolanAutopilotContext } from "./core/nolan-context.js";
|
|
25
|
+
import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
|
|
24
26
|
import { collectProjectContext } from "./system-prompt.js";
|
|
25
27
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
26
|
-
import { MOONSHOT_OAUTH_KEY } from "@prestyj/core";
|
|
28
|
+
import { MOONSHOT_OAUTH_KEY, XIAOMI_CREDITS_KEY } from "@prestyj/core";
|
|
27
29
|
import { loginAnthropic } from "./core/oauth/anthropic.js";
|
|
28
30
|
import { loginOpenAI } from "./core/oauth/openai.js";
|
|
29
31
|
import { loginGemini } from "./core/oauth/gemini.js";
|
|
@@ -38,7 +40,7 @@ import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSuppor
|
|
|
38
40
|
import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
|
|
39
41
|
import { loadCustomCommands } from "./core/custom-commands.js";
|
|
40
42
|
import { discoverProjects, listRecentSessions } from "./core/project-discovery.js";
|
|
41
|
-
import { loadTasksSync, saveTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
|
|
43
|
+
import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
|
|
42
44
|
import { initLogger, log } from "./core/logger.js";
|
|
43
45
|
import { RADIO_STATIONS, getCurrentStation, playRadio, stopRadio } from "./core/radio.js";
|
|
44
46
|
import { enrichProcessPath } from "./core/shell-path.js";
|
|
@@ -79,6 +81,7 @@ async function loadAppSettings() {
|
|
|
79
81
|
// Preserve the per-project map verbatim (validated + written by the
|
|
80
82
|
// model/thinking handlers below).
|
|
81
83
|
projectModels: raw.projectModels && typeof raw.projectModels === "object" ? raw.projectModels : undefined,
|
|
84
|
+
autopilot: raw.autopilot && typeof raw.autopilot === "object" ? raw.autopilot : undefined,
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
catch {
|
|
@@ -102,6 +105,19 @@ async function saveProjectModelPrefs(cwd, prefs) {
|
|
|
102
105
|
s.projectModels = { ...(s.projectModels ?? {}), [key]: prefs };
|
|
103
106
|
await saveAppSettings(s);
|
|
104
107
|
}
|
|
108
|
+
/** Read this project's persisted autopilot flag (default off). */
|
|
109
|
+
async function loadAutopilot(cwd) {
|
|
110
|
+
const s = await loadAppSettings();
|
|
111
|
+
return s.autopilot?.[projectModelKey(cwd)] ?? false;
|
|
112
|
+
}
|
|
113
|
+
/** Persist this project's autopilot flag via read-modify-write so the rest of
|
|
114
|
+
* the settings file (projectsRoot, model map, other projects) is preserved. */
|
|
115
|
+
async function saveAutopilot(cwd, enabled) {
|
|
116
|
+
const s = await loadAppSettings();
|
|
117
|
+
const key = projectModelKey(cwd);
|
|
118
|
+
s.autopilot = { ...(s.autopilot ?? {}), [key]: enabled };
|
|
119
|
+
await saveAppSettings(s);
|
|
120
|
+
}
|
|
105
121
|
/**
|
|
106
122
|
* Persist the active model selection to ~/.ezcoder/settings.json so it survives app
|
|
107
123
|
* restarts. Mirrors the CLI's handleModelSelect persistence (App.tsx).
|
|
@@ -632,6 +648,31 @@ async function createSession(deps, opts) {
|
|
|
632
648
|
for (const c of clients)
|
|
633
649
|
c.res.write(frame);
|
|
634
650
|
}
|
|
651
|
+
// Turn any thrown value into the same clear headline/message/guidance shape
|
|
652
|
+
// the TUI shows (see gg-ai's formatError) instead of a bare `err.message`, log
|
|
653
|
+
// the full detail, and broadcast it under `type` ("error" or "nolan_error").
|
|
654
|
+
// Without this the webview only ever saw a raw provider string like
|
|
655
|
+
// `400 {"code":"400",...}` with no "is this me or them / when does it reset"
|
|
656
|
+
// context that the CLI has always given.
|
|
657
|
+
function broadcastError(type, logLabel, err) {
|
|
658
|
+
const f = formatError(err);
|
|
659
|
+
log("ERROR", "app-sidecar", logLabel, {
|
|
660
|
+
headline: f.headline,
|
|
661
|
+
source: f.source,
|
|
662
|
+
...(f.message ? { message: f.message } : {}),
|
|
663
|
+
...(f.provider ? { provider: f.provider } : {}),
|
|
664
|
+
...(f.statusCode != null ? { statusCode: String(f.statusCode) } : {}),
|
|
665
|
+
...(f.requestId ? { requestId: f.requestId } : {}),
|
|
666
|
+
});
|
|
667
|
+
broadcast(type, {
|
|
668
|
+
headline: f.headline,
|
|
669
|
+
...(f.message ? { message: f.message } : {}),
|
|
670
|
+
guidance: f.guidance,
|
|
671
|
+
...(f.provider ? { provider: f.provider } : {}),
|
|
672
|
+
...(f.statusCode != null ? { statusCode: f.statusCode } : {}),
|
|
673
|
+
...(f.resetsAt != null ? { resetsAt: f.resetsAt } : {}),
|
|
674
|
+
});
|
|
675
|
+
}
|
|
635
676
|
// The session file path to resume (passed by the daemon's POST /session);
|
|
636
677
|
// empty/unset starts a fresh session.
|
|
637
678
|
const resumeSessionPath = opts.sessionPath;
|
|
@@ -680,7 +721,7 @@ async function createSession(deps, opts) {
|
|
|
680
721
|
let gitIsRepo = await isGitRepo(cwd).catch(() => false);
|
|
681
722
|
function currentContextWindow() {
|
|
682
723
|
const st = session.getState();
|
|
683
|
-
return getContextWindow(st.model, { provider: st.provider });
|
|
724
|
+
return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
|
|
684
725
|
}
|
|
685
726
|
// Shared shape merged into /state + the SSE `ready` frame so the footer can
|
|
686
727
|
// render context %, branch, and tasks immediately on connect.
|
|
@@ -731,9 +772,7 @@ async function createSession(deps, opts) {
|
|
|
731
772
|
session.eventBus.on("turn_end", (d) => broadcast("turn_end", d));
|
|
732
773
|
session.eventBus.on("agent_done", (d) => broadcast("agent_done", d));
|
|
733
774
|
session.eventBus.on("error", (d) => {
|
|
734
|
-
|
|
735
|
-
log("ERROR", "app-sidecar", "agent error", { message });
|
|
736
|
-
broadcast("error", { message });
|
|
775
|
+
broadcastError("error", "agent error", d.error);
|
|
737
776
|
});
|
|
738
777
|
session.eventBus.on("model_change", (d) => broadcast("model_change", d));
|
|
739
778
|
session.eventBus.on("hook", (d) => broadcast("hook", d));
|
|
@@ -741,6 +780,24 @@ async function createSession(deps, opts) {
|
|
|
741
780
|
session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
|
|
742
781
|
let running = false;
|
|
743
782
|
let titleGenerated = false;
|
|
783
|
+
// Autopilot (auto-review) toggle for THIS window's project. Loaded from
|
|
784
|
+
// ezcoder-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
|
|
785
|
+
// runAutopilotCycle after the user's turn settles — Nolan auto-reviews the work
|
|
786
|
+
// and drives the review→prompt→review loop.
|
|
787
|
+
let autopilot = await loadAutopilot(cwd);
|
|
788
|
+
// True while an autopilot review is in flight (used to defer nolanAuto model
|
|
789
|
+
// switches, like nolanRunning does for chat Nolan, and to drive the spinner).
|
|
790
|
+
let autopilotReviewing = false;
|
|
791
|
+
// True for the WHOLE autopilot cycle (reviews + injected runs). The build
|
|
792
|
+
// `running` flag is false during the review windows between injected runs, so
|
|
793
|
+
// this is the extra guard that makes a user /prompt queue as steering instead
|
|
794
|
+
// of starting a run that would collide with an injected one on the same
|
|
795
|
+
// session (AgentSession.prompt has no concurrency guard).
|
|
796
|
+
let autopilotActive = false;
|
|
797
|
+
// Set by /cancel to break out of an in-flight autopilot cycle between steps.
|
|
798
|
+
let autopilotCancelled = false;
|
|
799
|
+
// Hard cap on review→prompt→review rounds per user turn (loop safety).
|
|
800
|
+
const MAX_AUTOPILOT_ROUNDS = 3;
|
|
744
801
|
// ── Telegram serve (remote control via Telegram) ───────────
|
|
745
802
|
// A single embedded serve session lives in this sidecar process. Only the main
|
|
746
803
|
// window's home screen exposes the controls, so there's one bot per app.
|
|
@@ -803,14 +860,59 @@ async function createSession(deps, opts) {
|
|
|
803
860
|
ken.eventBus.on("server_tool_call", (d) => broadcast("nolan_server_tool_call", d));
|
|
804
861
|
ken.eventBus.on("turn_end", (d) => broadcast("nolan_turn_end", d));
|
|
805
862
|
ken.eventBus.on("error", (d) => {
|
|
806
|
-
|
|
807
|
-
log("ERROR", "app-sidecar", "ken error", { message });
|
|
808
|
-
broadcast("nolan_error", { message });
|
|
863
|
+
broadcastError("nolan_error", "ken error", d.error);
|
|
809
864
|
});
|
|
810
865
|
nolanSession = ken;
|
|
811
866
|
log("INFO", "app-sidecar", "ken session ready", { provider: st.provider, model: st.model });
|
|
812
867
|
return ken;
|
|
813
868
|
}
|
|
869
|
+
// ── Autopilot Nolan (auto-reviewer) ──────────────────────────
|
|
870
|
+
// A THIRD read-only AgentSession, separate from chat Nolan. In autopilot mode
|
|
871
|
+
// Nolan silently reviews each finished EZ Coder turn and returns a verdict
|
|
872
|
+
// (PROMPT / ALL_CLEAR / HUMAN). Its bus is intentionally NOT bridged to the
|
|
873
|
+
// nolan_* chat bubbles — the review is silent; we read its final assistant text
|
|
874
|
+
// and parse it. Uses the lean autopilot system prompt + the same read-only
|
|
875
|
+
// tools. Created lazily on the first autopilot cycle.
|
|
876
|
+
let nolanAutoSession = null;
|
|
877
|
+
let nolanAutoAbort = new AbortController();
|
|
878
|
+
let pendingNolanAutoModel = null;
|
|
879
|
+
async function syncNolanAutoModel(provider, model) {
|
|
880
|
+
if (autopilotReviewing) {
|
|
881
|
+
pendingNolanAutoModel = { provider, model };
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (!nolanAutoSession)
|
|
885
|
+
return;
|
|
886
|
+
const st = nolanAutoSession.getState();
|
|
887
|
+
if (st.provider === provider && st.model === model)
|
|
888
|
+
return;
|
|
889
|
+
await nolanAutoSession.switchModel(provider, model);
|
|
890
|
+
log("INFO", "app-sidecar", "ken autopilot session model synced", { provider, model });
|
|
891
|
+
}
|
|
892
|
+
async function ensureNolanAutoSession() {
|
|
893
|
+
if (nolanAutoSession)
|
|
894
|
+
return nolanAutoSession;
|
|
895
|
+
const st = session.getState();
|
|
896
|
+
const ken = new AgentSession({
|
|
897
|
+
provider: st.provider,
|
|
898
|
+
model: st.model,
|
|
899
|
+
cwd,
|
|
900
|
+
systemPrompt: buildNolanAutopilotSystemPrompt(),
|
|
901
|
+
allowedTools: NOLAN_ALLOWED_TOOLS,
|
|
902
|
+
allowedMcpServers: NOLAN_ALLOWED_MCP_SERVERS,
|
|
903
|
+
transient: true,
|
|
904
|
+
signal: nolanAutoAbort.signal,
|
|
905
|
+
});
|
|
906
|
+
await ken.initialize();
|
|
907
|
+
// Deliberately no bus bridge: the review is silent. Errors surface via the
|
|
908
|
+
// runAutopilotReview try/catch as autopilot_error frames.
|
|
909
|
+
nolanAutoSession = ken;
|
|
910
|
+
log("INFO", "app-sidecar", "ken autopilot session ready", {
|
|
911
|
+
provider: st.provider,
|
|
912
|
+
model: st.model,
|
|
913
|
+
});
|
|
914
|
+
return ken;
|
|
915
|
+
}
|
|
814
916
|
// Resumed session: if it already has a conversation, generate its title now so
|
|
815
917
|
// the title bar shows it immediately on load (not just after the next prompt).
|
|
816
918
|
{
|
|
@@ -836,9 +938,7 @@ async function createSession(deps, opts) {
|
|
|
836
938
|
await run();
|
|
837
939
|
}
|
|
838
940
|
catch (err) {
|
|
839
|
-
|
|
840
|
-
broadcast("error", { message });
|
|
841
|
-
log("ERROR", "app-sidecar", "run failed", { message });
|
|
941
|
+
broadcastError("error", "run failed", err);
|
|
842
942
|
}
|
|
843
943
|
finally {
|
|
844
944
|
running = false;
|
|
@@ -847,6 +947,14 @@ async function createSession(deps, opts) {
|
|
|
847
947
|
gitBranch = await getGitBranch(cwd).catch(() => gitBranch);
|
|
848
948
|
gitIsRepo = await isGitRepo(cwd).catch(() => gitIsRepo);
|
|
849
949
|
broadcast("run_end", {});
|
|
950
|
+
// Autopilot's review loop is driven explicitly from POST /prompt (see
|
|
951
|
+
// runAutopilotCycle), NOT from this shared finally — that keeps the
|
|
952
|
+
// injected EZ Coder runs this cycle triggers from recursively re-entering
|
|
953
|
+
// the loop through the same bracket.
|
|
954
|
+
// The agent may have marked project tasks done during the run — prune the
|
|
955
|
+
// completed ones so they drop out of the Tasks modal automatically (users
|
|
956
|
+
// never have to delete finished tasks by hand).
|
|
957
|
+
broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
|
|
850
958
|
// Queue drains into the run as steering, so it's empty by run_end —
|
|
851
959
|
// sync the webview indicator.
|
|
852
960
|
broadcast("queued", { count: session.getQueuedCount() });
|
|
@@ -862,6 +970,79 @@ async function createSession(deps, opts) {
|
|
|
862
970
|
}
|
|
863
971
|
}
|
|
864
972
|
}
|
|
973
|
+
// ── Autopilot orchestration ─────────────────────────────────
|
|
974
|
+
// One review = prompt the nolanAuto session with the review digest, read its
|
|
975
|
+
// final assistant text, parse a verdict. Returns null on failure (surfaced as
|
|
976
|
+
// an autopilot_error frame) so the cycle stops rather than looping blind.
|
|
977
|
+
async function runAutopilotReview() {
|
|
978
|
+
autopilotReviewing = true;
|
|
979
|
+
broadcast("autopilot_review_start", {});
|
|
980
|
+
try {
|
|
981
|
+
const ken = await ensureNolanAutoSession();
|
|
982
|
+
const projectContext = await collectProjectContext(cwd).catch(() => []);
|
|
983
|
+
const digest = buildNolanAutopilotContext({
|
|
984
|
+
projectContext,
|
|
985
|
+
cwd,
|
|
986
|
+
gitBranch,
|
|
987
|
+
messages: session.getMessages(),
|
|
988
|
+
});
|
|
989
|
+
await ken.prompt(digest);
|
|
990
|
+
return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
broadcastError("autopilot_error", "autopilot review failed", err);
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
finally {
|
|
997
|
+
autopilotReviewing = false;
|
|
998
|
+
// Apply any model switch that landed mid-review.
|
|
999
|
+
const pending = pendingNolanAutoModel;
|
|
1000
|
+
pendingNolanAutoModel = null;
|
|
1001
|
+
if (pending)
|
|
1002
|
+
await syncNolanAutoModel(pending.provider, pending.model);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// Drive the review→prompt→review loop for one finished user turn. Only ever
|
|
1006
|
+
// called from POST /prompt after the user's own run resolves — never from the
|
|
1007
|
+
// task runner, resume, /ken, or error paths, so there's no recursion and no
|
|
1008
|
+
// guard tangle. Bounded by MAX_AUTOPILOT_ROUNDS and cancellable between steps.
|
|
1009
|
+
async function runAutopilotCycle() {
|
|
1010
|
+
if (!autopilot || autopilotCancelled)
|
|
1011
|
+
return;
|
|
1012
|
+
autopilotActive = true;
|
|
1013
|
+
try {
|
|
1014
|
+
// Lean context per user turn: wipe prior review history so each new turn
|
|
1015
|
+
// starts cheap, while within this cycle the few review messages persist so
|
|
1016
|
+
// Nolan remembers what he already asked EZ Coder to fix.
|
|
1017
|
+
await nolanAutoSession?.newSession().catch(() => { });
|
|
1018
|
+
for (let round = 1; round <= MAX_AUTOPILOT_ROUNDS; round++) {
|
|
1019
|
+
if (autopilotCancelled)
|
|
1020
|
+
return;
|
|
1021
|
+
const verdict = await runAutopilotReview();
|
|
1022
|
+
if (!verdict || autopilotCancelled)
|
|
1023
|
+
return;
|
|
1024
|
+
if (verdict.kind === "all_clear") {
|
|
1025
|
+
broadcast("autopilot_done", {});
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (verdict.kind === "human") {
|
|
1029
|
+
broadcast("autopilot_human", { reason: verdict.reason });
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
// prompt → show a compact Nolan-tinted marker (not the prompt body), then
|
|
1033
|
+
// feed EZ Coder. Bracketed by runAgent so the run streams normally; the
|
|
1034
|
+
// shared finally no longer re-triggers autopilot, so this can't recurse.
|
|
1035
|
+
broadcast("autopilot_prompted", { round, body: verdict.body });
|
|
1036
|
+
await runAgent(verdict.body, () => session.prompt(verdict.body));
|
|
1037
|
+
if (autopilotCancelled)
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
broadcast("autopilot_capped", { rounds: MAX_AUTOPILOT_ROUNDS });
|
|
1041
|
+
}
|
|
1042
|
+
finally {
|
|
1043
|
+
autopilotActive = false;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
865
1046
|
// ── Task runner (project task list → sessions) ──────────────
|
|
866
1047
|
// Mirrors the CLI's task flow: each task runs in its OWN fresh session, with a
|
|
867
1048
|
// completion hint instructing the agent to mark the task done via the tasks
|
|
@@ -883,8 +1064,8 @@ async function createSession(deps, opts) {
|
|
|
883
1064
|
`tasks({ action: "done", id: "${shortId}" })`;
|
|
884
1065
|
await runAgent(task.title, () => session.prompt(task.prompt + completionHint));
|
|
885
1066
|
// The agent typically marks the task done via the tasks tool during the run;
|
|
886
|
-
// push the refreshed list so the
|
|
887
|
-
broadcast("tasks_list", { tasks:
|
|
1067
|
+
// prune completed tasks and push the refreshed list so the modal drops them.
|
|
1068
|
+
broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
|
|
888
1069
|
return true;
|
|
889
1070
|
}
|
|
890
1071
|
async function runTasks(startId, all) {
|
|
@@ -982,6 +1163,7 @@ async function createSession(deps, opts) {
|
|
|
982
1163
|
thinkingLevel: session.getThinkingLevel() ?? null,
|
|
983
1164
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
984
1165
|
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
1166
|
+
autopilot,
|
|
985
1167
|
...footerExtras(),
|
|
986
1168
|
});
|
|
987
1169
|
return;
|
|
@@ -1005,6 +1187,7 @@ async function createSession(deps, opts) {
|
|
|
1005
1187
|
thinkingLevel: session.getThinkingLevel() ?? null,
|
|
1006
1188
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1007
1189
|
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
1190
|
+
autopilot,
|
|
1008
1191
|
...footerExtras(),
|
|
1009
1192
|
},
|
|
1010
1193
|
})}\n\n`);
|
|
@@ -1328,10 +1511,13 @@ async function createSession(deps, opts) {
|
|
|
1328
1511
|
json(res, 400, { error: "empty prompt" });
|
|
1329
1512
|
return;
|
|
1330
1513
|
}
|
|
1331
|
-
if (running) {
|
|
1332
|
-
// Queue prompts as mid-run steering (mirrors the CLI).
|
|
1333
|
-
//
|
|
1334
|
-
//
|
|
1514
|
+
if (running || autopilotActive) {
|
|
1515
|
+
// Queue prompts as mid-run steering (mirrors the CLI). Also queue while
|
|
1516
|
+
// an autopilot cycle is active but between injected runs (build idle,
|
|
1517
|
+
// Nolan reviewing) so the message never starts a run that collides with
|
|
1518
|
+
// an injected one on the same session. Attachments are persisted to
|
|
1519
|
+
// .gg/uploads first so the queued media rides the same native-block
|
|
1520
|
+
// path as a non-queued attachment prompt when it drains.
|
|
1335
1521
|
const prepared = attachments.length > 0 ? await prepareAttachments(cwd, attachments) : [];
|
|
1336
1522
|
const count = session.queueMessage(text, prepared);
|
|
1337
1523
|
broadcast("queued", { count });
|
|
@@ -1339,6 +1525,9 @@ async function createSession(deps, opts) {
|
|
|
1339
1525
|
return;
|
|
1340
1526
|
}
|
|
1341
1527
|
json(res, 202, { accepted: true });
|
|
1528
|
+
// Fresh user turn: clear any cancel flag left from a prior cycle so this
|
|
1529
|
+
// turn's autopilot review can run.
|
|
1530
|
+
autopilotCancelled = false;
|
|
1342
1531
|
await runAgent(text, async () => {
|
|
1343
1532
|
if (attachments.length > 0) {
|
|
1344
1533
|
// Persist each attachment under .ezcoder/uploads so files are inspectable
|
|
@@ -1354,6 +1543,11 @@ async function createSession(deps, opts) {
|
|
|
1354
1543
|
await session.prompt(text);
|
|
1355
1544
|
}
|
|
1356
1545
|
});
|
|
1546
|
+
// After the user's run settles, kick off Nolan's auto-review loop. This is
|
|
1547
|
+
// the ONLY entry point into the cycle — it drives any follow-up EZ Coder
|
|
1548
|
+
// runs itself, so the shared runAgent finally never recurses.
|
|
1549
|
+
if (autopilot && !autopilotCancelled)
|
|
1550
|
+
await runAutopilotCycle();
|
|
1357
1551
|
});
|
|
1358
1552
|
return;
|
|
1359
1553
|
}
|
|
@@ -1394,9 +1588,7 @@ async function createSession(deps, opts) {
|
|
|
1394
1588
|
await session.persistNolanTurn(text, reply);
|
|
1395
1589
|
}
|
|
1396
1590
|
catch (err) {
|
|
1397
|
-
|
|
1398
|
-
log("ERROR", "app-sidecar", "ken run failed", { message });
|
|
1399
|
-
broadcast("nolan_error", { message });
|
|
1591
|
+
broadcastError("nolan_error", "ken run failed", err);
|
|
1400
1592
|
}
|
|
1401
1593
|
finally {
|
|
1402
1594
|
nolanRunning = false;
|
|
@@ -1418,6 +1610,24 @@ async function createSession(deps, opts) {
|
|
|
1418
1610
|
json(res, 200, { cancelled: true });
|
|
1419
1611
|
return;
|
|
1420
1612
|
}
|
|
1613
|
+
if (method === "POST" && url === "/autopilot") {
|
|
1614
|
+
void readBody(req).then(async (raw) => {
|
|
1615
|
+
let enabled;
|
|
1616
|
+
try {
|
|
1617
|
+
enabled = Boolean(JSON.parse(raw).enabled);
|
|
1618
|
+
}
|
|
1619
|
+
catch {
|
|
1620
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
autopilot = enabled;
|
|
1624
|
+
await saveAutopilot(cwd, enabled);
|
|
1625
|
+
log("INFO", "app-sidecar", "autopilot toggled", { enabled: String(enabled) });
|
|
1626
|
+
broadcast("autopilot", { autopilot: enabled });
|
|
1627
|
+
json(res, 200, { autopilot: enabled });
|
|
1628
|
+
});
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1421
1631
|
if (method === "POST" && url === "/enhance") {
|
|
1422
1632
|
void readBody(req).then(async (raw) => {
|
|
1423
1633
|
let text;
|
|
@@ -1447,7 +1657,7 @@ async function createSession(deps, opts) {
|
|
|
1447
1657
|
return;
|
|
1448
1658
|
}
|
|
1449
1659
|
if (method === "GET" && url === "/tasks") {
|
|
1450
|
-
json(res, 200, { tasks:
|
|
1660
|
+
json(res, 200, { tasks: pruneDoneTasksSync(cwd) });
|
|
1451
1661
|
return;
|
|
1452
1662
|
}
|
|
1453
1663
|
// ── Radio (app-wide) ──────────────────────────────────────
|
|
@@ -1652,6 +1862,7 @@ async function createSession(deps, opts) {
|
|
|
1652
1862
|
await auth.reload();
|
|
1653
1863
|
await session.switchModel(target.provider, target.id);
|
|
1654
1864
|
await syncNolanModel(target.provider, target.id);
|
|
1865
|
+
await syncNolanAutoModel(target.provider, target.id);
|
|
1655
1866
|
// Clamp the reasoning level to what the new model supports (mirrors the
|
|
1656
1867
|
// CLI): keep thinking on at the first supported tier if it was on but
|
|
1657
1868
|
// the prior level is unsupported here; leave it off if it was off.
|
|
@@ -1733,6 +1944,13 @@ async function createSession(deps, opts) {
|
|
|
1733
1944
|
running = false;
|
|
1734
1945
|
// Stop a run-all sweep so the next pending task isn't auto-started.
|
|
1735
1946
|
taskRunAll = false;
|
|
1947
|
+
// Stop any in-flight autopilot cycle: flag it so the loop bails between
|
|
1948
|
+
// steps, and abort a review that's mid-prompt on the nolanAuto session.
|
|
1949
|
+
autopilotCancelled = true;
|
|
1950
|
+
nolanAutoAbort.abort();
|
|
1951
|
+
nolanAutoAbort = new AbortController();
|
|
1952
|
+
nolanAutoSession?.setSignal(nolanAutoAbort.signal);
|
|
1953
|
+
autopilotReviewing = false;
|
|
1736
1954
|
// Drop any queued steering and return it so the webview can restore it to
|
|
1737
1955
|
// the composer.
|
|
1738
1956
|
const drained = session.drainQueue();
|
|
@@ -1802,10 +2020,12 @@ async function createSession(deps, opts) {
|
|
|
1802
2020
|
void readBody(req).then(async (raw) => {
|
|
1803
2021
|
let provider = "";
|
|
1804
2022
|
let key;
|
|
2023
|
+
let variant;
|
|
1805
2024
|
try {
|
|
1806
2025
|
const body = JSON.parse(raw);
|
|
1807
2026
|
provider = body.provider ?? "";
|
|
1808
2027
|
key = (body.key ?? "").trim();
|
|
2028
|
+
variant = body.variant;
|
|
1809
2029
|
}
|
|
1810
2030
|
catch {
|
|
1811
2031
|
json(res, 400, { error: "invalid JSON body" });
|
|
@@ -1820,13 +2040,20 @@ async function createSession(deps, opts) {
|
|
|
1820
2040
|
json(res, 400, { error: "API key is required" });
|
|
1821
2041
|
return;
|
|
1822
2042
|
}
|
|
2043
|
+
// Providers with multiple API-key variants (currently only Xiaomi: Token
|
|
2044
|
+
// Plan vs. API Credits) store under the chosen variant's key/baseUrl,
|
|
2045
|
+
// defaulting to the first variant. Single-variant providers fall back to
|
|
2046
|
+
// the legacy provider-id storage key + flat apiKeyBaseUrl.
|
|
2047
|
+
const chosenVariant = meta.apiKeyVariants?.find((v) => v.key === variant) ?? meta.apiKeyVariants?.[0];
|
|
2048
|
+
const storageKey = chosenVariant?.key ?? provider;
|
|
2049
|
+
const baseUrl = chosenVariant?.baseUrl ?? meta.apiKeyBaseUrl;
|
|
1823
2050
|
const creds = {
|
|
1824
2051
|
accessToken: key,
|
|
1825
2052
|
refreshToken: "",
|
|
1826
2053
|
expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000 * 100, // ~100y
|
|
1827
|
-
...(
|
|
2054
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
1828
2055
|
};
|
|
1829
|
-
await auth.setCredentials(
|
|
2056
|
+
await auth.setCredentials(storageKey, creds);
|
|
1830
2057
|
broadcast("auth_done", { provider });
|
|
1831
2058
|
json(res, 200, { ok: true });
|
|
1832
2059
|
});
|
|
@@ -1923,6 +2150,10 @@ async function createSession(deps, opts) {
|
|
|
1923
2150
|
// "disconnect" fully removes Kimi OAuth and the API key.
|
|
1924
2151
|
if (provider === "moonshot")
|
|
1925
2152
|
await auth.clearCredentials(MOONSHOT_OAUTH_KEY);
|
|
2153
|
+
// Xiaomi's API Credits credential lives under a distinct key — clear it
|
|
2154
|
+
// too so "disconnect" fully removes both the Token Plan and Credits keys.
|
|
2155
|
+
if (provider === "xiaomi")
|
|
2156
|
+
await auth.clearCredentials(XIAOMI_CREDITS_KEY);
|
|
1926
2157
|
broadcast("auth_done", { provider });
|
|
1927
2158
|
json(res, 200, { ok: true });
|
|
1928
2159
|
});
|
|
@@ -2206,7 +2437,9 @@ async function createSession(deps, opts) {
|
|
|
2206
2437
|
for (const c of clients)
|
|
2207
2438
|
c.res.end();
|
|
2208
2439
|
nolanAbort.abort();
|
|
2440
|
+
nolanAutoAbort.abort();
|
|
2209
2441
|
await nolanSession?.dispose().catch(() => { });
|
|
2442
|
+
await nolanAutoSession?.dispose().catch(() => { });
|
|
2210
2443
|
await session.dispose().catch(() => { });
|
|
2211
2444
|
}
|
|
2212
2445
|
return {
|