@extrachill/chat 0.3.1 → 0.5.1

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.
@@ -1,34 +1,47 @@
1
- import { useState, useRef, useCallback, type KeyboardEvent, type FormEvent } from 'react';
1
+ import { useState, useRef, useCallback, type KeyboardEvent, type FormEvent, type DragEvent, type ClipboardEvent } from 'react';
2
2
 
3
3
  export interface ChatInputProps {
4
- /** Called when the user submits a message. */
5
- onSend: (content: string) => void;
4
+ /** Called when the user submits a message (with optional file attachments). */
5
+ onSend: (content: string, files?: File[]) => void;
6
6
  /** Whether input is disabled (e.g. while waiting for response). */
7
7
  disabled?: boolean;
8
8
  /** Placeholder text. Defaults to 'Type a message...'. */
9
9
  placeholder?: string;
10
10
  /** Maximum number of rows the textarea auto-grows to. Defaults to 6. */
11
11
  maxRows?: number;
12
+ /** Accepted file types for the file picker. Defaults to 'image/*,video/*'. */
13
+ accept?: string;
14
+ /** Maximum number of files per message. Defaults to 5. */
15
+ maxFiles?: number;
16
+ /** Whether to show the attachment button. Defaults to true. */
17
+ allowAttachments?: boolean;
12
18
  /** Additional CSS class name. */
13
19
  className?: string;
14
20
  }
15
21
 
16
22
  /**
17
- * Chat input with auto-growing textarea and keyboard shortcuts.
23
+ * Chat input with auto-growing textarea, keyboard shortcuts, and file attachments.
18
24
  *
19
25
  * - Enter sends the message
20
26
  * - Shift+Enter adds a newline
21
27
  * - Textarea auto-grows up to `maxRows`
28
+ * - File attachment via button, drag-and-drop, or clipboard paste
22
29
  */
