@promptbook/cli 0.112.0-65 → 0.112.0-66

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 (35) hide show
  1. package/esm/index.es.js +1233 -298
  2. package/esm/index.es.js.map +1 -1
  3. package/esm/scripts/run-agent-messages/ui/buildAgentRunInitialsVisual.d.ts +4 -0
  4. package/esm/scripts/run-agent-messages/ui/buildAgentRunUiFrame.d.ts +5 -0
  5. package/esm/scripts/run-agent-messages/ui/loadAgentRunUiMetadata.d.ts +16 -0
  6. package/esm/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +6 -0
  7. package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
  8. package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +49 -0
  9. package/esm/scripts/run-codex-prompts/ui/renderCoderRunUi.d.ts +4 -1
  10. package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  11. package/esm/src/avatars/visuals/minecraft2AvatarVisual.d.ts +7 -0
  12. package/esm/src/avatars/visuals/minecraftAvatarVisualShared.d.ts +48 -0
  13. package/esm/src/cli/cli-commands/coder/ensureCoderGitignoreFile.d.ts +1 -1
  14. package/esm/src/config.d.ts +3 -3
  15. package/esm/src/utils/files/getPromptbookTempPath.d.ts +24 -0
  16. package/esm/src/utils/files/getPromptbookTempPath.test.d.ts +1 -0
  17. package/esm/src/version.d.ts +1 -1
  18. package/package.json +1 -1
  19. package/umd/index.umd.js +1232 -297
  20. package/umd/index.umd.js.map +1 -1
  21. package/umd/scripts/run-agent-messages/ui/buildAgentRunInitialsVisual.d.ts +4 -0
  22. package/umd/scripts/run-agent-messages/ui/buildAgentRunUiFrame.d.ts +5 -0
  23. package/umd/scripts/run-agent-messages/ui/loadAgentRunUiMetadata.d.ts +16 -0
  24. package/umd/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +6 -0
  25. package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
  26. package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +49 -0
  27. package/umd/scripts/run-codex-prompts/ui/renderCoderRunUi.d.ts +4 -1
  28. package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  29. package/umd/src/avatars/visuals/minecraft2AvatarVisual.d.ts +7 -0
  30. package/umd/src/avatars/visuals/minecraftAvatarVisualShared.d.ts +48 -0
  31. package/umd/src/cli/cli-commands/coder/ensureCoderGitignoreFile.d.ts +1 -1
  32. package/umd/src/config.d.ts +3 -3
  33. package/umd/src/utils/files/getPromptbookTempPath.d.ts +24 -0
  34. package/umd/src/utils/files/getPromptbookTempPath.test.d.ts +1 -0
  35. 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 { writeFile, stat, mkdir, readFile, access, constants, readdir, watch, unlink, rm, rename, rmdir, appendFile } from 'fs/promises';
5
- import { join, basename, dirname, isAbsolute, relative, resolve, extname } from 'path';
5
+ import { posix, join, basename, dirname, isAbsolute, relative, resolve, extname } from 'path';
6
6
  import * as fs from 'fs';
7
7
  import { mkdirSync, writeFileSync, statSync, readFileSync, existsSync } from 'fs';
8
8
  import { forTime, forEver } from 'waitasecond';
@@ -57,7 +57,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
57
57
  * @generated
58
58
  * @see https://github.com/webgptorg/promptbook
59
59
  */
60
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-65';
60
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-66';
61
61
  /**
62
62
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
63
63
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -990,6 +990,45 @@ function saturate(amount) {
990
990
  }
991
991
  // TODO: Maybe implement by mix+hsl
992
992
 
993
+ /**
994
+ * Relative directory name without the `./` prefix for Git ignore rules and glob patterns.
995
+ *
996
+ * @private internal utility for Promptbook-owned temp files
997
+ */
998
+ const PROMPTBOOK_TEMP_DIRECTORY_NAME = '.promptbook';
999
+ /**
1000
+ * Builds one project-relative path inside the shared Promptbook working directory.
1001
+ *
1002
+ * @private internal utility for Promptbook-owned temp files
1003
+ */
1004
+ function getPromptbookTempPath(...pathSegments) {
1005
+ return `./${getPromptbookTempPosixPath(...pathSegments)}`;
1006
+ }
1007
+ /**
1008
+ * Builds one absolute filesystem path inside the shared Promptbook working directory for a project root.
1009
+ *
1010
+ * @private internal utility for Promptbook-owned temp files
1011
+ */
1012
+ function resolvePromptbookTempPath(projectPath, ...pathSegments) {
1013
+ return join(projectPath, PROMPTBOOK_TEMP_DIRECTORY_NAME, ...pathSegments);
1014
+ }
1015
+ /**
1016
+ * Builds one POSIX path fragment inside the shared Promptbook working directory for globs and generated text files.
1017
+ *
1018
+ * @private internal utility for Promptbook-owned temp files
1019
+ */
1020
+ function getPromptbookTempPosixPath(...pathSegments) {
1021
+ return posix.join(PROMPTBOOK_TEMP_DIRECTORY_NAME, ...pathSegments);
1022
+ }
1023
+ /**
1024
+ * Builds one rooted `.gitignore` rule targeting a path inside the shared Promptbook working directory.
1025
+ *
1026
+ * @private internal utility for Promptbook-owned temp files
1027
+ */
1028
+ function getPromptbookTempGitignoreRule(...pathSegments) {
1029
+ return `/${getPromptbookTempPosixPath(...pathSegments)}`;
1030
+ }
1031
+
993
1032
  /**
994
1033
  * Returns the same value that is passed as argument.
995
1034
  * No side effects.
@@ -1232,7 +1271,6 @@ const DEFAULT_BOOKS_DIRNAME = './books';
1232
1271
  */
1233
1272
  const DEFAULT_AGENTS_DIRNAME = './agents';
1234
1273
  // <- TODO: [🕝] Make also `AGENTS_DIRNAME_ALTERNATIVES`
1235
- // TODO: Just `.promptbook` in config, hardcode subfolders like `download-cache` or `execution-cache`
1236
1274
  /**
1237
1275
  * Where to store the temporary downloads
1238
1276
  *
@@ -1240,7 +1278,7 @@ const DEFAULT_AGENTS_DIRNAME = './agents';
1240
1278
  *
1241
1279
  * @public exported from `@promptbook/core`
1242
1280
  */
1243
- const DEFAULT_DOWNLOAD_CACHE_DIRNAME = './.promptbook/download-cache';
1281
+ const DEFAULT_DOWNLOAD_CACHE_DIRNAME = getPromptbookTempPath('download-cache');
1244
1282
  /**
1245
1283
  * Where to store the cache of executions for promptbook CLI
1246
1284
  *
@@ -1248,7 +1286,7 @@ const DEFAULT_DOWNLOAD_CACHE_DIRNAME = './.promptbook/download-cache';
1248
1286
  *
1249
1287
  * @public exported from `@promptbook/core`
1250
1288
  */
1251
- const DEFAULT_EXECUTION_CACHE_DIRNAME = './.promptbook/execution-cache';
1289
+ const DEFAULT_EXECUTION_CACHE_DIRNAME = getPromptbookTempPath('execution-cache');
1252
1290
  /**
1253
1291
  * Where to store the scrape cache
1254
1292
  *
@@ -1256,7 +1294,7 @@ const DEFAULT_EXECUTION_CACHE_DIRNAME = './.promptbook/execution-cache';
1256
1294
  *
1257
1295
  * @public exported from `@promptbook/core`
1258
1296
  */
1259
- const DEFAULT_SCRAPE_CACHE_DIRNAME = './.promptbook/scrape-cache';
1297
+ const DEFAULT_SCRAPE_CACHE_DIRNAME = getPromptbookTempPath('scrape-cache');
1260
1298
  /**
1261
1299
  * Id of application for the CLI when using remote server
1262
1300
  *
@@ -2175,9 +2213,8 @@ const SOURCE_FILE_IGNORE_GLOBS = [
2175
2213
  '**/.git/**',
2176
2214
  '**/.idea/**',
2177
2215
  '**/.vscode/**',
2178
- '**/.promptbook/**',
2216
+ `**/${getPromptbookTempPosixPath()}/**`,
2179
2217
  '**/.next/**',
2180
- '**/.tmp/**',
2181
2218
  '**/tmp/**',
2182
2219
  '**/coverage/**',
2183
2220
  '**/dist/**',
@@ -2997,13 +3034,9 @@ function escapeRegExp$1(value) {
2997
3034
  */
2998
3035
  const GITIGNORE_FILE_PATH = '.gitignore';
2999
3036
  /**
3000
- * Promptbook coder temp directory that should stay out of version control.
3037
+ * Shared Promptbook working directory that should stay out of version control.
3001
3038
  */
3002
- const CODER_TEMP_GITIGNORE_RULE = '/.tmp';
3003
- /**
3004
- * Promptbook coder cache directory that should stay out of version control.
3005
- */
3006
- const CODER_CACHE_GITIGNORE_RULE = '/.promptbook/ptbk-coder';
3039
+ const CODER_TEMP_GITIGNORE_RULE = getPromptbookTempGitignoreRule();
3007
3040
  /**
3008
3041
  * Promptbook coder environment file that should stay out of version control.
3009
3042
  */
@@ -3013,7 +3046,7 @@ const CODER_ENV_GITIGNORE_RULE = '.env';
3013
3046
  */
3014
3047
  const CODER_GITIGNORE_HEADER = '# Promptbook Coder';
3015
3048
  /**
3016
- * Ensures `.gitignore` contains the standalone Promptbook coder temp and cache entries.
3049
+ * Ensures `.gitignore` contains the shared Promptbook working-directory entry.
3017
3050
  *
3018
3051
  * @private function of `initializeCoderProjectConfiguration`
3019
3052
  */
