@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/umd/index.umd.js CHANGED
@@ -58,7 +58,7 @@
58
58
  * @generated
59
59
  * @see https://github.com/webgptorg/promptbook
60
60
  */
61
- const PROMPTBOOK_ENGINE_VERSION = '0.114.0-19';
61
+ const PROMPTBOOK_ENGINE_VERSION = '0.114.0-20';
62
62
  /**
63
63
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
64
64
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -1760,6 +1760,32 @@
1760
1760
  */
1761
1761
  const PTBK_THINKING_LEVEL_ENV = 'PTBK_THINKING_LEVEL';
1762
1762
 
1763
+ /**
1764
+ * Supported values of the `--git-changes` option, which decides what happens with a dirty working tree.
1765
+ *
1766
+ * - `fail` — refuse to start while the working tree has uncommitted changes
1767
+ * - `ignore` — start anyway and leave the uncommitted changes where they are
1768
+ * - `continue` — resume the single prompt which was left in the middle of its implementation
1769
+ *
1770
+ * @private internal shared utility of `ptbk coder run`
1771
+ */
1772
+ const GIT_CHANGES_MODE_VALUES = ['fail', 'ignore', 'continue'];
1773
+ /**
1774
+ * Mode applied when `--git-changes` is not used at all.
1775
+ *
1776
+ * @private internal shared utility of `ptbk coder run`
1777
+ */
1778
+ const DEFAULT_GIT_CHANGES_MODE = 'fail';
1779
+ /**
1780
+ * Checks whether a value is a supported `--git-changes` mode.
1781
+ *
1782
+ * @private internal shared utility of `ptbk coder run`
1783
+ */
1784
+ function isGitChangesMode(value) {
1785
+ return GIT_CHANGES_MODE_VALUES.includes(value);
1786
+ }
1787
+ // Note: [🟡] Code for CLI command [run](src/cli/cli-commands/coder/run.ts) should never be published outside of `@promptbook/cli`
1788
+
1763
1789
  /**
1764
1790
  * Supported thinking-level CLI values for coding-agent runners with configurable reasoning effort.
1765
1791
  *
@@ -1818,6 +1844,17 @@
1818
1844
 
1819
1845
  OpenAI examples: gpt-5.2-codex, default
1820
1846
  Gemini examples: gemini-3-flash-preview, default
1847
+ `);
1848
+ /**
1849
+ * Commander description for the `--git-changes` option.
1850
+ *
1851
+ * @private internal utility of `promptbookCli`
1852
+ */
1853
+ const GIT_CHANGES_OPTION_DESCRIPTION = _spaceTrim.spaceTrim(`
1854
+ Decide what happens when the working tree has uncommitted changes before a prompt starts:
1855
+ - fail: refuse to start and ask for a commit or a stash (the default)
1856
+ - ignore: start anyway and leave the uncommitted changes where they are
1857
+ - continue: resume the single prompt which was left in the middle of its implementation
1821
1858
  `);
1822
1859
  /**
1823
1860
  * Registers runner selection flags on a command.
@@ -1848,7 +1885,9 @@
1848
1885
  function addPromptRunnerExecutionOptions(command) {
1849
1886
  addPromptRunnerRuntimeOptions(command);
1850
1887
  command.option('--no-commit', 'Leave successful changes in the working directory instead of creating git commits');
1851
- command.option('--ignore-git-changes', 'Skip clean working tree check before running prompts', false);
1888
+ command.addOption(new commander.Option('--git-changes <mode>', GIT_CHANGES_OPTION_DESCRIPTION)
1889
+ .choices([...GIT_CHANGES_MODE_VALUES])
1890
+ .default(DEFAULT_GIT_CHANGES_MODE));
1852
1891
  command.option('--no-normalize-line-endings', 'Disable automatic LF normalization for files changed in each coding round');
1853
1892
  command.option('--auto-push', 'Automatically git push after each commit', false);
1854
1893
  command.option('--auto-pull', 'Automatically git pull before the first and each subsequent prompt', false);
@@ -1859,11 +1898,12 @@
1859
1898
  * @private internal utility of `promptbookCli`
1860
1899
  */
1861
1900
  function normalizePromptRunnerCliOptions(cliOptions, options) {
1901
+ var _a;
1862
1902
  const selectionOptions = normalizePromptRunnerSelectionCliOptions(cliOptions, options);
1863
1903
  return {
1864
1904
  ...selectionOptions,
1865
1905
  noCommit: !cliOptions.commit,
1866
- ignoreGitChanges: cliOptions.ignoreGitChanges,
1906
+ gitChanges: (_a = cliOptions.gitChanges) !== null && _a !== void 0 ? _a : DEFAULT_GIT_CHANGES_MODE,
1867
1907
  normalizeLineEndings: cliOptions.normalizeLineEndings,
1868
1908
  autoPush: cliOptions.autoPush,
1869
1909
  autoPull: cliOptions.autoPull,
@@ -22762,7 +22802,7 @@
22762
22802
  Git working tree has changes outside of \`${normalizeGitPath(AGENT_QUEUED_MESSAGES_DIRECTORY_PATH)}/\`.
22763
22803
 
22764
22804
  Please commit or stash unrelated changes before running \`ptbk agent-folder\`
22765
- OR run with \`--ignore-git-changes\`.
22805
+ OR run with \`--git-changes ignore\`.
22766
22806
 
22767
22807
  Unrelated changed files:
22768
22808
  ${unrelatedChangedFiles.map((relativePath) => `- \`${relativePath}\``).join('\n')}
@@ -22818,7 +22858,7 @@
22818
22858
  * Runs the clean working tree guard unless the user explicitly disabled it.
22819
22859
  */
22820
22860
  async function ensureCleanQueueIfNeeded(projectPath, options) {
22821
- if (options.ignoreGitChanges) {
22861
+ if (options.gitChanges !== 'fail') {
22822
22862
  return;
22823
22863
  }
22824
22864
  await ensureWorkingTreeCleanForAgentQueue(projectPath);
@@ -23340,6 +23380,58 @@
23340
23380
  */
23341
23381
  const WAITING_FOR_MESSAGE_LABEL = 'Waiting for the message';
23342
23382
 
23383
+ /**
23384
+ * Colors of the key badges, so one control key looks the same in its pill and in its answer.
23385
+ *
23386
+ * @private internal constant of `ptbk coder` terminal controls
23387
+ */
23388
+ const CODER_RUN_CONTROL_KEY_BADGE_COLORIZERS = {
23389
+ P: (badgeText) => colors__default["default"].bgYellow.black(badgeText),
23390
+ S: (badgeText) => colors__default["default"].bgCyan.black(badgeText),
23391
+ X: (badgeText) => colors__default["default"].bgBlue.white(badgeText),
23392
+ };
23393
+ /**
23394
+ * Builds the colored badge of one control key, for example ` S `.
23395
+ *
23396
+ * @private internal utility of `ptbk coder` terminal controls
23397
+ */
23398
+ function buildCoderRunControlKeyBadge(controlKey) {
23399
+ return CODER_RUN_CONTROL_KEY_BADGE_COLORIZERS[controlKey](` ${controlKey} `);
23400
+ }
23401
+
23402
+ /**
23403
+ * Marker printed before every control answer so it stands out from the runner output.
23404
+ *
23405
+ * @private internal constant of `ptbk coder` terminal controls
23406
+ */
23407
+ const CODER_RUN_CONTROL_FEEDBACK_MARKER = '»';
23408
+ /**
23409
+ * Colors of the control answers by their meaning.
23410
+ *
23411
+ * @private internal constant of `ptbk coder` terminal controls
23412
+ */
23413
+ const CODER_RUN_CONTROL_FEEDBACK_COLORIZERS = {
23414
+ success: (message) => colors__default["default"].green(message),
23415
+ info: (message) => colors__default["default"].cyan(message),
23416
+ warning: (message) => colors__default["default"].yellow(message),
23417
+ };
23418
+ /**
23419
+ * Formats one control answer as a single colored terminal line.
23420
+ *
23421
+ * The same line is printed by the plain console listener and rendered inside the `Controls` box of the
23422
+ * rich terminal UI, so both modes answer a pressed control key identically.
23423
+ *
23424
+ * @param feedback Answer of the runner to the pressed control key.
23425
+ * @param repeatCount How many times this exact answer was given in a row, rendered from the second time on.
23426
+ *
23427
+ * @private internal utility of `ptbk coder` terminal controls
23428
+ */
23429
+ function formatCoderRunControlFeedback(feedback, repeatCount = 1) {
23430
+ const colorizeMessage = CODER_RUN_CONTROL_FEEDBACK_COLORIZERS[feedback.tone];
23431
+ const repeatSuffix = repeatCount > 1 ? colors__default["default"].gray(` (×${repeatCount})`) : '';
23432
+ return `${colors__default["default"].gray(CODER_RUN_CONTROL_FEEDBACK_MARKER)} ${buildCoderRunControlKeyBadge(feedback.controlKey)} ${colorizeMessage(feedback.message)}${repeatSuffix}`;
23433
+ }
23434
+
23343
23435
  /**
23344
23436
  * Centers an ANSI-colored line within the available frame width.
23345
23437
  *
@@ -23514,7 +23606,8 @@
23514
23606
  return {
23515
23607
  badge: buildRunningPhaseBadge(phase),
23516
23608
  stateMessage: statusMessage,
23517
- pauseControl: colors__default["default"].bgYellow.black(' P ') + colors__default["default"].white(' Pause'),
23609
+ // Note: The two paused variants above deliberately recolor the `P` badge, everything else uses the shared one
23610
+ pauseControl: buildCoderRunControlKeyBadge('P') + colors__default["default"].white(' Pause'),
23518
23611
  };
23519
23612
  }
23520
23613
  /**
@@ -23555,20 +23648,34 @@
23555
23648
  ],
23556
23649
  });
23557
23650
  }
23651
+ /**
23652
+ * Builds the body of the `Controls` box.
23653
+ *
23654
+ * The pills are always shown, and right after a control key was pressed the answer of the runner to
23655
+ * that key is shown underneath, so no key press ever leaves the frame unchanged.
23656
+ */
23657
+ function buildControlsBoxLines(options) {
23658
+ const { controlPills, controlFeedback } = options;
23659
+ const controlPillsLine = controlPills.join(' ');
23660
+ if (controlFeedback === undefined) {
23661
+ return [controlPillsLine];
23662
+ }
23663
+ return [controlPillsLine, formatCoderRunControlFeedback(controlFeedback, controlFeedback.repeatCount)];
23664
+ }
23558
23665
  /**
23559
23666
  * Builds the `S` wait-skip control label.
23560
23667
  */
23561
23668
  function buildSkipCurrentWaitControl() {
23562
- return colors__default["default"].bgCyan.black(' S ') + colors__default["default"].white(' Skip current waiting');
23669
+ return buildCoderRunControlKeyBadge('S') + colors__default["default"].white(' Skip current waiting');
23563
23670
  }
23564
23671
  /**
23565
23672
  * Builds the dynamic `X` control label.
23566
23673
  */
23567
23674
  function buildEndAfterCurrentPromptControl(isEndAfterCurrentPromptRequested, sessionTotal) {
23568
23675
  if (isEndAfterCurrentPromptRequested) {
23569
- return colors__default["default"].bgBlue.white(' X ') + colors__default["default"].white(` Do all ${formatControlPromptCount(sessionTotal)}`);
23676
+ return buildCoderRunControlKeyBadge('X') + colors__default["default"].white(` Do all ${formatControlPromptCount(sessionTotal)}`);
23570
23677
  }
23571
- return colors__default["default"].bgBlue.white(' X ') + colors__default["default"].white(' End with this prompt');
23678
+ return buildCoderRunControlKeyBadge('X') + colors__default["default"].white(' End with this prompt');
23572
23679
  }
23573
23680
  /**
23574
23681
  * Formats one prompt count for the `X` control.
@@ -23786,10 +23893,13 @@
23786
23893
  : [options.statusMessage, ...options.detailLines.map((detailLine) => `• ${detailLine}`)];
23787
23894
  const userMessageBoxes = buildUserMessageBoxes(options, totalWidth);
23788
23895
  const visibleOutputLines = buildVisibleOutputLines(options.agentOutputLines);
23789
- const controls = buildControlPills({
23790
- pauseControl: pausePresentation.pauseControl,
23791
- pendingEnterLabel: options.pendingEnterLabel,
23792
- }).join(' ');
23896
+ const controlsBoxLines = buildControlsBoxLines({
23897
+ controlPills: buildControlPills({
23898
+ pauseControl: pausePresentation.pauseControl,
23899
+ pendingEnterLabel: options.pendingEnterLabel,
23900
+ }),
23901
+ controlFeedback: options.controlFeedback,
23902
+ });
23793
23903
  const frame = [
23794
23904
  ...buildAgentRunInitialsVisual(options.config.localAgentName || 'Local Agent', totalWidth),
23795
23905
  '',
@@ -23802,7 +23912,7 @@
23802
23912
  if (options.errors.length > 0) {
23803
23913
  frame.push(...renderBox('Errors', options.errors.map((errorLine) => `${colors__default["default"].red('✗')} ${errorLine}`), totalWidth, colors__default["default"].red.bold));
23804
23914
  }
23805
- frame.push(...renderBox('Controls', [controls], totalWidth, colors__default["default"].white.bold));
23915
+ frame.push(...renderBox('Controls', controlsBoxLines, totalWidth, colors__default["default"].white.bold));
23806
23916
  return frame;
23807
23917
  }
23808
23918
  /**
@@ -25124,6 +25234,254 @@
25124
25234
  });
25125
25235
  }
25126
25236
 
25237
+ /**
25238
+ * Default label used before the next pause checkpoint is known.
25239
+ */
25240
+ const DEFAULT_PAUSE_TARGET_LABEL = 'the next task';
25241
+ /**
25242
+ * Current pause state.
25243
+ */
25244
+ let pauseState = 'RUNNING';
25245
+ /**
25246
+ * Label of the next checkpoint where the requested pause will take effect.
25247
+ */
25248
+ let pauseTargetLabel = DEFAULT_PAUSE_TARGET_LABEL;
25249
+ /**
25250
+ * Token of the currently active timed wait, if the runner is inside one.
25251
+ */
25252
+ let activeSkippableWaitToken;
25253
+ /**
25254
+ * Whether the user requested the active timed wait to end immediately.
25255
+ */
25256
+ let isSkipCurrentWaitRequested = false;
25257
+ /**
25258
+ * Whether the runner should stop after the current prompt round reaches its normal boundary.
25259
+ */
25260
+ let isEndAfterCurrentPromptRequested$1 = false;
25261
+ /**
25262
+ * Promise resolvers waiting for the active timed wait to be skipped.
25263
+ */
25264
+ const SKIP_CURRENT_WAIT_RESOLVERS = new Map();
25265
+ /**
25266
+ * Stores one new pause state in the shared runner controller.
25267
+ */
25268
+ function setPauseState(nextPauseState) {
25269
+ pauseState = nextPauseState;
25270
+ }
25271
+ /**
25272
+ * Stores one new pause target label in the shared runner controller.
25273
+ */
25274
+ function setPauseTargetLabel(nextPauseTargetLabel) {
25275
+ pauseTargetLabel = nextPauseTargetLabel.trim() || DEFAULT_PAUSE_TARGET_LABEL;
25276
+ }
25277
+ /**
25278
+ * Stores the dynamic end-after-current-prompt state.
25279
+ */
25280
+ function setEndAfterCurrentPromptRequested(isRequested) {
25281
+ isEndAfterCurrentPromptRequested$1 = isRequested;
25282
+ }
25283
+ /**
25284
+ * Applies the same three-state toggle used by the `P` hotkey.
25285
+ */
25286
+ function togglePauseState() {
25287
+ if (pauseState === 'RUNNING') {
25288
+ setPauseState('PAUSING');
25289
+ return 'REQUESTED_PAUSE';
25290
+ }
25291
+ if (pauseState === 'PAUSING') {
25292
+ setPauseState('RUNNING');
25293
+ resetPauseTargetLabel();
25294
+ return 'CANCELLED_PAUSE';
25295
+ }
25296
+ setPauseState('RUNNING');
25297
+ resetPauseTargetLabel();
25298
+ return 'RESUMED';
25299
+ }
25300
+ /**
25301
+ * Applies the two-state toggle used by the `X` hotkey.
25302
+ */
25303
+ function toggleEndAfterCurrentPromptState() {
25304
+ if (!isEndAfterCurrentPromptRequested$1) {
25305
+ setEndAfterCurrentPromptRequested(true);
25306
+ return 'REQUESTED_END';
25307
+ }
25308
+ setEndAfterCurrentPromptRequested(false);
25309
+ return 'CANCELLED_END';
25310
+ }
25311
+ /**
25312
+ * Returns whether the dynamic end-after-current-prompt control is active.
25313
+ */
25314
+ function getEndAfterCurrentPromptState() {
25315
+ return isEndAfterCurrentPromptRequested$1;
25316
+ }
25317
+ /**
25318
+ * Starts one timed wait which can be skipped by the `S` hotkey.
25319
+ */
25320
+ function beginSkippableWait() {
25321
+ const waitToken = Symbol('coder-run-skippable-wait');
25322
+ activeSkippableWaitToken = waitToken;
25323
+ isSkipCurrentWaitRequested = false;
25324
+ SKIP_CURRENT_WAIT_RESOLVERS.clear();
25325
+ return waitToken;
25326
+ }
25327
+ /**
25328
+ * Finishes one timed wait and clears any skip request that belonged to it.
25329
+ */
25330
+ function finishSkippableWait(waitToken) {
25331
+ if (activeSkippableWaitToken !== waitToken) {
25332
+ return;
25333
+ }
25334
+ activeSkippableWaitToken = undefined;
25335
+ isSkipCurrentWaitRequested = false;
25336
+ resolveSkipCurrentWaitResolvers(waitToken);
25337
+ SKIP_CURRENT_WAIT_RESOLVERS.delete(waitToken);
25338
+ }
25339
+ /**
25340
+ * Requests that the currently active timed wait ends immediately.
25341
+ */
25342
+ function requestSkipCurrentWait() {
25343
+ if (activeSkippableWaitToken === undefined) {
25344
+ return 'NO_ACTIVE_WAIT';
25345
+ }
25346
+ isSkipCurrentWaitRequested = true;
25347
+ resolveSkipCurrentWaitResolvers(activeSkippableWaitToken);
25348
+ return 'REQUESTED_SKIP';
25349
+ }
25350
+ /**
25351
+ * Returns whether one timed wait should end early.
25352
+ */
25353
+ function shouldSkipCurrentWait(waitToken) {
25354
+ return activeSkippableWaitToken === waitToken && isSkipCurrentWaitRequested;
25355
+ }
25356
+ /**
25357
+ * Waits for either a timeout or an `S` hotkey skip request.
25358
+ */
25359
+ async function waitForSkippableMilliseconds(waitToken, durationMs) {
25360
+ if (durationMs <= 0 || shouldSkipCurrentWait(waitToken)) {
25361
+ return;
25362
+ }
25363
+ await new Promise((resolve) => {
25364
+ let timeout = setTimeout(resolveWait, durationMs);
25365
+ const resolver = () => resolveWait();
25366
+ const resolvers = getSkipCurrentWaitResolvers(waitToken);
25367
+ resolvers.add(resolver);
25368
+ function resolveWait() {
25369
+ if (timeout !== undefined) {
25370
+ clearTimeout(timeout);
25371
+ timeout = undefined;
25372
+ }
25373
+ resolvers.delete(resolver);
25374
+ if (resolvers.size === 0) {
25375
+ SKIP_CURRENT_WAIT_RESOLVERS.delete(waitToken);
25376
+ }
25377
+ resolve();
25378
+ }
25379
+ });
25380
+ }
25381
+ /**
25382
+ * Restores all shared terminal controls to their default state.
25383
+ */
25384
+ function resetCoderRunControls() {
25385
+ setPauseState('RUNNING');
25386
+ resetPauseTargetLabel();
25387
+ setEndAfterCurrentPromptRequested(false);
25388
+ if (activeSkippableWaitToken !== undefined) {
25389
+ resolveSkipCurrentWaitResolvers(activeSkippableWaitToken);
25390
+ }
25391
+ activeSkippableWaitToken = undefined;
25392
+ isSkipCurrentWaitRequested = false;
25393
+ SKIP_CURRENT_WAIT_RESOLVERS.clear();
25394
+ }
25395
+ /**
25396
+ * If the execution is paused, it will wait until it is resumed.
25397
+ *
25398
+ * @param options.silent - When `true`, suppresses console output (used when the terminal UI handles display).
25399
+ * @param options.onPaused - Callback invoked when entering the PAUSED state.
25400
+ * @param options.onResumed - Callback invoked when leaving the PAUSED state.
25401
+ */
25402
+ async function checkPause(options) {
25403
+ var _a, _b;
25404
+ if (pauseState === 'PAUSING') {
25405
+ setPauseState('PAUSED');
25406
+ if (!(options === null || options === void 0 ? void 0 : options.silent)) {
25407
+ console.log(colors__default["default"].bgWhite.black(`Paused before ${getPauseTargetLabel()}`) +
25408
+ colors__default["default"].gray(' (Press "p" to resume)'));
25409
+ }
25410
+ (_a = options === null || options === void 0 ? void 0 : options.onPaused) === null || _a === void 0 ? void 0 : _a.call(options);
25411
+ while (getPauseState() === 'PAUSED') {
25412
+ await new Promise((resolve) => setTimeout(resolve, 100));
25413
+ }
25414
+ (_b = options === null || options === void 0 ? void 0 : options.onResumed) === null || _b === void 0 ? void 0 : _b.call(options);
25415
+ if (!(options === null || options === void 0 ? void 0 : options.silent)) {
25416
+ console.log(colors__default["default"].green('Resuming...'));
25417
+ }
25418
+ }
25419
+ }
25420
+ /**
25421
+ * Returns the current pause state for external consumers such as the terminal UI.
25422
+ */
25423
+ function getPauseState() {
25424
+ return pauseState;
25425
+ }
25426
+ /**
25427
+ * Returns the label of the next checkpoint where pausing will take effect.
25428
+ */
25429
+ function getPauseTargetLabel() {
25430
+ return pauseTargetLabel;
25431
+ }
25432
+ /**
25433
+ * Updates the label of the next pause checkpoint.
25434
+ */
25435
+ function announcePauseTargetLabel(nextPauseTargetLabel) {
25436
+ setPauseTargetLabel(nextPauseTargetLabel);
25437
+ }
25438
+ /**
25439
+ * Restores the default generic pause target label.
25440
+ */
25441
+ function resetPauseTargetLabel() {
25442
+ setPauseTargetLabel(DEFAULT_PAUSE_TARGET_LABEL);
25443
+ }
25444
+ /**
25445
+ * Requests a pause from an external controller (e.g. the Ink UI).
25446
+ */
25447
+ function requestPause() {
25448
+ if (pauseState === 'RUNNING') {
25449
+ setPauseState('PAUSING');
25450
+ }
25451
+ }
25452
+ /**
25453
+ * Resumes execution from an external controller after a pause.
25454
+ */
25455
+ function requestResume() {
25456
+ setPauseState('RUNNING');
25457
+ resetPauseTargetLabel();
25458
+ }
25459
+ /**
25460
+ * Gets or creates the resolver set for one timed wait.
25461
+ */
25462
+ function getSkipCurrentWaitResolvers(waitToken) {
25463
+ const existingResolvers = SKIP_CURRENT_WAIT_RESOLVERS.get(waitToken);
25464
+ if (existingResolvers !== undefined) {
25465
+ return existingResolvers;
25466
+ }
25467
+ const resolvers = new Set();
25468
+ SKIP_CURRENT_WAIT_RESOLVERS.set(waitToken, resolvers);
25469
+ return resolvers;
25470
+ }
25471
+ /**
25472
+ * Resolves all pending skip waiters for one timed wait.
25473
+ */
25474
+ function resolveSkipCurrentWaitResolvers(waitToken) {
25475
+ const resolvers = SKIP_CURRENT_WAIT_RESOLVERS.get(waitToken);
25476
+ if (resolvers === undefined) {
25477
+ return;
25478
+ }
25479
+ for (const resolver of resolvers) {
25480
+ resolver();
25481
+ }
25482
+ resolvers.clear();
25483
+ }
25484
+
25127
25485
  /**
25128
25486
  * Minimum timer duration used to avoid a zero-millisecond polling loop.
25129
25487
  */
@@ -25173,6 +25531,32 @@
25173
25531
  await new Promise((resolve) => setTimeout(resolve, durationMs));
25174
25532
  }
25175
25533
 
25534
+ /**
25535
+ * Waits until one wall-clock deadline has passed, or until the user skips the wait with the `S` control.
25536
+ *
25537
+ * This is the single way `ptbk coder` waits for a deadline the user is allowed to cut short, so every
25538
+ * wait which shows the `S Skip current waiting` control really reacts to it: the pacing waits between
25539
+ * prompts, the cool-down after an error, the harness session-limit waits and the server keep-alive poll.
25540
+ *
25541
+ * @private internal utility of `ptbk coder` wait handling
25542
+ */
25543
+ async function waitForSkippableWorldTimeDeadline(options) {
25544
+ const { deadlineTimeMs, pollIntervalMs, onTick } = options;
25545
+ const waitToken = beginSkippableWait();
25546
+ try {
25547
+ await waitUntilWorldTimeDeadline({
25548
+ deadlineTimeMs,
25549
+ pollIntervalMs,
25550
+ onTick,
25551
+ shouldStopWaiting: () => shouldSkipCurrentWait(waitToken),
25552
+ waitForMilliseconds: (waitDurationMs) => waitForSkippableMilliseconds(waitToken, waitDurationMs),
25553
+ });
25554
+ }
25555
+ finally {
25556
+ finishSkippableWait(waitToken);
25557
+ }
25558
+ }
25559
+
25176
25560
  /**
25177
25561
  * Base delimiter used for passing large prompts through stdin.
25178
25562
  */
@@ -25580,6 +25964,10 @@
25580
25964
  }