23
30
  export function ChatInput({
24
31
  onSend,
25
32
  disabled = false,
26
33
  placeholder = 'Type a message...',
27
34
  maxRows = 6,
35
+ accept = 'image/*,video/*',
36
+ maxFiles = 5,
37
+ allowAttachments = true,
28
38
  className,
29
39
  }: ChatInputProps) {
30
40
  const [value, setValue] = useState('');
41
+ const [files, setFiles] = useState<File[]>([]);
42
+ const [isDragging, setIsDragging] = useState(false);
31
43
  const textareaRef = useRef<HTMLTextAreaElement>(null);
44
+ const fileInputRef = useRef<HTMLInputElement>(null);
32
45
  const cooldownRef = useRef(false);
33
46
 
34
47
  const resize = useCallback(() => {
@@ -40,24 +53,35 @@ export function ChatInput({
40
53
  el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
41
54
  }, [maxRows]);
42
55
 
56
+ const addFiles = useCallback((newFiles: FileList | File[]) => {
57
+ const fileArray = Array.from(newFiles);
58
+ setFiles((prev) => {
59
+ const combined = [...prev, ...fileArray];
60
+ return combined.slice(0, maxFiles);
61
+ });
62
+ }, [maxFiles]);
63
+
64
+ const removeFile = useCallback((index: number) => {
65
+ setFiles((prev) => prev.filter((_, i) => i !== index));
66
+ }, []);
67
+
43
68
  const handleSubmit = useCallback((e?: FormEvent) => {
44
69
  e?.preventDefault();
45
70
  const trimmed = value.trim();
46
- if (!trimmed || disabled || cooldownRef.current) return;
71
+ if ((!trimmed && files.length === 0) || disabled || cooldownRef.current) return;
47
72
 
48
- // Debounce to prevent double-submit
49
73
  cooldownRef.current = true;
50
74
  setTimeout(() => { cooldownRef.current = false; }, 300);
51
75
 
52
- onSend(trimmed);
76
+ onSend(trimmed, files.length > 0 ? files : undefined);
53
77
  setValue('');
78
+ setFiles([]);
54
79
 
55
- // Reset textarea height after clearing
56
80
  requestAnimationFrame(() => {
57
81
  const el = textareaRef.current;
58
82
  if (el) el.style.height = 'auto';
59
83
  });
60
- }, [value, disabled, onSend]);
84
+ }, [value, files, disabled, onSend]);
61
85
 
62
86
  const handleKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
63
87
  if (e.key === 'Enter' && !e.shiftKey) {
@@ -66,34 +90,137 @@ export function ChatInput({
66
90
  }
67
91
  }, [handleSubmit]);
68
92
 
93
+ const handleDragOver = useCallback((e: DragEvent) => {
94
+ e.preventDefault();
95
+ if (allowAttachments) setIsDragging(true);
96
+ }, [allowAttachments]);
97
+
98
+ const handleDragLeave = useCallback((e: DragEvent) => {
99
+ e.preventDefault();
100
+ setIsDragging(false);
101
+ }, []);
102
+
103
+ const handleDrop = useCallback((e: DragEvent) => {
104
+ e.preventDefault();
105
+ setIsDragging(false);
106
+ if (!allowAttachments || !e.dataTransfer.files.length) return;
107
+ addFiles(e.dataTransfer.files);
108
+ }, [allowAttachments, addFiles]);
109
+
110
+ const handlePaste = useCallback((e: ClipboardEvent) => {
111
+ if (!allowAttachments) return;
112
+ const pastedFiles = Array.from(e.clipboardData.items)
113
+ .filter((item) => item.kind === 'file')
114
+ .map((item) => item.getAsFile())
115
+ .filter((f): f is File => f !== null);
116
+
117
+ if (pastedFiles.length > 0) {
118
+ addFiles(pastedFiles);
119
+ }
120
+ }, [allowAttachments, addFiles]);
121
+
69
122
  const baseClass = 'ec-chat-input';
70
- const classes = [baseClass, className].filter(Boolean).join(' ');
123
+ const classes = [
124
+ baseClass,
125
+ isDragging ? `${baseClass}--dragging` : '',
126
+ className,
127
+ ].filter(Boolean).join(' ');
128
+
129
+ return (
130
+ <form
131
+ className={classes}
132
+ onSubmit={handleSubmit}
133
+ onDragOver={handleDragOver}
134
+ onDragLeave={handleDragLeave}
135
+ onDrop={handleDrop}
136
+ >
137
+ {files.length > 0 && (
138
+ <div className={`${baseClass}__attachments`}>
139
+ {files.map((file, i) => (
140
+ <FilePreview key={`${file.name}-${i}`} file={file} onRemove={() => removeFile(i)} />
141
+ ))}
142
+ </div>
143
+ )}
144
+ <div className={`${baseClass}__row`}>
145
+ {allowAttachments && (
146
+ <>
147
+ <input
148
+ ref={fileInputRef}
149
+ type="file"
150
+ accept={accept}
151
+ multiple
152
+ className={`${baseClass}__file-input`}
153
+ onChange={(e) => {
154
+ if (e.target.files) addFiles(e.target.files);
155
+ e.target.value = '';
156
+ }}
157
+ tabIndex={-1}
158
+ aria-hidden="true"
159
+ />
160
+ <button
161
+ type="button"
162
+ className={`${baseClass}__attach`}
163
+ onClick={() => fileInputRef.current?.click()}
164
+ disabled={disabled}
165
+ aria-label="Attach file"
166
+ title="Attach file"
167
+ >
168
+ <AttachIcon />
169
+ </button>
170
+ </>
171
+ )}
172
+ <textarea
173
+ ref={textareaRef}
174
+ className={`${baseClass}__textarea`}
175
+ value={value}
176
+ onChange={(e) => { setValue(e.target.value); resize(); }}
177
+ onKeyDown={handleKeyDown}
178
+ onPaste={handlePaste}
179
+ placeholder={placeholder}
180
+ disabled={disabled}
181
+ rows={1}
182
+ aria-label={placeholder}
183
+ />
184
+ <button
185
+ className={`${baseClass}__send`}
186
+ type="submit"
187
+ disabled={disabled || (!value.trim() && files.length === 0)}
188
+ aria-label="Send message"
189
+ >
190
+ <SendIcon />
191
+ </button>
192
+ </div>
193
+ </form>
194
+ );
195
+ }
196
+
197
+ /* ---- File Preview ---- */
198
+
199
+ function FilePreview({ file, onRemove }: { file: File; onRemove: () => void }) {
200
+ const isImage = file.type.startsWith('image/');
201
+ const preview = isImage ? URL.createObjectURL(file) : null;
71
202
 
72
203
  return (
73
- <form className={classes} onSubmit={handleSubmit}>
74
- <textarea
75
- ref={textareaRef}
76
- className={`${baseClass}__textarea`}
77
- value={value}
78
- onChange={(e) => { setValue(e.target.value); resize(); }}
79
- onKeyDown={handleKeyDown}
80
- placeholder={placeholder}
81
- disabled={disabled}
82
- rows={1}
83
- aria-label={placeholder}
84
- />
204
+ <div className="ec-chat-input__preview">
205
+ {preview ? (
206
+ <img src={preview} alt={file.name} className="ec-chat-input__preview-image" />
207
+ ) : (
208
+ <span className="ec-chat-input__preview-name">{file.name}</span>
209
+ )}
85
210
  <button
86
- className={`${baseClass}__send`}
87
- type="submit"
88
- disabled={disabled || !value.trim()}
89
- aria-label="Send message"
211
+ type="button"
212
+ className="ec-chat-input__preview-remove"
213
+ onClick={onRemove}
214
+ aria-label={`Remove ${file.name}`}
90
215
  >
91
- <SendIcon />
216
+ &times;
92
217
  </button>
93
- </form>
218
+ </div>
94
219
  );
95
220
  }
