@runtypelabs/persona 1.48.0 → 2.0.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 (69) hide show
  1. package/README.md +140 -8
  2. package/dist/index.cjs +90 -39
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1055 -24
  5. package/dist/index.d.ts +1055 -24
  6. package/dist/index.global.js +111 -60
  7. package/dist/index.global.js.map +1 -1
  8. package/dist/index.js +90 -39
  9. package/dist/index.js.map +1 -1
  10. package/dist/install.global.js +1 -1
  11. package/dist/install.global.js.map +1 -1
  12. package/dist/widget.css +836 -513
  13. package/package.json +1 -1
  14. package/src/artifacts-session.test.ts +80 -0
  15. package/src/client.test.ts +20 -21
  16. package/src/client.ts +153 -4
  17. package/src/components/approval-bubble.ts +45 -42
  18. package/src/components/artifact-card.ts +91 -0
  19. package/src/components/artifact-pane.ts +501 -0
  20. package/src/components/composer-builder.ts +32 -27
  21. package/src/components/event-stream-view.ts +40 -40
  22. package/src/components/feedback.ts +36 -36
  23. package/src/components/forms.ts +11 -11
  24. package/src/components/header-builder.test.ts +32 -0
  25. package/src/components/header-builder.ts +55 -36
  26. package/src/components/header-layouts.ts +58 -125
  27. package/src/components/launcher.ts +36 -21
  28. package/src/components/message-bubble.ts +92 -65
  29. package/src/components/messages.ts +2 -2
  30. package/src/components/panel.ts +42 -11
  31. package/src/components/reasoning-bubble.ts +23 -23
  32. package/src/components/registry.ts +4 -0
  33. package/src/components/suggestions.ts +1 -1
  34. package/src/components/tool-bubble.ts +32 -32
  35. package/src/defaults.ts +30 -4
  36. package/src/index.ts +80 -2
  37. package/src/install.ts +22 -0
  38. package/src/plugins/types.ts +23 -0
  39. package/src/postprocessors.ts +2 -2
  40. package/src/runtime/host-layout.ts +174 -0
  41. package/src/runtime/init.test.ts +236 -0
  42. package/src/runtime/init.ts +114 -55
  43. package/src/session.ts +135 -2
  44. package/src/styles/tailwind.css +1 -1
  45. package/src/styles/widget.css +836 -513
  46. package/src/types/theme.ts +354 -0
  47. package/src/types.ts +314 -15
  48. package/src/ui.docked.test.ts +104 -0
  49. package/src/ui.ts +940 -227
  50. package/src/utils/artifact-gate.test.ts +255 -0
  51. package/src/utils/artifact-gate.ts +142 -0
  52. package/src/utils/artifact-resize.test.ts +64 -0
  53. package/src/utils/artifact-resize.ts +67 -0
  54. package/src/utils/attachment-manager.ts +10 -10
  55. package/src/utils/code-generators.test.ts +52 -0
  56. package/src/utils/code-generators.ts +40 -36
  57. package/src/utils/dock.ts +17 -0
  58. package/src/utils/dom-context.test.ts +504 -0
  59. package/src/utils/dom-context.ts +896 -0
  60. package/src/utils/dom.ts +12 -1
  61. package/src/utils/message-fingerprint.test.ts +187 -0
  62. package/src/utils/message-fingerprint.ts +105 -0
  63. package/src/utils/migration.ts +179 -0
  64. package/src/utils/morph.ts +1 -1
  65. package/src/utils/plugins.ts +175 -0
  66. package/src/utils/positioning.ts +4 -4
  67. package/src/utils/theme.test.ts +125 -0
  68. package/src/utils/theme.ts +216 -60
  69. package/src/utils/tokens.ts +682 -0
package/src/ui.ts CHANGED
@@ -20,15 +20,19 @@ import {
20
20
  InjectSystemMessageOptions,
21
21
  LoadingIndicatorRenderContext,
22
22
  IdleIndicatorRenderContext,
23
- VoiceStatus
23
+ VoiceStatus,
24
+ PersonaArtifactRecord,
25
+ PersonaArtifactManualUpsert
24
26
  } from "./types";
25
27
  import { AttachmentManager } from "./utils/attachment-manager";
26
28
  import { createTextPart, ALL_SUPPORTED_MIME_TYPES } from "./utils/content";
27
29
  import { applyThemeVariables, createThemeObserver } from "./utils/theme";
28
30
  import { renderLucideIcon } from "./utils/icons";
29
- import { createElement } from "./utils/dom";
31
+ import { createElement, createElementInDocument } from "./utils/dom";
30
32
  import { morphMessages } from "./utils/morph";
33
+ import { computeMessageFingerprint, createMessageCache, getCachedWrapper, setCachedWrapper, pruneCache } from "./utils/message-fingerprint";
31
34
  import { statusCopy } from "./utils/constants";
35
+ import { isDockedMountMode } from "./utils/dock";
32
36
  import { createLauncherButton } from "./components/launcher";
33
37
  import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
34
38
  import { buildHeaderWithLayout } from "./components/header-layouts";
@@ -43,6 +47,14 @@ import { createSuggestions } from "./components/suggestions";
43
47
  import { EventStreamBuffer } from "./utils/event-stream-buffer";
44
48
  import { EventStreamStore } from "./utils/event-stream-store";
45
49
  import { createEventStreamView } from "./components/event-stream-view";
50
+ import { createArtifactPane, type ArtifactPaneApi } from "./components/artifact-pane";
51
+ import {
52
+ artifactsSidebarEnabled,
53
+ applyArtifactLayoutCssVars,
54
+ applyArtifactPaneAppearance,
55
+ shouldExpandLauncherForArtifacts
56
+ } from "./utils/artifact-gate";
57
+ import { readFlexGapPx, resolveArtifactPaneWidthPx } from "./utils/artifact-resize";
46
58
  import { enhanceWithForms } from "./components/forms";
47
59
  import { pluginRegistry } from "./plugins/registry";
48
60
  import { mergeWithDefaults } from "./defaults";
@@ -277,6 +289,14 @@ type Controller = {
277
289
  hideEventStream: () => void;
278
290
  /** Returns current visibility state of the event stream panel */
279
291
  isEventStreamVisible: () => boolean;
292
+ /** Show artifact sidebar (no-op if features.artifacts.enabled is false) */
293
+ showArtifacts: () => void;
294
+ /** Hide artifact sidebar */
295
+ hideArtifacts: () => void;
296
+ /** Upsert an artifact programmatically */
297
+ upsertArtifact: (manual: PersonaArtifactManualUpsert) => PersonaArtifactRecord | null;
298
+ selectArtifact: (id: string) => void;
299
+ clearArtifacts: () => void;
280
300
  /**
281
301
  * Focus the chat input. Returns true if focus succeeded, false if panel is closed
282
302
  * (launcher mode) or textarea is unavailable.
@@ -467,6 +487,7 @@ export const createAgentExperience = (
467
487
  let prevAutoExpand = autoExpand;
468
488
  let prevLauncherEnabled = launcherEnabled;
469
489
  let prevHeaderLayout = config.layout?.header?.layout;
490
+ let wasMobileFullscreen = false;
470
491
  let open = launcherEnabled ? autoExpand : true;
471
492
 
472
493
  // Track pending resubmit state for injection-triggered resubmit
@@ -591,20 +612,11 @@ export const createAgentExperience = (
591
612
  let attachmentInput: HTMLInputElement | null = panelElements.attachmentInput;
592
613
  let attachmentPreviewsContainer: HTMLElement | null = panelElements.attachmentPreviewsContainer;
593
614
 
594
- // Initialize attachment manager if attachments are enabled
615
+ // Initialized after composer plugins rebind footer DOM (see `bindComposerRefsFromFooter`)
595
616
  let attachmentManager: AttachmentManager | null = null;
596
- if (config.attachments?.enabled && attachmentInput && attachmentPreviewsContainer) {
597
- attachmentManager = AttachmentManager.fromConfig(config.attachments);
598
- attachmentManager.setPreviewsContainer(attachmentPreviewsContainer);
599
617
 
600
- // Wire up file input change event
601
- attachmentInput.addEventListener("change", (e) => {
602
- const target = e.target as HTMLInputElement;
603
- attachmentManager?.handleFileSelect(target.files);
604
- // Reset input so same file can be selected again
605
- target.value = "";
606
- });
607
- }
618
+ /** Wired after `handleMicButtonClick` is defined; used by `renderComposer` `onVoiceToggle`. */
619
+ let composerVoiceBridge: (() => void) | null = null;
608
620
 
609
621
  // Plugin hook: renderHeader - allow plugins to provide custom header
610
622
  const headerPlugin = plugins.find(p => p.renderHeader);
@@ -620,7 +632,7 @@ export const createAgentExperience = (
620
632
  });
