@promptbook/components 0.112.0-62 → 0.112.0-63

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 (57) hide show
  1. package/esm/index.es.js +2880 -775
  2. package/esm/index.es.js.map +1 -1
  3. package/esm/src/_packages/components.index.d.ts +4 -0
  4. package/esm/src/_packages/core.index.d.ts +2 -0
  5. package/esm/src/_packages/types.index.d.ts +2 -0
  6. package/esm/src/book-components/Chat/Chat/TeamToolCallModalContent.d.ts +0 -2
  7. package/esm/src/book-components/Chat/Chat/renderTimeoutToolCallDetails.d.ts +7 -1
  8. package/esm/src/book-components/Chat/Chat/useChatInputAreaComposer.d.ts +1 -1
  9. package/esm/src/book-components/Chat/Chat/useChatInputAreaDictation.d.ts +2 -2
  10. package/esm/src/book-components/Chat/hooks/useChatAutoScroll.d.ts +6 -3
  11. package/esm/src/book-components/Chat/types/ChatMessage.d.ts +34 -0
  12. package/esm/src/cli/cli-commands/agent/agentProjectPaths.d.ts +54 -0
  13. package/esm/src/cli/cli-commands/agent/agentRunCliOptions.d.ts +13 -0
  14. package/esm/src/cli/cli-commands/agent/init.d.ts +10 -0
  15. package/esm/src/cli/cli-commands/agent/initializeAgentProjectConfiguration.d.ts +21 -0
  16. package/esm/src/cli/cli-commands/agent/printAgentInitializationSummary.d.ts +7 -0
  17. package/esm/src/cli/cli-commands/agent/run.d.ts +10 -0
  18. package/esm/src/cli/cli-commands/agent/run.test.d.ts +1 -0
  19. package/esm/src/cli/cli-commands/agent/tick.d.ts +10 -0
  20. package/esm/src/cli/cli-commands/agent.d.ts +15 -0
  21. package/esm/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +86 -0
  22. package/esm/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +11 -0
  23. package/esm/src/commitments/KNOWLEDGE/KNOWLEDGE.test.d.ts +1 -0
  24. package/esm/src/commitments/_common/toolRuntimeContext.d.ts +6 -0
  25. package/esm/src/commitments/index.d.ts +2 -1
  26. package/esm/src/llm-providers/openai/OpenAiAgentKitExecutionTools.d.ts +4 -2
  27. package/esm/src/llm-providers/openai/OpenAiAgentKitExecutionToolsOptions.d.ts +9 -0
  28. package/esm/src/version.d.ts +1 -1
  29. package/package.json +1 -1
  30. package/umd/index.umd.js +2880 -775
  31. package/umd/index.umd.js.map +1 -1
  32. package/umd/src/_packages/components.index.d.ts +4 -0
  33. package/umd/src/_packages/core.index.d.ts +2 -0
  34. package/umd/src/_packages/types.index.d.ts +2 -0
  35. package/umd/src/book-components/Chat/Chat/TeamToolCallModalContent.d.ts +0 -2
  36. package/umd/src/book-components/Chat/Chat/renderTimeoutToolCallDetails.d.ts +7 -1
  37. package/umd/src/book-components/Chat/Chat/useChatInputAreaComposer.d.ts +1 -1
  38. package/umd/src/book-components/Chat/Chat/useChatInputAreaDictation.d.ts +2 -2
  39. package/umd/src/book-components/Chat/hooks/useChatAutoScroll.d.ts +6 -3
  40. package/umd/src/book-components/Chat/types/ChatMessage.d.ts +34 -0
  41. package/umd/src/cli/cli-commands/agent/agentProjectPaths.d.ts +54 -0
  42. package/umd/src/cli/cli-commands/agent/agentRunCliOptions.d.ts +13 -0
  43. package/umd/src/cli/cli-commands/agent/init.d.ts +10 -0
  44. package/umd/src/cli/cli-commands/agent/initializeAgentProjectConfiguration.d.ts +21 -0
  45. package/umd/src/cli/cli-commands/agent/printAgentInitializationSummary.d.ts +7 -0
  46. package/umd/src/cli/cli-commands/agent/run.d.ts +10 -0
  47. package/umd/src/cli/cli-commands/agent/run.test.d.ts +1 -0
  48. package/umd/src/cli/cli-commands/agent/tick.d.ts +10 -0
  49. package/umd/src/cli/cli-commands/agent.d.ts +15 -0
  50. package/umd/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +86 -0
  51. package/umd/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +11 -0
  52. package/umd/src/commitments/KNOWLEDGE/KNOWLEDGE.test.d.ts +1 -0
  53. package/umd/src/commitments/_common/toolRuntimeContext.d.ts +6 -0
  54. package/umd/src/commitments/index.d.ts +2 -1
  55. package/umd/src/llm-providers/openai/OpenAiAgentKitExecutionTools.d.ts +4 -2
  56. package/umd/src/llm-providers/openai/OpenAiAgentKitExecutionToolsOptions.d.ts +9 -0
  57. package/umd/src/version.d.ts +1 -1
package/esm/index.es.js CHANGED
@@ -40,7 +40,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
40
40
  * @generated
41
41
  * @see https://github.com/webgptorg/promptbook
42
42
  */
43
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-62';
43
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-63';
44
44
  /**
45
45
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
46
46
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -10898,6 +10898,18 @@ function parseDataUrlKnowledgeSource(source) {
10898
10898
  }
10899
10899
  // Note: [💞] Ignore a discrepancy between file name and entity name
10900
10900
 
10901
+ /**
10902
+ * Name of the tool used by agents to search configured `KNOWLEDGE` sources.
10903
+ *
10904
+ * @public exported from `@promptbook/core`
10905
+ */
10906
+ const KNOWLEDGE_SEARCH_TOOL_NAME = 'knowledge_search';
10907
+ /**
10908
+ * Title of the system-message section generated for `KNOWLEDGE` commitments.
10909
+ *
10910
+ * @private constant of `KnowledgeCommitmentDefinition`
10911
+ */
10912
+ const KNOWLEDGE_SEARCH_SYSTEM_SECTION_TITLE = 'Knowledge Search';
10901
10913
  /**
10902
10914
  * KNOWLEDGE commitment definition
10903
10915
  *
@@ -11019,9 +11031,17 @@ class KnowledgeCommitmentDefinition extends BaseCommitmentDefinition {
11019
11031
  knowledgeInfoEntries.push(`Knowledge Source Inline: ${inlineSource.filename} (derived from inline content and processed for retrieval during chat)`);
11020
11032
  }
11021
11033
  if (knowledgeInfoEntries.length === 0) {
11022
- return nextRequirements;
11034
+ return addKnowledgeSearchToolAndSystemSection(nextRequirements);
11023
11035
  }
11024
- return this.appendToSystemMessage(nextRequirements, knowledgeInfoEntries.join('\n'), '\n\n');
11036
+ return addKnowledgeSearchToolAndSystemSection(nextRequirements);
11037
+ }
11038
+ /**
11039
+ * Gets human-readable titles for tool functions provided by this commitment.
11040
+ */
11041
+ getToolTitles() {
11042
+ return {
11043
+ [KNOWLEDGE_SEARCH_TOOL_NAME]: 'Knowledge search',
11044
+ };
11025
11045
  }
11026
11046
  }
11027
11047
  /**
@@ -11035,6 +11055,128 @@ function hasMeaningfulNonUrlText(content, urls) {
11035
11055
  const significantText = contentWithoutUrls.replace(/[\s.,!?;:'"`()[\]{}<>/-]+/g, '');
11036
11056
  return significantText.length > 0;
11037
11057
  }
11058
+ /**
11059
+ * Adds the shared `knowledge_search` tool definition and the consolidated system-message section.
11060
+ *
11061
+ * @param requirements - Requirements after one `KNOWLEDGE` commitment was applied.
11062
+ * @returns Requirements with the knowledge search instructions and tool definition.
11063
+ *
11064
+ * @private internal utility of `KnowledgeCommitmentDefinition`
11065
+ */
11066
+ function addKnowledgeSearchToolAndSystemSection(requirements) {
11067
+ const nextRequirements = addKnowledgeSearchTool(requirements);
11068
+ const section = createKnowledgeSearchSystemSection(nextRequirements);
11069
+ const sectionHeader = `## ${KNOWLEDGE_SEARCH_SYSTEM_SECTION_TITLE}`;
11070
+ if (nextRequirements.systemMessage.includes(sectionHeader)) {
11071
+ return {
11072
+ ...nextRequirements,
11073
+ systemMessage: nextRequirements.systemMessage.replace(new RegExp(`## ${KNOWLEDGE_SEARCH_SYSTEM_SECTION_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?(?=\\n\\n##|$)`), section),
11074
+ };
11075
+ }
11076
+ return {
11077
+ ...nextRequirements,
11078
+ systemMessage: nextRequirements.systemMessage.trim()
11079
+ ? `${nextRequirements.systemMessage}\n\n${section}`
11080
+ : section,
11081
+ };
11082
+ }
11083
+ /**
11084
+ * Adds the `knowledge_search` model tool when it is not already present.
11085
+ *
11086
+ * @param requirements - Current model requirements.
11087
+ * @returns Requirements with the tool definition available to the model.
11088
+ *
11089
+ * @private internal utility of `KnowledgeCommitmentDefinition`
11090
+ */
11091
+ function addKnowledgeSearchTool(requirements) {
11092
+ const existingTools = requirements.tools || [];
11093
+ if (existingTools.some((tool) => tool.name === KNOWLEDGE_SEARCH_TOOL_NAME)) {
11094
+ return requirements;
11095
+ }
11096
+ return {
11097
+ ...requirements,
11098
+ tools: [
11099
+ ...existingTools,
11100
+ {
11101
+ name: KNOWLEDGE_SEARCH_TOOL_NAME,
11102
+ description: spaceTrim$1(`
11103
+ Search the agent's configured knowledge sources and return relevant excerpts with citation ids.
11104
+ Use this before answering questions that may depend on the agent's KNOWLEDGE commitments.
11105
+ `),
11106
+ parameters: {
11107
+ type: 'object',
11108
+ properties: {
11109
+ query: {
11110
+ type: 'string',
11111
+ description: 'The natural-language search query for the knowledge base.',
11112
+ },
11113
+ limit: {
11114
+ type: 'integer',
11115
+ description: 'Maximum number of matching source excerpts to return.',
11116
+ },
11117
+ },
11118
+ required: ['query'],
11119
+ },
11120
+ },
11121
+ ],
11122
+ };
11123
+ }
11124
+ /**
11125
+ * Creates the model-facing system-message section for knowledge search.
11126
+ *
11127
+ * @param requirements - Current model requirements.
11128
+ * @returns Markdown system-message section.
11129
+ *
11130
+ * @private internal utility of `KnowledgeCommitmentDefinition`
11131
+ */
11132
+ function createKnowledgeSearchSystemSection(requirements) {
11133
+ const sourceEntries = createKnowledgeSourceSystemEntries(requirements);
11134
+ const sourceList = sourceEntries.length > 0 ? sourceEntries.map((entry) => `- ${entry}`).join('\n') : '- None';
11135
+ return spaceTrim$1(`
11136
+ ## ${KNOWLEDGE_SEARCH_SYSTEM_SECTION_TITLE}
11137
+
11138
+ - Use \`${KNOWLEDGE_SEARCH_TOOL_NAME}\` to search the configured knowledge sources before answering questions that depend on this agent's knowledge base.
11139
+ - Base source-backed factual answers on the returned excerpts.
11140
+ - When you use a returned excerpt, include its citation marker in the answer body, for example \`[0:0]\`.
11141
+ - If the search returns no relevant information, say that the knowledge base did not contain the answer instead of inventing it.
11142
+
11143
+ Configured knowledge sources:
11144
+ ${sourceList}
11145
+ `);
11146
+ }
11147
+ /**
11148
+ * Builds a stable list of configured knowledge sources for system-message diagnostics.
11149
+ *
11150
+ * @param requirements - Current model requirements.
11151
+ * @returns Human-readable source entries.
11152
+ *
11153
+ * @private internal utility of `KnowledgeCommitmentDefinition`
11154
+ */
11155
+ function createKnowledgeSourceSystemEntries(requirements) {
11156
+ var _a;
11157
+ const entries = [];
11158
+ const seenEntries = new Set();
11159
+ for (const source of requirements.knowledgeSources || []) {
11160
+ const entry = `Source URL: ${source} (processed for retrieval during chat)`;
11161
+ if (seenEntries.has(entry)) {
11162
+ continue;
11163
+ }
11164
+ seenEntries.add(entry);
11165
+ entries.push(entry);
11166
+ }
11167
+ const inlineSources = (((_a = requirements._metadata) === null || _a === void 0 ? void 0 : _a.inlineKnowledgeSources) || [])
11168
+ .map((source) => source.filename)
11169
+ .filter(Boolean);
11170
+ for (const filename of inlineSources) {
11171
+ const entry = `Knowledge Source Inline: ${filename} (Inline source: processed for retrieval during chat)`;
11172
+ if (seenEntries.has(entry)) {
11173
+ continue;
11174
+ }
11175
+ seenEntries.add(entry);
11176
+ entries.push(entry);
11177
+ }
11178
+ return entries;
11179
+ }
11038
11180
 
11039
11181
  /**
11040
11182
  * LANGUAGE commitment definition
@@ -15637,7 +15779,7 @@ function resolveUseCalendarToolRuntime(args) {
15637
15779
  const runtimeContext = (readToolRuntimeContextFromToolArgs(args) ||
15638
15780
  {});
15639
15781
  const configuredCalendars = normalizeConfiguredCalendars$1((_a = runtimeContext.calendars) === null || _a === void 0 ? void 0 : _a.connections);
15640
- const calendarArgument = normalizeOptionalText$1(args.calendarUrl);
15782
+ const calendarArgument = normalizeOptionalText$2(args.calendarUrl);
15641
15783
  let calendarReference = null;
15642
15784
  if (calendarArgument) {
15643
15785
  calendarReference = parseGoogleCalendarReference(calendarArgument);
@@ -15657,7 +15799,7 @@ function resolveUseCalendarToolRuntime(args) {
15657
15799
  if (!calendarReference) {
15658
15800
  throw new Error('Calendar is required but was not resolved.');
15659
15801
  }
15660
- const accessToken = normalizeOptionalText$1((_b = runtimeContext.calendars) === null || _b === void 0 ? void 0 : _b.googleAccessToken) || '';
15802
+ const accessToken = normalizeOptionalText$2((_b = runtimeContext.calendars) === null || _b === void 0 ? void 0 : _b.googleAccessToken) || '';
15661
15803
  if (!accessToken) {
15662
15804
  throw new CalendarWalletCredentialRequiredError({
15663
15805
  calendarReference,
@@ -15690,7 +15832,7 @@ function normalizeConfiguredCalendars$1(rawCalendars) {
15690
15832
  continue;
15691
15833
  }
15692
15834
  const calendar = rawCalendar;
15693
- const rawUrl = normalizeOptionalText$1(calendar.url);
15835
+ const rawUrl = normalizeOptionalText$2(calendar.url);
15694
15836
  if (!rawUrl) {
15695
15837
  continue;
15696
15838
  }
@@ -15741,7 +15883,7 @@ function createCalendarWalletCredentialRequiredResult(error) {
15741
15883
  *
15742
15884
  * @private function of resolveUseCalendarToolRuntimeOrWalletCredentialResult
15743
15885
  */
