@promptbook/cli 0.114.0-19 → 0.114.0-20

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 (59) hide show
  1. package/README.md +5 -5
  2. package/esm/index.es.js +1586 -1213
  3. package/esm/index.es.js.map +1 -1
  4. package/esm/scripts/run-codex-prompts/common/CoderRunControlFeedback.d.ts +54 -0
  5. package/esm/scripts/run-codex-prompts/common/applyCoderRunControlKey.d.ts +14 -0
  6. package/esm/scripts/run-codex-prompts/common/buildCoderRunControlKeyBadge.d.ts +7 -0
  7. package/esm/scripts/run-codex-prompts/common/formatCoderRunControlFeedback.d.ts +13 -0
  8. package/esm/scripts/run-codex-prompts/common/listenForCoderRunControls.d.ts +17 -0
  9. package/esm/scripts/run-codex-prompts/common/waitForPause.d.ts +0 -8
  10. package/esm/scripts/run-codex-prompts/common/waitForSkippableWorldTimeDeadline.d.ts +15 -0
  11. package/esm/scripts/run-codex-prompts/prompts/buildPromptLabel.d.ts +6 -0
  12. package/esm/scripts/run-codex-prompts/prompts/buildPromptStatusDetails.d.ts +7 -0
  13. package/esm/scripts/run-codex-prompts/prompts/listPromptsByStatus.d.ts +7 -0
  14. package/esm/scripts/run-codex-prompts/prompts/markPromptDone.d.ts +15 -4
  15. package/esm/scripts/run-codex-prompts/prompts/markPromptFailed.d.ts +34 -1
  16. package/esm/scripts/run-codex-prompts/prompts/promptRunnerAttribution.d.ts +24 -0
  17. package/esm/scripts/run-codex-prompts/prompts/resolveInterruptedPrompt.d.ts +9 -0
  18. package/esm/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +15 -1
  19. package/esm/scripts/run-codex-prompts/ui/buildCoderRunAgentVisual.d.ts +1 -1
  20. package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +6 -1
  21. package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +11 -0
  22. package/esm/src/cli/cli-commands/coder/GitChangesMode.d.ts +28 -0
  23. package/esm/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +9 -2
  24. package/esm/src/version.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/src/cli/cli-commands/agents-server/startAgentsServer/createLocalAgentRunOptions.ts +1 -1
  27. package/src/cli/cli-commands/coder/GitChangesMode.ts +35 -0
  28. package/src/cli/cli-commands/coder/run.ts +3 -1
  29. package/src/cli/cli-commands/coder/server.ts +1 -1
  30. package/src/cli/cli-commands/common/promptRunnerCliOptions.ts +22 -4
  31. package/src/other/templates/getTemplatesPipelineCollection.ts +778 -706
  32. package/src/utils/agents/terminalAgentAvatarVisual.ts +1 -1
  33. package/src/version.ts +2 -2
  34. package/src/versions.txt +1 -0
  35. package/umd/index.umd.js +1586 -1213
  36. package/umd/index.umd.js.map +1 -1
  37. package/umd/scripts/run-codex-prompts/common/CoderRunControlFeedback.d.ts +54 -0
  38. package/umd/scripts/run-codex-prompts/common/applyCoderRunControlKey.d.ts +14 -0
  39. package/umd/scripts/run-codex-prompts/common/buildCoderRunControlKeyBadge.d.ts +7 -0
  40. package/umd/scripts/run-codex-prompts/common/formatCoderRunControlFeedback.d.ts +13 -0
  41. package/umd/scripts/run-codex-prompts/common/listenForCoderRunControls.d.ts +17 -0
  42. package/umd/scripts/run-codex-prompts/common/waitForPause.d.ts +0 -8
  43. package/umd/scripts/run-codex-prompts/common/waitForSkippableWorldTimeDeadline.d.ts +15 -0
  44. package/umd/scripts/run-codex-prompts/prompts/buildPromptLabel.d.ts +6 -0
  45. package/umd/scripts/run-codex-prompts/prompts/buildPromptStatusDetails.d.ts +7 -0
  46. package/umd/scripts/run-codex-prompts/prompts/listPromptsByStatus.d.ts +7 -0
  47. package/umd/scripts/run-codex-prompts/prompts/markPromptDone.d.ts +15 -4
  48. package/umd/scripts/run-codex-prompts/prompts/markPromptFailed.d.ts +34 -1
  49. package/umd/scripts/run-codex-prompts/prompts/promptRunnerAttribution.d.ts +24 -0
  50. package/umd/scripts/run-codex-prompts/prompts/resolveInterruptedPrompt.d.ts +9 -0
  51. package/umd/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +15 -1
  52. package/umd/scripts/run-codex-prompts/ui/buildCoderRunAgentVisual.d.ts +1 -1
  53. package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +6 -1
  54. package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +11 -0
  55. package/umd/src/cli/cli-commands/coder/GitChangesMode.d.ts +28 -0
  56. package/umd/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +9 -2
  57. package/umd/src/version.d.ts +1 -1
  58. package/esm/scripts/run-codex-prompts/ui/buildCoderRunOctopusVisual.d.ts +0 -13
  59. package/umd/scripts/run-codex-prompts/ui/buildCoderRunOctopusVisual.d.ts +0 -13
package/esm/index.es.js CHANGED
@@ -48,7 +48,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
48
48
  * @generated
49
49
  * @see https://github.com/webgptorg/promptbook
50
50
  */
51
- const PROMPTBOOK_ENGINE_VERSION = '0.114.0-19';
51
+ const PROMPTBOOK_ENGINE_VERSION = '0.114.0-20';
52
52
  /**
53
53
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
54
54
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -1750,6 +1750,32 @@ const PTBK_MODEL_ENV = 'PTBK_MODEL';
1750
1750
  */
1751
1751
  const PTBK_THINKING_LEVEL_ENV = 'PTBK_THINKING_LEVEL';
1752
1752
 
1753
+ /**
1754
+ * Supported values of the `--git-changes` option, which decides what happens with a dirty working tree.
1755
+ *
1756
+ * - `fail` — refuse to start while the working tree has uncommitted changes
1757
+ * - `ignore` — start anyway and leave the uncommitted changes where they are
1758
+ * - `continue` — resume the single prompt which was left in the middle of its implementation
1759
+ *
1760
+ * @private internal shared utility of `ptbk coder run`
1761
+ */
1762
+ const GIT_CHANGES_MODE_VALUES = ['fail', 'ignore', 'continue'];
1763
+ /**
1764
+ * Mode applied when `--git-changes` is not used at all.
1765
+ *
1766
+ * @private internal shared utility of `ptbk coder run`
1767
+ */
1768
+ const DEFAULT_GIT_CHANGES_MODE = 'fail';
1769
+ /**
1770
+ * Checks whether a value is a supported `--git-changes` mode.
1771
+ *
1772
+ * @private internal shared utility of `ptbk coder run`
1773
+ */
1774
+ function isGitChangesMode(value) {
1775
+ return GIT_CHANGES_MODE_VALUES.includes(value);
1776
+ }
1777
+ // Note: [🟡] Code for CLI command [run](src/cli/cli-commands/coder/run.ts) should never be published outside of `@promptbook/cli`
1778
+
1753
1779
  /**
1754
1780
  * Supported thinking-level CLI values for coding-agent runners with configurable reasoning effort.
1755
1781
  *
@@ -1809,6 +1835,17 @@ const PROMPT_RUNNER_MODEL_OPTION_DESCRIPTION = spaceTrim$1(`
1809
1835
  OpenAI examples: gpt-5.2-codex, default
1810
1836
  Gemini examples: gemini-3-flash-preview, default
1811
1837
  `);
1838
+ /**
1839
+ * Commander description for the `--git-changes` option.
1840
+ *
1841
+ * @private internal utility of `promptbookCli`
1842
+ */
1843
+ const GIT_CHANGES_OPTION_DESCRIPTION = spaceTrim$1(`
1844
+ Decide what happens when the working tree has uncommitted changes before a prompt starts:
1845
+ - fail: refuse to start and ask for a commit or a stash (the default)
1846
+ - ignore: start anyway and leave the uncommitted changes where they are
1847
+ - continue: resume the single prompt which was left in the middle of its implementation
1848
+ `);
1812
1849
  /**
1813
1850
  * Registers runner selection flags on a command.
1814
1851
  *
@@ -1838,7 +1875,9 @@ function addPromptRunnerRuntimeOptions(command) {
1838
1875
  function addPromptRunnerExecutionOptions(command) {
1839
1876
  addPromptRunnerRuntimeOptions(command);
1840
1877
  command.option('--no-commit', 'Leave successful changes in the working directory instead of creating git commits');
1841
- command.option('--ignore-git-changes', 'Skip clean working tree check before running prompts', false);
1878
+ command.addOption(new Option('--git-changes <mode>', GIT_CHANGES_OPTION_DESCRIPTION)
1879
+ .choices([...GIT_CHANGES_MODE_VALUES])
1880
+ .default(DEFAULT_GIT_CHANGES_MODE));
1842
1881
  command.option('--no-normalize-line-endings', 'Disable automatic LF normalization for files changed in each coding round');
1843
1882
  command.option('--auto-push', 'Automatically git push after each commit', false);
1844
1883
  command.option('--auto-pull', 'Automatically git pull before the first and each subsequent prompt', false);
@@ -1849,11 +1888,12 @@ function addPromptRunnerExecutionOptions(command) {
1849
1888
  * @private internal utility of `promptbookCli`
1850
1889
  */
