vanilla-agent 1.28.0 → 1.30.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.
package/dist/widget.css CHANGED
@@ -86,7 +86,8 @@
86
86
  gap: 1.5rem;
87
87
  }
88
88
 
89
- :root {
89
+ /* Widget CSS Variables - scoped to widget root to avoid polluting global namespace */
90
+ #vanilla-agent-root {
90
91
  --cw-radius-sm: 0.75rem;
91
92
  --cw-radius-md: 1rem;
92
93
  --cw-radius-lg: 1.5rem;
@@ -119,19 +120,19 @@
119
120
  --cw-md-h6-margin: 0.5rem 0 0.25rem;
120
121
  --cw-md-h6-line-height: 1.5;
121
122
 
122
- /* Markdown Table Variables (defaults - theme-aware values set on #vanilla-agent-root) */
123
+ /* Markdown Table Variables */
123
124
  --cw-md-table-border-color: #e5e7eb;
124
125
  --cw-md-table-header-bg: #f8fafc;
125
126
  --cw-md-table-header-weight: 600;
126
127
  --cw-md-table-cell-padding: 0.5rem 0.75rem;
127
128
  --cw-md-table-border-radius: 0.375rem;
128
129
 
129
- /* Markdown Horizontal Rule Variables (defaults - theme-aware values set on #vanilla-agent-root) */
130
+ /* Markdown Horizontal Rule Variables */
130
131
  --cw-md-hr-color: #e5e7eb;
131
132
  --cw-md-hr-height: 1px;
132
133
  --cw-md-hr-margin: 1rem 0;
133
134
 
134
- /* Markdown Blockquote Variables (defaults - theme-aware values set on #vanilla-agent-root) */
135
+ /* Markdown Blockquote Variables */
135
136
  --cw-md-blockquote-border-color: #3b82f6;
136
137
  --cw-md-blockquote-border-width: 3px;
137
138
  --cw-md-blockquote-padding: 0.5rem 1rem;
@@ -140,7 +141,7 @@
140
141
  --cw-md-blockquote-text-color: #6b7280;
141
142
  --cw-md-blockquote-font-style: italic;
142
143
 
143
- /* Markdown Code Block Variables (defaults - theme-aware values set on #vanilla-agent-root) */
144
+ /* Markdown Code Block Variables */
144
145
  --cw-md-code-block-bg: #f3f4f6;
145
146
  --cw-md-code-block-border-color: #e5e7eb;
146
147
  --cw-md-code-block-text-color: inherit;
@@ -148,7 +149,7 @@
148
149
  --cw-md-code-block-border-radius: 0.375rem;
149
150
  --cw-md-code-block-font-size: 0.875rem;
150
151
 
151
- /* Markdown Inline Code Variables (defaults - theme-aware values set on #vanilla-agent-root) */
152
+ /* Markdown Inline Code Variables */
152
153
  --cw-md-inline-code-bg: #f3f4f6;
153
154
  --cw-md-inline-code-padding: 0.125rem 0.375rem;
154
155
  --cw-md-inline-code-border-radius: 0.25rem;
@@ -677,7 +678,7 @@
677
678
  }
678
679
 
679
680
  /* Ensure textarea in composer form has no border on focus */
