@prestyj/cli 4.4.0 → 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.
Files changed (64) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +9 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +5 -2
  7. package/dist/config.js.map +1 -1
  8. package/dist/config.test.js +10 -0
  9. package/dist/config.test.js.map +1 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +8 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/goal-worker.js +1 -1
  14. package/dist/core/goal-worker.js.map +1 -1
  15. package/dist/core/goal-worker.test.js +1 -1
  16. package/dist/core/goal-worker.test.js.map +1 -1
  17. package/dist/core/mcp/defaults.d.ts.map +1 -1
  18. package/dist/core/mcp/defaults.js +39 -3
  19. package/dist/core/mcp/defaults.js.map +1 -1
  20. package/dist/core/prompt-commands.d.ts.map +1 -1
  21. package/dist/core/prompt-commands.js +3 -1
  22. package/dist/core/prompt-commands.js.map +1 -1
  23. package/dist/core/settings-manager.d.ts +1 -0
  24. package/dist/core/settings-manager.d.ts.map +1 -1
  25. package/dist/core/settings-manager.js +2 -0
  26. package/dist/core/settings-manager.js.map +1 -1
  27. package/dist/system-prompt.d.ts.map +1 -1
  28. package/dist/system-prompt.js +18 -6
  29. package/dist/system-prompt.js.map +1 -1
  30. package/dist/system-prompt.test.js +24 -0
  31. package/dist/system-prompt.test.js.map +1 -1
  32. package/dist/tools/enter-plan.d.ts +1 -1
  33. package/dist/tools/enter-plan.d.ts.map +1 -1
  34. package/dist/tools/enter-plan.js +8 -1
  35. package/dist/tools/enter-plan.js.map +1 -1
  36. package/dist/tools/index.d.ts +6 -2
  37. package/dist/tools/index.d.ts.map +1 -1
  38. package/dist/tools/index.js.map +1 -1
  39. package/dist/ui/App.d.ts +4 -1
  40. package/dist/ui/App.d.ts.map +1 -1
  41. package/dist/ui/App.js +140 -4
  42. package/dist/ui/App.js.map +1 -1
  43. package/dist/ui/app-items.d.ts +1 -1
  44. package/dist/ui/app-items.d.ts.map +1 -1
  45. package/dist/ui/components/ChatScreen.d.ts +1 -0
  46. package/dist/ui/components/ChatScreen.d.ts.map +1 -1
  47. package/dist/ui/components/ChatScreen.js +1 -1
  48. package/dist/ui/components/ChatScreen.js.map +1 -1
  49. package/dist/ui/components/InputArea.d.ts +2 -1
  50. package/dist/ui/components/InputArea.d.ts.map +1 -1
  51. package/dist/ui/components/InputArea.js +7 -1
  52. package/dist/ui/components/InputArea.js.map +1 -1
  53. package/dist/ui/render.d.ts +9 -1
  54. package/dist/ui/render.d.ts.map +1 -1
  55. package/dist/ui/render.js +2 -0
  56. package/dist/ui/render.js.map +1 -1
  57. package/dist/ui/terminal-history-status-renderers.d.ts +1 -1
  58. package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -1
  59. package/dist/ui/terminal-history-status-renderers.js +1 -0
  60. package/dist/ui/terminal-history-status-renderers.js.map +1 -1
  61. package/dist/ui/transcript/presentation.d.ts.map +1 -1
  62. package/dist/ui/transcript/presentation.js +1 -0
  63. package/dist/ui/transcript/presentation.js.map +1 -1
  64. package/package.json +4 -3
