@promptbook/cli 0.112.0-117 → 0.112.0-119

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 (90) hide show
  1. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatSidebarDefault.tsx +5 -6
  2. package/apps/agents-server/src/app/api/page-preview/check/route.ts +31 -0
  3. package/apps/agents-server/src/app/api/page-preview/screenshot/route.ts +57 -0
  4. package/apps/agents-server/src/app/api/upload/route.ts +10 -1
  5. package/apps/agents-server/src/app/s3/[first]/[second]/[hash]/[filename]/route.ts +52 -0
  6. package/apps/agents-server/src/database/$provideClientSql.ts +37 -0
  7. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +41 -0
  8. package/apps/agents-server/src/tools/$provideCdnForServer.ts +24 -0
  9. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +30 -2
  10. package/apps/agents-server/src/utils/cdn/utils/getUserFileCdnKey.ts +10 -3
  11. package/apps/agents-server/src/utils/externalChatRunner/processExternalUserChatJob.ts +17 -7
  12. package/apps/agents-server/src/utils/iframe/checkIfUrlCanBeEmbedded.ts +68 -0
  13. package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +17 -7
  14. package/apps/agents-server/src/utils/userChat/createImmediateUserChatAnswerModelRequirements.ts +11 -0
  15. package/apps/agents-server/src/utils/userChat/listUserChats.ts +5 -7
  16. package/esm/index.es.js +442 -66
  17. package/esm/index.es.js.map +1 -1
  18. package/esm/scripts/run-codex-prompts/common/parseDuration.d.ts +19 -0
  19. package/esm/src/_packages/node.index.d.ts +10 -0
  20. package/esm/src/book-3.0/BookNodeAgentSource.d.ts +1 -1
  21. package/esm/src/book-3.0/CliAgent.d.ts +7 -2
  22. package/esm/src/book-3.0/cliAgentEnv.d.ts +33 -0
  23. package/esm/src/book-components/BookEditor/BookEditor.d.ts +1 -1
  24. package/esm/src/book-components/BookEditor/BookEditorForClient.d.ts +1 -1
  25. package/esm/src/book-components/Chat/Chat/CitationIframePreview.d.ts +20 -0
  26. package/esm/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
  27. package/esm/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +1 -1
  28. package/esm/src/book-components/_common/Modal/Modal.d.ts +1 -1
  29. package/esm/src/book-components/icons/AboutIcon.d.ts +1 -1
  30. package/esm/src/book-components/icons/DownloadIcon.d.ts +1 -1
  31. package/esm/src/book-components/icons/ExitFullscreenIcon.d.ts +1 -1
  32. package/esm/src/book-components/icons/FullscreenIcon.d.ts +1 -1
  33. package/esm/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +2 -18
  34. package/esm/src/version.d.ts +1 -1
  35. package/package.json +1 -1
  36. package/src/_packages/node.index.ts +10 -0
  37. package/src/avatars/avatarAnimationScheduler.ts +33 -2
  38. package/src/avatars/visuals/fractalAvatarVisual.ts +5 -4
  39. package/src/avatars/visuals/minecraft2AvatarVisual.ts +16 -11
  40. package/src/avatars/visuals/minecraftAvatarVisual.ts +21 -7
  41. package/src/avatars/visuals/octopus3d2AvatarVisual.ts +69 -17
  42. package/src/avatars/visuals/octopus3d3AvatarVisual.ts +81 -18
  43. package/src/avatars/visuals/octopus3dAvatarVisual.ts +69 -17
  44. package/src/book-3.0/Book.ts +3 -1
  45. package/src/book-3.0/BookNodeAgentSource.ts +2 -2
  46. package/src/book-3.0/CliAgent.ts +84 -6
  47. package/src/book-3.0/LiteAgent.ts +1 -1
  48. package/src/book-3.0/cliAgentEnv.ts +46 -0
  49. package/src/book-components/BookEditor/BookEditor.tsx +6 -6
  50. package/src/book-components/BookEditor/BookEditorForClient.tsx +1 -1
  51. package/src/book-components/Chat/Chat/Chat.module.css +45 -0
  52. package/src/book-components/Chat/Chat/ChatCitationModal.tsx +2 -2
  53. package/src/book-components/Chat/Chat/CitationIframePreview.tsx +83 -0
  54. package/src/book-components/_common/Dropdown/Dropdown.tsx +1 -1
  55. package/src/book-components/_common/MenuHoisting/MenuHoistingContext.tsx +1 -1
  56. package/src/book-components/_common/Modal/Modal.tsx +1 -1
  57. package/src/book-components/icons/AboutIcon.tsx +1 -1
  58. package/src/book-components/icons/DownloadIcon.tsx +1 -1
  59. package/src/book-components/icons/ExitFullscreenIcon.tsx +1 -1
  60. package/src/book-components/icons/FullscreenIcon.tsx +1 -1
  61. package/src/cli/cli-commands/agents-server/buildAgentsServer.ts +31 -1
  62. package/src/cli/cli-commands/coder/run.ts +28 -3
  63. package/src/cli/cli-commands/common/promptRunnerCliOptions.ts +9 -29
  64. package/src/execution/createPipelineExecutor/getKnowledgeForTask.ts +1 -1
  65. package/src/llm-providers/openai/OpenAiAgentKitExecutionToolsToolBuilder.ts +1 -1
  66. package/src/llm-providers/openai/OpenAiAssistantExecutionToolsToolRunner.ts +1 -1
  67. package/src/llm-providers/openai/OpenAiVectorStoreKnowledgeSourcePreparer.ts +1 -1
  68. package/src/other/templates/getTemplatesPipelineCollection.ts +734 -711
  69. package/src/scripting/javascript/JavascriptEvalExecutionTools.ts +1 -1
  70. package/src/version.ts +2 -2
  71. package/src/versions.txt +2 -0
  72. package/umd/index.umd.js +442 -66
  73. package/umd/index.umd.js.map +1 -1
  74. package/umd/scripts/run-codex-prompts/common/parseDuration.d.ts +19 -0
  75. package/umd/src/_packages/node.index.d.ts +10 -0
  76. package/umd/src/book-3.0/BookNodeAgentSource.d.ts +1 -1
  77. package/umd/src/book-3.0/CliAgent.d.ts +7 -2
  78. package/umd/src/book-3.0/cliAgentEnv.d.ts +33 -0
  79. package/umd/src/book-components/BookEditor/BookEditor.d.ts +1 -1
  80. package/umd/src/book-components/BookEditor/BookEditorForClient.d.ts +1 -1
  81. package/umd/src/book-components/Chat/Chat/CitationIframePreview.d.ts +20 -0
  82. package/umd/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
  83. package/umd/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +1 -1
  84. package/umd/src/book-components/_common/Modal/Modal.d.ts +1 -1
  85. package/umd/src/book-components/icons/AboutIcon.d.ts +1 -1
  86. package/umd/src/book-components/icons/DownloadIcon.d.ts +1 -1
  87. package/umd/src/book-components/icons/ExitFullscreenIcon.d.ts +1 -1
  88. package/umd/src/book-components/icons/FullscreenIcon.d.ts +1 -1
  89. package/umd/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +2 -18
  90. package/umd/src/version.d.ts +1 -1
