@promptbook/cli 0.112.0-126 → 0.112.0-128

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 (190) hide show
  1. package/README.md +5 -5
  2. package/apps/agents-server/README.md +3 -0
  3. package/apps/agents-server/playwright.config.ts +1 -0
  4. package/apps/agents-server/public/promptbook-logo-blue.png +0 -0
  5. package/apps/agents-server/public/promptbook-logo-white.png +0 -0
  6. package/apps/agents-server/src/app/[agentName]/layout.tsx +1 -0
  7. package/apps/agents-server/src/app/actions.ts +2 -2
  8. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +68 -36
  9. package/apps/agents-server/src/app/agents/[agentName]/ActiveAgentBreadcrumbBinder.tsx +30 -0
  10. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +2 -0
  11. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatHistoryPayloadState.ts +38 -4
  12. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatHistorySyncOperations.ts +4 -1
  13. package/apps/agents-server/src/app/agents/[agentName]/chat/CanonicalAgentChatSurface.tsx +19 -15
  14. package/apps/agents-server/src/app/agents/[agentName]/chat/useCanonicalAgentChatPanelState.ts +58 -7
  15. package/apps/agents-server/src/app/agents/[agentName]/layout.tsx +47 -1
  16. package/apps/agents-server/src/app/api/chat/citation-label/route.ts +133 -0
  17. package/apps/agents-server/src/app/globals.css +78 -0
  18. package/apps/agents-server/src/components/Header/ActiveAgentBreadcrumbContext.tsx +82 -0
  19. package/apps/agents-server/src/components/Header/useHeaderActiveAgent.ts +26 -7
  20. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +35 -32
  21. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/ManGoNewAgentWizard.tsx +151 -0
  22. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/ManGoOnboardingNavigation.tsx +50 -0
  23. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/BookLanguagePanel.tsx +125 -0
  24. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/DropZone.tsx +83 -0
  25. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/EmailTestRun.tsx +216 -0
  26. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/KnowledgeList.tsx +79 -0
  27. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/Logo.tsx +20 -0
  28. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/MarkdownBookEditor.tsx +154 -0
  29. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/MarkdownPreview.tsx +188 -0
  30. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/RailStepper.tsx +115 -0
  31. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/StepFrame.tsx +47 -0
  32. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/TestChat.tsx +159 -0
  33. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/WizardShell.tsx +114 -0
  34. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/brand/RailArtwork.tsx +60 -0
  35. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/steps/BookStep.tsx +135 -0
  36. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/steps/DoneStep.tsx +215 -0
  37. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/steps/KnowledgeStep.tsx +152 -0
  38. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/steps/TestStep.tsx +262 -0
  39. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/steps/ZadaniStep.tsx +64 -0
  40. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/ui/Badge.tsx +41 -0
  41. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/ui/Banner.tsx +69 -0
  42. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/ui/Button.tsx +77 -0
  43. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/ui/Card.tsx +33 -0
  44. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/ui/Field.tsx +103 -0
  45. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/ui/IconButton.tsx +40 -0
  46. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/ui/Spinner.tsx +14 -0
  47. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/ui/tokens.ts +37 -0
  48. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/config/bookSections.ts +62 -0
  49. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/config/emailScenarios.ts +22 -0
  50. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/config/steps.ts +46 -0
  51. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/index.ts +11 -0
  52. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/lib/cn.ts +4 -0
  53. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/lib/format.ts +9 -0
  54. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/lib/id.ts +11 -0
  55. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/agentEvalService.ts +30 -0
  56. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/agentTestService.ts +69 -0
  57. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/bookService.ts +21 -0
  58. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/createManGoAgentSource.ts +107 -0
  59. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/draftService.ts +45 -0
  60. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/uploadService.ts +27 -0
  61. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/state/OnboardingProvider.tsx +121 -0
  62. package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/types.ts +52 -0
  63. package/apps/agents-server/src/components/NewAgentDialog/createNewAgentWizardSource.ts +1 -1
  64. package/apps/agents-server/src/components/NewAgentDialog/trackNewAgentCreationEvent.ts +1 -1
  65. package/apps/agents-server/src/components/NewAgentDialog/useNewAgentDialog.tsx +76 -0
  66. package/apps/agents-server/src/constants/newAgentWizard.ts +17 -8
  67. package/apps/agents-server/src/database/getMetadata.ts +37 -1
  68. package/apps/agents-server/src/database/metadataDefaults.ts +18 -7
  69. package/apps/agents-server/src/database/migrations/2026-06-2200-new-agent-wizard-metadata-key.sql +27 -0
  70. package/apps/agents-server/src/database/migrations/2026-06-2201-new-agent-wizard-mango-default.sql +11 -0
  71. package/apps/agents-server/src/generated/reservedPaths.ts +2 -0
  72. package/apps/agents-server/src/languages/translations/english.yaml +1 -1
  73. package/apps/agents-server/src/tools/createChatAttachmentToolFunctions.ts +8 -4
  74. package/apps/agents-server/src/utils/chat/resolveAgentsServerCitationLabel.ts +130 -0
  75. package/apps/agents-server/src/utils/chat/resolveCitationSourceLabel.ts +436 -0
  76. package/apps/agents-server/src/utils/knowledge/resolveWebsiteKnowledgeSourcesForServer.ts +10 -1
  77. package/apps/agents-server/src/utils/session.ts +62 -5
  78. package/apps/agents-server/src/utils/transpilers/resolveTranspiledTeamExport.ts +10 -10
  79. package/apps/agents-server/src/utils/userChat/userChatProgressCard.ts +198 -63
  80. package/apps/agents-server/src/utils/vpsConfiguration.ts +1 -0
  81. package/apps/agents-server/src/utils/vpsSelfUpdate.ts +81 -8
  82. package/esm/index.es.js +1052 -455
  83. package/esm/index.es.js.map +1 -1
  84. package/esm/scripts/run-codex-prompts/git/commitChanges.d.ts +3 -1
  85. package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -2
  86. package/esm/scripts/run-codex-prompts/server/buildCoderServerPromptResponse.d.ts +50 -0
  87. package/esm/scripts/run-codex-prompts/server/buildCoderServerRunState.d.ts +23 -0
  88. package/esm/scripts/run-codex-prompts/server/coderServerHtml.d.ts +1 -1
  89. package/esm/scripts/run-codex-prompts/server/runCoderHttpServer.d.ts +13 -1
  90. package/esm/scripts/run-codex-prompts/server/updatePromptSection.d.ts +1 -1
  91. package/esm/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +1 -0
  92. package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +4 -0
  93. package/esm/scripts/run-codex-prompts/ui/renderCoderRunUi.d.ts +1 -0
  94. package/esm/src/_packages/components.index.d.ts +2 -0
  95. package/esm/src/_packages/types.index.d.ts +2 -0
  96. package/esm/src/avatars/avatarAnimationScheduler.d.ts +4 -0
  97. package/esm/src/book-components/Chat/Chat/ChatCitationModal.d.ts +2 -0
  98. package/esm/src/book-components/Chat/Chat/ChatMessageItem.d.ts +4 -0
  99. package/esm/src/book-components/Chat/Chat/ChatMessageList.d.ts +4 -0
  100. package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +5 -0
  101. package/esm/src/book-components/Chat/Chat/ChatToolCallModal.d.ts +2 -0
  102. package/esm/src/book-components/Chat/Chat/ChatToolCallModalContent.d.ts +3 -1
  103. package/esm/src/book-components/Chat/Chat/TeamToolCallModalContent.d.ts +2 -0
  104. package/esm/src/book-components/Chat/SourceChip/SourceChip.d.ts +6 -1
  105. package/esm/src/book-components/Chat/hooks/useResolvedCitationLabel.d.ts +12 -0
  106. package/esm/src/book-components/Chat/types/ChatMessage.d.ts +4 -0
  107. package/esm/src/book-components/Chat/types/ChatParticipant.d.ts +1 -1
  108. package/esm/src/book-components/Chat/types/CitationLabelResolver.d.ts +8 -0
  109. package/esm/src/book-components/Chat/utils/citationHelpers.d.ts +9 -0
  110. package/esm/src/book-components/Chat/utils/decodeJsonUnicodeEscapesInMarkdownText.d.ts +14 -0
  111. package/esm/src/book-components/Chat/utils/decodeJsonUnicodeEscapesInMarkdownText.test.d.ts +1 -0
  112. package/esm/src/book-components/Chat/utils/parseCitationsFromContent.d.ts +4 -0
  113. package/esm/src/version.d.ts +1 -1
  114. package/package.json +1 -1
  115. package/src/_packages/components.index.ts +2 -0
  116. package/src/_packages/types.index.ts +2 -0
  117. package/src/avatars/avatarAnimationScheduler.ts +61 -20
  118. package/src/avatars/visuals/octopus3d2AvatarVisual.ts +106 -21
  119. package/src/avatars/visuals/octopus3d3AvatarVisual.ts +131 -28
  120. package/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments/augmentAgentModelRequirementsFromSource.ts +18 -2
  121. package/src/book-2.0/book-language-documentation/renderGroupedCommitmentDocumentationMarkdown.ts +7 -1
  122. package/src/book-components/Chat/Chat/Chat.tsx +4 -0
  123. package/src/book-components/Chat/Chat/ChatCitationModal.tsx +17 -3
  124. package/src/book-components/Chat/Chat/ChatMessageItem.tsx +22 -2
  125. package/src/book-components/Chat/Chat/ChatMessageList.tsx +6 -0
  126. package/src/book-components/Chat/Chat/ChatProps.tsx +6 -0
  127. package/src/book-components/Chat/Chat/ChatToolCallModal.tsx +4 -0
  128. package/src/book-components/Chat/Chat/ChatToolCallModalContent.tsx +4 -0
  129. package/src/book-components/Chat/Chat/TeamToolCallModalContent.tsx +16 -3
  130. package/src/book-components/Chat/Chat/useChatPostprocessedMessages.ts +13 -2
  131. package/src/book-components/Chat/SourceChip/SourceChip.module.css +8 -0
  132. package/src/book-components/Chat/SourceChip/SourceChip.tsx +19 -5
  133. package/src/book-components/Chat/hooks/useResolvedCitationLabel.ts +65 -0
  134. package/src/book-components/Chat/save/html/htmlSaveFormatDefinition.ts +315 -88
  135. package/src/book-components/Chat/save/react/reactSaveFormatDefinition.ts +7 -1
  136. package/src/book-components/Chat/types/ChatMessage.ts +5 -0
  137. package/src/book-components/Chat/types/ChatParticipant.ts +1 -1
  138. package/src/book-components/Chat/types/CitationLabelResolver.ts +9 -0
  139. package/src/book-components/Chat/utils/citationHelpers.ts +90 -2
  140. package/src/book-components/Chat/utils/createCitationFootnoteRenderModel.ts +3 -0
  141. package/src/book-components/Chat/utils/decodeJsonUnicodeEscapesInMarkdownText.ts +178 -0
  142. package/src/book-components/Chat/utils/parseCitationsFromContent.ts +6 -0
  143. package/src/cli/cli-commands/agents-server/ensureAgentsServerEnvFile.ts +13 -2
  144. package/src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.ts +1 -1
  145. package/src/cli/cli-commands/coder/run.ts +17 -20
  146. package/src/cli/cli-commands/coder/server.ts +13 -16
  147. package/src/cli/common/$deprecateCliCommand.ts +10 -1
  148. package/src/commitments/GOAL/GOAL.ts +7 -2
  149. package/src/commitments/LANGUAGE/LANGUAGE.ts +7 -2
  150. package/src/commitments/TEAM/TEAM.ts +12 -1
  151. package/src/commitments/USE_PROJECT/createUseProjectToolFunctions.ts +8 -1
  152. package/src/import-plugins/JsonFileImportPlugin.ts +15 -2
  153. package/src/import-plugins/TextFileImportPlugin.ts +8 -1
  154. package/src/llm-providers/_common/register/$registeredLlmToolsMessage.ts +6 -1
  155. package/src/other/templates/getTemplatesPipelineCollection.ts +713 -785
  156. package/src/utils/chat/chatAttachments/resolveChatAttachmentContent.ts +8 -1
  157. package/src/version.ts +2 -2
  158. package/src/versions.txt +1 -0
  159. package/umd/index.umd.js +1055 -458
  160. package/umd/index.umd.js.map +1 -1
  161. package/umd/scripts/run-codex-prompts/git/commitChanges.d.ts +3 -1
  162. package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -2
  163. package/umd/scripts/run-codex-prompts/server/buildCoderServerPromptResponse.d.ts +50 -0
  164. package/umd/scripts/run-codex-prompts/server/buildCoderServerRunState.d.ts +23 -0
  165. package/umd/scripts/run-codex-prompts/server/coderServerHtml.d.ts +1 -1
  166. package/umd/scripts/run-codex-prompts/server/runCoderHttpServer.d.ts +13 -1
  167. package/umd/scripts/run-codex-prompts/server/updatePromptSection.d.ts +1 -1
  168. package/umd/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +1 -0
  169. package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +4 -0
  170. package/umd/scripts/run-codex-prompts/ui/renderCoderRunUi.d.ts +1 -0
  171. package/umd/src/_packages/components.index.d.ts +2 -0
  172. package/umd/src/_packages/types.index.d.ts +2 -0
  173. package/umd/src/avatars/avatarAnimationScheduler.d.ts +4 -0
  174. package/umd/src/book-components/Chat/Chat/ChatCitationModal.d.ts +2 -0
  175. package/umd/src/book-components/Chat/Chat/ChatMessageItem.d.ts +4 -0
  176. package/umd/src/book-components/Chat/Chat/ChatMessageList.d.ts +4 -0
  177. package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +5 -0
  178. package/umd/src/book-components/Chat/Chat/ChatToolCallModal.d.ts +2 -0
  179. package/umd/src/book-components/Chat/Chat/ChatToolCallModalContent.d.ts +3 -1
  180. package/umd/src/book-components/Chat/Chat/TeamToolCallModalContent.d.ts +2 -0
  181. package/umd/src/book-components/Chat/SourceChip/SourceChip.d.ts +6 -1
  182. package/umd/src/book-components/Chat/hooks/useResolvedCitationLabel.d.ts +12 -0
  183. package/umd/src/book-components/Chat/types/ChatMessage.d.ts +4 -0
  184. package/umd/src/book-components/Chat/types/ChatParticipant.d.ts +1 -1
  185. package/umd/src/book-components/Chat/types/CitationLabelResolver.d.ts +8 -0
  186. package/umd/src/book-components/Chat/utils/citationHelpers.d.ts +9 -0
  187. package/umd/src/book-components/Chat/utils/decodeJsonUnicodeEscapesInMarkdownText.d.ts +14 -0
  188. package/umd/src/book-components/Chat/utils/decodeJsonUnicodeEscapesInMarkdownText.test.d.ts +1 -0
  189. package/umd/src/book-components/Chat/utils/parseCitationsFromContent.d.ts +4 -0
  190. package/umd/src/version.d.ts +1 -1
