@runtypelabs/persona 4.10.0 → 4.11.0

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 (61) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-4ROVJ1gA.d.cts → types-DveIaNx6.d.cts} +11 -316
  4. package/dist/animations/{types-4ROVJ1gA.d.ts → types-DveIaNx6.d.ts} +11 -316
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/chunk-MMUPR2JW.js +1 -0
  8. package/dist/codegen.cjs +1 -1
  9. package/dist/codegen.js +1 -1
  10. package/dist/{context-mentions-7S5KVUTG.js → context-mentions-ONG7A7M6.js} +1 -1
  11. package/dist/context-mentions.d.cts +35 -316
  12. package/dist/context-mentions.d.ts +35 -316
  13. package/dist/index.cjs +67 -67
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +40 -317
  16. package/dist/index.d.ts +40 -317
  17. package/dist/index.global.js +55 -55
  18. package/dist/index.global.js.map +1 -1
  19. package/dist/index.js +78 -78
  20. package/dist/index.js.map +1 -1
  21. package/dist/smart-dom-reader.d.cts +35 -316
  22. package/dist/smart-dom-reader.d.ts +35 -316
  23. package/dist/theme-editor-preview.cjs +66 -66
  24. package/dist/theme-editor-preview.d.cts +35 -316
  25. package/dist/theme-editor-preview.d.ts +35 -316
  26. package/dist/theme-editor-preview.js +55 -55
  27. package/dist/theme-editor.cjs +5 -5
  28. package/dist/theme-editor.d.cts +35 -316
  29. package/dist/theme-editor.d.ts +35 -316
  30. package/dist/theme-editor.js +9 -9
  31. package/dist/widget.css +1 -1
  32. package/package.json +1 -1
  33. package/src/components/approval-bubble.ts +0 -1
  34. package/src/components/composer-parts.ts +19 -19
  35. package/src/components/context-mention-button.test.ts +5 -1
  36. package/src/components/context-mention-button.ts +6 -4
  37. package/src/components/header-builder.test.ts +3 -3
  38. package/src/components/header-parts.ts +14 -108
  39. package/src/components/message-bubble.test.ts +49 -0
  40. package/src/components/message-bubble.ts +8 -7
  41. package/src/components/reasoning-bubble.ts +0 -2
  42. package/src/components/tool-bubble.ts +0 -2
  43. package/src/generated/runtype-openapi-contract.ts +11 -317
  44. package/src/index-core.ts +7 -1
  45. package/src/runtime/host-layout.test.ts +94 -0
  46. package/src/runtime/host-layout.ts +9 -1
  47. package/src/runtime/init.test.ts +33 -0
  48. package/src/runtime/init.ts +4 -0
  49. package/src/styles/widget.css +118 -58
  50. package/src/theme-editor/sections.test.ts +33 -0
  51. package/src/theme-editor/sections.ts +10 -1
  52. package/src/types.ts +25 -0
  53. package/src/ui.attachments-drop.test.ts +2 -1
  54. package/src/ui.message-width.test.ts +407 -0
  55. package/src/ui.mount-fill-min-width.test.ts +81 -0
  56. package/src/ui.ts +158 -238
  57. package/src/utils/table-scroll-fade.test.ts +123 -0
  58. package/src/utils/table-scroll-fade.ts +75 -0
  59. package/src/utils/tooltip.test.ts +226 -0
  60. package/src/utils/tooltip.ts +245 -0
  61. package/dist/chunk-IPVK3KOM.js +0 -1
package/src/ui.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { escapeHtml, createMarkdownProcessorFromConfig } from "./postprocessors";
2
2
  import { resolveSanitizer } from "./utils/sanitize";
3
3
  import { stabilizeStreamingTables } from "./utils/streaming-table";
4
+ import { wrapScrollableTables, refreshTableScrollFades } from "./utils/table-scroll-fade";
4
5
  import { onMarkdownParsersReady, getMarkdownParsersSync } from "./markdown-parsers-loader";
5
6
  import { AgentWidgetSession, AgentWidgetSessionStatus } from "./session";
