@kenkaiiii/ggcoder 4.3.212 → 4.3.214
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -8
- package/dist/cli.d.ts +3 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +112 -61
- package/dist/cli.js.map +1 -1
- package/dist/core/continue-replay-inventory.test.d.ts +2 -0
- package/dist/core/continue-replay-inventory.test.d.ts.map +1 -0
- package/dist/core/continue-replay-inventory.test.js +42 -0
- package/dist/core/continue-replay-inventory.test.js.map +1 -0
- package/dist/core/goal-controller.d.ts +2 -0
- package/dist/core/goal-controller.d.ts.map +1 -1
- package/dist/core/goal-controller.js +283 -24
- package/dist/core/goal-controller.js.map +1 -1
- package/dist/core/goal-controller.test.js +413 -16
- package/dist/core/goal-controller.test.js.map +1 -1
- package/dist/core/goal-lifecycle-smoke.test.js +48 -6
- package/dist/core/goal-lifecycle-smoke.test.js.map +1 -1
- package/dist/core/goal-prerequisites.d.ts +5 -0
- package/dist/core/goal-prerequisites.d.ts.map +1 -1
- package/dist/core/goal-prerequisites.js +37 -0
- package/dist/core/goal-prerequisites.js.map +1 -1
- package/dist/core/goal-prerequisites.test.js +29 -1
- package/dist/core/goal-prerequisites.test.js.map +1 -1
- package/dist/core/goal-references.d.ts +14 -0
- package/dist/core/goal-references.d.ts.map +1 -0
- package/dist/core/goal-references.js +153 -0
- package/dist/core/goal-references.js.map +1 -0
- package/dist/core/goal-references.test.d.ts +2 -0
- package/dist/core/goal-references.test.d.ts.map +1 -0
- package/dist/core/goal-references.test.js +77 -0
- package/dist/core/goal-references.test.js.map +1 -0
- package/dist/core/goal-store.d.ts +25 -0
- package/dist/core/goal-store.d.ts.map +1 -1
- package/dist/core/goal-store.js +150 -36
- package/dist/core/goal-store.js.map +1 -1
- package/dist/core/goal-store.test.js +19 -2
- package/dist/core/goal-store.test.js.map +1 -1
- package/dist/core/goal-verifier.d.ts.map +1 -1
- package/dist/core/goal-verifier.js +4 -1
- package/dist/core/goal-verifier.js.map +1 -1
- package/dist/core/goal-verifier.test.js +43 -0
- package/dist/core/goal-verifier.test.js.map +1 -1
- package/dist/core/goal-worker.d.ts +2 -0
- package/dist/core/goal-worker.d.ts.map +1 -1
- package/dist/core/goal-worker.js +33 -9
- package/dist/core/goal-worker.js.map +1 -1
- package/dist/core/goal-worker.test.js +49 -1
- package/dist/core/goal-worker.test.js.map +1 -1
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +28 -846
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +40 -78
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/runtime-mode.d.ts +14 -0
- package/dist/core/runtime-mode.d.ts.map +1 -0
- package/dist/core/runtime-mode.js +10 -0
- package/dist/core/runtime-mode.js.map +1 -0
- package/dist/core/session-restore-display.test.d.ts +2 -0
- package/dist/core/session-restore-display.test.d.ts.map +1 -0
- package/dist/core/session-restore-display.test.js +100 -0
- package/dist/core/session-restore-display.test.js.map +1 -0
- package/dist/core/verify-commands.js +4 -4
- package/dist/core/verify-commands.js.map +1 -1
- package/dist/system-prompt.d.ts +2 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +51 -37
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +147 -40
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/bash.d.ts +3 -2
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +11 -4
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +5 -3
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +14 -4
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +0 -10
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/goal-mode.test.d.ts +2 -0
- package/dist/tools/goal-mode.test.d.ts.map +1 -0
- package/dist/tools/goal-mode.test.js +121 -0
- package/dist/tools/goal-mode.test.js.map +1 -0
- package/dist/tools/goals.d.ts +15 -3
- package/dist/tools/goals.d.ts.map +1 -1
- package/dist/tools/goals.js +336 -26
- package/dist/tools/goals.js.map +1 -1
- package/dist/tools/goals.test.js +346 -6
- package/dist/tools/goals.test.js.map +1 -1
- package/dist/tools/index.d.ts +7 -10
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -19
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/plan-mode.test.js +34 -224
- package/dist/tools/plan-mode.test.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +2 -6
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/subagent.d.ts +3 -2
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +4 -9
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/write.d.ts +5 -3
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +14 -13
- package/dist/tools/write.js.map +1 -1
- package/dist/tools/write.test.js +0 -16
- package/dist/tools/write.test.js.map +1 -1
- package/dist/ui/App.d.ts +145 -28
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +1153 -864
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/activity-phrases.d.ts.map +1 -1
- package/dist/ui/activity-phrases.js +0 -2
- package/dist/ui/activity-phrases.js.map +1 -1
- package/dist/ui/app-state-persistence.test.js +173 -5
- package/dist/ui/app-state-persistence.test.js.map +1 -1
- package/dist/ui/chat-layout-pinning.test.d.ts +2 -0
- package/dist/ui/chat-layout-pinning.test.d.ts.map +1 -0
- package/dist/ui/chat-layout-pinning.test.js +407 -0
- package/dist/ui/chat-layout-pinning.test.js.map +1 -0
- package/dist/ui/components/ActivityIndicator.d.ts +1 -2
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +63 -94
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/AssistantMessage.d.ts +6 -2
- package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
- package/dist/ui/components/AssistantMessage.js +9 -4
- package/dist/ui/components/AssistantMessage.js.map +1 -1
- package/dist/ui/components/AssistantMessage.test.d.ts +2 -0
- package/dist/ui/components/AssistantMessage.test.d.ts.map +1 -0
- package/dist/ui/components/AssistantMessage.test.js +369 -0
- package/dist/ui/components/AssistantMessage.test.js.map +1 -0
- package/dist/ui/components/BackgroundTasksBar.d.ts +1 -3
- package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
- package/dist/ui/components/BackgroundTasksBar.js +2 -4
- package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
- package/dist/ui/components/Banner.d.ts +1 -3
- package/dist/ui/components/Banner.d.ts.map +1 -1
- package/dist/ui/components/Banner.js +7 -3
- package/dist/ui/components/Banner.js.map +1 -1
- package/dist/ui/components/Footer.d.ts +26 -4
- package/dist/ui/components/Footer.d.ts.map +1 -1
- package/dist/ui/components/Footer.js +73 -21
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/components/GoalOverlay.d.ts +28 -20
- package/dist/ui/components/GoalOverlay.d.ts.map +1 -1
- package/dist/ui/components/GoalOverlay.js +283 -253
- package/dist/ui/components/GoalOverlay.js.map +1 -1
- package/dist/ui/components/InputArea.d.ts +2 -6
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/InputArea.js +40 -32
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/InputArea.test.js +11 -1
- package/dist/ui/components/InputArea.test.js.map +1 -1
- package/dist/ui/components/Markdown.d.ts +11 -11
- package/dist/ui/components/Markdown.d.ts.map +1 -1
- package/dist/ui/components/Markdown.js +25 -198
- package/dist/ui/components/Markdown.js.map +1 -1
- package/dist/ui/components/PlanOverlay.d.ts.map +1 -1
- package/dist/ui/components/PlanOverlay.js +1 -1
- package/dist/ui/components/PlanOverlay.js.map +1 -1
- package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ServerToolExecution.js +3 -2
- package/dist/ui/components/ServerToolExecution.js.map +1 -1
- package/dist/ui/components/SlashCommandMenu.d.ts +4 -3
- package/dist/ui/components/SlashCommandMenu.d.ts.map +1 -1
- package/dist/ui/components/SlashCommandMenu.js +38 -26
- package/dist/ui/components/SlashCommandMenu.js.map +1 -1
- package/dist/ui/components/StreamingArea.d.ts +11 -2
- package/dist/ui/components/StreamingArea.d.ts.map +1 -1
- package/dist/ui/components/StreamingArea.js +20 -23
- package/dist/ui/components/StreamingArea.js.map +1 -1
- package/dist/ui/components/StreamingArea.test.d.ts +2 -0
- package/dist/ui/components/StreamingArea.test.d.ts.map +1 -0
- package/dist/ui/components/StreamingArea.test.js +18 -0
- package/dist/ui/components/StreamingArea.test.js.map +1 -0
- package/dist/ui/components/ToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolExecution.js +11 -27
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolGroupExecution.js +9 -124
- package/dist/ui/components/ToolGroupExecution.js.map +1 -1
- package/dist/ui/components/UserMessage.d.ts.map +1 -1
- package/dist/ui/components/UserMessage.js +15 -10
- package/dist/ui/components/UserMessage.js.map +1 -1
- package/dist/ui/components/UserMessage.test.d.ts +2 -0
- package/dist/ui/components/UserMessage.test.d.ts.map +1 -0
- package/dist/ui/components/UserMessage.test.js +39 -0
- package/dist/ui/components/UserMessage.test.js.map +1 -0
- package/dist/ui/footer-status-layout.test.js +21 -7
- package/dist/ui/footer-status-layout.test.js.map +1 -1
- package/dist/ui/goal-events.d.ts +13 -0
- package/dist/ui/goal-events.d.ts.map +1 -1
- package/dist/ui/goal-events.js +81 -16
- package/dist/ui/goal-events.js.map +1 -1
- package/dist/ui/goal-events.test.js +76 -2
- package/dist/ui/goal-events.test.js.map +1 -1
- package/dist/ui/goal-lifecycle-orchestration.test.js +131 -34
- package/dist/ui/goal-lifecycle-orchestration.test.js.map +1 -1
- package/dist/ui/goal-overlay.test.js +121 -43
- package/dist/ui/goal-overlay.test.js.map +1 -1
- package/dist/ui/goal-summary.d.ts +14 -0
- package/dist/ui/goal-summary.d.ts.map +1 -0
- package/dist/ui/goal-summary.js +194 -0
- package/dist/ui/goal-summary.js.map +1 -0
- package/dist/ui/hooks/useAgentLoop.d.ts +8 -2
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +20 -9
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.test.d.ts +2 -0
- package/dist/ui/hooks/useAgentLoop.test.d.ts.map +1 -0
- package/dist/ui/hooks/useAgentLoop.test.js +8 -0
- package/dist/ui/hooks/useAgentLoop.test.js.map +1 -0
- package/dist/ui/hooks/useTerminalSize.d.ts +5 -9
- package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
- package/dist/ui/hooks/useTerminalSize.js +9 -14
- package/dist/ui/hooks/useTerminalSize.js.map +1 -1
- package/dist/ui/live-item-flush.d.ts +2 -2
- package/dist/ui/live-item-flush.d.ts.map +1 -1
- package/dist/ui/live-item-flush.js +8 -4
- package/dist/ui/live-item-flush.js.map +1 -1
- package/dist/ui/long-prompt-regression-harness.test.d.ts +2 -0
- package/dist/ui/long-prompt-regression-harness.test.d.ts.map +1 -0
- package/dist/ui/long-prompt-regression-harness.test.js +195 -0
- package/dist/ui/long-prompt-regression-harness.test.js.map +1 -0
- package/dist/ui/plan-overlay.test.js +7 -29
- package/dist/ui/plan-overlay.test.js.map +1 -1
- package/dist/ui/queued-message.test.d.ts.map +1 -1
- package/dist/ui/queued-message.test.js +76 -14
- package/dist/ui/queued-message.test.js.map +1 -1
- package/dist/ui/render.d.ts +21 -24
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +46 -28
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/render.test.d.ts +2 -0
- package/dist/ui/render.test.d.ts.map +1 -0
- package/dist/ui/render.test.js +16 -0
- package/dist/ui/render.test.js.map +1 -0
- package/dist/ui/scroll-stabilization.test.js +1 -1
- package/dist/ui/scroll-stabilization.test.js.map +1 -1
- package/dist/ui/slash-command-images.test.js +79 -4
- package/dist/ui/slash-command-images.test.js.map +1 -1
- package/dist/ui/terminal-history.d.ts +26 -0
- package/dist/ui/terminal-history.d.ts.map +1 -0
- package/dist/ui/terminal-history.js +910 -0
- package/dist/ui/terminal-history.js.map +1 -0
- package/dist/ui/terminal-history.test.d.ts +2 -0
- package/dist/ui/terminal-history.test.d.ts.map +1 -0
- package/dist/ui/terminal-history.test.js +314 -0
- package/dist/ui/terminal-history.test.js.map +1 -0
- package/dist/ui/tool-group-summary.d.ts +16 -0
- package/dist/ui/tool-group-summary.d.ts.map +1 -0
- package/dist/ui/tool-group-summary.js +123 -0
- package/dist/ui/tool-group-summary.js.map +1 -0
- package/dist/ui/tui-history-parity.test.d.ts +2 -0
- package/dist/ui/tui-history-parity.test.d.ts.map +1 -0
- package/dist/ui/tui-history-parity.test.js +243 -0
- package/dist/ui/tui-history-parity.test.js.map +1 -0
- package/dist/ui/utils/assistant-stream-split.d.ts +6 -0
- package/dist/ui/utils/assistant-stream-split.d.ts.map +1 -0
- package/dist/ui/utils/assistant-stream-split.js +37 -0
- package/dist/ui/utils/assistant-stream-split.js.map +1 -0
- package/dist/ui/utils/assistant-stream-split.test.d.ts +2 -0
- package/dist/ui/utils/assistant-stream-split.test.d.ts.map +1 -0
- package/dist/ui/utils/assistant-stream-split.test.js +58 -0
- package/dist/ui/utils/assistant-stream-split.test.js.map +1 -0
- package/dist/ui/utils/latex-to-unicode.d.ts +22 -0
- package/dist/ui/utils/latex-to-unicode.d.ts.map +1 -0
- package/dist/ui/utils/latex-to-unicode.js +538 -0
- package/dist/ui/utils/latex-to-unicode.js.map +1 -0
- package/dist/ui/utils/markdown-renderer.d.ts +20 -0
- package/dist/ui/utils/markdown-renderer.d.ts.map +1 -0
- package/dist/ui/utils/markdown-renderer.js +327 -0
- package/dist/ui/utils/markdown-renderer.js.map +1 -0
- package/dist/ui/utils/markdown-table.d.ts +9 -0
- package/dist/ui/utils/markdown-table.d.ts.map +1 -0
- package/dist/ui/utils/markdown-table.js +95 -0
- package/dist/ui/utils/markdown-table.js.map +1 -0
- package/dist/ui/utils/text-utils.d.ts +8 -0
- package/dist/ui/utils/text-utils.d.ts.map +1 -0
- package/dist/ui/utils/text-utils.js +16 -0
- package/dist/ui/utils/text-utils.js.map +1 -0
- package/dist/ui/utils/token-to-ansi.js +19 -9
- package/dist/ui/utils/token-to-ansi.js.map +1 -1
- package/dist/ui/utils/user-message-display.d.ts +7 -0
- package/dist/ui/utils/user-message-display.d.ts.map +1 -0
- package/dist/ui/utils/user-message-display.js +26 -0
- package/dist/ui/utils/user-message-display.js.map +1 -0
- package/dist/utils/format.js +0 -9
- package/dist/utils/format.js.map +1 -1
- package/package.json +9 -4
- package/dist/tools/enter-plan.d.ts +0 -8
- package/dist/tools/enter-plan.d.ts.map +0 -1
- package/dist/tools/enter-plan.js +0 -30
- package/dist/tools/enter-plan.js.map +0 -1
- package/dist/tools/exit-plan.d.ts +0 -8
- package/dist/tools/exit-plan.d.ts.map +0 -1
- package/dist/tools/exit-plan.js +0 -36
- package/dist/tools/exit-plan.js.map +0 -1
- package/dist/tools/tasks.d.ts +0 -16
- package/dist/tools/tasks.d.ts.map +0 -1
- package/dist/tools/tasks.js +0 -133
- package/dist/tools/tasks.js.map +0 -1
- package/dist/ui/components/EyesOverlay.d.ts +0 -10
- package/dist/ui/components/EyesOverlay.d.ts.map +0 -1
- package/dist/ui/components/EyesOverlay.js +0 -220
- package/dist/ui/components/EyesOverlay.js.map +0 -1
- package/dist/ui/components/TaskOverlay.d.ts +0 -10
- package/dist/ui/components/TaskOverlay.d.ts.map +0 -1
- package/dist/ui/components/TaskOverlay.js +0 -267
- package/dist/ui/components/TaskOverlay.js.map +0 -1
package/dist/ui/App.js
CHANGED
|
@@ -1,39 +1,37 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
|
|
3
|
-
import { Box, Text,
|
|
3
|
+
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 {
|
|
8
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
7
|
+
import { writeFileSync } from "node:fs";
|
|
11
8
|
import { playNotificationSound } from "../utils/sound.js";
|
|
12
9
|
import { formatError, } from "@kenkaiiii/gg-ai";
|
|
13
10
|
import { extractImagePaths } from "../utils/image.js";
|
|
11
|
+
import { buildGoalReferenceContext, formatGoalReferencesForPrompt, } from "../core/goal-references.js";
|
|
14
12
|
import { useAgentLoop } from "./hooks/useAgentLoop.js";
|
|
15
|
-
import { isEyesActive, journalCount } from "@kenkaiiii/ggcoder-eyes";
|
|
16
13
|
import { UserMessage } from "./components/UserMessage.js";
|
|
17
14
|
import { AssistantMessage } from "./components/AssistantMessage.js";
|
|
18
15
|
import { ToolExecution } from "./components/ToolExecution.js";
|
|
16
|
+
import { ToolUseLoader } from "./components/ToolUseLoader.js";
|
|
19
17
|
import { ToolGroupExecution } from "./components/ToolGroupExecution.js";
|
|
20
18
|
import { ServerToolExecution } from "./components/ServerToolExecution.js";
|
|
19
|
+
import { MessageResponse } from "./components/MessageResponse.js";
|
|
21
20
|
import { SubAgentPanel } from "./components/SubAgentPanel.js";
|
|
22
21
|
import { CompactionSpinner, CompactionDone } from "./components/CompactionNotice.js";
|
|
23
22
|
import { createWebSearchTool } from "../tools/web-search.js";
|
|
24
23
|
import { StreamingArea } from "./components/StreamingArea.js";
|
|
25
24
|
import { ActivityIndicator } from "./components/ActivityIndicator.js";
|
|
26
25
|
import { InputArea } from "./components/InputArea.js";
|
|
27
|
-
import { Footer } from "./components/Footer.js";
|
|
26
|
+
import { Footer, doesFooterFitOnOneLine } from "./components/Footer.js";
|
|
28
27
|
import { GoalStatusBar, reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
|
|
29
28
|
import { Banner } from "./components/Banner.js";
|
|
30
29
|
import { PlanOverlay } from "./components/PlanOverlay.js";
|
|
31
30
|
import { ModelSelector } from "./components/ModelSelector.js";
|
|
32
|
-
import { TaskOverlay } from "./components/TaskOverlay.js";
|
|
33
31
|
import { GoalOverlay } from "./components/GoalOverlay.js";
|
|
34
32
|
import { PixelOverlay } from "./components/PixelOverlay.js";
|
|
33
|
+
import { buildGoalFinalSummarySections, buildGoalSummaryRows, goalPassedDetail, } from "./goal-summary.js";
|
|
35
34
|
import { SkillsOverlay } from "./components/SkillsOverlay.js";
|
|
36
|
-
import { EyesOverlay } from "./components/EyesOverlay.js";
|
|
37
35
|
import { ThemeSelector } from "./components/ThemeSelector.js";
|
|
38
36
|
import { BackgroundTasksBar, getFooterStatusLayoutDecision, } from "./components/BackgroundTasksBar.js";
|
|
39
37
|
import { useTheme, useSetTheme } from "./theme/theme.js";
|
|
@@ -59,8 +57,9 @@ import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMes
|
|
|
59
57
|
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
60
58
|
import { getMCPServers } from "../core/mcp/index.js";
|
|
61
59
|
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
60
|
+
import { splitAssistantStreamingText } from "./utils/assistant-stream-split.js";
|
|
61
|
+
import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
|
|
62
|
+
import { canCompleteGoalRun, decideGoalNextAction, } from "../core/goal-controller.js";
|
|
64
63
|
import { runGoalPrerequisiteChecks } from "../core/goal-prerequisites.js";
|
|
65
64
|
import { runGoalVerifierCommand } from "../core/goal-verifier.js";
|
|
66
65
|
import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
|
|
@@ -117,6 +116,56 @@ export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS,
|
|
|
117
116
|
fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
|
|
118
117
|
};
|
|
119
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
|
+
}
|
|
120
169
|
export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
|
|
121
170
|
if (inputImages.length === 0)
|
|
122
171
|
return text;
|
|
@@ -156,14 +205,13 @@ export function buildUserContentWithAttachments(text, inputImages, modelSupports
|
|
|
156
205
|
// If only text parts remain after stripping images, simplify to plain string
|
|
157
206
|
return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
|
|
158
207
|
}
|
|
159
|
-
/** Tools that get aggregated into a single compact group when
|
|
208
|
+
/** Tools that get aggregated into a single compact group when possible. */
|
|
160
209
|
const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
161
210
|
const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
|
|
162
211
|
/**
|
|
163
|
-
* Cap memory by replacing old
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* entries so GC can reclaim the original data.
|
|
212
|
+
* Cap memory by replacing old finalized rows with tiny tombstones. The full
|
|
213
|
+
* transcript is already printed into terminal scrollback, so the in-memory copy
|
|
214
|
+
* only needs enough recent structure to survive remounts and session mirroring.
|
|
167
215
|
*/
|
|
168
216
|
const MAX_LIVE_HISTORY = 200;
|
|
169
217
|
function compactHistory(items) {
|
|
@@ -190,66 +238,41 @@ function summarizeGoalCompletion(summary) {
|
|
|
190
238
|
const verificationLine = lines.find((line) => /^(Verification|Verified|Result):/i.test(line));
|
|
191
239
|
return statusLine ?? changedLine ?? verificationLine ?? lines[0];
|
|
192
240
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return tasks.filter((task) => task.status === status).length;
|
|
198
|
-
}
|
|
199
|
-
function firstText(values) {
|
|
200
|
-
return values.find((value) => value !== undefined && value.trim().length > 0)?.trim();
|
|
201
|
-
}
|
|
202
|
-
function truncateGoalSummary(value, maxLength = 90) {
|
|
203
|
-
const normalized = value.replace(/\s+/g, " ").trim();
|
|
204
|
-
if (normalized.length <= maxLength)
|
|
241
|
+
const GOAL_PROGRESS_TEXT_LIMIT = 72;
|
|
242
|
+
export function truncateGoalProgressText(text) {
|
|
243
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
244
|
+
if (normalized.length <= GOAL_PROGRESS_TEXT_LIMIT)
|
|
205
245
|
return normalized;
|
|
206
|
-
return `${normalized.slice(0,
|
|
246
|
+
return `${normalized.slice(0, GOAL_PROGRESS_TEXT_LIMIT - 1).trimEnd()}…`;
|
|
207
247
|
}
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
value: run.tasks.length > 0 ? `${doneTasks}/${run.tasks.length} done` : "none",
|
|
220
|
-
...(taskSuffix.length > 0 ? { detail: taskSuffix.join(", ") } : {}),
|
|
221
|
-
});
|
|
222
|
-
const verifierResult = run.verifier?.lastResult;
|
|
223
|
-
const verifierDetail = firstText([verifierResult?.outputPath, run.verifier?.command]);
|
|
224
|
-
rows.push({
|
|
225
|
-
label: "Verifier",
|
|
226
|
-
value: verifierResult?.status ?? (run.verifier?.command ? "ready" : "missing"),
|
|
227
|
-
...(verifierDetail ? { detail: truncateGoalSummary(verifierDetail) } : {}),
|
|
228
|
-
});
|
|
229
|
-
const latestEvidence = run.evidence.at(-1);
|
|
230
|
-
rows.push({
|
|
231
|
-
label: "Evidence",
|
|
232
|
-
value: `${run.evidence.length} recorded`,
|
|
233
|
-
...(latestEvidence
|
|
234
|
-
? { detail: truncateGoalSummary(latestEvidence.path ?? latestEvidence.label) }
|
|
235
|
-
: {}),
|
|
236
|
-
});
|
|
237
|
-
if (run.status === "blocked" || run.status === "paused" || run.blockers.length > 0) {
|
|
238
|
-
rows.push({
|
|
239
|
-
label: run.status === "paused" ? "Paused on" : "Blocked on",
|
|
240
|
-
value: truncateGoalSummary(goalHasBlockingPrerequisites(run)
|
|
241
|
-
? formatGoalBlockingPrerequisites(run)
|
|
242
|
-
: (run.blockers[0] ?? "manual review"), 110),
|
|
243
|
-
});
|
|
248
|
+
function formatGoalWorkerFinishedTitle(taskTitle, status) {
|
|
249
|
+
const prefix = status === "done" ? "Done" : "Failed";
|
|
250
|
+
return truncateGoalProgressText(`${prefix}: ${taskTitle}`);
|
|
251
|
+
}
|
|
252
|
+
function goalProgressLoaderStatus(item) {
|
|
253
|
+
if (item.status === "failed" || item.status === "fail" || item.status === "blocked")
|
|
254
|
+
return "error";
|
|
255
|
+
if (item.phase === "worker_finished" ||
|
|
256
|
+
item.phase === "verifier_finished" ||
|
|
257
|
+
item.phase === "terminal") {
|
|
258
|
+
return "done";
|
|
244
259
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
260
|
+
return "running";
|
|
261
|
+
}
|
|
262
|
+
function goalProgressColor(item, theme) {
|
|
263
|
+
const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
|
|
264
|
+
if (isError)
|
|
265
|
+
return theme.error;
|
|
266
|
+
if (item.phase === "worker_finished" || item.phase === "terminal")
|
|
267
|
+
return theme.success;
|
|
268
|
+
if (item.phase === "verifier_finished" || item.phase === "verifier_started")
|
|
269
|
+
return theme.accent;
|
|
270
|
+
if (item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working") {
|
|
271
|
+
return theme.secondary;
|
|
251
272
|
}
|
|
252
|
-
|
|
273
|
+
if (item.phase === "continuing")
|
|
274
|
+
return theme.warning;
|
|
275
|
+
return theme.primary;
|
|
253
276
|
}
|
|
254
277
|
function goalTerminalProgressId(run) {
|
|
255
278
|
return `goal-terminal-${run.id}`;
|
|
@@ -262,10 +285,20 @@ function goalTerminalRunIdFromItem(item) {
|
|
|
262
285
|
return item.id.slice("goal-terminal-".length);
|
|
263
286
|
}
|
|
264
287
|
function goalProgressMatchesDraft(item, draft) {
|
|
265
|
-
return (item.
|
|
288
|
+
return (item.phase === draft.phase &&
|
|
289
|
+
item.title === draft.title &&
|
|
266
290
|
item.detail === draft.detail &&
|
|
291
|
+
item.workerId === draft.workerId &&
|
|
267
292
|
item.status === draft.status &&
|
|
268
|
-
JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? [])
|
|
293
|
+
JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []) &&
|
|
294
|
+
JSON.stringify(item.summarySections ?? []) === JSON.stringify(draft.summarySections ?? []));
|
|
295
|
+
}
|
|
296
|
+
export function appendGoalProgressDraft(items, draft, makeId) {
|
|
297
|
+
const previous = items.at(-1);
|
|
298
|
+
if (previous?.kind === "goal_progress" && goalProgressMatchesDraft(previous, draft)) {
|
|
299
|
+
return items;
|
|
300
|
+
}
|
|
301
|
+
return [...items, { ...draft, id: makeId() }];
|
|
269
302
|
}
|
|
270
303
|
/**
|
|
271
304
|
* Reconcile terminal Goal cards that are already visible in this UI session.
|
|
@@ -277,6 +310,16 @@ function goalProgressMatchesDraft(item, draft) {
|
|
|
277
310
|
* event append that card first, then use this helper to tombstone stale older
|
|
278
311
|
* cards for the same run.
|
|
279
312
|
*/
|
|
313
|
+
export function getNextGeneratedItemId(items) {
|
|
314
|
+
let max = -1;
|
|
315
|
+
for (const item of items) {
|
|
316
|
+
const raw = item.id.startsWith("ui-") ? item.id.slice(3) : item.id;
|
|
317
|
+
const n = Number(raw);
|
|
318
|
+
if (Number.isInteger(n) && n >= 0 && n > max)
|
|
319
|
+
max = n;
|
|
320
|
+
}
|
|
321
|
+
return max + 1;
|
|
322
|
+
}
|
|
280
323
|
export function completedItemsWithDurableGoalTerminalProgress(items, runs) {
|
|
281
324
|
const runIds = new Set(runs.map((run) => run.id));
|
|
282
325
|
const terminalByRun = new Map(runs
|
|
@@ -304,8 +347,9 @@ export function formatGoalTerminalProgress(run) {
|
|
|
304
347
|
kind: "goal_progress",
|
|
305
348
|
phase: "terminal",
|
|
306
349
|
title: `Goal passed: ${run.title}`,
|
|
307
|
-
detail:
|
|
350
|
+
detail: goalPassedDetail(run),
|
|
308
351
|
summaryRows: buildGoalSummaryRows(run),
|
|
352
|
+
summarySections: buildGoalFinalSummarySections(run),
|
|
309
353
|
status: run.status,
|
|
310
354
|
};
|
|
311
355
|
case "failed":
|
|
@@ -345,29 +389,186 @@ export function formatGoalTerminalProgress(run) {
|
|
|
345
389
|
}
|
|
346
390
|
}
|
|
347
391
|
export function shouldHideHistoryForOverlayView(isOverlayView, _isAgentRunning) {
|
|
348
|
-
// Overlay panes are standalone full-screen states.
|
|
349
|
-
//
|
|
392
|
+
// Overlay panes are standalone full-screen states. Finalized chat rows are
|
|
393
|
+
// printed outside Ink, so overlays should never replay transcript UI behind them.
|
|
350
394
|
return isOverlayView;
|
|
351
395
|
}
|
|
352
396
|
export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
|
|
353
|
-
return isAgentRunning &&
|
|
397
|
+
return isAgentRunning && overlayPane === "goal";
|
|
354
398
|
}
|
|
355
399
|
export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender: _stabilizeOverlayPaneRerender, }) {
|
|
356
400
|
return shouldHideHistoryForOverlay;
|
|
357
401
|
}
|
|
358
|
-
export function
|
|
359
|
-
|
|
402
|
+
export function getDoneFlushDecision({ planOverlayPending, goalMode, goalAutoExpand, }) {
|
|
403
|
+
return {
|
|
404
|
+
showDoneStatus: !(planOverlayPending ||
|
|
405
|
+
goalMode === "planner" ||
|
|
406
|
+
goalMode === "setup" ||
|
|
407
|
+
goalAutoExpand),
|
|
408
|
+
flushLiveItems: true,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
export function getGoalSetupFinishedPaneTransition() {
|
|
412
|
+
return {
|
|
413
|
+
overlay: "goal",
|
|
414
|
+
goalAutoExpand: true,
|
|
415
|
+
planAutoExpand: false,
|
|
416
|
+
suppressDoneStatus: true,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
export function getGoalSetupPaneTransitionAfterRun({ isGoalSetupCommand, setupPanePending, }) {
|
|
420
|
+
return isGoalSetupCommand && setupPanePending ? getGoalSetupFinishedPaneTransition() : null;
|
|
421
|
+
}
|
|
422
|
+
export function shouldResetUIForSetupPaneTransition({ hasResetUI, hasSessionStore, }) {
|
|
423
|
+
// Opening a review pane is a full-screen state transition. A bare React state
|
|
424
|
+
// flip hides history in the virtual tree, but it does not reset Ink/log-update's
|
|
425
|
+
// already-written terminal frame, so the pane can render below prior chat.
|
|
426
|
+
return hasResetUI && hasSessionStore;
|
|
427
|
+
}
|
|
428
|
+
export const shouldResetUIForGoalSetupPaneTransition = shouldResetUIForSetupPaneTransition;
|
|
429
|
+
export function getGoalActivationPaneTransition() {
|
|
430
|
+
return { overlay: null, goalAutoExpand: false, planAutoExpand: false, resetReviewScreen: true };
|
|
431
|
+
}
|
|
432
|
+
export function getGoalContinuationChoiceKey({ runId, decision, }) {
|
|
433
|
+
switch (decision.kind) {
|
|
434
|
+
case "create_task":
|
|
435
|
+
return `${runId}:create_task:${decision.title}:${decision.prompt}`;
|
|
436
|
+
case "start_worker":
|
|
437
|
+
case "pause":
|
|
438
|
+
return `${runId}:${decision.kind}:${decision.task.id}:${decision.attempts}`;
|
|
439
|
+
case "run_verifier":
|
|
440
|
+
return `${runId}:run_verifier:${decision.command}`;
|
|
441
|
+
case "blocked":
|
|
442
|
+
case "complete":
|
|
443
|
+
case "terminal":
|
|
444
|
+
case "wait":
|
|
445
|
+
return `${runId}:${decision.kind}:${decision.reason}`;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, hasParagraphBreakLiveUserMessage = false, }) {
|
|
449
|
+
const shouldPreserveStatic = isUserScrolled || hasTallLiveUserMessage || hasParagraphBreakLiveUserMessage;
|
|
450
|
+
const shouldAutoFollow = !(isUserScrolled || hasTallLiveUserMessage);
|
|
360
451
|
return {
|
|
361
|
-
preserveStatic:
|
|
362
|
-
autoFollow:
|
|
452
|
+
preserveStatic: shouldPreserveStatic && hasNewOutput,
|
|
453
|
+
autoFollow: shouldAutoFollow,
|
|
363
454
|
};
|
|
364
455
|
}
|
|
456
|
+
export function nextGoalModeAfterAgentDone({ currentMode, runningGoalIds, queuedSyntheticEvents, activeContinuationFlights = 0, wasGoalSetupTurn, }) {
|
|
457
|
+
if (wasGoalSetupTurn)
|
|
458
|
+
return "off";
|
|
459
|
+
if (currentMode === "planner" || currentMode === "setup")
|
|
460
|
+
return currentMode;
|
|
461
|
+
if (queuedSyntheticEvents > 0)
|
|
462
|
+
return "coordinator";
|
|
463
|
+
if (activeContinuationFlights > 0)
|
|
464
|
+
return "coordinator";
|
|
465
|
+
if (currentMode === "coordinator" && runningGoalIds > 0)
|
|
466
|
+
return "coordinator";
|
|
467
|
+
return "off";
|
|
468
|
+
}
|
|
469
|
+
export function hasParagraphBreakLiveUserMessage(text) {
|
|
470
|
+
return /\n[ \t]*\n/.test(text);
|
|
471
|
+
}
|
|
365
472
|
export function isTallLiveUserMessage(text, rows) {
|
|
366
473
|
return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
|
|
367
474
|
}
|
|
368
475
|
export function getStaticHistoryKey({ resizeKey }) {
|
|
369
476
|
return `${resizeKey}`;
|
|
370
477
|
}
|
|
478
|
+
const MIN_LIVE_AREA_ROWS = 3;
|
|
479
|
+
const INPUT_AREA_ROWS = 3;
|
|
480
|
+
const STATUS_SLOT_ROWS = 2;
|
|
481
|
+
const FOOTER_ONE_LINE_ROWS = 1;
|
|
482
|
+
const FOOTER_TWO_LINE_ROWS = 2;
|
|
483
|
+
const GOAL_STATUS_ROWS = 1;
|
|
484
|
+
const COLLAPSED_FOOTER_STATUS_ROWS = 1;
|
|
485
|
+
const MAX_EXPANDED_BACKGROUND_TASK_ROWS = 7;
|
|
486
|
+
function isAgentSpacingKind(kind) {
|
|
487
|
+
return [
|
|
488
|
+
"assistant",
|
|
489
|
+
"queued",
|
|
490
|
+
"goal_progress",
|
|
491
|
+
"tool_start",
|
|
492
|
+
"tool_done",
|
|
493
|
+
"tool_group",
|
|
494
|
+
"server_tool_start",
|
|
495
|
+
"server_tool_done",
|
|
496
|
+
"subagent_group",
|
|
497
|
+
].includes(kind);
|
|
498
|
+
}
|
|
499
|
+
function isToolBoundaryKind(kind) {
|
|
500
|
+
return [
|
|
501
|
+
"goal_progress",
|
|
502
|
+
"tool_start",
|
|
503
|
+
"tool_done",
|
|
504
|
+
"tool_group",
|
|
505
|
+
"server_tool_start",
|
|
506
|
+
"server_tool_done",
|
|
507
|
+
"subagent_group",
|
|
508
|
+
].includes(kind);
|
|
509
|
+
}
|
|
510
|
+
function isAgentSpacingItem(item) {
|
|
511
|
+
return isAgentSpacingKind(item.kind);
|
|
512
|
+
}
|
|
513
|
+
export function shouldTopSpaceAfterPrintedAgentBoundary({ currentKind, previousLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
|
|
514
|
+
const needsExternalSpacing = [
|
|
515
|
+
"goal_progress",
|
|
516
|
+
"tool_start",
|
|
517
|
+
"tool_group",
|
|
518
|
+
"assistant",
|
|
519
|
+
"queued",
|
|
520
|
+
].includes(currentKind);
|
|
521
|
+
if (!needsExternalSpacing)
|
|
522
|
+
return false;
|
|
523
|
+
if (previousLiveItem !== undefined)
|
|
524
|
+
return false;
|
|
525
|
+
const previousKind = lastPendingHistoryItem?.kind ?? lastHistoryItem?.kind;
|
|
526
|
+
return previousKind !== undefined && isAgentSpacingKind(previousKind);
|
|
527
|
+
}
|
|
528
|
+
export function shouldTopSpaceAssistantAfterToolBoundary({ text, previousLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
|
|
529
|
+
if (text.trim().length === 0)
|
|
530
|
+
return false;
|
|
531
|
+
if (shouldTopSpaceAfterPrintedAgentBoundary({
|
|
532
|
+
currentKind: "assistant",
|
|
533
|
+
previousLiveItem,
|
|
534
|
+
lastPendingHistoryItem,
|
|
535
|
+
lastHistoryItem,
|
|
536
|
+
})) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
const previousKind = previousLiveItem?.kind;
|
|
540
|
+
return previousKind !== undefined && isToolBoundaryKind(previousKind);
|
|
541
|
+
}
|
|
542
|
+
export function shouldTopSpaceStreamingAssistant({ visibleStreamingText, lastLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
|
|
543
|
+
return shouldTopSpaceAssistantAfterToolBoundary({
|
|
544
|
+
text: visibleStreamingText,
|
|
545
|
+
previousLiveItem: lastLiveItem,
|
|
546
|
+
lastPendingHistoryItem,
|
|
547
|
+
lastHistoryItem,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
export function getChatControlsLayoutDecision({ rows, agentRunning, activityVisible, doneStatusVisible, stallStatusVisible, exitPending, footerStatusLayout, taskBarExpanded, goalStatusEntryCount, footerFitsOnOneLine, }) {
|
|
551
|
+
const statusRows = activityVisible || stallStatusVisible || doneStatusVisible || agentRunning
|
|
552
|
+
? STATUS_SLOT_ROWS
|
|
553
|
+
: 0;
|
|
554
|
+
const footerRows = exitPending || footerFitsOnOneLine ? FOOTER_ONE_LINE_ROWS : FOOTER_TWO_LINE_ROWS;
|
|
555
|
+
const goalRows = !exitPending && goalStatusEntryCount > 0 ? GOAL_STATUS_ROWS : 0;
|
|
556
|
+
const footerStatusRows = footerStatusLayout.stack
|
|
557
|
+
? Number(footerStatusLayout.hasBackgroundTasks) + Number(footerStatusLayout.hasUpdateNotice)
|
|
558
|
+
: footerStatusLayout.hasBackgroundTasks || footerStatusLayout.hasUpdateNotice
|
|
559
|
+
? COLLAPSED_FOOTER_STATUS_ROWS
|
|
560
|
+
: 0;
|
|
561
|
+
const expandedTaskRows = taskBarExpanded && footerStatusLayout.hasBackgroundTasks
|
|
562
|
+
? MAX_EXPANDED_BACKGROUND_TASK_ROWS - COLLAPSED_FOOTER_STATUS_ROWS
|
|
563
|
+
: 0;
|
|
564
|
+
const controlsRows = statusRows + INPUT_AREA_ROWS + footerRows + goalRows + footerStatusRows + expandedTaskRows;
|
|
565
|
+
const maxControlsRows = Math.max(1, rows - MIN_LIVE_AREA_ROWS);
|
|
566
|
+
const boundedControlsRows = Math.min(controlsRows, maxControlsRows);
|
|
567
|
+
return {
|
|
568
|
+
controlsRows: boundedControlsRows,
|
|
569
|
+
liveAreaRows: Math.max(MIN_LIVE_AREA_ROWS, rows - boundedControlsRows),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
371
572
|
// flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
|
|
372
573
|
/** Check whether an item is still active (running spinner, pending result). */
|
|
373
574
|
export function isActiveItem(item) {
|
|
@@ -386,15 +587,17 @@ export function isActiveItem(item) {
|
|
|
386
587
|
}
|
|
387
588
|
}
|
|
388
589
|
/**
|
|
389
|
-
* Partition live items into completed (flushable to
|
|
590
|
+
* Partition live items into completed (flushable to finalized history) and still-active.
|
|
390
591
|
* Completed items precede active ones — we flush the longest contiguous prefix
|
|
391
592
|
* of completed items to keep ordering stable.
|
|
392
593
|
*/
|
|
393
|
-
function partitionCompleted(items) {
|
|
394
|
-
// Find the first active item — everything before it is safe to flush
|
|
594
|
+
export function partitionCompleted(items) {
|
|
595
|
+
// Find the first active item — everything before it is safe to flush as a
|
|
596
|
+
// single chronological prefix. Splitting assistant text out of that prefix
|
|
597
|
+
// lets later tool rows print to scrollback above the message that introduced
|
|
598
|
+
// them, so keep the prefix intact.
|
|
395
599
|
const firstActiveIdx = items.findIndex(isActiveItem);
|
|
396
600
|
if (firstActiveIdx === -1) {
|
|
397
|
-
// All items are completed
|
|
398
601
|
return { flushed: items, remaining: [] };
|
|
399
602
|
}
|
|
400
603
|
if (firstActiveIdx === 0) {
|
|
@@ -405,6 +608,29 @@ function partitionCompleted(items) {
|
|
|
405
608
|
remaining: items.slice(firstActiveIdx),
|
|
406
609
|
};
|
|
407
610
|
}
|
|
611
|
+
function normalizeAssistantText(text) {
|
|
612
|
+
return stripDoneMarkers(text).trim();
|
|
613
|
+
}
|
|
614
|
+
function isSameAssistantText(item, text) {
|
|
615
|
+
return item.kind === "assistant" && normalizeAssistantText(item.text) === text;
|
|
616
|
+
}
|
|
617
|
+
export function pinStreamingTextBeforeToolBoundary({ items, visibleStreamingText, thinking, thinkingMs, makeId, }) {
|
|
618
|
+
const text = normalizeAssistantText(visibleStreamingText);
|
|
619
|
+
if (text.length === 0)
|
|
620
|
+
return items;
|
|
621
|
+
if (items.some((item) => item.kind === "assistant"))
|
|
622
|
+
return items;
|
|
623
|
+
return [
|
|
624
|
+
...items,
|
|
625
|
+
{
|
|
626
|
+
kind: "assistant",
|
|
627
|
+
text,
|
|
628
|
+
thinking: thinking.length > 0 ? thinking : undefined,
|
|
629
|
+
thinkingMs: thinking.length > 0 ? thinkingMs : undefined,
|
|
630
|
+
id: makeId(),
|
|
631
|
+
},
|
|
632
|
+
];
|
|
633
|
+
}
|
|
408
634
|
// ── Duration summary ─────────────────────────────────────
|
|
409
635
|
function formatDuration(ms) {
|
|
410
636
|
const totalSec = Math.round(ms / 1000);
|
|
@@ -452,8 +678,8 @@ function pickDurationVerb(toolsUsed) {
|
|
|
452
678
|
return "Ran & investigated for";
|
|
453
679
|
if (has("bash"))
|
|
454
680
|
return "Executed commands for";
|
|
455
|
-
if (hasAny("
|
|
456
|
-
return "Managed
|
|
681
|
+
if (hasAny("task-output", "task-stop"))
|
|
682
|
+
return "Managed background processes for";
|
|
457
683
|
if (has("grep") && has("read"))
|
|
458
684
|
return "Investigated for";
|
|
459
685
|
if (has("grep") && has("find"))
|
|
@@ -481,65 +707,16 @@ function pickDurationVerb(toolsUsed) {
|
|
|
481
707
|
];
|
|
482
708
|
return phrases[Math.floor(Math.random() * phrases.length)];
|
|
483
709
|
}
|
|
484
|
-
// ── Animated thinking border ────────────────────────────────
|
|
485
|
-
const THINKING_BORDER_COLORS = ["#60a5fa", "#818cf8", "#a78bfa", "#818cf8", "#60a5fa"];
|
|
486
|
-
// ── Task count helper ───────────────────────────────────────
|
|
487
|
-
function getTaskCount(cwd) {
|
|
488
|
-
try {
|
|
489
|
-
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
490
|
-
const data = readFileSync(join(homedir(), ".gg-tasks", "projects", hash, "tasks.json"), "utf-8");
|
|
491
|
-
const tasks = JSON.parse(data);
|
|
492
|
-
return tasks.filter((t) => t.status !== "done").length;
|
|
493
|
-
}
|
|
494
|
-
catch {
|
|
495
|
-
return 0;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
function getNextPendingTask(cwd) {
|
|
499
|
-
try {
|
|
500
|
-
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
501
|
-
const data = readFileSync(join(homedir(), ".gg-tasks", "projects", hash, "tasks.json"), "utf-8");
|
|
502
|
-
const tasks = JSON.parse(data);
|
|
503
|
-
const pending = tasks.find((t) => t.status === "pending");
|
|
504
|
-
if (!pending)
|
|
505
|
-
return null;
|
|
506
|
-
return {
|
|
507
|
-
id: pending.id,
|
|
508
|
-
title: pending.title,
|
|
509
|
-
prompt: pending.prompt || pending.text || pending.title,
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
catch {
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
function markTaskInProgress(cwd, taskId) {
|
|
517
|
-
try {
|
|
518
|
-
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
519
|
-
const filePath = join(homedir(), ".gg-tasks", "projects", hash, "tasks.json");
|
|
520
|
-
const data = readFileSync(filePath, "utf-8");
|
|
521
|
-
const tasks = JSON.parse(data);
|
|
522
|
-
const updated = tasks.map((t) => (t.id === taskId ? { ...t, status: "in-progress" } : t));
|
|
523
|
-
writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
|
|
524
|
-
}
|
|
525
|
-
catch {
|
|
526
|
-
// ignore
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
710
|
// ── App Component ──────────────────────────────────────────
|
|
530
711
|
export function App(props) {
|
|
531
712
|
const theme = useTheme();
|
|
532
713
|
const switchTheme = useSetTheme();
|
|
533
|
-
const {
|
|
714
|
+
const { write: writeStdout } = useStdout();
|
|
715
|
+
const { columns, rows } = useTerminalSize();
|
|
534
716
|
// Hoisted before terminal title hook so it can reference them
|
|
535
717
|
const [lastUserMessage, setLastUserMessage] = useState("");
|
|
536
718
|
const [exitPending, setExitPending] = useState(false);
|
|
537
|
-
|
|
538
|
-
// mode survives /clear's unmount/remount, matching the prior behavior
|
|
539
|
-
// where /clear didn't toggle plan mode off.
|
|
540
|
-
const [planMode, setPlanMode] = useState(props.planModeRef?.current ?? false);
|
|
541
|
-
const planModeLocalRef = useRef(false);
|
|
542
|
-
planModeLocalRef.current = planMode;
|
|
719
|
+
const [goalMode, setGoalMode] = useState(props.sessionStore?.goalMode ?? props.goalModeRef?.current ?? "off");
|
|
543
720
|
// Terminal title — updated later after agentLoop is created
|
|
544
721
|
// (hoisted here so the hook is always called in the same order)
|
|
545
722
|
const [titleRunning, setTitleRunning] = useState(false);
|
|
@@ -549,14 +726,11 @@ export function App(props) {
|
|
|
549
726
|
isRunning: titleRunning,
|
|
550
727
|
sessionTitle,
|
|
551
728
|
});
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
// so slice(0) returns the full array regardless of length.
|
|
729
|
+
// Completed transcript rows are kept as durable session data but are no longer
|
|
730
|
+
// rendered through Ink history. They are serialized once into real terminal
|
|
731
|
+
// scrollback via terminalHistoryPrinter, while Ink owns only live rows and
|
|
732
|
+
// controls. This avoids Static/log-update replay drift on resize/remount.
|
|
557
733
|
const [history, setHistory] = useState(() => {
|
|
558
|
-
// sessionStore wins (lives across remount). Falls back to initialHistory
|
|
559
|
-
// (loaded from a session file at startup), then a fresh banner-only list.
|
|
560
734
|
const stored = props.sessionStore?.history;
|
|
561
735
|
if (stored && stored.length > 0)
|
|
562
736
|
return stored;
|
|
@@ -567,24 +741,19 @@ export function App(props) {
|
|
|
567
741
|
});
|
|
568
742
|
// Items from the current/last turn — rendered in the live area so they stay visible.
|
|
569
743
|
// Seed from sessionStore so Goal progress/completion rows and other live output
|
|
570
|
-
// survive pane/overlay/resize remounts before they are
|
|
744
|
+
// survive pane/overlay/resize remounts before they are finalized.
|
|
571
745
|
const [liveItems, setLiveItems] = useState(() => props.sessionStore?.liveItems ?? []);
|
|
572
746
|
// overlay seeded from sessionStore (lives across remount). Falls back to
|
|
573
747
|
// props.initialOverlay (CLI launched with one), then null.
|
|
574
748
|
const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
|
|
575
|
-
const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
|
|
576
|
-
const [goalCount, setGoalCount] = useState(0);
|
|
577
749
|
const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
|
|
578
|
-
const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
579
750
|
const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
|
|
580
|
-
// Seed from sessionStore so "Run All" chaining survives the resetUI()
|
|
581
|
-
// remount that startTask() triggers between tasks.
|
|
582
|
-
const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
|
|
583
|
-
const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
|
|
584
|
-
const startTaskRef = useRef(() => { });
|
|
585
751
|
const agentRunningRef = useRef(false);
|
|
586
752
|
const runningGoalIdsRef = useRef(new Set());
|
|
587
753
|
const activeVerifierRunIdsRef = useRef(new Set());
|
|
754
|
+
const queuedGoalSyntheticEventsRef = useRef(0);
|
|
755
|
+
const goalContinuationFlightsRef = useRef(new Set());
|
|
756
|
+
const goalContinuationRecentChoicesRef = useRef(new Map());
|
|
588
757
|
const startGoalRunRef = useRef(() => { });
|
|
589
758
|
const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
|
|
590
759
|
const currentPixelFixRef = useRef(null);
|
|
@@ -594,12 +763,14 @@ export function App(props) {
|
|
|
594
763
|
const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
|
|
595
764
|
// Suppress "done" status when a plan overlay is about to open
|
|
596
765
|
const planOverlayPendingRef = useRef(false);
|
|
766
|
+
const goalSetupPanePendingRef = useRef(false);
|
|
597
767
|
const [gitBranch, setGitBranch] = useState(null);
|
|
598
768
|
const [currentModel, setCurrentModel] = useState(props.model);
|
|
599
769
|
const [currentProvider, setCurrentProvider] = useState(props.provider);
|
|
600
770
|
const [currentTools, setCurrentTools] = useState(props.tools);
|
|
601
771
|
const currentToolsRef = useRef(props.tools);
|
|
602
772
|
const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
|
|
773
|
+
const [renderMarkdown, setRenderMarkdown] = useState(true);
|
|
603
774
|
const messagesRef = useRef(props.sessionStore?.messages ?? props.messages);
|
|
604
775
|
const repoMapInjectionEnabledRef = useRef(true);
|
|
605
776
|
const repoMapDirtyRef = useRef(true);
|
|
@@ -608,10 +779,12 @@ export function App(props) {
|
|
|
608
779
|
const repoMapChangedCountRef = useRef(0);
|
|
609
780
|
const repoMapCacheRef = useRef(createRepoMapCache());
|
|
610
781
|
const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
|
|
782
|
+
const [goalAutoExpand, setGoalAutoExpand] = useState(props.sessionStore?.goalAutoExpand ?? false);
|
|
783
|
+
const goalAutoExpandRef = useRef(props.sessionStore?.goalAutoExpand ?? false);
|
|
611
784
|
const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
|
|
612
785
|
const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
|
|
613
786
|
const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
|
|
614
|
-
const
|
|
787
|
+
const goalModeStateRef = useRef(goalMode);
|
|
615
788
|
// Stuck-guard for the plan-continuation follow-up nudge. Tracks how many
|
|
616
789
|
// times we've nudged the agent to continue the same step. Reset whenever a
|
|
617
790
|
// new [DONE:n] marker advances progress (see onTurnText). Caps at 2 nudges
|
|
@@ -619,23 +792,14 @@ export function App(props) {
|
|
|
619
792
|
const followUpNudgesRef = useRef({ step: 0, count: 0 });
|
|
620
793
|
// Seed the per-item ID counter so it doesn't collide with IDs already in
|
|
621
794
|
// sessionStore.history (which survives remount). Without this, a remount
|
|
622
|
-
// (resize, overlay toggle, etc.) starts the counter at 0
|
|
623
|
-
// generate ids "0", "1", "2"… that collide with
|
|
624
|
-
// previous mount, triggering React's duplicate-key
|
|
625
|
-
// duplicate/omitted renders.
|
|
626
|
-
const nextIdRef = useRef((
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
];
|
|
631
|
-
let max = -1;
|
|
632
|
-
for (const item of items) {
|
|
633
|
-
const n = Number(item.id);
|
|
634
|
-
if (Number.isFinite(n) && n > max)
|
|
635
|
-
max = n;
|
|
636
|
-
}
|
|
637
|
-
return max + 1;
|
|
638
|
-
})());
|
|
795
|
+
// (resize, overlay toggle, goal pane open, etc.) starts the counter at 0
|
|
796
|
+
// and new items generate ids "ui-0", "ui-1", "ui-2"… that collide with
|
|
797
|
+
// the same ids from the previous mount, triggering React's duplicate-key
|
|
798
|
+
// warning and causing duplicate/omitted renders.
|
|
799
|
+
const nextIdRef = useRef(getNextGeneratedItemId([
|
|
800
|
+
...(props.sessionStore?.history ?? props.initialHistory ?? []),
|
|
801
|
+
...(props.sessionStore?.liveItems ?? []),
|
|
802
|
+
]));
|
|
639
803
|
const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
|
|
640
804
|
const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
|
|
641
805
|
const persistedIndexRef = useRef(messagesRef.current.length);
|
|
@@ -666,8 +830,11 @@ export function App(props) {
|
|
|
666
830
|
*/
|
|
667
831
|
const triggerAutoSetupRef = useRef(async () => { });
|
|
668
832
|
const getId = () => `ui-${nextIdRef.current++}`;
|
|
833
|
+
const appendGoalAgentTransition = useCallback((text) => {
|
|
834
|
+
setLiveItems((prev) => [...prev, { kind: "goal_agent_transition", text, id: getId() }]);
|
|
835
|
+
}, []);
|
|
669
836
|
const appendGoalProgress = useCallback((item) => {
|
|
670
|
-
setLiveItems((prev) =>
|
|
837
|
+
setLiveItems((prev) => appendGoalProgressDraft(prev, item, getId));
|
|
671
838
|
}, []);
|
|
672
839
|
const goalNumberForRun = useCallback((runId) => Math.max(1, goalStatusEntries.findIndex((entry) => entry.runId === runId) + 1), [goalStatusEntries]);
|
|
673
840
|
const clearGoalStatusEntry = useCallback((runId) => {
|
|
@@ -686,21 +853,59 @@ export function App(props) {
|
|
|
686
853
|
return next;
|
|
687
854
|
});
|
|
688
855
|
}, [props.sessionStore]);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
856
|
+
const sessionStore = props.sessionStore;
|
|
857
|
+
const terminalHistoryContextRef = useRef({
|
|
858
|
+
theme,
|
|
859
|
+
columns,
|
|
860
|
+
version: props.version,
|
|
861
|
+
model: currentModel,
|
|
862
|
+
provider: currentProvider,
|
|
863
|
+
cwd: displayedCwd,
|
|
864
|
+
});
|
|
865
|
+
useEffect(() => {
|
|
866
|
+
terminalHistoryContextRef.current = {
|
|
867
|
+
theme,
|
|
868
|
+
columns,
|
|
869
|
+
version: props.version,
|
|
870
|
+
model: currentModel,
|
|
871
|
+
provider: currentProvider,
|
|
872
|
+
cwd: displayedCwd,
|
|
873
|
+
};
|
|
874
|
+
}, [theme, columns, props.version, currentModel, currentProvider, displayedCwd]);
|
|
875
|
+
const printHistoryItems = useCallback((items, options) => {
|
|
876
|
+
if (!props.terminalHistoryPrinter || items.length === 0)
|
|
877
|
+
return;
|
|
878
|
+
props.terminalHistoryPrinter.print(items, terminalHistoryContextRef.current, {
|
|
879
|
+
...options,
|
|
880
|
+
write: writeStdout,
|
|
881
|
+
});
|
|
882
|
+
}, [props.terminalHistoryPrinter, writeStdout]);
|
|
883
|
+
const pendingHistoryFlushRef = useRef([]);
|
|
884
|
+
const streamedAssistantFlushRef = useRef({
|
|
885
|
+
flushedChars: 0,
|
|
886
|
+
text: "",
|
|
887
|
+
});
|
|
888
|
+
const [historyFlushGeneration, setHistoryFlushGeneration] = useState(0);
|
|
694
889
|
const queueFlush = useCallback((items) => {
|
|
695
|
-
|
|
890
|
+
const flushed = trimFlushedItems(items);
|
|
891
|
+
if (flushed.length === 0)
|
|
696
892
|
return;
|
|
697
|
-
|
|
698
|
-
if (
|
|
893
|
+
pendingHistoryFlushRef.current = [...pendingHistoryFlushRef.current, ...flushed];
|
|
894
|
+
if (sessionStore) {
|
|
699
895
|
const queuedIds = new Set(items.map((item) => item.id));
|
|
700
|
-
|
|
896
|
+
sessionStore.liveItems = (sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
|
|
701
897
|
}
|
|
702
|
-
|
|
703
|
-
}, [
|
|
898
|
+
setHistoryFlushGeneration((generation) => generation + 1);
|
|
899
|
+
}, [sessionStore]);
|
|
900
|
+
const finalizeSubmittedUserItem = useCallback((item) => {
|
|
901
|
+
streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
|
|
902
|
+
setLiveItems((prev) => {
|
|
903
|
+
if (prev.length > 0)
|
|
904
|
+
queueFlush(prev);
|
|
905
|
+
queueFlush([item]);
|
|
906
|
+
return [];
|
|
907
|
+
});
|
|
908
|
+
}, [queueFlush]);
|
|
704
909
|
// Mirror runtime state choices (model/provider/thinking) into renderApp's
|
|
705
910
|
// closure so unmount/remount preserves them.
|
|
706
911
|
const onRuntimeStateChange = props.onRuntimeStateChange;
|
|
@@ -715,12 +920,35 @@ export function App(props) {
|
|
|
715
920
|
thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
|
|
716
921
|
});
|
|
717
922
|
}, [thinkingEnabled, currentModel, onRuntimeStateChange]);
|
|
923
|
+
useEffect(() => {
|
|
924
|
+
printHistoryItems(history);
|
|
925
|
+
}, [history, printHistoryItems]);
|
|
926
|
+
useEffect(() => {
|
|
927
|
+
const flushed = pendingHistoryFlushRef.current;
|
|
928
|
+
if (flushed.length === 0)
|
|
929
|
+
return;
|
|
930
|
+
pendingHistoryFlushRef.current = [];
|
|
931
|
+
printHistoryItems(flushed);
|
|
932
|
+
const flushedIds = new Set(flushed.map((item) => item.id));
|
|
933
|
+
setLiveItems((prev) => prev.filter((item) => !flushedIds.has(item.id)));
|
|
934
|
+
setHistory((prev) => {
|
|
935
|
+
const existingIds = new Set(prev.map((item) => item.id));
|
|
936
|
+
const nextItems = flushed.filter((item) => !existingIds.has(item.id));
|
|
937
|
+
if (nextItems.length === 0)
|
|
938
|
+
return prev;
|
|
939
|
+
const next = compactHistory([...prev, ...nextItems]);
|
|
940
|
+
if (sessionStore)
|
|
941
|
+
sessionStore.history = next;
|
|
942
|
+
return next;
|
|
943
|
+
});
|
|
944
|
+
}, [historyFlushGeneration, printHistoryItems, sessionStore]);
|
|
718
945
|
// Mirror session state into renderApp's closure so resetUI() can re-seed
|
|
719
946
|
// the conversation on remount. Each panel that previously did a bare ANSI
|
|
720
|
-
// screen clear (overlay open/close, plan accept/reject, /clear
|
|
947
|
+
// screen clear (overlay open/close, plan accept/reject, /clear)
|
|
721
948
|
// now goes through resetUI; without these mirrors, the chat would vanish.
|
|
722
|
-
const
|
|
949
|
+
const historyRef = useRef(history);
|
|
723
950
|
useEffect(() => {
|
|
951
|
+
historyRef.current = history;
|
|
724
952
|
if (sessionStore)
|
|
725
953
|
sessionStore.history = history;
|
|
726
954
|
}, [history, sessionStore]);
|
|
@@ -744,10 +972,19 @@ export function App(props) {
|
|
|
744
972
|
if (sessionStore)
|
|
745
973
|
sessionStore.overlay = overlay;
|
|
746
974
|
}, [overlay, sessionStore]);
|
|
975
|
+
useEffect(() => {
|
|
976
|
+
goalAutoExpandRef.current = goalAutoExpand;
|
|
977
|
+
if (sessionStore)
|
|
978
|
+
sessionStore.goalAutoExpand = goalAutoExpand;
|
|
979
|
+
}, [goalAutoExpand, sessionStore]);
|
|
747
980
|
useEffect(() => {
|
|
748
981
|
if (sessionStore)
|
|
749
982
|
sessionStore.goalStatusEntries = goalStatusEntries;
|
|
750
983
|
}, [goalStatusEntries, sessionStore]);
|
|
984
|
+
useEffect(() => {
|
|
985
|
+
if (sessionStore)
|
|
986
|
+
sessionStore.goalMode = goalMode;
|
|
987
|
+
}, [goalMode, sessionStore]);
|
|
751
988
|
// pendingAction is consumed via a useEffect AFTER agentLoop is created
|
|
752
989
|
// — see below where useAgentLoop is set up.
|
|
753
990
|
const pendingActionConsumedRef = useRef(false);
|
|
@@ -769,10 +1006,8 @@ export function App(props) {
|
|
|
769
1006
|
void reconcileActiveGoalRuns(props.cwd, {
|
|
770
1007
|
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
771
1008
|
}).then(({ runs }) => {
|
|
772
|
-
const counts = summarizeGoalCountsFromRuns(runs);
|
|
773
1009
|
if (cancelled)
|
|
774
1010
|
return;
|
|
775
|
-
setGoalCount(counts.active);
|
|
776
1011
|
setHistory((prev) => completedItemsWithDurableGoalTerminalProgress(prev, runs));
|
|
777
1012
|
setGoalStatusEntries((prev) => {
|
|
778
1013
|
const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
|
|
@@ -813,19 +1048,23 @@ export function App(props) {
|
|
|
813
1048
|
useEffect(() => {
|
|
814
1049
|
currentToolsRef.current = currentTools;
|
|
815
1050
|
}, [currentTools]);
|
|
816
|
-
// ──
|
|
817
|
-
// Sync
|
|
1051
|
+
// ── Runtime mode wiring ──────────────────────────────────
|
|
1052
|
+
// Sync runtime mode refs with React state.
|
|
818
1053
|
useEffect(() => {
|
|
819
|
-
|
|
820
|
-
if (props.
|
|
821
|
-
props.
|
|
822
|
-
}
|
|
823
|
-
}, [
|
|
1054
|
+
goalModeStateRef.current = goalMode;
|
|
1055
|
+
if (props.goalModeRef) {
|
|
1056
|
+
props.goalModeRef.current = goalMode;
|
|
1057
|
+
}
|
|
1058
|
+
}, [goalMode, props.goalModeRef]);
|
|
1059
|
+
const setActiveGoalReferences = useCallback((references) => {
|
|
1060
|
+
if (props.goalReferencesRef)
|
|
1061
|
+
props.goalReferencesRef.current = references;
|
|
1062
|
+
}, [props.goalReferencesRef]);
|
|
824
1063
|
const rebuildSystemPrompt = useCallback(async (options) => {
|
|
825
1064
|
const approvedPlanPath = options?.clearApprovedPlan
|
|
826
1065
|
? undefined
|
|
827
1066
|
: (options?.approvedPlanPath ?? approvedPlanPathRef.current);
|
|
828
|
-
return buildSystemPrompt(options?.cwd ?? cwdRef.current, props.skills,
|
|
1067
|
+
return buildSystemPrompt(options?.cwd ?? cwdRef.current, props.skills, false, approvedPlanPath, (options?.tools ?? currentToolsRef.current).map((tool) => tool.name), options?.activeLanguages ?? injectedLanguagesRef.current, options?.goalMode ?? goalModeStateRef.current);
|
|
829
1068
|
}, [props.skills]);
|
|
830
1069
|
const replaceSystemPrompt = useCallback(async (options) => {
|
|
831
1070
|
const newPrompt = await rebuildSystemPrompt(options);
|
|
@@ -834,6 +1073,28 @@ export function App(props) {
|
|
|
834
1073
|
}
|
|
835
1074
|
return newPrompt;
|
|
836
1075
|
}, [rebuildSystemPrompt]);
|
|
1076
|
+
const setGoalModeAndPrompt = useCallback(async (nextMode, options) => {
|
|
1077
|
+
goalModeStateRef.current = nextMode;
|
|
1078
|
+
if (props.goalModeRef)
|
|
1079
|
+
props.goalModeRef.current = nextMode;
|
|
1080
|
+
if (props.sessionStore)
|
|
1081
|
+
props.sessionStore.goalMode = nextMode;
|
|
1082
|
+
setGoalMode(nextMode);
|
|
1083
|
+
await replaceSystemPrompt({ ...options, goalMode: nextMode });
|
|
1084
|
+
}, [props.goalModeRef, props.sessionStore, replaceSystemPrompt]);
|
|
1085
|
+
const clearGoalModeIfIdle = useCallback(() => {
|
|
1086
|
+
setTimeout(() => {
|
|
1087
|
+
if (goalModeStateRef.current === "off")
|
|
1088
|
+
return;
|
|
1089
|
+
if (runningGoalIdsRef.current.size > 0)
|
|
1090
|
+
return;
|
|
1091
|
+
if (activeVerifierRunIdsRef.current.size > 0)
|
|
1092
|
+
return;
|
|
1093
|
+
if (queuedGoalSyntheticEventsRef.current > 0)
|
|
1094
|
+
return;
|
|
1095
|
+
void setGoalModeAndPrompt("off");
|
|
1096
|
+
}, 0);
|
|
1097
|
+
}, [setGoalModeAndPrompt]);
|
|
837
1098
|
/**
|
|
838
1099
|
* Unified "apply detection result" pipeline. Called from three sites:
|
|
839
1100
|
* 1. Initial mount (existing project at startup).
|
|
@@ -918,53 +1179,6 @@ export function App(props) {
|
|
|
918
1179
|
useEffect(() => {
|
|
919
1180
|
void applyLanguageDetectionRef.current("initial");
|
|
920
1181
|
}, []);
|
|
921
|
-
// Rebuild system prompt when plan mode changes
|
|
922
|
-
useEffect(() => {
|
|
923
|
-
void replaceSystemPrompt({ planMode });
|
|
924
|
-
}, [planMode, replaceSystemPrompt]);
|
|
925
|
-
// Wire onEnterPlan callback ref
|
|
926
|
-
useEffect(() => {
|
|
927
|
-
if (props.onEnterPlanRef) {
|
|
928
|
-
props.onEnterPlanRef.current = (reason) => {
|
|
929
|
-
setPlanMode(true);
|
|
930
|
-
const msg = reason ? `Plan Mode Activated — ${reason}` : "Plan Mode Activated";
|
|
931
|
-
setLiveItems((prev) => [
|
|
932
|
-
...prev,
|
|
933
|
-
{ kind: "plan_transition", text: msg, active: true, id: getId() },
|
|
934
|
-
]);
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
}, [props.onEnterPlanRef]);
|
|
938
|
-
// Wire onExitPlan callback ref
|
|
939
|
-
useEffect(() => {
|
|
940
|
-
if (props.onExitPlanRef) {
|
|
941
|
-
props.onExitPlanRef.current = async (planPath) => {
|
|
942
|
-
// Deactivate plan mode, store approved plan path, open pane
|
|
943
|
-
planModeStateRef.current = false;
|
|
944
|
-
setPlanMode(false);
|
|
945
|
-
approvedPlanPathRef.current = planPath;
|
|
946
|
-
await replaceSystemPrompt({ planMode: false, approvedPlanPath: planPath });
|
|
947
|
-
// Use setTimeout to open pane after the current tool execution completes,
|
|
948
|
-
// so the turn can finish and the UI transitions cleanly
|
|
949
|
-
// Flag that the plan overlay is about to open — suppresses the
|
|
950
|
-
// premature "done" status that fires when the agent loop finishes
|
|
951
|
-
planOverlayPendingRef.current = true;
|
|
952
|
-
setTimeout(() => {
|
|
953
|
-
setPlanAutoExpand(true);
|
|
954
|
-
setOverlay("plan");
|
|
955
|
-
// Don't clear planOverlayPendingRef here — keep it true until
|
|
956
|
-
// the user actually approves/rejects the plan. Clearing it on a
|
|
957
|
-
// timer causes a race where agent_done fires after the 300ms
|
|
958
|
-
// timeout but before the user interacts, triggering a premature
|
|
959
|
-
// completion sound.
|
|
960
|
-
}, 300);
|
|
961
|
-
return ("Plan submitted. Exiting plan mode.\n" +
|
|
962
|
-
"The plan pane is opening for user review.\n" +
|
|
963
|
-
"Plan saved at: " +
|
|
964
|
-
planPath);
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
}, [props.onExitPlanRef, replaceSystemPrompt]);
|
|
968
1182
|
const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
|
|
969
1183
|
const sm = sessionManagerRef.current;
|
|
970
1184
|
if (!sm)
|
|
@@ -1011,7 +1225,7 @@ export function App(props) {
|
|
|
1011
1225
|
* Other tool kinds skip detection entirely to avoid wasted filesystem stats.
|
|
1012
1226
|
*
|
|
1013
1227
|
* No restart required: the system prompt is mutated in place, same mechanism
|
|
1014
|
-
*
|
|
1228
|
+
* used for pixel-fix chdir.
|
|
1015
1229
|
*
|
|
1016
1230
|
* Stored in a ref so `onToolEnd` (whose useCallback dep array is intentionally
|
|
1017
1231
|
* empty to keep agent-loop options stable) can call the freshest version.
|
|
@@ -1312,7 +1526,6 @@ export function App(props) {
|
|
|
1312
1526
|
}, [
|
|
1313
1527
|
persistNewMessages,
|
|
1314
1528
|
stripRepoMapMessages,
|
|
1315
|
-
planMode,
|
|
1316
1529
|
props.cwd,
|
|
1317
1530
|
props.skills,
|
|
1318
1531
|
currentProvider,
|
|
@@ -1322,6 +1535,11 @@ export function App(props) {
|
|
|
1322
1535
|
resolveCredentials,
|
|
1323
1536
|
]),
|
|
1324
1537
|
onTurnText: useCallback((text, thinking, thinkingMs) => {
|
|
1538
|
+
if (goalModeStateRef.current === "planner") {
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const hadStreamedAssistantFlush = streamedAssistantFlushRef.current.flushedChars > 0;
|
|
1542
|
+
const unflushedAssistantText = text.slice(streamedAssistantFlushRef.current.flushedChars);
|
|
1325
1543
|
// Track [DONE:n] markers for plan step progress
|
|
1326
1544
|
if (planStepsRef.current.length > 0) {
|
|
1327
1545
|
const completed = findCompletedMarkers(text);
|
|
@@ -1336,15 +1554,9 @@ export function App(props) {
|
|
|
1336
1554
|
followUpNudgesRef.current = { step: 0, count: 0 };
|
|
1337
1555
|
}
|
|
1338
1556
|
}
|
|
1339
|
-
// Flush
|
|
1340
|
-
//
|
|
1341
|
-
//
|
|
1342
|
-
//
|
|
1343
|
-
// Items are queued in pendingFlushRef (not sent to setHistory directly)
|
|
1344
|
-
// so the Static write happens in a SEPARATE render cycle from the
|
|
1345
|
-
// live-area change — avoiding both Ink cursor-math clipping and the
|
|
1346
|
-
// brief duplicate that occurred when setHistory was nested inside the
|
|
1347
|
-
// setLiveItems updater.
|
|
1557
|
+
// Flush completed rows from the previous turn to finalized terminal
|
|
1558
|
+
// history. Ink keeps only the active turn, preventing live-area growth
|
|
1559
|
+
// and avoiding Static/log-update replay during resize/remount churn.
|
|
1348
1560
|
setLiveItems((prev) => {
|
|
1349
1561
|
const flushed = flushOnTurnText(prev);
|
|
1350
1562
|
if (flushed.length > 0) {
|
|
@@ -1353,10 +1565,7 @@ export function App(props) {
|
|
|
1353
1565
|
// Split text on [DONE:N] markers so each marker renders inline as
|
|
1354
1566
|
// a styled "✓ Step N: <description>" item at the position the
|
|
1355
1567
|
// agent emitted it, instead of vanishing into stripped whitespace.
|
|
1356
|
-
|
|
1357
|
-
// marker-stripped text when there are no markers (keeps the
|
|
1358
|
-
// common case zero-cost).
|
|
1359
|
-
const segments = segmentDisplayText(text, planStepsRef.current);
|
|
1568
|
+
const segments = segmentDisplayText(unflushedAssistantText, planStepsRef.current);
|
|
1360
1569
|
const items = [];
|
|
1361
1570
|
let thinkingAttached = false;
|
|
1362
1571
|
for (const seg of segments) {
|
|
@@ -1369,7 +1578,7 @@ export function App(props) {
|
|
|
1369
1578
|
// contains multiple text chunks split by markers.
|
|
1370
1579
|
thinking: thinkingAttached ? undefined : thinking,
|
|
1371
1580
|
thinkingMs: thinkingAttached ? undefined : thinkingMs,
|
|
1372
|
-
|
|
1581
|
+
continuation: hadStreamedAssistantFlush,
|
|
1373
1582
|
id: getId(),
|
|
1374
1583
|
});
|
|
1375
1584
|
thinkingAttached = true;
|
|
@@ -1384,36 +1593,51 @@ export function App(props) {
|
|
|
1384
1593
|
}
|
|
1385
1594
|
}
|
|
1386
1595
|
// No segments at all (text was empty/whitespace, no markers).
|
|
1387
|
-
// Still
|
|
1388
|
-
// there was thinking content for this turn.
|
|
1596
|
+
// Still persist an assistant item so a thinking block renders in
|
|
1597
|
+
// terminal history if there was thinking content for this turn.
|
|
1389
1598
|
if (items.length === 0) {
|
|
1390
1599
|
items.push({
|
|
1391
1600
|
kind: "assistant",
|
|
1392
1601
|
text: "",
|
|
1393
1602
|
thinking,
|
|
1394
1603
|
thinkingMs,
|
|
1395
|
-
planMode: planModeLocalRef.current,
|
|
1396
1604
|
id: getId(),
|
|
1397
1605
|
});
|
|
1398
1606
|
}
|
|
1399
|
-
|
|
1607
|
+
const assistantItems = prev.filter((item) => item.kind === "assistant");
|
|
1608
|
+
const newAssistantText = normalizeAssistantText(unflushedAssistantText);
|
|
1609
|
+
const duplicatePinnedText = newAssistantText.length > 0 &&
|
|
1610
|
+
[...assistantItems, ...pendingHistoryFlushRef.current, ...historyRef.current].some((item) => isSameAssistantText(item, newAssistantText));
|
|
1611
|
+
const nextItems = duplicatePinnedText
|
|
1612
|
+
? items.filter((item) => !isSameAssistantText(item, newAssistantText))
|
|
1613
|
+
: items;
|
|
1614
|
+
const flushablePrev = prev.filter((item) => item.kind !== "assistant");
|
|
1615
|
+
if (flushablePrev.length > 0)
|
|
1616
|
+
queueFlush(flushablePrev);
|
|
1617
|
+
streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
|
|
1618
|
+
return [...assistantItems, ...nextItems];
|
|
1400
1619
|
});
|
|
1401
|
-
}, []),
|
|
1402
|
-
onToolStart: useCallback((toolCallId, name, args) => {
|
|
1620
|
+
}, [queueFlush]),
|
|
1621
|
+
onToolStart: useCallback((toolCallId, name, args, stream) => {
|
|
1403
1622
|
log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
|
|
1404
1623
|
const startedAt = Date.now();
|
|
1405
1624
|
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1625
|
+
const appendToolStart = (prev) => {
|
|
1626
|
+
const visible = pinStreamingTextBeforeToolBoundary({
|
|
1627
|
+
items: prev,
|
|
1628
|
+
visibleStreamingText: stream.text,
|
|
1629
|
+
thinking: stream.thinking,
|
|
1630
|
+
thinkingMs: stream.thinkingMs,
|
|
1631
|
+
makeId: getId,
|
|
1632
|
+
});
|
|
1633
|
+
const { flushed, remaining } = partitionCompleted(visible);
|
|
1411
1634
|
if (flushed.length > 0) {
|
|
1412
1635
|
queueFlush(flushed);
|
|
1413
1636
|
}
|
|
1414
1637
|
return remaining;
|
|
1415
|
-
}
|
|
1638
|
+
};
|
|
1416
1639
|
if (name === "subagent") {
|
|
1640
|
+
setLiveItems(appendToolStart);
|
|
1417
1641
|
// Create or update the sub-agent group item
|
|
1418
1642
|
const newAgent = {
|
|
1419
1643
|
toolCallId,
|
|
@@ -1438,25 +1662,32 @@ export function App(props) {
|
|
|
1438
1662
|
});
|
|
1439
1663
|
}
|
|
1440
1664
|
else if (AGGREGATABLE_TOOLS.has(name)) {
|
|
1441
|
-
// Group concurrent read-only tools into a single compact item
|
|
1442
1665
|
setLiveItems((prev) => {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
if (
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1666
|
+
const reusableGroupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
|
|
1667
|
+
item.tools.every((tool) => tool.name === name && !tool.isError));
|
|
1668
|
+
const prior = reusableGroupIdx === -1 ? [] : prev.slice(0, reusableGroupIdx);
|
|
1669
|
+
if (reusableGroupIdx !== -1 && prior.every((item) => !isActiveItem(item))) {
|
|
1670
|
+
const flushablePrior = prior.filter((item) => item.kind !== "assistant");
|
|
1671
|
+
if (flushablePrior.length > 0)
|
|
1672
|
+
queueFlush(flushablePrior);
|
|
1673
|
+
const pinnedPrior = prior.filter((item) => item.kind === "assistant");
|
|
1674
|
+
const candidates = prev.slice(reusableGroupIdx);
|
|
1675
|
+
const group = candidates[0];
|
|
1676
|
+
return [
|
|
1677
|
+
...pinnedPrior,
|
|
1678
|
+
{
|
|
1679
|
+
...group,
|
|
1680
|
+
tools: [
|
|
1681
|
+
...group.tools,
|
|
1682
|
+
{ toolCallId, name, args, status: "running", animateUntil },
|
|
1683
|
+
],
|
|
1684
|
+
},
|
|
1685
|
+
...candidates.slice(1),
|
|
1686
|
+
];
|
|
1457
1687
|
}
|
|
1688
|
+
const remaining = appendToolStart(prev);
|
|
1458
1689
|
return [
|
|
1459
|
-
...
|
|
1690
|
+
...remaining,
|
|
1460
1691
|
{
|
|
1461
1692
|
kind: "tool_group",
|
|
1462
1693
|
tools: [{ toolCallId, name, args, status: "running", animateUntil }],
|
|
@@ -1467,11 +1698,11 @@ export function App(props) {
|
|
|
1467
1698
|
}
|
|
1468
1699
|
else {
|
|
1469
1700
|
setLiveItems((prev) => [
|
|
1470
|
-
...prev,
|
|
1701
|
+
...appendToolStart(prev),
|
|
1471
1702
|
{ kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
|
|
1472
1703
|
]);
|
|
1473
1704
|
}
|
|
1474
|
-
}, []),
|
|
1705
|
+
}, [queueFlush]),
|
|
1475
1706
|
onToolUpdate: useCallback((toolCallId, update) => {
|
|
1476
1707
|
const u = update;
|
|
1477
1708
|
// Bash progress streaming — append output to tool_start item
|
|
@@ -1544,7 +1775,7 @@ export function App(props) {
|
|
|
1544
1775
|
};
|
|
1545
1776
|
const next = [...prev];
|
|
1546
1777
|
next[groupIdx] = { ...group, agents: updatedAgents };
|
|
1547
|
-
// Flush completed items to
|
|
1778
|
+
// Flush completed items to finalized history to keep the live area small
|
|
1548
1779
|
const { flushed, remaining } = partitionCompleted(next);
|
|
1549
1780
|
if (flushed.length > 0) {
|
|
1550
1781
|
queueFlush(flushed);
|
|
@@ -1603,7 +1834,7 @@ export function App(props) {
|
|
|
1603
1834
|
];
|
|
1604
1835
|
}
|
|
1605
1836
|
}
|
|
1606
|
-
// Flush completed items to
|
|
1837
|
+
// Flush completed items to finalized history to keep the live area small
|
|
1607
1838
|
const { flushed, remaining } = partitionCompleted(updated);
|
|
1608
1839
|
if (flushed.length > 0) {
|
|
1609
1840
|
queueFlush(flushed);
|
|
@@ -1619,14 +1850,21 @@ export function App(props) {
|
|
|
1619
1850
|
});
|
|
1620
1851
|
}
|
|
1621
1852
|
}, []),
|
|
1622
|
-
onServerToolCall: useCallback((id, name, input) => {
|
|
1853
|
+
onServerToolCall: useCallback((id, name, input, stream) => {
|
|
1623
1854
|
log("INFO", "server_tool", `Server tool call: ${name}`, { id });
|
|
1624
1855
|
const startedAt = Date.now();
|
|
1625
1856
|
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
1626
|
-
// Flush completed items (including assistant text)
|
|
1627
|
-
//
|
|
1857
|
+
// Flush completed items (including assistant text) before adding server
|
|
1858
|
+
// tool UI — same rationale as onToolStart.
|
|
1628
1859
|
setLiveItems((prev) => {
|
|
1629
|
-
const
|
|
1860
|
+
const visible = pinStreamingTextBeforeToolBoundary({
|
|
1861
|
+
items: prev,
|
|
1862
|
+
visibleStreamingText: stream.text,
|
|
1863
|
+
thinking: stream.thinking,
|
|
1864
|
+
thinkingMs: stream.thinkingMs,
|
|
1865
|
+
makeId: getId,
|
|
1866
|
+
});
|
|
1867
|
+
const { flushed, remaining } = partitionCompleted(visible);
|
|
1630
1868
|
if (flushed.length > 0) {
|
|
1631
1869
|
queueFlush(flushed);
|
|
1632
1870
|
}
|
|
@@ -1643,7 +1881,7 @@ export function App(props) {
|
|
|
1643
1881
|
},
|
|
1644
1882
|
];
|
|
1645
1883
|
});
|
|
1646
|
-
}, []),
|
|
1884
|
+
}, [queueFlush]),
|
|
1647
1885
|
onServerToolResult: useCallback((toolUseId, resultType, data) => {
|
|
1648
1886
|
log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
|
|
1649
1887
|
setLiveItems((prev) => {
|
|
@@ -1677,14 +1915,14 @@ export function App(props) {
|
|
|
1677
1915
|
},
|
|
1678
1916
|
];
|
|
1679
1917
|
}
|
|
1680
|
-
// Flush completed items to
|
|
1918
|
+
// Flush completed items to finalized history
|
|
1681
1919
|
const { flushed, remaining } = partitionCompleted(updated);
|
|
1682
1920
|
if (flushed.length > 0) {
|
|
1683
1921
|
queueFlush(flushed);
|
|
1684
1922
|
}
|
|
1685
1923
|
return remaining;
|
|
1686
1924
|
});
|
|
1687
|
-
}, []),
|
|
1925
|
+
}, [queueFlush]),
|
|
1688
1926
|
onTurnEnd: useCallback((turn, stopReason, usage) => {
|
|
1689
1927
|
log("INFO", "turn", `Turn ${turn} ended`, {
|
|
1690
1928
|
stopReason,
|
|
@@ -1700,8 +1938,8 @@ export function App(props) {
|
|
|
1700
1938
|
lastActualTokensRef.current =
|
|
1701
1939
|
currentProvider === "anthropic" ? inputContext : inputContext + usage.outputTokens;
|
|
1702
1940
|
lastActualTokensTimestampRef.current = Date.now();
|
|
1703
|
-
// For tool-only turns (no text), flush completed items to
|
|
1704
|
-
// liveItems doesn't grow unbounded across consecutive
|
|
1941
|
+
// For tool-only turns (no text), flush completed items to finalized
|
|
1942
|
+
// history so liveItems doesn't grow unbounded across consecutive turns.
|
|
1705
1943
|
setLiveItems((prev) => {
|
|
1706
1944
|
const { flushed, remaining } = flushOnTurnEnd(prev, stopReason);
|
|
1707
1945
|
if (flushed.length > 0) {
|
|
@@ -1709,44 +1947,42 @@ export function App(props) {
|
|
|
1709
1947
|
}
|
|
1710
1948
|
return remaining;
|
|
1711
1949
|
});
|
|
1712
|
-
}, []),
|
|
1950
|
+
}, [queueFlush]),
|
|
1713
1951
|
onDone: useCallback((durationMs, toolsUsed) => {
|
|
1714
1952
|
log("INFO", "agent", `Agent done`, {
|
|
1715
1953
|
duration: `${durationMs}ms`,
|
|
1716
1954
|
toolsUsed: toolsUsed.join(",") || "none",
|
|
1717
1955
|
});
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
|
|
1723
|
-
playNotificationSound();
|
|
1724
|
-
// Two-phase flush to avoid Ink text clipping.
|
|
1725
|
-
// Phase 1 (here): clear the live area so Ink commits a render with
|
|
1726
|
-
// the smaller output and updates its internal line counter.
|
|
1727
|
-
// Phase 2 (useEffect below): push items to Static history in a
|
|
1728
|
-
// separate render cycle so the Static write never coincides with
|
|
1729
|
-
// a live-area height change in the same frame.
|
|
1730
|
-
setLiveItems((prev) => {
|
|
1731
|
-
if (prev.length > 0)
|
|
1732
|
-
queueFlush(prev);
|
|
1733
|
-
return [];
|
|
1956
|
+
const doneDecision = getDoneFlushDecision({
|
|
1957
|
+
planOverlayPending: planOverlayPendingRef.current,
|
|
1958
|
+
goalMode: goalModeStateRef.current,
|
|
1959
|
+
goalAutoExpand: goalAutoExpandRef.current,
|
|
1734
1960
|
});
|
|
1735
|
-
//
|
|
1736
|
-
//
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1961
|
+
// Don't show "done" status when plan/goal review panes are about to open —
|
|
1962
|
+
// the agent loop finished but we're waiting for user approval/review.
|
|
1963
|
+
// Still flush live transcript rows before the pane remounts; otherwise
|
|
1964
|
+
// setup output remains in ephemeral liveItems and appears to vanish.
|
|
1965
|
+
if (doneDecision.showDoneStatus) {
|
|
1966
|
+
setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
|
|
1967
|
+
playNotificationSound();
|
|
1968
|
+
}
|
|
1969
|
+
// Finalize rows now; the sink writes them outside Ink and then the
|
|
1970
|
+
// live area is cleared, so there is no Static/live repaint race.
|
|
1971
|
+
if (doneDecision.flushLiveItems) {
|
|
1972
|
+
setLiveItems((prev) => {
|
|
1973
|
+
if (prev.length > 0)
|
|
1974
|
+
queueFlush(prev);
|
|
1975
|
+
return [];
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
const nextGoalMode = nextGoalModeAfterAgentDone({
|
|
1979
|
+
currentMode: goalModeStateRef.current,
|
|
1980
|
+
runningGoalIds: runningGoalIdsRef.current.size,
|
|
1981
|
+
queuedSyntheticEvents: queuedGoalSyntheticEventsRef.current,
|
|
1982
|
+
activeContinuationFlights: goalContinuationFlightsRef.current.size,
|
|
1983
|
+
});
|
|
1984
|
+
if (nextGoalMode !== goalModeStateRef.current) {
|
|
1985
|
+
void setGoalModeAndPrompt(nextGoalMode);
|
|
1750
1986
|
}
|
|
1751
1987
|
// Goal loop: after the orchestrator handles a worker/verifier event,
|
|
1752
1988
|
// continue the same Goal automatically until it reaches a terminal state.
|
|
@@ -1788,12 +2024,16 @@ export function App(props) {
|
|
|
1788
2024
|
}
|
|
1789
2025
|
})();
|
|
1790
2026
|
}
|
|
1791
|
-
}, []),
|
|
2027
|
+
}, [setGoalModeAndPrompt]),
|
|
1792
2028
|
onAborted: useCallback(() => {
|
|
1793
2029
|
log("WARN", "agent", "Agent run aborted by user");
|
|
1794
|
-
setRunAllTasks(false);
|
|
1795
2030
|
setRunAllPixel(false);
|
|
1796
2031
|
currentPixelFixRef.current = null;
|
|
2032
|
+
queuedGoalSyntheticEventsRef.current = 0;
|
|
2033
|
+
goalSetupPanePendingRef.current = false;
|
|
2034
|
+
setActiveGoalReferences(undefined);
|
|
2035
|
+
if (goalModeStateRef.current !== "off")
|
|
2036
|
+
void setGoalModeAndPrompt("off");
|
|
1797
2037
|
setDoneStatus(null);
|
|
1798
2038
|
setLiveItems((prev) => {
|
|
1799
2039
|
const next = prev.map((item) => {
|
|
@@ -1836,7 +2076,7 @@ export function App(props) {
|
|
|
1836
2076
|
});
|
|
1837
2077
|
return [...next, { kind: "stopped", text: "Request was stopped.", id: getId() }];
|
|
1838
2078
|
});
|
|
1839
|
-
}, []),
|
|
2079
|
+
}, [setActiveGoalReferences, setGoalModeAndPrompt]),
|
|
1840
2080
|
onQueuedStart: useCallback((content) => {
|
|
1841
2081
|
// When a queued message starts processing, show it as a UserItem
|
|
1842
2082
|
// and flush prior items to history. Synthetic system events are hidden
|
|
@@ -1848,6 +2088,8 @@ export function App(props) {
|
|
|
1848
2088
|
.map((c) => c.text)
|
|
1849
2089
|
.join("\n");
|
|
1850
2090
|
if (isGoalSyntheticEvent(displayText)) {
|
|
2091
|
+
queuedGoalSyntheticEventsRef.current = Math.max(0, queuedGoalSyntheticEventsRef.current - 1);
|
|
2092
|
+
void setGoalModeAndPrompt("coordinator");
|
|
1851
2093
|
const eventInfo = parseGoalSyntheticEvent(displayText);
|
|
1852
2094
|
setLiveItems((prev) => {
|
|
1853
2095
|
if (prev.length > 0)
|
|
@@ -1870,11 +2112,6 @@ export function App(props) {
|
|
|
1870
2112
|
const imageCount = typeof content === "string"
|
|
1871
2113
|
? undefined
|
|
1872
2114
|
: content.filter((c) => c.type === "image").length || undefined;
|
|
1873
|
-
setLiveItems((prev) => {
|
|
1874
|
-
if (prev.length > 0)
|
|
1875
|
-
queueFlush(prev);
|
|
1876
|
-
return [];
|
|
1877
|
-
});
|
|
1878
2115
|
const userItem = {
|
|
1879
2116
|
kind: "user",
|
|
1880
2117
|
text: displayText,
|
|
@@ -1883,8 +2120,8 @@ export function App(props) {
|
|
|
1883
2120
|
};
|
|
1884
2121
|
setLastUserMessage(displayText);
|
|
1885
2122
|
setDoneStatus(null);
|
|
1886
|
-
|
|
1887
|
-
}, []),
|
|
2123
|
+
finalizeSubmittedUserItem(userItem);
|
|
2124
|
+
}, [appendGoalProgress, finalizeSubmittedUserItem, setGoalModeAndPrompt]),
|
|
1888
2125
|
// Inject a "continue with the next step" follow-up when the agent
|
|
1889
2126
|
// would otherwise stop mid-plan. The prompt-only instruction wasn't
|
|
1890
2127
|
// enough — some models (notably Opus) treat each [DONE:n] as a
|
|
@@ -1954,27 +2191,6 @@ export function App(props) {
|
|
|
1954
2191
|
]);
|
|
1955
2192
|
}
|
|
1956
2193
|
};
|
|
1957
|
-
// Phase 2 of the two-phase flush: after onDone clears liveItems (phase 1)
|
|
1958
|
-
// and Ink renders the smaller live area (updating its internal line
|
|
1959
|
-
// counter), this effect pushes the stashed items into Static history.
|
|
1960
|
-
// Because the Static write happens in a SEPARATE render cycle from the
|
|
1961
|
-
// live-area shrink, Ink's log-update never needs to erase the old tall
|
|
1962
|
-
// live area AND write Static content in the same frame — avoiding the
|
|
1963
|
-
// cursor-math mismatch that caused text clipping.
|
|
1964
|
-
useEffect(() => {
|
|
1965
|
-
if (pendingFlushRef.current.length > 0) {
|
|
1966
|
-
const items = pendingFlushRef.current;
|
|
1967
|
-
pendingFlushRef.current = [];
|
|
1968
|
-
setHistory((h) => {
|
|
1969
|
-
const next = compactHistory([...h, ...trimFlushedItems(items)]);
|
|
1970
|
-
if (sessionStore)
|
|
1971
|
-
sessionStore.history = next;
|
|
1972
|
-
return next;
|
|
1973
|
-
});
|
|
1974
|
-
if (sessionStore)
|
|
1975
|
-
sessionStore.liveItems = liveItems;
|
|
1976
|
-
}
|
|
1977
|
-
}, [flushGeneration]);
|
|
1978
2194
|
// Sync terminal title with agent loop state
|
|
1979
2195
|
useEffect(() => {
|
|
1980
2196
|
setTitleRunning(agentLoop.isRunning);
|
|
@@ -2002,19 +2218,29 @@ export function App(props) {
|
|
|
2002
2218
|
return () => clearTimeout(timer);
|
|
2003
2219
|
}
|
|
2004
2220
|
}, [agentLoop.isRunning, sessionStore, props.resetUI]);
|
|
2005
|
-
// Consume
|
|
2006
|
-
// for paths that remount AND immediately drive
|
|
2007
|
-
//
|
|
2008
|
-
//
|
|
2221
|
+
// Consume pending post-remount work once on mount. Set by resetUI options
|
|
2222
|
+
// for paths that remount AND immediately drive work (plan accept/reject,
|
|
2223
|
+
// pixel fix, Goal approval). The work survives the unmount because
|
|
2224
|
+
// it lives in renderApp's closure (sessionStore), not React state.
|
|
2009
2225
|
useEffect(() => {
|
|
2010
2226
|
if (pendingActionConsumedRef.current)
|
|
2011
2227
|
return;
|
|
2012
2228
|
const action = sessionStore?.pendingAction;
|
|
2013
|
-
|
|
2229
|
+
const pendingGoalRun = sessionStore?.pendingGoalRun;
|
|
2230
|
+
if (!action && !pendingGoalRun)
|
|
2014
2231
|
return;
|
|
2015
2232
|
pendingActionConsumedRef.current = true;
|
|
2016
|
-
if (sessionStore)
|
|
2233
|
+
if (sessionStore) {
|
|
2017
2234
|
sessionStore.pendingAction = undefined;
|
|
2235
|
+
sessionStore.pendingGoalRun = undefined;
|
|
2236
|
+
}
|
|
2237
|
+
setDoneStatus(null);
|
|
2238
|
+
if (pendingGoalRun) {
|
|
2239
|
+
startGoalRunRef.current(pendingGoalRun);
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
if (!action)
|
|
2243
|
+
return;
|
|
2018
2244
|
if (action.planEvent) {
|
|
2019
2245
|
const ev = action.planEvent;
|
|
2020
2246
|
setLiveItems((prev) => [
|
|
@@ -2028,7 +2254,6 @@ export function App(props) {
|
|
|
2028
2254
|
{ kind: "info", text: action.infoText, id: getId() },
|
|
2029
2255
|
]);
|
|
2030
2256
|
}
|
|
2031
|
-
setDoneStatus(null);
|
|
2032
2257
|
void agentLoop.run(action.prompt).catch((err) => {
|
|
2033
2258
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2034
2259
|
log("ERROR", "error", errMsg);
|
|
@@ -2036,14 +2261,6 @@ export function App(props) {
|
|
|
2036
2261
|
});
|
|
2037
2262
|
// Intentional one-shot: run once on mount, never re-fire on re-render.
|
|
2038
2263
|
}, []);
|
|
2039
|
-
// Refresh eyes badge count when the agent settles (end of a turn) — a turn
|
|
2040
|
-
// may have logged new rough/wish/blocked signals. Also covers the case where
|
|
2041
|
-
// /eyes was run for the first time (manifest now exists).
|
|
2042
|
-
useEffect(() => {
|
|
2043
|
-
if (!agentLoop.isRunning) {
|
|
2044
|
-
setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
2045
|
-
}
|
|
2046
|
-
}, [agentLoop.isRunning, props.cwd]);
|
|
2047
2264
|
const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
|
|
2048
2265
|
const trimmed = input.trim();
|
|
2049
2266
|
if (trimmed.startsWith("/")) {
|
|
@@ -2098,7 +2315,8 @@ export function App(props) {
|
|
|
2098
2315
|
}
|
|
2099
2316
|
// Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
|
|
2100
2317
|
// React state in place without touching terminal scrollback.
|
|
2101
|
-
|
|
2318
|
+
pendingHistoryFlushRef.current = [];
|
|
2319
|
+
props.terminalHistoryPrinter?.clear();
|
|
2102
2320
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2103
2321
|
setLiveItems([]);
|
|
2104
2322
|
setDoneStatus(null);
|
|
@@ -2121,44 +2339,19 @@ export function App(props) {
|
|
|
2121
2339
|
setOverlay("theme");
|
|
2122
2340
|
return;
|
|
2123
2341
|
}
|
|
2124
|
-
//
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
setLiveItems((prev) => [
|
|
2130
|
-
...prev,
|
|
2342
|
+
// Handle /markdown — Gemini-style rendered/raw markdown toggle
|
|
2343
|
+
if (trimmed === "/markdown" || trimmed === "/md") {
|
|
2344
|
+
setRenderMarkdown((prev) => {
|
|
2345
|
+
const next = !prev;
|
|
2346
|
+
setLiveItems([
|
|
2131
2347
|
{
|
|
2132
2348
|
kind: "info",
|
|
2133
|
-
text:
|
|
2349
|
+
text: next ? "Rendered markdown mode." : "Raw markdown mode.",
|
|
2134
2350
|
id: getId(),
|
|
2135
2351
|
},
|
|
2136
2352
|
]);
|
|
2137
|
-
return;
|
|
2138
|
-
}
|
|
2139
|
-
setOverlay("eyes");
|
|
2140
|
-
return;
|
|
2141
|
-
}
|
|
2142
|
-
// Handle /plan — toggle plan mode
|
|
2143
|
-
if (trimmed === "/plan" || trimmed === "/plan on") {
|
|
2144
|
-
setPlanMode(true);
|
|
2145
|
-
setLiveItems((prev) => [
|
|
2146
|
-
...prev,
|
|
2147
|
-
{ kind: "plan_transition", text: "Plan Mode Activated", active: true, id: getId() },
|
|
2148
|
-
]);
|
|
2149
|
-
return;
|
|
2150
|
-
}
|
|
2151
|
-
if (trimmed === "/plan off") {
|
|
2152
|
-
setPlanMode(false);
|
|
2153
|
-
setLiveItems((prev) => [
|
|
2154
|
-
...prev,
|
|
2155
|
-
{
|
|
2156
|
-
kind: "plan_transition",
|
|
2157
|
-
text: "Plan Mode Deactivated",
|
|
2158
|
-
active: false,
|
|
2159
|
-
id: getId(),
|
|
2160
|
-
},
|
|
2161
|
-
]);
|
|
2353
|
+
return next;
|
|
2354
|
+
});
|
|
2162
2355
|
return;
|
|
2163
2356
|
}
|
|
2164
2357
|
// Handle /clearplan — dismiss the approved plan
|
|
@@ -2217,55 +2410,45 @@ export function App(props) {
|
|
|
2217
2410
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2218
2411
|
props.sessionStore.overlay = "goal";
|
|
2219
2412
|
props.sessionStore.planAutoExpand = false;
|
|
2413
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2220
2414
|
props.resetUI();
|
|
2221
2415
|
}
|
|
2222
2416
|
else {
|
|
2223
2417
|
if (props.sessionStore) {
|
|
2224
2418
|
props.sessionStore.overlay = "goal";
|
|
2225
2419
|
props.sessionStore.planAutoExpand = false;
|
|
2420
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2226
2421
|
if (agentLoop.isRunning)
|
|
2227
2422
|
props.sessionStore.pendingResetUI = true;
|
|
2228
2423
|
}
|
|
2229
2424
|
setPlanAutoExpand(false);
|
|
2425
|
+
setGoalAutoExpand(false);
|
|
2230
2426
|
setOverlay("goal");
|
|
2231
2427
|
}
|
|
2232
2428
|
return;
|
|
2233
2429
|
}
|
|
2234
|
-
// Handle /plans — open plan pane
|
|
2235
|
-
if (trimmed === "/plans") {
|
|
2236
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2237
|
-
props.sessionStore.overlay = "plan";
|
|
2238
|
-
props.sessionStore.planAutoExpand = false;
|
|
2239
|
-
props.resetUI();
|
|
2240
|
-
}
|
|
2241
|
-
else {
|
|
2242
|
-
if (props.sessionStore) {
|
|
2243
|
-
props.sessionStore.overlay = "plan";
|
|
2244
|
-
props.sessionStore.planAutoExpand = false;
|
|
2245
|
-
if (agentLoop.isRunning)
|
|
2246
|
-
props.sessionStore.pendingResetUI = true;
|
|
2247
|
-
}
|
|
2248
|
-
setPlanAutoExpand(false);
|
|
2249
|
-
setOverlay("plan");
|
|
2250
|
-
}
|
|
2251
|
-
return;
|
|
2252
|
-
}
|
|
2253
2430
|
// Handle prompt-template commands (built-in + custom from .gg/commands/)
|
|
2254
2431
|
const promptCommandRoute = routePromptCommandInput(trimmed, PROMPT_COMMANDS, customCommands);
|
|
2255
2432
|
if (promptCommandRoute) {
|
|
2256
2433
|
const { cmdName, cmdArgs, fullPrompt } = promptCommandRoute;
|
|
2257
2434
|
log("INFO", "command", `Prompt command: /${cmdName}${cmdArgs ? ` (args: ${cmdArgs})` : ""}`);
|
|
2258
|
-
// Move live items into history before starting
|
|
2259
|
-
setLiveItems((prev) => {
|
|
2260
|
-
if (prev.length > 0) {
|
|
2261
|
-
pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
|
|
2262
|
-
}
|
|
2263
|
-
return [];
|
|
2264
|
-
});
|
|
2265
2435
|
const hasImages = inputImages.length > 0;
|
|
2436
|
+
const isGoalSetupCommand = isGoalPromptCommandName(cmdName);
|
|
2437
|
+
let promptForAgent = fullPrompt;
|
|
2438
|
+
if (isGoalSetupCommand) {
|
|
2439
|
+
const referenceContext = await buildGoalReferenceContext({
|
|
2440
|
+
cwd: props.cwd,
|
|
2441
|
+
originalGoalPrompt: fullPrompt,
|
|
2442
|
+
attachments: inputImages,
|
|
2443
|
+
});
|
|
2444
|
+
setActiveGoalReferences(referenceContext.references);
|
|
2445
|
+
promptForAgent = referenceContext.promptSection
|
|
2446
|
+
? `${fullPrompt}\n\n${referenceContext.promptSection}`
|
|
2447
|
+
: fullPrompt;
|
|
2448
|
+
}
|
|
2266
2449
|
const modelInfo = getModel(currentModel);
|
|
2267
2450
|
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
2268
|
-
const userContent = buildUserContentWithAttachments(
|
|
2451
|
+
const userContent = buildUserContentWithAttachments(promptForAgent, inputImages, modelSupportsImages);
|
|
2269
2452
|
// Show the typed command as the user message
|
|
2270
2453
|
const userItem = {
|
|
2271
2454
|
kind: "user",
|
|
@@ -2275,15 +2458,30 @@ export function App(props) {
|
|
|
2275
2458
|
};
|
|
2276
2459
|
setLastUserMessage(trimmed);
|
|
2277
2460
|
setDoneStatus(null);
|
|
2278
|
-
|
|
2461
|
+
finalizeSubmittedUserItem(userItem);
|
|
2279
2462
|
// Send the full prompt to the agent, with user args appended if provided
|
|
2280
2463
|
try {
|
|
2281
|
-
|
|
2464
|
+
if (isGoalSetupCommand) {
|
|
2465
|
+
goalSetupPanePendingRef.current = true;
|
|
2466
|
+
await runGoalPromptSetupSequence({
|
|
2467
|
+
userContent,
|
|
2468
|
+
fullPrompt: promptForAgent,
|
|
2469
|
+
messagesRef,
|
|
2470
|
+
setGoalModeAndPrompt,
|
|
2471
|
+
runAgent: (content) => agentLoop.run(content),
|
|
2472
|
+
onStage: appendGoalAgentTransition,
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
else {
|
|
2476
|
+
await agentLoop.run(userContent);
|
|
2477
|
+
}
|
|
2282
2478
|
}
|
|
2283
2479
|
catch (err) {
|
|
2284
2480
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2285
2481
|
log("ERROR", "error", msg);
|
|
2286
2482
|
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2483
|
+
if (isGoalSetupCommand)
|
|
2484
|
+
goalSetupPanePendingRef.current = false;
|
|
2287
2485
|
setLiveItems((prev) => [
|
|
2288
2486
|
...prev,
|
|
2289
2487
|
isAbort
|
|
@@ -2291,6 +2489,41 @@ export function App(props) {
|
|
|
2291
2489
|
: toErrorItem(err, getId()),
|
|
2292
2490
|
]);
|
|
2293
2491
|
}
|
|
2492
|
+
finally {
|
|
2493
|
+
if (isGoalSetupCommand) {
|
|
2494
|
+
setActiveGoalReferences(undefined);
|
|
2495
|
+
const paneTransition = getGoalSetupPaneTransitionAfterRun({
|
|
2496
|
+
isGoalSetupCommand,
|
|
2497
|
+
setupPanePending: goalSetupPanePendingRef.current,
|
|
2498
|
+
});
|
|
2499
|
+
goalSetupPanePendingRef.current = false;
|
|
2500
|
+
if (goalModeStateRef.current !== "off") {
|
|
2501
|
+
await setGoalModeAndPrompt("off");
|
|
2502
|
+
}
|
|
2503
|
+
if (paneTransition) {
|
|
2504
|
+
goalAutoExpandRef.current = paneTransition.goalAutoExpand;
|
|
2505
|
+
setTimeout(() => {
|
|
2506
|
+
const resetUI = props.resetUI;
|
|
2507
|
+
const sessionStore = props.sessionStore;
|
|
2508
|
+
if (shouldResetUIForGoalSetupPaneTransition({
|
|
2509
|
+
hasResetUI: resetUI !== undefined,
|
|
2510
|
+
hasSessionStore: sessionStore !== undefined,
|
|
2511
|
+
}) &&
|
|
2512
|
+
resetUI &&
|
|
2513
|
+
sessionStore) {
|
|
2514
|
+
sessionStore.overlay = paneTransition.overlay;
|
|
2515
|
+
sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
|
|
2516
|
+
sessionStore.planAutoExpand = paneTransition.planAutoExpand;
|
|
2517
|
+
resetUI();
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
setGoalAutoExpand(paneTransition.goalAutoExpand);
|
|
2521
|
+
setPlanAutoExpand(paneTransition.planAutoExpand);
|
|
2522
|
+
setOverlay(paneTransition.overlay);
|
|
2523
|
+
}, 300);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2294
2527
|
// Reload custom commands in case a setup command created new ones
|
|
2295
2528
|
reloadCustomCommands();
|
|
2296
2529
|
return;
|
|
@@ -2326,17 +2559,6 @@ export function App(props) {
|
|
|
2326
2559
|
setLiveItems((prev) => [...prev, queuedItem]);
|
|
2327
2560
|
return;
|
|
2328
2561
|
}
|
|
2329
|
-
// Move any remaining live items into history (Static) before starting a
|
|
2330
|
-
// new turn. Must go through queueFlush so flushGeneration bumps and the
|
|
2331
|
-
// drain effect actually runs — mutating pendingFlushRef directly here
|
|
2332
|
-
// stashed items that nothing was signalled to pick up, so they sat in
|
|
2333
|
-
// limbo until some unrelated later code path happened to call queueFlush.
|
|
2334
|
-
setLiveItems((prev) => {
|
|
2335
|
-
if (prev.length > 0) {
|
|
2336
|
-
queueFlush(prev);
|
|
2337
|
-
}
|
|
2338
|
-
return [];
|
|
2339
|
-
});
|
|
2340
2562
|
// Build display text — strip image paths, show badges instead
|
|
2341
2563
|
let displayText = input;
|
|
2342
2564
|
if (hasImages) {
|
|
@@ -2358,7 +2580,7 @@ export function App(props) {
|
|
|
2358
2580
|
planStepsRef.current = [];
|
|
2359
2581
|
setPlanSteps([]);
|
|
2360
2582
|
}
|
|
2361
|
-
|
|
2583
|
+
finalizeSubmittedUserItem(userItem);
|
|
2362
2584
|
// Run agent
|
|
2363
2585
|
try {
|
|
2364
2586
|
await agentLoop.run(userContent);
|
|
@@ -2376,11 +2598,20 @@ export function App(props) {
|
|
|
2376
2598
|
}
|
|
2377
2599
|
}, [
|
|
2378
2600
|
agentLoop,
|
|
2379
|
-
|
|
2601
|
+
appendGoalAgentTransition,
|
|
2380
2602
|
compactConversation,
|
|
2603
|
+
currentModel,
|
|
2604
|
+
finalizeSubmittedUserItem,
|
|
2605
|
+
props.cwd,
|
|
2606
|
+
props.onSlashCommand,
|
|
2607
|
+
props.resetUI,
|
|
2608
|
+
props.sessionStore,
|
|
2381
2609
|
rebuildSystemPrompt,
|
|
2382
|
-
replaceSystemPrompt,
|
|
2383
2610
|
refreshRepoMap,
|
|
2611
|
+
reloadCustomCommands,
|
|
2612
|
+
replaceSystemPrompt,
|
|
2613
|
+
setActiveGoalReferences,
|
|
2614
|
+
setGoalModeAndPrompt,
|
|
2384
2615
|
stripRepoMapMessages,
|
|
2385
2616
|
]);
|
|
2386
2617
|
const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
|
|
@@ -2518,90 +2749,96 @@ export function App(props) {
|
|
|
2518
2749
|
const promptByName = new Map(PROMPT_COMMANDS.map((c) => [c.name, c]));
|
|
2519
2750
|
const fromPrompt = (name) => {
|
|
2520
2751
|
const c = promptByName.get(name);
|
|
2521
|
-
return c
|
|
2752
|
+
return c
|
|
2753
|
+
? {
|
|
2754
|
+
name: c.name,
|
|
2755
|
+
aliases: c.aliases,
|
|
2756
|
+
description: c.description,
|
|
2757
|
+
sectionTitle: "workflows",
|
|
2758
|
+
}
|
|
2759
|
+
: null;
|
|
2522
2760
|
};
|
|
2523
2761
|
const promptOrder = [
|
|
2524
2762
|
// Project audits / one-shot analysis
|
|
2525
2763
|
"goal",
|
|
2526
2764
|
"init",
|
|
2527
|
-
"research",
|
|
2528
|
-
"scan",
|
|
2529
|
-
"verify",
|
|
2530
2765
|
"expand",
|
|
2531
2766
|
"bullet-proof",
|
|
2532
|
-
"simplify",
|
|
2533
2767
|
"compare",
|
|
2534
|
-
"batch",
|
|
2535
2768
|
// Setup / installers
|
|
2536
|
-
"setup-lint",
|
|
2537
|
-
"setup-tests",
|
|
2538
2769
|
"setup-commit",
|
|
2539
|
-
"setup-update",
|
|
2540
|
-
"setup-eyes",
|
|
2541
|
-
"eyes-improve",
|
|
2542
2770
|
"setup-skills",
|
|
2543
2771
|
];
|
|
2544
2772
|
const orderedPromptCommands = promptOrder
|
|
2545
2773
|
.map(fromPrompt)
|
|
2546
2774
|
.filter((c) => c !== null);
|
|
2547
2775
|
const knownPromptNames = new Set(promptOrder);
|
|
2548
|
-
const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({
|
|
2776
|
+
const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({
|
|
2777
|
+
name: c.name,
|
|
2778
|
+
aliases: c.aliases,
|
|
2779
|
+
description: c.description,
|
|
2780
|
+
sectionTitle: "workflows",
|
|
2781
|
+
}));
|
|
2549
2782
|
return [
|
|
2550
2783
|
// Session actions (most frequent)
|
|
2551
|
-
{ name: "model", aliases: ["m"], description: "Switch model" },
|
|
2552
|
-
{ name: "compact", aliases: ["c"], description: "Compact
|
|
2553
|
-
{ name: "clear", aliases: [], description: "Clear session
|
|
2554
|
-
{ name: "theme", aliases: ["t"], description: "Switch theme" },
|
|
2555
|
-
{ name: "plans", aliases: [], description: "Open plans pane" },
|
|
2784
|
+
{ name: "model", aliases: ["m"], description: "Switch model", sectionTitle: "built-in" },
|
|
2785
|
+
{ name: "compact", aliases: ["c"], description: "Compact context", sectionTitle: "built-in" },
|
|
2786
|
+
{ name: "clear", aliases: [], description: "Clear session", sectionTitle: "built-in" },
|
|
2787
|
+
{ name: "theme", aliases: ["t"], description: "Switch theme", sectionTitle: "built-in" },
|
|
2556
2788
|
...orderedPromptCommands,
|
|
2557
2789
|
...remainingPromptCommands,
|
|
2558
2790
|
...customCommands.map((cmd) => ({
|
|
2559
2791
|
name: cmd.name,
|
|
2560
2792
|
aliases: [],
|
|
2561
2793
|
description: cmd.description,
|
|
2794
|
+
sectionTitle: "custom",
|
|
2562
2795
|
})),
|
|
2563
|
-
{
|
|
2796
|
+
{
|
|
2797
|
+
name: "quit",
|
|
2798
|
+
aliases: ["q", "exit"],
|
|
2799
|
+
description: "Exit ggcoder",
|
|
2800
|
+
sectionTitle: "built-in",
|
|
2801
|
+
},
|
|
2564
2802
|
];
|
|
2565
2803
|
}, [customCommands]);
|
|
2566
|
-
const
|
|
2804
|
+
const normalizeStatusText = (text) => text.replace(/\\n/g, "\n").replace(/^\n+|\n+$/g, "");
|
|
2805
|
+
const renderStatusMessage = (key, glyph, content, glyphColor = theme.commandColor, options = {}) => (_jsxs(Box, { flexDirection: "row", paddingLeft: 1, marginTop: 1, flexShrink: 1, children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: glyphColor, bold: options.bold ?? true, children: glyph }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(Text, { color: options.muted ? theme.textDim : theme.commandColor, bold: options.bold, wrap: "wrap", children: content }) })] }, key));
|
|
2806
|
+
const renderItem = (item, index, items) => {
|
|
2807
|
+
const previousLiveItem = index > 0 ? items[index - 1] : undefined;
|
|
2808
|
+
const shouldTopSpacePrintedBoundary = shouldTopSpaceAfterPrintedAgentBoundary({
|
|
2809
|
+
currentKind: item.kind,
|
|
2810
|
+
previousLiveItem,
|
|
2811
|
+
lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
|
|
2812
|
+
lastHistoryItem: history.at(-1),
|
|
2813
|
+
});
|
|
2814
|
+
const assistantMarginTop = item.kind === "assistant" &&
|
|
2815
|
+
(shouldTopSpacePrintedBoundary ||
|
|
2816
|
+
shouldTopSpaceAssistantAfterToolBoundary({
|
|
2817
|
+
text: item.text,
|
|
2818
|
+
previousLiveItem,
|
|
2819
|
+
lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
|
|
2820
|
+
lastHistoryItem: history.at(-1),
|
|
2821
|
+
}))
|
|
2822
|
+
? 1
|
|
2823
|
+
: 0;
|
|
2824
|
+
const withPrintedBoundarySpacing = (node) => shouldTopSpacePrintedBoundary ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: node }, `${item.id}-printed-boundary`)) : (node);
|
|
2567
2825
|
switch (item.kind) {
|
|
2568
2826
|
case "tombstone":
|
|
2569
2827
|
return null;
|
|
2570
2828
|
case "banner":
|
|
2571
|
-
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd
|
|
2829
|
+
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd }, item.id));
|
|
2572
2830
|
case "user":
|
|
2573
2831
|
return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
|
|
2574
|
-
case "task":
|
|
2575
|
-
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));
|
|
2576
2832
|
case "goal":
|
|
2577
|
-
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));
|
|
2833
|
+
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: truncateGoalProgressText(item.title) }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }) }, item.id));
|
|
2578
2834
|
case "goal_progress": {
|
|
2579
|
-
const
|
|
2580
|
-
const
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
: item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working"
|
|
2587
|
-
? theme.secondary
|
|
2588
|
-
: item.phase === "continuing"
|
|
2589
|
-
? theme.warning
|
|
2590
|
-
: item.phase === "verifier_started"
|
|
2591
|
-
? theme.accent
|
|
2592
|
-
: item.phase === "worker_started"
|
|
2593
|
-
? theme.primary
|
|
2594
|
-
: item.phase === "terminal"
|
|
2595
|
-
? theme.success
|
|
2596
|
-
: theme.primary;
|
|
2597
|
-
const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
|
|
2598
|
-
? "✓ "
|
|
2599
|
-
: item.phase === "terminal"
|
|
2600
|
-
? item.status === "passed"
|
|
2601
|
-
? "◆ "
|
|
2602
|
-
: "! "
|
|
2603
|
-
: "↻ ";
|
|
2604
|
-
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: glyph }), _jsx(Text, { color: color, bold: true, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }), item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.detail}` })) : null, item.summaryRows && item.summaryRows.length > 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, flexShrink: 1, children: item.summaryRows.map((row) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.textDim, children: row.label.padEnd(10) }), _jsx(Text, { color: theme.text, children: row.value }), row.detail ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", row.detail] }) : null] }, row.label))) })) : null] }, item.id));
|
|
2835
|
+
const color = goalProgressColor(item, theme);
|
|
2836
|
+
const loaderStatus = goalProgressLoaderStatus(item);
|
|
2837
|
+
const hasBody = !!item.detail ||
|
|
2838
|
+
(item.summaryRows !== undefined && item.summaryRows.length > 0) ||
|
|
2839
|
+
(item.summarySections !== undefined && item.summarySections.length > 0);
|
|
2840
|
+
const headerContentWidth = Math.max(10, columns - 3);
|
|
2841
|
+
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));
|
|
2605
2842
|
}
|
|
2606
2843
|
case "style_pack": {
|
|
2607
2844
|
const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
|
|
@@ -2611,61 +2848,60 @@ export function App(props) {
|
|
|
2611
2848
|
case "setup_hint":
|
|
2612
2849
|
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.language, paddingX: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.language, bold: true, children: "◆ " }), _jsx(Text, { color: theme.language, bold: true, children: "NO STYLE PACKS DETECTED" })] }), _jsx(Text, { color: theme.textMuted, wrap: "wrap", children: "This directory has no recognized language manifest at its root." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.textMuted, children: "Tip: run " }), _jsx(Text, { color: theme.language, bold: true, children: "/setup" }), _jsx(Text, { color: theme.textMuted, children: " to audit project hygiene or bootstrap a new project from scratch" })] }) })] }, item.id));
|
|
2613
2850
|
case "assistant":
|
|
2614
|
-
return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs,
|
|
2851
|
+
return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, renderMarkdown: renderMarkdown, availableTerminalHeight: measuredLiveAreaRows, marginTop: assistantMarginTop }, item.id));
|
|
2615
2852
|
case "tool_start":
|
|
2616
|
-
return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
|
|
2853
|
+
return withPrintedBoundarySpacing(_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
|
|
2617
2854
|
case "tool_done":
|
|
2618
|
-
return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
|
|
2855
|
+
return withPrintedBoundarySpacing(_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
|
|
2619
2856
|
case "tool_group":
|
|
2620
|
-
return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
|
|
2857
|
+
return withPrintedBoundarySpacing(_jsx(ToolGroupExecution, { tools: item.tools }, item.id));
|
|
2621
2858
|
case "server_tool_start":
|
|
2622
|
-
return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
|
|
2859
|
+
return withPrintedBoundarySpacing(_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
|
|
2623
2860
|
case "server_tool_done":
|
|
2624
|
-
return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
|
|
2861
|
+
return withPrintedBoundarySpacing(_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
|
|
2625
2862
|
case "error": {
|
|
2626
2863
|
const showMessage = item.message && item.message !== item.headline;
|
|
2627
|
-
return (_jsxs(Box, { marginTop: 1, flexDirection: "column",
|
|
2864
|
+
return (_jsxs(Box, { flexDirection: "row", paddingLeft: 1, marginTop: 1, flexShrink: 1, children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: theme.error, bold: true, children: "✗ " }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Text, { color: theme.error, wrap: "wrap", children: item.headline }), showMessage && (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.message })), _jsx(Text, { color: theme.textDim, wrap: "wrap", children: `→ ${item.guidance}` })] })] }, item.id));
|
|
2628
2865
|
}
|
|
2629
2866
|
case "info":
|
|
2630
|
-
return (
|
|
2867
|
+
return renderStatusMessage(item.id, "○ ", item.text, theme.commandColor, { muted: true });
|
|
2631
2868
|
case "update_notice":
|
|
2632
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, borderStyle: "round", borderColor: theme.
|
|
2869
|
+
return (_jsx(Box, { marginTop: 1, flexShrink: 1, borderStyle: "round", borderColor: theme.commandColor, paddingX: 1, children: _jsxs(Text, { color: theme.commandColor, bold: true, wrap: "wrap", children: ["✨ ", item.text] }) }, item.id));
|
|
2633
2870
|
case "plan_transition":
|
|
2634
|
-
return (
|
|
2871
|
+
return renderStatusMessage(item.id, "● ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
|
|
2872
|
+
case "goal_agent_transition":
|
|
2873
|
+
return renderStatusMessage(item.id, "● ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
|
|
2635
2874
|
case "thinking_transition": {
|
|
2636
|
-
const glyphColor = item.active ?
|
|
2637
|
-
return (
|
|
2638
|
-
}
|
|
2639
|
-
case "model_transition": {
|
|
2640
|
-
const glyphColor = THINKING_BORDER_COLORS[0];
|
|
2641
|
-
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "▸ " }), _jsx(Text, { color: theme.textDim, children: "Switched to " }), _jsx(Text, { color: theme.primary, bold: true, children: item.modelName })] }, item.id));
|
|
2642
|
-
}
|
|
2643
|
-
case "theme_transition": {
|
|
2644
|
-
const glyphColor = THINKING_BORDER_COLORS[0];
|
|
2645
|
-
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "◐ " }), _jsx(Text, { color: theme.textDim, children: "Theme switched to " }), _jsx(Text, { color: theme.primary, bold: true, children: item.themeName })] }, item.id));
|
|
2875
|
+
const glyphColor = item.active ? theme.commandColor : theme.textDim;
|
|
2876
|
+
return renderStatusMessage(item.id, "✻ ", item.active ? "Thinking ON" : "Thinking OFF", glyphColor, { bold: true, muted: !item.active });
|
|
2646
2877
|
}
|
|
2878
|
+
case "model_transition":
|
|
2879
|
+
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 });
|
|
2880
|
+
case "theme_transition":
|
|
2881
|
+
return renderStatusMessage(item.id, "◐ ", _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.textDim, children: "Theme switched to " }), _jsx(Text, { color: theme.commandColor, bold: true, children: item.themeName })] }), theme.commandColor, { bold: true });
|
|
2647
2882
|
case "plan_event": {
|
|
2648
|
-
// Plan-domain status changes (approve / reject / dismiss).
|
|
2649
|
-
//
|
|
2650
|
-
// distinct from the model/thinking gradient.
|
|
2883
|
+
// Plan-domain status changes (approve / reject / dismiss). Use the
|
|
2884
|
+
// command accent so transient TUI status rows share one purple voice.
|
|
2651
2885
|
const label = item.event === "approved"
|
|
2652
2886
|
? "Plan approved"
|
|
2653
2887
|
: item.event === "rejected"
|
|
2654
2888
|
? "Plan rejected"
|
|
2655
2889
|
: "Plan dismissed";
|
|
2656
|
-
return (
|
|
2890
|
+
return renderStatusMessage(item.id, "○ ", _jsxs(_Fragment, { children: [_jsx(Text, { children: label }), item.detail ? _jsx(Text, { color: theme.textDim, children: ` — "${item.detail}"` }) : null] }), theme.commandColor, { bold: true });
|
|
2657
2891
|
}
|
|
2658
2892
|
case "stopped":
|
|
2659
2893
|
// Cancellation / abort acknowledgement (ESC, auto-setup cancel, etc.).
|
|
2660
2894
|
// Muted dim treatment — this is an ack, not a state change worth a
|
|
2661
2895
|
// gradient. Glyph `⊘` reads as "stop" without being alarming.
|
|
2662
|
-
return (
|
|
2896
|
+
return renderStatusMessage(item.id, "⊘ ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
|
|
2663
2897
|
case "step_done":
|
|
2664
2898
|
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "✓ " }), _jsx(Text, { color: theme.success, bold: true, children: `Step ${item.stepNum} done` }), item.description ? (_jsx(Text, { color: theme.textDim, children: ` — ${item.description}` })) : null] }) }, item.id));
|
|
2665
|
-
case "queued":
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2899
|
+
case "queued": {
|
|
2900
|
+
const suffix = item.imageCount
|
|
2901
|
+
? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
|
|
2902
|
+
: "";
|
|
2903
|
+
return withPrintedBoundarySpacing(_jsxs(Box, { flexDirection: "row", paddingLeft: 1, marginTop: 1, flexShrink: 1, children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: theme.warning, bold: true, children: "• " }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsxs(Text, { color: theme.text, wrap: "wrap", children: [_jsx(Text, { color: theme.textDim, children: "Queued: " }), item.text || "(empty)", suffix] }) })] }, item.id));
|
|
2904
|
+
}
|
|
2669
2905
|
case "compacting":
|
|
2670
2906
|
return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
|
|
2671
2907
|
case "compacted":
|
|
@@ -2673,87 +2909,16 @@ export function App(props) {
|
|
|
2673
2909
|
case "duration":
|
|
2674
2910
|
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.textDim, children: ["✻ ", item.verb, " ", formatDuration(item.durationMs)] }) }, item.id));
|
|
2675
2911
|
case "subagent_group":
|
|
2676
|
-
return _jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id);
|
|
2912
|
+
return withPrintedBoundarySpacing(_jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id));
|
|
2677
2913
|
}
|
|
2678
2914
|
};
|
|
2679
|
-
// ── Start a task (shared by manual "work on it" and run-all) ──
|
|
2680
|
-
const startTask = useCallback((title, prompt, taskId) => {
|
|
2681
|
-
setTaskCount(getTaskCount(props.cwd));
|
|
2682
|
-
const shortId = taskId.slice(0, 8);
|
|
2683
|
-
const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
|
|
2684
|
-
`tasks({ action: "done", id: "${shortId}" })`;
|
|
2685
|
-
const fullPrompt = prompt + completionHint;
|
|
2686
|
-
if (props.resetUI && props.sessionStore) {
|
|
2687
|
-
// Preserve the current system prompt (may differ from the launch
|
|
2688
|
-
// config — e.g. plan mode toggled or skills changed).
|
|
2689
|
-
const sysMsg = messagesRef.current[0];
|
|
2690
|
-
const newMessages = sysMsg && sysMsg.role === "system" ? [sysMsg] : messagesRef.current.slice(0, 1);
|
|
2691
|
-
const taskItem = { kind: "task", title, id: String(nextIdRef.current++) };
|
|
2692
|
-
const sm = sessionManagerRef.current;
|
|
2693
|
-
void (async () => {
|
|
2694
|
-
let newSessionPath;
|
|
2695
|
-
if (sm) {
|
|
2696
|
-
try {
|
|
2697
|
-
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
2698
|
-
newSessionPath = s.path;
|
|
2699
|
-
log("INFO", "tasks", "New session for task", { path: s.path });
|
|
2700
|
-
}
|
|
2701
|
-
catch {
|
|
2702
|
-
// session creation is best-effort
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
if (props.sessionStore)
|
|
2706
|
-
props.sessionStore.overlay = null;
|
|
2707
|
-
props.resetUI?.({
|
|
2708
|
-
wipeSession: true,
|
|
2709
|
-
messages: newMessages,
|
|
2710
|
-
history: [{ kind: "banner", id: "banner" }, taskItem],
|
|
2711
|
-
sessionPath: newSessionPath,
|
|
2712
|
-
pendingAction: { prompt: fullPrompt },
|
|
2713
|
-
});
|
|
2714
|
-
})();
|
|
2715
|
-
return;
|
|
2716
|
-
}
|
|
2717
|
-
// Fallback path (resetUI not wired — tests).
|
|
2718
|
-
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2719
|
-
setLiveItems([]);
|
|
2720
|
-
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
2721
|
-
agentLoop.reset();
|
|
2722
|
-
persistedIndexRef.current = messagesRef.current.length;
|
|
2723
|
-
const sm = sessionManagerRef.current;
|
|
2724
|
-
if (sm) {
|
|
2725
|
-
void sm.create(props.cwd, currentProvider, currentModel).then((s) => {
|
|
2726
|
-
sessionPathRef.current = s.path;
|
|
2727
|
-
log("INFO", "tasks", "New session for task", { path: s.path });
|
|
2728
|
-
});
|
|
2729
|
-
}
|
|
2730
|
-
const taskItem = { kind: "task", title, id: getId() };
|
|
2731
|
-
setLastUserMessage(title);
|
|
2732
|
-
setDoneStatus(null);
|
|
2733
|
-
setLiveItems([taskItem]);
|
|
2734
|
-
void (async () => {
|
|
2735
|
-
try {
|
|
2736
|
-
await agentLoop.run(fullPrompt);
|
|
2737
|
-
}
|
|
2738
|
-
catch (err) {
|
|
2739
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2740
|
-
log("ERROR", "error", msg);
|
|
2741
|
-
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2742
|
-
setLiveItems((prev) => [
|
|
2743
|
-
...prev,
|
|
2744
|
-
isAbort
|
|
2745
|
-
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2746
|
-
: toErrorItem(err, getId()),
|
|
2747
|
-
]);
|
|
2748
|
-
setRunAllTasks(false);
|
|
2749
|
-
}
|
|
2750
|
-
})();
|
|
2751
|
-
}, [props.cwd, props.resetUI, props.sessionStore, agentLoop, currentProvider, currentModel]);
|
|
2752
2915
|
const openOverlay = useCallback((kind) => {
|
|
2753
2916
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2754
2917
|
props.sessionStore.overlay = kind;
|
|
2755
2918
|
if (kind !== "plan")
|
|
2756
2919
|
props.sessionStore.planAutoExpand = false;
|
|
2920
|
+
if (kind !== "goal")
|
|
2921
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2757
2922
|
props.resetUI();
|
|
2758
2923
|
}
|
|
2759
2924
|
else {
|
|
@@ -2761,12 +2926,16 @@ export function App(props) {
|
|
|
2761
2926
|
props.sessionStore.overlay = kind;
|
|
2762
2927
|
if (kind !== "plan")
|
|
2763
2928
|
props.sessionStore.planAutoExpand = false;
|
|
2929
|
+
if (kind !== "goal")
|
|
2930
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2764
2931
|
if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
|
|
2765
2932
|
props.sessionStore.pendingResetUI = true;
|
|
2766
2933
|
}
|
|
2767
2934
|
}
|
|
2768
2935
|
if (kind !== "plan")
|
|
2769
2936
|
setPlanAutoExpand(false);
|
|
2937
|
+
if (kind !== "goal")
|
|
2938
|
+
setGoalAutoExpand(false);
|
|
2770
2939
|
setOverlay(kind);
|
|
2771
2940
|
}
|
|
2772
2941
|
}, [agentLoop.isRunning, props]);
|
|
@@ -2788,6 +2957,8 @@ export function App(props) {
|
|
|
2788
2957
|
? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
|
|
2789
2958
|
: `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
|
|
2790
2959
|
if (agentRunningRef.current) {
|
|
2960
|
+
queuedGoalSyntheticEventsRef.current += 1;
|
|
2961
|
+
void setGoalModeAndPrompt("coordinator");
|
|
2791
2962
|
appendGoalProgress({
|
|
2792
2963
|
kind: "goal_progress",
|
|
2793
2964
|
phase: "orchestrator_reviewing",
|
|
@@ -2809,12 +2980,19 @@ export function App(props) {
|
|
|
2809
2980
|
});
|
|
2810
2981
|
setLastUserMessage("");
|
|
2811
2982
|
setDoneStatus(null);
|
|
2812
|
-
void
|
|
2983
|
+
void (async () => {
|
|
2984
|
+
await setGoalModeAndPrompt("coordinator");
|
|
2985
|
+
await agentLoop.run(eventText);
|
|
2986
|
+
})().catch((err) => {
|
|
2813
2987
|
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2814
2988
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2989
|
+
clearGoalModeIfIdle();
|
|
2815
2990
|
});
|
|
2816
|
-
}, [agentLoop, appendGoalProgress]);
|
|
2991
|
+
}, [agentLoop, appendGoalProgress, clearGoalModeIfIdle, setGoalModeAndPrompt]);
|
|
2817
2992
|
const continueGoalRun = useCallback((runId) => {
|
|
2993
|
+
if (goalContinuationFlightsRef.current.has(runId))
|
|
2994
|
+
return;
|
|
2995
|
+
goalContinuationFlightsRef.current.add(runId);
|
|
2818
2996
|
void (async () => {
|
|
2819
2997
|
const latestRun = await reconcileActiveGoalRuns(props.cwd, {
|
|
2820
2998
|
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
@@ -2822,11 +3000,24 @@ export function App(props) {
|
|
|
2822
3000
|
if (!latestRun) {
|
|
2823
3001
|
runningGoalIdsRef.current.delete(runId);
|
|
2824
3002
|
clearGoalStatusEntry(runId);
|
|
3003
|
+
clearGoalModeIfIdle();
|
|
2825
3004
|
return;
|
|
2826
3005
|
}
|
|
2827
3006
|
const decision = decideGoalNextAction(latestRun);
|
|
2828
3007
|
if (decision.kind === "wait")
|
|
2829
3008
|
return;
|
|
3009
|
+
const choiceKey = getGoalContinuationChoiceKey({ runId: latestRun.id, decision });
|
|
3010
|
+
const now = Date.now();
|
|
3011
|
+
const recentChoiceAt = goalContinuationRecentChoicesRef.current.get(choiceKey);
|
|
3012
|
+
if (recentChoiceAt !== undefined && now - recentChoiceAt < 5000)
|
|
3013
|
+
return;
|
|
3014
|
+
goalContinuationRecentChoicesRef.current.set(choiceKey, now);
|
|
3015
|
+
if (goalContinuationRecentChoicesRef.current.size > 100) {
|
|
3016
|
+
for (const [key, startedAt] of goalContinuationRecentChoicesRef.current) {
|
|
3017
|
+
if (now - startedAt > 60_000)
|
|
3018
|
+
goalContinuationRecentChoicesRef.current.delete(key);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
2830
3021
|
if (decision.kind === "terminal" ||
|
|
2831
3022
|
decision.kind === "blocked" ||
|
|
2832
3023
|
decision.kind === "pause") {
|
|
@@ -2856,6 +3047,7 @@ export function App(props) {
|
|
|
2856
3047
|
}
|
|
2857
3048
|
runningGoalIdsRef.current.delete(runId);
|
|
2858
3049
|
clearGoalStatusEntry(runId);
|
|
3050
|
+
clearGoalModeIfIdle();
|
|
2859
3051
|
return;
|
|
2860
3052
|
}
|
|
2861
3053
|
let runForNextAction = latestRun;
|
|
@@ -2886,18 +3078,28 @@ export function App(props) {
|
|
|
2886
3078
|
detail: "choosing next step",
|
|
2887
3079
|
});
|
|
2888
3080
|
startGoalRunRef.current(runForNextAction);
|
|
2889
|
-
})()
|
|
3081
|
+
})()
|
|
3082
|
+
.catch((err) => {
|
|
2890
3083
|
runningGoalIdsRef.current.delete(runId);
|
|
2891
3084
|
clearGoalStatusEntry(runId);
|
|
2892
3085
|
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2893
3086
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3087
|
+
})
|
|
3088
|
+
.finally(() => {
|
|
3089
|
+
goalContinuationFlightsRef.current.delete(runId);
|
|
3090
|
+
clearGoalModeIfIdle();
|
|
2894
3091
|
});
|
|
2895
|
-
}, [
|
|
3092
|
+
}, [
|
|
3093
|
+
appendGoalProgress,
|
|
3094
|
+
clearGoalModeIfIdle,
|
|
3095
|
+
clearGoalStatusEntry,
|
|
3096
|
+
props.cwd,
|
|
3097
|
+
upsertGoalStatusEntry,
|
|
3098
|
+
]);
|
|
2896
3099
|
const handleGoalWorkerComplete = useCallback((run, completion) => {
|
|
2897
3100
|
const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
|
|
2898
3101
|
completion.worker.goalTaskId;
|
|
2899
3102
|
const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
|
|
2900
|
-
void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
|
|
2901
3103
|
appendGoalProgress({
|
|
2902
3104
|
kind: "goal_progress",
|
|
2903
3105
|
phase: "worker_finished",
|
|
@@ -2951,7 +3153,16 @@ export function App(props) {
|
|
|
2951
3153
|
}, [handleGoalWorkerComplete, props.cwd]);
|
|
2952
3154
|
const startGoalRun = useCallback((run) => {
|
|
2953
3155
|
runningGoalIdsRef.current.add(run.id);
|
|
3156
|
+
upsertGoalStatusEntry({
|
|
3157
|
+
runId: run.id,
|
|
3158
|
+
label: run.title,
|
|
3159
|
+
phase: "orchestrating",
|
|
3160
|
+
startedAt: Date.now(),
|
|
3161
|
+
detail: "choosing next step",
|
|
3162
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3163
|
+
});
|
|
2954
3164
|
void (async () => {
|
|
3165
|
+
await setGoalModeAndPrompt("coordinator");
|
|
2955
3166
|
const currentRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
2956
3167
|
const prereqCheck = await runGoalPrerequisiteChecks(props.cwd, currentRun);
|
|
2957
3168
|
const checkedRun = prereqCheck.checkedCount > 0
|
|
@@ -2961,14 +3172,12 @@ export function App(props) {
|
|
|
2961
3172
|
})
|
|
2962
3173
|
: currentRun;
|
|
2963
3174
|
if (goalHasBlockingPrerequisites(checkedRun)) {
|
|
2964
|
-
setOverlay(null);
|
|
2965
3175
|
const detail = formatGoalBlockingPrerequisites(checkedRun);
|
|
2966
3176
|
await upsertGoalRun(props.cwd, {
|
|
2967
3177
|
...checkedRun,
|
|
2968
3178
|
status: "blocked",
|
|
2969
3179
|
blockers: Array.from(new Set([...checkedRun.blockers, detail])),
|
|
2970
3180
|
});
|
|
2971
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2972
3181
|
appendGoalProgress({
|
|
2973
3182
|
kind: "goal_progress",
|
|
2974
3183
|
phase: "terminal",
|
|
@@ -2978,6 +3187,7 @@ export function App(props) {
|
|
|
2978
3187
|
});
|
|
2979
3188
|
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
2980
3189
|
clearGoalStatusEntry(checkedRun.id);
|
|
3190
|
+
clearGoalModeIfIdle();
|
|
2981
3191
|
return;
|
|
2982
3192
|
}
|
|
2983
3193
|
const decision = decideGoalNextAction(checkedRun);
|
|
@@ -2990,6 +3200,7 @@ export function App(props) {
|
|
|
2990
3200
|
}
|
|
2991
3201
|
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
2992
3202
|
clearGoalStatusEntry(checkedRun.id);
|
|
3203
|
+
clearGoalModeIfIdle();
|
|
2993
3204
|
return;
|
|
2994
3205
|
}
|
|
2995
3206
|
if (decision.kind === "wait") {
|
|
@@ -3015,7 +3226,6 @@ export function App(props) {
|
|
|
3015
3226
|
}
|
|
3016
3227
|
if (decision.kind === "complete") {
|
|
3017
3228
|
await upsertGoalRun(props.cwd, { ...checkedRun, status: "passed" });
|
|
3018
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3019
3229
|
appendGoalProgress({
|
|
3020
3230
|
kind: "goal_progress",
|
|
3021
3231
|
phase: "terminal",
|
|
@@ -3025,6 +3235,7 @@ export function App(props) {
|
|
|
3025
3235
|
});
|
|
3026
3236
|
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
3027
3237
|
clearGoalStatusEntry(checkedRun.id);
|
|
3238
|
+
clearGoalModeIfIdle();
|
|
3028
3239
|
return;
|
|
3029
3240
|
}
|
|
3030
3241
|
if (decision.kind === "run_verifier") {
|
|
@@ -3048,7 +3259,6 @@ export function App(props) {
|
|
|
3048
3259
|
status: "blocked",
|
|
3049
3260
|
blockers: [...checkedRun.blockers, decision.reason],
|
|
3050
3261
|
});
|
|
3051
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3052
3262
|
appendGoalProgress({
|
|
3053
3263
|
kind: "goal_progress",
|
|
3054
3264
|
phase: "terminal",
|
|
@@ -3058,6 +3268,7 @@ export function App(props) {
|
|
|
3058
3268
|
});
|
|
3059
3269
|
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
3060
3270
|
clearGoalStatusEntry(checkedRun.id);
|
|
3271
|
+
clearGoalModeIfIdle();
|
|
3061
3272
|
return;
|
|
3062
3273
|
}
|
|
3063
3274
|
if (decision.kind === "pause") {
|
|
@@ -3077,7 +3288,6 @@ export function App(props) {
|
|
|
3077
3288
|
continueRequestedAt: undefined,
|
|
3078
3289
|
blockers: Array.from(new Set([...runWithPauseEvidence.blockers, decision.reason])),
|
|
3079
3290
|
});
|
|
3080
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3081
3291
|
appendGoalProgress({
|
|
3082
3292
|
kind: "goal_progress",
|
|
3083
3293
|
phase: "terminal",
|
|
@@ -3087,6 +3297,7 @@ export function App(props) {
|
|
|
3087
3297
|
});
|
|
3088
3298
|
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
3089
3299
|
clearGoalStatusEntry(checkedRun.id);
|
|
3300
|
+
clearGoalModeIfIdle();
|
|
3090
3301
|
return;
|
|
3091
3302
|
}
|
|
3092
3303
|
const runWithAttempt = (await updateGoalTask(props.cwd, checkedRun.id, decision.task.id, {
|
|
@@ -3099,7 +3310,7 @@ export function App(props) {
|
|
|
3099
3310
|
goalRunId: checkedRun.id,
|
|
3100
3311
|
goalTaskId: decision.task.id,
|
|
3101
3312
|
taskTitle: decision.task.title,
|
|
3102
|
-
prompt: decision.task.prompt,
|
|
3313
|
+
prompt: buildGoalTaskPromptWithReferences(checkedRun, decision.task.prompt),
|
|
3103
3314
|
});
|
|
3104
3315
|
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === checkedRun.id) ??
|
|
3105
3316
|
runWithAttempt;
|
|
@@ -3112,8 +3323,6 @@ export function App(props) {
|
|
|
3112
3323
|
? { ...item, status: "running", workerId: worker.id, attempts: decision.attempts }
|
|
3113
3324
|
: item),
|
|
3114
3325
|
});
|
|
3115
|
-
setOverlay(null);
|
|
3116
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3117
3326
|
appendGoalProgress({
|
|
3118
3327
|
kind: "goal_progress",
|
|
3119
3328
|
phase: "worker_started",
|
|
@@ -3133,6 +3342,7 @@ export function App(props) {
|
|
|
3133
3342
|
});
|
|
3134
3343
|
})().catch((err) => {
|
|
3135
3344
|
clearGoalStatusEntry(run.id);
|
|
3345
|
+
clearGoalModeIfIdle();
|
|
3136
3346
|
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3137
3347
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3138
3348
|
});
|
|
@@ -3141,11 +3351,14 @@ export function App(props) {
|
|
|
3141
3351
|
currentProvider,
|
|
3142
3352
|
currentModel,
|
|
3143
3353
|
appendGoalProgress,
|
|
3354
|
+
clearGoalModeIfIdle,
|
|
3144
3355
|
clearGoalStatusEntry,
|
|
3145
3356
|
goalNumberForRun,
|
|
3357
|
+
setGoalModeAndPrompt,
|
|
3146
3358
|
upsertGoalStatusEntry,
|
|
3147
3359
|
]);
|
|
3148
3360
|
const verifyGoalRun = useCallback(async (run) => {
|
|
3361
|
+
await setGoalModeAndPrompt("coordinator");
|
|
3149
3362
|
if (!run.verifier?.command) {
|
|
3150
3363
|
await appendGoalEvidence(props.cwd, run.id, {
|
|
3151
3364
|
kind: "summary",
|
|
@@ -3166,6 +3379,7 @@ export function App(props) {
|
|
|
3166
3379
|
});
|
|
3167
3380
|
runningGoalIdsRef.current.delete(run.id);
|
|
3168
3381
|
clearGoalStatusEntry(run.id);
|
|
3382
|
+
clearGoalModeIfIdle();
|
|
3169
3383
|
return;
|
|
3170
3384
|
}
|
|
3171
3385
|
activeVerifierRunIdsRef.current.add(run.id);
|
|
@@ -3212,11 +3426,22 @@ export function App(props) {
|
|
|
3212
3426
|
command: run.verifier?.command,
|
|
3213
3427
|
lastResult: verification,
|
|
3214
3428
|
},
|
|
3429
|
+
...(status === "pass"
|
|
3430
|
+
? {
|
|
3431
|
+
completionAudit: {
|
|
3432
|
+
status: "unknown",
|
|
3433
|
+
summary: "Final completion audit pending for latest verifier result.",
|
|
3434
|
+
checkedAt: verification.checkedAt,
|
|
3435
|
+
verifierCheckedAt: verification.checkedAt,
|
|
3436
|
+
...(verification.outputPath ? { outputPath: verification.outputPath } : {}),
|
|
3437
|
+
},
|
|
3438
|
+
}
|
|
3439
|
+
: {}),
|
|
3215
3440
|
};
|
|
3216
3441
|
const completionCheck = canCompleteGoalRun(runWithVerifier);
|
|
3217
3442
|
const verifiedRun = await upsertGoalRun(props.cwd, {
|
|
3218
3443
|
...runWithVerifier,
|
|
3219
|
-
continueRequestedAt:
|
|
3444
|
+
continueRequestedAt: latestRun.continueRequestedAt,
|
|
3220
3445
|
status: status === "pass" && completionCheck.ok ? "passed" : "ready",
|
|
3221
3446
|
});
|
|
3222
3447
|
await appendGoalEvidence(props.cwd, run.id, {
|
|
@@ -3230,7 +3455,6 @@ export function App(props) {
|
|
|
3230
3455
|
reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
|
|
3231
3456
|
content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
|
|
3232
3457
|
});
|
|
3233
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3234
3458
|
appendGoalProgress({
|
|
3235
3459
|
kind: "goal_progress",
|
|
3236
3460
|
phase: "verifier_finished",
|
|
@@ -3249,22 +3473,25 @@ export function App(props) {
|
|
|
3249
3473
|
const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
|
|
3250
3474
|
runGoalSyntheticEvent(eventText);
|
|
3251
3475
|
const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
|
|
3252
|
-
if (continuationRun?.continueRequestedAt || status === "fail") {
|
|
3476
|
+
if (continuationRun?.continueRequestedAt || status === "fail" || status === "pass") {
|
|
3253
3477
|
setTimeout(() => continueGoalRun(run.id), 500);
|
|
3254
3478
|
}
|
|
3255
3479
|
})
|
|
3256
3480
|
.catch((err) => {
|
|
3257
3481
|
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3258
3482
|
clearGoalStatusEntry(run.id);
|
|
3483
|
+
clearGoalModeIfIdle();
|
|
3259
3484
|
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3260
3485
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
|
|
3261
3486
|
});
|
|
3262
3487
|
}, [
|
|
3263
3488
|
props.cwd,
|
|
3264
3489
|
appendGoalProgress,
|
|
3490
|
+
clearGoalModeIfIdle,
|
|
3265
3491
|
clearGoalStatusEntry,
|
|
3266
3492
|
goalNumberForRun,
|
|
3267
3493
|
runGoalSyntheticEvent,
|
|
3494
|
+
setGoalModeAndPrompt,
|
|
3268
3495
|
upsertGoalStatusEntry,
|
|
3269
3496
|
]);
|
|
3270
3497
|
const pauseGoalRun = useCallback((run) => {
|
|
@@ -3278,7 +3505,6 @@ export function App(props) {
|
|
|
3278
3505
|
status: "paused",
|
|
3279
3506
|
activeWorkerId: undefined,
|
|
3280
3507
|
});
|
|
3281
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3282
3508
|
appendGoalProgress({
|
|
3283
3509
|
kind: "goal_progress",
|
|
3284
3510
|
phase: "terminal",
|
|
@@ -3287,19 +3513,14 @@ export function App(props) {
|
|
|
3287
3513
|
status: "paused",
|
|
3288
3514
|
});
|
|
3289
3515
|
clearGoalStatusEntry(run.id);
|
|
3516
|
+
clearGoalModeIfIdle();
|
|
3290
3517
|
})().catch((err) => {
|
|
3291
3518
|
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3292
3519
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3293
3520
|
});
|
|
3294
|
-
}, [appendGoalProgress, clearGoalStatusEntry, props.cwd]);
|
|
3521
|
+
}, [appendGoalProgress, clearGoalModeIfIdle, clearGoalStatusEntry, props.cwd]);
|
|
3295
3522
|
// Keep refs in sync for access from stale closures (onDone)
|
|
3296
|
-
startTaskRef.current = startTask;
|
|
3297
3523
|
startGoalRunRef.current = startGoalRun;
|
|
3298
|
-
useEffect(() => {
|
|
3299
|
-
runAllTasksRef.current = runAllTasks;
|
|
3300
|
-
if (props.sessionStore)
|
|
3301
|
-
props.sessionStore.runAllTasks = runAllTasks;
|
|
3302
|
-
}, [runAllTasks, props.sessionStore]);
|
|
3303
3524
|
useEffect(() => {
|
|
3304
3525
|
agentRunningRef.current = agentLoop.isRunning;
|
|
3305
3526
|
}, [agentLoop.isRunning]);
|
|
@@ -3348,13 +3569,14 @@ export function App(props) {
|
|
|
3348
3569
|
injectedLanguagesRef.current = detectedForPixelFix;
|
|
3349
3570
|
const newSystemPrompt = await rebuildSystemPrompt({
|
|
3350
3571
|
cwd: prep.projectPath,
|
|
3351
|
-
planMode: false,
|
|
3352
3572
|
clearApprovedPlan: true,
|
|
3353
3573
|
activeLanguages: detectedForPixelFix,
|
|
3354
3574
|
tools: toolsForPixelFix,
|
|
3355
3575
|
});
|
|
3356
3576
|
// Now that the cwd swap is committed, reset chat. Do not clear the
|
|
3357
3577
|
// terminal here; terminal clear sequences can erase saved scrollback.
|
|
3578
|
+
pendingHistoryFlushRef.current = [];
|
|
3579
|
+
props.terminalHistoryPrinter?.clear();
|
|
3358
3580
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3359
3581
|
setLiveItems([]);
|
|
3360
3582
|
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
@@ -3374,10 +3596,10 @@ export function App(props) {
|
|
|
3374
3596
|
messagesRef.current.unshift({ role: "system", content: newSystemPrompt });
|
|
3375
3597
|
}
|
|
3376
3598
|
const title = `Fix ${errorId.slice(0, 12)}… in ${prep.projectName}`;
|
|
3377
|
-
const
|
|
3599
|
+
const goalItem = { kind: "goal", title, id: getId() };
|
|
3378
3600
|
setLastUserMessage(title);
|
|
3379
3601
|
setDoneStatus(null);
|
|
3380
|
-
setLiveItems([
|
|
3602
|
+
setLiveItems([goalItem]);
|
|
3381
3603
|
await agentLoop.run(prep.prompt);
|
|
3382
3604
|
}
|
|
3383
3605
|
catch (err) {
|
|
@@ -3398,266 +3620,333 @@ export function App(props) {
|
|
|
3398
3620
|
if (props.sessionStore)
|
|
3399
3621
|
props.sessionStore.runAllPixel = runAllPixel;
|
|
3400
3622
|
}, [runAllPixel, props.sessionStore]);
|
|
3401
|
-
const isTaskView = overlay === "tasks";
|
|
3402
3623
|
const isGoalView = overlay === "goal";
|
|
3403
3624
|
const isSkillsView = overlay === "skills";
|
|
3404
3625
|
const isPlanView = overlay === "plan";
|
|
3405
|
-
const isEyesView = overlay === "eyes";
|
|
3406
3626
|
const footerStatusLayout = getFooterStatusLayoutDecision({
|
|
3407
3627
|
columns,
|
|
3408
3628
|
backgroundTaskCount: bgTasks.length,
|
|
3409
|
-
eyesCount,
|
|
3410
3629
|
updatePending,
|
|
3411
3630
|
});
|
|
3631
|
+
const activityVisible = agentLoop.isRunning && agentLoop.activityPhase !== "idle";
|
|
3632
|
+
const stallStatusVisible = !activityVisible && !!agentLoop.stallError;
|
|
3633
|
+
const doneStatusVisible = !activityVisible && !stallStatusVisible && !!doneStatus && !agentLoop.isRunning;
|
|
3634
|
+
const statusSlotVisible = activityVisible || stallStatusVisible || doneStatusVisible;
|
|
3635
|
+
const [controlsHeight, setControlsHeight] = useState(0);
|
|
3636
|
+
const controlsObserverRef = useRef(null);
|
|
3637
|
+
const mainControlsRef = useCallback((node) => {
|
|
3638
|
+
if (controlsObserverRef.current) {
|
|
3639
|
+
controlsObserverRef.current.disconnect();
|
|
3640
|
+
controlsObserverRef.current = null;
|
|
3641
|
+
}
|
|
3642
|
+
if (!node || typeof ResizeObserver === "undefined")
|
|
3643
|
+
return;
|
|
3644
|
+
const observer = new ResizeObserver((entries) => {
|
|
3645
|
+
const entry = entries[0];
|
|
3646
|
+
if (!entry)
|
|
3647
|
+
return;
|
|
3648
|
+
const roundedHeight = Math.round(entry.contentRect.height);
|
|
3649
|
+
setControlsHeight((prev) => (roundedHeight !== prev ? roundedHeight : prev));
|
|
3650
|
+
});
|
|
3651
|
+
observer.observe(node);
|
|
3652
|
+
controlsObserverRef.current = observer;
|
|
3653
|
+
}, []);
|
|
3654
|
+
useEffect(() => () => controlsObserverRef.current?.disconnect(), []);
|
|
3655
|
+
const footerFitsOnOneLine = doesFooterFitOnOneLine({
|
|
3656
|
+
columns,
|
|
3657
|
+
model: currentModel,
|
|
3658
|
+
tokensIn: agentLoop.contextUsed,
|
|
3659
|
+
contextWindowOptions,
|
|
3660
|
+
cwd: displayedCwd,
|
|
3661
|
+
gitBranch,
|
|
3662
|
+
thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
|
|
3663
|
+
goalMode,
|
|
3664
|
+
});
|
|
3665
|
+
const chatControlsLayout = getChatControlsLayoutDecision({
|
|
3666
|
+
rows,
|
|
3667
|
+
columns,
|
|
3668
|
+
agentRunning: agentLoop.isRunning,
|
|
3669
|
+
activityVisible,
|
|
3670
|
+
doneStatusVisible,
|
|
3671
|
+
stallStatusVisible,
|
|
3672
|
+
exitPending,
|
|
3673
|
+
footerStatusLayout,
|
|
3674
|
+
taskBarExpanded,
|
|
3675
|
+
goalStatusEntryCount: goalStatusEntries.length,
|
|
3676
|
+
footerFitsOnOneLine,
|
|
3677
|
+
});
|
|
3678
|
+
const stableControlsRows = controlsHeight > 0 ? controlsHeight : chatControlsLayout.controlsRows;
|
|
3679
|
+
const measuredLiveAreaRows = Math.max(MIN_LIVE_AREA_ROWS, rows - stableControlsRows - 1);
|
|
3412
3680
|
const isPixelView = overlay === "pixel";
|
|
3413
|
-
const
|
|
3414
|
-
const
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3681
|
+
const hasLiveAssistantItem = liveItems.some((item) => item.kind === "assistant");
|
|
3682
|
+
const rawVisibleStreamingText = goalModeStateRef.current === "planner" || hasLiveAssistantItem ? "" : agentLoop.streamingText;
|
|
3683
|
+
useEffect(() => {
|
|
3684
|
+
if (!rawVisibleStreamingText) {
|
|
3685
|
+
streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
if (rawVisibleStreamingText === streamedAssistantFlushRef.current.text)
|
|
3689
|
+
return;
|
|
3690
|
+
const alreadyFlushed = streamedAssistantFlushRef.current.flushedChars;
|
|
3691
|
+
const unflushedText = rawVisibleStreamingText.slice(alreadyFlushed);
|
|
3692
|
+
const split = splitAssistantStreamingText(unflushedText);
|
|
3693
|
+
if (split.flushedText.length > 0) {
|
|
3694
|
+
queueFlush([
|
|
3695
|
+
{
|
|
3696
|
+
kind: "assistant",
|
|
3697
|
+
text: split.flushedText,
|
|
3698
|
+
continuation: streamedAssistantFlushRef.current.flushedChars > 0,
|
|
3699
|
+
id: getId(),
|
|
3700
|
+
},
|
|
3701
|
+
]);
|
|
3702
|
+
streamedAssistantFlushRef.current = {
|
|
3703
|
+
flushedChars: alreadyFlushed + split.flushedText.length,
|
|
3704
|
+
text: rawVisibleStreamingText,
|
|
3705
|
+
};
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
streamedAssistantFlushRef.current = {
|
|
3709
|
+
...streamedAssistantFlushRef.current,
|
|
3710
|
+
text: rawVisibleStreamingText,
|
|
3711
|
+
};
|
|
3712
|
+
}, [rawVisibleStreamingText, queueFlush]);
|
|
3713
|
+
const visibleStreamingText = rawVisibleStreamingText.slice(streamedAssistantFlushRef.current.flushedChars);
|
|
3714
|
+
const shouldReserveStreamingSpacing = agentLoop.isRunning &&
|
|
3715
|
+
!hasLiveAssistantItem &&
|
|
3716
|
+
(visibleStreamingText.trim().length > 0 || liveItems.some(isAgentSpacingItem));
|
|
3717
|
+
const shouldTopSpaceStreamingText = shouldTopSpaceStreamingAssistant({
|
|
3718
|
+
visibleStreamingText,
|
|
3719
|
+
lastLiveItem: liveItems.at(-1),
|
|
3720
|
+
lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
|
|
3721
|
+
lastHistoryItem: history.at(-1),
|
|
3418
3722
|
});
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3723
|
+
return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, autoExpandNewest: goalAutoExpand, onClose: () => {
|
|
3724
|
+
goalAutoExpandRef.current = false;
|
|
3725
|
+
setGoalAutoExpand(false);
|
|
3726
|
+
if (props.sessionStore)
|
|
3727
|
+
props.sessionStore.goalAutoExpand = false;
|
|
3728
|
+
closeOverlay();
|
|
3729
|
+
}, onRunGoal: (run) => {
|
|
3730
|
+
const paneTransition = getGoalActivationPaneTransition();
|
|
3731
|
+
goalAutoExpandRef.current = paneTransition.goalAutoExpand;
|
|
3732
|
+
setGoalAutoExpand(paneTransition.goalAutoExpand);
|
|
3733
|
+
setPlanAutoExpand(paneTransition.planAutoExpand);
|
|
3734
|
+
if (props.sessionStore) {
|
|
3735
|
+
props.sessionStore.overlay = paneTransition.overlay;
|
|
3736
|
+
props.sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
|
|
3737
|
+
props.sessionStore.planAutoExpand = paneTransition.planAutoExpand;
|
|
3738
|
+
}
|
|
3739
|
+
if (paneTransition.resetReviewScreen && props.resetUI && props.sessionStore) {
|
|
3740
|
+
props.sessionStore.pendingGoalRun = run;
|
|
3741
|
+
props.resetUI();
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
setOverlay(paneTransition.overlay);
|
|
3745
|
+
startGoalRun(run);
|
|
3746
|
+
}, onVerifyGoal: (run) => {
|
|
3747
|
+
void verifyGoalRun(run);
|
|
3748
|
+
}, onPauseGoal: (run) => {
|
|
3749
|
+
pauseGoalRun(run);
|
|
3750
|
+
}, onRefineGoal: (run, feedback) => {
|
|
3751
|
+
goalAutoExpandRef.current = true;
|
|
3752
|
+
setGoalAutoExpand(true);
|
|
3753
|
+
void (async () => {
|
|
3754
|
+
try {
|
|
3755
|
+
await setGoalModeAndPrompt("setup");
|
|
3756
|
+
await agentLoop.run(`Refine Goal run ${run.id} (${run.title}) based on this user feedback. Update durable Goal setup only, then stop and reopen the Goal pane for review.\n\nFeedback: ${feedback}`);
|
|
3429
3757
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
if (agentLoop.isRunning)
|
|
3434
|
-
props.sessionStore.pendingResetUI = true;
|
|
3435
|
-
}
|
|
3436
|
-
setTaskCount(getTaskCount(props.cwd));
|
|
3437
|
-
setOverlay(null);
|
|
3758
|
+
catch (err) {
|
|
3759
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3760
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3438
3761
|
}
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3762
|
+
finally {
|
|
3763
|
+
await setGoalModeAndPrompt("off");
|
|
3764
|
+
const paneTransition = getGoalSetupFinishedPaneTransition();
|
|
3765
|
+
goalAutoExpandRef.current = paneTransition.goalAutoExpand;
|
|
3766
|
+
setTimeout(() => {
|
|
3767
|
+
const resetUI = props.resetUI;
|
|
3768
|
+
const sessionStore = props.sessionStore;
|
|
3769
|
+
if (shouldResetUIForGoalSetupPaneTransition({
|
|
3770
|
+
hasResetUI: resetUI !== undefined,
|
|
3771
|
+
hasSessionStore: sessionStore !== undefined,
|
|
3772
|
+
}) &&
|
|
3773
|
+
resetUI &&
|
|
3774
|
+
sessionStore) {
|
|
3775
|
+
sessionStore.overlay = paneTransition.overlay;
|
|
3776
|
+
sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
|
|
3777
|
+
sessionStore.planAutoExpand = paneTransition.planAutoExpand;
|
|
3778
|
+
resetUI();
|
|
3779
|
+
return;
|
|
3780
|
+
}
|
|
3781
|
+
setGoalAutoExpand(paneTransition.goalAutoExpand);
|
|
3782
|
+
setPlanAutoExpand(paneTransition.planAutoExpand);
|
|
3783
|
+
setOverlay(paneTransition.overlay);
|
|
3784
|
+
}, 300);
|
|
3449
3785
|
}
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
}, onPauseGoal: (run) => {
|
|
3459
|
-
pauseGoalRun(run);
|
|
3460
|
-
} })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
3461
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3786
|
+
})();
|
|
3787
|
+
} })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
3788
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3789
|
+
props.sessionStore.overlay = null;
|
|
3790
|
+
props.resetUI();
|
|
3791
|
+
}
|
|
3792
|
+
else {
|
|
3793
|
+
if (props.sessionStore) {
|
|
3462
3794
|
props.sessionStore.overlay = null;
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
else {
|
|
3466
|
-
if (props.sessionStore) {
|
|
3467
|
-
props.sessionStore.overlay = null;
|
|
3468
|
-
if (agentLoop.isRunning)
|
|
3469
|
-
props.sessionStore.pendingResetUI = true;
|
|
3470
|
-
}
|
|
3471
|
-
setOverlay(null);
|
|
3795
|
+
if (agentLoop.isRunning)
|
|
3796
|
+
props.sessionStore.pendingResetUI = true;
|
|
3472
3797
|
}
|
|
3473
|
-
}, onFixOne: (entry) => {
|
|
3474
3798
|
setOverlay(null);
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
}
|
|
3494
|
-
setOverlay(null);
|
|
3495
|
-
}
|
|
3496
|
-
} })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
|
|
3497
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3799
|
+
}
|
|
3800
|
+
}, onFixOne: (entry) => {
|
|
3801
|
+
setOverlay(null);
|
|
3802
|
+
startPixelFix(entry.errorId);
|
|
3803
|
+
}, onFixAll: (entries) => {
|
|
3804
|
+
const first = entries.find((e) => e.status === "open") ?? entries[0];
|
|
3805
|
+
if (!first)
|
|
3806
|
+
return;
|
|
3807
|
+
setOverlay(null);
|
|
3808
|
+
setRunAllPixel(true);
|
|
3809
|
+
startPixelFix(first.errorId);
|
|
3810
|
+
} })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
|
|
3811
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3812
|
+
props.sessionStore.overlay = null;
|
|
3813
|
+
props.resetUI();
|
|
3814
|
+
}
|
|
3815
|
+
else {
|
|
3816
|
+
if (props.sessionStore) {
|
|
3498
3817
|
props.sessionStore.overlay = null;
|
|
3499
|
-
|
|
3818
|
+
if (agentLoop.isRunning)
|
|
3819
|
+
props.sessionStore.pendingResetUI = true;
|
|
3500
3820
|
}
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
} })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
|
|
3513
|
-
planOverlayPendingRef.current = false;
|
|
3514
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3821
|
+
setOverlay(null);
|
|
3822
|
+
}
|
|
3823
|
+
} })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
|
|
3824
|
+
planOverlayPendingRef.current = false;
|
|
3825
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3826
|
+
props.sessionStore.overlay = null;
|
|
3827
|
+
props.sessionStore.planAutoExpand = false;
|
|
3828
|
+
props.resetUI();
|
|
3829
|
+
}
|
|
3830
|
+
else {
|
|
3831
|
+
if (props.sessionStore) {
|
|
3515
3832
|
props.sessionStore.overlay = null;
|
|
3516
3833
|
props.sessionStore.planAutoExpand = false;
|
|
3517
|
-
|
|
3834
|
+
if (agentLoop.isRunning)
|
|
3835
|
+
props.sessionStore.pendingResetUI = true;
|
|
3518
3836
|
}
|
|
3519
|
-
|
|
3520
|
-
|
|
3837
|
+
setPlanAutoExpand(false);
|
|
3838
|
+
setOverlay(null);
|
|
3839
|
+
}
|
|
3840
|
+
}, onApprove: (planPath) => {
|
|
3841
|
+
log("INFO", "plan", "Plan approved — transitioning to implementation", {
|
|
3842
|
+
planPath,
|
|
3843
|
+
});
|
|
3844
|
+
planOverlayPendingRef.current = false;
|
|
3845
|
+
void (async () => {
|
|
3846
|
+
try {
|
|
3847
|
+
// Read plan steps for progress tracking — handed to the new
|
|
3848
|
+
// mount via sessionStore.planSteps below.
|
|
3849
|
+
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
3850
|
+
const steps = extractPlanSteps(planContent);
|
|
3851
|
+
// Build the new system prompt with the approved plan baked in.
|
|
3852
|
+
const newPrompt = await rebuildSystemPrompt({
|
|
3853
|
+
approvedPlanPath: planPath,
|
|
3854
|
+
});
|
|
3855
|
+
// Create a new session file BEFORE remount so the new tree
|
|
3856
|
+
// picks it up via sessionStore.sessionPath.
|
|
3857
|
+
let newSessionPath;
|
|
3858
|
+
const sm = sessionManagerRef.current;
|
|
3859
|
+
if (sm) {
|
|
3860
|
+
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
3861
|
+
newSessionPath = s.path;
|
|
3862
|
+
}
|
|
3863
|
+
if (props.resetUI && props.sessionStore) {
|
|
3864
|
+
// Clear the overlay so the new mount lands on the chat,
|
|
3865
|
+
// not back inside the plan pane.
|
|
3521
3866
|
props.sessionStore.overlay = null;
|
|
3522
3867
|
props.sessionStore.planAutoExpand = false;
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
setPlanAutoExpand(false);
|
|
3527
|
-
setOverlay(null);
|
|
3528
|
-
}
|
|
3529
|
-
}, onApprove: (planPath) => {
|
|
3530
|
-
log("INFO", "plan", "Plan approved — transitioning to implementation", {
|
|
3531
|
-
planPath,
|
|
3532
|
-
});
|
|
3533
|
-
planOverlayPendingRef.current = false;
|
|
3534
|
-
void (async () => {
|
|
3535
|
-
try {
|
|
3536
|
-
// Read plan steps for progress tracking — handed to the new
|
|
3537
|
-
// mount via sessionStore.planSteps below.
|
|
3538
|
-
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
3539
|
-
const steps = extractPlanSteps(planContent);
|
|
3540
|
-
// Build the new system prompt with the approved plan baked in.
|
|
3541
|
-
const newPrompt = await rebuildSystemPrompt({
|
|
3542
|
-
planMode: false,
|
|
3868
|
+
props.resetUI({
|
|
3869
|
+
wipeSession: true,
|
|
3870
|
+
messages: [{ role: "system", content: newPrompt }],
|
|
3543
3871
|
approvedPlanPath: planPath,
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
if (sm) {
|
|
3550
|
-
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
3551
|
-
newSessionPath = s.path;
|
|
3552
|
-
}
|
|
3553
|
-
if (props.resetUI && props.sessionStore) {
|
|
3554
|
-
// Clear the overlay so the new mount lands on the chat,
|
|
3555
|
-
// not back inside the plan pane.
|
|
3556
|
-
props.sessionStore.overlay = null;
|
|
3557
|
-
props.sessionStore.planAutoExpand = false;
|
|
3558
|
-
props.resetUI({
|
|
3559
|
-
wipeSession: true,
|
|
3560
|
-
messages: [{ role: "system", content: newPrompt }],
|
|
3561
|
-
approvedPlanPath: planPath,
|
|
3562
|
-
planSteps: steps,
|
|
3563
|
-
sessionPath: newSessionPath,
|
|
3564
|
-
pendingAction: {
|
|
3565
|
-
prompt: "The plan has been approved. Implement it now, following each step in order.",
|
|
3566
|
-
planEvent: { event: "approved" },
|
|
3567
|
-
},
|
|
3568
|
-
});
|
|
3569
|
-
return;
|
|
3570
|
-
}
|
|
3571
|
-
// Fallback path (resetUI not wired — tests). Mutate in place.
|
|
3572
|
-
approvedPlanPathRef.current = planPath;
|
|
3573
|
-
planStepsRef.current = steps;
|
|
3574
|
-
setPlanSteps(steps);
|
|
3575
|
-
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3576
|
-
setLiveItems([]);
|
|
3577
|
-
setPlanAutoExpand(false);
|
|
3578
|
-
setOverlay(null);
|
|
3579
|
-
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
3580
|
-
agentLoop.reset();
|
|
3581
|
-
persistedIndexRef.current = messagesRef.current.length;
|
|
3582
|
-
if (newSessionPath)
|
|
3583
|
-
sessionPathRef.current = newSessionPath;
|
|
3584
|
-
setLiveItems([
|
|
3585
|
-
{
|
|
3586
|
-
kind: "info",
|
|
3587
|
-
text: "Plan approved — starting fresh session for implementation",
|
|
3588
|
-
id: getId(),
|
|
3872
|
+
planSteps: steps,
|
|
3873
|
+
sessionPath: newSessionPath,
|
|
3874
|
+
pendingAction: {
|
|
3875
|
+
prompt: "The plan has been approved. Implement it now, following each step in order.",
|
|
3876
|
+
planEvent: { event: "approved" },
|
|
3589
3877
|
},
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
|
|
3593
|
-
}
|
|
3594
|
-
catch (err) {
|
|
3595
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3596
|
-
log("ERROR", "error", errMsg);
|
|
3597
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3878
|
+
});
|
|
3879
|
+
return;
|
|
3598
3880
|
}
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3881
|
+
// Fallback path (resetUI not wired — tests). Mutate in place.
|
|
3882
|
+
approvedPlanPathRef.current = planPath;
|
|
3883
|
+
planStepsRef.current = steps;
|
|
3884
|
+
setPlanSteps(steps);
|
|
3885
|
+
pendingHistoryFlushRef.current = [];
|
|
3886
|
+
props.terminalHistoryPrinter?.clear();
|
|
3887
|
+
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3888
|
+
setLiveItems([]);
|
|
3889
|
+
setPlanAutoExpand(false);
|
|
3890
|
+
setOverlay(null);
|
|
3891
|
+
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
3892
|
+
agentLoop.reset();
|
|
3893
|
+
persistedIndexRef.current = messagesRef.current.length;
|
|
3894
|
+
if (newSessionPath)
|
|
3895
|
+
sessionPathRef.current = newSessionPath;
|
|
3896
|
+
setLiveItems([
|
|
3897
|
+
{
|
|
3898
|
+
kind: "info",
|
|
3899
|
+
text: "Plan approved — starting fresh session for implementation",
|
|
3900
|
+
id: getId(),
|
|
3613
3901
|
},
|
|
3614
|
-
|
|
3615
|
-
|
|
3902
|
+
]);
|
|
3903
|
+
setDoneStatus(null);
|
|
3904
|
+
await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
|
|
3616
3905
|
}
|
|
3617
|
-
|
|
3618
|
-
setOverlay(null);
|
|
3619
|
-
setDoneStatus(null);
|
|
3620
|
-
setLiveItems((prev) => [
|
|
3621
|
-
...prev,
|
|
3622
|
-
{ kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
|
|
3623
|
-
]);
|
|
3624
|
-
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
3906
|
+
catch (err) {
|
|
3625
3907
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3626
3908
|
log("ERROR", "error", errMsg);
|
|
3627
3909
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3910
|
+
}
|
|
3911
|
+
})();
|
|
3912
|
+
}, onReject: (planPath, feedback) => {
|
|
3913
|
+
planOverlayPendingRef.current = false;
|
|
3914
|
+
const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
|
|
3915
|
+
`Please revise the plan based on this feedback.`;
|
|
3916
|
+
if (props.resetUI && props.sessionStore) {
|
|
3917
|
+
props.sessionStore.overlay = null;
|
|
3918
|
+
props.sessionStore.planAutoExpand = false;
|
|
3919
|
+
// No wipeSession — keep history and messages so the agent picks
|
|
3920
|
+
// up the rejection mid-conversation.
|
|
3921
|
+
props.resetUI({
|
|
3922
|
+
pendingAction: {
|
|
3923
|
+
prompt: rejectionMsg,
|
|
3924
|
+
planEvent: { event: "rejected", detail: feedback },
|
|
3925
|
+
},
|
|
3628
3926
|
});
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
id: getId(),
|
|
3653
|
-
},
|
|
3654
|
-
]);
|
|
3655
|
-
}, 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 ||
|
|
3656
|
-
footerStatusLayout.hasEyesSignals ||
|
|
3657
|
-
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 ||
|
|
3658
|
-
(!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
|
|
3659
|
-
? 1
|
|
3660
|
-
: 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
setPlanAutoExpand(false);
|
|
3930
|
+
setOverlay(null);
|
|
3931
|
+
setDoneStatus(null);
|
|
3932
|
+
setLiveItems((prev) => [
|
|
3933
|
+
...prev,
|
|
3934
|
+
{ kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
|
|
3935
|
+
]);
|
|
3936
|
+
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
3937
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3938
|
+
log("ERROR", "error", errMsg);
|
|
3939
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3940
|
+
});
|
|
3941
|
+
} })) : (_jsxs(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: [_jsxs(Box, { flexDirection: "column", maxHeight: measuredLiveAreaRows, flexGrow: 0, flexShrink: 1, overflowY: "hidden", children: [liveItems.map((item, index, items) => renderItem(item, index, items)), _jsx(StreamingArea, { isRunning: agentLoop.isRunning, streamingText: visibleStreamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, reserveSpacing: shouldReserveStreamingSpacing, renderMarkdown: renderMarkdown, availableTerminalHeight: measuredLiveAreaRows, assistantMarginTop: shouldTopSpaceStreamingText ? 1 : 0, continuation: streamedAssistantFlushRef.current.flushedChars > 0 })] }), _jsxs(Box, { ref: mainControlsRef, flexDirection: "column", flexShrink: 0, flexGrow: 0, children: [agentLoop.queuedCount > 0 && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.warning, bold: true, children: "• " }), _jsxs(Text, { color: theme.textDim, children: [agentLoop.queuedCount, " message", agentLoop.queuedCount > 1 ? "s" : "", " queued"] })] })), _jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: theme.textDim, width: columns, height: 0 }), _jsx(Box, { paddingLeft: 1, paddingRight: 1, width: columns, children: statusSlotVisible ? (activityVisible ? (_jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, thinkingMs: agentLoop.thinkingMs, isThinking: agentLoop.isThinking, thinkingEnabled: thinkingEnabled, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, userMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, staticDisplay: true })) : stallStatusVisible ? (_jsx(Text, { color: theme.warning, wrap: "truncate", children: "⚠ API provider stream interrupted — retries exhausted. Your conversation is preserved." })) : doneStatus ? (_jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] })) : (_jsxs(Text, { children: [_jsx(Text, { color: theme.commandColor, children: "⠿ " }), _jsx(Text, { color: theme.textDim, children: "Ready to go.." }), !renderMarkdown && (_jsx(Text, { color: theme.warning, children: " · raw markdown mode" }))] }))) : (_jsxs(Text, { children: [_jsx(Text, { color: theme.commandColor, children: "⠿ " }), _jsx(Text, { color: theme.textDim, children: "Ready to go.." })] })) })] }), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleGoal: () => {
|
|
3942
|
+
openOverlay("goal");
|
|
3943
|
+
}, onToggleSkills: () => {
|
|
3944
|
+
openOverlay("skills");
|
|
3945
|
+
}, onTogglePixel: () => {
|
|
3946
|
+
openOverlay("pixel");
|
|
3947
|
+
}, onToggleMarkdown: () => {
|
|
3948
|
+
setRenderMarkdown((prev) => !prev);
|
|
3949
|
+
}, cwd: props.cwd, commands: allCommands }), 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, goalMode: goalMode, exitPending: exitPending, renderMarkdown: renderMarkdown }), !exitPending && _jsx(GoalStatusBar, { entries: goalStatusEntries })] })), (footerStatusLayout.hasBackgroundTasks || 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.hasUpdateNotice && (_jsx(Box, { paddingLeft: footerStatusLayout.stack || !footerStatusLayout.hasBackgroundTasks ? 1 : 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] })] })) }));
|
|
3661
3950
|
}
|
|
3662
3951
|
function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
|
|
3663
3952
|
const status = enabled ? "on" : "off";
|