vanilla-agent 0.2.0 → 1.1.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/src/ui.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { escapeHtml } from "./postprocessors";
2
- import { ChatWidgetSession, ChatWidgetSessionStatus } from "./session";
3
- import { ChatWidgetConfig, ChatWidgetMessage } from "./types";
2
+ import { AgentWidgetSession, AgentWidgetSessionStatus } from "./session";
3
+ import { AgentWidgetConfig, AgentWidgetMessage } from "./types";
4
4
  import { applyThemeVariables } from "./utils/theme";
5
5
  import { renderLucideIcon } from "./utils/icons";
6
6
  import { createElement } from "./utils/dom";
@@ -8,22 +8,24 @@ import { statusCopy } from "./utils/constants";
8
8
  import { createLauncherButton } from "./components/launcher";
9
9
  import { createWrapper, buildPanel } from "./components/panel";
10
10
  import { MessageTransform } from "./components/message-bubble";
11
- import { createStandardBubble } from "./components/message-bubble";
11
+ import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
12
12
  import { createReasoningBubble } from "./components/reasoning-bubble";
13
13
  import { createToolBubble } from "./components/tool-bubble";
14
14
  import { createSuggestions } from "./components/suggestions";
15
15
  import { enhanceWithForms } from "./components/forms";
16
16
  import { pluginRegistry } from "./plugins/registry";
17
+ import { mergeWithDefaults } from "./defaults";
17
18
 
18
19
  type Controller = {
19
- update: (config: ChatWidgetConfig) => void;
20
+ update: (config: AgentWidgetConfig) => void;
20
21
  destroy: () => void;
21
22
  open: () => void;
22
23
  close: () => void;
23
24
  toggle: () => void;
25
+ clearChat: () => void;
24
26
  };
25
27
 