621
633
  if (customHeader) {
622
634
  // Replace the default header with custom header
623
- const existingHeader = container.querySelector('.tvw-border-b-cw-divider');
635
+ const existingHeader = container.querySelector('.persona-border-b-persona-divider');
624
636
  if (existingHeader) {
625
637
  existingHeader.replaceWith(customHeader);
626
638
  header = customHeader;
@@ -647,9 +659,9 @@ export const createAgentExperience = (
647
659
  eventStreamView.update();
648
660
  }
649
661
  if (eventStreamToggleBtn) {
650
- eventStreamToggleBtn.classList.remove("tvw-text-cw-muted");
651
- eventStreamToggleBtn.classList.add("tvw-text-cw-accent");
652
- eventStreamToggleBtn.style.boxShadow = "inset 0 0 0 1.5px var(--cw-accent, #3b82f6)";
662
+ eventStreamToggleBtn.classList.remove("persona-text-persona-muted");
663
+ eventStreamToggleBtn.classList.add("persona-text-persona-accent");
664
+ eventStreamToggleBtn.style.boxShadow = "inset 0 0 0 1.5px var(--persona-accent, #3b82f6)";
653
665
  const activeClasses = config.features?.eventStream?.classNames?.toggleButtonActive;
654
666
  if (activeClasses) activeClasses.split(/\s+/).forEach(c => c && eventStreamToggleBtn!.classList.add(c));
655
667
  }
@@ -676,8 +688,8 @@ export const createAgentExperience = (
676
688
  }
677
689
  body.style.display = "";
678
690
  if (eventStreamToggleBtn) {
679
- eventStreamToggleBtn.classList.remove("tvw-text-cw-accent");
680
- eventStreamToggleBtn.classList.add("tvw-text-cw-muted");
691
+ eventStreamToggleBtn.classList.remove("persona-text-persona-accent");
692
+ eventStreamToggleBtn.classList.add("persona-text-persona-muted");
681
693
  eventStreamToggleBtn.style.boxShadow = "";
682
694
  const activeClasses = config.features?.eventStream?.classNames?.toggleButtonActive;
683
695
  if (activeClasses) activeClasses.split(/\s+/).forEach(c => c && eventStreamToggleBtn!.classList.remove(c));
@@ -694,7 +706,7 @@ export const createAgentExperience = (
694
706
  let eventStreamToggleBtn: HTMLButtonElement | null = null;
695
707
  if (showEventStreamToggle) {
696
708
  const esClassNames = config.features?.eventStream?.classNames;
697
- const toggleBtnClasses = "tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-text-cw-muted hover:tvw-bg-gray-100 tvw-cursor-pointer tvw-border-none tvw-bg-transparent tvw-p-1" + (esClassNames?.toggleButton ? " " + esClassNames.toggleButton : "");
709
+ const toggleBtnClasses = "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full persona-text-persona-muted hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none persona-bg-transparent persona-p-1" + (esClassNames?.toggleButton ? " " + esClassNames.toggleButton : "");
698
710
  eventStreamToggleBtn = createElement("button", toggleBtnClasses) as HTMLButtonElement;
699
711
  eventStreamToggleBtn.style.width = "28px";
700
712
  eventStreamToggleBtn.style.height = "28px";
@@ -723,9 +735,38 @@ export const createAgentExperience = (
723
735
  });
724
736
  }
725
737
 
738
+ const ensureComposerAttachmentSurface = (rootFooter: HTMLElement) => {
739
+ const att = config.attachments;
740
+ if (!att?.enabled) return;
741
+ let previews = rootFooter.querySelector<HTMLElement>(".persona-attachment-previews");
742
+ if (!previews) {
743
+ previews = createElement(
744
+ "div",
745
+ "persona-attachment-previews persona-flex persona-flex-wrap persona-gap-2 persona-mb-2"
746
+ );
747
+ previews.style.display = "none";
748
+ const form = rootFooter.querySelector("[data-persona-composer-form]");
749
+ if (form?.parentNode) {
750
+ form.parentNode.insertBefore(previews, form);
751
+ } else {
752
+ rootFooter.insertBefore(previews, rootFooter.firstChild);
753
+ }
754
+ }
755
+ if (!rootFooter.querySelector<HTMLInputElement>('input[type="file"]')) {
756
+ const fileIn = createElement("input") as HTMLInputElement;
757
+ fileIn.type = "file";
758
+ fileIn.accept = (att.allowedTypes ?? ALL_SUPPORTED_MIME_TYPES).join(",");
759
+ fileIn.multiple = (att.maxFiles ?? 4) > 1;
760
+ fileIn.style.display = "none";
761
+ fileIn.setAttribute("aria-label", att.buttonTooltipText ?? "Attach files");
762
+ rootFooter.appendChild(fileIn);
763
+ }
764
+ };
765
+
726
766
  // Plugin hook: renderComposer - allow plugins to provide custom composer
727
767
  const composerPlugin = plugins.find(p => p.renderComposer);
728
768
  if (composerPlugin?.renderComposer) {
769
+ const composerCfg = config.composer;
729
770
  const customComposer = composerPlugin.renderComposer({
730
771
  config,
731
772
  defaultRenderer: () => {
@@ -737,17 +778,79 @@ export const createAgentExperience = (
737
778
  session.sendMessage(text);
738
779
  }
739
780
  },
740
- disabled: false
781
+ streaming: false,
782
+ disabled: false,
783
+ openAttachmentPicker: () => {
784
+ attachmentInput?.click();
785
+ },
786
+ models: composerCfg?.models,
787
+ selectedModelId: composerCfg?.selectedModelId,
788
+ onModelChange: (modelId: string) => {
789
+ config.composer = { ...config.composer, selectedModelId: modelId };
790
+ },
791
+ onVoiceToggle:
792
+ config.voiceRecognition?.enabled === true
793
+ ? () => {
794
+ composerVoiceBridge?.();
795
+ }
796
+ : undefined
741
797
  });
742
798
  if (customComposer) {
743
799
  // Replace the default footer with custom composer
744
800
  footer.replaceWith(customComposer);
745
801
  footer = customComposer;
746
- // Note: When using custom composer, textarea/sendButton/etc may not exist
747
- // The plugin is responsible for providing its own submit handling
748
802
  }
749
803
  }
750
804
 
805
+ const bindComposerRefsFromFooter = (rootFooter: HTMLElement) => {
806
+ const form = rootFooter.querySelector<HTMLFormElement>("[data-persona-composer-form]");
807
+ const ta = rootFooter.querySelector<HTMLTextAreaElement>("[data-persona-composer-input]");
808
+ const sb = rootFooter.querySelector<HTMLButtonElement>("[data-persona-composer-submit]");
809
+ const mic = rootFooter.querySelector<HTMLButtonElement>("[data-persona-composer-mic]");
810
+ const st = rootFooter.querySelector<HTMLElement>("[data-persona-composer-status]");
811
+ if (form) composerForm = form;
812
+ if (ta) textarea = ta;
813
+ if (sb) sendButton = sb;
814
+ if (mic) {
815
+ micButton = mic;
816
+ micButtonWrapper = mic.parentElement as HTMLElement | null;
817
+ }
818
+ if (st) statusText = st;
819
+ const sug = rootFooter.querySelector<HTMLElement>(
820
+ ".persona-mb-3.persona-flex.persona-flex-wrap.persona-gap-2"
821
+ );
822
+ if (sug) suggestions = sug;
823
+ const attBtn = rootFooter.querySelector<HTMLButtonElement>(".persona-attachment-button");
824
+ if (attBtn) {
825
+ attachmentButton = attBtn;
826
+ attachmentButtonWrapper = attBtn.parentElement as HTMLElement | null;
827
+ }
828
+ attachmentInput = rootFooter.querySelector<HTMLInputElement>('input[type="file"]');
829
+ attachmentPreviewsContainer = rootFooter.querySelector<HTMLElement>(".persona-attachment-previews");
830
+ const ar = rootFooter.querySelector<HTMLElement>(".persona-widget-composer .persona-flex.persona-items-center.persona-justify-between");
831
+ if (ar) _actionsRow = ar;
832
+ };
833
+ ensureComposerAttachmentSurface(footer);
834
+ bindComposerRefsFromFooter(footer);
835
+
836
+ // Apply contentMaxWidth to composer form if configured
837
+ const contentMaxWidth = config.layout?.contentMaxWidth;
838
+ if (contentMaxWidth && composerForm) {
839
+ composerForm.style.maxWidth = contentMaxWidth;
840
+ composerForm.style.marginLeft = "auto";
841
+ composerForm.style.marginRight = "auto";
842
+ }
843
+
844
+ if (config.attachments?.enabled && attachmentInput && attachmentPreviewsContainer) {
845
+ attachmentManager = AttachmentManager.fromConfig(config.attachments);
846
+ attachmentManager.setPreviewsContainer(attachmentPreviewsContainer);
847
+ attachmentInput.addEventListener("change", (e) => {
848
+ const target = e.target as HTMLInputElement;
849
+ attachmentManager?.handleFileSelect(target.files);
850
+ target.value = "";
851
+ });
852
+ }
853
+
751
854
  // Slot system: allow custom content injection into specific regions
752
855
  const renderSlots = () => {
753
856
  const slots = config.layout?.slots ?? {};
@@ -757,7 +860,7 @@ export const createAgentExperience = (
757
860
  switch (slot) {
758
861
  case "body-top":
759
862
  // Default: the intro card
760
- return container.querySelector(".tvw-rounded-2xl.tvw-bg-cw-surface.tvw-p-6") as HTMLElement || null;
863
+ return container.querySelector(".persona-rounded-2xl.persona-bg-persona-surface.persona-p-6") as HTMLElement || null;
761
864
  case "messages":
762
865
  return messagesWrapper;
763
866
  case "footer-top":
@@ -784,7 +887,7 @@ export const createAgentExperience = (
784
887
  header.appendChild(element);
785
888
  } else {
786
889
  // header-center: insert after icon/title
787
- const titleSection = header.querySelector(".tvw-flex-col");
890
+ const titleSection = header.querySelector(".persona-flex-col");
788
891
  if (titleSection) {
789
892
  titleSection.parentNode?.insertBefore(element, titleSection.nextSibling);
790
893
  } else {
@@ -794,7 +897,7 @@ export const createAgentExperience = (
794
897
  break;
795
898
  case "body-top": {
796
899
  // Replace or prepend to body
797
- const introCard = body.querySelector(".tvw-rounded-2xl.tvw-bg-cw-surface.tvw-p-6");
900
+ const introCard = body.querySelector(".persona-rounded-2xl.persona-bg-persona-surface.persona-p-6");
798
901
  if (introCard) {
799
902
  introCard.replaceWith(element);
800
903
  } else {
@@ -904,7 +1007,7 @@ export const createAgentExperience = (
904
1007
 
905
1008
  messagesWrapper.addEventListener('click', (event) => {
906
1009
  const target = event.target as HTMLElement;
907
- const actionBtn = target.closest('.tvw-message-action-btn[data-action]') as HTMLElement;
1010
+ const actionBtn = target.closest('.persona-message-action-btn[data-action]') as HTMLElement;
908
1011
  if (!actionBtn) return;
909
1012
 
910
1013
  event.preventDefault();
@@ -926,14 +1029,14 @@ export const createAgentExperience = (
926
1029
  const textToCopy = message.content || "";
927
1030
  navigator.clipboard.writeText(textToCopy).then(() => {
928
1031
  // Show success feedback - swap icon temporarily
929
- actionBtn.classList.add("tvw-message-action-success");
1032
+ actionBtn.classList.add("persona-message-action-success");
930
1033
  const checkIcon = renderLucideIcon("check", 14, "currentColor", 2);
931
1034
  if (checkIcon) {
932
1035
  actionBtn.innerHTML = "";
933
1036
  actionBtn.appendChild(checkIcon);
934
1037
  }
935
1038
  setTimeout(() => {
936
- actionBtn.classList.remove("tvw-message-action-success");
1039
+ actionBtn.classList.remove("persona-message-action-success");
937
1040
  const originalIcon = renderLucideIcon("copy", 14, "currentColor", 2);
938
1041
  if (originalIcon) {
939
1042
  actionBtn.innerHTML = "";
@@ -955,17 +1058,17 @@ export const createAgentExperience = (
955
1058
  if (wasActive) {
956
1059
  // Toggle off
957
1060
  messageVoteState.delete(messageId);
958
- actionBtn.classList.remove("tvw-message-action-active");
1061
+ actionBtn.classList.remove("persona-message-action-active");
959
1062
  } else {
960
1063
  // Clear opposite vote button
961
1064
  const oppositeAction = action === 'upvote' ? 'downvote' : 'upvote';
962
1065
  const oppositeBtn = actionsContainer.querySelector(`[data-action="${oppositeAction}"]`);
963
1066
  if (oppositeBtn) {
964
- oppositeBtn.classList.remove("tvw-message-action-active");
1067
+ oppositeBtn.classList.remove("persona-message-action-active");
965
1068
  }
966
1069
 
967
1070
  messageVoteState.set(messageId, action);
968
- actionBtn.classList.add("tvw-message-action-active");
1071
+ actionBtn.classList.add("persona-message-action-active");
969
1072
 
970
1073
  // Trigger feedback
971
1074
  const messages = session.getMessages();
@@ -1021,32 +1124,334 @@ export const createAgentExperience = (
1021
1124
  session.resolveApproval(approvalMessage.approval, decision);
1022
1125
  });
1023
1126
 
1024
- panel.appendChild(container);
1127
+ let artifactPaneApi: ArtifactPaneApi | null = null;
1128
+ let artifactPanelResizeObs: ResizeObserver | null = null;
1129
+ let lastArtifactsState: {
1130
+ artifacts: PersonaArtifactRecord[];
1131
+ selectedId: string | null;
1132
+ } = { artifacts: [], selectedId: null };
1133
+ let artifactsPaneUserHidden = false;
1134
+ const sessionRef: { current: AgentWidgetSession | null } = { current: null };
1135
+
1136
+ // Click delegation for artifact download buttons
1137
+ messagesWrapper.addEventListener('click', (event) => {
1138
+ const target = event.target as HTMLElement;
1139
+ const dlBtn = target.closest('[data-download-artifact]') as HTMLElement;
1140
+ if (!dlBtn) return;
1141
+ event.preventDefault();
1142
+ event.stopPropagation();
1143
+ const artifactId = dlBtn.getAttribute('data-download-artifact');
1144
+ if (!artifactId) return;
1145
+ // Try session state first, fall back to content stored in the card's rawContent props
1146
+ const artifact = session.getArtifactById(artifactId);
1147
+ let markdown = artifact?.markdown;
1148
+ let title = artifact?.title || 'artifact';
1149
+ if (!markdown) {
1150
+ // After page refresh, session state is gone — read from the persisted card message
1151
+ const cardEl = dlBtn.closest('[data-open-artifact]');
1152
+ const msgEl = cardEl?.closest('[data-message-id]');
1153
+ const msgId = msgEl?.getAttribute('data-message-id');
1154
+ if (msgId) {
1155
+ const msgs = session.getMessages();
1156
+ const msg = msgs.find(m => m.id === msgId);
1157
+ if (msg?.rawContent) {
1158
+ try {
1159
+ const parsed = JSON.parse(msg.rawContent);
1160
+ markdown = parsed?.props?.markdown;
1161
+ title = parsed?.props?.title || title;
1162
+ } catch { /* ignore */ }
1163
+ }
1164
+ }
1165
+ }
1166
+ if (!markdown) return;
1167
+ const blob = new Blob([markdown], { type: 'text/markdown' });
1168
+ const url = URL.createObjectURL(blob);
1169
+ const a = document.createElement('a');
1170
+ a.href = url;
1171
+ a.download = `${title}.md`;
1172
+ a.click();
1173
+ URL.revokeObjectURL(url);
1174
+ });
1175
+
1176
+ // Click delegation for artifact reference cards
1177
+ messagesWrapper.addEventListener('click', (event) => {
1178
+ const target = event.target as HTMLElement;
1179
+ const card = target.closest('[data-open-artifact]') as HTMLElement;
1180
+ if (!card) return;
1181
+ const artifactId = card.getAttribute('data-open-artifact');
1182
+ if (!artifactId) return;
1183
+ event.preventDefault();
1184
+ event.stopPropagation();
1185
+ session.selectArtifact(artifactId);
1186
+ syncArtifactPane();
1187
+ });
1188
+
1189
+ // Keyboard support for artifact cards
1190
+ messagesWrapper.addEventListener('keydown', (event) => {
1191
+ if (event.key !== 'Enter' && event.key !== ' ') return;
1192
+ const target = event.target as HTMLElement;
1193
+ if (!target.hasAttribute('data-open-artifact')) return;
1194
+ event.preventDefault();
1195
+ target.click();
1196
+ });
1197
+
1198
+ let artifactSplitRoot: HTMLElement | null = null;
1199
+ let artifactResizeHandle: HTMLElement | null = null;
1200
+ let artifactResizeUnbind: (() => void) | null = null;
1201
+ let artifactResizeDocEnd: (() => void) | null = null;
1202
+ let reconcileArtifactResize: () => void = () => {};
1203
+
1204
+ function stopArtifactResizePointer() {
1205
+ artifactResizeDocEnd?.();
1206
+ artifactResizeDocEnd = null;
1207
+ }
1208
+
1209
+ /** Flush split: overlay handle on the seam so it does not consume flex gap (extension + resizable). */
1210
+ const positionExtensionArtifactResizeHandle = () => {
1211
+ if (!artifactSplitRoot || !artifactResizeHandle) return;
1212
+ const ext = mount.classList.contains("persona-artifact-appearance-seamless");
1213
+ const ownerWin = mount.ownerDocument.defaultView ?? window;
1214
+ const mobile = ownerWin.innerWidth <= 640;
1215
+ if (!ext || mount.classList.contains("persona-artifact-narrow-host") || mobile) {
1216
+ artifactResizeHandle.style.removeProperty("position");
1217
+ artifactResizeHandle.style.removeProperty("left");
1218
+ artifactResizeHandle.style.removeProperty("top");
1219
+ artifactResizeHandle.style.removeProperty("bottom");
1220
+ artifactResizeHandle.style.removeProperty("width");
1221
+ artifactResizeHandle.style.removeProperty("z-index");
1222
+ return;
1223
+ }
1224
+ const chat = artifactSplitRoot.firstElementChild as HTMLElement | null;
1225
+ if (!chat || chat === artifactResizeHandle) return;
1226
+ const hitW = 10;
1227
+ artifactResizeHandle.style.position = "absolute";
1228
+ artifactResizeHandle.style.top = "0";
1229
+ artifactResizeHandle.style.bottom = "0";
1230
+ artifactResizeHandle.style.width = `${hitW}px`;
1231
+ artifactResizeHandle.style.zIndex = "5";
1232
+ const left = chat.offsetWidth - hitW / 2;
1233
+ artifactResizeHandle.style.left = `${Math.max(0, left)}px`;
1234
+ };
1235
+
1236
+ /** No-op until artifact pane is created; replaced below when artifacts are enabled. */
1237
+ let applyLauncherArtifactPanelWidth: () => void = () => {};
1238
+
1239
+ const syncArtifactPane = () => {
1240
+ if (!artifactPaneApi || !artifactsSidebarEnabled(config)) return;
1241
+ applyArtifactLayoutCssVars(mount, config);
1242
+ applyArtifactPaneAppearance(mount, config);
1243
+ applyLauncherArtifactPanelWidth();
1244
+ const threshold = config.features?.artifacts?.layout?.narrowHostMaxWidth ?? 520;
1245
+ const w = panel.getBoundingClientRect().width || 0;
1246
+ mount.classList.toggle("persona-artifact-narrow-host", w > 0 && w <= threshold);
1247
+ artifactPaneApi.update(lastArtifactsState);
1248
+ if (artifactsPaneUserHidden) {
1249
+ artifactPaneApi.setMobileOpen(false);
1250
+ artifactPaneApi.element.classList.add("persona-hidden");
1251
+ artifactPaneApi.backdrop?.classList.add("persona-hidden");
1252
+ } else if (lastArtifactsState.artifacts.length > 0) {
1253
+ // User chose “show” again (e.g. programmatic showArtifacts): clear dismiss chrome
1254
+ // and force drawer open so narrow-host / mobile slide-out is not stuck off-screen.
1255
+ artifactPaneApi.element.classList.remove("persona-hidden");
1256
+ artifactPaneApi.setMobileOpen(true);
1257
+ }
1258
+ reconcileArtifactResize();
1259
+ };
1260
+
1261
+ if (artifactsSidebarEnabled(config)) {
1262
+ panel.style.position = "relative";
1263
+ const chatColumn = createElement(
1264
+ "div",
1265
+ "persona-flex persona-flex-1 persona-flex-col persona-min-w-0 persona-min-h-0"
1266
+ );
1267
+ const splitRoot = createElement(
1268
+ "div",
1269
+ "persona-flex persona-h-full persona-w-full persona-min-h-0 persona-artifact-split-root"
1270
+ );
1271
+ chatColumn.appendChild(container);
1272
+ artifactPaneApi = createArtifactPane(config, {
1273
+ onSelect: (id) => sessionRef.current?.selectArtifact(id),
1274
+ onDismiss: () => {
1275
+ artifactsPaneUserHidden = true;
1276
+ syncArtifactPane();
1277
+ }
1278
+ });
1279
+ artifactPaneApi.element.classList.add("persona-hidden");
1280
+ artifactSplitRoot = splitRoot;
1281
+ splitRoot.appendChild(chatColumn);
1282
+ splitRoot.appendChild(artifactPaneApi.element);
1283
+ if (artifactPaneApi.backdrop) {
1284
+ panel.appendChild(artifactPaneApi.backdrop);
1285
+ }
1286
+ panel.appendChild(splitRoot);
1287
+
1288
+ reconcileArtifactResize = () => {
1289
+ if (!artifactSplitRoot || !artifactPaneApi) return;
1290
+ const want = config.features?.artifacts?.layout?.resizable === true;
1291
+ if (!want) {
1292
+ artifactResizeUnbind?.();
1293
+ artifactResizeUnbind = null;
1294
+ stopArtifactResizePointer();
1295
+ if (artifactResizeHandle) {
1296
+ artifactResizeHandle.remove();
1297
+ artifactResizeHandle = null;
1298
+ }
1299
+ artifactPaneApi.element.style.removeProperty("width");
1300
+ artifactPaneApi.element.style.removeProperty("maxWidth");
1301
+ return;
1302
+ }
1303
+ if (!artifactResizeHandle) {
1304
+ const handle = createElement(
1305
+ "div",
1306
+ "persona-artifact-split-handle persona-shrink-0 persona-h-full"
1307
+ );
1308
+ handle.setAttribute("role", "separator");
1309
+ handle.setAttribute("aria-orientation", "vertical");
1310
+ handle.setAttribute("aria-label", "Resize artifacts panel");
1311
+ handle.tabIndex = 0;
1312
+
1313
+ const doc = mount.ownerDocument;
1314
+ const win = doc.defaultView ?? window;
1315
+
1316
+ const onPointerDown = (e: PointerEvent) => {
1317
+ if (!artifactPaneApi || e.button !== 0) return;
1318
+ if (mount.classList.contains("persona-artifact-narrow-host")) return;
1319
+ if (win.innerWidth <= 640) return;
1320
+ e.preventDefault();
1321
+ stopArtifactResizePointer();
1322
+ const startX = e.clientX;
1323
+ const startW = artifactPaneApi.element.getBoundingClientRect().width;
1324
+ const layout = config.features?.artifacts?.layout;
1325
+ const onMove = (ev: PointerEvent) => {
1326
+ const splitW = artifactSplitRoot!.getBoundingClientRect().width;
1327
+ const extensionChrome = mount.classList.contains("persona-artifact-appearance-seamless");
1328
+ const gapPx = extensionChrome ? 0 : readFlexGapPx(artifactSplitRoot!, win);
1329
+ const handleW = extensionChrome ? 0 : handle.getBoundingClientRect().width || 6;
1330
+ // Handle is left of the artifact: drag left widens artifact, drag right narrows it.
1331
+ const next = startW - (ev.clientX - startX);
1332
+ const clamped = resolveArtifactPaneWidthPx(
1333
+ next,
1334
+ splitW,
1335
+ gapPx,
1336
+ handleW,
1337
+ layout?.resizableMinWidth,
1338
+ layout?.resizableMaxWidth
1339
+ );
1340
+ artifactPaneApi!.element.style.width = `${clamped}px`;
1341
+ artifactPaneApi!.element.style.maxWidth = "none";
1342
+ positionExtensionArtifactResizeHandle();
1343
+ };
1344
+ const onUp = () => {
1345
+ doc.removeEventListener("pointermove", onMove);
1346
+ doc.removeEventListener("pointerup", onUp);
1347
+ doc.removeEventListener("pointercancel", onUp);
1348
+ artifactResizeDocEnd = null;
1349
+ try {
1350
+ handle.releasePointerCapture(e.pointerId);
1351
+ } catch {
1352
+ /* ignore */
1353
+ }
1354
+ };
1355
+ artifactResizeDocEnd = onUp;
1356
+ doc.addEventListener("pointermove", onMove);
1357
+ doc.addEventListener("pointerup", onUp);
1358
+ doc.addEventListener("pointercancel", onUp);
1359
+ try {
1360
+ handle.setPointerCapture(e.pointerId);
1361
+ } catch {
1362
+ /* ignore */
1363
+ }
1364
+ };
1365
+
1366
+ handle.addEventListener("pointerdown", onPointerDown);
1367
+ artifactResizeHandle = handle;
1368
+ artifactSplitRoot.insertBefore(handle, artifactPaneApi.element);
1369
+ artifactResizeUnbind = () => {
1370
+ handle.removeEventListener("pointerdown", onPointerDown);
1371
+ };
1372
+ }
1373
+ if (artifactResizeHandle) {
1374
+ const has =
1375
+ lastArtifactsState.artifacts.length > 0 && !artifactsPaneUserHidden;
1376
+ artifactResizeHandle.classList.toggle("persona-hidden", !has);
1377
+ positionExtensionArtifactResizeHandle();
1378
+ }
1379
+ };
1380
+
1381
+ applyLauncherArtifactPanelWidth = () => {
1382
+ if (!launcherEnabled || !artifactPaneApi) return;
1383
+ const sidebarMode = config.launcher?.sidebarMode ?? false;
1384
+ if (sidebarMode) return;
1385
+ const ownerWindow = mount.ownerDocument.defaultView ?? window;
1386
+ const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
1387
+ const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
1388
+ if (mobileFullscreen && ownerWindow.innerWidth <= mobileBreakpoint) return;
1389
+ if (!shouldExpandLauncherForArtifacts(config, launcherEnabled)) return;
1390
+
1391
+ const base = config.launcher?.width ?? config.launcherWidth ?? "min(400px, calc(100vw - 24px))";
1392
+ const expanded =
1393
+ config.features?.artifacts?.layout?.expandedPanelWidth ??
1394
+ "min(720px, calc(100vw - 24px))";
1395
+ const hasVisible =
1396
+ lastArtifactsState.artifacts.length > 0 && !artifactsPaneUserHidden;
1397
+ if (hasVisible) {
1398
+ panel.style.width = expanded;
1399
+ panel.style.maxWidth = expanded;
1400
+ } else {
1401
+ panel.style.width = base;
1402
+ panel.style.maxWidth = base;
1403
+ }
1404
+ };
1405
+
1406
+ if (typeof ResizeObserver !== "undefined") {
1407
+ artifactPanelResizeObs = new ResizeObserver(() => {
1408
+ syncArtifactPane();
1409
+ });
1410
+ artifactPanelResizeObs.observe(panel);
1411
+ }
1412
+ } else {
1413
+ panel.appendChild(container);
1414
+ }
1025
1415
  mount.appendChild(wrapper);
1026
1416
 
1027
1417
  // Apply full-height and sidebar styles if enabled
1028
1418
  // This ensures the widget fills its container height with proper flex layout
1029
1419
  const applyFullHeightStyles = () => {
1420
+ const dockedMode = isDockedMountMode(config);
1030
1421
  const sidebarMode = config.launcher?.sidebarMode ?? false;
1031
- const fullHeight = sidebarMode || (config.launcher?.fullHeight ?? false);
1422
+ const fullHeight = dockedMode || sidebarMode || (config.launcher?.fullHeight ?? false);
1423
+ /** Script-tag / div embed: launcher off, host supplies a sized mount. */
1424
+ const isInlineEmbed = config.launcher?.enabled === false;
1032
1425
  const theme = config.theme ?? {};
1033
-
1426
+
1427
+ // Mobile fullscreen detection
1428
+ // Use mount's ownerDocument window to get correct viewport width when widget is inside an iframe
1429
+ const ownerWindow = mount.ownerDocument.defaultView ?? window;
1430
+ const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
1431
+ const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
1432
+ const isMobileViewport = ownerWindow.innerWidth <= mobileBreakpoint;
1433
+ const shouldGoFullscreen = mobileFullscreen && isMobileViewport && launcherEnabled;
1434
+
1034
1435
  // Determine panel styling based on mode, with theme overrides
1035
1436
  const position = config.launcher?.position ?? 'bottom-left';
1036
1437
  const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
1037
-
1438
+
1038
1439
  // Default values based on mode
1039
- const defaultPanelBorder = sidebarMode ? 'none' : '1px solid var(--tvw-cw-border)';
1040
- const defaultPanelShadow = sidebarMode
1041
- ? (isLeftSidebar ? '2px 0 12px rgba(0, 0, 0, 0.08)' : '-2px 0 12px rgba(0, 0, 0, 0.08)')
1042
- : '0 25px 50px -12px rgba(0, 0, 0, 0.25)';
1043
- const defaultPanelBorderRadius = sidebarMode ? '0' : '16px';
1044
-
1440
+ const defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-persona-border)';
1441
+ const defaultPanelShadow = shouldGoFullscreen
1442
+ ? 'none'
1443
+ : sidebarMode
1444
+ ? (isLeftSidebar ? 'var(--persona-palette-shadows-sidebar-left, 2px 0 12px rgba(0, 0, 0, 0.08))' : 'var(--persona-palette-shadows-sidebar-right, -2px 0 12px rgba(0, 0, 0, 0.08))')
1445
+ : 'var(--persona-palette-shadows-xl, 0 25px 50px -12px rgba(0, 0, 0, 0.25))';
1446
+ const defaultPanelBorderRadius = (sidebarMode || shouldGoFullscreen)
1447
+ ? '0'
1448
+ : 'var(--persona-panel-radius, var(--persona-radius-xl, 0.75rem))';
1449
+
1045
1450
  // Apply theme overrides or defaults
1046
1451
  const panelBorder = theme.panelBorder ?? defaultPanelBorder;
1047
1452
  const panelShadow = theme.panelShadow ?? defaultPanelShadow;
1048
1453
  const panelBorderRadius = theme.panelBorderRadius ?? defaultPanelBorderRadius;
1049
-
1454
+
1050
1455
  // Reset all inline styles first to handle mode toggling
1051
1456
  // This ensures styles don't persist when switching between modes
1052
1457
  mount.style.cssText = '';
@@ -1056,14 +1461,87 @@ export const createAgentExperience = (
1056
1461
  body.style.cssText = '';
1057
1462
  footer.style.cssText = '';
1058
1463
 
1464
+ // Mobile fullscreen: fill entire viewport with no radius/shadow/margins
1465
+ if (shouldGoFullscreen) {
1466
+ // Remove position offset classes
1467
+ wrapper.classList.remove(
1468
+ 'persona-bottom-6', 'persona-right-6', 'persona-left-6', 'persona-top-6',
1469
+ 'persona-bottom-4', 'persona-right-4', 'persona-left-4', 'persona-top-4'
1470
+ );
1471
+
1472
+ // Wrapper — fill entire viewport
1473
+ wrapper.style.cssText = `
1474
+ position: fixed !important;
1475
+ inset: 0 !important;
1476
+ width: 100% !important;
1477
+ height: 100% !important;
1478
+ max-height: 100% !important;
1479
+ margin: 0 !important;
1480
+ padding: 0 !important;
1481
+ display: flex !important;
1482
+ flex-direction: column !important;
1483
+ z-index: inherit !important;
1484
+ `;
1485
+
1486
+ // Panel — fill wrapper, no radius/shadow
1487
+ panel.style.cssText = `
1488
+ position: relative !important;
1489
+ display: flex !important;
1490
+ flex-direction: column !important;
1491
+ flex: 1 1 0% !important;
1492
+ width: 100% !important;
1493
+ max-width: 100% !important;
1494
+ height: 100% !important;
1495
+ min-height: 0 !important;
1496
+ margin: 0 !important;
1497
+ padding: 0 !important;
1498
+ box-shadow: none !important;
1499
+ border-radius: 0 !important;
1500
+ `;
1501
+
1502
+ // Container — fill panel, no radius/border
1503
+ container.style.cssText = `
1504
+ display: flex !important;
1505
+ flex-direction: column !important;
1506
+ flex: 1 1 0% !important;
1507
+ width: 100% !important;
1508
+ height: 100% !important;
1509
+ min-height: 0 !important;
1510
+ max-height: 100% !important;
1511
+ overflow: hidden !important;
1512
+ border-radius: 0 !important;
1513
+ border: none !important;
1514
+ `;
1515
+
1516
+ // Body — scrollable messages
1517
+ body.style.flex = '1 1 0%';
1518
+ body.style.minHeight = '0';
1519
+ body.style.overflowY = 'auto';
1520
+
1521
+ // Footer — pinned at bottom
1522
+ footer.style.flexShrink = '0';
1523
+
1524
+ wasMobileFullscreen = true;
1525
+ return; // Skip remaining mode logic
1526
+ }
1527
+
1059
1528
  // Re-apply panel width/maxWidth from initial setup
1060
1529
  const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
1061
1530
  const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
1062
- if (!sidebarMode) {
1063
- panel.style.width = width;
1064
- panel.style.maxWidth = width;
1531
+ if (!sidebarMode && !dockedMode) {
1532
+ if (isInlineEmbed && fullHeight) {
1533
+ panel.style.width = "100%";
1534
+ panel.style.maxWidth = "100%";
1535
+ } else {
1536
+ panel.style.width = width;
1537
+ panel.style.maxWidth = width;
1538
+ }
1539
+ } else if (dockedMode) {
1540
+ panel.style.width = "100%";
1541
+ panel.style.maxWidth = "100%";
1065
1542
  }
1066
-
1543
+ applyLauncherArtifactPanelWidth();
1544
+
1067
1545
  // Apply panel styling
1068
1546
  // Box-shadow is applied to panel (parent) instead of container to avoid
1069
1547
  // rendering artifacts when container has overflow:hidden + border-radius
@@ -1072,16 +1550,16 @@ export const createAgentExperience = (
1072
1550
  panel.style.borderRadius = panelBorderRadius;
1073
1551
  container.style.border = panelBorder;
1074
1552
  container.style.borderRadius = panelBorderRadius;
1075
-
1076
- // Check if this is inline embed mode (launcher disabled) vs launcher mode
1077
- const isInlineEmbed = config.launcher?.enabled === false;
1078
-
1553
+
1079
1554
  if (fullHeight) {
1080
1555
  // Mount container
1081
1556
  mount.style.display = 'flex';
1082
1557
  mount.style.flexDirection = 'column';
1083
1558
  mount.style.height = '100%';
1084
1559
  mount.style.minHeight = '0';
1560
+ if (isInlineEmbed) {
1561
+ mount.style.width = '100%';
1562
+ }
1085
1563
 
1086
1564
  // Wrapper
1087
1565
  // - Inline embed: needs overflow:hidden to contain the flex layout
@@ -1125,11 +1603,11 @@ export const createAgentExperience = (
1125
1603
  // Handle positioning classes based on mode
1126
1604
  // First remove all position classes to reset state
1127
1605
  wrapper.classList.remove(
1128
- 'tvw-bottom-6', 'tvw-right-6', 'tvw-left-6', 'tvw-top-6',
1129
- 'tvw-bottom-4', 'tvw-right-4', 'tvw-left-4', 'tvw-top-4'
1606
+ 'persona-bottom-6', 'persona-right-6', 'persona-left-6', 'persona-top-6',
1607
+ 'persona-bottom-4', 'persona-right-4', 'persona-left-4', 'persona-top-4'
1130
1608
  );
1131
1609
 
1132
- if (!sidebarMode && !isInlineEmbed) {
1610
+ if (!sidebarMode && !isInlineEmbed && !dockedMode) {
1133
1611
  // Restore positioning classes when not in sidebar mode (launcher mode only)
1134
1612
  const positionClasses = positionMap[position as keyof typeof positionMap] ?? positionMap['bottom-right'];
1135
1613
  positionClasses.split(' ').forEach(cls => wrapper.classList.add(cls));
@@ -1202,7 +1680,7 @@ export const createAgentExperience = (
1202
1680
  // Use both -moz-available (Firefox) and stretch (standard) for cross-browser support
1203
1681
  // Append to cssText to allow multiple fallback values for the same property
1204
1682
  // Only apply to launcher mode (not sidebar or inline embed)
1205
- if (!isInlineEmbed) {
1683
+ if (!isInlineEmbed && !dockedMode) {
1206
1684
  const maxHeightStyles = 'max-height: -moz-available !important; max-height: stretch !important;';
1207
1685
  const paddingStyles = sidebarMode ? '' : 'padding-top: 1.25em !important;';
1208
1686
  wrapper.style.cssText += maxHeightStyles + paddingStyles;
@@ -1211,9 +1689,30 @@ export const createAgentExperience = (
1211
1689
  applyFullHeightStyles();
1212
1690
  // Apply theme variables after applyFullHeightStyles since it resets mount.style.cssText
1213
1691
  applyThemeVariables(mount, config);
1692
+ applyArtifactLayoutCssVars(mount, config);
1693
+ applyArtifactPaneAppearance(mount, config);
1214
1694
 
1215
1695
  const destroyCallbacks: Array<() => void> = [];
1216
1696
 
1697
+ if (artifactPanelResizeObs) {
1698
+ destroyCallbacks.push(() => {
1699
+ artifactPanelResizeObs?.disconnect();
1700
+ artifactPanelResizeObs = null;
1701
+ });
1702
+ }
1703
+
1704
+ destroyCallbacks.push(() => {
1705
+ artifactResizeUnbind?.();
1706
+ artifactResizeUnbind = null;
1707
+ stopArtifactResizePointer();
1708
+ if (artifactResizeHandle) {
1709
+ artifactResizeHandle.remove();
1710
+ artifactResizeHandle = null;
1711
+ }
1712
+ artifactPaneApi?.element.style.removeProperty("width");
1713
+ artifactPaneApi?.element.style.removeProperty("maxWidth");
1714
+ });
1715
+
1217
1716
  // Event stream cleanup
1218
1717
  if (showEventStreamToggle) {
1219
1718
  destroyCallbacks.push(() => {
@@ -1257,6 +1756,8 @@ export const createAgentExperience = (
1257
1756
  let closeHandler: (() => void) | null = null;
1258
1757
  let session: AgentWidgetSession;
1259
1758
  let isStreaming = false;
1759
+ const messageCache = createMessageCache();
1760
+ let configVersion = 0;
1260
1761
  let shouldAutoScroll = true;
1261
1762
  let lastScrollTop = 0;
1262
1763
  let lastAutoScrollTime = 0;
@@ -1530,7 +2031,20 @@ export const createAgentExperience = (
1530
2031
 
1531
2032
  const inlineLoadingRenderer = getInlineLoadingIndicatorRenderer();
1532
2033
 
2034
+ // Track active message IDs for cache pruning
2035
+ const activeMessageIds = new Set<string>();
2036
+
1533
2037
  messages.forEach((message) => {
2038
+ activeMessageIds.add(message.id);
2039
+
2040
+ // Fingerprint cache: skip re-rendering unchanged messages
2041
+ const fingerprint = computeMessageFingerprint(message, configVersion);
2042
+ const cachedWrapper = getCachedWrapper(messageCache, message.id, fingerprint);
2043
+ if (cachedWrapper) {
2044
+ tempContainer.appendChild(cachedWrapper.cloneNode(true));
2045
+ return;
2046
+ }
2047
+
1534
2048
  let bubble: HTMLElement | null = null;
1535
2049
 
1536
2050
  // Try plugins first
@@ -1612,36 +2126,59 @@ export const createAgentExperience = (
1612
2126
  transform
1613
2127
  });
1614
2128
  if (componentBubble) {
1615
- // Wrap component in standard bubble styling
1616
- const componentWrapper = document.createElement("div");
1617
- componentWrapper.className = [
1618
- "vanilla-message-bubble",
1619
- "tvw-max-w-[85%]",
1620
- "tvw-rounded-2xl",
1621
- "tvw-bg-cw-surface",
1622
- "tvw-border",
1623
- "tvw-border-cw-message-border",
1624
- "tvw-p-4"
1625
- ].join(" ");
1626
- // Set id for idiomorph matching
1627
- componentWrapper.id = `bubble-${message.id}`;
1628
- componentWrapper.setAttribute("data-message-id", message.id);
1629
-
1630
- // Add text content above component if present (combined text+component response)
1631
- if (message.content && message.content.trim()) {
1632
- const textDiv = document.createElement("div");
1633
- textDiv.className = "tvw-mb-3 tvw-text-sm tvw-leading-relaxed";
1634
- textDiv.innerHTML = transform({
1635
- text: message.content,
1636
- message,
1637
- streaming: Boolean(message.streaming),
1638
- raw: message.rawContent
1639
- });
1640
- componentWrapper.appendChild(textDiv);
1641
- }
2129
+ const wrapChrome = config.wrapComponentDirectiveInBubble !== false;
2130
+ if (wrapChrome) {
2131
+ const componentWrapper = document.createElement("div");
2132
+ componentWrapper.className = [
2133
+ "vanilla-message-bubble",
2134
+ "persona-max-w-[85%]",
2135
+ "persona-rounded-2xl",
2136
+ "persona-bg-persona-surface",
2137
+ "persona-border",
2138
+ "persona-border-persona-message-border",
2139
+ "persona-p-4"
2140
+ ].join(" ");
2141
+ componentWrapper.id = `bubble-${message.id}`;
2142
+ componentWrapper.setAttribute("data-message-id", message.id);
2143
+
2144
+ if (message.content && message.content.trim()) {
2145
+ const textDiv = document.createElement("div");
2146
+ textDiv.className = "persona-mb-3 persona-text-sm persona-leading-relaxed";
2147
+ textDiv.innerHTML = transform({
2148
+ text: message.content,
2149
+ message,
2150
+ streaming: Boolean(message.streaming),
2151
+ raw: message.rawContent
2152
+ });
2153
+ componentWrapper.appendChild(textDiv);
2154
+ }
1642
2155
 
1643
- componentWrapper.appendChild(componentBubble);
1644
- bubble = componentWrapper;
2156
+ componentWrapper.appendChild(componentBubble);
2157
+ bubble = componentWrapper;
2158
+ } else {
2159
+ const stack = document.createElement("div");
2160
+ stack.className =
2161
+ "persona-flex persona-flex-col persona-w-full persona-max-w-full persona-gap-3 persona-items-stretch";
2162
+ stack.id = `bubble-${message.id}`;
2163
+ stack.setAttribute("data-message-id", message.id);
2164
+ stack.setAttribute("data-persona-component-directive", "true");
2165
+
2166
+ if (message.content && message.content.trim()) {
2167
+ const textDiv = document.createElement("div");
2168
+ textDiv.className =
2169
+ "persona-text-sm persona-leading-relaxed persona-text-persona-primary persona-w-full";
2170
+ textDiv.innerHTML = transform({
2171
+ text: message.content,
2172
+ message,
2173
+ streaming: Boolean(message.streaming),
2174
+ raw: message.rawContent
2175
+ });
2176
+ stack.appendChild(textDiv);
2177
+ }
2178
+
2179
+ stack.appendChild(componentBubble);
2180
+ bubble = stack;
2181
+ }
1645
2182
  }
1646
2183
  }
1647
2184
  }
@@ -1693,17 +2230,24 @@ export const createAgentExperience = (
1693
2230
  }
1694
2231
 
1695
2232
  const wrapper = document.createElement("div");
1696
- wrapper.className = "tvw-flex";
2233
+ wrapper.className = "persona-flex";
1697
2234
  // Set id for idiomorph matching
1698
2235
  wrapper.id = `wrapper-${message.id}`;
1699
2236
  wrapper.setAttribute("data-wrapper-id", message.id);
1700
2237
  if (message.role === "user") {
1701
- wrapper.classList.add("tvw-justify-end");
2238
+ wrapper.classList.add("persona-justify-end");
2239
+ }
2240
+ if (bubble?.getAttribute("data-persona-component-directive") === "true") {
2241
+ wrapper.classList.add("persona-w-full");
1702
2242
  }
1703
2243
  wrapper.appendChild(bubble);
2244
+ setCachedWrapper(messageCache, message.id, fingerprint, wrapper);
1704
2245
  tempContainer.appendChild(wrapper);
1705
2246
  });
1706
2247
 
2248
+ // Remove cache entries for messages that no longer exist
2249
+ pruneCache(messageCache, activeMessageIds);
2250
+
1707
2251
  // Add standalone typing indicator only if streaming but no assistant message is streaming yet
1708
2252
  // (This shows while waiting for the stream to start)
1709
2253
  // Check for ANY streaming assistant message, even if empty (to avoid duplicate bubbles)
@@ -1752,30 +2296,30 @@ export const createAgentExperience = (
1752
2296
  const showBubble = config.loadingIndicator?.showBubble !== false; // default true
1753
2297
  typingBubble.className = showBubble
1754
2298
  ? [
1755
- "tvw-max-w-[85%]",
1756
- "tvw-rounded-2xl",
1757
- "tvw-text-sm",
1758
- "tvw-leading-relaxed",
1759
- "tvw-shadow-sm",
1760
- "tvw-bg-cw-surface",
1761
- "tvw-border",
1762
- "tvw-border-cw-message-border",
1763
- "tvw-text-cw-primary",
1764
- "tvw-px-5",
1765
- "tvw-py-3"
2299
+ "persona-max-w-[85%]",
2300
+ "persona-rounded-2xl",
2301
+ "persona-text-sm",
2302
+ "persona-leading-relaxed",
2303
+ "persona-shadow-sm",
2304
+ "persona-bg-persona-surface",
2305
+ "persona-border",
2306
+ "persona-border-persona-message-border",
2307
+ "persona-text-persona-primary",
2308
+ "persona-px-5",
2309
+ "persona-py-3"
1766
2310
  ].join(" ")
1767
2311
  : [
1768
- "tvw-max-w-[85%]",
1769
- "tvw-text-sm",
1770
- "tvw-leading-relaxed",
1771
- "tvw-text-cw-primary"
2312
+ "persona-max-w-[85%]",
2313
+ "persona-text-sm",
2314
+ "persona-leading-relaxed",
2315
+ "persona-text-persona-primary"
1772
2316
  ].join(" ");
1773
2317
  typingBubble.setAttribute("data-typing-indicator", "true");
1774
2318
 
1775
2319
  typingBubble.appendChild(typingIndicator);
1776
2320
 
1777
2321
  const typingWrapper = document.createElement("div");
1778
- typingWrapper.className = "tvw-flex";
2322
+ typingWrapper.className = "persona-flex";
1779
2323
  // Set id for idiomorph matching
1780
2324
  typingWrapper.id = "wrapper-typing-indicator";
1781
2325
  typingWrapper.setAttribute("data-wrapper-id", "typing-indicator");
@@ -1816,30 +2360,30 @@ export const createAgentExperience = (
1816
2360
  const showBubble = config.loadingIndicator?.showBubble !== false; // default true
1817
2361
  idleBubble.className = showBubble
1818
2362
  ? [
1819
- "tvw-max-w-[85%]",
1820
- "tvw-rounded-2xl",
1821
- "tvw-text-sm",
1822
- "tvw-leading-relaxed",
1823
- "tvw-shadow-sm",
1824
- "tvw-bg-cw-surface",
1825
- "tvw-border",
1826
- "tvw-border-cw-message-border",
1827
- "tvw-text-cw-primary",
1828
- "tvw-px-5",
1829
- "tvw-py-3"
2363
+ "persona-max-w-[85%]",
2364
+ "persona-rounded-2xl",
2365
+ "persona-text-sm",
2366
+ "persona-leading-relaxed",
2367
+ "persona-shadow-sm",
2368
+ "persona-bg-persona-surface",
2369
+ "persona-border",
2370
+ "persona-border-persona-message-border",
2371
+ "persona-text-persona-primary",
2372
+ "persona-px-5",
2373
+ "persona-py-3"
1830
2374
  ].join(" ")
1831
2375
  : [
1832
- "tvw-max-w-[85%]",
1833
- "tvw-text-sm",
1834
- "tvw-leading-relaxed",
1835
- "tvw-text-cw-primary"
2376
+ "persona-max-w-[85%]",
2377
+ "persona-text-sm",
2378
+ "persona-leading-relaxed",
2379
+ "persona-text-persona-primary"
1836
2380
  ].join(" ");
1837
2381
  idleBubble.setAttribute("data-idle-indicator", "true");
1838
2382
 
1839
2383
  idleBubble.appendChild(idleIndicator);
1840
2384
 
1841
2385
  const idleWrapper = document.createElement("div");
1842
- idleWrapper.className = "tvw-flex";
2386
+ idleWrapper.className = "persona-flex";
1843
2387
  // Set id for idiomorph matching
1844
2388
  idleWrapper.id = "wrapper-idle-indicator";
1845
2389
  idleWrapper.setAttribute("data-wrapper-id", "idle-indicator");
@@ -1867,10 +2411,12 @@ export const createAgentExperience = (
1867
2411
 
1868
2412
  const updateOpenState = () => {
1869
2413
  if (!launcherEnabled) return;
2414
+ const dockedMode = isDockedMountMode(config);
1870
2415
  if (open) {
1871
- wrapper.classList.remove("tvw-pointer-events-none", "tvw-opacity-0");
1872
- panel.classList.remove("tvw-scale-95", "tvw-opacity-0");
1873
- panel.classList.add("tvw-scale-100", "tvw-opacity-100");
2416
+ wrapper.style.display = dockedMode ? "flex" : "";
2417
+ wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2418
+ panel.classList.remove("persona-scale-95", "persona-opacity-0");
2419
+ panel.classList.add("persona-scale-100", "persona-opacity-100");
1874
2420
  // Hide launcher button when widget is open
1875
2421
  if (launcherButtonInstance) {
1876
2422
  launcherButtonInstance.element.style.display = "none";
@@ -1878,9 +2424,16 @@ export const createAgentExperience = (
1878
2424
  customLauncherElement.style.display = "none";
1879
2425
  }
1880
2426
  } else {
1881
- wrapper.classList.add("tvw-pointer-events-none", "tvw-opacity-0");
1882
- panel.classList.remove("tvw-scale-100", "tvw-opacity-100");
1883
- panel.classList.add("tvw-scale-95", "tvw-opacity-0");
2427
+ if (dockedMode) {
2428
+ wrapper.style.display = "none";
2429
+ wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2430
+ panel.classList.remove("persona-scale-100", "persona-opacity-100", "persona-scale-95", "persona-opacity-0");
2431
+ } else {
2432
+ wrapper.style.display = "";
2433
+ wrapper.classList.add("persona-pointer-events-none", "persona-opacity-0");
2434
+ panel.classList.remove("persona-scale-100", "persona-opacity-100");
2435
+ panel.classList.add("persona-scale-95", "persona-opacity-0");
2436
+ }
1884
2437
  // Show launcher button when widget is closed
1885
2438
  if (launcherButtonInstance) {
1886
2439
  launcherButtonInstance.element.style.display = "";
@@ -1935,6 +2488,17 @@ export const createAgentExperience = (
1935
2488
  suggestionsManager.buttons.forEach((btn) => {
1936
2489
  btn.disabled = disabled;
1937
2490
  });
2491
+ footer.dataset.personaComposerStreaming = disabled ? "true" : "false";
2492
+ footer.querySelectorAll<HTMLElement>("[data-persona-composer-disable-when-streaming]").forEach((el) => {
2493
+ if (
2494
+ el instanceof HTMLButtonElement ||
2495
+ el instanceof HTMLInputElement ||
2496
+ el instanceof HTMLTextAreaElement ||
2497
+ el instanceof HTMLSelectElement
2498
+ ) {
2499
+ el.disabled = disabled;
2500
+ }
2501
+ });
1938
2502
  };
1939
2503
 
1940
2504
  const maybeFocusInput = () => {
@@ -2082,9 +2646,15 @@ export const createAgentExperience = (
2082
2646
  }
2083
2647
  break;
2084
2648
  }
2649
+ },
2650
+ onArtifactsState(state) {
2651
+ lastArtifactsState = state;
2652
+ syncArtifactPane();
2085
2653
  }
2086
2654
  });
2087
2655
 
2656
+ sessionRef.current = session;
2657
+
2088
2658
  // Setup Runtype voice provider when configured (connects WebSocket for server-side STT)
2089
2659
  if (config.voiceRecognition?.provider?.type === 'runtype') {
2090
2660
  try {
@@ -2312,7 +2882,7 @@ export const createAgentExperience = (
2312
2882
  const recordingIconColor = voiceConfig.recordingIconColor;
2313
2883
  const recordingBorderColor = voiceConfig.recordingBorderColor;
2314
2884
 
2315
- micButton.classList.add("tvw-voice-recording");
2885
+ micButton.classList.add("persona-voice-recording");
2316
2886
  micButton.style.backgroundColor = recordingBackgroundColor;
2317
2887
 
2318
2888
  if (recordingIconColor) {
@@ -2360,7 +2930,24 @@ export const createAgentExperience = (
2360
2930
  persistVoiceMetadata();
2361
2931
 
2362
2932
  if (micButton) {
2363
- removeRuntypeMicStateStyles();
2933
+ micButton.classList.remove("persona-voice-recording");
2934
+
2935
+ // Restore original styles
2936
+ if (originalMicStyles) {
2937
+ micButton.style.backgroundColor = originalMicStyles.backgroundColor;
2938
+ micButton.style.color = originalMicStyles.color;
2939
+ micButton.style.borderColor = originalMicStyles.borderColor;
2940
+
2941
+ // Restore SVG stroke color if present
2942
+ const svg = micButton.querySelector("svg");
2943
+ if (svg) {
2944
+ svg.setAttribute("stroke", originalMicStyles.color || "currentColor");
2945
+ }
2946
+
2947
+ originalMicStyles = null;
2948
+ }
2949
+
2950
+ micButton.setAttribute("aria-label", "Start voice recognition");
2364
2951
  }
2365
2952
  };
2366
2953
 
@@ -2375,10 +2962,10 @@ export const createAgentExperience = (
2375
2962
 
2376
2963
  if (!hasVoiceInput) return null;
2377
2964
 
2378
- const micButtonWrapper = createElement("div", "tvw-send-button-wrapper");
2965
+ const micButtonWrapper = createElement("div", "persona-send-button-wrapper");
2379
2966
  const micButton = createElement(
2380
2967
  "button",
2381
- "tvw-rounded-button tvw-flex tvw-items-center tvw-justify-center disabled:tvw-opacity-50 tvw-cursor-pointer"
2968
+ "persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer"
2382
2969
  ) as HTMLButtonElement;
2383
2970
 
2384
2971
  micButton.type = "button";
@@ -2416,14 +3003,14 @@ export const createAgentExperience = (
2416
3003
  if (backgroundColor) {
2417
3004
  micButton.style.backgroundColor = backgroundColor;
2418
3005
  } else {
2419
- micButton.classList.add("tvw-bg-cw-primary");
3006
+ micButton.classList.add("persona-bg-persona-primary");
2420
3007
  }
2421
3008
 
2422
3009
  // Apply icon/text color
2423
3010
  if (iconColor) {
2424
3011
  micButton.style.color = iconColor;
2425
3012
  } else if (!iconColor && !sendButtonConfig?.textColor) {
2426
- micButton.classList.add("tvw-text-white");
3013
+ micButton.classList.add("persona-text-white");
2427
3014
  }
2428
3015
 
2429
3016
  // Apply border styling
@@ -2451,7 +3038,7 @@ export const createAgentExperience = (
2451
3038
  const tooltipText = voiceConfig?.tooltipText ?? "Start voice recognition";
2452
3039
  const showTooltip = voiceConfig?.showTooltip ?? false;
2453
3040
  if (showTooltip && tooltipText) {
2454
- const tooltip = createElement("div", "tvw-send-button-tooltip");
3041
+ const tooltip = createElement("div", "persona-send-button-tooltip");
2455
3042
  tooltip.textContent = tooltipText;
2456
3043
  micButtonWrapper.appendChild(tooltip);
2457
3044
  }
@@ -2486,7 +3073,7 @@ export const createAgentExperience = (
2486
3073
  /** Remove all voice state CSS classes */
2487
3074
  const removeAllVoiceStateClasses = () => {
2488
3075
  if (!micButton) return;
2489
- micButton.classList.remove("tvw-voice-recording", "tvw-voice-processing", "tvw-voice-speaking");
3076
+ micButton.classList.remove("persona-voice-recording", "persona-voice-processing", "persona-voice-speaking");
2490
3077
  };
2491
3078
 
2492
3079
  // --- Per-state style application ---
@@ -2499,7 +3086,7 @@ export const createAgentExperience = (
2499
3086
  const recordingIconColor = voiceConfig.recordingIconColor;
2500
3087
  const recordingBorderColor = voiceConfig.recordingBorderColor;
2501
3088
  removeAllVoiceStateClasses();
2502
- micButton.classList.add("tvw-voice-recording");
3089
+ micButton.classList.add("persona-voice-recording");
2503
3090
  micButton.style.backgroundColor = recordingBackgroundColor;
2504
3091
  if (recordingIconColor) {
2505
3092
  micButton.style.color = recordingIconColor;
@@ -2521,7 +3108,7 @@ export const createAgentExperience = (
2521
3108
  const borderColor = voiceConfig.processingBorderColor ?? originalMicStyles?.borderColor ?? "";
2522
3109
 
2523
3110
  removeAllVoiceStateClasses();
2524
- micButton.classList.add("tvw-voice-processing");
3111
+ micButton.classList.add("persona-voice-processing");
2525
3112
  micButton.style.backgroundColor = bgColor;
2526
3113
  micButton.style.borderColor = borderColor;
2527
3114
  const resolvedColor = iconColor || "currentColor";
@@ -2553,7 +3140,7 @@ export const createAgentExperience = (
2553
3140
  ?? (interruptionMode === "barge-in" ? (voiceConfig.recordingBorderColor ?? "") : (originalMicStyles?.borderColor ?? ""));
2554
3141
 
2555
3142
  removeAllVoiceStateClasses();
2556
- micButton.classList.add("tvw-voice-speaking");
3143
+ micButton.classList.add("persona-voice-speaking");
2557
3144
  micButton.style.backgroundColor = bgColor;
2558
3145
  micButton.style.borderColor = borderColor;
2559
3146
  const resolvedColor = iconColor || "currentColor";
@@ -2573,7 +3160,7 @@ export const createAgentExperience = (
2573
3160
  }
2574
3161
  // In "barge-in" mode, add recording class to show mic is hot
2575
3162
  if (interruptionMode === "barge-in") {
2576
- micButton.classList.add("tvw-voice-recording");
3163
+ micButton.classList.add("persona-voice-recording");
2577
3164
  }
2578
3165
  };
2579
3166
 
@@ -2660,6 +3247,8 @@ export const createAgentExperience = (
2660
3247
  }
2661
3248
  };
2662
3249
 
3250
+ composerVoiceBridge = handleMicButtonClick;
3251
+
2663
3252
  if (micButton) {
2664
3253
  micButton.addEventListener("click", handleMicButtonClick);
2665
3254
 
@@ -2763,26 +3352,48 @@ export const createAgentExperience = (
2763
3352
  }
2764
3353
 
2765
3354
  const recalcPanelHeight = () => {
3355
+ const dockedMode = isDockedMountMode(config);
2766
3356
  const sidebarMode = config.launcher?.sidebarMode ?? false;
2767
- const fullHeight = sidebarMode || (config.launcher?.fullHeight ?? false);
2768
-
2769
- if (!launcherEnabled) {
3357
+ const fullHeight = dockedMode || sidebarMode || (config.launcher?.fullHeight ?? false);
3358
+
3359
+ // Mobile fullscreen: re-apply fullscreen styles on resize (handles orientation changes)
3360
+ const ownerWindow = mount.ownerDocument.defaultView ?? window;
3361
+ const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
3362
+ const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
3363
+ const isMobileViewport = ownerWindow.innerWidth <= mobileBreakpoint;
3364
+ const shouldGoFullscreen = mobileFullscreen && isMobileViewport && launcherEnabled;
3365
+
3366
+ if (shouldGoFullscreen) {
3367
+ applyFullHeightStyles();
3368
+ applyThemeVariables(mount, config);
3369
+ return;
3370
+ }
3371
+
3372
+ // Exiting mobile fullscreen (e.g., orientation change to landscape) — reset all styles
3373
+ if (wasMobileFullscreen) {
3374
+ wasMobileFullscreen = false;
3375
+ applyFullHeightStyles();
3376
+ applyThemeVariables(mount, config);
3377
+ }
3378
+
3379
+ if (!launcherEnabled && !dockedMode) {
2770
3380
  panel.style.height = "";
2771
3381
  panel.style.width = "";
2772
3382
  return;
2773
3383
  }
2774
-
3384
+
2775
3385
  // In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
2776
- if (!sidebarMode) {
3386
+ if (!sidebarMode && !dockedMode) {
2777
3387
  const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
2778
3388
  const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
2779
3389
  panel.style.width = width;
2780
3390
  panel.style.maxWidth = width;
2781
3391
  }
2782
-
3392
+ applyLauncherArtifactPanelWidth();
3393
+
2783
3394
  // In fullHeight mode, don't set a fixed height
2784
3395
  if (!fullHeight) {
2785
- const viewportHeight = window.innerHeight;
3396
+ const viewportHeight = ownerWindow.innerHeight;
2786
3397
  const verticalMargin = 64; // leave space for launcher's offset
2787
3398
  const heightOffset = config.launcher?.heightOffset ?? 0;
2788
3399
  const available = Math.max(200, viewportHeight - verticalMargin);
@@ -2793,8 +3404,9 @@ export const createAgentExperience = (
2793
3404
  };
2794
3405
 
2795
3406
  recalcPanelHeight();
2796
- window.addEventListener("resize", recalcPanelHeight);
2797
- destroyCallbacks.push(() => window.removeEventListener("resize", recalcPanelHeight));
3407
+ const ownerWindow = mount.ownerDocument.defaultView ?? window;
3408
+ ownerWindow.addEventListener("resize", recalcPanelHeight);
3409
+ destroyCallbacks.push(() => ownerWindow.removeEventListener("resize", recalcPanelHeight));
2798
3410
 
2799
3411
  lastScrollTop = body.scrollTop;
2800
3412
 
@@ -2837,8 +3449,7 @@ export const createAgentExperience = (
2837
3449
  if (launcherEnabled) {
2838
3450
  closeButton.style.display = "";
2839
3451
  closeHandler = () => {
2840
- open = false;
2841
- updateOpenState();
3452
+ setOpenState(false, "user");
2842
3453
  };
2843
3454
  closeButton.addEventListener("click", closeHandler);
2844
3455
  } else {
@@ -2856,6 +3467,7 @@ export const createAgentExperience = (
2856
3467
  clearChatButton.addEventListener("click", () => {
2857
3468
  // Clear messages in session (this will trigger onMessagesChanged which re-renders)
2858
3469
  session.clearMessages();
3470
+ messageCache.clear();
2859
3471
 
2860
3472
  // Always clear the default localStorage key
2861
3473
  try {
@@ -2914,14 +3526,18 @@ export const createAgentExperience = (
2914
3526
 
2915
3527
  setupClearChatButton();
2916
3528
 
2917
- composerForm.addEventListener("submit", handleSubmit);
2918
- textarea.addEventListener("keydown", handleInputEnter);
2919
- textarea.addEventListener("paste", handleInputPaste);
3529
+ if (composerForm) {
3530
+ composerForm.addEventListener("submit", handleSubmit);
3531
+ }
3532
+ textarea?.addEventListener("keydown", handleInputEnter);
3533
+ textarea?.addEventListener("paste", handleInputPaste);
2920
3534
 
2921
3535
  destroyCallbacks.push(() => {
2922
- composerForm.removeEventListener("submit", handleSubmit);
2923
- textarea.removeEventListener("keydown", handleInputEnter);
2924
- textarea.removeEventListener("paste", handleInputPaste);
3536
+ if (composerForm) {
3537
+ composerForm.removeEventListener("submit", handleSubmit);
3538
+ }
3539
+ textarea?.removeEventListener("keydown", handleInputEnter);
3540
+ textarea?.removeEventListener("paste", handleInputPaste);
2925
3541
  });
2926
3542
 
2927
3543
  destroyCallbacks.push(() => {
@@ -2941,11 +3557,16 @@ export const createAgentExperience = (
2941
3557
  const controller: Controller = {
2942
3558
  update(nextConfig: AgentWidgetConfig) {
2943
3559
  const previousToolCallConfig = config.toolCall;
3560
+ const previousMessageActions = config.messageActions;
3561
+ const previousLayoutMessages = config.layout?.messages;
2944
3562
  const previousColorScheme = config.colorScheme;
2945
3563
  config = { ...config, ...nextConfig };
2946
3564
  // applyFullHeightStyles resets mount.style.cssText, so call it before applyThemeVariables
2947
3565
  applyFullHeightStyles();
2948
3566
  applyThemeVariables(mount, config);
3567
+ applyArtifactLayoutCssVars(mount, config);
3568
+ applyArtifactPaneAppearance(mount, config);
3569
+ syncArtifactPane();
2949
3570
 
2950
3571
  // Re-setup theme observer if colorScheme changed
2951
3572
  if (config.colorScheme !== previousColorScheme) {
@@ -2984,7 +3605,7 @@ export const createAgentExperience = (
2984
3605
  // Add header toggle button if not present
2985
3606
  if (!eventStreamToggleBtn && header) {
2986
3607
  const dynEsClassNames = config.features?.eventStream?.classNames;
2987
- const dynToggleBtnClasses = "tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-text-cw-muted hover:tvw-bg-gray-100 tvw-cursor-pointer tvw-border-none tvw-bg-transparent tvw-p-1" + (dynEsClassNames?.toggleButton ? " " + dynEsClassNames.toggleButton : "");
3608
+ const dynToggleBtnClasses = "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full persona-text-persona-muted hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none persona-bg-transparent persona-p-1" + (dynEsClassNames?.toggleButton ? " " + dynEsClassNames.toggleButton : "");
2988
3609
  eventStreamToggleBtn = createElement("button", dynToggleBtnClasses) as HTMLButtonElement;
2989
3610
  eventStreamToggleBtn.style.width = "28px";
2990
3611
  eventStreamToggleBtn.style.height = "28px";
@@ -3116,11 +3737,11 @@ export const createAgentExperience = (
3116
3737
  panelElements.clearChatButtonWrapper.style.display = showClearChat ? "" : "none";
3117
3738
  // When clear chat is hidden, close button needs ml-auto to stay right-aligned
3118
3739
  const { closeButtonWrapper } = panelElements;
3119
- if (closeButtonWrapper && !closeButtonWrapper.classList.contains("tvw-absolute")) {
3740
+ if (closeButtonWrapper && !closeButtonWrapper.classList.contains("persona-absolute")) {
3120
3741
  if (showClearChat) {
3121
- closeButtonWrapper.classList.remove("tvw-ml-auto");
3742
+ closeButtonWrapper.classList.remove("persona-ml-auto");
3122
3743
  } else {
3123
- closeButtonWrapper.classList.add("tvw-ml-auto");
3744
+ closeButtonWrapper.classList.add("persona-ml-auto");
3124
3745
  }
3125
3746
  }
3126
3747
  }
@@ -3165,9 +3786,13 @@ export const createAgentExperience = (
3165
3786
  recalcPanelHeight();
3166
3787
  refreshCloseButton();
3167
3788
 
3168
- // Re-render messages if toolCall config changed (to apply new styles)
3789
+ // Re-render messages if config affecting message rendering changed
3169
3790
  const toolCallConfigChanged = JSON.stringify(nextConfig.toolCall) !== JSON.stringify(previousToolCallConfig);
3170
- if (toolCallConfigChanged && session) {
3791
+ const messageActionsChanged = JSON.stringify(config.messageActions) !== JSON.stringify(previousMessageActions);
3792
+ const layoutMessagesChanged = JSON.stringify(config.layout?.messages) !== JSON.stringify(previousLayoutMessages);
3793
+ const messagesConfigChanged = toolCallConfigChanged || messageActionsChanged || layoutMessagesChanged;
3794
+ if (messagesConfigChanged && session) {
3795
+ configVersion++;
3171
3796
  renderMessagesWithPlugins(messagesWrapper, session.getMessages(), postprocess);
3172
3797
  }
3173
3798
 
@@ -3181,8 +3806,8 @@ export const createAgentExperience = (
3181
3806
  const headerIconSize = launcher.headerIconSize ?? "48px";
3182
3807
 
3183
3808
  if (iconHolder) {
3184
- const headerEl = container.querySelector(".tvw-border-b-cw-divider");
3185
- const headerCopy = headerEl?.querySelector(".tvw-flex-col");
3809
+ const headerEl = container.querySelector(".persona-border-b-persona-divider");
3810
+ const headerCopy = headerEl?.querySelector(".persona-flex-col");
3186
3811
 
3187
3812
  // Handle hide/show
3188
3813
  if (shouldHideIcon) {
@@ -3232,7 +3857,7 @@ export const createAgentExperience = (
3232
3857
  const newImg = document.createElement("img");
3233
3858
  newImg.src = launcher.iconUrl;
3234
3859
  newImg.alt = "";
3235
- newImg.className = "tvw-rounded-xl tvw-object-cover";
3860
+ newImg.className = "persona-rounded-xl persona-object-cover";
3236
3861
  newImg.style.height = headerIconSize;
3237
3862
  newImg.style.width = headerIconSize;
3238
3863
  iconHolder.replaceChildren(newImg);
@@ -3283,7 +3908,7 @@ export const createAgentExperience = (
3283
3908
  // Update placement if changed - move the wrapper (not just the button) to preserve tooltip
3284
3909
  const { closeButtonWrapper } = panelElements;
3285
3910
  const isTopRight = closeButtonPlacement === "top-right";
3286
- const currentlyTopRight = closeButtonWrapper?.classList.contains("tvw-absolute");
3911
+ const currentlyTopRight = closeButtonWrapper?.classList.contains("persona-absolute");
3287
3912
 
3288
3913
  if (closeButtonWrapper && isTopRight !== currentlyTopRight) {
3289
3914
  // Placement changed - need to move wrapper and update classes
@@ -3291,16 +3916,16 @@ export const createAgentExperience = (
3291
3916
 
3292
3917
  // Update wrapper classes
3293
3918
  if (isTopRight) {
3294
- closeButtonWrapper.className = "tvw-absolute tvw-top-4 tvw-right-4 tvw-z-50";
3919
+ closeButtonWrapper.className = "persona-absolute persona-top-4 persona-right-4 persona-z-50";
3295
3920
  container.style.position = "relative";
3296
3921
  container.appendChild(closeButtonWrapper);
3297
3922
  } else {
3298
3923
  // Check if clear chat is inline to determine if we need ml-auto
3299
3924
  const clearChatPlacement = launcher.clearChat?.placement ?? "inline";
3300
3925
  const clearChatEnabled = launcher.clearChat?.enabled ?? true;
3301
- closeButtonWrapper.className = (clearChatEnabled && clearChatPlacement === "inline") ? "" : "tvw-ml-auto";
3926
+ closeButtonWrapper.className = (clearChatEnabled && clearChatPlacement === "inline") ? "" : "persona-ml-auto";
3302
3927
  // Find header element
3303
- const header = container.querySelector(".tvw-border-b-cw-divider");
3928
+ const header = container.querySelector(".persona-border-b-persona-divider");
3304
3929
  if (header) {
3305
3930
  header.appendChild(closeButtonWrapper);
3306
3931
  }
@@ -3310,18 +3935,18 @@ export const createAgentExperience = (
3310
3935
  // Apply close button styling from config
3311
3936
  if (launcher.closeButtonColor) {
3312
3937
  closeButton.style.color = launcher.closeButtonColor;
3313
- closeButton.classList.remove("tvw-text-cw-muted");
3938
+ closeButton.classList.remove("persona-text-persona-muted");
3314
3939
  } else {
3315
3940
  closeButton.style.color = "";
3316
- closeButton.classList.add("tvw-text-cw-muted");
3941
+ closeButton.classList.add("persona-text-persona-muted");
3317
3942
  }
3318
3943
 
3319
3944
  if (launcher.closeButtonBackgroundColor) {
3320
3945
  closeButton.style.backgroundColor = launcher.closeButtonBackgroundColor;
3321
- closeButton.classList.remove("hover:tvw-bg-gray-100");
3946
+ closeButton.classList.remove("hover:persona-bg-gray-100");
3322
3947
  } else {
3323
3948
  closeButton.style.backgroundColor = "";
3324
- closeButton.classList.add("hover:tvw-bg-gray-100");
3949
+ closeButton.classList.add("hover:persona-bg-gray-100");
3325
3950
  }
3326
3951
 
3327
3952
  // Apply border if width and/or color are provided
@@ -3329,18 +3954,18 @@ export const createAgentExperience = (
3329
3954
  const borderWidth = launcher.closeButtonBorderWidth || "0px";
3330
3955
  const borderColor = launcher.closeButtonBorderColor || "transparent";
3331
3956
  closeButton.style.border = `${borderWidth} solid ${borderColor}`;
3332
- closeButton.classList.remove("tvw-border-none");
3957
+ closeButton.classList.remove("persona-border-none");
3333
3958
  } else {
3334
3959
  closeButton.style.border = "";
3335
- closeButton.classList.add("tvw-border-none");
3960
+ closeButton.classList.add("persona-border-none");
3336
3961
  }
3337
3962
 
3338
3963
  if (launcher.closeButtonBorderRadius) {
3339
3964
  closeButton.style.borderRadius = launcher.closeButtonBorderRadius;
3340
- closeButton.classList.remove("tvw-rounded-full");
3965
+ closeButton.classList.remove("persona-rounded-full");
3341
3966
  } else {
3342
3967
  closeButton.style.borderRadius = "";
3343
- closeButton.classList.add("tvw-rounded-full");
3968
+ closeButton.classList.add("persona-rounded-full");
3344
3969
  }
3345
3970
 
3346
3971
  // Update padding
@@ -3392,13 +4017,21 @@ export const createAgentExperience = (
3392
4017
  const showTooltip = () => {
3393
4018
  if (portaledTooltip || !closeButton) return; // Already showing or button doesn't exist
3394
4019
 
4020
+ const tooltipDocument = closeButton.ownerDocument;
4021
+ const tooltipContainer = tooltipDocument.body;
4022
+ if (!tooltipContainer) return;
4023
+
3395
4024
  // Create tooltip element
3396
- portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
4025
+ portaledTooltip = createElementInDocument(
4026
+ tooltipDocument,
4027
+ "div",
4028
+ "persona-clear-chat-tooltip"
4029
+ );
3397
4030
  portaledTooltip.textContent = closeButtonTooltipText;
3398
4031
 
3399
4032
  // Add arrow
3400
- const arrow = createElement("div");
3401
- arrow.className = "tvw-clear-chat-tooltip-arrow";
4033
+ const arrow = createElementInDocument(tooltipDocument, "div");
4034
+ arrow.className = "persona-clear-chat-tooltip-arrow";
3402
4035
  portaledTooltip.appendChild(arrow);
3403
4036
 
3404
4037
  // Get button position
@@ -3411,7 +4044,7 @@ export const createAgentExperience = (
3411
4044
  portaledTooltip.style.transform = "translate(-50%, -100%)";
3412
4045
 
3413
4046
  // Append to body
3414
- document.body.appendChild(portaledTooltip);
4047
+ tooltipContainer.appendChild(portaledTooltip);
3415
4048
  };
3416
4049
 
3417
4050
  const hideTooltip = () => {
@@ -3462,36 +4095,36 @@ export const createAgentExperience = (
3462
4095
 
3463
4096
  // When clear chat is hidden, close button needs ml-auto to stay right-aligned
3464
4097
  const { closeButtonWrapper } = panelElements;
3465
- if (closeButtonWrapper && !closeButtonWrapper.classList.contains("tvw-absolute")) {
4098
+ if (closeButtonWrapper && !closeButtonWrapper.classList.contains("persona-absolute")) {
3466
4099
  if (shouldShowClearChat) {
3467
- closeButtonWrapper.classList.remove("tvw-ml-auto");
4100
+ closeButtonWrapper.classList.remove("persona-ml-auto");
3468
4101
  } else {
3469
- closeButtonWrapper.classList.add("tvw-ml-auto");
4102
+ closeButtonWrapper.classList.add("persona-ml-auto");
3470
4103
  }
3471
4104
  }
3472
4105
 
3473
4106
  // Update placement if changed
3474
4107
  const isTopRight = clearChatPlacement === "top-right";
3475
- const currentlyTopRight = clearChatButtonWrapper.classList.contains("tvw-absolute");
4108
+ const currentlyTopRight = clearChatButtonWrapper.classList.contains("persona-absolute");
3476
4109
 
3477
4110
  if (isTopRight !== currentlyTopRight && shouldShowClearChat) {
3478
4111
  clearChatButtonWrapper.remove();
3479
4112
 
3480
4113
  if (isTopRight) {
3481
- // Don't use tvw-clear-chat-button-wrapper class for top-right mode as its
4114
+ // Don't use persona-clear-chat-button-wrapper class for top-right mode as its
3482
4115
  // display: inline-flex causes alignment issues with the close button
3483
- clearChatButtonWrapper.className = "tvw-absolute tvw-top-4 tvw-z-50";
4116
+ clearChatButtonWrapper.className = "persona-absolute persona-top-4 persona-z-50";
3484
4117
  // Position to the left of the close button (which is at right: 1rem/16px)
3485
4118
  // Close button is ~32px wide, plus small gap = 48px from right
3486
4119
  clearChatButtonWrapper.style.right = "48px";
3487
4120
  container.style.position = "relative";
3488
4121
  container.appendChild(clearChatButtonWrapper);
3489
4122
  } else {
3490
- clearChatButtonWrapper.className = "tvw-relative tvw-ml-auto tvw-clear-chat-button-wrapper";
4123
+ clearChatButtonWrapper.className = "persona-relative persona-ml-auto persona-clear-chat-button-wrapper";
3491
4124
  // Clear the inline right style when switching back to inline mode
3492
4125
  clearChatButtonWrapper.style.right = "";
3493
4126
  // Find header and insert before close button
3494
- const header = container.querySelector(".tvw-border-b-cw-divider");
4127
+ const header = container.querySelector(".persona-border-b-persona-divider");
3495
4128
  const closeButtonWrapperEl = panelElements.closeButtonWrapper;
3496
4129
  if (header && closeButtonWrapperEl && closeButtonWrapperEl.parentElement === header) {
3497
4130
  header.insertBefore(clearChatButtonWrapper, closeButtonWrapperEl);
@@ -3502,13 +4135,13 @@ export const createAgentExperience = (
3502
4135
 
3503
4136
  // Also update close button's ml-auto class based on clear chat position
3504
4137
  const closeButtonWrapperEl = panelElements.closeButtonWrapper;
3505
- if (closeButtonWrapperEl && !closeButtonWrapperEl.classList.contains("tvw-absolute")) {
4138
+ if (closeButtonWrapperEl && !closeButtonWrapperEl.classList.contains("persona-absolute")) {
3506
4139
  if (isTopRight) {
3507
4140
  // Clear chat moved to top-right, close needs ml-auto
3508
- closeButtonWrapperEl.classList.add("tvw-ml-auto");
4141
+ closeButtonWrapperEl.classList.add("persona-ml-auto");
3509
4142
  } else {
3510
4143
  // Clear chat is inline, close doesn't need ml-auto
3511
- closeButtonWrapperEl.classList.remove("tvw-ml-auto");
4144
+ closeButtonWrapperEl.classList.remove("persona-ml-auto");
3512
4145
  }
3513
4146
  }
3514
4147
  }
@@ -3534,19 +4167,19 @@ export const createAgentExperience = (
3534
4167
  // Update icon color
3535
4168
  if (clearChatIconColor) {
3536
4169
  clearChatButton.style.color = clearChatIconColor;
3537
- clearChatButton.classList.remove("tvw-text-cw-muted");
4170
+ clearChatButton.classList.remove("persona-text-persona-muted");
3538
4171
  } else {
3539
4172
  clearChatButton.style.color = "";
3540
- clearChatButton.classList.add("tvw-text-cw-muted");
4173
+ clearChatButton.classList.add("persona-text-persona-muted");
3541
4174
  }
3542
4175
 
3543
4176
  // Update background color
3544
4177
  if (clearChatConfig.backgroundColor) {
3545
4178
  clearChatButton.style.backgroundColor = clearChatConfig.backgroundColor;
3546
- clearChatButton.classList.remove("hover:tvw-bg-gray-100");
4179
+ clearChatButton.classList.remove("hover:persona-bg-gray-100");
3547
4180
  } else {
3548
4181
  clearChatButton.style.backgroundColor = "";
3549
- clearChatButton.classList.add("hover:tvw-bg-gray-100");
4182
+ clearChatButton.classList.add("hover:persona-bg-gray-100");
3550
4183
  }
3551
4184
 
3552
4185
  // Update border
@@ -3554,19 +4187,19 @@ export const createAgentExperience = (
3554
4187
  const borderWidth = clearChatConfig.borderWidth || "0px";
3555
4188
  const borderColor = clearChatConfig.borderColor || "transparent";
3556
4189
  clearChatButton.style.border = `${borderWidth} solid ${borderColor}`;
3557
- clearChatButton.classList.remove("tvw-border-none");
4190
+ clearChatButton.classList.remove("persona-border-none");
3558
4191
  } else {
3559
4192
  clearChatButton.style.border = "";
3560
- clearChatButton.classList.add("tvw-border-none");
4193
+ clearChatButton.classList.add("persona-border-none");
3561
4194
  }
3562
4195
 
3563
4196
  // Update border radius
3564
4197
  if (clearChatConfig.borderRadius) {
3565
4198
  clearChatButton.style.borderRadius = clearChatConfig.borderRadius;
3566
- clearChatButton.classList.remove("tvw-rounded-full");
4199
+ clearChatButton.classList.remove("persona-rounded-full");
3567
4200
  } else {
3568
4201
  clearChatButton.style.borderRadius = "";
3569
- clearChatButton.classList.add("tvw-rounded-full");
4202
+ clearChatButton.classList.add("persona-rounded-full");
3570
4203
  }
3571
4204
 
3572
4205
  // Update padding
@@ -3604,13 +4237,21 @@ export const createAgentExperience = (
3604
4237
  const showTooltip = () => {
3605
4238
  if (portaledTooltip || !clearChatButton) return; // Already showing or button doesn't exist
3606
4239
 
4240
+ const tooltipDocument = clearChatButton.ownerDocument;
4241
+ const tooltipContainer = tooltipDocument.body;
4242
+ if (!tooltipContainer) return;
4243
+
3607
4244
  // Create tooltip element
3608
- portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
4245
+ portaledTooltip = createElementInDocument(
4246
+ tooltipDocument,
4247
+ "div",
4248
+ "persona-clear-chat-tooltip"
4249
+ );
3609
4250
  portaledTooltip.textContent = clearChatTooltipText;
3610
4251
 
3611
4252
  // Add arrow
3612
- const arrow = createElement("div");
3613
- arrow.className = "tvw-clear-chat-tooltip-arrow";
4253
+ const arrow = createElementInDocument(tooltipDocument, "div");
4254
+ arrow.className = "persona-clear-chat-tooltip-arrow";
3614
4255
  portaledTooltip.appendChild(arrow);
3615
4256
 
3616
4257
  // Get button position
@@ -3623,7 +4264,7 @@ export const createAgentExperience = (
3623
4264
  portaledTooltip.style.transform = "translate(-50%, -100%)";
3624
4265
 
3625
4266
  // Append to body
3626
- document.body.appendChild(portaledTooltip);
4267
+ tooltipContainer.appendChild(portaledTooltip);
3627
4268
  };
3628
4269
 
3629
4270
  const hideTooltip = () => {
@@ -3744,18 +4385,18 @@ export const createAgentExperience = (
3744
4385
  const backgroundColor = voiceConfig.backgroundColor ?? sendButtonConfig.backgroundColor;
3745
4386
  if (backgroundColor) {
3746
4387
  micButton.style.backgroundColor = backgroundColor;
3747
- micButton.classList.remove("tvw-bg-cw-primary");
4388
+ micButton.classList.remove("persona-bg-persona-primary");
3748
4389
  } else {
3749
4390
  micButton.style.backgroundColor = "";
3750
- micButton.classList.add("tvw-bg-cw-primary");
4391
+ micButton.classList.add("persona-bg-persona-primary");
3751
4392
  }
3752
4393
 
3753
4394
  if (iconColor) {
3754
4395
  micButton.style.color = iconColor;
3755
- micButton.classList.remove("tvw-text-white");
4396
+ micButton.classList.remove("persona-text-white");
3756
4397
  } else if (!iconColor && !sendButtonConfig.textColor) {
3757
4398
  micButton.style.color = "";
3758
- micButton.classList.add("tvw-text-white");
4399
+ micButton.classList.add("persona-text-white");
3759
4400
  }
3760
4401
 
3761
4402
  // Update border styling
@@ -3789,14 +4430,14 @@ export const createAgentExperience = (
3789
4430
  }
3790
4431
 
3791
4432
  // Update tooltip
3792
- const tooltip = micButtonWrapper?.querySelector(".tvw-send-button-tooltip") as HTMLElement | null;
4433
+ const tooltip = micButtonWrapper?.querySelector(".persona-send-button-tooltip") as HTMLElement | null;
3793
4434
  const tooltipText = voiceConfig.tooltipText ?? "Start voice recognition";
3794
4435
  const showTooltip = voiceConfig.showTooltip ?? false;
3795
4436
  if (showTooltip && tooltipText) {
3796
4437
  if (!tooltip) {
3797
4438
  // Create tooltip if it doesn't exist
3798
4439
  const newTooltip = document.createElement("div");
3799
- newTooltip.className = "tvw-send-button-tooltip";
4440
+ newTooltip.className = "persona-send-button-tooltip";
3800
4441
  newTooltip.textContent = tooltipText;
3801
4442
  micButtonWrapper?.insertBefore(newTooltip, micButton);
3802
4443
  } else {
@@ -3837,7 +4478,7 @@ export const createAgentExperience = (
3837
4478
 
3838
4479
  // Create previews container if not exists
3839
4480
  if (!attachmentPreviewsContainer) {
3840
- attachmentPreviewsContainer = createElement("div", "tvw-attachment-previews tvw-flex tvw-flex-wrap tvw-gap-2 tvw-mb-2");
4481
+ attachmentPreviewsContainer = createElement("div", "persona-attachment-previews persona-flex persona-flex-wrap persona-gap-2 persona-mb-2");
3841
4482
  attachmentPreviewsContainer.style.display = "none";
3842
4483
  composerForm.insertBefore(attachmentPreviewsContainer, textarea);
3843
4484
  }
@@ -3854,12 +4495,12 @@ export const createAgentExperience = (
3854
4495
  }
3855
4496
 
3856
4497
  // Create attachment button wrapper
3857
- attachmentButtonWrapper = createElement("div", "tvw-send-button-wrapper");
4498
+ attachmentButtonWrapper = createElement("div", "persona-send-button-wrapper");
3858
4499
 
3859
4500
  // Create attachment button
3860
4501
  attachmentButton = createElement(
3861
4502
  "button",
3862
- "tvw-rounded-button tvw-flex tvw-items-center tvw-justify-center disabled:tvw-opacity-50 tvw-cursor-pointer tvw-attachment-button"
4503
+ "persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer persona-attachment-button"
3863
4504
  ) as HTMLButtonElement;
3864
4505
  attachmentButton.type = "button";
3865
4506
  attachmentButton.setAttribute("aria-label", attachmentsConfig.buttonTooltipText ?? "Attach file");
@@ -3878,14 +4519,14 @@ export const createAgentExperience = (
3878
4519
  attachmentButton.style.fontSize = "18px";
3879
4520
  attachmentButton.style.lineHeight = "1";
3880
4521
  attachmentButton.style.backgroundColor = "transparent";
3881
- attachmentButton.style.color = "var(--cw-primary, #111827)";
4522
+ attachmentButton.style.color = "var(--persona-primary, #111827)";
3882
4523
  attachmentButton.style.border = "none";
3883
4524
  attachmentButton.style.borderRadius = "6px";
3884
4525
  attachmentButton.style.transition = "background-color 0.15s ease";
3885
4526
 
3886
4527
  // Add hover effect via mouseenter/mouseleave
3887
4528
  attachmentButton.addEventListener("mouseenter", () => {
3888
- attachmentButton!.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
4529
+ attachmentButton!.style.backgroundColor = "var(--persona-palette-colors-black-alpha-50, rgba(0, 0, 0, 0.05))";
3889
4530
  });
3890
4531
  attachmentButton.addEventListener("mouseleave", () => {
3891
4532
  attachmentButton!.style.backgroundColor = "transparent";
@@ -3907,7 +4548,7 @@ export const createAgentExperience = (
3907
4548
 
3908
4549
  // Add tooltip
3909
4550
  const attachTooltipText = attachmentsConfig.buttonTooltipText ?? "Attach file";
3910
- const tooltip = createElement("div", "tvw-send-button-tooltip");
4551
+ const tooltip = createElement("div", "persona-send-button-tooltip");
3911
4552
  tooltip.textContent = attachTooltipText;
3912
4553
  attachmentButtonWrapper.appendChild(tooltip);
3913
4554
 
@@ -3995,7 +4636,7 @@ export const createAgentExperience = (
3995
4636
  if (textColor) {
3996
4637
  sendButton.style.color = textColor;
3997
4638
  } else {
3998
- sendButton.classList.add("tvw-text-white");
4639
+ sendButton.classList.add("persona-text-white");
3999
4640
  }
4000
4641
  }
4001
4642
  } else {
@@ -4003,18 +4644,18 @@ export const createAgentExperience = (
4003
4644
  if (textColor) {
4004
4645
  sendButton.style.color = textColor;
4005
4646
  } else {
4006
- sendButton.classList.add("tvw-text-white");
4647
+ sendButton.classList.add("persona-text-white");
4007
4648
  }
4008
4649
  }
4009
4650
 
4010
4651
  // Update classes
4011
- sendButton.className = "tvw-rounded-button tvw-flex tvw-items-center tvw-justify-center disabled:tvw-opacity-50 tvw-cursor-pointer";
4652
+ sendButton.className = "persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer";
4012
4653
 
4013
4654
  if (backgroundColor) {
4014
4655
  sendButton.style.backgroundColor = backgroundColor;
4015
- sendButton.classList.remove("tvw-bg-cw-primary");
4656
+ sendButton.classList.remove("persona-bg-persona-primary");
4016
4657
  } else {
4017
- sendButton.classList.add("tvw-bg-cw-primary");
4658
+ sendButton.classList.add("persona-bg-persona-primary");
4018
4659
  }
4019
4660
  } else {
4020
4661
  // Text mode: existing behavior
@@ -4027,19 +4668,19 @@ export const createAgentExperience = (
4027
4668
  sendButton.style.lineHeight = "";
4028
4669
 
4029
4670
  // Update classes
4030
- sendButton.className = "tvw-rounded-button tvw-bg-cw-accent tvw-px-4 tvw-py-2 tvw-text-sm tvw-font-semibold tvw-text-white disabled:tvw-opacity-50 tvw-cursor-pointer";
4671
+ sendButton.className = "persona-rounded-button persona-bg-persona-accent persona-px-4 persona-py-2 persona-text-sm persona-font-semibold persona-text-white disabled:persona-opacity-50 persona-cursor-pointer";
4031
4672
 
4032
4673
  if (backgroundColor) {
4033
4674
  sendButton.style.backgroundColor = backgroundColor;
4034
- sendButton.classList.remove("tvw-bg-cw-accent");
4675
+ sendButton.classList.remove("persona-bg-persona-accent");
4035
4676
  } else {
4036
- sendButton.classList.add("tvw-bg-cw-accent");
4677
+ sendButton.classList.add("persona-bg-persona-accent");
4037
4678
  }
4038
4679
 
4039
4680
  if (textColor) {
4040
4681
  sendButton.style.color = textColor;
4041
4682
  } else {
4042
- sendButton.classList.add("tvw-text-white");
4683
+ sendButton.classList.add("persona-text-white");
4043
4684
  }
4044
4685
  }
4045
4686
 
@@ -4074,12 +4715,12 @@ export const createAgentExperience = (
4074
4715
  }
4075
4716
 
4076
4717
  // Update tooltip
4077
- const tooltip = sendButtonWrapper?.querySelector(".tvw-send-button-tooltip") as HTMLElement | null;
4718
+ const tooltip = sendButtonWrapper?.querySelector(".persona-send-button-tooltip") as HTMLElement | null;
4078
4719
  if (showTooltip && tooltipText) {
4079
4720
  if (!tooltip) {
4080
4721
  // Create tooltip if it doesn't exist
4081
4722
  const newTooltip = document.createElement("div");
4082
- newTooltip.className = "tvw-send-button-tooltip";
4723
+ newTooltip.className = "persona-send-button-tooltip";
4083
4724
  newTooltip.textContent = tooltipText;
4084
4725
  sendButtonWrapper?.insertBefore(newTooltip, sendButton);
4085
4726
  } else {
@@ -4122,7 +4763,9 @@ export const createAgentExperience = (
4122
4763
  },
4123
4764
  clearChat() {
4124
4765
  // Clear messages in session (this will trigger onMessagesChanged which re-renders)
4766
+ artifactsPaneUserHidden = false;
4125
4767
  session.clearMessages();
4768
+ messageCache.clear();
4126
4769
 
4127
4770
  // Always clear the default localStorage key
4128
4771
  try {
@@ -4338,6 +4981,31 @@ export const createAgentExperience = (
4338
4981
  isEventStreamVisible(): boolean {
4339
4982
  return eventStreamVisible;
4340
4983
  },
4984
+ showArtifacts(): void {
4985
+ if (!artifactsSidebarEnabled(config)) return;
4986
+ artifactsPaneUserHidden = false;
4987
+ syncArtifactPane();
4988
+ artifactPaneApi?.setMobileOpen(true);
4989
+ },
4990
+ hideArtifacts(): void {
4991
+ if (!artifactsSidebarEnabled(config)) return;
4992
+ artifactsPaneUserHidden = true;
4993
+ syncArtifactPane();
4994
+ },
4995
+ upsertArtifact(manual: PersonaArtifactManualUpsert): PersonaArtifactRecord | null {
4996
+ if (!artifactsSidebarEnabled(config)) return null;
4997
+ // Programmatic adds should surface the pane even if the user previously hit Close.
4998
+ artifactsPaneUserHidden = false;
4999
+ return session.upsertArtifact(manual);
5000
+ },
5001
+ selectArtifact(id: string): void {
5002
+ if (!artifactsSidebarEnabled(config)) return;
5003
+ session.selectArtifact(id);
5004
+ },
5005
+ clearArtifacts(): void {
5006
+ if (!artifactsSidebarEnabled(config)) return;
5007
+ session.clearArtifacts();
5008
+ },
4341
5009
  focusInput(): boolean {
4342
5010
  if (launcherEnabled && !open) return false;
4343
5011
  if (!textarea) return false;
@@ -4397,7 +5065,7 @@ export const createAgentExperience = (
4397
5065
  }
4398
5066
 
4399
5067
  // Remove any existing feedback forms
4400
- const existingFeedback = messagesWrapper.querySelector('.tvw-feedback-container');
5068
+ const existingFeedback = messagesWrapper.querySelector('.persona-feedback-container');
4401
5069
  if (existingFeedback) {
4402
5070
  existingFeedback.remove();
4403
5071
  }
@@ -4424,7 +5092,7 @@ export const createAgentExperience = (
4424
5092
  }
4425
5093
 
4426
5094
  // Remove any existing feedback forms
4427
- const existingFeedback = messagesWrapper.querySelector('.tvw-feedback-container');
5095
+ const existingFeedback = messagesWrapper.querySelector('.persona-feedback-container');
4428
5096
  if (existingFeedback) {
4429
5097
  existingFeedback.remove();
4430
5098
  }
@@ -4523,6 +5191,51 @@ export const createAgentExperience = (
4523
5191
  window.removeEventListener("persona:hideEventStream", handleHideEvent);
4524
5192
  });
4525
5193
  }
5194
+
5195
+ const handleShowArtifacts = (e: Event) => {
5196
+ const detail = (e as CustomEvent).detail;
5197
+ if (!detail?.instanceId || detail.instanceId === instanceId) {
5198
+ controller.showArtifacts();
5199
+ }
5200
+ };
5201
+ const handleHideArtifacts = (e: Event) => {
5202
+ const detail = (e as CustomEvent).detail;
5203
+ if (!detail?.instanceId || detail.instanceId === instanceId) {
5204
+ controller.hideArtifacts();
5205
+ }
5206
+ };
5207
+ const handleUpsertArtifact = (e: Event) => {
5208
+ const detail = (e as CustomEvent).detail;
5209
+ if (detail?.instanceId && detail.instanceId !== instanceId) return;
5210
+ if (detail?.artifact) {
5211
+ controller.upsertArtifact(detail.artifact as PersonaArtifactManualUpsert);
5212
+ }
5213
+ };
5214
+ const handleSelectArtifact = (e: Event) => {
5215
+ const detail = (e as CustomEvent).detail;
5216
+ if (detail?.instanceId && detail.instanceId !== instanceId) return;
5217
+ if (typeof detail?.id === "string") {
5218
+ controller.selectArtifact(detail.id);
5219
+ }
5220
+ };
5221
+ const handleClearArtifacts = (e: Event) => {
5222
+ const detail = (e as CustomEvent).detail;
5223
+ if (!detail?.instanceId || detail.instanceId === instanceId) {
5224
+ controller.clearArtifacts();
5225
+ }
5226
+ };
5227
+ window.addEventListener("persona:showArtifacts", handleShowArtifacts);
5228
+ window.addEventListener("persona:hideArtifacts", handleHideArtifacts);
5229
+ window.addEventListener("persona:upsertArtifact", handleUpsertArtifact);
5230
+ window.addEventListener("persona:selectArtifact", handleSelectArtifact);
5231
+ window.addEventListener("persona:clearArtifacts", handleClearArtifacts);
5232
+ destroyCallbacks.push(() => {
5233
+ window.removeEventListener("persona:showArtifacts", handleShowArtifacts);
5234
+ window.removeEventListener("persona:hideArtifacts", handleHideArtifacts);
5235
+ window.removeEventListener("persona:upsertArtifact", handleUpsertArtifact);
5236
+ window.removeEventListener("persona:selectArtifact", handleSelectArtifact);
5237
+ window.removeEventListener("persona:clearArtifacts", handleClearArtifacts);
5238
+ });
4526
5239
  }
4527
5240
 
4528
5241
  // ============================================================================