vanilla-agent 1.29.0 → 1.31.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
@@ -1240,8 +1240,12 @@
1240
1240
  margin-top: 0.5rem;
1241
1241
  padding-top: 0.5rem;
1242
1242
  border-top: 1px solid var(--cw-divider, #f1f5f9);
1243
- /* Fade in when first shown (for "always" visibility) */
1244
- 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;
1245
1249
  }
1246
1250
 
1247
1251
  /* Action bar alignment */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-agent",
3
- "version": "1.29.0",
3
+ "version": "1.31.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");
@@ -94,6 +94,7 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
94
94
 
95
95
  // Build header using layout config if available, otherwise use standard builder
96
96
  const headerLayoutConfig = config?.layout?.header;
97
+ const showHeader = config?.layout?.showHeader !== false; // default to true
97
98
  const headerElements: HeaderElements = headerLayoutConfig
98
99
  ? buildHeaderWithLayout(config!, headerLayoutConfig, { showClose })
99
100
  : buildHeader({ config, showClose });
@@ -132,10 +133,26 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
132
133
 
133
134
  // Build composer/footer using extracted builder
134
135
  const composerElements: ComposerElements = buildComposer({ config });
136
+ const showFooter = config?.layout?.showFooter !== false; // default to true
135
137
 
136
138
  // Assemble container with header, body, and footer
137
- attachHeaderToContainer(container, headerElements, config);
138
- container.append(body, composerElements.footer);
139
+ if (showHeader) {
140
+ attachHeaderToContainer(container, headerElements, config);
141
+ } else {
142
+ // Hide header completely
143
+ headerElements.header.style.display = 'none';
144
+ attachHeaderToContainer(container, headerElements, config);
145
+ }
146
+
147
+ container.append(body);
148
+
149
+ if (showFooter) {
150
+ container.append(composerElements.footer);
151
+ } else {
152
+ // Hide footer completely
153
+ composerElements.footer.style.display = 'none';
154
+ container.append(composerElements.footer);
155
+ }
139
156
 
140
157
  return {
141
158
  container,
@@ -4,7 +4,34 @@ import { describeReasonStatus } from "../utils/formatting";
4
4
  import { renderLucideIcon } from "../utils/icons";
5
5
 
6
6
  // Expansion state per widget instance
7
- const reasoningExpansionState = new Set<string>();
7
+ export const reasoningExpansionState = new Set<string>();
8
+
9
+ // Helper function to update reasoning bubble UI after expansion state changes
10
+ export const updateReasoningBubbleUI = (messageId: string, bubble: HTMLElement): void => {
11
+ const expanded = reasoningExpansionState.has(messageId);
12
+ const header = bubble.querySelector('button[data-expand-header="true"]') as HTMLElement;
13
+ const content = bubble.querySelector('.tvw-border-t') as HTMLElement;
14
+
15
+ if (!header || !content) return;
16
+
17
+ header.setAttribute("aria-expanded", expanded ? "true" : "false");
18
+
19
+ // Find toggle icon container - it's the direct child div of headerMeta (which has tvw-ml-auto)
20
+ const headerMeta = header.querySelector('.tvw-ml-auto') as HTMLElement;
21
+ const toggleIcon = headerMeta?.querySelector(':scope > .tvw-flex.tvw-items-center') as HTMLElement;
22
+ if (toggleIcon) {
23
+ toggleIcon.innerHTML = "";
24
+ const iconColor = "currentColor";
25
+ const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
26
+ if (chevronIcon) {
27
+ toggleIcon.appendChild(chevronIcon);
28
+ } else {
29
+ toggleIcon.textContent = expanded ? "Hide" : "Show";
30
+ }
31
+ }
32
+
33
+ content.style.display = expanded ? "" : "none";
34
+ };
8
35
 
9
36
  export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement => {
10
37
  const reasoning = message.reasoning;
@@ -26,6 +53,9 @@ export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement
26
53
  "tvw-py-0"
27
54
  ].join(" ")
28
55
  );
56
+ // Set id for idiomorph matching
57
+ bubble.id = `bubble-${message.id}`;
58
+ bubble.setAttribute("data-message-id", message.id);
29
59
 
30
60
  if (!reasoning) {
31
61
  return bubble;
@@ -38,6 +68,8 @@ export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement
38
68
  ) as HTMLButtonElement;
39
69
  header.type = "button";
40
70
  header.setAttribute("aria-expanded", expanded ? "true" : "false");
71
+ header.setAttribute("data-expand-header", "true");
72
+ header.setAttribute("data-bubble-type", "reasoning");
41
73
 
42
74
  const headerContent = createElement("div", "tvw-flex tvw-flex-col tvw-text-left");
43
75
  const title = createElement("span", "tvw-text-xs tvw-text-cw-primary");
@@ -102,28 +134,6 @@ export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement
102
134
  content.style.display = expanded ? "" : "none";
103
135
  };
104
136
 
105
- const toggleExpansion = () => {
106
- expanded = !expanded;
107
- if (expanded) {
108
- reasoningExpansionState.add(message.id);
109
- } else {
110
- reasoningExpansionState.delete(message.id);
111
- }
112
- applyExpansionState();
113
- };
114
-
115
- header.addEventListener("pointerdown", (event) => {
116
- event.preventDefault();
117
- toggleExpansion();
118
- });
119
-
120
- header.addEventListener("keydown", (event) => {
121
- if (event.key === "Enter" || event.key === " ") {
122
- event.preventDefault();
123
- toggleExpansion();
124
- }
125
- });
126
-
127
137
  applyExpansionState();
