vanilla-agent 1.12.0 → 1.13.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/index.cjs +20 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.global.js +38 -37
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +21 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +83 -53
- package/src/ui.ts +11 -3
- package/src/utils/code-generators.ts +5 -1
- package/src/utils/formatting.ts +25 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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",
|
package/src/client.ts
CHANGED
|
@@ -876,6 +876,7 @@ export class AgentWidgetClient {
|
|
|
876
876
|
// Try to extract text from final structured content
|
|
877
877
|
const parser = streamParsers.get(assistant.id);
|
|
878
878
|
let extractedText: string | null = null;
|
|
879
|
+
let asyncPending = false;
|
|
879
880
|
|
|
880
881
|
if (parser) {
|
|
881
882
|
// First check if parser already has extracted text
|
|
@@ -890,6 +891,7 @@ export class AgentWidgetClient {
|
|
|
890
891
|
// Try parser.processChunk as last resort
|
|
891
892
|
const parsedResult = parser.processChunk(contentToProcess);
|
|
892
893
|
if (parsedResult instanceof Promise) {
|
|
894
|
+
asyncPending = true;
|
|
893
895
|
parsedResult.then((result) => {
|
|
894
896
|
// Extract text from result (could be string or object)
|
|
895
897
|
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
@@ -898,6 +900,9 @@ export class AgentWidgetClient {
|
|
|
898
900
|
if (currentAssistant && currentAssistant.id === assistant.id) {
|
|
899
901
|
currentAssistant.content = text;
|
|
900
902
|
currentAssistant.streaming = false;
|
|
903
|
+
// Clean up
|
|
904
|
+
streamParsers.delete(currentAssistant.id);
|
|
905
|
+
rawContentBuffers.delete(currentAssistant.id);
|
|
901
906
|
emitMessage(currentAssistant);
|
|
902
907
|
}
|
|
903
908
|
}
|
|
@@ -909,26 +914,29 @@ export class AgentWidgetClient {
|
|
|
909
914
|
}
|
|
910
915
|
}
|
|
911
916
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
// Clean up parser and buffer
|
|
921
|
-
const parserToClose = streamParsers.get(assistant.id);
|
|
922
|
-
if (parserToClose) {
|
|
923
|
-
const closeResult = parserToClose.close?.();
|
|
924
|
-
if (closeResult instanceof Promise) {
|
|
925
|
-
closeResult.catch(() => {});
|
|
917
|
+
// Skip sync emit if we're waiting on async parser
|
|
918
|
+
if (!asyncPending) {
|
|
919
|
+
// Set content: use extracted text if available, otherwise use raw content
|
|
920
|
+
if (extractedText !== null && extractedText.trim() !== "") {
|
|
921
|
+
assistant.content = extractedText;
|
|
922
|
+
} else if (!rawContentBuffers.has(assistant.id)) {
|
|
923
|
+
// Only use raw final content if we didn't accumulate chunks
|
|
924
|
+
assistant.content = ensureStringContent(finalContent);
|
|
926
925
|
}
|
|
927
|
-
|
|
926
|
+
|
|
927
|
+
// Clean up parser and buffer
|
|
928
|
+
const parserToClose = streamParsers.get(assistant.id);
|
|
929
|
+
if (parserToClose) {
|
|
930
|
+
const closeResult = parserToClose.close?.();
|
|
931
|
+
if (closeResult instanceof Promise) {
|
|
932
|
+
closeResult.catch(() => {});
|
|
933
|
+
}
|
|
934
|
+
streamParsers.delete(assistant.id);
|
|
935
|
+
}
|
|
936
|
+
rawContentBuffers.delete(assistant.id);
|
|
937
|
+
assistant.streaming = false;
|
|
938
|
+
emitMessage(assistant);
|
|
928
939
|
}
|
|
929
|
-
rawContentBuffers.delete(assistant.id);
|
|
930
|
-
assistant.streaming = false;
|
|
931
|
-
emitMessage(assistant);
|
|
932
940
|
}
|
|
933
941
|
}
|
|
934
942
|
} else if (payloadType === "step_complete") {
|
|
@@ -945,6 +953,7 @@ export class AgentWidgetClient {
|
|
|
945
953
|
// Check if we already have extracted text from streaming
|
|
946
954
|
const parser = streamParsers.get(assistant.id);
|
|
947
955
|
let hasExtractedText = false;
|
|
956
|
+
let asyncPending = false;
|
|
948
957
|
|
|
949
958
|
if (parser) {
|
|
950
959
|
// First check if parser already extracted text during streaming
|
|
@@ -971,6 +980,7 @@ export class AgentWidgetClient {
|
|
|
971
980
|
// Try parser
|
|
972
981
|
const parsedResult = parser.processChunk(contentToProcess);
|
|
973
982
|
if (parsedResult instanceof Promise) {
|
|
983
|
+
asyncPending = true;
|
|
974
984
|
parsedResult.then((result) => {
|
|
975
985
|
// Extract text from result (could be string or object)
|
|
976
986
|
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
@@ -980,22 +990,27 @@ export class AgentWidgetClient {
|
|
|
980
990
|
if (currentAssistant && currentAssistant.id === assistant.id) {
|
|
981
991
|
currentAssistant.content = text;
|
|
982
992
|
currentAssistant.streaming = false;
|
|
993
|
+
// Clean up
|
|
994
|
+
streamParsers.delete(currentAssistant.id);
|
|
995
|
+
rawContentBuffers.delete(currentAssistant.id);
|
|
983
996
|
emitMessage(currentAssistant);
|
|
984
997
|
}
|
|
985
998
|
} else {
|
|
986
999
|
// No extracted text - check if we should show raw content
|
|
987
1000
|
const finalExtractedText = parser.getExtractedText();
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1001
|
+
const currentAssistant = assistantMessage;
|
|
1002
|
+
if (currentAssistant && currentAssistant.id === assistant.id) {
|
|
1003
|
+
if (finalExtractedText !== null && finalExtractedText.trim() !== "") {
|
|
1004
|
+
currentAssistant.content = finalExtractedText;
|
|
1005
|
+
} else if (!rawContentBuffers.has(currentAssistant.id)) {
|
|
992
1006
|
// Only show raw content if we never had any extracted text
|
|
993
|
-
|
|
994
|
-
currentAssistant.content = ensureStringContent(finalContent);
|
|
995
|
-
}
|
|
996
|
-
currentAssistant.streaming = false;
|
|
997
|
-
emitMessage(currentAssistant);
|
|
1007
|
+
currentAssistant.content = ensureStringContent(finalContent);
|
|
998
1008
|
}
|
|
1009
|
+
currentAssistant.streaming = false;
|
|
1010
|
+
// Clean up
|
|
1011
|
+
streamParsers.delete(currentAssistant.id);
|
|
1012
|
+
rawContentBuffers.delete(currentAssistant.id);
|
|
1013
|
+
emitMessage(currentAssistant);
|
|
999
1014
|
}
|
|
1000
1015
|
}
|
|
1001
1016
|
});
|
|
@@ -1019,29 +1034,32 @@ export class AgentWidgetClient {
|
|
|
1019
1034
|
}
|
|
1020
1035
|
}
|
|
1021
1036
|
|
|
1022
|
-
//
|
|
1023
|
-
if (!
|
|
1024
|
-
|
|
1025
|
-
assistant.rawContent
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
// Only show raw content if we never extracted any text and no buffer was used
|
|
1029
|
-
if (!hasExtractedText && !rawContentBuffers.has(assistant.id)) {
|
|
1030
|
-
// No extracted text and no streaming happened - show raw content
|
|
1031
|
-
assistant.content = ensureStringContent(finalContent);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// Clean up parser and buffer
|
|
1035
|
-
if (parser) {
|
|
1036
|
-
const closeResult = parser.close?.();
|
|
1037
|
-
if (closeResult instanceof Promise) {
|
|
1038
|
-
closeResult.catch(() => {});
|
|
1037
|
+
// Skip sync emit if we're waiting on async parser
|
|
1038
|
+
if (!asyncPending) {
|
|
1039
|
+
// Ensure rawContent is set even if there's no parser (for action parsing)
|
|
1040
|
+
if (!assistant.rawContent) {
|
|
1041
|
+
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
1042
|
+
assistant.rawContent = rawBuffer ?? ensureStringContent(finalContent);
|
|
1039
1043
|
}
|
|
1044
|
+
|
|
1045
|
+
// Only show raw content if we never extracted any text and no buffer was used
|
|
1046
|
+
if (!hasExtractedText && !rawContentBuffers.has(assistant.id)) {
|
|
1047
|
+
// No extracted text and no streaming happened - show raw content
|
|
1048
|
+
assistant.content = ensureStringContent(finalContent);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Clean up parser and buffer
|
|
1052
|
+
if (parser) {
|
|
1053
|
+
const closeResult = parser.close?.();
|
|
1054
|
+
if (closeResult instanceof Promise) {
|
|
1055
|
+
closeResult.catch(() => {});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
streamParsers.delete(assistant.id);
|
|
1059
|
+
rawContentBuffers.delete(assistant.id);
|
|
1060
|
+
assistant.streaming = false;
|
|
1061
|
+
emitMessage(assistant);
|
|
1040
1062
|
}
|
|
1041
|
-
streamParsers.delete(assistant.id);
|
|
1042
|
-
rawContentBuffers.delete(assistant.id);
|
|
1043
|
-
assistant.streaming = false;
|
|
1044
|
-
emitMessage(assistant);
|
|
1045
1063
|
} else {
|
|
1046
1064
|
// No final content, just mark as complete and clean up
|
|
1047
1065
|
streamParsers.delete(assistant.id);
|
|
@@ -1090,12 +1108,20 @@ export class AgentWidgetClient {
|
|
|
1090
1108
|
// Clean up parser and buffer
|
|
1091
1109
|
streamParsers.delete(assistant.id);
|
|
1092
1110
|
rawContentBuffers.delete(assistant.id);
|
|
1093
|
-
|
|
1111
|
+
|
|
1112
|
+
// Only emit if something actually changed to avoid flicker
|
|
1113
|
+
const contentChanged = displayContent !== assistant.content;
|
|
1114
|
+
const streamingChanged = assistant.streaming !== false;
|
|
1115
|
+
|
|
1116
|
+
if (contentChanged) {
|
|
1094
1117
|
assistant.content = displayContent;
|
|
1095
|
-
emitMessage(assistant);
|
|
1096
1118
|
}
|
|
1097
1119
|
assistant.streaming = false;
|
|
1098
|
-
|
|
1120
|
+
|
|
1121
|
+
// Only emit if content or streaming state changed
|
|
1122
|
+
if (contentChanged || streamingChanged) {
|
|
1123
|
+
emitMessage(assistant);
|
|
1124
|
+
}
|
|
1099
1125
|
} else {
|
|
1100
1126
|
// No final content, just mark as complete and clean up
|
|
1101
1127
|
if (assistantMessage !== null) {
|
|
@@ -1104,8 +1130,12 @@ export class AgentWidgetClient {
|
|
|
1104
1130
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
1105
1131
|
streamParsers.delete(msg.id);
|
|
1106
1132
|
rawContentBuffers.delete(msg.id);
|
|
1107
|
-
|
|
1108
|
-
|
|
1133
|
+
|
|
1134
|
+
// Only emit if streaming state changed
|
|
1135
|
+
if (msg.streaming !== false) {
|
|
1136
|
+
msg.streaming = false;
|
|
1137
|
+
emitMessage(msg);
|
|
1138
|
+
}
|
|
1109
1139
|
}
|
|
1110
1140
|
}
|
|
1111
1141
|
onEvent({ type: "status", status: "idle" });
|
package/src/ui.ts
CHANGED
|
@@ -764,8 +764,8 @@ export const createAgentExperience = (
|
|
|
764
764
|
};
|
|
765
765
|
|
|
766
766
|
|
|
767
|
-
// Message rendering with plugin support
|
|
768
|
-
const
|
|
767
|
+
// Message rendering with plugin support (implementation)
|
|
768
|
+
const renderMessagesWithPluginsImpl = (
|
|
769
769
|
container: HTMLElement,
|
|
770
770
|
messages: AgentWidgetMessage[],
|
|
771
771
|
transform: MessageTransform
|
|
@@ -915,8 +915,13 @@ export const createAgentExperience = (
|
|
|
915
915
|
const hasStreamingAssistantMessage = messages.some(
|
|
916
916
|
(msg) => msg.role === "assistant" && msg.streaming
|
|
917
917
|
);
|
|
918
|
+
|
|
919
|
+
// Also check if there's a recently completed assistant message (streaming just ended)
|
|
920
|
+
// This prevents flicker when the message completes but isStreaming hasn't updated yet
|
|
921
|
+
const lastMessage = messages[messages.length - 1];
|
|
922
|
+
const hasRecentAssistantResponse = lastMessage?.role === "assistant" && !lastMessage.streaming;
|
|
918
923
|
|
|
919
|
-
if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage) {
|
|
924
|
+
if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage && !hasRecentAssistantResponse) {
|
|
920
925
|
const typingIndicator = createTypingIndicator();
|
|
921
926
|
|
|
922
927
|
// Create a bubble wrapper for the typing indicator (similar to assistant messages)
|
|
@@ -956,6 +961,9 @@ export const createAgentExperience = (
|
|
|
956
961
|
});
|
|
957
962
|
};
|
|
958
963
|
|
|
964
|
+
// Alias for clarity - the implementation handles flicker prevention via typing indicator logic
|
|
965
|
+
const renderMessagesWithPlugins = renderMessagesWithPluginsImpl;
|
|
966
|
+
|
|
959
967
|
const updateOpenState = () => {
|
|
960
968
|
if (!launcherEnabled) return;
|
|
961
969
|
if (open) {
|
|
@@ -792,6 +792,7 @@ function generateScriptInstallerCode(config: any): string {
|
|
|
792
792
|
lines.push(` debug: ${config.debug},`);
|
|
793
793
|
}
|
|
794
794
|
|
|
795
|
+
lines.push(" postprocessMessage: ({ text }) => window.AgentWidget.markdownPostprocessor(text)");
|
|
795
796
|
lines.push(" }");
|
|
796
797
|
lines.push(" };");
|
|
797
798
|
lines.push("</script>");
|
|
@@ -925,6 +926,7 @@ function generateScriptManualCode(config: any): string {
|
|
|
925
926
|
lines.push(` debug: ${config.debug},`);
|
|
926
927
|
}
|
|
927
928
|
|
|
929
|
+
lines.push(" postprocessMessage: ({ text }) => window.AgentWidget.markdownPostprocessor(text)");
|
|
928
930
|
lines.push(" }");
|
|
929
931
|
lines.push(" });");
|
|
930
932
|
lines.push("</script>");
|
|
@@ -1181,7 +1183,8 @@ function generateScriptAdvancedCode(config: any): string {
|
|
|
1181
1183
|
lines.push(" requestMiddleware: ({ payload }) => ({");
|
|
1182
1184
|
lines.push(" ...payload,");
|
|
1183
1185
|
lines.push(" metadata: domContextProvider()");
|
|
1184
|
-
lines.push(" })");
|
|
1186
|
+
lines.push(" }),");
|
|
1187
|
+
lines.push(" postprocessMessage: ({ text }) => agentWidget.markdownPostprocessor(text)");
|
|
1185
1188
|
lines.push(" });");
|
|
1186
1189
|
lines.push("");
|
|
1187
1190
|
lines.push(" // Initialize widget when DOM is loaded");
|
|
@@ -1208,6 +1211,7 @@ function generateScriptAdvancedCode(config: any): string {
|
|
|
1208
1211
|
lines.push(" // Initialize widget with DOM context");
|
|
1209
1212
|
lines.push(" const handle = agentWidget.initAgentWidget({");
|
|
1210
1213
|
lines.push(" target: 'body',");
|
|
1214
|
+
lines.push(" useShadowDom: false,");
|
|
1211
1215
|
lines.push(" config: widgetConfig");
|
|
1212
1216
|
lines.push(" });");
|
|
1213
1217
|
lines.push("");
|
package/src/utils/formatting.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { AgentWidgetReasoning, AgentWidgetToolCall, AgentWidgetStreamParser, AgentWidgetStreamParserResult } from "../types";
|
|
2
2
|
import { parse as parsePartialJson, STR, OBJ } from "partial-json";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Unescapes JSON string escape sequences that LLMs often double-escape.
|
|
6
|
+
* Converts literal \n, \r, \t sequences to actual control characters.
|
|
7
|
+
*/
|
|
8
|
+
const unescapeJsonString = (str: string): string => {
|
|
9
|
+
return str
|
|
10
|
+
.replace(/\\n/g, '\n')
|
|
11
|
+
.replace(/\\r/g, '\r')
|
|
12
|
+
.replace(/\\t/g, '\t')
|
|
13
|
+
.replace(/\\"/g, '"')
|
|
14
|
+
.replace(/\\\\/g, '\\');
|
|
15
|
+
};
|
|
16
|
+
|
|
4
17
|
export const formatUnknownValue = (value: unknown): string => {
|
|
5
18
|
if (value === null) return "null";
|
|
6
19
|
if (value === undefined) return "";
|
|
@@ -277,7 +290,7 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
|
|
|
277
290
|
// Check for component directives - extract text if present for combined text+component
|
|
278
291
|
if (parsed.component && typeof parsed.component === "string") {
|
|
279
292
|
// For component directives, extract text if present, otherwise empty
|
|
280
|
-
extractedText = typeof parsed.text === "string" ? parsed.text : "";
|
|
293
|
+
extractedText = typeof parsed.text === "string" ? unescapeJsonString(parsed.text) : "";
|
|
281
294
|
}
|
|
282
295
|
// Check for form directives - these also don't have text fields
|
|
283
296
|
else if (parsed.type === "init" && parsed.form) {
|
|
@@ -286,7 +299,7 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
|
|
|
286
299
|
}
|
|
287
300
|
// Extract text field if available
|
|
288
301
|
else if (typeof parsed.text === "string") {
|
|
289
|
-
extractedText = parsed.text;
|
|
302
|
+
extractedText = unescapeJsonString(parsed.text);
|
|
290
303
|
}
|
|
291
304
|
}
|
|
292
305
|
} catch (error) {
|
|
@@ -332,10 +345,15 @@ export const createFlexibleJsonStreamParser = (
|
|
|
332
345
|
const defaultExtractor = (parsed: any): string | null => {
|
|
333
346
|
if (!parsed || typeof parsed !== "object") return null;
|
|
334
347
|
|
|
348
|
+
// Helper to safely extract and unescape text
|
|
349
|
+
const getText = (value: any): string | null => {
|
|
350
|
+
return typeof value === "string" ? unescapeJsonString(value) : null;
|
|
351
|
+
};
|
|
352
|
+
|
|
335
353
|
// Check for component directives - extract text if present for combined text+component
|
|
336
354
|
if (parsed.component && typeof parsed.component === "string") {
|
|
337
355
|
// For component directives, extract text if present, otherwise empty
|
|
338
|
-
return typeof parsed.text === "string" ? parsed.text : "";
|
|
356
|
+
return typeof parsed.text === "string" ? unescapeJsonString(parsed.text) : "";
|
|
339
357
|
}
|
|
340
358
|
|
|
341
359
|
// Check for form directives - these also don't have text fields
|
|
@@ -348,18 +366,18 @@ export const createFlexibleJsonStreamParser = (
|
|
|
348
366
|
if (parsed.action) {
|
|
349
367
|
switch (parsed.action) {
|
|
350
368
|
case 'nav_then_click':
|
|
351
|
-
return parsed.on_load_text || parsed.text || null;
|
|
369
|
+
return getText(parsed.on_load_text) || getText(parsed.text) || null;
|
|
352
370
|
case 'message':
|
|
353
371
|
case 'message_and_click':
|
|
354
372
|
case 'checkout':
|
|
355
|
-
return parsed.text || null;
|
|
373
|
+
return getText(parsed.text) || null;
|
|
356
374
|
default:
|
|
357
|
-
return parsed.text || parsed.display_text || parsed.message || null;
|
|
375
|
+
return getText(parsed.text) || getText(parsed.display_text) || getText(parsed.message) || null;
|
|
358
376
|
}
|
|
359
377
|
}
|
|
360
378
|
|
|
361
379
|
// Fallback to common text field names
|
|
362
|
-
return parsed.text || parsed.display_text || parsed.message || parsed.content || null;
|
|
380
|
+
return getText(parsed.text) || getText(parsed.display_text) || getText(parsed.message) || getText(parsed.content) || null;
|
|
363
381
|
};
|
|
364
382
|
|
|
365
383
|
const extractText = textExtractor || defaultExtractor;
|