@nastechai/agent 0.16.0 → 0.17.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/eslint.config.js +23 -0
- package/index.html +24 -0
- package/package.json +54 -26
- package/package.json.bak +89 -0
- package/package.json.pub +88 -0
- package/src/App.tsx +1173 -0
- package/src/components/AuthWidget.tsx +150 -0
- package/src/components/AutoField.tsx +206 -0
- package/src/components/Backdrop.tsx +93 -0
- package/src/components/ChatSidebar.tsx +394 -0
- package/src/components/DeleteConfirmDialog.tsx +40 -0
- package/src/components/LanguageSwitcher.tsx +186 -0
- package/src/components/Markdown.tsx +383 -0
- package/src/components/ModelInfoCard.tsx +112 -0
- package/src/components/ModelPickerDialog.tsx +470 -0
- package/src/components/OAuthLoginModal.tsx +374 -0
- package/src/components/OAuthProvidersCard.tsx +287 -0
- package/src/components/PlatformsCard.tsx +97 -0
- package/src/components/ScheduleBuilder.tsx +273 -0
- package/src/components/SidebarFooter.tsx +42 -0
- package/src/components/SidebarStatusStrip.tsx +72 -0
- package/src/components/SlashPopover.tsx +171 -0
- package/src/components/ThemeSwitcher.tsx +243 -0
- package/src/components/ToolCall.tsx +228 -0
- package/src/components/ToolsetConfigDrawer.tsx +448 -0
- package/src/contexts/PageHeaderProvider.tsx +139 -0
- package/src/contexts/SystemActions.tsx +120 -0
- package/src/contexts/page-header-context.ts +12 -0
- package/src/contexts/system-actions-context.ts +18 -0
- package/src/contexts/usePageHeader.ts +10 -0
- package/src/contexts/useSystemActions.ts +15 -0
- package/src/hooks/useModalBehavior.ts +44 -0
- package/src/hooks/useSidebarStatus.ts +27 -0
- package/src/i18n/af.ts +702 -0
- package/src/i18n/context.tsx +123 -0
- package/src/i18n/de.ts +701 -0
- package/src/i18n/en.ts +708 -0
- package/src/i18n/es.ts +701 -0
- package/src/i18n/fr.ts +701 -0
- package/src/i18n/ga.ts +702 -0
- package/src/i18n/hu.ts +702 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/it.ts +701 -0
- package/src/i18n/ja.ts +702 -0
- package/src/i18n/ko.ts +702 -0
- package/src/i18n/pt.ts +702 -0
- package/src/i18n/ru.ts +702 -0
- package/src/i18n/tr.ts +702 -0
- package/src/i18n/types.ts +710 -0
- package/src/i18n/uk.ts +702 -0
- package/src/i18n/zh-hant.ts +702 -0
- package/src/i18n/zh.ts +698 -0
- package/src/index.css +274 -0
- package/src/lib/api.ts +1585 -0
- package/src/lib/dashboard-flags.ts +15 -0
- package/src/lib/format.ts +9 -0
- package/src/lib/fuzzy.ts +192 -0
- package/src/lib/gatewayClient.ts +253 -0
- package/src/lib/nested.ts +23 -0
- package/src/lib/resolve-page-title.ts +41 -0
- package/src/lib/schedule.ts +382 -0
- package/src/lib/slashExec.ts +163 -0
- package/src/lib/utils.ts +35 -0
- package/src/main.tsx +25 -0
- package/src/pages/AnalyticsPage.tsx +601 -0
- package/src/pages/ChannelsPage.tsx +772 -0
- package/src/pages/ChatPage.tsx +889 -0
- package/src/pages/ConfigPage.tsx +660 -0
- package/src/pages/CronPage.tsx +524 -0
- package/src/pages/DocsPage.tsx +69 -0
- package/src/pages/EnvPage.tsx +918 -0
- package/src/pages/LogsPage.tsx +246 -0
- package/src/pages/McpPage.tsx +757 -0
- package/src/pages/ModelsPage.tsx +994 -0
- package/src/pages/PairingPage.tsx +276 -0
- package/src/pages/PluginsPage.tsx +580 -0
- package/src/pages/ProfilesPage.tsx +559 -0
- package/src/pages/SessionsPage.tsx +936 -0
- package/src/pages/SkillsPage.tsx +557 -0
- package/src/pages/SystemPage.tsx +1259 -0
- package/src/pages/WebhooksPage.tsx +483 -0
- package/src/plugins/PluginPage.tsx +64 -0
- package/src/plugins/index.ts +6 -0
- package/src/plugins/registry.ts +151 -0
- package/src/plugins/sdk.d.ts +160 -0
- package/src/plugins/slots.ts +199 -0
- package/src/plugins/types.ts +37 -0
- package/src/plugins/usePlugins.ts +133 -0
- package/src/themes/context.tsx +443 -0
- package/src/themes/fonts.ts +160 -0
- package/src/themes/index.ts +3 -0
- package/src/themes/presets.ts +477 -0
- package/src/themes/types.ts +187 -0
- package/tsconfig.app.json +34 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +124 -0
- package/vite.config.ts.timestamp-1780999102396-af6b77b30ebd8.mjs +105 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { Button } from "@nastechai/ui/ui/components/button";
|
|
2
|
+
import { Checkbox } from "@nastechai/ui/ui/components/checkbox";
|
|
3
|
+
import { ListItem } from "@nastechai/ui/ui/components/list-item";
|
|
4
|
+
import { Spinner } from "@nastechai/ui/ui/components/spinner";
|
|
5
|
+
import { Input } from "@nastechai/ui/ui/components/input";
|
|
6
|
+
import { Label } from "@nastechai/ui/ui/components/label";
|
|
7
|
+
import type { GatewayClient } from "@/lib/gatewayClient";
|
|
8
|
+
import { Check, Search, X } from "lucide-react";
|
|
9
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
10
|
+
import { createPortal } from "react-dom";
|
|
11
|
+
import { cn, themedBody } from "@/lib/utils";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Two-stage model picker modal.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors ui-tui/src/components/modelPicker.tsx:
|
|
17
|
+
* Stage 1: pick provider (authenticated providers only)
|
|
18
|
+
* Stage 2: pick model within that provider
|
|
19
|
+
*
|
|
20
|
+
* Two invocation modes:
|
|
21
|
+
*
|
|
22
|
+
* 1. Chat-session mode (ChatSidebar) — pass `gw` + `sessionId`. The picker
|
|
23
|
+
* loads options via `model.options` JSON-RPC and emits the result as a
|
|
24
|
+
* slash command string (`/model <model> --provider <slug> [--global]`)
|
|
25
|
+
* through `onSubmit`, which the ChatPage pipes to `slashExec`.
|
|
26
|
+
*
|
|
27
|
+
* 2. Standalone mode (ModelsPage, Config settings) — pass a `loader` and
|
|
28
|
+
* `onApply`. The picker fetches options via the REST endpoint and calls
|
|
29
|
+
* `onApply(provider, model, persistGlobal)` instead of emitting a slash
|
|
30
|
+
* command. This lets the Models page reuse the same UI without
|
|
31
|
+
* requiring an open chat PTY.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
interface ModelOptionProvider {
|
|
35
|
+
name: string;
|
|
36
|
+
slug: string;
|
|
37
|
+
models?: string[];
|
|
38
|
+
total_models?: number;
|
|
39
|
+
is_current?: boolean;
|
|
40
|
+
warning?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ModelOptionsResponse {
|
|
44
|
+
model?: string;
|
|
45
|
+
provider?: string;
|
|
46
|
+
providers?: ModelOptionProvider[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Props {
|
|
50
|
+
/** Chat-mode: when present, picker emits a slash command via onSubmit. */
|
|
51
|
+
gw?: GatewayClient;
|
|
52
|
+
sessionId?: string;
|
|
53
|
+
onSubmit?(slashCommand: string): void;
|
|
54
|
+
|
|
55
|
+
/** Standalone-mode: when present (and onSubmit absent), picker calls onApply. */
|
|
56
|
+
loader?(): Promise<ModelOptionsResponse>;
|
|
57
|
+
onApply?(args: {
|
|
58
|
+
provider: string;
|
|
59
|
+
model: string;
|
|
60
|
+
persistGlobal: boolean;
|
|
61
|
+
}): Promise<void> | void;
|
|
62
|
+
|
|
63
|
+
onClose(): void;
|
|
64
|
+
title?: string;
|
|
65
|
+
/** If true, hides "Persist globally" checkbox — always saves to config.yaml. */
|
|
66
|
+
alwaysGlobal?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ModelPickerDialog(props: Props) {
|
|
70
|
+
const {
|
|
71
|
+
gw,
|
|
72
|
+
sessionId,
|
|
73
|
+
onSubmit,
|
|
74
|
+
loader,
|
|
75
|
+
onApply,
|
|
76
|
+
onClose,
|
|
77
|
+
title = "Switch Model",
|
|
78
|
+
alwaysGlobal = false,
|
|
79
|
+
} = props;
|
|
80
|
+
const standalone = !!loader && !!onApply;
|
|
81
|
+
|
|
82
|
+
const [providers, setProviders] = useState<ModelOptionProvider[]>([]);
|
|
83
|
+
const [currentModel, setCurrentModel] = useState("");
|
|
84
|
+
const [currentProviderSlug, setCurrentProviderSlug] = useState("");
|
|
85
|
+
const [loading, setLoading] = useState(true);
|
|
86
|
+
const [error, setError] = useState<string | null>(null);
|
|
87
|
+
const [selectedSlug, setSelectedSlug] = useState("");
|
|
88
|
+
const [selectedModel, setSelectedModel] = useState("");
|
|
89
|
+
const [query, setQuery] = useState("");
|
|
90
|
+
const [persistGlobal, setPersistGlobal] = useState(alwaysGlobal);
|
|
91
|
+
const [applying, setApplying] = useState(false);
|
|
92
|
+
const closedRef = useRef(false);
|
|
93
|
+
|
|
94
|
+
// Load providers + models on open.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
closedRef.current = false;
|
|
97
|
+
|
|
98
|
+
const promise = standalone
|
|
99
|
+
? (loader as () => Promise<ModelOptionsResponse>)()
|
|
100
|
+
: (gw as GatewayClient).request<ModelOptionsResponse>(
|
|
101
|
+
"model.options",
|
|
102
|
+
sessionId ? { session_id: sessionId } : {},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
promise
|
|
106
|
+
.then((r) => {
|
|
107
|
+
if (closedRef.current) return;
|
|
108
|
+
const next = r?.providers ?? [];
|
|
109
|
+
setProviders(next);
|
|
110
|
+
setCurrentModel(String(r?.model ?? ""));
|
|
111
|
+
setCurrentProviderSlug(String(r?.provider ?? ""));
|
|
112
|
+
setSelectedSlug(
|
|
113
|
+
(next.find((p) => p.is_current) ?? next[0])?.slug ?? "",
|
|
114
|
+
);
|
|
115
|
+
setSelectedModel("");
|
|
116
|
+
setLoading(false);
|
|
117
|
+
})
|
|
118
|
+
.catch((e) => {
|
|
119
|
+
if (closedRef.current) return;
|
|
120
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
121
|
+
setLoading(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return () => {
|
|
125
|
+
closedRef.current = true;
|
|
126
|
+
};
|
|
127
|
+
// Deliberately omit props from deps — stable for the dialog's lifetime.
|
|
128
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
// Esc closes.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const onKey = (e: KeyboardEvent) => {
|
|
134
|
+
if (e.key === "Escape") {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
onClose();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
window.addEventListener("keydown", onKey);
|
|
140
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
141
|
+
}, [onClose]);
|
|
142
|
+
|
|
143
|
+
const selectedProvider = useMemo(
|
|
144
|
+
() => providers.find((p) => p.slug === selectedSlug) ?? null,
|
|
145
|
+
[providers, selectedSlug],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const models = useMemo(
|
|
149
|
+
() => selectedProvider?.models ?? [],
|
|
150
|
+
[selectedProvider],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const needle = query.trim().toLowerCase();
|
|
154
|
+
|
|
155
|
+
const filteredProviders = useMemo(
|
|
156
|
+
() =>
|
|
157
|
+
!needle
|
|
158
|
+
? providers
|
|
159
|
+
: providers.filter(
|
|
160
|
+
(p) =>
|
|
161
|
+
p.name.toLowerCase().includes(needle) ||
|
|
162
|
+
p.slug.toLowerCase().includes(needle) ||
|
|
163
|
+
(p.models ?? []).some((m) => m.toLowerCase().includes(needle)),
|
|
164
|
+
),
|
|
165
|
+
[providers, needle],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const filteredModels = useMemo(
|
|
169
|
+
() =>
|
|
170
|
+
!needle ? models : models.filter((m) => m.toLowerCase().includes(needle)),
|
|
171
|
+
[models, needle],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const canConfirm = !!selectedProvider && !!selectedModel && !applying;
|
|
175
|
+
|
|
176
|
+
const confirm = async () => {
|
|
177
|
+
if (!canConfirm || !selectedProvider) return;
|
|
178
|
+
if (standalone && onApply) {
|
|
179
|
+
setApplying(true);
|
|
180
|
+
try {
|
|
181
|
+
await onApply({
|
|
182
|
+
provider: selectedProvider.slug,
|
|
183
|
+
model: selectedModel,
|
|
184
|
+
persistGlobal,
|
|
185
|
+
});
|
|
186
|
+
onClose();
|
|
187
|
+
} catch (e) {
|
|
188
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
189
|
+
} finally {
|
|
190
|
+
setApplying(false);
|
|
191
|
+
}
|
|
192
|
+
} else if (onSubmit) {
|
|
193
|
+
const global = persistGlobal ? " --global" : "";
|
|
194
|
+
onSubmit(
|
|
195
|
+
`/model ${selectedModel} --provider ${selectedProvider.slug}${global}`,
|
|
196
|
+
);
|
|
197
|
+
onClose();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Portal to document.body: the main dashboard column in App.tsx is
|
|
202
|
+
// `relative z-2`, which creates a stacking context that traps fixed
|
|
203
|
+
// descendants below the app sidebar (z-50). Without the portal this
|
|
204
|
+
// modal's z-[100] is scoped to z-2 and the sidebar covers its left
|
|
205
|
+
// edge — visible especially in the Large theme variants where the
|
|
206
|
+
// larger root font widens the dialog into the sidebar's column. See
|
|
207
|
+
// Toast.tsx for the same pattern.
|
|
208
|
+
return createPortal(
|
|
209
|
+
<div
|
|
210
|
+
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
|
|
211
|
+
onClick={(e) => e.target === e.currentTarget && onClose()}
|
|
212
|
+
role="dialog"
|
|
213
|
+
aria-modal="true"
|
|
214
|
+
aria-labelledby="model-picker-title"
|
|
215
|
+
>
|
|
216
|
+
<div className={cn(themedBody, "relative w-full max-w-3xl max-h-[80vh] border border-border bg-card shadow-2xl flex flex-col")}>
|
|
217
|
+
<Button
|
|
218
|
+
ghost
|
|
219
|
+
size="icon"
|
|
220
|
+
onClick={onClose}
|
|
221
|
+
className="absolute right-2 top-2 text-muted-foreground hover:text-foreground"
|
|
222
|
+
aria-label="Close"
|
|
223
|
+
>
|
|
224
|
+
<X />
|
|
225
|
+
</Button>
|
|
226
|
+
|
|
227
|
+
<header className="p-5 pb-3 border-b border-border">
|
|
228
|
+
<h2
|
|
229
|
+
id="model-picker-title"
|
|
230
|
+
className="font-mondwest text-display text-base tracking-wider"
|
|
231
|
+
>
|
|
232
|
+
{title}
|
|
233
|
+
</h2>
|
|
234
|
+
<p className="text-xs text-muted-foreground mt-1 font-mono">
|
|
235
|
+
current: {currentModel || "(unknown)"}
|
|
236
|
+
{currentProviderSlug && ` · ${currentProviderSlug}`}
|
|
237
|
+
</p>
|
|
238
|
+
</header>
|
|
239
|
+
|
|
240
|
+
<div className="px-5 pt-3 pb-2 border-b border-border">
|
|
241
|
+
<div className="relative">
|
|
242
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
|
243
|
+
<Input
|
|
244
|
+
autoFocus
|
|
245
|
+
placeholder="Filter providers and models…"
|
|
246
|
+
value={query}
|
|
247
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
248
|
+
className="pl-7 h-8 text-sm"
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div className="flex-1 min-h-0 grid grid-cols-[200px_1fr] overflow-hidden">
|
|
254
|
+
<ProviderColumn
|
|
255
|
+
loading={loading}
|
|
256
|
+
error={error}
|
|
257
|
+
providers={filteredProviders}
|
|
258
|
+
total={providers.length}
|
|
259
|
+
selectedSlug={selectedSlug}
|
|
260
|
+
query={needle}
|
|
261
|
+
onSelect={(slug) => {
|
|
262
|
+
setSelectedSlug(slug);
|
|
263
|
+
setSelectedModel("");
|
|
264
|
+
}}
|
|
265
|
+
/>
|
|
266
|
+
|
|
267
|
+
<ModelColumn
|
|
268
|
+
provider={selectedProvider}
|
|
269
|
+
models={filteredModels}
|
|
270
|
+
allModels={models}
|
|
271
|
+
selectedModel={selectedModel}
|
|
272
|
+
currentModel={currentModel}
|
|
273
|
+
currentProviderSlug={currentProviderSlug}
|
|
274
|
+
onSelect={setSelectedModel}
|
|
275
|
+
onConfirm={(m) => {
|
|
276
|
+
setSelectedModel(m);
|
|
277
|
+
// Confirm on next tick so state settles.
|
|
278
|
+
window.setTimeout(confirm, 0);
|
|
279
|
+
}}
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<footer className="border-t border-border p-3 flex items-center justify-between gap-3 flex-wrap">
|
|
284
|
+
{alwaysGlobal ? (
|
|
285
|
+
<span className="text-xs text-muted-foreground">
|
|
286
|
+
Saves to config.yaml — applies to new sessions.
|
|
287
|
+
</span>
|
|
288
|
+
) : (
|
|
289
|
+
<div className="flex items-center gap-2">
|
|
290
|
+
<Checkbox
|
|
291
|
+
checked={persistGlobal}
|
|
292
|
+
id="model-picker-persist-global"
|
|
293
|
+
onCheckedChange={(checked) =>
|
|
294
|
+
setPersistGlobal(checked === true)
|
|
295
|
+
}
|
|
296
|
+
/>
|
|
297
|
+
|
|
298
|
+
<Label
|
|
299
|
+
className="font-mondwest normal-case tracking-normal text-xs text-muted-foreground cursor-pointer"
|
|
300
|
+
htmlFor="model-picker-persist-global"
|
|
301
|
+
>
|
|
302
|
+
Persist globally (otherwise this session only)
|
|
303
|
+
</Label>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
<div className="flex items-center gap-2 ml-auto">
|
|
308
|
+
<Button outlined onClick={onClose} disabled={applying}>
|
|
309
|
+
Cancel
|
|
310
|
+
</Button>
|
|
311
|
+
<Button onClick={confirm} disabled={!canConfirm}>
|
|
312
|
+
{applying ? <Spinner /> : "Switch"}
|
|
313
|
+
</Button>
|
|
314
|
+
</div>
|
|
315
|
+
</footer>
|
|
316
|
+
</div>
|
|
317
|
+
</div>,
|
|
318
|
+
document.body,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* ------------------------------------------------------------------ */
|
|
323
|
+
/* Provider column */
|
|
324
|
+
/* ------------------------------------------------------------------ */
|
|
325
|
+
|
|
326
|
+
function ProviderColumn({
|
|
327
|
+
loading,
|
|
328
|
+
error,
|
|
329
|
+
providers,
|
|
330
|
+
total,
|
|
331
|
+
selectedSlug,
|
|
332
|
+
query,
|
|
333
|
+
onSelect,
|
|
334
|
+
}: {
|
|
335
|
+
loading: boolean;
|
|
336
|
+
error: string | null;
|
|
337
|
+
providers: ModelOptionProvider[];
|
|
338
|
+
total: number;
|
|
339
|
+
selectedSlug: string;
|
|
340
|
+
query: string;
|
|
341
|
+
onSelect(slug: string): void;
|
|
342
|
+
}) {
|
|
343
|
+
return (
|
|
344
|
+
<div className="border-r border-border overflow-y-auto">
|
|
345
|
+
{loading && (
|
|
346
|
+
<div className="flex items-center gap-2 p-4 text-xs text-muted-foreground">
|
|
347
|
+
<Spinner className="text-xs" /> loading…
|
|
348
|
+
</div>
|
|
349
|
+
)}
|
|
350
|
+
|
|
351
|
+
{error && <div className="p-4 text-xs text-destructive">{error}</div>}
|
|
352
|
+
|
|
353
|
+
{!loading && !error && providers.length === 0 && (
|
|
354
|
+
<div className="p-4 text-xs text-muted-foreground italic">
|
|
355
|
+
{query
|
|
356
|
+
? "no matches"
|
|
357
|
+
: total === 0
|
|
358
|
+
? "no authenticated providers"
|
|
359
|
+
: "no matches"}
|
|
360
|
+
</div>
|
|
361
|
+
)}
|
|
362
|
+
|
|
363
|
+
{providers.map((p) => {
|
|
364
|
+
const active = p.slug === selectedSlug;
|
|
365
|
+
return (
|
|
366
|
+
<ListItem
|
|
367
|
+
key={p.slug}
|
|
368
|
+
active={active}
|
|
369
|
+
onClick={() => onSelect(p.slug)}
|
|
370
|
+
className={`items-start text-xs border-l-2 ${
|
|
371
|
+
active ? "border-l-primary" : "border-l-transparent"
|
|
372
|
+
}`}
|
|
373
|
+
>
|
|
374
|
+
<div className="flex-1 min-w-0">
|
|
375
|
+
<div className="flex items-center gap-1.5">
|
|
376
|
+
<span className="font-medium truncate">{p.name}</span>
|
|
377
|
+
{p.is_current && <CurrentTag />}
|
|
378
|
+
</div>
|
|
379
|
+
<div className="text-xs text-text-secondary font-mono truncate">
|
|
380
|
+
{p.slug} · {p.total_models ?? p.models?.length ?? 0} models
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
</ListItem>
|
|
384
|
+
);
|
|
385
|
+
})}
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* ------------------------------------------------------------------ */
|
|
391
|
+
/* Model column */
|
|
392
|
+
/* ------------------------------------------------------------------ */
|
|
393
|
+
|
|
394
|
+
function ModelColumn({
|
|
395
|
+
provider,
|
|
396
|
+
models,
|
|
397
|
+
allModels,
|
|
398
|
+
selectedModel,
|
|
399
|
+
currentModel,
|
|
400
|
+
currentProviderSlug,
|
|
401
|
+
onSelect,
|
|
402
|
+
onConfirm,
|
|
403
|
+
}: {
|
|
404
|
+
provider: ModelOptionProvider | null;
|
|
405
|
+
models: string[];
|
|
406
|
+
allModels: string[];
|
|
407
|
+
selectedModel: string;
|
|
408
|
+
currentModel: string;
|
|
409
|
+
currentProviderSlug: string;
|
|
410
|
+
onSelect(model: string): void;
|
|
411
|
+
onConfirm(model: string): void;
|
|
412
|
+
}) {
|
|
413
|
+
if (!provider) {
|
|
414
|
+
return (
|
|
415
|
+
<div className="overflow-y-auto">
|
|
416
|
+
<div className="p-4 text-xs text-muted-foreground italic">
|
|
417
|
+
pick a provider →
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<div className="overflow-y-auto">
|
|
425
|
+
{provider.warning && (
|
|
426
|
+
<div className="p-3 text-xs text-destructive border-b border-border">
|
|
427
|
+
{provider.warning}
|
|
428
|
+
</div>
|
|
429
|
+
)}
|
|
430
|
+
|
|
431
|
+
{models.length === 0 ? (
|
|
432
|
+
<div className="p-4 text-xs text-muted-foreground italic">
|
|
433
|
+
{allModels.length
|
|
434
|
+
? "no models match your filter"
|
|
435
|
+
: "no models listed for this provider"}
|
|
436
|
+
</div>
|
|
437
|
+
) : (
|
|
438
|
+
models.map((m) => {
|
|
439
|
+
const active = m === selectedModel;
|
|
440
|
+
const isCurrent =
|
|
441
|
+
m === currentModel && provider.slug === currentProviderSlug;
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<ListItem
|
|
445
|
+
key={m}
|
|
446
|
+
active={active}
|
|
447
|
+
onClick={() => onSelect(m)}
|
|
448
|
+
onDoubleClick={() => onConfirm(m)}
|
|
449
|
+
className="px-3 py-1.5 text-xs font-mono"
|
|
450
|
+
>
|
|
451
|
+
<Check
|
|
452
|
+
className={`h-3 w-3 shrink-0 ${active ? "text-primary" : "text-transparent"}`}
|
|
453
|
+
/>
|
|
454
|
+
<span className="flex-1 truncate">{m}</span>
|
|
455
|
+
{isCurrent && <CurrentTag />}
|
|
456
|
+
</ListItem>
|
|
457
|
+
);
|
|
458
|
+
})
|
|
459
|
+
)}
|
|
460
|
+
</div>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function CurrentTag() {
|
|
465
|
+
return (
|
|
466
|
+
<span className="text-display text-xs tracking-wider text-primary shrink-0">
|
|
467
|
+
current
|
|
468
|
+
</span>
|
|
469
|
+
);
|
|
470
|
+
}
|