@surf-kit/agent 0.2.2 → 0.3.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.
Files changed (51) hide show
  1. package/dist/chat/index.cjs +625 -204
  2. package/dist/chat/index.cjs.map +1 -1
  3. package/dist/chat/index.d.cts +11 -6
  4. package/dist/chat/index.d.ts +11 -6
  5. package/dist/chat/index.js +606 -185
  6. package/dist/chat/index.js.map +1 -1
  7. package/dist/{chat--OifhIRe.d.ts → chat-BIIDOGrD.d.ts} +10 -1
  8. package/dist/{chat-ChYl2XjV.d.cts → chat-CGamM7Mz.d.cts} +10 -1
  9. package/dist/{hooks-DLfF18IU.d.cts → hooks-B1NYoLLs.d.cts} +21 -5
  10. package/dist/{hooks-BGs8-4GK.d.ts → hooks-CTeEqnBQ.d.ts} +21 -5
  11. package/dist/hooks.cjs +126 -81
  12. package/dist/hooks.cjs.map +1 -1
  13. package/dist/hooks.d.cts +3 -3
  14. package/dist/hooks.d.ts +3 -3
  15. package/dist/hooks.js +126 -81
  16. package/dist/hooks.js.map +1 -1
  17. package/dist/index.cjs +686 -265
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +3 -3
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.js +645 -224
  22. package/dist/index.js.map +1 -1
  23. package/dist/layouts/index.cjs +646 -225
  24. package/dist/layouts/index.cjs.map +1 -1
  25. package/dist/layouts/index.d.cts +1 -1
  26. package/dist/layouts/index.d.ts +1 -1
  27. package/dist/layouts/index.js +622 -201
  28. package/dist/layouts/index.js.map +1 -1
  29. package/dist/mcp/index.cjs +1 -1
  30. package/dist/mcp/index.cjs.map +1 -1
  31. package/dist/mcp/index.js +2 -2
  32. package/dist/mcp/index.js.map +1 -1
  33. package/dist/response/index.cjs +66 -12
  34. package/dist/response/index.cjs.map +1 -1
  35. package/dist/response/index.d.cts +2 -2
  36. package/dist/response/index.d.ts +2 -2
  37. package/dist/response/index.js +64 -10
  38. package/dist/response/index.js.map +1 -1
  39. package/dist/sources/index.cjs +30 -1
  40. package/dist/sources/index.cjs.map +1 -1
  41. package/dist/sources/index.js +30 -1
  42. package/dist/sources/index.js.map +1 -1
  43. package/dist/streaming/index.cjs +202 -93
  44. package/dist/streaming/index.cjs.map +1 -1
  45. package/dist/streaming/index.d.cts +4 -3
  46. package/dist/streaming/index.d.ts +4 -3
  47. package/dist/streaming/index.js +172 -73
  48. package/dist/streaming/index.js.map +1 -1
  49. package/dist/{streaming-DbQxScpi.d.ts → streaming-Bx-ff2tt.d.ts} +1 -1
  50. package/dist/{streaming-DfT22A0z.d.cts → streaming-x7umFHoP.d.cts} +1 -1
  51. package/package.json +15 -4
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  // src/chat/AgentChat/AgentChat.tsx
4
- import { twMerge as twMerge8 } from "tailwind-merge";
4
+ import { twMerge as twMerge9 } from "tailwind-merge";
5
5
 
6
6
  // src/hooks/useAgentChat.ts
7
7
  import { useReducer, useCallback, useRef } from "react";
@@ -12,7 +12,8 @@ var initialState = {
12
12
  error: null,
13
13
  inputValue: "",
14
14
  streamPhase: "idle",
15
- streamingContent: ""
15
+ streamingContent: "",
16
+ streamingAgent: null
16
17
  };
