@kenkaiiii/ggcoder 4.3.224 → 4.3.226
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/dist/cli/auth.d.ts +4 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +344 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/pixel.d.ts +27 -0
- package/dist/cli/pixel.d.ts.map +1 -0
- package/dist/cli/pixel.js +103 -0
- package/dist/cli/pixel.js.map +1 -0
- package/dist/cli/shared.d.ts +13 -0
- package/dist/cli/shared.d.ts.map +1 -0
- package/dist/cli/shared.js +82 -0
- package/dist/cli/shared.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -537
- package/dist/cli.js.map +1 -1
- package/dist/core/runtime-mode.js +1 -1
- package/dist/core/runtime-mode.js.map +1 -1
- package/dist/system-prompt.js +2 -2
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +2 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/plan-mode.test.js +13 -1
- package/dist/tools/plan-mode.test.js.map +1 -1
- package/dist/tools/read-only-bash.d.ts +13 -0
- package/dist/tools/read-only-bash.d.ts.map +1 -0
- package/dist/tools/read-only-bash.js +155 -0
- package/dist/tools/read-only-bash.js.map +1 -0
- package/dist/tools/read-only-bash.test.d.ts +2 -0
- package/dist/tools/read-only-bash.test.d.ts.map +1 -0
- package/dist/tools/read-only-bash.test.js +47 -0
- package/dist/tools/read-only-bash.test.js.map +1 -0
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +123 -1039
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/assistant-stream-flush-parity.test.d.ts +2 -0
- package/dist/ui/assistant-stream-flush-parity.test.d.ts.map +1 -0
- package/dist/ui/assistant-stream-flush-parity.test.js +85 -0
- package/dist/ui/assistant-stream-flush-parity.test.js.map +1 -0
- package/dist/ui/components/ChatLayout.d.ts +8 -1
- package/dist/ui/components/ChatLayout.d.ts.map +1 -1
- package/dist/ui/components/ChatLayout.js +4 -2
- package/dist/ui/components/ChatLayout.js.map +1 -1
- package/dist/ui/components/ChatLivePane.d.ts.map +1 -1
- package/dist/ui/components/ChatLivePane.js +15 -1
- package/dist/ui/components/ChatLivePane.js.map +1 -1
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/InputArea.js +1 -3
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/SelectList.d.ts.map +1 -1
- package/dist/ui/components/SelectList.js +5 -0
- package/dist/ui/components/SelectList.js.map +1 -1
- package/dist/ui/components/SlashStyledSelectList.d.ts.map +1 -1
- package/dist/ui/components/SlashStyledSelectList.js +5 -0
- package/dist/ui/components/SlashStyledSelectList.js.map +1 -1
- package/dist/ui/components/ToolGroupExecution.d.ts +5 -2
- package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolGroupExecution.js +3 -3
- package/dist/ui/components/ToolGroupExecution.js.map +1 -1
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.d.ts.map +1 -1
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/index.js.map +1 -1
- package/dist/ui/hooks/useChatLayoutMeasurements.d.ts.map +1 -1
- package/dist/ui/hooks/useChatLayoutMeasurements.js +4 -1
- package/dist/ui/hooks/useChatLayoutMeasurements.js.map +1 -1
- package/dist/ui/hooks/useContextCompaction.d.ts +41 -0
- package/dist/ui/hooks/useContextCompaction.d.ts.map +1 -0
- package/dist/ui/hooks/useContextCompaction.js +149 -0
- package/dist/ui/hooks/useContextCompaction.js.map +1 -0
- package/dist/ui/hooks/useGoalOrchestration.d.ts +81 -0
- package/dist/ui/hooks/useGoalOrchestration.d.ts.map +1 -0
- package/dist/ui/hooks/useGoalOrchestration.js +745 -0
- package/dist/ui/hooks/useGoalOrchestration.js.map +1 -0
- package/dist/ui/hooks/useModeState.d.ts +61 -0
- package/dist/ui/hooks/useModeState.d.ts.map +1 -0
- package/dist/ui/hooks/useModeState.js +86 -0
- package/dist/ui/hooks/useModeState.js.map +1 -0
- package/dist/ui/hooks/usePixelFixFlow.d.ts +57 -0
- package/dist/ui/hooks/usePixelFixFlow.d.ts.map +1 -0
- package/dist/ui/hooks/usePixelFixFlow.js +102 -0
- package/dist/ui/hooks/usePixelFixFlow.js.map +1 -0
- package/dist/ui/hooks/useSessionPersistence.d.ts +34 -0
- package/dist/ui/hooks/useSessionPersistence.d.ts.map +1 -0
- package/dist/ui/hooks/useSessionPersistence.js +67 -0
- package/dist/ui/hooks/useSessionPersistence.js.map +1 -0
- package/dist/ui/live-area-clamp.test.d.ts +2 -0
- package/dist/ui/live-area-clamp.test.d.ts.map +1 -0
- package/dist/ui/live-area-clamp.test.js +79 -0
- package/dist/ui/live-area-clamp.test.js.map +1 -0
- package/dist/ui/live-area-height.d.ts +35 -0
- package/dist/ui/live-area-height.d.ts.map +1 -0
- package/dist/ui/live-area-height.js +75 -0
- package/dist/ui/live-area-height.js.map +1 -0
- package/dist/ui/live-area-height.test.d.ts +2 -0
- package/dist/ui/live-area-height.test.d.ts.map +1 -0
- package/dist/ui/live-area-height.test.js +67 -0
- package/dist/ui/live-area-height.test.js.map +1 -0
- package/dist/ui/live-frame-height.test.d.ts +2 -0
- package/dist/ui/live-frame-height.test.d.ts.map +1 -0
- package/dist/ui/live-frame-height.test.js +91 -0
- package/dist/ui/live-frame-height.test.js.map +1 -0
- package/dist/ui/terminal-history.d.ts.map +1 -1
- package/dist/ui/terminal-history.js +13 -4
- package/dist/ui/terminal-history.js.map +1 -1
- package/dist/ui/terminal-history.test.js +63 -3
- package/dist/ui/terminal-history.test.js.map +1 -1
- package/dist/ui/tool-group-summary.d.ts +9 -1
- package/dist/ui/tool-group-summary.d.ts.map +1 -1
- package/dist/ui/tool-group-summary.js +7 -6
- package/dist/ui/tool-group-summary.js.map +1 -1
- package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -1
- package/dist/ui/transcript/TranscriptRenderer.js +10 -2
- package/dist/ui/transcript/TranscriptRenderer.js.map +1 -1
- package/dist/ui/transcript/spacing.d.ts +15 -5
- package/dist/ui/transcript/spacing.d.ts.map +1 -1
- package/dist/ui/transcript/spacing.js +35 -9
- package/dist/ui/transcript/spacing.js.map +1 -1
- package/dist/ui/utils/assistant-stream-split.d.ts +17 -0
- package/dist/ui/utils/assistant-stream-split.d.ts.map +1 -1
- package/dist/ui/utils/assistant-stream-split.js +38 -1
- package/dist/ui/utils/assistant-stream-split.js.map +1 -1
- package/dist/ui/utils/assistant-stream-split.test.js +29 -6
- package/dist/ui/utils/assistant-stream-split.test.js.map +1 -1
- package/dist/ui/utils/terminal-input.d.ts +3 -0
- package/dist/ui/utils/terminal-input.d.ts.map +1 -0
- package/dist/ui/utils/terminal-input.js +28 -0
- package/dist/ui/utils/terminal-input.js.map +1 -0
- package/dist/ui/utils/terminal-input.test.d.ts +2 -0
- package/dist/ui/utils/terminal-input.test.d.ts.map +1 -0
- package/dist/ui/utils/terminal-input.test.js +15 -0
- package/dist/ui/utils/terminal-input.test.js.map +1 -0
- package/package.json +12 -4
package/dist/ui/App.js
CHANGED
|
@@ -5,6 +5,11 @@ import { useTerminalSize } from "./hooks/useTerminalSize.js";
|
|
|
5
5
|
import { useChatLayoutMeasurements } from "./hooks/useChatLayoutMeasurements.js";
|
|
6
6
|
import { useTaskPickerController } from "./hooks/useTaskPickerController.js";
|
|
7
7
|
import { useGoalPickerController } from "./hooks/useGoalPickerController.js";
|
|
8
|
+
import { useModeState } from "./hooks/useModeState.js";
|
|
9
|
+
import { useSessionPersistence } from "./hooks/useSessionPersistence.js";
|
|
10
|
+
import { useContextCompaction } from "./hooks/useContextCompaction.js";
|
|
11
|
+
import { usePixelFixFlow } from "./hooks/usePixelFixFlow.js";
|
|
12
|
+
import { useGoalOrchestration } from "./hooks/useGoalOrchestration.js";
|
|
8
13
|
import { useDoublePress } from "./hooks/useDoublePress.js";
|
|
9
14
|
import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
|
|
10
15
|
import { playNotificationSound } from "../utils/sound.js";
|
|
@@ -20,38 +25,30 @@ import { reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStat
|
|
|
20
25
|
import { useTheme, useSetTheme } from "./theme/theme.js";
|
|
21
26
|
import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
|
|
22
27
|
import { getGitBranch } from "../utils/git.js";
|
|
23
|
-
import { getModel
|
|
28
|
+
import { getModel } from "../core/model-registry.js";
|
|
24
29
|
import { SessionManager } from "../core/session-manager.js";
|
|
25
|
-
import { appendMessagesToSession as appendSessionMessages, createCompactedSessionCheckpoint, } from "../core/session-compaction.js";
|
|
26
30
|
import { log } from "../core/logger.js";
|
|
27
31
|
import { getPendingUpdate, startPeriodicUpdateCheck, stopPeriodicUpdateCheck, } from "../core/auto-update.js";
|
|
28
32
|
import { generateSessionTitle } from "../utils/session-title.js";
|
|
29
33
|
import { SettingsManager } from "../core/settings-manager.js";
|
|
30
|
-
import { shouldCompact, compact, getCompactionReserveTokens, } from "../core/compaction/compactor.js";
|
|
31
|
-
import { estimateConversationTokens } from "../core/compaction/token-estimator.js";
|
|
32
34
|
import { PROMPT_COMMANDS, getPromptCommand } from "../core/prompt-commands.js";
|
|
33
35
|
import { isFirstTimeSetup, markSetupAudited, getAnnouncedLanguages, markLanguagesAnnounced, } from "../core/setup-history.js";
|
|
34
36
|
import { loadCustomCommands } from "../core/custom-commands.js";
|
|
35
|
-
import { buildSystemPrompt } from "../system-prompt.js";
|
|
36
37
|
import { detectLanguages } from "../core/language-detector.js";
|
|
37
38
|
import { detectVerifyCommands } from "../core/verify-commands.js";
|
|
38
39
|
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
39
40
|
import { getMCPServers } from "../core/mcp/index.js";
|
|
40
41
|
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
|
|
41
42
|
import { splitAssistantStreamingText } from "./utils/assistant-stream-split.js";
|
|
42
|
-
import {
|
|
43
|
+
import { goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, upsertGoalRun, } from "../core/goal-store.js";
|
|
43
44
|
import { getNextPendingTask, markTaskInProgress } from "../core/tasks-store.js";
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import { runGoalVerifierCommand } from "../core/goal-verifier.js";
|
|
47
|
-
import { checkGoalWorktreeIntegration, isGoalWorktreeDirtyError } from "../core/goal-worktree.js";
|
|
48
|
-
import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
|
|
49
|
-
import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
|
|
45
|
+
import { listGoalWorkers, stopGoalWorker } from "../core/goal-worker.js";
|
|
46
|
+
import { isGoalSyntheticEvent, parseGoalSyntheticEvent } from "./goal-events.js";
|
|
50
47
|
import { buildUserContentWithAttachments } from "./prompt-routing.js";
|
|
51
48
|
import { submitPromptCommand } from "./submit-prompt-command.js";
|
|
52
49
|
import { handleUiSlashCommand } from "./submit-slash-commands.js";
|
|
53
50
|
import { getNextThinkingLevel, isThinkingLevelSupported } from "./thinking-level.js";
|
|
54
|
-
import { appendGoalProgressDraft, completedItemsWithDurableGoalTerminalProgress,
|
|
51
|
+
import { appendGoalProgressDraft, completedItemsWithDurableGoalTerminalProgress, } from "./goal-progress.js";
|
|
55
52
|
import { getDoneFlushDecision, nextGoalModeAfterAgentDone, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
|
|
56
53
|
import { isTranscriptSpacingItem } from "./transcript/spacing.js";
|
|
57
54
|
import { renderTranscriptItem } from "./transcript/TranscriptRenderer.js";
|
|
@@ -59,7 +56,6 @@ import { formatDuration } from "./duration-format.js";
|
|
|
59
56
|
import { pickDurationVerb } from "./duration-summary.js";
|
|
60
57
|
import { toErrorItem } from "./error-item.js";
|
|
61
58
|
import { addLinesChanged, buildSessionSummary, createSessionStats, recordServerToolCall, recordToolEnd, recordTurnEnd, } from "./session-summary.js";
|
|
62
|
-
import { buildGoalDirtyWorktreePauseRun, buildGoalDirtyWorktreeUserPrompt, buildGoalTaskPromptWithReferences, buildGoalUserPauseRun, goalDirtyWorktreeInfoText, goalRunNeedsExplicitContinuationAfterWorker, goalTaskProgress, shouldKeepGoalRunTrackedAfterDecision, shouldRunGoalTaskInMainCheckout, } from "./goal-run-helpers.js";
|
|
63
59
|
import { compactHistory, getNextGeneratedItemId, isActiveItem, isSameAssistantText, normalizeAssistantText, partitionCompleted, pinStreamingTextBeforeToolBoundary, removeItemsWithIds, uniqueItemsById, } from "./item-helpers.js";
|
|
64
60
|
export { buildGoalSetupPromptFromPlanner, buildUserContentWithAttachments, collectAssistantTextSince, isGoalPromptCommandName, routePromptCommandInput, runGoalPromptSetupSequence, } from "./prompt-routing.js";
|
|
65
61
|
export { getNextThinkingLevel } from "./thinking-level.js";
|
|
@@ -88,8 +84,6 @@ export function App(props) {
|
|
|
88
84
|
const [lastUserMessage, setLastUserMessage] = useState("");
|
|
89
85
|
const [exitPending, setExitPending] = useState(false);
|
|
90
86
|
const [quittingSummary, setQuittingSummary] = useState(null);
|
|
91
|
-
const [goalMode, setGoalMode] = useState(props.sessionStore?.goalMode ?? props.goalModeRef?.current ?? "off");
|
|
92
|
-
const [planMode, setPlanMode] = useState(props.sessionStore?.planMode ?? props.planModeRef?.current ?? false);
|
|
93
87
|
// Terminal title — updated later after agentLoop is created
|
|
94
88
|
// (hoisted here so the hook is always called in the same order)
|
|
95
89
|
const [titleRunning, setTitleRunning] = useState(false);
|
|
@@ -163,8 +157,6 @@ export function App(props) {
|
|
|
163
157
|
const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
|
|
164
158
|
const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
|
|
165
159
|
const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
|
|
166
|
-
const goalModeStateRef = useRef(goalMode);
|
|
167
|
-
const planModeStateRef = useRef(planMode);
|
|
168
160
|
// Stuck-guard for the plan-continuation follow-up nudge. Tracks how many
|
|
169
161
|
// times we've nudged the agent to continue the same step. Reset whenever a
|
|
170
162
|
// new [DONE:n] marker advances progress (see onTurnText). Caps at 2 nudges
|
|
@@ -189,8 +181,6 @@ export function App(props) {
|
|
|
189
181
|
const lastActualTokensRef = useRef(0);
|
|
190
182
|
/** Timestamp (ms) when lastActualTokensRef was last updated by turn_end. */
|
|
191
183
|
const lastActualTokensTimestampRef = useRef(0);
|
|
192
|
-
/** Timestamp of last compaction — used for time-based cooldown and staleness detection. */
|
|
193
|
-
const lastCompactionTimeRef = useRef(0);
|
|
194
184
|
/**
|
|
195
185
|
* Languages whose style packs are currently injected into the system prompt.
|
|
196
186
|
* Grown by `maybeInjectLanguagePacks` after `write`/`bash` tool results when
|
|
@@ -236,6 +226,22 @@ export function App(props) {
|
|
|
236
226
|
});
|
|
237
227
|
}, [props.sessionStore]);
|
|
238
228
|
const sessionStore = props.sessionStore;
|
|
229
|
+
const { goalMode, planMode, goalModeStateRef, rebuildSystemPrompt, replaceSystemPrompt, setGoalModeAndPrompt, setPlanModeAndPrompt, clearGoalModeIfIdle, } = useModeState({
|
|
230
|
+
initialGoalMode: props.sessionStore?.goalMode ?? props.goalModeRef?.current ?? "off",
|
|
231
|
+
initialPlanMode: props.sessionStore?.planMode ?? props.planModeRef?.current ?? false,
|
|
232
|
+
skills: props.skills,
|
|
233
|
+
goalModeRef: props.goalModeRef,
|
|
234
|
+
planModeRef: props.planModeRef,
|
|
235
|
+
sessionStore: props.sessionStore,
|
|
236
|
+
cwdRef,
|
|
237
|
+
currentToolsRef,
|
|
238
|
+
approvedPlanPathRef,
|
|
239
|
+
injectedLanguagesRef,
|
|
240
|
+
messagesRef,
|
|
241
|
+
runningGoalIdsRef,
|
|
242
|
+
activeVerifierRunIdsRef,
|
|
243
|
+
queuedGoalSyntheticEventsRef,
|
|
244
|
+
});
|
|
239
245
|
const { pendingHistoryFlushRef, streamedAssistantFlushRef, queueFlush, finalizeSubmittedUserItem, clearPendingHistory, } = useTranscriptHistory({
|
|
240
246
|
terminalHistoryPrinter: props.terminalHistoryPrinter,
|
|
241
247
|
terminalHistoryContext: {
|
|
@@ -389,36 +395,10 @@ export function App(props) {
|
|
|
389
395
|
useEffect(() => {
|
|
390
396
|
currentToolsRef.current = currentTools;
|
|
391
397
|
}, [currentTools]);
|
|
392
|
-
// ── Runtime mode wiring ──────────────────────────────────
|
|
393
|
-
// Sync runtime mode refs with React state.
|
|
394
|
-
useEffect(() => {
|
|
395
|
-
goalModeStateRef.current = goalMode;
|
|
396
|
-
if (props.goalModeRef) {
|
|
397
|
-
props.goalModeRef.current = goalMode;
|
|
398
|
-
}
|
|
399
|
-
}, [goalMode, props.goalModeRef]);
|
|
400
|
-
useEffect(() => {
|
|
401
|
-
planModeStateRef.current = planMode;
|
|
402
|
-
if (props.planModeRef)
|
|
403
|
-
props.planModeRef.current = planMode;
|
|
404
|
-
}, [planMode, props.planModeRef]);
|
|
405
398
|
const setActiveGoalReferences = useCallback((references) => {
|
|
406
399
|
if (props.goalReferencesRef)
|
|
407
400
|
props.goalReferencesRef.current = references;
|
|
408
401
|
}, [props.goalReferencesRef]);
|
|
409
|
-
const rebuildSystemPrompt = useCallback(async (options) => {
|
|
410
|
-
const approvedPlanPath = options?.clearApprovedPlan
|
|
411
|
-
? undefined
|
|
412
|
-
: (options?.approvedPlanPath ?? approvedPlanPathRef.current);
|
|
413
|
-
return buildSystemPrompt(options?.cwd ?? cwdRef.current, props.skills, options?.planMode ?? planModeStateRef.current, approvedPlanPath, (options?.tools ?? currentToolsRef.current).map((tool) => tool.name), options?.activeLanguages ?? injectedLanguagesRef.current, options?.goalMode ?? goalModeStateRef.current);
|
|
414
|
-
}, [props.skills]);
|
|
415
|
-
const replaceSystemPrompt = useCallback(async (options) => {
|
|
416
|
-
const newPrompt = await rebuildSystemPrompt(options);
|
|
417
|
-
if (messagesRef.current[0]?.role === "system") {
|
|
418
|
-
messagesRef.current[0] = { role: "system", content: newPrompt };
|
|
419
|
-
}
|
|
420
|
-
return newPrompt;
|
|
421
|
-
}, [rebuildSystemPrompt]);
|
|
422
402
|
useEffect(() => {
|
|
423
403
|
if (!props.connectInitialMcpTools)
|
|
424
404
|
return;
|
|
@@ -442,37 +422,6 @@ export function App(props) {
|
|
|
442
422
|
cancelled = true;
|
|
443
423
|
};
|
|
444
424
|
}, [props.connectInitialMcpTools, replaceSystemPrompt]);
|
|
445
|
-
const setGoalModeAndPrompt = useCallback(async (nextMode, options) => {
|
|
446
|
-
goalModeStateRef.current = nextMode;
|
|
447
|
-
if (props.goalModeRef)
|
|
448
|
-
props.goalModeRef.current = nextMode;
|
|
449
|
-
if (props.sessionStore)
|
|
450
|
-
props.sessionStore.goalMode = nextMode;
|
|
451
|
-
setGoalMode(nextMode);
|
|
452
|
-
await replaceSystemPrompt({ ...options, goalMode: nextMode });
|
|
453
|
-
}, [props.goalModeRef, props.sessionStore, replaceSystemPrompt]);
|
|
454
|
-
const setPlanModeAndPrompt = useCallback(async (nextMode) => {
|
|
455
|
-
planModeStateRef.current = nextMode;
|
|
456
|
-
if (props.planModeRef)
|
|
457
|
-
props.planModeRef.current = nextMode;
|
|
458
|
-
if (props.sessionStore)
|
|
459
|
-
props.sessionStore.planMode = nextMode;
|
|
460
|
-
setPlanMode(nextMode);
|
|
461
|
-
await replaceSystemPrompt({ planMode: nextMode });
|
|
462
|
-
}, [props.planModeRef, props.sessionStore, replaceSystemPrompt]);
|
|
463
|
-
const clearGoalModeIfIdle = useCallback(() => {
|
|
464
|
-
setTimeout(() => {
|
|
465
|
-
if (goalModeStateRef.current === "off")
|
|
466
|
-
return;
|
|
467
|
-
if (runningGoalIdsRef.current.size > 0)
|
|
468
|
-
return;
|
|
469
|
-
if (activeVerifierRunIdsRef.current.size > 0)
|
|
470
|
-
return;
|
|
471
|
-
if (queuedGoalSyntheticEventsRef.current > 0)
|
|
472
|
-
return;
|
|
473
|
-
void setGoalModeAndPrompt("off");
|
|
474
|
-
}, 0);
|
|
475
|
-
}, [setGoalModeAndPrompt]);
|
|
476
425
|
/**
|
|
477
426
|
* Unified "apply detection result" pipeline. Called from three sites:
|
|
478
427
|
* 1. Initial mount (existing project at startup).
|
|
@@ -557,45 +506,17 @@ export function App(props) {
|
|
|
557
506
|
useEffect(() => {
|
|
558
507
|
void applyLanguageDetectionRef.current("initial");
|
|
559
508
|
}, []);
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
cwd: cwdRef.current,
|
|
572
|
-
provider: currentProvider,
|
|
573
|
-
model: currentModel,
|
|
574
|
-
messages: compactedMessages,
|
|
575
|
-
});
|
|
576
|
-
sessionPathRef.current = session.path;
|
|
577
|
-
sessionStatsRef.current.sessionId = session.id;
|
|
578
|
-
persistedIndexRef.current = compactedMessages.length;
|
|
579
|
-
if (sessionStore) {
|
|
580
|
-
sessionStore.sessionPath = session.path;
|
|
581
|
-
sessionStore.sessionId = session.id;
|
|
582
|
-
sessionStore.messages = [...compactedMessages];
|
|
583
|
-
}
|
|
584
|
-
log("INFO", "compaction", "Persisted compacted session checkpoint", { path: session.path });
|
|
585
|
-
}, [currentModel, currentProvider, sessionStore]);
|
|
586
|
-
const persistNewMessages = useCallback(async () => {
|
|
587
|
-
const sp = sessionPathRef.current;
|
|
588
|
-
if (!sp)
|
|
589
|
-
return;
|
|
590
|
-
const allMsgs = messagesRef.current;
|
|
591
|
-
await appendMessagesToSession(sp, allMsgs, persistedIndexRef.current);
|
|
592
|
-
persistedIndexRef.current = allMsgs.length;
|
|
593
|
-
if (sessionStore) {
|
|
594
|
-
sessionStore.messages = [...allMsgs];
|
|
595
|
-
sessionStore.sessionPath = sp;
|
|
596
|
-
sessionStore.sessionId = sessionStatsRef.current.sessionId;
|
|
597
|
-
}
|
|
598
|
-
}, [appendMessagesToSession, sessionStore]);
|
|
509
|
+
const { persistCompactedSession, persistNewMessages } = useSessionPersistence({
|
|
510
|
+
sessionManagerRef,
|
|
511
|
+
sessionPathRef,
|
|
512
|
+
sessionStatsRef,
|
|
513
|
+
persistedIndexRef,
|
|
514
|
+
messagesRef,
|
|
515
|
+
cwdRef,
|
|
516
|
+
currentProvider,
|
|
517
|
+
currentModel,
|
|
518
|
+
sessionStore,
|
|
519
|
+
});
|
|
599
520
|
/**
|
|
600
521
|
* Run the language detector against the current cwd. If the detected set is a
|
|
601
522
|
* strict superset of what's already injected, rebuild the system prompt with
|
|
@@ -630,129 +551,25 @@ export function App(props) {
|
|
|
630
551
|
});
|
|
631
552
|
}
|
|
632
553
|
}, [props.settingsFile]);
|
|
633
|
-
const compactionAbortRef =
|
|
634
|
-
const compactConversation = useCallback(async (messages, signal) => {
|
|
635
|
-
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
636
|
-
const tokensBefore = estimateConversationTokens(messages);
|
|
637
|
-
const spinId = getId();
|
|
638
|
-
log("INFO", "compaction", `Running compaction`, {
|
|
639
|
-
messages: String(messages.length),
|
|
640
|
-
estimatedTokens: String(tokensBefore),
|
|
641
|
-
contextWindow: String(contextWindow),
|
|
642
|
-
});
|
|
643
|
-
// Show animated spinner
|
|
644
|
-
setLiveItems((prev) => [...prev, { kind: "compacting", id: spinId }]);
|
|
645
|
-
const ownedAbort = signal ? null : new AbortController();
|
|
646
|
-
const compactionSignal = signal ?? ownedAbort?.signal;
|
|
647
|
-
if (ownedAbort)
|
|
648
|
-
compactionAbortRef.current = ownedAbort;
|
|
649
|
-
try {
|
|
650
|
-
// Resolve fresh credentials for compaction too
|
|
651
|
-
let compactApiKey = activeApiKey;
|
|
652
|
-
let compactAccountId = activeAccountId;
|
|
653
|
-
let compactProjectId = activeProjectId;
|
|
654
|
-
let compactBaseUrl = activeBaseUrl;
|
|
655
|
-
if (props.authStorage) {
|
|
656
|
-
const creds = await props.authStorage.resolveCredentials(currentProvider);
|
|
657
|
-
compactApiKey = creds.accessToken;
|
|
658
|
-
compactAccountId = creds.accountId;
|
|
659
|
-
compactProjectId = creds.projectId;
|
|
660
|
-
compactBaseUrl = creds.baseUrl ?? compactBaseUrl;
|
|
661
|
-
}
|
|
662
|
-
const result = await compact(messages, {
|
|
663
|
-
provider: currentProvider,
|
|
664
|
-
model: currentModel,
|
|
665
|
-
apiKey: compactApiKey,
|
|
666
|
-
accountId: compactAccountId,
|
|
667
|
-
projectId: compactProjectId,
|
|
668
|
-
baseUrl: compactBaseUrl,
|
|
669
|
-
contextWindow,
|
|
670
|
-
signal: compactionSignal,
|
|
671
|
-
approvedPlanPath: approvedPlanPathRef.current,
|
|
672
|
-
});
|
|
673
|
-
if (result.result.compacted) {
|
|
674
|
-
// Replace spinner with completed notice
|
|
675
|
-
setLiveItems((prev) => prev.map((item) => item.id === spinId
|
|
676
|
-
? {
|
|
677
|
-
kind: "compacted",
|
|
678
|
-
originalCount: result.result.originalCount,
|
|
679
|
-
newCount: result.result.newCount,
|
|
680
|
-
tokensBefore: result.result.tokensBeforeEstimate,
|
|
681
|
-
tokensAfter: result.result.tokensAfterEstimate,
|
|
682
|
-
id: spinId,
|
|
683
|
-
}
|
|
684
|
-
: item));
|
|
685
|
-
}
|
|
686
|
-
else {
|
|
687
|
-
// Nothing was actually compacted — remove spinner silently
|
|
688
|
-
log("INFO", "compaction", `Compaction skipped: ${result.result.reason ?? "unknown"}`);
|
|
689
|
-
setLiveItems((prev) => prev.filter((item) => item.id !== spinId));
|
|
690
|
-
}
|
|
691
|
-
return result.messages;
|
|
692
|
-
}
|
|
693
|
-
catch (err) {
|
|
694
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
695
|
-
const isAbort = compactionSignal?.aborted || msg.includes("aborted") || msg.includes("abort");
|
|
696
|
-
log(isAbort ? "WARN" : "ERROR", "compaction", isAbort ? "Compaction aborted" : `Compaction failed: ${msg}`);
|
|
697
|
-
setLiveItems((prev) => isAbort
|
|
698
|
-
? prev.filter((item) => item.id !== spinId)
|
|
699
|
-
: prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
|
|
700
|
-
return messages; // Return unchanged on failure/abort
|
|
701
|
-
}
|
|
702
|
-
finally {
|
|
703
|
-
if (ownedAbort && compactionAbortRef.current === ownedAbort)
|
|
704
|
-
compactionAbortRef.current = null;
|
|
705
|
-
}
|
|
706
|
-
}, [
|
|
554
|
+
const { compactionAbortRef, compactConversation, transformContext } = useContextCompaction({
|
|
707
555
|
currentModel,
|
|
708
556
|
currentProvider,
|
|
557
|
+
maxTokens: props.maxTokens,
|
|
558
|
+
authStorage: props.authStorage,
|
|
559
|
+
contextWindowOptions,
|
|
709
560
|
activeApiKey,
|
|
710
561
|
activeAccountId,
|
|
711
562
|
activeProjectId,
|
|
712
563
|
activeBaseUrl,
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const autoCompact = settings?.get("autoCompact") ?? true;
|
|
723
|
-
const threshold = settings?.get("compactThreshold") ?? 0.8;
|
|
724
|
-
// Force-compact on context overflow regardless of settings
|
|
725
|
-
if (options?.force) {
|
|
726
|
-
const result = await compactConversation(messages);
|
|
727
|
-
if (result !== messages) {
|
|
728
|
-
messagesRef.current = result;
|
|
729
|
-
await persistCompactedSession(result);
|
|
730
|
-
}
|
|
731
|
-
lastCompactionTimeRef.current = Date.now();
|
|
732
|
-
return result;
|
|
733
|
-
}
|
|
734
|
-
if (!autoCompact)
|
|
735
|
-
return messages;
|
|
736
|
-
// Time-based cooldown: skip if compaction ran within the last 30 seconds
|
|
737
|
-
if (Date.now() - lastCompactionTimeRef.current < 30_000) {
|
|
738
|
-
log("INFO", "compaction", `Skipping compaction — cooldown active`);
|
|
739
|
-
return messages;
|
|
740
|
-
}
|
|
741
|
-
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
742
|
-
const reserveTokens = getCompactionReserveTokens(props.maxTokens);
|
|
743
|
-
const tokensFresh = lastActualTokensTimestampRef.current > lastCompactionTimeRef.current;
|
|
744
|
-
const actualTokens = lastActualTokensRef.current > 0 && tokensFresh ? lastActualTokensRef.current : undefined;
|
|
745
|
-
if (shouldCompact(messages, contextWindow, threshold, actualTokens, reserveTokens)) {
|
|
746
|
-
const result = await compactConversation(messages);
|
|
747
|
-
if (result !== messages) {
|
|
748
|
-
messagesRef.current = result;
|
|
749
|
-
await persistCompactedSession(result);
|
|
750
|
-
}
|
|
751
|
-
lastCompactionTimeRef.current = Date.now();
|
|
752
|
-
return result;
|
|
753
|
-
}
|
|
754
|
-
return messages;
|
|
755
|
-
}, [currentModel, compactConversation, contextWindowOptions, persistCompactedSession]);
|
|
564
|
+
setLiveItems,
|
|
565
|
+
getId,
|
|
566
|
+
approvedPlanPathRef,
|
|
567
|
+
settingsRef,
|
|
568
|
+
messagesRef,
|
|
569
|
+
lastActualTokensRef,
|
|
570
|
+
lastActualTokensTimestampRef,
|
|
571
|
+
persistCompactedSession,
|
|
572
|
+
});
|
|
756
573
|
// ── Background task bar state (external store) ──────────
|
|
757
574
|
const { bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, } = useTaskBarStore();
|
|
758
575
|
useTaskBarPolling(props.processManager);
|
|
@@ -1308,13 +1125,19 @@ export function App(props) {
|
|
|
1308
1125
|
setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
|
|
1309
1126
|
playNotificationSound();
|
|
1310
1127
|
}
|
|
1311
|
-
// Finalize rows
|
|
1312
|
-
//
|
|
1128
|
+
// Finalize rows. Do NOT clear the live area here — keep the items
|
|
1129
|
+
// mounted and let the flush drain effect write them to scrollback
|
|
1130
|
+
// FIRST and only then remove them from the live area. Clearing live
|
|
1131
|
+
// up front (return []) erases the rows a frame before the sink writes
|
|
1132
|
+
// them back into scrollback, which makes each finalized item blink
|
|
1133
|
+
// out and the TUI jump as the agent finishes. Write-then-clear keeps
|
|
1134
|
+
// every row continuously on screen (live → scrollback), matching how
|
|
1135
|
+
// Ink's <Static> moves a finalized item in a single atomic frame.
|
|
1313
1136
|
if (doneDecision.flushLiveItems) {
|
|
1314
1137
|
setLiveItems((prev) => {
|
|
1315
1138
|
if (prev.length > 0)
|
|
1316
1139
|
queueFlush(prev);
|
|
1317
|
-
return
|
|
1140
|
+
return prev;
|
|
1318
1141
|
});
|
|
1319
1142
|
}
|
|
1320
1143
|
const nextGoalMode = nextGoalModeAfterAgentDone({
|
|
@@ -1448,10 +1271,13 @@ export function App(props) {
|
|
|
1448
1271
|
queuedGoalSyntheticEventsRef.current = Math.max(0, queuedGoalSyntheticEventsRef.current - 1);
|
|
1449
1272
|
void setGoalModeAndPrompt("coordinator");
|
|
1450
1273
|
const eventInfo = parseGoalSyntheticEvent(displayText);
|
|
1274
|
+
// Write-then-clear: keep the rows mounted and let the flush drain
|
|
1275
|
+
// print them to scrollback before removing them, so they don't blink
|
|
1276
|
+
// out of the live area a frame before reappearing in scrollback.
|
|
1451
1277
|
setLiveItems((prev) => {
|
|
1452
1278
|
if (prev.length > 0)
|
|
1453
1279
|
queueFlush(prev);
|
|
1454
|
-
return
|
|
1280
|
+
return prev;
|
|
1455
1281
|
});
|
|
1456
1282
|
setDoneStatus(null);
|
|
1457
1283
|
appendGoalProgress({
|
|
@@ -2061,727 +1887,40 @@ export function App(props) {
|
|
|
2061
1887
|
setOverlay(kind);
|
|
2062
1888
|
}
|
|
2063
1889
|
}, [agentLoop.isRunning, props]);
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
: `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
|
|
2069
|
-
const route = routeGoalSyntheticEvent({
|
|
2070
|
-
agentRunning: agentRunningRef.current,
|
|
2071
|
-
queuedSyntheticEvents: queuedGoalSyntheticEventsRef.current,
|
|
2072
|
-
});
|
|
2073
|
-
if (route.action === "queue") {
|
|
2074
|
-
queuedGoalSyntheticEventsRef.current = route.nextQueuedSyntheticEvents;
|
|
2075
|
-
void setGoalModeAndPrompt(route.nextGoalMode);
|
|
2076
|
-
appendGoalProgress({
|
|
2077
|
-
kind: "goal_progress",
|
|
2078
|
-
phase: "orchestrator_reviewing",
|
|
2079
|
-
title: "Goal update queued for orchestrator",
|
|
2080
|
-
detail: `${detail} It will report back after the current turn.`,
|
|
2081
|
-
workerId: eventInfo?.worker,
|
|
2082
|
-
status: eventInfo?.status,
|
|
2083
|
-
});
|
|
2084
|
-
agentLoop.queueMessage(eventText);
|
|
2085
|
-
return;
|
|
2086
|
-
}
|
|
2087
|
-
appendGoalProgress({
|
|
2088
|
-
kind: "goal_progress",
|
|
2089
|
-
phase: "orchestrator_reviewing",
|
|
2090
|
-
title: "Orchestrator reviewing Goal update",
|
|
2091
|
-
detail,
|
|
2092
|
-
workerId: eventInfo?.worker,
|
|
2093
|
-
status: eventInfo?.status,
|
|
2094
|
-
});
|
|
2095
|
-
setLastUserMessage("");
|
|
2096
|
-
setDoneStatus(null);
|
|
2097
|
-
void (async () => {
|
|
2098
|
-
await setGoalModeAndPrompt("coordinator");
|
|
2099
|
-
await agentLoop.run(eventText);
|
|
2100
|
-
})().catch((err) => {
|
|
2101
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2102
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2103
|
-
clearGoalModeIfIdle();
|
|
2104
|
-
});
|
|
2105
|
-
}, [agentLoop, appendGoalProgress, clearGoalModeIfIdle, setGoalModeAndPrompt]);
|
|
2106
|
-
const continueGoalRun = useCallback((runId) => {
|
|
2107
|
-
if (goalContinuationFlightsRef.current.has(runId))
|
|
2108
|
-
return;
|
|
2109
|
-
goalContinuationFlightsRef.current.add(runId);
|
|
2110
|
-
void (async () => {
|
|
2111
|
-
const latestRun = await reconcileActiveGoalRuns(props.cwd, {
|
|
2112
|
-
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
2113
|
-
}).then(({ runs }) => runs.find((item) => item.id === runId) ?? null);
|
|
2114
|
-
if (!latestRun) {
|
|
2115
|
-
runningGoalIdsRef.current.delete(runId);
|
|
2116
|
-
clearGoalStatusEntry(runId);
|
|
2117
|
-
clearGoalModeIfIdle();
|
|
2118
|
-
return;
|
|
2119
|
-
}
|
|
2120
|
-
const decision = decideGoalNextAction(latestRun);
|
|
2121
|
-
if (!shouldKeepGoalRunTrackedAfterDecision(decision)) {
|
|
2122
|
-
runningGoalIdsRef.current.delete(runId);
|
|
2123
|
-
clearGoalModeIfIdle();
|
|
2124
|
-
}
|
|
2125
|
-
if (decision.kind === "wait")
|
|
2126
|
-
return;
|
|
2127
|
-
const choiceKey = getGoalContinuationChoiceKey({ runId: latestRun.id, decision });
|
|
2128
|
-
const now = Date.now();
|
|
2129
|
-
const recentChoiceAt = goalContinuationRecentChoicesRef.current.get(choiceKey);
|
|
2130
|
-
if (recentChoiceAt !== undefined && now - recentChoiceAt < 5000)
|
|
2131
|
-
return;
|
|
2132
|
-
goalContinuationRecentChoicesRef.current.set(choiceKey, now);
|
|
2133
|
-
if (goalContinuationRecentChoicesRef.current.size > 100) {
|
|
2134
|
-
for (const [key, startedAt] of goalContinuationRecentChoicesRef.current) {
|
|
2135
|
-
if (now - startedAt > 60_000)
|
|
2136
|
-
goalContinuationRecentChoicesRef.current.delete(key);
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
if (decision.kind === "terminal" ||
|
|
2140
|
-
decision.kind === "blocked" ||
|
|
2141
|
-
decision.kind === "pause") {
|
|
2142
|
-
const status = decision.kind === "terminal"
|
|
2143
|
-
? decision.status
|
|
2144
|
-
: decision.kind === "blocked"
|
|
2145
|
-
? "blocked"
|
|
2146
|
-
: "paused";
|
|
2147
|
-
const nextRun = {
|
|
2148
|
-
...latestRun,
|
|
2149
|
-
status,
|
|
2150
|
-
continueRequestedAt: undefined,
|
|
2151
|
-
blockers: decision.kind === "blocked" || decision.kind === "pause"
|
|
2152
|
-
? Array.from(new Set([...latestRun.blockers, decision.reason]))
|
|
2153
|
-
: latestRun.blockers,
|
|
2154
|
-
};
|
|
2155
|
-
await upsertGoalRun(props.cwd, nextRun);
|
|
2156
|
-
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2157
|
-
kind: "continuation_stopped",
|
|
2158
|
-
reason: decision.reason,
|
|
2159
|
-
content: `terminal=${status}`,
|
|
2160
|
-
});
|
|
2161
|
-
const terminalProgress = formatGoalTerminalProgress(nextRun);
|
|
2162
|
-
if (terminalProgress) {
|
|
2163
|
-
const item = { ...terminalProgress, id: goalTerminalProgressId(nextRun) };
|
|
2164
|
-
setLiveItems((prev) => completedItemsWithDurableGoalTerminalProgress([...prev, item], [nextRun]));
|
|
2165
|
-
}
|
|
2166
|
-
runningGoalIdsRef.current.delete(runId);
|
|
2167
|
-
clearGoalStatusEntry(runId);
|
|
2168
|
-
clearGoalModeIfIdle();
|
|
2169
|
-
return;
|
|
2170
|
-
}
|
|
2171
|
-
let runForNextAction = latestRun;
|
|
2172
|
-
if (latestRun.continueRequestedAt &&
|
|
2173
|
-
!listGoalWorkers(props.cwd).some((worker) => worker.status === "running") &&
|
|
2174
|
-
activeVerifierRunIdsRef.current.size === 0) {
|
|
2175
|
-
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2176
|
-
kind: "continuation_consumed",
|
|
2177
|
-
reason: `Continuation request consumed by ${decision.kind}.`,
|
|
2178
|
-
});
|
|
2179
|
-
runForNextAction = await upsertGoalRun(props.cwd, {
|
|
2180
|
-
...latestRun,
|
|
2181
|
-
continueRequestedAt: undefined,
|
|
2182
|
-
});
|
|
2183
|
-
}
|
|
2184
|
-
appendGoalProgress({
|
|
2185
|
-
kind: "goal_progress",
|
|
2186
|
-
phase: "continuing",
|
|
2187
|
-
title: `Choosing next Goal step: ${latestRun.title}`,
|
|
2188
|
-
detail: "Latest result is recorded; starting the next worker task or verifier automatically.",
|
|
2189
|
-
status: latestRun.status,
|
|
2190
|
-
});
|
|
2191
|
-
upsertGoalStatusEntry({
|
|
2192
|
-
runId: latestRun.id,
|
|
2193
|
-
label: latestRun.title,
|
|
2194
|
-
phase: "orchestrating",
|
|
2195
|
-
startedAt: Date.now(),
|
|
2196
|
-
detail: "choosing next step",
|
|
2197
|
-
});
|
|
2198
|
-
startGoalRunRef.current(runForNextAction);
|
|
2199
|
-
})()
|
|
2200
|
-
.catch((err) => {
|
|
2201
|
-
runningGoalIdsRef.current.delete(runId);
|
|
2202
|
-
clearGoalStatusEntry(runId);
|
|
2203
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2204
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2205
|
-
})
|
|
2206
|
-
.finally(() => {
|
|
2207
|
-
goalContinuationFlightsRef.current.delete(runId);
|
|
2208
|
-
clearGoalModeIfIdle();
|
|
2209
|
-
});
|
|
2210
|
-
}, [
|
|
2211
|
-
appendGoalProgress,
|
|
2212
|
-
clearGoalModeIfIdle,
|
|
2213
|
-
clearGoalStatusEntry,
|
|
2214
|
-
props.cwd,
|
|
2215
|
-
upsertGoalStatusEntry,
|
|
2216
|
-
]);
|
|
2217
|
-
const handleGoalWorkerComplete = useCallback((run, completion) => {
|
|
2218
|
-
const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
|
|
2219
|
-
completion.worker.goalTaskId;
|
|
2220
|
-
const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
|
|
2221
|
-
appendGoalProgress({
|
|
2222
|
-
kind: "goal_progress",
|
|
2223
|
-
phase: "worker_finished",
|
|
2224
|
-
title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
|
|
2225
|
-
detail: summarizeGoalCompletion(completion.summary),
|
|
2226
|
-
workerId: completion.worker.id,
|
|
2227
|
-
status: completion.status,
|
|
2228
|
-
});
|
|
2229
|
-
const taskProgress = goalTaskProgress(run, run.tasks.find((task) => task.id === completion.worker.goalTaskId));
|
|
2230
|
-
upsertGoalStatusEntry({
|
|
2231
|
-
runId: run.id,
|
|
2232
|
-
label: run.title,
|
|
2233
|
-
phase: completion.status === "done" ? "reviewing" : "failed",
|
|
2234
|
-
startedAt: Date.now(),
|
|
2235
|
-
detail: completion.status === "done" ? "reviewing result" : "task failed",
|
|
2236
|
-
workerId: completion.worker.id,
|
|
2237
|
-
goalNumber: goalNumberForRun(run.id),
|
|
2238
|
-
...taskProgress,
|
|
2239
|
-
});
|
|
2240
|
-
runGoalSyntheticEvent(eventText);
|
|
2241
|
-
void (async () => {
|
|
2242
|
-
if (listGoalWorkers(completion.worker.projectPath).some((worker) => worker.status === "running"))
|
|
2243
|
-
return;
|
|
2244
|
-
if (activeVerifierRunIdsRef.current.size > 0)
|
|
2245
|
-
return;
|
|
2246
|
-
const runs = await loadGoalRuns(completion.worker.projectPath);
|
|
2247
|
-
const queued = runs.find((item) => goalRunNeedsExplicitContinuationAfterWorker(item));
|
|
2248
|
-
if (queued)
|
|
2249
|
-
setTimeout(() => continueGoalRun(queued.id), 750);
|
|
2250
|
-
})().catch((err) => log("ERROR", "goal", err instanceof Error ? err.message : String(err)));
|
|
2251
|
-
}, [
|
|
2252
|
-
appendGoalProgress,
|
|
2253
|
-
continueGoalRun,
|
|
2254
|
-
goalNumberForRun,
|
|
2255
|
-
runGoalSyntheticEvent,
|
|
2256
|
-
upsertGoalStatusEntry,
|
|
2257
|
-
]);
|
|
2258
|
-
useEffect(() => {
|
|
2259
|
-
return subscribeGoalWorkerCompletions((completion) => {
|
|
2260
|
-
void (async () => {
|
|
2261
|
-
const latestRun = (await loadGoalRuns(completion.worker.projectPath)).find((item) => item.id === completion.worker.goalRunId) ?? null;
|
|
2262
|
-
if (!latestRun) {
|
|
2263
|
-
log("WARN", "goal", `Worker completion for unknown Goal ${completion.worker.goalRunId}`);
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
runningGoalIdsRef.current.add(latestRun.id);
|
|
2267
|
-
handleGoalWorkerComplete(latestRun, completion);
|
|
2268
|
-
})().catch((err) => {
|
|
2269
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2270
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2271
|
-
});
|
|
2272
|
-
}, props.cwd);
|
|
2273
|
-
}, [handleGoalWorkerComplete, props.cwd]);
|
|
2274
|
-
const startGoalRun = useCallback((run) => {
|
|
2275
|
-
runningGoalIdsRef.current.add(run.id);
|
|
2276
|
-
upsertGoalStatusEntry({
|
|
2277
|
-
runId: run.id,
|
|
2278
|
-
label: run.title,
|
|
2279
|
-
phase: "orchestrating",
|
|
2280
|
-
startedAt: Date.now(),
|
|
2281
|
-
detail: "choosing next step",
|
|
2282
|
-
goalNumber: goalNumberForRun(run.id),
|
|
2283
|
-
});
|
|
2284
|
-
void (async () => {
|
|
2285
|
-
await setGoalModeAndPrompt("coordinator");
|
|
2286
|
-
const currentRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
2287
|
-
const prereqCheck = await runGoalPrerequisiteChecks(props.cwd, currentRun);
|
|
2288
|
-
const checkedRun = prereqCheck.checkedCount > 0
|
|
2289
|
-
? await upsertGoalRun(props.cwd, {
|
|
2290
|
-
...prereqCheck.run,
|
|
2291
|
-
status: goalHasBlockingPrerequisites(prereqCheck.run) ? "blocked" : "ready",
|
|
2292
|
-
})
|
|
2293
|
-
: currentRun;
|
|
2294
|
-
if (goalHasBlockingPrerequisites(checkedRun)) {
|
|
2295
|
-
const detail = formatGoalBlockingPrerequisites(checkedRun);
|
|
2296
|
-
await upsertGoalRun(props.cwd, {
|
|
2297
|
-
...checkedRun,
|
|
2298
|
-
status: "blocked",
|
|
2299
|
-
blockers: Array.from(new Set([...checkedRun.blockers, detail])),
|
|
2300
|
-
});
|
|
2301
|
-
appendGoalProgress({
|
|
2302
|
-
kind: "goal_progress",
|
|
2303
|
-
phase: "terminal",
|
|
2304
|
-
title: `Goal blocked: ${checkedRun.title}`,
|
|
2305
|
-
detail,
|
|
2306
|
-
status: "blocked",
|
|
2307
|
-
});
|
|
2308
|
-
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
2309
|
-
clearGoalStatusEntry(checkedRun.id);
|
|
2310
|
-
clearGoalModeIfIdle();
|
|
2311
|
-
return;
|
|
2312
|
-
}
|
|
2313
|
-
const decision = decideGoalNextAction(checkedRun);
|
|
2314
|
-
await appendGoalDecision(props.cwd, checkedRun.id, decision);
|
|
2315
|
-
if (!shouldKeepGoalRunTrackedAfterDecision(decision)) {
|
|
2316
|
-
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
2317
|
-
}
|
|
2318
|
-
if (decision.kind === "terminal") {
|
|
2319
|
-
const terminalProgress = formatGoalTerminalProgress(checkedRun);
|
|
2320
|
-
if (terminalProgress) {
|
|
2321
|
-
const item = { ...terminalProgress, id: goalTerminalProgressId(checkedRun) };
|
|
2322
|
-
setLiveItems((prev) => completedItemsWithDurableGoalTerminalProgress([...prev, item], [checkedRun]));
|
|
2323
|
-
}
|
|
2324
|
-
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
2325
|
-
clearGoalStatusEntry(checkedRun.id);
|
|
2326
|
-
clearGoalModeIfIdle();
|
|
2327
|
-
return;
|
|
2328
|
-
}
|
|
2329
|
-
if (decision.kind === "wait") {
|
|
2330
|
-
appendGoalProgress({
|
|
2331
|
-
kind: "goal_progress",
|
|
2332
|
-
phase: "worker_started",
|
|
2333
|
-
title: decision.workerId
|
|
2334
|
-
? `Goal working: ${checkedRun.title}`
|
|
2335
|
-
: `Goal needs orchestration: ${checkedRun.title}`,
|
|
2336
|
-
detail: decision.workerId
|
|
2337
|
-
? decision.reason
|
|
2338
|
-
: `${decision.reason} Asking the orchestrator to unblock or revise the Goal plan.`,
|
|
2339
|
-
workerId: decision.workerId,
|
|
2340
|
-
});
|
|
2341
|
-
upsertGoalStatusEntry({
|
|
2342
|
-
runId: checkedRun.id,
|
|
2343
|
-
label: checkedRun.title,
|
|
2344
|
-
phase: decision.workerId ? "worker" : "orchestrating",
|
|
2345
|
-
startedAt: Date.now(),
|
|
2346
|
-
detail: decision.reason,
|
|
2347
|
-
workerId: decision.workerId,
|
|
2348
|
-
goalNumber: goalNumberForRun(checkedRun.id),
|
|
2349
|
-
});
|
|
2350
|
-
if (!decision.workerId) {
|
|
2351
|
-
const eventText = `Goal continuation is waiting with no active worker for Goal ${checkedRun.id} (${checkedRun.title}).\n` +
|
|
2352
|
-
`Reason: ${decision.reason}\n\n` +
|
|
2353
|
-
`Inspect the durable Goal state with the goals tool, resolve blocked dependencies by creating or updating concrete worker tasks, and then continue the Goal. If no local/free action can proceed, record an explicit blocker with exact user instructions. Do not stop after only explaining the state.`;
|
|
2354
|
-
setLastUserMessage("");
|
|
2355
|
-
setDoneStatus(null);
|
|
2356
|
-
await agentLoop.run(eventText);
|
|
2357
|
-
}
|
|
2358
|
-
return;
|
|
2359
|
-
}
|
|
2360
|
-
if (decision.kind === "complete") {
|
|
2361
|
-
await upsertGoalRun(props.cwd, { ...checkedRun, status: "passed" });
|
|
2362
|
-
appendGoalProgress({
|
|
2363
|
-
kind: "goal_progress",
|
|
2364
|
-
phase: "terminal",
|
|
2365
|
-
title: `Goal passed: ${checkedRun.title}`,
|
|
2366
|
-
detail: decision.reason,
|
|
2367
|
-
status: "passed",
|
|
2368
|
-
});
|
|
2369
|
-
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
2370
|
-
clearGoalStatusEntry(checkedRun.id);
|
|
2371
|
-
clearGoalModeIfIdle();
|
|
2372
|
-
return;
|
|
2373
|
-
}
|
|
2374
|
-
if (decision.kind === "run_verifier") {
|
|
2375
|
-
await verifyGoalRun(checkedRun);
|
|
2376
|
-
return;
|
|
2377
|
-
}
|
|
2378
|
-
if (decision.kind === "create_task") {
|
|
2379
|
-
const latestRunBeforeCreate = (await loadGoalRuns(props.cwd)).find((item) => item.id === checkedRun.id) ?? checkedRun;
|
|
2380
|
-
const existingSameTitleTask = latestRunBeforeCreate.tasks.find((item) => item.title === decision.title);
|
|
2381
|
-
if (existingSameTitleTask) {
|
|
2382
|
-
const runWithExistingTask = await upsertGoalRun(props.cwd, {
|
|
2383
|
-
...latestRunBeforeCreate,
|
|
2384
|
-
status: "ready",
|
|
2385
|
-
});
|
|
2386
|
-
appendGoalProgress({
|
|
2387
|
-
kind: "goal_progress",
|
|
2388
|
-
phase: "continuing",
|
|
2389
|
-
title: `Goal task already exists: ${decision.title}`,
|
|
2390
|
-
detail: "Reusing the existing Goal task instead of creating a duplicate.",
|
|
2391
|
-
status: "ready",
|
|
2392
|
-
});
|
|
2393
|
-
startGoalRunRef.current(runWithExistingTask);
|
|
2394
|
-
return;
|
|
2395
|
-
}
|
|
2396
|
-
await updateGoalTask(props.cwd, checkedRun.id, `auto-${Date.now()}`, {
|
|
2397
|
-
title: decision.title,
|
|
2398
|
-
prompt: decision.prompt,
|
|
2399
|
-
status: "pending",
|
|
2400
|
-
});
|
|
2401
|
-
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === checkedRun.id) ?? checkedRun;
|
|
2402
|
-
const runWithTask = await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
|
|
2403
|
-
appendGoalProgress({
|
|
2404
|
-
kind: "goal_progress",
|
|
2405
|
-
phase: "continuing",
|
|
2406
|
-
title: `Goal task created: ${decision.title}`,
|
|
2407
|
-
detail: "Starting the new Goal task now.",
|
|
2408
|
-
status: "ready",
|
|
2409
|
-
});
|
|
2410
|
-
startGoalRunRef.current(runWithTask);
|
|
2411
|
-
return;
|
|
2412
|
-
}
|
|
2413
|
-
if (decision.kind === "blocked") {
|
|
2414
|
-
await upsertGoalRun(props.cwd, {
|
|
2415
|
-
...checkedRun,
|
|
2416
|
-
status: "blocked",
|
|
2417
|
-
blockers: [...checkedRun.blockers, decision.reason],
|
|
2418
|
-
});
|
|
2419
|
-
appendGoalProgress({
|
|
2420
|
-
kind: "goal_progress",
|
|
2421
|
-
phase: "terminal",
|
|
2422
|
-
title: `Goal blocked: ${checkedRun.title}`,
|
|
2423
|
-
detail: decision.reason,
|
|
2424
|
-
status: "blocked",
|
|
2425
|
-
});
|
|
2426
|
-
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
2427
|
-
clearGoalStatusEntry(checkedRun.id);
|
|
2428
|
-
clearGoalModeIfIdle();
|
|
2429
|
-
return;
|
|
2430
|
-
}
|
|
2431
|
-
if (decision.kind === "pause") {
|
|
2432
|
-
const runWithBlockedTask = (await updateGoalTask(props.cwd, checkedRun.id, decision.task.id, {
|
|
2433
|
-
status: "blocked",
|
|
2434
|
-
attempts: decision.attempts,
|
|
2435
|
-
lastSummary: "Paused after worker attempt limit.",
|
|
2436
|
-
})) ?? checkedRun;
|
|
2437
|
-
const runWithPauseEvidence = (await appendGoalEvidence(props.cwd, checkedRun.id, {
|
|
2438
|
-
kind: "summary",
|
|
2439
|
-
label: "Goal paused",
|
|
2440
|
-
content: decision.reason,
|
|
2441
|
-
})) ?? runWithBlockedTask;
|
|
2442
|
-
await upsertGoalRun(props.cwd, {
|
|
2443
|
-
...runWithPauseEvidence,
|
|
2444
|
-
status: "paused",
|
|
2445
|
-
continueRequestedAt: undefined,
|
|
2446
|
-
blockers: Array.from(new Set([...runWithPauseEvidence.blockers, decision.reason])),
|
|
2447
|
-
});
|
|
2448
|
-
appendGoalProgress({
|
|
2449
|
-
kind: "goal_progress",
|
|
2450
|
-
phase: "terminal",
|
|
2451
|
-
title: `Goal paused: ${checkedRun.title}`,
|
|
2452
|
-
detail: decision.reason,
|
|
2453
|
-
status: "paused",
|
|
2454
|
-
});
|
|
2455
|
-
runningGoalIdsRef.current.delete(checkedRun.id);
|
|
2456
|
-
clearGoalStatusEntry(checkedRun.id);
|
|
2457
|
-
clearGoalModeIfIdle();
|
|
2458
|
-
return;
|
|
2459
|
-
}
|
|
2460
|
-
const runWithAttempt = (await updateGoalTask(props.cwd, checkedRun.id, decision.task.id, {
|
|
2461
|
-
attempts: decision.attempts,
|
|
2462
|
-
})) ?? checkedRun;
|
|
2463
|
-
const worker = await startGoalWorker({
|
|
2464
|
-
cwd: props.cwd,
|
|
2465
|
-
provider: currentProvider,
|
|
2466
|
-
model: currentModel,
|
|
2467
|
-
thinkingLevel,
|
|
2468
|
-
goalRunId: checkedRun.id,
|
|
2469
|
-
goalTaskId: decision.task.id,
|
|
2470
|
-
taskTitle: decision.task.title,
|
|
2471
|
-
prompt: buildGoalTaskPromptWithReferences(checkedRun, decision.task.prompt),
|
|
2472
|
-
isolateWorktree: shouldRunGoalTaskInMainCheckout(decision.task.title) ? false : undefined,
|
|
2473
|
-
});
|
|
2474
|
-
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === checkedRun.id) ??
|
|
2475
|
-
runWithAttempt;
|
|
2476
|
-
await upsertGoalRun(props.cwd, {
|
|
2477
|
-
...latestRun,
|
|
2478
|
-
status: "running",
|
|
2479
|
-
activeWorkerId: worker.id,
|
|
2480
|
-
continueRequestedAt: undefined,
|
|
2481
|
-
tasks: latestRun.tasks.map((item) => item.id === decision.task.id
|
|
2482
|
-
? { ...item, status: "running", workerId: worker.id, attempts: decision.attempts }
|
|
2483
|
-
: item),
|
|
2484
|
-
});
|
|
2485
|
-
appendGoalProgress({
|
|
2486
|
-
kind: "goal_progress",
|
|
2487
|
-
phase: "worker_started",
|
|
2488
|
-
title: `Worker started: ${decision.task.title}`,
|
|
2489
|
-
detail: "Task is running in the background.",
|
|
2490
|
-
workerId: worker.id,
|
|
2491
|
-
status: worker.status,
|
|
2492
|
-
});
|
|
2493
|
-
upsertGoalStatusEntry({
|
|
2494
|
-
runId: checkedRun.id,
|
|
2495
|
-
label: checkedRun.title,
|
|
2496
|
-
phase: "worker",
|
|
2497
|
-
startedAt: Date.now(),
|
|
2498
|
-
detail: "background worker running",
|
|
2499
|
-
workerId: worker.id,
|
|
2500
|
-
goalNumber: goalNumberForRun(checkedRun.id),
|
|
2501
|
-
...goalTaskProgress(checkedRun, decision.task),
|
|
2502
|
-
});
|
|
2503
|
-
})().catch(async (err) => {
|
|
2504
|
-
clearGoalStatusEntry(run.id);
|
|
2505
|
-
clearGoalModeIfIdle();
|
|
2506
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2507
|
-
if (isGoalWorktreeDirtyError(err)) {
|
|
2508
|
-
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
2509
|
-
const pausedRun = await upsertGoalRun(props.cwd, buildGoalDirtyWorktreePauseRun(latestRun, err));
|
|
2510
|
-
runningGoalIdsRef.current.delete(pausedRun.id);
|
|
2511
|
-
appendGoalProgress({
|
|
2512
|
-
kind: "goal_progress",
|
|
2513
|
-
phase: "terminal",
|
|
2514
|
-
title: `Goal paused: ${pausedRun.title}`,
|
|
2515
|
-
detail: "Working tree has uncommitted changes; waiting for the user to choose commit, stash, or pause.",
|
|
2516
|
-
status: "paused",
|
|
2517
|
-
});
|
|
2518
|
-
setLiveItems((prev) => [
|
|
2519
|
-
...prev,
|
|
2520
|
-
{ kind: "info", text: goalDirtyWorktreeInfoText(), id: getId() },
|
|
2521
|
-
]);
|
|
2522
|
-
void agentLoop.run(buildGoalDirtyWorktreeUserPrompt(err)).catch((agentErr) => {
|
|
2523
|
-
log("ERROR", "goal", agentErr instanceof Error ? agentErr.message : String(agentErr));
|
|
2524
|
-
setLiveItems((prev) => [...prev, toErrorItem(agentErr, getId(), "Goal")]);
|
|
2525
|
-
});
|
|
2526
|
-
return;
|
|
2527
|
-
}
|
|
2528
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2529
|
-
});
|
|
2530
|
-
}, [
|
|
2531
|
-
props.cwd,
|
|
1890
|
+
const { continueGoalRun, startGoalRun, pauseGoalRun } = useGoalOrchestration({
|
|
1891
|
+
cwd: props.cwd,
|
|
1892
|
+
resetUI: props.resetUI,
|
|
1893
|
+
sessionStore: props.sessionStore,
|
|
2532
1894
|
currentProvider,
|
|
2533
1895
|
currentModel,
|
|
2534
1896
|
thinkingLevel,
|
|
2535
1897
|
agentLoop,
|
|
2536
1898
|
appendGoalProgress,
|
|
2537
|
-
clearGoalModeIfIdle,
|
|
2538
|
-
clearGoalStatusEntry,
|
|
2539
1899
|
goalNumberForRun,
|
|
2540
|
-
setGoalModeAndPrompt,
|
|
2541
|
-
upsertGoalStatusEntry,
|
|
2542
|
-
]);
|
|
2543
|
-
const verifyGoalRun = useCallback(async (run) => {
|
|
2544
|
-
await setGoalModeAndPrompt("coordinator");
|
|
2545
|
-
if (!run.verifier?.command) {
|
|
2546
|
-
await appendGoalEvidence(props.cwd, run.id, {
|
|
2547
|
-
kind: "summary",
|
|
2548
|
-
label: "Missing verifier",
|
|
2549
|
-
content: "No verifier command is configured.",
|
|
2550
|
-
});
|
|
2551
|
-
await upsertGoalRun(props.cwd, {
|
|
2552
|
-
...run,
|
|
2553
|
-
status: "blocked",
|
|
2554
|
-
blockers: [...run.blockers, "No verifier command configured."],
|
|
2555
|
-
});
|
|
2556
|
-
appendGoalProgress({
|
|
2557
|
-
kind: "goal_progress",
|
|
2558
|
-
phase: "terminal",
|
|
2559
|
-
title: `Goal blocked: ${run.title}`,
|
|
2560
|
-
detail: "No verifier command is configured.",
|
|
2561
|
-
status: "blocked",
|
|
2562
|
-
});
|
|
2563
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
2564
|
-
clearGoalStatusEntry(run.id);
|
|
2565
|
-
clearGoalModeIfIdle();
|
|
2566
|
-
return;
|
|
2567
|
-
}
|
|
2568
|
-
const integration = await checkGoalWorktreeIntegration(props.cwd, run);
|
|
2569
|
-
if (!integration.ok) {
|
|
2570
|
-
const runWithEvidence = (await appendGoalEvidence(props.cwd, run.id, {
|
|
2571
|
-
kind: "summary",
|
|
2572
|
-
label: "Goal worktree integration required",
|
|
2573
|
-
content: integration.summary,
|
|
2574
|
-
})) ?? run;
|
|
2575
|
-
await upsertGoalRun(props.cwd, {
|
|
2576
|
-
...runWithEvidence,
|
|
2577
|
-
status: "blocked",
|
|
2578
|
-
blockers: Array.from(new Set([...runWithEvidence.blockers, integration.summary])),
|
|
2579
|
-
});
|
|
2580
|
-
appendGoalProgress({
|
|
2581
|
-
kind: "goal_progress",
|
|
2582
|
-
phase: "terminal",
|
|
2583
|
-
title: `Goal blocked before verifier: ${run.title}`,
|
|
2584
|
-
detail: integration.summary,
|
|
2585
|
-
status: "blocked",
|
|
2586
|
-
});
|
|
2587
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
2588
|
-
clearGoalStatusEntry(run.id);
|
|
2589
|
-
clearGoalModeIfIdle();
|
|
2590
|
-
return;
|
|
2591
|
-
}
|
|
2592
|
-
activeVerifierRunIdsRef.current.add(run.id);
|
|
2593
|
-
await upsertGoalRun(props.cwd, {
|
|
2594
|
-
...run,
|
|
2595
|
-
status: "verifying",
|
|
2596
|
-
continueRequestedAt: undefined,
|
|
2597
|
-
});
|
|
2598
|
-
appendGoalProgress({
|
|
2599
|
-
kind: "goal_progress",
|
|
2600
|
-
phase: "verifier_started",
|
|
2601
|
-
title: `Verifier started: ${run.title}`,
|
|
2602
|
-
detail: run.verifier.command,
|
|
2603
|
-
status: "verifying",
|
|
2604
|
-
});
|
|
2605
|
-
const startedAt = Date.now();
|
|
2606
|
-
const verifierTimeoutMs = Number(process.env.GG_GOAL_VERIFIER_TIMEOUT_MS ?? 10 * 60 * 1000);
|
|
2607
|
-
upsertGoalStatusEntry({
|
|
2608
|
-
runId: run.id,
|
|
2609
|
-
label: run.title,
|
|
2610
|
-
phase: "verifier",
|
|
2611
|
-
startedAt,
|
|
2612
|
-
detail: run.verifier.command,
|
|
2613
|
-
goalNumber: goalNumberForRun(run.id),
|
|
2614
|
-
});
|
|
2615
|
-
void runGoalVerifierCommand({
|
|
2616
|
-
cwd: run.verifier.cwd ?? props.cwd,
|
|
2617
|
-
runId: run.id,
|
|
2618
|
-
command: run.verifier.command,
|
|
2619
|
-
timeoutMs: verifierTimeoutMs,
|
|
2620
|
-
now: () => startedAt,
|
|
2621
|
-
})
|
|
2622
|
-
.then(async ({ verification, failureClass, durationMs }) => {
|
|
2623
|
-
activeVerifierRunIdsRef.current.delete(run.id);
|
|
2624
|
-
const status = verification.status;
|
|
2625
|
-
const summary = verification.summary;
|
|
2626
|
-
const outputPath = verification.outputPath;
|
|
2627
|
-
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
2628
|
-
const runWithVerifier = {
|
|
2629
|
-
...latestRun,
|
|
2630
|
-
verifier: {
|
|
2631
|
-
...latestRun.verifier,
|
|
2632
|
-
description: latestRun.verifier?.description ?? "Goal verifier",
|
|
2633
|
-
command: run.verifier?.command,
|
|
2634
|
-
...(run.verifier?.cwd ? { cwd: run.verifier.cwd } : {}),
|
|
2635
|
-
lastResult: verification,
|
|
2636
|
-
},
|
|
2637
|
-
...(status === "pass"
|
|
2638
|
-
? {
|
|
2639
|
-
completionAudit: {
|
|
2640
|
-
status: "unknown",
|
|
2641
|
-
summary: "Final completion audit pending for latest verifier result.",
|
|
2642
|
-
checkedAt: verification.checkedAt,
|
|
2643
|
-
verifierCheckedAt: verification.checkedAt,
|
|
2644
|
-
...(verification.outputPath ? { outputPath: verification.outputPath } : {}),
|
|
2645
|
-
},
|
|
2646
|
-
}
|
|
2647
|
-
: {}),
|
|
2648
|
-
};
|
|
2649
|
-
const completionCheck = canCompleteGoalRun(runWithVerifier);
|
|
2650
|
-
const verifiedRun = await upsertGoalRun(props.cwd, {
|
|
2651
|
-
...runWithVerifier,
|
|
2652
|
-
continueRequestedAt: latestRun.continueRequestedAt,
|
|
2653
|
-
status: status === "pass" && completionCheck.ok ? "passed" : "ready",
|
|
2654
|
-
});
|
|
2655
|
-
await appendGoalEvidence(props.cwd, run.id, {
|
|
2656
|
-
kind: "command",
|
|
2657
|
-
label: `Verifier ${status}`,
|
|
2658
|
-
content: `${failureClass}: ${summary}`.slice(0, 4000),
|
|
2659
|
-
path: outputPath,
|
|
2660
|
-
});
|
|
2661
|
-
await appendGoalDecision(props.cwd, run.id, {
|
|
2662
|
-
kind: `verifier_${status}`,
|
|
2663
|
-
reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
|
|
2664
|
-
content: `outputPath=${outputPath ?? ""}; cwd=${run.verifier?.cwd ?? props.cwd}; durationMs=${durationMs}`,
|
|
2665
|
-
});
|
|
2666
|
-
appendGoalProgress({
|
|
2667
|
-
kind: "goal_progress",
|
|
2668
|
-
phase: "verifier_finished",
|
|
2669
|
-
title: `Verifier ${status}: ${run.title}`,
|
|
2670
|
-
detail: summarizeGoalCompletion(summary),
|
|
2671
|
-
status,
|
|
2672
|
-
});
|
|
2673
|
-
upsertGoalStatusEntry({
|
|
2674
|
-
runId: run.id,
|
|
2675
|
-
label: run.title,
|
|
2676
|
-
phase: status === "pass" ? "reviewing" : "failed",
|
|
2677
|
-
startedAt: Date.now(),
|
|
2678
|
-
detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
|
|
2679
|
-
goalNumber: goalNumberForRun(run.id),
|
|
2680
|
-
});
|
|
2681
|
-
const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
|
|
2682
|
-
runGoalSyntheticEvent(eventText);
|
|
2683
|
-
const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
|
|
2684
|
-
if (continuationRun?.continueRequestedAt || status === "fail" || status === "pass") {
|
|
2685
|
-
setTimeout(() => continueGoalRun(run.id), 500);
|
|
2686
|
-
}
|
|
2687
|
-
})
|
|
2688
|
-
.catch((err) => {
|
|
2689
|
-
activeVerifierRunIdsRef.current.delete(run.id);
|
|
2690
|
-
clearGoalStatusEntry(run.id);
|
|
2691
|
-
clearGoalModeIfIdle();
|
|
2692
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2693
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
|
|
2694
|
-
});
|
|
2695
|
-
}, [
|
|
2696
|
-
props.cwd,
|
|
2697
|
-
appendGoalProgress,
|
|
2698
|
-
clearGoalModeIfIdle,
|
|
2699
1900
|
clearGoalStatusEntry,
|
|
2700
|
-
goalNumberForRun,
|
|
2701
|
-
runGoalSyntheticEvent,
|
|
2702
|
-
setGoalModeAndPrompt,
|
|
2703
1901
|
upsertGoalStatusEntry,
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
}
|
|
2726
|
-
const startTask = useCallback((title, prompt, taskId) => {
|
|
2727
|
-
const taskCwd = cwdRef.current;
|
|
2728
|
-
const shortId = taskId.slice(0, 8);
|
|
2729
|
-
const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
|
|
2730
|
-
`tasks({ action: "done", id: "${shortId}" })`;
|
|
2731
|
-
const fullPrompt = prompt + completionHint;
|
|
2732
|
-
if (props.resetUI && props.sessionStore) {
|
|
2733
|
-
const sysMsg = messagesRef.current[0];
|
|
2734
|
-
const newMessages = sysMsg && sysMsg.role === "system" ? [sysMsg] : messagesRef.current.slice(0, 1);
|
|
2735
|
-
const taskItem = { kind: "task", title, id: getId() };
|
|
2736
|
-
const sm = sessionManagerRef.current;
|
|
2737
|
-
void (async () => {
|
|
2738
|
-
let newSessionPath;
|
|
2739
|
-
if (sm) {
|
|
2740
|
-
try {
|
|
2741
|
-
const session = await sm.create(taskCwd, currentProvider, currentModel);
|
|
2742
|
-
newSessionPath = session.path;
|
|
2743
|
-
log("INFO", "tasks", "New session for task", { path: session.path });
|
|
2744
|
-
}
|
|
2745
|
-
catch {
|
|
2746
|
-
// Session creation is best-effort.
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
if (props.sessionStore)
|
|
2750
|
-
props.sessionStore.overlay = null;
|
|
2751
|
-
props.resetUI?.({
|
|
2752
|
-
wipeSession: true,
|
|
2753
|
-
messages: newMessages,
|
|
2754
|
-
history: [{ kind: "banner", id: "banner" }, taskItem],
|
|
2755
|
-
sessionPath: newSessionPath,
|
|
2756
|
-
pendingAction: { prompt: fullPrompt },
|
|
2757
|
-
});
|
|
2758
|
-
})();
|
|
2759
|
-
return;
|
|
2760
|
-
}
|
|
2761
|
-
clearPendingHistory();
|
|
2762
|
-
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2763
|
-
setLiveItems([]);
|
|
2764
|
-
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
2765
|
-
agentLoop.reset();
|
|
2766
|
-
persistedIndexRef.current = messagesRef.current.length;
|
|
2767
|
-
const sm = sessionManagerRef.current;
|
|
2768
|
-
if (sm) {
|
|
2769
|
-
void sm.create(taskCwd, currentProvider, currentModel).then((session) => {
|
|
2770
|
-
sessionPathRef.current = session.path;
|
|
2771
|
-
log("INFO", "tasks", "New session for task", { path: session.path });
|
|
2772
|
-
});
|
|
2773
|
-
}
|
|
2774
|
-
const taskItem = { kind: "task", title, id: getId() };
|
|
2775
|
-
setLastUserMessage(title);
|
|
2776
|
-
setDoneStatus(null);
|
|
2777
|
-
setLiveItems([taskItem]);
|
|
2778
|
-
void agentLoop.run(fullPrompt).catch((err) => {
|
|
2779
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2780
|
-
});
|
|
2781
|
-
}, [agentLoop, currentModel, currentProvider, props]);
|
|
2782
|
-
// Keep refs in sync for access from stale closures (onDone)
|
|
2783
|
-
startTaskRef.current = startTask;
|
|
2784
|
-
startGoalRunRef.current = startGoalRun;
|
|
1902
|
+
setGoalModeAndPrompt,
|
|
1903
|
+
clearGoalModeIfIdle,
|
|
1904
|
+
agentRunningRef,
|
|
1905
|
+
runningGoalIdsRef,
|
|
1906
|
+
activeVerifierRunIdsRef,
|
|
1907
|
+
queuedGoalSyntheticEventsRef,
|
|
1908
|
+
goalContinuationFlightsRef,
|
|
1909
|
+
goalContinuationRecentChoicesRef,
|
|
1910
|
+
startGoalRunRef,
|
|
1911
|
+
startTaskRef,
|
|
1912
|
+
messagesRef,
|
|
1913
|
+
persistedIndexRef,
|
|
1914
|
+
sessionManagerRef,
|
|
1915
|
+
sessionPathRef,
|
|
1916
|
+
cwdRef,
|
|
1917
|
+
setLiveItems,
|
|
1918
|
+
setHistory,
|
|
1919
|
+
setLastUserMessage,
|
|
1920
|
+
setDoneStatus,
|
|
1921
|
+
getId,
|
|
1922
|
+
clearPendingHistory,
|
|
1923
|
+
});
|
|
2785
1924
|
useEffect(() => {
|
|
2786
1925
|
runAllTasksRef.current = runAllTasks;
|
|
2787
1926
|
if (props.sessionStore)
|
|
@@ -2790,94 +1929,35 @@ export function App(props) {
|
|
|
2790
1929
|
useEffect(() => {
|
|
2791
1930
|
agentRunningRef.current = agentLoop.isRunning;
|
|
2792
1931
|
}, [agentLoop.isRunning]);
|
|
2793
|
-
const startPixelFix =
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
// new project re-detects from scratch on the next tool call. Also
|
|
2823
|
-
// reset the setup-hint flag so the new project's first badge re-
|
|
2824
|
-
// surfaces the tip (different project, may need the reminder).
|
|
2825
|
-
injectedLanguagesRef.current = new Set();
|
|
2826
|
-
setupHintShownRef.current = false;
|
|
2827
|
-
const detectedForPixelFix = detectLanguages(prep.projectPath);
|
|
2828
|
-
injectedLanguagesRef.current = detectedForPixelFix;
|
|
2829
|
-
const newSystemPrompt = await rebuildSystemPrompt({
|
|
2830
|
-
cwd: prep.projectPath,
|
|
2831
|
-
clearApprovedPlan: true,
|
|
2832
|
-
activeLanguages: detectedForPixelFix,
|
|
2833
|
-
tools: toolsForPixelFix,
|
|
2834
|
-
});
|
|
2835
|
-
// Now that the cwd swap is committed, reset chat. Do not clear the
|
|
2836
|
-
// terminal here; terminal clear sequences can erase saved scrollback.
|
|
2837
|
-
clearPendingHistory();
|
|
2838
|
-
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2839
|
-
setLiveItems([]);
|
|
2840
|
-
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
2841
|
-
agentLoop.reset();
|
|
2842
|
-
persistedIndexRef.current = messagesRef.current.length;
|
|
2843
|
-
const sm = sessionManagerRef.current;
|
|
2844
|
-
if (sm) {
|
|
2845
|
-
void sm.create(prep.projectPath, currentProvider, currentModel).then((s) => {
|
|
2846
|
-
sessionPathRef.current = s.path;
|
|
2847
|
-
log("INFO", "pixel", "New session for pixel fix", { path: s.path });
|
|
2848
|
-
});
|
|
2849
|
-
}
|
|
2850
|
-
if (messagesRef.current[0]?.role === "system") {
|
|
2851
|
-
messagesRef.current[0] = { role: "system", content: newSystemPrompt };
|
|
2852
|
-
}
|
|
2853
|
-
else {
|
|
2854
|
-
messagesRef.current.unshift({ role: "system", content: newSystemPrompt });
|
|
2855
|
-
}
|
|
2856
|
-
const title = `Fix ${errorId.slice(0, 12)}… in ${prep.projectName}`;
|
|
2857
|
-
const goalItem = { kind: "goal", title, id: getId() };
|
|
2858
|
-
setLastUserMessage(title);
|
|
2859
|
-
setDoneStatus(null);
|
|
2860
|
-
setLiveItems([goalItem]);
|
|
2861
|
-
await agentLoop.run(prep.prompt);
|
|
2862
|
-
}
|
|
2863
|
-
catch (err) {
|
|
2864
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2865
|
-
log("ERROR", "pixel", msg);
|
|
2866
|
-
currentPixelFixRef.current = null;
|
|
2867
|
-
setRunAllPixel(false);
|
|
2868
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2869
|
-
}
|
|
2870
|
-
})();
|
|
2871
|
-
}, [props.cwd, agentLoop, currentProvider, currentModel]);
|
|
2872
|
-
startPixelFixRef.current = startPixelFix;
|
|
2873
|
-
// Seed from sessionStore so "Fix All" chaining survives a deferred
|
|
2874
|
-
// resetUI() if it fires between pixel fixes (e.g. user toggled a pane).
|
|
2875
|
-
const [runAllPixel, setRunAllPixel] = useState(props.sessionStore?.runAllPixel ?? false);
|
|
2876
|
-
useEffect(() => {
|
|
2877
|
-
runAllPixelRef.current = runAllPixel;
|
|
2878
|
-
if (props.sessionStore)
|
|
2879
|
-
props.sessionStore.runAllPixel = runAllPixel;
|
|
2880
|
-
}, [runAllPixel, props.sessionStore]);
|
|
1932
|
+
const { startPixelFix, setRunAllPixel } = usePixelFixFlow({
|
|
1933
|
+
agentLoop,
|
|
1934
|
+
cwd: props.cwd,
|
|
1935
|
+
currentProvider,
|
|
1936
|
+
currentModel,
|
|
1937
|
+
rebuildToolsForCwd: props.rebuildToolsForCwd,
|
|
1938
|
+
sessionStore: props.sessionStore,
|
|
1939
|
+
currentPixelFixRef,
|
|
1940
|
+
runAllPixelRef,
|
|
1941
|
+
startPixelFixRef,
|
|
1942
|
+
cwdRef,
|
|
1943
|
+
currentToolsRef,
|
|
1944
|
+
injectedLanguagesRef,
|
|
1945
|
+
setupHintShownRef,
|
|
1946
|
+
messagesRef,
|
|
1947
|
+
persistedIndexRef,
|
|
1948
|
+
sessionManagerRef,
|
|
1949
|
+
sessionPathRef,
|
|
1950
|
+
setDisplayedCwd,
|
|
1951
|
+
setCurrentTools,
|
|
1952
|
+
setHistory,
|
|
1953
|
+
setLiveItems,
|
|
1954
|
+
setLastUserMessage,
|
|
1955
|
+
setDoneStatus,
|
|
1956
|
+
rebuildSystemPrompt,
|
|
1957
|
+
clearPendingHistory,
|
|
1958
|
+
getId,
|
|
1959
|
+
initialRunAllPixel: props.sessionStore?.runAllPixel ?? false,
|
|
1960
|
+
});
|
|
2881
1961
|
const isSkillsView = overlay === "skills";
|
|
2882
1962
|
const isPlanView = overlay === "plan";
|
|
2883
1963
|
const { footerStatusLayout, activityVisible, stallStatusVisible, statusSlotVisible, mainControlsRef, measuredLiveAreaRows, } = useChatLayoutMeasurements({
|
|
@@ -2953,6 +2033,10 @@ export function App(props) {
|
|
|
2953
2033
|
lastPendingHistoryItem,
|
|
2954
2034
|
lastHistoryItem,
|
|
2955
2035
|
});
|
|
2036
|
+
// When earlier paragraphs of THIS response were already flushed to scrollback
|
|
2037
|
+
// mid-stream, the live remainder is the next paragraph — re-insert the blank
|
|
2038
|
+
// line that separated them so the live tail lines up with the flushed history.
|
|
2039
|
+
const streamingContinuesFlushed = streamedAssistantFlushRef.current.flushedChars > 0;
|
|
2956
2040
|
const visibleQueuedCount = liveItems.filter((item) => item.kind === "queued").length;
|
|
2957
2041
|
const hiddenQueuedCount = Math.max(0, agentLoop.queuedCount - visibleQueuedCount);
|
|
2958
2042
|
const shouldTopSpaceQueueIndicator = hiddenQueuedCount > 0 &&
|
|
@@ -3204,7 +2288,7 @@ export function App(props) {
|
|
|
3204
2288
|
if (quittingSummary) {
|
|
3205
2289
|
return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: _jsx(SessionSummaryDisplay, { summary: quittingSummary }) }));
|
|
3206
2290
|
}
|
|
3207
|
-
return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: fullScreenOverlay ? (_jsx(FullScreenOverlayRouter, { overlay: fullScreenOverlay, version: props.version, cwd: props.cwd, agentRunning: agentLoop.isRunning, planAutoExpand: planAutoExpand, onClosePixel: handleCloseRemountableOverlay, onPixelFixOne: handlePixelFixOne, onPixelFixAll: handlePixelFixAll, onCloseSkills: handleCloseRemountableOverlay, onClosePlan: handleClosePlanOverlay, onApprovePlan: handleApprovePlan, onRejectPlan: handleRejectPlan })) : (_jsx(ChatScreen, { columns: columns, liveItems: uniqueItemsById(liveItems), renderItem: renderItem, isRunning: agentLoop.isRunning, visibleStreamingText: visibleStreamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, reserveStreamingSpacing: shouldReserveStreamingSpacing, renderMarkdown: renderMarkdown, measuredLiveAreaRows: measuredLiveAreaRows, assistantMarginTop: shouldTopSpaceStreamingText ? 1 : 0, streamingContinuation:
|
|
2291
|
+
return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: fullScreenOverlay ? (_jsx(FullScreenOverlayRouter, { overlay: fullScreenOverlay, version: props.version, cwd: props.cwd, agentRunning: agentLoop.isRunning, planAutoExpand: planAutoExpand, onClosePixel: handleCloseRemountableOverlay, onPixelFixOne: handlePixelFixOne, onPixelFixAll: handlePixelFixAll, onCloseSkills: handleCloseRemountableOverlay, onClosePlan: handleClosePlanOverlay, onApprovePlan: handleApprovePlan, onRejectPlan: handleRejectPlan })) : (_jsx(ChatScreen, { columns: columns, liveItems: uniqueItemsById(liveItems), renderItem: renderItem, isRunning: agentLoop.isRunning, visibleStreamingText: visibleStreamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, reserveStreamingSpacing: shouldReserveStreamingSpacing, renderMarkdown: renderMarkdown, measuredLiveAreaRows: measuredLiveAreaRows, assistantMarginTop: shouldTopSpaceStreamingText || streamingContinuesFlushed ? 1 : 0, streamingContinuation: streamingContinuesFlushed, controlsRef: mainControlsRef, hiddenQueuedCount: hiddenQueuedCount, queueIndicatorMarginTop: shouldTopSpaceQueueIndicator ? 2 : 1, theme: theme, statusSlotVisible: statusSlotVisible, activityVisible: activityVisible, stallStatusVisible: stallStatusVisible, doneStatus: doneStatus, activityPhase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, isThinking: agentLoop.isThinking, thinkingLevel: thinkingLevel, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, lastUserMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, formatDuration: formatDuration, inputControls: {
|
|
3208
2292
|
onSubmit: handleSubmit,
|
|
3209
2293
|
onAbort: handleAbort,
|
|
3210
2294
|
inputActive: !taskBarFocused && !overlay,
|