@chrysb/alphaclaw 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/bin/alphaclaw.js +79 -0
- package/lib/public/css/shell.css +57 -2
- package/lib/public/css/theme.css +231 -0
- package/lib/public/js/app.js +330 -89
- package/lib/public/js/components/action-button.js +92 -0
- package/lib/public/js/components/channels.js +16 -7
- package/lib/public/js/components/confirm-dialog.js +25 -19
- package/lib/public/js/components/credentials-modal.js +32 -23
- package/lib/public/js/components/device-pairings.js +15 -2
- package/lib/public/js/components/envars.js +22 -65
- package/lib/public/js/components/features.js +1 -1
- package/lib/public/js/components/gateway.js +139 -32
- package/lib/public/js/components/global-restart-banner.js +31 -0
- package/lib/public/js/components/google.js +9 -9
- package/lib/public/js/components/icons.js +19 -0
- package/lib/public/js/components/info-tooltip.js +18 -0
- package/lib/public/js/components/loading-spinner.js +32 -0
- package/lib/public/js/components/modal-shell.js +42 -0
- package/lib/public/js/components/models.js +34 -29
- package/lib/public/js/components/onboarding/welcome-form-step.js +45 -32
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
- package/lib/public/js/components/onboarding/welcome-setup-step.js +7 -24
- package/lib/public/js/components/page-header.js +13 -0
- package/lib/public/js/components/pairings.js +15 -2
- package/lib/public/js/components/providers.js +216 -142
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/secret-input.js +1 -0
- package/lib/public/js/components/telegram-workspace.js +37 -49
- package/lib/public/js/components/toast.js +34 -5
- package/lib/public/js/components/toggle-switch.js +25 -0
- package/lib/public/js/components/update-action-button.js +13 -53
- package/lib/public/js/components/watchdog-tab.js +312 -0
- package/lib/public/js/components/webhooks.js +1010 -0
- package/lib/public/js/components/welcome.js +2 -1
- package/lib/public/js/lib/api.js +102 -1
- package/lib/public/js/lib/model-config.js +0 -5
- package/lib/server/alphaclaw-version.js +5 -3
- package/lib/server/constants.js +35 -0
- package/lib/server/discord-api.js +48 -0
- package/lib/server/gateway.js +64 -4
- package/lib/server/log-writer.js +102 -0
- package/lib/server/onboarding/github.js +21 -1
- package/lib/server/openclaw-version.js +2 -6
- package/lib/server/restart-required-state.js +86 -0
- package/lib/server/routes/auth.js +9 -4
- package/lib/server/routes/proxy.js +12 -14
- package/lib/server/routes/system.js +61 -15
- package/lib/server/routes/telegram.js +17 -48
- package/lib/server/routes/watchdog.js +68 -0
- package/lib/server/routes/webhooks.js +214 -0
- package/lib/server/telegram-api.js +11 -0
- package/lib/server/watchdog-db.js +148 -0
- package/lib/server/watchdog-notify.js +93 -0
- package/lib/server/watchdog.js +585 -0
- package/lib/server/webhook-middleware.js +195 -0
- package/lib/server/webhooks-db.js +265 -0
- package/lib/server/webhooks.js +238 -0
- package/lib/server.js +119 -4
- package/lib/setup/core-prompts/AGENTS.md +84 -0
- package/lib/setup/core-prompts/TOOLS.md +13 -0
- package/lib/setup/core-prompts/UI-DRY-OPPORTUNITIES.md +50 -0
- package/lib/setup/gitignore +2 -0
- package/package.json +11 -1
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
fetchCodexStatus,
|
|
11
11
|
disconnectCodex,
|
|
12
12
|
exchangeCodexOAuth,
|
|
13
|
-
restartGateway,
|
|
14
13
|
} from "../lib/api.js";
|
|
15
14
|
import { showToast } from "./toast.js";
|
|
16
15
|
import { Badge } from "./badge.js";
|
|
17
16
|
import { SecretInput } from "./secret-input.js";
|
|
17
|
+
import { PageHeader } from "./page-header.js";
|
|
18
|
+
import { LoadingSpinner } from "./loading-spinner.js";
|
|
19
|
+
import { ActionButton } from "./action-button.js";
|
|
18
20
|
import {
|
|
19
21
|
getModelProvider,
|
|
20
22
|
getAuthProviderFromModelProvider,
|
|
@@ -35,12 +37,15 @@ const kAiCredentialKeys = Object.values(kProviderAuthFields)
|
|
|
35
37
|
.filter((key, idx, arr) => arr.indexOf(key) === idx);
|
|
36
38
|
let kProvidersTabCache = null;
|
|
37
39
|
|
|
38
|
-
const FeatureTags = ({ provider }) => {
|
|
39
|
-
const
|
|
40
|
-
|
|
40
|
+
const FeatureTags = ({ provider, features = null }) => {
|
|
41
|
+
const resolvedFeatures = Array.isArray(features)
|
|
42
|
+
? features
|
|
43
|
+
: kProviderFeatures[provider] || [];
|
|
44
|
+
const uniqueFeatures = Array.from(new Set(resolvedFeatures));
|
|
45
|
+
if (!uniqueFeatures.length) return null;
|
|
41
46
|
return html`
|
|
42
47
|
<div class="flex flex-wrap gap-1.5">
|
|
43
|
-
${
|
|
48
|
+
${uniqueFeatures.map(
|
|
44
49
|
(f) => html`
|
|
45
50
|
<span
|
|
46
51
|
class="text-xs px-1.5 py-0.5 rounded-md bg-white/5 text-gray-400"
|
|
@@ -52,25 +57,37 @@ const FeatureTags = ({ provider }) => {
|
|
|
52
57
|
`;
|
|
53
58
|
};
|
|
54
59
|
|
|
55
|
-
export const Providers = () => {
|
|
56
|
-
const [envVars, setEnvVars] = useState(
|
|
60
|
+
export const Providers = ({ onRestartRequired = () => {} }) => {
|
|
61
|
+
const [envVars, setEnvVars] = useState(
|
|
62
|
+
() => kProvidersTabCache?.envVars || [],
|
|
63
|
+
);
|
|
57
64
|
const [models, setModels] = useState(() => kProvidersTabCache?.models || []);
|
|
58
|
-
const [selectedModel, setSelectedModel] = useState(
|
|
59
|
-
|
|
65
|
+
const [selectedModel, setSelectedModel] = useState(
|
|
66
|
+
() => kProvidersTabCache?.selectedModel || "",
|
|
67
|
+
);
|
|
68
|
+
const [showAllModels, setShowAllModels] = useState(
|
|
69
|
+
() => kProvidersTabCache?.showAllModels || false,
|
|
70
|
+
);
|
|
60
71
|
const [savingChanges, setSavingChanges] = useState(false);
|
|
61
|
-
const [codexStatus, setCodexStatus] = useState(
|
|
72
|
+
const [codexStatus, setCodexStatus] = useState(
|
|
73
|
+
() => kProvidersTabCache?.codexStatus || { connected: false },
|
|
74
|
+
);
|
|
62
75
|
const [codexManualInput, setCodexManualInput] = useState("");
|
|
63
76
|
const [codexExchanging, setCodexExchanging] = useState(false);
|
|
64
77
|
const [codexAuthStarted, setCodexAuthStarted] = useState(false);
|
|
65
78
|
const [codexAuthWaiting, setCodexAuthWaiting] = useState(false);
|
|
66
79
|
const [modelsLoading, setModelsLoading] = useState(() => !kProvidersTabCache);
|
|
67
|
-
const [modelsError, setModelsError] = useState(
|
|
80
|
+
const [modelsError, setModelsError] = useState(
|
|
81
|
+
() => kProvidersTabCache?.modelsError || "",
|
|
82
|
+
);
|
|
68
83
|
const [ready, setReady] = useState(() => !!kProvidersTabCache);
|
|
69
|
-
const [savedModel, setSavedModel] = useState(
|
|
84
|
+
const [savedModel, setSavedModel] = useState(
|
|
85
|
+
() => kProvidersTabCache?.savedModel || "",
|
|
86
|
+
);
|
|
70
87
|
const [modelDirty, setModelDirty] = useState(false);
|
|
71
|
-
const [savedAiValues, setSavedAiValues] = useState(
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
const [savedAiValues, setSavedAiValues] = useState(
|
|
89
|
+
() => kProvidersTabCache?.savedAiValues || {},
|
|
90
|
+
);
|
|
74
91
|
const [showMoreProviders, setShowMoreProviders] = useState(false);
|
|
75
92
|
const codexPopupPollRef = useRef(null);
|
|
76
93
|
|
|
@@ -85,7 +102,9 @@ export const Providers = () => {
|
|
|
85
102
|
fetchCodexStatus(),
|
|
86
103
|
]);
|
|
87
104
|
setEnvVars(env.vars || []);
|
|
88
|
-
const catalogModels = Array.isArray(modelCatalog.models)
|
|
105
|
+
const catalogModels = Array.isArray(modelCatalog.models)
|
|
106
|
+
? modelCatalog.models
|
|
107
|
+
: [];
|
|
89
108
|
setModels(catalogModels);
|
|
90
109
|
const currentModel = modelStatus.modelKey || "";
|
|
91
110
|
setSelectedModel(currentModel);
|
|
@@ -110,7 +129,7 @@ export const Providers = () => {
|
|
|
110
129
|
};
|
|
111
130
|
} catch (err) {
|
|
112
131
|
setModelsError("Failed to load provider settings");
|
|
113
|
-
showToast(`Failed to load provider settings: ${err.message}`, "
|
|
132
|
+
showToast(`Failed to load provider settings: ${err.message}`, "error");
|
|
114
133
|
} finally {
|
|
115
134
|
setReady(true);
|
|
116
135
|
setModelsLoading(false);
|
|
@@ -125,10 +144,16 @@ export const Providers = () => {
|
|
|
125
144
|
setCodexAuthStarted(false);
|
|
126
145
|
setCodexAuthWaiting(false);
|
|
127
146
|
}
|
|
128
|
-
kProvidersTabCache = {
|
|
147
|
+
kProvidersTabCache = {
|
|
148
|
+
...(kProvidersTabCache || {}),
|
|
149
|
+
codexStatus: codex || { connected: false },
|
|
150
|
+
};
|
|
129
151
|
} catch {
|
|
130
152
|
setCodexStatus({ connected: false });
|
|
131
|
-
kProvidersTabCache = {
|
|
153
|
+
kProvidersTabCache = {
|
|
154
|
+
...(kProvidersTabCache || {}),
|
|
155
|
+
codexStatus: { connected: false },
|
|
156
|
+
};
|
|
132
157
|
}
|
|
133
158
|
};
|
|
134
159
|
|
|
@@ -136,20 +161,26 @@ export const Providers = () => {
|
|
|
136
161
|
refresh();
|
|
137
162
|
}, []);
|
|
138
163
|
|
|
139
|
-
useEffect(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
164
|
+
useEffect(
|
|
165
|
+
() => () => {
|
|
166
|
+
if (codexPopupPollRef.current) {
|
|
167
|
+
clearInterval(codexPopupPollRef.current);
|
|
168
|
+
codexPopupPollRef.current = null;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
[],
|
|
172
|
+
);
|
|
145
173
|
|
|
146
174
|
useEffect(() => {
|
|
147
175
|
const onMessage = async (e) => {
|
|
148
176
|
if (e.data?.codex === "success") {
|
|
149
|
-
showToast("Codex connected", "
|
|
177
|
+
showToast("Codex connected", "success");
|
|
150
178
|
await refreshCodexConnection();
|
|
151
179
|
} else if (e.data?.codex === "error") {
|
|
152
|
-
showToast(
|
|
180
|
+
showToast(
|
|
181
|
+
`Codex auth failed: ${e.data.message || "unknown error"}`,
|
|
182
|
+
"error",
|
|
183
|
+
);
|
|
153
184
|
}
|
|
154
185
|
};
|
|
155
186
|
window.addEventListener("message", onMessage);
|
|
@@ -165,7 +196,9 @@ export const Providers = () => {
|
|
|
165
196
|
};
|
|
166
197
|
|
|
167
198
|
const selectedModelProvider = getModelProvider(selectedModel);
|
|
168
|
-
const selectedAuthProvider = getAuthProviderFromModelProvider(
|
|
199
|
+
const selectedAuthProvider = getAuthProviderFromModelProvider(
|
|
200
|
+
selectedModelProvider,
|
|
201
|
+
);
|
|
169
202
|
const primaryProvider = kProviderOrder.includes(selectedAuthProvider)
|
|
170
203
|
? selectedAuthProvider
|
|
171
204
|
: kProviderOrder[0];
|
|
@@ -174,36 +207,47 @@ export const Providers = () => {
|
|
|
174
207
|
const baseModelOptions = showAllModels
|
|
175
208
|
? models
|
|
176
209
|
: featuredModels.length > 0
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const selectedModelOption = models.find(
|
|
210
|
+
? featuredModels
|
|
211
|
+
: models;
|
|
212
|
+
const selectedModelOption = models.find(
|
|
213
|
+
(model) => model.key === selectedModel,
|
|
214
|
+
);
|
|
180
215
|
const modelOptions =
|
|
181
216
|
selectedModelOption &&
|
|
182
217
|
!baseModelOptions.some((model) => model.key === selectedModelOption.key)
|
|
183
218
|
? [...baseModelOptions, selectedModelOption]
|
|
184
219
|
: baseModelOptions;
|
|
185
|
-
const canToggleFullCatalog =
|
|
220
|
+
const canToggleFullCatalog =
|
|
221
|
+
featuredModels.length > 0 && models.length > featuredModels.length;
|
|
186
222
|
|
|
187
223
|
const aiCredentialsDirty = kAiCredentialKeys.some(
|
|
188
224
|
(key) => getKeyVal(envVars, key) !== (savedAiValues[key] || ""),
|
|
189
225
|
);
|
|
190
226
|
const hasSelectedProviderAuth =
|
|
191
227
|
selectedModelProvider === "anthropic"
|
|
192
|
-
? !!(
|
|
228
|
+
? !!(
|
|
229
|
+
getKeyVal(envVars, "ANTHROPIC_API_KEY") ||
|
|
230
|
+
getKeyVal(envVars, "ANTHROPIC_TOKEN")
|
|
231
|
+
)
|
|
193
232
|
: selectedModelProvider === "openai"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const canSaveChanges =
|
|
233
|
+
? !!getKeyVal(envVars, "OPENAI_API_KEY")
|
|
234
|
+
: selectedModelProvider === "openai-codex"
|
|
235
|
+
? !!(codexStatus.connected || getKeyVal(envVars, "OPENAI_API_KEY"))
|
|
236
|
+
: selectedModelProvider === "google"
|
|
237
|
+
? !!getKeyVal(envVars, "GEMINI_API_KEY")
|
|
238
|
+
: false;
|
|
239
|
+
const canSaveChanges =
|
|
240
|
+
!savingChanges &&
|
|
241
|
+
(aiCredentialsDirty || (modelDirty && hasSelectedProviderAuth));
|
|
201
242
|
|
|
202
243
|
const saveChanges = async () => {
|
|
203
244
|
if (savingChanges) return;
|
|
204
245
|
if (!modelDirty && !aiCredentialsDirty) return;
|
|
205
246
|
if (modelDirty && !hasSelectedProviderAuth) {
|
|
206
|
-
showToast(
|
|
247
|
+
showToast(
|
|
248
|
+
"Add credentials for the selected model provider before saving model changes",
|
|
249
|
+
"error",
|
|
250
|
+
);
|
|
207
251
|
return;
|
|
208
252
|
}
|
|
209
253
|
setSavingChanges(true);
|
|
@@ -215,30 +259,38 @@ export const Providers = () => {
|
|
|
215
259
|
.filter((v) => v.editable)
|
|
216
260
|
.map((v) => ({ key: v.key, value: v.value }));
|
|
217
261
|
const envResult = await saveEnvVars(payload);
|
|
218
|
-
if (!envResult.ok)
|
|
219
|
-
|
|
262
|
+
if (!envResult.ok)
|
|
263
|
+
throw new Error(envResult.error || "Failed to save env vars");
|
|
264
|
+
if (envResult.restartRequired) onRestartRequired(true);
|
|
220
265
|
}
|
|
221
266
|
|
|
222
267
|
if (modelDirty && targetModel) {
|
|
223
268
|
const modelResult = await setPrimaryModel(targetModel);
|
|
224
|
-
if (!modelResult.ok)
|
|
269
|
+
if (!modelResult.ok)
|
|
270
|
+
throw new Error(modelResult.error || "Failed to set primary model");
|
|
225
271
|
const status = await fetchModelStatus();
|
|
226
272
|
if (status?.ok === false) {
|
|
227
273
|
throw new Error(status.error || "Failed to verify primary model");
|
|
228
274
|
}
|
|
229
275
|
const activeModel = status?.modelKey || "";
|
|
230
276
|
if (activeModel && activeModel !== targetModel) {
|
|
231
|
-
throw new Error(
|
|
277
|
+
throw new Error(
|
|
278
|
+
`Primary model did not apply. Expected ${targetModel} but active is ${activeModel}`,
|
|
279
|
+
);
|
|
232
280
|
}
|
|
233
281
|
setSavedModel(targetModel);
|
|
234
282
|
setModelDirty(false);
|
|
235
|
-
kProvidersTabCache = {
|
|
283
|
+
kProvidersTabCache = {
|
|
284
|
+
...(kProvidersTabCache || {}),
|
|
285
|
+
selectedModel: targetModel,
|
|
286
|
+
savedModel: targetModel,
|
|
287
|
+
};
|
|
236
288
|
}
|
|
237
289
|
|
|
238
|
-
showToast("Changes saved", "green");
|
|
239
290
|
await refresh();
|
|
291
|
+
showToast("Changes saved", "success");
|
|
240
292
|
} catch (err) {
|
|
241
|
-
showToast(err.message || "Failed to save changes", "
|
|
293
|
+
showToast(err.message || "Failed to save changes", "error");
|
|
242
294
|
} finally {
|
|
243
295
|
setSavingChanges(false);
|
|
244
296
|
}
|
|
@@ -248,7 +300,11 @@ export const Providers = () => {
|
|
|
248
300
|
if (codexStatus.connected) return;
|
|
249
301
|
setCodexAuthStarted(true);
|
|
250
302
|
setCodexAuthWaiting(true);
|
|
251
|
-
const popup = window.open(
|
|
303
|
+
const popup = window.open(
|
|
304
|
+
"/auth/codex/start",
|
|
305
|
+
"codex-auth",
|
|
306
|
+
"popup=yes,width=640,height=780",
|
|
307
|
+
);
|
|
252
308
|
if (!popup || popup.closed) {
|
|
253
309
|
setCodexAuthWaiting(false);
|
|
254
310
|
window.location.href = "/auth/codex/start";
|
|
@@ -271,14 +327,15 @@ export const Providers = () => {
|
|
|
271
327
|
setCodexExchanging(true);
|
|
272
328
|
try {
|
|
273
329
|
const result = await exchangeCodexOAuth(codexManualInput.trim());
|
|
274
|
-
if (!result.ok)
|
|
330
|
+
if (!result.ok)
|
|
331
|
+
throw new Error(result.error || "Codex OAuth exchange failed");
|
|
275
332
|
setCodexManualInput("");
|
|
276
|
-
showToast("Codex connected", "
|
|
333
|
+
showToast("Codex connected", "success");
|
|
277
334
|
setCodexAuthStarted(false);
|
|
278
335
|
setCodexAuthWaiting(false);
|
|
279
336
|
await refreshCodexConnection();
|
|
280
337
|
} catch (err) {
|
|
281
|
-
showToast(err.message || "Codex OAuth exchange failed", "
|
|
338
|
+
showToast(err.message || "Codex OAuth exchange failed", "error");
|
|
282
339
|
} finally {
|
|
283
340
|
setCodexExchanging(false);
|
|
284
341
|
}
|
|
@@ -287,10 +344,10 @@ export const Providers = () => {
|
|
|
287
344
|
const handleCodexDisconnect = async () => {
|
|
288
345
|
const result = await disconnectCodex();
|
|
289
346
|
if (!result.ok) {
|
|
290
|
-
showToast(result.error || "Failed to disconnect Codex", "
|
|
347
|
+
showToast(result.error || "Failed to disconnect Codex", "error");
|
|
291
348
|
return;
|
|
292
349
|
}
|
|
293
|
-
showToast("Codex disconnected", "
|
|
350
|
+
showToast("Codex disconnected", "success");
|
|
294
351
|
setCodexAuthStarted(false);
|
|
295
352
|
setCodexAuthWaiting(false);
|
|
296
353
|
setCodexManualInput("");
|
|
@@ -307,7 +364,8 @@ export const Providers = () => {
|
|
|
307
364
|
target="_blank"
|
|
308
365
|
class="text-xs hover:underline"
|
|
309
366
|
style="color: var(--accent-link)"
|
|
310
|
-
|
|
367
|
+
>Get</a
|
|
368
|
+
>`
|
|
311
369
|
: null}
|
|
312
370
|
</div>
|
|
313
371
|
<${SecretInput}
|
|
@@ -336,48 +394,49 @@ export const Providers = () => {
|
|
|
336
394
|
<div class="flex gap-2">
|
|
337
395
|
<button
|
|
338
396
|
onclick=${startCodexAuth}
|
|
339
|
-
class="text-xs font-medium px-3 py-1.5 rounded-lg
|
|
397
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary"
|
|
340
398
|
>
|
|
341
399
|
Reconnect Codex
|
|
342
400
|
</button>
|
|
343
401
|
<button
|
|
344
402
|
onclick=${handleCodexDisconnect}
|
|
345
|
-
class="text-xs font-medium px-3 py-1.5 rounded-lg
|
|
403
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-ghost"
|
|
346
404
|
>
|
|
347
405
|
Disconnect
|
|
348
406
|
</button>
|
|
349
407
|
</div>
|
|
350
408
|
`
|
|
351
409
|
: !codexAuthStarted
|
|
352
|
-
|
|
353
|
-
<button
|
|
354
|
-
onclick=${startCodexAuth}
|
|
355
|
-
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
|
|
356
|
-
>
|
|
357
|
-
Connect Codex OAuth
|
|
358
|
-
</button>
|
|
359
|
-
`
|
|
360
|
-
: html`
|
|
361
|
-
<div class="flex items-center justify-between gap-2">
|
|
362
|
-
<p class="text-xs text-gray-500">
|
|
363
|
-
${codexAuthWaiting
|
|
364
|
-
? "Complete login in the popup, then paste the redirect URL."
|
|
365
|
-
: "Paste the redirect URL from your browser to finish connecting."}
|
|
366
|
-
</p>
|
|
410
|
+
? html`
|
|
367
411
|
<button
|
|
368
412
|
onclick=${startCodexAuth}
|
|
369
|
-
class="text-xs font-medium px-3 py-1.5 rounded-lg
|
|
413
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
|
|
370
414
|
>
|
|
371
|
-
|
|
415
|
+
Connect Codex OAuth
|
|
372
416
|
</button>
|
|
373
|
-
|
|
374
|
-
`
|
|
417
|
+
`
|
|
418
|
+
: html`
|
|
419
|
+
<div class="flex items-center justify-between gap-2">
|
|
420
|
+
<p class="text-xs text-gray-500">
|
|
421
|
+
${codexAuthWaiting
|
|
422
|
+
? "Complete login in the popup, then paste the redirect URL."
|
|
423
|
+
: "Paste the redirect URL from your browser to finish connecting."}
|
|
424
|
+
</p>
|
|
425
|
+
<button
|
|
426
|
+
onclick=${startCodexAuth}
|
|
427
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary shrink-0"
|
|
428
|
+
>
|
|
429
|
+
Restart
|
|
430
|
+
</button>
|
|
431
|
+
</div>
|
|
432
|
+
`}
|
|
375
433
|
${!codexStatus.connected && codexAuthStarted
|
|
376
434
|
? html`
|
|
377
435
|
<p class="text-xs text-gray-500">
|
|
378
436
|
After login, copy the full redirect URL (starts with
|
|
379
|
-
<code class="text-xs bg-black/30 px-1 rounded"
|
|
380
|
-
|
|
437
|
+
<code class="text-xs bg-black/30 px-1 rounded"
|
|
438
|
+
>http://localhost:1455/auth/callback</code
|
|
439
|
+
>) and paste it here.
|
|
381
440
|
</p>
|
|
382
441
|
<input
|
|
383
442
|
type="text"
|
|
@@ -386,13 +445,16 @@ export const Providers = () => {
|
|
|
386
445
|
placeholder="http://localhost:1455/auth/callback?code=...&state=..."
|
|
387
446
|
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 outline-none focus:border-gray-500"
|
|
388
447
|
/>
|
|
389
|
-
|
|
390
|
-
|
|
448
|
+
<${ActionButton}
|
|
449
|
+
onClick=${completeCodexAuth}
|
|
391
450
|
disabled=${!codexManualInput.trim() || codexExchanging}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
451
|
+
loading=${codexExchanging}
|
|
452
|
+
tone="primary"
|
|
453
|
+
size="sm"
|
|
454
|
+
idleLabel="Complete Codex OAuth"
|
|
455
|
+
loadingLabel="Completing..."
|
|
456
|
+
className="text-xs font-medium px-3 py-1.5"
|
|
457
|
+
/>
|
|
396
458
|
`
|
|
397
459
|
: null}
|
|
398
460
|
</div>
|
|
@@ -407,6 +469,7 @@ export const Providers = () => {
|
|
|
407
469
|
const fields = kProviderAuthFields[provider] || [];
|
|
408
470
|
const hasCodex = provider === "openai";
|
|
409
471
|
const hasKey = providerHasKey(provider);
|
|
472
|
+
const openAiFeatures = kProviderFeatures.openai || [];
|
|
410
473
|
return html`
|
|
411
474
|
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
412
475
|
<div class="flex items-center gap-2">
|
|
@@ -414,25 +477,43 @@ export const Providers = () => {
|
|
|
414
477
|
${kProviderLabels[provider] || provider}
|
|
415
478
|
</h3>
|
|
416
479
|
${hasKey
|
|
417
|
-
? html`<span
|
|
480
|
+
? html`<span
|
|
481
|
+
class="inline-block w-1.5 h-1.5 rounded-full bg-green-500"
|
|
482
|
+
/>`
|
|
418
483
|
: null}
|
|
419
484
|
</div>
|
|
420
485
|
${fields.map((field) => renderCredentialField(field))}
|
|
486
|
+
${provider === "openai"
|
|
487
|
+
? html`<${FeatureTags} features=${openAiFeatures} />`
|
|
488
|
+
: null}
|
|
421
489
|
${hasCodex ? renderCodexOAuth() : null}
|
|
422
|
-
|
|
490
|
+
${provider !== "openai"
|
|
491
|
+
? html`<${FeatureTags} provider=${provider} />`
|
|
492
|
+
: null}
|
|
423
493
|
</div>
|
|
424
494
|
`;
|
|
425
495
|
};
|
|
426
496
|
|
|
427
497
|
if (!ready) {
|
|
428
498
|
return html`
|
|
429
|
-
<div class="
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
499
|
+
<div class="space-y-4">
|
|
500
|
+
<${PageHeader}
|
|
501
|
+
title="Providers"
|
|
502
|
+
actions=${html`
|
|
503
|
+
<${ActionButton}
|
|
504
|
+
disabled=${true}
|
|
505
|
+
tone="primary"
|
|
506
|
+
size="sm"
|
|
507
|
+
idleLabel="Save changes"
|
|
508
|
+
className="transition-all"
|
|
509
|
+
/>
|
|
510
|
+
`}
|
|
511
|
+
/>
|
|
512
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
513
|
+
<div class="flex items-center gap-2 text-sm text-gray-400">
|
|
514
|
+
<${LoadingSpinner} className="h-4 w-4" />
|
|
515
|
+
Loading provider settings...
|
|
516
|
+
</div>
|
|
436
517
|
</div>
|
|
437
518
|
</div>
|
|
438
519
|
`;
|
|
@@ -449,6 +530,22 @@ export const Providers = () => {
|
|
|
449
530
|
|
|
450
531
|
return html`
|
|
451
532
|
<div class="space-y-4">
|
|
533
|
+
<${PageHeader}
|
|
534
|
+
title="Providers"
|
|
535
|
+
actions=${html`
|
|
536
|
+
<${ActionButton}
|
|
537
|
+
onClick=${saveChanges}
|
|
538
|
+
disabled=${!canSaveChanges}
|
|
539
|
+
loading=${savingChanges}
|
|
540
|
+
tone="primary"
|
|
541
|
+
size="sm"
|
|
542
|
+
idleLabel="Save changes"
|
|
543
|
+
loadingLabel="Saving..."
|
|
544
|
+
className="transition-all"
|
|
545
|
+
/>
|
|
546
|
+
`}
|
|
547
|
+
/>
|
|
548
|
+
|
|
452
549
|
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
453
550
|
<h2 class="font-semibold text-sm">Primary Agent Model</h2>
|
|
454
551
|
<select
|
|
@@ -457,17 +554,27 @@ export const Providers = () => {
|
|
|
457
554
|
const next = e.target.value;
|
|
458
555
|
setSelectedModel(next);
|
|
459
556
|
setModelDirty(next !== savedModel);
|
|
460
|
-
kProvidersTabCache = {
|
|
557
|
+
kProvidersTabCache = {
|
|
558
|
+
...(kProvidersTabCache || {}),
|
|
559
|
+
selectedModel: next,
|
|
560
|
+
};
|
|
461
561
|
}}
|
|
462
562
|
class="w-full bg-black/30 border border-border rounded-lg pl-3 pr-8 py-2 text-sm text-gray-200 outline-none focus:border-gray-500"
|
|
463
563
|
>
|
|
464
564
|
<option value="">Select a model</option>
|
|
465
565
|
${modelOptions.map(
|
|
466
|
-
(model) =>
|
|
566
|
+
(model) =>
|
|
567
|
+
html`<option value=${model.key}>
|
|
568
|
+
${model.label || model.key}
|
|
569
|
+
</option>`,
|
|
467
570
|
)}
|
|
468
571
|
</select>
|
|
469
572
|
<p class="text-xs text-gray-600">
|
|
470
|
-
${modelsLoading
|
|
573
|
+
${modelsLoading
|
|
574
|
+
? "Loading model catalog..."
|
|
575
|
+
: modelsError
|
|
576
|
+
? modelsError
|
|
577
|
+
: ""}
|
|
471
578
|
</p>
|
|
472
579
|
${canToggleFullCatalog
|
|
473
580
|
? html`
|
|
@@ -477,12 +584,17 @@ export const Providers = () => {
|
|
|
477
584
|
onclick=${() =>
|
|
478
585
|
setShowAllModels((prev) => {
|
|
479
586
|
const next = !prev;
|
|
480
|
-
kProvidersTabCache = {
|
|
587
|
+
kProvidersTabCache = {
|
|
588
|
+
...(kProvidersTabCache || {}),
|
|
589
|
+
showAllModels: next,
|
|
590
|
+
};
|
|
481
591
|
return next;
|
|
482
592
|
})}
|
|
483
593
|
class="text-xs text-gray-500 hover:text-gray-300"
|
|
484
594
|
>
|
|
485
|
-
${showAllModels
|
|
595
|
+
${showAllModels
|
|
596
|
+
? "Show recommended models"
|
|
597
|
+
: "Show full model catalog"}
|
|
486
598
|
</button>
|
|
487
599
|
</div>
|
|
488
600
|
`
|
|
@@ -495,67 +607,29 @@ export const Providers = () => {
|
|
|
495
607
|
${otherProviders
|
|
496
608
|
.filter((p) => kCoreProviders.has(p))
|
|
497
609
|
.map((provider) => renderProviderCard(provider))}
|
|
498
|
-
|
|
499
610
|
${showMoreProviders
|
|
500
611
|
? otherProviders
|
|
501
612
|
.filter((p) => !kCoreProviders.has(p))
|
|
502
613
|
.map((provider) => renderProviderCard(provider))
|
|
503
614
|
: null}
|
|
504
|
-
|
|
505
615
|
${otherProviders.some((p) => !kCoreProviders.has(p))
|
|
506
616
|
? html`
|
|
507
617
|
<button
|
|
508
618
|
type="button"
|
|
509
619
|
onclick=${() => setShowMoreProviders((prev) => !prev)}
|
|
510
|
-
class="w-full text-xs
|
|
620
|
+
class="w-full text-xs px-3 py-1.5 rounded-lg ac-btn-ghost"
|
|
511
621
|
>
|
|
512
|
-
${showMoreProviders
|
|
622
|
+
${showMoreProviders
|
|
623
|
+
? "Hide additional providers"
|
|
624
|
+
: "More providers"}
|
|
513
625
|
</button>
|
|
514
626
|
`
|
|
515
627
|
: null}
|
|
516
|
-
|
|
517
|
-
${restartRequired
|
|
518
|
-
? html`<div
|
|
519
|
-
class="bg-yellow-500/10 border border-yellow-500/30 rounded-xl p-4 flex items-center justify-between gap-3"
|
|
520
|
-
>
|
|
521
|
-
<p class="text-sm text-yellow-200">
|
|
522
|
-
Gateway restart required to apply changes.
|
|
523
|
-
</p>
|
|
524
|
-
<button
|
|
525
|
-
onclick=${async () => {
|
|
526
|
-
if (restartingGateway) return;
|
|
527
|
-
setRestartingGateway(true);
|
|
528
|
-
try {
|
|
529
|
-
await restartGateway();
|
|
530
|
-
setRestartRequired(false);
|
|
531
|
-
showToast("Gateway restarted", "success");
|
|
532
|
-
} catch (err) {
|
|
533
|
-
showToast("Restart failed: " + err.message, "error");
|
|
534
|
-
} finally {
|
|
535
|
-
setRestartingGateway(false);
|
|
536
|
-
}
|
|
537
|
-
}}
|
|
538
|
-
disabled=${restartingGateway}
|
|
539
|
-
class="text-xs px-2.5 py-1 rounded-lg border border-yellow-500/40 text-yellow-200 hover:border-yellow-400 hover:text-yellow-100 transition-colors shrink-0 ${restartingGateway
|
|
540
|
-
? "opacity-60 cursor-not-allowed"
|
|
541
|
-
: ""}"
|
|
542
|
-
>
|
|
543
|
-
${restartingGateway ? "Restarting..." : "Restart Gateway"}
|
|
544
|
-
</button>
|
|
545
|
-
</div>`
|
|
546
|
-
: null}
|
|
547
|
-
|
|
548
|
-
<button
|
|
549
|
-
onclick=${saveChanges}
|
|
550
|
-
disabled=${!canSaveChanges}
|
|
551
|
-
class="w-full text-sm font-medium px-4 py-2.5 rounded-xl transition-all ac-btn-cyan"
|
|
552
|
-
>
|
|
553
|
-
${savingChanges ? "Saving..." : "Save changes"}
|
|
554
|
-
</button>
|
|
555
628
|
${modelDirty && !hasSelectedProviderAuth
|
|
556
629
|
? html`
|
|
557
630
|
<p class="text-xs text-yellow-500">
|
|
558
|
-
Set credentials for the selected provider before saving this model
|
|
631
|
+
Set credentials for the selected provider before saving this model
|
|
632
|
+
change.
|
|
559
633
|
</p>
|
|
560
634
|
`
|
|
561
635
|
: null}
|
|
@@ -43,7 +43,7 @@ export function ScopePicker({ scopes, onToggle, apiStatus, loading }) {
|
|
|
43
43
|
const api = status[s.key];
|
|
44
44
|
let apiIndicator = null;
|
|
45
45
|
if (loading && !api && (readOn || writeOn)) {
|
|
46
|
-
apiIndicator = html`<span class="text-gray-500 text-xs flex items-center gap-1"><span class="inline-block w-3 h-3 border-2 border-gray-500 border-t-transparent rounded-full
|
|
46
|
+
apiIndicator = html`<span class="text-gray-500 text-xs flex items-center gap-1"><span class="inline-block w-3 h-3 border-2 border-gray-500 border-t-transparent rounded-full ac-spinner"></span></span>`;
|
|
47
47
|
} else if (api) {
|
|
48
48
|
if (api.status === 'ok') {
|
|
49
49
|
apiIndicator = html`<a href=${api.enableUrl || getApiEnableUrl(s.key)} target="_blank" class="text-green-500 hover:text-green-300 text-xs px-1.5 py-0.5 rounded bg-green-500/10">API ✓</a>`;
|