@kenkaiiii/ggcoder 4.3.217 → 4.3.218

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 (126) hide show
  1. package/dist/cli/command-routing.d.ts +28 -0
  2. package/dist/cli/command-routing.d.ts.map +1 -0
  3. package/dist/cli/command-routing.js +51 -0
  4. package/dist/cli/command-routing.js.map +1 -0
  5. package/dist/cli/command-routing.test.d.ts +2 -0
  6. package/dist/cli/command-routing.test.d.ts.map +1 -0
  7. package/dist/cli/command-routing.test.js +59 -0
  8. package/dist/cli/command-routing.test.js.map +1 -0
  9. package/dist/cli.d.ts +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +46 -103
  12. package/dist/cli.js.map +1 -1
  13. package/dist/core/agent-session-compaction.test.d.ts +2 -0
  14. package/dist/core/agent-session-compaction.test.d.ts.map +1 -0
  15. package/dist/core/agent-session-compaction.test.js +121 -0
  16. package/dist/core/agent-session-compaction.test.js.map +1 -0
  17. package/dist/core/agent-session.d.ts.map +1 -1
  18. package/dist/core/agent-session.js +6 -7
  19. package/dist/core/agent-session.js.map +1 -1
  20. package/dist/core/goal-store.d.ts +9 -0
  21. package/dist/core/goal-store.d.ts.map +1 -1
  22. package/dist/core/goal-store.js +24 -0
  23. package/dist/core/goal-store.js.map +1 -1
  24. package/dist/core/goal-worker.d.ts +7 -0
  25. package/dist/core/goal-worker.d.ts.map +1 -1
  26. package/dist/core/goal-worker.js +62 -20
  27. package/dist/core/goal-worker.js.map +1 -1
  28. package/dist/core/goal-worker.test.js +92 -2
  29. package/dist/core/goal-worker.test.js.map +1 -1
  30. package/dist/core/goal-worktree.d.ts +33 -0
  31. package/dist/core/goal-worktree.d.ts.map +1 -0
  32. package/dist/core/goal-worktree.js +67 -0
  33. package/dist/core/goal-worktree.js.map +1 -0
  34. package/dist/core/goal-worktree.test.d.ts +2 -0
  35. package/dist/core/goal-worktree.test.d.ts.map +1 -0
  36. package/dist/core/goal-worktree.test.js +101 -0
  37. package/dist/core/goal-worktree.test.js.map +1 -0
  38. package/dist/core/repomap-budget.d.ts +7 -0
  39. package/dist/core/repomap-budget.d.ts.map +1 -0
  40. package/dist/core/repomap-budget.js +10 -0
  41. package/dist/core/repomap-budget.js.map +1 -0
  42. package/dist/core/repomap-budget.test.d.ts +2 -0
  43. package/dist/core/repomap-budget.test.d.ts.map +1 -0
  44. package/dist/core/repomap-budget.test.js +26 -0
  45. package/dist/core/repomap-budget.test.js.map +1 -0
  46. package/dist/system-prompt.js +2 -2
  47. package/dist/system-prompt.js.map +1 -1
  48. package/dist/system-prompt.test.js +2 -2
  49. package/dist/system-prompt.test.js.map +1 -1
  50. package/dist/tools/goals.d.ts.map +1 -1
  51. package/dist/tools/goals.js +30 -26
  52. package/dist/tools/goals.js.map +1 -1
  53. package/dist/tools/goals.test.js +47 -1
  54. package/dist/tools/goals.test.js.map +1 -1
  55. package/dist/ui/App.d.ts +11 -381
  56. package/dist/ui/App.d.ts.map +1 -1
  57. package/dist/ui/App.js +61 -554
  58. package/dist/ui/App.js.map +1 -1
  59. package/dist/ui/app-items.d.ts +207 -0
  60. package/dist/ui/app-items.d.ts.map +1 -0
  61. package/dist/ui/app-items.js +2 -0
  62. package/dist/ui/app-items.js.map +1 -0
  63. package/dist/ui/app-state-persistence.test.js +4 -1
  64. package/dist/ui/app-state-persistence.test.js.map +1 -1
  65. package/dist/ui/chat-layout-pinning.test.js +2 -1
  66. package/dist/ui/chat-layout-pinning.test.js.map +1 -1
  67. package/dist/ui/components/ToolUseLoader.d.ts +3 -1
  68. package/dist/ui/components/ToolUseLoader.d.ts.map +1 -1
  69. package/dist/ui/components/ToolUseLoader.js +2 -2
  70. package/dist/ui/components/ToolUseLoader.js.map +1 -1
  71. package/dist/ui/goal-events.test.js +2 -1
  72. package/dist/ui/goal-events.test.js.map +1 -1
  73. package/dist/ui/goal-lifecycle-orchestration.test.js +20 -1
  74. package/dist/ui/goal-lifecycle-orchestration.test.js.map +1 -1
  75. package/dist/ui/goal-progress.d.ts +31 -0
  76. package/dist/ui/goal-progress.d.ts.map +1 -0
  77. package/dist/ui/goal-progress.js +152 -0
  78. package/dist/ui/goal-progress.js.map +1 -0
  79. package/dist/ui/item-helpers.d.ts +24 -0
  80. package/dist/ui/item-helpers.d.ts.map +1 -0
  81. package/dist/ui/item-helpers.js +96 -0
  82. package/dist/ui/item-helpers.js.map +1 -0
  83. package/dist/ui/layout-decisions.d.ts +113 -0
  84. package/dist/ui/layout-decisions.d.ts.map +1 -0
  85. package/dist/ui/layout-decisions.js +173 -0
  86. package/dist/ui/layout-decisions.js.map +1 -0
  87. package/dist/ui/prompt-routing.d.ts +29 -0
  88. package/dist/ui/prompt-routing.d.ts.map +1 -0
  89. package/dist/ui/prompt-routing.js +105 -0
  90. package/dist/ui/prompt-routing.js.map +1 -0
  91. package/dist/ui/queued-message.test.js +1 -1
  92. package/dist/ui/queued-message.test.js.map +1 -1
  93. package/dist/ui/render.d.ts +1 -0
  94. package/dist/ui/render.d.ts.map +1 -1
  95. package/dist/ui/render.js +1 -0
  96. package/dist/ui/render.js.map +1 -1
  97. package/dist/ui/scroll-stabilization.test.js +1 -1
  98. package/dist/ui/scroll-stabilization.test.js.map +1 -1
  99. package/dist/ui/slash-command-images.test.js +1 -1
  100. package/dist/ui/slash-command-images.test.js.map +1 -1
  101. package/dist/ui/terminal-history-format.d.ts +41 -0
  102. package/dist/ui/terminal-history-format.d.ts.map +1 -0
  103. package/dist/ui/terminal-history-format.js +131 -0
  104. package/dist/ui/terminal-history-format.js.map +1 -0
  105. package/dist/ui/terminal-history-spacing.d.ts +3 -0
  106. package/dist/ui/terminal-history-spacing.d.ts.map +1 -0
  107. package/dist/ui/terminal-history-spacing.js +28 -0
  108. package/dist/ui/terminal-history-spacing.js.map +1 -0
  109. package/dist/ui/terminal-history-status-renderers.d.ts +16 -0
  110. package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -0
  111. package/dist/ui/terminal-history-status-renderers.js +83 -0
  112. package/dist/ui/terminal-history-status-renderers.js.map +1 -0
  113. package/dist/ui/terminal-history.d.ts.map +1 -1
  114. package/dist/ui/terminal-history.js +17 -233
  115. package/dist/ui/terminal-history.js.map +1 -1
  116. package/dist/ui/terminal-history.test.js +2 -1
  117. package/dist/ui/terminal-history.test.js.map +1 -1
  118. package/dist/ui/thinking-level-cycle.test.js +7 -1
  119. package/dist/ui/thinking-level-cycle.test.js.map +1 -1
  120. package/dist/ui/thinking-level.d.ts +5 -0
  121. package/dist/ui/thinking-level.d.ts.map +1 -0
  122. package/dist/ui/thinking-level.js +30 -0
  123. package/dist/ui/thinking-level.js.map +1 -0
  124. package/dist/ui/tui-history-parity.test.js +4 -3
  125. package/dist/ui/tui-history-parity.test.js.map +1 -1
  126. package/package.json +3 -3
