@prestyj/cli 5.7.0 → 5.8.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 +243 -54
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +5 -2
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/shared.d.ts.map +1 -1
- package/dist/cli/shared.js +2 -0
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +18 -3
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +7 -0
- package/dist/config.test.js.map +1 -1
- package/dist/core/agent-session-compaction.test.js +521 -10
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session-memory-tail.test.js +1 -1
- package/dist/core/agent-session-memory-tail.test.js.map +1 -1
- package/dist/core/agent-session-queue.test.js +1 -1
- package/dist/core/agent-session-queue.test.js.map +1 -1
- package/dist/core/agent-session-review-coverage.test.js +26 -0
- package/dist/core/agent-session-review-coverage.test.js.map +1 -1
- package/dist/core/agent-session-tool-result-policy.test.d.ts +2 -0
- package/dist/core/agent-session-tool-result-policy.test.d.ts.map +1 -0
- package/dist/core/agent-session-tool-result-policy.test.js +25 -0
- package/dist/core/agent-session-tool-result-policy.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +40 -8
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +285 -87
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-providers.d.ts.map +1 -1
- package/dist/core/auth-providers.js +15 -8
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/compaction/active-context.d.ts +17 -0
- package/dist/core/compaction/active-context.d.ts.map +1 -0
- package/dist/core/compaction/active-context.js +20 -0
- package/dist/core/compaction/active-context.js.map +1 -0
- package/dist/core/compaction/active-context.test.d.ts +2 -0
- package/dist/core/compaction/active-context.test.d.ts.map +1 -0
- package/dist/core/compaction/active-context.test.js +36 -0
- package/dist/core/compaction/active-context.test.js.map +1 -0
- package/dist/core/compaction/compactor.d.ts +19 -18
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +64 -42
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +122 -65
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/token-estimator.d.ts +25 -1
- package/dist/core/compaction/token-estimator.d.ts.map +1 -1
- package/dist/core/compaction/token-estimator.js +86 -1
- package/dist/core/compaction/token-estimator.js.map +1 -1
- package/dist/core/compaction/token-estimator.test.js +147 -1
- package/dist/core/compaction/token-estimator.test.js.map +1 -1
- package/dist/core/compaction/tool-result-pruner.d.ts +37 -0
- package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -0
- package/dist/core/compaction/tool-result-pruner.js +103 -0
- package/dist/core/compaction/tool-result-pruner.js.map +1 -0
- package/dist/core/compaction/tool-result-pruner.test.d.ts +2 -0
- package/dist/core/compaction/tool-result-pruner.test.d.ts.map +1 -0
- package/dist/core/compaction/tool-result-pruner.test.js +148 -0
- package/dist/core/compaction/tool-result-pruner.test.js.map +1 -0
- package/dist/core/ideal-review.d.ts +6 -0
- package/dist/core/ideal-review.d.ts.map +1 -1
- package/dist/core/ideal-review.js +14 -0
- package/dist/core/ideal-review.js.map +1 -1
- package/dist/core/ideal-review.test.js +10 -1
- package/dist/core/ideal-review.test.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/project-discovery.d.ts +6 -9
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +35 -28
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.js +148 -0
- package/dist/core/project-discovery.test.js.map +1 -1
- package/dist/core/resolve-start.test.js +1 -0
- package/dist/core/resolve-start.test.js.map +1 -1
- package/dist/core/session-compaction.d.ts +3 -0
- package/dist/core/session-compaction.d.ts.map +1 -1
- package/dist/core/session-compaction.js +14 -1
- package/dist/core/session-compaction.js.map +1 -1
- package/dist/core/session-compaction.test.js +5 -0
- package/dist/core/session-compaction.test.js.map +1 -1
- package/dist/core/session-manager.d.ts +8 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +7 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +17 -0
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/session-preview.d.ts +11 -0
- package/dist/core/session-preview.d.ts.map +1 -0
- package/dist/core/session-preview.js +50 -0
- package/dist/core/session-preview.js.map +1 -0
- 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 +3 -2
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +2 -4
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +4 -0
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +13 -3
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/tools/bash.d.ts +9 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +28 -18
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/bash.test.d.ts +2 -0
- package/dist/tools/bash.test.d.ts.map +1 -0
- package/dist/tools/bash.test.js +56 -0
- package/dist/tools/bash.test.js.map +1 -0
- package/dist/tools/overflow.d.ts +12 -2
- package/dist/tools/overflow.d.ts.map +1 -1
- package/dist/tools/overflow.js +49 -4
- package/dist/tools/overflow.js.map +1 -1
- package/dist/tools/overflow.test.d.ts +2 -0
- package/dist/tools/overflow.test.d.ts.map +1 -0
- package/dist/tools/overflow.test.js +62 -0
- package/dist/tools/overflow.test.js.map +1 -0
- package/dist/tools/subagent-shared.d.ts +8 -0
- package/dist/tools/subagent-shared.d.ts.map +1 -1
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent.d.ts +3 -8
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +10 -8
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/subagent.test.js +17 -1
- package/dist/tools/subagent.test.js.map +1 -1
- package/dist/ui/App.d.ts +0 -2
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +6 -82
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ModelSelector.d.ts.map +1 -1
- package/dist/ui/components/ModelSelector.js +1 -0
- package/dist/ui/components/ModelSelector.js.map +1 -1
- package/dist/ui/components/SubAgentPanel.d.ts +2 -0
- package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
- package/dist/ui/components/SubAgentPanel.js +4 -2
- package/dist/ui/components/SubAgentPanel.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +2 -4
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +7 -3
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.test.js +31 -0
- package/dist/ui/hooks/useAgentLoop.test.js.map +1 -1
- package/dist/ui/hooks/useContextCompaction.d.ts +5 -8
- package/dist/ui/hooks/useContextCompaction.d.ts.map +1 -1
- package/dist/ui/hooks/useContextCompaction.js +77 -16
- package/dist/ui/hooks/useContextCompaction.js.map +1 -1
- package/dist/ui/hooks/useContextCompaction.test.d.ts +2 -0
- package/dist/ui/hooks/useContextCompaction.test.d.ts.map +1 -0
- package/dist/ui/hooks/useContextCompaction.test.js +135 -0
- package/dist/ui/hooks/useContextCompaction.test.js.map +1 -0
- package/dist/ui/hooks/useSessionPersistence.d.ts.map +1 -1
- package/dist/ui/hooks/useSessionPersistence.js +3 -0
- package/dist/ui/hooks/useSessionPersistence.js.map +1 -1
- package/dist/ui/hooks/useTerminalTitle.d.ts +3 -3
- package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
- package/dist/ui/hooks/useTerminalTitle.js +5 -9
- package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
- package/dist/ui/login.d.ts.map +1 -1
- package/dist/ui/login.js +7 -2
- package/dist/ui/login.js.map +1 -1
- package/dist/ui/render.d.ts +0 -2
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +0 -4
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/render.test.js +10 -1
- package/dist/ui/render.test.js.map +1 -1
- package/dist/ui/terminal-history.js +6 -2
- package/dist/ui/terminal-history.js.map +1 -1
- package/dist/ui/terminal-history.test.js +2 -2
- package/dist/ui/terminal-history.test.js.map +1 -1
- package/dist/ui/tui-history-parity.test.js +8 -1
- package/dist/ui/tui-history-parity.test.js.map +1 -1
- package/dist/utils/git.d.ts +2 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +12 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/git.test.d.ts +2 -0
- package/dist/utils/git.test.d.ts.map +1 -0
- package/dist/utils/git.test.js +36 -0
- package/dist/utils/git.test.js.map +1 -0
- package/package.json +4 -4
- package/dist/utils/session-title.d.ts +0 -19
- package/dist/utils/session-title.d.ts.map +0 -1
- package/dist/utils/session-title.js +0 -82
- package/dist/utils/session-title.js.map +0 -1
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ Switch mid-conversation with `/model`. Not locked to anyone.
|
|
|
71
71
|
|---|---|---|
|
|
72
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
|
-
| **Moonshot** | Kimi K2.
|
|
74
|
+
| **Moonshot** | Kimi K3, Kimi K2.7 Code | OAuth or 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
77
|
| **Xiaomi (MiMo)** | MiMo-V2.5-Pro, MiMo-V2.5-Pro-UltraSpeed, MiMo-V2.5 (image + video) | API key |
|
|
@@ -80,7 +80,7 @@ Switch mid-conversation with `/model`. Not locked to anyone.
|
|
|
80
80
|
|
|
81
81
|
The same conversation, the same tools, the same project context — only the model changes. Use a strong reasoning model when you need it, swap to a fast cheap one for grunt work, never restart your session.
|
|
82
82
|
|
|
83
|
-
**Attachments.** Drag, paste, or type a path to attach images and video in the chat input. Video is sent natively to models that support it (Gemini 3.x, Kimi K2.
|
|
83
|
+
**Attachments.** Drag, paste, or type a path to attach images and video in the chat input. Video is sent natively to models that support it (Gemini 3.x, Kimi K3/K2.7 Code, MiniMax M3, MiMo-V2.5); for other models the video is saved to a temp file and the model is told to inspect it with ffmpeg or its own tools.
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
package/dist/app-sidecar.js
CHANGED
|
@@ -34,6 +34,7 @@ import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-c
|
|
|
34
34
|
import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
|
|
35
35
|
import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
|
|
36
36
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
37
|
+
import { cleanupToolOutputs } from "./tools/overflow.js";
|
|
37
38
|
import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@prestyj/core";
|
|
38
39
|
import { loginAnthropic } from "./core/oauth/anthropic.js";
|
|
39
40
|
import { loginOpenAI } from "./core/oauth/openai.js";
|
|
@@ -44,7 +45,8 @@ import { ensureAppDirs, loadSavedSettings } from "./config.js";
|
|
|
44
45
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
45
46
|
import { getModel, getMaxThinkingLevel, getContextWindow, MODELS } from "./core/model-registry.js";
|
|
46
47
|
import { resolveStartOrFallback } from "./core/resolve-start.js";
|
|
47
|
-
import { getGitBranch, isGitRepo } from "./utils/git.js";
|
|
48
|
+
import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
|
|
49
|
+
import { extractPlanSteps } from "./utils/plan-steps.js";
|
|
48
50
|
import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
|
|
49
51
|
import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
|
|
50
52
|
import { loadCustomCommands } from "./core/custom-commands.js";
|
|
@@ -63,16 +65,20 @@ import { awardPrompt, awardCommits } from "./core/progress/engine.js";
|
|
|
63
65
|
import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
|
|
64
66
|
import { rebuildFromSessions } from "./core/progress/rebuild.js";
|
|
65
67
|
const ALL_PROVIDERS = [
|
|
68
|
+
// US
|
|
66
69
|
"anthropic",
|
|
67
|
-
"xiaomi",
|
|
68
70
|
"openai",
|
|
69
71
|
"gemini",
|
|
70
|
-
"
|
|
72
|
+
"xai",
|
|
73
|
+
// China
|
|
71
74
|
"moonshot",
|
|
75
|
+
"glm",
|
|
72
76
|
"minimax",
|
|
77
|
+
"xiaomi",
|
|
73
78
|
"deepseek",
|
|
74
|
-
|
|
79
|
+
// Japan, then provider-agnostic gateway last
|
|
75
80
|
"sakana",
|
|
81
|
+
"openrouter",
|
|
76
82
|
];
|
|
77
83
|
function appSettingsFile() {
|
|
78
84
|
return path.join(os.homedir(), ".ezcoder", "ezcoder-app.json");
|
|
@@ -505,6 +511,9 @@ async function main() {
|
|
|
505
511
|
// before anything spawns (bash tool, background tasks, LSP, git helpers all
|
|
506
512
|
// inherit it). Best-effort — never blocks startup beyond its internal cap.
|
|
507
513
|
await enrichProcessPath();
|
|
514
|
+
// Sweep recoverable full tool outputs (~/.ezcoder/tool-output/) older than 48h.
|
|
515
|
+
// Fire-and-forget: cleanup must never delay or break startup.
|
|
516
|
+
void cleanupToolOutputs().catch(() => { });
|
|
508
517
|
const auth = new AuthStorage(paths.authFile);
|
|
509
518
|
await auth.load();
|
|
510
519
|
// Every window's session lives here as an in-process object, keyed by the id
|
|
@@ -535,6 +544,11 @@ async function main() {
|
|
|
535
544
|
});
|
|
536
545
|
const usageCache = new Map();
|
|
537
546
|
const usageRequests = new Map();
|
|
547
|
+
// 429 backoff: when the provider rate-limits the usage endpoint, hold the
|
|
548
|
+
// error result until this timestamp instead of re-polling (and re-logging a
|
|
549
|
+
// WARN) every 60s — the old cadence hammered a limited endpoint for hours.
|
|
550
|
+
const usageRateLimitedUntil = new Map();
|
|
551
|
+
const USAGE_RATE_LIMIT_BACKOFF_MS = 5 * 60_000;
|
|
538
552
|
async function fetchUsageProvider(provider) {
|
|
539
553
|
const displayName = provider === "anthropic" ? "Anthropic" : "Codex";
|
|
540
554
|
if (!(await auth.hasProviderAuth(provider))) {
|
|
@@ -543,14 +557,24 @@ async function main() {
|
|
|
543
557
|
try {
|
|
544
558
|
let credentials = await auth.resolveCredentials(provider);
|
|
545
559
|
try {
|
|
546
|
-
|
|
560
|
+
const snapshot = {
|
|
561
|
+
...(await fetchSubscriptionUsage(provider, credentials)),
|
|
562
|
+
connected: true,
|
|
563
|
+
};
|
|
564
|
+
usageRateLimitedUntil.delete(provider);
|
|
565
|
+
return snapshot;
|
|
547
566
|
}
|
|
548
567
|
catch (error) {
|
|
549
568
|
// A provider can revoke an access token before its stored expiry. Refresh
|
|
550
569
|
// once on 401, matching inference auth recovery, then retry the usage call.
|
|
551
570
|
if (error instanceof SubscriptionUsageError && error.status === 401) {
|
|
552
571
|
credentials = await auth.resolveCredentials(provider, { forceRefresh: true });
|
|
553
|
-
|
|
572
|
+
const snapshot = {
|
|
573
|
+
...(await fetchSubscriptionUsage(provider, credentials)),
|
|
574
|
+
connected: true,
|
|
575
|
+
};
|
|
576
|
+
usageRateLimitedUntil.delete(provider);
|
|
577
|
+
return snapshot;
|
|
554
578
|
}
|
|
555
579
|
throw error;
|
|
556
580
|
}
|
|
@@ -558,6 +582,9 @@ async function main() {
|
|
|
558
582
|
catch (error) {
|
|
559
583
|
const message = error instanceof Error ? error.message : String(error);
|
|
560
584
|
log("WARN", "app-sidecar", "subscription usage fetch failed", { provider, message });
|
|
585
|
+
if (error instanceof SubscriptionUsageError && error.status === 429) {
|
|
586
|
+
usageRateLimitedUntil.set(provider, Date.now() + USAGE_RATE_LIMIT_BACKOFF_MS);
|
|
587
|
+
}
|
|
561
588
|
const connected = await auth.hasProviderAuth(provider);
|
|
562
589
|
return {
|
|
563
590
|
provider,
|
|
@@ -586,9 +613,12 @@ async function main() {
|
|
|
586
613
|
const missingReset = result.connected &&
|
|
587
614
|
result.windows.length > 0 &&
|
|
588
615
|
result.windows.some((window) => window.resetsAt === undefined);
|
|
616
|
+
const rateLimitedUntil = usageRateLimitedUntil.get(provider) ?? 0;
|
|
589
617
|
usageCache.set(provider, {
|
|
590
618
|
result,
|
|
591
|
-
expiresAt: Date.now()
|
|
619
|
+
expiresAt: rateLimitedUntil > Date.now()
|
|
620
|
+
? rateLimitedUntil
|
|
621
|
+
: Date.now() + (missingReset ? 10_000 : 60_000),
|
|
592
622
|
});
|
|
593
623
|
return result;
|
|
594
624
|
}
|
|
@@ -1011,6 +1041,11 @@ async function createSession(deps, opts) {
|
|
|
1011
1041
|
signal: abort.signal,
|
|
1012
1042
|
// Keep MCP startup off the readiness path in both modes.
|
|
1013
1043
|
backgroundMcpConnect: true,
|
|
1044
|
+
// Keep restore-time auto-compaction off the readiness path too: its summary
|
|
1045
|
+
// LLM call (30s timeout) used to freeze waitForReady — and with it the whole
|
|
1046
|
+
// window (project picker, session list) — whenever a resumed session was
|
|
1047
|
+
// over the context threshold. First prompt compacts instead, with UI events.
|
|
1048
|
+
deferLoadCompaction: true,
|
|
1014
1049
|
};
|
|
1015
1050
|
let session;
|
|
1016
1051
|
if (mode === "chat") {
|
|
@@ -1038,7 +1073,9 @@ async function createSession(deps, opts) {
|
|
|
1038
1073
|
// Unattended task runs cannot approve plans; implement directly instead.
|
|
1039
1074
|
if (taskRunActive)
|
|
1040
1075
|
return false;
|
|
1076
|
+
deactivateApprovedPlan();
|
|
1041
1077
|
await session.setPlanMode(true);
|
|
1078
|
+
broadcast("plan_progress", { total: 0, completed: [] });
|
|
1042
1079
|
broadcast("plan_enter", { reason: reason ?? "" });
|
|
1043
1080
|
void session.persistAppMarker("plan", { reason: reason ?? "" }).catch(() => { });
|
|
1044
1081
|
},
|
|
@@ -1073,11 +1110,17 @@ async function createSession(deps, opts) {
|
|
|
1073
1110
|
}
|
|
1074
1111
|
}
|
|
1075
1112
|
log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1113
|
+
// Workspace extras (context window, git status, background tasks). Git state
|
|
1114
|
+
// is resolved once at startup and refreshed after every run; the context
|
|
1078
1115
|
// window follows the active model.
|
|
1079
|
-
|
|
1080
|
-
|
|
1116
|
+
const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
|
|
1117
|
+
getGitBranch(cwd).catch(() => null),
|
|
1118
|
+
isGitRepo(cwd).catch(() => false),
|
|
1119
|
+
getGitDirtyFileCount(cwd).catch(() => 0),
|
|
1120
|
+
]);
|
|
1121
|
+
let gitBranch = initialGitBranch;
|
|
1122
|
+
let gitIsRepo = initialGitIsRepo;
|
|
1123
|
+
let gitDirtyFileCount = initialDirtyFileCount;
|
|
1081
1124
|
function currentContextWindow() {
|
|
1082
1125
|
const st = session.getState();
|
|
1083
1126
|
return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
|
|
@@ -1089,6 +1132,7 @@ async function createSession(deps, opts) {
|
|
|
1089
1132
|
contextWindow: currentContextWindow(),
|
|
1090
1133
|
gitBranch,
|
|
1091
1134
|
isGitRepo: gitIsRepo,
|
|
1135
|
+
gitDirtyFileCount,
|
|
1092
1136
|
tasks: session.listBackgroundProcesses(),
|
|
1093
1137
|
};
|
|
1094
1138
|
}
|
|
@@ -1098,8 +1142,93 @@ async function createSession(deps, opts) {
|
|
|
1098
1142
|
// failures (e.g. repeated invalid-argument errors) instead of leaving the
|
|
1099
1143
|
// fatal-abort path with no forensic trail.
|
|
1100
1144
|
const toolCallNames = new Map();
|
|
1145
|
+
// Approved-plan progress belongs beside the plan file, not in the webview.
|
|
1146
|
+
// The implementation can rewrite/expand that file mid-run, so a step count
|
|
1147
|
+
// frozen at approval time becomes dishonest (the exact stale-total bug the
|
|
1148
|
+
// CLI already fixed). Re-read the live file after tools/markers, retain the
|
|
1149
|
+
// last valid step section during transient edits, and send one authoritative
|
|
1150
|
+
// snapshot to the app.
|
|
1151
|
+
let approvedPlanPath = null;
|
|
1152
|
+
let approvedPlanTotal = 0;
|
|
1153
|
+
let approvedPlanMarkers = new Set();
|
|
1154
|
+
let approvedPlanGeneration = 0;
|
|
1155
|
+
let planMarkerTail = "";
|
|
1156
|
+
let planProgressSync = Promise.resolve(false);
|
|
1157
|
+
function planProgressPayload() {
|
|
1158
|
+
const completed = [...approvedPlanMarkers]
|
|
1159
|
+
.filter((step) => step >= 1 && step <= approvedPlanTotal)
|
|
1160
|
+
.sort((a, b) => a - b);
|
|
1161
|
+
return { total: approvedPlanTotal, completed };
|
|
1162
|
+
}
|
|
1163
|
+
async function syncApprovedPlanProgress(generation) {
|
|
1164
|
+
const planPath = approvedPlanPath;
|
|
1165
|
+
if (planPath === null || generation !== approvedPlanGeneration)
|
|
1166
|
+
return false;
|
|
1167
|
+
const content = await fs.readFile(planPath, "utf-8").catch(() => null);
|
|
1168
|
+
if (approvedPlanPath !== planPath || generation !== approvedPlanGeneration)
|
|
1169
|
+
return false;
|
|
1170
|
+
if (content !== null) {
|
|
1171
|
+
const freshTotal = extractPlanSteps(content).length;
|
|
1172
|
+
// During an in-place rewrite the step section can briefly disappear.
|
|
1173
|
+
// Keep the last real total instead of flashing 0 or declaring completion.
|
|
1174
|
+
if (freshTotal > 0 || approvedPlanTotal === 0)
|
|
1175
|
+
approvedPlanTotal = freshTotal;
|
|
1176
|
+
}
|
|
1177
|
+
broadcast("plan_progress", planProgressPayload());
|
|
1178
|
+
return (approvedPlanTotal > 0 &&
|
|
1179
|
+
Array.from({ length: approvedPlanTotal }, (_, index) => index + 1).every((step) => approvedPlanMarkers.has(step)));
|
|
1180
|
+
}
|
|
1181
|
+
function queueApprovedPlanProgressSync() {
|
|
1182
|
+
const generation = approvedPlanGeneration;
|
|
1183
|
+
planProgressSync = planProgressSync
|
|
1184
|
+
.catch(() => false)
|
|
1185
|
+
.then(() => syncApprovedPlanProgress(generation))
|
|
1186
|
+
.catch((error) => {
|
|
1187
|
+
log("WARN", "app-sidecar", "plan progress refresh failed", {
|
|
1188
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1189
|
+
});
|
|
1190
|
+
return false;
|
|
1191
|
+
});
|
|
1192
|
+
return planProgressSync;
|
|
1193
|
+
}
|
|
1194
|
+
async function activateApprovedPlan(planPath) {
|
|
1195
|
+
deactivateApprovedPlan();
|
|
1196
|
+
await session.setApprovedPlan(planPath);
|
|
1197
|
+
if (!planPath)
|
|
1198
|
+
return 0;
|
|
1199
|
+
approvedPlanPath = planPath;
|
|
1200
|
+
await queueApprovedPlanProgressSync();
|
|
1201
|
+
return approvedPlanTotal;
|
|
1202
|
+
}
|
|
1203
|
+
function deactivateApprovedPlan() {
|
|
1204
|
+
approvedPlanGeneration++;
|
|
1205
|
+
approvedPlanPath = null;
|
|
1206
|
+
approvedPlanTotal = 0;
|
|
1207
|
+
approvedPlanMarkers = new Set();
|
|
1208
|
+
planMarkerTail = "";
|
|
1209
|
+
planProgressSync = Promise.resolve(false);
|
|
1210
|
+
}
|
|
1211
|
+
function recordApprovedPlanMarkers(text) {
|
|
1212
|
+
if (approvedPlanPath === null || !text)
|
|
1213
|
+
return;
|
|
1214
|
+
const candidate = planMarkerTail + text;
|
|
1215
|
+
let changed = false;
|
|
1216
|
+
for (const match of candidate.matchAll(/\[DONE:(\d+)\]/gi)) {
|
|
1217
|
+
const step = Number.parseInt(match[1], 10);
|
|
1218
|
+
if (step >= 1 && !approvedPlanMarkers.has(step)) {
|
|
1219
|
+
approvedPlanMarkers.add(step);
|
|
1220
|
+
changed = true;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
planMarkerTail = candidate.slice(-32);
|
|
1224
|
+
if (changed)
|
|
1225
|
+
void queueApprovedPlanProgressSync();
|
|
1226
|
+
}
|
|
1101
1227
|
// Forward every relevant bus event to the webview.
|
|
1102
|
-
session.eventBus.on("text_delta", (d) =>
|
|
1228
|
+
session.eventBus.on("text_delta", (d) => {
|
|
1229
|
+
broadcast("text_delta", d);
|
|
1230
|
+
recordApprovedPlanMarkers(d.text);
|
|
1231
|
+
});
|
|
1103
1232
|
session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
|
|
1104
1233
|
session.eventBus.on("tool_call_start", (d) => {
|
|
1105
1234
|
toolCallNames.set(d.toolCallId, d.name);
|
|
@@ -1122,6 +1251,11 @@ async function createSession(deps, opts) {
|
|
|
1122
1251
|
if (name === "tasks") {
|
|
1123
1252
|
broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
|
|
1124
1253
|
}
|
|
1254
|
+
// Any tool can mutate the approved plan (including bash), so refresh after
|
|
1255
|
+
// every completed call while tracking is active. The file is tiny and this
|
|
1256
|
+
// keeps the displayed total aligned before the next completion marker.
|
|
1257
|
+
if (approvedPlanPath !== null)
|
|
1258
|
+
void queueApprovedPlanProgressSync();
|
|
1125
1259
|
});
|
|
1126
1260
|
// Native server tools (e.g. Anthropic web_search) do NOT end the turn — text
|
|
1127
1261
|
// streams before and after them in the SAME turn. The webview must reset its
|
|
@@ -1146,15 +1280,16 @@ async function createSession(deps, opts) {
|
|
|
1146
1280
|
});
|
|
1147
1281
|
const cancelledRunEndGenerations = new Set();
|
|
1148
1282
|
let pendingCancelDrain = null;
|
|
1149
|
-
let titleGenerated = false;
|
|
1150
1283
|
// Bumped by /cancel — a run whose cancel generation changed mid-flight was
|
|
1151
1284
|
// canceled and earns no XP.
|
|
1152
1285
|
let cancelGeneration = 0;
|
|
1153
1286
|
// Autopilot (auto-review) toggle for THIS window's project. Loaded from
|
|
1154
1287
|
// ezcoder-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
|
|
1155
1288
|
// runAutopilotCycle after the user's turn settles — Nolan auto-reviews the work
|
|
1156
|
-
// and drives the review→prompt→review loop.
|
|
1289
|
+
// and drives the review→prompt→review loop. Nolan is the sole verification
|
|
1290
|
+
// owner in this mode, so suppress the build session's redundant Ideal hook.
|
|
1157
1291
|
let autopilot = mode === "code" && (await loadAutopilot(cwd));
|
|
1292
|
+
session.setIdealReviewSuppressed(autopilot);
|
|
1158
1293
|
// True while an autopilot review is in flight (used to defer nolanAuto model
|
|
1159
1294
|
// switches, like nolanRunning does for chat Nolan, and to drive the spinner).
|
|
1160
1295
|
let autopilotReviewing = false;
|
|
@@ -1351,6 +1486,9 @@ async function createSession(deps, opts) {
|
|
|
1351
1486
|
// (often >5 min) regardless of the user's global speedProfile pick.
|
|
1352
1487
|
forceLongCacheRetention: true,
|
|
1353
1488
|
});
|
|
1489
|
+
// Nolan is already the independent autopilot reviewer; recursively running
|
|
1490
|
+
// his own Ideal self-review adds latency and can corrupt the verdict shape.
|
|
1491
|
+
ken.setIdealReviewSuppressed(true);
|
|
1354
1492
|
await ken.initialize();
|
|
1355
1493
|
// Deliberately no bus bridge: the review is silent. Errors surface via the
|
|
1356
1494
|
// runAutopilotReview try/catch as autopilot_error frames.
|
|
@@ -1361,20 +1499,6 @@ async function createSession(deps, opts) {
|
|
|
1361
1499
|
});
|
|
1362
1500
|
return ken;
|
|
1363
1501
|
}
|
|
1364
|
-
// Resumed session: if it already has a conversation, generate its title now so
|
|
1365
|
-
// the title bar shows it immediately on load (not just after the next prompt).
|
|
1366
|
-
{
|
|
1367
|
-
const hasHistory = session
|
|
1368
|
-
.getMessages()
|
|
1369
|
-
.some((m) => m.role === "user" || m.role === "assistant");
|
|
1370
|
-
if (hasHistory) {
|
|
1371
|
-
titleGenerated = true;
|
|
1372
|
-
void session.generateTitle().then((title) => {
|
|
1373
|
-
if (title)
|
|
1374
|
-
broadcast("session_title", { title });
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
1502
|
function abortOwnedWork() {
|
|
1379
1503
|
cancelGeneration++;
|
|
1380
1504
|
abort.abort();
|
|
@@ -1437,10 +1561,34 @@ async function createSession(deps, opts) {
|
|
|
1437
1561
|
// Fire-and-forget — XP must never delay or break run teardown.
|
|
1438
1562
|
void progress.awardRun(cwd, runStartedAt, opts.id);
|
|
1439
1563
|
}
|
|
1440
|
-
// A run may have switched branches
|
|
1441
|
-
// background tasks
|
|
1442
|
-
gitBranch = await
|
|
1443
|
-
|
|
1564
|
+
// A run may have switched branches, changed files, or spawned/finished
|
|
1565
|
+
// background tasks. Refresh the workspace extras once it settles.
|
|
1566
|
+
[gitBranch, gitIsRepo, gitDirtyFileCount] = await Promise.all([
|
|
1567
|
+
getGitBranch(cwd).catch(() => gitBranch),
|
|
1568
|
+
isGitRepo(cwd).catch(() => gitIsRepo),
|
|
1569
|
+
getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
|
|
1570
|
+
]);
|
|
1571
|
+
// Serialize behind any marker/tool-triggered refresh so the terminal
|
|
1572
|
+
// progress snapshot uses the live plan file. Once every canonical step
|
|
1573
|
+
// is complete, remove the approved plan from future system prompts and
|
|
1574
|
+
// clear the widget before run_end paints the idle activity bar.
|
|
1575
|
+
if (runSucceeded &&
|
|
1576
|
+
!cancelled &&
|
|
1577
|
+
approvedPlanPath !== null &&
|
|
1578
|
+
(await queueApprovedPlanProgressSync())) {
|
|
1579
|
+
try {
|
|
1580
|
+
await session.setApprovedPlan(undefined);
|
|
1581
|
+
deactivateApprovedPlan();
|
|
1582
|
+
broadcast("plan_progress", { total: 0, completed: [] });
|
|
1583
|
+
}
|
|
1584
|
+
catch (error) {
|
|
1585
|
+
// Keep tracking when prompt cleanup fails; hiding the widget here
|
|
1586
|
+
// would claim completion while the approved-plan contract remained.
|
|
1587
|
+
log("WARN", "app-sidecar", "completed plan cleanup failed", {
|
|
1588
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1444
1592
|
if (ownsGeneration)
|
|
1445
1593
|
finishOwnedGeneration(generation, false);
|
|
1446
1594
|
// A cancelled injected run is still owned by the surrounding autopilot
|
|
@@ -1463,13 +1611,6 @@ async function createSession(deps, opts) {
|
|
|
1463
1611
|
broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
|
|
1464
1612
|
broadcast("queued", { count: session.getQueuedCount() });
|
|
1465
1613
|
broadcast("extras", footerExtras());
|
|
1466
|
-
if (!titleGenerated) {
|
|
1467
|
-
titleGenerated = true;
|
|
1468
|
-
void session.generateTitle().then((title) => {
|
|
1469
|
-
if (title)
|
|
1470
|
-
broadcast("session_title", { title });
|
|
1471
|
-
});
|
|
1472
|
-
}
|
|
1473
1614
|
}
|
|
1474
1615
|
}
|
|
1475
1616
|
// ── Autopilot orchestration ─────────────────────────────────
|
|
@@ -1574,6 +1715,7 @@ async function createSession(deps, opts) {
|
|
|
1574
1715
|
const generation = runLifecycle.begin(abortOwnedWork).generation;
|
|
1575
1716
|
pendingCancelDrain = null;
|
|
1576
1717
|
autopilotActive = true;
|
|
1718
|
+
session.setIdealReviewSuppressed(true);
|
|
1577
1719
|
// Generation captured by the last plan review; acceptPlan re-checks it so
|
|
1578
1720
|
// a user Accept/Reject landing mid-review always wins.
|
|
1579
1721
|
let planGenAtReview = -1;
|
|
@@ -1600,22 +1742,22 @@ async function createSession(deps, opts) {
|
|
|
1600
1742
|
if (pendingPlanPath === null || planGeneration !== planGenAtReview)
|
|
1601
1743
|
return false;
|
|
1602
1744
|
const planPath = pendingPlanPath;
|
|
1745
|
+
let planTotal;
|
|
1603
1746
|
try {
|
|
1604
|
-
await session.newSession();
|
|
1747
|
+
await session.newSession(true);
|
|
1605
1748
|
injectedAutopilotPrompts = [];
|
|
1606
|
-
|
|
1607
|
-
await session.setApprovedPlan(planPath);
|
|
1749
|
+
planTotal = await activateApprovedPlan(planPath);
|
|
1608
1750
|
}
|
|
1609
1751
|
catch (err) {
|
|
1610
1752
|
broadcastError("autopilot_error", "autopilot plan accept failed", err);
|
|
1611
1753
|
return false;
|
|
1612
1754
|
}
|
|
1613
1755
|
clearPendingPlan();
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
// session_reset clears it — accepted must land first.
|
|
1756
|
+
// Keep the approval marker ahead of the reset, then seed the reset
|
|
1757
|
+
// with the sidecar's canonical count from the actual plan file.
|
|
1617
1758
|
broadcast("autopilot_plan_accepted", {});
|
|
1618
|
-
broadcast("session_reset", {});
|
|
1759
|
+
broadcast("session_reset", { planTotal });
|
|
1760
|
+
broadcast("plan_progress", planProgressPayload());
|
|
1619
1761
|
// Persisted into the NEW session so a resume shows the marker.
|
|
1620
1762
|
void session.persistAutopilotMarker("plan_approved");
|
|
1621
1763
|
return true;
|
|
@@ -1687,6 +1829,7 @@ async function createSession(deps, opts) {
|
|
|
1687
1829
|
}
|
|
1688
1830
|
finally {
|
|
1689
1831
|
autopilotActive = false;
|
|
1832
|
+
session.setIdealReviewSuppressed(autopilot);
|
|
1690
1833
|
finishOwnedGeneration(generation, true);
|
|
1691
1834
|
queueMicrotask(() => void runStrandedQueue());
|
|
1692
1835
|
}
|
|
@@ -1786,9 +1929,9 @@ async function createSession(deps, opts) {
|
|
|
1786
1929
|
async function runTaskByIdInner(task) {
|
|
1787
1930
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1788
1931
|
await session.newSession();
|
|
1932
|
+
deactivateApprovedPlan();
|
|
1789
1933
|
injectedAutopilotPrompts = [];
|
|
1790
1934
|
clearPendingPlan();
|
|
1791
|
-
titleGenerated = false;
|
|
1792
1935
|
broadcast("session_reset", {});
|
|
1793
1936
|
markTaskInProgress(cwd, task.id);
|
|
1794
1937
|
broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
|
|
@@ -1872,6 +2015,30 @@ async function createSession(deps, opts) {
|
|
|
1872
2015
|
tasksPoll.unref?.();
|
|
1873
2016
|
};
|
|
1874
2017
|
scheduleTasksPoll(1500);
|
|
2018
|
+
// Files can change outside the agent (editor saves, terminal commits), so keep
|
|
2019
|
+
// the dirty count current while idle. Branch/repo state already refreshes after
|
|
2020
|
+
// agent runs; polling only the count avoids spawning three git processes per tick.
|
|
2021
|
+
let gitPoll;
|
|
2022
|
+
let gitPollStopped = false;
|
|
2023
|
+
const scheduleGitPoll = (delay) => {
|
|
2024
|
+
if (gitPollStopped)
|
|
2025
|
+
return;
|
|
2026
|
+
gitPoll = setTimeout(() => {
|
|
2027
|
+
void getGitDirtyFileCount(cwd)
|
|
2028
|
+
.catch(() => gitDirtyFileCount)
|
|
2029
|
+
.then((nextDirtyFileCount) => {
|
|
2030
|
+
if (gitPollStopped)
|
|
2031
|
+
return;
|
|
2032
|
+
if (nextDirtyFileCount !== gitDirtyFileCount) {
|
|
2033
|
+
gitDirtyFileCount = nextDirtyFileCount;
|
|
2034
|
+
broadcast("extras", footerExtras());
|
|
2035
|
+
}
|
|
2036
|
+
scheduleGitPoll(5000);
|
|
2037
|
+
});
|
|
2038
|
+
}, delay);
|
|
2039
|
+
gitPoll.unref?.();
|
|
2040
|
+
};
|
|
2041
|
+
scheduleGitPoll(5000);
|
|
1875
2042
|
function readBody(req) {
|
|
1876
2043
|
return new Promise((resolve, reject) => {
|
|
1877
2044
|
const chunks = [];
|
|
@@ -2145,6 +2312,20 @@ async function createSession(deps, opts) {
|
|
|
2145
2312
|
void (async () => {
|
|
2146
2313
|
const commandCandidates = [...PROMPT_COMMANDS, ...(await loadCustomCommands(cwd))];
|
|
2147
2314
|
const messages = session.getMessages();
|
|
2315
|
+
// An Ideal hook is injected immediately after the assistant's candidate
|
|
2316
|
+
// no-tool response. That response is review scratch, not a user-visible
|
|
2317
|
+
// final answer. Live SSE drops it when the hook event arrives; mark the
|
|
2318
|
+
// same assistant message here so resumed history stays identical.
|
|
2319
|
+
const hiddenIdealDrafts = new Set();
|
|
2320
|
+
for (let i = 0; i < messages.length - 1; i++) {
|
|
2321
|
+
const draft = messages[i];
|
|
2322
|
+
const hookPrompt = messages[i + 1];
|
|
2323
|
+
if (draft?.role !== "assistant" || hookPrompt?.role !== "user")
|
|
2324
|
+
continue;
|
|
2325
|
+
const restored = restoreUserRow(hookPrompt.content);
|
|
2326
|
+
if (detectHookKind(restored.text) === "ideal")
|
|
2327
|
+
hiddenIdealDrafts.add(draft);
|
|
2328
|
+
}
|
|
2148
2329
|
// Pre-index tool results by toolCallId so we can pair tool calls with
|
|
2149
2330
|
// their results (for sub-agent status + image extraction).
|
|
2150
2331
|
const toolResultMap = new Map();
|
|
@@ -2362,10 +2543,11 @@ async function createSession(deps, opts) {
|
|
|
2362
2543
|
}
|
|
2363
2544
|
}
|
|
2364
2545
|
}
|
|
2365
|
-
else {
|
|
2546
|
+
else if (!hiddenIdealDrafts.has(msg)) {
|
|
2366
2547
|
// Assistant: one wire row per persisted text block — live streaming
|
|
2367
2548
|
// splits bubbles at server_tool_call boundaries, and the persisted
|
|
2368
|
-
// content keeps those blocks separate.
|
|
2549
|
+
// content keeps those blocks separate. Ideal-review candidate drafts
|
|
2550
|
+
// are intentionally omitted to match the live pre-final hook flow.
|
|
2369
2551
|
for (const blockText of restoreAssistantTexts(msg.content)) {
|
|
2370
2552
|
history.push({
|
|
2371
2553
|
role: "assistant",
|
|
@@ -2645,6 +2827,9 @@ async function createSession(deps, opts) {
|
|
|
2645
2827
|
return;
|
|
2646
2828
|
}
|
|
2647
2829
|
autopilot = enabled;
|
|
2830
|
+
// A toggle-off during an active cycle takes effect after Nolan finishes;
|
|
2831
|
+
// until then, injected build runs must not re-enable Ideal self-review.
|
|
2832
|
+
session.setIdealReviewSuppressed(enabled || autopilotActive);
|
|
2648
2833
|
await saveAutopilot(cwd, enabled);
|
|
2649
2834
|
log("INFO", "app-sidecar", "autopilot toggled", { enabled: String(enabled) });
|
|
2650
2835
|
broadcast("autopilot", { autopilot: enabled });
|
|
@@ -3104,6 +3289,7 @@ async function createSession(deps, opts) {
|
|
|
3104
3289
|
if (mode === "chat") {
|
|
3105
3290
|
await session.persistAppMarker("agent_handoff", { chatAgent });
|
|
3106
3291
|
}
|
|
3292
|
+
deactivateApprovedPlan();
|
|
3107
3293
|
injectedAutopilotPrompts = [];
|
|
3108
3294
|
clearPendingPlan();
|
|
3109
3295
|
broadcast("session_reset", {});
|
|
@@ -3156,12 +3342,12 @@ async function createSession(deps, opts) {
|
|
|
3156
3342
|
broadcast("autopilot_ignored", {});
|
|
3157
3343
|
}
|
|
3158
3344
|
try {
|
|
3159
|
-
await session.newSession();
|
|
3345
|
+
await session.newSession(true);
|
|
3160
3346
|
injectedAutopilotPrompts = [];
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
broadcast("
|
|
3164
|
-
json(res, 200, { ok: true });
|
|
3347
|
+
const planTotal = await activateApprovedPlan(planPath);
|
|
3348
|
+
broadcast("session_reset", { planTotal });
|
|
3349
|
+
broadcast("plan_progress", planProgressPayload());
|
|
3350
|
+
json(res, 200, { ok: true, planTotal });
|
|
3165
3351
|
}
|
|
3166
3352
|
catch (err) {
|
|
3167
3353
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -3589,6 +3775,9 @@ async function createSession(deps, opts) {
|
|
|
3589
3775
|
tasksPollStopped = true;
|
|
3590
3776
|
if (tasksPoll)
|
|
3591
3777
|
clearTimeout(tasksPoll);
|
|
3778
|
+
gitPollStopped = true;
|
|
3779
|
+
if (gitPoll)
|
|
3780
|
+
clearTimeout(gitPoll);
|
|
3592
3781
|
// Stop the Telegram serve loop + dispose its per-chat sessions.
|
|
3593
3782
|
if (serveController)
|
|
3594
3783
|
await serveController.stop().catch(() => { });
|