@@ -8,56 +8,75 @@
8
8
  type AvatarAnimationListener = (now: number) => void;
9
9
 
10
10
  /**
11
- * Target frames per second for the shared avatar animation loop.
11
+ * One scheduled avatar animation entry tracked by the shared loop.
12
12
  *
13
- * Animated octopus visuals change slowly enough that 24 fps is indistinguishable
14
- * from 60 fps in practice, while cutting rendering work by ~60% when multiple
15
- * avatars are on screen simultaneously.
13
+ * @private utility of the avatar rendering system
14
+ */
15
+ type AvatarAnimationListenerEntry = {
16
+ readonly listener: AvatarAnimationListener;
17
+ lastRunAtMs: number;
18
+ };
19
+
20
+ /**
21
+ * Target frames per second for a single animated avatar.
22
+ *
23
+ * Animated octopus visuals change slowly enough that ~24 fps per avatar is indistinguishable
24
+ * from 60 fps in practice. Combined with `MAX_AVATAR_LISTENERS_PER_FRAME` this bounds total
25
+ * rendering work even when many avatars are visible.
16
26
  *
17
27
  * @private utility of the avatar rendering system
18
28
  */
19
29
  const AVATAR_TARGET_FPS = 24;
20
30
 
21
31
  /**
22
- * Minimum elapsed time in milliseconds required between avatar render passes.
32
+ * Minimum elapsed time in milliseconds required between two consecutive renders of the same avatar.
23
33
  *
24
34
  * @private utility of the avatar rendering system
25
35
  */
