vanilla-agent 1.5.0 → 1.7.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
@@ -797,7 +797,7 @@ form:focus-within textarea {
797
797
  }
798
798
 
799
799
  /* Markdown content overflow handling */
800
- #vanilla-agent-root pre {
800
+ .vanilla-message-bubble pre {
801
801
  overflow-x: auto;
802
802
  max-width: 100%;
803
803
  word-wrap: break-word;
@@ -812,7 +812,7 @@ form:focus-within textarea {
812
812
  border: 1px solid #e5e7eb;
813
813
  }
814
814
 
815
- #vanilla-agent-root code {
815
+ .vanilla-message-bubble code {
816
816
  word-break: break-word;
817
817
  word-wrap: break-word;
818
818
  white-space: pre-wrap;
@@ -821,14 +821,14 @@ form:focus-within textarea {
821
821
  font-size: 0.875em;
822
822
  }
823
823
 
824
- #vanilla-agent-root pre code {
824
+ .vanilla-message-bubble pre code {
825
825
  font-size: inherit;
826
826
  background-color: transparent;
827
827
  padding: 0;
828
828
  border-radius: 0;
829
829
  }
830
830
 
831
- #vanilla-agent-root img {
831
+ .vanilla-message-bubble img {
832
832
  max-width: 100%;
833
833
  height: auto;
834
834
  display: block;
@@ -836,11 +836,28 @@ form:focus-within textarea {
836
836
  border-radius: 0.375rem;
837
837
  }
838
838
 
839
+ /* Ensure all links in chat bubbles have underlines */
840
+ .vanilla-message-bubble a {
841
+ text-decoration: underline;
842
+ }
843
+
844
+ .vanilla-message-bubble a:visited {
845
+ text-decoration: underline;
846
+ }
847
+
848
+ .vanilla-message-bubble a:hover {
849
+ text-decoration: underline;
850
+ }
851
+
852
+ .vanilla-message-bubble a:active {
853
+ text-decoration: underline;
854
+ }
855
+
839
856
  /* Ensure links in user messages match the text color */
840
- #vanilla-agent-root .tvw-text-white a,
841
- #vanilla-agent-root .tvw-text-white a:visited,
842
- #vanilla-agent-root .tvw-text-white a:hover,
843
- #vanilla-agent-root .tvw-text-white a:active {
857
+ .vanilla-message-user-bubble a,
858
+ .vanilla-message-user-bubble a:visited,
859
+ .vanilla-message-user-bubble a:hover,
860
+ .vanilla-message-user-bubble a:active {
844
861
  color: inherit;
845
862
  text-decoration: underline;
846
863
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-agent",
3
- "version": "1.5.0",
3
+ "version": "1.7.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",
@@ -30,13 +30,15 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^20.12.7",
33
+ "@vitest/ui": "^4.0.9",
33
34
  "eslint": "^8.57.0",
34
35
  "eslint-config-prettier": "^9.1.0",
35
36
  "postcss": "^8.4.38",
36
37
  "rimraf": "^5.0.5",
37
38
  "tailwindcss": "^3.4.10",
38
39
  "tsup": "^8.0.1",
39
- "typescript": "^5.4.5"
40
+ "typescript": "^5.4.5",
41
+ "vitest": "^4.0.9"
40
42
  },
41
43
  "engines": {
42
44
  "node": ">=18.17.0"
@@ -58,6 +60,9 @@
58
60
  "build:client": "tsup src/index.ts --format esm,cjs,iife --global-name AgentWidget --minify --sourcemap --splitting false --dts --loader \".css=text\"",
59
61
  "build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
60
62
  "lint": "eslint . --ext .ts",
61
- "typecheck": "tsc --noEmit"
63
+ "typecheck": "tsc --noEmit",
64
+ "test": "vitest",
65
+ "test:ui": "vitest --ui",
66
+ "test:run": "vitest run"
62
67
  }
63
68
  }
@@ -22,7 +22,7 @@ describe('AgentWidgetClient - JSON Streaming', () => {
22
22
  '',
23
23
  'data: {"type":"step_start","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","stepType":"prompt","index":1,"totalSteps":1,"startedAt":"2025-11-12T23:47:39.565Z"}',
24
24
  '',
25
- 'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"{\n"}',
25
+ 'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"{\\n"}',
26
26
  '',
27
27
  'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" "}',
28
28
  '',
package/src/client.ts CHANGED
@@ -1,5 +1,19 @@
1
- import { AgentWidgetConfig, AgentWidgetMessage, AgentWidgetEvent, AgentWidgetStreamParser } from "./types";
2
- import { extractTextFromJson, createPlainTextParser } from "./utils/formatting";
1
+ import {
2
+ AgentWidgetConfig,
3
+ AgentWidgetMessage,
4
+ AgentWidgetEvent,
5
+ AgentWidgetStreamParser,
6
+ AgentWidgetContextProvider,
7
+ AgentWidgetRequestMiddleware,
8
+ AgentWidgetRequestPayload
9
+ } from "./types";
10
+ import {
11
+ extractTextFromJson,
12
+ createPlainTextParser,
13
+ createJsonStreamParser,
14
+ createRegexJsonParser,
15
+ createXmlParser
16
+ } from "./utils/formatting";
3
17
 
4
18
  type DispatchOptions = {
5
19
  messages: AgentWidgetMessage[];
@@ -10,11 +24,30 @@ type SSEHandler = (event: AgentWidgetEvent) => void;
10
24
 
11
25
  const DEFAULT_ENDPOINT = "https://api.travrse.ai/v1/dispatch";
12
26
 
27
+ /**
28
+ * Maps parserType string to the corresponding parser factory function
29
+ */
30
+ function getParserFromType(parserType?: "plain" | "json" | "regex-json" | "xml"): () => AgentWidgetStreamParser {
31
+ switch (parserType) {
32
+ case "json":
33
+ return createJsonStreamParser;
34
+ case "regex-json":
35
+ return createRegexJsonParser;
36
+ case "xml":
37
+ return createXmlParser;
38
+ case "plain":
39
+ default:
40
+ return createPlainTextParser;
41
+ }
42
+ }
43
+
13
44
  export class AgentWidgetClient {
14
45
  private readonly apiUrl: string;
15
46
  private readonly headers: Record<string, string>;
16
47
  private readonly debug: boolean;
17
48
  private readonly createStreamParser: () => AgentWidgetStreamParser;
49
+ private readonly contextProviders: AgentWidgetContextProvider[];
50
+ private readonly requestMiddleware?: AgentWidgetRequestMiddleware;
18
51
 
19
52
  constructor(private config: AgentWidgetConfig = {}) {
20
53
  this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
@@ -23,8 +56,10 @@ export class AgentWidgetClient {
23
56
  ...config.headers
24
57
  };
25
58
  this.debug = Boolean(config.debug);
26
- // Use custom stream parser from config, or fall back to plain text parser
27
- this.createStreamParser = config.streamParser ?? createPlainTextParser;
59
+ // Use custom stream parser if provided, otherwise use parserType, or fall back to plain text parser
60
+ this.createStreamParser = config.streamParser ?? getParserFromType(config.parserType);
61
+ this.contextProviders = config.contextProviders ?? [];
62
+ this.requestMiddleware = config.requestMiddleware;
28
63
  }
29
64
 
30
65
  public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
@@ -35,23 +70,7 @@ export class AgentWidgetClient {
35
70
 
36
71
  onEvent({ type: "status", status: "connecting" });
37
72
 
38
- // Build simplified payload with just messages and optional flowId
39
- // Sort by createdAt to ensure chronological order (not local sequence)
40
- const body = {
41
- messages: options.messages
42
- .slice()
43
- .sort((a, b) => {
44
- const timeA = new Date(a.createdAt).getTime();
45
- const timeB = new Date(b.createdAt).getTime();
46
- return timeA - timeB;
47
- })
48
- .map((message) => ({
49
- role: message.role,
50
- content: message.content,
51
- createdAt: message.createdAt
52
- })),
53
- ...(this.config.flowId && { flowId: this.config.flowId })
54
- };
73
+ const body = await this.buildPayload(options.messages);
55
74
 
56
75
  if (this.debug) {
57
76
  // eslint-disable-next-line no-console
@@ -81,6 +100,73 @@ export class AgentWidgetClient {
81
100
  }
82
101
  }
83
102
 
103
+ private async buildPayload(
104
+ messages: AgentWidgetMessage[]
105
+ ): Promise<AgentWidgetRequestPayload> {
106
+ const normalizedMessages = messages
107
+ .slice()
108
+ .sort((a, b) => {
109
+ const timeA = new Date(a.createdAt).getTime();
110
+ const timeB = new Date(b.createdAt).getTime();
111
+ return timeA - timeB;
112
+ })
113
+ .map((message) => ({
114
+ role: message.role,
115
+ content: message.content,
116
+ createdAt: message.createdAt
117
+ }));
118
+
119
+ const payload: AgentWidgetRequestPayload = {
120
+ messages: normalizedMessages,
121
+ ...(this.config.flowId && { flowId: this.config.flowId })
122
+ };
123
+
124
+ if (this.contextProviders.length) {
125
+ const contextAggregate: Record<string, unknown> = {};
126
+ await Promise.all(
127
+ this.contextProviders.map(async (provider) => {
128
+ try {
129
+ const result = await provider({
130
+ messages,
131
+ config: this.config
132
+ });
133
+ if (result && typeof result === "object") {
134
+ Object.assign(contextAggregate, result);
135
+ }
136
+ } catch (error) {
137
+ if (typeof console !== "undefined") {
138
+ // eslint-disable-next-line no-console
139
+ console.warn("[AgentWidget] Context provider failed:", error);
140
+ }
141
+ }
142
+ })
143
+ );
144
+
145
+ if (Object.keys(contextAggregate).length) {
146
+ payload.context = contextAggregate;
147
+ }
148
+ }
149
+
150
+ if (this.requestMiddleware) {
151
+ try {
152
+ const result = await this.requestMiddleware({
153
+ payload: { ...payload },
154
+ config: this.config
155
+ });
156
+ if (result && typeof result === "object") {
157
+ return result as AgentWidgetRequestPayload;
158
+ }
159
+ } catch (error) {
160
+ if (typeof console !== "undefined") {
161
+ // eslint-disable-next-line no-console
162
+ console.error("[AgentWidget] Request middleware error:", error);
163
+ }
164
+ }
165
+ }
166
+
167
+ return payload;
168
+ }
169
+
84
170
  private async streamResponse(
85
171
  body: ReadableStream<Uint8Array>,
86
172
  onEvent: SSEHandler
@@ -554,6 +640,8 @@ export class AgentWidgetClient {
554
640
  // Accumulate raw content for structured format parsing
555
641
  const rawBuffer = rawContentBuffers.get(assistant.id) ?? "";
556
642
  const accumulatedRaw = rawBuffer + chunk;
643
+ // Store raw content for action parsing, but NEVER set assistant.content to raw JSON
644
+ assistant.rawContent = accumulatedRaw;
557
645
 
558
646
  // Use stream parser to parse
559
647
  if (!streamParsers.has(assistant.id)) {
@@ -578,6 +666,7 @@ export class AgentWidgetClient {
578
666
  // Clear any raw buffer/parser since we're in plain text mode
579
667
  rawContentBuffers.delete(assistant.id);
580
668
  streamParsers.delete(assistant.id);
669
+ assistant.rawContent = undefined;
581
670
  emitMessage(assistant);
582
671
  continue;
583
672
  }
@@ -606,15 +695,18 @@ export class AgentWidgetClient {
606
695
  currentAssistant.content += chunk;
607
696
  rawContentBuffers.delete(currentAssistant.id);
608
697
  streamParsers.delete(currentAssistant.id);
698
+ currentAssistant.rawContent = undefined;
609
699
  emitMessage(currentAssistant);
610
700
  }
611
701
  }
612
702
  // Otherwise wait for more chunks (incomplete structured format)
703
+ // Don't emit message if parser hasn't extracted text yet
613
704
  }).catch(() => {
614
705
  // On error, treat as plain text
615
706
  assistant.content += chunk;
616
707
  rawContentBuffers.delete(assistant.id);
617
708
  streamParsers.delete(assistant.id);
709
+ assistant.rawContent = undefined;
618
710
  emitMessage(assistant);
619
711
  });
620
712
  } else {
@@ -633,17 +725,16 @@ export class AgentWidgetClient {
633
725
  // Clear any raw buffer/parser if we were in structured format mode
634
726
  rawContentBuffers.delete(assistant.id);
635
727
  streamParsers.delete(assistant.id);
728
+ assistant.rawContent = undefined;
636
729
  emitMessage(assistant);
637
730
  }
638
731
  // Otherwise wait for more chunks (incomplete structured format)
732
+ // Don't emit message if parser hasn't extracted text yet
639
733
  }
640
734
 
641
- // Also check if we already have extracted text from previous chunks
642
- const currentText = parser.getExtractedText();
643
- if (currentText != null && currentText !== "" && currentText !== assistant.content) {
644
- assistant.content = currentText;
645
- emitMessage(assistant);
646
- }
735
+ // IMPORTANT: Don't call getExtractedText() and emit messages here
736
+ // This was causing raw JSON to be displayed because getExtractedText()
737
+ // wasn't extracting the "text" field correctly during streaming
647
738
  }
648
739
  if (payload.isComplete) {
649
740
  const finalContent = payload.result?.response ?? assistant.content;
@@ -651,6 +742,7 @@ export class AgentWidgetClient {
651
742
  // Check if we have raw content buffer that needs final processing
652
743
  const rawBuffer = rawContentBuffers.get(assistant.id);
653
744
  const contentToProcess = rawBuffer ?? ensureStringContent(finalContent);
745
+ assistant.rawContent = contentToProcess;
654
746
 
655
747
  // Try to extract text from final structured content
656
748
  const parser = streamParsers.get(assistant.id);
@@ -728,14 +820,18 @@ export class AgentWidgetClient {
728
820
  if (parser) {
729
821
  // First check if parser already extracted text during streaming
730
822
  const currentExtractedText = parser.getExtractedText();
823
+ const rawBuffer = rawContentBuffers.get(assistant.id);
824
+ const contentToProcess = rawBuffer ?? ensureStringContent(finalContent);
825
+
826
+ // Always set rawContent so action parsers can access the raw JSON
827
+ assistant.rawContent = contentToProcess;
828
+
731
829
  if (currentExtractedText !== null && currentExtractedText.trim() !== "") {
732
830
  // We already have extracted text from streaming - use it
733
831
  assistant.content = currentExtractedText;
734
832
  hasExtractedText = true;
735
833
  } else {
736
834
  // No extracted text yet - try to extract from final content
737
- const rawBuffer = rawContentBuffers.get(assistant.id);
738
- const contentToProcess = rawBuffer ?? ensureStringContent(finalContent);
739
835
 
740
836
  // Try fast path first
741
837
  const extractedText = extractTextFromJson(contentToProcess);
@@ -794,6 +890,12 @@ export class AgentWidgetClient {
794
890
  }
795
891
  }
796
892
 
893
+ // Ensure rawContent is set even if there's no parser (for action parsing)
894
+ if (!assistant.rawContent) {
895
+ const rawBuffer = rawContentBuffers.get(assistant.id);
896
+ assistant.rawContent = rawBuffer ?? ensureStringContent(finalContent);
897
+ }
898
+
797
899
  // Only show raw content if we never extracted any text and no buffer was used
798
900
  if (!hasExtractedText && !rawContentBuffers.has(assistant.id)) {
799
901
  // No extracted text and no streaming happened - show raw content
@@ -825,6 +927,7 @@ export class AgentWidgetClient {
825
927
  // Check if we have raw content buffer that needs final processing
826
928
  const rawBuffer = rawContentBuffers.get(assistant.id);
827
929
  const stringContent = rawBuffer ?? ensureStringContent(finalContent);
930
+ assistant.rawContent = stringContent;
828
931
  // Try to extract text from structured content
829
932
  let displayContent = ensureStringContent(finalContent);
830
933
  const parser = streamParsers.get(assistant.id);
@@ -5,6 +5,7 @@ export type MessageTransform = (context: {
5
5
  text: string;
6
6
  message: AgentWidgetMessage;
7
7
  streaming: boolean;
8
+ raw?: string;
8
9
  }) => string;
9
10
 
10
11
  // Create typing indicator element
@@ -41,6 +42,7 @@ export const createStandardBubble = (
41
42
  transform: MessageTransform
42
43
  ): HTMLElement => {
43
44
  const classes = [
45
+ "vanilla-message-bubble",
44
46
  "tvw-max-w-[85%]",
45
47
  "tvw-rounded-2xl",
46
48
  "tvw-text-sm",
@@ -50,6 +52,7 @@ export const createStandardBubble = (
50
52
 
51
53
  if (message.role === "user") {
52
54
  classes.push(
55
+ "vanilla-message-user-bubble",
53
56
  "tvw-ml-auto",
54
57
  "tvw-bg-cw-accent",
55
58
  "tvw-text-white",
@@ -58,6 +61,7 @@ export const createStandardBubble = (
58
61
  );
59
62
  } else {
60
63
  classes.push(
64
+ "vanilla-message-assistant-bubble",
61
65
  "tvw-bg-cw-surface",
62
66
  "tvw-border",
63
67
  "tvw-border-cw-message-border",
@@ -74,7 +78,8 @@ export const createStandardBubble = (
74
78
  contentDiv.innerHTML = transform({
75
79
  text: message.content,
76
80
  message,
77
- streaming: Boolean(message.streaming)
81
+ streaming: Boolean(message.streaming),
82
+ raw: message.rawContent
78
83
  });
79
84
  bubble.appendChild(contentDiv);
80
85
 
@@ -11,6 +11,8 @@ export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement
11
11
  const bubble = createElement(
12
12
  "div",
13
13
  [
14
+ "vanilla-message-bubble",
15
+ "vanilla-reasoning-bubble",
14
16
  "tvw-w-full",
15
17
  "tvw-max-w-[85%]",
16
18
  "tvw-rounded-2xl",
@@ -13,6 +13,8 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
13
13
  const bubble = createElement(
14
14
  "div",
15
15
  [
16
+ "vanilla-message-bubble",
17
+ "vanilla-tool-bubble",
16
18
  "tvw-w-full",
17
19
  "tvw-max-w-[85%]",
18
20
  "tvw-rounded-2xl",
package/src/index.ts CHANGED
@@ -25,6 +25,12 @@ export {
25
25
  type AgentWidgetSessionStatus
26
26
  } from "./session";
27
27
  export { AgentWidgetClient } from "./client";
28
+ export { createLocalStorageAdapter } from "./utils/storage";
29
+ export {
30
+ createActionManager,
31
+ defaultActionHandlers,
32
+ defaultJsonActionParser
33
+ } from "./utils/actions";
28
34
  export {
29
35
  markdownPostprocessor,
30
36
  escapeHtml,
@@ -113,46 +113,18 @@ export const initAgentWidget = (
113
113
  mountStyles(host);
114
114
  }
115
115
 
116
- let controller = createAgentExperience(mount, options.config);
116
+ let controller = createAgentExperience(mount, options.config, {
117
+ debugTools: options.debugTools
118
+ });
117
119
  options.onReady?.();
118
120
 
119
121
  const handle: AgentWidgetInitHandle = {
122
+ ...controller,
120
123
  host,
121
- update(nextConfig: AgentWidgetConfig) {
122
- controller.update(nextConfig);
123
- },
124
- open() {
125
- controller.open();
126
- },
127
- close() {
128
- controller.close();
129
- },
130
- toggle() {
131
- controller.toggle();
132
- },
133
- clearChat() {
134
- controller.clearChat();
135
- },
136
- setMessage(message: string) {
137
- return controller.setMessage(message);
138
- },
139
- submitMessage(message?: string) {
140
- return controller.submitMessage(message);
141
- },
142
- startVoiceRecognition() {
143
- return controller.startVoiceRecognition();
144
- },
145
- stopVoiceRecognition() {
146
- return controller.stopVoiceRecognition();
147
- },
148
- injectTestMessage(event: AgentWidgetEvent) {
149
- controller.injectTestMessage(event);
150
- },
151
124
  destroy() {
152
125
  controller.destroy();
153
126
  host.remove();
154
- // Clean up window reference if it was set
155
- if (options.windowKey && typeof window !== 'undefined') {
127
+ if (options.windowKey && typeof window !== "undefined") {
156
128
  delete (window as any)[options.windowKey];
157
129
  }
158
130
  }
package/src/session.ts CHANGED
@@ -133,6 +133,21 @@ export class AgentWidgetSession {
133
133
  this.callbacks.onMessagesChanged([...this.messages]);
134
134
  }
135
135
 
136
+ public hydrateMessages(messages: AgentWidgetMessage[]) {
137
+ this.abortController?.abort();
138
+ this.abortController = null;
139
+ this.messages = this.sortMessages(
140
+ messages.map((message) => ({
141
+ ...message,
142
+ streaming: false,
143
+ sequence: message.sequence ?? this.nextSequence()
144
+ }))
145
+ );
146
+ this.setStreaming(false);
147
+ this.setStatus("idle");
148
+ this.callbacks.onMessagesChanged([...this.messages]);
149
+ }
150
+
136
151
  private handleEvent = (event: AgentWidgetEvent) => {
137
152
  if (event.type === "message") {
138
153
  this.upsertMessage(event.message);
@@ -797,7 +797,7 @@ form:focus-within textarea {
797
797
  }
798
798
 
799
799
  /* Markdown content overflow handling */
800
- #vanilla-agent-root pre {
800
+ .vanilla-message-bubble pre {
801
801
  overflow-x: auto;
802
802
  max-width: 100%;
803
803
  word-wrap: break-word;
@@ -812,7 +812,7 @@ form:focus-within textarea {
812
812
  border: 1px solid #e5e7eb;
813
813
  }
814
814
 
815
- #vanilla-agent-root code {
815
+ .vanilla-message-bubble code {
816
816
  word-break: break-word;
817
817
  word-wrap: break-word;
818
818
  white-space: pre-wrap;
@@ -821,14 +821,14 @@ form:focus-within textarea {
821
821
  font-size: 0.875em;
822
822
  }
823
823
 
824
- #vanilla-agent-root pre code {
824
+ .vanilla-message-bubble pre code {
825
825
  font-size: inherit;
826
826
  background-color: transparent;
827
827
  padding: 0;
828
828
  border-radius: 0;
829
829
  }
830
830
 
831
- #vanilla-agent-root img {
831
+ .vanilla-message-bubble img {
832
832
  max-width: 100%;
833
833
  height: auto;
834
834
  display: block;
@@ -836,11 +836,28 @@ form:focus-within textarea {
836
836
  border-radius: 0.375rem;
837
837
  }
838
838
 
839
+ /* Ensure all links in chat bubbles have underlines */
840
+ .vanilla-message-bubble a {
841
+ text-decoration: underline;
842
+ }
843
+
844
+ .vanilla-message-bubble a:visited {
845
+ text-decoration: underline;
846
+ }
847
+
848
+ .vanilla-message-bubble a:hover {
849
+ text-decoration: underline;
850
+ }
851
+
852
+ .vanilla-message-bubble a:active {
853
+ text-decoration: underline;
854
+ }
855
+
839
856
  /* Ensure links in user messages match the text color */
840
- #vanilla-agent-root .tvw-text-white a,
841
- #vanilla-agent-root .tvw-text-white a:visited,
842
- #vanilla-agent-root .tvw-text-white a:hover,
843
- #vanilla-agent-root .tvw-text-white a:active {
857
+ .vanilla-message-user-bubble a,
858
+ .vanilla-message-user-bubble a:visited,
859
+ .vanilla-message-user-bubble a:hover,
860
+ .vanilla-message-user-bubble a:active {
844
861
  color: inherit;
845
862
  text-decoration: underline;
846
863
  }