pi-llama-cpp 0.13.0 → 0.14.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/README.md +7 -8
- package/package.json +2 -2
- package/src/api/client.ts +25 -0
- package/src/constants.ts +0 -5
- package/src/enums/status.ts +0 -1
- package/src/interfaces/endpoints/models.ts +1 -1
- package/src/interfaces/settings.ts +1 -1
- package/src/interfaces/sortBy.ts +4 -0
- package/src/managers/command/models.ts +247 -0
- package/src/managers/command.ts +29 -461
- package/src/managers/events.ts +2 -2
- package/src/managers/server.ts +27 -3
- package/src/managers/settings.ts +12 -88
- package/src/models/baseModel.ts +27 -10
- package/src/models/legacyModel.ts +2 -2
- package/src/models/routerModel.ts +2 -2
- package/src/models/singleModel.ts +1 -1
- package/src/server.ts +11 -29
- package/src/sse/client.ts +113 -59
- package/src/sse/fetch.ts +43 -0
- package/src/sse/manager.ts +9 -27
- package/src/sse/types.ts +0 -4
- package/src/ui/dialog/base.ts +118 -0
- package/src/ui/dialog/confirm.ts +45 -0
- package/src/ui/dialog/factory.ts +111 -0
- package/src/ui/dialog/input.ts +63 -0
- package/src/ui/dialog/options.ts +23 -0
- package/src/ui/editors/editorOptions.ts +43 -0
- package/src/ui/editors/itemBuilder.ts +47 -0
- package/src/ui/editors/listEditor.ts +291 -0
- package/src/ui/editors/override/entry.ts +24 -0
- package/src/ui/editors/override/entryEditor.ts +166 -0
- package/src/ui/editors/override/fields.ts +294 -0
- package/src/ui/editors/override/handlers.ts +118 -0
- package/src/ui/editors/override/itemBuilder.ts +42 -0
- package/src/ui/editors/override/overrideList.ts +127 -0
- package/src/ui/editors/server/builder.ts +60 -0
- package/src/ui/editors/server/fields.ts +84 -0
- package/src/ui/editors/server/handlers.ts +32 -0
- package/src/ui/editors/server/itemBuilder.ts +68 -0
- package/src/ui/editors/server/serverEditor.ts +161 -0
- package/src/ui/editors/server/utils.ts +45 -0
- package/src/ui/editors/server/wizard.ts +110 -0
- package/src/ui/editors/settingField.ts +37 -0
- package/src/ui/editors/settingsListFactory.ts +33 -0
- package/src/ui/settings/index.ts +237 -0
- package/src/ui/strings.ts +8 -4
- package/src/utils/serverIds.ts +21 -0
- package/src/utils/settingsStore.ts +1 -1
- package/src/utils/urlResolver.ts +129 -0
- package/src/utils/urls.ts +33 -13
- package/tests/commandManager.test.ts +14 -9
- package/tests/dialog.test.ts +55 -4
- package/tests/legacyModel.test.ts +34 -28
- package/tests/overrides.test.ts +111 -88
- package/tests/routerModel.test.ts +67 -68
- package/tests/server.test.ts +0 -12
- package/tests/settings.test.ts +10 -8
- package/tests/singleModel.test.ts +12 -12
- package/tests/sseManager.test.ts +6 -24
- package/src/ui/dialog.ts +0 -290
- package/src/ui/overrideEntryEditor.ts +0 -119
- package/src/ui/overrideSettingsList.ts +0 -710
- package/src/ui/serverListEditor.ts +0 -59
- package/src/ui/serverSettingsList.ts +0 -513
|
@@ -1,710 +0,0 @@
|
|
|
1
|
-
import type { ModelCostRates } from "@earendil-works/pi-ai";
|
|
2
|
-
import {
|
|
3
|
-
getSettingsListTheme,
|
|
4
|
-
type Theme,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import type {
|
|
7
|
-
Component,
|
|
8
|
-
Focusable,
|
|
9
|
-
KeybindingsManager,
|
|
10
|
-
TUI,
|
|
11
|
-
} from "@earendil-works/pi-tui";
|
|
12
|
-
import { SettingsList, type SettingItem } from "@earendil-works/pi-tui";
|
|
13
|
-
import type { LlamaServer, ModelOverride } from "../interfaces/settings";
|
|
14
|
-
import { ConfirmDialog, InputDialog } from "./dialog";
|
|
15
|
-
import {
|
|
16
|
-
addOverrideEntry,
|
|
17
|
-
applyCostFieldValue,
|
|
18
|
-
formatOverrideSummary,
|
|
19
|
-
parseCostValue,
|
|
20
|
-
removeOverrideEntry,
|
|
21
|
-
updateOverrideEntry,
|
|
22
|
-
} from "./overrideEntryEditor";
|
|
23
|
-
import { fieldMessage, FIELDS, HINTS, TITLES } from "./strings";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Options for the SettingsList-based overrides editor.
|
|
27
|
-
*/
|
|
28
|
-
export interface OverrideSettingsListOptions {
|
|
29
|
-
/** TUI instance, used to request re-renders after async persists */
|
|
30
|
-
tui: TUI;
|
|
31
|
-
/** Theme for dialogs (from the ctx.ui.custom factory) */
|
|
32
|
-
theme: Theme;
|
|
33
|
-
/** App keybindings manager (injected by ctx.ui.custom) */
|
|
34
|
-
keybindings: KeybindingsManager;
|
|
35
|
-
/**
|
|
36
|
-
* Snapshot of the merged `llamaSettings.servers` to edit. Replaced
|
|
37
|
-
* in place (`options.servers = next`) after every successful persist —
|
|
38
|
-
* this object is shared between the top-level list and the per-server
|
|
39
|
-
* entry editors so re-entering a server sees the adopted values.
|
|
40
|
-
*/
|
|
41
|
-
servers: LlamaServer[];
|
|
42
|
-
/** Persists a new server list; a rejection keeps the current list */
|
|
43
|
-
persist: (next: LlamaServer[]) => Promise<void>;
|
|
44
|
-
/** Closes the editor (called on Esc in the server list) */
|
|
45
|
-
done: () => void;
|
|
46
|
-
/** Notifies about persistence errors */
|
|
47
|
-
onError: (message: string) => void;
|
|
48
|
-
/** Called after a successful add/edit/delete */
|
|
49
|
-
onChanged: () => void;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ─── Field items for one override entry ───────────────────────────────────
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Builds the SettingItem for one override entry's editable fields.
|
|
56
|
-
*
|
|
57
|
-
* - Pattern / costs: infinite → `InputDialog` submenus
|
|
58
|
-
* - Capabilities: finite → `["text", "text | image"]`
|
|
59
|
-
* - Reasoning: finite → `["true", "false"]`
|
|
60
|
-
*
|
|
61
|
-
* Commits are reported uniformly through the containing `SettingsList`'s
|
|
62
|
-
* `onChange` (both finite cycling and `done(value)` from the input
|
|
63
|
-
* submenus arrive there), so no per-field callback is needed here.
|
|
64
|
-
*/
|
|
65
|
-
/**
|
|
66
|
-
* Canonical display label for one of an override's editable fields —
|
|
67
|
-
* the single source of truth for both the field rows' `currentValue`
|
|
68
|
-
* and the post-commit refresh of an open field submenu, so the two
|
|
69
|
-
* can't drift.
|
|
70
|
-
*/
|
|
71
|
-
export const overrideFieldValue = (
|
|
72
|
-
field: string,
|
|
73
|
-
override: ModelOverride,
|
|
74
|
-
): string => {
|
|
75
|
-
if (field.startsWith("cost.")) {
|
|
76
|
-
const costField = field.split(".")[1] as keyof ModelCostRates;
|
|
77
|
-
return String(override.cost?.[costField] ?? 0);
|
|
78
|
-
}
|
|
79
|
-
if (field === "capabilities")
|
|
80
|
-
return override.capabilities?.join(" | ") ?? "text";
|
|
81
|
-
if (field === "reasoning")
|
|
82
|
-
return override.reasoning === undefined
|
|
83
|
-
? "true"
|
|
84
|
-
: override.reasoning
|
|
85
|
-
? "true"
|
|
86
|
-
: "false";
|
|
87
|
-
if (field === "maxTokens") return String(override.maxTokens ?? 0);
|
|
88
|
-
if (field === "contextSize") return String(override.contextSize ?? 0);
|
|
89
|
-
return "";
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export const buildOverrideFieldItems = (
|
|
93
|
-
entry: { pattern: string; override: ModelOverride },
|
|
94
|
-
theme: Theme,
|
|
95
|
-
tui: TUI,
|
|
96
|
-
): SettingItem[] => [
|
|
97
|
-
// Pattern — infinite
|
|
98
|
-
{
|
|
99
|
-
id: "pattern",
|
|
100
|
-
label: FIELDS.pattern.label,
|
|
101
|
-
description: FIELDS.pattern.description,
|
|
102
|
-
currentValue: entry.pattern,
|
|
103
|
-
submenu: InputDialog.inputSubmenu(
|
|
104
|
-
theme,
|
|
105
|
-
tui,
|
|
106
|
-
TITLES.edit(FIELDS.pattern.label),
|
|
107
|
-
fieldMessage(FIELDS.pattern),
|
|
108
|
-
FIELDS.pattern.placeholder,
|
|
109
|
-
(raw) => {
|
|
110
|
-
const trimmed = raw.trim();
|
|
111
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
112
|
-
},
|
|
113
|
-
),
|
|
114
|
-
},
|
|
115
|
-
// Input cost — infinite
|
|
116
|
-
{
|
|
117
|
-
id: "cost.input",
|
|
118
|
-
label: FIELDS.inputCost.label,
|
|
119
|
-
description: FIELDS.inputCost.description,
|
|
120
|
-
currentValue: overrideFieldValue("cost.input", entry.override),
|
|
121
|
-
submenu: InputDialog.inputSubmenu(
|
|
122
|
-
theme,
|
|
123
|
-
tui,
|
|
124
|
-
TITLES.edit(FIELDS.inputCost.label),
|
|
125
|
-
fieldMessage(FIELDS.inputCost),
|
|
126
|
-
FIELDS.inputCost.placeholder,
|
|
127
|
-
(raw) => {
|
|
128
|
-
const parsed = parseCostValue(raw);
|
|
129
|
-
return parsed === null ? null : String(parsed);
|
|
130
|
-
},
|
|
131
|
-
),
|
|
132
|
-
},
|
|
133
|
-
// Output cost — infinite
|
|
134
|
-
{
|
|
135
|
-
id: "cost.output",
|
|
136
|
-
label: FIELDS.outputCost.label,
|
|
137
|
-
description: FIELDS.outputCost.description,
|
|
138
|
-
currentValue: overrideFieldValue("cost.output", entry.override),
|
|
139
|
-
submenu: InputDialog.inputSubmenu(
|
|
140
|
-
theme,
|
|
141
|
-
tui,
|
|
142
|
-
TITLES.edit(FIELDS.outputCost.label),
|
|
143
|
-
fieldMessage(FIELDS.outputCost),
|
|
144
|
-
FIELDS.outputCost.placeholder,
|
|
145
|
-
(raw) => {
|
|
146
|
-
const parsed = parseCostValue(raw);
|
|
147
|
-
return parsed === null ? null : String(parsed);
|
|
148
|
-
},
|
|
149
|
-
),
|
|
150
|
-
},
|
|
151
|
-
// Cache read cost — infinite
|
|
152
|
-
{
|
|
153
|
-
id: "cost.cacheRead",
|
|
154
|
-
label: FIELDS.cacheReadCost.label,
|
|
155
|
-
description: FIELDS.cacheReadCost.description,
|
|
156
|
-
currentValue: overrideFieldValue("cost.cacheRead", entry.override),
|
|
157
|
-
submenu: InputDialog.inputSubmenu(
|
|
158
|
-
theme,
|
|
159
|
-
tui,
|
|
160
|
-
TITLES.edit(FIELDS.cacheReadCost.label),
|
|
161
|
-
fieldMessage(FIELDS.cacheReadCost),
|
|
162
|
-
FIELDS.cacheReadCost.placeholder,
|
|
163
|
-
(raw) => {
|
|
164
|
-
const parsed = parseCostValue(raw);
|
|
165
|
-
return parsed === null ? null : String(parsed);
|
|
166
|
-
},
|
|
167
|
-
),
|
|
168
|
-
},
|
|
169
|
-
// Cache write cost — infinite
|
|
170
|
-
{
|
|
171
|
-
id: "cost.cacheWrite",
|
|
172
|
-
label: FIELDS.cacheWriteCost.label,
|
|
173
|
-
description: FIELDS.cacheWriteCost.description,
|
|
174
|
-
currentValue: overrideFieldValue("cost.cacheWrite", entry.override),
|
|
175
|
-
submenu: InputDialog.inputSubmenu(
|
|
176
|
-
theme,
|
|
177
|
-
tui,
|
|
178
|
-
TITLES.edit(FIELDS.cacheWriteCost.label),
|
|
179
|
-
fieldMessage(FIELDS.cacheWriteCost),
|
|
180
|
-
FIELDS.cacheWriteCost.placeholder,
|
|
181
|
-
(raw) => {
|
|
182
|
-
const parsed = parseCostValue(raw);
|
|
183
|
-
return parsed === null ? null : String(parsed);
|
|
184
|
-
},
|
|
185
|
-
),
|
|
186
|
-
},
|
|
187
|
-
// Capabilities — finite: text or text | image
|
|
188
|
-
{
|
|
189
|
-
id: "capabilities",
|
|
190
|
-
label: FIELDS.capabilities.label,
|
|
191
|
-
description: FIELDS.capabilities.description,
|
|
192
|
-
currentValue: overrideFieldValue("capabilities", entry.override),
|
|
193
|
-
values: ["text", "text | image"],
|
|
194
|
-
},
|
|
195
|
-
// Reasoning — finite: true or false
|
|
196
|
-
{
|
|
197
|
-
id: "reasoning",
|
|
198
|
-
label: FIELDS.reasoning.label,
|
|
199
|
-
description: FIELDS.reasoning.description,
|
|
200
|
-
currentValue: overrideFieldValue("reasoning", entry.override),
|
|
201
|
-
values: ["true", "false"],
|
|
202
|
-
},
|
|
203
|
-
// Context size — infinite
|
|
204
|
-
{
|
|
205
|
-
id: "contextSize",
|
|
206
|
-
label: FIELDS.contextSize.label,
|
|
207
|
-
description: FIELDS.contextSize.description,
|
|
208
|
-
currentValue: overrideFieldValue("contextSize", entry.override),
|
|
209
|
-
submenu: InputDialog.inputSubmenu(
|
|
210
|
-
theme,
|
|
211
|
-
tui,
|
|
212
|
-
TITLES.edit(FIELDS.contextSize.label),
|
|
213
|
-
fieldMessage(FIELDS.contextSize),
|
|
214
|
-
FIELDS.contextSize.placeholder,
|
|
215
|
-
(raw) => {
|
|
216
|
-
const n = Number(raw.trim());
|
|
217
|
-
return n >= 0 && isFinite(n) ? String(n) : null;
|
|
218
|
-
},
|
|
219
|
-
),
|
|
220
|
-
},
|
|
221
|
-
// Max tokens — infinite
|
|
222
|
-
{
|
|
223
|
-
id: "maxTokens",
|
|
224
|
-
label: FIELDS.maxTokens.label,
|
|
225
|
-
description: FIELDS.maxTokens.description,
|
|
226
|
-
currentValue: overrideFieldValue("maxTokens", entry.override),
|
|
227
|
-
submenu: InputDialog.inputSubmenu(
|
|
228
|
-
theme,
|
|
229
|
-
tui,
|
|
230
|
-
TITLES.edit(FIELDS.maxTokens.label),
|
|
231
|
-
fieldMessage(FIELDS.maxTokens),
|
|
232
|
-
FIELDS.maxTokens.placeholder,
|
|
233
|
-
(raw) => {
|
|
234
|
-
const n = Number(raw.trim());
|
|
235
|
-
return n >= 0 && isFinite(n) ? String(n) : null;
|
|
236
|
-
},
|
|
237
|
-
),
|
|
238
|
-
},
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Parses the capabilities label back to an array or undefined.
|
|
243
|
-
*/
|
|
244
|
-
const parseCapabilitiesLabel = (
|
|
245
|
-
label: string,
|
|
246
|
-
): ("text" | "image")[] | undefined => {
|
|
247
|
-
if (label === "text") return ["text"];
|
|
248
|
-
if (label === "text | image") return ["text", "image"];
|
|
249
|
-
return undefined;
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Parses the reasoning label back to a boolean or undefined.
|
|
254
|
-
*/
|
|
255
|
-
const parseReasoningLabel = (label: string): boolean | undefined => {
|
|
256
|
-
if (label === "true") return true;
|
|
257
|
-
if (label === "false") return false;
|
|
258
|
-
return undefined;
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
// ─── Override entry list (per-server) ─────────────────────────────────────
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* The override entries of a single server, as a list view with add/delete.
|
|
265
|
-
*/
|
|
266
|
-
class OverrideEntryListEditor implements Component, Focusable {
|
|
267
|
-
private settingsList: SettingsList | null = null;
|
|
268
|
-
private mode: "list" | "add" | "confirm" = "list";
|
|
269
|
-
private addDialog: InputDialog | undefined;
|
|
270
|
-
private confirmDialog: ConfirmDialog | undefined;
|
|
271
|
-
private isFocused = false;
|
|
272
|
-
private _selectedIndex = 0;
|
|
273
|
-
private submenuOpen = false;
|
|
274
|
-
/** Currently open field submenu, so commits can refresh its rows */
|
|
275
|
-
private fieldList: SettingsList | null = null;
|
|
276
|
-
|
|
277
|
-
constructor(
|
|
278
|
-
/** Shared options object — `persistSnapshot` updates `options.servers`
|
|
279
|
-
* in place so later drill-downs (and sibling editors) see the change */
|
|
280
|
-
private readonly options: OverrideSettingsListOptions,
|
|
281
|
-
private readonly serverIndex: number,
|
|
282
|
-
/** Closes this editor (called on Esc in the entry list) */
|
|
283
|
-
private readonly done: () => void,
|
|
284
|
-
) {
|
|
285
|
-
this.settingsList = this.buildSettingsList();
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// -- Focusable -----------------------------------------------------------
|
|
289
|
-
|
|
290
|
-
get focused(): boolean {
|
|
291
|
-
return this.isFocused;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
set focused(value: boolean) {
|
|
295
|
-
this.isFocused = value;
|
|
296
|
-
if (this.addDialog) this.addDialog.focused = value;
|
|
297
|
-
if (this.confirmDialog) this.confirmDialog.focused = value;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// -- Component -----------------------------------------------------------
|
|
301
|
-
|
|
302
|
-
invalidate(): void {
|
|
303
|
-
this.settingsList?.invalidate();
|
|
304
|
-
this.addDialog?.invalidate();
|
|
305
|
-
this.confirmDialog?.invalidate();
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
handleInput(data: string): void {
|
|
309
|
-
if (this.mode === "add") {
|
|
310
|
-
this.addDialog?.handleInput(data);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (this.mode === "confirm") {
|
|
315
|
-
this.confirmDialog?.handleInput(data);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// A pattern row's field submenu is open: delegate everything (Esc,
|
|
320
|
-
// a/d, arrows) to the containing SettingsList, which forwards input
|
|
321
|
-
// to the submenu. Intercepting here would close the whole editor on
|
|
322
|
-
// Esc and trigger add/delete while typing in a field.
|
|
323
|
-
if (this.submenuOpen && this.settingsList) {
|
|
324
|
-
this.settingsList.handleInput(data);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const kb = this.options.keybindings;
|
|
329
|
-
if (kb.matches(data, "tui.select.cancel")) {
|
|
330
|
-
this.done();
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
// Track selection for add/delete (delegate to SettingsList for rendering)
|
|
334
|
-
if (kb.matches(data, "tui.select.up")) {
|
|
335
|
-
this._selectedIndex =
|
|
336
|
-
this._selectedIndex === 0
|
|
337
|
-
? this.getEntries().length - 1
|
|
338
|
-
: this._selectedIndex - 1;
|
|
339
|
-
}
|
|
340
|
-
if (kb.matches(data, "tui.select.down")) {
|
|
341
|
-
this._selectedIndex =
|
|
342
|
-
this._selectedIndex === this.getEntries().length - 1
|
|
343
|
-
? 0
|
|
344
|
-
: this._selectedIndex + 1;
|
|
345
|
-
}
|
|
346
|
-
if (data === "a") {
|
|
347
|
-
this.beginAdd();
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (data === "d") {
|
|
351
|
-
this.beginConfirm();
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
if (this.settingsList) {
|
|
355
|
-
this.settingsList.handleInput(data);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
render(width: number): string[] {
|
|
360
|
-
if (this.mode === "add") {
|
|
361
|
-
return this.addDialog?.render(width) ?? [];
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (this.mode === "confirm") {
|
|
365
|
-
return this.confirmDialog?.render(width) ?? [];
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (this.settingsList) {
|
|
369
|
-
// The shortcuts live in the rows' description; only when the list is
|
|
370
|
-
// empty (no rows → no description) show them as the hint line
|
|
371
|
-
const lines = this.settingsList.render(width);
|
|
372
|
-
if (this.getEntries().length === 0) {
|
|
373
|
-
lines[lines.length - 1] = getSettingsListTheme().hint(
|
|
374
|
-
HINTS.emptyOverrideEntries,
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
return lines;
|
|
378
|
-
}
|
|
379
|
-
return ["Loading..."];
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// -- helpers ---------------------------------------------------------------
|
|
383
|
-
|
|
384
|
-
private getEntries(): { pattern: string; override: ModelOverride }[] {
|
|
385
|
-
const server = this.options.servers[this.serverIndex];
|
|
386
|
-
return Object.entries(server?.overrides ?? {}).map(
|
|
387
|
-
([pattern, override]) => ({ pattern, override }),
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
private buildSettingsList(): SettingsList {
|
|
392
|
-
const { theme, tui } = this.options;
|
|
393
|
-
// A fresh list never has a submenu open (also covers the pattern-key
|
|
394
|
-
// rebuild path, which discards the open field submenu)
|
|
395
|
-
this.submenuOpen = false;
|
|
396
|
-
const items: SettingItem[] = this.getEntries().map((entry, i) => ({
|
|
397
|
-
id: `entry-${i}`,
|
|
398
|
-
label: entry.pattern,
|
|
399
|
-
// Shortcuts live here instead of a custom hint line; the summary
|
|
400
|
-
// itself is already shown as the row's currentValue
|
|
401
|
-
description: HINTS.overrideEntryRow,
|
|
402
|
-
currentValue: formatOverrideSummary(entry.override),
|
|
403
|
-
submenu: (_cv, done) => {
|
|
404
|
-
// Re-read the entry so the submenu prefills with the current
|
|
405
|
-
// values (the list is rebuilt after every successful persist,
|
|
406
|
-
// so `i` is stable for this list instance's lifetime)
|
|
407
|
-
const current = this.getEntries()[i] ?? entry;
|
|
408
|
-
const fieldItems = buildOverrideFieldItems(current, theme, tui);
|
|
409
|
-
const fieldList = new SettingsList(
|
|
410
|
-
fieldItems,
|
|
411
|
-
Math.min(fieldItems.length + 2, 15),
|
|
412
|
-
getSettingsListTheme(),
|
|
413
|
-
// Field commits — finite cycling (capabilities, reasoning) and
|
|
414
|
-
// input submenus (pattern, costs) — all arrive here
|
|
415
|
-
(field, value) => void this.handleFieldChange(i, field, value),
|
|
416
|
-
() => {
|
|
417
|
-
this.submenuOpen = false;
|
|
418
|
-
this.fieldList = null;
|
|
419
|
-
done();
|
|
420
|
-
},
|
|
421
|
-
);
|
|
422
|
-
// Kept so handleFieldChange can refresh the open submenu's rows
|
|
423
|
-
// after a commit (the submenu stays open across edits)
|
|
424
|
-
this.fieldList = fieldList;
|
|
425
|
-
this.submenuOpen = true;
|
|
426
|
-
return fieldList;
|
|
427
|
-
},
|
|
428
|
-
}));
|
|
429
|
-
|
|
430
|
-
return new SettingsList(
|
|
431
|
-
items,
|
|
432
|
-
Math.min(items.length + 2, 15),
|
|
433
|
-
getSettingsListTheme(),
|
|
434
|
-
() => {},
|
|
435
|
-
() => {
|
|
436
|
-
this.done();
|
|
437
|
-
},
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Rebuilds the entry list and places the cursor on `targetIndex`
|
|
443
|
-
* (clamped to the last entry). A fresh SettingsList starts its cursor
|
|
444
|
-
* at index 0 and `selectedIndex` is private, so `selectItem` is the
|
|
445
|
-
* supported way to restore the position; the manual `_selectedIndex`
|
|
446
|
-
* tracker (used for add/delete targeting) is kept in sync.
|
|
447
|
-
*/
|
|
448
|
-
private rebuildList(targetIndex: number): void {
|
|
449
|
-
this.settingsList = this.buildSettingsList();
|
|
450
|
-
const count = this.getEntries().length;
|
|
451
|
-
if (count === 0) {
|
|
452
|
-
this._selectedIndex = 0;
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
const target = Math.min(targetIndex, count - 1);
|
|
456
|
-
this._selectedIndex = target;
|
|
457
|
-
this.settingsList.selectItem(`entry-${target}`);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
private beginAdd(): void {
|
|
461
|
-
this.mode = "add";
|
|
462
|
-
this.addDialog = new InputDialog({
|
|
463
|
-
theme: this.options.theme,
|
|
464
|
-
tui: this.options.tui,
|
|
465
|
-
title: TITLES.addOverride,
|
|
466
|
-
message: fieldMessage(FIELDS.pattern),
|
|
467
|
-
placeholder: FIELDS.pattern.placeholder,
|
|
468
|
-
validate: (raw) => {
|
|
469
|
-
const trimmed = raw.trim();
|
|
470
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
471
|
-
},
|
|
472
|
-
onSubmit: (value) => {
|
|
473
|
-
this.addDialog = undefined;
|
|
474
|
-
void this.saveAdd(value);
|
|
475
|
-
},
|
|
476
|
-
onCancel: () => {
|
|
477
|
-
this.addDialog = undefined;
|
|
478
|
-
this.mode = "list";
|
|
479
|
-
this.options.tui.requestRender();
|
|
480
|
-
},
|
|
481
|
-
});
|
|
482
|
-
this.addDialog.focused = this.isFocused;
|
|
483
|
-
this.options.tui.requestRender();
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
private beginConfirm(): void {
|
|
487
|
-
const entry = this.getEntries()[this._selectedIndex];
|
|
488
|
-
if (!entry) return;
|
|
489
|
-
|
|
490
|
-
this.mode = "confirm";
|
|
491
|
-
this.confirmDialog = new ConfirmDialog({
|
|
492
|
-
theme: this.options.theme,
|
|
493
|
-
tui: this.options.tui,
|
|
494
|
-
title: TITLES.deleteOverride,
|
|
495
|
-
message: `Delete "${entry.pattern}"?`,
|
|
496
|
-
onConfirm: () => {
|
|
497
|
-
this.confirmDialog = undefined;
|
|
498
|
-
void this.deleteSelected();
|
|
499
|
-
},
|
|
500
|
-
onCancel: () => {
|
|
501
|
-
this.confirmDialog = undefined;
|
|
502
|
-
this.mode = "list";
|
|
503
|
-
this.options.tui.requestRender();
|
|
504
|
-
},
|
|
505
|
-
});
|
|
506
|
-
this.confirmDialog.focused = this.isFocused;
|
|
507
|
-
this.options.tui.requestRender();
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
private async saveAdd(pattern: string): Promise<void> {
|
|
511
|
-
const server = this.options.servers[this.serverIndex];
|
|
512
|
-
if (!server) {
|
|
513
|
-
this.mode = "list";
|
|
514
|
-
this.options.tui.requestRender();
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Uniquify the pattern
|
|
519
|
-
const existing = new Set(Object.keys(server.overrides ?? {}));
|
|
520
|
-
let uniquePattern = pattern;
|
|
521
|
-
let n = 2;
|
|
522
|
-
while (existing.has(uniquePattern)) {
|
|
523
|
-
uniquePattern = `${pattern}-${n++}`;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const next = addOverrideEntry(
|
|
527
|
-
this.options.servers,
|
|
528
|
-
this.serverIndex,
|
|
529
|
-
uniquePattern,
|
|
530
|
-
);
|
|
531
|
-
await this.persistSnapshot(next, () => {
|
|
532
|
-
this.mode = "list";
|
|
533
|
-
// The new entry is appended last — put the cursor on it
|
|
534
|
-
this.rebuildList(this.getEntries().length - 1);
|
|
535
|
-
this.options.onChanged();
|
|
536
|
-
this.options.tui.requestRender();
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
private async deleteSelected(): Promise<void> {
|
|
541
|
-
const idx = this._selectedIndex;
|
|
542
|
-
const next = removeOverrideEntry(
|
|
543
|
-
this.options.servers,
|
|
544
|
-
this.serverIndex,
|
|
545
|
-
idx,
|
|
546
|
-
);
|
|
547
|
-
await this.persistSnapshot(next, () => {
|
|
548
|
-
this.mode = "list";
|
|
549
|
-
// The entry that followed the deleted one now sits at the same
|
|
550
|
-
// index (rebuildList clamps when the last entry was deleted)
|
|
551
|
-
this.rebuildList(idx);
|
|
552
|
-
this.options.onChanged();
|
|
553
|
-
this.options.tui.requestRender();
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Persists `next`; on success adopts it as the editor's snapshot and
|
|
559
|
-
* runs `onSuccess`; on failure notifies and keeps the current state.
|
|
560
|
-
*
|
|
561
|
-
* Adopting the persisted list is essential: the editor holds a snapshot
|
|
562
|
-
* taken when it opened (not a live reference), so without this the next
|
|
563
|
-
* edit would build on stale data and rebuilt rows would show old values.
|
|
564
|
-
*/
|
|
565
|
-
private async persistSnapshot(
|
|
566
|
-
next: LlamaServer[],
|
|
567
|
-
onSuccess: () => void,
|
|
568
|
-
): Promise<void> {
|
|
569
|
-
try {
|
|
570
|
-
await this.options.persist(next);
|
|
571
|
-
} catch (err) {
|
|
572
|
-
this.options.onError(String(err));
|
|
573
|
-
this.mode = "list";
|
|
574
|
-
this.options.tui.requestRender();
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
this.options.servers = next;
|
|
578
|
-
onSuccess();
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
private async handleFieldChange(
|
|
582
|
-
entryIndex: number,
|
|
583
|
-
field: string,
|
|
584
|
-
value: string,
|
|
585
|
-
): Promise<void> {
|
|
586
|
-
const entries = this.getEntries();
|
|
587
|
-
const entry = entries[entryIndex];
|
|
588
|
-
if (!entry) return;
|
|
589
|
-
|
|
590
|
-
const server = this.options.servers[this.serverIndex];
|
|
591
|
-
if (!server) return;
|
|
592
|
-
|
|
593
|
-
if (field === "pattern") {
|
|
594
|
-
// Pattern change: update the key (row label changes → full rebuild)
|
|
595
|
-
const next = updateOverrideEntry(
|
|
596
|
-
this.options.servers,
|
|
597
|
-
this.serverIndex,
|
|
598
|
-
entryIndex,
|
|
599
|
-
value,
|
|
600
|
-
entry.override,
|
|
601
|
-
);
|
|
602
|
-
await this.persistSnapshot(next, () => {
|
|
603
|
-
// Keep the cursor on the renamed entry
|
|
604
|
-
this.rebuildList(entryIndex);
|
|
605
|
-
this.options.tui.requestRender();
|
|
606
|
-
});
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const updatedOverride: ModelOverride = { ...entry.override };
|
|
611
|
-
|
|
612
|
-
if (field.startsWith("cost.")) {
|
|
613
|
-
const costField = field.split(".")[1] as keyof ModelCostRates;
|
|
614
|
-
const nextCost = applyCostFieldValue(
|
|
615
|
-
entry.override.cost,
|
|
616
|
-
costField,
|
|
617
|
-
Number(value),
|
|
618
|
-
);
|
|
619
|
-
if (nextCost) updatedOverride.cost = nextCost;
|
|
620
|
-
else delete updatedOverride.cost;
|
|
621
|
-
} else if (field === "capabilities") {
|
|
622
|
-
updatedOverride.capabilities = parseCapabilitiesLabel(value);
|
|
623
|
-
} else if (field === "reasoning") {
|
|
624
|
-
const reasoning = parseReasoningLabel(value);
|
|
625
|
-
if (reasoning === undefined) {
|
|
626
|
-
delete updatedOverride.reasoning;
|
|
627
|
-
} else {
|
|
628
|
-
updatedOverride.reasoning = reasoning;
|
|
629
|
-
}
|
|
630
|
-
} else if (field === "maxTokens") {
|
|
631
|
-
const n = Number(value);
|
|
632
|
-
if (isFinite(n) && n > 0) updatedOverride.maxTokens = n;
|
|
633
|
-
else delete updatedOverride.maxTokens;
|
|
634
|
-
} else if (field === "contextSize") {
|
|
635
|
-
const n = Number(value);
|
|
636
|
-
if (isFinite(n) && n > 0) updatedOverride.contextSize = n;
|
|
637
|
-
else delete updatedOverride.contextSize;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const next = updateOverrideEntry(
|
|
641
|
-
this.options.servers,
|
|
642
|
-
this.serverIndex,
|
|
643
|
-
entryIndex,
|
|
644
|
-
entry.pattern,
|
|
645
|
-
updatedOverride,
|
|
646
|
-
);
|
|
647
|
-
// Keep the field submenu open — refresh the entry's summary row and
|
|
648
|
-
// the open submenu's own row so re-entering the field prefills the
|
|
649
|
-
// value just saved (InputDialog prefills from the row's currentValue)
|
|
650
|
-
await this.persistSnapshot(next, () => {
|
|
651
|
-
this.settingsList?.updateValue(
|
|
652
|
-
`entry-${entryIndex}`,
|
|
653
|
-
formatOverrideSummary(updatedOverride),
|
|
654
|
-
);
|
|
655
|
-
this.fieldList?.updateValue(
|
|
656
|
-
field,
|
|
657
|
-
overrideFieldValue(field, updatedOverride),
|
|
658
|
-
);
|
|
659
|
-
this.options.tui.requestRender();
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// ─── Top-level overrides editor ───────────────────────────────────────────
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* Builds the top-level SettingsList for the overrides editor.
|
|
668
|
-
* One row per server; Enter drills into the server's override entries.
|
|
669
|
-
*/
|
|
670
|
-
export const createOverrideSettingsList = (
|
|
671
|
-
options: OverrideSettingsListOptions,
|
|
672
|
-
): SettingsList => {
|
|
673
|
-
// Assigned right after the items are built; the submenu closures below
|
|
674
|
-
// only run after that, so they can safely reference the list
|
|
675
|
-
let serverList: SettingsList;
|
|
676
|
-
|
|
677
|
-
const serverItems: SettingItem[] = options.servers.map((server, i) => ({
|
|
678
|
-
id: `server-${i}`,
|
|
679
|
-
label: server.url,
|
|
680
|
-
description: "Enter: edit this server's override entries",
|
|
681
|
-
currentValue: `${Object.keys(server.overrides ?? {}).length} entries`,
|
|
682
|
-
submenu: (_cv, done) =>
|
|
683
|
-
// Pass the shared options object through: the entry editor replaces
|
|
684
|
-
// `options.servers` after each successful persist, so a fresh editor
|
|
685
|
-
// created on the next drill-down starts from the adopted list
|
|
686
|
-
new OverrideEntryListEditor(options, i, () => {
|
|
687
|
-
// The entry count may have changed (add/delete/pattern rename)
|
|
688
|
-
// while this editor was open — refresh the row before redisplay
|
|
689
|
-
const current = options.servers[i];
|
|
690
|
-
serverList?.updateValue(
|
|
691
|
-
`server-${i}`,
|
|
692
|
-
`${Object.keys(current?.overrides ?? {}).length} entries`,
|
|
693
|
-
);
|
|
694
|
-
done();
|
|
695
|
-
}),
|
|
696
|
-
}));
|
|
697
|
-
|
|
698
|
-
serverList = new SettingsList(
|
|
699
|
-
serverItems,
|
|
700
|
-
Math.min(serverItems.length + 2, 15),
|
|
701
|
-
getSettingsListTheme(),
|
|
702
|
-
() => {
|
|
703
|
-
// Server rows only open submenus; nothing changes at this level
|
|
704
|
-
},
|
|
705
|
-
() => {
|
|
706
|
-
options.done();
|
|
707
|
-
},
|
|
708
|
-
);
|
|
709
|
-
return serverList;
|
|
710
|
-
};
|