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