package/dist/ui/App.js CHANGED
@@ -4,7 +4,6 @@ import { Box, Text, useStdout } from "ink";
4
4
  import { useTerminalSize } from "./hooks/useTerminalSize.js";
5
5
  import { useDoublePress } from "./hooks/useDoublePress.js";
6
6
  import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
7
- import { writeFileSync } from "node:fs";
8
7
  import { playNotificationSound } from "../utils/sound.js";
9
8
  import { formatError, } from "@kenkaiiii/gg-ai";
10
9
  import { extractImagePaths } from "../utils/image.js";
@@ -30,14 +29,14 @@ import { PlanOverlay } from "./components/PlanOverlay.js";
30
29
  import { ModelSelector } from "./components/ModelSelector.js";
31
30
  import { GoalOverlay } from "./components/GoalOverlay.js";
32
31
  import { PixelOverlay } from "./components/PixelOverlay.js";
33
- import { buildGoalFinalSummarySections, buildGoalSummaryRows, goalPassedDetail, } from "./goal-summary.js";
34
32
  import { SkillsOverlay } from "./components/SkillsOverlay.js";
35
33
  import { ThemeSelector } from "./components/ThemeSelector.js";
36
34
  import { BackgroundTasksBar, getFooterStatusLayoutDecision, } from "./components/BackgroundTasksBar.js";
37
35
  import { useTheme, useSetTheme } from "./theme/theme.js";
38
36
  import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
39
37
  import { getGitBranch } from "../utils/git.js";
40
- import { getModel, getContextWindow, getMaxThinkingLevel } from "../core/model-registry.js";
38
+ import { getModel, getContextWindow } from "../core/model-registry.js";
39
+ import { BLACK_CIRCLE } from "./constants/figures.js";
41
40
  import { SessionManager } from "../core/session-manager.js";
42
41
  import { appendMessagesToSession as appendSessionMessages, createCompactedSessionCheckpoint, } from "../core/session-compaction.js";
43
42
  import { log } from "../core/logger.js";
@@ -52,18 +51,29 @@ import { loadCustomCommands } from "../core/custom-commands.js";
52
51
  import { buildSystemPrompt } from "../system-prompt.js";
53
52
  import { detectLanguages, LANGUAGE_DISPLAY_NAMES, } from "../core/language-detector.js";
54
53
  import { detectVerifyCommands } from "../core/verify-commands.js";
55
- import { FOCUSED_REPO_MAP_MAX_CHARS, FIRST_TURN_REPO_MAP_MAX_CHARS, buildRepoMap, createRepoMapCache, } from "../core/repomap.js";
54
+ import { buildRepoMap, createRepoMapCache, } from "../core/repomap.js";
55
+ import { getRepoMapBudgetForContext } from "../core/repomap-budget.js";
56
56
  import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMessages, } from "../core/repomap-context.js";
57
57
  import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
58
58
  import { getMCPServers } from "../core/mcp/index.js";
59
59
  import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
60
60
  import { splitAssistantStreamingText } from "./utils/assistant-stream-split.js";
61
61
  import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
62
- import { canCompleteGoalRun, decideGoalNextAction, } from "../core/goal-controller.js";
62
+ import { canCompleteGoalRun, decideGoalNextAction } from "../core/goal-controller.js";
63
63
  import { runGoalPrerequisiteChecks } from "../core/goal-prerequisites.js";
64
64
  import { runGoalVerifierCommand } from "../core/goal-verifier.js";
65
65
  import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
66
66
  import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
