@witqq/agent-sdk 0.6.1 → 0.8.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 (145) hide show
  1. package/README.md +539 -6
  2. package/dist/{types-BvwNzZCj.d.cts → agent-CW9XbmG_.d.ts} +148 -95
  3. package/dist/{types-BvwNzZCj.d.ts → agent-DxY68NZL.d.cts} +148 -95
  4. package/dist/auth/index.cjs +260 -2
  5. package/dist/auth/index.cjs.map +1 -1
  6. package/dist/auth/index.d.cts +21 -138
  7. package/dist/auth/index.d.ts +21 -138
  8. package/dist/auth/index.js +260 -3
  9. package/dist/auth/index.js.map +1 -1
  10. package/dist/backends/claude.cjs +653 -140
  11. package/dist/backends/claude.cjs.map +1 -1
  12. package/dist/backends/claude.d.cts +4 -1
  13. package/dist/backends/claude.d.ts +4 -1
  14. package/dist/backends/claude.js +653 -140
  15. package/dist/backends/claude.js.map +1 -1
  16. package/dist/backends/copilot.cjs +428 -88
  17. package/dist/backends/copilot.cjs.map +1 -1
  18. package/dist/backends/copilot.d.cts +13 -4
  19. package/dist/backends/copilot.d.ts +13 -4
  20. package/dist/backends/copilot.js +428 -88
  21. package/dist/backends/copilot.js.map +1 -1
  22. package/dist/backends/vercel-ai.cjs +349 -77
  23. package/dist/backends/vercel-ai.cjs.map +1 -1
  24. package/dist/backends/vercel-ai.d.cts +3 -1
  25. package/dist/backends/vercel-ai.d.ts +3 -1
  26. package/dist/backends/vercel-ai.js +349 -77
  27. package/dist/backends/vercel-ai.js.map +1 -1
  28. package/dist/backends-BSrsBYFn.d.cts +39 -0
  29. package/dist/backends-BSrsBYFn.d.ts +39 -0
  30. package/dist/chat/accumulator.cjs +147 -0
  31. package/dist/chat/accumulator.cjs.map +1 -0
  32. package/dist/chat/accumulator.d.cts +64 -0
  33. package/dist/chat/accumulator.d.ts +64 -0
  34. package/dist/chat/accumulator.js +145 -0
  35. package/dist/chat/accumulator.js.map +1 -0
  36. package/dist/chat/backends.cjs +3524 -0
  37. package/dist/chat/backends.cjs.map +1 -0
  38. package/dist/chat/backends.d.cts +66 -0
  39. package/dist/chat/backends.d.ts +66 -0
  40. package/dist/chat/backends.js +3512 -0
  41. package/dist/chat/backends.js.map +1 -0
  42. package/dist/chat/context.cjs +280 -0
  43. package/dist/chat/context.cjs.map +1 -0
  44. package/dist/chat/context.d.cts +191 -0
  45. package/dist/chat/context.d.ts +191 -0
  46. package/dist/chat/context.js +277 -0
  47. package/dist/chat/context.js.map +1 -0
  48. package/dist/chat/core.cjs +305 -0
  49. package/dist/chat/core.cjs.map +1 -0
  50. package/dist/chat/core.d.cts +84 -0
  51. package/dist/chat/core.d.ts +84 -0
  52. package/dist/chat/core.js +282 -0
  53. package/dist/chat/core.js.map +1 -0
  54. package/dist/chat/errors.cjs +273 -0
  55. package/dist/chat/errors.cjs.map +1 -0
  56. package/dist/chat/errors.d.cts +97 -0
  57. package/dist/chat/errors.d.ts +97 -0
  58. package/dist/chat/errors.js +266 -0
  59. package/dist/chat/errors.js.map +1 -0
  60. package/dist/chat/events.cjs +203 -0
  61. package/dist/chat/events.cjs.map +1 -0
  62. package/dist/chat/events.d.cts +245 -0
  63. package/dist/chat/events.d.ts +245 -0
  64. package/dist/chat/events.js +196 -0
  65. package/dist/chat/events.js.map +1 -0
  66. package/dist/chat/index.cjs +5550 -0
  67. package/dist/chat/index.cjs.map +1 -0
  68. package/dist/chat/index.d.cts +77 -0
  69. package/dist/chat/index.d.ts +77 -0
  70. package/dist/chat/index.js +5505 -0
  71. package/dist/chat/index.js.map +1 -0
  72. package/dist/chat/react/theme.css +2517 -0
  73. package/dist/chat/react.cjs +3589 -0
  74. package/dist/chat/react.cjs.map +1 -0
  75. package/dist/chat/react.d.cts +1088 -0
  76. package/dist/chat/react.d.ts +1088 -0
  77. package/dist/chat/react.js +3547 -0
  78. package/dist/chat/react.js.map +1 -0
  79. package/dist/chat/runtime.cjs +1245 -0
  80. package/dist/chat/runtime.cjs.map +1 -0
  81. package/dist/chat/runtime.d.cts +182 -0
  82. package/dist/chat/runtime.d.ts +182 -0
  83. package/dist/chat/runtime.js +1243 -0
  84. package/dist/chat/runtime.js.map +1 -0
  85. package/dist/chat/server.cjs +2668 -0
  86. package/dist/chat/server.cjs.map +1 -0
  87. package/dist/chat/server.d.cts +648 -0
  88. package/dist/chat/server.d.ts +648 -0
  89. package/dist/chat/server.js +2628 -0
  90. package/dist/chat/server.js.map +1 -0
  91. package/dist/chat/sessions.cjs +380 -0
  92. package/dist/chat/sessions.cjs.map +1 -0
  93. package/dist/chat/sessions.d.cts +158 -0
  94. package/dist/chat/sessions.d.ts +158 -0
  95. package/dist/chat/sessions.js +376 -0
  96. package/dist/chat/sessions.js.map +1 -0
  97. package/dist/chat/sqlite.cjs +441 -0
  98. package/dist/chat/sqlite.cjs.map +1 -0
  99. package/dist/chat/sqlite.d.cts +128 -0
  100. package/dist/chat/sqlite.d.ts +128 -0
  101. package/dist/chat/sqlite.js +435 -0
  102. package/dist/chat/sqlite.js.map +1 -0
  103. package/dist/chat/state.cjs +190 -0
  104. package/dist/chat/state.cjs.map +1 -0
  105. package/dist/chat/state.d.cts +95 -0
  106. package/dist/chat/state.d.ts +95 -0
  107. package/dist/chat/state.js +180 -0
  108. package/dist/chat/state.js.map +1 -0
  109. package/dist/chat/storage.cjs +249 -0
  110. package/dist/chat/storage.cjs.map +1 -0
  111. package/dist/chat/storage.d.cts +197 -0
  112. package/dist/chat/storage.d.ts +197 -0
  113. package/dist/chat/storage.js +245 -0
  114. package/dist/chat/storage.js.map +1 -0
  115. package/dist/errors-C-so0M4t.d.cts +33 -0
  116. package/dist/errors-C-so0M4t.d.ts +33 -0
  117. package/dist/errors-CmVvczxZ.d.cts +28 -0
  118. package/dist/errors-CmVvczxZ.d.ts +28 -0
  119. package/dist/in-process-transport-C1JnJGVR.d.ts +228 -0
  120. package/dist/in-process-transport-C7DSqPyX.d.cts +228 -0
  121. package/dist/index.cjs +365 -59
  122. package/dist/index.cjs.map +1 -1
  123. package/dist/index.d.cts +322 -125
  124. package/dist/index.d.ts +322 -125
  125. package/dist/index.js +359 -60
  126. package/dist/index.js.map +1 -1
  127. package/dist/provider-types-PTSlRPNB.d.cts +39 -0
  128. package/dist/provider-types-PTSlRPNB.d.ts +39 -0
  129. package/dist/refresh-manager-B81PpYBr.d.cts +153 -0
  130. package/dist/refresh-manager-Dlv_iNZi.d.ts +153 -0
  131. package/dist/testing.cjs +383 -0
  132. package/dist/testing.cjs.map +1 -0
  133. package/dist/testing.d.cts +132 -0
  134. package/dist/testing.d.ts +132 -0
  135. package/dist/testing.js +377 -0
  136. package/dist/testing.js.map +1 -0
  137. package/dist/token-store-CSUBgYwn.d.ts +48 -0
  138. package/dist/token-store-CuC4hB9Z.d.cts +48 -0
  139. package/dist/transport-Cdh3M0tS.d.cts +68 -0
  140. package/dist/transport-Ciap4PWK.d.ts +68 -0
  141. package/dist/types-4vbcmPTp.d.cts +143 -0
  142. package/dist/types-BxggH0Yh.d.ts +143 -0
  143. package/dist/types-DRgd_9R7.d.cts +363 -0
  144. package/dist/types-ajANVzf7.d.ts +363 -0
  145. package/package.json +178 -6
