@surf-kit/agent 0.2.2 → 0.4.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 +733 -222
  2. package/dist/chat/index.cjs.map +1 -1
  3. package/dist/chat/index.d.cts +13 -7
  4. package/dist/chat/index.d.ts +13 -7
  5. package/dist/chat/index.js +715 -204
  6. package/dist/chat/index.js.map +1 -1
  7. package/dist/{chat-ChYl2XjV.d.cts → chat-BRY3xGg_.d.cts} +11 -2
  8. package/dist/{chat--OifhIRe.d.ts → chat-CcKc6OAR.d.ts} +11 -2
  9. package/dist/{hooks-BGs8-4GK.d.ts → hooks-BLeiVk-x.d.ts} +22 -5
  10. package/dist/{hooks-DLfF18IU.d.cts → hooks-CSGGLd7j.d.cts} +22 -5
  11. package/dist/hooks.cjs +160 -85
  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 +160 -85
  16. package/dist/hooks.js.map +1 -1
  17. package/dist/index.cjs +794 -283
  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 +758 -247
  22. package/dist/index.js.map +1 -1
  23. package/dist/layouts/index.cjs +754 -243
  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 +733 -222
  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 +100 -15
  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 +99 -14
  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 +213 -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 +183 -73
  48. package/dist/streaming/index.js.map +1 -1
  49. package/dist/{streaming-DfT22A0z.d.cts → streaming-BHPXnwwo.d.cts} +3 -1
  50. package/dist/{streaming-DbQxScpi.d.ts → streaming-C6mbU7My.d.ts} +3 -1
  51. package/package.json +17 -5
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,17 @@ 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_CONTENT_RESET":
38
+ return { ...state, streamingContent: "" };
39
+ case "STREAM_AGENT":
40
+ return { ...state, streamingAgent: action.agent };
35
41
  case "SEND_SUCCESS":
