@boundless.network/setup 0.1.3 → 0.1.4
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/app.js +50 -28
- package/dist/lib/catalog.js +1 -0
- package/dist/ui/menu.js +5 -2
- package/dist/ui/table.js +9 -6
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -22,12 +22,30 @@ import { LOGO, LOGO_COLUMNS, LOGO_ROWS } from "./ui/logo.js";
|
|
|
22
22
|
import { ModelSelect } from "./ui/table.js";
|
|
23
23
|
import { BAD, BRAND, MUTED, OK, WARN } from "./ui/theme.js";
|
|
24
24
|
const NAV = "↑↓ navigate enter select";
|
|
25
|
+
const STEPS = new Set(["welcome", "existing", "harnesses", "scope", "store", "model", "review"]);
|
|
26
|
+
const withBack = (hints, canBack) => (canBack ? `${hints} esc back` : hints);
|
|
25
27
|
function message(error) {
|
|
26
28
|
return error instanceof Error ? error.message : String(error);
|
|
27
29
|
}
|
|
28
30
|
export function App({ onFinish }) {
|
|
29
31
|
const { exit } = useApp();
|
|
30
32
|
const [screen, setScreen] = useState({ kind: "boot" });
|
|
33
|
+
const history = useRef([]).current;
|
|
34
|
+
const canBack = history.length > 0;
|
|
35
|
+
const go = (next) => {
|
|
36
|
+
if (STEPS.has(screen.kind)) {
|
|
37
|
+
history.push(screen);
|
|
38
|
+
}
|
|
39
|
+
setScreen(next);
|
|
40
|
+
};
|
|
41
|
+
const back = () => {
|
|
42
|
+
const previous = history.pop();
|
|
43
|
+
if (previous) {
|
|
44
|
+
setScreen(previous);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const forget = () => history.splice(0);
|
|
48
|
+
const pickModels = () => go(wizard.catalog.length > 0 ? { kind: "model", role: "primary" } : { kind: "catalog" });
|
|
31
49
|
const wizard = useRef({
|
|
32
50
|
background: "",
|
|
33
51
|
backups: [],
|
|
@@ -81,12 +99,12 @@ export function App({ onFinish }) {
|
|
|
81
99
|
if (role === "primary") {
|
|
82
100
|
wizard.primary = id;
|
|
83
101
|
if (hasBackgroundSlot()) {
|
|
84
|
-
|
|
102
|
+
go({ kind: "model", role: "background" });
|
|
85
103
|
return;
|
|
86
104
|
}
|
|
87
105
|
}
|
|
88
106
|
wizard.background = id;
|
|
89
|
-
|
|
107
|
+
go({ kind: "review" });
|
|
90
108
|
};
|
|
91
109
|
const finish = () => {
|
|
92
110
|
onFinish(summary());
|
|
@@ -98,69 +116,70 @@ export function App({ onFinish }) {
|
|
|
98
116
|
case "welcome":
|
|
99
117
|
return (_jsx(Welcome, { onSelect: (choice) => {
|
|
100
118
|
if (choice === "continue") {
|
|
101
|
-
|
|
119
|
+
go(wizard.key ? { kind: "harnesses" } : wizard.existing?.valid ? { kind: "existing" } : { kind: "signin" });
|
|
102
120
|
}
|
|
103
121
|
else if (choice === "cancel") {
|
|
104
122
|
exit();
|
|
105
123
|
}
|
|
106
124
|
else {
|
|
107
|
-
|
|
125
|
+
go({ kind: "info", topic: choice });
|
|
108
126
|
}
|
|
109
127
|
}, wizard: wizard }));
|
|
110
128
|
case "info":
|
|
111
|
-
return _jsx(Info, { onBack:
|
|
129
|
+
return _jsx(Info, { onBack: back, topic: screen.topic });
|
|
112
130
|
case "existing":
|
|
113
|
-
return (_jsx(Existing, { onSelect: (choice) => {
|
|
131
|
+
return (_jsx(Existing, { canBack: canBack, onBack: back, onSelect: (choice) => {
|
|
114
132
|
if (choice === "reuse") {
|
|
115
133
|
wizard.key = wizard.existing?.key ?? null;
|
|
116
134
|
wizard.keyStore = wizard.existing?.source ?? "";
|
|
117
|
-
|
|
135
|
+
go({ kind: "harnesses" });
|
|
118
136
|
}
|
|
119
137
|
else if (choice === "new") {
|
|
120
|
-
|
|
138
|
+
go({ kind: "signin" });
|
|
121
139
|
}
|
|
122
140
|
else {
|
|
123
141
|
exit();
|
|
124
142
|
}
|
|
125
143
|
}, source: wizard.existing?.source ?? "" }));
|
|
126
144
|
case "signin":
|
|
127
|
-
return (_jsx(SignIn, { config: wizard.config, env: wizard.env, onCancel:
|
|
145
|
+
return (_jsx(SignIn, { config: wizard.config, env: wizard.env, onCancel: back, onError: (error) => fail(error, { kind: "signin" }), onToken: (token) => setScreen({ kind: "exchange", token }) }));
|
|
128
146
|
case "exchange":
|
|
129
147
|
return (_jsx(Exchange, { onDone: (key, name) => {
|
|
130
148
|
wizard.key = key;
|
|
131
149
|
wizard.keyName = name;
|
|
150
|
+
forget();
|
|
132
151
|
setScreen({ kind: "harnesses" });
|
|
133
152
|
}, onError: (error) => fail(error, error instanceof SetupError && error.code === "setup_recovery_required" ? null : screen), token: screen.token }));
|
|
134
153
|
case "error":
|
|
135
154
|
return (_jsx(ErrorScreen, { message: screen.message, onSelect: (choice) => (choice === "retry" && screen.retry ? setScreen(screen.retry) : exit()), retry: screen.retry !== null }));
|
|
136
155
|
case "harnesses":
|
|
137
|
-
return (_jsx(Harnesses, { initial: wizard.harnesses.length > 0 ? wizard.harnesses : [...wizard.installed], installed: wizard.installed, onSubmit: (ids) => {
|
|
156
|
+
return (_jsx(Harnesses, { canBack: canBack, initial: wizard.harnesses.length > 0 ? wizard.harnesses : [...wizard.installed], installed: wizard.installed, onBack: back, onSubmit: (ids) => {
|
|
138
157
|
wizard.harnesses = ids;
|
|
139
|
-
|
|
158
|
+
go({ kind: "scope" });
|
|
140
159
|
} }));
|
|
141
160
|
case "scope":
|
|
142
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Where should this apply?" }), _jsx(Select, { onSelect: (scope) => {
|
|
161
|
+
return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "Where should this apply?" }), _jsx(Select, { initial: wizard.scope, onCancel: back, onSelect: (scope) => {
|
|
143
162
|
wizard.scope = scope;
|
|
144
163
|
if (scope === "project") {
|
|
145
164
|
wizard.store = null;
|
|
146
165
|
wizard.keyFile = projectKeyFile(wizard.env.cwd);
|
|
147
166
|
wizard.keyStore = tilde(wizard.keyFile);
|
|
148
|
-
|
|
167
|
+
pickModels();
|
|
149
168
|
}
|
|
150
169
|
else {
|
|
151
|
-
|
|
170
|
+
go({ kind: "store" });
|
|
152
171
|
}
|
|
153
172
|
}, options: [
|
|
154
173
|
{ hint: "recommended", label: "My user account", value: "user" },
|
|
155
174
|
{ hint: tilde(wizard.env.cwd), label: "Only this project", value: "project" },
|
|
156
175
|
] })] }));
|
|
157
176
|
case "store":
|
|
158
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Where should I keep the key?" }), _jsx(Muted, { center: true, children: "Your shell profile will read it from there; the key itself never goes into a profile." }), _jsx(Select, { onSelect: (kind) => {
|
|
177
|
+
return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "Where should I keep the key?" }), _jsx(Muted, { center: true, children: "Your shell profile will read it from there; the key itself never goes into a profile." }), _jsx(Select, { initial: wizard.store?.kind, onCancel: back, onSelect: (kind) => {
|
|
159
178
|
const store = wizard.stores.find((candidate) => candidate.kind === kind) ?? null;
|
|
160
179
|
wizard.store = store;
|
|
161
180
|
wizard.keyFile = store?.path ?? null;
|
|
162
181
|
wizard.keyStore = store?.label ?? "";
|
|
163
|
-
|
|
182
|
+
pickModels();
|
|
164
183
|
}, options: wizard.stores.map((store, index) => ({
|
|
165
184
|
hint: index === 0 ? "recommended" : undefined,
|
|
166
185
|
label: store.label,
|
|
@@ -172,11 +191,14 @@ export function App({ onFinish }) {
|
|
|
172
191
|
setScreen({ kind: "model", role: "primary" });
|
|
173
192
|
}, onError: (error) => fail(error, { kind: "catalog" }), wizard: wizard }));
|
|
174
193
|
case "model":
|
|
175
|
-
return (_jsx(ModelPicker, { catalog: wizard.catalog, onSelect: (id) => chooseModel(screen.role, id), role: screen.role }));
|
|
194
|
+
return (_jsx(ModelPicker, { canBack: canBack, catalog: wizard.catalog, initial: screen.role === "primary" ? wizard.primary : wizard.background, onBack: back, onSelect: (id) => chooseModel(screen.role, id), role: screen.role }));
|
|
176
195
|
case "review":
|
|
177
|
-
return (_jsx(Review, { harnesses: chosen(), input: planInput(), onCancel: () => exit(), onPlans: (plans) => {
|
|
196
|
+
return (_jsx(Review, { harnesses: chosen(), input: planInput(), canBack: canBack, onBack: back, onCancel: () => exit(), onPlans: (plans) => {
|
|
178
197
|
wizard.plans = plans;
|
|
179
|
-
}, onProceed: () =>
|
|
198
|
+
}, onProceed: () => {
|
|
199
|
+
forget();
|
|
200
|
+
setScreen({ kind: "apply" });
|
|
201
|
+
}, wizard: wizard }));
|
|
180
202
|
case "apply":
|
|
181
203
|
return (_jsx(Apply, { onDone: () => setScreen(wizard.plans.some(({ plan }) => plan.manual.length > 0) ? { kind: "manual" } : { kind: "test" }), onError: (error) => fail(error, { kind: "review" }), wizard: wizard }));
|
|
182
204
|
case "manual":
|
|
@@ -247,8 +269,8 @@ function Info({ onBack, topic }) {
|
|
|
247
269
|
});
|
|
248
270
|
return (_jsxs(Frame, { hints: "enter back", children: [_jsx(Title, { children: topic === "what" ? "What this does" : "Privacy & data" }), topic === "what" ? (_jsxs(_Fragment, { children: [_jsxs(Para, { children: ["1. Opens ", CONSOLE, "/authorize in your browser. You sign in (or sign up) and approve \"Boundless local setup\"."] }), _jsx(Para, { children: "2. Exchanges that approval for one inference key named setup-\u2026 on your account. The secret is shown once, to this program only." }), _jsxs(Para, { children: ["3. Stores the key in your credential store or a private 0600 file, and points your shell at it via ", KEY_ENV, "."] }), _jsx(Para, { children: "4. Backs up and merges each chosen harness's configuration, preserving unrelated providers and settings." }), _jsx(Para, { children: "5. Sends one small request per model to prove it works. That request uses account credits; nothing is purchased." })] })) : (_jsxs(_Fragment, { children: [_jsxs(Para, { children: ["Nothing from this directory is uploaded. The only network calls are to ", CONSOLE, ", its authorization server, and the inference gateway."] }), _jsx(Para, { children: "The sign-in token is used once for the key exchange and then discarded; no refresh token is kept." }), _jsx(Para, { children: "The API key is written only to the store you pick and to harness files that need it, all with owner-only permissions. It never appears in command arguments, shell history, logs, or this screen." }), _jsxs(Para, { children: ["Manage or revoke the key at ", KEYS_URL, "."] })] }))] }));
|
|
249
271
|
}
|
|
250
|
-
function Existing({ onSelect, source }) {
|
|
251
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "A Boundless key is already configured" }), _jsxs(Muted, { center: true, children: ["Found in ", source, ", and the gateway accepts it."] }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: onSelect, options: [
|
|
272
|
+
function Existing({ canBack, onBack, onSelect, source, }) {
|
|
273
|
+
return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "A Boundless key is already configured" }), _jsxs(Muted, { center: true, children: ["Found in ", source, ", and the gateway accepts it."] }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onCancel: onBack, onSelect: onSelect, options: [
|
|
252
274
|
{ hint: "recommended", label: "Reuse it", value: "reuse" },
|
|
253
275
|
{ label: "Sign in and create a separate key", value: "new" },
|
|
254
276
|
{ label: "Cancel", value: "cancel" },
|
|
@@ -294,14 +316,14 @@ function Exchange({ onDone, onError, token }) {
|
|
|
294
316
|
function ErrorScreen({ message: text, onSelect, retry }) {
|
|
295
317
|
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Something went wrong" }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: BAD, children: text }) }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: onSelect, options: retry ? [{ label: "Try again", value: "retry" }, { label: "Cancel", value: "cancel" }] : [{ label: "Exit", value: "cancel" }] }) })] }));
|
|
296
318
|
}
|
|
297
|
-
function Harnesses({ initial, installed, onSubmit }) {
|
|
319
|
+
function Harnesses({ canBack, initial, installed, onBack, onSubmit, }) {
|
|
298
320
|
const [empty, setEmpty] = useState(false);
|
|
299
321
|
const options = HARNESSES.map((harness) => ({
|
|
300
322
|
hint: installed.has(harness.id) ? "installed" : "not found",
|
|
301
323
|
label: harness.name,
|
|
302
324
|
value: harness.id,
|
|
303
325
|
}));
|
|
304
|
-
return (_jsxs(Frame, { hints: "space toggle enter continue", children: [_jsx(Title, { children: "Which harnesses should I configure?" }), _jsx(Muted, { center: true, children: "Installed ones are preselected. Harnesses that are not installed get their configuration written for when they are." }), _jsx(Box, { justifyContent: "center", children: _jsx(MultiSelect, { initial: initial, onSubmit: (ids) => (ids.length === 0 ? setEmpty(true) : onSubmit(ids)), options: options }) }), empty ? (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: WARN, children: "Pick at least one." }) })) : null] }));
|
|
326
|
+
return (_jsxs(Frame, { hints: withBack("space toggle a all/none enter continue", canBack), children: [_jsx(Title, { children: "Which harnesses should I configure?" }), _jsx(Muted, { center: true, children: "Installed ones are preselected. Harnesses that are not installed get their configuration written for when they are." }), _jsx(Box, { justifyContent: "center", children: _jsx(MultiSelect, { initial: initial, onCancel: onBack, onSubmit: (ids) => (ids.length === 0 ? setEmpty(true) : onSubmit(ids)), options: options }) }), empty ? (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: WARN, children: "Pick at least one." }) })) : null] }));
|
|
305
327
|
}
|
|
306
328
|
function Catalog({ onDone, onError, wizard }) {
|
|
307
329
|
useEffect(() => {
|
|
@@ -309,10 +331,10 @@ function Catalog({ onDone, onError, wizard }) {
|
|
|
309
331
|
}, []);
|
|
310
332
|
return (_jsx(Frame, { hints: "", children: _jsx(Spinner, { label: "Reading the model catalog" }) }));
|
|
311
333
|
}
|
|
312
|
-
function ModelPicker({ catalog, onSelect, role, }) {
|
|
313
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: role === "primary" ? "Which model should be the default?" : "Which model for background work?" }), _jsxs(Muted, { center: true, children: [role === "primary" ? "Every configured harness gets this model." : "Cheap, fast tasks (Claude Code haiku slot, OpenCode small_model, omp tiny).", " Prices are USD per 1M tokens from the live catalog."] }), _jsx(ModelSelect, { models: catalog, onSelect: onSelect, recommended: role === "primary" ? RECOMMENDED_MODEL : RECOMMENDED_BACKGROUND }, role)] }));
|
|
334
|
+
function ModelPicker({ canBack, catalog, initial, onBack, onSelect, role, }) {
|
|
335
|
+
return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: role === "primary" ? "Which model should be the default?" : "Which model for background work?" }), _jsxs(Muted, { center: true, children: [role === "primary" ? "Every configured harness gets this model." : "Cheap, fast tasks (Claude Code haiku slot, OpenCode small_model, omp tiny).", " Prices are USD per 1M tokens from the live catalog."] }), _jsx(ModelSelect, { initial: initial, models: catalog, onBack: onBack, onSelect: onSelect, recommended: role === "primary" ? RECOMMENDED_MODEL : RECOMMENDED_BACKGROUND }, role)] }));
|
|
314
336
|
}
|
|
315
|
-
function Review({ harnesses, input, onCancel, onPlans, onProceed, wizard, }) {
|
|
337
|
+
function Review({ canBack, harnesses, input, onBack, onCancel, onPlans, onProceed, wizard, }) {
|
|
316
338
|
const [plans, setPlans] = useState(null);
|
|
317
339
|
useEffect(() => {
|
|
318
340
|
Promise.all(harnesses.map(async (harness) => ({ harness, plan: await harness.plan(input) }))).then((planned) => {
|
|
@@ -329,11 +351,11 @@ function Review({ harnesses, input, onCancel, onPlans, onProceed, wizard, }) {
|
|
|
329
351
|
: env.os === "windows"
|
|
330
352
|
? "user environment variable"
|
|
331
353
|
: `exported as ${KEY_ENV} by your shell profile`;
|
|
332
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Review" }), _jsx(Facts, { facts: [
|
|
354
|
+
return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "Review" }), _jsx(Facts, { facts: [
|
|
333
355
|
{ label: "Key", value: `${wizard.keyStore} · ${exposure}` },
|
|
334
356
|
{ label: "Model", value: wizard.primary },
|
|
335
357
|
...(wizard.background !== wizard.primary ? [{ label: "Background", value: wizard.background }] : []),
|
|
336
|
-
] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.map(({ harness, plan }) => (_jsxs(Box, { children: [_jsx(Text, { children: harness.name.padEnd(16) }), _jsx(Text, { color: plan.changes.length > 0 ? BRAND : MUTED, children: plan.changes.length > 0 ? plan.changes.map((change) => tilde(change.path)).join(", ") : "manual steps, shown after apply" })] }, harness.id))) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.flatMap(({ plan }) => plan.notes.slice(0, 1)).map((note) => (_jsx(Text, { color: MUTED, children: note }, note))) }), _jsx(Muted, { children: "Existing files are backed up next to themselves before they change. Credentials are never displayed." }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: (choice) => (choice === "apply" ? onProceed() : onCancel()), options: [
|
|
358
|
+
] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.map(({ harness, plan }) => (_jsxs(Box, { children: [_jsx(Text, { children: harness.name.padEnd(16) }), _jsx(Text, { color: plan.changes.length > 0 ? BRAND : MUTED, children: plan.changes.length > 0 ? plan.changes.map((change) => tilde(change.path)).join(", ") : "manual steps, shown after apply" })] }, harness.id))) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.flatMap(({ plan }) => plan.notes.slice(0, 1)).map((note) => (_jsx(Text, { color: MUTED, children: note }, note))) }), _jsx(Muted, { children: "Existing files are backed up next to themselves before they change. Credentials are never displayed." }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onCancel: onBack, onSelect: (choice) => (choice === "apply" ? onProceed() : onCancel()), options: [
|
|
337
359
|
{ label: "Apply", value: "apply" },
|
|
338
360
|
{ label: "Cancel", value: "cancel" },
|
|
339
361
|
] }) })] }));
|
package/dist/lib/catalog.js
CHANGED
|
@@ -27,6 +27,7 @@ export async function keyWorks(gatewayUrl, key) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
/** Exact USD, never rounded away: trailing zeros trimmed, up to twelve decimals. */
|
|
30
|
+
export const usdPerMillion = (amount) => `$${amount.toFixed(2)}`;
|
|
30
31
|
export function usd(amount) {
|
|
31
32
|
return `$${amount.toFixed(12).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
32
33
|
}
|
package/dist/ui/menu.js
CHANGED
|
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { BRAND, MUTED } from "./theme.js";
|
|
5
|
-
export function Select({ onCancel, onSelect, options, }) {
|
|
6
|
-
const [index, setIndex] = useState(0);
|
|
5
|
+
export function Select({ initial, onCancel, onSelect, options, }) {
|
|
6
|
+
const [index, setIndex] = useState(Math.max(options.findIndex((option) => option.value === initial), 0));
|
|
7
7
|
const move = (delta) => {
|
|
8
8
|
let next = index;
|
|
9
9
|
for (let step = 0; step < options.length; step += 1) {
|
|
@@ -46,6 +46,9 @@ export function MultiSelect({ initial, onCancel, onSubmit, options, }) {
|
|
|
46
46
|
else if (key.downArrow || input === "j") {
|
|
47
47
|
setIndex((index + 1) % options.length);
|
|
48
48
|
}
|
|
49
|
+
else if (input === "a") {
|
|
50
|
+
setChosen(chosen.size === options.length ? new Set() : new Set(options.map((option) => option.value)));
|
|
51
|
+
}
|
|
49
52
|
else if (input === " ") {
|
|
50
53
|
const value = options[index]?.value;
|
|
51
54
|
if (value !== undefined) {
|
package/dist/ui/table.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { usdPerMillion } from "../lib/catalog.js";
|
|
5
5
|
import { BRAND, MUTED } from "./theme.js";
|
|
6
6
|
const HEADER = ["ID", "Input /1M", "Output /1M", "Context", ""];
|
|
7
7
|
const cells = (model, recommended) => [
|
|
8
8
|
model.id,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
usdPerMillion(model.inputPerMillionTokens),
|
|
10
|
+
usdPerMillion(model.outputPerMillionTokens),
|
|
11
11
|
model.contextTokens.toLocaleString("en-US"),
|
|
12
12
|
model.id === recommended ? "recommended" : "",
|
|
13
13
|
];
|
|
14
|
-
export function ModelSelect({ models, onSelect, recommended, }) {
|
|
15
|
-
const start = Math.max(models.findIndex((model) => model.id === recommended), 0);
|
|
14
|
+
export function ModelSelect({ initial, models, onBack, onSelect, recommended, }) {
|
|
15
|
+
const start = Math.max(models.findIndex((model) => model.id === initial), models.findIndex((model) => model.id === recommended), 0);
|
|
16
16
|
const [index, setIndex] = useState(start);
|
|
17
17
|
useInput((input, key) => {
|
|
18
|
-
if (key.
|
|
18
|
+
if (key.escape) {
|
|
19
|
+
onBack();
|
|
20
|
+
}
|
|
21
|
+
else if (key.upArrow || input === "k") {
|
|
19
22
|
setIndex((index + models.length - 1) % models.length);
|
|
20
23
|
}
|
|
21
24
|
else if (key.downArrow || input === "j") {
|
package/package.json
CHANGED