vanilla-agent 1.7.2 → 1.8.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.8.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",
@@ -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
 
@@ -285,7 +295,9 @@ export const createAgentExperience = (
285
295
  };
286
296
 
287
297
  const getMessagesForPersistence = () =>
288
- session ? stripStreamingFromMessages(session.getMessages()) : [];
298
+ session
299
+ ? stripStreamingFromMessages(session.getMessages()).filter(msg => !(msg as any).__skipPersist)
300
+ : [];
289
301
 
290
302
  function persistState(messagesOverride?: AgentWidgetMessage[]) {
291
303
  if (!storageAdapter?.save || !session) return;
@@ -597,15 +609,39 @@ export const createAgentExperience = (
597
609
  }
598
610
  };
599
611
 
600
- const setOpenState = (nextOpen: boolean) => {
612
+ const setOpenState = (nextOpen: boolean, source: "user" | "auto" | "api" | "system" = "user") => {
601
613
  if (!launcherEnabled) return;
602
614
  if (open === nextOpen) return;
615
+
616
+ const prevOpen = open;
603
617
  open = nextOpen;
604
618
  updateOpenState();
619
+
605
620
  if (open) {
606
621
  recalcPanelHeight();
607
622
  scheduleAutoScroll(true);
608
623
  }
624
+
625
+ // Emit widget state events
626
+ const stateEvent: AgentWidgetStateEvent = {
627
+ open,
628
+ source,
629
+ timestamp: Date.now()
630
+ };
631
+
632
+ if (open && !prevOpen) {
633
+ eventBus.emit("widget:opened", stateEvent);
634
+ } else if (!open && prevOpen) {
635
+ eventBus.emit("widget:closed", stateEvent);
636
+ }
637
+
638
+ // Emit general state snapshot
639
+ eventBus.emit("widget:state", {
640
+ open,
641
+ launcherEnabled,
642
+ voiceActive: voiceState.active,
643
+ streaming: session.isStreaming()
644
+ });
609
645
  };
610
646
 
611
647
  const setComposerDisabled = (disabled: boolean) => {
@@ -664,7 +700,7 @@ export const createAgentExperience = (
664
700
  suggestionsManager.render([], session, textarea, messages);
665
701
  } else {
666
702
  // Show suggestions if no user message yet
667
- suggestionsManager.render(config.suggestionChips, session, textarea, messages);
703
+ suggestionsManager.render(config.suggestionChips, session, textarea, messages, config.suggestionChipsConfig);
668
704
  }
669
705
  }
670
706
  scheduleAutoScroll(!isStreaming);
@@ -1059,7 +1095,7 @@ export const createAgentExperience = (
1059
1095
  destroyCallbacks.push(autoResumeUnsub);
1060
1096
 
1061
1097
  const toggleOpen = () => {
1062
- setOpenState(!open);
1098
+ setOpenState(!open, "user");
1063
1099
  };
1064
1100
 
1065
1101
  let launcherButtonInstance = launcherEnabled
@@ -1070,7 +1106,7 @@ export const createAgentExperience = (
1070
1106
  mount.appendChild(launcherButtonInstance.element);
1071
1107
  }
1072
1108
  updateOpenState();
1073
- suggestionsManager.render(config.suggestionChips, session, textarea);
1109
+ suggestionsManager.render(config.suggestionChips, session, textarea, undefined, config.suggestionChipsConfig);
1074
1110
  updateCopy();
1075
1111
  setComposerDisabled(session.isStreaming());
1076
1112
  scheduleAutoScroll(true);
@@ -1271,11 +1307,11 @@ export const createAgentExperience = (
1271
1307
  updateOpenState();
1272
1308
  } else {
1273
1309
  // Launcher was just enabled - respect autoExpand setting
1274
- setOpenState(autoExpand);
1310
+ setOpenState(autoExpand, "auto");
1275
1311
  }
1276
1312
  } else if (autoExpandChanged) {
1277
1313
  // autoExpand value changed - update state to match
1278
- setOpenState(autoExpand);
1314
+ setOpenState(autoExpand, "auto");
1279
1315
  }
1280
1316
  // Otherwise, preserve current open state (user may have manually opened/closed)
1281
1317
 
@@ -1716,7 +1752,7 @@ export const createAgentExperience = (
1716
1752
  session.getMessages(),
1717
1753
  postprocess
1718
1754
  );
1719
- suggestionsManager.render(config.suggestionChips, session, textarea);
1755
+ suggestionsManager.render(config.suggestionChips, session, textarea, undefined, config.suggestionChipsConfig);
1720
1756
  updateCopy();
1721
1757
  setComposerDisabled(session.isStreaming());
1722
1758
 
@@ -2013,15 +2049,15 @@ export const createAgentExperience = (
2013
2049
  },
2014
2050
  open() {
2015
2051
  if (!launcherEnabled) return;
2016
- setOpenState(true);
2052
+ setOpenState(true, "api");
2017
2053
  },
2018
2054
  close() {
2019
2055
  if (!launcherEnabled) return;
2020
- setOpenState(false);
2056
+ setOpenState(false, "api");
2021
2057
  },
2022
2058
  toggle() {
2023
2059
  if (!launcherEnabled) return;
2024
- setOpenState(!open);
2060
+ setOpenState(!open, "api");
2025
2061
  },
2026
2062
  clearChat() {
2027
2063
  // Clear messages in session (this will trigger onMessagesChanged which re-renders)
@@ -2082,7 +2118,7 @@ export const createAgentExperience = (
2082
2118
 
2083
2119
  // Auto-open widget if closed and launcher is enabled
2084
2120
  if (!open && launcherEnabled) {
2085
- setOpenState(true);
2121
+ setOpenState(true, "system");
2086
2122
  }
2087
2123
 
2088
2124
  textarea.value = message;
@@ -2098,7 +2134,7 @@ export const createAgentExperience = (
2098
2134
 
2099
2135
  // Auto-open widget if closed and launcher is enabled
2100
2136
  if (!open && launcherEnabled) {
2101
- setOpenState(true);
2137
+ setOpenState(true, "system");
2102
2138
  }
2103
2139
 
2104
2140
  textarea.value = "";
@@ -2113,7 +2149,7 @@ export const createAgentExperience = (
2113
2149
 
2114
2150
  // Auto-open widget if closed and launcher is enabled
2115
2151
  if (!open && launcherEnabled) {
2116
- setOpenState(true);
2152
+ setOpenState(true, "system");
2117
2153
  }
2118
2154
 
2119
2155
  voiceState.manuallyDeactivated = false;
@@ -2132,7 +2168,7 @@ export const createAgentExperience = (
2132
2168
  injectTestMessage(event: AgentWidgetEvent) {
2133
2169
  // Auto-open widget if closed and launcher is enabled
2134
2170
  if (!open && launcherEnabled) {
2135
- setOpenState(true);
2171
+ setOpenState(true, "system");
2136
2172
  }
2137
2173
  session.injectTestEvent(event);
2138
2174
  },
@@ -2156,6 +2192,21 @@ export const createAgentExperience = (
2156
2192
  off(event, handler) {
2157
2193
  eventBus.off(event, handler);
2158
2194
  },
2195
+ // State query methods
2196
+ isOpen(): boolean {
2197
+ return launcherEnabled && open;
2198
+ },
2199
+ isVoiceActive(): boolean {
2200
+ return voiceState.active;
2201
+ },
2202
+ getState(): AgentWidgetStateSnapshot {
2203
+ return {
2204
+ open: launcherEnabled && open,
2205
+ launcherEnabled,
2206
+ voiceActive: voiceState.active,
2207
+ streaming: session.isStreaming()
2208
+ };
2209
+ },
2159
2210
  destroy() {
2160
2211
  destroyCallbacks.forEach((cb) => cb());
2161
2212
  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 {