@kenkaiiii/ggcoder 4.3.205 → 4.3.206

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 (173) 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 +377 -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.d.ts +47 -0
  38. package/dist/core/goal-worker.d.ts.map +1 -0
  39. package/dist/core/goal-worker.js +329 -0
  40. package/dist/core/goal-worker.js.map +1 -0
  41. package/dist/core/goal-worker.test.d.ts +2 -0
  42. package/dist/core/goal-worker.test.d.ts.map +1 -0
  43. package/dist/core/goal-worker.test.js +206 -0
  44. package/dist/core/goal-worker.test.js.map +1 -0
  45. package/dist/core/oauth/gemini.d.ts.map +1 -1
  46. package/dist/core/oauth/gemini.js +138 -30
  47. package/dist/core/oauth/gemini.js.map +1 -1
  48. package/dist/core/oauth/gemini.test.d.ts +2 -0
  49. package/dist/core/oauth/gemini.test.d.ts.map +1 -0
  50. package/dist/core/oauth/gemini.test.js +154 -0
  51. package/dist/core/oauth/gemini.test.js.map +1 -0
  52. package/dist/core/prompt-commands.d.ts.map +1 -1
  53. package/dist/core/prompt-commands.js +124 -0
  54. package/dist/core/prompt-commands.js.map +1 -1
  55. package/dist/core/prompt-commands.test.js +36 -0
  56. package/dist/core/prompt-commands.test.js.map +1 -1
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +1 -1
  60. package/dist/index.js.map +1 -1
  61. package/dist/interactive.d.ts.map +1 -1
  62. package/dist/interactive.js +20 -11
  63. package/dist/interactive.js.map +1 -1
  64. package/dist/system-prompt.d.ts.map +1 -1
  65. package/dist/system-prompt.js +18 -50
  66. package/dist/system-prompt.js.map +1 -1
  67. package/dist/system-prompt.test.js +124 -1
  68. package/dist/system-prompt.test.js.map +1 -1
  69. package/dist/tools/edit-diff.d.ts.map +1 -1
  70. package/dist/tools/edit-diff.js +71 -32
  71. package/dist/tools/edit-diff.js.map +1 -1
  72. package/dist/tools/edit-diff.test.js +14 -0
  73. package/dist/tools/edit-diff.test.js.map +1 -1
  74. package/dist/tools/edit.d.ts.map +1 -1
  75. package/dist/tools/edit.js +16 -6
  76. package/dist/tools/edit.js.map +1 -1
  77. package/dist/tools/edit.test.js +27 -0
  78. package/dist/tools/edit.test.js.map +1 -1
  79. package/dist/tools/enter-plan.d.ts.map +1 -1
  80. package/dist/tools/enter-plan.js +1 -0
  81. package/dist/tools/enter-plan.js.map +1 -1
  82. package/dist/tools/goals.d.ts +110 -0
  83. package/dist/tools/goals.d.ts.map +1 -0
  84. package/dist/tools/goals.js +500 -0
  85. package/dist/tools/goals.js.map +1 -0
  86. package/dist/tools/goals.test.d.ts +2 -0
  87. package/dist/tools/goals.test.d.ts.map +1 -0
  88. package/dist/tools/goals.test.js +431 -0
  89. package/dist/tools/goals.test.js.map +1 -0
  90. package/dist/tools/index.d.ts +2 -0
  91. package/dist/tools/index.d.ts.map +1 -1
  92. package/dist/tools/index.js +6 -0
  93. package/dist/tools/index.js.map +1 -1
  94. package/dist/tools/prompt-hints.d.ts.map +1 -1
  95. package/dist/tools/prompt-hints.js +2 -0
  96. package/dist/tools/prompt-hints.js.map +1 -1
  97. package/dist/tools/source-path.d.ts +9 -0
  98. package/dist/tools/source-path.d.ts.map +1 -0
  99. package/dist/tools/source-path.js +119 -0
  100. package/dist/tools/source-path.js.map +1 -0
  101. package/dist/tools/source-path.test.d.ts +2 -0
  102. package/dist/tools/source-path.test.d.ts.map +1 -0
  103. package/dist/tools/source-path.test.js +80 -0
  104. package/dist/tools/source-path.test.js.map +1 -0
  105. package/dist/tools/subagent.js +16 -0
  106. package/dist/tools/subagent.js.map +1 -1
  107. package/dist/ui/App.d.ts +36 -3
  108. package/dist/ui/App.d.ts.map +1 -1
  109. package/dist/ui/App.js +879 -57
  110. package/dist/ui/App.js.map +1 -1
  111. package/dist/ui/activity-phrases.d.ts.map +1 -1
  112. package/dist/ui/activity-phrases.js +7 -3
  113. package/dist/ui/activity-phrases.js.map +1 -1
  114. package/dist/ui/app-state-persistence.test.d.ts +2 -0
  115. package/dist/ui/app-state-persistence.test.d.ts.map +1 -0
  116. package/dist/ui/app-state-persistence.test.js +56 -0
  117. package/dist/ui/app-state-persistence.test.js.map +1 -0
  118. package/dist/ui/components/BackgroundTasksBar.d.ts +16 -1
  119. package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
  120. package/dist/ui/components/BackgroundTasksBar.js +15 -2
  121. package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
  122. package/dist/ui/components/Banner.d.ts +2 -1
  123. package/dist/ui/components/Banner.d.ts.map +1 -1
  124. package/dist/ui/components/Banner.js +3 -3
  125. package/dist/ui/components/Banner.js.map +1 -1
  126. package/dist/ui/components/GoalOverlay.d.ts +21 -0
  127. package/dist/ui/components/GoalOverlay.d.ts.map +1 -0
  128. package/dist/ui/components/GoalOverlay.js +336 -0
  129. package/dist/ui/components/GoalOverlay.js.map +1 -0
  130. package/dist/ui/components/GoalStatusBar.d.ts +24 -0
  131. package/dist/ui/components/GoalStatusBar.d.ts.map +1 -0
  132. package/dist/ui/components/GoalStatusBar.js +113 -0
  133. package/dist/ui/components/GoalStatusBar.js.map +1 -0
  134. package/dist/ui/components/InputArea.d.ts +2 -1
  135. package/dist/ui/components/InputArea.d.ts.map +1 -1
  136. package/dist/ui/components/InputArea.js +6 -1
  137. package/dist/ui/components/InputArea.js.map +1 -1
  138. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  139. package/dist/ui/components/ToolExecution.js +94 -1
  140. package/dist/ui/components/ToolExecution.js.map +1 -1
  141. package/dist/ui/footer-status-layout.test.d.ts +2 -0
  142. package/dist/ui/footer-status-layout.test.d.ts.map +1 -0
  143. package/dist/ui/footer-status-layout.test.js +56 -0
  144. package/dist/ui/footer-status-layout.test.js.map +1 -0
  145. package/dist/ui/goal-events.d.ts +20 -0
  146. package/dist/ui/goal-events.d.ts.map +1 -0
  147. package/dist/ui/goal-events.js +102 -0
  148. package/dist/ui/goal-events.js.map +1 -0
  149. package/dist/ui/goal-events.test.d.ts +2 -0
  150. package/dist/ui/goal-events.test.d.ts.map +1 -0
  151. package/dist/ui/goal-events.test.js +208 -0
  152. package/dist/ui/goal-events.test.js.map +1 -0
  153. package/dist/ui/goal-overlay.test.d.ts +2 -0
  154. package/dist/ui/goal-overlay.test.d.ts.map +1 -0
  155. package/dist/ui/goal-overlay.test.js +122 -0
  156. package/dist/ui/goal-overlay.test.js.map +1 -0
  157. package/dist/ui/goal-status-bar.test.d.ts +2 -0
  158. package/dist/ui/goal-status-bar.test.d.ts.map +1 -0
  159. package/dist/ui/goal-status-bar.test.js +143 -0
  160. package/dist/ui/goal-status-bar.test.js.map +1 -0
  161. package/dist/ui/live-item-flush.test.js +48 -0
  162. package/dist/ui/live-item-flush.test.js.map +1 -1
  163. package/dist/ui/render.d.ts +8 -3
  164. package/dist/ui/render.d.ts.map +1 -1
  165. package/dist/ui/render.js +10 -3
  166. package/dist/ui/render.js.map +1 -1
  167. package/dist/ui/scroll-stabilization.test.d.ts +2 -0
  168. package/dist/ui/scroll-stabilization.test.d.ts.map +1 -0
  169. package/dist/ui/scroll-stabilization.test.js +23 -0
  170. package/dist/ui/scroll-stabilization.test.js.map +1 -0
  171. package/dist/utils/format.js +44 -0
  172. package/dist/utils/format.js.map +1 -1
  173. 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
  /**
@@ -114,6 +120,77 @@ function compactHistory(items) {
114
120
  }
115
121
  return compacted;
116
122
  }
123
+ function summarizeGoalCompletion(summary) {
124
+ const lines = summary
125
+ .split("\n")
126
+ .map((line) => line.trim())
127
+ .filter((line) => line.length > 0 && line !== "[agent_done]");
128
+ const statusLine = lines.find((line) => /^Status:/i.test(line));
129
+ const changedLine = lines.find((line) => /^(Changed|Implemented|Fixed|Added|Key findings|Full verifier)/i.test(line));
130
+ const verificationLine = lines.find((line) => /^(Verification|Verified|Result):/i.test(line));
131
+ return statusLine ?? changedLine ?? verificationLine ?? lines[0];
132
+ }
133
+ function formatGoalWorkerFinishedTitle(taskTitle, status) {
134
+ return status === "done"
135
+ ? `Worker finished: ${taskTitle}. Reporting back.`
136
+ : `Worker failed: ${taskTitle}. Reporting back.`;
137
+ }
138
+ export function formatGoalTerminalProgress(run) {
139
+ switch (run.status) {
140
+ case "passed":
141
+ return {
142
+ kind: "goal_progress",
143
+ phase: "terminal",
144
+ title: `Goal passed: ${run.title}`,
145
+ detail: "Verifier evidence is recorded; auto-continuation stopped.",
146
+ status: run.status,
147
+ };
148
+ case "failed":
149
+ return {
150
+ kind: "goal_progress",
151
+ phase: "terminal",
152
+ title: `Goal failed: ${run.title}`,
153
+ detail: "Auto-continuation stopped. Check Goal tasks for the failing step.",
154
+ status: run.status,
155
+ };
156
+ case "blocked":
157
+ return {
158
+ kind: "goal_progress",
159
+ phase: "terminal",
160
+ title: `Goal blocked: ${run.title}`,
161
+ detail: goalHasBlockingPrerequisites(run)
162
+ ? formatGoalBlockingPrerequisites(run)
163
+ : (run.blockers[0] ?? "A prerequisite or missing verifier blocked progress."),
164
+ status: run.status,
165
+ };
166
+ case "paused":
167
+ return {
168
+ kind: "goal_progress",
169
+ phase: "terminal",
170
+ title: `Goal paused: ${run.title}`,
171
+ detail: run.blockers[0] ?? "Auto-continuation paused.",
172
+ status: run.status,
173
+ };
174
+ case "draft":
175
+ case "ready":
176
+ case "running":
177
+ case "verifying":
178
+ return null;
179
+ }
180
+ }
181
+ export function shouldHideHistoryForOverlayView(isOverlayView, isAgentRunning) {
182
+ // Overlay panes should be clean, full-screen views like Tasks/Plans/Skills.
183
+ // When the agent is idle, pane open/close goes through resetUI(), so history
184
+ // remains in sessionStore and is reprinted after the pane closes. While the
185
+ // agent is running we keep Static mounted because resetUI would abort the run.
186
+ return isOverlayView && !isAgentRunning;
187
+ }
188
+ export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, }) {
189
+ return {
190
+ preserveStatic: isUserScrolled && hasNewOutput,
191
+ autoFollow: !isUserScrolled,
192
+ };
193
+ }
117
194
  // flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
118
195
  /** Check whether an item is still active (running spinner, pending result). */