128
138
 
129
139
  bubble.append(header, content);
@@ -4,7 +4,35 @@ import { formatUnknownValue, describeToolTitle } from "../utils/formatting";
4
4
  import { renderLucideIcon } from "../utils/icons";
5
5
 
6
6
  // Expansion state per widget instance
7
- const toolExpansionState = new Set<string>();
7
+ export const toolExpansionState = new Set<string>();
8
+
9
+ // Helper function to update tool bubble UI after expansion state changes
10
+ export const updateToolBubbleUI = (messageId: string, bubble: HTMLElement, config?: AgentWidgetConfig): void => {
11
+ const expanded = toolExpansionState.has(messageId);
12
+ const toolCallConfig = config?.toolCall ?? {};
13
+ const header = bubble.querySelector('button[data-expand-header="true"]') as HTMLElement;
14
+ const content = bubble.querySelector('.tvw-border-t') as HTMLElement;
15
+
16
+ if (!header || !content) return;
17
+
18
+ header.setAttribute("aria-expanded", expanded ? "true" : "false");
19
+
20
+ // Find toggle icon container - it's the direct child div of headerMeta (which has tvw-ml-auto)
21
+ const headerMeta = header.querySelector('.tvw-ml-auto') as HTMLElement;
22
+ const toggleIcon = headerMeta?.querySelector(':scope > .tvw-flex.tvw-items-center') as HTMLElement;
23
+ if (toggleIcon) {
24
+ toggleIcon.innerHTML = "";
25
+ const iconColor = toolCallConfig.toggleTextColor || toolCallConfig.headerTextColor || "currentColor";
26
+ const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
27
+ if (chevronIcon) {
28
+ toggleIcon.appendChild(chevronIcon);
29
+ } else {
30
+ toggleIcon.textContent = expanded ? "Hide" : "Show";
31
+ }
32
+ }
33
+
34
+ content.style.display = expanded ? "" : "none";
35
+ };
8
36
 
9
37
  export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidgetConfig): HTMLElement => {
10
38
  const tool = message.toolCall;
@@ -28,6 +56,9 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
28
56
  "tvw-py-0"
29
57
  ].join(" ")
30
58
  );
59
+ // Set id for idiomorph matching
60
+ bubble.id = `bubble-${message.id}`;
61
+ bubble.setAttribute("data-message-id", message.id);
31
62
 
32
63
  // Apply bubble-level styles
33
64
  if (toolCallConfig.backgroundColor) {
@@ -54,6 +85,8 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
54
85
  ) as HTMLButtonElement;
55
86
  header.type = "button";
56
87
  header.setAttribute("aria-expanded", expanded ? "true" : "false");
88
+ header.setAttribute("data-expand-header", "true");
89
+ header.setAttribute("data-bubble-type", "tool");
57
90
 
58
91
  // Apply header styles
59
92
  if (toolCallConfig.headerBackgroundColor) {
@@ -245,28 +278,6 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
245
278
  content.style.display = expanded ? "" : "none";
246
279
  };
247
280
 
248
- const toggleToolExpansion = () => {
249
- expanded = !expanded;
250
- if (expanded) {
251
- toolExpansionState.add(message.id);
252
- } else {
253
- toolExpansionState.delete(message.id);
254
- }
255
- applyToolExpansion();
256
- };
257
-
258
- header.addEventListener("pointerdown", (event) => {
259
- event.preventDefault();
260
- toggleToolExpansion();
261
- });
262
-
263
- header.addEventListener("keydown", (event) => {
264
- if (event.key === "Enter" || event.key === " ") {
265
- event.preventDefault();
266
- toggleToolExpansion();
267
- }
268
- });
269
-
270
281
  applyToolExpansion();
271
282
 
272
283
  bubble.append(header, content);
@@ -1240,8 +1240,12 @@
1240
1240
  margin-top: 0.5rem;
1241
1241
  padding-top: 0.5rem;
1242
1242
  border-top: 1px solid var(--cw-divider, #f1f5f9);
1243
- /* Fade in when first shown (for "always" visibility) */
1244
- 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;
1245
1249
  }
1246
1250
 
1247
1251
  /* Action bar alignment */
package/src/types.ts CHANGED
@@ -790,6 +790,19 @@ export type AgentWidgetLayoutConfig = {
790
790
  messages?: AgentWidgetMessageLayoutConfig;
791
791
  /** Slot renderers for custom content injection */
792
792
  slots?: Partial<Record<WidgetLayoutSlot, SlotRenderer>>;
793
+ /**
794
+ * Show/hide the header section entirely.
795
+ * When false, the header (including icon, title, buttons) is completely hidden.
796
+ * @default true
797
+ */
798
+ showHeader?: boolean;
799
+ /**
800
+ * Show/hide the footer/composer section entirely.
801
+ * When false, the footer (including input field, send button, suggestions) is completely hidden.
802
+ * Useful for read-only conversation previews.
803
+ * @default true
804
+ */
805
+ showFooter?: boolean;
793
806
  };
794
807
 
795
808
  // ============================================================================