vanilla-agent 1.7.2 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-agent",
3
- "version": "1.7.2",
3
+ "version": "1.9.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",
@@ -67,6 +67,8 @@ export interface PanelElements {
67
67
  clearChatButton: HTMLButtonElement | null;
68
68
  clearChatButtonWrapper: HTMLElement | null;
69
69
  iconHolder: HTMLElement;
70
+ headerTitle: HTMLElement;
71
+ headerSubtitle: HTMLElement;
70
72
  }
71
73
 
72
74
  export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelElements => {
@@ -789,7 +791,9 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
789
791
  closeButtonWrapper,
790
792
  clearChatButton,
791
793
  clearChatButtonWrapper,
792
- iconHolder
794
+ iconHolder,
795
+ headerTitle: title,
796
+ headerSubtitle: subtitle
793
797
  };
794
798
  };
795
799
 
@@ -1,16 +1,28 @@
1
1
  import { createElement } from "../utils/dom";
2
2
  import { AgentWidgetSession } from "../session";
3
- import { AgentWidgetMessage } from "../types";
3
+ import { AgentWidgetMessage, AgentWidgetSuggestionChipsConfig } from "../types";
4
4
 
5
5
  export interface SuggestionButtons {
6
6
  buttons: HTMLButtonElement[];
7
- render: (chips: string[] | undefined, session: AgentWidgetSession, textarea: HTMLTextAreaElement, messages?: AgentWidgetMessage[]) => void;
7
+ render: (
8
+ chips: string[] | undefined,
9
+ session: AgentWidgetSession,
10
+ textarea: HTMLTextAreaElement,
11
+ messages?: AgentWidgetMessage[],
12
+ config?: AgentWidgetSuggestionChipsConfig
13
+ ) => void;
8
14
  }
9
15
 
