vanilla-agent 1.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-agent",
3
- "version": "1.11.0",
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
- // Set content: use extracted text if available, otherwise use raw content
913
- if (extractedText !== null && extractedText.trim() !== "") {
914
- assistant.content = extractedText;
915
- } else if (!rawContentBuffers.has(assistant.id)) {
916
- // Only use raw final content if we didn't accumulate chunks
917
- assistant.content = ensureStringContent(finalContent);
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
- streamParsers.delete(assistant.id);
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
- if (finalExtractedText === null || finalExtractedText.trim() === "") {
989
- // No extracted text available - show raw content only if no streaming happened
990
- const currentAssistant = assistantMessage;
991
- if (currentAssistant && currentAssistant.id === assistant.id) {
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
- if (!rawContentBuffers.has(assistant.id)) {
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
- // Ensure rawContent is set even if there's no parser (for action parsing)
1023
- if (!assistant.rawContent) {
1024
- const rawBuffer = rawContentBuffers.get(assistant.id);
1025
- assistant.rawContent = rawBuffer ?? ensureStringContent(finalContent);
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
- if (displayContent !== assistant.content) {
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
- emitMessage(assistant);
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
- msg.streaming = false;
1108
- emitMessage(msg);
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/index.ts CHANGED
@@ -59,6 +59,8 @@ export {
59
59
  createRegexJsonParser,
60
60
  createXmlParser
61
61
  } from "./utils/formatting";
62
+ export { generateCodeSnippet } from "./utils/code-generators";
63
+ export type { CodeFormat } from "./utils/code-generators";
62
64
  export type { AgentWidgetInitHandle };
63
65
 
64
66
  // Plugin system exports
@@ -104,7 +104,7 @@ export const initAgentWidget = (
104
104
 
105
105
  target.appendChild(host);
106
106
 
107
- const useShadow = options.useShadowDom !== false;
107
+ const useShadow = options.useShadowDom === true;
108
108
  let mount: HTMLElement;
109
109
  let root: ShadowRoot | HTMLElement;
110
110
 
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 renderMessagesWithPlugins = (
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) {