vanilla-agent 1.9.0 → 1.11.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.
@@ -0,0 +1,137 @@
1
+ import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
2
+ import { componentRegistry, ComponentContext } from "../components/registry";
3
+ import { ComponentDirective, createComponentStreamParser } from "./component-parser";
4
+ import { createStandardBubble, MessageTransform } from "../components/message-bubble";
5
+
6
+ /**
7
+ * Options for component middleware
8
+ */
9
+ export interface ComponentMiddlewareOptions {
10
+ config: AgentWidgetConfig;
11
+ message: AgentWidgetMessage;
12
+ transform: MessageTransform;
13
+ onPropsUpdate?: (props: Record<string, unknown>) => void;
14
+ }
15
+
16
+ /**
17
+ * Renders a component directive into an HTMLElement
18
+ */
19
+ export function renderComponentDirective(
20
+ directive: ComponentDirective,
21
+ options: ComponentMiddlewareOptions
22
+ ): HTMLElement | null {
23
+ const { config, message, onPropsUpdate } = options;
24
+
25
+ // Get component renderer from registry
26
+ const renderer = componentRegistry.get(directive.component);
27
+ if (!renderer) {
28
+ // Component not found, fall back to default rendering
29
+ console.warn(
30
+ `[ComponentMiddleware] Component "${directive.component}" not found in registry. Falling back to default rendering.`
31
+ );
32
+ return null;
33
+ }
34
+
35
+ // Create component context
36
+ const context: ComponentContext = {
37
+ message,
38
+ config,
39
+ updateProps: (newProps: Record<string, unknown>) => {
40
+ if (onPropsUpdate) {
41
+ onPropsUpdate(newProps);
42
+ }
43
+ }
44
+ };
45
+
46
+ try {
47
+ // Render the component
48
+ const element = renderer(directive.props, context);
49
+ return element;
50
+ } catch (error) {
51
+ console.error(
52
+ `[ComponentMiddleware] Error rendering component "${directive.component}":`,
53
+ error
54
+ );
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Creates middleware that processes component directives from streamed JSON
61
+ */
62
+ export function createComponentMiddleware() {
63
+ const parser = createComponentStreamParser();
64
+
65
+ return {
66
+ /**
67
+ * Process accumulated content and extract component directive
68
+ */
69
+ processChunk: (accumulatedContent: string): ComponentDirective | null => {
70
+ return parser.processChunk(accumulatedContent);
71
+ },
72
+
73
+ /**
74
+ * Get the currently extracted directive
75
+ */
76
+ getDirective: (): ComponentDirective | null => {
77
+ return parser.getExtractedDirective();
78
+ },
79
+
80
+ /**
81
+ * Reset the parser state
82
+ */
83
+ reset: () => {
84
+ parser.reset();
85
+ }
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Checks if a message contains a component directive in its raw content
91
+ */
92
+ export function hasComponentDirective(message: AgentWidgetMessage): boolean {
93
+ if (!message.rawContent) return false;
94
+
95
+ try {
96
+ const parsed = JSON.parse(message.rawContent);
97
+ return (
98
+ typeof parsed === "object" &&
99
+ parsed !== null &&
100
+ "component" in parsed &&
101
+ typeof parsed.component === "string"
102
+ );
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Extracts component directive from a complete message
110
+ */
111
+ export function extractComponentDirectiveFromMessage(
112
+ message: AgentWidgetMessage
113
+ ): ComponentDirective | null {
114
+ if (!message.rawContent) return null;
115
+
116
+ try {
117
+ const parsed = JSON.parse(message.rawContent);
118
+ if (
119
+ typeof parsed === "object" &&
120
+ parsed !== null &&
121
+ "component" in parsed &&
122
+ typeof parsed.component === "string"
123
+ ) {
124
+ return {
125
+ component: parsed.component,
126
+ props: (parsed.props && typeof parsed.props === "object" && parsed.props !== null
127
+ ? parsed.props
128
+ : {}) as Record<string, unknown>,
129
+ raw: message.rawContent
130
+ };
131
+ }
132
+ } catch {
133
+ // Not valid JSON or not a component directive
134
+ }
135
+
136
+ return null;
137
+ }
@@ -0,0 +1,119 @@
1
+ import { parse as parsePartialJson, STR, OBJ } from "partial-json";
2
+
3
+ /**
4
+ * Represents a component directive extracted from JSON
5
+ */
6
+ export interface ComponentDirective {
7
+ component: string;
8
+ props: Record<string, unknown>;
9
+ raw: string;
10
+ }
11
+
12
+ /**
13
+ * Checks if a parsed object is a component directive
14
+ */
15
+ function isComponentDirective(obj: unknown): obj is { component: string; props?: unknown } {
16
+ if (!obj || typeof obj !== "object") return false;
17
+ if (!("component" in obj)) return false;
18
+ const component = (obj as { component: unknown }).component;
19
+ return typeof component === "string" && component.length > 0;
20
+ }
21
+
22
+ /**
23
+ * Extracts component directive from parsed JSON object
24
+ */
25
+ function extractComponentDirective(
26
+ parsed: unknown,
27
+ rawJson: string
28
+ ): ComponentDirective | null {
29
+ if (!isComponentDirective(parsed)) {
30
+ return null;
31
+ }
32
+
33
+ const props = parsed.props && typeof parsed.props === "object" && parsed.props !== null
34
+ ? (parsed.props as Record<string, unknown>)
35
+ : {};
36
+
37
+ return {
38
+ component: parsed.component,
39
+ props,
40
+ raw: rawJson
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Creates a parser that extracts component directives from JSON streams
46
+ * This parser looks for objects with a "component" field and extracts
47
+ * the component name and props incrementally as they stream in.
48
+ */
49
+ export function createComponentStreamParser() {
50
+ let extractedDirective: ComponentDirective | null = null;
51
+ let processedLength = 0;
52
+
53
+ return {
54
+ /**
55
+ * Get the currently extracted component directive
56
+ */
57
+ getExtractedDirective: (): ComponentDirective | null => {
58
+ return extractedDirective;
59
+ },
60
+
61
+ /**
62
+ * Process a chunk of JSON and extract component directive if present
63
+ */
64
+ processChunk: (accumulatedContent: string): ComponentDirective | null => {
65
+ // Validate that the accumulated content looks like JSON
66
+ const trimmed = accumulatedContent.trim();
67
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
68
+ return null;
69
+ }
70
+
71
+ // Skip if no new content
72
+ if (accumulatedContent.length <= processedLength) {
73
+ return extractedDirective;
74
+ }
75
+
76
+ try {
77
+ // Parse partial JSON - allow partial strings and objects during streaming
78
+ // STR | OBJ allows incomplete strings and objects during streaming
79
+ const parsed = parsePartialJson(accumulatedContent, STR | OBJ);
80
+
81
+ // Try to extract component directive
82
+ const directive = extractComponentDirective(parsed, accumulatedContent);
83
+ if (directive) {
84
+ extractedDirective = directive;
85
+ }
86
+ } catch (error) {
87
+ // If parsing fails completely, keep the last extracted directive
88
+ // This can happen with very malformed JSON during streaming
89
+ }
90
+
91
+ // Update processed length
92
+ processedLength = accumulatedContent.length;
93
+
94
+ return extractedDirective;
95
+ },
96
+
97
+ /**
98
+ * Reset the parser state
99
+ */
100
+ reset: () => {
101
+ extractedDirective = null;
102
+ processedLength = 0;
103
+ }
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Type guard to check if an object is a component directive
109
+ */
110
+ export function isComponentDirectiveType(obj: unknown): obj is ComponentDirective {
111
+ return (
112
+ typeof obj === "object" &&
113
+ obj !== null &&
114
+ "component" in obj &&
115
+ typeof (obj as { component: unknown }).component === "string" &&
116
+ "props" in obj &&
117
+ typeof (obj as { props: unknown }).props === "object"
118
+ );
119
+ }
@@ -13,3 +13,4 @@ export const statusCopy: Record<AgentWidgetSessionStatus, string> = {
13
13
 
14
14
 
15
15
 
16
+
package/src/utils/dom.ts CHANGED
@@ -22,3 +22,4 @@ export const createFragment = (): DocumentFragment => {
22
22
 
23
23
 
24
24
 
25
+
@@ -263,8 +263,8 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
263
263
 
264
264
  // Skip if no new content
265
265
  if (accumulatedContent.length <= processedLength) {
266
- return extractedText !== null
267
- ? { text: extractedText, raw: accumulatedContent }
266
+ return extractedText !== null || extractedText === ""
267
+ ? { text: extractedText || "", raw: accumulatedContent }
268
268
  : null;
269
269
  }
270
270
 
@@ -273,9 +273,21 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
273
273
  // STR | OBJ allows incomplete strings and objects during streaming
274
274
  const parsed = parsePartialJson(accumulatedContent, STR | OBJ);
275
275
 
276
- // Extract text field if available
277
- if (parsed && typeof parsed === "object" && typeof parsed.text === "string") {
278
- extractedText = parsed.text;
276
+ if (parsed && typeof parsed === "object") {
277
+ // Check for component directives - extract text if present for combined text+component
278
+ if (parsed.component && typeof parsed.component === "string") {
279
+ // For component directives, extract text if present, otherwise empty
280
+ extractedText = typeof parsed.text === "string" ? parsed.text : "";
281
+ }
282
+ // Check for form directives - these also don't have text fields
283
+ else if (parsed.type === "init" && parsed.form) {
284
+ // For form directives, return empty - they're handled by form postprocessor
285
+ extractedText = "";
286
+ }
287
+ // Extract text field if available
288
+ else if (typeof parsed.text === "string") {
289
+ extractedText = parsed.text;
290
+ }
279
291
  }
280
292
  } catch (error) {
281
293
  // If parsing fails completely, keep the last extracted text
@@ -285,7 +297,8 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
285
297
  // Update processed length
286
298
  processedLength = accumulatedContent.length;
287
299
 
288
- // Return both the extracted text and raw JSON
300
+ // Always return raw JSON for component/form directive detection
301
+ // Return empty string for text if it's a component/form directive
289
302
  if (extractedText !== null) {
290
303
  return {
291
304
  text: extractedText,
@@ -318,6 +331,18 @@ export const createFlexibleJsonStreamParser = (
318
331
  // Default text extractor that handles common patterns
319
332
  const defaultExtractor = (parsed: any): string | null => {
320
333
  if (!parsed || typeof parsed !== "object") return null;
334
+
335
+ // Check for component directives - extract text if present for combined text+component
336
+ if (parsed.component && typeof parsed.component === "string") {
337
+ // For component directives, extract text if present, otherwise empty
338
+ return typeof parsed.text === "string" ? parsed.text : "";
339
+ }
340
+
341
+ // Check for form directives - these also don't have text fields
342
+ if (parsed.type === "init" && parsed.form) {
343
+ // For form directives, return empty - they're handled by form postprocessor
344
+ return "";
345
+ }
321
346
 
322
347
  // Check for action-based text fields
323
348
  if (parsed.action) {
@@ -373,8 +398,8 @@ export const createFlexibleJsonStreamParser = (
373
398
  // Update processed length
374
399
  processedLength = accumulatedContent.length;
375
400
 
376
- // Always return the raw JSON for action parsing
377
- // Text may be null during early streaming, that's ok
401
+ // Always return the raw JSON for action parsing and component detection
402
+ // Text may be null or empty for component/form directives, that's ok
378
403
  return {
379
404
  text: extractedText || "",
380
405
  raw: accumulatedContent
@@ -14,3 +14,4 @@ export const positionMap: Record<
14
14
 
15
15
 
16
16
 
17
+
@@ -22,3 +22,4 @@ export const applyThemeVariables = (
22
22
 
23
23
 
24
24
 
25
+