@prestyj/cli 4.3.239 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +17 -2
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/shared.d.ts.map +1 -1
- package/dist/cli/shared.js +27 -5
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -2
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +10 -0
- package/dist/config.test.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +34 -15
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +18 -0
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +62 -8
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/auto-update.js +2 -2
- package/dist/core/auto-update.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +4 -0
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/goal-worker.js +1 -1
- package/dist/core/goal-worker.js.map +1 -1
- package/dist/core/goal-worker.test.js +1 -1
- package/dist/core/goal-worker.test.js.map +1 -1
- package/dist/core/mcp/defaults.d.ts.map +1 -1
- package/dist/core/mcp/defaults.js +39 -3
- package/dist/core/mcp/defaults.js.map +1 -1
- package/dist/core/model-registry.d.ts +21 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +47 -16
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-registry.test.js +13 -0
- package/dist/core/model-registry.test.js.map +1 -1
- package/dist/core/oauth/gemini.d.ts.map +1 -1
- package/dist/core/oauth/gemini.js +14 -5
- package/dist/core/oauth/gemini.js.map +1 -1
- package/dist/core/oauth/kimi.d.ts +46 -0
- package/dist/core/oauth/kimi.d.ts.map +1 -0
- package/dist/core/oauth/kimi.js +278 -0
- package/dist/core/oauth/kimi.js.map +1 -0
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +3 -1
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +2 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +22 -2
- package/dist/interactive.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +54 -23
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +32 -1
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/enter-plan.d.ts +1 -1
- package/dist/tools/enter-plan.d.ts.map +1 -1
- package/dist/tools/enter-plan.js +8 -1
- package/dist/tools/enter-plan.js.map +1 -1
- package/dist/tools/goals.d.ts +1 -1
- package/dist/tools/index.d.ts +16 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +9 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts +23 -4
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +35 -10
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/read.d.ts +6 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +70 -4
- package/dist/tools/read.js.map +1 -1
- package/dist/ui/App.d.ts +7 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +304 -28
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/app-items.d.ts +4 -2
- package/dist/ui/app-items.d.ts.map +1 -1
- package/dist/ui/app-items.js +13 -4
- package/dist/ui/app-items.js.map +1 -1
- package/dist/ui/chat-layout-pinning.test.js +1 -1
- package/dist/ui/chat-layout-pinning.test.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts +7 -0
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +38 -12
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/AssistantMessage.test.js +1 -1
- package/dist/ui/components/AssistantMessage.test.js.map +1 -1
- package/dist/ui/components/ChatInputStack.d.ts +2 -0
- package/dist/ui/components/ChatInputStack.d.ts.map +1 -1
- package/dist/ui/components/ChatInputStack.js.map +1 -1
- package/dist/ui/components/ChatScreen.d.ts +1 -0
- package/dist/ui/components/ChatScreen.d.ts.map +1 -1
- package/dist/ui/components/ChatScreen.js +1 -1
- package/dist/ui/components/ChatScreen.js.map +1 -1
- package/dist/ui/components/ChatStatusRow.d.ts +2 -0
- package/dist/ui/components/ChatStatusRow.d.ts.map +1 -1
- package/dist/ui/components/ChatStatusRow.js +40 -1
- package/dist/ui/components/ChatStatusRow.js.map +1 -1
- package/dist/ui/components/InputArea.d.ts +2 -1
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/InputArea.js +24 -8
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/MessageResponse.d.ts +7 -1
- package/dist/ui/components/MessageResponse.d.ts.map +1 -1
- package/dist/ui/components/MessageResponse.js +2 -2
- package/dist/ui/components/MessageResponse.js.map +1 -1
- package/dist/ui/components/ToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolExecution.js +72 -15
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/components/UserMessage.d.ts +2 -1
- package/dist/ui/components/UserMessage.d.ts.map +1 -1
- package/dist/ui/components/UserMessage.js +6 -2
- package/dist/ui/components/UserMessage.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +16 -4
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +46 -2
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/layout-decisions.d.ts +4 -0
- package/dist/ui/layout-decisions.d.ts.map +1 -1
- package/dist/ui/layout-decisions.js.map +1 -1
- package/dist/ui/login.js +2 -2
- package/dist/ui/login.js.map +1 -1
- package/dist/ui/prompt-routing.d.ts +2 -2
- package/dist/ui/prompt-routing.d.ts.map +1 -1
- package/dist/ui/prompt-routing.js +50 -1
- package/dist/ui/prompt-routing.js.map +1 -1
- package/dist/ui/queued-message.test.js +5 -1
- package/dist/ui/queued-message.test.js.map +1 -1
- package/dist/ui/render.d.ts +10 -1
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +35 -0
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/slash-command-images.test.js +55 -3
- package/dist/ui/slash-command-images.test.js.map +1 -1
- package/dist/ui/submit-prompt-command.d.ts.map +1 -1
- package/dist/ui/submit-prompt-command.js +6 -3
- package/dist/ui/submit-prompt-command.js.map +1 -1
- package/dist/ui/terminal-history-status-renderers.d.ts +1 -1
- package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -1
- package/dist/ui/terminal-history-status-renderers.js +1 -0
- package/dist/ui/terminal-history-status-renderers.js.map +1 -1
- package/dist/ui/terminal-history.js +36 -10
- package/dist/ui/terminal-history.js.map +1 -1
- package/dist/ui/terminal-history.test.js +3 -3
- package/dist/ui/terminal-history.test.js.map +1 -1
- package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -1
- package/dist/ui/transcript/TranscriptRenderer.js +12 -10
- package/dist/ui/transcript/TranscriptRenderer.js.map +1 -1
- package/dist/ui/transcript/presentation.d.ts.map +1 -1
- package/dist/ui/transcript/presentation.js +17 -2
- package/dist/ui/transcript/presentation.js.map +1 -1
- package/dist/ui/tui-history-parity.test.js +7 -3
- package/dist/ui/tui-history-parity.test.js.map +1 -1
- package/dist/ui/utils/assistant-stream-split.d.ts +10 -0
- package/dist/ui/utils/assistant-stream-split.d.ts.map +1 -1
- package/dist/ui/utils/assistant-stream-split.js +19 -0
- package/dist/ui/utils/assistant-stream-split.js.map +1 -1
- package/dist/ui/utils/assistant-stream-split.test.js +21 -1
- package/dist/ui/utils/assistant-stream-split.test.js.map +1 -1
- package/dist/utils/image.d.ts +37 -1
- package/dist/utils/image.d.ts.map +1 -1
- package/dist/utils/image.js +156 -1
- package/dist/utils/image.js.map +1 -1
- package/dist/utils/plan-steps.d.ts +29 -2
- package/dist/utils/plan-steps.d.ts.map +1 -1
- package/dist/utils/plan-steps.js +88 -16
- package/dist/utils/plan-steps.js.map +1 -1
- package/dist/utils/plan-steps.test.js +144 -1
- package/dist/utils/plan-steps.test.js.map +1 -1
- package/package.json +4 -3
package/dist/ui/App.js
CHANGED
|
@@ -12,7 +12,7 @@ import { useDoublePress } from "./hooks/useDoublePress.js";
|
|
|
12
12
|
import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
|
|
13
13
|
import { playNotificationSound } from "../utils/sound.js";
|
|
14
14
|
import {} from "@prestyj/ai";
|
|
15
|
-
import { downscaleForPreview,
|
|
15
|
+
import { downscaleForPreview, extractMediaPaths } from "../utils/image.js";
|
|
16
16
|
import { useAgentLoop } from "./hooks/useAgentLoop.js";
|
|
17
17
|
import { useTranscriptHistory } from "./hooks/useTranscriptHistory.js";
|
|
18
18
|
import { createWebSearchTool } from "../tools/web-search.js";
|
|
@@ -23,7 +23,7 @@ import { SessionSummaryDisplay } from "./components/SessionSummary.js";
|
|
|
23
23
|
import { useTheme, useSetTheme } from "./theme/theme.js";
|
|
24
24
|
import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
|
|
25
25
|
import { getGitBranch } from "../utils/git.js";
|
|
26
|
-
import { getModel } from "../core/model-registry.js";
|
|
26
|
+
import { getModel, getVideoByteLimit } from "../core/model-registry.js";
|
|
27
27
|
import { SessionManager } from "../core/session-manager.js";
|
|
28
28
|
import { log } from "../core/logger.js";
|
|
29
29
|
import { getPendingUpdate, startPeriodicUpdateCheck, stopPeriodicUpdateCheck, } from "../core/auto-update.js";
|
|
@@ -36,10 +36,10 @@ import { detectLanguages } from "../core/language-detector.js";
|
|
|
36
36
|
import { detectVerifyCommands } from "../core/verify-commands.js";
|
|
37
37
|
import { RewindOverlay } from "./components/RewindOverlay.js";
|
|
38
38
|
import { reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
|
|
39
|
-
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
40
|
-
import {
|
|
39
|
+
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, rebasePlanSteps, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
40
|
+
import { getAllMcpServers } from "../core/mcp/index.js";
|
|
41
41
|
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
|
|
42
|
-
import { splitAssistantStreamingText } from "./utils/assistant-stream-split.js";
|
|
42
|
+
import { splitAssistantStreamingText, estimateRenderedRows, } from "./utils/assistant-stream-split.js";
|
|
43
43
|
import { getNextPendingTask, markTaskInProgress } from "../core/task-store.js";
|
|
44
44
|
import { buildUserContentWithAttachments } from "./prompt-routing.js";
|
|
45
45
|
import { submitPromptCommand } from "./submit-prompt-command.js";
|
|
@@ -282,6 +282,10 @@ export function App(props) {
|
|
|
282
282
|
const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
|
|
283
283
|
const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
|
|
284
284
|
const startTaskRef = useRef(() => { });
|
|
285
|
+
// True while a task-pane task (single or run-all) is executing. Used to
|
|
286
|
+
// suppress the interactive plan-mode approval pane, which would otherwise
|
|
287
|
+
// stall an unattended task run waiting for the user to approve a plan.
|
|
288
|
+
const taskRunningRef = useRef(props.sessionStore?.taskRunning ?? false);
|
|
285
289
|
const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
|
|
286
290
|
const currentPixelFixRef = useRef(null);
|
|
287
291
|
const startPixelFixRef = useRef(() => { });
|
|
@@ -343,6 +347,8 @@ export function App(props) {
|
|
|
343
347
|
createSessionStats({ sessionId: props.sessionStore?.sessionId ?? props.sessionId }));
|
|
344
348
|
const [idealReviewEnabled, setIdealReviewEnabled] = useState(props.sessionStore?.idealReviewEnabled ?? props.idealReviewEnabled ?? true);
|
|
345
349
|
const idealReviewEnabledRef = useRef(idealReviewEnabled);
|
|
350
|
+
const [autoApprovePlansEnabled, setAutoApprovePlansEnabled] = useState(props.sessionStore?.autoApprovePlans ?? props.autoApprovePlans ?? true);
|
|
351
|
+
const autoApprovePlansEnabledRef = useRef(autoApprovePlansEnabled);
|
|
346
352
|
/** Last actual API-reported input token count (from turn_end). */
|
|
347
353
|
const lastActualTokensRef = useRef(0);
|
|
348
354
|
/** Timestamp (ms) when lastActualTokensRef was last updated by turn_end. */
|
|
@@ -368,11 +374,21 @@ export function App(props) {
|
|
|
368
374
|
*/
|
|
369
375
|
const triggerAutoSetupRef = useRef(async () => { });
|
|
370
376
|
const getId = () => `ui-${nextIdRef.current++}`;
|
|
377
|
+
// Single predicate for "no human is present to approve a plan": task-list
|
|
378
|
+
// runs (single + run-all, via taskRunningRef) and pixel-fix runs (single +
|
|
379
|
+
// run-all-pixel, via currentPixelFixRef). Every autonomous context routes
|
|
380
|
+
// through here so future ones only update one place.
|
|
381
|
+
const isUnattendedRun = () => taskRunningRef.current || currentPixelFixRef.current !== null;
|
|
371
382
|
useEffect(() => {
|
|
372
383
|
idealReviewEnabledRef.current = idealReviewEnabled;
|
|
373
384
|
if (props.sessionStore)
|
|
374
385
|
props.sessionStore.idealReviewEnabled = idealReviewEnabled;
|
|
375
386
|
}, [idealReviewEnabled, props.sessionStore]);
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
autoApprovePlansEnabledRef.current = autoApprovePlansEnabled;
|
|
389
|
+
if (props.sessionStore)
|
|
390
|
+
props.sessionStore.autoApprovePlans = autoApprovePlansEnabled;
|
|
391
|
+
}, [autoApprovePlansEnabled, props.sessionStore]);
|
|
376
392
|
const sessionStore = props.sessionStore;
|
|
377
393
|
const { planMode, goalMode, rebuildSystemPrompt, replaceSystemPrompt, setPlanModeAndPrompt, setGoalModeAndPrompt, } = useModeState({
|
|
378
394
|
initialPlanMode: props.sessionStore?.planMode ?? props.planModeRef?.current ?? false,
|
|
@@ -577,8 +593,25 @@ export function App(props) {
|
|
|
577
593
|
void props
|
|
578
594
|
.connectInitialMcpTools()
|
|
579
595
|
.then((mcpTools) => {
|
|
580
|
-
if (cancelled
|
|
596
|
+
if (cancelled)
|
|
581
597
|
return;
|
|
598
|
+
if (mcpTools.length === 0) {
|
|
599
|
+
// No MCP tools connected (e.g. kencode-search failed to boot). The
|
|
600
|
+
// system prompt was built without them, so the model is never told
|
|
601
|
+
// to call tools it doesn't have — but flag it so the user knows
|
|
602
|
+
// public-code research is degraded this session.
|
|
603
|
+
log("WARN", "mcp", "No MCP tools connected at startup");
|
|
604
|
+
setLiveItems((prev) => [
|
|
605
|
+
...prev,
|
|
606
|
+
{
|
|
607
|
+
kind: "info",
|
|
608
|
+
text: "Research tools (kencode-search) didn't connect — public-code search is unavailable this session.",
|
|
609
|
+
id: getId(),
|
|
610
|
+
},
|
|
611
|
+
]);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
log("INFO", "mcp", `MCP tools ready: ${mcpTools.length} tool(s)`);
|
|
582
615
|
setCurrentTools((prev) => {
|
|
583
616
|
const next = [...prev.filter((tool) => !tool.name.startsWith("mcp__")), ...mcpTools];
|
|
584
617
|
currentToolsRef.current = next;
|
|
@@ -772,6 +805,8 @@ export function App(props) {
|
|
|
772
805
|
tools: currentTools,
|
|
773
806
|
webSearch: props.webSearch,
|
|
774
807
|
maxTokens: props.maxTokens,
|
|
808
|
+
supportsImages: getModel(currentModel)?.supportsImages ?? true,
|
|
809
|
+
supportsVideo: getModel(currentModel)?.supportsVideo ?? false,
|
|
775
810
|
thinking: thinkingLevel,
|
|
776
811
|
apiKey: activeApiKey,
|
|
777
812
|
baseUrl: activeBaseUrl,
|
|
@@ -889,10 +924,32 @@ export function App(props) {
|
|
|
889
924
|
if (planStepsRef.current.length > 0) {
|
|
890
925
|
const completed = findCompletedMarkers(text);
|
|
891
926
|
if (completed.size > 0) {
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
927
|
+
const planPath = approvedPlanPathRef.current;
|
|
928
|
+
// The agent can rewrite/expand the approved plan mid-run, so the
|
|
929
|
+
// snapshot captured at approval goes stale (wrong total, and new
|
|
930
|
+
// [DONE:n] markers match nothing). Re-extract from the live plan
|
|
931
|
+
// file and re-base onto it before applying markers. The file read
|
|
932
|
+
// is async; apply markers inside the same async step to avoid a
|
|
933
|
+
// race with the snapshot. Fall back to the in-memory snapshot if
|
|
934
|
+
// there is no plan path or the read fails.
|
|
935
|
+
const applyMarkers = (base) => {
|
|
936
|
+
const updated = markStepsCompleted(base, completed);
|
|
937
|
+
if (updated !== planStepsRef.current) {
|
|
938
|
+
planStepsRef.current = updated;
|
|
939
|
+
setPlanSteps(updated);
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
if (planPath) {
|
|
943
|
+
void import("node:fs/promises")
|
|
944
|
+
.then(({ readFile }) => readFile(planPath, "utf-8"))
|
|
945
|
+
.then((planContent) => {
|
|
946
|
+
const fresh = extractPlanSteps(planContent);
|
|
947
|
+
applyMarkers(rebasePlanSteps(planStepsRef.current, fresh));
|
|
948
|
+
})
|
|
949
|
+
.catch(() => applyMarkers(planStepsRef.current));
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
applyMarkers(planStepsRef.current);
|
|
896
953
|
}
|
|
897
954
|
// Real progress happened — reset the stuck-guard so the next
|
|
898
955
|
// step gets its own fresh nudge budget.
|
|
@@ -1229,6 +1286,18 @@ export function App(props) {
|
|
|
1229
1286
|
log("INFO", "server_tool", `Server tool call: ${name}`, { id });
|
|
1230
1287
|
const startedAt = Date.now();
|
|
1231
1288
|
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
1289
|
+
// Feed the pinned LiveToolPanel so provider-side tools (Anthropic's
|
|
1290
|
+
// native web_search) appear in the same rolling window as client
|
|
1291
|
+
// tools. `input` carries the tool args (e.g. { query }) the row reads.
|
|
1292
|
+
setLiveToolFeed((prev) => [
|
|
1293
|
+
...prev,
|
|
1294
|
+
{
|
|
1295
|
+
id,
|
|
1296
|
+
name,
|
|
1297
|
+
args: (input ?? {}),
|
|
1298
|
+
status: "running",
|
|
1299
|
+
},
|
|
1300
|
+
].slice(-(LIVE_TOOL_PANEL_ROWS * 2)));
|
|
1232
1301
|
// Flush completed items (including assistant text) before adding server
|
|
1233
1302
|
// tool UI — same rationale as onToolStart.
|
|
1234
1303
|
setLiveItems((prev) => {
|
|
@@ -1243,6 +1312,11 @@ export function App(props) {
|
|
|
1243
1312
|
if (flushed.length > 0) {
|
|
1244
1313
|
queueFlush(flushed);
|
|
1245
1314
|
}
|
|
1315
|
+
// The pre-tool text was just pinned; the hook resets its streaming
|
|
1316
|
+
// buffer at this same boundary. Reset the progressive-flush offset
|
|
1317
|
+
// so post-tool text is measured from zero (stale flushedChars would
|
|
1318
|
+
// otherwise slice into the fresh, shorter buffer).
|
|
1319
|
+
streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
|
|
1246
1320
|
return [
|
|
1247
1321
|
...remaining,
|
|
1248
1322
|
{
|
|
@@ -1259,6 +1333,9 @@ export function App(props) {
|
|
|
1259
1333
|
}, [queueFlush]),
|
|
1260
1334
|
onServerToolResult: useCallback((toolUseId, resultType, data) => {
|
|
1261
1335
|
log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
|
|
1336
|
+
// Mark the panel entry done. Aborts never reach here (handled in
|
|
1337
|
+
// onAborted), so a result that arrives is always a normal completion.
|
|
1338
|
+
setLiveToolFeed((prev) => prev.map((entry) => entry.id === toolUseId ? { ...entry, status: "done" } : entry));
|
|
1262
1339
|
setLiveItems((prev) => {
|
|
1263
1340
|
let updated;
|
|
1264
1341
|
const startIdx = prev.findIndex((item) => item.kind === "server_tool_start" && item.serverToolCallId === toolUseId);
|
|
@@ -1324,11 +1401,16 @@ export function App(props) {
|
|
|
1324
1401
|
return remaining;
|
|
1325
1402
|
});
|
|
1326
1403
|
}, [queueFlush]),
|
|
1327
|
-
onDone: useCallback((durationMs, toolsUsed) => {
|
|
1404
|
+
onDone: useCallback((durationMs, toolsUsed, runStats) => {
|
|
1328
1405
|
log("INFO", "agent", `Agent done`, {
|
|
1329
1406
|
duration: `${durationMs}ms`,
|
|
1330
1407
|
toolsUsed: toolsUsed.join(",") || "none",
|
|
1331
1408
|
});
|
|
1409
|
+
// The task run (if any) is finished. Run-all re-arms this below when it
|
|
1410
|
+
// auto-starts the next task.
|
|
1411
|
+
taskRunningRef.current = false;
|
|
1412
|
+
if (props.sessionStore)
|
|
1413
|
+
props.sessionStore.taskRunning = false;
|
|
1332
1414
|
const doneDecision = getDoneFlushDecision({
|
|
1333
1415
|
planOverlayPending: planOverlayPendingRef.current,
|
|
1334
1416
|
});
|
|
@@ -1337,7 +1419,13 @@ export function App(props) {
|
|
|
1337
1419
|
// Still flush live transcript rows before the pane remounts; otherwise
|
|
1338
1420
|
// setup output remains in ephemeral liveItems and appears to vanish.
|
|
1339
1421
|
if (doneDecision.showDoneStatus) {
|
|
1340
|
-
setDoneStatus({
|
|
1422
|
+
setDoneStatus({
|
|
1423
|
+
durationMs,
|
|
1424
|
+
toolsUsed,
|
|
1425
|
+
verb: pickDurationVerb(toolsUsed),
|
|
1426
|
+
counts: runStats?.counts,
|
|
1427
|
+
tokens: runStats?.tokens,
|
|
1428
|
+
});
|
|
1341
1429
|
playNotificationSound();
|
|
1342
1430
|
}
|
|
1343
1431
|
// Keep the final assistant response mounted in the live frame after a
|
|
@@ -1465,10 +1553,14 @@ export function App(props) {
|
|
|
1465
1553
|
const imageCount = typeof content === "string"
|
|
1466
1554
|
? undefined
|
|
1467
1555
|
: content.filter((c) => c.type === "image").length || undefined;
|
|
1556
|
+
const videoCount = typeof content === "string"
|
|
1557
|
+
? undefined
|
|
1558
|
+
: content.filter((c) => c.type === "video").length || undefined;
|
|
1468
1559
|
const userItem = {
|
|
1469
1560
|
kind: "user",
|
|
1470
1561
|
text: displayText,
|
|
1471
1562
|
imageCount,
|
|
1563
|
+
videoCount,
|
|
1472
1564
|
id: getId(),
|
|
1473
1565
|
};
|
|
1474
1566
|
setLastUserMessage(displayText);
|
|
@@ -1505,6 +1597,13 @@ export function App(props) {
|
|
|
1505
1597
|
},
|
|
1506
1598
|
];
|
|
1507
1599
|
}, []),
|
|
1600
|
+
onRetry: useCallback(() => {
|
|
1601
|
+
// Roll back any pending progressive flushes from the aborted attempt.
|
|
1602
|
+
// Without this, a stall retry regenerates the preamble and the old
|
|
1603
|
+
// flushed paragraph + the new one both end up in terminal history.
|
|
1604
|
+
pendingHistoryFlushRef.current = pendingHistoryFlushRef.current.filter((item) => item.kind !== "assistant");
|
|
1605
|
+
streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
|
|
1606
|
+
}, []),
|
|
1508
1607
|
});
|
|
1509
1608
|
const continueGoalRun = useCallback((run) => {
|
|
1510
1609
|
const cwd = cwdRef.current;
|
|
@@ -1874,6 +1973,12 @@ export function App(props) {
|
|
|
1874
1973
|
if (sessionStore) {
|
|
1875
1974
|
sessionStore.pendingAction = undefined;
|
|
1876
1975
|
}
|
|
1976
|
+
// Re-arm the task-running flag after the remount: startTask sets it but the
|
|
1977
|
+
// resetUI() remount drops the plain ref, so without this an unattended task
|
|
1978
|
+
// run would look interactive (and stall on plan-mode approval).
|
|
1979
|
+
if (sessionStore?.taskRunning) {
|
|
1980
|
+
taskRunningRef.current = true;
|
|
1981
|
+
}
|
|
1877
1982
|
setDoneStatus(null);
|
|
1878
1983
|
if (action.planEvent) {
|
|
1879
1984
|
const ev = action.planEvent;
|
|
@@ -1891,6 +1996,9 @@ export function App(props) {
|
|
|
1891
1996
|
void agentLoop.run(action.prompt).catch((err) => {
|
|
1892
1997
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1893
1998
|
log("ERROR", "error", errMsg);
|
|
1999
|
+
if (agentLoop.isRunning) {
|
|
2000
|
+
agentLoop.reset();
|
|
2001
|
+
}
|
|
1894
2002
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
1895
2003
|
});
|
|
1896
2004
|
// Intentional one-shot: run once on mount, never re-fire on re-render.
|
|
@@ -1929,6 +2037,26 @@ export function App(props) {
|
|
|
1929
2037
|
]);
|
|
1930
2038
|
return;
|
|
1931
2039
|
}
|
|
2040
|
+
if (trimmed === "/autoplan-on" || trimmed === "/autoplan-off") {
|
|
2041
|
+
const next = trimmed === "/autoplan-on";
|
|
2042
|
+
setAutoApprovePlansEnabled(next);
|
|
2043
|
+
if (props.settingsFile) {
|
|
2044
|
+
const sm = new SettingsManager(props.settingsFile);
|
|
2045
|
+
await sm.load();
|
|
2046
|
+
await sm.set("autoApprovePlans", next);
|
|
2047
|
+
}
|
|
2048
|
+
setLiveItems((prev) => [
|
|
2049
|
+
...prev,
|
|
2050
|
+
{
|
|
2051
|
+
kind: "info",
|
|
2052
|
+
text: next
|
|
2053
|
+
? "Auto-approve plans enabled — unattended runs approve plans and keep going. Use /autoplan-off to disable."
|
|
2054
|
+
: "Auto-approve plans disabled — unattended runs decline plan mode instead. Use /autoplan-on to enable.",
|
|
2055
|
+
id: getId(),
|
|
2056
|
+
},
|
|
2057
|
+
]);
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
1932
2060
|
// /rewind — open the checkpoint picker (needs React state + the store).
|
|
1933
2061
|
if (trimmed === "/rewind") {
|
|
1934
2062
|
const store = props.checkpointStore;
|
|
@@ -2056,31 +2184,35 @@ export function App(props) {
|
|
|
2056
2184
|
}
|
|
2057
2185
|
// ── Build user content (shared by normal + queued paths) ──
|
|
2058
2186
|
const hasImages = inputImages.length > 0;
|
|
2187
|
+
const imageCount = inputImages.filter((img) => img.kind === "image").length;
|
|
2188
|
+
const videoCount = inputImages.filter((img) => img.kind === "video").length;
|
|
2059
2189
|
const modelInfo = getModel(currentModel);
|
|
2060
2190
|
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
2061
|
-
const
|
|
2191
|
+
const modelSupportsVideo = modelInfo?.supportsVideo ?? false;
|
|
2192
|
+
const userContent = buildUserContentWithAttachments(input, inputImages, modelSupportsImages, modelSupportsVideo);
|
|
2062
2193
|
// ── Queue message if agent is already running ──
|
|
2063
2194
|
if (agentLoop.isRunning) {
|
|
2064
2195
|
log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
|
|
2065
2196
|
agentLoop.queueMessage(userContent, input);
|
|
2066
2197
|
let displayText = input;
|
|
2067
2198
|
if (hasImages) {
|
|
2068
|
-
const { cleanText } = await
|
|
2199
|
+
const { cleanText } = await extractMediaPaths(input, props.cwd);
|
|
2069
2200
|
displayText = cleanText;
|
|
2070
2201
|
}
|
|
2071
2202
|
const queuedItem = {
|
|
2072
2203
|
kind: "queued",
|
|
2073
2204
|
text: displayText,
|
|
2074
|
-
imageCount:
|
|
2205
|
+
imageCount: imageCount > 0 ? imageCount : undefined,
|
|
2206
|
+
videoCount: videoCount > 0 ? videoCount : undefined,
|
|
2075
2207
|
id: getId(),
|
|
2076
2208
|
};
|
|
2077
2209
|
setLiveItems((prev) => [...prev, queuedItem]);
|
|
2078
2210
|
return;
|
|
2079
2211
|
}
|
|
2080
|
-
// Build display text — strip image paths, show badges instead
|
|
2212
|
+
// Build display text — strip image/video paths, show badges instead
|
|
2081
2213
|
let displayText = input;
|
|
2082
2214
|
if (hasImages) {
|
|
2083
|
-
const { cleanText } = await
|
|
2215
|
+
const { cleanText } = await extractMediaPaths(input, props.cwd);
|
|
2084
2216
|
displayText = cleanText;
|
|
2085
2217
|
}
|
|
2086
2218
|
let imagePreviews;
|
|
@@ -2096,7 +2228,8 @@ export function App(props) {
|
|
|
2096
2228
|
const userItem = {
|
|
2097
2229
|
kind: "user",
|
|
2098
2230
|
text: displayText,
|
|
2099
|
-
imageCount:
|
|
2231
|
+
imageCount: imageCount > 0 ? imageCount : undefined,
|
|
2232
|
+
videoCount: videoCount > 0 ? videoCount : undefined,
|
|
2100
2233
|
imagePreviews,
|
|
2101
2234
|
pasteInfo,
|
|
2102
2235
|
id: getId(),
|
|
@@ -2130,6 +2263,12 @@ export function App(props) {
|
|
|
2130
2263
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2131
2264
|
log("ERROR", "error", msg);
|
|
2132
2265
|
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2266
|
+
// If the agent loop threw but left isRunning in a stale true state
|
|
2267
|
+
// (can happen when the finally block hasn't been processed by React
|
|
2268
|
+
// yet), reset it so the user isn't deadlocked with a non-working UI.
|
|
2269
|
+
if (agentLoop.isRunning) {
|
|
2270
|
+
agentLoop.reset();
|
|
2271
|
+
}
|
|
2133
2272
|
setLiveItems((prev) => [
|
|
2134
2273
|
...prev,
|
|
2135
2274
|
isAbort
|
|
@@ -2220,8 +2359,14 @@ export function App(props) {
|
|
|
2220
2359
|
rebuildPromptWithTools(next);
|
|
2221
2360
|
return next;
|
|
2222
2361
|
});
|
|
2223
|
-
// Reconnect MCP servers
|
|
2224
|
-
|
|
2362
|
+
// Reconnect MCP servers ONLY when the resolved server set actually
|
|
2363
|
+
// changes. GLM is the only provider with a different set (Z.AI
|
|
2364
|
+
// servers), so a switch that doesn't involve GLM on either side
|
|
2365
|
+
// keeps the identical set — tearing down a live stdio child (e.g.
|
|
2366
|
+
// kencode-search) and re-spawning `npx` there only risks a failed
|
|
2367
|
+
// re-spawn that would silently drop the tools.
|
|
2368
|
+
const glmInvolved = newProvider === "glm" || prevProvider === "glm";
|
|
2369
|
+
if (props.mcpManager && glmInvolved) {
|
|
2225
2370
|
void (async () => {
|
|
2226
2371
|
// Disconnect old MCP servers
|
|
2227
2372
|
await props.mcpManager.dispose();
|
|
@@ -2240,7 +2385,11 @@ export function App(props) {
|
|
|
2240
2385
|
apiKey = props.credentialsByProvider?.["glm"]?.accessToken;
|
|
2241
2386
|
}
|
|
2242
2387
|
try {
|
|
2243
|
-
|
|
2388
|
+
// Use getAllMcpServers so user-configured servers (from
|
|
2389
|
+
// ~/.ezcoder/mcp.json and ./.gg/mcp.json) survive the reconnect —
|
|
2390
|
+
// getMCPServers returns provider defaults only.
|
|
2391
|
+
const servers = await getAllMcpServers(newProvider, apiKey, props.cwd);
|
|
2392
|
+
const mcpTools = await props.mcpManager.connectAll(servers);
|
|
2244
2393
|
setCurrentTools((prev) => {
|
|
2245
2394
|
const next = [...prev.filter((t) => !t.name.startsWith("mcp__")), ...mcpTools];
|
|
2246
2395
|
rebuildPromptWithTools(next);
|
|
@@ -2262,7 +2411,26 @@ export function App(props) {
|
|
|
2262
2411
|
}
|
|
2263
2412
|
return newProvider;
|
|
2264
2413
|
});
|
|
2265
|
-
|
|
2414
|
+
// The `read` tool's video capability (its description + native-video
|
|
2415
|
+
// execute path) is baked in at creation from the model's `maxVideoBytes`.
|
|
2416
|
+
// Switching to/from a video-capable model (e.g. text-only MiMo-V2.5-Pro →
|
|
2417
|
+
// omnimodal MiMo-V2.5) must rebuild it, or the tool keeps telling the model
|
|
2418
|
+
// it can't watch video. Rebuild reuses the read tracker, so read-before-edit
|
|
2419
|
+
// history survives. Provider-change rebuilds the prompt above; this covers
|
|
2420
|
+
// same-provider model switches too.
|
|
2421
|
+
setCurrentModel((prevModel) => {
|
|
2422
|
+
if (props.rebuildReadTool &&
|
|
2423
|
+
getVideoByteLimit(prevModel) !== getVideoByteLimit(newModelId)) {
|
|
2424
|
+
const newReadTool = props.rebuildReadTool(newModelId);
|
|
2425
|
+
setCurrentTools((prev) => {
|
|
2426
|
+
const next = prev.map((tool) => (tool.name === "read" ? newReadTool : tool));
|
|
2427
|
+
currentToolsRef.current = next;
|
|
2428
|
+
void replaceSystemPrompt({ tools: next });
|
|
2429
|
+
return next;
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
return newModelId;
|
|
2433
|
+
});
|
|
2266
2434
|
const modelInfo = getModel(newModelId);
|
|
2267
2435
|
const displayName = modelInfo?.name ?? newModelId;
|
|
2268
2436
|
setLiveItems((prev) => [
|
|
@@ -2277,7 +2445,14 @@ export function App(props) {
|
|
|
2277
2445
|
await sm.set("defaultModel", newModelId);
|
|
2278
2446
|
});
|
|
2279
2447
|
}
|
|
2280
|
-
}, [
|
|
2448
|
+
}, [
|
|
2449
|
+
props.settingsFile,
|
|
2450
|
+
props.mcpManager,
|
|
2451
|
+
props.credentialsByProvider,
|
|
2452
|
+
props.authStorage,
|
|
2453
|
+
props.rebuildReadTool,
|
|
2454
|
+
replaceSystemPrompt,
|
|
2455
|
+
]);
|
|
2281
2456
|
const handleThemeSelect = useCallback((name) => {
|
|
2282
2457
|
setOverlay(null);
|
|
2283
2458
|
if (switchTheme) {
|
|
@@ -2339,6 +2514,14 @@ export function App(props) {
|
|
|
2339
2514
|
: "Enable pre-final ideal review",
|
|
2340
2515
|
sectionTitle: "built-in",
|
|
2341
2516
|
},
|
|
2517
|
+
{
|
|
2518
|
+
name: autoApprovePlansEnabled ? "autoplan-off" : "autoplan-on",
|
|
2519
|
+
aliases: [],
|
|
2520
|
+
description: autoApprovePlansEnabled
|
|
2521
|
+
? "Disable auto-approving plans in unattended runs"
|
|
2522
|
+
: "Enable auto-approving plans in unattended runs",
|
|
2523
|
+
sectionTitle: "built-in",
|
|
2524
|
+
},
|
|
2342
2525
|
{
|
|
2343
2526
|
name: "rewind",
|
|
2344
2527
|
aliases: [],
|
|
@@ -2360,7 +2543,7 @@ export function App(props) {
|
|
|
2360
2543
|
sectionTitle: "built-in",
|
|
2361
2544
|
},
|
|
2362
2545
|
];
|
|
2363
|
-
}, [customCommands, idealReviewEnabled]);
|
|
2546
|
+
}, [customCommands, idealReviewEnabled, autoApprovePlansEnabled]);
|
|
2364
2547
|
const renderItem = (item, index, items) => renderTranscriptItem({
|
|
2365
2548
|
item,
|
|
2366
2549
|
index,
|
|
@@ -2446,6 +2629,9 @@ export function App(props) {
|
|
|
2446
2629
|
// onDone can invoke it from stale closures.
|
|
2447
2630
|
const startTask = useCallback((title, prompt, taskId) => {
|
|
2448
2631
|
const taskCwd = cwdRef.current;
|
|
2632
|
+
taskRunningRef.current = true;
|
|
2633
|
+
if (props.sessionStore)
|
|
2634
|
+
props.sessionStore.taskRunning = true;
|
|
2449
2635
|
const shortId = taskId.slice(0, 8);
|
|
2450
2636
|
const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
|
|
2451
2637
|
`tasks({ action: "done", id: "${shortId}" })`;
|
|
@@ -2497,6 +2683,9 @@ export function App(props) {
|
|
|
2497
2683
|
setDoneStatus(null);
|
|
2498
2684
|
setLiveItems([taskItem]);
|
|
2499
2685
|
void agentLoop.run(fullPrompt).catch((err) => {
|
|
2686
|
+
if (agentLoop.isRunning) {
|
|
2687
|
+
agentLoop.reset();
|
|
2688
|
+
}
|
|
2500
2689
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2501
2690
|
});
|
|
2502
2691
|
}, [agentLoop, currentModel, currentProvider, props]);
|
|
@@ -2545,7 +2734,23 @@ export function App(props) {
|
|
|
2545
2734
|
// every chunk boundary. Slicing by the prospective flush here keeps the live
|
|
2546
2735
|
// frame height monotonic, so the footer never bounces.
|
|
2547
2736
|
const alreadyFlushedChars = streamedAssistantFlushRef.current.flushedChars;
|
|
2548
|
-
|
|
2737
|
+
// Retry-safety gate: don't commit streamed paragraphs to permanent scrollback
|
|
2738
|
+
// while the text is still small enough to live entirely in the live region.
|
|
2739
|
+
// A silent stall-retry (agent-loop.ts) restarts the LLM call from scratch and
|
|
2740
|
+
// regenerates the opening text — reworded, so the byte-identical dedup can't
|
|
2741
|
+
// catch it. Anything already printed to scrollback can't be un-written, so the
|
|
2742
|
+
// regen appends as a second ⏺ bullet that paraphrases the first. Keeping short
|
|
2743
|
+
// streamed text live (it clears via setStreamingText("") on retry) closes that
|
|
2744
|
+
// hole. We only start flushing once the unflushed text would overflow the live
|
|
2745
|
+
// area — the original anti-jump purpose, which only matters for long responses
|
|
2746
|
+
// that are far less likely to be a stalled preamble. Once flushing has begun
|
|
2747
|
+
// for this turn (alreadyFlushedChars > 0) we keep flushing every boundary so
|
|
2748
|
+
// committed continuation paragraphs stay consistent with the live tail.
|
|
2749
|
+
const unflushedStreamingRows = rawVisibleStreamingText
|
|
2750
|
+
? estimateRenderedRows(rawVisibleStreamingText.slice(alreadyFlushedChars), columns)
|
|
2751
|
+
: 0;
|
|
2752
|
+
const shouldFlushStreamedText = alreadyFlushedChars > 0 || unflushedStreamingRows > measuredLiveAreaRows;
|
|
2753
|
+
const pendingFlushChars = rawVisibleStreamingText && shouldFlushStreamedText
|
|
2549
2754
|
? splitAssistantStreamingText(rawVisibleStreamingText.slice(alreadyFlushedChars)).flushedText
|
|
2550
2755
|
.length
|
|
2551
2756
|
: 0;
|
|
@@ -2556,6 +2761,13 @@ export function App(props) {
|
|
|
2556
2761
|
}
|
|
2557
2762
|
if (rawVisibleStreamingText === streamedAssistantFlushRef.current.text)
|
|
2558
2763
|
return;
|
|
2764
|
+
if (!shouldFlushStreamedText) {
|
|
2765
|
+
streamedAssistantFlushRef.current = {
|
|
2766
|
+
...streamedAssistantFlushRef.current,
|
|
2767
|
+
text: rawVisibleStreamingText,
|
|
2768
|
+
};
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2559
2771
|
const alreadyFlushed = streamedAssistantFlushRef.current.flushedChars;
|
|
2560
2772
|
const unflushedText = rawVisibleStreamingText.slice(alreadyFlushed);
|
|
2561
2773
|
const split = splitAssistantStreamingText(unflushedText);
|
|
@@ -2578,7 +2790,7 @@ export function App(props) {
|
|
|
2578
2790
|
...streamedAssistantFlushRef.current,
|
|
2579
2791
|
text: rawVisibleStreamingText,
|
|
2580
2792
|
};
|
|
2581
|
-
}, [rawVisibleStreamingText, queueFlush]);
|
|
2793
|
+
}, [rawVisibleStreamingText, shouldFlushStreamedText, queueFlush]);
|
|
2582
2794
|
const visibleStreamingText = stripDoneMarkers(rawVisibleStreamingText.slice(alreadyFlushedChars + pendingFlushChars));
|
|
2583
2795
|
const lastLiveItem = liveItems.at(-1);
|
|
2584
2796
|
// For spacing decisions, the previous row is the last item that actually
|
|
@@ -2731,13 +2943,48 @@ export function App(props) {
|
|
|
2731
2943
|
setOverlay(null);
|
|
2732
2944
|
};
|
|
2733
2945
|
const handleEnterPlanMode = useCallback(async (reason) => {
|
|
2946
|
+
// During an unattended run with auto-approve disabled, plan mode would open
|
|
2947
|
+
// an approval pane and stall the loop waiting on a human. Decline plan mode
|
|
2948
|
+
// so the agent implements directly. When auto-approve is enabled we DO allow
|
|
2949
|
+
// plan mode — handleExitPlanMode auto-approves the plan in place and the run
|
|
2950
|
+
// keeps going, preserving the explore→plan→implement process.
|
|
2951
|
+
if (isUnattendedRun() && !autoApprovePlansEnabledRef.current)
|
|
2952
|
+
return false;
|
|
2734
2953
|
await setPlanModeAndPrompt(true);
|
|
2735
2954
|
setLiveItems((prev) => [
|
|
2736
2955
|
...prev,
|
|
2737
2956
|
{ kind: "plan_transition", text: reason ?? "", id: getId(), active: true },
|
|
2738
2957
|
]);
|
|
2958
|
+
return true;
|
|
2739
2959
|
}, [setPlanModeAndPrompt]);
|
|
2740
|
-
const handleExitPlanMode = useCallback(async (
|
|
2960
|
+
const handleExitPlanMode = useCallback(async (planPath) => {
|
|
2961
|
+
// Unattended run + auto-approve enabled: approve the plan IN PLACE (no
|
|
2962
|
+
// remount, no message wipe) so the appended task-completion hint survives
|
|
2963
|
+
// and run-all can advance. Swap the system prompt from "Plan Mode (ACTIVE)"
|
|
2964
|
+
// to the "Approved Plan" + step-tracking section, surface an auditable
|
|
2965
|
+
// transcript row, and tell the model to implement now.
|
|
2966
|
+
if (isUnattendedRun() && autoApprovePlansEnabledRef.current) {
|
|
2967
|
+
log("INFO", "plan", "Plan auto-approved during unattended run", { planPath });
|
|
2968
|
+
let steps = [];
|
|
2969
|
+
try {
|
|
2970
|
+
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
2971
|
+
steps = extractPlanSteps(planContent);
|
|
2972
|
+
}
|
|
2973
|
+
catch {
|
|
2974
|
+
// Plan with no readable steps still proceeds (just no step widget).
|
|
2975
|
+
}
|
|
2976
|
+
approvedPlanPathRef.current = planPath;
|
|
2977
|
+
planStepsRef.current = steps;
|
|
2978
|
+
setPlanSteps(steps);
|
|
2979
|
+
await setPlanModeAndPrompt(false);
|
|
2980
|
+
await replaceSystemPrompt({ approvedPlanPath: planPath, planMode: false });
|
|
2981
|
+
setLiveItems((prev) => [
|
|
2982
|
+
...prev,
|
|
2983
|
+
{ kind: "plan_event", event: "auto-approved", detail: planPath, id: getId() },
|
|
2984
|
+
]);
|
|
2985
|
+
return (`Plan auto-approved (unattended run). The approved plan is at ${planPath}. ` +
|
|
2986
|
+
`Implement it now, following each step in order.`);
|
|
2987
|
+
}
|
|
2741
2988
|
await setPlanModeAndPrompt(false);
|
|
2742
2989
|
planOverlayPendingRef.current = true;
|
|
2743
2990
|
setPlanAutoExpand(true);
|
|
@@ -2747,13 +2994,38 @@ export function App(props) {
|
|
|
2747
2994
|
}
|
|
2748
2995
|
setOverlay("plan");
|
|
2749
2996
|
return "Plan submitted for user review. Wait for the user to approve, reject, or dismiss it before implementing.";
|
|
2750
|
-
}, [props.sessionStore, setPlanModeAndPrompt]);
|
|
2997
|
+
}, [props.sessionStore, setPlanModeAndPrompt, replaceSystemPrompt]);
|
|
2751
2998
|
useEffect(() => {
|
|
2752
2999
|
if (!props.planCallbacks)
|
|
2753
3000
|
return;
|
|
2754
3001
|
props.planCallbacks.onEnterPlan = handleEnterPlanMode;
|
|
2755
3002
|
props.planCallbacks.onExitPlan = handleExitPlanMode;
|
|
2756
3003
|
}, [handleEnterPlanMode, handleExitPlanMode, props.planCallbacks]);
|
|
3004
|
+
// Toggle auto-approve-plans (Ctrl+O / /autoplan-on|off). Persists the setting
|
|
3005
|
+
// so it survives restarts and pushes a transient info row for feedback.
|
|
3006
|
+
const handleToggleAutoApprove = useCallback(() => {
|
|
3007
|
+
const next = !autoApprovePlansEnabledRef.current;
|
|
3008
|
+
setAutoApprovePlansEnabled(next);
|
|
3009
|
+
if (props.settingsFile) {
|
|
3010
|
+
const sm = new SettingsManager(props.settingsFile);
|
|
3011
|
+
void sm
|
|
3012
|
+
.load()
|
|
3013
|
+
.then(() => sm.set("autoApprovePlans", next))
|
|
3014
|
+
.catch(() => {
|
|
3015
|
+
/* persistence is best-effort */
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
setLiveItems((prev) => [
|
|
3019
|
+
...prev,
|
|
3020
|
+
{
|
|
3021
|
+
kind: "info",
|
|
3022
|
+
text: next
|
|
3023
|
+
? "Auto-approve plans enabled — unattended runs approve plans and keep going. Use /autoplan-off to disable."
|
|
3024
|
+
: "Auto-approve plans disabled — unattended runs decline plan mode instead. Use /autoplan-on to enable.",
|
|
3025
|
+
id: getId(),
|
|
3026
|
+
},
|
|
3027
|
+
]);
|
|
3028
|
+
}, [props.settingsFile]);
|
|
2757
3029
|
const handleCloseGoalOverlay = () => {
|
|
2758
3030
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2759
3031
|
props.sessionStore.overlay = null;
|
|
@@ -3018,6 +3290,9 @@ export function App(props) {
|
|
|
3018
3290
|
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
3019
3291
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3020
3292
|
log("ERROR", "error", errMsg);
|
|
3293
|
+
if (agentLoop.isRunning) {
|
|
3294
|
+
agentLoop.reset();
|
|
3295
|
+
}
|
|
3021
3296
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3022
3297
|
});
|
|
3023
3298
|
};
|
|
@@ -3046,6 +3321,7 @@ export function App(props) {
|
|
|
3046
3321
|
onTogglePixel: () => openOverlay("pixel"),
|
|
3047
3322
|
onToggleGoal: () => openOverlay("goal"),
|
|
3048
3323
|
onToggleMarkdown: () => setRenderMarkdown((prev) => !prev),
|
|
3324
|
+
onToggleAutoApprove: handleToggleAutoApprove,
|
|
3049
3325
|
cwd: props.cwd,
|
|
3050
3326
|
commands: allCommands,
|
|
3051
3327
|
mouseScroll: props.fullscreen,
|