@@ -93,6 +93,63 @@ const LIGHT_DIRECTION: Point3D = normalizeVector3({
93
93
  */
94
94
  const OCTOPUS_TENTACLE_COUNT = 8;
95
95
 
96
+ /**
97
+ * Per-avatar stable state derived once from the seeded random factory and reused across frames.
98
+ *
99
+ * These values depend only on the avatar definition (name + hash + colors) and never change
100
+ * while the avatar is mounted, so computing them once and caching eliminates the largest
101
+ * allocation/computation spike in the hot render path.
102
+ *
103
+ * @private helper of `octopus3d3AvatarVisual`
104
+ */
105
+ type Octopus3d3StableState = {
106
+ readonly morphologyProfile: Octopus3MorphologyProfile;
107
+ readonly animationPhase: number;
108
+ readonly leftEyePhaseOffset: number;
109
+ readonly rightEyePhaseOffset: number;
110
+ readonly tentacleProfiles: ReadonlyArray<ContinuousOctopusTentacleProfile>;
111
+ };
112
+
113
+ /**
114
+ * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
115
+ * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
116
+ * React `useMemo`). Using a `WeakMap` ensures the entry is collected when the component unmounts.
117
+ *
118
+ * @private helper of `octopus3d3AvatarVisual`
119
+ */
120
+ const stableStateCache = new WeakMap<(salt: string) => () => number, Octopus3d3StableState>();
121
+
122
+ /**
123
+ * Returns the stable per-avatar state, computing it on first access and returning the cached
124
+ * result on every subsequent call within the same `<Avatar/>` mount.
125
+ *
126
+ * @private helper of `octopus3d3AvatarVisual`
127
+ */
128
+ function getOctopus3d3StableState(createRandom: (salt: string) => () => number): Octopus3d3StableState {
129
+ const cached = stableStateCache.get(createRandom);
130
+
131
+ if (cached !== undefined) {
132
+ return cached;
133
+ }
134
+
135
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
136
+ const animationRandom = createRandom('octopus3d3-animation-profile');
137
+ const eyeRandom = createRandom('octopus3d3-eye-profile');
138
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
139
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
140
+ const state: Octopus3d3StableState = {
141
+ morphologyProfile,
142
+ animationPhase: animationRandom() * Math.PI * 2,
143
+ leftEyePhaseOffset,
144
+ rightEyePhaseOffset,
145
+ tentacleProfiles: createContinuousTentacleProfiles(createRandom, morphologyProfile),
146
+ };
147
+
148
+ stableStateCache.set(createRandom, state);
149
+
150
+ return state;
151
+ }
152
+
96
153
  /**
97
154
  * Octopus 3D 3 avatar visual.
98
155
  *
@@ -106,11 +163,8 @@ export const octopus3d3AvatarVisual: AvatarVisualDefinition = {
106
163
  isAnimated: true,
107
164
  supportsPointerTracking: true,
108
165
  render({ context, size, palette, createRandom, timeMs, interaction }) {
109
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
110
- const animationRandom = createRandom('octopus3d3-animation-profile');
111
- const eyeRandom = createRandom('octopus3d3-eye-profile');
112
- const animationPhase = animationRandom() * Math.PI * 2;
113
- const tentacleProfiles = createContinuousTentacleProfiles(createRandom, morphologyProfile);
166
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles } =
167
+ getOctopus3d3StableState(createRandom);
114
168
  const sceneCenterX = size * 0.5;
115
169
  const sceneCenterY = size * 0.535;
116
170
  const bob = Math.sin(timeMs / 960 + animationPhase) * size * 0.012;
@@ -213,7 +267,7 @@ export const octopus3d3AvatarVisual: AvatarVisualDefinition = {
213
267
  size,
214
268
  palette,
215
269
  timeMs,
216
- animationPhase + eyeRandom() * 0.7,
270
+ animationPhase + leftEyePhaseOffset,
217
271
  interaction,
218
272
  morphologyProfile.face.eyeStyle,
219
273
  );
@@ -230,7 +284,7 @@ export const octopus3d3AvatarVisual: AvatarVisualDefinition = {
230
284
  size,
231
285
  palette,
232
286
  timeMs,
233
- animationPhase + 0.85 + eyeRandom() * 0.7,
287
+ animationPhase + 0.85 + rightEyePhaseOffset,
234
288
  interaction,
235
289
  morphologyProfile.face.eyeStyle,
236
290
  );
@@ -338,6 +392,9 @@ function drawContinuousOctopusAtmosphere(
338
392
  /**
339
393
  * Draws the soft lower shadow that anchors the octopus in the avatar frame.
340
394
  *
395
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
396
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
397
+ *
341
398
  * @private helper of `octopus3d3AvatarVisual`
342
399
  */
343
400
  function drawContinuousOctopusShadow(
@@ -351,19 +408,25 @@ function drawContinuousOctopusShadow(
351
408
  timeMs: number,
352
409
  morphologyProfile: Octopus3MorphologyProfile,
353
410
  ): void {
411
+ const cx = size * 0.5 + interaction.gazeX * size * 0.045;
412
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
413
+ const rx = size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02);
414
+ const ry = size * 0.06;
415
+
416
+ // Scale the context so that drawing a circle produces the correct ellipse aspect ratio,
417
+ // then fill with a radial gradient that approximates the blurry edge without context.filter.
354
418
  context.save();
355
- context.fillStyle = `${palette.shadow}66`;
356
- context.filter = `blur(${size * 0.025}px)`;
419
+ context.translate(cx, cy);
420
+ context.scale(1, ry / rx);
421
+ const blurRadius = rx * 1.4;
422
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
423
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
424
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
425
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
426
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
427
+ context.fillStyle = shadowGradient;
357
428
  context.beginPath();
358
- context.ellipse(
359
- size * 0.5 + interaction.gazeX * size * 0.045,
360
- size * 0.9 + Math.sin(timeMs / 980) * size * 0.007,
361
- size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02),
362
- size * 0.06,
363
- 0,
364
- 0,
365
- Math.PI * 2,
366
- );
429
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
367
430
  context.fill();
368
431
  context.restore();
369
432
  }
@@ -55,6 +55,53 @@ const LIGHT_DIRECTION: Point3D = normalizeVector3({
55
55
  z: 0.94,
56
56
  });
57
57
 
58
+ /**
59
+ * Per-avatar stable state derived once from the seeded random factory and reused across frames.
60
+ *
61
+ * @private helper of `octopus3dAvatarVisual`
62
+ */
63
+ type Octopus3dStableState = {
64
+ readonly morphologyProfile: Octopus3MorphologyProfile;
65
+ readonly animationPhase: number;
66
+ readonly leftEyePhaseOffset: number;
67
+ readonly rightEyePhaseOffset: number;
68
+ };
69
+
70
+ /**
71
+ * Cache keyed by the `createRandom` factory reference (stable per mounted `<Avatar/>`).
72
+ *
73
+ * @private helper of `octopus3dAvatarVisual`
74
+ */
75
+ const octopus3dStableStateCache = new WeakMap<(salt: string) => () => number, Octopus3dStableState>();
76
+
77
+ /**
78
+ * Returns the stable per-avatar state, computing it on first access and caching for subsequent frames.
79
+ *
80
+ * @private helper of `octopus3dAvatarVisual`
81
+ */
82
+ function getOctopus3dStableState(createRandom: (salt: string) => () => number): Octopus3dStableState {
83
+ const cached = octopus3dStableStateCache.get(createRandom);
84
+
85
+ if (cached !== undefined) {
86
+ return cached;
87
+ }
88
+
89
+ const animationRandom = createRandom('octopus3d-animation-profile');
90
+ const eyeRandom = createRandom('octopus3d-eye-profile');
91
+ const leftEyePhaseOffset = eyeRandom() * 0.6;
92
+ const rightEyePhaseOffset = eyeRandom() * 0.6;
93
+ const state: Octopus3dStableState = {
94
+ morphologyProfile: createOctopus3MorphologyProfile(createRandom),
95
+ animationPhase: animationRandom() * Math.PI * 2,
96
+ leftEyePhaseOffset,
97
+ rightEyePhaseOffset,
98
+ };
99
+
100
+ octopus3dStableStateCache.set(createRandom, state);
101
+
102
+ return state;
103
+ }
104
+
58
105
  /**
59
106
  * Proper 3D Octopus visual built from projected organic meshes and tentacles.
60
107
  *
@@ -67,10 +114,8 @@ export const octopus3dAvatarVisual: AvatarVisualDefinition = {
67
114
  isAnimated: true,
68
115
  supportsPointerTracking: true,
69
116
  render({ context, size, palette, createRandom, timeMs, interaction }) {
70
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
71
- const animationRandom = createRandom('octopus3d-animation-profile');
72
- const eyeRandom = createRandom('octopus3d-eye-profile');
73
- const animationPhase = animationRandom() * Math.PI * 2;
117
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } =
118
+ getOctopus3dStableState(createRandom);
74
119
  const sceneCenterX = size * 0.5;
75
120
  const sceneCenterY = size * 0.56;
76
121
  const bob = Math.sin(timeMs / 920 + animationPhase) * size * 0.014;
@@ -201,7 +246,7 @@ export const octopus3dAvatarVisual: AvatarVisualDefinition = {
201
246
  size,
202
247
  palette,
203
248
  timeMs,
204
- animationPhase + eyeRandom() * 0.6,
249
+ animationPhase + leftEyePhaseOffset,
205
250
  interaction,
206
251
  morphologyProfile.face.eyeStyle,
207
252
  );
@@ -228,7 +273,7 @@ export const octopus3dAvatarVisual: AvatarVisualDefinition = {
228
273
  size,
229
274
  palette,
230
275
  timeMs,
231
- animationPhase + 0.7 + eyeRandom() * 0.6,
276
+ animationPhase + 0.7 + rightEyePhaseOffset,
232
277
  interaction,
233
278
  morphologyProfile.face.eyeStyle,
234
279
  );
@@ -334,6 +379,9 @@ function drawOctopus3dAtmosphere(
334
379
  /**
335
380
  * Draws the soft ground shadow below the octopus.
336
381
  *
382
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
383
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
384
+ *
337
385
  * @private helper of `octopus3dAvatarVisual`
338
386
  */
339
387
  function drawOctopus3dShadow(
@@ -346,19 +394,23 @@ function drawOctopus3dShadow(
346
394
  },
347
395
  timeMs: number,
348
396
  ): void {
397
+ const cx = size * 0.5 + interaction.gazeX * size * 0.04;
398
+ const cy = size * 0.87 + Math.sin(timeMs / 920) * size * 0.008;
399
+ const rx = size * (0.18 + interaction.intensity * 0.02);
400
+ const ry = size * 0.06;
401
+
349
402
  context.save();
350
- context.fillStyle = `${palette.shadow}66`;
351
- context.filter = `blur(${size * 0.022}px)`;
403
+ context.translate(cx, cy);
404
+ context.scale(1, ry / rx);
405
+ const blurRadius = rx * 1.4;
406
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
407
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
408
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
409
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
410
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
411
+ context.fillStyle = shadowGradient;
352
412
  context.beginPath();
353
- context.ellipse(
354
- size * 0.5 + interaction.gazeX * size * 0.04,
355
- size * 0.87 + Math.sin(timeMs / 920) * size * 0.008,
356
- size * (0.18 + interaction.intensity * 0.02),
357
- size * 0.06,
358
- 0,
359
- 0,
360
- Math.PI * 2,
361
- );
413
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
362
414
  context.fill();
363
415
  context.restore();
364
416
  }
@@ -215,7 +215,9 @@ function parseBookMessageHeader(line: string): Pick<BookMessageBlock, 'marker' |
215
215
  * @private internal utility of `Book`
216
216
  */
217
217
  function parseCommitmentHeader(line: string): Commitment | null {
218
- const match = /^([A-Z][A-Z0-9]*(?: [A-Z0-9]+)*)(?:\s+(.*))?$/u.exec(line);
218
+ // Require at least 2 characters in the first keyword word to avoid treating common
219
+ // single-letter words (e.g. "V" in Czech, "I" or "A" in English) as commitment headers.
220
+ const match = /^([A-Z][A-Z0-9]+(?: [A-Z0-9]+)*)(?:\s+(.*))?$/u.exec(line);
219
221
  if (!match) {
220
222
  return null;
221
223
  }
@@ -23,7 +23,7 @@ export type BookNodeAgentSource = Book | string_book;
23
23
  */
24
24
  export type BookNodeAgentSourceOptions = {
25
25
  readonly agentPath?: string;
26
- readonly book?: BookNodeAgentSource;
26
+ readonly book?: string | BookNodeAgentSource;
27
27
  readonly currentWorkingDirectory?: string;
28
28
  };
29
29
 
@@ -92,7 +92,7 @@ export async function resolveBookNodeAgentSource(
92
92
  *
93
93
  * @private internal utility of `resolveBookNodeAgentSource`
94
94
  */
95
- function normalizeBookNodeAgentSource(book: BookNodeAgentSource): string_book {
95
+ function normalizeBookNodeAgentSource(book: string | BookNodeAgentSource): string_book {
96
96
  if (book instanceof Book) {
97
97
  return book.stringify();
98
98
  }
@@ -3,22 +3,30 @@ import { dirname } from 'path';
3
3
  import { executeAgentChatTurn } from '../../scripts/run-agent-chat/executeAgentChatTurn';
4
4
  import { NotAllowed } from '../errors/NotAllowed';
5
5
  import { resolvePromptbookTemporaryPath } from '../utils/filesystem/promptbookTemporaryPath';
6
+ import { spaceTrim } from '../utils/organization/spaceTrim';
6
7
  import type { BookNodeAgentSourceOptions, ResolvedBookNodeAgentSource } from './BookNodeAgentSource';
7
8
  import { resolveBookNodeAgentSource } from './BookNodeAgentSource';
9
+ import {
10
+ CLI_AGENT_HARNESS_NAMES,
11
+ CLI_AGENT_THINKING_LEVEL_VALUES,
12
+ PTBK_HARNESS_ENV,
13
+ PTBK_MODEL_ENV,
14
+ PTBK_THINKING_LEVEL_ENV,
15
+ } from './cliAgentEnv';
8
16
 
9
17
  /**
10
18
  * CLI harness names supported by `ptbk agent exec`.
11
19
  *
12
20
  * @public exported from `@promptbook/node`
13
21
  */
14
- export type CliAgentHarness = 'openai-codex' | 'github-copilot' | 'cline' | 'claude-code' | 'opencode' | 'gemini';
22
+ export type CliAgentHarness = (typeof CLI_AGENT_HARNESS_NAMES)[number];
15
23
 
16
24
  /**
17
25
  * Thinking levels supported by CLI coding harnesses.
18
26
  *
19
27
  * @public exported from `@promptbook/node`
20
28
  */
21
- export type CliAgentThinkingLevel = 'low' | 'medium' | 'high' | 'xhigh';
29
+ export type CliAgentThinkingLevel = (typeof CLI_AGENT_THINKING_LEVEL_VALUES)[number];
22
30
 
23
31
  /**
24
32
  * Per-run CLI options exposed by `CliAgent`.
@@ -31,6 +39,7 @@ export type CliAgentRunOptions = {
31
39
  readonly allowCredits?: boolean;
32
40
  readonly context?: string;
33
41
  readonly harness?: CliAgentHarness;
42
+ readonly isVerbose?: boolean;
34
43
  readonly model?: string;
35
44
  readonly noUi?: boolean;
36
45
  readonly thinkingLevel?: CliAgentThinkingLevel;
@@ -56,6 +65,9 @@ const DEFAULT_CLI_AGENT_IS_NO_UI = true;
56
65
  * It uses the same harnesses and execution path as `ptbk agent exec`, running the runner
57
66
  * in-process instead of spawning a separate CLI process.
58
67
  *
68
+ * When no `harness` is provided in the constructor or per-run options, `CliAgent` falls back
69
+ * to the `PTBK_HARNESS` environment variable, mirroring `ptbk agent exec` behavior.
70
+ *
59
71
  * @public exported from `@promptbook/node`
60
72
  */
61
73
  export class CliAgent {
@@ -81,15 +93,30 @@ export class CliAgent {
81
93
  const agentPath = await this.resolveExecutableAgentPath(resolvedSource);
82
94
  const mergedOptions = mergeCliAgentRunOptions(this.options, options);
83
95
 
96
+ const harness = mergedOptions.harness ?? resolveCliAgentHarnessFromEnv();
97
+
98
+ if (!harness) {
99
+ throw new NotAllowed(
100
+ spaceTrim(`
101
+ No harness specified for \`CliAgent\`. Pass \`harness\` in the constructor options or per-run options,
102
+ or set the \`${PTBK_HARNESS_ENV}\` environment variable.
103
+
104
+ Available harnesses: ${CLI_AGENT_HARNESS_NAMES.join(', ')}
105
+
106
+ Example: \`PTBK_HARNESS=claude-code\`
107
+ `),
108
+ );
109
+ }
110
+
84
111
  const result = await executeAgentChatTurn({
85
112
  agentPath,
86
113
  messages: [{ sender: 'USER', content: normalizedMessage }],
87
- agentName: mergedOptions.harness,
88
- model: mergedOptions.model,
114
+ agentName: harness,
115
+ model: mergedOptions.model ?? process.env[PTBK_MODEL_ENV],
89
116
  noUi: mergedOptions.noUi ?? DEFAULT_CLI_AGENT_IS_NO_UI,
90
- thinkingLevel: mergedOptions.thinkingLevel,
117
+ thinkingLevel: mergedOptions.thinkingLevel ?? resolveCliAgentThinkingLevelFromEnv(),
91
118
  allowCredits: mergedOptions.allowCredits ?? false,
92
- isVerbose: false,
119
+ isVerbose: mergedOptions.isVerbose ?? false,
93
120
  context: mergedOptions.context,
94
121
  currentWorkingDirectory: resolvedSource.currentWorkingDirectory,
95
122
  });
@@ -128,12 +155,63 @@ function mergeCliAgentRunOptions(defaults: CliAgentRunOptions, overrides: CliAge
128
155
  allowCredits: overrides.allowCredits ?? defaults.allowCredits,
129
156
  context: overrides.context ?? defaults.context,
130
157
  harness: overrides.harness ?? defaults.harness,
158
+ isVerbose: overrides.isVerbose ?? defaults.isVerbose,
131
159
  model: overrides.model ?? defaults.model,
132
160
  noUi: overrides.noUi ?? defaults.noUi,
133
161
  thinkingLevel: overrides.thinkingLevel ?? defaults.thinkingLevel,
134
162
  };
135
163
  }
136
164
 
165
+ /**
166
+ * Reads and validates the harness name from the `PTBK_HARNESS` environment variable.
167
+ *
168
+ * @private internal utility of `CliAgent`
169
+ */
170
+ function resolveCliAgentHarnessFromEnv(): CliAgentHarness | undefined {
171
+ const envValue = process.env[PTBK_HARNESS_ENV];
172
+
173
+ if (!envValue) {
174
+ return undefined;
175
+ }
176
+
177
+ if (!(CLI_AGENT_HARNESS_NAMES as ReadonlyArray<string>).includes(envValue)) {
178
+ throw new NotAllowed(
179
+ spaceTrim(`
180
+ Invalid value for \`${PTBK_HARNESS_ENV}\` environment variable: \`${envValue}\`
181
+
182
+ Must be one of: ${CLI_AGENT_HARNESS_NAMES.join(', ')}
183
+ `),
184
+ );
185
+ }
186
+
187
+ return envValue as CliAgentHarness;
188
+ }
189
+
190
+ /**
191
+ * Reads and validates the thinking level from the `PTBK_THINKING_LEVEL` environment variable.
192
+ *
193
+ * @private internal utility of `CliAgent`
194
+ */
195
+ function resolveCliAgentThinkingLevelFromEnv(): CliAgentThinkingLevel | undefined {
196
+ const envValue = process.env[PTBK_THINKING_LEVEL_ENV];
197
+
198
+ if (!envValue) {
199
+ return undefined;
200
+ }
201
+
202
+ if (!(CLI_AGENT_THINKING_LEVEL_VALUES as ReadonlyArray<string>).includes(envValue)) {
203
+ throw new NotAllowed(
204
+ spaceTrim(`
205
+ Invalid value for \`${PTBK_THINKING_LEVEL_ENV}\` environment variable: \`${envValue}\`
206
+
207
+ Must be one of: ${CLI_AGENT_THINKING_LEVEL_VALUES.join(', ')}
208
+ `),
209
+ );
210
+ }
211
+
212
+ return envValue as CliAgentThinkingLevel;
213
+ }
214
+
137
215
  /**
138
216
  * Creates the stable temporary path used when `CliAgent` is initialized from in-memory Book source.
139
217
  *
@@ -305,7 +305,7 @@ async function resolveLiteAgentScriptExecutionTools(
305
305
  if (options.scriptExecutionTools) {
306
306
  return Array.isArray(options.scriptExecutionTools)
307
307
  ? options.scriptExecutionTools
308
- : [options.scriptExecutionTools];
308
+ : [options.scriptExecutionTools as ScriptExecutionTools];
309
309
  }
310
310
 
311
311
  return $provideScriptingForNode({
@@ -0,0 +1,46 @@
1
+ // Note: [💞] Ignore a discrepancy between file name and entity name
2
+
3
+ /**
4
+ * All CLI harness names supported by `CliAgent` and `ptbk agent exec`.
5
+ *
6
+ * @public exported from `@promptbook/node`
7
+ */
8
+ export const CLI_AGENT_HARNESS_NAMES = [
9
+ 'openai-codex',
10
+ 'github-copilot',
11
+ 'cline',
12
+ 'claude-code',
13
+ 'opencode',
14
+ 'gemini',
15
+ ] as const;
16
+
17
+ /**
18
+ * All supported thinking-level values for CLI coding-agent runners.
19
+ *
20
+ * @public exported from `@promptbook/node`
21
+ */
22
+ export const CLI_AGENT_THINKING_LEVEL_VALUES = ['low', 'medium', 'high', 'xhigh'] as const;
23
+
24
+ /**
25
+ * Environment variable used as the default runner identifier when `--harness` is omitted or not set in `CliAgent`.
26
+ *
27
+ * Set this to one of the harness names (`openai-codex`, `github-copilot`, `cline`, `claude-code`, `opencode`, `gemini`)
28
+ * so that `CliAgent` and `ptbk agent exec` can run without an explicit `harness` option.
29
+ *
30
+ * @public exported from `@promptbook/node`
31
+ */
32
+ export const PTBK_HARNESS_ENV = 'PTBK_HARNESS';
33
+
34
+ /**
35
+ * Environment variable used as the default runner model when `--model` is omitted or not set in `CliAgent`.
36
+ *
37
+ * @public exported from `@promptbook/node`
38
+ */
39
+ export const PTBK_MODEL_ENV = 'PTBK_MODEL';
40
+
41
+ /**
42
+ * Environment variable used as the default thinking level when `--thinking-level` is omitted or not set in `CliAgent`.
43
+ *
44
+ * @public exported from `@promptbook/node`
45
+ */
46
+ export const PTBK_THINKING_LEVEL_ENV = 'PTBK_THINKING_LEVEL';
@@ -2,7 +2,7 @@
2
2
  // <- Note: [👲] 'use client' is enforced by Next.js when building the https://book-components.ptbk.io/ but in ideal case,
3
3
  // this would not be here because the `@promptbook/components` package should be React library independent of Next.js specifics
4
4
 
5
- import { CSSProperties, useState } from 'react';
5
+ import { type CSSProperties, useState } from 'react';
6
6
  import { createPortal } from 'react-dom';
7
7
  import type { Promisable } from 'type-fest';
8
8
  import type { string_book } from '../../book-2.0/agent-source/string_book';
@@ -14,8 +14,8 @@ import type { string_css_value } from '../../types/string_markdown';
14
14
  import { countLines } from '../../utils/expectation-counters/countLines';
15
15
  import type { HoistedMenuItem } from '../_common/MenuHoisting/MenuHoistingContext';
16
16
  import { classNames } from '../_common/react-utils/classNames';
17
- import { DEFAULT_IS_VERBOSE } from './BookEditorBrowserConfig';
18
17
  import styles from './BookEditor.module.css';
18
+ import { DEFAULT_IS_VERBOSE } from './BookEditorBrowserConfig';
19
19
  import { BookEditorMonaco } from './BookEditorMonaco';
20
20
  import type { BookEditorTheme } from './BookEditorTheme';
21
21
  import { BOOK_EDITOR_RENDER_THEME, resolveBookEditorRenderTheme } from './BookEditorTheme';
@@ -300,11 +300,11 @@ export function BookEditor(props: BookEditorProps) {
300
300
  isBorderRadiusDisabled = false,
301
301
  isReadonly = false,
302
302
  translations,
303
- isUploadButtonShown = true,
303
+ isUploadButtonShown = false,
304
304
  isCameraButtonShown,
305
- isDownloadButtonShown = true,
306
- isAboutButtonShown = true,
307
- isFullscreenButtonShown = true,
305
+ isDownloadButtonShown = false,
306
+ isAboutButtonShown = false,
307
+ isFullscreenButtonShown = false,
308
308
  sync,
309
309
  monacoModelPath,
310
310
  hoistedMenuItems,
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { BookEditorProps } from './BookEditor';
2
+ import type { BookEditorProps } from './BookEditor';
3
3
 
4
4
  type BookEditorComponent = typeof import('./BookEditor').BookEditor;
5
5
 
@@ -3311,6 +3311,51 @@
3311
3311
  display: block;
3312
3312
  }
3313
3313
 
3314
+ .citationIframeLoading {
3315
+ width: 100%;
3316
+ height: 60vh;
3317
+ min-height: 400px;
3318
+ display: flex;
3319
+ align-items: center;
3320
+ justify-content: center;
3321
+ color: #888;
3322
+ font-size: 0.9em;
3323
+ }
3324
+
3325
+ .citationScreenshotFallback {
3326
+ width: 100%;
3327
+ position: relative;
3328
+ display: flex;
3329
+ flex-direction: column;
3330
+ }
3331
+
3332
+ .citationScreenshotImage {
3333
+ width: 100%;
3334
+ max-height: 60vh;
3335
+ min-height: 400px;
3336
+ object-fit: cover;
3337
+ object-position: top;
3338
+ display: block;
3339
+ }
3340
+
3341
+ .citationScreenshotLink {
3342
+ display: block;
3343
+ text-align: center;
3344
+ padding: 10px 16px;
3345
+ background: #f0f0f0;
3346
+ color: #333;
3347
+ font-size: 0.9em;
3348
+ font-weight: 500;
3349
+ text-decoration: none;
3350
+ border-top: 1px solid #ddd;
3351
+ transition: background 0.15s;
3352
+ }
3353
+
3354
+ .citationScreenshotLink:hover {
3355
+ background: #e0e0e0;
3356
+ text-decoration: underline;
3357
+ }
3358
+
3314
3359
  .citationMetadata {
3315
3360
  background: #f8f8f8;
3316
3361
  padding: 16px;
@@ -8,6 +8,7 @@ import type { ChatParticipant } from '../types/ChatParticipant';
8
8
  import { getCitationLabel, isPlainTextCitation, resolveCitationPreviewUrl } from '../utils/citationHelpers';
9
9
  import type { ParsedCitation } from '../utils/parseCitationsFromContent';
10
10
  import styles from './Chat.module.css';
11
+ import { CitationIframePreview } from './CitationIframePreview';
11
12
  import type { ChatSoundSystem } from './ChatProps';
12
13
 
13
14
  /**
@@ -84,9 +85,8 @@ export function ChatCitationModal(props: ChatCitationModalProps) {
84
85
  }}
85
86
  />
86
87
  ) : (
87
- <iframe
88
+ <CitationIframePreview
88
89
  src={previewUrl ?? citation.source}
89
- className={styles.citationIframe}
90
90
  title={`Preview of ${label}`}
91
91
  />
92
92
  )}