@@ -3032,7 +3065,7 @@ async function ensureCoderGitignoreFile(projectPath) {
3032
3065
  * Returns the Promptbook coder gitignore rules that still need to be added.
3033
3066
  */
3034
3067
  function getMissingCoderGitignoreRules(gitignoreContent) {
3035
- const requiredRules = [CODER_TEMP_GITIGNORE_RULE, CODER_CACHE_GITIGNORE_RULE, CODER_ENV_GITIGNORE_RULE];
3068
+ const requiredRules = [CODER_TEMP_GITIGNORE_RULE, CODER_ENV_GITIGNORE_RULE];
3036
3069
  return requiredRules.filter((rule) => !hasGitignoreRule(gitignoreContent, rule));
3037
3070
  }
3038
3071
  /**
@@ -22688,6 +22721,177 @@ function drawFractalCore(context, size, palette, timeMs, corePhase) {
22688
22721
  context.restore();
22689
22722
  }
22690
22723
 
22724
+ /* eslint-disable no-magic-numbers */
22725
+ /**
22726
+ * Builds the seeded six-face texture pack used by the Minecraft-style head cuboid.
22727
+ *
22728
+ * @param random Seeded random generator.
22729
+ * @param palette Derived avatar palette.
22730
+ * @param hasHeadband Whether the generated avatar should include a colored headband.
22731
+ * @returns Head cuboid textures.
22732
+ *
22733
+ * @private helper of the Minecraft avatar visuals
22734
+ */
22735
+ function createMinecraftHeadTextures(random, palette, hasHeadband) {
22736
+ const faceTexture = createMinecraftFaceTexture(random, palette, hasHeadband);
22737
+ const hairColor = random() < 0.5 ? palette.primary : palette.secondary;
22738
+ const skinColor = palette.highlight;
22739
+ const headbandColor = hasHeadband ? palette.accent : hairColor;
22740
+ const sideTexture = createFilledTexture(skinColor);
22741
+ const backTexture = createFilledTexture(skinColor);
22742
+ const topTexture = createFilledTexture(hairColor);
22743
+ const bottomTexture = createFilledTexture(`${palette.shadow}cc`);
22744
+ fillTextureRect(sideTexture, 0, 0, 8, 3, hairColor);
22745
+ fillTextureRect(backTexture, 0, 0, 8, 5, hairColor);
22746
+ fillTextureRect(backTexture, 1, 5, 6, 1, hairColor);
22747
+ if (hasHeadband) {
22748
+ fillTextureRect(sideTexture, 0, 2, 8, 1, headbandColor);
22749
+ fillTextureRect(backTexture, 0, 2, 8, 1, headbandColor);
22750
+ fillTextureRect(topTexture, 0, 4, 8, 1, headbandColor);
22751
+ }
22752
+ sideTexture[4][4] = `${palette.shadow}99`;
22753
+ sideTexture[5][4] = `${palette.shadow}cc`;
22754
+ backTexture[6][2] = `${palette.shadow}99`;
22755
+ backTexture[6][5] = `${palette.shadow}99`;
22756
+ return {
22757
+ front: faceTexture,
22758
+ back: backTexture,
22759
+ left: sideTexture,
22760
+ right: mirrorMinecraftTexture(sideTexture),
22761
+ top: topTexture,
22762
+ bottom: bottomTexture,
22763
+ };
22764
+ }
22765
+ /**
22766
+ * Builds the seeded six-face texture pack used by the Minecraft-style torso cuboid.
22767
+ *
22768
+ * @param random Seeded random generator.
22769
+ * @param palette Derived avatar palette.
22770
+ * @returns Torso cuboid textures.
22771
+ *
22772
+ * @private helper of the Minecraft avatar visuals
22773
+ */
22774
+ function createMinecraftTorsoTextures(random, palette) {
22775
+ const frontTexture = createMinecraftShirtTexture(random, palette);
22776
+ const sideTexture = createFilledTexture(palette.primary);
22777
+ const backTexture = createFilledTexture(palette.primary);
22778
+ const topTexture = createFilledTexture(`${palette.highlight}dd`);
22779
+ const bottomTexture = createFilledTexture(`${palette.shadow}dd`);
22780
+ const stripeColor = random() < 0.5 ? palette.secondary : palette.highlight;
22781
+ fillTextureRect(sideTexture, 0, 0, 8, 2, palette.shadow);
22782
+ fillTextureRect(backTexture, 0, 0, 8, 2, palette.shadow);
22783
+ fillTextureRect(backTexture, 3, 2, 2, 6, stripeColor);
22784
+ fillTextureRect(sideTexture, 4, 2, 1, 6, stripeColor);
22785
+ fillTextureRect(topTexture, 0, 0, 8, 2, palette.shadow);
22786
+ fillTextureRect(topTexture, 2, 2, 4, 4, stripeColor);
22787
+ return {
22788
+ front: frontTexture,
22789
+ back: backTexture,
22790
+ left: sideTexture,
22791
+ right: mirrorMinecraftTexture(sideTexture),
22792
+ top: topTexture,
22793
+ bottom: bottomTexture,
22794
+ };
22795
+ }
22796
+ /**
22797
+ * Mirrors one Minecraft texture horizontally.
22798
+ *
22799
+ * @param texture Source texture.
22800
+ * @returns Mirrored texture copy.
22801
+ *
22802
+ * @private helper of the Minecraft avatar visuals
22803
+ */
22804
+ function mirrorMinecraftTexture(texture) {
22805
+ return texture.map((row) => [...row].reverse());
22806
+ }
22807
+ /**
22808
+ * Creates the front-face pixel texture for the cube head.
22809
+ *
22810
+ * @param random Seeded random generator.
22811
+ * @param palette Derived avatar palette.
22812
+ * @param hasHeadband Whether the avatar should render a headband row.
22813
+ * @returns 8x8 pixel texture.
22814
+ *
22815
+ * @private helper of the Minecraft avatar visuals
22816
+ */
22817
+ function createMinecraftFaceTexture(random, palette, hasHeadband) {
22818
+ const texture = createFilledTexture(palette.highlight);
22819
+ const hairlineColor = random() < 0.5 ? palette.primary : palette.secondary;
22820
+ const cheekColor = random() < 0.5 ? `${palette.accent}bb` : `${palette.secondary}bb`;
22821
+ fillTextureRect(texture, 0, 0, 8, 2, hairlineColor);
22822
+ texture[2][0] = hairlineColor;
22823
+ texture[2][7] = hairlineColor;
22824
+ texture[3][0] = hairlineColor;
22825
+ texture[3][7] = hairlineColor;
22826
+ if (hasHeadband) {
22827
+ fillTextureRect(texture, 0, 2, 8, 1, palette.accent);
22828
+ }
22829
+ texture[3][2] = palette.ink;
22830
+ texture[3][5] = palette.ink;
22831
+ texture[4][2] = '#ffffff';
22832
+ texture[4][5] = '#ffffff';
22833
+ texture[5][1] = cheekColor;
22834
+ texture[5][6] = cheekColor;
22835
+ texture[5][3] = palette.shadow;
22836
+ texture[5][4] = palette.shadow;
22837
+ texture[6][3] = palette.shadow;
22838
+ texture[6][4] = palette.shadow;
22839
+ return texture;
22840
+ }
22841
+ /**
22842
+ * Creates the front-face pixel texture for the torso.
22843
+ *
22844
+ * @param random Seeded random generator.
22845
+ * @param palette Derived avatar palette.
22846
+ * @returns 8x8 torso texture.
22847
+ *
22848
+ * @private helper of the Minecraft avatar visuals
22849
+ */
22850
+ function createMinecraftShirtTexture(random, palette) {
22851
+ const texture = createFilledTexture(palette.primary);
22852
+ const stripeColor = random() < 0.5 ? palette.secondary : palette.highlight;
22853
+ fillTextureRect(texture, 0, 0, 8, 2, palette.shadow);
22854
+ for (let rowIndex = 2; rowIndex < 8; rowIndex++) {
22855
+ texture[rowIndex][3] = stripeColor;
22856
+ texture[rowIndex][4] = stripeColor;
22857
+ }
22858
+ texture[4][1] = palette.accent;
22859
+ texture[4][6] = palette.accent;
22860
+ texture[5][2] = palette.highlight;
22861
+ texture[5][5] = palette.highlight;
22862
+ return texture;
22863
+ }
22864
+ /**
22865
+ * Creates one solid-color 8x8 Minecraft texture.
22866
+ *
22867
+ * @param color Fill color.
22868
+ * @returns Filled 8x8 texture.
22869
+ *
22870
+ * @private helper of the Minecraft avatar visuals
22871
+ */
22872
+ function createFilledTexture(color) {
22873
+ return Array.from({ length: 8 }, () => Array.from({ length: 8 }, () => color));
22874
+ }
22875
+ /**
22876
+ * Fills one rectangular area inside a mutable Minecraft texture.
22877
+ *
22878
+ * @param texture Mutable target texture.
22879
+ * @param x Left texture coordinate.
22880
+ * @param y Top texture coordinate.
22881
+ * @param width Rectangle width.
22882
+ * @param height Rectangle height.
22883
+ * @param color Fill color.
22884
+ *
22885
+ * @private helper of the Minecraft avatar visuals
22886
+ */
22887
+ function fillTextureRect(texture, x, y, width, height, color) {
22888
+ for (let rowIndex = y; rowIndex < y + height; rowIndex++) {
22889
+ for (let columnIndex = x; columnIndex < x + width; columnIndex++) {
22890
+ texture[rowIndex][columnIndex] = color;
22891
+ }
22892
+ }
22893
+ }
22894
+
22691
22895
  /* eslint-disable no-magic-numbers */
22692
22896
  /**
22693
22897
  * Minecraft-style 3D avatar visual.
@@ -22712,8 +22916,8 @@ const minecraftAvatarVisual = {
22712
22916
  const bodyX = size * 0.33;
22713
22917
  const bodyY = headY + headSize * 0.96;
22714
22918
  const hasHeadband = random() < 0.5;
22715
- const faceTexture = createMinecraftFaceTexture(createRandom('minecraft-face'), palette, hasHeadband);
22716
- const shirtTexture = createMinecraftShirtTexture(createRandom('minecraft-shirt'), palette);
22919
+ const headTextures = createMinecraftHeadTextures(createRandom('minecraft-face'), palette, hasHeadband);
22920
+ const torsoTextures = createMinecraftTorsoTextures(createRandom('minecraft-shirt'), palette);
22717
22921
  drawAvatarFrame(context, size, palette);
22718
22922
  const spotlight = context.createRadialGradient(size * 0.5, size * 0.18, size * 0.05, size * 0.5, size * 0.18, size * 0.5);
22719
22923
  spotlight.addColorStop(0, `${palette.highlight}66`);
@@ -22733,7 +22937,7 @@ const minecraftAvatarVisual = {
22733
22937
  width: bodyWidth,
22734
22938
  height: bodyHeight,
22735
22939
  depth: bodyDepth,
22736
- frontTexture: shirtTexture,
22940
+ frontTexture: torsoTextures.front,
22737
22941
  topColor: `${palette.highlight}cc`,
22738
22942
  sideColor: `${palette.secondary}dd`,
22739
22943
  outlineColor: `${palette.shadow}aa`,
@@ -22744,7 +22948,7 @@ const minecraftAvatarVisual = {
22744
22948
  width: headSize,
22745
22949
  height: headSize,
22746
22950
  depth,
22747
- frontTexture: faceTexture,
22951
+ frontTexture: headTextures.front,
22748
22952
  topColor: `${palette.highlight}ee`,
22749
22953
  sideColor: `${palette.secondary}ee`,
22750
22954
  outlineColor: `${palette.shadow}cc`,
@@ -22809,72 +23013,505 @@ function drawVoxelCuboid(context, cuboid) {
22809
23013
  context.closePath();
22810
23014
  context.stroke();
22811
23015
  }
23016
+
23017
+ /* eslint-disable no-magic-numbers */
22812
23018
  /**
22813
- * Creates the front-face pixel texture for the cube head.
23019
+ * Fixed scene camera distance used for the proper-3D projection.
22814
23020
  *
22815
- * @param random Seeded random generator.
23021
+ * @private helper of `minecraft2AvatarVisual`
23022
+ */
23023
+ const CAMERA_DISTANCE_RATIO = 1.4;
23024
+ /**
23025
+ * Shared light direction used to shade projected cuboid faces.
23026
+ *
23027
+ * @private helper of `minecraft2AvatarVisual`
23028
+ */
23029
+ const LIGHT_DIRECTION = normalizeVector3({
23030
+ x: 0.4,
23031
+ y: -0.65,
23032
+ z: 0.92,
23033
+ });
23034
+ /**
23035
+ * Minecraft 3D 2 avatar visual.
23036
+ *
23037
+ * @private built-in avatar visual
23038
+ */
23039
+ const minecraft2AvatarVisual = {
23040
+ id: 'minecraft2',
23041
+ title: 'Minecraft 3D 2',
23042
+ description: 'Proper 3D Minecraft-style portrait with textured cuboids and pointer-driven head turns.',
23043
+ isAnimated: true,
23044
+ supportsPointerTracking: true,
23045
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
23046
+ const spotlightY = size * 0.22;
23047
+ const headRandom = createRandom('minecraft2-head');
23048
+ const hasHeadband = headRandom() < 0.5;
23049
+ const headTextures = createMinecraftHeadTextures(createRandom('minecraft2-head-textures'), palette, hasHeadband);
23050
+ const torsoTextures = createMinecraftTorsoTextures(createRandom('minecraft2-body-textures'), palette);
23051
+ const bob = Math.sin(timeMs / 880) * size * 0.014;
23052
+ const bodyYaw = -0.24 + Math.sin(timeMs / 2300) * 0.06 + interaction.bodyOffsetX * 0.16;
23053
+ const bodyPitch = -0.12 + Math.cos(timeMs / 2800) * 0.02 - interaction.bodyOffsetY * 0.06;
23054
+ const headYaw = -0.18 + Math.sin(timeMs / 1900 + 0.6) * 0.05 + interaction.gazeX * 0.62;
23055
+ const headPitch = -0.12 + Math.cos(timeMs / 2400 + 1.1) * 0.03 - interaction.gazeY * 0.38;
23056
+ const sceneCenterX = size * 0.5;
23057
+ const sceneCenterY = size * 0.57;
23058
+ const bodyWidth = size * 0.225;
23059
+ const bodyHeight = size * 0.245;
23060
+ const bodyDepth = size * 0.145;
23061
+ const headSize = size * 0.24;
23062
+ const headLift = size * 0.205;
23063
+ const headForwardShift = interaction.intensity * size * 0.018;
23064
+ const sceneCuboids = [
23065
+ {
23066
+ center: {
23067
+ x: interaction.bodyOffsetX * size * 0.026,
23068
+ y: size * 0.05 + interaction.bodyOffsetY * size * 0.018 + bob,
23069
+ z: 0,
23070
+ },
23071
+ width: bodyWidth,
23072
+ height: bodyHeight,
23073
+ depth: bodyDepth,
23074
+ rotationX: bodyPitch,
23075
+ rotationY: bodyYaw,
23076
+ textures: torsoTextures,
23077
+ outlineColor: `${palette.shadow}cc`,
23078
+ },
23079
+ {
23080
+ center: {
23081
+ x: interaction.bodyOffsetX * size * 0.018 + interaction.gazeX * size * 0.016,
23082
+ y: -headLift + bob * 1.15,
23083
+ z: headForwardShift,
23084
+ },
23085
+ width: headSize,
23086
+ height: headSize,
23087
+ depth: headSize,
23088
+ rotationX: headPitch,
23089
+ rotationY: headYaw,
23090
+ textures: headTextures,
23091
+ outlineColor: `${palette.shadow}dd`,
23092
+ },
23093
+ ];
23094
+ const visibleFaces = sceneCuboids
23095
+ .flatMap((cuboid) => resolveVisibleCuboidFaces(cuboid, size, sceneCenterX, sceneCenterY))
23096
+ .sort((firstFace, secondFace) => firstFace.averageDepth - secondFace.averageDepth);
23097
+ drawAvatarFrame(context, size, palette);
23098
+ drawMinecraftBackdrop(context, size, palette, sceneCenterX, spotlightY, interaction, timeMs);
23099
+ drawMinecraftShadow(context, size, palette, interaction, timeMs);
23100
+ for (const visibleFace of visibleFaces) {
23101
+ drawTexturedProjectedFace(context, visibleFace);
23102
+ }
23103
+ },
23104
+ };
23105
+ /**
23106
+ * Draws the shared background atmosphere behind the Minecraft 3D 2 portrait.
23107
+ *
23108
+ * @param context Canvas 2D context.
23109
+ * @param size Canvas size in CSS pixels.
22816
23110
  * @param palette Derived avatar palette.
22817
- * @param hasHeadband Whether the avatar should render a headband row.
22818
- * @returns 8x8 pixel texture.
23111
+ * @param sceneCenterX Horizontal scene center.
23112
+ * @param spotlightY Vertical spotlight anchor.
23113
+ * @param interaction Smoothed pointer-aware interaction state.
23114
+ * @param timeMs Current animation time in milliseconds.
22819
23115
  *
22820
- * @private helper of `minecraftAvatarVisual`
23116
+ * @private helper of `minecraft2AvatarVisual`
22821
23117
  */
