@promptbook/cli 0.112.0-45 → 0.112.0-47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +16 -16
  2. package/esm/index.es.js +884 -285
  3. package/esm/index.es.js.map +1 -1
  4. package/esm/scripts/run-codex-prompts/common/waitForPause.d.ts +13 -1
  5. package/esm/scripts/run-codex-prompts/git/commitChanges.d.ts +3 -1
  6. package/esm/scripts/run-codex-prompts/ui/buildCoderRunOctopusVisual.d.ts +13 -0
  7. package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +3 -1
  8. package/esm/scripts/run-codex-prompts/ui/coderRunUiRefresh.d.ts +23 -0
  9. package/esm/scripts/run-codex-prompts/ui/coderRunUiText.d.ts +36 -0
  10. package/esm/scripts/run-codex-prompts/ui/renderCoderRunUi.d.ts +4 -3
  11. package/esm/scripts/utils/emojiTags/scanEmojiTagUsage.d.ts +51 -0
  12. package/esm/src/avatars/AvatarOrImage.d.ts +45 -0
  13. package/esm/src/avatars/index.d.ts +1 -0
  14. package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +6 -1
  15. package/esm/src/avatars/visuals/asciiOctopusAvatarVisual.d.ts +7 -0
  16. package/esm/src/avatars/visuals/avatarVisualRegistry.test.d.ts +1 -0
  17. package/esm/src/avatars/visuals/fractalAvatarVisual.d.ts +7 -0
  18. package/esm/src/avatars/visuals/octopus2AvatarVisual.d.ts +7 -0
  19. package/esm/src/avatars/visuals/octopus3AvatarVisual.d.ts +7 -0
  20. package/esm/src/avatars/visuals/octopusAvatarVisualShared.d.ts +125 -0
  21. package/esm/src/book-components/Chat/Chat/ChatMessageItem.d.ts +1 -1
  22. package/esm/src/book-components/Chat/Chat/ChatMessageList.d.ts +1 -1
  23. package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +1 -1
  24. package/esm/src/book-components/Chat/Chat/ChatToolCallModalComponents.d.ts +8 -2
  25. package/esm/src/book-components/Chat/hooks/useChatCompleteNotification.d.ts +2 -0
  26. package/esm/src/book-components/Chat/types/ChatParticipant.d.ts +10 -0
  27. package/esm/src/cli/cli-commands/coder/ensureCoderGitignoreFile.d.ts +1 -1
  28. package/esm/src/config.d.ts +2 -2
  29. package/esm/src/llm-providers/agent/RemoteAgent.d.ts +3 -0
  30. package/esm/src/utils/agents/resolveAgentAvatarImageUrl.d.ts +49 -5
  31. package/esm/src/utils/agents/resolveAgentAvatarImageUrl.test.d.ts +1 -0
  32. package/esm/src/version.d.ts +1 -1
  33. package/package.json +4 -2
  34. package/umd/index.umd.js +883 -284
  35. package/umd/index.umd.js.map +1 -1
  36. package/umd/scripts/run-codex-prompts/common/waitForPause.d.ts +13 -1
  37. package/umd/scripts/run-codex-prompts/git/commitChanges.d.ts +3 -1
  38. package/umd/scripts/run-codex-prompts/ui/buildCoderRunOctopusVisual.d.ts +13 -0
  39. package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +3 -1
  40. package/umd/scripts/run-codex-prompts/ui/coderRunUiRefresh.d.ts +23 -0
  41. package/umd/scripts/run-codex-prompts/ui/coderRunUiText.d.ts +36 -0
  42. package/umd/scripts/run-codex-prompts/ui/renderCoderRunUi.d.ts +4 -3
  43. package/umd/scripts/utils/emojiTags/scanEmojiTagUsage.d.ts +51 -0
  44. package/umd/src/avatars/AvatarOrImage.d.ts +45 -0
  45. package/umd/src/avatars/index.d.ts +1 -0
  46. package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +6 -1
  47. package/umd/src/avatars/visuals/asciiOctopusAvatarVisual.d.ts +7 -0
  48. package/umd/src/avatars/visuals/avatarVisualRegistry.test.d.ts +1 -0
  49. package/umd/src/avatars/visuals/fractalAvatarVisual.d.ts +7 -0
  50. package/umd/src/avatars/visuals/octopus2AvatarVisual.d.ts +7 -0
  51. package/umd/src/avatars/visuals/octopus3AvatarVisual.d.ts +7 -0
  52. package/umd/src/avatars/visuals/octopusAvatarVisualShared.d.ts +125 -0
  53. package/umd/src/book-components/Chat/Chat/ChatMessageItem.d.ts +1 -1
  54. package/umd/src/book-components/Chat/Chat/ChatMessageList.d.ts +1 -1
  55. package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +1 -1
  56. package/umd/src/book-components/Chat/Chat/ChatToolCallModalComponents.d.ts +8 -2
  57. package/umd/src/book-components/Chat/hooks/useChatCompleteNotification.d.ts +2 -0
  58. package/umd/src/book-components/Chat/types/ChatParticipant.d.ts +10 -0
  59. package/umd/src/cli/cli-commands/coder/ensureCoderGitignoreFile.d.ts +1 -1
  60. package/umd/src/config.d.ts +2 -2
  61. package/umd/src/llm-providers/agent/RemoteAgent.d.ts +3 -0
  62. package/umd/src/utils/agents/resolveAgentAvatarImageUrl.d.ts +49 -5
  63. package/umd/src/utils/agents/resolveAgentAvatarImageUrl.test.d.ts +1 -0
  64. package/umd/src/version.d.ts +1 -1
package/umd/index.umd.js CHANGED
@@ -60,7 +60,7 @@
60
60
  * @generated
61
61
  * @see https://github.com/webgptorg/promptbook
62
62
  */
63
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-45';
63
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-47';
64
64
  /**
65
65
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
66
66
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -1047,7 +1047,7 @@
1047
1047
  *
1048
1048
  * @public exported from `@promptbook/core`
1049
1049
  */
1050
- const CLAIM = `Turn your company's scattered knowledge into AI ready books`;
1050
+ const CLAIM = `Create persistent AI agents that turn your company's scattered knowledge into action`;
1051
1051
  // <- TODO: [🐊] Pick the best claim
1052
1052
  /**
1053
1053
  * Color of the Promptbook
@@ -1100,7 +1100,7 @@
1100
1100
  *
1101
1101
  * @public exported from `@promptbook/core`
1102
1102
  */
1103
- const DEFAULT_BOOK_TITLE = `✨ Untitled Book`;
1103
+ const DEFAULT_BOOK_TITLE = `🐙 Untitled agent`;
1104
1104
  /**
1105
1105
  * When the title of task is not provided, the default title is used
1106
1106
  *
@@ -2490,37 +2490,68 @@
2490
2490
  }
2491
2491
  // Note: [🟡] Code for coder init environment bootstrapping [ensureCoderEnvFile](src/cli/cli-commands/coder/ensureCoderEnvFile.ts) should never be published outside of `@promptbook/cli`
2492
2492
 
2493
+ /**
2494
+ * @@@
2495
+ *
2496
+ * @public exported from `@promptbook/utils`
2497
+ */
2498
+ function escapeRegExp$1(value) {
2499
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2500
+ }
2501
+
2493
2502
  /**
2494
2503
  * Relative path to `.gitignore` in the initialized project.
2495
2504
  */
2496
2505
  const GITIGNORE_FILE_PATH = '.gitignore';
2497
2506
  /**
2498
- * `.gitignore` block required by standalone Promptbook coder projects.
2507
+ * Promptbook coder temp directory that should stay out of version control.
2499
2508
  */
2500
- const CODER_GITIGNORE_BLOCK = _spaceTrim.spaceTrim(`
2501
- # Promptbook Coder
2502
- /.tmp
2503
- `);
2509
+ const CODER_TEMP_GITIGNORE_RULE = '/.tmp';
2504
2510
  /**
2505
- * Ensures `.gitignore` contains the standalone Promptbook coder cache entry.
2511
+ * Promptbook coder cache directory that should stay out of version control.
2512
+ */
2513
+ const CODER_CACHE_GITIGNORE_RULE = '/.promptbook/ptbk-coder';
2514
+ /**
2515
+ * Standard header used when appending Promptbook coder rules into `.gitignore`.
2516
+ */
2517
+ const CODER_GITIGNORE_HEADER = '# Promptbook Coder';
2518
+ /**
2519
+ * Ensures `.gitignore` contains the standalone Promptbook coder temp and cache entries.
2506
2520
  *
2507
2521
  * @private function of `initializeCoderProjectConfiguration`
2508
2522
  */
2509
2523
  async function ensureCoderGitignoreFile(projectPath) {
2510
2524
  const gitignorePath = path.join(projectPath, GITIGNORE_FILE_PATH);
2511
2525
  const currentGitignoreContent = await readTextFileIfExists(gitignorePath);
2512
- if (currentGitignoreContent !== undefined && hasTmpGitignoreRule(currentGitignoreContent)) {
2526
+ const missingRules = getMissingCoderGitignoreRules(currentGitignoreContent || '');
2527
+ if (currentGitignoreContent !== undefined && missingRules.length === 0) {
2513
2528
  return 'unchanged';
2514
2529
  }
2515
- const nextGitignoreContent = appendBlock(currentGitignoreContent || '', CODER_GITIGNORE_BLOCK);
2530
+ const nextGitignoreContent = appendBlock(currentGitignoreContent || '', buildCoderGitignoreBlock(missingRules));
2516
2531
  await promises.writeFile(gitignorePath, nextGitignoreContent, 'utf-8');
2517
2532
  return currentGitignoreContent === undefined ? 'created' : 'updated';
2518
2533
  }
2519
2534
  /**
2520
- * Detects whether `.gitignore` already covers the standalone coder temp directory.
2535
+ * Returns the Promptbook coder gitignore rules that still need to be added.
2521
2536
  */
2522
- function hasTmpGitignoreRule(gitignoreContent) {
2523
- return /(^|[\r\n])\/?\.tmp(?:[\r\n]|$)/u.test(gitignoreContent);
2537
+ function getMissingCoderGitignoreRules(gitignoreContent) {
2538
+ const requiredRules = [CODER_TEMP_GITIGNORE_RULE, CODER_CACHE_GITIGNORE_RULE];
2539
+ return requiredRules.filter((rule) => !hasGitignoreRule(gitignoreContent, rule));
2540
+ }
2541
+ /**
2542
+ * Builds the Promptbook coder `.gitignore` block for the missing rules only.
2543
+ */
2544
+ function buildCoderGitignoreBlock(missingRules) {
2545
+ return [CODER_GITIGNORE_HEADER, ...missingRules].join('\n');
2546
+ }
2547
+ /**
2548
+ * Detects whether `.gitignore` already covers one exact rule.
2549
+ */
2550
+ function hasGitignoreRule(gitignoreContent, rule) {
2551
+ const normalizedRulePattern = rule.startsWith('/')
2552
+ ? `/?${escapeRegExp$1(rule.slice(1))}`
2553
+ : escapeRegExp$1(rule);
2554
+ return new RegExp(`(^|[\\r\\n])${normalizedRulePattern}(?:[\\r\\n]|$)`, 'u').test(gitignoreContent);
2524
2555
  }
2525
2556
  // Note: [🟡] Code for coder init gitignore bootstrapping [ensureCoderGitignoreFile](src/cli/cli-commands/coder/ensureCoderGitignoreFile.ts) should never be published outside of `@promptbook/cli`
2526
2557
 
@@ -2657,7 +2688,7 @@
2657
2688
  ${typescript.flattenDiagnosticMessageText(parsedFile.error.messageText, '\n')}
2658
2689
  `));
2659
2690
  }
2660
- if (!isPlainObject(parsedFile.config)) {
2691
+ if (!isPlainObject$1(parsedFile.config)) {
2661
2692
  throw new ParseError(_spaceTrim.spaceTrim(`
2662
2693
  File \`${relativeFilePath}\` must contain one top-level JSON object.
2663
2694
  `));
