@kenkaiiii/ggcoder 5.17.0 → 5.19.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 +228 -53
- 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 +15 -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 +84 -1
- 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-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 +12 -0
- package/dist/core/agent-session-tool-result-policy.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +19 -8
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +111 -55
- 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/compactor.d.ts +9 -1
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +43 -9
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +49 -0
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/project-discovery.d.ts +4 -11
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +33 -30
- 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 +1 -0
- 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/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 +3 -66
- 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/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.7 | API key |
|
|
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.7, 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.
|
|
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
|
@@ -44,7 +44,8 @@ import { ensureAppDirs, loadSavedSettings } from "./config.js";
|
|
|
44
44
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
45
45
|
import { getModel, getMaxThinkingLevel, getContextWindow, MODELS } from "./core/model-registry.js";
|
|
46
46
|
import { resolveStartOrFallback } from "./core/resolve-start.js";
|
|
47
|
-
import { getGitBranch, isGitRepo } from "./utils/git.js";
|
|
47
|
+
import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
|
|
48
|
+
import { extractPlanSteps } from "./utils/plan-steps.js";
|
|
48
49
|
import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
|
|
49
50
|
import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
|
|
50
51
|
import { loadCustomCommands } from "./core/custom-commands.js";
|
|
@@ -63,16 +64,20 @@ import { awardPrompt, awardCommits } from "./core/progress/engine.js";
|
|
|
63
64
|
import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
|
|
64
65
|
import { rebuildFromSessions } from "./core/progress/rebuild.js";
|
|
65
66
|
const ALL_PROVIDERS = [
|
|
67
|
+
// US
|
|
66
68
|
"anthropic",
|
|
67
|
-
"xiaomi",
|
|
68
69
|
"openai",
|
|
69
70
|
"gemini",
|
|
70
|
-
"
|
|
71
|
+
"xai",
|
|
72
|
+
// China
|
|
71
73
|
"moonshot",
|
|
74
|
+
"glm",
|
|
72
75
|
"minimax",
|
|
76
|
+
"xiaomi",
|
|
73
77
|
"deepseek",
|
|
74
|
-
|
|
78
|
+
// Japan, then provider-agnostic gateway last
|
|
75
79
|
"sakana",
|
|
80
|
+
"openrouter",
|
|
76
81
|
];
|
|
77
82
|
function appSettingsFile() {
|
|
78
83
|
return path.join(os.homedir(), ".gg", "gg-app.json");
|
|
@@ -535,6 +540,11 @@ async function main() {
|
|
|
535
540
|
});
|
|
536
541
|
const usageCache = new Map();
|
|
537
542
|
const usageRequests = new Map();
|
|
543
|
+
// 429 backoff: when the provider rate-limits the usage endpoint, hold the
|
|
544
|
+
// error result until this timestamp instead of re-polling (and re-logging a
|
|
545
|
+
// WARN) every 60s — the old cadence hammered a limited endpoint for hours.
|
|
546
|
+
const usageRateLimitedUntil = new Map();
|
|
547
|
+
const USAGE_RATE_LIMIT_BACKOFF_MS = 5 * 60_000;
|
|
538
548
|
async function fetchUsageProvider(provider) {
|
|
539
549
|
const displayName = provider === "anthropic" ? "Anthropic" : "Codex";
|
|
540
550
|
if (!(await auth.hasProviderAuth(provider))) {
|
|
@@ -543,14 +553,24 @@ async function main() {
|
|
|
543
553
|
try {
|
|
544
554
|
let credentials = await auth.resolveCredentials(provider);
|
|
545
555
|
try {
|
|
546
|
-
|
|
556
|
+
const snapshot = {
|
|
557
|
+
...(await fetchSubscriptionUsage(provider, credentials)),
|
|
558
|
+
connected: true,
|
|
559
|
+
};
|
|
560
|
+
usageRateLimitedUntil.delete(provider);
|
|
561
|
+
return snapshot;
|
|
547
562
|
}
|
|
548
563
|
catch (error) {
|
|
549
564
|
// A provider can revoke an access token before its stored expiry. Refresh
|
|
550
565
|
// once on 401, matching inference auth recovery, then retry the usage call.
|
|
551
566
|
if (error instanceof SubscriptionUsageError && error.status === 401) {
|
|
552
567
|
credentials = await auth.resolveCredentials(provider, { forceRefresh: true });
|
|
553
|
-
|
|
568
|
+
const snapshot = {
|
|
569
|
+
...(await fetchSubscriptionUsage(provider, credentials)),
|
|
570
|
+
connected: true,
|
|
571
|
+
};
|
|
572
|
+
usageRateLimitedUntil.delete(provider);
|
|
573
|
+
return snapshot;
|
|
554
574
|
}
|
|
555
575
|
throw error;
|
|
556
576
|
}
|
|
@@ -558,6 +578,9 @@ async function main() {
|
|
|
558
578
|
catch (error) {
|
|
559
579
|
const message = error instanceof Error ? error.message : String(error);
|
|
560
580
|
log("WARN", "app-sidecar", "subscription usage fetch failed", { provider, message });
|
|
581
|
+
if (error instanceof SubscriptionUsageError && error.status === 429) {
|
|
582
|
+
usageRateLimitedUntil.set(provider, Date.now() + USAGE_RATE_LIMIT_BACKOFF_MS);
|
|
583
|
+
}
|
|
561
584
|
const connected = await auth.hasProviderAuth(provider);
|
|
562
585
|
return {
|
|
563
586
|
provider,
|
|
@@ -586,9 +609,12 @@ async function main() {
|
|
|
586
609
|
const missingReset = result.connected &&
|
|
587
610
|
result.windows.length > 0 &&
|
|
588
611
|
result.windows.some((window) => window.resetsAt === undefined);
|
|
612
|
+
const rateLimitedUntil = usageRateLimitedUntil.get(provider) ?? 0;
|
|
589
613
|
usageCache.set(provider, {
|
|
590
614
|
result,
|
|
591
|
-
expiresAt: Date.now()
|
|
615
|
+
expiresAt: rateLimitedUntil > Date.now()
|
|
616
|
+
? rateLimitedUntil
|
|
617
|
+
: Date.now() + (missingReset ? 10_000 : 60_000),
|
|
592
618
|
});
|
|
593
619
|
return result;
|
|
594
620
|
}
|
|
@@ -1011,6 +1037,11 @@ async function createSession(deps, opts) {
|
|
|
1011
1037
|
signal: abort.signal,
|
|
1012
1038
|
// Keep MCP startup off the readiness path in both modes.
|
|
1013
1039
|
backgroundMcpConnect: true,
|
|
1040
|
+
// Keep restore-time auto-compaction off the readiness path too: its summary
|
|
1041
|
+
// LLM call (30s timeout) used to freeze waitForReady — and with it the whole
|
|
1042
|
+
// window (project picker, session list) — whenever a resumed session was
|
|
1043
|
+
// over the context threshold. First prompt compacts instead, with UI events.
|
|
1044
|
+
deferLoadCompaction: true,
|
|
1014
1045
|
};
|
|
1015
1046
|
let session;
|
|
1016
1047
|
if (mode === "chat") {
|
|
@@ -1035,7 +1066,9 @@ async function createSession(deps, opts) {
|
|
|
1035
1066
|
...baseSessionOptions,
|
|
1036
1067
|
// Plan mode belongs only to the coding agent.
|
|
1037
1068
|
onEnterPlan: async (reason) => {
|
|
1069
|
+
deactivateApprovedPlan();
|
|
1038
1070
|
await session.setPlanMode(true);
|
|
1071
|
+
broadcast("plan_progress", { total: 0, completed: [] });
|
|
1039
1072
|
broadcast("plan_enter", { reason: reason ?? "" });
|
|
1040
1073
|
void session.persistAppMarker("plan", { reason: reason ?? "" }).catch(() => { });
|
|
1041
1074
|
},
|
|
@@ -1065,11 +1098,17 @@ async function createSession(deps, opts) {
|
|
|
1065
1098
|
}
|
|
1066
1099
|
}
|
|
1067
1100
|
log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1101
|
+
// Workspace extras (context window, git status, background tasks). Git state
|
|
1102
|
+
// is resolved once at startup and refreshed after every run; the context
|
|
1070
1103
|
// window follows the active model.
|
|
1071
|
-
|
|
1072
|
-
|
|
1104
|
+
const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
|
|
1105
|
+
getGitBranch(cwd).catch(() => null),
|
|
1106
|
+
isGitRepo(cwd).catch(() => false),
|
|
1107
|
+
getGitDirtyFileCount(cwd).catch(() => 0),
|
|
1108
|
+
]);
|
|
1109
|
+
let gitBranch = initialGitBranch;
|
|
1110
|
+
let gitIsRepo = initialGitIsRepo;
|
|
1111
|
+
let gitDirtyFileCount = initialDirtyFileCount;
|
|
1073
1112
|
function currentContextWindow() {
|
|
1074
1113
|
const st = session.getState();
|
|
1075
1114
|
return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
|
|
@@ -1081,6 +1120,7 @@ async function createSession(deps, opts) {
|
|
|
1081
1120
|
contextWindow: currentContextWindow(),
|
|
1082
1121
|
gitBranch,
|
|
1083
1122
|
isGitRepo: gitIsRepo,
|
|
1123
|
+
gitDirtyFileCount,
|
|
1084
1124
|
tasks: session.listBackgroundProcesses(),
|
|
1085
1125
|
};
|
|
1086
1126
|
}
|
|
@@ -1090,8 +1130,93 @@ async function createSession(deps, opts) {
|
|
|
1090
1130
|
// failures (e.g. repeated invalid-argument errors) instead of leaving the
|
|
1091
1131
|
// fatal-abort path with no forensic trail.
|
|
1092
1132
|
const toolCallNames = new Map();
|
|
1133
|
+
// Approved-plan progress belongs beside the plan file, not in the webview.
|
|
1134
|
+
// The implementation can rewrite/expand that file mid-run, so a step count
|
|
1135
|
+
// frozen at approval time becomes dishonest (the exact stale-total bug the
|
|
1136
|
+
// CLI already fixed). Re-read the live file after tools/markers, retain the
|
|
1137
|
+
// last valid step section during transient edits, and send one authoritative
|
|
1138
|
+
// snapshot to the app.
|
|
1139
|
+
let approvedPlanPath = null;
|
|
1140
|
+
let approvedPlanTotal = 0;
|
|
1141
|
+
let approvedPlanMarkers = new Set();
|
|
1142
|
+
let approvedPlanGeneration = 0;
|
|
1143
|
+
let planMarkerTail = "";
|
|
1144
|
+
let planProgressSync = Promise.resolve(false);
|
|
1145
|
+
function planProgressPayload() {
|
|
1146
|
+
const completed = [...approvedPlanMarkers]
|
|
1147
|
+
.filter((step) => step >= 1 && step <= approvedPlanTotal)
|
|
1148
|
+
.sort((a, b) => a - b);
|
|
1149
|
+
return { total: approvedPlanTotal, completed };
|
|
1150
|
+
}
|
|
1151
|
+
async function syncApprovedPlanProgress(generation) {
|
|
1152
|
+
const planPath = approvedPlanPath;
|
|
1153
|
+
if (planPath === null || generation !== approvedPlanGeneration)
|
|
1154
|
+
return false;
|
|
1155
|
+
const content = await fs.readFile(planPath, "utf-8").catch(() => null);
|
|
1156
|
+
if (approvedPlanPath !== planPath || generation !== approvedPlanGeneration)
|
|
1157
|
+
return false;
|
|
1158
|
+
if (content !== null) {
|
|
1159
|
+
const freshTotal = extractPlanSteps(content).length;
|
|
1160
|
+
// During an in-place rewrite the step section can briefly disappear.
|
|
1161
|
+
// Keep the last real total instead of flashing 0 or declaring completion.
|
|
1162
|
+
if (freshTotal > 0 || approvedPlanTotal === 0)
|
|
1163
|
+
approvedPlanTotal = freshTotal;
|
|
1164
|
+
}
|
|
1165
|
+
broadcast("plan_progress", planProgressPayload());
|
|
1166
|
+
return (approvedPlanTotal > 0 &&
|
|
1167
|
+
Array.from({ length: approvedPlanTotal }, (_, index) => index + 1).every((step) => approvedPlanMarkers.has(step)));
|
|
1168
|
+
}
|
|
1169
|
+
function queueApprovedPlanProgressSync() {
|
|
1170
|
+
const generation = approvedPlanGeneration;
|
|
1171
|
+
planProgressSync = planProgressSync
|
|
1172
|
+
.catch(() => false)
|
|
1173
|
+
.then(() => syncApprovedPlanProgress(generation))
|
|
1174
|
+
.catch((error) => {
|
|
1175
|
+
log("WARN", "app-sidecar", "plan progress refresh failed", {
|
|
1176
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1177
|
+
});
|
|
1178
|
+
return false;
|
|
1179
|
+
});
|
|
1180
|
+
return planProgressSync;
|
|
1181
|
+
}
|
|
1182
|
+
async function activateApprovedPlan(planPath) {
|
|
1183
|
+
deactivateApprovedPlan();
|
|
1184
|
+
await session.setApprovedPlan(planPath);
|
|
1185
|
+
if (!planPath)
|
|
1186
|
+
return 0;
|
|
1187
|
+
approvedPlanPath = planPath;
|
|
1188
|
+
await queueApprovedPlanProgressSync();
|
|
1189
|
+
return approvedPlanTotal;
|
|
1190
|
+
}
|
|
1191
|
+
function deactivateApprovedPlan() {
|
|
1192
|
+
approvedPlanGeneration++;
|
|
1193
|
+
approvedPlanPath = null;
|
|
1194
|
+
approvedPlanTotal = 0;
|
|
1195
|
+
approvedPlanMarkers = new Set();
|
|
1196
|
+
planMarkerTail = "";
|
|
1197
|
+
planProgressSync = Promise.resolve(false);
|
|
1198
|
+
}
|
|
1199
|
+
function recordApprovedPlanMarkers(text) {
|
|
1200
|
+
if (approvedPlanPath === null || !text)
|
|
1201
|
+
return;
|
|
1202
|
+
const candidate = planMarkerTail + text;
|
|
1203
|
+
let changed = false;
|
|
1204
|
+
for (const match of candidate.matchAll(/\[DONE:(\d+)\]/gi)) {
|
|
1205
|
+
const step = Number.parseInt(match[1], 10);
|
|
1206
|
+
if (step >= 1 && !approvedPlanMarkers.has(step)) {
|
|
1207
|
+
approvedPlanMarkers.add(step);
|
|
1208
|
+
changed = true;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
planMarkerTail = candidate.slice(-32);
|
|
1212
|
+
if (changed)
|
|
1213
|
+
void queueApprovedPlanProgressSync();
|
|
1214
|
+
}
|
|
1093
1215
|
// Forward every relevant bus event to the webview.
|
|
1094
|
-
session.eventBus.on("text_delta", (d) =>
|
|
1216
|
+
session.eventBus.on("text_delta", (d) => {
|
|
1217
|
+
broadcast("text_delta", d);
|
|
1218
|
+
recordApprovedPlanMarkers(d.text);
|
|
1219
|
+
});
|
|
1095
1220
|
session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
|
|
1096
1221
|
session.eventBus.on("tool_call_start", (d) => {
|
|
1097
1222
|
toolCallNames.set(d.toolCallId, d.name);
|
|
@@ -1108,6 +1233,11 @@ async function createSession(deps, opts) {
|
|
|
1108
1233
|
...(d.isError ? { result: d.result.slice(0, 500) } : {}),
|
|
1109
1234
|
});
|
|
1110
1235
|
broadcast("tool_call_end", d);
|
|
1236
|
+
// Any tool can mutate the approved plan (including bash), so refresh after
|
|
1237
|
+
// every completed call while tracking is active. The file is tiny and this
|
|
1238
|
+
// keeps the displayed total aligned before the next completion marker.
|
|
1239
|
+
if (approvedPlanPath !== null)
|
|
1240
|
+
void queueApprovedPlanProgressSync();
|
|
1111
1241
|
});
|
|
1112
1242
|
// Native server tools (e.g. Anthropic web_search) do NOT end the turn — text
|
|
1113
1243
|
// streams before and after them in the SAME turn. The webview must reset its
|
|
@@ -1132,7 +1262,6 @@ async function createSession(deps, opts) {
|
|
|
1132
1262
|
});
|
|
1133
1263
|
const cancelledRunEndGenerations = new Set();
|
|
1134
1264
|
let pendingCancelDrain = null;
|
|
1135
|
-
let titleGenerated = false;
|
|
1136
1265
|
// Bumped by /cancel — a run whose cancel generation changed mid-flight was
|
|
1137
1266
|
// canceled and earns no XP.
|
|
1138
1267
|
let cancelGeneration = 0;
|
|
@@ -1347,20 +1476,6 @@ async function createSession(deps, opts) {
|
|
|
1347
1476
|
});
|
|
1348
1477
|
return ken;
|
|
1349
1478
|
}
|
|
1350
|
-
// Resumed session: if it already has a conversation, generate its title now so
|
|
1351
|
-
// the title bar shows it immediately on load (not just after the next prompt).
|
|
1352
|
-
{
|
|
1353
|
-
const hasHistory = session
|
|
1354
|
-
.getMessages()
|
|
1355
|
-
.some((m) => m.role === "user" || m.role === "assistant");
|
|
1356
|
-
if (hasHistory) {
|
|
1357
|
-
titleGenerated = true;
|
|
1358
|
-
void session.generateTitle().then((title) => {
|
|
1359
|
-
if (title)
|
|
1360
|
-
broadcast("session_title", { title });
|
|
1361
|
-
});
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
1479
|
function abortOwnedWork() {
|
|
1365
1480
|
cancelGeneration++;
|
|
1366
1481
|
abort.abort();
|
|
@@ -1423,10 +1538,34 @@ async function createSession(deps, opts) {
|
|
|
1423
1538
|
// Fire-and-forget — XP must never delay or break run teardown.
|
|
1424
1539
|
void progress.awardRun(cwd, runStartedAt, opts.id);
|
|
1425
1540
|
}
|
|
1426
|
-
// A run may have switched branches
|
|
1427
|
-
// background tasks
|
|
1428
|
-
gitBranch = await
|
|
1429
|
-
|
|
1541
|
+
// A run may have switched branches, changed files, or spawned/finished
|
|
1542
|
+
// background tasks. Refresh the workspace extras once it settles.
|
|
1543
|
+
[gitBranch, gitIsRepo, gitDirtyFileCount] = await Promise.all([
|
|
1544
|
+
getGitBranch(cwd).catch(() => gitBranch),
|
|
1545
|
+
isGitRepo(cwd).catch(() => gitIsRepo),
|
|
1546
|
+
getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
|
|
1547
|
+
]);
|
|
1548
|
+
// Serialize behind any marker/tool-triggered refresh so the terminal
|
|
1549
|
+
// progress snapshot uses the live plan file. Once every canonical step
|
|
1550
|
+
// is complete, remove the approved plan from future system prompts and
|
|
1551
|
+
// clear the widget before run_end paints the idle activity bar.
|
|
1552
|
+
if (runSucceeded &&
|
|
1553
|
+
!cancelled &&
|
|
1554
|
+
approvedPlanPath !== null &&
|
|
1555
|
+
(await queueApprovedPlanProgressSync())) {
|
|
1556
|
+
try {
|
|
1557
|
+
await session.setApprovedPlan(undefined);
|
|
1558
|
+
deactivateApprovedPlan();
|
|
1559
|
+
broadcast("plan_progress", { total: 0, completed: [] });
|
|
1560
|
+
}
|
|
1561
|
+
catch (error) {
|
|
1562
|
+
// Keep tracking when prompt cleanup fails; hiding the widget here
|
|
1563
|
+
// would claim completion while the approved-plan contract remained.
|
|
1564
|
+
log("WARN", "app-sidecar", "completed plan cleanup failed", {
|
|
1565
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1430
1569
|
if (ownsGeneration)
|
|
1431
1570
|
finishOwnedGeneration(generation, false);
|
|
1432
1571
|
// A cancelled injected run is still owned by the surrounding autopilot
|
|
@@ -1445,13 +1584,6 @@ async function createSession(deps, opts) {
|
|
|
1445
1584
|
broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
|
|
1446
1585
|
broadcast("queued", { count: session.getQueuedCount() });
|
|
1447
1586
|
broadcast("extras", footerExtras());
|
|
1448
|
-
if (!titleGenerated) {
|
|
1449
|
-
titleGenerated = true;
|
|
1450
|
-
void session.generateTitle().then((title) => {
|
|
1451
|
-
if (title)
|
|
1452
|
-
broadcast("session_title", { title });
|
|
1453
|
-
});
|
|
1454
|
-
}
|
|
1455
1587
|
}
|
|
1456
1588
|
}
|
|
1457
1589
|
// ── Autopilot orchestration ─────────────────────────────────
|
|
@@ -1582,22 +1714,22 @@ async function createSession(deps, opts) {
|
|
|
1582
1714
|
if (pendingPlanPath === null || planGeneration !== planGenAtReview)
|
|
1583
1715
|
return false;
|
|
1584
1716
|
const planPath = pendingPlanPath;
|
|
1717
|
+
let planTotal;
|
|
1585
1718
|
try {
|
|
1586
|
-
await session.newSession();
|
|
1719
|
+
await session.newSession(true);
|
|
1587
1720
|
injectedAutopilotPrompts = [];
|
|
1588
|
-
|
|
1589
|
-
await session.setApprovedPlan(planPath);
|
|
1721
|
+
planTotal = await activateApprovedPlan(planPath);
|
|
1590
1722
|
}
|
|
1591
1723
|
catch (err) {
|
|
1592
1724
|
broadcastError("autopilot_error", "autopilot plan accept failed", err);
|
|
1593
1725
|
return false;
|
|
1594
1726
|
}
|
|
1595
1727
|
clearPendingPlan();
|
|
1596
|
-
//
|
|
1597
|
-
//
|
|
1598
|
-
// session_reset clears it — accepted must land first.
|
|
1728
|
+
// Keep the approval marker ahead of the reset, then seed the reset
|
|
1729
|
+
// with the sidecar's canonical count from the actual plan file.
|
|
1599
1730
|
broadcast("autopilot_plan_accepted", {});
|
|
1600
|
-
broadcast("session_reset", {});
|
|
1731
|
+
broadcast("session_reset", { planTotal });
|
|
1732
|
+
broadcast("plan_progress", planProgressPayload());
|
|
1601
1733
|
// Persisted into the NEW session so a resume shows the marker.
|
|
1602
1734
|
void session.persistAutopilotMarker("plan_approved");
|
|
1603
1735
|
return true;
|
|
@@ -1755,9 +1887,9 @@ async function createSession(deps, opts) {
|
|
|
1755
1887
|
return false;
|
|
1756
1888
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1757
1889
|
await session.newSession();
|
|
1890
|
+
deactivateApprovedPlan();
|
|
1758
1891
|
injectedAutopilotPrompts = [];
|
|
1759
1892
|
clearPendingPlan();
|
|
1760
|
-
titleGenerated = false;
|
|
1761
1893
|
broadcast("session_reset", {});
|
|
1762
1894
|
markTaskInProgress(cwd, task.id);
|
|
1763
1895
|
broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
|
|
@@ -1838,6 +1970,30 @@ async function createSession(deps, opts) {
|
|
|
1838
1970
|
tasksPoll.unref?.();
|
|
1839
1971
|
};
|
|
1840
1972
|
scheduleTasksPoll(1500);
|
|
1973
|
+
// Files can change outside the agent (editor saves, terminal commits), so keep
|
|
1974
|
+
// the dirty count current while idle. Branch/repo state already refreshes after
|
|
1975
|
+
// agent runs; polling only the count avoids spawning three git processes per tick.
|
|
1976
|
+
let gitPoll;
|
|
1977
|
+
let gitPollStopped = false;
|
|
1978
|
+
const scheduleGitPoll = (delay) => {
|
|
1979
|
+
if (gitPollStopped)
|
|
1980
|
+
return;
|
|
1981
|
+
gitPoll = setTimeout(() => {
|
|
1982
|
+
void getGitDirtyFileCount(cwd)
|
|
1983
|
+
.catch(() => gitDirtyFileCount)
|
|
1984
|
+
.then((nextDirtyFileCount) => {
|
|
1985
|
+
if (gitPollStopped)
|
|
1986
|
+
return;
|
|
1987
|
+
if (nextDirtyFileCount !== gitDirtyFileCount) {
|
|
1988
|
+
gitDirtyFileCount = nextDirtyFileCount;
|
|
1989
|
+
broadcast("extras", footerExtras());
|
|
1990
|
+
}
|
|
1991
|
+
scheduleGitPoll(5000);
|
|
1992
|
+
});
|
|
1993
|
+
}, delay);
|
|
1994
|
+
gitPoll.unref?.();
|
|
1995
|
+
};
|
|
1996
|
+
scheduleGitPoll(5000);
|
|
1841
1997
|
function readBody(req) {
|
|
1842
1998
|
return new Promise((resolve, reject) => {
|
|
1843
1999
|
const chunks = [];
|
|
@@ -2111,6 +2267,20 @@ async function createSession(deps, opts) {
|
|
|
2111
2267
|
void (async () => {
|
|
2112
2268
|
const commandCandidates = [...PROMPT_COMMANDS, ...(await loadCustomCommands(cwd))];
|
|
2113
2269
|
const messages = session.getMessages();
|
|
2270
|
+
// An Ideal hook is injected immediately after the assistant's candidate
|
|
2271
|
+
// no-tool response. That response is review scratch, not a user-visible
|
|
2272
|
+
// final answer. Live SSE drops it when the hook event arrives; mark the
|
|
2273
|
+
// same assistant message here so resumed history stays identical.
|
|
2274
|
+
const hiddenIdealDrafts = new Set();
|
|
2275
|
+
for (let i = 0; i < messages.length - 1; i++) {
|
|
2276
|
+
const draft = messages[i];
|
|
2277
|
+
const hookPrompt = messages[i + 1];
|
|
2278
|
+
if (draft?.role !== "assistant" || hookPrompt?.role !== "user")
|
|
2279
|
+
continue;
|
|
2280
|
+
const restored = restoreUserRow(hookPrompt.content);
|
|
2281
|
+
if (detectHookKind(restored.text) === "ideal")
|
|
2282
|
+
hiddenIdealDrafts.add(draft);
|
|
2283
|
+
}
|
|
2114
2284
|
// Pre-index tool results by toolCallId so we can pair tool calls with
|
|
2115
2285
|
// their results (for sub-agent status + image extraction).
|
|
2116
2286
|
const toolResultMap = new Map();
|
|
@@ -2328,10 +2498,11 @@ async function createSession(deps, opts) {
|
|
|
2328
2498
|
}
|
|
2329
2499
|
}
|
|
2330
2500
|
}
|
|
2331
|
-
else {
|
|
2501
|
+
else if (!hiddenIdealDrafts.has(msg)) {
|
|
2332
2502
|
// Assistant: one wire row per persisted text block — live streaming
|
|
2333
2503
|
// splits bubbles at server_tool_call boundaries, and the persisted
|
|
2334
|
-
// content keeps those blocks separate.
|
|
2504
|
+
// content keeps those blocks separate. Ideal-review candidate drafts
|
|
2505
|
+
// are intentionally omitted to match the live pre-final hook flow.
|
|
2335
2506
|
for (const blockText of restoreAssistantTexts(msg.content)) {
|
|
2336
2507
|
history.push({
|
|
2337
2508
|
role: "assistant",
|
|
@@ -2984,6 +3155,7 @@ async function createSession(deps, opts) {
|
|
|
2984
3155
|
if (mode === "chat") {
|
|
2985
3156
|
await session.persistAppMarker("agent_handoff", { chatAgent });
|
|
2986
3157
|
}
|
|
3158
|
+
deactivateApprovedPlan();
|
|
2987
3159
|
injectedAutopilotPrompts = [];
|
|
2988
3160
|
clearPendingPlan();
|
|
2989
3161
|
broadcast("session_reset", {});
|
|
@@ -3036,12 +3208,12 @@ async function createSession(deps, opts) {
|
|
|
3036
3208
|
broadcast("autopilot_ignored", {});
|
|
3037
3209
|
}
|
|
3038
3210
|
try {
|
|
3039
|
-
await session.newSession();
|
|
3211
|
+
await session.newSession(true);
|
|
3040
3212
|
injectedAutopilotPrompts = [];
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
broadcast("
|
|
3044
|
-
json(res, 200, { ok: true });
|
|
3213
|
+
const planTotal = await activateApprovedPlan(planPath);
|
|
3214
|
+
broadcast("session_reset", { planTotal });
|
|
3215
|
+
broadcast("plan_progress", planProgressPayload());
|
|
3216
|
+
json(res, 200, { ok: true, planTotal });
|
|
3045
3217
|
}
|
|
3046
3218
|
catch (err) {
|
|
3047
3219
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -3469,6 +3641,9 @@ async function createSession(deps, opts) {
|
|
|
3469
3641
|
tasksPollStopped = true;
|
|
3470
3642
|
if (tasksPoll)
|
|
3471
3643
|
clearTimeout(tasksPoll);
|
|
3644
|
+
gitPollStopped = true;
|
|
3645
|
+
if (gitPoll)
|
|
3646
|
+
clearTimeout(gitPoll);
|
|
3472
3647
|
// Stop the Telegram serve loop + dispose its per-chat sessions.
|
|
3473
3648
|
if (serveController)
|
|
3474
3649
|
await serveController.stop().catch(() => { });
|