10
16
  export const createSuggestions = (container: HTMLElement): SuggestionButtons => {
11
17
  const suggestionButtons: HTMLButtonElement[] = [];
12
18
 
13
- const render = (chips: string[] | undefined, session: AgentWidgetSession, textarea: HTMLTextAreaElement, messages?: AgentWidgetMessage[]) => {
19
+ const render = (
20
+ chips: string[] | undefined,
21
+ session: AgentWidgetSession,
22
+ textarea: HTMLTextAreaElement,
23
+ messages?: AgentWidgetMessage[],
24
+ chipsConfig?: AgentWidgetSuggestionChipsConfig
25
+ ) => {
14
26
  container.innerHTML = "";
15
27
  suggestionButtons.length = 0;
16
28
  if (!chips || !chips.length) return;
@@ -23,6 +35,20 @@ export const createSuggestions = (container: HTMLElement): SuggestionButtons =>
23
35
 
24
36
  const fragment = document.createDocumentFragment();
25
37
  const streaming = session ? session.isStreaming() : false;
38
+
39
+ // Get font family mapping function
40
+ const getFontFamilyValue = (family: "sans-serif" | "serif" | "mono"): string => {
41
+ switch (family) {
42
+ case "serif":
43
+ return 'Georgia, "Times New Roman", Times, serif';
44
+ case "mono":
45
+ return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
46
+ case "sans-serif":
47
+ default:
48
+ return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
49
+ }
50
+ };
51
+
26
52
  chips.forEach((chip) => {
27
53
  const btn = createElement(
28
54
  "button",
@@ -31,6 +57,25 @@ export const createSuggestions = (container: HTMLElement): SuggestionButtons =>
31
57
  btn.type = "button";
32
58
  btn.textContent = chip;
33
59
  btn.disabled = streaming;
60
+
61
+ // Apply typography settings
62
+ if (chipsConfig?.fontFamily) {
63
+ btn.style.fontFamily = getFontFamilyValue(chipsConfig.fontFamily);
64
+ }
65
+ if (chipsConfig?.fontWeight) {
66
+ btn.style.fontWeight = chipsConfig.fontWeight;
67
+ }
68
+
69
+ // Apply padding settings
70
+ if (chipsConfig?.paddingX) {
71
+ btn.style.paddingLeft = chipsConfig.paddingX;
72
+ btn.style.paddingRight = chipsConfig.paddingX;
73
+ }
74
+ if (chipsConfig?.paddingY) {
75
+ btn.style.paddingTop = chipsConfig.paddingY;
76
+ btn.style.paddingBottom = chipsConfig.paddingY;
77
+ }
78
+
34
79
  btn.addEventListener("click", () => {
35
80
  if (!session || session.isStreaming()) return;
36
81
  textarea.value = "";
package/src/defaults.ts CHANGED
@@ -128,6 +128,12 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
128
128
  "Tell me about your features",
129
129
  "How does this work?",
130
130
  ],
131
+ suggestionChipsConfig: {
132
+ fontFamily: "sans-serif",
133
+ fontWeight: "500",
134
+ paddingX: "12px",
135
+ paddingY: "6px",
136
+ },
131
137
  debug: false,
132
138
  };
133
139
 
@@ -176,5 +182,9 @@ export function mergeWithDefaults(
176
182
  ...config.features,
177
183
  },
178
184
  suggestionChips: config.suggestionChips ?? DEFAULT_WIDGET_CONFIG.suggestionChips,
185
+ suggestionChipsConfig: {
186
+ ...DEFAULT_WIDGET_CONFIG.suggestionChipsConfig,
187
+ ...config.suggestionChipsConfig,
188
+ },
179
189
  };
180
190
  }
package/src/types.ts CHANGED
@@ -51,6 +51,7 @@ export type AgentWidgetActionParser = (
51
51
  export type AgentWidgetActionHandlerResult = {
52
52
  handled?: boolean;
53
53
  displayText?: string;
54
+ persistMessage?: boolean; // If false, prevents message from being saved to history
54
55
  };
55
56
 
56
57
  export type AgentWidgetActionContext = {
@@ -92,11 +93,27 @@ export type AgentWidgetActionEventPayload = {
92
93
  message: AgentWidgetMessage;
93
94
  };
94
95
 
96
+ export type AgentWidgetStateEvent = {
97
+ open: boolean;
98
+ source: "user" | "auto" | "api" | "system";
99
+ timestamp: number;
100
+ };
101
+
102
+ export type AgentWidgetStateSnapshot = {
103
+ open: boolean;
104
+ launcherEnabled: boolean;
105
+ voiceActive: boolean;
106
+ streaming: boolean;
107
+ };
108
+
95
109
  export type AgentWidgetControllerEventMap = {
96
110
  "assistant:message": AgentWidgetMessage;
97
111
  "assistant:complete": AgentWidgetMessage;
98
112
  "voice:state": AgentWidgetVoiceStateEvent;
99
113
  "action:detected": AgentWidgetActionEventPayload;
114
+ "widget:opened": AgentWidgetStateEvent;
115
+ "widget:closed": AgentWidgetStateEvent;
116
+ "widget:state": AgentWidgetStateSnapshot;
100
117
  };
101
118
 
102
119
  export type AgentWidgetFeatureFlags = {
@@ -258,6 +275,13 @@ export type AgentWidgetToolCallConfig = {
258
275
  labelTextColor?: string;
259
276
  };
260
277
 
278
+ export type AgentWidgetSuggestionChipsConfig = {
279
+ fontFamily?: "sans-serif" | "serif" | "mono";
280
+ fontWeight?: string;
281
+ paddingX?: string;
282
+ paddingY?: string;
283
+ };
284
+
261
285
  /**
262
286
  * Interface for pluggable stream parsers that extract text from streaming responses.
263
287
  * Parsers handle incremental parsing to extract text values from structured formats (JSON, XML, etc.).
@@ -330,6 +354,7 @@ export type AgentWidgetConfig = {
330
354
  launcher?: AgentWidgetLauncherConfig;
331
355
  initialMessages?: AgentWidgetMessage[];
332
356
  suggestionChips?: string[];
357
+ suggestionChipsConfig?: AgentWidgetSuggestionChipsConfig;
333
358
  debug?: boolean;
334
359
  formEndpoint?: string;
335
360
  launcherWidth?: string;
package/src/ui.ts CHANGED
@@ -7,7 +7,9 @@ import {
7
7
  AgentWidgetStorageAdapter,
8
8
  AgentWidgetStoredState,
9
9
  AgentWidgetControllerEventMap,
10
- AgentWidgetVoiceStateEvent
10
+ AgentWidgetVoiceStateEvent,
11
+ AgentWidgetStateEvent,
12
+ AgentWidgetStateSnapshot
11
13
  } from "./types";
12
14
  import { applyThemeVariables } from "./utils/theme";
13
15
  import { renderLucideIcon } from "./utils/icons";
@@ -73,6 +75,10 @@ type Controller = {
73
75
  event: K,
74
76
  handler: (payload: AgentWidgetControllerEventMap[K]) => void
75
77
  ) => void;
78
+ // State query methods
79
+ isOpen: () => boolean;
80
+ isVoiceActive: () => boolean;
81
+ getState: () => AgentWidgetStateSnapshot;
76
82
  };
77
83
 
78
84
  const buildPostprocessor = (
@@ -84,14 +90,18 @@ const buildPostprocessor = (
84
90
  const rawPayload = context.message.rawContent ?? null;
85
91
 
86
92
  if (actionManager) {
87
- const actionOverride = actionManager.process({
93
+ const actionResult = actionManager.process({
88
94
  text: nextText,
89
95
  raw: rawPayload ?? nextText,
90
96
  message: context.message,
91
97
  streaming: context.streaming
92
98
  });
93
- if (actionOverride !== null) {
94
- nextText = actionOverride;
99
+ if (actionResult !== null) {
100
+ nextText = actionResult.text;
101
+ // Mark message as non-persistable if persist is false
102
+ if (!actionResult.persist) {
103
+ (context.message as any).__skipPersist = true;
104
+ }
95
105
  }
96
106
  }
97
107
 
@@ -214,7 +224,9 @@ export const createAgentExperience = (
214
224
  introTitle,
215
225
  introSubtitle,
216
226
  closeButton,
217
- iconHolder
227
+ iconHolder,
228
+ headerTitle,
229
+ headerSubtitle
218
230
  } = panelElements;
219
231
 
220
232
  // Use mutable references for mic button so we can update them dynamically
@@ -285,7 +297,9 @@ export const createAgentExperience = (
285
297
  };
286
298
 
287
299
  const getMessagesForPersistence = () =>
288
- session ? stripStreamingFromMessages(session.getMessages()) : [];
300
+ session
301
+ ? stripStreamingFromMessages(session.getMessages()).filter(msg => !(msg as any).__skipPersist)
302
+ : [];
289
303
 
290
304
  function persistState(messagesOverride?: AgentWidgetMessage[]) {
291
305
  if (!storageAdapter?.save || !session) return;
@@ -597,15 +611,39 @@ export const createAgentExperience = (
597
611
  }
598
612
  };
599
613
 
600
- const setOpenState = (nextOpen: boolean) => {
614
+ const setOpenState = (nextOpen: boolean, source: "user" | "auto" | "api" | "system" = "user") => {
601
615
  if (!launcherEnabled) return;
602
616
  if (open === nextOpen) return;
617
+
618
+ const prevOpen = open;
603
619
  open = nextOpen;
604
620
  updateOpenState();
621
+
605
622
  if (open) {
606
623
  recalcPanelHeight();
607
624
  scheduleAutoScroll(true);
608
625
  }
626
+
627
+ // Emit widget state events
628
+ const stateEvent: AgentWidgetStateEvent = {
629
+ open,
630
+ source,
631
+ timestamp: Date.now()
632
+ };
633
+
634
+ if (open && !prevOpen) {
635
+ eventBus.emit("widget:opened", stateEvent);
636
+ } else if (!open && prevOpen) {
637
+ eventBus.emit("widget:closed", stateEvent);
638
+ }
639
+
640
+ // Emit general state snapshot
641
+ eventBus.emit("widget:state", {
642
+ open,
643
+ launcherEnabled,
644
+ voiceActive: voiceState.active,
645
+ streaming: session.isStreaming()
646
+ });
609
647
  };
610
648
 
611
649
  const setComposerDisabled = (disabled: boolean) => {
@@ -664,7 +702,7 @@ export const createAgentExperience = (
664
702
  suggestionsManager.render([], session, textarea, messages);
665
703
  } else {
666
704
  // Show suggestions if no user message yet
667
- suggestionsManager.render(config.suggestionChips, session, textarea, messages);
705
+ suggestionsManager.render(config.suggestionChips, session, textarea, messages, config.suggestionChipsConfig);
668
706
  }
669
707
  }
670
708
  scheduleAutoScroll(!isStreaming);
@@ -1059,7 +1097,7 @@ export const createAgentExperience = (
1059
1097
  destroyCallbacks.push(autoResumeUnsub);
1060
1098
 
1061
1099
  const toggleOpen = () => {
1062
- setOpenState(!open);
1100
+ setOpenState(!open, "user");
1063
1101
  };
1064
1102
 
1065
1103
  let launcherButtonInstance = launcherEnabled
@@ -1070,7 +1108,7 @@ export const createAgentExperience = (
1070
1108
  mount.appendChild(launcherButtonInstance.element);
1071
1109
  }
1072
1110
  updateOpenState();
1073
- suggestionsManager.render(config.suggestionChips, session, textarea);
1111
+ suggestionsManager.render(config.suggestionChips, session, textarea, undefined, config.suggestionChipsConfig);
1074
1112
  updateCopy();
1075
1113
  setComposerDisabled(session.isStreaming());
1076
1114
  scheduleAutoScroll(true);
@@ -1259,6 +1297,14 @@ export const createAgentExperience = (
1259
1297
  launcherButtonInstance.update(config);
1260
1298
  }
1261
1299
 
1300
+ // Update panel header title and subtitle
1301
+ if (headerTitle && config.launcher?.title !== undefined) {
1302
+ headerTitle.textContent = config.launcher.title;
1303
+ }
1304
+ if (headerSubtitle && config.launcher?.subtitle !== undefined) {
1305
+ headerSubtitle.textContent = config.launcher.subtitle;
1306
+ }
1307
+
1262
1308
  // Only update open state if launcher enabled state changed or autoExpand value changed
1263
1309
  const launcherEnabledChanged = launcherEnabled !== prevLauncherEnabled;
1264
1310
  const autoExpandChanged = autoExpand !== prevAutoExpand;
@@ -1271,11 +1317,11 @@ export const createAgentExperience = (
1271
1317
  updateOpenState();
1272
1318
  } else {
1273
1319
  // Launcher was just enabled - respect autoExpand setting
1274
- setOpenState(autoExpand);
1320
+ setOpenState(autoExpand, "auto");
1275
1321
  }
1276
1322
  } else if (autoExpandChanged) {
1277
1323
  // autoExpand value changed - update state to match
1278
- setOpenState(autoExpand);
1324
+ setOpenState(autoExpand, "auto");
1279
1325
  }
1280
1326
  // Otherwise, preserve current open state (user may have manually opened/closed)
1281
1327
 
@@ -1716,7 +1762,7 @@ export const createAgentExperience = (
1716
1762
  session.getMessages(),
1717
1763
  postprocess
1718
1764
  );
1719
- suggestionsManager.render(config.suggestionChips, session, textarea);
1765
+ suggestionsManager.render(config.suggestionChips, session, textarea, undefined, config.suggestionChipsConfig);
1720
1766
  updateCopy();
1721
1767
  setComposerDisabled(session.isStreaming());
1722
1768
 
@@ -2013,15 +2059,15 @@ export const createAgentExperience = (
2013
2059
  },
2014
2060
  open() {
2015
2061
  if (!launcherEnabled) return;
2016
- setOpenState(true);
2062
+ setOpenState(true, "api");
2017
2063
  },
2018
2064
  close() {
2019
2065
  if (!launcherEnabled) return;
2020
- setOpenState(false);
2066
+ setOpenState(false, "api");
2021
2067
  },
2022
2068
  toggle() {
2023
2069
  if (!launcherEnabled) return;
2024
- setOpenState(!open);
2070
+ setOpenState(!open, "api");
2025
2071
  },
2026
2072
  clearChat() {
2027
2073
  // Clear messages in session (this will trigger onMessagesChanged which re-renders)
@@ -2082,7 +2128,7 @@ export const createAgentExperience = (
2082
2128
 
2083
2129
  // Auto-open widget if closed and launcher is enabled
2084
2130
  if (!open && launcherEnabled) {
2085
- setOpenState(true);
2131
+ setOpenState(true, "system");
2086
2132
  }
2087
2133
 
2088
2134
  textarea.value = message;
@@ -2098,7 +2144,7 @@ export const createAgentExperience = (
2098
2144
 
2099
2145
  // Auto-open widget if closed and launcher is enabled
2100
2146
  if (!open && launcherEnabled) {
2101
- setOpenState(true);
2147
+ setOpenState(true, "system");
2102
2148
  }
2103
2149
 
2104
2150
  textarea.value = "";
@@ -2113,7 +2159,7 @@ export const createAgentExperience = (
2113
2159
 
2114
2160
  // Auto-open widget if closed and launcher is enabled
2115
2161
  if (!open && launcherEnabled) {
2116
- setOpenState(true);
2162
+ setOpenState(true, "system");
2117
2163
  }
2118
2164
 
2119
2165
  voiceState.manuallyDeactivated = false;
@@ -2132,7 +2178,7 @@ export const createAgentExperience = (
2132
2178
  injectTestMessage(event: AgentWidgetEvent) {
2133
2179
  // Auto-open widget if closed and launcher is enabled
2134
2180
  if (!open && launcherEnabled) {
2135
- setOpenState(true);
2181
+ setOpenState(true, "system");
2136
2182
  }
2137
2183
  session.injectTestEvent(event);
2138
2184
  },
@@ -2156,6 +2202,21 @@ export const createAgentExperience = (
2156
2202
  off(event, handler) {
2157
2203
  eventBus.off(event, handler);
2158
2204
  },
2205
+ // State query methods
2206
+ isOpen(): boolean {
2207
+ return launcherEnabled && open;
2208
+ },
2209
+ isVoiceActive(): boolean {
2210
+ return voiceState.active;
2211
+ },
2212
+ getState(): AgentWidgetStateSnapshot {
2213
+ return {
2214
+ open: launcherEnabled && open,
2215
+ launcherEnabled,
2216
+ voiceActive: voiceState.active,
2217
+ streaming: session.isStreaming()
2218
+ };
2219
+ },
2159
2220
  destroy() {
2160
2221
  destroyCallbacks.forEach((cb) => cb());
2161
2222
  wrapper.remove();
@@ -140,7 +140,7 @@ export const createActionManager = (options: ActionManagerOptions) => {
140
140
  }));
141
141
  };
142
142
 
143
- const process = (context: ActionManagerProcessContext): string | null => {
143
+ const process = (context: ActionManagerProcessContext): { text: string; persist: boolean } | null => {
144
144
  if (
145
145
  context.streaming ||
146
146
  context.message.role !== "assistant" ||
@@ -202,12 +202,11 @@ export const createActionManager = (options: ActionManagerOptions) => {
202
202
 
203
203
  if (!handlerResult) continue;
204
204
 
205
- if (handlerResult.displayText !== undefined && handlerResult.handled) {
206
- return handlerResult.displayText;
207
- }
208
-
209
205
  if (handlerResult.handled) {
210
- return "";
206
+ // persistMessage defaults to true if not specified
207
+ const persist = handlerResult.persistMessage !== false;
208
+ const displayText = handlerResult.displayText !== undefined ? handlerResult.displayText : "";
209
+ return { text: displayText, persist };
211
210
  }
212
211
  } catch (error) {
213
212
  if (typeof console !== "undefined") {
@@ -217,7 +216,7 @@ export const createActionManager = (options: ActionManagerOptions) => {
217
216
  }
218
217
  }
219
218
 
220
- return "";
219
+ return { text: "", persist: true };
221
220
  };
222
221
 
223
222
  return {