@kenkaiiii/ggcoder 4.3.205 → 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.
Files changed (193) hide show
  1. package/dist/cli.js +38 -26
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.js +2 -2
  4. package/dist/core/compaction/compactor.d.ts.map +1 -1
  5. package/dist/core/compaction/compactor.js +6 -0
  6. package/dist/core/compaction/compactor.js.map +1 -1
  7. package/dist/core/compaction/compactor.test.js +16 -0
  8. package/dist/core/compaction/compactor.test.js.map +1 -1
  9. package/dist/core/goal-controller.d.ts +57 -0
  10. package/dist/core/goal-controller.d.ts.map +1 -0
  11. package/dist/core/goal-controller.js +285 -0
  12. package/dist/core/goal-controller.js.map +1 -0
  13. package/dist/core/goal-controller.test.d.ts +2 -0
  14. package/dist/core/goal-controller.test.d.ts.map +1 -0
  15. package/dist/core/goal-controller.test.js +419 -0
  16. package/dist/core/goal-controller.test.js.map +1 -0
  17. package/dist/core/goal-lifecycle-smoke.test.d.ts +2 -0
  18. package/dist/core/goal-lifecycle-smoke.test.d.ts.map +1 -0
  19. package/dist/core/goal-lifecycle-smoke.test.js +207 -0
  20. package/dist/core/goal-lifecycle-smoke.test.js.map +1 -0
  21. package/dist/core/goal-store.d.ts +164 -0
  22. package/dist/core/goal-store.d.ts.map +1 -0
  23. package/dist/core/goal-store.js +721 -0
  24. package/dist/core/goal-store.js.map +1 -0
  25. package/dist/core/goal-store.test.d.ts +2 -0
  26. package/dist/core/goal-store.test.d.ts.map +1 -0
  27. package/dist/core/goal-store.test.js +341 -0
  28. package/dist/core/goal-store.test.js.map +1 -0
  29. package/dist/core/goal-verifier.d.ts +17 -0
  30. package/dist/core/goal-verifier.d.ts.map +1 -0
  31. package/dist/core/goal-verifier.js +84 -0
  32. package/dist/core/goal-verifier.js.map +1 -0
  33. package/dist/core/goal-verifier.test.d.ts +2 -0
  34. package/dist/core/goal-verifier.test.d.ts.map +1 -0
  35. package/dist/core/goal-verifier.test.js +88 -0
  36. package/dist/core/goal-verifier.test.js.map +1 -0
  37. package/dist/core/goal-worker-dev-server-lifecycle.test.d.ts +2 -0
  38. package/dist/core/goal-worker-dev-server-lifecycle.test.d.ts.map +1 -0
  39. package/dist/core/goal-worker-dev-server-lifecycle.test.js +68 -0
  40. package/dist/core/goal-worker-dev-server-lifecycle.test.js.map +1 -0
  41. package/dist/core/goal-worker.d.ts +51 -0
  42. package/dist/core/goal-worker.d.ts.map +1 -0
  43. package/dist/core/goal-worker.js +339 -0
  44. package/dist/core/goal-worker.js.map +1 -0
  45. package/dist/core/goal-worker.test.d.ts +2 -0
  46. package/dist/core/goal-worker.test.d.ts.map +1 -0
  47. package/dist/core/goal-worker.test.js +224 -0
  48. package/dist/core/goal-worker.test.js.map +1 -0
  49. package/dist/core/model-registry.test.js +51 -1
  50. package/dist/core/model-registry.test.js.map +1 -1
  51. package/dist/core/oauth/gemini.d.ts.map +1 -1
  52. package/dist/core/oauth/gemini.js +138 -30
  53. package/dist/core/oauth/gemini.js.map +1 -1
  54. package/dist/core/oauth/gemini.test.d.ts +2 -0
  55. package/dist/core/oauth/gemini.test.d.ts.map +1 -0
  56. package/dist/core/oauth/gemini.test.js +154 -0
  57. package/dist/core/oauth/gemini.test.js.map +1 -0
  58. package/dist/core/process-manager-dev-server-repro.test.d.ts +2 -0
  59. package/dist/core/process-manager-dev-server-repro.test.d.ts.map +1 -0
  60. package/dist/core/process-manager-dev-server-repro.test.js +100 -0
  61. package/dist/core/process-manager-dev-server-repro.test.js.map +1 -0
  62. package/dist/core/process-manager.js +2 -2
  63. package/dist/core/process-manager.js.map +1 -1
  64. package/dist/core/prompt-commands.d.ts.map +1 -1
  65. package/dist/core/prompt-commands.js +125 -0
  66. package/dist/core/prompt-commands.js.map +1 -1
  67. package/dist/core/prompt-commands.test.js +38 -0
  68. package/dist/core/prompt-commands.test.js.map +1 -1
  69. package/dist/index.d.ts +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +1 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/interactive.d.ts.map +1 -1
  74. package/dist/interactive.js +20 -11
  75. package/dist/interactive.js.map +1 -1
  76. package/dist/system-prompt.d.ts.map +1 -1
  77. package/dist/system-prompt.js +19 -50
  78. package/dist/system-prompt.js.map +1 -1
  79. package/dist/system-prompt.test.js +124 -1
  80. package/dist/system-prompt.test.js.map +1 -1
  81. package/dist/tools/edit-diff.d.ts.map +1 -1
  82. package/dist/tools/edit-diff.js +71 -32
  83. package/dist/tools/edit-diff.js.map +1 -1
  84. package/dist/tools/edit-diff.test.js +14 -0
  85. package/dist/tools/edit-diff.test.js.map +1 -1
  86. package/dist/tools/edit.d.ts.map +1 -1
  87. package/dist/tools/edit.js +38 -18
  88. package/dist/tools/edit.js.map +1 -1
  89. package/dist/tools/edit.test.js +56 -6
  90. package/dist/tools/edit.test.js.map +1 -1
  91. package/dist/tools/enter-plan.d.ts.map +1 -1
  92. package/dist/tools/enter-plan.js +1 -0
  93. package/dist/tools/enter-plan.js.map +1 -1
  94. package/dist/tools/goals.d.ts +110 -0
  95. package/dist/tools/goals.d.ts.map +1 -0
  96. package/dist/tools/goals.js +500 -0
  97. package/dist/tools/goals.js.map +1 -0
  98. package/dist/tools/goals.test.d.ts +2 -0
  99. package/dist/tools/goals.test.d.ts.map +1 -0
  100. package/dist/tools/goals.test.js +431 -0
  101. package/dist/tools/goals.test.js.map +1 -0
  102. package/dist/tools/index.d.ts +2 -0
  103. package/dist/tools/index.d.ts.map +1 -1
  104. package/dist/tools/index.js +6 -0
  105. package/dist/tools/index.js.map +1 -1
  106. package/dist/tools/prompt-hints.d.ts.map +1 -1
  107. package/dist/tools/prompt-hints.js +2 -0
  108. package/dist/tools/prompt-hints.js.map +1 -1
  109. package/dist/tools/source-path.d.ts +9 -0
  110. package/dist/tools/source-path.d.ts.map +1 -0
  111. package/dist/tools/source-path.js +119 -0
  112. package/dist/tools/source-path.js.map +1 -0
  113. package/dist/tools/source-path.test.d.ts +2 -0
  114. package/dist/tools/source-path.test.d.ts.map +1 -0
  115. package/dist/tools/source-path.test.js +80 -0
  116. package/dist/tools/source-path.test.js.map +1 -0
  117. package/dist/tools/subagent.js +16 -0
  118. package/dist/tools/subagent.js.map +1 -1
  119. package/dist/ui/App.d.ts +73 -4
  120. package/dist/ui/App.d.ts.map +1 -1
  121. package/dist/ui/App.js +1068 -140
  122. package/dist/ui/App.js.map +1 -1
  123. package/dist/ui/activity-phrases.d.ts.map +1 -1
  124. package/dist/ui/activity-phrases.js +7 -3
  125. package/dist/ui/activity-phrases.js.map +1 -1
  126. package/dist/ui/app-state-persistence.test.d.ts +2 -0
  127. package/dist/ui/app-state-persistence.test.d.ts.map +1 -0
  128. package/dist/ui/app-state-persistence.test.js +130 -0
  129. package/dist/ui/app-state-persistence.test.js.map +1 -0
  130. package/dist/ui/components/BackgroundTasksBar.d.ts +16 -1
  131. package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
  132. package/dist/ui/components/BackgroundTasksBar.js +15 -2
  133. package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
  134. package/dist/ui/components/Banner.d.ts +2 -1
  135. package/dist/ui/components/Banner.d.ts.map +1 -1
  136. package/dist/ui/components/Banner.js +3 -3
  137. package/dist/ui/components/Banner.js.map +1 -1
  138. package/dist/ui/components/GoalOverlay.d.ts +74 -0
  139. package/dist/ui/components/GoalOverlay.d.ts.map +1 -0
  140. package/dist/ui/components/GoalOverlay.js +675 -0
  141. package/dist/ui/components/GoalOverlay.js.map +1 -0
  142. package/dist/ui/components/GoalStatusBar.d.ts +24 -0
  143. package/dist/ui/components/GoalStatusBar.d.ts.map +1 -0
  144. package/dist/ui/components/GoalStatusBar.js +113 -0
  145. package/dist/ui/components/GoalStatusBar.js.map +1 -0
  146. package/dist/ui/components/InputArea.d.ts +2 -1
  147. package/dist/ui/components/InputArea.d.ts.map +1 -1
  148. package/dist/ui/components/InputArea.js +44 -2
  149. package/dist/ui/components/InputArea.js.map +1 -1
  150. package/dist/ui/components/InputArea.test.d.ts +2 -0
  151. package/dist/ui/components/InputArea.test.d.ts.map +1 -0
  152. package/dist/ui/components/InputArea.test.js +79 -0
  153. package/dist/ui/components/InputArea.test.js.map +1 -0
  154. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  155. package/dist/ui/components/ToolExecution.js +96 -3
  156. package/dist/ui/components/ToolExecution.js.map +1 -1
  157. package/dist/ui/footer-status-layout.test.d.ts +2 -0
  158. package/dist/ui/footer-status-layout.test.d.ts.map +1 -0
  159. package/dist/ui/footer-status-layout.test.js +56 -0
  160. package/dist/ui/footer-status-layout.test.js.map +1 -0
  161. package/dist/ui/goal-events.d.ts +107 -0
  162. package/dist/ui/goal-events.d.ts.map +1 -0
  163. package/dist/ui/goal-events.js +323 -0
  164. package/dist/ui/goal-events.js.map +1 -0
  165. package/dist/ui/goal-events.test.d.ts +2 -0
  166. package/dist/ui/goal-events.test.d.ts.map +1 -0
  167. package/dist/ui/goal-events.test.js +293 -0
  168. package/dist/ui/goal-events.test.js.map +1 -0
  169. package/dist/ui/goal-overlay.test.d.ts +2 -0
  170. package/dist/ui/goal-overlay.test.d.ts.map +1 -0
  171. package/dist/ui/goal-overlay.test.js +276 -0
  172. package/dist/ui/goal-overlay.test.js.map +1 -0
  173. package/dist/ui/goal-status-bar.test.d.ts +2 -0
  174. package/dist/ui/goal-status-bar.test.d.ts.map +1 -0
  175. package/dist/ui/goal-status-bar.test.js +143 -0
  176. package/dist/ui/goal-status-bar.test.js.map +1 -0
  177. package/dist/ui/live-item-flush.test.js +48 -0
  178. package/dist/ui/live-item-flush.test.js.map +1 -1
  179. package/dist/ui/render.d.ts +11 -4
  180. package/dist/ui/render.d.ts.map +1 -1
  181. package/dist/ui/render.js +12 -3
  182. package/dist/ui/render.js.map +1 -1
  183. package/dist/ui/scroll-stabilization.test.d.ts +2 -0
  184. package/dist/ui/scroll-stabilization.test.d.ts.map +1 -0
  185. package/dist/ui/scroll-stabilization.test.js +70 -0
  186. package/dist/ui/scroll-stabilization.test.js.map +1 -0
  187. package/dist/ui/slash-command-images.test.d.ts +2 -0
  188. package/dist/ui/slash-command-images.test.d.ts.map +1 -0
  189. package/dist/ui/slash-command-images.test.js +47 -0
  190. package/dist/ui/slash-command-images.test.js.map +1 -0
  191. package/dist/utils/format.js +44 -0
  192. package/dist/utils/format.js.map +1 -1
  193. 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 ggcoder bugs should be reported. Surfaced in the guidance line. */
