@chrysb/alphaclaw 0.5.1-beta.0 → 0.5.2
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/bin/alphaclaw.js +3 -1
- package/lib/public/css/theme.css +57 -0
- package/lib/public/js/app.js +63 -26
- package/lib/public/js/components/models-tab/index.js +203 -48
- package/lib/public/js/components/models-tab/provider-auth-card.js +20 -2
- package/lib/public/js/components/models.js +8 -9
- package/lib/public/js/components/onboarding/use-welcome-storage.js +2 -2
- package/lib/public/js/components/onboarding/welcome-config.js +1 -1
- package/lib/public/js/components/onboarding/welcome-form-step.js +2 -23
- package/lib/public/js/components/onboarding/welcome-header.js +19 -13
- package/lib/public/js/components/onboarding/welcome-import-step.js +6 -5
- package/lib/public/js/components/onboarding/welcome-pre-step.js +81 -0
- package/lib/public/js/components/onboarding/welcome-secret-review-step.js +12 -6
- package/lib/public/js/components/onboarding/welcome-secret-review-utils.js +19 -0
- package/lib/public/js/components/providers.js +9 -13
- package/lib/public/js/components/usage-tab/index.js +0 -43
- package/lib/public/js/components/usage-tab/use-usage-tab.js +0 -48
- package/lib/public/js/components/welcome/index.js +13 -2
- package/lib/public/js/components/welcome/use-welcome.js +19 -4
- package/lib/public/js/lib/api.js +0 -14
- package/lib/public/js/lib/model-config.js +149 -2
- package/lib/server/auth-profiles.js +14 -0
- package/lib/server/constants.js +23 -4
- package/lib/server/db/usage/index.js +0 -4
- package/lib/server/gateway.js +18 -2
- package/lib/server/onboarding/import/import-applier.js +127 -0
- package/lib/server/onboarding/import/import-scanner.js +8 -1
- package/lib/server/onboarding/import/secret-detector.js +52 -6
- package/lib/server/onboarding/index.js +126 -0
- package/lib/server/onboarding/openclaw.js +88 -5
- package/lib/server/onboarding/workspace.js +10 -0
- package/lib/server/routes/onboarding.js +12 -3
- package/lib/server/routes/proxy.js +7 -4
- package/lib/server/routes/system.js +14 -0
- package/lib/server/routes/usage.js +0 -80
- package/lib/server/webhook-middleware.js +5 -2
- package/lib/server.js +6 -11
- package/package.json +1 -1
- package/lib/server/db/usage/backfill.js +0 -416
package/bin/alphaclaw.js
CHANGED
|
@@ -481,6 +481,8 @@ if (!kSetupPassword) {
|
|
|
481
481
|
|
|
482
482
|
process.env.OPENCLAW_HOME = rootDir;
|
|
483
483
|
process.env.OPENCLAW_CONFIG_PATH = path.join(openclawDir, "openclaw.json");
|
|
484
|
+
process.env.GOG_KEYRING_PASSWORD =
|
|
485
|
+
process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
|
|
484
486
|
|
|
485
487
|
// ---------------------------------------------------------------------------
|
|
486
488
|
// 8. Install gog (Google Workspace CLI) if not present
|
|
@@ -516,7 +518,7 @@ if (!gogInstalled) {
|
|
|
516
518
|
}
|
|
517
519
|
|
|
518
520
|
// ---------------------------------------------------------------------------
|
|
519
|
-
//
|
|
521
|
+
// 9. Install/reconcile system cron entry
|
|
520
522
|
// ---------------------------------------------------------------------------
|
|
521
523
|
|
|
522
524
|
const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");
|
package/lib/public/css/theme.css
CHANGED
|
@@ -246,6 +246,45 @@ textarea:focus {
|
|
|
246
246
|
background: rgba(99, 235, 255, 0.08);
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
.ac-path-card {
|
|
250
|
+
border: 1px solid var(--panel-border-contrast);
|
|
251
|
+
background: rgba(0, 0, 0, 0.2);
|
|
252
|
+
transition:
|
|
253
|
+
border-color 0.15s ease,
|
|
254
|
+
background 0.15s ease,
|
|
255
|
+
box-shadow 0.15s ease,
|
|
256
|
+
transform 0.15s ease;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.ac-path-card:hover {
|
|
260
|
+
border-color: rgba(99, 235, 255, 0.5);
|
|
261
|
+
background: rgba(99, 235, 255, 0.04);
|
|
262
|
+
box-shadow:
|
|
263
|
+
inset 0 0 0 1px rgba(99, 235, 255, 0.1),
|
|
264
|
+
0 0 12px rgba(99, 235, 255, 0.08);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.ac-path-card:hover .ac-path-icon {
|
|
268
|
+
color: var(--accent);
|
|
269
|
+
border-color: rgba(99, 235, 255, 0.3);
|
|
270
|
+
background: rgba(99, 235, 255, 0.1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.ac-path-card:hover .ac-path-title {
|
|
274
|
+
color: #b9f8ff;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.ac-path-card:hover .ac-path-desc {
|
|
278
|
+
color: #94a3b8;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.ac-path-icon {
|
|
282
|
+
transition:
|
|
283
|
+
color 0.15s ease,
|
|
284
|
+
border-color 0.15s ease,
|
|
285
|
+
background 0.15s ease;
|
|
286
|
+
}
|
|
287
|
+
|
|
249
288
|
.ac-btn-secondary {
|
|
250
289
|
border: 1px solid var(--panel-border-contrast);
|
|
251
290
|
color: #d1d5db;
|
|
@@ -426,6 +465,24 @@ textarea:focus {
|
|
|
426
465
|
}
|
|
427
466
|
}
|
|
428
467
|
|
|
468
|
+
@keyframes acStepPillPulse {
|
|
469
|
+
0%,
|
|
470
|
+
100% {
|
|
471
|
+
box-shadow:
|
|
472
|
+
0 0 0 0 rgba(99, 235, 255, 0.14),
|
|
473
|
+
0 0 5px rgba(99, 235, 255, 0.18);
|
|
474
|
+
}
|
|
475
|
+
50% {
|
|
476
|
+
box-shadow:
|
|
477
|
+
0 0 0 3px rgba(99, 235, 255, 0.08),
|
|
478
|
+
0 0 9px rgba(99, 235, 255, 0.32);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.ac-step-pill-pulse {
|
|
483
|
+
animation: acStepPillPulse 2.6s ease-in-out infinite;
|
|
484
|
+
}
|
|
485
|
+
|
|
429
486
|
.ac-status-dot {
|
|
430
487
|
width: 8px;
|
|
431
488
|
height: 8px;
|
package/lib/public/js/app.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { h, render } from "https://esm.sh/preact";
|
|
2
2
|
import { useState, useEffect } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Router,
|
|
6
|
+
Route,
|
|
7
|
+
Switch,
|
|
8
|
+
useLocation,
|
|
9
|
+
} from "https://esm.sh/wouter-preact";
|
|
5
10
|
import { logout } from "./lib/api.js";
|
|
6
11
|
import { Welcome } from "./components/welcome/index.js";
|
|
7
12
|
import { ToastContainer } from "./components/toast.js";
|
|
@@ -14,7 +19,6 @@ import {
|
|
|
14
19
|
EnvarsRoute,
|
|
15
20
|
GeneralRoute,
|
|
16
21
|
ModelsRoute,
|
|
17
|
-
ProvidersRoute,
|
|
18
22
|
RouteRedirect,
|
|
19
23
|
TelegramRoute,
|
|
20
24
|
UsageRoute,
|
|
@@ -24,34 +28,48 @@ import {
|
|
|
24
28
|
import { useAppShellController } from "./hooks/use-app-shell-controller.js";
|
|
25
29
|
import { useAppShellUi } from "./hooks/use-app-shell-ui.js";
|
|
26
30
|
import { useBrowseNavigation } from "./hooks/use-browse-navigation.js";
|
|
27
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
getHashRouterPath,
|
|
33
|
+
useHashLocation,
|
|
34
|
+
} from "./hooks/use-hash-location.js";
|
|
28
35
|
import { readUiSettings, writeUiSettings } from "./lib/ui-settings.js";
|
|
29
36
|
|
|
30
37
|
const html = htm.bind(h);
|
|
31
|
-
const kDoctorWarningDismissedUntilUiSettingKey =
|
|
38
|
+
const kDoctorWarningDismissedUntilUiSettingKey =
|
|
39
|
+
"doctorWarningDismissedUntilMs";
|
|
32
40
|
const kOneWeekMs = 7 * 24 * 60 * 60 * 1000;
|
|
33
41
|
|
|
34
42
|
const App = () => {
|
|
35
43
|
const [location, setLocation] = useLocation();
|
|
36
|
-
const [doctorWarningDismissedUntilMs, setDoctorWarningDismissedUntilMs] =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
const [doctorWarningDismissedUntilMs, setDoctorWarningDismissedUntilMs] =
|
|
45
|
+
useState(() => {
|
|
46
|
+
const settings = readUiSettings();
|
|
47
|
+
return Number(settings[kDoctorWarningDismissedUntilUiSettingKey] || 0);
|
|
48
|
+
});
|
|
40
49
|
|
|
41
|
-
const { state: controllerState, actions: controllerActions } =
|
|
42
|
-
|
|
43
|
-
});
|
|
44
|
-
const { refs: shellRefs, state: shellState, actions: shellActions } = useAppShellUi();
|
|
45
|
-
const { state: browseState, actions: browseActions, constants: browseConstants } =
|
|
46
|
-
useBrowseNavigation({
|
|
50
|
+
const { state: controllerState, actions: controllerActions } =
|
|
51
|
+
useAppShellController({
|
|
47
52
|
location,
|
|
48
|
-
setLocation,
|
|
49
|
-
onCloseMobileSidebar: shellActions.closeMobileSidebar,
|
|
50
53
|
});
|
|
54
|
+
const {
|
|
55
|
+
refs: shellRefs,
|
|
56
|
+
state: shellState,
|
|
57
|
+
actions: shellActions,
|
|
58
|
+
} = useAppShellUi();
|
|
59
|
+
const {
|
|
60
|
+
state: browseState,
|
|
61
|
+
actions: browseActions,
|
|
62
|
+
constants: browseConstants,
|
|
63
|
+
} = useBrowseNavigation({
|
|
64
|
+
location,
|
|
65
|
+
setLocation,
|
|
66
|
+
onCloseMobileSidebar: shellActions.closeMobileSidebar,
|
|
67
|
+
});
|
|
51
68
|
|
|
52
69
|
useEffect(() => {
|
|
53
70
|
const settings = readUiSettings();
|
|
54
|
-
settings[kDoctorWarningDismissedUntilUiSettingKey] =
|
|
71
|
+
settings[kDoctorWarningDismissedUntilUiSettingKey] =
|
|
72
|
+
doctorWarningDismissedUntilMs;
|
|
55
73
|
writeUiSettings(settings);
|
|
56
74
|
}, [doctorWarningDismissedUntilMs]);
|
|
57
75
|
|
|
@@ -83,10 +101,13 @@ const App = () => {
|
|
|
83
101
|
if (!controllerState.onboarded) {
|
|
84
102
|
return html`
|
|
85
103
|
<div
|
|
86
|
-
class="min-h-screen flex
|
|
104
|
+
class="min-h-screen flex flex-col items-center pt-12 pb-8 px-4"
|
|
87
105
|
style="position: relative; z-index: 1"
|
|
88
106
|
>
|
|
89
|
-
<${Welcome}
|
|
107
|
+
<${Welcome}
|
|
108
|
+
onComplete=${controllerActions.handleOnboardingComplete}
|
|
109
|
+
acVersion=${controllerState.acVersion}
|
|
110
|
+
/>
|
|
90
111
|
</div>
|
|
91
112
|
<${ToastContainer} />
|
|
92
113
|
`;
|
|
@@ -150,7 +171,9 @@ const App = () => {
|
|
|
150
171
|
onNavigateToBrowseFile=${browseActions.navigateToBrowseFile}
|
|
151
172
|
onEditSelectedBrowseFile=${() =>
|
|
152
173
|
setLocation(
|
|
153
|
-
browseActions.buildBrowseRoute(browseState.selectedBrowsePath, {
|
|
174
|
+
browseActions.buildBrowseRoute(browseState.selectedBrowsePath, {
|
|
175
|
+
view: "edit",
|
|
176
|
+
}),
|
|
154
177
|
)}
|
|
155
178
|
onClearSelection=${() => {
|
|
156
179
|
browseActions.clearBrowsePreview();
|
|
@@ -163,14 +186,22 @@ const App = () => {
|
|
|
163
186
|
onscroll=${shellActions.handlePaneScroll}
|
|
164
187
|
style=${{ display: browseState.isBrowseRoute ? "none" : "block" }}
|
|
165
188
|
>
|
|
166
|
-
<div
|
|
189
|
+
<div
|
|
190
|
+
class=${`mobile-topbar ${shellState.mobileTopbarScrolled ? "is-scrolled" : ""}`}
|
|
191
|
+
>
|
|
167
192
|
<button
|
|
168
193
|
class="mobile-topbar-menu"
|
|
169
|
-
onclick=${() =>
|
|
194
|
+
onclick=${() =>
|
|
195
|
+
shellActions.setMobileSidebarOpen((open) => !open)}
|
|
170
196
|
aria-label="Open menu"
|
|
171
197
|
aria-expanded=${shellState.mobileSidebarOpen ? "true" : "false"}
|
|
172
198
|
>
|
|
173
|
-
<svg
|
|
199
|
+
<svg
|
|
200
|
+
width="18"
|
|
201
|
+
height="18"
|
|
202
|
+
viewBox="0 0 16 16"
|
|
203
|
+
fill="currentColor"
|
|
204
|
+
>
|
|
174
205
|
<path
|
|
175
206
|
d="M2 3.75a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 8zm0 4.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z"
|
|
176
207
|
/>
|
|
@@ -201,7 +232,9 @@ const App = () => {
|
|
|
201
232
|
onOpenclawUpdate=${controllerActions.handleOpenclawUpdate}
|
|
202
233
|
onRestartRequired=${controllerActions.setRestartRequired}
|
|
203
234
|
onDismissDoctorWarning=${() =>
|
|
204
|
-
setDoctorWarningDismissedUntilMs(
|
|
235
|
+
setDoctorWarningDismissedUntilMs(
|
|
236
|
+
Date.now() + kOneWeekMs,
|
|
237
|
+
)}
|
|
205
238
|
/>
|
|
206
239
|
</${Route}>
|
|
207
240
|
<${Route} path="/doctor">
|
|
@@ -232,7 +265,9 @@ const App = () => {
|
|
|
232
265
|
<${Route} path="/usage/:sessionId">
|
|
233
266
|
${(params) => html`
|
|
234
267
|
<${UsageRoute}
|
|
235
|
-
sessionId=${decodeURIComponent(
|
|
268
|
+
sessionId=${decodeURIComponent(
|
|
269
|
+
params.sessionId || "",
|
|
270
|
+
)}
|
|
236
271
|
onSetLocation=${setLocation}
|
|
237
272
|
/>
|
|
238
273
|
`}
|
|
@@ -280,7 +315,9 @@ const App = () => {
|
|
|
280
315
|
<div class="app-statusbar">
|
|
281
316
|
<div class="statusbar-left">
|
|
282
317
|
${controllerState.acVersion
|
|
283
|
-
? html`<span style="color: var(--text-muted)"
|
|
318
|
+
? html`<span style="color: var(--text-muted)"
|
|
319
|
+
>v${controllerState.acVersion}</span
|
|
320
|
+
>`
|
|
284
321
|
: null}
|
|
285
322
|
</div>
|
|
286
323
|
<div class="statusbar-right">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useState, useMemo } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { useState, useMemo, useRef, useEffect } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { PageHeader } from "../page-header.js";
|
|
5
5
|
import { LoadingSpinner } from "../loading-spinner.js";
|
|
@@ -7,14 +7,26 @@ import { ActionButton } from "../action-button.js";
|
|
|
7
7
|
import { Badge } from "../badge.js";
|
|
8
8
|
import { useModels } from "./use-models.js";
|
|
9
9
|
import { ProviderAuthCard } from "./provider-auth-card.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getModelProvider,
|
|
12
|
+
getAuthProviderFromModelProvider,
|
|
13
|
+
getFeaturedModels,
|
|
14
|
+
kProviderLabels,
|
|
15
|
+
kProviderOrder,
|
|
16
|
+
} from "../../lib/model-config.js";
|
|
11
17
|
|
|
12
18
|
const html = htm.bind(h);
|
|
13
19
|
|
|
20
|
+
const getModelsTabAuthProvider = (modelKey) => {
|
|
21
|
+
const provider = getModelProvider(modelKey);
|
|
22
|
+
if (provider === "openai-codex") return "openai-codex";
|
|
23
|
+
return getAuthProviderFromModelProvider(provider);
|
|
24
|
+
};
|
|
25
|
+
|
|
14
26
|
const deriveRequiredProviders = (configuredModels) => {
|
|
15
27
|
const providers = new Set();
|
|
16
28
|
for (const modelKey of Object.keys(configuredModels)) {
|
|
17
|
-
const provider =
|
|
29
|
+
const provider = getModelsTabAuthProvider(modelKey);
|
|
18
30
|
if (provider) providers.add(provider);
|
|
19
31
|
}
|
|
20
32
|
return [...providers];
|
|
@@ -24,9 +36,172 @@ const kProviderDisplayOrder = [
|
|
|
24
36
|
"anthropic",
|
|
25
37
|
"openai",
|
|
26
38
|
"openai-codex",
|
|
27
|
-
"
|
|
39
|
+
...kProviderOrder.filter((provider) => !["anthropic", "openai"].includes(provider)),
|
|
28
40
|
];
|
|
29
41
|
|
|
42
|
+
const getModelCatalogProvider = (model) =>
|
|
43
|
+
String(model?.provider || getModelProvider(model?.key)).trim();
|
|
44
|
+
|
|
45
|
+
const getProviderSortIndex = (provider) => {
|
|
46
|
+
const index = kProviderDisplayOrder.indexOf(provider);
|
|
47
|
+
return index >= 0 ? index : Number.MAX_SAFE_INTEGER;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const formatProviderSectionLabel = (provider) =>
|
|
51
|
+
String(kProviderLabels[provider] || provider).toUpperCase();
|
|
52
|
+
|
|
53
|
+
const normalizeSearch = (value) => String(value || "").trim().toLowerCase();
|
|
54
|
+
const buildModelSearchText = (model) =>
|
|
55
|
+
[
|
|
56
|
+
model?.label || "",
|
|
57
|
+
model?.key || "",
|
|
58
|
+
model?.provider || getModelProvider(model?.key),
|
|
59
|
+
]
|
|
60
|
+
.join(" ")
|
|
61
|
+
.toLowerCase();
|
|
62
|
+
|
|
63
|
+
const SearchableModelPicker = ({
|
|
64
|
+
options = [],
|
|
65
|
+
popularModels = [],
|
|
66
|
+
placeholder = "Add model...",
|
|
67
|
+
onSelect = () => {},
|
|
68
|
+
}) => {
|
|
69
|
+
const [query, setQuery] = useState("");
|
|
70
|
+
const [open, setOpen] = useState(false);
|
|
71
|
+
const rootRef = useRef(null);
|
|
72
|
+
const normalizedQuery = normalizeSearch(query);
|
|
73
|
+
const filteredOptions = useMemo(
|
|
74
|
+
() =>
|
|
75
|
+
normalizedQuery
|
|
76
|
+
? options.filter((option) =>
|
|
77
|
+
buildModelSearchText(option).includes(normalizedQuery),
|
|
78
|
+
)
|
|
79
|
+
: options,
|
|
80
|
+
[options, normalizedQuery],
|
|
81
|
+
);
|
|
82
|
+
const groupedOptions = useMemo(() => {
|
|
83
|
+
const groups = [];
|
|
84
|
+
const showPopularGroup = !normalizedQuery;
|
|
85
|
+
const visibleOptionKeys = new Set(filteredOptions.map((option) => option.key));
|
|
86
|
+
const visiblePopularModels = popularModels.filter((model) =>
|
|
87
|
+
visibleOptionKeys.has(model.key),
|
|
88
|
+
);
|
|
89
|
+
if (showPopularGroup && visiblePopularModels.length > 0) {
|
|
90
|
+
groups.push({
|
|
91
|
+
provider: "popular",
|
|
92
|
+
label: "POPULAR",
|
|
93
|
+
options: visiblePopularModels,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
for (const option of filteredOptions) {
|
|
97
|
+
const provider = getModelCatalogProvider(option);
|
|
98
|
+
const label = formatProviderSectionLabel(provider);
|
|
99
|
+
const currentGroup = groups[groups.length - 1];
|
|
100
|
+
if (!currentGroup || currentGroup.provider !== provider) {
|
|
101
|
+
groups.push({ provider, label, options: [option] });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
currentGroup.options.push(option);
|
|
105
|
+
}
|
|
106
|
+
return groups;
|
|
107
|
+
}, [filteredOptions, popularModels, normalizedQuery]);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const handleOutsidePointer = (event) => {
|
|
111
|
+
if (!rootRef.current?.contains(event.target)) {
|
|
112
|
+
setOpen(false);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
document.addEventListener("mousedown", handleOutsidePointer);
|
|
116
|
+
return () => document.removeEventListener("mousedown", handleOutsidePointer);
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
const handleSelect = (modelKey) => {
|
|
120
|
+
if (!modelKey) return;
|
|
121
|
+
onSelect(modelKey);
|
|
122
|
+
setQuery("");
|
|
123
|
+
setOpen(false);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleKeyDown = (event) => {
|
|
127
|
+
const firstVisibleOption = groupedOptions[0]?.options?.[0];
|
|
128
|
+
if (event.key === "Escape") {
|
|
129
|
+
setOpen(false);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (event.key === "Enter" && firstVisibleOption?.key) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
handleSelect(firstVisibleOption.key);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return html`
|
|
139
|
+
<div class="relative" ref=${rootRef}>
|
|
140
|
+
<input
|
|
141
|
+
type="text"
|
|
142
|
+
value=${query}
|
|
143
|
+
placeholder=${placeholder}
|
|
144
|
+
onFocus=${() => setOpen(true)}
|
|
145
|
+
onInput=${(event) => {
|
|
146
|
+
setQuery(event.target.value);
|
|
147
|
+
setOpen(true);
|
|
148
|
+
}}
|
|
149
|
+
onKeyDown=${handleKeyDown}
|
|
150
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500"
|
|
151
|
+
/>
|
|
152
|
+
${open
|
|
153
|
+
? html`
|
|
154
|
+
<div
|
|
155
|
+
class="absolute left-0 right-0 top-full mt-2 z-20 bg-modal border border-border rounded-xl shadow-2xl overflow-hidden"
|
|
156
|
+
>
|
|
157
|
+
<div class="max-h-80 overflow-y-auto">
|
|
158
|
+
${filteredOptions.length > 0
|
|
159
|
+
? groupedOptions.map(
|
|
160
|
+
(group, index) => html`
|
|
161
|
+
<div key=${group.provider}>
|
|
162
|
+
<div
|
|
163
|
+
class=${`sticky top-0 z-10 h-[22px] px-3 text-[12px] font-semibold tracking-wide text-gray-400 bg-[#151922] border-b border-border flex items-center ${
|
|
164
|
+
index > 0 ? "border-t border-border" : ""
|
|
165
|
+
}`}
|
|
166
|
+
>
|
|
167
|
+
${group.label}
|
|
168
|
+
</div>
|
|
169
|
+
${group.options.map(
|
|
170
|
+
(model) => html`
|
|
171
|
+
<button
|
|
172
|
+
key=${model.key}
|
|
173
|
+
type="button"
|
|
174
|
+
onMouseDown=${(event) => event.preventDefault()}
|
|
175
|
+
onClick=${() => handleSelect(model.key)}
|
|
176
|
+
class="w-full text-left px-3 py-2 hover:bg-white/5 border-b border-border last:border-b-0"
|
|
177
|
+
>
|
|
178
|
+
<div class="flex flex-col gap-1">
|
|
179
|
+
<div class="text-sm text-gray-200">
|
|
180
|
+
${model.label || model.key}
|
|
181
|
+
</div>
|
|
182
|
+
<div class="text-xs text-gray-500 font-mono">
|
|
183
|
+
${model.key}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</button>
|
|
187
|
+
`,
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
`,
|
|
191
|
+
)
|
|
192
|
+
: html`
|
|
193
|
+
<div class="px-3 py-3 text-xs text-gray-500">
|
|
194
|
+
No models match that search.
|
|
195
|
+
</div>
|
|
196
|
+
`}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
`
|
|
200
|
+
: null}
|
|
201
|
+
</div>
|
|
202
|
+
`;
|
|
203
|
+
};
|
|
204
|
+
|
|
30
205
|
export const Models = ({ onRestartRequired = () => {} }) => {
|
|
31
206
|
const {
|
|
32
207
|
catalog,
|
|
@@ -52,26 +227,28 @@ export const Models = ({ onRestartRequired = () => {} }) => {
|
|
|
52
227
|
refreshCodexStatus,
|
|
53
228
|
} = useModels();
|
|
54
229
|
|
|
55
|
-
const [showAllModels, setShowAllModels] = useState(false);
|
|
56
|
-
|
|
57
230
|
const configuredKeys = useMemo(
|
|
58
231
|
() => new Set(Object.keys(configuredModels)),
|
|
59
232
|
[configuredModels],
|
|
60
233
|
);
|
|
61
234
|
|
|
62
235
|
const featuredModels = useMemo(() => getFeaturedModels(catalog), [catalog]);
|
|
236
|
+
const popularPickerModels = useMemo(
|
|
237
|
+
() => featuredModels.filter((model) => !configuredKeys.has(model.key)),
|
|
238
|
+
[featuredModels, configuredKeys],
|
|
239
|
+
);
|
|
63
240
|
|
|
64
241
|
const pickerModels = useMemo(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
242
|
+
return [...catalog]
|
|
243
|
+
.filter((model) => !configuredKeys.has(model.key))
|
|
244
|
+
.sort((a, b) => {
|
|
245
|
+
const providerCompare =
|
|
246
|
+
getProviderSortIndex(getModelCatalogProvider(a)) -
|
|
247
|
+
getProviderSortIndex(getModelCatalogProvider(b));
|
|
248
|
+
if (providerCompare !== 0) return providerCompare;
|
|
249
|
+
return String(a.label || a.key).localeCompare(String(b.label || b.key));
|
|
250
|
+
});
|
|
251
|
+
}, [catalog, configuredKeys]);
|
|
75
252
|
|
|
76
253
|
const requiredProviders = useMemo(
|
|
77
254
|
() => deriveRequiredProviders(configuredModels),
|
|
@@ -106,7 +283,7 @@ export const Models = ({ onRestartRequired = () => {} }) => {
|
|
|
106
283
|
() =>
|
|
107
284
|
Object.keys(configuredModels).map((key) => {
|
|
108
285
|
const catalogEntry = catalog.find((m) => m.key === key);
|
|
109
|
-
const provider =
|
|
286
|
+
const provider = getModelsTabAuthProvider(key);
|
|
110
287
|
const hasAuth = !!providerHasAuth[provider];
|
|
111
288
|
return {
|
|
112
289
|
key,
|
|
@@ -213,38 +390,16 @@ export const Models = ({ onRestartRequired = () => {} }) => {
|
|
|
213
390
|
</div>
|
|
214
391
|
`}
|
|
215
392
|
|
|
216
|
-
<div class="
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
e.target.value = "";
|
|
393
|
+
<div class="space-y-2">
|
|
394
|
+
<${SearchableModelPicker}
|
|
395
|
+
options=${pickerModels}
|
|
396
|
+
popularModels=${popularPickerModels}
|
|
397
|
+
placeholder="Add model..."
|
|
398
|
+
onSelect=${(modelKey) => {
|
|
399
|
+
addModel(modelKey);
|
|
400
|
+
if (!primary) setPrimaryModel(modelKey);
|
|
226
401
|
}}
|
|
227
|
-
|
|
228
|
-
>
|
|
229
|
-
<option value="">Select a model to add...</option>
|
|
230
|
-
${pickerModels.map(
|
|
231
|
-
(m) =>
|
|
232
|
-
html`<option value=${m.key}>${m.label || m.key}</option>`,
|
|
233
|
-
)}
|
|
234
|
-
</select>
|
|
235
|
-
${canToggleFullCatalog
|
|
236
|
-
? html`
|
|
237
|
-
<button
|
|
238
|
-
type="button"
|
|
239
|
-
onclick=${() => setShowAllModels((prev) => !prev)}
|
|
240
|
-
class="text-xs text-gray-500 hover:text-gray-300"
|
|
241
|
-
>
|
|
242
|
-
${showAllModels
|
|
243
|
-
? "Show recommended models"
|
|
244
|
-
: "Show full model catalog"}
|
|
245
|
-
</button>
|
|
246
|
-
`
|
|
247
|
-
: null}
|
|
402
|
+
/>
|
|
248
403
|
</div>
|
|
249
404
|
|
|
250
405
|
${loading
|
|
@@ -6,6 +6,10 @@ import { SecretInput } from "../secret-input.js";
|
|
|
6
6
|
import { ActionButton } from "../action-button.js";
|
|
7
7
|
import { exchangeCodexOAuth, disconnectCodex } from "../../lib/api.js";
|
|
8
8
|
import { showToast } from "../toast.js";
|
|
9
|
+
import {
|
|
10
|
+
kProviderAuthFields,
|
|
11
|
+
kProviderLabels,
|
|
12
|
+
} from "../../lib/model-config.js";
|
|
9
13
|
|
|
10
14
|
const html = htm.bind(h);
|
|
11
15
|
|
|
@@ -71,10 +75,24 @@ const kDefaultMode = {
|
|
|
71
75
|
field: "key",
|
|
72
76
|
};
|
|
73
77
|
|
|
78
|
+
const buildDefaultProviderModes = (provider) => {
|
|
79
|
+
const fields = kProviderAuthFields[provider] || [];
|
|
80
|
+
if (fields.length === 0) return [kDefaultMode];
|
|
81
|
+
return fields.map((fieldDef) => ({
|
|
82
|
+
id: "api_key",
|
|
83
|
+
label: fieldDef.label || "API Key",
|
|
84
|
+
profileSuffix: "default",
|
|
85
|
+
placeholder: fieldDef.placeholder || "...",
|
|
86
|
+
hint: fieldDef.hint,
|
|
87
|
+
url: fieldDef.url,
|
|
88
|
+
field: "key",
|
|
89
|
+
}));
|
|
90
|
+
};
|
|
91
|
+
|
|
74
92
|
const getProviderMeta = (provider) =>
|
|
75
93
|
kProviderMeta[provider] || {
|
|
76
|
-
label: provider,
|
|
77
|
-
modes:
|
|
94
|
+
label: kProviderLabels[provider] || provider,
|
|
95
|
+
modes: buildDefaultProviderModes(provider),
|
|
78
96
|
};
|
|
79
97
|
|
|
80
98
|
const resolveProfileId = (mode, provider) => {
|
|
@@ -137,7 +137,10 @@ export const Models = () => {
|
|
|
137
137
|
|
|
138
138
|
const setEnvValue = (key, value) => {
|
|
139
139
|
setEnvVars((prev) => {
|
|
140
|
-
const
|
|
140
|
+
const existing = prev.some((entry) => entry.key === key);
|
|
141
|
+
const next = existing
|
|
142
|
+
? prev.map((v) => (v.key === key ? { ...v, value } : v))
|
|
143
|
+
: [...prev, { key, value, editable: true }];
|
|
141
144
|
kModelsTabCache = { ...(kModelsTabCache || {}), envVars: next };
|
|
142
145
|
return next;
|
|
143
146
|
});
|
|
@@ -263,15 +266,11 @@ export const Models = () => {
|
|
|
263
266
|
(key) => getKeyVal(envVars, key) !== (savedAiValues[key] || ""),
|
|
264
267
|
);
|
|
265
268
|
const hasSelectedProviderAuth =
|
|
266
|
-
selectedModelProvider === "
|
|
267
|
-
? !!(getKeyVal(envVars, "ANTHROPIC_API_KEY") || getKeyVal(envVars, "ANTHROPIC_TOKEN"))
|
|
268
|
-
: selectedModelProvider === "openai"
|
|
269
|
-
? !!getKeyVal(envVars, "OPENAI_API_KEY")
|
|
270
|
-
: selectedModelProvider === "openai-codex"
|
|
269
|
+
selectedModelProvider === "openai-codex"
|
|
271
270
|
? !!codexStatus.connected
|
|
272
|
-
:
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
: (kProviderAuthFields[selectedAuthProvider] || []).some((field) =>
|
|
272
|
+
Boolean(getKeyVal(envVars, field.key)),
|
|
273
|
+
);
|
|
275
274
|
const canSaveChanges = !savingChanges && (aiCredentialsDirty || (modelDirty && hasSelectedProviderAuth));
|
|
276
275
|
|
|
277
276
|
const renderCredentialField = (field) => html`
|
|
@@ -30,8 +30,8 @@ export const useWelcomeStorage = ({
|
|
|
30
30
|
String(initialSetupState?.[kOnboardingStepKey] || ""),
|
|
31
31
|
10,
|
|
32
32
|
);
|
|
33
|
-
if (!Number.isFinite(parsedStep)) return
|
|
34
|
-
const clampedStep = Math.max(
|
|
33
|
+
if (!Number.isFinite(parsedStep)) return -1;
|
|
34
|
+
const clampedStep = Math.max(-1, Math.min(kPairingStepIndex, parsedStep));
|
|
35
35
|
if (clampedStep === kSetupStepIndex && shouldRecoverFromSetupState) return 0;
|
|
36
36
|
return clampedStep;
|
|
37
37
|
});
|