67
+ import { buildUserContentWithAttachments, isGoalPromptCommandName, routePromptCommandInput, runGoalPromptSetupSequence, } from "./prompt-routing.js";
68
+ import { getNextThinkingLevel, isThinkingLevelSupported } from "./thinking-level.js";
69
+ import { appendGoalProgressDraft, completedItemsWithDurableGoalTerminalProgress, formatGoalTerminalProgress, formatGoalWorkerFinishedTitle, getGoalContinuationChoiceKey, goalTerminalProgressId, routeGoalSyntheticEvent, summarizeGoalCompletion, truncateGoalProgressText, } from "./goal-progress.js";
70
+ import { getChatControlsLayoutDecision, getDoneFlushDecision, getGoalActivationPaneTransition, getGoalSetupFinishedPaneTransition, getGoalSetupPaneTransitionAfterRun, isAgentSpacingItem, MIN_LIVE_AREA_ROWS, nextGoalModeAfterAgentDone, shouldResetUIForGoalSetupPaneTransition, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceAssistantAfterToolBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
71
+ import { compactHistory, getNextGeneratedItemId, isActiveItem, isSameAssistantText, normalizeAssistantText, partitionCompleted, pinStreamingTextBeforeToolBoundary, } from "./item-helpers.js";
72
+ export { buildGoalSetupPromptFromPlanner, buildUserContentWithAttachments, collectAssistantTextSince, isGoalPromptCommandName, routePromptCommandInput, runGoalPromptSetupSequence, } from "./prompt-routing.js";
73
+ export { getNextThinkingLevel } from "./thinking-level.js";
74
+ export { appendGoalProgressDraft, completedItemsWithDurableGoalTerminalProgress, formatGoalTerminalProgress, getGoalContinuationChoiceKey, routeGoalSyntheticEvent, truncateGoalProgressText, } from "./goal-progress.js";
75
+ export { getChatControlsLayoutDecision, getDoneFlushDecision, getGoalActivationPaneTransition, getGoalSetupFinishedPaneTransition, getGoalSetupPaneTransitionAfterRun, getScrollStabilizationDecision, getStaticHistoryKey, hasParagraphBreakLiveUserMessage, isTallLiveUserMessage, nextGoalModeAfterAgentDone, shouldHideHistoryForOverlayView, shouldHideStaticItemsForOverlayView, shouldResetUIForGoalSetupPaneTransition, shouldStabilizeOverlayPaneRerender, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceAssistantAfterToolBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
76
+ export { getNextGeneratedItemId, isActiveItem, partitionCompleted, pinStreamingTextBeforeToolBoundary, } from "./item-helpers.js";
67
77
  /** Where ggcoder bugs should be reported. Surfaced in the guidance line. */
68
78
  const GGCODER_BUG_REPORT_URL = "github.com/kenkaiiii/gg-framework/issues";
69
79
  /**
@@ -97,114 +107,6 @@ function toErrorItem(err, id, contextPrefix) {
97
107
  id,
98
108
  };
99
109
  }
100
- export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS, customCommands = []) {
101
- const trimmed = input.trim();
102
- if (!trimmed.startsWith("/"))
103
- return null;
104
- const parts = trimmed.slice(1).split(" ");
105
- const cmdName = parts[0];
106
- const cmdArgs = parts.slice(1).join(" ").trim();
107
- const builtinCmd = promptCommands.find((c) => c.name === cmdName || c.aliases.includes(cmdName));
108
- const customCmd = !builtinCmd ? customCommands.find((c) => c.name === cmdName) : undefined;
109
- const promptText = builtinCmd?.prompt ?? customCmd?.prompt;
110
- if (!promptText)
111
- return null;
112
- return {
113
- cmdName,
114
- cmdArgs,
115
- promptText,
116
- fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
117
- };
118
- }
119
- const GOAL_PLANNER_OUTPUT_MAX_CHARS = 2400;
120
- function messageTextContent(message) {
121
- if (typeof message.content === "string")
122
- return message.content;
123
- return message.content
124
- .filter((part) => part.type === "text")
125
- .map((part) => part.text)
126
- .join("\n");
127
- }
128
- export function collectAssistantTextSince(messages, startIndex, maxChars = GOAL_PLANNER_OUTPUT_MAX_CHARS) {
129
- const text = messages
130
- .slice(startIndex)
131
- .filter((message) => message.role === "assistant")
132
- .map(messageTextContent)
133
- .join("\n")
134
- .trim();
135
- if (text.length <= maxChars)
136
- return text;
137
- return text.slice(0, maxChars).trimEnd() + "\n[planner output truncated]";
138
- }
139
- export function buildGoalSetupPromptFromPlanner({ originalGoalPrompt, plannerOutput, }) {
140
- const compactPlannerOutput = plannerOutput.trim() || "GOAL_PLAN\nresearch=none\nEND_GOAL_PLAN";
141
- return (`${originalGoalPrompt.trim()}\n\n` +
142
- `## Goal Planner Output\n\n${compactPlannerOutput}\n\n` +
143
- `Use the original objective plus this planner output to create durable Goal setup only. ` +
144
- `Do not redo planner research unless the planner output is unusable.`);
145
- }
146
- export function isGoalPromptCommandName(cmdName) {
147
- return getPromptCommand(cmdName)?.name === "goal";
148
- }
149
- export async function runGoalPromptSetupSequence({ userContent, fullPrompt, messagesRef, setGoalModeAndPrompt, runAgent, onStage, }) {
150
- onStage?.("Planning Goal setup");
151
- await setGoalModeAndPrompt("planner");
152
- const plannerStartIndex = messagesRef.current.length;
153
- await runAgent(userContent);
154
- const plannerOutput = collectAssistantTextSince(messagesRef.current, plannerStartIndex);
155
- const setupPrompt = buildGoalSetupPromptFromPlanner({
156
- originalGoalPrompt: fullPrompt,
157
- plannerOutput,
158
- });
159
- await setGoalModeAndPrompt("setup");
160
- onStage?.("Creating Goal run");
161
- await runAgent(setupPrompt);
162
- }
163
- function buildGoalTaskPromptWithReferences(run, taskPrompt) {
164
- if (taskPrompt.includes("## Goal References (MANDATORY)"))
165
- return taskPrompt;
166
- const references = formatGoalReferencesForPrompt(run.references ?? []);
167
- return references ? `${references}\n\n${taskPrompt}` : taskPrompt;
168
- }
169
- export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
170
- if (inputImages.length === 0)
171
- return text;
172
- const parts = [];
173
- if (text) {
174
- parts.push({ type: "text", text });
175
- }
176
- for (const img of inputImages) {
177
- if (img.kind === "text") {
178
- parts.push({
179
- type: "text",
180
- text: `<file name="${img.fileName}">\n${img.data}\n</file>`,
181
- });
182
- }
183
- else if (modelSupportsImages) {
184
- parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
185
- }
186
- else {
187
- // GLM models: save image to temp file and instruct model to use vision MCP tool
188
- const ext = img.mediaType.split("/")[1] ?? "png";
189
- const tmpPath = `/tmp/ggcoder-img-${Date.now()}.${ext}`;
190
- try {
191
- writeFileSync(tmpPath, Buffer.from(img.data, "base64"));
192
- parts.push({
193
- type: "text",
194
- text: `[User attached an image saved at: ${tmpPath} — use the image_analysis tool to view and analyze it]`,
195
- });
196
- }
197
- catch {
198
- parts.push({
199
- type: "text",
200
- text: `[User attached an image but it could not be saved for analysis]`,
201
- });
202
- }
203
- }
204
- }
205
- // If only text parts remain after stripping images, simplify to plain string
206
- return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
207
- }
208
110
  /** Tools that get aggregated into a single compact group when possible. */
209
111
  const AGGREGATABLE_TOOLS = new Set([
210
112
  "read",
@@ -215,66 +117,17 @@ const AGGREGATABLE_TOOLS = new Set([
215
117
  "mcp__kencode-search__referenceSources",
216
118
  "mcp__kencode-search__discoverRepos",
217
119
  ]);
218
- const OPENAI_GPT_THINKING_LEVELS = ["medium", "high", "xhigh"];
219
- function isOpenAIGptModel(provider, model) {
220
- return provider === "openai" && model.startsWith("gpt-");
221
- }
222
- export function getNextThinkingLevel(provider, model, current) {
223
- if (!isOpenAIGptModel(provider, model)) {
224
- return current ? undefined : getMaxThinkingLevel(model);
225
- }
226
- if (!current)
227
- return "medium";
228
- const index = OPENAI_GPT_THINKING_LEVELS.indexOf(current);
229
- if (index === -1)
230
- return "medium";
231
- return OPENAI_GPT_THINKING_LEVELS[index + 1];
232
- }
233
120
  const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
234
- /**
235
- * Cap memory by replacing old finalized rows with tiny tombstones. The full
236
- * transcript is already printed into terminal scrollback, so the in-memory copy
237
- * only needs enough recent structure to survive remounts and session mirroring.
238
- */
239
- const MAX_LIVE_HISTORY = 200;
240
- function compactHistory(items) {
241
- if (items.length <= MAX_LIVE_HISTORY)
242
- return items;
243
- const cutoff = items.length - MAX_LIVE_HISTORY;
244
- const compacted = new Array(items.length);
245
- for (let i = 0; i < cutoff; i++) {
246
- const it = items[i];
247
- compacted[i] = it.kind === "tombstone" ? it : { kind: "tombstone", id: it.id };
248
- }
249
- for (let i = cutoff; i < items.length; i++) {
250
- compacted[i] = items[i];
251
- }
252
- return compacted;
253
- }
254
- function summarizeGoalCompletion(summary) {
255
- const lines = summary
256
- .split("\n")
257
- .map((line) => line.trim())
258
- .filter((line) => line.length > 0 && line !== "[agent_done]");
259
- const statusLine = lines.find((line) => /^Status:/i.test(line));
260
- const changedLine = lines.find((line) => /^(Changed|Implemented|Fixed|Added|Key findings|Full verifier)/i.test(line));
261
- const verificationLine = lines.find((line) => /^(Verification|Verified|Result):/i.test(line));
262
- return statusLine ?? changedLine ?? verificationLine ?? lines[0];
263
- }
264
- const GOAL_PROGRESS_TEXT_LIMIT = 72;
265
- export function truncateGoalProgressText(text) {
266
- const normalized = text.replace(/\s+/g, " ").trim();
267
- if (normalized.length <= GOAL_PROGRESS_TEXT_LIMIT)
268
- return normalized;
269
- return `${normalized.slice(0, GOAL_PROGRESS_TEXT_LIMIT - 1).trimEnd()}…`;
270
- }
271
- function formatGoalWorkerFinishedTitle(taskTitle, status) {
272
- const prefix = status === "done" ? "Done" : "Failed";
273
- return truncateGoalProgressText(`${prefix}: ${taskTitle}`);
121
+ function buildGoalTaskPromptWithReferences(run, taskPrompt) {
122
+ if (taskPrompt.includes("## Goal References (MANDATORY)"))
123
+ return taskPrompt;
124
+ const references = formatGoalReferencesForPrompt(run.references ?? []);
125
+ return references ? `${references}\n\n${taskPrompt}` : taskPrompt;
274
126
  }
275
127
  function goalProgressLoaderStatus(item) {
276
- if (item.status === "failed" || item.status === "fail" || item.status === "blocked")
128
+ if (item.status === "failed" || item.status === "fail" || item.status === "blocked") {
277
129
  return "error";
130
+ }
278
131
  if (item.phase === "worker_finished" ||
279
132
  item.phase === "verifier_finished" ||
280
133
  item.phase === "terminal") {
@@ -297,373 +150,6 @@ function goalProgressColor(item, theme) {
297
150
  return theme.warning;
298
151
  return theme.primary;
299
152
  }
300
- function goalTerminalProgressId(run) {
301
- return `goal-terminal-${run.id}`;
302
- }
303
- function goalTerminalRunIdFromItem(item) {
304
- if (item.kind !== "goal_progress" || item.phase !== "terminal")
305
- return undefined;
306
- if (!item.id.startsWith("goal-terminal-"))
307
- return undefined;
308
- return item.id.slice("goal-terminal-".length);
309
- }
310
- function goalProgressMatchesDraft(item, draft) {
311
- return (item.phase === draft.phase &&
312
- item.title === draft.title &&
313
- item.detail === draft.detail &&
314
- item.workerId === draft.workerId &&
315
- item.status === draft.status &&
316
- JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []) &&
317
- JSON.stringify(item.summarySections ?? []) === JSON.stringify(draft.summarySections ?? []));
318
- }
319
- export function appendGoalProgressDraft(items, draft, makeId) {
320
- const previous = items.at(-1);
321
- if (previous?.kind === "goal_progress" && goalProgressMatchesDraft(previous, draft)) {
322
- return items;
323
- }
324
- return [...items, { ...draft, id: makeId() }];
325
- }
326
- /**
327
- * Reconcile terminal Goal cards that are already visible in this UI session.
328
- *
329
- * This intentionally does not synthesize missing cards from durable GoalRun
330
- * state. Goal terminal cards are event notifications: they should appear when
331
- * the terminal event happens in the current UI, not whenever a fresh session
332
- * polls old Goal runs from the Goal pane. Callers that just observed a terminal
333
- * event append that card first, then use this helper to tombstone stale older
334
- * cards for the same run.
335
- */
336
- export function getNextGeneratedItemId(items) {
337
- let max = -1;
338
- for (const item of items) {
339
- const raw = item.id.startsWith("ui-") ? item.id.slice(3) : item.id;
340
- const n = Number(raw);
341
- if (Number.isInteger(n) && n >= 0 && n > max)
342
- max = n;
343
- }
344
- return max + 1;
345
- }
346
- export function completedItemsWithDurableGoalTerminalProgress(items, runs) {
347
- const runIds = new Set(runs.map((run) => run.id));
348
- const terminalByRun = new Map(runs
349
- .map((run) => [run.id, formatGoalTerminalProgress(run)])
350
- .filter((entry) => entry[1] !== null));
351
- if (runIds.size === 0)
352
- return items;
353
- let changed = false;
354
- const reconciled = items.map((item, index) => {
355
- const runId = goalTerminalRunIdFromItem(item);
356
- if (!runId || !runIds.has(runId))
357
- return item;
358
- const draft = terminalByRun.get(runId);
359
- if (draft && goalProgressMatchesDraft(item, draft))
360
- return item;
361
- changed = true;
362
- return { kind: "tombstone", id: `tombstone-${item.id}-${index}` };
363
- });
364
- return changed ? reconciled : items;
365
- }
366
- export function formatGoalTerminalProgress(run) {
367
- switch (run.status) {
368
- case "passed":
369
- return {
370
- kind: "goal_progress",
371
- phase: "terminal",
372
- title: `Goal passed: ${run.title}`,
373
- detail: goalPassedDetail(run),
374
- summaryRows: buildGoalSummaryRows(run),
375
- summarySections: buildGoalFinalSummarySections(run),
376
- status: run.status,
377
- };
378
- case "failed":
379
- return {
380
- kind: "goal_progress",
381
- phase: "terminal",
382
- title: `Goal failed: ${run.title}`,
383
- detail: "Auto-continuation stopped. Check Goal tasks for the failing step.",
384
- summaryRows: buildGoalSummaryRows(run),
385
- status: run.status,
386
- };
387
- case "blocked":
388
- return {
389
- kind: "goal_progress",
390
- phase: "terminal",
391
- title: `Goal blocked: ${run.title}`,
392
- detail: goalHasBlockingPrerequisites(run)
393
- ? formatGoalBlockingPrerequisites(run)
394
- : (run.blockers[0] ?? "A prerequisite or missing verifier blocked progress."),
395
- summaryRows: buildGoalSummaryRows(run),
396
- status: run.status,
397
- };
398
- case "paused":
399
- return {
400
- kind: "goal_progress",
401
- phase: "terminal",
402
- title: `Goal paused: ${run.title}`,
403
- detail: run.blockers[0] ?? "Auto-continuation paused.",
404
- summaryRows: buildGoalSummaryRows(run),
405
- status: run.status,
406
- };
407
- case "draft":
408
- case "ready":
409
- case "running":
410
- case "verifying":
411
- return null;
412
- }
413
- }
414
- export function shouldHideHistoryForOverlayView(isOverlayView, _isAgentRunning) {
415
- // Overlay panes are standalone full-screen states. Finalized chat rows are
416
- // printed outside Ink, so overlays should never replay transcript UI behind them.
417
- return isOverlayView;
418
- }
419
- export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
420
- return isAgentRunning && overlayPane === "goal";
421
- }
422
- export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender: _stabilizeOverlayPaneRerender, }) {
423
- return shouldHideHistoryForOverlay;
424
- }
425
- export function getDoneFlushDecision({ planOverlayPending, goalMode, goalAutoExpand, }) {
426
- return {
427
- showDoneStatus: !(planOverlayPending ||
428
- goalMode === "planner" ||
429
- goalMode === "setup" ||
430
- goalAutoExpand),
431
- flushLiveItems: true,
432
- };
433
- }
434
- export function getGoalSetupFinishedPaneTransition() {
435
- return {
436
- overlay: "goal",
437
- goalAutoExpand: true,
438
- planAutoExpand: false,
439
- suppressDoneStatus: true,
440
- };
441
- }
442
- export function getGoalSetupPaneTransitionAfterRun({ isGoalSetupCommand, setupPanePending, }) {
443
- return isGoalSetupCommand && setupPanePending ? getGoalSetupFinishedPaneTransition() : null;
444
- }
445
- export function shouldResetUIForSetupPaneTransition({ hasResetUI, hasSessionStore, }) {
446
- // Opening a review pane is a full-screen state transition. A bare React state
447
- // flip hides history in the virtual tree, but it does not reset Ink/log-update's
448
- // already-written terminal frame, so the pane can render below prior chat.
449
- return hasResetUI && hasSessionStore;
450
- }
451
- export const shouldResetUIForGoalSetupPaneTransition = shouldResetUIForSetupPaneTransition;
452
- export function getGoalActivationPaneTransition() {
453
- return { overlay: null, goalAutoExpand: false, planAutoExpand: false, resetReviewScreen: true };
454
- }
455
- export function getGoalContinuationChoiceKey({ runId, decision, }) {
456
- switch (decision.kind) {
457
- case "create_task":
458
- return `${runId}:create_task:${decision.title}:${decision.prompt}`;
459
- case "start_worker":
460
- case "pause":
461
- return `${runId}:${decision.kind}:${decision.task.id}:${decision.attempts}`;
462
- case "run_verifier":
463
- return `${runId}:run_verifier:${decision.command}`;
464
- case "blocked":
465
- case "complete":
466
- case "terminal":
467
- case "wait":
468
- return `${runId}:${decision.kind}:${decision.reason}`;
469
- }
470
- }
471
- export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, hasParagraphBreakLiveUserMessage = false, }) {
472
- const shouldPreserveStatic = isUserScrolled || hasTallLiveUserMessage || hasParagraphBreakLiveUserMessage;
473
- const shouldAutoFollow = !(isUserScrolled || hasTallLiveUserMessage);
474
- return {
475
- preserveStatic: shouldPreserveStatic && hasNewOutput,
476
- autoFollow: shouldAutoFollow,
477
- };
478
- }
479
- export function nextGoalModeAfterAgentDone({ currentMode, runningGoalIds, queuedSyntheticEvents, activeContinuationFlights = 0, wasGoalSetupTurn, }) {
480
- if (wasGoalSetupTurn)
481
- return "off";
482
- if (currentMode === "planner" || currentMode === "setup")
483
- return currentMode;
484
- if (queuedSyntheticEvents > 0)
485
- return "coordinator";
486
- if (activeContinuationFlights > 0)
487
- return "coordinator";
488
- if (currentMode === "coordinator" && runningGoalIds > 0)
489
- return "coordinator";
490
- return "off";
491
- }
492
- export function hasParagraphBreakLiveUserMessage(text) {
493
- return /\n[ \t]*\n/.test(text);
494
- }
495
- export function isTallLiveUserMessage(text, rows) {
496
- return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
497
- }
498
- export function getStaticHistoryKey({ resizeKey }) {
499
- return `${resizeKey}`;
500
- }
501
- const MIN_LIVE_AREA_ROWS = 3;
502
- const INPUT_AREA_ROWS = 3;
503
- const STATUS_SLOT_ROWS = 2;
504
- const FOOTER_ONE_LINE_ROWS = 1;
505
- const FOOTER_TWO_LINE_ROWS = 2;
506
- const GOAL_STATUS_ROWS = 1;
507
- const COLLAPSED_FOOTER_STATUS_ROWS = 1;
508
- const MAX_EXPANDED_BACKGROUND_TASK_ROWS = 7;
509
- function isAgentSpacingKind(kind) {
510
- return [
511
- "assistant",
512
- "queued",
513
- "goal_progress",
514
- "tool_start",
515
- "tool_done",
516
- "tool_group",
517
- "server_tool_start",
518
- "server_tool_done",
519
- "subagent_group",
520
- "info",
521
- "error",
522
- "stopped",
523
- "plan_transition",
524
- "goal_agent_transition",
525
- "model_transition",
526
- "theme_transition",
527
- "plan_event",
528
- "update_notice",
529
- "compacting",
530
- "compacted",
531
- "style_pack",
532
- "setup_hint",
533
- ].includes(kind);
534
- }
535
- function isToolBoundaryKind(kind) {
536
- return [
537
- "goal_progress",
538
- "tool_start",
539
- "tool_done",
540
- "tool_group",
541
- "server_tool_start",
542
- "server_tool_done",
543
- "subagent_group",
544
- ].includes(kind);
545
- }
546
- function isAgentSpacingItem(item) {
547
- return isAgentSpacingKind(item.kind);
548
- }
549
- export function shouldTopSpaceAfterPrintedAgentBoundary({ currentKind, previousLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
550
- const needsExternalSpacing = isAgentSpacingKind(currentKind);
551
- if (!needsExternalSpacing)
552
- return false;
553
- if (previousLiveItem !== undefined)
554
- return false;
555
- const previousKind = lastPendingHistoryItem?.kind ?? lastHistoryItem?.kind;
556
- return previousKind !== undefined && isAgentSpacingKind(previousKind);
557
- }
558
- export function shouldTopSpaceAssistantAfterToolBoundary({ text, previousLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
559
- if (text.trim().length === 0)
560
- return false;
561
- if (shouldTopSpaceAfterPrintedAgentBoundary({
562
- currentKind: "assistant",
563
- previousLiveItem,
564
- lastPendingHistoryItem,
565
- lastHistoryItem,
566
- })) {
567
- return true;
568
- }
569
- const previousKind = previousLiveItem?.kind;
570
- return previousKind !== undefined && isToolBoundaryKind(previousKind);
571
- }
572
- export function shouldTopSpaceStreamingAssistant({ visibleStreamingText, lastLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
573
- return shouldTopSpaceAssistantAfterToolBoundary({
574
- text: visibleStreamingText,
575
- previousLiveItem: lastLiveItem,
576
- lastPendingHistoryItem,
577
- lastHistoryItem,
578
- });
579
- }
580
- export function getChatControlsLayoutDecision({ rows, agentRunning, activityVisible, doneStatusVisible, stallStatusVisible, exitPending, footerStatusLayout, taskBarExpanded, goalStatusEntryCount, footerFitsOnOneLine, }) {
581
- const statusRows = activityVisible || stallStatusVisible || doneStatusVisible || agentRunning
582
- ? STATUS_SLOT_ROWS
583
- : 0;
584
- const footerRows = exitPending || footerFitsOnOneLine ? FOOTER_ONE_LINE_ROWS : FOOTER_TWO_LINE_ROWS;
585
- const goalRows = !exitPending && goalStatusEntryCount > 0 ? GOAL_STATUS_ROWS : 0;
586
- const footerStatusRows = footerStatusLayout.stack
587
- ? Number(footerStatusLayout.hasBackgroundTasks) + Number(footerStatusLayout.hasUpdateNotice)
588
- : footerStatusLayout.hasBackgroundTasks || footerStatusLayout.hasUpdateNotice
589
- ? COLLAPSED_FOOTER_STATUS_ROWS
590
- : 0;
591
- const expandedTaskRows = taskBarExpanded && footerStatusLayout.hasBackgroundTasks
592
- ? MAX_EXPANDED_BACKGROUND_TASK_ROWS - COLLAPSED_FOOTER_STATUS_ROWS
593
- : 0;
594
- const controlsRows = statusRows + INPUT_AREA_ROWS + footerRows + goalRows + footerStatusRows + expandedTaskRows;
595
- const maxControlsRows = Math.max(1, rows - MIN_LIVE_AREA_ROWS);
596
- const boundedControlsRows = Math.min(controlsRows, maxControlsRows);
597
- return {
598
- controlsRows: boundedControlsRows,
599
- liveAreaRows: Math.max(MIN_LIVE_AREA_ROWS, rows - boundedControlsRows),
600
- };
601
- }
602
- // flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
603
- /** Check whether an item is still active (running spinner, pending result). */
604
- export function isActiveItem(item) {
605
- switch (item.kind) {
606
- case "tool_start":
607
- case "server_tool_start":
608
- case "queued":
609
- case "compacting":
610
- return true;
611
- case "tool_group":
612
- return item.tools.some((t) => t.status === "running");
613
- case "subagent_group":
614
- return item.agents.some((a) => a.status === "running");
615
- default:
616
- return false;
617
- }
618
- }
619
- /**
620
- * Partition live items into completed (flushable to finalized history) and still-active.
621
- * Completed items precede active ones — we flush the longest contiguous prefix
622
- * of completed items to keep ordering stable.
623
- */
624
- export function partitionCompleted(items) {
625
- // Find the first active item — everything before it is safe to flush as a
626
- // single chronological prefix. Splitting assistant text out of that prefix
627
- // lets later tool rows print to scrollback above the message that introduced
628
- // them, so keep the prefix intact.
629
- const firstActiveIdx = items.findIndex(isActiveItem);
630
- if (firstActiveIdx === -1) {
631
- return { flushed: items, remaining: [] };
632
- }
633
- if (firstActiveIdx === 0) {
634
- return { flushed: [], remaining: items };
635
- }
636
- return {
637
- flushed: items.slice(0, firstActiveIdx),
638
- remaining: items.slice(firstActiveIdx),
639
- };
640
- }
641
- function normalizeAssistantText(text) {
642
- return stripDoneMarkers(text).trim();
643
- }
644
- function isReasoningMarkerText(text) {
645
- return /^(?:currentItem\?\.type\s*=+\s*)?["']?reasoning["']?$/u.test(text.trim());
646
- }
647
- function isSameAssistantText(item, text) {
648
- return item.kind === "assistant" && normalizeAssistantText(item.text) === text;
649
- }
650
- export function pinStreamingTextBeforeToolBoundary({ items, visibleStreamingText, thinking, thinkingMs, makeId, }) {
651
- const text = normalizeAssistantText(visibleStreamingText);
652
- if (text.length === 0 || isReasoningMarkerText(text))
653
- return items;
654
- if (items.some((item) => item.kind === "assistant"))
655
- return items;
656
- return [
657
- ...items,
658
- {
659
- kind: "assistant",
660
- text,
661
- thinking: thinking.length > 0 ? thinking : undefined,
662
- thinkingMs: thinking.length > 0 ? thinkingMs : undefined,
663
- id: makeId(),
664
- },
665
- ];
666
- }
667
153
  // ── Duration summary ─────────────────────────────────────
668
154
  function formatDuration(ms) {
669
155
  const totalSec = Math.round(ms / 1000);
@@ -949,11 +435,8 @@ export function App(props) {
949
435
  onRuntimeStateChange?.({ provider: currentProvider });
950
436
  }, [currentProvider, onRuntimeStateChange]);
951
437
  useEffect(() => {
952
- if (thinkingLevel && !isOpenAIGptModel(currentProvider, currentModel)) {
953
- const maxLevel = getMaxThinkingLevel(currentModel);
954
- if (thinkingLevel !== maxLevel) {
955
- setThinkingLevel(maxLevel);
956
- }
438
+ if (thinkingLevel && !isThinkingLevelSupported(currentProvider, currentModel, thinkingLevel)) {
439
+ setThinkingLevel(getNextThinkingLevel(currentProvider, currentModel, undefined));
957
440
  }
958
441
  }, [currentProvider, currentModel, thinkingLevel]);
959
442
  useEffect(() => {
@@ -1114,6 +597,29 @@ export function App(props) {
1114
597
  }
1115
598
  return newPrompt;
1116
599
  }, [rebuildSystemPrompt]);
600
+ useEffect(() => {
601
+ if (!props.connectInitialMcpTools)
602
+ return;
603
+ let cancelled = false;
604
+ void props
605
+ .connectInitialMcpTools()
606
+ .then((mcpTools) => {
607
+ if (cancelled || mcpTools.length === 0)
608
+ return;
609
+ setCurrentTools((prev) => {
610
+ const next = [...prev.filter((tool) => !tool.name.startsWith("mcp__")), ...mcpTools];
611
+ currentToolsRef.current = next;
612
+ void replaceSystemPrompt({ tools: next });
613
+ return next;
614
+ });
615
+ })
616
+ .catch((err) => {
617
+ log("WARN", "mcp", `MCP initialization failed: ${err instanceof Error ? err.message : String(err)}`);
618
+ });
619
+ return () => {
620
+ cancelled = true;
621
+ };
622
+ }, [props.connectInitialMcpTools, replaceSystemPrompt]);
1117
623
  const setGoalModeAndPrompt = useCallback(async (nextMode, options) => {
1118
624
  goalModeStateRef.current = nextMode;
1119
625
  if (props.goalModeRef)
@@ -1378,13 +884,10 @@ export function App(props) {
1378
884
  (props.repoMapReadFilesRef?.current.size ?? 0));
1379
885
  }, [props.repoMapChangedFilesRef, props.repoMapReadFilesRef]);
1380
886
  const getRepoMapBudget = useCallback(() => {
1381
- const userTurns = messagesRef.current.filter((message) => message.role === "user").length;
1382
- const readCount = props.repoMapReadFilesRef?.current.size ?? 0;
1383
- if (userTurns <= 1 && readCount === 0)
1384
- return FIRST_TURN_REPO_MAP_MAX_CHARS;
1385
- if (readCount > 0)
1386
- return FOCUSED_REPO_MAP_MAX_CHARS;
1387
- return FOCUSED_REPO_MAP_MAX_CHARS + 1000;
887
+ return getRepoMapBudgetForContext({
888
+ messages: messagesRef.current,
889
+ readFileCount: props.repoMapReadFilesRef?.current.size ?? 0,
890
+ });
1388
891
  }, [props.repoMapReadFilesRef]);
1389
892
  const refreshRepoMap = useCallback(async (latestUserPrompt) => {
1390
893
  const rendered = await buildRepoMap({
@@ -2879,7 +2382,7 @@ export function App(props) {
2879
2382
  (item.summaryRows !== undefined && item.summaryRows.length > 0) ||
2880
2383
  (item.summarySections !== undefined && item.summarySections.length > 0);
2881
2384
  const headerContentWidth = Math.max(10, columns - 3);
2882
- return withPrintedBoundarySpacing(_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginTop: 1, flexShrink: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(ToolUseLoader, { status: loaderStatus, staticDisplay: true }), _jsx(Box, { flexGrow: 1, width: headerContentWidth, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: truncateGoalProgressText(item.title) }), item.workerId ? (_jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] })) : null] }) })] }), hasBody ? (_jsx(MessageResponse, { children: _jsxs(Box, { flexDirection: "column", flexShrink: 1, children: [item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: truncateGoalProgressText(item.detail) })) : null, item.summaryRows?.map((row) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.textDim, children: row.label.padEnd(12) }), _jsx(Text, { color: theme.text, children: truncateGoalProgressText(row.value) }), row.detail ? (_jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", truncateGoalProgressText(row.detail)] })) : null] }, row.label))), item.summarySections?.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: theme.textDim, bold: true, children: section.title }), section.lines.map((line, sectionLineIndex) => (_jsx(Text, { color: theme.text, wrap: "wrap", children: `• ${truncateGoalProgressText(line)}` }, `${section.title}-${sectionLineIndex}`)))] }, section.title)))] }) })) : null] }, item.id));
2385
+ return withPrintedBoundarySpacing(_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginTop: 1, flexShrink: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(ToolUseLoader, { status: loaderStatus, staticDisplay: true, color: color }), _jsx(Box, { flexGrow: 1, width: headerContentWidth, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: truncateGoalProgressText(item.title) }), item.workerId ? (_jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] })) : null] }) })] }), hasBody ? (_jsx(MessageResponse, { children: _jsxs(Box, { flexDirection: "column", flexShrink: 1, children: [item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: truncateGoalProgressText(item.detail) })) : null, item.summaryRows?.map((row) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.textDim, children: row.label.padEnd(12) }), _jsx(Text, { color: theme.text, children: truncateGoalProgressText(row.value) }), row.detail ? (_jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", truncateGoalProgressText(row.detail)] })) : null] }, row.label))), item.summarySections?.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: theme.textDim, bold: true, children: section.title }), section.lines.map((line, sectionLineIndex) => (_jsx(Text, { color: theme.text, wrap: "wrap", children: `• ${truncateGoalProgressText(line)}` }, `${section.title}-${sectionLineIndex}`)))] }, section.title)))] }) })) : null] }, item.id));
2883
2386
  }
2884
2387
  case "style_pack": {
2885
2388
  const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
@@ -2909,9 +2412,9 @@ export function App(props) {
2909
2412
  case "update_notice":
2910
2413
  return (_jsx(Box, { paddingLeft: 1, marginTop: 1, flexShrink: 1, children: _jsx(Box, { flexShrink: 1, borderStyle: "round", borderColor: theme.commandColor, paddingX: 1, children: _jsxs(Text, { color: theme.commandColor, bold: true, wrap: "wrap", children: ["✨ ", item.text] }) }) }, item.id));
2911
2414
  case "plan_transition":
2912
- return renderStatusMessage(item.id, "● ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
2415
+ return renderStatusMessage(item.id, `${BLACK_CIRCLE} `, normalizeStatusText(item.text), theme.commandColor, { bold: true });
2913
2416
  case "goal_agent_transition":
2914
- return renderStatusMessage(item.id, "● ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
2417
+ return renderStatusMessage(item.id, `${BLACK_CIRCLE} `, normalizeStatusText(item.text), theme.commandColor, { bold: true });
2915
2418
  case "model_transition":
2916
2419
  return renderStatusMessage(item.id, "▸ ", _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.textDim, children: "Switched to " }), _jsx(Text, { color: theme.commandColor, bold: true, children: item.modelName })] }), theme.commandColor, { bold: true });
2917
2420
  case "theme_transition":
@@ -2993,9 +2496,13 @@ export function App(props) {
2993
2496
  const detail = eventInfo?.kind === "worker"
2994
2497
  ? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
2995
2498
  : `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
2996
- if (agentRunningRef.current) {
2997
- queuedGoalSyntheticEventsRef.current += 1;
2998
- void setGoalModeAndPrompt("coordinator");
2499
+ const route = routeGoalSyntheticEvent({
2500
+ agentRunning: agentRunningRef.current,
2501
+ queuedSyntheticEvents: queuedGoalSyntheticEventsRef.current,
2502
+ });
2503
+ if (route.action === "queue") {
2504
+ queuedGoalSyntheticEventsRef.current = route.nextQueuedSyntheticEvents;
2505
+ void setGoalModeAndPrompt(route.nextGoalMode);
2999
2506
  appendGoalProgress({
3000
2507
  kind: "goal_progress",
3001
2508
  phase: "orchestrator_reviewing",