@prestyj/cli 4.3.205 → 4.3.209
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/dist/cli.js +48 -33
- package/dist/cli.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +6 -0
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +16 -0
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/goal-controller.d.ts +57 -0
- package/dist/core/goal-controller.d.ts.map +1 -0
- package/dist/core/goal-controller.js +285 -0
- package/dist/core/goal-controller.js.map +1 -0
- package/dist/core/goal-controller.test.d.ts +2 -0
- package/dist/core/goal-controller.test.d.ts.map +1 -0
- package/dist/core/goal-controller.test.js +419 -0
- package/dist/core/goal-controller.test.js.map +1 -0
- package/dist/core/goal-lifecycle-smoke.test.d.ts +2 -0
- package/dist/core/goal-lifecycle-smoke.test.d.ts.map +1 -0
- package/dist/core/goal-lifecycle-smoke.test.js +207 -0
- package/dist/core/goal-lifecycle-smoke.test.js.map +1 -0
- package/dist/core/goal-store.d.ts +164 -0
- package/dist/core/goal-store.d.ts.map +1 -0
- package/dist/core/goal-store.js +721 -0
- package/dist/core/goal-store.js.map +1 -0
- package/dist/core/goal-store.test.d.ts +2 -0
- package/dist/core/goal-store.test.d.ts.map +1 -0
- package/dist/core/goal-store.test.js +341 -0
- package/dist/core/goal-store.test.js.map +1 -0
- package/dist/core/goal-verifier.d.ts +17 -0
- package/dist/core/goal-verifier.d.ts.map +1 -0
- package/dist/core/goal-verifier.js +84 -0
- package/dist/core/goal-verifier.js.map +1 -0
- package/dist/core/goal-verifier.test.d.ts +2 -0
- package/dist/core/goal-verifier.test.d.ts.map +1 -0
- package/dist/core/goal-verifier.test.js +88 -0
- package/dist/core/goal-verifier.test.js.map +1 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.d.ts +2 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.d.ts.map +1 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.js +68 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.js.map +1 -0
- package/dist/core/goal-worker.d.ts +51 -0
- package/dist/core/goal-worker.d.ts.map +1 -0
- package/dist/core/goal-worker.js +339 -0
- package/dist/core/goal-worker.js.map +1 -0
- package/dist/core/goal-worker.test.d.ts +2 -0
- package/dist/core/goal-worker.test.d.ts.map +1 -0
- package/dist/core/goal-worker.test.js +224 -0
- package/dist/core/goal-worker.test.js.map +1 -0
- package/dist/core/model-registry.test.js +51 -1
- package/dist/core/model-registry.test.js.map +1 -1
- package/dist/core/process-manager-dev-server-repro.test.d.ts +2 -0
- package/dist/core/process-manager-dev-server-repro.test.d.ts.map +1 -0
- package/dist/core/process-manager-dev-server-repro.test.js +136 -0
- package/dist/core/process-manager-dev-server-repro.test.js.map +1 -0
- package/dist/core/process-manager.d.ts +9 -0
- package/dist/core/process-manager.d.ts.map +1 -1
- package/dist/core/process-manager.js +22 -8
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +52 -0
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +24 -0
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/repomap.js +8 -1
- package/dist/core/repomap.js.map +1 -1
- package/dist/core/repomap.test.js +32 -0
- package/dist/core/repomap.test.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +20 -11
- package/dist/interactive.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +18 -40
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +124 -1
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +2 -51
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit-diff.d.ts.map +1 -1
- package/dist/tools/edit-diff.js +71 -32
- package/dist/tools/edit-diff.js.map +1 -1
- package/dist/tools/edit-diff.test.js +14 -0
- package/dist/tools/edit-diff.test.js.map +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +38 -18
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +56 -6
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/goals.d.ts +110 -0
- package/dist/tools/goals.d.ts.map +1 -0
- package/dist/tools/goals.js +500 -0
- package/dist/tools/goals.js.map +1 -0
- package/dist/tools/goals.test.d.ts +2 -0
- package/dist/tools/goals.test.d.ts.map +1 -0
- package/dist/tools/goals.test.js +431 -0
- package/dist/tools/goals.test.js.map +1 -0
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +22 -5
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/grep.test.d.ts +2 -0
- package/dist/tools/grep.test.d.ts.map +1 -0
- package/dist/tools/grep.test.js +20 -0
- package/dist/tools/grep.test.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/safe-env.d.ts +2 -0
- package/dist/tools/safe-env.d.ts.map +1 -0
- package/dist/tools/safe-env.js +52 -0
- package/dist/tools/safe-env.js.map +1 -0
- package/dist/tools/source-path.js +1 -1
- package/dist/tools/web-fetch.d.ts +5 -0
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +12 -1
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-fetch.test.js +9 -0
- package/dist/tools/web-fetch.test.js.map +1 -1
- package/dist/ui/App.d.ts +73 -4
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +1087 -184
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/app-state-persistence.test.d.ts +2 -0
- package/dist/ui/app-state-persistence.test.d.ts.map +1 -0
- package/dist/ui/app-state-persistence.test.js +186 -0
- package/dist/ui/app-state-persistence.test.js.map +1 -0
- package/dist/ui/components/BackgroundTasksBar.d.ts +16 -1
- package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
- package/dist/ui/components/BackgroundTasksBar.js +15 -2
- package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
- package/dist/ui/components/Banner.d.ts +2 -1
- package/dist/ui/components/Banner.d.ts.map +1 -1
- package/dist/ui/components/Banner.js +3 -3
- package/dist/ui/components/Banner.js.map +1 -1
- package/dist/ui/components/GoalOverlay.d.ts +74 -0
- package/dist/ui/components/GoalOverlay.d.ts.map +1 -0
- package/dist/ui/components/GoalOverlay.js +675 -0
- package/dist/ui/components/GoalOverlay.js.map +1 -0
- package/dist/ui/components/GoalStatusBar.d.ts +24 -0
- package/dist/ui/components/GoalStatusBar.d.ts.map +1 -0
- package/dist/ui/components/GoalStatusBar.js +113 -0
- package/dist/ui/components/GoalStatusBar.js.map +1 -0
- package/dist/ui/components/InputArea.d.ts +2 -1
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/InputArea.js +44 -2
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/InputArea.test.d.ts +2 -0
- package/dist/ui/components/InputArea.test.d.ts.map +1 -0
- package/dist/ui/components/InputArea.test.js +79 -0
- package/dist/ui/components/InputArea.test.js.map +1 -0
- package/dist/ui/components/ToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolExecution.js +27 -2
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/footer-status-layout.test.d.ts +2 -0
- package/dist/ui/footer-status-layout.test.d.ts.map +1 -0
- package/dist/ui/footer-status-layout.test.js +56 -0
- package/dist/ui/footer-status-layout.test.js.map +1 -0
- package/dist/ui/goal-events.d.ts +107 -0
- package/dist/ui/goal-events.d.ts.map +1 -0
- package/dist/ui/goal-events.js +323 -0
- package/dist/ui/goal-events.js.map +1 -0
- package/dist/ui/goal-events.test.d.ts +2 -0
- package/dist/ui/goal-events.test.d.ts.map +1 -0
- package/dist/ui/goal-events.test.js +312 -0
- package/dist/ui/goal-events.test.js.map +1 -0
- package/dist/ui/goal-overlay.test.d.ts +2 -0
- package/dist/ui/goal-overlay.test.d.ts.map +1 -0
- package/dist/ui/goal-overlay.test.js +306 -0
- package/dist/ui/goal-overlay.test.js.map +1 -0
- package/dist/ui/goal-status-bar.test.d.ts +2 -0
- package/dist/ui/goal-status-bar.test.d.ts.map +1 -0
- package/dist/ui/goal-status-bar.test.js +143 -0
- package/dist/ui/goal-status-bar.test.js.map +1 -0
- package/dist/ui/hooks/useAgentLoop.d.ts +3 -1
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +10 -2
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/live-item-flush.test.js +48 -0
- package/dist/ui/live-item-flush.test.js.map +1 -1
- package/dist/ui/render.d.ts +18 -4
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +13 -3
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/scroll-stabilization.test.d.ts +2 -0
- package/dist/ui/scroll-stabilization.test.d.ts.map +1 -0
- package/dist/ui/scroll-stabilization.test.js +70 -0
- package/dist/ui/scroll-stabilization.test.js.map +1 -0
- package/dist/ui/slash-command-images.test.d.ts +2 -0
- package/dist/ui/slash-command-images.test.d.ts.map +1 -0
- package/dist/ui/slash-command-images.test.js +57 -0
- package/dist/ui/slash-command-images.test.js.map +1 -0
- package/package.json +6 -5
package/dist/ui/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
|
|
3
|
-
import { Box, Text, Static
|
|
3
|
+
import { Box, Text, Static } from "ink";
|
|
4
4
|
import { useTerminalSize } from "./hooks/useTerminalSize.js";
|
|
5
5
|
import { useDoublePress } from "./hooks/useDoublePress.js";
|
|
6
6
|
import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
|
|
@@ -25,15 +25,17 @@ import { StreamingArea } from "./components/StreamingArea.js";
|
|
|
25
25
|
import { ActivityIndicator } from "./components/ActivityIndicator.js";
|
|
26
26
|
import { InputArea } from "./components/InputArea.js";
|
|
27
27
|
import { Footer } from "./components/Footer.js";
|
|
28
|
+
import { GoalStatusBar, reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
|
|
28
29
|
import { Banner } from "./components/Banner.js";
|
|
29
30
|
import { PlanOverlay } from "./components/PlanOverlay.js";
|
|
30
31
|
import { ModelSelector } from "./components/ModelSelector.js";
|
|
31
32
|
import { TaskOverlay } from "./components/TaskOverlay.js";
|
|
33
|
+
import { GoalOverlay } from "./components/GoalOverlay.js";
|
|
32
34
|
import { PixelOverlay } from "./components/PixelOverlay.js";
|
|
33
35
|
import { SkillsOverlay } from "./components/SkillsOverlay.js";
|
|
34
36
|
import { EyesOverlay } from "./components/EyesOverlay.js";
|
|
35
37
|
import { ThemeSelector } from "./components/ThemeSelector.js";
|
|
36
|
-
import { BackgroundTasksBar } from "./components/BackgroundTasksBar.js";
|
|
38
|
+
import { BackgroundTasksBar, getFooterStatusLayoutDecision, } from "./components/BackgroundTasksBar.js";
|
|
37
39
|
import { useTheme, useSetTheme } from "./theme/theme.js";
|
|
38
40
|
import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
|
|
39
41
|
import { getGitBranch } from "../utils/git.js";
|
|
@@ -57,6 +59,11 @@ import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMes
|
|
|
57
59
|
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
58
60
|
import { getMCPServers } from "../core/mcp/index.js";
|
|
59
61
|
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
|
|
62
|
+
import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, summarizeGoalCounts, summarizeGoalCountsFromRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
|
|
63
|
+
import { canCompleteGoalRun, decideGoalNextAction, shouldCreateVerifierFixTask, } from "../core/goal-controller.js";
|
|
64
|
+
import { runGoalVerifierCommand } from "../core/goal-verifier.js";
|
|
65
|
+
import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
|
|
66
|
+
import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
|
|
60
67
|
/** Where ezcoder bugs should be reported. Surfaced in the guidance line. */
|
|
61
68
|
const GGCODER_BUG_REPORT_URL = "github.com/Gahroot/ezcoder/issues";
|
|
62
69
|
/**
|
|
@@ -90,6 +97,64 @@ function toErrorItem(err, id, contextPrefix) {
|
|
|
90
97
|
id,
|
|
91
98
|
};
|
|
92
99
|
}
|
|
100
|
+
export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS, customCommands = []) {
|
|
101
|
+
const trimmed = input.trim();
|
|
102
|
+
if (!trimmed.startsWith("/"))
|
|
103
|
+
return null;
|
|
104
|
+
const parts = trimmed.slice(1).split(" ");
|
|
105
|
+
const cmdName = parts[0];
|
|
106
|
+
const cmdArgs = parts.slice(1).join(" ").trim();
|
|
107
|
+
const builtinCmd = promptCommands.find((c) => c.name === cmdName || c.aliases.includes(cmdName));
|
|
108
|
+
const customCmd = !builtinCmd ? customCommands.find((c) => c.name === cmdName) : undefined;
|
|
109
|
+
const promptText = builtinCmd?.prompt ?? customCmd?.prompt;
|
|
110
|
+
if (!promptText)
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
cmdName,
|
|
114
|
+
cmdArgs,
|
|
115
|
+
promptText,
|
|
116
|
+
fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
|
|
120
|
+
if (inputImages.length === 0)
|
|
121
|
+
return text;
|
|
122
|
+
const parts = [];
|
|
123
|
+
if (text) {
|
|
124
|
+
parts.push({ type: "text", text });
|
|
125
|
+
}
|
|
126
|
+
for (const img of inputImages) {
|
|
127
|
+
if (img.kind === "text") {
|
|
128
|
+
parts.push({
|
|
129
|
+
type: "text",
|
|
130
|
+
text: `<file name="${img.fileName}">\n${img.data}\n</file>`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else if (modelSupportsImages) {
|
|
134
|
+
parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// GLM models: save image to temp file and instruct model to use vision MCP tool
|
|
138
|
+
const ext = img.mediaType.split("/")[1] ?? "png";
|
|
139
|
+
const tmpPath = `/tmp/ezcoder-img-${Date.now()}.${ext}`;
|
|
140
|
+
try {
|
|
141
|
+
writeFileSync(tmpPath, Buffer.from(img.data, "base64"));
|
|
142
|
+
parts.push({
|
|
143
|
+
type: "text",
|
|
144
|
+
text: `[User attached an image saved at: ${tmpPath} — use the image_analysis tool to view and analyze it]`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
parts.push({
|
|
149
|
+
type: "text",
|
|
150
|
+
text: `[User attached an image but it could not be saved for analysis]`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// If only text parts remain after stripping images, simplify to plain string
|
|
156
|
+
return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
|
|
157
|
+
}
|
|
93
158
|
/** Tools that get aggregated into a single compact group when concurrent. */
|
|
94
159
|
const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
95
160
|
const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
|
|
@@ -114,6 +179,149 @@ function compactHistory(items) {
|
|
|
114
179
|
}
|
|
115
180
|
return compacted;
|
|
116
181
|
}
|
|
182
|
+
function summarizeGoalCompletion(summary) {
|
|
183
|
+
const lines = summary
|
|
184
|
+
.split("\n")
|
|
185
|
+
.map((line) => line.trim())
|
|
186
|
+
.filter((line) => line.length > 0 && line !== "[agent_done]");
|
|
187
|
+
const statusLine = lines.find((line) => /^Status:/i.test(line));
|
|
188
|
+
const changedLine = lines.find((line) => /^(Changed|Implemented|Fixed|Added|Key findings|Full verifier)/i.test(line));
|
|
189
|
+
const verificationLine = lines.find((line) => /^(Verification|Verified|Result):/i.test(line));
|
|
190
|
+
return statusLine ?? changedLine ?? verificationLine ?? lines[0];
|
|
191
|
+
}
|
|
192
|
+
function formatGoalWorkerFinishedTitle(taskTitle, status) {
|
|
193
|
+
return status === "done" ? `Done: ${taskTitle}` : `Failed: ${taskTitle}`;
|
|
194
|
+
}
|
|
195
|
+
function countGoalTasksByStatus(tasks, status) {
|
|
196
|
+
return tasks.filter((task) => task.status === status).length;
|
|
197
|
+
}
|
|
198
|
+
function firstText(values) {
|
|
199
|
+
return values.find((value) => value !== undefined && value.trim().length > 0)?.trim();
|
|
200
|
+
}
|
|
201
|
+
function truncateGoalSummary(value, maxLength = 90) {
|
|
202
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
203
|
+
if (normalized.length <= maxLength)
|
|
204
|
+
return normalized;
|
|
205
|
+
return `${normalized.slice(0, maxLength - 1)}…`;
|
|
206
|
+
}
|
|
207
|
+
export function buildGoalSummaryRows(run) {
|
|
208
|
+
const rows = [];
|
|
209
|
+
const doneTasks = countGoalTasksByStatus(run.tasks, "done");
|
|
210
|
+
const failedTasks = countGoalTasksByStatus(run.tasks, "failed");
|
|
211
|
+
const blockedTasks = countGoalTasksByStatus(run.tasks, "blocked");
|
|
212
|
+
const taskSuffix = [
|
|
213
|
+
failedTasks > 0 ? `${failedTasks} failed` : undefined,
|
|
214
|
+
blockedTasks > 0 ? `${blockedTasks} blocked` : undefined,
|
|
215
|
+
].filter((item) => item !== undefined);
|
|
216
|
+
rows.push({
|
|
217
|
+
label: "Tasks",
|
|
218
|
+
value: run.tasks.length > 0 ? `${doneTasks}/${run.tasks.length} done` : "none",
|
|
219
|
+
...(taskSuffix.length > 0 ? { detail: taskSuffix.join(", ") } : {}),
|
|
220
|
+
});
|
|
221
|
+
const verifierResult = run.verifier?.lastResult;
|
|
222
|
+
const verifierDetail = firstText([verifierResult?.outputPath, run.verifier?.command]);
|
|
223
|
+
rows.push({
|
|
224
|
+
label: "Verifier",
|
|
225
|
+
value: verifierResult?.status ?? (run.verifier?.command ? "ready" : "missing"),
|
|
226
|
+
...(verifierDetail ? { detail: truncateGoalSummary(verifierDetail) } : {}),
|
|
227
|
+
});
|
|
228
|
+
const latestEvidence = run.evidence.at(-1);
|
|
229
|
+
rows.push({
|
|
230
|
+
label: "Evidence",
|
|
231
|
+
value: `${run.evidence.length} recorded`,
|
|
232
|
+
...(latestEvidence
|
|
233
|
+
? { detail: truncateGoalSummary(latestEvidence.path ?? latestEvidence.label) }
|
|
234
|
+
: {}),
|
|
235
|
+
});
|
|
236
|
+
if (run.status === "blocked" || run.status === "paused" || run.blockers.length > 0) {
|
|
237
|
+
rows.push({
|
|
238
|
+
label: run.status === "paused" ? "Paused on" : "Blocked on",
|
|
239
|
+
value: truncateGoalSummary(goalHasBlockingPrerequisites(run)
|
|
240
|
+
? formatGoalBlockingPrerequisites(run)
|
|
241
|
+
: (run.blockers[0] ?? "manual review"), 110),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else if (run.successCriteria.length > 0) {
|
|
245
|
+
rows.push({
|
|
246
|
+
label: "Criteria",
|
|
247
|
+
value: `${run.successCriteria.length} checked`,
|
|
248
|
+
detail: truncateGoalSummary(run.successCriteria[0] ?? "", 80),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return rows.slice(0, 4);
|
|
252
|
+
}
|
|
253
|
+
export function formatGoalTerminalProgress(run) {
|
|
254
|
+
switch (run.status) {
|
|
255
|
+
case "passed":
|
|
256
|
+
return {
|
|
257
|
+
kind: "goal_progress",
|
|
258
|
+
phase: "terminal",
|
|
259
|
+
title: `Goal passed: ${run.title}`,
|
|
260
|
+
detail: "Verifier evidence is recorded; auto-continuation stopped.",
|
|
261
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
262
|
+
status: run.status,
|
|
263
|
+
};
|
|
264
|
+
case "failed":
|
|
265
|
+
return {
|
|
266
|
+
kind: "goal_progress",
|
|
267
|
+
phase: "terminal",
|
|
268
|
+
title: `Goal failed: ${run.title}`,
|
|
269
|
+
detail: "Auto-continuation stopped. Check Goal tasks for the failing step.",
|
|
270
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
271
|
+
status: run.status,
|
|
272
|
+
};
|
|
273
|
+
case "blocked":
|
|
274
|
+
return {
|
|
275
|
+
kind: "goal_progress",
|
|
276
|
+
phase: "terminal",
|
|
277
|
+
title: `Goal blocked: ${run.title}`,
|
|
278
|
+
detail: goalHasBlockingPrerequisites(run)
|
|
279
|
+
? formatGoalBlockingPrerequisites(run)
|
|
280
|
+
: (run.blockers[0] ?? "A prerequisite or missing verifier blocked progress."),
|
|
281
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
282
|
+
status: run.status,
|
|
283
|
+
};
|
|
284
|
+
case "paused":
|
|
285
|
+
return {
|
|
286
|
+
kind: "goal_progress",
|
|
287
|
+
phase: "terminal",
|
|
288
|
+
title: `Goal paused: ${run.title}`,
|
|
289
|
+
detail: run.blockers[0] ?? "Auto-continuation paused.",
|
|
290
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
291
|
+
status: run.status,
|
|
292
|
+
};
|
|
293
|
+
case "draft":
|
|
294
|
+
case "ready":
|
|
295
|
+
case "running":
|
|
296
|
+
case "verifying":
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
export function shouldHideHistoryForOverlayView(_isOverlayView, _isAgentRunning) {
|
|
301
|
+
// Ink Static is append-only. Passing [] for overlay panes rewrites the Static
|
|
302
|
+
// accumulator and can destroy scrollback when the pane closes. Keep history
|
|
303
|
+
// mounted and let overlays render below it.
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
|
|
307
|
+
return isAgentRunning && (overlayPane === "goal" || overlayPane === "plan");
|
|
308
|
+
}
|
|
309
|
+
export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender, }) {
|
|
310
|
+
return shouldHideHistoryForOverlay && !stabilizeOverlayPaneRerender;
|
|
311
|
+
}
|
|
312
|
+
export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, }) {
|
|
313
|
+
const shouldStabilize = isUserScrolled || hasTallLiveUserMessage;
|
|
314
|
+
return {
|
|
315
|
+
preserveStatic: shouldStabilize && hasNewOutput,
|
|
316
|
+
autoFollow: !shouldStabilize,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
export function isTallLiveUserMessage(text, rows) {
|
|
320
|
+
return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
|
|
321
|
+
}
|
|
322
|
+
export function getStaticHistoryKey({ resizeKey }) {
|
|
323
|
+
return `${resizeKey}`;
|
|
324
|
+
}
|
|
117
325
|
// flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
|
|
118
326
|
/** Check whether an item is still active (running spinner, pending result). */
|
|
119
327
|
function isActiveItem(item) {
|
|
@@ -271,11 +479,29 @@ function markTaskInProgress(cwd, taskId) {
|
|
|
271
479
|
// ignore
|
|
272
480
|
}
|
|
273
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Mark a task as done in the on-disk tasks.json. Used by run-all auto-advance
|
|
484
|
+
* so the in-flight task is recorded as completed even when the agent forgets
|
|
485
|
+
* to call the `tasks` tool. Errors are swallowed — if the file is missing or
|
|
486
|
+
* the id is stale, the next auto-advance step still proceeds correctly.
|
|
487
|
+
*/
|
|
488
|
+
function markTaskDone(cwd, taskId) {
|
|
489
|
+
try {
|
|
490
|
+
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
491
|
+
const filePath = join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json");
|
|
492
|
+
const data = readFileSync(filePath, "utf-8");
|
|
493
|
+
const tasks = JSON.parse(data);
|
|
494
|
+
const updated = tasks.map((t) => (t.id === taskId ? { ...t, status: "done" } : t));
|
|
495
|
+
writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
// ignore
|
|
499
|
+
}
|
|
500
|
+
}
|
|
274
501
|
// ── App Component ──────────────────────────────────────────
|
|
275
502
|
export function App(props) {
|
|
276
503
|
const theme = useTheme();
|
|
277
504
|
const switchTheme = useSetTheme();
|
|
278
|
-
const { stdout } = useStdout();
|
|
279
505
|
const { columns, resizeKey } = useTerminalSize();
|
|
280
506
|
// Hoisted before terminal title hook so it can reference them
|
|
281
507
|
const [lastUserMessage, setLastUserMessage] = useState("");
|
|
@@ -311,26 +537,38 @@ export function App(props) {
|
|
|
311
537
|
}
|
|
312
538
|
return [{ kind: "banner", id: "banner" }];
|
|
313
539
|
});
|
|
314
|
-
// Items from the current/last turn — rendered in the live area so they stay visible
|
|
315
|
-
|
|
540
|
+
// Items from the current/last turn — rendered in the live area so they stay visible.
|
|
541
|
+
// Seed from sessionStore so Goal progress/completion rows and other live output
|
|
542
|
+
// survive pane/overlay/resize remounts before they are flushed to <Static>.
|
|
543
|
+
const [liveItems, setLiveItems] = useState(() => props.sessionStore?.liveItems ?? []);
|
|
316
544
|
// overlay seeded from sessionStore (lives across remount). Falls back to
|
|
317
545
|
// props.initialOverlay (CLI launched with one), then null.
|
|
318
546
|
const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
|
|
319
547
|
const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
|
|
548
|
+
const [goalCount, setGoalCount] = useState(0);
|
|
549
|
+
const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
|
|
320
550
|
const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
321
551
|
const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
|
|
322
552
|
// Seed from sessionStore so "Run All" chaining survives the resetUI()
|
|
323
553
|
// remount that startTask() triggers between tasks.
|
|
324
554
|
const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
|
|
325
555
|
const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
|
|
556
|
+
// Task id of the run-all task currently driving the agent. Tracked so the
|
|
557
|
+
// run-all `onDone` auto-advance can mark the just-finished task as done
|
|
558
|
+
// even when the agent forgets to call the tasks tool. Mirrored through
|
|
559
|
+
// sessionStore so the value survives the resetUI() remount between tasks.
|
|
560
|
+
const currentTaskIdRef = useRef(props.sessionStore?.currentTaskId ?? null);
|
|
326
561
|
const startTaskRef = useRef(() => { });
|
|
562
|
+
const agentRunningRef = useRef(false);
|
|
563
|
+
const runningGoalIdsRef = useRef(new Set());
|
|
564
|
+
const activeVerifierRunIdsRef = useRef(new Set());
|
|
565
|
+
const startGoalRunRef = useRef(() => { });
|
|
327
566
|
const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
|
|
328
567
|
const currentPixelFixRef = useRef(null);
|
|
329
568
|
const startPixelFixRef = useRef(() => { });
|
|
330
569
|
const cwdRef = useRef(props.cwd);
|
|
331
570
|
const [displayedCwd, setDisplayedCwd] = useState(props.cwd);
|
|
332
|
-
const [
|
|
333
|
-
const [doneStatus, setDoneStatus] = useState(null);
|
|
571
|
+
const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
|
|
334
572
|
// Suppress "done" status when a plan overlay is about to open
|
|
335
573
|
const planOverlayPendingRef = useRef(false);
|
|
336
574
|
const [gitBranch, setGitBranch] = useState(null);
|
|
@@ -363,9 +601,12 @@ export function App(props) {
|
|
|
363
601
|
// previous mount, triggering React's duplicate-key warning and causing
|
|
364
602
|
// duplicate/omitted renders.
|
|
365
603
|
const nextIdRef = useRef((() => {
|
|
366
|
-
const
|
|
604
|
+
const items = [
|
|
605
|
+
...(props.sessionStore?.history ?? props.initialHistory ?? []),
|
|
606
|
+
...(props.sessionStore?.liveItems ?? []),
|
|
607
|
+
];
|
|
367
608
|
let max = -1;
|
|
368
|
-
for (const item of
|
|
609
|
+
for (const item of items) {
|
|
369
610
|
const n = Number(item.id);
|
|
370
611
|
if (Number.isFinite(n) && n > max)
|
|
371
612
|
max = n;
|
|
@@ -401,7 +642,27 @@ export function App(props) {
|
|
|
401
642
|
* language-detection path when this cwd has never been audited before.
|
|
402
643
|
*/
|
|
403
644
|
const triggerAutoSetupRef = useRef(async () => { });
|
|
404
|
-
const getId = () =>
|
|
645
|
+
const getId = () => `ui-${nextIdRef.current++}`;
|
|
646
|
+
const appendGoalProgress = useCallback((item) => {
|
|
647
|
+
setLiveItems((prev) => [...prev, { ...item, id: getId() }]);
|
|
648
|
+
}, []);
|
|
649
|
+
const goalNumberForRun = useCallback((runId) => Math.max(1, goalStatusEntries.findIndex((entry) => entry.runId === runId) + 1), [goalStatusEntries]);
|
|
650
|
+
const clearGoalStatusEntry = useCallback((runId) => {
|
|
651
|
+
setGoalStatusEntries((prev) => {
|
|
652
|
+
const next = removeGoalStatusEntry(prev, runId);
|
|
653
|
+
if (props.sessionStore)
|
|
654
|
+
props.sessionStore.goalStatusEntries = next;
|
|
655
|
+
return next;
|
|
656
|
+
});
|
|
657
|
+
}, [props.sessionStore]);
|
|
658
|
+
const upsertGoalStatusEntry = useCallback((entry) => {
|
|
659
|
+
setGoalStatusEntries((prev) => {
|
|
660
|
+
const next = syncGoalStatusEntries(prev, entry);
|
|
661
|
+
if (props.sessionStore)
|
|
662
|
+
props.sessionStore.goalStatusEntries = next;
|
|
663
|
+
return next;
|
|
664
|
+
});
|
|
665
|
+
}, [props.sessionStore]);
|
|
405
666
|
// Two-phase flush: items waiting to be moved to Static history after the
|
|
406
667
|
// live area has been cleared and Ink has committed the smaller output.
|
|
407
668
|
const pendingFlushRef = useRef([]);
|
|
@@ -411,8 +672,12 @@ export function App(props) {
|
|
|
411
672
|
if (items.length === 0)
|
|
412
673
|
return;
|
|
413
674
|
pendingFlushRef.current = [...pendingFlushRef.current, ...items];
|
|
675
|
+
if (props.sessionStore) {
|
|
676
|
+
const queuedIds = new Set(items.map((item) => item.id));
|
|
677
|
+
props.sessionStore.liveItems = (props.sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
|
|
678
|
+
}
|
|
414
679
|
setFlushGeneration((g) => g + 1);
|
|
415
|
-
}, []);
|
|
680
|
+
}, [props.sessionStore]);
|
|
416
681
|
// Mirror runtime state choices (model/provider/thinking) into renderApp's
|
|
417
682
|
// closure so unmount/remount preserves them.
|
|
418
683
|
const onRuntimeStateChange = props.onRuntimeStateChange;
|
|
@@ -436,6 +701,14 @@ export function App(props) {
|
|
|
436
701
|
if (sessionStore)
|
|
437
702
|
sessionStore.history = history;
|
|
438
703
|
}, [history, sessionStore]);
|
|
704
|
+
useEffect(() => {
|
|
705
|
+
if (sessionStore)
|
|
706
|
+
sessionStore.liveItems = liveItems;
|
|
707
|
+
}, [liveItems, sessionStore]);
|
|
708
|
+
useEffect(() => {
|
|
709
|
+
if (sessionStore)
|
|
710
|
+
sessionStore.doneStatus = doneStatus;
|
|
711
|
+
}, [doneStatus, sessionStore]);
|
|
439
712
|
useEffect(() => {
|
|
440
713
|
if (sessionStore)
|
|
441
714
|
sessionStore.planSteps = planSteps;
|
|
@@ -448,6 +721,10 @@ export function App(props) {
|
|
|
448
721
|
if (sessionStore)
|
|
449
722
|
sessionStore.overlay = overlay;
|
|
450
723
|
}, [overlay, sessionStore]);
|
|
724
|
+
useEffect(() => {
|
|
725
|
+
if (sessionStore)
|
|
726
|
+
sessionStore.goalStatusEntries = goalStatusEntries;
|
|
727
|
+
}, [goalStatusEntries, sessionStore]);
|
|
451
728
|
// pendingAction is consumed via a useEffect AFTER agentLoop is created
|
|
452
729
|
// — see below where useAgentLoop is set up.
|
|
453
730
|
const pendingActionConsumedRef = useRef(false);
|
|
@@ -463,6 +740,36 @@ export function App(props) {
|
|
|
463
740
|
useEffect(() => {
|
|
464
741
|
getGitBranch(displayedCwd).then(setGitBranch);
|
|
465
742
|
}, [displayedCwd]);
|
|
743
|
+
useEffect(() => {
|
|
744
|
+
let cancelled = false;
|
|
745
|
+
const refreshGoalCount = () => {
|
|
746
|
+
void reconcileActiveGoalRuns(props.cwd, {
|
|
747
|
+
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
748
|
+
}).then(({ runs }) => {
|
|
749
|
+
const counts = summarizeGoalCountsFromRuns(runs);
|
|
750
|
+
if (cancelled)
|
|
751
|
+
return;
|
|
752
|
+
setGoalCount(counts.active);
|
|
753
|
+
setGoalStatusEntries((prev) => {
|
|
754
|
+
const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
|
|
755
|
+
isWorkerActive: (workerId, run) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId &&
|
|
756
|
+
worker.goalRunId === run.id &&
|
|
757
|
+
worker.status === "running"),
|
|
758
|
+
isVerifierActive: (run) => activeVerifierRunIdsRef.current.has(run.id),
|
|
759
|
+
});
|
|
760
|
+
if (props.sessionStore)
|
|
761
|
+
props.sessionStore.goalStatusEntries = next;
|
|
762
|
+
return next;
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
};
|
|
766
|
+
refreshGoalCount();
|
|
767
|
+
const interval = setInterval(refreshGoalCount, 1000);
|
|
768
|
+
return () => {
|
|
769
|
+
cancelled = true;
|
|
770
|
+
clearInterval(interval);
|
|
771
|
+
};
|
|
772
|
+
}, [props.cwd]);
|
|
466
773
|
// Periodic update check during long sessions
|
|
467
774
|
useEffect(() => {
|
|
468
775
|
startPeriodicUpdateCheck(props.version, (msg) => {
|
|
@@ -619,14 +926,6 @@ export function App(props) {
|
|
|
619
926
|
// premature "done" status that fires when the agent loop finishes
|
|
620
927
|
planOverlayPendingRef.current = true;
|
|
621
928
|
setTimeout(() => {
|
|
622
|
-
// NOTE: this is the one open-overlay path that does NOT remount via
|
|
623
|
-
// resetUI. It runs while the agent is still mid-turn (after the
|
|
624
|
-
// exit_plan tool returned but before onDone fires), and unmounting
|
|
625
|
-
// here would kill the in-flight agent stream. Keep the bare ANSI
|
|
626
|
-
// clear; the drift bug is tolerable across just the agent's
|
|
627
|
-
// wrap-up turn, and onApprove/onReject both remount cleanly via
|
|
628
|
-
// resetUI when the user resolves the plan.
|
|
629
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
630
929
|
setPlanAutoExpand(true);
|
|
631
930
|
setOverlay("plan");
|
|
632
931
|
// Don't clear planOverlayPendingRef here — keep it true until
|
|
@@ -641,7 +940,7 @@ export function App(props) {
|
|
|
641
940
|
planPath);
|
|
642
941
|
};
|
|
643
942
|
}
|
|
644
|
-
}, [props.onExitPlanRef, replaceSystemPrompt
|
|
943
|
+
}, [props.onExitPlanRef, replaceSystemPrompt]);
|
|
645
944
|
const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
|
|
646
945
|
const sm = sessionManagerRef.current;
|
|
647
946
|
if (!sm)
|
|
@@ -712,7 +1011,8 @@ export function App(props) {
|
|
|
712
1011
|
});
|
|
713
1012
|
}
|
|
714
1013
|
}, [props.settingsFile]);
|
|
715
|
-
const
|
|
1014
|
+
const compactionAbortRef = useRef(null);
|
|
1015
|
+
const compactConversation = useCallback(async (messages, signal) => {
|
|
716
1016
|
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
717
1017
|
const tokensBefore = estimateConversationTokens(messages);
|
|
718
1018
|
const spinId = getId();
|
|
@@ -723,6 +1023,10 @@ export function App(props) {
|
|
|
723
1023
|
});
|
|
724
1024
|
// Show animated spinner
|
|
725
1025
|
setLiveItems((prev) => [...prev, { kind: "compacting", id: spinId }]);
|
|
1026
|
+
const ownedAbort = signal ? null : new AbortController();
|
|
1027
|
+
const compactionSignal = signal ?? ownedAbort?.signal;
|
|
1028
|
+
if (ownedAbort)
|
|
1029
|
+
compactionAbortRef.current = ownedAbort;
|
|
726
1030
|
try {
|
|
727
1031
|
// Resolve fresh credentials for compaction too
|
|
728
1032
|
let compactApiKey = activeApiKey;
|
|
@@ -744,7 +1048,7 @@ export function App(props) {
|
|
|
744
1048
|
projectId: compactProjectId,
|
|
745
1049
|
baseUrl: compactBaseUrl,
|
|
746
1050
|
contextWindow,
|
|
747
|
-
signal:
|
|
1051
|
+
signal: compactionSignal,
|
|
748
1052
|
approvedPlanPath: approvedPlanPathRef.current,
|
|
749
1053
|
});
|
|
750
1054
|
if (result.result.compacted) {
|
|
@@ -769,10 +1073,16 @@ export function App(props) {
|
|
|
769
1073
|
}
|
|
770
1074
|
catch (err) {
|
|
771
1075
|
const msg = err instanceof Error ? err.message : String(err);
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
setLiveItems((prev) =>
|
|
775
|
-
|
|
1076
|
+
const isAbort = compactionSignal?.aborted || msg.includes("aborted") || msg.includes("abort");
|
|
1077
|
+
log(isAbort ? "WARN" : "ERROR", "compaction", isAbort ? "Compaction aborted" : `Compaction failed: ${msg}`);
|
|
1078
|
+
setLiveItems((prev) => isAbort
|
|
1079
|
+
? prev.filter((item) => item.id !== spinId)
|
|
1080
|
+
: prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
|
|
1081
|
+
return messages; // Return unchanged on failure/abort
|
|
1082
|
+
}
|
|
1083
|
+
finally {
|
|
1084
|
+
if (ownedAbort && compactionAbortRef.current === ownedAbort)
|
|
1085
|
+
compactionAbortRef.current = null;
|
|
776
1086
|
}
|
|
777
1087
|
}, [
|
|
778
1088
|
currentModel,
|
|
@@ -1376,7 +1686,7 @@ export function App(props) {
|
|
|
1376
1686
|
return remaining;
|
|
1377
1687
|
});
|
|
1378
1688
|
}, []),
|
|
1379
|
-
onDone: useCallback((durationMs, toolsUsed) => {
|
|
1689
|
+
onDone: useCallback((durationMs, toolsUsed, errored) => {
|
|
1380
1690
|
log("INFO", "agent", `Agent done`, {
|
|
1381
1691
|
duration: `${durationMs}ms`,
|
|
1382
1692
|
toolsUsed: toolsUsed.join(",") || "none",
|
|
@@ -1403,6 +1713,18 @@ export function App(props) {
|
|
|
1403
1713
|
if (runAllTasksRef.current) {
|
|
1404
1714
|
setTimeout(() => {
|
|
1405
1715
|
const cwd = cwdRef.current;
|
|
1716
|
+
// Clean turn: mark the in-flight task done so the row flips to ✓.
|
|
1717
|
+
// Error path: leave it at `in-progress` so the failed task is
|
|
1718
|
+
// visibly distinct from completed ones. The chain still advances
|
|
1719
|
+
// either way — matching how errors were handled before this fix.
|
|
1720
|
+
const finishedId = currentTaskIdRef.current;
|
|
1721
|
+
if (finishedId) {
|
|
1722
|
+
if (!errored)
|
|
1723
|
+
markTaskDone(cwd, finishedId);
|
|
1724
|
+
currentTaskIdRef.current = null;
|
|
1725
|
+
if (props.sessionStore)
|
|
1726
|
+
props.sessionStore.currentTaskId = null;
|
|
1727
|
+
}
|
|
1406
1728
|
const next = getNextPendingTask(cwd);
|
|
1407
1729
|
if (next) {
|
|
1408
1730
|
markTaskInProgress(cwd, next.id);
|
|
@@ -1414,6 +1736,11 @@ export function App(props) {
|
|
|
1414
1736
|
}
|
|
1415
1737
|
}, 500);
|
|
1416
1738
|
}
|
|
1739
|
+
// Goal loop: after the orchestrator handles a worker/verifier event,
|
|
1740
|
+
// continue the same Goal automatically until it reaches a terminal state.
|
|
1741
|
+
for (const runId of [...runningGoalIdsRef.current]) {
|
|
1742
|
+
setTimeout(() => continueGoalRun(runId), 500);
|
|
1743
|
+
}
|
|
1417
1744
|
// Pixel fix: observe branch + commits, patch status, optionally pick
|
|
1418
1745
|
// up the next open error if run-all is active.
|
|
1419
1746
|
const pendingFix = currentPixelFixRef.current;
|
|
@@ -1455,6 +1782,12 @@ export function App(props) {
|
|
|
1455
1782
|
setRunAllTasks(false);
|
|
1456
1783
|
setRunAllPixel(false);
|
|
1457
1784
|
currentPixelFixRef.current = null;
|
|
1785
|
+
// Clear the run-all task id so a subsequent run-all does not
|
|
1786
|
+
// mistakenly mark this aborted task as done. The on-disk row stays
|
|
1787
|
+
// at `in-progress`, which is the correct truth for an aborted task.
|
|
1788
|
+
currentTaskIdRef.current = null;
|
|
1789
|
+
if (props.sessionStore)
|
|
1790
|
+
props.sessionStore.currentTaskId = null;
|
|
1458
1791
|
setDoneStatus(null);
|
|
1459
1792
|
setLiveItems((prev) => {
|
|
1460
1793
|
const next = prev.map((item) => {
|
|
@@ -1500,13 +1833,34 @@ export function App(props) {
|
|
|
1500
1833
|
}, []),
|
|
1501
1834
|
onQueuedStart: useCallback((content) => {
|
|
1502
1835
|
// When a queued message starts processing, show it as a UserItem
|
|
1503
|
-
// and flush prior items to history
|
|
1836
|
+
// and flush prior items to history. Synthetic system events are hidden
|
|
1837
|
+
// from the transcript but still routed through the main agent context.
|
|
1504
1838
|
const displayText = typeof content === "string"
|
|
1505
1839
|
? content
|
|
1506
1840
|
: content
|
|
1507
1841
|
.filter((c) => c.type === "text")
|
|
1508
1842
|
.map((c) => c.text)
|
|
1509
1843
|
.join("\n");
|
|
1844
|
+
if (isGoalSyntheticEvent(displayText)) {
|
|
1845
|
+
const eventInfo = parseGoalSyntheticEvent(displayText);
|
|
1846
|
+
setLiveItems((prev) => {
|
|
1847
|
+
if (prev.length > 0)
|
|
1848
|
+
queueFlush(prev);
|
|
1849
|
+
return [];
|
|
1850
|
+
});
|
|
1851
|
+
setDoneStatus(null);
|
|
1852
|
+
appendGoalProgress({
|
|
1853
|
+
kind: "goal_progress",
|
|
1854
|
+
phase: "orchestrator_reviewing",
|
|
1855
|
+
title: "Orchestrator reviewing Goal update",
|
|
1856
|
+
detail: eventInfo?.kind === "worker"
|
|
1857
|
+
? `Worker ${eventInfo.worker ?? "finished"} reported back${eventInfo.task ? ` on ${eventInfo.task}` : ""}. Inspecting Goal state.`
|
|
1858
|
+
: `Verifier reported ${eventInfo?.status ?? "status"}. Inspecting evidence and next action.`,
|
|
1859
|
+
workerId: eventInfo?.worker,
|
|
1860
|
+
status: eventInfo?.status,
|
|
1861
|
+
});
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1510
1864
|
const imageCount = typeof content === "string"
|
|
1511
1865
|
? undefined
|
|
1512
1866
|
: content.filter((c) => c.type === "image").length || undefined;
|
|
@@ -1605,7 +1959,14 @@ export function App(props) {
|
|
|
1605
1959
|
if (pendingFlushRef.current.length > 0) {
|
|
1606
1960
|
const items = pendingFlushRef.current;
|
|
1607
1961
|
pendingFlushRef.current = [];
|
|
1608
|
-
setHistory((h) =>
|
|
1962
|
+
setHistory((h) => {
|
|
1963
|
+
const next = compactHistory([...h, ...trimFlushedItems(items)]);
|
|
1964
|
+
if (sessionStore)
|
|
1965
|
+
sessionStore.history = next;
|
|
1966
|
+
return next;
|
|
1967
|
+
});
|
|
1968
|
+
if (sessionStore)
|
|
1969
|
+
sessionStore.liveItems = liveItems;
|
|
1609
1970
|
}
|
|
1610
1971
|
}, [flushGeneration]);
|
|
1611
1972
|
// Sync terminal title with agent loop state
|
|
@@ -1698,11 +2059,15 @@ export function App(props) {
|
|
|
1698
2059
|
}
|
|
1699
2060
|
// Handle /compact — compact conversation
|
|
1700
2061
|
if (trimmed === "/compact" || trimmed === "/c") {
|
|
1701
|
-
const
|
|
1702
|
-
|
|
2062
|
+
const ac = new AbortController();
|
|
2063
|
+
compactionAbortRef.current = ac;
|
|
2064
|
+
const compacted = await compactConversation(messagesRef.current, ac.signal);
|
|
2065
|
+
if (!ac.signal.aborted && compacted !== messagesRef.current) {
|
|
1703
2066
|
messagesRef.current = compacted;
|
|
1704
2067
|
await persistCompactedSession(compacted);
|
|
1705
2068
|
}
|
|
2069
|
+
if (compactionAbortRef.current === ac)
|
|
2070
|
+
compactionAbortRef.current = null;
|
|
1706
2071
|
return;
|
|
1707
2072
|
}
|
|
1708
2073
|
// Handle /quit — exit the agent
|
|
@@ -1710,14 +2075,10 @@ export function App(props) {
|
|
|
1710
2075
|
process.exit(0);
|
|
1711
2076
|
}
|
|
1712
2077
|
// Handle /clear — tear down the entire Ink instance and rebuild fresh.
|
|
1713
|
-
//
|
|
1714
|
-
//
|
|
1715
|
-
//
|
|
1716
|
-
//
|
|
1717
|
-
// terminal-state assumptions that ANSI clearing breaks. The reliable
|
|
1718
|
-
// fix is unmount + render again. Runtime state (model, provider,
|
|
1719
|
-
// thinking) survives via renderApp's closure-held `runtimeState`,
|
|
1720
|
-
// mirrored from React state via the useEffects above.
|
|
2078
|
+
// Avoid direct ANSI terminal clears here; they can erase scrollback.
|
|
2079
|
+
// Runtime state (model, provider, thinking) survives via renderApp's
|
|
2080
|
+
// closure-held `runtimeState`, mirrored from React state via the
|
|
2081
|
+
// useEffects above.
|
|
1721
2082
|
if (trimmed === "/clear") {
|
|
1722
2083
|
if (props.resetUI) {
|
|
1723
2084
|
void (async () => {
|
|
@@ -1730,8 +2091,7 @@ export function App(props) {
|
|
|
1730
2091
|
return;
|
|
1731
2092
|
}
|
|
1732
2093
|
// Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
|
|
1733
|
-
// React state in place
|
|
1734
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2094
|
+
// React state in place without touching terminal scrollback.
|
|
1735
2095
|
pendingFlushRef.current = [];
|
|
1736
2096
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
1737
2097
|
setLiveItems([]);
|
|
@@ -1747,7 +2107,6 @@ export function App(props) {
|
|
|
1747
2107
|
agentLoop.reset();
|
|
1748
2108
|
setSessionTitle(undefined);
|
|
1749
2109
|
sessionTitleGeneratedRef.current = false;
|
|
1750
|
-
setStaticKey((k) => k + 1);
|
|
1751
2110
|
setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
|
|
1752
2111
|
return;
|
|
1753
2112
|
}
|
|
@@ -1847,6 +2206,25 @@ export function App(props) {
|
|
|
1847
2206
|
]);
|
|
1848
2207
|
return;
|
|
1849
2208
|
}
|
|
2209
|
+
// Handle /goals — open goal pane
|
|
2210
|
+
if (trimmed === "/goals") {
|
|
2211
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2212
|
+
props.sessionStore.overlay = "goal";
|
|
2213
|
+
props.sessionStore.planAutoExpand = false;
|
|
2214
|
+
props.resetUI();
|
|
2215
|
+
}
|
|
2216
|
+
else {
|
|
2217
|
+
if (props.sessionStore) {
|
|
2218
|
+
props.sessionStore.overlay = "goal";
|
|
2219
|
+
props.sessionStore.planAutoExpand = false;
|
|
2220
|
+
if (agentLoop.isRunning)
|
|
2221
|
+
props.sessionStore.pendingResetUI = true;
|
|
2222
|
+
}
|
|
2223
|
+
setPlanAutoExpand(false);
|
|
2224
|
+
setOverlay("goal");
|
|
2225
|
+
}
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
1850
2228
|
// Handle /plans — open plan pane
|
|
1851
2229
|
if (trimmed === "/plans") {
|
|
1852
2230
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
@@ -1867,49 +2245,49 @@ export function App(props) {
|
|
|
1867
2245
|
return;
|
|
1868
2246
|
}
|
|
1869
2247
|
// Handle prompt-template commands (built-in + custom from .ezcoder/commands/)
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
const cmdName =
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
log("INFO", "command", `Prompt command: /${cmdName}${cmdArgs ? ` (args: ${cmdArgs})` : ""}`);
|
|
1879
|
-
// Move live items into history before starting
|
|
1880
|
-
setLiveItems((prev) => {
|
|
1881
|
-
if (prev.length > 0) {
|
|
1882
|
-
pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
|
|
1883
|
-
}
|
|
1884
|
-
return [];
|
|
1885
|
-
});
|
|
1886
|
-
// Show the command name as the user message
|
|
1887
|
-
const userItem = { kind: "user", text: trimmed, id: getId() };
|
|
1888
|
-
setLastUserMessage(trimmed);
|
|
1889
|
-
setDoneStatus(null);
|
|
1890
|
-
setLiveItems([userItem]);
|
|
1891
|
-
// Send the full prompt to the agent, with user args appended if provided
|
|
1892
|
-
const fullPrompt = cmdArgs
|
|
1893
|
-
? `${promptText}\n\n## User Instructions\n\n${cmdArgs}`
|
|
1894
|
-
: promptText;
|
|
1895
|
-
try {
|
|
1896
|
-
await agentLoop.run(fullPrompt);
|
|
1897
|
-
}
|
|
1898
|
-
catch (err) {
|
|
1899
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1900
|
-
log("ERROR", "error", msg);
|
|
1901
|
-
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
1902
|
-
setLiveItems((prev) => [
|
|
1903
|
-
...prev,
|
|
1904
|
-
isAbort
|
|
1905
|
-
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
1906
|
-
: toErrorItem(err, getId()),
|
|
1907
|
-
]);
|
|
2248
|
+
const promptCommandRoute = routePromptCommandInput(trimmed, PROMPT_COMMANDS, customCommands);
|
|
2249
|
+
if (promptCommandRoute) {
|
|
2250
|
+
const { cmdName, cmdArgs, fullPrompt } = promptCommandRoute;
|
|
2251
|
+
log("INFO", "command", `Prompt command: /${cmdName}${cmdArgs ? ` (args: ${cmdArgs})` : ""}`);
|
|
2252
|
+
// Move live items into history before starting
|
|
2253
|
+
setLiveItems((prev) => {
|
|
2254
|
+
if (prev.length > 0) {
|
|
2255
|
+
pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
|
|
1908
2256
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2257
|
+
return [];
|
|
2258
|
+
});
|
|
2259
|
+
const hasImages = inputImages.length > 0;
|
|
2260
|
+
const modelInfo = getModel(currentModel);
|
|
2261
|
+
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
2262
|
+
const userContent = buildUserContentWithAttachments(fullPrompt, inputImages, modelSupportsImages);
|
|
2263
|
+
// Show the typed command as the user message
|
|
2264
|
+
const userItem = {
|
|
2265
|
+
kind: "user",
|
|
2266
|
+
text: trimmed,
|
|
2267
|
+
imageCount: hasImages ? inputImages.length : undefined,
|
|
2268
|
+
id: getId(),
|
|
2269
|
+
};
|
|
2270
|
+
setLastUserMessage(trimmed);
|
|
2271
|
+
setDoneStatus(null);
|
|
2272
|
+
setLiveItems([userItem]);
|
|
2273
|
+
// Send the full prompt to the agent, with user args appended if provided
|
|
2274
|
+
try {
|
|
2275
|
+
await agentLoop.run(userContent);
|
|
1912
2276
|
}
|
|
2277
|
+
catch (err) {
|
|
2278
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2279
|
+
log("ERROR", "error", msg);
|
|
2280
|
+
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2281
|
+
setLiveItems((prev) => [
|
|
2282
|
+
...prev,
|
|
2283
|
+
isAbort
|
|
2284
|
+
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2285
|
+
: toErrorItem(err, getId()),
|
|
2286
|
+
]);
|
|
2287
|
+
}
|
|
2288
|
+
// Reload custom commands in case a setup command created new ones
|
|
2289
|
+
reloadCustomCommands();
|
|
2290
|
+
return;
|
|
1913
2291
|
}
|
|
1914
2292
|
// Check slash commands
|
|
1915
2293
|
if (props.onSlashCommand && input.startsWith("/")) {
|
|
@@ -1923,47 +2301,7 @@ export function App(props) {
|
|
|
1923
2301
|
const hasImages = inputImages.length > 0;
|
|
1924
2302
|
const modelInfo = getModel(currentModel);
|
|
1925
2303
|
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
1926
|
-
|
|
1927
|
-
if (hasImages) {
|
|
1928
|
-
const parts = [];
|
|
1929
|
-
if (trimmed) {
|
|
1930
|
-
parts.push({ type: "text", text: trimmed });
|
|
1931
|
-
}
|
|
1932
|
-
for (const img of inputImages) {
|
|
1933
|
-
if (img.kind === "text") {
|
|
1934
|
-
parts.push({
|
|
1935
|
-
type: "text",
|
|
1936
|
-
text: `<file name="${img.fileName}">\n${img.data}\n</file>`,
|
|
1937
|
-
});
|
|
1938
|
-
}
|
|
1939
|
-
else if (modelSupportsImages) {
|
|
1940
|
-
parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
|
|
1941
|
-
}
|
|
1942
|
-
else {
|
|
1943
|
-
// GLM models: save image to temp file and instruct model to use vision MCP tool
|
|
1944
|
-
const ext = img.mediaType.split("/")[1] ?? "png";
|
|
1945
|
-
const tmpPath = `/tmp/ezcoder-img-${Date.now()}.${ext}`;
|
|
1946
|
-
try {
|
|
1947
|
-
writeFileSync(tmpPath, Buffer.from(img.data, "base64"));
|
|
1948
|
-
parts.push({
|
|
1949
|
-
type: "text",
|
|
1950
|
-
text: `[User attached an image saved at: ${tmpPath} — use the image_analysis tool to view and analyze it]`,
|
|
1951
|
-
});
|
|
1952
|
-
}
|
|
1953
|
-
catch {
|
|
1954
|
-
parts.push({
|
|
1955
|
-
type: "text",
|
|
1956
|
-
text: `[User attached an image but it could not be saved for analysis]`,
|
|
1957
|
-
});
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
// If only text parts remain after stripping images, simplify to plain string
|
|
1962
|
-
userContent = parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
|
|
1963
|
-
}
|
|
1964
|
-
else {
|
|
1965
|
-
userContent = input;
|
|
1966
|
-
}
|
|
2304
|
+
const userContent = buildUserContentWithAttachments(input, inputImages, modelSupportsImages);
|
|
1967
2305
|
// ── Queue message if agent is already running ──
|
|
1968
2306
|
if (agentLoop.isRunning) {
|
|
1969
2307
|
log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
|
|
@@ -2045,6 +2383,9 @@ export function App(props) {
|
|
|
2045
2383
|
agentLoop.clearQueue();
|
|
2046
2384
|
agentLoop.abort();
|
|
2047
2385
|
}
|
|
2386
|
+
else if (compactionAbortRef.current) {
|
|
2387
|
+
compactionAbortRef.current.abort();
|
|
2388
|
+
}
|
|
2048
2389
|
else {
|
|
2049
2390
|
handleDoubleExit();
|
|
2050
2391
|
}
|
|
@@ -2175,6 +2516,7 @@ export function App(props) {
|
|
|
2175
2516
|
};
|
|
2176
2517
|
const promptOrder = [
|
|
2177
2518
|
// Project audits / one-shot analysis
|
|
2519
|
+
"goal",
|
|
2178
2520
|
"init",
|
|
2179
2521
|
"research",
|
|
2180
2522
|
"scan",
|
|
@@ -2220,11 +2562,41 @@ export function App(props) {
|
|
|
2220
2562
|
case "tombstone":
|
|
2221
2563
|
return null;
|
|
2222
2564
|
case "banner":
|
|
2223
|
-
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount }, item.id));
|
|
2565
|
+
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount, goalCount: goalCount }, item.id));
|
|
2224
2566
|
case "user":
|
|
2225
2567
|
return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
|
|
2226
2568
|
case "task":
|
|
2227
2569
|
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "▶ " }), _jsx(Text, { color: theme.textDim, children: "Task: " }), _jsx(Text, { color: theme.success, children: item.title })] }) }, item.id));
|
|
2570
|
+
case "goal":
|
|
2571
|
+
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "▶ " }), _jsx(Text, { color: theme.textDim, children: "Goal: " }), _jsx(Text, { color: theme.success, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }) }, item.id));
|
|
2572
|
+
case "goal_progress": {
|
|
2573
|
+
const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
|
|
2574
|
+
const color = isError
|
|
2575
|
+
? theme.error
|
|
2576
|
+
: item.phase === "worker_finished"
|
|
2577
|
+
? theme.success
|
|
2578
|
+
: item.phase === "verifier_finished"
|
|
2579
|
+
? theme.accent
|
|
2580
|
+
: item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working"
|
|
2581
|
+
? theme.secondary
|
|
2582
|
+
: item.phase === "continuing"
|
|
2583
|
+
? theme.warning
|
|
2584
|
+
: item.phase === "verifier_started"
|
|
2585
|
+
? theme.accent
|
|
2586
|
+
: item.phase === "worker_started"
|
|
2587
|
+
? theme.primary
|
|
2588
|
+
: item.phase === "terminal"
|
|
2589
|
+
? theme.success
|
|
2590
|
+
: theme.primary;
|
|
2591
|
+
const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
|
|
2592
|
+
? "✓ "
|
|
2593
|
+
: item.phase === "terminal"
|
|
2594
|
+
? item.status === "passed"
|
|
2595
|
+
? "◆ "
|
|
2596
|
+
: "! "
|
|
2597
|
+
: "↻ ";
|
|
2598
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: glyph }), _jsx(Text, { color: color, bold: true, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }), item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.detail}` })) : null, item.summaryRows && item.summaryRows.length > 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, flexShrink: 1, children: item.summaryRows.map((row) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.textDim, children: row.label.padEnd(10) }), _jsx(Text, { color: theme.text, children: row.value }), row.detail ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", row.detail] }) : null] }, row.label))) })) : null] }, item.id));
|
|
2599
|
+
}
|
|
2228
2600
|
case "style_pack": {
|
|
2229
2601
|
const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
|
|
2230
2602
|
const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
|
|
@@ -2324,8 +2696,14 @@ export function App(props) {
|
|
|
2324
2696
|
// session creation is best-effort
|
|
2325
2697
|
}
|
|
2326
2698
|
}
|
|
2327
|
-
if (props.sessionStore)
|
|
2699
|
+
if (props.sessionStore) {
|
|
2328
2700
|
props.sessionStore.overlay = null;
|
|
2701
|
+
// Mirror through sessionStore so the post-resetUI remount can
|
|
2702
|
+
// re-seed the ref. markTaskDone() in the run-all onDone branch
|
|
2703
|
+
// reads currentTaskIdRef.current, which is seeded from this.
|
|
2704
|
+
props.sessionStore.currentTaskId = taskId;
|
|
2705
|
+
}
|
|
2706
|
+
currentTaskIdRef.current = taskId;
|
|
2329
2707
|
props.resetUI?.({
|
|
2330
2708
|
wipeSession: true,
|
|
2331
2709
|
messages: newMessages,
|
|
@@ -2337,7 +2715,9 @@ export function App(props) {
|
|
|
2337
2715
|
return;
|
|
2338
2716
|
}
|
|
2339
2717
|
// Fallback path (resetUI not wired — tests).
|
|
2340
|
-
|
|
2718
|
+
currentTaskIdRef.current = taskId;
|
|
2719
|
+
if (props.sessionStore)
|
|
2720
|
+
props.sessionStore.currentTaskId = taskId;
|
|
2341
2721
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2342
2722
|
setLiveItems([]);
|
|
2343
2723
|
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
@@ -2371,22 +2751,553 @@ export function App(props) {
|
|
|
2371
2751
|
setRunAllTasks(false);
|
|
2372
2752
|
}
|
|
2373
2753
|
})();
|
|
2754
|
+
}, [props.cwd, props.resetUI, props.sessionStore, agentLoop, currentProvider, currentModel]);
|
|
2755
|
+
const openOverlay = useCallback((kind) => {
|
|
2756
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2757
|
+
props.sessionStore.overlay = kind;
|
|
2758
|
+
if (kind !== "plan")
|
|
2759
|
+
props.sessionStore.planAutoExpand = false;
|
|
2760
|
+
props.resetUI();
|
|
2761
|
+
}
|
|
2762
|
+
else {
|
|
2763
|
+
if (props.sessionStore) {
|
|
2764
|
+
props.sessionStore.overlay = kind;
|
|
2765
|
+
if (kind !== "plan")
|
|
2766
|
+
props.sessionStore.planAutoExpand = false;
|
|
2767
|
+
if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
|
|
2768
|
+
props.sessionStore.pendingResetUI = true;
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
if (kind !== "plan")
|
|
2772
|
+
setPlanAutoExpand(false);
|
|
2773
|
+
setOverlay(kind);
|
|
2774
|
+
}
|
|
2775
|
+
}, [agentLoop.isRunning, props]);
|
|
2776
|
+
const closeOverlay = useCallback(() => {
|
|
2777
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2778
|
+
props.sessionStore.overlay = null;
|
|
2779
|
+
props.resetUI();
|
|
2780
|
+
}
|
|
2781
|
+
else {
|
|
2782
|
+
if (props.sessionStore) {
|
|
2783
|
+
props.sessionStore.overlay = null;
|
|
2784
|
+
}
|
|
2785
|
+
setOverlay(null);
|
|
2786
|
+
}
|
|
2787
|
+
}, [agentLoop.isRunning, overlay, props]);
|
|
2788
|
+
const runGoalSyntheticEvent = useCallback((eventText) => {
|
|
2789
|
+
const eventInfo = parseGoalSyntheticEvent(eventText);
|
|
2790
|
+
const detail = eventInfo?.kind === "worker"
|
|
2791
|
+
? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
|
|
2792
|
+
: `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
|
|
2793
|
+
if (agentRunningRef.current) {
|
|
2794
|
+
appendGoalProgress({
|
|
2795
|
+
kind: "goal_progress",
|
|
2796
|
+
phase: "orchestrator_reviewing",
|
|
2797
|
+
title: "Goal update queued for orchestrator",
|
|
2798
|
+
detail: `${detail} It will report back after the current turn.`,
|
|
2799
|
+
workerId: eventInfo?.worker,
|
|
2800
|
+
status: eventInfo?.status,
|
|
2801
|
+
});
|
|
2802
|
+
agentLoop.queueMessage(eventText);
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
appendGoalProgress({
|
|
2806
|
+
kind: "goal_progress",
|
|
2807
|
+
phase: "orchestrator_reviewing",
|
|
2808
|
+
title: "Orchestrator reviewing Goal update",
|
|
2809
|
+
detail,
|
|
2810
|
+
workerId: eventInfo?.worker,
|
|
2811
|
+
status: eventInfo?.status,
|
|
2812
|
+
});
|
|
2813
|
+
setLastUserMessage("");
|
|
2814
|
+
setDoneStatus(null);
|
|
2815
|
+
void agentLoop.run(eventText).catch((err) => {
|
|
2816
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2817
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2818
|
+
});
|
|
2819
|
+
}, [agentLoop, appendGoalProgress]);
|
|
2820
|
+
const continueGoalRun = useCallback((runId) => {
|
|
2821
|
+
void (async () => {
|
|
2822
|
+
const latestRun = await reconcileActiveGoalRuns(props.cwd, {
|
|
2823
|
+
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
2824
|
+
}).then(({ runs }) => runs.find((item) => item.id === runId) ?? null);
|
|
2825
|
+
if (!latestRun) {
|
|
2826
|
+
runningGoalIdsRef.current.delete(runId);
|
|
2827
|
+
clearGoalStatusEntry(runId);
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
const decision = decideGoalNextAction(latestRun);
|
|
2831
|
+
if (decision.kind === "wait")
|
|
2832
|
+
return;
|
|
2833
|
+
if (decision.kind === "terminal" ||
|
|
2834
|
+
decision.kind === "blocked" ||
|
|
2835
|
+
decision.kind === "pause") {
|
|
2836
|
+
const status = decision.kind === "terminal"
|
|
2837
|
+
? decision.status
|
|
2838
|
+
: decision.kind === "blocked"
|
|
2839
|
+
? "blocked"
|
|
2840
|
+
: "paused";
|
|
2841
|
+
const nextRun = {
|
|
2842
|
+
...latestRun,
|
|
2843
|
+
status,
|
|
2844
|
+
continueRequestedAt: undefined,
|
|
2845
|
+
blockers: decision.kind === "blocked" || decision.kind === "pause"
|
|
2846
|
+
? Array.from(new Set([...latestRun.blockers, decision.reason]))
|
|
2847
|
+
: latestRun.blockers,
|
|
2848
|
+
};
|
|
2849
|
+
await upsertGoalRun(props.cwd, nextRun);
|
|
2850
|
+
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2851
|
+
kind: "continuation_stopped",
|
|
2852
|
+
reason: decision.reason,
|
|
2853
|
+
content: `terminal=${status}`,
|
|
2854
|
+
});
|
|
2855
|
+
const terminalProgress = formatGoalTerminalProgress(nextRun);
|
|
2856
|
+
if (terminalProgress)
|
|
2857
|
+
appendGoalProgress(terminalProgress);
|
|
2858
|
+
runningGoalIdsRef.current.delete(runId);
|
|
2859
|
+
clearGoalStatusEntry(runId);
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
let runForNextAction = latestRun;
|
|
2863
|
+
if (latestRun.continueRequestedAt &&
|
|
2864
|
+
!listGoalWorkers(props.cwd).some((worker) => worker.status === "running") &&
|
|
2865
|
+
activeVerifierRunIdsRef.current.size === 0) {
|
|
2866
|
+
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2867
|
+
kind: "continuation_consumed",
|
|
2868
|
+
reason: `Continuation request consumed by ${decision.kind}.`,
|
|
2869
|
+
});
|
|
2870
|
+
runForNextAction = await upsertGoalRun(props.cwd, {
|
|
2871
|
+
...latestRun,
|
|
2872
|
+
continueRequestedAt: undefined,
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
appendGoalProgress({
|
|
2876
|
+
kind: "goal_progress",
|
|
2877
|
+
phase: "continuing",
|
|
2878
|
+
title: `Choosing next Goal step: ${latestRun.title}`,
|
|
2879
|
+
detail: "Latest result is recorded; starting the next worker task or verifier automatically.",
|
|
2880
|
+
status: latestRun.status,
|
|
2881
|
+
});
|
|
2882
|
+
upsertGoalStatusEntry({
|
|
2883
|
+
runId: latestRun.id,
|
|
2884
|
+
label: latestRun.title,
|
|
2885
|
+
phase: "orchestrating",
|
|
2886
|
+
startedAt: Date.now(),
|
|
2887
|
+
detail: "choosing next step",
|
|
2888
|
+
});
|
|
2889
|
+
startGoalRunRef.current(runForNextAction);
|
|
2890
|
+
})().catch((err) => {
|
|
2891
|
+
runningGoalIdsRef.current.delete(runId);
|
|
2892
|
+
clearGoalStatusEntry(runId);
|
|
2893
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2894
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2895
|
+
});
|
|
2896
|
+
}, [appendGoalProgress, clearGoalStatusEntry, props.cwd, upsertGoalStatusEntry]);
|
|
2897
|
+
const handleGoalWorkerComplete = useCallback((run, completion) => {
|
|
2898
|
+
const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
|
|
2899
|
+
completion.worker.goalTaskId;
|
|
2900
|
+
const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
|
|
2901
|
+
void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
|
|
2902
|
+
appendGoalProgress({
|
|
2903
|
+
kind: "goal_progress",
|
|
2904
|
+
phase: "worker_finished",
|
|
2905
|
+
title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
|
|
2906
|
+
detail: summarizeGoalCompletion(completion.summary),
|
|
2907
|
+
workerId: completion.worker.id,
|
|
2908
|
+
status: completion.status,
|
|
2909
|
+
});
|
|
2910
|
+
upsertGoalStatusEntry({
|
|
2911
|
+
runId: run.id,
|
|
2912
|
+
label: taskTitle,
|
|
2913
|
+
phase: completion.status === "done" ? "reviewing" : "failed",
|
|
2914
|
+
startedAt: Date.now(),
|
|
2915
|
+
detail: completion.status === "done" ? "reviewing result" : "task failed",
|
|
2916
|
+
workerId: completion.worker.id,
|
|
2917
|
+
goalNumber: goalNumberForRun(run.id),
|
|
2918
|
+
});
|
|
2919
|
+
runGoalSyntheticEvent(eventText);
|
|
2920
|
+
void (async () => {
|
|
2921
|
+
if (listGoalWorkers(completion.worker.cwd).some((worker) => worker.status === "running"))
|
|
2922
|
+
return;
|
|
2923
|
+
if (activeVerifierRunIdsRef.current.size > 0)
|
|
2924
|
+
return;
|
|
2925
|
+
const runs = await loadGoalRuns(completion.worker.cwd);
|
|
2926
|
+
const queued = runs.find((item) => item.continueRequestedAt && !goalHasBlockingPrerequisites(item));
|
|
2927
|
+
if (queued)
|
|
2928
|
+
setTimeout(() => continueGoalRun(queued.id), 750);
|
|
2929
|
+
})().catch((err) => log("ERROR", "goal", err instanceof Error ? err.message : String(err)));
|
|
2930
|
+
}, [
|
|
2931
|
+
appendGoalProgress,
|
|
2932
|
+
continueGoalRun,
|
|
2933
|
+
goalNumberForRun,
|
|
2934
|
+
runGoalSyntheticEvent,
|
|
2935
|
+
upsertGoalStatusEntry,
|
|
2936
|
+
]);
|
|
2937
|
+
useEffect(() => {
|
|
2938
|
+
return subscribeGoalWorkerCompletions((completion) => {
|
|
2939
|
+
void (async () => {
|
|
2940
|
+
const latestRun = (await loadGoalRuns(completion.worker.cwd)).find((item) => item.id === completion.worker.goalRunId) ?? null;
|
|
2941
|
+
if (!latestRun) {
|
|
2942
|
+
log("WARN", "goal", `Worker completion for unknown Goal ${completion.worker.goalRunId}`);
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
runningGoalIdsRef.current.add(latestRun.id);
|
|
2946
|
+
handleGoalWorkerComplete(latestRun, completion);
|
|
2947
|
+
})().catch((err) => {
|
|
2948
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2949
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2950
|
+
});
|
|
2951
|
+
}, props.cwd);
|
|
2952
|
+
}, [handleGoalWorkerComplete, props.cwd]);
|
|
2953
|
+
const startGoalRun = useCallback((run) => {
|
|
2954
|
+
runningGoalIdsRef.current.add(run.id);
|
|
2955
|
+
void (async () => {
|
|
2956
|
+
if (goalHasBlockingPrerequisites(run)) {
|
|
2957
|
+
setOverlay(null);
|
|
2958
|
+
const detail = formatGoalBlockingPrerequisites(run);
|
|
2959
|
+
await upsertGoalRun(props.cwd, {
|
|
2960
|
+
...run,
|
|
2961
|
+
status: "blocked",
|
|
2962
|
+
blockers: Array.from(new Set([...run.blockers, detail])),
|
|
2963
|
+
});
|
|
2964
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2965
|
+
appendGoalProgress({
|
|
2966
|
+
kind: "goal_progress",
|
|
2967
|
+
phase: "terminal",
|
|
2968
|
+
title: `Goal blocked: ${run.title}`,
|
|
2969
|
+
detail,
|
|
2970
|
+
status: "blocked",
|
|
2971
|
+
});
|
|
2972
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
2973
|
+
clearGoalStatusEntry(run.id);
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
const decision = decideGoalNextAction(run);
|
|
2977
|
+
await appendGoalDecision(props.cwd, run.id, decision);
|
|
2978
|
+
if (decision.kind === "terminal") {
|
|
2979
|
+
const terminalProgress = formatGoalTerminalProgress(run);
|
|
2980
|
+
if (terminalProgress)
|
|
2981
|
+
appendGoalProgress(terminalProgress);
|
|
2982
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
2983
|
+
clearGoalStatusEntry(run.id);
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
if (decision.kind === "wait") {
|
|
2987
|
+
appendGoalProgress({
|
|
2988
|
+
kind: "goal_progress",
|
|
2989
|
+
phase: "worker_started",
|
|
2990
|
+
title: decision.workerId ? `Goal working: ${run.title}` : `Goal active: ${run.title}`,
|
|
2991
|
+
detail: decision.reason,
|
|
2992
|
+
workerId: decision.workerId,
|
|
2993
|
+
});
|
|
2994
|
+
upsertGoalStatusEntry({
|
|
2995
|
+
runId: run.id,
|
|
2996
|
+
label: run.title,
|
|
2997
|
+
phase: decision.workerId ? "worker" : "orchestrating",
|
|
2998
|
+
startedAt: Date.now(),
|
|
2999
|
+
detail: decision.reason,
|
|
3000
|
+
workerId: decision.workerId,
|
|
3001
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3002
|
+
});
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
if (decision.kind === "complete") {
|
|
3006
|
+
await upsertGoalRun(props.cwd, { ...run, status: "passed" });
|
|
3007
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3008
|
+
appendGoalProgress({
|
|
3009
|
+
kind: "goal_progress",
|
|
3010
|
+
phase: "terminal",
|
|
3011
|
+
title: `Goal passed: ${run.title}`,
|
|
3012
|
+
detail: decision.reason,
|
|
3013
|
+
status: "passed",
|
|
3014
|
+
});
|
|
3015
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3016
|
+
clearGoalStatusEntry(run.id);
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
if (decision.kind === "run_verifier") {
|
|
3020
|
+
await verifyGoalRun(run);
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
if (decision.kind === "create_task") {
|
|
3024
|
+
await updateGoalTask(props.cwd, run.id, `auto-${Date.now()}`, {
|
|
3025
|
+
title: decision.title,
|
|
3026
|
+
prompt: decision.prompt,
|
|
3027
|
+
status: "pending",
|
|
3028
|
+
});
|
|
3029
|
+
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
3030
|
+
await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
|
|
3031
|
+
setTimeout(() => continueGoalRun(run.id), 250);
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
if (decision.kind === "blocked") {
|
|
3035
|
+
await upsertGoalRun(props.cwd, {
|
|
3036
|
+
...run,
|
|
3037
|
+
status: "blocked",
|
|
3038
|
+
blockers: [...run.blockers, decision.reason],
|
|
3039
|
+
});
|
|
3040
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3041
|
+
appendGoalProgress({
|
|
3042
|
+
kind: "goal_progress",
|
|
3043
|
+
phase: "terminal",
|
|
3044
|
+
title: `Goal blocked: ${run.title}`,
|
|
3045
|
+
detail: decision.reason,
|
|
3046
|
+
status: "blocked",
|
|
3047
|
+
});
|
|
3048
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3049
|
+
clearGoalStatusEntry(run.id);
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
if (decision.kind === "pause") {
|
|
3053
|
+
await updateGoalTask(props.cwd, run.id, decision.task.id, {
|
|
3054
|
+
status: "blocked",
|
|
3055
|
+
attempts: decision.attempts,
|
|
3056
|
+
lastSummary: "Paused after worker attempt limit.",
|
|
3057
|
+
});
|
|
3058
|
+
await upsertGoalRun(props.cwd, {
|
|
3059
|
+
...run,
|
|
3060
|
+
status: "paused",
|
|
3061
|
+
blockers: [...run.blockers, decision.reason],
|
|
3062
|
+
});
|
|
3063
|
+
await appendGoalEvidence(props.cwd, run.id, {
|
|
3064
|
+
kind: "summary",
|
|
3065
|
+
label: "Goal paused",
|
|
3066
|
+
content: decision.reason,
|
|
3067
|
+
});
|
|
3068
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3069
|
+
appendGoalProgress({
|
|
3070
|
+
kind: "goal_progress",
|
|
3071
|
+
phase: "terminal",
|
|
3072
|
+
title: `Goal paused: ${run.title}`,
|
|
3073
|
+
detail: decision.reason,
|
|
3074
|
+
status: "paused",
|
|
3075
|
+
});
|
|
3076
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3077
|
+
clearGoalStatusEntry(run.id);
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
await updateGoalTask(props.cwd, run.id, decision.task.id, { attempts: decision.attempts });
|
|
3081
|
+
const worker = await startGoalWorker({
|
|
3082
|
+
cwd: props.cwd,
|
|
3083
|
+
provider: currentProvider,
|
|
3084
|
+
model: currentModel,
|
|
3085
|
+
goalRunId: run.id,
|
|
3086
|
+
goalTaskId: decision.task.id,
|
|
3087
|
+
taskTitle: decision.task.title,
|
|
3088
|
+
prompt: decision.task.prompt,
|
|
3089
|
+
});
|
|
3090
|
+
await upsertGoalRun(props.cwd, {
|
|
3091
|
+
...run,
|
|
3092
|
+
status: "running",
|
|
3093
|
+
activeWorkerId: worker.id,
|
|
3094
|
+
continueRequestedAt: undefined,
|
|
3095
|
+
});
|
|
3096
|
+
setOverlay(null);
|
|
3097
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3098
|
+
appendGoalProgress({
|
|
3099
|
+
kind: "goal_progress",
|
|
3100
|
+
phase: "worker_started",
|
|
3101
|
+
title: `Worker started: ${decision.task.title}`,
|
|
3102
|
+
detail: "Task is running in the background.",
|
|
3103
|
+
workerId: worker.id,
|
|
3104
|
+
status: worker.status,
|
|
3105
|
+
});
|
|
3106
|
+
upsertGoalStatusEntry({
|
|
3107
|
+
runId: run.id,
|
|
3108
|
+
label: decision.task.title,
|
|
3109
|
+
phase: "worker",
|
|
3110
|
+
startedAt: Date.now(),
|
|
3111
|
+
detail: "background worker running",
|
|
3112
|
+
workerId: worker.id,
|
|
3113
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3114
|
+
});
|
|
3115
|
+
})().catch((err) => {
|
|
3116
|
+
clearGoalStatusEntry(run.id);
|
|
3117
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3118
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3119
|
+
});
|
|
2374
3120
|
}, [
|
|
2375
3121
|
props.cwd,
|
|
2376
|
-
props.resetUI,
|
|
2377
|
-
props.sessionStore,
|
|
2378
|
-
stdout,
|
|
2379
|
-
agentLoop,
|
|
2380
3122
|
currentProvider,
|
|
2381
3123
|
currentModel,
|
|
3124
|
+
appendGoalProgress,
|
|
3125
|
+
clearGoalStatusEntry,
|
|
3126
|
+
goalNumberForRun,
|
|
3127
|
+
upsertGoalStatusEntry,
|
|
3128
|
+
]);
|
|
3129
|
+
const verifyGoalRun = useCallback(async (run) => {
|
|
3130
|
+
if (!run.verifier?.command) {
|
|
3131
|
+
await appendGoalEvidence(props.cwd, run.id, {
|
|
3132
|
+
kind: "summary",
|
|
3133
|
+
label: "Missing verifier",
|
|
3134
|
+
content: "No verifier command is configured.",
|
|
3135
|
+
});
|
|
3136
|
+
await upsertGoalRun(props.cwd, {
|
|
3137
|
+
...run,
|
|
3138
|
+
status: "blocked",
|
|
3139
|
+
blockers: [...run.blockers, "No verifier command configured."],
|
|
3140
|
+
});
|
|
3141
|
+
appendGoalProgress({
|
|
3142
|
+
kind: "goal_progress",
|
|
3143
|
+
phase: "terminal",
|
|
3144
|
+
title: `Goal blocked: ${run.title}`,
|
|
3145
|
+
detail: "No verifier command is configured.",
|
|
3146
|
+
status: "blocked",
|
|
3147
|
+
});
|
|
3148
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3149
|
+
clearGoalStatusEntry(run.id);
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
activeVerifierRunIdsRef.current.add(run.id);
|
|
3153
|
+
await upsertGoalRun(props.cwd, {
|
|
3154
|
+
...run,
|
|
3155
|
+
status: "verifying",
|
|
3156
|
+
continueRequestedAt: undefined,
|
|
3157
|
+
});
|
|
3158
|
+
appendGoalProgress({
|
|
3159
|
+
kind: "goal_progress",
|
|
3160
|
+
phase: "verifier_started",
|
|
3161
|
+
title: `Verifier started: ${run.title}`,
|
|
3162
|
+
detail: run.verifier.command,
|
|
3163
|
+
status: "verifying",
|
|
3164
|
+
});
|
|
3165
|
+
const startedAt = Date.now();
|
|
3166
|
+
const verifierTimeoutMs = Number(process.env.GG_GOAL_VERIFIER_TIMEOUT_MS ?? 10 * 60 * 1000);
|
|
3167
|
+
upsertGoalStatusEntry({
|
|
3168
|
+
runId: run.id,
|
|
3169
|
+
label: run.title,
|
|
3170
|
+
phase: "verifier",
|
|
3171
|
+
startedAt,
|
|
3172
|
+
detail: run.verifier.command,
|
|
3173
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3174
|
+
});
|
|
3175
|
+
void runGoalVerifierCommand({
|
|
3176
|
+
cwd: props.cwd,
|
|
3177
|
+
runId: run.id,
|
|
3178
|
+
command: run.verifier.command,
|
|
3179
|
+
timeoutMs: verifierTimeoutMs,
|
|
3180
|
+
now: () => startedAt,
|
|
3181
|
+
})
|
|
3182
|
+
.then(async ({ verification, failureClass, durationMs }) => {
|
|
3183
|
+
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3184
|
+
const status = verification.status;
|
|
3185
|
+
const summary = verification.summary;
|
|
3186
|
+
const outputPath = verification.outputPath;
|
|
3187
|
+
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
3188
|
+
const runWithVerifier = {
|
|
3189
|
+
...latestRun,
|
|
3190
|
+
verifier: {
|
|
3191
|
+
...latestRun.verifier,
|
|
3192
|
+
description: latestRun.verifier?.description ?? "Goal verifier",
|
|
3193
|
+
command: run.verifier?.command,
|
|
3194
|
+
lastResult: verification,
|
|
3195
|
+
},
|
|
3196
|
+
};
|
|
3197
|
+
const completionCheck = canCompleteGoalRun(runWithVerifier);
|
|
3198
|
+
const verifiedRun = await upsertGoalRun(props.cwd, {
|
|
3199
|
+
...runWithVerifier,
|
|
3200
|
+
continueRequestedAt: undefined,
|
|
3201
|
+
status: status === "pass" && completionCheck.ok
|
|
3202
|
+
? "passed"
|
|
3203
|
+
: status === "pass"
|
|
3204
|
+
? "ready"
|
|
3205
|
+
: "failed",
|
|
3206
|
+
});
|
|
3207
|
+
await appendGoalEvidence(props.cwd, run.id, {
|
|
3208
|
+
kind: "command",
|
|
3209
|
+
label: `Verifier ${status}`,
|
|
3210
|
+
content: `${failureClass}: ${summary}`.slice(0, 4000),
|
|
3211
|
+
path: outputPath,
|
|
3212
|
+
});
|
|
3213
|
+
await appendGoalDecision(props.cwd, run.id, {
|
|
3214
|
+
kind: `verifier_${status}`,
|
|
3215
|
+
reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
|
|
3216
|
+
content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
|
|
3217
|
+
});
|
|
3218
|
+
if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
|
|
3219
|
+
await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
|
|
3220
|
+
title: "Fix verifier failure",
|
|
3221
|
+
prompt: `Goal verifier failed after ${durationMs}ms. Original goal: ${run.goal}\n\n` +
|
|
3222
|
+
`Verifier command: ${run.verifier?.command}\n\n` +
|
|
3223
|
+
`Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
|
|
3224
|
+
status: "pending",
|
|
3225
|
+
});
|
|
3226
|
+
const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
|
|
3227
|
+
await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
|
|
3228
|
+
}
|
|
3229
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3230
|
+
appendGoalProgress({
|
|
3231
|
+
kind: "goal_progress",
|
|
3232
|
+
phase: "verifier_finished",
|
|
3233
|
+
title: `Verifier ${status}: ${run.title}`,
|
|
3234
|
+
detail: summarizeGoalCompletion(summary),
|
|
3235
|
+
status,
|
|
3236
|
+
});
|
|
3237
|
+
upsertGoalStatusEntry({
|
|
3238
|
+
runId: run.id,
|
|
3239
|
+
label: run.title,
|
|
3240
|
+
phase: status === "pass" ? "reviewing" : "failed",
|
|
3241
|
+
startedAt: Date.now(),
|
|
3242
|
+
detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
|
|
3243
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3244
|
+
});
|
|
3245
|
+
const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
|
|
3246
|
+
runGoalSyntheticEvent(eventText);
|
|
3247
|
+
const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
|
|
3248
|
+
if (continuationRun?.continueRequestedAt && status === "pass") {
|
|
3249
|
+
setTimeout(() => continueGoalRun(run.id), 500);
|
|
3250
|
+
}
|
|
3251
|
+
})
|
|
3252
|
+
.catch((err) => {
|
|
3253
|
+
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3254
|
+
clearGoalStatusEntry(run.id);
|
|
3255
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3256
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
|
|
3257
|
+
});
|
|
3258
|
+
}, [
|
|
3259
|
+
props.cwd,
|
|
3260
|
+
appendGoalProgress,
|
|
3261
|
+
clearGoalStatusEntry,
|
|
3262
|
+
goalNumberForRun,
|
|
3263
|
+
runGoalSyntheticEvent,
|
|
3264
|
+
upsertGoalStatusEntry,
|
|
2382
3265
|
]);
|
|
3266
|
+
const pauseGoalRun = useCallback((run) => {
|
|
3267
|
+
void (async () => {
|
|
3268
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3269
|
+
if (run.activeWorkerId)
|
|
3270
|
+
await stopGoalWorker(run.activeWorkerId);
|
|
3271
|
+
await upsertGoalRun(props.cwd, { ...run, status: "paused", activeWorkerId: undefined });
|
|
3272
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3273
|
+
appendGoalProgress({
|
|
3274
|
+
kind: "goal_progress",
|
|
3275
|
+
phase: "terminal",
|
|
3276
|
+
title: `Goal paused: ${run.title}`,
|
|
3277
|
+
detail: "Auto-continuation stopped until resumed.",
|
|
3278
|
+
status: "paused",
|
|
3279
|
+
});
|
|
3280
|
+
clearGoalStatusEntry(run.id);
|
|
3281
|
+
})().catch((err) => {
|
|
3282
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3283
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3284
|
+
});
|
|
3285
|
+
}, [appendGoalProgress, clearGoalStatusEntry, props.cwd]);
|
|
2383
3286
|
// Keep refs in sync for access from stale closures (onDone)
|
|
2384
3287
|
startTaskRef.current = startTask;
|
|
3288
|
+
startGoalRunRef.current = startGoalRun;
|
|
2385
3289
|
useEffect(() => {
|
|
2386
3290
|
runAllTasksRef.current = runAllTasks;
|
|
2387
|
-
if (props.sessionStore)
|
|
3291
|
+
if (props.sessionStore) {
|
|
2388
3292
|
props.sessionStore.runAllTasks = runAllTasks;
|
|
3293
|
+
// Keep currentTaskId in sync with the ref so a sessionStore-seeded
|
|
3294
|
+
// remount picks up the same value the imperative sites wrote.
|
|
3295
|
+
props.sessionStore.currentTaskId = currentTaskIdRef.current;
|
|
3296
|
+
}
|
|
2389
3297
|
}, [runAllTasks, props.sessionStore]);
|
|
3298
|
+
useEffect(() => {
|
|
3299
|
+
agentRunningRef.current = agentLoop.isRunning;
|
|
3300
|
+
}, [agentLoop.isRunning]);
|
|
2390
3301
|
const startPixelFix = useCallback((errorId) => {
|
|
2391
3302
|
void (async () => {
|
|
2392
3303
|
try {
|
|
@@ -2437,14 +3348,10 @@ export function App(props) {
|
|
|
2437
3348
|
activeLanguages: detectedForPixelFix,
|
|
2438
3349
|
tools: toolsForPixelFix,
|
|
2439
3350
|
});
|
|
2440
|
-
// Now that the cwd swap is committed, reset chat.
|
|
2441
|
-
//
|
|
2442
|
-
// staticKey would print a second banner with the new cwd — leaving
|
|
2443
|
-
// two banners stacked in the scrollback.
|
|
2444
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
3351
|
+
// Now that the cwd swap is committed, reset chat. Do not clear the
|
|
3352
|
+
// terminal here; terminal clear sequences can erase saved scrollback.
|
|
2445
3353
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2446
3354
|
setLiveItems([]);
|
|
2447
|
-
setStaticKey((k) => k + 1);
|
|
2448
3355
|
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
2449
3356
|
agentLoop.reset();
|
|
2450
3357
|
persistedIndexRef.current = messagesRef.current.length;
|
|
@@ -2476,7 +3383,7 @@ export function App(props) {
|
|
|
2476
3383
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2477
3384
|
}
|
|
2478
3385
|
})();
|
|
2479
|
-
}, [props.cwd,
|
|
3386
|
+
}, [props.cwd, agentLoop, currentProvider, currentModel]);
|
|
2480
3387
|
startPixelFixRef.current = startPixelFix;
|
|
2481
3388
|
// Seed from sessionStore so "Fix All" chaining survives a deferred
|
|
2482
3389
|
// resetUI() if it fires between pixel fixes (e.g. user toggled a pane).
|
|
@@ -2487,12 +3394,30 @@ export function App(props) {
|
|
|
2487
3394
|
props.sessionStore.runAllPixel = runAllPixel;
|
|
2488
3395
|
}, [runAllPixel, props.sessionStore]);
|
|
2489
3396
|
const isTaskView = overlay === "tasks";
|
|
3397
|
+
const isGoalView = overlay === "goal";
|
|
2490
3398
|
const isSkillsView = overlay === "skills";
|
|
2491
3399
|
const isPlanView = overlay === "plan";
|
|
2492
3400
|
const isEyesView = overlay === "eyes";
|
|
3401
|
+
const footerStatusLayout = getFooterStatusLayoutDecision({
|
|
3402
|
+
columns,
|
|
3403
|
+
backgroundTaskCount: bgTasks.length,
|
|
3404
|
+
eyesCount,
|
|
3405
|
+
updatePending,
|
|
3406
|
+
});
|
|
2493
3407
|
const isPixelView = overlay === "pixel";
|
|
2494
|
-
const isOverlayView = isTaskView || isSkillsView || isPlanView || isEyesView || isPixelView;
|
|
2495
|
-
|
|
3408
|
+
const isOverlayView = isTaskView || isGoalView || isSkillsView || isPlanView || isEyesView || isPixelView;
|
|
3409
|
+
const shouldHideHistoryForOverlay = shouldHideHistoryForOverlayView(isOverlayView, agentLoop.isRunning);
|
|
3410
|
+
const stabilizeOverlayPaneRerender = shouldStabilizeOverlayPaneRerender({
|
|
3411
|
+
overlayPane: overlay,
|
|
3412
|
+
isAgentRunning: agentLoop.isRunning,
|
|
3413
|
+
});
|
|
3414
|
+
const staticItems = shouldHideStaticItemsForOverlayView({
|
|
3415
|
+
shouldHideHistoryForOverlay,
|
|
3416
|
+
stabilizeOverlayPaneRerender,
|
|
3417
|
+
})
|
|
3418
|
+
? []
|
|
3419
|
+
: history;
|
|
3420
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: staticItems, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, getStaticHistoryKey({ resizeKey })), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
2496
3421
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2497
3422
|
props.sessionStore.overlay = null;
|
|
2498
3423
|
props.resetUI();
|
|
@@ -2517,6 +3442,16 @@ export function App(props) {
|
|
|
2517
3442
|
markTaskInProgress(props.cwd, next.id);
|
|
2518
3443
|
startTask(next.title, next.prompt, next.id);
|
|
2519
3444
|
}
|
|
3445
|
+
} })) : isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
3446
|
+
void summarizeGoalCounts(props.cwd).then((counts) => setGoalCount(counts.active));
|
|
3447
|
+
closeOverlay();
|
|
3448
|
+
}, onRunGoal: (run) => {
|
|
3449
|
+
setOverlay(null);
|
|
3450
|
+
startGoalRun(run);
|
|
3451
|
+
}, onVerifyGoal: (run) => {
|
|
3452
|
+
void verifyGoalRun(run);
|
|
3453
|
+
}, onPauseGoal: (run) => {
|
|
3454
|
+
pauseGoalRun(run);
|
|
2520
3455
|
} })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
2521
3456
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2522
3457
|
props.sessionStore.overlay = null;
|
|
@@ -2632,10 +3567,8 @@ export function App(props) {
|
|
|
2632
3567
|
approvedPlanPathRef.current = planPath;
|
|
2633
3568
|
planStepsRef.current = steps;
|
|
2634
3569
|
setPlanSteps(steps);
|
|
2635
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2636
3570
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2637
3571
|
setLiveItems([]);
|
|
2638
|
-
setStaticKey((k) => k + 1);
|
|
2639
3572
|
setPlanAutoExpand(false);
|
|
2640
3573
|
setOverlay(null);
|
|
2641
3574
|
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
@@ -2676,8 +3609,6 @@ export function App(props) {
|
|
|
2676
3609
|
});
|
|
2677
3610
|
return;
|
|
2678
3611
|
}
|
|
2679
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2680
|
-
setStaticKey((k) => k + 1);
|
|
2681
3612
|
setPlanAutoExpand(false);
|
|
2682
3613
|
setOverlay(null);
|
|
2683
3614
|
setDoneStatus(null);
|
|
@@ -2692,50 +3623,17 @@ export function App(props) {
|
|
|
2692
3623
|
});
|
|
2693
3624
|
} })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 1, children: [liveItems.map((item) => renderItem(item)), _jsx(StreamingArea, { isRunning: agentLoop.isRunning, streamingText: agentLoop.streamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, planMode: planMode })] }), agentLoop.isRunning && agentLoop.activityPhase !== "idle" ? (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: agentLoop.activityPhase === "thinking" ? THINKING_BORDER_COLORS[0] : "transparent", paddingLeft: 1, paddingRight: 1, width: columns, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, thinkingMs: agentLoop.thinkingMs, isThinking: agentLoop.isThinking, thinkingEnabled: thinkingEnabled, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, userMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), planMode: planMode, retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, staticDisplay: true }) })) : agentLoop.stallError ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.warning, children: "⚠ API provider stream interrupted — retries exhausted." }), _jsx(Text, { color: theme.textDim, children: " Your conversation is preserved. Send a message to continue." })] })) : (doneStatus &&
|
|
2694
3625
|
!agentLoop.isRunning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), agentLoop.queuedCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.accent, children: ["⏳ ", agentLoop.queuedCount, " message", agentLoop.queuedCount > 1 ? "s" : "", " queued"] }) })), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleTasks: () => {
|
|
2695
|
-
// While the agent is running, skip the screen-clear + staticKey
|
|
2696
|
-
// bump that would otherwise wipe the chat history from scrollback.
|
|
2697
3626
|
// Just flip the overlay state — Ink's log-update handles the
|
|
2698
3627
|
// live-area transition (chat input → TaskOverlay) natively, and
|
|
2699
3628
|
// the chat history above stays in scrollback. When the overlay
|
|
2700
3629
|
// closes, the history is still there (banner included).
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
}
|
|
2705
|
-
else {
|
|
2706
|
-
if (props.sessionStore) {
|
|
2707
|
-
props.sessionStore.overlay = "tasks";
|
|
2708
|
-
if (agentLoop.isRunning)
|
|
2709
|
-
props.sessionStore.pendingResetUI = true;
|
|
2710
|
-
}
|
|
2711
|
-
setOverlay("tasks");
|
|
2712
|
-
}
|
|
3630
|
+
openOverlay("tasks");
|
|
3631
|
+
}, onToggleGoal: () => {
|
|
3632
|
+
openOverlay("goal");
|
|
2713
3633
|
}, onToggleSkills: () => {
|
|
2714
|
-
|
|
2715
|
-
props.sessionStore.overlay = "skills";
|
|
2716
|
-
props.resetUI();
|
|
2717
|
-
}
|
|
2718
|
-
else {
|
|
2719
|
-
if (props.sessionStore) {
|
|
2720
|
-
props.sessionStore.overlay = "skills";
|
|
2721
|
-
if (agentLoop.isRunning)
|
|
2722
|
-
props.sessionStore.pendingResetUI = true;
|
|
2723
|
-
}
|
|
2724
|
-
setOverlay("skills");
|
|
2725
|
-
}
|
|
3634
|
+
openOverlay("skills");
|
|
2726
3635
|
}, onTogglePixel: () => {
|
|
2727
|
-
|
|
2728
|
-
props.sessionStore.overlay = "pixel";
|
|
2729
|
-
props.resetUI();
|
|
2730
|
-
}
|
|
2731
|
-
else {
|
|
2732
|
-
if (props.sessionStore) {
|
|
2733
|
-
props.sessionStore.overlay = "pixel";
|
|
2734
|
-
if (agentLoop.isRunning)
|
|
2735
|
-
props.sessionStore.pendingResetUI = true;
|
|
2736
|
-
}
|
|
2737
|
-
setOverlay("pixel");
|
|
2738
|
-
}
|
|
3636
|
+
openOverlay("pixel");
|
|
2739
3637
|
}, onTogglePlanMode: () => {
|
|
2740
3638
|
const next = !planMode;
|
|
2741
3639
|
setPlanMode(next);
|
|
@@ -2749,7 +3647,12 @@ export function App(props) {
|
|
|
2749
3647
|
id: getId(),
|
|
2750
3648
|
},
|
|
2751
3649
|
]);
|
|
2752
|
-
}, cwd: props.cwd, commands: allCommands, eyesCount: eyesCount }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : overlay === "theme" ? (_jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: () => setOverlay(null), currentTheme: theme.name })) : (_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, cwd: displayedCwd, gitBranch: gitBranch, thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined, planMode: planMode, exitPending: exitPending })
|
|
3650
|
+
}, cwd: props.cwd, commands: allCommands, eyesCount: eyesCount }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : overlay === "theme" ? (_jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: () => setOverlay(null), currentTheme: theme.name })) : (_jsxs(_Fragment, { children: [_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, cwd: displayedCwd, gitBranch: gitBranch, thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined, planMode: planMode, exitPending: exitPending }), !exitPending && _jsx(GoalStatusBar, { entries: goalStatusEntries })] })), (footerStatusLayout.hasBackgroundTasks ||
|
|
3651
|
+
footerStatusLayout.hasEyesSignals ||
|
|
3652
|
+
footerStatusLayout.hasUpdateNotice) && (_jsxs(Box, { flexDirection: footerStatusLayout.stack ? "column" : "row", width: columns, children: [footerStatusLayout.hasBackgroundTasks && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate, compact: footerStatusLayout.compactBackgroundTasks })), footerStatusLayout.hasEyesSignals && (_jsx(Box, { paddingLeft: footerStatusLayout.stack || bgTasks.length === 0 ? 1 : 2, paddingRight: 1, children: _jsx(Text, { color: theme.accent, bold: true, wrap: "truncate", children: `${eyesCount} eyes signal${eyesCount === 1 ? "" : "s"} · Run /eyes-improve to enhance EZ Coder` }) })), footerStatusLayout.hasUpdateNotice && (_jsx(Box, { paddingLeft: footerStatusLayout.stack ||
|
|
3653
|
+
(!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
|
|
3654
|
+
? 1
|
|
3655
|
+
: 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
|
|
2753
3656
|
}
|
|
2754
3657
|
function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
|
|
2755
3658
|
const status = enabled ? "on" : "off";
|