@@ -0,0 +1,3589 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/chat/react/ChatProvider.ts
6
+ var ChatRuntimeContext = react.createContext(null);
7
+ function ChatProvider({ runtime, children }) {
8
+ return react.createElement(ChatRuntimeContext.Provider, { value: runtime }, children);
9
+ }
10
+ function useChatRuntime() {
11
+ const runtime = react.useContext(ChatRuntimeContext);
12
+ if (!runtime) {
13
+ throw new Error("useChatRuntime must be used within a ChatProvider");
14
+ }
15
+ return runtime;
16
+ }
17
+
18
+ // src/chat/types.ts
19
+ function createChatId() {
20
+ return crypto.randomUUID();
21
+ }
22
+ function isObservableSession(session) {
23
+ return "subscribe" in session && typeof session.subscribe === "function" && "getSnapshot" in session && typeof session.getSnapshot === "function";
24
+ }
25
+
26
+ // src/chat/bridge.ts
27
+ function chatEventToAgentEvent(event) {
28
+ switch (event.type) {
29
+ case "message:delta":
30
+ return { type: "text_delta", text: event.text };
31
+ case "thinking:start":
32
+ return { type: "thinking_start" };
33
+ case "thinking:delta":
34
+ return { type: "thinking_delta", text: event.text };
35
+ case "thinking:end":
36
+ return { type: "thinking_end" };
37
+ case "tool:start":
38
+ return {
39
+ type: "tool_call_start",
40
+ toolCallId: event.toolCallId,
41
+ toolName: event.toolName,
42
+ args: event.args
43
+ };
44
+ case "tool:complete":
45
+ return {
46
+ type: "tool_call_end",
47
+ toolCallId: event.toolCallId,
48
+ toolName: event.toolName,
49
+ result: event.result
50
+ };
51
+ case "error":
52
+ return { type: "error", error: event.error, recoverable: event.recoverable, code: event.code };
53
+ default:
54
+ return null;
55
+ }
56
+ }
57
+
58
+ // src/chat/accumulator.ts
59
+ var MessageAccumulator = class {
60
+ messageId;
61
+ parts = [];
62
+ status = "pending";
63
+ currentTextPart = null;
64
+ currentReasoningPart = null;
65
+ toolCallParts = /* @__PURE__ */ new Map();
66
+ _finalized = false;
67
+ constructor(messageId) {
68
+ this.messageId = messageId ?? createChatId();
69
+ }
70
+ /** Get current message ID */
71
+ get id() {
72
+ return this.messageId;
73
+ }
74
+ /**
75
+ * Apply an AgentEvent to accumulate into the message
76
+ * @param event - AgentEvent to process
77
+ * @throws Error if accumulator is already finalized
78
+ */
79
+ apply(event) {
80
+ if (this._finalized) throw new Error("Cannot apply events to finalized accumulator");
81
+ if (this.status === "pending") {
82
+ this.status = "streaming";
83
+ }
84
+ switch (event.type) {
85
+ case "text_delta":
86
+ this.handleTextDelta(event.text);
87
+ break;
88
+ case "thinking_start":
89
+ this.finalizeCurrentText();
90
+ this.currentReasoningPart = { type: "reasoning", text: "", status: "streaming" };
91
+ this.parts.push(this.currentReasoningPart);
92
+ break;
93
+ case "thinking_delta":
94
+ if (this.currentReasoningPart) {
95
+ this.currentReasoningPart.text += event.text;
96
+ }
97
+ break;
98
+ case "thinking_end":
99
+ if (this.currentReasoningPart) {
100
+ this.currentReasoningPart.status = "complete";
101
+ this.currentReasoningPart = null;
102
+ }
103
+ break;
104
+ case "tool_call_start": {
105
+ this.finalizeCurrentText();
106
+ const toolPart = {
107
+ type: "tool_call",
108
+ toolCallId: event.toolCallId,
109
+ name: event.toolName,
110
+ args: event.args,
111
+ status: "running"
112
+ };
113
+ this.toolCallParts.set(event.toolCallId, toolPart);
114
+ this.parts.push(toolPart);
115
+ break;
116
+ }
117
+ case "tool_call_end": {
118
+ const existing = this.toolCallParts.get(event.toolCallId);
119
+ if (existing) {
120
+ existing.result = event.result;
121
+ existing.status = "complete";
122
+ }
123
+ break;
124
+ }
125
+ case "error":
126
+ this.status = "error";
127
+ break;
128
+ }
129
+ }
130
+ handleTextDelta(text) {
131
+ if (!this.currentTextPart) {
132
+ this.currentTextPart = { type: "text", text: "", status: "streaming" };
133
+ this.parts.push(this.currentTextPart);
134
+ }
135
+ this.currentTextPart.text += text;
136
+ }
137
+ finalizeCurrentText() {
138
+ if (this.currentTextPart) {
139
+ this.currentTextPart.status = "complete";
140
+ this.currentTextPart = null;
141
+ }
142
+ }
143
+ /**
144
+ * Get a snapshot of the current accumulated message (for streaming UI)
145
+ * @returns ChatMessage with current parts and "streaming" status
146
+ */
147
+ snapshot() {
148
+ const now = (/* @__PURE__ */ new Date()).toISOString();
149
+ return {
150
+ id: this.messageId,
151
+ role: "assistant",
152
+ parts: this.parts.map((p) => ({ ...p })),
153
+ status: this.status === "pending" ? "pending" : "streaming",
154
+ createdAt: now,
155
+ updatedAt: now
156
+ };
157
+ }
158
+ /**
159
+ * Finalize the accumulator and return the complete ChatMessage
160
+ * @returns Completed ChatMessage with all parts finalized
161
+ * @throws Error if accumulator is already finalized
162
+ */
163
+ finalize() {
164
+ if (this._finalized) throw new Error("Accumulator already finalized");
165
+ this._finalized = true;
166
+ this.finalizeCurrentText();
167
+ if (this.currentReasoningPart) {
168
+ this.currentReasoningPart.status = "complete";
169
+ this.currentReasoningPart = null;
170
+ }
171
+ for (const [, toolPart] of this.toolCallParts) {
172
+ if (toolPart.status === "running" || toolPart.status === "pending") {
173
+ toolPart.status = "error";
174
+ }
175
+ }
176
+ if (this.status !== "error" && this.status !== "cancelled") {
177
+ this.status = "complete";
178
+ }
179
+ const now = (/* @__PURE__ */ new Date()).toISOString();
180
+ return {
181
+ id: this.messageId,
182
+ role: "assistant",
183
+ parts: this.parts,
184
+ status: this.status,
185
+ createdAt: now,
186
+ updatedAt: now
187
+ };
188
+ }
189
+ /** Check if the accumulator has been finalized */
190
+ get finalized() {
191
+ return this._finalized;
192
+ }
193
+ };
194
+
195
+ // src/chat/react/useChat.ts
196
+ function useChat(options = {}) {
197
+ const runtime = useChatRuntime();
198
+ const [sessionId, setSessionId] = react.useState(
199
+ options.sessionId ?? null
200
+ );
201
+ const [messages, setMessages] = react.useState([]);
202
+ const [isGenerating, setIsGenerating] = react.useState(false);
203
+ const [status, setStatus] = react.useState("idle");
204
+ const [error, setError] = react.useState(null);
205
+ const [usage, setUsage] = react.useState(null);
206
+ const generatingRef = react.useRef(false);
207
+ const lastUserMessageRef = react.useRef(null);
208
+ const onErrorRef = react.useRef(options.onError);
209
+ onErrorRef.current = options.onError;
210
+ const dismissTimerRef = react.useRef(null);
211
+ const autoDismissMs = options.autoDismissMs ?? 0;
212
+ react.useEffect(() => {
213
+ if (!error || autoDismissMs <= 0) return;
214
+ dismissTimerRef.current = setTimeout(() => {
215
+ setError(null);
216
+ dismissTimerRef.current = null;
217
+ }, autoDismissMs);
218
+ return () => {
219
+ if (dismissTimerRef.current) {
220
+ clearTimeout(dismissTimerRef.current);
221
+ dismissTimerRef.current = null;
222
+ }
223
+ };
224
+ }, [error, autoDismissMs]);
225
+ react.useEffect(() => {
226
+ if (!sessionId) {
227
+ setMessages([]);
228
+ return;
229
+ }
230
+ runtime.getSession(sessionId).then((session) => {
231
+ if (session) {
232
+ setMessages([...session.messages]);
233
+ }
234
+ });
235
+ }, [sessionId, runtime]);
236
+ react.useEffect(() => {
237
+ return runtime.onSessionChange(() => {
238
+ const activeId = runtime.activeSessionId;
239
+ if (activeId && activeId !== sessionId) {
240
+ setSessionId(activeId);
241
+ setError(null);
242
+ }
243
+ });
244
+ }, [runtime, sessionId]);
245
+ const ensureSession = react.useCallback(async () => {
246
+ if (sessionId) return sessionId;
247
+ const session = await runtime.createSession({
248
+ config: { model: "", backend: "" }
249
+ });
250
+ setSessionId(session.id);
251
+ return session.id;
252
+ }, [sessionId, runtime]);
253
+ const sendMessage = react.useCallback(
254
+ async (content) => {
255
+ if (generatingRef.current) return;
256
+ lastUserMessageRef.current = content;
257
+ setError(null);
258
+ generatingRef.current = true;
259
+ setIsGenerating(true);
260
+ setStatus("streaming");
261
+ const accumulator = new MessageAccumulator();
262
+ let hasEvents = false;
263
+ try {
264
+ const sid = await ensureSession();
265
+ const now = (/* @__PURE__ */ new Date()).toISOString();
266
+ const userMsg = {
267
+ id: crypto.randomUUID(),
268
+ role: "user",
269
+ parts: [{ type: "text", text: content, status: "complete" }],
270
+ status: "complete",
271
+ createdAt: now,
272
+ updatedAt: now
273
+ };
274
+ setMessages((prev) => [...prev, userMsg]);
275
+ for await (const event of runtime.send(sid, content)) {
276
+ if (event.type === "usage") {
277
+ setUsage({
278
+ promptTokens: event.promptTokens,
279
+ completionTokens: event.completionTokens,
280
+ totalTokens: event.promptTokens + event.completionTokens,
281
+ model: event.model
282
+ });
283
+ }
284
+ const agentEvent = chatEventToAgentEvent(event);
285
+ if (agentEvent) {
286
+ accumulator.apply(agentEvent);
287
+ hasEvents = true;
288
+ const snapshot = accumulator.snapshot();
289
+ setMessages((prev) => {
290
+ const last = prev[prev.length - 1];
291
+ if (last && last.id === snapshot.id) {
292
+ return [...prev.slice(0, -1), snapshot];
293
+ }
294
+ return [...prev, snapshot];
295
+ });
296
+ }
297
+ }
298
+ const session = await runtime.getSession(sid);
299
+ if (session) {
300
+ setMessages([...session.messages]);
301
+ } else if (hasEvents) {
302
+ const final = accumulator.finalize();
303
+ setMessages((prev) => {
304
+ const last = prev[prev.length - 1];
305
+ if (last && last.id === final.id) {
306
+ return [...prev.slice(0, -1), final];
307
+ }
308
+ return [...prev, final];
309
+ });
310
+ }
311
+ } catch (err) {
312
+ const e = err instanceof Error ? err : new Error(String(err));
313
+ setError(e);
314
+ onErrorRef.current?.(e);
315
+ if (hasEvents && !accumulator.finalized) {
316
+ accumulator.apply({ type: "error", error: e.message, recoverable: false });
317
+ const errorSnapshot = accumulator.finalize();
318
+ setMessages((prev) => {
319
+ const last = prev[prev.length - 1];
320
+ if (last && last.id === errorSnapshot.id) {
321
+ return [...prev.slice(0, -1), errorSnapshot];
322
+ }
323
+ return prev;
324
+ });
325
+ }
326
+ } finally {
327
+ generatingRef.current = false;
328
+ setIsGenerating(false);
329
+ setStatus(runtime.status);
330
+ }
331
+ },
332
+ [ensureSession, runtime]
333
+ );
334
+ const stop = react.useCallback(() => {
335
+ runtime.abort();
336
+ }, [runtime]);
337
+ const clearError = react.useCallback(() => {
338
+ setError(null);
339
+ }, []);
340
+ const retryLastMessage = react.useCallback(async () => {
341
+ if (!lastUserMessageRef.current || generatingRef.current) return;
342
+ await sendMessage(lastUserMessageRef.current);
343
+ }, [sendMessage]);
344
+ const newSession = react.useCallback(async () => {
345
+ const session = await runtime.createSession({
346
+ config: { model: "", backend: "" }
347
+ });
348
+ setSessionId(session.id);
349
+ setMessages([]);
350
+ setError(null);
351
+ lastUserMessageRef.current = null;
352
+ return session.id;
353
+ }, [runtime]);
354
+ return {
355
+ sessionId,
356
+ messages,
357
+ sendMessage,
358
+ stop,
359
+ isGenerating,
360
+ status,
361
+ error,
362
+ clearError,
363
+ retryLastMessage,
364
+ newSession,
365
+ usage
366
+ };
367
+ }
368
+ var EMPTY_MESSAGES = [];
369
+ function useMessages(options) {
370
+ const runtime = useChatRuntime();
371
+ const { sessionId } = options;
372
+ const sessionRef = react.useRef(null);
373
+ const messagesRef = react.useRef(EMPTY_MESSAGES);
374
+ const isLoadedRef = react.useRef(false);
375
+ const versionRef = react.useRef(0);
376
+ const listenersRef = react.useRef(/* @__PURE__ */ new Set());
377
+ const emitChange = react.useCallback(() => {
378
+ versionRef.current++;
379
+ for (const listener of listenersRef.current) {
380
+ listener();
381
+ }
382
+ }, []);
383
+ const subscribe = react.useCallback(
384
+ (callback) => {
385
+ listenersRef.current.add(callback);
386
+ return () => {
387
+ listenersRef.current.delete(callback);
388
+ };
389
+ },
390
+ []
391
+ );
392
+ const getSnapshot = react.useCallback(() => {
393
+ return messagesRef.current;
394
+ }, []);
395
+ react.useEffect(() => {
396
+ let cancelled = false;
397
+ let unsubscribe;
398
+ let pollInterval;
399
+ async function load() {
400
+ const session = await runtime.getSession(sessionId);
401
+ if (cancelled) return;
402
+ if (!session) {
403
+ sessionRef.current = null;
404
+ messagesRef.current = EMPTY_MESSAGES;
405
+ isLoadedRef.current = false;
406
+ emitChange();
407
+ return;
408
+ }
409
+ sessionRef.current = session;
410
+ messagesRef.current = session.messages;
411
+ isLoadedRef.current = true;
412
+ emitChange();
413
+ if (isObservableSession(session)) {
414
+ unsubscribe = session.subscribe(() => {
415
+ const snapshot = session.getSnapshot();
416
+ messagesRef.current = snapshot.messages;
417
+ emitChange();
418
+ });
419
+ } else {
420
+ pollInterval = setInterval(async () => {
421
+ if (cancelled) return;
422
+ const updated = await runtime.getSession(sessionId);
423
+ if (cancelled || !updated) return;
424
+ if (updated.messages.length !== messagesRef.current.length) {
425
+ messagesRef.current = updated.messages;
426
+ emitChange();
427
+ }
428
+ }, 500);
429
+ }
430
+ }
431
+ load();
432
+ return () => {
433
+ cancelled = true;
434
+ unsubscribe?.();
435
+ if (pollInterval) clearInterval(pollInterval);
436
+ };
437
+ }, [sessionId, runtime, emitChange]);
438
+ const messages = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
439
+ return {
440
+ messages,
441
+ isLoaded: isLoadedRef.current
442
+ };
443
+ }
444
+ function toSessionInfo(s) {
445
+ return {
446
+ id: s.id,
447
+ title: s.title,
448
+ status: s.status,
449
+ messageCount: s.metadata.messageCount,
450
+ lastMessage: s.messages[s.messages.length - 1],
451
+ createdAt: s.createdAt,
452
+ updatedAt: s.updatedAt
453
+ };
454
+ }
455
+ function useSessions() {
456
+ const runtime = useChatRuntime();
457
+ const [sessions, setSessions] = react.useState([]);
458
+ const [loading, setLoading] = react.useState(true);
459
+ const [error, setError] = react.useState(null);
460
+ const fetchSessions = react.useCallback(async () => {
461
+ try {
462
+ const list = await runtime.listSessions();
463
+ setSessions(list.map(toSessionInfo));
464
+ setError(null);
465
+ } catch (err) {
466
+ setError(err instanceof Error ? err : new Error(String(err)));
467
+ } finally {
468
+ setLoading(false);
469
+ }
470
+ }, [runtime]);
471
+ react.useEffect(() => {
472
+ fetchSessions();
473
+ }, [fetchSessions]);
474
+ react.useEffect(() => {
475
+ return runtime.onSessionChange(() => {
476
+ fetchSessions();
477
+ });
478
+ }, [runtime, fetchSessions]);
479
+ const refresh = react.useCallback(() => {
480
+ setLoading(true);
481
+ fetchSessions();
482
+ }, [fetchSessions]);
483
+ return { sessions, loading, error, refresh };
484
+ }
485
+ function parseBlocks(text) {
486
+ const tokens = [];
487
+ const lines = text.split("\n");
488
+ let i = 0;
489
+ while (i < lines.length) {
490
+ const line = lines[i];
491
+ const fenceMatch = line.match(/^```(\w*)/);
492
+ if (fenceMatch) {
493
+ const language = fenceMatch[1] || void 0;
494
+ const codeLines = [];
495
+ i++;
496
+ while (i < lines.length && !lines[i].startsWith("```")) {
497
+ codeLines.push(lines[i]);
498
+ i++;
499
+ }
500
+ i++;
501
+ tokens.push({ type: "code_block", code: codeLines.join("\n"), language });
502
+ continue;
503
+ }
504
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
505
+ if (headingMatch) {
506
+ tokens.push({ type: "heading", level: headingMatch[1].length, content: headingMatch[2] });
507
+ i++;
508
+ continue;
509
+ }
510
+ if (line.startsWith("> ")) {
511
+ const quoteLines = [];
512
+ while (i < lines.length && lines[i].startsWith("> ")) {
513
+ quoteLines.push(lines[i].slice(2));
514
+ i++;
515
+ }
516
+ tokens.push({ type: "blockquote", content: quoteLines.join("\n") });
517
+ continue;
518
+ }
519
+ if (/^[-*]\s+/.test(line)) {
520
+ const items = [];
521
+ while (i < lines.length && /^[-*]\s+/.test(lines[i])) {
522
+ items.push(lines[i].replace(/^[-*]\s+/, ""));
523
+ i++;
524
+ }
525
+ tokens.push({ type: "list", ordered: false, items });
526
+ continue;
527
+ }
528
+ if (/^\d+\.\s+/.test(line)) {
529
+ const items = [];
530
+ while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
531
+ items.push(lines[i].replace(/^\d+\.\s+/, ""));
532
+ i++;
533
+ }
534
+ tokens.push({ type: "list", ordered: true, items });
535
+ continue;
536
+ }
537
+ if (line.trim() === "") {
538
+ i++;
539
+ continue;
540
+ }
541
+ const paraLines = [];
542
+ while (i < lines.length && lines[i].trim() !== "" && !lines[i].match(/^(#{1,6}\s|```|>\s|[-*]\s|\d+\.\s)/)) {
543
+ paraLines.push(lines[i]);
544
+ i++;
545
+ }
546
+ if (paraLines.length > 0) {
547
+ tokens.push({ type: "paragraph", content: paraLines.join("\n") });
548
+ }
549
+ }
550
+ return tokens;
551
+ }
552
+ function parseInline(text, props) {
553
+ const nodes = [];
554
+ const regex = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*]+\*)|(_[^_]+_)|(\[[^\]]+\]\([^)]+\))/g;
555
+ let lastIndex = 0;
556
+ let match;
557
+ while ((match = regex.exec(text)) !== null) {
558
+ if (match.index > lastIndex) {
559
+ nodes.push(text.slice(lastIndex, match.index));
560
+ }
561
+ const fragment = match[0];
562
+ if (fragment.startsWith("`")) {
563
+ const code = fragment.slice(1, -1);
564
+ nodes.push(react.createElement("code", { key: `ic-${match.index}`, "data-md-inline-code": true }, code));
565
+ } else if (fragment.startsWith("**")) {
566
+ const content = fragment.slice(2, -2);
567
+ nodes.push(react.createElement("strong", { key: `b-${match.index}` }, content));
568
+ } else if (fragment.startsWith("*") || fragment.startsWith("_")) {
569
+ const content = fragment.slice(1, -1);
570
+ nodes.push(react.createElement("em", { key: `i-${match.index}` }, content));
571
+ } else if (fragment.startsWith("[")) {
572
+ const linkMatch = fragment.match(/\[([^\]]+)\]\(([^)]+)\)/);
573
+ if (linkMatch) {
574
+ nodes.push(
575
+ props?.renderLink ? props.renderLink(linkMatch[2], linkMatch[1]) : react.createElement("a", { key: `a-${match.index}`, href: linkMatch[2] }, linkMatch[1])
576
+ );
577
+ }
578
+ }
579
+ lastIndex = match.index + fragment.length;
580
+ }
581
+ if (lastIndex < text.length) {
582
+ nodes.push(text.slice(lastIndex));
583
+ }
584
+ return nodes;
585
+ }
586
+ function renderBlock(token, index, props) {
587
+ switch (token.type) {
588
+ case "heading":
589
+ return react.createElement(
590
+ `h${token.level}`,
591
+ { key: index, "data-md-heading": true },
592
+ ...parseInline(token.content, props)
593
+ );
594
+ case "paragraph":
595
+ return react.createElement(
596
+ "p",
597
+ { key: index, "data-md-paragraph": true },
598
+ ...parseInline(token.content, props)
599
+ );
600
+ case "code_block":
601
+ if (props.renderCode) {
602
+ return props.renderCode(token.code, token.language);
603
+ }
604
+ return react.createElement(
605
+ "pre",
606
+ { key: index, "data-md-code-block": true },
607
+ react.createElement(
608
+ "code",
609
+ { className: token.language ? `language-${token.language}` : void 0 },
610
+ token.code
611
+ )
612
+ );
613
+ case "blockquote":
614
+ return react.createElement(
615
+ "blockquote",
616
+ { key: index, "data-md-blockquote": true },
617
+ ...parseInline(token.content, props)
618
+ );
619
+ case "list": {
620
+ const tag = token.ordered ? "ol" : "ul";
621
+ const items = token.items.map(
622
+ (item, i) => react.createElement("li", { key: i }, ...parseInline(item, props))
623
+ );
624
+ return react.createElement(tag, { key: index, "data-md-list": true }, ...items);
625
+ }
626
+ }
627
+ }
628
+ function MarkdownRenderer(props) {
629
+ const tokens = parseBlocks(props.content);
630
+ const children = tokens.map((token, i) => renderBlock(token, i, props));
631
+ return react.createElement("div", { "data-md-root": true }, ...children);
632
+ }
633
+ function ThinkingBlock({ text, isStreaming, defaultOpen }) {
634
+ const attrs = {
635
+ "data-thinking": "true"
636
+ };
637
+ if (isStreaming) {
638
+ attrs["data-streaming"] = "true";
639
+ }
640
+ if (defaultOpen) {
641
+ attrs.open = true;
642
+ }
643
+ return react.createElement(
644
+ "details",
645
+ attrs,
646
+ react.createElement("summary", null, isStreaming ? "Thinking..." : "Reasoning"),
647
+ react.createElement("div", null, text)
648
+ );
649
+ }
650
+ function ToolCallView({ part, onApprove, onDeny, renderArgs, renderResult }) {
651
+ const children = [];
652
+ children.push(
653
+ react.createElement(
654
+ "div",
655
+ { key: "header", "data-tool-header": "true" },
656
+ react.createElement("span", { "data-tool-label": "name" }, part.name),
657
+ react.createElement("span", { "data-tool-label": "status" }, part.status)
658
+ )
659
+ );
660
+ if (part.args !== void 0) {
661
+ children.push(
662
+ renderArgs ? renderArgs(part.args) : react.createElement(
663
+ "details",
664
+ { key: "args", "data-tool-details": "args" },
665
+ react.createElement("summary", null, "Arguments"),
666
+ react.createElement("pre", { "data-tool-label": "args" }, JSON.stringify(part.args, null, 2))
667
+ )
668
+ );
669
+ }
670
+ if (part.result !== void 0) {
671
+ children.push(
672
+ renderResult ? renderResult(part.result) : react.createElement(
673
+ "details",
674
+ { key: "result", "data-tool-details": "result", open: true },
675
+ react.createElement("summary", null, "Result"),
676
+ react.createElement("pre", { "data-tool-label": "result" }, JSON.stringify(part.result, null, 2))
677
+ )
678
+ );
679
+ }
680
+ if (part.error) {
681
+ children.push(
682
+ react.createElement("span", { key: "error", "data-tool-label": "error", role: "alert" }, part.error)
683
+ );
684
+ }
685
+ if (part.status === "requires_approval") {
686
+ children.push(
687
+ react.createElement(
688
+ "div",
689
+ { key: "actions", "data-tool-actions": "true" },
690
+ react.createElement("button", { key: "approve", onClick: onApprove, "data-action": "approve" }, "Approve"),
691
+ react.createElement("button", { key: "deny", onClick: onDeny, "data-action": "deny" }, "Deny")
692
+ )
693
+ );
694
+ }
695
+ return react.createElement(
696
+ "div",
697
+ {
698
+ "data-tool-status": part.status,
699
+ "data-tool-name": part.name
700
+ },
701
+ ...children
702
+ );
703
+ }
704
+
705
+ // src/chat/react/Message.ts
706
+ function defaultRenderText(part, index) {
707
+ return react.createElement(
708
+ "div",
709
+ { key: index, "data-part": "text" },
710
+ react.createElement(MarkdownRenderer, { content: part.text })
711
+ );
712
+ }
713
+ function defaultRenderReasoning(part, index) {
714
+ return react.createElement(ThinkingBlock, {
715
+ key: index,
716
+ text: part.text,
717
+ isStreaming: part.status === "streaming"
718
+ });
719
+ }
720
+ function defaultRenderToolCall(part, index) {
721
+ return react.createElement(
722
+ "div",
723
+ { key: index, "data-part": "tool_call" },
724
+ react.createElement(ToolCallView, { part })
725
+ );
726
+ }
727
+ function defaultRenderSource(part, index) {
728
+ return react.createElement("a", { key: index, href: part.url, "data-part": "source" }, part.title ?? part.url);
729
+ }
730
+ function defaultRenderFile(part, index) {
731
+ return react.createElement("span", { key: index, "data-part": "file" }, part.name);
732
+ }
733
+ function renderPart(props, part, index) {
734
+ switch (part.type) {
735
+ case "text":
736
+ return (props.renderText ?? defaultRenderText)(part, index);
737
+ case "reasoning":
738
+ return (props.renderReasoning ?? defaultRenderReasoning)(part, index);
739
+ case "tool_call":
740
+ return (props.renderToolCall ?? defaultRenderToolCall)(part, index);
741
+ case "source":
742
+ return (props.renderSource ?? defaultRenderSource)(part, index);
743
+ case "file":
744
+ return (props.renderFile ?? defaultRenderFile)(part, index);
745
+ }
746
+ }
747
+ function Message(props) {
748
+ const { message } = props;
749
+ const children = message.parts.map((part, i) => renderPart(props, part, i));
750
+ return react.createElement(
751
+ "div",
752
+ {
753
+ "data-role": message.role,
754
+ "data-status": message.status
755
+ },
756
+ ...children
757
+ );
758
+ }
759
+ function useToolApproval(messages, onApprove, onDeny) {
760
+ const pendingRequests = react.useMemo(() => {
761
+ const requests = [];
762
+ for (const msg of messages) {
763
+ for (const part of msg.parts) {
764
+ if (part.type === "tool_call" && part.status === "requires_approval") {
765
+ requests.push({
766
+ toolCallId: part.toolCallId,
767
+ toolName: part.name,
768
+ toolArgs: part.args ?? {},
769
+ messageId: msg.id
770
+ });
771
+ }
772
+ }
773
+ }
774
+ return requests;
775
+ }, [messages]);
776
+ const approve = react.useCallback((toolCallId) => {
777
+ onApprove?.(toolCallId);
778
+ }, [onApprove]);
779
+ const deny = react.useCallback((toolCallId) => {
780
+ onDeny?.(toolCallId);
781
+ }, [onDeny]);
782
+ return { pendingRequests, approve, deny };
783
+ }
784
+ var ThreadSlotsContext = react.createContext(null);
785
+ function ThreadProvider({
786
+ children,
787
+ renderMessage,
788
+ renderToolCall,
789
+ renderThinkingBlock
790
+ }) {
791
+ const value = { renderMessage, renderToolCall, renderThinkingBlock };
792
+ return react.createElement(ThreadSlotsContext.Provider, { value }, children);
793
+ }
794
+ function useThreadSlots() {
795
+ const ctx = react.useContext(ThreadSlotsContext);
796
+ if (!ctx) {
797
+ throw new Error("useThreadSlots must be used within a ThreadProvider");
798
+ }
799
+ return ctx;
800
+ }
801
+ function useOptionalThreadSlots() {
802
+ return react.useContext(ThreadSlotsContext);
803
+ }
804
+
805
+ // src/chat/react/Thread.ts
806
+ function Thread({
807
+ messages,
808
+ isGenerating,
809
+ autoScroll = true,
810
+ className
811
+ }) {
812
+ const sentinelRef = react.useRef(null);
813
+ const containerRef = react.useRef(null);
814
+ const [userScrolledUp, setUserScrolledUp] = react.useState(false);
815
+ const isScrollingProgrammatically = react.useRef(false);
816
+ const handleScroll = react.useCallback(() => {
817
+ if (isScrollingProgrammatically.current) return;
818
+ const el = containerRef.current;
819
+ if (!el) return;
820
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 1;
821
+ setUserScrolledUp(!atBottom);
822
+ }, []);
823
+ const scrollToBottom = react.useCallback(() => {
824
+ isScrollingProgrammatically.current = true;
825
+ sentinelRef.current?.scrollIntoView({ behavior: "smooth" });
826
+ setUserScrolledUp(false);
827
+ setTimeout(() => {
828
+ isScrollingProgrammatically.current = false;
829
+ }, 500);
830
+ }, []);
831
+ react.useEffect(() => {
832
+ if (!autoScroll || userScrolledUp) return;
833
+ sentinelRef.current?.scrollIntoView({ behavior: "smooth" });
834
+ }, [messages, autoScroll, userScrolledUp]);
835
+ const slots = useOptionalThreadSlots();
836
+ const attrs = { "data-thread": "true", className };
837
+ if (isGenerating) {
838
+ attrs["data-thread-loading"] = "true";
839
+ }
840
+ attrs.ref = containerRef;
841
+ attrs.onScroll = handleScroll;
842
+ const children = [];
843
+ if (messages.length === 0 && !isGenerating) {
844
+ children.push(
845
+ react.createElement(
846
+ "div",
847
+ { key: "__empty", "data-thread-empty": "true" },
848
+ "Start a conversation"
849
+ )
850
+ );
851
+ }
852
+ for (let i = 0; i < messages.length; i++) {
853
+ const msg = messages[i];
854
+ const content = slots?.renderMessage ? slots.renderMessage(msg, i) : react.createElement(Message, {
855
+ key: msg.id,
856
+ message: msg,
857
+ renderToolCall: slots?.renderToolCall,
858
+ renderReasoning: slots?.renderThinkingBlock ? (part, idx) => slots.renderThinkingBlock(part, idx) : void 0
859
+ });
860
+ children.push(
861
+ react.createElement(
862
+ "div",
863
+ { key: msg.id, "data-thread-message": "true", "data-role": msg.role },
864
+ content
865
+ )
866
+ );
867
+ }
868
+ if (isGenerating) {
869
+ children.push(
870
+ react.createElement(
871
+ "div",
872
+ { key: "__loading", "data-thread-loading-indicator": "true" },
873
+ react.createElement("span", null),
874
+ react.createElement("span", null),
875
+ react.createElement("span", null)
876
+ )
877
+ );
878
+ }
879
+ children.push(react.createElement("div", { key: "__sentinel", ref: sentinelRef }));
880
+ if (userScrolledUp) {
881
+ children.push(
882
+ react.createElement("button", {
883
+ key: "__scroll-to-bottom",
884
+ "data-action": "scroll-to-bottom",
885
+ type: "button",
886
+ onClick: scrollToBottom,
887
+ "aria-label": "Scroll to bottom"
888
+ })
889
+ );
890
+ }
891
+ return react.createElement("div", attrs, ...children);
892
+ }
893
+ function Composer({
894
+ onSend,
895
+ onStop,
896
+ isGenerating,
897
+ disabled,
898
+ placeholder = "Type a message...",
899
+ maxRows = 5,
900
+ className
901
+ }) {
902
+ const textareaRef = react.useRef(null);
903
+ const [value, setValue] = react.useState("");
904
+ const resize = react.useCallback(() => {
905
+ const el = textareaRef.current;
906
+ if (!el) return;
907
+ el.style.height = "auto";
908
+ const lineHeight = parseInt(getComputedStyle(el).lineHeight) || 20;
909
+ const maxHeight = lineHeight * maxRows;
910
+ el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
911
+ }, [maxRows]);
912
+ react.useEffect(() => {
913
+ resize();
914
+ }, [value, resize]);
915
+ const handleSend = react.useCallback(() => {
916
+ const trimmed = value.trim();
917
+ if (!trimmed || isGenerating) return;
918
+ onSend(trimmed);
919
+ setValue("");
920
+ }, [value, isGenerating, onSend]);
921
+ const handleKeyDown = react.useCallback(
922
+ (e) => {
923
+ if (e.key === "Enter" && !e.shiftKey) {
924
+ e.preventDefault();
925
+ handleSend();
926
+ }
927
+ },
928
+ [handleSend]
929
+ );
930
+ const handleChange = react.useCallback(
931
+ (e) => {
932
+ setValue(e.target.value);
933
+ },
934
+ []
935
+ );
936
+ const children = [
937
+ react.createElement(
938
+ "div",
939
+ { key: "input-wrapper", "data-input": "" },
940
+ react.createElement("textarea", {
941
+ ref: textareaRef,
942
+ value,
943
+ onChange: handleChange,
944
+ onKeyDown: handleKeyDown,
945
+ placeholder,
946
+ disabled: disabled || false,
947
+ "aria-label": "Message input",
948
+ rows: 1
949
+ }),
950
+ isGenerating ? react.createElement(
951
+ "button",
952
+ {
953
+ key: "stop",
954
+ "data-action": "stop",
955
+ onClick: onStop,
956
+ type: "button"
957
+ },
958
+ "Stop"
959
+ ) : react.createElement(
960
+ "button",
961
+ {
962
+ key: "send",
963
+ "data-action": "send",
964
+ onClick: handleSend,
965
+ disabled: !value.trim() || isGenerating,
966
+ type: "button"
967
+ },
968
+ "Send"
969
+ )
970
+ )
971
+ ];
972
+ return react.createElement(
973
+ "div",
974
+ { "data-composer": "true", className },
975
+ ...children
976
+ );
977
+ }
978
+ function isFullSession(item) {
979
+ return "messages" in item && Array.isArray(item.messages);
980
+ }
981
+ function formatRelativeTime(date) {
982
+ const now = Date.now();
983
+ const diff = now - date.getTime();
984
+ const seconds = Math.floor(diff / 1e3);
985
+ if (seconds < 60) return "now";
986
+ const minutes = Math.floor(seconds / 60);
987
+ if (minutes < 60) return `${minutes}m`;
988
+ const hours = Math.floor(minutes / 60);
989
+ if (hours < 24) return `${hours}h`;
990
+ const days = Math.floor(hours / 24);
991
+ if (days < 30) return `${days}d`;
992
+ const months = Math.floor(days / 30);
993
+ return `${months}mo`;
994
+ }
995
+ function normalizeSession(item) {
996
+ if (isFullSession(item)) {
997
+ return {
998
+ id: item.id,
999
+ title: item.title,
1000
+ status: item.status,
1001
+ messageCount: item.metadata?.messageCount ?? item.messages.length,
1002
+ lastMessage: item.messages[item.messages.length - 1],
1003
+ createdAt: item.createdAt,
1004
+ updatedAt: item.updatedAt
1005
+ };
1006
+ }
1007
+ return item;
1008
+ }
1009
+ function ThreadList({
1010
+ sessions,
1011
+ activeSessionId,
1012
+ onSelect,
1013
+ onCreate,
1014
+ onDelete,
1015
+ searchQuery,
1016
+ onSearchChange,
1017
+ className
1018
+ }) {
1019
+ const handleSearchChange = react.useCallback(
1020
+ (e) => {
1021
+ onSearchChange?.(e.target.value);
1022
+ },
1023
+ [onSearchChange]
1024
+ );
1025
+ const normalized = react.useMemo(() => sessions.map(normalizeSession), [sessions]);
1026
+ const filtered = react.useMemo(() => {
1027
+ if (!searchQuery) return normalized;
1028
+ const q = searchQuery.toLowerCase();
1029
+ return normalized.filter((s) => (s.title ?? "").toLowerCase().includes(q));
1030
+ }, [normalized, searchQuery]);
1031
+ const children = [];
1032
+ children.push(
1033
+ react.createElement(
1034
+ "div",
1035
+ { key: "header", "data-thread-list-header": "true" },
1036
+ react.createElement("input", {
1037
+ "data-thread-list-search": "true",
1038
+ value: searchQuery ?? "",
1039
+ onChange: handleSearchChange,
1040
+ placeholder: "Search..."
1041
+ }),
1042
+ react.createElement(
1043
+ "button",
1044
+ {
1045
+ "data-action": "create-session",
1046
+ onClick: onCreate,
1047
+ type: "button"
1048
+ },
1049
+ "+"
1050
+ )
1051
+ )
1052
+ );
1053
+ const items = filtered.map((session) => {
1054
+ const isActive = session.id === activeSessionId;
1055
+ const itemChildren = [
1056
+ react.createElement("span", { key: "title", "data-session-title": "true" }, session.title ?? "Untitled")
1057
+ ];
1058
+ if (session.updatedAt) {
1059
+ const timeStr = formatRelativeTime(new Date(session.updatedAt));
1060
+ itemChildren.push(
1061
+ react.createElement("span", { key: "time", "data-session-time": "true" }, timeStr)
1062
+ );
1063
+ }
1064
+ if (onDelete) {
1065
+ itemChildren.push(
1066
+ react.createElement(
1067
+ "button",
1068
+ {
1069
+ key: "delete",
1070
+ "data-action": "delete-session",
1071
+ onClick: (e) => {
1072
+ e.stopPropagation();
1073
+ onDelete(session.id);
1074
+ },
1075
+ type: "button"
1076
+ },
1077
+ "\xD7"
1078
+ )
1079
+ );
1080
+ }
1081
+ return react.createElement(
1082
+ "div",
1083
+ {
1084
+ key: session.id,
1085
+ "data-session-item": "true",
1086
+ "data-session-active": isActive ? "true" : "false",
1087
+ "data-session-status": session.status ?? "active",
1088
+ onClick: () => onSelect(session.id)
1089
+ },
1090
+ ...itemChildren
1091
+ );
1092
+ });
1093
+ children.push(
1094
+ react.createElement(
1095
+ "div",
1096
+ { key: "items", "data-thread-list-items": "true" },
1097
+ ...items
1098
+ )
1099
+ );
1100
+ return react.createElement(
1101
+ "div",
1102
+ { "data-thread-list": "true", className },
1103
+ ...children
1104
+ );
1105
+ }
1106
+ function useSSE(url, options = {}) {
1107
+ const [status, setStatus] = react.useState(url === null ? "idle" : "idle");
1108
+ const [lastEvent, setLastEvent] = react.useState(null);
1109
+ const abortRef = react.useRef(null);
1110
+ const reconnectTimerRef = react.useRef(null);
1111
+ const optionsRef = react.useRef(options);
1112
+ optionsRef.current = options;
1113
+ const disconnect = react.useCallback(() => {
1114
+ if (reconnectTimerRef.current !== null) {
1115
+ clearTimeout(reconnectTimerRef.current);
1116
+ reconnectTimerRef.current = null;
1117
+ }
1118
+ if (abortRef.current) {
1119
+ abortRef.current.abort();
1120
+ abortRef.current = null;
1121
+ }
1122
+ setStatus("closed");
1123
+ }, []);
1124
+ const connect = react.useCallback(() => {
1125
+ if (!url) return;
1126
+ if (reconnectTimerRef.current !== null) {
1127
+ clearTimeout(reconnectTimerRef.current);
1128
+ reconnectTimerRef.current = null;
1129
+ }
1130
+ if (abortRef.current) {
1131
+ abortRef.current.abort();
1132
+ }
1133
+ const controller = new AbortController();
1134
+ abortRef.current = controller;
1135
+ setStatus("connecting");
1136
+ (async () => {
1137
+ try {
1138
+ const fetchInit = {
1139
+ method: optionsRef.current.method ?? "GET",
1140
+ headers: {
1141
+ Accept: "text/event-stream",
1142
+ ...optionsRef.current.headers
1143
+ },
1144
+ signal: controller.signal
1145
+ };
1146
+ if (fetchInit.method === "POST" && optionsRef.current.body !== void 0) {
1147
+ fetchInit.headers["Content-Type"] = "application/json";
1148
+ fetchInit.body = JSON.stringify(optionsRef.current.body);
1149
+ }
1150
+ const response = await fetch(url, fetchInit);
1151
+ if (!response.ok) {
1152
+ throw new Error(`SSE request failed: ${response.status}`);
1153
+ }
1154
+ if (!response.body) {
1155
+ throw new Error("SSE response has no body");
1156
+ }
1157
+ setStatus("open");
1158
+ const reader = response.body.getReader();
1159
+ const decoder = new TextDecoder();
1160
+ let buffer = "";
1161
+ let dataLines = [];
1162
+ const dispatchEvent = () => {
1163
+ if (dataLines.length === 0) return;
1164
+ const data = dataLines.join("\n");
1165
+ dataLines = [];
1166
+ try {
1167
+ const parsed = JSON.parse(data);
1168
+ setLastEvent(parsed);
1169
+ optionsRef.current.onEvent?.(parsed);
1170
+ } catch {
1171
+ }
1172
+ };
1173
+ while (true) {
1174
+ const { done, value } = await reader.read();
1175
+ if (done) break;
1176
+ buffer += decoder.decode(value, { stream: true });
1177
+ const lines = buffer.split("\n");
1178
+ buffer = lines.pop();
1179
+ for (const line of lines) {
1180
+ if (line === "") {
1181
+ dispatchEvent();
1182
+ } else if (line.startsWith("data:")) {
1183
+ dataLines.push(line.slice(5).trimStart());
1184
+ } else if (line.startsWith("event:")) {
1185
+ }
1186
+ }
1187
+ }
1188
+ if (dataLines.length > 0) {
1189
+ dispatchEvent();
1190
+ }
1191
+ if (!controller.signal.aborted) {
1192
+ setStatus("closed");
1193
+ }
1194
+ } catch (err) {
1195
+ if (controller.signal.aborted) return;
1196
+ const error = err instanceof Error ? err : new Error(String(err));
1197
+ setStatus("error");
1198
+ optionsRef.current.onError?.(error);
1199
+ if (optionsRef.current.reconnect && !controller.signal.aborted) {
1200
+ const delay = optionsRef.current.reconnectInterval ?? 3e3;
1201
+ reconnectTimerRef.current = setTimeout(() => {
1202
+ if (abortRef.current && !abortRef.current.signal.aborted) {
1203
+ connect();
1204
+ }
1205
+ }, delay);
1206
+ }
1207
+ }
1208
+ })();
1209
+ }, [url]);
1210
+ react.useEffect(() => {
1211
+ return () => {
1212
+ if (reconnectTimerRef.current !== null) {
1213
+ clearTimeout(reconnectTimerRef.current);
1214
+ reconnectTimerRef.current = null;
1215
+ }
1216
+ if (abortRef.current) {
1217
+ abortRef.current.abort();
1218
+ abortRef.current = null;
1219
+ }
1220
+ };
1221
+ }, []);
1222
+ return { status, connect, disconnect, lastEvent };
1223
+ }
1224
+ function useModels() {
1225
+ const runtime = useChatRuntime();
1226
+ const [models, setModels] = react.useState([]);
1227
+ const [isLoading, setIsLoading] = react.useState(true);
1228
+ const [error, setError] = react.useState(null);
1229
+ const mountedRef = react.useRef(true);
1230
+ const fetchModels = react.useCallback(async () => {
1231
+ setIsLoading(true);
1232
+ setError(null);
1233
+ try {
1234
+ const result = await runtime.listModels();
1235
+ if (!mountedRef.current) return;
1236
+ const mapped = result.map((m) => ({
1237
+ id: m.id,
1238
+ name: m.name ?? m.id,
1239
+ tier: m.provider,
1240
+ provider: m.provider
1241
+ }));
1242
+ setModels(mapped);
1243
+ } catch (err) {
1244
+ if (!mountedRef.current) return;
1245
+ setError(err instanceof Error ? err : new Error(String(err)));
1246
+ } finally {
1247
+ if (mountedRef.current) {
1248
+ setIsLoading(false);
1249
+ }
1250
+ }
1251
+ }, [runtime]);
1252
+ react.useEffect(() => {
1253
+ mountedRef.current = true;
1254
+ fetchModels();
1255
+ return () => {
1256
+ mountedRef.current = false;
1257
+ };
1258
+ }, [fetchModels]);
1259
+ const search = react.useCallback(
1260
+ (query) => {
1261
+ const q = query.toLowerCase();
1262
+ return models.filter((m) => m.name.toLowerCase().includes(q));
1263
+ },
1264
+ [models]
1265
+ );
1266
+ return { models, isLoading, error, refresh: fetchModels, search };
1267
+ }
1268
+ function ModelSelector({
1269
+ models,
1270
+ selectedModel,
1271
+ onSelect,
1272
+ placeholder = "Select model",
1273
+ className,
1274
+ allowFreeText = true
1275
+ }) {
1276
+ const [open, setOpen] = react.useState(false);
1277
+ const [search, setSearch] = react.useState("");
1278
+ const [highlightIndex, setHighlightIndex] = react.useState(0);
1279
+ const [freeText, setFreeText] = react.useState(selectedModel ?? "");
1280
+ const containerRef = react.useRef(null);
1281
+ const filtered = react.useMemo(() => {
1282
+ if (!search) return models;
1283
+ const q = search.toLowerCase();
1284
+ return models.filter((m) => m.name.toLowerCase().includes(q));
1285
+ }, [models, search]);
1286
+ react.useEffect(() => {
1287
+ setHighlightIndex(0);
1288
+ }, [filtered.length]);
1289
+ const selectedInfo = react.useMemo(
1290
+ () => models.find((m) => m.id === selectedModel),
1291
+ [models, selectedModel]
1292
+ );
1293
+ const handleToggle = react.useCallback(() => {
1294
+ setOpen((prev) => {
1295
+ if (!prev) {
1296
+ setSearch("");
1297
+ setHighlightIndex(0);
1298
+ }
1299
+ return !prev;
1300
+ });
1301
+ }, []);
1302
+ const handleSelect = react.useCallback(
1303
+ (modelId) => {
1304
+ onSelect(modelId);
1305
+ setOpen(false);
1306
+ setSearch("");
1307
+ },
1308
+ [onSelect]
1309
+ );
1310
+ const handleSearchChange = react.useCallback((e) => {
1311
+ setSearch(e.target.value);
1312
+ }, []);
1313
+ const handleKeyDown = react.useCallback(
1314
+ (e) => {
1315
+ if (e.key === "ArrowDown") {
1316
+ e.preventDefault();
1317
+ setHighlightIndex((prev) => Math.min(prev + 1, filtered.length - 1));
1318
+ } else if (e.key === "ArrowUp") {
1319
+ e.preventDefault();
1320
+ setHighlightIndex((prev) => Math.max(prev - 1, 0));
1321
+ } else if (e.key === "Enter") {
1322
+ e.preventDefault();
1323
+ if (filtered[highlightIndex]) {
1324
+ handleSelect(filtered[highlightIndex].id);
1325
+ }
1326
+ } else if (e.key === "Escape") {
1327
+ e.preventDefault();
1328
+ setOpen(false);
1329
+ }
1330
+ },
1331
+ [filtered, highlightIndex, handleSelect]
1332
+ );
1333
+ if (models.length === 0 && allowFreeText) {
1334
+ return react.createElement(
1335
+ "div",
1336
+ {
1337
+ "data-model-selector": "true",
1338
+ "data-model-selector-freetext": "true",
1339
+ className,
1340
+ ref: containerRef
1341
+ },
1342
+ react.createElement("input", {
1343
+ "data-model-input": "true",
1344
+ value: freeText,
1345
+ placeholder: "Enter model name...",
1346
+ onChange: (e) => setFreeText(e.target.value),
1347
+ onKeyDown: (e) => {
1348
+ if (e.key === "Enter") {
1349
+ e.preventDefault();
1350
+ const val = freeText.trim();
1351
+ if (val) onSelect(val);
1352
+ }
1353
+ }
1354
+ }),
1355
+ react.createElement(
1356
+ "button",
1357
+ {
1358
+ type: "button",
1359
+ "data-action": "apply-model",
1360
+ onClick: () => {
1361
+ const val = freeText.trim();
1362
+ if (val) onSelect(val);
1363
+ }
1364
+ },
1365
+ "Apply"
1366
+ )
1367
+ );
1368
+ }
1369
+ const children = [];
1370
+ children.push(
1371
+ react.createElement(
1372
+ "button",
1373
+ {
1374
+ key: "trigger",
1375
+ "data-model-selector-trigger": "true",
1376
+ onClick: handleToggle,
1377
+ type: "button"
1378
+ },
1379
+ selectedInfo ? selectedInfo.name : placeholder
1380
+ )
1381
+ );
1382
+ if (open) {
1383
+ const dropdownChildren = [];
1384
+ dropdownChildren.push(
1385
+ react.createElement("input", {
1386
+ key: "search",
1387
+ "data-model-selector-search": "true",
1388
+ value: search,
1389
+ onChange: handleSearchChange,
1390
+ onKeyDown: handleKeyDown,
1391
+ placeholder: "Search models...",
1392
+ autoFocus: true
1393
+ })
1394
+ );
1395
+ const hasMultipleProviders = new Set(filtered.map((m) => m.provider).filter(Boolean)).size > 1;
1396
+ filtered.forEach((model, idx) => {
1397
+ const isSelected = model.id === selectedModel;
1398
+ const isHighlighted = idx === highlightIndex;
1399
+ const attrs = {
1400
+ key: model.id,
1401
+ "data-model-option": "true",
1402
+ onClick: () => handleSelect(model.id)
1403
+ };
1404
+ if (model.tier) {
1405
+ attrs["data-tier"] = model.tier;
1406
+ }
1407
+ if (model.provider && hasMultipleProviders) {
1408
+ attrs["data-model-provider"] = model.provider;
1409
+ }
1410
+ if (isSelected) {
1411
+ attrs["data-model-selected"] = "true";
1412
+ }
1413
+ if (isHighlighted) {
1414
+ attrs["data-model-highlighted"] = "true";
1415
+ }
1416
+ const label = model.provider && hasMultipleProviders ? `${model.name} (${model.provider})` : model.name;
1417
+ dropdownChildren.push(react.createElement("div", attrs, label));
1418
+ });
1419
+ children.push(
1420
+ react.createElement(
1421
+ "div",
1422
+ { key: "dropdown", "data-model-selector-dropdown": "true" },
1423
+ ...dropdownChildren
1424
+ )
1425
+ );
1426
+ }
1427
+ return react.createElement(
1428
+ "div",
1429
+ {
1430
+ "data-model-selector": "true",
1431
+ className,
1432
+ ref: containerRef
1433
+ },
1434
+ ...children
1435
+ );
1436
+ }
1437
+ function useCopilotAuth(options) {
1438
+ const { baseUrl, headers } = options;
1439
+ const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
1440
+ const [status, setStatus] = react.useState("idle");
1441
+ const [error, setError] = react.useState(null);
1442
+ const [token, setToken] = react.useState(null);
1443
+ const [deviceCode, setDeviceCode] = react.useState(null);
1444
+ const [verificationUrl, setVerificationUrl] = react.useState(null);
1445
+ const onAuthenticatedRef = react.useRef(options.onAuthenticated);
1446
+ onAuthenticatedRef.current = options.onAuthenticated;
1447
+ const onErrorRef = react.useRef(options.onError);
1448
+ onErrorRef.current = options.onError;
1449
+ const post = react.useCallback(
1450
+ async (path, body) => {
1451
+ const res = await fetchFn(`${baseUrl}${path}`, {
1452
+ method: "POST",
1453
+ headers: { "Content-Type": "application/json", ...headers },
1454
+ body: body ? JSON.stringify(body) : void 0
1455
+ });
1456
+ const data = await res.json();
1457
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
1458
+ return data;
1459
+ },
1460
+ [baseUrl, fetchFn, headers]
1461
+ );
1462
+ const start = react.useCallback(async () => {
1463
+ setStatus("pending");
1464
+ setError(null);
1465
+ try {
1466
+ const result = await post("/auth/start", { provider: "copilot" });
1467
+ setDeviceCode(result.userCode);
1468
+ setVerificationUrl(result.verificationUrl);
1469
+ await post("/auth/copilot/poll");
1470
+ const authToken = {
1471
+ accessToken: "server-managed",
1472
+ tokenType: "bearer",
1473
+ obtainedAt: Date.now()
1474
+ };
1475
+ setToken(authToken);
1476
+ setStatus("authenticated");
1477
+ onAuthenticatedRef.current?.(authToken);
1478
+ } catch (err) {
1479
+ const e = err instanceof Error ? err : new Error(String(err));
1480
+ setError(e);
1481
+ setStatus("error");
1482
+ onErrorRef.current?.(e);
1483
+ }
1484
+ }, [post]);
1485
+ const reset = react.useCallback(() => {
1486
+ setStatus("idle");
1487
+ setError(null);
1488
+ setToken(null);
1489
+ setDeviceCode(null);
1490
+ setVerificationUrl(null);
1491
+ }, []);
1492
+ return { status, error, token, deviceCode, verificationUrl, start, reset };
1493
+ }
1494
+ function useClaudeAuth(options) {
1495
+ const { baseUrl, headers } = options;
1496
+ const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
1497
+ const [status, setStatus] = react.useState("idle");
1498
+ const [error, setError] = react.useState(null);
1499
+ const [token, setToken] = react.useState(null);
1500
+ const [authorizeUrl, setAuthorizeUrl] = react.useState(null);
1501
+ const onAuthenticatedRef = react.useRef(options.onAuthenticated);
1502
+ onAuthenticatedRef.current = options.onAuthenticated;
1503
+ const onErrorRef = react.useRef(options.onError);
1504
+ onErrorRef.current = options.onError;
1505
+ const post = react.useCallback(
1506
+ async (path, body) => {
1507
+ const res = await fetchFn(`${baseUrl}${path}`, {
1508
+ method: "POST",
1509
+ headers: { "Content-Type": "application/json", ...headers },
1510
+ body: body ? JSON.stringify(body) : void 0
1511
+ });
1512
+ const data = await res.json();
1513
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
1514
+ return data;
1515
+ },
1516
+ [baseUrl, fetchFn, headers]
1517
+ );
1518
+ const start = react.useCallback(async () => {
1519
+ setStatus("pending");
1520
+ setError(null);
1521
+ try {
1522
+ const result = await post("/auth/start", { provider: "claude" });
1523
+ setAuthorizeUrl(result.authorizeUrl);
1524
+ } catch (err) {
1525
+ const e = err instanceof Error ? err : new Error(String(err));
1526
+ setError(e);
1527
+ setStatus("error");
1528
+ onErrorRef.current?.(e);
1529
+ }
1530
+ }, [post]);
1531
+ const complete = react.useCallback(async (codeOrUrl) => {
1532
+ try {
1533
+ await post("/auth/claude/complete", { code: codeOrUrl });
1534
+ const authToken = {
1535
+ accessToken: "server-managed",
1536
+ tokenType: "bearer",
1537
+ obtainedAt: Date.now()
1538
+ };
1539
+ setToken(authToken);
1540
+ setStatus("authenticated");
1541
+ onAuthenticatedRef.current?.(authToken);
1542
+ } catch (err) {
1543
+ const e = err instanceof Error ? err : new Error(String(err));
1544
+ setError(e);
1545
+ setStatus("error");
1546
+ onErrorRef.current?.(e);
1547
+ }
1548
+ }, [post]);
1549
+ const reset = react.useCallback(() => {
1550
+ setStatus("idle");
1551
+ setError(null);
1552
+ setToken(null);
1553
+ setAuthorizeUrl(null);
1554
+ }, []);
1555
+ return { status, error, token, authorizeUrl, start, complete, reset };
1556
+ }
1557
+ function useApiKeyAuth(options) {
1558
+ const { baseUrl, headers } = options;
1559
+ const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
1560
+ const [status, setStatus] = react.useState("idle");
1561
+ const [error, setError] = react.useState(null);
1562
+ const [token, setToken] = react.useState(null);
1563
+ const onAuthenticatedRef = react.useRef(options.onAuthenticated);
1564
+ onAuthenticatedRef.current = options.onAuthenticated;
1565
+ const onErrorRef = react.useRef(options.onError);
1566
+ onErrorRef.current = options.onError;
1567
+ const submit = react.useCallback(async (key, apiBaseUrl) => {
1568
+ if (!key || !key.trim()) {
1569
+ const e = new Error("API key cannot be empty");
1570
+ setError(e);
1571
+ setStatus("error");
1572
+ onErrorRef.current?.(e);
1573
+ return;
1574
+ }
1575
+ setStatus("pending");
1576
+ setError(null);
1577
+ try {
1578
+ const res = await fetchFn(`${baseUrl}/auth/vercel/complete`, {
1579
+ method: "POST",
1580
+ headers: { "Content-Type": "application/json", ...headers },
1581
+ body: JSON.stringify({
1582
+ apiKey: key.trim(),
1583
+ ...apiBaseUrl ? { baseUrl: apiBaseUrl } : {}
1584
+ })
1585
+ });
1586
+ const data = await res.json();
1587
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
1588
+ const authToken = {
1589
+ accessToken: "server-managed",
1590
+ tokenType: "bearer",
1591
+ obtainedAt: Date.now()
1592
+ };
1593
+ setToken(authToken);
1594
+ setStatus("authenticated");
1595
+ onAuthenticatedRef.current?.(authToken);
1596
+ } catch (err) {
1597
+ const e = err instanceof Error ? err : new Error(String(err));
1598
+ setError(e);
1599
+ setStatus("error");
1600
+ onErrorRef.current?.(e);
1601
+ }
1602
+ }, [baseUrl, fetchFn, headers]);
1603
+ const reset = react.useCallback(() => {
1604
+ setStatus("idle");
1605
+ setError(null);
1606
+ setToken(null);
1607
+ }, []);
1608
+ return { status, error, token, submit, reset };
1609
+ }
1610
+
1611
+ // src/chat/react/useRemoteAuth.ts
1612
+ function useRemoteAuth(options) {
1613
+ const { backend, baseUrl, onAuthenticated, headers } = options;
1614
+ const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
1615
+ const [status, setStatus] = react.useState("idle");
1616
+ const [error, setError] = react.useState(null);
1617
+ const [token, setToken] = react.useState(null);
1618
+ const [savedProviders, setSavedProviders] = react.useState([]);
1619
+ const onAuthenticatedRef = react.useRef(onAuthenticated);
1620
+ onAuthenticatedRef.current = onAuthenticated;
1621
+ const handleSuccess = react.useCallback((authToken) => {
1622
+ setToken(authToken);
1623
+ setStatus("authenticated");
1624
+ onAuthenticatedRef.current?.(authToken);
1625
+ }, []);
1626
+ const handleError = react.useCallback((err) => {
1627
+ setError(err);
1628
+ setStatus("error");
1629
+ }, []);
1630
+ const hookOpts = { baseUrl, headers, fetch: fetchFn, onAuthenticated: handleSuccess, onError: handleError };
1631
+ const copilot = useCopilotAuth(hookOpts);
1632
+ const claude = useClaudeAuth(hookOpts);
1633
+ const apiKey = useApiKeyAuth(hookOpts);
1634
+ const post = react.useCallback(
1635
+ async (path, body) => {
1636
+ const res = await fetchFn(`${baseUrl}${path}`, {
1637
+ method: "POST",
1638
+ headers: { "Content-Type": "application/json", ...headers },
1639
+ body: body ? JSON.stringify(body) : void 0
1640
+ });
1641
+ const data = await res.json();
1642
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
1643
+ return data;
1644
+ },
1645
+ [baseUrl, fetchFn, headers]
1646
+ );
1647
+ const get = react.useCallback(
1648
+ async (path) => {
1649
+ const res = await fetchFn(`${baseUrl}${path}`, {
1650
+ method: "GET",
1651
+ headers: { ...headers }
1652
+ });
1653
+ const data = await res.json();
1654
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
1655
+ return data;
1656
+ },
1657
+ [baseUrl, fetchFn, headers]
1658
+ );
1659
+ const startDeviceFlow = react.useCallback(async () => {
1660
+ if (backend !== "copilot") return;
1661
+ setStatus("pending");
1662
+ setError(null);
1663
+ await copilot.start();
1664
+ }, [backend, copilot]);
1665
+ const startOAuthFlow = react.useCallback(async () => {
1666
+ if (backend !== "claude") return;
1667
+ setStatus("pending");
1668
+ setError(null);
1669
+ await claude.start();
1670
+ }, [backend, claude]);
1671
+ const completeOAuth = react.useCallback(
1672
+ async (codeOrUrl) => {
1673
+ await claude.complete(codeOrUrl);
1674
+ },
1675
+ [claude]
1676
+ );
1677
+ const submitApiKey = react.useCallback(
1678
+ async (key, apiBaseUrl) => {
1679
+ if (backend !== "vercel-ai") return;
1680
+ await apiKey.submit(key, apiBaseUrl);
1681
+ },
1682
+ [backend, apiKey]
1683
+ );
1684
+ const loadSavedTokens = react.useCallback(async () => {
1685
+ try {
1686
+ const data = await get("/tokens/saved");
1687
+ setSavedProviders(data.saved || []);
1688
+ } catch {
1689
+ }
1690
+ }, [get]);
1691
+ const useSavedToken = react.useCallback(
1692
+ async (provider) => {
1693
+ try {
1694
+ await post("/tokens/use", { provider });
1695
+ const authToken = {
1696
+ accessToken: "server-managed",
1697
+ tokenType: "bearer",
1698
+ obtainedAt: Date.now()
1699
+ };
1700
+ setToken(authToken);
1701
+ setStatus("authenticated");
1702
+ onAuthenticatedRef.current?.(authToken);
1703
+ } catch (err) {
1704
+ setError(err instanceof Error ? err : new Error(String(err)));
1705
+ setStatus("error");
1706
+ }
1707
+ },
1708
+ [post]
1709
+ );
1710
+ const clearTokens = react.useCallback(async () => {
1711
+ try {
1712
+ await post("/tokens/clear");
1713
+ setSavedProviders([]);
1714
+ } catch {
1715
+ }
1716
+ }, [post]);
1717
+ const reset = react.useCallback(() => {
1718
+ setStatus("idle");
1719
+ setError(null);
1720
+ setToken(null);
1721
+ copilot.reset();
1722
+ claude.reset();
1723
+ apiKey.reset();
1724
+ setSavedProviders([]);
1725
+ }, [copilot, claude, apiKey]);
1726
+ const start = react.useCallback(async (provider) => {
1727
+ const target = provider ?? backend;
1728
+ setStatus("pending");
1729
+ setError(null);
1730
+ switch (target) {
1731
+ case "copilot":
1732
+ await copilot.start();
1733
+ break;
1734
+ case "claude":
1735
+ await claude.start();
1736
+ break;
1737
+ case "vercel-ai": {
1738
+ const e = new Error("vercel-ai requires submitApiKey(key, baseUrl) \u2014 cannot auto-start");
1739
+ setError(e);
1740
+ setStatus("error");
1741
+ return;
1742
+ }
1743
+ default: {
1744
+ const e = new Error(`Unknown auth provider: ${target}`);
1745
+ setError(e);
1746
+ setStatus("error");
1747
+ return;
1748
+ }
1749
+ }
1750
+ }, [backend, copilot, claude]);
1751
+ return {
1752
+ status,
1753
+ error,
1754
+ startDeviceFlow,
1755
+ deviceCode: copilot.deviceCode,
1756
+ verificationUrl: copilot.verificationUrl,
1757
+ startOAuthFlow,
1758
+ authorizeUrl: claude.authorizeUrl,
1759
+ completeOAuth,
1760
+ submitApiKey,
1761
+ start,
1762
+ token,
1763
+ reset,
1764
+ savedProviders,
1765
+ loadSavedTokens,
1766
+ useSavedToken,
1767
+ clearTokens
1768
+ };
1769
+ }
1770
+
1771
+ // src/chat/listener-set.ts
1772
+ var ListenerSet = class {
1773
+ _listeners = /* @__PURE__ */ new Set();
1774
+ /** Add a listener. Returns an unsubscribe function. */
1775
+ add(callback) {
1776
+ this._listeners.add(callback);
1777
+ return () => {
1778
+ this._listeners.delete(callback);
1779
+ };
1780
+ }
1781
+ /** Notify all listeners with the given arguments. Errors are isolated per listener. */
1782
+ notify(...args) {
1783
+ for (const cb of this._listeners) {
1784
+ try {
1785
+ cb(...args);
1786
+ } catch {
1787
+ }
1788
+ }
1789
+ }
1790
+ /** Remove all listeners. */
1791
+ clear() {
1792
+ this._listeners.clear();
1793
+ }
1794
+ /** Current number of listeners. */
1795
+ get size() {
1796
+ return this._listeners.size;
1797
+ }
1798
+ };
1799
+
1800
+ // src/chat/react/RemoteChatClient.ts
1801
+ var RemoteChatClient = class {
1802
+ _status = "idle";
1803
+ _activeSessionId = null;
1804
+ _selectedProviderId = null;
1805
+ _abortController = null;
1806
+ baseUrl;
1807
+ headers;
1808
+ _fetch;
1809
+ _selectionListeners = new ListenerSet();
1810
+ constructor(options) {
1811
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
1812
+ this.headers = options.headers ?? {};
1813
+ this._fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
1814
+ }
1815
+ // ─── Lifecycle ──────────────────────────────────────────────
1816
+ get status() {
1817
+ return this._status;
1818
+ }
1819
+ async dispose() {
1820
+ this.abort();
1821
+ this._status = "disposed";
1822
+ }
1823
+ assertNotDisposed() {
1824
+ if (this._status === "disposed") {
1825
+ throw new Error("Runtime is disposed");
1826
+ }
1827
+ }
1828
+ // ─── Provider Selection ────────────────────────────────────
1829
+ get selectedProviderId() {
1830
+ return this._selectedProviderId;
1831
+ }
1832
+ selectProvider(providerId) {
1833
+ this.assertNotDisposed();
1834
+ this._selectedProviderId = providerId;
1835
+ this._notifySelectionChange(providerId);
1836
+ }
1837
+ onSelectionChange(callback) {
1838
+ return this._selectionListeners.add(callback);
1839
+ }
1840
+ _notifySelectionChange(providerId) {
1841
+ this._selectionListeners.notify(providerId);
1842
+ }
1843
+ // ─── Sessions ───────────────────────────────────────────────
1844
+ get activeSessionId() {
1845
+ return this._activeSessionId;
1846
+ }
1847
+ async createSession(options) {
1848
+ this.assertNotDisposed();
1849
+ const res = await this._post("/sessions/create", options);
1850
+ const session = await res.json();
1851
+ this._activeSessionId = session.id;
1852
+ this._notifySessionChange();
1853
+ return session;
1854
+ }
1855
+ async getSession(id) {
1856
+ this.assertNotDisposed();
1857
+ const res = await this._get(`/sessions/${id}`);
1858
+ if (res.status === 404) return null;
1859
+ return await res.json();
1860
+ }
1861
+ async listSessions(_options) {
1862
+ this.assertNotDisposed();
1863
+ const res = await this._get("/sessions");
1864
+ return await res.json();
1865
+ }
1866
+ async deleteSession(id) {
1867
+ this.assertNotDisposed();
1868
+ await this._delete(`/sessions/${id}`);
1869
+ if (this._activeSessionId === id) {
1870
+ this._activeSessionId = null;
1871
+ }
1872
+ this._notifySessionChange();
1873
+ }
1874
+ /**
1875
+ * Fetch context window stats from server.
1876
+ * Returns null if stats not available (e.g. no messages sent yet).
1877
+ */
1878
+ async getContextStats(sessionId) {
1879
+ this.assertNotDisposed();
1880
+ const res = await this._get(`/sessions/${sessionId}/context-stats`);
1881
+ const data = await res.json();
1882
+ return data;
1883
+ }
1884
+ async switchSession(id) {
1885
+ this.assertNotDisposed();
1886
+ const session = await this.getSession(id);
1887
+ if (!session) throw new Error(`Session not found: ${id}`);
1888
+ this._activeSessionId = session.id;
1889
+ this._notifySessionChange();
1890
+ return session;
1891
+ }
1892
+ // ─── Messaging ──────────────────────────────────────────────
1893
+ async *send(sessionId, message, options) {
1894
+ this.assertNotDisposed();
1895
+ this._status = "streaming";
1896
+ this._abortController = new AbortController();
1897
+ try {
1898
+ const res = await this._fetch(`${this.baseUrl}/send`, {
1899
+ method: "POST",
1900
+ headers: {
1901
+ "Content-Type": "application/json",
1902
+ Accept: "text/event-stream",
1903
+ ...this.headers
1904
+ },
1905
+ body: JSON.stringify({
1906
+ sessionId,
1907
+ message,
1908
+ model: options?.model,
1909
+ providerId: this._selectedProviderId ?? void 0
1910
+ }),
1911
+ signal: this._abortController.signal
1912
+ });
1913
+ if (!res.ok) {
1914
+ throw new Error(`Send failed: ${res.status} ${res.statusText}`);
1915
+ }
1916
+ if (!res.body) {
1917
+ throw new Error("No response body for SSE stream");
1918
+ }
1919
+ yield* this._parseSSEStream(res.body, this._abortController.signal);
1920
+ } catch (err) {
1921
+ if (err instanceof DOMException && err.name === "AbortError") ; else {
1922
+ this._status = "error";
1923
+ throw err;
1924
+ }
1925
+ } finally {
1926
+ this._abortController = null;
1927
+ if (this._status === "streaming") {
1928
+ this._status = "idle";
1929
+ }
1930
+ this._notifySessionChange();
1931
+ }
1932
+ }
1933
+ abort() {
1934
+ if (this._abortController) {
1935
+ this._abortController.abort();
1936
+ this._abortController = null;
1937
+ }
1938
+ if (this._status === "streaming") {
1939
+ this._status = "idle";
1940
+ }
1941
+ this._post("/abort", {}).catch(() => {
1942
+ });
1943
+ }
1944
+ // ─── Discovery ─────────────────────────────────────────────
1945
+ async listModels() {
1946
+ this.assertNotDisposed();
1947
+ const res = await this._get("/models");
1948
+ return await res.json();
1949
+ }
1950
+ async listBackends() {
1951
+ this.assertNotDisposed();
1952
+ const res = await this._get("/backends");
1953
+ return await res.json();
1954
+ }
1955
+ // ─── Providers ──────────────────────────────────────────────
1956
+ async listProviders() {
1957
+ this.assertNotDisposed();
1958
+ const res = await this._get("/providers");
1959
+ return await res.json();
1960
+ }
1961
+ async createProvider(config) {
1962
+ this.assertNotDisposed();
1963
+ const res = await this._post("/providers", config);
1964
+ return await res.json();
1965
+ }
1966
+ async updateProvider(id, changes) {
1967
+ this.assertNotDisposed();
1968
+ await this._put(`/providers/${id}`, changes);
1969
+ }
1970
+ async deleteProvider(id) {
1971
+ this.assertNotDisposed();
1972
+ await this._delete(`/providers/${id}`);
1973
+ }
1974
+ // ─── Session Change Notifications ────────────────────────────
1975
+ _sessionListeners = new ListenerSet();
1976
+ onSessionChange(callback) {
1977
+ return this._sessionListeners.add(callback);
1978
+ }
1979
+ _notifySessionChange() {
1980
+ this._sessionListeners.notify();
1981
+ }
1982
+ // ─── Internal HTTP helpers ──────────────────────────────────
1983
+ async _get(path) {
1984
+ const res = await this._fetch(`${this.baseUrl}${path}`, {
1985
+ method: "GET",
1986
+ headers: { ...this.headers }
1987
+ });
1988
+ if (!res.ok && res.status !== 404) {
1989
+ throw new Error(`GET ${path} failed: ${res.status} ${res.statusText}`);
1990
+ }
1991
+ return res;
1992
+ }
1993
+ async _post(path, body) {
1994
+ const res = await this._fetch(`${this.baseUrl}${path}`, {
1995
+ method: "POST",
1996
+ headers: {
1997
+ "Content-Type": "application/json",
1998
+ ...this.headers
1999
+ },
2000
+ body: JSON.stringify(body)
2001
+ });
2002
+ if (!res.ok) {
2003
+ throw new Error(`POST ${path} failed: ${res.status} ${res.statusText}`);
2004
+ }
2005
+ return res;
2006
+ }
2007
+ async _delete(path) {
2008
+ const res = await this._fetch(`${this.baseUrl}${path}`, {
2009
+ method: "DELETE",
2010
+ headers: { ...this.headers }
2011
+ });
2012
+ if (!res.ok) {
2013
+ throw new Error(`DELETE ${path} failed: ${res.status} ${res.statusText}`);
2014
+ }
2015
+ return res;
2016
+ }
2017
+ async _put(path, body) {
2018
+ const res = await this._fetch(`${this.baseUrl}${path}`, {
2019
+ method: "PUT",
2020
+ headers: {
2021
+ "Content-Type": "application/json",
2022
+ ...this.headers
2023
+ },
2024
+ body: JSON.stringify(body)
2025
+ });
2026
+ if (!res.ok) {
2027
+ throw new Error(`PUT ${path} failed: ${res.status} ${res.statusText}`);
2028
+ }
2029
+ return res;
2030
+ }
2031
+ // ─── SSE Parser ─────────────────────────────────────────────
2032
+ async *_parseSSEStream(body, signal) {
2033
+ const reader = body.getReader();
2034
+ const decoder = new TextDecoder();
2035
+ let buffer = "";
2036
+ const abortPromise = new Promise((_, reject) => {
2037
+ if (signal.aborted) {
2038
+ reject(new DOMException("Aborted", "AbortError"));
2039
+ return;
2040
+ }
2041
+ signal.addEventListener("abort", () => {
2042
+ reject(new DOMException("Aborted", "AbortError"));
2043
+ }, { once: true });
2044
+ });
2045
+ try {
2046
+ while (true) {
2047
+ if (signal.aborted) break;
2048
+ const { done, value } = await Promise.race([
2049
+ reader.read(),
2050
+ abortPromise
2051
+ ]);
2052
+ if (done) break;
2053
+ buffer += decoder.decode(value, { stream: true });
2054
+ const lines = buffer.split("\n");
2055
+ buffer = lines.pop();
2056
+ for (const line of lines) {
2057
+ if (signal.aborted) return;
2058
+ if (line.startsWith("data: ")) {
2059
+ const data = line.slice(6).trim();
2060
+ if (data === "[DONE]") return;
2061
+ try {
2062
+ yield JSON.parse(data);
2063
+ } catch {
2064
+ }
2065
+ }
2066
+ }
2067
+ }
2068
+ } catch (err) {
2069
+ if (err instanceof DOMException && err.name === "AbortError") ; else {
2070
+ throw err;
2071
+ }
2072
+ } finally {
2073
+ reader.releaseLock();
2074
+ }
2075
+ }
2076
+ };
2077
+
2078
+ // src/chat/react/useRemoteChat.ts
2079
+ function useRemoteChat(options) {
2080
+ const {
2081
+ chatBaseUrl,
2082
+ authBaseUrl,
2083
+ backend,
2084
+ onReady,
2085
+ headers
2086
+ } = options;
2087
+ const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
2088
+ const [phase, setPhase] = react.useState("initializing");
2089
+ const [runtime, setRuntime] = react.useState(null);
2090
+ const [sessionId, setSessionId] = react.useState(null);
2091
+ const [error, setError] = react.useState(null);
2092
+ const onReadyRef = react.useRef(onReady);
2093
+ onReadyRef.current = onReady;
2094
+ const mountedRef = react.useRef(true);
2095
+ react.useEffect(() => {
2096
+ return () => {
2097
+ mountedRef.current = false;
2098
+ };
2099
+ }, []);
2100
+ const auth = useRemoteAuth({
2101
+ backend,
2102
+ baseUrl: authBaseUrl,
2103
+ fetch: fetchFn,
2104
+ headers
2105
+ });
2106
+ const restoredRef = react.useRef(false);
2107
+ const [tokensLoaded, setTokensLoaded] = react.useState(false);
2108
+ react.useEffect(() => {
2109
+ if (restoredRef.current) return;
2110
+ restoredRef.current = true;
2111
+ (async () => {
2112
+ try {
2113
+ await auth.loadSavedTokens();
2114
+ } catch {
2115
+ }
2116
+ if (mountedRef.current) setTokensLoaded(true);
2117
+ })();
2118
+ }, []);
2119
+ react.useEffect(() => {
2120
+ if (!tokensLoaded) return;
2121
+ if (auth.status === "idle" && auth.savedProviders.includes(backend) && phase === "initializing") {
2122
+ auth.useSavedToken(backend).catch(() => {
2123
+ if (mountedRef.current) setPhase("unauthenticated");
2124
+ });
2125
+ } else if (auth.status === "idle" && !auth.savedProviders.includes(backend) && phase === "initializing") {
2126
+ setPhase("unauthenticated");
2127
+ }
2128
+ }, [tokensLoaded, auth.status, auth.savedProviders, backend, phase]);
2129
+ react.useEffect(() => {
2130
+ if (auth.status === "pending") {
2131
+ setPhase("authenticating");
2132
+ } else if (auth.status === "error" && auth.error) {
2133
+ setError(auth.error);
2134
+ setPhase("error");
2135
+ }
2136
+ }, [auth.status, auth.error]);
2137
+ const creatingRef = react.useRef(false);
2138
+ react.useEffect(() => {
2139
+ if (auth.status !== "authenticated" || creatingRef.current || runtime !== null) return;
2140
+ creatingRef.current = true;
2141
+ setPhase("creating");
2142
+ setError(null);
2143
+ (async () => {
2144
+ try {
2145
+ const rt = new RemoteChatClient({
2146
+ baseUrl: chatBaseUrl,
2147
+ headers,
2148
+ fetch: fetchFn
2149
+ });
2150
+ const session = await rt.createSession({});
2151
+ if (!mountedRef.current) return;
2152
+ setRuntime(rt);
2153
+ setSessionId(session.id);
2154
+ setPhase("ready");
2155
+ onReadyRef.current?.();
2156
+ } catch (err) {
2157
+ if (!mountedRef.current) return;
2158
+ creatingRef.current = false;
2159
+ setError(err instanceof Error ? err : new Error(String(err)));
2160
+ setPhase("error");
2161
+ }
2162
+ })();
2163
+ }, [auth.status, chatBaseUrl]);
2164
+ const newSession = react.useCallback(async () => {
2165
+ if (!runtime) throw new Error("Runtime not ready");
2166
+ const session = await runtime.createSession({});
2167
+ setSessionId(session.id);
2168
+ return session.id;
2169
+ }, [runtime]);
2170
+ const logout = react.useCallback(async () => {
2171
+ try {
2172
+ await auth.clearTokens();
2173
+ } catch {
2174
+ }
2175
+ if (runtime) {
2176
+ try {
2177
+ runtime.dispose();
2178
+ } catch {
2179
+ }
2180
+ }
2181
+ auth.reset();
2182
+ setRuntime(null);
2183
+ setSessionId(null);
2184
+ setError(null);
2185
+ setTokensLoaded(false);
2186
+ creatingRef.current = false;
2187
+ restoredRef.current = false;
2188
+ setPhase("unauthenticated");
2189
+ }, [auth.clearTokens, auth.reset, runtime]);
2190
+ return {
2191
+ phase,
2192
+ runtime,
2193
+ sessionId,
2194
+ auth,
2195
+ error,
2196
+ newSession,
2197
+ logout
2198
+ };
2199
+ }
2200
+ function CopilotAuthForm({ auth, onAuthComplete }) {
2201
+ const children = [];
2202
+ if (auth.status === "idle") {
2203
+ children.push(
2204
+ react.createElement("button", {
2205
+ key: "start",
2206
+ type: "button",
2207
+ "data-action": "start-auth",
2208
+ onClick: () => {
2209
+ auth.startDeviceFlow().then(() => onAuthComplete());
2210
+ }
2211
+ }, "Authenticate with GitHub")
2212
+ );
2213
+ }
2214
+ if (auth.deviceCode) {
2215
+ children.push(
2216
+ react.createElement("div", { key: "code", "data-device-code": "true" }, auth.deviceCode)
2217
+ );
2218
+ if (auth.verificationUrl) {
2219
+ children.push(
2220
+ react.createElement("a", {
2221
+ key: "url",
2222
+ href: auth.verificationUrl,
2223
+ target: "_blank",
2224
+ rel: "noreferrer"
2225
+ }, "Open GitHub \u2192")
2226
+ );
2227
+ }
2228
+ }
2229
+ if (auth.status === "pending") {
2230
+ children.push(
2231
+ react.createElement("span", { key: "wait", "data-auth-loading": "true" }, "Waiting...")
2232
+ );
2233
+ }
2234
+ if (auth.status === "authenticated") {
2235
+ children.push(
2236
+ react.createElement(
2237
+ "div",
2238
+ { key: "done", "data-auth-success": "true" },
2239
+ "\u2713 Authenticated",
2240
+ react.createElement("button", {
2241
+ type: "button",
2242
+ "data-action": "continue",
2243
+ onClick: onAuthComplete
2244
+ }, "Continue \u2192")
2245
+ )
2246
+ );
2247
+ }
2248
+ if (auth.error) {
2249
+ children.push(
2250
+ react.createElement("div", { key: "error", "data-auth-error-display": "true" }, auth.error.message)
2251
+ );
2252
+ }
2253
+ return react.createElement("div", { "data-auth-flow": "copilot" }, ...children);
2254
+ }
2255
+ function ClaudeAuthForm({ auth, onAuthComplete }) {
2256
+ const codeRef = react.useRef(null);
2257
+ const children = [];
2258
+ if (auth.status === "idle") {
2259
+ children.push(
2260
+ react.createElement("button", {
2261
+ key: "start",
2262
+ type: "button",
2263
+ "data-action": "start-auth",
2264
+ onClick: () => auth.startOAuthFlow()
2265
+ }, "Authenticate with Claude")
2266
+ );
2267
+ }
2268
+ if (auth.authorizeUrl) {
2269
+ children.push(
2270
+ react.createElement("a", {
2271
+ key: "url",
2272
+ href: auth.authorizeUrl,
2273
+ target: "_blank",
2274
+ rel: "noreferrer"
2275
+ }, "Open authorization page \u2192")
2276
+ );
2277
+ children.push(
2278
+ react.createElement(
2279
+ "div",
2280
+ { key: "complete", "data-auth-complete": "true" },
2281
+ react.createElement("input", {
2282
+ ref: codeRef,
2283
+ placeholder: "Paste code or redirect URL..."
2284
+ }),
2285
+ react.createElement("button", {
2286
+ type: "button",
2287
+ "data-action": "complete-auth",
2288
+ onClick: () => {
2289
+ const v = codeRef.current?.value?.trim();
2290
+ if (v) auth.completeOAuth(v).then(() => onAuthComplete());
2291
+ }
2292
+ }, "Submit")
2293
+ )
2294
+ );
2295
+ }
2296
+ if (auth.status === "authenticated") {
2297
+ children.push(
2298
+ react.createElement(
2299
+ "div",
2300
+ { key: "done", "data-auth-success": "true" },
2301
+ "\u2713 Authenticated",
2302
+ react.createElement("button", {
2303
+ type: "button",
2304
+ "data-action": "continue",
2305
+ onClick: onAuthComplete
2306
+ }, "Continue \u2192")
2307
+ )
2308
+ );
2309
+ }
2310
+ if (auth.error) {
2311
+ children.push(
2312
+ react.createElement("div", { key: "error", "data-auth-error-display": "true" }, auth.error.message)
2313
+ );
2314
+ }
2315
+ return react.createElement("div", { "data-auth-flow": "claude" }, ...children);
2316
+ }
2317
+ function VercelAIAuthForm({ auth, onAuthComplete }) {
2318
+ const apiKeyRef = react.useRef(null);
2319
+ const baseUrlRef = react.useRef(null);
2320
+ const children = [];
2321
+ if (auth.status !== "authenticated") {
2322
+ children.push(
2323
+ react.createElement(
2324
+ "div",
2325
+ { key: "apikey", "data-auth-apikey": "true" },
2326
+ react.createElement("input", {
2327
+ ref: baseUrlRef,
2328
+ placeholder: "Base URL (default: openai.com/v1)"
2329
+ }),
2330
+ react.createElement("input", {
2331
+ ref: apiKeyRef,
2332
+ type: "password",
2333
+ placeholder: "API Key (sk-...)"
2334
+ }),
2335
+ react.createElement("button", {
2336
+ type: "button",
2337
+ "data-action": "submit-apikey",
2338
+ onClick: () => {
2339
+ const k = apiKeyRef.current?.value?.trim();
2340
+ if (k) {
2341
+ auth.submitApiKey(k, baseUrlRef.current?.value?.trim() || void 0).then(() => onAuthComplete());
2342
+ }
2343
+ }
2344
+ }, "Connect")
2345
+ )
2346
+ );
2347
+ }
2348
+ if (auth.status === "authenticated") {
2349
+ children.push(
2350
+ react.createElement(
2351
+ "div",
2352
+ { key: "done", "data-auth-success": "true" },
2353
+ "\u2713 Connected",
2354
+ react.createElement("button", {
2355
+ type: "button",
2356
+ "data-action": "continue",
2357
+ onClick: onAuthComplete
2358
+ }, "Continue \u2192")
2359
+ )
2360
+ );
2361
+ }
2362
+ if (auth.error) {
2363
+ children.push(
2364
+ react.createElement("div", { key: "error", "data-auth-error-display": "true" }, auth.error.message)
2365
+ );
2366
+ }
2367
+ return react.createElement("div", { "data-auth-flow": "vercel-ai" }, ...children);
2368
+ }
2369
+ function BackendSelector({
2370
+ backends,
2371
+ onSelect,
2372
+ className
2373
+ }) {
2374
+ const handleClick = react.useCallback(
2375
+ (name) => () => onSelect(name),
2376
+ [onSelect]
2377
+ );
2378
+ const items = backends.map(
2379
+ (backend) => react.createElement(
2380
+ "button",
2381
+ {
2382
+ key: backend.name,
2383
+ type: "button",
2384
+ "data-backend-item": "true",
2385
+ "data-backend-name": backend.name,
2386
+ onClick: handleClick(backend.name)
2387
+ },
2388
+ backend.name
2389
+ )
2390
+ );
2391
+ return react.createElement(
2392
+ "div",
2393
+ { "data-backend-selector": "true", className },
2394
+ ...items
2395
+ );
2396
+ }
2397
+ function useBackends() {
2398
+ const runtime = useChatRuntime();
2399
+ const [backends, setBackends] = react.useState([]);
2400
+ const [isLoading, setIsLoading] = react.useState(true);
2401
+ const [error, setError] = react.useState(null);
2402
+ const mountedRef = react.useRef(true);
2403
+ const fetchBackends = react.useCallback(() => {
2404
+ setIsLoading(true);
2405
+ setError(null);
2406
+ try {
2407
+ const result = runtime.listBackends();
2408
+ if (result instanceof Promise) {
2409
+ result.then((data) => {
2410
+ if (mountedRef.current) {
2411
+ setBackends(data);
2412
+ setIsLoading(false);
2413
+ }
2414
+ }).catch((err) => {
2415
+ if (mountedRef.current) {
2416
+ setError(err instanceof Error ? err : new Error(String(err)));
2417
+ setIsLoading(false);
2418
+ }
2419
+ });
2420
+ } else {
2421
+ if (mountedRef.current) {
2422
+ setBackends(result);
2423
+ setIsLoading(false);
2424
+ }
2425
+ }
2426
+ } catch (err) {
2427
+ if (mountedRef.current) {
2428
+ setError(err instanceof Error ? err : new Error(String(err)));
2429
+ setIsLoading(false);
2430
+ }
2431
+ }
2432
+ }, [runtime]);
2433
+ react.useEffect(() => {
2434
+ mountedRef.current = true;
2435
+ fetchBackends();
2436
+ return () => {
2437
+ mountedRef.current = false;
2438
+ };
2439
+ }, [fetchBackends]);
2440
+ return { backends, isLoading, error, refresh: fetchBackends };
2441
+ }
2442
+ function isProviderCapable(runtime) {
2443
+ return typeof runtime === "object" && runtime !== null && typeof runtime.listProviders === "function";
2444
+ }
2445
+ function useProviders() {
2446
+ const runtime = useChatRuntime();
2447
+ const [providers, setProviders] = react.useState([]);
2448
+ const [isLoading, setIsLoading] = react.useState(true);
2449
+ const [error, setError] = react.useState(null);
2450
+ const mountedRef = react.useRef(true);
2451
+ const fetchProviders = react.useCallback(() => {
2452
+ setIsLoading(true);
2453
+ setError(null);
2454
+ if (!isProviderCapable(runtime)) {
2455
+ setIsLoading(false);
2456
+ return;
2457
+ }
2458
+ runtime.listProviders().then((data) => {
2459
+ if (mountedRef.current) {
2460
+ setProviders(data);
2461
+ setIsLoading(false);
2462
+ }
2463
+ }).catch((err) => {
2464
+ if (mountedRef.current) {
2465
+ setError(err instanceof Error ? err : new Error(String(err)));
2466
+ setIsLoading(false);
2467
+ }
2468
+ });
2469
+ }, [runtime]);
2470
+ react.useEffect(() => {
2471
+ mountedRef.current = true;
2472
+ fetchProviders();
2473
+ return () => {
2474
+ mountedRef.current = false;
2475
+ };
2476
+ }, [fetchProviders]);
2477
+ const createProvider = react.useCallback(
2478
+ async (config) => {
2479
+ if (!isProviderCapable(runtime)) return;
2480
+ try {
2481
+ await runtime.createProvider(config);
2482
+ fetchProviders();
2483
+ } catch (err) {
2484
+ setError(err instanceof Error ? err : new Error(String(err)));
2485
+ }
2486
+ },
2487
+ [runtime, fetchProviders]
2488
+ );
2489
+ const updateProvider = react.useCallback(
2490
+ async (id, changes) => {
2491
+ if (!isProviderCapable(runtime)) return;
2492
+ try {
2493
+ await runtime.updateProvider(id, changes);
2494
+ fetchProviders();
2495
+ } catch (err) {
2496
+ setError(err instanceof Error ? err : new Error(String(err)));
2497
+ }
2498
+ },
2499
+ [runtime, fetchProviders]
2500
+ );
2501
+ const deleteProvider = react.useCallback(
2502
+ async (id) => {
2503
+ if (!isProviderCapable(runtime)) return;
2504
+ try {
2505
+ await runtime.deleteProvider(id);
2506
+ fetchProviders();
2507
+ } catch (err) {
2508
+ setError(err instanceof Error ? err : new Error(String(err)));
2509
+ }
2510
+ },
2511
+ [runtime, fetchProviders]
2512
+ );
2513
+ const selectProvider = react.useCallback(
2514
+ (id) => {
2515
+ if (!isProviderCapable(runtime)) return;
2516
+ runtime.selectProvider(id);
2517
+ },
2518
+ [runtime]
2519
+ );
2520
+ return { providers, isLoading, error, refresh: fetchProviders, createProvider, updateProvider, deleteProvider, selectProvider };
2521
+ }
2522
+ function ProviderSelector({
2523
+ providers,
2524
+ activeProviderId,
2525
+ onSelect,
2526
+ onSettingsClick,
2527
+ className
2528
+ }) {
2529
+ const [open, setOpen] = react.useState(false);
2530
+ const [highlightIndex, setHighlightIndex] = react.useState(0);
2531
+ const containerRef = react.useRef(null);
2532
+ const activeProvider = react.useMemo(
2533
+ () => providers.find((p) => p.id === activeProviderId),
2534
+ [providers, activeProviderId]
2535
+ );
2536
+ react.useEffect(() => {
2537
+ if (!open) return;
2538
+ const handler = (e) => {
2539
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
2540
+ setOpen(false);
2541
+ }
2542
+ };
2543
+ document.addEventListener("mousedown", handler);
2544
+ return () => document.removeEventListener("mousedown", handler);
2545
+ }, [open]);
2546
+ react.useEffect(() => {
2547
+ setHighlightIndex(0);
2548
+ }, [providers.length]);
2549
+ const handleToggle = react.useCallback(() => {
2550
+ setOpen((prev) => {
2551
+ if (!prev) setHighlightIndex(0);
2552
+ return !prev;
2553
+ });
2554
+ }, []);
2555
+ const handleSelect = react.useCallback(
2556
+ (id) => {
2557
+ onSelect(id);
2558
+ setOpen(false);
2559
+ },
2560
+ [onSelect]
2561
+ );
2562
+ const handleKeyDown = react.useCallback(
2563
+ (e) => {
2564
+ if (e.key === "ArrowDown") {
2565
+ e.preventDefault();
2566
+ setHighlightIndex((prev) => Math.min(prev + 1, providers.length - 1));
2567
+ } else if (e.key === "ArrowUp") {
2568
+ e.preventDefault();
2569
+ setHighlightIndex((prev) => Math.max(prev - 1, 0));
2570
+ } else if (e.key === "Enter") {
2571
+ e.preventDefault();
2572
+ if (providers[highlightIndex]) {
2573
+ handleSelect(providers[highlightIndex].id);
2574
+ }
2575
+ } else if (e.key === "Escape") {
2576
+ e.preventDefault();
2577
+ setOpen(false);
2578
+ }
2579
+ },
2580
+ [providers, highlightIndex, handleSelect]
2581
+ );
2582
+ const children = [];
2583
+ children.push(
2584
+ react.createElement(
2585
+ "button",
2586
+ {
2587
+ key: "trigger",
2588
+ type: "button",
2589
+ "data-provider-trigger": "true",
2590
+ onClick: handleToggle,
2591
+ onKeyDown: handleKeyDown
2592
+ },
2593
+ activeProvider ? activeProvider.label : "Select provider"
2594
+ )
2595
+ );
2596
+ if (open) {
2597
+ const dropdownChildren = [];
2598
+ providers.forEach((provider, idx) => {
2599
+ const isActive = provider.id === activeProviderId;
2600
+ const isHighlighted = idx === highlightIndex;
2601
+ const attrs = {
2602
+ key: provider.id,
2603
+ "data-provider-item": "true",
2604
+ onClick: () => handleSelect(provider.id)
2605
+ };
2606
+ if (isActive) attrs["data-provider-active"] = "true";
2607
+ if (isHighlighted) attrs["data-provider-highlighted"] = "true";
2608
+ dropdownChildren.push(
2609
+ react.createElement(
2610
+ "div",
2611
+ attrs,
2612
+ react.createElement("span", { "data-provider-label": "true" }, provider.label),
2613
+ react.createElement("span", { "data-provider-model": "true" }, provider.model)
2614
+ )
2615
+ );
2616
+ });
2617
+ if (onSettingsClick) {
2618
+ dropdownChildren.push(
2619
+ react.createElement(
2620
+ "button",
2621
+ {
2622
+ key: "settings",
2623
+ type: "button",
2624
+ "data-provider-settings-btn": "true",
2625
+ onClick: (e) => {
2626
+ e.stopPropagation();
2627
+ setOpen(false);
2628
+ onSettingsClick();
2629
+ }
2630
+ },
2631
+ "\u2699 Settings"
2632
+ )
2633
+ );
2634
+ }
2635
+ children.push(
2636
+ react.createElement(
2637
+ "div",
2638
+ { key: "dropdown", "data-provider-dropdown": "true", onKeyDown: handleKeyDown },
2639
+ ...dropdownChildren
2640
+ )
2641
+ );
2642
+ }
2643
+ return react.createElement(
2644
+ "div",
2645
+ {
2646
+ "data-provider-selector": "true",
2647
+ className,
2648
+ ref: containerRef
2649
+ },
2650
+ ...children
2651
+ );
2652
+ }
2653
+ function ProviderModelSelector({
2654
+ providers = [],
2655
+ models = [],
2656
+ activeProviderId,
2657
+ selectedModel,
2658
+ onSelectProvider,
2659
+ onSelectModel,
2660
+ onSettingsClick,
2661
+ placeholder = "Select model",
2662
+ className
2663
+ }) {
2664
+ const [open, setOpen] = react.useState(false);
2665
+ const [search, setSearch] = react.useState("");
2666
+ const [highlightIndex, setHighlightIndex] = react.useState(0);
2667
+ const containerRef = react.useRef(null);
2668
+ const isProviderMode = providers.length > 0;
2669
+ const items = react.useMemo(() => {
2670
+ if (isProviderMode) {
2671
+ return providers.map((p) => ({
2672
+ id: p.id,
2673
+ label: p.label,
2674
+ sublabel: p.model,
2675
+ type: "provider"
2676
+ }));
2677
+ }
2678
+ return models.map((m) => ({
2679
+ id: m.id,
2680
+ label: m.name,
2681
+ sublabel: m.provider,
2682
+ tier: m.tier,
2683
+ type: "model"
2684
+ }));
2685
+ }, [isProviderMode, providers, models]);
2686
+ const filtered = react.useMemo(() => {
2687
+ if (!search) return items;
2688
+ const q = search.toLowerCase();
2689
+ return items.filter(
2690
+ (item) => item.label.toLowerCase().includes(q) || item.sublabel && item.sublabel.toLowerCase().includes(q)
2691
+ );
2692
+ }, [items, search]);
2693
+ react.useEffect(() => {
2694
+ setHighlightIndex(0);
2695
+ }, [filtered.length]);
2696
+ react.useEffect(() => {
2697
+ if (!open) return;
2698
+ const handler = (e) => {
2699
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
2700
+ setOpen(false);
2701
+ }
2702
+ };
2703
+ document.addEventListener("mousedown", handler);
2704
+ return () => document.removeEventListener("mousedown", handler);
2705
+ }, [open]);
2706
+ const triggerLabel = react.useMemo(() => {
2707
+ if (isProviderMode && activeProviderId) {
2708
+ const p = providers.find((prov) => prov.id === activeProviderId);
2709
+ return p ? p.label : placeholder;
2710
+ }
2711
+ if (!isProviderMode && selectedModel) {
2712
+ const m = models.find((mod) => mod.id === selectedModel);
2713
+ return m ? m.name : selectedModel;
2714
+ }
2715
+ return placeholder;
2716
+ }, [isProviderMode, activeProviderId, providers, selectedModel, models, placeholder]);
2717
+ const handleToggle = react.useCallback(() => {
2718
+ setOpen((prev) => {
2719
+ if (!prev) {
2720
+ setSearch("");
2721
+ setHighlightIndex(0);
2722
+ }
2723
+ return !prev;
2724
+ });
2725
+ }, []);
2726
+ const handleSelect = react.useCallback(
2727
+ (item) => {
2728
+ if (item.type === "provider" && onSelectProvider) {
2729
+ onSelectProvider(item.id);
2730
+ } else if (item.type === "model" && onSelectModel) {
2731
+ onSelectModel(item.id);
2732
+ }
2733
+ setOpen(false);
2734
+ setSearch("");
2735
+ },
2736
+ [onSelectProvider, onSelectModel]
2737
+ );
2738
+ const handleSearchChange = react.useCallback((e) => {
2739
+ setSearch(e.target.value);
2740
+ }, []);
2741
+ const handleKeyDown = react.useCallback(
2742
+ (e) => {
2743
+ if (e.key === "ArrowDown") {
2744
+ e.preventDefault();
2745
+ setHighlightIndex((prev) => Math.min(prev + 1, filtered.length - 1));
2746
+ } else if (e.key === "ArrowUp") {
2747
+ e.preventDefault();
2748
+ setHighlightIndex((prev) => Math.max(prev - 1, 0));
2749
+ } else if (e.key === "Enter") {
2750
+ e.preventDefault();
2751
+ if (filtered[highlightIndex]) {
2752
+ handleSelect(filtered[highlightIndex]);
2753
+ }
2754
+ } else if (e.key === "Escape") {
2755
+ e.preventDefault();
2756
+ setOpen(false);
2757
+ }
2758
+ },
2759
+ [filtered, highlightIndex, handleSelect]
2760
+ );
2761
+ const children = [];
2762
+ children.push(
2763
+ react.createElement(
2764
+ "button",
2765
+ {
2766
+ key: "trigger",
2767
+ type: "button",
2768
+ "data-pms-trigger": "true",
2769
+ onClick: handleToggle
2770
+ },
2771
+ triggerLabel
2772
+ )
2773
+ );
2774
+ if (open) {
2775
+ const dropdownChildren = [];
2776
+ if (items.length > 3) {
2777
+ dropdownChildren.push(
2778
+ react.createElement("input", {
2779
+ key: "search",
2780
+ "data-pms-search": "true",
2781
+ value: search,
2782
+ onChange: handleSearchChange,
2783
+ onKeyDown: handleKeyDown,
2784
+ placeholder: isProviderMode ? "Search providers..." : "Search models...",
2785
+ autoFocus: true
2786
+ })
2787
+ );
2788
+ }
2789
+ filtered.forEach((item, idx) => {
2790
+ const isActive = isProviderMode ? item.id === activeProviderId : item.id === selectedModel;
2791
+ const isHighlighted = idx === highlightIndex;
2792
+ const attrs = {
2793
+ key: item.id,
2794
+ "data-pms-item": "true",
2795
+ "data-pms-type": item.type,
2796
+ onClick: () => handleSelect(item)
2797
+ };
2798
+ if (item.tier) attrs["data-tier"] = item.tier;
2799
+ if (isActive) attrs["data-pms-active"] = "true";
2800
+ if (isHighlighted) attrs["data-pms-highlighted"] = "true";
2801
+ const itemChildren = [
2802
+ react.createElement("span", { key: "label", "data-pms-label": "true" }, item.label)
2803
+ ];
2804
+ if (item.sublabel) {
2805
+ itemChildren.push(
2806
+ react.createElement("span", { key: "sub", "data-pms-sublabel": "true" }, item.sublabel)
2807
+ );
2808
+ }
2809
+ dropdownChildren.push(react.createElement("div", attrs, ...itemChildren));
2810
+ });
2811
+ if (onSettingsClick) {
2812
+ dropdownChildren.push(
2813
+ react.createElement(
2814
+ "button",
2815
+ {
2816
+ key: "settings",
2817
+ type: "button",
2818
+ "data-pms-settings": "true",
2819
+ onClick: (e) => {
2820
+ e.stopPropagation();
2821
+ setOpen(false);
2822
+ onSettingsClick();
2823
+ }
2824
+ },
2825
+ "\u2699 Settings"
2826
+ )
2827
+ );
2828
+ }
2829
+ children.push(
2830
+ react.createElement(
2831
+ "div",
2832
+ { key: "dropdown", "data-pms-dropdown": "true", onKeyDown: handleKeyDown },
2833
+ ...dropdownChildren
2834
+ )
2835
+ );
2836
+ }
2837
+ return react.createElement(
2838
+ "div",
2839
+ {
2840
+ "data-provider-model-selector": "true",
2841
+ "data-pms-mode": isProviderMode ? "provider" : "model",
2842
+ className,
2843
+ ref: containerRef
2844
+ },
2845
+ ...children
2846
+ );
2847
+ }
2848
+ var BACKENDS = [
2849
+ { id: "copilot", label: "GitHub Copilot" },
2850
+ { id: "claude", label: "Claude" },
2851
+ { id: "vercel-ai", label: "Vercel AI" }
2852
+ ];
2853
+ var AUTH_FORMS = {
2854
+ copilot: CopilotAuthForm,
2855
+ claude: ClaudeAuthForm,
2856
+ "vercel-ai": VercelAIAuthForm
2857
+ };
2858
+ function ProviderSettings({
2859
+ providers,
2860
+ onClose,
2861
+ onProviderCreated,
2862
+ onProviderDeleted,
2863
+ onProviderUpdated,
2864
+ onAuthCompleted,
2865
+ authBaseUrl = "/api/auth",
2866
+ className
2867
+ }) {
2868
+ const [view, setView] = react.useState("list");
2869
+ const [addStep, setAddStep] = react.useState("select-backend");
2870
+ const [selectedBackend, setSelectedBackend] = react.useState("copilot");
2871
+ const [editingId, setEditingId] = react.useState(null);
2872
+ const [availableModels, setAvailableModels] = react.useState([]);
2873
+ const modelRef = react.useRef(null);
2874
+ const labelRef = react.useRef(null);
2875
+ const editModelRef = react.useRef(null);
2876
+ const editLabelRef = react.useRef(null);
2877
+ const runtime = useChatRuntime();
2878
+ const auth = useRemoteAuth({
2879
+ backend: selectedBackend,
2880
+ baseUrl: authBaseUrl
2881
+ });
2882
+ const handleBackendSelect = react.useCallback((backend) => {
2883
+ setSelectedBackend(backend);
2884
+ auth.reset();
2885
+ setAddStep("auth");
2886
+ }, [auth]);
2887
+ const handleAuthComplete = react.useCallback(() => {
2888
+ onAuthCompleted?.(selectedBackend);
2889
+ setAddStep("configure");
2890
+ }, [selectedBackend, onAuthCompleted]);
2891
+ react.useEffect(() => {
2892
+ if (addStep !== "configure" && view !== "edit") return;
2893
+ const load = async () => {
2894
+ try {
2895
+ const models = await runtime.listModels();
2896
+ setAvailableModels(models);
2897
+ } catch {
2898
+ }
2899
+ };
2900
+ load();
2901
+ }, [addStep, view, runtime]);
2902
+ const handleCreate = react.useCallback(() => {
2903
+ const modelEl = modelRef.current;
2904
+ const model = modelEl?.value?.trim();
2905
+ const label = labelRef.current?.value?.trim() || model;
2906
+ if (!model) return;
2907
+ const existing = providers.find((p) => p.backend === selectedBackend);
2908
+ if (existing) {
2909
+ onProviderUpdated?.(existing.id, { model, label });
2910
+ } else {
2911
+ onProviderCreated?.({ backend: selectedBackend, model, label });
2912
+ }
2913
+ setView("list");
2914
+ setAddStep("select-backend");
2915
+ setAvailableModels([]);
2916
+ auth.reset();
2917
+ }, [selectedBackend, providers, onProviderCreated, onProviderUpdated, auth]);
2918
+ const handleEdit = react.useCallback((id) => {
2919
+ setEditingId(id);
2920
+ setView("edit");
2921
+ }, []);
2922
+ const handleUpdate = react.useCallback(() => {
2923
+ if (!editingId) return;
2924
+ const modelEl = editModelRef.current;
2925
+ const model = modelEl?.value?.trim();
2926
+ const label = editLabelRef.current?.value?.trim();
2927
+ if (!model && !label) return;
2928
+ const changes = {};
2929
+ if (model) changes.model = model;
2930
+ if (label) changes.label = label;
2931
+ onProviderUpdated?.(editingId, changes);
2932
+ setView("list");
2933
+ setEditingId(null);
2934
+ }, [editingId, onProviderUpdated]);
2935
+ const handleDelete = react.useCallback((id) => {
2936
+ onProviderDeleted?.(id);
2937
+ }, [onProviderDeleted]);
2938
+ const handleStartAdd = react.useCallback(() => {
2939
+ setView("add");
2940
+ setAddStep("select-backend");
2941
+ auth.reset();
2942
+ }, [auth]);
2943
+ if (view === "list") {
2944
+ const items = providers.map(
2945
+ (p) => react.createElement(
2946
+ "div",
2947
+ { key: p.id, "data-provider-settings-item": "true" },
2948
+ react.createElement("span", { "data-provider-settings-label": "true" }, p.label),
2949
+ react.createElement("span", { "data-provider-settings-model": "true" }, `${p.backend} / ${p.model}`),
2950
+ react.createElement(
2951
+ "div",
2952
+ { "data-provider-settings-actions": "true" },
2953
+ react.createElement("button", {
2954
+ type: "button",
2955
+ "data-action": "edit-provider",
2956
+ onClick: () => handleEdit(p.id)
2957
+ }, "Edit"),
2958
+ react.createElement("button", {
2959
+ type: "button",
2960
+ "data-action": "delete-provider",
2961
+ onClick: () => handleDelete(p.id)
2962
+ }, "Delete")
2963
+ )
2964
+ )
2965
+ );
2966
+ return react.createElement(
2967
+ "div",
2968
+ { "data-provider-settings": "true", className },
2969
+ react.createElement(
2970
+ "div",
2971
+ { "data-provider-settings-header": "true" },
2972
+ react.createElement("span", null, "Providers"),
2973
+ onClose ? react.createElement("button", {
2974
+ type: "button",
2975
+ "data-provider-settings-close": "true",
2976
+ onClick: onClose
2977
+ }, "\u2715") : null
2978
+ ),
2979
+ react.createElement(
2980
+ "div",
2981
+ { "data-provider-settings-list": "true" },
2982
+ ...items,
2983
+ items.length === 0 ? react.createElement("div", { "data-provider-settings-empty": "true" }, "No providers configured") : null
2984
+ ),
2985
+ react.createElement("button", {
2986
+ type: "button",
2987
+ "data-action": "add-provider",
2988
+ onClick: handleStartAdd
2989
+ }, "+ Add Provider")
2990
+ );
2991
+ }
2992
+ if (view === "add") {
2993
+ const formChildren = [];
2994
+ formChildren.push(
2995
+ react.createElement(
2996
+ "div",
2997
+ { "data-provider-settings-header": "true", key: "header" },
2998
+ react.createElement("span", null, "Add Provider"),
2999
+ react.createElement("button", {
3000
+ type: "button",
3001
+ "data-provider-settings-close": "true",
3002
+ onClick: () => {
3003
+ setView("list");
3004
+ setAddStep("select-backend");
3005
+ }
3006
+ }, "\u2190 Back")
3007
+ )
3008
+ );
3009
+ if (addStep === "select-backend") {
3010
+ formChildren.push(
3011
+ react.createElement(
3012
+ "div",
3013
+ { key: "backends", "data-provider-settings-backends": "true" },
3014
+ ...BACKENDS.map(
3015
+ (b) => react.createElement("button", {
3016
+ key: b.id,
3017
+ type: "button",
3018
+ "data-provider-backend-option": b.id,
3019
+ onClick: () => handleBackendSelect(b.id)
3020
+ }, b.label)
3021
+ )
3022
+ )
3023
+ );
3024
+ } else if (addStep === "auth") {
3025
+ const FormComponent = selectedBackend ? AUTH_FORMS[selectedBackend] : null;
3026
+ formChildren.push(
3027
+ react.createElement(
3028
+ "div",
3029
+ { key: "auth", "data-provider-settings-auth": "true" },
3030
+ FormComponent ? react.createElement(FormComponent, { auth, onAuthComplete: handleAuthComplete }) : null
3031
+ )
3032
+ );
3033
+ } else if (addStep === "configure") {
3034
+ const modelInput = availableModels.length > 0 ? react.createElement(
3035
+ "select",
3036
+ {
3037
+ ref: modelRef,
3038
+ "data-input": "model",
3039
+ defaultValue: ""
3040
+ },
3041
+ react.createElement("option", { value: "", disabled: true }, "Select a model\u2026"),
3042
+ ...availableModels.map(
3043
+ (m) => react.createElement("option", { key: m.id, value: m.id }, m.name || m.id)
3044
+ )
3045
+ ) : react.createElement("input", {
3046
+ ref: modelRef,
3047
+ placeholder: "Model name (e.g. gpt-5-mini)",
3048
+ "data-input": "model"
3049
+ });
3050
+ formChildren.push(
3051
+ react.createElement(
3052
+ "div",
3053
+ { key: "config", "data-provider-settings-form": "true" },
3054
+ modelInput,
3055
+ react.createElement("input", {
3056
+ ref: labelRef,
3057
+ placeholder: "Display label (e.g. GPT-5 Mini)",
3058
+ "data-input": "label"
3059
+ }),
3060
+ react.createElement("button", {
3061
+ type: "button",
3062
+ "data-action": "save-provider",
3063
+ onClick: handleCreate
3064
+ }, "Save Provider")
3065
+ )
3066
+ );
3067
+ }
3068
+ return react.createElement(
3069
+ "div",
3070
+ { "data-provider-settings": "true", className },
3071
+ ...formChildren
3072
+ );
3073
+ }
3074
+ const editingProvider = providers.find((p) => p.id === editingId);
3075
+ return react.createElement(
3076
+ "div",
3077
+ { "data-provider-settings": "true", className },
3078
+ react.createElement(
3079
+ "div",
3080
+ { "data-provider-settings-header": "true" },
3081
+ react.createElement("span", null, "Edit Provider"),
3082
+ react.createElement("button", {
3083
+ type: "button",
3084
+ "data-provider-settings-close": "true",
3085
+ onClick: () => {
3086
+ setView("list");
3087
+ setEditingId(null);
3088
+ }
3089
+ }, "\u2190 Back")
3090
+ ),
3091
+ react.createElement(
3092
+ "div",
3093
+ { "data-provider-settings-form": "true" },
3094
+ availableModels.length > 0 ? react.createElement(
3095
+ "select",
3096
+ {
3097
+ ref: editModelRef,
3098
+ defaultValue: editingProvider?.model ?? "",
3099
+ "data-input": "model"
3100
+ },
3101
+ react.createElement("option", { value: "", disabled: true }, "Select a model\u2026"),
3102
+ ...availableModels.map(
3103
+ (m) => react.createElement("option", { key: m.id, value: m.id }, m.name || m.id)
3104
+ )
3105
+ ) : react.createElement("input", {
3106
+ ref: editModelRef,
3107
+ defaultValue: editingProvider?.model ?? "",
3108
+ placeholder: "Model name",
3109
+ "data-input": "model"
3110
+ }),
3111
+ react.createElement("input", {
3112
+ ref: editLabelRef,
3113
+ defaultValue: editingProvider?.label ?? "",
3114
+ placeholder: "Display label",
3115
+ "data-input": "label"
3116
+ }),
3117
+ react.createElement("button", {
3118
+ type: "button",
3119
+ "data-action": "update-provider",
3120
+ onClick: handleUpdate
3121
+ }, "Update")
3122
+ )
3123
+ );
3124
+ }
3125
+ function formatTokens(n) {
3126
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
3127
+ return String(n);
3128
+ }
3129
+ function ContextStatsDisplay({ stats, className }) {
3130
+ if (!stats) return null;
3131
+ const hasRealData = stats.realPromptTokens != null && stats.modelContextWindow != null;
3132
+ if (!hasRealData) return null;
3133
+ const promptTokens = stats.realPromptTokens;
3134
+ const contextWindow = stats.modelContextWindow;
3135
+ const availableBudget = Math.max(0, contextWindow - promptTokens);
3136
+ const usagePercent = contextWindow > 0 ? Math.round(promptTokens / contextWindow * 100) : 0;
3137
+ return react.createElement(
3138
+ "div",
3139
+ {
3140
+ "data-context-stats": "",
3141
+ "data-context-truncated": stats.wasTruncated ? "true" : "false",
3142
+ className
3143
+ },
3144
+ react.createElement(
3145
+ "span",
3146
+ { "data-context-tokens": "" },
3147
+ `${formatTokens(promptTokens)} tokens`
3148
+ ),
3149
+ react.createElement(
3150
+ "span",
3151
+ { "data-context-budget": "" },
3152
+ `${formatTokens(availableBudget)} available`
3153
+ ),
3154
+ react.createElement(
3155
+ "span",
3156
+ { "data-context-usage": "", "data-usage-percent": String(usagePercent) },
3157
+ `${usagePercent}%`
3158
+ ),
3159
+ stats.removedCount > 0 ? react.createElement(
3160
+ "span",
3161
+ { "data-context-removed": "" },
3162
+ `${stats.removedCount} trimmed`
3163
+ ) : null
3164
+ );
3165
+ }
3166
+ function ChatLayout({ children, sidebar, overlay, className }) {
3167
+ const overlayNodes = Array.isArray(overlay) ? overlay : [overlay ?? null];
3168
+ return react.createElement(
3169
+ "div",
3170
+ { "data-chat-ui": "", className },
3171
+ ...overlayNodes,
3172
+ sidebar ?? null,
3173
+ children
3174
+ );
3175
+ }
3176
+ function ChatHeader({
3177
+ showBackendSelector = false,
3178
+ showModelSelector = true,
3179
+ hasProviders = false,
3180
+ backends = [],
3181
+ models = [],
3182
+ selectedModel,
3183
+ onBackendSelect,
3184
+ onModelSelect,
3185
+ BackendSelectorComponent: BSC = BackendSelector,
3186
+ ModelSelectorComponent: MSC = ModelSelector
3187
+ }) {
3188
+ const children = [];
3189
+ if (showBackendSelector) {
3190
+ children.push(
3191
+ react.createElement(BSC, {
3192
+ key: "backend-selector",
3193
+ backends,
3194
+ onSelect: onBackendSelect ?? (() => {
3195
+ })
3196
+ })
3197
+ );
3198
+ }
3199
+ if (showModelSelector && !hasProviders && models.length > 0) {
3200
+ children.push(
3201
+ react.createElement(MSC, {
3202
+ key: "model-selector",
3203
+ models,
3204
+ selectedModel,
3205
+ onSelect: onModelSelect ?? (() => {
3206
+ })
3207
+ })
3208
+ );
3209
+ }
3210
+ if (children.length === 0) return null;
3211
+ return react.createElement("div", { "data-chat-header": "" }, ...children);
3212
+ }
3213
+ function UsageBadge({ usage, className }) {
3214
+ if (!usage) return null;
3215
+ return react.createElement(
3216
+ "span",
3217
+ { "data-usage-badge": "true", className },
3218
+ react.createElement("span", { "data-usage-tokens": "prompt" }, `\u2191${usage.promptTokens}`),
3219
+ react.createElement("span", { "data-usage-tokens": "completion" }, `\u2193${usage.completionTokens}`),
3220
+ react.createElement("span", { "data-usage-tokens": "total" }, `\u03A3${usage.totalTokens}`)
3221
+ );
3222
+ }
3223
+
3224
+ // src/chat/react/ChatInputArea.ts
3225
+ function ChatInputArea({
3226
+ onSend,
3227
+ onStop,
3228
+ isGenerating,
3229
+ placeholder,
3230
+ providers = [],
3231
+ models = [],
3232
+ activeProviderId,
3233
+ selectedModel,
3234
+ onSelectProvider,
3235
+ onSelectModel,
3236
+ onSettingsClick,
3237
+ ComposerComponent: CC = Composer,
3238
+ ProviderModelSelectorComponent: PMSC = ProviderModelSelector,
3239
+ usage
3240
+ }) {
3241
+ const selectorRow = react.createElement(
3242
+ "div",
3243
+ { "data-chat-input-controls": "" },
3244
+ react.createElement(PMSC, {
3245
+ providers,
3246
+ models,
3247
+ activeProviderId,
3248
+ selectedModel,
3249
+ onSelectProvider,
3250
+ onSelectModel,
3251
+ onSettingsClick
3252
+ }),
3253
+ usage ? react.createElement(UsageBadge, { usage }) : null
3254
+ );
3255
+ return react.createElement(
3256
+ "div",
3257
+ { "data-chat-input-area": "" },
3258
+ react.createElement(
3259
+ "div",
3260
+ { "data-chat-input-container": "" },
3261
+ selectorRow,
3262
+ react.createElement(CC, {
3263
+ onSend,
3264
+ onStop,
3265
+ isGenerating,
3266
+ placeholder
3267
+ })
3268
+ )
3269
+ );
3270
+ }
3271
+ var FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
3272
+ var CLOSE_ANIMATION_MS = 150;
3273
+ function ChatSettingsOverlay({
3274
+ open,
3275
+ onClose,
3276
+ providers = [],
3277
+ authBaseUrl,
3278
+ onProviderCreated,
3279
+ onProviderDeleted,
3280
+ onProviderUpdated,
3281
+ onAuthCompleted,
3282
+ ProviderSettingsComponent: PSC = ProviderSettings
3283
+ }) {
3284
+ const [isClosing, setIsClosing] = react.useState(false);
3285
+ const contentRef = react.useRef(null);
3286
+ const previousFocusRef = react.useRef(null);
3287
+ const handleClose = react.useCallback(() => {
3288
+ setIsClosing(true);
3289
+ setTimeout(() => {
3290
+ setIsClosing(false);
3291
+ onClose();
3292
+ }, CLOSE_ANIMATION_MS);
3293
+ }, [onClose]);
3294
+ const handleKeyDown = react.useCallback((e) => {
3295
+ if (e.key === "Escape") {
3296
+ e.stopPropagation();
3297
+ handleClose();
3298
+ return;
3299
+ }
3300
+ if (e.key === "Tab" && contentRef.current) {
3301
+ const focusable = Array.from(contentRef.current.querySelectorAll(FOCUSABLE));
3302
+ if (focusable.length === 0) return;
3303
+ const first = focusable[0];
3304
+ const last = focusable[focusable.length - 1];
3305
+ if (e.shiftKey && document.activeElement === first) {
3306
+ e.preventDefault();
3307
+ last.focus();
3308
+ } else if (!e.shiftKey && document.activeElement === last) {
3309
+ e.preventDefault();
3310
+ first.focus();
3311
+ }
3312
+ }
3313
+ }, [handleClose]);
3314
+ const handleBackdropClick = react.useCallback((e) => {
3315
+ if (e.target === e.currentTarget) handleClose();
3316
+ }, [handleClose]);
3317
+ react.useEffect(() => {
3318
+ if (!open) return;
3319
+ previousFocusRef.current = document.activeElement;
3320
+ document.addEventListener("keydown", handleKeyDown);
3321
+ const timer = setTimeout(() => {
3322
+ if (contentRef.current) {
3323
+ const first = contentRef.current.querySelector(FOCUSABLE);
3324
+ if (first) first.focus();
3325
+ }
3326
+ }, 50);
3327
+ return () => {
3328
+ document.removeEventListener("keydown", handleKeyDown);
3329
+ clearTimeout(timer);
3330
+ const prev = previousFocusRef.current;
3331
+ if (prev && typeof prev.focus === "function") prev.focus();
3332
+ };
3333
+ }, [open, handleKeyDown]);
3334
+ if (!open && !isClosing) return null;
3335
+ return react.createElement(
3336
+ "div",
3337
+ {
3338
+ "data-provider-settings-overlay": "true",
3339
+ "data-closing": isClosing ? "true" : void 0,
3340
+ onClick: handleBackdropClick,
3341
+ role: "dialog",
3342
+ "aria-modal": "true",
3343
+ "aria-label": "Provider settings"
3344
+ },
3345
+ react.createElement(
3346
+ "div",
3347
+ {
3348
+ ref: contentRef,
3349
+ "data-provider-settings-content": "true",
3350
+ "data-closing": isClosing ? "true" : void 0
3351
+ },
3352
+ react.createElement(PSC, {
3353
+ providers,
3354
+ authBaseUrl,
3355
+ onClose: handleClose,
3356
+ onProviderCreated: onProviderCreated ?? void 0,
3357
+ onProviderDeleted: onProviderDeleted ?? void 0,
3358
+ onProviderUpdated: onProviderUpdated ?? void 0,
3359
+ onAuthCompleted: onAuthCompleted ?? void 0
3360
+ })
3361
+ )
3362
+ );
3363
+ }
3364
+
3365
+ // src/chat/react/ChatUI.ts
3366
+ function ChatUIInner({
3367
+ slots,
3368
+ className,
3369
+ showSidebar = true,
3370
+ showModelSelector = true,
3371
+ showBackendSelector: showBackendSelectorProp,
3372
+ showProviderSelector: _showProviderSelectorProp,
3373
+ authBaseUrl,
3374
+ placeholder
3375
+ }) {
3376
+ const runtime = useChatRuntime();
3377
+ const { messages, sendMessage, stop, isGenerating, newSession, error, clearError, retryLastMessage, usage } = useChat();
3378
+ const { sessions } = useSessions();
3379
+ const { models, refresh: refreshModels } = useModels();
3380
+ const { backends } = useBackends();
3381
+ const { providers, createProvider, updateProvider, deleteProvider, selectProvider, refresh } = useProviders();
3382
+ const [settingsOpen, setSettingsOpen] = react.useState(false);
3383
+ const [activeProviderId, setActiveProviderId] = react.useState(void 0);
3384
+ const [selectedModelId, setSelectedModelId] = react.useState(void 0);
3385
+ const [searchQuery, setSearchQuery] = react.useState("");
3386
+ const hasProviders = providers.length > 0;
3387
+ react.useEffect(() => {
3388
+ if (providers.length > 0 && !activeProviderId) {
3389
+ setActiveProviderId(providers[0].id);
3390
+ selectProvider(providers[0].id);
3391
+ }
3392
+ }, [providers, activeProviderId, selectProvider]);
3393
+ const showBackendSelectorResolved = showBackendSelectorProp ?? false;
3394
+ const handleSelect = react.useCallback(async (id) => {
3395
+ try {
3396
+ await runtime.switchSession(id);
3397
+ } catch {
3398
+ }
3399
+ }, [runtime]);
3400
+ const handleCreate = react.useCallback(async () => {
3401
+ try {
3402
+ await newSession();
3403
+ } catch {
3404
+ }
3405
+ }, [newSession]);
3406
+ const handleDelete = react.useCallback(async (id) => {
3407
+ try {
3408
+ await runtime.deleteSession(id);
3409
+ } catch {
3410
+ }
3411
+ }, [runtime]);
3412
+ const handleModelSelect = react.useCallback((modelId) => {
3413
+ setSelectedModelId(modelId);
3414
+ }, []);
3415
+ const handleBackendSelect = react.useCallback((_name) => {
3416
+ refreshModels();
3417
+ }, [refreshModels]);
3418
+ const handleProviderSelect = react.useCallback((id) => {
3419
+ selectProvider(id);
3420
+ setActiveProviderId(id);
3421
+ }, [selectProvider]);
3422
+ const hasSlotOverrides = !!(slots?.renderToolCall || slots?.renderMessage || slots?.renderThinkingBlock);
3423
+ const ThreadComponent = slots?.thread ?? Thread;
3424
+ const ThreadListComponent = slots?.threadList ?? ThreadList;
3425
+ const ContextStatsComponent = slots?.contextStats ?? ContextStatsDisplay;
3426
+ const [contextStats, setContextStats] = react.useState(null);
3427
+ react.useEffect(() => {
3428
+ if (!runtime.activeSessionId) {
3429
+ setContextStats(null);
3430
+ return;
3431
+ }
3432
+ const result = runtime.getContextStats(runtime.activeSessionId);
3433
+ if (result instanceof Promise) {
3434
+ result.then(setContextStats, () => setContextStats(null));
3435
+ } else {
3436
+ setContextStats(result);
3437
+ }
3438
+ }, [runtime, runtime.activeSessionId, messages.length]);
3439
+ const showEmptyState = !hasProviders && messages.length === 0;
3440
+ const mainContent = react.createElement(
3441
+ "div",
3442
+ { "data-chat-main": "" },
3443
+ react.createElement(ChatHeader, {
3444
+ showBackendSelector: showBackendSelectorResolved,
3445
+ showModelSelector,
3446
+ hasProviders,
3447
+ backends,
3448
+ models,
3449
+ selectedModel: selectedModelId,
3450
+ onBackendSelect: handleBackendSelect,
3451
+ onModelSelect: handleModelSelect,
3452
+ BackendSelectorComponent: slots?.backendSelector,
3453
+ ModelSelectorComponent: slots?.modelSelector
3454
+ }),
3455
+ contextStats ? react.createElement(ContextStatsComponent, { stats: contextStats }) : null,
3456
+ error ? react.createElement(
3457
+ "div",
3458
+ { "data-chat-error": "" },
3459
+ react.createElement("span", { "data-chat-error-text": "" }, error.message),
3460
+ react.createElement(
3461
+ "div",
3462
+ { "data-chat-error-actions": "" },
3463
+ react.createElement("button", {
3464
+ "data-action": "retry",
3465
+ type: "button",
3466
+ onClick: retryLastMessage
3467
+ }, "Retry"),
3468
+ react.createElement("button", {
3469
+ "data-action": "dismiss-error",
3470
+ type: "button",
3471
+ onClick: clearError
3472
+ }, "\u2715")
3473
+ ),
3474
+ error.stack ? react.createElement(
3475
+ "details",
3476
+ { "data-chat-error-details": "" },
3477
+ react.createElement("summary", null, "Details"),
3478
+ react.createElement("pre", null, error.stack)
3479
+ ) : null
3480
+ ) : null,
3481
+ showEmptyState ? react.createElement(
3482
+ "div",
3483
+ { "data-chat-empty-state": "" },
3484
+ react.createElement("div", { "data-chat-empty-title": "" }, "Connect a provider to start chatting"),
3485
+ react.createElement("button", {
3486
+ "data-action": "open-settings",
3487
+ onClick: () => setSettingsOpen(true)
3488
+ }, "+ Connect Provider")
3489
+ ) : react.createElement(ThreadComponent, { messages, isGenerating, autoScroll: true }),
3490
+ react.createElement(ChatInputArea, {
3491
+ onSend: sendMessage,
3492
+ onStop: stop,
3493
+ isGenerating,
3494
+ placeholder,
3495
+ providers,
3496
+ models,
3497
+ activeProviderId,
3498
+ selectedModel: selectedModelId,
3499
+ onSelectProvider: handleProviderSelect,
3500
+ onSelectModel: handleModelSelect,
3501
+ onSettingsClick: () => setSettingsOpen(true),
3502
+ ComposerComponent: slots?.composer,
3503
+ ProviderModelSelectorComponent: slots?.providerModelSelector,
3504
+ usage
3505
+ })
3506
+ );
3507
+ const wrappedMain = hasSlotOverrides ? react.createElement(ThreadProvider, {
3508
+ renderToolCall: slots.renderToolCall,
3509
+ renderMessage: slots.renderMessage,
3510
+ renderThinkingBlock: slots.renderThinkingBlock,
3511
+ children: mainContent
3512
+ }) : mainContent;
3513
+ const sidebar = showSidebar ? react.createElement(ThreadListComponent, {
3514
+ sessions,
3515
+ activeSessionId: runtime.activeSessionId ?? void 0,
3516
+ onSelect: handleSelect,
3517
+ onCreate: handleCreate,
3518
+ onDelete: handleDelete,
3519
+ searchQuery,
3520
+ onSearchChange: setSearchQuery
3521
+ }) : void 0;
3522
+ const settingsOverlay = react.createElement(ChatSettingsOverlay, {
3523
+ open: settingsOpen,
3524
+ onClose: () => setSettingsOpen(false),
3525
+ providers,
3526
+ authBaseUrl,
3527
+ onProviderCreated: (p) => createProvider({ backend: p.backend, model: p.model, label: p.label ?? "" }),
3528
+ onProviderDeleted: (id) => deleteProvider(id),
3529
+ onProviderUpdated: (id, changes) => updateProvider(id, changes),
3530
+ onAuthCompleted: () => refresh(),
3531
+ ProviderSettingsComponent: slots?.providerSettings
3532
+ });
3533
+ return react.createElement(ChatLayout, {
3534
+ className,
3535
+ sidebar,
3536
+ overlay: [slots?.authDialog ?? null, settingsOverlay],
3537
+ children: wrappedMain
3538
+ });
3539
+ }
3540
+ function ChatUI({ runtime, ...rest }) {
3541
+ return react.createElement(ChatProvider, {
3542
+ runtime,
3543
+ children: react.createElement(ChatUIInner, rest)
3544
+ });
3545
+ }
3546
+
3547
+ exports.BackendSelector = BackendSelector;
3548
+ exports.ChatHeader = ChatHeader;
3549
+ exports.ChatInputArea = ChatInputArea;
3550
+ exports.ChatLayout = ChatLayout;
3551
+ exports.ChatProvider = ChatProvider;
3552
+ exports.ChatSettingsOverlay = ChatSettingsOverlay;
3553
+ exports.ChatUI = ChatUI;
3554
+ exports.ClaudeAuthForm = ClaudeAuthForm;
3555
+ exports.Composer = Composer;
3556
+ exports.ContextStatsDisplay = ContextStatsDisplay;
3557
+ exports.CopilotAuthForm = CopilotAuthForm;
3558
+ exports.MarkdownRenderer = MarkdownRenderer;
3559
+ exports.Message = Message;
3560
+ exports.ModelSelector = ModelSelector;
3561
+ exports.ProviderModelSelector = ProviderModelSelector;
3562
+ exports.ProviderSelector = ProviderSelector;
3563
+ exports.ProviderSettings = ProviderSettings;
3564
+ exports.RemoteChatClient = RemoteChatClient;
3565
+ exports.ThinkingBlock = ThinkingBlock;
3566
+ exports.Thread = Thread;
3567
+ exports.ThreadList = ThreadList;
3568
+ exports.ThreadProvider = ThreadProvider;
3569
+ exports.ToolCallView = ToolCallView;
3570
+ exports.UsageBadge = UsageBadge;
3571
+ exports.VercelAIAuthForm = VercelAIAuthForm;
3572
+ exports.useApiKeyAuth = useApiKeyAuth;
3573
+ exports.useBackends = useBackends;
3574
+ exports.useChat = useChat;
3575
+ exports.useChatRuntime = useChatRuntime;
3576
+ exports.useClaudeAuth = useClaudeAuth;
3577
+ exports.useCopilotAuth = useCopilotAuth;
3578
+ exports.useMessages = useMessages;
3579
+ exports.useModels = useModels;
3580
+ exports.useOptionalThreadSlots = useOptionalThreadSlots;
3581
+ exports.useProviders = useProviders;
3582
+ exports.useRemoteAuth = useRemoteAuth;
3583
+ exports.useRemoteChat = useRemoteChat;
3584
+ exports.useSSE = useSSE;
3585
+ exports.useSessions = useSessions;
3586
+ exports.useThreadSlots = useThreadSlots;
3587
+ exports.useToolApproval = useToolApproval;
3588
+ //# sourceMappingURL=react.cjs.map
3589
+ //# sourceMappingURL=react.cjs.map