@witqq/agent-sdk 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -34
- package/dist/{types-CqvUAYxt.d.cts → agent-CW9XbmG_.d.ts} +137 -102
- package/dist/{types-CqvUAYxt.d.ts → agent-DxY68NZL.d.cts} +137 -102
- package/dist/auth/index.cjs +72 -1
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +21 -154
- package/dist/auth/index.d.ts +21 -154
- package/dist/auth/index.js +72 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/backends/claude.cjs +480 -261
- package/dist/backends/claude.cjs.map +1 -1
- package/dist/backends/claude.d.cts +3 -1
- package/dist/backends/claude.d.ts +3 -1
- package/dist/backends/claude.js +480 -261
- package/dist/backends/claude.js.map +1 -1
- package/dist/backends/copilot.cjs +329 -97
- package/dist/backends/copilot.cjs.map +1 -1
- package/dist/backends/copilot.d.cts +12 -4
- package/dist/backends/copilot.d.ts +12 -4
- package/dist/backends/copilot.js +329 -97
- package/dist/backends/copilot.js.map +1 -1
- package/dist/backends/vercel-ai.cjs +294 -61
- package/dist/backends/vercel-ai.cjs.map +1 -1
- package/dist/backends/vercel-ai.d.cts +3 -1
- package/dist/backends/vercel-ai.d.ts +3 -1
- package/dist/backends/vercel-ai.js +294 -61
- package/dist/backends/vercel-ai.js.map +1 -1
- package/dist/backends-BSrsBYFn.d.cts +39 -0
- package/dist/backends-BSrsBYFn.d.ts +39 -0
- package/dist/chat/accumulator.cjs +1 -1
- package/dist/chat/accumulator.cjs.map +1 -1
- package/dist/chat/accumulator.d.cts +5 -2
- package/dist/chat/accumulator.d.ts +5 -2
- package/dist/chat/accumulator.js +1 -1
- package/dist/chat/accumulator.js.map +1 -1
- package/dist/chat/backends.cjs +736 -746
- package/dist/chat/backends.cjs.map +1 -1
- package/dist/chat/backends.d.cts +10 -6
- package/dist/chat/backends.d.ts +10 -6
- package/dist/chat/backends.js +736 -725
- package/dist/chat/backends.js.map +1 -1
- package/dist/chat/context.cjs +50 -0
- package/dist/chat/context.cjs.map +1 -1
- package/dist/chat/context.d.cts +27 -3
- package/dist/chat/context.d.ts +27 -3
- package/dist/chat/context.js +50 -0
- package/dist/chat/context.js.map +1 -1
- package/dist/chat/core.cjs +25 -2
- package/dist/chat/core.cjs.map +1 -1
- package/dist/chat/core.d.cts +30 -381
- package/dist/chat/core.d.ts +30 -381
- package/dist/chat/core.js +24 -3
- package/dist/chat/core.js.map +1 -1
- package/dist/chat/errors.cjs +48 -26
- package/dist/chat/errors.cjs.map +1 -1
- package/dist/chat/errors.d.cts +6 -31
- package/dist/chat/errors.d.ts +6 -31
- package/dist/chat/errors.js +48 -25
- package/dist/chat/errors.js.map +1 -1
- package/dist/chat/events.cjs.map +1 -1
- package/dist/chat/events.d.cts +6 -2
- package/dist/chat/events.d.ts +6 -2
- package/dist/chat/events.js.map +1 -1
- package/dist/chat/index.cjs +1199 -1008
- package/dist/chat/index.cjs.map +1 -1
- package/dist/chat/index.d.cts +35 -10
- package/dist/chat/index.d.ts +35 -10
- package/dist/chat/index.js +1196 -987
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react/theme.css +2517 -0
- package/dist/chat/react.cjs +2003 -1153
- package/dist/chat/react.cjs.map +1 -1
- package/dist/chat/react.d.cts +590 -121
- package/dist/chat/react.d.ts +590 -121
- package/dist/chat/react.js +1984 -1151
- package/dist/chat/react.js.map +1 -1
- package/dist/chat/runtime.cjs +401 -186
- package/dist/chat/runtime.cjs.map +1 -1
- package/dist/chat/runtime.d.cts +92 -28
- package/dist/chat/runtime.d.ts +92 -28
- package/dist/chat/runtime.js +401 -186
- package/dist/chat/runtime.js.map +1 -1
- package/dist/chat/server.cjs +2234 -209
- package/dist/chat/server.cjs.map +1 -1
- package/dist/chat/server.d.cts +451 -90
- package/dist/chat/server.d.ts +451 -90
- package/dist/chat/server.js +2221 -210
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/sessions.cjs +25 -43
- package/dist/chat/sessions.cjs.map +1 -1
- package/dist/chat/sessions.d.cts +37 -118
- package/dist/chat/sessions.d.ts +37 -118
- package/dist/chat/sessions.js +25 -43
- package/dist/chat/sessions.js.map +1 -1
- package/dist/chat/sqlite.cjs +441 -0
- package/dist/chat/sqlite.cjs.map +1 -0
- package/dist/chat/sqlite.d.cts +128 -0
- package/dist/chat/sqlite.d.ts +128 -0
- package/dist/chat/sqlite.js +435 -0
- package/dist/chat/sqlite.js.map +1 -0
- package/dist/chat/state.cjs +14 -1
- package/dist/chat/state.cjs.map +1 -1
- package/dist/chat/state.d.cts +5 -2
- package/dist/chat/state.d.ts +5 -2
- package/dist/chat/state.js +14 -1
- package/dist/chat/state.js.map +1 -1
- package/dist/chat/storage.cjs +19 -10
- package/dist/chat/storage.cjs.map +1 -1
- package/dist/chat/storage.d.cts +11 -5
- package/dist/chat/storage.d.ts +11 -5
- package/dist/chat/storage.js +19 -10
- package/dist/chat/storage.js.map +1 -1
- package/dist/errors-C-so0M4t.d.cts +33 -0
- package/dist/errors-C-so0M4t.d.ts +33 -0
- package/dist/errors-CmVvczxZ.d.cts +28 -0
- package/dist/errors-CmVvczxZ.d.ts +28 -0
- package/dist/{in-process-transport-C2oPTYs6.d.ts → in-process-transport-C1JnJGVR.d.ts} +28 -23
- package/dist/{in-process-transport-DG-w5G6k.d.cts → in-process-transport-C7DSqPyX.d.cts} +28 -23
- package/dist/index.cjs +340 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +292 -123
- package/dist/index.d.ts +292 -123
- package/dist/index.js +334 -47
- package/dist/index.js.map +1 -1
- package/dist/provider-types-PTSlRPNB.d.cts +39 -0
- package/dist/provider-types-PTSlRPNB.d.ts +39 -0
- package/dist/refresh-manager-B81PpYBr.d.cts +153 -0
- package/dist/refresh-manager-Dlv_iNZi.d.ts +153 -0
- package/dist/testing.cjs +383 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +132 -0
- package/dist/testing.d.ts +132 -0
- package/dist/testing.js +377 -0
- package/dist/testing.js.map +1 -0
- package/dist/token-store-CSUBgYwn.d.ts +48 -0
- package/dist/token-store-CuC4hB9Z.d.cts +48 -0
- package/dist/{transport-DX1Nhm4N.d.cts → transport-Cdh3M0tS.d.cts} +5 -4
- package/dist/{transport-D1OaUgRk.d.ts → transport-Ciap4PWK.d.ts} +5 -4
- package/dist/{types-CGF7AEX1.d.cts → types-4vbcmPTp.d.cts} +4 -2
- package/dist/{types-Bh5AhqD-.d.ts → types-BxggH0Yh.d.ts} +4 -2
- package/dist/types-DRgd_9R7.d.cts +363 -0
- package/dist/types-ajANVzf7.d.ts +363 -0
- package/package.json +31 -6
- package/dist/errors-BDLbNu9w.d.cts +0 -13
- package/dist/errors-BDLbNu9w.d.ts +0 -13
- package/dist/types-DLZzlJxt.d.ts +0 -39
- package/dist/types-tE0CXwBl.d.cts +0 -39
package/dist/chat/react.js
CHANGED
|
@@ -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
|
-
|
|
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/
|
|
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,6 +271,14 @@ 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) {
|
|
834
284
|
accumulator.apply(agentEvent);
|
|
@@ -885,6 +335,10 @@ function useChat(options = {}) {
|
|
|
885
335
|
const clearError = useCallback(() => {
|
|
886
336
|
setError(null);
|
|
887
337
|
}, []);
|
|
338
|
+
const retryLastMessage = useCallback(async () => {
|
|
339
|
+
if (!lastUserMessageRef.current || generatingRef.current) return;
|
|
340
|
+
await sendMessage(lastUserMessageRef.current);
|
|
341
|
+
}, [sendMessage]);
|
|
888
342
|
const newSession = useCallback(async () => {
|
|
889
343
|
const session = await runtime.createSession({
|
|
890
344
|
config: { model: "", backend: "" }
|
|
@@ -892,6 +346,7 @@ function useChat(options = {}) {
|
|
|
892
346
|
setSessionId(session.id);
|
|
893
347
|
setMessages([]);
|
|
894
348
|
setError(null);
|
|
349
|
+
lastUserMessageRef.current = null;
|
|
895
350
|
return session.id;
|
|
896
351
|
}, [runtime]);
|
|
897
352
|
return {
|
|
@@ -903,7 +358,9 @@ function useChat(options = {}) {
|
|
|
903
358
|
status,
|
|
904
359
|
error,
|
|
905
360
|
clearError,
|
|
906
|
-
|
|
361
|
+
retryLastMessage,
|
|
362
|
+
newSession,
|
|
363
|
+
usage
|
|
907
364
|
};
|
|
908
365
|
}
|
|
909
366
|
var EMPTY_MESSAGES = [];
|
|
@@ -951,7 +408,7 @@ function useMessages(options) {
|
|
|
951
408
|
messagesRef.current = session.messages;
|
|
952
409
|
isLoadedRef.current = true;
|
|
953
410
|
emitChange();
|
|
954
|
-
if (session
|
|
411
|
+
if (isObservableSession(session)) {
|
|
955
412
|
unsubscribe = session.subscribe(() => {
|
|
956
413
|
const snapshot = session.getSnapshot();
|
|
957
414
|
messagesRef.current = snapshot.messages;
|
|
@@ -1023,124 +480,6 @@ function useSessions() {
|
|
|
1023
480
|
}, [fetchSessions]);
|
|
1024
481
|
return { sessions, loading, error, refresh };
|
|
1025
482
|
}
|
|
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
483
|
function parseBlocks(text) {
|
|
1145
484
|
const tokens = [];
|
|
1146
485
|
const lines = text.split("\n");
|
|
@@ -1289,23 +628,174 @@ function MarkdownRenderer(props) {
|
|
|
1289
628
|
const children = tokens.map((token, i) => renderBlock(token, i, props));
|
|
1290
629
|
return createElement("div", { "data-md-root": true }, ...children);
|
|
1291
630
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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");
|
|
631
|
+
function ThinkingBlock({ text, isStreaming, defaultOpen }) {
|
|
632
|
+
const attrs = {
|
|
633
|
+
"data-thinking": "true"
|
|
634
|
+
};
|
|
635
|
+
if (isStreaming) {
|
|
636
|
+
attrs["data-streaming"] = "true";
|
|
1306
637
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
638
|
+
if (defaultOpen) {
|
|
639
|
+
attrs.open = true;
|
|
640
|
+
}
|
|
641
|
+
return createElement(
|
|
642
|
+
"details",
|
|
643
|
+
attrs,
|
|
644
|
+
createElement("summary", null, isStreaming ? "Thinking..." : "Reasoning"),
|
|
645
|
+
createElement("div", null, text)
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
function ToolCallView({ part, onApprove, onDeny, renderArgs, renderResult }) {
|
|
649
|
+
const children = [];
|
|
650
|
+
children.push(
|
|
651
|
+
createElement(
|
|
652
|
+
"div",
|
|
653
|
+
{ key: "header", "data-tool-header": "true" },
|
|
654
|
+
createElement("span", { "data-tool-label": "name" }, part.name),
|
|
655
|
+
createElement("span", { "data-tool-label": "status" }, part.status)
|
|
656
|
+
)
|
|
657
|
+
);
|
|
658
|
+
if (part.args !== void 0) {
|
|
659
|
+
children.push(
|
|
660
|
+
renderArgs ? renderArgs(part.args) : createElement(
|
|
661
|
+
"details",
|
|
662
|
+
{ key: "args", "data-tool-details": "args" },
|
|
663
|
+
createElement("summary", null, "Arguments"),
|
|
664
|
+
createElement("pre", { "data-tool-label": "args" }, JSON.stringify(part.args, null, 2))
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
if (part.result !== void 0) {
|
|
669
|
+
children.push(
|
|
670
|
+
renderResult ? renderResult(part.result) : createElement(
|
|
671
|
+
"details",
|
|
672
|
+
{ key: "result", "data-tool-details": "result", open: true },
|
|
673
|
+
createElement("summary", null, "Result"),
|
|
674
|
+
createElement("pre", { "data-tool-label": "result" }, JSON.stringify(part.result, null, 2))
|
|
675
|
+
)
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
if (part.error) {
|
|
679
|
+
children.push(
|
|
680
|
+
createElement("span", { key: "error", "data-tool-label": "error", role: "alert" }, part.error)
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
if (part.status === "requires_approval") {
|
|
684
|
+
children.push(
|
|
685
|
+
createElement(
|
|
686
|
+
"div",
|
|
687
|
+
{ key: "actions", "data-tool-actions": "true" },
|
|
688
|
+
createElement("button", { key: "approve", onClick: onApprove, "data-action": "approve" }, "Approve"),
|
|
689
|
+
createElement("button", { key: "deny", onClick: onDeny, "data-action": "deny" }, "Deny")
|
|
690
|
+
)
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return createElement(
|
|
694
|
+
"div",
|
|
695
|
+
{
|
|
696
|
+
"data-tool-status": part.status,
|
|
697
|
+
"data-tool-name": part.name
|
|
698
|
+
},
|
|
699
|
+
...children
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/chat/react/Message.ts
|
|
704
|
+
function defaultRenderText(part, index) {
|
|
705
|
+
return createElement(
|
|
706
|
+
"div",
|
|
707
|
+
{ key: index, "data-part": "text" },
|
|
708
|
+
createElement(MarkdownRenderer, { content: part.text })
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
function defaultRenderReasoning(part, index) {
|
|
712
|
+
return createElement(ThinkingBlock, {
|
|
713
|
+
key: index,
|
|
714
|
+
text: part.text,
|
|
715
|
+
isStreaming: part.status === "streaming"
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
function defaultRenderToolCall(part, index) {
|
|
719
|
+
return createElement(
|
|
720
|
+
"div",
|
|
721
|
+
{ key: index, "data-part": "tool_call" },
|
|
722
|
+
createElement(ToolCallView, { part })
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
function defaultRenderSource(part, index) {
|
|
726
|
+
return createElement("a", { key: index, href: part.url, "data-part": "source" }, part.title ?? part.url);
|
|
727
|
+
}
|
|
728
|
+
function defaultRenderFile(part, index) {
|
|
729
|
+
return createElement("span", { key: index, "data-part": "file" }, part.name);
|
|
730
|
+
}
|
|
731
|
+
function renderPart(props, part, index) {
|
|
732
|
+
switch (part.type) {
|
|
733
|
+
case "text":
|
|
734
|
+
return (props.renderText ?? defaultRenderText)(part, index);
|
|
735
|
+
case "reasoning":
|
|
736
|
+
return (props.renderReasoning ?? defaultRenderReasoning)(part, index);
|
|
737
|
+
case "tool_call":
|
|
738
|
+
return (props.renderToolCall ?? defaultRenderToolCall)(part, index);
|
|
739
|
+
case "source":
|
|
740
|
+
return (props.renderSource ?? defaultRenderSource)(part, index);
|
|
741
|
+
case "file":
|
|
742
|
+
return (props.renderFile ?? defaultRenderFile)(part, index);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function Message(props) {
|
|
746
|
+
const { message } = props;
|
|
747
|
+
const children = message.parts.map((part, i) => renderPart(props, part, i));
|
|
748
|
+
return createElement(
|
|
749
|
+
"div",
|
|
750
|
+
{
|
|
751
|
+
"data-role": message.role,
|
|
752
|
+
"data-status": message.status
|
|
753
|
+
},
|
|
754
|
+
...children
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
function useToolApproval(messages, onApprove, onDeny) {
|
|
758
|
+
const pendingRequests = useMemo(() => {
|
|
759
|
+
const requests = [];
|
|
760
|
+
for (const msg of messages) {
|
|
761
|
+
for (const part of msg.parts) {
|
|
762
|
+
if (part.type === "tool_call" && part.status === "requires_approval") {
|
|
763
|
+
requests.push({
|
|
764
|
+
toolCallId: part.toolCallId,
|
|
765
|
+
toolName: part.name,
|
|
766
|
+
toolArgs: part.args ?? {},
|
|
767
|
+
messageId: msg.id
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return requests;
|
|
773
|
+
}, [messages]);
|
|
774
|
+
const approve = useCallback((toolCallId) => {
|
|
775
|
+
onApprove?.(toolCallId);
|
|
776
|
+
}, [onApprove]);
|
|
777
|
+
const deny = useCallback((toolCallId) => {
|
|
778
|
+
onDeny?.(toolCallId);
|
|
779
|
+
}, [onDeny]);
|
|
780
|
+
return { pendingRequests, approve, deny };
|
|
781
|
+
}
|
|
782
|
+
var ThreadSlotsContext = createContext(null);
|
|
783
|
+
function ThreadProvider({
|
|
784
|
+
children,
|
|
785
|
+
renderMessage,
|
|
786
|
+
renderToolCall,
|
|
787
|
+
renderThinkingBlock
|
|
788
|
+
}) {
|
|
789
|
+
const value = { renderMessage, renderToolCall, renderThinkingBlock };
|
|
790
|
+
return createElement(ThreadSlotsContext.Provider, { value }, children);
|
|
791
|
+
}
|
|
792
|
+
function useThreadSlots() {
|
|
793
|
+
const ctx = useContext(ThreadSlotsContext);
|
|
794
|
+
if (!ctx) {
|
|
795
|
+
throw new Error("useThreadSlots must be used within a ThreadProvider");
|
|
796
|
+
}
|
|
797
|
+
return ctx;
|
|
798
|
+
}
|
|
1309
799
|
function useOptionalThreadSlots() {
|
|
1310
800
|
return useContext(ThreadSlotsContext);
|
|
1311
801
|
}
|
|
@@ -1320,12 +810,22 @@ function Thread({
|
|
|
1320
810
|
const sentinelRef = useRef(null);
|
|
1321
811
|
const containerRef = useRef(null);
|
|
1322
812
|
const [userScrolledUp, setUserScrolledUp] = useState(false);
|
|
813
|
+
const isScrollingProgrammatically = useRef(false);
|
|
1323
814
|
const handleScroll = useCallback(() => {
|
|
815
|
+
if (isScrollingProgrammatically.current) return;
|
|
1324
816
|
const el = containerRef.current;
|
|
1325
817
|
if (!el) return;
|
|
1326
818
|
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 1;
|
|
1327
819
|
setUserScrolledUp(!atBottom);
|
|
1328
820
|
}, []);
|
|
821
|
+
const scrollToBottom = useCallback(() => {
|
|
822
|
+
isScrollingProgrammatically.current = true;
|
|
823
|
+
sentinelRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
824
|
+
setUserScrolledUp(false);
|
|
825
|
+
setTimeout(() => {
|
|
826
|
+
isScrollingProgrammatically.current = false;
|
|
827
|
+
}, 500);
|
|
828
|
+
}, []);
|
|
1329
829
|
useEffect(() => {
|
|
1330
830
|
if (!autoScroll || userScrolledUp) return;
|
|
1331
831
|
sentinelRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
@@ -1337,20 +837,55 @@ function Thread({
|
|
|
1337
837
|
}
|
|
1338
838
|
attrs.ref = containerRef;
|
|
1339
839
|
attrs.onScroll = handleScroll;
|
|
1340
|
-
const children =
|
|
840
|
+
const children = [];
|
|
841
|
+
if (messages.length === 0 && !isGenerating) {
|
|
842
|
+
children.push(
|
|
843
|
+
createElement(
|
|
844
|
+
"div",
|
|
845
|
+
{ key: "__empty", "data-thread-empty": "true" },
|
|
846
|
+
"Start a conversation"
|
|
847
|
+
)
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
for (let i = 0; i < messages.length; i++) {
|
|
851
|
+
const msg = messages[i];
|
|
1341
852
|
const content = slots?.renderMessage ? slots.renderMessage(msg, i) : createElement(Message, {
|
|
1342
853
|
key: msg.id,
|
|
1343
854
|
message: msg,
|
|
1344
855
|
renderToolCall: slots?.renderToolCall,
|
|
1345
856
|
renderReasoning: slots?.renderThinkingBlock ? (part, idx) => slots.renderThinkingBlock(part, idx) : void 0
|
|
1346
857
|
});
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
858
|
+
children.push(
|
|
859
|
+
createElement(
|
|
860
|
+
"div",
|
|
861
|
+
{ key: msg.id, "data-thread-message": "true", "data-role": msg.role },
|
|
862
|
+
content
|
|
863
|
+
)
|
|
1351
864
|
);
|
|
1352
|
-
}
|
|
865
|
+
}
|
|
866
|
+
if (isGenerating) {
|
|
867
|
+
children.push(
|
|
868
|
+
createElement(
|
|
869
|
+
"div",
|
|
870
|
+
{ key: "__loading", "data-thread-loading-indicator": "true" },
|
|
871
|
+
createElement("span", null),
|
|
872
|
+
createElement("span", null),
|
|
873
|
+
createElement("span", null)
|
|
874
|
+
)
|
|
875
|
+
);
|
|
876
|
+
}
|
|
1353
877
|
children.push(createElement("div", { key: "__sentinel", ref: sentinelRef }));
|
|
878
|
+
if (userScrolledUp) {
|
|
879
|
+
children.push(
|
|
880
|
+
createElement("button", {
|
|
881
|
+
key: "__scroll-to-bottom",
|
|
882
|
+
"data-action": "scroll-to-bottom",
|
|
883
|
+
type: "button",
|
|
884
|
+
onClick: scrollToBottom,
|
|
885
|
+
"aria-label": "Scroll to bottom"
|
|
886
|
+
})
|
|
887
|
+
);
|
|
888
|
+
}
|
|
1354
889
|
return createElement("div", attrs, ...children);
|
|
1355
890
|
}
|
|
1356
891
|
function Composer({
|
|
@@ -1397,21 +932,20 @@ function Composer({
|
|
|
1397
932
|
[]
|
|
1398
933
|
);
|
|
1399
934
|
const children = [
|
|
1400
|
-
createElement(
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
createElement(
|
|
935
|
+
createElement(
|
|
936
|
+
"div",
|
|
937
|
+
{ key: "input-wrapper", "data-input": "" },
|
|
938
|
+
createElement("textarea", {
|
|
939
|
+
ref: textareaRef,
|
|
940
|
+
value,
|
|
941
|
+
onChange: handleChange,
|
|
942
|
+
onKeyDown: handleKeyDown,
|
|
943
|
+
placeholder,
|
|
944
|
+
disabled: disabled || false,
|
|
945
|
+
"aria-label": "Message input",
|
|
946
|
+
rows: 1
|
|
947
|
+
}),
|
|
948
|
+
isGenerating ? createElement(
|
|
1415
949
|
"button",
|
|
1416
950
|
{
|
|
1417
951
|
key: "stop",
|
|
@@ -1420,11 +954,7 @@ function Composer({
|
|
|
1420
954
|
type: "button"
|
|
1421
955
|
},
|
|
1422
956
|
"Stop"
|
|
1423
|
-
)
|
|
1424
|
-
);
|
|
1425
|
-
} else {
|
|
1426
|
-
children.push(
|
|
1427
|
-
createElement(
|
|
957
|
+
) : createElement(
|
|
1428
958
|
"button",
|
|
1429
959
|
{
|
|
1430
960
|
key: "send",
|
|
@@ -1435,8 +965,8 @@ function Composer({
|
|
|
1435
965
|
},
|
|
1436
966
|
"Send"
|
|
1437
967
|
)
|
|
1438
|
-
)
|
|
1439
|
-
|
|
968
|
+
)
|
|
969
|
+
];
|
|
1440
970
|
return createElement(
|
|
1441
971
|
"div",
|
|
1442
972
|
{ "data-composer": "true", className },
|
|
@@ -1446,6 +976,20 @@ function Composer({
|
|
|
1446
976
|
function isFullSession(item) {
|
|
1447
977
|
return "messages" in item && Array.isArray(item.messages);
|
|
1448
978
|
}
|
|
979
|
+
function formatRelativeTime(date) {
|
|
980
|
+
const now = Date.now();
|
|
981
|
+
const diff = now - date.getTime();
|
|
982
|
+
const seconds = Math.floor(diff / 1e3);
|
|
983
|
+
if (seconds < 60) return "now";
|
|
984
|
+
const minutes = Math.floor(seconds / 60);
|
|
985
|
+
if (minutes < 60) return `${minutes}m`;
|
|
986
|
+
const hours = Math.floor(minutes / 60);
|
|
987
|
+
if (hours < 24) return `${hours}h`;
|
|
988
|
+
const days = Math.floor(hours / 24);
|
|
989
|
+
if (days < 30) return `${days}d`;
|
|
990
|
+
const months = Math.floor(days / 30);
|
|
991
|
+
return `${months}mo`;
|
|
992
|
+
}
|
|
1449
993
|
function normalizeSession(item) {
|
|
1450
994
|
if (isFullSession(item)) {
|
|
1451
995
|
return {
|
|
@@ -1483,32 +1027,38 @@ function ThreadList({
|
|
|
1483
1027
|
return normalized.filter((s) => (s.title ?? "").toLowerCase().includes(q));
|
|
1484
1028
|
}, [normalized, searchQuery]);
|
|
1485
1029
|
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
1030
|
children.push(
|
|
1496
1031
|
createElement(
|
|
1497
|
-
"
|
|
1498
|
-
{
|
|
1499
|
-
|
|
1500
|
-
"data-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1032
|
+
"div",
|
|
1033
|
+
{ key: "header", "data-thread-list-header": "true" },
|
|
1034
|
+
createElement("input", {
|
|
1035
|
+
"data-thread-list-search": "true",
|
|
1036
|
+
value: searchQuery ?? "",
|
|
1037
|
+
onChange: handleSearchChange,
|
|
1038
|
+
placeholder: "Search..."
|
|
1039
|
+
}),
|
|
1040
|
+
createElement(
|
|
1041
|
+
"button",
|
|
1042
|
+
{
|
|
1043
|
+
"data-action": "create-session",
|
|
1044
|
+
onClick: onCreate,
|
|
1045
|
+
type: "button"
|
|
1046
|
+
},
|
|
1047
|
+
"+"
|
|
1048
|
+
)
|
|
1505
1049
|
)
|
|
1506
1050
|
);
|
|
1507
1051
|
const items = filtered.map((session) => {
|
|
1508
1052
|
const isActive = session.id === activeSessionId;
|
|
1509
1053
|
const itemChildren = [
|
|
1510
|
-
createElement("span", { key: "title" }, session.title ?? "Untitled")
|
|
1054
|
+
createElement("span", { key: "title", "data-session-title": "true" }, session.title ?? "Untitled")
|
|
1511
1055
|
];
|
|
1056
|
+
if (session.updatedAt) {
|
|
1057
|
+
const timeStr = formatRelativeTime(new Date(session.updatedAt));
|
|
1058
|
+
itemChildren.push(
|
|
1059
|
+
createElement("span", { key: "time", "data-session-time": "true" }, timeStr)
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1512
1062
|
if (onDelete) {
|
|
1513
1063
|
itemChildren.push(
|
|
1514
1064
|
createElement(
|
|
@@ -1522,7 +1072,7 @@ function ThreadList({
|
|
|
1522
1072
|
},
|
|
1523
1073
|
type: "button"
|
|
1524
1074
|
},
|
|
1525
|
-
"
|
|
1075
|
+
"\xD7"
|
|
1526
1076
|
)
|
|
1527
1077
|
);
|
|
1528
1078
|
}
|
|
@@ -1532,6 +1082,7 @@ function ThreadList({
|
|
|
1532
1082
|
key: session.id,
|
|
1533
1083
|
"data-session-item": "true",
|
|
1534
1084
|
"data-session-active": isActive ? "true" : "false",
|
|
1085
|
+
"data-session-status": session.status ?? "active",
|
|
1535
1086
|
onClick: () => onSelect(session.id)
|
|
1536
1087
|
},
|
|
1537
1088
|
...itemChildren
|
|
@@ -1582,13 +1133,19 @@ function useSSE(url, options = {}) {
|
|
|
1582
1133
|
setStatus("connecting");
|
|
1583
1134
|
(async () => {
|
|
1584
1135
|
try {
|
|
1585
|
-
const
|
|
1136
|
+
const fetchInit = {
|
|
1137
|
+
method: optionsRef.current.method ?? "GET",
|
|
1586
1138
|
headers: {
|
|
1587
1139
|
Accept: "text/event-stream",
|
|
1588
1140
|
...optionsRef.current.headers
|
|
1589
1141
|
},
|
|
1590
1142
|
signal: controller.signal
|
|
1591
|
-
}
|
|
1143
|
+
};
|
|
1144
|
+
if (fetchInit.method === "POST" && optionsRef.current.body !== void 0) {
|
|
1145
|
+
fetchInit.headers["Content-Type"] = "application/json";
|
|
1146
|
+
fetchInit.body = JSON.stringify(optionsRef.current.body);
|
|
1147
|
+
}
|
|
1148
|
+
const response = await fetch(url, fetchInit);
|
|
1592
1149
|
if (!response.ok) {
|
|
1593
1150
|
throw new Error(`SSE request failed: ${response.status}`);
|
|
1594
1151
|
}
|
|
@@ -1677,7 +1234,8 @@ function useModels() {
|
|
|
1677
1234
|
const mapped = result.map((m) => ({
|
|
1678
1235
|
id: m.id,
|
|
1679
1236
|
name: m.name ?? m.id,
|
|
1680
|
-
tier: m.provider
|
|
1237
|
+
tier: m.provider,
|
|
1238
|
+
provider: m.provider
|
|
1681
1239
|
}));
|
|
1682
1240
|
setModels(mapped);
|
|
1683
1241
|
} catch (err) {
|
|
@@ -1710,11 +1268,13 @@ function ModelSelector({
|
|
|
1710
1268
|
selectedModel,
|
|
1711
1269
|
onSelect,
|
|
1712
1270
|
placeholder = "Select model",
|
|
1713
|
-
className
|
|
1271
|
+
className,
|
|
1272
|
+
allowFreeText = true
|
|
1714
1273
|
}) {
|
|
1715
1274
|
const [open, setOpen] = useState(false);
|
|
1716
1275
|
const [search, setSearch] = useState("");
|
|
1717
1276
|
const [highlightIndex, setHighlightIndex] = useState(0);
|
|
1277
|
+
const [freeText, setFreeText] = useState(selectedModel ?? "");
|
|
1718
1278
|
const containerRef = useRef(null);
|
|
1719
1279
|
const filtered = useMemo(() => {
|
|
1720
1280
|
if (!search) return models;
|
|
@@ -1768,6 +1328,42 @@ function ModelSelector({
|
|
|
1768
1328
|
},
|
|
1769
1329
|
[filtered, highlightIndex, handleSelect]
|
|
1770
1330
|
);
|
|
1331
|
+
if (models.length === 0 && allowFreeText) {
|
|
1332
|
+
return createElement(
|
|
1333
|
+
"div",
|
|
1334
|
+
{
|
|
1335
|
+
"data-model-selector": "true",
|
|
1336
|
+
"data-model-selector-freetext": "true",
|
|
1337
|
+
className,
|
|
1338
|
+
ref: containerRef
|
|
1339
|
+
},
|
|
1340
|
+
createElement("input", {
|
|
1341
|
+
"data-model-input": "true",
|
|
1342
|
+
value: freeText,
|
|
1343
|
+
placeholder: "Enter model name...",
|
|
1344
|
+
onChange: (e) => setFreeText(e.target.value),
|
|
1345
|
+
onKeyDown: (e) => {
|
|
1346
|
+
if (e.key === "Enter") {
|
|
1347
|
+
e.preventDefault();
|
|
1348
|
+
const val = freeText.trim();
|
|
1349
|
+
if (val) onSelect(val);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}),
|
|
1353
|
+
createElement(
|
|
1354
|
+
"button",
|
|
1355
|
+
{
|
|
1356
|
+
type: "button",
|
|
1357
|
+
"data-action": "apply-model",
|
|
1358
|
+
onClick: () => {
|
|
1359
|
+
const val = freeText.trim();
|
|
1360
|
+
if (val) onSelect(val);
|
|
1361
|
+
}
|
|
1362
|
+
},
|
|
1363
|
+
"Apply"
|
|
1364
|
+
)
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1771
1367
|
const children = [];
|
|
1772
1368
|
children.push(
|
|
1773
1369
|
createElement(
|
|
@@ -1794,6 +1390,7 @@ function ModelSelector({
|
|
|
1794
1390
|
autoFocus: true
|
|
1795
1391
|
})
|
|
1796
1392
|
);
|
|
1393
|
+
const hasMultipleProviders = new Set(filtered.map((m) => m.provider).filter(Boolean)).size > 1;
|
|
1797
1394
|
filtered.forEach((model, idx) => {
|
|
1798
1395
|
const isSelected = model.id === selectedModel;
|
|
1799
1396
|
const isHighlighted = idx === highlightIndex;
|
|
@@ -1805,13 +1402,17 @@ function ModelSelector({
|
|
|
1805
1402
|
if (model.tier) {
|
|
1806
1403
|
attrs["data-tier"] = model.tier;
|
|
1807
1404
|
}
|
|
1405
|
+
if (model.provider && hasMultipleProviders) {
|
|
1406
|
+
attrs["data-model-provider"] = model.provider;
|
|
1407
|
+
}
|
|
1808
1408
|
if (isSelected) {
|
|
1809
1409
|
attrs["data-model-selected"] = "true";
|
|
1810
1410
|
}
|
|
1811
1411
|
if (isHighlighted) {
|
|
1812
1412
|
attrs["data-model-highlighted"] = "true";
|
|
1813
1413
|
}
|
|
1814
|
-
|
|
1414
|
+
const label = model.provider && hasMultipleProviders ? `${model.name} (${model.provider})` : model.name;
|
|
1415
|
+
dropdownChildren.push(createElement("div", attrs, label));
|
|
1815
1416
|
});
|
|
1816
1417
|
children.push(
|
|
1817
1418
|
createElement(
|
|
@@ -1831,124 +1432,203 @@ function ModelSelector({
|
|
|
1831
1432
|
...children
|
|
1832
1433
|
);
|
|
1833
1434
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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;
|
|
1435
|
+
function useCopilotAuth(options) {
|
|
1436
|
+
const { baseUrl, headers } = options;
|
|
1437
|
+
const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1846
1438
|
const [status, setStatus] = useState("idle");
|
|
1847
1439
|
const [error, setError] = useState(null);
|
|
1848
1440
|
const [token, setToken] = useState(null);
|
|
1849
1441
|
const [deviceCode, setDeviceCode] = useState(null);
|
|
1850
1442
|
const [verificationUrl, setVerificationUrl] = useState(null);
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
const
|
|
1856
|
-
|
|
1443
|
+
const onAuthenticatedRef = useRef(options.onAuthenticated);
|
|
1444
|
+
onAuthenticatedRef.current = options.onAuthenticated;
|
|
1445
|
+
const onErrorRef = useRef(options.onError);
|
|
1446
|
+
onErrorRef.current = options.onError;
|
|
1447
|
+
const post = useCallback(
|
|
1448
|
+
async (path, body) => {
|
|
1449
|
+
const res = await fetchFn(`${baseUrl}${path}`, {
|
|
1450
|
+
method: "POST",
|
|
1451
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
1452
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1453
|
+
});
|
|
1454
|
+
const data = await res.json();
|
|
1455
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
1456
|
+
return data;
|
|
1457
|
+
},
|
|
1458
|
+
[baseUrl, fetchFn, headers]
|
|
1459
|
+
);
|
|
1460
|
+
const start = useCallback(async () => {
|
|
1857
1461
|
setStatus("pending");
|
|
1858
1462
|
setError(null);
|
|
1859
1463
|
try {
|
|
1860
|
-
const
|
|
1861
|
-
const auth = new CopilotAuth2();
|
|
1862
|
-
const result = await auth.startDeviceFlow();
|
|
1464
|
+
const result = await post("/auth/start", { provider: "copilot" });
|
|
1863
1465
|
setDeviceCode(result.userCode);
|
|
1864
1466
|
setVerificationUrl(result.verificationUrl);
|
|
1865
|
-
|
|
1467
|
+
await post("/auth/copilot/poll");
|
|
1468
|
+
const authToken = {
|
|
1469
|
+
accessToken: "server-managed",
|
|
1470
|
+
tokenType: "bearer",
|
|
1471
|
+
obtainedAt: Date.now()
|
|
1472
|
+
};
|
|
1866
1473
|
setToken(authToken);
|
|
1867
1474
|
setStatus("authenticated");
|
|
1868
1475
|
onAuthenticatedRef.current?.(authToken);
|
|
1869
1476
|
} catch (err) {
|
|
1870
|
-
|
|
1477
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1478
|
+
setError(e);
|
|
1871
1479
|
setStatus("error");
|
|
1480
|
+
onErrorRef.current?.(e);
|
|
1872
1481
|
}
|
|
1873
|
-
}, [
|
|
1874
|
-
const
|
|
1875
|
-
|
|
1876
|
-
|
|
1482
|
+
}, [post]);
|
|
1483
|
+
const reset = useCallback(() => {
|
|
1484
|
+
setStatus("idle");
|
|
1485
|
+
setError(null);
|
|
1486
|
+
setToken(null);
|
|
1487
|
+
setDeviceCode(null);
|
|
1488
|
+
setVerificationUrl(null);
|
|
1489
|
+
}, []);
|
|
1490
|
+
return { status, error, token, deviceCode, verificationUrl, start, reset };
|
|
1491
|
+
}
|
|
1492
|
+
function useClaudeAuth(options) {
|
|
1493
|
+
const { baseUrl, headers } = options;
|
|
1494
|
+
const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1495
|
+
const [status, setStatus] = useState("idle");
|
|
1496
|
+
const [error, setError] = useState(null);
|
|
1497
|
+
const [token, setToken] = useState(null);
|
|
1498
|
+
const [authorizeUrl, setAuthorizeUrl] = useState(null);
|
|
1499
|
+
const onAuthenticatedRef = useRef(options.onAuthenticated);
|
|
1500
|
+
onAuthenticatedRef.current = options.onAuthenticated;
|
|
1501
|
+
const onErrorRef = useRef(options.onError);
|
|
1502
|
+
onErrorRef.current = options.onError;
|
|
1503
|
+
const post = useCallback(
|
|
1504
|
+
async (path, body) => {
|
|
1505
|
+
const res = await fetchFn(`${baseUrl}${path}`, {
|
|
1506
|
+
method: "POST",
|
|
1507
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
1508
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1509
|
+
});
|
|
1510
|
+
const data = await res.json();
|
|
1511
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
1512
|
+
return data;
|
|
1513
|
+
},
|
|
1514
|
+
[baseUrl, fetchFn, headers]
|
|
1515
|
+
);
|
|
1516
|
+
const start = useCallback(async () => {
|
|
1517
|
+
setStatus("pending");
|
|
1877
1518
|
setError(null);
|
|
1878
1519
|
try {
|
|
1879
|
-
const
|
|
1880
|
-
const auth = new ClaudeAuth2();
|
|
1881
|
-
const result = auth.startOAuthFlow();
|
|
1520
|
+
const result = await post("/auth/start", { provider: "claude" });
|
|
1882
1521
|
setAuthorizeUrl(result.authorizeUrl);
|
|
1883
|
-
completeAuthRef.current = result.completeAuth;
|
|
1884
1522
|
} catch (err) {
|
|
1885
|
-
|
|
1523
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1524
|
+
setError(e);
|
|
1886
1525
|
setStatus("error");
|
|
1526
|
+
onErrorRef.current?.(e);
|
|
1887
1527
|
}
|
|
1888
|
-
}, [
|
|
1889
|
-
const
|
|
1890
|
-
if (!completeAuthRef.current) return;
|
|
1528
|
+
}, [post]);
|
|
1529
|
+
const complete = useCallback(async (codeOrUrl) => {
|
|
1891
1530
|
try {
|
|
1892
|
-
|
|
1531
|
+
await post("/auth/claude/complete", { code: codeOrUrl });
|
|
1532
|
+
const authToken = {
|
|
1533
|
+
accessToken: "server-managed",
|
|
1534
|
+
tokenType: "bearer",
|
|
1535
|
+
obtainedAt: Date.now()
|
|
1536
|
+
};
|
|
1893
1537
|
setToken(authToken);
|
|
1894
1538
|
setStatus("authenticated");
|
|
1895
1539
|
onAuthenticatedRef.current?.(authToken);
|
|
1896
1540
|
} catch (err) {
|
|
1897
|
-
|
|
1541
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1542
|
+
setError(e);
|
|
1898
1543
|
setStatus("error");
|
|
1544
|
+
onErrorRef.current?.(e);
|
|
1899
1545
|
}
|
|
1546
|
+
}, [post]);
|
|
1547
|
+
const reset = useCallback(() => {
|
|
1548
|
+
setStatus("idle");
|
|
1549
|
+
setError(null);
|
|
1550
|
+
setToken(null);
|
|
1551
|
+
setAuthorizeUrl(null);
|
|
1900
1552
|
}, []);
|
|
1901
|
-
|
|
1902
|
-
|
|
1553
|
+
return { status, error, token, authorizeUrl, start, complete, reset };
|
|
1554
|
+
}
|
|
1555
|
+
function useApiKeyAuth(options) {
|
|
1556
|
+
const { baseUrl, headers } = options;
|
|
1557
|
+
const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1558
|
+
const [status, setStatus] = useState("idle");
|
|
1559
|
+
const [error, setError] = useState(null);
|
|
1560
|
+
const [token, setToken] = useState(null);
|
|
1561
|
+
const onAuthenticatedRef = useRef(options.onAuthenticated);
|
|
1562
|
+
onAuthenticatedRef.current = options.onAuthenticated;
|
|
1563
|
+
const onErrorRef = useRef(options.onError);
|
|
1564
|
+
onErrorRef.current = options.onError;
|
|
1565
|
+
const submit = useCallback(async (key, apiBaseUrl) => {
|
|
1903
1566
|
if (!key || !key.trim()) {
|
|
1904
|
-
|
|
1567
|
+
const e = new Error("API key cannot be empty");
|
|
1568
|
+
setError(e);
|
|
1905
1569
|
setStatus("error");
|
|
1570
|
+
onErrorRef.current?.(e);
|
|
1906
1571
|
return;
|
|
1907
1572
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1573
|
+
setStatus("pending");
|
|
1574
|
+
setError(null);
|
|
1575
|
+
try {
|
|
1576
|
+
const res = await fetchFn(`${baseUrl}/auth/vercel/complete`, {
|
|
1577
|
+
method: "POST",
|
|
1578
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
1579
|
+
body: JSON.stringify({
|
|
1580
|
+
apiKey: key.trim(),
|
|
1581
|
+
...apiBaseUrl ? { baseUrl: apiBaseUrl } : {}
|
|
1582
|
+
})
|
|
1583
|
+
});
|
|
1584
|
+
const data = await res.json();
|
|
1585
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
1586
|
+
const authToken = {
|
|
1587
|
+
accessToken: "server-managed",
|
|
1588
|
+
tokenType: "bearer",
|
|
1589
|
+
obtainedAt: Date.now()
|
|
1590
|
+
};
|
|
1591
|
+
setToken(authToken);
|
|
1592
|
+
setStatus("authenticated");
|
|
1593
|
+
onAuthenticatedRef.current?.(authToken);
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1596
|
+
setError(e);
|
|
1597
|
+
setStatus("error");
|
|
1598
|
+
onErrorRef.current?.(e);
|
|
1599
|
+
}
|
|
1600
|
+
}, [baseUrl, fetchFn, headers]);
|
|
1917
1601
|
const reset = useCallback(() => {
|
|
1918
1602
|
setStatus("idle");
|
|
1919
1603
|
setError(null);
|
|
1920
1604
|
setToken(null);
|
|
1921
|
-
setDeviceCode(null);
|
|
1922
|
-
setVerificationUrl(null);
|
|
1923
|
-
setAuthorizeUrl(null);
|
|
1924
|
-
completeAuthRef.current = null;
|
|
1925
1605
|
}, []);
|
|
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
|
-
};
|
|
1606
|
+
return { status, error, token, submit, reset };
|
|
1939
1607
|
}
|
|
1608
|
+
|
|
1609
|
+
// src/chat/react/useRemoteAuth.ts
|
|
1940
1610
|
function useRemoteAuth(options) {
|
|
1941
1611
|
const { backend, baseUrl, onAuthenticated, headers } = options;
|
|
1942
1612
|
const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1943
1613
|
const [status, setStatus] = useState("idle");
|
|
1944
1614
|
const [error, setError] = useState(null);
|
|
1945
1615
|
const [token, setToken] = useState(null);
|
|
1946
|
-
const [deviceCode, setDeviceCode] = useState(null);
|
|
1947
|
-
const [verificationUrl, setVerificationUrl] = useState(null);
|
|
1948
|
-
const [authorizeUrl, setAuthorizeUrl] = useState(null);
|
|
1949
1616
|
const [savedProviders, setSavedProviders] = useState([]);
|
|
1950
1617
|
const onAuthenticatedRef = useRef(onAuthenticated);
|
|
1951
1618
|
onAuthenticatedRef.current = onAuthenticated;
|
|
1619
|
+
const handleSuccess = useCallback((authToken) => {
|
|
1620
|
+
setToken(authToken);
|
|
1621
|
+
setStatus("authenticated");
|
|
1622
|
+
onAuthenticatedRef.current?.(authToken);
|
|
1623
|
+
}, []);
|
|
1624
|
+
const handleError = useCallback((err) => {
|
|
1625
|
+
setError(err);
|
|
1626
|
+
setStatus("error");
|
|
1627
|
+
}, []);
|
|
1628
|
+
const hookOpts = { baseUrl, headers, fetch: fetchFn, onAuthenticated: handleSuccess, onError: handleError };
|
|
1629
|
+
const copilot = useCopilotAuth(hookOpts);
|
|
1630
|
+
const claude = useClaudeAuth(hookOpts);
|
|
1631
|
+
const apiKey = useApiKeyAuth(hookOpts);
|
|
1952
1632
|
const post = useCallback(
|
|
1953
1633
|
async (path, body) => {
|
|
1954
1634
|
const res = await fetchFn(`${baseUrl}${path}`, {
|
|
@@ -1978,82 +1658,26 @@ function useRemoteAuth(options) {
|
|
|
1978
1658
|
if (backend !== "copilot") return;
|
|
1979
1659
|
setStatus("pending");
|
|
1980
1660
|
setError(null);
|
|
1981
|
-
|
|
1982
|
-
|
|
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]);
|
|
1661
|
+
await copilot.start();
|
|
1662
|
+
}, [backend, copilot]);
|
|
1999
1663
|
const startOAuthFlow = useCallback(async () => {
|
|
2000
1664
|
if (backend !== "claude") return;
|
|
2001
1665
|
setStatus("pending");
|
|
2002
1666
|
setError(null);
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
setAuthorizeUrl(result.authorizeUrl);
|
|
2006
|
-
} catch (err) {
|
|
2007
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2008
|
-
setStatus("error");
|
|
2009
|
-
}
|
|
2010
|
-
}, [backend, post]);
|
|
1667
|
+
await claude.start();
|
|
1668
|
+
}, [backend, claude]);
|
|
2011
1669
|
const completeOAuth = useCallback(
|
|
2012
1670
|
async (codeOrUrl) => {
|
|
2013
|
-
|
|
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
|
-
}
|
|
1671
|
+
await claude.complete(codeOrUrl);
|
|
2027
1672
|
},
|
|
2028
|
-
[
|
|
1673
|
+
[claude]
|
|
2029
1674
|
);
|
|
2030
1675
|
const submitApiKey = useCallback(
|
|
2031
1676
|
async (key, apiBaseUrl) => {
|
|
2032
1677
|
if (backend !== "vercel-ai") return;
|
|
2033
|
-
|
|
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
|
-
}
|
|
1678
|
+
await apiKey.submit(key, apiBaseUrl);
|
|
2055
1679
|
},
|
|
2056
|
-
[backend,
|
|
1680
|
+
[backend, apiKey]
|
|
2057
1681
|
);
|
|
2058
1682
|
const loadSavedTokens = useCallback(async () => {
|
|
2059
1683
|
try {
|
|
@@ -2092,55 +1716,44 @@ function useRemoteAuth(options) {
|
|
|
2092
1716
|
setStatus("idle");
|
|
2093
1717
|
setError(null);
|
|
2094
1718
|
setToken(null);
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
1719
|
+
copilot.reset();
|
|
1720
|
+
claude.reset();
|
|
1721
|
+
apiKey.reset();
|
|
2098
1722
|
setSavedProviders([]);
|
|
2099
|
-
}, []);
|
|
1723
|
+
}, [copilot, claude, apiKey]);
|
|
2100
1724
|
const start = useCallback(async (provider) => {
|
|
2101
1725
|
const target = provider ?? backend;
|
|
2102
1726
|
setStatus("pending");
|
|
2103
1727
|
setError(null);
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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}`);
|
|
1728
|
+
switch (target) {
|
|
1729
|
+
case "copilot":
|
|
1730
|
+
await copilot.start();
|
|
1731
|
+
break;
|
|
1732
|
+
case "claude":
|
|
1733
|
+
await claude.start();
|
|
1734
|
+
break;
|
|
1735
|
+
case "vercel-ai": {
|
|
1736
|
+
const e = new Error("vercel-ai requires submitApiKey(key, baseUrl) \u2014 cannot auto-start");
|
|
1737
|
+
setError(e);
|
|
1738
|
+
setStatus("error");
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
default: {
|
|
1742
|
+
const e = new Error(`Unknown auth provider: ${target}`);
|
|
1743
|
+
setError(e);
|
|
1744
|
+
setStatus("error");
|
|
1745
|
+
return;
|
|
2130
1746
|
}
|
|
2131
|
-
} catch (err) {
|
|
2132
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2133
|
-
setStatus("error");
|
|
2134
1747
|
}
|
|
2135
|
-
}, [backend,
|
|
1748
|
+
}, [backend, copilot, claude]);
|
|
2136
1749
|
return {
|
|
2137
1750
|
status,
|
|
2138
1751
|
error,
|
|
2139
1752
|
startDeviceFlow,
|
|
2140
|
-
deviceCode,
|
|
2141
|
-
verificationUrl,
|
|
1753
|
+
deviceCode: copilot.deviceCode,
|
|
1754
|
+
verificationUrl: copilot.verificationUrl,
|
|
2142
1755
|
startOAuthFlow,
|
|
2143
|
-
authorizeUrl,
|
|
1756
|
+
authorizeUrl: claude.authorizeUrl,
|
|
2144
1757
|
completeOAuth,
|
|
2145
1758
|
submitApiKey,
|
|
2146
1759
|
start,
|
|
@@ -2153,18 +1766,45 @@ function useRemoteAuth(options) {
|
|
|
2153
1766
|
};
|
|
2154
1767
|
}
|
|
2155
1768
|
|
|
2156
|
-
// src/chat/
|
|
2157
|
-
var
|
|
1769
|
+
// src/chat/listener-set.ts
|
|
1770
|
+
var ListenerSet = class {
|
|
1771
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
1772
|
+
/** Add a listener. Returns an unsubscribe function. */
|
|
1773
|
+
add(callback) {
|
|
1774
|
+
this._listeners.add(callback);
|
|
1775
|
+
return () => {
|
|
1776
|
+
this._listeners.delete(callback);
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
/** Notify all listeners with the given arguments. Errors are isolated per listener. */
|
|
1780
|
+
notify(...args) {
|
|
1781
|
+
for (const cb of this._listeners) {
|
|
1782
|
+
try {
|
|
1783
|
+
cb(...args);
|
|
1784
|
+
} catch {
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
/** Remove all listeners. */
|
|
1789
|
+
clear() {
|
|
1790
|
+
this._listeners.clear();
|
|
1791
|
+
}
|
|
1792
|
+
/** Current number of listeners. */
|
|
1793
|
+
get size() {
|
|
1794
|
+
return this._listeners.size;
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
// src/chat/react/RemoteChatClient.ts
|
|
1799
|
+
var RemoteChatClient = class {
|
|
2158
1800
|
_status = "idle";
|
|
2159
1801
|
_activeSessionId = null;
|
|
2160
|
-
|
|
2161
|
-
_currentModel;
|
|
1802
|
+
_selectedProviderId = null;
|
|
2162
1803
|
_abortController = null;
|
|
2163
|
-
_tools = /* @__PURE__ */ new Map();
|
|
2164
|
-
_middlewares = [];
|
|
2165
1804
|
baseUrl;
|
|
2166
1805
|
headers;
|
|
2167
1806
|
_fetch;
|
|
1807
|
+
_selectionListeners = new ListenerSet();
|
|
2168
1808
|
constructor(options) {
|
|
2169
1809
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
2170
1810
|
this.headers = options.headers ?? {};
|
|
@@ -2183,6 +1823,21 @@ var RemoteChatRuntime = class {
|
|
|
2183
1823
|
throw new Error("Runtime is disposed");
|
|
2184
1824
|
}
|
|
2185
1825
|
}
|
|
1826
|
+
// ─── Provider Selection ────────────────────────────────────
|
|
1827
|
+
get selectedProviderId() {
|
|
1828
|
+
return this._selectedProviderId;
|
|
1829
|
+
}
|
|
1830
|
+
selectProvider(providerId) {
|
|
1831
|
+
this.assertNotDisposed();
|
|
1832
|
+
this._selectedProviderId = providerId;
|
|
1833
|
+
this._notifySelectionChange(providerId);
|
|
1834
|
+
}
|
|
1835
|
+
onSelectionChange(callback) {
|
|
1836
|
+
return this._selectionListeners.add(callback);
|
|
1837
|
+
}
|
|
1838
|
+
_notifySelectionChange(providerId) {
|
|
1839
|
+
this._selectionListeners.notify(providerId);
|
|
1840
|
+
}
|
|
2186
1841
|
// ─── Sessions ───────────────────────────────────────────────
|
|
2187
1842
|
get activeSessionId() {
|
|
2188
1843
|
return this._activeSessionId;
|
|
@@ -2214,16 +1869,22 @@ var RemoteChatRuntime = class {
|
|
|
2214
1869
|
}
|
|
2215
1870
|
this._notifySessionChange();
|
|
2216
1871
|
}
|
|
2217
|
-
|
|
1872
|
+
/**
|
|
1873
|
+
* Fetch context window stats from server.
|
|
1874
|
+
* Returns null if stats not available (e.g. no messages sent yet).
|
|
1875
|
+
*/
|
|
1876
|
+
async getContextStats(sessionId) {
|
|
2218
1877
|
this.assertNotDisposed();
|
|
2219
|
-
await this.
|
|
2220
|
-
|
|
1878
|
+
const res = await this._get(`/sessions/${sessionId}/context-stats`);
|
|
1879
|
+
const data = await res.json();
|
|
1880
|
+
return data;
|
|
2221
1881
|
}
|
|
2222
1882
|
async switchSession(id) {
|
|
2223
1883
|
this.assertNotDisposed();
|
|
2224
1884
|
const session = await this.getSession(id);
|
|
2225
1885
|
if (!session) throw new Error(`Session not found: ${id}`);
|
|
2226
1886
|
this._activeSessionId = session.id;
|
|
1887
|
+
this._notifySessionChange();
|
|
2227
1888
|
return session;
|
|
2228
1889
|
}
|
|
2229
1890
|
// ─── Messaging ──────────────────────────────────────────────
|
|
@@ -2242,7 +1903,8 @@ var RemoteChatRuntime = class {
|
|
|
2242
1903
|
body: JSON.stringify({
|
|
2243
1904
|
sessionId,
|
|
2244
1905
|
message,
|
|
2245
|
-
model: options?.model
|
|
1906
|
+
model: options?.model,
|
|
1907
|
+
providerId: this._selectedProviderId ?? void 0
|
|
2246
1908
|
}),
|
|
2247
1909
|
signal: this._abortController.signal
|
|
2248
1910
|
});
|
|
@@ -2277,64 +1939,43 @@ var RemoteChatRuntime = class {
|
|
|
2277
1939
|
this._post("/abort", {}).catch(() => {
|
|
2278
1940
|
});
|
|
2279
1941
|
}
|
|
2280
|
-
// ───
|
|
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
|
-
}
|
|
1942
|
+
// ─── Discovery ─────────────────────────────────────────────
|
|
2297
1943
|
async listModels() {
|
|
2298
1944
|
this.assertNotDisposed();
|
|
2299
1945
|
const res = await this._get("/models");
|
|
2300
1946
|
return await res.json();
|
|
2301
1947
|
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
registerTool(tool) {
|
|
2307
|
-
this._tools.set(tool.name, tool);
|
|
1948
|
+
async listBackends() {
|
|
1949
|
+
this.assertNotDisposed();
|
|
1950
|
+
const res = await this._get("/backends");
|
|
1951
|
+
return await res.json();
|
|
2308
1952
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
1953
|
+
// ─── Providers ──────────────────────────────────────────────
|
|
1954
|
+
async listProviders() {
|
|
1955
|
+
this.assertNotDisposed();
|
|
1956
|
+
const res = await this._get("/providers");
|
|
1957
|
+
return await res.json();
|
|
2311
1958
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
this.
|
|
1959
|
+
async createProvider(config) {
|
|
1960
|
+
this.assertNotDisposed();
|
|
1961
|
+
const res = await this._post("/providers", config);
|
|
1962
|
+
return await res.json();
|
|
2315
1963
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
1964
|
+
async updateProvider(id, changes) {
|
|
1965
|
+
this.assertNotDisposed();
|
|
1966
|
+
await this._put(`/providers/${id}`, changes);
|
|
2319
1967
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
1968
|
+
async deleteProvider(id) {
|
|
1969
|
+
this.assertNotDisposed();
|
|
1970
|
+
await this._delete(`/providers/${id}`);
|
|
2323
1971
|
}
|
|
2324
|
-
|
|
1972
|
+
// ─── Session Change Notifications ────────────────────────────
|
|
1973
|
+
_sessionListeners = new ListenerSet();
|
|
2325
1974
|
onSessionChange(callback) {
|
|
2326
|
-
this._sessionListeners.add(callback);
|
|
2327
|
-
return () => {
|
|
2328
|
-
this._sessionListeners.delete(callback);
|
|
2329
|
-
};
|
|
1975
|
+
return this._sessionListeners.add(callback);
|
|
2330
1976
|
}
|
|
2331
1977
|
_notifySessionChange() {
|
|
2332
|
-
|
|
2333
|
-
try {
|
|
2334
|
-
cb();
|
|
2335
|
-
} catch {
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
1978
|
+
this._sessionListeners.notify();
|
|
2338
1979
|
}
|
|
2339
1980
|
// ─── Internal HTTP helpers ──────────────────────────────────
|
|
2340
1981
|
async _get(path) {
|
|
@@ -2371,6 +2012,20 @@ var RemoteChatRuntime = class {
|
|
|
2371
2012
|
}
|
|
2372
2013
|
return res;
|
|
2373
2014
|
}
|
|
2015
|
+
async _put(path, body) {
|
|
2016
|
+
const res = await this._fetch(`${this.baseUrl}${path}`, {
|
|
2017
|
+
method: "PUT",
|
|
2018
|
+
headers: {
|
|
2019
|
+
"Content-Type": "application/json",
|
|
2020
|
+
...this.headers
|
|
2021
|
+
},
|
|
2022
|
+
body: JSON.stringify(body)
|
|
2023
|
+
});
|
|
2024
|
+
if (!res.ok) {
|
|
2025
|
+
throw new Error(`PUT ${path} failed: ${res.status} ${res.statusText}`);
|
|
2026
|
+
}
|
|
2027
|
+
return res;
|
|
2028
|
+
}
|
|
2374
2029
|
// ─── SSE Parser ─────────────────────────────────────────────
|
|
2375
2030
|
async *_parseSSEStream(body, signal) {
|
|
2376
2031
|
const reader = body.getReader();
|
|
@@ -2425,8 +2080,7 @@ function useRemoteChat(options) {
|
|
|
2425
2080
|
authBaseUrl,
|
|
2426
2081
|
backend,
|
|
2427
2082
|
onReady,
|
|
2428
|
-
headers
|
|
2429
|
-
autoRestore = true
|
|
2083
|
+
headers
|
|
2430
2084
|
} = options;
|
|
2431
2085
|
const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2432
2086
|
const [phase, setPhase] = useState("initializing");
|
|
@@ -2450,7 +2104,7 @@ function useRemoteChat(options) {
|
|
|
2450
2104
|
const restoredRef = useRef(false);
|
|
2451
2105
|
const [tokensLoaded, setTokensLoaded] = useState(false);
|
|
2452
2106
|
useEffect(() => {
|
|
2453
|
-
if (
|
|
2107
|
+
if (restoredRef.current) return;
|
|
2454
2108
|
restoredRef.current = true;
|
|
2455
2109
|
(async () => {
|
|
2456
2110
|
try {
|
|
@@ -2459,7 +2113,7 @@ function useRemoteChat(options) {
|
|
|
2459
2113
|
}
|
|
2460
2114
|
if (mountedRef.current) setTokensLoaded(true);
|
|
2461
2115
|
})();
|
|
2462
|
-
}, [
|
|
2116
|
+
}, []);
|
|
2463
2117
|
useEffect(() => {
|
|
2464
2118
|
if (!tokensLoaded) return;
|
|
2465
2119
|
if (auth.status === "idle" && auth.savedProviders.includes(backend) && phase === "initializing") {
|
|
@@ -2486,7 +2140,7 @@ function useRemoteChat(options) {
|
|
|
2486
2140
|
setError(null);
|
|
2487
2141
|
(async () => {
|
|
2488
2142
|
try {
|
|
2489
|
-
const rt = new
|
|
2143
|
+
const rt = new RemoteChatClient({
|
|
2490
2144
|
baseUrl: chatBaseUrl,
|
|
2491
2145
|
headers,
|
|
2492
2146
|
fetch: fetchFn
|
|
@@ -2541,174 +2195,1353 @@ function useRemoteChat(options) {
|
|
|
2541
2195
|
logout
|
|
2542
2196
|
};
|
|
2543
2197
|
}
|
|
2544
|
-
function
|
|
2545
|
-
backends,
|
|
2546
|
-
selectedBackend: controlledBackend,
|
|
2547
|
-
onBackendChange,
|
|
2548
|
-
onAuthenticated,
|
|
2549
|
-
renderCopilotFlow,
|
|
2550
|
-
renderClaudeFlow,
|
|
2551
|
-
renderApiKeyFlow,
|
|
2552
|
-
className
|
|
2553
|
-
}) {
|
|
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]
|
|
2564
|
-
);
|
|
2565
|
-
const auth = useAuth({ backend: activeBackend, onAuthenticated });
|
|
2198
|
+
function CopilotAuthForm({ auth, onAuthComplete }) {
|
|
2566
2199
|
const children = [];
|
|
2567
|
-
|
|
2568
|
-
(
|
|
2569
|
-
"button",
|
|
2570
|
-
|
|
2571
|
-
key: backend,
|
|
2200
|
+
if (auth.status === "idle") {
|
|
2201
|
+
children.push(
|
|
2202
|
+
createElement("button", {
|
|
2203
|
+
key: "start",
|
|
2572
2204
|
type: "button",
|
|
2573
|
-
"data-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
)
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
"div",
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2205
|
+
"data-action": "start-auth",
|
|
2206
|
+
onClick: () => {
|
|
2207
|
+
auth.startDeviceFlow().then(() => onAuthComplete());
|
|
2208
|
+
}
|
|
2209
|
+
}, "Authenticate with GitHub")
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
if (auth.deviceCode) {
|
|
2213
|
+
children.push(
|
|
2214
|
+
createElement("div", { key: "code", "data-device-code": "true" }, auth.deviceCode)
|
|
2215
|
+
);
|
|
2216
|
+
if (auth.verificationUrl) {
|
|
2217
|
+
children.push(
|
|
2218
|
+
createElement("a", {
|
|
2219
|
+
key: "url",
|
|
2220
|
+
href: auth.verificationUrl,
|
|
2221
|
+
target: "_blank",
|
|
2222
|
+
rel: "noreferrer"
|
|
2223
|
+
}, "Open GitHub \u2192")
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
if (auth.status === "pending") {
|
|
2228
|
+
children.push(
|
|
2229
|
+
createElement("span", { key: "wait", "data-auth-loading": "true" }, "Waiting...")
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
if (auth.status === "authenticated") {
|
|
2233
|
+
children.push(
|
|
2234
|
+
createElement(
|
|
2235
|
+
"div",
|
|
2236
|
+
{ key: "done", "data-auth-success": "true" },
|
|
2237
|
+
"\u2713 Authenticated",
|
|
2238
|
+
createElement("button", {
|
|
2239
|
+
type: "button",
|
|
2240
|
+
"data-action": "continue",
|
|
2241
|
+
onClick: onAuthComplete
|
|
2242
|
+
}, "Continue \u2192")
|
|
2243
|
+
)
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
if (auth.error) {
|
|
2247
|
+
children.push(
|
|
2248
|
+
createElement("div", { key: "error", "data-auth-error-display": "true" }, auth.error.message)
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
return createElement("div", { "data-auth-flow": "copilot" }, ...children);
|
|
2252
|
+
}
|
|
2253
|
+
function ClaudeAuthForm({ auth, onAuthComplete }) {
|
|
2254
|
+
const codeRef = useRef(null);
|
|
2255
|
+
const children = [];
|
|
2256
|
+
if (auth.status === "idle") {
|
|
2257
|
+
children.push(
|
|
2258
|
+
createElement("button", {
|
|
2259
|
+
key: "start",
|
|
2260
|
+
type: "button",
|
|
2261
|
+
"data-action": "start-auth",
|
|
2262
|
+
onClick: () => auth.startOAuthFlow()
|
|
2263
|
+
}, "Authenticate with Claude")
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
if (auth.authorizeUrl) {
|
|
2267
|
+
children.push(
|
|
2268
|
+
createElement("a", {
|
|
2269
|
+
key: "url",
|
|
2270
|
+
href: auth.authorizeUrl,
|
|
2271
|
+
target: "_blank",
|
|
2272
|
+
rel: "noreferrer"
|
|
2273
|
+
}, "Open authorization page \u2192")
|
|
2274
|
+
);
|
|
2275
|
+
children.push(
|
|
2276
|
+
createElement(
|
|
2277
|
+
"div",
|
|
2278
|
+
{ key: "complete", "data-auth-complete": "true" },
|
|
2279
|
+
createElement("input", {
|
|
2280
|
+
ref: codeRef,
|
|
2281
|
+
placeholder: "Paste code or redirect URL..."
|
|
2282
|
+
}),
|
|
2283
|
+
createElement("button", {
|
|
2284
|
+
type: "button",
|
|
2285
|
+
"data-action": "complete-auth",
|
|
2286
|
+
onClick: () => {
|
|
2287
|
+
const v = codeRef.current?.value?.trim();
|
|
2288
|
+
if (v) auth.completeOAuth(v).then(() => onAuthComplete());
|
|
2289
|
+
}
|
|
2290
|
+
}, "Submit")
|
|
2291
|
+
)
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
if (auth.status === "authenticated") {
|
|
2295
|
+
children.push(
|
|
2296
|
+
createElement(
|
|
2297
|
+
"div",
|
|
2298
|
+
{ key: "done", "data-auth-success": "true" },
|
|
2299
|
+
"\u2713 Authenticated",
|
|
2300
|
+
createElement("button", {
|
|
2301
|
+
type: "button",
|
|
2302
|
+
"data-action": "continue",
|
|
2303
|
+
onClick: onAuthComplete
|
|
2304
|
+
}, "Continue \u2192")
|
|
2305
|
+
)
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
if (auth.error) {
|
|
2309
|
+
children.push(
|
|
2310
|
+
createElement("div", { key: "error", "data-auth-error-display": "true" }, auth.error.message)
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
return createElement("div", { "data-auth-flow": "claude" }, ...children);
|
|
2314
|
+
}
|
|
2315
|
+
function VercelAIAuthForm({ auth, onAuthComplete }) {
|
|
2316
|
+
const apiKeyRef = useRef(null);
|
|
2317
|
+
const baseUrlRef = useRef(null);
|
|
2318
|
+
const children = [];
|
|
2319
|
+
if (auth.status !== "authenticated") {
|
|
2320
|
+
children.push(
|
|
2321
|
+
createElement(
|
|
2322
|
+
"div",
|
|
2323
|
+
{ key: "apikey", "data-auth-apikey": "true" },
|
|
2324
|
+
createElement("input", {
|
|
2325
|
+
ref: baseUrlRef,
|
|
2326
|
+
placeholder: "Base URL (default: openai.com/v1)"
|
|
2327
|
+
}),
|
|
2328
|
+
createElement("input", {
|
|
2329
|
+
ref: apiKeyRef,
|
|
2330
|
+
type: "password",
|
|
2331
|
+
placeholder: "API Key (sk-...)"
|
|
2332
|
+
}),
|
|
2333
|
+
createElement("button", {
|
|
2334
|
+
type: "button",
|
|
2335
|
+
"data-action": "submit-apikey",
|
|
2336
|
+
onClick: () => {
|
|
2337
|
+
const k = apiKeyRef.current?.value?.trim();
|
|
2338
|
+
if (k) {
|
|
2339
|
+
auth.submitApiKey(k, baseUrlRef.current?.value?.trim() || void 0).then(() => onAuthComplete());
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}, "Connect")
|
|
2343
|
+
)
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
if (auth.status === "authenticated") {
|
|
2347
|
+
children.push(
|
|
2348
|
+
createElement(
|
|
2349
|
+
"div",
|
|
2350
|
+
{ key: "done", "data-auth-success": "true" },
|
|
2351
|
+
"\u2713 Connected",
|
|
2352
|
+
createElement("button", {
|
|
2353
|
+
type: "button",
|
|
2354
|
+
"data-action": "continue",
|
|
2355
|
+
onClick: onAuthComplete
|
|
2356
|
+
}, "Continue \u2192")
|
|
2357
|
+
)
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
if (auth.error) {
|
|
2361
|
+
children.push(
|
|
2362
|
+
createElement("div", { key: "error", "data-auth-error-display": "true" }, auth.error.message)
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
return createElement("div", { "data-auth-flow": "vercel-ai" }, ...children);
|
|
2366
|
+
}
|
|
2367
|
+
function BackendSelector({
|
|
2368
|
+
backends,
|
|
2369
|
+
onSelect,
|
|
2370
|
+
className
|
|
2371
|
+
}) {
|
|
2372
|
+
const handleClick = useCallback(
|
|
2373
|
+
(name) => () => onSelect(name),
|
|
2374
|
+
[onSelect]
|
|
2375
|
+
);
|
|
2376
|
+
const items = backends.map(
|
|
2377
|
+
(backend) => createElement(
|
|
2378
|
+
"button",
|
|
2379
|
+
{
|
|
2380
|
+
key: backend.name,
|
|
2381
|
+
type: "button",
|
|
2382
|
+
"data-backend-item": "true",
|
|
2383
|
+
"data-backend-name": backend.name,
|
|
2384
|
+
onClick: handleClick(backend.name)
|
|
2385
|
+
},
|
|
2386
|
+
backend.name
|
|
2387
|
+
)
|
|
2388
|
+
);
|
|
2389
|
+
return createElement(
|
|
2390
|
+
"div",
|
|
2391
|
+
{ "data-backend-selector": "true", className },
|
|
2392
|
+
...items
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
function useBackends() {
|
|
2396
|
+
const runtime = useChatRuntime();
|
|
2397
|
+
const [backends, setBackends] = useState([]);
|
|
2398
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
2399
|
+
const [error, setError] = useState(null);
|
|
2400
|
+
const mountedRef = useRef(true);
|
|
2401
|
+
const fetchBackends = useCallback(() => {
|
|
2402
|
+
setIsLoading(true);
|
|
2403
|
+
setError(null);
|
|
2404
|
+
try {
|
|
2405
|
+
const result = runtime.listBackends();
|
|
2406
|
+
if (result instanceof Promise) {
|
|
2407
|
+
result.then((data) => {
|
|
2408
|
+
if (mountedRef.current) {
|
|
2409
|
+
setBackends(data);
|
|
2410
|
+
setIsLoading(false);
|
|
2411
|
+
}
|
|
2412
|
+
}).catch((err) => {
|
|
2413
|
+
if (mountedRef.current) {
|
|
2414
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2415
|
+
setIsLoading(false);
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2418
|
+
} else {
|
|
2419
|
+
if (mountedRef.current) {
|
|
2420
|
+
setBackends(result);
|
|
2421
|
+
setIsLoading(false);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
} catch (err) {
|
|
2425
|
+
if (mountedRef.current) {
|
|
2426
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2427
|
+
setIsLoading(false);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}, [runtime]);
|
|
2431
|
+
useEffect(() => {
|
|
2432
|
+
mountedRef.current = true;
|
|
2433
|
+
fetchBackends();
|
|
2434
|
+
return () => {
|
|
2435
|
+
mountedRef.current = false;
|
|
2436
|
+
};
|
|
2437
|
+
}, [fetchBackends]);
|
|
2438
|
+
return { backends, isLoading, error, refresh: fetchBackends };
|
|
2439
|
+
}
|
|
2440
|
+
function isProviderCapable(runtime) {
|
|
2441
|
+
return typeof runtime === "object" && runtime !== null && typeof runtime.listProviders === "function";
|
|
2442
|
+
}
|
|
2443
|
+
function useProviders() {
|
|
2444
|
+
const runtime = useChatRuntime();
|
|
2445
|
+
const [providers, setProviders] = useState([]);
|
|
2446
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
2447
|
+
const [error, setError] = useState(null);
|
|
2448
|
+
const mountedRef = useRef(true);
|
|
2449
|
+
const fetchProviders = useCallback(() => {
|
|
2450
|
+
setIsLoading(true);
|
|
2451
|
+
setError(null);
|
|
2452
|
+
if (!isProviderCapable(runtime)) {
|
|
2453
|
+
setIsLoading(false);
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
runtime.listProviders().then((data) => {
|
|
2457
|
+
if (mountedRef.current) {
|
|
2458
|
+
setProviders(data);
|
|
2459
|
+
setIsLoading(false);
|
|
2460
|
+
}
|
|
2461
|
+
}).catch((err) => {
|
|
2462
|
+
if (mountedRef.current) {
|
|
2463
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2464
|
+
setIsLoading(false);
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
}, [runtime]);
|
|
2468
|
+
useEffect(() => {
|
|
2469
|
+
mountedRef.current = true;
|
|
2470
|
+
fetchProviders();
|
|
2471
|
+
return () => {
|
|
2472
|
+
mountedRef.current = false;
|
|
2473
|
+
};
|
|
2474
|
+
}, [fetchProviders]);
|
|
2475
|
+
const createProvider = useCallback(
|
|
2476
|
+
async (config) => {
|
|
2477
|
+
if (!isProviderCapable(runtime)) return;
|
|
2478
|
+
try {
|
|
2479
|
+
await runtime.createProvider(config);
|
|
2480
|
+
fetchProviders();
|
|
2481
|
+
} catch (err) {
|
|
2482
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2483
|
+
}
|
|
2484
|
+
},
|
|
2485
|
+
[runtime, fetchProviders]
|
|
2486
|
+
);
|
|
2487
|
+
const updateProvider = useCallback(
|
|
2488
|
+
async (id, changes) => {
|
|
2489
|
+
if (!isProviderCapable(runtime)) return;
|
|
2490
|
+
try {
|
|
2491
|
+
await runtime.updateProvider(id, changes);
|
|
2492
|
+
fetchProviders();
|
|
2493
|
+
} catch (err) {
|
|
2494
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2495
|
+
}
|
|
2496
|
+
},
|
|
2497
|
+
[runtime, fetchProviders]
|
|
2498
|
+
);
|
|
2499
|
+
const deleteProvider = useCallback(
|
|
2500
|
+
async (id) => {
|
|
2501
|
+
if (!isProviderCapable(runtime)) return;
|
|
2502
|
+
try {
|
|
2503
|
+
await runtime.deleteProvider(id);
|
|
2504
|
+
fetchProviders();
|
|
2505
|
+
} catch (err) {
|
|
2506
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2507
|
+
}
|
|
2508
|
+
},
|
|
2509
|
+
[runtime, fetchProviders]
|
|
2510
|
+
);
|
|
2511
|
+
const selectProvider = useCallback(
|
|
2512
|
+
(id) => {
|
|
2513
|
+
if (!isProviderCapable(runtime)) return;
|
|
2514
|
+
runtime.selectProvider(id);
|
|
2515
|
+
},
|
|
2516
|
+
[runtime]
|
|
2517
|
+
);
|
|
2518
|
+
return { providers, isLoading, error, refresh: fetchProviders, createProvider, updateProvider, deleteProvider, selectProvider };
|
|
2519
|
+
}
|
|
2520
|
+
function ProviderSelector({
|
|
2521
|
+
providers,
|
|
2522
|
+
activeProviderId,
|
|
2523
|
+
onSelect,
|
|
2524
|
+
onSettingsClick,
|
|
2525
|
+
className
|
|
2526
|
+
}) {
|
|
2527
|
+
const [open, setOpen] = useState(false);
|
|
2528
|
+
const [highlightIndex, setHighlightIndex] = useState(0);
|
|
2529
|
+
const containerRef = useRef(null);
|
|
2530
|
+
const activeProvider = useMemo(
|
|
2531
|
+
() => providers.find((p) => p.id === activeProviderId),
|
|
2532
|
+
[providers, activeProviderId]
|
|
2533
|
+
);
|
|
2534
|
+
useEffect(() => {
|
|
2535
|
+
if (!open) return;
|
|
2536
|
+
const handler = (e) => {
|
|
2537
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
2538
|
+
setOpen(false);
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
2541
|
+
document.addEventListener("mousedown", handler);
|
|
2542
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
2543
|
+
}, [open]);
|
|
2544
|
+
useEffect(() => {
|
|
2545
|
+
setHighlightIndex(0);
|
|
2546
|
+
}, [providers.length]);
|
|
2547
|
+
const handleToggle = useCallback(() => {
|
|
2548
|
+
setOpen((prev) => {
|
|
2549
|
+
if (!prev) setHighlightIndex(0);
|
|
2550
|
+
return !prev;
|
|
2551
|
+
});
|
|
2552
|
+
}, []);
|
|
2553
|
+
const handleSelect = useCallback(
|
|
2554
|
+
(id) => {
|
|
2555
|
+
onSelect(id);
|
|
2556
|
+
setOpen(false);
|
|
2557
|
+
},
|
|
2558
|
+
[onSelect]
|
|
2559
|
+
);
|
|
2560
|
+
const handleKeyDown = useCallback(
|
|
2561
|
+
(e) => {
|
|
2562
|
+
if (e.key === "ArrowDown") {
|
|
2563
|
+
e.preventDefault();
|
|
2564
|
+
setHighlightIndex((prev) => Math.min(prev + 1, providers.length - 1));
|
|
2565
|
+
} else if (e.key === "ArrowUp") {
|
|
2566
|
+
e.preventDefault();
|
|
2567
|
+
setHighlightIndex((prev) => Math.max(prev - 1, 0));
|
|
2568
|
+
} else if (e.key === "Enter") {
|
|
2569
|
+
e.preventDefault();
|
|
2570
|
+
if (providers[highlightIndex]) {
|
|
2571
|
+
handleSelect(providers[highlightIndex].id);
|
|
2572
|
+
}
|
|
2573
|
+
} else if (e.key === "Escape") {
|
|
2574
|
+
e.preventDefault();
|
|
2575
|
+
setOpen(false);
|
|
2576
|
+
}
|
|
2577
|
+
},
|
|
2578
|
+
[providers, highlightIndex, handleSelect]
|
|
2579
|
+
);
|
|
2580
|
+
const children = [];
|
|
2581
|
+
children.push(
|
|
2582
|
+
createElement(
|
|
2583
|
+
"button",
|
|
2584
|
+
{
|
|
2585
|
+
key: "trigger",
|
|
2586
|
+
type: "button",
|
|
2587
|
+
"data-provider-trigger": "true",
|
|
2588
|
+
onClick: handleToggle,
|
|
2589
|
+
onKeyDown: handleKeyDown
|
|
2590
|
+
},
|
|
2591
|
+
activeProvider ? activeProvider.label : "Select provider"
|
|
2592
|
+
)
|
|
2593
|
+
);
|
|
2594
|
+
if (open) {
|
|
2595
|
+
const dropdownChildren = [];
|
|
2596
|
+
providers.forEach((provider, idx) => {
|
|
2597
|
+
const isActive = provider.id === activeProviderId;
|
|
2598
|
+
const isHighlighted = idx === highlightIndex;
|
|
2599
|
+
const attrs = {
|
|
2600
|
+
key: provider.id,
|
|
2601
|
+
"data-provider-item": "true",
|
|
2602
|
+
onClick: () => handleSelect(provider.id)
|
|
2603
|
+
};
|
|
2604
|
+
if (isActive) attrs["data-provider-active"] = "true";
|
|
2605
|
+
if (isHighlighted) attrs["data-provider-highlighted"] = "true";
|
|
2606
|
+
dropdownChildren.push(
|
|
2607
|
+
createElement(
|
|
2608
|
+
"div",
|
|
2609
|
+
attrs,
|
|
2610
|
+
createElement("span", { "data-provider-label": "true" }, provider.label),
|
|
2611
|
+
createElement("span", { "data-provider-model": "true" }, provider.model)
|
|
2612
|
+
)
|
|
2613
|
+
);
|
|
2614
|
+
});
|
|
2615
|
+
if (onSettingsClick) {
|
|
2616
|
+
dropdownChildren.push(
|
|
2617
|
+
createElement(
|
|
2618
|
+
"button",
|
|
2619
|
+
{
|
|
2620
|
+
key: "settings",
|
|
2621
|
+
type: "button",
|
|
2622
|
+
"data-provider-settings-btn": "true",
|
|
2623
|
+
onClick: (e) => {
|
|
2624
|
+
e.stopPropagation();
|
|
2625
|
+
setOpen(false);
|
|
2626
|
+
onSettingsClick();
|
|
2627
|
+
}
|
|
2628
|
+
},
|
|
2629
|
+
"\u2699 Settings"
|
|
2630
|
+
)
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
children.push(
|
|
2634
|
+
createElement(
|
|
2635
|
+
"div",
|
|
2636
|
+
{ key: "dropdown", "data-provider-dropdown": "true", onKeyDown: handleKeyDown },
|
|
2637
|
+
...dropdownChildren
|
|
2638
|
+
)
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
return createElement(
|
|
2642
|
+
"div",
|
|
2643
|
+
{
|
|
2644
|
+
"data-provider-selector": "true",
|
|
2645
|
+
className,
|
|
2646
|
+
ref: containerRef
|
|
2647
|
+
},
|
|
2648
|
+
...children
|
|
2649
|
+
);
|
|
2650
|
+
}
|
|
2651
|
+
function ProviderModelSelector({
|
|
2652
|
+
providers = [],
|
|
2653
|
+
models = [],
|
|
2654
|
+
activeProviderId,
|
|
2655
|
+
selectedModel,
|
|
2656
|
+
onSelectProvider,
|
|
2657
|
+
onSelectModel,
|
|
2658
|
+
onSettingsClick,
|
|
2659
|
+
placeholder = "Select model",
|
|
2660
|
+
className
|
|
2661
|
+
}) {
|
|
2662
|
+
const [open, setOpen] = useState(false);
|
|
2663
|
+
const [search, setSearch] = useState("");
|
|
2664
|
+
const [highlightIndex, setHighlightIndex] = useState(0);
|
|
2665
|
+
const containerRef = useRef(null);
|
|
2666
|
+
const isProviderMode = providers.length > 0;
|
|
2667
|
+
const items = useMemo(() => {
|
|
2668
|
+
if (isProviderMode) {
|
|
2669
|
+
return providers.map((p) => ({
|
|
2670
|
+
id: p.id,
|
|
2671
|
+
label: p.label,
|
|
2672
|
+
sublabel: p.model,
|
|
2673
|
+
type: "provider"
|
|
2674
|
+
}));
|
|
2675
|
+
}
|
|
2676
|
+
return models.map((m) => ({
|
|
2677
|
+
id: m.id,
|
|
2678
|
+
label: m.name,
|
|
2679
|
+
sublabel: m.provider,
|
|
2680
|
+
tier: m.tier,
|
|
2681
|
+
type: "model"
|
|
2682
|
+
}));
|
|
2683
|
+
}, [isProviderMode, providers, models]);
|
|
2684
|
+
const filtered = useMemo(() => {
|
|
2685
|
+
if (!search) return items;
|
|
2686
|
+
const q = search.toLowerCase();
|
|
2687
|
+
return items.filter(
|
|
2688
|
+
(item) => item.label.toLowerCase().includes(q) || item.sublabel && item.sublabel.toLowerCase().includes(q)
|
|
2689
|
+
);
|
|
2690
|
+
}, [items, search]);
|
|
2691
|
+
useEffect(() => {
|
|
2692
|
+
setHighlightIndex(0);
|
|
2693
|
+
}, [filtered.length]);
|
|
2694
|
+
useEffect(() => {
|
|
2695
|
+
if (!open) return;
|
|
2696
|
+
const handler = (e) => {
|
|
2697
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
2698
|
+
setOpen(false);
|
|
2699
|
+
}
|
|
2700
|
+
};
|
|
2701
|
+
document.addEventListener("mousedown", handler);
|
|
2702
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
2703
|
+
}, [open]);
|
|
2704
|
+
const triggerLabel = useMemo(() => {
|
|
2705
|
+
if (isProviderMode && activeProviderId) {
|
|
2706
|
+
const p = providers.find((prov) => prov.id === activeProviderId);
|
|
2707
|
+
return p ? p.label : placeholder;
|
|
2708
|
+
}
|
|
2709
|
+
if (!isProviderMode && selectedModel) {
|
|
2710
|
+
const m = models.find((mod) => mod.id === selectedModel);
|
|
2711
|
+
return m ? m.name : selectedModel;
|
|
2712
|
+
}
|
|
2713
|
+
return placeholder;
|
|
2714
|
+
}, [isProviderMode, activeProviderId, providers, selectedModel, models, placeholder]);
|
|
2715
|
+
const handleToggle = useCallback(() => {
|
|
2716
|
+
setOpen((prev) => {
|
|
2717
|
+
if (!prev) {
|
|
2718
|
+
setSearch("");
|
|
2719
|
+
setHighlightIndex(0);
|
|
2618
2720
|
}
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
);
|
|
2721
|
+
return !prev;
|
|
2722
|
+
});
|
|
2723
|
+
}, []);
|
|
2724
|
+
const handleSelect = useCallback(
|
|
2725
|
+
(item) => {
|
|
2726
|
+
if (item.type === "provider" && onSelectProvider) {
|
|
2727
|
+
onSelectProvider(item.id);
|
|
2728
|
+
} else if (item.type === "model" && onSelectModel) {
|
|
2729
|
+
onSelectModel(item.id);
|
|
2626
2730
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2731
|
+
setOpen(false);
|
|
2732
|
+
setSearch("");
|
|
2733
|
+
},
|
|
2734
|
+
[onSelectProvider, onSelectModel]
|
|
2735
|
+
);
|
|
2736
|
+
const handleSearchChange = useCallback((e) => {
|
|
2737
|
+
setSearch(e.target.value);
|
|
2738
|
+
}, []);
|
|
2739
|
+
const handleKeyDown = useCallback(
|
|
2740
|
+
(e) => {
|
|
2741
|
+
if (e.key === "ArrowDown") {
|
|
2742
|
+
e.preventDefault();
|
|
2743
|
+
setHighlightIndex((prev) => Math.min(prev + 1, filtered.length - 1));
|
|
2744
|
+
} else if (e.key === "ArrowUp") {
|
|
2745
|
+
e.preventDefault();
|
|
2746
|
+
setHighlightIndex((prev) => Math.max(prev - 1, 0));
|
|
2747
|
+
} else if (e.key === "Enter") {
|
|
2748
|
+
e.preventDefault();
|
|
2749
|
+
if (filtered[highlightIndex]) {
|
|
2750
|
+
handleSelect(filtered[highlightIndex]);
|
|
2751
|
+
}
|
|
2752
|
+
} else if (e.key === "Escape") {
|
|
2753
|
+
e.preventDefault();
|
|
2754
|
+
setOpen(false);
|
|
2631
2755
|
}
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2756
|
+
},
|
|
2757
|
+
[filtered, highlightIndex, handleSelect]
|
|
2758
|
+
);
|
|
2759
|
+
const children = [];
|
|
2760
|
+
children.push(
|
|
2761
|
+
createElement(
|
|
2762
|
+
"button",
|
|
2763
|
+
{
|
|
2764
|
+
key: "trigger",
|
|
2765
|
+
type: "button",
|
|
2766
|
+
"data-pms-trigger": "true",
|
|
2767
|
+
onClick: handleToggle
|
|
2768
|
+
},
|
|
2769
|
+
triggerLabel
|
|
2770
|
+
)
|
|
2771
|
+
);
|
|
2772
|
+
if (open) {
|
|
2773
|
+
const dropdownChildren = [];
|
|
2774
|
+
if (items.length > 3) {
|
|
2775
|
+
dropdownChildren.push(
|
|
2776
|
+
createElement("input", {
|
|
2777
|
+
key: "search",
|
|
2778
|
+
"data-pms-search": "true",
|
|
2779
|
+
value: search,
|
|
2780
|
+
onChange: handleSearchChange,
|
|
2781
|
+
onKeyDown: handleKeyDown,
|
|
2782
|
+
placeholder: isProviderMode ? "Search providers..." : "Search models...",
|
|
2783
|
+
autoFocus: true
|
|
2784
|
+
})
|
|
2636
2785
|
);
|
|
2637
2786
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
if (
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
},
|
|
2657
|
-
"Start OAuth Flow"
|
|
2658
|
-
)
|
|
2659
|
-
);
|
|
2660
|
-
}
|
|
2661
|
-
if (auth.authorizeUrl) {
|
|
2662
|
-
claudeChildren.push(
|
|
2663
|
-
createElement("a", { key: "url", "data-authorize-url": "true", href: auth.authorizeUrl }, auth.authorizeUrl)
|
|
2787
|
+
filtered.forEach((item, idx) => {
|
|
2788
|
+
const isActive = isProviderMode ? item.id === activeProviderId : item.id === selectedModel;
|
|
2789
|
+
const isHighlighted = idx === highlightIndex;
|
|
2790
|
+
const attrs = {
|
|
2791
|
+
key: item.id,
|
|
2792
|
+
"data-pms-item": "true",
|
|
2793
|
+
"data-pms-type": item.type,
|
|
2794
|
+
onClick: () => handleSelect(item)
|
|
2795
|
+
};
|
|
2796
|
+
if (item.tier) attrs["data-tier"] = item.tier;
|
|
2797
|
+
if (isActive) attrs["data-pms-active"] = "true";
|
|
2798
|
+
if (isHighlighted) attrs["data-pms-highlighted"] = "true";
|
|
2799
|
+
const itemChildren = [
|
|
2800
|
+
createElement("span", { key: "label", "data-pms-label": "true" }, item.label)
|
|
2801
|
+
];
|
|
2802
|
+
if (item.sublabel) {
|
|
2803
|
+
itemChildren.push(
|
|
2804
|
+
createElement("span", { key: "sub", "data-pms-sublabel": "true" }, item.sublabel)
|
|
2664
2805
|
);
|
|
2665
2806
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
);
|
|
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" },
|
|
2807
|
+
dropdownChildren.push(createElement("div", attrs, ...itemChildren));
|
|
2808
|
+
});
|
|
2809
|
+
if (onSettingsClick) {
|
|
2810
|
+
dropdownChildren.push(
|
|
2682
2811
|
createElement(
|
|
2683
2812
|
"button",
|
|
2684
2813
|
{
|
|
2685
|
-
key: "
|
|
2814
|
+
key: "settings",
|
|
2686
2815
|
type: "button",
|
|
2687
|
-
"data-
|
|
2688
|
-
onClick: () =>
|
|
2816
|
+
"data-pms-settings": "true",
|
|
2817
|
+
onClick: (e) => {
|
|
2818
|
+
e.stopPropagation();
|
|
2819
|
+
setOpen(false);
|
|
2820
|
+
onSettingsClick();
|
|
2821
|
+
}
|
|
2689
2822
|
},
|
|
2690
|
-
"
|
|
2823
|
+
"\u2699 Settings"
|
|
2691
2824
|
)
|
|
2692
2825
|
);
|
|
2693
2826
|
}
|
|
2694
|
-
}
|
|
2695
|
-
children.push(createElement("div", contentAttrs, flowContent));
|
|
2696
|
-
if (auth.error) {
|
|
2697
2827
|
children.push(
|
|
2698
2828
|
createElement(
|
|
2699
2829
|
"div",
|
|
2700
|
-
{ key: "
|
|
2701
|
-
|
|
2830
|
+
{ key: "dropdown", "data-pms-dropdown": "true", onKeyDown: handleKeyDown },
|
|
2831
|
+
...dropdownChildren
|
|
2702
2832
|
)
|
|
2703
2833
|
);
|
|
2704
2834
|
}
|
|
2705
2835
|
return createElement(
|
|
2706
2836
|
"div",
|
|
2707
|
-
{
|
|
2837
|
+
{
|
|
2838
|
+
"data-provider-model-selector": "true",
|
|
2839
|
+
"data-pms-mode": isProviderMode ? "provider" : "model",
|
|
2840
|
+
className,
|
|
2841
|
+
ref: containerRef
|
|
2842
|
+
},
|
|
2708
2843
|
...children
|
|
2709
2844
|
);
|
|
2710
2845
|
}
|
|
2846
|
+
var BACKENDS = [
|
|
2847
|
+
{ id: "copilot", label: "GitHub Copilot" },
|
|
2848
|
+
{ id: "claude", label: "Claude" },
|
|
2849
|
+
{ id: "vercel-ai", label: "Vercel AI" }
|
|
2850
|
+
];
|
|
2851
|
+
var AUTH_FORMS = {
|
|
2852
|
+
copilot: CopilotAuthForm,
|
|
2853
|
+
claude: ClaudeAuthForm,
|
|
2854
|
+
"vercel-ai": VercelAIAuthForm
|
|
2855
|
+
};
|
|
2856
|
+
function ProviderSettings({
|
|
2857
|
+
providers,
|
|
2858
|
+
onClose,
|
|
2859
|
+
onProviderCreated,
|
|
2860
|
+
onProviderDeleted,
|
|
2861
|
+
onProviderUpdated,
|
|
2862
|
+
onAuthCompleted,
|
|
2863
|
+
authBaseUrl = "/api/auth",
|
|
2864
|
+
className
|
|
2865
|
+
}) {
|
|
2866
|
+
const [view, setView] = useState("list");
|
|
2867
|
+
const [addStep, setAddStep] = useState("select-backend");
|
|
2868
|
+
const [selectedBackend, setSelectedBackend] = useState("copilot");
|
|
2869
|
+
const [editingId, setEditingId] = useState(null);
|
|
2870
|
+
const [availableModels, setAvailableModels] = useState([]);
|
|
2871
|
+
const modelRef = useRef(null);
|
|
2872
|
+
const labelRef = useRef(null);
|
|
2873
|
+
const editModelRef = useRef(null);
|
|
2874
|
+
const editLabelRef = useRef(null);
|
|
2875
|
+
const runtime = useChatRuntime();
|
|
2876
|
+
const auth = useRemoteAuth({
|
|
2877
|
+
backend: selectedBackend,
|
|
2878
|
+
baseUrl: authBaseUrl
|
|
2879
|
+
});
|
|
2880
|
+
const handleBackendSelect = useCallback((backend) => {
|
|
2881
|
+
setSelectedBackend(backend);
|
|
2882
|
+
auth.reset();
|
|
2883
|
+
setAddStep("auth");
|
|
2884
|
+
}, [auth]);
|
|
2885
|
+
const handleAuthComplete = useCallback(() => {
|
|
2886
|
+
onAuthCompleted?.(selectedBackend);
|
|
2887
|
+
setAddStep("configure");
|
|
2888
|
+
}, [selectedBackend, onAuthCompleted]);
|
|
2889
|
+
useEffect(() => {
|
|
2890
|
+
if (addStep !== "configure" && view !== "edit") return;
|
|
2891
|
+
const load = async () => {
|
|
2892
|
+
try {
|
|
2893
|
+
const models = await runtime.listModels();
|
|
2894
|
+
setAvailableModels(models);
|
|
2895
|
+
} catch {
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
load();
|
|
2899
|
+
}, [addStep, view, runtime]);
|
|
2900
|
+
const handleCreate = useCallback(() => {
|
|
2901
|
+
const modelEl = modelRef.current;
|
|
2902
|
+
const model = modelEl?.value?.trim();
|
|
2903
|
+
const label = labelRef.current?.value?.trim() || model;
|
|
2904
|
+
if (!model) return;
|
|
2905
|
+
const existing = providers.find((p) => p.backend === selectedBackend);
|
|
2906
|
+
if (existing) {
|
|
2907
|
+
onProviderUpdated?.(existing.id, { model, label });
|
|
2908
|
+
} else {
|
|
2909
|
+
onProviderCreated?.({ backend: selectedBackend, model, label });
|
|
2910
|
+
}
|
|
2911
|
+
setView("list");
|
|
2912
|
+
setAddStep("select-backend");
|
|
2913
|
+
setAvailableModels([]);
|
|
2914
|
+
auth.reset();
|
|
2915
|
+
}, [selectedBackend, providers, onProviderCreated, onProviderUpdated, auth]);
|
|
2916
|
+
const handleEdit = useCallback((id) => {
|
|
2917
|
+
setEditingId(id);
|
|
2918
|
+
setView("edit");
|
|
2919
|
+
}, []);
|
|
2920
|
+
const handleUpdate = useCallback(() => {
|
|
2921
|
+
if (!editingId) return;
|
|
2922
|
+
const modelEl = editModelRef.current;
|
|
2923
|
+
const model = modelEl?.value?.trim();
|
|
2924
|
+
const label = editLabelRef.current?.value?.trim();
|
|
2925
|
+
if (!model && !label) return;
|
|
2926
|
+
const changes = {};
|
|
2927
|
+
if (model) changes.model = model;
|
|
2928
|
+
if (label) changes.label = label;
|
|
2929
|
+
onProviderUpdated?.(editingId, changes);
|
|
2930
|
+
setView("list");
|
|
2931
|
+
setEditingId(null);
|
|
2932
|
+
}, [editingId, onProviderUpdated]);
|
|
2933
|
+
const handleDelete = useCallback((id) => {
|
|
2934
|
+
onProviderDeleted?.(id);
|
|
2935
|
+
}, [onProviderDeleted]);
|
|
2936
|
+
const handleStartAdd = useCallback(() => {
|
|
2937
|
+
setView("add");
|
|
2938
|
+
setAddStep("select-backend");
|
|
2939
|
+
auth.reset();
|
|
2940
|
+
}, [auth]);
|
|
2941
|
+
if (view === "list") {
|
|
2942
|
+
const items = providers.map(
|
|
2943
|
+
(p) => createElement(
|
|
2944
|
+
"div",
|
|
2945
|
+
{ key: p.id, "data-provider-settings-item": "true" },
|
|
2946
|
+
createElement("span", { "data-provider-settings-label": "true" }, p.label),
|
|
2947
|
+
createElement("span", { "data-provider-settings-model": "true" }, `${p.backend} / ${p.model}`),
|
|
2948
|
+
createElement(
|
|
2949
|
+
"div",
|
|
2950
|
+
{ "data-provider-settings-actions": "true" },
|
|
2951
|
+
createElement("button", {
|
|
2952
|
+
type: "button",
|
|
2953
|
+
"data-action": "edit-provider",
|
|
2954
|
+
onClick: () => handleEdit(p.id)
|
|
2955
|
+
}, "Edit"),
|
|
2956
|
+
createElement("button", {
|
|
2957
|
+
type: "button",
|
|
2958
|
+
"data-action": "delete-provider",
|
|
2959
|
+
onClick: () => handleDelete(p.id)
|
|
2960
|
+
}, "Delete")
|
|
2961
|
+
)
|
|
2962
|
+
)
|
|
2963
|
+
);
|
|
2964
|
+
return createElement(
|
|
2965
|
+
"div",
|
|
2966
|
+
{ "data-provider-settings": "true", className },
|
|
2967
|
+
createElement(
|
|
2968
|
+
"div",
|
|
2969
|
+
{ "data-provider-settings-header": "true" },
|
|
2970
|
+
createElement("span", null, "Providers"),
|
|
2971
|
+
onClose ? createElement("button", {
|
|
2972
|
+
type: "button",
|
|
2973
|
+
"data-provider-settings-close": "true",
|
|
2974
|
+
onClick: onClose
|
|
2975
|
+
}, "\u2715") : null
|
|
2976
|
+
),
|
|
2977
|
+
createElement(
|
|
2978
|
+
"div",
|
|
2979
|
+
{ "data-provider-settings-list": "true" },
|
|
2980
|
+
...items,
|
|
2981
|
+
items.length === 0 ? createElement("div", { "data-provider-settings-empty": "true" }, "No providers configured") : null
|
|
2982
|
+
),
|
|
2983
|
+
createElement("button", {
|
|
2984
|
+
type: "button",
|
|
2985
|
+
"data-action": "add-provider",
|
|
2986
|
+
onClick: handleStartAdd
|
|
2987
|
+
}, "+ Add Provider")
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
if (view === "add") {
|
|
2991
|
+
const formChildren = [];
|
|
2992
|
+
formChildren.push(
|
|
2993
|
+
createElement(
|
|
2994
|
+
"div",
|
|
2995
|
+
{ "data-provider-settings-header": "true", key: "header" },
|
|
2996
|
+
createElement("span", null, "Add Provider"),
|
|
2997
|
+
createElement("button", {
|
|
2998
|
+
type: "button",
|
|
2999
|
+
"data-provider-settings-close": "true",
|
|
3000
|
+
onClick: () => {
|
|
3001
|
+
setView("list");
|
|
3002
|
+
setAddStep("select-backend");
|
|
3003
|
+
}
|
|
3004
|
+
}, "\u2190 Back")
|
|
3005
|
+
)
|
|
3006
|
+
);
|
|
3007
|
+
if (addStep === "select-backend") {
|
|
3008
|
+
formChildren.push(
|
|
3009
|
+
createElement(
|
|
3010
|
+
"div",
|
|
3011
|
+
{ key: "backends", "data-provider-settings-backends": "true" },
|
|
3012
|
+
...BACKENDS.map(
|
|
3013
|
+
(b) => createElement("button", {
|
|
3014
|
+
key: b.id,
|
|
3015
|
+
type: "button",
|
|
3016
|
+
"data-provider-backend-option": b.id,
|
|
3017
|
+
onClick: () => handleBackendSelect(b.id)
|
|
3018
|
+
}, b.label)
|
|
3019
|
+
)
|
|
3020
|
+
)
|
|
3021
|
+
);
|
|
3022
|
+
} else if (addStep === "auth") {
|
|
3023
|
+
const FormComponent = selectedBackend ? AUTH_FORMS[selectedBackend] : null;
|
|
3024
|
+
formChildren.push(
|
|
3025
|
+
createElement(
|
|
3026
|
+
"div",
|
|
3027
|
+
{ key: "auth", "data-provider-settings-auth": "true" },
|
|
3028
|
+
FormComponent ? createElement(FormComponent, { auth, onAuthComplete: handleAuthComplete }) : null
|
|
3029
|
+
)
|
|
3030
|
+
);
|
|
3031
|
+
} else if (addStep === "configure") {
|
|
3032
|
+
const modelInput = availableModels.length > 0 ? createElement(
|
|
3033
|
+
"select",
|
|
3034
|
+
{
|
|
3035
|
+
ref: modelRef,
|
|
3036
|
+
"data-input": "model",
|
|
3037
|
+
defaultValue: ""
|
|
3038
|
+
},
|
|
3039
|
+
createElement("option", { value: "", disabled: true }, "Select a model\u2026"),
|
|
3040
|
+
...availableModels.map(
|
|
3041
|
+
(m) => createElement("option", { key: m.id, value: m.id }, m.name || m.id)
|
|
3042
|
+
)
|
|
3043
|
+
) : createElement("input", {
|
|
3044
|
+
ref: modelRef,
|
|
3045
|
+
placeholder: "Model name (e.g. gpt-5-mini)",
|
|
3046
|
+
"data-input": "model"
|
|
3047
|
+
});
|
|
3048
|
+
formChildren.push(
|
|
3049
|
+
createElement(
|
|
3050
|
+
"div",
|
|
3051
|
+
{ key: "config", "data-provider-settings-form": "true" },
|
|
3052
|
+
modelInput,
|
|
3053
|
+
createElement("input", {
|
|
3054
|
+
ref: labelRef,
|
|
3055
|
+
placeholder: "Display label (e.g. GPT-5 Mini)",
|
|
3056
|
+
"data-input": "label"
|
|
3057
|
+
}),
|
|
3058
|
+
createElement("button", {
|
|
3059
|
+
type: "button",
|
|
3060
|
+
"data-action": "save-provider",
|
|
3061
|
+
onClick: handleCreate
|
|
3062
|
+
}, "Save Provider")
|
|
3063
|
+
)
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
return createElement(
|
|
3067
|
+
"div",
|
|
3068
|
+
{ "data-provider-settings": "true", className },
|
|
3069
|
+
...formChildren
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
const editingProvider = providers.find((p) => p.id === editingId);
|
|
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, "Edit Provider"),
|
|
3080
|
+
createElement("button", {
|
|
3081
|
+
type: "button",
|
|
3082
|
+
"data-provider-settings-close": "true",
|
|
3083
|
+
onClick: () => {
|
|
3084
|
+
setView("list");
|
|
3085
|
+
setEditingId(null);
|
|
3086
|
+
}
|
|
3087
|
+
}, "\u2190 Back")
|
|
3088
|
+
),
|
|
3089
|
+
createElement(
|
|
3090
|
+
"div",
|
|
3091
|
+
{ "data-provider-settings-form": "true" },
|
|
3092
|
+
availableModels.length > 0 ? createElement(
|
|
3093
|
+
"select",
|
|
3094
|
+
{
|
|
3095
|
+
ref: editModelRef,
|
|
3096
|
+
defaultValue: editingProvider?.model ?? "",
|
|
3097
|
+
"data-input": "model"
|
|
3098
|
+
},
|
|
3099
|
+
createElement("option", { value: "", disabled: true }, "Select a model\u2026"),
|
|
3100
|
+
...availableModels.map(
|
|
3101
|
+
(m) => createElement("option", { key: m.id, value: m.id }, m.name || m.id)
|
|
3102
|
+
)
|
|
3103
|
+
) : createElement("input", {
|
|
3104
|
+
ref: editModelRef,
|
|
3105
|
+
defaultValue: editingProvider?.model ?? "",
|
|
3106
|
+
placeholder: "Model name",
|
|
3107
|
+
"data-input": "model"
|
|
3108
|
+
}),
|
|
3109
|
+
createElement("input", {
|
|
3110
|
+
ref: editLabelRef,
|
|
3111
|
+
defaultValue: editingProvider?.label ?? "",
|
|
3112
|
+
placeholder: "Display label",
|
|
3113
|
+
"data-input": "label"
|
|
3114
|
+
}),
|
|
3115
|
+
createElement("button", {
|
|
3116
|
+
type: "button",
|
|
3117
|
+
"data-action": "update-provider",
|
|
3118
|
+
onClick: handleUpdate
|
|
3119
|
+
}, "Update")
|
|
3120
|
+
)
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
function formatTokens(n) {
|
|
3124
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
3125
|
+
return String(n);
|
|
3126
|
+
}
|
|
3127
|
+
function ContextStatsDisplay({ stats, className }) {
|
|
3128
|
+
if (!stats) return null;
|
|
3129
|
+
const hasRealData = stats.realPromptTokens != null && stats.modelContextWindow != null;
|
|
3130
|
+
if (!hasRealData) return null;
|
|
3131
|
+
const promptTokens = stats.realPromptTokens;
|
|
3132
|
+
const contextWindow = stats.modelContextWindow;
|
|
3133
|
+
const availableBudget = Math.max(0, contextWindow - promptTokens);
|
|
3134
|
+
const usagePercent = contextWindow > 0 ? Math.round(promptTokens / contextWindow * 100) : 0;
|
|
3135
|
+
return createElement(
|
|
3136
|
+
"div",
|
|
3137
|
+
{
|
|
3138
|
+
"data-context-stats": "",
|
|
3139
|
+
"data-context-truncated": stats.wasTruncated ? "true" : "false",
|
|
3140
|
+
className
|
|
3141
|
+
},
|
|
3142
|
+
createElement(
|
|
3143
|
+
"span",
|
|
3144
|
+
{ "data-context-tokens": "" },
|
|
3145
|
+
`${formatTokens(promptTokens)} tokens`
|
|
3146
|
+
),
|
|
3147
|
+
createElement(
|
|
3148
|
+
"span",
|
|
3149
|
+
{ "data-context-budget": "" },
|
|
3150
|
+
`${formatTokens(availableBudget)} available`
|
|
3151
|
+
),
|
|
3152
|
+
createElement(
|
|
3153
|
+
"span",
|
|
3154
|
+
{ "data-context-usage": "", "data-usage-percent": String(usagePercent) },
|
|
3155
|
+
`${usagePercent}%`
|
|
3156
|
+
),
|
|
3157
|
+
stats.removedCount > 0 ? createElement(
|
|
3158
|
+
"span",
|
|
3159
|
+
{ "data-context-removed": "" },
|
|
3160
|
+
`${stats.removedCount} trimmed`
|
|
3161
|
+
) : null
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
function ChatLayout({ children, sidebar, overlay, className }) {
|
|
3165
|
+
const overlayNodes = Array.isArray(overlay) ? overlay : [overlay ?? null];
|
|
3166
|
+
return createElement(
|
|
3167
|
+
"div",
|
|
3168
|
+
{ "data-chat-ui": "", className },
|
|
3169
|
+
...overlayNodes,
|
|
3170
|
+
sidebar ?? null,
|
|
3171
|
+
children
|
|
3172
|
+
);
|
|
3173
|
+
}
|
|
3174
|
+
function ChatHeader({
|
|
3175
|
+
showBackendSelector = false,
|
|
3176
|
+
showModelSelector = true,
|
|
3177
|
+
hasProviders = false,
|
|
3178
|
+
backends = [],
|
|
3179
|
+
models = [],
|
|
3180
|
+
selectedModel,
|
|
3181
|
+
onBackendSelect,
|
|
3182
|
+
onModelSelect,
|
|
3183
|
+
BackendSelectorComponent: BSC = BackendSelector,
|
|
3184
|
+
ModelSelectorComponent: MSC = ModelSelector
|
|
3185
|
+
}) {
|
|
3186
|
+
const children = [];
|
|
3187
|
+
if (showBackendSelector) {
|
|
3188
|
+
children.push(
|
|
3189
|
+
createElement(BSC, {
|
|
3190
|
+
key: "backend-selector",
|
|
3191
|
+
backends,
|
|
3192
|
+
onSelect: onBackendSelect ?? (() => {
|
|
3193
|
+
})
|
|
3194
|
+
})
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
if (showModelSelector && !hasProviders && models.length > 0) {
|
|
3198
|
+
children.push(
|
|
3199
|
+
createElement(MSC, {
|
|
3200
|
+
key: "model-selector",
|
|
3201
|
+
models,
|
|
3202
|
+
selectedModel,
|
|
3203
|
+
onSelect: onModelSelect ?? (() => {
|
|
3204
|
+
})
|
|
3205
|
+
})
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3208
|
+
if (children.length === 0) return null;
|
|
3209
|
+
return createElement("div", { "data-chat-header": "" }, ...children);
|
|
3210
|
+
}
|
|
3211
|
+
function UsageBadge({ usage, className }) {
|
|
3212
|
+
if (!usage) return null;
|
|
3213
|
+
return createElement(
|
|
3214
|
+
"span",
|
|
3215
|
+
{ "data-usage-badge": "true", className },
|
|
3216
|
+
createElement("span", { "data-usage-tokens": "prompt" }, `\u2191${usage.promptTokens}`),
|
|
3217
|
+
createElement("span", { "data-usage-tokens": "completion" }, `\u2193${usage.completionTokens}`),
|
|
3218
|
+
createElement("span", { "data-usage-tokens": "total" }, `\u03A3${usage.totalTokens}`)
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
// src/chat/react/ChatInputArea.ts
|
|
3223
|
+
function ChatInputArea({
|
|
3224
|
+
onSend,
|
|
3225
|
+
onStop,
|
|
3226
|
+
isGenerating,
|
|
3227
|
+
placeholder,
|
|
3228
|
+
providers = [],
|
|
3229
|
+
models = [],
|
|
3230
|
+
activeProviderId,
|
|
3231
|
+
selectedModel,
|
|
3232
|
+
onSelectProvider,
|
|
3233
|
+
onSelectModel,
|
|
3234
|
+
onSettingsClick,
|
|
3235
|
+
ComposerComponent: CC = Composer,
|
|
3236
|
+
ProviderModelSelectorComponent: PMSC = ProviderModelSelector,
|
|
3237
|
+
usage
|
|
3238
|
+
}) {
|
|
3239
|
+
const selectorRow = createElement(
|
|
3240
|
+
"div",
|
|
3241
|
+
{ "data-chat-input-controls": "" },
|
|
3242
|
+
createElement(PMSC, {
|
|
3243
|
+
providers,
|
|
3244
|
+
models,
|
|
3245
|
+
activeProviderId,
|
|
3246
|
+
selectedModel,
|
|
3247
|
+
onSelectProvider,
|
|
3248
|
+
onSelectModel,
|
|
3249
|
+
onSettingsClick
|
|
3250
|
+
}),
|
|
3251
|
+
usage ? createElement(UsageBadge, { usage }) : null
|
|
3252
|
+
);
|
|
3253
|
+
return createElement(
|
|
3254
|
+
"div",
|
|
3255
|
+
{ "data-chat-input-area": "" },
|
|
3256
|
+
createElement(
|
|
3257
|
+
"div",
|
|
3258
|
+
{ "data-chat-input-container": "" },
|
|
3259
|
+
selectorRow,
|
|
3260
|
+
createElement(CC, {
|
|
3261
|
+
onSend,
|
|
3262
|
+
onStop,
|
|
3263
|
+
isGenerating,
|
|
3264
|
+
placeholder
|
|
3265
|
+
})
|
|
3266
|
+
)
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
var FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
3270
|
+
var CLOSE_ANIMATION_MS = 150;
|
|
3271
|
+
function ChatSettingsOverlay({
|
|
3272
|
+
open,
|
|
3273
|
+
onClose,
|
|
3274
|
+
providers = [],
|
|
3275
|
+
authBaseUrl,
|
|
3276
|
+
onProviderCreated,
|
|
3277
|
+
onProviderDeleted,
|
|
3278
|
+
onProviderUpdated,
|
|
3279
|
+
onAuthCompleted,
|
|
3280
|
+
ProviderSettingsComponent: PSC = ProviderSettings
|
|
3281
|
+
}) {
|
|
3282
|
+
const [isClosing, setIsClosing] = useState(false);
|
|
3283
|
+
const contentRef = useRef(null);
|
|
3284
|
+
const previousFocusRef = useRef(null);
|
|
3285
|
+
const handleClose = useCallback(() => {
|
|
3286
|
+
setIsClosing(true);
|
|
3287
|
+
setTimeout(() => {
|
|
3288
|
+
setIsClosing(false);
|
|
3289
|
+
onClose();
|
|
3290
|
+
}, CLOSE_ANIMATION_MS);
|
|
3291
|
+
}, [onClose]);
|
|
3292
|
+
const handleKeyDown = useCallback((e) => {
|
|
3293
|
+
if (e.key === "Escape") {
|
|
3294
|
+
e.stopPropagation();
|
|
3295
|
+
handleClose();
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
if (e.key === "Tab" && contentRef.current) {
|
|
3299
|
+
const focusable = Array.from(contentRef.current.querySelectorAll(FOCUSABLE));
|
|
3300
|
+
if (focusable.length === 0) return;
|
|
3301
|
+
const first = focusable[0];
|
|
3302
|
+
const last = focusable[focusable.length - 1];
|
|
3303
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
3304
|
+
e.preventDefault();
|
|
3305
|
+
last.focus();
|
|
3306
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
3307
|
+
e.preventDefault();
|
|
3308
|
+
first.focus();
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
}, [handleClose]);
|
|
3312
|
+
const handleBackdropClick = useCallback((e) => {
|
|
3313
|
+
if (e.target === e.currentTarget) handleClose();
|
|
3314
|
+
}, [handleClose]);
|
|
3315
|
+
useEffect(() => {
|
|
3316
|
+
if (!open) return;
|
|
3317
|
+
previousFocusRef.current = document.activeElement;
|
|
3318
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
3319
|
+
const timer = setTimeout(() => {
|
|
3320
|
+
if (contentRef.current) {
|
|
3321
|
+
const first = contentRef.current.querySelector(FOCUSABLE);
|
|
3322
|
+
if (first) first.focus();
|
|
3323
|
+
}
|
|
3324
|
+
}, 50);
|
|
3325
|
+
return () => {
|
|
3326
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
3327
|
+
clearTimeout(timer);
|
|
3328
|
+
const prev = previousFocusRef.current;
|
|
3329
|
+
if (prev && typeof prev.focus === "function") prev.focus();
|
|
3330
|
+
};
|
|
3331
|
+
}, [open, handleKeyDown]);
|
|
3332
|
+
if (!open && !isClosing) return null;
|
|
3333
|
+
return createElement(
|
|
3334
|
+
"div",
|
|
3335
|
+
{
|
|
3336
|
+
"data-provider-settings-overlay": "true",
|
|
3337
|
+
"data-closing": isClosing ? "true" : void 0,
|
|
3338
|
+
onClick: handleBackdropClick,
|
|
3339
|
+
role: "dialog",
|
|
3340
|
+
"aria-modal": "true",
|
|
3341
|
+
"aria-label": "Provider settings"
|
|
3342
|
+
},
|
|
3343
|
+
createElement(
|
|
3344
|
+
"div",
|
|
3345
|
+
{
|
|
3346
|
+
ref: contentRef,
|
|
3347
|
+
"data-provider-settings-content": "true",
|
|
3348
|
+
"data-closing": isClosing ? "true" : void 0
|
|
3349
|
+
},
|
|
3350
|
+
createElement(PSC, {
|
|
3351
|
+
providers,
|
|
3352
|
+
authBaseUrl,
|
|
3353
|
+
onClose: handleClose,
|
|
3354
|
+
onProviderCreated: onProviderCreated ?? void 0,
|
|
3355
|
+
onProviderDeleted: onProviderDeleted ?? void 0,
|
|
3356
|
+
onProviderUpdated: onProviderUpdated ?? void 0,
|
|
3357
|
+
onAuthCompleted: onAuthCompleted ?? void 0
|
|
3358
|
+
})
|
|
3359
|
+
)
|
|
3360
|
+
);
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
// src/chat/react/ChatUI.ts
|
|
3364
|
+
function ChatUIInner({
|
|
3365
|
+
slots,
|
|
3366
|
+
className,
|
|
3367
|
+
showSidebar = true,
|
|
3368
|
+
showModelSelector = true,
|
|
3369
|
+
showBackendSelector: showBackendSelectorProp,
|
|
3370
|
+
showProviderSelector: _showProviderSelectorProp,
|
|
3371
|
+
authBaseUrl,
|
|
3372
|
+
placeholder
|
|
3373
|
+
}) {
|
|
3374
|
+
const runtime = useChatRuntime();
|
|
3375
|
+
const { messages, sendMessage, stop, isGenerating, newSession, error, clearError, retryLastMessage, usage } = useChat();
|
|
3376
|
+
const { sessions } = useSessions();
|
|
3377
|
+
const { models, refresh: refreshModels } = useModels();
|
|
3378
|
+
const { backends } = useBackends();
|
|
3379
|
+
const { providers, createProvider, updateProvider, deleteProvider, selectProvider, refresh } = useProviders();
|
|
3380
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
3381
|
+
const [activeProviderId, setActiveProviderId] = useState(void 0);
|
|
3382
|
+
const [selectedModelId, setSelectedModelId] = useState(void 0);
|
|
3383
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
3384
|
+
const hasProviders = providers.length > 0;
|
|
3385
|
+
useEffect(() => {
|
|
3386
|
+
if (providers.length > 0 && !activeProviderId) {
|
|
3387
|
+
setActiveProviderId(providers[0].id);
|
|
3388
|
+
selectProvider(providers[0].id);
|
|
3389
|
+
}
|
|
3390
|
+
}, [providers, activeProviderId, selectProvider]);
|
|
3391
|
+
const showBackendSelectorResolved = showBackendSelectorProp ?? false;
|
|
3392
|
+
const handleSelect = useCallback(async (id) => {
|
|
3393
|
+
try {
|
|
3394
|
+
await runtime.switchSession(id);
|
|
3395
|
+
} catch {
|
|
3396
|
+
}
|
|
3397
|
+
}, [runtime]);
|
|
3398
|
+
const handleCreate = useCallback(async () => {
|
|
3399
|
+
try {
|
|
3400
|
+
await newSession();
|
|
3401
|
+
} catch {
|
|
3402
|
+
}
|
|
3403
|
+
}, [newSession]);
|
|
3404
|
+
const handleDelete = useCallback(async (id) => {
|
|
3405
|
+
try {
|
|
3406
|
+
await runtime.deleteSession(id);
|
|
3407
|
+
} catch {
|
|
3408
|
+
}
|
|
3409
|
+
}, [runtime]);
|
|
3410
|
+
const handleModelSelect = useCallback((modelId) => {
|
|
3411
|
+
setSelectedModelId(modelId);
|
|
3412
|
+
}, []);
|
|
3413
|
+
const handleBackendSelect = useCallback((_name) => {
|
|
3414
|
+
refreshModels();
|
|
3415
|
+
}, [refreshModels]);
|
|
3416
|
+
const handleProviderSelect = useCallback((id) => {
|
|
3417
|
+
selectProvider(id);
|
|
3418
|
+
setActiveProviderId(id);
|
|
3419
|
+
}, [selectProvider]);
|
|
3420
|
+
const hasSlotOverrides = !!(slots?.renderToolCall || slots?.renderMessage || slots?.renderThinkingBlock);
|
|
3421
|
+
const ThreadComponent = slots?.thread ?? Thread;
|
|
3422
|
+
const ThreadListComponent = slots?.threadList ?? ThreadList;
|
|
3423
|
+
const ContextStatsComponent = slots?.contextStats ?? ContextStatsDisplay;
|
|
3424
|
+
const [contextStats, setContextStats] = useState(null);
|
|
3425
|
+
useEffect(() => {
|
|
3426
|
+
if (!runtime.activeSessionId) {
|
|
3427
|
+
setContextStats(null);
|
|
3428
|
+
return;
|
|
3429
|
+
}
|
|
3430
|
+
const result = runtime.getContextStats(runtime.activeSessionId);
|
|
3431
|
+
if (result instanceof Promise) {
|
|
3432
|
+
result.then(setContextStats, () => setContextStats(null));
|
|
3433
|
+
} else {
|
|
3434
|
+
setContextStats(result);
|
|
3435
|
+
}
|
|
3436
|
+
}, [runtime, runtime.activeSessionId, messages.length]);
|
|
3437
|
+
const showEmptyState = !hasProviders && messages.length === 0;
|
|
3438
|
+
const mainContent = createElement(
|
|
3439
|
+
"div",
|
|
3440
|
+
{ "data-chat-main": "" },
|
|
3441
|
+
createElement(ChatHeader, {
|
|
3442
|
+
showBackendSelector: showBackendSelectorResolved,
|
|
3443
|
+
showModelSelector,
|
|
3444
|
+
hasProviders,
|
|
3445
|
+
backends,
|
|
3446
|
+
models,
|
|
3447
|
+
selectedModel: selectedModelId,
|
|
3448
|
+
onBackendSelect: handleBackendSelect,
|
|
3449
|
+
onModelSelect: handleModelSelect,
|
|
3450
|
+
BackendSelectorComponent: slots?.backendSelector,
|
|
3451
|
+
ModelSelectorComponent: slots?.modelSelector
|
|
3452
|
+
}),
|
|
3453
|
+
contextStats ? createElement(ContextStatsComponent, { stats: contextStats }) : null,
|
|
3454
|
+
error ? createElement(
|
|
3455
|
+
"div",
|
|
3456
|
+
{ "data-chat-error": "" },
|
|
3457
|
+
createElement("span", { "data-chat-error-text": "" }, error.message),
|
|
3458
|
+
createElement(
|
|
3459
|
+
"div",
|
|
3460
|
+
{ "data-chat-error-actions": "" },
|
|
3461
|
+
createElement("button", {
|
|
3462
|
+
"data-action": "retry",
|
|
3463
|
+
type: "button",
|
|
3464
|
+
onClick: retryLastMessage
|
|
3465
|
+
}, "Retry"),
|
|
3466
|
+
createElement("button", {
|
|
3467
|
+
"data-action": "dismiss-error",
|
|
3468
|
+
type: "button",
|
|
3469
|
+
onClick: clearError
|
|
3470
|
+
}, "\u2715")
|
|
3471
|
+
),
|
|
3472
|
+
error.stack ? createElement(
|
|
3473
|
+
"details",
|
|
3474
|
+
{ "data-chat-error-details": "" },
|
|
3475
|
+
createElement("summary", null, "Details"),
|
|
3476
|
+
createElement("pre", null, error.stack)
|
|
3477
|
+
) : null
|
|
3478
|
+
) : null,
|
|
3479
|
+
showEmptyState ? createElement(
|
|
3480
|
+
"div",
|
|
3481
|
+
{ "data-chat-empty-state": "" },
|
|
3482
|
+
createElement("div", { "data-chat-empty-title": "" }, "Connect a provider to start chatting"),
|
|
3483
|
+
createElement("button", {
|
|
3484
|
+
"data-action": "open-settings",
|
|
3485
|
+
onClick: () => setSettingsOpen(true)
|
|
3486
|
+
}, "+ Connect Provider")
|
|
3487
|
+
) : createElement(ThreadComponent, { messages, isGenerating, autoScroll: true }),
|
|
3488
|
+
createElement(ChatInputArea, {
|
|
3489
|
+
onSend: sendMessage,
|
|
3490
|
+
onStop: stop,
|
|
3491
|
+
isGenerating,
|
|
3492
|
+
placeholder,
|
|
3493
|
+
providers,
|
|
3494
|
+
models,
|
|
3495
|
+
activeProviderId,
|
|
3496
|
+
selectedModel: selectedModelId,
|
|
3497
|
+
onSelectProvider: handleProviderSelect,
|
|
3498
|
+
onSelectModel: handleModelSelect,
|
|
3499
|
+
onSettingsClick: () => setSettingsOpen(true),
|
|
3500
|
+
ComposerComponent: slots?.composer,
|
|
3501
|
+
ProviderModelSelectorComponent: slots?.providerModelSelector,
|
|
3502
|
+
usage
|
|
3503
|
+
})
|
|
3504
|
+
);
|
|
3505
|
+
const wrappedMain = hasSlotOverrides ? createElement(ThreadProvider, {
|
|
3506
|
+
renderToolCall: slots.renderToolCall,
|
|
3507
|
+
renderMessage: slots.renderMessage,
|
|
3508
|
+
renderThinkingBlock: slots.renderThinkingBlock,
|
|
3509
|
+
children: mainContent
|
|
3510
|
+
}) : mainContent;
|
|
3511
|
+
const sidebar = showSidebar ? createElement(ThreadListComponent, {
|
|
3512
|
+
sessions,
|
|
3513
|
+
activeSessionId: runtime.activeSessionId ?? void 0,
|
|
3514
|
+
onSelect: handleSelect,
|
|
3515
|
+
onCreate: handleCreate,
|
|
3516
|
+
onDelete: handleDelete,
|
|
3517
|
+
searchQuery,
|
|
3518
|
+
onSearchChange: setSearchQuery
|
|
3519
|
+
}) : void 0;
|
|
3520
|
+
const settingsOverlay = createElement(ChatSettingsOverlay, {
|
|
3521
|
+
open: settingsOpen,
|
|
3522
|
+
onClose: () => setSettingsOpen(false),
|
|
3523
|
+
providers,
|
|
3524
|
+
authBaseUrl,
|
|
3525
|
+
onProviderCreated: (p) => createProvider({ backend: p.backend, model: p.model, label: p.label ?? "" }),
|
|
3526
|
+
onProviderDeleted: (id) => deleteProvider(id),
|
|
3527
|
+
onProviderUpdated: (id, changes) => updateProvider(id, changes),
|
|
3528
|
+
onAuthCompleted: () => refresh(),
|
|
3529
|
+
ProviderSettingsComponent: slots?.providerSettings
|
|
3530
|
+
});
|
|
3531
|
+
return createElement(ChatLayout, {
|
|
3532
|
+
className,
|
|
3533
|
+
sidebar,
|
|
3534
|
+
overlay: [slots?.authDialog ?? null, settingsOverlay],
|
|
3535
|
+
children: wrappedMain
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
function ChatUI({ runtime, ...rest }) {
|
|
3539
|
+
return createElement(ChatProvider, {
|
|
3540
|
+
runtime,
|
|
3541
|
+
children: createElement(ChatUIInner, rest)
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
2711
3544
|
|
|
2712
|
-
export {
|
|
3545
|
+
export { BackendSelector, ChatHeader, ChatInputArea, ChatLayout, ChatProvider, ChatSettingsOverlay, ChatUI, ClaudeAuthForm, Composer, ContextStatsDisplay, CopilotAuthForm, MarkdownRenderer, Message, ModelSelector, 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 };
|
|
2713
3546
|
//# sourceMappingURL=react.js.map
|
|
2714
3547
|
//# sourceMappingURL=react.js.map
|