25581
25965
  /**
25582
25966
  * Waits until the Claude Code session can be resumed, keeping terminal status clear.
25967
+ *
25968
+ * The wait runs in the `waiting` phase, where the terminal UI offers `S Skip current waiting`, so it is
25969
+ * a skippable wait: pressing `S` resumes the session with `--resume` immediately instead of sitting out
25970
+ * the reset window.
25583
25971
  */
25584
25972
  async function waitForClaudeCodeSessionLimitReset(sessionLimit, resurrectionCount, options) {
25585
25973
  var _a, _b;
@@ -25590,7 +25978,7 @@
25590
25978
  if ((_a = options.shouldPrintLiveOutput) !== null && _a !== void 0 ? _a : true) {
25591
25979
  console.warn(colors__default["default"].yellow(`[claude-code] Session limit detected for ${sessionLimit.sessionId}. Resurrection #${resurrectionCount} will resume with --resume after ${formatDurationMs(delayMs)}. ${resetSummary}`));
25592
25980
  }
25593
- await waitUntilWorldTimeDeadline({
25981
+ await waitForSkippableWorldTimeDeadline({
25594
25982
  deadlineTimeMs: resetDeadlineTimeMs,
25595
25983
  pollIntervalMs: CLAUDE_CODE_SESSION_RESURRECTION_POLL_MS,
25596
25984
  onTick: async (remainingDelayMs) => {
@@ -27029,292 +27417,100 @@
27029
27417
  }
27030
27418
 
27031
27419
  /**
27032
- * Default label used before the next pause checkpoint is known.
27033
- */
27034
- const DEFAULT_PAUSE_TARGET_LABEL = 'the next task';
27035
- /**
27036
- * Current pause state.
27037
- */
27038
- let pauseState = 'RUNNING';
27039
- /**
27040
- * Label of the next checkpoint where the requested pause will take effect.
27041
- */
27042
- let pauseTargetLabel = DEFAULT_PAUSE_TARGET_LABEL;
27043
- /**
27044
- * Token of the currently active timed wait, if the runner is inside one.
27045
- */
27046
- let activeSkippableWaitToken;
27047
- /**
27048
- * Whether the user requested the active timed wait to end immediately.
27049
- */
27050
- let isSkipCurrentWaitRequested = false;
27051
- /**
27052
- * Whether the runner should stop after the current prompt round reaches its normal boundary.
27053
- */
27054
- let isEndAfterCurrentPromptRequested$1 = false;
27055
- /**
27056
- * Promise resolvers waiting for the active timed wait to be skipped.
27057
- */
27058
- const SKIP_CURRENT_WAIT_RESOLVERS = new Map();
27059
- /**
27060
- * Stores one new pause state in the shared runner controller.
27061
- */
27062
- function setPauseState(nextPauseState) {
27063
- pauseState = nextPauseState;
27064
- }
27065
- /**
27066
- * Stores one new pause target label in the shared runner controller.
27067
- */
27068
- function setPauseTargetLabel(nextPauseTargetLabel) {
27069
- pauseTargetLabel = nextPauseTargetLabel.trim() || DEFAULT_PAUSE_TARGET_LABEL;
27070
- }
27071
- /**
27072
- * Stores the dynamic end-after-current-prompt state.
27073
- */
27074
- function setEndAfterCurrentPromptRequested(isRequested) {
27075
- isEndAfterCurrentPromptRequested$1 = isRequested;
27076
- }
27077
- /**
27078
- * Applies the same three-state toggle used by the `P` hotkey.
27079
- */
27080
- function togglePauseState() {
27081
- if (pauseState === 'RUNNING') {
27082
- setPauseState('PAUSING');
27083
- return 'REQUESTED_PAUSE';
27084
- }
27085
- if (pauseState === 'PAUSING') {
27086
- setPauseState('RUNNING');
27087
- resetPauseTargetLabel();
27088
- return 'CANCELLED_PAUSE';
27089
- }
27090
- setPauseState('RUNNING');
27091
- resetPauseTargetLabel();
27092
- return 'RESUMED';
27093
- }
27094
- /**
27095
- * Applies the two-state toggle used by the `X` hotkey.
27096
- */
27097
- function toggleEndAfterCurrentPromptState() {
27098
- if (!isEndAfterCurrentPromptRequested$1) {
27099
- setEndAfterCurrentPromptRequested(true);
27100
- return 'REQUESTED_END';
27101
- }
27102
- setEndAfterCurrentPromptRequested(false);
27103
- return 'CANCELLED_END';
27104
- }
27105
- /**
27106
- * Returns whether the dynamic end-after-current-prompt control is active.
27107
- */
27108
- function getEndAfterCurrentPromptState() {
27109
- return isEndAfterCurrentPromptRequested$1;
27110
- }
27111
- /**
27112
- * Starts one timed wait which can be skipped by the `S` hotkey.
27113
- */
27114
- function beginSkippableWait() {
27115
- const waitToken = Symbol('coder-run-skippable-wait');
27116
- activeSkippableWaitToken = waitToken;
27117
- isSkipCurrentWaitRequested = false;
27118
- SKIP_CURRENT_WAIT_RESOLVERS.clear();
27119
- return waitToken;
27120
- }
27121
- /**
27122
- * Finishes one timed wait and clears any skip request that belonged to it.
27123
- */
27124
- function finishSkippableWait(waitToken) {
27125
- if (activeSkippableWaitToken !== waitToken) {
27126
- return;
27127
- }
27128
- activeSkippableWaitToken = undefined;
27129
- isSkipCurrentWaitRequested = false;
27130
- resolveSkipCurrentWaitResolvers(waitToken);
27131
- SKIP_CURRENT_WAIT_RESOLVERS.delete(waitToken);
27132
- }
27133
- /**
27134
- * Requests that the currently active timed wait ends immediately.
27135
- */
27136
- function requestSkipCurrentWait() {
27137
- if (activeSkippableWaitToken === undefined) {
27138
- return 'NO_ACTIVE_WAIT';
27139
- }
27140
- isSkipCurrentWaitRequested = true;
27141
- resolveSkipCurrentWaitResolvers(activeSkippableWaitToken);
27142
- return 'REQUESTED_SKIP';
27143
- }
27144
- /**
27145
- * Returns whether one timed wait should end early.
27146
- */
27147
- function shouldSkipCurrentWait(waitToken) {
27148
- return activeSkippableWaitToken === waitToken && isSkipCurrentWaitRequested;
27149
- }
27150
- /**
27151
- * Waits for either a timeout or an `S` hotkey skip request.
27420
+ * Applies the terminal control bound to one pressed key and describes what that key press did.
27421
+ *
27422
+ * This is the single place where the `P`, `S` and `X` keys are turned into runner actions, so the
27423
+ * plain console listener and the rich terminal UI always perform the same action and always answer
27424
+ * the user, even when the pressed control changed nothing.
27425
+ *
27426
+ * @param keyName Name of the pressed key as reported by the `keypress` event.
27427
+ * @returns The answer shown to the user, or `undefined` when the key is not a control key.
27428
+ *
27429
+ * @private internal utility of `ptbk coder` terminal controls
27152
27430
  */
27153
- async function waitForSkippableMilliseconds(waitToken, durationMs) {
27154
- if (durationMs <= 0 || shouldSkipCurrentWait(waitToken)) {
27155
- return;
27431
+ function applyCoderRunControlKey(keyName) {
27432
+ switch (keyName) {
27433
+ case 'p':
27434
+ return describePauseToggle(togglePauseState());
27435
+ case 's':
27436
+ return describeSkipCurrentWait(requestSkipCurrentWait());
27437
+ case 'x':
27438
+ return describeEndAfterCurrentPrompt(toggleEndAfterCurrentPromptState());
27439
+ default:
27440
+ return undefined;
27156
27441
  }
27157
- await new Promise((resolve) => {
27158
- let timeout = setTimeout(resolveWait, durationMs);
27159
- const resolver = () => resolveWait();
27160
- const resolvers = getSkipCurrentWaitResolvers(waitToken);
27161
- resolvers.add(resolver);
27162
- function resolveWait() {
27163
- if (timeout !== undefined) {
27164
- clearTimeout(timeout);
27165
- timeout = undefined;
27166
- }
27167
- resolvers.delete(resolver);
27168
- if (resolvers.size === 0) {
27169
- SKIP_CURRENT_WAIT_RESOLVERS.delete(waitToken);
27170
- }
27171
- resolve();
27172
- }
27173
- });
27174
27442
  }
27175
27443
  /**
27176
- * Restores all shared terminal controls to their default state.
27444
+ * Describes what pressing `P` did.
27445
+ *
27446
+ * @private helper of `applyCoderRunControlKey`
27177
27447
  */
27178
- function resetCoderRunControls() {
27179
- setPauseState('RUNNING');
27180
- resetPauseTargetLabel();
27181
- setEndAfterCurrentPromptRequested(false);
27182
- if (activeSkippableWaitToken !== undefined) {
27183
- resolveSkipCurrentWaitResolvers(activeSkippableWaitToken);
27448
+ function describePauseToggle(toggleResult) {
27449
+ if (toggleResult === 'REQUESTED_PAUSE') {
27450
+ return {
27451
+ controlKey: 'P',
27452
+ message: `Pause requested, pausing before ${getPauseTargetLabel()}`,
27453
+ tone: 'warning',
27454
+ };
27184
27455
  }
27185
- activeSkippableWaitToken = undefined;
27186
- isSkipCurrentWaitRequested = false;
27187
- SKIP_CURRENT_WAIT_RESOLVERS.clear();
27188
- }
27189
- /**
27190
- * Listens for the terminal control keys.
27191
- */
27192
- function listenForCoderRunControls() {
27193
- if (!process.stdin.isTTY) {
27194
- return;
27456
+ if (toggleResult === 'CANCELLED_PAUSE') {
27457
+ return {
27458
+ controlKey: 'P',
27459
+ message: 'Pause cancelled, the run continues',
27460
+ tone: 'success',
27461
+ };
27195
27462
  }
27196
- readline__namespace.emitKeypressEvents(process.stdin);
27197
- process.stdin.setRawMode(true);
27198
- process.stdin.on('keypress', (str, key) => {
27199
- if (key.ctrl && key.name === 'c') {
27200
- process.exit();
27201
- }
27202
- if (key.name === 'p') {
27203
- const toggleResult = togglePauseState();
27204
- if (toggleResult === 'REQUESTED_PAUSE') {
27205
- // Note: Using console.log here which adds a new line.
27206
- // This is intentional to prevent the message from being overwritten.
27207
- console.log(colors__default["default"].bgWhite('Pausing...'));
27208
- }
27209
- else if (toggleResult === 'CANCELLED_PAUSE') {
27210
- console.log(colors__default["default"].green('Pause cancelled. Resuming...'));
27211
- }
27212
- }
27213
- if (key.name === 's') {
27214
- if (requestSkipCurrentWait() === 'REQUESTED_SKIP') {
27215
- console.log(colors__default["default"].green('Skipping current wait...'));
27216
- }
27217
- }
27218
- if (key.name === 'x') {
27219
- const toggleResult = toggleEndAfterCurrentPromptState();
27220
- if (toggleResult === 'REQUESTED_END') {
27221
- console.log(colors__default["default"].yellow('Will end after the current prompt finishes.'));
27222
- }
27223
- else {
27224
- console.log(colors__default["default"].green('End request cancelled. Continuing all prompts.'));
27225
- }
27226
- }
27227
- });
27463
+ return {
27464
+ controlKey: 'P',
27465
+ message: 'Resuming the run',
27466
+ tone: 'success',
27467
+ };
27228
27468
  }
27229
27469
  /**
27230
- * If the execution is paused, it will wait until it is resumed.
27470
+ * Describes what pressing `S` did.
27231
27471
  *
27232
- * @param options.silent - When `true`, suppresses console output (used when the terminal UI handles display).
27233
- * @param options.onPaused - Callback invoked when entering the PAUSED state.
27234
- * @param options.onResumed - Callback invoked when leaving the PAUSED state.
27235
- */
27236
- async function checkPause(options) {
27237
- var _a, _b;
27238
- if (pauseState === 'PAUSING') {
27239
- setPauseState('PAUSED');
27240
- if (!(options === null || options === void 0 ? void 0 : options.silent)) {
27241
- console.log(colors__default["default"].bgWhite.black(`Paused before ${getPauseTargetLabel()}`) +
27242
- colors__default["default"].gray(' (Press "p" to resume)'));
27243
- }
27244
- (_a = options === null || options === void 0 ? void 0 : options.onPaused) === null || _a === void 0 ? void 0 : _a.call(options);
27245
- while (getPauseState() === 'PAUSED') {
27246
- await new Promise((resolve) => setTimeout(resolve, 100));
27247
- }
27248
- (_b = options === null || options === void 0 ? void 0 : options.onResumed) === null || _b === void 0 ? void 0 : _b.call(options);
27249
- if (!(options === null || options === void 0 ? void 0 : options.silent)) {
27250
- console.log(colors__default["default"].green('Resuming...'));
27251
- }
27252
- }
27253
- }
27254
- /**
27255
- * Returns the current pause state for external consumers such as the terminal UI.
27472
+ * @private helper of `applyCoderRunControlKey`
27256
27473
  */
27257
- function getPauseState() {
27258
- return pauseState;
27259
- }
27260
- /**
27261
- * Returns the label of the next checkpoint where pausing will take effect.
27262
- */
27263
- function getPauseTargetLabel() {
27264
- return pauseTargetLabel;
27265
- }
27266
- /**
27267
- * Updates the label of the next pause checkpoint.
27268
- */
27269
- function announcePauseTargetLabel(nextPauseTargetLabel) {
27270
- setPauseTargetLabel(nextPauseTargetLabel);
27271
- }
27272
- /**
27273
- * Restores the default generic pause target label.
27274
- */
27275
- function resetPauseTargetLabel() {
27276
- setPauseTargetLabel(DEFAULT_PAUSE_TARGET_LABEL);
27277
- }
27278
- /**
27279
- * Requests a pause from an external controller (e.g. the Ink UI).
27280
- */
27281
- function requestPause() {
27282
- if (pauseState === 'RUNNING') {
27283
- setPauseState('PAUSING');
27474
+ function describeSkipCurrentWait(skipResult) {
27475
+ if (skipResult === 'REQUESTED_SKIP') {
27476
+ return {
27477
+ controlKey: 'S',
27478
+ message: 'Skipping the current waiting, continuing right now',
27479
+ tone: 'success',
27480
+ };
27284
27481
  }
27482
+ return {
27483
+ controlKey: 'S',
27484
+ message: 'Nothing to skip, the coder is not waiting right now',
27485
+ tone: 'info',
27486
+ };
27285
27487
  }
27286
27488
  /**
27287
- * Resumes execution from an external controller after a pause.
27288
- */
27289
- function requestResume() {
27290
- setPauseState('RUNNING');
27291
- resetPauseTargetLabel();
27292
- }
27293
- /**
27294
- * Gets or creates the resolver set for one timed wait.
27489
+ * Describes what pressing `X` did.
27490
+ *
27491
+ * @private helper of `applyCoderRunControlKey`
27295
27492
  */
27296
- function getSkipCurrentWaitResolvers(waitToken) {
27297
- const existingResolvers = SKIP_CURRENT_WAIT_RESOLVERS.get(waitToken);
27298
- if (existingResolvers !== undefined) {
27299
- return existingResolvers;
27493
+ function describeEndAfterCurrentPrompt(toggleResult) {
27494
+ if (toggleResult === 'REQUESTED_END') {
27495
+ return {
27496
+ controlKey: 'X',
27497
+ message: 'Ending after the current prompt finishes',
27498
+ tone: 'warning',
27499
+ };
27300
27500
  }
27301
- const resolvers = new Set();
27302
- SKIP_CURRENT_WAIT_RESOLVERS.set(waitToken, resolvers);
27303
- return resolvers;
27501
+ return {
27502
+ controlKey: 'X',
27503
+ message: 'End cancelled, the whole queue will be processed',
27504
+ tone: 'success',
27505
+ };
27304
27506
  }
27507
+
27305
27508
  /**
27306
- * Resolves all pending skip waiters for one timed wait.
27509
+ * How long one control answer stays visible in the rich terminal UI.
27510
+ *
27511
+ * @private internal constant of `ptbk coder` terminal controls
27307
27512
  */
27308
- function resolveSkipCurrentWaitResolvers(waitToken) {
27309
- const resolvers = SKIP_CURRENT_WAIT_RESOLVERS.get(waitToken);
27310
- if (resolvers === undefined) {
27311
- return;
27312
- }
27313
- for (const resolver of resolvers) {
27314
- resolver();
27315
- }
27316
- resolvers.clear();
27317
- }
27513
+ const CODER_RUN_CONTROL_FEEDBACK_DURATION_MS = 5000;
27318
27514
 
27319
27515
  /**
27320
27516
  * Human-readable label for a priority scope without any bounds.
@@ -27439,85 +27635,696 @@
27439
27635
  // Note: [🟡] Code for priority filtering [priorityFilter](scripts/run-codex-prompts/prompts/priorityFilter.ts) should never be published outside of `@promptbook/cli`
27440
27636
 
27441
27637
  /**
27442
- * Fixed left-side head used across all octopus frames.
27638
+ * Minimum width used for the rich coder-run frame.
27443
27639
  */
27444
- const OCTOPUS_HEAD_LINES = [
27445
- colors__default["default"].magenta.bold(' .-""""-.'),
27446
- colors__default["default"].magenta.bold(" .' .-. '."),
27447
- `${colors__default["default"].magenta.bold(' / (')}${colors__default["default"].yellow.bold('o o')}${colors__default["default"].magenta.bold(') \\')}`,
27448
- colors__default["default"].magenta.bold(' | ^ |'),
27449
- colors__default["default"].magenta.bold(' | \\___/ |'),
27450
- colors__default["default"].magenta.bold(' \\___________/'),
27451
- ];
27640
+ const MIN_FRAME_WIDTH = 56;
27452
27641
  /**
27453
- * Gap between the head silhouette and the animated tentacles.
27642
+ * Maximum width used for the rich coder-run frame.
27454
27643
  */
27455
- const OCTOPUS_HEAD_TO_TENTACLE_GAP = ' ';
27644
+ const MAX_FRAME_WIDTH = 96;
27456
27645
  /**
27457
- * Animated right-side tentacle poses.
27646
+ * Builds the complete boxed terminal frame for the rich `ptbk coder run` UI.
27458
27647
  */
27459
- const OCTOPUS_TENTACLE_FRAMES = [
27460
- [
27461
- ` ${colors__default["default"].green.bold('ptbk.io')}`,
27462
- colors__default["default"].cyan(' __ __'),
27463
- colors__default["default"].cyan(' _/\\/\\/ \\_/\\/ \\_/\\_'),
27464
- colors__default["default"].cyan('_/\\/ _ /\\/ _ /\\/\\__'),
27465
- colors__default["default"].cyan('\\__/ /_/\\/ \\_/ \\_/ \\_/'),
27466
- colors__default["default"].cyan(' /_/ \\__/ \\__/ /'),
27467
- ],
27468
- [
27469
- ` ${colors__default["default"].green.bold('ptbk.io')}`,
27470
- colors__default["default"].cyan(' __ __'),
27471
- colors__default["default"].cyan(' _/\\/ \\_/\\/\\_ \\_/\\_'),
27472
- colors__default["default"].cyan('_/\\/ _ /\\/ _ \\/\\/ \\__'),
27473
- colors__default["default"].cyan('\\__/ /_/\\/ \\_/ \\_ /\\_/'),
27474
- colors__default["default"].cyan(' /_/ \\__/ \\__/ /'),
27475
- ],
27476
- [
27477
- ` ${colors__default["default"].green.bold('ptbk.io')}`,
27478
- colors__default["default"].cyan(' __ __'),
27479
- colors__default["default"].cyan(' _/\\/\\/ \\_/\\_ \\_/\\_'),
27480
- colors__default["default"].cyan('_/\\/ _ /\\/ _ /\\/ \\_'),
27481
- colors__default["default"].cyan('\\__/ /_/\\/ \\_/ \\_/ _/'),
27482
- colors__default["default"].cyan(' /_/ \\__/ \\__/'),
27483
- ],
27484
- [
27485
- ` ${colors__default["default"].green.bold('ptbk.io')}`,
27486
- colors__default["default"].cyan(' __ __'),
27487
- colors__default["default"].cyan(' _/\\/ \\_/\\/ \\_/\\/\\_'),
27488
- colors__default["default"].cyan('_/\\/ _ /\\/ _ /\\/\\ \\'),
27489
- colors__default["default"].cyan('\\__/ /_/\\/ \\_/ \\_ \\_/'),
27490
- colors__default["default"].cyan(' /_/ \\__/ \\__\\_/'),
27491
- ],
27492
- ];
27648
+ function buildCoderRunUiFrame(options) {
27649
+ const totalWidth = Math.max(MIN_FRAME_WIDTH, Math.min(options.terminalWidth, MAX_FRAME_WIDTH));
27650
+ const isPromptActive = options.phase === 'running' || options.phase === 'verifying' || options.phase === 'loading';
27651
+ const promptStatusPrefix = isPromptActive ? `${colors__default["default"].yellow(`${options.spinner} `)}` : '';
27652
+ const pausePresentation = buildPausePresentation(options.phase, options.pauseState, options.pauseTargetLabel, options.statusMessage);
27653
+ const sessionLines = buildSessionLines(options, totalWidth, pausePresentation);
27654
+ const currentTaskLines = options.currentPromptLabel
27655
+ ? [
27656
+ `${promptStatusPrefix}${colors__default["default"].bold.white(fitPlainText(options.currentPromptLabel, totalWidth - 8))}`,
27657
+ `Attempt ${options.currentAttempt}/${options.maxAttempts} · ${options.statusMessage}`,
27658
+ ...options.detailLines.map((detailLine) => `• ${detailLine}`),
27659
+ ]
27660
+ : [options.statusMessage, ...options.detailLines.map((detailLine) => `• ${detailLine}`)];
27661
+ const visibleOutputLines = buildVisibleOutputLines(options.agentOutputLines);
27662
+ const controlsBoxLines = buildControlsBoxLines({
27663
+ controlPills: buildCoderRunControlPills({
27664
+ phase: options.phase,
27665
+ pauseControl: pausePresentation.pauseControl,
27666
+ pendingEnterLabel: options.pendingEnterLabel,
27667
+ isEndAfterCurrentPromptRequested: options.isEndAfterCurrentPromptRequested,
27668
+ sessionTotal: options.progress.sessionTotal,
27669
+ }),
27670
+ controlFeedback: options.controlFeedback,
27671
+ });
27672
+ const frame = [
27673
+ ...buildFrameHeaderVisual(options, totalWidth),
27674
+ '',
27675
+ ...renderBox('Session', sessionLines, totalWidth, colors__default["default"].yellow.bold),
27676
+ ...renderBox(options.currentPromptLabel ? 'Current task' : 'Queue', currentTaskLines, totalWidth, colors__default["default"].magenta.bold),
27677
+ ...renderBox('Live output', visibleOutputLines, totalWidth, colors__default["default"].green.bold),
27678
+ ];
27679
+ if (options.errors.length > 0) {
27680
+ const bodyWidth = Math.max(10, totalWidth - 4);
27681
+ frame.push(...renderBox('Errors', buildErrorDisplayLines(options.errors, bodyWidth), totalWidth, colors__default["default"].red.bold));
27682
+ }
27683
+ frame.push(...renderBox('Controls', controlsBoxLines, totalWidth, colors__default["default"].white.bold));
27684
+ return frame;
27685
+ }
27493
27686
  /**
27494
- * Builds the horizontal octopus illustration shown above the coder-run dashboard.
27687
+ * Builds the header visual above the dashboard boxes.
27688
+ *
27689
+ * Shows the ASCII-art `--agent` avatar visual as soon as one is available. Until the avatar is
27690
+ * loaded - and when no avatar can be rendered at all - the header stays empty, because the
27691
+ * placeholder ASCII `PTBK.IO` wordmark only added terminal noise while nothing was rendered yet.
27692
+ */
27693
+ function buildFrameHeaderVisual(options, totalWidth) {
27694
+ if (options.agentVisualLines !== undefined && options.agentVisualLines.length > 0) {
27695
+ return options.agentVisualLines.map((agentVisualLine) => centerAnsiText(agentVisualLine, totalWidth));
27696
+ }
27697
+ if (options.agentVisual !== undefined) {
27698
+ const agentVisualLines = options.agentVisual.renderFrame({
27699
+ animationTimeMs: options.animationTimeMs,
27700
+ });
27701
+ if (agentVisualLines.length > 0) {
27702
+ return agentVisualLines.map((agentVisualLine) => centerAnsiText(agentVisualLine, totalWidth));
27703
+ }
27704
+ }
27705
+ return [];
27706
+ }
27707
+ /**
27708
+ * Builds the structured session lines that combine state, runner, queue, and timing metadata.
27709
+ */
27710
+ function buildSessionLines(options, totalWidth, pausePresentation) {
27711
+ const bodyWidth = Math.max(10, totalWidth - 4);
27712
+ return buildSessionRows(options, bodyWidth, pausePresentation).map((sessionRow) => buildLabeledSessionLine(sessionRow.label, sessionRow.value, bodyWidth));
27713
+ }
27714
+ /**
27715
+ * Builds the session rows so the renderer can keep one consistent structure without duplicating labels.
27716
+ */
27717
+ function buildSessionRows(options, bodyWidth, pausePresentation) {
27718
+ const runnerParts = [options.config.agentName || 'No agent selected'];
27719
+ if (options.config.modelName) {
27720
+ runnerParts.push(options.config.modelName);
27721
+ }
27722
+ if (options.config.thinkingLevel) {
27723
+ runnerParts.push(`thinking ${options.config.thinkingLevel}`);
27724
+ }
27725
+ const configurationRows = [
27726
+ ...buildOptionalSessionRow('Context', options.config.context),
27727
+ ...buildOptionalSessionRow('Server', options.config.serverUrl
27728
+ ? buildTerminalUrlLink(options.config.serverUrl, bodyWidth - SESSION_LABEL_WIDTH - 1)
27729
+ : undefined),
27730
+ ...buildOptionalSessionRow('Test', options.config.testCommand),
27731
+ ];
27732
+ return [
27733
+ {
27734
+ label: 'State',
27735
+ value: `${pausePresentation.badge} ${pausePresentation.stateMessage}`,
27736
+ },
27737
+ {
27738
+ label: 'Runner',
27739
+ value: runnerParts.join(' · '),
27740
+ },
27741
+ ...configurationRows,
27742
+ ...buildScriptPathSessionRows(options.currentScriptPaths || [], bodyWidth),
27743
+ {
27744
+ label: 'This run',
27745
+ value: buildThisRunSummary(options.progress),
27746
+ },
27747
+ {
27748
+ label: 'Backlog',
27749
+ value: buildBacklogSummary(options.progress),
27750
+ },
27751
+ {
27752
+ label: 'Scope',
27753
+ value: buildScopeSummary(options.progress, options.config),
27754
+ },
27755
+ {
27756
+ label: 'Timing',
27757
+ value: buildTimingSummary(options.progress),
27758
+ },
27759
+ {
27760
+ label: 'Progress',
27761
+ value: buildProgressBar$1(options.progress.percentage, bodyWidth - SESSION_LABEL_WIDTH - 1, `${options.progress.percentage}% complete (${options.progress.sessionDone}/${options.progress.sessionTotal} done)`),
27762
+ },
27763
+ ];
27764
+ }
27765
+ /**
27766
+ * Builds zero or one structured session row for optional metadata.
27767
+ */
27768
+ function buildOptionalSessionRow(label, value) {
27769
+ if (!value) {
27770
+ return [];
27771
+ }
27772
+ return [{ label, value }];
27773
+ }
27774
+ /**
27775
+ * Builds the active-session summary shown in the session box.
27776
+ */
27777
+ function buildThisRunSummary(progress) {
27778
+ if (progress.sessionTotal === 0) {
27779
+ return 'No runnable prompts in current scope';
27780
+ }
27781
+ return `Task ${progress.currentPromptIndex}/${progress.sessionTotal} · ${progress.sessionDone} done · ${progress.sessionRemaining} left`;
27782
+ }
27783
+ /**
27784
+ * Builds the backlog/filter summary shown in the session box.
27785
+ */
27786
+ function buildBacklogSummary(progress) {
27787
+ const parts = [`Repo ${progress.totalPrompts} total`];
27788
+ if (progress.skippedPrompts > 0) {
27789
+ parts.push(`${formatPromptCount$1(progress.skippedPrompts)} outside priority scope`);
27790
+ }
27791
+ return parts.join(' · ');
27792
+ }
27793
+ /**
27794
+ * Builds the priority/write-order summary shown in the session box.
27795
+ */
27796
+ function buildScopeSummary(progress, config) {
27797
+ var _a;
27798
+ const parts = [formatPriorityFilter((_a = config.priorityFilter) !== null && _a !== void 0 ? _a : {})];
27799
+ if (config.limit !== undefined) {
27800
+ parts.push(`Limit ${formatPromptRunCount(config.limit)}`);
27801
+ }
27802
+ if (progress.toBeWrittenPrompts > 0) {
27803
+ parts.push(`Write ${formatPromptCount$1(progress.toBeWrittenPrompts)} first`);
27804
+ }
27805
+ return parts.join(' · ');
27806
+ }
27807
+ /**
27808
+ * Builds the elapsed/estimate summary shown in the session box.
27809
+ */
27810
+ function buildTimingSummary(progress) {
27811
+ return `Elapsed ${progress.elapsedText} · Total ${progress.estimatedTotalText} · ETA ${progress.estimatedLabel}`;
27812
+ }
27813
+ /**
27814
+ * Formats a prompt count with singular/plural wording.
27815
+ */
27816
+ function formatPromptCount$1(count) {
27817
+ return `${count} prompt${count === 1 ? '' : 's'}`;
27818
+ }
27819
+ /**
27820
+ * Formats a configured successful prompt-run limit with singular/plural wording.
27821
+ */
27822
+ function formatPromptRunCount(count) {
27823
+ return `${count} prompt run${count === 1 ? '' : 's'}`;
27824
+ }
27825
+
27826
+ /**
27827
+ * ANSI sequence that clears the current terminal row.
27828
+ *
27829
+ * @private internal constant of coder run UI
27830
+ */
27831
+ const CLEAR_CURRENT_LINE = '\x1b[2K';
27832
+ /**
27833
+ * ANSI control character that moves the cursor to the start of its current row.
27834
+ *
27835
+ * @private internal constant of coder run UI
27836
+ */
27837
+ const MOVE_CURSOR_TO_LINE_START = '\r';
27838
+ /**
27839
+ * Builds one atomic ANSI update that changes the displayed coder-run terminal frame.
27840
+ *
27841
+ * A change in frame height requires a full rewrite to reserve or clear rows correctly. Frames with
27842
+ * the same height only rewrite contiguous groups of changed rows, returning to the frame bottom
27843
+ * between groups so each group can be assembled into the same output payload safely.
27844
+ *
27845
+ * @returns One terminal output payload, or `undefined` when the frame is already up to date.
27495
27846
  *
27496
27847
  * @private internal utility of coder run UI
27497
27848
  */
27498
- function buildCoderRunOctopusVisual(options) {
27499
- const tentacleFrame = OCTOPUS_TENTACLE_FRAMES[((options.animationFrame % OCTOPUS_TENTACLE_FRAMES.length) + OCTOPUS_TENTACLE_FRAMES.length) %
27500
- OCTOPUS_TENTACLE_FRAMES.length];
27501
- const visualLines = OCTOPUS_HEAD_LINES.map((headLine, lineIndex) => `${headLine}${OCTOPUS_HEAD_TO_TENTACLE_GAP}${tentacleFrame[lineIndex]}`);
27502
- const visualWidth = visualLines.reduce((maxWidth, line) => Math.max(maxWidth, visibleLength(line)), 0);
27503
- visualLines.map((line) => centerAnsiText(padAnsiText(line, visualWidth), options.totalWidth));
27504
- /*
27505
- Note: Octopus art should look better, now using just text
27506
- 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
27507
- https://www.mrgoodfish.com/wp-content/uploads/2022/09/Eledone_moschata__.png
27508
- https://fish-commercial-names.ec.europa.eu/fish-names/jakarta.faces.resource/pictograms/octopus_vulgaris.jpg.xhtml?ln=images
27509
- https://www.asciiart.eu/image-to-ascii
27510
- */
27511
- // 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
27512
- return _spaceTrim__default["default"](`
27513
-
27514
- ▄▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄ ▄▄ ▄▄ ▄▄ ▄▄▄
27515
- ██▄█▀ ██ ██▄██ ██▄█▀ ██ ██▀██
27516
- ██ ██ ██▄█▀ ██ ██ ██ ▀███▀
27517
-
27518
- `)
27519
- .split('\n')
27520
- .map((line) => centerAnsiText(line, options.totalWidth));
27849
+ function buildCoderRunUiTerminalFrameUpdate(options) {
27850
+ const { previousFrameLines, nextFrameLines } = options;
27851
+ if (previousFrameLines.length === 0 && nextFrameLines.length === 0) {
27852
+ return undefined;
27853
+ }
27854
+ if (previousFrameLines.length === 0 || previousFrameLines.length !== nextFrameLines.length) {
27855
+ return buildFullCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines);
27856
+ }
27857
+ return buildChangedCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines);
27858
+ }
27859
+ /**
27860
+ * Builds an ANSI update which completely rewrites the reserved terminal frame area.
27861
+ *
27862
+ * @private helper of `buildCoderRunUiTerminalFrameUpdate`
27863
+ */
27864
+ function buildFullCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines) {
27865
+ var _a;
27866
+ const previousFrameLineCount = previousFrameLines.length;
27867
+ const lineCountToRewrite = Math.max(previousFrameLineCount, nextFrameLines.length);
27868
+ const outputParts = [];
27869
+ if (previousFrameLineCount > 1) {
27870
+ outputParts.push(moveCursorUp(previousFrameLineCount - 1));
27871
+ }
27872
+ for (let lineIndex = 0; lineIndex < lineCountToRewrite; lineIndex++) {
27873
+ outputParts.push(CLEAR_CURRENT_LINE, MOVE_CURSOR_TO_LINE_START, (_a = nextFrameLines[lineIndex]) !== null && _a !== void 0 ? _a : '');
27874
+ if (lineIndex < lineCountToRewrite - 1) {
27875
+ outputParts.push('\n');
27876
+ }
27877
+ }
27878
+ const clearedTrailingLineCount = lineCountToRewrite - nextFrameLines.length;
27879
+ if (clearedTrailingLineCount > 0) {
27880
+ outputParts.push(moveCursorUp(clearedTrailingLineCount));
27881
+ }
27882
+ outputParts.push(MOVE_CURSOR_TO_LINE_START);
27883
+ return outputParts.join('');
27884
+ }
27885
+ /**
27886
+ * Builds an ANSI update that rewrites only contiguous groups of rows which changed.
27887
+ *
27888
+ * @private helper of `buildCoderRunUiTerminalFrameUpdate`
27889
+ */
27890
+ function buildChangedCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines) {
27891
+ const outputParts = [];
27892
+ let lineIndex = 0;
27893
+ while (lineIndex < nextFrameLines.length) {
27894
+ if (previousFrameLines[lineIndex] === nextFrameLines[lineIndex]) {
27895
+ lineIndex++;
27896
+ continue;
27897
+ }
27898
+ const changedRangeStartLineIndex = lineIndex;
27899
+ while (lineIndex + 1 < nextFrameLines.length &&
27900
+ previousFrameLines[lineIndex + 1] !== nextFrameLines[lineIndex + 1]) {
27901
+ lineIndex++;
27902
+ }
27903
+ appendChangedCoderRunUiTerminalFrameRange({
27904
+ outputParts,
27905
+ frameLineCount: nextFrameLines.length,
27906
+ changedRangeStartLineIndex,
27907
+ changedRangeEndLineIndex: lineIndex,
27908
+ nextFrameLines,
27909
+ });
27910
+ lineIndex++;
27911
+ }
27912
+ return outputParts.length === 0 ? undefined : outputParts.join('');
27913
+ }
27914
+ /**
27915
+ * Appends the cursor movements and row rewrites for one contiguous changed frame range.
27916
+ *
27917
+ * @private helper of `buildChangedCoderRunUiTerminalFrameUpdate`
27918
+ */
27919
+ function appendChangedCoderRunUiTerminalFrameRange(options) {
27920
+ const { outputParts, frameLineCount, changedRangeStartLineIndex, changedRangeEndLineIndex, nextFrameLines } = options;
27921
+ const linesUpFromFrameBottom = frameLineCount - 1 - changedRangeStartLineIndex;
27922
+ if (linesUpFromFrameBottom > 0) {
27923
+ outputParts.push(moveCursorUp(linesUpFromFrameBottom));
27924
+ }
27925
+ for (let lineIndex = changedRangeStartLineIndex; lineIndex <= changedRangeEndLineIndex; lineIndex++) {
27926
+ outputParts.push(CLEAR_CURRENT_LINE, MOVE_CURSOR_TO_LINE_START, nextFrameLines[lineIndex]);
27927
+ if (lineIndex < changedRangeEndLineIndex) {
27928
+ outputParts.push('\n');
27929
+ }
27930
+ }
27931
+ const linesDownToFrameBottom = frameLineCount - 1 - changedRangeEndLineIndex;
27932
+ if (linesDownToFrameBottom > 0) {
27933
+ outputParts.push(moveCursorDown(linesDownToFrameBottom));
27934
+ }
27935
+ outputParts.push(MOVE_CURSOR_TO_LINE_START);
27936
+ }
27937
+ /**
27938
+ * Builds an ANSI sequence which moves the terminal cursor up by a positive number of rows.
27939
+ *
27940
+ * @private helper of `buildCoderRunUiTerminalFrameUpdate`
27941
+ */
27942
+ function moveCursorUp(lineCount) {
27943
+ return `\x1b[${lineCount}A`;
27944
+ }
27945
+ /**
27946
+ * Builds an ANSI sequence which moves the terminal cursor down by a positive number of rows.
27947
+ *
27948
+ * @private helper of `buildCoderRunUiTerminalFrameUpdate`
27949
+ */
27950
+ function moveCursorDown(lineCount) {
27951
+ return `\x1b[${lineCount}B`;
27952
+ }
27953
+
27954
+ /**
27955
+ * Calendar formats used when displaying the estimated completion time.
27956
+ *
27957
+ * Uses 24-hour format (`HH:mm`) so estimates are unambiguous (for example `17:30` instead of `5:30`).
27958
+ */
27959
+ const ESTIMATED_DONE_CALENDAR_FORMATS = {
27960
+ sameDay: '[Today] HH:mm',
27961
+ nextDay: '[Tomorrow] HH:mm',
27962
+ nextWeek: 'dddd HH:mm',
27963
+ lastDay: '[Yesterday] HH:mm',
27964
+ lastWeek: 'dddd HH:mm',
27965
+ sameElse: 'MMM D HH:mm',
27966
+ };
27967
+ /**
27968
+ * Formats a duration into a compact string such as "3h 12m" or "45s".
27969
+ */
27970
+ function formatDurationBrief(duration) {
27971
+ const totalSeconds = Math.max(0, Math.round(duration.asSeconds()));
27972
+ const hours = Math.floor(totalSeconds / 3600);
27973
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
27974
+ const seconds = totalSeconds % 60;
27975
+ const parts = [];
27976
+ if (hours > 0) {
27977
+ parts.push(`${hours}h`);
27978
+ }
27979
+ if (minutes > 0) {
27980
+ parts.push(`${minutes}m`);
27981
+ }
27982
+ if (!parts.length && seconds > 0) {
27983
+ parts.push(`${seconds}s`);
27984
+ }
27985
+ if (!parts.length) {
27986
+ parts.push('0s');
27987
+ }
27988
+ return parts.join(' ');
27989
+ }
27990
+
27991
+ /**
27992
+ * Builds a session-scoped progress snapshot from prompt stats and elapsed active time.
27993
+ *
27994
+ * When the current session has not finished any prompt yet but a `cachedAveragePromptDurationMs` is
27995
+ * provided (loaded from the per-config estimate cache stored in the temp folder), the snapshot uses
27996
+ * that historical average to project the remaining work instead of waiting for the first completion.
27997
+ */
27998
+ function buildCoderRunProgressSnapshot(stats, elapsedDuration, initialDone, cachedAveragePromptDurationMs, runLimit) {
27999
+ const totalPrompts = stats.done + stats.forAgent + stats.toBeWritten;
28000
+ const completedSessionPrompts = Math.max(0, stats.done - initialDone);
28001
+ const availableSessionTotal = completedSessionPrompts + stats.forAgent;
28002
+ const sessionTotal = resolveSessionTotal(availableSessionTotal, runLimit);
28003
+ const sessionDone = Math.min(completedSessionPrompts, sessionTotal);
28004
+ const sessionRemaining = Math.max(0, sessionTotal - sessionDone);
28005
+ const currentPromptIndex = sessionTotal > 0 ? Math.min(sessionDone + 1, sessionTotal) : 0;
28006
+ const percentage = sessionTotal > 0 ? Math.round((sessionDone / sessionTotal) * 100) : 0;
28007
+ const elapsedText = formatDurationBrief(elapsedDuration);
28008
+ const elapsedMs = elapsedDuration.asMilliseconds();
28009
+ let estimatedTotalText = 'estimating...';
28010
+ let estimatedLabel = 'after first completion';
28011
+ let isEstimatedTotalKnown = false;
28012
+ let estimatedTotalMs;
28013
+ if (sessionTotal > 0 && sessionDone > 0) {
28014
+ estimatedTotalMs = (elapsedMs * sessionTotal) / sessionDone;
28015
+ }
28016
+ else if (sessionTotal > 0 &&
28017
+ cachedAveragePromptDurationMs !== undefined &&
28018
+ cachedAveragePromptDurationMs > 0) {
28019
+ const projectedRemainingMs = sessionRemaining * cachedAveragePromptDurationMs;
28020
+ estimatedTotalMs = elapsedMs + projectedRemainingMs;
28021
+ }
28022
+ if (estimatedTotalMs !== undefined) {
28023
+ const estimatedRemainingMs = Math.max(0, estimatedTotalMs - elapsedMs);
28024
+ const estimatedTotalDuration = moment__default["default"].duration(estimatedTotalMs);
28025
+ const estimatedCompletion = moment__default["default"]().add(estimatedRemainingMs, 'milliseconds');
28026
+ estimatedTotalText = formatDurationBrief(estimatedTotalDuration);
28027
+ estimatedLabel = estimatedCompletion.calendar(null, ESTIMATED_DONE_CALENDAR_FORMATS);
28028
+ isEstimatedTotalKnown = true;
28029
+ }
28030
+ return {
28031
+ totalPrompts,
28032
+ sessionDone,
28033
+ sessionRemaining,
28034
+ sessionTotal,
28035
+ currentPromptIndex,
28036
+ skippedPrompts: stats.outsidePriorityRange,
28037
+ toBeWrittenPrompts: stats.toBeWritten,
28038
+ percentage,
28039
+ elapsedText,
28040
+ estimatedTotalText,
28041
+ estimatedLabel,
28042
+ isEstimatedTotalKnown,
28043
+ };
28044
+ }
28045
+ /**
28046
+ * Resolves the number of prompt runs represented by the current session progress.
28047
+ */
28048
+ function resolveSessionTotal(availableSessionTotal, runLimit) {
28049
+ if (runLimit === undefined) {
28050
+ return availableSessionTotal;
28051
+ }
28052
+ return Math.min(availableSessionTotal, runLimit);
28053
+ }
28054
+
28055
+ /**
28056
+ * Tracks active coder-run time while excluding pauses and user-confirmation waits.
28057
+ */
28058
+ class CoderRunTimer {
28059
+ /**
28060
+ * Creates a timer anchored at the provided start time.
28061
+ */
28062
+ constructor(startTime, isPausedInitially = false) {
28063
+ this.startTime = startTime;
28064
+ /**
28065
+ * Total milliseconds spent in paused state across the run.
28066
+ */
28067
+ this.pausedMs = 0;
28068
+ if (isPausedInitially) {
28069
+ this.pausedSince = startTime.clone();
28070
+ }
28071
+ }
28072
+ /**
28073
+ * Pauses active-time tracking until `resume()` is called.
28074
+ */
28075
+ pause() {
28076
+ if (this.pausedSince === undefined) {
28077
+ this.pausedSince = moment__default["default"]();
28078
+ }
28079
+ }
28080
+ /**
28081
+ * Resumes active-time tracking after a pause.
28082
+ */
28083
+ resume() {
28084
+ if (this.pausedSince !== undefined) {
28085
+ this.pausedMs += moment__default["default"]().diff(this.pausedSince);
28086
+ this.pausedSince = undefined;
28087
+ }
28088
+ }
28089
+ /**
28090
+ * Returns the currently accumulated active duration.
28091
+ */
28092
+ getElapsedDuration() {
28093
+ const wallMs = moment__default["default"]().diff(this.startTime);
28094
+ const currentPauseMs = this.pausedSince !== undefined ? moment__default["default"]().diff(this.pausedSince) : 0;
28095
+ return moment__default["default"].duration(Math.max(0, wallMs - this.pausedMs - currentPauseMs));
28096
+ }
28097
+ }
28098
+
28099
+ /**
28100
+ * Maximum number of agent output lines kept in the scrolling output area.
28101
+ *
28102
+ * @private internal constant of coder run UI
28103
+ */
28104
+ const MAX_AGENT_OUTPUT_LINES = 12;
28105
+ /**
28106
+ * Reactive state manager for the coder run terminal UI.
28107
+ *
28108
+ * Holds all data the Ink components need and emits `'change'` events
28109
+ * whenever any property is updated so the UI can re-render.
28110
+ *
28111
+ * @private internal utility of coder run UI
28112
+ */
28113
+ class CoderRunUiState extends events.EventEmitter {
28114
+ constructor(startTime) {
28115
+ super();
28116
+ this.config = { agentName: '' };
28117
+ this.currentPromptLabel = '';
28118
+ this.currentScriptPaths = [];
28119
+ this.currentAttempt = 1;
28120
+ this.maxAttempts = 3;
28121
+ this.detailLines = [];
28122
+ this.messagePreviewLines = [];
28123
+ this.messagePreviewSections = [];
28124
+ this.agentStatusLines = [];
28125
+ this.agentStatusTableRows = [];
28126
+ this.agentOutputLines = [];
28127
+ this.phase = 'initializing';
28128
+ this.statusMessage = 'Initializing...';
28129
+ this.errors = [];
28130
+ this.stats = { done: 0, forAgent: 0, outsidePriorityRange: 0, toBeWritten: 0 };
28131
+ // Match the plain CLI progress header and count active run time from startup.
28132
+ // Explicit waits/pauses are excluded later through `pauseTimer()` / `resumeTimer()`.
28133
+ this.timer = new CoderRunTimer(startTime);
28134
+ }
28135
+ /**
28136
+ * Pauses the elapsed timer (e.g. while waiting for user input or paused state).
28137
+ */
28138
+ pauseTimer() {
28139
+ this.timer.pause();
28140
+ this.emitChange();
28141
+ }
28142
+ /**
28143
+ * Resumes the elapsed timer after a pause.
28144
+ */
28145
+ resumeTimer() {
28146
+ this.timer.resume();
28147
+ this.emitChange();
28148
+ }
28149
+ /**
28150
+ * Replaces the configuration shown in the UI header.
28151
+ */
28152
+ setConfig(config) {
28153
+ this.config = config;
28154
+ this.emitChange();
28155
+ }
28156
+ /**
28157
+ * Replaces the ASCII-art agent visual renderer shown above the dashboard boxes.
28158
+ */
28159
+ setAgentVisual(agentVisual) {
28160
+ this.agentVisual = agentVisual;
28161
+ this.emitChange();
28162
+ }
28163
+ /**
28164
+ * Feeds new prompt statistics from the main loop so the progress bar updates.
28165
+ */
28166
+ updateProgress(stats) {
28167
+ if (this.initialDone === undefined && (stats.done > 0 || stats.forAgent > 0 || stats.toBeWritten > 0)) {
28168
+ this.initialDone = stats.done;
28169
+ }
28170
+ this.stats = stats;
28171
+ this.emitChange();
28172
+ }
28173
+ /**
28174
+ * Sets the cached average prompt duration (in milliseconds) loaded from the temp-folder estimate cache.
28175
+ *
28176
+ * Used to project completion estimates before the first prompt of the current session finishes.
28177
+ */
28178
+ setCachedAveragePromptDurationMs(cachedAveragePromptDurationMs) {
28179
+ this.cachedAveragePromptDurationMs = cachedAveragePromptDurationMs;
28180
+ this.emitChange();
28181
+ }
28182
+ /**
28183
+ * Computes a progress snapshot on demand so elapsed time ticks with periodic re-renders.
28184
+ */
28185
+ getProgress() {
28186
+ var _a;
28187
+ return buildCoderRunProgressSnapshot(this.stats, this.timer.getElapsedDuration(), (_a = this.initialDone) !== null && _a !== void 0 ? _a : this.stats.done, this.cachedAveragePromptDurationMs, this.config.limit);
28188
+ }
28189
+ /**
28190
+ * Sets the label of the prompt currently being processed and resets per-prompt state.
28191
+ */
28192
+ setCurrentPrompt(label) {
28193
+ this.currentPromptLabel = label;
28194
+ this.currentScriptPaths = [];
28195
+ this.detailLines = [];
28196
+ this.messagePreviewLines = [];
28197
+ this.messagePreviewSections = [];
28198
+ this.pendingEnterLabel = undefined;
28199
+ this.agentOutputLines = [];
28200
+ this.currentAttempt = 1;
28201
+ this.emitChange();
28202
+ }
28203
+ /**
28204
+ * Sets the temporary shell script currently used by the active prompt.
28205
+ */
28206
+ setCurrentScriptPath(scriptPath) {
28207
+ this.currentScriptPaths = scriptPath ? [scriptPath] : [];
28208
+ this.emitChange();
28209
+ }
28210
+ /**
28211
+ * Sets multiple temporary shell scripts used by a shared multi-agent session.
28212
+ */
28213
+ setCurrentScriptPaths(scriptPaths) {
28214
+ this.currentScriptPaths = [...scriptPaths];
28215
+ this.emitChange();
28216
+ }
28217
+ /**
28218
+ * Updates the current retry attempt number.
28219
+ */
28220
+ setAttempt(attempt) {
28221
+ this.currentAttempt = attempt;
28222
+ this.emitChange();
28223
+ }
28224
+ /**
28225
+ * Appends raw agent output text, keeping only the last `MAX_AGENT_OUTPUT_LINES`.
28226
+ */
28227
+ addAgentOutput(text) {
28228
+ const lines = text.split(/\r?\n/).filter((line) => line.trim() !== '');
28229
+ if (lines.length === 0) {
28230
+ return;
28231
+ }
28232
+ this.agentOutputLines.push(...lines);
28233
+ if (this.agentOutputLines.length > MAX_AGENT_OUTPUT_LINES) {
28234
+ this.agentOutputLines = this.agentOutputLines.slice(-MAX_AGENT_OUTPUT_LINES);
28235
+ }
28236
+ this.emitChange();
28237
+ }
28238
+ /**
28239
+ * Transitions the execution phase shown in the UI.
28240
+ */
28241
+ setPhase(phase) {
28242
+ this.phase = phase;
28243
+ this.emitChange();
28244
+ }
28245
+ /**
28246
+ * Updates the status message line beneath the current prompt label.
28247
+ */
28248
+ setStatusMessage(message) {
28249
+ this.statusMessage = message;
28250
+ this.emitChange();
28251
+ }
28252
+ /**
28253
+ * Replaces the contextual detail lines shown beneath the current prompt status.
28254
+ */
28255
+ setDetailLines(detailLines) {
28256
+ this.detailLines = detailLines.filter((detailLine) => detailLine.trim() !== '');
28257
+ this.emitChange();
28258
+ }
28259
+ /**
28260
+ * Replaces the exact user-message preview lines shown in agent-specific panels.
28261
+ */
28262
+ setMessagePreviewLines(messagePreviewLines) {
28263
+ this.messagePreviewLines = [...messagePreviewLines];
28264
+ this.emitChange();
28265
+ }
28266
+ /**
28267
+ * Replaces the structured user-message panels shown in agent-specific dashboards.
28268
+ */
28269
+ setMessagePreviewSections(messagePreviewSections) {
28270
+ this.messagePreviewSections = [...messagePreviewSections];
28271
+ this.emitChange();
28272
+ }
28273
+ /**
28274
+ * Replaces agent status rows shown by agent-specific terminal frames.
28275
+ */
28276
+ setAgentStatusLines(agentStatusLines) {
28277
+ this.agentStatusLines = [...agentStatusLines];
28278
+ this.emitChange();
28279
+ }
28280
+ /**
28281
+ * Replaces structured agent status rows shown by agent-specific terminal frames.
28282
+ */
28283
+ setAgentStatusTableRows(agentStatusTableRows) {
28284
+ this.agentStatusTableRows = [...agentStatusTableRows];
28285
+ this.emitChange();
28286
+ }
28287
+ /**
28288
+ * Sets or clears the Enter-key action label shown in the controls panel.
28289
+ */
28290
+ setPendingEnterLabel(pendingEnterLabel) {
28291
+ this.pendingEnterLabel = pendingEnterLabel;
28292
+ this.emitChange();
28293
+ }
28294
+ /**
28295
+ * Shows the answer of the runner to one pressed control key, or clears the currently shown one.
28296
+ *
28297
+ * Pressing one control twice in a row can produce the very same answer, for example when there is
28298
+ * nothing to skip. The repeat count is therefore tracked here, so that even such a press changes the
28299
+ * rendered frame and the user always sees that the key arrived.
28300
+ */
28301
+ setControlFeedback(controlFeedback) {
28302
+ this.controlFeedback =
28303
+ controlFeedback === undefined
28304
+ ? undefined
28305
+ : { ...controlFeedback, repeatCount: this.countControlFeedbackRepeat(controlFeedback) };
28306
+ this.emitChange();
28307
+ }
28308
+ /**
28309
+ * Appends an error message to the error list shown in the UI.
28310
+ */
28311
+ addError(errorMessage) {
28312
+ this.errors.push(errorMessage);
28313
+ this.emitChange();
28314
+ }
28315
+ /**
28316
+ * Counts how many times the shown answer repeats the previously shown one.
28317
+ */
28318
+ countControlFeedbackRepeat(controlFeedback) {
28319
+ const previousControlFeedback = this.controlFeedback;
28320
+ const isSameControlFeedback = previousControlFeedback !== undefined &&
28321
+ previousControlFeedback.controlKey === controlFeedback.controlKey &&
28322
+ previousControlFeedback.message === controlFeedback.message;
28323
+ return isSameControlFeedback ? previousControlFeedback.repeatCount + 1 : 1;
28324
+ }
28325
+ emitChange() {
28326
+ this.emit('change');
28327
+ }
27521
28328
  }
27522
28329
 
27523
28330
  /**
@@ -28187,807 +28994,139 @@
28187
28994
  * @param options Agent source, canvas factory, and optional terminal color settings.
28188
28995
  * @returns Runtime terminal avatar visual renderer.
28189
28996
  *
28190
- * @private shared helper for terminal avatar rendering
28191
- */
28192
- function createTerminalAgentAvatarVisual(options) {
28193
- const renderInputs = createTerminalAgentAvatarVisualRenderInputs(options.agentSource, options.defaultAvatarVisualId || DEFAULT_AGENT_AVATAR_VISUAL_ID);
28194
- return {
28195
- isAnimated: renderInputs.resolvedAvatarRenderDefinition.avatarVisual.isAnimated,
28196
- renderFrame({ animationTimeMs }) {
28197
- return renderTerminalAgentAvatarVisualFrame({
28198
- ...renderInputs,
28199
- animationTimeMs,
28200
- colorDepth: options.colorDepth,
28201
- createCanvas: options.createCanvas,
28202
- });
28203
- },
28204
- };
28205
- }
28206
- /**
28207
- * Creates stable avatar render inputs from one agent source.
28208
- *
28209
- * @private shared helper for terminal avatar rendering
28210
- */
28211
- function createTerminalAgentAvatarVisualRenderInputs(agentSource, defaultAvatarVisualId) {
28212
- const agentBasicInformation = parseAgentSource(agentSource);
28213
- const avatarDefinition = createAvatarDefinitionFromAgentBasicInformation(agentBasicInformation);
28214
- const avatarVisualId = resolveTerminalAgentAvatarVisualIdFromAgentBasicInformation(agentBasicInformation, defaultAvatarVisualId);
28215
- const resolvedAvatarRenderDefinition = resolveAvatarRenderDefinition({
28216
- avatarDefinition,
28217
- visualId: avatarVisualId,
28218
- surface: 'transparent',
28219
- });
28220
- return {
28221
- avatarDefinition,
28222
- avatarVisualId,
28223
- resolvedAvatarRenderDefinition,
28224
- };
28225
- }
28226
- /**
28227
- * Resolves the terminal avatar visual id from already parsed agent information.
28228
- *
28229
- * @private shared helper for terminal avatar rendering
28230
- */
28231
- function resolveTerminalAgentAvatarVisualIdFromAgentBasicInformation(agentBasicInformation, defaultAvatarVisualId) {
28232
- return resolveAgentAvatarVisualId(agentBasicInformation, resolveAvatarVisualId(agentBasicInformation.meta.visual) || defaultAvatarVisualId);
28233
- }
28234
- /**
28235
- * Renders one terminal avatar frame through the shared avatar-to-ASCII pipeline.
28236
- *
28237
- * Visuals which are made of characters themselves, for example `AsciiOctopus`, paint the terminal
28238
- * character grid directly, because rasterizing them into half-block ASCII art averages every glyph
28239
- * away and makes them indistinguishable from the other blob-shaped visuals.
28240
- *
28241
- * @private shared helper for terminal avatar rendering
28242
- */
28243
- function renderTerminalAgentAvatarVisualFrame(options) {
28244
- const terminalTextLines = renderAvatarVisualTerminalTextLines({
28245
- avatarDefinition: options.avatarDefinition,
28246
- visualId: options.avatarVisualId,
28247
- surface: 'transparent',
28248
- columns: TERMINAL_AGENT_AVATAR_VISUAL_COLUMNS,
28249
- rows: TERMINAL_AGENT_AVATAR_VISUAL_ROWS,
28250
- colorDepth: options.colorDepth,
28251
- timeMs: options.animationTimeMs,
28252
- resolvedAvatarRenderDefinition: options.resolvedAvatarRenderDefinition,
28253
- });
28254
- if (terminalTextLines !== null) {
28255
- return terminalTextLines;
28256
- }
28257
- if (options.createCanvas === undefined) {
28258
- // Note: A pixel-based visual cannot be rasterized without a canvas, so the caller keeps its default banner
28259
- return [];
28260
- }
28261
- return renderAvatarVisualAsciiArt({
28262
- avatarDefinition: options.avatarDefinition,
28263
- visualId: options.avatarVisualId,
28264
- surface: 'transparent',
28265
- columns: TERMINAL_AGENT_AVATAR_VISUAL_COLUMNS,
28266
- rows: TERMINAL_AGENT_AVATAR_VISUAL_ROWS,
28267
- canvasWidth: TERMINAL_AGENT_AVATAR_VISUAL_CANVAS_WIDTH,
28268
- canvasHeight: TERMINAL_AGENT_AVATAR_VISUAL_CANVAS_HEIGHT,
28269
- colorDepth: options.colorDepth,
28270
- timeMs: options.animationTimeMs,
28271
- createCanvas: options.createCanvas,
28272
- resolvedAvatarRenderDefinition: options.resolvedAvatarRenderDefinition,
28273
- });
28274
- }
28275
-
28276
- /**
28277
- * Refresh cadence used only while the rich coder UI needs animated updates.
28278
- *
28279
- * @private internal constant of coder run UI
28280
- */
28281
- const ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS = TERMINAL_AGENT_AVATAR_VISUAL_REFRESH_INTERVAL_MS;
28282
- /**
28283
- * Phases that still benefit from automatic refreshes because the frame can change
28284
- * over time even without new runner output.
28285
- *
28286
- * @private internal constant of coder run UI
28287
- */
28288
- const AUTO_REFRESH_PHASES = ['initializing', 'loading', 'running', 'verifying'];
28289
- /**
28290
- * Phases where an animated `--agent` visual should keep rendering like it does on the web.
28291
- *
28292
- * @private internal constant of coder run UI
28293
- */
28294
- const AGENT_VISUAL_AUTO_REFRESH_PHASES = [...AUTO_REFRESH_PHASES, 'waiting'];
28295
- /**
28296
- * Returns whether the rich coder UI should keep animating on its own.
28297
- *
28298
- * @private internal utility of coder run UI
28299
- */
28300
- function isCoderRunUiAutoRefreshing(phase, pauseState, isAgentVisualAnimated = false) {
28301
- // `PAUSING` still means the current task is winding down, so keep active
28302
- // animations/timers running until the runner reaches the fully paused state.
28303
- if (pauseState === 'PAUSED') {
28304
- return false;
28305
- }
28306
- if (isAgentVisualAnimated) {
28307
- return AGENT_VISUAL_AUTO_REFRESH_PHASES.includes(phase);
28308
- }
28309
- return AUTO_REFRESH_PHASES.includes(phase);
28310
- }
28311
- /**
28312
- * Returns the automatic refresh interval for the current UI state.
28313
- *
28314
- * Waiting, paused, and completed states return `undefined` so the rich UI stays
28315
- * perfectly still until actual state changes arrive.
28316
- *
28317
- * @private internal utility of coder run UI
28318
- */
28319
- function getCoderRunUiAutoRefreshInterval(phase, pauseState, isAgentVisualAnimated = false) {
28320
- return isCoderRunUiAutoRefreshing(phase, pauseState, isAgentVisualAnimated)
28321
- ? ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS
28322
- : undefined;
28323
- }
28324
-
28325
- /**
28326
- * Minimum width used for the rich coder-run frame.
28327
- */
28328
- const MIN_FRAME_WIDTH = 56;
28329
- /**
28330
- * Maximum width used for the rich coder-run frame.
28331
- */
28332
- const MAX_FRAME_WIDTH = 96;
28333
- /**
28334
- * Builds the complete boxed terminal frame for the rich `ptbk coder run` UI.
28335
- */
28336
- function buildCoderRunUiFrame(options) {
28337
- const totalWidth = Math.max(MIN_FRAME_WIDTH, Math.min(options.terminalWidth, MAX_FRAME_WIDTH));
28338
- const isPromptActive = options.phase === 'running' || options.phase === 'verifying' || options.phase === 'loading';
28339
- const promptStatusPrefix = isPromptActive ? `${colors__default["default"].yellow(`${options.spinner} `)}` : '';
28340
- const octopusAnimationFrame = isCoderRunUiAutoRefreshing(options.phase, options.pauseState)
28341
- ? options.animationFrame
28342
- : 0;
28343
- const pausePresentation = buildPausePresentation(options.phase, options.pauseState, options.pauseTargetLabel, options.statusMessage);
28344
- const sessionLines = buildSessionLines(options, totalWidth, pausePresentation);
28345
- const currentTaskLines = options.currentPromptLabel
28346
- ? [
28347
- `${promptStatusPrefix}${colors__default["default"].bold.white(fitPlainText(options.currentPromptLabel, totalWidth - 8))}`,
28348
- `Attempt ${options.currentAttempt}/${options.maxAttempts} · ${options.statusMessage}`,
28349
- ...options.detailLines.map((detailLine) => `• ${detailLine}`),
28350
- ]
28351
- : [options.statusMessage, ...options.detailLines.map((detailLine) => `• ${detailLine}`)];
28352
- const visibleOutputLines = buildVisibleOutputLines(options.agentOutputLines);
28353
- const controls = buildCoderRunControlPills({
28354
- phase: options.phase,
28355
- pauseControl: pausePresentation.pauseControl,
28356
- pendingEnterLabel: options.pendingEnterLabel,
28357
- isEndAfterCurrentPromptRequested: options.isEndAfterCurrentPromptRequested,
28358
- sessionTotal: options.progress.sessionTotal,
28359
- }).join(' ');
28360
- const frame = [
28361
- ...buildFrameHeaderVisual(options, totalWidth, octopusAnimationFrame),
28362
- '',
28363
- ...renderBox('Session', sessionLines, totalWidth, colors__default["default"].yellow.bold),
28364
- ...renderBox(options.currentPromptLabel ? 'Current task' : 'Queue', currentTaskLines, totalWidth, colors__default["default"].magenta.bold),
28365
- ...renderBox('Live output', visibleOutputLines, totalWidth, colors__default["default"].green.bold),
28366
- ];
28367
- if (options.errors.length > 0) {
28368
- const bodyWidth = Math.max(10, totalWidth - 4);
28369
- frame.push(...renderBox('Errors', buildErrorDisplayLines(options.errors, bodyWidth), totalWidth, colors__default["default"].red.bold));
28370
- }
28371
- frame.push(...renderBox('Controls', [controls], totalWidth, colors__default["default"].white.bold));
28372
- return frame;
28373
- }
28374
- /**
28375
- * Builds the header visual above the dashboard boxes.
28376
- *
28377
- * Shows the ASCII-art `--agent` avatar visual when one is available and
28378
- * falls back to the default brand banner otherwise.
28379
- */
28380
- function buildFrameHeaderVisual(options, totalWidth, octopusAnimationFrame) {
28381
- if (options.agentVisualLines !== undefined && options.agentVisualLines.length > 0) {
28382
- return options.agentVisualLines.map((agentVisualLine) => centerAnsiText(agentVisualLine, totalWidth));
28383
- }
28384
- if (options.agentVisual !== undefined) {
28385
- const agentVisualLines = options.agentVisual.renderFrame({
28386
- animationTimeMs: options.animationTimeMs,
28387
- });
28388
- if (agentVisualLines.length > 0) {
28389
- return agentVisualLines.map((agentVisualLine) => centerAnsiText(agentVisualLine, totalWidth));
28390
- }
28391
- }
28392
- return buildCoderRunOctopusVisual({ totalWidth, animationFrame: octopusAnimationFrame });
28393
- }
28394
- /**
28395
- * Builds the structured session lines that combine state, runner, queue, and timing metadata.
28396
- */
28397
- function buildSessionLines(options, totalWidth, pausePresentation) {
28398
- const bodyWidth = Math.max(10, totalWidth - 4);
28399
- return buildSessionRows(options, bodyWidth, pausePresentation).map((sessionRow) => buildLabeledSessionLine(sessionRow.label, sessionRow.value, bodyWidth));
28400
- }
28401
- /**
28402
- * Builds the session rows so the renderer can keep one consistent structure without duplicating labels.
28403
- */
28404
- function buildSessionRows(options, bodyWidth, pausePresentation) {
28405
- const runnerParts = [options.config.agentName || 'No agent selected'];
28406
- if (options.config.modelName) {
28407
- runnerParts.push(options.config.modelName);
28408
- }
28409
- if (options.config.thinkingLevel) {
28410
- runnerParts.push(`thinking ${options.config.thinkingLevel}`);
28411
- }
28412
- const configurationRows = [
28413
- ...buildOptionalSessionRow('Context', options.config.context),
28414
- ...buildOptionalSessionRow('Server', options.config.serverUrl
28415
- ? buildTerminalUrlLink(options.config.serverUrl, bodyWidth - SESSION_LABEL_WIDTH - 1)
28416
- : undefined),
28417
- ...buildOptionalSessionRow('Test', options.config.testCommand),
28418
- ];
28419
- return [
28420
- {
28421
- label: 'State',
28422
- value: `${pausePresentation.badge} ${pausePresentation.stateMessage}`,
28423
- },
28424
- {
28425
- label: 'Runner',
28426
- value: runnerParts.join(' · '),
28427
- },
28428
- ...configurationRows,
28429
- ...buildScriptPathSessionRows(options.currentScriptPaths || [], bodyWidth),
28430
- {
28431
- label: 'This run',
28432
- value: buildThisRunSummary(options.progress),
28433
- },
28434
- {
28435
- label: 'Backlog',
28436
- value: buildBacklogSummary(options.progress),
28437
- },
28438
- {
28439
- label: 'Scope',
28440
- value: buildScopeSummary(options.progress, options.config),
28441
- },
28442
- {
28443
- label: 'Timing',
28444
- value: buildTimingSummary(options.progress),
28445
- },
28446
- {
28447
- label: 'Progress',
28448
- value: buildProgressBar$1(options.progress.percentage, bodyWidth - SESSION_LABEL_WIDTH - 1, `${options.progress.percentage}% complete (${options.progress.sessionDone}/${options.progress.sessionTotal} done)`),
28449
- },
28450
- ];
28451
- }
28452
- /**
28453
- * Builds zero or one structured session row for optional metadata.
28454
- */
28455
- function buildOptionalSessionRow(label, value) {
28456
- if (!value) {
28457
- return [];
28458
- }
28459
- return [{ label, value }];
28460
- }
28461
- /**
28462
- * Builds the active-session summary shown in the session box.
28463
- */
28464
- function buildThisRunSummary(progress) {
28465
- if (progress.sessionTotal === 0) {
28466
- return 'No runnable prompts in current scope';
28467
- }
28468
- return `Task ${progress.currentPromptIndex}/${progress.sessionTotal} · ${progress.sessionDone} done · ${progress.sessionRemaining} left`;
28469
- }
28470
- /**
28471
- * Builds the backlog/filter summary shown in the session box.
28472
- */
28473
- function buildBacklogSummary(progress) {
28474
- const parts = [`Repo ${progress.totalPrompts} total`];
28475
- if (progress.skippedPrompts > 0) {
28476
- parts.push(`${formatPromptCount$1(progress.skippedPrompts)} outside priority scope`);
28477
- }
28478
- return parts.join(' · ');
28479
- }
28480
- /**
28481
- * Builds the priority/write-order summary shown in the session box.
28482
- */
28483
- function buildScopeSummary(progress, config) {
28484
- var _a;
28485
- const parts = [formatPriorityFilter((_a = config.priorityFilter) !== null && _a !== void 0 ? _a : {})];
28486
- if (config.limit !== undefined) {
28487
- parts.push(`Limit ${formatPromptRunCount(config.limit)}`);
28488
- }
28489
- if (progress.toBeWrittenPrompts > 0) {
28490
- parts.push(`Write ${formatPromptCount$1(progress.toBeWrittenPrompts)} first`);
28491
- }
28492
- return parts.join(' · ');
28493
- }
28494
- /**
28495
- * Builds the elapsed/estimate summary shown in the session box.
28496
- */
28497
- function buildTimingSummary(progress) {
28498
- return `Elapsed ${progress.elapsedText} · Total ${progress.estimatedTotalText} · ETA ${progress.estimatedLabel}`;
28499
- }
28500
- /**
28501
- * Formats a prompt count with singular/plural wording.
28502
- */
28503
- function formatPromptCount$1(count) {
28504
- return `${count} prompt${count === 1 ? '' : 's'}`;
28505
- }
28506
- /**
28507
- * Formats a configured successful prompt-run limit with singular/plural wording.
28508
- */
28509
- function formatPromptRunCount(count) {
28510
- return `${count} prompt run${count === 1 ? '' : 's'}`;
28511
- }
28512
-
28513
- /**
28514
- * ANSI sequence that clears the current terminal row.
28515
- *
28516
- * @private internal constant of coder run UI
28517
- */
28518
- const CLEAR_CURRENT_LINE = '\x1b[2K';
28519
- /**
28520
- * ANSI control character that moves the cursor to the start of its current row.
28521
- *
28522
- * @private internal constant of coder run UI
28523
- */
28524
- const MOVE_CURSOR_TO_LINE_START = '\r';
28525
- /**
28526
- * Builds one atomic ANSI update that changes the displayed coder-run terminal frame.
28527
- *
28528
- * A change in frame height requires a full rewrite to reserve or clear rows correctly. Frames with
28529
- * the same height only rewrite contiguous groups of changed rows, returning to the frame bottom
28530
- * between groups so each group can be assembled into the same output payload safely.
28531
- *
28532
- * @returns One terminal output payload, or `undefined` when the frame is already up to date.
28533
- *
28534
- * @private internal utility of coder run UI
28535
- */
28536
- function buildCoderRunUiTerminalFrameUpdate(options) {
28537
- const { previousFrameLines, nextFrameLines } = options;
28538
- if (previousFrameLines.length === 0 && nextFrameLines.length === 0) {
28539
- return undefined;
28540
- }
28541
- if (previousFrameLines.length === 0 || previousFrameLines.length !== nextFrameLines.length) {
28542
- return buildFullCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines);
28543
- }
28544
- return buildChangedCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines);
28545
- }
28546
- /**
28547
- * Builds an ANSI update which completely rewrites the reserved terminal frame area.
28548
- *
28549
- * @private helper of `buildCoderRunUiTerminalFrameUpdate`
28550
- */
28551
- function buildFullCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines) {
28552
- var _a;
28553
- const previousFrameLineCount = previousFrameLines.length;
28554
- const lineCountToRewrite = Math.max(previousFrameLineCount, nextFrameLines.length);
28555
- const outputParts = [];
28556
- if (previousFrameLineCount > 1) {
28557
- outputParts.push(moveCursorUp(previousFrameLineCount - 1));
28558
- }
28559
- for (let lineIndex = 0; lineIndex < lineCountToRewrite; lineIndex++) {
28560
- outputParts.push(CLEAR_CURRENT_LINE, MOVE_CURSOR_TO_LINE_START, (_a = nextFrameLines[lineIndex]) !== null && _a !== void 0 ? _a : '');
28561
- if (lineIndex < lineCountToRewrite - 1) {
28562
- outputParts.push('\n');
28563
- }
28564
- }
28565
- const clearedTrailingLineCount = lineCountToRewrite - nextFrameLines.length;
28566
- if (clearedTrailingLineCount > 0) {
28567
- outputParts.push(moveCursorUp(clearedTrailingLineCount));
28568
- }
28569
- outputParts.push(MOVE_CURSOR_TO_LINE_START);
28570
- return outputParts.join('');
28571
- }
28572
- /**
28573
- * Builds an ANSI update that rewrites only contiguous groups of rows which changed.
28574
- *
28575
- * @private helper of `buildCoderRunUiTerminalFrameUpdate`
28576
- */
28577
- function buildChangedCoderRunUiTerminalFrameUpdate(previousFrameLines, nextFrameLines) {
28578
- const outputParts = [];
28579
- let lineIndex = 0;
28580
- while (lineIndex < nextFrameLines.length) {
28581
- if (previousFrameLines[lineIndex] === nextFrameLines[lineIndex]) {
28582
- lineIndex++;
28583
- continue;
28584
- }
28585
- const changedRangeStartLineIndex = lineIndex;
28586
- while (lineIndex + 1 < nextFrameLines.length &&
28587
- previousFrameLines[lineIndex + 1] !== nextFrameLines[lineIndex + 1]) {
28588
- lineIndex++;
28589
- }
28590
- appendChangedCoderRunUiTerminalFrameRange({
28591
- outputParts,
28592
- frameLineCount: nextFrameLines.length,
28593
- changedRangeStartLineIndex,
28594
- changedRangeEndLineIndex: lineIndex,
28595
- nextFrameLines,
28596
- });
28597
- lineIndex++;
28598
- }
28599
- return outputParts.length === 0 ? undefined : outputParts.join('');
28600
- }
28601
- /**
28602
- * Appends the cursor movements and row rewrites for one contiguous changed frame range.
28603
- *
28604
- * @private helper of `buildChangedCoderRunUiTerminalFrameUpdate`
28605
- */
28606
- function appendChangedCoderRunUiTerminalFrameRange(options) {
28607
- const { outputParts, frameLineCount, changedRangeStartLineIndex, changedRangeEndLineIndex, nextFrameLines } = options;
28608
- const linesUpFromFrameBottom = frameLineCount - 1 - changedRangeStartLineIndex;
28609
- if (linesUpFromFrameBottom > 0) {
28610
- outputParts.push(moveCursorUp(linesUpFromFrameBottom));
28611
- }
28612
- for (let lineIndex = changedRangeStartLineIndex; lineIndex <= changedRangeEndLineIndex; lineIndex++) {
28613
- outputParts.push(CLEAR_CURRENT_LINE, MOVE_CURSOR_TO_LINE_START, nextFrameLines[lineIndex]);
28614
- if (lineIndex < changedRangeEndLineIndex) {
28615
- outputParts.push('\n');
28616
- }
28617
- }
28618
- const linesDownToFrameBottom = frameLineCount - 1 - changedRangeEndLineIndex;
28619
- if (linesDownToFrameBottom > 0) {
28620
- outputParts.push(moveCursorDown(linesDownToFrameBottom));
28621
- }
28622
- outputParts.push(MOVE_CURSOR_TO_LINE_START);
28623
- }
28624
- /**
28625
- * Builds an ANSI sequence which moves the terminal cursor up by a positive number of rows.
28626
- *
28627
- * @private helper of `buildCoderRunUiTerminalFrameUpdate`
28628
- */
28629
- function moveCursorUp(lineCount) {
28630
- return `\x1b[${lineCount}A`;
28631
- }
28632
- /**
28633
- * Builds an ANSI sequence which moves the terminal cursor down by a positive number of rows.
28634
- *
28635
- * @private helper of `buildCoderRunUiTerminalFrameUpdate`
28636
- */
28637
- function moveCursorDown(lineCount) {
28638
- return `\x1b[${lineCount}B`;
28639
- }
28640
-
28641
- /**
28642
- * Calendar formats used when displaying the estimated completion time.
28643
- *
28644
- * Uses 24-hour format (`HH:mm`) so estimates are unambiguous (for example `17:30` instead of `5:30`).
28645
- */
28646
- const ESTIMATED_DONE_CALENDAR_FORMATS = {
28647
- sameDay: '[Today] HH:mm',
28648
- nextDay: '[Tomorrow] HH:mm',
28649
- nextWeek: 'dddd HH:mm',
28650
- lastDay: '[Yesterday] HH:mm',
28651
- lastWeek: 'dddd HH:mm',
28652
- sameElse: 'MMM D HH:mm',
28653
- };
28654
- /**
28655
- * Formats a duration into a compact string such as "3h 12m" or "45s".
28656
- */
28657
- function formatDurationBrief(duration) {
28658
- const totalSeconds = Math.max(0, Math.round(duration.asSeconds()));
28659
- const hours = Math.floor(totalSeconds / 3600);
28660
- const minutes = Math.floor((totalSeconds % 3600) / 60);
28661
- const seconds = totalSeconds % 60;
28662
- const parts = [];
28663
- if (hours > 0) {
28664
- parts.push(`${hours}h`);
28665
- }
28666
- if (minutes > 0) {
28667
- parts.push(`${minutes}m`);
28668
- }
28669
- if (!parts.length && seconds > 0) {
28670
- parts.push(`${seconds}s`);
28671
- }
28672
- if (!parts.length) {
28673
- parts.push('0s');
28674
- }
28675
- return parts.join(' ');
28676
- }
28677
-
28678
- /**
28679
- * Builds a session-scoped progress snapshot from prompt stats and elapsed active time.
28680
- *
28681
- * When the current session has not finished any prompt yet but a `cachedAveragePromptDurationMs` is
28682
- * provided (loaded from the per-config estimate cache stored in the temp folder), the snapshot uses
28683
- * that historical average to project the remaining work instead of waiting for the first completion.
28684
- */
28685
- function buildCoderRunProgressSnapshot(stats, elapsedDuration, initialDone, cachedAveragePromptDurationMs, runLimit) {
28686
- const totalPrompts = stats.done + stats.forAgent + stats.toBeWritten;
28687
- const completedSessionPrompts = Math.max(0, stats.done - initialDone);
28688
- const availableSessionTotal = completedSessionPrompts + stats.forAgent;
28689
- const sessionTotal = resolveSessionTotal(availableSessionTotal, runLimit);
28690
- const sessionDone = Math.min(completedSessionPrompts, sessionTotal);
28691
- const sessionRemaining = Math.max(0, sessionTotal - sessionDone);
28692
- const currentPromptIndex = sessionTotal > 0 ? Math.min(sessionDone + 1, sessionTotal) : 0;
28693
- const percentage = sessionTotal > 0 ? Math.round((sessionDone / sessionTotal) * 100) : 0;
28694
- const elapsedText = formatDurationBrief(elapsedDuration);
28695
- const elapsedMs = elapsedDuration.asMilliseconds();
28696
- let estimatedTotalText = 'estimating...';
28697
- let estimatedLabel = 'after first completion';
28698
- let isEstimatedTotalKnown = false;
28699
- let estimatedTotalMs;
28700
- if (sessionTotal > 0 && sessionDone > 0) {
28701
- estimatedTotalMs = (elapsedMs * sessionTotal) / sessionDone;
28702
- }
28703
- else if (sessionTotal > 0 &&
28704
- cachedAveragePromptDurationMs !== undefined &&
28705
- cachedAveragePromptDurationMs > 0) {
28706
- const projectedRemainingMs = sessionRemaining * cachedAveragePromptDurationMs;
28707
- estimatedTotalMs = elapsedMs + projectedRemainingMs;
28708
- }
28709
- if (estimatedTotalMs !== undefined) {
28710
- const estimatedRemainingMs = Math.max(0, estimatedTotalMs - elapsedMs);
28711
- const estimatedTotalDuration = moment__default["default"].duration(estimatedTotalMs);
28712
- const estimatedCompletion = moment__default["default"]().add(estimatedRemainingMs, 'milliseconds');
28713
- estimatedTotalText = formatDurationBrief(estimatedTotalDuration);
28714
- estimatedLabel = estimatedCompletion.calendar(null, ESTIMATED_DONE_CALENDAR_FORMATS);
28715
- isEstimatedTotalKnown = true;
28716
- }
28997
+ * @private shared helper for terminal avatar rendering
28998
+ */
28999
+ function createTerminalAgentAvatarVisual(options) {
29000
+ const renderInputs = createTerminalAgentAvatarVisualRenderInputs(options.agentSource, options.defaultAvatarVisualId || DEFAULT_AGENT_AVATAR_VISUAL_ID);
28717
29001
  return {
28718
- totalPrompts,
28719
- sessionDone,
28720
- sessionRemaining,
28721
- sessionTotal,
28722
- currentPromptIndex,
28723
- skippedPrompts: stats.outsidePriorityRange,
28724
- toBeWrittenPrompts: stats.toBeWritten,
28725
- percentage,
28726
- elapsedText,
28727
- estimatedTotalText,
28728
- estimatedLabel,
28729
- isEstimatedTotalKnown,
29002
+ isAnimated: renderInputs.resolvedAvatarRenderDefinition.avatarVisual.isAnimated,
29003
+ renderFrame({ animationTimeMs }) {
29004
+ return renderTerminalAgentAvatarVisualFrame({
29005
+ ...renderInputs,
29006
+ animationTimeMs,
29007
+ colorDepth: options.colorDepth,
29008
+ createCanvas: options.createCanvas,
29009
+ });
29010
+ },
28730
29011
  };
28731
29012
  }
28732
29013
  /**
28733
- * Resolves the number of prompt runs represented by the current session progress.
29014
+ * Creates stable avatar render inputs from one agent source.
29015
+ *
29016
+ * @private shared helper for terminal avatar rendering
28734
29017
  */
28735
- function resolveSessionTotal(availableSessionTotal, runLimit) {
28736
- if (runLimit === undefined) {
28737
- return availableSessionTotal;
28738
- }
28739
- return Math.min(availableSessionTotal, runLimit);
29018
+ function createTerminalAgentAvatarVisualRenderInputs(agentSource, defaultAvatarVisualId) {
29019
+ const agentBasicInformation = parseAgentSource(agentSource);
29020
+ const avatarDefinition = createAvatarDefinitionFromAgentBasicInformation(agentBasicInformation);
29021
+ const avatarVisualId = resolveTerminalAgentAvatarVisualIdFromAgentBasicInformation(agentBasicInformation, defaultAvatarVisualId);
29022
+ const resolvedAvatarRenderDefinition = resolveAvatarRenderDefinition({
29023
+ avatarDefinition,
29024
+ visualId: avatarVisualId,
29025
+ surface: 'transparent',
29026
+ });
29027
+ return {
29028
+ avatarDefinition,
29029
+ avatarVisualId,
29030
+ resolvedAvatarRenderDefinition,
29031
+ };
28740
29032
  }
28741
-
28742
29033
  /**
28743
- * Tracks active coder-run time while excluding pauses and user-confirmation waits.
29034
+ * Resolves the terminal avatar visual id from already parsed agent information.
29035
+ *
29036
+ * @private shared helper for terminal avatar rendering
28744
29037
  */
28745
- class CoderRunTimer {
28746
- /**
28747
- * Creates a timer anchored at the provided start time.
28748
- */
28749
- constructor(startTime, isPausedInitially = false) {
28750
- this.startTime = startTime;
28751
- /**
28752
- * Total milliseconds spent in paused state across the run.
28753
- */
28754
- this.pausedMs = 0;
28755
- if (isPausedInitially) {
28756
- this.pausedSince = startTime.clone();
28757
- }
28758
- }
28759
- /**
28760
- * Pauses active-time tracking until `resume()` is called.
28761
- */
28762
- pause() {
28763
- if (this.pausedSince === undefined) {
28764
- this.pausedSince = moment__default["default"]();
28765
- }
28766
- }
28767
- /**
28768
- * Resumes active-time tracking after a pause.
28769
- */
28770
- resume() {
28771
- if (this.pausedSince !== undefined) {
28772
- this.pausedMs += moment__default["default"]().diff(this.pausedSince);
28773
- this.pausedSince = undefined;
28774
- }
29038
+ function resolveTerminalAgentAvatarVisualIdFromAgentBasicInformation(agentBasicInformation, defaultAvatarVisualId) {
29039
+ return resolveAgentAvatarVisualId(agentBasicInformation, resolveAvatarVisualId(agentBasicInformation.meta.visual) || defaultAvatarVisualId);
29040
+ }
29041
+ /**
29042
+ * Renders one terminal avatar frame through the shared avatar-to-ASCII pipeline.
29043
+ *
29044
+ * Visuals which are made of characters themselves, for example `AsciiOctopus`, paint the terminal
29045
+ * character grid directly, because rasterizing them into half-block ASCII art averages every glyph
29046
+ * away and makes them indistinguishable from the other blob-shaped visuals.
29047
+ *
29048
+ * @private shared helper for terminal avatar rendering
29049
+ */
29050
+ function renderTerminalAgentAvatarVisualFrame(options) {
29051
+ const terminalTextLines = renderAvatarVisualTerminalTextLines({
29052
+ avatarDefinition: options.avatarDefinition,
29053
+ visualId: options.avatarVisualId,
29054
+ surface: 'transparent',
29055
+ columns: TERMINAL_AGENT_AVATAR_VISUAL_COLUMNS,
29056
+ rows: TERMINAL_AGENT_AVATAR_VISUAL_ROWS,
29057
+ colorDepth: options.colorDepth,
29058
+ timeMs: options.animationTimeMs,
29059
+ resolvedAvatarRenderDefinition: options.resolvedAvatarRenderDefinition,
29060
+ });
29061
+ if (terminalTextLines !== null) {
29062
+ return terminalTextLines;
28775
29063
  }
28776
- /**
28777
- * Returns the currently accumulated active duration.
28778
- */
28779
- getElapsedDuration() {
28780
- const wallMs = moment__default["default"]().diff(this.startTime);
28781
- const currentPauseMs = this.pausedSince !== undefined ? moment__default["default"]().diff(this.pausedSince) : 0;
28782
- return moment__default["default"].duration(Math.max(0, wallMs - this.pausedMs - currentPauseMs));
29064
+ if (options.createCanvas === undefined) {
29065
+ // Note: A pixel-based visual cannot be rasterized without a canvas, so the caller renders no avatar
29066
+ return [];
28783
29067
  }
29068
+ return renderAvatarVisualAsciiArt({
29069
+ avatarDefinition: options.avatarDefinition,
29070
+ visualId: options.avatarVisualId,
29071
+ surface: 'transparent',
29072
+ columns: TERMINAL_AGENT_AVATAR_VISUAL_COLUMNS,
29073
+ rows: TERMINAL_AGENT_AVATAR_VISUAL_ROWS,
29074
+ canvasWidth: TERMINAL_AGENT_AVATAR_VISUAL_CANVAS_WIDTH,
29075
+ canvasHeight: TERMINAL_AGENT_AVATAR_VISUAL_CANVAS_HEIGHT,
29076
+ colorDepth: options.colorDepth,
29077
+ timeMs: options.animationTimeMs,
29078
+ createCanvas: options.createCanvas,
29079
+ resolvedAvatarRenderDefinition: options.resolvedAvatarRenderDefinition,
29080
+ });
28784
29081
  }
28785
29082
 
28786
29083
  /**
28787
- * Maximum number of agent output lines kept in the scrolling output area.
29084
+ * Refresh cadence used only while the rich coder UI needs animated updates.
28788
29085
  *
28789
29086
  * @private internal constant of coder run UI
28790
29087
  */
28791
- const MAX_AGENT_OUTPUT_LINES = 12;
29088
+ const ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS = TERMINAL_AGENT_AVATAR_VISUAL_REFRESH_INTERVAL_MS;
28792
29089
  /**
28793
- * Reactive state manager for the coder run terminal UI.
29090
+ * Phases that still benefit from automatic refreshes because the frame can change
29091
+ * over time even without new runner output.
28794
29092
  *
28795
- * Holds all data the Ink components need and emits `'change'` events
28796
- * whenever any property is updated so the UI can re-render.
29093
+ * @private internal constant of coder run UI
29094
+ */
29095
+ const AUTO_REFRESH_PHASES = ['initializing', 'loading', 'running', 'verifying'];
29096
+ /**
29097
+ * Phases where an animated `--agent` visual should keep rendering like it does on the web.
29098
+ *
29099
+ * @private internal constant of coder run UI
29100
+ */
29101
+ const AGENT_VISUAL_AUTO_REFRESH_PHASES = [...AUTO_REFRESH_PHASES, 'waiting'];
29102
+ /**
29103
+ * Returns whether the rich coder UI should keep animating on its own.
28797
29104
  *
28798
29105
  * @private internal utility of coder run UI
28799
29106
  */
28800
- class CoderRunUiState extends events.EventEmitter {
28801
- constructor(startTime) {
28802
- super();
28803
- this.config = { agentName: '' };
28804
- this.currentPromptLabel = '';
28805
- this.currentScriptPaths = [];
28806
- this.currentAttempt = 1;
28807
- this.maxAttempts = 3;
28808
- this.detailLines = [];
28809
- this.messagePreviewLines = [];
28810
- this.messagePreviewSections = [];
28811
- this.agentStatusLines = [];
28812
- this.agentStatusTableRows = [];
28813
- this.agentOutputLines = [];
28814
- this.phase = 'initializing';
28815
- this.statusMessage = 'Initializing...';
28816
- this.errors = [];
28817
- this.stats = { done: 0, forAgent: 0, outsidePriorityRange: 0, toBeWritten: 0 };
28818
- // Match the plain CLI progress header and count active run time from startup.
28819
- // Explicit waits/pauses are excluded later through `pauseTimer()` / `resumeTimer()`.
28820
- this.timer = new CoderRunTimer(startTime);
28821
- }
28822
- /**
28823
- * Pauses the elapsed timer (e.g. while waiting for user input or paused state).
28824
- */
28825
- pauseTimer() {
28826
- this.timer.pause();
28827
- this.emitChange();
28828
- }
28829
- /**
28830
- * Resumes the elapsed timer after a pause.
28831
- */
28832
- resumeTimer() {
28833
- this.timer.resume();
28834
- this.emitChange();
28835
- }
28836
- /**
28837
- * Replaces the configuration shown in the UI header.
28838
- */
28839
- setConfig(config) {
28840
- this.config = config;
28841
- this.emitChange();
28842
- }
28843
- /**
28844
- * Replaces the ASCII-art agent visual renderer shown instead of the default brand banner.
28845
- */
28846
- setAgentVisual(agentVisual) {
28847
- this.agentVisual = agentVisual;
28848
- this.emitChange();
28849
- }
28850
- /**
28851
- * Feeds new prompt statistics from the main loop so the progress bar updates.
28852
- */
28853
- updateProgress(stats) {
28854
- if (this.initialDone === undefined && (stats.done > 0 || stats.forAgent > 0 || stats.toBeWritten > 0)) {
28855
- this.initialDone = stats.done;
28856
- }
28857
- this.stats = stats;
28858
- this.emitChange();
28859
- }
28860
- /**
28861
- * Sets the cached average prompt duration (in milliseconds) loaded from the temp-folder estimate cache.
28862
- *
28863
- * Used to project completion estimates before the first prompt of the current session finishes.
28864
- */
28865
- setCachedAveragePromptDurationMs(cachedAveragePromptDurationMs) {
28866
- this.cachedAveragePromptDurationMs = cachedAveragePromptDurationMs;
28867
- this.emitChange();
28868
- }
28869
- /**
28870
- * Computes a progress snapshot on demand so elapsed time ticks with periodic re-renders.
28871
- */
28872
- getProgress() {
28873
- var _a;
28874
- return buildCoderRunProgressSnapshot(this.stats, this.timer.getElapsedDuration(), (_a = this.initialDone) !== null && _a !== void 0 ? _a : this.stats.done, this.cachedAveragePromptDurationMs, this.config.limit);
28875
- }
28876
- /**
28877
- * Sets the label of the prompt currently being processed and resets per-prompt state.
28878
- */
28879
- setCurrentPrompt(label) {
28880
- this.currentPromptLabel = label;
28881
- this.currentScriptPaths = [];
28882
- this.detailLines = [];
28883
- this.messagePreviewLines = [];
28884
- this.messagePreviewSections = [];
28885
- this.pendingEnterLabel = undefined;
28886
- this.agentOutputLines = [];
28887
- this.currentAttempt = 1;
28888
- this.emitChange();
28889
- }
28890
- /**
28891
- * Sets the temporary shell script currently used by the active prompt.
28892
- */
28893
- setCurrentScriptPath(scriptPath) {
28894
- this.currentScriptPaths = scriptPath ? [scriptPath] : [];
28895
- this.emitChange();
28896
- }
28897
- /**
28898
- * Sets multiple temporary shell scripts used by a shared multi-agent session.
28899
- */
28900
- setCurrentScriptPaths(scriptPaths) {
28901
- this.currentScriptPaths = [...scriptPaths];
28902
- this.emitChange();
28903
- }
28904
- /**
28905
- * Updates the current retry attempt number.
28906
- */
28907
- setAttempt(attempt) {
28908
- this.currentAttempt = attempt;
28909
- this.emitChange();
28910
- }
28911
- /**
28912
- * Appends raw agent output text, keeping only the last `MAX_AGENT_OUTPUT_LINES`.
28913
- */
28914
- addAgentOutput(text) {
28915
- const lines = text.split(/\r?\n/).filter((line) => line.trim() !== '');
28916
- if (lines.length === 0) {
28917
- return;
28918
- }
28919
- this.agentOutputLines.push(...lines);
28920
- if (this.agentOutputLines.length > MAX_AGENT_OUTPUT_LINES) {
28921
- this.agentOutputLines = this.agentOutputLines.slice(-MAX_AGENT_OUTPUT_LINES);
28922
- }
28923
- this.emitChange();
28924
- }
28925
- /**
28926
- * Transitions the execution phase shown in the UI.
28927
- */
28928
- setPhase(phase) {
28929
- this.phase = phase;
28930
- this.emitChange();
28931
- }
28932
- /**
28933
- * Updates the status message line beneath the current prompt label.
28934
- */
28935
- setStatusMessage(message) {
28936
- this.statusMessage = message;
28937
- this.emitChange();
28938
- }
28939
- /**
28940
- * Replaces the contextual detail lines shown beneath the current prompt status.
28941
- */
28942
- setDetailLines(detailLines) {
28943
- this.detailLines = detailLines.filter((detailLine) => detailLine.trim() !== '');
28944
- this.emitChange();
28945
- }
28946
- /**
28947
- * Replaces the exact user-message preview lines shown in agent-specific panels.
28948
- */
28949
- setMessagePreviewLines(messagePreviewLines) {
28950
- this.messagePreviewLines = [...messagePreviewLines];
28951
- this.emitChange();
28952
- }
28953
- /**
28954
- * Replaces the structured user-message panels shown in agent-specific dashboards.
28955
- */
28956
- setMessagePreviewSections(messagePreviewSections) {
28957
- this.messagePreviewSections = [...messagePreviewSections];
28958
- this.emitChange();
28959
- }
28960
- /**
28961
- * Replaces agent status rows shown by agent-specific terminal frames.
28962
- */
28963
- setAgentStatusLines(agentStatusLines) {
28964
- this.agentStatusLines = [...agentStatusLines];
28965
- this.emitChange();
28966
- }
28967
- /**
28968
- * Replaces structured agent status rows shown by agent-specific terminal frames.
28969
- */
28970
- setAgentStatusTableRows(agentStatusTableRows) {
28971
- this.agentStatusTableRows = [...agentStatusTableRows];
28972
- this.emitChange();
28973
- }
28974
- /**
28975
- * Sets or clears the Enter-key action label shown in the controls panel.
28976
- */
28977
- setPendingEnterLabel(pendingEnterLabel) {
28978
- this.pendingEnterLabel = pendingEnterLabel;
28979
- this.emitChange();
28980
- }
28981
- /**
28982
- * Appends an error message to the error list shown in the UI.
28983
- */
28984
- addError(errorMessage) {
28985
- this.errors.push(errorMessage);
28986
- this.emitChange();
29107
+ function isCoderRunUiAutoRefreshing(phase, pauseState, isAgentVisualAnimated = false) {
29108
+ // `PAUSING` still means the current task is winding down, so keep active
29109
+ // animations/timers running until the runner reaches the fully paused state.
29110
+ if (pauseState === 'PAUSED') {
29111
+ return false;
28987
29112
  }
28988
- emitChange() {
28989
- this.emit('change');
29113
+ if (isAgentVisualAnimated) {
29114
+ return AGENT_VISUAL_AUTO_REFRESH_PHASES.includes(phase);
28990
29115
  }
29116
+ return AUTO_REFRESH_PHASES.includes(phase);
29117
+ }
29118
+ /**
29119
+ * Returns the automatic refresh interval for the current UI state.
29120
+ *
29121
+ * Waiting, paused, and completed states return `undefined` so the rich UI stays
29122
+ * perfectly still until actual state changes arrive.
29123
+ *
29124
+ * @private internal utility of coder run UI
29125
+ */
29126
+ function getCoderRunUiAutoRefreshInterval(phase, pauseState, isAgentVisualAnimated = false) {
29127
+ return isCoderRunUiAutoRefreshing(phase, pauseState, isAgentVisualAnimated)
29128
+ ? ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS
29129
+ : undefined;
28991
29130
  }
28992
29131
 
28993
29132
  /**
@@ -29085,6 +29224,7 @@
29085
29224
  let isRendering = false;
29086
29225
  let renderScheduled = false;
29087
29226
  let autoRefreshTimeout;
29227
+ let controlFeedbackTimeout;
29088
29228
  let isDisposed = false;
29089
29229
  /**
29090
29230
  * Schedules a render on the next tick if one isn't already pending.
@@ -29150,9 +29290,32 @@
29150
29290
  pendingEnterLabel: state.pendingEnterLabel,
29151
29291
  agentOutputLines: state.agentOutputLines,
29152
29292
  errors: state.errors,
29293
+ controlFeedback: state.controlFeedback,
29153
29294
  progress: state.getProgress(),
29154
29295
  });
29155
29296
  }
29297
+ /**
29298
+ * Answers one pressed control key in the frame and hides that answer again after a short moment.
29299
+ */
29300
+ function showControlFeedback(controlFeedback) {
29301
+ clearControlFeedbackTimeout();
29302
+ // Note: Every state change re-renders the frame, so the answer appears with the very next frame
29303
+ state.setControlFeedback(controlFeedback);
29304
+ controlFeedbackTimeout = setTimeout(() => {
29305
+ controlFeedbackTimeout = undefined;
29306
+ state.setControlFeedback(undefined);
29307
+ }, CODER_RUN_CONTROL_FEEDBACK_DURATION_MS);
29308
+ }
29309
+ /**
29310
+ * Stops the timer which hides the currently shown control answer.
29311
+ */
29312
+ function clearControlFeedbackTimeout() {
29313
+ if (controlFeedbackTimeout === undefined) {
29314
+ return;
29315
+ }
29316
+ clearTimeout(controlFeedbackTimeout);
29317
+ controlFeedbackTimeout = undefined;
29318
+ }
29156
29319
  /**
29157
29320
  * Clears previously rendered lines and writes a new frame only where needed.
29158
29321
  */
@@ -29185,19 +29348,10 @@
29185
29348
  cleanup();
29186
29349
  process.exit(0);
29187
29350
  }
29188
- if (key.name === 'p') {
29189
- togglePauseState();
29190
- scheduleRender();
29191
- return;
29192
- }
29193
- if (key.name === 's') {
29194
- requestSkipCurrentWait();
29195
- scheduleRender();
29196
- return;
29197
- }
29198
- if (key.name === 'x') {
29199
- toggleEndAfterCurrentPromptState();
29200
- scheduleRender();
29351
+ // Note: [🏹] The very same key handling is shared with the plain console mode, see `applyCoderRunControlKey`
29352
+ const controlFeedback = applyCoderRunControlKey(key.name);
29353
+ if (controlFeedback !== undefined) {
29354
+ showControlFeedback(controlFeedback);
29201
29355
  return;
29202
29356
  }
29203
29357
  if ((key.name === 'return' || key.name === 'enter') && pendingEnterResolver) {
@@ -29223,6 +29377,7 @@
29223
29377
  clearTimeout(autoRefreshTimeout);
29224
29378
  autoRefreshTimeout = undefined;
29225
29379
  }
29380
+ clearControlFeedbackTimeout();
29226
29381
  state.off('change', scheduleRender);
29227
29382
  process.stdin.off('keypress', keypressHandler);
29228
29383
  process.stdout.off('resize', scheduleRender);
@@ -29283,7 +29438,7 @@
29283
29438
  waitBetweenPrompts: 0,
29284
29439
  waitAfterError: 0,
29285
29440
  noCommit: options.noCommit,
29286
- ignoreGitChanges: options.ignoreGitChanges,
29441
+ gitChanges: options.gitChanges,
29287
29442
  normalizeLineEndings: options.normalizeLineEndings,
29288
29443
  allowCredits: options.allowCredits,
29289
29444
  autoMigrate: false,
@@ -39851,11 +40006,11 @@
39851
40006
  * Validates constraints specific to never-ending agent watch loops.
39852
40007
  */
39853
40008
  function validateAgentWatchOptions(commandDisplayName, options) {
39854
- if (!options.noCommit || options.ignoreGitChanges) {
40009
+ if (!options.noCommit || options.gitChanges !== 'fail') {
39855
40010
  return;
39856
40011
  }
39857
40012
  throw new NotAllowed(_spaceTrim.spaceTrim(`
39858
- Flag \`--no-commit\` requires \`--ignore-git-changes\` when used with \`${commandDisplayName}\`.
40013
+ Flag \`--no-commit\` requires \`--git-changes ignore\` when used with \`${commandDisplayName}\`.
39859
40014
 
39860
40015
  Without commits, answered messages remain in the working tree and the next message round would fail the clean
39861
40016
  working tree check.
@@ -40168,7 +40323,7 @@
40168
40323
  noUi: options.noUi,
40169
40324
  thinkingLevel: options.thinkingLevel,
40170
40325
  noCommit: true,
40171
- ignoreGitChanges: true,
40326
+ gitChanges: 'ignore',
40172
40327
  normalizeLineEndings: false,
40173
40328
  allowCredits: options.allowCredits,
40174
40329
  autoPush: false,
@@ -43766,6 +43921,8 @@
43766
43921
  Features:
43767
43922
  - Automatically stages and commits changes with agent identity unless --no-commit is used
43768
43923
  - Commits only the prompt file and the files the coding agent has changed, leaving unrelated changes alone
43924
+ - Refuses to start on a dirty working tree unless --git-changes says what should happen with the changes
43925
+ - Optional --git-changes continue resumes the single prompt an interrupted coder left in the [^] status
43769
43926
  - Optional post-commit git push with explicit --auto-push opt-in
43770
43927
  - Optional pre-prompt git pull with explicit --auto-pull opt-in
43771
43928
  - Optional --isolate runs every prompt in its own temporary git worktree and merges it back when verified
@@ -43846,7 +44003,7 @@
43846
44003
  waitBetweenPrompts,
43847
44004
  waitAfterError,
43848
44005
  noCommit: runnerOptions.noCommit,
43849
- ignoreGitChanges: runnerOptions.ignoreGitChanges,
44006
+ gitChanges: runnerOptions.gitChanges,
43850
44007
  agentName: runnerOptions.agentName,
43851
44008
  model: runnerOptions.model,
43852
44009
  agent,
@@ -43987,7 +44144,7 @@
43987
44144
  waitBetweenPrompts,
43988
44145
  waitAfterError,
43989
44146
  noCommit: runnerOptions.noCommit,
43990
- ignoreGitChanges: runnerOptions.ignoreGitChanges,
44147
+ gitChanges: runnerOptions.gitChanges,
43991
44148
  agentName: runnerOptions.agentName,
43992
44149
  model: runnerOptions.model,
43993
44150
  agent,
@@ -67418,7 +67575,7 @@
67418
67575
  waitBetweenPrompts: 0,
67419
67576
  waitAfterError: 0,
67420
67577
  noCommit: true,
67421
- ignoreGitChanges: true,
67578
+ gitChanges: 'ignore',
67422
67579
  normalizeLineEndings: false,
67423
67580
  allowCredits: options.allowCredits,
67424
67581
  isVerbose: options.isVerbose,
@@ -73211,13 +73368,13 @@
73211
73368
  }
73212
73369
 
73213
73370
  /**
73214
- * Lists todo prompts across all files.
73371
+ * Lists prompts in one checklist status across all files.
73215
73372
  */
73216
- function listTodoPrompts(files) {
73373
+ function listPromptsByStatus(files, status) {
73217
73374
  const prompts = [];
73218
73375
  for (const file of files) {
73219
73376
  for (const section of file.sections) {
73220
- if (section.status === 'todo') {
73377
+ if (section.status === status) {
73221
73378
  prompts.push({ file, section });
73222
73379
  }
73223
73380
  }
@@ -73225,6 +73382,13 @@
73225
73382
  return prompts;
73226
73383
  }
73227
73384
 
73385
+ /**
73386
+ * Lists todo prompts across all files.
73387
+ */
73388
+ function listTodoPrompts(files) {
73389
+ return listPromptsByStatus(files, 'todo');
73390
+ }
73391
+
73228
73392
  /**
73229
73393
  * Lists todo prompts that still contain authoring placeholders.
73230
73394
  */
@@ -73802,7 +73966,7 @@
73802
73966
  /**
73803
73967
  * CLI usage text for this script.
73804
73968
  */
73805
- 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]';
73969
+ 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]';
73806
73970
  /**
73807
73971
  * Top-level flags supported by this command.
73808
73972
  */
@@ -73829,7 +73993,7 @@
73829
73993
  '--wait-after-error',
73830
73994
  '--no-auto',
73831
73995
  '--no-commit',
73832
- '--ignore-git-changes',
73996
+ '--git-changes',
73833
73997
  '--no-normalize-line-endings',
73834
73998
  '--auto-push',
73835
73999
  '--auto-pull',
@@ -73873,7 +74037,8 @@
73873
74037
  const hasLimitFlag = args.includes('--limit');
73874
74038
  const limit = parseLimit(readOptionValue(args, '--limit'), hasLimitFlag);
73875
74039
  const noCommit = args.includes('--no-commit');
73876
- const ignoreGitChanges = args.includes('--ignore-git-changes');
74040
+ const hasGitChangesFlag = args.includes('--git-changes');
74041
+ const gitChanges = parseGitChangesOption(readOptionValue(args, '--git-changes'), hasGitChangesFlag);
73877
74042
  const normalizeLineEndings = !args.includes('--no-normalize-line-endings');
73878
74043
  const allowCredits = args.includes('--allow-credits');
73879
74044
  const autoMigrate = args.includes('--auto-migrate');
@@ -73911,7 +74076,7 @@
73911
74076
  waitBetweenPrompts,
73912
74077
  waitAfterError,
73913
74078
  noCommit,
73914
- ignoreGitChanges,
74079
+ gitChanges,
73915
74080
  normalizeLineEndings,
73916
74081
  allowCredits,
73917
74082
  autoMigrate,
@@ -73949,6 +74114,21 @@
73949
74114
  }
73950
74115
  return value;
73951
74116
  }
74117
+ /**
74118
+ * Parses and validates the requested behavior for a dirty working tree.
74119
+ */
74120
+ function parseGitChangesOption(value, hasGitChangesFlag) {
74121
+ if (value === undefined) {
74122
+ if (hasGitChangesFlag) {
74123
+ exitWithUsageError(`Missing value for --git-changes. Use one of: ${GIT_CHANGES_MODE_VALUES.join(', ')}.`);
74124
+ }
74125
+ return DEFAULT_GIT_CHANGES_MODE;
74126
+ }
74127
+ if (!isGitChangesMode(value)) {
74128
+ exitWithUsageError(`Invalid value for --git-changes: "${value}". Use one of: ${GIT_CHANGES_MODE_VALUES.join(', ')}.`);
74129
+ }
74130
+ return value;
74131
+ }
73952
74132
  /**
73953
74133
  * Reads a duration-typed CLI flag, applying the provided default when the flag is absent.
73954
74134
  */
@@ -74406,27 +74586,19 @@
74406
74586
  return;
74407
74587
  }
74408
74588
  const deadlineTimeMs = (_a = options.deadlineTimeMs) !== null && _a !== void 0 ? _a : Date.now() + durationMs;
74409
- const waitToken = beginSkippableWait();
74410
- try {
74411
- await waitUntilWorldTimeDeadline({
74412
- deadlineTimeMs,
74413
- pollIntervalMs: WAIT_COUNTDOWN_UPDATE_INTERVAL_MS,
74414
- shouldStopWaiting: () => shouldSkipCurrentWait(waitToken),
74415
- waitForMilliseconds: (waitDurationMs) => waitForSkippableMilliseconds(waitToken, waitDurationMs),
74416
- onTick: (remainingDurationMs) => {
74417
- const visibleRemainingDurationMs = Math.min(remainingDurationMs, durationMs);
74418
- const statusMessage = describeCoderRunWait(waitKind, visibleRemainingDurationMs, durationMs);
74419
- if (!isRichUiEnabled) {
74420
- console.info(colors__default["default"].gray(`${statusMessage}...`));
74421
- return;
74422
- }
74423
- uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage(`${statusMessage}...`);
74424
- },
74425
- });
74426
- }
74427
- finally {
74428
- finishSkippableWait(waitToken);
74429
- }
74589
+ await waitForSkippableWorldTimeDeadline({
74590
+ deadlineTimeMs,
74591
+ pollIntervalMs: WAIT_COUNTDOWN_UPDATE_INTERVAL_MS,
74592
+ onTick: (remainingDurationMs) => {
74593
+ const visibleRemainingDurationMs = Math.min(remainingDurationMs, durationMs);
74594
+ const statusMessage = describeCoderRunWait(waitKind, visibleRemainingDurationMs, durationMs);
74595
+ if (!isRichUiEnabled) {
74596
+ console.info(colors__default["default"].gray(`${statusMessage}...`));
74597
+ return;
74598
+ }
74599
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage(`${statusMessage}...`);
74600
+ },
74601
+ });
74430
74602
  }
74431
74603
 
74432
74604
  /**
@@ -74441,6 +74613,36 @@
74441
74613
  });
74442
74614
  }
74443
74615
 
74616
+ /**
74617
+ * Listens for the terminal control keys while the plain console output is used.
74618
+ *
74619
+ * Every recognized key press is answered with one printed line, because the plain mode has no frame
74620
+ * which could show the new state on its own. The rich terminal UI installs its own listener in
74621
+ * [`renderCoderRunUi`](../ui/renderCoderRunUi.ts) and shares the very same key handling through
74622
+ * [`applyCoderRunControlKey`](./applyCoderRunControlKey.ts).
74623
+ *
74624
+ * @private internal utility of `ptbk coder` terminal controls
74625
+ */
74626
+ function listenForCoderRunControls() {
74627
+ if (!process.stdin.isTTY) {
74628
+ return;
74629
+ }
74630
+ readline__namespace.emitKeypressEvents(process.stdin);
74631
+ process.stdin.setRawMode(true);
74632
+ process.stdin.on('keypress', (_pressedText, key) => {
74633
+ if (key.ctrl && key.name === 'c') {
74634
+ process.exit();
74635
+ }
74636
+ const controlFeedback = applyCoderRunControlKey(key.name);
74637
+ if (controlFeedback === undefined) {
74638
+ return;
74639
+ }
74640
+ // Note: Using console.log here which adds a new line.
74641
+ // This is intentional to prevent the message from being overwritten.
74642
+ console.log(formatCoderRunControlFeedback(controlFeedback));
74643
+ });
74644
+ }
74645
+
74444
74646
  /**
74445
74647
  * Checks working tree clean.
74446
74648
  */
@@ -74459,12 +74661,14 @@
74459
74661
  async function ensureWorkingTreeClean() {
74460
74662
  const isClean = await isWorkingTreeClean(process.cwd());
74461
74663
  if (!isClean) {
74462
- throw new Error(_spaceTrim.spaceTrim(`
74664
+ throw new NotAllowed(_spaceTrim.spaceTrim(`
74463
74665
  Git working tree is not clean.
74464
74666
 
74465
74667
  Please commit or stash your changes before running this script
74466
- OR run script with flag --ignore-git-changes
74467
-
74668
+ OR decide what should happen with them:
74669
+
74670
+ - \`--git-changes ignore\` leaves the changes where they are and starts the next prompt anyway
74671
+ - \`--git-changes continue\` resumes the single prompt which was left in the middle of its implementation
74468
74672
 
74469
74673
  Aborting
74470
74674
  `));
@@ -76031,6 +76235,51 @@
76031
76235
  return `(failed after ${attemptCount} attempts) `;
76032
76236
  }
76033
76237
 
76238
+ /**
76239
+ * Separator which introduces the harness that started a prompt another harness has taken over.
76240
+ *
76241
+ * It is used both for writing and for reading the attribution, so the status line of a continued prompt
76242
+ * stays parseable when yet another harness continues it later.
76243
+ */
76244
+ const STARTED_BY_SEPARATOR = ', started by ';
76245
+ /**
76246
+ * Matches the attribution of one status line, which reaches from `by ` up to the step summary.
76247
+ */
76248
+ const PROMPT_RUNNER_ATTRIBUTION_PATTERN = /(?:^|\s)by (?<attribution>.*?)(?: - |$)/u;
76249
+ /**
76250
+ * Formats who worked on one prompt for its status line.
76251
+ *
76252
+ * Produces `` by Claude Code `claude-opus-5` `` for a prompt implemented by a single harness and
76253
+ * `` by Claude Code `claude-opus-5`, started by OpenAI Codex `gpt-5.6-luna` `` for a prompt which one
76254
+ * harness started and another one continued through `--git-changes continue`.
76255
+ */
76256
+ function formatPromptRunnerAttribution(options) {
76257
+ const { currentRunnerSignature, startedByRunnerSignature } = options;
76258
+ if (startedByRunnerSignature === undefined || startedByRunnerSignature === currentRunnerSignature) {
76259
+ return `by ${currentRunnerSignature}`;
76260
+ }
76261
+ return `by ${currentRunnerSignature}${STARTED_BY_SEPARATOR}${startedByRunnerSignature}`;
76262
+ }
76263
+ /**
76264
+ * Reads which harness started the work recorded on one prompt status line.
76265
+ *
76266
+ * A line written by a single harness names that harness, and a line of a prompt which was already taken
76267
+ * over keeps naming the harness which started it, so the original author survives any number of takeovers.
76268
+ */
76269
+ function parsePromptStartedByRunnerSignature(statusLine) {
76270
+ var _a, _b, _c;
76271
+ 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();
76272
+ if (!attribution) {
76273
+ return undefined;
76274
+ }
76275
+ const startedBySeparatorIndex = attribution.indexOf(STARTED_BY_SEPARATOR);
76276
+ if (startedBySeparatorIndex === -1) {
76277
+ return attribution;
76278
+ }
76279
+ return attribution.slice(startedBySeparatorIndex + STARTED_BY_SEPARATOR.length).trim() || undefined;
76280
+ }
76281
+ // Note: [💞] Ignore a discrepancy between file name and exported helper names
76282
+
76034
76283
  /**
76035
76284
  * Builds the shared body of a prompt status line, used by both the in-progress `[^]` and the done `[x]` status.
76036
76285
  *
@@ -76038,14 +76287,18 @@
76038
76287
  * ``by OpenAI Codex `gpt-5.6-luna` thinking `max` (ChatGPT account) - Implementation ~$0.2036 10 minutes``.
76039
76288
  */
76040
76289
  function buildPromptStatusDetails(options) {
76041
- const { steps, inProgressStepKind, runnerName, modelName, attemptCount, loginMethod, thinkingLevel } = options;
76290
+ const { steps, inProgressStepKind, runnerName, modelName, startedByRunnerSignature, attemptCount, loginMethod, thinkingLevel, } = options;
76042
76291
  const runnerSignature = formatRunnerSignature(runnerName, modelName, thinkingLevel);
76043
76292
  const attemptMetadata = formatPromptAttemptMetadata('done', attemptCount);
76044
76293
  const loginMethodLabel = formatCodexLoginMethod(loginMethod);
76045
76294
  const loginMethodSuffix = loginMethodLabel ? ` (${loginMethodLabel})` : '';
76295
+ const attribution = formatPromptRunnerAttribution({
76296
+ currentRunnerSignature: `${runnerSignature}${loginMethodSuffix}`,
76297
+ startedByRunnerSignature,
76298
+ });
76046
76299
  const stepsSummary = formatCoderRunSteps(steps, inProgressStepKind);
76047
76300
  const stepsSuffix = stepsSummary === '' ? '' : ` - ${stepsSummary}`;
76048
- return `${attemptMetadata}by ${runnerSignature}${loginMethodSuffix}${stepsSuffix}`;
76301
+ return `${attemptMetadata}${attribution}${stepsSuffix}`;
76049
76302
  }
76050
76303
 
76051
76304
  /**
@@ -76088,29 +76341,26 @@
76088
76341
  /**
76089
76342
  * Marks a prompt section as done and records the per-step usage pricing and runner details.
76090
76343
  */
76091
- function markPromptDone(file, section, steps, runnerName, modelName, attemptCount = 1, loginMethod, thinkingLevel) {
76092
- const statusDetails = buildPromptStatusDetails({
76093
- steps,
76094
- runnerName,
76095
- modelName,
76096
- attemptCount,
76097
- loginMethod,
76098
- thinkingLevel,
76099
- });
76100
- writePromptStatusLine(file, section, `[x] ${statusDetails}`);
76344
+ function markPromptDone(options) {
76345
+ const { file, section, ...statusDetailsOptions } = options;
76346
+ writePromptStatusLine(file, section, `[x] ${buildPromptStatusDetails(statusDetailsOptions)}`);
76101
76347
  }
76102
76348
 
76103
76349
  /**
76104
76350
  * Marks a prompt section as failed and records runner details.
76105
76351
  */
76106
- function markPromptFailed(file, section, runnerName, modelName, promptExecutionStartedDate, attemptCount = 1) {
76107
- const runnerSignature = formatRunnerSignature(runnerName, modelName);
76352
+ function markPromptFailed(options) {
76353
+ const { file, section, runnerName, modelName, startedByRunnerSignature, promptExecutionStartedDate, attemptCount } = options;
76354
+ const attribution = formatPromptRunnerAttribution({
76355
+ currentRunnerSignature: formatRunnerSignature(runnerName, modelName),
76356
+ startedByRunnerSignature,
76357
+ });
76108
76358
  const attemptMetadata = formatPromptAttemptMetadata('failed', attemptCount);
76109
76359
  const duration = moment__default["default"]().diff(promptExecutionStartedDate);
76110
76360
  const durationString = moment__default["default"].duration(duration).humanize();
76111
76361
  const failureDetails = attemptMetadata === ''
76112
- ? `failed after ${durationString} by ${runnerSignature}`
76113
- : `${attemptMetadata}${durationString} by ${runnerSignature}`;
76362
+ ? `failed after ${durationString} ${attribution}`
76363
+ : `${attemptMetadata}${durationString} ${attribution}`;
76114
76364
  writePromptStatusLine(file, section, `[!] ${failureDetails}`);
76115
76365
  }
76116
76366
 
@@ -76202,6 +76452,12 @@
76202
76452
  const codexPrompt = appendCoderContext(promptWithAgent, resolvedCoderContext);
76203
76453
  // Note: Temporary scripts and runtime logs stay in the original project so they outlive an isolated worktree
76204
76454
  const scriptPath = buildScriptPath(nextPrompt.file, nextPrompt.section);
76455
+ // Note: Read before the first `[^]` rewrite of this round, which would overwrite the harness recorded so far
76456
+ const startedByRunnerSignature = resolveStartedByRunnerSignature({
76457
+ nextPrompt,
76458
+ runnerMetadata,
76459
+ thinkingLevel: options.thinkingLevel,
76460
+ });
76205
76461
  setPromptRoundRunningState({ isRichUiEnabled, promptLabel, scriptPath, uiHandle });
76206
76462
  await waitForRequestedPause({
76207
76463
  checkpointLabel: 'preparing the current prompt execution',
@@ -76234,6 +76490,7 @@
76234
76490
  onStepStarted: (progress) => recordPromptRoundInProgress({
76235
76491
  nextPrompt,
76236
76492
  runnerMetadata,
76493
+ startedByRunnerSignature,
76237
76494
  thinkingLevel: options.thinkingLevel,
76238
76495
  attemptCount,
76239
76496
  progress,
@@ -76244,6 +76501,7 @@
76244
76501
  options,
76245
76502
  nextPrompt,
76246
76503
  runnerMetadata,
76504
+ startedByRunnerSignature,
76247
76505
  promptExecutionStartedDate,
76248
76506
  result,
76249
76507
  commitMessage,
@@ -76277,6 +76535,7 @@
76277
76535
  await finalizeFailedPromptRound({
76278
76536
  nextPrompt,
76279
76537
  runnerMetadata,
76538
+ startedByRunnerSignature,
76280
76539
  promptExecutionStartedDate,
76281
76540
  attemptCount,
76282
76541
  error: lastError,
@@ -76289,6 +76548,23 @@
76289
76548
  throw lastError;
76290
76549
  }, { preserveArtifactsOnSuccess: options.preserveLogs });
76291
76550
  }
76551
+ /**
76552
+ * Reads which harness left the prompt of this round in the middle of its implementation.
76553
+ *
76554
+ * Only a prompt resumed through `--git-changes continue` still carries the in-progress `[^]` status when its
76555
+ * round starts, so every other round reports no earlier harness. A harness which continues its own work is
76556
+ * reported as no earlier harness either, because the status line would just name it twice.
76557
+ */
76558
+ function resolveStartedByRunnerSignature(options) {
76559
+ const { nextPrompt, runnerMetadata, thinkingLevel } = options;
76560
+ if (nextPrompt.section.status !== 'in-progress') {
76561
+ return undefined;
76562
+ }
76563
+ const { line } = resolvePromptStatusLine(nextPrompt.file, nextPrompt.section);
76564
+ const startedByRunnerSignature = parsePromptStartedByRunnerSignature(line);
76565
+ const currentRunnerSignature = formatRunnerSignature(runnerMetadata.runnerName, runnerMetadata.modelName, thinkingLevel);
76566
+ return startedByRunnerSignature === currentRunnerSignature ? undefined : startedByRunnerSignature;
76567
+ }
76292
76568
  /**
76293
76569
  * Records into the prompt file that the prompt is being implemented right now.
76294
76570
  *
@@ -76297,7 +76573,7 @@
76297
76573
  * crashes leaves the `[^]` status behind as the signal that this task was left in the middle.
76298
76574
  */
76299
76575
  async function recordPromptRoundInProgress(options) {
76300
- const { nextPrompt, runnerMetadata, thinkingLevel, attemptCount, progress } = options;
76576
+ const { nextPrompt, runnerMetadata, startedByRunnerSignature, thinkingLevel, attemptCount, progress } = options;
76301
76577
  markPromptInProgress({
76302
76578
  file: nextPrompt.file,
76303
76579
  section: nextPrompt.section,
@@ -76305,6 +76581,7 @@
76305
76581
  inProgressStepKind: progress.startedStepKind,
76306
76582
  runnerName: runnerMetadata.runnerName,
76307
76583
  modelName: runnerMetadata.modelName,
76584
+ startedByRunnerSignature,
76308
76585
  attemptCount,
76309
76586
  loginMethod: progress.loginMethod,
76310
76587
  thinkingLevel,
@@ -76362,14 +76639,24 @@
76362
76639
  * Finalizes a successful prompt round, including prompt bookkeeping and commit flow.
76363
76640
  */
76364
76641
  async function finalizeSuccessfulPromptRound(options) {
76365
- const { options: runOptions, nextPrompt, runnerMetadata, promptExecutionStartedDate, result, commitMessage, logPath, roundCommitScope, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
76642
+ const { options: runOptions, nextPrompt, runnerMetadata, startedByRunnerSignature, promptExecutionStartedDate, result, commitMessage, logPath, roundCommitScope, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
76366
76643
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.stopCapturingAgentOutput();
76367
76644
  await waitForRequestedPause({
76368
76645
  checkpointLabel: 'recording the successful prompt result',
76369
76646
  phase: 'running',
76370
76647
  statusMessage: 'Recording prompt result',
76371
76648
  });
76372
- markPromptDone(nextPrompt.file, nextPrompt.section, result.steps, runnerMetadata.runnerName, runnerMetadata.modelName, result.attemptCount, result.loginMethod, runOptions.thinkingLevel);
76649
+ markPromptDone({
76650
+ file: nextPrompt.file,
76651
+ section: nextPrompt.section,
76652
+ steps: result.steps,
76653
+ runnerName: runnerMetadata.runnerName,
76654
+ modelName: runnerMetadata.modelName,
76655
+ startedByRunnerSignature,
76656
+ attemptCount: result.attemptCount,
76657
+ loginMethod: result.loginMethod,
76658
+ thinkingLevel: runOptions.thinkingLevel,
76659
+ });
76373
76660
  // Note: The prompt status is always written into the original project, an isolated round transports
76374
76661
  // its own changes back through the merge instead
76375
76662
  await writePromptFile(nextPrompt.file);
@@ -76420,7 +76707,7 @@
76420
76707
  * Finalizes a failed prompt round, persisting prompt failure metadata before rethrowing.
76421
76708
  */
76422
76709
  async function finalizeFailedPromptRound(options) {
76423
- const { nextPrompt, runnerMetadata, promptExecutionStartedDate, attemptCount, error, options: runOptions, roundCommitScope, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
76710
+ const { nextPrompt, runnerMetadata, startedByRunnerSignature, promptExecutionStartedDate, attemptCount, error, options: runOptions, roundCommitScope, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
76424
76711
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.stopCapturingAgentOutput();
76425
76712
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setPhase('error');
76426
76713
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.addError(error instanceof Error ? error.message : String(error));
@@ -76429,7 +76716,15 @@
76429
76716
  phase: 'error',
76430
76717
  statusMessage: 'Recording prompt failure',
76431
76718
  });
76432
- markPromptFailed(nextPrompt.file, nextPrompt.section, runnerMetadata.runnerName, runnerMetadata.modelName, promptExecutionStartedDate, attemptCount);
76719
+ markPromptFailed({
76720
+ file: nextPrompt.file,
76721
+ section: nextPrompt.section,
76722
+ runnerName: runnerMetadata.runnerName,
76723
+ modelName: runnerMetadata.modelName,
76724
+ startedByRunnerSignature,
76725
+ promptExecutionStartedDate,
76726
+ attemptCount,
76727
+ });
76433
76728
  await writePromptFile(nextPrompt.file);
76434
76729
  await writePromptErrorLog({
76435
76730
  file: nextPrompt.file,
@@ -77182,6 +77477,50 @@
77182
77477
  }
77183
77478
  }
77184
77479
 
77480
+ /**
77481
+ * Builds a label that identifies the prompt section by index.
77482
+ */
77483
+ function buildPromptLabel(file, section) {
77484
+ return `${path.relative(process.cwd(), file.path).replace(/\\/g, '/')}#${section.index + 1}`;
77485
+ }
77486
+
77487
+ /**
77488
+ * Resolves the one prompt which `--git-changes continue` resumes.
77489
+ *
77490
+ * A coder which was killed or crashed leaves exactly one prompt behind in the in-progress `[^]` status,
77491
+ * so anything else means the working tree changes cannot be attributed to a single interrupted prompt.
77492
+ */
77493
+ function resolveInterruptedPrompt(files) {
77494
+ const interruptedPrompts = listPromptsByStatus(files, 'in-progress');
77495
+ if (interruptedPrompts.length === 0) {
77496
+ throw new NotFoundError(_spaceTrim.spaceTrim(`
77497
+ Flag \`--git-changes continue\` found no interrupted prompt to continue.
77498
+
77499
+ Continuing resumes the prompt an earlier coder left in the middle of its implementation, which is the
77500
+ one marked with the in-progress \`[^]\` status. No prompt carries that status right now.
77501
+
77502
+ Use \`--git-changes ignore\` to start the next \`[ ]\` prompt with the current working tree changes
77503
+ left in place, or \`--git-changes fail\` to commit or stash them first.
77504
+ `));
77505
+ }
77506
+ if (interruptedPrompts.length > 1) {
77507
+ throw new NotAllowed(_spaceTrim.spaceTrim((block) => `
77508
+ Flag \`--git-changes continue\` found ${interruptedPrompts.length} interrupted prompts, but it can
77509
+ continue only one.
77510
+
77511
+ The working tree changes cannot be attributed to a single prompt while more than one of them carries
77512
+ the in-progress \`[^]\` status. Resolve the extra ones by marking them \`[x]\`, \`[!]\` or \`[ ]\`
77513
+ and run the coder again.
77514
+
77515
+ Interrupted prompts:
77516
+ ${block(interruptedPrompts
77517
+ .map(({ file, section }) => `- \`${buildPromptLabel(file, section)}\``)
77518
+ .join('\n'))}
77519
+ `));
77520
+ }
77521
+ return interruptedPrompts[0];
77522
+ }
77523
+
77185
77524
  /**
77186
77525
  * Prints a summary of the prompt that will run next.
77187
77526
  */
@@ -77248,7 +77587,7 @@
77248
77587
  * visuals are rendered through the shared canvas avatar pipeline into terminal ASCII art, using a
77249
77588
  * transparent horizontal canvas instead of the website's framed 1:1 surface.
77250
77589
  *
77251
- * The visual is decorative, so any failure returns `null` and the caller keeps the default brand banner.
77590
+ * The visual is decorative, so any failure returns `null` and the caller renders no header visual at all.
77252
77591
  *
77253
77592
  * @param agentSource Source of the `--agent` book file.
77254
77593
  * @returns ANSI-colored ASCII-art renderer or `null` when the visual cannot be rendered.
@@ -77410,6 +77749,8 @@
77410
77749
  let previousRoundEndTime;
77411
77750
  let completedRunCount = 0;
77412
77751
  let hasRunTestBefore = false;
77752
+ // Note: Only the very first round resumes the interrupted prompt, every later round starts from a clean tree
77753
+ let isContinuingInterruptedPrompt = options.gitChanges === 'continue';
77413
77754
  while (just(true)) {
77414
77755
  if (options.autoPull && !options.dryRun) {
77415
77756
  await waitForRequestedPause({
@@ -77434,6 +77775,7 @@
77434
77775
  progressDisplay,
77435
77776
  uiHandle,
77436
77777
  promptRunnerIdentity,
77778
+ isContinuingInterruptedPrompt,
77437
77779
  });
77438
77780
  }
77439
77781
  if (!hasRunTestBefore) {
@@ -77448,6 +77790,7 @@
77448
77790
  uiHandle,
77449
77791
  waitForRequestedPause,
77450
77792
  hasWaitedForStart,
77793
+ isContinuingInterruptedPrompt,
77451
77794
  });
77452
77795
  hasRunTestBefore = true;
77453
77796
  }
@@ -77462,6 +77805,7 @@
77462
77805
  progressDisplay,
77463
77806
  uiHandle,
77464
77807
  promptRunnerIdentity,
77808
+ isContinuingInterruptedPrompt,
77465
77809
  });
77466
77810
  hasShownUpcomingTasks || (hasShownUpcomingTasks = showUpcomingTasksOnce({
77467
77811
  hasShownUpcomingTasks,
@@ -77482,7 +77826,12 @@
77482
77826
  }
77483
77827
  if (options.keepAlive) {
77484
77828
  announceKeepAliveStatus(promptQueueSnapshot, isRichUiEnabled, uiHandle);
77485
- await new Promise((resolve) => setTimeout(resolve, KEEP_ALIVE_POLL_INTERVAL_MS));
77829
+ // Note: The keep-alive poll runs in the `waiting` phase, where `S Skip current waiting`
77830
+ // is offered, so pressing `S` looks for new prompts right away
77831
+ await waitForSkippableWorldTimeDeadline({
77832
+ deadlineTimeMs: Date.now() + KEEP_ALIVE_POLL_INTERVAL_MS,
77833
+ pollIntervalMs: KEEP_ALIVE_POLL_INTERVAL_MS,
77834
+ });
77486
77835
  continue;
77487
77836
  }
77488
77837
  finishWhenNoPromptIsAvailable(promptQueueSnapshot, isRichUiEnabled, uiHandle);
@@ -77518,7 +77867,7 @@
77518
77867
  progressDisplay,
77519
77868
  uiHandle,
77520
77869
  });
77521
- if (!options.ignoreGitChanges) {
77870
+ if (isCleanWorkingTreeRequired(options.gitChanges, isContinuingInterruptedPrompt)) {
77522
77871
  await waitForRequestedPause({
77523
77872
  checkpointLabel: 'checking the git working tree',
77524
77873
  phase: 'loading',
@@ -77542,6 +77891,7 @@
77542
77891
  uiHandle,
77543
77892
  waitForRequestedPause,
77544
77893
  });
77894
+ isContinuingInterruptedPrompt = false;
77545
77895
  previousRoundStartTime = currentRoundStartTime;
77546
77896
  previousRoundEndTime = Date.now();
77547
77897
  completedRunCount += 1;
@@ -77585,9 +77935,9 @@
77585
77935
  Flag \`--allow-destructive-auto-migrate\` requires \`--auto-migrate\`.
77586
77936
  `));
77587
77937
  }
77588
- if (options.noCommit && !options.waitForUser && !options.ignoreGitChanges) {
77938
+ if (options.noCommit && !options.waitForUser && options.gitChanges !== 'ignore') {
77589
77939
  throw new NotAllowed(_spaceTrim.spaceTrim(`
77590
- Flag \`--no-commit\` requires \`--ignore-git-changes\` when running in auto mode (the default; pass \`--no-auto\` for interactive confirmation).
77940
+ Flag \`--no-commit\` requires \`--git-changes ignore\` when running in auto mode (the default; pass \`--no-auto\` for interactive confirmation).
77591
77941
 
77592
77942
  Without commits, the next prompt round would fail the clean working tree check.
77593
77943
  `));
@@ -77606,6 +77956,13 @@
77606
77956
  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.
77607
77957
  `));
77608
77958
  }
77959
+ if (options.isIsolated && options.gitChanges === 'continue') {
77960
+ throw new NotAllowed(_spaceTrim.spaceTrim(`
77961
+ Flag \`--isolate\` cannot be combined with \`--git-changes continue\`.
77962
+
77963
+ 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.
77964
+ `));
77965
+ }
77609
77966
  if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit <= 0)) {
77610
77967
  throw new NotAllowed(_spaceTrim.spaceTrim(`
77611
77968
  Flag \`--limit\` expects a positive integer.
@@ -77614,6 +77971,18 @@
77614
77971
  `));
77615
77972
  }
77616
77973
  }
77974
+ /**
77975
+ * Decides whether the working tree has to be verified clean before the next prompt starts.
77976
+ *
77977
+ * `--git-changes continue` waives the check for the single round which resumes the interrupted prompt,
77978
+ * because that round is started exactly for the uncommitted changes the interrupted prompt left behind.
77979
+ */
77980
+ function isCleanWorkingTreeRequired(gitChanges, isContinuingInterruptedPrompt) {
77981
+ if (gitChanges === 'ignore') {
77982
+ return false;
77983
+ }
77984
+ return !isContinuingInterruptedPrompt;
77985
+ }
77617
77986
  /**
77618
77987
  * Pulls the latest repository state before loading prompts when the feature is enabled.
77619
77988
  */
@@ -77672,7 +78041,7 @@
77672
78041
  * Runs the optional pre-coding verification and, when requested, its one repair prompt.
77673
78042
  */
77674
78043
  async function runTestBeforeIfNeeded(options) {
77675
- const { options: runOptions, runner, runnerMetadata, resolvedCoderContext, resolvedAgentSystemMessage, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, hasWaitedForStart, } = options;
78044
+ const { options: runOptions, runner, runnerMetadata, resolvedCoderContext, resolvedAgentSystemMessage, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, hasWaitedForStart, isContinuingInterruptedPrompt, } = options;
77676
78045
  if (runOptions.testBefore === 'no') {
77677
78046
  return hasWaitedForStart;
77678
78047
  }
@@ -77683,7 +78052,7 @@
77683
78052
  Pass one with ${'`--test <test-command>`'} or use the default ${'`npm test`'} command by providing the mode through the CLI.
77684
78053
  `));
77685
78054
  }
77686
- if (!runOptions.ignoreGitChanges) {
78055
+ if (isCleanWorkingTreeRequired(runOptions.gitChanges, isContinuingInterruptedPrompt)) {
77687
78056
  await waitForRequestedPause({
77688
78057
  checkpointLabel: 'checking the git working tree before testing',
77689
78058
  phase: 'loading',
@@ -77852,9 +78221,9 @@
77852
78221
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage(`Running prompts with ${runnerName}`);
77853
78222
  }
77854
78223
  /**
77855
- * Prepares the `--agent` book avatar ASCII-art renderer and shows it instead of the default brand banner.
78224
+ * Prepares the `--agent` book avatar ASCII-art renderer and shows it above the dashboard boxes.
77856
78225
  *
77857
- * Keeps the default banner when no agent is selected, the UI is disabled, or the visual cannot be rendered.
78226
+ * Leaves the header empty when no agent is selected, the UI is disabled, or the visual cannot be rendered.
77858
78227
  */
77859
78228
  async function initializeRunUiAgentVisual(uiHandle, agentSource) {
77860
78229
  if (!uiHandle || !agentSource) {
@@ -77869,7 +78238,7 @@
77869
78238
  * Loads prompt files, updates progress displays, and selects the next runnable prompt.
77870
78239
  */
77871
78240
  async function loadPromptQueueSnapshot(options) {
77872
- const { options: runOptions, isRichUiEnabled, progressDisplay, uiHandle, promptRunnerIdentity } = options;
78241
+ const { options: runOptions, isRichUiEnabled, progressDisplay, uiHandle, promptRunnerIdentity, isContinuingInterruptedPrompt, } = options;
77873
78242
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setCurrentScriptPath(undefined);
77874
78243
  const promptFiles = await loadPromptFiles(PROMPTS_DIR$1);
77875
78244
  const stats = summarizePrompts(promptFiles, runOptions.priorityFilter);
@@ -77881,7 +78250,11 @@
77881
78250
  return {
77882
78251
  promptFiles,
77883
78252
  stats,
77884
- nextPrompt: findNextTodoPrompt(promptFiles, runOptions.priorityFilter, promptRunnerIdentity),
78253
+ // Note: A resumed prompt is picked by its `[^]` status alone, the priority and runner filters select
78254
+ // which prompt is started next, not which unfinished work is continued
78255
+ nextPrompt: isContinuingInterruptedPrompt
78256
+ ? resolveInterruptedPrompt(promptFiles)
78257
+ : findNextTodoPrompt(promptFiles, runOptions.priorityFilter, promptRunnerIdentity),
77885
78258
  };
77886
78259
  }
77887
78260
  /**