26
36
  const AVATAR_TARGET_FRAME_INTERVAL_MS = 1000 / AVATAR_TARGET_FPS;
27
37
 
28
38
  /**
29
- * Next registration id used by the shared avatar animation scheduler.
39
+ * Hard cap on how many avatar listeners can run inside the same animation frame.
40
+ *
41
+ * The default visual takes several milliseconds to render, so firing every visible avatar
42
+ * on the same animation frame stalls the main thread for tens of milliseconds and drops the
43
+ * overall page FPS to single digits. Bounding the number of listeners that fire per frame
44
+ * spreads the work across animation frames and keeps the UI responsive even with many
45
+ * visible avatars — each individual avatar still reaches `AVATAR_TARGET_FPS` because work
46
+ * is interleaved instead of skipped.
30
47
  *
31
48
  * @private utility of the avatar rendering system
32
49
  */
33
- let nextAvatarAnimationListenerId = 1;
50
+ const MAX_AVATAR_LISTENERS_PER_FRAME = 2;
34
51
 
35
52
  /**
36
- * Active avatar animation callbacks keyed by their registration id.
53
+ * Next registration id used by the shared avatar animation scheduler.
37
54
  *
38
55
  * @private utility of the avatar rendering system
39
56
  */
40
- const avatarAnimationListeners = new Map<number, AvatarAnimationListener>();
57
+ let nextAvatarAnimationListenerId = 1;
41
58
 