36
42
  return {
37
43
  ...state,
@@ -47,7 +53,8 @@ function reducer(state, action) {
47
53
  isLoading: false,
48
54
  error: action.error,
49
55
  streamPhase: "idle",
50
- streamingContent: ""
56
+ streamingContent: "",
57
+ streamingAgent: null
51
58
  };
52
59
  case "LOAD_CONVERSATION":
53
60
  return {
@@ -73,115 +80,172 @@ function useAgentChat(config2) {
73
80
  const configRef = useRef(config2);
74
81
  configRef.current = config2;
75
82
  const lastUserMessageRef = useRef(null);
83
+ const lastUserAttachmentsRef = useRef(void 0);
84
+ const abortControllerRef = useRef(null);
76
85
  const sendMessage = useCallback(
77
- async (content) => {
78
- const { apiUrl, streamPath = "/chat/stream", headers = {}, timeout = 3e4 } = configRef.current;
86
+ async (content, attachments) => {
87
+ const { apiUrl, streamPath = "/chat/stream", headers: headersOrFn, timeout = 3e4, bodyExtra } = configRef.current;
88
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
79
89
  lastUserMessageRef.current = content;
90
+ lastUserAttachmentsRef.current = attachments;
80
91
  const userMessage = {
81
92
  id: generateMessageId(),
82
93
  role: "user",
83
94
  content,
95
+ attachments,
84
96
  timestamp: /* @__PURE__ */ new Date()
85
97
  };
86
98
  dispatch({ type: "SEND_START", message: userMessage });
87
99
  const controller = new AbortController();
100
+ abortControllerRef.current = controller;
88
101
  const timeoutId = setTimeout(() => controller.abort(), timeout);
102
+ const ctx = {
103
+ accumulatedContent: "",
104
+ agentResponse: null,
105
+ capturedAgent: null,
106
+ capturedConversationId: null,
107
+ hadStreamError: false
108
+ };
89
109
  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;
110
+ const url = `${apiUrl}${streamPath}`;
111
+ const mergedHeaders = {
112
+ "Content-Type": "application/json",
113
+ Accept: "text/event-stream",
114
+ ...headers
115
+ };
116
+ const requestBody = {
117
+ message: content,
118
+ conversation_id: state.conversationId,
119
+ ...bodyExtra
120
+ };
121
+ if (attachments && attachments.length > 0) {
122
+ requestBody.attachments = attachments.map((a) => ({
123
+ filename: a.filename,
124
+ content_type: a.content_type,
125
+ data: a.data
126
+ }));
114
127
  }
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 }
128
+ const body = JSON.stringify(requestBody);
129
+ const handleEvent = (event) => {
130
+ switch (event.type) {
131
+ case "agent":
132
+ ctx.capturedAgent = event.agent;
133
+ dispatch({ type: "STREAM_AGENT", agent: ctx.capturedAgent });
134
+ break;
135
+ case "phase":
136
+ dispatch({ type: "STREAM_PHASE", phase: event.phase });
137
+ break;
138
+ case "delta":
139
+ ctx.accumulatedContent += event.content;
140
+ dispatch({ type: "STREAM_CONTENT", content: event.content });
141
+ break;
142
+ case "delta_reset":
143
+ ctx.accumulatedContent = "";
144
+ dispatch({ type: "STREAM_CONTENT_RESET" });
145
+ break;
146
+ case "done":
147
+ ctx.agentResponse = event.response;
148
+ ctx.capturedConversationId = event.conversation_id ?? null;
149
+ break;
150
+ case "error":
151
+ ctx.hadStreamError = true;
152
+ dispatch({ type: "SEND_ERROR", error: event.error });
153
+ break;
154
+ }
155
+ };
156
+ const { streamAdapter } = configRef.current;
157
+ if (streamAdapter) {
158
+ await streamAdapter(
159
+ url,
160
+ { method: "POST", headers: mergedHeaders, body, signal: controller.signal },
161
+ handleEvent
162
+ );
163
+ clearTimeout(timeoutId);
164
+ } else {
165
+ const response = await fetch(url, {
166
+ method: "POST",
167
+ headers: mergedHeaders,
168
+ body,
169
+ signal: controller.signal
120
170
  });
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;
171
+ clearTimeout(timeoutId);
172
+ if (!response.ok) {
173
+ dispatch({
174
+ type: "SEND_ERROR",
175
+ error: {
176
+ code: "API_ERROR",
177
+ message: `HTTP ${response.status}: ${response.statusText}`,
178
+ retryable: response.status >= 500
179
+ }
180
+ });
181
+ return;
182
+ }
183
+ const reader = response.body?.getReader();
184
+ if (!reader) {
185
+ dispatch({
186
+ type: "SEND_ERROR",
187
+ error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
188
+ });
189
+ return;
190
+ }
191
+ const decoder = new TextDecoder();
192
+ let buffer = "";
193
+ while (true) {
194
+ const { done, value } = await reader.read();
195
+ if (done) break;
196
+ buffer += decoder.decode(value, { stream: true });
197
+ const lines = buffer.split("\n");
198
+ buffer = lines.pop() ?? "";
199
+ for (const line of lines) {
200
+ if (!line.startsWith("data: ")) continue;
201
+ const data = line.slice(6).trim();
202
+ if (data === "[DONE]") continue;
203
+ try {
204
+ const event = JSON.parse(data);
205
+ handleEvent(event);
206
+ } catch {
159
207
  }
160
- } catch {
161
208
  }
162
209
  }
163
210
  }
211
+ if (ctx.hadStreamError) return;
164
212
  const assistantMessage = {
165
213
  id: generateMessageId(),
166
214
  role: "assistant",
167
- content: agentResponse?.message ?? accumulatedContent,
168
- response: agentResponse ?? void 0,
169
- agent: capturedAgent ?? void 0,
215
+ content: ctx.agentResponse?.message ?? ctx.accumulatedContent,
216
+ response: ctx.agentResponse ?? void 0,
217
+ agent: ctx.capturedAgent ?? void 0,
170
218
  timestamp: /* @__PURE__ */ new Date()
171
219
  };
172
220
  dispatch({
173
221
  type: "SEND_SUCCESS",
174
222
  message: assistantMessage,
175
- streamingContent: accumulatedContent,
176
- conversationId: capturedConversationId
223
+ streamingContent: ctx.accumulatedContent,
224
+ conversationId: ctx.capturedConversationId
177
225
  });
178
226
  } catch (err) {
179
227
  clearTimeout(timeoutId);
180
228
  if (err.name === "AbortError") {
181
- dispatch({
182
- type: "SEND_ERROR",
183
- error: { code: "TIMEOUT", message: "Request timed out", retryable: true }
184
- });
229
+ if (ctx.accumulatedContent) {
230
+ const partialMessage = {
231
+ id: generateMessageId(),
232
+ role: "assistant",
233
+ content: ctx.accumulatedContent,
234
+ agent: ctx.capturedAgent ?? void 0,
235
+ timestamp: /* @__PURE__ */ new Date()
236
+ };
237
+ dispatch({
238
+ type: "SEND_SUCCESS",
239
+ message: partialMessage,
240
+ streamingContent: ctx.accumulatedContent,
241
+ conversationId: ctx.capturedConversationId
242
+ });
243
+ } else {
244
+ dispatch({
245
+ type: "SEND_ERROR",
246
+ error: { code: "ABORTED", message: "Request stopped", retryable: true }
247
+ });
248
+ }
185
249
  } else {
186
250
  dispatch({
187
251
  type: "SEND_ERROR",
@@ -192,6 +256,8 @@ function useAgentChat(config2) {
192
256
  }
193
257
  });
194
258
  }
259
+ } finally {
260
+ abortControllerRef.current = null;
195
261
  }
196
262
  },
197
263
  [state.conversationId]
@@ -204,7 +270,8 @@ function useAgentChat(config2) {
204
270
  }, []);
205
271
  const submitFeedback = useCallback(
206
272
  async (messageId, rating, comment) => {
207
- const { apiUrl, feedbackPath = "/feedback", headers = {} } = configRef.current;
273
+ const { apiUrl, feedbackPath = "/feedback", headers: headersOrFn } = configRef.current;
274
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
208
275
  await fetch(`${apiUrl}${feedbackPath}`, {
209
276
  method: "POST",
210
277
  headers: { "Content-Type": "application/json", ...headers },
@@ -215,12 +282,16 @@ function useAgentChat(config2) {
215
282
  );
216
283
  const retry = useCallback(async () => {
217
284
  if (lastUserMessageRef.current) {
218
- await sendMessage(lastUserMessageRef.current);
285
+ await sendMessage(lastUserMessageRef.current, lastUserAttachmentsRef.current);
219
286
  }
220
287
  }, [sendMessage]);
288
+ const stop = useCallback(() => {
289
+ abortControllerRef.current?.abort();
290
+ }, []);
221
291
  const reset = useCallback(() => {
222
292
  dispatch({ type: "RESET" });
223
293
  lastUserMessageRef.current = null;
294
+ lastUserAttachmentsRef.current = void 0;
224
295
  }, []);
225
296
  const actions = {
226
297
  sendMessage,
@@ -228,6 +299,7 @@ function useAgentChat(config2) {
228
299
  loadConversation,
229
300
  submitFeedback,
230
301
  retry,
302
+ stop,
231
303
  reset
232
304
  };
233
305
  return { state, actions };
@@ -235,7 +307,7 @@ function useAgentChat(config2) {
235
307
 
236
308
  // src/chat/MessageThread/MessageThread.tsx
237
309
  import { twMerge as twMerge5 } from "tailwind-merge";
238
- import { useEffect, useRef as useRef2 } from "react";
310
+ import { useCallback as useCallback2, useEffect, useRef as useRef2 } from "react";
239
311
 
240
312
  // src/chat/MessageBubble/MessageBubble.tsx
241
313
  import { twMerge as twMerge4 } from "tailwind-merge";
@@ -244,8 +316,10 @@ import { twMerge as twMerge4 } from "tailwind-merge";
244
316
  import { Badge as Badge2 } from "@surf-kit/core";
245
317
 
246
318
  // src/response/ResponseMessage/ResponseMessage.tsx
319
+ import React from "react";
247
320
  import ReactMarkdown from "react-markdown";
248
321
  import rehypeSanitize from "rehype-sanitize";
322
+ import remarkGfm from "remark-gfm";
249
323
  import { twMerge } from "tailwind-merge";
250
324
  import { jsx } from "react/jsx-runtime";
251
325
  function normalizeMarkdownLists(content) {
@@ -268,7 +342,12 @@ function ResponseMessage({ content, className }) {
268
342
  "[&_h3]:text-sm [&_h3]:font-semibold [&_h3]:text-accent [&_h3]:mt-2 [&_h3]:mb-1",
269
343
  "[&_code]:bg-surface-raised [&_code]:text-accent [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_code]:font-mono",
270
344
  "[&_pre]:bg-surface-raised [&_pre]:border [&_pre]:border-border [&_pre]:rounded-xl [&_pre]:p-4 [&_pre]:overflow-x-auto",
345
+ "[&_hr]:my-3 [&_hr]:border-border",
271
346
  "[&_blockquote]:border-l-2 [&_blockquote]:border-border-strong [&_blockquote]:pl-4 [&_blockquote]:text-text-secondary",
347
+ "[&_table]:w-full [&_table]:text-sm [&_table]:border-collapse [&_table]:my-2",
348
+ "[&_thead]:border-b [&_thead]:border-border",
349
+ "[&_th]:text-left [&_th]:px-2 [&_th]:py-1.5 [&_th]:font-semibold",
350
+ "[&_td]:px-2 [&_td]:py-1.5 [&_td]:border-t [&_td]:border-border/50",
272
351
  "[&_a]:text-accent [&_a]:underline-offset-2 [&_a]:hover:text-accent/80",
273
352
  className
274
353
  ),
@@ -276,6 +355,7 @@ function ResponseMessage({ content, className }) {
276
355
  children: /* @__PURE__ */ jsx(
277
356
  ReactMarkdown,
278
357
  {
358
+ remarkPlugins: [remarkGfm],
279
359
  rehypePlugins: [rehypeSanitize],
280
360
  components: {
281
361
  script: () => null,
@@ -283,12 +363,29 @@ function ResponseMessage({ content, className }) {
283
363
  p: ({ children }) => /* @__PURE__ */ jsx("p", { className: "my-2", children }),
284
364
  ul: ({ children }) => /* @__PURE__ */ jsx("ul", { className: "my-2 list-disc pl-6", children }),
285
365
  ol: ({ children }) => /* @__PURE__ */ jsx("ol", { className: "my-2 list-decimal pl-6", children }),
286
- li: ({ children }) => /* @__PURE__ */ jsx("li", { className: "my-1", children }),
366
+ li: ({ children, ...props }) => {
367
+ let content2 = children;
368
+ if (props.ordered) {
369
+ content2 = React.Children.map(children, (child, i) => {
370
+ if (i === 0 && typeof child === "string") {
371
+ return child.replace(/^\d+[.)]\s*/, "");
372
+ }
373
+ return child;
374
+ });
375
+ }
376
+ return /* @__PURE__ */ jsx("li", { className: "my-1", children: content2 });
377
+ },
287
378
  strong: ({ children }) => /* @__PURE__ */ jsx("strong", { className: "font-semibold", children }),
379
+ em: ({ children }) => /* @__PURE__ */ jsx("em", { className: "italic text-text-secondary", children }),
288
380
  h1: ({ children }) => /* @__PURE__ */ jsx("h1", { className: "text-base font-bold mt-4 mb-2", children }),
289
381
  h2: ({ children }) => /* @__PURE__ */ jsx("h2", { className: "text-sm font-bold mt-3 mb-1", children }),
290
382
  h3: ({ children }) => /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold mt-2 mb-1", children }),
291
- code: ({ children }) => /* @__PURE__ */ jsx("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children })
383
+ hr: () => /* @__PURE__ */ jsx("hr", { className: "my-3 border-border" }),
384
+ code: ({ children }) => /* @__PURE__ */ jsx("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children }),
385
+ table: ({ children }) => /* @__PURE__ */ jsx("div", { className: "overflow-x-auto my-2", children: /* @__PURE__ */ jsx("table", { className: "w-full text-sm border-collapse", children }) }),
386
+ thead: ({ children }) => /* @__PURE__ */ jsx("thead", { className: "border-b border-border", children }),
387
+ th: ({ children }) => /* @__PURE__ */ jsx("th", { className: "text-left px-2 py-1.5 font-semibold", children }),
388
+ td: ({ children }) => /* @__PURE__ */ jsx("td", { className: "px-2 py-1.5 border-t border-border/50", children })
292
389
  },
293
390
  children: normalizeMarkdownLists(content)
294
391
  }
@@ -298,7 +395,9 @@ function ResponseMessage({ content, className }) {
298
395
  }
299
396
 
300
397
  // src/response/StructuredResponse/StructuredResponse.tsx
301
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
398
+ import ReactMarkdown2 from "react-markdown";
399
+ import rehypeSanitize2 from "rehype-sanitize";
400
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
302
401
  function tryParse(value) {
303
402
  if (value === void 0 || value === null) return null;
304
403
  if (typeof value === "string") {
@@ -310,6 +409,25 @@ function tryParse(value) {
310
409
  }
311
410
  return value;
312
411
  }
412
+ function InlineMarkdown({ text }) {
413
+ return /* @__PURE__ */ jsx2(
414
+ ReactMarkdown2,
415
+ {
416
+ rehypePlugins: [rehypeSanitize2],
417
+ components: {
418
+ // Unwrap block-level <p> so content stays inline within its parent
419
+ p: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children }),
420
+ strong: ({ children }) => /* @__PURE__ */ jsx2("strong", { className: "font-semibold", children }),
421
+ em: ({ children }) => /* @__PURE__ */ jsx2("em", { className: "italic", children }),
422
+ code: ({ children }) => /* @__PURE__ */ jsx2("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children }),
423
+ // Prevent block elements that would break layout
424
+ script: () => null,
425
+ iframe: () => null
426
+ },
427
+ children: text
428
+ }
429
+ );
430
+ }
313
431
  function renderSteps(data) {
314
432
  const steps = tryParse(data.steps);
315
433
  if (!steps || !Array.isArray(steps)) return null;
@@ -322,7 +440,7 @@ function renderSteps(data) {
322
440
  children: i + 1
323
441
  }
324
442
  ),
325
- /* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: step })
443
+ /* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: /* @__PURE__ */ jsx2(InlineMarkdown, { text: step }) })
326
444
  ] }, i)) });
327
445
  }
328
446
  function renderTable(data) {
@@ -388,7 +506,7 @@ function renderList(data) {
388
506
  title && /* @__PURE__ */ jsx2("p", { className: "text-xs font-semibold uppercase tracking-wider text-text-secondary mb-1", children: title }),
389
507
  /* @__PURE__ */ jsx2("ul", { className: "flex flex-col gap-1.5", children: items.map((item, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2.5", children: [
390
508
  /* @__PURE__ */ jsx2("span", { className: "mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-accent", "aria-hidden": "true" }),
391
- /* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: item })
509
+ /* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: /* @__PURE__ */ jsx2(InlineMarkdown, { text: item }) })
392
510
  ] }, i)) })
393
511
  ] });
394
512
  }
@@ -423,7 +541,14 @@ function renderWarning(data) {
423
541
  }
424
542
  );
425
543
  }
