@prestyj/cli 4.3.201 → 4.3.207
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 +68 -51
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -2
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +6 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +9 -1
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts +1 -0
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +7 -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.d.ts +1 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +26 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-registry.test.js +64 -1
- package/dist/core/model-registry.test.js.map +1 -1
- package/dist/core/oauth/gemini.d.ts +4 -0
- package/dist/core/oauth/gemini.d.ts.map +1 -0
- package/dist/core/oauth/gemini.js +377 -0
- package/dist/core/oauth/gemini.js.map +1 -0
- package/dist/core/oauth/gemini.test.d.ts +2 -0
- package/dist/core/oauth/gemini.test.d.ts.map +1 -0
- package/dist/core/oauth/gemini.test.js +154 -0
- package/dist/core/oauth/gemini.test.js.map +1 -0
- package/dist/core/oauth/openai.d.ts.map +1 -1
- package/dist/core/oauth/openai.js +7 -5
- package/dist/core/oauth/openai.js.map +1 -1
- package/dist/core/oauth/types.d.ts +1 -0
- package/dist/core/oauth/types.d.ts.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 +100 -0
- package/dist/core/process-manager-dev-server-repro.test.js.map +1 -0
- package/dist/core/process-manager.js +2 -2
- 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 +125 -0
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +38 -0
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/repomap.js +3 -1
- package/dist/core/repomap.js.map +1 -1
- package/dist/core/repomap.test.js +18 -0
- package/dist/core/repomap.test.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +11 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +21 -10
- package/dist/interactive.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +19 -39
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +131 -6
- package/dist/system-prompt.test.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/enter-plan.d.ts.map +1 -1
- package/dist/tools/enter-plan.js +1 -0
- package/dist/tools/enter-plan.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +2 -0
- package/dist/tools/find.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 +2 -0
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +6 -2
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/source-path.d.ts +9 -0
- package/dist/tools/source-path.d.ts.map +1 -0
- package/dist/tools/source-path.js +120 -0
- package/dist/tools/source-path.js.map +1 -0
- package/dist/tools/source-path.test.d.ts +2 -0
- package/dist/tools/source-path.test.d.ts.map +1 -0
- package/dist/tools/source-path.test.js +80 -0
- package/dist/tools/source-path.test.js.map +1 -0
- package/dist/tools/subagent.js +16 -0
- package/dist/tools/subagent.js.map +1 -1
- package/dist/ui/App.d.ts +75 -4
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +1085 -152
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/activity-phrases.d.ts.map +1 -1
- package/dist/ui/activity-phrases.js +7 -3
- package/dist/ui/activity-phrases.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 +130 -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/ModelSelector.d.ts.map +1 -1
- package/dist/ui/components/ModelSelector.js +1 -0
- package/dist/ui/components/ModelSelector.js.map +1 -1
- package/dist/ui/components/ToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolExecution.js +96 -3
- 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 +293 -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 +276 -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 +2 -0
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +3 -0
- 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/login.d.ts.map +1 -1
- package/dist/ui/login.js +1 -0
- package/dist/ui/login.js.map +1 -1
- package/dist/ui/pixel.js +1 -1
- package/dist/ui/render.d.ts +13 -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 +47 -0
- package/dist/ui/slash-command-images.test.js.map +1 -0
- package/dist/utils/format.js +44 -0
- package/dist/utils/format.js.map +1 -1
- package/package.json +6 -5
package/dist/ui/App.js
CHANGED
|
@@ -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,10 @@ 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, projectDir, summarizeGoalCounts, summarizeGoalCountsFromRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
|
|
63
|
+
import { canCompleteGoalRun, decideGoalNextAction, shouldCreateVerifierFixTask, } from "../core/goal-controller.js";
|
|
64
|
+
import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
|
|
65
|
+
import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
|
|
60
66
|
/** Where ezcoder bugs should be reported. Surfaced in the guidance line. */
|
|
61
67
|
const GGCODER_BUG_REPORT_URL = "github.com/Gahroot/ezcoder/issues";
|
|
62
68
|
/**
|
|
@@ -90,6 +96,64 @@ function toErrorItem(err, id, contextPrefix) {
|
|
|
90
96
|
id,
|
|
91
97
|
};
|
|
92
98
|
}
|
|
99
|
+
export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS, customCommands = []) {
|
|
100
|
+
const trimmed = input.trim();
|
|
101
|
+
if (!trimmed.startsWith("/"))
|
|
102
|
+
return null;
|
|
103
|
+
const parts = trimmed.slice(1).split(" ");
|
|
104
|
+
const cmdName = parts[0];
|
|
105
|
+
const cmdArgs = parts.slice(1).join(" ").trim();
|
|
106
|
+
const builtinCmd = promptCommands.find((c) => c.name === cmdName || c.aliases.includes(cmdName));
|
|
107
|
+
const customCmd = !builtinCmd ? customCommands.find((c) => c.name === cmdName) : undefined;
|
|
108
|
+
const promptText = builtinCmd?.prompt ?? customCmd?.prompt;
|
|
109
|
+
if (!promptText)
|
|
110
|
+
return null;
|
|
111
|
+
return {
|
|
112
|
+
cmdName,
|
|
113
|
+
cmdArgs,
|
|
114
|
+
promptText,
|
|
115
|
+
fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
|
|
119
|
+
if (inputImages.length === 0)
|
|
120
|
+
return text;
|
|
121
|
+
const parts = [];
|
|
122
|
+
if (text) {
|
|
123
|
+
parts.push({ type: "text", text });
|
|
124
|
+
}
|
|
125
|
+
for (const img of inputImages) {
|
|
126
|
+
if (img.kind === "text") {
|
|
127
|
+
parts.push({
|
|
128
|
+
type: "text",
|
|
129
|
+
text: `<file name="${img.fileName}">\n${img.data}\n</file>`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else if (modelSupportsImages) {
|
|
133
|
+
parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// GLM models: save image to temp file and instruct model to use vision MCP tool
|
|
137
|
+
const ext = img.mediaType.split("/")[1] ?? "png";
|
|
138
|
+
const tmpPath = `/tmp/ezcoder-img-${Date.now()}.${ext}`;
|
|
139
|
+
try {
|
|
140
|
+
writeFileSync(tmpPath, Buffer.from(img.data, "base64"));
|
|
141
|
+
parts.push({
|
|
142
|
+
type: "text",
|
|
143
|
+
text: `[User attached an image saved at: ${tmpPath} — use the image_analysis tool to view and analyze it]`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
parts.push({
|
|
148
|
+
type: "text",
|
|
149
|
+
text: `[User attached an image but it could not be saved for analysis]`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// If only text parts remain after stripping images, simplify to plain string
|
|
155
|
+
return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
|
|
156
|
+
}
|
|
93
157
|
/** Tools that get aggregated into a single compact group when concurrent. */
|
|
94
158
|
const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
95
159
|
const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
|
|
@@ -114,6 +178,152 @@ function compactHistory(items) {
|
|
|
114
178
|
}
|
|
115
179
|
return compacted;
|
|
116
180
|
}
|
|
181
|
+
function summarizeGoalCompletion(summary) {
|
|
182
|
+
const lines = summary
|
|
183
|
+
.split("\n")
|
|
184
|
+
.map((line) => line.trim())
|
|
185
|
+
.filter((line) => line.length > 0 && line !== "[agent_done]");
|
|
186
|
+
const statusLine = lines.find((line) => /^Status:/i.test(line));
|
|
187
|
+
const changedLine = lines.find((line) => /^(Changed|Implemented|Fixed|Added|Key findings|Full verifier)/i.test(line));
|
|
188
|
+
const verificationLine = lines.find((line) => /^(Verification|Verified|Result):/i.test(line));
|
|
189
|
+
return statusLine ?? changedLine ?? verificationLine ?? lines[0];
|
|
190
|
+
}
|
|
191
|
+
function formatGoalWorkerFinishedTitle(taskTitle, status) {
|
|
192
|
+
return status === "done"
|
|
193
|
+
? `Worker finished: ${taskTitle}. Reporting back.`
|
|
194
|
+
: `Worker failed: ${taskTitle}. Reporting back.`;
|
|
195
|
+
}
|
|
196
|
+
function countGoalTasksByStatus(tasks, status) {
|
|
197
|
+
return tasks.filter((task) => task.status === status).length;
|
|
198
|
+
}
|
|
199
|
+
function firstText(values) {
|
|
200
|
+
return values.find((value) => value !== undefined && value.trim().length > 0)?.trim();
|
|
201
|
+
}
|
|
202
|
+
function truncateGoalSummary(value, maxLength = 90) {
|
|
203
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
204
|
+
if (normalized.length <= maxLength)
|
|
205
|
+
return normalized;
|
|
206
|
+
return `${normalized.slice(0, maxLength - 1)}…`;
|
|
207
|
+
}
|
|
208
|
+
export function buildGoalSummaryRows(run) {
|
|
209
|
+
const rows = [];
|
|
210
|
+
const doneTasks = countGoalTasksByStatus(run.tasks, "done");
|
|
211
|
+
const failedTasks = countGoalTasksByStatus(run.tasks, "failed");
|
|
212
|
+
const blockedTasks = countGoalTasksByStatus(run.tasks, "blocked");
|
|
213
|
+
const taskSuffix = [
|
|
214
|
+
failedTasks > 0 ? `${failedTasks} failed` : undefined,
|
|
215
|
+
blockedTasks > 0 ? `${blockedTasks} blocked` : undefined,
|
|
216
|
+
].filter((item) => item !== undefined);
|
|
217
|
+
rows.push({
|
|
218
|
+
label: "Tasks",
|
|
219
|
+
value: run.tasks.length > 0 ? `${doneTasks}/${run.tasks.length} done` : "none",
|
|
220
|
+
...(taskSuffix.length > 0 ? { detail: taskSuffix.join(", ") } : {}),
|
|
221
|
+
});
|
|
222
|
+
const verifierResult = run.verifier?.lastResult;
|
|
223
|
+
const verifierDetail = firstText([verifierResult?.outputPath, run.verifier?.command]);
|
|
224
|
+
rows.push({
|
|
225
|
+
label: "Verifier",
|
|
226
|
+
value: verifierResult?.status ?? (run.verifier?.command ? "ready" : "missing"),
|
|
227
|
+
...(verifierDetail ? { detail: truncateGoalSummary(verifierDetail) } : {}),
|
|
228
|
+
});
|
|
229
|
+
const latestEvidence = run.evidence.at(-1);
|
|
230
|
+
rows.push({
|
|
231
|
+
label: "Evidence",
|
|
232
|
+
value: `${run.evidence.length} recorded`,
|
|
233
|
+
...(latestEvidence
|
|
234
|
+
? { detail: truncateGoalSummary(latestEvidence.path ?? latestEvidence.label) }
|
|
235
|
+
: {}),
|
|
236
|
+
});
|
|
237
|
+
if (run.status === "blocked" || run.status === "paused" || run.blockers.length > 0) {
|
|
238
|
+
rows.push({
|
|
239
|
+
label: run.status === "paused" ? "Paused on" : "Blocked on",
|
|
240
|
+
value: truncateGoalSummary(goalHasBlockingPrerequisites(run)
|
|
241
|
+
? formatGoalBlockingPrerequisites(run)
|
|
242
|
+
: (run.blockers[0] ?? "manual review"), 110),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else if (run.successCriteria.length > 0) {
|
|
246
|
+
rows.push({
|
|
247
|
+
label: "Criteria",
|
|
248
|
+
value: `${run.successCriteria.length} checked`,
|
|
249
|
+
detail: truncateGoalSummary(run.successCriteria[0] ?? "", 80),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return rows.slice(0, 4);
|
|
253
|
+
}
|
|
254
|
+
export function formatGoalTerminalProgress(run) {
|
|
255
|
+
switch (run.status) {
|
|
256
|
+
case "passed":
|
|
257
|
+
return {
|
|
258
|
+
kind: "goal_progress",
|
|
259
|
+
phase: "terminal",
|
|
260
|
+
title: `Goal passed: ${run.title}`,
|
|
261
|
+
detail: "Verifier evidence is recorded; auto-continuation stopped.",
|
|
262
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
263
|
+
status: run.status,
|
|
264
|
+
};
|
|
265
|
+
case "failed":
|
|
266
|
+
return {
|
|
267
|
+
kind: "goal_progress",
|
|
268
|
+
phase: "terminal",
|
|
269
|
+
title: `Goal failed: ${run.title}`,
|
|
270
|
+
detail: "Auto-continuation stopped. Check Goal tasks for the failing step.",
|
|
271
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
272
|
+
status: run.status,
|
|
273
|
+
};
|
|
274
|
+
case "blocked":
|
|
275
|
+
return {
|
|
276
|
+
kind: "goal_progress",
|
|
277
|
+
phase: "terminal",
|
|
278
|
+
title: `Goal blocked: ${run.title}`,
|
|
279
|
+
detail: goalHasBlockingPrerequisites(run)
|
|
280
|
+
? formatGoalBlockingPrerequisites(run)
|
|
281
|
+
: (run.blockers[0] ?? "A prerequisite or missing verifier blocked progress."),
|
|
282
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
283
|
+
status: run.status,
|
|
284
|
+
};
|
|
285
|
+
case "paused":
|
|
286
|
+
return {
|
|
287
|
+
kind: "goal_progress",
|
|
288
|
+
phase: "terminal",
|
|
289
|
+
title: `Goal paused: ${run.title}`,
|
|
290
|
+
detail: run.blockers[0] ?? "Auto-continuation paused.",
|
|
291
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
292
|
+
status: run.status,
|
|
293
|
+
};
|
|
294
|
+
case "draft":
|
|
295
|
+
case "ready":
|
|
296
|
+
case "running":
|
|
297
|
+
case "verifying":
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export function shouldHideHistoryForOverlayView(isOverlayView, isAgentRunning) {
|
|
302
|
+
// Overlay panes should be clean, full-screen views like Tasks/Plans/Skills.
|
|
303
|
+
// When the agent is idle, pane open/close goes through resetUI(), so history
|
|
304
|
+
// remains in sessionStore and is reprinted after the pane closes. While the
|
|
305
|
+
// agent is running we keep Static mounted because resetUI would abort the run.
|
|
306
|
+
return isOverlayView && !isAgentRunning;
|
|
307
|
+
}
|
|
308
|
+
export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
|
|
309
|
+
return isAgentRunning && (overlayPane === "goal" || overlayPane === "plan");
|
|
310
|
+
}
|
|
311
|
+
export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender, }) {
|
|
312
|
+
return shouldHideHistoryForOverlay && !stabilizeOverlayPaneRerender;
|
|
313
|
+
}
|
|
314
|
+
export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, }) {
|
|
315
|
+
const shouldStabilize = isUserScrolled || hasTallLiveUserMessage;
|
|
316
|
+
return {
|
|
317
|
+
preserveStatic: shouldStabilize && hasNewOutput,
|
|
318
|
+
autoFollow: !shouldStabilize,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
export function isTallLiveUserMessage(text, rows) {
|
|
322
|
+
return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
|
|
323
|
+
}
|
|
324
|
+
export function getStaticHistoryKey({ resizeKey, staticKey, }) {
|
|
325
|
+
return `${resizeKey}-${staticKey}`;
|
|
326
|
+
}
|
|
117
327
|
// flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
|
|
118
328
|
/** Check whether an item is still active (running spinner, pending result). */
|
|
119
329
|
function isActiveItem(item) {
|
|
@@ -311,17 +521,16 @@ export function App(props) {
|
|
|
311
521
|
}
|
|
312
522
|
return [{ kind: "banner", id: "banner" }];
|
|
313
523
|
});
|
|
314
|
-
// Items from the current/last turn — rendered in the live area so they stay visible
|
|
315
|
-
|
|
524
|
+
// Items from the current/last turn — rendered in the live area so they stay visible.
|
|
525
|
+
// Seed from sessionStore so Goal progress/completion rows and other live output
|
|
526
|
+
// survive pane/overlay/resize remounts before they are flushed to <Static>.
|
|
527
|
+
const [liveItems, setLiveItems] = useState(() => props.sessionStore?.liveItems ?? []);
|
|
316
528
|
// overlay seeded from sessionStore (lives across remount). Falls back to
|
|
317
|
-
// props.initialOverlay (CLI launched with one)
|
|
318
|
-
|
|
319
|
-
// value already incorporated initialOverlay at session creation). Without
|
|
320
|
-
// this distinction, closing an overlay that was opened via initialOverlay
|
|
321
|
-
// (e.g. `ezcoder pixel`) re-asserts the overlay on resetUI remount because
|
|
322
|
-
// `null ?? "pixel"` is `"pixel"`.
|
|
323
|
-
const [overlay, setOverlay] = useState(props.sessionStore ? (props.sessionStore.overlay ?? null) : (props.initialOverlay ?? null));
|
|
529
|
+
// props.initialOverlay (CLI launched with one), then null.
|
|
530
|
+
const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
|
|
324
531
|
const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
|
|
532
|
+
const [goalCount, setGoalCount] = useState(0);
|
|
533
|
+
const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
|
|
325
534
|
const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
326
535
|
const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
|
|
327
536
|
// Seed from sessionStore so "Run All" chaining survives the resetUI()
|
|
@@ -329,13 +538,17 @@ export function App(props) {
|
|
|
329
538
|
const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
|
|
330
539
|
const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
|
|
331
540
|
const startTaskRef = useRef(() => { });
|
|
541
|
+
const agentRunningRef = useRef(false);
|
|
542
|
+
const runningGoalIdsRef = useRef(new Set());
|
|
543
|
+
const activeVerifierRunIdsRef = useRef(new Set());
|
|
544
|
+
const startGoalRunRef = useRef(() => { });
|
|
332
545
|
const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
|
|
333
546
|
const currentPixelFixRef = useRef(null);
|
|
334
547
|
const startPixelFixRef = useRef(() => { });
|
|
335
548
|
const cwdRef = useRef(props.cwd);
|
|
336
549
|
const [displayedCwd, setDisplayedCwd] = useState(props.cwd);
|
|
337
550
|
const [staticKey, setStaticKey] = useState(0);
|
|
338
|
-
const [doneStatus, setDoneStatus] = useState(null);
|
|
551
|
+
const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
|
|
339
552
|
// Suppress "done" status when a plan overlay is about to open
|
|
340
553
|
const planOverlayPendingRef = useRef(false);
|
|
341
554
|
const [gitBranch, setGitBranch] = useState(null);
|
|
@@ -368,9 +581,12 @@ export function App(props) {
|
|
|
368
581
|
// previous mount, triggering React's duplicate-key warning and causing
|
|
369
582
|
// duplicate/omitted renders.
|
|
370
583
|
const nextIdRef = useRef((() => {
|
|
371
|
-
const
|
|
584
|
+
const items = [
|
|
585
|
+
...(props.sessionStore?.history ?? props.initialHistory ?? []),
|
|
586
|
+
...(props.sessionStore?.liveItems ?? []),
|
|
587
|
+
];
|
|
372
588
|
let max = -1;
|
|
373
|
-
for (const item of
|
|
589
|
+
for (const item of items) {
|
|
374
590
|
const n = Number(item.id);
|
|
375
591
|
if (Number.isFinite(n) && n > max)
|
|
376
592
|
max = n;
|
|
@@ -406,7 +622,27 @@ export function App(props) {
|
|
|
406
622
|
* language-detection path when this cwd has never been audited before.
|
|
407
623
|
*/
|
|
408
624
|
const triggerAutoSetupRef = useRef(async () => { });
|
|
409
|
-
const getId = () =>
|
|
625
|
+
const getId = () => `ui-${nextIdRef.current++}`;
|
|
626
|
+
const appendGoalProgress = useCallback((item) => {
|
|
627
|
+
setLiveItems((prev) => [...prev, { ...item, id: getId() }]);
|
|
628
|
+
}, []);
|
|
629
|
+
const goalNumberForRun = useCallback((runId) => Math.max(1, goalStatusEntries.findIndex((entry) => entry.runId === runId) + 1), [goalStatusEntries]);
|
|
630
|
+
const clearGoalStatusEntry = useCallback((runId) => {
|
|
631
|
+
setGoalStatusEntries((prev) => {
|
|
632
|
+
const next = removeGoalStatusEntry(prev, runId);
|
|
633
|
+
if (props.sessionStore)
|
|
634
|
+
props.sessionStore.goalStatusEntries = next;
|
|
635
|
+
return next;
|
|
636
|
+
});
|
|
637
|
+
}, [props.sessionStore]);
|
|
638
|
+
const upsertGoalStatusEntry = useCallback((entry) => {
|
|
639
|
+
setGoalStatusEntries((prev) => {
|
|
640
|
+
const next = syncGoalStatusEntries(prev, entry);
|
|
641
|
+
if (props.sessionStore)
|
|
642
|
+
props.sessionStore.goalStatusEntries = next;
|
|
643
|
+
return next;
|
|
644
|
+
});
|
|
645
|
+
}, [props.sessionStore]);
|
|
410
646
|
// Two-phase flush: items waiting to be moved to Static history after the
|
|
411
647
|
// live area has been cleared and Ink has committed the smaller output.
|
|
412
648
|
const pendingFlushRef = useRef([]);
|
|
@@ -416,8 +652,12 @@ export function App(props) {
|
|
|
416
652
|
if (items.length === 0)
|
|
417
653
|
return;
|
|
418
654
|
pendingFlushRef.current = [...pendingFlushRef.current, ...items];
|
|
655
|
+
if (props.sessionStore) {
|
|
656
|
+
const queuedIds = new Set(items.map((item) => item.id));
|
|
657
|
+
props.sessionStore.liveItems = (props.sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
|
|
658
|
+
}
|
|
419
659
|
setFlushGeneration((g) => g + 1);
|
|
420
|
-
}, []);
|
|
660
|
+
}, [props.sessionStore]);
|
|
421
661
|
// Mirror runtime state choices (model/provider/thinking) into renderApp's
|
|
422
662
|
// closure so unmount/remount preserves them.
|
|
423
663
|
const onRuntimeStateChange = props.onRuntimeStateChange;
|
|
@@ -441,6 +681,14 @@ export function App(props) {
|
|
|
441
681
|
if (sessionStore)
|
|
442
682
|
sessionStore.history = history;
|
|
443
683
|
}, [history, sessionStore]);
|
|
684
|
+
useEffect(() => {
|
|
685
|
+
if (sessionStore)
|
|
686
|
+
sessionStore.liveItems = liveItems;
|
|
687
|
+
}, [liveItems, sessionStore]);
|
|
688
|
+
useEffect(() => {
|
|
689
|
+
if (sessionStore)
|
|
690
|
+
sessionStore.doneStatus = doneStatus;
|
|
691
|
+
}, [doneStatus, sessionStore]);
|
|
444
692
|
useEffect(() => {
|
|
445
693
|
if (sessionStore)
|
|
446
694
|
sessionStore.planSteps = planSteps;
|
|
@@ -453,6 +701,10 @@ export function App(props) {
|
|
|
453
701
|
if (sessionStore)
|
|
454
702
|
sessionStore.overlay = overlay;
|
|
455
703
|
}, [overlay, sessionStore]);
|
|
704
|
+
useEffect(() => {
|
|
705
|
+
if (sessionStore)
|
|
706
|
+
sessionStore.goalStatusEntries = goalStatusEntries;
|
|
707
|
+
}, [goalStatusEntries, sessionStore]);
|
|
456
708
|
// pendingAction is consumed via a useEffect AFTER agentLoop is created
|
|
457
709
|
// — see below where useAgentLoop is set up.
|
|
458
710
|
const pendingActionConsumedRef = useRef(false);
|
|
@@ -460,13 +712,44 @@ export function App(props) {
|
|
|
460
712
|
const currentCreds = props.credentialsByProvider?.[currentProvider];
|
|
461
713
|
const activeApiKey = currentCreds?.accessToken ?? props.apiKey;
|
|
462
714
|
const activeAccountId = currentCreds?.accountId ?? props.accountId;
|
|
463
|
-
const
|
|
715
|
+
const activeProjectId = currentCreds?.projectId ?? props.projectId;
|
|
716
|
+
const activeBaseUrl = currentProvider === "gemini" ? undefined : (currentCreds?.baseUrl ?? props.baseUrl);
|
|
464
717
|
const contextWindowOptions = useMemo(() => ({ provider: currentProvider, accountId: activeAccountId }), [currentProvider, activeAccountId]);
|
|
465
718
|
// Load git branch — re-runs whenever the displayed cwd changes (e.g. when
|
|
466
719
|
// a pixel fix moves the agent into a different project root).
|
|
467
720
|
useEffect(() => {
|
|
468
721
|
getGitBranch(displayedCwd).then(setGitBranch);
|
|
469
722
|
}, [displayedCwd]);
|
|
723
|
+
useEffect(() => {
|
|
724
|
+
let cancelled = false;
|
|
725
|
+
const refreshGoalCount = () => {
|
|
726
|
+
void reconcileActiveGoalRuns(props.cwd, {
|
|
727
|
+
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
728
|
+
}).then(({ runs }) => {
|
|
729
|
+
const counts = summarizeGoalCountsFromRuns(runs);
|
|
730
|
+
if (cancelled)
|
|
731
|
+
return;
|
|
732
|
+
setGoalCount(counts.active);
|
|
733
|
+
setGoalStatusEntries((prev) => {
|
|
734
|
+
const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
|
|
735
|
+
isWorkerActive: (workerId, run) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId &&
|
|
736
|
+
worker.goalRunId === run.id &&
|
|
737
|
+
worker.status === "running"),
|
|
738
|
+
isVerifierActive: (run) => activeVerifierRunIdsRef.current.has(run.id),
|
|
739
|
+
});
|
|
740
|
+
if (props.sessionStore)
|
|
741
|
+
props.sessionStore.goalStatusEntries = next;
|
|
742
|
+
return next;
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
};
|
|
746
|
+
refreshGoalCount();
|
|
747
|
+
const interval = setInterval(refreshGoalCount, 1000);
|
|
748
|
+
return () => {
|
|
749
|
+
cancelled = true;
|
|
750
|
+
clearInterval(interval);
|
|
751
|
+
};
|
|
752
|
+
}, [props.cwd]);
|
|
470
753
|
// Periodic update check during long sessions
|
|
471
754
|
useEffect(() => {
|
|
472
755
|
startPeriodicUpdateCheck(props.version, (msg) => {
|
|
@@ -716,7 +999,8 @@ export function App(props) {
|
|
|
716
999
|
});
|
|
717
1000
|
}
|
|
718
1001
|
}, [props.settingsFile]);
|
|
719
|
-
const
|
|
1002
|
+
const compactionAbortRef = useRef(null);
|
|
1003
|
+
const compactConversation = useCallback(async (messages, signal) => {
|
|
720
1004
|
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
721
1005
|
const tokensBefore = estimateConversationTokens(messages);
|
|
722
1006
|
const spinId = getId();
|
|
@@ -727,15 +1011,21 @@ export function App(props) {
|
|
|
727
1011
|
});
|
|
728
1012
|
// Show animated spinner
|
|
729
1013
|
setLiveItems((prev) => [...prev, { kind: "compacting", id: spinId }]);
|
|
1014
|
+
const ownedAbort = signal ? null : new AbortController();
|
|
1015
|
+
const compactionSignal = signal ?? ownedAbort?.signal;
|
|
1016
|
+
if (ownedAbort)
|
|
1017
|
+
compactionAbortRef.current = ownedAbort;
|
|
730
1018
|
try {
|
|
731
1019
|
// Resolve fresh credentials for compaction too
|
|
732
1020
|
let compactApiKey = activeApiKey;
|
|
733
1021
|
let compactAccountId = activeAccountId;
|
|
1022
|
+
let compactProjectId = activeProjectId;
|
|
734
1023
|
let compactBaseUrl = activeBaseUrl;
|
|
735
1024
|
if (props.authStorage) {
|
|
736
1025
|
const creds = await props.authStorage.resolveCredentials(currentProvider);
|
|
737
1026
|
compactApiKey = creds.accessToken;
|
|
738
1027
|
compactAccountId = creds.accountId;
|
|
1028
|
+
compactProjectId = creds.projectId;
|
|
739
1029
|
compactBaseUrl = creds.baseUrl ?? compactBaseUrl;
|
|
740
1030
|
}
|
|
741
1031
|
const result = await compact(messages, {
|
|
@@ -743,9 +1033,10 @@ export function App(props) {
|
|
|
743
1033
|
model: currentModel,
|
|
744
1034
|
apiKey: compactApiKey,
|
|
745
1035
|
accountId: compactAccountId,
|
|
1036
|
+
projectId: compactProjectId,
|
|
746
1037
|
baseUrl: compactBaseUrl,
|
|
747
1038
|
contextWindow,
|
|
748
|
-
signal:
|
|
1039
|
+
signal: compactionSignal,
|
|
749
1040
|
approvedPlanPath: approvedPlanPathRef.current,
|
|
750
1041
|
});
|
|
751
1042
|
if (result.result.compacted) {
|
|
@@ -770,16 +1061,23 @@ export function App(props) {
|
|
|
770
1061
|
}
|
|
771
1062
|
catch (err) {
|
|
772
1063
|
const msg = err instanceof Error ? err.message : String(err);
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
setLiveItems((prev) =>
|
|
776
|
-
|
|
1064
|
+
const isAbort = compactionSignal?.aborted || msg.includes("aborted") || msg.includes("abort");
|
|
1065
|
+
log(isAbort ? "WARN" : "ERROR", "compaction", isAbort ? "Compaction aborted" : `Compaction failed: ${msg}`);
|
|
1066
|
+
setLiveItems((prev) => isAbort
|
|
1067
|
+
? prev.filter((item) => item.id !== spinId)
|
|
1068
|
+
: prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
|
|
1069
|
+
return messages; // Return unchanged on failure/abort
|
|
1070
|
+
}
|
|
1071
|
+
finally {
|
|
1072
|
+
if (ownedAbort && compactionAbortRef.current === ownedAbort)
|
|
1073
|
+
compactionAbortRef.current = null;
|
|
777
1074
|
}
|
|
778
1075
|
}, [
|
|
779
1076
|
currentModel,
|
|
780
1077
|
currentProvider,
|
|
781
1078
|
activeApiKey,
|
|
782
1079
|
activeAccountId,
|
|
1080
|
+
activeProjectId,
|
|
783
1081
|
activeBaseUrl,
|
|
784
1082
|
contextWindowOptions,
|
|
785
1083
|
props.authStorage,
|
|
@@ -901,10 +1199,14 @@ export function App(props) {
|
|
|
901
1199
|
const resolveCredentials = useCallback(async (opts) => {
|
|
902
1200
|
if (props.authStorage) {
|
|
903
1201
|
const creds = await props.authStorage.resolveCredentials(currentProvider, opts);
|
|
904
|
-
return {
|
|
1202
|
+
return {
|
|
1203
|
+
apiKey: creds.accessToken,
|
|
1204
|
+
accountId: creds.accountId,
|
|
1205
|
+
projectId: creds.projectId,
|
|
1206
|
+
};
|
|
905
1207
|
}
|
|
906
|
-
return { apiKey: activeApiKey, accountId: activeAccountId };
|
|
907
|
-
}, [props.authStorage, currentProvider, activeApiKey, activeAccountId]);
|
|
1208
|
+
return { apiKey: activeApiKey, accountId: activeAccountId, projectId: activeProjectId };
|
|
1209
|
+
}, [props.authStorage, currentProvider, activeApiKey, activeAccountId, activeProjectId]);
|
|
908
1210
|
const agentLoop = useAgentLoop(messagesRef, {
|
|
909
1211
|
provider: currentProvider,
|
|
910
1212
|
model: currentModel,
|
|
@@ -915,6 +1217,7 @@ export function App(props) {
|
|
|
915
1217
|
apiKey: activeApiKey,
|
|
916
1218
|
baseUrl: activeBaseUrl,
|
|
917
1219
|
accountId: activeAccountId,
|
|
1220
|
+
projectId: activeProjectId,
|
|
918
1221
|
resolveCredentials,
|
|
919
1222
|
transformContext,
|
|
920
1223
|
}, {
|
|
@@ -1409,6 +1712,11 @@ export function App(props) {
|
|
|
1409
1712
|
}
|
|
1410
1713
|
}, 500);
|
|
1411
1714
|
}
|
|
1715
|
+
// Goal loop: after the orchestrator handles a worker/verifier event,
|
|
1716
|
+
// continue the same Goal automatically until it reaches a terminal state.
|
|
1717
|
+
for (const runId of [...runningGoalIdsRef.current]) {
|
|
1718
|
+
setTimeout(() => continueGoalRun(runId), 500);
|
|
1719
|
+
}
|
|
1412
1720
|
// Pixel fix: observe branch + commits, patch status, optionally pick
|
|
1413
1721
|
// up the next open error if run-all is active.
|
|
1414
1722
|
const pendingFix = currentPixelFixRef.current;
|
|
@@ -1495,13 +1803,34 @@ export function App(props) {
|
|
|
1495
1803
|
}, []),
|
|
1496
1804
|
onQueuedStart: useCallback((content) => {
|
|
1497
1805
|
// When a queued message starts processing, show it as a UserItem
|
|
1498
|
-
// and flush prior items to history
|
|
1806
|
+
// and flush prior items to history. Synthetic system events are hidden
|
|
1807
|
+
// from the transcript but still routed through the main agent context.
|
|
1499
1808
|
const displayText = typeof content === "string"
|
|
1500
1809
|
? content
|
|
1501
1810
|
: content
|
|
1502
1811
|
.filter((c) => c.type === "text")
|
|
1503
1812
|
.map((c) => c.text)
|
|
1504
1813
|
.join("\n");
|
|
1814
|
+
if (isGoalSyntheticEvent(displayText)) {
|
|
1815
|
+
const eventInfo = parseGoalSyntheticEvent(displayText);
|
|
1816
|
+
setLiveItems((prev) => {
|
|
1817
|
+
if (prev.length > 0)
|
|
1818
|
+
queueFlush(prev);
|
|
1819
|
+
return [];
|
|
1820
|
+
});
|
|
1821
|
+
setDoneStatus(null);
|
|
1822
|
+
appendGoalProgress({
|
|
1823
|
+
kind: "goal_progress",
|
|
1824
|
+
phase: "orchestrator_reviewing",
|
|
1825
|
+
title: "Orchestrator reviewing Goal update",
|
|
1826
|
+
detail: eventInfo?.kind === "worker"
|
|
1827
|
+
? `Worker ${eventInfo.worker ?? "finished"} reported back${eventInfo.task ? ` on ${eventInfo.task}` : ""}. Inspecting Goal state.`
|
|
1828
|
+
: `Verifier reported ${eventInfo?.status ?? "status"}. Inspecting evidence and next action.`,
|
|
1829
|
+
workerId: eventInfo?.worker,
|
|
1830
|
+
status: eventInfo?.status,
|
|
1831
|
+
});
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1505
1834
|
const imageCount = typeof content === "string"
|
|
1506
1835
|
? undefined
|
|
1507
1836
|
: content.filter((c) => c.type === "image").length || undefined;
|
|
@@ -1600,7 +1929,14 @@ export function App(props) {
|
|
|
1600
1929
|
if (pendingFlushRef.current.length > 0) {
|
|
1601
1930
|
const items = pendingFlushRef.current;
|
|
1602
1931
|
pendingFlushRef.current = [];
|
|
1603
|
-
setHistory((h) =>
|
|
1932
|
+
setHistory((h) => {
|
|
1933
|
+
const next = compactHistory([...h, ...trimFlushedItems(items)]);
|
|
1934
|
+
if (sessionStore)
|
|
1935
|
+
sessionStore.history = next;
|
|
1936
|
+
return next;
|
|
1937
|
+
});
|
|
1938
|
+
if (sessionStore)
|
|
1939
|
+
sessionStore.liveItems = liveItems;
|
|
1604
1940
|
}
|
|
1605
1941
|
}, [flushGeneration]);
|
|
1606
1942
|
// Sync terminal title with agent loop state
|
|
@@ -1687,17 +2023,21 @@ export function App(props) {
|
|
|
1687
2023
|
await applyLanguageDetectionRef.current("input");
|
|
1688
2024
|
}
|
|
1689
2025
|
// Handle /model directly — open inline selector
|
|
1690
|
-
if (trimmed === "/model" || trimmed === "/m") {
|
|
2026
|
+
if (trimmed === "/model" || trimmed === "/m" || trimmed === "/models") {
|
|
1691
2027
|
setOverlay("model");
|
|
1692
2028
|
return;
|
|
1693
2029
|
}
|
|
1694
2030
|
// Handle /compact — compact conversation
|
|
1695
2031
|
if (trimmed === "/compact" || trimmed === "/c") {
|
|
1696
|
-
const
|
|
1697
|
-
|
|
2032
|
+
const ac = new AbortController();
|
|
2033
|
+
compactionAbortRef.current = ac;
|
|
2034
|
+
const compacted = await compactConversation(messagesRef.current, ac.signal);
|
|
2035
|
+
if (!ac.signal.aborted && compacted !== messagesRef.current) {
|
|
1698
2036
|
messagesRef.current = compacted;
|
|
1699
2037
|
await persistCompactedSession(compacted);
|
|
1700
2038
|
}
|
|
2039
|
+
if (compactionAbortRef.current === ac)
|
|
2040
|
+
compactionAbortRef.current = null;
|
|
1701
2041
|
return;
|
|
1702
2042
|
}
|
|
1703
2043
|
// Handle /quit — exit the agent
|
|
@@ -1842,6 +2182,27 @@ export function App(props) {
|
|
|
1842
2182
|
]);
|
|
1843
2183
|
return;
|
|
1844
2184
|
}
|
|
2185
|
+
// Handle /goals — open goal pane
|
|
2186
|
+
if (trimmed === "/goals") {
|
|
2187
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2188
|
+
props.sessionStore.overlay = "goal";
|
|
2189
|
+
props.sessionStore.planAutoExpand = false;
|
|
2190
|
+
props.resetUI();
|
|
2191
|
+
}
|
|
2192
|
+
else {
|
|
2193
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2194
|
+
setStaticKey((key) => key + 1);
|
|
2195
|
+
if (props.sessionStore) {
|
|
2196
|
+
props.sessionStore.overlay = "goal";
|
|
2197
|
+
props.sessionStore.planAutoExpand = false;
|
|
2198
|
+
if (agentLoop.isRunning)
|
|
2199
|
+
props.sessionStore.pendingResetUI = true;
|
|
2200
|
+
}
|
|
2201
|
+
setPlanAutoExpand(false);
|
|
2202
|
+
setOverlay("goal");
|
|
2203
|
+
}
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
1845
2206
|
// Handle /plans — open plan pane
|
|
1846
2207
|
if (trimmed === "/plans") {
|
|
1847
2208
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
@@ -1862,49 +2223,49 @@ export function App(props) {
|
|
|
1862
2223
|
return;
|
|
1863
2224
|
}
|
|
1864
2225
|
// Handle prompt-template commands (built-in + custom from .ezcoder/commands/)
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
const cmdName =
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
log("INFO", "command", `Prompt command: /${cmdName}${cmdArgs ? ` (args: ${cmdArgs})` : ""}`);
|
|
1874
|
-
// Move live items into history before starting
|
|
1875
|
-
setLiveItems((prev) => {
|
|
1876
|
-
if (prev.length > 0) {
|
|
1877
|
-
pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
|
|
1878
|
-
}
|
|
1879
|
-
return [];
|
|
1880
|
-
});
|
|
1881
|
-
// Show the command name as the user message
|
|
1882
|
-
const userItem = { kind: "user", text: trimmed, id: getId() };
|
|
1883
|
-
setLastUserMessage(trimmed);
|
|
1884
|
-
setDoneStatus(null);
|
|
1885
|
-
setLiveItems([userItem]);
|
|
1886
|
-
// Send the full prompt to the agent, with user args appended if provided
|
|
1887
|
-
const fullPrompt = cmdArgs
|
|
1888
|
-
? `${promptText}\n\n## User Instructions\n\n${cmdArgs}`
|
|
1889
|
-
: promptText;
|
|
1890
|
-
try {
|
|
1891
|
-
await agentLoop.run(fullPrompt);
|
|
1892
|
-
}
|
|
1893
|
-
catch (err) {
|
|
1894
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1895
|
-
log("ERROR", "error", msg);
|
|
1896
|
-
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
1897
|
-
setLiveItems((prev) => [
|
|
1898
|
-
...prev,
|
|
1899
|
-
isAbort
|
|
1900
|
-
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
1901
|
-
: toErrorItem(err, getId()),
|
|
1902
|
-
]);
|
|
2226
|
+
const promptCommandRoute = routePromptCommandInput(trimmed, PROMPT_COMMANDS, customCommands);
|
|
2227
|
+
if (promptCommandRoute) {
|
|
2228
|
+
const { cmdName, cmdArgs, fullPrompt } = promptCommandRoute;
|
|
2229
|
+
log("INFO", "command", `Prompt command: /${cmdName}${cmdArgs ? ` (args: ${cmdArgs})` : ""}`);
|
|
2230
|
+
// Move live items into history before starting
|
|
2231
|
+
setLiveItems((prev) => {
|
|
2232
|
+
if (prev.length > 0) {
|
|
2233
|
+
pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
|
|
1903
2234
|
}
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2235
|
+
return [];
|
|
2236
|
+
});
|
|
2237
|
+
const hasImages = inputImages.length > 0;
|
|
2238
|
+
const modelInfo = getModel(currentModel);
|
|
2239
|
+
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
2240
|
+
const userContent = buildUserContentWithAttachments(fullPrompt, inputImages, modelSupportsImages);
|
|
2241
|
+
// Show the typed command as the user message
|
|
2242
|
+
const userItem = {
|
|
2243
|
+
kind: "user",
|
|
2244
|
+
text: trimmed,
|
|
2245
|
+
imageCount: hasImages ? inputImages.length : undefined,
|
|
2246
|
+
id: getId(),
|
|
2247
|
+
};
|
|
2248
|
+
setLastUserMessage(trimmed);
|
|
2249
|
+
setDoneStatus(null);
|
|
2250
|
+
setLiveItems([userItem]);
|
|
2251
|
+
// Send the full prompt to the agent, with user args appended if provided
|
|
2252
|
+
try {
|
|
2253
|
+
await agentLoop.run(userContent);
|
|
1907
2254
|
}
|
|
2255
|
+
catch (err) {
|
|
2256
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2257
|
+
log("ERROR", "error", msg);
|
|
2258
|
+
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2259
|
+
setLiveItems((prev) => [
|
|
2260
|
+
...prev,
|
|
2261
|
+
isAbort
|
|
2262
|
+
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2263
|
+
: toErrorItem(err, getId()),
|
|
2264
|
+
]);
|
|
2265
|
+
}
|
|
2266
|
+
// Reload custom commands in case a setup command created new ones
|
|
2267
|
+
reloadCustomCommands();
|
|
2268
|
+
return;
|
|
1908
2269
|
}
|
|
1909
2270
|
// Check slash commands
|
|
1910
2271
|
if (props.onSlashCommand && input.startsWith("/")) {
|
|
@@ -1918,47 +2279,7 @@ export function App(props) {
|
|
|
1918
2279
|
const hasImages = inputImages.length > 0;
|
|
1919
2280
|
const modelInfo = getModel(currentModel);
|
|
1920
2281
|
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
1921
|
-
|
|
1922
|
-
if (hasImages) {
|
|
1923
|
-
const parts = [];
|
|
1924
|
-
if (trimmed) {
|
|
1925
|
-
parts.push({ type: "text", text: trimmed });
|
|
1926
|
-
}
|
|
1927
|
-
for (const img of inputImages) {
|
|
1928
|
-
if (img.kind === "text") {
|
|
1929
|
-
parts.push({
|
|
1930
|
-
type: "text",
|
|
1931
|
-
text: `<file name="${img.fileName}">\n${img.data}\n</file>`,
|
|
1932
|
-
});
|
|
1933
|
-
}
|
|
1934
|
-
else if (modelSupportsImages) {
|
|
1935
|
-
parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
|
|
1936
|
-
}
|
|
1937
|
-
else {
|
|
1938
|
-
// GLM models: save image to temp file and instruct model to use vision MCP tool
|
|
1939
|
-
const ext = img.mediaType.split("/")[1] ?? "png";
|
|
1940
|
-
const tmpPath = `/tmp/ezcoder-img-${Date.now()}.${ext}`;
|
|
1941
|
-
try {
|
|
1942
|
-
writeFileSync(tmpPath, Buffer.from(img.data, "base64"));
|
|
1943
|
-
parts.push({
|
|
1944
|
-
type: "text",
|
|
1945
|
-
text: `[User attached an image saved at: ${tmpPath} — use the image_analysis tool to view and analyze it]`,
|
|
1946
|
-
});
|
|
1947
|
-
}
|
|
1948
|
-
catch {
|
|
1949
|
-
parts.push({
|
|
1950
|
-
type: "text",
|
|
1951
|
-
text: `[User attached an image but it could not be saved for analysis]`,
|
|
1952
|
-
});
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
// If only text parts remain after stripping images, simplify to plain string
|
|
1957
|
-
userContent = parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
|
|
1958
|
-
}
|
|
1959
|
-
else {
|
|
1960
|
-
userContent = input;
|
|
1961
|
-
}
|
|
2282
|
+
const userContent = buildUserContentWithAttachments(input, inputImages, modelSupportsImages);
|
|
1962
2283
|
// ── Queue message if agent is already running ──
|
|
1963
2284
|
if (agentLoop.isRunning) {
|
|
1964
2285
|
log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
|
|
@@ -2040,6 +2361,9 @@ export function App(props) {
|
|
|
2040
2361
|
agentLoop.clearQueue();
|
|
2041
2362
|
agentLoop.abort();
|
|
2042
2363
|
}
|
|
2364
|
+
else if (compactionAbortRef.current) {
|
|
2365
|
+
compactionAbortRef.current.abort();
|
|
2366
|
+
}
|
|
2043
2367
|
else {
|
|
2044
2368
|
handleDoubleExit();
|
|
2045
2369
|
}
|
|
@@ -2170,6 +2494,7 @@ export function App(props) {
|
|
|
2170
2494
|
};
|
|
2171
2495
|
const promptOrder = [
|
|
2172
2496
|
// Project audits / one-shot analysis
|
|
2497
|
+
"goal",
|
|
2173
2498
|
"init",
|
|
2174
2499
|
"research",
|
|
2175
2500
|
"scan",
|
|
@@ -2215,11 +2540,29 @@ export function App(props) {
|
|
|
2215
2540
|
case "tombstone":
|
|
2216
2541
|
return null;
|
|
2217
2542
|
case "banner":
|
|
2218
|
-
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount }, item.id));
|
|
2543
|
+
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount, goalCount: goalCount }, item.id));
|
|
2219
2544
|
case "user":
|
|
2220
2545
|
return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
|
|
2221
2546
|
case "task":
|
|
2222
2547
|
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));
|
|
2548
|
+
case "goal":
|
|
2549
|
+
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));
|
|
2550
|
+
case "goal_progress": {
|
|
2551
|
+
const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
|
|
2552
|
+
const color = item.phase === "terminal" && !isError
|
|
2553
|
+
? theme.success
|
|
2554
|
+
: isError
|
|
2555
|
+
? theme.warning
|
|
2556
|
+
: theme.primary;
|
|
2557
|
+
const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
|
|
2558
|
+
? "✓ "
|
|
2559
|
+
: item.phase === "terminal"
|
|
2560
|
+
? item.status === "passed"
|
|
2561
|
+
? "◆ "
|
|
2562
|
+
: "! "
|
|
2563
|
+
: "↻ ";
|
|
2564
|
+
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));
|
|
2565
|
+
}
|
|
2223
2566
|
case "style_pack": {
|
|
2224
2567
|
const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
|
|
2225
2568
|
const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
|
|
@@ -2375,13 +2718,602 @@ export function App(props) {
|
|
|
2375
2718
|
currentProvider,
|
|
2376
2719
|
currentModel,
|
|
2377
2720
|
]);
|
|
2721
|
+
const openOverlay = useCallback((kind) => {
|
|
2722
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2723
|
+
props.sessionStore.overlay = kind;
|
|
2724
|
+
if (kind !== "plan")
|
|
2725
|
+
props.sessionStore.planAutoExpand = false;
|
|
2726
|
+
props.resetUI();
|
|
2727
|
+
}
|
|
2728
|
+
else {
|
|
2729
|
+
if (props.sessionStore) {
|
|
2730
|
+
props.sessionStore.overlay = kind;
|
|
2731
|
+
if (kind !== "plan")
|
|
2732
|
+
props.sessionStore.planAutoExpand = false;
|
|
2733
|
+
if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
|
|
2734
|
+
props.sessionStore.pendingResetUI = true;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
if (kind !== "plan")
|
|
2738
|
+
setPlanAutoExpand(false);
|
|
2739
|
+
setOverlay(kind);
|
|
2740
|
+
}
|
|
2741
|
+
}, [agentLoop.isRunning, props, stdout]);
|
|
2742
|
+
const closeOverlay = useCallback(() => {
|
|
2743
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2744
|
+
props.sessionStore.overlay = null;
|
|
2745
|
+
props.resetUI();
|
|
2746
|
+
}
|
|
2747
|
+
else {
|
|
2748
|
+
if (props.sessionStore) {
|
|
2749
|
+
props.sessionStore.overlay = null;
|
|
2750
|
+
}
|
|
2751
|
+
setOverlay(null);
|
|
2752
|
+
}
|
|
2753
|
+
}, [agentLoop.isRunning, overlay, props, stdout]);
|
|
2754
|
+
const runGoalSyntheticEvent = useCallback((eventText) => {
|
|
2755
|
+
const eventInfo = parseGoalSyntheticEvent(eventText);
|
|
2756
|
+
const detail = eventInfo?.kind === "worker"
|
|
2757
|
+
? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
|
|
2758
|
+
: `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
|
|
2759
|
+
if (agentRunningRef.current) {
|
|
2760
|
+
appendGoalProgress({
|
|
2761
|
+
kind: "goal_progress",
|
|
2762
|
+
phase: "orchestrator_reviewing",
|
|
2763
|
+
title: "Goal update queued for orchestrator",
|
|
2764
|
+
detail: `${detail} It will report back after the current turn.`,
|
|
2765
|
+
workerId: eventInfo?.worker,
|
|
2766
|
+
status: eventInfo?.status,
|
|
2767
|
+
});
|
|
2768
|
+
agentLoop.queueMessage(eventText);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
appendGoalProgress({
|
|
2772
|
+
kind: "goal_progress",
|
|
2773
|
+
phase: "orchestrator_reviewing",
|
|
2774
|
+
title: "Orchestrator reviewing Goal update",
|
|
2775
|
+
detail,
|
|
2776
|
+
workerId: eventInfo?.worker,
|
|
2777
|
+
status: eventInfo?.status,
|
|
2778
|
+
});
|
|
2779
|
+
setLastUserMessage("");
|
|
2780
|
+
setDoneStatus(null);
|
|
2781
|
+
void agentLoop.run(eventText).catch((err) => {
|
|
2782
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2783
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2784
|
+
});
|
|
2785
|
+
}, [agentLoop, appendGoalProgress]);
|
|
2786
|
+
const continueGoalRun = useCallback((runId) => {
|
|
2787
|
+
void (async () => {
|
|
2788
|
+
const latestRun = await reconcileActiveGoalRuns(props.cwd, {
|
|
2789
|
+
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
2790
|
+
}).then(({ runs }) => runs.find((item) => item.id === runId) ?? null);
|
|
2791
|
+
if (!latestRun) {
|
|
2792
|
+
runningGoalIdsRef.current.delete(runId);
|
|
2793
|
+
clearGoalStatusEntry(runId);
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
const decision = decideGoalNextAction(latestRun);
|
|
2797
|
+
if (decision.kind === "wait")
|
|
2798
|
+
return;
|
|
2799
|
+
if (decision.kind === "terminal" ||
|
|
2800
|
+
decision.kind === "blocked" ||
|
|
2801
|
+
decision.kind === "pause") {
|
|
2802
|
+
const status = decision.kind === "terminal"
|
|
2803
|
+
? decision.status
|
|
2804
|
+
: decision.kind === "blocked"
|
|
2805
|
+
? "blocked"
|
|
2806
|
+
: "paused";
|
|
2807
|
+
const nextRun = {
|
|
2808
|
+
...latestRun,
|
|
2809
|
+
status,
|
|
2810
|
+
continueRequestedAt: undefined,
|
|
2811
|
+
blockers: decision.kind === "blocked" || decision.kind === "pause"
|
|
2812
|
+
? Array.from(new Set([...latestRun.blockers, decision.reason]))
|
|
2813
|
+
: latestRun.blockers,
|
|
2814
|
+
};
|
|
2815
|
+
await upsertGoalRun(props.cwd, nextRun);
|
|
2816
|
+
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2817
|
+
kind: "continuation_stopped",
|
|
2818
|
+
reason: decision.reason,
|
|
2819
|
+
content: `terminal=${status}`,
|
|
2820
|
+
});
|
|
2821
|
+
const terminalProgress = formatGoalTerminalProgress(nextRun);
|
|
2822
|
+
if (terminalProgress)
|
|
2823
|
+
appendGoalProgress(terminalProgress);
|
|
2824
|
+
runningGoalIdsRef.current.delete(runId);
|
|
2825
|
+
clearGoalStatusEntry(runId);
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
let runForNextAction = latestRun;
|
|
2829
|
+
if (latestRun.continueRequestedAt &&
|
|
2830
|
+
!listGoalWorkers(props.cwd).some((worker) => worker.status === "running") &&
|
|
2831
|
+
activeVerifierRunIdsRef.current.size === 0) {
|
|
2832
|
+
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2833
|
+
kind: "continuation_consumed",
|
|
2834
|
+
reason: `Continuation request consumed by ${decision.kind}.`,
|
|
2835
|
+
});
|
|
2836
|
+
runForNextAction = await upsertGoalRun(props.cwd, {
|
|
2837
|
+
...latestRun,
|
|
2838
|
+
continueRequestedAt: undefined,
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
appendGoalProgress({
|
|
2842
|
+
kind: "goal_progress",
|
|
2843
|
+
phase: "continuing",
|
|
2844
|
+
title: `Continuing Goal: ${latestRun.title}`,
|
|
2845
|
+
detail: "Starting the next worker task or verifier step automatically.",
|
|
2846
|
+
status: latestRun.status,
|
|
2847
|
+
});
|
|
2848
|
+
upsertGoalStatusEntry({
|
|
2849
|
+
runId: latestRun.id,
|
|
2850
|
+
label: latestRun.title,
|
|
2851
|
+
phase: "orchestrating",
|
|
2852
|
+
startedAt: Date.now(),
|
|
2853
|
+
detail: "choosing next step",
|
|
2854
|
+
});
|
|
2855
|
+
startGoalRunRef.current(runForNextAction);
|
|
2856
|
+
})().catch((err) => {
|
|
2857
|
+
runningGoalIdsRef.current.delete(runId);
|
|
2858
|
+
clearGoalStatusEntry(runId);
|
|
2859
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2860
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2861
|
+
});
|
|
2862
|
+
}, [appendGoalProgress, clearGoalStatusEntry, props.cwd, upsertGoalStatusEntry]);
|
|
2863
|
+
const handleGoalWorkerComplete = useCallback((run, completion) => {
|
|
2864
|
+
const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
|
|
2865
|
+
completion.worker.goalTaskId;
|
|
2866
|
+
const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
|
|
2867
|
+
void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
|
|
2868
|
+
appendGoalProgress({
|
|
2869
|
+
kind: "goal_progress",
|
|
2870
|
+
phase: "worker_finished",
|
|
2871
|
+
title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
|
|
2872
|
+
detail: summarizeGoalCompletion(completion.summary),
|
|
2873
|
+
workerId: completion.worker.id,
|
|
2874
|
+
status: completion.status,
|
|
2875
|
+
});
|
|
2876
|
+
upsertGoalStatusEntry({
|
|
2877
|
+
runId: run.id,
|
|
2878
|
+
label: taskTitle,
|
|
2879
|
+
phase: completion.status === "done" ? "reviewing" : "failed",
|
|
2880
|
+
startedAt: Date.now(),
|
|
2881
|
+
detail: completion.status === "done" ? "reviewing result" : "task failed",
|
|
2882
|
+
workerId: completion.worker.id,
|
|
2883
|
+
goalNumber: goalNumberForRun(run.id),
|
|
2884
|
+
});
|
|
2885
|
+
runGoalSyntheticEvent(eventText);
|
|
2886
|
+
void (async () => {
|
|
2887
|
+
if (listGoalWorkers(completion.worker.cwd).some((worker) => worker.status === "running"))
|
|
2888
|
+
return;
|
|
2889
|
+
if (activeVerifierRunIdsRef.current.size > 0)
|
|
2890
|
+
return;
|
|
2891
|
+
const runs = await loadGoalRuns(completion.worker.cwd);
|
|
2892
|
+
const queued = runs.find((item) => item.continueRequestedAt && !goalHasBlockingPrerequisites(item));
|
|
2893
|
+
if (queued)
|
|
2894
|
+
setTimeout(() => continueGoalRun(queued.id), 750);
|
|
2895
|
+
})().catch((err) => log("ERROR", "goal", err instanceof Error ? err.message : String(err)));
|
|
2896
|
+
}, [
|
|
2897
|
+
appendGoalProgress,
|
|
2898
|
+
continueGoalRun,
|
|
2899
|
+
goalNumberForRun,
|
|
2900
|
+
runGoalSyntheticEvent,
|
|
2901
|
+
upsertGoalStatusEntry,
|
|
2902
|
+
]);
|
|
2903
|
+
useEffect(() => {
|
|
2904
|
+
return subscribeGoalWorkerCompletions((completion) => {
|
|
2905
|
+
void (async () => {
|
|
2906
|
+
const latestRun = (await loadGoalRuns(completion.worker.cwd)).find((item) => item.id === completion.worker.goalRunId) ?? null;
|
|
2907
|
+
if (!latestRun) {
|
|
2908
|
+
log("WARN", "goal", `Worker completion for unknown Goal ${completion.worker.goalRunId}`);
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
runningGoalIdsRef.current.add(latestRun.id);
|
|
2912
|
+
handleGoalWorkerComplete(latestRun, completion);
|
|
2913
|
+
})().catch((err) => {
|
|
2914
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2915
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2916
|
+
});
|
|
2917
|
+
}, props.cwd);
|
|
2918
|
+
}, [handleGoalWorkerComplete, props.cwd]);
|
|
2919
|
+
const startGoalRun = useCallback((run) => {
|
|
2920
|
+
runningGoalIdsRef.current.add(run.id);
|
|
2921
|
+
void (async () => {
|
|
2922
|
+
if (goalHasBlockingPrerequisites(run)) {
|
|
2923
|
+
setOverlay(null);
|
|
2924
|
+
const detail = formatGoalBlockingPrerequisites(run);
|
|
2925
|
+
await upsertGoalRun(props.cwd, {
|
|
2926
|
+
...run,
|
|
2927
|
+
status: "blocked",
|
|
2928
|
+
blockers: Array.from(new Set([...run.blockers, detail])),
|
|
2929
|
+
});
|
|
2930
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2931
|
+
appendGoalProgress({
|
|
2932
|
+
kind: "goal_progress",
|
|
2933
|
+
phase: "terminal",
|
|
2934
|
+
title: `Goal blocked: ${run.title}`,
|
|
2935
|
+
detail,
|
|
2936
|
+
status: "blocked",
|
|
2937
|
+
});
|
|
2938
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
2939
|
+
clearGoalStatusEntry(run.id);
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
const decision = decideGoalNextAction(run);
|
|
2943
|
+
await appendGoalDecision(props.cwd, run.id, decision);
|
|
2944
|
+
if (decision.kind === "terminal") {
|
|
2945
|
+
const terminalProgress = formatGoalTerminalProgress(run);
|
|
2946
|
+
if (terminalProgress)
|
|
2947
|
+
appendGoalProgress(terminalProgress);
|
|
2948
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
2949
|
+
clearGoalStatusEntry(run.id);
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
if (decision.kind === "wait") {
|
|
2953
|
+
appendGoalProgress({
|
|
2954
|
+
kind: "goal_progress",
|
|
2955
|
+
phase: "worker_started",
|
|
2956
|
+
title: decision.workerId ? `Goal working: ${run.title}` : `Goal active: ${run.title}`,
|
|
2957
|
+
detail: decision.reason,
|
|
2958
|
+
workerId: decision.workerId,
|
|
2959
|
+
});
|
|
2960
|
+
upsertGoalStatusEntry({
|
|
2961
|
+
runId: run.id,
|
|
2962
|
+
label: run.title,
|
|
2963
|
+
phase: decision.workerId ? "worker" : "orchestrating",
|
|
2964
|
+
startedAt: Date.now(),
|
|
2965
|
+
detail: decision.reason,
|
|
2966
|
+
workerId: decision.workerId,
|
|
2967
|
+
goalNumber: goalNumberForRun(run.id),
|
|
2968
|
+
});
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
if (decision.kind === "complete") {
|
|
2972
|
+
await upsertGoalRun(props.cwd, { ...run, status: "passed" });
|
|
2973
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2974
|
+
appendGoalProgress({
|
|
2975
|
+
kind: "goal_progress",
|
|
2976
|
+
phase: "terminal",
|
|
2977
|
+
title: `Goal passed: ${run.title}`,
|
|
2978
|
+
detail: decision.reason,
|
|
2979
|
+
status: "passed",
|
|
2980
|
+
});
|
|
2981
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
2982
|
+
clearGoalStatusEntry(run.id);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
if (decision.kind === "run_verifier") {
|
|
2986
|
+
await verifyGoalRun(run);
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
if (decision.kind === "create_task") {
|
|
2990
|
+
await updateGoalTask(props.cwd, run.id, `auto-${Date.now()}`, {
|
|
2991
|
+
title: decision.title,
|
|
2992
|
+
prompt: decision.prompt,
|
|
2993
|
+
status: "pending",
|
|
2994
|
+
});
|
|
2995
|
+
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
2996
|
+
await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
|
|
2997
|
+
setTimeout(() => continueGoalRun(run.id), 250);
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
if (decision.kind === "blocked") {
|
|
3001
|
+
await upsertGoalRun(props.cwd, {
|
|
3002
|
+
...run,
|
|
3003
|
+
status: "blocked",
|
|
3004
|
+
blockers: [...run.blockers, decision.reason],
|
|
3005
|
+
});
|
|
3006
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3007
|
+
appendGoalProgress({
|
|
3008
|
+
kind: "goal_progress",
|
|
3009
|
+
phase: "terminal",
|
|
3010
|
+
title: `Goal blocked: ${run.title}`,
|
|
3011
|
+
detail: decision.reason,
|
|
3012
|
+
status: "blocked",
|
|
3013
|
+
});
|
|
3014
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3015
|
+
clearGoalStatusEntry(run.id);
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
if (decision.kind === "pause") {
|
|
3019
|
+
await updateGoalTask(props.cwd, run.id, decision.task.id, {
|
|
3020
|
+
status: "blocked",
|
|
3021
|
+
attempts: decision.attempts,
|
|
3022
|
+
lastSummary: "Paused after worker attempt limit.",
|
|
3023
|
+
});
|
|
3024
|
+
await upsertGoalRun(props.cwd, {
|
|
3025
|
+
...run,
|
|
3026
|
+
status: "paused",
|
|
3027
|
+
blockers: [...run.blockers, decision.reason],
|
|
3028
|
+
});
|
|
3029
|
+
await appendGoalEvidence(props.cwd, run.id, {
|
|
3030
|
+
kind: "summary",
|
|
3031
|
+
label: "Goal paused",
|
|
3032
|
+
content: decision.reason,
|
|
3033
|
+
});
|
|
3034
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3035
|
+
appendGoalProgress({
|
|
3036
|
+
kind: "goal_progress",
|
|
3037
|
+
phase: "terminal",
|
|
3038
|
+
title: `Goal paused: ${run.title}`,
|
|
3039
|
+
detail: decision.reason,
|
|
3040
|
+
status: "paused",
|
|
3041
|
+
});
|
|
3042
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3043
|
+
clearGoalStatusEntry(run.id);
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
await updateGoalTask(props.cwd, run.id, decision.task.id, { attempts: decision.attempts });
|
|
3047
|
+
const worker = await startGoalWorker({
|
|
3048
|
+
cwd: props.cwd,
|
|
3049
|
+
provider: currentProvider,
|
|
3050
|
+
model: currentModel,
|
|
3051
|
+
goalRunId: run.id,
|
|
3052
|
+
goalTaskId: decision.task.id,
|
|
3053
|
+
taskTitle: decision.task.title,
|
|
3054
|
+
prompt: decision.task.prompt,
|
|
3055
|
+
});
|
|
3056
|
+
await upsertGoalRun(props.cwd, {
|
|
3057
|
+
...run,
|
|
3058
|
+
status: "running",
|
|
3059
|
+
activeWorkerId: worker.id,
|
|
3060
|
+
continueRequestedAt: undefined,
|
|
3061
|
+
});
|
|
3062
|
+
setOverlay(null);
|
|
3063
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3064
|
+
appendGoalProgress({
|
|
3065
|
+
kind: "goal_progress",
|
|
3066
|
+
phase: "worker_started",
|
|
3067
|
+
title: `Worker started: ${decision.task.title}`,
|
|
3068
|
+
detail: "Task is running in the background.",
|
|
3069
|
+
workerId: worker.id,
|
|
3070
|
+
status: worker.status,
|
|
3071
|
+
});
|
|
3072
|
+
upsertGoalStatusEntry({
|
|
3073
|
+
runId: run.id,
|
|
3074
|
+
label: decision.task.title,
|
|
3075
|
+
phase: "worker",
|
|
3076
|
+
startedAt: Date.now(),
|
|
3077
|
+
detail: "background worker running",
|
|
3078
|
+
workerId: worker.id,
|
|
3079
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3080
|
+
});
|
|
3081
|
+
})().catch((err) => {
|
|
3082
|
+
clearGoalStatusEntry(run.id);
|
|
3083
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3084
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3085
|
+
});
|
|
3086
|
+
}, [
|
|
3087
|
+
props.cwd,
|
|
3088
|
+
currentProvider,
|
|
3089
|
+
currentModel,
|
|
3090
|
+
appendGoalProgress,
|
|
3091
|
+
clearGoalStatusEntry,
|
|
3092
|
+
goalNumberForRun,
|
|
3093
|
+
upsertGoalStatusEntry,
|
|
3094
|
+
]);
|
|
3095
|
+
const verifyGoalRun = useCallback(async (run) => {
|
|
3096
|
+
if (!run.verifier?.command) {
|
|
3097
|
+
await appendGoalEvidence(props.cwd, run.id, {
|
|
3098
|
+
kind: "summary",
|
|
3099
|
+
label: "Missing verifier",
|
|
3100
|
+
content: "No verifier command is configured.",
|
|
3101
|
+
});
|
|
3102
|
+
await upsertGoalRun(props.cwd, {
|
|
3103
|
+
...run,
|
|
3104
|
+
status: "blocked",
|
|
3105
|
+
blockers: [...run.blockers, "No verifier command configured."],
|
|
3106
|
+
});
|
|
3107
|
+
appendGoalProgress({
|
|
3108
|
+
kind: "goal_progress",
|
|
3109
|
+
phase: "terminal",
|
|
3110
|
+
title: `Goal blocked: ${run.title}`,
|
|
3111
|
+
detail: "No verifier command is configured.",
|
|
3112
|
+
status: "blocked",
|
|
3113
|
+
});
|
|
3114
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3115
|
+
clearGoalStatusEntry(run.id);
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
activeVerifierRunIdsRef.current.add(run.id);
|
|
3119
|
+
await upsertGoalRun(props.cwd, {
|
|
3120
|
+
...run,
|
|
3121
|
+
status: "verifying",
|
|
3122
|
+
continueRequestedAt: undefined,
|
|
3123
|
+
});
|
|
3124
|
+
appendGoalProgress({
|
|
3125
|
+
kind: "goal_progress",
|
|
3126
|
+
phase: "verifier_started",
|
|
3127
|
+
title: `Verifier started: ${run.title}`,
|
|
3128
|
+
detail: run.verifier.command,
|
|
3129
|
+
status: "verifying",
|
|
3130
|
+
});
|
|
3131
|
+
const startedAt = Date.now();
|
|
3132
|
+
const verifierTimeoutMs = Number(process.env.GG_GOAL_VERIFIER_TIMEOUT_MS ?? 10 * 60 * 1000);
|
|
3133
|
+
upsertGoalStatusEntry({
|
|
3134
|
+
runId: run.id,
|
|
3135
|
+
label: run.title,
|
|
3136
|
+
phase: "verifier",
|
|
3137
|
+
startedAt,
|
|
3138
|
+
detail: run.verifier.command,
|
|
3139
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3140
|
+
});
|
|
3141
|
+
const { spawn } = await import("node:child_process");
|
|
3142
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
3143
|
+
const { join } = await import("node:path");
|
|
3144
|
+
const logDir = join(projectDir(props.cwd), "verifiers");
|
|
3145
|
+
await mkdir(logDir, { recursive: true });
|
|
3146
|
+
const outputPath = join(logDir, `${run.id}-${startedAt}.log`);
|
|
3147
|
+
const child = spawn(run.verifier.command, {
|
|
3148
|
+
cwd: props.cwd,
|
|
3149
|
+
shell: true,
|
|
3150
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3151
|
+
env: { ...process.env },
|
|
3152
|
+
});
|
|
3153
|
+
let output = "";
|
|
3154
|
+
child.stdout?.on("data", (chunk) => {
|
|
3155
|
+
output += chunk.toString("utf-8");
|
|
3156
|
+
if (output.length > 20_000)
|
|
3157
|
+
output = output.slice(output.length - 20_000);
|
|
3158
|
+
});
|
|
3159
|
+
child.stderr?.on("data", (chunk) => {
|
|
3160
|
+
output += chunk.toString("utf-8");
|
|
3161
|
+
if (output.length > 20_000)
|
|
3162
|
+
output = output.slice(output.length - 20_000);
|
|
3163
|
+
});
|
|
3164
|
+
let verifierSettled = false;
|
|
3165
|
+
let timedOut = false;
|
|
3166
|
+
const timeout = verifierTimeoutMs > 0
|
|
3167
|
+
? setTimeout(() => {
|
|
3168
|
+
timedOut = true;
|
|
3169
|
+
if (child.pid)
|
|
3170
|
+
child.kill("SIGTERM");
|
|
3171
|
+
const killTimer = setTimeout(() => {
|
|
3172
|
+
if (!verifierSettled && child.pid)
|
|
3173
|
+
child.kill("SIGKILL");
|
|
3174
|
+
}, 5000);
|
|
3175
|
+
killTimer.unref?.();
|
|
3176
|
+
finishVerifier(124, `Verifier timed out after ${verifierTimeoutMs}ms and was terminated.\n${output}`);
|
|
3177
|
+
}, verifierTimeoutMs)
|
|
3178
|
+
: undefined;
|
|
3179
|
+
timeout?.unref?.();
|
|
3180
|
+
const finishVerifier = (code, forcedOutput) => {
|
|
3181
|
+
if (verifierSettled)
|
|
3182
|
+
return;
|
|
3183
|
+
verifierSettled = true;
|
|
3184
|
+
if (timeout)
|
|
3185
|
+
clearTimeout(timeout);
|
|
3186
|
+
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3187
|
+
void (async () => {
|
|
3188
|
+
const status = code === 0 ? "pass" : "fail";
|
|
3189
|
+
const failureClass = timedOut
|
|
3190
|
+
? "verifier_timeout"
|
|
3191
|
+
: forcedOutput?.startsWith("Verifier process error:")
|
|
3192
|
+
? "verifier_spawn_error"
|
|
3193
|
+
: status === "fail"
|
|
3194
|
+
? "verifier_failure"
|
|
3195
|
+
: "verifier_pass";
|
|
3196
|
+
const summary = (forcedOutput ?? output).trim() ||
|
|
3197
|
+
(code === 0 ? "Verifier passed." : "Verifier failed.");
|
|
3198
|
+
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
3199
|
+
await writeFile(outputPath, summary + "\n", "utf-8");
|
|
3200
|
+
const runWithVerifier = {
|
|
3201
|
+
...latestRun,
|
|
3202
|
+
verifier: {
|
|
3203
|
+
...latestRun.verifier,
|
|
3204
|
+
description: latestRun.verifier?.description ?? "Goal verifier",
|
|
3205
|
+
command: run.verifier?.command,
|
|
3206
|
+
lastResult: {
|
|
3207
|
+
status,
|
|
3208
|
+
summary,
|
|
3209
|
+
command: run.verifier?.command,
|
|
3210
|
+
exitCode: code ?? 1,
|
|
3211
|
+
outputPath,
|
|
3212
|
+
checkedAt: new Date().toISOString(),
|
|
3213
|
+
},
|
|
3214
|
+
},
|
|
3215
|
+
};
|
|
3216
|
+
const completionCheck = canCompleteGoalRun(runWithVerifier);
|
|
3217
|
+
const verifiedRun = await upsertGoalRun(props.cwd, {
|
|
3218
|
+
...runWithVerifier,
|
|
3219
|
+
continueRequestedAt: undefined,
|
|
3220
|
+
status: status === "pass" && completionCheck.ok
|
|
3221
|
+
? "passed"
|
|
3222
|
+
: status === "pass"
|
|
3223
|
+
? "ready"
|
|
3224
|
+
: "failed",
|
|
3225
|
+
});
|
|
3226
|
+
await appendGoalEvidence(props.cwd, run.id, {
|
|
3227
|
+
kind: "command",
|
|
3228
|
+
label: `Verifier ${status}`,
|
|
3229
|
+
content: `${failureClass}: ${summary}`.slice(0, 4000),
|
|
3230
|
+
path: outputPath,
|
|
3231
|
+
});
|
|
3232
|
+
await appendGoalDecision(props.cwd, run.id, {
|
|
3233
|
+
kind: `verifier_${status}`,
|
|
3234
|
+
reason: `${failureClass}: verifier exited with code ${code ?? 1}.`,
|
|
3235
|
+
content: `outputPath=${outputPath}; durationMs=${Date.now() - startedAt}`,
|
|
3236
|
+
});
|
|
3237
|
+
if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
|
|
3238
|
+
await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
|
|
3239
|
+
title: "Fix verifier failure",
|
|
3240
|
+
prompt: `Goal verifier failed after ${Date.now() - startedAt}ms. Original goal: ${run.goal}\n\n` +
|
|
3241
|
+
`Verifier command: ${run.verifier?.command}\n\n` +
|
|
3242
|
+
`Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
|
|
3243
|
+
status: "pending",
|
|
3244
|
+
});
|
|
3245
|
+
const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
|
|
3246
|
+
await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
|
|
3247
|
+
}
|
|
3248
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3249
|
+
appendGoalProgress({
|
|
3250
|
+
kind: "goal_progress",
|
|
3251
|
+
phase: "verifier_finished",
|
|
3252
|
+
title: `Verifier ${status}: ${run.title}`,
|
|
3253
|
+
detail: summarizeGoalCompletion(summary),
|
|
3254
|
+
status,
|
|
3255
|
+
});
|
|
3256
|
+
upsertGoalStatusEntry({
|
|
3257
|
+
runId: run.id,
|
|
3258
|
+
label: run.title,
|
|
3259
|
+
phase: status === "pass" ? "reviewing" : "failed",
|
|
3260
|
+
startedAt: Date.now(),
|
|
3261
|
+
detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
|
|
3262
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3263
|
+
});
|
|
3264
|
+
const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status, run.verifier?.command ?? "", code ?? 1, summary);
|
|
3265
|
+
runGoalSyntheticEvent(eventText);
|
|
3266
|
+
const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
|
|
3267
|
+
if (continuationRun?.continueRequestedAt && status === "pass") {
|
|
3268
|
+
setTimeout(() => continueGoalRun(run.id), 500);
|
|
3269
|
+
}
|
|
3270
|
+
})().catch((err) => {
|
|
3271
|
+
clearGoalStatusEntry(run.id);
|
|
3272
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3273
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
|
|
3274
|
+
});
|
|
3275
|
+
};
|
|
3276
|
+
child.on("close", (code) => finishVerifier(code));
|
|
3277
|
+
child.on("error", (err) => finishVerifier(1, `Verifier process error: ${err.message}`));
|
|
3278
|
+
}, [
|
|
3279
|
+
props.cwd,
|
|
3280
|
+
appendGoalProgress,
|
|
3281
|
+
clearGoalStatusEntry,
|
|
3282
|
+
goalNumberForRun,
|
|
3283
|
+
runGoalSyntheticEvent,
|
|
3284
|
+
upsertGoalStatusEntry,
|
|
3285
|
+
]);
|
|
3286
|
+
const pauseGoalRun = useCallback((run) => {
|
|
3287
|
+
void (async () => {
|
|
3288
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3289
|
+
if (run.activeWorkerId)
|
|
3290
|
+
await stopGoalWorker(run.activeWorkerId);
|
|
3291
|
+
await upsertGoalRun(props.cwd, { ...run, status: "paused", activeWorkerId: undefined });
|
|
3292
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3293
|
+
appendGoalProgress({
|
|
3294
|
+
kind: "goal_progress",
|
|
3295
|
+
phase: "terminal",
|
|
3296
|
+
title: `Goal paused: ${run.title}`,
|
|
3297
|
+
detail: "Auto-continuation stopped until resumed.",
|
|
3298
|
+
status: "paused",
|
|
3299
|
+
});
|
|
3300
|
+
clearGoalStatusEntry(run.id);
|
|
3301
|
+
})().catch((err) => {
|
|
3302
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3303
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3304
|
+
});
|
|
3305
|
+
}, [appendGoalProgress, clearGoalStatusEntry, props.cwd]);
|
|
2378
3306
|
// Keep refs in sync for access from stale closures (onDone)
|
|
2379
3307
|
startTaskRef.current = startTask;
|
|
3308
|
+
startGoalRunRef.current = startGoalRun;
|
|
2380
3309
|
useEffect(() => {
|
|
2381
3310
|
runAllTasksRef.current = runAllTasks;
|
|
2382
3311
|
if (props.sessionStore)
|
|
2383
3312
|
props.sessionStore.runAllTasks = runAllTasks;
|
|
2384
3313
|
}, [runAllTasks, props.sessionStore]);
|
|
3314
|
+
useEffect(() => {
|
|
3315
|
+
agentRunningRef.current = agentLoop.isRunning;
|
|
3316
|
+
}, [agentLoop.isRunning]);
|
|
2385
3317
|
const startPixelFix = useCallback((errorId) => {
|
|
2386
3318
|
void (async () => {
|
|
2387
3319
|
try {
|
|
@@ -2482,12 +3414,29 @@ export function App(props) {
|
|
|
2482
3414
|
props.sessionStore.runAllPixel = runAllPixel;
|
|
2483
3415
|
}, [runAllPixel, props.sessionStore]);
|
|
2484
3416
|
const isTaskView = overlay === "tasks";
|
|
3417
|
+
const isGoalView = overlay === "goal";
|
|
2485
3418
|
const isSkillsView = overlay === "skills";
|
|
2486
3419
|
const isPlanView = overlay === "plan";
|
|
2487
3420
|
const isEyesView = overlay === "eyes";
|
|
3421
|
+
const footerStatusLayout = getFooterStatusLayoutDecision({
|
|
3422
|
+
columns,
|
|
3423
|
+
backgroundTaskCount: bgTasks.length,
|
|
3424
|
+
eyesCount,
|
|
3425
|
+
updatePending,
|
|
3426
|
+
});
|
|
2488
3427
|
const isPixelView = overlay === "pixel";
|
|
2489
|
-
const isOverlayView = isTaskView || isSkillsView || isPlanView || isEyesView || isPixelView;
|
|
2490
|
-
|
|
3428
|
+
const isOverlayView = isTaskView || isGoalView || isSkillsView || isPlanView || isEyesView || isPixelView;
|
|
3429
|
+
const shouldHideHistoryForOverlay = shouldHideHistoryForOverlayView(isOverlayView, agentLoop.isRunning);
|
|
3430
|
+
const stabilizeOverlayPaneRerender = shouldStabilizeOverlayPaneRerender({
|
|
3431
|
+
overlayPane: overlay,
|
|
3432
|
+
isAgentRunning: agentLoop.isRunning,
|
|
3433
|
+
});
|
|
3434
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: shouldHideStaticItemsForOverlayView({
|
|
3435
|
+
shouldHideHistoryForOverlay,
|
|
3436
|
+
stabilizeOverlayPaneRerender,
|
|
3437
|
+
})
|
|
3438
|
+
? []
|
|
3439
|
+
: history, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, getStaticHistoryKey({ resizeKey, staticKey })), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
2491
3440
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2492
3441
|
props.sessionStore.overlay = null;
|
|
2493
3442
|
props.resetUI();
|
|
@@ -2512,6 +3461,16 @@ export function App(props) {
|
|
|
2512
3461
|
markTaskInProgress(props.cwd, next.id);
|
|
2513
3462
|
startTask(next.title, next.prompt, next.id);
|
|
2514
3463
|
}
|
|
3464
|
+
} })) : isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
3465
|
+
void summarizeGoalCounts(props.cwd).then((counts) => setGoalCount(counts.active));
|
|
3466
|
+
closeOverlay();
|
|
3467
|
+
}, onRunGoal: (run) => {
|
|
3468
|
+
setOverlay(null);
|
|
3469
|
+
startGoalRun(run);
|
|
3470
|
+
}, onVerifyGoal: (run) => {
|
|
3471
|
+
void verifyGoalRun(run);
|
|
3472
|
+
}, onPauseGoal: (run) => {
|
|
3473
|
+
pauseGoalRun(run);
|
|
2515
3474
|
} })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
2516
3475
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2517
3476
|
props.sessionStore.overlay = null;
|
|
@@ -2693,44 +3652,13 @@ export function App(props) {
|
|
|
2693
3652
|
// live-area transition (chat input → TaskOverlay) natively, and
|
|
2694
3653
|
// the chat history above stays in scrollback. When the overlay
|
|
2695
3654
|
// closes, the history is still there (banner included).
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
}
|
|
2700
|
-
else {
|
|
2701
|
-
if (props.sessionStore) {
|
|
2702
|
-
props.sessionStore.overlay = "tasks";
|
|
2703
|
-
if (agentLoop.isRunning)
|
|
2704
|
-
props.sessionStore.pendingResetUI = true;
|
|
2705
|
-
}
|
|
2706
|
-
setOverlay("tasks");
|
|
2707
|
-
}
|
|
3655
|
+
openOverlay("tasks");
|
|
3656
|
+
}, onToggleGoal: () => {
|
|
3657
|
+
openOverlay("goal");
|
|
2708
3658
|
}, onToggleSkills: () => {
|
|
2709
|
-
|
|
2710
|
-
props.sessionStore.overlay = "skills";
|
|
2711
|
-
props.resetUI();
|
|
2712
|
-
}
|
|
2713
|
-
else {
|
|
2714
|
-
if (props.sessionStore) {
|
|
2715
|
-
props.sessionStore.overlay = "skills";
|
|
2716
|
-
if (agentLoop.isRunning)
|
|
2717
|
-
props.sessionStore.pendingResetUI = true;
|
|
2718
|
-
}
|
|
2719
|
-
setOverlay("skills");
|
|
2720
|
-
}
|
|
3659
|
+
openOverlay("skills");
|
|
2721
3660
|
}, onTogglePixel: () => {
|
|
2722
|
-
|
|
2723
|
-
props.sessionStore.overlay = "pixel";
|
|
2724
|
-
props.resetUI();
|
|
2725
|
-
}
|
|
2726
|
-
else {
|
|
2727
|
-
if (props.sessionStore) {
|
|
2728
|
-
props.sessionStore.overlay = "pixel";
|
|
2729
|
-
if (agentLoop.isRunning)
|
|
2730
|
-
props.sessionStore.pendingResetUI = true;
|
|
2731
|
-
}
|
|
2732
|
-
setOverlay("pixel");
|
|
2733
|
-
}
|
|
3661
|
+
openOverlay("pixel");
|
|
2734
3662
|
}, onTogglePlanMode: () => {
|
|
2735
3663
|
const next = !planMode;
|
|
2736
3664
|
setPlanMode(next);
|
|
@@ -2744,7 +3672,12 @@ export function App(props) {
|
|
|
2744
3672
|
id: getId(),
|
|
2745
3673
|
},
|
|
2746
3674
|
]);
|
|
2747
|
-
}, 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 })
|
|
3675
|
+
}, 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 ||
|
|
3676
|
+
footerStatusLayout.hasEyesSignals ||
|
|
3677
|
+
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 ||
|
|
3678
|
+
(!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
|
|
3679
|
+
? 1
|
|
3680
|
+
: 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
|
|
2748
3681
|
}
|
|
2749
3682
|
function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
|
|
2750
3683
|
const status = enabled ? "on" : "off";
|