1851
1890
  function normalizePromptRunnerCliOptions(cliOptions, options) {
1891
+ var _a;
1852
1892
  const selectionOptions = normalizePromptRunnerSelectionCliOptions(cliOptions, options);
1853
1893
  return {
1854
1894
  ...selectionOptions,
1855
1895
  noCommit: !cliOptions.commit,
1856
- ignoreGitChanges: cliOptions.ignoreGitChanges,
1896
+ gitChanges: (_a = cliOptions.gitChanges) !== null && _a !== void 0 ? _a : DEFAULT_GIT_CHANGES_MODE,
1857
1897
  normalizeLineEndings: cliOptions.normalizeLineEndings,
1858
1898
  autoPush: cliOptions.autoPush,
1859
1899
  autoPull: cliOptions.autoPull,
@@ -22752,7 +22792,7 @@ async function ensureWorkingTreeCleanForAgentQueue(projectPath) {
22752
22792
  Git working tree has changes outside of \`${normalizeGitPath(AGENT_QUEUED_MESSAGES_DIRECTORY_PATH)}/\`.
22753
22793
 
22754
22794
  Please commit or stash unrelated changes before running \`ptbk agent-folder\`
22755
- OR run with \`--ignore-git-changes\`.
22795
+ OR run with \`--git-changes ignore\`.
22756
22796
 
22757
22797
  Unrelated changed files:
22758
22798
  ${unrelatedChangedFiles.map((relativePath) => `- \`${relativePath}\``).join('\n')}
@@ -22808,7 +22848,7 @@ async function pullLatestChangesForAgentQueueIfEnabled(options) {
22808
22848
  * Runs the clean working tree guard unless the user explicitly disabled it.
22809
22849
  */
22810
22850
  async function ensureCleanQueueIfNeeded(projectPath, options) {
22811
- if (options.ignoreGitChanges) {
22851
+ if (options.gitChanges !== 'fail') {
22812
22852
  return;
22813
22853
  }
22814
22854
  await ensureWorkingTreeCleanForAgentQueue(projectPath);
@@ -23330,6 +23370,58 @@ function buildAgentMessageScriptPath(projectPath, messageFile) {
23330
23370
  */
23331
23371
  const WAITING_FOR_MESSAGE_LABEL = 'Waiting for the message';
23332
23372
 
23373
+ /**
23374
+ * Colors of the key badges, so one control key looks the same in its pill and in its answer.
23375
+ *
23376
+ * @private internal constant of `ptbk coder` terminal controls
23377
+ */
23378
+ const CODER_RUN_CONTROL_KEY_BADGE_COLORIZERS = {
23379
+ P: (badgeText) => colors.bgYellow.black(badgeText),
23380
+ S: (badgeText) => colors.bgCyan.black(badgeText),
23381
+ X: (badgeText) => colors.bgBlue.white(badgeText),
23382
+ };
23383
+ /**
23384
+ * Builds the colored badge of one control key, for example ` S `.
23385
+ *
23386
+ * @private internal utility of `ptbk coder` terminal controls
23387
+ */
23388
+ function buildCoderRunControlKeyBadge(controlKey) {
23389
+ return CODER_RUN_CONTROL_KEY_BADGE_COLORIZERS[controlKey](` ${controlKey} `);
23390
+ }
23391
+
23392
+ /**
23393
+ * Marker printed before every control answer so it stands out from the runner output.
23394
+ *
23395
+ * @private internal constant of `ptbk coder` terminal controls
23396
+ */
23397
+ const CODER_RUN_CONTROL_FEEDBACK_MARKER = '»';
23398
+ /**
23399
+ * Colors of the control answers by their meaning.
23400
+ *
23401
+ * @private internal constant of `ptbk coder` terminal controls
23402
+ */
23403
+ const CODER_RUN_CONTROL_FEEDBACK_COLORIZERS = {
23404
+ success: (message) => colors.green(message),
23405
+ info: (message) => colors.cyan(message),
23406
+ warning: (message) => colors.yellow(message),
23407
+ };
23408
+ /**
23409
+ * Formats one control answer as a single colored terminal line.
23410
+ *
23411
+ * The same line is printed by the plain console listener and rendered inside the `Controls` box of the
23412
+ * rich terminal UI, so both modes answer a pressed control key identically.
23413
+ *
23414
+ * @param feedback Answer of the runner to the pressed control key.
23415
+ * @param repeatCount How many times this exact answer was given in a row, rendered from the second time on.
23416
+ *
23417
+ * @private internal utility of `ptbk coder` terminal controls
23418
+ */
23419
+ function formatCoderRunControlFeedback(feedback, repeatCount = 1) {
23420
+ const colorizeMessage = CODER_RUN_CONTROL_FEEDBACK_COLORIZERS[feedback.tone];
23421
+ const repeatSuffix = repeatCount > 1 ? colors.gray(` (×${repeatCount})`) : '';
23422
+ return `${colors.gray(CODER_RUN_CONTROL_FEEDBACK_MARKER)} ${buildCoderRunControlKeyBadge(feedback.controlKey)} ${colorizeMessage(feedback.message)}${repeatSuffix}`;
23423
+ }
23424
+
23333
23425
  /**
23334
23426
  * Centers an ANSI-colored line within the available frame width.
23335
23427
  *
@@ -23504,7 +23596,8 @@ function buildPausePresentation(phase, pauseState, pauseTargetLabel, statusMessa
23504
23596
  return {
23505
23597
  badge: buildRunningPhaseBadge(phase),
23506
23598
  stateMessage: statusMessage,
23507
- pauseControl: colors.bgYellow.black(' P ') + colors.white(' Pause'),
23599
+ // Note: The two paused variants above deliberately recolor the `P` badge, everything else uses the shared one
23600
+ pauseControl: buildCoderRunControlKeyBadge('P') + colors.white(' Pause'),
23508
23601
  };
23509
23602
  }
23510
23603
  /**
@@ -23545,20 +23638,34 @@ function buildCoderRunControlPills(options) {
23545
23638
  ],
23546
23639
  });
23547
23640
  }
23641
+ /**
23642
+ * Builds the body of the `Controls` box.
23643
+ *
23644
+ * The pills are always shown, and right after a control key was pressed the answer of the runner to
23645
+ * that key is shown underneath, so no key press ever leaves the frame unchanged.
23646
+ */
23647
+ function buildControlsBoxLines(options) {
23648
+ const { controlPills, controlFeedback } = options;
23649
+ const controlPillsLine = controlPills.join(' ');
23650
+ if (controlFeedback === undefined) {
23651
+ return [controlPillsLine];
23652
+ }
23653
+ return [controlPillsLine, formatCoderRunControlFeedback(controlFeedback, controlFeedback.repeatCount)];
23654
+ }
23548
23655
  /**
23549
23656
  * Builds the `S` wait-skip control label.
23550
23657
  */
23551
23658
  function buildSkipCurrentWaitControl() {
23552
- return colors.bgCyan.black(' S ') + colors.white(' Skip current waiting');
23659
+ return buildCoderRunControlKeyBadge('S') + colors.white(' Skip current waiting');
23553
23660
  }
23554
23661
  /**
23555
23662
  * Builds the dynamic `X` control label.
23556
23663
  */
23557
23664
  function buildEndAfterCurrentPromptControl(isEndAfterCurrentPromptRequested, sessionTotal) {
23558
23665
  if (isEndAfterCurrentPromptRequested) {
23559
- return colors.bgBlue.white(' X ') + colors.white(` Do all ${formatControlPromptCount(sessionTotal)}`);
23666
+ return buildCoderRunControlKeyBadge('X') + colors.white(` Do all ${formatControlPromptCount(sessionTotal)}`);
23560
23667
  }
23561
- return colors.bgBlue.white(' X ') + colors.white(' End with this prompt');
23668
+ return buildCoderRunControlKeyBadge('X') + colors.white(' End with this prompt');
23562
23669
  }
23563
23670
  /**
23564
23671
  * Formats one prompt count for the `X` control.
@@ -23776,10 +23883,13 @@ function buildAgentRunUiFrame(options) {
23776
23883
  : [options.statusMessage, ...options.detailLines.map((detailLine) => `• ${detailLine}`)];
23777
23884
  const userMessageBoxes = buildUserMessageBoxes(options, totalWidth);
23778
23885
  const visibleOutputLines = buildVisibleOutputLines(options.agentOutputLines);
23779
- const controls = buildControlPills({
23780
- pauseControl: pausePresentation.pauseControl,
23781
- pendingEnterLabel: options.pendingEnterLabel,
23782
- }).join(' ');
23886
+ const controlsBoxLines = buildControlsBoxLines({
23887
+ controlPills: buildControlPills({
23888
+ pauseControl: pausePresentation.pauseControl,
23889
+ pendingEnterLabel: options.pendingEnterLabel,
23890
+ }),
23891
+ controlFeedback: options.controlFeedback,
23892
+ });
23783
23893
  const frame = [
23784
23894
  ...buildAgentRunInitialsVisual(options.config.localAgentName || 'Local Agent', totalWidth),
23785
23895
  '',
@@ -23792,7 +23902,7 @@ function buildAgentRunUiFrame(options) {
23792
23902
  if (options.errors.length > 0) {
23793
23903
  frame.push(...renderBox('Errors', options.errors.map((errorLine) => `${colors.red('✗')} ${errorLine}`), totalWidth, colors.red.bold));
23794
23904
  }
23795
- frame.push(...renderBox('Controls', [controls], totalWidth, colors.white.bold));
23905
+ frame.push(...renderBox('Controls', controlsBoxLines, totalWidth, colors.white.bold));
23796
23906
  return frame;
23797
23907
  }
23798
23908
  /**
@@ -25114,6 +25224,254 @@ async function $runGoScriptWithOutput(options) {
25114
25224
  });
25115
25225
  }
25116
25226
 
25227
+ /**
25228
+ * Default label used before the next pause checkpoint is known.
25229
+ */
25230
+ const DEFAULT_PAUSE_TARGET_LABEL = 'the next task';
25231
+ /**
25232
+ * Current pause state.
25233
+ */
25234
+ let pauseState = 'RUNNING';
25235
+ /**
25236
+ * Label of the next checkpoint where the requested pause will take effect.
25237
+ */
25238
+ let pauseTargetLabel = DEFAULT_PAUSE_TARGET_LABEL;
25239
+ /**
25240
+ * Token of the currently active timed wait, if the runner is inside one.
25241
+ */
25242
+ let activeSkippableWaitToken;
25243
+ /**
25244
+ * Whether the user requested the active timed wait to end immediately.
25245
+ */
25246
+ let isSkipCurrentWaitRequested = false;
25247
+ /**
25248
+ * Whether the runner should stop after the current prompt round reaches its normal boundary.
25249
+ */
25250
+ let isEndAfterCurrentPromptRequested$1 = false;
25251
+ /**
25252
+ * Promise resolvers waiting for the active timed wait to be skipped.
25253
+ */
25254
+ const SKIP_CURRENT_WAIT_RESOLVERS = new Map();
25255
+ /**
25256
+ * Stores one new pause state in the shared runner controller.
25257
+ */
25258
+ function setPauseState(nextPauseState) {
25259
+ pauseState = nextPauseState;
25260
+ }
25261
+ /**
25262
+ * Stores one new pause target label in the shared runner controller.
25263
+ */
25264
+ function setPauseTargetLabel(nextPauseTargetLabel) {
25265
+ pauseTargetLabel = nextPauseTargetLabel.trim() || DEFAULT_PAUSE_TARGET_LABEL;
25266
+ }
25267
+ /**
25268
+ * Stores the dynamic end-after-current-prompt state.
25269
+ */
25270
+ function setEndAfterCurrentPromptRequested(isRequested) {
25271
+ isEndAfterCurrentPromptRequested$1 = isRequested;
25272
+ }
25273
+ /**
25274
+ * Applies the same three-state toggle used by the `P` hotkey.
25275
+ */
25276
+ function togglePauseState() {
25277
+ if (pauseState === 'RUNNING') {
25278
+ setPauseState('PAUSING');
25279
+ return 'REQUESTED_PAUSE';
25280
+ }
25281
+ if (pauseState === 'PAUSING') {
25282
+ setPauseState('RUNNING');
25283
+ resetPauseTargetLabel();
25284
+ return 'CANCELLED_PAUSE';
25285
+ }
25286
+ setPauseState('RUNNING');
25287
+ resetPauseTargetLabel();
25288
+ return 'RESUMED';
25289
+ }
25290
+ /**
25291
+ * Applies the two-state toggle used by the `X` hotkey.
25292
+ */
25293
+ function toggleEndAfterCurrentPromptState() {
25294
+ if (!isEndAfterCurrentPromptRequested$1) {
25295
+ setEndAfterCurrentPromptRequested(true);
25296
+ return 'REQUESTED_END';
25297
+ }
25298
+ setEndAfterCurrentPromptRequested(false);
25299
+ return 'CANCELLED_END';
25300
+ }
25301
+ /**
25302
+ * Returns whether the dynamic end-after-current-prompt control is active.
25303
+ */
25304
+ function getEndAfterCurrentPromptState() {
25305
+ return isEndAfterCurrentPromptRequested$1;
25306
+ }
25307
+ /**
25308
+ * Starts one timed wait which can be skipped by the `S` hotkey.
25309
+ */
25310
+ function beginSkippableWait() {
25311
+ const waitToken = Symbol('coder-run-skippable-wait');
25312
+ activeSkippableWaitToken = waitToken;
25313
+ isSkipCurrentWaitRequested = false;
25314
+ SKIP_CURRENT_WAIT_RESOLVERS.clear();
25315
+ return waitToken;
25316
+ }
25317
+ /**
25318
+ * Finishes one timed wait and clears any skip request that belonged to it.
25319
+ */
25320
+ function finishSkippableWait(waitToken) {
25321
+ if (activeSkippableWaitToken !== waitToken) {
25322
+ return;
25323
+ }
25324
+ activeSkippableWaitToken = undefined;
25325
+ isSkipCurrentWaitRequested = false;
25326
+ resolveSkipCurrentWaitResolvers(waitToken);
25327
+ SKIP_CURRENT_WAIT_RESOLVERS.delete(waitToken);
25328
+ }
25329
+ /**
25330
+ * Requests that the currently active timed wait ends immediately.
25331
+ */
25332
+ function requestSkipCurrentWait() {
25333
+ if (activeSkippableWaitToken === undefined) {
25334
+ return 'NO_ACTIVE_WAIT';
25335
+ }
25336
+ isSkipCurrentWaitRequested = true;
25337
+ resolveSkipCurrentWaitResolvers(activeSkippableWaitToken);
25338
+ return 'REQUESTED_SKIP';
25339
+ }
25340
+ /**
25341
+ * Returns whether one timed wait should end early.
25342
+ */
25343
+ function shouldSkipCurrentWait(waitToken) {
25344
+ return activeSkippableWaitToken === waitToken && isSkipCurrentWaitRequested;
25345
+ }
25346
+ /**
25347
+ * Waits for either a timeout or an `S` hotkey skip request.
25348
+ */
25349
+ async function waitForSkippableMilliseconds(waitToken, durationMs) {
25350
+ if (durationMs <= 0 || shouldSkipCurrentWait(waitToken)) {
25351
+ return;
25352
+ }
25353
+ await new Promise((resolve) => {
25354
+ let timeout = setTimeout(resolveWait, durationMs);
25355
+ const resolver = () => resolveWait();
25356
+ const resolvers = getSkipCurrentWaitResolvers(waitToken);
25357
+ resolvers.add(resolver);
25358
+ function resolveWait() {
25359
+ if (timeout !== undefined) {
25360
+ clearTimeout(timeout);
25361
+ timeout = undefined;
25362
+ }
25363
+ resolvers.delete(resolver);
25364
+ if (resolvers.size === 0) {
25365
+ SKIP_CURRENT_WAIT_RESOLVERS.delete(waitToken);
25366
+ }
25367
+ resolve();
25368
+ }
25369
+ });
25370
+ }
25371
+ /**
25372
+ * Restores all shared terminal controls to their default state.
25373
+ */
25374
+ function resetCoderRunControls() {
25375
+ setPauseState('RUNNING');
25376
+ resetPauseTargetLabel();
25377
+ setEndAfterCurrentPromptRequested(false);
25378
+ if (activeSkippableWaitToken !== undefined) {
25379
+ resolveSkipCurrentWaitResolvers(activeSkippableWaitToken);
25380
+ }
25381
+ activeSkippableWaitToken = undefined;
25382
+ isSkipCurrentWaitRequested = false;
25383
+ SKIP_CURRENT_WAIT_RESOLVERS.clear();
25384
+ }
25385
+ /**
25386
+ * If the execution is paused, it will wait until it is resumed.
25387
+ *
25388
+ * @param options.silent - When `true`, suppresses console output (used when the terminal UI handles display).
25389
+ * @param options.onPaused - Callback invoked when entering the PAUSED state.
25390
+ * @param options.onResumed - Callback invoked when leaving the PAUSED state.
25391
+ */
25392
+ async function checkPause(options) {
25393
+ var _a, _b;
25394
+ if (pauseState === 'PAUSING') {
25395
+ setPauseState('PAUSED');
25396
+ if (!(options === null || options === void 0 ? void 0 : options.silent)) {
25397
+ console.log(colors.bgWhite.black(`Paused before ${getPauseTargetLabel()}`) +
25398
+ colors.gray(' (Press "p" to resume)'));
25399
+ }
25400
+ (_a = options === null || options === void 0 ? void 0 : options.onPaused) === null || _a === void 0 ? void 0 : _a.call(options);
25401
+ while (getPauseState() === 'PAUSED') {
25402
+ await new Promise((resolve) => setTimeout(resolve, 100));
25403
+ }
25404
+ (_b = options === null || options === void 0 ? void 0 : options.onResumed) === null || _b === void 0 ? void 0 : _b.call(options);
25405
+ if (!(options === null || options === void 0 ? void 0 : options.silent)) {
25406
+ console.log(colors.green('Resuming...'));
25407
+ }
25408
+ }
25409
+ }
25410
+ /**
25411
+ * Returns the current pause state for external consumers such as the terminal UI.
25412
+ */
25413
+ function getPauseState() {
25414
+ return pauseState;
25415
+ }
25416
+ /**
25417
+ * Returns the label of the next checkpoint where pausing will take effect.
25418
+ */
25419
+ function getPauseTargetLabel() {
25420
+ return pauseTargetLabel;
25421
+ }
25422
+ /**
25423
+ * Updates the label of the next pause checkpoint.
25424
+ */
25425
+ function announcePauseTargetLabel(nextPauseTargetLabel) {
25426
+ setPauseTargetLabel(nextPauseTargetLabel);
25427
+ }
25428
+ /**
25429
+ * Restores the default generic pause target label.
25430
+ */
25431
+ function resetPauseTargetLabel() {
25432
+ setPauseTargetLabel(DEFAULT_PAUSE_TARGET_LABEL);
25433
+ }
25434
+ /**
25435
+ * Requests a pause from an external controller (e.g. the Ink UI).
25436
+ */
25437
+ function requestPause() {
25438
+ if (pauseState === 'RUNNING') {
25439
+ setPauseState('PAUSING');
25440
+ }
25441
+ }
25442
+ /**
25443
+ * Resumes execution from an external controller after a pause.
25444
+ */
25445
+ function requestResume() {
25446
+ setPauseState('RUNNING');
25447
+ resetPauseTargetLabel();
25448
+ }
25449
+ /**
25450
+ * Gets or creates the resolver set for one timed wait.
25451
+ */
25452
+ function getSkipCurrentWaitResolvers(waitToken) {
25453
+ const existingResolvers = SKIP_CURRENT_WAIT_RESOLVERS.get(waitToken);
25454
+ if (existingResolvers !== undefined) {
25455
+ return existingResolvers;
25456
+ }
25457
+ const resolvers = new Set();
25458
+ SKIP_CURRENT_WAIT_RESOLVERS.set(waitToken, resolvers);
25459
+ return resolvers;
25460
+ }
25461
+ /**
25462
+ * Resolves all pending skip waiters for one timed wait.
25463
+ */
25464
+ function resolveSkipCurrentWaitResolvers(waitToken) {
25465
+ const resolvers = SKIP_CURRENT_WAIT_RESOLVERS.get(waitToken);
25466
+ if (resolvers === undefined) {
25467
+ return;
25468
+ }
25469
+ for (const resolver of resolvers) {
25470
+ resolver();
25471
+ }
25472
+ resolvers.clear();
25473
+ }
25474
+
25117
25475
  /**
25118
25476
  * Minimum timer duration used to avoid a zero-millisecond polling loop.
25119
25477
  */
@@ -25163,6 +25521,32 @@ async function waitForRegularMilliseconds(durationMs) {
25163
25521
  await new Promise((resolve) => setTimeout(resolve, durationMs));
25164
25522
  }
25165
25523
 
25524
+ /**
25525
+ * Waits until one wall-clock deadline has passed, or until the user skips the wait with the `S` control.
25526
+ *
25527
+ * This is the single way `ptbk coder` waits for a deadline the user is allowed to cut short, so every
25528
+ * wait which shows the `S Skip current waiting` control really reacts to it: the pacing waits between
25529
+ * prompts, the cool-down after an error, the harness session-limit waits and the server keep-alive poll.
25530
+ *
25531
+ * @private internal utility of `ptbk coder` wait handling
25532
+ */
25533
+ async function waitForSkippableWorldTimeDeadline(options) {
25534
+ const { deadlineTimeMs, pollIntervalMs, onTick } = options;
25535
+ const waitToken = beginSkippableWait();
25536
+ try {
25537
+ await waitUntilWorldTimeDeadline({
25538
+ deadlineTimeMs,
25539
+ pollIntervalMs,
25540
+ onTick,
25541
+ shouldStopWaiting: () => shouldSkipCurrentWait(waitToken),
25542
+ waitForMilliseconds: (waitDurationMs) => waitForSkippableMilliseconds(waitToken, waitDurationMs),
25543
+ });
25544
+ }
25545
+ finally {
25546
+ finishSkippableWait(waitToken);
25547
+ }
25548
+ }
25549
+
25166
25550
  /**
25167
25551
  * Base delimiter used for passing large prompts through stdin.
25168
25552
  */
@@ -25570,6 +25954,10 @@ class ClaudeCodeRunner {
25570
25954
  }
25571
25955
  /**
25572
25956
  * Waits until the Claude Code session can be resumed, keeping terminal status clear.
25957
+ *
25958
+ * The wait runs in the `waiting` phase, where the terminal UI offers `S Skip current waiting`, so it is
25959
+ * a skippable wait: pressing `S` resumes the session with `--resume` immediately instead of sitting out
25960
+ * the reset window.
25573
25961
  */
25574
25962
  async function waitForClaudeCodeSessionLimitReset(sessionLimit, resurrectionCount, options) {
25575
25963
  var _a, _b;
@@ -25580,7 +25968,7 @@ async function waitForClaudeCodeSessionLimitReset(sessionLimit, resurrectionCoun
25580
25968
  if ((_a = options.shouldPrintLiveOutput) !== null && _a !== void 0 ? _a : true) {
25581
25969
  console.warn(colors.yellow(`[claude-code] Session limit detected for ${sessionLimit.sessionId}. Resurrection #${resurrectionCount} will resume with --resume after ${formatDurationMs(delayMs)}. ${resetSummary}`));
25582
25970
  }
25583
- await waitUntilWorldTimeDeadline({
25971
+ await waitForSkippableWorldTimeDeadline({
25584
25972
  deadlineTimeMs: resetDeadlineTimeMs,
25585
25973
  pollIntervalMs: CLAUDE_CODE_SESSION_RESURRECTION_POLL_MS,
25586
25974
  onTick: async (remainingDelayMs) => {
@@ -27019,292 +27407,100 @@ function exitForMissingModel(agentName, availableModels, exampleUsages) {
27019
27407
  }
27020
27408
 
27021
27409
  /**
27022
- * Default label used before the next pause checkpoint is known.
27023
- */
27024
- const DEFAULT_PAUSE_TARGET_LABEL = 'the next task';
27025
- /**
27026
- * Current pause state.
27027
- */
27028
- let pauseState = 'RUNNING';
27029
- /**
27030
- * Label of the next checkpoint where the requested pause will take effect.
27031
- */
27032
- let pauseTargetLabel = DEFAULT_PAUSE_TARGET_LABEL;
27033
- /**
27034
- * Token of the currently active timed wait, if the runner is inside one.
27035
- */
27036
- let activeSkippableWaitToken;
27037
- /**
27038
- * Whether the user requested the active timed wait to end immediately.
27039
- */
27040
- let isSkipCurrentWaitRequested = false;
27041
- /**
27042
- * Whether the runner should stop after the current prompt round reaches its normal boundary.
27043
- */
27044
- let isEndAfterCurrentPromptRequested$1 = false;
27045
- /**
27046
- * Promise resolvers waiting for the active timed wait to be skipped.
27047
- */
27048
- const SKIP_CURRENT_WAIT_RESOLVERS = new Map();
27049
- /**
27050
- * Stores one new pause state in the shared runner controller.
27051
- */
27052
- function setPauseState(nextPauseState) {
27053
- pauseState = nextPauseState;
27054
- }
27055
- /**
27056
- * Stores one new pause target label in the shared runner controller.
27057
- */
27058
- function setPauseTargetLabel(nextPauseTargetLabel) {
27059
- pauseTargetLabel = nextPauseTargetLabel.trim() || DEFAULT_PAUSE_TARGET_LABEL;
27060
- }
27061
- /**
27062
- * Stores the dynamic end-after-current-prompt state.
27063
- */
27064
- function setEndAfterCurrentPromptRequested(isRequested) {
27065
- isEndAfterCurrentPromptRequested$1 = isRequested;
27066
- }
27067
- /**
27068
- * Applies the same three-state toggle used by the `P` hotkey.
27069
- */
27070
- function togglePauseState() {
27071
- if (pauseState === 'RUNNING') {
27072
- setPauseState('PAUSING');
27073
- return 'REQUESTED_PAUSE';
27074
- }
27075
- if (pauseState === 'PAUSING') {
27076
- setPauseState('RUNNING');
27077
- resetPauseTargetLabel();
27078
- return 'CANCELLED_PAUSE';
27079
- }
27080
- setPauseState('RUNNING');
27081
- resetPauseTargetLabel();
27082
- return 'RESUMED';
27083
- }
27084
- /**
27085
- * Applies the two-state toggle used by the `X` hotkey.
27086
- */
27087
- function toggleEndAfterCurrentPromptState() {
27088
- if (!isEndAfterCurrentPromptRequested$1) {
27089
- setEndAfterCurrentPromptRequested(true);
27090
- return 'REQUESTED_END';
27091
- }
27092
- setEndAfterCurrentPromptRequested(false);
27093
- return 'CANCELLED_END';
27094
- }
27095
- /**
27096
- * Returns whether the dynamic end-after-current-prompt control is active.
27097
- */
27098
- function getEndAfterCurrentPromptState() {
27099
- return isEndAfterCurrentPromptRequested$1;
27100
- }
27101
- /**
27102
- * Starts one timed wait which can be skipped by the `S` hotkey.
27103
- */
27104
- function beginSkippableWait() {
27105
- const waitToken = Symbol('coder-run-skippable-wait');
27106
- activeSkippableWaitToken = waitToken;
27107
- isSkipCurrentWaitRequested = false;
27108
- SKIP_CURRENT_WAIT_RESOLVERS.clear();
27109
- return waitToken;
27110
- }
27111
- /**
27112
- * Finishes one timed wait and clears any skip request that belonged to it.
27113
- */
27114
- function finishSkippableWait(waitToken) {
27115
- if (activeSkippableWaitToken !== waitToken) {
27116
- return;
27117
- }
27118
- activeSkippableWaitToken = undefined;
27119
- isSkipCurrentWaitRequested = false;
27120
- resolveSkipCurrentWaitResolvers(waitToken);
27121
- SKIP_CURRENT_WAIT_RESOLVERS.delete(waitToken);
27122
- }
27123
- /**
27124
- * Requests that the currently active timed wait ends immediately.
27125
- */
27126
- function requestSkipCurrentWait() {
27127
- if (activeSkippableWaitToken === undefined) {
27128
- return 'NO_ACTIVE_WAIT';
27129
- }
27130
- isSkipCurrentWaitRequested = true;
27131
- resolveSkipCurrentWaitResolvers(activeSkippableWaitToken);
27132
- return 'REQUESTED_SKIP';
27133
- }
27134
- /**
27135
- * Returns whether one timed wait should end early.
27136
- */
27137
- function shouldSkipCurrentWait(waitToken) {
27138
- return activeSkippableWaitToken === waitToken && isSkipCurrentWaitRequested;
27139
- }
27140
- /**
27141
- * Waits for either a timeout or an `S` hotkey skip request.
27410
+ * Applies the terminal control bound to one pressed key and describes what that key press did.
27411
+ *
27412
+ * This is the single place where the `P`, `S` and `X` keys are turned into runner actions, so the
27413
+ * plain console listener and the rich terminal UI always perform the same action and always answer
27414
+ * the user, even when the pressed control changed nothing.
27415
+ *
27416
+ * @param keyName Name of the pressed key as reported by the `keypress` event.
27417
+ * @returns The answer shown to the user, or `undefined` when the key is not a control key.
27418
+ *
27419
+ * @private internal utility of `ptbk coder` terminal controls
27142
27420
  */
27143
- async function waitForSkippableMilliseconds(waitToken, durationMs) {
27144
- if (durationMs <= 0 || shouldSkipCurrentWait(waitToken)) {
27145
- return;
27421
+ function applyCoderRunControlKey(keyName) {
27422
+ switch (keyName) {
27423
+ case 'p':
27424
+ return describePauseToggle(togglePauseState());
27425
+ case 's':
27426
+ return describeSkipCurrentWait(requestSkipCurrentWait());
27427
+ case 'x':
27428
+ return describeEndAfterCurrentPrompt(toggleEndAfterCurrentPromptState());
27429
+ default:
27430
+ return undefined;
27146
27431
  }
27147
- await new Promise((resolve) => {
27148
- let timeout = setTimeout(resolveWait, durationMs);
27149
- const resolver = () => resolveWait();
27150
- const resolvers = getSkipCurrentWaitResolvers(waitToken);
27151
- resolvers.add(resolver);
27152
- function resolveWait() {
27153
- if (timeout !== undefined) {
27154
- clearTimeout(timeout);
27155
- timeout = undefined;
27156
- }
27157
- resolvers.delete(resolver);
27158
- if (resolvers.size === 0) {
27159
- SKIP_CURRENT_WAIT_RESOLVERS.delete(waitToken);
27160
- }
27161
- resolve();
27162
- }
27163
- });
27164
27432
  }
27165
27433
  /**
27166
- * Restores all shared terminal controls to their default state.
27434
+ * Describes what pressing `P` did.
27435
+ *
27436
+ * @private helper of `applyCoderRunControlKey`
27167
27437
  */
27168
- function resetCoderRunControls() {
27169
- setPauseState('RUNNING');
27170
- resetPauseTargetLabel();
27171
- setEndAfterCurrentPromptRequested(false);
27172
- if (activeSkippableWaitToken !== undefined) {
27173
- resolveSkipCurrentWaitResolvers(activeSkippableWaitToken);
27438
+ function describePauseToggle(toggleResult) {
27439
+ if (toggleResult === 'REQUESTED_PAUSE') {
27440
+ return {
27441
+ controlKey: 'P',
27442
+ message: `Pause requested, pausing before ${getPauseTargetLabel()}`,
27443
+ tone: 'warning',
27444
+ };
27174
27445
  }
27175
- activeSkippableWaitToken = undefined;
27176
- isSkipCurrentWaitRequested = false;
27177
- SKIP_CURRENT_WAIT_RESOLVERS.clear();
27178
- }
27179
- /**
27180
- * Listens for the terminal control keys.
27181
- */
27182
- function listenForCoderRunControls() {
27183
- if (!process.stdin.isTTY) {
27184
- return;
27446
+ if (toggleResult === 'CANCELLED_PAUSE') {
27447
+ return {
27448
+ controlKey: 'P',
27449
+ message: 'Pause cancelled, the run continues',
27450
+ tone: 'success',
27451
+ };
27185
27452
  }
27186
- readline.emitKeypressEvents(process.stdin);
27187
- process.stdin.setRawMode(true);
27188
- process.stdin.on('keypress', (str, key) => {
27189
- if (key.ctrl && key.name === 'c') {
27190
- process.exit();
27191
- }
27192
- if (key.name === 'p') {
27193
- const toggleResult = togglePauseState();
27194
- if (toggleResult === 'REQUESTED_PAUSE') {
27195
- // Note: Using console.log here which adds a new line.
27196
- // This is intentional to prevent the message from being overwritten.
27197
- console.log(colors.bgWhite('Pausing...'));
27198
- }
27199
- else if (toggleResult === 'CANCELLED_PAUSE') {
27200
- console.log(colors.green('Pause cancelled. Resuming...'));
27201
- }
27202
- }
27203
- if (key.name === 's') {
27204
- if (requestSkipCurrentWait() === 'REQUESTED_SKIP') {
27205
- console.log(colors.green('Skipping current wait...'));
27206
- }
27207
- }
27208
- if (key.name === 'x') {
27209
- const toggleResult = toggleEndAfterCurrentPromptState();
27210
- if (toggleResult === 'REQUESTED_END') {
27211
- console.log(colors.yellow('Will end after the current prompt finishes.'));
27212
- }
27213
- else {
27214
- console.log(colors.green('End request cancelled. Continuing all prompts.'));
27215
- }
27216
- }
27217
- });
27453
+ return {
27454
+ controlKey: 'P',
27455
+ message: 'Resuming the run',
27456
+ tone: 'success',
27457
+ };
27218
27458
  }
27219
27459
  /**
27220
- * If the execution is paused, it will wait until it is resumed.
27460
+ * Describes what pressing `S` did.
27221
27461
  *
27222
- * @param options.silent - When `true`, suppresses console output (used when the terminal UI handles display).
27223
- * @param options.onPaused - Callback invoked when entering the PAUSED state.
27224
- * @param options.onResumed - Callback invoked when leaving the PAUSED state.
27225
- */
27226
- async function checkPause(options) {
27227
- var _a, _b;
27228
- if (pauseState === 'PAUSING') {
27229
- setPauseState('PAUSED');
27230
- if (!(options === null || options === void 0 ? void 0 : options.silent)) {
27231
- console.log(colors.bgWhite.black(`Paused before ${getPauseTargetLabel()}`) +
27232
- colors.gray(' (Press "p" to resume)'));
27233
- }
27234
- (_a = options === null || options === void 0 ? void 0 : options.onPaused) === null || _a === void 0 ? void 0 : _a.call(options);
27235
- while (getPauseState() === 'PAUSED') {
27236
- await new Promise((resolve) => setTimeout(resolve, 100));
27237
- }
27238
- (_b = options === null || options === void 0 ? void 0 : options.onResumed) === null || _b === void 0 ? void 0 : _b.call(options);
27239
- if (!(options === null || options === void 0 ? void 0 : options.silent)) {
27240
- console.log(colors.green('Resuming...'));
27241
- }
27242
- }
27243
- }
27244
- /**
27245
- * Returns the current pause state for external consumers such as the terminal UI.
27462
+ * @private helper of `applyCoderRunControlKey`
27246
27463
  */
27247
- function getPauseState() {
27248
- return pauseState;
27249
- }
27250
- /**
27251
- * Returns the label of the next checkpoint where pausing will take effect.
27252
- */
27253
- function getPauseTargetLabel() {
27254
- return pauseTargetLabel;
27255
- }
27256
- /**
27257
- * Updates the label of the next pause checkpoint.
27258
- */
27259
- function announcePauseTargetLabel(nextPauseTargetLabel) {
27260
- setPauseTargetLabel(nextPauseTargetLabel);
27261
- }
27262
- /**
27263
- * Restores the default generic pause target label.
27264
- */
27265
- function resetPauseTargetLabel() {
27266
- setPauseTargetLabel(DEFAULT_PAUSE_TARGET_LABEL);
27267
- }
27268
- /**
27269
- * Requests a pause from an external controller (e.g. the Ink UI).
27270
- */
27271
- function requestPause() {
27272
- if (pauseState === 'RUNNING') {
27273
- setPauseState('PAUSING');
27464
+ function describeSkipCurrentWait(skipResult) {
27465
+ if (skipResult === 'REQUESTED_SKIP') {
27466
+ return {
27467
+ controlKey: 'S',
27468
+ message: 'Skipping the current waiting, continuing right now',
27469
+ tone: 'success',
27470
+ };
27274
27471
  }
27472
+ return {
27473
+ controlKey: 'S',
27474
+ message: 'Nothing to skip, the coder is not waiting right now',
27475
+ tone: 'info',
27476
+ };
27275
27477
  }
27276
27478
  /**
27277
- * Resumes execution from an external controller after a pause.
27278
- */
27279
- function requestResume() {
27280
- setPauseState('RUNNING');
27281
- resetPauseTargetLabel();
27282
- }
27283
- /**
27284
- * Gets or creates the resolver set for one timed wait.
27479
+ * Describes what pressing `X` did.
27480
+ *
27481
+ * @private helper of `applyCoderRunControlKey`
27285
27482
  */
27286
- function getSkipCurrentWaitResolvers(waitToken) {
27287
- const existingResolvers = SKIP_CURRENT_WAIT_RESOLVERS.get(waitToken);
27288
- if (existingResolvers !== undefined) {
27289
- return existingResolvers;
27483
+ function describeEndAfterCurrentPrompt(toggleResult) {
27484
+ if (toggleResult === 'REQUESTED_END') {
27485
+ return {
27486
+ controlKey: 'X',
27487
+ message: 'Ending after the current prompt finishes',
27488
+ tone: 'warning',
27489
+ };
27290
27490
  }
27291
- const resolvers = new Set();
27292
- SKIP_CURRENT_WAIT_RESOLVERS.set(waitToken, resolvers);
27293
- return resolvers;
27491
+ return {
27492
+ controlKey: 'X',
27493
+ message: 'End cancelled, the whole queue will be processed',
27494
+ tone: 'success',
27495
+ };
27294
27496
  }
27497
+
27295
27498
  /**
27296
- * Resolves all pending skip waiters for one timed wait.
27499
+ * How long one control answer stays visible in the rich terminal UI.
27500
+ *
27501
+ * @private internal constant of `ptbk coder` terminal controls
27297
27502
  */
27298
- function resolveSkipCurrentWaitResolvers(waitToken) {
27299
- const resolvers = SKIP_CURRENT_WAIT_RESOLVERS.get(waitToken);
27300
- if (resolvers === undefined) {
27301
- return;
27302
- }
27303
- for (const resolver of resolvers) {
27304
- resolver();
27305
- }
27306
- resolvers.clear();
27307
- }
27503
+ const CODER_RUN_CONTROL_FEEDBACK_DURATION_MS = 5000;
27308
27504
 
27309
27505
  /**
27310
27506
  * Human-readable label for a priority scope without any bounds.
@@ -27429,85 +27625,696 @@ function assertPriorityBoundary(optionName, priority) {
27429
27625
  // Note: [🟡] Code for priority filtering [priorityFilter](scripts/run-codex-prompts/prompts/priorityFilter.ts) should never be published outside of `@promptbook/cli`
27430
27626
 
27431
27627
  /**
27432
- * Fixed left-side head used across all octopus frames.
27628
+ * Minimum width used for the rich coder-run frame.
27433
27629
  */
27434
- const OCTOPUS_HEAD_LINES = [
27435
- colors.magenta.bold(' .-""""-.'),
27436
- colors.magenta.bold(" .' .-. '."),
27437
- `${colors.magenta.bold(' / (')}${colors.yellow.bold('o o')}${colors.magenta.bold(') \\')}`,
27438
- colors.magenta.bold(' | ^ |'),
27439
- colors.magenta.bold(' | \\___/ |'),
27440
- colors.magenta.bold(' \\___________/'),
27441
- ];
27630
+ const MIN_FRAME_WIDTH = 56;
27442
27631
  /**
27443
- * Gap between the head silhouette and the animated tentacles.
27632
+ * Maximum width used for the rich coder-run frame.
27444
27633
  */
27445
- const OCTOPUS_HEAD_TO_TENTACLE_GAP = ' ';
27634
+ const MAX_FRAME_WIDTH = 96;
27446
27635
  /**
27447
- * Animated right-side tentacle poses.
27636
+ * Builds the complete boxed terminal frame for the rich `ptbk coder run` UI.
27448
27637
  */
27449
- const OCTOPUS_TENTACLE_FRAMES = [
27450
- [
27451
- ` ${colors.green.bold('ptbk.io')}`,
27452
- colors.cyan(' __ __'),
27453
- colors.cyan(' _/\\/\\/ \\_/\\/ \\_/\\_'),
27454
- colors.cyan('_/\\/ _ /\\/ _ /\\/\\__'),
27455
- colors.cyan('\\__/ /_/\\/ \\_/ \\_/ \\_/'),
27456
- colors.cyan(' /_/ \\__/ \\__/ /'),
27457
- ],
27458
- [
27459
- ` ${colors.green.bold('ptbk.io')}`,
27460
- colors.cyan(' __ __'),
27461
- colors.cyan(' _/\\/ \\_/\\/\\_ \\_/\\_'),
27462
- colors.cyan('_/\\/ _ /\\/ _ \\/\\/ \\__'),
27463
- colors.cyan('\\__/ /_/\\/ \\_/ \\_ /\\_/'),
27464
- colors.cyan(' /_/ \\__/ \\__/ /'),
27465
- ],
27466
- [
27467
- ` ${colors.green.bold('ptbk.io')}`,
27468
- colors.cyan(' __ __'),
27469
- colors.cyan(' _/\\/\\/ \\_/\\_ \\_/\\_'),
27470
- colors.cyan('_/\\/ _ /\\/ _ /\\/ \\_'),
27471
- colors.cyan('\\__/ /_/\\/ \\_/ \\_/ _/'),
27472
- colors.cyan(' /_/ \\__/ \\__/'),
27473
- ],
27474
- [
27475
- ` ${colors.green.bold('ptbk.io')}`,
27476
- colors.cyan(' __ __'),
27477
- colors.cyan(' _/\\/ \\_/\\/ \\_/\\/\\_'),
27478
- colors.cyan('_/\\/ _ /\\/ _ /\\/\\ \\'),
27479
- colors.cyan('\\__/ /_/\\/ \\_/ \\_ \\_/'),
27480
- colors.cyan(' /_/ \\__/ \\__\\_/'),
27481
- ],
27482
- ];
27638
+ function buildCoderRunUiFrame(options) {
27639
+ const totalWidth = Math.max(MIN_FRAME_WIDTH, Math.min(options.terminalWidth, MAX_FRAME_WIDTH));
27640
+ const isPromptActive = options.phase === 'running' || options.phase === 'verifying' || options.phase === 'loading';
27641
+ const promptStatusPrefix = isPromptActive ? `${colors.yellow(`${options.spinner} `)}` : '';
27642
+ const pausePresentation = buildPausePresentation(options.phase, options.pauseState, options.pauseTargetLabel, options.statusMessage);
27643
+ const sessionLines = buildSessionLines(options, totalWidth, pausePresentation);
27644
+ const currentTaskLines = options.currentPromptLabel
27645
+ ? [
27646
+ `${promptStatusPrefix}${colors.bold.white(fitPlainText(options.currentPromptLabel, totalWidth - 8))}`,
27647
+ `Attempt ${options.currentAttempt}/${options.maxAttempts} · ${options.statusMessage}`,
27648
+ ...options.detailLines.map((detailLine) => `• ${detailLine}`),
27649
+ ]
27650
+ : [options.statusMessage, ...options.detailLines.map((detailLine) => `• ${detailLine}`)];
27651
+ const visibleOutputLines = buildVisibleOutputLines(options.agentOutputLines);
27652
+ const controlsBoxLines = buildControlsBoxLines({
27653
+ controlPills: buildCoderRunControlPills({
27654
+ phase: options.phase,
27655
+ pauseControl: pausePresentation.pauseControl,
27656
+ pendingEnterLabel: options.pendingEnterLabel,
27657
+ isEndAfterCurrentPromptRequested: options.isEndAfterCurrentPromptRequested,
27658
+ sessionTotal: options.progress.sessionTotal,
27659
+ }),
27660
+ controlFeedback: options.controlFeedback,
27661
+ });
27662
+ const frame = [
27663
+ ...buildFrameHeaderVisual(options, totalWidth),
27664
+ '',
27665
+ ...renderBox('Session', sessionLines, totalWidth, colors.yellow.bold),
27666
+ ...renderBox(options.currentPromptLabel ? 'Current task' : 'Queue', currentTaskLines, totalWidth, colors.magenta.bold),
27667
+ ...renderBox('Live output', visibleOutputLines, totalWidth, colors.green.bold),
27668
+ ];
27669
+ if (options.errors.length > 0) {
27670
+ const bodyWidth = Math.max(10, totalWidth - 4);
27671
+ frame.push(...renderBox('Errors', buildErrorDisplayLines(options.errors, bodyWidth), totalWidth, colors.red.bold));
27672
+ }
27673
+ frame.push(...renderBox('Controls', controlsBoxLines, totalWidth, colors.white.bold));
27674
+ return frame;
27675
+ }
27483
27676
  /**
27484
- * Builds the horizontal octopus illustration shown above the coder-run dashboard.
27677
+ * Builds the header visual above the dashboard boxes.
27678
+ *
27679
+ * Shows the ASCII-art `--agent` avatar visual as soon as one is available. Until the avatar is
27680
+ * loaded - and when no avatar can be rendered at all - the header stays empty, because the
27681
+ * placeholder ASCII `PTBK.IO` wordmark only added terminal noise while nothing was rendered yet.
27682
+ */
27683
+ function buildFrameHeaderVisual(options, totalWidth) {
27684
+ if (options.agentVisualLines !== undefined && options.agentVisualLines.length > 0) {
27685
+ return options.agentVisualLines.map((agentVisualLine) => centerAnsiText(agentVisualLine, totalWidth));
27686
+ }
27687
+ if (options.agentVisual !== undefined) {
27688
+ const agentVisualLines = options.agentVisual.renderFrame({
27689
+ animationTimeMs: options.animationTimeMs,
27690
+ });
27691
+ if (agentVisualLines.length > 0) {
27692
+ return agentVisualLines.map((agentVisualLine) => centerAnsiText(agentVisualLine, totalWidth));
27693
+ }
27694
+ }
27695
+ return [];
27696
+ }
27697
+ /**
27698
+ * Builds the structured session lines that combine state, runner, queue, and timing metadata.
27699
+ */
27700
+ function buildSessionLines(options, totalWidth, pausePresentation) {
27701
+ const bodyWidth = Math.max(10, totalWidth - 4);
27702
+ return buildSessionRows(options, bodyWidth, pausePresentation).map((sessionRow) => buildLabeledSessionLine(sessionRow.label, sessionRow.value, bodyWidth));
27703
+ }
27704
+ /**
27705
+ * Builds the session rows so the renderer can keep one consistent structure without duplicating labels.
27706
+ */
27707
+ function buildSessionRows(options, bodyWidth, pausePresentation) {
27708
+ const runnerParts = [options.config.agentName || 'No agent selected'];
27709
+ if (options.config.modelName) {
27710
+ runnerParts.push(options.config.modelName);
27711
+ }
27712
+ if (options.config.thinkingLevel) {
27713
+ runnerParts.push(`thinking ${options.config.thinkingLevel}`);
27714
+ }
27715
+ const configurationRows = [
27716
+ ...buildOptionalSessionRow('Context', options.config.context),
27717
+ ...buildOptionalSessionRow('Server', options.config.serverUrl
27718
+ ? buildTerminalUrlLink(options.config.serverUrl, bodyWidth - SESSION_LABEL_WIDTH - 1)
27719
+ : undefined),
27720
+ ...buildOptionalSessionRow('Test', options.config.testCommand),
27721
+ ];
27722
+ return [
27723
+ {
27724
+ label: 'State',
27725
+ value: `${pausePresentation.badge} ${pausePresentation.stateMessage}`,
27726
+ },
27727
+ {
27728
+ label: 'Runner',
27729
+ value: runnerParts.join(' · '),
27730
+ },
27731
+ ...configurationRows,
27732
+ ...buildScriptPathSessionRows(options.currentScriptPaths || [], bodyWidth),
27733
+ {
27734
+ label: 'This run',
27735
+ value: buildThisRunSummary(options.progress),
27736
+ },
27737
+ {
27738
+ label: 'Backlog',
27739
+ value: buildBacklogSummary(options.progress),
27740
+ },
27741
+ {
27742
+ label: 'Scope',
27743
+ value: buildScopeSummary(options.progress, options.config),
27744
+ },
27745
+ {
27746
+ label: 'Timing',
27747
+ value: buildTimingSummary(options.progress),
27748
+ },
27749
+ {
27750
+ label: 'Progress',
27751
+ value: buildProgressBar$1(options.progress.percentage, bodyWidth - SESSION_LABEL_WIDTH - 1, `${options.progress.percentage}% complete (${options.progress.sessionDone}/${options.progress.sessionTotal} done)`),
27752
+ },
27753
+ ];
27754
+ }
27755
+ /**
27756
+ * Builds zero or one structured session row for optional metadata.
27757
+ */
27758
+ function buildOptionalSessionRow(label, value) {
27759
+ if (!value) {
27760
+ return [];
27761
+ }
27762
+ return [{ label, value }];
27763
+ }
27764
+ /**
27765
+ * Builds the active-session summary shown in the session box.
27766
+ */
27767
+ function buildThisRunSummary(progress) {
27768
+ if (progress.sessionTotal === 0) {
27769
+ return 'No runnable prompts in current scope';
27770
+ }
27771
+ return `Task ${progress.currentPromptIndex}/${progress.sessionTotal} · ${progress.sessionDone} done · ${progress.sessionRemaining} left`;
27772
+ }
27773
+ /**
27774
+ * Builds the backlog/filter summary shown in the session box.
27775
+ */
27776
+ function buildBacklogSummary(progress) {
27777
+ const parts = [`Repo ${progress.totalPrompts} total`];
27778
+ if (progress.skippedPrompts > 0) {
27779
+ parts.push(`${formatPromptCount$1(progress.skippedPrompts)} outside priority scope`);
27780
+ }
27781
+ return parts.join(' · ');
27782
+ }
27783
+ /**
27784
+ * Builds the priority/write-order summary shown in the session box.
27785
+ */
27786
+ function buildScopeSummary(progress, config) {
27787
+ var _a;
27788
+ const parts = [formatPriorityFilter((_a = config.priorityFilter) !== null && _a !== void 0 ? _a : {})];
27789
+ if (config.limit !== undefined) {
27790
+ parts.push(`Limit ${formatPromptRunCount(config.limit)}`);
27791
+ }
27792
+ if (progress.toBeWrittenPrompts > 0) {
27793
+ parts.push(`Write ${formatPromptCount$1(progress.toBeWrittenPrompts)} first`);
27794
+ }
27795
+ return parts.join(' · ');
27796
+ }
27797
+ /**
27798
+ * Builds the elapsed/estimate summary shown in the session box.
27799
+ */
27800
+ function buildTimingSummary(progress) {
27801
+ return `Elapsed ${progress.elapsedText} · Total ${progress.estimatedTotalText} · ETA ${progress.estimatedLabel}`;
27802
+ }
27803
+ /**
27804
+ * Formats a prompt count with singular/plural wording.
27805
+ */
27806
+ function formatPromptCount$1(count) {
27807
+ return `${count} prompt${count === 1 ? '' : 's'}`;
27808
+ }
27809
+ /**
27810
+ * Formats a configured successful prompt-run limit with singular/plural wording.
27811
+ */
27812
+ function formatPromptRunCount(count) {
27813
+ return `${count} prompt run${count === 1 ? '' : 's'}`;
27814
+ }
27815
+
27816
+ /**
27817
+ * ANSI sequence that clears the current terminal row.
27818
+ *
27819
+ * @private internal constant of coder run UI
27820
+ */
27821
+ const CLEAR_CURRENT_LINE = '\x1b[2K';
27822
+ /**
27823
+ * ANSI control character that moves the cursor to the start of its current row.
27824
+ *
27825
+ * @private internal constant of coder run UI
27826
+ */
27827
+ const MOVE_CURSOR_TO_LINE_START = '\r';
27828
+ /**
27829
+ * Builds one atomic ANSI update that changes the displayed coder-run terminal frame.
27830
+ *
27831
+ * A change in frame height requires a full rewrite to reserve or clear rows correctly. Frames with
27832
+ * the same height only rewrite contiguous groups of changed rows, returning to the frame bottom
27833
+ * between groups so each group can be assembled into the same output payload safely.
27834
+ *
27835
+ * @returns One terminal output payload, or `undefined` when the frame is already up to date.
27485
27836
  *
27486
27837
  * @private internal utility of coder run UI
27487
27838
  */
27488
- function buildCoderRunOctopusVisual(options) {
27489
- const tentacleFrame = OCTOPUS_TENTACLE_FRAMES[((options.animationFrame % OCTOPUS_TENTACLE_FRAMES.length) + OCTOPUS_TENTACLE_FRAMES.length) %
27490
- OCTOPUS_TENTACLE_FRAMES.length];
27491
- const visualLines = OCTOPUS_HEAD_LINES.map((headLine, lineIndex) => `${headLine}${OCTOPUS_HEAD_TO_TENTACLE_GAP}${tentacleFrame[lineIndex]}`);
27492
- const visualWidth = visualLines.reduce((maxWidth, line) => Math.max(maxWidth, visibleLength(line)), 0);
27493
- visualLines.map((line) => centerAnsiText(padAnsiText(line, visualWidth), options.totalWidth));
27494
- /*
27495
- Note: Octopus art should look better, now using just text
27496
- https://www.google.com/search?sca_esv=52e84acb78c558cc&sxsrf=ANbL-n7DVKf71T1HSPRpM-2skfMss0jh7w:1776693767588&udm=2&fbs=ADc_l-ZseckkBJUFopaGDNYa-HGjo4_b6b_a7pIHTL5Y9QnExg6xJqXbG7aOLcH8CWqOtkzCrjxXWZVmrIhYPvZzFDVUIb7oTJfuJ6idsCc5GA1j5KGoi2q3sW0uDBWWfYgbuxGWTQPZMetvj33BdP833wZm47mxW-6rC3bTQWluwJdOsgloPieyQvTfF2uNgIZ_K0KZ-WzpL1An8GuRrKqHdvl8T306FA&q=octopus&sa=X&ved=2ahUKEwil7ZCHzPyTAxUghP0HHY-_Js8QtKgLegQIOhAB&biw=1745&bih=903&dpr=1.1#sv=CAMSVhoyKhBlLVl1bHNmeVhneml6a1dNMg5ZdWxzZnlYZ3ppemtXTToOTVJvdXhTdk5STkZ4d00gBCocCgZtb3NhaWMSEGUtWXVsc2Z5WGd6aXprV00YADABGAcg46LBxA9KCBABGAEgASgB
27497
- https://www.mrgoodfish.com/wp-content/uploads/2022/09/Eledone_moschata__.png
27498
- https://fish-commercial-names.ec.europa.eu/fish-names/jakarta.faces.resource/pictograms/octopus_vulgaris.jpg.xhtml?ln=images
27499
- https://www.asciiart.eu/image-to-ascii
27500
- */
27501
- // Note: Created by https://patorjk.com/software/taag/#p=display&f=ANSI+Compact&t=ptbk.io&x=none&v=4&h=4&w=80&we=false
27502
- return _spaceTrim(`
27503
-
27504
- ▄▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄ ▄▄ ▄▄ ▄▄ ▄▄▄
27505
- ██▄█▀ ██ ██▄██ ██▄█▀ ██ ██▀██
27506
- ██ ██ ██▄█▀ ██ ██ ██ ▀███▀
27507
-
27508
- `)
27509
- .split('\n')
27510
- .map((line) => centerAnsiText(line, options.totalWidth));
27839
+ function buildCoderRunUiTerminalFrameUpdate(options) {
27840
+ const { previousFrameLines, nextFrameLines } = options;
27841
+ if (previousFrameLines.length === 0 && nextFrameLines.length === 0) {
27842
+ return undefined;
27843
+ }
27844
+ if (previousFrameLines.length === 0 || previousFrameLines.length !== nextFrameLines.length) {
27845
+ return buildFullCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines);
27846
+ }
27847
+ return buildChangedCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines);
27848
+ }
27849
+ /**
27850
+ * Builds an ANSI update which completely rewrites the reserved terminal frame area.
27851
+ *
27852
+ * @private helper of `buildCoderRunUiTerminalFrameUpdate`
27853
+ */
27854
+ function buildFullCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines) {
27855
+ var _a;
27856
+ const previousFrameLineCount = previousFrameLines.length;
27857
+ const lineCountToRewrite = Math.max(previousFrameLineCount, nextFrameLines.length);
27858
+ const outputParts = [];
27859
+ if (previousFrameLineCount > 1) {
27860
+ outputParts.push(moveCursorUp(previousFrameLineCount - 1));
27861
+ }
27862
+ for (let lineIndex = 0; lineIndex < lineCountToRewrite; lineIndex++) {
27863
+ outputParts.push(CLEAR_CURRENT_LINE, MOVE_CURSOR_TO_LINE_START, (_a = nextFrameLines[lineIndex]) !== null && _a !== void 0 ? _a : '');
27864
+ if (lineIndex < lineCountToRewrite - 1) {
27865
+ outputParts.push('\n');
27866
+ }
27867
+ }
27868
+ const clearedTrailingLineCount = lineCountToRewrite - nextFrameLines.length;
27869
+ if (clearedTrailingLineCount > 0) {
27870
+ outputParts.push(moveCursorUp(clearedTrailingLineCount));
27871
+ }
27872
+ outputParts.push(MOVE_CURSOR_TO_LINE_START);
27873
+ return outputParts.join('');
27874
+ }
27875
+ /**
27876
+ * Builds an ANSI update that rewrites only contiguous groups of rows which changed.
27877
+ *
27878
+ * @private helper of `buildCoderRunUiTerminalFrameUpdate`
27879
+ */
27880
+ function buildChangedCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines) {
27881
+ const outputParts = [];
27882
+ let lineIndex = 0;
27883
+ while (lineIndex < nextFrameLines.length) {
27884
+ if (previousFrameLines[lineIndex] === nextFrameLines[lineIndex]) {
27885
+ lineIndex++;
27886
+ continue;
27887
+ }
27888
+ const changedRangeStartLineIndex = lineIndex;
27889
+ while (lineIndex + 1 < nextFrameLines.length &&
27890
+ previousFrameLines[lineIndex + 1] !== nextFrameLines[lineIndex + 1]) {
27891
+ lineIndex++;
27892
+ }
27893
+ appendChangedCoderRunUiTerminalFrameRange({
27894
+ outputParts,
27895
+ frameLineCount: nextFrameLines.length,
27896
+ changedRangeStartLineIndex,
27897
+ changedRangeEndLineIndex: lineIndex,
27898
+ nextFrameLines,
27899
+ });
27900
+ lineIndex++;
27901
+ }
27902
+ return outputParts.length === 0 ? undefined : outputParts.join('');
27903
+ }
27904
+ /**
27905
+ * Appends the cursor movements and row rewrites for one contiguous changed frame range.
27906
+ *
27907
+ * @private helper of `buildChangedCoderRunUiTerminalFrameUpdate`
27908
+ */
27909
+ function appendChangedCoderRunUiTerminalFrameRange(options) {
27910
+ const { outputParts, frameLineCount, changedRangeStartLineIndex, changedRangeEndLineIndex, nextFrameLines } = options;
27911
+ const linesUpFromFrameBottom = frameLineCount - 1 - changedRangeStartLineIndex;
27912
+ if (linesUpFromFrameBottom > 0) {
27913
+ outputParts.push(moveCursorUp(linesUpFromFrameBottom));
27914
+ }
27915
+ for (let lineIndex = changedRangeStartLineIndex; lineIndex <= changedRangeEndLineIndex; lineIndex++) {
27916
+ outputParts.push(CLEAR_CURRENT_LINE, MOVE_CURSOR_TO_LINE_START, nextFrameLines[lineIndex]);
27917
+ if (lineIndex < changedRangeEndLineIndex) {
27918
+ outputParts.push('\n');
27919
+ }
27920
+ }
27921
+ const linesDownToFrameBottom = frameLineCount - 1 - changedRangeEndLineIndex;
27922
+ if (linesDownToFrameBottom > 0) {
27923
+ outputParts.push(moveCursorDown(linesDownToFrameBottom));
27924
+ }
27925
+ outputParts.push(MOVE_CURSOR_TO_LINE_START);
27926
+ }
27927
+ /**
27928
+ * Builds an ANSI sequence which moves the terminal cursor up by a positive number of rows.
27929
+ *
27930
+ * @private helper of `buildCoderRunUiTerminalFrameUpdate`
27931
+ */
27932
+ function moveCursorUp(lineCount) {
27933
+ return `\x1b[${lineCount}A`;
27934
+ }
27935
+ /**
27936
+ * Builds an ANSI sequence which moves the terminal cursor down by a positive number of rows.
27937
+ *
27938
+ * @private helper of `buildCoderRunUiTerminalFrameUpdate`
27939
+ */
27940
+ function moveCursorDown(lineCount) {
27941
+ return `\x1b[${lineCount}B`;
27942
+ }
27943
+
27944
+ /**
27945
+ * Calendar formats used when displaying the estimated completion time.
27946
+ *
27947
+ * Uses 24-hour format (`HH:mm`) so estimates are unambiguous (for example `17:30` instead of `5:30`).
27948
+ */
27949
+ const ESTIMATED_DONE_CALENDAR_FORMATS = {
27950
+ sameDay: '[Today] HH:mm',
27951
+ nextDay: '[Tomorrow] HH:mm',
27952
+ nextWeek: 'dddd HH:mm',
27953
+ lastDay: '[Yesterday] HH:mm',
27954
+ lastWeek: 'dddd HH:mm',
27955
+ sameElse: 'MMM D HH:mm',
27956
+ };
27957
+ /**
27958
+ * Formats a duration into a compact string such as "3h 12m" or "45s".
27959
+ */
27960
+ function formatDurationBrief(duration) {
27961
+ const totalSeconds = Math.max(0, Math.round(duration.asSeconds()));
27962
+ const hours = Math.floor(totalSeconds / 3600);
27963
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
27964
+ const seconds = totalSeconds % 60;
27965
+ const parts = [];
27966
+ if (hours > 0) {
27967
+ parts.push(`${hours}h`);
27968
+ }
27969
+ if (minutes > 0) {
27970
+ parts.push(`${minutes}m`);
27971
+ }
27972
+ if (!parts.length && seconds > 0) {
27973
+ parts.push(`${seconds}s`);
27974
+ }
27975
+ if (!parts.length) {
27976
+ parts.push('0s');
27977
+ }
27978
+ return parts.join(' ');
27979
+ }
27980
+
27981
+ /**
27982
+ * Builds a session-scoped progress snapshot from prompt stats and elapsed active time.
27983
+ *
27984
+ * When the current session has not finished any prompt yet but a `cachedAveragePromptDurationMs` is
27985
+ * provided (loaded from the per-config estimate cache stored in the temp folder), the snapshot uses
27986
+ * that historical average to project the remaining work instead of waiting for the first completion.
27987
+ */
27988
+ function buildCoderRunProgressSnapshot(stats, elapsedDuration, initialDone, cachedAveragePromptDurationMs, runLimit) {
27989
+ const totalPrompts = stats.done + stats.forAgent + stats.toBeWritten;
27990
+ const completedSessionPrompts = Math.max(0, stats.done - initialDone);
27991
+ const availableSessionTotal = completedSessionPrompts + stats.forAgent;
27992
+ const sessionTotal = resolveSessionTotal(availableSessionTotal, runLimit);
27993
+ const sessionDone = Math.min(completedSessionPrompts, sessionTotal);
27994
+ const sessionRemaining = Math.max(0, sessionTotal - sessionDone);
27995
+ const currentPromptIndex = sessionTotal > 0 ? Math.min(sessionDone + 1, sessionTotal) : 0;
27996
+ const percentage = sessionTotal > 0 ? Math.round((sessionDone / sessionTotal) * 100) : 0;
27997
+ const elapsedText = formatDurationBrief(elapsedDuration);
27998
+ const elapsedMs = elapsedDuration.asMilliseconds();
27999
+ let estimatedTotalText = 'estimating...';
28000
+ let estimatedLabel = 'after first completion';
28001
+ let isEstimatedTotalKnown = false;
28002
+ let estimatedTotalMs;
28003
+ if (sessionTotal > 0 && sessionDone > 0) {
28004
+ estimatedTotalMs = (elapsedMs * sessionTotal) / sessionDone;
28005
+ }
28006
+ else if (sessionTotal > 0 &&
28007
+ cachedAveragePromptDurationMs !== undefined &&
28008
+ cachedAveragePromptDurationMs > 0) {
28009
+ const projectedRemainingMs = sessionRemaining * cachedAveragePromptDurationMs;
28010
+ estimatedTotalMs = elapsedMs + projectedRemainingMs;
28011
+ }
28012
+ if (estimatedTotalMs !== undefined) {
28013
+ const estimatedRemainingMs = Math.max(0, estimatedTotalMs - elapsedMs);
28014
+ const estimatedTotalDuration = moment.duration(estimatedTotalMs);
28015
+ const estimatedCompletion = moment().add(estimatedRemainingMs, 'milliseconds');
28016
+ estimatedTotalText = formatDurationBrief(estimatedTotalDuration);
28017
+ estimatedLabel = estimatedCompletion.calendar(null, ESTIMATED_DONE_CALENDAR_FORMATS);
28018
+ isEstimatedTotalKnown = true;
28019
+ }
28020
+ return {
28021
+ totalPrompts,
28022
+ sessionDone,
28023
+ sessionRemaining,
28024
+ sessionTotal,
28025
+ currentPromptIndex,
28026
+ skippedPrompts: stats.outsidePriorityRange,
28027
+ toBeWrittenPrompts: stats.toBeWritten,
28028
+ percentage,
28029
+ elapsedText,
28030
+ estimatedTotalText,
28031
+ estimatedLabel,
28032
+ isEstimatedTotalKnown,
28033
+ };
28034
+ }
28035
+ /**
28036
+ * Resolves the number of prompt runs represented by the current session progress.
28037
+ */
28038
+ function resolveSessionTotal(availableSessionTotal, runLimit) {
28039
+ if (runLimit === undefined) {
28040
+ return availableSessionTotal;
28041
+ }
28042
+ return Math.min(availableSessionTotal, runLimit);
28043
+ }
28044
+
28045
+ /**
28046
+ * Tracks active coder-run time while excluding pauses and user-confirmation waits.
28047
+ */
28048
+ class CoderRunTimer {
28049
+ /**
28050
+ * Creates a timer anchored at the provided start time.
28051
+ */
28052
+ constructor(startTime, isPausedInitially = false) {
28053
+ this.startTime = startTime;
28054
+ /**
28055
+ * Total milliseconds spent in paused state across the run.
28056
+ */
28057
+ this.pausedMs = 0;
28058
+ if (isPausedInitially) {
28059
+ this.pausedSince = startTime.clone();
28060
+ }
28061
+ }
28062
+ /**
28063
+ * Pauses active-time tracking until `resume()` is called.
28064
+ */
28065
+ pause() {
28066
+ if (this.pausedSince === undefined) {
28067
+ this.pausedSince = moment();
28068
+ }
28069
+ }
28070
+ /**
28071
+ * Resumes active-time tracking after a pause.
28072
+ */
28073
+ resume() {
28074
+ if (this.pausedSince !== undefined) {
28075
+ this.pausedMs += moment().diff(this.pausedSince);
28076
+ this.pausedSince = undefined;
28077
+ }
28078
+ }
28079
+ /**
28080
+ * Returns the currently accumulated active duration.
28081
+ */
28082
+ getElapsedDuration() {
28083
+ const wallMs = moment().diff(this.startTime);
28084
+ const currentPauseMs = this.pausedSince !== undefined ? moment().diff(this.pausedSince) : 0;
28085
+ return moment.duration(Math.max(0, wallMs - this.pausedMs - currentPauseMs));
28086
+ }
28087
+ }
28088
+
28089
+ /**
28090
+ * Maximum number of agent output lines kept in the scrolling output area.
28091
+ *
28092
+ * @private internal constant of coder run UI
28093
+ */
28094
+ const MAX_AGENT_OUTPUT_LINES = 12;
28095
+ /**
28096
+ * Reactive state manager for the coder run terminal UI.
28097
+ *
28098
+ * Holds all data the Ink components need and emits `'change'` events
28099
+ * whenever any property is updated so the UI can re-render.
28100
+ *
28101
+ * @private internal utility of coder run UI
28102
+ */
28103
+ class CoderRunUiState extends EventEmitter {
28104
+ constructor(startTime) {
28105
+ super();
28106
+ this.config = { agentName: '' };
28107
+ this.currentPromptLabel = '';
28108
+ this.currentScriptPaths = [];
28109
+ this.currentAttempt = 1;
28110
+ this.maxAttempts = 3;
28111
+ this.detailLines = [];
28112
+ this.messagePreviewLines = [];
28113
+ this.messagePreviewSections = [];
28114
+ this.agentStatusLines = [];
28115
+ this.agentStatusTableRows = [];
28116
+ this.agentOutputLines = [];
28117
+ this.phase = 'initializing';
28118
+ this.statusMessage = 'Initializing...';
28119
+ this.errors = [];
28120
+ this.stats = { done: 0, forAgent: 0, outsidePriorityRange: 0, toBeWritten: 0 };
28121
+ // Match the plain CLI progress header and count active run time from startup.
28122
+ // Explicit waits/pauses are excluded later through `pauseTimer()` / `resumeTimer()`.
28123
+ this.timer = new CoderRunTimer(startTime);
28124
+ }
28125
+ /**
28126
+ * Pauses the elapsed timer (e.g. while waiting for user input or paused state).
28127
+ */
28128
+ pauseTimer() {
28129
+ this.timer.pause();
28130
+ this.emitChange();
28131
+ }
28132
+ /**
28133
+ * Resumes the elapsed timer after a pause.
28134
+ */
28135
+ resumeTimer() {
28136
+ this.timer.resume();
28137
+ this.emitChange();
28138
+ }
28139
+ /**
28140
+ * Replaces the configuration shown in the UI header.
28141
+ */
28142
+ setConfig(config) {
28143
+ this.config = config;
28144
+ this.emitChange();
28145
+ }
28146
+ /**
28147
+ * Replaces the ASCII-art agent visual renderer shown above the dashboard boxes.
28148
+ */
28149
+ setAgentVisual(agentVisual) {
28150
+ this.agentVisual = agentVisual;
28151
+ this.emitChange();
28152
+ }
28153
+ /**
28154
+ * Feeds new prompt statistics from the main loop so the progress bar updates.
28155
+ */
28156
+ updateProgress(stats) {
28157
+ if (this.initialDone === undefined && (stats.done > 0 || stats.forAgent > 0 || stats.toBeWritten > 0)) {
28158
+ this.initialDone = stats.done;
28159
+ }
28160
+ this.stats = stats;
28161
+ this.emitChange();
28162
+ }
28163
+ /**
28164
+ * Sets the cached average prompt duration (in milliseconds) loaded from the temp-folder estimate cache.
28165
+ *
28166
+ * Used to project completion estimates before the first prompt of the current session finishes.
28167
+ */
28168
+ setCachedAveragePromptDurationMs(cachedAveragePromptDurationMs) {
28169
+ this.cachedAveragePromptDurationMs = cachedAveragePromptDurationMs;
28170
+ this.emitChange();
28171
+ }
28172
+ /**
28173
+ * Computes a progress snapshot on demand so elapsed time ticks with periodic re-renders.
28174
+ */
28175
+ getProgress() {
28176
+ var _a;
28177
+ return buildCoderRunProgressSnapshot(this.stats, this.timer.getElapsedDuration(), (_a = this.initialDone) !== null && _a !== void 0 ? _a : this.stats.done, this.cachedAveragePromptDurationMs, this.config.limit);
28178
+ }
28179
+ /**
28180
+ * Sets the label of the prompt currently being processed and resets per-prompt state.
28181
+ */
28182
+ setCurrentPrompt(label) {
28183
+ this.currentPromptLabel = label;
28184
+ this.currentScriptPaths = [];
28185
+ this.detailLines = [];
28186
+ this.messagePreviewLines = [];
28187
+ this.messagePreviewSections = [];
28188
+ this.pendingEnterLabel = undefined;
28189
+ this.agentOutputLines = [];
28190
+ this.currentAttempt = 1;
28191
+ this.emitChange();
28192
+ }
28193
+ /**
28194
+ * Sets the temporary shell script currently used by the active prompt.
28195
+ */
28196
+ setCurrentScriptPath(scriptPath) {
28197
+ this.currentScriptPaths = scriptPath ? [scriptPath] : [];
28198
+ this.emitChange();
28199
+ }
28200
+ /**
28201
+ * Sets multiple temporary shell scripts used by a shared multi-agent session.
28202
+ */
28203
+ setCurrentScriptPaths(scriptPaths) {
28204
+ this.currentScriptPaths = [...scriptPaths];
28205
+ this.emitChange();
28206
+ }
28207
+ /**
28208
+ * Updates the current retry attempt number.
28209
+ */
28210
+ setAttempt(attempt) {
28211
+ this.currentAttempt = attempt;
28212
+ this.emitChange();
28213
+ }
28214
+ /**
28215
+ * Appends raw agent output text, keeping only the last `MAX_AGENT_OUTPUT_LINES`.
28216
+ */
28217
+ addAgentOutput(text) {
28218
+ const lines = text.split(/\r?\n/).filter((line) => line.trim() !== '');
28219
+ if (lines.length === 0) {
28220
+ return;
28221
+ }
28222
+ this.agentOutputLines.push(...lines);
28223
+ if (this.agentOutputLines.length > MAX_AGENT_OUTPUT_LINES) {
28224
+ this.agentOutputLines = this.agentOutputLines.slice(-MAX_AGENT_OUTPUT_LINES);
28225
+ }
28226
+ this.emitChange();
28227
+ }
28228
+ /**
28229
+ * Transitions the execution phase shown in the UI.
28230
+ */
28231
+ setPhase(phase) {
28232
+ this.phase = phase;
28233
+ this.emitChange();
28234
+ }
28235
+ /**
28236
+ * Updates the status message line beneath the current prompt label.
28237
+ */
28238
+ setStatusMessage(message) {
28239
+ this.statusMessage = message;
28240
+ this.emitChange();
28241
+ }
28242
+ /**
28243
+ * Replaces the contextual detail lines shown beneath the current prompt status.
28244
+ */
28245
+ setDetailLines(detailLines) {
28246
+ this.detailLines = detailLines.filter((detailLine) => detailLine.trim() !== '');
28247
+ this.emitChange();
28248
+ }
28249
+ /**
28250
+ * Replaces the exact user-message preview lines shown in agent-specific panels.
28251
+ */
28252
+ setMessagePreviewLines(messagePreviewLines) {
28253
+ this.messagePreviewLines = [...messagePreviewLines];
28254
+ this.emitChange();
28255
+ }
28256
+ /**
28257
+ * Replaces the structured user-message panels shown in agent-specific dashboards.
28258
+ */
28259
+ setMessagePreviewSections(messagePreviewSections) {
28260
+ this.messagePreviewSections = [...messagePreviewSections];
28261
+ this.emitChange();
28262
+ }
28263
+ /**
28264
+ * Replaces agent status rows shown by agent-specific terminal frames.
28265
+ */
28266
+ setAgentStatusLines(agentStatusLines) {
28267
+ this.agentStatusLines = [...agentStatusLines];
28268
+ this.emitChange();
28269
+ }
28270
+ /**
28271
+ * Replaces structured agent status rows shown by agent-specific terminal frames.
28272
+ */
28273
+ setAgentStatusTableRows(agentStatusTableRows) {
28274
+ this.agentStatusTableRows = [...agentStatusTableRows];
28275
+ this.emitChange();
28276
+ }
28277
+ /**
28278
+ * Sets or clears the Enter-key action label shown in the controls panel.
28279
+ */
28280
+ setPendingEnterLabel(pendingEnterLabel) {
28281
+ this.pendingEnterLabel = pendingEnterLabel;
28282
+ this.emitChange();
28283
+ }
28284
+ /**
28285
+ * Shows the answer of the runner to one pressed control key, or clears the currently shown one.
28286
+ *
28287
+ * Pressing one control twice in a row can produce the very same answer, for example when there is
28288
+ * nothing to skip. The repeat count is therefore tracked here, so that even such a press changes the
28289
+ * rendered frame and the user always sees that the key arrived.
28290
+ */
28291
+ setControlFeedback(controlFeedback) {
28292
+ this.controlFeedback =
28293
+ controlFeedback === undefined
28294
+ ? undefined
28295
+ : { ...controlFeedback, repeatCount: this.countControlFeedbackRepeat(controlFeedback) };
28296
+ this.emitChange();
28297
+ }
28298
+ /**
28299
+ * Appends an error message to the error list shown in the UI.
28300
+ */
28301
+ addError(errorMessage) {
28302
+ this.errors.push(errorMessage);
28303
+ this.emitChange();
28304
+ }
28305
+ /**
28306
+ * Counts how many times the shown answer repeats the previously shown one.
28307
+ */
28308
+ countControlFeedbackRepeat(controlFeedback) {
28309
+ const previousControlFeedback = this.controlFeedback;
28310
+ const isSameControlFeedback = previousControlFeedback !== undefined &&
28311
+ previousControlFeedback.controlKey === controlFeedback.controlKey &&
28312
+ previousControlFeedback.message === controlFeedback.message;
28313
+ return isSameControlFeedback ? previousControlFeedback.repeatCount + 1 : 1;
28314
+ }
28315
+ emitChange() {
28316
+ this.emit('change');
28317
+ }
27511
28318
  }
27512
28319
 
27513
28320
  /**
@@ -28177,807 +28984,139 @@ const TERMINAL_AGENT_AVATAR_VISUAL_CANVAS_HEIGHT = DEFAULT_AVATAR_SIZE;
28177
28984
  * @param options Agent source, canvas factory, and optional terminal color settings.
28178
28985
  * @returns Runtime terminal avatar visual renderer.
28179
28986
  *
28180
- * @private shared helper for terminal avatar rendering
28181
- */
28182
- function createTerminalAgentAvatarVisual(options) {
28183
- const renderInputs = createTerminalAgentAvatarVisualRenderInputs(options.agentSource, options.defaultAvatarVisualId || DEFAULT_AGENT_AVATAR_VISUAL_ID);
28184
- return {
28185
- isAnimated: renderInputs.resolvedAvatarRenderDefinition.avatarVisual.isAnimated,
28186
- renderFrame({ animationTimeMs }) {
28187
- return renderTerminalAgentAvatarVisualFrame({
28188
- ...renderInputs,
28189
- animationTimeMs,
28190
- colorDepth: options.colorDepth,
28191
- createCanvas: options.createCanvas,
28192
- });
28193
- },
28194
- };
28195
- }
28196
- /**
28197
- * Creates stable avatar render inputs from one agent source.
28198
- *
28199
- * @private shared helper for terminal avatar rendering
28200
- */
28201
- function createTerminalAgentAvatarVisualRenderInputs(agentSource, defaultAvatarVisualId) {
28202
- const agentBasicInformation = parseAgentSource(agentSource);
28203
- const avatarDefinition = createAvatarDefinitionFromAgentBasicInformation(agentBasicInformation);
28204
- const avatarVisualId = resolveTerminalAgentAvatarVisualIdFromAgentBasicInformation(agentBasicInformation, defaultAvatarVisualId);
28205
- const resolvedAvatarRenderDefinition = resolveAvatarRenderDefinition({
28206
- avatarDefinition,
28207
- visualId: avatarVisualId,
28208
- surface: 'transparent',
28209
- });
28210
- return {
28211
- avatarDefinition,
28212
- avatarVisualId,
28213
- resolvedAvatarRenderDefinition,
28214
- };
28215
- }
28216
- /**
28217
- * Resolves the terminal avatar visual id from already parsed agent information.
28218
- *
28219
- * @private shared helper for terminal avatar rendering
28220
- */
28221
- function resolveTerminalAgentAvatarVisualIdFromAgentBasicInformation(agentBasicInformation, defaultAvatarVisualId) {
28222
- return resolveAgentAvatarVisualId(agentBasicInformation, resolveAvatarVisualId(agentBasicInformation.meta.visual) || defaultAvatarVisualId);
28223
- }
28224
- /**
28225
- * Renders one terminal avatar frame through the shared avatar-to-ASCII pipeline.
28226
- *
28227
- * Visuals which are made of characters themselves, for example `AsciiOctopus`, paint the terminal
28228
- * character grid directly, because rasterizing them into half-block ASCII art averages every glyph
28229
- * away and makes them indistinguishable from the other blob-shaped visuals.
28230
- *
28231
- * @private shared helper for terminal avatar rendering
28232
- */
28233
- function renderTerminalAgentAvatarVisualFrame(options) {
28234
- const terminalTextLines = renderAvatarVisualTerminalTextLines({
28235
- avatarDefinition: options.avatarDefinition,
28236
- visualId: options.avatarVisualId,
28237
- surface: 'transparent',
28238
- columns: TERMINAL_AGENT_AVATAR_VISUAL_COLUMNS,
28239
- rows: TERMINAL_AGENT_AVATAR_VISUAL_ROWS,
28240
- colorDepth: options.colorDepth,
28241
- timeMs: options.animationTimeMs,
28242
- resolvedAvatarRenderDefinition: options.resolvedAvatarRenderDefinition,
28243
- });
28244
- if (terminalTextLines !== null) {
28245
- return terminalTextLines;
28246
- }
28247
- if (options.createCanvas === undefined) {
28248
- // Note: A pixel-based visual cannot be rasterized without a canvas, so the caller keeps its default banner
28249
- return [];
28250
- }
28251
- return renderAvatarVisualAsciiArt({
28252
- avatarDefinition: options.avatarDefinition,
28253
- visualId: options.avatarVisualId,
28254
- surface: 'transparent',
28255
- columns: TERMINAL_AGENT_AVATAR_VISUAL_COLUMNS,
28256
- rows: TERMINAL_AGENT_AVATAR_VISUAL_ROWS,
28257
- canvasWidth: TERMINAL_AGENT_AVATAR_VISUAL_CANVAS_WIDTH,
28258
- canvasHeight: TERMINAL_AGENT_AVATAR_VISUAL_CANVAS_HEIGHT,
28259
- colorDepth: options.colorDepth,
28260
- timeMs: options.animationTimeMs,
28261
- createCanvas: options.createCanvas,
28262
- resolvedAvatarRenderDefinition: options.resolvedAvatarRenderDefinition,
28263
- });
28264
- }
28265
-
28266
- /**
28267
- * Refresh cadence used only while the rich coder UI needs animated updates.
28268
- *
28269
- * @private internal constant of coder run UI
28270
- */
28271
- const ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS = TERMINAL_AGENT_AVATAR_VISUAL_REFRESH_INTERVAL_MS;
28272
- /**
28273
- * Phases that still benefit from automatic refreshes because the frame can change
28274
- * over time even without new runner output.
28275
- *
28276
- * @private internal constant of coder run UI
28277
- */
28278
- const AUTO_REFRESH_PHASES = ['initializing', 'loading', 'running', 'verifying'];
28279
- /**
28280
- * Phases where an animated `--agent` visual should keep rendering like it does on the web.
28281
- *
28282
- * @private internal constant of coder run UI
28283
- */
28284
- const AGENT_VISUAL_AUTO_REFRESH_PHASES = [...AUTO_REFRESH_PHASES, 'waiting'];
28285
- /**
28286
- * Returns whether the rich coder UI should keep animating on its own.
28287
- *
28288
- * @private internal utility of coder run UI
28289
- */
28290
- function isCoderRunUiAutoRefreshing(phase, pauseState, isAgentVisualAnimated = false) {
28291
- // `PAUSING` still means the current task is winding down, so keep active
28292
- // animations/timers running until the runner reaches the fully paused state.
28293
- if (pauseState === 'PAUSED') {
28294
- return false;
28295
- }
28296
- if (isAgentVisualAnimated) {
28297
- return AGENT_VISUAL_AUTO_REFRESH_PHASES.includes(phase);
28298
- }
28299
- return AUTO_REFRESH_PHASES.includes(phase);
28300
- }
28301
- /**
28302
- * Returns the automatic refresh interval for the current UI state.
28303
- *
28304
- * Waiting, paused, and completed states return `undefined` so the rich UI stays
28305
- * perfectly still until actual state changes arrive.
28306
- *
28307
- * @private internal utility of coder run UI
28308
- */
28309
- function getCoderRunUiAutoRefreshInterval(phase, pauseState, isAgentVisualAnimated = false) {
28310
- return isCoderRunUiAutoRefreshing(phase, pauseState, isAgentVisualAnimated)
28311
- ? ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS
28312
- : undefined;
28313
- }
28314
-
28315
- /**
28316
- * Minimum width used for the rich coder-run frame.
28317
- */
28318
- const MIN_FRAME_WIDTH = 56;
28319
- /**
28320
- * Maximum width used for the rich coder-run frame.
28321
- */
28322
- const MAX_FRAME_WIDTH = 96;
28323
- /**
28324
- * Builds the complete boxed terminal frame for the rich `ptbk coder run` UI.
28325
- */
28326
- function buildCoderRunUiFrame(options) {
28327
- const totalWidth = Math.max(MIN_FRAME_WIDTH, Math.min(options.terminalWidth, MAX_FRAME_WIDTH));
28328
- const isPromptActive = options.phase === 'running' || options.phase === 'verifying' || options.phase === 'loading';
28329
- const promptStatusPrefix = isPromptActive ? `${colors.yellow(`${options.spinner} `)}` : '';
28330
- const octopusAnimationFrame = isCoderRunUiAutoRefreshing(options.phase, options.pauseState)
28331
- ? options.animationFrame
28332
- : 0;
28333
- const pausePresentation = buildPausePresentation(options.phase, options.pauseState, options.pauseTargetLabel, options.statusMessage);
28334
- const sessionLines = buildSessionLines(options, totalWidth, pausePresentation);
28335
- const currentTaskLines = options.currentPromptLabel
28336
- ? [
28337
- `${promptStatusPrefix}${colors.bold.white(fitPlainText(options.currentPromptLabel, totalWidth - 8))}`,
28338
- `Attempt ${options.currentAttempt}/${options.maxAttempts} · ${options.statusMessage}`,
28339
- ...options.detailLines.map((detailLine) => `• ${detailLine}`),
28340
- ]
28341
- : [options.statusMessage, ...options.detailLines.map((detailLine) => `• ${detailLine}`)];
28342
- const visibleOutputLines = buildVisibleOutputLines(options.agentOutputLines);
28343
- const controls = buildCoderRunControlPills({
28344
- phase: options.phase,
28345
- pauseControl: pausePresentation.pauseControl,
28346
- pendingEnterLabel: options.pendingEnterLabel,
28347
- isEndAfterCurrentPromptRequested: options.isEndAfterCurrentPromptRequested,
28348
- sessionTotal: options.progress.sessionTotal,
28349
- }).join(' ');
28350
- const frame = [
28351
- ...buildFrameHeaderVisual(options, totalWidth, octopusAnimationFrame),
28352
- '',
28353
- ...renderBox('Session', sessionLines, totalWidth, colors.yellow.bold),
28354
- ...renderBox(options.currentPromptLabel ? 'Current task' : 'Queue', currentTaskLines, totalWidth, colors.magenta.bold),
28355
- ...renderBox('Live output', visibleOutputLines, totalWidth, colors.green.bold),
28356
- ];
28357
- if (options.errors.length > 0) {
28358
- const bodyWidth = Math.max(10, totalWidth - 4);
28359
- frame.push(...renderBox('Errors', buildErrorDisplayLines(options.errors, bodyWidth), totalWidth, colors.red.bold));
28360
- }
28361
- frame.push(...renderBox('Controls', [controls], totalWidth, colors.white.bold));
28362
- return frame;
28363
- }
28364
- /**
28365
- * Builds the header visual above the dashboard boxes.
28366
- *
28367
- * Shows the ASCII-art `--agent` avatar visual when one is available and
28368
- * falls back to the default brand banner otherwise.
28369
- */
28370
- function buildFrameHeaderVisual(options, totalWidth, octopusAnimationFrame) {
28371
- if (options.agentVisualLines !== undefined && options.agentVisualLines.length > 0) {
28372
- return options.agentVisualLines.map((agentVisualLine) => centerAnsiText(agentVisualLine, totalWidth));
28373
- }
28374
- if (options.agentVisual !== undefined) {
28375
- const agentVisualLines = options.agentVisual.renderFrame({
28376
- animationTimeMs: options.animationTimeMs,
28377
- });
28378
- if (agentVisualLines.length > 0) {
28379
- return agentVisualLines.map((agentVisualLine) => centerAnsiText(agentVisualLine, totalWidth));
28380
- }
28381
- }
28382
- return buildCoderRunOctopusVisual({ totalWidth, animationFrame: octopusAnimationFrame });
28383
- }
28384
- /**
28385
- * Builds the structured session lines that combine state, runner, queue, and timing metadata.
28386
- */
28387
- function buildSessionLines(options, totalWidth, pausePresentation) {
28388
- const bodyWidth = Math.max(10, totalWidth - 4);
28389
- return buildSessionRows(options, bodyWidth, pausePresentation).map((sessionRow) => buildLabeledSessionLine(sessionRow.label, sessionRow.value, bodyWidth));
28390
- }
28391
- /**
28392
- * Builds the session rows so the renderer can keep one consistent structure without duplicating labels.
28393
- */
28394
- function buildSessionRows(options, bodyWidth, pausePresentation) {
28395
- const runnerParts = [options.config.agentName || 'No agent selected'];
28396
- if (options.config.modelName) {
28397
- runnerParts.push(options.config.modelName);
28398
- }
28399
- if (options.config.thinkingLevel) {
28400
- runnerParts.push(`thinking ${options.config.thinkingLevel}`);
28401
- }
28402
- const configurationRows = [
28403
- ...buildOptionalSessionRow('Context', options.config.context),
28404
- ...buildOptionalSessionRow('Server', options.config.serverUrl
28405
- ? buildTerminalUrlLink(options.config.serverUrl, bodyWidth - SESSION_LABEL_WIDTH - 1)
28406
- : undefined),
28407
- ...buildOptionalSessionRow('Test', options.config.testCommand),
28408
- ];
28409
- return [
28410
- {
28411
- label: 'State',
28412
- value: `${pausePresentation.badge} ${pausePresentation.stateMessage}`,
28413
- },
28414
- {
28415
- label: 'Runner',
28416
- value: runnerParts.join(' · '),
28417
- },
28418
- ...configurationRows,
28419
- ...buildScriptPathSessionRows(options.currentScriptPaths || [], bodyWidth),
28420
- {
28421
- label: 'This run',
28422
- value: buildThisRunSummary(options.progress),
28423
- },
28424
- {
28425
- label: 'Backlog',
28426
- value: buildBacklogSummary(options.progress),
28427
- },
28428
- {
28429
- label: 'Scope',
28430
- value: buildScopeSummary(options.progress, options.config),
28431
- },
28432
- {
28433
- label: 'Timing',
28434
- value: buildTimingSummary(options.progress),
28435
- },
28436
- {
28437
- label: 'Progress',
28438
- value: buildProgressBar$1(options.progress.percentage, bodyWidth - SESSION_LABEL_WIDTH - 1, `${options.progress.percentage}% complete (${options.progress.sessionDone}/${options.progress.sessionTotal} done)`),
28439
- },
28440
- ];
28441
- }
28442
- /**
28443
- * Builds zero or one structured session row for optional metadata.
28444
- */
28445
- function buildOptionalSessionRow(label, value) {
28446
- if (!value) {
28447
- return [];
28448
- }
28449
- return [{ label, value }];
28450
- }
28451
- /**
28452
- * Builds the active-session summary shown in the session box.
28453
- */
28454
- function buildThisRunSummary(progress) {
28455
- if (progress.sessionTotal === 0) {
28456
- return 'No runnable prompts in current scope';
28457
- }
28458
- return `Task ${progress.currentPromptIndex}/${progress.sessionTotal} · ${progress.sessionDone} done · ${progress.sessionRemaining} left`;
28459
- }
28460
- /**
28461
- * Builds the backlog/filter summary shown in the session box.
28462
- */
28463
- function buildBacklogSummary(progress) {
28464
- const parts = [`Repo ${progress.totalPrompts} total`];
28465
- if (progress.skippedPrompts > 0) {
28466
- parts.push(`${formatPromptCount$1(progress.skippedPrompts)} outside priority scope`);
28467
- }
28468
- return parts.join(' · ');
28469
- }
28470
- /**
28471
- * Builds the priority/write-order summary shown in the session box.
28472
- */
28473
- function buildScopeSummary(progress, config) {
28474
- var _a;
28475
- const parts = [formatPriorityFilter((_a = config.priorityFilter) !== null && _a !== void 0 ? _a : {})];
28476
- if (config.limit !== undefined) {
28477
- parts.push(`Limit ${formatPromptRunCount(config.limit)}`);
28478
- }
28479
- if (progress.toBeWrittenPrompts > 0) {
28480
- parts.push(`Write ${formatPromptCount$1(progress.toBeWrittenPrompts)} first`);
28481
- }
28482
- return parts.join(' · ');
28483
- }
28484
- /**
28485
- * Builds the elapsed/estimate summary shown in the session box.
28486
- */
28487
- function buildTimingSummary(progress) {
28488
- return `Elapsed ${progress.elapsedText} · Total ${progress.estimatedTotalText} · ETA ${progress.estimatedLabel}`;
28489
- }
28490
- /**
28491
- * Formats a prompt count with singular/plural wording.
28492
- */
28493
- function formatPromptCount$1(count) {
28494
- return `${count} prompt${count === 1 ? '' : 's'}`;
28495
- }
28496
- /**
28497
- * Formats a configured successful prompt-run limit with singular/plural wording.
28498
- */
28499
- function formatPromptRunCount(count) {
28500
- return `${count} prompt run${count === 1 ? '' : 's'}`;
28501
- }
28502
-
28503
- /**
28504
- * ANSI sequence that clears the current terminal row.
28505
- *
28506
- * @private internal constant of coder run UI
28507
- */
28508
- const CLEAR_CURRENT_LINE = '\x1b[2K';
28509
- /**
28510
- * ANSI control character that moves the cursor to the start of its current row.
28511
- *
28512
- * @private internal constant of coder run UI
28513
- */
28514
- const MOVE_CURSOR_TO_LINE_START = '\r';
28515
- /**
28516
- * Builds one atomic ANSI update that changes the displayed coder-run terminal frame.
28517
- *
28518
- * A change in frame height requires a full rewrite to reserve or clear rows correctly. Frames with
28519
- * the same height only rewrite contiguous groups of changed rows, returning to the frame bottom
28520
- * between groups so each group can be assembled into the same output payload safely.
28521
- *
28522
- * @returns One terminal output payload, or `undefined` when the frame is already up to date.
28523
- *
28524
- * @private internal utility of coder run UI
28525
- */
28526
- function buildCoderRunUiTerminalFrameUpdate(options) {
28527
- const { previousFrameLines, nextFrameLines } = options;
28528
- if (previousFrameLines.length === 0 && nextFrameLines.length === 0) {
28529
- return undefined;
28530
- }
28531
- if (previousFrameLines.length === 0 || previousFrameLines.length !== nextFrameLines.length) {
28532
- return buildFullCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines);
28533
- }
28534
- return buildChangedCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines);
28535
- }
28536
- /**
28537
- * Builds an ANSI update which completely rewrites the reserved terminal frame area.
28538
- *
28539
- * @private helper of `buildCoderRunUiTerminalFrameUpdate`
28540
- */
28541
- function buildFullCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines) {
28542
- var _a;
28543
- const previousFrameLineCount = previousFrameLines.length;
28544
- const lineCountToRewrite = Math.max(previousFrameLineCount, nextFrameLines.length);
28545
- const outputParts = [];
28546
- if (previousFrameLineCount > 1) {
28547
- outputParts.push(moveCursorUp(previousFrameLineCount - 1));
28548
- }
28549
- for (let lineIndex = 0; lineIndex < lineCountToRewrite; lineIndex++) {
28550
- outputParts.push(CLEAR_CURRENT_LINE, MOVE_CURSOR_TO_LINE_START, (_a = nextFrameLines[lineIndex]) !== null && _a !== void 0 ? _a : '');
28551
- if (lineIndex < lineCountToRewrite - 1) {
28552
- outputParts.push('\n');
28553
- }
28554
- }
28555
- const clearedTrailingLineCount = lineCountToRewrite - nextFrameLines.length;
28556
- if (clearedTrailingLineCount > 0) {
28557
- outputParts.push(moveCursorUp(clearedTrailingLineCount));
28558
- }
28559
- outputParts.push(MOVE_CURSOR_TO_LINE_START);
28560
- return outputParts.join('');
28561
- }
28562
- /**
28563
- * Builds an ANSI update that rewrites only contiguous groups of rows which changed.
28564
- *
28565
- * @private helper of `buildCoderRunUiTerminalFrameUpdate`
28566
- */
28567
- function buildChangedCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines) {
28568
- const outputParts = [];
28569
- let lineIndex = 0;
28570
- while (lineIndex < nextFrameLines.length) {
28571
- if (previousFrameLines[lineIndex] === nextFrameLines[lineIndex]) {
28572
- lineIndex++;
28573
- continue;
28574
- }
28575
- const changedRangeStartLineIndex = lineIndex;
28576
- while (lineIndex + 1 < nextFrameLines.length &&
28577
- previousFrameLines[lineIndex + 1] !== nextFrameLines[lineIndex + 1]) {
28578
- lineIndex++;
28579
- }
28580
- appendChangedCoderRunUiTerminalFrameRange({
28581
- outputParts,
28582
- frameLineCount: nextFrameLines.length,
28583
- changedRangeStartLineIndex,
28584
- changedRangeEndLineIndex: lineIndex,
28585
- nextFrameLines,
28586
- });
28587
- lineIndex++;
28588
- }
28589
- return outputParts.length === 0 ? undefined : outputParts.join('');
28590
- }
28591
- /**
28592
- * Appends the cursor movements and row rewrites for one contiguous changed frame range.
28593
- *
28594
- * @private helper of `buildChangedCoderRunUiTerminalFrameUpdate`
28595
- */
28596
- function appendChangedCoderRunUiTerminalFrameRange(options) {
28597
- const { outputParts, frameLineCount, changedRangeStartLineIndex, changedRangeEndLineIndex, nextFrameLines } = options;
28598
- const linesUpFromFrameBottom = frameLineCount - 1 - changedRangeStartLineIndex;
28599
- if (linesUpFromFrameBottom > 0) {
28600
- outputParts.push(moveCursorUp(linesUpFromFrameBottom));
28601
- }
28602
- for (let lineIndex = changedRangeStartLineIndex; lineIndex <= changedRangeEndLineIndex; lineIndex++) {
28603
- outputParts.push(CLEAR_CURRENT_LINE, MOVE_CURSOR_TO_LINE_START, nextFrameLines[lineIndex]);
28604
- if (lineIndex < changedRangeEndLineIndex) {
28605
- outputParts.push('\n');
28606
- }
28607
- }
28608
- const linesDownToFrameBottom = frameLineCount - 1 - changedRangeEndLineIndex;
28609
- if (linesDownToFrameBottom > 0) {
28610
- outputParts.push(moveCursorDown(linesDownToFrameBottom));
28611
- }
28612
- outputParts.push(MOVE_CURSOR_TO_LINE_START);
28613
- }
28614
- /**
28615
- * Builds an ANSI sequence which moves the terminal cursor up by a positive number of rows.
28616
- *
28617
- * @private helper of `buildCoderRunUiTerminalFrameUpdate`
28618
- */
28619
- function moveCursorUp(lineCount) {
28620
- return `\x1b[${lineCount}A`;
28621
- }
28622
- /**
28623
- * Builds an ANSI sequence which moves the terminal cursor down by a positive number of rows.
28624
- *
28625
- * @private helper of `buildCoderRunUiTerminalFrameUpdate`
28626
- */
28627
- function moveCursorDown(lineCount) {
28628
- return `\x1b[${lineCount}B`;
28629
- }
28630
-
28631
- /**
28632
- * Calendar formats used when displaying the estimated completion time.
28633
- *
28634
- * Uses 24-hour format (`HH:mm`) so estimates are unambiguous (for example `17:30` instead of `5:30`).
28635
- */
28636
- const ESTIMATED_DONE_CALENDAR_FORMATS = {
28637
- sameDay: '[Today] HH:mm',
28638
- nextDay: '[Tomorrow] HH:mm',
28639
- nextWeek: 'dddd HH:mm',
28640
- lastDay: '[Yesterday] HH:mm',
28641
- lastWeek: 'dddd HH:mm',
28642
- sameElse: 'MMM D HH:mm',
28643
- };
28644
- /**
28645
- * Formats a duration into a compact string such as "3h 12m" or "45s".
28646
- */
28647
- function formatDurationBrief(duration) {
28648
- const totalSeconds = Math.max(0, Math.round(duration.asSeconds()));
28649
- const hours = Math.floor(totalSeconds / 3600);
28650
- const minutes = Math.floor((totalSeconds % 3600) / 60);
28651
- const seconds = totalSeconds % 60;
28652
- const parts = [];
28653
- if (hours > 0) {
28654
- parts.push(`${hours}h`);
28655
- }
28656
- if (minutes > 0) {
28657
- parts.push(`${minutes}m`);
28658
- }
28659
- if (!parts.length && seconds > 0) {
28660
- parts.push(`${seconds}s`);
28661
- }
28662
- if (!parts.length) {
28663
- parts.push('0s');
28664
- }
28665
- return parts.join(' ');
28666
- }
28667
-
28668
- /**
28669
- * Builds a session-scoped progress snapshot from prompt stats and elapsed active time.
28670
- *
28671
- * When the current session has not finished any prompt yet but a `cachedAveragePromptDurationMs` is
28672
- * provided (loaded from the per-config estimate cache stored in the temp folder), the snapshot uses
28673
- * that historical average to project the remaining work instead of waiting for the first completion.
28674
- */
28675
- function buildCoderRunProgressSnapshot(stats, elapsedDuration, initialDone, cachedAveragePromptDurationMs, runLimit) {
28676
- const totalPrompts = stats.done + stats.forAgent + stats.toBeWritten;
28677
- const completedSessionPrompts = Math.max(0, stats.done - initialDone);
28678
- const availableSessionTotal = completedSessionPrompts + stats.forAgent;
28679
- const sessionTotal = resolveSessionTotal(availableSessionTotal, runLimit);
28680
- const sessionDone = Math.min(completedSessionPrompts, sessionTotal);
28681
- const sessionRemaining = Math.max(0, sessionTotal - sessionDone);
28682
- const currentPromptIndex = sessionTotal > 0 ? Math.min(sessionDone + 1, sessionTotal) : 0;
28683
- const percentage = sessionTotal > 0 ? Math.round((sessionDone / sessionTotal) * 100) : 0;
28684
- const elapsedText = formatDurationBrief(elapsedDuration);
28685
- const elapsedMs = elapsedDuration.asMilliseconds();
28686
- let estimatedTotalText = 'estimating...';
28687
- let estimatedLabel = 'after first completion';
28688
- let isEstimatedTotalKnown = false;
28689
- let estimatedTotalMs;
28690
- if (sessionTotal > 0 && sessionDone > 0) {
28691
- estimatedTotalMs = (elapsedMs * sessionTotal) / sessionDone;
28692
- }
28693
- else if (sessionTotal > 0 &&
28694
- cachedAveragePromptDurationMs !== undefined &&
28695
- cachedAveragePromptDurationMs > 0) {
28696
- const projectedRemainingMs = sessionRemaining * cachedAveragePromptDurationMs;
28697
- estimatedTotalMs = elapsedMs + projectedRemainingMs;
28698
- }
28699
- if (estimatedTotalMs !== undefined) {
28700
- const estimatedRemainingMs = Math.max(0, estimatedTotalMs - elapsedMs);
28701
- const estimatedTotalDuration = moment.duration(estimatedTotalMs);
28702
- const estimatedCompletion = moment().add(estimatedRemainingMs, 'milliseconds');
28703
- estimatedTotalText = formatDurationBrief(estimatedTotalDuration);
28704
- estimatedLabel = estimatedCompletion.calendar(null, ESTIMATED_DONE_CALENDAR_FORMATS);
28705
- isEstimatedTotalKnown = true;
28706
- }
28987
+ * @private shared helper for terminal avatar rendering
28988
+ */
28989
+ function createTerminalAgentAvatarVisual(options) {
28990
+ const renderInputs = createTerminalAgentAvatarVisualRenderInputs(options.agentSource, options.defaultAvatarVisualId || DEFAULT_AGENT_AVATAR_VISUAL_ID);
28707
28991
  return {
28708
- totalPrompts,
28709
- sessionDone,
28710
- sessionRemaining,
28711
- sessionTotal,
28712
- currentPromptIndex,
28713
- skippedPrompts: stats.outsidePriorityRange,
28714
- toBeWrittenPrompts: stats.toBeWritten,
28715
- percentage,
28716
- elapsedText,
28717
- estimatedTotalText,
28718
- estimatedLabel,
28719
- isEstimatedTotalKnown,
28992
+ isAnimated: renderInputs.resolvedAvatarRenderDefinition.avatarVisual.isAnimated,
28993
+ renderFrame({ animationTimeMs }) {
28994
+ return renderTerminalAgentAvatarVisualFrame({
28995
+ ...renderInputs,
28996
+ animationTimeMs,
28997
+ colorDepth: options.colorDepth,
28998
+ createCanvas: options.createCanvas,
28999
+ });
29000
+ },
28720
29001
  };
28721
29002
  }
28722
29003
  /**
28723
- * Resolves the number of prompt runs represented by the current session progress.
29004
+ * Creates stable avatar render inputs from one agent source.
29005
+ *
29006
+ * @private shared helper for terminal avatar rendering
28724
29007
  */
28725
- function resolveSessionTotal(availableSessionTotal, runLimit) {
28726
- if (runLimit === undefined) {
28727
- return availableSessionTotal;
28728
- }
28729
- return Math.min(availableSessionTotal, runLimit);
29008
+ function createTerminalAgentAvatarVisualRenderInputs(agentSource, defaultAvatarVisualId) {
29009
+ const agentBasicInformation = parseAgentSource(agentSource);
29010
+ const avatarDefinition = createAvatarDefinitionFromAgentBasicInformation(agentBasicInformation);
29011
+ const avatarVisualId = resolveTerminalAgentAvatarVisualIdFromAgentBasicInformation(agentBasicInformation, defaultAvatarVisualId);
29012
+ const resolvedAvatarRenderDefinition = resolveAvatarRenderDefinition({
29013
+ avatarDefinition,
29014
+ visualId: avatarVisualId,
29015
+ surface: 'transparent',
29016
+ });
29017
+ return {
29018
+ avatarDefinition,
29019
+ avatarVisualId,
29020
+ resolvedAvatarRenderDefinition,
29021
+ };
28730
29022
  }
28731
-
28732
29023
  /**
28733
- * Tracks active coder-run time while excluding pauses and user-confirmation waits.
29024
+ * Resolves the terminal avatar visual id from already parsed agent information.
29025
+ *
29026
+ * @private shared helper for terminal avatar rendering
28734
29027
  */
28735
- class CoderRunTimer {
28736
- /**
28737
- * Creates a timer anchored at the provided start time.
28738
- */
28739
- constructor(startTime, isPausedInitially = false) {
28740
- this.startTime = startTime;
28741
- /**
28742
- * Total milliseconds spent in paused state across the run.
28743
- */
28744
- this.pausedMs = 0;
28745
- if (isPausedInitially) {
28746
- this.pausedSince = startTime.clone();
28747
- }
28748
- }
28749
- /**
28750
- * Pauses active-time tracking until `resume()` is called.
28751
- */
28752
- pause() {
28753
- if (this.pausedSince === undefined) {
28754
- this.pausedSince = moment();
28755
- }
28756
- }
28757
- /**
28758
- * Resumes active-time tracking after a pause.
28759
- */
28760
- resume() {
28761
- if (this.pausedSince !== undefined) {
28762
- this.pausedMs += moment().diff(this.pausedSince);
28763
- this.pausedSince = undefined;
28764
- }
29028
+ function resolveTerminalAgentAvatarVisualIdFromAgentBasicInformation(agentBasicInformation, defaultAvatarVisualId) {
29029
+ return resolveAgentAvatarVisualId(agentBasicInformation, resolveAvatarVisualId(agentBasicInformation.meta.visual) || defaultAvatarVisualId);
29030
+ }
29031
+ /**
29032
+ * Renders one terminal avatar frame through the shared avatar-to-ASCII pipeline.
29033
+ *
29034
+ * Visuals which are made of characters themselves, for example `AsciiOctopus`, paint the terminal
29035
+ * character grid directly, because rasterizing them into half-block ASCII art averages every glyph
29036
+ * away and makes them indistinguishable from the other blob-shaped visuals.
29037
+ *
29038
+ * @private shared helper for terminal avatar rendering
29039
+ */
29040
+ function renderTerminalAgentAvatarVisualFrame(options) {
29041
+ const terminalTextLines = renderAvatarVisualTerminalTextLines({
29042
+ avatarDefinition: options.avatarDefinition,
29043
+ visualId: options.avatarVisualId,
29044
+ surface: 'transparent',
29045
+ columns: TERMINAL_AGENT_AVATAR_VISUAL_COLUMNS,
29046
+ rows: TERMINAL_AGENT_AVATAR_VISUAL_ROWS,
29047
+ colorDepth: options.colorDepth,
29048
+ timeMs: options.animationTimeMs,
29049
+ resolvedAvatarRenderDefinition: options.resolvedAvatarRenderDefinition,
29050
+ });
29051
+ if (terminalTextLines !== null) {
29052
+ return terminalTextLines;
28765
29053
  }
28766
- /**
28767
- * Returns the currently accumulated active duration.
28768
- */
28769
- getElapsedDuration() {
28770
- const wallMs = moment().diff(this.startTime);
28771
- const currentPauseMs = this.pausedSince !== undefined ? moment().diff(this.pausedSince) : 0;
28772
- return moment.duration(Math.max(0, wallMs - this.pausedMs - currentPauseMs));
29054
+ if (options.createCanvas === undefined) {
29055
+ // Note: A pixel-based visual cannot be rasterized without a canvas, so the caller renders no avatar
29056
+ return [];
28773
29057
  }
29058
+ return renderAvatarVisualAsciiArt({
29059
+ avatarDefinition: options.avatarDefinition,
29060
+ visualId: options.avatarVisualId,
29061
+ surface: 'transparent',
29062
+ columns: TERMINAL_AGENT_AVATAR_VISUAL_COLUMNS,
29063
+ rows: TERMINAL_AGENT_AVATAR_VISUAL_ROWS,
29064
+ canvasWidth: TERMINAL_AGENT_AVATAR_VISUAL_CANVAS_WIDTH,
29065
+ canvasHeight: TERMINAL_AGENT_AVATAR_VISUAL_CANVAS_HEIGHT,
29066
+ colorDepth: options.colorDepth,
29067
+ timeMs: options.animationTimeMs,
29068
+ createCanvas: options.createCanvas,
29069
+ resolvedAvatarRenderDefinition: options.resolvedAvatarRenderDefinition,
29070
+ });
28774
29071
  }
28775
29072
 
28776
29073
  /**
28777
- * Maximum number of agent output lines kept in the scrolling output area.
29074
+ * Refresh cadence used only while the rich coder UI needs animated updates.
28778
29075
  *
28779
29076
  * @private internal constant of coder run UI
28780
29077
  */
28781
- const MAX_AGENT_OUTPUT_LINES = 12;
29078
+ const ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS = TERMINAL_AGENT_AVATAR_VISUAL_REFRESH_INTERVAL_MS;
28782
29079
  /**
28783
- * Reactive state manager for the coder run terminal UI.
29080
+ * Phases that still benefit from automatic refreshes because the frame can change
29081
+ * over time even without new runner output.
28784
29082
  *
28785
- * Holds all data the Ink components need and emits `'change'` events
28786
- * whenever any property is updated so the UI can re-render.
29083
+ * @private internal constant of coder run UI
29084
+ */
29085
+ const AUTO_REFRESH_PHASES = ['initializing', 'loading', 'running', 'verifying'];
29086
+ /**
29087
+ * Phases where an animated `--agent` visual should keep rendering like it does on the web.
29088
+ *
29089
+ * @private internal constant of coder run UI
29090
+ */
29091
+ const AGENT_VISUAL_AUTO_REFRESH_PHASES = [...AUTO_REFRESH_PHASES, 'waiting'];
29092
+ /**
29093
+ * Returns whether the rich coder UI should keep animating on its own.
28787
29094
  *
28788
29095
  * @private internal utility of coder run UI
28789
29096
  */
28790
- class CoderRunUiState extends EventEmitter {
28791
- constructor(startTime) {
28792
- super();
28793
- this.config = { agentName: '' };
28794
- this.currentPromptLabel = '';
28795
- this.currentScriptPaths = [];
28796
- this.currentAttempt = 1;
28797
- this.maxAttempts = 3;
28798
- this.detailLines = [];
28799
- this.messagePreviewLines = [];
28800
- this.messagePreviewSections = [];
28801
- this.agentStatusLines = [];
28802
- this.agentStatusTableRows = [];
28803
- this.agentOutputLines = [];
28804
- this.phase = 'initializing';
28805
- this.statusMessage = 'Initializing...';
28806
- this.errors = [];
28807
- this.stats = { done: 0, forAgent: 0, outsidePriorityRange: 0, toBeWritten: 0 };
28808
- // Match the plain CLI progress header and count active run time from startup.
28809
- // Explicit waits/pauses are excluded later through `pauseTimer()` / `resumeTimer()`.
28810
- this.timer = new CoderRunTimer(startTime);
28811
- }
28812
- /**
28813
- * Pauses the elapsed timer (e.g. while waiting for user input or paused state).
28814
- */
28815
- pauseTimer() {
28816
- this.timer.pause();
28817
- this.emitChange();
28818
- }
28819
- /**
28820
- * Resumes the elapsed timer after a pause.
28821
- */
28822
- resumeTimer() {
28823
- this.timer.resume();
28824
- this.emitChange();
28825
- }
28826
- /**
28827
- * Replaces the configuration shown in the UI header.
28828
- */
28829
- setConfig(config) {
28830
- this.config = config;
28831
- this.emitChange();
28832
- }
28833
- /**
28834
- * Replaces the ASCII-art agent visual renderer shown instead of the default brand banner.
28835
- */
28836
- setAgentVisual(agentVisual) {
28837
- this.agentVisual = agentVisual;
28838
- this.emitChange();
28839
- }
28840
- /**
28841
- * Feeds new prompt statistics from the main loop so the progress bar updates.
28842
- */
28843
- updateProgress(stats) {
28844
- if (this.initialDone === undefined && (stats.done > 0 || stats.forAgent > 0 || stats.toBeWritten > 0)) {
28845
- this.initialDone = stats.done;
28846
- }
28847
- this.stats = stats;
28848
- this.emitChange();
28849
- }
28850
- /**
28851
- * Sets the cached average prompt duration (in milliseconds) loaded from the temp-folder estimate cache.
28852
- *
28853
- * Used to project completion estimates before the first prompt of the current session finishes.
28854
- */
28855
- setCachedAveragePromptDurationMs(cachedAveragePromptDurationMs) {
28856
- this.cachedAveragePromptDurationMs = cachedAveragePromptDurationMs;
28857
- this.emitChange();
28858
- }
28859
- /**
28860
- * Computes a progress snapshot on demand so elapsed time ticks with periodic re-renders.
28861
- */
28862
- getProgress() {
28863
- var _a;
28864
- return buildCoderRunProgressSnapshot(this.stats, this.timer.getElapsedDuration(), (_a = this.initialDone) !== null && _a !== void 0 ? _a : this.stats.done, this.cachedAveragePromptDurationMs, this.config.limit);
28865
- }
28866
- /**
28867
- * Sets the label of the prompt currently being processed and resets per-prompt state.
28868
- */
28869
- setCurrentPrompt(label) {
28870
- this.currentPromptLabel = label;
28871
- this.currentScriptPaths = [];
28872
- this.detailLines = [];
28873
- this.messagePreviewLines = [];
28874
- this.messagePreviewSections = [];
28875
- this.pendingEnterLabel = undefined;
28876
- this.agentOutputLines = [];
28877
- this.currentAttempt = 1;
28878
- this.emitChange();
28879
- }
28880
- /**
28881
- * Sets the temporary shell script currently used by the active prompt.
28882
- */
28883
- setCurrentScriptPath(scriptPath) {
28884
- this.currentScriptPaths = scriptPath ? [scriptPath] : [];
28885
- this.emitChange();
28886
- }
28887
- /**
28888
- * Sets multiple temporary shell scripts used by a shared multi-agent session.
28889
- */
28890
- setCurrentScriptPaths(scriptPaths) {
28891
- this.currentScriptPaths = [...scriptPaths];
28892
- this.emitChange();
28893
- }
28894
- /**
28895
- * Updates the current retry attempt number.
28896
- */
28897
- setAttempt(attempt) {
28898
- this.currentAttempt = attempt;
28899
- this.emitChange();
28900
- }
28901
- /**
28902
- * Appends raw agent output text, keeping only the last `MAX_AGENT_OUTPUT_LINES`.
28903
- */
28904
- addAgentOutput(text) {
28905
- const lines = text.split(/\r?\n/).filter((line) => line.trim() !== '');
28906
- if (lines.length === 0) {
28907
- return;
28908
- }
28909
- this.agentOutputLines.push(...lines);
28910
- if (this.agentOutputLines.length > MAX_AGENT_OUTPUT_LINES) {
28911
- this.agentOutputLines = this.agentOutputLines.slice(-MAX_AGENT_OUTPUT_LINES);
28912
- }
28913
- this.emitChange();
28914
- }
28915
- /**
28916
- * Transitions the execution phase shown in the UI.
28917
- */
28918
- setPhase(phase) {
28919
- this.phase = phase;
28920
- this.emitChange();
28921
- }
28922
- /**
28923
- * Updates the status message line beneath the current prompt label.
28924
- */
28925
- setStatusMessage(message) {
28926
- this.statusMessage = message;
28927
- this.emitChange();
28928
- }
28929
- /**
28930
- * Replaces the contextual detail lines shown beneath the current prompt status.
28931
- */
28932
- setDetailLines(detailLines) {
28933
- this.detailLines = detailLines.filter((detailLine) => detailLine.trim() !== '');
28934
- this.emitChange();
28935
- }
28936
- /**
28937
- * Replaces the exact user-message preview lines shown in agent-specific panels.
28938
- */
28939
- setMessagePreviewLines(messagePreviewLines) {
28940
- this.messagePreviewLines = [...messagePreviewLines];
28941
- this.emitChange();
28942
- }
28943
- /**
28944
- * Replaces the structured user-message panels shown in agent-specific dashboards.
28945
- */
28946
- setMessagePreviewSections(messagePreviewSections) {
28947
- this.messagePreviewSections = [...messagePreviewSections];
28948
- this.emitChange();
28949
- }
28950
- /**
28951
- * Replaces agent status rows shown by agent-specific terminal frames.
28952
- */
28953
- setAgentStatusLines(agentStatusLines) {
28954
- this.agentStatusLines = [...agentStatusLines];
28955
- this.emitChange();
28956
- }
28957
- /**
28958
- * Replaces structured agent status rows shown by agent-specific terminal frames.
28959
- */
28960
- setAgentStatusTableRows(agentStatusTableRows) {
28961
- this.agentStatusTableRows = [...agentStatusTableRows];
28962
- this.emitChange();
28963
- }
28964
- /**
28965
- * Sets or clears the Enter-key action label shown in the controls panel.
28966
- */
28967
- setPendingEnterLabel(pendingEnterLabel) {
28968
- this.pendingEnterLabel = pendingEnterLabel;
28969
- this.emitChange();
28970
- }
28971
- /**
28972
- * Appends an error message to the error list shown in the UI.
28973
- */
28974
- addError(errorMessage) {
28975
- this.errors.push(errorMessage);
28976
- this.emitChange();
29097
+ function isCoderRunUiAutoRefreshing(phase, pauseState, isAgentVisualAnimated = false) {
29098
+ // `PAUSING` still means the current task is winding down, so keep active
29099
+ // animations/timers running until the runner reaches the fully paused state.
29100
+ if (pauseState === 'PAUSED') {
29101
+ return false;
28977
29102
  }
28978
- emitChange() {
28979
- this.emit('change');
29103
+ if (isAgentVisualAnimated) {
29104
+ return AGENT_VISUAL_AUTO_REFRESH_PHASES.includes(phase);
28980
29105
  }
29106
+ return AUTO_REFRESH_PHASES.includes(phase);
29107
+ }
29108
+ /**
29109
+ * Returns the automatic refresh interval for the current UI state.
29110
+ *
29111
+ * Waiting, paused, and completed states return `undefined` so the rich UI stays
29112
+ * perfectly still until actual state changes arrive.
29113
+ *
29114
+ * @private internal utility of coder run UI
29115
+ */
29116
+ function getCoderRunUiAutoRefreshInterval(phase, pauseState, isAgentVisualAnimated = false) {
29117
+ return isCoderRunUiAutoRefreshing(phase, pauseState, isAgentVisualAnimated)
29118
+ ? ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS
29119
+ : undefined;
28981
29120
  }
28982
29121
 
28983
29122
  /**
@@ -29075,6 +29214,7 @@ function renderCoderRunUi(startTime, options = {}) {
29075
29214
  let isRendering = false;
29076
29215
  let renderScheduled = false;
29077
29216
  let autoRefreshTimeout;
29217
+ let controlFeedbackTimeout;
29078
29218
  let isDisposed = false;
29079
29219
  /**
29080
29220
  * Schedules a render on the next tick if one isn't already pending.
@@ -29140,9 +29280,32 @@ function renderCoderRunUi(startTime, options = {}) {
29140
29280
  pendingEnterLabel: state.pendingEnterLabel,
29141
29281
  agentOutputLines: state.agentOutputLines,
29142
29282
  errors: state.errors,
29283
+ controlFeedback: state.controlFeedback,
29143
29284
  progress: state.getProgress(),
29144
29285
  });
29145
29286
  }
29287
+ /**
29288
+ * Answers one pressed control key in the frame and hides that answer again after a short moment.
29289
+ */
29290
+ function showControlFeedback(controlFeedback) {
29291
+ clearControlFeedbackTimeout();
29292
+ // Note: Every state change re-renders the frame, so the answer appears with the very next frame
29293
+ state.setControlFeedback(controlFeedback);
29294
+ controlFeedbackTimeout = setTimeout(() => {
29295
+ controlFeedbackTimeout = undefined;
29296
+ state.setControlFeedback(undefined);
29297
+ }, CODER_RUN_CONTROL_FEEDBACK_DURATION_MS);
29298
+ }
29299
+ /**
29300
+ * Stops the timer which hides the currently shown control answer.
29301
+ */
29302
+ function clearControlFeedbackTimeout() {
29303
+ if (controlFeedbackTimeout === undefined) {
29304
+ return;
29305
+ }
29306
+ clearTimeout(controlFeedbackTimeout);
29307
+ controlFeedbackTimeout = undefined;
29308
+ }
29146
29309
  /**
29147
29310
  * Clears previously rendered lines and writes a new frame only where needed.
29148
29311
  */
@@ -29175,19 +29338,10 @@ function renderCoderRunUi(startTime, options = {}) {
29175
29338
  cleanup();
29176
29339
  process.exit(0);
29177
29340
  }
29178
- if (key.name === 'p') {
29179
- togglePauseState();
29180
- scheduleRender();
29181
- return;
29182
- }
29183
- if (key.name === 's') {
29184
- requestSkipCurrentWait();
29185
- scheduleRender();
29186
- return;
29187
- }
29188
- if (key.name === 'x') {
29189
- toggleEndAfterCurrentPromptState();
29190
- scheduleRender();
29341
+ // Note: [🏹] The very same key handling is shared with the plain console mode, see `applyCoderRunControlKey`
29342
+ const controlFeedback = applyCoderRunControlKey(key.name);
29343
+ if (controlFeedback !== undefined) {
29344
+ showControlFeedback(controlFeedback);
29191
29345
  return;
29192
29346
  }
29193
29347
  if ((key.name === 'return' || key.name === 'enter') && pendingEnterResolver) {
@@ -29213,6 +29367,7 @@ function renderCoderRunUi(startTime, options = {}) {
29213
29367
  clearTimeout(autoRefreshTimeout);
29214
29368
  autoRefreshTimeout = undefined;
29215
29369
  }
29370
+ clearControlFeedbackTimeout();
29216
29371
  state.off('change', scheduleRender);
29217
29372
  process.stdin.off('keypress', keypressHandler);
29218
29373
  process.stdout.off('resize', scheduleRender);
@@ -29273,7 +29428,7 @@ function createCoderRunOptionsForAgent(options) {
29273
29428
  waitBetweenPrompts: 0,
29274
29429
  waitAfterError: 0,
29275
29430
  noCommit: options.noCommit,
29276
- ignoreGitChanges: options.ignoreGitChanges,
29431
+ gitChanges: options.gitChanges,
29277
29432
  normalizeLineEndings: options.normalizeLineEndings,
29278
29433
  allowCredits: options.allowCredits,
29279
29434
  autoMigrate: false,
@@ -39841,11 +39996,11 @@ class RunMultipleAgentMessageTaskScheduler {
39841
39996
  * Validates constraints specific to never-ending agent watch loops.
39842
39997
  */
39843
39998
  function validateAgentWatchOptions(commandDisplayName, options) {
39844
- if (!options.noCommit || options.ignoreGitChanges) {
39999
+ if (!options.noCommit || options.gitChanges !== 'fail') {
39845
40000
  return;
39846
40001
  }
39847
40002
  throw new NotAllowed(spaceTrim$1(`
39848
- Flag \`--no-commit\` requires \`--ignore-git-changes\` when used with \`${commandDisplayName}\`.
40003
+ Flag \`--no-commit\` requires \`--git-changes ignore\` when used with \`${commandDisplayName}\`.
39849
40004
 
39850
40005
  Without commits, answered messages remain in the working tree and the next message round would fail the clean
39851
40006
  working tree check.
@@ -40158,7 +40313,7 @@ function createLocalAgentRunOptions(options, localAgentRunnerLimits) {
40158
40313
  noUi: options.noUi,
40159
40314
  thinkingLevel: options.thinkingLevel,
40160
40315
  noCommit: true,
40161
- ignoreGitChanges: true,
40316
+ gitChanges: 'ignore',
40162
40317
  normalizeLineEndings: false,
40163
40318
  allowCredits: options.allowCredits,
40164
40319
  autoPush: false,
@@ -43756,6 +43911,8 @@ function $initializeCoderRunCommand(program) {
43756
43911
  Features:
43757
43912
  - Automatically stages and commits changes with agent identity unless --no-commit is used
43758
43913
  - Commits only the prompt file and the files the coding agent has changed, leaving unrelated changes alone
43914
+ - Refuses to start on a dirty working tree unless --git-changes says what should happen with the changes
43915
+ - Optional --git-changes continue resumes the single prompt an interrupted coder left in the [^] status
43759
43916
  - Optional post-commit git push with explicit --auto-push opt-in
43760
43917
  - Optional pre-prompt git pull with explicit --auto-pull opt-in
43761
43918
  - Optional --isolate runs every prompt in its own temporary git worktree and merges it back when verified
@@ -43836,7 +43993,7 @@ function $initializeCoderRunCommand(program) {
43836
43993
  waitBetweenPrompts,
43837
43994
  waitAfterError,
43838
43995
  noCommit: runnerOptions.noCommit,
43839
- ignoreGitChanges: runnerOptions.ignoreGitChanges,
43996
+ gitChanges: runnerOptions.gitChanges,
43840
43997
  agentName: runnerOptions.agentName,
43841
43998
  model: runnerOptions.model,
43842
43999
  agent,
@@ -43977,7 +44134,7 @@ function $initializeCoderServerCommand(program) {
43977
44134
  waitBetweenPrompts,
43978
44135
  waitAfterError,
43979
44136
  noCommit: runnerOptions.noCommit,
43980
- ignoreGitChanges: runnerOptions.ignoreGitChanges,
44137
+ gitChanges: runnerOptions.gitChanges,
43981
44138
  agentName: runnerOptions.agentName,
43982
44139
  model: runnerOptions.model,
43983
44140
  agent,
@@ -67408,7 +67565,7 @@ function createPromptRunnerOptions(options) {
67408
67565
  waitBetweenPrompts: 0,
67409
67566
  waitAfterError: 0,
67410
67567
  noCommit: true,
67411
- ignoreGitChanges: true,
67568
+ gitChanges: 'ignore',
67412
67569
  normalizeLineEndings: false,
67413
67570
  allowCredits: options.allowCredits,
67414
67571
  isVerbose: options.isVerbose,
@@ -73201,13 +73358,13 @@ function isPromptToBeWritten(file, section) {
73201
73358
  }
73202
73359
 
73203
73360
  /**
73204
- * Lists todo prompts across all files.
73361
+ * Lists prompts in one checklist status across all files.
73205
73362
  */
73206
- function listTodoPrompts(files) {
73363
+ function listPromptsByStatus(files, status) {
73207
73364
  const prompts = [];
73208
73365
  for (const file of files) {
73209
73366
  for (const section of file.sections) {
73210
- if (section.status === 'todo') {
73367
+ if (section.status === status) {
73211
73368
  prompts.push({ file, section });
73212
73369
  }
73213
73370
  }
@@ -73215,6 +73372,13 @@ function listTodoPrompts(files) {
73215
73372
  return prompts;
73216
73373
  }
73217
73374
 
73375
+ /**
73376
+ * Lists todo prompts across all files.
73377
+ */
73378
+ function listTodoPrompts(files) {
73379
+ return listPromptsByStatus(files, 'todo');
73380
+ }
73381
+
73218
73382
  /**
73219
73383
  * Lists todo prompts that still contain authoring placeholders.
73220
73384
  */
@@ -73792,7 +73956,7 @@ const DEFAULT_WAIT_AFTER_ERROR_MS = 10 * 60 * 1000;
73792
73956
  /**
73793
73957
  * CLI usage text for this script.
73794
73958
  */
73795
- const USAGE = 'Usage: run-codex-prompts [--dry-run] [--harness <harness-name>] [--model <model>] [--context <context-or-file>] [--test <test-command...>] [--test-before <no|yes-and-fail|yes-and-fix>] [--preserve-logs] [--isolate] [--no-ui] [--thinking-level <thinking-level>] [--priority <minimum-priority>] [--min-priority <minimum-priority>] [--max-priority <maximum-priority>] [--limit <run-count>] [--allow-credits] [--auto-migrate] [--allow-destructive-auto-migrate] [--wait-after-prompt <duration>] [--wait-between-prompts <duration>] [--wait-after-error <duration>] [--no-auto] [--no-commit] [--ignore-git-changes] [--no-normalize-line-endings] [--auto-push] [--auto-pull]';
73959
+ const USAGE = 'Usage: run-codex-prompts [--dry-run] [--harness <harness-name>] [--model <model>] [--context <context-or-file>] [--test <test-command...>] [--test-before <no|yes-and-fail|yes-and-fix>] [--preserve-logs] [--isolate] [--no-ui] [--thinking-level <thinking-level>] [--priority <minimum-priority>] [--min-priority <minimum-priority>] [--max-priority <maximum-priority>] [--limit <run-count>] [--allow-credits] [--auto-migrate] [--allow-destructive-auto-migrate] [--wait-after-prompt <duration>] [--wait-between-prompts <duration>] [--wait-after-error <duration>] [--no-auto] [--no-commit] [--git-changes <fail|ignore|continue>] [--no-normalize-line-endings] [--auto-push] [--auto-pull]';
73796
73960
  /**
73797
73961
  * Top-level flags supported by this command.
73798
73962
  */
@@ -73819,7 +73983,7 @@ const KNOWN_OPTION_FLAGS = new Set([
73819
73983
  '--wait-after-error',
73820
73984
  '--no-auto',
73821
73985
  '--no-commit',
73822
- '--ignore-git-changes',
73986
+ '--git-changes',
73823
73987
  '--no-normalize-line-endings',
73824
73988
  '--auto-push',
73825
73989
  '--auto-pull',
@@ -73863,7 +74027,8 @@ function parseRunOptions(args) {
73863
74027
  const hasLimitFlag = args.includes('--limit');
73864
74028
  const limit = parseLimit(readOptionValue(args, '--limit'), hasLimitFlag);
73865
74029
  const noCommit = args.includes('--no-commit');
73866
- const ignoreGitChanges = args.includes('--ignore-git-changes');
74030
+ const hasGitChangesFlag = args.includes('--git-changes');
74031
+ const gitChanges = parseGitChangesOption(readOptionValue(args, '--git-changes'), hasGitChangesFlag);
73867
74032
  const normalizeLineEndings = !args.includes('--no-normalize-line-endings');
73868
74033
  const allowCredits = args.includes('--allow-credits');
73869
74034
  const autoMigrate = args.includes('--auto-migrate');
@@ -73901,7 +74066,7 @@ function parseRunOptions(args) {
73901
74066
  waitBetweenPrompts,
73902
74067
  waitAfterError,
73903
74068
  noCommit,
73904
- ignoreGitChanges,
74069
+ gitChanges,
73905
74070
  normalizeLineEndings,
73906
74071
  allowCredits,
73907
74072
  autoMigrate,
@@ -73939,6 +74104,21 @@ function parseTestBeforeOption(value, hasTestBeforeFlag) {
73939
74104
  }
73940
74105
  return value;
73941
74106
  }
74107
+ /**
74108
+ * Parses and validates the requested behavior for a dirty working tree.
74109
+ */
74110
+ function parseGitChangesOption(value, hasGitChangesFlag) {
74111
+ if (value === undefined) {
74112
+ if (hasGitChangesFlag) {
74113
+ exitWithUsageError(`Missing value for --git-changes. Use one of: ${GIT_CHANGES_MODE_VALUES.join(', ')}.`);
74114
+ }
74115
+ return DEFAULT_GIT_CHANGES_MODE;
74116
+ }
74117
+ if (!isGitChangesMode(value)) {
74118
+ exitWithUsageError(`Invalid value for --git-changes: "${value}". Use one of: ${GIT_CHANGES_MODE_VALUES.join(', ')}.`);
74119
+ }
74120
+ return value;
74121
+ }
73942
74122
  /**
73943
74123
  * Reads a duration-typed CLI flag, applying the provided default when the flag is absent.
73944
74124
  */
@@ -74396,27 +74576,19 @@ async function sleepWithCountdown(options) {
74396
74576
  return;
74397
74577
  }
74398
74578
  const deadlineTimeMs = (_a = options.deadlineTimeMs) !== null && _a !== void 0 ? _a : Date.now() + durationMs;
74399
- const waitToken = beginSkippableWait();
74400
- try {
74401
- await waitUntilWorldTimeDeadline({
74402
- deadlineTimeMs,
74403
- pollIntervalMs: WAIT_COUNTDOWN_UPDATE_INTERVAL_MS,
74404
- shouldStopWaiting: () => shouldSkipCurrentWait(waitToken),
74405
- waitForMilliseconds: (waitDurationMs) => waitForSkippableMilliseconds(waitToken, waitDurationMs),
74406
- onTick: (remainingDurationMs) => {
74407
- const visibleRemainingDurationMs = Math.min(remainingDurationMs, durationMs);
74408
- const statusMessage = describeCoderRunWait(waitKind, visibleRemainingDurationMs, durationMs);
74409
- if (!isRichUiEnabled) {
74410
- console.info(colors.gray(`${statusMessage}...`));
74411
- return;
74412
- }
74413
- uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage(`${statusMessage}...`);
74414
- },
74415
- });
74416
- }
74417
- finally {
74418
- finishSkippableWait(waitToken);
74419
- }
74579
+ await waitForSkippableWorldTimeDeadline({
74580
+ deadlineTimeMs,
74581
+ pollIntervalMs: WAIT_COUNTDOWN_UPDATE_INTERVAL_MS,
74582
+ onTick: (remainingDurationMs) => {
74583
+ const visibleRemainingDurationMs = Math.min(remainingDurationMs, durationMs);
74584
+ const statusMessage = describeCoderRunWait(waitKind, visibleRemainingDurationMs, durationMs);
74585
+ if (!isRichUiEnabled) {
74586
+ console.info(colors.gray(`${statusMessage}...`));
74587
+ return;
74588
+ }
74589
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage(`${statusMessage}...`);
74590
+ },
74591
+ });
74420
74592
  }
74421
74593
 
74422
74594
  /**
@@ -74431,6 +74603,36 @@ async function resolveCoderContext(contextReference, currentWorkingDirectory) {
74431
74603
  });
74432
74604
  }
74433
74605
 
74606
+ /**
74607
+ * Listens for the terminal control keys while the plain console output is used.
74608
+ *
74609
+ * Every recognized key press is answered with one printed line, because the plain mode has no frame
74610
+ * which could show the new state on its own. The rich terminal UI installs its own listener in
74611
+ * [`renderCoderRunUi`](../ui/renderCoderRunUi.ts) and shares the very same key handling through
74612
+ * [`applyCoderRunControlKey`](./applyCoderRunControlKey.ts).
74613
+ *
74614
+ * @private internal utility of `ptbk coder` terminal controls
74615
+ */
74616
+ function listenForCoderRunControls() {
74617
+ if (!process.stdin.isTTY) {
74618
+ return;
74619
+ }
74620
+ readline.emitKeypressEvents(process.stdin);
74621
+ process.stdin.setRawMode(true);
74622
+ process.stdin.on('keypress', (_pressedText, key) => {
74623
+ if (key.ctrl && key.name === 'c') {
74624
+ process.exit();
74625
+ }
74626
+ const controlFeedback = applyCoderRunControlKey(key.name);
74627
+ if (controlFeedback === undefined) {
74628
+ return;
74629
+ }
74630
+ // Note: Using console.log here which adds a new line.
74631
+ // This is intentional to prevent the message from being overwritten.
74632
+ console.log(formatCoderRunControlFeedback(controlFeedback));
74633
+ });
74634
+ }
74635
+
74434
74636
  /**
74435
74637
  * Checks working tree clean.
74436
74638
  */
@@ -74449,12 +74651,14 @@ async function isWorkingTreeClean(path) {
74449
74651
  async function ensureWorkingTreeClean() {
74450
74652
  const isClean = await isWorkingTreeClean(process.cwd());
74451
74653
  if (!isClean) {
74452
- throw new Error(spaceTrim$1(`
74654
+ throw new NotAllowed(spaceTrim$1(`
74453
74655
  Git working tree is not clean.
74454
74656
 
74455
74657
  Please commit or stash your changes before running this script
74456
- OR run script with flag --ignore-git-changes
74457
-
74658
+ OR decide what should happen with them:
74659
+
74660
+ - \`--git-changes ignore\` leaves the changes where they are and starts the next prompt anyway
74661
+ - \`--git-changes continue\` resumes the single prompt which was left in the middle of its implementation
74458
74662
 
74459
74663
  Aborting
74460
74664
  `));
@@ -76021,6 +76225,51 @@ function formatPromptAttemptMetadata(status, attemptCount) {
76021
76225
  return `(failed after ${attemptCount} attempts) `;
76022
76226
  }
76023
76227
 
76228
+ /**
76229
+ * Separator which introduces the harness that started a prompt another harness has taken over.
76230
+ *
76231
+ * It is used both for writing and for reading the attribution, so the status line of a continued prompt
76232
+ * stays parseable when yet another harness continues it later.
76233
+ */
76234
+ const STARTED_BY_SEPARATOR = ', started by ';
76235
+ /**
76236
+ * Matches the attribution of one status line, which reaches from `by ` up to the step summary.
76237
+ */
76238
+ const PROMPT_RUNNER_ATTRIBUTION_PATTERN = /(?:^|\s)by (?<attribution>.*?)(?: - |$)/u;
76239
+ /**
76240
+ * Formats who worked on one prompt for its status line.
76241
+ *
76242
+ * Produces `` by Claude Code `claude-opus-5` `` for a prompt implemented by a single harness and
76243
+ * `` by Claude Code `claude-opus-5`, started by OpenAI Codex `gpt-5.6-luna` `` for a prompt which one
76244
+ * harness started and another one continued through `--git-changes continue`.
76245
+ */
76246
+ function formatPromptRunnerAttribution(options) {
76247
+ const { currentRunnerSignature, startedByRunnerSignature } = options;
76248
+ if (startedByRunnerSignature === undefined || startedByRunnerSignature === currentRunnerSignature) {
76249
+ return `by ${currentRunnerSignature}`;
76250
+ }
76251
+ return `by ${currentRunnerSignature}${STARTED_BY_SEPARATOR}${startedByRunnerSignature}`;
76252
+ }
76253
+ /**
76254
+ * Reads which harness started the work recorded on one prompt status line.
76255
+ *
76256
+ * A line written by a single harness names that harness, and a line of a prompt which was already taken
76257
+ * over keeps naming the harness which started it, so the original author survives any number of takeovers.
76258
+ */
76259
+ function parsePromptStartedByRunnerSignature(statusLine) {
76260
+ var _a, _b, _c;
76261
+ const attribution = (_c = (_b = (_a = statusLine.match(PROMPT_RUNNER_ATTRIBUTION_PATTERN)) === null || _a === void 0 ? void 0 : _a.groups) === null || _b === void 0 ? void 0 : _b.attribution) === null || _c === void 0 ? void 0 : _c.trim();
76262
+ if (!attribution) {
76263
+ return undefined;
76264
+ }
76265
+ const startedBySeparatorIndex = attribution.indexOf(STARTED_BY_SEPARATOR);
76266
+ if (startedBySeparatorIndex === -1) {
76267
+ return attribution;
76268
+ }
76269
+ return attribution.slice(startedBySeparatorIndex + STARTED_BY_SEPARATOR.length).trim() || undefined;
76270
+ }
76271
+ // Note: [💞] Ignore a discrepancy between file name and exported helper names
76272
+
76024
76273
  /**
76025
76274
  * Builds the shared body of a prompt status line, used by both the in-progress `[^]` and the done `[x]` status.
76026
76275
  *
@@ -76028,14 +76277,18 @@ function formatPromptAttemptMetadata(status, attemptCount) {
76028
76277
  * ``by OpenAI Codex `gpt-5.6-luna` thinking `max` (ChatGPT account) - Implementation ~$0.2036 10 minutes``.
76029
76278
  */
76030
76279
  function buildPromptStatusDetails(options) {
76031
- const { steps, inProgressStepKind, runnerName, modelName, attemptCount, loginMethod, thinkingLevel } = options;
76280
+ const { steps, inProgressStepKind, runnerName, modelName, startedByRunnerSignature, attemptCount, loginMethod, thinkingLevel, } = options;
76032
76281
  const runnerSignature = formatRunnerSignature(runnerName, modelName, thinkingLevel);
76033
76282
  const attemptMetadata = formatPromptAttemptMetadata('done', attemptCount);
76034
76283
  const loginMethodLabel = formatCodexLoginMethod(loginMethod);
76035
76284
  const loginMethodSuffix = loginMethodLabel ? ` (${loginMethodLabel})` : '';
76285
+ const attribution = formatPromptRunnerAttribution({
76286
+ currentRunnerSignature: `${runnerSignature}${loginMethodSuffix}`,
76287
+ startedByRunnerSignature,
76288
+ });
76036
76289
  const stepsSummary = formatCoderRunSteps(steps, inProgressStepKind);
76037
76290
  const stepsSuffix = stepsSummary === '' ? '' : ` - ${stepsSummary}`;
76038
- return `${attemptMetadata}by ${runnerSignature}${loginMethodSuffix}${stepsSuffix}`;
76291
+ return `${attemptMetadata}${attribution}${stepsSuffix}`;
76039
76292
  }
76040
76293
 
76041
76294
  /**
@@ -76078,29 +76331,26 @@ function writePromptStatusLine(file, section, replacementStatusLine) {
76078
76331
  /**
76079
76332
  * Marks a prompt section as done and records the per-step usage pricing and runner details.
76080
76333
  */
76081
- function markPromptDone(file, section, steps, runnerName, modelName, attemptCount = 1, loginMethod, thinkingLevel) {
76082
- const statusDetails = buildPromptStatusDetails({
76083
- steps,
76084
- runnerName,
76085
- modelName,
76086
- attemptCount,
76087
- loginMethod,
76088
- thinkingLevel,
76089
- });
76090
- writePromptStatusLine(file, section, `[x] ${statusDetails}`);
76334
+ function markPromptDone(options) {
76335
+ const { file, section, ...statusDetailsOptions } = options;
76336
+ writePromptStatusLine(file, section, `[x] ${buildPromptStatusDetails(statusDetailsOptions)}`);
76091
76337
  }
76092
76338
 
76093
76339
  /**
76094
76340
  * Marks a prompt section as failed and records runner details.
76095
76341
  */
76096
- function markPromptFailed(file, section, runnerName, modelName, promptExecutionStartedDate, attemptCount = 1) {
76097
- const runnerSignature = formatRunnerSignature(runnerName, modelName);
76342
+ function markPromptFailed(options) {
76343
+ const { file, section, runnerName, modelName, startedByRunnerSignature, promptExecutionStartedDate, attemptCount } = options;
76344
+ const attribution = formatPromptRunnerAttribution({
76345
+ currentRunnerSignature: formatRunnerSignature(runnerName, modelName),
76346
+ startedByRunnerSignature,
76347
+ });
76098
76348
  const attemptMetadata = formatPromptAttemptMetadata('failed', attemptCount);
76099
76349
  const duration = moment().diff(promptExecutionStartedDate);
76100
76350
  const durationString = moment.duration(duration).humanize();
76101
76351
  const failureDetails = attemptMetadata === ''
76102
- ? `failed after ${durationString} by ${runnerSignature}`
76103
- : `${attemptMetadata}${durationString} by ${runnerSignature}`;
76352
+ ? `failed after ${durationString} ${attribution}`
76353
+ : `${attemptMetadata}${durationString} ${attribution}`;
76104
76354
  writePromptStatusLine(file, section, `[!] ${failureDetails}`);
76105
76355
  }
76106
76356
 
@@ -76192,6 +76442,12 @@ async function runPromptRound({ options, runner, runnerMetadata, nextPrompt, pro
76192
76442
  const codexPrompt = appendCoderContext(promptWithAgent, resolvedCoderContext);
76193
76443
  // Note: Temporary scripts and runtime logs stay in the original project so they outlive an isolated worktree
76194
76444
  const scriptPath = buildScriptPath(nextPrompt.file, nextPrompt.section);
76445
+ // Note: Read before the first `[^]` rewrite of this round, which would overwrite the harness recorded so far
76446
+ const startedByRunnerSignature = resolveStartedByRunnerSignature({
76447
+ nextPrompt,
76448
+ runnerMetadata,
76449
+ thinkingLevel: options.thinkingLevel,
76450
+ });
76195
76451
  setPromptRoundRunningState({ isRichUiEnabled, promptLabel, scriptPath, uiHandle });
76196
76452
  await waitForRequestedPause({
76197
76453
  checkpointLabel: 'preparing the current prompt execution',
@@ -76224,6 +76480,7 @@ async function runPromptRound({ options, runner, runnerMetadata, nextPrompt, pro
76224
76480
  onStepStarted: (progress) => recordPromptRoundInProgress({
76225
76481
  nextPrompt,
76226
76482
  runnerMetadata,
76483
+ startedByRunnerSignature,
76227
76484
  thinkingLevel: options.thinkingLevel,
76228
76485
  attemptCount,
76229
76486
  progress,
@@ -76234,6 +76491,7 @@ async function runPromptRound({ options, runner, runnerMetadata, nextPrompt, pro
76234
76491
  options,
76235
76492
  nextPrompt,
76236
76493
  runnerMetadata,
76494
+ startedByRunnerSignature,
76237
76495
  promptExecutionStartedDate,
76238
76496
  result,
76239
76497
  commitMessage,
@@ -76267,6 +76525,7 @@ async function runPromptRound({ options, runner, runnerMetadata, nextPrompt, pro
76267
76525
  await finalizeFailedPromptRound({
76268
76526
  nextPrompt,
76269
76527
  runnerMetadata,
76528
+ startedByRunnerSignature,
76270
76529
  promptExecutionStartedDate,
76271
76530
  attemptCount,
76272
76531
  error: lastError,
@@ -76279,6 +76538,23 @@ async function runPromptRound({ options, runner, runnerMetadata, nextPrompt, pro
76279
76538
  throw lastError;
76280
76539
  }, { preserveArtifactsOnSuccess: options.preserveLogs });
76281
76540
  }
76541
+ /**
76542
+ * Reads which harness left the prompt of this round in the middle of its implementation.
76543
+ *
76544
+ * Only a prompt resumed through `--git-changes continue` still carries the in-progress `[^]` status when its
76545
+ * round starts, so every other round reports no earlier harness. A harness which continues its own work is
76546
+ * reported as no earlier harness either, because the status line would just name it twice.
76547
+ */
76548
+ function resolveStartedByRunnerSignature(options) {
76549
+ const { nextPrompt, runnerMetadata, thinkingLevel } = options;
76550
+ if (nextPrompt.section.status !== 'in-progress') {
76551
+ return undefined;
76552
+ }
76553
+ const { line } = resolvePromptStatusLine(nextPrompt.file, nextPrompt.section);
76554
+ const startedByRunnerSignature = parsePromptStartedByRunnerSignature(line);
76555
+ const currentRunnerSignature = formatRunnerSignature(runnerMetadata.runnerName, runnerMetadata.modelName, thinkingLevel);
76556
+ return startedByRunnerSignature === currentRunnerSignature ? undefined : startedByRunnerSignature;
76557
+ }
76282
76558
  /**
76283
76559
  * Records into the prompt file that the prompt is being implemented right now.
76284
76560
  *
@@ -76287,7 +76563,7 @@ async function runPromptRound({ options, runner, runnerMetadata, nextPrompt, pro
76287
76563
  * crashes leaves the `[^]` status behind as the signal that this task was left in the middle.
76288
76564
  */
76289
76565
  async function recordPromptRoundInProgress(options) {
76290
- const { nextPrompt, runnerMetadata, thinkingLevel, attemptCount, progress } = options;
76566
+ const { nextPrompt, runnerMetadata, startedByRunnerSignature, thinkingLevel, attemptCount, progress } = options;
76291
76567
  markPromptInProgress({
76292
76568
  file: nextPrompt.file,
76293
76569
  section: nextPrompt.section,
@@ -76295,6 +76571,7 @@ async function recordPromptRoundInProgress(options) {
76295
76571
  inProgressStepKind: progress.startedStepKind,
76296
76572
  runnerName: runnerMetadata.runnerName,
76297
76573
  modelName: runnerMetadata.modelName,
76574
+ startedByRunnerSignature,
76298
76575
  attemptCount,
76299
76576
  loginMethod: progress.loginMethod,
76300
76577
  thinkingLevel,
@@ -76352,14 +76629,24 @@ function setPromptRoundRunningState(options) {
76352
76629
  * Finalizes a successful prompt round, including prompt bookkeeping and commit flow.
76353
76630
  */
76354
76631
  async function finalizeSuccessfulPromptRound(options) {
76355
- const { options: runOptions, nextPrompt, runnerMetadata, promptExecutionStartedDate, result, commitMessage, logPath, roundCommitScope, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
76632
+ const { options: runOptions, nextPrompt, runnerMetadata, startedByRunnerSignature, promptExecutionStartedDate, result, commitMessage, logPath, roundCommitScope, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
76356
76633
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.stopCapturingAgentOutput();
76357
76634
  await waitForRequestedPause({
76358
76635
  checkpointLabel: 'recording the successful prompt result',
76359
76636
  phase: 'running',
76360
76637
  statusMessage: 'Recording prompt result',
76361
76638
  });
76362
- markPromptDone(nextPrompt.file, nextPrompt.section, result.steps, runnerMetadata.runnerName, runnerMetadata.modelName, result.attemptCount, result.loginMethod, runOptions.thinkingLevel);
76639
+ markPromptDone({
76640
+ file: nextPrompt.file,
76641
+ section: nextPrompt.section,
76642
+ steps: result.steps,
76643
+ runnerName: runnerMetadata.runnerName,
76644
+ modelName: runnerMetadata.modelName,
76645
+ startedByRunnerSignature,
76646
+ attemptCount: result.attemptCount,
76647
+ loginMethod: result.loginMethod,
76648
+ thinkingLevel: runOptions.thinkingLevel,
76649
+ });
76363
76650
  // Note: The prompt status is always written into the original project, an isolated round transports
76364
76651
  // its own changes back through the merge instead
76365
76652
  await writePromptFile(nextPrompt.file);
@@ -76410,7 +76697,7 @@ async function finalizeSuccessfulPromptRound(options) {
76410
76697
  * Finalizes a failed prompt round, persisting prompt failure metadata before rethrowing.
76411
76698
  */
76412
76699
  async function finalizeFailedPromptRound(options) {
76413
- const { nextPrompt, runnerMetadata, promptExecutionStartedDate, attemptCount, error, options: runOptions, roundCommitScope, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
76700
+ const { nextPrompt, runnerMetadata, startedByRunnerSignature, promptExecutionStartedDate, attemptCount, error, options: runOptions, roundCommitScope, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
76414
76701
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.stopCapturingAgentOutput();
76415
76702
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setPhase('error');
76416
76703
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.addError(error instanceof Error ? error.message : String(error));
@@ -76419,7 +76706,15 @@ async function finalizeFailedPromptRound(options) {
76419
76706
  phase: 'error',
76420
76707
  statusMessage: 'Recording prompt failure',
76421
76708
  });
76422
- markPromptFailed(nextPrompt.file, nextPrompt.section, runnerMetadata.runnerName, runnerMetadata.modelName, promptExecutionStartedDate, attemptCount);
76709
+ markPromptFailed({
76710
+ file: nextPrompt.file,
76711
+ section: nextPrompt.section,
76712
+ runnerName: runnerMetadata.runnerName,
76713
+ modelName: runnerMetadata.modelName,
76714
+ startedByRunnerSignature,
76715
+ promptExecutionStartedDate,
76716
+ attemptCount,
76717
+ });
76423
76718
  await writePromptFile(nextPrompt.file);
76424
76719
  await writePromptErrorLog({
76425
76720
  file: nextPrompt.file,
@@ -77172,6 +77467,50 @@ function printUpcomingTasks(tasks) {
77172
77467
  }
77173
77468
  }
77174
77469
 
77470
+ /**
77471
+ * Builds a label that identifies the prompt section by index.
77472
+ */
77473
+ function buildPromptLabel(file, section) {
77474
+ return `${relative(process.cwd(), file.path).replace(/\\/g, '/')}#${section.index + 1}`;
77475
+ }
77476
+
77477
+ /**
77478
+ * Resolves the one prompt which `--git-changes continue` resumes.
77479
+ *
77480
+ * A coder which was killed or crashed leaves exactly one prompt behind in the in-progress `[^]` status,
77481
+ * so anything else means the working tree changes cannot be attributed to a single interrupted prompt.
77482
+ */
77483
+ function resolveInterruptedPrompt(files) {
77484
+ const interruptedPrompts = listPromptsByStatus(files, 'in-progress');
77485
+ if (interruptedPrompts.length === 0) {
77486
+ throw new NotFoundError(spaceTrim$1(`
77487
+ Flag \`--git-changes continue\` found no interrupted prompt to continue.
77488
+
77489
+ Continuing resumes the prompt an earlier coder left in the middle of its implementation, which is the
77490
+ one marked with the in-progress \`[^]\` status. No prompt carries that status right now.
77491
+
77492
+ Use \`--git-changes ignore\` to start the next \`[ ]\` prompt with the current working tree changes
77493
+ left in place, or \`--git-changes fail\` to commit or stash them first.
77494
+ `));
77495
+ }
77496
+ if (interruptedPrompts.length > 1) {
77497
+ throw new NotAllowed(spaceTrim$1((block) => `
77498
+ Flag \`--git-changes continue\` found ${interruptedPrompts.length} interrupted prompts, but it can
77499
+ continue only one.
77500
+
77501
+ The working tree changes cannot be attributed to a single prompt while more than one of them carries
77502
+ the in-progress \`[^]\` status. Resolve the extra ones by marking them \`[x]\`, \`[!]\` or \`[ ]\`
77503
+ and run the coder again.
77504
+
77505
+ Interrupted prompts:
77506
+ ${block(interruptedPrompts
77507
+ .map(({ file, section }) => `- \`${buildPromptLabel(file, section)}\``)
77508
+ .join('\n'))}
77509
+ `));
77510
+ }
77511
+ return interruptedPrompts[0];
77512
+ }
77513
+
77175
77514
  /**
77176
77515
  * Prints a summary of the prompt that will run next.
77177
77516
  */
@@ -77238,7 +77577,7 @@ function $detectTerminalAnsiColorDepth() {
77238
77577
  * visuals are rendered through the shared canvas avatar pipeline into terminal ASCII art, using a
77239
77578
  * transparent horizontal canvas instead of the website's framed 1:1 surface.
77240
77579
  *
77241
- * The visual is decorative, so any failure returns `null` and the caller keeps the default brand banner.
77580
+ * The visual is decorative, so any failure returns `null` and the caller renders no header visual at all.
77242
77581
  *
77243
77582
  * @param agentSource Source of the `--agent` book file.
77244
77583
  * @returns ANSI-colored ASCII-art renderer or `null` when the visual cannot be rendered.
@@ -77400,6 +77739,8 @@ async function runCodexPrompts(providedOptions) {
77400
77739
  let previousRoundEndTime;
77401
77740
  let completedRunCount = 0;
77402
77741
  let hasRunTestBefore = false;
77742
+ // Note: Only the very first round resumes the interrupted prompt, every later round starts from a clean tree
77743
+ let isContinuingInterruptedPrompt = options.gitChanges === 'continue';
77403
77744
  while (just(true)) {
77404
77745
  if (options.autoPull && !options.dryRun) {
77405
77746
  await waitForRequestedPause({
@@ -77424,6 +77765,7 @@ async function runCodexPrompts(providedOptions) {
77424
77765
  progressDisplay,
77425
77766
  uiHandle,
77426
77767
  promptRunnerIdentity,
77768
+ isContinuingInterruptedPrompt,
77427
77769
  });
77428
77770
  }
77429
77771
  if (!hasRunTestBefore) {
@@ -77438,6 +77780,7 @@ async function runCodexPrompts(providedOptions) {
77438
77780
  uiHandle,
77439
77781
  waitForRequestedPause,
77440
77782
  hasWaitedForStart,
77783
+ isContinuingInterruptedPrompt,
77441
77784
  });
77442
77785
  hasRunTestBefore = true;
77443
77786
  }
@@ -77452,6 +77795,7 @@ async function runCodexPrompts(providedOptions) {
77452
77795
  progressDisplay,
77453
77796
  uiHandle,
77454
77797
  promptRunnerIdentity,
77798
+ isContinuingInterruptedPrompt,
77455
77799
  });
77456
77800
  hasShownUpcomingTasks || (hasShownUpcomingTasks = showUpcomingTasksOnce({
77457
77801
  hasShownUpcomingTasks,
@@ -77472,7 +77816,12 @@ async function runCodexPrompts(providedOptions) {
77472
77816
  }
77473
77817
  if (options.keepAlive) {
77474
77818
  announceKeepAliveStatus(promptQueueSnapshot, isRichUiEnabled, uiHandle);
77475
- await new Promise((resolve) => setTimeout(resolve, KEEP_ALIVE_POLL_INTERVAL_MS));
77819
+ // Note: The keep-alive poll runs in the `waiting` phase, where `S Skip current waiting`
77820
+ // is offered, so pressing `S` looks for new prompts right away
77821
+ await waitForSkippableWorldTimeDeadline({
77822
+ deadlineTimeMs: Date.now() + KEEP_ALIVE_POLL_INTERVAL_MS,
77823
+ pollIntervalMs: KEEP_ALIVE_POLL_INTERVAL_MS,
77824
+ });
77476
77825
  continue;
77477
77826
  }
77478
77827
  finishWhenNoPromptIsAvailable(promptQueueSnapshot, isRichUiEnabled, uiHandle);
@@ -77508,7 +77857,7 @@ async function runCodexPrompts(providedOptions) {
77508
77857
  progressDisplay,
77509
77858
  uiHandle,
77510
77859
  });
77511
- if (!options.ignoreGitChanges) {
77860
+ if (isCleanWorkingTreeRequired(options.gitChanges, isContinuingInterruptedPrompt)) {
77512
77861
  await waitForRequestedPause({
77513
77862
  checkpointLabel: 'checking the git working tree',
77514
77863
  phase: 'loading',
@@ -77532,6 +77881,7 @@ async function runCodexPrompts(providedOptions) {
77532
77881
  uiHandle,
77533
77882
  waitForRequestedPause,
77534
77883
  });
77884
+ isContinuingInterruptedPrompt = false;
77535
77885
  previousRoundStartTime = currentRoundStartTime;
77536
77886
  previousRoundEndTime = Date.now();
77537
77887
  completedRunCount += 1;
@@ -77575,9 +77925,9 @@ function validateRunCodexPromptOptions(options) {
77575
77925
  Flag \`--allow-destructive-auto-migrate\` requires \`--auto-migrate\`.
77576
77926
  `));
77577
77927
  }
77578
- if (options.noCommit && !options.waitForUser && !options.ignoreGitChanges) {
77928
+ if (options.noCommit && !options.waitForUser && options.gitChanges !== 'ignore') {
77579
77929
  throw new NotAllowed(spaceTrim$1(`
77580
- Flag \`--no-commit\` requires \`--ignore-git-changes\` when running in auto mode (the default; pass \`--no-auto\` for interactive confirmation).
77930
+ Flag \`--no-commit\` requires \`--git-changes ignore\` when running in auto mode (the default; pass \`--no-auto\` for interactive confirmation).
77581
77931
 
77582
77932
  Without commits, the next prompt round would fail the clean working tree check.
77583
77933
  `));
@@ -77596,6 +77946,13 @@ function validateRunCodexPromptOptions(options) {
77596
77946
  An isolated task is implemented in a temporary worktree and reaches the original branch only through a merge, which requires the round to end with a commit.
77597
77947
  `));
77598
77948
  }
77949
+ if (options.isIsolated && options.gitChanges === 'continue') {
77950
+ throw new NotAllowed(spaceTrim$1(`
77951
+ Flag \`--isolate\` cannot be combined with \`--git-changes continue\`.
77952
+
77953
+ An isolated task is implemented in a fresh temporary worktree checked out from the last commit, so the uncommitted changes of the interrupted prompt would be left behind instead of being continued.
77954
+ `));
77955
+ }
77599
77956
  if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit <= 0)) {
77600
77957
  throw new NotAllowed(spaceTrim$1(`
77601
77958
  Flag \`--limit\` expects a positive integer.
@@ -77604,6 +77961,18 @@ function validateRunCodexPromptOptions(options) {
77604
77961
  `));
77605
77962
  }
77606
77963
  }
77964
+ /**
77965
+ * Decides whether the working tree has to be verified clean before the next prompt starts.
77966
+ *
77967
+ * `--git-changes continue` waives the check for the single round which resumes the interrupted prompt,
77968
+ * because that round is started exactly for the uncommitted changes the interrupted prompt left behind.
77969
+ */
77970
+ function isCleanWorkingTreeRequired(gitChanges, isContinuingInterruptedPrompt) {
77971
+ if (gitChanges === 'ignore') {
77972
+ return false;
77973
+ }
77974
+ return !isContinuingInterruptedPrompt;
77975
+ }
77607
77976
  /**
77608
77977
  * Pulls the latest repository state before loading prompts when the feature is enabled.
77609
77978
  */
@@ -77662,7 +78031,7 @@ function normalizeRunOptions(options) {
77662
78031
  * Runs the optional pre-coding verification and, when requested, its one repair prompt.
77663
78032
  */
77664
78033
  async function runTestBeforeIfNeeded(options) {
77665
- const { options: runOptions, runner, runnerMetadata, resolvedCoderContext, resolvedAgentSystemMessage, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, hasWaitedForStart, } = options;
78034
+ const { options: runOptions, runner, runnerMetadata, resolvedCoderContext, resolvedAgentSystemMessage, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, hasWaitedForStart, isContinuingInterruptedPrompt, } = options;
77666
78035
  if (runOptions.testBefore === 'no') {
77667
78036
  return hasWaitedForStart;
77668
78037
  }
@@ -77673,7 +78042,7 @@ async function runTestBeforeIfNeeded(options) {
77673
78042
  Pass one with ${'`--test <test-command>`'} or use the default ${'`npm test`'} command by providing the mode through the CLI.
77674
78043
  `));
77675
78044
  }
77676
- if (!runOptions.ignoreGitChanges) {
78045
+ if (isCleanWorkingTreeRequired(runOptions.gitChanges, isContinuingInterruptedPrompt)) {
77677
78046
  await waitForRequestedPause({
77678
78047
  checkpointLabel: 'checking the git working tree before testing',
77679
78048
  phase: 'loading',
@@ -77842,9 +78211,9 @@ function initializeRunUi(uiHandle, runnerName, actualRunnerModel, options) {
77842
78211
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage(`Running prompts with ${runnerName}`);
77843
78212
  }
77844
78213
  /**
77845
- * Prepares the `--agent` book avatar ASCII-art renderer and shows it instead of the default brand banner.
78214
+ * Prepares the `--agent` book avatar ASCII-art renderer and shows it above the dashboard boxes.
77846
78215
  *
77847
- * Keeps the default banner when no agent is selected, the UI is disabled, or the visual cannot be rendered.
78216
+ * Leaves the header empty when no agent is selected, the UI is disabled, or the visual cannot be rendered.
77848
78217
  */
77849
78218
  async function initializeRunUiAgentVisual(uiHandle, agentSource) {
77850
78219
  if (!uiHandle || !agentSource) {
@@ -77859,7 +78228,7 @@ async function initializeRunUiAgentVisual(uiHandle, agentSource) {
77859
78228
  * Loads prompt files, updates progress displays, and selects the next runnable prompt.
77860
78229
  */
77861
78230
  async function loadPromptQueueSnapshot(options) {
77862
- const { options: runOptions, isRichUiEnabled, progressDisplay, uiHandle, promptRunnerIdentity } = options;
78231
+ const { options: runOptions, isRichUiEnabled, progressDisplay, uiHandle, promptRunnerIdentity, isContinuingInterruptedPrompt, } = options;
77863
78232
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setCurrentScriptPath(undefined);
77864
78233
  const promptFiles = await loadPromptFiles(PROMPTS_DIR$1);
77865
78234
  const stats = summarizePrompts(promptFiles, runOptions.priorityFilter);
@@ -77871,7 +78240,11 @@ async function loadPromptQueueSnapshot(options) {
77871
78240
  return {
77872
78241
  promptFiles,
77873
78242
  stats,
77874
- nextPrompt: findNextTodoPrompt(promptFiles, runOptions.priorityFilter, promptRunnerIdentity),
78243
+ // Note: A resumed prompt is picked by its `[^]` status alone, the priority and runner filters select
78244
+ // which prompt is started next, not which unfinished work is continued
78245
+ nextPrompt: isContinuingInterruptedPrompt
78246
+ ? resolveInterruptedPrompt(promptFiles)
78247
+ : findNextTodoPrompt(promptFiles, runOptions.priorityFilter, promptRunnerIdentity),
77875
78248
  };
77876
78249
  }
77877
78250
  /**