@@ -2671,7 +2702,7 @@
2671
2702
  if (value === undefined) {
2672
2703
  return {};
2673
2704
  }
2674
- if (!isPlainObject(value)) {
2705
+ if (!isPlainObject$1(value)) {
2675
2706
  throw new ParseError(_spaceTrim.spaceTrim(`
2676
2707
  File \`${relativeFilePath}\` contains invalid \`${fieldPath}\`.
2677
2708
 
@@ -2716,7 +2747,7 @@
2716
2747
  /**
2717
2748
  * Checks whether one parsed JSON value is a plain object.
2718
2749
  */
2719
- function isPlainObject(value) {
2750
+ function isPlainObject$1(value) {
2720
2751
  return typeof value === 'object' && value !== null && !Array.isArray(value);
2721
2752
  }
2722
2753
  // Note: [🟡] Code for coder init JSON merging [mergeStringRecordJsonFile](src/cli/cli-commands/coder/mergeStringRecordJsonFile.ts) should never be published outside of `@promptbook/cli`
@@ -6196,15 +6227,6 @@
6196
6227
  // TODO: [🧠][🈴] Where is the best location for this file
6197
6228
  // Note: [💞] Ignore a discrepancy between file name and entity name
6198
6229
 
6199
- /**
6200
- * @@@
6201
- *
6202
- * @public exported from `@promptbook/utils`
6203
- */
6204
- function escapeRegExp$1(value) {
6205
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6206
- }
6207
-
6208
6230
  /**
6209
6231
  * HTTP header used by Promptbook clients to advertise their release version.
6210
6232
  *
@@ -43291,6 +43313,246 @@
43291
43313
  // TODO: Mirror from Collboard or some common package
43292
43314
  // Note: [💞] Ignore a discrepancy between file name and entity name
43293
43315
 
43316
+ /**
43317
+ * Default file globs scanned for emoji tags.
43318
+ */
43319
+ const DEFAULT_INCLUDE_GLOBS = ['**/*.{ts,tsx,js,jsx,json,md,txt}'];
43320
+ /**
43321
+ * Default ignored paths while scanning the repository.
43322
+ */
43323
+ const DEFAULT_IGNORE_GLOBS = ['**/node_modules/**', '**/.git/**', '**/.promptbook/ptbk-coder/**'];
43324
+ /**
43325
+ * Directory used for Promptbook coder runtime caches.
43326
+ */
43327
+ const PTBK_CODER_CACHE_DIRECTORY_PATH = '.promptbook/ptbk-coder';
43328
+ /**
43329
+ * Relative cache file path storing per-file emoji-tag scan results.
43330
+ */
43331
+ const EMOJI_TAG_SCAN_CACHE_FILE_PATH = `${PTBK_CODER_CACHE_DIRECTORY_PATH}/emoji-tag-scan-cache.json`;
43332
+ /**
43333
+ * Current schema version of the persisted emoji-tag scan cache.
43334
+ */
43335
+ const EMOJI_TAG_SCAN_CACHE_VERSION = 1;
43336
+ /**
43337
+ * Scans repository files for bracketed emoji tags while reusing per-file cache entries for unchanged files.
43338
+ */
43339
+ async function scanEmojiTagUsage(options) {
43340
+ var _a, _b, _c, _d;
43341
+ const rootDir = (_a = options.rootDir) !== null && _a !== void 0 ? _a : process.cwd();
43342
+ const includeGlobs = (_b = options.includeGlobs) !== null && _b !== void 0 ? _b : DEFAULT_INCLUDE_GLOBS;
43343
+ const ignoreGlobs = (_c = options.ignoreGlobs) !== null && _c !== void 0 ? _c : DEFAULT_IGNORE_GLOBS;
43344
+ const tagPrefix = (_d = options.tagPrefix) !== null && _d !== void 0 ? _d : '';
43345
+ const filesToScan = await findFilesToScan(rootDir, includeGlobs, ignoreGlobs);
43346
+ const matcher = buildEmojiTagMatcher(options.candidateEmojis, tagPrefix);
43347
+ const usedEmojis = new Set();
43348
+ const existingCache = await readEmojiTagScanCache(rootDir);
43349
+ const nextCacheFiles = { ...existingCache.files };
43350
+ let isCacheDirty = false;
43351
+ let scannedFileCount = 0;
43352
+ let reusedFileCount = 0;
43353
+ for (const filePath of filesToScan) {
43354
+ try {
43355
+ const fileStats = fs.statSync(filePath);
43356
+ const cacheKey = toCacheKey(rootDir, filePath);
43357
+ const cachedFile = nextCacheFiles[cacheKey];
43358
+ const cachedEmojis = getCachedFileEmojis(cachedFile, fileStats.mtimeMs, fileStats.size, tagPrefix);
43359
+ if (cachedEmojis) {
43360
+ reusedFileCount += 1;
43361
+ addEmojis(usedEmojis, cachedEmojis);
43362
+ continue;
43363
+ }
43364
+ const fileContent = fs.readFileSync(filePath, 'utf-8'); /* Note: sync file reads are fine for local tooling. */
43365
+ const scannedEmojis = scanFileForEmojiTags(fileContent, matcher);
43366
+ addEmojis(usedEmojis, scannedEmojis);
43367
+ nextCacheFiles[cacheKey] = updateCachedFile(cachedFile, fileStats.mtimeMs, fileStats.size, tagPrefix, scannedEmojis);
43368
+ scannedFileCount += 1;
43369
+ isCacheDirty = true;
43370
+ }
43371
+ catch (error) {
43372
+ if (options.onFileError) {
43373
+ options.onFileError(normalizeError(error), filePath);
43374
+ continue;
43375
+ }
43376
+ throw error;
43377
+ }
43378
+ }
43379
+ if (isCacheDirty) {
43380
+ await writeEmojiTagScanCache(rootDir, {
43381
+ version: EMOJI_TAG_SCAN_CACHE_VERSION,
43382
+ files: nextCacheFiles,
43383
+ });
43384
+ }
43385
+ return {
43386
+ usedEmojis,
43387
+ scannedFileCount,
43388
+ reusedFileCount,
43389
+ };
43390
+ }
43391
+ /**
43392
+ * Resolves files to scan for matching emoji tags.
43393
+ */
43394
+ async function findFilesToScan(rootDir, includeGlobs, ignoreGlobs) {
43395
+ const files = new Set();
43396
+ for (const pattern of includeGlobs) {
43397
+ const matches = await glob__default["default"](pattern, {
43398
+ cwd: rootDir,
43399
+ ignore: Array.from(new Set([...ignoreGlobs, ...DEFAULT_IGNORE_GLOBS])),
43400
+ nodir: true,
43401
+ absolute: true,
43402
+ });
43403
+ for (const match of matches) {
43404
+ files.add(match);
43405
+ }
43406
+ }
43407
+ return Array.from(files);
43408
+ }
43409
+ /**
43410
+ * Builds a regex that matches one exact bracketed emoji-tag form.
43411
+ */
43412
+ function buildEmojiTagMatcher(candidateEmojis, tagPrefix) {
43413
+ const escapedEmojiAlternatives = Array.from(candidateEmojis)
43414
+ .sort((leftEmoji, rightEmoji) => rightEmoji.length - leftEmoji.length)
43415
+ .map((emoji) => escapeRegExp$1(emoji))
43416
+ .join('|');
43417
+ if (escapedEmojiAlternatives === '') {
43418
+ return /$^/u;
43419
+ }
43420
+ return new RegExp(`\\[${escapeRegExp$1(tagPrefix)}(?<emoji>${escapedEmojiAlternatives})\\]`, 'gu');
43421
+ }
43422
+ /**
43423
+ * Extracts matching emojis from one file content.
43424
+ */
43425
+ function scanFileForEmojiTags(fileContent, matcher) {
43426
+ var _a;
43427
+ matcher.lastIndex = 0;
43428
+ const matchedEmojis = new Set();
43429
+ for (const match of fileContent.matchAll(matcher)) {
43430
+ const emoji = (_a = match.groups) === null || _a === void 0 ? void 0 : _a.emoji;
43431
+ if (emoji) {
43432
+ matchedEmojis.add(emoji);
43433
+ }
43434
+ }
43435
+ return Array.from(matchedEmojis);
43436
+ }
43437
+ /**
43438
+ * Returns cached emojis when the file metadata still matches the stored cache entry.
43439
+ */
43440
+ function getCachedFileEmojis(cachedFile, mtimeMs, size, tagPrefix) {
43441
+ if (!cachedFile || cachedFile.mtimeMs !== mtimeMs || cachedFile.size !== size) {
43442
+ return undefined;
43443
+ }
43444
+ const cachedEmojis = cachedFile.tagsByPrefix[tagPrefix];
43445
+ return Array.isArray(cachedEmojis) ? cachedEmojis : undefined;
43446
+ }
43447
+ /**
43448
+ * Stores the latest scan result for one file while preserving other prefix caches when the file revision is unchanged.
43449
+ */
43450
+ function updateCachedFile(cachedFile, mtimeMs, size, tagPrefix, emojis) {
43451
+ const isSameFileRevision = (cachedFile === null || cachedFile === void 0 ? void 0 : cachedFile.mtimeMs) === mtimeMs && cachedFile.size === size;
43452
+ return {
43453
+ mtimeMs,
43454
+ size,
43455
+ tagsByPrefix: {
43456
+ ...(isSameFileRevision ? cachedFile.tagsByPrefix : {}),
43457
+ [tagPrefix]: [...emojis],
43458
+ },
43459
+ };
43460
+ }
43461
+ /**
43462
+ * Loads the persisted emoji-tag scan cache and falls back to an empty cache when it is missing or invalid.
43463
+ */
43464
+ async function readEmojiTagScanCache(rootDir) {
43465
+ try {
43466
+ const cacheContent = await promises.readFile(path.join(rootDir, EMOJI_TAG_SCAN_CACHE_FILE_PATH), 'utf-8');
43467
+ return normalizeEmojiTagScanCache(JSON.parse(cacheContent));
43468
+ }
43469
+ catch (_a) {
43470
+ return createEmptyEmojiTagScanCache();
43471
+ }
43472
+ }
43473
+ /**
43474
+ * Persists the updated emoji-tag scan cache as a best-effort optimization.
43475
+ */
43476
+ async function writeEmojiTagScanCache(rootDir, cache) {
43477
+ try {
43478
+ await promises.mkdir(path.join(rootDir, PTBK_CODER_CACHE_DIRECTORY_PATH), { recursive: true });
43479
+ await promises.writeFile(path.join(rootDir, EMOJI_TAG_SCAN_CACHE_FILE_PATH), `${JSON.stringify(cache, null, 2)}\n`, 'utf-8');
43480
+ }
43481
+ catch (_a) {
43482
+ // Note: Cache writes are only an optimization; scanning still succeeds when the cache cannot be written.
43483
+ }
43484
+ }
43485
+ /**
43486
+ * Normalizes one parsed cache payload into the current typed cache shape.
43487
+ */
43488
+ function normalizeEmojiTagScanCache(value) {
43489
+ if (!isPlainObject(value) || value.version !== EMOJI_TAG_SCAN_CACHE_VERSION || !isPlainObject(value.files)) {
43490
+ return createEmptyEmojiTagScanCache();
43491
+ }
43492
+ const files = {};
43493
+ for (const [filePath, cachedValue] of Object.entries(value.files)) {
43494
+ if (!isPlainObject(cachedValue)) {
43495
+ continue;
43496
+ }
43497
+ const { mtimeMs, size, tagsByPrefix } = cachedValue;
43498
+ if (typeof mtimeMs !== 'number' || typeof size !== 'number' || !isPlainObject(tagsByPrefix)) {
43499
+ continue;
43500
+ }
43501
+ const normalizedTagsByPrefix = {};
43502
+ for (const [tagPrefix, cachedEmojis] of Object.entries(tagsByPrefix)) {
43503
+ if (!Array.isArray(cachedEmojis)) {
43504
+ continue;
43505
+ }
43506
+ normalizedTagsByPrefix[tagPrefix] = cachedEmojis.filter((emoji) => typeof emoji === 'string');
43507
+ }
43508
+ files[filePath] = {
43509
+ mtimeMs,
43510
+ size,
43511
+ tagsByPrefix: normalizedTagsByPrefix,
43512
+ };
43513
+ }
43514
+ return {
43515
+ version: EMOJI_TAG_SCAN_CACHE_VERSION,
43516
+ files,
43517
+ };
43518
+ }
43519
+ /**
43520
+ * Creates an empty cache payload for emoji-tag scans.
43521
+ */
43522
+ function createEmptyEmojiTagScanCache() {
43523
+ return {
43524
+ version: EMOJI_TAG_SCAN_CACHE_VERSION,
43525
+ files: {},
43526
+ };
43527
+ }
43528
+ /**
43529
+ * Converts an absolute file path into a stable cache key relative to the scanned root.
43530
+ */
43531
+ function toCacheKey(rootDir, filePath) {
43532
+ return path.relative(rootDir, filePath).replace(/\\/gu, '/');
43533
+ }
43534
+ /**
43535
+ * Adds multiple emojis into one target set.
43536
+ */
43537
+ function addEmojis(target, emojis) {
43538
+ for (const emoji of emojis) {
43539
+ target.add(emoji);
43540
+ }
43541
+ }
43542
+ /**
43543
+ * Checks whether one unknown JSON value is a plain object.
43544
+ */
43545
+ function isPlainObject(value) {
43546
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
43547
+ }
43548
+ /**
43549
+ * Normalizes thrown values into proper `Error` objects for optional callbacks.
43550
+ */
43551
+ function normalizeError(error) {
43552
+ return error instanceof Error ? error : new Error(String(error));
43553
+ }
43554
+ // Note: [?] Code in this file should never be published in any package
43555
+
43294
43556
  // find-fresh-emoji-tags.ts
43295
43557
  // Note: When run as a standalone script, call the exported function
43296
43558
  if (require.main === module) {
@@ -43320,29 +43582,16 @@
43320
43582
  async function findFreshEmojiTag() {
43321
43583
  initializeFindFreshEmojiTagRun();
43322
43584
  console.info(`🤪 Find fresh emoji tag`);
43323
- const allFiles = await glob__default["default"]('**/*.{ts,tsx,js,jsx,json,md,txt}', {
43324
- ignore: '**/node_modules/**', // <- TODO: [🚰] Ignore also hidden folders like *(`.promptbook`, `.next`, `.git`,...)*
43325
- });
43326
43585
  const allEmojis = EMOJIS_OF_SINGLE_PICTOGRAM;
43327
- // const allEmojis = new Set<string_char_emoji>(['🧎' as string_char_emoji, '🥎' as string_char_emoji]);
43328
- const usedEmojis = new Set();
43329
- for (const file of allFiles) {
43330
- try {
43331
- const content = fs.readFileSync(file, 'utf-8'); /* <- Note: Its OK to use sync in tooling for scripts */
43332
- for (const emoji of allEmojis) {
43333
- const tag = `[${emoji}]`;
43334
- if (content.includes(tag)) {
43335
- usedEmojis.add(emoji);
43336
- }
43337
- }
43338
- }
43339
- catch (error) {
43340
- console.error(colors__default["default"].red('Error in checking file file /' + file));
43586
+ const { usedEmojis } = await scanEmojiTagUsage({
43587
+ candidateEmojis: allEmojis,
43588
+ tagPrefix: '',
43589
+ onFileError: (error, filePath) => {
43590
+ console.error(colors__default["default"].red('Error in checking file /' + filePath));
43341
43591
  console.error(error);
43342
- }
43343
- }
43344
- //console.info({ usedEmojis });
43345
- const freshEmojis = difference(allEmojis, usedEmojis);
43592
+ },
43593
+ });
43594
+ const freshEmojis = new Set(Array.from(allEmojis).filter((emoji) => !usedEmojis.has(emoji)));
43346
43595
  console.info(colors__default["default"].green(`Avialable fresh tags:`));
43347
43596
  const randomEmojis = [...$shuffleItems(...Array.from(freshEmojis))].splice(0, 10);
43348
43597
  // const randomEmojis = freshEmojis;
@@ -43950,9 +44199,14 @@
43950
44199
  const includeGlobs = (_b = options.includeGlobs) !== null && _b !== void 0 ? _b : ['**/*.{ts,tsx,js,jsx,json,md,txt}'];
43951
44200
  const ignoreGlobs = (_c = options.ignoreGlobs) !== null && _c !== void 0 ? _c : ['**/node_modules/**'];
43952
44201
  const tagPrefix = (_d = options.tagPrefix) !== null && _d !== void 0 ? _d : PROMPT_EMOJI_TAG_PREFIX;
43953
- const filesToScan = await findFilesToScan(rootDir, includeGlobs, ignoreGlobs);
43954
- const usedEmojis = collectUsedPromptEmojis(filesToScan, tagPrefix);
43955
- const freshEmojis = difference(EMOJIS_OF_SINGLE_PICTOGRAM, usedEmojis);
44202
+ const { usedEmojis } = await scanEmojiTagUsage({
44203
+ rootDir,
44204
+ includeGlobs,
44205
+ ignoreGlobs,
44206
+ tagPrefix,
44207
+ candidateEmojis: EMOJIS_OF_SINGLE_PICTOGRAM,
44208
+ });
44209
+ const freshEmojis = new Set(Array.from(EMOJIS_OF_SINGLE_PICTOGRAM).filter((emoji) => !usedEmojis.has(emoji)));
43956
44210
  const shuffledEmojis = $shuffleItems(...Array.from(freshEmojis));
43957
44211
  const selectedEmojis = shuffledEmojis.slice(0, count);
43958
44212
  if (selectedEmojis.length < count) {
@@ -43964,40 +44218,6 @@
43964
44218
  tagPrefix,
43965
44219
  };
43966
44220
  }
43967
- /**
43968
- * Resolves files to scan for existing prompt emoji tags.
43969
- */
43970
- async function findFilesToScan(rootDir, includeGlobs, ignoreGlobs) {
43971
- const files = new Set();
43972
- for (const pattern of includeGlobs) {
43973
- const matches = await glob__default["default"](pattern, {
43974
- cwd: rootDir,
43975
- ignore: ignoreGlobs,
43976
- nodir: true,
43977
- absolute: true,
43978
- });
43979
- for (const match of matches) {
43980
- files.add(match);
43981
- }
43982
- }
43983
- return Array.from(files);
43984
- }
43985
- /**
43986
- * Collects emojis already used with the configured prompt prefix.
43987
- */
43988
- function collectUsedPromptEmojis(filePaths, tagPrefix) {
43989
- const usedEmojis = new Set();
43990
- for (const file of filePaths) {
43991
- const content = fs.readFileSync(file, 'utf-8'); /* Note: sync read is fine for script tooling. */
43992
- for (const emoji of EMOJIS_OF_SINGLE_PICTOGRAM) {
43993
- const tag = formatPromptEmojiTag(emoji, tagPrefix);
43994
- if (content.includes(tag)) {
43995
- usedEmojis.add(emoji);
43996
- }
43997
- }
43998
- }
43999
- return usedEmojis;
44000
- }
44001
44221
  // Note: [?] Code in this file should never be published in any package
44002
44222
 
44003
44223
  var promptEmojiTags = /*#__PURE__*/Object.freeze({
@@ -45026,6 +45246,27 @@
45026
45246
  * Current pause state.
45027
45247
  */
45028
45248
  let pauseState = 'RUNNING';
45249
+ /**
45250
+ * Stores one new pause state in the shared runner controller.
45251
+ */
45252
+ function setPauseState(nextPauseState) {
45253
+ pauseState = nextPauseState;
45254
+ }
45255
+ /**
45256
+ * Applies the same three-state toggle used by the `P` hotkey.
45257
+ */
45258
+ function togglePauseState() {
45259
+ if (pauseState === 'RUNNING') {
45260
+ setPauseState('PAUSING');
45261
+ return 'REQUESTED_PAUSE';
45262
+ }
45263
+ if (pauseState === 'PAUSING') {
45264
+ setPauseState('RUNNING');
45265
+ return 'CANCELLED_PAUSE';
45266
+ }
45267
+ setPauseState('RUNNING');
45268
+ return 'RESUMED';
45269
+ }
45029
45270
  /**
45030
45271
  * Listens for the "p" key to pause and resume.
45031
45272
  */
@@ -45040,19 +45281,13 @@
45040
45281
  process.exit();
45041
45282
  }
45042
45283
  if (key.name === 'p') {
45043
- if (pauseState === 'RUNNING') {
45044
- pauseState = 'PAUSING';
45284
+ const toggleResult = togglePauseState();
45285
+ if (toggleResult === 'REQUESTED_PAUSE') {
45045
45286
  // Note: Using console.log here which adds a new line.
45046
45287
  // This is intentional to prevent the message from being overwritten.
45047
45288
  console.log(colors__default["default"].bgWhite('Pausing...'));
45048
45289
  }
45049
- else if (pauseState === 'PAUSED') {
45050
- pauseState = 'RUNNING';
45051
- // The checkPause loop will terminate.
45052
- }
45053
- else if (pauseState === 'PAUSING') {
45054
- // If user presses 'p' again while pausing, cancel the pause.
45055
- pauseState = 'RUNNING';
45290
+ else if (toggleResult === 'CANCELLED_PAUSE') {
45056
45291
  console.log(colors__default["default"].green('Pause cancelled. Resuming...'));
45057
45292
  }
45058
45293
  }
@@ -45068,12 +45303,12 @@
45068
45303
  async function checkPause(options) {
45069
45304
  var _a, _b;
45070
45305
  if (pauseState === 'PAUSING') {
45071
- pauseState = 'PAUSED';
45306
+ setPauseState('PAUSED');
45072
45307
  if (!(options === null || options === void 0 ? void 0 : options.silent)) {
45073
45308
  console.log(colors__default["default"].bgWhite.black('Paused') + colors__default["default"].gray(' (Press "p" to resume)'));
45074
45309
  }
45075
45310
  (_a = options === null || options === void 0 ? void 0 : options.onPaused) === null || _a === void 0 ? void 0 : _a.call(options);
45076
- while (pauseState === 'PAUSED') {
45311
+ while (getPauseState() === 'PAUSED') {
45077
45312
  await new Promise((resolve) => setTimeout(resolve, 100));
45078
45313
  }
45079
45314
  (_b = options === null || options === void 0 ? void 0 : options.onResumed) === null || _b === void 0 ? void 0 : _b.call(options);
@@ -45088,20 +45323,6 @@
45088
45323
  function getPauseState() {
45089
45324
  return pauseState;
45090
45325
  }
45091
- /**
45092
- * Requests a pause from an external controller (e.g. the Ink UI).
45093
- */
45094
- function requestPause() {
45095
- if (pauseState === 'RUNNING') {
45096
- pauseState = 'PAUSING';
45097
- }
45098
- }
45099
- /**
45100
- * Resumes execution from an external controller after a pause.
45101
- */
45102
- function requestResume() {
45103
- pauseState = 'RUNNING';
45104
- }
45105
45326
 
45106
45327
  /**
45107
45328
  * Environment variable that configures the name used for agent commits.
@@ -45446,7 +45667,8 @@
45446
45667
 
45447
45668
  /**
45448
45669
  * Commits staged changes with the provided message using the dedicated coding-agent identity when configured,
45449
- * otherwise falls back to the default Git configuration. Remote pushing is opt-in via `options.autoPush`.
45670
+ * otherwise falls back to the default Git configuration. Remote pushing is opt-in via `options.autoPush`,
45671
+ * while `options.excludePaths` can keep temporary artifacts out of the created commit.
45450
45672
  */
45451
45673
  async function commitChanges(message, options) {
45452
45674
  const projectPath = process.cwd();
@@ -45456,11 +45678,7 @@
45456
45678
  try {
45457
45679
  const agentEnv = buildAgentGitEnv();
45458
45680
  const signingFlag = buildAgentGitSigningFlag();
45459
- await runGitCommand({
45460
- command: 'git add .',
45461
- cwd: projectPath,
45462
- env: agentEnv,
45463
- });
45681
+ await stageCommitChanges(projectPath, agentEnv, options === null || options === void 0 ? void 0 : options.excludePaths);
45464
45682
  await runGitCommand({
45465
45683
  command: buildGitCommitCommand(commitMessagePath, signingFlag),
45466
45684
  cwd: projectPath,
@@ -45474,6 +45692,56 @@
45474
45692
  await promises.unlink(commitMessagePath).catch(() => undefined);
45475
45693
  }
45476
45694
  }
45695
+ /**
45696
+ * Stages repository changes and optionally unstages temporary files that should not end up inside the commit.
45697
+ */
45698
+ async function stageCommitChanges(projectPath, agentEnv, excludePaths) {
45699
+ await runGitCommand({
45700
+ command: 'git add .',
45701
+ cwd: projectPath,
45702
+ env: agentEnv,
45703
+ });
45704
+ const excludedGitPaths = normalizeExcludedGitPaths(projectPath, excludePaths);
45705
+ if (excludedGitPaths.length === 0) {
45706
+ return;
45707
+ }
45708
+ await runGitCommand({
45709
+ command: `git reset --quiet HEAD -- ${excludedGitPaths.map(quoteShellPath).join(' ')}`,
45710
+ cwd: projectPath,
45711
+ env: agentEnv,
45712
+ isVerbose: false,
45713
+ });
45714
+ }
45715
+ /**
45716
+ * Converts excluded filesystem paths into unique repository-relative Git paths.
45717
+ */
45718
+ function normalizeExcludedGitPaths(projectPath, excludePaths) {
45719
+ if (!excludePaths || excludePaths.length === 0) {
45720
+ return [];
45721
+ }
45722
+ return [
45723
+ ...new Set(excludePaths
45724
+ .map((excludePath) => normalizeExcludedGitPath(projectPath, excludePath))
45725
+ .filter((gitPath) => Boolean(gitPath))),
45726
+ ];
45727
+ }
45728
+ /**
45729
+ * Converts one excluded filesystem path into a Git-friendly repository-relative path.
45730
+ */
45731
+ function normalizeExcludedGitPath(projectPath, excludePath) {
45732
+ const absoluteExcludePath = path.resolve(projectPath, excludePath);
45733
+ const relativeExcludePath = path.relative(projectPath, absoluteExcludePath).replace(/\\/gu, '/');
45734
+ if (relativeExcludePath === '' || relativeExcludePath === '.' || relativeExcludePath.startsWith('../')) {
45735
+ return undefined;
45736
+ }
45737
+ return relativeExcludePath;
45738
+ }
45739
+ /**
45740
+ * Quotes one Git path for safe shell execution.
45741
+ */
45742
+ function quoteShellPath(path) {
45743
+ return JSON.stringify(path);
45744
+ }
45477
45745
  /**
45478
45746
  * Branded error used when pushing committed changes fails.
45479
45747
  */
@@ -48694,49 +48962,215 @@ bash "$1"
48694
48962
  return `${scriptPath}.test.sh`;
48695
48963
  }
48696
48964
 
48965
+ /**
48966
+ * Centers an ANSI-colored line within the available frame width.
48967
+ *
48968
+ * @private internal utility of coder run UI
48969
+ */
48970
+ function centerAnsiText(text, width) {
48971
+ const paddingWidth = Math.max(0, Math.floor((width - visibleLength(text)) / 2));
48972
+ return `${' '.repeat(paddingWidth)}${text}`;
48973
+ }
48974
+ /**
48975
+ * Pads or truncates a possibly ANSI-colored line to the target visible width.
48976
+ *
48977
+ * @private internal utility of coder run UI
48978
+ */
48979
+ function padAnsiText(text, width) {
48980
+ const fittedText = fitAnsiText(text, width);
48981
+ return fittedText + ' '.repeat(Math.max(0, width - visibleLength(fittedText)));
48982
+ }
48983
+ /**
48984
+ * Truncates a possibly ANSI-colored line to the target visible width.
48985
+ *
48986
+ * @private internal utility of coder run UI
48987
+ */
48988
+ function fitAnsiText(text, width) {
48989
+ if (visibleLength(text) <= width) {
48990
+ return text;
48991
+ }
48992
+ return fitPlainText(stripAnsi(text), width);
48993
+ }
48994
+ /**
48995
+ * Truncates a plain-text line to the target width with an ellipsis.
48996
+ *
48997
+ * @private internal utility of coder run UI
48998
+ */
48999
+ function fitPlainText(text, width) {
49000
+ if (text.length <= width) {
49001
+ return text;
49002
+ }
49003
+ if (width <= 3) {
49004
+ return '.'.repeat(width);
49005
+ }
49006
+ return `${text.slice(0, width - 3)}...`;
49007
+ }
49008
+ /**
49009
+ * Measures visible string width by stripping ANSI escape codes.
49010
+ *
49011
+ * @private internal utility of coder run UI
49012
+ */
49013
+ function visibleLength(text) {
49014
+ return stripAnsi(text).length;
49015
+ }
49016
+ /**
49017
+ * Strips ANSI escape codes from a string.
49018
+ *
49019
+ * @private internal utility of coder run UI
49020
+ */
49021
+ function stripAnsi(text) {
49022
+ // eslint-disable-next-line no-control-regex
49023
+ return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
49024
+ }
49025
+
49026
+ /**
49027
+ * Fixed left-side head used across all octopus frames.
49028
+ */
49029
+ const OCTOPUS_HEAD_LINES = [
49030
+ colors__default["default"].magenta.bold(' .-""""-.'),
49031
+ colors__default["default"].magenta.bold(" .' .-. '."),
49032
+ `${colors__default["default"].magenta.bold(' / (')}${colors__default["default"].yellow.bold('o o')}${colors__default["default"].magenta.bold(') \\')}`,
49033
+ colors__default["default"].magenta.bold(' | ^ |'),
49034
+ colors__default["default"].magenta.bold(' | \\___/ |'),
49035
+ colors__default["default"].magenta.bold(' \\___________/'),
49036
+ ];
49037
+ /**
49038
+ * Gap between the head silhouette and the animated tentacles.
49039
+ */
49040
+ const OCTOPUS_HEAD_TO_TENTACLE_GAP = ' ';
49041
+ /**
49042
+ * Animated right-side tentacle poses.
49043
+ */
49044
+ const OCTOPUS_TENTACLE_FRAMES = [
49045
+ [
49046
+ ` ${colors__default["default"].green.bold('ptbk.io')}`,
49047
+ colors__default["default"].cyan(' __ __'),
49048
+ colors__default["default"].cyan(' _/\\/\\/ \\_/\\/ \\_/\\_'),
49049
+ colors__default["default"].cyan('_/\\/ _ /\\/ _ /\\/\\__'),
49050
+ colors__default["default"].cyan('\\__/ /_/\\/ \\_/ \\_/ \\_/'),
49051
+ colors__default["default"].cyan(' /_/ \\__/ \\__/ /'),
49052
+ ],
49053
+ [
49054
+ ` ${colors__default["default"].green.bold('ptbk.io')}`,
49055
+ colors__default["default"].cyan(' __ __'),
49056
+ colors__default["default"].cyan(' _/\\/ \\_/\\/\\_ \\_/\\_'),
49057
+ colors__default["default"].cyan('_/\\/ _ /\\/ _ \\/\\/ \\__'),
49058
+ colors__default["default"].cyan('\\__/ /_/\\/ \\_/ \\_ /\\_/'),
49059
+ colors__default["default"].cyan(' /_/ \\__/ \\__/ /'),
49060
+ ],
49061
+ [
49062
+ ` ${colors__default["default"].green.bold('ptbk.io')}`,
49063
+ colors__default["default"].cyan(' __ __'),
49064
+ colors__default["default"].cyan(' _/\\/\\/ \\_/\\_ \\_/\\_'),
49065
+ colors__default["default"].cyan('_/\\/ _ /\\/ _ /\\/ \\_'),
49066
+ colors__default["default"].cyan('\\__/ /_/\\/ \\_/ \\_/ _/'),
49067
+ colors__default["default"].cyan(' /_/ \\__/ \\__/'),
49068
+ ],
49069
+ [
49070
+ ` ${colors__default["default"].green.bold('ptbk.io')}`,
49071
+ colors__default["default"].cyan(' __ __'),
49072
+ colors__default["default"].cyan(' _/\\/ \\_/\\/ \\_/\\/\\_'),
49073
+ colors__default["default"].cyan('_/\\/ _ /\\/ _ /\\/\\ \\'),
49074
+ colors__default["default"].cyan('\\__/ /_/\\/ \\_/ \\_ \\_/'),
49075
+ colors__default["default"].cyan(' /_/ \\__/ \\__\\_/'),
49076
+ ],
49077
+ ];
49078
+ /**
49079
+ * Builds the horizontal octopus illustration shown above the coder-run dashboard.
49080
+ *
49081
+ * @private internal utility of coder run UI
49082
+ */
49083
+ function buildCoderRunOctopusVisual(options) {
49084
+ const tentacleFrame = OCTOPUS_TENTACLE_FRAMES[((options.animationFrame % OCTOPUS_TENTACLE_FRAMES.length) + OCTOPUS_TENTACLE_FRAMES.length) %
49085
+ OCTOPUS_TENTACLE_FRAMES.length];
49086
+ const visualLines = OCTOPUS_HEAD_LINES.map((headLine, lineIndex) => `${headLine}${OCTOPUS_HEAD_TO_TENTACLE_GAP}${tentacleFrame[lineIndex]}`);
49087
+ const visualWidth = visualLines.reduce((maxWidth, line) => Math.max(maxWidth, visibleLength(line)), 0);
49088
+ visualLines.map((line) => centerAnsiText(padAnsiText(line, visualWidth), options.totalWidth));
49089
+ /*
49090
+ Note: Octopus art should look better, now using just text
49091
+ 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
49092
+ https://www.mrgoodfish.com/wp-content/uploads/2022/09/Eledone_moschata__.png
49093
+ https://fish-commercial-names.ec.europa.eu/fish-names/jakarta.faces.resource/pictograms/octopus_vulgaris.jpg.xhtml?ln=images
49094
+ https://www.asciiart.eu/image-to-ascii
49095
+ */
49096
+ // 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
49097
+ return _spaceTrim__default["default"](`
49098
+
49099
+ ▄▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄ ▄▄ ▄▄ ▄▄ ▄▄▄
49100
+ ██▄█▀ ██ ██▄██ ██▄█▀ ██ ██▀██
49101
+ ██ ██ ██▄█▀ ██ ██ ▄ ██ ▀███▀
49102
+
49103
+ `)
49104
+ .split('\n')
49105
+ .map((line) => centerAnsiText(line, options.totalWidth));
49106
+ }
49107
+
49108
+ /**
49109
+ * Refresh cadence used only while the rich coder UI needs animated updates.
49110
+ *
49111
+ * @private internal constant of coder run UI
49112
+ */
49113
+ const ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS = 300;
49114
+ /**
49115
+ * Phases that still benefit from automatic refreshes because the frame can change
49116
+ * over time even without new runner output.
49117
+ *
49118
+ * @private internal constant of coder run UI
49119
+ */
49120
+ const AUTO_REFRESH_PHASES = ['initializing', 'loading', 'running', 'verifying'];
49121
+ /**
49122
+ * Returns whether the rich coder UI should keep animating on its own.
49123
+ *
49124
+ * @private internal utility of coder run UI
49125
+ */
49126
+ function isCoderRunUiAutoRefreshing(phase, pauseState) {
49127
+ // `PAUSING` still means the current task is winding down, so keep active
49128
+ // animations/timers running until the runner reaches the fully paused state.
49129
+ if (pauseState === 'PAUSED') {
49130
+ return false;
49131
+ }
49132
+ return AUTO_REFRESH_PHASES.includes(phase);
49133
+ }
49134
+ /**
49135
+ * Returns the automatic refresh interval for the current UI state.
49136
+ *
49137
+ * Waiting, paused, and completed states return `undefined` so the rich UI stays
49138
+ * perfectly still until actual state changes arrive.
49139
+ *
49140
+ * @private internal utility of coder run UI
49141
+ */
49142
+ function getCoderRunUiAutoRefreshInterval(phase, pauseState) {
49143
+ return isCoderRunUiAutoRefreshing(phase, pauseState) ? ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS : undefined;
49144
+ }
49145
+
48697
49146
  /**
48698
49147
  * Maximum number of output lines reserved for agent output in the UI.
48699
49148
  */
48700
49149
  const MAX_VISIBLE_OUTPUT_LINES = 8;
49150
+ /**
49151
+ * Minimum width used for the rich coder-run frame.
49152
+ */
49153
+ const MIN_FRAME_WIDTH = 56;
49154
+ /**
49155
+ * Maximum width used for the rich coder-run frame.
49156
+ */
49157
+ const MAX_FRAME_WIDTH = 96;
49158
+ /**
49159
+ * Visible width reserved for aligned labels in the session box.
49160
+ */
49161
+ const SESSION_LABEL_WIDTH = 8;
48701
49162
  /**
48702
49163
  * Builds the complete boxed terminal frame for the rich `ptbk coder run` UI.
48703
49164
  */
48704
49165
  function buildCoderRunUiFrame(options) {
48705
- const totalWidth = Math.max(56, Math.min(options.terminalWidth, 96));
49166
+ const totalWidth = Math.max(MIN_FRAME_WIDTH, Math.min(options.terminalWidth, MAX_FRAME_WIDTH));
48706
49167
  const isPromptActive = options.phase === 'running' || options.phase === 'verifying' || options.phase === 'loading';
48707
49168
  const promptStatusPrefix = isPromptActive ? `${colors__default["default"].yellow(`${options.spinner} `)}` : '';
48708
- const sessionScopeLine = options.progress.sessionTotal > 0
48709
- ? `Working on ${options.progress.currentPromptIndex}/${options.progress.sessionTotal} prompts with Priority ≥${options.config.priority}`
48710
- : `No runnable prompts with Priority ≥${options.config.priority}`;
48711
- const sessionCountLine = `Done ${options.progress.sessionDone}/${options.progress.sessionTotal} this run · Repo total ${options.progress.totalPrompts}`;
48712
- const sessionQueueParts = [];
48713
- if (options.progress.skippedPrompts > 0) {
48714
- sessionQueueParts.push(`Skipping ${formatPromptCount(options.progress.skippedPrompts)} with Priority <${options.config.priority}`);
48715
- }
48716
- if (options.progress.toBeWrittenPrompts > 0) {
48717
- sessionQueueParts.push(`Write first ${formatPromptCount(options.progress.toBeWrittenPrompts)}`);
48718
- }
48719
- const sessionLines = [
48720
- `${buildPhaseBadge(options.phase, options.pauseState)} ${fitPlainText(options.statusMessage, totalWidth - 18)}`,
48721
- sessionScopeLine,
48722
- sessionCountLine,
48723
- ...(sessionQueueParts.length > 0 ? [sessionQueueParts.join(' · ')] : []),
48724
- `Elapsed ${options.progress.elapsedText} · Est. total ${options.progress.estimatedTotalText} · Est. done ${options.progress.estimatedLabel}`,
48725
- buildProgressBar(options.progress.percentage, totalWidth - 6, `${options.progress.percentage}% complete (${options.progress.sessionDone}/${options.progress.sessionTotal} done)`),
48726
- ];
48727
- const metadataParts = [options.config.agentName || 'No agent selected'];
48728
- if (options.config.modelName) {
48729
- metadataParts.push(options.config.modelName);
48730
- }
48731
- if (options.config.thinkingLevel) {
48732
- metadataParts.push(`thinking ${options.config.thinkingLevel}`);
48733
- }
48734
- const runnerDetails = [
48735
- [`${colors__default["default"].bgCyan.black(' PTBK ')}`, colors__default["default"].bgBlue.white(' CODER '), colors__default["default"].bold.white(' Promptbook Coder')]
48736
- .join(''),
48737
- metadataParts.join(' · '),
48738
- buildConfigSummaryLine(options.config),
48739
- ];
49169
+ const octopusAnimationFrame = isCoderRunUiAutoRefreshing(options.phase, options.pauseState)
49170
+ ? options.animationFrame
49171
+ : 0;
49172
+ const pausePresentation = buildPausePresentation(options.phase, options.pauseState, options.statusMessage);
49173
+ const sessionLines = buildSessionLines(options, totalWidth, pausePresentation);
48740
49174
  const currentTaskLines = options.currentPromptLabel
48741
49175
  ? [
48742
49176
  `${promptStatusPrefix}${colors__default["default"].bold.white(fitPlainText(options.currentPromptLabel, totalWidth - 8))}`,
@@ -48744,12 +49178,11 @@ bash "$1"
48744
49178
  ...options.detailLines.map((detailLine) => `• ${detailLine}`),
48745
49179
  ]
48746
49180
  : [options.statusMessage, ...options.detailLines.map((detailLine) => `• ${detailLine}`)];
48747
- const visibleOutputLines = options.agentOutputLines.length > 0
48748
- ? options.agentOutputLines.slice(-MAX_VISIBLE_OUTPUT_LINES).map((line) => `› ${stripAnsi(line)}`)
48749
- : ['No live agent output yet.'];
48750
- const controls = buildControlPills(options.pauseState, options.pendingEnterLabel).join(' ');
49181
+ const visibleOutputLines = buildVisibleOutputLines(options.agentOutputLines);
49182
+ const controls = buildControlPills(pausePresentation.pauseControl, options.pendingEnterLabel).join(' ');
48751
49183
  const frame = [
48752
- ...renderBox('Brand', runnerDetails, totalWidth, colors__default["default"].cyan.bold),
49184
+ ...buildCoderRunOctopusVisual({ totalWidth, animationFrame: octopusAnimationFrame }),
49185
+ '',
48753
49186
  ...renderBox('Session', sessionLines, totalWidth, colors__default["default"].yellow.bold),
48754
49187
  ...renderBox(options.currentPromptLabel ? 'Current task' : 'Queue', currentTaskLines, totalWidth, colors__default["default"].magenta.bold),
48755
49188
  ...renderBox('Live output', visibleOutputLines, totalWidth, colors__default["default"].green.bold),
@@ -48760,6 +49193,72 @@ bash "$1"
48760
49193
  frame.push(...renderBox('Controls', [controls], totalWidth, colors__default["default"].white.bold));
48761
49194
  return frame;
48762
49195
  }
49196
+ /**
49197
+ * Builds the structured session lines that combine state, runner, queue, and timing metadata.
49198
+ */
49199
+ function buildSessionLines(options, totalWidth, pausePresentation) {
49200
+ const bodyWidth = Math.max(10, totalWidth - 4);
49201
+ return buildSessionRows(options, bodyWidth, pausePresentation).map((sessionRow) => buildLabeledSessionLine(sessionRow.label, sessionRow.value, bodyWidth));
49202
+ }
49203
+ /**
49204
+ * Builds the session rows so the renderer can keep one consistent structure without duplicating labels.
49205
+ */
49206
+ function buildSessionRows(options, bodyWidth, pausePresentation) {
49207
+ const runnerParts = [options.config.agentName || 'No agent selected'];
49208
+ if (options.config.modelName) {
49209
+ runnerParts.push(options.config.modelName);
49210
+ }
49211
+ if (options.config.thinkingLevel) {
49212
+ runnerParts.push(`thinking ${options.config.thinkingLevel}`);
49213
+ }
49214
+ const configurationRows = [
49215
+ ...buildOptionalSessionRow('Context', options.config.context),
49216
+ ...buildOptionalSessionRow('Test', options.config.testCommand),
49217
+ ];
49218
+ return [
49219
+ {
49220
+ label: 'State',
49221
+ value: `${pausePresentation.badge} ${pausePresentation.stateMessage}`,
49222
+ },
49223
+ {
49224
+ label: 'Runner',
49225
+ value: runnerParts.join(' · '),
49226
+ },
49227
+ ...configurationRows,
49228
+ {
49229
+ label: 'This run',
49230
+ value: buildThisRunSummary(options.progress),
49231
+ },
49232
+ {
49233
+ label: 'Backlog',
49234
+ value: buildBacklogSummary(options.progress),
49235
+ },
49236
+ {
49237
+ label: 'Scope',
49238
+ value: buildScopeSummary(options.progress, options.config),
49239
+ },
49240
+ {
49241
+ label: 'Timing',
49242
+ value: buildTimingSummary(options.progress),
49243
+ },
49244
+ {
49245
+ label: 'Progress',
49246
+ value: buildProgressBar(options.progress.percentage, bodyWidth - SESSION_LABEL_WIDTH - 1, `${options.progress.percentage}% complete (${options.progress.sessionDone}/${options.progress.sessionTotal} done)`),
49247
+ },
49248
+ ];
49249
+ }
49250
+ /**
49251
+ * Builds the fixed-height live output section so streaming updates do not keep resizing the frame.
49252
+ */
49253
+ function buildVisibleOutputLines(agentOutputLines) {
49254
+ const visibleOutputLines = agentOutputLines.length > 0
49255
+ ? agentOutputLines.slice(-MAX_VISIBLE_OUTPUT_LINES).map((line) => `› ${stripAnsi(line)}`)
49256
+ : [colors__default["default"].gray('No live agent output yet.')];
49257
+ while (visibleOutputLines.length < MAX_VISIBLE_OUTPUT_LINES) {
49258
+ visibleOutputLines.push('');
49259
+ }
49260
+ return visibleOutputLines;
49261
+ }
48763
49262
  /**
48764
49263
  * Renders a framed box with a colored title and padded body lines.
48765
49264
  */
@@ -48777,25 +49276,84 @@ bash "$1"
48777
49276
  return [topBorder, ...body, bottomBorder];
48778
49277
  }
48779
49278
  /**
48780
- * Builds the compact config summary line shown in the branding box.
49279
+ * Builds one aligned labeled line inside the session box.
48781
49280
  */
48782
- function buildConfigSummaryLine(config) {
48783
- const parts = [`Priority ≥${config.priority}`];
48784
- if (config.context) {
48785
- parts.unshift(`Context ${config.context}`);
49281
+ function buildLabeledSessionLine(label, value, bodyWidth) {
49282
+ const formattedLabel = colors__default["default"].gray(label.padEnd(SESSION_LABEL_WIDTH));
49283
+ return `${formattedLabel} ${fitAnsiText(value, bodyWidth - SESSION_LABEL_WIDTH - 1)}`;
49284
+ }
49285
+ /**
49286
+ * Builds zero or one structured session row for optional metadata.
49287
+ */
49288
+ function buildOptionalSessionRow(label, value) {
49289
+ if (!value) {
49290
+ return [];
48786
49291
  }
48787
- if (config.testCommand) {
48788
- parts.push(`Test ${config.testCommand}`);
49292
+ return [{ label, value }];
49293
+ }
49294
+ /**
49295
+ * Builds the active-session summary shown in the session box.
49296
+ */
49297
+ function buildThisRunSummary(progress) {
49298
+ if (progress.sessionTotal === 0) {
49299
+ return 'No runnable prompts in current scope';
49300
+ }
49301
+ return `Task ${progress.currentPromptIndex}/${progress.sessionTotal} · ${progress.sessionDone} done · ${progress.sessionRemaining} left`;
49302
+ }
49303
+ /**
49304
+ * Builds the backlog/filter summary shown in the session box.
49305
+ */
49306
+ function buildBacklogSummary(progress) {
49307
+ const parts = [`Repo ${progress.totalPrompts} total`];
49308
+ if (progress.skippedPrompts > 0) {
49309
+ parts.push(`${formatPromptCount(progress.skippedPrompts)} below priority`);
48789
49310
  }
48790
49311
  return parts.join(' · ');
48791
49312
  }
49313
+ /**
49314
+ * Builds the priority/write-order summary shown in the session box.
49315
+ */
49316
+ function buildScopeSummary(progress, config) {
49317
+ const parts = [`Priority ≥${config.priority}`];
49318
+ if (progress.toBeWrittenPrompts > 0) {
49319
+ parts.push(`Write ${formatPromptCount(progress.toBeWrittenPrompts)} first`);
49320
+ }
49321
+ return parts.join(' · ');
49322
+ }
49323
+ /**
49324
+ * Builds the elapsed/estimate summary shown in the session box.
49325
+ */
49326
+ function buildTimingSummary(progress) {
49327
+ return `Elapsed ${progress.elapsedText} · Total ${progress.estimatedTotalText} · ETA ${progress.estimatedLabel}`;
49328
+ }
48792
49329
  /**
48793
49330
  * Builds the colored phase badge shown in the session box.
48794
49331
  */
48795
- function buildPhaseBadge(phase, pauseState) {
48796
- if (pauseState !== 'RUNNING' || phase === 'paused') {
48797
- return colors__default["default"].bgYellow.black(' PAUSED ');
49332
+ function buildPausePresentation(phase, pauseState, statusMessage) {
49333
+ if (pauseState === 'PAUSING') {
49334
+ return {
49335
+ badge: colors__default["default"].bgYellow.black(' PAUSING '),
49336
+ stateMessage: 'Pausing before the next task',
49337
+ pauseControl: colors__default["default"].bgMagenta.white(' P ') + colors__default["default"].white(' Cancel pause'),
49338
+ };
49339
+ }
49340
+ if (pauseState === 'PAUSED') {
49341
+ return {
49342
+ badge: colors__default["default"].bgWhite.black(' PAUSED '),
49343
+ stateMessage: 'Paused until resumed',
49344
+ pauseControl: colors__default["default"].bgGreen.black(' P ') + colors__default["default"].white(' Resume'),
49345
+ };
48798
49346
  }
49347
+ return {
49348
+ badge: buildRunningPhaseBadge(phase),
49349
+ stateMessage: statusMessage,
49350
+ pauseControl: colors__default["default"].bgYellow.black(' P ') + colors__default["default"].white(' Pause'),
49351
+ };
49352
+ }
49353
+ /**
49354
+ * Builds the active phase badge shown in the session box while the runner is not paused.
49355
+ */
49356
+ function buildRunningPhaseBadge(phase) {
48799
49357
  switch (phase) {
48800
49358
  case 'loading':
48801
49359
  case 'initializing':
@@ -48810,6 +49368,8 @@ bash "$1"
48810
49368
  return colors__default["default"].bgGreen.black(' DONE ');
48811
49369
  case 'error':
48812
49370
  return colors__default["default"].bgRed.white(' ERROR ');
49371
+ case 'paused':
49372
+ return colors__default["default"].bgWhite.black(' READY ');
48813
49373
  default:
48814
49374
  return colors__default["default"].bgWhite.black(' READY ');
48815
49375
  }
@@ -48822,7 +49382,7 @@ bash "$1"
48822
49382
  const barWidth = Math.max(10, availableWidth - percentageLabel.length - 1);
48823
49383
  const filledWidth = Math.round((percentage / 100) * barWidth);
48824
49384
  const emptyWidth = Math.max(0, barWidth - filledWidth);
48825
- return `${colors__default["default"].green('█'.repeat(filledWidth))}${colors__default["default"].gray('░'.repeat(emptyWidth))} ${percentageLabel}`;
49385
+ return `${colors__default["default"].green('█'.repeat(filledWidth))}${colors__default["default"].blue('░'.repeat(emptyWidth))} ${percentageLabel}`;
48826
49386
  }
48827
49387
  /**
48828
49388
  * Formats a prompt count with singular/plural wording.
@@ -48833,58 +49393,15 @@ bash "$1"
48833
49393
  /**
48834
49394
  * Builds the control pills shown in the footer box.
48835
49395
  */
48836
- function buildControlPills(pauseState, pendingEnterLabel) {
49396
+ function buildControlPills(pauseControl, pendingEnterLabel) {
48837
49397
  const pills = [];
48838
49398
  if (pendingEnterLabel) {
48839
49399
  pills.push(colors__default["default"].bgWhite.black(' ENTER ') + colors__default["default"].white(` ${pendingEnterLabel}`));
48840
49400
  }
48841
- pills.push(pauseState === 'RUNNING'
48842
- ? colors__default["default"].bgYellow.black(' P ') + colors__default["default"].white(' Pause')
48843
- : colors__default["default"].bgYellow.black(' P ') + colors__default["default"].white(' Resume'));
49401
+ pills.push(pauseControl);
48844
49402
  pills.push(colors__default["default"].bgRed.white(' CTRL+C ') + colors__default["default"].white(' Exit'));
48845
49403
  return pills;
48846
49404
  }
48847
- /**
48848
- * Pads or truncates a possibly ANSI-colored line to the target visible width.
48849
- */
48850
- function padAnsiText(text, width) {
48851
- const fittedText = fitAnsiText(text, width);
48852
- return fittedText + ' '.repeat(Math.max(0, width - visibleLength(fittedText)));
48853
- }
48854
- /**
48855
- * Truncates a possibly ANSI-colored line to the target visible width.
48856
- */
48857
- function fitAnsiText(text, width) {
48858
- if (visibleLength(text) <= width) {
48859
- return text;
48860
- }
48861
- return fitPlainText(stripAnsi(text), width);
48862
- }
48863
- /**
48864
- * Truncates a plain-text line to the target width with an ellipsis.
48865
- */
48866
- function fitPlainText(text, width) {
48867
- if (text.length <= width) {
48868
- return text;
48869
- }
48870
- if (width <= 3) {
48871
- return '.'.repeat(width);
48872
- }
48873
- return `${text.slice(0, width - 3)}...`;
48874
- }
48875
- /**
48876
- * Measures visible string width by stripping ANSI escape codes.
48877
- */
48878
- function visibleLength(text) {
48879
- return stripAnsi(text).length;
48880
- }
48881
- /**
48882
- * Strips ANSI escape codes from a string.
48883
- */
48884
- function stripAnsi(text) {
48885
- // eslint-disable-next-line no-control-regex
48886
- return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
48887
- }
48888
49405
 
48889
49406
  /**
48890
49407
  * Maximum number of agent output lines kept in the scrolling output area.
@@ -49025,12 +49542,6 @@ bash "$1"
49025
49542
  }
49026
49543
  }
49027
49544
 
49028
- /**
49029
- * Refresh interval for the terminal UI in milliseconds.
49030
- *
49031
- * @private internal constant of coder run UI
49032
- */
49033
- const UI_REFRESH_INTERVAL_MS = 200;
49034
49545
  /**
49035
49546
  * Spinner animation frames.
49036
49547
  *
@@ -49059,9 +49570,10 @@ bash "$1"
49059
49570
  /**
49060
49571
  * Boots the ANSI terminal UI for `ptbk coder run`.
49061
49572
  *
49062
- * The UI reserves a fixed number of terminal lines and repaints them periodically.
49063
- * Between repaints, any console output from runners is captured and fed into the
49064
- * scrolling agent-output area.
49573
+ * The UI reserves a fixed number of terminal lines and refreshes them incrementally.
49574
+ * While a prompt is actively running, it schedules lightweight timed refreshes for
49575
+ * the spinner/progress area; otherwise it redraws only when real state changes arrive.
49576
+ * Any console output from runners is captured and fed into the scrolling agent-output area.
49065
49577
  *
49066
49578
  * On non-interactive (non-TTY) terminals the UI is skipped entirely and
49067
49579
  * only the state object is provided.
@@ -49111,72 +49623,140 @@ bash "$1"
49111
49623
  process.stdin.setRawMode(true);
49112
49624
  }
49113
49625
  let spinnerFrame = 0;
49114
- let previousFrameLineCount = 0;
49626
+ let previousFrameLines = [];
49115
49627
  let isRendering = false;
49116
49628
  let renderScheduled = false;
49629
+ let autoRefreshTimeout;
49630
+ let isDisposed = false;
49117
49631
  /**
49118
49632
  * Schedules a render on the next tick if one isn't already pending.
49119
49633
  * Prevents overlapping renders that cause cursor desync.
49120
49634
  */
49121
49635
  function scheduleRender() {
49122
- if (renderScheduled) {
49636
+ if (renderScheduled || isDisposed) {
49123
49637
  return;
49124
49638
  }
49125
49639
  renderScheduled = true;
49126
49640
  setImmediate(() => {
49127
49641
  renderScheduled = false;
49642
+ if (isDisposed) {
49643
+ return;
49644
+ }
49128
49645
  render();
49129
49646
  });
49130
49647
  }
49131
49648
  /**
49132
- * Clears previously rendered lines and writes a new frame.
49649
+ * Re-schedules automatic animation refreshes only while the frame can change by itself.
49650
+ */
49651
+ function scheduleAutoRefresh() {
49652
+ if (autoRefreshTimeout) {
49653
+ clearTimeout(autoRefreshTimeout);
49654
+ autoRefreshTimeout = undefined;
49655
+ }
49656
+ const autoRefreshInterval = getCoderRunUiAutoRefreshInterval(state.phase, getPauseState());
49657
+ if (autoRefreshInterval === undefined) {
49658
+ return;
49659
+ }
49660
+ autoRefreshTimeout = setTimeout(() => {
49661
+ autoRefreshTimeout = undefined;
49662
+ scheduleRender();
49663
+ }, autoRefreshInterval);
49664
+ }
49665
+ /**
49666
+ * Moves the cursor relative to the bottom of the current frame and rewrites one line in place.
49667
+ */
49668
+ function rewriteFrameLine(frameLineCount, lineIndex, line) {
49669
+ const linesUpFromBottom = Math.max(0, frameLineCount - 1 - lineIndex);
49670
+ if (linesUpFromBottom > 0) {
49671
+ process.stdout.write(`\x1b[${linesUpFromBottom}A`);
49672
+ }
49673
+ readline.clearLine(process.stdout, 0);
49674
+ readline.cursorTo(process.stdout, 0);
49675
+ process.stdout.write(line);
49676
+ readline.cursorTo(process.stdout, 0);
49677
+ if (linesUpFromBottom > 0) {
49678
+ process.stdout.write(`\x1b[${linesUpFromBottom}B`);
49679
+ readline.cursorTo(process.stdout, 0);
49680
+ }
49681
+ }
49682
+ /**
49683
+ * Fully rewrites the reserved frame area.
49684
+ */
49685
+ function renderFullFrame(lines) {
49686
+ var _a;
49687
+ const previousFrameLineCount = previousFrameLines.length;
49688
+ const linesToRewriteCount = Math.max(previousFrameLineCount, lines.length);
49689
+ if (previousFrameLineCount > 1) {
49690
+ process.stdout.write(`\x1b[${previousFrameLineCount - 1}A`);
49691
+ }
49692
+ for (let i = 0; i < linesToRewriteCount; i++) {
49693
+ readline.clearLine(process.stdout, 0);
49694
+ readline.cursorTo(process.stdout, 0);
49695
+ process.stdout.write((_a = lines[i]) !== null && _a !== void 0 ? _a : '');
49696
+ if (i < linesToRewriteCount - 1) {
49697
+ process.stdout.write('\n');
49698
+ }
49699
+ }
49700
+ const clearedTrailingLines = linesToRewriteCount - lines.length;
49701
+ if (clearedTrailingLines > 0) {
49702
+ process.stdout.write(`\x1b[${clearedTrailingLines}A`);
49703
+ }
49704
+ readline.cursorTo(process.stdout, 0);
49705
+ }
49706
+ /**
49707
+ * Updates only the frame rows whose visible content changed.
49708
+ */
49709
+ function renderChangedLines(lines) {
49710
+ for (let i = 0; i < lines.length; i++) {
49711
+ if (previousFrameLines[i] === lines[i]) {
49712
+ continue;
49713
+ }
49714
+ rewriteFrameLine(lines.length, i, lines[i]);
49715
+ }
49716
+ }
49717
+ /**
49718
+ * Builds the current frame snapshot from the latest state.
49719
+ */
49720
+ function buildFrameLines() {
49721
+ return buildCoderRunUiFrame({
49722
+ terminalWidth: getTerminalWidth(),
49723
+ animationFrame: spinnerFrame,
49724
+ spinner: SPINNER_FRAMES[spinnerFrame],
49725
+ pauseState: getPauseState(),
49726
+ config: state.config,
49727
+ phase: state.phase,
49728
+ currentPromptLabel: state.currentPromptLabel,
49729
+ currentAttempt: state.currentAttempt,
49730
+ maxAttempts: state.maxAttempts,
49731
+ statusMessage: state.statusMessage,
49732
+ detailLines: state.detailLines,
49733
+ pendingEnterLabel: state.pendingEnterLabel,
49734
+ agentOutputLines: state.agentOutputLines,
49735
+ errors: state.errors,
49736
+ progress: state.getProgress(),
49737
+ });
49738
+ }
49739
+ /**
49740
+ * Clears previously rendered lines and writes a new frame only where needed.
49133
49741
  */
49134
- function render() {
49135
- if (isRendering) {
49742
+ function render(options) {
49743
+ if (isRendering || isDisposed) {
49136
49744
  return;
49137
49745
  }
49138
49746
  isRendering = true;
49139
49747
  try {
49140
- const lines = buildCoderRunUiFrame({
49141
- terminalWidth: getTerminalWidth(),
49142
- spinner: SPINNER_FRAMES[spinnerFrame],
49143
- pauseState: getPauseState(),
49144
- config: state.config,
49145
- phase: state.phase,
49146
- currentPromptLabel: state.currentPromptLabel,
49147
- currentAttempt: state.currentAttempt,
49148
- maxAttempts: state.maxAttempts,
49149
- statusMessage: state.statusMessage,
49150
- detailLines: state.detailLines,
49151
- pendingEnterLabel: state.pendingEnterLabel,
49152
- agentOutputLines: state.agentOutputLines,
49153
- errors: state.errors,
49154
- progress: state.getProgress(),
49155
- });
49156
- if (previousFrameLineCount > 0) {
49157
- process.stdout.write(`\x1b[${previousFrameLineCount}A`);
49158
- }
49159
- for (let i = 0; i < lines.length; i++) {
49160
- readline.clearLine(process.stdout, 0);
49161
- readline.cursorTo(process.stdout, 0);
49162
- process.stdout.write(lines[i]);
49163
- if (i < lines.length - 1) {
49164
- process.stdout.write('\n');
49165
- }
49748
+ const lines = buildFrameLines();
49749
+ if (previousFrameLines.length === 0 || previousFrameLines.length !== lines.length) {
49750
+ renderFullFrame(lines);
49166
49751
  }
49167
- if (lines.length < previousFrameLineCount) {
49168
- for (let i = lines.length; i < previousFrameLineCount; i++) {
49169
- process.stdout.write('\n');
49170
- readline.clearLine(process.stdout, 0);
49171
- readline.cursorTo(process.stdout, 0);
49172
- }
49173
- const overshoot = previousFrameLineCount - lines.length;
49174
- if (overshoot > 0) {
49175
- process.stdout.write(`\x1b[${overshoot}A`);
49176
- }
49752
+ else {
49753
+ renderChangedLines(lines);
49177
49754
  }
49178
- previousFrameLineCount = lines.length;
49755
+ previousFrameLines = [...lines];
49179
49756
  spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
49757
+ if (!(options === null || options === void 0 ? void 0 : options.skipAutoRefresh)) {
49758
+ scheduleAutoRefresh();
49759
+ }
49180
49760
  }
49181
49761
  finally {
49182
49762
  isRendering = false;
@@ -49188,12 +49768,8 @@ bash "$1"
49188
49768
  process.exit(0);
49189
49769
  }
49190
49770
  if (key.name === 'p') {
49191
- if (getPauseState() === 'RUNNING') {
49192
- requestPause();
49193
- }
49194
- else {
49195
- requestResume();
49196
- }
49771
+ togglePauseState();
49772
+ scheduleRender();
49197
49773
  return;
49198
49774
  }
49199
49775
  if ((key.name === 'return' || key.name === 'enter') && pendingEnterResolver) {
@@ -49204,17 +49780,24 @@ bash "$1"
49204
49780
  }
49205
49781
  };
49206
49782
  process.stdin.on('keypress', keypressHandler);
49783
+ process.stdout.on('resize', scheduleRender);
49207
49784
  process.stdout.write('\n');
49208
49785
  render();
49209
- const interval = setInterval(scheduleRender, UI_REFRESH_INTERVAL_MS);
49210
49786
  state.on('change', scheduleRender);
49211
49787
  /**
49212
49788
  * Tears down the terminal UI and restores console / stdin state.
49213
49789
  */
49214
49790
  function cleanup() {
49215
- clearInterval(interval);
49791
+ if (isDisposed) {
49792
+ return;
49793
+ }
49794
+ if (autoRefreshTimeout) {
49795
+ clearTimeout(autoRefreshTimeout);
49796
+ autoRefreshTimeout = undefined;
49797
+ }
49216
49798
  state.off('change', scheduleRender);
49217
49799
  process.stdin.off('keypress', keypressHandler);
49800
+ process.stdout.off('resize', scheduleRender);
49218
49801
  if (process.stdin.isTTY) {
49219
49802
  process.stdin.setRawMode(false);
49220
49803
  }
@@ -49226,8 +49809,9 @@ bash "$1"
49226
49809
  console.warn = originalConsoleWarn;
49227
49810
  console.error = originalConsoleError;
49228
49811
  console.log = originalConsoleLog;
49229
- render();
49812
+ render({ skipAutoRefresh: true });
49230
49813
  process.stdout.write('\n');
49814
+ isDisposed = true;
49231
49815
  }
49232
49816
  return {
49233
49817
  state,
@@ -49320,6 +49904,23 @@ bash "$1"
49320
49904
  const isRichUiEnabled = !options.dryRun && !options.noUi && Boolean(process.stdout.isTTY);
49321
49905
  const progressDisplay = options.dryRun || options.noUi || isRichUiEnabled ? undefined : new CliProgressDisplay(runStartDate, options.priority);
49322
49906
  const uiHandle = isRichUiEnabled ? renderCoderRunUi(runStartDate) : undefined;
49907
+ const waitForRequestedPause = async () => {
49908
+ await checkPause({
49909
+ silent: isRichUiEnabled,
49910
+ onPaused: () => {
49911
+ progressDisplay === null || progressDisplay === void 0 ? void 0 : progressDisplay.pauseTimer();
49912
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.pauseTimer();
49913
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setPhase('paused');
49914
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage('Paused');
49915
+ },
49916
+ onResumed: () => {
49917
+ progressDisplay === null || progressDisplay === void 0 ? void 0 : progressDisplay.resumeTimer();
49918
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.resumeTimer();
49919
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setPhase('loading');
49920
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage('Resuming...');
49921
+ },
49922
+ });
49923
+ };
49323
49924
  // When the Ink UI is active it handles keyboard input itself, so skip the raw stdin listener.
49324
49925
  if (!isRichUiEnabled) {
49325
49926
  listenForPause();
@@ -49436,19 +50037,7 @@ bash "$1"
49436
50037
  let hasShownUpcomingTasks = false;
49437
50038
  let hasWaitedForStart = false;
49438
50039
  while (just(true)) {
49439
- await checkPause({
49440
- silent: isRichUiEnabled,
49441
- onPaused: () => {
49442
- progressDisplay === null || progressDisplay === void 0 ? void 0 : progressDisplay.pauseTimer();
49443
- uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.pauseTimer();
49444
- uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setPhase('paused');
49445
- uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage('Paused');
49446
- },
49447
- onResumed: () => {
49448
- progressDisplay === null || progressDisplay === void 0 ? void 0 : progressDisplay.resumeTimer();
49449
- uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.resumeTimer();
49450
- },
49451
- });
50040
+ await waitForRequestedPause();
49452
50041
  if (isRichUiEnabled) {
49453
50042
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setPhase('loading');
49454
50043
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage('Loading prompts...');
@@ -49516,6 +50105,7 @@ bash "$1"
49516
50105
  const commitMessage = buildCommitMessage(nextPrompt.file, nextPrompt.section);
49517
50106
  const codexPrompt = appendCoderContext(buildCodexPrompt(nextPrompt.file, nextPrompt.section), resolvedCoderContext);
49518
50107
  const scriptPath = buildScriptPath(nextPrompt.file, nextPrompt.section);
50108
+ await waitForRequestedPause();
49519
50109
  if (isRichUiEnabled) {
49520
50110
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setCurrentPrompt(promptLabel);
49521
50111
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setPhase('running');
@@ -49574,7 +50164,11 @@ bash "$1"
49574
50164
  progressDisplay === null || progressDisplay === void 0 ? void 0 : progressDisplay.resumeTimer();
49575
50165
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.resumeTimer();
49576
50166
  }
49577
- await commitChanges(commitMessage, { autoPush: options.autoPush });
50167
+ await commitChanges(commitMessage, {
50168
+ autoPush: options.autoPush,
50169
+ // Keep the live runtime log out of default commits because it is deleted after a successful round.
50170
+ excludePaths: options.preserveLogs ? undefined : [logPath],
50171
+ });
49578
50172
  await runPostPromptAutoMigrationIfEnabled(options);
49579
50173
  }
49580
50174
  catch (error) {
@@ -53576,9 +54170,10 @@ bash "$1"
53576
54170
  * Build a minimal agent source snapshot for remote agents.
53577
54171
  */
53578
54172
  function buildRemoteAgentSource(profile, meta) {
54173
+ const isMetaImageExplicit = profile.isMetaImageExplicit !== false;
53579
54174
  const metaLines = [
53580
54175
  formatMetaLine('FULLNAME', meta === null || meta === void 0 ? void 0 : meta.fullname),
53581
- formatMetaLine('IMAGE', meta === null || meta === void 0 ? void 0 : meta.image),
54176
+ formatMetaLine('IMAGE', isMetaImageExplicit ? meta === null || meta === void 0 ? void 0 : meta.image : undefined),
53582
54177
  formatMetaLine('DESCRIPTION', meta === null || meta === void 0 ? void 0 : meta.description),
53583
54178
  formatMetaLine('COLOR', meta === null || meta === void 0 ? void 0 : meta.color),
53584
54179
  formatMetaLine('FONT', meta === null || meta === void 0 ? void 0 : meta.font),
@@ -53677,6 +54272,8 @@ bash "$1"
53677
54272
  remoteAgent._isVoiceCallingEnabled = profile.isVoiceCallingEnabled === true; // [✨✷] Store voice calling status
53678
54273
  remoteAgent._isVoiceTtsSttEnabled = profile.isVoiceTtsSttEnabled !== false;
53679
54274
  remoteAgent.knowledgeSources = profile.knowledgeSources || [];
54275
+ remoteAgent.isMetaImageExplicit = profile.isMetaImageExplicit !== false;
54276
+ remoteAgent.avatarVisualId = profile.avatarVisualId;
53680
54277
  return remoteAgent;
53681
54278
  }
53682
54279
  /**
@@ -53692,6 +54289,8 @@ bash "$1"
53692
54289
  this.toolTitles = {};
53693
54290
  this._isVoiceCallingEnabled = false; // [✨✷] Track voice calling status
53694
54291
  this._isVoiceTtsSttEnabled = true;
54292
+ this.isMetaImageExplicit = true;
54293
+ this.avatarVisualId = undefined;
53695
54294
  this.knowledgeSources = [];
53696
54295
  this.agentUrl = options.agentUrl;
53697
54296
  }