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