96
221
 
222
+ /* ---- Icons ---- */
223
+
97
224
  function SendIcon() {
98
225
  return (
99
226
  <svg
@@ -112,3 +239,21 @@ function SendIcon() {
112
239
  </svg>
113
240
  );
114
241
  }
242
+
243
+ function AttachIcon() {
244
+ return (
245
+ <svg
246
+ className="ec-chat-input__attach-icon"
247
+ viewBox="0 0 24 24"
248
+ width="20"
249
+ height="20"
250
+ fill="none"
251
+ stroke="currentColor"
252
+ strokeWidth="2"
253
+ strokeLinecap="round"
254
+ strokeLinejoin="round"
255
+ >
256
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
257
+ </svg>
258
+ );
259
+ }
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode, lazy, Suspense } from 'react';
2
- import type { ChatMessage as ChatMessageType, ContentFormat } from '../types/index.ts';
2
+ import type { ChatMessage as ChatMessageType, ContentFormat, MediaAttachment } from '../types/index.ts';
3
3
  import { markdownToHtml } from '../markdown.ts';
4
4
 
5
5
  const ReactMarkdown = lazy(() => import('react-markdown'));
@@ -22,6 +22,7 @@ export interface ChatMessageProps {
22
22
  *
23
23
  * User messages align right, assistant messages align left.
24
24
  * Markdown content is rendered via react-markdown (lazy-loaded).
25
+ * Media attachments render inline below the text content.
25
26
  */
26
27
  export function ChatMessage({
27
28
  message,
@@ -34,13 +35,20 @@ export function ChatMessage({
34
35
  const roleClass = isUser ? `${baseClass}--user` : `${baseClass}--assistant`;
35
36
  const classes = [baseClass, roleClass, className].filter(Boolean).join(' ');
36
37
 
38
+ const hasText = message.content.trim().length > 0;
39
+ const hasAttachments = message.attachments && message.attachments.length > 0;
40
+
37
41
  return (
38
42
  <div className={classes} data-message-id={message.id}>
39
43
  <div className={`${baseClass}__bubble`}>
40
- {renderContent
41
- ? renderContent(message.content, message.role)
42
- : <DefaultContent content={message.content} format={contentFormat} />
43
- }
44
+ {hasText && (
45
+ renderContent
46
+ ? renderContent(message.content, message.role)
47
+ : <DefaultContent content={message.content} format={contentFormat} />
48
+ )}
49
+ {hasAttachments && (
50
+ <MessageAttachments attachments={message.attachments!} />
51
+ )}
44
52
  </div>
45
53
  {message.timestamp && (
46
54
  <time
@@ -103,3 +111,92 @@ function formatTime(iso: string): string {
103
111
  return '';
104
112
  }
105
113
  }
114
+
115
+ /* ---- Media Attachments ---- */
116
+
117
+ function MessageAttachments({ attachments }: { attachments: MediaAttachment[] }) {
118
+ const images = attachments.filter((a) => a.type === 'image');
119
+ const videos = attachments.filter((a) => a.type === 'video');
120
+ const files = attachments.filter((a) => a.type === 'file');
121
+
122
+ return (
123
+ <div className="ec-chat-message__attachments">
124
+ {images.length > 0 && (
125
+ <div className="ec-chat-message__images">
126
+ {images.map((img, i) => (
127
+ <a
128
+ key={i}
129
+ href={img.url}
130
+ target="_blank"
131
+ rel="noopener noreferrer"
132
+ className="ec-chat-message__image-link"
133
+ >
134
+ <img
135
+ src={img.thumbnailUrl ?? img.url}
136
+ alt={img.alt ?? img.filename ?? 'Image attachment'}
137
+ className="ec-chat-message__image"
138
+ loading="lazy"
139
+ />
140
+ </a>
141
+ ))}
142
+ </div>
143
+ )}
144
+ {videos.map((vid, i) => (
145
+ <video
146
+ key={i}
147
+ src={vid.url}
148
+ controls
149
+ className="ec-chat-message__video"
150
+ preload="metadata"
151
+ >
152
+ <track kind="captions" />
153
+ </video>
154
+ ))}
155
+ {files.map((file, i) => (
156
+ <a
157
+ key={i}
158
+ href={file.url}
159
+ download={file.filename}
160
+ className="ec-chat-message__file"
161
+ target="_blank"
162
+ rel="noopener noreferrer"
163
+ >
164
+ <FileIcon />
165
+ <span className="ec-chat-message__file-name">
166
+ {file.filename ?? 'Download file'}
167
+ </span>
168
+ {file.size != null && (
169
+ <span className="ec-chat-message__file-size">
170
+ {formatFileSize(file.size)}
171
+ </span>
172
+ )}
173
+ </a>
174
+ ))}
175
+ </div>
176
+ );
177
+ }
178
+
179
+ function FileIcon() {
180
+ return (
181
+ <svg
182
+ className="ec-chat-message__file-icon"
183
+ viewBox="0 0 24 24"
184
+ width="16"
185
+ height="16"
186
+ fill="none"
187
+ stroke="currentColor"
188
+ strokeWidth="2"
189
+ strokeLinecap="round"
190
+ strokeLinejoin="round"
191
+ >
192
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
193
+ <polyline points="14 2 14 8 20 8" />
194
+ </svg>
195
+ );
196
+ }
197
+
198
+ function formatFileSize(bytes: number): string {
199
+ if (bytes < 1024) return `${bytes} B`;
200
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
201
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
202
+ }
@@ -1,8 +1,9 @@
1
1
  import { useState, useCallback, useRef, useEffect } from 'react';
2
- import type { ChatMessage } from '../types/message.ts';
2
+ import type { ChatMessage, ToolCall } from '../types/message.ts';
3
3
  import type { ChatSession } from '../types/session.ts';
4
4
  import type { ChatAvailability } from '../types/session.ts';
5
- import type { FetchFn, ChatApiConfig } from '../api.ts';
5
+ import type { MediaAttachment } from '../types/message.ts';
6
+ import type { FetchFn, ChatApiConfig, SendAttachment } from '../api.ts';
6
7
  import {
7
8
  sendMessage as apiSendMessage,
8
9
  continueResponse as apiContinueResponse,
@@ -50,6 +51,23 @@ export interface UseChatOptions {
50
51
  * Called when an error occurs.
51
52
  */
52
53
  onError?: (error: Error) => void;
54
+ /**
55
+ * Called after each turn when tool calls are present in the response.
56
+ * Use this to react to tool executions (e.g. invalidate caches,
57
+ * apply diffs to the editor, update external state).
58
+ */
59
+ onToolCalls?: (toolCalls: ToolCall[]) => void;
60
+ /**
61
+ * Arbitrary metadata forwarded to the backend with each message.
62
+ * Use for context scoping (e.g. `{ selected_pipeline_id: 42 }`,
63
+ * `{ post_id: 100, context: 'editor' }`).
64
+ */
65
+ metadata?: Record<string, unknown>;
66
+ /**
67
+ * Optional context filter for session listing.
68
+ * Only sessions created in the matching context are shown.
69
+ */
70
+ sessionContext?: string;
53
71
  }
54
72
 
55
73
  /**
@@ -66,12 +84,19 @@ export interface UseChatReturn {
66
84
  availability: ChatAvailability;
67
85
  /** Active session ID. */
68
86
  sessionId: string | null;
87
+ /**
88
+ * The session ID that initiated the current request.
89
+ * Use to avoid stale loading indicators when the user switches
90
+ * sessions while a request is in flight.
91
+ * Null when idle.
92
+ */
93
+ processingSessionId: string | null;
69
94
  /** List of sessions. */
70
95
  sessions: ChatSession[];
71
96
  /** Whether sessions are loading. */
72
97
  sessionsLoading: boolean;
73
- /** Send a user message. */
74
- sendMessage: (content: string) => void;
98
+ /** Send a user message (with optional file attachments). */
99
+ sendMessage: (content: string, files?: File[]) => void;
75
100
  /** Switch to a different session. */
76
101
  switchSession: (sessionId: string) => void;
77
102
  /** Create a new session. */
@@ -141,12 +166,16 @@ export function useChat({
141
166
  maxContinueTurns = 20,
142
167
  onMessage,
143
168
  onError,
169
+ onToolCalls,
170
+ metadata,
171
+ sessionContext,
144
172
  }: UseChatOptions): UseChatReturn {
145
173
  const [messages, setMessages] = useState<ChatMessage[]>(initialMessages ?? []);
146
174
  const [isLoading, setIsLoading] = useState(false);
147
175
  const [turnCount, setTurnCount] = useState(0);
148
176
  const [availability, setAvailability] = useState<ChatAvailability>({ status: 'ready' });
149
177
  const [sessionId, setSessionId] = useState<string | null>(initialSessionId ?? null);
178
+ const [processingSessionId, setProcessingSessionId] = useState<string | null>(null);
150
179
  const [sessions, setSessions] = useState<ChatSession[]>([]);
151
180
  const [sessionsLoading, setSessionsLoading] = useState(false);
152
181
 
@@ -157,12 +186,26 @@ export function useChat({
157
186
  const sessionIdRef = useRef(sessionId);
158
187
  sessionIdRef.current = sessionId;
159
188
 
189
+ // Refs for latest callback/metadata values (avoid stale closures).
190
+ const onToolCallsRef = useRef(onToolCalls);
191
+ onToolCallsRef.current = onToolCalls;
192
+ const metadataRef = useRef(metadata);
193
+ metadataRef.current = metadata;
194
+ const sessionContextRef = useRef(sessionContext);
195
+ sessionContextRef.current = sessionContext;
196
+ // Guard against concurrent session creation.
197
+ const isCreatingRef = useRef(false);
198
+
160
199
  // Load sessions on mount
161
200
  useEffect(() => {
162
201
  const loadSessions = async () => {
163
202
  setSessionsLoading(true);
164
203
  try {
165
- const list = await apiListSessions(configRef.current);
204
+ const list = await apiListSessions(
205
+ configRef.current,
206
+ 20,
207
+ sessionContextRef.current,
208
+ );
166
209
  setSessions(list);
167
210
  } catch (err) {
168
211
  // Sessions not available — degrade gracefully
@@ -176,8 +219,47 @@ export function useChat({
176
219
  // eslint-disable-next-line react-hooks/exhaustive-deps
177
220
  }, []);
178
221
 
179
- const sendMessage = useCallback(async (content: string) => {
180
- if (isLoading) return;
222
+ /**
223
+ * Collect tool calls from a list of messages and fire the onToolCalls callback.
224
+ */
225
+ const fireToolCalls = useCallback((msgs: ChatMessage[]) => {
226
+ const cb = onToolCallsRef.current;
227
+ if (!cb) return;
228
+
229
+ const allToolCalls: ToolCall[] = [];
230
+ for (const msg of msgs) {
231
+ if (msg.toolCalls?.length) {
232
+ allToolCalls.push(...msg.toolCalls);
233
+ }
234
+ }
235
+ if (allToolCalls.length > 0) {
236
+ cb(allToolCalls);
237
+ }
238
+ }, []);
239
+
240
+ const sendMessage = useCallback(async (content: string, files?: File[]) => {
241
+ if (isLoading || isCreatingRef.current) return;
242
+
243
+ // Build optimistic attachment previews from local files.
244
+ let optimisticAttachments: MediaAttachment[] | undefined;
245
+ let sendAttachments: SendAttachment[] | undefined;
246
+
247
+ if (files?.length) {
248
+ optimisticAttachments = files.map((file) => ({
249
+ type: file.type.startsWith('image/') ? 'image' as const
250
+ : file.type.startsWith('video/') ? 'video' as const
251
+ : 'file' as const,
252
+ url: URL.createObjectURL(file),
253
+ filename: file.name,
254
+ mimeType: file.type,
255
+ size: file.size,
256
+ }));
257
+
258
+ sendAttachments = files.map((file) => ({
259
+ filename: file.name,
260
+ mime_type: file.type,
261
+ }));
262
+ }
181
263
 
182
264
  // Optimistically add user message
183
265
  const userMessage: ChatMessage = {
@@ -185,27 +267,45 @@ export function useChat({
185
267
  role: 'user',
186
268
  content,
187
269
  timestamp: new Date().toISOString(),
270
+ attachments: optimisticAttachments,
188
271
  };
189
272
 
273
+ // Guard against concurrent session creation.
274
+ if (!sessionIdRef.current) {
275
+ isCreatingRef.current = true;
276
+ }
277
+
190
278
  setMessages((prev) => [...prev, userMessage]);
191
279
  onMessage?.(userMessage);
192
280
  setIsLoading(true);
193
281
  setTurnCount(0);
194
282
 
283
+ // Track which session initiated the request.
284
+ const initiatingSessionId = sessionIdRef.current;
285
+ setProcessingSessionId(initiatingSessionId);
286
+
195
287
  try {
196
288
  const result = await apiSendMessage(
197
289
  configRef.current,
198
290
  content,
199
291
  sessionIdRef.current ?? undefined,
292
+ sendAttachments,
293
+ metadataRef.current,
200
294
  );
201
295
 
296
+ isCreatingRef.current = false;
297
+
202
298
  // Update session ID (may be newly created)
203
299
  setSessionId(result.sessionId);
204
300
  sessionIdRef.current = result.sessionId;
301
+ setProcessingSessionId(result.sessionId);
205
302
 
206
303
  // Replace all messages with the full normalized conversation
207
304
  setMessages(result.messages);
208
305
 
306
+ // Fire tool call callback for the initial response.
307
+ fireToolCalls(result.messages);
308
+
209
309
  // Handle multi-turn continuation
210
310
  if (!result.completed && !result.maxTurnsReached) {
211
311
  let completed = false;
@@ -225,16 +325,20 @@ export function useChat({
225
325
  onMessage?.(msg);
226
326
  }
227
327
 
328
+ // Fire tool call callback for each continuation turn.
329
+ fireToolCalls(continuation.messages);
330
+
228
331
  completed = continuation.completed || continuation.maxTurnsReached;
229
332
  }
230
333
  }
231
334
 
232
335
  // Refresh sessions list after a message
233
- apiListSessions(configRef.current)
336
+ apiListSessions(configRef.current, 20, sessionContextRef.current)
234
337
  .then(setSessions)
235
338
  .catch(() => { /* ignore */ });
236
339
 
237
340
  } catch (err) {
341
+ isCreatingRef.current = false;
238
342
  const error = toError(err);
239
343
  onError?.(error);
240
344
 
@@ -254,8 +358,9 @@ export function useChat({
254
358
  } finally {
255
359
  setIsLoading(false);
256
360
  setTurnCount(0);
361
+ setProcessingSessionId(null);
257
362
  }
258
- }, [isLoading, maxContinueTurns, onMessage, onError]);
363
+ }, [isLoading, maxContinueTurns, onMessage, onError, fireToolCalls]);
259
364
 
260
365
  const switchSession = useCallback(async (newSessionId: string) => {
261
366
  setSessionId(newSessionId);
@@ -301,7 +406,11 @@ export function useChat({
301
406
  const refreshSessions = useCallback(async () => {
302
407
  setSessionsLoading(true);
303
408
  try {
304
- const list = await apiListSessions(configRef.current);
409
+ const list = await apiListSessions(
410
+ configRef.current,
411
+ 20,
412
+ sessionContextRef.current,
413
+ );
305
414
  setSessions(list);
306
415
  } catch (err) {
307
416
  onError?.(toError(err));
@@ -316,6 +425,7 @@ export function useChat({
316
425
  turnCount,
317
426
  availability,
318
427
  sessionId,
428
+ processingSessionId,
319
429
  sessions,
320
430
  sessionsLoading,
321
431
  sendMessage,
package/src/index.ts CHANGED
@@ -3,18 +3,20 @@ export type {
3
3
  MessageRole,
4
4
  ToolCall,
5
5
  ToolResultMeta,
6
+ MediaAttachment,
6
7
  ChatMessage,
7
8
  ContentFormat,
8
9
  ChatSession,
9
10
  ChatAvailability,
10
11
  ChatInitialState,
12
+ RawAttachment,
11
13
  RawMessage,
12
14
  RawSession,
13
15
  SessionMetadata,
14
16
  } from './types/index.ts';
15
17
 
16
18
  // API
17
- export type { FetchFn, FetchOptions, ChatApiConfig, SendResult, ContinueResult } from './api.ts';
19
+ export type { FetchFn, FetchOptions, ChatApiConfig, SendResult, ContinueResult, SendAttachment } from './api.ts';
18
20
  export {
19
21
  sendMessage,
20
22
  continueResponse,