6
7
  import {
@@ -45,7 +46,8 @@ import { createTextPart, ALL_SUPPORTED_MIME_TYPES } from "./utils/content";
45
46
  import { applyThemeVariables, createThemeObserver, getActiveTheme } from "./utils/theme";
46
47
  import { resolveTokenValue } from "./utils/tokens";
47
48
  import { renderLucideIcon } from "./utils/icons";
48
- import { createElement, createElementInDocument } from "./utils/dom";
49
+ import { createElement } from "./utils/dom";
50
+ import { attachTooltip } from "./utils/tooltip";
49
51
  import { downloadInfoFor } from "./utils/artifact-file";
50
52
  import { artifactCopyText } from "./components/artifact-preview";
51
53
  import { morphMessages } from "./utils/morph";
@@ -66,7 +68,7 @@ import {
66
68
  resolveFollowStateFromScroll,
67
69
  resolveFollowStateFromWheel
68
70
  } from "./utils/auto-follow";
69
- import { statusCopy, DEFAULT_OVERLAY_Z_INDEX, PORTALED_OVERLAY_Z_INDEX } from "./utils/constants";
71
+ import { statusCopy, DEFAULT_OVERLAY_Z_INDEX } from "./utils/constants";
70
72
  import {
71
73
  applyStreamBuffer,
72
74
  createSkeletonPlaceholder,
@@ -2965,6 +2967,17 @@ export const createAgentExperience = (
2965
2967
  body.style.display = "none";
2966
2968
  }
2967
2969
 
2970
+ // Shrinkable baseline for every fill mount (launcher-off embed OR fullHeight/
2971
+ // docked/sidebar), applied outside the fullHeight branch so the launcher-off,
2972
+ // no-fullHeight embed keeps it too: the cssText reset above wipes the min-width
2973
+ // the host layer set, and without it a wide artifact split isn't forced to
2974
+ // shrink within the mount. In practice the column host's own min-width:0 is
2975
+ // what caps container overflow (the mount is a cross-axis child), but the
2976
+ // baseline is kept here so intent is consistent across every fill mode.
2977
+ if (fullHeight || isInlineEmbed) {
2978
+ mount.style.minWidth = "0";
2979
+ }
2980
+
2968
2981
  const restoreBodyScrollTop = (): void => {
2969
2982
  if (prevBodyScrollTop <= 0) return;
2970
2983
  const ownerWindow = body.ownerDocument.defaultView ?? window;
@@ -3131,6 +3144,8 @@ export const createAgentExperience = (
3131
3144
  mount.style.flexDirection = 'column';
3132
3145
  mount.style.height = '100%';
3133
3146
  mount.style.minHeight = '0';
3147
+ // min-width:0 baseline is applied above (outside this branch) for every
3148
+ // fill mount, including the launcher-off, no-fullHeight embed.
3134
3149
  if (isInlineEmbed) {
3135
3150
  mount.style.width = '100%';
3136
3151
  }
@@ -4158,6 +4173,46 @@ export const createAgentExperience = (
4158
4173
 
4159
4174
 
4160
4175
  // Message rendering with plugin support (implementation)
4176
+ const applyMessageRowLayout = (
4177
+ wrapper: HTMLElement,
4178
+ role: AgentWidgetMessage["role"]
4179
+ ): void => {
4180
+ const sizingRole = role === "user" ? "user" : "assistant";
4181
+ const roleLayout = config.layout?.messages?.[sizingRole];
4182
+ const width = roleLayout?.width ?? "content";
4183
+ const maxWidth =
4184
+ roleLayout?.maxWidth ?? (width === "full" ? "100%" : "85%");
4185
+
4186
+ wrapper.classList.add("persona-message-row");
4187
+ wrapper.classList.remove(
4188
+ "persona-message-row-user",
4189
+ "persona-message-row-assistant",
4190
+ "persona-message-row-system",
4191
+ "persona-message-width-content",
4192
+ "persona-message-width-full"
4193
+ );
4194
+ wrapper.classList.add(
4195
+ `persona-message-row-${role}`,
4196
+ `persona-message-width-${width}`
4197
+ );
4198
+ wrapper.classList.toggle("persona-justify-end", role === "user");
4199
+ wrapper.setAttribute("data-message-role", role);
4200
+ wrapper.setAttribute("data-message-width", width);
4201
+ wrapper.style.setProperty("--persona-message-row-max-width", maxWidth);
4202
+ };
4203
+
4204
+ const createMessageRow = (
4205
+ id: string,
4206
+ role: AgentWidgetMessage["role"]
4207
+ ): HTMLElement => {
4208
+ const wrapper = document.createElement("div");
4209
+ wrapper.className = "persona-flex";
4210
+ wrapper.id = `wrapper-${id}`;
4211
+ wrapper.setAttribute("data-wrapper-id", id);
4212
+ applyMessageRowLayout(wrapper, role);
4213
+ return wrapper;
4214
+ };
4215
+
4161
4216
  const renderMessagesWithPluginsImpl = (
4162
4217
  container: HTMLElement,
4163
4218
  messages: AgentWidgetMessage[],
@@ -4199,6 +4254,7 @@ export const createAgentExperience = (
4199
4254
 
4200
4255
  // Track active message IDs for cache pruning
4201
4256
  const activeMessageIds = new Set<string>();
4257
+ const messageRolesById = new Map<string, AgentWidgetMessage["role"]>();
4202
4258
  // Track ask_user_question tool-call ids whose bubbles were rendered this
4203
4259
  // pass: used to prune stale sheets from the composer overlay afterward.
4204
4260
  const liveAskToolIds = new Set<string>();
@@ -4252,6 +4308,7 @@ export const createAgentExperience = (
4252
4308
 
4253
4309
  messages.forEach((message) => {
4254
4310
  activeMessageIds.add(message.id);
4311
+ messageRolesById.set(message.id, message.role);
4255
4312
 
4256
4313
  const askWithPlugin = hasAskPlugin && isAskUserQuestionMessage(message);
4257
4314
  const approvalWithPlugin =
@@ -4431,10 +4488,7 @@ export const createAgentExperience = (
4431
4488
 
4432
4489
  // Append a stub wrapper for the morph pass; hydrate the real bubble
4433
4490
  // into it post-morph so its event listeners survive.
4434
- const stub = document.createElement("div");
4435
- stub.className = "persona-flex";
4436
- stub.id = `wrapper-${message.id}`;
4437
- stub.setAttribute("data-wrapper-id", message.id);
4491
+ const stub = createMessageRow(message.id, message.role);
4438
4492
  stub.setAttribute("data-ask-plugin-stub", "true");
4439
4493
  stub.setAttribute("data-preserve-runtime", "true");
4440
4494
  tempContainer.appendChild(stub);
@@ -4509,10 +4563,7 @@ export const createAgentExperience = (
4509
4563
  } else {
4510
4564
  // A fresh live bubble to hydrate (needsRebuild), or fingerprint
4511
4565
  // unchanged so we reuse the preserved live wrapper (`bubble: null`).
4512
- const stub = document.createElement("div");
4513
- stub.className = "persona-flex";
4514
- stub.id = `wrapper-${message.id}`;
4515
- stub.setAttribute("data-wrapper-id", message.id);
4566
+ const stub = createMessageRow(message.id, message.role);
4516
4567
  stub.setAttribute("data-approval-plugin-stub", "true");
4517
4568
  stub.setAttribute("data-preserve-runtime", "true");
4518
4569
  tempContainer.appendChild(stub);
@@ -4629,7 +4680,6 @@ export const createAgentExperience = (
4629
4680
  const componentWrapper = document.createElement("div");
4630
4681
  componentWrapper.className = [
4631
4682
  "persona-message-bubble",
4632
- "persona-max-w-[85%]",
4633
4683
  "persona-rounded-2xl",
4634
4684
  "persona-bg-persona-surface",
4635
4685
  "persona-border",
@@ -4685,15 +4735,9 @@ export const createAgentExperience = (
4685
4735
  // Otherwise fall through to the standard render path so the message
4686
4736
  // text is at least visible.
4687
4737
  if (liveBubble || lastFp != null) {
4688
- const stub = document.createElement("div");
4689
- stub.className = "persona-flex";
4690
- stub.id = `wrapper-${message.id}`;
4691
- stub.setAttribute("data-wrapper-id", message.id);
4738
+ const stub = createMessageRow(message.id, message.role);
4692
4739
  stub.setAttribute("data-component-directive-stub", "true");
4693
4740
  stub.setAttribute("data-preserve-runtime", "true");
4694
- if (!wrapChrome) {
4695
- stub.classList.add("persona-w-full");
4696
- }
4697
4741
  tempContainer.appendChild(stub);
4698
4742
  componentDirectiveHydrate.push({
4699
4743
  messageId: message.id,
@@ -4750,17 +4794,7 @@ export const createAgentExperience = (
4750
4794
  }
4751
4795
  }
4752
4796
 
4753
- const wrapper = document.createElement("div");
4754
- wrapper.className = "persona-flex";
4755
- // Set id for idiomorph matching
4756
- wrapper.id = `wrapper-${message.id}`;
4757
- wrapper.setAttribute("data-wrapper-id", message.id);
4758
- if (message.role === "user") {
4759
- wrapper.classList.add("persona-justify-end");
4760
- }
4761
- if (bubble?.getAttribute("data-persona-component-directive") === "true") {
4762
- wrapper.classList.add("persona-w-full");
4763
- }
4797
+ const wrapper = createMessageRow(message.id, message.role);
4764
4798
  wrapper.appendChild(bubble);
4765
4799
  setCachedWrapper(messageCache, message.id, fingerprint, wrapper);
4766
4800
  tempContainer.appendChild(wrapper);
@@ -4819,10 +4853,11 @@ export const createAgentExperience = (
4819
4853
  return;
4820
4854
  }
4821
4855
 
4822
- const groupWrapper = document.createElement("div");
4823
- groupWrapper.className = "persona-flex";
4824
- groupWrapper.id = `wrapper-tool-group-${groupIndex}-${group[0].id}`;
4825
- groupWrapper.setAttribute("data-wrapper-id", `tool-group-${groupIndex}-${group[0].id}`);
4856
+ const groupWrapper = createMessageRow(
4857
+ `tool-group-${groupIndex}-${group[0].id}`,
4858
+ "assistant"
4859
+ );
4860
+ groupWrapper.setAttribute("data-persona-tool-group-row", "true");
4826
4861
 
4827
4862
  const groupContainer = document.createElement("div");
4828
4863
  groupContainer.className =
@@ -4862,6 +4897,10 @@ export const createAgentExperience = (
4862
4897
  wrapper.remove();
4863
4898
  return;
4864
4899
  }
4900
+ // The outer group row owns role sizing. Inner message rows fill that
4901
+ // resolved track instead of applying the role max-width a second
4902
+ // time (which would turn an 85% cap into 72.25%).
4903
+ wrapper.style.setProperty("--persona-message-row-max-width", "100%");
4865
4904
  const item = document.createElement("div");
4866
4905
  item.className = "persona-tool-group-item persona-relative";
4867
4906
  item.setAttribute("data-persona-tool-group-item", "true");
@@ -4925,7 +4964,6 @@ export const createAgentExperience = (
4925
4964
  const showBubble = config.loadingIndicator?.showBubble !== false; // default true
4926
4965
  typingBubble.className = showBubble
4927
4966
  ? [
4928
- "persona-max-w-[85%]",
4929
4967
  "persona-rounded-2xl",
4930
4968
  "persona-text-sm",
4931
4969
  "persona-leading-relaxed",
@@ -4937,7 +4975,6 @@ export const createAgentExperience = (
4937
4975
  "persona-py-3"
4938
4976
  ].join(" ")
4939
4977
  : [
4940
- "persona-max-w-[85%]",
4941
4978
  "persona-text-sm",
4942
4979
  "persona-leading-relaxed",
4943
4980
  "persona-text-persona-primary"
@@ -4947,11 +4984,10 @@ export const createAgentExperience = (
4947
4984
 
4948
4985
  typingBubble.appendChild(typingIndicator);
4949
4986
 
4950
- const typingWrapper = document.createElement("div");
4951
- typingWrapper.className = "persona-flex";
4952
- // Set id for idiomorph matching
4953
- typingWrapper.id = "wrapper-typing-indicator";
4954
- typingWrapper.setAttribute("data-wrapper-id", "typing-indicator");
4987
+ const typingWrapper = createMessageRow(
4988
+ "typing-indicator",
4989
+ "assistant"
4990
+ );
4955
4991
  typingWrapper.appendChild(typingBubble);
4956
4992
  tempContainer.appendChild(typingWrapper);
4957
4993
  }
@@ -4989,7 +5025,6 @@ export const createAgentExperience = (
4989
5025
  const showBubble = config.loadingIndicator?.showBubble !== false; // default true
4990
5026
  idleBubble.className = showBubble
4991
5027
  ? [
4992
- "persona-max-w-[85%]",
4993
5028
  "persona-rounded-2xl",
4994
5029
  "persona-text-sm",
4995
5030
  "persona-leading-relaxed",
@@ -5002,7 +5037,6 @@ export const createAgentExperience = (
5002
5037
  "persona-py-3"
5003
5038
  ].join(" ")
5004
5039
  : [
5005
- "persona-max-w-[85%]",
5006
5040
  "persona-text-sm",
5007
5041
  "persona-leading-relaxed",
5008
5042
  "persona-text-persona-primary"
@@ -5011,27 +5045,35 @@ export const createAgentExperience = (
5011
5045
 
5012
5046
  idleBubble.appendChild(idleIndicator);
5013
5047
 
5014
- const idleWrapper = document.createElement("div");
5015
- idleWrapper.className = "persona-flex";
5016
- // Set id for idiomorph matching
5017
- idleWrapper.id = "wrapper-idle-indicator";
5018
- idleWrapper.setAttribute("data-wrapper-id", "idle-indicator");
5048
+ const idleWrapper = createMessageRow("idle-indicator", "assistant");
5019
5049
  idleWrapper.appendChild(idleBubble);
5020
5050
  tempContainer.appendChild(idleWrapper);
5021
5051
  }
5022
5052
  }
5023
5053
 
5054
+ // Wrap wide tables in a horizontal-scroll container before morphing so the
5055
+ // wrapper exists on both sides of the diff (survives streaming re-renders).
5056
+ wrapScrollableTables(tempContainer);
5057
+
5024
5058
  // Use idiomorph to morph the container contents
5025
5059
  morphMessages(container, tempContainer);
5026
5060
 
5061
+ // Set initial edge-fade state on the live table wrappers and attach the
5062
+ // delegated scroll listener (once) that keeps the fades in sync.
5063
+ refreshTableScrollFades(container);
5064
+
5027
5065
  // Hydrate plugin-rendered ask-question bubbles into their stub wrappers.
5028
5066
  // Idiomorph imports new nodes via `document.importNode`, which strips
5029
5067
  // listeners, so we built only an empty stub during morph and now inject
5030
5068
  // the real, listener-bearing bubble directly into the live DOM.
5031
5069
  if (askPluginHydrate.length > 0) {
5032
5070
  for (const { messageId, fingerprint, bubble } of askPluginHydrate) {
5033
- const wrapper = container.querySelector(`#wrapper-${messageId}`);
5071
+ const wrapper = container.querySelector<HTMLElement>(`#wrapper-${messageId}`);
5034
5072
  if (!wrapper) continue;
5073
+ applyMessageRowLayout(
5074
+ wrapper,
5075
+ messageRolesById.get(messageId) ?? "assistant"
5076
+ );
5035
5077
  if (bubble === null) {
5036
5078
  // No fresh bubble built this pass: either the plugin opted out
5037
5079
  // and a previously-mounted bubble already lives here (preserved by
@@ -5057,8 +5099,12 @@ export const createAgentExperience = (
5057
5099
  // the ask-question hydration above.
5058
5100
  if (componentDirectiveHydrate.length > 0) {
5059
5101
  for (const { messageId, fingerprint, bubble } of componentDirectiveHydrate) {
5060
- const wrapper = container.querySelector(`#wrapper-${messageId}`);
5102
+ const wrapper = container.querySelector<HTMLElement>(`#wrapper-${messageId}`);
5061
5103
  if (!wrapper) continue;
5104
+ applyMessageRowLayout(
5105
+ wrapper,
5106
+ messageRolesById.get(messageId) ?? "assistant"
5107
+ );
5062
5108
  if (bubble === null) {
5063
5109
  // Fingerprint matched the previous pass: the live wrapper (kept
5064
5110
  // alive by `data-preserve-runtime`) still holds the listener-bearing
@@ -5081,8 +5127,12 @@ export const createAgentExperience = (
5081
5127
  // mirroring the ask-question / component-directive hydration above.
5082
5128
  if (approvalPluginHydrate.length > 0) {
5083
5129
  for (const { messageId, fingerprint, bubble } of approvalPluginHydrate) {
5084
- const wrapper = container.querySelector(`#wrapper-${messageId}`);
5130
+ const wrapper = container.querySelector<HTMLElement>(`#wrapper-${messageId}`);
5085
5131
  if (!wrapper) continue;
5132
+ applyMessageRowLayout(
5133
+ wrapper,
5134
+ messageRolesById.get(messageId) ?? "assistant"
5135
+ );
5086
5136
  if (bubble === null) {
5087
5137
  // Fingerprint matched the previous pass (or the plugin opted out
5088
5138
  // after a prior render): the live wrapper, kept alive by
@@ -6776,7 +6826,8 @@ export const createAgentExperience = (
6776
6826
  ) as HTMLButtonElement;
6777
6827
 
6778
6828
  micButton.type = "button";
6779
- micButton.setAttribute("aria-label", "Start voice recognition");
6829
+ const tooltipText = voiceConfig?.tooltipText ?? "Start voice recognition";
6830
+ micButton.setAttribute("aria-label", tooltipText);
6780
6831
 
6781
6832
  const micIconName = voiceConfig?.iconName ?? "mic";
6782
6833
  const buttonSize = sendButtonConfig?.size ?? "40px";
@@ -6837,15 +6888,13 @@ export const createAgentExperience = (
6837
6888
  }
6838
6889
 
6839
6890
  micButtonWrapper.appendChild(micButton);
6840
-
6841
- // Add tooltip if enabled
6842
- const tooltipText = voiceConfig?.tooltipText ?? "Start voice recognition";
6843
6891
  const showTooltip = voiceConfig?.showTooltip ?? false;
6844
- if (showTooltip && tooltipText) {
6845
- const tooltip = createElement("div", "persona-send-button-tooltip");
6846
- tooltip.textContent = tooltipText;
6847
- micButtonWrapper.appendChild(tooltip);
6848
- }
6892
+ attachTooltip({
6893
+ anchor: micButton,
6894
+ trigger: micButtonWrapper,
6895
+ text: () => micButton.getAttribute("aria-label") ?? tooltipText,
6896
+ enabled: showTooltip,
6897
+ });
6849
6898
 
6850
6899
  return { micButton, micButtonWrapper };
6851
6900
  };
@@ -7360,6 +7409,9 @@ export const createAgentExperience = (
7360
7409
  if (typeof ResizeObserver !== "undefined") {
7361
7410
  const contentResizeObserver = new ResizeObserver(() => {
7362
7411
  handleContentResize();
7412
+ // A resize can start or stop a table overflowing without a render or a
7413
+ // scroll, which are the only other things that recompute the fades.
7414
+ refreshTableScrollFades(messagesWrapper);
7363
7415
  });
7364
7416
  contentResizeObserver.observe(messagesWrapper);
7365
7417
  contentResizeObserver.observe(body);
@@ -8185,76 +8237,13 @@ export const createAgentExperience = (
8185
8237
  closeButton.setAttribute("aria-label", closeButtonTooltipText);
8186
8238
 
8187
8239
  if (closeButtonWrapper) {
8188
- // Clean up old tooltip event listeners if they exist
8189
- if ((closeButtonWrapper as any)._cleanupTooltip) {
8190
- (closeButtonWrapper as any)._cleanupTooltip();
8191
- delete (closeButtonWrapper as any)._cleanupTooltip;
8192
- }
8193
-
8194
- // Set up new portaled tooltip with event listeners
8195
- if (closeButtonShowTooltip && closeButtonTooltipText) {
8196
- let portaledTooltip: HTMLElement | null = null;
8197
-
8198
- const showTooltip = () => {
8199
- if (portaledTooltip || !closeButton) return; // Already showing or button doesn't exist
8200
-
8201
- const tooltipDocument = closeButton.ownerDocument;
8202
- const tooltipContainer = tooltipDocument.body;
8203
- if (!tooltipContainer) return;
8204
-
8205
- // Create tooltip element
8206
- portaledTooltip = createElementInDocument(
8207
- tooltipDocument,
8208
- "div",
8209
- "persona-clear-chat-tooltip"
8210
- );
8211
- portaledTooltip.textContent = closeButtonTooltipText;
8212
-
8213
- // Add arrow
8214
- const arrow = createElementInDocument(tooltipDocument, "div");
8215
- arrow.className = "persona-clear-chat-tooltip-arrow";
8216
- portaledTooltip.appendChild(arrow);
8217
-
8218
- // Get button position
8219
- const buttonRect = closeButton.getBoundingClientRect();
8220
-
8221
- // Position tooltip above button
8222
- portaledTooltip.style.position = "fixed";
8223
- portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
8224
- portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
8225
- portaledTooltip.style.top = `${buttonRect.top - 8}px`;
8226
- portaledTooltip.style.transform = "translate(-50%, -100%)";
8227
-
8228
- // Append to body
8229
- tooltipContainer.appendChild(portaledTooltip);
8230
- };
8231
-
8232
- const hideTooltip = () => {
8233
- if (portaledTooltip && portaledTooltip.parentNode) {
8234
- portaledTooltip.parentNode.removeChild(portaledTooltip);
8235
- portaledTooltip = null;
8236
- }
8237
- };
8238
-
8239
- // Add event listeners
8240
- closeButtonWrapper.addEventListener("mouseenter", showTooltip);
8241
- closeButtonWrapper.addEventListener("mouseleave", hideTooltip);
8242
- closeButton.addEventListener("focus", showTooltip);
8243
- closeButton.addEventListener("blur", hideTooltip);
8244
-
8245
- // Store cleanup function on the wrapper for later use
8246
- (closeButtonWrapper as any)._cleanupTooltip = () => {
8247
- hideTooltip();
8248
- if (closeButtonWrapper) {
8249
- closeButtonWrapper.removeEventListener("mouseenter", showTooltip);
8250
- closeButtonWrapper.removeEventListener("mouseleave", hideTooltip);
8251
- }
8252
- if (closeButton) {
8253
- closeButton.removeEventListener("focus", showTooltip);
8254
- closeButton.removeEventListener("blur", hideTooltip);
8255
- }
8256
- };
8257
- }
8240
+ attachTooltip({
8241
+ anchor: closeButton,
8242
+ trigger: closeButtonWrapper,
8243
+ text: () =>
8244
+ closeButton?.getAttribute("aria-label") ?? closeButtonTooltipText,
8245
+ enabled: closeButtonShowTooltip,
8246
+ });
8258
8247
  }
8259
8248
  }
8260
8249
 
@@ -8420,76 +8409,14 @@ export const createAgentExperience = (
8420
8409
  clearChatButton.setAttribute("aria-label", clearChatTooltipText);
8421
8410
 
8422
8411
  if (clearChatButtonWrapper) {
8423
- // Clean up old tooltip event listeners if they exist
8424
- if ((clearChatButtonWrapper as any)._cleanupTooltip) {
8425
- (clearChatButtonWrapper as any)._cleanupTooltip();
8426
- delete (clearChatButtonWrapper as any)._cleanupTooltip;
8427
- }
8428
-
8429
- // Set up new portaled tooltip with event listeners
8430
- if (clearChatShowTooltip && clearChatTooltipText) {
8431
- let portaledTooltip: HTMLElement | null = null;
8432
-
8433
- const showTooltip = () => {
8434
- if (portaledTooltip || !clearChatButton) return; // Already showing or button doesn't exist
8435
-
8436
- const tooltipDocument = clearChatButton.ownerDocument;
8437
- const tooltipContainer = tooltipDocument.body;
8438
- if (!tooltipContainer) return;
8439
-
8440
- // Create tooltip element
8441
- portaledTooltip = createElementInDocument(
8442
- tooltipDocument,
8443
- "div",
8444
- "persona-clear-chat-tooltip"
8445
- );
8446
- portaledTooltip.textContent = clearChatTooltipText;
8447
-
8448
- // Add arrow
8449
- const arrow = createElementInDocument(tooltipDocument, "div");
8450
- arrow.className = "persona-clear-chat-tooltip-arrow";
8451
- portaledTooltip.appendChild(arrow);
8452
-
8453
- // Get button position
8454
- const buttonRect = clearChatButton.getBoundingClientRect();
8455
-
8456
- // Position tooltip above button
8457
- portaledTooltip.style.position = "fixed";
8458
- portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
8459
- portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
8460
- portaledTooltip.style.top = `${buttonRect.top - 8}px`;
8461
- portaledTooltip.style.transform = "translate(-50%, -100%)";
8462
-
8463
- // Append to body
8464
- tooltipContainer.appendChild(portaledTooltip);
8465
- };
8466
-
8467
- const hideTooltip = () => {
8468
- if (portaledTooltip && portaledTooltip.parentNode) {
8469
- portaledTooltip.parentNode.removeChild(portaledTooltip);
8470
- portaledTooltip = null;
8471
- }
8472
- };
8473
-
8474
- // Add event listeners
8475
- clearChatButtonWrapper.addEventListener("mouseenter", showTooltip);
8476
- clearChatButtonWrapper.addEventListener("mouseleave", hideTooltip);
8477
- clearChatButton.addEventListener("focus", showTooltip);
8478
- clearChatButton.addEventListener("blur", hideTooltip);
8479
-
8480
- // Store cleanup function on the button for later use
8481
- (clearChatButtonWrapper as any)._cleanupTooltip = () => {
8482
- hideTooltip();
8483
- if (clearChatButtonWrapper) {
8484
- clearChatButtonWrapper.removeEventListener("mouseenter", showTooltip);
8485
- clearChatButtonWrapper.removeEventListener("mouseleave", hideTooltip);
8486
- }
8487
- if (clearChatButton) {
8488
- clearChatButton.removeEventListener("focus", showTooltip);
8489
- clearChatButton.removeEventListener("blur", hideTooltip);
8490
- }
8491
- };
8492
- }
8412
+ attachTooltip({
8413
+ anchor: clearChatButton,
8414
+ trigger: clearChatButtonWrapper,
8415
+ text: () =>
8416
+ clearChatButton?.getAttribute("aria-label") ??
8417
+ clearChatTooltipText,
8418
+ enabled: clearChatShowTooltip,
8419
+ });
8493
8420
  }
8494
8421
  }
8495
8422
  }
@@ -8628,24 +8555,16 @@ export const createAgentExperience = (
8628
8555
  micButton.style.paddingBottom = "";
8629
8556
  }
8630
8557
 
8631
- // Update tooltip
8632
- const tooltip = micButtonWrapper?.querySelector(".persona-send-button-tooltip") as HTMLElement | null;
8633
8558
  const tooltipText = voiceConfig.tooltipText ?? "Start voice recognition";
8634
8559
  const showTooltip = voiceConfig.showTooltip ?? false;
8635
- if (showTooltip && tooltipText) {
8636
- if (!tooltip) {
8637
- // Create tooltip if it doesn't exist
8638
- const newTooltip = document.createElement("div");
8639
- newTooltip.className = "persona-send-button-tooltip";
8640
- newTooltip.textContent = tooltipText;
8641
- micButtonWrapper?.insertBefore(newTooltip, micButton);
8642
- } else {
8643
- tooltip.textContent = tooltipText;
8644
- tooltip.style.display = "";
8645
- }
8646
- } else if (tooltip) {
8647
- // Hide tooltip if disabled
8648
- tooltip.style.display = "none";
8560
+ micButton.setAttribute("aria-label", tooltipText);
8561
+ if (micButtonWrapper) {
8562
+ attachTooltip({
8563
+ anchor: micButton,
8564
+ trigger: micButtonWrapper,
8565
+ text: () => micButton?.getAttribute("aria-label") ?? tooltipText,
8566
+ enabled: showTooltip,
8567
+ });
8649
8568
  }
8650
8569
 
8651
8570
  // Show and update disabled state
@@ -8737,11 +8656,13 @@ export const createAgentExperience = (
8737
8656
 
8738
8657
  attachmentButtonWrapper.appendChild(attachmentButton);
8739
8658
 
8740
- // Add tooltip
8741
8659
  const attachTooltipText = attachmentsConfig.buttonTooltipText ?? "Attach file";
8742
- const tooltip = createElement("div", "persona-send-button-tooltip");
8743
- tooltip.textContent = attachTooltipText;
8744
- attachmentButtonWrapper.appendChild(tooltip);
8660
+ attachTooltip({
8661
+ anchor: attachmentButton,
8662
+ trigger: attachmentButtonWrapper,
8663
+ text: () =>
8664
+ attachmentButton?.getAttribute("aria-label") ?? attachTooltipText,
8665
+ });
8745
8666
 
8746
8667
  // Insert into left actions container (fall back to the form when a
8747
8668
  // custom composer has no left cluster).
@@ -8803,10 +8724,14 @@ export const createAgentExperience = (
8803
8724
  );
8804
8725
  if (iconSvg) attachmentButton.appendChild(iconSvg);
8805
8726
  else attachmentButton.textContent = "📎";
8806
- const attachTooltip = attachmentButtonWrapper?.querySelector(
8807
- ".persona-send-button-tooltip"
8808
- );
8809
- if (attachTooltip) attachTooltip.textContent = tooltipText;
8727
+ if (attachmentButtonWrapper) {
8728
+ attachTooltip({
8729
+ anchor: attachmentButton,
8730
+ trigger: attachmentButtonWrapper,
8731
+ text: () =>
8732
+ attachmentButton?.getAttribute("aria-label") ?? tooltipText,
8733
+ });
8734
+ }
8810
8735
  }
8811
8736
  } else {
8812
8737
  // Hide attachment button if disabled
@@ -8935,21 +8860,16 @@ export const createAgentExperience = (
8935
8860
  sendButton.style.paddingBottom = "";
8936
8861
  }
8937
8862
 
8938
- // Update tooltip
8939
- const tooltip = sendButtonWrapper?.querySelector(".persona-send-button-tooltip") as HTMLElement | null;
8940
- if (showTooltip && tooltipText) {
8941
- if (!tooltip) {
8942
- // Create tooltip if it doesn't exist
8943
- const newTooltip = document.createElement("div");
8944
- newTooltip.className = "persona-send-button-tooltip";
8945
- newTooltip.textContent = tooltipText;
8946
- sendButtonWrapper?.insertBefore(newTooltip, sendButton);
8947
- } else {
8948
- tooltip.textContent = tooltipText;
8949
- tooltip.style.display = "";
8950
- }
8951
- } else if (tooltip) {
8952
- tooltip.style.display = "none";
8863
+ if (!session.isStreaming()) {
8864
+ sendButton.setAttribute("aria-label", tooltipText);
8865
+ }
8866
+ if (sendButtonWrapper) {
8867
+ attachTooltip({
8868
+ anchor: sendButton,
8869
+ trigger: sendButtonWrapper,
8870
+ text: () => sendButton.getAttribute("aria-label") ?? tooltipText,
8871
+ enabled: showTooltip,
8872
+ });
8953
8873
  }
8954
8874
 
8955
8875
  // Update contentMaxWidth on messages wrapper and composer. Same