119
196
  function isActiveItem(item) {
@@ -311,12 +388,16 @@ export function App(props) {
311
388
  }
312
389
  return [{ kind: "banner", id: "banner" }];
313
390
  });
314
- // Items from the current/last turn — rendered in the live area so they stay visible
315
- const [liveItems, setLiveItems] = useState([]);
391
+ // Items from the current/last turn — rendered in the live area so they stay visible.
392
+ // Seed from sessionStore so Goal progress/completion rows and other live output
393
+ // survive pane/overlay/resize remounts before they are flushed to <Static>.
394
+ const [liveItems, setLiveItems] = useState(() => props.sessionStore?.liveItems ?? []);
316
395
  // overlay seeded from sessionStore (lives across remount). Falls back to
317
396
  // props.initialOverlay (CLI launched with one), then null.
318
397
  const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
319
398
  const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
399
+ const [goalCount, setGoalCount] = useState(0);
400
+ const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
320
401
  const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
321
402
  const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
322
403
  // Seed from sessionStore so "Run All" chaining survives the resetUI()
@@ -324,6 +405,10 @@ export function App(props) {
324
405
  const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
325
406
  const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
326
407
  const startTaskRef = useRef(() => { });
408
+ const agentRunningRef = useRef(false);
409
+ const runningGoalIdsRef = useRef(new Set());
410
+ const activeVerifierRunIdsRef = useRef(new Set());
411
+ const startGoalRunRef = useRef(() => { });
327
412
  const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
328
413
  const currentPixelFixRef = useRef(null);
329
414
  const startPixelFixRef = useRef(() => { });
@@ -363,9 +448,12 @@ export function App(props) {
363
448
  // previous mount, triggering React's duplicate-key warning and causing
364
449
  // duplicate/omitted renders.
365
450
  const nextIdRef = useRef((() => {
366
- const hist = props.sessionStore?.history ?? props.initialHistory ?? [];
451
+ const items = [
452
+ ...(props.sessionStore?.history ?? props.initialHistory ?? []),
453
+ ...(props.sessionStore?.liveItems ?? []),
454
+ ];
367
455
  let max = -1;
368
- for (const item of hist) {
456
+ for (const item of items) {
369
457
  const n = Number(item.id);
370
458
  if (Number.isFinite(n) && n > max)
371
459
  max = n;
@@ -401,7 +489,27 @@ export function App(props) {
401
489
  * language-detection path when this cwd has never been audited before.
402
490
  */
403
491
  const triggerAutoSetupRef = useRef(async () => { });
404
- const getId = () => String(nextIdRef.current++);
492
+ const getId = () => `ui-${nextIdRef.current++}`;
493
+ const appendGoalProgress = useCallback((item) => {
494
+ setLiveItems((prev) => [...prev, { ...item, id: getId() }]);
495
+ }, []);
496
+ const goalNumberForRun = useCallback((runId) => Math.max(1, goalStatusEntries.findIndex((entry) => entry.runId === runId) + 1), [goalStatusEntries]);
497
+ const clearGoalStatusEntry = useCallback((runId) => {
498
+ setGoalStatusEntries((prev) => {
499
+ const next = removeGoalStatusEntry(prev, runId);
500
+ if (props.sessionStore)
501
+ props.sessionStore.goalStatusEntries = next;
502
+ return next;
503
+ });
504
+ }, [props.sessionStore]);
505
+ const upsertGoalStatusEntry = useCallback((entry) => {
506
+ setGoalStatusEntries((prev) => {
507
+ const next = syncGoalStatusEntries(prev, entry);
508
+ if (props.sessionStore)
509
+ props.sessionStore.goalStatusEntries = next;
510
+ return next;
511
+ });
512
+ }, [props.sessionStore]);
405
513
  // Two-phase flush: items waiting to be moved to Static history after the
406
514
  // live area has been cleared and Ink has committed the smaller output.
407
515
  const pendingFlushRef = useRef([]);
@@ -411,8 +519,12 @@ export function App(props) {
411
519
  if (items.length === 0)
412
520
  return;
413
521
  pendingFlushRef.current = [...pendingFlushRef.current, ...items];
522
+ if (props.sessionStore) {
523
+ const queuedIds = new Set(items.map((item) => item.id));
524
+ props.sessionStore.liveItems = (props.sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
525
+ }
414
526
  setFlushGeneration((g) => g + 1);
415
- }, []);
527
+ }, [props.sessionStore]);
416
528
  // Mirror runtime state choices (model/provider/thinking) into renderApp's
417
529
  // closure so unmount/remount preserves them.
418
530
  const onRuntimeStateChange = props.onRuntimeStateChange;
@@ -436,6 +548,10 @@ export function App(props) {
436
548
  if (sessionStore)
437
549
  sessionStore.history = history;
438
550
  }, [history, sessionStore]);
551
+ useEffect(() => {
552
+ if (sessionStore)
553
+ sessionStore.liveItems = liveItems;
554
+ }, [liveItems, sessionStore]);
439
555
  useEffect(() => {
440
556
  if (sessionStore)
441
557
  sessionStore.planSteps = planSteps;
@@ -448,6 +564,10 @@ export function App(props) {
448
564
  if (sessionStore)
449
565
  sessionStore.overlay = overlay;
450
566
  }, [overlay, sessionStore]);
567
+ useEffect(() => {
568
+ if (sessionStore)
569
+ sessionStore.goalStatusEntries = goalStatusEntries;
570
+ }, [goalStatusEntries, sessionStore]);
451
571
  // pendingAction is consumed via a useEffect AFTER agentLoop is created
452
572
  // — see below where useAgentLoop is set up.
453
573
  const pendingActionConsumedRef = useRef(false);
@@ -463,6 +583,36 @@ export function App(props) {
463
583
  useEffect(() => {
464
584
  getGitBranch(displayedCwd).then(setGitBranch);
465
585
  }, [displayedCwd]);
586
+ useEffect(() => {
587
+ let cancelled = false;
588
+ const refreshGoalCount = () => {
589
+ void reconcileActiveGoalRuns(props.cwd, {
590
+ isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
591
+ }).then(({ runs }) => {
592
+ const counts = summarizeGoalCountsFromRuns(runs);
593
+ if (cancelled)
594
+ return;
595
+ setGoalCount(counts.active);
596
+ setGoalStatusEntries((prev) => {
597
+ const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
598
+ isWorkerActive: (workerId, run) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId &&
599
+ worker.goalRunId === run.id &&
600
+ worker.status === "running"),
601
+ isVerifierActive: (run) => activeVerifierRunIdsRef.current.has(run.id),
602
+ });
603
+ if (props.sessionStore)
604
+ props.sessionStore.goalStatusEntries = next;
605
+ return next;
606
+ });
607
+ });
608
+ };
609
+ refreshGoalCount();
610
+ const interval = setInterval(refreshGoalCount, 1000);
611
+ return () => {
612
+ cancelled = true;
613
+ clearInterval(interval);
614
+ };
615
+ }, [props.cwd]);
466
616
  // Periodic update check during long sessions
467
617
  useEffect(() => {
468
618
  startPeriodicUpdateCheck(props.version, (msg) => {
@@ -712,7 +862,8 @@ export function App(props) {
712
862
  });
713
863
  }
714
864
  }, [props.settingsFile]);
715
- const compactConversation = useCallback(async (messages) => {
865
+ const compactionAbortRef = useRef(null);
866
+ const compactConversation = useCallback(async (messages, signal) => {
716
867
  const contextWindow = getContextWindow(currentModel, contextWindowOptions);
717
868
  const tokensBefore = estimateConversationTokens(messages);
718
869
  const spinId = getId();
@@ -723,6 +874,10 @@ export function App(props) {
723
874
  });
724
875
  // Show animated spinner
725
876
  setLiveItems((prev) => [...prev, { kind: "compacting", id: spinId }]);
877
+ const ownedAbort = signal ? null : new AbortController();
878
+ const compactionSignal = signal ?? ownedAbort?.signal;
879
+ if (ownedAbort)
880
+ compactionAbortRef.current = ownedAbort;
726
881
  try {
727
882
  // Resolve fresh credentials for compaction too
728
883
  let compactApiKey = activeApiKey;
@@ -744,7 +899,7 @@ export function App(props) {
744
899
  projectId: compactProjectId,
745
900
  baseUrl: compactBaseUrl,
746
901
  contextWindow,
747
- signal: undefined,
902
+ signal: compactionSignal,
748
903
  approvedPlanPath: approvedPlanPathRef.current,
749
904
  });
750
905
  if (result.result.compacted) {
@@ -769,10 +924,16 @@ export function App(props) {
769
924
  }
770
925
  catch (err) {
771
926
  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
927
+ const isAbort = compactionSignal?.aborted || msg.includes("aborted") || msg.includes("abort");
928
+ log(isAbort ? "WARN" : "ERROR", "compaction", isAbort ? "Compaction aborted" : `Compaction failed: ${msg}`);
929
+ setLiveItems((prev) => isAbort
930
+ ? prev.filter((item) => item.id !== spinId)
931
+ : prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
932
+ return messages; // Return unchanged on failure/abort
933
+ }
934
+ finally {
935
+ if (ownedAbort && compactionAbortRef.current === ownedAbort)
936
+ compactionAbortRef.current = null;
776
937
  }
777
938
  }, [
778
939
  currentModel,
@@ -1414,6 +1575,11 @@ export function App(props) {
1414
1575
  }
1415
1576
  }, 500);
1416
1577
  }
1578
+ // Goal loop: after the orchestrator handles a worker/verifier event,
1579
+ // continue the same Goal automatically until it reaches a terminal state.
1580
+ for (const runId of [...runningGoalIdsRef.current]) {
1581
+ setTimeout(() => continueGoalRun(runId), 500);
1582
+ }
1417
1583
  // Pixel fix: observe branch + commits, patch status, optionally pick
1418
1584
  // up the next open error if run-all is active.
1419
1585
  const pendingFix = currentPixelFixRef.current;
@@ -1500,13 +1666,34 @@ export function App(props) {
1500
1666
  }, []),
1501
1667
  onQueuedStart: useCallback((content) => {
1502
1668
  // When a queued message starts processing, show it as a UserItem
1503
- // and flush prior items to history
1669
+ // and flush prior items to history. Synthetic system events are hidden
1670
+ // from the transcript but still routed through the main agent context.
1504
1671
  const displayText = typeof content === "string"
1505
1672
  ? content
1506
1673
  : content
1507
1674
  .filter((c) => c.type === "text")
1508
1675
  .map((c) => c.text)
1509
1676
  .join("\n");
1677
+ if (isGoalSyntheticEvent(displayText)) {
1678
+ const eventInfo = parseGoalSyntheticEvent(displayText);
1679
+ setLiveItems((prev) => {
1680
+ if (prev.length > 0)
1681
+ queueFlush(prev);
1682
+ return [];
1683
+ });
1684
+ setDoneStatus(null);
1685
+ appendGoalProgress({
1686
+ kind: "goal_progress",
1687
+ phase: "orchestrator_reviewing",
1688
+ title: "Orchestrator reviewing Goal update",
1689
+ detail: eventInfo?.kind === "worker"
1690
+ ? `Worker ${eventInfo.worker ?? "finished"} reported back${eventInfo.task ? ` on ${eventInfo.task}` : ""}. Inspecting Goal state.`
1691
+ : `Verifier reported ${eventInfo?.status ?? "status"}. Inspecting evidence and next action.`,
1692
+ workerId: eventInfo?.worker,
1693
+ status: eventInfo?.status,
1694
+ });
1695
+ return;
1696
+ }
1510
1697
  const imageCount = typeof content === "string"
1511
1698
  ? undefined
1512
1699
  : content.filter((c) => c.type === "image").length || undefined;
@@ -1605,7 +1792,14 @@ export function App(props) {
1605
1792
  if (pendingFlushRef.current.length > 0) {
1606
1793
  const items = pendingFlushRef.current;
1607
1794
  pendingFlushRef.current = [];
1608
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(items)]));
1795
+ setHistory((h) => {
1796
+ const next = compactHistory([...h, ...trimFlushedItems(items)]);
1797
+ if (sessionStore)
1798
+ sessionStore.history = next;
1799
+ return next;
1800
+ });
1801
+ if (sessionStore)
1802
+ sessionStore.liveItems = liveItems;
1609
1803
  }
1610
1804
  }, [flushGeneration]);
1611
1805
  // Sync terminal title with agent loop state
@@ -1698,11 +1892,15 @@ export function App(props) {
1698
1892
  }
1699
1893
  // Handle /compact — compact conversation
1700
1894
  if (trimmed === "/compact" || trimmed === "/c") {
1701
- const compacted = await compactConversation(messagesRef.current);
1702
- if (compacted !== messagesRef.current) {
1895
+ const ac = new AbortController();
1896
+ compactionAbortRef.current = ac;
1897
+ const compacted = await compactConversation(messagesRef.current, ac.signal);
1898
+ if (!ac.signal.aborted && compacted !== messagesRef.current) {
1703
1899
  messagesRef.current = compacted;
1704
1900
  await persistCompactedSession(compacted);
1705
1901
  }
1902
+ if (compactionAbortRef.current === ac)
1903
+ compactionAbortRef.current = null;
1706
1904
  return;
1707
1905
  }
1708
1906
  // Handle /quit — exit the agent
@@ -1847,6 +2045,27 @@ export function App(props) {
1847
2045
  ]);
1848
2046
  return;
1849
2047
  }
2048
+ // Handle /goals — open goal pane
2049
+ if (trimmed === "/goals") {
2050
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2051
+ props.sessionStore.overlay = "goal";
2052
+ props.sessionStore.planAutoExpand = false;
2053
+ props.resetUI();
2054
+ }
2055
+ else {
2056
+ stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2057
+ setStaticKey((key) => key + 1);
2058
+ if (props.sessionStore) {
2059
+ props.sessionStore.overlay = "goal";
2060
+ props.sessionStore.planAutoExpand = false;
2061
+ if (agentLoop.isRunning)
2062
+ props.sessionStore.pendingResetUI = true;
2063
+ }
2064
+ setPlanAutoExpand(false);
2065
+ setOverlay("goal");
2066
+ }
2067
+ return;
2068
+ }
1850
2069
  // Handle /plans — open plan pane
1851
2070
  if (trimmed === "/plans") {
1852
2071
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
@@ -2045,6 +2264,9 @@ export function App(props) {
2045
2264
  agentLoop.clearQueue();
2046
2265
  agentLoop.abort();
2047
2266
  }
2267
+ else if (compactionAbortRef.current) {
2268
+ compactionAbortRef.current.abort();
2269
+ }
2048
2270
  else {
2049
2271
  handleDoubleExit();
2050
2272
  }
@@ -2175,6 +2397,7 @@ export function App(props) {
2175
2397
  };
2176
2398
  const promptOrder = [
2177
2399
  // Project audits / one-shot analysis
2400
+ "goal",
2178
2401
  "init",
2179
2402
  "research",
2180
2403
  "scan",
@@ -2220,11 +2443,29 @@ export function App(props) {
2220
2443
  case "tombstone":
2221
2444
  return null;
2222
2445
  case "banner":
2223
- return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount }, item.id));
2446
+ return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount, goalCount: goalCount }, item.id));
2224
2447
  case "user":