42
59
  /**
43
- * Active shared animation-frame handle.
60
+ * Active avatar animation callbacks keyed by their registration id.
44
61
  *
45
62
  * @private utility of the avatar rendering system
46
63
  */
47
- let avatarAnimationFrameId: number | null = null;
64
+ const avatarAnimationListeners = new Map<number, AvatarAnimationListenerEntry>();
48
65
 
49
66
  /**
50
- * Timestamp of the most recently rendered avatar frame.
51
- *
52
- * Used to throttle callbacks to `AVATAR_TARGET_FRAME_INTERVAL_MS`.
67
+ * Active shared animation-frame handle.
53
68
  *
54
69
  * @private utility of the avatar rendering system
55
70
  */
56
- let lastAvatarFrameTime = 0;
71
+ let avatarAnimationFrameId: number | null = null;
57
72
 
58
73
  /**
59
74
  * Registers one avatar animation callback in the shared animation loop.
60
75
  *
76
+ * Each listener is staggered at registration time so it does not fire on the same animation
77
+ * frame as the existing listeners, and the shared loop further caps how many listeners run
78
+ * per frame, keeping the main thread responsive when many avatars are mounted.
79
+ *
61
80
  * @param avatarAnimationListener Frame callback invoked on every animation frame.
62
81
  * @returns Cleanup function that unregisters the callback.
63
82
  *
@@ -69,7 +88,15 @@ export function retainAvatarAnimationListener(avatarAnimationListener: AvatarAni
69
88
  }
70
89
 
71
90
  const listenerId = nextAvatarAnimationListenerId++;
72
- avatarAnimationListeners.set(listenerId, avatarAnimationListener);
91
+ const existingListenerCount = avatarAnimationListeners.size;
92
+ const staggerOffsetMs =
93
+ ((existingListenerCount % Math.max(1, MAX_AVATAR_LISTENERS_PER_FRAME)) * AVATAR_TARGET_FRAME_INTERVAL_MS) /
94
+ Math.max(1, MAX_AVATAR_LISTENERS_PER_FRAME);
95
+ avatarAnimationListeners.set(listenerId, {
96
+ listener: avatarAnimationListener,
97
+ // Phase the listener so multiple new listeners do not synchronize on the same frame.
98
+ lastRunAtMs: performance.now() - AVATAR_TARGET_FRAME_INTERVAL_MS + staggerOffsetMs,
99
+ });
73
100
  ensureAvatarAnimationLoop();
74
101
 
75
102
  return () => {
@@ -95,14 +122,28 @@ function ensureAvatarAnimationLoop(): void {
95
122
  const runFrame = (now: number) => {
96
123
  avatarAnimationFrameId = null;
97
124
 
98
- if (now - lastAvatarFrameTime >= AVATAR_TARGET_FRAME_INTERVAL_MS) {
99
- lastAvatarFrameTime = now;
125
+ const dueListenerEntries: Array<AvatarAnimationListenerEntry> = [];
100
126
 
101
- for (const avatarAnimationListener of [...avatarAnimationListeners.values()]) {
102
- avatarAnimationListener(now);
127
+ for (const listenerEntry of avatarAnimationListeners.values()) {
128
+ if (now - listenerEntry.lastRunAtMs >= AVATAR_TARGET_FRAME_INTERVAL_MS) {
129
+ dueListenerEntries.push(listenerEntry);
103
130
  }
104
131
  }
105
132
 
133
+ // Run the most stale listeners first so each avatar keeps its target FPS even when
134
+ // total work exceeds the per-frame budget.
135
+ dueListenerEntries.sort(
136
+ (firstListenerEntry, secondListenerEntry) => firstListenerEntry.lastRunAtMs - secondListenerEntry.lastRunAtMs,
137
+ );
138
+
139
+ const listenersToRunCount = Math.min(dueListenerEntries.length, MAX_AVATAR_LISTENERS_PER_FRAME);
140
+
141
+ for (let listenerIndex = 0; listenerIndex < listenersToRunCount; listenerIndex++) {
142
+ const listenerEntry = dueListenerEntries[listenerIndex]!;
143
+ listenerEntry.lastRunAtMs = now;
144
+ listenerEntry.listener(now);
145
+ }
146
+
106
147
  ensureAvatarAnimationLoop();
107
148
  };
108
149
 
@@ -331,9 +331,29 @@ function drawBlobbyOctopusShadow(
331
331
  context.restore();
332
332
  }
333
333
 
334
+ /**
335
+ * Number of latitude segments used by the single blobby octopus mesh.
336
+ *
337
+ * @private helper of `octopus3d2AvatarVisual`
338
+ */
339
+ const LATITUDE_PATCH_COUNT = 12;
340
+
341
+ /**
342
+ * Number of longitude segments used by the single blobby octopus mesh.
343
+ *
344
+ * @private helper of `octopus3d2AvatarVisual`
345
+ */
346
+ const LONGITUDE_PATCH_COUNT = 24;
347
+
334
348
  /**
335
349
  * Resolves all visible projected patches for the single blobby octopus mesh.
336
350
  *
351
+ * Within a single frame, mesh corner samples and longitude-only lobe-wave values are
352
+ * quantized to the patch grid and computed once each rather than re-evaluated for every
353
+ * patch corner — the patch loop alone would call `sampleBlobbyOctopusSurfacePoint`
354
+ * `latitudePatchCount * longitudePatchCount * 4` times without caching, even though most
355
+ * corners are shared between neighboring patches.
356
+ *
337
357
  * @private helper of `octopus3d2AvatarVisual`
338
358
  */