61
67
  const GGCODER_BUG_REPORT_URL = "github.com/kenkaiiii/gg-framework/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/ggcoder-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,12 +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
- const [liveItems, setLiveItems] = useState([]);
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
529
  // props.initialOverlay (CLI launched with one), then null.
318
530
  const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
319
531
  const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
532
+ const [goalCount, setGoalCount] = useState(0);
533
+ const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
320
534
  const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
321
535
  const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
322
536
  // Seed from sessionStore so "Run All" chaining survives the resetUI()
@@ -324,13 +538,17 @@ export function App(props) {
324
538
  const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
325
539
  const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
326
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(() => { });
327
545
  const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
328
546
  const currentPixelFixRef = useRef(null);
329
547
  const startPixelFixRef = useRef(() => { });
330
548
  const cwdRef = useRef(props.cwd);
331
549
  const [displayedCwd, setDisplayedCwd] = useState(props.cwd);
332
550
  const [staticKey, setStaticKey] = useState(0);
333
- const [doneStatus, setDoneStatus] = useState(null);
551
+ const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
334
552
  // Suppress "done" status when a plan overlay is about to open
335
553
  const planOverlayPendingRef = useRef(false);
336
554
  const [gitBranch, setGitBranch] = useState(null);
@@ -363,9 +581,12 @@ export function App(props) {
363
581
  // previous mount, triggering React's duplicate-key warning and causing
364
582
  // duplicate/omitted renders.
365
583
  const nextIdRef = useRef((() => {
366
- const hist = props.sessionStore?.history ?? props.initialHistory ?? [];
584
+ const items = [
585
+ ...(props.sessionStore?.history ?? props.initialHistory ?? []),
586
+ ...(props.sessionStore?.liveItems ?? []),
587
+ ];
367
588
  let max = -1;
368
- for (const item of hist) {
589
+ for (const item of items) {
369
590
  const n = Number(item.id);
370
591
  if (Number.isFinite(n) && n > max)
371
592
  max = n;
@@ -401,7 +622,27 @@ export function App(props) {
401
622
  * language-detection path when this cwd has never been audited before.
402
623
  */
403
624
  const triggerAutoSetupRef = useRef(async () => { });
404
- const getId = () => String(nextIdRef.current++);
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]);
405
646
  // Two-phase flush: items waiting to be moved to Static history after the
406
647
  // live area has been cleared and Ink has committed the smaller output.
407
648
  const pendingFlushRef = useRef([]);
@@ -411,8 +652,12 @@ export function App(props) {
411
652
  if (items.length === 0)
412
653
  return;
413
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
+ }
414
659
  setFlushGeneration((g) => g + 1);
415
- }, []);
660
+ }, [props.sessionStore]);
416
661
  // Mirror runtime state choices (model/provider/thinking) into renderApp's
417
662
  // closure so unmount/remount preserves them.
418
663
  const onRuntimeStateChange = props.onRuntimeStateChange;
@@ -436,6 +681,14 @@ export function App(props) {
436
681
  if (sessionStore)
437
682
  sessionStore.history = history;
438
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]);
439
692
  useEffect(() => {
440
693
  if (sessionStore)
441
694
  sessionStore.planSteps = planSteps;
@@ -448,6 +701,10 @@ export function App(props) {
448
701
  if (sessionStore)
449
702
  sessionStore.overlay = overlay;
450
703
  }, [overlay, sessionStore]);
704
+ useEffect(() => {
705
+ if (sessionStore)
706
+ sessionStore.goalStatusEntries = goalStatusEntries;
707
+ }, [goalStatusEntries, sessionStore]);
451
708
  // pendingAction is consumed via a useEffect AFTER agentLoop is created
452
709
  // — see below where useAgentLoop is set up.
453
710
  const pendingActionConsumedRef = useRef(false);
@@ -463,6 +720,36 @@ export function App(props) {
463
720
  useEffect(() => {
464
721
  getGitBranch(displayedCwd).then(setGitBranch);
465
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]);
466
753
  // Periodic update check during long sessions
467
754
  useEffect(() => {
468
755
  startPeriodicUpdateCheck(props.version, (msg) => {
@@ -712,7 +999,8 @@ export function App(props) {
712
999
  });
713
1000
  }
714
1001
  }, [props.settingsFile]);
715
- const compactConversation = useCallback(async (messages) => {
1002
+ const compactionAbortRef = useRef(null);
1003
+ const compactConversation = useCallback(async (messages, signal) => {
716
1004
  const contextWindow = getContextWindow(currentModel, contextWindowOptions);
717
1005
  const tokensBefore = estimateConversationTokens(messages);
718
1006
  const spinId = getId();
@@ -723,6 +1011,10 @@ export function App(props) {
723
1011
  });
724
1012
  // Show animated spinner
725
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;
726
1018
  try {
727
1019
  // Resolve fresh credentials for compaction too
728
1020
  let compactApiKey = activeApiKey;
@@ -744,7 +1036,7 @@ export function App(props) {
744
1036
  projectId: compactProjectId,
745
1037
  baseUrl: compactBaseUrl,
746
1038
  contextWindow,
747
- signal: undefined,
1039
+ signal: compactionSignal,
748
1040
  approvedPlanPath: approvedPlanPathRef.current,
749
1041
  });
750
1042
  if (result.result.compacted) {
@@ -769,10 +1061,16 @@ export function App(props) {
769
1061
  }
770
1062
  catch (err) {
771
1063
  const msg = err instanceof Error ? err.message : String(err);
772
- log("ERROR", "compaction", `Compaction failed: ${msg}`);
773
- // Replace spinner with error
774
- setLiveItems((prev) => prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
775
- return messages; // Return unchanged on failure
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;
776
1074
  }
777
1075
  }, [
778
1076
  currentModel,
@@ -1414,6 +1712,11 @@ export function App(props) {
1414
1712
  }
1415
1713
  }, 500);
1416
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
+ }
1417
1720
  // Pixel fix: observe branch + commits, patch status, optionally pick
1418
1721
  // up the next open error if run-all is active.
1419
1722
  const pendingFix = currentPixelFixRef.current;
@@ -1500,13 +1803,34 @@ export function App(props) {
1500
1803
  }, []),
1501
1804
  onQueuedStart: useCallback((content) => {
1502
1805
  // When a queued message starts processing, show it as a UserItem
1503
- // 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.
1504
1808
  const displayText = typeof content === "string"
1505
1809
  ? content
1506
1810
  : content
1507
1811
  .filter((c) => c.type === "text")
1508
1812
  .map((c) => c.text)
1509
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
+ }
1510
1834
  const imageCount = typeof content === "string"
1511
1835
  ? undefined
1512
1836
  : content.filter((c) => c.type === "image").length || undefined;
@@ -1605,7 +1929,14 @@ export function App(props) {
1605
1929
  if (pendingFlushRef.current.length > 0) {
1606
1930
  const items = pendingFlushRef.current;
1607
1931
  pendingFlushRef.current = [];
1608
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(items)]));
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;
1609
1940
  }
1610
1941
  }, [flushGeneration]);
1611
1942
  // Sync terminal title with agent loop state
@@ -1698,11 +2029,15 @@ export function App(props) {
1698
2029
  }
1699
2030
  // Handle /compact — compact conversation
1700
2031
  if (trimmed === "/compact" || trimmed === "/c") {
1701
- const compacted = await compactConversation(messagesRef.current);
1702
- if (compacted !== messagesRef.current) {
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) {
1703
2036
  messagesRef.current = compacted;
1704
2037
  await persistCompactedSession(compacted);
1705
2038
  }
2039
+ if (compactionAbortRef.current === ac)
2040
+ compactionAbortRef.current = null;
1706
2041
  return;
1707
2042
  }
1708
2043
  // Handle /quit — exit the agent
@@ -1847,6 +2182,27 @@ export function App(props) {
1847
2182
  ]);