2225
2448
  return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
2226
2449
  case "task":
2227
2450
  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));
2451
+ case "goal":
2452
+ 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));
2453
+ case "goal_progress": {
2454
+ const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
2455
+ const color = item.phase === "terminal" && !isError
2456
+ ? theme.success
2457
+ : isError
2458
+ ? theme.warning
2459
+ : theme.primary;
2460
+ const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
2461
+ ? "✓ "
2462
+ : item.phase === "terminal"
2463
+ ? item.status === "passed"
2464
+ ? "◆ "
2465
+ : "! "
2466
+ : "↻ ";
2467
+ 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.id));
2468
+ }
2228
2469
  case "style_pack": {
2229
2470
  const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
2230
2471
  const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
@@ -2380,13 +2621,602 @@ export function App(props) {
2380
2621
  currentProvider,
2381
2622
  currentModel,
2382
2623
  ]);
2624
+ const openOverlay = useCallback((kind) => {
2625
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2626
+ props.sessionStore.overlay = kind;
2627
+ if (kind !== "plan")
2628
+ props.sessionStore.planAutoExpand = false;
2629
+ props.resetUI();
2630
+ }
2631
+ else {
2632
+ if (props.sessionStore) {
2633
+ props.sessionStore.overlay = kind;
2634
+ if (kind !== "plan")
2635
+ props.sessionStore.planAutoExpand = false;
2636
+ if (agentLoop.isRunning)
2637
+ props.sessionStore.pendingResetUI = true;
2638
+ }
2639
+ if (kind !== "plan")
2640
+ setPlanAutoExpand(false);
2641
+ setOverlay(kind);
2642
+ }
2643
+ }, [agentLoop.isRunning, props, stdout]);
2644
+ const closeOverlay = useCallback(() => {
2645
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2646
+ props.sessionStore.overlay = null;
2647
+ props.resetUI();
2648
+ }
2649
+ else {
2650
+ if (props.sessionStore) {
2651
+ props.sessionStore.overlay = null;
2652
+ if (agentLoop.isRunning)
2653
+ props.sessionStore.pendingResetUI = true;
2654
+ }
2655
+ setOverlay(null);
2656
+ }
2657
+ }, [agentLoop.isRunning, overlay, props, stdout]);
2658
+ const runGoalSyntheticEvent = useCallback((eventText) => {
2659
+ const eventInfo = parseGoalSyntheticEvent(eventText);
2660
+ const detail = eventInfo?.kind === "worker"
2661
+ ? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
2662
+ : `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
2663
+ if (agentRunningRef.current) {
2664
+ appendGoalProgress({
2665
+ kind: "goal_progress",
2666
+ phase: "orchestrator_reviewing",
2667
+ title: "Goal update queued for orchestrator",
2668
+ detail: `${detail} It will report back after the current turn.`,
2669
+ workerId: eventInfo?.worker,
2670
+ status: eventInfo?.status,
2671
+ });
2672
+ agentLoop.queueMessage(eventText);
2673
+ return;
2674
+ }
2675
+ appendGoalProgress({
2676
+ kind: "goal_progress",
2677
+ phase: "orchestrator_reviewing",
2678
+ title: "Orchestrator reviewing Goal update",
2679
+ detail,
2680
+ workerId: eventInfo?.worker,
2681
+ status: eventInfo?.status,
2682
+ });
2683
+ setLastUserMessage("");
2684
+ setDoneStatus(null);
2685
+ void agentLoop.run(eventText).catch((err) => {
2686
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2687
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2688
+ });
2689
+ }, [agentLoop, appendGoalProgress]);
2690
+ const continueGoalRun = useCallback((runId) => {
2691
+ void (async () => {
2692
+ const latestRun = await reconcileActiveGoalRuns(props.cwd, {
2693
+ isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
2694
+ }).then(({ runs }) => runs.find((item) => item.id === runId) ?? null);
2695
+ if (!latestRun) {
2696
+ runningGoalIdsRef.current.delete(runId);
2697
+ clearGoalStatusEntry(runId);
2698
+ return;
2699
+ }
2700
+ const decision = decideGoalNextAction(latestRun);
2701
+ if (decision.kind === "wait")
2702
+ return;
2703
+ if (decision.kind === "terminal" ||
2704
+ decision.kind === "blocked" ||
2705
+ decision.kind === "pause") {
2706
+ const status = decision.kind === "terminal"
2707
+ ? decision.status
2708
+ : decision.kind === "blocked"
2709
+ ? "blocked"
2710
+ : "paused";
2711
+ const nextRun = {
2712
+ ...latestRun,
2713
+ status,
2714
+ continueRequestedAt: undefined,
2715
+ blockers: decision.kind === "blocked" || decision.kind === "pause"
2716
+ ? Array.from(new Set([...latestRun.blockers, decision.reason]))
2717
+ : latestRun.blockers,
2718
+ };
2719
+ await upsertGoalRun(props.cwd, nextRun);
2720
+ await appendGoalDecision(props.cwd, latestRun.id, {
2721
+ kind: "continuation_stopped",
2722
+ reason: decision.reason,
2723
+ content: `terminal=${status}`,
2724
+ });
2725
+ const terminalProgress = formatGoalTerminalProgress(nextRun);
2726
+ if (terminalProgress)
2727
+ appendGoalProgress(terminalProgress);
2728
+ runningGoalIdsRef.current.delete(runId);
2729
+ clearGoalStatusEntry(runId);
2730
+ return;
2731
+ }
2732
+ let runForNextAction = latestRun;
2733
+ if (latestRun.continueRequestedAt &&
2734
+ !listGoalWorkers(props.cwd).some((worker) => worker.status === "running") &&
2735
+ activeVerifierRunIdsRef.current.size === 0) {
2736
+ await appendGoalDecision(props.cwd, latestRun.id, {
2737
+ kind: "continuation_consumed",
2738
+ reason: `Continuation request consumed by ${decision.kind}.`,
2739
+ });
2740
+ runForNextAction = await upsertGoalRun(props.cwd, {
2741
+ ...latestRun,
2742
+ continueRequestedAt: undefined,
2743
+ });
2744
+ }
2745
+ appendGoalProgress({
2746
+ kind: "goal_progress",
2747
+ phase: "continuing",
2748
+ title: `Continuing Goal: ${latestRun.title}`,
2749
+ detail: "Starting the next worker task or verifier step automatically.",
2750
+ status: latestRun.status,
2751
+ });
2752
+ upsertGoalStatusEntry({
2753
+ runId: latestRun.id,
2754
+ label: latestRun.title,
2755
+ phase: "orchestrating",
2756
+ startedAt: Date.now(),
2757
+ detail: "choosing next step",
2758
+ });
2759
+ startGoalRunRef.current(runForNextAction);
2760
+ })().catch((err) => {
2761
+ runningGoalIdsRef.current.delete(runId);
2762
+ clearGoalStatusEntry(runId);
2763
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2764
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2765
+ });
2766
+ }, [appendGoalProgress, clearGoalStatusEntry, props.cwd, upsertGoalStatusEntry]);
2767
+ const handleGoalWorkerComplete = useCallback((run, completion) => {
2768
+ const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
2769
+ completion.worker.goalTaskId;
2770
+ const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
2771
+ void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
2772
+ appendGoalProgress({
2773
+ kind: "goal_progress",
2774
+ phase: "worker_finished",
2775
+ title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
2776
+ detail: summarizeGoalCompletion(completion.summary),
2777
+ workerId: completion.worker.id,
2778
+ status: completion.status,
2779
+ });
2780
+ upsertGoalStatusEntry({
2781
+ runId: run.id,
2782
+ label: taskTitle,
2783
+ phase: completion.status === "done" ? "reviewing" : "failed",
2784
+ startedAt: Date.now(),
2785
+ detail: completion.status === "done" ? "reviewing result" : "task failed",
2786
+ workerId: completion.worker.id,
2787
+ goalNumber: goalNumberForRun(run.id),
2788
+ });
2789
+ runGoalSyntheticEvent(eventText);
2790
+ void (async () => {
2791
+ if (listGoalWorkers(completion.worker.cwd).some((worker) => worker.status === "running"))
2792
+ return;
2793
+ if (activeVerifierRunIdsRef.current.size > 0)
2794
+ return;
2795
+ const runs = await loadGoalRuns(completion.worker.cwd);
2796
+ const queued = runs.find((item) => item.continueRequestedAt && !goalHasBlockingPrerequisites(item));
2797
+ if (queued)
2798
+ setTimeout(() => continueGoalRun(queued.id), 750);
2799
+ })().catch((err) => log("ERROR", "goal", err instanceof Error ? err.message : String(err)));
2800
+ }, [
2801
+ appendGoalProgress,
2802
+ continueGoalRun,
2803
+ goalNumberForRun,
2804
+ runGoalSyntheticEvent,
2805
+ upsertGoalStatusEntry,
2806
+ ]);
2807
+ useEffect(() => {
2808
+ return subscribeGoalWorkerCompletions((completion) => {
2809
+ void (async () => {
2810
+ const latestRun = (await loadGoalRuns(completion.worker.cwd)).find((item) => item.id === completion.worker.goalRunId) ?? null;
2811
+ if (!latestRun) {
2812
+ log("WARN", "goal", `Worker completion for unknown Goal ${completion.worker.goalRunId}`);
2813
+ return;
2814
+ }
2815
+ runningGoalIdsRef.current.add(latestRun.id);
2816
+ handleGoalWorkerComplete(latestRun, completion);
2817
+ })().catch((err) => {
2818
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2819
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2820
+ });
2821
+ }, props.cwd);
2822
+ }, [handleGoalWorkerComplete, props.cwd]);
2823
+ const startGoalRun = useCallback((run) => {
2824
+ runningGoalIdsRef.current.add(run.id);
2825
+ void (async () => {
2826
+ if (goalHasBlockingPrerequisites(run)) {
2827
+ setOverlay(null);
2828
+ const detail = formatGoalBlockingPrerequisites(run);
2829
+ await upsertGoalRun(props.cwd, {
2830
+ ...run,
2831
+ status: "blocked",
2832
+ blockers: Array.from(new Set([...run.blockers, detail])),
2833
+ });
2834
+ setGoalCount((await summarizeGoalCounts(props.cwd)).active);
2835
+ appendGoalProgress({
2836
+ kind: "goal_progress",
2837
+ phase: "terminal",
2838
+ title: `Goal blocked: ${run.title}`,
2839
+ detail,
2840
+ status: "blocked",
2841
+ });
2842
+ runningGoalIdsRef.current.delete(run.id);
2843
+ clearGoalStatusEntry(run.id);
2844
+ return;
2845
+ }
2846
+ const decision = decideGoalNextAction(run);
2847
+ await appendGoalDecision(props.cwd, run.id, decision);
2848
+ if (decision.kind === "terminal") {
2849
+ const terminalProgress = formatGoalTerminalProgress(run);
2850
+ if (terminalProgress)
2851
+ appendGoalProgress(terminalProgress);
2852
+ runningGoalIdsRef.current.delete(run.id);
2853
+ clearGoalStatusEntry(run.id);
2854
+ return;
2855
+ }
2856
+ if (decision.kind === "wait") {
2857
+ appendGoalProgress({
2858
+ kind: "goal_progress",
2859
+ phase: "worker_started",
2860
+ title: decision.workerId ? `Goal working: ${run.title}` : `Goal active: ${run.title}`,
2861
+ detail: decision.reason,
2862
+ workerId: decision.workerId,
2863
+ });
2864
+ upsertGoalStatusEntry({
2865
+ runId: run.id,
2866
+ label: run.title,
2867
+ phase: decision.workerId ? "worker" : "orchestrating",
2868
+ startedAt: Date.now(),
2869
+ detail: decision.reason,
2870
+ workerId: decision.workerId,
2871
+ goalNumber: goalNumberForRun(run.id),
2872
+ });
2873
+ return;
2874
+ }
2875
+ if (decision.kind === "complete") {
2876
+ await upsertGoalRun(props.cwd, { ...run, status: "passed" });
2877
+ setGoalCount((await summarizeGoalCounts(props.cwd)).active);
2878
+ appendGoalProgress({
2879
+ kind: "goal_progress",
2880
+ phase: "terminal",
2881
+ title: `Goal passed: ${run.title}`,
2882
+ detail: decision.reason,
2883
+ status: "passed",
2884
+ });
2885
+ runningGoalIdsRef.current.delete(run.id);
2886
+ clearGoalStatusEntry(run.id);
2887
+ return;
2888
+ }
2889
+ if (decision.kind === "run_verifier") {
2890
+ await verifyGoalRun(run);
2891
+ return;
2892
+ }
2893
+ if (decision.kind === "create_task") {
2894
+ await updateGoalTask(props.cwd, run.id, `auto-${Date.now()}`, {
2895
+ title: decision.title,
2896
+ prompt: decision.prompt,
2897
+ status: "pending",
2898
+ });
2899
+ const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
2900
+ await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
2901
+ setTimeout(() => continueGoalRun(run.id), 250);
2902
+ return;
2903
+ }
2904
+ if (decision.kind === "blocked") {
2905
+ await upsertGoalRun(props.cwd, {
2906
+ ...run,
2907
+ status: "blocked",
2908
+ blockers: [...run.blockers, decision.reason],
2909
+ });
2910
+ setGoalCount((await summarizeGoalCounts(props.cwd)).active);
2911
+ appendGoalProgress({
2912
+ kind: "goal_progress",
2913
+ phase: "terminal",
2914
+ title: `Goal blocked: ${run.title}`,
2915
+ detail: decision.reason,
2916
+ status: "blocked",
2917
+ });
2918
+ runningGoalIdsRef.current.delete(run.id);
2919
+ clearGoalStatusEntry(run.id);
2920
+ return;
2921
+ }
2922
+ if (decision.kind === "pause") {
2923
+ await updateGoalTask(props.cwd, run.id, decision.task.id, {
2924
+ status: "blocked",
2925
+ attempts: decision.attempts,
2926
+ lastSummary: "Paused after worker attempt limit.",
2927
+ });
2928
+ await upsertGoalRun(props.cwd, {
2929
+ ...run,
2930
+ status: "paused",
2931
+ blockers: [...run.blockers, decision.reason],
2932
+ });
2933
+ await appendGoalEvidence(props.cwd, run.id, {
2934
+ kind: "summary",
2935
+ label: "Goal paused",
2936
+ content: decision.reason,
2937
+ });
2938
+ setGoalCount((await summarizeGoalCounts(props.cwd)).active);
2939
+ appendGoalProgress({
2940
+ kind: "goal_progress",
2941
+ phase: "terminal",
2942
+ title: `Goal paused: ${run.title}`,
2943
+ detail: decision.reason,
2944
+ status: "paused",
2945
+ });
2946
+ runningGoalIdsRef.current.delete(run.id);
2947
+ clearGoalStatusEntry(run.id);
2948
+ return;
2949
+ }
2950
+ await updateGoalTask(props.cwd, run.id, decision.task.id, { attempts: decision.attempts });
2951
+ const worker = await startGoalWorker({
2952
+ cwd: props.cwd,
2953
+ provider: currentProvider,
2954
+ model: currentModel,
2955
+ goalRunId: run.id,
2956
+ goalTaskId: decision.task.id,
2957
+ prompt: decision.task.prompt,
2958
+ });
2959
+ await upsertGoalRun(props.cwd, {
2960
+ ...run,
2961
+ status: "running",
2962
+ activeWorkerId: worker.id,
2963
+ continueRequestedAt: undefined,
2964
+ });
2965
+ setOverlay(null);
2966
+ setGoalCount((await summarizeGoalCounts(props.cwd)).active);
2967
+ appendGoalProgress({
2968
+ kind: "goal_progress",
2969
+ phase: "worker_started",
2970
+ title: `Worker started: ${decision.task.title}`,
2971
+ detail: "Task is running in the background.",
2972
+ workerId: worker.id,
2973
+ status: worker.status,
2974
+ });
2975
+ upsertGoalStatusEntry({
2976
+ runId: run.id,
2977
+ label: decision.task.title,
2978
+ phase: "worker",
2979
+ startedAt: Date.now(),
2980
+ detail: "background worker running",
2981
+ workerId: worker.id,
2982
+ goalNumber: goalNumberForRun(run.id),
2983
+ });
2984
+ })().catch((err) => {
2985
+ clearGoalStatusEntry(run.id);
2986
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2987
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2988
+ });
2989
+ }, [
2990
+ props.cwd,
2991
+ currentProvider,
2992
+ currentModel,
2993
+ appendGoalProgress,
2994
+ clearGoalStatusEntry,
2995
+ goalNumberForRun,
2996
+ upsertGoalStatusEntry,
2997
+ ]);
2998
+ const verifyGoalRun = useCallback(async (run) => {
2999
+ if (!run.verifier?.command) {
3000
+ await appendGoalEvidence(props.cwd, run.id, {
3001
+ kind: "summary",
3002
+ label: "Missing verifier",
3003
+ content: "No verifier command is configured.",
3004
+ });
3005
+ await upsertGoalRun(props.cwd, {
3006
+ ...run,
3007
+ status: "blocked",
3008
+ blockers: [...run.blockers, "No verifier command configured."],
3009
+ });
3010
+ appendGoalProgress({
3011
+ kind: "goal_progress",
3012
+ phase: "terminal",
3013
+ title: `Goal blocked: ${run.title}`,
3014
+ detail: "No verifier command is configured.",
3015
+ status: "blocked",
3016
+ });
3017
+ runningGoalIdsRef.current.delete(run.id);
3018
+ clearGoalStatusEntry(run.id);
3019
+ return;
3020
+ }
3021
+ activeVerifierRunIdsRef.current.add(run.id);
3022
+ await upsertGoalRun(props.cwd, {
3023
+ ...run,
3024
+ status: "verifying",
3025
+ continueRequestedAt: undefined,
3026
+ });
3027
+ appendGoalProgress({
3028
+ kind: "goal_progress",
3029
+ phase: "verifier_started",
3030
+ title: `Verifier started: ${run.title}`,
3031
+ detail: run.verifier.command,
3032
+ status: "verifying",
3033
+ });
3034
+ const startedAt = Date.now();
3035
+ const verifierTimeoutMs = Number(process.env.GG_GOAL_VERIFIER_TIMEOUT_MS ?? 10 * 60 * 1000);
3036
+ upsertGoalStatusEntry({
3037
+ runId: run.id,
3038
+ label: run.title,
3039
+ phase: "verifier",
3040
+ startedAt,
3041
+ detail: run.verifier.command,
3042
+ goalNumber: goalNumberForRun(run.id),
3043
+ });
3044
+ const { spawn } = await import("node:child_process");
3045
+ const { mkdir, writeFile } = await import("node:fs/promises");
3046
+ const { join } = await import("node:path");
3047
+ const logDir = join(projectDir(props.cwd), "verifiers");
3048
+ await mkdir(logDir, { recursive: true });
3049
+ const outputPath = join(logDir, `${run.id}-${startedAt}.log`);
3050
+ const child = spawn(run.verifier.command, {
3051
+ cwd: props.cwd,
3052
+ shell: true,
3053
+ stdio: ["ignore", "pipe", "pipe"],
3054
+ env: { ...process.env },
3055
+ });
3056
+ let output = "";
3057
+ child.stdout?.on("data", (chunk) => {
3058
+ output += chunk.toString("utf-8");
3059
+ if (output.length > 20_000)
3060
+ output = output.slice(output.length - 20_000);
3061
+ });
3062
+ child.stderr?.on("data", (chunk) => {
3063
+ output += chunk.toString("utf-8");
3064
+ if (output.length > 20_000)
3065
+ output = output.slice(output.length - 20_000);
3066
+ });
3067
+ let verifierSettled = false;
3068
+ let timedOut = false;
3069
+ const timeout = verifierTimeoutMs > 0
3070
+ ? setTimeout(() => {
3071
+ timedOut = true;
3072
+ if (child.pid)
3073
+ child.kill("SIGTERM");
3074
+ const killTimer = setTimeout(() => {
3075
+ if (!verifierSettled && child.pid)
3076
+ child.kill("SIGKILL");
3077
+ }, 5000);
3078
+ killTimer.unref?.();
3079
+ finishVerifier(124, `Verifier timed out after ${verifierTimeoutMs}ms and was terminated.\n${output}`);
3080
+ }, verifierTimeoutMs)
3081
+ : undefined;
3082
+ timeout?.unref?.();
3083
+ const finishVerifier = (code, forcedOutput) => {
3084
+ if (verifierSettled)
3085
+ return;
3086
+ verifierSettled = true;
3087
+ if (timeout)
3088
+ clearTimeout(timeout);
3089
+ activeVerifierRunIdsRef.current.delete(run.id);
3090
+ void (async () => {
3091
+ const status = code === 0 ? "pass" : "fail";
3092
+ const failureClass = timedOut
3093
+ ? "verifier_timeout"
3094
+ : forcedOutput?.startsWith("Verifier process error:")
3095
+ ? "verifier_spawn_error"
3096
+ : status === "fail"
3097
+ ? "verifier_failure"
3098
+ : "verifier_pass";
3099
+ const summary = (forcedOutput ?? output).trim() ||
3100
+ (code === 0 ? "Verifier passed." : "Verifier failed.");
3101
+ const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3102
+ await writeFile(outputPath, summary + "\n", "utf-8");
3103
+ const runWithVerifier = {
3104
+ ...latestRun,
3105
+ verifier: {
3106
+ ...latestRun.verifier,
3107
+ description: latestRun.verifier?.description ?? "Goal verifier",
3108
+ command: run.verifier?.command,
3109
+ lastResult: {
3110
+ status,
3111
+ summary,
3112
+ command: run.verifier?.command,
3113
+ exitCode: code ?? 1,
3114
+ outputPath,
3115
+ checkedAt: new Date().toISOString(),
3116
+ },
3117
+ },
3118
+ };
3119
+ const completionCheck = canCompleteGoalRun(runWithVerifier);
3120
+ const verifiedRun = await upsertGoalRun(props.cwd, {
3121
+ ...runWithVerifier,
3122
+ continueRequestedAt: undefined,
3123
+ status: status === "pass" && completionCheck.ok
3124
+ ? "passed"
3125
+ : status === "pass"
3126
+ ? "ready"
3127
+ : "failed",
3128
+ });
3129
+ await appendGoalEvidence(props.cwd, run.id, {
3130
+ kind: "command",
3131
+ label: `Verifier ${status}`,
3132
+ content: `${failureClass}: ${summary}`.slice(0, 4000),
3133
+ path: outputPath,
3134
+ });
3135
+ await appendGoalDecision(props.cwd, run.id, {
3136
+ kind: `verifier_${status}`,
3137
+ reason: `${failureClass}: verifier exited with code ${code ?? 1}.`,
3138
+ content: `outputPath=${outputPath}; durationMs=${Date.now() - startedAt}`,
3139
+ });
3140
+ if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
3141
+ await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
3142
+ title: "Fix verifier failure",
3143
+ prompt: `Goal verifier failed after ${Date.now() - startedAt}ms. Original goal: ${run.goal}\n\n` +
3144
+ `Verifier command: ${run.verifier?.command}\n\n` +
3145
+ `Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
3146
+ status: "pending",
3147
+ });
3148
+ const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
3149
+ await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
3150
+ }
3151
+ setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3152
+ appendGoalProgress({
3153
+ kind: "goal_progress",
3154
+ phase: "verifier_finished",
3155
+ title: `Verifier ${status}: ${run.title}`,
3156
+ detail: summarizeGoalCompletion(summary),
3157
+ status,
3158
+ });
3159
+ upsertGoalStatusEntry({
3160
+ runId: run.id,
3161
+ label: run.title,
3162
+ phase: status === "pass" ? "reviewing" : "failed",
3163
+ startedAt: Date.now(),
3164
+ detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
3165
+ goalNumber: goalNumberForRun(run.id),
3166
+ });
3167
+ const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status, run.verifier?.command ?? "", code ?? 1, summary);
3168
+ runGoalSyntheticEvent(eventText);
3169
+ const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
3170
+ if (continuationRun?.continueRequestedAt && status === "pass") {
3171
+ setTimeout(() => continueGoalRun(run.id), 500);
3172
+ }
3173
+ })().catch((err) => {
3174
+ clearGoalStatusEntry(run.id);
3175
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3176
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
3177
+ });
3178
+ };
3179
+ child.on("close", (code) => finishVerifier(code));
3180
+ child.on("error", (err) => finishVerifier(1, `Verifier process error: ${err.message}`));
3181
+ }, [
3182
+ props.cwd,
3183
+ appendGoalProgress,
3184
+ clearGoalStatusEntry,
3185
+ goalNumberForRun,
3186
+ runGoalSyntheticEvent,
3187
+ upsertGoalStatusEntry,
3188
+ ]);
3189
+ const pauseGoalRun = useCallback((run) => {
3190
+ void (async () => {
3191
+ runningGoalIdsRef.current.delete(run.id);
3192
+ if (run.activeWorkerId)
3193
+ await stopGoalWorker(run.activeWorkerId);
3194
+ await upsertGoalRun(props.cwd, { ...run, status: "paused", activeWorkerId: undefined });
3195
+ setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3196
+ appendGoalProgress({
3197
+ kind: "goal_progress",
3198
+ phase: "terminal",
3199
+ title: `Goal paused: ${run.title}`,
3200
+ detail: "Auto-continuation stopped until resumed.",
3201
+ status: "paused",
3202
+ });
3203
+ clearGoalStatusEntry(run.id);
3204
+ })().catch((err) => {
3205
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3206
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3207
+ });
3208
+ }, [appendGoalProgress, clearGoalStatusEntry, props.cwd]);
2383
3209
  // Keep refs in sync for access from stale closures (onDone)