339
359
  function resolveVisibleBlobbyOctopusPatches(options: {
@@ -363,29 +383,68 @@ function resolveVisibleBlobbyOctopusPatches(options: {
363
383
  animationPhase,
364
384
  timeMs,
365
385
  } = options;
366
- const latitudePatchCount = 12;
367
- const longitudePatchCount = 24;
386
+ const latitudePatchCount = LATITUDE_PATCH_COUNT;
387
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT;
368
388
  const surfacePatches: Array<BlobbyOctopusSurfacePatch> = [];
389
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
390
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
391
+
392
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
393
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
394
+ }
395
+
396
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
397
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
398
+ }
399
+
400
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
401
+
402
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
403
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveLowerLobeWave(
404
+ longitudeBoundaries[boundaryIndex]!,
405
+ morphologyProfile,
406
+ animationPhase,
407
+ timeMs,
408
+ );
409
+ }
410
+
411
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
412
+ const transformedCornerSamples = new Array<Point3D>(cornerCount);
413
+
414
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
415
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex]!;
416
+
417
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
418
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex]!;
419
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
420
+ const cornerSample = sampleBlobbyOctopusSurfacePointWithLongitudeCache(
421
+ options,
422
+ cornerLatitude,
423
+ cornerLongitude,
424
+ cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]!,
425
+ );
426
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
427
+ }
428
+ }
369
429
 
370
430
  for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
371
- const startLatitude = -Math.PI / 2 + (latitudeIndex / latitudePatchCount) * Math.PI;
372
- const endLatitude = -Math.PI / 2 + ((latitudeIndex + 1) / latitudePatchCount) * Math.PI;
431
+ const startLatitude = latitudeBoundaries[latitudeIndex]!;
432
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1]!;
373
433
  const centerLatitude = (startLatitude + endLatitude) / 2;
374
434
  const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
435
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
436
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
375
437
 
376
438
  for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
377
- const startLongitude = -Math.PI + (longitudeIndex / longitudePatchCount) * Math.PI * 2;
378
- const endLongitude = -Math.PI + ((longitudeIndex + 1) / longitudePatchCount) * Math.PI * 2;
439
+ const startLongitude = longitudeBoundaries[longitudeIndex]!;
440
+ const endLongitude = longitudeBoundaries[longitudeIndex + 1]!;
379
441
  const centerLongitude = (startLongitude + endLongitude) / 2;