15744
- function normalizeOptionalText$1(value) {
15886
+ function normalizeOptionalText$2(value) {
15745
15887
  if (typeof value !== 'string') {
15746
15888
  return undefined;
15747
15889
  }
@@ -15773,13 +15915,13 @@ function createUseCalendarToolFunctions() {
15773
15915
  async [UseCalendarToolNames.listEvents](args) {
15774
15916
  return withUseCalendarRuntime(args, async ({ calendarReference, accessToken }) => {
15775
15917
  const query = {};
15776
- if (normalizeOptionalText(args.timeMin)) {
15918
+ if (normalizeOptionalText$1(args.timeMin)) {
15777
15919
  query.timeMin = args.timeMin.trim();
15778
15920
  }
15779
- if (normalizeOptionalText(args.timeMax)) {
15921
+ if (normalizeOptionalText$1(args.timeMax)) {
15780
15922
  query.timeMax = args.timeMax.trim();
15781
15923
  }
15782
- if (normalizeOptionalText(args.query)) {
15924
+ if (normalizeOptionalText$1(args.query)) {
15783
15925
  query.q = args.query.trim();
15784
15926
  }
15785
15927
  if (typeof args.maxResults === 'number' && Number.isFinite(args.maxResults) && args.maxResults > 0) {
@@ -15791,7 +15933,7 @@ function createUseCalendarToolFunctions() {
15791
15933
  if (args.orderBy === 'startTime' || args.orderBy === 'updated') {
15792
15934
  query.orderBy = args.orderBy;
15793
15935
  }
15794
- if (normalizeOptionalText(args.timeZone)) {
15936
+ if (normalizeOptionalText$1(args.timeZone)) {
15795
15937
  query.timeZone = args.timeZone.trim();
15796
15938
  }
15797
15939
  const payload = await callGoogleCalendarApi(accessToken, {
@@ -15829,11 +15971,11 @@ function createUseCalendarToolFunctions() {
15829
15971
  return withUseCalendarRuntime(args, async ({ calendarReference, accessToken }) => {
15830
15972
  const requestBody = createGoogleCalendarEventPayload({
15831
15973
  summary: normalizeRequiredText(args.summary, 'summary'),
15832
- description: normalizeOptionalText(args.description),
15833
- location: normalizeOptionalText(args.location),
15974
+ description: normalizeOptionalText$1(args.description),
15975
+ location: normalizeOptionalText$1(args.location),
15834
15976
  start: normalizeRequiredText(args.start, 'start'),
15835
15977
  end: normalizeRequiredText(args.end, 'end'),
15836
- timeZone: normalizeOptionalText(args.timeZone),
15978
+ timeZone: normalizeOptionalText$1(args.timeZone),
15837
15979
  attendees: normalizeAttendees(args.attendees),
15838
15980
  reminderMinutes: normalizeReminderMinutes(args.reminderMinutes),
15839
15981
  });
@@ -15855,12 +15997,12 @@ function createUseCalendarToolFunctions() {
15855
15997
  return withUseCalendarRuntime(args, async ({ calendarReference, accessToken }) => {
15856
15998
  const eventId = normalizeRequiredText(args.eventId, 'eventId');
15857
15999
  const requestBody = createGoogleCalendarEventPayload({
15858
- summary: normalizeOptionalText(args.summary),
15859
- description: normalizeOptionalText(args.description),
15860
- location: normalizeOptionalText(args.location),
15861
- start: normalizeOptionalText(args.start),
15862
- end: normalizeOptionalText(args.end),
15863
- timeZone: normalizeOptionalText(args.timeZone),
16000
+ summary: normalizeOptionalText$1(args.summary),
16001
+ description: normalizeOptionalText$1(args.description),
16002
+ location: normalizeOptionalText$1(args.location),
16003
+ start: normalizeOptionalText$1(args.start),
16004
+ end: normalizeOptionalText$1(args.end),
16005
+ timeZone: normalizeOptionalText$1(args.timeZone),
15864
16006
  attendees: normalizeAttendees(args.attendees),
15865
16007
  reminderMinutes: normalizeReminderMinutes(args.reminderMinutes),
15866
16008
  });
@@ -15907,7 +16049,7 @@ function createUseCalendarToolFunctions() {
15907
16049
  path: `/calendars/${encodeGoogleCalendarId(calendarReference.calendarId)}/events/${encodeURIComponent(eventId)}`,
15908
16050
  });
15909
16051
  const existingAttendees = ((existingEvent === null || existingEvent === void 0 ? void 0 : existingEvent.attendees) || [])
15910
- .map((attendee) => normalizeOptionalText(attendee.email))
16052
+ .map((attendee) => normalizeOptionalText$1(attendee.email))
15911
16053
  .filter((email) => Boolean(email));
15912
16054
  const mergedAttendees = [...new Set([...existingAttendees, ...guests])];
15913
16055
  const payload = await callGoogleCalendarApi(accessToken, {
@@ -15955,7 +16097,7 @@ function encodeGoogleCalendarId(calendarId) {
15955
16097
  * @private function of createUseCalendarToolFunctions
15956
16098
  */
15957
16099
  function normalizeRequiredText(value, fieldName) {
15958
- const normalizedValue = normalizeOptionalText(value);
16100
+ const normalizedValue = normalizeOptionalText$1(value);
15959
16101
  if (!normalizedValue) {
15960
16102
  throw new Error(`Tool "${fieldName}" requires non-empty value.`);
15961
16103
  }
@@ -15966,7 +16108,7 @@ function normalizeRequiredText(value, fieldName) {
15966
16108
  *
15967
16109
  * @private function of createUseCalendarToolFunctions
15968
16110
  */
15969
- function normalizeOptionalText(value) {
16111
+ function normalizeOptionalText$1(value) {
15970
16112
  if (typeof value !== 'string') {
15971
16113
  return undefined;
15972
16114
  }
@@ -20652,6 +20794,580 @@ class UseUserLocationCommitmentDefinition extends BaseCommitmentDefinition {
20652
20794
  }
20653
20795
  // Note: [💞] Ignore a discrepancy between file name and entity name
20654
20796
 
20797
+ /**
20798
+ * Names of tools used by the WALLET commitment.
20799
+ *
20800
+ * @private constant of WalletCommitmentDefinition
20801
+ */
20802
+ const WalletToolNames = {
20803
+ retrieve: 'retrieve_wallet_records',
20804
+ store: 'store_wallet_record',
20805
+ update: 'update_wallet_record',
20806
+ delete: 'delete_wallet_record',
20807
+ request: 'request_wallet_record',
20808
+ };
20809
+
20810
+ /**
20811
+ * Creates WALLET system-message instructions.
20812
+ *
20813
+ * @private function of WalletCommitmentDefinition
20814
+ */
20815
+ function createWalletSystemMessage(extraInstructions) {
20816
+ return spaceTrim$1((block) => `
20817
+ Wallet:
20818
+ - Use "${WalletToolNames.retrieve}" before authenticated operations.
20819
+ - Use "${WalletToolNames.store}" and "${WalletToolNames.update}" to maintain credentials.
20820
+ - Use "${WalletToolNames.delete}" to remove invalid credentials.
20821
+ - Use "${WalletToolNames.request}" to request missing credentials via UI popup.
20822
+ - Scope records by user (\`isUserScoped\`) and/or by agent (\`isGlobal=false\`) as needed.
20823
+ - Never expose raw credentials in chat responses.
20824
+ ${block(extraInstructions)}
20825
+ `);
20826
+ }
20827
+
20828
+ /**
20829
+ * Resolves disabled message for wallet runtime context.
20830
+ *
20831
+ * @private function of WalletCommitmentDefinition
20832
+ */
20833
+ function resolveWalletDisabledMessage(runtimeContext) {
20834
+ if (runtimeContext.isPrivateMode) {
20835
+ return 'Wallet is disabled because private mode is active.';
20836
+ }
20837
+ if (runtimeContext.isTeamConversation) {
20838
+ return 'Wallet is disabled for TEAM conversations.';
20839
+ }
20840
+ if (!runtimeContext.enabled) {
20841
+ return 'Wallet is disabled for unauthenticated users.';
20842
+ }
20843
+ return null;
20844
+ }
20845
+ /**
20846
+ * Resolves runtime adapter for wallet tools or returns disabled payload when unavailable.
20847
+ *
20848
+ * @private function of WalletCommitmentDefinition
20849
+ */
20850
+ function getWalletToolRuntimeAdapterOrDisabledResult(action, runtimeContext) {
20851
+ const disabledMessage = resolveWalletDisabledMessage(runtimeContext);
20852
+ if (disabledMessage) {
20853
+ return {
20854
+ adapter: null,
20855
+ disabledResult: {
20856
+ action,
20857
+ status: 'disabled',
20858
+ records: action === 'retrieve' ? [] : undefined,
20859
+ message: disabledMessage,
20860
+ },
20861
+ };
20862
+ }
20863
+ {
20864
+ return {
20865
+ adapter: null,
20866
+ disabledResult: {
20867
+ action,
20868
+ status: 'disabled',
20869
+ records: action === 'retrieve' ? [] : undefined,
20870
+ message: 'Wallet runtime is not available in this environment.',
20871
+ },
20872
+ };
20873
+ }
20874
+ }
20875
+
20876
+ /**
20877
+ * Parses store/update wallet payload.
20878
+ *
20879
+ * @private function of WalletCommitmentDefinition
20880
+ */
20881
+ function parseWalletPayload(args) {
20882
+ const recordType = parseWalletRecordType(args.recordType);
20883
+ return {
20884
+ recordType,
20885
+ service: parseWalletService(args.service),
20886
+ key: parseWalletKey(args.key),
20887
+ isUserScoped: args.isUserScoped === true,
20888
+ isGlobal: args.isGlobal === true,
20889
+ ...parseWalletSecrets({
20890
+ recordType,
20891
+ username: args.username,
20892
+ password: args.password,
20893
+ secret: args.secret,
20894
+ cookies: args.cookies,
20895
+ }),
20896
+ };
20897
+ }
20898
+ /**
20899
+ * Parses text argument and returns trimmed text when available.
20900
+ *
20901
+ * @private function of WalletCommitmentDefinition
20902
+ */
20903
+ function normalizeOptionalText(value) {
20904
+ if (typeof value !== 'string') {
20905
+ return undefined;
20906
+ }
20907
+ const trimmed = value.trim();
20908
+ return trimmed || undefined;
20909
+ }
20910
+ /**
20911
+ * Parses wallet service argument.
20912
+ *
20913
+ * @private function of WalletCommitmentDefinition
20914
+ */
20915
+ function parseWalletService(value) {
20916
+ return (normalizeOptionalText(value) || 'generic').toLowerCase();
20917
+ }
20918
+ /**
20919
+ * Parses wallet key argument.
20920
+ *
20921
+ * @private function of WalletCommitmentDefinition
20922
+ */
20923
+ function parseWalletKey(value) {
20924
+ return normalizeOptionalText(value) || 'default';
20925
+ }
20926
+ /**
20927
+ * Parses one wallet record id argument.
20928
+ *
20929
+ * @private function of WalletCommitmentDefinition
20930
+ */
20931
+ function parseWalletId(value) {
20932
+ const walletId = normalizeOptionalText(value);
20933
+ if (!walletId) {
20934
+ throw new Error('Wallet id is required.');
20935
+ }
20936
+ return walletId;
20937
+ }
20938
+ /**
20939
+ * Parses wallet record type.
20940
+ *
20941
+ * @private function of WalletCommitmentDefinition
20942
+ */
20943
+ function parseWalletRecordType(value, fallback) {
20944
+ var _a;
20945
+ const normalizedType = (_a = normalizeOptionalText(value)) === null || _a === void 0 ? void 0 : _a.toUpperCase();
20946
+ if (normalizedType === 'USERNAME_PASSWORD') {
20947
+ return 'USERNAME_PASSWORD';
20948
+ }
20949
+ if (normalizedType === 'SESSION_COOKIE') {
20950
+ return 'SESSION_COOKIE';
20951
+ }
20952
+ if (normalizedType === 'ACCESS_TOKEN') {
20953
+ return 'ACCESS_TOKEN';
20954
+ }
20955
+ if (fallback) {
20956
+ return fallback;
20957
+ }
20958
+ throw new Error('Unsupported wallet recordType. Expected one of: USERNAME_PASSWORD, SESSION_COOKIE, ACCESS_TOKEN.');
20959
+ }
20960
+ /**
20961
+ * Parses wallet secret fields according to record type.
20962
+ *
20963
+ * @private function of WalletCommitmentDefinition
20964
+ */
20965
+ function parseWalletSecrets(args) {
20966
+ const username = normalizeOptionalText(args.username);
20967
+ const password = normalizeOptionalText(args.password);
20968
+ const secret = normalizeOptionalText(args.secret);
20969
+ const cookies = normalizeOptionalText(args.cookies);
20970
+ if (args.recordType === 'USERNAME_PASSWORD') {
20971
+ if (!username || !password) {
20972
+ throw new Error('Both username and password are required for USERNAME_PASSWORD.');
20973
+ }
20974
+ return { username, password };
20975
+ }
20976
+ if (args.recordType === 'SESSION_COOKIE') {
20977
+ if (!cookies) {
20978
+ throw new Error('Cookies are required for SESSION_COOKIE.');
20979
+ }
20980
+ return { cookies };
20981
+ }
20982
+ if (!secret) {
20983
+ throw new Error('Secret is required for ACCESS_TOKEN.');
20984
+ }
20985
+ return { secret };
20986
+ }
20987
+ /**
20988
+ * Collection of WALLET tool argument parsers.
20989
+ *
20990
+ * @private function of WalletCommitmentDefinition
20991
+ */
20992
+ const parseWalletToolArgs = {
20993
+ /**
20994
+ * Parses retrieve arguments.
20995
+ */
20996
+ retrieve(args) {
20997
+ const limit = typeof args.limit === 'number' && Number.isFinite(args.limit) ? Math.floor(args.limit) : undefined;
20998
+ return {
20999
+ query: normalizeOptionalText(args.query),
21000
+ recordType: normalizeOptionalText(args.recordType) ? parseWalletRecordType(args.recordType) : undefined,
21001
+ service: normalizeOptionalText(args.service) ? parseWalletService(args.service) : undefined,
21002
+ key: normalizeOptionalText(args.key) ? parseWalletKey(args.key) : undefined,
21003
+ limit: limit && limit > 0 ? Math.min(limit, 20) : undefined,
21004
+ };
21005
+ },
21006
+ /**
21007
+ * Parses store payload.
21008
+ */
21009
+ store(args) {
21010
+ return parseWalletPayload(args);
21011
+ },
21012
+ /**
21013
+ * Parses update payload.
21014
+ */
21015
+ update(args) {
21016
+ const walletId = parseWalletId(args.walletId);
21017
+ const record = parseWalletPayload(args);
21018
+ return {
21019
+ ...record,
21020
+ walletId,
21021
+ };
21022
+ },
21023
+ /**
21024
+ * Parses delete payload.
21025
+ */
21026
+ delete(args) {
21027
+ return { walletId: parseWalletId(args.walletId) };
21028
+ },
21029
+ /**
21030
+ * Parses request payload for user wallet input prompt.
21031
+ */
21032
+ request(args) {
21033
+ return {
21034
+ recordType: parseWalletRecordType(args.recordType, 'ACCESS_TOKEN'),
21035
+ service: parseWalletService(args.service),
21036
+ key: parseWalletKey(args.key),
21037
+ message: normalizeOptionalText(args.message),
21038
+ isUserScoped: args.isUserScoped === true,
21039
+ isGlobal: args.isGlobal === true,
21040
+ };
21041
+ },
21042
+ };
21043
+
21044
+ /**
21045
+ * Resolves runtime context from hidden tool arguments.
21046
+ *
21047
+ * @private function of WalletCommitmentDefinition
21048
+ */
21049
+ function resolveWalletRuntimeContext(args) {
21050
+ const runtimeContext = readToolRuntimeContextFromToolArgs(args);
21051
+ const memoryContext = runtimeContext === null || runtimeContext === void 0 ? void 0 : runtimeContext.memory;
21052
+ return {
21053
+ enabled: (memoryContext === null || memoryContext === void 0 ? void 0 : memoryContext.enabled) === true,
21054
+ userId: memoryContext === null || memoryContext === void 0 ? void 0 : memoryContext.userId,
21055
+ username: memoryContext === null || memoryContext === void 0 ? void 0 : memoryContext.username,
21056
+ agentId: memoryContext === null || memoryContext === void 0 ? void 0 : memoryContext.agentId,
21057
+ agentName: memoryContext === null || memoryContext === void 0 ? void 0 : memoryContext.agentName,
21058
+ isTeamConversation: (memoryContext === null || memoryContext === void 0 ? void 0 : memoryContext.isTeamConversation) === true,
21059
+ isPrivateMode: (memoryContext === null || memoryContext === void 0 ? void 0 : memoryContext.isPrivateMode) === true,
21060
+ };
21061
+ }
21062
+
21063
+ /**
21064
+ * Creates runtime wallet tool function implementations.
21065
+ *
21066
+ * @private function of WalletCommitmentDefinition
21067
+ */
21068
+ function createWalletToolFunctions() {
21069
+ return {
21070
+ async [WalletToolNames.retrieve](args) {
21071
+ const runtimeContext = resolveWalletRuntimeContext(args);
21072
+ const { adapter, disabledResult } = getWalletToolRuntimeAdapterOrDisabledResult('retrieve', runtimeContext);
21073
+ if (!adapter || disabledResult) {
21074
+ return JSON.stringify(disabledResult);
21075
+ }
21076
+ try {
21077
+ const parsedArgs = parseWalletToolArgs.retrieve(args);
21078
+ const records = await adapter.retrieveWalletRecords(parsedArgs, runtimeContext);
21079
+ return JSON.stringify({
21080
+ action: 'retrieve',
21081
+ status: 'ok',
21082
+ query: parsedArgs.query,
21083
+ records,
21084
+ });
21085
+ }
21086
+ catch (error) {
21087
+ return JSON.stringify({
21088
+ action: 'retrieve',
21089
+ status: 'error',
21090
+ records: [],
21091
+ message: error instanceof Error ? error.message : String(error),
21092
+ });
21093
+ }
21094
+ },
21095
+ async [WalletToolNames.store](args) {
21096
+ const runtimeContext = resolveWalletRuntimeContext(args);
21097
+ const { adapter, disabledResult } = getWalletToolRuntimeAdapterOrDisabledResult('store', runtimeContext);
21098
+ if (!adapter || disabledResult) {
21099
+ return JSON.stringify(disabledResult);
21100
+ }
21101
+ try {
21102
+ const parsedArgs = parseWalletToolArgs.store(args);
21103
+ const record = await adapter.storeWalletRecord(parsedArgs, runtimeContext);
21104
+ return JSON.stringify({
21105
+ action: 'store',
21106
+ status: 'stored',
21107
+ record,
21108
+ });
21109
+ }
21110
+ catch (error) {
21111
+ return JSON.stringify({
21112
+ action: 'store',
21113
+ status: 'error',
21114
+ message: error instanceof Error ? error.message : String(error),
21115
+ });
21116
+ }
21117
+ },
21118
+ async [WalletToolNames.update](args) {
21119
+ const runtimeContext = resolveWalletRuntimeContext(args);
21120
+ const { adapter, disabledResult } = getWalletToolRuntimeAdapterOrDisabledResult('update', runtimeContext);
21121
+ if (!adapter || disabledResult) {
21122
+ return JSON.stringify(disabledResult);
21123
+ }
21124
+ try {
21125
+ const parsedArgs = parseWalletToolArgs.update(args);
21126
+ const record = await adapter.updateWalletRecord(parsedArgs, runtimeContext);
21127
+ return JSON.stringify({
21128
+ action: 'update',
21129
+ status: 'updated',
21130
+ record,
21131
+ });
21132
+ }
21133
+ catch (error) {
21134
+ return JSON.stringify({
21135
+ action: 'update',
21136
+ status: 'error',
21137
+ message: error instanceof Error ? error.message : String(error),
21138
+ });
21139
+ }
21140
+ },
21141
+ async [WalletToolNames.delete](args) {
21142
+ const runtimeContext = resolveWalletRuntimeContext(args);
21143
+ const { adapter, disabledResult } = getWalletToolRuntimeAdapterOrDisabledResult('delete', runtimeContext);
21144
+ if (!adapter || disabledResult) {
21145
+ return JSON.stringify(disabledResult);
21146
+ }
21147
+ try {
21148
+ const parsedArgs = parseWalletToolArgs.delete(args);
21149
+ const deleted = await adapter.deleteWalletRecord(parsedArgs, runtimeContext);
21150
+ return JSON.stringify({
21151
+ action: 'delete',
21152
+ status: 'deleted',
21153
+ walletId: deleted.id,
21154
+ });
21155
+ }
21156
+ catch (error) {
21157
+ return JSON.stringify({
21158
+ action: 'delete',
21159
+ status: 'error',
21160
+ message: error instanceof Error ? error.message : String(error),
21161
+ });
21162
+ }
21163
+ },
21164
+ async [WalletToolNames.request](args) {
21165
+ const runtimeContext = resolveWalletRuntimeContext(args);
21166
+ const disabledMessage = resolveWalletDisabledMessage(runtimeContext);
21167
+ if (disabledMessage) {
21168
+ return JSON.stringify({
21169
+ action: 'request',
21170
+ status: 'disabled',
21171
+ message: disabledMessage,
21172
+ });
21173
+ }
21174
+ const request = parseWalletToolArgs.request(args);
21175
+ return JSON.stringify({
21176
+ action: 'request',
21177
+ status: 'requested',
21178
+ request,
21179
+ message: request.message ||
21180
+ `Request user to provide ${request.recordType} credentials for service "${request.service}".`,
21181
+ });
21182
+ },
21183
+ };
21184
+ }
21185
+
21186
+ /**
21187
+ * Creates tool definitions required by WALLET commitment.
21188
+ *
21189
+ * @private function of WalletCommitmentDefinition
21190
+ */
21191
+ function createWalletTools(existingTools) {
21192
+ const tools = [...(existingTools || [])];
21193
+ addWalletToolIfMissing(tools, {
21194
+ name: WalletToolNames.retrieve,
21195
+ description: 'Retrieve wallet records relevant to the current task.',
21196
+ parameters: {
21197
+ type: 'object',
21198
+ properties: {
21199
+ query: { type: 'string', description: 'Optional text query used to filter wallet records.' },
21200
+ recordType: {
21201
+ type: 'string',
21202
+ description: 'Optional record type filter (USERNAME_PASSWORD, SESSION_COOKIE, ACCESS_TOKEN).',
21203
+ },
21204
+ service: { type: 'string', description: 'Optional service filter, for example github.' },
21205
+ key: { type: 'string', description: 'Optional wallet key filter.' },
21206
+ limit: { type: 'integer', description: 'Optional maximum number of records (default 5, max 20).' },
21207
+ },
21208
+ required: [],
21209
+ },
21210
+ });
21211
+ addWalletToolIfMissing(tools, {
21212
+ name: WalletToolNames.store,
21213
+ description: 'Store one wallet record.',
21214
+ parameters: {
21215
+ type: 'object',
21216
+ properties: {
21217
+ recordType: {
21218
+ type: 'string',
21219
+ description: 'Record type: USERNAME_PASSWORD, SESSION_COOKIE, ACCESS_TOKEN.',
21220
+ },
21221
+ service: { type: 'string', description: 'Service identifier, for example github.' },
21222
+ key: { type: 'string', description: 'Logical credential key.' },
21223
+ username: { type: 'string', description: 'Username for USERNAME_PASSWORD.' },
21224
+ password: { type: 'string', description: 'Password for USERNAME_PASSWORD.' },
21225
+ secret: { type: 'string', description: 'Token/API key for ACCESS_TOKEN.' },
21226
+ cookies: { type: 'string', description: 'Cookie header/json for SESSION_COOKIE.' },
21227
+ isUserScoped: { type: 'boolean', description: 'Set true to scope this record to current user.' },
21228
+ isGlobal: { type: 'boolean', description: 'Set true to make this record global.' },
21229
+ },
21230
+ required: ['recordType', 'service'],
21231
+ },
21232
+ });
21233
+ addWalletToolIfMissing(tools, {
21234
+ name: WalletToolNames.update,
21235
+ description: 'Update one existing wallet record.',
21236
+ parameters: {
21237
+ type: 'object',
21238
+ properties: {
21239
+ walletId: { type: 'string', description: 'Wallet record id to update.' },
21240
+ recordType: {
21241
+ type: 'string',
21242
+ description: 'Record type: USERNAME_PASSWORD, SESSION_COOKIE, ACCESS_TOKEN.',
21243
+ },
21244
+ service: { type: 'string', description: 'Service identifier, for example github.' },
21245
+ key: { type: 'string', description: 'Logical credential key.' },
21246
+ username: { type: 'string', description: 'Username for USERNAME_PASSWORD.' },
21247
+ password: { type: 'string', description: 'Password for USERNAME_PASSWORD.' },
21248
+ secret: { type: 'string', description: 'Token/API key for ACCESS_TOKEN.' },
21249
+ cookies: { type: 'string', description: 'Cookie header/json for SESSION_COOKIE.' },
21250
+ isUserScoped: { type: 'boolean', description: 'Set true to scope this record to current user.' },
21251
+ isGlobal: { type: 'boolean', description: 'Set true to make this record global.' },
21252
+ },
21253
+ required: ['walletId', 'recordType', 'service'],
21254
+ },
21255
+ });
21256
+ addWalletToolIfMissing(tools, {
21257
+ name: WalletToolNames.delete,
21258
+ description: 'Delete one wallet record.',
21259
+ parameters: {
21260
+ type: 'object',
21261
+ properties: {
21262
+ walletId: { type: 'string', description: 'Wallet record id to delete.' },
21263
+ },
21264
+ required: ['walletId'],
21265
+ },
21266
+ });
21267
+ addWalletToolIfMissing(tools, {
21268
+ name: WalletToolNames.request,
21269
+ description: 'Request missing credential from user via popup.',
21270
+ parameters: {
21271
+ type: 'object',
21272
+ properties: {
21273
+ recordType: {
21274
+ type: 'string',
21275
+ description: 'Requested record type: USERNAME_PASSWORD, SESSION_COOKIE, ACCESS_TOKEN.',
21276
+ },
21277
+ service: { type: 'string', description: 'Service identifier.' },
21278
+ key: { type: 'string', description: 'Logical credential key.' },
21279
+ message: { type: 'string', description: 'Optional UI message for user.' },
21280
+ isUserScoped: {
21281
+ type: 'boolean',
21282
+ description: 'Set true when record should be scoped to current user.',
21283
+ },
21284
+ isGlobal: { type: 'boolean', description: 'Set true when record should be global.' },
21285
+ },
21286
+ required: [],
21287
+ },
21288
+ });
21289
+ return tools;
21290
+ }
21291
+ /**
21292
+ * Registers one wallet tool when missing in current tool list.
21293
+ *
21294
+ * @private function of WalletCommitmentDefinition
21295
+ */
21296
+ function addWalletToolIfMissing(tools, tool) {
21297
+ if (!tools.some((existingTool) => existingTool.name === tool.name)) {
21298
+ tools.push(tool);
21299
+ }
21300
+ }
21301
+
21302
+ /**
21303
+ * Gets markdown documentation for WALLET commitment.
21304
+ *
21305
+ * @private function of WalletCommitmentDefinition
21306
+ */
21307
+ function getWalletCommitmentDocumentation(type) {
21308
+ return spaceTrim$1(`
21309
+ # ${type}
21310
+
21311
+ Enables private credential storage for tokens, usernames/passwords, and session cookies.
21312
+ `);
21313
+ }
21314
+
21315
+ /**
21316
+ * Gets human-readable titles for WALLET tool functions.
21317
+ *
21318
+ * @private function of WalletCommitmentDefinition
21319
+ */
21320
+ function getWalletToolTitles() {
21321
+ return {
21322
+ [WalletToolNames.retrieve]: 'Wallet',
21323
+ [WalletToolNames.store]: 'Store wallet record',
21324
+ [WalletToolNames.update]: 'Update wallet record',
21325
+ [WalletToolNames.delete]: 'Delete wallet record',
21326
+ [WalletToolNames.request]: 'Request wallet record',
21327
+ };
21328
+ }
21329
+
21330
+ /**
21331
+ * WALLET commitment definition.
21332
+ *
21333
+ * @private [🪔] Maybe export the commitments through some package
21334
+ */
21335
+ class WalletCommitmentDefinition extends BaseCommitmentDefinition {
21336
+ constructor(type = 'WALLET') {
21337
+ super(type);
21338
+ }
21339
+ get requiresContent() {
21340
+ return false;
21341
+ }
21342
+ get description() {
21343
+ return 'Enable persistent private credential storage (tokens, logins, cookies) scoped per agent or globally.';
21344
+ }
21345
+ get icon() {
21346
+ return '👛';
21347
+ }
21348
+ get documentation() {
21349
+ return getWalletCommitmentDocumentation(this.type);
21350
+ }
21351
+ applyToAgentModelRequirements(requirements, content) {
21352
+ const extraInstructions = formatOptionalInstructionBlock('Wallet instructions', content);
21353
+ return this.appendToSystemMessage({
21354
+ ...requirements,
21355
+ tools: createWalletTools(requirements.tools),
21356
+ _metadata: {
21357
+ ...requirements._metadata,
21358
+ useWallet: content || true,
21359
+ },
21360
+ }, createWalletSystemMessage(extraInstructions));
21361
+ }
21362
+ getToolTitles() {
21363
+ return getWalletToolTitles();
21364
+ }
21365
+ getToolFunctions() {
21366
+ return createWalletToolFunctions();
21367
+ }
21368
+ }
21369
+ // Note: [💞] Ignore a discrepancy between file name and entity name
21370
+
20655
21371
  /**
20656
21372
  * `WRITING RULES` commitment definition.
20657
21373
  *
@@ -20926,6 +21642,8 @@ const COMMITMENT_REGISTRY = [
20926
21642
  new MessageSuffixCommitmentDefinition(),
20927
21643
  new MessageCommitmentDefinition('MESSAGE'),
20928
21644
  new MessageCommitmentDefinition('MESSAGES'),
21645
+ new WalletCommitmentDefinition('WALLET'),
21646
+ new WalletCommitmentDefinition('WALLETS'),
20929
21647
  new ScenarioCommitmentDefinition('SCENARIO'),
20930
21648
  new ScenarioCommitmentDefinition('SCENARIOS'),
20931
21649
  new DeleteCommitmentDefinition('DELETE'),
@@ -21595,7 +22313,6 @@ function createInheritanceCapability(content) {
21595
22313
  if (isVoidPseudoAgentReference(reference)) {
21596
22314
  label = VOID_PSEUDO_AGENT_REFERENCE; // <- {Void} label
21597
22315
  iconName = 'ShieldAlert';
21598
- return null; // <- Note: Do not show `{Void}` in capabilities, it's only used for internal logic
21599
22316
  }
21600
22317
  return {
21601
22318
  type: 'inheritance',
@@ -23806,11 +24523,11 @@ function useBookEditorMonacoInteractions({ editor, handleFiles, }) {
23806
24523
  */
23807
24524
  const IMPORTANT_COMMITMENT_TYPE_SORT_ORDER = new Map([
23808
24525
  ['GOAL', 0],
23809
- ['GOALS', 1],
23810
- ['RULE', 2],
23811
- ['RULES', 3],
23812
- ['KNOWLEDGE', 4],
23813
- ['TEAM', 5],
24526
+ ['RULE', 1],
24527
+ ['KNOWLEDGE', 2],
24528
+ ['TEAM', 3],
24529
+ ['GOALS', 4],
24530
+ ['RULES', 5],
23814
24531
  ]);
23815
24532
  /**
23816
24533
  * Sort rank used when unfinished, low-level, and deprecated commitments should be grouped last.
@@ -24325,6 +25042,16 @@ function ensureBookEditorMonacoLanguage(monaco, theme = 'LIGHT') {
24325
25042
  const commitmentDefinitions = getAllCommitmentDefinitions();
24326
25043
  const commitmentTypes = [...new Set(commitmentDefinitions.map(({ type }) => type))];
24327
25044
  const commitmentDefinitionByType = new Map(commitmentDefinitions.map((definition) => [definition.type, definition]));
25045
+ const completionCommitmentTypes = [...commitmentTypes].sort((leftType, rightType) => {
25046
+ const leftDefinition = commitmentDefinitionByType.get(leftType);
25047
+ const rightDefinition = commitmentDefinitionByType.get(rightType);
25048
+ const leftRank = (leftDefinition === null || leftDefinition === void 0 ? void 0 : leftDefinition.isUnfinished) ? 1 : 0;
25049
+ const rightRank = (rightDefinition === null || rightDefinition === void 0 ? void 0 : rightDefinition.isUnfinished) ? 1 : 0;
25050
+ if (leftRank !== rightRank) {
25051
+ return leftRank - rightRank;
25052
+ }
25053
+ return commitmentTypes.indexOf(leftType) - commitmentTypes.indexOf(rightType);
25054
+ });
24328
25055
  const noteLikeCommitmentTypeSet = new Set([...TODO_COMMITMENT_TYPES, ...NOTE_COMMITMENT_TYPES]);
24329
25056
  const noteLikeCommitmentStates = createNoteLikeCommitmentStates(commitmentTypes);
24330
25057
  const executableCommitmentTypes = commitmentTypes.filter((type) => !noteLikeCommitmentTypeSet.has(type.toUpperCase()));
@@ -24381,7 +25108,7 @@ function ensureBookEditorMonacoLanguage(monaco, theme = 'LIGHT') {
24381
25108
  startColumn: word.startColumn,
24382
25109
  endColumn: word.endColumn,
24383
25110
  };
24384
- const suggestions = commitmentTypes.map((type, index) => {
25111
+ const suggestions = completionCommitmentTypes.map((type, index) => {
24385
25112
  var _a;
24386
25113
  const definition = commitmentDefinitionByType.get(type);
24387
25114
  const notice = definition ? getCommitmentNoticeMetadata(definition) : null;
@@ -25087,22 +25814,13 @@ function createUploadStats(uploadItems) {
25087
25814
  };
25088
25815
  }
25089
25816
  /**
25090
- * Handles file uploads and placeholder rendering inside `BookEditorMonaco`.
25817
+ * Internal upload item state shared by Monaco upload helpers.
25091
25818
  *
25092
25819
  * @private function of BookEditorMonaco
25093
25820
  */
25094
- function useBookEditorMonacoUploads({ editor, monaco, onFileUpload }) {
25821
+ function useBookEditorMonacoUploadItemsState() {
25095
25822
  const [uploadItems, setUploadItemsState] = useState([]);
25096
25823
  const uploadItemsRef = useRef([]);
25097
- const uploadFilesRef = useRef(new Map());
25098
- const uploadDecorationIdsRef = useRef(new Map());
25099
- const uploadControllersRef = useRef(new Map());
25100
- const uploadQueueTimerRef = useRef(null);
25101
- const editorUpdateTimerRef = useRef(null);
25102
- const progressUpdateTimerRef = useRef(null);
25103
- const pendingReplacementsRef = useRef([]);
25104
- const pendingProgressUpdatesRef = useRef(new Map());
25105
- const processUploadQueueRef = useRef(() => undefined);
25106
25824
  const setUploadItems = useCallback((updater) => {
25107
25825
  const next = updater(uploadItemsRef.current);
25108
25826
  uploadItemsRef.current = next;
@@ -25111,6 +25829,31 @@ function useBookEditorMonacoUploads({ editor, monaco, onFileUpload }) {
25111
25829
  const updateUploadItem = useCallback((uploadId, createNextUploadItem) => {
25112
25830
  setUploadItems((currentUploadItems) => replaceUploadItemById(currentUploadItems, uploadId, createNextUploadItem));
25113
25831
  }, [setUploadItems]);
25832
+ const uploadStats = useMemo(() => createUploadStats(uploadItems), [uploadItems]);
25833
+ const activeUploadItems = useMemo(() => uploadItems.filter((uploadItem) => uploadItem.status !== 'completed'), [uploadItems]);
25834
+ return {
25835
+ uploadItems,
25836
+ uploadItemsRef,
25837
+ setUploadItems,
25838
+ updateUploadItem,
25839
+ uploadStats,
25840
+ activeUploadItems,
25841
+ };
25842
+ }
25843
+ /**
25844
+ * Debounces upload progress updates before they hit React state.
25845
+ *
25846
+ * @private function of BookEditorMonaco
25847
+ */
25848
+ function useBookEditorMonacoUploadProgressQueue({ setUploadItems }) {
25849
+ const progressUpdateTimerRef = useRef(null);
25850
+ const pendingProgressUpdatesRef = useRef(new Map());
25851
+ const flushProgressUpdates = useCallback(() => {
25852
+ progressUpdateTimerRef.current = null;
25853
+ const progressUpdates = pendingProgressUpdatesRef.current;
25854
+ pendingProgressUpdatesRef.current = new Map();
25855
+ setUploadItems((currentUploadItems) => applyProgressUpdates(currentUploadItems, progressUpdates));
25856
+ }, [setUploadItems]);
25114
25857
  const queueProgressUpdate = useCallback((uploadId, progress, loadedBytes, totalBytes) => {
25115
25858
  pendingProgressUpdatesRef.current.set(uploadId, {
25116
25859
  progress,
@@ -25121,12 +25864,27 @@ function useBookEditorMonacoUploads({ editor, monaco, onFileUpload }) {
25121
25864
  return;
25122
25865
  }
25123
25866
  progressUpdateTimerRef.current = window.setTimeout(() => {
25124
- progressUpdateTimerRef.current = null;
25125
- const progressUpdates = pendingProgressUpdatesRef.current;
25126
- pendingProgressUpdatesRef.current = new Map();
25127
- setUploadItems((currentUploadItems) => applyProgressUpdates(currentUploadItems, progressUpdates));
25867
+ flushProgressUpdates();
25128
25868
  }, BookEditorMonacoConstants.UPLOAD_PROGRESS_DEBOUNCE_MS);
25129
- }, [setUploadItems]);
25869
+ }, [flushProgressUpdates]);
25870
+ const clearProgressQueue = useCallback(() => {
25871
+ clearScheduledTimer(progressUpdateTimerRef.current);
25872
+ progressUpdateTimerRef.current = null;
25873
+ pendingProgressUpdatesRef.current = new Map();
25874
+ }, []);
25875
+ return {
25876
+ queueProgressUpdate,
25877
+ clearProgressQueue,
25878
+ };
25879
+ }
25880
+ /**
25881
+ * Manages Monaco placeholder insertion and later replacement with uploaded URLs.
25882
+ *
25883
+ * @private function of BookEditorMonaco
25884
+ */
25885
+ function useBookEditorMonacoUploadEditorSync({ editor, uploadFilesRef, uploadDecorationIdsRef, }) {
25886
+ const editorUpdateTimerRef = useRef(null);
25887
+ const pendingReplacementsRef = useRef([]);
25130
25888
  const flushEditorReplacements = useCallback(() => {
25131
25889
  if (!editor) {
25132
25890
  return;
@@ -25160,23 +25918,20 @@ function useBookEditorMonacoUploads({ editor, monaco, onFileUpload }) {
25160
25918
  editor.deltaDecorations(decorationsToRemove, []);
25161
25919
  }
25162
25920
  }, [editor]);
25921
+ const registerUploadPlaceholderResources = useCallback((placeholders, decorationIds) => {
25922
+ placeholders.forEach((placeholder, index) => {
25923
+ uploadFilesRef.current.set(placeholder.id, placeholder.file);
25924
+ const decorationId = decorationIds[index];
25925
+ if (decorationId) {
25926
+ uploadDecorationIdsRef.current.set(placeholder.id, decorationId);
25927
+ }
25928
+ });
25929
+ }, []);
25163
25930
  const queueEditorReplacement = useCallback((uploadId, replacementText) => {
25164
- const decorationId = uploadDecorationIdsRef.current.get(uploadId);
25165
- if (!decorationId) {
25931
+ const hasQueuedReplacement = queueEditorReplacementItem(uploadId, replacementText, uploadDecorationIdsRef, pendingReplacementsRef);
25932
+ if (!hasQueuedReplacement) {
25166
25933
  return;
25167
25934
  }
25168
- const pendingIndex = pendingReplacementsRef.current.findIndex((item) => item.uploadId === uploadId);
25169
- const nextReplacement = {
25170
- uploadId,
25171
- decorationId,
25172
- replacementText,
25173
- };
25174
- if (pendingIndex >= 0) {
25175
- pendingReplacementsRef.current[pendingIndex] = nextReplacement;
25176
- }
25177
- else {
25178
- pendingReplacementsRef.current.push(nextReplacement);
25179
- }
25180
25935
  if (editorUpdateTimerRef.current !== null) {
25181
25936
  return;
25182
25937
  }
@@ -25184,16 +25939,81 @@ function useBookEditorMonacoUploads({ editor, monaco, onFileUpload }) {
25184
25939
  editorUpdateTimerRef.current = null;
25185
25940
  flushEditorReplacements();
25186
25941
  }, BookEditorMonacoConstants.UPLOAD_EDIT_DEBOUNCE_MS);
25187
- }, [flushEditorReplacements]);
25188
- const registerUploadPlaceholderResources = useCallback((placeholders, decorationIds) => {
25189
- placeholders.forEach((placeholder, index) => {
25190
- uploadFilesRef.current.set(placeholder.id, placeholder.file);
25191
- const decorationId = decorationIds[index];
25192
- if (decorationId) {
25193
- uploadDecorationIdsRef.current.set(placeholder.id, decorationId);
25194
- }
25195
- });
25942
+ }, [flushEditorReplacements, uploadDecorationIdsRef]);
25943
+ const clearEditorSync = useCallback(() => {
25944
+ clearScheduledTimer(editorUpdateTimerRef.current);
25945
+ editorUpdateTimerRef.current = null;
25946
+ pendingReplacementsRef.current = [];
25196
25947
  }, []);
25948
+ return {
25949
+ registerUploadPlaceholderResources,
25950
+ queueEditorReplacement,
25951
+ clearEditorSync,
25952
+ };
25953
+ }
25954
+ /**
25955
+ * Enqueues replacement text for one upload placeholder.
25956
+ *
25957
+ * @private function of BookEditorMonaco
25958
+ */
25959
+ function queueEditorReplacementItem(uploadId, replacementText, uploadDecorationIdsRef, pendingReplacementsRef) {
25960
+ const decorationId = uploadDecorationIdsRef.current.get(uploadId);
25961
+ if (!decorationId) {
25962
+ return false;
25963
+ }
25964
+ const pendingIndex = pendingReplacementsRef.current.findIndex((item) => item.uploadId === uploadId);
25965
+ const nextReplacement = {
25966
+ uploadId,
25967
+ decorationId,
25968
+ replacementText,
25969
+ };
25970
+ if (pendingIndex >= 0) {
25971
+ pendingReplacementsRef.current[pendingIndex] = nextReplacement;
25972
+ }
25973
+ else {
25974
+ pendingReplacementsRef.current.push(nextReplacement);
25975
+ }
25976
+ return true;
25977
+ }
25978
+ /**
25979
+ * Inserts Monaco placeholders and queues matching upload items.
25980
+ *
25981
+ * @private function of BookEditorMonaco
25982
+ */
25983
+ function enqueueFilesForUpload({ editor, monaco, registerUploadPlaceholderResources, setUploadItems, }) {
25984
+ return (files) => {
25985
+ if (!editor || !monaco) {
25986
+ return false;
25987
+ }
25988
+ const model = editor.getModel();
25989
+ if (!model) {
25990
+ return false;
25991
+ }
25992
+ const placeholders = createUploadPlaceholderEntries(files);
25993
+ const insertPlan = createUploadPlaceholderInsertPlan(model, monaco, placeholders);
25994
+ editor.executeEdits('upload-placeholders', [
25995
+ {
25996
+ range: new monaco.Range(insertPlan.insertLine, insertPlan.insertColumn, insertPlan.insertLine, insertPlan.insertColumn),
25997
+ text: insertPlan.textToInsert,
25998
+ forceMoveMarkers: true,
25999
+ },
26000
+ ]);
26001
+ const placeholderDecorations = createUploadPlaceholderDecorations(model, monaco, placeholders, insertPlan.insertStartOffset, insertPlan.prefixLength);
26002
+ const decorationIds = editor.deltaDecorations([], placeholderDecorations);
26003
+ registerUploadPlaceholderResources(placeholders, decorationIds);
26004
+ setUploadItems((currentUploadItems) => [...currentUploadItems, ...createQueuedUploadItems(placeholders)]);
26005
+ return true;
26006
+ };
26007
+ }
26008
+ /**
26009
+ * Manages upload concurrency, retries, pausing and completion side effects.
26010
+ *
26011
+ * @private function of BookEditorMonaco
26012
+ */
26013
+ function useBookEditorMonacoUploadQueue({ onFileUpload, uploadItemsRef, uploadFilesRef, queueProgressUpdate, queueEditorReplacement, setUploadItems, updateUploadItem, }) {
26014
+ const uploadControllersRef = useRef(new Map());
26015
+ const uploadQueueTimerRef = useRef(null);
26016
+ const processUploadQueueRef = useRef(() => undefined);
25197
26017
  const queueUploadProcessing = useCallback(() => {
25198
26018
  if (uploadQueueTimerRef.current !== null) {
25199
26019
  return;
@@ -25306,6 +26126,8 @@ function useBookEditorMonacoUploads({ editor, monaco, onFileUpload }) {
25306
26126
  onFileUpload,
25307
26127
  queueProgressUpdate,
25308
26128
  queueUploadProcessing,
26129
+ uploadFilesRef,
26130
+ uploadItemsRef,
25309
26131
  ]);
25310
26132
  const processUploadQueue = useCallback(() => {
25311
26133
  if (!onFileUpload) {
@@ -25318,34 +26140,45 @@ function useBookEditorMonacoUploads({ editor, monaco, onFileUpload }) {
25318
26140
  queuedUploadIds.forEach((queuedUploadId) => {
25319
26141
  void startUpload(queuedUploadId);
25320
26142
  });
25321
- }, [onFileUpload, startUpload]);
26143
+ }, [onFileUpload, startUpload, uploadItemsRef]);
25322
26144
  processUploadQueueRef.current = processUploadQueue;
25323
26145
  const pauseUpload = useCallback((uploadId) => {
26146
+ var _a;
25324
26147
  pauseQueuedUpload(uploadId);
25325
- const controller = uploadControllersRef.current.get(uploadId);
25326
- controller === null || controller === void 0 ? void 0 : controller.abort();
26148
+ (_a = uploadControllersRef.current.get(uploadId)) === null || _a === void 0 ? void 0 : _a.abort();
25327
26149
  }, [pauseQueuedUpload]);
25328
26150
  const resumeUpload = useCallback((uploadId) => {
25329
26151
  resetUploadForRetry(uploadId);
25330
26152
  queueUploadProcessing();
25331
26153
  }, [queueUploadProcessing, resetUploadForRetry]);
25332
- useEffect(() => {
25333
- return () => {
25334
- clearScheduledTimer(uploadQueueTimerRef.current);
25335
- clearScheduledTimer(editorUpdateTimerRef.current);
25336
- clearScheduledTimer(progressUpdateTimerRef.current);
25337
- for (const controller of uploadControllersRef.current.values()) {
25338
- controller.abort();
25339
- }
25340
- uploadControllersRef.current.clear();
25341
- };
26154
+ const clearUploadQueue = useCallback(() => {
26155
+ clearScheduledTimer(uploadQueueTimerRef.current);
26156
+ uploadQueueTimerRef.current = null;
26157
+ for (const controller of uploadControllersRef.current.values()) {
26158
+ controller.abort();
26159
+ }
26160
+ uploadControllersRef.current.clear();
25342
26161
  }, []);
26162
+ useEffect(() => clearUploadQueue, [clearUploadQueue]);
26163
+ return {
26164
+ pauseUpload,
26165
+ resumeUpload,
26166
+ queueUploadProcessing,
26167
+ clearUploadQueue,
26168
+ };
26169
+ }
26170
+ /**
26171
+ * Clears upload UI state shortly after all uploads finish.
26172
+ *
26173
+ * @private function of BookEditorMonaco
26174
+ */
26175
+ function useCompletedUploadsAutoClear({ uploadItems, uploadFilesRef, uploadDecorationIdsRef, setUploadItems, }) {
25343
26176
  useEffect(() => {
25344
26177
  if (uploadItems.length === 0) {
25345
26178
  return;
25346
26179
  }
25347
- const hasActive = uploadItems.some((item) => item.status !== 'completed');
25348
- if (hasActive) {
26180
+ const hasActiveUploads = uploadItems.some((item) => item.status !== 'completed');
26181
+ if (hasActiveUploads) {
25349
26182
  return;
25350
26183
  }
25351
26184
  const timer = window.setTimeout(() => {
@@ -25356,45 +26189,61 @@ function useBookEditorMonacoUploads({ editor, monaco, onFileUpload }) {
25356
26189
  return () => {
25357
26190
  clearTimeout(timer);
25358
26191
  };
25359
- }, [setUploadItems, uploadItems]);
25360
- const enqueueFilesForUpload = useCallback((files) => {
25361
- if (!editor || !monaco) {
25362
- return false;
25363
- }
25364
- const model = editor.getModel();
25365
- if (!model) {
25366
- return false;
25367
- }
25368
- const placeholders = createUploadPlaceholderEntries(files);
25369
- const insertPlan = createUploadPlaceholderInsertPlan(model, monaco, placeholders);
25370
- editor.executeEdits('upload-placeholders', [
25371
- {
25372
- range: new monaco.Range(insertPlan.insertLine, insertPlan.insertColumn, insertPlan.insertLine, insertPlan.insertColumn),
25373
- text: insertPlan.textToInsert,
25374
- forceMoveMarkers: true,
25375
- },
25376
- ]);
25377
- const placeholderDecorations = createUploadPlaceholderDecorations(model, monaco, placeholders, insertPlan.insertStartOffset, insertPlan.prefixLength);
25378
- const decorationIds = editor.deltaDecorations([], placeholderDecorations);
25379
- registerUploadPlaceholderResources(placeholders, decorationIds);
25380
- setUploadItems((currentUploadItems) => [...currentUploadItems, ...createQueuedUploadItems(placeholders)]);
25381
- return true;
25382
- }, [editor, monaco, registerUploadPlaceholderResources, setUploadItems]);
26192
+ }, [setUploadItems, uploadDecorationIdsRef, uploadFilesRef, uploadItems]);
26193
+ }
26194
+ /**
26195
+ * Handles file uploads and placeholder rendering inside `BookEditorMonaco`.
26196
+ *
26197
+ * @private function of BookEditorMonaco
26198
+ */
26199
+ function useBookEditorMonacoUploads({ editor, monaco, onFileUpload }) {
26200
+ const uploadFilesRef = useRef(new Map());
26201
+ const uploadDecorationIdsRef = useRef(new Map());
26202
+ const { uploadItems, uploadItemsRef, setUploadItems, updateUploadItem, uploadStats, activeUploadItems } = useBookEditorMonacoUploadItemsState();
26203
+ const { queueProgressUpdate, clearProgressQueue } = useBookEditorMonacoUploadProgressQueue({ setUploadItems });
26204
+ const { registerUploadPlaceholderResources, queueEditorReplacement, clearEditorSync } = useBookEditorMonacoUploadEditorSync({
26205
+ editor,
26206
+ uploadFilesRef,
26207
+ uploadDecorationIdsRef,
26208
+ });
26209
+ const enqueueFiles = useMemo(() => enqueueFilesForUpload({
26210
+ editor,
26211
+ monaco,
26212
+ registerUploadPlaceholderResources,
26213
+ setUploadItems,
26214
+ }), [editor, monaco, registerUploadPlaceholderResources, setUploadItems]);
26215
+ const { pauseUpload, resumeUpload, queueUploadProcessing, clearUploadQueue } = useBookEditorMonacoUploadQueue({
26216
+ onFileUpload,
26217
+ uploadItemsRef,
26218
+ uploadFilesRef,
26219
+ queueProgressUpdate,
26220
+ queueEditorReplacement,
26221
+ setUploadItems,
26222
+ updateUploadItem,
26223
+ });
26224
+ useCompletedUploadsAutoClear({
26225
+ uploadItems,
26226
+ uploadFilesRef,
26227
+ uploadDecorationIdsRef,
26228
+ setUploadItems,
26229
+ });
26230
+ useEffect(() => {
26231
+ return () => {
26232
+ clearEditorSync();
26233
+ clearProgressQueue();
26234
+ clearUploadQueue();
26235
+ };
26236
+ }, [clearEditorSync, clearProgressQueue, clearUploadQueue]);
25383
26237
  const handleFiles = useCallback(async (files) => {
25384
- if (!onFileUpload) {
26238
+ if (!onFileUpload || files.length === 0) {
25385
26239
  return;
25386
26240
  }
25387
- if (files.length === 0) {
25388
- return;
25389
- }
25390
- const hasQueuedUploads = enqueueFilesForUpload(files);
26241
+ const hasQueuedUploads = enqueueFiles(files);
25391
26242
  if (!hasQueuedUploads) {
25392
26243
  return;
25393
26244
  }
25394
26245
  queueUploadProcessing();
25395
- }, [enqueueFilesForUpload, onFileUpload, queueUploadProcessing]);
25396
- const uploadStats = useMemo(() => createUploadStats(uploadItems), [uploadItems]);
25397
- const activeUploadItems = useMemo(() => uploadItems.filter((uploadItem) => uploadItem.status !== 'completed'), [uploadItems]);
26246
+ }, [enqueueFiles, onFileUpload, queueUploadProcessing]);
25398
26247
  return {
25399
26248
  activeUploadItems,
25400
26249
  uploadItems,
@@ -28370,7 +29219,7 @@ function insertTextAtSelection(params) {
28370
29219
  *
28371
29220
  * @private function of `useChatInputAreaComposer`
28372
29221
  */
28373
- function resolveTextareaSelection(textareaElement, messageContent, selectionStart, selectionEnd) {
29222
+ function resolveTextareaSelection$1(textareaElement, messageContent, selectionStart, selectionEnd) {
28374
29223
  var _a, _b;
28375
29224
  const resolvedSelectionStart = (_a = selectionStart !== null && selectionStart !== void 0 ? selectionStart : textareaElement.selectionStart) !== null && _a !== void 0 ? _a : messageContent.length;
28376
29225
  const resolvedSelectionEnd = (_b = selectionEnd !== null && selectionEnd !== void 0 ? selectionEnd : textareaElement.selectionEnd) !== null && _b !== void 0 ? _b : resolvedSelectionStart;
@@ -28428,13 +29277,21 @@ function hasPendingEnterIntentStayedCurrent(params) {
28428
29277
  areAttachmentSnapshotsEqual(snapshot.attachmentIds, uploadedFiles.map((uploadedFile) => uploadedFile.id)) &&
28429
29278
  getReplyingToMessageId(replyingToMessage) === snapshot.replyingToMessageId);
28430
29279
  }
29280
+ /**
29281
+ * Returns true when the composer contains text or attachments that can be sent.
29282
+ *
29283
+ * @private function of `useChatInputAreaComposer`
29284
+ */
29285
+ function hasSendableComposerContent(messageContent, attachmentCount) {
29286
+ return spaceTrim$1(messageContent) !== '' || attachmentCount > 0;
29287
+ }
28431
29288
  /**
28432
29289
  * Returns true when the deferred Enter snapshot still contains something sendable.
28433
29290
  *
28434
29291
  * @private function of `useChatInputAreaComposer`
28435
29292
  */
28436
29293
  function hasPendingEnterIntentContent(snapshot) {
28437
- return spaceTrim$1(snapshot.value) !== '' || snapshot.attachmentIds.length > 0;
29294
+ return hasSendableComposerContent(snapshot.value, snapshot.attachmentIds.length);
28438
29295
  }
28439
29296
  /**
28440
29297
  * Builds the attachment payload expected by `onMessage`.
@@ -28477,16 +29334,36 @@ async function resolvePendingEnterIntent(params) {
28477
29334
  handleInsertNewline(snapshot.selectionStart, snapshot.selectionEnd);
28478
29335
  }
28479
29336
  /**
28480
- * Manages textarea state, send/newline behavior, and deferred Enter resolution for `<ChatInputArea/>`.
29337
+ * Returns the required `onMessage` callback or throws when the composer is misconfigured.
28481
29338
  *
28482
- * @private function of `<ChatInputArea/>`
29339
+ * @private function of `useChatInputAreaComposer`
28483
29340
  */
28484
- function useChatInputAreaComposer(props) {
28485
- const { onMessage, onChange, defaultMessage, enterBehavior, resolveEnterBehavior, isFocusedOnLoad, isMobile, uploadedFiles, uploadedFilesRef, clearUploadedFiles, replyingToMessage, onCancelReply, soundSystem, } = props;
28486
- const textareaRef = useRef(null);
29341
+ function getRequiredOnMessage(onMessage) {
29342
+ if (!onMessage) {
29343
+ throw new Error(`Can not find onMessage callback`);
29344
+ }
29345
+ return onMessage;
29346
+ }
29347
+ /**
29348
+ * Returns the active composer textarea or throws when the DOM node is missing.
29349
+ *
29350
+ * @private function of `useChatInputAreaComposer`
29351
+ */
29352
+ function getRequiredTextareaElement(textareaRef) {
29353
+ const textareaElement = textareaRef.current;
29354
+ if (!textareaElement) {
29355
+ throw new Error(`Can not find textarea`);
29356
+ }
29357
+ return textareaElement;
29358
+ }
29359
+ /**
29360
+ * Manages controlled message text state and change propagation for the composer.
29361
+ *
29362
+ * @private function of `useChatInputAreaComposer`
29363
+ */
29364
+ function useChatInputAreaMessageContentState({ defaultMessage, onChange }) {
28487
29365
  const [messageContent, setMessageContent] = useState(defaultMessage || '');
28488
29366
  const messageContentRef = useRef(messageContent);
28489
- const isResolvingEnterBehaviorRef = useRef(false);
28490
29367
  const applyMessageContent = useCallback((nextContent) => {
28491
29368
  messageContentRef.current = nextContent;
28492
29369
  setMessageContent(nextContent);
@@ -28497,6 +29374,22 @@ function useChatInputAreaComposer(props) {
28497
29374
  messageContentRef.current = nextDefaultMessage;
28498
29375
  setMessageContent(nextDefaultMessage);
28499
29376
  }, [defaultMessage]);
29377
+ const handleTextInputChange = useCallback((event) => {
29378
+ applyMessageContent(event.target.value);
29379
+ }, [applyMessageContent]);
29380
+ return {
29381
+ messageContent,
29382
+ messageContentRef,
29383
+ applyMessageContent,
29384
+ handleTextInputChange,
29385
+ };
29386
+ }
29387
+ /**
29388
+ * Applies initial-focus behavior for the composer textarea.
29389
+ *
29390
+ * @private function of `useChatInputAreaComposer`
29391
+ */
29392
+ function useChatInputAreaComposerFocus({ textareaRef, isFocusedOnLoad, isMobile, }) {
28500
29393
  useEffect(( /* Focus textarea on page load */) => {
28501
29394
  if (!textareaRef.current) {
28502
29395
  return;
@@ -28505,16 +29398,20 @@ function useChatInputAreaComposer(props) {
28505
29398
  if (shouldFocus) {
28506
29399
  textareaRef.current.focus();
28507
29400
  }
28508
- }, [isFocusedOnLoad, isMobile]);
28509
- const handleTextInputChange = useCallback((event) => {
28510
- applyMessageContent(event.target.value);
28511
- }, [applyMessageContent]);
28512
- const handleInsertNewline = useCallback((selectionStart, selectionEnd) => {
29401
+ }, [isFocusedOnLoad, isMobile, textareaRef]);
29402
+ }
29403
+ /**
29404
+ * Creates the newline insertion action used by the composer Enter handling.
29405
+ *
29406
+ * @private function of `useChatInputAreaComposer`
29407
+ */
29408
+ function useChatInputAreaNewlineHandler({ textareaRef, messageContentRef, applyMessageContent, }) {
29409
+ return useCallback((selectionStart, selectionEnd) => {
28513
29410
  const textareaElement = textareaRef.current;
28514
29411
  if (!textareaElement) {
28515
29412
  return;
28516
29413
  }
28517
- const resolvedSelection = resolveTextareaSelection(textareaElement, messageContentRef.current, selectionStart, selectionEnd);
29414
+ const resolvedSelection = resolveTextareaSelection$1(textareaElement, messageContentRef.current, selectionStart, selectionEnd);
28518
29415
  const insertion = insertTextAtSelection({
28519
29416
  currentValue: messageContentRef.current,
28520
29417
  insertedText: '\n',
@@ -28523,20 +29420,22 @@ function useChatInputAreaComposer(props) {
28523
29420
  });
28524
29421
  applyMessageContent(insertion.nextValue);
28525
29422
  focusTextareaCaret(textareaElement, insertion.caret);
28526
- }, [applyMessageContent]);
28527
- const handleSend = useCallback(async () => {
28528
- if (!onMessage) {
28529
- throw new Error(`Can not find onMessage callback`);
28530
- }
28531
- const textareaElement = textareaRef.current;
28532
- if (!textareaElement) {
28533
- throw new Error(`Can not find textarea`);
28534
- }
29423
+ }, [applyMessageContent, messageContentRef, textareaRef]);
29424
+ }
29425
+ /**
29426
+ * Creates the send action while keeping focus restoration and attachments in sync.
29427
+ *
29428
+ * @private function of `useChatInputAreaComposer`
29429
+ */
29430
+ function useChatInputAreaSendAction({ onMessage, textareaRef, uploadedFiles, clearUploadedFiles, messageContentRef, applyMessageContent, replyingToMessage, onCancelReply, soundSystem, }) {
29431
+ return useCallback(async () => {
29432
+ const onMessageCallback = getRequiredOnMessage(onMessage);
29433
+ const textareaElement = getRequiredTextareaElement(textareaRef);
28535
29434
  const wasTextareaFocused = document.activeElement === textareaElement;
28536
29435
  try {
28537
29436
  const attachments = createMessageAttachments(uploadedFiles);
28538
29437
  const contentToSend = messageContentRef.current;
28539
- if (spaceTrim$1(contentToSend) === '' && attachments.length === 0) {
29438
+ if (!hasSendableComposerContent(contentToSend, attachments.length)) {
28540
29439
  throw new Error(`You need to write some text or upload a file`);
28541
29440
  }
28542
29441
  if (soundSystem) {
@@ -28547,7 +29446,7 @@ function useChatInputAreaComposer(props) {
28547
29446
  if (wasTextareaFocused) {
28548
29447
  textareaElement.focus();
28549
29448
  }
28550
- await onMessage(contentToSend, attachments, replyingToMessage || null);
29449
+ await onMessageCallback(contentToSend, attachments, replyingToMessage || null);
28551
29450
  onCancelReply === null || onCancelReply === void 0 ? void 0 : onCancelReply();
28552
29451
  }
28553
29452
  catch (error) {
@@ -28560,12 +29459,22 @@ function useChatInputAreaComposer(props) {
28560
29459
  }, [
28561
29460
  applyMessageContent,
28562
29461
  clearUploadedFiles,
29462
+ messageContentRef,
28563
29463
  onCancelReply,
28564
29464
  onMessage,
28565
29465
  replyingToMessage,
28566
29466
  soundSystem,
29467
+ textareaRef,
28567
29468
  uploadedFiles,
28568
29469
  ]);
29470
+ }
29471
+ /**
29472
+ * Creates the keyboard handler that coordinates reply cancel, send, newline, and deferred Enter behavior.
29473
+ *
29474
+ * @private function of `useChatInputAreaComposer`
29475
+ */
29476
+ function useChatInputAreaEnterKeyHandler({ textareaRef, enterBehavior, resolveEnterBehavior, messageContentRef, uploadedFilesRef, replyingToMessage, onCancelReply, handleInsertNewline, handleSend, }) {
29477
+ const isResolvingEnterBehaviorRef = useRef(false);
28569
29478
  const handleImmediateEnterAction = useCallback((isCtrlPressed) => {
28570
29479
  const resolvedAction = resolveChatEnterAction(enterBehavior || 'SEND', isCtrlPressed);
28571
29480
  if (resolvedAction === 'SEND') {
@@ -28600,30 +29509,78 @@ function useChatInputAreaComposer(props) {
28600
29509
  }).finally(() => {
28601
29510
  isResolvingEnterBehaviorRef.current = false;
28602
29511
  });
28603
- }, [handleInsertNewline, handleSend, replyingToMessage, uploadedFilesRef]);
28604
- const handleComposerKeyDown = useCallback((event) => {
28605
- if (event.key === 'Escape' && replyingToMessage && onCancelReply) {
28606
- event.preventDefault();
28607
- onCancelReply();
28608
- return;
29512
+ }, [handleInsertNewline, handleSend, messageContentRef, replyingToMessage, textareaRef, uploadedFilesRef]);
29513
+ const handleReplyCancelShortcut = useCallback((event) => {
29514
+ if (event.key !== 'Escape' || !replyingToMessage || !onCancelReply) {
29515
+ return false;
28609
29516
  }
29517
+ event.preventDefault();
29518
+ onCancelReply();
29519
+ return true;
29520
+ }, [onCancelReply, replyingToMessage]);
29521
+ const handleComposerEnterKeyDown = useCallback((event) => {
28610
29522
  if (!isComposerEnterAction(event)) {
28611
- return;
29523
+ return false;
28612
29524
  }
28613
29525
  event.preventDefault();
28614
29526
  if (shouldResolveDeferredEnterBehavior(enterBehavior, event.ctrlKey, resolveEnterBehavior)) {
28615
29527
  handleDeferredEnterAction(resolveEnterBehavior);
28616
- return;
29528
+ return true;
28617
29529
  }
28618
29530
  handleImmediateEnterAction(event.ctrlKey);
28619
- }, [
28620
- enterBehavior,
28621
- handleDeferredEnterAction,
28622
- handleImmediateEnterAction,
28623
- onCancelReply,
29531
+ return true;
29532
+ }, [enterBehavior, handleDeferredEnterAction, handleImmediateEnterAction, resolveEnterBehavior]);
29533
+ return useCallback((event) => {
29534
+ if (handleReplyCancelShortcut(event)) {
29535
+ return;
29536
+ }
29537
+ handleComposerEnterKeyDown(event);
29538
+ }, [handleComposerEnterKeyDown, handleReplyCancelShortcut]);
29539
+ }
29540
+ /**
29541
+ * Manages textarea state, send/newline behavior, and deferred Enter resolution for `<ChatInputArea/>`.
29542
+ *
29543
+ * @private function of `<ChatInputArea/>`
29544
+ */
29545
+ function useChatInputAreaComposer(props) {
29546
+ const { onMessage, onChange, defaultMessage, enterBehavior, resolveEnterBehavior, isFocusedOnLoad, isMobile, uploadedFiles, uploadedFilesRef, clearUploadedFiles, replyingToMessage, onCancelReply, soundSystem, } = props;
29547
+ const textareaRef = useRef(null);
29548
+ const { messageContent, messageContentRef, applyMessageContent, handleTextInputChange } = useChatInputAreaMessageContentState({
29549
+ defaultMessage,
29550
+ onChange,
29551
+ });
29552
+ useChatInputAreaComposerFocus({
29553
+ textareaRef,
29554
+ isFocusedOnLoad,
29555
+ isMobile,
29556
+ });
29557
+ const handleInsertNewline = useChatInputAreaNewlineHandler({
29558
+ textareaRef,
29559
+ messageContentRef,
29560
+ applyMessageContent,
29561
+ });
29562
+ const handleSend = useChatInputAreaSendAction({
29563
+ onMessage,
29564
+ textareaRef,
29565
+ uploadedFiles,
29566
+ clearUploadedFiles,
29567
+ messageContentRef,
29568
+ applyMessageContent,
28624
29569
  replyingToMessage,
29570
+ onCancelReply,
29571
+ soundSystem,
29572
+ });
29573
+ const handleComposerKeyDown = useChatInputAreaEnterKeyHandler({
29574
+ textareaRef,
29575
+ enterBehavior,
28625
29576
  resolveEnterBehavior,
28626
- ]);
29577
+ messageContentRef,
29578
+ uploadedFilesRef,
29579
+ replyingToMessage,
29580
+ onCancelReply,
29581
+ handleInsertNewline,
29582
+ handleSend,
29583
+ });
28627
29584
  return {
28628
29585
  textareaRef,
28629
29586
  messageContent,
@@ -29139,33 +30096,122 @@ function createDictationChunkId() {
29139
30096
  return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
29140
30097
  }
29141
30098
  /**
29142
- * Manages speech-recognition state, transcript refinement, and correction UI.
30099
+ * Resolves the current textarea selection with a stable fallback to the message length.
29143
30100
  *
29144
- * @private function of `<ChatInputArea/>`
30101
+ * @private function of `useChatInputAreaDictation`
29145
30102
  */
29146
- function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguage, textareaRef, messageContentRef, applyMessageContent, }) {
29147
- const pendingStopFallbackRef = useRef(null);
29148
- const replaceSelectionOnNextFinalRef = useRef(false);
29149
- const [dictationUiState, setDictationUiState] = useState('idle');
29150
- const [dictationInterimText, setDictationInterimText] = useState('');
29151
- const [dictationError, setDictationError] = useState(null);
29152
- const [dictationLastFinalChunk, setDictationLastFinalChunk] = useState('');
29153
- const [dictationEditableChunk, setDictationEditableChunk] = useState('');
29154
- const [dictationChunks, setDictationChunks] = useState([]);
29155
- const [isDictationPanelExpanded, setIsDictationPanelExpanded] = useState(false);
29156
- const { dictationSettings, setDictationSettings, dictationDictionary, setDictationDictionary } = useChatInputAreaDictationPersistence();
29157
- const { speechRecognitionUiDescriptor, resolvedSpeechRecognitionLanguage, isBrowserSpeechFallbackSupported, microphoneSettingsUrl, } = useChatInputAreaDictationSupport({
29158
- dictationUiState,
29159
- speechRecognitionLanguage,
29160
- });
29161
- const clearPendingStopFallback = useCallback(() => {
29162
- if (!pendingStopFallbackRef.current) {
29163
- return;
29164
- }
29165
- clearTimeout(pendingStopFallbackRef.current);
29166
- pendingStopFallbackRef.current = null;
29167
- }, []);
29168
- const focusTextareaSelection = useCallback((selectionStart, selectionEnd) => {
30103
+ function resolveTextareaSelection(textareaElement, fallbackLength) {
30104
+ var _a, _b;
30105
+ const selectionStart = (_a = textareaElement.selectionStart) !== null && _a !== void 0 ? _a : fallbackLength;
30106
+ const selectionEnd = (_b = textareaElement.selectionEnd) !== null && _b !== void 0 ? _b : selectionStart;
30107
+ return {
30108
+ selectionStart,
30109
+ selectionEnd,
30110
+ };
30111
+ }
30112
+ /**
30113
+ * Creates the persisted metadata entry for one finalized dictated chunk.
30114
+ *
30115
+ * @private function of `useChatInputAreaDictation`
30116
+ */
30117
+ function createFinalizedDictationChunk(params) {
30118
+ const { beforeValue, finalText, start } = params;
30119
+ return {
30120
+ id: createDictationChunkId(),
30121
+ beforeValue,
30122
+ finalText,
30123
+ start,
30124
+ };
30125
+ }
30126
+ /**
30127
+ * Clears the pending stop fallback timeout when one exists.
30128
+ *
30129
+ * @private function of `useChatInputAreaDictation`
30130
+ */
30131
+ function clearPendingStopFallbackTimeout(pendingStopFallbackRef) {
30132
+ if (!pendingStopFallbackRef.current) {
30133
+ return;
30134
+ }
30135
+ clearTimeout(pendingStopFallbackRef.current);
30136
+ pendingStopFallbackRef.current = null;
30137
+ }
30138
+ /**
30139
+ * Captures whether the next finalized chunk should replace the current selection.
30140
+ *
30141
+ * @private function of `useChatInputAreaDictation`
30142
+ */
30143
+ function captureSelectionReplacementIntent(textareaRef, replaceSelectionOnNextFinalRef) {
30144
+ const textarea = textareaRef.current;
30145
+ if (!textarea) {
30146
+ return;
30147
+ }
30148
+ const { selectionStart, selectionEnd } = resolveTextareaSelection(textarea, 0);
30149
+ replaceSelectionOnNextFinalRef.current = selectionStart !== selectionEnd;
30150
+ }
30151
+ /**
30152
+ * Produces the corrected chunk value only when a real correction should be applied.
30153
+ *
30154
+ * @private function of `useChatInputAreaDictation`
30155
+ */
30156
+ function resolveCorrectedDictationChunk(dictationEditableChunk, dictationLastFinalChunk) {
30157
+ const correctedChunk = normalizeDictationWhitespace(dictationEditableChunk);
30158
+ if (!correctedChunk || !dictationLastFinalChunk || correctedChunk === dictationLastFinalChunk) {
30159
+ return null;
30160
+ }
30161
+ return correctedChunk;
30162
+ }
30163
+ /**
30164
+ * Replaces the last tracked chunk text inside the tracked chunk list.
30165
+ *
30166
+ * @private function of `useChatInputAreaDictation`
30167
+ */
30168
+ function replaceLastDictationChunk(previousChunks, correctedChunk) {
30169
+ if (previousChunks.length === 0) {
30170
+ return previousChunks;
30171
+ }
30172
+ const nextChunks = [...previousChunks];
30173
+ const lastChunk = nextChunks[nextChunks.length - 1];
30174
+ if (!lastChunk) {
30175
+ return previousChunks;
30176
+ }
30177
+ nextChunks[nextChunks.length - 1] = {
30178
+ ...lastChunk,
30179
+ finalText: correctedChunk,
30180
+ };
30181
+ return nextChunks;
30182
+ }
30183
+ /**
30184
+ * Opens browser microphone settings in a new tab when a settings URL is available.
30185
+ *
30186
+ * @private function of `useChatInputAreaDictation`
30187
+ */
30188
+ function openBrowserSettings(microphoneSettingsUrl) {
30189
+ if (!microphoneSettingsUrl) {
30190
+ return;
30191
+ }
30192
+ window.open(microphoneSettingsUrl, '_blank', 'noopener,noreferrer');
30193
+ }
30194
+ /**
30195
+ * Resolves whether the dictation panel should be visible for the current state snapshot.
30196
+ *
30197
+ * @private function of `useChatInputAreaDictation`
30198
+ */
30199
+ function resolveShouldShowDictationPanel(params) {
30200
+ const { speechRecognition, isDictationPanelExpanded, dictationUiState, dictationInterimText, dictationError, dictationLastFinalChunk, } = params;
30201
+ return Boolean(speechRecognition &&
30202
+ (isDictationPanelExpanded ||
30203
+ dictationUiState !== 'idle' ||
30204
+ Boolean(dictationInterimText) ||
30205
+ Boolean(dictationError) ||
30206
+ Boolean(dictationLastFinalChunk)));
30207
+ }
30208
+ /**
30209
+ * Focuses the textarea and restores a given selection range after programmatic edits.
30210
+ *
30211
+ * @private function of `useChatInputAreaDictation`
30212
+ */
30213
+ function useTextareaSelectionFocus(textareaRef) {
30214
+ return useCallback((selectionStart, selectionEnd) => {
29169
30215
  requestAnimationFrame(() => {
29170
30216
  const textarea = textareaRef.current;
29171
30217
  if (!textarea) {
@@ -29175,8 +30221,15 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29175
30221
  textarea.setSelectionRange(selectionStart, selectionEnd);
29176
30222
  });
29177
30223
  }, [textareaRef]);
29178
- const handleDictationFinalResult = useCallback((rawText) => {
29179
- var _a, _b;
30224
+ }
30225
+ /**
30226
+ * Builds the final-result handler that refines text, inserts it, and exposes it for correction.
30227
+ *
30228
+ * @private function of `useChatInputAreaDictation`
30229
+ */
30230
+ function useChatInputAreaDictationFinalResultHandler({ textareaRef, messageContentRef, applyMessageContent, dictationSettings, dictationDictionary, replaceSelectionOnNextFinalRef, focusTextareaSelection, state, }) {
30231
+ const { setDictationUiState, setDictationInterimText, setDictationError, setDictationLastFinalChunk, setDictationEditableChunk, setDictationChunks, setIsDictationPanelExpanded, } = state;
30232
+ return useCallback((rawText) => {
29180
30233
  const textarea = textareaRef.current;
29181
30234
  if (!textarea) {
29182
30235
  return;
@@ -29186,8 +30239,7 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29186
30239
  if (!refinedText) {
29187
30240
  return;
29188
30241
  }
29189
- const selectionStart = (_a = textarea.selectionStart) !== null && _a !== void 0 ? _a : previousMessageContent.length;
29190
- const selectionEnd = (_b = textarea.selectionEnd) !== null && _b !== void 0 ? _b : selectionStart;
30242
+ const { selectionStart, selectionEnd } = resolveTextareaSelection(textarea, previousMessageContent.length);
29191
30243
  const insertion = insertDictationChunk({
29192
30244
  currentValue: previousMessageContent,
29193
30245
  dictatedText: refinedText,
@@ -29202,12 +30254,11 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29202
30254
  applyMessageContent(insertion.nextValue);
29203
30255
  setDictationChunks((previousChunks) => [
29204
30256
  ...previousChunks,
29205
- {
29206
- id: createDictationChunkId(),
30257
+ createFinalizedDictationChunk({
29207
30258
  beforeValue: previousMessageContent,
29208
30259
  finalText: refinedText,
29209
30260
  start: insertion.start,
29210
- },
30261
+ }),
29211
30262
  ]);
29212
30263
  setDictationLastFinalChunk(refinedText);
29213
30264
  setDictationEditableChunk(refinedText);
@@ -29219,9 +30270,25 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29219
30270
  dictationSettings,
29220
30271
  focusTextareaSelection,
29221
30272
  messageContentRef,
30273
+ replaceSelectionOnNextFinalRef,
30274
+ setDictationChunks,
30275
+ setDictationEditableChunk,
30276
+ setDictationError,
30277
+ setDictationInterimText,
30278
+ setDictationLastFinalChunk,
30279
+ setDictationUiState,
30280
+ setIsDictationPanelExpanded,
29222
30281
  textareaRef,
29223
30282
  ]);
29224
- const handleSpeechRecognitionEvent = useCallback((event) => {
30283
+ }
30284
+ /**
30285
+ * Builds the event handler that translates speech-recognition events into dictation UI updates.
30286
+ *
30287
+ * @private function of `useChatInputAreaDictation`
30288
+ */
30289
+ function useChatInputAreaDictationSpeechRecognitionEventHandler({ clearPendingStopFallback, handleDictationFinalResult, state, }) {
30290
+ const { setDictationUiState, setDictationInterimText, setDictationError, setIsDictationPanelExpanded } = state;
30291
+ return useCallback((event) => {
29225
30292
  switch (event.type) {
29226
30293
  case 'START':
29227
30294
  clearPendingStopFallback();
@@ -29255,7 +30322,21 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29255
30322
  setDictationInterimText('');
29256
30323
  return;
29257
30324
  }
29258
- }, [clearPendingStopFallback, handleDictationFinalResult]);
30325
+ }, [
30326
+ clearPendingStopFallback,
30327
+ handleDictationFinalResult,
30328
+ setDictationError,
30329
+ setDictationInterimText,
30330
+ setDictationUiState,
30331
+ setIsDictationPanelExpanded,
30332
+ ]);
30333
+ }
30334
+ /**
30335
+ * Wires the speech-recognition subscription and shutdown cleanup for dictation.
30336
+ *
30337
+ * @private function of `useChatInputAreaDictation`
30338
+ */
30339
+ function useChatInputAreaDictationSpeechRecognitionLifecycle({ speechRecognition, clearPendingStopFallback, handleSpeechRecognitionEvent, }) {
29259
30340
  useEffect(() => {
29260
30341
  if (!speechRecognition) {
29261
30342
  return;
@@ -29272,26 +30353,37 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29272
30353
  speechRecognition === null || speechRecognition === void 0 ? void 0 : speechRecognition.$stop();
29273
30354
  };
29274
30355
  }, [clearPendingStopFallback, speechRecognition]);
30356
+ }
30357
+ /**
30358
+ * Builds start/stop/toggle handlers for the speech-recognition session.
30359
+ *
30360
+ * @private function of `useChatInputAreaDictation`
30361
+ */
30362
+ function useChatInputAreaDictationVoiceInputControls({ speechRecognition, textareaRef, resolvedSpeechRecognitionLanguage, whisperMode, pendingStopFallbackRef, replaceSelectionOnNextFinalRef, clearPendingStopFallback, state, }) {
30363
+ const { dictationUiState, setDictationUiState, setDictationInterimText, setDictationError } = state;
29275
30364
  const startVoiceInput = useCallback(() => {
29276
- var _a, _b;
29277
30365
  if (!speechRecognition) {
29278
30366
  return;
29279
30367
  }
29280
- const textarea = textareaRef.current;
29281
- if (textarea) {
29282
- const selectionStart = (_a = textarea.selectionStart) !== null && _a !== void 0 ? _a : 0;
29283
- const selectionEnd = (_b = textarea.selectionEnd) !== null && _b !== void 0 ? _b : selectionStart;
29284
- replaceSelectionOnNextFinalRef.current = selectionStart !== selectionEnd;
29285
- }
30368
+ captureSelectionReplacementIntent(textareaRef, replaceSelectionOnNextFinalRef);
29286
30369
  setDictationError(null);
29287
30370
  setDictationInterimText('');
29288
30371
  setDictationUiState('listening');
29289
30372
  speechRecognition.$start({
29290
30373
  language: resolvedSpeechRecognitionLanguage,
29291
30374
  interimResults: true,
29292
- whisperMode: dictationSettings.whisperMode,
30375
+ whisperMode,
29293
30376
  });
29294
- }, [dictationSettings.whisperMode, resolvedSpeechRecognitionLanguage, speechRecognition, textareaRef]);
30377
+ }, [
30378
+ replaceSelectionOnNextFinalRef,
30379
+ resolvedSpeechRecognitionLanguage,
30380
+ setDictationError,
30381
+ setDictationInterimText,
30382
+ setDictationUiState,
30383
+ speechRecognition,
30384
+ textareaRef,
30385
+ whisperMode,
30386
+ ]);
29295
30387
  const stopVoiceInput = useCallback(() => {
29296
30388
  if (!speechRecognition) {
29297
30389
  return;
@@ -29303,7 +30395,13 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29303
30395
  setDictationUiState('idle');
29304
30396
  setDictationInterimText('');
29305
30397
  }, STOP_LISTENING_FALLBACK_TIMEOUT_MS);
29306
- }, [clearPendingStopFallback, speechRecognition]);
30398
+ }, [
30399
+ clearPendingStopFallback,
30400
+ pendingStopFallbackRef,
30401
+ setDictationInterimText,
30402
+ setDictationUiState,
30403
+ speechRecognition,
30404
+ ]);
29307
30405
  const handleToggleVoiceInput = useCallback(() => {
29308
30406
  if (!speechRecognition) {
29309
30407
  return;
@@ -29314,6 +30412,22 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29314
30412
  }
29315
30413
  startVoiceInput();
29316
30414
  }, [dictationUiState, speechRecognition, startVoiceInput, stopVoiceInput]);
30415
+ const handleRetryPermissionRequest = useCallback(() => {
30416
+ setDictationError(null);
30417
+ setDictationUiState('idle');
30418
+ handleToggleVoiceInput();
30419
+ }, [handleToggleVoiceInput, setDictationError, setDictationUiState]);
30420
+ return {
30421
+ handleToggleVoiceInput,
30422
+ handleRetryPermissionRequest,
30423
+ };
30424
+ }
30425
+ /**
30426
+ * Builds the correction and backtrack handlers for the latest finalized chunk.
30427
+ *
30428
+ * @private function of `useChatInputAreaDictation`
30429
+ */
30430
+ function useChatInputAreaDictationCorrectionHandlers({ dictationChunks, dictationLastFinalChunk, dictationEditableChunk, dictationDictionary, messageContentRef, applyMessageContent, focusTextareaSelection, setDictationChunks, setDictationLastFinalChunk, setDictationEditableChunk, setDictationDictionary, }) {
29317
30431
  const handleBacktrackLastChunk = useCallback(() => {
29318
30432
  var _a;
29319
30433
  const previousChunks = [...dictationChunks];
@@ -29327,50 +30441,123 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29327
30441
  setDictationLastFinalChunk(previousFinalChunk);
29328
30442
  setDictationEditableChunk(previousFinalChunk);
29329
30443
  focusTextareaSelection(lastChunk.start, lastChunk.start);
29330
- }, [applyMessageContent, dictationChunks, focusTextareaSelection]);
30444
+ }, [
30445
+ applyMessageContent,
30446
+ dictationChunks,
30447
+ focusTextareaSelection,
30448
+ setDictationChunks,
30449
+ setDictationEditableChunk,
30450
+ setDictationLastFinalChunk,
30451
+ ]);
29331
30452
  const handleApplyCorrection = useCallback(() => {
29332
- const correctedChunk = normalizeDictationWhitespace(dictationEditableChunk);
29333
- const previousChunk = dictationLastFinalChunk;
29334
- if (!correctedChunk || !previousChunk || correctedChunk === previousChunk) {
30453
+ const correctedChunk = resolveCorrectedDictationChunk(dictationEditableChunk, dictationLastFinalChunk);
30454
+ if (!correctedChunk) {
29335
30455
  return;
29336
30456
  }
29337
- const nextMessageContent = replaceLastOccurrence(messageContentRef.current, previousChunk, correctedChunk);
30457
+ const nextMessageContent = replaceLastOccurrence(messageContentRef.current, dictationLastFinalChunk, correctedChunk);
29338
30458
  applyMessageContent(nextMessageContent);
29339
30459
  setDictationLastFinalChunk(correctedChunk);
29340
- setDictationChunks((previousChunks) => {
29341
- if (previousChunks.length === 0) {
29342
- return previousChunks;
29343
- }
29344
- const nextChunks = [...previousChunks];
29345
- const lastChunk = nextChunks[nextChunks.length - 1];
29346
- if (!lastChunk) {
29347
- return previousChunks;
29348
- }
29349
- nextChunks[nextChunks.length - 1] = {
29350
- ...lastChunk,
29351
- finalText: correctedChunk,
29352
- };
29353
- return nextChunks;
29354
- });
29355
- setDictationDictionary(learnDictationDictionary(previousChunk, correctedChunk, dictationDictionary));
30460
+ setDictationChunks((previousChunks) => replaceLastDictationChunk(previousChunks, correctedChunk));
30461
+ setDictationDictionary(learnDictationDictionary(dictationLastFinalChunk, correctedChunk, dictationDictionary));
29356
30462
  }, [
29357
30463
  applyMessageContent,
29358
30464
  dictationDictionary,
29359
30465
  dictationEditableChunk,
29360
30466
  dictationLastFinalChunk,
29361
30467
  messageContentRef,
30468
+ setDictationChunks,
29362
30469
  setDictationDictionary,
30470
+ setDictationLastFinalChunk,
29363
30471
  ]);
29364
- const handleRetryPermissionRequest = useCallback(() => {
29365
- setDictationError(null);
29366
- setDictationUiState('idle');
29367
- handleToggleVoiceInput();
29368
- }, [handleToggleVoiceInput]);
30472
+ return {
30473
+ handleBacktrackLastChunk,
30474
+ handleApplyCorrection,
30475
+ };
30476
+ }
30477
+ /**
30478
+ * Manages speech-recognition state, transcript refinement, and correction UI.
30479
+ *
30480
+ * @private function of `<ChatInputArea/>`
30481
+ */
30482
+ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguage, textareaRef, messageContentRef, applyMessageContent, }) {
30483
+ const pendingStopFallbackRef = useRef(null);
30484
+ const replaceSelectionOnNextFinalRef = useRef(false);
30485
+ const [dictationUiState, setDictationUiState] = useState('idle');
30486
+ const [dictationInterimText, setDictationInterimText] = useState('');
30487
+ const [dictationError, setDictationError] = useState(null);
30488
+ const [dictationLastFinalChunk, setDictationLastFinalChunk] = useState('');
30489
+ const [dictationEditableChunk, setDictationEditableChunk] = useState('');
30490
+ const [dictationChunks, setDictationChunks] = useState([]);
30491
+ const [isDictationPanelExpanded, setIsDictationPanelExpanded] = useState(false);
30492
+ const { dictationSettings, setDictationSettings, dictationDictionary, setDictationDictionary } = useChatInputAreaDictationPersistence();
30493
+ const { speechRecognitionUiDescriptor, resolvedSpeechRecognitionLanguage, isBrowserSpeechFallbackSupported, microphoneSettingsUrl, } = useChatInputAreaDictationSupport({
30494
+ dictationUiState,
30495
+ speechRecognitionLanguage,
30496
+ });
30497
+ const dictationState = {
30498
+ dictationUiState,
30499
+ setDictationUiState,
30500
+ setDictationInterimText,
30501
+ setDictationError,
30502
+ setDictationLastFinalChunk,
30503
+ setDictationEditableChunk,
30504
+ setDictationChunks,
30505
+ setIsDictationPanelExpanded,
30506
+ };
30507
+ const clearPendingStopFallback = useCallback(() => {
30508
+ clearPendingStopFallbackTimeout(pendingStopFallbackRef);
30509
+ }, []);
30510
+ const focusTextareaSelection = useTextareaSelectionFocus(textareaRef);
30511
+ const handleDictationFinalResult = useChatInputAreaDictationFinalResultHandler({
30512
+ textareaRef,
30513
+ messageContentRef,
30514
+ applyMessageContent,
30515
+ dictationSettings,
30516
+ dictationDictionary,
30517
+ replaceSelectionOnNextFinalRef,
30518
+ focusTextareaSelection,
30519
+ state: dictationState,
30520
+ });
30521
+ const handleSpeechRecognitionEvent = useChatInputAreaDictationSpeechRecognitionEventHandler({
30522
+ clearPendingStopFallback,
30523
+ handleDictationFinalResult,
30524
+ state: dictationState,
30525
+ });
30526
+ useChatInputAreaDictationSpeechRecognitionLifecycle({
30527
+ speechRecognition,
30528
+ clearPendingStopFallback,
30529
+ handleSpeechRecognitionEvent,
30530
+ });
30531
+ const { handleToggleVoiceInput, handleRetryPermissionRequest } = useChatInputAreaDictationVoiceInputControls({
30532
+ speechRecognition,
30533
+ textareaRef,
30534
+ resolvedSpeechRecognitionLanguage,
30535
+ whisperMode: dictationSettings.whisperMode,
30536
+ pendingStopFallbackRef,
30537
+ replaceSelectionOnNextFinalRef,
30538
+ clearPendingStopFallback,
30539
+ state: {
30540
+ dictationUiState,
30541
+ setDictationUiState,
30542
+ setDictationInterimText,
30543
+ setDictationError,
30544
+ },
30545
+ });
30546
+ const { handleBacktrackLastChunk, handleApplyCorrection } = useChatInputAreaDictationCorrectionHandlers({
30547
+ dictationChunks,
30548
+ dictationLastFinalChunk,
30549
+ dictationEditableChunk,
30550
+ dictationDictionary,
30551
+ messageContentRef,
30552
+ applyMessageContent,
30553
+ focusTextareaSelection,
30554
+ setDictationChunks,
30555
+ setDictationLastFinalChunk,
30556
+ setDictationEditableChunk,
30557
+ setDictationDictionary,
30558
+ });
29369
30559
  const handleOpenBrowserSettings = useCallback(() => {
29370
- if (!microphoneSettingsUrl) {
29371
- return;
29372
- }
29373
- window.open(microphoneSettingsUrl, '_blank', 'noopener,noreferrer');
30560
+ openBrowserSettings(microphoneSettingsUrl);
29374
30561
  }, [microphoneSettingsUrl]);
29375
30562
  const toggleDictationPanel = useCallback(() => {
29376
30563
  setIsDictationPanelExpanded((value) => !value);
@@ -29384,12 +30571,14 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29384
30571
  [settingName]: checked,
29385
30572
  }));
29386
30573
  }, [setDictationSettings]);
29387
- const shouldShowDictationPanel = Boolean(speechRecognition &&
29388
- (isDictationPanelExpanded ||
29389
- dictationUiState !== 'idle' ||
29390
- Boolean(dictationInterimText) ||
29391
- Boolean(dictationError) ||
29392
- Boolean(dictationLastFinalChunk)));
30574
+ const shouldShowDictationPanel = resolveShouldShowDictationPanel({
30575
+ speechRecognition,
30576
+ isDictationPanelExpanded,
30577
+ dictationUiState,
30578
+ dictationInterimText,
30579
+ dictationError,
30580
+ dictationLastFinalChunk,
30581
+ });
29393
30582
  return {
29394
30583
  speechRecognitionUiDescriptor,
29395
30584
  shouldShowDictationPanel,
@@ -29414,13 +30603,206 @@ function useChatInputAreaDictation({ speechRecognition, speechRecognitionLanguag
29414
30603
  };
29415
30604
  }
29416
30605
 
30606
+ /**
30607
+ * Creates the shared color styles used by the composer.
30608
+ *
30609
+ * @private component of `<ChatInputArea/>`
30610
+ */
30611
+ function createChatInputAreaColorModel(participants, buttonColor) {
30612
+ var _a;
30613
+ const myColor = ((_a = participants.find((participant) => participant.isMe)) === null || _a === void 0 ? void 0 : _a.color) || USER_CHAT_COLOR;
30614
+ const inputBgColor = Color.from(myColor).then(lighten(0.4)).then(grayscale(0.7));
30615
+ const inputTextColor = inputBgColor.then(textColor);
30616
+ return {
30617
+ inputContainerStyle: {
30618
+ '--chat-placeholder-color': '#fff',
30619
+ '--input-bg-color': inputBgColor.toHex(),
30620
+ '--input-text-color': inputTextColor.toHex(),
30621
+ '--brand-color': buttonColor.toHex(),
30622
+ },
30623
+ primaryButtonStyle: {
30624
+ backgroundColor: buttonColor.toHex(),
30625
+ color: buttonColor.then(textColor).toHex(),
30626
+ },
30627
+ };
30628
+ }
30629
+ /**
30630
+ * Builds the reply preview view model when the composer is replying to a message.
30631
+ *
30632
+ * @private component of `<ChatInputArea/>`
30633
+ */
30634
+ function createChatInputAreaReplyPreviewModel(params) {
30635
+ const { replyingToMessage, participants, chatUiTranslations, onCancelReply } = params;
30636
+ if (!replyingToMessage) {
30637
+ return null;
30638
+ }
30639
+ const previewText = resolveChatMessageReplyPreviewText(replyingToMessage, {
30640
+ maxLength: 180,
30641
+ emptyLabel: 'Original message',
30642
+ });
30643
+ const senderLabel = resolveChatMessageReplySenderLabel({
30644
+ sender: replyingToMessage.sender,
30645
+ participants,
30646
+ });
30647
+ if (!previewText || !senderLabel) {
30648
+ return null;
30649
+ }
30650
+ return {
30651
+ label: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.replyingToLabel) || 'Replying to',
30652
+ senderLabel,
30653
+ previewText,
30654
+ dismissLabel: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.cancelReplyLabel) || 'Cancel reply',
30655
+ onDismiss: onCancelReply || undefined,
30656
+ };
30657
+ }
30658
+ /**
30659
+ * Adds drag-and-drop handlers only when uploads are enabled.
30660
+ *
30661
+ * @private component of `<ChatInputArea/>`
30662
+ */
30663
+ function createChatInputAreaRootProps(params) {
30664
+ const { onFileUpload, handleDrop, handleDragOver, handleDragLeave } = params;
30665
+ if (!onFileUpload) {
30666
+ return {};
30667
+ }
30668
+ return {
30669
+ onDrop: handleDrop,
30670
+ onDragOver: handleDragOver,
30671
+ onDragLeave: handleDragLeave,
30672
+ };
30673
+ }
30674
+ /**
30675
+ * Formats uploaded file sizes for the preview chips.
30676
+ *
30677
+ * @private component of `<ChatInputArea/>`
30678
+ */
30679
+ function formatUploadedFileSizeInKilobytes(fileSizeInBytes) {
30680
+ return `${(fileSizeInBytes / 1024).toFixed(1)} KB`;
30681
+ }
30682
+ /**
30683
+ * Renders the optional reply preview section.
30684
+ *
30685
+ * @private component of `<ChatInputArea/>`
30686
+ */
30687
+ function ChatInputAreaReplyPreviewSection({ replyPreview }) {
30688
+ if (!replyPreview) {
30689
+ return null;
30690
+ }
30691
+ return (jsx(ChatReplyPreview, { label: replyPreview.label, senderLabel: replyPreview.senderLabel, previewText: replyPreview.previewText, className: styles$5.replyComposerPreview, dismissLabel: replyPreview.dismissLabel, onDismiss: replyPreview.onDismiss }));
30692
+ }
30693
+ /**
30694
+ * Renders uploaded file previews above the composer.
30695
+ *
30696
+ * @private component of `<ChatInputArea/>`
30697
+ */
30698
+ function ChatInputAreaUploadedFilesSection(props) {
30699
+ const { uploadedFiles, removeUploadedFile } = props;
30700
+ if (uploadedFiles.length === 0) {
30701
+ return null;
30702
+ }
30703
+ return (jsx("div", { className: styles$5.filePreviewContainer, children: uploadedFiles.map((uploadedFile) => (jsxs("div", { className: styles$5.filePreview, children: [jsx("div", { className: styles$5.fileIcon, children: "\uD83D\uDCCE" }), jsxs("div", { className: styles$5.fileInfo, children: [jsx("div", { className: styles$5.fileName, children: uploadedFile.file.name }), jsx("div", { className: styles$5.fileSize, children: formatUploadedFileSizeInKilobytes(uploadedFile.file.size) })] }), jsx("button", { className: styles$5.removeFileButton, onClick: () => removeUploadedFile(uploadedFile.id), title: "Remove file", children: jsx(CloseIcon, {}) })] }, uploadedFile.id))) }));
30704
+ }
30705
+ /**
30706
+ * Renders the hidden file input and attachment trigger when uploads are enabled.
30707
+ *
30708
+ * @private component of `<ChatInputArea/>`
30709
+ */
30710
+ function ChatInputAreaAttachmentButton(props) {
30711
+ const { onFileUpload, fileInputRef, handleFileInputChange, onButtonClick, openFilePicker, isUploading, primaryButtonStyle, } = props;
30712
+ if (!onFileUpload) {
30713
+ return null;
30714
+ }
30715
+ return (jsxs(Fragment, { children: [jsx("input", { ref: fileInputRef, type: "file", multiple: true, style: { display: 'none' }, onChange: handleFileInputChange }), jsx("button", { type: "button", style: primaryButtonStyle, className: classNames(styles$5.attachmentButton, chatCssClassNames.inputAttachmentButton), onClick: onButtonClick(openFilePicker), disabled: isUploading, title: "Attach file", children: jsx(AttachmentIcon, { size: 20 }) })] }));
30716
+ }
30717
+ /**
30718
+ * Creates the voice button colors from the current dictation state.
30719
+ *
30720
+ * @private component of `<ChatInputArea/>`
30721
+ */
30722
+ function createVoiceButtonStyle(params) {
30723
+ const { buttonColor, speechRecognitionUiDescriptor } = params;
30724
+ if (speechRecognitionUiDescriptor.isButtonActive) {
30725
+ return {
30726
+ backgroundColor: Color.from('#ff4444').toHex(),
30727
+ color: Color.from('#ffffff').toHex(),
30728
+ };
30729
+ }
30730
+ return {
30731
+ backgroundColor: buttonColor.toHex(),
30732
+ color: buttonColor.then(textColor).toHex(),
30733
+ };
30734
+ }
30735
+ /**
30736
+ * Renders the voice input trigger when speech recognition is available.
30737
+ *
30738
+ * @private component of `<ChatInputArea/>`
30739
+ */
30740
+ function ChatInputAreaVoiceButton(props) {
30741
+ const { speechRecognition, isVoiceCalling, buttonColor, speechRecognitionUiDescriptor, onButtonClick, handleToggleVoiceInput, } = props;
30742
+ if (!speechRecognition) {
30743
+ return null;
30744
+ }
30745
+ return (jsx("button", { "data-button-type": "voice", disabled: speechRecognitionUiDescriptor.isButtonDisabled, style: createVoiceButtonStyle({
30746
+ buttonColor,
30747
+ speechRecognitionUiDescriptor,
30748
+ }), className: classNames(styles$5.voiceButton, chatCssClassNames.inputVoiceButton, (isVoiceCalling || speechRecognitionUiDescriptor.isButtonActive) && styles$5.voiceButtonActive), onClick: onButtonClick((event) => {
30749
+ event.preventDefault();
30750
+ handleToggleVoiceInput();
30751
+ }), title: speechRecognitionUiDescriptor.buttonTitle, "aria-label": speechRecognitionUiDescriptor.buttonTitle, children: jsx(MicIcon, { size: 25 }) }));
30752
+ }
30753
+ /**
30754
+ * Renders the composer send button.
30755
+ *
30756
+ * @private component of `<ChatInputArea/>`
30757
+ */
30758
+ function ChatInputAreaSendButton(props) {
30759
+ const { primaryButtonStyle, onButtonClick, handleSend } = props;
30760
+ return (jsx("button", { "data-button-type": "call-to-action", className: chatCssClassNames.inputSendButton, style: primaryButtonStyle, onClick: onButtonClick((event) => {
30761
+ event.preventDefault();
30762
+ /* not await */ handleSend();
30763
+ }), children: jsx(SendIcon, { size: 25 }) }));
30764
+ }
30765
+ /**
30766
+ * Renders the optional dictation panel below the composer.
30767
+ *
30768
+ * @private component of `<ChatInputArea/>`
30769
+ */
30770
+ function ChatInputAreaDictationPanelSection(props) {
30771
+ const { speechRecognition } = props;
30772
+ if (!speechRecognition) {
30773
+ return null;
30774
+ }
30775
+ return (jsx(ChatInputAreaDictationPanel, { bubbleText: props.speechRecognitionUiDescriptor.bubbleText, bubbleTone: props.speechRecognitionUiDescriptor.bubbleTone, shouldShowPanel: props.shouldShowDictationPanel, isExpanded: props.isDictationPanelExpanded, interimText: props.dictationInterimText, error: props.dictationError, lastFinalChunk: props.dictationLastFinalChunk, editableChunk: props.dictationEditableChunk, canBacktrack: props.canBacktrack, dictationSettings: props.dictationSettings, isBrowserSpeechFallbackSupported: props.isBrowserSpeechFallbackSupported, canOpenBrowserSettings: props.canOpenBrowserSettings, onToggleExpanded: props.toggleDictationPanel, onExpand: props.expandDictationPanel, onEditableChunkChange: props.setDictationEditableChunk, onRetryPermissionRequest: props.handleRetryPermissionRequest, onOpenBrowserSettings: props.handleOpenBrowserSettings, onApplyCorrection: props.handleApplyCorrection, onBacktrackLastChunk: props.handleBacktrackLastChunk, onDictationSettingChange: props.handleDictationSettingChange }));
30776
+ }
30777
+ /**
30778
+ * Renders the temporary upload progress indicator.
30779
+ *
30780
+ * @private component of `<ChatInputArea/>`
30781
+ */
30782
+ function ChatInputAreaUploadProgress({ isUploading }) {
30783
+ if (!isUploading) {
30784
+ return null;
30785
+ }
30786
+ return (jsxs("div", { className: styles$5.uploadProgress, children: [jsx("div", { className: styles$5.uploadProgressBar, children: jsx("div", { className: styles$5.uploadProgressFill }) }), jsx("span", { children: "Uploading files..." })] }));
30787
+ }
30788
+ /**
30789
+ * Renders the drag-and-drop overlay when uploads are enabled and active.
30790
+ *
30791
+ * @private component of `<ChatInputArea/>`
30792
+ */
30793
+ function ChatInputAreaDragOverlay(props) {
30794
+ const { isDragOver, onFileUpload } = props;
30795
+ if (!isDragOver || !onFileUpload) {
30796
+ return null;
30797
+ }
30798
+ return (jsx("div", { className: styles$5.dragOverlay, children: jsxs("div", { className: styles$5.dragOverlayContent, children: [jsx(AttachmentIcon, { size: 48 }), jsx("span", { children: "Drop files here to upload" })] }) }));
30799
+ }
29417
30800
  /**
29418
30801
  * Renders the chat input area with text, file upload, and voice controls.
29419
30802
  *
29420
30803
  * @private component of `<Chat/>`
29421
30804
  */
29422
30805
  function ChatInputArea(props) {
29423
- var _a;
29424
30806
  const { onMessage, onChange, onFileUpload, speechRecognition, speechRecognitionLanguage, defaultMessage, replyingToMessage, onCancelReply, enterBehavior, resolveEnterBehavior, placeholderMessageContent, isFocusedOnLoad, isMobile, isVoiceCalling, participants, buttonColor, soundSystem, onButtonClick, chatInputClassName, chatUiTranslations, } = props;
29425
30807
  const { fileInputRef, uploadedFiles, uploadedFilesRef, isDragOver, isUploading, handleDrop, handleDragOver, handleDragLeave, handlePaste, handleFileInputChange, removeUploadedFile, clearUploadedFiles, openFilePicker, } = useChatInputAreaAttachments({
29426
30808
  onFileUpload,
@@ -29450,52 +30832,20 @@ function ChatInputArea(props) {
29450
30832
  if (!onMessage) {
29451
30833
  return null;
29452
30834
  }
29453
- const myColor = ((_a = participants.find((participant) => participant.isMe)) === null || _a === void 0 ? void 0 : _a.color) || USER_CHAT_COLOR;
29454
- const inputBgColor = Color.from(myColor).then(lighten(0.4)).then(grayscale(0.7));
29455
- const inputTextColor = inputBgColor.then(textColor);
29456
- const replyPreviewLabel = (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.replyingToLabel) || 'Replying to';
29457
- const cancelReplyLabel = (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.cancelReplyLabel) || 'Cancel reply';
29458
- const replyPreviewText = replyingToMessage
29459
- ? resolveChatMessageReplyPreviewText(replyingToMessage, { maxLength: 180, emptyLabel: 'Original message' })
29460
- : null;
29461
- const replySenderLabel = replyingToMessage
29462
- ? resolveChatMessageReplySenderLabel({
29463
- sender: replyingToMessage.sender,
29464
- participants,
29465
- })
29466
- : null;
29467
- return (jsxs("div", { className: classNames(styles$5.chatInput, chatInputClassName, isDragOver && styles$5.dragOver), ...(onFileUpload
29468
- ? {
29469
- onDrop: handleDrop,
29470
- onDragOver: handleDragOver,
29471
- onDragLeave: handleDragLeave,
29472
- }
29473
- : {}), children: [replyingToMessage && replyPreviewText && replySenderLabel && (jsx(ChatReplyPreview, { label: replyPreviewLabel, senderLabel: replySenderLabel, previewText: replyPreviewText, className: styles$5.replyComposerPreview, dismissLabel: cancelReplyLabel, onDismiss: onCancelReply || undefined })), uploadedFiles.length > 0 && (jsx("div", { className: styles$5.filePreviewContainer, children: uploadedFiles.map((uploadedFile) => (jsxs("div", { className: styles$5.filePreview, children: [jsx("div", { className: styles$5.fileIcon, children: "\uD83D\uDCCE" }), jsxs("div", { className: styles$5.fileInfo, children: [jsx("div", { className: styles$5.fileName, children: uploadedFile.file.name }), jsxs("div", { className: styles$5.fileSize, children: [(uploadedFile.file.size / 1024).toFixed(1), " KB"] })] }), jsx("button", { className: styles$5.removeFileButton, onClick: () => removeUploadedFile(uploadedFile.id), title: "Remove file", children: jsx(CloseIcon, {}) })] }, uploadedFile.id))) })), jsxs("div", { className: classNames(styles$5.inputContainer, chatCssClassNames.inputContainer), style: {
29474
- '--chat-placeholder-color': '#fff',
29475
- '--input-bg-color': inputBgColor.toHex(),
29476
- '--input-text-color': inputTextColor.toHex(),
29477
- '--brand-color': buttonColor.toHex(),
29478
- }, children: [jsx("textarea", { ref: textareaRef, className: chatCssClassNames.inputTextarea, onPaste: handlePaste, value: messageContent, placeholder: placeholderMessageContent || 'Write a message...', onChange: handleTextInputChange, onKeyDown: handleComposerKeyDown }), onFileUpload && (jsxs(Fragment, { children: [jsx("input", { ref: fileInputRef, type: "file", multiple: true, style: { display: 'none' }, onChange: handleFileInputChange }), jsx("button", { type: "button", style: {
29479
- backgroundColor: buttonColor.toHex(),
29480
- color: buttonColor.then(textColor).toHex(),
29481
- }, className: classNames(styles$5.attachmentButton, chatCssClassNames.inputAttachmentButton), onClick: onButtonClick(openFilePicker), disabled: isUploading, title: "Attach file", children: jsx(AttachmentIcon, { size: 20 }) })] })), speechRecognition && (jsx("button", { "data-button-type": "voice", disabled: speechRecognitionUiDescriptor.isButtonDisabled, style: {
29482
- backgroundColor: speechRecognitionUiDescriptor.isButtonActive
29483
- ? Color.from('#ff4444').toHex()
29484
- : buttonColor.toHex(),
29485
- color: speechRecognitionUiDescriptor.isButtonActive
29486
- ? Color.from('#ffffff').toHex()
29487
- : buttonColor.then(textColor).toHex(),
29488
- }, className: classNames(styles$5.voiceButton, chatCssClassNames.inputVoiceButton, (isVoiceCalling || speechRecognitionUiDescriptor.isButtonActive) &&
29489
- styles$5.voiceButtonActive), onClick: onButtonClick((event) => {
29490
- event.preventDefault();
29491
- handleToggleVoiceInput();
29492
- }), title: speechRecognitionUiDescriptor.buttonTitle, "aria-label": speechRecognitionUiDescriptor.buttonTitle, children: jsx(MicIcon, { size: 25 }) })), jsx("button", { "data-button-type": "call-to-action", className: chatCssClassNames.inputSendButton, style: {
29493
- backgroundColor: buttonColor.toHex(),
29494
- color: buttonColor.then(textColor).toHex(),
29495
- }, onClick: onButtonClick((event) => {
29496
- event.preventDefault();
29497
- /* not await */ handleSend();
29498
- }), children: jsx(SendIcon, { size: 25 }) })] }), speechRecognition && (jsx(ChatInputAreaDictationPanel, { bubbleText: speechRecognitionUiDescriptor.bubbleText, bubbleTone: speechRecognitionUiDescriptor.bubbleTone, shouldShowPanel: shouldShowDictationPanel, isExpanded: isDictationPanelExpanded, interimText: dictationInterimText, error: dictationError, lastFinalChunk: dictationLastFinalChunk, editableChunk: dictationEditableChunk, canBacktrack: canBacktrack, dictationSettings: dictationSettings, isBrowserSpeechFallbackSupported: isBrowserSpeechFallbackSupported, canOpenBrowserSettings: canOpenBrowserSettings, onToggleExpanded: toggleDictationPanel, onExpand: expandDictationPanel, onEditableChunkChange: setDictationEditableChunk, onRetryPermissionRequest: handleRetryPermissionRequest, onOpenBrowserSettings: handleOpenBrowserSettings, onApplyCorrection: handleApplyCorrection, onBacktrackLastChunk: handleBacktrackLastChunk, onDictationSettingChange: handleDictationSettingChange })), isUploading && (jsxs("div", { className: styles$5.uploadProgress, children: [jsx("div", { className: styles$5.uploadProgressBar, children: jsx("div", { className: styles$5.uploadProgressFill }) }), jsx("span", { children: "Uploading files..." })] })), isDragOver && onFileUpload && (jsx("div", { className: styles$5.dragOverlay, children: jsxs("div", { className: styles$5.dragOverlayContent, children: [jsx(AttachmentIcon, { size: 48 }), jsx("span", { children: "Drop files here to upload" })] }) }))] }));
30835
+ const { inputContainerStyle, primaryButtonStyle } = createChatInputAreaColorModel(participants, buttonColor);
30836
+ const replyPreview = createChatInputAreaReplyPreviewModel({
30837
+ replyingToMessage,
30838
+ participants,
30839
+ chatUiTranslations,
30840
+ onCancelReply,
30841
+ });
30842
+ const rootProps = createChatInputAreaRootProps({
30843
+ onFileUpload,
30844
+ handleDrop,
30845
+ handleDragOver,
30846
+ handleDragLeave,
30847
+ });
30848
+ return (jsxs("div", { className: classNames(styles$5.chatInput, chatInputClassName, isDragOver && styles$5.dragOver), ...rootProps, children: [jsx(ChatInputAreaReplyPreviewSection, { replyPreview: replyPreview }), jsx(ChatInputAreaUploadedFilesSection, { uploadedFiles: uploadedFiles, removeUploadedFile: removeUploadedFile }), jsxs("div", { className: classNames(styles$5.inputContainer, chatCssClassNames.inputContainer), style: inputContainerStyle, children: [jsx("textarea", { ref: textareaRef, className: chatCssClassNames.inputTextarea, onPaste: handlePaste, value: messageContent, placeholder: placeholderMessageContent || 'Write a message...', onChange: handleTextInputChange, onKeyDown: handleComposerKeyDown }), jsx(ChatInputAreaAttachmentButton, { onFileUpload: onFileUpload, fileInputRef: fileInputRef, handleFileInputChange: handleFileInputChange, onButtonClick: onButtonClick, openFilePicker: openFilePicker, isUploading: isUploading, primaryButtonStyle: primaryButtonStyle }), jsx(ChatInputAreaVoiceButton, { speechRecognition: speechRecognition, isVoiceCalling: isVoiceCalling, buttonColor: buttonColor, speechRecognitionUiDescriptor: speechRecognitionUiDescriptor, onButtonClick: onButtonClick, handleToggleVoiceInput: handleToggleVoiceInput }), jsx(ChatInputAreaSendButton, { primaryButtonStyle: primaryButtonStyle, onButtonClick: onButtonClick, handleSend: handleSend })] }), jsx(ChatInputAreaDictationPanelSection, { speechRecognition: speechRecognition, speechRecognitionUiDescriptor: speechRecognitionUiDescriptor, shouldShowDictationPanel: shouldShowDictationPanel, isDictationPanelExpanded: isDictationPanelExpanded, dictationInterimText: dictationInterimText, dictationError: dictationError, dictationLastFinalChunk: dictationLastFinalChunk, dictationEditableChunk: dictationEditableChunk, canBacktrack: canBacktrack, dictationSettings: dictationSettings, isBrowserSpeechFallbackSupported: isBrowserSpeechFallbackSupported, canOpenBrowserSettings: canOpenBrowserSettings, toggleDictationPanel: toggleDictationPanel, expandDictationPanel: expandDictationPanel, setDictationEditableChunk: setDictationEditableChunk, handleRetryPermissionRequest: handleRetryPermissionRequest, handleOpenBrowserSettings: handleOpenBrowserSettings, handleApplyCorrection: handleApplyCorrection, handleBacktrackLastChunk: handleBacktrackLastChunk, handleDictationSettingChange: handleDictationSettingChange }), jsx(ChatInputAreaUploadProgress, { isUploading: isUploading }), jsx(ChatInputAreaDragOverlay, { isDragOver: isDragOver, onFileUpload: onFileUpload })] }));
29499
30849
  }
29500
30850
 
29501
30851
  // Note: [💞] Ignore a discrepancy between file name and entity name
@@ -36458,6 +37808,12 @@ const OVERWRITTEN_COMMITMENT_GROUP_BY_TYPE = new Map([
36458
37808
  ['GOAL', 'GOAL'],
36459
37809
  ['GOALS', 'GOAL'],
36460
37810
  ]);
37811
+ /**
37812
+ * Legacy commitments that should be parsed for compatibility but ignored by the model-requirements pipeline.
37813
+ *
37814
+ * @private internal constant of `filterCommitmentsForAgentModelRequirements`
37815
+ */
37816
+ const IGNORED_COMMITMENT_TYPES = new Set(['WALLET', 'WALLETS']);
36461
37817
  /**
36462
37818
  * Applies the commitment filtering rules used before commitment definitions are executed.
36463
37819
  *
@@ -36506,6 +37862,9 @@ function filterOverwrittenCommitments(commitments) {
36506
37862
  function filterDeletedCommitments(commitments) {
36507
37863
  const filteredCommitments = [];
36508
37864
  for (const commitment of commitments) {
37865
+ if (isIgnoredCommitmentType(commitment.type)) {
37866
+ continue;
37867
+ }
36509
37868
  if (!isDeleteCommitmentType(commitment.type)) {
36510
37869
  filteredCommitments.push(commitment);
36511
37870
  continue;
@@ -36536,6 +37895,17 @@ function filterDeletedCommitments(commitments) {
36536
37895
  function isDeleteCommitmentType(commitmentType) {
36537
37896
  return DELETE_COMMITMENT_TYPES.has(commitmentType);
36538
37897
  }
37898
+ /**
37899
+ * Checks whether a parsed commitment is intentionally ignored by the current model compiler.
37900
+ *
37901
+ * @param commitmentType - Commitment type to check.
37902
+ * @returns `true` when the commitment should not affect model requirements.
37903
+ *
37904
+ * @private internal utility of `filterDeletedCommitments`
37905
+ */
37906
+ function isIgnoredCommitmentType(commitmentType) {
37907
+ return IGNORED_COMMITMENT_TYPES.has(commitmentType);
37908
+ }
36539
37909
  /**
36540
37910
  * Extracts normalized parameter names used for DELETE-like invalidation matching.
36541
37911
  *
@@ -41233,8 +42603,8 @@ class OpenAiAgentKitExecutionTools extends OpenAiVectorStoreHandler {
41233
42603
  * Prepares an AgentKit agent with optional knowledge sources and tool definitions.
41234
42604
  */
41235
42605
  async prepareAgentKitAgent(options) {
41236
- var _a, _b, _c;
41237
- const { name, instructions, knowledgeSources, tools, nativeAgentKitTools, vectorStoreId: cachedVectorStoreId, storeAsPrepared, } = options;
42606
+ var _a, _b;
42607
+ const { name, instructions, knowledgeSources, tools, vectorStoreId: cachedVectorStoreId, storeAsPrepared, } = options;
41238
42608
  await this.ensureAgentKitDefaults();
41239
42609
  if (this.options.isVerbose) {
41240
42610
  console.info('[🤰]', 'Preparing OpenAI AgentKit agent', {
@@ -41242,11 +42612,10 @@ class OpenAiAgentKitExecutionTools extends OpenAiVectorStoreHandler {
41242
42612
  instructionsLength: instructions.length,
41243
42613
  knowledgeSourcesCount: (_a = knowledgeSources === null || knowledgeSources === void 0 ? void 0 : knowledgeSources.length) !== null && _a !== void 0 ? _a : 0,
41244
42614
  toolsCount: (_b = tools === null || tools === void 0 ? void 0 : tools.length) !== null && _b !== void 0 ? _b : 0,
41245
- nativeAgentKitToolsCount: (_c = nativeAgentKitTools === null || nativeAgentKitTools === void 0 ? void 0 : nativeAgentKitTools.length) !== null && _c !== void 0 ? _c : 0,
41246
42615
  });
41247
42616
  }
41248
42617
  let vectorStoreId = cachedVectorStoreId;
41249
- if (!vectorStoreId && knowledgeSources && knowledgeSources.length > 0) {
42618
+ if (this.isNativeKnowledgeSearchEnabled && !vectorStoreId && knowledgeSources && knowledgeSources.length > 0) {
41250
42619
  const vectorStoreResult = await this.createVectorStoreWithKnowledgeSources({
41251
42620
  client: await this.getClient(),
41252
42621
  name,
@@ -41255,13 +42624,19 @@ class OpenAiAgentKitExecutionTools extends OpenAiVectorStoreHandler {
41255
42624
  });
41256
42625
  vectorStoreId = vectorStoreResult.vectorStoreId;
41257
42626
  }
41258
- else if (vectorStoreId && this.options.isVerbose) {
42627
+ else if (this.isNativeKnowledgeSearchEnabled && vectorStoreId && this.options.isVerbose) {
41259
42628
  console.info('[🤰]', 'Using cached vector store for AgentKit agent', {
41260
42629
  name,
41261
42630
  vectorStoreId,
41262
42631
  });
41263
42632
  }
41264
- const agentKitTools = this.buildAgentKitTools({ tools, nativeAgentKitTools, vectorStoreId });
42633
+ if (!this.isNativeKnowledgeSearchEnabled) {
42634
+ vectorStoreId = undefined;
42635
+ }
42636
+ const agentKitTools = this.buildAgentKitTools({
42637
+ tools,
42638
+ vectorStoreId,
42639
+ });
41265
42640
  const openAiAgentKitAgent = new Agent$1({
41266
42641
  name,
41267
42642
  model: this.agentKitModelName,
@@ -41280,7 +42655,7 @@ class OpenAiAgentKitExecutionTools extends OpenAiVectorStoreHandler {
41280
42655
  name,
41281
42656
  model: this.agentKitModelName,
41282
42657
  toolCount: agentKitTools.length,
41283
- hasVectorStore: Boolean(vectorStoreId),
42658
+ hasVectorStore: this.isNativeKnowledgeSearchEnabled && Boolean(vectorStoreId),
41284
42659
  });
41285
42660
  }
41286
42661
  return preparedAgent;
@@ -41300,14 +42675,11 @@ class OpenAiAgentKitExecutionTools extends OpenAiVectorStoreHandler {
41300
42675
  * Builds the tool list for AgentKit, including hosted file search when applicable.
41301
42676
  */
41302
42677
  buildAgentKitTools(options) {
41303
- const { tools, nativeAgentKitTools, vectorStoreId } = options;
42678
+ const { tools, vectorStoreId } = options;
41304
42679
  const agentKitTools = [];
41305
42680
  if (vectorStoreId) {
41306
42681
  agentKitTools.push(fileSearchTool(vectorStoreId));
41307
42682
  }
41308
- if (nativeAgentKitTools && nativeAgentKitTools.length > 0) {
41309
- agentKitTools.push(...nativeAgentKitTools);
41310
- }
41311
42683
  if (tools && tools.length > 0) {
41312
42684
  let scriptTools = null;
41313
42685
  for (const toolDefinition of tools) {
@@ -41782,6 +43154,12 @@ class OpenAiAgentKitExecutionTools extends OpenAiVectorStoreHandler {
41782
43154
  get agentKitOptions() {
41783
43155
  return this.options;
41784
43156
  }
43157
+ /**
43158
+ * Returns true when hosted OpenAI vector-store search should back `knowledgeSources`.
43159
+ */
43160
+ get isNativeKnowledgeSearchEnabled() {
43161
+ return this.agentKitOptions.isNativeKnowledgeSearchEnabled !== false;
43162
+ }
41785
43163
  /**
41786
43164
  * Discriminant for type guards.
41787
43165
  */
@@ -47509,6 +48887,7 @@ const TOOL_TITLES = {
47509
48887
  request_wallet_record: { title: 'Requesting wallet', emoji: '👛' },
47510
48888
  web_search: { title: 'Searching the web', emoji: '🔎' },
47511
48889
  deep_search: { title: 'Deep research', emoji: '🔬' },
48890
+ knowledge_search: { title: 'Searching knowledge', emoji: '📚' },
47512
48891
  useSearchEngine: { title: 'Searching the web', emoji: '🔎' },
47513
48892
  search: { title: 'Searching the web', emoji: '🔎' },
47514
48893
  useBrowser: { title: 'Browsing the web', emoji: '🌐' },
@@ -48384,6 +49763,13 @@ const ChatMessageItem = memo(
48384
49763
  content: sanitizedContentWithoutButtons,
48385
49764
  citations: message.citations,
48386
49765
  }), [message.citations, sanitizedContentWithoutButtons]);
49766
+ const structuredSourceCitations = useMemo(() => {
49767
+ const footnoteSourceKeys = new Set(citationFootnoteRenderModel.footnotes.map((footnote) => footnote.citation.source.trim().toLowerCase()));
49768
+ return (message.sources || []).filter((source) => {
49769
+ const sourceKey = source.source.trim().toLowerCase();
49770
+ return sourceKey && !footnoteSourceKeys.has(sourceKey);
49771
+ });
49772
+ }, [citationFootnoteRenderModel.footnotes, message.sources]);
48387
49773
  const contentSegments = useMemo(() => splitMessageContentIntoSegments(citationFootnoteRenderModel.content), [citationFootnoteRenderModel.content]);
48388
49774
  const hasMapSegment = useMemo(() => contentSegments.some((segment) => segment.type === 'map'), [contentSegments]);
48389
49775
  const [localHoveredRating, setLocalHoveredRating] = useState(0);
@@ -48655,7 +50041,7 @@ const ChatMessageItem = memo(
48655
50041
  '--message-bg-color': isAgentArticleMode ? articleModeBackgroundColor : color.toHex(),
48656
50042
  '--message-text-color': isAgentArticleMode ? articleModeTextColor : colorOfText.toHex(),
48657
50043
  '--chat-message-swipe-offset': swipeTranslation,
48658
- }, onPointerDown: handleReplyPointerDown, onPointerMove: handleReplyPointerMove, onPointerUp: handleReplyPointerEnd, onPointerCancel: resetReplySwipe, children: [isReplyActionEnabled && (jsx("div", { className: classNames(styles$5.replySwipeIndicator, isMe && styles$5.replySwipeIndicatorRight, isReplySwipeArmed && styles$5.replySwipeIndicatorActive), "aria-hidden": "true", children: jsx(Reply, { className: styles$5.replySwipeIndicatorIcon }) })), !shouldRenderArticleActionsBar && renderMessageReadAndCopyControls(), message.isVoiceCall && (jsx("div", { className: styles$5.voiceCallIndicator, children: jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: jsx("path", { d: "M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z" }) }) })), replyingToMessage && replyPreviewText && replySenderLabel && (jsx(ChatReplyPreview, { label: replyPreviewLabel, senderLabel: replySenderLabel, previewText: replyPreviewText, className: styles$5.replyBubblePreview })), jsx("div", { ref: contentWithoutButtonsRef, children: jsx(ChatMessageRichContent, { content: message.content, contentSegments: contentSegments, streamingFeaturePlaceholderKind: streamingFeaturePlaceholderKind, onCreateAgent: onCreateAgent, mode: mode }) }), message.attachments && message.attachments.length > 0 && (jsx("div", { className: styles$5.attachments, children: message.attachments.map((attachment, index) => (jsxs("a", { href: attachment.url, target: "_blank", rel: "noopener noreferrer", className: styles$5.attachment, title: attachment.name, children: [jsx("span", { className: styles$5.attachmentIcon, children: "\uD83D\uDCCE" }), jsx("span", { className: styles$5.attachmentName, children: attachment.name })] }, index))) })), jsx(ChatMessageToolCallChips, { chips: toolCallChips, onToolCallClick: onToolCallClick }), citationFootnoteRenderModel.footnotes.length > 0 && (jsx("div", { className: styles$5.citationFootnotes, children: citationFootnoteRenderModel.footnotes.map((footnote) => (jsxs("div", { className: styles$5.citationFootnoteItem, children: [jsx("span", { className: styles$5.citationFootnoteNumber, children: footnote.number }), jsx(SourceChip, { citation: footnote.citation, onClick: onCitationClick, isCitationIdVisible: false })] }, `citation-footnote-${footnote.number}-${footnote.citation.source}`))) })), transitiveCitations.length > 0 && (jsx("div", { className: styles$5.sourceCitations, children: transitiveCitations.map((citation, index) => (jsx(SourceChip, { citation: citation, suffix: `by ${citation.origin.label}`, onClick: onCitationClick }, `team-source-${citation.source}-${index}`))) })), shouldShowButtons && (jsx("div", { className: styles$5.messageButtons, children: renderableButtons.map(({ button, buttonIndex }) => (jsx("button", { type: "button", className: classNames(styles$5.messageButton, button.type === 'action' && styles$5.actionMessageButton), onClick: (event) => {
50044
+ }, onPointerDown: handleReplyPointerDown, onPointerMove: handleReplyPointerMove, onPointerUp: handleReplyPointerEnd, onPointerCancel: resetReplySwipe, children: [isReplyActionEnabled && (jsx("div", { className: classNames(styles$5.replySwipeIndicator, isMe && styles$5.replySwipeIndicatorRight, isReplySwipeArmed && styles$5.replySwipeIndicatorActive), "aria-hidden": "true", children: jsx(Reply, { className: styles$5.replySwipeIndicatorIcon }) })), !shouldRenderArticleActionsBar && renderMessageReadAndCopyControls(), message.isVoiceCall && (jsx("div", { className: styles$5.voiceCallIndicator, children: jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: jsx("path", { d: "M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z" }) }) })), replyingToMessage && replyPreviewText && replySenderLabel && (jsx(ChatReplyPreview, { label: replyPreviewLabel, senderLabel: replySenderLabel, previewText: replyPreviewText, className: styles$5.replyBubblePreview })), jsx("div", { ref: contentWithoutButtonsRef, children: jsx(ChatMessageRichContent, { content: message.content, contentSegments: contentSegments, streamingFeaturePlaceholderKind: streamingFeaturePlaceholderKind, onCreateAgent: onCreateAgent, mode: mode }) }), message.attachments && message.attachments.length > 0 && (jsx("div", { className: styles$5.attachments, children: message.attachments.map((attachment, index) => (jsxs("a", { href: attachment.url, target: "_blank", rel: "noopener noreferrer", className: styles$5.attachment, title: attachment.name, children: [jsx("span", { className: styles$5.attachmentIcon, children: "\uD83D\uDCCE" }), jsx("span", { className: styles$5.attachmentName, children: attachment.name })] }, index))) })), jsx(ChatMessageToolCallChips, { chips: toolCallChips, onToolCallClick: onToolCallClick }), citationFootnoteRenderModel.footnotes.length > 0 && (jsx("div", { className: styles$5.citationFootnotes, children: citationFootnoteRenderModel.footnotes.map((footnote) => (jsxs("div", { className: styles$5.citationFootnoteItem, children: [jsx("span", { className: styles$5.citationFootnoteNumber, children: footnote.number }), jsx(SourceChip, { citation: footnote.citation, onClick: onCitationClick, isCitationIdVisible: false })] }, `citation-footnote-${footnote.number}-${footnote.citation.source}`))) })), structuredSourceCitations.length > 0 && (jsx("div", { className: styles$5.sourceCitations, children: structuredSourceCitations.map((citation, index) => (jsx(SourceChip, { citation: citation, onClick: onCitationClick, isCitationIdVisible: false }, `message-source-${citation.source}-${citation.id}-${index}`))) })), transitiveCitations.length > 0 && (jsx("div", { className: styles$5.sourceCitations, children: transitiveCitations.map((citation, index) => (jsx(SourceChip, { citation: citation, suffix: `by ${citation.origin.label}`, onClick: onCitationClick }, `team-source-${citation.source}-${index}`))) })), shouldShowButtons && (jsx("div", { className: styles$5.messageButtons, children: renderableButtons.map(({ button, buttonIndex }) => (jsx("button", { type: "button", className: classNames(styles$5.messageButton, button.type === 'action' && styles$5.actionMessageButton), onClick: (event) => {
48659
50045
  event.stopPropagation();
48660
50046
  if (button.type === 'message') {
48661
50047
  const quickMessageHandler = onQuickMessageButton || onMessage;
@@ -48707,6 +50093,9 @@ const ChatMessageItem = memo(
48707
50093
  if (prev.message.citations !== next.message.citations) {
48708
50094
  return false;
48709
50095
  }
50096
+ if (prev.message.sources !== next.message.sources) {
50097
+ return false;
50098
+ }
48710
50099
  if (JSON.stringify(prev.message.attachments) !== JSON.stringify(next.message.attachments)) {
48711
50100
  return false;
48712
50101
  }
@@ -48821,68 +50210,201 @@ function resolveRenderedMessageKey(message) {
48821
50210
  return message.clientMessageId || message.id || message.content;
48822
50211
  }
48823
50212
 
50213
+ /**
50214
+ * Supported star values rendered by the rating UI.
50215
+ *
50216
+ * @private component of `<ChatRatingModal/>`
50217
+ */
50218
+ const STAR_RATINGS = [1, 2, 3, 4, 5];
50219
+ /**
50220
+ * Resolves the key used to persist message rating state.
50221
+ *
50222
+ * @private function of `<ChatRatingModal/>`
50223
+ */
50224
+ function resolveChatRatingMessageKey(message) {
50225
+ return message.id || message.content /* <-[??] */;
50226
+ }
50227
+ /**
50228
+ * Returns whether the modal should render the report-issue flow.
50229
+ *
50230
+ * @private function of `<ChatRatingModal/>`
50231
+ */
50232
+ function isReportIssueFeedbackMode(feedbackMode) {
50233
+ return feedbackMode === 'report_issue';
50234
+ }
50235
+ /**
50236
+ * Finds the previous user question from postprocessed messages when it directly precedes the selected response.
50237
+ *
50238
+ * @private function of `<ChatRatingModal/>`
50239
+ */
50240
+ function resolveUserQuestionFromPostprocessedMessages(postprocessedMessages, selectedMessage) {
50241
+ const selectedMessageIndex = postprocessedMessages.findIndex((message) => message.id === selectedMessage.id);
50242
+ if (selectedMessageIndex <= 0) {
50243
+ return null;
50244
+ }
50245
+ const previousMessage = postprocessedMessages[selectedMessageIndex - 1];
50246
+ if ((previousMessage === null || previousMessage === void 0 ? void 0 : previousMessage.sender) !== 'USER') {
50247
+ return null;
50248
+ }
50249
+ return previousMessage.content;
50250
+ }
50251
+ /**
50252
+ * Finds the nearest earlier user question in the original message list.
50253
+ *
50254
+ * @private function of `<ChatRatingModal/>`
50255
+ */
50256
+ function resolveUserQuestionFromMessages(messages, selectedMessage) {
50257
+ const selectedMessageIndex = messages.findIndex((message) => message.id === selectedMessage.id);
50258
+ for (let index = selectedMessageIndex - 1; index >= 0; index--) {
50259
+ const currentMessage = messages[index];
50260
+ if ((currentMessage === null || currentMessage === void 0 ? void 0 : currentMessage.sender) === 'USER') {
50261
+ return currentMessage.content;
50262
+ }
50263
+ }
50264
+ return null;
50265
+ }
50266
+ /**
50267
+ * Resolves the user question shown above the feedback fields.
50268
+ *
50269
+ * @private function of `<ChatRatingModal/>`
50270
+ */
50271
+ function resolveChatRatingUserQuestion(params) {
50272
+ const { postprocessedMessages, messages, selectedMessage } = params;
50273
+ return (resolveUserQuestionFromPostprocessedMessages(postprocessedMessages, selectedMessage) ||
50274
+ resolveUserQuestionFromMessages(messages, selectedMessage) ||
50275
+ '');
50276
+ }
50277
+ /**
50278
+ * Builds the localized text rendered by the modal.
50279
+ *
50280
+ * @private function of `<ChatRatingModal/>`
50281
+ */
50282
+ function createChatRatingModalCopy(params) {
50283
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
50284
+ const { feedbackMode, feedbackTranslations } = params;
50285
+ const isReportIssueMode = isReportIssueFeedbackMode(feedbackMode);
50286
+ return {
50287
+ title: isReportIssueMode
50288
+ ? (_a = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.reportIssueModalTitle) !== null && _a !== void 0 ? _a : 'Report issue'
50289
+ : (_b = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.rateResponseModalTitle) !== null && _b !== void 0 ? _b : 'Rate this response',
50290
+ userQuestionLabel: (_c = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.userQuestionLabel) !== null && _c !== void 0 ? _c : 'Your question:',
50291
+ expectedAnswerLabel: isReportIssueMode
50292
+ ? (_d = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.reportIssueExpectedAnswerLabel) !== null && _d !== void 0 ? _d : 'What should the answer include?'
50293
+ : (_e = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.expectedAnswerLabel) !== null && _e !== void 0 ? _e : 'Expected answer:',
50294
+ expectedAnswerPlaceholder: (_f = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.expectedAnswerPlaceholder) !== null && _f !== void 0 ? _f : 'Expected answer (optional)',
50295
+ noteLabel: isReportIssueMode
50296
+ ? (_g = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.reportIssueDetailsLabel) !== null && _g !== void 0 ? _g : 'Issue details:'
50297
+ : (_h = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.noteLabel) !== null && _h !== void 0 ? _h : 'Note:',
50298
+ notePlaceholder: isReportIssueMode
50299
+ ? (_j = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.reportIssueDetailsPlaceholder) !== null && _j !== void 0 ? _j : 'Describe what went wrong (optional)'
50300
+ : (_k = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.notePlaceholder) !== null && _k !== void 0 ? _k : 'Add a note (optional)',
50301
+ cancelLabel: (feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.cancelLabel) || 'Cancel',
50302
+ submitLabel: isReportIssueMode
50303
+ ? (_l = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.reportIssueSubmitLabel) !== null && _l !== void 0 ? _l : 'Report issue'
50304
+ : (_m = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.submitLabel) !== null && _m !== void 0 ? _m : 'Submit',
50305
+ };
50306
+ }
50307
+ /**
50308
+ * Resolves the active rating, preferring hover preview over persisted value.
50309
+ *
50310
+ * @private function of `<ChatRatingModal/>`
50311
+ */
50312
+ function resolveSelectedChatRating(params) {
50313
+ const { hoveredRating, messageRatings, selectedMessage } = params;
50314
+ return hoveredRating || messageRatings.get(resolveChatRatingMessageKey(selectedMessage)) || 0;
50315
+ }
50316
+ /**
50317
+ * Resolves the color for one star in the rating picker.
50318
+ *
50319
+ * @private function of `<ChatRatingModal/>`
50320
+ */
50321
+ function resolveChatRatingStarColor(star, selectedRating, mode) {
50322
+ if (star <= selectedRating) {
50323
+ return '#FFD700';
50324
+ }
50325
+ return mode === 'LIGHT' ? '#ccc' : '#555';
50326
+ }
50327
+ /**
50328
+ * Returns the placeholder used in the expected-answer field.
50329
+ *
50330
+ * @private function of `<ChatRatingModal/>`
50331
+ */
50332
+ function resolveExpectedAnswerPlaceholder(selectedMessage, modalCopy) {
50333
+ return selectedMessage.content || modalCopy.expectedAnswerPlaceholder;
50334
+ }
50335
+ /**
50336
+ * Closes the modal when the mobile backdrop itself is clicked.
50337
+ *
50338
+ * @private function of `<ChatRatingModal/>`
50339
+ */
50340
+ function createChatRatingBackdropClickHandler(params) {
50341
+ const { isMobile, onClose } = params;
50342
+ return (event) => {
50343
+ if (event.target === event.currentTarget && isMobile) {
50344
+ onClose();
50345
+ }
50346
+ };
50347
+ }
50348
+ /**
50349
+ * Renders the optional star selector for the rating flow.
50350
+ *
50351
+ * @private component of `<ChatRatingModal/>`
50352
+ */
50353
+ function ChatRatingModalStarsSection(props) {
50354
+ const { feedbackMode, hoveredRating, messageRatings, mode, selectedMessage, setHoveredRating, setMessageRatings } = props;
50355
+ if (isReportIssueFeedbackMode(feedbackMode)) {
50356
+ return null;
50357
+ }
50358
+ const selectedRating = resolveSelectedChatRating({
50359
+ hoveredRating,
50360
+ messageRatings,
50361
+ selectedMessage,
50362
+ });
50363
+ return (jsx("div", { className: styles$5.stars, children: STAR_RATINGS.map((star) => (jsx("span", { onClick: () => setMessageRatings((previousRatings) => {
50364
+ const nextRatings = new Map(previousRatings);
50365
+ nextRatings.set(resolveChatRatingMessageKey(selectedMessage), star);
50366
+ return nextRatings;
50367
+ }), onMouseEnter: () => setHoveredRating(star), onMouseLeave: () => setHoveredRating(0), className: classNames(styles$5.ratingModalStar), style: {
50368
+ color: resolveChatRatingStarColor(star, selectedRating, mode),
50369
+ }, children: "\u2B50" }, star))) }));
50370
+ }
50371
+ /**
50372
+ * Renders the footer action buttons for the rating modal.
50373
+ *
50374
+ * @private component of `<ChatRatingModal/>`
50375
+ */
50376
+ function ChatRatingModalActions(props) {
50377
+ const { cancelLabel, onClose, submitLabel, submitRating } = props;
50378
+ return (jsxs("div", { className: styles$5.ratingActions, children: [jsx("button", { onClick: onClose, children: cancelLabel }), jsx("button", { onClick: submitRating, children: submitLabel })] }));
50379
+ }
48824
50380
  /**
48825
50381
  * Modal that captures per-message rating feedback.
48826
50382
  *
48827
50383
  * @private component of `<Chat/>`
48828
50384
  */
48829
50385
  function ChatRatingModal(props) {
48830
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
48831
50386
  const { isOpen, selectedMessage, postprocessedMessages, messages, hoveredRating, messageRatings, textRating, feedbackMode, feedbackTranslations, mode, isMobile, onClose, setHoveredRating, setMessageRatings, setSelectedMessage, setTextRating, submitRating, } = props;
48832
50387
  if (!isOpen || !selectedMessage) {
48833
50388
  return null;
48834
50389
  }
48835
- const isReportIssueMode = feedbackMode === 'report_issue';
48836
- const userQuestion = (() => {
48837
- var _a, _b;
48838
- const idx = postprocessedMessages.findIndex((message) => message.id === selectedMessage.id);
48839
- if (idx > 0) {
48840
- const prev = postprocessedMessages[idx - 1];
48841
- if ((prev === null || prev === void 0 ? void 0 : prev.sender) === 'USER') {
48842
- return prev.content;
48843
- }
48844
- }
48845
- for (let i = messages.findIndex((message) => message.id === selectedMessage.id) - 1; i >= 0; i--) {
48846
- if (((_a = messages[i]) === null || _a === void 0 ? void 0 : _a.sender) === 'USER') {
48847
- return ((_b = messages[i]) === null || _b === void 0 ? void 0 : _b.content) || '';
48848
- }
48849
- }
48850
- return '';
48851
- })();
48852
- return (jsx("div", { className: styles$5.ratingModal, "data-chat-modal": "rating", "data-chat-theme": mode.toLowerCase(), onClick: (event) => {
48853
- if (event.target === event.currentTarget && isMobile) {
48854
- onClose();
48855
- }
48856
- }, children: jsxs("div", { className: styles$5.ratingModalContent, children: [jsx("h3", { children: isReportIssueMode
48857
- ? (_a = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.reportIssueModalTitle) !== null && _a !== void 0 ? _a : 'Report issue'
48858
- : (_b = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.rateResponseModalTitle) !== null && _b !== void 0 ? _b : 'Rate this response' }), !isReportIssueMode && (jsx("div", { className: styles$5.stars, children: [1, 2, 3, 4, 5].map((star) => (jsx("span", { onClick: () => setMessageRatings((previousRatings) => {
48859
- const nextRatings = new Map(previousRatings);
48860
- nextRatings.set(selectedMessage.id || selectedMessage.content /* <-[??] */, star);
48861
- return nextRatings;
48862
- }), onMouseEnter: () => setHoveredRating(star), onMouseLeave: () => setHoveredRating(0), className: classNames(styles$5.ratingModalStar), style: {
48863
- color: star <=
48864
- (hoveredRating ||
48865
- messageRatings.get(selectedMessage.id || selectedMessage.content /* <-[??] */) ||
48866
- 0)
48867
- ? '#FFD700'
48868
- : mode === 'LIGHT'
48869
- ? '#ccc'
48870
- : '#555',
48871
- }, children: "\u2B50" }, star))) })), (_c = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.userQuestionLabel) !== null && _c !== void 0 ? _c : 'Your question:', jsx("textarea", { readOnly: true, value: userQuestion, className: styles$5.ratingInput }), isReportIssueMode
48872
- ? (_d = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.reportIssueExpectedAnswerLabel) !== null && _d !== void 0 ? _d : 'What should the answer include?'
48873
- : (_e = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.expectedAnswerLabel) !== null && _e !== void 0 ? _e : 'Expected answer:', jsx("textarea", { placeholder: selectedMessage.content ||
48874
- (feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.expectedAnswerPlaceholder) ||
48875
- 'Expected answer (optional)', defaultValue: selectedMessage.expectedAnswer || selectedMessage.content, onChange: (event) => {
50390
+ const modalCopy = createChatRatingModalCopy({
50391
+ feedbackMode,
50392
+ feedbackTranslations,
50393
+ });
50394
+ const userQuestion = resolveChatRatingUserQuestion({
50395
+ postprocessedMessages,
50396
+ messages,
50397
+ selectedMessage,
50398
+ });
50399
+ const handleBackdropClick = createChatRatingBackdropClickHandler({
50400
+ isMobile,
50401
+ onClose,
50402
+ });
50403
+ return (jsx("div", { className: styles$5.ratingModal, "data-chat-modal": "rating", "data-chat-theme": mode.toLowerCase(), onClick: handleBackdropClick, children: jsxs("div", { className: styles$5.ratingModalContent, children: [jsx("h3", { children: modalCopy.title }), jsx(ChatRatingModalStarsSection, { feedbackMode: feedbackMode, hoveredRating: hoveredRating, messageRatings: messageRatings, mode: mode, selectedMessage: selectedMessage, setHoveredRating: setHoveredRating, setMessageRatings: setMessageRatings }), modalCopy.userQuestionLabel, jsx("textarea", { readOnly: true, value: userQuestion, className: styles$5.ratingInput }), modalCopy.expectedAnswerLabel, jsx("textarea", { placeholder: resolveExpectedAnswerPlaceholder(selectedMessage, modalCopy), defaultValue: selectedMessage.expectedAnswer || selectedMessage.content, onChange: (event) => {
48876
50404
  if (selectedMessage) {
48877
50405
  setSelectedMessage({ ...selectedMessage, expectedAnswer: event.target.value });
48878
50406
  }
48879
- }, className: styles$5.ratingInput }), isReportIssueMode
48880
- ? (_f = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.reportIssueDetailsLabel) !== null && _f !== void 0 ? _f : 'Issue details:'
48881
- : (_g = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.noteLabel) !== null && _g !== void 0 ? _g : 'Note:', jsx("textarea", { placeholder: isReportIssueMode
48882
- ? (_h = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.reportIssueDetailsPlaceholder) !== null && _h !== void 0 ? _h : 'Describe what went wrong (optional)'
48883
- : (_j = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.notePlaceholder) !== null && _j !== void 0 ? _j : 'Add a note (optional)', defaultValue: textRating, onChange: (event) => setTextRating(event.target.value), className: styles$5.ratingInput }), jsxs("div", { className: styles$5.ratingActions, children: [jsx("button", { onClick: onClose, children: (feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.cancelLabel) || 'Cancel' }), jsx("button", { onClick: submitRating, children: isReportIssueMode
48884
- ? (_k = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.reportIssueSubmitLabel) !== null && _k !== void 0 ? _k : 'Report issue'
48885
- : (_l = feedbackTranslations === null || feedbackTranslations === void 0 ? void 0 : feedbackTranslations.submitLabel) !== null && _l !== void 0 ? _l : 'Submit' })] })] }) }));
50407
+ }, className: styles$5.ratingInput }), modalCopy.noteLabel, jsx("textarea", { placeholder: modalCopy.notePlaceholder, defaultValue: textRating, onChange: (event) => setTextRating(event.target.value), className: styles$5.ratingInput }), jsx(ChatRatingModalActions, { cancelLabel: modalCopy.cancelLabel, onClose: onClose, submitLabel: modalCopy.submitLabel, submitRating: submitRating })] }) }));
48886
50408
  }
48887
50409
 
48888
50410
  /**
@@ -49988,7 +51510,7 @@ function renderSearchToolCallDetails(options) {
49988
51510
  const { results, rawText } = extractSearchResults(resultRaw);
49989
51511
  const hasResults = results.length > 0;
49990
51512
  const hasRawText = !hasResults && !!rawText && rawText.trim().length > 0;
49991
- return (jsxs(Fragment, { children: [jsxs("div", { className: styles$5.searchModalHeader, children: [jsx("span", { className: styles$5.searchModalIcon, children: "\uD83D\uDD0E" }), jsx("h3", { className: styles$5.searchModalQuery, children: args.query || args.searchText || 'Search Results' })] }), jsx("div", { className: styles$5.searchModalContent, children: hasResults ? (jsx("div", { className: styles$5.searchResultsList, children: results.map((item, index) => (jsxs("div", { className: styles$5.searchResultItem, children: [jsx("div", { className: styles$5.searchResultUrl, children: item.url && (jsx("a", { href: item.url, target: "_blank", rel: "noreferrer", children: item.url })) }), jsx("h4", { className: styles$5.searchResultTitle, children: item.url ? (jsx("a", { href: item.url, target: "_blank", rel: "noreferrer", children: item.title || 'Untitled' })) : (item.title || 'Untitled') }), jsx("p", { className: styles$5.searchResultSnippet, children: item.snippet || item.content || '' })] }, index))) })) : hasRawText ? (jsx(MarkdownContent, { className: styles$5.searchResultsRaw, content: rawText })) : toolCallState !== 'COMPLETE' ? (jsxs(Fragment, { children: [renderToolCallProgressPlaceholder({
51513
+ return (jsxs(Fragment, { children: [jsxs("div", { className: styles$5.searchModalHeader, children: [jsx("span", { className: styles$5.searchModalIcon, children: "\uD83D\uDD0E" }), jsx("h3", { className: styles$5.searchModalQuery, children: args.query || args.searchText || 'Search Results' })] }), jsx("div", { className: styles$5.searchModalContent, children: hasResults ? (jsx("div", { className: styles$5.searchResultsList, children: results.map((item, index) => (jsxs("div", { className: styles$5.searchResultItem, children: [jsx("div", { className: styles$5.searchResultUrl, children: item.url && (jsx("a", { href: item.url, target: "_blank", rel: "noreferrer", children: item.url })) }), jsx("h4", { className: styles$5.searchResultTitle, children: item.url ? (jsx("a", { href: item.url, target: "_blank", rel: "noreferrer", children: item.title || item.source || 'Untitled' })) : (item.title || item.source || 'Untitled') }), jsx("p", { className: styles$5.searchResultSnippet, children: item.snippet || item.content || item.excerpt || '' })] }, index))) })) : hasRawText ? (jsx(MarkdownContent, { className: styles$5.searchResultsRaw, content: rawText })) : toolCallState !== 'COMPLETE' ? (jsxs(Fragment, { children: [renderToolCallProgressPlaceholder({
49992
51514
  title: 'Search results pending',
49993
51515
  message: resolveToolCallProgressMessage(toolCall),
49994
51516
  }), jsxs("div", { className: styles$5.toolCallDetailsCard, children: [jsxs("div", { className: styles$5.toolCallDetailsCardRow, children: [jsx("strong", { children: "Query" }), jsx("span", { children: String(args.query || args.searchText || 'Search query is being prepared.') })] }), args.location && (jsxs("div", { className: styles$5.toolCallDetailsCardRow, children: [jsx("strong", { children: "Location" }), jsx("span", { children: String(args.location) })] })), args.engine && (jsxs("div", { className: styles$5.toolCallDetailsCardRow, children: [jsx("strong", { children: "Engine" }), jsx("span", { children: String(args.engine) })] }))] })] })) : (jsx("div", { className: styles$5.noResults, children: resultRaw ? 'No search results found.' : 'Search results are not available.' })) })] }));
@@ -50193,6 +51715,18 @@ const DEFAULT_TIMEOUT_SNOOZE_DURATION_MILLISECONDS = 5 * 60 * 1000;
50193
51715
  * @private function of ChatToolCallModal
50194
51716
  */
50195
51717
  function renderTimeoutToolCallDetails(options) {
51718
+ const viewModel = createTimeoutToolCallDetailsViewModel(options);
51719
+ return (jsxs(Fragment, { children: [jsxs("div", { className: styles$5.searchModalHeader, children: [jsx("span", { className: styles$5.searchModalIcon, children: "\u23F1\uFE0F" }), jsx("h3", { className: styles$5.searchModalQuery, children: viewModel.title })] }), jsxs("div", { className: styles$5.searchModalContent, children: [renderTimeoutToolCallClock(viewModel), renderTimeoutToolCallSummary(viewModel), renderTimeoutToolCallActions(viewModel)] })] }));
51720
+ }
51721
+ /**
51722
+ * Prepares the values rendered by the timeout detail view.
51723
+ *
51724
+ * @param options - Raw timeout tool call data.
51725
+ * @returns Derived timeout detail view model.
51726
+ *
51727
+ * @private function of ChatToolCallModal
51728
+ */
51729
+ function createTimeoutToolCallDetailsViewModel(options) {
50196
51730
  const { toolCallName, args, resultRaw, toolCallDate, onRequestAdvancedView, locale, chatUiTranslations } = options;
50197
51731
  const timeoutPresentation = resolveTimeoutToolCallPresentation({
50198
51732
  toolCallName,
@@ -50201,40 +51735,120 @@ function renderTimeoutToolCallDetails(options) {
50201
51735
  currentDate: new Date(),
50202
51736
  locale,
50203
51737
  });
50204
- const clockDate = (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.dueAtDate) || toolCallDate;
50205
- const isValidClockDate = !!clockDate && !Number.isNaN(clockDate.getTime());
50206
- const title = (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.action) === 'cancel'
50207
- ? timeoutPresentation.status === 'cancelled'
50208
- ? (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutCancelledTitle) || 'Timeout cancelled'
50209
- : (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutUpdateTitle) || 'Timeout update'
50210
- : (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutTitle) || 'Timeout scheduled';
50211
- const primarySentence = timeoutPresentation
50212
- ? buildTimeoutToolPrimarySentence(timeoutPresentation, chatUiTranslations)
50213
- : (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutLoadingMessage) || 'Timeout details are still loading.';
50214
- const scheduleSentence = timeoutPresentation
50215
- ? buildTimeoutToolScheduleSentence(timeoutPresentation, chatUiTranslations)
50216
- : null;
50217
- const cancelCommand = (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.timeoutId) && timeoutPresentation.action !== 'cancel'
50218
- ? createCancelTimeoutQuickActionCommand(timeoutPresentation.timeoutId)
50219
- : null;
50220
- const snoozeCommand = createSnoozeTimeoutQuickActionCommand((timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.message) || undefined);
50221
- const timezoneLine = (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.localTimezone)
50222
- ? `${(chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutTimezoneLabel) || 'Timezone:'} ${timeoutPresentation.localTimezone}`
50223
- : null;
50224
- return (jsxs(Fragment, { children: [jsxs("div", { className: styles$5.searchModalHeader, children: [jsx("span", { className: styles$5.searchModalIcon, children: "\u23F1\uFE0F" }), jsx("h3", { className: styles$5.searchModalQuery, children: title })] }), jsxs("div", { className: styles$5.searchModalContent, children: [isValidClockDate && clockDate ? (renderToolCallClockPanel({
50225
- date: clockDate,
50226
- relativeLabel: (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.relativeDueLabel) || null,
50227
- timezoneLabel: timezoneLine,
50228
- locale,
50229
- })) : (jsx("p", { className: styles$5.toolCallEmpty, children: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutUnavailableMessage) || 'Scheduled time is unavailable.' })), jsxs("div", { className: styles$5.timeoutToolSummary, children: [jsx("p", { className: styles$5.timeoutToolPrimarySentence, children: primarySentence }), scheduleSentence && jsx("p", { className: styles$5.timeoutToolSecondarySentence, children: scheduleSentence }), (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.localDueDateLabel) && (jsxs("p", { className: styles$5.timeoutToolSecondarySentence, children: [(chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutDateLabel) || 'Date:', ' ', timeoutPresentation.localDueDateLabel] })), (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.message) && (jsxs("p", { className: styles$5.timeoutToolSecondarySentence, children: [(chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutMessageLabel) || 'Message:', ' ', timeoutPresentation.message] }))] }), jsxs("div", { className: styles$5.timeoutToolActionRow, role: "group", "aria-label": (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutActionGroupLabel) || 'Timeout quick actions', children: [jsx("button", { type: "button", className: styles$5.timeoutToolActionButton, onClick: () => {
50230
- if (!cancelCommand) {
50231
- return;
50232
- }
50233
- copyTimeoutQuickActionCommand(cancelCommand);
50234
- }, disabled: !cancelCommand, "aria-label": (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutCancelAriaLabel) || 'Cancel timeout', children: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutCancelButton) || 'Cancel' }), jsx("button", { type: "button", className: styles$5.timeoutToolActionButton, onClick: () => {
50235
- copyTimeoutQuickActionCommand(snoozeCommand);
50236
- }, "aria-label": (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutSnoozeAriaLabel) || 'Snooze timeout', children: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutSnoozeButton) || 'Snooze' }), onRequestAdvancedView && (jsx("button", { type: "button", className: styles$5.timeoutToolActionButton, onClick: onRequestAdvancedView, "aria-label": (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutViewAdvancedAriaLabel) ||
50237
- 'View advanced timeout details', children: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutViewAdvancedButton) || 'View advanced' }))] })] })] }));
51738
+ const labels = resolveTimeoutToolCallDetailLabels(chatUiTranslations);
51739
+ return {
51740
+ labels,
51741
+ locale,
51742
+ onRequestAdvancedView,
51743
+ title: resolveTimeoutToolCallTitle(timeoutPresentation, labels),
51744
+ clockDate: (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.dueAtDate) || toolCallDate,
51745
+ primarySentence: timeoutPresentation
51746
+ ? buildTimeoutToolPrimarySentence(timeoutPresentation, chatUiTranslations)
51747
+ : labels.loadingMessage,
51748
+ scheduleSentence: timeoutPresentation
51749
+ ? buildTimeoutToolScheduleSentence(timeoutPresentation, chatUiTranslations)
51750
+ : null,
51751
+ relativeDueLabel: (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.relativeDueLabel) || null,
51752
+ timezoneLine: (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.localTimezone)
51753
+ ? `${labels.timezoneLabel} ${timeoutPresentation.localTimezone}`
51754
+ : null,
51755
+ localDueDateLabel: (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.localDueDateLabel) || null,
51756
+ message: (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.message) || null,
51757
+ cancelCommand: (timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.timeoutId) && timeoutPresentation.action !== 'cancel'
51758
+ ? createCancelTimeoutQuickActionCommand(timeoutPresentation.timeoutId)
51759
+ : null,
51760
+ snoozeCommand: createSnoozeTimeoutQuickActionCommand((timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.message) || undefined),
51761
+ };
51762
+ }
51763
+ /**
51764
+ * Resolves the timeout detail title for the current presentation state.
51765
+ *
51766
+ * @param timeoutPresentation - Friendly timeout presentation data.
51767
+ * @param labels - Localized labels with fallbacks.
51768
+ * @returns One timeout modal title.
51769
+ *
51770
+ * @private function of ChatToolCallModal
51771
+ */
51772
+ function resolveTimeoutToolCallTitle(timeoutPresentation, labels) {
51773
+ if ((timeoutPresentation === null || timeoutPresentation === void 0 ? void 0 : timeoutPresentation.action) !== 'cancel') {
51774
+ return labels.titleScheduled;
51775
+ }
51776
+ return timeoutPresentation.status === 'cancelled' ? labels.titleCancelled : labels.titleUpdated;
51777
+ }
51778
+ /**
51779
+ * Resolves timeout-detail translations with stable fallback strings.
51780
+ *
51781
+ * @param chatUiTranslations - Optional translation overrides.
51782
+ * @returns Complete timeout detail labels.
51783
+ *
51784
+ * @private function of ChatToolCallModal
51785
+ */
51786
+ function resolveTimeoutToolCallDetailLabels(chatUiTranslations) {
51787
+ return {
51788
+ titleScheduled: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutTitle) || 'Timeout scheduled',
51789
+ titleCancelled: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutCancelledTitle) || 'Timeout cancelled',
51790
+ titleUpdated: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutUpdateTitle) || 'Timeout update',
51791
+ loadingMessage: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutLoadingMessage) || 'Timeout details are still loading.',
51792
+ unavailableMessage: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutUnavailableMessage) || 'Scheduled time is unavailable.',
51793
+ dateLabel: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutDateLabel) || 'Date:',
51794
+ messageLabel: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutMessageLabel) || 'Message:',
51795
+ timezoneLabel: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutTimezoneLabel) || 'Timezone:',
51796
+ actionGroupLabel: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutActionGroupLabel) || 'Timeout quick actions',
51797
+ cancelAriaLabel: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutCancelAriaLabel) || 'Cancel timeout',
51798
+ cancelButton: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutCancelButton) || 'Cancel',
51799
+ snoozeAriaLabel: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutSnoozeAriaLabel) || 'Snooze timeout',
51800
+ snoozeButton: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutSnoozeButton) || 'Snooze',
51801
+ viewAdvancedAriaLabel: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutViewAdvancedAriaLabel) || 'View advanced timeout details',
51802
+ viewAdvancedButton: (chatUiTranslations === null || chatUiTranslations === void 0 ? void 0 : chatUiTranslations.toolCallTimeoutViewAdvancedButton) || 'View advanced',
51803
+ };
51804
+ }
51805
+ /**
51806
+ * Renders the timeout clock panel or its unavailable fallback.
51807
+ *
51808
+ * @param viewModel - Prepared timeout detail state.
51809
+ * @returns Clock section element.
51810
+ *
51811
+ * @private function of ChatToolCallModal
51812
+ */
51813
+ function renderTimeoutToolCallClock(viewModel) {
51814
+ if (viewModel.clockDate && !Number.isNaN(viewModel.clockDate.getTime())) {
51815
+ return renderToolCallClockPanel({
51816
+ date: viewModel.clockDate,
51817
+ relativeLabel: viewModel.relativeDueLabel,
51818
+ timezoneLabel: viewModel.timezoneLine,
51819
+ locale: viewModel.locale,
51820
+ });
51821
+ }
51822
+ return jsx("p", { className: styles$5.toolCallEmpty, children: viewModel.labels.unavailableMessage });
51823
+ }
51824
+ /**
51825
+ * Renders the timeout summary sentences and optional metadata lines.
51826
+ *
51827
+ * @param viewModel - Prepared timeout detail state.
51828
+ * @returns Summary section element.
51829
+ *
51830
+ * @private function of ChatToolCallModal
51831
+ */
51832
+ function renderTimeoutToolCallSummary(viewModel) {
51833
+ return (jsxs("div", { className: styles$5.timeoutToolSummary, children: [jsx("p", { className: styles$5.timeoutToolPrimarySentence, children: viewModel.primarySentence }), viewModel.scheduleSentence && (jsx("p", { className: styles$5.timeoutToolSecondarySentence, children: viewModel.scheduleSentence })), viewModel.localDueDateLabel && (jsxs("p", { className: styles$5.timeoutToolSecondarySentence, children: [viewModel.labels.dateLabel, " ", viewModel.localDueDateLabel] })), viewModel.message && (jsxs("p", { className: styles$5.timeoutToolSecondarySentence, children: [viewModel.labels.messageLabel, " ", viewModel.message] }))] }));
51834
+ }
51835
+ /**
51836
+ * Renders timeout quick-action buttons.
51837
+ *
51838
+ * @param viewModel - Prepared timeout detail state.
51839
+ * @returns Quick actions element.
51840
+ *
51841
+ * @private function of ChatToolCallModal
51842
+ */
51843
+ function renderTimeoutToolCallActions(viewModel) {
51844
+ return (jsxs("div", { className: styles$5.timeoutToolActionRow, role: "group", "aria-label": viewModel.labels.actionGroupLabel, children: [jsx("button", { type: "button", className: styles$5.timeoutToolActionButton, onClick: () => {
51845
+ if (!viewModel.cancelCommand) {
51846
+ return;
51847
+ }
51848
+ copyTimeoutQuickActionCommand(viewModel.cancelCommand);
51849
+ }, disabled: !viewModel.cancelCommand, "aria-label": viewModel.labels.cancelAriaLabel, children: viewModel.labels.cancelButton }), jsx("button", { type: "button", className: styles$5.timeoutToolActionButton, onClick: () => {
51850
+ copyTimeoutQuickActionCommand(viewModel.snoozeCommand);
51851
+ }, "aria-label": viewModel.labels.snoozeAriaLabel, children: viewModel.labels.snoozeButton }), viewModel.onRequestAdvancedView && (jsx("button", { type: "button", className: styles$5.timeoutToolActionButton, onClick: viewModel.onRequestAdvancedView, "aria-label": viewModel.labels.viewAdvancedAriaLabel, children: viewModel.labels.viewAdvancedButton }))] }));
50238
51852
  }
50239
51853
  /**
50240
51854
  * Copies one quick action command to clipboard when supported.
@@ -50476,6 +52090,7 @@ function renderGenericToolCallDetails(options) {
50476
52090
  function isSearchToolCallName(toolName) {
50477
52091
  return (toolName === 'web_search' ||
50478
52092
  toolName === 'deep_search' ||
52093
+ toolName === 'knowledge_search' ||
50479
52094
  toolName === 'useSearchEngine' ||
50480
52095
  toolName === 'search');
50481
52096
  }
@@ -50822,6 +52437,279 @@ const PauseIcon = ({ size = 16 }) => (jsx("svg", { width: size, height: size, vi
50822
52437
  */
50823
52438
  const PlayIcon = ({ size = 16 }) => (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "currentColor", role: "img", "aria-label": "Play", children: jsx("path", { d: "M8 5v14l11-7L8 5z" }) }));
50824
52439
 
52440
+ /**
52441
+ * Resolves one delay value, including random ranges.
52442
+ *
52443
+ * @private function of `MockedChat`
52444
+ */
52445
+ function getDelay(val, fallback) {
52446
+ if (Array.isArray(val) && val.length === 2) {
52447
+ const [min, max] = val;
52448
+ return Math.floor(Math.random() * (max - min + 1)) + min;
52449
+ }
52450
+ if (typeof val === 'number') {
52451
+ return val;
52452
+ }
52453
+ return fallback;
52454
+ }
52455
+ /**
52456
+ * Normalizes message offsets into a monotonic deterministic timeline.
52457
+ *
52458
+ * @private function of `MockedChat`
52459
+ */
52460
+ function normalizeMessageOffsets(messageOffsetsMs, originalMessages) {
52461
+ if (!messageOffsetsMs || messageOffsetsMs.length === 0) {
52462
+ return null;
52463
+ }
52464
+ let previousOffset = 0;
52465
+ return originalMessages.map((_message, messageIndex) => {
52466
+ const rawOffset = messageOffsetsMs[messageIndex];
52467
+ const nextOffset = Number.isFinite(rawOffset) ? Math.max(0, Math.floor(rawOffset || 0)) : previousOffset;
52468
+ previousOffset = Math.max(previousOffset, nextOffset);
52469
+ return previousOffset;
52470
+ });
52471
+ }
52472
+ /**
52473
+ * Resolves the deterministic wait before one message when fixed offsets are supplied.
52474
+ *
52475
+ * @private function of `MockedChat`
52476
+ */
52477
+ function resolveOffsetDelayBeforeMessage(normalizedMessageOffsetsMs, messageIndex) {
52478
+ if (!normalizedMessageOffsetsMs) {
52479
+ return 0;
52480
+ }
52481
+ const currentOffset = normalizedMessageOffsetsMs[messageIndex] || 0;
52482
+ const previousOffset = messageIndex > 0 ? normalizedMessageOffsetsMs[messageIndex - 1] || 0 : 0;
52483
+ return Math.max(0, currentOffset - previousOffset);
52484
+ }
52485
+ /**
52486
+ * Creates the message shape rendered while the mocked transcript is being simulated.
52487
+ *
52488
+ * @private function of `MockedChat`
52489
+ */
52490
+ function createDisplayedMockedChatMessage(originalMessage, content, isComplete) {
52491
+ return {
52492
+ id: originalMessage.id,
52493
+ createdAt: originalMessage.createdAt,
52494
+ sender: originalMessage.sender,
52495
+ content,
52496
+ isComplete,
52497
+ expectedAnswer: originalMessage.expectedAnswer,
52498
+ isVoiceCall: originalMessage.isVoiceCall,
52499
+ };
52500
+ }
52501
+ /**
52502
+ * Appends one new rendered message to the mocked transcript.
52503
+ *
52504
+ * @private function of `MockedChat`
52505
+ */
52506
+ function appendDisplayedMessage(setDisplayedMessages, nextMessage) {
52507
+ setDisplayedMessages((previousMessages) => [...previousMessages, nextMessage]);
52508
+ }
52509
+ /**
52510
+ * Replaces the most recently rendered message in the mocked transcript.
52511
+ *
52512
+ * @private function of `MockedChat`
52513
+ */
52514
+ function replaceLastDisplayedMessage(setDisplayedMessages, nextMessage) {
52515
+ setDisplayedMessages((previousMessages) => {
52516
+ const nextMessages = [...previousMessages];
52517
+ nextMessages[nextMessages.length - 1] = nextMessage;
52518
+ return nextMessages;
52519
+ });
52520
+ }
52521
+ /**
52522
+ * Resets all simulation-owned UI state before one playback run starts.
52523
+ *
52524
+ * @private function of `MockedChat`
52525
+ */
52526
+ function resetSimulationState(params) {
52527
+ const { setDisplayedMessages, setLocalAppendedMessages, setIsSimulationComplete } = params;
52528
+ setDisplayedMessages([]);
52529
+ setLocalAppendedMessages([]);
52530
+ setIsSimulationComplete(false);
52531
+ }
52532
+ /**
52533
+ * Finalizes the playback run and notifies the caller.
52534
+ *
52535
+ * @private function of `MockedChat`
52536
+ */
52537
+ function completeSimulation(params) {
52538
+ var _a;
52539
+ const { setIsSimulationComplete, onSimulationCompleteRef } = params;
52540
+ setIsSimulationComplete(true);
52541
+ (_a = onSimulationCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onSimulationCompleteRef);
52542
+ }
52543
+ /**
52544
+ * Waits for one delay and reports whether the current playback was cancelled.
52545
+ *
52546
+ * @private function of `MockedChat`
52547
+ */
52548
+ async function waitForDelay(delayMs, isCancelledRef) {
52549
+ if (delayMs > 0) {
52550
+ await forTime(delayMs);
52551
+ }
52552
+ return isCancelledRef();
52553
+ }
52554
+ /**
52555
+ * Waits for a paused simulation to resume when a pause has been requested.
52556
+ *
52557
+ * @private function of `MockedChat`
52558
+ */
52559
+ async function waitForPauseIfRequested(params) {
52560
+ const { pauseRequestedRef, waitIfPaused, isCancelledRef } = params;
52561
+ if (!pauseRequestedRef.current) {
52562
+ return false;
52563
+ }
52564
+ await waitIfPaused(isCancelledRef);
52565
+ return isCancelledRef();
52566
+ }
52567
+ /**
52568
+ * Resolves the first wait before the simulated transcript starts rendering.
52569
+ *
52570
+ * @private function of `MockedChat`
52571
+ */
52572
+ function resolveInitialSimulationDelay(params) {
52573
+ const { delays, normalizedMessageOffsetsMs, firstMessageIndex } = params;
52574
+ if (normalizedMessageOffsetsMs) {
52575
+ return resolveOffsetDelayBeforeMessage(normalizedMessageOffsetsMs, firstMessageIndex);
52576
+ }
52577
+ return getDelay(delays.beforeFirstMessage, 1000);
52578
+ }
52579
+ /**
52580
+ * Returns true when the next inter-message wait should use a longer pause.
52581
+ *
52582
+ * @private function of `MockedChat`
52583
+ */
52584
+ function shouldUseLongPause(params) {
52585
+ const { delays, originalMessages, messageIndex } = params;
52586
+ return (!!delays.longPauseChance &&
52587
+ Math.random() < delays.longPauseChance &&
52588
+ messageIndex > 0 &&
52589
+ originalMessages[messageIndex].sender !== originalMessages[messageIndex - 1].sender);
52590
+ }
52591
+ /**
52592
+ * Resolves the wait between two rendered messages.
52593
+ *
52594
+ * @private function of `MockedChat`
52595
+ */
52596
+ function resolveInterMessageDelay(params) {
52597
+ const { delays, originalMessages, normalizedMessageOffsetsMs, messageIndex } = params;
52598
+ if (normalizedMessageOffsetsMs) {
52599
+ return resolveOffsetDelayBeforeMessage(normalizedMessageOffsetsMs, messageIndex);
52600
+ }
52601
+ if (shouldUseLongPause({
52602
+ delays,
52603
+ originalMessages,
52604
+ messageIndex,
52605
+ })) {
52606
+ return getDelay(delays.longPauseDuration, 2000);
52607
+ }
52608
+ return getDelay(delays.thinkingBetweenMessages, 2000);
52609
+ }
52610
+ /**
52611
+ * Types out one message word by word and marks it complete when finished.
52612
+ *
52613
+ * @private function of `MockedChat`
52614
+ */
52615
+ async function typeMockedChatMessage(params) {
52616
+ const { currentMessage, delays, normalizedMessageOffsetsMs, setDisplayedMessages, isCancelledRef } = params;
52617
+ const incompleteMessage = createDisplayedMockedChatMessage(currentMessage, '', false);
52618
+ appendDisplayedMessage(setDisplayedMessages, incompleteMessage);
52619
+ const words = currentMessage.content.split(' ');
52620
+ let currentContent = '';
52621
+ for (let wordIndex = 0; wordIndex < words.length; wordIndex++) {
52622
+ if (isCancelledRef()) {
52623
+ return true;
52624
+ }
52625
+ const word = words[wordIndex];
52626
+ currentContent += (wordIndex > 0 ? ' ' : '') + word;
52627
+ replaceLastDisplayedMessage(setDisplayedMessages, createDisplayedMockedChatMessage(currentMessage, currentContent, false));
52628
+ const wordDelayMs = getDelay(delays.waitAfterWord, 100) + getDelay(delays.extraWordDelay, 50);
52629
+ if (await waitForDelay(wordDelayMs, isCancelledRef)) {
52630
+ return true;
52631
+ }
52632
+ }
52633
+ replaceLastDisplayedMessage(setDisplayedMessages, createDisplayedMockedChatMessage(currentMessage, currentMessage.content, true));
52634
+ if (!normalizedMessageOffsetsMs && (await waitForDelay(200, isCancelledRef))) {
52635
+ return true;
52636
+ }
52637
+ return false;
52638
+ }
52639
+ /**
52640
+ * Runs one full mocked-chat playback cycle.
52641
+ *
52642
+ * @private function of `MockedChat`
52643
+ */
52644
+ async function simulateMockedChatPlayback(props) {
52645
+ const { delays, originalMessages, normalizedMessageOffsetsMs, pauseRequestedRef, waitIfPaused, setDisplayedMessages, setLocalAppendedMessages, setIsSimulationComplete, onSimulationCompleteRef, isCancelledRef, } = props;
52646
+ resetSimulationState({
52647
+ setDisplayedMessages,
52648
+ setLocalAppendedMessages,
52649
+ setIsSimulationComplete,
52650
+ });
52651
+ if (originalMessages.length === 0) {
52652
+ completeSimulation({
52653
+ setIsSimulationComplete,
52654
+ onSimulationCompleteRef,
52655
+ });
52656
+ return;
52657
+ }
52658
+ const showIntermediateMessages = delays.showIntermediateMessages || 0;
52659
+ if (showIntermediateMessages > 0) {
52660
+ setDisplayedMessages(originalMessages.slice(0, showIntermediateMessages));
52661
+ }
52662
+ if (await waitForDelay(resolveInitialSimulationDelay({
52663
+ delays,
52664
+ normalizedMessageOffsetsMs,
52665
+ firstMessageIndex: showIntermediateMessages,
52666
+ }), isCancelledRef)) {
52667
+ return;
52668
+ }
52669
+ for (let messageIndex = showIntermediateMessages; messageIndex < originalMessages.length; messageIndex++) {
52670
+ if (await waitForPauseIfRequested({
52671
+ pauseRequestedRef,
52672
+ waitIfPaused,
52673
+ isCancelledRef,
52674
+ })) {
52675
+ return;
52676
+ }
52677
+ const currentMessage = originalMessages[messageIndex];
52678
+ if (!currentMessage) {
52679
+ continue;
52680
+ }
52681
+ if (messageIndex > showIntermediateMessages) {
52682
+ if (await waitForDelay(resolveInterMessageDelay({
52683
+ delays,
52684
+ originalMessages,
52685
+ normalizedMessageOffsetsMs,
52686
+ messageIndex,
52687
+ }), isCancelledRef)) {
52688
+ return;
52689
+ }
52690
+ if (await waitForPauseIfRequested({
52691
+ pauseRequestedRef,
52692
+ waitIfPaused,
52693
+ isCancelledRef,
52694
+ })) {
52695
+ return;
52696
+ }
52697
+ }
52698
+ if (await typeMockedChatMessage({
52699
+ currentMessage,
52700
+ delays,
52701
+ normalizedMessageOffsetsMs,
52702
+ setDisplayedMessages,
52703
+ isCancelledRef,
52704
+ })) {
52705
+ return;
52706
+ }
52707
+ }
52708
+ completeSimulation({
52709
+ setIsSimulationComplete,
52710
+ onSimulationCompleteRef,
52711
+ });
52712
+ }
50825
52713
  /**
50826
52714
  * MockedChat component that shows the same chat as Chat but emulates ongoing discussion
50827
52715
  * with realistic typing delays and thinking pauses.
@@ -50830,16 +52718,6 @@ const PlayIcon = ({ size = 16 }) => (jsx("svg", { width: size, height: size, vie
50830
52718
  */
50831
52719
  function MockedChat(props) {
50832
52720
  const { delayConfig, messages: originalMessages, isResettable = true, isPausable = true, messageOffsetsMs, appendMessagesLocallyOnSend = false, onSimulationComplete, isSaveButtonEnabled = true, ...chatProps } = props;
50833
- // Helper to get random delay from config
50834
- function getDelay(val, fallback) {
50835
- if (Array.isArray(val) && val.length === 2) {
50836
- const [min, max] = val;
50837
- return Math.floor(Math.random() * (max - min + 1)) + min;
50838
- }
50839
- if (typeof val === 'number')
50840
- return val;
50841
- return fallback;
50842
- }
50843
52721
  const delays = {
50844
52722
  ...MOCKED_CHAT_DELAY_CONFIGS.NORMAL_FLOW,
50845
52723
  ...delayConfig,
@@ -50847,18 +52725,7 @@ function MockedChat(props) {
50847
52725
  const [displayedMessages, setDisplayedMessages] = useState([]);
50848
52726
  const [localAppendedMessages, setLocalAppendedMessages] = useState([]);
50849
52727
  const [isSimulationComplete, setIsSimulationComplete] = useState(false);
50850
- const normalizedMessageOffsetsMs = useMemo(() => {
50851
- if (!messageOffsetsMs || messageOffsetsMs.length === 0) {
50852
- return null;
50853
- }
50854
- let previousOffset = 0;
50855
- return originalMessages.map((_message, messageIndex) => {
50856
- const rawOffset = messageOffsetsMs[messageIndex];
50857
- const nextOffset = Number.isFinite(rawOffset) ? Math.max(0, Math.floor(rawOffset || 0)) : previousOffset;
50858
- previousOffset = Math.max(previousOffset, nextOffset);
50859
- return previousOffset;
50860
- });
50861
- }, [messageOffsetsMs, originalMessages]);
52728
+ const normalizedMessageOffsetsMs = useMemo(() => normalizeMessageOffsets(messageOffsetsMs, originalMessages), [messageOffsetsMs, originalMessages]);
50862
52729
  // Playback state machine
50863
52730
  // RUNNING -> (user clicks Pause) -> PAUSING (finish current message) -> PAUSED
50864
52731
  // PAUSED -> (user clicks Resume) -> RUNNING
@@ -50879,193 +52746,48 @@ function MockedChat(props) {
50879
52746
  setIsSimulationComplete(false);
50880
52747
  setResetNonce((nonce) => nonce + 1);
50881
52748
  };
50882
- }, [resetNonce, isResettable]);
52749
+ }, [isResettable]);
50883
52750
  // Helper: Wait while paused (entered only between messages, never mid-typing)
50884
52751
  const waitIfPaused = async (isCancelledRef) => {
50885
- if (!pauseRequestedRef.current)
52752
+ if (!pauseRequestedRef.current) {
50886
52753
  return;
52754
+ }
50887
52755
  setPlaybackState('PAUSED');
50888
52756
  // Busy wait with small sleeps until resume
50889
52757
  while (pauseRequestedRef.current) {
50890
- if (isCancelledRef())
52758
+ if (isCancelledRef()) {
50891
52759
  return;
52760
+ }
50892
52761
  await forTime(100);
50893
52762
  }
50894
- // Resumed
50895
52763
  setPlaybackState('RUNNING');
50896
52764
  };
50897
52765
  const requestPause = () => {
50898
52766
  if (playbackState === 'RUNNING') {
50899
52767
  pauseRequestedRef.current = true;
50900
- // Will flip to PAUSING when current message completes
50901
52768
  setPlaybackState('PAUSING');
50902
52769
  }
50903
52770
  };
50904
52771
  const resume = () => {
50905
52772
  pauseRequestedRef.current = false;
50906
52773
  if (playbackState !== 'RUNNING') {
50907
- // Actual state will become RUNNING after loop exits waitIfPaused
50908
52774
  setPlaybackState('RUNNING');
50909
52775
  }
50910
52776
  };
50911
- /**
50912
- * Resolves deterministic waiting time before one message when fixed offsets are supplied.
50913
- */
50914
- const resolveOffsetDelayBeforeMessage = useCallback((messageIndex) => {
50915
- if (!normalizedMessageOffsetsMs) {
50916
- return 0;
50917
- }
50918
- const currentOffset = normalizedMessageOffsetsMs[messageIndex] || 0;
50919
- const previousOffset = messageIndex > 0 ? normalizedMessageOffsetsMs[messageIndex - 1] || 0 : 0;
50920
- return Math.max(0, currentOffset - previousOffset);
50921
- }, [normalizedMessageOffsetsMs]);
50922
52777
  useEffect(() => {
50923
52778
  let isCancelled = false;
50924
- const simulateChat = async () => {
50925
- var _a, _b;
50926
- // Reset state
50927
- setDisplayedMessages([]);
50928
- setLocalAppendedMessages([]);
50929
- setIsSimulationComplete(false);
50930
- if (originalMessages.length === 0) {
50931
- setIsSimulationComplete(true);
50932
- (_a = onSimulationCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onSimulationCompleteRef);
50933
- return;
50934
- }
50935
- // Show intermediate messages immediately
50936
- const showIntermediateMessages = delays.showIntermediateMessages || 0;
50937
- if (showIntermediateMessages > 0) {
50938
- setDisplayedMessages(originalMessages.slice(0, showIntermediateMessages));
50939
- }
50940
- // Wait before first rendered message.
50941
- const firstMessageIndex = showIntermediateMessages;
50942
- const initialDelay = normalizedMessageOffsetsMs
50943
- ? resolveOffsetDelayBeforeMessage(firstMessageIndex)
50944
- : getDelay(delays.beforeFirstMessage, 1000);
50945
- if (initialDelay > 0) {
50946
- await forTime(initialDelay);
50947
- }
50948
- if (isCancelled)
50949
- return;
50950
- for (let i = showIntermediateMessages; i < originalMessages.length; i++) {
50951
- // If a pause was requested earlier, we only pause between messages
50952
- if (pauseRequestedRef.current) {
50953
- await waitIfPaused(() => isCancelled);
50954
- if (isCancelled)
50955
- return;
50956
- }
50957
- if (isCancelled)
50958
- return;
50959
- const currentMessage = originalMessages[i];
50960
- if (!currentMessage)
50961
- continue;
50962
- // Add delay between rendered messages.
50963
- if (i > showIntermediateMessages) {
50964
- if (normalizedMessageOffsetsMs) {
50965
- const deterministicDelay = resolveOffsetDelayBeforeMessage(i);
50966
- if (deterministicDelay > 0) {
50967
- await forTime(deterministicDelay);
50968
- if (isCancelled)
50969
- return;
50970
- }
50971
- }
50972
- else {
50973
- // Sometimes do a longer pause (agent switch or random)
50974
- let didLongPause = false;
50975
- if (delays.longPauseChance &&
50976
- Math.random() < delays.longPauseChance &&
50977
- i > 0 &&
50978
- originalMessages[i].sender !== originalMessages[i - 1].sender) {
50979
- await forTime(getDelay(delays.longPauseDuration, 2000));
50980
- didLongPause = true;
50981
- if (isCancelled)
50982
- return;
50983
- }
50984
- // Otherwise normal thinking delay
50985
- if (!didLongPause) {
50986
- await forTime(getDelay(delays.thinkingBetweenMessages, 2000));
50987
- if (isCancelled)
50988
- return;
50989
- }
50990
- }
50991
- // Pause check (still between messages)
50992
- if (pauseRequestedRef.current) {
50993
- await waitIfPaused(() => isCancelled);
50994
- if (isCancelled)
50995
- return;
50996
- }
50997
- }
50998
- // Show incomplete message first (for typing effect)
50999
- const incompleteMessage = {
51000
- // channel: 'PROMPTBOOK_CHAT',
51001
- id: currentMessage.id,
51002
- createdAt: currentMessage.createdAt,
51003
- sender: currentMessage.sender,
51004
- content: '',
51005
- isComplete: false,
51006
- expectedAnswer: currentMessage.expectedAnswer,
51007
- isVoiceCall: currentMessage.isVoiceCall,
51008
- };
51009
- setDisplayedMessages((prev) => [...prev, incompleteMessage]);
51010
- // Split message content into words
51011
- const words = currentMessage.content.split(' ');
51012
- let currentContent = '';
51013
- // Type each word with delay (randomized)
51014
- for (let wordIndex = 0; wordIndex < words.length; wordIndex++) {
51015
- if (isCancelled)
51016
- return;
51017
- const word = words[wordIndex];
51018
- currentContent += (wordIndex > 0 ? ' ' : '') + word;
51019
- // Update the message with current content
51020
- const updatingMessage = {
51021
- // channel: 'PROMPTBOOK_CHAT',
51022
- id: currentMessage.id,
51023
- createdAt: currentMessage.createdAt,
51024
- sender: currentMessage.sender,
51025
- content: currentContent,
51026
- isComplete: false,
51027
- expectedAnswer: currentMessage.expectedAnswer,
51028
- isVoiceCall: currentMessage.isVoiceCall,
51029
- };
51030
- setDisplayedMessages((prev) => {
51031
- const newMessages = [...prev];
51032
- newMessages[newMessages.length - 1] = updatingMessage;
51033
- return newMessages;
51034
- });
51035
- // Wait after word with extra delay (randomized)
51036
- await forTime(getDelay(delays.waitAfterWord, 100) + getDelay(delays.extraWordDelay, 50));
51037
- if (isCancelled)
51038
- return;
51039
- }
51040
- // Mark message as complete
51041
- const completeMessage = {
51042
- // channel: 'PROMPTBOOK_CHAT',
51043
- id: currentMessage.id,
51044
- createdAt: currentMessage.createdAt,
51045
- sender: currentMessage.sender,
51046
- content: currentMessage.content,
51047
- isComplete: true,
51048
- expectedAnswer: currentMessage.expectedAnswer,
51049
- isVoiceCall: currentMessage.isVoiceCall,
51050
- };
51051
- setDisplayedMessages((prev) => {
51052
- const newMessages = [...prev];
51053
- newMessages[newMessages.length - 1] = completeMessage;
51054
- return newMessages;
51055
- });
51056
- if (!normalizedMessageOffsetsMs) {
51057
- // Small pause after completing the message
51058
- await forTime(200);
51059
- if (isCancelled)
51060
- return;
51061
- }
51062
- // Transition PAUSING -> PAUSED (after finishing current message)
51063
- if (pauseRequestedRef.current && playbackState === 'PAUSING') ;
51064
- }
51065
- setIsSimulationComplete(true);
51066
- (_b = onSimulationCompleteRef.current) === null || _b === void 0 ? void 0 : _b.call(onSimulationCompleteRef);
51067
- };
51068
- simulateChat().catch((error) => {
52779
+ simulateMockedChatPlayback({
52780
+ delays,
52781
+ originalMessages,
52782
+ normalizedMessageOffsetsMs,
52783
+ pauseRequestedRef,
52784
+ waitIfPaused,
52785
+ setDisplayedMessages,
52786
+ setLocalAppendedMessages,
52787
+ setIsSimulationComplete,
52788
+ onSimulationCompleteRef,
52789
+ isCancelledRef: () => isCancelled,
52790
+ }).catch((error) => {
51069
52791
  var _a;
51070
52792
  if (!isCancelled) {
51071
52793
  console.error('Error in MockedChat simulation:', error);
@@ -51085,7 +52807,6 @@ function MockedChat(props) {
51085
52807
  delays.thinkingBetweenMessages,
51086
52808
  delays.waitAfterWord,
51087
52809
  delays.extraWordDelay,
51088
- resolveOffsetDelayBeforeMessage,
51089
52810
  resetNonce,
51090
52811
  ]);
51091
52812
  const renderedMessages = useMemo(() => [...displayedMessages, ...localAppendedMessages], [displayedMessages, localAppendedMessages]);
@@ -51133,103 +52854,273 @@ function MockedChat(props) {
51133
52854
  }
51134
52855
 
51135
52856
  /**
51136
- * Renders TEAM conversation details, nested actions, and citations.
52857
+ * Default label used when the agent name is unavailable.
51137
52858
  *
51138
- * @param options - TEAM modal rendering options.
52859
+ * @private function of ChatToolCallModal
52860
+ */
52861
+ const DEFAULT_AGENT_LABEL = 'Agent';
52862
+ /**
52863
+ * Default label used when the teammate name is unavailable.
51139
52864
  *
51140
52865
  * @private function of ChatToolCallModal
51141
52866
  */
51142
- function TeamToolCallModalContent(options) {
51143
- var _a, _b, _c, _d, _e, _f;
51144
- const { teamResult, toolCallDate, teamToolCallSummary, selectedTeamToolCall, onSelectTeamToolCall, onClearSelectedTeamToolCall, teamProfiles, toolTitles, agentParticipant, buttonColor, } = options;
51145
- const teammateUrl = ((_a = teamResult.teammate) === null || _a === void 0 ? void 0 : _a.url) || '';
51146
- const baseTime = toolCallDate ? toolCallDate.getTime() : Date.now();
51147
- const teamToolCalls = teamToolCallSummary.toolCalls;
51148
- const teamCitations = teamToolCallSummary.citations;
51149
- const hasTeamToolCalls = teamToolCalls.length > 0;
51150
- const hasTeamCitations = teamCitations.length > 0;
51151
- const messages = (teamResult.conversation || [])
51152
- .filter((entry) => entry && entry.content)
51153
- .map((entry, index) => ({
52867
+ const DEFAULT_TEAMMATE_LABEL = 'Teammate';
52868
+ /**
52869
+ * Default header color used when the agent does not define one.
52870
+ *
52871
+ * @private function of ChatToolCallModal
52872
+ */
52873
+ const DEFAULT_AGENT_HEADER_COLOR = '#64748b';
52874
+ /**
52875
+ * Default teammate accent color used across the TEAM modal.
52876
+ *
52877
+ * @private function of ChatToolCallModal
52878
+ */
52879
+ const DEFAULT_TEAMMATE_COLOR = '#0ea5e9';
52880
+ /**
52881
+ * Returns whether one TEAM conversation entry has renderable content.
52882
+ *
52883
+ * @private function of ChatToolCallModal
52884
+ */
52885
+ function hasTeamConversationContent(entry) {
52886
+ return Boolean(entry && entry.content);
52887
+ }
52888
+ /**
52889
+ * Resolves which side of the TEAM conversation one entry belongs to.
52890
+ *
52891
+ * @private function of ChatToolCallModal
52892
+ */
52893
+ function resolveTeamConversationSender(entry) {
52894
+ return entry.sender === 'TEAMMATE' || entry.role === 'TEAMMATE' ? 'TEAMMATE' : 'AGENT';
52895
+ }
52896
+ /**
52897
+ * Creates one conversation message for the TEAM preview chat.
52898
+ *
52899
+ * @private function of ChatToolCallModal
52900
+ */
52901
+ function createTeamConversationMessage(entry, index, baseTime) {
52902
+ return {
51154
52903
  id: `team-${index}`,
51155
52904
  createdAt: new Date(baseTime + index * 1000).toISOString(),
51156
- sender: entry.sender === 'TEAMMATE' || entry.role === 'TEAMMATE' ? 'TEAMMATE' : 'AGENT',
51157
- content: entry.content || '',
52905
+ sender: resolveTeamConversationSender(entry),
52906
+ content: entry.content,
51158
52907
  isComplete: true,
51159
- }));
51160
- if (messages.length === 0) {
51161
- if (teamResult.request) {
51162
- messages.push({
51163
- id: 'team-request',
51164
- createdAt: new Date(baseTime).toISOString(),
51165
- sender: 'AGENT',
51166
- content: teamResult.request,
51167
- isComplete: true,
51168
- });
51169
- }
51170
- if (teamResult.response) {
51171
- messages.push({
51172
- id: 'team-response',
51173
- createdAt: new Date(baseTime + 1000).toISOString(),
51174
- sender: 'TEAMMATE',
51175
- content: teamResult.response,
51176
- isComplete: true,
51177
- });
51178
- }
52908
+ };
52909
+ }
52910
+ /**
52911
+ * Appends the request/response fallback pair when no structured conversation is available.
52912
+ *
52913
+ * @private function of ChatToolCallModal
52914
+ */
52915
+ function appendFallbackConversationMessages(messages, teamResult, baseTime) {
52916
+ if (messages.length > 0) {
52917
+ return;
52918
+ }
52919
+ if (teamResult.request) {
52920
+ messages.push({
52921
+ id: 'team-request',
52922
+ createdAt: new Date(baseTime).toISOString(),
52923
+ sender: 'AGENT',
52924
+ content: teamResult.request,
52925
+ isComplete: true,
52926
+ });
52927
+ }
52928
+ if (teamResult.response) {
52929
+ messages.push({
52930
+ id: 'team-response',
52931
+ createdAt: new Date(baseTime + 1000).toISOString(),
52932
+ sender: 'TEAMMATE',
52933
+ content: teamResult.response,
52934
+ isComplete: true,
52935
+ });
51179
52936
  }
51180
- const agentName = ((_c = (_b = teamResult.conversation) === null || _b === void 0 ? void 0 : _b.find((entry) => entry.sender === 'AGENT' || entry.role === 'AGENT')) === null || _c === void 0 ? void 0 : _c.name) || 'Agent';
51181
- const teammateConversationName = ((_e = (_d = teamResult.conversation) === null || _d === void 0 ? void 0 : _d.find((entry) => entry.sender === 'TEAMMATE' || entry.role === 'TEAMMATE')) === null || _e === void 0 ? void 0 : _e.name) || '';
52937
+ }
52938
+ /**
52939
+ * Builds the conversation transcript shown in the TEAM modal.
52940
+ *
52941
+ * @private function of ChatToolCallModal
52942
+ */
52943
+ function createTeamConversationMessages(teamResult, baseTime) {
52944
+ const messages = (teamResult.conversation || [])
52945
+ .filter(hasTeamConversationContent)
52946
+ .map((entry, index) => createTeamConversationMessage(entry, index, baseTime));
52947
+ appendFallbackConversationMessages(messages, teamResult, baseTime);
52948
+ return messages;
52949
+ }
52950
+ /**
52951
+ * Finds the first conversation name used by a specific sender.
52952
+ *
52953
+ * @private function of ChatToolCallModal
52954
+ */
52955
+ function resolveConversationName(teamResult, sender, fallbackName) {
52956
+ var _a, _b;
52957
+ return (((_b = (_a = teamResult.conversation) === null || _a === void 0 ? void 0 : _a.find((entry) => resolveTeamConversationSender(entry) === sender)) === null || _b === void 0 ? void 0 : _b.name) || fallbackName);
52958
+ }
52959
+ /**
52960
+ * Resolves the public teammate link when it points to a real agent.
52961
+ *
52962
+ * @private function of ChatToolCallModal
52963
+ */
52964
+ function resolveTeammateLink(teammateUrl) {
52965
+ return teammateUrl && teammateUrl !== 'VOID' && !isPseudoAgentUrl(teammateUrl) ? teammateUrl : undefined;
52966
+ }
52967
+ /**
52968
+ * Builds the TEAM modal header data and chat participants.
52969
+ *
52970
+ * @private function of ChatToolCallModal
52971
+ */
52972
+ function createTeamConversationViewModel({ agentParticipant, teamProfiles, teamResult, }) {
52973
+ var _a, _b;
52974
+ const teammateUrl = ((_a = teamResult.teammate) === null || _a === void 0 ? void 0 : _a.url) || '';
51182
52975
  const teammateProfile = teammateUrl ? teamProfiles[teammateUrl] : undefined;
51183
52976
  const teammateFallbackProfile = teammateUrl
51184
52977
  ? resolveAgentProfileFallback({
51185
52978
  url: teammateUrl,
51186
- label: (_f = teamResult.teammate) === null || _f === void 0 ? void 0 : _f.label,
52979
+ label: (_b = teamResult.teammate) === null || _b === void 0 ? void 0 : _b.label,
51187
52980
  })
51188
- : { label: 'Teammate', imageUrl: null };
52981
+ : { label: DEFAULT_TEAMMATE_LABEL, imageUrl: null };
52982
+ const agentName = resolveConversationName(teamResult, 'AGENT', DEFAULT_AGENT_LABEL);
52983
+ const teammateConversationName = resolveConversationName(teamResult, 'TEAMMATE', '');
51189
52984
  const resolvedAgentLabel = resolvePreferredAgentLabel([agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.fullname, agentName], agentName);
51190
- const resolvedAgentAvatar = (agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.avatarSrc) || null;
51191
- const resolvedAgentAvatarDefinition = agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.avatarDefinition;
51192
- const resolvedAgentAvatarVisualId = agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.avatarVisualId;
51193
- const resolvedAgentHeaderColor = (agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.color)
51194
- ? Color.fromSafe(agentParticipant.color).toHex()
51195
- : '#64748b';
51196
52985
  const resolvedTeammateLabel = resolvePreferredAgentLabel([teammateConversationName, teammateProfile === null || teammateProfile === void 0 ? void 0 : teammateProfile.label, teammateFallbackProfile.label], teammateFallbackProfile.label);
51197
- const resolvedTeammateAvatar = (teammateProfile === null || teammateProfile === void 0 ? void 0 : teammateProfile.imageUrl) || teammateFallbackProfile.imageUrl || null;
51198
- const teammateLink = teammateUrl && teammateUrl !== 'VOID' && !isPseudoAgentUrl(teammateUrl) ? teammateUrl : undefined;
51199
- const participants = [
51200
- {
51201
- name: 'AGENT',
51202
- fullname: resolvedAgentLabel,
51203
- color: (agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.color) || '#64748b',
51204
- avatarSrc: resolvedAgentAvatar || undefined,
51205
- avatarDefinition: resolvedAgentAvatarDefinition,
51206
- avatarVisualId: resolvedAgentAvatarVisualId,
51207
- isMe: true,
51208
- },
51209
- {
51210
- name: 'TEAMMATE',
51211
- fullname: resolvedTeammateLabel,
51212
- color: '#0ea5e9',
51213
- avatarSrc: resolvedTeammateAvatar || undefined,
51214
- },
51215
- ];
51216
- return (jsxs(Fragment, { children: [jsx("div", { className: classNames(styles$5.searchModalHeader, styles$5.teamModalHeader), children: jsxs("div", { className: styles$5.teamHeaderParticipants, children: [jsx(TeamHeaderProfile, { label: resolvedAgentLabel, avatarSrc: resolvedAgentAvatar, avatarDefinition: resolvedAgentAvatarDefinition, avatarVisualId: resolvedAgentAvatarVisualId, fallbackColor: resolvedAgentHeaderColor }), jsx("span", { className: styles$5.teamHeaderDivider, children: "talking with" }), jsx(TeamHeaderProfile, { label: resolvedTeammateLabel, avatarSrc: resolvedTeammateAvatar, fallbackColor: "#0ea5e9", href: teammateLink })] }) }), jsxs("div", { className: styles$5.searchModalContent, children: [messages.length > 0 ? (jsx("div", { className: styles$5.teamChatContainer, children: jsx(MockedChat, { title: `Chat between ${resolvedAgentLabel} and ${resolvedTeammateLabel}`, messages: messages, participants: participants, isResettable: false, isPausable: false, isSaveButtonEnabled: false, isCopyButtonEnabled: false, layout: "STANDALONE", delayConfig: {
51217
- // Note+TODO: For some strange reason, <MockedChat/> is not running and stays static on the initial frame, so doing this hack to force it to show the entire chat at once. Need to investigate why the animation is not running as expected and then just use `delayConfig={FAST_FLOW}`
51218
- ...FAST_FLOW,
51219
- beforeFirstMessage: 0,
51220
- showIntermediateMessages: messages.length,
51221
- }, visualMode: "BUBBLE_MODE" }) })) : (jsx("div", { className: styles$5.noResults, children: "No teammate conversation available." })), (hasTeamToolCalls || hasTeamCitations) && (jsxs("div", { className: styles$5.teamToolCallSection, children: [hasTeamToolCalls && (jsxs("div", { className: styles$5.teamToolCallGroup, children: [jsx("div", { className: styles$5.teamToolCallHeading, children: "Actions" }), jsx("div", { className: styles$5.teamToolCallChips, children: teamToolCalls.map((toolCallEntry, index) => {
51222
- const chipletInfo = getToolCallChipletInfo(toolCallEntry.toolCall, undefined, toolTitles);
51223
- const chipletText = buildToolCallChipText(chipletInfo);
51224
- return (jsxs("button", { className: styles$5.completedToolCall, onClick: () => {
51225
- onSelectTeamToolCall(toolCallEntry);
51226
- }, children: [jsx("span", { children: chipletText }), jsxs("span", { className: styles$5.toolCallOrigin, children: ["by ", toolCallEntry.origin.label] })] }, `team-tool-${index}`));
51227
- }) })] })), hasTeamCitations && (jsxs("div", { className: styles$5.teamToolCallGroup, children: [jsx("div", { className: styles$5.teamToolCallHeading, children: "Sources" }), jsx("div", { className: styles$5.teamToolCallChips, children: teamCitations.map((citation, index) => (jsx(SourceChip, { citation: citation, suffix: `by ${citation.origin.label}` }, `team-source-${citation.source}-${index}`))) })] })), selectedTeamToolCall && (jsxs("div", { className: styles$5.teamToolCallDetails, children: [jsxs("div", { className: styles$5.teamToolCallDetailsHeader, children: [jsxs("span", { className: styles$5.teamToolCallDetailsTitle, children: ["Action details", jsxs("span", { className: styles$5.toolCallOrigin, children: ["by ", selectedTeamToolCall.origin.label] })] }), jsx("button", { type: "button", className: styles$5.teamToolCallDetailsClear, onClick: onClearSelectedTeamToolCall, children: "Clear" })] }), renderToolCallDetails({
51228
- toolCall: selectedTeamToolCall.toolCall,
51229
- toolTitles,
51230
- agentParticipant,
51231
- buttonColor,
51232
- })] }))] }))] })] }));
52986
+ const agentHeader = {
52987
+ label: resolvedAgentLabel,
52988
+ avatarSrc: (agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.avatarSrc) || null,
52989
+ avatarDefinition: agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.avatarDefinition,
52990
+ avatarVisualId: agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.avatarVisualId,
52991
+ fallbackColor: (agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.color)
52992
+ ? Color.fromSafe(agentParticipant.color).toHex()
52993
+ : DEFAULT_AGENT_HEADER_COLOR,
52994
+ };
52995
+ const teammateHeader = {
52996
+ label: resolvedTeammateLabel,
52997
+ avatarSrc: (teammateProfile === null || teammateProfile === void 0 ? void 0 : teammateProfile.imageUrl) || teammateFallbackProfile.imageUrl || null,
52998
+ fallbackColor: DEFAULT_TEAMMATE_COLOR,
52999
+ href: resolveTeammateLink(teammateUrl),
53000
+ };
53001
+ return {
53002
+ agentHeader,
53003
+ teammateHeader,
53004
+ participants: [
53005
+ {
53006
+ name: 'AGENT',
53007
+ fullname: agentHeader.label,
53008
+ color: (agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.color) || DEFAULT_AGENT_HEADER_COLOR,
53009
+ avatarSrc: agentHeader.avatarSrc || undefined,
53010
+ avatarDefinition: agentHeader.avatarDefinition,
53011
+ avatarVisualId: agentHeader.avatarVisualId,
53012
+ isMe: true,
53013
+ },
53014
+ {
53015
+ name: 'TEAMMATE',
53016
+ fullname: teammateHeader.label,
53017
+ color: DEFAULT_TEAMMATE_COLOR,
53018
+ avatarSrc: teammateHeader.avatarSrc || undefined,
53019
+ },
53020
+ ],
53021
+ };
53022
+ }
53023
+ /**
53024
+ * Renders the TEAM conversation header profiles.
53025
+ *
53026
+ * @private component of ChatToolCallModal
53027
+ */
53028
+ function TeamConversationHeader(props) {
53029
+ const { agentHeader, teammateHeader } = props;
53030
+ return (jsx("div", { className: classNames(styles$5.searchModalHeader, styles$5.teamModalHeader), children: jsxs("div", { className: styles$5.teamHeaderParticipants, children: [jsx(TeamHeaderProfile, { label: agentHeader.label, avatarSrc: agentHeader.avatarSrc, avatarDefinition: agentHeader.avatarDefinition, avatarVisualId: agentHeader.avatarVisualId, fallbackColor: agentHeader.fallbackColor }), jsx("span", { className: styles$5.teamHeaderDivider, children: "talking with" }), jsx(TeamHeaderProfile, { label: teammateHeader.label, avatarSrc: teammateHeader.avatarSrc, fallbackColor: teammateHeader.fallbackColor, href: teammateHeader.href })] }) }));
53031
+ }
53032
+ /**
53033
+ * Renders the TEAM conversation transcript or the empty-state fallback.
53034
+ *
53035
+ * @private component of ChatToolCallModal
53036
+ */
53037
+ function TeamConversationSection(props) {
53038
+ const { agentLabel, teammateLabel, messages, participants } = props;
53039
+ if (messages.length === 0) {
53040
+ return jsx("div", { className: styles$5.noResults, children: "No teammate conversation available." });
53041
+ }
53042
+ return (jsx("div", { className: styles$5.teamChatContainer, children: jsx(MockedChat, { title: `Chat between ${agentLabel} and ${teammateLabel}`, messages: messages, participants: participants, isResettable: false, isPausable: false, isSaveButtonEnabled: false, isCopyButtonEnabled: false, layout: "STANDALONE", delayConfig: {
53043
+ // Note+TODO: For some strange reason, <MockedChat/> is not running and stays static on the initial frame, so doing this hack to force it to show the entire chat at once. Need to investigate why the animation is not running as expected and then just use `delayConfig={FAST_FLOW}`
53044
+ ...FAST_FLOW,
53045
+ beforeFirstMessage: 0,
53046
+ showIntermediateMessages: messages.length,
53047
+ }, visualMode: "BUBBLE_MODE" }) }));
53048
+ }
53049
+ /**
53050
+ * Renders action chips for teammate-executed tool calls.
53051
+ *
53052
+ * @private component of ChatToolCallModal
53053
+ */
53054
+ function TeamToolCallActionsGroup(props) {
53055
+ const { onSelectTeamToolCall, teamToolCalls, toolTitles } = props;
53056
+ if (teamToolCalls.length === 0) {
53057
+ return null;
53058
+ }
53059
+ return (jsxs("div", { className: styles$5.teamToolCallGroup, children: [jsx("div", { className: styles$5.teamToolCallHeading, children: "Actions" }), jsx("div", { className: styles$5.teamToolCallChips, children: teamToolCalls.map((toolCallEntry, index) => {
53060
+ const chipletInfo = getToolCallChipletInfo(toolCallEntry.toolCall, undefined, toolTitles);
53061
+ const chipletText = buildToolCallChipText(chipletInfo);
53062
+ return (jsxs("button", { className: styles$5.completedToolCall, onClick: () => {
53063
+ onSelectTeamToolCall(toolCallEntry);
53064
+ }, children: [jsx("span", { children: chipletText }), jsxs("span", { className: styles$5.toolCallOrigin, children: ["by ", toolCallEntry.origin.label] })] }, `team-tool-${index}`));
53065
+ }) })] }));
53066
+ }
53067
+ /**
53068
+ * Renders citations surfaced from the teammate exchange.
53069
+ *
53070
+ * @private component of ChatToolCallModal
53071
+ */
53072
+ function TeamToolCallSourcesGroup(props) {
53073
+ const { teamCitations } = props;
53074
+ if (teamCitations.length === 0) {
53075
+ return null;
53076
+ }
53077
+ return (jsxs("div", { className: styles$5.teamToolCallGroup, children: [jsx("div", { className: styles$5.teamToolCallHeading, children: "Sources" }), jsx("div", { className: styles$5.teamToolCallChips, children: teamCitations.map((citation, index) => (jsx(SourceChip, { citation: citation, suffix: `by ${citation.origin.label}` }, `team-source-${citation.source}-${index}`))) })] }));
53078
+ }
53079
+ /**
53080
+ * Renders the nested action details for the selected teammate tool call.
53081
+ *
53082
+ * @private component of ChatToolCallModal
53083
+ */
53084
+ function TeamToolCallDetailsPanel(props) {
53085
+ const { agentParticipant, buttonColor, onClearSelectedTeamToolCall, selectedTeamToolCall, toolTitles } = props;
53086
+ if (!selectedTeamToolCall) {
53087
+ return null;
53088
+ }
53089
+ return (jsxs("div", { className: styles$5.teamToolCallDetails, children: [jsxs("div", { className: styles$5.teamToolCallDetailsHeader, children: [jsxs("span", { className: styles$5.teamToolCallDetailsTitle, children: ["Action details", jsxs("span", { className: styles$5.toolCallOrigin, children: ["by ", selectedTeamToolCall.origin.label] })] }), jsx("button", { type: "button", className: styles$5.teamToolCallDetailsClear, onClick: onClearSelectedTeamToolCall, children: "Clear" })] }), renderToolCallDetails({
53090
+ toolCall: selectedTeamToolCall.toolCall,
53091
+ toolTitles,
53092
+ agentParticipant,
53093
+ buttonColor,
53094
+ })] }));
53095
+ }
53096
+ /**
53097
+ * Renders the nested tool-call and citation summary below the TEAM transcript.
53098
+ *
53099
+ * @private component of ChatToolCallModal
53100
+ */
53101
+ function TeamToolCallSummarySection(props) {
53102
+ const { agentParticipant, buttonColor, onClearSelectedTeamToolCall, onSelectTeamToolCall, selectedTeamToolCall, teamToolCallSummary, toolTitles, } = props;
53103
+ const hasSummaryContent = teamToolCallSummary.toolCalls.length > 0 || teamToolCallSummary.citations.length > 0;
53104
+ if (!hasSummaryContent) {
53105
+ return null;
53106
+ }
53107
+ return (jsxs("div", { className: styles$5.teamToolCallSection, children: [jsx(TeamToolCallActionsGroup, { teamToolCalls: teamToolCallSummary.toolCalls, onSelectTeamToolCall: onSelectTeamToolCall, toolTitles: toolTitles }), jsx(TeamToolCallSourcesGroup, { teamCitations: teamToolCallSummary.citations }), jsx(TeamToolCallDetailsPanel, { selectedTeamToolCall: selectedTeamToolCall, onClearSelectedTeamToolCall: onClearSelectedTeamToolCall, toolTitles: toolTitles, agentParticipant: agentParticipant, buttonColor: buttonColor })] }));
53108
+ }
53109
+ /**
53110
+ * Renders TEAM conversation details, nested actions, and citations.
53111
+ *
53112
+ * @private function of ChatToolCallModal
53113
+ */
53114
+ function TeamToolCallModalContent(options) {
53115
+ const { teamResult, toolCallDate, teamToolCallSummary, selectedTeamToolCall, onSelectTeamToolCall, onClearSelectedTeamToolCall, teamProfiles, toolTitles, agentParticipant, buttonColor, } = options;
53116
+ const baseTime = toolCallDate ? toolCallDate.getTime() : Date.now();
53117
+ const messages = createTeamConversationMessages(teamResult, baseTime);
53118
+ const { agentHeader, teammateHeader, participants } = createTeamConversationViewModel({
53119
+ teamResult,
53120
+ teamProfiles,
53121
+ agentParticipant,
53122
+ });
53123
+ return (jsxs(Fragment, { children: [jsx(TeamConversationHeader, { agentHeader: agentHeader, teammateHeader: teammateHeader }), jsxs("div", { className: styles$5.searchModalContent, children: [jsx(TeamConversationSection, { agentLabel: agentHeader.label, teammateLabel: teammateHeader.label, messages: messages, participants: participants }), jsx(TeamToolCallSummarySection, { teamToolCallSummary: teamToolCallSummary, selectedTeamToolCall: selectedTeamToolCall, onSelectTeamToolCall: onSelectTeamToolCall, onClearSelectedTeamToolCall: onClearSelectedTeamToolCall, toolTitles: toolTitles, agentParticipant: agentParticipant, buttonColor: buttonColor })] })] }));
51233
53124
  }
51234
53125
 
51235
53126
  /**
@@ -51681,8 +53572,314 @@ function useChatActionsOverlap(config) {
51681
53572
 
51682
53573
  /**
51683
53574
  * Debounce window for synchronizing `isAutoScrolling` with the latest scroll position.
53575
+ *
53576
+ * @private function of `useChatAutoScroll`
51684
53577
  */
51685
53578
  const SCROLL_EVENT_DEBOUNCE_MS = 50;
53579
+ /**
53580
+ * Faster follow-up used while the latest assistant message is still streaming.
53581
+ *
53582
+ * @private function of `useChatAutoScroll`
53583
+ */
53584
+ const STREAMING_SCROLL_CHECK_DELAY_MS = 10;
53585
+ /**
53586
+ * Viewport width treated as mobile for chat scrolling behavior.
53587
+ *
53588
+ * @private function of `useChatAutoScroll`
53589
+ */
53590
+ const MOBILE_BREAKPOINT_PX = 768;
53591
+ /**
53592
+ * Mobile-device user agent matcher used by the chat viewport detection.
53593
+ *
53594
+ * @private function of `useChatAutoScroll`
53595
+ */
53596
+ const MOBILE_DEVICE_USER_AGENT_PATTERN = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
53597
+ /**
53598
+ * Detects whether the current viewport should use the mobile scrolling path.
53599
+ *
53600
+ * @returns `true` when the viewport or device matches the mobile heuristics.
53601
+ *
53602
+ * @private function of `useChatAutoScroll`
53603
+ */
53604
+ function isMobileChatViewport() {
53605
+ return window.innerWidth <= MOBILE_BREAKPOINT_PX || MOBILE_DEVICE_USER_AGENT_PATTERN.test(navigator.userAgent);
53606
+ }
53607
+ /**
53608
+ * Tracks whether the chat should currently use mobile scrolling behavior.
53609
+ *
53610
+ * @private function of `useChatAutoScroll`
53611
+ */
53612
+ function useMobileChatViewport() {
53613
+ const [isMobile, setIsMobile] = useState(false);
53614
+ useEffect(() => {
53615
+ const handleViewportChange = () => {
53616
+ setIsMobile(isMobileChatViewport());
53617
+ };
53618
+ handleViewportChange();
53619
+ window.addEventListener('resize', handleViewportChange);
53620
+ return () => window.removeEventListener('resize', handleViewportChange);
53621
+ }, []);
53622
+ return isMobile;
53623
+ }
53624
+ /**
53625
+ * Checks whether the messages container is effectively scrolled to the bottom.
53626
+ *
53627
+ * @param element - Scrollable chat messages container.
53628
+ * @param bottomThreshold - Distance from the bottom still considered "at bottom".
53629
+ * @returns `true` when the container is within the threshold from the bottom.
53630
+ *
53631
+ * @private function of `useChatAutoScroll`
53632
+ */
53633
+ function isChatScrolledToBottom(element, bottomThreshold) {
53634
+ const { scrollTop, scrollHeight, clientHeight } = element;
53635
+ return scrollTop + clientHeight >= scrollHeight - bottomThreshold;
53636
+ }
53637
+ /**
53638
+ * Clears a scheduled timeout when one is currently active.
53639
+ *
53640
+ * @private function of `useChatAutoScroll`
53641
+ */
53642
+ function clearScheduledTimeout(timeoutRef) {
53643
+ if (timeoutRef.current) {
53644
+ clearTimeout(timeoutRef.current);
53645
+ timeoutRef.current = null;
53646
+ }
53647
+ }
53648
+ /**
53649
+ * Performs the concrete scroll-to-bottom operation for the current platform.
53650
+ *
53651
+ * @private function of `useChatAutoScroll`
53652
+ */
53653
+ function performScrollToBottom({ behavior, chatMessagesElement, isMobile, smoothScroll, }) {
53654
+ if (isMobile) {
53655
+ chatMessagesElement.scrollTo({
53656
+ top: chatMessagesElement.scrollHeight,
53657
+ behavior: smoothScroll ? behavior : 'auto',
53658
+ });
53659
+ return;
53660
+ }
53661
+ if (smoothScroll && behavior === 'smooth') {
53662
+ chatMessagesElement.style.scrollBehavior = 'smooth';
53663
+ chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
53664
+ chatMessagesElement.style.scrollBehavior = 'auto';
53665
+ return;
53666
+ }
53667
+ chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
53668
+ }
53669
+ /**
53670
+ * Detects whether the user currently has a non-collapsed selection inside the chat container.
53671
+ *
53672
+ * @private function of `useChatAutoScroll`
53673
+ */
53674
+ function hasExpandedSelectionInChat(chatMessagesElement) {
53675
+ const selection = window.getSelection();
53676
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
53677
+ return false;
53678
+ }
53679
+ const range = selection.getRangeAt(0);
53680
+ return chatMessagesElement.contains(range.startContainer) || chatMessagesElement.contains(range.endContainer);
53681
+ }
53682
+ /**
53683
+ * Computes the scroll metrics needed to decide whether new content should trigger auto-scroll.
53684
+ *
53685
+ * @private function of `useChatAutoScroll`
53686
+ */
53687
+ function getMessagesChangeMetrics({ bottomThreshold, chatMessagesElement, previousScrollHeight, }) {
53688
+ const currentScrollHeight = chatMessagesElement.scrollHeight;
53689
+ return {
53690
+ currentScrollHeight,
53691
+ hasNewContent: currentScrollHeight > previousScrollHeight,
53692
+ wasAtBottomBeforeNewContent: chatMessagesElement.scrollTop + chatMessagesElement.clientHeight >= previousScrollHeight - bottomThreshold,
53693
+ };
53694
+ }
53695
+ /**
53696
+ * Evaluates whether new content should keep the chat pinned to the latest message.
53697
+ *
53698
+ * @private function of `useChatAutoScroll`
53699
+ */
53700
+ function shouldAutoScrollForMessagesChange({ hasManualScroll, hasNewContent, hasSelectionInChat, isAutoScrolling, wasAtBottomBeforeNewContent, }) {
53701
+ return hasNewContent && isAutoScrolling && wasAtBottomBeforeNewContent && !hasSelectionInChat && !hasManualScroll;
53702
+ }
53703
+ /**
53704
+ * Evaluates whether the latest DOM update should schedule a follow-up scroll.
53705
+ *
53706
+ * @private function of `useChatAutoScroll`
53707
+ */
53708
+ function evaluateMessagesChangeAutoScroll({ bottomThreshold, chatMessagesElement, hasManualScroll, isAutoScrolling, previousScrollHeight, }) {
53709
+ const metrics = getMessagesChangeMetrics({
53710
+ bottomThreshold,
53711
+ chatMessagesElement,
53712
+ previousScrollHeight,
53713
+ });
53714
+ return {
53715
+ currentScrollHeight: metrics.currentScrollHeight,
53716
+ shouldScheduleScroll: shouldAutoScrollForMessagesChange({
53717
+ hasManualScroll,
53718
+ hasNewContent: metrics.hasNewContent,
53719
+ hasSelectionInChat: hasExpandedSelectionInChat(chatMessagesElement),
53720
+ isAutoScrolling,
53721
+ wasAtBottomBeforeNewContent: metrics.wasAtBottomBeforeNewContent,
53722
+ }),
53723
+ };
53724
+ }
53725
+ /**
53726
+ * Chooses the follow-up delay for the next automatic scroll attempt.
53727
+ *
53728
+ * @private function of `useChatAutoScroll`
53729
+ */
53730
+ function getMessagesChangeDelay(isStreaming, scrollCheckDelay) {
53731
+ return isStreaming ? STREAMING_SCROLL_CHECK_DELAY_MS : scrollCheckDelay;
53732
+ }
53733
+ /**
53734
+ * Chooses the scroll behavior for the next automatic scroll attempt.
53735
+ *
53736
+ * @private function of `useChatAutoScroll`
53737
+ */
53738
+ function getMessagesChangeScrollBehavior(isStreaming) {
53739
+ return isStreaming ? 'auto' : 'smooth';
53740
+ }
53741
+ /**
53742
+ * Updates the manual-scroll and auto-scroll flags from the latest direct scroll event.
53743
+ *
53744
+ * @private function of `useChatAutoScroll`
53745
+ */
53746
+ function updateAutoScrollStateFromScroll({ hasManualScrollRef, isAtBottom, isAutoScrolling, setIsAutoScrolling, }) {
53747
+ hasManualScrollRef.current = !isAtBottom;
53748
+ if (!isAtBottom && isAutoScrolling) {
53749
+ setIsAutoScrolling(false);
53750
+ }
53751
+ }
53752
+ /**
53753
+ * Schedules the debounced reconciliation that keeps `isAutoScrolling` in sync with the viewport.
53754
+ *
53755
+ * @private function of `useChatAutoScroll`
53756
+ */
53757
+ function scheduleAutoScrollStateSync({ element, scrollTimeoutRef, syncAutoScrollState, }) {
53758
+ clearScheduledTimeout(scrollTimeoutRef);
53759
+ scrollTimeoutRef.current = setTimeout(() => {
53760
+ syncAutoScrollState(element);
53761
+ }, SCROLL_EVENT_DEBOUNCE_MS);
53762
+ }
53763
+ /**
53764
+ * Schedules the delayed scroll that follows a new message or streaming chunk.
53765
+ *
53766
+ * @private function of `useChatAutoScroll`
53767
+ */
53768
+ function scheduleMessagesChangeAutoScroll({ hasManualScrollRef, isStreaming, messagesChangeTimeoutRef, scrollCheckDelay, scrollToBottom, }) {
53769
+ clearScheduledTimeout(messagesChangeTimeoutRef);
53770
+ messagesChangeTimeoutRef.current = setTimeout(() => {
53771
+ if (hasManualScrollRef.current) {
53772
+ return;
53773
+ }
53774
+ scrollToBottom(getMessagesChangeScrollBehavior(isStreaming));
53775
+ }, getMessagesChangeDelay(isStreaming, scrollCheckDelay));
53776
+ }
53777
+ /**
53778
+ * Initializes the chat container reference and restores the pinned-to-bottom state when needed.
53779
+ *
53780
+ * @private function of `useChatAutoScroll`
53781
+ */
53782
+ function initializeChatMessagesElement({ element, isAutoScrolling, lastScrollHeightRef, scrollToBottom, }) {
53783
+ lastScrollHeightRef.current = element.scrollHeight;
53784
+ if (!isAutoScrolling) {
53785
+ return;
53786
+ }
53787
+ requestAnimationFrame(() => {
53788
+ scrollToBottom('auto');
53789
+ });
53790
+ }
53791
+ /**
53792
+ * Creates the debounced scroll handler that reacts to direct user scrolling.
53793
+ *
53794
+ * @private function of `useChatAutoScroll`
53795
+ */
53796
+ function useChatScrollHandler({ checkIfAtBottom, hasManualScrollRef, isAutoScrolling, scrollTimeoutRef, setIsAutoScrolling, }) {
53797
+ const syncAutoScrollState = useCallback((element) => {
53798
+ const isAtBottom = checkIfAtBottom(element);
53799
+ setIsAutoScrolling((currentValue) => (currentValue === isAtBottom ? currentValue : isAtBottom));
53800
+ }, [checkIfAtBottom, setIsAutoScrolling]);
53801
+ return useCallback((event) => {
53802
+ const element = event.currentTarget;
53803
+ const isAtBottom = checkIfAtBottom(element);
53804
+ updateAutoScrollStateFromScroll({
53805
+ hasManualScrollRef,
53806
+ isAtBottom,
53807
+ isAutoScrolling,
53808
+ setIsAutoScrolling,
53809
+ });
53810
+ scheduleAutoScrollStateSync({
53811
+ element,
53812
+ scrollTimeoutRef,
53813
+ syncAutoScrollState,
53814
+ });
53815
+ }, [
53816
+ checkIfAtBottom,
53817
+ hasManualScrollRef,
53818
+ isAutoScrolling,
53819
+ scrollTimeoutRef,
53820
+ setIsAutoScrolling,
53821
+ syncAutoScrollState,
53822
+ ]);
53823
+ }
53824
+ /**
53825
+ * Creates the message-change handler that decides whether the chat should follow new content.
53826
+ *
53827
+ * @private function of `useChatAutoScroll`
53828
+ */
53829
+ function useChatMessagesChangeHandler({ bottomThreshold, chatMessagesRef, hasManualScrollRef, isAutoScrolling, lastScrollHeightRef, messagesChangeTimeoutRef, scrollCheckDelay, scrollToBottom, }) {
53830
+ return useCallback((isStreaming = false) => {
53831
+ const chatMessagesElement = chatMessagesRef.current;
53832
+ if (!chatMessagesElement) {
53833
+ return;
53834
+ }
53835
+ const autoScrollDecision = evaluateMessagesChangeAutoScroll({
53836
+ bottomThreshold,
53837
+ chatMessagesElement,
53838
+ hasManualScroll: hasManualScrollRef.current,
53839
+ isAutoScrolling,
53840
+ previousScrollHeight: lastScrollHeightRef.current,
53841
+ });
53842
+ lastScrollHeightRef.current = autoScrollDecision.currentScrollHeight;
53843
+ if (!autoScrollDecision.shouldScheduleScroll) {
53844
+ return;
53845
+ }
53846
+ scheduleMessagesChangeAutoScroll({
53847
+ hasManualScrollRef,
53848
+ isStreaming,
53849
+ messagesChangeTimeoutRef,
53850
+ scrollCheckDelay,
53851
+ scrollToBottom,
53852
+ });
53853
+ }, [
53854
+ bottomThreshold,
53855
+ chatMessagesRef,
53856
+ hasManualScrollRef,
53857
+ isAutoScrolling,
53858
+ lastScrollHeightRef,
53859
+ messagesChangeTimeoutRef,
53860
+ scrollCheckDelay,
53861
+ scrollToBottom,
53862
+ ]);
53863
+ }
53864
+ /**
53865
+ * Creates the ref callback that captures the chat container and initializes its scroll state.
53866
+ *
53867
+ * @private function of `useChatAutoScroll`
53868
+ */
53869
+ function useChatMessagesRefCallback({ chatMessagesRef, isAutoScrolling, lastScrollHeightRef, scrollToBottom, }) {
53870
+ return useCallback((element) => {
53871
+ chatMessagesRef.current = element;
53872
+ if (!element) {
53873
+ return;
53874
+ }
53875
+ initializeChatMessagesElement({
53876
+ element,
53877
+ isAutoScrolling,
53878
+ lastScrollHeightRef,
53879
+ scrollToBottom,
53880
+ });
53881
+ }, [chatMessagesRef, isAutoScrolling, lastScrollHeightRef, scrollToBottom]);
53882
+ }
51686
53883
  /**
51687
53884
  * Hook for managing auto-scroll behavior in chat components
51688
53885
  *
@@ -51696,153 +53893,61 @@ const SCROLL_EVENT_DEBOUNCE_MS = 50;
51696
53893
  */
51697
53894
  function useChatAutoScroll(config = {}) {
51698
53895
  const { bottomThreshold = 100, smoothScroll = true, scrollCheckDelay = 100 } = config;
53896
+ const isMobile = useMobileChatViewport();
51699
53897
  const [isAutoScrolling, setIsAutoScrolling] = useState(true);
51700
- const [isMobile, setIsMobile] = useState(false);
51701
53898
  const chatMessagesRef = useRef(null);
51702
53899
  const scrollTimeoutRef = useRef(null);
51703
53900
  const lastScrollHeightRef = useRef(0);
51704
- // Tracks whether the user moved away from the bottom so we can suspend auto-scroll.
51705
53901
  const hasManualScrollRef = useRef(false);
51706
- // Detect mobile device
51707
- useEffect(() => {
51708
- const checkMobile = () => {
51709
- const isMobileDevice = window.innerWidth <= 768 ||
51710
- /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
51711
- setIsMobile(isMobileDevice);
51712
- };
51713
- checkMobile();
51714
- window.addEventListener('resize', checkMobile);
51715
- return () => window.removeEventListener('resize', checkMobile);
51716
- }, []);
51717
- // Check if user is at the bottom of the chat
51718
- const checkIfAtBottom = useCallback((element) => {
51719
- const { scrollTop, scrollHeight, clientHeight } = element;
51720
- return scrollTop + clientHeight >= scrollHeight - bottomThreshold;
51721
- }, [bottomThreshold]);
51722
- // Scroll to bottom function
53902
+ const messagesChangeTimeoutRef = useRef(null);
53903
+ const checkIfAtBottom = useCallback((element) => isChatScrolledToBottom(element, bottomThreshold), [bottomThreshold]);
51723
53904
  const scrollToBottom = useCallback((behavior = 'smooth') => {
51724
53905
  const chatMessagesElement = chatMessagesRef.current;
51725
- if (!chatMessagesElement)
53906
+ if (!chatMessagesElement) {
51726
53907
  return;
51727
- if (isMobile) {
51728
- // Mobile-optimized scrolling
51729
- chatMessagesElement.scrollTo({
51730
- top: chatMessagesElement.scrollHeight,
51731
- behavior: smoothScroll ? behavior : 'auto',
51732
- });
51733
- }
51734
- else {
51735
- // Desktop scrolling
51736
- if (smoothScroll && behavior === 'smooth') {
51737
- chatMessagesElement.style.scrollBehavior = 'smooth';
51738
- chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
51739
- chatMessagesElement.style.scrollBehavior = 'auto';
51740
- }
51741
- else {
51742
- chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
51743
- }
51744
53908
  }
53909
+ performScrollToBottom({
53910
+ behavior,
53911
+ chatMessagesElement,
53912
+ isMobile,
53913
+ smoothScroll,
53914
+ });
51745
53915
  hasManualScrollRef.current = false;
51746
53916
  }, [isMobile, smoothScroll]);
51747
- // Handle scroll events
51748
- const handleScroll = useCallback((event) => {
51749
- const element = event.target;
51750
- if (!element)
51751
- return;
51752
- const atBottom = checkIfAtBottom(element);
51753
- hasManualScrollRef.current = !atBottom;
51754
- // User scroll should take precedence over app-driven scroll intent.
51755
- if (!atBottom && isAutoScrolling) {
51756
- setIsAutoScrolling(false);
51757
- }
51758
- // Clear any pending scroll timeout
51759
- if (scrollTimeoutRef.current) {
51760
- clearTimeout(scrollTimeoutRef.current);
51761
- }
51762
- // Debounce scroll position check to avoid too frequent updates
51763
- scrollTimeoutRef.current = setTimeout(() => {
51764
- const isAtBottom = checkIfAtBottom(element);
51765
- setIsAutoScrolling((currentState) => (currentState === isAtBottom ? currentState : isAtBottom));
51766
- }, SCROLL_EVENT_DEBOUNCE_MS);
51767
- }, [checkIfAtBottom, isAutoScrolling]);
51768
- const messagesChangeTimeoutRef = useRef(null);
51769
- // Auto-scroll when messages change (if user is at bottom)
51770
- const handleMessagesChange = useCallback((isStreaming = false) => {
51771
- const chatMessagesElement = chatMessagesRef.current;
51772
- if (!chatMessagesElement)
51773
- return;
51774
- // Check if this is a new message (scroll height increased)
51775
- const previousScrollHeight = lastScrollHeightRef.current;
51776
- const currentScrollHeight = chatMessagesElement.scrollHeight;
51777
- const hasNewContent = currentScrollHeight > previousScrollHeight;
51778
- const wasAtBottomBeforeNewContent = chatMessagesElement.scrollTop + chatMessagesElement.clientHeight >=
51779
- previousScrollHeight - bottomThreshold;
51780
- lastScrollHeightRef.current = currentScrollHeight;
51781
- if (!hasNewContent)
51782
- return;
51783
- // Only auto-scroll if user does NOT have a selection inside chat container
51784
- const selection = window.getSelection();
51785
- let hasSelectionInChat = false;
51786
- if (selection && selection.rangeCount > 0) {
51787
- const range = selection.getRangeAt(0);
51788
- if (chatMessagesElement.contains(range.startContainer) ||
51789
- chatMessagesElement.contains(range.endContainer)) {
51790
- if (!selection.isCollapsed) {
51791
- hasSelectionInChat = true;
51792
- }
51793
- }
51794
- }
51795
- if (isAutoScrolling && wasAtBottomBeforeNewContent && !hasSelectionInChat && !hasManualScrollRef.current) {
51796
- if (messagesChangeTimeoutRef.current) {
51797
- clearTimeout(messagesChangeTimeoutRef.current);
51798
- }
51799
- // Delay scroll slightly to ensure DOM has updated
51800
- messagesChangeTimeoutRef.current = setTimeout(() => {
51801
- if (hasManualScrollRef.current) {
51802
- return;
51803
- }
51804
- scrollToBottom(isStreaming ? 'auto' : 'smooth');
51805
- }, isStreaming ? 10 : scrollCheckDelay);
51806
- }
51807
- }, [bottomThreshold, isAutoScrolling, scrollToBottom, scrollCheckDelay]);
51808
- // Ref callback for chat messages container
51809
- const chatMessagesRefCallback = useCallback((element) => {
51810
- chatMessagesRef.current = element;
51811
- if (element) {
51812
- // Update last scroll height
51813
- lastScrollHeightRef.current = element.scrollHeight;
51814
- // If auto-scrolling is enabled, scroll to bottom
51815
- if (isAutoScrolling) {
51816
- // Use requestAnimationFrame for smoother initial scroll
51817
- requestAnimationFrame(() => {
51818
- scrollToBottom('auto');
51819
- });
51820
- }
51821
- }
51822
- }, [isAutoScrolling, scrollToBottom]);
51823
- // Manual scroll to bottom (for button click)
51824
- const handleScrollToBottomClick = useCallback(() => {
51825
- setIsAutoScrolling(true);
51826
- scrollToBottom('smooth');
51827
- }, [scrollToBottom]);
51828
- // Force auto-scroll back on (useful for programmatic control)
51829
- const enableAutoScroll = useCallback(() => {
53917
+ const handleScroll = useChatScrollHandler({
53918
+ checkIfAtBottom,
53919
+ hasManualScrollRef,
53920
+ isAutoScrolling,
53921
+ scrollTimeoutRef,
53922
+ setIsAutoScrolling,
53923
+ });
53924
+ const handleMessagesChange = useChatMessagesChangeHandler({
53925
+ bottomThreshold,
53926
+ chatMessagesRef,
53927
+ hasManualScrollRef,
53928
+ isAutoScrolling,
53929
+ lastScrollHeightRef,
53930
+ messagesChangeTimeoutRef,
53931
+ scrollCheckDelay,
53932
+ scrollToBottom,
53933
+ });
53934
+ const chatMessagesRefCallback = useChatMessagesRefCallback({
53935
+ chatMessagesRef,
53936
+ isAutoScrolling,
53937
+ lastScrollHeightRef,
53938
+ scrollToBottom,
53939
+ });
53940
+ const activateAutoScroll = useCallback(() => {
51830
53941
  setIsAutoScrolling(true);
51831
53942
  scrollToBottom('smooth');
51832
53943
  }, [scrollToBottom]);
51833
- // Disable auto-scroll (useful for programmatic control)
51834
53944
  const disableAutoScroll = useCallback(() => {
51835
53945
  setIsAutoScrolling(false);
51836
53946
  }, []);
51837
- // Cleanup timeout on unmount
51838
53947
  useEffect(() => {
51839
53948
  return () => {
51840
- if (scrollTimeoutRef.current) {
51841
- clearTimeout(scrollTimeoutRef.current);
51842
- }
51843
- if (messagesChangeTimeoutRef.current) {
51844
- clearTimeout(messagesChangeTimeoutRef.current);
51845
- }
53949
+ clearScheduledTimeout(scrollTimeoutRef);
53950
+ clearScheduledTimeout(messagesChangeTimeoutRef);
51846
53951
  };
51847
53952
  }, []);
51848
53953
  return {
@@ -51850,8 +53955,8 @@ function useChatAutoScroll(config = {}) {
51850
53955
  chatMessagesRef: chatMessagesRefCallback,
51851
53956
  handleScroll,
51852
53957
  handleMessagesChange,
51853
- scrollToBottom: handleScrollToBottomClick,
51854
- enableAutoScroll,
53958
+ scrollToBottom: activateAutoScroll,
53959
+ enableAutoScroll: activateAutoScroll,
51855
53960
  disableAutoScroll,
51856
53961
  isMobile,
51857
53962
  };