22822
- function createMinecraftFaceTexture(random, palette, hasHeadband) {
22823
- const texture = Array.from({ length: 8 }, () => Array.from({ length: 8 }, () => palette.highlight));
22824
- const hairlineColor = random() < 0.5 ? palette.primary : palette.secondary;
22825
- const cheekColor = random() < 0.5 ? `${palette.accent}bb` : `${palette.secondary}bb`;
22826
- for (let rowIndex = 0; rowIndex < 2; rowIndex++) {
22827
- for (let columnIndex = 0; columnIndex < 8; columnIndex++) {
22828
- texture[rowIndex][columnIndex] = hairlineColor;
23118
+ function drawMinecraftBackdrop(context, size, palette, sceneCenterX, spotlightY, interaction, timeMs) {
23119
+ const spotlightGradient = context.createRadialGradient(sceneCenterX + interaction.gazeX * size * 0.08, spotlightY + interaction.gazeY * size * 0.05, size * 0.03, sceneCenterX, spotlightY, size * 0.52);
23120
+ spotlightGradient.addColorStop(0, `${palette.highlight}66`);
23121
+ spotlightGradient.addColorStop(0.42, `${palette.accent}1d`);
23122
+ spotlightGradient.addColorStop(1, `${palette.highlight}00`);
23123
+ context.fillStyle = spotlightGradient;
23124
+ context.fillRect(0, 0, size, size);
23125
+ const rimGradient = context.createLinearGradient(0, size * 0.14, 0, size * 0.92);
23126
+ rimGradient.addColorStop(0, `${palette.highlight}12`);
23127
+ rimGradient.addColorStop(0.55, `${palette.secondary}0a`);
23128
+ rimGradient.addColorStop(1, `${palette.shadow}00`);
23129
+ context.fillStyle = rimGradient;
23130
+ context.fillRect(0, 0, size, size);
23131
+ context.save();
23132
+ context.globalAlpha = 0.08 + interaction.intensity * 0.04;
23133
+ context.fillStyle = palette.highlight;
23134
+ context.beginPath();
23135
+ context.arc(size * 0.72 + Math.sin(timeMs / 1600) * size * 0.03, size * 0.2 + Math.cos(timeMs / 1400) * size * 0.018, size * 0.025, 0, Math.PI * 2);
23136
+ context.fill();
23137
+ context.restore();
23138
+ }
23139
+ /**
23140
+ * Draws the soft floor shadow used to anchor the cuboids in the frame.
23141
+ *
23142
+ * @param context Canvas 2D context.
23143
+ * @param size Canvas size in CSS pixels.
23144
+ * @param palette Derived avatar palette.
23145
+ * @param interaction Smoothed pointer-aware interaction state.
23146
+ * @param timeMs Current animation time in milliseconds.
23147
+ *
23148
+ * @private helper of `minecraft2AvatarVisual`
23149
+ */
23150
+ function drawMinecraftShadow(context, size, palette, interaction, timeMs) {
23151
+ context.save();
23152
+ context.fillStyle = `${palette.shadow}66`;
23153
+ context.filter = `blur(${size * 0.02}px)`;
23154
+ context.beginPath();
23155
+ context.ellipse(size * 0.5 + interaction.gazeX * size * 0.03, size * 0.85 + Math.sin(timeMs / 880) * size * 0.01, size * (0.16 + interaction.intensity * 0.015), size * 0.055, 0, 0, Math.PI * 2);
23156
+ context.fill();
23157
+ context.restore();
23158
+ }
23159
+ /**
23160
+ * Resolves all visible projected faces for one scene cuboid.
23161
+ *
23162
+ * @param cuboid Scene cuboid definition.
23163
+ * @param size Canvas size in CSS pixels.
23164
+ * @param sceneCenterX Horizontal scene center.
23165
+ * @param sceneCenterY Vertical scene center.
23166
+ * @returns Visible faces sorted later by depth.
23167
+ *
23168
+ * @private helper of `minecraft2AvatarVisual`
23169
+ */
23170
+ function resolveVisibleCuboidFaces(cuboid, size, sceneCenterX, sceneCenterY) {
23171
+ const halfWidth = cuboid.width / 2;
23172
+ const halfHeight = cuboid.height / 2;
23173
+ const halfDepth = cuboid.depth / 2;
23174
+ const faceDefinitions = [
23175
+ {
23176
+ texture: cuboid.textures.front,
23177
+ corners: [
23178
+ { x: -halfWidth, y: -halfHeight, z: halfDepth },
23179
+ { x: halfWidth, y: -halfHeight, z: halfDepth },
23180
+ { x: halfWidth, y: halfHeight, z: halfDepth },
23181
+ { x: -halfWidth, y: halfHeight, z: halfDepth },
23182
+ ],
23183
+ },
23184
+ {
23185
+ texture: cuboid.textures.back,
23186
+ corners: [
23187
+ { x: halfWidth, y: -halfHeight, z: -halfDepth },
23188
+ { x: -halfWidth, y: -halfHeight, z: -halfDepth },
23189
+ { x: -halfWidth, y: halfHeight, z: -halfDepth },
23190
+ { x: halfWidth, y: halfHeight, z: -halfDepth },
23191
+ ],
23192
+ },
23193
+ {
23194
+ texture: cuboid.textures.right,
23195
+ corners: [
23196
+ { x: halfWidth, y: -halfHeight, z: halfDepth },
23197
+ { x: halfWidth, y: -halfHeight, z: -halfDepth },
23198
+ { x: halfWidth, y: halfHeight, z: -halfDepth },
23199
+ { x: halfWidth, y: halfHeight, z: halfDepth },
23200
+ ],
23201
+ },
23202
+ {
23203
+ texture: cuboid.textures.left,
23204
+ corners: [
23205
+ { x: -halfWidth, y: -halfHeight, z: -halfDepth },
23206
+ { x: -halfWidth, y: -halfHeight, z: halfDepth },
23207
+ { x: -halfWidth, y: halfHeight, z: halfDepth },
23208
+ { x: -halfWidth, y: halfHeight, z: -halfDepth },
23209
+ ],
23210
+ },
23211
+ {
23212
+ texture: cuboid.textures.top,
23213
+ corners: [
23214
+ { x: -halfWidth, y: -halfHeight, z: -halfDepth },
23215
+ { x: halfWidth, y: -halfHeight, z: -halfDepth },
23216
+ { x: halfWidth, y: -halfHeight, z: halfDepth },
23217
+ { x: -halfWidth, y: -halfHeight, z: halfDepth },
23218
+ ],
23219
+ },
23220
+ {
23221
+ texture: cuboid.textures.bottom,
23222
+ corners: [
23223
+ { x: -halfWidth, y: halfHeight, z: halfDepth },
23224
+ { x: halfWidth, y: halfHeight, z: halfDepth },
23225
+ { x: halfWidth, y: halfHeight, z: -halfDepth },
23226
+ { x: -halfWidth, y: halfHeight, z: -halfDepth },
23227
+ ],
23228
+ },
23229
+ ];
23230
+ const visibleFaces = faceDefinitions
23231
+ .map((faceDefinition) => {
23232
+ const transformedCorners = faceDefinition.corners.map((corner) => transformScenePoint(corner, cuboid.center, cuboid.rotationX, cuboid.rotationY));
23233
+ const faceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
23234
+ if (faceNormal.z <= 0.02) {
23235
+ return null;
22829
23236
  }
23237
+ const projectedCorners = transformedCorners.map((corner) => projectScenePoint(corner, size, sceneCenterX, sceneCenterY));
23238
+ return {
23239
+ corners: projectedCorners,
23240
+ texture: faceDefinition.texture,
23241
+ averageDepth: transformedCorners.reduce((depthSum, corner) => depthSum + corner.z, 0) / transformedCorners.length,
23242
+ lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION), -1, 1),
23243
+ outlineColor: cuboid.outlineColor,
23244
+ };
23245
+ });
23246
+ return visibleFaces.filter((visibleFace) => visibleFace !== null);
23247
+ }
23248
+ /**
23249
+ * Draws one projected textured face by tessellating its texture cells into quads.
23250
+ *
23251
+ * @param context Canvas 2D context.
23252
+ * @param face Visible projected face.
23253
+ *
23254
+ * @private helper of `minecraft2AvatarVisual`
23255
+ */
23256
+ function drawTexturedProjectedFace(context, face) {
23257
+ var _a;
23258
+ const rows = face.texture.length;
23259
+ const columns = ((_a = face.texture[0]) === null || _a === void 0 ? void 0 : _a.length) || 0;
23260
+ if (rows === 0 || columns === 0) {
23261
+ return;
22830
23262
  }
22831
- texture[2][0] = hairlineColor;
22832
- texture[2][7] = hairlineColor;
22833
- texture[3][0] = hairlineColor;
22834
- texture[3][7] = hairlineColor;
22835
- if (hasHeadband) {
22836
- for (let columnIndex = 0; columnIndex < 8; columnIndex++) {
22837
- texture[2][columnIndex] = palette.accent;
23263
+ for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
23264
+ for (let columnIndex = 0; columnIndex < columns; columnIndex++) {
23265
+ const startX = columnIndex / columns;
23266
+ const endX = (columnIndex + 1) / columns;
23267
+ const startY = rowIndex / rows;
23268
+ const endY = (rowIndex + 1) / rows;
23269
+ drawProjectedQuad(context, [
23270
+ interpolateProjectedQuad(face.corners, startX, startY),
23271
+ interpolateProjectedQuad(face.corners, endX, startY),
23272
+ interpolateProjectedQuad(face.corners, endX, endY),
23273
+ interpolateProjectedQuad(face.corners, startX, endY),
23274
+ ], face.texture[rowIndex][columnIndex]);
22838
23275
  }
22839
23276
  }
22840
- texture[3][2] = palette.ink;
22841
- texture[3][5] = palette.ink;
22842
- texture[4][2] = '#ffffff';
22843
- texture[4][5] = '#ffffff';
22844
- texture[5][1] = cheekColor;
22845
- texture[5][6] = cheekColor;
22846
- texture[5][3] = palette.shadow;
22847
- texture[5][4] = palette.shadow;
22848
- texture[6][3] = palette.shadow;
22849
- texture[6][4] = palette.shadow;
22850
- return texture;
23277
+ if (face.lightIntensity > 0) {
23278
+ drawProjectedQuad(context, face.corners, `rgba(255, 255, 255, ${0.15 * face.lightIntensity})`);
23279
+ }
23280
+ else if (face.lightIntensity < 0) {
23281
+ drawProjectedQuad(context, face.corners, `rgba(0, 0, 0, ${0.22 * Math.abs(face.lightIntensity)})`);
23282
+ }
23283
+ context.save();
23284
+ context.beginPath();
23285
+ context.moveTo(face.corners[0].x, face.corners[0].y);
23286
+ for (let cornerIndex = 1; cornerIndex < face.corners.length; cornerIndex++) {
23287
+ context.lineTo(face.corners[cornerIndex].x, face.corners[cornerIndex].y);
23288
+ }
23289
+ context.closePath();
23290
+ context.strokeStyle = face.outlineColor;
23291
+ context.lineWidth = Math.max(1.1, getProjectedQuadPerimeter(face.corners) * 0.0045);
23292
+ context.lineJoin = 'round';
23293
+ context.stroke();
23294
+ context.restore();
22851
23295
  }
22852
23296
  /**
22853
- * Creates the front-face pixel texture for the torso.
23297
+ * Draws one filled projected quad.
22854
23298
  *
22855
- * @param random Seeded random generator.
22856
- * @param palette Derived avatar palette.
22857
- * @returns 8x8 torso texture.
23299
+ * @param context Canvas 2D context.
23300
+ * @param corners Quad corners in clockwise order.
23301
+ * @param fillStyle CSS fill style.
22858
23302
  *
22859
- * @private helper of `minecraftAvatarVisual`
23303
+ * @private helper of `minecraft2AvatarVisual`
22860
23304
  */
22861
- function createMinecraftShirtTexture(random, palette) {
22862
- const texture = Array.from({ length: 8 }, () => Array.from({ length: 8 }, () => palette.primary));
22863
- const stripeColor = random() < 0.5 ? palette.secondary : palette.highlight;
22864
- for (let rowIndex = 0; rowIndex < 2; rowIndex++) {
22865
- for (let columnIndex = 0; columnIndex < 8; columnIndex++) {
22866
- texture[rowIndex][columnIndex] = palette.shadow;
22867
- }
23305
+ function drawProjectedQuad(context, corners, fillStyle) {
23306
+ context.beginPath();
23307
+ context.moveTo(corners[0].x, corners[0].y);
23308
+ context.lineTo(corners[1].x, corners[1].y);
23309
+ context.lineTo(corners[2].x, corners[2].y);
23310
+ context.lineTo(corners[3].x, corners[3].y);
23311
+ context.closePath();
23312
+ context.fillStyle = fillStyle;
23313
+ context.fill();
23314
+ }
23315
+ /**
23316
+ * Interpolates one point inside a projected quad across both quad axes.
23317
+ *
23318
+ * @param corners Quad corners in clockwise order.
23319
+ * @param horizontalRatio Horizontal ratio in the range `[0, 1]`.
23320
+ * @param verticalRatio Vertical ratio in the range `[0, 1]`.
23321
+ * @returns Interpolated projected point.
23322
+ *
23323
+ * @private helper of `minecraft2AvatarVisual`
23324
+ */
23325
+ function interpolateProjectedQuad(corners, horizontalRatio, verticalRatio) {
23326
+ const topPoint = interpolateProjectedPoint(corners[0], corners[1], horizontalRatio);
23327
+ const bottomPoint = interpolateProjectedPoint(corners[3], corners[2], horizontalRatio);
23328
+ return interpolateProjectedPoint(topPoint, bottomPoint, verticalRatio);
23329
+ }
23330
+ /**
23331
+ * Interpolates between two projected points.
23332
+ *
23333
+ * @param startPoint Start point.
23334
+ * @param endPoint End point.
23335
+ * @param ratio Interpolation ratio in the range `[0, 1]`.
23336
+ * @returns Interpolated projected point.
23337
+ *
23338
+ * @private helper of `minecraft2AvatarVisual`
23339
+ */
23340
+ function interpolateProjectedPoint(startPoint, endPoint, ratio) {
23341
+ return {
23342
+ x: startPoint.x + (endPoint.x - startPoint.x) * ratio,
23343
+ y: startPoint.y + (endPoint.y - startPoint.y) * ratio,
23344
+ z: startPoint.z + (endPoint.z - startPoint.z) * ratio,
23345
+ };
23346
+ }
23347
+ /**
23348
+ * Projects one rotated scene point into canvas coordinates.
23349
+ *
23350
+ * @param point Scene point.
23351
+ * @param size Canvas size in CSS pixels.
23352
+ * @param sceneCenterX Horizontal scene center.
23353
+ * @param sceneCenterY Vertical scene center.
23354
+ * @returns Projected point.
23355
+ *
23356
+ * @private helper of `minecraft2AvatarVisual`
23357
+ */
23358
+ function projectScenePoint(point, size, sceneCenterX, sceneCenterY) {
23359
+ const cameraDistance = size * CAMERA_DISTANCE_RATIO;
23360
+ const perspectiveScale = cameraDistance / Math.max(cameraDistance - point.z, cameraDistance * 0.35);
23361
+ return {
23362
+ x: sceneCenterX + point.x * perspectiveScale,
23363
+ y: sceneCenterY + point.y * perspectiveScale,
23364
+ z: point.z,
23365
+ };
23366
+ }
23367
+ /**
23368
+ * Applies the local cuboid rotations and translation to one scene point.
23369
+ *
23370
+ * @param localPoint Point in cuboid-local space.
23371
+ * @param center Cuboid center in scene space.
23372
+ * @param rotationX Cuboid pitch in radians.
23373
+ * @param rotationY Cuboid yaw in radians.
23374
+ * @returns Transformed scene-space point.
23375
+ *
23376
+ * @private helper of `minecraft2AvatarVisual`
23377
+ */
23378
+ function transformScenePoint(localPoint, center, rotationX, rotationY) {
23379
+ const yawedPoint = rotatePointAroundY(localPoint, rotationY);
23380
+ const pitchedPoint = rotatePointAroundX(yawedPoint, rotationX);
23381
+ return {
23382
+ x: center.x + pitchedPoint.x,
23383
+ y: center.y + pitchedPoint.y,
23384
+ z: center.z + pitchedPoint.z,
23385
+ };
23386
+ }
23387
+ /**
23388
+ * Rotates one point around the local Y axis.
23389
+ *
23390
+ * @param point Source point.
23391
+ * @param angle Rotation angle in radians.
23392
+ * @returns Rotated point.
23393
+ *
23394
+ * @private helper of `minecraft2AvatarVisual`
23395
+ */
23396
+ function rotatePointAroundY(point, angle) {
23397
+ const cosine = Math.cos(angle);
23398
+ const sine = Math.sin(angle);
23399
+ return {
23400
+ x: point.x * cosine + point.z * sine,
23401
+ y: point.y,
23402
+ z: -point.x * sine + point.z * cosine,
23403
+ };
23404
+ }
23405
+ /**
23406
+ * Rotates one point around the local X axis.
23407
+ *
23408
+ * @param point Source point.
23409
+ * @param angle Rotation angle in radians.
23410
+ * @returns Rotated point.
23411
+ *
23412
+ * @private helper of `minecraft2AvatarVisual`
23413
+ */
23414
+ function rotatePointAroundX(point, angle) {
23415
+ const cosine = Math.cos(angle);
23416
+ const sine = Math.sin(angle);
23417
+ return {
23418
+ x: point.x,
23419
+ y: point.y * cosine - point.z * sine,
23420
+ z: point.y * sine + point.z * cosine,
23421
+ };
23422
+ }
23423
+ /**
23424
+ * Subtracts one 3D point from another.
23425
+ *
23426
+ * @param leftPoint Left point.
23427
+ * @param rightPoint Right point.
23428
+ * @returns Difference vector.
23429
+ *
23430
+ * @private helper of `minecraft2AvatarVisual`
23431
+ */
23432
+ function subtractPoint3D(leftPoint, rightPoint) {
23433
+ return {
23434
+ x: leftPoint.x - rightPoint.x,
23435
+ y: leftPoint.y - rightPoint.y,
23436
+ z: leftPoint.z - rightPoint.z,
23437
+ };
23438
+ }
23439
+ /**
23440
+ * Computes the 3D cross product of two vectors.
23441
+ *
23442
+ * @param leftVector Left vector.
23443
+ * @param rightVector Right vector.
23444
+ * @returns Cross product.
23445
+ *
23446
+ * @private helper of `minecraft2AvatarVisual`
23447
+ */
23448
+ function crossProduct3D(leftVector, rightVector) {
23449
+ return {
23450
+ x: leftVector.y * rightVector.z - leftVector.z * rightVector.y,
23451
+ y: leftVector.z * rightVector.x - leftVector.x * rightVector.z,
23452
+ z: leftVector.x * rightVector.y - leftVector.y * rightVector.x,
23453
+ };
23454
+ }
23455
+ /**
23456
+ * Computes the 3D dot product of two vectors.
23457
+ *
23458
+ * @param leftVector Left vector.
23459
+ * @param rightVector Right vector.
23460
+ * @returns Dot product.
23461
+ *
23462
+ * @private helper of `minecraft2AvatarVisual`
23463
+ */
23464
+ function dotProduct3D(leftVector, rightVector) {
23465
+ return leftVector.x * rightVector.x + leftVector.y * rightVector.y + leftVector.z * rightVector.z;
23466
+ }
23467
+ /**
23468
+ * Normalizes one 3D vector while keeping zero vectors stable.
23469
+ *
23470
+ * @param vector Source vector.
23471
+ * @returns Normalized vector.
23472
+ *
23473
+ * @private helper of `minecraft2AvatarVisual`
23474
+ */
23475
+ function normalizeVector3(vector) {
23476
+ const length = Math.hypot(vector.x, vector.y, vector.z);
23477
+ if (length === 0) {
23478
+ return vector;
22868
23479
  }
22869
- for (let rowIndex = 2; rowIndex < 8; rowIndex++) {
22870
- texture[rowIndex][3] = stripeColor;
22871
- texture[rowIndex][4] = stripeColor;
23480
+ return {
23481
+ x: vector.x / length,
23482
+ y: vector.y / length,
23483
+ z: vector.z / length,
23484
+ };
23485
+ }
23486
+ /**
23487
+ * Clamps one number into the provided range.
23488
+ *
23489
+ * @param value Input value.
23490
+ * @param minimumValue Inclusive lower bound.
23491
+ * @param maximumValue Inclusive upper bound.
23492
+ * @returns Clamped value.
23493
+ *
23494
+ * @private helper of `minecraft2AvatarVisual`
23495
+ */
23496
+ function clampNumber$1(value, minimumValue, maximumValue) {
23497
+ return Math.min(maximumValue, Math.max(minimumValue, value));
23498
+ }
23499
+ /**
23500
+ * Measures the perimeter of one projected quad.
23501
+ *
23502
+ * @param corners Quad corners.
23503
+ * @returns Perimeter length.
23504
+ *
23505
+ * @private helper of `minecraft2AvatarVisual`
23506
+ */
23507
+ function getProjectedQuadPerimeter(corners) {
23508
+ let perimeter = 0;
23509
+ for (let cornerIndex = 0; cornerIndex < corners.length; cornerIndex++) {
23510
+ const currentCorner = corners[cornerIndex];
23511
+ const nextCorner = corners[(cornerIndex + 1) % corners.length];
23512
+ perimeter += Math.hypot(nextCorner.x - currentCorner.x, nextCorner.y - currentCorner.y);
22872
23513
  }
22873
- texture[4][1] = palette.accent;
22874
- texture[4][6] = palette.accent;
22875
- texture[5][2] = palette.highlight;
22876
- texture[5][5] = palette.highlight;
22877
- return texture;
23514
+ return perimeter;
22878
23515
  }
22879
23516
 
22880
23517
  /* eslint-disable no-magic-numbers */
@@ -24479,6 +25116,7 @@ const AVATAR_VISUALS = [
24479
25116
  octopus3AvatarVisual,
24480
25117
  asciiOctopusAvatarVisual,
24481
25118
  minecraftAvatarVisual,
25119
+ minecraft2AvatarVisual,
24482
25120
  fractalAvatarVisual,
24483
25121
  orbAvatarVisual,
24484
25122
  ];
@@ -26687,7 +27325,10 @@ function buildTeamSystemMessageBody(teamEntries) {
26687
27325
  */
26688
27326
  function buildTeamToolDescription(entry) {
26689
27327
  const detailLines = collectTeamEntryDetails(entry).map(({ label, content }) => `${label}: ${content}`);
26690
- return [`Consult teammate ${entry.teammate.label}`, ...detailLines].join('\n');
27328
+ return spaceTrim$1((block) => `
27329
+ Consult teammate ${entry.teammate.label}
27330
+ ${block(detailLines.join('\n'))}
27331
+ `);
26691
27332
  }
26692
27333
  /**
26693
27334
  * Collects structured teammate details that should stay visible to the model.
@@ -32184,7 +32825,10 @@ function createListedTimeoutsAssistantMessage(options) {
32184
32825
  if (hiddenCount > 0) {
32185
32826
  summaryRows.push(`...and ${hiddenCount} more.`);
32186
32827
  }
32187
- return [`Found ${options.total} ${options.total === 1 ? 'timeout' : 'timeouts'}:`, ...summaryRows].join('\n');
32828
+ return spaceTrim$1((block) => `
32829
+ Found ${options.total} ${options.total === 1 ? 'timeout' : 'timeouts'}:
32830
+ ${block(summaryRows.join('\n'))}
32831
+ `);
32188
32832
  }
32189
32833
  /**
32190
32834
  * Formats one timeout row for assistant-visible timeout listings.
@@ -37175,8 +37819,10 @@ async function renderMarkdownServerIndex(runtime) {
37175
37819
  **Application mode:** ${serverInfo.isApplicationModeAllowed ? 'enabled' : 'disabled'}
37176
37820
  ${block(!runtime.configuration.isApplicationModeAllowed || runtime.configuration.collection === null
37177
37821
  ? ''
37178
- : '**Pipelines in collection:**\n' +
37179
- serverInfo.pipelines.map((pipelineUrl) => `- ${pipelineUrl}`).join('\n'))}
37822
+ : spaceTrim$1((nestedBlock) => `
37823
+ **Pipelines in collection:**
37824
+ ${nestedBlock(serverInfo.pipelines.map((pipelineUrl) => `- ${pipelineUrl}`).join('\n'))}
37825
+ `))}
37180
37826
  **Running executions:** ${serverInfo.runningExecutions}
37181
37827
 
37182
37828
  ---
@@ -50905,7 +51551,7 @@ function stringifyUnknownError$2(error) {
50905
51551
  */
50906
51552
  async function commitChanges(message, options) {
50907
51553
  const projectPath = process.cwd();
50908
- const commitMessagePath = join(projectPath, '.tmp', 'codex-prompts', `COMMIT_MESSAGE_${Date.now()}.txt`);
51554
+ const commitMessagePath = resolvePromptbookTempPath(projectPath, 'scripts', 'codex-prompts', `COMMIT_MESSAGE_${Date.now()}.txt`);
50909
51555
  await mkdir(dirname(commitMessagePath), { recursive: true });
50910
51556
  await writeFile(commitMessagePath, message, 'utf-8');
50911
51557
  try {
@@ -51873,17 +52519,18 @@ function buildGitHubCopilotScript(options) {
51873
52519
 
51874
52520
  unset GITHUB_TOKEN
51875
52521
 
51876
- copilot -p "$(cat <<'${delimiter}'
51877
-
51878
- ${block(options.prompt)}
51879
-
51880
- ${delimiter}
51881
- )" \\
52522
+ # Avoid passing the prompt as one CLI argument because large agent prompts can exceed Windows/MSYS limits.
52523
+ copilot \\
51882
52524
  --yolo \\
51883
52525
  --no-ask-user \\
51884
52526
  --no-color \\
51885
52527
  --output-format json \\
51886
- --stream off${modelArgument}${thinkingLevelArgument}
52528
+ --stream off${modelArgument}${thinkingLevelArgument} \\
52529
+ <<'${delimiter}'
52530
+
52531
+ ${block(options.prompt)}
52532
+
52533
+ ${delimiter}
51887
52534
  `);
51888
52535
  }
51889
52536
 
@@ -53162,148 +53809,14 @@ function buildCoderRunOctopusVisual(options) {
53162
53809
  .map((line) => centerAnsiText(line, options.totalWidth));
53163
53810
  }
53164
53811
 
53165
- /**
53166
- * Refresh cadence used only while the rich coder UI needs animated updates.
53167
- *
53168
- * @private internal constant of coder run UI
53169
- */
53170
- const ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS = 300;
53171
- /**
53172
- * Phases that still benefit from automatic refreshes because the frame can change
53173
- * over time even without new runner output.
53174
- *
53175
- * @private internal constant of coder run UI
53176
- */
53177
- const AUTO_REFRESH_PHASES = ['initializing', 'loading', 'running', 'verifying'];
53178
- /**
53179
- * Returns whether the rich coder UI should keep animating on its own.
53180
- *
53181
- * @private internal utility of coder run UI
53182
- */
53183
- function isCoderRunUiAutoRefreshing(phase, pauseState) {
53184
- // `PAUSING` still means the current task is winding down, so keep active
53185
- // animations/timers running until the runner reaches the fully paused state.
53186
- if (pauseState === 'PAUSED') {
53187
- return false;
53188
- }
53189
- return AUTO_REFRESH_PHASES.includes(phase);
53190
- }
53191
- /**
53192
- * Returns the automatic refresh interval for the current UI state.
53193
- *
53194
- * Waiting, paused, and completed states return `undefined` so the rich UI stays
53195
- * perfectly still until actual state changes arrive.
53196
- *
53197
- * @private internal utility of coder run UI
53198
- */
53199
- function getCoderRunUiAutoRefreshInterval(phase, pauseState) {
53200
- return isCoderRunUiAutoRefreshing(phase, pauseState) ? ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS : undefined;
53201
- }
53202
-
53203
53812
  /**
53204
53813
  * Maximum number of output lines reserved for agent output in the UI.
53205
53814
  */
53206
53815
  const MAX_VISIBLE_OUTPUT_LINES = 8;
53207
- /**
53208
- * Minimum width used for the rich coder-run frame.
53209
- */
53210
- const MIN_FRAME_WIDTH = 56;
53211
- /**
53212
- * Maximum width used for the rich coder-run frame.
53213
- */
53214
- const MAX_FRAME_WIDTH = 96;
53215
53816
  /**
53216
53817
  * Visible width reserved for aligned labels in the session box.
53217
53818
  */
53218
53819
  const SESSION_LABEL_WIDTH = 8;
53219
- /**
53220
- * Builds the complete boxed terminal frame for the rich `ptbk coder run` UI.
53221
- */
53222
- function buildCoderRunUiFrame(options) {
53223
- const totalWidth = Math.max(MIN_FRAME_WIDTH, Math.min(options.terminalWidth, MAX_FRAME_WIDTH));
53224
- const isPromptActive = options.phase === 'running' || options.phase === 'verifying' || options.phase === 'loading';
53225
- const promptStatusPrefix = isPromptActive ? `${colors.yellow(`${options.spinner} `)}` : '';
53226
- const octopusAnimationFrame = isCoderRunUiAutoRefreshing(options.phase, options.pauseState)
53227
- ? options.animationFrame
53228
- : 0;
53229
- const pausePresentation = buildPausePresentation(options.phase, options.pauseState, options.statusMessage);
53230
- const sessionLines = buildSessionLines(options, totalWidth, pausePresentation);
53231
- const currentTaskLines = options.currentPromptLabel
53232
- ? [
53233
- `${promptStatusPrefix}${colors.bold.white(fitPlainText(options.currentPromptLabel, totalWidth - 8))}`,
53234
- `Attempt ${options.currentAttempt}/${options.maxAttempts} · ${options.statusMessage}`,
53235
- ...options.detailLines.map((detailLine) => `• ${detailLine}`),
53236
- ]
53237
- : [options.statusMessage, ...options.detailLines.map((detailLine) => `• ${detailLine}`)];
53238
- const visibleOutputLines = buildVisibleOutputLines(options.agentOutputLines);
53239
- const controls = buildControlPills(pausePresentation.pauseControl, options.pendingEnterLabel).join(' ');
53240
- const frame = [
53241
- ...buildCoderRunOctopusVisual({ totalWidth, animationFrame: octopusAnimationFrame }),
53242
- '',
53243
- ...renderBox('Session', sessionLines, totalWidth, colors.yellow.bold),
53244
- ...renderBox(options.currentPromptLabel ? 'Current task' : 'Queue', currentTaskLines, totalWidth, colors.magenta.bold),
53245
- ...renderBox('Live output', visibleOutputLines, totalWidth, colors.green.bold),
53246
- ];
53247
- if (options.errors.length > 0) {
53248
- frame.push(...renderBox('Errors', options.errors.map((errorLine) => `${colors.red('✗')} ${errorLine}`), totalWidth, colors.red.bold));
53249
- }
53250
- frame.push(...renderBox('Controls', [controls], totalWidth, colors.white.bold));
53251
- return frame;
53252
- }
53253
- /**
53254
- * Builds the structured session lines that combine state, runner, queue, and timing metadata.
53255
- */
53256
- function buildSessionLines(options, totalWidth, pausePresentation) {
53257
- const bodyWidth = Math.max(10, totalWidth - 4);
53258
- return buildSessionRows(options, bodyWidth, pausePresentation).map((sessionRow) => buildLabeledSessionLine(sessionRow.label, sessionRow.value, bodyWidth));
53259
- }
53260
- /**
53261
- * Builds the session rows so the renderer can keep one consistent structure without duplicating labels.
53262
- */
53263
- function buildSessionRows(options, bodyWidth, pausePresentation) {
53264
- const runnerParts = [options.config.agentName || 'No agent selected'];
53265
- if (options.config.modelName) {
53266
- runnerParts.push(options.config.modelName);
53267
- }
53268
- if (options.config.thinkingLevel) {
53269
- runnerParts.push(`thinking ${options.config.thinkingLevel}`);
53270
- }
53271
- const configurationRows = [
53272
- ...buildOptionalSessionRow('Context', options.config.context),
53273
- ...buildOptionalSessionRow('Test', options.config.testCommand),
53274
- ];
53275
- return [
53276
- {
53277
- label: 'State',
53278
- value: `${pausePresentation.badge} ${pausePresentation.stateMessage}`,
53279
- },
53280
- {
53281
- label: 'Runner',
53282
- value: runnerParts.join(' · '),
53283
- },
53284
- ...configurationRows,
53285
- {
53286
- label: 'This run',
53287
- value: buildThisRunSummary(options.progress),
53288
- },
53289
- {
53290
- label: 'Backlog',
53291
- value: buildBacklogSummary(options.progress),
53292
- },
53293
- {
53294
- label: 'Scope',
53295
- value: buildScopeSummary(options.progress, options.config),
53296
- },
53297
- {
53298
- label: 'Timing',
53299
- value: buildTimingSummary(options.progress),
53300
- },
53301
- {
53302
- label: 'Progress',
53303
- value: buildProgressBar$1(options.progress.percentage, bodyWidth - SESSION_LABEL_WIDTH - 1, `${options.progress.percentage}% complete (${options.progress.sessionDone}/${options.progress.sessionTotal} done)`),
53304
- },
53305
- ];
53306
- }
53307
53820
  /**
53308
53821
  * Builds the fixed-height live output section so streaming updates do not keep resizing the frame.
53309
53822
  */
@@ -53339,50 +53852,6 @@ function buildLabeledSessionLine(label, value, bodyWidth) {
53339
53852
  const formattedLabel = colors.gray(label.padEnd(SESSION_LABEL_WIDTH));
53340
53853
  return `${formattedLabel} ${fitAnsiText(value, bodyWidth - SESSION_LABEL_WIDTH - 1)}`;
53341
53854
  }
53342
- /**
53343
- * Builds zero or one structured session row for optional metadata.
53344
- */
53345
- function buildOptionalSessionRow(label, value) {
53346
- if (!value) {
53347
- return [];
53348
- }
53349
- return [{ label, value }];
53350
- }
53351
- /**
53352
- * Builds the active-session summary shown in the session box.
53353
- */
53354
- function buildThisRunSummary(progress) {
53355
- if (progress.sessionTotal === 0) {
53356
- return 'No runnable prompts in current scope';
53357
- }
53358
- return `Task ${progress.currentPromptIndex}/${progress.sessionTotal} · ${progress.sessionDone} done · ${progress.sessionRemaining} left`;
53359
- }
53360
- /**
53361
- * Builds the backlog/filter summary shown in the session box.
53362
- */
53363
- function buildBacklogSummary(progress) {
53364
- const parts = [`Repo ${progress.totalPrompts} total`];
53365
- if (progress.skippedPrompts > 0) {
53366
- parts.push(`${formatPromptCount$1(progress.skippedPrompts)} below priority`);
53367
- }
53368
- return parts.join(' · ');
53369
- }
53370
- /**
53371
- * Builds the priority/write-order summary shown in the session box.
53372
- */
53373
- function buildScopeSummary(progress, config) {
53374
- const parts = [`Priority ≥${config.priority}`];
53375
- if (progress.toBeWrittenPrompts > 0) {
53376
- parts.push(`Write ${formatPromptCount$1(progress.toBeWrittenPrompts)} first`);
53377
- }
53378
- return parts.join(' · ');
53379
- }
53380
- /**
53381
- * Builds the elapsed/estimate summary shown in the session box.
53382
- */
53383
- function buildTimingSummary(progress) {
53384
- return `Elapsed ${progress.elapsedText} · Total ${progress.estimatedTotalText} · ETA ${progress.estimatedLabel}`;
53385
- }
53386
53855
  /**
53387
53856
  * Builds the colored phase badge shown in the session box.
53388
53857
  */
@@ -53407,6 +53876,28 @@ function buildPausePresentation(phase, pauseState, statusMessage) {
53407
53876
  pauseControl: colors.bgYellow.black(' P ') + colors.white(' Pause'),
53408
53877
  };
53409
53878
  }
53879
+ /**
53880
+ * Builds the progress bar shown in the session box.
53881
+ */
53882
+ function buildProgressBar$1(percentage, availableWidth, label) {
53883
+ const percentageLabel = label;
53884
+ const barWidth = Math.max(10, availableWidth - percentageLabel.length - 1);
53885
+ const filledWidth = Math.round((percentage / 100) * barWidth);
53886
+ const emptyWidth = Math.max(0, barWidth - filledWidth);
53887
+ return `${colors.green('█'.repeat(filledWidth))}${colors.blue('░'.repeat(emptyWidth))} ${percentageLabel}`;
53888
+ }
53889
+ /**
53890
+ * Builds the control pills shown in the footer box.
53891
+ */
53892
+ function buildControlPills(pauseControl, pendingEnterLabel) {
53893
+ const pills = [];
53894
+ if (pendingEnterLabel) {
53895
+ pills.push(colors.bgWhite.black(' ENTER ') + colors.white(` ${pendingEnterLabel}`));
53896
+ }
53897
+ pills.push(pauseControl);
53898
+ pills.push(colors.bgRed.white(' CTRL+C ') + colors.white(' Exit'));
53899
+ return pills;
53900
+ }
53410
53901
  /**
53411
53902
  * Builds the active phase badge shown in the session box while the runner is not paused.
53412
53903
  */
@@ -53431,33 +53922,190 @@ function buildRunningPhaseBadge(phase) {
53431
53922
  return colors.bgWhite.black(' READY ');
53432
53923
  }
53433
53924
  }
53925
+
53434
53926
  /**
53435
- * Builds the progress bar shown in the session box.
53927
+ * Refresh cadence used only while the rich coder UI needs animated updates.
53928
+ *
53929
+ * @private internal constant of coder run UI
53436
53930
  */
53437
- function buildProgressBar$1(percentage, availableWidth, label) {
53438
- const percentageLabel = label;
53439
- const barWidth = Math.max(10, availableWidth - percentageLabel.length - 1);
53440
- const filledWidth = Math.round((percentage / 100) * barWidth);
53441
- const emptyWidth = Math.max(0, barWidth - filledWidth);
53442
- return `${colors.green('█'.repeat(filledWidth))}${colors.blue('░'.repeat(emptyWidth))} ${percentageLabel}`;
53931
+ const ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS = 300;
53932
+ /**
53933
+ * Phases that still benefit from automatic refreshes because the frame can change
53934
+ * over time even without new runner output.
53935
+ *
53936
+ * @private internal constant of coder run UI
53937
+ */
53938
+ const AUTO_REFRESH_PHASES = ['initializing', 'loading', 'running', 'verifying'];
53939
+ /**
53940
+ * Returns whether the rich coder UI should keep animating on its own.
53941
+ *
53942
+ * @private internal utility of coder run UI
53943
+ */
53944
+ function isCoderRunUiAutoRefreshing(phase, pauseState) {
53945
+ // `PAUSING` still means the current task is winding down, so keep active
53946
+ // animations/timers running until the runner reaches the fully paused state.
53947
+ if (pauseState === 'PAUSED') {
53948
+ return false;
53949
+ }
53950
+ return AUTO_REFRESH_PHASES.includes(phase);
53443
53951
  }
53444
53952
  /**
53445
- * Formats a prompt count with singular/plural wording.
53953
+ * Returns the automatic refresh interval for the current UI state.
53954
+ *
53955
+ * Waiting, paused, and completed states return `undefined` so the rich UI stays
53956
+ * perfectly still until actual state changes arrive.
53957
+ *
53958
+ * @private internal utility of coder run UI
53446
53959
  */
53447
- function formatPromptCount$1(count) {
53448
- return `${count} prompt${count === 1 ? '' : 's'}`;
53960
+ function getCoderRunUiAutoRefreshInterval(phase, pauseState) {
53961
+ return isCoderRunUiAutoRefreshing(phase, pauseState) ? ACTIVE_CODER_RUN_UI_REFRESH_INTERVAL_MS : undefined;
53449
53962
  }
53963
+
53450
53964
  /**
53451
- * Builds the control pills shown in the footer box.
53965
+ * Minimum width used for the rich coder-run frame.
53452
53966
  */
53453
- function buildControlPills(pauseControl, pendingEnterLabel) {
53454
- const pills = [];
53455
- if (pendingEnterLabel) {
53456
- pills.push(colors.bgWhite.black(' ENTER ') + colors.white(` ${pendingEnterLabel}`));
53967
+ const MIN_FRAME_WIDTH$1 = 56;
53968
+ /**
53969
+ * Maximum width used for the rich coder-run frame.
53970
+ */
53971
+ const MAX_FRAME_WIDTH$1 = 96;
53972
+ /**
53973
+ * Builds the complete boxed terminal frame for the rich `ptbk coder run` UI.
53974
+ */
53975
+ function buildCoderRunUiFrame(options) {
53976
+ const totalWidth = Math.max(MIN_FRAME_WIDTH$1, Math.min(options.terminalWidth, MAX_FRAME_WIDTH$1));
53977
+ const isPromptActive = options.phase === 'running' || options.phase === 'verifying' || options.phase === 'loading';
53978
+ const promptStatusPrefix = isPromptActive ? `${colors.yellow(`${options.spinner} `)}` : '';
53979
+ const octopusAnimationFrame = isCoderRunUiAutoRefreshing(options.phase, options.pauseState)
53980
+ ? options.animationFrame
53981
+ : 0;
53982
+ const pausePresentation = buildPausePresentation(options.phase, options.pauseState, options.statusMessage);
53983
+ const sessionLines = buildSessionLines$1(options, totalWidth, pausePresentation);
53984
+ const currentTaskLines = options.currentPromptLabel
53985
+ ? [
53986
+ `${promptStatusPrefix}${colors.bold.white(fitPlainText(options.currentPromptLabel, totalWidth - 8))}`,
53987
+ `Attempt ${options.currentAttempt}/${options.maxAttempts} · ${options.statusMessage}`,
53988
+ ...options.detailLines.map((detailLine) => `• ${detailLine}`),
53989
+ ]
53990
+ : [options.statusMessage, ...options.detailLines.map((detailLine) => `• ${detailLine}`)];
53991
+ const visibleOutputLines = buildVisibleOutputLines(options.agentOutputLines);
53992
+ const controls = buildControlPills(pausePresentation.pauseControl, options.pendingEnterLabel).join(' ');
53993
+ const frame = [
53994
+ ...buildCoderRunOctopusVisual({ totalWidth, animationFrame: octopusAnimationFrame }),
53995
+ '',
53996
+ ...renderBox('Session', sessionLines, totalWidth, colors.yellow.bold),
53997
+ ...renderBox(options.currentPromptLabel ? 'Current task' : 'Queue', currentTaskLines, totalWidth, colors.magenta.bold),
53998
+ ...renderBox('Live output', visibleOutputLines, totalWidth, colors.green.bold),
53999
+ ];
54000
+ if (options.errors.length > 0) {
54001
+ frame.push(...renderBox('Errors', options.errors.map((errorLine) => `${colors.red('✗')} ${errorLine}`), totalWidth, colors.red.bold));
53457
54002
  }
53458
- pills.push(pauseControl);
53459
- pills.push(colors.bgRed.white(' CTRL+C ') + colors.white(' Exit'));
53460
- return pills;
54003
+ frame.push(...renderBox('Controls', [controls], totalWidth, colors.white.bold));
54004
+ return frame;
54005
+ }
54006
+ /**
54007
+ * Builds the structured session lines that combine state, runner, queue, and timing metadata.
54008
+ */
54009
+ function buildSessionLines$1(options, totalWidth, pausePresentation) {
54010
+ const bodyWidth = Math.max(10, totalWidth - 4);
54011
+ return buildSessionRows(options, bodyWidth, pausePresentation).map((sessionRow) => buildLabeledSessionLine(sessionRow.label, sessionRow.value, bodyWidth));
54012
+ }
54013
+ /**
54014
+ * Builds the session rows so the renderer can keep one consistent structure without duplicating labels.
54015
+ */
54016
+ function buildSessionRows(options, bodyWidth, pausePresentation) {
54017
+ const runnerParts = [options.config.agentName || 'No agent selected'];
54018
+ if (options.config.modelName) {
54019
+ runnerParts.push(options.config.modelName);
54020
+ }
54021
+ if (options.config.thinkingLevel) {
54022
+ runnerParts.push(`thinking ${options.config.thinkingLevel}`);
54023
+ }
54024
+ const configurationRows = [
54025
+ ...buildOptionalSessionRow('Context', options.config.context),
54026
+ ...buildOptionalSessionRow('Test', options.config.testCommand),
54027
+ ];
54028
+ return [
54029
+ {
54030
+ label: 'State',
54031
+ value: `${pausePresentation.badge} ${pausePresentation.stateMessage}`,
54032
+ },
54033
+ {
54034
+ label: 'Runner',
54035
+ value: runnerParts.join(' · '),
54036
+ },
54037
+ ...configurationRows,
54038
+ {
54039
+ label: 'This run',
54040
+ value: buildThisRunSummary(options.progress),
54041
+ },
54042
+ {
54043
+ label: 'Backlog',
54044
+ value: buildBacklogSummary(options.progress),
54045
+ },
54046
+ {
54047
+ label: 'Scope',
54048
+ value: buildScopeSummary(options.progress, options.config),
54049
+ },
54050
+ {
54051
+ label: 'Timing',
54052
+ value: buildTimingSummary(options.progress),
54053
+ },
54054
+ {
54055
+ label: 'Progress',
54056
+ value: buildProgressBar$1(options.progress.percentage, bodyWidth - SESSION_LABEL_WIDTH - 1, `${options.progress.percentage}% complete (${options.progress.sessionDone}/${options.progress.sessionTotal} done)`),
54057
+ },
54058
+ ];
54059
+ }
54060
+ /**
54061
+ * Builds zero or one structured session row for optional metadata.
54062
+ */
54063
+ function buildOptionalSessionRow(label, value) {
54064
+ if (!value) {
54065
+ return [];
54066
+ }
54067
+ return [{ label, value }];
54068
+ }
54069
+ /**
54070
+ * Builds the active-session summary shown in the session box.
54071
+ */
54072
+ function buildThisRunSummary(progress) {
54073
+ if (progress.sessionTotal === 0) {
54074
+ return 'No runnable prompts in current scope';
54075
+ }
54076
+ return `Task ${progress.currentPromptIndex}/${progress.sessionTotal} · ${progress.sessionDone} done · ${progress.sessionRemaining} left`;
54077
+ }
54078
+ /**
54079
+ * Builds the backlog/filter summary shown in the session box.
54080
+ */
54081
+ function buildBacklogSummary(progress) {
54082
+ const parts = [`Repo ${progress.totalPrompts} total`];
54083
+ if (progress.skippedPrompts > 0) {
54084
+ parts.push(`${formatPromptCount$1(progress.skippedPrompts)} below priority`);
54085
+ }
54086
+ return parts.join(' · ');
54087
+ }
54088
+ /**
54089
+ * Builds the priority/write-order summary shown in the session box.
54090
+ */
54091
+ function buildScopeSummary(progress, config) {
54092
+ const parts = [`Priority ≥${config.priority}`];
54093
+ if (progress.toBeWrittenPrompts > 0) {
54094
+ parts.push(`Write ${formatPromptCount$1(progress.toBeWrittenPrompts)} first`);
54095
+ }
54096
+ return parts.join(' · ');
54097
+ }
54098
+ /**
54099
+ * Builds the elapsed/estimate summary shown in the session box.
54100
+ */
54101
+ function buildTimingSummary(progress) {
54102
+ return `Elapsed ${progress.elapsedText} · Total ${progress.estimatedTotalText} · ETA ${progress.estimatedLabel}`;
54103
+ }
54104
+ /**
54105
+ * Formats a prompt count with singular/plural wording.
54106
+ */
54107
+ function formatPromptCount$1(count) {
54108
+ return `${count} prompt${count === 1 ? '' : 's'}`;
53461
54109
  }
53462
54110
 
53463
54111
  /**
@@ -53600,6 +54248,7 @@ class CoderRunUiState extends EventEmitter {
53600
54248
  this.currentAttempt = 1;
53601
54249
  this.maxAttempts = 3;
53602
54250
  this.detailLines = [];
54251
+ this.messagePreviewLines = [];
53603
54252
  this.agentOutputLines = [];
53604
54253
  this.phase = 'initializing';
53605
54254
  this.statusMessage = 'Initializing...';
@@ -53653,6 +54302,7 @@ class CoderRunUiState extends EventEmitter {
53653
54302
  setCurrentPrompt(label) {
53654
54303
  this.currentPromptLabel = label;
53655
54304
  this.detailLines = [];
54305
+ this.messagePreviewLines = [];
53656
54306
  this.pendingEnterLabel = undefined;
53657
54307
  this.agentOutputLines = [];
53658
54308
  this.currentAttempt = 1;
@@ -53700,6 +54350,13 @@ class CoderRunUiState extends EventEmitter {
53700
54350
  this.detailLines = detailLines.filter((detailLine) => detailLine.trim() !== '');
53701
54351
  this.emitChange();
53702
54352
  }
54353
+ /**
54354
+ * Replaces the exact user-message preview lines shown in agent-specific panels.
54355
+ */
54356
+ setMessagePreviewLines(messagePreviewLines) {
54357
+ this.messagePreviewLines = [...messagePreviewLines];
54358
+ this.emitChange();
54359
+ }
53703
54360
  /**
53704
54361
  * Sets or clears the Enter-key action label shown in the controls panel.
53705
54362
  */
@@ -53757,8 +54414,9 @@ function getTerminalWidth() {
53757
54414
  *
53758
54415
  * @private internal entry point of coder run UI
53759
54416
  */
53760
- function renderCoderRunUi(startTime) {
54417
+ function renderCoderRunUi(startTime, options = {}) {
53761
54418
  const state = new CoderRunUiState(startTime);
54419
+ const buildFrameLinesFromState = options.buildFrameLines || buildCoderRunUiFrame;
53762
54420
  if (!process.stdout.isTTY) {
53763
54421
  return {
53764
54422
  state,
@@ -53895,7 +54553,7 @@ function renderCoderRunUi(startTime) {
53895
54553
  * Builds the current frame snapshot from the latest state.
53896
54554
  */
53897
54555
  function buildFrameLines() {
53898
- return buildCoderRunUiFrame({
54556
+ return buildFrameLinesFromState({
53899
54557
  terminalWidth: getTerminalWidth(),
53900
54558
  animationFrame: spinnerFrame,
53901
54559
  spinner: SPINNER_FRAMES[spinnerFrame],
@@ -53907,6 +54565,7 @@ function renderCoderRunUi(startTime) {
53907
54565
  maxAttempts: state.maxAttempts,
53908
54566
  statusMessage: state.statusMessage,
53909
54567
  detailLines: state.detailLines,
54568
+ messagePreviewLines: state.messagePreviewLines,
53910
54569
  pendingEnterLabel: state.pendingEnterLabel,
53911
54570
  agentOutputLines: state.agentOutputLines,
53912
54571
  errors: state.errors,
@@ -58987,7 +59646,7 @@ function buildAgentMessagePrompt(messageRelativePath) {
58987
59646
  */
58988
59647
  function buildAgentMessageScriptPath(projectPath, messageFile) {
58989
59648
  const scriptFileName = `${messageFile.fileName.replace(/\.[^.]+$/u, '')}.sh`;
58990
- return join(projectPath, '.tmp', 'agent-messages', scriptFileName);
59649
+ return resolvePromptbookTempPath(projectPath, 'scripts', 'agent-messages', scriptFileName);
58991
59650
  }
58992
59651
 
58993
59652
  /**
@@ -59043,6 +59702,242 @@ function isFileNotFoundError(error) {
59043
59702
  (error.code === 'ENOENT' || error.code === 'ENOTDIR'));
59044
59703
  }
59045
59704
 
59705
+ /**
59706
+ * Compact 3x5 block font used for agent-name initials in the terminal dashboard.
59707
+ */
59708
+ const BLOCK_FONT = {
59709
+ A: ['███', '█ █', '███', '█ █', '█ █'],
59710
+ B: ['██ ', '█ █', '██ ', '█ █', '██ '],
59711
+ C: ['███', '█ ', '█ ', '█ ', '███'],
59712
+ D: ['██ ', '█ █', '█ █', '█ █', '██ '],
59713
+ E: ['███', '█ ', '██ ', '█ ', '███'],
59714
+ F: ['███', '█ ', '██ ', '█ ', '█ '],
59715
+ G: ['███', '█ ', '█ ██', '█ █', '████'],
59716
+ H: ['█ █', '█ █', '███', '█ █', '█ █'],
59717
+ I: ['███', ' █ ', ' █ ', ' █ ', '███'],
59718
+ J: ['███', ' █', ' █', '█ █', '██ '],
59719
+ K: ['█ █', '█ █', '██ ', '█ █', '█ █'],
59720
+ L: ['█ ', '█ ', '█ ', '█ ', '███'],
59721
+ M: ['█ █', '███', '███', '█ █', '█ █'],
59722
+ N: ['█ █', '███', '███', '███', '█ █'],
59723
+ O: ['███', '█ █', '█ █', '█ █', '███'],
59724
+ P: ['███', '█ █', '███', '█ ', '█ '],
59725
+ Q: ['███', '█ █', '█ █', '███', ' █'],
59726
+ R: ['███', '█ █', '███', '██ ', '█ █'],
59727
+ S: ['███', '█ ', '███', ' █', '███'],
59728
+ T: ['███', ' █ ', ' █ ', ' █ ', ' █ '],
59729
+ U: ['█ █', '█ █', '█ █', '█ █', '███'],
59730
+ V: ['█ █', '█ █', '█ █', '█ █', ' █ '],
59731
+ W: ['█ █', '█ █', '███', '███', '█ █'],
59732
+ X: ['█ █', '█ █', ' █ ', '█ █', '█ █'],
59733
+ Y: ['█ █', '█ █', ' █ ', ' █ ', ' █ '],
59734
+ Z: ['███', ' █', ' █ ', '█ ', '███'],
59735
+ 0: ['███', '█ █', '█ █', '█ █', '███'],
59736
+ 1: [' ██', ' █', ' █', ' █', '███'],
59737
+ 2: ['███', ' █', '███', '█ ', '███'],
59738
+ 3: ['███', ' █', ' ██', ' █', '███'],
59739
+ 4: ['█ █', '█ █', '███', ' █', ' █'],
59740
+ 5: ['███', '█ ', '███', ' █', '███'],
59741
+ 6: ['███', '█ ', '███', '█ █', '███'],
59742
+ 7: ['███', ' █', ' █', ' █', ' █'],
59743
+ 8: ['███', '█ █', '███', '█ █', '███'],
59744
+ 9: ['███', '█ █', '███', ' █', '███'],
59745
+ };
59746
+ /**
59747
+ * Fallback glyph used when the initials contain unsupported characters.
59748
+ */
59749
+ const UNKNOWN_LETTER = ['███', ' █', ' ██', ' ', ' ██'];
59750
+ /**
59751
+ * Builds a compact centered initials banner for `ptbk agent run`.
59752
+ */
59753
+ function buildAgentRunInitialsVisual(agentName, totalWidth) {
59754
+ const initials = extractAgentInitials(agentName);
59755
+ const glyphRows = Array.from({ length: 5 }, () => '');
59756
+ for (const initial of initials) {
59757
+ const glyph = BLOCK_FONT[initial] || UNKNOWN_LETTER;
59758
+ for (let rowIndex = 0; rowIndex < glyph.length; rowIndex++) {
59759
+ glyphRows[rowIndex] = `${glyphRows[rowIndex]}${glyph[rowIndex]} `;
59760
+ }
59761
+ }
59762
+ const trimmedGlyphRows = glyphRows.map((glyphRow) => glyphRow.trimEnd());
59763
+ const visualWidth = trimmedGlyphRows.reduce((maxWidth, glyphRow) => Math.max(maxWidth, visibleLength(glyphRow)), 0);
59764
+ return trimmedGlyphRows.map((glyphRow, rowIndex) => {
59765
+ const coloredRow = rowIndex === 2 ? colors.cyan.bold(glyphRow) : rowIndex === 0 ? colors.blue.bold(glyphRow) : colors.white.bold(glyphRow);
59766
+ return centerAnsiText(padAnsiText(coloredRow, visualWidth), totalWidth);
59767
+ });
59768
+ }
59769
+ /**
59770
+ * Extracts readable initials from the local agent title.
59771
+ */
59772
+ function extractAgentInitials(agentName) {
59773
+ const normalizedAlphanumericName = agentName.replace(/[^A-Za-z0-9]/gu, '').toUpperCase();
59774
+ const words = agentName
59775
+ .trim()
59776
+ .split(/[^A-Za-z0-9]+/u)
59777
+ .filter(Boolean)
59778
+ .map((word) => word[0].toUpperCase());
59779
+ if (words.length > 1) {
59780
+ return words.slice(0, 3);
59781
+ }
59782
+ const fallbackLetters = normalizedAlphanumericName.slice(0, 2).split('');
59783
+ return fallbackLetters.length > 0 ? fallbackLetters : ['A'];
59784
+ }
59785
+
59786
+ /**
59787
+ * Minimum width used for the rich agent-run frame.
59788
+ */
59789
+ const MIN_FRAME_WIDTH = 56;
59790
+ /**
59791
+ * Maximum width used for the rich agent-run frame.
59792
+ */
59793
+ const MAX_FRAME_WIDTH = 96;
59794
+ /**
59795
+ * Maximum number of source-message lines shown in the dedicated preview box.
59796
+ */
59797
+ const MAX_MESSAGE_PREVIEW_LINES = 6;
59798
+ /**
59799
+ * Builds the complete boxed terminal frame for the rich `ptbk agent run` UI.
59800
+ */
59801
+ function buildAgentRunUiFrame(options) {
59802
+ const totalWidth = Math.max(MIN_FRAME_WIDTH, Math.min(options.terminalWidth, MAX_FRAME_WIDTH));
59803
+ const isPromptActive = options.phase === 'running' || options.phase === 'verifying' || options.phase === 'loading';
59804
+ const promptStatusPrefix = isPromptActive ? `${colors.yellow(`${options.spinner} `)}` : '';
59805
+ const pausePresentation = buildPausePresentation(options.phase, options.pauseState, options.statusMessage);
59806
+ const sessionLines = buildSessionLines(options, totalWidth, pausePresentation);
59807
+ const currentTaskLines = options.currentPromptLabel
59808
+ ? [
59809
+ `${promptStatusPrefix}${colors.bold.white(fitPlainText(options.currentPromptLabel, totalWidth - 8))}`,
59810
+ `Attempt ${options.currentAttempt}/${options.maxAttempts} · ${options.statusMessage}`,
59811
+ ...options.detailLines.map((detailLine) => `• ${detailLine}`),
59812
+ ]
59813
+ : [options.statusMessage, ...options.detailLines.map((detailLine) => `• ${detailLine}`)];
59814
+ const userMessageLines = buildUserMessagePreviewLines(options.messagePreviewLines, totalWidth);
59815
+ const visibleOutputLines = buildVisibleOutputLines(options.agentOutputLines);
59816
+ const controls = buildControlPills(pausePresentation.pauseControl, options.pendingEnterLabel).join(' ');
59817
+ const frame = [
59818
+ ...buildAgentRunInitialsVisual(options.config.localAgentName || 'Local Agent', totalWidth),
59819
+ '',
59820
+ ...renderBox('Session', sessionLines, totalWidth, colors.yellow.bold),
59821
+ ...renderBox('Current task', currentTaskLines, totalWidth, colors.magenta.bold),
59822
+ ...renderBox('User message', userMessageLines, totalWidth, colors.cyan.bold),
59823
+ ...renderBox('Live output', visibleOutputLines, totalWidth, colors.green.bold),
59824
+ ];
59825
+ if (options.errors.length > 0) {
59826
+ frame.push(...renderBox('Errors', options.errors.map((errorLine) => `${colors.red('✗')} ${errorLine}`), totalWidth, colors.red.bold));
59827
+ }
59828
+ frame.push(...renderBox('Controls', [controls], totalWidth, colors.white.bold));
59829
+ return frame;
59830
+ }
59831
+ /**
59832
+ * Builds the structured session lines for the agent-specific dashboard.
59833
+ */
59834
+ function buildSessionLines(options, totalWidth, pausePresentation) {
59835
+ const bodyWidth = Math.max(10, totalWidth - 4);
59836
+ const finishedMessages = options.progress.sessionDone;
59837
+ const queuedMessages = options.progress.sessionRemaining;
59838
+ const totalMessages = options.progress.sessionTotal;
59839
+ const localAgentName = options.config.localAgentName || 'Local Agent';
59840
+ const runnerParts = [options.config.agentName || 'No runner selected'];
59841
+ if (options.config.modelName) {
59842
+ runnerParts.push(options.config.modelName);
59843
+ }
59844
+ if (options.config.thinkingLevel) {
59845
+ runnerParts.push(`thinking ${options.config.thinkingLevel}`);
59846
+ }
59847
+ const sessionRows = [
59848
+ {
59849
+ label: 'State',
59850
+ value: `${pausePresentation.badge} ${pausePresentation.stateMessage}`,
59851
+ },
59852
+ {
59853
+ label: 'Agent',
59854
+ value: localAgentName,
59855
+ },
59856
+ {
59857
+ label: 'Runner',
59858
+ value: runnerParts.join(' · '),
59859
+ },
59860
+ {
59861
+ label: 'Queue',
59862
+ value: `${totalMessages} total · ${finishedMessages} finished · ${queuedMessages} queued`,
59863
+ },
59864
+ {
59865
+ label: 'Timing',
59866
+ value: `Elapsed ${options.progress.elapsedText} · Total ${options.progress.estimatedTotalText} · ETA ${options.progress.estimatedLabel}`,
59867
+ },
59868
+ {
59869
+ label: 'Progress',
59870
+ value: buildProgressBar$1(options.progress.percentage, bodyWidth - SESSION_LABEL_WIDTH - 1, `${options.progress.percentage}% complete (${finishedMessages}/${totalMessages || 0} finished)`),
59871
+ },
59872
+ ];
59873
+ return sessionRows.map((sessionRow) => buildLabeledSessionLine(sessionRow.label, sessionRow.value, bodyWidth));
59874
+ }
59875
+ /**
59876
+ * Fits the most recent user message into a fixed-height panel while preserving line breaks.
59877
+ */
59878
+ function buildUserMessagePreviewLines(messagePreviewLines, totalWidth) {
59879
+ const previewWidth = Math.max(10, totalWidth - 4);
59880
+ const rawLines = messagePreviewLines && messagePreviewLines.length > 0
59881
+ ? messagePreviewLines.map((messagePreviewLine) => messagePreviewLine.replace(/\t/gu, ' '))
59882
+ : ['No `MESSAGE @User` content found in the queued message.'];
59883
+ const visibleLines = rawLines.slice(0, MAX_MESSAGE_PREVIEW_LINES).map((line) => fitPlainText(line, previewWidth));
59884
+ if (rawLines.length > MAX_MESSAGE_PREVIEW_LINES) {
59885
+ visibleLines[MAX_MESSAGE_PREVIEW_LINES - 1] = fitPlainText(`${visibleLines[MAX_MESSAGE_PREVIEW_LINES - 1]} …`, previewWidth);
59886
+ }
59887
+ while (visibleLines.length < MAX_MESSAGE_PREVIEW_LINES) {
59888
+ visibleLines.push('');
59889
+ }
59890
+ return visibleLines;
59891
+ }
59892
+
59893
+ /**
59894
+ * Reads the local agent title and latest queued user message for the rich agent dashboard.
59895
+ */
59896
+ async function loadAgentRunUiMetadata(projectPath, queuedMessage) {
59897
+ const [localAgentName, queuedMessageContent] = await Promise.all([
59898
+ readLocalAgentName(projectPath),
59899
+ readFile(queuedMessage.absolutePath, 'utf-8'),
59900
+ ]);
59901
+ return {
59902
+ localAgentName,
59903
+ latestUserMessageLines: extractLatestUserMessageLines(queuedMessageContent),
59904
+ };
59905
+ }
59906
+ /**
59907
+ * Reads the local `agent.book` title and falls back to a stable generic name when unavailable.
59908
+ */
59909
+ async function readLocalAgentName(projectPath) {
59910
+ try {
59911
+ const agentSource = await readFile(join(projectPath, AGENT_BOOK_FILE_PATH), 'utf-8');
59912
+ return parseAgentSourceWithCommitments(agentSource).agentName || 'Local Agent';
59913
+ }
59914
+ catch (error) {
59915
+ if (error &&
59916
+ typeof error === 'object' &&
59917
+ 'code' in error &&
59918
+ (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
59919
+ return 'Local Agent';
59920
+ }
59921
+ throw error;
59922
+ }
59923
+ }
59924
+ /**
59925
+ * Extracts the latest `MESSAGE @User` block while preserving the original line breaks.
59926
+ */
59927
+ function extractLatestUserMessageLines(messageContent) {
59928
+ const messageBlockPattern = /^MESSAGE\s+@User\b[^\n]*\n([\s\S]*?)(?=^MESSAGE\s+@|(?![\s\S]))/gmu;
59929
+ let latestMatch = null;
59930
+ while (true) {
59931
+ const currentMatch = messageBlockPattern.exec(messageContent);
59932
+ if (!currentMatch) {
59933
+ break;
59934
+ }
59935
+ latestMatch = currentMatch;
59936
+ }
59937
+ const latestUserMessageContent = ((latestMatch === null || latestMatch === void 0 ? void 0 : latestMatch[1]) || messageContent).trim();
59938
+ return latestUserMessageContent.length > 0 ? latestUserMessageContent.split(/\r?\n/gu) : [];
59939
+ }
59940
+
59046
59941
  /**
59047
59942
  * Converts `ptbk agent` options into the shared runner option shape.
59048
59943
  */
@@ -59091,7 +59986,8 @@ function validateAgentRunOptions(options) {
59091
59986
  async function tickAgentMessages(options, tickOptions = {}) {
59092
59987
  validateAgentRunOptions(options);
59093
59988
  const projectPath = process.cwd();
59094
- let queuedMessage = await findNextQueuedMessage(projectPath);
59989
+ let queueSnapshot = await loadAgentMessageQueueSnapshot(projectPath);
59990
+ let queuedMessage = queueSnapshot.queuedMessages[0];
59095
59991
  if (!queuedMessage) {
59096
59992
  announceNoQueuedMessages(tickOptions);
59097
59993
  return { isMessageProcessed: false };
@@ -59100,7 +59996,8 @@ async function tickAgentMessages(options, tickOptions = {}) {
59100
59996
  await ensureCleanQueueIfNeeded(projectPath, options);
59101
59997
  console.info(colors.gray('Pulling latest changes before answering the next message...'));
59102
59998
  await pullLatestChanges();
59103
- queuedMessage = await findNextQueuedMessage(projectPath);
59999
+ queueSnapshot = await loadAgentMessageQueueSnapshot(projectPath);
60000
+ queuedMessage = queueSnapshot.queuedMessages[0];
59104
60001
  if (!queuedMessage) {
59105
60002
  announceNoQueuedMessages(tickOptions);
59106
60003
  return { isMessageProcessed: false };
@@ -59109,7 +60006,8 @@ async function tickAgentMessages(options, tickOptions = {}) {
59109
60006
  await ensureCleanQueueIfNeeded(projectPath, options);
59110
60007
  const sharedRunOptions = createCoderRunOptionsForAgent(options);
59111
60008
  const { runner, actualRunnerModel } = resolvePromptRunner(sharedRunOptions);
59112
- const uiHandle = createAgentRunUiHandle(options, runner, actualRunnerModel, queuedMessage);
60009
+ const agentUiMetadata = await loadAgentRunUiMetadata(projectPath, queuedMessage);
60010
+ const uiHandle = createAgentRunUiHandle(options, runner, actualRunnerModel, queuedMessage, queueSnapshot, agentUiMetadata);
59113
60011
  try {
59114
60012
  const finishedMessage = await runQueuedAgentMessage({
59115
60013
  projectPath,
@@ -59118,6 +60016,10 @@ async function tickAgentMessages(options, tickOptions = {}) {
59118
60016
  queuedMessage,
59119
60017
  uiHandle,
59120
60018
  });
60019
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.updateProgress(createAgentQueueProgressSnapshot({
60020
+ finishedMessageCount: queueSnapshot.finishedMessageCount + 1,
60021
+ queuedMessages: queueSnapshot.queuedMessages.slice(1),
60022
+ }));
59121
60023
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage('Message answered');
59122
60024
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setPhase('done');
59123
60025
  return {
@@ -59186,24 +60088,21 @@ async function runQueuedAgentMessage(options) {
59186
60088
  /**
59187
60089
  * Creates and seeds the rich terminal UI for an agent message run.
59188
60090
  */
59189
- function createAgentRunUiHandle(options, runner, actualRunnerModel, queuedMessage) {
60091
+ function createAgentRunUiHandle(options, runner, actualRunnerModel, queuedMessage, queueSnapshot, agentUiMetadata) {
59190
60092
  if (options.noUi || !process.stdout.isTTY) {
59191
60093
  return undefined;
59192
60094
  }
59193
- const uiHandle = renderCoderRunUi(moment());
60095
+ const uiHandle = renderCoderRunUi(moment(), { buildFrameLines: buildAgentRunUiFrame });
59194
60096
  uiHandle.state.setConfig({
59195
60097
  agentName: runner.name,
60098
+ localAgentName: agentUiMetadata.localAgentName,
59196
60099
  modelName: actualRunnerModel,
59197
60100
  thinkingLevel: options.thinkingLevel,
59198
60101
  priority: 0,
59199
60102
  });
59200
- uiHandle.state.updateProgress({
59201
- done: 0,
59202
- forAgent: 1,
59203
- belowMinimumPriority: 0,
59204
- toBeWritten: 0,
59205
- });
60103
+ uiHandle.state.updateProgress(createAgentQueueProgressSnapshot(queueSnapshot));
59206
60104
  uiHandle.state.setCurrentPrompt(queuedMessage.relativePath);
60105
+ uiHandle.state.setMessagePreviewLines([...agentUiMetadata.latestUserMessageLines]);
59207
60106
  uiHandle.state.setPhase('loading');
59208
60107
  uiHandle.state.setStatusMessage('Preparing message');
59209
60108
  return uiHandle;
@@ -59233,11 +60132,15 @@ function buildCommitIncludePaths(queuedMessage, finishedMessage, isQueuedMessage
59233
60132
  return [finishedMessage.relativePath];
59234
60133
  }
59235
60134
  /**
59236
- * Finds the next queued message or returns undefined when the queue is empty.
60135
+ * Converts agent queue counts into the prompt-style snapshot used by the shared rich UI state.
59237
60136
  */
59238
- async function findNextQueuedMessage(projectPath) {
59239
- const queuedMessages = await listQueuedAgentMessages(projectPath);
59240
- return queuedMessages[0];
60137
+ function createAgentQueueProgressSnapshot(queueSnapshot) {
60138
+ return {
60139
+ done: queueSnapshot.finishedMessageCount,
60140
+ forAgent: queueSnapshot.queuedMessages.length,
60141
+ belowMinimumPriority: 0,
60142
+ toBeWritten: 0,
60143
+ };
59241
60144
  }
59242
60145
  /**
59243
60146
  * Runs the clean working tree guard unless the user explicitly disabled it.
@@ -59278,6 +60181,38 @@ function announceNoQueuedMessages(options) {
59278
60181
  }
59279
60182
  console.info(colors.gray('No queued agent messages.'));
59280
60183
  }
60184
+ /**
60185
+ * Reads current queued and finished message counts for the agent dashboard.
60186
+ */
60187
+ async function loadAgentMessageQueueSnapshot(projectPath) {
60188
+ const [queuedMessages, finishedMessageCount] = await Promise.all([
60189
+ listQueuedAgentMessages(projectPath),
60190
+ countMarkdownFiles(join(projectPath, AGENT_FINISHED_MESSAGES_DIRECTORY_PATH)),
60191
+ ]);
60192
+ return {
60193
+ queuedMessages,
60194
+ finishedMessageCount,
60195
+ };
60196
+ }
60197
+ /**
60198
+ * Counts markdown files inside one queue directory and treats a missing directory as empty.
60199
+ */
60200
+ async function countMarkdownFiles(directoryPath) {
60201
+ try {
60202
+ const directoryEntries = await readdir(directoryPath, { withFileTypes: true });
60203
+ return directoryEntries.filter((directoryEntry) => directoryEntry.isFile() && /\.m(?:d|arkdown)$/iu.test(directoryEntry.name))
60204
+ .length;
60205
+ }
60206
+ catch (error) {
60207
+ if (error &&
60208
+ typeof error === 'object' &&
60209
+ 'code' in error &&
60210
+ (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
60211
+ return 0;
60212
+ }
60213
+ throw error;
60214
+ }
60215
+ }
59281
60216
 
59282
60217
  var tickAgentMessages$1 = /*#__PURE__*/Object.freeze({
59283
60218
  __proto__: null,
@@ -59406,11 +60341,11 @@ const DEFAULT_INCLUDE_GLOBS = ['**/*.{ts,tsx,js,jsx,json,md,txt}'];
59406
60341
  /**
59407
60342
  * Default ignored paths while scanning the repository.
59408
60343
  */
59409
- const DEFAULT_IGNORE_GLOBS = ['**/node_modules/**', '**/.git/**', '**/.promptbook/ptbk-coder/**'];
60344
+ const DEFAULT_IGNORE_GLOBS = ['**/node_modules/**', '**/.git/**', `**/${getPromptbookTempPosixPath('ptbk-coder')}/**`];
59410
60345
  /**
59411
60346
  * Directory used for Promptbook coder runtime caches.
59412
60347
  */
59413
- const PTBK_CODER_CACHE_DIRECTORY_PATH = '.promptbook/ptbk-coder';
60348
+ const PTBK_CODER_CACHE_DIRECTORY_PATH = getPromptbookTempPath('ptbk-coder');
59414
60349
  /**
59415
60350
  * Relative cache file path storing per-file emoji-tag scan results.
59416
60351
  */