@prestyj/cli 4.3.207 → 4.3.209
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.js +10 -7
- package/dist/cli.js.map +1 -1
- package/dist/core/process-manager-dev-server-repro.test.js +37 -1
- package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
- package/dist/core/process-manager.d.ts +9 -0
- package/dist/core/process-manager.d.ts.map +1 -1
- package/dist/core/process-manager.js +20 -6
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/repomap.js +8 -1
- package/dist/core/repomap.js.map +1 -1
- package/dist/core/repomap.test.js +32 -0
- package/dist/core/repomap.test.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +2 -51
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +22 -5
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/grep.test.d.ts +2 -0
- package/dist/tools/grep.test.d.ts.map +1 -0
- package/dist/tools/grep.test.js +20 -0
- package/dist/tools/grep.test.js.map +1 -0
- package/dist/tools/safe-env.d.ts +2 -0
- package/dist/tools/safe-env.d.ts.map +1 -0
- package/dist/tools/safe-env.js +52 -0
- package/dist/tools/safe-env.js.map +1 -0
- package/dist/tools/web-fetch.d.ts +5 -0
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +12 -1
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-fetch.test.js +9 -0
- package/dist/tools/web-fetch.test.js.map +1 -1
- package/dist/ui/App.d.ts +3 -3
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +186 -211
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/app-state-persistence.test.js +60 -4
- package/dist/ui/app-state-persistence.test.js.map +1 -1
- package/dist/ui/goal-events.test.js +19 -0
- package/dist/ui/goal-events.test.js.map +1 -1
- package/dist/ui/goal-overlay.test.js +30 -0
- package/dist/ui/goal-overlay.test.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +3 -1
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +10 -2
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/render.d.ts +7 -0
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +1 -0
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/scroll-stabilization.test.js +2 -2
- package/dist/ui/scroll-stabilization.test.js.map +1 -1
- package/dist/ui/slash-command-images.test.js +10 -0
- package/dist/ui/slash-command-images.test.js.map +1 -1
- package/package.json +6 -5
package/dist/ui/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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, Static
|
|
3
|
+
import { Box, Text, Static } 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";
|
|
@@ -59,8 +59,9 @@ import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMes
|
|
|
59
59
|
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
60
60
|
import { getMCPServers } from "../core/mcp/index.js";
|
|
61
61
|
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
|
|
62
|
-
import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns,
|
|
62
|
+
import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, summarizeGoalCounts, summarizeGoalCountsFromRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
|
|
63
63
|
import { canCompleteGoalRun, decideGoalNextAction, shouldCreateVerifierFixTask, } from "../core/goal-controller.js";
|
|
64
|
+
import { runGoalVerifierCommand } from "../core/goal-verifier.js";
|
|
64
65
|
import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
|
|
65
66
|
import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
|
|
66
67
|
/** Where ezcoder bugs should be reported. Surfaced in the guidance line. */
|
|
@@ -189,9 +190,7 @@ function summarizeGoalCompletion(summary) {
|
|
|
189
190
|
return statusLine ?? changedLine ?? verificationLine ?? lines[0];
|
|
190
191
|
}
|
|
191
192
|
function formatGoalWorkerFinishedTitle(taskTitle, status) {
|
|
192
|
-
return status === "done"
|
|
193
|
-
? `Worker finished: ${taskTitle}. Reporting back.`
|
|
194
|
-
: `Worker failed: ${taskTitle}. Reporting back.`;
|
|
193
|
+
return status === "done" ? `Done: ${taskTitle}` : `Failed: ${taskTitle}`;
|
|
195
194
|
}
|
|
196
195
|
function countGoalTasksByStatus(tasks, status) {
|
|
197
196
|
return tasks.filter((task) => task.status === status).length;
|
|
@@ -298,12 +297,11 @@ export function formatGoalTerminalProgress(run) {
|
|
|
298
297
|
return null;
|
|
299
298
|
}
|
|
300
299
|
}
|
|
301
|
-
export function shouldHideHistoryForOverlayView(
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
return isOverlayView && !isAgentRunning;
|
|
300
|
+
export function shouldHideHistoryForOverlayView(_isOverlayView, _isAgentRunning) {
|
|
301
|
+
// Ink Static is append-only. Passing [] for overlay panes rewrites the Static
|
|
302
|
+
// accumulator and can destroy scrollback when the pane closes. Keep history
|
|
303
|
+
// mounted and let overlays render below it.
|
|
304
|
+
return false;
|
|
307
305
|
}
|
|
308
306
|
export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
|
|
309
307
|
return isAgentRunning && (overlayPane === "goal" || overlayPane === "plan");
|
|
@@ -321,8 +319,8 @@ export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, h
|
|
|
321
319
|
export function isTallLiveUserMessage(text, rows) {
|
|
322
320
|
return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
|
|
323
321
|
}
|
|
324
|
-
export function getStaticHistoryKey({ resizeKey
|
|
325
|
-
return `${resizeKey}
|
|
322
|
+
export function getStaticHistoryKey({ resizeKey }) {
|
|
323
|
+
return `${resizeKey}`;
|
|
326
324
|
}
|
|
327
325
|
// flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
|
|
328
326
|
/** Check whether an item is still active (running spinner, pending result). */
|
|
@@ -481,11 +479,29 @@ function markTaskInProgress(cwd, taskId) {
|
|
|
481
479
|
// ignore
|
|
482
480
|
}
|
|
483
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Mark a task as done in the on-disk tasks.json. Used by run-all auto-advance
|
|
484
|
+
* so the in-flight task is recorded as completed even when the agent forgets
|
|
485
|
+
* to call the `tasks` tool. Errors are swallowed — if the file is missing or
|
|
486
|
+
* the id is stale, the next auto-advance step still proceeds correctly.
|
|
487
|
+
*/
|
|
488
|
+
function markTaskDone(cwd, taskId) {
|
|
489
|
+
try {
|
|
490
|
+
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
491
|
+
const filePath = join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json");
|
|
492
|
+
const data = readFileSync(filePath, "utf-8");
|
|
493
|
+
const tasks = JSON.parse(data);
|
|
494
|
+
const updated = tasks.map((t) => (t.id === taskId ? { ...t, status: "done" } : t));
|
|
495
|
+
writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
// ignore
|
|
499
|
+
}
|
|
500
|
+
}
|
|
484
501
|
// ── App Component ──────────────────────────────────────────
|
|
485
502
|
export function App(props) {
|
|
486
503
|
const theme = useTheme();
|
|
487
504
|
const switchTheme = useSetTheme();
|
|
488
|
-
const { stdout } = useStdout();
|
|
489
505
|
const { columns, resizeKey } = useTerminalSize();
|
|
490
506
|
// Hoisted before terminal title hook so it can reference them
|
|
491
507
|
const [lastUserMessage, setLastUserMessage] = useState("");
|
|
@@ -537,6 +553,11 @@ export function App(props) {
|
|
|
537
553
|
// remount that startTask() triggers between tasks.
|
|
538
554
|
const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
|
|
539
555
|
const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
|
|
556
|
+
// Task id of the run-all task currently driving the agent. Tracked so the
|
|
557
|
+
// run-all `onDone` auto-advance can mark the just-finished task as done
|
|
558
|
+
// even when the agent forgets to call the tasks tool. Mirrored through
|
|
559
|
+
// sessionStore so the value survives the resetUI() remount between tasks.
|
|
560
|
+
const currentTaskIdRef = useRef(props.sessionStore?.currentTaskId ?? null);
|
|
540
561
|
const startTaskRef = useRef(() => { });
|
|
541
562
|
const agentRunningRef = useRef(false);
|
|
542
563
|
const runningGoalIdsRef = useRef(new Set());
|
|
@@ -547,7 +568,6 @@ export function App(props) {
|
|
|
547
568
|
const startPixelFixRef = useRef(() => { });
|
|
548
569
|
const cwdRef = useRef(props.cwd);
|
|
549
570
|
const [displayedCwd, setDisplayedCwd] = useState(props.cwd);
|
|
550
|
-
const [staticKey, setStaticKey] = useState(0);
|
|
551
571
|
const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
|
|
552
572
|
// Suppress "done" status when a plan overlay is about to open
|
|
553
573
|
const planOverlayPendingRef = useRef(false);
|
|
@@ -906,14 +926,6 @@ export function App(props) {
|
|
|
906
926
|
// premature "done" status that fires when the agent loop finishes
|
|
907
927
|
planOverlayPendingRef.current = true;
|
|
908
928
|
setTimeout(() => {
|
|
909
|
-
// NOTE: this is the one open-overlay path that does NOT remount via
|
|
910
|
-
// resetUI. It runs while the agent is still mid-turn (after the
|
|
911
|
-
// exit_plan tool returned but before onDone fires), and unmounting
|
|
912
|
-
// here would kill the in-flight agent stream. Keep the bare ANSI
|
|
913
|
-
// clear; the drift bug is tolerable across just the agent's
|
|
914
|
-
// wrap-up turn, and onApprove/onReject both remount cleanly via
|
|
915
|
-
// resetUI when the user resolves the plan.
|
|
916
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
917
929
|
setPlanAutoExpand(true);
|
|
918
930
|
setOverlay("plan");
|
|
919
931
|
// Don't clear planOverlayPendingRef here — keep it true until
|
|
@@ -928,7 +940,7 @@ export function App(props) {
|
|
|
928
940
|
planPath);
|
|
929
941
|
};
|
|
930
942
|
}
|
|
931
|
-
}, [props.onExitPlanRef, replaceSystemPrompt
|
|
943
|
+
}, [props.onExitPlanRef, replaceSystemPrompt]);
|
|
932
944
|
const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
|
|
933
945
|
const sm = sessionManagerRef.current;
|
|
934
946
|
if (!sm)
|
|
@@ -1674,7 +1686,7 @@ export function App(props) {
|
|
|
1674
1686
|
return remaining;
|
|
1675
1687
|
});
|
|
1676
1688
|
}, []),
|
|
1677
|
-
onDone: useCallback((durationMs, toolsUsed) => {
|
|
1689
|
+
onDone: useCallback((durationMs, toolsUsed, errored) => {
|
|
1678
1690
|
log("INFO", "agent", `Agent done`, {
|
|
1679
1691
|
duration: `${durationMs}ms`,
|
|
1680
1692
|
toolsUsed: toolsUsed.join(",") || "none",
|
|
@@ -1701,6 +1713,18 @@ export function App(props) {
|
|
|
1701
1713
|
if (runAllTasksRef.current) {
|
|
1702
1714
|
setTimeout(() => {
|
|
1703
1715
|
const cwd = cwdRef.current;
|
|
1716
|
+
// Clean turn: mark the in-flight task done so the row flips to ✓.
|
|
1717
|
+
// Error path: leave it at `in-progress` so the failed task is
|
|
1718
|
+
// visibly distinct from completed ones. The chain still advances
|
|
1719
|
+
// either way — matching how errors were handled before this fix.
|
|
1720
|
+
const finishedId = currentTaskIdRef.current;
|
|
1721
|
+
if (finishedId) {
|
|
1722
|
+
if (!errored)
|
|
1723
|
+
markTaskDone(cwd, finishedId);
|
|
1724
|
+
currentTaskIdRef.current = null;
|
|
1725
|
+
if (props.sessionStore)
|
|
1726
|
+
props.sessionStore.currentTaskId = null;
|
|
1727
|
+
}
|
|
1704
1728
|
const next = getNextPendingTask(cwd);
|
|
1705
1729
|
if (next) {
|
|
1706
1730
|
markTaskInProgress(cwd, next.id);
|
|
@@ -1758,6 +1782,12 @@ export function App(props) {
|
|
|
1758
1782
|
setRunAllTasks(false);
|
|
1759
1783
|
setRunAllPixel(false);
|
|
1760
1784
|
currentPixelFixRef.current = null;
|
|
1785
|
+
// Clear the run-all task id so a subsequent run-all does not
|
|
1786
|
+
// mistakenly mark this aborted task as done. The on-disk row stays
|
|
1787
|
+
// at `in-progress`, which is the correct truth for an aborted task.
|
|
1788
|
+
currentTaskIdRef.current = null;
|
|
1789
|
+
if (props.sessionStore)
|
|
1790
|
+
props.sessionStore.currentTaskId = null;
|
|
1761
1791
|
setDoneStatus(null);
|
|
1762
1792
|
setLiveItems((prev) => {
|
|
1763
1793
|
const next = prev.map((item) => {
|
|
@@ -2045,14 +2075,10 @@ export function App(props) {
|
|
|
2045
2075
|
process.exit(0);
|
|
2046
2076
|
}
|
|
2047
2077
|
// Handle /clear — tear down the entire Ink instance and rebuild fresh.
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2050
|
-
//
|
|
2051
|
-
//
|
|
2052
|
-
// terminal-state assumptions that ANSI clearing breaks. The reliable
|
|
2053
|
-
// fix is unmount + render again. Runtime state (model, provider,
|
|
2054
|
-
// thinking) survives via renderApp's closure-held `runtimeState`,
|
|
2055
|
-
// mirrored from React state via the useEffects above.
|
|
2078
|
+
// Avoid direct ANSI terminal clears here; they can erase scrollback.
|
|
2079
|
+
// Runtime state (model, provider, thinking) survives via renderApp's
|
|
2080
|
+
// closure-held `runtimeState`, mirrored from React state via the
|
|
2081
|
+
// useEffects above.
|
|
2056
2082
|
if (trimmed === "/clear") {
|
|
2057
2083
|
if (props.resetUI) {
|
|
2058
2084
|
void (async () => {
|
|
@@ -2065,8 +2091,7 @@ export function App(props) {
|
|
|
2065
2091
|
return;
|
|
2066
2092
|
}
|
|
2067
2093
|
// Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
|
|
2068
|
-
// React state in place
|
|
2069
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2094
|
+
// React state in place without touching terminal scrollback.
|
|
2070
2095
|
pendingFlushRef.current = [];
|
|
2071
2096
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2072
2097
|
setLiveItems([]);
|
|
@@ -2082,7 +2107,6 @@ export function App(props) {
|
|
|
2082
2107
|
agentLoop.reset();
|
|
2083
2108
|
setSessionTitle(undefined);
|
|
2084
2109
|
sessionTitleGeneratedRef.current = false;
|
|
2085
|
-
setStaticKey((k) => k + 1);
|
|
2086
2110
|
setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
|
|
2087
2111
|
return;
|
|
2088
2112
|
}
|
|
@@ -2190,8 +2214,6 @@ export function App(props) {
|
|
|
2190
2214
|
props.resetUI();
|
|
2191
2215
|
}
|
|
2192
2216
|
else {
|
|
2193
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2194
|
-
setStaticKey((key) => key + 1);
|
|
2195
2217
|
if (props.sessionStore) {
|
|
2196
2218
|
props.sessionStore.overlay = "goal";
|
|
2197
2219
|
props.sessionStore.planAutoExpand = false;
|
|
@@ -2549,11 +2571,23 @@ export function App(props) {
|
|
|
2549
2571
|
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));
|
|
2550
2572
|
case "goal_progress": {
|
|
2551
2573
|
const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
|
|
2552
|
-
const color =
|
|
2553
|
-
? theme.
|
|
2554
|
-
:
|
|
2555
|
-
? theme.
|
|
2556
|
-
:
|
|
2574
|
+
const color = isError
|
|
2575
|
+
? theme.error
|
|
2576
|
+
: item.phase === "worker_finished"
|
|
2577
|
+
? theme.success
|
|
2578
|
+
: item.phase === "verifier_finished"
|
|
2579
|
+
? theme.accent
|
|
2580
|
+
: item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working"
|
|
2581
|
+
? theme.secondary
|
|
2582
|
+
: item.phase === "continuing"
|
|
2583
|
+
? theme.warning
|
|
2584
|
+
: item.phase === "verifier_started"
|
|
2585
|
+
? theme.accent
|
|
2586
|
+
: item.phase === "worker_started"
|
|
2587
|
+
? theme.primary
|
|
2588
|
+
: item.phase === "terminal"
|
|
2589
|
+
? theme.success
|
|
2590
|
+
: theme.primary;
|
|
2557
2591
|
const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
|
|
2558
2592
|
? "✓ "
|
|
2559
2593
|
: item.phase === "terminal"
|
|
@@ -2662,8 +2696,14 @@ export function App(props) {
|
|
|
2662
2696
|
// session creation is best-effort
|
|
2663
2697
|
}
|
|
2664
2698
|
}
|
|
2665
|
-
if (props.sessionStore)
|
|
2699
|
+
if (props.sessionStore) {
|
|
2666
2700
|
props.sessionStore.overlay = null;
|
|
2701
|
+
// Mirror through sessionStore so the post-resetUI remount can
|
|
2702
|
+
// re-seed the ref. markTaskDone() in the run-all onDone branch
|
|
2703
|
+
// reads currentTaskIdRef.current, which is seeded from this.
|
|
2704
|
+
props.sessionStore.currentTaskId = taskId;
|
|
2705
|
+
}
|
|
2706
|
+
currentTaskIdRef.current = taskId;
|
|
2667
2707
|
props.resetUI?.({
|
|
2668
2708
|
wipeSession: true,
|
|
2669
2709
|
messages: newMessages,
|
|
@@ -2675,7 +2715,9 @@ export function App(props) {
|
|
|
2675
2715
|
return;
|
|
2676
2716
|
}
|
|
2677
2717
|
// Fallback path (resetUI not wired — tests).
|
|
2678
|
-
|
|
2718
|
+
currentTaskIdRef.current = taskId;
|
|
2719
|
+
if (props.sessionStore)
|
|
2720
|
+
props.sessionStore.currentTaskId = taskId;
|
|
2679
2721
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2680
2722
|
setLiveItems([]);
|
|
2681
2723
|
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
@@ -2709,15 +2751,7 @@ export function App(props) {
|
|
|
2709
2751
|
setRunAllTasks(false);
|
|
2710
2752
|
}
|
|
2711
2753
|
})();
|
|
2712
|
-
}, [
|
|
2713
|
-
props.cwd,
|
|
2714
|
-
props.resetUI,
|
|
2715
|
-
props.sessionStore,
|
|
2716
|
-
stdout,
|
|
2717
|
-
agentLoop,
|
|
2718
|
-
currentProvider,
|
|
2719
|
-
currentModel,
|
|
2720
|
-
]);
|
|
2754
|
+
}, [props.cwd, props.resetUI, props.sessionStore, agentLoop, currentProvider, currentModel]);
|
|
2721
2755
|
const openOverlay = useCallback((kind) => {
|
|
2722
2756
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2723
2757
|
props.sessionStore.overlay = kind;
|
|
@@ -2738,7 +2772,7 @@ export function App(props) {
|
|
|
2738
2772
|
setPlanAutoExpand(false);
|
|
2739
2773
|
setOverlay(kind);
|
|
2740
2774
|
}
|
|
2741
|
-
}, [agentLoop.isRunning, props
|
|
2775
|
+
}, [agentLoop.isRunning, props]);
|
|
2742
2776
|
const closeOverlay = useCallback(() => {
|
|
2743
2777
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2744
2778
|
props.sessionStore.overlay = null;
|
|
@@ -2750,7 +2784,7 @@ export function App(props) {
|
|
|
2750
2784
|
}
|
|
2751
2785
|
setOverlay(null);
|
|
2752
2786
|
}
|
|
2753
|
-
}, [agentLoop.isRunning, overlay, props
|
|
2787
|
+
}, [agentLoop.isRunning, overlay, props]);
|
|
2754
2788
|
const runGoalSyntheticEvent = useCallback((eventText) => {
|
|
2755
2789
|
const eventInfo = parseGoalSyntheticEvent(eventText);
|
|
2756
2790
|
const detail = eventInfo?.kind === "worker"
|
|
@@ -2841,8 +2875,8 @@ export function App(props) {
|
|
|
2841
2875
|
appendGoalProgress({
|
|
2842
2876
|
kind: "goal_progress",
|
|
2843
2877
|
phase: "continuing",
|
|
2844
|
-
title: `
|
|
2845
|
-
detail: "
|
|
2878
|
+
title: `Choosing next Goal step: ${latestRun.title}`,
|
|
2879
|
+
detail: "Latest result is recorded; starting the next worker task or verifier automatically.",
|
|
2846
2880
|
status: latestRun.status,
|
|
2847
2881
|
});
|
|
2848
2882
|
upsertGoalStatusEntry({
|
|
@@ -3138,143 +3172,89 @@ export function App(props) {
|
|
|
3138
3172
|
detail: run.verifier.command,
|
|
3139
3173
|
goalNumber: goalNumberForRun(run.id),
|
|
3140
3174
|
});
|
|
3141
|
-
|
|
3142
|
-
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
3143
|
-
const { join } = await import("node:path");
|
|
3144
|
-
const logDir = join(projectDir(props.cwd), "verifiers");
|
|
3145
|
-
await mkdir(logDir, { recursive: true });
|
|
3146
|
-
const outputPath = join(logDir, `${run.id}-${startedAt}.log`);
|
|
3147
|
-
const child = spawn(run.verifier.command, {
|
|
3175
|
+
void runGoalVerifierCommand({
|
|
3148
3176
|
cwd: props.cwd,
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
output += chunk.toString("utf-8");
|
|
3156
|
-
if (output.length > 20_000)
|
|
3157
|
-
output = output.slice(output.length - 20_000);
|
|
3158
|
-
});
|
|
3159
|
-
child.stderr?.on("data", (chunk) => {
|
|
3160
|
-
output += chunk.toString("utf-8");
|
|
3161
|
-
if (output.length > 20_000)
|
|
3162
|
-
output = output.slice(output.length - 20_000);
|
|
3163
|
-
});
|
|
3164
|
-
let verifierSettled = false;
|
|
3165
|
-
let timedOut = false;
|
|
3166
|
-
const timeout = verifierTimeoutMs > 0
|
|
3167
|
-
? setTimeout(() => {
|
|
3168
|
-
timedOut = true;
|
|
3169
|
-
if (child.pid)
|
|
3170
|
-
child.kill("SIGTERM");
|
|
3171
|
-
const killTimer = setTimeout(() => {
|
|
3172
|
-
if (!verifierSettled && child.pid)
|
|
3173
|
-
child.kill("SIGKILL");
|
|
3174
|
-
}, 5000);
|
|
3175
|
-
killTimer.unref?.();
|
|
3176
|
-
finishVerifier(124, `Verifier timed out after ${verifierTimeoutMs}ms and was terminated.\n${output}`);
|
|
3177
|
-
}, verifierTimeoutMs)
|
|
3178
|
-
: undefined;
|
|
3179
|
-
timeout?.unref?.();
|
|
3180
|
-
const finishVerifier = (code, forcedOutput) => {
|
|
3181
|
-
if (verifierSettled)
|
|
3182
|
-
return;
|
|
3183
|
-
verifierSettled = true;
|
|
3184
|
-
if (timeout)
|
|
3185
|
-
clearTimeout(timeout);
|
|
3177
|
+
runId: run.id,
|
|
3178
|
+
command: run.verifier.command,
|
|
3179
|
+
timeoutMs: verifierTimeoutMs,
|
|
3180
|
+
now: () => startedAt,
|
|
3181
|
+
})
|
|
3182
|
+
.then(async ({ verification, failureClass, durationMs }) => {
|
|
3186
3183
|
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
label: `Verifier ${status}`,
|
|
3229
|
-
content: `${failureClass}: ${summary}`.slice(0, 4000),
|
|
3230
|
-
path: outputPath,
|
|
3231
|
-
});
|
|
3232
|
-
await appendGoalDecision(props.cwd, run.id, {
|
|
3233
|
-
kind: `verifier_${status}`,
|
|
3234
|
-
reason: `${failureClass}: verifier exited with code ${code ?? 1}.`,
|
|
3235
|
-
content: `outputPath=${outputPath}; durationMs=${Date.now() - startedAt}`,
|
|
3236
|
-
});
|
|
3237
|
-
if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
|
|
3238
|
-
await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
|
|
3239
|
-
title: "Fix verifier failure",
|
|
3240
|
-
prompt: `Goal verifier failed after ${Date.now() - startedAt}ms. Original goal: ${run.goal}\n\n` +
|
|
3241
|
-
`Verifier command: ${run.verifier?.command}\n\n` +
|
|
3242
|
-
`Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
|
|
3243
|
-
status: "pending",
|
|
3244
|
-
});
|
|
3245
|
-
const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
|
|
3246
|
-
await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
|
|
3247
|
-
}
|
|
3248
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3249
|
-
appendGoalProgress({
|
|
3250
|
-
kind: "goal_progress",
|
|
3251
|
-
phase: "verifier_finished",
|
|
3252
|
-
title: `Verifier ${status}: ${run.title}`,
|
|
3253
|
-
detail: summarizeGoalCompletion(summary),
|
|
3254
|
-
status,
|
|
3255
|
-
});
|
|
3256
|
-
upsertGoalStatusEntry({
|
|
3257
|
-
runId: run.id,
|
|
3258
|
-
label: run.title,
|
|
3259
|
-
phase: status === "pass" ? "reviewing" : "failed",
|
|
3260
|
-
startedAt: Date.now(),
|
|
3261
|
-
detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
|
|
3262
|
-
goalNumber: goalNumberForRun(run.id),
|
|
3184
|
+
const status = verification.status;
|
|
3185
|
+
const summary = verification.summary;
|
|
3186
|
+
const outputPath = verification.outputPath;
|
|
3187
|
+
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
3188
|
+
const runWithVerifier = {
|
|
3189
|
+
...latestRun,
|
|
3190
|
+
verifier: {
|
|
3191
|
+
...latestRun.verifier,
|
|
3192
|
+
description: latestRun.verifier?.description ?? "Goal verifier",
|
|
3193
|
+
command: run.verifier?.command,
|
|
3194
|
+
lastResult: verification,
|
|
3195
|
+
},
|
|
3196
|
+
};
|
|
3197
|
+
const completionCheck = canCompleteGoalRun(runWithVerifier);
|
|
3198
|
+
const verifiedRun = await upsertGoalRun(props.cwd, {
|
|
3199
|
+
...runWithVerifier,
|
|
3200
|
+
continueRequestedAt: undefined,
|
|
3201
|
+
status: status === "pass" && completionCheck.ok
|
|
3202
|
+
? "passed"
|
|
3203
|
+
: status === "pass"
|
|
3204
|
+
? "ready"
|
|
3205
|
+
: "failed",
|
|
3206
|
+
});
|
|
3207
|
+
await appendGoalEvidence(props.cwd, run.id, {
|
|
3208
|
+
kind: "command",
|
|
3209
|
+
label: `Verifier ${status}`,
|
|
3210
|
+
content: `${failureClass}: ${summary}`.slice(0, 4000),
|
|
3211
|
+
path: outputPath,
|
|
3212
|
+
});
|
|
3213
|
+
await appendGoalDecision(props.cwd, run.id, {
|
|
3214
|
+
kind: `verifier_${status}`,
|
|
3215
|
+
reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
|
|
3216
|
+
content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
|
|
3217
|
+
});
|
|
3218
|
+
if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
|
|
3219
|
+
await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
|
|
3220
|
+
title: "Fix verifier failure",
|
|
3221
|
+
prompt: `Goal verifier failed after ${durationMs}ms. Original goal: ${run.goal}\n\n` +
|
|
3222
|
+
`Verifier command: ${run.verifier?.command}\n\n` +
|
|
3223
|
+
`Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
|
|
3224
|
+
status: "pending",
|
|
3263
3225
|
});
|
|
3264
|
-
const
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3226
|
+
const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
|
|
3227
|
+
await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
|
|
3228
|
+
}
|
|
3229
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3230
|
+
appendGoalProgress({
|
|
3231
|
+
kind: "goal_progress",
|
|
3232
|
+
phase: "verifier_finished",
|
|
3233
|
+
title: `Verifier ${status}: ${run.title}`,
|
|
3234
|
+
detail: summarizeGoalCompletion(summary),
|
|
3235
|
+
status,
|
|
3274
3236
|
});
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3237
|
+
upsertGoalStatusEntry({
|
|
3238
|
+
runId: run.id,
|
|
3239
|
+
label: run.title,
|
|
3240
|
+
phase: status === "pass" ? "reviewing" : "failed",
|
|
3241
|
+
startedAt: Date.now(),
|
|
3242
|
+
detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
|
|
3243
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3244
|
+
});
|
|
3245
|
+
const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
|
|
3246
|
+
runGoalSyntheticEvent(eventText);
|
|
3247
|
+
const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
|
|
3248
|
+
if (continuationRun?.continueRequestedAt && status === "pass") {
|
|
3249
|
+
setTimeout(() => continueGoalRun(run.id), 500);
|
|
3250
|
+
}
|
|
3251
|
+
})
|
|
3252
|
+
.catch((err) => {
|
|
3253
|
+
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3254
|
+
clearGoalStatusEntry(run.id);
|
|
3255
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3256
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
|
|
3257
|
+
});
|
|
3278
3258
|
}, [
|
|
3279
3259
|
props.cwd,
|
|
3280
3260
|
appendGoalProgress,
|
|
@@ -3308,8 +3288,12 @@ export function App(props) {
|
|
|
3308
3288
|
startGoalRunRef.current = startGoalRun;
|
|
3309
3289
|
useEffect(() => {
|
|
3310
3290
|
runAllTasksRef.current = runAllTasks;
|
|
3311
|
-
if (props.sessionStore)
|
|
3291
|
+
if (props.sessionStore) {
|
|
3312
3292
|
props.sessionStore.runAllTasks = runAllTasks;
|
|
3293
|
+
// Keep currentTaskId in sync with the ref so a sessionStore-seeded
|
|
3294
|
+
// remount picks up the same value the imperative sites wrote.
|
|
3295
|
+
props.sessionStore.currentTaskId = currentTaskIdRef.current;
|
|
3296
|
+
}
|
|
3313
3297
|
}, [runAllTasks, props.sessionStore]);
|
|
3314
3298
|
useEffect(() => {
|
|
3315
3299
|
agentRunningRef.current = agentLoop.isRunning;
|
|
@@ -3364,14 +3348,10 @@ export function App(props) {
|
|
|
3364
3348
|
activeLanguages: detectedForPixelFix,
|
|
3365
3349
|
tools: toolsForPixelFix,
|
|
3366
3350
|
});
|
|
3367
|
-
// Now that the cwd swap is committed, reset chat.
|
|
3368
|
-
//
|
|
3369
|
-
// staticKey would print a second banner with the new cwd — leaving
|
|
3370
|
-
// two banners stacked in the scrollback.
|
|
3371
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
3351
|
+
// Now that the cwd swap is committed, reset chat. Do not clear the
|
|
3352
|
+
// terminal here; terminal clear sequences can erase saved scrollback.
|
|
3372
3353
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3373
3354
|
setLiveItems([]);
|
|
3374
|
-
setStaticKey((k) => k + 1);
|
|
3375
3355
|
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
3376
3356
|
agentLoop.reset();
|
|
3377
3357
|
persistedIndexRef.current = messagesRef.current.length;
|
|
@@ -3403,7 +3383,7 @@ export function App(props) {
|
|
|
3403
3383
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3404
3384
|
}
|
|
3405
3385
|
})();
|
|
3406
|
-
}, [props.cwd,
|
|
3386
|
+
}, [props.cwd, agentLoop, currentProvider, currentModel]);
|
|
3407
3387
|
startPixelFixRef.current = startPixelFix;
|
|
3408
3388
|
// Seed from sessionStore so "Fix All" chaining survives a deferred
|
|
3409
3389
|
// resetUI() if it fires between pixel fixes (e.g. user toggled a pane).
|
|
@@ -3431,12 +3411,13 @@ export function App(props) {
|
|
|
3431
3411
|
overlayPane: overlay,
|
|
3432
3412
|
isAgentRunning: agentLoop.isRunning,
|
|
3433
3413
|
});
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3414
|
+
const staticItems = shouldHideStaticItemsForOverlayView({
|
|
3415
|
+
shouldHideHistoryForOverlay,
|
|
3416
|
+
stabilizeOverlayPaneRerender,
|
|
3417
|
+
})
|
|
3418
|
+
? []
|
|
3419
|
+
: history;
|
|
3420
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: staticItems, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, getStaticHistoryKey({ resizeKey })), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
3440
3421
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3441
3422
|
props.sessionStore.overlay = null;
|
|
3442
3423
|
props.resetUI();
|
|
@@ -3586,10 +3567,8 @@ export function App(props) {
|
|
|
3586
3567
|
approvedPlanPathRef.current = planPath;
|
|
3587
3568
|
planStepsRef.current = steps;
|
|
3588
3569
|
setPlanSteps(steps);
|
|
3589
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
3590
3570
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3591
3571
|
setLiveItems([]);
|
|
3592
|
-
setStaticKey((k) => k + 1);
|
|
3593
3572
|
setPlanAutoExpand(false);
|
|
3594
3573
|
setOverlay(null);
|
|
3595
3574
|
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
@@ -3630,8 +3609,6 @@ export function App(props) {
|
|
|
3630
3609
|
});
|
|
3631
3610
|
return;
|
|
3632
3611
|
}
|
|
3633
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
3634
|
-
setStaticKey((k) => k + 1);
|
|
3635
3612
|
setPlanAutoExpand(false);
|
|
3636
3613
|
setOverlay(null);
|
|
3637
3614
|
setDoneStatus(null);
|
|
@@ -3646,8 +3623,6 @@ export function App(props) {
|
|
|
3646
3623
|
});
|
|
3647
3624
|
} })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 1, children: [liveItems.map((item) => renderItem(item)), _jsx(StreamingArea, { isRunning: agentLoop.isRunning, streamingText: agentLoop.streamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, planMode: planMode })] }), agentLoop.isRunning && agentLoop.activityPhase !== "idle" ? (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: agentLoop.activityPhase === "thinking" ? THINKING_BORDER_COLORS[0] : "transparent", paddingLeft: 1, paddingRight: 1, width: columns, children: _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), planMode: planMode, retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, staticDisplay: true }) })) : agentLoop.stallError ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.warning, children: "⚠ API provider stream interrupted — retries exhausted." }), _jsx(Text, { color: theme.textDim, children: " Your conversation is preserved. Send a message to continue." })] })) : (doneStatus &&
|
|
3648
3625
|
!agentLoop.isRunning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), agentLoop.queuedCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.accent, children: ["⏳ ", agentLoop.queuedCount, " message", agentLoop.queuedCount > 1 ? "s" : "", " queued"] }) })), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleTasks: () => {
|
|
3649
|
-
// While the agent is running, skip the screen-clear + staticKey
|
|
3650
|
-
// bump that would otherwise wipe the chat history from scrollback.
|
|
3651
3626
|
// Just flip the overlay state — Ink's log-update handles the
|
|
3652
3627
|
// live-area transition (chat input → TaskOverlay) natively, and
|
|
3653
3628
|
// the chat history above stays in scrollback. When the overlay
|