680
- textarea:focus {
681
+ .tvw-widget-composer textarea:focus {
681
682
  border: none !important;
682
683
  outline: none !important;
683
684
  border-width: 0 !important;
@@ -687,11 +688,11 @@ textarea:focus {
687
688
  }
688
689
 
689
690
  /* Prevent form container from showing focus styles */
690
- form:focus-within {
691
+ .tvw-widget-composer:focus-within {
691
692
  outline: none !important;
692
693
  }
693
694
 
694
- form:focus-within textarea {
695
+ .tvw-widget-composer:focus-within textarea {
695
696
  border: none !important;
696
697
  outline: none !important;
697
698
  }
@@ -804,7 +805,7 @@ form:focus-within textarea {
804
805
  }
805
806
 
806
807
  /* Typing indicator animation */
807
- @keyframes typing {
808
+ @keyframes tvw-typing {
808
809
  0%, 100% {
809
810
  opacity: 0.5;
810
811
  transform: translateY(0);
@@ -816,7 +817,7 @@ form:focus-within textarea {
816
817
  }
817
818
 
818
819
  .tvw-animate-typing {
819
- animation: typing 1s infinite;
820
+ animation: tvw-typing 1s infinite;
820
821
  }
821
822
 
822
823
  .tvw-space-x-1 > * + * {
@@ -848,7 +849,7 @@ form:focus-within textarea {
848
849
  }
849
850
 
850
851
  /* Voice recognition recording animation */
851
- @keyframes voice-recording-pulse {
852
+ @keyframes tvw-voice-recording-pulse {
852
853
  0%, 100% {
853
854
  opacity: 1;
854
855
  transform: scale(1);
@@ -860,11 +861,11 @@ form:focus-within textarea {
860
861
  }
861
862
 
862
863
  .tvw-voice-recording {
863
- animation: voice-recording-pulse 1.5s ease-in-out infinite;
864
+ animation: tvw-voice-recording-pulse 1.5s ease-in-out infinite;
864
865
  }
865
866
 
866
867
  .tvw-voice-recording svg {
867
- animation: voice-recording-pulse 1.5s ease-in-out infinite;
868
+ animation: tvw-voice-recording-pulse 1.5s ease-in-out infinite;
868
869
  }
869
870
 
870
871
  /* Markdown content overflow handling */
@@ -1239,8 +1240,12 @@ form:focus-within textarea {
1239
1240
  margin-top: 0.5rem;
1240
1241
  padding-top: 0.5rem;
1241
1242
  border-top: 1px solid var(--cw-divider, #f1f5f9);
1242
- /* Fade in when first shown (for "always" visibility) */
1243
- animation: tvw-message-actions-fade-in 0.3s ease-out;
1243
+ }
1244
+
1245
+ /* Fade in animation only for "always" visibility mode (not hover) */
1246
+ /* forwards ensures final state is kept, idiomorph preserves element so animation only plays once */
1247
+ .tvw-message-actions:not(.tvw-message-actions-hover) {
1248
+ animation: tvw-message-actions-fade-in 0.3s ease-out forwards;
1244
1249
  }
1245
1250
 
1246
1251
  /* Action bar alignment */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-agent",
3
- "version": "1.28.0",
3
+ "version": "1.30.0",
4
4
  "description": "Themeable, plugable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -23,6 +23,7 @@
23
23
  "src"
24
24
  ],
25
25
  "dependencies": {
26
+ "idiomorph": "^0.7.4",
26
27
  "lucide": "^0.552.0",
27
28
  "marked": "^12.0.2",
28
29
  "partial-json": "^0.1.7",
@@ -0,0 +1,37 @@
1
+ declare module "idiomorph" {
2
+ export interface IdiomorphCallbacks {
3
+ beforeNodeAdded?: (node: Node) => boolean | void;
4
+ afterNodeAdded?: (node: Node) => void;
5
+ beforeNodeMorphed?: (oldNode: Node, newNode: Node) => boolean | void;
6
+ afterNodeMorphed?: (oldNode: Node, newNode: Node) => void;
7
+ beforeNodeRemoved?: (node: Node) => boolean | void;
8
+ afterNodeRemoved?: (node: Node) => void;
9
+ beforeAttributeUpdated?: (
10
+ attributeName: string,
11
+ node: Node,
12
+ mutationType: "update" | "remove"
13
+ ) => boolean | void;
14
+ }
15
+
16
+ export interface IdiomorphOptions {
17
+ morphStyle?: "outerHTML" | "innerHTML";
18
+ ignoreActive?: boolean;
19
+ ignoreActiveValue?: boolean;
20
+ restoreFocus?: boolean;
21
+ callbacks?: IdiomorphCallbacks;
22
+ head?: {
23
+ style?: "merge" | "append" | "morph" | "none";
24
+ };
25
+ }
26
+
27
+ export interface IdiomorphStatic {
28
+ morph(
29
+ oldNode: Element | Document,
30
+ newContent: Element | Node | string,
31
+ options?: IdiomorphOptions
32
+ ): void;
33
+ defaults: IdiomorphOptions;
34
+ }
35
+
36
+ export const Idiomorph: IdiomorphStatic;
37
+ }
@@ -246,6 +246,9 @@ export const createMessageActions = (
246
246
  visibility === "hover" ? "tvw-message-actions-hover" : ""
247
247
  }`
248
248
  );
249
+ // Set id for idiomorph matching (prevents recreation on morph)
250
+ container.id = `actions-${message.id}`;
251
+ container.setAttribute("data-actions-for", message.id);
249
252
 
250
253
  // Track vote state for this message
251
254
  let currentVote: "upvote" | "downvote" | null = null;
@@ -414,6 +417,9 @@ export const createStandardBubble = (
414
417
  // Create the bubble element
415
418
  const classes = getBubbleClasses(message.role, layout);
416
419
  const bubble = createElement("div", classes.join(" "));
420
+ // Set id for idiomorph matching
421
+ bubble.id = `bubble-${message.id}`;
422
+ bubble.setAttribute("data-message-id", message.id);
417
423
 
418
424
  // Add message content
419
425
  const contentDiv = document.createElement("div");
@@ -26,6 +26,9 @@ export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement
26
26
  "tvw-py-0"
27
27
  ].join(" ")
28
28
  );
29
+ // Set id for idiomorph matching
30
+ bubble.id = `bubble-${message.id}`;
31
+ bubble.setAttribute("data-message-id", message.id);
29
32
 
30
33
  if (!reasoning) {
31
34
  return bubble;
@@ -28,6 +28,9 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
28
28
  "tvw-py-0"
29
29
  ].join(" ")
30
30
  );
31
+ // Set id for idiomorph matching
32
+ bubble.id = `bubble-${message.id}`;
33
+ bubble.setAttribute("data-message-id", message.id);
31
34
 
32
35
  // Apply bubble-level styles
33
36
  if (toolCallConfig.backgroundColor) {
@@ -3,7 +3,8 @@
3
3
  @tailwind utilities;
4
4
 
5
5
  @layer base {
6
- :root {
6
+ /* Widget CSS Variables - scoped to widget root to avoid polluting global namespace */
7
+ #vanilla-agent-root {
7
8
  --travrse-primary: #111827;
8
9
  --travrse-secondary: #4338ca;
9
10
  --travrse-surface: #ffffff;
@@ -86,7 +86,8 @@
86
86
  gap: 1.5rem;
87
87
  }
88
88
 
89
- :root {
89
+ /* Widget CSS Variables - scoped to widget root to avoid polluting global namespace */
90
+ #vanilla-agent-root {
90
91
  --cw-radius-sm: 0.75rem;
91
92
  --cw-radius-md: 1rem;
92
93
  --cw-radius-lg: 1.5rem;
@@ -119,19 +120,19 @@
119
120
  --cw-md-h6-margin: 0.5rem 0 0.25rem;
120
121
  --cw-md-h6-line-height: 1.5;
121
122
 
122
- /* Markdown Table Variables (defaults - theme-aware values set on #vanilla-agent-root) */
123
+ /* Markdown Table Variables */
123
124
  --cw-md-table-border-color: #e5e7eb;
124
125
  --cw-md-table-header-bg: #f8fafc;
125
126
  --cw-md-table-header-weight: 600;
126
127
  --cw-md-table-cell-padding: 0.5rem 0.75rem;
127
128
  --cw-md-table-border-radius: 0.375rem;
128
129
 
129
- /* Markdown Horizontal Rule Variables (defaults - theme-aware values set on #vanilla-agent-root) */
130
+ /* Markdown Horizontal Rule Variables */
130
131
  --cw-md-hr-color: #e5e7eb;
131
132
  --cw-md-hr-height: 1px;
132
133
  --cw-md-hr-margin: 1rem 0;
133
134
 
134
- /* Markdown Blockquote Variables (defaults - theme-aware values set on #vanilla-agent-root) */
135
+ /* Markdown Blockquote Variables */
135
136
  --cw-md-blockquote-border-color: #3b82f6;
136
137
  --cw-md-blockquote-border-width: 3px;
137
138
  --cw-md-blockquote-padding: 0.5rem 1rem;
@@ -140,7 +141,7 @@
140
141
  --cw-md-blockquote-text-color: #6b7280;
141
142
  --cw-md-blockquote-font-style: italic;
142
143
 
143
- /* Markdown Code Block Variables (defaults - theme-aware values set on #vanilla-agent-root) */
144
+ /* Markdown Code Block Variables */
144
145
  --cw-md-code-block-bg: #f3f4f6;
145
146
  --cw-md-code-block-border-color: #e5e7eb;
146
147
  --cw-md-code-block-text-color: inherit;
@@ -148,7 +149,7 @@
148
149
  --cw-md-code-block-border-radius: 0.375rem;
149
150
  --cw-md-code-block-font-size: 0.875rem;
150
151
 
151
- /* Markdown Inline Code Variables (defaults - theme-aware values set on #vanilla-agent-root) */
152
+ /* Markdown Inline Code Variables */
152
153
  --cw-md-inline-code-bg: #f3f4f6;
153
154
  --cw-md-inline-code-padding: 0.125rem 0.375rem;
154
155
  --cw-md-inline-code-border-radius: 0.25rem;
@@ -677,7 +678,7 @@
677
678
  }
678
679
 
679
680
  /* Ensure textarea in composer form has no border on focus */
680
- textarea:focus {
681
+ .tvw-widget-composer textarea:focus {
681
682
  border: none !important;
682
683
  outline: none !important;
683
684
  border-width: 0 !important;
@@ -687,11 +688,11 @@ textarea:focus {
687
688
  }
688
689
 
689
690
  /* Prevent form container from showing focus styles */
690
- form:focus-within {
691
+ .tvw-widget-composer:focus-within {
691
692
  outline: none !important;
692
693
  }
693
694
 
694
- form:focus-within textarea {
695
+ .tvw-widget-composer:focus-within textarea {
695
696
  border: none !important;
696
697
  outline: none !important;
697
698
  }
@@ -804,7 +805,7 @@ form:focus-within textarea {
804
805
  }
805
806
 
806
807
  /* Typing indicator animation */
807
- @keyframes typing {
808
+ @keyframes tvw-typing {
808
809
  0%, 100% {
809
810
  opacity: 0.5;
810
811
  transform: translateY(0);
@@ -816,7 +817,7 @@ form:focus-within textarea {
816
817
  }
817
818
 
818
819
  .tvw-animate-typing {
819
- animation: typing 1s infinite;
820
+ animation: tvw-typing 1s infinite;
820
821
  }
821
822
 
822
823
  .tvw-space-x-1 > * + * {
@@ -848,7 +849,7 @@ form:focus-within textarea {
848
849
  }
849
850
 
850
851
  /* Voice recognition recording animation */
851
- @keyframes voice-recording-pulse {
852
+ @keyframes tvw-voice-recording-pulse {
852
853
  0%, 100% {
853
854
  opacity: 1;
854
855
  transform: scale(1);
@@ -860,11 +861,11 @@ form:focus-within textarea {
860
861
  }
861
862
 
862
863
  .tvw-voice-recording {
863
- animation: voice-recording-pulse 1.5s ease-in-out infinite;
864
+ animation: tvw-voice-recording-pulse 1.5s ease-in-out infinite;
864
865
  }
865
866
 
866
867
  .tvw-voice-recording svg {
867
- animation: voice-recording-pulse 1.5s ease-in-out infinite;
868
+ animation: tvw-voice-recording-pulse 1.5s ease-in-out infinite;
868
869
  }
869
870
 
870
871
  /* Markdown content overflow handling */
@@ -1239,8 +1240,12 @@ form:focus-within textarea {
1239
1240
  margin-top: 0.5rem;
1240
1241
  padding-top: 0.5rem;
1241
1242
  border-top: 1px solid var(--cw-divider, #f1f5f9);
1242
- /* Fade in when first shown (for "always" visibility) */
1243
- animation: tvw-message-actions-fade-in 0.3s ease-out;
1243
+ }
1244
+
1245
+ /* Fade in animation only for "always" visibility mode (not hover) */
1246
+ /* forwards ensures final state is kept, idiomorph preserves element so animation only plays once */
1247
+ .tvw-message-actions:not(.tvw-message-actions-hover) {
1248
+ animation: tvw-message-actions-fade-in 0.3s ease-out forwards;
1244
1249
  }
1245
1250
 
1246
1251
  /* Action bar alignment */
package/src/ui.ts CHANGED
@@ -17,9 +17,11 @@ import {
17
17
  import { applyThemeVariables, createThemeObserver } from "./utils/theme";
18
18
  import { renderLucideIcon } from "./utils/icons";
19
19
  import { createElement } from "./utils/dom";
20
+ import { morphMessages } from "./utils/morph";
20
21
  import { statusCopy } from "./utils/constants";
21
22
  import { createLauncherButton } from "./components/launcher";
22
23
  import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
24
+ import { buildHeaderWithLayout } from "./components/header-layouts";
23
25
  import { positionMap } from "./utils/positioning";
24
26
  import type { HeaderElements, ComposerElements } from "./components/panel";
25
27
  import { MessageTransform, MessageActionCallbacks } from "./components/message-bubble";
@@ -235,6 +237,7 @@ export const createAgentExperience = (
235
237
  let autoExpand = config.launcher?.autoExpand ?? false;
236
238
  let prevAutoExpand = autoExpand;
237
239
  let prevLauncherEnabled = launcherEnabled;
240
+ let prevHeaderLayout = config.layout?.header?.layout;
238
241
  let open = launcherEnabled ? autoExpand : true;
239
242
  let postprocess = buildPostprocessor(config, actionManager);
240
243
  let showReasoning = config.features?.showReasoning ?? true;
@@ -904,8 +907,8 @@ export const createAgentExperience = (
904
907
  messages: AgentWidgetMessage[],
905
908
  transform: MessageTransform
906
909
  ) => {
907
- container.innerHTML = "";
908
- const fragment = document.createDocumentFragment();
910
+ // Build new content in a temporary container for morphing
911
+ const tempContainer = document.createElement("div");
909
912
 
910
913
  messages.forEach((message) => {
911
914
  let bubble: HTMLElement | null = null;
@@ -976,8 +979,8 @@ export const createAgentExperience = (
976
979
  });
977
980
  if (componentBubble) {
978
981
  // Wrap component in standard bubble styling
979
- const wrapper = document.createElement("div");
980
- wrapper.className = [
982
+ const componentWrapper = document.createElement("div");
983
+ componentWrapper.className = [
981
984
  "vanilla-message-bubble",
982
985
  "tvw-max-w-[85%]",
983
986
  "tvw-rounded-2xl",
@@ -986,7 +989,9 @@ export const createAgentExperience = (
986
989
  "tvw-border-cw-message-border",
987
990
  "tvw-p-4"
988
991
  ].join(" ");
989
- wrapper.setAttribute("data-message-id", message.id);
992
+ // Set id for idiomorph matching
993
+ componentWrapper.id = `bubble-${message.id}`;
994
+ componentWrapper.setAttribute("data-message-id", message.id);
990
995
 
991
996
  // Add text content above component if present (combined text+component response)
992
997
  if (message.content && message.content.trim()) {
@@ -998,11 +1003,11 @@ export const createAgentExperience = (
998
1003
  streaming: Boolean(message.streaming),
999
1004
  raw: message.rawContent
1000
1005
  });
1001
- wrapper.appendChild(textDiv);
1006
+ componentWrapper.appendChild(textDiv);
1002
1007
  }
1003
1008
 
1004
- wrapper.appendChild(componentBubble);
1005
- bubble = wrapper;
1009
+ componentWrapper.appendChild(componentBubble);
1010
+ bubble = componentWrapper;
1006
1011
  }
1007
1012
  }
1008
1013
  }
@@ -1048,11 +1053,14 @@ export const createAgentExperience = (
1048
1053
 
1049
1054
  const wrapper = document.createElement("div");
1050
1055
  wrapper.className = "tvw-flex";
1056
+ // Set id for idiomorph matching
1057
+ wrapper.id = `wrapper-${message.id}`;
1058
+ wrapper.setAttribute("data-wrapper-id", message.id);
1051
1059
  if (message.role === "user") {
1052
1060
  wrapper.classList.add("tvw-justify-end");
1053
1061
  }
1054
1062
  wrapper.appendChild(bubble);
1055
- fragment.appendChild(wrapper);
1063
+ tempContainer.appendChild(wrapper);
1056
1064
  });
1057
1065
 
1058
1066
  // Add standalone typing indicator only if streaming but no assistant message is streaming yet
@@ -1085,16 +1093,21 @@ export const createAgentExperience = (
1085
1093
  "tvw-px-5",
1086
1094
  "tvw-py-3"
1087
1095
  ].join(" ");
1096
+ typingBubble.setAttribute("data-typing-indicator", "true");
1088
1097
 
1089
1098
  typingBubble.appendChild(typingIndicator);
1090
1099
 
1091
1100
  const typingWrapper = document.createElement("div");
1092
1101
  typingWrapper.className = "tvw-flex";
1102
+ // Set id for idiomorph matching
1103
+ typingWrapper.id = "wrapper-typing-indicator";
1104
+ typingWrapper.setAttribute("data-wrapper-id", "typing-indicator");
1093
1105
  typingWrapper.appendChild(typingBubble);
1094
- fragment.appendChild(typingWrapper);
1106
+ tempContainer.appendChild(typingWrapper);
1095
1107
  }
1096
1108
 
1097
- container.appendChild(fragment);
1109
+ // Use idiomorph to morph the container contents
1110
+ morphMessages(container, tempContainer);
1098
1111
  // Defer scroll to next frame for smoother animation and to prevent jolt
1099
1112
  // This allows the browser to update layout (e.g., typing indicator removal) before scrolling
1100
1113
  // Use double RAF to ensure layout has fully settled before starting scroll animation
@@ -1902,6 +1915,66 @@ export const createAgentExperience = (
1902
1915
  headerSubtitle.textContent = config.launcher.subtitle;
1903
1916
  }
1904
1917
 
1918
+ // Update header layout if it changed
1919
+ const headerLayoutConfig = config.layout?.header;
1920
+ const headerLayoutChanged = headerLayoutConfig?.layout !== prevHeaderLayout;
1921
+
1922
+ if (headerLayoutChanged && header) {
1923
+ // Rebuild header with new layout
1924
+ const newHeaderElements = headerLayoutConfig
1925
+ ? buildHeaderWithLayout(config, headerLayoutConfig, {
1926
+ showClose: launcherEnabled,
1927
+ onClose: () => setOpenState(false, "user")
1928
+ })
1929
+ : buildHeader({
1930
+ config,
1931
+ showClose: launcherEnabled,
1932
+ onClose: () => setOpenState(false, "user")
1933
+ });
1934
+
1935
+ // Replace the old header with the new one
1936
+ header.replaceWith(newHeaderElements.header);
1937
+
1938
+ // Update references
1939
+ header = newHeaderElements.header;
1940
+ iconHolder = newHeaderElements.iconHolder;
1941
+ headerTitle = newHeaderElements.headerTitle;
1942
+ headerSubtitle = newHeaderElements.headerSubtitle;
1943
+ closeButton = newHeaderElements.closeButton;
1944
+
1945
+ prevHeaderLayout = headerLayoutConfig?.layout;
1946
+ } else if (headerLayoutConfig) {
1947
+ // Apply visibility settings without rebuilding
1948
+ if (iconHolder) {
1949
+ iconHolder.style.display = headerLayoutConfig.showIcon === false ? "none" : "";
1950
+ }
1951
+ if (headerTitle) {
1952
+ headerTitle.style.display = headerLayoutConfig.showTitle === false ? "none" : "";
1953
+ }
1954
+ if (headerSubtitle) {
1955
+ headerSubtitle.style.display = headerLayoutConfig.showSubtitle === false ? "none" : "";
1956
+ }
1957
+ if (closeButton) {
1958
+ closeButton.style.display = headerLayoutConfig.showCloseButton === false ? "none" : "";
1959
+ }
1960
+ if (panelElements.clearChatButtonWrapper) {
1961
+ // showClearChat explicitly controls visibility when set
1962
+ const showClearChat = headerLayoutConfig.showClearChat;
1963
+ if (showClearChat !== undefined) {
1964
+ panelElements.clearChatButtonWrapper.style.display = showClearChat ? "" : "none";
1965
+ // When clear chat is hidden, close button needs ml-auto to stay right-aligned
1966
+ const { closeButtonWrapper } = panelElements;
1967
+ if (closeButtonWrapper && !closeButtonWrapper.classList.contains("tvw-absolute")) {
1968
+ if (showClearChat) {
1969
+ closeButtonWrapper.classList.remove("tvw-ml-auto");
1970
+ } else {
1971
+ closeButtonWrapper.classList.add("tvw-ml-auto");
1972
+ }
1973
+ }
1974
+ }
1975
+ }
1976
+ }
1977
+
1905
1978
  // Only update open state if launcher enabled state changed or autoExpand value changed
1906
1979
  const launcherEnabledChanged = launcherEnabled !== prevLauncherEnabled;
1907
1980
  const autoExpandChanged = autoExpand !== prevAutoExpand;
@@ -1937,20 +2010,23 @@ export const createAgentExperience = (
1937
2010
  // Update panel icon sizes
1938
2011
  const launcher = config.launcher ?? {};
1939
2012
  const headerIconHidden = launcher.headerIconHidden ?? false;
2013
+ const layoutShowIcon = config.layout?.header?.showIcon;
2014
+ // Hide icon if either headerIconHidden is true OR layout.header.showIcon is false
2015
+ const shouldHideIcon = headerIconHidden || layoutShowIcon === false;
1940
2016
  const headerIconName = launcher.headerIconName;
1941
2017
  const headerIconSize = launcher.headerIconSize ?? "48px";
1942
-
2018
+
1943
2019
  if (iconHolder) {
1944
- const header = container.querySelector(".tvw-border-b-cw-divider");
1945
- const headerCopy = header?.querySelector(".tvw-flex-col");
1946
-
2020
+ const headerEl = container.querySelector(".tvw-border-b-cw-divider");
2021
+ const headerCopy = headerEl?.querySelector(".tvw-flex-col");
2022
+
1947
2023
  // Handle hide/show
1948
- if (headerIconHidden) {
2024
+ if (shouldHideIcon) {
1949
2025
  // Hide iconHolder
1950
2026
  iconHolder.style.display = "none";
1951
2027
  // Ensure headerCopy is still in header
1952
- if (header && headerCopy && !header.contains(headerCopy)) {
1953
- header.insertBefore(headerCopy, header.firstChild);
2028
+ if (headerEl && headerCopy && !headerEl.contains(headerCopy)) {
2029
+ headerEl.insertBefore(headerCopy, headerEl.firstChild);
1954
2030
  }
1955
2031
  } else {
1956
2032
  // Show iconHolder
@@ -1959,13 +2035,13 @@ export const createAgentExperience = (
1959
2035
  iconHolder.style.width = headerIconSize;
1960
2036
 
1961
2037
  // Ensure iconHolder is before headerCopy in header
1962
- if (header && headerCopy) {
1963
- if (!header.contains(iconHolder)) {
1964
- header.insertBefore(iconHolder, headerCopy);
2038
+ if (headerEl && headerCopy) {
2039
+ if (!headerEl.contains(iconHolder)) {
2040
+ headerEl.insertBefore(iconHolder, headerCopy);
1965
2041
  } else if (iconHolder.nextSibling !== headerCopy) {
1966
2042
  // Reorder if needed
1967
2043
  iconHolder.remove();
1968
- header.insertBefore(iconHolder, headerCopy);
2044
+ headerEl.insertBefore(iconHolder, headerCopy);
1969
2045
  }
1970
2046
  }
1971
2047
 
@@ -2015,7 +2091,26 @@ export const createAgentExperience = (
2015
2091
  }
2016
2092
  }
2017
2093
  }
2094
+
2095
+ // Handle title/subtitle visibility from layout config
2096
+ const layoutShowTitle = config.layout?.header?.showTitle;
2097
+ const layoutShowSubtitle = config.layout?.header?.showSubtitle;
2098
+ if (headerTitle) {
2099
+ headerTitle.style.display = layoutShowTitle === false ? "none" : "";
2100
+ }
2101
+ if (headerSubtitle) {
2102
+ headerSubtitle.style.display = layoutShowSubtitle === false ? "none" : "";
2103
+ }
2104
+
2018
2105
  if (closeButton) {
2106
+ // Handle close button visibility from layout config
2107
+ const layoutShowCloseButton = config.layout?.header?.showCloseButton;
2108
+ if (layoutShowCloseButton === false) {
2109
+ closeButton.style.display = "none";
2110
+ } else {
2111
+ closeButton.style.display = "";
2112
+ }
2113
+
2019
2114
  const closeButtonSize = launcher.closeButtonSize ?? "32px";
2020
2115
  const closeButtonPlacement = launcher.closeButtonPlacement ?? "inline";
2021
2116
  closeButton.style.height = closeButtonSize;
@@ -2189,17 +2284,33 @@ export const createAgentExperience = (
2189
2284
  if (clearChatButton) {
2190
2285
  const clearChatConfig = launcher.clearChat ?? {};
2191
2286
  const clearChatEnabled = clearChatConfig.enabled ?? true;
2287
+ const layoutShowClearChat = config.layout?.header?.showClearChat;
2288
+ // layout.header.showClearChat takes precedence if explicitly set
2289
+ // Otherwise fall back to launcher.clearChat.enabled
2290
+ const shouldShowClearChat = layoutShowClearChat !== undefined
2291
+ ? layoutShowClearChat
2292
+ : clearChatEnabled;
2192
2293
  const clearChatPlacement = clearChatConfig.placement ?? "inline";
2193
2294
 
2194
- // Show/hide button based on enabled state
2295
+ // Show/hide button based on layout config (primary) or launcher config (fallback)
2195
2296
  if (clearChatButtonWrapper) {
2196
- clearChatButtonWrapper.style.display = clearChatEnabled ? "" : "none";
2297
+ clearChatButtonWrapper.style.display = shouldShowClearChat ? "" : "none";
2298
+
2299
+ // When clear chat is hidden, close button needs ml-auto to stay right-aligned
2300
+ const { closeButtonWrapper } = panelElements;
2301
+ if (closeButtonWrapper && !closeButtonWrapper.classList.contains("tvw-absolute")) {
2302
+ if (shouldShowClearChat) {
2303
+ closeButtonWrapper.classList.remove("tvw-ml-auto");
2304
+ } else {
2305
+ closeButtonWrapper.classList.add("tvw-ml-auto");
2306
+ }
2307
+ }
2197
2308
 
2198
2309
  // Update placement if changed
2199
2310
  const isTopRight = clearChatPlacement === "top-right";
2200
2311
  const currentlyTopRight = clearChatButtonWrapper.classList.contains("tvw-absolute");
2201
2312
 
2202
- if (isTopRight !== currentlyTopRight && clearChatEnabled) {
2313
+ if (isTopRight !== currentlyTopRight && shouldShowClearChat) {
2203
2314
  clearChatButtonWrapper.remove();
2204
2315
 
2205
2316
  if (isTopRight) {
@@ -2239,7 +2350,7 @@ export const createAgentExperience = (
2239
2350
  }
2240
2351
  }
2241
2352
 
2242
- if (clearChatEnabled) {
2353
+ if (shouldShowClearChat) {
2243
2354
  // Update size
2244
2355
  const clearChatSize = clearChatConfig.size ?? "32px";
2245
2356
  clearChatButton.style.height = clearChatSize;