380
- const localCorners = [
381
- sampleBlobbyOctopusSurfacePoint(options, startLatitude, startLongitude),
382
- sampleBlobbyOctopusSurfacePoint(options, startLatitude, endLongitude),
383
- sampleBlobbyOctopusSurfacePoint(options, endLatitude, endLongitude),
384
- sampleBlobbyOctopusSurfacePoint(options, endLatitude, startLongitude),
385
- ] as const;
386
- const transformedCorners = localCorners.map((localCorner) =>
387
- transformScenePoint(localCorner, center, rotationX, rotationY),
388
- ) as [Point3D, Point3D, Point3D, Point3D];
442
+ const transformedCorners: [Point3D, Point3D, Point3D, Point3D] = [
443
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex]!,
444
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1]!,
445
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1]!,
446
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex]!,
447
+ ];
389
448
  const surfaceNormal = normalizeVector3(
390
449
  crossProduct3D(
391
450
  subtractPoint3D(transformedCorners[1], transformedCorners[0]),
@@ -397,14 +456,20 @@ function resolveVisibleBlobbyOctopusPatches(options: {
397
456
  continue;
398
457
  }
399
458
 
400
- const projectedCorners = transformedCorners.map((transformedCorner) =>
401
- projectScenePoint(transformedCorner, size, sceneCenterX, sceneCenterY),
402
- ) as [ProjectedPoint, ProjectedPoint, ProjectedPoint, ProjectedPoint];
459
+ const projectedCorners: [ProjectedPoint, ProjectedPoint, ProjectedPoint, ProjectedPoint] = [
460
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
461
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
462
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
463
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
464
+ ];
403
465
  surfacePatches.push({
404
466
  corners: projectedCorners,
405
467
  averageDepth:
406
- transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
407
- transformedCorners.length,
468
+ (transformedCorners[0].z +
469
+ transformedCorners[1].z +
470
+ transformedCorners[2].z +
471
+ transformedCorners[3].z) /
472
+ 4,
408
473
  lightIntensity: clampNumber(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1),
409
474
  fillStyle: resolveBlobbySurfacePatchFillStyle(
410
475
  palette,
@@ -432,13 +497,33 @@ function sampleBlobbyOctopusSurfacePoint(
432
497
  options: BlobbyOctopusSurfaceOptions,
433
498
  latitude: number,
434
499
  longitude: number,
500
+ ): Point3D {
501
+ const { morphologyProfile, animationPhase, timeMs } = options;
502
+ return sampleBlobbyOctopusSurfacePointWithLongitudeCache(
503
+ options,
504
+ latitude,
505
+ longitude,
506
+ resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, timeMs),
507
+ );
508
+ }
509
+
510
+ /**
511
+ * Samples one point on the continuous Octopus 3D 2 surface using a precomputed lower-lobe wave
512
+ * to skip the per-call trig evaluation for `latitudePatchCount + 1` longitude-shared corners.
513
+ *
514
+ * @private helper of `octopus3d2AvatarVisual`
515
+ */
516
+ function sampleBlobbyOctopusSurfacePointWithLongitudeCache(
517
+ options: BlobbyOctopusSurfaceOptions,
518
+ latitude: number,
519
+ longitude: number,
520
+ lowerLobeWave: number,
435
521
  ): Point3D {
436
522
  const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
437
523
  const cosineLatitude = Math.max(0, Math.cos(latitude));
438
524
  const verticalProgress = (Math.sin(latitude) + 1) / 2;
439
525
  const upperBlend = Math.pow(1 - verticalProgress, 1.2);
440
526
  const lowerBlend = Math.pow(verticalProgress, 1.42);
441
- const lowerLobeWave = resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, timeMs);
442
527
  const skirtEnvelope = Math.pow(cosineLatitude, 0.5) * lowerBlend;
443
528
  const horizontalScale =
444
529
  1.02 +
@@ -431,9 +431,29 @@ function drawContinuousOctopusShadow(
431
431
  context.restore();
432
432
  }
433
433
 
434
+ /**
435
+ * Number of latitude segments used by the continuous Octopus 3D 3 mesh.
436
+ *
437
+ * @private helper of `octopus3d3AvatarVisual`
438
+ */
439
+ const LATITUDE_PATCH_COUNT = 16;
440
+
441
+ /**
442
+ * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
443
+ *
444
+ * @private helper of `octopus3d3AvatarVisual`
445
+ */
446
+ const LONGITUDE_PATCH_COUNT = 40;
447
+
434
448
  /**
435
449
  * Resolves visible projected patches for the continuous octopus mesh.
436
450
  *
451
+ * Within a single frame, mesh corner samples and longitude-only computations (tentacle
452
+ * influence and lobe wave) are quantized to the patch grid and computed once each rather
453
+ * than re-evaluated for every patch corner — the patch loop alone calls `sampleContinuousOctopusSurfacePoint`
454
+ * for `latitudePatchCount * longitudePatchCount * 4` corners without caching, and each call
455
+ * triggers an inner 8-iteration `Math.exp` loop, which dominates the per-frame cost.
456
+ *
437
457
  * @private helper of `octopus3d3AvatarVisual`
438
458
  */
439
459
  function resolveVisibleContinuousOctopusPatches(
@@ -448,29 +468,84 @@ function resolveVisibleContinuousOctopusPatches(
448
468
  },
449
469
  ): Array<ContinuousOctopusSurfacePatch> {
450
470
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
451
- const latitudePatchCount = 16;
452
- const longitudePatchCount = 40;
471
+ const latitudePatchCount = LATITUDE_PATCH_COUNT;
472
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT;
453
473
  const surfacePatches: Array<ContinuousOctopusSurfacePatch> = [];
474
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
475
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
476
+
477
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
478
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
479
+ }
480
+
481
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
482
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
483
+ }
484
+
485
+ const cachedTentacleInfluencesByCornerLongitude = new Array<ContinuousOctopusTentacleInfluence>(
486
+ longitudePatchCount + 1,
487
+ );
488
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
489
+ const cachedTentacleInfluencesByPatchCenterLongitude = new Array<ContinuousOctopusTentacleInfluence>(
490
+ longitudePatchCount,
491
+ );
492
+ const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
493
+ const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
494
+
495
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
496
+ const cornerLongitude = longitudeBoundaries[boundaryIndex]!;
497
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveContinuousTentacleInfluence(
498
+ options,
499
+ cornerLongitude,
500
+ );
501
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveContinuousLobeWave(options, cornerLongitude);
502
+ }
503
+
504
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
505
+ const patchCenterLongitude = (longitudeBoundaries[longitudeIndex]! + longitudeBoundaries[longitudeIndex + 1]!) / 2;
506
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveContinuousTentacleInfluence(
507
+ options,
508
+ patchCenterLongitude,
509
+ );
510
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveContinuousLobeWave(options, patchCenterLongitude);
511
+ cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
512
+ }
513
+
514
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
515
+ const transformedCornerSamples = new Array<Point3D>(cornerCount);
516
+
517
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
518
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex]!;
519
+
520
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
521
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex]!;
522
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
523
+ const cornerSample = sampleContinuousOctopusSurfacePointWithLongitudeCache(
524
+ options,
525
+ cornerLatitude,
526
+ cornerLongitude,
527
+ cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex]!,
528
+ cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]!,
529
+ );
530
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
531
+ }
532
+ }
454
533
 
455
534
  for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
456
- const startLatitude = -Math.PI / 2 + (latitudeIndex / latitudePatchCount) * Math.PI;
457
- const endLatitude = -Math.PI / 2 + ((latitudeIndex + 1) / latitudePatchCount) * Math.PI;
535
+ const startLatitude = latitudeBoundaries[latitudeIndex]!;
536
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1]!;
458
537
  const centerLatitude = (startLatitude + endLatitude) / 2;
459
538
  const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
539
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
540
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
460
541
 
461
542
  for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