17
18
  function reducer(state, action) {
18
19
  switch (action.type) {
@@ -26,12 +27,15 @@ function reducer(state, action) {
26
27
  error: null,
27
28
  inputValue: "",
28
29
  streamPhase: "thinking",
29
- streamingContent: ""
30
+ streamingContent: "",
31
+ streamingAgent: null
30
32
  };
31
33
  case "STREAM_PHASE":
32
34
  return { ...state, streamPhase: action.phase };
33
35
  case "STREAM_CONTENT":
34
36
  return { ...state, streamingContent: state.streamingContent + action.content };
37
+ case "STREAM_AGENT":
38
+ return { ...state, streamingAgent: action.agent };
35
39
  case "SEND_SUCCESS":
36
40
  return {
37
41
  ...state,
@@ -47,7 +51,8 @@ function reducer(state, action) {
47
51
  isLoading: false,
48
52
  error: action.error,
49
53
  streamPhase: "idle",
50
- streamingContent: ""
54
+ streamingContent: "",
55
+ streamingAgent: null
51
56
  };
52
57
  case "LOAD_CONVERSATION":
53
58
  return {
@@ -73,107 +78,142 @@ function useAgentChat(config2) {
73
78
  const configRef = useRef(config2);
74
79
  configRef.current = config2;
75
80
  const lastUserMessageRef = useRef(null);
81
+ const lastUserAttachmentsRef = useRef(void 0);
76
82
  const sendMessage = useCallback(
77
- async (content) => {
78
- const { apiUrl, streamPath = "/chat/stream", headers = {}, timeout = 3e4 } = configRef.current;
83
+ async (content, attachments) => {
84
+ const { apiUrl, streamPath = "/chat/stream", headers: headersOrFn, timeout = 3e4, bodyExtra } = configRef.current;
85
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
79
86
  lastUserMessageRef.current = content;
87
+ lastUserAttachmentsRef.current = attachments;
80
88
  const userMessage = {
81
89
  id: generateMessageId(),
82
90
  role: "user",
83
91
  content,
92
+ attachments,
84
93
  timestamp: /* @__PURE__ */ new Date()
85
94
  };
86
95
  dispatch({ type: "SEND_START", message: userMessage });
87
96
  const controller = new AbortController();
88
97
  const timeoutId = setTimeout(() => controller.abort(), timeout);
89
98
  try {
90
- const response = await fetch(`${apiUrl}${streamPath}`, {
91
- method: "POST",
92
- headers: {
93
- "Content-Type": "application/json",
94
- Accept: "text/event-stream",
95
- ...headers
96
- },
97
- body: JSON.stringify({
98
- message: content,
99
- conversation_id: state.conversationId
100
- }),
101
- signal: controller.signal
102
- });
103
- clearTimeout(timeoutId);
104
- if (!response.ok) {
105
- dispatch({
106
- type: "SEND_ERROR",
107
- error: {
108
- code: "API_ERROR",
109
- message: `HTTP ${response.status}: ${response.statusText}`,
110
- retryable: response.status >= 500
111
- }
112
- });
113
- return;
99
+ const url = `${apiUrl}${streamPath}`;
100
+ const mergedHeaders = {
101
+ "Content-Type": "application/json",
102
+ Accept: "text/event-stream",
103
+ ...headers
104
+ };
105
+ const requestBody = {
106
+ message: content,
107
+ conversation_id: state.conversationId,
108
+ ...bodyExtra
109
+ };
110
+ if (attachments && attachments.length > 0) {
111
+ requestBody.attachments = attachments.map((a) => ({
112
+ filename: a.filename,
113
+ content_type: a.content_type,
114
+ data: a.data
115
+ }));
114
116
  }
115
- const reader = response.body?.getReader();
116
- if (!reader) {
117
- dispatch({
118
- type: "SEND_ERROR",
119
- error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
117
+ const body = JSON.stringify(requestBody);
118
+ const ctx = {
119
+ accumulatedContent: "",
120
+ agentResponse: null,
121
+ capturedAgent: null,
122
+ capturedConversationId: null,
123
+ hadStreamError: false
124
+ };
125
+ const handleEvent = (event) => {
126
+ switch (event.type) {
127
+ case "agent":
128
+ ctx.capturedAgent = event.agent;
129
+ dispatch({ type: "STREAM_AGENT", agent: ctx.capturedAgent });
130
+ break;
131
+ case "phase":
132
+ dispatch({ type: "STREAM_PHASE", phase: event.phase });
133
+ break;
134
+ case "delta":
135
+ ctx.accumulatedContent += event.content;
136
+ dispatch({ type: "STREAM_CONTENT", content: event.content });
137
+ break;
138
+ case "done":
139
+ ctx.agentResponse = event.response;
140
+ ctx.capturedConversationId = event.conversation_id ?? null;
141
+ break;
142
+ case "error":
143
+ ctx.hadStreamError = true;
144
+ dispatch({ type: "SEND_ERROR", error: event.error });
145
+ break;
146
+ }
147
+ };
148
+ const { streamAdapter } = configRef.current;
149
+ if (streamAdapter) {
150
+ await streamAdapter(
151
+ url,
152
+ { method: "POST", headers: mergedHeaders, body, signal: controller.signal },
153
+ handleEvent
154
+ );
155
+ clearTimeout(timeoutId);
156
+ } else {
157
+ const response = await fetch(url, {
158
+ method: "POST",
159
+ headers: mergedHeaders,
160
+ body,
161
+ signal: controller.signal
120
162
  });
121
- return;
122
- }
123
- const decoder = new TextDecoder();
124
- let buffer = "";
125
- let accumulatedContent = "";
126
- let agentResponse = null;
127
- let capturedAgent = null;
128
- let capturedConversationId = null;
129
- while (true) {
130
- const { done, value } = await reader.read();
131
- if (done) break;
132
- buffer += decoder.decode(value, { stream: true });
133
- const lines = buffer.split("\n");
134
- buffer = lines.pop() ?? "";
135
- for (const line of lines) {
136
- if (!line.startsWith("data: ")) continue;
137
- const data = line.slice(6).trim();
138
- if (data === "[DONE]") continue;
139
- try {
140
- const event = JSON.parse(data);
141
- switch (event.type) {
142
- case "agent":
143
- capturedAgent = event.agent;
144
- break;
145
- case "phase":
146
- dispatch({ type: "STREAM_PHASE", phase: event.phase });
147
- break;
148
- case "delta":
149
- accumulatedContent += event.content;
150
- dispatch({ type: "STREAM_CONTENT", content: event.content });
151
- break;
152
- case "done":
153
- agentResponse = event.response;
154
- capturedConversationId = event.conversation_id ?? null;
155
- break;
156
- case "error":
157
- dispatch({ type: "SEND_ERROR", error: event.error });
158
- return;
163
+ clearTimeout(timeoutId);
164
+ if (!response.ok) {
165
+ dispatch({
166
+ type: "SEND_ERROR",
167
+ error: {
168
+ code: "API_ERROR",
169
+ message: `HTTP ${response.status}: ${response.statusText}`,
170
+ retryable: response.status >= 500
171
+ }
172
+ });
173
+ return;
174
+ }
175
+ const reader = response.body?.getReader();
176
+ if (!reader) {
177
+ dispatch({
178
+ type: "SEND_ERROR",
179
+ error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
180
+ });
181
+ return;
182
+ }
183
+ const decoder = new TextDecoder();
184
+ let buffer = "";
185
+ while (true) {
186
+ const { done, value } = await reader.read();
187
+ if (done) break;
188
+ buffer += decoder.decode(value, { stream: true });
189
+ const lines = buffer.split("\n");
190
+ buffer = lines.pop() ?? "";
191
+ for (const line of lines) {
192
+ if (!line.startsWith("data: ")) continue;
193
+ const data = line.slice(6).trim();
194
+ if (data === "[DONE]") continue;
195
+ try {
196
+ const event = JSON.parse(data);
197
+ handleEvent(event);
198
+ } catch {
159
199
  }
160
- } catch {
161
200
  }
162
201
  }
163
202
  }
203
+ if (ctx.hadStreamError) return;
164
204
  const assistantMessage = {
165
205
  id: generateMessageId(),
166
206
  role: "assistant",
167
- content: agentResponse?.message ?? accumulatedContent,
168
- response: agentResponse ?? void 0,
169
- agent: capturedAgent ?? void 0,
207
+ content: ctx.agentResponse?.message ?? ctx.accumulatedContent,
208
+ response: ctx.agentResponse ?? void 0,
209
+ agent: ctx.capturedAgent ?? void 0,
170
210
  timestamp: /* @__PURE__ */ new Date()
171
211
  };
172
212
  dispatch({
173
213
  type: "SEND_SUCCESS",
174
214
  message: assistantMessage,
175
- streamingContent: accumulatedContent,
176
- conversationId: capturedConversationId
215
+ streamingContent: ctx.accumulatedContent,
216
+ conversationId: ctx.capturedConversationId
177
217
  });
178
218
  } catch (err) {
179
219
  clearTimeout(timeoutId);
@@ -204,7 +244,8 @@ function useAgentChat(config2) {
204
244
  }, []);
205
245
  const submitFeedback = useCallback(
206
246
  async (messageId, rating, comment) => {
207
- const { apiUrl, feedbackPath = "/feedback", headers = {} } = configRef.current;
247
+ const { apiUrl, feedbackPath = "/feedback", headers: headersOrFn } = configRef.current;
248
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
208
249
  await fetch(`${apiUrl}${feedbackPath}`, {
209
250
  method: "POST",
210
251
  headers: { "Content-Type": "application/json", ...headers },
@@ -215,12 +256,13 @@ function useAgentChat(config2) {
215
256
  );
216
257
  const retry = useCallback(async () => {
217
258
  if (lastUserMessageRef.current) {
218
- await sendMessage(lastUserMessageRef.current);
259
+ await sendMessage(lastUserMessageRef.current, lastUserAttachmentsRef.current);
219
260
  }
220
261
  }, [sendMessage]);
221
262
  const reset = useCallback(() => {
222
263
  dispatch({ type: "RESET" });
223
264
  lastUserMessageRef.current = null;
265
+ lastUserAttachmentsRef.current = void 0;
224
266
  }, []);
225
267
  const actions = {
226
268
  sendMessage,
@@ -235,7 +277,7 @@ function useAgentChat(config2) {
235
277
 
236
278
  // src/chat/MessageThread/MessageThread.tsx
237
279
  import { twMerge as twMerge5 } from "tailwind-merge";
238
- import { useEffect, useRef as useRef2 } from "react";
280
+ import { useCallback as useCallback2, useEffect, useRef as useRef2 } from "react";
239
281
 
240
282
  // src/chat/MessageBubble/MessageBubble.tsx
241
283
  import { twMerge as twMerge4 } from "tailwind-merge";
@@ -244,6 +286,7 @@ import { twMerge as twMerge4 } from "tailwind-merge";
244
286
  import { Badge as Badge2 } from "@surf-kit/core";
245
287
 
246
288
  // src/response/ResponseMessage/ResponseMessage.tsx
289
+ import React from "react";
247
290
  import ReactMarkdown from "react-markdown";
248
291
  import rehypeSanitize from "rehype-sanitize";
249
292
  import { twMerge } from "tailwind-merge";
@@ -268,6 +311,7 @@ function ResponseMessage({ content, className }) {
268
311
  "[&_h3]:text-sm [&_h3]:font-semibold [&_h3]:text-accent [&_h3]:mt-2 [&_h3]:mb-1",
269
312
  "[&_code]:bg-surface-raised [&_code]:text-accent [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_code]:font-mono",
270
313
  "[&_pre]:bg-surface-raised [&_pre]:border [&_pre]:border-border [&_pre]:rounded-xl [&_pre]:p-4 [&_pre]:overflow-x-auto",
314
+ "[&_hr]:my-3 [&_hr]:border-border",
271
315
  "[&_blockquote]:border-l-2 [&_blockquote]:border-border-strong [&_blockquote]:pl-4 [&_blockquote]:text-text-secondary",
272
316
  "[&_a]:text-accent [&_a]:underline-offset-2 [&_a]:hover:text-accent/80",
273
317
  className
@@ -283,11 +327,24 @@ function ResponseMessage({ content, className }) {
283
327
  p: ({ children }) => /* @__PURE__ */ jsx("p", { className: "my-2", children }),
284
328
  ul: ({ children }) => /* @__PURE__ */ jsx("ul", { className: "my-2 list-disc pl-6", children }),
285
329
  ol: ({ children }) => /* @__PURE__ */ jsx("ol", { className: "my-2 list-decimal pl-6", children }),
286
- li: ({ children }) => /* @__PURE__ */ jsx("li", { className: "my-1", children }),
330
+ li: ({ children, ...props }) => {
331
+ let content2 = children;
332
+ if (props.ordered) {
333
+ content2 = React.Children.map(children, (child, i) => {
334
+ if (i === 0 && typeof child === "string") {
335
+ return child.replace(/^\d+[.)]\s*/, "");
336
+ }
337
+ return child;
338
+ });
339
+ }
340
+ return /* @__PURE__ */ jsx("li", { className: "my-1", children: content2 });
341
+ },
287
342
  strong: ({ children }) => /* @__PURE__ */ jsx("strong", { className: "font-semibold", children }),
343
+ em: ({ children }) => /* @__PURE__ */ jsx("em", { className: "italic text-text-secondary", children }),
288
344
  h1: ({ children }) => /* @__PURE__ */ jsx("h1", { className: "text-base font-bold mt-4 mb-2", children }),
289
345
  h2: ({ children }) => /* @__PURE__ */ jsx("h2", { className: "text-sm font-bold mt-3 mb-1", children }),
290
346
  h3: ({ children }) => /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold mt-2 mb-1", children }),
347
+ hr: () => /* @__PURE__ */ jsx("hr", { className: "my-3 border-border" }),
291
348
  code: ({ children }) => /* @__PURE__ */ jsx("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children })
292
349
  },
293
350
  children: normalizeMarkdownLists(content)
@@ -423,7 +480,14 @@ function renderWarning(data) {
423
480
  }
424
481
  );
425
482
  }
426
- function StructuredResponse({ uiHint, data, className }) {
483
+ function StructuredResponse({ uiHint, data: rawData, className }) {
484
+ const data = typeof rawData === "string" ? (() => {
485
+ try {
486
+ return JSON.parse(rawData);
487
+ } catch {
488
+ return null;
489
+ }
490
+ })() : rawData;
427
491
  if (!data) return null;
428
492
  let content;
429
493
  switch (uiHint) {
@@ -503,7 +567,36 @@ function SourceCard({ source, variant = "compact", onNavigate, className }) {
503
567
  children: [
504
568
  /* @__PURE__ */ jsxs2("div", { className: "flex items-start justify-between gap-2", children: [
505
569
  /* @__PURE__ */ jsxs2("div", { className: "flex-1 min-w-0", children: [
506
- /* @__PURE__ */ jsx3("p", { className: "text-sm font-medium text-text-primary truncate", children: source.title }),
570
+ source.url ? /* @__PURE__ */ jsxs2(
571
+ "a",
572
+ {
573
+ href: source.url,
574
+ target: "_blank",
575
+ rel: "noopener noreferrer",
576
+ className: "text-sm font-medium text-accent hover:underline truncate block",
577
+ onClick: (e) => e.stopPropagation(),
578
+ children: [
579
+ source.title,
580
+ /* @__PURE__ */ jsxs2(
581
+ "svg",
582
+ {
583
+ className: "inline-block ml-1 w-3 h-3 opacity-60",
584
+ viewBox: "0 0 24 24",
585
+ fill: "none",
586
+ stroke: "currentColor",
587
+ strokeWidth: "2",
588
+ strokeLinecap: "round",
589
+ strokeLinejoin: "round",
590
+ children: [
591
+ /* @__PURE__ */ jsx3("path", { d: "M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" }),
592
+ /* @__PURE__ */ jsx3("polyline", { points: "15 3 21 3 21 9" }),
593
+ /* @__PURE__ */ jsx3("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
594
+ ]
595
+ }
596
+ )
597
+ ]
598
+ }
599
+ ) : /* @__PURE__ */ jsx3("p", { className: "text-sm font-medium text-text-primary truncate", children: source.title }),
507
600
  source.section && /* @__PURE__ */ jsx3("p", { className: "text-[11px] font-semibold uppercase tracking-wider text-text-secondary truncate mt-0.5", children: source.section })
508
601
  ] }),
509
602
  /* @__PURE__ */ jsx3(
@@ -637,13 +730,16 @@ function AgentResponse({
637
730
  }) {
638
731
  return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-4 ${className ?? ""}`, "data-testid": "agent-response", children: [
639
732
  /* @__PURE__ */ jsx6(ResponseMessage, { content: response.message }),
640
- response.ui_hint !== "text" && response.structured_data && /* @__PURE__ */ jsx6(
641
- StructuredResponse,
642
- {
643
- uiHint: response.ui_hint,
644
- data: response.structured_data
645
- }
646
- ),
733
+ response.ui_hint !== "text" && response.structured_data && (() => {
734
+ const parsed = typeof response.structured_data === "string" ? (() => {
735
+ try {
736
+ return JSON.parse(response.structured_data);
737
+ } catch {
738
+ return null;
739
+ }
740
+ })() : response.structured_data;
741
+ return parsed ? /* @__PURE__ */ jsx6(StructuredResponse, { uiHint: response.ui_hint, data: parsed }) : null;
742
+ })(),
647
743
  (showConfidence || showVerification) && /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2 mt-1", "data-testid": "response-meta", children: [
648
744
  showConfidence && /* @__PURE__ */ jsxs4(
649
745
  Badge2,
@@ -694,6 +790,31 @@ function AgentResponse({
694
790
 
695
791
  // src/chat/MessageBubble/MessageBubble.tsx
696
792
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
793
+ function DocumentIcon() {
794
+ return /* @__PURE__ */ jsxs5("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
795
+ /* @__PURE__ */ jsx7("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
796
+ /* @__PURE__ */ jsx7("polyline", { points: "14 2 14 8 20 8" }),
797
+ /* @__PURE__ */ jsx7("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
798
+ /* @__PURE__ */ jsx7("line", { x1: "16", y1: "17", x2: "8", y2: "17" })
799
+ ] });
800
+ }
801
+ function AttachmentThumbnail({ attachment }) {
802
+ const isImage = attachment.content_type.startsWith("image/");
803
+ if (isImage) {
804
+ return /* @__PURE__ */ jsx7("div", { className: "rounded-lg overflow-hidden border border-black/10 max-w-[240px]", children: /* @__PURE__ */ jsx7(
805
+ "img",
806
+ {
807
+ src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
808
+ alt: attachment.filename,
809
+ className: "max-w-full max-h-[200px] object-contain"
810
+ }
811
+ ) });
812
+ }
813
+ return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 px-3 py-2 rounded-lg border border-black/10 bg-black/5", children: [
814
+ /* @__PURE__ */ jsx7(DocumentIcon, {}),
815
+ /* @__PURE__ */ jsx7("span", { className: "text-xs truncate max-w-[160px]", children: attachment.filename })
816
+ ] });
817
+ }
697
818
  function MessageBubble({
698
819
  message,
699
820
  showAgent,
@@ -701,23 +822,29 @@ function MessageBubble({
701
822
  showConfidence = true,
702
823
  showVerification = true,
703
824
  animated = true,
825
+ userBubbleClassName,
704
826
  className
705
827
  }) {
706
828
  const isUser = message.role === "user";
829
+ const hasAttachments = message.attachments && message.attachments.length > 0;
707
830
  if (isUser) {
708
831
  return /* @__PURE__ */ jsx7(
709
832
  "div",
710
833
  {
711
834
  "data-message-id": message.id,
712
835
  className: twMerge4("flex w-full justify-end", className),
713
- children: /* @__PURE__ */ jsx7(
836
+ children: /* @__PURE__ */ jsxs5(
714
837
  "div",
715
838
  {
716
839
  className: twMerge4(
717
- "max-w-[70%] rounded-[18px] rounded-br-[4px] px-4 py-2.5 bg-accent text-brand-cream break-words whitespace-pre-wrap text-sm leading-relaxed",
718
- animated && "motion-safe:animate-slideFromRight"
840
+ "max-w-[70%] rounded-[18px] rounded-br-[4px] px-4 py-2.5 bg-[#e8e8e8] text-[#1a1a1a] break-words whitespace-pre-wrap text-sm leading-relaxed",
841
+ animated && "motion-safe:animate-slideFromRight",
842
+ userBubbleClassName
719
843
  ),
720
- children: message.content
844
+ children: [
845
+ hasAttachments && /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-2 mb-2", children: message.attachments.map((att, i) => /* @__PURE__ */ jsx7(AttachmentThumbnail, { attachment: att }, `${att.filename}-${i}`)) }),
846
+ message.content
847
+ ]
721
848
  }
722
849
  )
723
850
  }
@@ -729,7 +856,7 @@ function MessageBubble({
729
856
  "data-message-id": message.id,
730
857
  className: twMerge4("flex w-full flex-col items-start gap-1.5", className),
731
858
  children: [
732
- showAgent && message.agent && /* @__PURE__ */ jsx7("div", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-text-muted px-1", children: message.agent.replace("_agent", "").replace("_", " ") }),
859
+ showAgent && message.agent && /* @__PURE__ */ jsx7("div", { className: "text-[11px] font-display font-semibold uppercase tracking-[0.08em] text-text-muted px-1", children: message.agent.replace("_agent", "").replace("_", " ") }),
733
860
  /* @__PURE__ */ jsx7(
734
861
  "div",
735
862
  {
@@ -755,34 +882,70 @@ function MessageBubble({
755
882
 
756
883
  // src/chat/MessageThread/MessageThread.tsx
757
884
  import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
758
- function MessageThread({ messages, streamingSlot, showSources, showConfidence, showVerification, className }) {
759
- const bottomRef = useRef2(null);
885
+ function MessageThread({ messages, streamingSlot, showAgent, showSources, showConfidence, showVerification, hideLastAssistant, userBubbleClassName, className }) {
886
+ const scrollRef = useRef2(null);
887
+ const isNearBottom = useRef2(true);
888
+ const isProgrammaticScroll = useRef2(false);
889
+ const hasStreaming = !!streamingSlot;
890
+ const scrollToBottom = useCallback2(() => {
891
+ const el = scrollRef.current;
892
+ if (el && isNearBottom.current) {
893
+ isProgrammaticScroll.current = true;
894
+ el.scrollTop = el.scrollHeight;
895
+ }
896
+ }, []);
897
+ const handleScroll = useCallback2(() => {
898
+ if (isProgrammaticScroll.current) {
899
+ isProgrammaticScroll.current = false;
900
+ return;
901
+ }
902
+ const el = scrollRef.current;
903
+ if (!el) return;
904
+ isNearBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
905
+ }, []);
906
+ useEffect(scrollToBottom, [messages.length, scrollToBottom]);
760
907
  useEffect(() => {
761
- bottomRef.current?.scrollIntoView?.({ behavior: "smooth" });
762
- }, [messages.length, streamingSlot]);
908
+ if (!hasStreaming) return;
909
+ let raf;
910
+ const tick = () => {
911
+ scrollToBottom();
912
+ raf = requestAnimationFrame(tick);
913
+ };
914
+ raf = requestAnimationFrame(tick);
915
+ return () => cancelAnimationFrame(raf);
916
+ }, [hasStreaming, scrollToBottom]);
763
917
  return /* @__PURE__ */ jsxs6(
764
918
  "div",
765
919
  {
920
+ ref: scrollRef,
766
921
  role: "log",
767
922
  "aria-live": "polite",
768
923
  "aria-label": "Message thread",
924
+ onScroll: handleScroll,
769
925
  className: twMerge5(
770
926
  "flex flex-col gap-4 overflow-y-auto flex-1 px-4 py-6",
771
927
  className
772
928
  ),
773
929
  children: [
774
- messages.map((message) => /* @__PURE__ */ jsx8(
775
- MessageBubble,
776
- {
777
- message,
778
- showSources,
779
- showConfidence,
780
- showVerification
781
- },
782
- message.id
783
- )),
784
- streamingSlot,
785
- /* @__PURE__ */ jsx8("div", { ref: bottomRef })
930
+ /* @__PURE__ */ jsx8("div", { className: "flex-1 shrink-0" }),
931
+ messages.map((message, i) => {
932
+ if (hideLastAssistant && i === messages.length - 1 && message.role === "assistant") {
933
+ return null;
934
+ }
935
+ return /* @__PURE__ */ jsx8(
936
+ MessageBubble,
937
+ {
938
+ message,
939
+ showAgent,
940
+ showSources,
941
+ showConfidence,
942
+ showVerification,
943
+ userBubbleClassName
944
+ },
945
+ message.id
946
+ );
947
+ }),
948
+ streamingSlot
786
949
  ]
787
950
  }
788
951
  );
@@ -790,8 +953,96 @@ function MessageThread({ messages, streamingSlot, showSources, showConfidence, s
790
953
 
791
954
  // src/chat/MessageComposer/MessageComposer.tsx
792
955
  import { twMerge as twMerge6 } from "tailwind-merge";
793
- import { useState as useState2, useRef as useRef3, useCallback as useCallback2 } from "react";
956
+ import { useState as useState2, useRef as useRef3, useCallback as useCallback3 } from "react";
794
957
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
958
+ var ALLOWED_TYPES = /* @__PURE__ */ new Set([
959
+ "image/png",
960
+ "image/jpeg",
961
+ "image/gif",
962
+ "image/webp",
963
+ "application/pdf"
964
+ ]);
965
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
966
+ var MAX_ATTACHMENTS = 5;
967
+ function ArrowUpIcon() {
968
+ return /* @__PURE__ */ jsxs7("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
969
+ /* @__PURE__ */ jsx9("path", { d: "M10 16V4" }),
970
+ /* @__PURE__ */ jsx9("path", { d: "M4 10l6-6 6 6" })
971
+ ] });
972
+ }
973
+ function StopIcon() {
974
+ return /* @__PURE__ */ jsx9("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx9("rect", { x: "3", y: "3", width: "10", height: "10", rx: "2" }) });
975
+ }
976
+ function PaperclipIcon() {
977
+ return /* @__PURE__ */ jsx9("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx9("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" }) });
978
+ }
979
+ function XIcon({ size = 14 }) {
980
+ return /* @__PURE__ */ jsxs7("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
981
+ /* @__PURE__ */ jsx9("path", { d: "M18 6L6 18" }),
982
+ /* @__PURE__ */ jsx9("path", { d: "M6 6l12 12" })
983
+ ] });
984
+ }
985
+ function DocumentIcon2() {
986
+ return /* @__PURE__ */ jsxs7("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
987
+ /* @__PURE__ */ jsx9("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
988
+ /* @__PURE__ */ jsx9("polyline", { points: "14 2 14 8 20 8" }),
989
+ /* @__PURE__ */ jsx9("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
990
+ /* @__PURE__ */ jsx9("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
991
+ /* @__PURE__ */ jsx9("polyline", { points: "10 9 9 9 8 9" })
992
+ ] });
993
+ }
994
+ function fileToBase64(file) {
995
+ return new Promise((resolve, reject) => {
996
+ const reader = new FileReader();
997
+ reader.onload = () => {
998
+ const result = reader.result;
999
+ const base64 = result.split(",")[1];
1000
+ resolve(base64);
1001
+ };
1002
+ reader.onerror = reject;
1003
+ reader.readAsDataURL(file);
1004
+ });
1005
+ }
1006
+ function AttachmentPreview({
1007
+ attachment,
1008
+ onRemove
1009
+ }) {
1010
+ const isImage = attachment.content_type.startsWith("image/");
1011
+ return /* @__PURE__ */ jsxs7("div", { className: "relative group flex-shrink-0", children: [
1012
+ isImage ? /* @__PURE__ */ jsx9("div", { className: "w-16 h-16 rounded-lg overflow-hidden border border-border/60 bg-surface-alt", children: /* @__PURE__ */ jsx9(
1013
+ "img",
1014
+ {
1015
+ src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
1016
+ alt: attachment.filename,
1017
+ className: "w-full h-full object-cover"
1018
+ }
1019
+ ) }) : /* @__PURE__ */ jsxs7("div", { className: "h-16 px-3 rounded-lg border border-border/60 bg-surface-alt flex items-center gap-2", children: [
1020
+ /* @__PURE__ */ jsx9("div", { className: "text-text-muted", children: /* @__PURE__ */ jsx9(DocumentIcon2, {}) }),
1021
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col min-w-0", children: [
1022
+ /* @__PURE__ */ jsx9("span", { className: "text-xs text-text-primary truncate max-w-[120px]", children: attachment.filename }),
1023
+ /* @__PURE__ */ jsx9("span", { className: "text-[10px] text-text-muted", children: "PDF" })
1024
+ ] })
1025
+ ] }),
1026
+ /* @__PURE__ */ jsx9(
1027
+ "button",
1028
+ {
1029
+ type: "button",
1030
+ onClick: onRemove,
1031
+ className: twMerge6(
1032
+ "absolute -top-1.5 -right-1.5",
1033
+ "w-5 h-5 rounded-full",
1034
+ "bg-text-muted/80 text-white",
1035
+ "flex items-center justify-center",
1036
+ "opacity-0 group-hover:opacity-100",
1037
+ "transition-opacity duration-150",
1038
+ "hover:bg-text-primary"
1039
+ ),
1040
+ "aria-label": `Remove ${attachment.filename}`,
1041
+ children: /* @__PURE__ */ jsx9(XIcon, { size: 10 })
1042
+ }
1043
+ )
1044
+ ] });
1045
+ }
795
1046
  function MessageComposer({
796
1047
  onSend,
797
1048
  isLoading = false,
@@ -799,23 +1050,29 @@ function MessageComposer({
799
1050
  className
800
1051
  }) {
801
1052
  const [value, setValue] = useState2("");
1053
+ const [attachments, setAttachments] = useState2([]);
1054
+ const [dragOver, setDragOver] = useState2(false);
802
1055
  const textareaRef = useRef3(null);
803
- const canSend = value.trim().length > 0 && !isLoading;
804
- const resetHeight = useCallback2(() => {
1056
+ const fileInputRef = useRef3(null);
1057
+ const canSend = (value.trim().length > 0 || attachments.length > 0) && !isLoading;
1058
+ const resetHeight = useCallback3(() => {
805
1059
  const el = textareaRef.current;
806
1060
  if (el) {
807
1061
  el.style.height = "auto";
808
1062
  el.style.overflowY = "hidden";
809
1063
  }
810
1064
  }, []);
811
- const handleSend = useCallback2(() => {
1065
+ const handleSend = useCallback3(() => {
812
1066
  if (!canSend) return;
813
- onSend(value.trim());
1067
+ const message = value.trim() || (attachments.length > 0 ? "Please analyse the attached file(s)." : "");
1068
+ if (!message && attachments.length === 0) return;
1069
+ onSend(message, attachments.length > 0 ? attachments : void 0);
814
1070
  setValue("");
1071
+ setAttachments([]);
815
1072
  resetHeight();
816
1073
  textareaRef.current?.focus();
817
- }, [canSend, onSend, value, resetHeight]);
818
- const handleKeyDown = useCallback2(
1074
+ }, [canSend, onSend, value, attachments, resetHeight]);
1075
+ const handleKeyDown = useCallback3(
819
1076
  (e) => {
820
1077
  if (e.key === "Enter" && !e.shiftKey) {
821
1078
  e.preventDefault();
@@ -824,64 +1081,194 @@ function MessageComposer({
824
1081
  },
825
1082
  [handleSend]
826
1083
  );
827
- const handleChange = useCallback2(
1084
+ const handleChange = useCallback3(
828
1085
  (e) => {
829
1086
  setValue(e.target.value);
830
1087
  const el = e.target;
831
1088
  el.style.height = "auto";
832
- const capped = Math.min(el.scrollHeight, 128);
1089
+ const capped = Math.min(el.scrollHeight, 200);
833
1090
  el.style.height = `${capped}px`;
834
- el.style.overflowY = el.scrollHeight > 128 ? "auto" : "hidden";
1091
+ el.style.overflowY = el.scrollHeight > 200 ? "auto" : "hidden";
835
1092
  },
836
1093
  []
837
1094
  );
1095
+ const addFiles = useCallback3(async (files) => {
1096
+ const fileArray = Array.from(files);
1097
+ for (const file of fileArray) {
1098
+ if (attachments.length >= MAX_ATTACHMENTS) break;
1099
+ if (!ALLOWED_TYPES.has(file.type)) continue;
1100
+ if (file.size > MAX_FILE_SIZE) continue;
1101
+ try {
1102
+ const data = await fileToBase64(file);
1103
+ const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0;
1104
+ const attachment = {
1105
+ filename: file.name,
1106
+ content_type: file.type,
1107
+ data,
1108
+ preview_url: previewUrl
1109
+ };
1110
+ setAttachments((prev) => {
1111
+ if (prev.length >= MAX_ATTACHMENTS) return prev;
1112
+ return [...prev, attachment];
1113
+ });
1114
+ } catch {
1115
+ }
1116
+ }
1117
+ }, [attachments.length]);
1118
+ const handleFileSelect = useCallback3(() => {
1119
+ fileInputRef.current?.click();
1120
+ }, []);
1121
+ const handleFileInputChange = useCallback3(
1122
+ (e) => {
1123
+ if (e.target.files) {
1124
+ void addFiles(e.target.files);
1125
+ e.target.value = "";
1126
+ }
1127
+ },
1128
+ [addFiles]
1129
+ );
1130
+ const removeAttachment = useCallback3((index) => {
1131
+ setAttachments((prev) => {
1132
+ const removed = prev[index];
1133
+ if (removed?.preview_url) URL.revokeObjectURL(removed.preview_url);
1134
+ return prev.filter((_, i) => i !== index);
1135
+ });
1136
+ }, []);
1137
+ const handlePaste = useCallback3(
1138
+ (e) => {
1139
+ const items = e.clipboardData.items;
1140
+ const files = [];
1141
+ for (const item of items) {
1142
+ if (item.kind === "file" && ALLOWED_TYPES.has(item.type)) {
1143
+ const file = item.getAsFile();
1144
+ if (file) files.push(file);
1145
+ }
1146
+ }
1147
+ if (files.length > 0) {
1148
+ void addFiles(files);
1149
+ }
1150
+ },
1151
+ [addFiles]
1152
+ );
1153
+ const handleDragOver = useCallback3((e) => {
1154
+ e.preventDefault();
1155
+ e.stopPropagation();
1156
+ setDragOver(true);
1157
+ }, []);
1158
+ const handleDragLeave = useCallback3((e) => {
1159
+ e.preventDefault();
1160
+ e.stopPropagation();
1161
+ setDragOver(false);
1162
+ }, []);
1163
+ const handleDrop = useCallback3(
1164
+ (e) => {
1165
+ e.preventDefault();
1166
+ e.stopPropagation();
1167
+ setDragOver(false);
1168
+ if (e.dataTransfer.files.length > 0) {
1169
+ void addFiles(e.dataTransfer.files);
1170
+ }
1171
+ },
1172
+ [addFiles]
1173
+ );
838
1174
  return /* @__PURE__ */ jsxs7(
839
1175
  "div",
840
1176
  {
841
1177
  className: twMerge6(
842
- "flex items-end gap-3 shrink-0 border-t border-border px-4 py-3",
1178
+ "relative shrink-0 rounded-3xl border bg-surface",
1179
+ "shadow-lg shadow-black/10",
1180
+ "transition-all duration-200",
1181
+ "focus-within:border-accent/40 focus-within:shadow-accent/5",
1182
+ dragOver ? "border-accent/60 bg-accent/5" : "border-border/60",
843
1183
  className
844
1184
  ),
1185
+ onDragOver: handleDragOver,
1186
+ onDragLeave: handleDragLeave,
1187
+ onDrop: handleDrop,
845
1188
  children: [
846
1189
  /* @__PURE__ */ jsx9(
847
- "textarea",
1190
+ "input",
848
1191
  {
849
- ref: textareaRef,
850
- value,
851
- onChange: handleChange,
852
- onKeyDown: handleKeyDown,
853
- placeholder,
854
- rows: 1,
855
- disabled: isLoading,
856
- className: twMerge6(
857
- "flex-1 resize-none rounded-xl border border-border bg-surface/80",
858
- "px-4 py-2.5 text-sm text-text-primary placeholder:text-text-muted",
859
- "focus:border-transparent focus:ring-2 focus:ring-accent/40 focus:outline-none",
860
- "disabled:opacity-50 disabled:cursor-not-allowed",
861
- "overflow-hidden",
862
- "transition-all duration-200"
863
- ),
864
- style: { colorScheme: "dark" },
865
- "aria-label": "Message input"
1192
+ ref: fileInputRef,
1193
+ type: "file",
1194
+ multiple: true,
1195
+ accept: "image/png,image/jpeg,image/gif,image/webp,application/pdf",
1196
+ onChange: handleFileInputChange,
1197
+ className: "hidden",
1198
+ "aria-hidden": "true"
866
1199
  }
867
1200
  ),
868
- /* @__PURE__ */ jsx9(
869
- "button",
1201
+ attachments.length > 0 && /* @__PURE__ */ jsx9("div", { className: "flex gap-2 px-4 pt-3 pb-1 overflow-x-auto", children: attachments.map((att, i) => /* @__PURE__ */ jsx9(
1202
+ AttachmentPreview,
870
1203
  {
871
- type: "button",
872
- onClick: handleSend,
873
- disabled: !value.trim() || isLoading,
874
- "aria-label": "Send message",
875
- className: twMerge6(
876
- "inline-flex items-center justify-center rounded-xl px-5 py-2.5",
877
- "text-sm font-semibold text-white shrink-0",
878
- "transition-all duration-200",
879
- "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
880
- value.trim() && !isLoading ? "bg-accent hover:bg-accent-hover hover:scale-[1.02] hover:shadow-glow-cyan active:scale-[0.98]" : "bg-accent/30 text-text-muted cursor-not-allowed"
881
- ),
882
- children: "Send"
883
- }
884
- )
1204
+ attachment: att,
1205
+ onRemove: () => removeAttachment(i)
1206
+ },
1207
+ `${att.filename}-${i}`
1208
+ )) }),
1209
+ dragOver && /* @__PURE__ */ jsx9("div", { className: "absolute inset-0 rounded-3xl flex items-center justify-center bg-accent/10 border-2 border-dashed border-accent/40 z-10 pointer-events-none", children: /* @__PURE__ */ jsx9("span", { className: "text-sm font-display font-semibold text-accent", children: "Drop files here" }) }),
1210
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-end", children: [
1211
+ /* @__PURE__ */ jsx9(
1212
+ "button",
1213
+ {
1214
+ type: "button",
1215
+ onClick: handleFileSelect,
1216
+ disabled: isLoading || attachments.length >= MAX_ATTACHMENTS,
1217
+ "aria-label": "Attach file",
1218
+ className: twMerge6(
1219
+ "flex-shrink-0 ml-2 mb-3",
1220
+ "inline-flex items-center justify-center",
1221
+ "w-9 h-9 rounded-full",
1222
+ "transition-all duration-200",
1223
+ "text-text-muted/60 hover:text-text-secondary hover:bg-text-muted/10",
1224
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
1225
+ "disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-transparent"
1226
+ ),
1227
+ children: /* @__PURE__ */ jsx9(PaperclipIcon, {})
1228
+ }
1229
+ ),
1230
+ /* @__PURE__ */ jsx9(
1231
+ "textarea",
1232
+ {
1233
+ ref: textareaRef,
1234
+ value,
1235
+ onChange: handleChange,
1236
+ onKeyDown: handleKeyDown,
1237
+ onPaste: handlePaste,
1238
+ placeholder,
1239
+ rows: 1,
1240
+ disabled: isLoading,
1241
+ className: twMerge6(
1242
+ "flex-1 resize-none bg-transparent",
1243
+ "pl-2 pr-14 pt-4 pb-4 text-[15px] leading-relaxed",
1244
+ "text-text-primary placeholder:text-text-muted/70",
1245
+ "focus:outline-none",
1246
+ "disabled:opacity-50 disabled:cursor-not-allowed",
1247
+ "overflow-hidden"
1248
+ ),
1249
+ style: { colorScheme: "dark" },
1250
+ "aria-label": "Message input"
1251
+ }
1252
+ ),
1253
+ /* @__PURE__ */ jsx9(
1254
+ "button",
1255
+ {
1256
+ type: "button",
1257
+ onClick: handleSend,
1258
+ disabled: !canSend,
1259
+ "aria-label": "Send message",
1260
+ className: twMerge6(
1261
+ "absolute bottom-3 right-3",
1262
+ "inline-flex items-center justify-center",
1263
+ "w-9 h-9 rounded-full",
1264
+ "transition-all duration-200",
1265
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
1266
+ canSend ? "bg-accent text-white hover:bg-accent-hover active:scale-90 shadow-md shadow-accent/25" : isLoading ? "bg-text-muted/20 text-text-secondary hover:bg-text-muted/30" : "bg-transparent text-text-muted/40 cursor-default"
1267
+ ),
1268
+ children: isLoading ? /* @__PURE__ */ jsx9(StopIcon, {}) : /* @__PURE__ */ jsx9(ArrowUpIcon, {})
1269
+ }
1270
+ )
1271
+ ] })
885
1272
  ]
886
1273
  }
887
1274
  );
@@ -894,6 +1281,7 @@ function WelcomeScreen({
894
1281
  title = "Welcome",
895
1282
  message = "How can I help you today?",
896
1283
  icon,
1284
+ iconClassName,
897
1285
  suggestedQuestions = [],
898
1286
  onQuestionSelect,
899
1287
  className
@@ -906,12 +1294,15 @@ function WelcomeScreen({
906
1294
  className
907
1295
  ),
908
1296
  children: [
909
- /* @__PURE__ */ jsx10(
1297
+ icon ? iconClassName ? /* @__PURE__ */ jsx10("div", { className: iconClassName, "aria-hidden": "true", children: icon }) : icon : /* @__PURE__ */ jsx10(
910
1298
  "div",
911
1299
  {
912
- className: "w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
1300
+ className: twMerge7(
1301
+ "w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
1302
+ iconClassName
1303
+ ),
913
1304
  "aria-hidden": "true",
914
- children: icon ?? /* @__PURE__ */ jsx10("span", { className: "text-2xl", children: "\u2726" })
1305
+ children: /* @__PURE__ */ jsx10("span", { className: "text-2xl", children: "\u2726" })
915
1306
  }
916
1307
  ),
917
1308
  /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-2", children: [
@@ -921,7 +1312,7 @@ function WelcomeScreen({
921
1312
  suggestedQuestions.length > 0 && /* @__PURE__ */ jsx10(
922
1313
  "div",
923
1314
  {
924
- className: "flex flex-wrap justify-center gap-2 max-w-md",
1315
+ className: "flex flex-wrap justify-center gap-2 max-w-xl",
925
1316
  role: "group",
926
1317
  "aria-label": "Suggested questions",
927
1318
  children: suggestedQuestions.map((question) => /* @__PURE__ */ jsx10(
@@ -930,7 +1321,7 @@ function WelcomeScreen({
930
1321
  type: "button",
931
1322
  onClick: () => onQuestionSelect?.(question),
932
1323
  className: twMerge7(
933
- "px-4 py-2 rounded-full text-sm",
1324
+ "px-3.5 py-1.5 rounded-full text-[12px]",
934
1325
  "border border-border bg-transparent text-text-secondary",
935
1326
  "hover:bg-accent/10 hover:border-interactive hover:text-text-primary",
936
1327
  "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
@@ -949,7 +1340,8 @@ function WelcomeScreen({
949
1340
 
950
1341
  // src/streaming/StreamingMessage/StreamingMessage.tsx
951
1342
  import { useEffect as useEffect3, useRef as useRef5 } from "react";
952
- import { Spinner } from "@surf-kit/core";
1343
+ import { twMerge as twMerge8 } from "tailwind-merge";
1344
+ import { WaveLoader } from "@surf-kit/core";
953
1345
 
954
1346
  // src/hooks/useCharacterDrain.ts
955
1347
  import { useState as useState3, useRef as useRef4, useEffect as useEffect2 } from "react";
@@ -978,7 +1370,10 @@ function useCharacterDrain(target, msPerChar = 15) {
978
1370
  const elapsed = now - lastTimeRef.current;
979
1371
  const charsToAdvance = Math.floor(elapsed / msPerCharRef.current);
980
1372
  if (charsToAdvance > 0 && indexRef.current < currentTarget.length) {
981
- const nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
1373
+ let nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
1374
+ while (nextIndex < currentTarget.length && currentTarget[nextIndex - 1].trim() === "") {
1375
+ nextIndex++;
1376
+ }
982
1377
  indexRef.current = nextIndex;
983
1378
  lastTimeRef.current = now;
984
1379
  setDisplayed(currentTarget.slice(0, nextIndex));
@@ -1023,14 +1418,35 @@ var phaseLabels = {
1023
1418
  generating: "Writing...",
1024
1419
  verifying: "Verifying..."
1025
1420
  };
1421
+ var CURSOR_STYLES = `
1422
+ .sk-streaming-cursor > :not(ul,ol,blockquote):last-child::after,
1423
+ .sk-streaming-cursor > :is(ul,ol):last-child > li:last-child::after,
1424
+ .sk-streaming-cursor > blockquote:last-child > p:last-child::after {
1425
+ content: "";
1426
+ display: inline-block;
1427
+ width: 2px;
1428
+ height: 1em;
1429
+ background: var(--color-accent, #38bdf8);
1430
+ animation: sk-cursor-blink 0.8s steps(1) infinite;
1431
+ margin-left: 2px;
1432
+ vertical-align: text-bottom;
1433
+ }
1434
+ @keyframes sk-cursor-blink {
1435
+ 0%, 60% { opacity: 1; }
1436
+ 61%, 100% { opacity: 0; }
1437
+ }
1438
+ `;
1026
1439
  function StreamingMessage({
1027
1440
  stream,
1028
1441
  onComplete,
1442
+ onDraining,
1029
1443
  showPhases = true,
1030
1444
  className
1031
1445
  }) {
1032
1446
  const onCompleteRef = useRef5(onComplete);
1033
1447
  onCompleteRef.current = onComplete;
1448
+ const onDrainingRef = useRef5(onDraining);
1449
+ onDrainingRef.current = onDraining;
1034
1450
  const wasActiveRef = useRef5(stream.active);
1035
1451
  useEffect3(() => {
1036
1452
  if (wasActiveRef.current && !stream.active) {
@@ -1039,35 +1455,40 @@ function StreamingMessage({
1039
1455
  wasActiveRef.current = stream.active;
1040
1456
  }, [stream.active]);
1041
1457
  const phaseLabel = phaseLabels[stream.phase];
1042
- const { displayed: displayedContent } = useCharacterDrain(stream.content);
1043
- return /* @__PURE__ */ jsxs9("div", { className, "data-testid": "streaming-message", children: [
1458
+ const { displayed: rawDisplayed, isDraining } = useCharacterDrain(stream.content);
1459
+ const displayedContent = stream.active || isDraining ? rawDisplayed.trimEnd() : rawDisplayed;
1460
+ useEffect3(() => {
1461
+ onDrainingRef.current?.(isDraining);
1462
+ }, [isDraining]);
1463
+ const agentLabel = stream.agent ? stream.agent.replace("_agent", "").replace("_", " ") : null;
1464
+ const showPhaseIndicator = showPhases && stream.active && stream.phase !== "idle" && !displayedContent;
1465
+ const showCursor = (stream.active || isDraining) && !!displayedContent;
1466
+ return /* @__PURE__ */ jsxs9("div", { className: twMerge8("flex w-full flex-col items-start", className), "data-testid": "streaming-message", children: [
1044
1467
  /* @__PURE__ */ jsxs9("div", { "aria-live": "assertive", className: "sr-only", children: [
1045
1468
  stream.active && stream.phase !== "idle" && "Response started",
1046
1469
  !stream.active && stream.content && "Response complete"
1047
1470
  ] }),
1471
+ showCursor && /* @__PURE__ */ jsx11("style", { children: CURSOR_STYLES }),
1472
+ agentLabel && /* @__PURE__ */ jsx11("div", { className: "text-[11px] font-display font-semibold uppercase tracking-[0.08em] text-text-muted px-1 mb-1.5", children: agentLabel }),
1048
1473
  /* @__PURE__ */ jsxs9("div", { className: "max-w-[88%] px-4 py-3 rounded-[18px] rounded-tl-[4px] bg-surface border border-border motion-safe:animate-springFromLeft", children: [
1049
- showPhases && stream.active && stream.phase !== "idle" && /* @__PURE__ */ jsxs9(
1474
+ showPhaseIndicator && /* @__PURE__ */ jsxs9(
1050
1475
  "div",
1051
1476
  {
1052
- className: "flex items-center gap-2 mb-2 text-sm text-text-secondary",
1477
+ className: "flex items-center gap-2 text-sm text-text-secondary",
1053
1478
  "data-testid": "phase-indicator",
1054
1479
  children: [
1055
- /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx11(Spinner, { size: "sm" }) }),
1480
+ /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx11(WaveLoader, { size: "sm", color: "#38bdf8" }) }),
1056
1481
  /* @__PURE__ */ jsx11("span", { children: phaseLabel })
1057
1482
  ]
1058
1483
  }
1059
1484
  ),
1060
- /* @__PURE__ */ jsxs9("div", { className: "text-sm leading-relaxed text-text-primary whitespace-pre-wrap", children: [
1061
- displayedContent,
1062
- stream.active && /* @__PURE__ */ jsx11(
1063
- "span",
1064
- {
1065
- className: "inline-block w-0.5 h-4 bg-accent align-text-bottom animate-pulse ml-0.5",
1066
- "aria-hidden": "true",
1067
- "data-testid": "streaming-cursor"
1068
- }
1069
- )
1070
- ] })
1485
+ displayedContent && /* @__PURE__ */ jsx11(
1486
+ ResponseMessage,
1487
+ {
1488
+ content: displayedContent,
1489
+ className: showCursor ? "sk-streaming-cursor" : void 0
1490
+ }
1491
+ )
1071
1492
  ] })
1072
1493
  ] });
1073
1494
  }
@@ -1098,7 +1519,7 @@ function AgentChat({
1098
1519
  return /* @__PURE__ */ jsxs10(
1099
1520
  "div",
1100
1521
  {
1101
- className: twMerge8(
1522
+ className: twMerge9(
1102
1523
  "flex flex-col h-full bg-canvas border border-border rounded-xl overflow-hidden",
1103
1524
  className
1104
1525
  ),
@@ -1601,7 +2022,7 @@ function ThinkingIndicator({ label = "Thinking...", className }) {
1601
2022
  }
1602
2023
 
1603
2024
  // src/streaming/ToolExecution/ToolExecution.tsx
1604
- import { Spinner as Spinner2 } from "@surf-kit/core";
2025
+ import { WaveLoader as WaveLoader2 } from "@surf-kit/core";
1605
2026
  import { jsx as jsx26, jsxs as jsxs23 } from "react/jsx-runtime";
1606
2027
  var defaultLabels = {
1607
2028
  search: "Searching knowledge base...",
@@ -1617,7 +2038,7 @@ function ToolExecution({ tool, label, className }) {
1617
2038
  role: "status",
1618
2039
  "data-testid": "tool-execution",
1619
2040
  children: [
1620
- /* @__PURE__ */ jsx26("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx26(Spinner2, { size: "sm" }) }),
2041
+ /* @__PURE__ */ jsx26("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx26(WaveLoader2, { size: "sm", color: "#38bdf8" }) }),
1621
2042
  /* @__PURE__ */ jsx26("span", { children: displayLabel })
1622
2043
  ]
1623
2044
  }
@@ -1625,7 +2046,7 @@ function ToolExecution({ tool, label, className }) {
1625
2046
  }
1626
2047
 
1627
2048
  // src/streaming/RetrievalProgress/RetrievalProgress.tsx
1628
- import { Spinner as Spinner3 } from "@surf-kit/core";
2049
+ import { WaveLoader as WaveLoader3 } from "@surf-kit/core";
1629
2050
  import { jsx as jsx27, jsxs as jsxs24 } from "react/jsx-runtime";
1630
2051
  function RetrievalProgress({ sources, isActive, className }) {
1631
2052
  return /* @__PURE__ */ jsxs24(
@@ -1637,7 +2058,7 @@ function RetrievalProgress({ sources, isActive, className }) {
1637
2058
  "data-testid": "retrieval-progress",
1638
2059
  children: [
1639
2060
  isActive && /* @__PURE__ */ jsxs24("div", { className: "flex items-center gap-2 text-sm text-text-secondary", children: [
1640
- /* @__PURE__ */ jsx27("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx27(Spinner3, { size: "sm" }) }),
2061
+ /* @__PURE__ */ jsx27("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx27(WaveLoader3, { size: "sm", color: "#38bdf8" }) }),
1641
2062
  /* @__PURE__ */ jsx27("span", { children: "Retrieving sources..." })
1642
2063
  ] }),
1643
2064
  sources.length > 0 && /* @__PURE__ */ jsx27("ul", { className: "space-y-1", "data-testid": "source-list", children: sources.map((source, index) => /* @__PURE__ */ jsxs24(
@@ -1735,7 +2156,7 @@ function TypewriterText({
1735
2156
  }
1736
2157
 
1737
2158
  // src/streaming/TypingIndicator/TypingIndicator.tsx
1738
- import { twMerge as twMerge9 } from "tailwind-merge";
2159
+ import { twMerge as twMerge10 } from "tailwind-merge";
1739
2160
  import { useReducedMotion as useReducedMotion2 } from "@surf-kit/hooks";
1740
2161
  import { jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
1741
2162
  var bounceKeyframes = `
@@ -1755,7 +2176,7 @@ function TypingIndicator({
1755
2176
  {
1756
2177
  role: "status",
1757
2178
  "aria-label": label ?? "typing",
1758
- className: twMerge9("inline-flex items-center gap-2", className),
2179
+ className: twMerge10("inline-flex items-center gap-2", className),
1759
2180
  "data-testid": "typing-indicator",
1760
2181
  children: [
1761
2182
  !reducedMotion && /* @__PURE__ */ jsx30("style", { children: bounceKeyframes }),
@@ -1777,7 +2198,7 @@ function TypingIndicator({
1777
2198
  }
1778
2199
 
1779
2200
  // src/streaming/TextGlimmer/TextGlimmer.tsx
1780
- import { twMerge as twMerge10 } from "tailwind-merge";
2201
+ import { twMerge as twMerge11 } from "tailwind-merge";
1781
2202
  import { useReducedMotion as useReducedMotion3 } from "@surf-kit/hooks";
1782
2203
  import { jsx as jsx31, jsxs as jsxs28 } from "react/jsx-runtime";
1783
2204
  var shimmerKeyframes = `
@@ -1794,7 +2215,7 @@ function TextGlimmer({ lines = 3, className }) {
1794
2215
  {
1795
2216
  role: "status",
1796
2217
  "aria-label": "Loading",
1797
- className: twMerge10("flex flex-col gap-2", className),
2218
+ className: twMerge11("flex flex-col gap-2", className),
1798
2219
  "data-testid": "text-glimmer",
1799
2220
  children: [
1800
2221
  !reducedMotion && /* @__PURE__ */ jsx31("style", { children: shimmerKeyframes }),
@@ -1820,7 +2241,7 @@ function TextGlimmer({ lines = 3, className }) {
1820
2241
  }
1821
2242
 
1822
2243
  // src/streaming/StreamingList/StreamingList.tsx
1823
- import { twMerge as twMerge11 } from "tailwind-merge";
2244
+ import { twMerge as twMerge12 } from "tailwind-merge";
1824
2245
  import { useReducedMotion as useReducedMotion4 } from "@surf-kit/hooks";
1825
2246
  import { jsx as jsx32, jsxs as jsxs29 } from "react/jsx-runtime";
1826
2247
  var fadeSlideInKeyframes = `
@@ -1838,13 +2259,13 @@ function StreamingList({
1838
2259
  }) {
1839
2260
  const reducedMotion = useReducedMotion4();
1840
2261
  if (items.length === 0 && !isStreaming) {
1841
- return emptyMessage ? /* @__PURE__ */ jsx32("p", { className: twMerge11("text-sm text-text-secondary", className), "data-testid": "streaming-list-empty", children: emptyMessage }) : null;
2262
+ return emptyMessage ? /* @__PURE__ */ jsx32("p", { className: twMerge12("text-sm text-text-secondary", className), "data-testid": "streaming-list-empty", children: emptyMessage }) : null;
1842
2263
  }
1843
2264
  return /* @__PURE__ */ jsxs29(
1844
2265
  "ul",
1845
2266
  {
1846
2267
  "aria-live": "polite",
1847
- className: twMerge11("list-none p-0 m-0", className),
2268
+ className: twMerge12("list-none p-0 m-0", className),
1848
2269
  "data-testid": "streaming-list",
1849
2270
  children: [
1850
2271
  !reducedMotion && /* @__PURE__ */ jsx32("style", { children: fadeSlideInKeyframes }),
@@ -1864,7 +2285,7 @@ function StreamingList({
1864
2285
  }
1865
2286
 
1866
2287
  // src/streaming/StreamingStructure/StreamingStructure.tsx
1867
- import { twMerge as twMerge12 } from "tailwind-merge";
2288
+ import { twMerge as twMerge13 } from "tailwind-merge";
1868
2289
  import { useReducedMotion as useReducedMotion5 } from "@surf-kit/hooks";
1869
2290
  import { jsx as jsx33, jsxs as jsxs30 } from "react/jsx-runtime";
1870
2291
  var fadeSlideInKeyframes2 = `
@@ -1913,7 +2334,7 @@ function StreamingStructure({
1913
2334
  "dl",
1914
2335
  {
1915
2336
  "aria-live": "polite",
1916
- className: twMerge12("m-0", className),
2337
+ className: twMerge13("m-0", className),
1917
2338
  "data-testid": "streaming-structure",
1918
2339
  children: [
1919
2340
  !reducedMotion && /* @__PURE__ */ jsx33("style", { children: fadeSlideInKeyframes2 }),
@@ -1936,7 +2357,7 @@ function StreamingStructure({
1936
2357
  }
1937
2358
 
1938
2359
  // src/chat/ConversationList/ConversationList.tsx
1939
- import { twMerge as twMerge13 } from "tailwind-merge";
2360
+ import { twMerge as twMerge14 } from "tailwind-merge";
1940
2361
  import { jsx as jsx34, jsxs as jsxs31 } from "react/jsx-runtime";
1941
2362
  function ConversationList({
1942
2363
  conversations,
@@ -1950,7 +2371,7 @@ function ConversationList({
1950
2371
  "nav",
1951
2372
  {
1952
2373
  "aria-label": "Conversation list",
1953
- className: twMerge13("flex flex-col h-full bg-canvas", className),
2374
+ className: twMerge14("flex flex-col h-full bg-canvas", className),
1954
2375
  children: [
1955
2376
  onNew && /* @__PURE__ */ jsx34("div", { className: "p-3 border-b border-border", children: /* @__PURE__ */ jsx34(
1956
2377
  "button",
@@ -1967,7 +2388,7 @@ function ConversationList({
1967
2388
  return /* @__PURE__ */ jsxs31(
1968
2389
  "li",
1969
2390
  {
1970
- className: twMerge13(
2391
+ className: twMerge14(
1971
2392
  "flex items-start border-b border-border transition-colors duration-200",
1972
2393
  "hover:bg-surface",
1973
2394
  isActive && "bg-surface-raised border-l-2 border-l-accent"
@@ -2027,8 +2448,8 @@ function ConversationList({
2027
2448
  }
2028
2449
 
2029
2450
  // src/layouts/AgentFullPage/AgentFullPage.tsx
2030
- import { twMerge as twMerge14 } from "tailwind-merge";
2031
- import { useState as useState7, useCallback as useCallback3 } from "react";
2451
+ import { twMerge as twMerge15 } from "tailwind-merge";
2452
+ import { useState as useState7, useCallback as useCallback4 } from "react";
2032
2453
  import { Fragment, jsx as jsx35, jsxs as jsxs32 } from "react/jsx-runtime";
2033
2454
  function AgentFullPage({
2034
2455
  endpoint,
@@ -2042,7 +2463,7 @@ function AgentFullPage({
2042
2463
  className
2043
2464
  }) {
2044
2465
  const [sidebarOpen, setSidebarOpen] = useState7(false);
2045
- const handleSelect = useCallback3(
2466
+ const handleSelect = useCallback4(
2046
2467
  (id) => {
2047
2468
  onConversationSelect?.(id);
2048
2469
  setSidebarOpen(false);
@@ -2052,7 +2473,7 @@ function AgentFullPage({
2052
2473
  return /* @__PURE__ */ jsxs32(
2053
2474
  "div",
2054
2475
  {
2055
- className: twMerge14("flex h-screen w-full overflow-hidden bg-brand-dark", className),
2476
+ className: twMerge15("flex h-screen w-full overflow-hidden bg-brand-dark", className),
2056
2477
  "data-testid": "agent-full-page",
2057
2478
  children: [
2058
2479
  showConversationList && /* @__PURE__ */ jsxs32(Fragment, { children: [
@@ -2067,7 +2488,7 @@ function AgentFullPage({
2067
2488
  /* @__PURE__ */ jsx35(
2068
2489
  "aside",
2069
2490
  {
2070
- className: twMerge14(
2491
+ className: twMerge15(
2071
2492
  "bg-brand-dark border-r border-brand-gold/15 w-72 shrink-0 flex-col z-40",
2072
2493
  // Desktop: always visible
2073
2494
  "hidden md:flex",
@@ -2133,7 +2554,7 @@ function AgentFullPage({
2133
2554
  }
2134
2555
 
2135
2556
  // src/layouts/AgentPanel/AgentPanel.tsx
2136
- import { twMerge as twMerge15 } from "tailwind-merge";
2557
+ import { twMerge as twMerge16 } from "tailwind-merge";
2137
2558
  import { useRef as useRef6, useEffect as useEffect5 } from "react";
2138
2559
  import { jsx as jsx36, jsxs as jsxs33 } from "react/jsx-runtime";
2139
2560
  function AgentPanel({
@@ -2158,13 +2579,13 @@ function AgentPanel({
2158
2579
  return /* @__PURE__ */ jsxs33(
2159
2580
  "div",
2160
2581
  {
2161
- className: twMerge15("fixed inset-0 z-50", !isOpen && "pointer-events-none"),
2582
+ className: twMerge16("fixed inset-0 z-50", !isOpen && "pointer-events-none"),
2162
2583
  "aria-hidden": !isOpen,
2163
2584
  children: [
2164
2585
  /* @__PURE__ */ jsx36(
2165
2586
  "div",
2166
2587
  {
2167
- className: twMerge15(
2588
+ className: twMerge16(
2168
2589
  "fixed inset-0 transition-opacity duration-300",
2169
2590
  isOpen ? "opacity-100 bg-brand-dark/70 backdrop-blur-sm pointer-events-auto" : "opacity-0 pointer-events-none"
2170
2591
  ),
@@ -2180,7 +2601,7 @@ function AgentPanel({
2180
2601
  "aria-label": title,
2181
2602
  "aria-modal": isOpen ? "true" : void 0,
2182
2603
  style: { width: widthStyle, maxWidth: "100vw" },
2183
- className: twMerge15(
2604
+ className: twMerge16(
2184
2605
  "fixed top-0 h-full flex flex-col z-50 bg-brand-dark shadow-card",
2185
2606
  "transition-transform duration-300 ease-in-out",
2186
2607
  side === "left" ? `left-0 border-r border-brand-gold/15 ${isOpen ? "translate-x-0" : "-translate-x-full"}` : `right-0 border-l border-brand-gold/15 ${isOpen ? "translate-x-0" : "translate-x-full"}`,
@@ -2222,8 +2643,8 @@ function AgentPanel({
2222
2643
  }
2223
2644
 
2224
2645
  // src/layouts/AgentWidget/AgentWidget.tsx
2225
- import { twMerge as twMerge16 } from "tailwind-merge";
2226
- import { useState as useState8, useCallback as useCallback4 } from "react";
2646
+ import { twMerge as twMerge17 } from "tailwind-merge";
2647
+ import { useState as useState8, useCallback as useCallback5 } from "react";
2227
2648
  import { jsx as jsx37, jsxs as jsxs34 } from "react/jsx-runtime";
2228
2649
  function AgentWidget({
2229
2650
  endpoint,
@@ -2233,7 +2654,7 @@ function AgentWidget({
2233
2654
  className
2234
2655
  }) {
2235
2656
  const [isOpen, setIsOpen] = useState8(false);
2236
- const toggle = useCallback4(() => {
2657
+ const toggle = useCallback5(() => {
2237
2658
  setIsOpen((prev) => !prev);
2238
2659
  }, []);
2239
2660
  const positionClasses = position === "bottom-left" ? "left-4 bottom-4" : "right-4 bottom-4";
@@ -2246,7 +2667,7 @@ function AgentWidget({
2246
2667
  role: "dialog",
2247
2668
  "aria-label": title,
2248
2669
  "aria-hidden": !isOpen,
2249
- className: twMerge16(
2670
+ className: twMerge17(
2250
2671
  "fixed z-50 flex flex-col",
2251
2672
  "w-[min(400px,calc(100vw-2rem))] h-[min(600px,calc(100vh-6rem))]",
2252
2673
  "rounded-2xl overflow-hidden border border-brand-gold/15",
@@ -2293,7 +2714,7 @@ function AgentWidget({
2293
2714
  onClick: toggle,
2294
2715
  "aria-label": isOpen ? "Close chat" : triggerLabel,
2295
2716
  "aria-expanded": isOpen,
2296
- className: twMerge16(
2717
+ className: twMerge17(
2297
2718
  "fixed z-50 flex items-center justify-center w-14 h-14 rounded-full",
2298
2719
  "bg-brand-blue text-brand-cream shadow-glow-cyan",
2299
2720
  "hover:bg-brand-cyan hover:shadow-glow-cyan hover:scale-105",
@@ -2311,7 +2732,7 @@ function AgentWidget({
2311
2732
  }
2312
2733
 
2313
2734
  // src/layouts/AgentEmbed/AgentEmbed.tsx
2314
- import { twMerge as twMerge17 } from "tailwind-merge";
2735
+ import { twMerge as twMerge18 } from "tailwind-merge";
2315
2736
  import { jsx as jsx38 } from "react/jsx-runtime";
2316
2737
  function AgentEmbed({
2317
2738
  endpoint,
@@ -2321,7 +2742,7 @@ function AgentEmbed({
2321
2742
  return /* @__PURE__ */ jsx38(
2322
2743
  "div",
2323
2744
  {
2324
- className: twMerge17("w-full h-full min-h-0", className),
2745
+ className: twMerge18("w-full h-full min-h-0", className),
2325
2746
  "data-testid": "agent-embed",
2326
2747
  children: /* @__PURE__ */ jsx38(
2327
2748
  AgentChat,
@@ -2337,8 +2758,8 @@ function AgentEmbed({
2337
2758
 
2338
2759
  // src/mcp/MCPToolCall/MCPToolCall.tsx
2339
2760
  import { cva } from "class-variance-authority";
2340
- import { twMerge as twMerge18 } from "tailwind-merge";
2341
- import { Badge as Badge7, Spinner as Spinner4 } from "@surf-kit/core";
2761
+ import { twMerge as twMerge19 } from "tailwind-merge";
2762
+ import { Badge as Badge7, WaveLoader as WaveLoader4 } from "@surf-kit/core";
2342
2763
  import { jsx as jsx39, jsxs as jsxs35 } from "react/jsx-runtime";
2343
2764
  var statusBadgeIntent = {
2344
2765
  pending: "default",
@@ -2381,7 +2802,7 @@ function MCPToolCall({ call, isExpanded = false, onToggleExpand, className }) {
2381
2802
  return /* @__PURE__ */ jsxs35(
2382
2803
  "div",
2383
2804
  {
2384
- className: twMerge18(container({ status: call.status }), className),
2805
+ className: twMerge19(container({ status: call.status }), className),
2385
2806
  "data-testid": "mcp-tool-call",
2386
2807
  children: [
2387
2808
  /* @__PURE__ */ jsxs35(
@@ -2398,7 +2819,7 @@ function MCPToolCall({ call, isExpanded = false, onToggleExpand, className }) {
2398
2819
  call.serverName && /* @__PURE__ */ jsx39("span", { className: "text-xs text-text-secondary truncate", children: call.serverName })
2399
2820
  ] }),
2400
2821
  /* @__PURE__ */ jsxs35("div", { className: "flex items-center gap-2 shrink-0", children: [
2401
- call.status === "running" && /* @__PURE__ */ jsx39("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx39(Spinner4, { size: "sm" }) }),
2822
+ call.status === "running" && /* @__PURE__ */ jsx39("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx39(WaveLoader4, { size: "sm", color: "#38bdf8" }) }),
2402
2823
  /* @__PURE__ */ jsx39(
2403
2824
  Badge7,
2404
2825
  {
@@ -2472,7 +2893,7 @@ function MCPToolCall({ call, isExpanded = false, onToggleExpand, className }) {
2472
2893
  }
2473
2894
 
2474
2895
  // src/mcp/MCPResourceView/MCPResourceView.tsx
2475
- import { twMerge as twMerge19 } from "tailwind-merge";
2896
+ import { twMerge as twMerge20 } from "tailwind-merge";
2476
2897
  import { jsx as jsx40, jsxs as jsxs36 } from "react/jsx-runtime";
2477
2898
  function isImageMime(mime) {
2478
2899
  return !!mime && mime.startsWith("image/");
@@ -2490,7 +2911,7 @@ function MCPResourceView({ resource, className }) {
2490
2911
  return /* @__PURE__ */ jsxs36(
2491
2912
  "div",
2492
2913
  {
2493
- className: twMerge19("rounded-lg border border-border bg-surface p-3 text-sm", className),
2914
+ className: twMerge20("rounded-lg border border-border bg-surface p-3 text-sm", className),
2494
2915
  "data-testid": "mcp-resource-view",
2495
2916
  children: [
2496
2917
  /* @__PURE__ */ jsxs36("div", { className: "mb-2", children: [
@@ -2542,7 +2963,7 @@ function MCPResourceView({ resource, className }) {
2542
2963
  // src/mcp/MCPServerStatus/MCPServerStatus.tsx
2543
2964
  import { useState as useState9 } from "react";
2544
2965
  import { cva as cva2 } from "class-variance-authority";
2545
- import { twMerge as twMerge20 } from "tailwind-merge";
2966
+ import { twMerge as twMerge21 } from "tailwind-merge";
2546
2967
  import { jsx as jsx41, jsxs as jsxs37 } from "react/jsx-runtime";
2547
2968
  var statusDot = cva2("inline-block h-2 w-2 rounded-full shrink-0", {
2548
2969
  variants: {
@@ -2570,7 +2991,7 @@ function MCPServerStatus({ server, className }) {
2570
2991
  return /* @__PURE__ */ jsxs37(
2571
2992
  "div",
2572
2993
  {
2573
- className: twMerge20("rounded-lg border border-border bg-surface p-3 text-sm", className),
2994
+ className: twMerge21("rounded-lg border border-border bg-surface p-3 text-sm", className),
2574
2995
  "data-testid": "mcp-server-status",
2575
2996
  children: [
2576
2997
  /* @__PURE__ */ jsxs37("div", { className: "flex items-center gap-2 mb-1", children: [
@@ -2684,7 +3105,7 @@ function MCPServerStatus({ server, className }) {
2684
3105
  // src/mcp/MCPApprovalDialog/MCPApprovalDialog.tsx
2685
3106
  import { useRef as useRef7, useEffect as useEffect6 } from "react";
2686
3107
  import { cva as cva3 } from "class-variance-authority";
2687
- import { twMerge as twMerge21 } from "tailwind-merge";
3108
+ import { twMerge as twMerge22 } from "tailwind-merge";
2688
3109
  import { useDialog, FocusScope } from "react-aria";
2689
3110
  import { Button as Button2, Badge as Badge8 } from "@surf-kit/core";
2690
3111
  import { jsx as jsx42, jsxs as jsxs38 } from "react/jsx-runtime";
@@ -2744,7 +3165,7 @@ function MCPApprovalDialog({
2744
3165
  {
2745
3166
  ...dialogProps,
2746
3167
  ref,
2747
- className: twMerge21(riskBorder({ risk: riskLevel }), className),
3168
+ className: twMerge22(riskBorder({ risk: riskLevel }), className),
2748
3169
  "data-testid": "mcp-approval-dialog",
2749
3170
  children: [
2750
3171
  /* @__PURE__ */ jsxs38("div", { className: "flex items-center justify-between mb-4", children: [