1848
2183
  return;
1849
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
+ }
1850
2206
  // Handle /plans — open plan pane
1851
2207
  if (trimmed === "/plans") {
1852
2208
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
@@ -1867,49 +2223,49 @@ export function App(props) {
1867
2223
  return;
1868
2224
  }
1869
2225
  // Handle prompt-template commands (built-in + custom from .gg/commands/)
1870
- if (trimmed.startsWith("/")) {
1871
- const parts = trimmed.slice(1).split(" ");
1872
- const cmdName = parts[0];
1873
- const cmdArgs = parts.slice(1).join(" ").trim();
1874
- const builtinCmd = getPromptCommand(cmdName);
1875
- const customCmd = !builtinCmd ? customCommands.find((c) => c.name === cmdName) : undefined;
1876
- const promptText = builtinCmd?.prompt ?? customCmd?.prompt;
1877
- if (promptText) {
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
- ]);
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];
1908
2234
  }
1909
- // Reload custom commands in case a setup command created new ones
1910
- reloadCustomCommands();
1911
- return;
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);
1912
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;
1913
2269
  }
1914
2270
  // Check slash commands
1915
2271
  if (props.onSlashCommand && input.startsWith("/")) {
@@ -1923,47 +2279,7 @@ export function App(props) {
1923
2279
  const hasImages = inputImages.length > 0;
1924
2280
  const modelInfo = getModel(currentModel);
1925
2281
  const modelSupportsImages = modelInfo?.supportsImages ?? true;
1926
- let userContent;
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/ggcoder-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
- }
2282
+ const userContent = buildUserContentWithAttachments(input, inputImages, modelSupportsImages);
1967
2283
  // ── Queue message if agent is already running ──
1968
2284
  if (agentLoop.isRunning) {
1969
2285
  log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
@@ -2045,6 +2361,9 @@ export function App(props) {
2045
2361
  agentLoop.clearQueue();
2046
2362
  agentLoop.abort();
2047
2363
  }
2364
+ else if (compactionAbortRef.current) {
2365
+ compactionAbortRef.current.abort();
2366
+ }
2048
2367
  else {
2049
2368
  handleDoubleExit();
2050
2369
  }
@@ -2175,6 +2494,7 @@ export function App(props) {
2175
2494
  };
2176
2495
  const promptOrder = [
2177
2496
  // Project audits / one-shot analysis
2497
+ "goal",
2178
2498
  "init",
2179
2499
  "research",
2180
2500
  "scan",
@@ -2220,11 +2540,29 @@ export function App(props) {
2220
2540
  case "tombstone":
2221
2541
  return null;
2222
2542
  case "banner":
2223
- 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));
2224
2544
  case "user":
2225
2545
  return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
2226
2546
  case "task":
2227
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
+ }
2228
2566
  case "style_pack": {
2229
2567
  const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
2230
2568
  const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
@@ -2380,13 +2718,602 @@ export function App(props) {
2380
2718
  currentProvider,
2381
2719
  currentModel,
2382
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]);
2383
3306
  // Keep refs in sync for access from stale closures (onDone)
2384
3307
  startTaskRef.current = startTask;
3308
+ startGoalRunRef.current = startGoalRun;
2385
3309
  useEffect(() => {
2386
3310
  runAllTasksRef.current = runAllTasks;
2387
3311
  if (props.sessionStore)
2388
3312
  props.sessionStore.runAllTasks = runAllTasks;
2389
3313
  }, [runAllTasks, props.sessionStore]);
3314
+ useEffect(() => {
3315
+ agentRunningRef.current = agentLoop.isRunning;
3316
+ }, [agentLoop.isRunning]);
2390
3317
  const startPixelFix = useCallback((errorId) => {
2391
3318
  void (async () => {
2392
3319
  try {
@@ -2487,12 +3414,29 @@ export function App(props) {
2487
3414
  props.sessionStore.runAllPixel = runAllPixel;
2488
3415
  }, [runAllPixel, props.sessionStore]);
2489
3416
  const isTaskView = overlay === "tasks";
3417
+ const isGoalView = overlay === "goal";
2490
3418
  const isSkillsView = overlay === "skills";
2491
3419
  const isPlanView = overlay === "plan";
2492
3420
  const isEyesView = overlay === "eyes";
3421
+ const footerStatusLayout = getFooterStatusLayoutDecision({
3422
+ columns,
3423
+ backgroundTaskCount: bgTasks.length,
3424
+ eyesCount,
3425
+ updatePending,
3426
+ });
2493
3427
  const isPixelView = overlay === "pixel";
2494
- const isOverlayView = isTaskView || isSkillsView || isPlanView || isEyesView || isPixelView;
2495
- return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: isOverlayView && !agentLoop.isRunning ? [] : history, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, `${resizeKey}-${staticKey}`), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
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: () => {
2496
3440
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2497
3441
  props.sessionStore.overlay = null;
2498
3442
  props.resetUI();
@@ -2517,6 +3461,16 @@ export function App(props) {
2517
3461
  markTaskInProgress(props.cwd, next.id);
2518
3462
  startTask(next.title, next.prompt, next.id);
2519
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);
2520
3474
  } })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
2521
3475
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2522
3476
  props.sessionStore.overlay = null;
@@ -2698,44 +3652,13 @@ export function App(props) {
2698
3652
  // live-area transition (chat input → TaskOverlay) natively, and
2699
3653
  // the chat history above stays in scrollback. When the overlay
2700
3654
  // closes, the history is still there (banner included).
2701
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2702
- props.sessionStore.overlay = "tasks";
2703
- props.resetUI();
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
- }
3655
+ openOverlay("tasks");
3656
+ }, onToggleGoal: () => {
3657
+ openOverlay("goal");
2713
3658
  }, onToggleSkills: () => {
2714
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
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
- }
3659
+ openOverlay("skills");
2726
3660
  }, onTogglePixel: () => {
2727
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
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
- }
3661
+ openOverlay("pixel");
2739
3662
  }, onTogglePlanMode: () => {
2740
3663
  const next = !planMode;
2741
3664
  setPlanMode(next);
@@ -2749,7 +3672,12 @@ export function App(props) {
2749
3672
  id: getId(),
2750
3673
  },
2751
3674
  ]);
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 })), (bgTasks.length > 0 || (eyesCount !== undefined && eyesCount > 0) || updatePending) && (_jsxs(Box, { children: [bgTasks.length > 0 && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate })), eyesCount !== undefined && eyesCount > 0 && (_jsx(Box, { paddingLeft: bgTasks.length > 0 ? 2 : 1, paddingRight: 1, children: _jsx(Text, { color: theme.accent, bold: true, children: `${eyesCount} eyes signal${eyesCount === 1 ? "" : "s"} · Run /eyes-improve to enhance GG Coder` }) })), updatePending && (_jsx(Box, { paddingLeft: bgTasks.length > 0 || (eyesCount !== undefined && eyesCount > 0) ? 2 : 1, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
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 GG 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" }) }))] }))] }))] }));
2753
3681
  }
2754
3682
  function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
2755
3683
  const status = enabled ? "on" : "off";