462
- const startLongitude = -Math.PI + (longitudeIndex / longitudePatchCount) * Math.PI * 2;
463
- const endLongitude = -Math.PI + ((longitudeIndex + 1) / longitudePatchCount) * Math.PI * 2;
464
- const centerLongitude = (startLongitude + endLongitude) / 2;
465
- const localCorners = [
466
- sampleContinuousOctopusSurfacePoint(options, startLatitude, startLongitude),
467
- sampleContinuousOctopusSurfacePoint(options, startLatitude, endLongitude),
468
- sampleContinuousOctopusSurfacePoint(options, endLatitude, endLongitude),
469
- sampleContinuousOctopusSurfacePoint(options, endLatitude, startLongitude),
470
- ] as const;
471
- const transformedCorners = localCorners.map((localCorner) =>
472
- transformScenePoint(localCorner, center, rotationX, rotationY),
473
- ) as [Point3D, Point3D, Point3D, Point3D];
543
+ const transformedCorners: [Point3D, Point3D, Point3D, Point3D] = [
544
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex]!,
545
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1]!,
546
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1]!,
547
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex]!,
548
+ ];
474
549
  const surfaceNormal = normalizeVector3(
475
550
  crossProduct3D(
476
551
  subtractPoint3D(transformedCorners[1], transformedCorners[0]),
@@ -482,24 +557,28 @@ function resolveVisibleContinuousOctopusPatches(
482
557
  continue;
483
558
  }
484
559
 
485
- const projectedCorners = transformedCorners.map((transformedCorner) =>
486
- projectScenePoint(transformedCorner, size, sceneCenterX, sceneCenterY),
487
- ) as [ProjectedPoint, ProjectedPoint, ProjectedPoint, ProjectedPoint];
488
- const tentacleInfluence = resolveContinuousTentacleInfluence(options, centerLongitude);
489
- const lowerLobeWave = resolveContinuousLobeWave(options, centerLongitude);
560
+ const projectedCorners: [ProjectedPoint, ProjectedPoint, ProjectedPoint, ProjectedPoint] = [
561
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
562
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
563
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
564
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
565
+ ];
490
566
 
491
567
  surfacePatches.push({
492
568
  corners: projectedCorners,
493
569
  averageDepth:
494
- transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
495
- transformedCorners.length,
570
+ (transformedCorners[0].z +
571
+ transformedCorners[1].z +
572
+ transformedCorners[2].z +
573
+ transformedCorners[3].z) /
574
+ 4,
496
575
  lightIntensity: clampNumber(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1),
497
576
  fillStyle: resolveContinuousSurfacePatchFillStyle(
498
577
  palette,
499
578
  verticalProgress,
500
- Math.max(0, Math.cos(centerLongitude)),
501
- tentacleInfluence.core,
502
- lowerLobeWave,
579
+ cachedCosByPatchCenterLongitude[longitudeIndex]!,
580
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex]!.core,
581
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex]!,
503
582
  ),
504
583
  outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
505
584
  });