26
- const buildPostprocessor = (cfg?: ChatWidgetConfig): MessageTransform => {
28
+ const buildPostprocessor = (cfg?: AgentWidgetConfig): MessageTransform => {
27
29
  if (cfg?.postprocessMessage) {
28
30
  return (context) =>
29
31
  cfg.postprocessMessage!({
@@ -35,16 +37,16 @@ const buildPostprocessor = (cfg?: ChatWidgetConfig): MessageTransform => {
35
37
  return ({ text }) => escapeHtml(text);
36
38
  };
37
39
 
38
- export const createChatExperience = (
40
+ export const createAgentExperience = (
39
41
  mount: HTMLElement,
40
- initialConfig?: ChatWidgetConfig
42
+ initialConfig?: AgentWidgetConfig
41
43
  ): Controller => {
42
44
  // Tailwind config uses important: "#vanilla-agent-root", so ensure mount has this ID
43
45
  if (!mount.id || mount.id !== "vanilla-agent-root") {
44
46
  mount.id = "vanilla-agent-root";
45
47
  }
46
48
 
47
- let config = { ...initialConfig };
49
+ let config = mergeWithDefaults(initialConfig) as AgentWidgetConfig;
48
50
  applyThemeVariables(mount, config);
49
51
 
50
52
  // Get plugins for this instance
@@ -61,7 +63,7 @@ export const createChatExperience = (
61
63
 
62
64
  // Get status indicator config
63
65
  const statusConfig = config.statusIndicator ?? {};
64
- const getStatusText = (status: ChatWidgetSessionStatus): string => {
66
+ const getStatusText = (status: AgentWidgetSessionStatus): string => {
65
67
  if (status === "idle") return statusConfig.idleText ?? statusCopy.idle;
66
68
  if (status === "connecting") return statusConfig.connectingText ?? statusCopy.connecting;
67
69
  if (status === "connected") return statusConfig.connectedText ?? statusCopy.connected;
@@ -97,7 +99,7 @@ export const createChatExperience = (
97
99
  const destroyCallbacks: Array<() => void> = [];
98
100
  const suggestionsManager = createSuggestions(suggestions);
99
101
  let closeHandler: (() => void) | null = null;
100
- let session: ChatWidgetSession;
102
+ let session: AgentWidgetSession;
101
103
  let isStreaming = false;
102
104
  let shouldAutoScroll = true;
103
105
  let lastScrollTop = 0;
@@ -146,39 +148,11 @@ export const createChatExperience = (
146
148
  });
147
149
  };
148
150
 
149
- // Create typing indicator element
150
- const createTypingIndicator = (): HTMLElement => {
151
- const container = document.createElement("div");
152
- container.className = "tvw-flex tvw-items-center tvw-space-x-1 tvw-h-5";
153
-
154
- const dot1 = document.createElement("div");
155
- dot1.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
156
- dot1.style.animationDelay = "0ms";
157
-
158
- const dot2 = document.createElement("div");
159
- dot2.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
160
- dot2.style.animationDelay = "250ms";
161
-
162
- const dot3 = document.createElement("div");
163
- dot3.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
164
- dot3.style.animationDelay = "500ms";
165
-
166
- const srOnly = document.createElement("span");
167
- srOnly.className = "tvw-sr-only";
168
- srOnly.textContent = "Loading";
169
-
170
- container.appendChild(dot1);
171
- container.appendChild(dot2);
172
- container.appendChild(dot3);
173
- container.appendChild(srOnly);
174
-
175
- return container;
176
- };
177
151
 
178
152
  // Message rendering with plugin support
179
153
  const renderMessagesWithPlugins = (
180
154
  container: HTMLElement,
181
- messages: ChatWidgetMessage[],
155
+ messages: AgentWidgetMessage[],
182
156
  transform: MessageTransform
183
157
  ) => {
184
158
  container.innerHTML = "";
@@ -256,12 +230,36 @@ export const createChatExperience = (
256
230
  fragment.appendChild(wrapper);
257
231
  });
258
232
 
259
- // Add typing indicator if streaming and there's at least one user message
260
- if (isStreaming && messages.some((msg) => msg.role === "user")) {
233
+ // Add standalone typing indicator only if streaming but no assistant message is streaming yet
234
+ // (This shows while waiting for the stream to start)
235
+ const hasStreamingAssistantMessage = messages.some(
236
+ (msg) => msg.role === "assistant" && msg.streaming && msg.content && msg.content.trim()
237
+ );
238
+
239
+ if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage) {
261
240
  const typingIndicator = createTypingIndicator();
241
+
242
+ // Create a bubble wrapper for the typing indicator (similar to assistant messages)
243
+ const typingBubble = document.createElement("div");
244
+ typingBubble.className = [
245
+ "tvw-max-w-[85%]",
246
+ "tvw-rounded-2xl",
247
+ "tvw-text-sm",
248
+ "tvw-leading-relaxed",
249
+ "tvw-shadow-sm",
250
+ "tvw-bg-cw-surface",
251
+ "tvw-border",
252
+ "tvw-border-cw-message-border",
253
+ "tvw-text-cw-primary",
254
+ "tvw-px-5",
255
+ "tvw-py-3"
256
+ ].join(" ");
257
+
258
+ typingBubble.appendChild(typingIndicator);
259
+
262
260
  const typingWrapper = document.createElement("div");
263
- typingWrapper.className = "tvw-flex tvw-justify-end";
264
- typingWrapper.appendChild(typingIndicator);
261
+ typingWrapper.className = "tvw-flex";
262
+ typingWrapper.appendChild(typingBubble);
265
263
  fragment.appendChild(typingWrapper);
266
264
  }
267
265
 
@@ -317,9 +315,14 @@ export const createChatExperience = (
317
315
  introSubtitle.textContent =
318
316
  config.copy?.welcomeSubtitle ??
319
317
  "Ask anything about your account or products.";
320
- textarea.placeholder = config.copy?.inputPlaceholder ?? "Type your message…";
321
- sendButton.textContent = config.copy?.sendButtonLabel ?? "Send";
322
-
318
+ textarea.placeholder = config.copy?.inputPlaceholder ?? "How can I help...";
319
+
320
+ // Only update send button text if NOT using icon mode
321
+ const useIcon = config.sendButton?.useIcon ?? false;
322
+ if (!useIcon) {
323
+ sendButton.textContent = config.copy?.sendButtonLabel ?? "Send";
324
+ }
325
+
323
326
  // Update textarea font family and weight
324
327
  const fontFamily = config.theme?.inputFontFamily ?? "sans-serif";
325
328
  const fontWeight = config.theme?.inputFontWeight ?? "400";
@@ -340,7 +343,7 @@ export const createChatExperience = (
340
343
  textarea.style.fontWeight = fontWeight;
341
344
  };
342
345
 
343
- session = new ChatWidgetSession(config, {
346
+ session = new AgentWidgetSession(config, {
344
347
  onMessagesChanged(messages) {
345
348
  renderMessagesWithPlugins(messagesWrapper, messages, postprocess);
346
349
  // Re-render suggestions to hide them after first user message
@@ -359,7 +362,7 @@ export const createChatExperience = (
359
362
  },
360
363
  onStatusChanged(status) {
361
364
  const currentStatusConfig = config.statusIndicator ?? {};
362
- const getCurrentStatusText = (status: ChatWidgetSessionStatus): string => {
365
+ const getCurrentStatusText = (status: AgentWidgetSessionStatus): string => {
363
366
  if (status === "idle") return currentStatusConfig.idleText ?? statusCopy.idle;
364
367
  if (status === "connecting") return currentStatusConfig.connectingText ?? statusCopy.connecting;
365
368
  if (status === "connected") return currentStatusConfig.connectedText ?? statusCopy.connected;
@@ -568,7 +571,7 @@ export const createChatExperience = (
568
571
  };
569
572
 
570
573
  // Function to create mic button dynamically
571
- const createMicButton = (voiceConfig: ChatWidgetConfig['voiceRecognition'], sendButtonConfig: ChatWidgetConfig['sendButton']): { micButton: HTMLButtonElement; micButtonWrapper: HTMLElement } | null => {
574
+ const createMicButton = (voiceConfig: AgentWidgetConfig['voiceRecognition'], sendButtonConfig: AgentWidgetConfig['sendButton']): { micButton: HTMLButtonElement; micButtonWrapper: HTMLElement } | null => {
572
575
  const hasSpeechRecognition =
573
576
  typeof window !== 'undefined' &&
574
577
  (typeof (window as any).webkitSpeechRecognition !== 'undefined' ||
@@ -711,7 +714,7 @@ export const createChatExperience = (
711
714
  return;
712
715
  }
713
716
  const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
714
- const width = launcherWidth ?? "min(360px, calc(100vw - 24px))";
717
+ const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
715
718
  panel.style.width = width;
716
719
  panel.style.maxWidth = width;
717
720
  const viewportHeight = window.innerHeight;
@@ -777,6 +780,25 @@ export const createChatExperience = (
777
780
 
778
781
  refreshCloseButton();
779
782
 
783
+ // Setup clear chat button click handler
784
+ const setupClearChatButton = () => {
785
+ const { clearChatButton } = panelElements;
786
+ if (!clearChatButton) return;
787
+
788
+ clearChatButton.addEventListener("click", () => {
789
+ // Clear messages in session (this will trigger onMessagesChanged which re-renders)
790
+ session.clearMessages();
791
+
792
+ // Dispatch custom event for external handlers (e.g., localStorage clearing in examples)
793
+ const clearEvent = new CustomEvent("vanilla-agent:clear-chat", {
794
+ detail: { timestamp: new Date().toISOString() }
795
+ });
796
+ window.dispatchEvent(clearEvent);
797
+ });
798
+ };
799
+
800
+ setupClearChatButton();
801
+
780
802
  composerForm.addEventListener("submit", handleSubmit);
781
803
  textarea.addEventListener("keydown", handleInputEnter);
782
804
 
@@ -796,7 +818,7 @@ export const createChatExperience = (
796
818
  }
797
819
 
798
820
  return {
799
- update(nextConfig: ChatWidgetConfig) {
821
+ update(nextConfig: AgentWidgetConfig) {
800
822
  config = { ...config, ...nextConfig };
801
823
  applyThemeVariables(mount, config);
802
824
 
@@ -995,6 +1017,259 @@ export const createChatExperience = (
995
1017
  closeButton.style.borderRadius = "";
996
1018
  closeButton.classList.add("tvw-rounded-full");
997
1019
  }
1020
+
1021
+ // Update padding
1022
+ if (launcher.closeButtonPaddingX) {
1023
+ closeButton.style.paddingLeft = launcher.closeButtonPaddingX;
1024
+ closeButton.style.paddingRight = launcher.closeButtonPaddingX;
1025
+ } else {
1026
+ closeButton.style.paddingLeft = "";
1027
+ closeButton.style.paddingRight = "";
1028
+ }
1029
+ if (launcher.closeButtonPaddingY) {
1030
+ closeButton.style.paddingTop = launcher.closeButtonPaddingY;
1031
+ closeButton.style.paddingBottom = launcher.closeButtonPaddingY;
1032
+ } else {
1033
+ closeButton.style.paddingTop = "";
1034
+ closeButton.style.paddingBottom = "";
1035
+ }
1036
+
1037
+ // Update icon
1038
+ const closeButtonIconName = launcher.closeButtonIconName ?? "x";
1039
+ const closeButtonIconText = launcher.closeButtonIconText ?? "×";
1040
+
1041
+ // Clear existing content and render new icon
1042
+ closeButton.innerHTML = "";
1043
+ const iconSvg = renderLucideIcon(closeButtonIconName, "20px", launcher.closeButtonColor || "", 2);
1044
+ if (iconSvg) {
1045
+ closeButton.appendChild(iconSvg);
1046
+ } else {
1047
+ closeButton.textContent = closeButtonIconText;
1048
+ }
1049
+
1050
+ // Update tooltip
1051
+ const { closeButtonWrapper } = panelElements;
1052
+ const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
1053
+ const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
1054
+
1055
+ closeButton.setAttribute("aria-label", closeButtonTooltipText);
1056
+
1057
+ if (closeButtonWrapper) {
1058
+ // Clean up old tooltip event listeners if they exist
1059
+ if ((closeButtonWrapper as any)._cleanupTooltip) {
1060
+ (closeButtonWrapper as any)._cleanupTooltip();
1061
+ delete (closeButtonWrapper as any)._cleanupTooltip;
1062
+ }
1063
+
1064
+ // Set up new portaled tooltip with event listeners
1065
+ if (closeButtonShowTooltip && closeButtonTooltipText) {
1066
+ let portaledTooltip: HTMLElement | null = null;
1067
+
1068
+ const showTooltip = () => {
1069
+ if (portaledTooltip || !closeButton) return; // Already showing or button doesn't exist
1070
+
1071
+ // Create tooltip element
1072
+ portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
1073
+ portaledTooltip.textContent = closeButtonTooltipText;
1074
+
1075
+ // Add arrow
1076
+ const arrow = createElement("div");
1077
+ arrow.className = "tvw-clear-chat-tooltip-arrow";
1078
+ portaledTooltip.appendChild(arrow);
1079
+
1080
+ // Get button position
1081
+ const buttonRect = closeButton.getBoundingClientRect();
1082
+
1083
+ // Position tooltip above button
1084
+ portaledTooltip.style.position = "fixed";
1085
+ portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
1086
+ portaledTooltip.style.top = `${buttonRect.top - 8}px`;
1087
+ portaledTooltip.style.transform = "translate(-50%, -100%)";
1088
+
1089
+ // Append to body
1090
+ document.body.appendChild(portaledTooltip);
1091
+ };
1092
+
1093
+ const hideTooltip = () => {
1094
+ if (portaledTooltip && portaledTooltip.parentNode) {
1095
+ portaledTooltip.parentNode.removeChild(portaledTooltip);
1096
+ portaledTooltip = null;
1097
+ }
1098
+ };
1099
+
1100
+ // Add event listeners
1101
+ closeButtonWrapper.addEventListener("mouseenter", showTooltip);
1102
+ closeButtonWrapper.addEventListener("mouseleave", hideTooltip);
1103
+ closeButton.addEventListener("focus", showTooltip);
1104
+ closeButton.addEventListener("blur", hideTooltip);
1105
+
1106
+ // Store cleanup function on the wrapper for later use
1107
+ (closeButtonWrapper as any)._cleanupTooltip = () => {
1108
+ hideTooltip();
1109
+ if (closeButtonWrapper) {
1110
+ closeButtonWrapper.removeEventListener("mouseenter", showTooltip);
1111
+ closeButtonWrapper.removeEventListener("mouseleave", hideTooltip);
1112
+ }
1113
+ if (closeButton) {
1114
+ closeButton.removeEventListener("focus", showTooltip);
1115
+ closeButton.removeEventListener("blur", hideTooltip);
1116
+ }
1117
+ };
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ // Update clear chat button styling from config
1123
+ const { clearChatButton, clearChatButtonWrapper } = panelElements;
1124
+ if (clearChatButton) {
1125
+ const clearChatConfig = launcher.clearChat ?? {};
1126
+ const clearChatEnabled = clearChatConfig.enabled ?? true;
1127
+
1128
+ // Show/hide button based on enabled state
1129
+ if (clearChatButtonWrapper) {
1130
+ clearChatButtonWrapper.style.display = clearChatEnabled ? "" : "none";
1131
+ }
1132
+
1133
+ if (clearChatEnabled) {
1134
+ // Update size
1135
+ const clearChatSize = clearChatConfig.size ?? "32px";
1136
+ clearChatButton.style.height = clearChatSize;
1137
+ clearChatButton.style.width = clearChatSize;
1138
+
1139
+ // Update icon
1140
+ const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
1141
+ const clearChatIconColor = clearChatConfig.iconColor ?? "";
1142
+
1143
+ // Clear existing icon and render new one
1144
+ clearChatButton.innerHTML = "";
1145
+ const iconSvg = renderLucideIcon(clearChatIconName, "20px", clearChatIconColor || "", 2);
1146
+ if (iconSvg) {
1147
+ clearChatButton.appendChild(iconSvg);
1148
+ }
1149
+
1150
+ // Update icon color
1151
+ if (clearChatIconColor) {
1152
+ clearChatButton.style.color = clearChatIconColor;
1153
+ clearChatButton.classList.remove("tvw-text-cw-muted");
1154
+ } else {
1155
+ clearChatButton.style.color = "";
1156
+ clearChatButton.classList.add("tvw-text-cw-muted");
1157
+ }
1158
+
1159
+ // Update background color
1160
+ if (clearChatConfig.backgroundColor) {
1161
+ clearChatButton.style.backgroundColor = clearChatConfig.backgroundColor;
1162
+ clearChatButton.classList.remove("hover:tvw-bg-gray-100");
1163
+ } else {
1164
+ clearChatButton.style.backgroundColor = "";
1165
+ clearChatButton.classList.add("hover:tvw-bg-gray-100");
1166
+ }
1167
+
1168
+ // Update border
1169
+ if (clearChatConfig.borderWidth || clearChatConfig.borderColor) {
1170
+ const borderWidth = clearChatConfig.borderWidth || "0px";
1171
+ const borderColor = clearChatConfig.borderColor || "transparent";
1172
+ clearChatButton.style.border = `${borderWidth} solid ${borderColor}`;
1173
+ clearChatButton.classList.remove("tvw-border-none");
1174
+ } else {
1175
+ clearChatButton.style.border = "";
1176
+ clearChatButton.classList.add("tvw-border-none");
1177
+ }
1178
+
1179
+ // Update border radius
1180
+ if (clearChatConfig.borderRadius) {
1181
+ clearChatButton.style.borderRadius = clearChatConfig.borderRadius;
1182
+ clearChatButton.classList.remove("tvw-rounded-full");
1183
+ } else {
1184
+ clearChatButton.style.borderRadius = "";
1185
+ clearChatButton.classList.add("tvw-rounded-full");
1186
+ }
1187
+
1188
+ // Update padding
1189
+ if (clearChatConfig.paddingX) {
1190
+ clearChatButton.style.paddingLeft = clearChatConfig.paddingX;
1191
+ clearChatButton.style.paddingRight = clearChatConfig.paddingX;
1192
+ } else {
1193
+ clearChatButton.style.paddingLeft = "";
1194
+ clearChatButton.style.paddingRight = "";
1195
+ }
1196
+ if (clearChatConfig.paddingY) {
1197
+ clearChatButton.style.paddingTop = clearChatConfig.paddingY;
1198
+ clearChatButton.style.paddingBottom = clearChatConfig.paddingY;
1199
+ } else {
1200
+ clearChatButton.style.paddingTop = "";
1201
+ clearChatButton.style.paddingBottom = "";
1202
+ }
1203
+
1204
+ const clearChatTooltipText = clearChatConfig.tooltipText ?? "Clear chat";
1205
+ const clearChatShowTooltip = clearChatConfig.showTooltip ?? true;
1206
+
1207
+ clearChatButton.setAttribute("aria-label", clearChatTooltipText);
1208
+
1209
+ if (clearChatButtonWrapper) {
1210
+ // Clean up old tooltip event listeners if they exist
1211
+ if ((clearChatButtonWrapper as any)._cleanupTooltip) {
1212
+ (clearChatButtonWrapper as any)._cleanupTooltip();
1213
+ delete (clearChatButtonWrapper as any)._cleanupTooltip;
1214
+ }
1215
+
1216
+ // Set up new portaled tooltip with event listeners
1217
+ if (clearChatShowTooltip && clearChatTooltipText) {
1218
+ let portaledTooltip: HTMLElement | null = null;
1219
+
1220
+ const showTooltip = () => {
1221
+ if (portaledTooltip || !clearChatButton) return; // Already showing or button doesn't exist
1222
+
1223
+ // Create tooltip element
1224
+ portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
1225
+ portaledTooltip.textContent = clearChatTooltipText;
1226
+
1227
+ // Add arrow
1228
+ const arrow = createElement("div");
1229
+ arrow.className = "tvw-clear-chat-tooltip-arrow";
1230
+ portaledTooltip.appendChild(arrow);
1231
+
1232
+ // Get button position
1233
+ const buttonRect = clearChatButton.getBoundingClientRect();
1234
+
1235
+ // Position tooltip above button
1236
+ portaledTooltip.style.position = "fixed";
1237
+ portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
1238
+ portaledTooltip.style.top = `${buttonRect.top - 8}px`;
1239
+ portaledTooltip.style.transform = "translate(-50%, -100%)";
1240
+
1241
+ // Append to body
1242
+ document.body.appendChild(portaledTooltip);
1243
+ };
1244
+
1245
+ const hideTooltip = () => {
1246
+ if (portaledTooltip && portaledTooltip.parentNode) {
1247
+ portaledTooltip.parentNode.removeChild(portaledTooltip);
1248
+ portaledTooltip = null;
1249
+ }
1250
+ };
1251
+
1252
+ // Add event listeners
1253
+ clearChatButtonWrapper.addEventListener("mouseenter", showTooltip);
1254
+ clearChatButtonWrapper.addEventListener("mouseleave", hideTooltip);
1255
+ clearChatButton.addEventListener("focus", showTooltip);
1256
+ clearChatButton.addEventListener("blur", hideTooltip);
1257
+
1258
+ // Store cleanup function on the button for later use
1259
+ (clearChatButtonWrapper as any)._cleanupTooltip = () => {
1260
+ hideTooltip();
1261
+ if (clearChatButtonWrapper) {
1262
+ clearChatButtonWrapper.removeEventListener("mouseenter", showTooltip);
1263
+ clearChatButtonWrapper.removeEventListener("mouseleave", hideTooltip);
1264
+ }
1265
+ if (clearChatButton) {
1266
+ clearChatButton.removeEventListener("focus", showTooltip);
1267
+ clearChatButton.removeEventListener("blur", hideTooltip);
1268
+ }
1269
+ };
1270
+ }
1271
+ }
1272
+ }
998
1273
  }
999
1274
 
1000
1275
  postprocess = buildPostprocessor(config);
@@ -1289,7 +1564,7 @@ export const createChatExperience = (
1289
1564
  // Update status text if status is currently set
1290
1565
  if (session) {
1291
1566
  const currentStatus = session.getStatus();
1292
- const getCurrentStatusText = (status: ChatWidgetSessionStatus): string => {
1567
+ const getCurrentStatusText = (status: AgentWidgetSessionStatus): string => {
1293
1568
  if (status === "idle") return statusIndicatorConfig.idleText ?? statusCopy.idle;
1294
1569
  if (status === "connecting") return statusIndicatorConfig.connectingText ?? statusCopy.connecting;
1295
1570
  if (status === "connected") return statusIndicatorConfig.connectedText ?? statusCopy.connected;
@@ -1311,6 +1586,16 @@ export const createChatExperience = (
1311
1586
  if (!launcherEnabled) return;
1312
1587
  setOpenState(!open);
1313
1588
  },
1589
+ clearChat() {
1590
+ // Clear messages in session (this will trigger onMessagesChanged which re-renders)
1591
+ session.clearMessages();
1592
+
1593
+ // Dispatch custom event for external handlers (e.g., localStorage clearing in examples)
1594
+ const clearEvent = new CustomEvent("vanilla-agent:clear-chat", {
1595
+ detail: { timestamp: new Date().toISOString() }
1596
+ });
1597
+ window.dispatchEvent(clearEvent);
1598
+ },
1314
1599
  destroy() {
1315
1600
  destroyCallbacks.forEach((cb) => cb());
1316
1601
  wrapper.remove();
@@ -1322,4 +1607,4 @@ export const createChatExperience = (
1322
1607
  };
1323
1608
  };
1324
1609
 
1325
- export type ChatWidgetController = Controller;
1610
+ export type AgentWidgetController = Controller;
@@ -1,6 +1,6 @@
1
- import { ChatWidgetSessionStatus } from "../session";
1
+ import { AgentWidgetSessionStatus } from "../session";
2
2
 
3
- export const statusCopy: Record<ChatWidgetSessionStatus, string> = {
3
+ export const statusCopy: Record<AgentWidgetSessionStatus, string> = {
4
4
  idle: "Online",
5
5
  connecting: "Connecting…",
6
6
  connected: "Streaming…",
@@ -9,3 +9,5 @@ export const statusCopy: Record<ChatWidgetSessionStatus, string> = {
9
9
 
10
10
 
11
11
 
12
+
13
+
package/src/utils/dom.ts CHANGED
@@ -18,3 +18,5 @@ export const createFragment = (): DocumentFragment => {
18
18
 
19
19
 
20
20
 
21
+
22
+
@@ -1,4 +1,4 @@
1
- import { ChatWidgetReasoning, ChatWidgetToolCall } from "../types";
1
+ import { AgentWidgetReasoning, AgentWidgetToolCall } from "../types";
2
2
 
3
3
  export const formatUnknownValue = (value: unknown): string => {
4
4
  if (value === null) return "null";
@@ -14,7 +14,7 @@ export const formatUnknownValue = (value: unknown): string => {
14
14
  }
15
15
  };
16
16
 
17
- export const formatReasoningDuration = (reasoning: ChatWidgetReasoning) => {
17
+ export const formatReasoningDuration = (reasoning: AgentWidgetReasoning) => {
18
18
  const end = reasoning.completedAt ?? Date.now();
19
19
  const start = reasoning.startedAt ?? end;
20
20
  const durationMs =
@@ -32,13 +32,13 @@ export const formatReasoningDuration = (reasoning: ChatWidgetReasoning) => {
32
32
  return `Thought for ${formatted} seconds`;
33
33
  };
34
34
 
35
- export const describeReasonStatus = (reasoning: ChatWidgetReasoning) => {
35
+ export const describeReasonStatus = (reasoning: AgentWidgetReasoning) => {
36
36
  if (reasoning.status === "complete") return formatReasoningDuration(reasoning);
37
37
  if (reasoning.status === "pending") return "Waiting";
38
38
  return "";
39
39
  };
40
40
 
41
- export const formatToolDuration = (tool: ChatWidgetToolCall) => {
41
+ export const formatToolDuration = (tool: AgentWidgetToolCall) => {
42
42
  const durationMs =
43
43
  typeof tool.duration === "number"
44
44
  ? tool.duration
@@ -60,13 +60,13 @@ export const formatToolDuration = (tool: ChatWidgetToolCall) => {
60
60
  return `Used tool for ${formatted} seconds`;
61
61
  };
62
62
 
63
- export const describeToolStatus = (status: ChatWidgetToolCall["status"]) => {
63
+ export const describeToolStatus = (status: AgentWidgetToolCall["status"]) => {
64
64
  if (status === "complete") return "";
65
65
  if (status === "pending") return "Starting";
66
66
  return "Running";
67
67
  };
68
68
 
69
- export const describeToolTitle = (tool: ChatWidgetToolCall) => {
69
+ export const describeToolTitle = (tool: AgentWidgetToolCall) => {
70
70
  if (tool.status === "complete") {
71
71
  return formatToolDuration(tool);
72
72
  }
@@ -75,3 +75,5 @@ export const describeToolTitle = (tool: ChatWidgetToolCall) => {
75
75
 
76
76
 
77
77
 
78
+
79
+
@@ -1,4 +1,4 @@
1
- import { icons } from "lucide";
1
+ import * as icons from "lucide";
2
2
  import type { IconNode } from "lucide";
3
3
 
4
4
  /**
@@ -10,3 +10,5 @@ export const positionMap: Record<
10
10
 
11
11
 
12
12
 
13
+
14
+
@@ -1,8 +1,8 @@
1
- import { ChatWidgetConfig } from "../types";
1
+ import { AgentWidgetConfig } from "../types";
2
2
 
3
3
  export const applyThemeVariables = (
4
4
  element: HTMLElement,
5
- config?: ChatWidgetConfig
5
+ config?: AgentWidgetConfig
6
6
  ) => {
7
7
  const theme = config?.theme ?? {};
8
8
  Object.entries(theme).forEach(([key, value]) => {
@@ -18,3 +18,5 @@ export const applyThemeVariables = (
18
18
 
19
19
 
20
20
 
21
+
22
+