@kenkaiiii/ggcoder 4.3.211 → 4.3.213
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 +23 -0
- package/dist/core/goal-prerequisites.d.ts.map +1 -0
- package/dist/core/goal-prerequisites.js +114 -0
- package/dist/core/goal-prerequisites.js.map +1 -0
- package/dist/core/goal-prerequisites.test.d.ts +2 -0
- package/dist/core/goal-prerequisites.test.d.ts.map +1 -0
- package/dist/core/goal-prerequisites.test.js +118 -0
- package/dist/core/goal-prerequisites.test.js.map +1 -0
- 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 +161 -38
- package/dist/core/goal-store.js.map +1 -1
- package/dist/core/goal-store.test.js +33 -8
- 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 -845
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +40 -74
- 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 +385 -35
- package/dist/tools/goals.js.map +1 -1
- package/dist/tools/goals.test.js +389 -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 +146 -30
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +1202 -910
- 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 +181 -13
- 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 +8 -0
- package/dist/ui/goal-events.d.ts.map +1 -1
- package/dist/ui/goal-events.js +28 -8
- package/dist/ui/goal-events.js.map +1 -1
- package/dist/ui/goal-events.test.js +40 -2
- package/dist/ui/goal-events.test.js.map +1 -1
- package/dist/ui/goal-lifecycle-orchestration.test.js +127 -34
- package/dist/ui/goal-lifecycle-orchestration.test.js.map +1 -1
- package/dist/ui/goal-overlay.test.js +122 -44
- 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 -5
- 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,10 @@ 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";
|
|
63
|
+
import { runGoalPrerequisiteChecks } from "../core/goal-prerequisites.js";
|
|
64
64
|
import { runGoalVerifierCommand } from "../core/goal-verifier.js";
|
|
65
65
|
import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
|
|
66
66
|
import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
|
|
@@ -116,6 +116,56 @@ export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS,
|
|
|
116
116
|
fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
|
|
117
117
|
};
|
|
118
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
|
+
}
|
|
119
169
|
export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
|
|
120
170
|
if (inputImages.length === 0)
|
|
121
171
|
return text;
|
|
@@ -155,14 +205,13 @@ export function buildUserContentWithAttachments(text, inputImages, modelSupports
|
|
|
155
205
|
// If only text parts remain after stripping images, simplify to plain string
|
|
156
206
|
return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
|
|
157
207
|
}
|
|
158
|
-
/** Tools that get aggregated into a single compact group when
|
|
208
|
+
/** Tools that get aggregated into a single compact group when possible. */
|
|
159
209
|
const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
160
210
|
const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
|
|
161
211
|
/**
|
|
162
|
-
* Cap memory by replacing old
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* 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.
|
|
166
215
|
*/
|
|
167
216
|
const MAX_LIVE_HISTORY = 200;
|
|
168
217
|
function compactHistory(items) {
|
|
@@ -192,63 +241,30 @@ function summarizeGoalCompletion(summary) {
|
|
|
192
241
|
function formatGoalWorkerFinishedTitle(taskTitle, status) {
|
|
193
242
|
return status === "done" ? `Done: ${taskTitle}` : `Failed: ${taskTitle}`;
|
|
194
243
|
}
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const normalized = value.replace(/\s+/g, " ").trim();
|
|
203
|
-
if (normalized.length <= maxLength)
|
|
204
|
-
return normalized;
|
|
205
|
-
return `${normalized.slice(0, maxLength - 1)}…`;
|
|
206
|
-
}
|
|
207
|
-
export function buildGoalSummaryRows(run) {
|
|
208
|
-
const rows = [];
|
|
209
|
-
const doneTasks = countGoalTasksByStatus(run.tasks, "done");
|
|
210
|
-
const failedTasks = countGoalTasksByStatus(run.tasks, "failed");
|
|
211
|
-
const blockedTasks = countGoalTasksByStatus(run.tasks, "blocked");
|
|
212
|
-
const taskSuffix = [
|
|
213
|
-
failedTasks > 0 ? `${failedTasks} failed` : undefined,
|
|
214
|
-
blockedTasks > 0 ? `${blockedTasks} blocked` : undefined,
|
|
215
|
-
].filter((item) => item !== undefined);
|
|
216
|
-
rows.push({
|
|
217
|
-
label: "Tasks",
|
|
218
|
-
value: run.tasks.length > 0 ? `${doneTasks}/${run.tasks.length} done` : "none",
|
|
219
|
-
...(taskSuffix.length > 0 ? { detail: taskSuffix.join(", ") } : {}),
|
|
220
|
-
});
|
|
221
|
-
const verifierResult = run.verifier?.lastResult;
|
|
222
|
-
const verifierDetail = firstText([verifierResult?.outputPath, run.verifier?.command]);
|
|
223
|
-
rows.push({
|
|
224
|
-
label: "Verifier",
|
|
225
|
-
value: verifierResult?.status ?? (run.verifier?.command ? "ready" : "missing"),
|
|
226
|
-
...(verifierDetail ? { detail: truncateGoalSummary(verifierDetail) } : {}),
|
|
227
|
-
});
|
|
228
|
-
const latestEvidence = run.evidence.at(-1);
|
|
229
|
-
rows.push({
|
|
230
|
-
label: "Evidence",
|
|
231
|
-
value: `${run.evidence.length} recorded`,
|
|
232
|
-
...(latestEvidence
|
|
233
|
-
? { detail: truncateGoalSummary(latestEvidence.path ?? latestEvidence.label) }
|
|
234
|
-
: {}),
|
|
235
|
-
});
|
|
236
|
-
if (run.status === "blocked" || run.status === "paused" || run.blockers.length > 0) {
|
|
237
|
-
rows.push({
|
|
238
|
-
label: run.status === "paused" ? "Paused on" : "Blocked on",
|
|
239
|
-
value: truncateGoalSummary(goalHasBlockingPrerequisites(run)
|
|
240
|
-
? formatGoalBlockingPrerequisites(run)
|
|
241
|
-
: (run.blockers[0] ?? "manual review"), 110),
|
|
242
|
-
});
|
|
244
|
+
function goalProgressLoaderStatus(item) {
|
|
245
|
+
if (item.status === "failed" || item.status === "fail" || item.status === "blocked")
|
|
246
|
+
return "error";
|
|
247
|
+
if (item.phase === "worker_finished" ||
|
|
248
|
+
item.phase === "verifier_finished" ||
|
|
249
|
+
item.phase === "terminal") {
|
|
250
|
+
return "done";
|
|
243
251
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
252
|
+
return "running";
|
|
253
|
+
}
|
|
254
|
+
function goalProgressColor(item, theme) {
|
|
255
|
+
const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
|
|
256
|
+
if (isError)
|
|
257
|
+
return theme.error;
|
|
258
|
+
if (item.phase === "worker_finished" || item.phase === "terminal")
|
|
259
|
+
return theme.success;
|
|
260
|
+
if (item.phase === "verifier_finished" || item.phase === "verifier_started")
|
|
261
|
+
return theme.accent;
|
|
262
|
+
if (item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working") {
|
|
263
|
+
return theme.secondary;
|
|
250
264
|
}
|
|
251
|
-
|
|
265
|
+
if (item.phase === "continuing")
|
|
266
|
+
return theme.warning;
|
|
267
|
+
return theme.primary;
|
|
252
268
|
}
|
|
253
269
|
function goalTerminalProgressId(run) {
|
|
254
270
|
return `goal-terminal-${run.id}`;
|
|
@@ -261,10 +277,20 @@ function goalTerminalRunIdFromItem(item) {
|
|
|
261
277
|
return item.id.slice("goal-terminal-".length);
|
|
262
278
|
}
|
|
263
279
|
function goalProgressMatchesDraft(item, draft) {
|
|
264
|
-
return (item.
|
|
280
|
+
return (item.phase === draft.phase &&
|
|
281
|
+
item.title === draft.title &&
|
|
265
282
|
item.detail === draft.detail &&
|
|
283
|
+
item.workerId === draft.workerId &&
|
|
266
284
|
item.status === draft.status &&
|
|
267
|
-
JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? [])
|
|
285
|
+
JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []) &&
|
|
286
|
+
JSON.stringify(item.summarySections ?? []) === JSON.stringify(draft.summarySections ?? []));
|
|
287
|
+
}
|
|
288
|
+
export function appendGoalProgressDraft(items, draft, makeId) {
|
|
289
|
+
const previous = items.at(-1);
|
|
290
|
+
if (previous?.kind === "goal_progress" && goalProgressMatchesDraft(previous, draft)) {
|
|
291
|
+
return items;
|
|
292
|
+
}
|
|
293
|
+
return [...items, { ...draft, id: makeId() }];
|
|
268
294
|
}
|
|
269
295
|
/**
|
|
270
296
|
* Reconcile terminal Goal cards that are already visible in this UI session.
|
|
@@ -276,6 +302,16 @@ function goalProgressMatchesDraft(item, draft) {
|
|
|
276
302
|
* event append that card first, then use this helper to tombstone stale older
|
|
277
303
|
* cards for the same run.
|
|
278
304
|
*/
|
|
305
|
+
export function getNextGeneratedItemId(items) {
|
|
306
|
+
let max = -1;
|
|
307
|
+
for (const item of items) {
|
|
308
|
+
const raw = item.id.startsWith("ui-") ? item.id.slice(3) : item.id;
|
|
309
|
+
const n = Number(raw);
|
|
310
|
+
if (Number.isInteger(n) && n >= 0 && n > max)
|
|
311
|
+
max = n;
|
|
312
|
+
}
|
|
313
|
+
return max + 1;
|
|
314
|
+
}
|
|
279
315
|
export function completedItemsWithDurableGoalTerminalProgress(items, runs) {
|
|
280
316
|
const runIds = new Set(runs.map((run) => run.id));
|
|
281
317
|
const terminalByRun = new Map(runs
|
|
@@ -303,8 +339,9 @@ export function formatGoalTerminalProgress(run) {
|
|
|
303
339
|
kind: "goal_progress",
|
|
304
340
|
phase: "terminal",
|
|
305
341
|
title: `Goal passed: ${run.title}`,
|
|
306
|
-
detail:
|
|
342
|
+
detail: goalPassedDetail(run),
|
|
307
343
|
summaryRows: buildGoalSummaryRows(run),
|
|
344
|
+
summarySections: buildGoalFinalSummarySections(run),
|
|
308
345
|
status: run.status,
|
|
309
346
|
};
|
|
310
347
|
case "failed":
|
|
@@ -343,31 +380,187 @@ export function formatGoalTerminalProgress(run) {
|
|
|
343
380
|
return null;
|
|
344
381
|
}
|
|
345
382
|
}
|
|
346
|
-
export function shouldHideHistoryForOverlayView(
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
return false;
|
|
383
|
+
export function shouldHideHistoryForOverlayView(isOverlayView, _isAgentRunning) {
|
|
384
|
+
// Overlay panes are standalone full-screen states. Finalized chat rows are
|
|
385
|
+
// printed outside Ink, so overlays should never replay transcript UI behind them.
|
|
386
|
+
return isOverlayView;
|
|
351
387
|
}
|
|
352
388
|
export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
|
|
353
|
-
return isAgentRunning &&
|
|
389
|
+
return isAgentRunning && overlayPane === "goal";
|
|
390
|
+
}
|
|
391
|
+
export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender: _stabilizeOverlayPaneRerender, }) {
|
|
392
|
+
return shouldHideHistoryForOverlay;
|
|
393
|
+
}
|
|
394
|
+
export function getDoneFlushDecision({ planOverlayPending, goalMode, goalAutoExpand, }) {
|
|
395
|
+
return {
|
|
396
|
+
showDoneStatus: !(planOverlayPending ||
|
|
397
|
+
goalMode === "planner" ||
|
|
398
|
+
goalMode === "setup" ||
|
|
399
|
+
goalAutoExpand),
|
|
400
|
+
flushLiveItems: true,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
export function getGoalSetupFinishedPaneTransition() {
|
|
404
|
+
return {
|
|
405
|
+
overlay: "goal",
|
|
406
|
+
goalAutoExpand: true,
|
|
407
|
+
planAutoExpand: false,
|
|
408
|
+
suppressDoneStatus: true,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
export function getGoalSetupPaneTransitionAfterRun({ isGoalSetupCommand, setupPanePending, }) {
|
|
412
|
+
return isGoalSetupCommand && setupPanePending ? getGoalSetupFinishedPaneTransition() : null;
|
|
413
|
+
}
|
|
414
|
+
export function shouldResetUIForSetupPaneTransition({ hasResetUI, hasSessionStore, }) {
|
|
415
|
+
// Opening a review pane is a full-screen state transition. A bare React state
|
|
416
|
+
// flip hides history in the virtual tree, but it does not reset Ink/log-update's
|
|
417
|
+
// already-written terminal frame, so the pane can render below prior chat.
|
|
418
|
+
return hasResetUI && hasSessionStore;
|
|
354
419
|
}
|
|
355
|
-
export
|
|
356
|
-
|
|
420
|
+
export const shouldResetUIForGoalSetupPaneTransition = shouldResetUIForSetupPaneTransition;
|
|
421
|
+
export function getGoalActivationPaneTransition() {
|
|
422
|
+
return { overlay: null, goalAutoExpand: false, planAutoExpand: false, resetReviewScreen: true };
|
|
423
|
+
}
|
|
424
|
+
export function getGoalContinuationChoiceKey({ runId, decision, }) {
|
|
425
|
+
switch (decision.kind) {
|
|
426
|
+
case "create_task":
|
|
427
|
+
return `${runId}:create_task:${decision.title}:${decision.prompt}`;
|
|
428
|
+
case "start_worker":
|
|
429
|
+
case "pause":
|
|
430
|
+
return `${runId}:${decision.kind}:${decision.task.id}:${decision.attempts}`;
|
|
431
|
+
case "run_verifier":
|
|
432
|
+
return `${runId}:run_verifier:${decision.command}`;
|
|
433
|
+
case "blocked":
|
|
434
|
+
case "complete":
|
|
435
|
+
case "terminal":
|
|
436
|
+
case "wait":
|
|
437
|
+
return `${runId}:${decision.kind}:${decision.reason}`;
|
|
438
|
+
}
|
|
357
439
|
}
|
|
358
|
-
export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, }) {
|
|
359
|
-
const
|
|
440
|
+
export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, hasParagraphBreakLiveUserMessage = false, }) {
|
|
441
|
+
const shouldPreserveStatic = isUserScrolled || hasTallLiveUserMessage || hasParagraphBreakLiveUserMessage;
|
|
442
|
+
const shouldAutoFollow = !(isUserScrolled || hasTallLiveUserMessage);
|
|
360
443
|
return {
|
|
361
|
-
preserveStatic:
|
|
362
|
-
autoFollow:
|
|
444
|
+
preserveStatic: shouldPreserveStatic && hasNewOutput,
|
|
445
|
+
autoFollow: shouldAutoFollow,
|
|
363
446
|
};
|
|
364
447
|
}
|
|
448
|
+
export function nextGoalModeAfterAgentDone({ currentMode, runningGoalIds, queuedSyntheticEvents, activeContinuationFlights = 0, wasGoalSetupTurn, }) {
|
|
449
|
+
if (wasGoalSetupTurn)
|
|
450
|
+
return "off";
|
|
451
|
+
if (currentMode === "planner" || currentMode === "setup")
|
|
452
|
+
return currentMode;
|
|
453
|
+
if (queuedSyntheticEvents > 0)
|
|
454
|
+
return "coordinator";
|
|
455
|
+
if (activeContinuationFlights > 0)
|
|
456
|
+
return "coordinator";
|
|
457
|
+
if (currentMode === "coordinator" && runningGoalIds > 0)
|
|
458
|
+
return "coordinator";
|
|
459
|
+
return "off";
|
|
460
|
+
}
|
|
461
|
+
export function hasParagraphBreakLiveUserMessage(text) {
|
|
462
|
+
return /\n[ \t]*\n/.test(text);
|
|
463
|
+
}
|
|
365
464
|
export function isTallLiveUserMessage(text, rows) {
|
|
366
465
|
return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
|
|
367
466
|
}
|
|
368
467
|
export function getStaticHistoryKey({ resizeKey }) {
|
|
369
468
|
return `${resizeKey}`;
|
|
370
469
|
}
|
|
470
|
+
const MIN_LIVE_AREA_ROWS = 3;
|
|
471
|
+
const INPUT_AREA_ROWS = 3;
|
|
472
|
+
const STATUS_SLOT_ROWS = 2;
|
|
473
|
+
const FOOTER_ONE_LINE_ROWS = 1;
|
|
474
|
+
const FOOTER_TWO_LINE_ROWS = 2;
|
|
475
|
+
const GOAL_STATUS_ROWS = 1;
|
|
476
|
+
const COLLAPSED_FOOTER_STATUS_ROWS = 1;
|
|
477
|
+
const MAX_EXPANDED_BACKGROUND_TASK_ROWS = 7;
|
|
478
|
+
function isAgentSpacingKind(kind) {
|
|
479
|
+
return [
|
|
480
|
+
"assistant",
|
|
481
|
+
"queued",
|
|
482
|
+
"goal_progress",
|
|
483
|
+
"tool_start",
|
|
484
|
+
"tool_done",
|
|
485
|
+
"tool_group",
|
|
486
|
+
"server_tool_start",
|
|
487
|
+
"server_tool_done",
|
|
488
|
+
"subagent_group",
|
|
489
|
+
].includes(kind);
|
|
490
|
+
}
|
|
491
|
+
function isToolBoundaryKind(kind) {
|
|
492
|
+
return [
|
|
493
|
+
"goal_progress",
|
|
494
|
+
"tool_start",
|
|
495
|
+
"tool_done",
|
|
496
|
+
"tool_group",
|
|
497
|
+
"server_tool_start",
|
|
498
|
+
"server_tool_done",
|
|
499
|
+
"subagent_group",
|
|
500
|
+
].includes(kind);
|
|
501
|
+
}
|
|
502
|
+
function isAgentSpacingItem(item) {
|
|
503
|
+
return isAgentSpacingKind(item.kind);
|
|
504
|
+
}
|
|
505
|
+
export function shouldTopSpaceAfterPrintedAgentBoundary({ currentKind, previousLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
|
|
506
|
+
const needsExternalSpacing = [
|
|
507
|
+
"goal_progress",
|
|
508
|
+
"tool_start",
|
|
509
|
+
"tool_group",
|
|
510
|
+
"assistant",
|
|
511
|
+
"queued",
|
|
512
|
+
].includes(currentKind);
|
|
513
|
+
if (!needsExternalSpacing)
|
|
514
|
+
return false;
|
|
515
|
+
if (previousLiveItem !== undefined)
|
|
516
|
+
return false;
|
|
517
|
+
const previousKind = lastPendingHistoryItem?.kind ?? lastHistoryItem?.kind;
|
|
518
|
+
return previousKind !== undefined && isAgentSpacingKind(previousKind);
|
|
519
|
+
}
|
|
520
|
+
export function shouldTopSpaceAssistantAfterToolBoundary({ text, previousLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
|
|
521
|
+
if (text.trim().length === 0)
|
|
522
|
+
return false;
|
|
523
|
+
if (shouldTopSpaceAfterPrintedAgentBoundary({
|
|
524
|
+
currentKind: "assistant",
|
|
525
|
+
previousLiveItem,
|
|
526
|
+
lastPendingHistoryItem,
|
|
527
|
+
lastHistoryItem,
|
|
528
|
+
})) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
const previousKind = previousLiveItem?.kind;
|
|
532
|
+
return previousKind !== undefined && isToolBoundaryKind(previousKind);
|
|
533
|
+
}
|
|
534
|
+
export function shouldTopSpaceStreamingAssistant({ visibleStreamingText, lastLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
|
|
535
|
+
return shouldTopSpaceAssistantAfterToolBoundary({
|
|
536
|
+
text: visibleStreamingText,
|
|
537
|
+
previousLiveItem: lastLiveItem,
|
|
538
|
+
lastPendingHistoryItem,
|
|
539
|
+
lastHistoryItem,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
export function getChatControlsLayoutDecision({ rows, agentRunning, activityVisible, doneStatusVisible, stallStatusVisible, exitPending, footerStatusLayout, taskBarExpanded, goalStatusEntryCount, footerFitsOnOneLine, }) {
|
|
543
|
+
const statusRows = activityVisible || stallStatusVisible || doneStatusVisible || agentRunning
|
|
544
|
+
? STATUS_SLOT_ROWS
|
|
545
|
+
: 0;
|
|
546
|
+
const footerRows = exitPending || footerFitsOnOneLine ? FOOTER_ONE_LINE_ROWS : FOOTER_TWO_LINE_ROWS;
|
|
547
|
+
const goalRows = !exitPending && goalStatusEntryCount > 0 ? GOAL_STATUS_ROWS : 0;
|
|
548
|
+
const footerStatusRows = footerStatusLayout.stack
|
|
549
|
+
? Number(footerStatusLayout.hasBackgroundTasks) + Number(footerStatusLayout.hasUpdateNotice)
|
|
550
|
+
: footerStatusLayout.hasBackgroundTasks || footerStatusLayout.hasUpdateNotice
|
|
551
|
+
? COLLAPSED_FOOTER_STATUS_ROWS
|
|
552
|
+
: 0;
|
|
553
|
+
const expandedTaskRows = taskBarExpanded && footerStatusLayout.hasBackgroundTasks
|
|
554
|
+
? MAX_EXPANDED_BACKGROUND_TASK_ROWS - COLLAPSED_FOOTER_STATUS_ROWS
|
|
555
|
+
: 0;
|
|
556
|
+
const controlsRows = statusRows + INPUT_AREA_ROWS + footerRows + goalRows + footerStatusRows + expandedTaskRows;
|
|
557
|
+
const maxControlsRows = Math.max(1, rows - MIN_LIVE_AREA_ROWS);
|
|
558
|
+
const boundedControlsRows = Math.min(controlsRows, maxControlsRows);
|
|
559
|
+
return {
|
|
560
|
+
controlsRows: boundedControlsRows,
|
|
561
|
+
liveAreaRows: Math.max(MIN_LIVE_AREA_ROWS, rows - boundedControlsRows),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
371
564
|
// flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
|
|
372
565
|
/** Check whether an item is still active (running spinner, pending result). */
|
|
373
566
|
export function isActiveItem(item) {
|
|
@@ -386,15 +579,17 @@ export function isActiveItem(item) {
|
|
|
386
579
|
}
|
|
387
580
|
}
|
|
388
581
|
/**
|
|
389
|
-
* Partition live items into completed (flushable to
|
|
582
|
+
* Partition live items into completed (flushable to finalized history) and still-active.
|
|
390
583
|
* Completed items precede active ones — we flush the longest contiguous prefix
|
|
391
584
|
* of completed items to keep ordering stable.
|
|
392
585
|
*/
|
|
393
|
-
function partitionCompleted(items) {
|
|
394
|
-
// Find the first active item — everything before it is safe to flush
|
|
586
|
+
export function partitionCompleted(items) {
|
|
587
|
+
// Find the first active item — everything before it is safe to flush as a
|
|
588
|
+
// single chronological prefix. Splitting assistant text out of that prefix
|
|
589
|
+
// lets later tool rows print to scrollback above the message that introduced
|
|
590
|
+
// them, so keep the prefix intact.
|
|
395
591
|
const firstActiveIdx = items.findIndex(isActiveItem);
|
|
396
592
|
if (firstActiveIdx === -1) {
|
|
397
|
-
// All items are completed
|
|
398
593
|
return { flushed: items, remaining: [] };
|
|
399
594
|
}
|
|
400
595
|
if (firstActiveIdx === 0) {
|
|
@@ -405,6 +600,29 @@ function partitionCompleted(items) {
|
|
|
405
600
|
remaining: items.slice(firstActiveIdx),
|
|
406
601
|
};
|
|
407
602
|
}
|
|
603
|
+
function normalizeAssistantText(text) {
|
|
604
|
+
return stripDoneMarkers(text).trim();
|
|
605
|
+
}
|
|
606
|
+
function isSameAssistantText(item, text) {
|
|
607
|
+
return item.kind === "assistant" && normalizeAssistantText(item.text) === text;
|
|
608
|
+
}
|
|
609
|
+
export function pinStreamingTextBeforeToolBoundary({ items, visibleStreamingText, thinking, thinkingMs, makeId, }) {
|
|
610
|
+
const text = normalizeAssistantText(visibleStreamingText);
|
|
611
|
+
if (text.length === 0)
|
|
612
|
+
return items;
|
|
613
|
+
if (items.some((item) => item.kind === "assistant"))
|
|
614
|
+
return items;
|
|
615
|
+
return [
|
|
616
|
+
...items,
|
|
617
|
+
{
|
|
618
|
+
kind: "assistant",
|
|
619
|
+
text,
|
|
620
|
+
thinking: thinking.length > 0 ? thinking : undefined,
|
|
621
|
+
thinkingMs: thinking.length > 0 ? thinkingMs : undefined,
|
|
622
|
+
id: makeId(),
|
|
623
|
+
},
|
|
624
|
+
];
|
|
625
|
+
}
|
|
408
626
|
// ── Duration summary ─────────────────────────────────────
|
|
409
627
|
function formatDuration(ms) {
|
|
410
628
|
const totalSec = Math.round(ms / 1000);
|
|
@@ -452,8 +670,8 @@ function pickDurationVerb(toolsUsed) {
|
|
|
452
670
|
return "Ran & investigated for";
|
|
453
671
|
if (has("bash"))
|
|
454
672
|
return "Executed commands for";
|
|
455
|
-
if (hasAny("
|
|
456
|
-
return "Managed
|
|
673
|
+
if (hasAny("task-output", "task-stop"))
|
|
674
|
+
return "Managed background processes for";
|
|
457
675
|
if (has("grep") && has("read"))
|
|
458
676
|
return "Investigated for";
|
|
459
677
|
if (has("grep") && has("find"))
|
|
@@ -481,65 +699,16 @@ function pickDurationVerb(toolsUsed) {
|
|
|
481
699
|
];
|
|
482
700
|
return phrases[Math.floor(Math.random() * phrases.length)];
|
|
483
701
|
}
|
|
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
702
|
// ── App Component ──────────────────────────────────────────
|
|
530
703
|
export function App(props) {
|
|
531
704
|
const theme = useTheme();
|
|
532
705
|
const switchTheme = useSetTheme();
|
|
533
|
-
const {
|
|
706
|
+
const { write: writeStdout } = useStdout();
|
|
707
|
+
const { columns, rows } = useTerminalSize();
|
|
534
708
|
// Hoisted before terminal title hook so it can reference them
|
|
535
709
|
const [lastUserMessage, setLastUserMessage] = useState("");
|
|
536
710
|
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;
|
|
711
|
+
const [goalMode, setGoalMode] = useState(props.sessionStore?.goalMode ?? props.goalModeRef?.current ?? "off");
|
|
543
712
|
// Terminal title — updated later after agentLoop is created
|
|
544
713
|
// (hoisted here so the hook is always called in the same order)
|
|
545
714
|
const [titleRunning, setTitleRunning] = useState(false);
|
|
@@ -549,14 +718,11 @@ export function App(props) {
|
|
|
549
718
|
isRunning: titleRunning,
|
|
550
719
|
sessionTitle,
|
|
551
720
|
});
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
// so slice(0) returns the full array regardless of length.
|
|
721
|
+
// Completed transcript rows are kept as durable session data but are no longer
|
|
722
|
+
// rendered through Ink history. They are serialized once into real terminal
|
|
723
|
+
// scrollback via terminalHistoryPrinter, while Ink owns only live rows and
|
|
724
|
+
// controls. This avoids Static/log-update replay drift on resize/remount.
|
|
557
725
|
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
726
|
const stored = props.sessionStore?.history;
|
|
561
727
|
if (stored && stored.length > 0)
|
|
562
728
|
return stored;
|
|
@@ -567,24 +733,19 @@ export function App(props) {
|
|
|
567
733
|
});
|
|
568
734
|
// Items from the current/last turn — rendered in the live area so they stay visible.
|
|
569
735
|
// Seed from sessionStore so Goal progress/completion rows and other live output
|
|
570
|
-
// survive pane/overlay/resize remounts before they are
|
|
736
|
+
// survive pane/overlay/resize remounts before they are finalized.
|
|
571
737
|
const [liveItems, setLiveItems] = useState(() => props.sessionStore?.liveItems ?? []);
|
|
572
738
|
// overlay seeded from sessionStore (lives across remount). Falls back to
|
|
573
739
|
// props.initialOverlay (CLI launched with one), then null.
|
|
574
740
|
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
741
|
const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
|
|
578
|
-
const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
579
742
|
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
743
|
const agentRunningRef = useRef(false);
|
|
586
744
|
const runningGoalIdsRef = useRef(new Set());
|
|
587
745
|
const activeVerifierRunIdsRef = useRef(new Set());
|
|
746
|
+
const queuedGoalSyntheticEventsRef = useRef(0);
|
|
747
|
+
const goalContinuationFlightsRef = useRef(new Set());
|
|
748
|
+
const goalContinuationRecentChoicesRef = useRef(new Map());
|
|
588
749
|
const startGoalRunRef = useRef(() => { });
|
|
589
750
|
const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
|
|
590
751
|
const currentPixelFixRef = useRef(null);
|
|
@@ -594,12 +755,14 @@ export function App(props) {
|
|
|
594
755
|
const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
|
|
595
756
|
// Suppress "done" status when a plan overlay is about to open
|
|
596
757
|
const planOverlayPendingRef = useRef(false);
|
|
758
|
+
const goalSetupPanePendingRef = useRef(false);
|
|
597
759
|
const [gitBranch, setGitBranch] = useState(null);
|
|
598
760
|
const [currentModel, setCurrentModel] = useState(props.model);
|
|
599
761
|
const [currentProvider, setCurrentProvider] = useState(props.provider);
|
|
600
762
|
const [currentTools, setCurrentTools] = useState(props.tools);
|
|
601
763
|
const currentToolsRef = useRef(props.tools);
|
|
602
764
|
const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
|
|
765
|
+
const [renderMarkdown, setRenderMarkdown] = useState(true);
|
|
603
766
|
const messagesRef = useRef(props.sessionStore?.messages ?? props.messages);
|
|
604
767
|
const repoMapInjectionEnabledRef = useRef(true);
|
|
605
768
|
const repoMapDirtyRef = useRef(true);
|
|
@@ -608,10 +771,12 @@ export function App(props) {
|
|
|
608
771
|
const repoMapChangedCountRef = useRef(0);
|
|
609
772
|
const repoMapCacheRef = useRef(createRepoMapCache());
|
|
610
773
|
const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
|
|
774
|
+
const [goalAutoExpand, setGoalAutoExpand] = useState(props.sessionStore?.goalAutoExpand ?? false);
|
|
775
|
+
const goalAutoExpandRef = useRef(props.sessionStore?.goalAutoExpand ?? false);
|
|
611
776
|
const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
|
|
612
777
|
const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
|
|
613
778
|
const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
|
|
614
|
-
const
|
|
779
|
+
const goalModeStateRef = useRef(goalMode);
|
|
615
780
|
// Stuck-guard for the plan-continuation follow-up nudge. Tracks how many
|
|
616
781
|
// times we've nudged the agent to continue the same step. Reset whenever a
|
|
617
782
|
// new [DONE:n] marker advances progress (see onTurnText). Caps at 2 nudges
|
|
@@ -619,23 +784,14 @@ export function App(props) {
|
|
|
619
784
|
const followUpNudgesRef = useRef({ step: 0, count: 0 });
|
|
620
785
|
// Seed the per-item ID counter so it doesn't collide with IDs already in
|
|
621
786
|
// 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
|
-
})());
|
|
787
|
+
// (resize, overlay toggle, goal pane open, etc.) starts the counter at 0
|
|
788
|
+
// and new items generate ids "ui-0", "ui-1", "ui-2"… that collide with
|
|
789
|
+
// the same ids from the previous mount, triggering React's duplicate-key
|
|
790
|
+
// warning and causing duplicate/omitted renders.
|
|
791
|
+
const nextIdRef = useRef(getNextGeneratedItemId([
|
|
792
|
+
...(props.sessionStore?.history ?? props.initialHistory ?? []),
|
|
793
|
+
...(props.sessionStore?.liveItems ?? []),
|
|
794
|
+
]));
|
|
639
795
|
const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
|
|
640
796
|
const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
|
|
641
797
|
const persistedIndexRef = useRef(messagesRef.current.length);
|
|
@@ -666,8 +822,11 @@ export function App(props) {
|
|
|
666
822
|
*/
|
|
667
823
|
const triggerAutoSetupRef = useRef(async () => { });
|
|
668
824
|
const getId = () => `ui-${nextIdRef.current++}`;
|
|
825
|
+
const appendGoalAgentTransition = useCallback((text) => {
|
|
826
|
+
setLiveItems((prev) => [...prev, { kind: "goal_agent_transition", text, id: getId() }]);
|
|
827
|
+
}, []);
|
|
669
828
|
const appendGoalProgress = useCallback((item) => {
|
|
670
|
-
setLiveItems((prev) =>
|
|
829
|
+
setLiveItems((prev) => appendGoalProgressDraft(prev, item, getId));
|
|
671
830
|
}, []);
|
|
672
831
|
const goalNumberForRun = useCallback((runId) => Math.max(1, goalStatusEntries.findIndex((entry) => entry.runId === runId) + 1), [goalStatusEntries]);
|
|
673
832
|
const clearGoalStatusEntry = useCallback((runId) => {
|
|
@@ -686,21 +845,59 @@ export function App(props) {
|
|
|
686
845
|
return next;
|
|
687
846
|
});
|
|
688
847
|
}, [props.sessionStore]);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
848
|
+
const sessionStore = props.sessionStore;
|
|
849
|
+
const terminalHistoryContextRef = useRef({
|
|
850
|
+
theme,
|
|
851
|
+
columns,
|
|
852
|
+
version: props.version,
|
|
853
|
+
model: currentModel,
|
|
854
|
+
provider: currentProvider,
|
|
855
|
+
cwd: displayedCwd,
|
|
856
|
+
});
|
|
857
|
+
useEffect(() => {
|
|
858
|
+
terminalHistoryContextRef.current = {
|
|
859
|
+
theme,
|
|
860
|
+
columns,
|
|
861
|
+
version: props.version,
|
|
862
|
+
model: currentModel,
|
|
863
|
+
provider: currentProvider,
|
|
864
|
+
cwd: displayedCwd,
|
|
865
|
+
};
|
|
866
|
+
}, [theme, columns, props.version, currentModel, currentProvider, displayedCwd]);
|
|
867
|
+
const printHistoryItems = useCallback((items, options) => {
|
|
868
|
+
if (!props.terminalHistoryPrinter || items.length === 0)
|
|
869
|
+
return;
|
|
870
|
+
props.terminalHistoryPrinter.print(items, terminalHistoryContextRef.current, {
|
|
871
|
+
...options,
|
|
872
|
+
write: writeStdout,
|
|
873
|
+
});
|
|
874
|
+
}, [props.terminalHistoryPrinter, writeStdout]);
|
|
875
|
+
const pendingHistoryFlushRef = useRef([]);
|
|
876
|
+
const streamedAssistantFlushRef = useRef({
|
|
877
|
+
flushedChars: 0,
|
|
878
|
+
text: "",
|
|
879
|
+
});
|
|
880
|
+
const [historyFlushGeneration, setHistoryFlushGeneration] = useState(0);
|
|
694
881
|
const queueFlush = useCallback((items) => {
|
|
695
|
-
|
|
882
|
+
const flushed = trimFlushedItems(items);
|
|
883
|
+
if (flushed.length === 0)
|
|
696
884
|
return;
|
|
697
|
-
|
|
698
|
-
if (
|
|
885
|
+
pendingHistoryFlushRef.current = [...pendingHistoryFlushRef.current, ...flushed];
|
|
886
|
+
if (sessionStore) {
|
|
699
887
|
const queuedIds = new Set(items.map((item) => item.id));
|
|
700
|
-
|
|
888
|
+
sessionStore.liveItems = (sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
|
|
701
889
|
}
|
|
702
|
-
|
|
703
|
-
}, [
|
|
890
|
+
setHistoryFlushGeneration((generation) => generation + 1);
|
|
891
|
+
}, [sessionStore]);
|
|
892
|
+
const finalizeSubmittedUserItem = useCallback((item) => {
|
|
893
|
+
streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
|
|
894
|
+
setLiveItems((prev) => {
|
|
895
|
+
if (prev.length > 0)
|
|
896
|
+
queueFlush(prev);
|
|
897
|
+
queueFlush([item]);
|
|
898
|
+
return [];
|
|
899
|
+
});
|
|
900
|
+
}, [queueFlush]);
|
|
704
901
|
// Mirror runtime state choices (model/provider/thinking) into renderApp's
|
|
705
902
|
// closure so unmount/remount preserves them.
|
|
706
903
|
const onRuntimeStateChange = props.onRuntimeStateChange;
|
|
@@ -715,12 +912,35 @@ export function App(props) {
|
|
|
715
912
|
thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
|
|
716
913
|
});
|
|
717
914
|
}, [thinkingEnabled, currentModel, onRuntimeStateChange]);
|
|
915
|
+
useEffect(() => {
|
|
916
|
+
printHistoryItems(history);
|
|
917
|
+
}, [history, printHistoryItems]);
|
|
918
|
+
useEffect(() => {
|
|
919
|
+
const flushed = pendingHistoryFlushRef.current;
|
|
920
|
+
if (flushed.length === 0)
|
|
921
|
+
return;
|
|
922
|
+
pendingHistoryFlushRef.current = [];
|
|
923
|
+
printHistoryItems(flushed);
|
|
924
|
+
const flushedIds = new Set(flushed.map((item) => item.id));
|
|
925
|
+
setLiveItems((prev) => prev.filter((item) => !flushedIds.has(item.id)));
|
|
926
|
+
setHistory((prev) => {
|
|
927
|
+
const existingIds = new Set(prev.map((item) => item.id));
|
|
928
|
+
const nextItems = flushed.filter((item) => !existingIds.has(item.id));
|
|
929
|
+
if (nextItems.length === 0)
|
|
930
|
+
return prev;
|
|
931
|
+
const next = compactHistory([...prev, ...nextItems]);
|
|
932
|
+
if (sessionStore)
|
|
933
|
+
sessionStore.history = next;
|
|
934
|
+
return next;
|
|
935
|
+
});
|
|
936
|
+
}, [historyFlushGeneration, printHistoryItems, sessionStore]);
|
|
718
937
|
// Mirror session state into renderApp's closure so resetUI() can re-seed
|
|
719
938
|
// the conversation on remount. Each panel that previously did a bare ANSI
|
|
720
|
-
// screen clear (overlay open/close, plan accept/reject, /clear
|
|
939
|
+
// screen clear (overlay open/close, plan accept/reject, /clear)
|
|
721
940
|
// now goes through resetUI; without these mirrors, the chat would vanish.
|
|
722
|
-
const
|
|
941
|
+
const historyRef = useRef(history);
|
|
723
942
|
useEffect(() => {
|
|
943
|
+
historyRef.current = history;
|
|
724
944
|
if (sessionStore)
|
|
725
945
|
sessionStore.history = history;
|
|
726
946
|
}, [history, sessionStore]);
|
|
@@ -744,10 +964,19 @@ export function App(props) {
|
|
|
744
964
|
if (sessionStore)
|
|
745
965
|
sessionStore.overlay = overlay;
|
|
746
966
|
}, [overlay, sessionStore]);
|
|
967
|
+
useEffect(() => {
|
|
968
|
+
goalAutoExpandRef.current = goalAutoExpand;
|
|
969
|
+
if (sessionStore)
|
|
970
|
+
sessionStore.goalAutoExpand = goalAutoExpand;
|
|
971
|
+
}, [goalAutoExpand, sessionStore]);
|
|
747
972
|
useEffect(() => {
|
|
748
973
|
if (sessionStore)
|
|
749
974
|
sessionStore.goalStatusEntries = goalStatusEntries;
|
|
750
975
|
}, [goalStatusEntries, sessionStore]);
|
|
976
|
+
useEffect(() => {
|
|
977
|
+
if (sessionStore)
|
|
978
|
+
sessionStore.goalMode = goalMode;
|
|
979
|
+
}, [goalMode, sessionStore]);
|
|
751
980
|
// pendingAction is consumed via a useEffect AFTER agentLoop is created
|
|
752
981
|
// — see below where useAgentLoop is set up.
|
|
753
982
|
const pendingActionConsumedRef = useRef(false);
|
|
@@ -769,10 +998,8 @@ export function App(props) {
|
|
|
769
998
|
void reconcileActiveGoalRuns(props.cwd, {
|
|
770
999
|
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
771
1000
|
}).then(({ runs }) => {
|
|
772
|
-
const counts = summarizeGoalCountsFromRuns(runs);
|
|
773
1001
|
if (cancelled)
|
|
774
1002
|
return;
|
|
775
|
-
setGoalCount(counts.active);
|
|
776
1003
|
setHistory((prev) => completedItemsWithDurableGoalTerminalProgress(prev, runs));
|
|
777
1004
|
setGoalStatusEntries((prev) => {
|
|
778
1005
|
const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
|
|
@@ -813,19 +1040,23 @@ export function App(props) {
|
|
|
813
1040
|
useEffect(() => {
|
|
814
1041
|
currentToolsRef.current = currentTools;
|
|
815
1042
|
}, [currentTools]);
|
|
816
|
-
// ──
|
|
817
|
-
// Sync
|
|
1043
|
+
// ── Runtime mode wiring ──────────────────────────────────
|
|
1044
|
+
// Sync runtime mode refs with React state.
|
|
818
1045
|
useEffect(() => {
|
|
819
|
-
|
|
820
|
-
if (props.
|
|
821
|
-
props.
|
|
822
|
-
}
|
|
823
|
-
}, [
|
|
1046
|
+
goalModeStateRef.current = goalMode;
|
|
1047
|
+
if (props.goalModeRef) {
|
|
1048
|
+
props.goalModeRef.current = goalMode;
|
|
1049
|
+
}
|
|
1050
|
+
}, [goalMode, props.goalModeRef]);
|
|
1051
|
+
const setActiveGoalReferences = useCallback((references) => {
|
|
1052
|
+
if (props.goalReferencesRef)
|
|
1053
|
+
props.goalReferencesRef.current = references;
|
|
1054
|
+
}, [props.goalReferencesRef]);
|
|
824
1055
|
const rebuildSystemPrompt = useCallback(async (options) => {
|
|
825
1056
|
const approvedPlanPath = options?.clearApprovedPlan
|
|
826
1057
|
? undefined
|
|
827
1058
|
: (options?.approvedPlanPath ?? approvedPlanPathRef.current);
|
|
828
|
-
return buildSystemPrompt(options?.cwd ?? cwdRef.current, props.skills,
|
|
1059
|
+
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
1060
|
}, [props.skills]);
|
|
830
1061
|
const replaceSystemPrompt = useCallback(async (options) => {
|
|
831
1062
|
const newPrompt = await rebuildSystemPrompt(options);
|
|
@@ -834,6 +1065,28 @@ export function App(props) {
|
|
|
834
1065
|
}
|
|
835
1066
|
return newPrompt;
|
|
836
1067
|
}, [rebuildSystemPrompt]);
|
|
1068
|
+
const setGoalModeAndPrompt = useCallback(async (nextMode, options) => {
|
|
1069
|
+
goalModeStateRef.current = nextMode;
|
|
1070
|
+
if (props.goalModeRef)
|
|
1071
|
+
props.goalModeRef.current = nextMode;
|
|
1072
|
+
if (props.sessionStore)
|
|
1073
|
+
props.sessionStore.goalMode = nextMode;
|
|
1074
|
+
setGoalMode(nextMode);
|
|
1075
|
+
await replaceSystemPrompt({ ...options, goalMode: nextMode });
|
|
1076
|
+
}, [props.goalModeRef, props.sessionStore, replaceSystemPrompt]);
|
|
1077
|
+
const clearGoalModeIfIdle = useCallback(() => {
|
|
1078
|
+
setTimeout(() => {
|
|
1079
|
+
if (goalModeStateRef.current === "off")
|
|
1080
|
+
return;
|
|
1081
|
+
if (runningGoalIdsRef.current.size > 0)
|
|
1082
|
+
return;
|
|
1083
|
+
if (activeVerifierRunIdsRef.current.size > 0)
|
|
1084
|
+
return;
|
|
1085
|
+
if (queuedGoalSyntheticEventsRef.current > 0)
|
|
1086
|
+
return;
|
|
1087
|
+
void setGoalModeAndPrompt("off");
|
|
1088
|
+
}, 0);
|
|
1089
|
+
}, [setGoalModeAndPrompt]);
|
|
837
1090
|
/**
|
|
838
1091
|
* Unified "apply detection result" pipeline. Called from three sites:
|
|
839
1092
|
* 1. Initial mount (existing project at startup).
|
|
@@ -918,53 +1171,6 @@ export function App(props) {
|
|
|
918
1171
|
useEffect(() => {
|
|
919
1172
|
void applyLanguageDetectionRef.current("initial");
|
|
920
1173
|
}, []);
|
|
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
1174
|
const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
|
|
969
1175
|
const sm = sessionManagerRef.current;
|
|
970
1176
|
if (!sm)
|
|
@@ -1011,7 +1217,7 @@ export function App(props) {
|
|
|
1011
1217
|
* Other tool kinds skip detection entirely to avoid wasted filesystem stats.
|
|
1012
1218
|
*
|
|
1013
1219
|
* No restart required: the system prompt is mutated in place, same mechanism
|
|
1014
|
-
*
|
|
1220
|
+
* used for pixel-fix chdir.
|
|
1015
1221
|
*
|
|
1016
1222
|
* Stored in a ref so `onToolEnd` (whose useCallback dep array is intentionally
|
|
1017
1223
|
* empty to keep agent-loop options stable) can call the freshest version.
|
|
@@ -1312,7 +1518,6 @@ export function App(props) {
|
|
|
1312
1518
|
}, [
|
|
1313
1519
|
persistNewMessages,
|
|
1314
1520
|
stripRepoMapMessages,
|
|
1315
|
-
planMode,
|
|
1316
1521
|
props.cwd,
|
|
1317
1522
|
props.skills,
|
|
1318
1523
|
currentProvider,
|
|
@@ -1322,6 +1527,11 @@ export function App(props) {
|
|
|
1322
1527
|
resolveCredentials,
|
|
1323
1528
|
]),
|
|
1324
1529
|
onTurnText: useCallback((text, thinking, thinkingMs) => {
|
|
1530
|
+
if (goalModeStateRef.current === "planner") {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const hadStreamedAssistantFlush = streamedAssistantFlushRef.current.flushedChars > 0;
|
|
1534
|
+
const unflushedAssistantText = text.slice(streamedAssistantFlushRef.current.flushedChars);
|
|
1325
1535
|
// Track [DONE:n] markers for plan step progress
|
|
1326
1536
|
if (planStepsRef.current.length > 0) {
|
|
1327
1537
|
const completed = findCompletedMarkers(text);
|
|
@@ -1336,15 +1546,9 @@ export function App(props) {
|
|
|
1336
1546
|
followUpNudgesRef.current = { step: 0, count: 0 };
|
|
1337
1547
|
}
|
|
1338
1548
|
}
|
|
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.
|
|
1549
|
+
// Flush completed rows from the previous turn to finalized terminal
|
|
1550
|
+
// history. Ink keeps only the active turn, preventing live-area growth
|
|
1551
|
+
// and avoiding Static/log-update replay during resize/remount churn.
|
|
1348
1552
|
setLiveItems((prev) => {
|
|
1349
1553
|
const flushed = flushOnTurnText(prev);
|
|
1350
1554
|
if (flushed.length > 0) {
|
|
@@ -1353,10 +1557,7 @@ export function App(props) {
|
|
|
1353
1557
|
// Split text on [DONE:N] markers so each marker renders inline as
|
|
1354
1558
|
// a styled "✓ Step N: <description>" item at the position the
|
|
1355
1559
|
// 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);
|
|
1560
|
+
const segments = segmentDisplayText(unflushedAssistantText, planStepsRef.current);
|
|
1360
1561
|
const items = [];
|
|
1361
1562
|
let thinkingAttached = false;
|
|
1362
1563
|
for (const seg of segments) {
|
|
@@ -1369,7 +1570,7 @@ export function App(props) {
|
|
|
1369
1570
|
// contains multiple text chunks split by markers.
|
|
1370
1571
|
thinking: thinkingAttached ? undefined : thinking,
|
|
1371
1572
|
thinkingMs: thinkingAttached ? undefined : thinkingMs,
|
|
1372
|
-
|
|
1573
|
+
continuation: hadStreamedAssistantFlush,
|
|
1373
1574
|
id: getId(),
|
|
1374
1575
|
});
|
|
1375
1576
|
thinkingAttached = true;
|
|
@@ -1384,36 +1585,51 @@ export function App(props) {
|
|
|
1384
1585
|
}
|
|
1385
1586
|
}
|
|
1386
1587
|
// No segments at all (text was empty/whitespace, no markers).
|
|
1387
|
-
// Still
|
|
1388
|
-
// there was thinking content for this turn.
|
|
1588
|
+
// Still persist an assistant item so a thinking block renders in
|
|
1589
|
+
// terminal history if there was thinking content for this turn.
|
|
1389
1590
|
if (items.length === 0) {
|
|
1390
1591
|
items.push({
|
|
1391
1592
|
kind: "assistant",
|
|
1392
1593
|
text: "",
|
|
1393
1594
|
thinking,
|
|
1394
1595
|
thinkingMs,
|
|
1395
|
-
planMode: planModeLocalRef.current,
|
|
1396
1596
|
id: getId(),
|
|
1397
1597
|
});
|
|
1398
1598
|
}
|
|
1399
|
-
|
|
1599
|
+
const assistantItems = prev.filter((item) => item.kind === "assistant");
|
|
1600
|
+
const newAssistantText = normalizeAssistantText(unflushedAssistantText);
|
|
1601
|
+
const duplicatePinnedText = newAssistantText.length > 0 &&
|
|
1602
|
+
[...assistantItems, ...pendingHistoryFlushRef.current, ...historyRef.current].some((item) => isSameAssistantText(item, newAssistantText));
|
|
1603
|
+
const nextItems = duplicatePinnedText
|
|
1604
|
+
? items.filter((item) => !isSameAssistantText(item, newAssistantText))
|
|
1605
|
+
: items;
|
|
1606
|
+
const flushablePrev = prev.filter((item) => item.kind !== "assistant");
|
|
1607
|
+
if (flushablePrev.length > 0)
|
|
1608
|
+
queueFlush(flushablePrev);
|
|
1609
|
+
streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
|
|
1610
|
+
return [...assistantItems, ...nextItems];
|
|
1400
1611
|
});
|
|
1401
|
-
}, []),
|
|
1402
|
-
onToolStart: useCallback((toolCallId, name, args) => {
|
|
1612
|
+
}, [queueFlush]),
|
|
1613
|
+
onToolStart: useCallback((toolCallId, name, args, stream) => {
|
|
1403
1614
|
log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
|
|
1404
1615
|
const startedAt = Date.now();
|
|
1405
1616
|
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1617
|
+
const appendToolStart = (prev) => {
|
|
1618
|
+
const visible = pinStreamingTextBeforeToolBoundary({
|
|
1619
|
+
items: prev,
|
|
1620
|
+
visibleStreamingText: stream.text,
|
|
1621
|
+
thinking: stream.thinking,
|
|
1622
|
+
thinkingMs: stream.thinkingMs,
|
|
1623
|
+
makeId: getId,
|
|
1624
|
+
});
|
|
1625
|
+
const { flushed, remaining } = partitionCompleted(visible);
|
|
1411
1626
|
if (flushed.length > 0) {
|
|
1412
1627
|
queueFlush(flushed);
|
|
1413
1628
|
}
|
|
1414
1629
|
return remaining;
|
|
1415
|
-
}
|
|
1630
|
+
};
|
|
1416
1631
|
if (name === "subagent") {
|
|
1632
|
+
setLiveItems(appendToolStart);
|
|
1417
1633
|
// Create or update the sub-agent group item
|
|
1418
1634
|
const newAgent = {
|
|
1419
1635
|
toolCallId,
|
|
@@ -1438,25 +1654,32 @@ export function App(props) {
|
|
|
1438
1654
|
});
|
|
1439
1655
|
}
|
|
1440
1656
|
else if (AGGREGATABLE_TOOLS.has(name)) {
|
|
1441
|
-
// Group concurrent read-only tools into a single compact item
|
|
1442
1657
|
setLiveItems((prev) => {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
if (
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1658
|
+
const reusableGroupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
|
|
1659
|
+
item.tools.every((tool) => tool.name === name && !tool.isError));
|
|
1660
|
+
const prior = reusableGroupIdx === -1 ? [] : prev.slice(0, reusableGroupIdx);
|
|
1661
|
+
if (reusableGroupIdx !== -1 && prior.every((item) => !isActiveItem(item))) {
|
|
1662
|
+
const flushablePrior = prior.filter((item) => item.kind !== "assistant");
|
|
1663
|
+
if (flushablePrior.length > 0)
|
|
1664
|
+
queueFlush(flushablePrior);
|
|
1665
|
+
const pinnedPrior = prior.filter((item) => item.kind === "assistant");
|
|
1666
|
+
const candidates = prev.slice(reusableGroupIdx);
|
|
1667
|
+
const group = candidates[0];
|
|
1668
|
+
return [
|
|
1669
|
+
...pinnedPrior,
|
|
1670
|
+
{
|
|
1671
|
+
...group,
|
|
1672
|
+
tools: [
|
|
1673
|
+
...group.tools,
|
|
1674
|
+
{ toolCallId, name, args, status: "running", animateUntil },
|
|
1675
|
+
],
|
|
1676
|
+
},
|
|
1677
|
+
...candidates.slice(1),
|
|
1678
|
+
];
|
|
1457
1679
|
}
|
|
1680
|
+
const remaining = appendToolStart(prev);
|
|
1458
1681
|
return [
|
|
1459
|
-
...
|
|
1682
|
+
...remaining,
|
|
1460
1683
|
{
|
|
1461
1684
|
kind: "tool_group",
|
|
1462
1685
|
tools: [{ toolCallId, name, args, status: "running", animateUntil }],
|
|
@@ -1467,11 +1690,11 @@ export function App(props) {
|
|
|
1467
1690
|
}
|
|
1468
1691
|
else {
|
|
1469
1692
|
setLiveItems((prev) => [
|
|
1470
|
-
...prev,
|
|
1693
|
+
...appendToolStart(prev),
|
|
1471
1694
|
{ kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
|
|
1472
1695
|
]);
|
|
1473
1696
|
}
|
|
1474
|
-
}, []),
|
|
1697
|
+
}, [queueFlush]),
|
|
1475
1698
|
onToolUpdate: useCallback((toolCallId, update) => {
|
|
1476
1699
|
const u = update;
|
|
1477
1700
|
// Bash progress streaming — append output to tool_start item
|
|
@@ -1544,7 +1767,7 @@ export function App(props) {
|
|
|
1544
1767
|
};
|
|
1545
1768
|
const next = [...prev];
|
|
1546
1769
|
next[groupIdx] = { ...group, agents: updatedAgents };
|
|
1547
|
-
// Flush completed items to
|
|
1770
|
+
// Flush completed items to finalized history to keep the live area small
|
|
1548
1771
|
const { flushed, remaining } = partitionCompleted(next);
|
|
1549
1772
|
if (flushed.length > 0) {
|
|
1550
1773
|
queueFlush(flushed);
|
|
@@ -1603,7 +1826,7 @@ export function App(props) {
|
|
|
1603
1826
|
];
|
|
1604
1827
|
}
|
|
1605
1828
|
}
|
|
1606
|
-
// Flush completed items to
|
|
1829
|
+
// Flush completed items to finalized history to keep the live area small
|
|
1607
1830
|
const { flushed, remaining } = partitionCompleted(updated);
|
|
1608
1831
|
if (flushed.length > 0) {
|
|
1609
1832
|
queueFlush(flushed);
|
|
@@ -1619,14 +1842,21 @@ export function App(props) {
|
|
|
1619
1842
|
});
|
|
1620
1843
|
}
|
|
1621
1844
|
}, []),
|
|
1622
|
-
onServerToolCall: useCallback((id, name, input) => {
|
|
1845
|
+
onServerToolCall: useCallback((id, name, input, stream) => {
|
|
1623
1846
|
log("INFO", "server_tool", `Server tool call: ${name}`, { id });
|
|
1624
1847
|
const startedAt = Date.now();
|
|
1625
1848
|
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
1626
|
-
// Flush completed items (including assistant text)
|
|
1627
|
-
//
|
|
1849
|
+
// Flush completed items (including assistant text) before adding server
|
|
1850
|
+
// tool UI — same rationale as onToolStart.
|
|
1628
1851
|
setLiveItems((prev) => {
|
|
1629
|
-
const
|
|
1852
|
+
const visible = pinStreamingTextBeforeToolBoundary({
|
|
1853
|
+
items: prev,
|
|
1854
|
+
visibleStreamingText: stream.text,
|
|
1855
|
+
thinking: stream.thinking,
|
|
1856
|
+
thinkingMs: stream.thinkingMs,
|
|
1857
|
+
makeId: getId,
|
|
1858
|
+
});
|
|
1859
|
+
const { flushed, remaining } = partitionCompleted(visible);
|
|
1630
1860
|
if (flushed.length > 0) {
|
|
1631
1861
|
queueFlush(flushed);
|
|
1632
1862
|
}
|
|
@@ -1643,7 +1873,7 @@ export function App(props) {
|
|
|
1643
1873
|
},
|
|
1644
1874
|
];
|
|
1645
1875
|
});
|
|
1646
|
-
}, []),
|
|
1876
|
+
}, [queueFlush]),
|
|
1647
1877
|
onServerToolResult: useCallback((toolUseId, resultType, data) => {
|
|
1648
1878
|
log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
|
|
1649
1879
|
setLiveItems((prev) => {
|
|
@@ -1677,14 +1907,14 @@ export function App(props) {
|
|
|
1677
1907
|
},
|
|
1678
1908
|
];
|
|
1679
1909
|
}
|
|
1680
|
-
// Flush completed items to
|
|
1910
|
+
// Flush completed items to finalized history
|
|
1681
1911
|
const { flushed, remaining } = partitionCompleted(updated);
|
|
1682
1912
|
if (flushed.length > 0) {
|
|
1683
1913
|
queueFlush(flushed);
|
|
1684
1914
|
}
|
|
1685
1915
|
return remaining;
|
|
1686
1916
|
});
|
|
1687
|
-
}, []),
|
|
1917
|
+
}, [queueFlush]),
|
|
1688
1918
|
onTurnEnd: useCallback((turn, stopReason, usage) => {
|
|
1689
1919
|
log("INFO", "turn", `Turn ${turn} ended`, {
|
|
1690
1920
|
stopReason,
|
|
@@ -1700,8 +1930,8 @@ export function App(props) {
|
|
|
1700
1930
|
lastActualTokensRef.current =
|
|
1701
1931
|
currentProvider === "anthropic" ? inputContext : inputContext + usage.outputTokens;
|
|
1702
1932
|
lastActualTokensTimestampRef.current = Date.now();
|
|
1703
|
-
// For tool-only turns (no text), flush completed items to
|
|
1704
|
-
// liveItems doesn't grow unbounded across consecutive
|
|
1933
|
+
// For tool-only turns (no text), flush completed items to finalized
|
|
1934
|
+
// history so liveItems doesn't grow unbounded across consecutive turns.
|
|
1705
1935
|
setLiveItems((prev) => {
|
|
1706
1936
|
const { flushed, remaining } = flushOnTurnEnd(prev, stopReason);
|
|
1707
1937
|
if (flushed.length > 0) {
|
|
@@ -1709,44 +1939,42 @@ export function App(props) {
|
|
|
1709
1939
|
}
|
|
1710
1940
|
return remaining;
|
|
1711
1941
|
});
|
|
1712
|
-
}, []),
|
|
1942
|
+
}, [queueFlush]),
|
|
1713
1943
|
onDone: useCallback((durationMs, toolsUsed) => {
|
|
1714
1944
|
log("INFO", "agent", `Agent done`, {
|
|
1715
1945
|
duration: `${durationMs}ms`,
|
|
1716
1946
|
toolsUsed: toolsUsed.join(",") || "none",
|
|
1717
1947
|
});
|
|
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 [];
|
|
1948
|
+
const doneDecision = getDoneFlushDecision({
|
|
1949
|
+
planOverlayPending: planOverlayPendingRef.current,
|
|
1950
|
+
goalMode: goalModeStateRef.current,
|
|
1951
|
+
goalAutoExpand: goalAutoExpandRef.current,
|
|
1734
1952
|
});
|
|
1735
|
-
//
|
|
1736
|
-
//
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1953
|
+
// Don't show "done" status when plan/goal review panes are about to open —
|
|
1954
|
+
// the agent loop finished but we're waiting for user approval/review.
|
|
1955
|
+
// Still flush live transcript rows before the pane remounts; otherwise
|
|
1956
|
+
// setup output remains in ephemeral liveItems and appears to vanish.
|
|
1957
|
+
if (doneDecision.showDoneStatus) {
|
|
1958
|
+
setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
|
|
1959
|
+
playNotificationSound();
|
|
1960
|
+
}
|
|
1961
|
+
// Finalize rows now; the sink writes them outside Ink and then the
|
|
1962
|
+
// live area is cleared, so there is no Static/live repaint race.
|
|
1963
|
+
if (doneDecision.flushLiveItems) {
|
|
1964
|
+
setLiveItems((prev) => {
|
|
1965
|
+
if (prev.length > 0)
|
|
1966
|
+
queueFlush(prev);
|
|
1967
|
+
return [];
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
const nextGoalMode = nextGoalModeAfterAgentDone({
|
|
1971
|
+
currentMode: goalModeStateRef.current,
|
|
1972
|
+
runningGoalIds: runningGoalIdsRef.current.size,
|
|
1973
|
+
queuedSyntheticEvents: queuedGoalSyntheticEventsRef.current,
|
|
1974
|
+
activeContinuationFlights: goalContinuationFlightsRef.current.size,
|
|
1975
|
+
});
|
|
1976
|
+
if (nextGoalMode !== goalModeStateRef.current) {
|
|
1977
|
+
void setGoalModeAndPrompt(nextGoalMode);
|
|
1750
1978
|
}
|
|
1751
1979
|
// Goal loop: after the orchestrator handles a worker/verifier event,
|
|
1752
1980
|
// continue the same Goal automatically until it reaches a terminal state.
|
|
@@ -1788,12 +2016,16 @@ export function App(props) {
|
|
|
1788
2016
|
}
|
|
1789
2017
|
})();
|
|
1790
2018
|
}
|
|
1791
|
-
}, []),
|
|
2019
|
+
}, [setGoalModeAndPrompt]),
|
|
1792
2020
|
onAborted: useCallback(() => {
|
|
1793
2021
|
log("WARN", "agent", "Agent run aborted by user");
|
|
1794
|
-
setRunAllTasks(false);
|
|
1795
2022
|
setRunAllPixel(false);
|
|
1796
2023
|
currentPixelFixRef.current = null;
|
|
2024
|
+
queuedGoalSyntheticEventsRef.current = 0;
|
|
2025
|
+
goalSetupPanePendingRef.current = false;
|
|
2026
|
+
setActiveGoalReferences(undefined);
|
|
2027
|
+
if (goalModeStateRef.current !== "off")
|
|
2028
|
+
void setGoalModeAndPrompt("off");
|
|
1797
2029
|
setDoneStatus(null);
|
|
1798
2030
|
setLiveItems((prev) => {
|
|
1799
2031
|
const next = prev.map((item) => {
|
|
@@ -1836,7 +2068,7 @@ export function App(props) {
|
|
|
1836
2068
|
});
|
|
1837
2069
|
return [...next, { kind: "stopped", text: "Request was stopped.", id: getId() }];
|
|
1838
2070
|
});
|
|
1839
|
-
}, []),
|
|
2071
|
+
}, [setActiveGoalReferences, setGoalModeAndPrompt]),
|
|
1840
2072
|
onQueuedStart: useCallback((content) => {
|
|
1841
2073
|
// When a queued message starts processing, show it as a UserItem
|
|
1842
2074
|
// and flush prior items to history. Synthetic system events are hidden
|
|
@@ -1848,6 +2080,8 @@ export function App(props) {
|
|
|
1848
2080
|
.map((c) => c.text)
|
|
1849
2081
|
.join("\n");
|
|
1850
2082
|
if (isGoalSyntheticEvent(displayText)) {
|
|
2083
|
+
queuedGoalSyntheticEventsRef.current = Math.max(0, queuedGoalSyntheticEventsRef.current - 1);
|
|
2084
|
+
void setGoalModeAndPrompt("coordinator");
|
|
1851
2085
|
const eventInfo = parseGoalSyntheticEvent(displayText);
|
|
1852
2086
|
setLiveItems((prev) => {
|
|
1853
2087
|
if (prev.length > 0)
|
|
@@ -1870,11 +2104,6 @@ export function App(props) {
|
|
|
1870
2104
|
const imageCount = typeof content === "string"
|
|
1871
2105
|
? undefined
|
|
1872
2106
|
: content.filter((c) => c.type === "image").length || undefined;
|
|
1873
|
-
setLiveItems((prev) => {
|
|
1874
|
-
if (prev.length > 0)
|
|
1875
|
-
queueFlush(prev);
|
|
1876
|
-
return [];
|
|
1877
|
-
});
|
|
1878
2107
|
const userItem = {
|
|
1879
2108
|
kind: "user",
|
|
1880
2109
|
text: displayText,
|
|
@@ -1883,8 +2112,8 @@ export function App(props) {
|
|
|
1883
2112
|
};
|
|
1884
2113
|
setLastUserMessage(displayText);
|
|
1885
2114
|
setDoneStatus(null);
|
|
1886
|
-
|
|
1887
|
-
}, []),
|
|
2115
|
+
finalizeSubmittedUserItem(userItem);
|
|
2116
|
+
}, [appendGoalProgress, finalizeSubmittedUserItem, setGoalModeAndPrompt]),
|
|
1888
2117
|
// Inject a "continue with the next step" follow-up when the agent
|
|
1889
2118
|
// would otherwise stop mid-plan. The prompt-only instruction wasn't
|
|
1890
2119
|
// enough — some models (notably Opus) treat each [DONE:n] as a
|
|
@@ -1954,27 +2183,6 @@ export function App(props) {
|
|
|
1954
2183
|
]);
|
|
1955
2184
|
}
|
|
1956
2185
|
};
|
|
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
2186
|
// Sync terminal title with agent loop state
|
|
1979
2187
|
useEffect(() => {
|
|
1980
2188
|
setTitleRunning(agentLoop.isRunning);
|
|
@@ -2002,19 +2210,29 @@ export function App(props) {
|
|
|
2002
2210
|
return () => clearTimeout(timer);
|
|
2003
2211
|
}
|
|
2004
2212
|
}, [agentLoop.isRunning, sessionStore, props.resetUI]);
|
|
2005
|
-
// Consume
|
|
2006
|
-
// for paths that remount AND immediately drive
|
|
2007
|
-
//
|
|
2008
|
-
//
|
|
2213
|
+
// Consume pending post-remount work once on mount. Set by resetUI options
|
|
2214
|
+
// for paths that remount AND immediately drive work (plan accept/reject,
|
|
2215
|
+
// pixel fix, Goal approval). The work survives the unmount because
|
|
2216
|
+
// it lives in renderApp's closure (sessionStore), not React state.
|
|
2009
2217
|
useEffect(() => {
|
|
2010
2218
|
if (pendingActionConsumedRef.current)
|
|
2011
2219
|
return;
|
|
2012
2220
|
const action = sessionStore?.pendingAction;
|
|
2013
|
-
|
|
2221
|
+
const pendingGoalRun = sessionStore?.pendingGoalRun;
|
|
2222
|
+
if (!action && !pendingGoalRun)
|
|
2014
2223
|
return;
|
|
2015
2224
|
pendingActionConsumedRef.current = true;
|
|
2016
|
-
if (sessionStore)
|
|
2225
|
+
if (sessionStore) {
|
|
2017
2226
|
sessionStore.pendingAction = undefined;
|
|
2227
|
+
sessionStore.pendingGoalRun = undefined;
|
|
2228
|
+
}
|
|
2229
|
+
setDoneStatus(null);
|
|
2230
|
+
if (pendingGoalRun) {
|
|
2231
|
+
startGoalRunRef.current(pendingGoalRun);
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
if (!action)
|
|
2235
|
+
return;
|
|
2018
2236
|
if (action.planEvent) {
|
|
2019
2237
|
const ev = action.planEvent;
|
|
2020
2238
|
setLiveItems((prev) => [
|
|
@@ -2028,7 +2246,6 @@ export function App(props) {
|
|
|
2028
2246
|
{ kind: "info", text: action.infoText, id: getId() },
|
|
2029
2247
|
]);
|
|
2030
2248
|
}
|
|
2031
|
-
setDoneStatus(null);
|
|
2032
2249
|
void agentLoop.run(action.prompt).catch((err) => {
|
|
2033
2250
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2034
2251
|
log("ERROR", "error", errMsg);
|
|
@@ -2036,14 +2253,6 @@ export function App(props) {
|
|
|
2036
2253
|
});
|
|
2037
2254
|
// Intentional one-shot: run once on mount, never re-fire on re-render.
|
|
2038
2255
|
}, []);
|
|
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
2256
|
const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
|
|
2048
2257
|
const trimmed = input.trim();
|
|
2049
2258
|
if (trimmed.startsWith("/")) {
|
|
@@ -2098,7 +2307,8 @@ export function App(props) {
|
|
|
2098
2307
|
}
|
|
2099
2308
|
// Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
|
|
2100
2309
|
// React state in place without touching terminal scrollback.
|
|
2101
|
-
|
|
2310
|
+
pendingHistoryFlushRef.current = [];
|
|
2311
|
+
props.terminalHistoryPrinter?.clear();
|
|
2102
2312
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2103
2313
|
setLiveItems([]);
|
|
2104
2314
|
setDoneStatus(null);
|
|
@@ -2121,44 +2331,19 @@ export function App(props) {
|
|
|
2121
2331
|
setOverlay("theme");
|
|
2122
2332
|
return;
|
|
2123
2333
|
}
|
|
2124
|
-
//
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
setLiveItems((prev) => [
|
|
2130
|
-
...prev,
|
|
2334
|
+
// Handle /markdown — Gemini-style rendered/raw markdown toggle
|
|
2335
|
+
if (trimmed === "/markdown" || trimmed === "/md") {
|
|
2336
|
+
setRenderMarkdown((prev) => {
|
|
2337
|
+
const next = !prev;
|
|
2338
|
+
setLiveItems([
|
|
2131
2339
|
{
|
|
2132
2340
|
kind: "info",
|
|
2133
|
-
text:
|
|
2341
|
+
text: next ? "Rendered markdown mode." : "Raw markdown mode.",
|
|
2134
2342
|
id: getId(),
|
|
2135
2343
|
},
|
|
2136
2344
|
]);
|
|
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
|
-
]);
|
|
2345
|
+
return next;
|
|
2346
|
+
});
|
|
2162
2347
|
return;
|
|
2163
2348
|
}
|
|
2164
2349
|
// Handle /clearplan — dismiss the approved plan
|
|
@@ -2217,55 +2402,45 @@ export function App(props) {
|
|
|
2217
2402
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2218
2403
|
props.sessionStore.overlay = "goal";
|
|
2219
2404
|
props.sessionStore.planAutoExpand = false;
|
|
2405
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2220
2406
|
props.resetUI();
|
|
2221
2407
|
}
|
|
2222
2408
|
else {
|
|
2223
2409
|
if (props.sessionStore) {
|
|
2224
2410
|
props.sessionStore.overlay = "goal";
|
|
2225
2411
|
props.sessionStore.planAutoExpand = false;
|
|
2412
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2226
2413
|
if (agentLoop.isRunning)
|
|
2227
2414
|
props.sessionStore.pendingResetUI = true;
|
|
2228
2415
|
}
|
|
2229
2416
|
setPlanAutoExpand(false);
|
|
2417
|
+
setGoalAutoExpand(false);
|
|
2230
2418
|
setOverlay("goal");
|
|
2231
2419
|
}
|
|
2232
2420
|
return;
|
|
2233
2421
|
}
|
|
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
2422
|
// Handle prompt-template commands (built-in + custom from .gg/commands/)
|
|
2254
2423
|
const promptCommandRoute = routePromptCommandInput(trimmed, PROMPT_COMMANDS, customCommands);
|
|
2255
2424
|
if (promptCommandRoute) {
|
|
2256
2425
|
const { cmdName, cmdArgs, fullPrompt } = promptCommandRoute;
|
|
2257
2426
|
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
2427
|
const hasImages = inputImages.length > 0;
|
|
2428
|
+
const isGoalSetupCommand = isGoalPromptCommandName(cmdName);
|
|
2429
|
+
let promptForAgent = fullPrompt;
|
|
2430
|
+
if (isGoalSetupCommand) {
|
|
2431
|
+
const referenceContext = await buildGoalReferenceContext({
|
|
2432
|
+
cwd: props.cwd,
|
|
2433
|
+
originalGoalPrompt: fullPrompt,
|
|
2434
|
+
attachments: inputImages,
|
|
2435
|
+
});
|
|
2436
|
+
setActiveGoalReferences(referenceContext.references);
|
|
2437
|
+
promptForAgent = referenceContext.promptSection
|
|
2438
|
+
? `${fullPrompt}\n\n${referenceContext.promptSection}`
|
|
2439
|
+
: fullPrompt;
|
|
2440
|
+
}
|
|
2266
2441
|
const modelInfo = getModel(currentModel);
|
|
2267
2442
|
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
2268
|
-
const userContent = buildUserContentWithAttachments(
|
|
2443
|
+
const userContent = buildUserContentWithAttachments(promptForAgent, inputImages, modelSupportsImages);
|
|
2269
2444
|
// Show the typed command as the user message
|
|
2270
2445
|
const userItem = {
|
|
2271
2446
|
kind: "user",
|
|
@@ -2275,15 +2450,30 @@ export function App(props) {
|
|
|
2275
2450
|
};
|
|
2276
2451
|
setLastUserMessage(trimmed);
|
|
2277
2452
|
setDoneStatus(null);
|
|
2278
|
-
|
|
2453
|
+
finalizeSubmittedUserItem(userItem);
|
|
2279
2454
|
// Send the full prompt to the agent, with user args appended if provided
|
|
2280
2455
|
try {
|
|
2281
|
-
|
|
2456
|
+
if (isGoalSetupCommand) {
|
|
2457
|
+
goalSetupPanePendingRef.current = true;
|
|
2458
|
+
await runGoalPromptSetupSequence({
|
|
2459
|
+
userContent,
|
|
2460
|
+
fullPrompt: promptForAgent,
|
|
2461
|
+
messagesRef,
|
|
2462
|
+
setGoalModeAndPrompt,
|
|
2463
|
+
runAgent: (content) => agentLoop.run(content),
|
|
2464
|
+
onStage: appendGoalAgentTransition,
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
else {
|
|
2468
|
+
await agentLoop.run(userContent);
|
|
2469
|
+
}
|
|
2282
2470
|
}
|
|
2283
2471
|
catch (err) {
|
|
2284
2472
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2285
2473
|
log("ERROR", "error", msg);
|
|
2286
2474
|
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2475
|
+
if (isGoalSetupCommand)
|
|
2476
|
+
goalSetupPanePendingRef.current = false;
|
|
2287
2477
|
setLiveItems((prev) => [
|
|
2288
2478
|
...prev,
|
|
2289
2479
|
isAbort
|
|
@@ -2291,6 +2481,41 @@ export function App(props) {
|
|
|
2291
2481
|
: toErrorItem(err, getId()),
|
|
2292
2482
|
]);
|
|
2293
2483
|
}
|
|
2484
|
+
finally {
|
|
2485
|
+
if (isGoalSetupCommand) {
|
|
2486
|
+
setActiveGoalReferences(undefined);
|
|
2487
|
+
const paneTransition = getGoalSetupPaneTransitionAfterRun({
|
|
2488
|
+
isGoalSetupCommand,
|
|
2489
|
+
setupPanePending: goalSetupPanePendingRef.current,
|
|
2490
|
+
});
|
|
2491
|
+
goalSetupPanePendingRef.current = false;
|
|
2492
|
+
if (goalModeStateRef.current !== "off") {
|
|
2493
|
+
await setGoalModeAndPrompt("off");
|
|
2494
|
+
}
|
|
2495
|
+
if (paneTransition) {
|
|
2496
|
+
goalAutoExpandRef.current = paneTransition.goalAutoExpand;
|
|
2497
|
+
setTimeout(() => {
|
|
2498
|
+
const resetUI = props.resetUI;
|
|
2499
|
+
const sessionStore = props.sessionStore;
|
|
2500
|
+
if (shouldResetUIForGoalSetupPaneTransition({
|
|
2501
|
+
hasResetUI: resetUI !== undefined,
|
|
2502
|
+
hasSessionStore: sessionStore !== undefined,
|
|
2503
|
+
}) &&
|
|
2504
|
+
resetUI &&
|
|
2505
|
+
sessionStore) {
|
|
2506
|
+
sessionStore.overlay = paneTransition.overlay;
|
|
2507
|
+
sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
|
|
2508
|
+
sessionStore.planAutoExpand = paneTransition.planAutoExpand;
|
|
2509
|
+
resetUI();
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
setGoalAutoExpand(paneTransition.goalAutoExpand);
|
|
2513
|
+
setPlanAutoExpand(paneTransition.planAutoExpand);
|
|
2514
|
+
setOverlay(paneTransition.overlay);
|
|
2515
|
+
}, 300);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2294
2519
|
// Reload custom commands in case a setup command created new ones
|
|
2295
2520
|
reloadCustomCommands();
|
|
2296
2521
|
return;
|
|
@@ -2326,17 +2551,6 @@ export function App(props) {
|
|
|
2326
2551
|
setLiveItems((prev) => [...prev, queuedItem]);
|
|
2327
2552
|
return;
|
|
2328
2553
|
}
|
|
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
2554
|
// Build display text — strip image paths, show badges instead
|
|
2341
2555
|
let displayText = input;
|
|
2342
2556
|
if (hasImages) {
|
|
@@ -2358,7 +2572,7 @@ export function App(props) {
|
|
|
2358
2572
|
planStepsRef.current = [];
|
|
2359
2573
|
setPlanSteps([]);
|
|
2360
2574
|
}
|
|
2361
|
-
|
|
2575
|
+
finalizeSubmittedUserItem(userItem);
|
|
2362
2576
|
// Run agent
|
|
2363
2577
|
try {
|
|
2364
2578
|
await agentLoop.run(userContent);
|
|
@@ -2376,11 +2590,20 @@ export function App(props) {
|
|
|
2376
2590
|
}
|
|
2377
2591
|
}, [
|
|
2378
2592
|
agentLoop,
|
|
2379
|
-
|
|
2593
|
+
appendGoalAgentTransition,
|
|
2380
2594
|
compactConversation,
|
|
2595
|
+
currentModel,
|
|
2596
|
+
finalizeSubmittedUserItem,
|
|
2597
|
+
props.cwd,
|
|
2598
|
+
props.onSlashCommand,
|
|
2599
|
+
props.resetUI,
|
|
2600
|
+
props.sessionStore,
|
|
2381
2601
|
rebuildSystemPrompt,
|
|
2382
|
-
replaceSystemPrompt,
|
|
2383
2602
|
refreshRepoMap,
|
|
2603
|
+
reloadCustomCommands,
|
|
2604
|
+
replaceSystemPrompt,
|
|
2605
|
+
setActiveGoalReferences,
|
|
2606
|
+
setGoalModeAndPrompt,
|
|
2384
2607
|
stripRepoMapMessages,
|
|
2385
2608
|
]);
|
|
2386
2609
|
const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
|
|
@@ -2518,90 +2741,96 @@ export function App(props) {
|
|
|
2518
2741
|
const promptByName = new Map(PROMPT_COMMANDS.map((c) => [c.name, c]));
|
|
2519
2742
|
const fromPrompt = (name) => {
|
|
2520
2743
|
const c = promptByName.get(name);
|
|
2521
|
-
return c
|
|
2744
|
+
return c
|
|
2745
|
+
? {
|
|
2746
|
+
name: c.name,
|
|
2747
|
+
aliases: c.aliases,
|
|
2748
|
+
description: c.description,
|
|
2749
|
+
sectionTitle: "workflows",
|
|
2750
|
+
}
|
|
2751
|
+
: null;
|
|
2522
2752
|
};
|
|
2523
2753
|
const promptOrder = [
|
|
2524
2754
|
// Project audits / one-shot analysis
|
|
2525
2755
|
"goal",
|
|
2526
2756
|
"init",
|
|
2527
|
-
"research",
|
|
2528
|
-
"scan",
|
|
2529
|
-
"verify",
|
|
2530
2757
|
"expand",
|
|
2531
2758
|
"bullet-proof",
|
|
2532
|
-
"simplify",
|
|
2533
2759
|
"compare",
|
|
2534
|
-
"batch",
|
|
2535
2760
|
// Setup / installers
|
|
2536
|
-
"setup-lint",
|
|
2537
|
-
"setup-tests",
|
|
2538
2761
|
"setup-commit",
|
|
2539
|
-
"setup-update",
|
|
2540
|
-
"setup-eyes",
|
|
2541
|
-
"eyes-improve",
|
|
2542
2762
|
"setup-skills",
|
|
2543
2763
|
];
|
|
2544
2764
|
const orderedPromptCommands = promptOrder
|
|
2545
2765
|
.map(fromPrompt)
|
|
2546
2766
|
.filter((c) => c !== null);
|
|
2547
2767
|
const knownPromptNames = new Set(promptOrder);
|
|
2548
|
-
const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({
|
|
2768
|
+
const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({
|
|
2769
|
+
name: c.name,
|
|
2770
|
+
aliases: c.aliases,
|
|
2771
|
+
description: c.description,
|
|
2772
|
+
sectionTitle: "workflows",
|
|
2773
|
+
}));
|
|
2549
2774
|
return [
|
|
2550
2775
|
// 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" },
|
|
2776
|
+
{ name: "model", aliases: ["m"], description: "Switch model", sectionTitle: "built-in" },
|
|
2777
|
+
{ name: "compact", aliases: ["c"], description: "Compact context", sectionTitle: "built-in" },
|
|
2778
|
+
{ name: "clear", aliases: [], description: "Clear session", sectionTitle: "built-in" },
|
|
2779
|
+
{ name: "theme", aliases: ["t"], description: "Switch theme", sectionTitle: "built-in" },
|
|
2556
2780
|
...orderedPromptCommands,
|
|
2557
2781
|
...remainingPromptCommands,
|
|
2558
2782
|
...customCommands.map((cmd) => ({
|
|
2559
2783
|
name: cmd.name,
|
|
2560
2784
|
aliases: [],
|
|
2561
2785
|
description: cmd.description,
|
|
2786
|
+
sectionTitle: "custom",
|
|
2562
2787
|
})),
|
|
2563
|
-
{
|
|
2788
|
+
{
|
|
2789
|
+
name: "quit",
|
|
2790
|
+
aliases: ["q", "exit"],
|
|
2791
|
+
description: "Exit ggcoder",
|
|
2792
|
+
sectionTitle: "built-in",
|
|
2793
|
+
},
|
|
2564
2794
|
];
|
|
2565
2795
|
}, [customCommands]);
|
|
2566
|
-
const
|
|
2796
|
+
const normalizeStatusText = (text) => text.replace(/\\n/g, "\n").replace(/^\n+|\n+$/g, "");
|
|
2797
|
+
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));
|
|
2798
|
+
const renderItem = (item, index, items) => {
|
|
2799
|
+
const previousLiveItem = index > 0 ? items[index - 1] : undefined;
|
|
2800
|
+
const shouldTopSpacePrintedBoundary = shouldTopSpaceAfterPrintedAgentBoundary({
|
|
2801
|
+
currentKind: item.kind,
|
|
2802
|
+
previousLiveItem,
|
|
2803
|
+
lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
|
|
2804
|
+
lastHistoryItem: history.at(-1),
|
|
2805
|
+
});
|
|
2806
|
+
const assistantMarginTop = item.kind === "assistant" &&
|
|
2807
|
+
(shouldTopSpacePrintedBoundary ||
|
|
2808
|
+
shouldTopSpaceAssistantAfterToolBoundary({
|
|
2809
|
+
text: item.text,
|
|
2810
|
+
previousLiveItem,
|
|
2811
|
+
lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
|
|
2812
|
+
lastHistoryItem: history.at(-1),
|
|
2813
|
+
}))
|
|
2814
|
+
? 1
|
|
2815
|
+
: 0;
|
|
2816
|
+
const withPrintedBoundarySpacing = (node) => shouldTopSpacePrintedBoundary ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: node }, `${item.id}-printed-boundary`)) : (node);
|
|
2567
2817
|
switch (item.kind) {
|
|
2568
2818
|
case "tombstone":
|
|
2569
2819
|
return null;
|
|
2570
2820
|
case "banner":
|
|
2571
|
-
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd
|
|
2821
|
+
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd }, item.id));
|
|
2572
2822
|
case "user":
|
|
2573
2823
|
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
2824
|
case "goal":
|
|
2577
2825
|
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));
|
|
2578
2826
|
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));
|
|
2827
|
+
const color = goalProgressColor(item, theme);
|
|
2828
|
+
const loaderStatus = goalProgressLoaderStatus(item);
|
|
2829
|
+
const hasBody = !!item.detail ||
|
|
2830
|
+
(item.summaryRows !== undefined && item.summaryRows.length > 0) ||
|
|
2831
|
+
(item.summarySections !== undefined && item.summarySections.length > 0);
|
|
2832
|
+
const headerContentWidth = Math.max(10, columns - 3);
|
|
2833
|
+
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: 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: 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: row.value }), row.detail ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", 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: `• ${line}` }, `${section.title}-${sectionLineIndex}`)))] }, section.title)))] }) })) : null] }, item.id));
|
|
2605
2834
|
}
|
|
2606
2835
|
case "style_pack": {
|
|
2607
2836
|
const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
|
|
@@ -2611,61 +2840,60 @@ export function App(props) {
|
|
|
2611
2840
|
case "setup_hint":
|
|
2612
2841
|
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
2842
|
case "assistant":
|
|
2614
|
-
return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs,
|
|
2843
|
+
return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, renderMarkdown: renderMarkdown, availableTerminalHeight: measuredLiveAreaRows, marginTop: assistantMarginTop }, item.id));
|
|
2615
2844
|
case "tool_start":
|
|
2616
|
-
return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
|
|
2845
|
+
return withPrintedBoundarySpacing(_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
|
|
2617
2846
|
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));
|
|
2847
|
+
return withPrintedBoundarySpacing(_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
|
|
2619
2848
|
case "tool_group":
|
|
2620
|
-
return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
|
|
2849
|
+
return withPrintedBoundarySpacing(_jsx(ToolGroupExecution, { tools: item.tools }, item.id));
|
|
2621
2850
|
case "server_tool_start":
|
|
2622
|
-
return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
|
|
2851
|
+
return withPrintedBoundarySpacing(_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
|
|
2623
2852
|
case "server_tool_done":
|
|
2624
|
-
return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
|
|
2853
|
+
return withPrintedBoundarySpacing(_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
|
|
2625
2854
|
case "error": {
|
|
2626
2855
|
const showMessage = item.message && item.message !== item.headline;
|
|
2627
|
-
return (_jsxs(Box, { marginTop: 1, flexDirection: "column",
|
|
2856
|
+
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
2857
|
}
|
|
2629
2858
|
case "info":
|
|
2630
|
-
return (
|
|
2859
|
+
return renderStatusMessage(item.id, "○ ", item.text, theme.commandColor, { muted: true });
|
|
2631
2860
|
case "update_notice":
|
|
2632
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, borderStyle: "round", borderColor: theme.
|
|
2861
|
+
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
2862
|
case "plan_transition":
|
|
2634
|
-
return (
|
|
2863
|
+
return renderStatusMessage(item.id, "● ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
|
|
2864
|
+
case "goal_agent_transition":
|
|
2865
|
+
return renderStatusMessage(item.id, "● ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
|
|
2635
2866
|
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));
|
|
2867
|
+
const glyphColor = item.active ? theme.commandColor : theme.textDim;
|
|
2868
|
+
return renderStatusMessage(item.id, "✻ ", item.active ? "Thinking ON" : "Thinking OFF", glyphColor, { bold: true, muted: !item.active });
|
|
2646
2869
|
}
|
|
2870
|
+
case "model_transition":
|
|
2871
|
+
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 });
|
|
2872
|
+
case "theme_transition":
|
|
2873
|
+
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
2874
|
case "plan_event": {
|
|
2648
|
-
// Plan-domain status changes (approve / reject / dismiss).
|
|
2649
|
-
//
|
|
2650
|
-
// distinct from the model/thinking gradient.
|
|
2875
|
+
// Plan-domain status changes (approve / reject / dismiss). Use the
|
|
2876
|
+
// command accent so transient TUI status rows share one purple voice.
|
|
2651
2877
|
const label = item.event === "approved"
|
|
2652
2878
|
? "Plan approved"
|
|
2653
2879
|
: item.event === "rejected"
|
|
2654
2880
|
? "Plan rejected"
|
|
2655
2881
|
: "Plan dismissed";
|
|
2656
|
-
return (
|
|
2882
|
+
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
2883
|
}
|
|
2658
2884
|
case "stopped":
|
|
2659
2885
|
// Cancellation / abort acknowledgement (ESC, auto-setup cancel, etc.).
|
|
2660
2886
|
// Muted dim treatment — this is an ack, not a state change worth a
|
|
2661
2887
|
// gradient. Glyph `⊘` reads as "stop" without being alarming.
|
|
2662
|
-
return (
|
|
2888
|
+
return renderStatusMessage(item.id, "⊘ ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
|
|
2663
2889
|
case "step_done":
|
|
2664
2890
|
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
|
-
|
|
2891
|
+
case "queued": {
|
|
2892
|
+
const suffix = item.imageCount
|
|
2893
|
+
? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
|
|
2894
|
+
: "";
|
|
2895
|
+
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));
|
|
2896
|
+
}
|
|
2669
2897
|
case "compacting":
|
|
2670
2898
|
return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
|
|
2671
2899
|
case "compacted":
|
|
@@ -2673,87 +2901,16 @@ export function App(props) {
|
|
|
2673
2901
|
case "duration":
|
|
2674
2902
|
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.textDim, children: ["✻ ", item.verb, " ", formatDuration(item.durationMs)] }) }, item.id));
|
|
2675
2903
|
case "subagent_group":
|
|
2676
|
-
return _jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id);
|
|
2904
|
+
return withPrintedBoundarySpacing(_jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id));
|
|
2677
2905
|
}
|
|
2678
2906
|
};
|
|
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
2907
|
const openOverlay = useCallback((kind) => {
|
|
2753
2908
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2754
2909
|
props.sessionStore.overlay = kind;
|
|
2755
2910
|
if (kind !== "plan")
|
|
2756
2911
|
props.sessionStore.planAutoExpand = false;
|
|
2912
|
+
if (kind !== "goal")
|
|
2913
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2757
2914
|
props.resetUI();
|
|
2758
2915
|
}
|
|
2759
2916
|
else {
|
|
@@ -2761,12 +2918,16 @@ export function App(props) {
|
|
|
2761
2918
|
props.sessionStore.overlay = kind;
|
|
2762
2919
|
if (kind !== "plan")
|
|
2763
2920
|
props.sessionStore.planAutoExpand = false;
|
|
2921
|
+
if (kind !== "goal")
|
|
2922
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2764
2923
|
if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
|
|
2765
2924
|
props.sessionStore.pendingResetUI = true;
|
|
2766
2925
|
}
|
|
2767
2926
|
}
|
|
2768
2927
|
if (kind !== "plan")
|
|
2769
2928
|
setPlanAutoExpand(false);
|
|
2929
|
+
if (kind !== "goal")
|
|
2930
|
+
setGoalAutoExpand(false);
|
|
2770
2931
|
setOverlay(kind);
|
|
2771
2932
|
}
|
|
2772
2933
|
}, [agentLoop.isRunning, props]);
|
|
@@ -2788,6 +2949,8 @@ export function App(props) {
|
|
|
2788
2949
|
? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
|
|
2789
2950
|
: `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
|
|
2790
2951
|
if (agentRunningRef.current) {
|
|
2952
|
+
queuedGoalSyntheticEventsRef.current += 1;
|
|
2953
|
+
void setGoalModeAndPrompt("coordinator");
|
|
2791
2954
|
appendGoalProgress({
|
|
2792
2955
|
kind: "goal_progress",
|
|
2793
2956
|
phase: "orchestrator_reviewing",
|
|
@@ -2809,12 +2972,19 @@ export function App(props) {
|
|
|
2809
2972
|
});
|
|
2810
2973
|
setLastUserMessage("");
|
|
2811
2974
|
setDoneStatus(null);
|
|
2812
|
-
void
|
|
2975
|
+
void (async () => {
|
|
2976
|
+
await setGoalModeAndPrompt("coordinator");
|
|
2977
|
+
await agentLoop.run(eventText);
|
|
2978
|
+
})().catch((err) => {
|
|
2813
2979
|
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2814
2980
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2981
|
+
clearGoalModeIfIdle();
|
|
2815
2982
|
});
|
|
2816
|
-
}, [agentLoop, appendGoalProgress]);
|
|
2983
|
+
}, [agentLoop, appendGoalProgress, clearGoalModeIfIdle, setGoalModeAndPrompt]);
|
|
2817
2984
|
const continueGoalRun = useCallback((runId) => {
|
|
2985
|
+
if (goalContinuationFlightsRef.current.has(runId))
|
|
2986
|
+
return;
|
|
2987
|
+
goalContinuationFlightsRef.current.add(runId);
|
|
2818
2988
|
void (async () => {
|
|
2819
2989
|
const latestRun = await reconcileActiveGoalRuns(props.cwd, {
|
|
2820
2990
|
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
@@ -2822,11 +2992,24 @@ export function App(props) {
|
|
|
2822
2992
|
if (!latestRun) {
|
|
2823
2993
|
runningGoalIdsRef.current.delete(runId);
|
|
2824
2994
|
clearGoalStatusEntry(runId);
|
|
2995
|
+
clearGoalModeIfIdle();
|
|
2825
2996
|
return;
|
|
2826
2997
|
}
|
|
2827
2998
|
const decision = decideGoalNextAction(latestRun);
|
|
2828
2999
|
if (decision.kind === "wait")
|
|
2829
3000
|
return;
|
|
3001
|
+
const choiceKey = getGoalContinuationChoiceKey({ runId: latestRun.id, decision });
|
|
3002
|
+
const now = Date.now();
|
|
3003
|
+
const recentChoiceAt = goalContinuationRecentChoicesRef.current.get(choiceKey);
|
|
3004
|
+
if (recentChoiceAt !== undefined && now - recentChoiceAt < 5000)
|
|
3005
|
+
return;
|
|
3006
|
+
goalContinuationRecentChoicesRef.current.set(choiceKey, now);
|
|
3007
|
+
if (goalContinuationRecentChoicesRef.current.size > 100) {
|
|
3008
|
+
for (const [key, startedAt] of goalContinuationRecentChoicesRef.current) {
|
|
3009
|
+
if (now - startedAt > 60_000)
|
|
3010
|
+
goalContinuationRecentChoicesRef.current.delete(key);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
2830
3013
|
if (decision.kind === "terminal" ||
|
|
2831
3014
|
decision.kind === "blocked" ||
|
|
2832
3015
|
decision.kind === "pause") {
|
|
@@ -2856,6 +3039,7 @@ export function App(props) {
|
|
|
2856
3039
|
}
|
|
2857
3040
|
runningGoalIdsRef.current.delete(runId);
|
|
2858
3041
|
clearGoalStatusEntry(runId);
|
|
3042
|
+
clearGoalModeIfIdle();
|
|
2859
3043
|
return;
|
|
2860
3044
|
}
|
|
2861
3045
|
let runForNextAction = latestRun;
|
|
@@ -2886,18 +3070,28 @@ export function App(props) {
|
|
|
2886
3070
|
detail: "choosing next step",
|
|
2887
3071
|
});
|
|
2888
3072
|
startGoalRunRef.current(runForNextAction);
|
|
2889
|
-
})()
|
|
3073
|
+
})()
|
|
3074
|
+
.catch((err) => {
|
|
2890
3075
|
runningGoalIdsRef.current.delete(runId);
|
|
2891
3076
|
clearGoalStatusEntry(runId);
|
|
2892
3077
|
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2893
3078
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3079
|
+
})
|
|
3080
|
+
.finally(() => {
|
|
3081
|
+
goalContinuationFlightsRef.current.delete(runId);
|
|
3082
|
+
clearGoalModeIfIdle();
|
|
2894
3083
|
});
|
|
2895
|
-
}, [
|
|
3084
|
+
}, [
|
|
3085
|
+
appendGoalProgress,
|
|
3086
|
+
clearGoalModeIfIdle,
|
|
3087
|
+
clearGoalStatusEntry,
|
|
3088
|
+
props.cwd,
|
|
3089
|
+
upsertGoalStatusEntry,
|
|
3090
|
+
]);
|
|
2896
3091
|
const handleGoalWorkerComplete = useCallback((run, completion) => {
|
|
2897
3092
|
const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
|
|
2898
3093
|
completion.worker.goalTaskId;
|
|
2899
3094
|
const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
|
|
2900
|
-
void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
|
|
2901
3095
|
appendGoalProgress({
|
|
2902
3096
|
kind: "goal_progress",
|
|
2903
3097
|
phase: "worker_finished",
|
|
@@ -2951,112 +3145,131 @@ export function App(props) {
|
|
|
2951
3145
|
}, [handleGoalWorkerComplete, props.cwd]);
|
|
2952
3146
|
const startGoalRun = useCallback((run) => {
|
|
2953
3147
|
runningGoalIdsRef.current.add(run.id);
|
|
3148
|
+
upsertGoalStatusEntry({
|
|
3149
|
+
runId: run.id,
|
|
3150
|
+
label: run.title,
|
|
3151
|
+
phase: "orchestrating",
|
|
3152
|
+
startedAt: Date.now(),
|
|
3153
|
+
detail: "choosing next step",
|
|
3154
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3155
|
+
});
|
|
2954
3156
|
void (async () => {
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
3157
|
+
await setGoalModeAndPrompt("coordinator");
|
|
3158
|
+
const currentRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
3159
|
+
const prereqCheck = await runGoalPrerequisiteChecks(props.cwd, currentRun);
|
|
3160
|
+
const checkedRun = prereqCheck.checkedCount > 0
|
|
3161
|
+
? await upsertGoalRun(props.cwd, {
|
|
3162
|
+
...prereqCheck.run,
|
|
3163
|
+
status: goalHasBlockingPrerequisites(prereqCheck.run) ? "blocked" : "ready",
|
|
3164
|
+
})
|
|
3165
|
+
: currentRun;
|
|
3166
|
+
if (goalHasBlockingPrerequisites(checkedRun)) {
|
|
3167
|
+
const detail = formatGoalBlockingPrerequisites(checkedRun);
|
|
2958
3168
|
await upsertGoalRun(props.cwd, {
|
|
2959
|
-
...
|
|
3169
|
+
...checkedRun,
|
|
2960
3170
|
status: "blocked",
|
|
2961
|
-
blockers: Array.from(new Set([...
|
|
3171
|
+
blockers: Array.from(new Set([...checkedRun.blockers, detail])),
|
|
2962
3172
|
});
|
|
2963
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2964
3173
|
appendGoalProgress({
|
|
2965
3174
|
kind: "goal_progress",
|
|
2966
3175
|
phase: "terminal",
|
|
2967
|
-
title: `Goal blocked: ${
|
|
3176
|
+
title: `Goal blocked: ${checkedRun.title}`,
|
|
2968
3177
|
detail,
|
|
2969
3178
|
status: "blocked",
|
|
2970
3179
|
});
|
|
2971
|
-
runningGoalIdsRef.current.delete(
|
|
2972
|
-
clearGoalStatusEntry(
|
|
3180
|
+
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
3181
|
+
clearGoalStatusEntry(checkedRun.id);
|
|
3182
|
+
clearGoalModeIfIdle();
|
|
2973
3183
|
return;
|
|
2974
3184
|
}
|
|
2975
|
-
const decision = decideGoalNextAction(
|
|
2976
|
-
await appendGoalDecision(props.cwd,
|
|
3185
|
+
const decision = decideGoalNextAction(checkedRun);
|
|
3186
|
+
await appendGoalDecision(props.cwd, checkedRun.id, decision);
|
|
2977
3187
|
if (decision.kind === "terminal") {
|
|
2978
|
-
const terminalProgress = formatGoalTerminalProgress(
|
|
3188
|
+
const terminalProgress = formatGoalTerminalProgress(checkedRun);
|
|
2979
3189
|
if (terminalProgress) {
|
|
2980
|
-
const item = { ...terminalProgress, id: goalTerminalProgressId(
|
|
2981
|
-
setLiveItems((prev) => completedItemsWithDurableGoalTerminalProgress([...prev, item], [
|
|
3190
|
+
const item = { ...terminalProgress, id: goalTerminalProgressId(checkedRun) };
|
|
3191
|
+
setLiveItems((prev) => completedItemsWithDurableGoalTerminalProgress([...prev, item], [checkedRun]));
|
|
2982
3192
|
}
|
|
2983
|
-
runningGoalIdsRef.current.delete(
|
|
2984
|
-
clearGoalStatusEntry(
|
|
3193
|
+
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
3194
|
+
clearGoalStatusEntry(checkedRun.id);
|
|
3195
|
+
clearGoalModeIfIdle();
|
|
2985
3196
|
return;
|
|
2986
3197
|
}
|
|
2987
3198
|
if (decision.kind === "wait") {
|
|
2988
3199
|
appendGoalProgress({
|
|
2989
3200
|
kind: "goal_progress",
|
|
2990
3201
|
phase: "worker_started",
|
|
2991
|
-
title: decision.workerId
|
|
3202
|
+
title: decision.workerId
|
|
3203
|
+
? `Goal working: ${checkedRun.title}`
|
|
3204
|
+
: `Goal active: ${checkedRun.title}`,
|
|
2992
3205
|
detail: decision.reason,
|
|
2993
3206
|
workerId: decision.workerId,
|
|
2994
3207
|
});
|
|
2995
3208
|
upsertGoalStatusEntry({
|
|
2996
|
-
runId:
|
|
2997
|
-
label:
|
|
3209
|
+
runId: checkedRun.id,
|
|
3210
|
+
label: checkedRun.title,
|
|
2998
3211
|
phase: decision.workerId ? "worker" : "orchestrating",
|
|
2999
3212
|
startedAt: Date.now(),
|
|
3000
3213
|
detail: decision.reason,
|
|
3001
3214
|
workerId: decision.workerId,
|
|
3002
|
-
goalNumber: goalNumberForRun(
|
|
3215
|
+
goalNumber: goalNumberForRun(checkedRun.id),
|
|
3003
3216
|
});
|
|
3004
3217
|
return;
|
|
3005
3218
|
}
|
|
3006
3219
|
if (decision.kind === "complete") {
|
|
3007
|
-
await upsertGoalRun(props.cwd, { ...
|
|
3008
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3220
|
+
await upsertGoalRun(props.cwd, { ...checkedRun, status: "passed" });
|
|
3009
3221
|
appendGoalProgress({
|
|
3010
3222
|
kind: "goal_progress",
|
|
3011
3223
|
phase: "terminal",
|
|
3012
|
-
title: `Goal passed: ${
|
|
3224
|
+
title: `Goal passed: ${checkedRun.title}`,
|
|
3013
3225
|
detail: decision.reason,
|
|
3014
3226
|
status: "passed",
|
|
3015
3227
|
});
|
|
3016
|
-
runningGoalIdsRef.current.delete(
|
|
3017
|
-
clearGoalStatusEntry(
|
|
3228
|
+
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
3229
|
+
clearGoalStatusEntry(checkedRun.id);
|
|
3230
|
+
clearGoalModeIfIdle();
|
|
3018
3231
|
return;
|
|
3019
3232
|
}
|
|
3020
3233
|
if (decision.kind === "run_verifier") {
|
|
3021
|
-
await verifyGoalRun(
|
|
3234
|
+
await verifyGoalRun(checkedRun);
|
|
3022
3235
|
return;
|
|
3023
3236
|
}
|
|
3024
3237
|
if (decision.kind === "create_task") {
|
|
3025
|
-
await updateGoalTask(props.cwd,
|
|
3238
|
+
await updateGoalTask(props.cwd, checkedRun.id, `auto-${Date.now()}`, {
|
|
3026
3239
|
title: decision.title,
|
|
3027
3240
|
prompt: decision.prompt,
|
|
3028
3241
|
status: "pending",
|
|
3029
3242
|
});
|
|
3030
|
-
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id ===
|
|
3243
|
+
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === checkedRun.id) ?? checkedRun;
|
|
3031
3244
|
await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
|
|
3032
|
-
setTimeout(() => continueGoalRun(
|
|
3245
|
+
setTimeout(() => continueGoalRun(checkedRun.id), 250);
|
|
3033
3246
|
return;
|
|
3034
3247
|
}
|
|
3035
3248
|
if (decision.kind === "blocked") {
|
|
3036
3249
|
await upsertGoalRun(props.cwd, {
|
|
3037
|
-
...
|
|
3250
|
+
...checkedRun,
|
|
3038
3251
|
status: "blocked",
|
|
3039
|
-
blockers: [...
|
|
3252
|
+
blockers: [...checkedRun.blockers, decision.reason],
|
|
3040
3253
|
});
|
|
3041
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3042
3254
|
appendGoalProgress({
|
|
3043
3255
|
kind: "goal_progress",
|
|
3044
3256
|
phase: "terminal",
|
|
3045
|
-
title: `Goal blocked: ${
|
|
3257
|
+
title: `Goal blocked: ${checkedRun.title}`,
|
|
3046
3258
|
detail: decision.reason,
|
|
3047
3259
|
status: "blocked",
|
|
3048
3260
|
});
|
|
3049
|
-
runningGoalIdsRef.current.delete(
|
|
3050
|
-
clearGoalStatusEntry(
|
|
3261
|
+
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
3262
|
+
clearGoalStatusEntry(checkedRun.id);
|
|
3263
|
+
clearGoalModeIfIdle();
|
|
3051
3264
|
return;
|
|
3052
3265
|
}
|
|
3053
3266
|
if (decision.kind === "pause") {
|
|
3054
|
-
const runWithBlockedTask = (await updateGoalTask(props.cwd,
|
|
3267
|
+
const runWithBlockedTask = (await updateGoalTask(props.cwd, checkedRun.id, decision.task.id, {
|
|
3055
3268
|
status: "blocked",
|
|
3056
3269
|
attempts: decision.attempts,
|
|
3057
3270
|
lastSummary: "Paused after worker attempt limit.",
|
|
3058
|
-
})) ??
|
|
3059
|
-
const runWithPauseEvidence = (await appendGoalEvidence(props.cwd,
|
|
3271
|
+
})) ?? checkedRun;
|
|
3272
|
+
const runWithPauseEvidence = (await appendGoalEvidence(props.cwd, checkedRun.id, {
|
|
3060
3273
|
kind: "summary",
|
|
3061
3274
|
label: "Goal paused",
|
|
3062
3275
|
content: decision.reason,
|
|
@@ -3067,31 +3280,32 @@ export function App(props) {
|
|
|
3067
3280
|
continueRequestedAt: undefined,
|
|
3068
3281
|
blockers: Array.from(new Set([...runWithPauseEvidence.blockers, decision.reason])),
|
|
3069
3282
|
});
|
|
3070
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3071
3283
|
appendGoalProgress({
|
|
3072
3284
|
kind: "goal_progress",
|
|
3073
3285
|
phase: "terminal",
|
|
3074
|
-
title: `Goal paused: ${
|
|
3286
|
+
title: `Goal paused: ${checkedRun.title}`,
|
|
3075
3287
|
detail: decision.reason,
|
|
3076
3288
|
status: "paused",
|
|
3077
3289
|
});
|
|
3078
|
-
runningGoalIdsRef.current.delete(
|
|
3079
|
-
clearGoalStatusEntry(
|
|
3290
|
+
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
3291
|
+
clearGoalStatusEntry(checkedRun.id);
|
|
3292
|
+
clearGoalModeIfIdle();
|
|
3080
3293
|
return;
|
|
3081
3294
|
}
|
|
3082
|
-
const runWithAttempt = (await updateGoalTask(props.cwd,
|
|
3295
|
+
const runWithAttempt = (await updateGoalTask(props.cwd, checkedRun.id, decision.task.id, {
|
|
3083
3296
|
attempts: decision.attempts,
|
|
3084
|
-
})) ??
|
|
3297
|
+
})) ?? checkedRun;
|
|
3085
3298
|
const worker = await startGoalWorker({
|
|
3086
3299
|
cwd: props.cwd,
|
|
3087
3300
|
provider: currentProvider,
|
|
3088
3301
|
model: currentModel,
|
|
3089
|
-
goalRunId:
|
|
3302
|
+
goalRunId: checkedRun.id,
|
|
3090
3303
|
goalTaskId: decision.task.id,
|
|
3091
3304
|
taskTitle: decision.task.title,
|
|
3092
|
-
prompt: decision.task.prompt,
|
|
3305
|
+
prompt: buildGoalTaskPromptWithReferences(checkedRun, decision.task.prompt),
|
|
3093
3306
|
});
|
|
3094
|
-
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id ===
|
|
3307
|
+
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === checkedRun.id) ??
|
|
3308
|
+
runWithAttempt;
|
|
3095
3309
|
await upsertGoalRun(props.cwd, {
|
|
3096
3310
|
...latestRun,
|
|
3097
3311
|
status: "running",
|
|
@@ -3101,8 +3315,6 @@ export function App(props) {
|
|
|
3101
3315
|
? { ...item, status: "running", workerId: worker.id, attempts: decision.attempts }
|
|
3102
3316
|
: item),
|
|
3103
3317
|
});
|
|
3104
|
-
setOverlay(null);
|
|
3105
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3106
3318
|
appendGoalProgress({
|
|
3107
3319
|
kind: "goal_progress",
|
|
3108
3320
|
phase: "worker_started",
|
|
@@ -3112,16 +3324,17 @@ export function App(props) {
|
|
|
3112
3324
|
status: worker.status,
|
|
3113
3325
|
});
|
|
3114
3326
|
upsertGoalStatusEntry({
|
|
3115
|
-
runId:
|
|
3327
|
+
runId: checkedRun.id,
|
|
3116
3328
|
label: decision.task.title,
|
|
3117
3329
|
phase: "worker",
|
|
3118
3330
|
startedAt: Date.now(),
|
|
3119
3331
|
detail: "background worker running",
|
|
3120
3332
|
workerId: worker.id,
|
|
3121
|
-
goalNumber: goalNumberForRun(
|
|
3333
|
+
goalNumber: goalNumberForRun(checkedRun.id),
|
|
3122
3334
|
});
|
|
3123
3335
|
})().catch((err) => {
|
|
3124
3336
|
clearGoalStatusEntry(run.id);
|
|
3337
|
+
clearGoalModeIfIdle();
|
|
3125
3338
|
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3126
3339
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3127
3340
|
});
|
|
@@ -3130,11 +3343,14 @@ export function App(props) {
|
|
|
3130
3343
|
currentProvider,
|
|
3131
3344
|
currentModel,
|
|
3132
3345
|
appendGoalProgress,
|
|
3346
|
+
clearGoalModeIfIdle,
|
|
3133
3347
|
clearGoalStatusEntry,
|
|
3134
3348
|
goalNumberForRun,
|
|
3349
|
+
setGoalModeAndPrompt,
|
|
3135
3350
|
upsertGoalStatusEntry,
|
|
3136
3351
|
]);
|
|
3137
3352
|
const verifyGoalRun = useCallback(async (run) => {
|
|
3353
|
+
await setGoalModeAndPrompt("coordinator");
|
|
3138
3354
|
if (!run.verifier?.command) {
|
|
3139
3355
|
await appendGoalEvidence(props.cwd, run.id, {
|
|
3140
3356
|
kind: "summary",
|
|
@@ -3155,6 +3371,7 @@ export function App(props) {
|
|
|
3155
3371
|
});
|
|
3156
3372
|
runningGoalIdsRef.current.delete(run.id);
|
|
3157
3373
|
clearGoalStatusEntry(run.id);
|
|
3374
|
+
clearGoalModeIfIdle();
|
|
3158
3375
|
return;
|
|
3159
3376
|
}
|
|
3160
3377
|
activeVerifierRunIdsRef.current.add(run.id);
|
|
@@ -3201,11 +3418,22 @@ export function App(props) {
|
|
|
3201
3418
|
command: run.verifier?.command,
|
|
3202
3419
|
lastResult: verification,
|
|
3203
3420
|
},
|
|
3421
|
+
...(status === "pass"
|
|
3422
|
+
? {
|
|
3423
|
+
completionAudit: {
|
|
3424
|
+
status: "unknown",
|
|
3425
|
+
summary: "Final completion audit pending for latest verifier result.",
|
|
3426
|
+
checkedAt: verification.checkedAt,
|
|
3427
|
+
verifierCheckedAt: verification.checkedAt,
|
|
3428
|
+
...(verification.outputPath ? { outputPath: verification.outputPath } : {}),
|
|
3429
|
+
},
|
|
3430
|
+
}
|
|
3431
|
+
: {}),
|
|
3204
3432
|
};
|
|
3205
3433
|
const completionCheck = canCompleteGoalRun(runWithVerifier);
|
|
3206
3434
|
const verifiedRun = await upsertGoalRun(props.cwd, {
|
|
3207
3435
|
...runWithVerifier,
|
|
3208
|
-
continueRequestedAt:
|
|
3436
|
+
continueRequestedAt: latestRun.continueRequestedAt,
|
|
3209
3437
|
status: status === "pass" && completionCheck.ok ? "passed" : "ready",
|
|
3210
3438
|
});
|
|
3211
3439
|
await appendGoalEvidence(props.cwd, run.id, {
|
|
@@ -3219,7 +3447,6 @@ export function App(props) {
|
|
|
3219
3447
|
reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
|
|
3220
3448
|
content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
|
|
3221
3449
|
});
|
|
3222
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3223
3450
|
appendGoalProgress({
|
|
3224
3451
|
kind: "goal_progress",
|
|
3225
3452
|
phase: "verifier_finished",
|
|
@@ -3238,22 +3465,25 @@ export function App(props) {
|
|
|
3238
3465
|
const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
|
|
3239
3466
|
runGoalSyntheticEvent(eventText);
|
|
3240
3467
|
const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
|
|
3241
|
-
if (continuationRun?.continueRequestedAt || status === "fail") {
|
|
3468
|
+
if (continuationRun?.continueRequestedAt || status === "fail" || status === "pass") {
|
|
3242
3469
|
setTimeout(() => continueGoalRun(run.id), 500);
|
|
3243
3470
|
}
|
|
3244
3471
|
})
|
|
3245
3472
|
.catch((err) => {
|
|
3246
3473
|
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3247
3474
|
clearGoalStatusEntry(run.id);
|
|
3475
|
+
clearGoalModeIfIdle();
|
|
3248
3476
|
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3249
3477
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
|
|
3250
3478
|
});
|
|
3251
3479
|
}, [
|
|
3252
3480
|
props.cwd,
|
|
3253
3481
|
appendGoalProgress,
|
|
3482
|
+
clearGoalModeIfIdle,
|
|
3254
3483
|
clearGoalStatusEntry,
|
|
3255
3484
|
goalNumberForRun,
|
|
3256
3485
|
runGoalSyntheticEvent,
|
|
3486
|
+
setGoalModeAndPrompt,
|
|
3257
3487
|
upsertGoalStatusEntry,
|
|
3258
3488
|
]);
|
|
3259
3489
|
const pauseGoalRun = useCallback((run) => {
|
|
@@ -3267,7 +3497,6 @@ export function App(props) {
|
|
|
3267
3497
|
status: "paused",
|
|
3268
3498
|
activeWorkerId: undefined,
|
|
3269
3499
|
});
|
|
3270
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3271
3500
|
appendGoalProgress({
|
|
3272
3501
|
kind: "goal_progress",
|
|
3273
3502
|
phase: "terminal",
|
|
@@ -3276,19 +3505,14 @@ export function App(props) {
|
|
|
3276
3505
|
status: "paused",
|
|
3277
3506
|
});
|
|
3278
3507
|
clearGoalStatusEntry(run.id);
|
|
3508
|
+
clearGoalModeIfIdle();
|
|
3279
3509
|
})().catch((err) => {
|
|
3280
3510
|
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3281
3511
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3282
3512
|
});
|
|
3283
|
-
}, [appendGoalProgress, clearGoalStatusEntry, props.cwd]);
|
|
3513
|
+
}, [appendGoalProgress, clearGoalModeIfIdle, clearGoalStatusEntry, props.cwd]);
|
|
3284
3514
|
// Keep refs in sync for access from stale closures (onDone)
|
|
3285
|
-
startTaskRef.current = startTask;
|
|
3286
3515
|
startGoalRunRef.current = startGoalRun;
|
|
3287
|
-
useEffect(() => {
|
|
3288
|
-
runAllTasksRef.current = runAllTasks;
|
|
3289
|
-
if (props.sessionStore)
|
|
3290
|
-
props.sessionStore.runAllTasks = runAllTasks;
|
|
3291
|
-
}, [runAllTasks, props.sessionStore]);
|
|
3292
3516
|
useEffect(() => {
|
|
3293
3517
|
agentRunningRef.current = agentLoop.isRunning;
|
|
3294
3518
|
}, [agentLoop.isRunning]);
|
|
@@ -3337,13 +3561,14 @@ export function App(props) {
|
|
|
3337
3561
|
injectedLanguagesRef.current = detectedForPixelFix;
|
|
3338
3562
|
const newSystemPrompt = await rebuildSystemPrompt({
|
|
3339
3563
|
cwd: prep.projectPath,
|
|
3340
|
-
planMode: false,
|
|
3341
3564
|
clearApprovedPlan: true,
|
|
3342
3565
|
activeLanguages: detectedForPixelFix,
|
|
3343
3566
|
tools: toolsForPixelFix,
|
|
3344
3567
|
});
|
|
3345
3568
|
// Now that the cwd swap is committed, reset chat. Do not clear the
|
|
3346
3569
|
// terminal here; terminal clear sequences can erase saved scrollback.
|
|
3570
|
+
pendingHistoryFlushRef.current = [];
|
|
3571
|
+
props.terminalHistoryPrinter?.clear();
|
|
3347
3572
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3348
3573
|
setLiveItems([]);
|
|
3349
3574
|
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
@@ -3363,10 +3588,10 @@ export function App(props) {
|
|
|
3363
3588
|
messagesRef.current.unshift({ role: "system", content: newSystemPrompt });
|
|
3364
3589
|
}
|
|
3365
3590
|
const title = `Fix ${errorId.slice(0, 12)}… in ${prep.projectName}`;
|
|
3366
|
-
const
|
|
3591
|
+
const goalItem = { kind: "goal", title, id: getId() };
|
|
3367
3592
|
setLastUserMessage(title);
|
|
3368
3593
|
setDoneStatus(null);
|
|
3369
|
-
setLiveItems([
|
|
3594
|
+
setLiveItems([goalItem]);
|
|
3370
3595
|
await agentLoop.run(prep.prompt);
|
|
3371
3596
|
}
|
|
3372
3597
|
catch (err) {
|
|
@@ -3387,266 +3612,333 @@ export function App(props) {
|
|
|
3387
3612
|
if (props.sessionStore)
|
|
3388
3613
|
props.sessionStore.runAllPixel = runAllPixel;
|
|
3389
3614
|
}, [runAllPixel, props.sessionStore]);
|
|
3390
|
-
const isTaskView = overlay === "tasks";
|
|
3391
3615
|
const isGoalView = overlay === "goal";
|
|
3392
3616
|
const isSkillsView = overlay === "skills";
|
|
3393
3617
|
const isPlanView = overlay === "plan";
|
|
3394
|
-
const isEyesView = overlay === "eyes";
|
|
3395
3618
|
const footerStatusLayout = getFooterStatusLayoutDecision({
|
|
3396
3619
|
columns,
|
|
3397
3620
|
backgroundTaskCount: bgTasks.length,
|
|
3398
|
-
eyesCount,
|
|
3399
3621
|
updatePending,
|
|
3400
3622
|
});
|
|
3623
|
+
const activityVisible = agentLoop.isRunning && agentLoop.activityPhase !== "idle";
|
|
3624
|
+
const stallStatusVisible = !activityVisible && !!agentLoop.stallError;
|
|
3625
|
+
const doneStatusVisible = !activityVisible && !stallStatusVisible && !!doneStatus && !agentLoop.isRunning;
|
|
3626
|
+
const statusSlotVisible = activityVisible || stallStatusVisible || doneStatusVisible;
|
|
3627
|
+
const [controlsHeight, setControlsHeight] = useState(0);
|
|
3628
|
+
const controlsObserverRef = useRef(null);
|
|
3629
|
+
const mainControlsRef = useCallback((node) => {
|
|
3630
|
+
if (controlsObserverRef.current) {
|
|
3631
|
+
controlsObserverRef.current.disconnect();
|
|
3632
|
+
controlsObserverRef.current = null;
|
|
3633
|
+
}
|
|
3634
|
+
if (!node || typeof ResizeObserver === "undefined")
|
|
3635
|
+
return;
|
|
3636
|
+
const observer = new ResizeObserver((entries) => {
|
|
3637
|
+
const entry = entries[0];
|
|
3638
|
+
if (!entry)
|
|
3639
|
+
return;
|
|
3640
|
+
const roundedHeight = Math.round(entry.contentRect.height);
|
|
3641
|
+
setControlsHeight((prev) => (roundedHeight !== prev ? roundedHeight : prev));
|
|
3642
|
+
});
|
|
3643
|
+
observer.observe(node);
|
|
3644
|
+
controlsObserverRef.current = observer;
|
|
3645
|
+
}, []);
|
|
3646
|
+
useEffect(() => () => controlsObserverRef.current?.disconnect(), []);
|
|
3647
|
+
const footerFitsOnOneLine = doesFooterFitOnOneLine({
|
|
3648
|
+
columns,
|
|
3649
|
+
model: currentModel,
|
|
3650
|
+
tokensIn: agentLoop.contextUsed,
|
|
3651
|
+
contextWindowOptions,
|
|
3652
|
+
cwd: displayedCwd,
|
|
3653
|
+
gitBranch,
|
|
3654
|
+
thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
|
|
3655
|
+
goalMode,
|
|
3656
|
+
});
|
|
3657
|
+
const chatControlsLayout = getChatControlsLayoutDecision({
|
|
3658
|
+
rows,
|
|
3659
|
+
columns,
|
|
3660
|
+
agentRunning: agentLoop.isRunning,
|
|
3661
|
+
activityVisible,
|
|
3662
|
+
doneStatusVisible,
|
|
3663
|
+
stallStatusVisible,
|
|
3664
|
+
exitPending,
|
|
3665
|
+
footerStatusLayout,
|
|
3666
|
+
taskBarExpanded,
|
|
3667
|
+
goalStatusEntryCount: goalStatusEntries.length,
|
|
3668
|
+
footerFitsOnOneLine,
|
|
3669
|
+
});
|
|
3670
|
+
const stableControlsRows = controlsHeight > 0 ? controlsHeight : chatControlsLayout.controlsRows;
|
|
3671
|
+
const measuredLiveAreaRows = Math.max(MIN_LIVE_AREA_ROWS, rows - stableControlsRows - 1);
|
|
3401
3672
|
const isPixelView = overlay === "pixel";
|
|
3402
|
-
const
|
|
3403
|
-
const
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3673
|
+
const hasLiveAssistantItem = liveItems.some((item) => item.kind === "assistant");
|
|
3674
|
+
const rawVisibleStreamingText = goalModeStateRef.current === "planner" || hasLiveAssistantItem ? "" : agentLoop.streamingText;
|
|
3675
|
+
useEffect(() => {
|
|
3676
|
+
if (!rawVisibleStreamingText) {
|
|
3677
|
+
streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3680
|
+
if (rawVisibleStreamingText === streamedAssistantFlushRef.current.text)
|
|
3681
|
+
return;
|
|
3682
|
+
const alreadyFlushed = streamedAssistantFlushRef.current.flushedChars;
|
|
3683
|
+
const unflushedText = rawVisibleStreamingText.slice(alreadyFlushed);
|
|
3684
|
+
const split = splitAssistantStreamingText(unflushedText);
|
|
3685
|
+
if (split.flushedText.length > 0) {
|
|
3686
|
+
queueFlush([
|
|
3687
|
+
{
|
|
3688
|
+
kind: "assistant",
|
|
3689
|
+
text: split.flushedText,
|
|
3690
|
+
continuation: streamedAssistantFlushRef.current.flushedChars > 0,
|
|
3691
|
+
id: getId(),
|
|
3692
|
+
},
|
|
3693
|
+
]);
|
|
3694
|
+
streamedAssistantFlushRef.current = {
|
|
3695
|
+
flushedChars: alreadyFlushed + split.flushedText.length,
|
|
3696
|
+
text: rawVisibleStreamingText,
|
|
3697
|
+
};
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
streamedAssistantFlushRef.current = {
|
|
3701
|
+
...streamedAssistantFlushRef.current,
|
|
3702
|
+
text: rawVisibleStreamingText,
|
|
3703
|
+
};
|
|
3704
|
+
}, [rawVisibleStreamingText, queueFlush]);
|
|
3705
|
+
const visibleStreamingText = rawVisibleStreamingText.slice(streamedAssistantFlushRef.current.flushedChars);
|
|
3706
|
+
const shouldReserveStreamingSpacing = agentLoop.isRunning &&
|
|
3707
|
+
!hasLiveAssistantItem &&
|
|
3708
|
+
(visibleStreamingText.trim().length > 0 || liveItems.some(isAgentSpacingItem));
|
|
3709
|
+
const shouldTopSpaceStreamingText = shouldTopSpaceStreamingAssistant({
|
|
3710
|
+
visibleStreamingText,
|
|
3711
|
+
lastLiveItem: liveItems.at(-1),
|
|
3712
|
+
lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
|
|
3713
|
+
lastHistoryItem: history.at(-1),
|
|
3407
3714
|
});
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3715
|
+
return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, autoExpandNewest: goalAutoExpand, onClose: () => {
|
|
3716
|
+
goalAutoExpandRef.current = false;
|
|
3717
|
+
setGoalAutoExpand(false);
|
|
3718
|
+
if (props.sessionStore)
|
|
3719
|
+
props.sessionStore.goalAutoExpand = false;
|
|
3720
|
+
closeOverlay();
|
|
3721
|
+
}, onRunGoal: (run) => {
|
|
3722
|
+
const paneTransition = getGoalActivationPaneTransition();
|
|
3723
|
+
goalAutoExpandRef.current = paneTransition.goalAutoExpand;
|
|
3724
|
+
setGoalAutoExpand(paneTransition.goalAutoExpand);
|
|
3725
|
+
setPlanAutoExpand(paneTransition.planAutoExpand);
|
|
3726
|
+
if (props.sessionStore) {
|
|
3727
|
+
props.sessionStore.overlay = paneTransition.overlay;
|
|
3728
|
+
props.sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
|
|
3729
|
+
props.sessionStore.planAutoExpand = paneTransition.planAutoExpand;
|
|
3730
|
+
}
|
|
3731
|
+
if (paneTransition.resetReviewScreen && props.resetUI && props.sessionStore) {
|
|
3732
|
+
props.sessionStore.pendingGoalRun = run;
|
|
3733
|
+
props.resetUI();
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
setOverlay(paneTransition.overlay);
|
|
3737
|
+
startGoalRun(run);
|
|
3738
|
+
}, onVerifyGoal: (run) => {
|
|
3739
|
+
void verifyGoalRun(run);
|
|
3740
|
+
}, onPauseGoal: (run) => {
|
|
3741
|
+
pauseGoalRun(run);
|
|
3742
|
+
}, onRefineGoal: (run, feedback) => {
|
|
3743
|
+
goalAutoExpandRef.current = true;
|
|
3744
|
+
setGoalAutoExpand(true);
|
|
3745
|
+
void (async () => {
|
|
3746
|
+
try {
|
|
3747
|
+
await setGoalModeAndPrompt("setup");
|
|
3748
|
+
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}`);
|
|
3418
3749
|
}
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
if (agentLoop.isRunning)
|
|
3423
|
-
props.sessionStore.pendingResetUI = true;
|
|
3424
|
-
}
|
|
3425
|
-
setTaskCount(getTaskCount(props.cwd));
|
|
3426
|
-
setOverlay(null);
|
|
3750
|
+
catch (err) {
|
|
3751
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3752
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3427
3753
|
}
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3754
|
+
finally {
|
|
3755
|
+
await setGoalModeAndPrompt("off");
|
|
3756
|
+
const paneTransition = getGoalSetupFinishedPaneTransition();
|
|
3757
|
+
goalAutoExpandRef.current = paneTransition.goalAutoExpand;
|
|
3758
|
+
setTimeout(() => {
|
|
3759
|
+
const resetUI = props.resetUI;
|
|
3760
|
+
const sessionStore = props.sessionStore;
|
|
3761
|
+
if (shouldResetUIForGoalSetupPaneTransition({
|
|
3762
|
+
hasResetUI: resetUI !== undefined,
|
|
3763
|
+
hasSessionStore: sessionStore !== undefined,
|
|
3764
|
+
}) &&
|
|
3765
|
+
resetUI &&
|
|
3766
|
+
sessionStore) {
|
|
3767
|
+
sessionStore.overlay = paneTransition.overlay;
|
|
3768
|
+
sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
|
|
3769
|
+
sessionStore.planAutoExpand = paneTransition.planAutoExpand;
|
|
3770
|
+
resetUI();
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
setGoalAutoExpand(paneTransition.goalAutoExpand);
|
|
3774
|
+
setPlanAutoExpand(paneTransition.planAutoExpand);
|
|
3775
|
+
setOverlay(paneTransition.overlay);
|
|
3776
|
+
}, 300);
|
|
3438
3777
|
}
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
}, onPauseGoal: (run) => {
|
|
3448
|
-
pauseGoalRun(run);
|
|
3449
|
-
} })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
3450
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3778
|
+
})();
|
|
3779
|
+
} })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
3780
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3781
|
+
props.sessionStore.overlay = null;
|
|
3782
|
+
props.resetUI();
|
|
3783
|
+
}
|
|
3784
|
+
else {
|
|
3785
|
+
if (props.sessionStore) {
|
|
3451
3786
|
props.sessionStore.overlay = null;
|
|
3452
|
-
|
|
3787
|
+
if (agentLoop.isRunning)
|
|
3788
|
+
props.sessionStore.pendingResetUI = true;
|
|
3453
3789
|
}
|
|
3454
|
-
else {
|
|
3455
|
-
if (props.sessionStore) {
|
|
3456
|
-
props.sessionStore.overlay = null;
|
|
3457
|
-
if (agentLoop.isRunning)
|
|
3458
|
-
props.sessionStore.pendingResetUI = true;
|
|
3459
|
-
}
|
|
3460
|
-
setOverlay(null);
|
|
3461
|
-
}
|
|
3462
|
-
}, onFixOne: (entry) => {
|
|
3463
|
-
setOverlay(null);
|
|
3464
|
-
startPixelFix(entry.errorId);
|
|
3465
|
-
}, onFixAll: (entries) => {
|
|
3466
|
-
const first = entries.find((e) => e.status === "open") ?? entries[0];
|
|
3467
|
-
if (!first)
|
|
3468
|
-
return;
|
|
3469
3790
|
setOverlay(null);
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
}
|
|
3486
|
-
|
|
3791
|
+
}
|
|
3792
|
+
}, onFixOne: (entry) => {
|
|
3793
|
+
setOverlay(null);
|
|
3794
|
+
startPixelFix(entry.errorId);
|
|
3795
|
+
}, onFixAll: (entries) => {
|
|
3796
|
+
const first = entries.find((e) => e.status === "open") ?? entries[0];
|
|
3797
|
+
if (!first)
|
|
3798
|
+
return;
|
|
3799
|
+
setOverlay(null);
|
|
3800
|
+
setRunAllPixel(true);
|
|
3801
|
+
startPixelFix(first.errorId);
|
|
3802
|
+
} })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
|
|
3803
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3804
|
+
props.sessionStore.overlay = null;
|
|
3805
|
+
props.resetUI();
|
|
3806
|
+
}
|
|
3807
|
+
else {
|
|
3808
|
+
if (props.sessionStore) {
|
|
3487
3809
|
props.sessionStore.overlay = null;
|
|
3488
|
-
|
|
3810
|
+
if (agentLoop.isRunning)
|
|
3811
|
+
props.sessionStore.pendingResetUI = true;
|
|
3489
3812
|
}
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
} })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
|
|
3502
|
-
planOverlayPendingRef.current = false;
|
|
3503
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3813
|
+
setOverlay(null);
|
|
3814
|
+
}
|
|
3815
|
+
} })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
|
|
3816
|
+
planOverlayPendingRef.current = false;
|
|
3817
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3818
|
+
props.sessionStore.overlay = null;
|
|
3819
|
+
props.sessionStore.planAutoExpand = false;
|
|
3820
|
+
props.resetUI();
|
|
3821
|
+
}
|
|
3822
|
+
else {
|
|
3823
|
+
if (props.sessionStore) {
|
|
3504
3824
|
props.sessionStore.overlay = null;
|
|
3505
3825
|
props.sessionStore.planAutoExpand = false;
|
|
3506
|
-
|
|
3826
|
+
if (agentLoop.isRunning)
|
|
3827
|
+
props.sessionStore.pendingResetUI = true;
|
|
3507
3828
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3829
|
+
setPlanAutoExpand(false);
|
|
3830
|
+
setOverlay(null);
|
|
3831
|
+
}
|
|
3832
|
+
}, onApprove: (planPath) => {
|
|
3833
|
+
log("INFO", "plan", "Plan approved — transitioning to implementation", {
|
|
3834
|
+
planPath,
|
|
3835
|
+
});
|
|
3836
|
+
planOverlayPendingRef.current = false;
|
|
3837
|
+
void (async () => {
|
|
3838
|
+
try {
|
|
3839
|
+
// Read plan steps for progress tracking — handed to the new
|
|
3840
|
+
// mount via sessionStore.planSteps below.
|
|
3841
|
+
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
3842
|
+
const steps = extractPlanSteps(planContent);
|
|
3843
|
+
// Build the new system prompt with the approved plan baked in.
|
|
3844
|
+
const newPrompt = await rebuildSystemPrompt({
|
|
3845
|
+
approvedPlanPath: planPath,
|
|
3846
|
+
});
|
|
3847
|
+
// Create a new session file BEFORE remount so the new tree
|
|
3848
|
+
// picks it up via sessionStore.sessionPath.
|
|
3849
|
+
let newSessionPath;
|
|
3850
|
+
const sm = sessionManagerRef.current;
|
|
3851
|
+
if (sm) {
|
|
3852
|
+
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
3853
|
+
newSessionPath = s.path;
|
|
3854
|
+
}
|
|
3855
|
+
if (props.resetUI && props.sessionStore) {
|
|
3856
|
+
// Clear the overlay so the new mount lands on the chat,
|
|
3857
|
+
// not back inside the plan pane.
|
|
3510
3858
|
props.sessionStore.overlay = null;
|
|
3511
3859
|
props.sessionStore.planAutoExpand = false;
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
setPlanAutoExpand(false);
|
|
3516
|
-
setOverlay(null);
|
|
3517
|
-
}
|
|
3518
|
-
}, onApprove: (planPath) => {
|
|
3519
|
-
log("INFO", "plan", "Plan approved — transitioning to implementation", {
|
|
3520
|
-
planPath,
|
|
3521
|
-
});
|
|
3522
|
-
planOverlayPendingRef.current = false;
|
|
3523
|
-
void (async () => {
|
|
3524
|
-
try {
|
|
3525
|
-
// Read plan steps for progress tracking — handed to the new
|
|
3526
|
-
// mount via sessionStore.planSteps below.
|
|
3527
|
-
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
3528
|
-
const steps = extractPlanSteps(planContent);
|
|
3529
|
-
// Build the new system prompt with the approved plan baked in.
|
|
3530
|
-
const newPrompt = await rebuildSystemPrompt({
|
|
3531
|
-
planMode: false,
|
|
3860
|
+
props.resetUI({
|
|
3861
|
+
wipeSession: true,
|
|
3862
|
+
messages: [{ role: "system", content: newPrompt }],
|
|
3532
3863
|
approvedPlanPath: planPath,
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
if (sm) {
|
|
3539
|
-
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
3540
|
-
newSessionPath = s.path;
|
|
3541
|
-
}
|
|
3542
|
-
if (props.resetUI && props.sessionStore) {
|
|
3543
|
-
// Clear the overlay so the new mount lands on the chat,
|
|
3544
|
-
// not back inside the plan pane.
|
|
3545
|
-
props.sessionStore.overlay = null;
|
|
3546
|
-
props.sessionStore.planAutoExpand = false;
|
|
3547
|
-
props.resetUI({
|
|
3548
|
-
wipeSession: true,
|
|
3549
|
-
messages: [{ role: "system", content: newPrompt }],
|
|
3550
|
-
approvedPlanPath: planPath,
|
|
3551
|
-
planSteps: steps,
|
|
3552
|
-
sessionPath: newSessionPath,
|
|
3553
|
-
pendingAction: {
|
|
3554
|
-
prompt: "The plan has been approved. Implement it now, following each step in order.",
|
|
3555
|
-
planEvent: { event: "approved" },
|
|
3556
|
-
},
|
|
3557
|
-
});
|
|
3558
|
-
return;
|
|
3559
|
-
}
|
|
3560
|
-
// Fallback path (resetUI not wired — tests). Mutate in place.
|
|
3561
|
-
approvedPlanPathRef.current = planPath;
|
|
3562
|
-
planStepsRef.current = steps;
|
|
3563
|
-
setPlanSteps(steps);
|
|
3564
|
-
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3565
|
-
setLiveItems([]);
|
|
3566
|
-
setPlanAutoExpand(false);
|
|
3567
|
-
setOverlay(null);
|
|
3568
|
-
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
3569
|
-
agentLoop.reset();
|
|
3570
|
-
persistedIndexRef.current = messagesRef.current.length;
|
|
3571
|
-
if (newSessionPath)
|
|
3572
|
-
sessionPathRef.current = newSessionPath;
|
|
3573
|
-
setLiveItems([
|
|
3574
|
-
{
|
|
3575
|
-
kind: "info",
|
|
3576
|
-
text: "Plan approved — starting fresh session for implementation",
|
|
3577
|
-
id: getId(),
|
|
3864
|
+
planSteps: steps,
|
|
3865
|
+
sessionPath: newSessionPath,
|
|
3866
|
+
pendingAction: {
|
|
3867
|
+
prompt: "The plan has been approved. Implement it now, following each step in order.",
|
|
3868
|
+
planEvent: { event: "approved" },
|
|
3578
3869
|
},
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
|
|
3582
|
-
}
|
|
3583
|
-
catch (err) {
|
|
3584
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3585
|
-
log("ERROR", "error", errMsg);
|
|
3586
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3870
|
+
});
|
|
3871
|
+
return;
|
|
3587
3872
|
}
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3873
|
+
// Fallback path (resetUI not wired — tests). Mutate in place.
|
|
3874
|
+
approvedPlanPathRef.current = planPath;
|
|
3875
|
+
planStepsRef.current = steps;
|
|
3876
|
+
setPlanSteps(steps);
|
|
3877
|
+
pendingHistoryFlushRef.current = [];
|
|
3878
|
+
props.terminalHistoryPrinter?.clear();
|
|
3879
|
+
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3880
|
+
setLiveItems([]);
|
|
3881
|
+
setPlanAutoExpand(false);
|
|
3882
|
+
setOverlay(null);
|
|
3883
|
+
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
3884
|
+
agentLoop.reset();
|
|
3885
|
+
persistedIndexRef.current = messagesRef.current.length;
|
|
3886
|
+
if (newSessionPath)
|
|
3887
|
+
sessionPathRef.current = newSessionPath;
|
|
3888
|
+
setLiveItems([
|
|
3889
|
+
{
|
|
3890
|
+
kind: "info",
|
|
3891
|
+
text: "Plan approved — starting fresh session for implementation",
|
|
3892
|
+
id: getId(),
|
|
3602
3893
|
},
|
|
3603
|
-
|
|
3604
|
-
|
|
3894
|
+
]);
|
|
3895
|
+
setDoneStatus(null);
|
|
3896
|
+
await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
|
|
3605
3897
|
}
|
|
3606
|
-
|
|
3607
|
-
setOverlay(null);
|
|
3608
|
-
setDoneStatus(null);
|
|
3609
|
-
setLiveItems((prev) => [
|
|
3610
|
-
...prev,
|
|
3611
|
-
{ kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
|
|
3612
|
-
]);
|
|
3613
|
-
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
3898
|
+
catch (err) {
|
|
3614
3899
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3615
3900
|
log("ERROR", "error", errMsg);
|
|
3616
3901
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3902
|
+
}
|
|
3903
|
+
})();
|
|
3904
|
+
}, onReject: (planPath, feedback) => {
|
|
3905
|
+
planOverlayPendingRef.current = false;
|
|
3906
|
+
const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
|
|
3907
|
+
`Please revise the plan based on this feedback.`;
|
|
3908
|
+
if (props.resetUI && props.sessionStore) {
|
|
3909
|
+
props.sessionStore.overlay = null;
|
|
3910
|
+
props.sessionStore.planAutoExpand = false;
|
|
3911
|
+
// No wipeSession — keep history and messages so the agent picks
|
|
3912
|
+
// up the rejection mid-conversation.
|
|
3913
|
+
props.resetUI({
|
|
3914
|
+
pendingAction: {
|
|
3915
|
+
prompt: rejectionMsg,
|
|
3916
|
+
planEvent: { event: "rejected", detail: feedback },
|
|
3917
|
+
},
|
|
3617
3918
|
});
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
id: getId(),
|
|
3642
|
-
},
|
|
3643
|
-
]);
|
|
3644
|
-
}, 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 ||
|
|
3645
|
-
footerStatusLayout.hasEyesSignals ||
|
|
3646
|
-
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 ||
|
|
3647
|
-
(!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
|
|
3648
|
-
? 1
|
|
3649
|
-
: 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
setPlanAutoExpand(false);
|
|
3922
|
+
setOverlay(null);
|
|
3923
|
+
setDoneStatus(null);
|
|
3924
|
+
setLiveItems((prev) => [
|
|
3925
|
+
...prev,
|
|
3926
|
+
{ kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
|
|
3927
|
+
]);
|
|
3928
|
+
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
3929
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3930
|
+
log("ERROR", "error", errMsg);
|
|
3931
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3932
|
+
});
|
|
3933
|
+
} })) : (_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: () => {
|
|
3934
|
+
openOverlay("goal");
|
|
3935
|
+
}, onToggleSkills: () => {
|
|
3936
|
+
openOverlay("skills");
|
|
3937
|
+
}, onTogglePixel: () => {
|
|
3938
|
+
openOverlay("pixel");
|
|
3939
|
+
}, onToggleMarkdown: () => {
|
|
3940
|
+
setRenderMarkdown((prev) => !prev);
|
|
3941
|
+
}, 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" }) }))] }))] })] })) }));
|
|
3650
3942
|
}
|
|
3651
3943
|
function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
|
|
3652
3944
|
const status = enabled ? "on" : "off";
|