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