426
- function StructuredResponse({ uiHint, data, className }) {
544
+ function StructuredResponse({ uiHint, data: rawData, className }) {
545
+ const data = typeof rawData === "string" ? (() => {
546
+ try {
547
+ return JSON.parse(rawData);
548
+ } catch {
549
+ return null;
550
+ }
551
+ })() : rawData;
427
552
  if (!data) return null;
428
553
  let content;
429
554
  switch (uiHint) {
@@ -503,7 +628,36 @@ function SourceCard({ source, variant = "compact", onNavigate, className }) {
503
628
  children: [
504
629
  /* @__PURE__ */ jsxs2("div", { className: "flex items-start justify-between gap-2", children: [
505
630
  /* @__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 }),
631
+ source.url ? /* @__PURE__ */ jsxs2(
632
+ "a",
633
+ {
634
+ href: source.url,
635
+ target: "_blank",
636
+ rel: "noopener noreferrer",
637
+ className: "text-sm font-medium text-accent hover:underline truncate block",
638
+ onClick: (e) => e.stopPropagation(),
639
+ children: [
640
+ source.title,
641
+ /* @__PURE__ */ jsxs2(
642
+ "svg",
643
+ {
644
+ className: "inline-block ml-1 w-3 h-3 opacity-60",
645
+ viewBox: "0 0 24 24",
646
+ fill: "none",
647
+ stroke: "currentColor",
648
+ strokeWidth: "2",
649
+ strokeLinecap: "round",
650
+ strokeLinejoin: "round",
651
+ children: [
652
+ /* @__PURE__ */ jsx3("path", { d: "M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" }),
653
+ /* @__PURE__ */ jsx3("polyline", { points: "15 3 21 3 21 9" }),
654
+ /* @__PURE__ */ jsx3("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
655
+ ]
656
+ }
657
+ )
658
+ ]
659
+ }
660
+ ) : /* @__PURE__ */ jsx3("p", { className: "text-sm font-medium text-text-primary truncate", children: source.title }),
507
661
  source.section && /* @__PURE__ */ jsx3("p", { className: "text-[11px] font-semibold uppercase tracking-wider text-text-secondary truncate mt-0.5", children: source.section })
508
662
  ] }),
509
663
  /* @__PURE__ */ jsx3(
@@ -637,13 +791,16 @@ function AgentResponse({
637
791
  }) {
638
792
  return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-4 ${className ?? ""}`, "data-testid": "agent-response", children: [
639
793
  /* @__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
- ),
794
+ response.ui_hint !== "text" && response.structured_data && (() => {
795
+ const parsed = typeof response.structured_data === "string" ? (() => {
796
+ try {
797
+ return JSON.parse(response.structured_data);
798
+ } catch {
799
+ return null;
800
+ }
801
+ })() : response.structured_data;
802
+ return parsed ? /* @__PURE__ */ jsx6(StructuredResponse, { uiHint: response.ui_hint, data: parsed }) : null;
803
+ })(),
647
804
  (showConfidence || showVerification) && /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2 mt-1", "data-testid": "response-meta", children: [
648
805
  showConfidence && /* @__PURE__ */ jsxs4(
649
806
  Badge2,
@@ -694,6 +851,31 @@ function AgentResponse({
694
851
 
695
852
  // src/chat/MessageBubble/MessageBubble.tsx
696
853
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
854
+ function DocumentIcon() {
855
+ 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: [
856
+ /* @__PURE__ */ jsx7("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
857
+ /* @__PURE__ */ jsx7("polyline", { points: "14 2 14 8 20 8" }),
858
+ /* @__PURE__ */ jsx7("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
859
+ /* @__PURE__ */ jsx7("line", { x1: "16", y1: "17", x2: "8", y2: "17" })
860
+ ] });
861
+ }
862
+ function AttachmentThumbnail({ attachment }) {
863
+ const isImage = attachment.content_type.startsWith("image/");
864
+ if (isImage) {
865
+ return /* @__PURE__ */ jsx7("div", { className: "rounded-lg overflow-hidden border border-black/10 max-w-[240px]", children: /* @__PURE__ */ jsx7(
866
+ "img",
867
+ {
868
+ src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
869
+ alt: attachment.filename,
870
+ className: "max-w-full max-h-[200px] object-contain"
871
+ }
872
+ ) });
873
+ }
874
+ return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 px-3 py-2 rounded-lg border border-black/10 bg-black/5", children: [
875
+ /* @__PURE__ */ jsx7(DocumentIcon, {}),
876
+ /* @__PURE__ */ jsx7("span", { className: "text-xs truncate max-w-[160px]", children: attachment.filename })
877
+ ] });
878
+ }
697
879
  function MessageBubble({
698
880
  message,
699
881
  showAgent,
@@ -701,23 +883,29 @@ function MessageBubble({
701
883
  showConfidence = true,
702
884
  showVerification = true,
703
885
  animated = true,
886
+ userBubbleClassName,
704
887
  className
705
888
  }) {
706
889
  const isUser = message.role === "user";
890
+ const hasAttachments = message.attachments && message.attachments.length > 0;
707
891
  if (isUser) {
708
892
  return /* @__PURE__ */ jsx7(
709
893
  "div",
710
894
  {
711
895
  "data-message-id": message.id,
712
896
  className: twMerge4("flex w-full justify-end", className),
713
- children: /* @__PURE__ */ jsx7(
897
+ children: /* @__PURE__ */ jsxs5(
714
898
  "div",
715
899
  {
716
900
  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"
901
+ "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",
902
+ animated && "motion-safe:animate-slideFromRight",
903
+ userBubbleClassName
719
904
  ),
720
- children: message.content
905
+ children: [
906
+ 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}`)) }),
907
+ message.content
908
+ ]
721
909
  }
722
910
  )
723
911
  }
@@ -729,7 +917,7 @@ function MessageBubble({
729
917
  "data-message-id": message.id,
730
918
  className: twMerge4("flex w-full flex-col items-start gap-1.5", className),
731
919
  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("_", " ") }),
920
+ 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
921
  /* @__PURE__ */ jsx7(
734
922
  "div",
735
923
  {
@@ -755,34 +943,97 @@ function MessageBubble({
755
943
 
756
944
  // src/chat/MessageThread/MessageThread.tsx
757
945
  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);
946
+ function MessageThread({ messages, streamingSlot, showAgent, showSources, showConfidence, showVerification, hideLastAssistant, userBubbleClassName, className }) {
947
+ const scrollRef = useRef2(null);
948
+ const shouldAutoScroll = useRef2(true);
949
+ const hasStreaming = !!streamingSlot;
950
+ const scrollToBottom = useCallback2(() => {
951
+ const el = scrollRef.current;
952
+ if (el && shouldAutoScroll.current) {
953
+ el.scrollTop = el.scrollHeight;
954
+ }
955
+ }, []);
760
956
  useEffect(() => {
761
- bottomRef.current?.scrollIntoView?.({ behavior: "smooth" });
762
- }, [messages.length, streamingSlot]);
957
+ const el = scrollRef.current;
958
+ if (!el) return;
959
+ const onWheel = (e) => {
960
+ if (e.deltaY < 0) {
961
+ shouldAutoScroll.current = false;
962
+ }
963
+ };
964
+ const onPointerDown = () => {
965
+ el.dataset.userPointer = "1";
966
+ };
967
+ const onPointerUp = () => {
968
+ delete el.dataset.userPointer;
969
+ };
970
+ el.addEventListener("wheel", onWheel, { passive: true });
971
+ el.addEventListener("pointerdown", onPointerDown);
972
+ window.addEventListener("pointerup", onPointerUp);
973
+ return () => {
974
+ el.removeEventListener("wheel", onWheel);
975
+ el.removeEventListener("pointerdown", onPointerDown);
976
+ window.removeEventListener("pointerup", onPointerUp);
977
+ };
978
+ }, []);
979
+ const handleScroll = useCallback2(() => {
980
+ const el = scrollRef.current;
981
+ if (!el) return;
982
+ const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
983
+ if (nearBottom) {
984
+ shouldAutoScroll.current = true;
985
+ } else if (el.dataset.userPointer) {
986
+ shouldAutoScroll.current = false;
987
+ }
988
+ }, []);
989
+ useEffect(scrollToBottom, [messages.length, scrollToBottom]);
990
+ useEffect(() => {
991
+ if (!hasStreaming) return;
992
+ let raf;
993
+ const tick = () => {
994
+ scrollToBottom();
995
+ raf = requestAnimationFrame(tick);
996
+ };
997
+ raf = requestAnimationFrame(tick);
998
+ return () => cancelAnimationFrame(raf);
999
+ }, [hasStreaming, scrollToBottom]);
1000
+ useEffect(() => {
1001
+ if (!hasStreaming) {
1002
+ shouldAutoScroll.current = true;
1003
+ }
1004
+ }, [hasStreaming]);
763
1005
  return /* @__PURE__ */ jsxs6(
764
1006
  "div",
765
1007
  {
1008
+ ref: scrollRef,
766
1009
  role: "log",
767
1010
  "aria-live": "polite",
768
1011
  "aria-label": "Message thread",
1012
+ onScroll: handleScroll,
769
1013
  className: twMerge5(
770
1014
  "flex flex-col gap-4 overflow-y-auto flex-1 px-4 py-6",
771
1015
  className
772
1016
  ),
773
1017
  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 })
1018
+ /* @__PURE__ */ jsx8("div", { className: "flex-1 shrink-0" }),
1019
+ messages.map((message, i) => {
1020
+ if (hideLastAssistant && i === messages.length - 1 && message.role === "assistant") {
1021
+ return null;
1022
+ }
1023
+ return /* @__PURE__ */ jsx8(
1024
+ MessageBubble,
1025
+ {
1026
+ message,
1027
+ showAgent,
1028
+ showSources,
1029
+ showConfidence,
1030
+ showVerification,
1031
+ userBubbleClassName
1032
+ },
1033
+ message.id
1034
+ );
1035
+ }),
1036
+ streamingSlot
786
1037
  ]
787
1038
  }
788
1039
  );