@@ -521,6 +600,32 @@ function sampleContinuousOctopusSurfacePoint(
521
600
  options: ContinuousOctopusSurfaceOptions,
522
601
  latitude: number,
523
602
  longitude: number,
603
+ ): Point3D {
604
+ return sampleContinuousOctopusSurfacePointWithLongitudeCache(
605
+ options,
606
+ latitude,
607
+ longitude,
608
+ resolveContinuousTentacleInfluence(options, longitude),
609
+ resolveContinuousLobeWave(options, longitude),
610
+ );
611
+ }
612
+
613
+ /**
614
+ * Samples one point on the continuous Octopus 3D 3 surface using precomputed longitude-only
615
+ * values to skip the per-call `Math.exp` tentacle-influence loop and the lobe-wave trig call.
616
+ *
617
+ * The patch loop quantizes the mesh into a fixed `(latitudePatchCount + 1) * (longitudePatchCount + 1)`
618
+ * corner grid, so the same longitude is reused across every latitude row and each
619
+ * tentacle/lobe value can be computed once per frame instead of `latitudePatchCount * 4` times.
620
+ *
621
+ * @private helper of `octopus3d3AvatarVisual`
622
+ */
623
+ function sampleContinuousOctopusSurfacePointWithLongitudeCache(
624
+ options: ContinuousOctopusSurfaceOptions,
625
+ latitude: number,
626
+ longitude: number,
627
+ tentacleInfluence: ContinuousOctopusTentacleInfluence,
628
+ lowerLobeWave: number,
524
629
  ): Point3D {
525
630
  const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
526
631
  const cosineLatitude = Math.max(0, Math.cos(latitude));
@@ -528,10 +633,8 @@ function sampleContinuousOctopusSurfacePoint(
528
633
  const upperBlend = Math.pow(1 - verticalProgress, 1.28);
529
634
  const lowerBlend = smoothStep(0.38, 1, verticalProgress);
530
635
  const tipBlend = smoothStep(0.68, 1, verticalProgress);
531
- const tentacleInfluence = resolveContinuousTentacleInfluence(options, longitude);
532
636
  const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
533
637
  const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
534
- const lowerLobeWave = resolveContinuousLobeWave(options, longitude);
535
638
  const mantleRipple =
536
639
  Math.sin(
537
640
  longitude * morphologyProfile.body.lobeCount +
@@ -282,12 +282,28 @@ function collectExampleInteractionLines(
282
282
  const initialMessage = parseResult.commitments.find((commitment) => commitment.type === 'INITIAL MESSAGE')?.content;
283
283
 
284
284
  if (initialMessage) {
285
- examples.push(`**Agent:**\n${initialMessage}`);
285
+ examples.push(
286
+ spaceTrim(
287
+ (block) => `
288
+ **Agent:**
289
+ ${block(initialMessage)}
290
+ `,
291
+ ),
292
+ );
286
293
  }
287
294
 
288
295
  if (samples && samples.length > 0) {
289
296
  for (const sample of samples) {
290
- examples.push(`**User:** ${sample.question}\n\n**Agent:**\n${sample.answer}`);
297
+ examples.push(
298
+ spaceTrim(
299
+ (block) => `
300
+ **User:** ${block(String(sample.question))}
301
+
302
+ **Agent:**
303
+ ${block(sample.answer)}
304
+ `,
305
+ ),
306
+ );
291
307
  }
292
308
  }
293
309
 
@@ -69,7 +69,13 @@ function removeLeadingTopLevelHeading(markdown: string): string {
69
69
  * @private internal utility of `createStandaloneBookLanguageMarkdown`
70
70
  */
71
71
  function renderDocumentationSection(title: string, documentation: string): string {
72
- return `#### ${title}\n\n${removeLeadingTopLevelHeading(documentation)}`;
72
+ return spaceTrim(
73
+ (block) => `
74
+ #### ${title}
75
+
76
+ ${block(removeLeadingTopLevelHeading(documentation))}
77
+ `,
78
+ );
73
79
  }
74
80
 
75
81
  /**
@@ -88,6 +88,7 @@ export function Chat(props: ChatProps) {
88
88
  feedbackMode = 'stars',
89
89
  feedbackTranslations,
90
90
  timingTranslations,
91
+ resolveCitationLabel,
91
92
  onFileUpload,
92
93
  chatLocale,
93
94
  speechRecognition,
@@ -337,6 +338,7 @@ export function Chat(props: ChatProps) {
337
338
  feedbackMode={feedbackMode}
338
339
  feedbackTranslations={feedbackTranslations}
339
340
  timingTranslations={timingTranslations}
341
+ resolveCitationLabel={resolveCitationLabel}
340
342
  chatLocale={chatLocale}
341
343
  onCopy={handleCopy}
342
344
  onMessage={onMessage}
@@ -404,6 +406,7 @@ export function Chat(props: ChatProps) {
404
406
  toolCallIdentity={selectedToolCallIdentity}
405
407
  onClose={closeToolCallModal}
406
408
  toolTitles={toolTitles}
409
+ resolveCitationLabel={resolveCitationLabel}
407
410
  agentParticipant={agentParticipant}
408
411
  buttonColor={buttonColor}
409
412
  teamAgentProfiles={teamAgentProfiles}
@@ -417,6 +420,7 @@ export function Chat(props: ChatProps) {
417
420
  isOpen={citationModalOpen}
418
421
  citation={selectedCitation}
419
422
  participants={participants}
423
+ resolveCitationLabel={resolveCitationLabel}
420
424
  soundSystem={soundSystem}
421
425
  onClose={closeCitationModal}
422
426
  />
@@ -5,7 +5,9 @@ import { DownloadIcon } from '../../icons/DownloadIcon';
5
5
  import { classNames } from '../../_common/react-utils/classNames';
6
6
  import { MarkdownContent } from '../MarkdownContent/MarkdownContent';
7
7
  import type { ChatParticipant } from '../types/ChatParticipant';
8
- import { getCitationLabel, isPlainTextCitation, resolveCitationPreviewUrl } from '../utils/citationHelpers';
8
+ import type { CitationLabelResolver } from '../types/CitationLabelResolver';
9
+ import { useResolvedCitationLabel } from '../hooks/useResolvedCitationLabel';
10
+ import { isPlainTextCitation, resolveCitationPreviewUrl } from '../utils/citationHelpers';
9
11
  import type { ParsedCitation } from '../utils/parseCitationsFromContent';
10
12
  import styles from './Chat.module.css';
11
13
  import { CitationIframePreview } from './CitationIframePreview';
@@ -20,17 +22,30 @@ export type ChatCitationModalProps = {
20
22
  isOpen: boolean;
21
23
  citation: ParsedCitation | null;
22
24
  participants: ReadonlyArray<ChatParticipant>;
25
+ resolveCitationLabel?: CitationLabelResolver;
23
26
  soundSystem?: ChatSoundSystem;
24
27
  onClose: () => void;
25
28
  };
26
29
 
30
+ /**
31
+ * Empty citation used to keep modal hooks unconditional while the modal is closed.
32
+ *
33
+ * @private component constant of `<ChatCitationModal/>`
34
+ */
35
+ const EMPTY_MODAL_CITATION: ParsedCitation = {
36
+ id: '',
37
+ source: '',
38
+ };
39
+
27
40
  /**
28
41
  * Modal that previews a citation source or excerpt.
29
42
  *
30
43
  * @private component of `<Chat/>`
31
44
  */
32
45
  export function ChatCitationModal(props: ChatCitationModalProps) {
33
- const { isOpen, citation, participants, soundSystem, onClose } = props;
46
+ const { isOpen, citation, participants, resolveCitationLabel, soundSystem, onClose } = props;
47
+ const resolvedCitation = citation || EMPTY_MODAL_CITATION;
48
+ const label = useResolvedCitationLabel(resolvedCitation, resolveCitationLabel);
34
49
 
35
50
  if (!isOpen || !citation) {
36
51
  return null;
@@ -43,7 +58,6 @@ export function ChatCitationModal(props: ChatCitationModalProps) {
43
58
  const previewSegment = previewBase.split('/').pop() || previewBase;
44
59
  const extension = previewSegment.split('.').pop()?.toLowerCase();
45
60
  const isImage = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'].includes(extension || '');
46
- const label = getCitationLabel(citation);
47
61
  const isTextCitation = isPlainTextCitation(citation);
48
62
  const hasTextPreview = !isValidUrl && (citation.excerpt || isTextCitation);
49
63
  const textPreviewContent = isTextCitation ? citation.source : citation.excerpt ?? '';