package/dist/ui/App.js CHANGED
@@ -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 || mcpTools.length === 0)
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;
@@ -1373,6 +1406,11 @@ export function App(props) {
1373
1406
  duration: `${durationMs}ms`,
1374
1407
  toolsUsed: toolsUsed.join(",") || "none",
1375
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;
1376
1414
  const doneDecision = getDoneFlushDecision({
1377
1415
  planOverlayPending: planOverlayPendingRef.current,
1378
1416
  });
@@ -1935,6 +1973,12 @@ export function App(props) {
1935
1973
  if (sessionStore) {
1936
1974
  sessionStore.pendingAction = undefined;
1937
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
+ }
1938
1982
  setDoneStatus(null);
1939
1983
  if (action.planEvent) {
1940
1984
  const ev = action.planEvent;
@@ -1993,6 +2037,26 @@ export function App(props) {
1993
2037
  ]);
1994
2038
  return;
1995
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
+ }
1996
2060
  // /rewind — open the checkpoint picker (needs React state + the store).
1997
2061
  if (trimmed === "/rewind") {
1998
2062
  const store = props.checkpointStore;
@@ -2450,6 +2514,14 @@ export function App(props) {
2450
2514
  : "Enable pre-final ideal review",
2451
2515
  sectionTitle: "built-in",
2452
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
+ },
2453
2525
  {
2454
2526
  name: "rewind",
2455
2527
  aliases: [],
@@ -2471,7 +2543,7 @@ export function App(props) {
2471
2543
  sectionTitle: "built-in",
2472
2544
  },
2473
2545
  ];
2474
- }, [customCommands, idealReviewEnabled]);
2546
+ }, [customCommands, idealReviewEnabled, autoApprovePlansEnabled]);
2475
2547
  const renderItem = (item, index, items) => renderTranscriptItem({
2476
2548
  item,
2477
2549
  index,
@@ -2557,6 +2629,9 @@ export function App(props) {
2557
2629
  // onDone can invoke it from stale closures.
2558
2630
  const startTask = useCallback((title, prompt, taskId) => {
2559
2631
  const taskCwd = cwdRef.current;
2632
+ taskRunningRef.current = true;
2633
+ if (props.sessionStore)
2634
+ props.sessionStore.taskRunning = true;
2560
2635
  const shortId = taskId.slice(0, 8);
2561
2636
  const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
2562
2637
  `tasks({ action: "done", id: "${shortId}" })`;
@@ -2868,13 +2943,48 @@ export function App(props) {
2868
2943
  setOverlay(null);
2869
2944
  };
2870
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;
2871
2953
  await setPlanModeAndPrompt(true);
2872
2954
  setLiveItems((prev) => [
2873
2955
  ...prev,
2874
2956
  { kind: "plan_transition", text: reason ?? "", id: getId(), active: true },
2875
2957
  ]);
2958
+ return true;
2876
2959
  }, [setPlanModeAndPrompt]);
2877
- const handleExitPlanMode = useCallback(async (_planPath) => {
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
+ }
2878
2988
  await setPlanModeAndPrompt(false);
2879
2989
  planOverlayPendingRef.current = true;
2880
2990
  setPlanAutoExpand(true);
@@ -2884,13 +2994,38 @@ export function App(props) {
2884
2994
  }
2885
2995
  setOverlay("plan");
2886
2996
  return "Plan submitted for user review. Wait for the user to approve, reject, or dismiss it before implementing.";
2887
- }, [props.sessionStore, setPlanModeAndPrompt]);
2997
+ }, [props.sessionStore, setPlanModeAndPrompt, replaceSystemPrompt]);
2888
2998
  useEffect(() => {
2889
2999
  if (!props.planCallbacks)
2890
3000
  return;
2891
3001
  props.planCallbacks.onEnterPlan = handleEnterPlanMode;
2892
3002
  props.planCallbacks.onExitPlan = handleExitPlanMode;
2893
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]);
2894
3029
  const handleCloseGoalOverlay = () => {
2895
3030
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2896
3031
  props.sessionStore.overlay = null;
@@ -3186,6 +3321,7 @@ export function App(props) {
3186
3321
  onTogglePixel: () => openOverlay("pixel"),
3187
3322
  onToggleGoal: () => openOverlay("goal"),
3188
3323
  onToggleMarkdown: () => setRenderMarkdown((prev) => !prev),
3324
+ onToggleAutoApprove: handleToggleAutoApprove,
3189
3325
  cwd: props.cwd,
3190
3326
  commands: allCommands,
3191
3327
  mouseScroll: props.fullscreen,