@@ -790,32 +1041,127 @@ function MessageThread({ messages, streamingSlot, showSources, showConfidence, s
790
1041
 
791
1042
  // src/chat/MessageComposer/MessageComposer.tsx
792
1043
  import { twMerge as twMerge6 } from "tailwind-merge";
793
- import { useState as useState2, useRef as useRef3, useCallback as useCallback2 } from "react";
1044
+ import { useState as useState2, useRef as useRef3, useCallback as useCallback3 } from "react";
794
1045
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1046
+ var ALLOWED_TYPES = /* @__PURE__ */ new Set([
1047
+ "image/png",
1048
+ "image/jpeg",
1049
+ "image/gif",
1050
+ "image/webp",
1051
+ "application/pdf"
1052
+ ]);
1053
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1054
+ var MAX_ATTACHMENTS = 5;
1055
+ function ArrowUpIcon() {
1056
+ 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: [
1057
+ /* @__PURE__ */ jsx9("path", { d: "M10 16V4" }),
1058
+ /* @__PURE__ */ jsx9("path", { d: "M4 10l6-6 6 6" })
1059
+ ] });
1060
+ }
1061
+ function StopIcon() {
1062
+ 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" }) });
1063
+ }
1064
+ function PaperclipIcon() {
1065
+ 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" }) });
1066
+ }
1067
+ function XIcon({ size = 14 }) {
1068
+ 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: [
1069
+ /* @__PURE__ */ jsx9("path", { d: "M18 6L6 18" }),
1070
+ /* @__PURE__ */ jsx9("path", { d: "M6 6l12 12" })
1071
+ ] });
1072
+ }
1073
+ function DocumentIcon2() {
1074
+ 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: [
1075
+ /* @__PURE__ */ jsx9("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
1076
+ /* @__PURE__ */ jsx9("polyline", { points: "14 2 14 8 20 8" }),
1077
+ /* @__PURE__ */ jsx9("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
1078
+ /* @__PURE__ */ jsx9("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
1079
+ /* @__PURE__ */ jsx9("polyline", { points: "10 9 9 9 8 9" })
1080
+ ] });
1081
+ }
1082
+ function fileToBase64(file) {
1083
+ return new Promise((resolve, reject) => {
1084
+ const reader = new FileReader();
1085
+ reader.onload = () => {
1086
+ const result = reader.result;
1087
+ const base64 = result.split(",")[1];
1088
+ resolve(base64);
1089
+ };
1090
+ reader.onerror = reject;
1091
+ reader.readAsDataURL(file);
1092
+ });
1093
+ }
1094
+ function AttachmentPreview({
1095
+ attachment,
1096
+ onRemove
1097
+ }) {
1098
+ const isImage = attachment.content_type.startsWith("image/");
1099
+ return /* @__PURE__ */ jsxs7("div", { className: "relative group flex-shrink-0", children: [
1100
+ isImage ? /* @__PURE__ */ jsx9("div", { className: "w-16 h-16 rounded-lg overflow-hidden border border-border/60 bg-surface-alt", children: /* @__PURE__ */ jsx9(
1101
+ "img",
1102
+ {
1103
+ src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
1104
+ alt: attachment.filename,
1105
+ className: "w-full h-full object-cover"
1106
+ }
1107
+ ) }) : /* @__PURE__ */ jsxs7("div", { className: "h-16 px-3 rounded-lg border border-border/60 bg-surface-alt flex items-center gap-2", children: [
1108
+ /* @__PURE__ */ jsx9("div", { className: "text-text-muted", children: /* @__PURE__ */ jsx9(DocumentIcon2, {}) }),
1109
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col min-w-0", children: [
1110
+ /* @__PURE__ */ jsx9("span", { className: "text-xs text-text-primary truncate max-w-[120px]", children: attachment.filename }),
1111
+ /* @__PURE__ */ jsx9("span", { className: "text-[10px] text-text-muted", children: "PDF" })
1112
+ ] })
1113
+ ] }),
1114
+ /* @__PURE__ */ jsx9(
1115
+ "button",
1116
+ {
1117
+ type: "button",
1118
+ onClick: onRemove,
1119
+ className: twMerge6(
1120
+ "absolute -top-1.5 -right-1.5",
1121
+ "w-5 h-5 rounded-full",
1122
+ "bg-text-muted/80 text-white",
1123
+ "flex items-center justify-center",
1124
+ "opacity-0 group-hover:opacity-100",
1125
+ "transition-opacity duration-150",
1126
+ "hover:bg-text-primary"
1127
+ ),
1128
+ "aria-label": `Remove ${attachment.filename}`,
1129
+ children: /* @__PURE__ */ jsx9(XIcon, { size: 10 })
1130
+ }
1131
+ )
1132
+ ] });
1133
+ }
795
1134
  function MessageComposer({
796
1135
  onSend,
1136
+ onStop,
797
1137
  isLoading = false,
798
1138
  placeholder = "Type a message...",
799
1139
  className
800
1140
  }) {
801
1141
  const [value, setValue] = useState2("");
1142
+ const [attachments, setAttachments] = useState2([]);
1143
+ const [dragOver, setDragOver] = useState2(false);
802
1144
  const textareaRef = useRef3(null);
803
- const canSend = value.trim().length > 0 && !isLoading;
804
- const resetHeight = useCallback2(() => {
1145
+ const fileInputRef = useRef3(null);
1146
+ const canSend = (value.trim().length > 0 || attachments.length > 0) && !isLoading;
1147
+ const resetHeight = useCallback3(() => {
805
1148
  const el = textareaRef.current;
806
1149
  if (el) {
807
1150
  el.style.height = "auto";
808
1151
  el.style.overflowY = "hidden";
809
1152
  }
810
1153
  }, []);
811
- const handleSend = useCallback2(() => {
1154
+ const handleSend = useCallback3(() => {
812
1155
  if (!canSend) return;
813
- onSend(value.trim());
1156
+ const message = value.trim() || (attachments.length > 0 ? "Please analyse the attached file(s)." : "");
1157
+ if (!message && attachments.length === 0) return;
1158
+ onSend(message, attachments.length > 0 ? attachments : void 0);
814
1159
  setValue("");
1160
+ setAttachments([]);
815
1161
  resetHeight();
816
1162
  textareaRef.current?.focus();
817
- }, [canSend, onSend, value, resetHeight]);
818
- const handleKeyDown = useCallback2(
1163
+ }, [canSend, onSend, value, attachments, resetHeight]);
1164
+ const handleKeyDown = useCallback3(
819
1165
  (e) => {
820
1166
  if (e.key === "Enter" && !e.shiftKey) {
821
1167
  e.preventDefault();
@@ -824,64 +1170,194 @@ function MessageComposer({
824
1170
  },
825
1171
  [handleSend]
826
1172
  );
827
- const handleChange = useCallback2(
1173
+ const handleChange = useCallback3(
828
1174
  (e) => {
829
1175
  setValue(e.target.value);
830
1176
  const el = e.target;
831
1177
  el.style.height = "auto";
832
- const capped = Math.min(el.scrollHeight, 128);
1178
+ const capped = Math.min(el.scrollHeight, 200);
833
1179
  el.style.height = `${capped}px`;
834
- el.style.overflowY = el.scrollHeight > 128 ? "auto" : "hidden";
1180
+ el.style.overflowY = el.scrollHeight > 200 ? "auto" : "hidden";
835
1181
  },
836
1182
  []
837
1183
  );
1184
+ const addFiles = useCallback3(async (files) => {
1185
+ const fileArray = Array.from(files);
1186
+ for (const file of fileArray) {
1187
+ if (attachments.length >= MAX_ATTACHMENTS) break;
1188
+ if (!ALLOWED_TYPES.has(file.type)) continue;
1189
+ if (file.size > MAX_FILE_SIZE) continue;
1190
+ try {
1191
+ const data = await fileToBase64(file);
1192
+ const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0;
1193
+ const attachment = {
1194
+ filename: file.name,
1195
+ content_type: file.type,
1196
+ data,
1197
+ preview_url: previewUrl
1198
+ };
1199
+ setAttachments((prev) => {
1200
+ if (prev.length >= MAX_ATTACHMENTS) return prev;
1201
+ return [...prev, attachment];
1202
+ });
1203
+ } catch {
1204
+ }
1205
+ }
1206
+ }, [attachments.length]);
1207
+ const handleFileSelect = useCallback3(() => {
1208
+ fileInputRef.current?.click();
1209
+ }, []);
1210
+ const handleFileInputChange = useCallback3(
1211
+ (e) => {
1212
+ if (e.target.files) {
1213
+ void addFiles(e.target.files);
1214
+ e.target.value = "";
1215
+ }
1216
+ },
1217
+ [addFiles]
1218
+ );
1219
+ const removeAttachment = useCallback3((index) => {
1220
+ setAttachments((prev) => {
1221
+ const removed = prev[index];
1222
+ if (removed?.preview_url) URL.revokeObjectURL(removed.preview_url);
1223
+ return prev.filter((_, i) => i !== index);
1224
+ });
1225
+ }, []);
1226
+ const handlePaste = useCallback3(
1227
+ (e) => {
1228
+ const items = e.clipboardData.items;
1229
+ const files = [];
1230
+ for (const item of items) {
1231
+ if (item.kind === "file" && ALLOWED_TYPES.has(item.type)) {
1232
+ const file = item.getAsFile();
1233
+ if (file) files.push(file);
1234
+ }
1235
+ }
1236
+ if (files.length > 0) {
1237
+ void addFiles(files);
1238
+ }
1239
+ },
1240
+ [addFiles]
1241
+ );
1242
+ const handleDragOver = useCallback3((e) => {
1243
+ e.preventDefault();
1244
+ e.stopPropagation();
1245
+ setDragOver(true);
1246
+ }, []);
1247
+ const handleDragLeave = useCallback3((e) => {
1248
+ e.preventDefault();
1249
+ e.stopPropagation();
1250
+ setDragOver(false);
1251
+ }, []);
1252
+ const handleDrop = useCallback3(
1253
+ (e) => {
1254
+ e.preventDefault();
1255
+ e.stopPropagation();
1256
+ setDragOver(false);
1257
+ if (e.dataTransfer.files.length > 0) {
1258
+ void addFiles(e.dataTransfer.files);
1259
+ }
1260
+ },
1261
+ [addFiles]
1262
+ );
838
1263
  return /* @__PURE__ */ jsxs7(
839
1264
  "div",
840
1265
  {
841
1266
  className: twMerge6(
842
- "flex items-end gap-3 shrink-0 border-t border-border px-4 py-3",
1267
+ "relative shrink-0 rounded-3xl border bg-surface",
1268
+ "shadow-lg shadow-black/10",
1269
+ "transition-all duration-200",
1270
+ "focus-within:border-accent/40 focus-within:shadow-accent/5",
1271
+ dragOver ? "border-accent/60 bg-accent/5" : "border-border/60",
843
1272
  className
844
1273
  ),
1274
+ onDragOver: handleDragOver,
1275
+ onDragLeave: handleDragLeave,
1276
+ onDrop: handleDrop,
845
1277
  children: [
846
1278
  /* @__PURE__ */ jsx9(
847
- "textarea",
1279
+ "input",
848
1280
  {
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"
1281
+ ref: fileInputRef,
1282
+ type: "file",
1283
+ multiple: true,
1284
+ accept: "image/png,image/jpeg,image/gif,image/webp,application/pdf",
1285
+ onChange: handleFileInputChange,
1286
+ className: "hidden",
1287
+ "aria-hidden": "true"
866
1288
  }
867
1289
  ),
868
- /* @__PURE__ */ jsx9(
869
- "button",
1290
+ 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(
1291
+ AttachmentPreview,
870
1292
  {
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
- )
1293
+ attachment: att,
1294
+ onRemove: () => removeAttachment(i)
1295
+ },
1296
+ `${att.filename}-${i}`
1297
+ )) }),
1298
+ 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" }) }),
1299
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-end", children: [
1300
+ /* @__PURE__ */ jsx9(
1301
+ "button",
1302
+ {
1303
+ type: "button",
1304
+ onClick: handleFileSelect,
1305
+ disabled: isLoading || attachments.length >= MAX_ATTACHMENTS,
1306
+ "aria-label": "Attach file",
1307
+ className: twMerge6(
1308
+ "flex-shrink-0 ml-2 mb-3",
1309
+ "inline-flex items-center justify-center",
1310
+ "w-9 h-9 rounded-full",
1311
+ "transition-all duration-200",
1312
+ "text-text-muted/60 hover:text-text-secondary hover:bg-text-muted/10",
1313
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
1314
+ "disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-transparent"
1315
+ ),
1316
+ children: /* @__PURE__ */ jsx9(PaperclipIcon, {})
1317
+ }
1318
+ ),
1319
+ /* @__PURE__ */ jsx9(
1320
+ "textarea",
1321
+ {
1322
+ ref: textareaRef,
1323
+ value,
1324
+ onChange: handleChange,
1325
+ onKeyDown: handleKeyDown,
1326
+ onPaste: handlePaste,
1327
+ placeholder,
1328
+ rows: 1,
1329
+ disabled: isLoading,
1330
+ className: twMerge6(
1331
+ "flex-1 resize-none bg-transparent",
1332
+ "pl-2 pr-14 pt-4 pb-4 text-[15px] leading-relaxed",
1333
+ "text-text-primary placeholder:text-text-muted/70",
1334
+ "focus:outline-none",
1335
+ "disabled:opacity-50 disabled:cursor-not-allowed",
1336
+ "overflow-hidden"
1337
+ ),
1338
+ style: { colorScheme: "dark" },
1339
+ "aria-label": "Message input"
1340
+ }
1341
+ ),
1342
+ /* @__PURE__ */ jsx9(
1343
+ "button",
1344
+ {
1345
+ type: "button",
1346
+ onClick: isLoading && onStop ? onStop : handleSend,
1347
+ disabled: !canSend && !isLoading,
1348
+ "aria-label": isLoading ? "Stop generating" : "Send message",
1349
+ className: twMerge6(
1350
+ "absolute bottom-3 right-3",
1351
+ "inline-flex items-center justify-center",
1352
+ "w-9 h-9 rounded-full",
1353
+ "transition-all duration-200",
1354
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
1355
+ 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 cursor-pointer" : "bg-transparent text-text-muted/40 cursor-default"
1356
+ ),
1357
+ children: isLoading ? /* @__PURE__ */ jsx9(StopIcon, {}) : /* @__PURE__ */ jsx9(ArrowUpIcon, {})
1358
+ }
1359
+ )
1360
+ ] })
885
1361
  ]
886
1362
  }
887
1363
  );
@@ -894,6 +1370,7 @@ function WelcomeScreen({
894
1370
  title = "Welcome",
895
1371
  message = "How can I help you today?",
896
1372
  icon,
1373
+ iconClassName,
897
1374
  suggestedQuestions = [],
898
1375
  onQuestionSelect,
899
1376
  className
@@ -906,12 +1383,15 @@ function WelcomeScreen({
906
1383
  className
907
1384
  ),
908
1385
  children: [
909
- /* @__PURE__ */ jsx10(
1386
+ icon ? iconClassName ? /* @__PURE__ */ jsx10("div", { className: iconClassName, "aria-hidden": "true", children: icon }) : icon : /* @__PURE__ */ jsx10(
910
1387
  "div",
911
1388
  {
912
- className: "w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
1389
+ className: twMerge7(
1390
+ "w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
1391
+ iconClassName
1392
+ ),
913
1393
  "aria-hidden": "true",
914
- children: icon ?? /* @__PURE__ */ jsx10("span", { className: "text-2xl", children: "\u2726" })
1394
+ children: /* @__PURE__ */ jsx10("span", { className: "text-2xl", children: "\u2726" })
915
1395
  }
916
1396
  ),
917
1397
  /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-2", children: [
@@ -921,7 +1401,7 @@ function WelcomeScreen({
921
1401
  suggestedQuestions.length > 0 && /* @__PURE__ */ jsx10(
922
1402
  "div",
923
1403
  {
924
- className: "flex flex-wrap justify-center gap-2 max-w-md",
1404
+ className: "flex flex-wrap justify-center gap-2 max-w-xl",
925
1405
  role: "group",
926
1406
  "aria-label": "Suggested questions",
927
1407
  children: suggestedQuestions.map((question) => /* @__PURE__ */ jsx10(
@@ -930,7 +1410,7 @@ function WelcomeScreen({
930
1410
  type: "button",
931
1411
  onClick: () => onQuestionSelect?.(question),
932
1412
  className: twMerge7(
933
- "px-4 py-2 rounded-full text-sm",
1413
+ "px-3.5 py-1.5 rounded-full text-[12px]",
934
1414
  "border border-border bg-transparent text-text-secondary",
935
1415
  "hover:bg-accent/10 hover:border-interactive hover:text-text-primary",
936
1416
  "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
@@ -949,7 +1429,8 @@ function WelcomeScreen({
949
1429
 
950
1430
  // src/streaming/StreamingMessage/StreamingMessage.tsx
951
1431
  import { useEffect as useEffect3, useRef as useRef5 } from "react";
952
- import { Spinner } from "@surf-kit/core";
1432
+ import { twMerge as twMerge8 } from "tailwind-merge";
1433
+ import { WaveLoader } from "@surf-kit/core";
953
1434
 
954
1435
  // src/hooks/useCharacterDrain.ts
955
1436
  import { useState as useState3, useRef as useRef4, useEffect as useEffect2 } from "react";
@@ -978,7 +1459,10 @@ function useCharacterDrain(target, msPerChar = 15) {
978
1459
  const elapsed = now - lastTimeRef.current;
979
1460
  const charsToAdvance = Math.floor(elapsed / msPerCharRef.current);
980
1461
  if (charsToAdvance > 0 && indexRef.current < currentTarget.length) {
981
- const nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
1462
+ let nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
1463
+ while (nextIndex < currentTarget.length && currentTarget[nextIndex - 1].trim() === "") {
1464
+ nextIndex++;
1465
+ }
982
1466
  indexRef.current = nextIndex;
983
1467
  lastTimeRef.current = now;
984
1468
  setDisplayed(currentTarget.slice(0, nextIndex));
@@ -1023,14 +1507,36 @@ var phaseLabels = {
1023
1507
  generating: "Writing...",
1024
1508
  verifying: "Verifying..."
1025
1509
  };
1510
+ var CURSOR_STYLES = `
1511
+ .sk-streaming-cursor > :not(ul,ol,blockquote,div:has(table)):last-child::after,
1512
+ .sk-streaming-cursor > :is(ul,ol):last-child > li:last-child::after,
1513
+ .sk-streaming-cursor > blockquote:last-child > p:last-child::after,
1514
+ .sk-streaming-cursor > div:has(table):last-child table tbody tr:last-child td:last-child::after {
1515
+ content: "";
1516
+ display: inline-block;
1517
+ width: 2px;
1518
+ height: 1em;
1519
+ background: var(--color-accent, #38bdf8);
1520
+ animation: sk-cursor-blink 0.8s steps(1) infinite;
1521
+ margin-left: 2px;
1522
+ vertical-align: text-bottom;
1523
+ }
1524
+ @keyframes sk-cursor-blink {
1525
+ 0%, 60% { opacity: 1; }
1526
+ 61%, 100% { opacity: 0; }
1527
+ }
1528
+ `;
1026
1529
  function StreamingMessage({
1027
1530
  stream,
1028
1531
  onComplete,
1532
+ onDraining,
1029
1533
  showPhases = true,
1030
1534
  className
1031
1535
  }) {
1032
1536
  const onCompleteRef = useRef5(onComplete);
1033
1537
  onCompleteRef.current = onComplete;
1538
+ const onDrainingRef = useRef5(onDraining);
1539
+ onDrainingRef.current = onDraining;
1034
1540
  const wasActiveRef = useRef5(stream.active);
1035
1541
  useEffect3(() => {
1036
1542
  if (wasActiveRef.current && !stream.active) {
@@ -1039,35 +1545,40 @@ function StreamingMessage({
1039
1545
  wasActiveRef.current = stream.active;
1040
1546
  }, [stream.active]);
1041
1547
  const phaseLabel = phaseLabels[stream.phase];
1042
- const { displayed: displayedContent } = useCharacterDrain(stream.content);
1043
- return /* @__PURE__ */ jsxs9("div", { className, "data-testid": "streaming-message", children: [
1548
+ const { displayed: rawDisplayed, isDraining } = useCharacterDrain(stream.content);
1549
+ const displayedContent = stream.active || isDraining ? rawDisplayed.trimEnd() : rawDisplayed;
1550
+ useEffect3(() => {
1551
+ onDrainingRef.current?.(isDraining);
1552
+ }, [isDraining]);
1553
+ const agentLabel = stream.agent ? stream.agent.replace("_agent", "").replace("_", " ") : null;
1554
+ const showPhaseIndicator = showPhases && stream.active && stream.phase !== "idle" && !displayedContent;
1555
+ const showCursor = (stream.active || isDraining) && !!displayedContent;
1556
+ return /* @__PURE__ */ jsxs9("div", { className: twMerge8("flex w-full flex-col items-start", className), "data-testid": "streaming-message", children: [
1044
1557
  /* @__PURE__ */ jsxs9("div", { "aria-live": "assertive", className: "sr-only", children: [
1045
1558
  stream.active && stream.phase !== "idle" && "Response started",
1046
1559
  !stream.active && stream.content && "Response complete"
1047
1560
  ] }),
1561
+ showCursor && /* @__PURE__ */ jsx11("style", { children: CURSOR_STYLES }),
1562
+ 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
1563
  /* @__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(
1564
+ showPhaseIndicator && /* @__PURE__ */ jsxs9(
1050
1565
  "div",
1051
1566
  {
1052
- className: "flex items-center gap-2 mb-2 text-sm text-text-secondary",
1567
+ className: "flex items-center gap-2 text-sm text-text-secondary",
1053
1568
  "data-testid": "phase-indicator",
1054
1569
  children: [
1055
- /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx11(Spinner, { size: "sm" }) }),
1570
+ /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx11(WaveLoader, { size: "sm", color: "#38bdf8" }) }),
1056
1571
  /* @__PURE__ */ jsx11("span", { children: phaseLabel })
1057
1572
  ]
1058
1573
  }
1059
1574
  ),
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
- ] })
1575
+ displayedContent && /* @__PURE__ */ jsx11(
1576
+ ResponseMessage,
1577
+ {
1578
+ content: displayedContent,
1579
+ className: showCursor ? "sk-streaming-cursor" : void 0
1580
+ }
1581
+ )
1071
1582
  ] })
1072
1583
  ] });
1073
1584
  }
@@ -1098,7 +1609,7 @@ function AgentChat({
1098
1609
  return /* @__PURE__ */ jsxs10(
1099
1610
  "div",
1100
1611
  {
1101
- className: twMerge8(
1612
+ className: twMerge9(
1102
1613
  "flex flex-col h-full bg-canvas border border-border rounded-xl overflow-hidden",
1103
1614
  className
1104
1615
  ),
@@ -1134,7 +1645,7 @@ function AgentChat({
1134
1645
  onQuestionSelect: handleQuestionSelect
1135
1646
  }
1136
1647
  ),
1137
- /* @__PURE__ */ jsx12(MessageComposer, { onSend: handleSend, isLoading: state.isLoading })
1648
+ /* @__PURE__ */ jsx12(MessageComposer, { onSend: handleSend, onStop: actions.stop, isLoading: state.isLoading })
1138
1649
  ]
1139
1650
  }
1140
1651
  );
@@ -1601,7 +2112,7 @@ function ThinkingIndicator({ label = "Thinking...", className }) {
1601
2112
  }
1602
2113
 
1603
2114
  // src/streaming/ToolExecution/ToolExecution.tsx
1604
- import { Spinner as Spinner2 } from "@surf-kit/core";
2115
+ import { WaveLoader as WaveLoader2 } from "@surf-kit/core";
1605
2116
  import { jsx as jsx26, jsxs as jsxs23 } from "react/jsx-runtime";
1606
2117
  var defaultLabels = {
1607
2118
  search: "Searching knowledge base...",
@@ -1617,7 +2128,7 @@ function ToolExecution({ tool, label, className }) {
1617
2128
  role: "status",
1618
2129
  "data-testid": "tool-execution",
1619
2130
  children: [
1620
- /* @__PURE__ */ jsx26("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx26(Spinner2, { size: "sm" }) }),
2131
+ /* @__PURE__ */ jsx26("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx26(WaveLoader2, { size: "sm", color: "#38bdf8" }) }),
1621
2132
  /* @__PURE__ */ jsx26("span", { children: displayLabel })
1622
2133
  ]
1623
2134
  }
@@ -1625,7 +2136,7 @@ function ToolExecution({ tool, label, className }) {
1625
2136
  }
1626
2137
 
1627
2138
  // src/streaming/RetrievalProgress/RetrievalProgress.tsx
1628
- import { Spinner as Spinner3 } from "@surf-kit/core";
2139
+ import { WaveLoader as WaveLoader3 } from "@surf-kit/core";
1629
2140
  import { jsx as jsx27, jsxs as jsxs24 } from "react/jsx-runtime";
1630
2141
  function RetrievalProgress({ sources, isActive, className }) {
1631
2142
  return /* @__PURE__ */ jsxs24(
@@ -1637,7 +2148,7 @@ function RetrievalProgress({ sources, isActive, className }) {
1637
2148
  "data-testid": "retrieval-progress",
1638
2149
  children: [
1639
2150
  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" }) }),
2151
+ /* @__PURE__ */ jsx27("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx27(WaveLoader3, { size: "sm", color: "#38bdf8" }) }),
1641
2152
  /* @__PURE__ */ jsx27("span", { children: "Retrieving sources..." })
1642
2153
  ] }),
1643
2154
  sources.length > 0 && /* @__PURE__ */ jsx27("ul", { className: "space-y-1", "data-testid": "source-list", children: sources.map((source, index) => /* @__PURE__ */ jsxs24(
@@ -1735,7 +2246,7 @@ function TypewriterText({
1735
2246
  }
1736
2247
 
1737
2248
  // src/streaming/TypingIndicator/TypingIndicator.tsx
1738
- import { twMerge as twMerge9 } from "tailwind-merge";
2249
+ import { twMerge as twMerge10 } from "tailwind-merge";
1739
2250
  import { useReducedMotion as useReducedMotion2 } from "@surf-kit/hooks";
1740
2251
  import { jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
1741
2252
  var bounceKeyframes = `
@@ -1755,7 +2266,7 @@ function TypingIndicator({
1755
2266
  {
1756
2267
  role: "status",
1757
2268
  "aria-label": label ?? "typing",
1758
- className: twMerge9("inline-flex items-center gap-2", className),
2269
+ className: twMerge10("inline-flex items-center gap-2", className),
1759
2270
  "data-testid": "typing-indicator",
1760
2271
  children: [
1761
2272
  !reducedMotion && /* @__PURE__ */ jsx30("style", { children: bounceKeyframes }),
@@ -1777,7 +2288,7 @@ function TypingIndicator({
1777
2288
  }
1778
2289
 
1779
2290
  // src/streaming/TextGlimmer/TextGlimmer.tsx
1780
- import { twMerge as twMerge10 } from "tailwind-merge";
2291
+ import { twMerge as twMerge11 } from "tailwind-merge";
1781
2292
  import { useReducedMotion as useReducedMotion3 } from "@surf-kit/hooks";
1782
2293
  import { jsx as jsx31, jsxs as jsxs28 } from "react/jsx-runtime";
1783
2294
  var shimmerKeyframes = `
@@ -1794,7 +2305,7 @@ function TextGlimmer({ lines = 3, className }) {
1794
2305
  {
1795
2306
  role: "status",
1796
2307
  "aria-label": "Loading",
1797
- className: twMerge10("flex flex-col gap-2", className),
2308
+ className: twMerge11("flex flex-col gap-2", className),
1798
2309
  "data-testid": "text-glimmer",
1799
2310
  children: [
1800
2311
  !reducedMotion && /* @__PURE__ */ jsx31("style", { children: shimmerKeyframes }),
@@ -1820,7 +2331,7 @@ function TextGlimmer({ lines = 3, className }) {
1820
2331
  }
1821
2332
 
1822
2333
  // src/streaming/StreamingList/StreamingList.tsx
1823
- import { twMerge as twMerge11 } from "tailwind-merge";
2334
+ import { twMerge as twMerge12 } from "tailwind-merge";
1824
2335
  import { useReducedMotion as useReducedMotion4 } from "@surf-kit/hooks";
1825
2336
  import { jsx as jsx32, jsxs as jsxs29 } from "react/jsx-runtime";
1826
2337
  var fadeSlideInKeyframes = `
@@ -1838,13 +2349,13 @@ function StreamingList({
1838
2349
  }) {
1839
2350
  const reducedMotion = useReducedMotion4();
1840
2351
  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;
2352
+ return emptyMessage ? /* @__PURE__ */ jsx32("p", { className: twMerge12("text-sm text-text-secondary", className), "data-testid": "streaming-list-empty", children: emptyMessage }) : null;
1842
2353
  }
1843
2354
  return /* @__PURE__ */ jsxs29(
1844
2355
  "ul",
1845
2356
  {
1846
2357
  "aria-live": "polite",
1847
- className: twMerge11("list-none p-0 m-0", className),
2358
+ className: twMerge12("list-none p-0 m-0", className),
1848
2359
  "data-testid": "streaming-list",
1849
2360
  children: [
1850
2361
  !reducedMotion && /* @__PURE__ */ jsx32("style", { children: fadeSlideInKeyframes }),
@@ -1864,7 +2375,7 @@ function StreamingList({
1864
2375
  }
1865
2376
 
1866
2377
  // src/streaming/StreamingStructure/StreamingStructure.tsx
1867
- import { twMerge as twMerge12 } from "tailwind-merge";
2378
+ import { twMerge as twMerge13 } from "tailwind-merge";
1868
2379
  import { useReducedMotion as useReducedMotion5 } from "@surf-kit/hooks";
1869
2380
  import { jsx as jsx33, jsxs as jsxs30 } from "react/jsx-runtime";
1870
2381
  var fadeSlideInKeyframes2 = `
@@ -1913,7 +2424,7 @@ function StreamingStructure({
1913
2424
  "dl",
1914
2425
  {
1915
2426
  "aria-live": "polite",
1916
- className: twMerge12("m-0", className),
2427
+ className: twMerge13("m-0", className),
1917
2428
  "data-testid": "streaming-structure",
1918
2429
  children: [
1919
2430
  !reducedMotion && /* @__PURE__ */ jsx33("style", { children: fadeSlideInKeyframes2 }),
@@ -1936,7 +2447,7 @@ function StreamingStructure({
1936
2447
  }
1937
2448
 
1938
2449
  // src/chat/ConversationList/ConversationList.tsx
1939
- import { twMerge as twMerge13 } from "tailwind-merge";
2450
+ import { twMerge as twMerge14 } from "tailwind-merge";
1940
2451
  import { jsx as jsx34, jsxs as jsxs31 } from "react/jsx-runtime";
1941
2452
  function ConversationList({
1942
2453
  conversations,
@@ -1950,14 +2461,14 @@ function ConversationList({
1950
2461
  "nav",
1951
2462
  {
1952
2463
  "aria-label": "Conversation list",
1953
- className: twMerge13("flex flex-col h-full bg-canvas", className),
2464
+ className: twMerge14("flex flex-col flex-1 min-h-0", className),
1954
2465
  children: [
1955
- onNew && /* @__PURE__ */ jsx34("div", { className: "p-3 border-b border-border", children: /* @__PURE__ */ jsx34(
2466
+ onNew && /* @__PURE__ */ jsx34("div", { className: "px-5 pt-1 pb-3 border-b border-border", children: /* @__PURE__ */ jsx34(
1956
2467
  "button",
1957
2468
  {
1958
2469
  type: "button",
1959
2470
  onClick: onNew,
1960
- className: "w-full px-4 py-2.5 rounded-xl text-sm font-semibold bg-accent text-white hover:bg-accent-hover transition-all duration-200",
2471
+ className: "w-full px-4 py-2 rounded-lg text-sm font-medium border border-border text-text-secondary hover:text-text-primary hover:bg-surface hover:border-border-strong transition-colors duration-150",
1961
2472
  children: "New conversation"
1962
2473
  }
1963
2474
  ) }),
@@ -1967,10 +2478,10 @@ function ConversationList({
1967
2478
  return /* @__PURE__ */ jsxs31(
1968
2479
  "li",
1969
2480
  {
1970
- className: twMerge13(
1971
- "flex items-start border-b border-border transition-colors duration-200",
1972
- "hover:bg-surface",
1973
- isActive && "bg-surface-raised border-l-2 border-l-accent"
2481
+ className: twMerge14(
2482
+ "flex items-start transition-colors duration-150",
2483
+ "hover:bg-surface-raised",
2484
+ isActive ? "bg-accent-subtlest border-l-[3px] border-l-accent" : "border-l-[3px] border-l-transparent"
1974
2485
  ),
1975
2486
  children: [
1976
2487
  /* @__PURE__ */ jsxs31(
@@ -1979,10 +2490,10 @@ function ConversationList({
1979
2490
  type: "button",
1980
2491
  onClick: () => onSelect(conversation.id),
1981
2492
  "aria-current": isActive ? "true" : void 0,
1982
- className: "flex-1 min-w-0 text-left px-4 py-3",
2493
+ className: "flex-1 min-w-0 text-left px-5 py-2.5",
1983
2494
  children: [
1984
- /* @__PURE__ */ jsx34("div", { className: "text-sm font-medium text-brand-cream truncate", children: conversation.title }),
1985
- /* @__PURE__ */ jsx34("div", { className: "text-xs text-brand-cream/40 truncate mt-0.5 leading-relaxed", children: conversation.lastMessage })
2495
+ /* @__PURE__ */ jsx34("div", { className: "text-sm font-medium text-text-primary truncate", children: conversation.title }),
2496
+ /* @__PURE__ */ jsx34("div", { className: "text-xs text-text-muted truncate mt-0.5 leading-relaxed", children: conversation.lastMessage })
1986
2497
  ]
1987
2498
  }
1988
2499
  ),
@@ -1992,7 +2503,7 @@ function ConversationList({
1992
2503
  type: "button",
1993
2504
  onClick: () => onDelete(conversation.id),
1994
2505
  "aria-label": `Delete ${conversation.title}`,
1995
- className: "shrink-0 p-1.5 m-2 rounded-lg text-brand-cream/25 hover:text-brand-watermelon hover:bg-brand-watermelon/10 transition-colors duration-200",
2506
+ className: "shrink-0 p-1.5 m-2 rounded-lg text-text-muted hover:text-status-error hover:bg-status-error/10 transition-colors duration-150",
1996
2507
  children: /* @__PURE__ */ jsxs31(
1997
2508
  "svg",
1998
2509
  {
@@ -2019,7 +2530,7 @@ function ConversationList({
2019
2530
  conversation.id
2020
2531
  );
2021
2532
  }),
2022
- conversations.length === 0 && /* @__PURE__ */ jsx34("li", { className: "px-4 py-8 text-center", children: /* @__PURE__ */ jsx34("span", { className: "text-sm text-brand-cream/30 font-body", children: "No conversations yet" }) })
2533
+ conversations.length === 0 && /* @__PURE__ */ jsx34("li", { className: "px-5 py-12 text-center", children: /* @__PURE__ */ jsx34("span", { className: "text-sm text-text-muted font-body", children: "No conversations yet" }) })
2023
2534
  ] })
2024
2535
  ]
2025
2536
  }
@@ -2027,9 +2538,9 @@ function ConversationList({
2027
2538
  }
2028
2539
 
2029
2540
  // src/layouts/AgentFullPage/AgentFullPage.tsx
2030
- import { twMerge as twMerge14 } from "tailwind-merge";
2031
- import { useState as useState7, useCallback as useCallback3 } from "react";
2032
- import { Fragment, jsx as jsx35, jsxs as jsxs32 } from "react/jsx-runtime";
2541
+ import { twMerge as twMerge15 } from "tailwind-merge";
2542
+ import { useState as useState7, useCallback as useCallback4 } from "react";
2543
+ import { Fragment as Fragment2, jsx as jsx35, jsxs as jsxs32 } from "react/jsx-runtime";
2033
2544
  function AgentFullPage({
2034
2545
  endpoint,
2035
2546
  title = "Chat",
@@ -2042,7 +2553,7 @@ function AgentFullPage({
2042
2553
  className
2043
2554
  }) {
2044
2555
  const [sidebarOpen, setSidebarOpen] = useState7(false);
2045
- const handleSelect = useCallback3(
2556
+ const handleSelect = useCallback4(
2046
2557
  (id) => {
2047
2558
  onConversationSelect?.(id);
2048
2559
  setSidebarOpen(false);
@@ -2052,10 +2563,10 @@ function AgentFullPage({
2052
2563
  return /* @__PURE__ */ jsxs32(
2053
2564
  "div",
2054
2565
  {
2055
- className: twMerge14("flex h-screen w-full overflow-hidden bg-brand-dark", className),
2566
+ className: twMerge15("flex h-screen w-full overflow-hidden bg-brand-dark", className),
2056
2567
  "data-testid": "agent-full-page",
2057
2568
  children: [
2058
- showConversationList && /* @__PURE__ */ jsxs32(Fragment, { children: [
2569
+ showConversationList && /* @__PURE__ */ jsxs32(Fragment2, { children: [
2059
2570
  sidebarOpen && /* @__PURE__ */ jsx35(
2060
2571
  "div",
2061
2572
  {
@@ -2067,7 +2578,7 @@ function AgentFullPage({
2067
2578
  /* @__PURE__ */ jsx35(
2068
2579
  "aside",
2069
2580
  {
2070
- className: twMerge14(
2581
+ className: twMerge15(
2071
2582
  "bg-brand-dark border-r border-brand-gold/15 w-72 shrink-0 flex-col z-40",
2072
2583
  // Desktop: always visible
2073
2584
  "hidden md:flex",
@@ -2133,7 +2644,7 @@ function AgentFullPage({
2133
2644
  }
2134
2645
 
2135
2646
  // src/layouts/AgentPanel/AgentPanel.tsx
2136
- import { twMerge as twMerge15 } from "tailwind-merge";
2647
+ import { twMerge as twMerge16 } from "tailwind-merge";
2137
2648
  import { useRef as useRef6, useEffect as useEffect5 } from "react";
2138
2649
  import { jsx as jsx36, jsxs as jsxs33 } from "react/jsx-runtime";
2139
2650
  function AgentPanel({
@@ -2158,13 +2669,13 @@ function AgentPanel({
2158
2669
  return /* @__PURE__ */ jsxs33(
2159
2670
  "div",
2160
2671
  {
2161
- className: twMerge15("fixed inset-0 z-50", !isOpen && "pointer-events-none"),
2672
+ className: twMerge16("fixed inset-0 z-50", !isOpen && "pointer-events-none"),
2162
2673
  "aria-hidden": !isOpen,
2163
2674
  children: [
2164
2675
  /* @__PURE__ */ jsx36(
2165
2676
  "div",
2166
2677
  {
2167
- className: twMerge15(
2678
+ className: twMerge16(
2168
2679
  "fixed inset-0 transition-opacity duration-300",
2169
2680
  isOpen ? "opacity-100 bg-brand-dark/70 backdrop-blur-sm pointer-events-auto" : "opacity-0 pointer-events-none"
2170
2681
  ),
@@ -2180,7 +2691,7 @@ function AgentPanel({
2180
2691
  "aria-label": title,
2181
2692
  "aria-modal": isOpen ? "true" : void 0,
2182
2693
  style: { width: widthStyle, maxWidth: "100vw" },
2183
- className: twMerge15(
2694
+ className: twMerge16(
2184
2695
  "fixed top-0 h-full flex flex-col z-50 bg-brand-dark shadow-card",
2185
2696
  "transition-transform duration-300 ease-in-out",
2186
2697
  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 +2733,8 @@ function AgentPanel({
2222
2733
  }
2223
2734
 
2224
2735
  // src/layouts/AgentWidget/AgentWidget.tsx
2225
- import { twMerge as twMerge16 } from "tailwind-merge";
2226
- import { useState as useState8, useCallback as useCallback4 } from "react";
2736
+ import { twMerge as twMerge17 } from "tailwind-merge";
2737
+ import { useState as useState8, useCallback as useCallback5 } from "react";
2227
2738
  import { jsx as jsx37, jsxs as jsxs34 } from "react/jsx-runtime";
2228
2739
  function AgentWidget({
2229
2740
  endpoint,
@@ -2233,7 +2744,7 @@ function AgentWidget({
2233
2744
  className
2234
2745
  }) {
2235
2746
  const [isOpen, setIsOpen] = useState8(false);
2236
- const toggle = useCallback4(() => {
2747
+ const toggle = useCallback5(() => {
2237
2748
  setIsOpen((prev) => !prev);
2238
2749
  }, []);
2239
2750
  const positionClasses = position === "bottom-left" ? "left-4 bottom-4" : "right-4 bottom-4";
@@ -2246,7 +2757,7 @@ function AgentWidget({
2246
2757
  role: "dialog",
2247
2758
  "aria-label": title,
2248
2759
  "aria-hidden": !isOpen,
2249
- className: twMerge16(
2760
+ className: twMerge17(
2250
2761
  "fixed z-50 flex flex-col",
2251
2762
  "w-[min(400px,calc(100vw-2rem))] h-[min(600px,calc(100vh-6rem))]",
2252
2763
  "rounded-2xl overflow-hidden border border-brand-gold/15",
@@ -2293,7 +2804,7 @@ function AgentWidget({
2293
2804
  onClick: toggle,
2294
2805
  "aria-label": isOpen ? "Close chat" : triggerLabel,
2295
2806
  "aria-expanded": isOpen,
2296
- className: twMerge16(
2807
+ className: twMerge17(
2297
2808
  "fixed z-50 flex items-center justify-center w-14 h-14 rounded-full",
2298
2809
  "bg-brand-blue text-brand-cream shadow-glow-cyan",
2299
2810
  "hover:bg-brand-cyan hover:shadow-glow-cyan hover:scale-105",
@@ -2311,7 +2822,7 @@ function AgentWidget({
2311
2822
  }
2312
2823
 
2313
2824
  // src/layouts/AgentEmbed/AgentEmbed.tsx
2314
- import { twMerge as twMerge17 } from "tailwind-merge";
2825
+ import { twMerge as twMerge18 } from "tailwind-merge";
2315
2826
  import { jsx as jsx38 } from "react/jsx-runtime";
2316
2827
  function AgentEmbed({
2317
2828
  endpoint,
@@ -2321,7 +2832,7 @@ function AgentEmbed({
2321
2832
  return /* @__PURE__ */ jsx38(
2322
2833
  "div",
2323
2834
  {
2324
- className: twMerge17("w-full h-full min-h-0", className),
2835
+ className: twMerge18("w-full h-full min-h-0", className),
2325
2836
  "data-testid": "agent-embed",
2326
2837
  children: /* @__PURE__ */ jsx38(
2327
2838
  AgentChat,
@@ -2337,8 +2848,8 @@ function AgentEmbed({
2337
2848
 
2338
2849
  // src/mcp/MCPToolCall/MCPToolCall.tsx
2339
2850
  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";
2851
+ import { twMerge as twMerge19 } from "tailwind-merge";
2852
+ import { Badge as Badge7, WaveLoader as WaveLoader4 } from "@surf-kit/core";
2342
2853
  import { jsx as jsx39, jsxs as jsxs35 } from "react/jsx-runtime";
2343
2854
  var statusBadgeIntent = {
2344
2855
  pending: "default",
@@ -2381,7 +2892,7 @@ function MCPToolCall({ call, isExpanded = false, onToggleExpand, className }) {
2381
2892
  return /* @__PURE__ */ jsxs35(
2382
2893
  "div",
2383
2894
  {
2384
- className: twMerge18(container({ status: call.status }), className),
2895
+ className: twMerge19(container({ status: call.status }), className),
2385
2896
  "data-testid": "mcp-tool-call",
2386
2897
  children: [
2387
2898
  /* @__PURE__ */ jsxs35(
@@ -2398,7 +2909,7 @@ function MCPToolCall({ call, isExpanded = false, onToggleExpand, className }) {
2398
2909
  call.serverName && /* @__PURE__ */ jsx39("span", { className: "text-xs text-text-secondary truncate", children: call.serverName })
2399
2910
  ] }),
2400
2911
  /* @__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" }) }),
2912
+ call.status === "running" && /* @__PURE__ */ jsx39("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx39(WaveLoader4, { size: "sm", color: "#38bdf8" }) }),
2402
2913
  /* @__PURE__ */ jsx39(
2403
2914
  Badge7,
2404
2915
  {
@@ -2472,7 +2983,7 @@ function MCPToolCall({ call, isExpanded = false, onToggleExpand, className }) {
2472
2983
  }
2473
2984
 
2474
2985
  // src/mcp/MCPResourceView/MCPResourceView.tsx
2475
- import { twMerge as twMerge19 } from "tailwind-merge";
2986
+ import { twMerge as twMerge20 } from "tailwind-merge";
2476
2987
  import { jsx as jsx40, jsxs as jsxs36 } from "react/jsx-runtime";
2477
2988
  function isImageMime(mime) {
2478
2989
  return !!mime && mime.startsWith("image/");
@@ -2490,7 +3001,7 @@ function MCPResourceView({ resource, className }) {
2490
3001
  return /* @__PURE__ */ jsxs36(
2491
3002
  "div",
2492
3003
  {
2493
- className: twMerge19("rounded-lg border border-border bg-surface p-3 text-sm", className),
3004
+ className: twMerge20("rounded-lg border border-border bg-surface p-3 text-sm", className),
2494
3005
  "data-testid": "mcp-resource-view",
2495
3006
  children: [
2496
3007
  /* @__PURE__ */ jsxs36("div", { className: "mb-2", children: [
@@ -2542,7 +3053,7 @@ function MCPResourceView({ resource, className }) {
2542
3053
  // src/mcp/MCPServerStatus/MCPServerStatus.tsx
2543
3054
  import { useState as useState9 } from "react";
2544
3055
  import { cva as cva2 } from "class-variance-authority";
2545
- import { twMerge as twMerge20 } from "tailwind-merge";
3056
+ import { twMerge as twMerge21 } from "tailwind-merge";
2546
3057
  import { jsx as jsx41, jsxs as jsxs37 } from "react/jsx-runtime";
2547
3058
  var statusDot = cva2("inline-block h-2 w-2 rounded-full shrink-0", {
2548
3059
  variants: {
@@ -2570,7 +3081,7 @@ function MCPServerStatus({ server, className }) {
2570
3081
  return /* @__PURE__ */ jsxs37(
2571
3082
  "div",
2572
3083
  {
2573
- className: twMerge20("rounded-lg border border-border bg-surface p-3 text-sm", className),
3084
+ className: twMerge21("rounded-lg border border-border bg-surface p-3 text-sm", className),
2574
3085
  "data-testid": "mcp-server-status",
2575
3086
  children: [
2576
3087
  /* @__PURE__ */ jsxs37("div", { className: "flex items-center gap-2 mb-1", children: [
@@ -2684,7 +3195,7 @@ function MCPServerStatus({ server, className }) {
2684
3195
  // src/mcp/MCPApprovalDialog/MCPApprovalDialog.tsx
2685
3196
  import { useRef as useRef7, useEffect as useEffect6 } from "react";
2686
3197
  import { cva as cva3 } from "class-variance-authority";
2687
- import { twMerge as twMerge21 } from "tailwind-merge";
3198
+ import { twMerge as twMerge22 } from "tailwind-merge";
2688
3199
  import { useDialog, FocusScope } from "react-aria";
2689
3200
  import { Button as Button2, Badge as Badge8 } from "@surf-kit/core";
2690
3201
  import { jsx as jsx42, jsxs as jsxs38 } from "react/jsx-runtime";
@@ -2744,7 +3255,7 @@ function MCPApprovalDialog({
2744
3255
  {
2745
3256
  ...dialogProps,
2746
3257
  ref,
2747
- className: twMerge21(riskBorder({ risk: riskLevel }), className),
3258
+ className: twMerge22(riskBorder({ risk: riskLevel }), className),
2748
3259
  "data-testid": "mcp-approval-dialog",
2749
3260
  children: [
2750
3261
  /* @__PURE__ */ jsxs38("div", { className: "flex items-center justify-between mb-4", children: [
@@ -2913,7 +3424,7 @@ function ThumbsFeedback({
2913
3424
  // src/feedback/FeedbackDialog/FeedbackDialog.tsx
2914
3425
  import { useState as useState11 } from "react";
2915
3426
  import { Dialog, Button as Button3, TextArea } from "@surf-kit/core";
2916
- import { Fragment as Fragment2, jsx as jsx44, jsxs as jsxs40 } from "react/jsx-runtime";
3427
+ import { Fragment as Fragment3, jsx as jsx44, jsxs as jsxs40 } from "react/jsx-runtime";
2917
3428
  function FeedbackDialog({ isOpen, onClose, onSubmit, className }) {
2918
3429
  const [comment, setComment] = useState11("");
2919
3430
  const handleSubmit = () => {
@@ -2929,7 +3440,7 @@ function FeedbackDialog({ isOpen, onClose, onSubmit, className }) {
2929
3440
  title: "Share your feedback",
2930
3441
  size: "sm",
2931
3442
  className,
2932
- footer: /* @__PURE__ */ jsxs40(Fragment2, { children: [
3443
+ footer: /* @__PURE__ */ jsxs40(Fragment3, { children: [
2933
3444
  /* @__PURE__ */ jsx44(Button3, { intent: "ghost", onPress: onClose, children: "Cancel" }),
2934
3445
  /* @__PURE__ */ jsx44(Button3, { intent: "primary", onPress: handleSubmit, isDisabled: comment.trim().length === 0, children: "Submit" })
2935
3446
  ] }),