2384
3210
  startTaskRef.current = startTask;
3211
+ startGoalRunRef.current = startGoalRun;
2385
3212
  useEffect(() => {
2386
3213
  runAllTasksRef.current = runAllTasks;
2387
3214
  if (props.sessionStore)
2388
3215
  props.sessionStore.runAllTasks = runAllTasks;
2389
3216
  }, [runAllTasks, props.sessionStore]);
3217
+ useEffect(() => {
3218
+ agentRunningRef.current = agentLoop.isRunning;
3219
+ }, [agentLoop.isRunning]);
2390
3220
  const startPixelFix = useCallback((errorId) => {
2391
3221
  void (async () => {
2392
3222
  try {
@@ -2487,12 +3317,20 @@ export function App(props) {
2487
3317
  props.sessionStore.runAllPixel = runAllPixel;
2488
3318
  }, [runAllPixel, props.sessionStore]);
2489
3319
  const isTaskView = overlay === "tasks";
3320
+ const isGoalView = overlay === "goal";
2490
3321
  const isSkillsView = overlay === "skills";
2491
3322
  const isPlanView = overlay === "plan";
2492
3323
  const isEyesView = overlay === "eyes";
3324
+ const footerStatusLayout = getFooterStatusLayoutDecision({
3325
+ columns,
3326
+ backgroundTaskCount: bgTasks.length,
3327
+ eyesCount,
3328
+ updatePending,
3329
+ });
2493
3330
  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: () => {
3331
+ const isOverlayView = isTaskView || isGoalView || isSkillsView || isPlanView || isEyesView || isPixelView;
3332
+ const shouldHideHistoryForOverlay = shouldHideHistoryForOverlayView(isOverlayView, agentLoop.isRunning);
3333
+ return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: shouldHideHistoryForOverlay ? [] : 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: () => {
2496
3334
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2497
3335
  props.sessionStore.overlay = null;
2498
3336
  props.resetUI();
@@ -2517,6 +3355,16 @@ export function App(props) {
2517
3355
  markTaskInProgress(props.cwd, next.id);
2518
3356
  startTask(next.title, next.prompt, next.id);
2519
3357
  }
3358
+ } })) : isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
3359
+ void summarizeGoalCounts(props.cwd).then((counts) => setGoalCount(counts.active));
3360
+ closeOverlay();
3361
+ }, onRunGoal: (run) => {
3362
+ setOverlay(null);
3363
+ startGoalRun(run);
3364
+ }, onVerifyGoal: (run) => {
3365
+ void verifyGoalRun(run);
3366
+ }, onPauseGoal: (run) => {
3367
+ pauseGoalRun(run);
2520
3368
  } })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
2521
3369
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2522
3370
  props.sessionStore.overlay = null;
@@ -2698,44 +3546,13 @@ export function App(props) {
2698
3546
  // live-area transition (chat input → TaskOverlay) natively, and
2699
3547
  // the chat history above stays in scrollback. When the overlay
2700
3548
  // 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
- }
3549
+ openOverlay("tasks");
3550
+ }, onToggleGoal: () => {
3551
+ openOverlay("goal");
2713
3552
  }, 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
- }
3553
+ openOverlay("skills");
2726
3554
  }, 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
- }
3555
+ openOverlay("pixel");
2739
3556
  }, onTogglePlanMode: () => {
2740
3557
  const next = !planMode;
2741
3558
  setPlanMode(next);
@@ -2749,7 +3566,12 @@ export function App(props) {
2749
3566
  id: getId(),
2750
3567
  },
2751
3568
  ]);
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" }) }))] }))] }))] }));
3569
+ }, 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 ||
3570
+ footerStatusLayout.hasEyesSignals ||
3571
+ 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 ||
3572
+ (!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
3573
+ ? 1
3574
+ : 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
2753
3575
  }
2754
3576
  function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
2755
3577
  const status = enabled ? "on" : "off";