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
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { ModelCost, ModelCostRates } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelOverride } from "../../../interfaces/settings";
|
|
3
|
+
import { FIELDS, type Field } from "../../strings";
|
|
4
|
+
import { SettingField } from "../settingField";
|
|
5
|
+
import { OverrideEntry } from "./entry";
|
|
6
|
+
|
|
7
|
+
/** Type of a field: "input" for free-text, "finite" for fixed options */
|
|
8
|
+
export type FieldType = "input" | "finite";
|
|
9
|
+
|
|
10
|
+
/** Validation function for input fields */
|
|
11
|
+
export type InputValidator = (raw: string) => string | null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Abstract base class for one editable field of a model override entry
|
|
15
|
+
* (Template Method): each subclass owns its display format, validation
|
|
16
|
+
* and apply logic in a single place, so the parse/format round-trips
|
|
17
|
+
* can't drift. Extends the shared {@link SettingField} base (id, label,
|
|
18
|
+
* description, placeholder from the FIELDS registry).
|
|
19
|
+
*/
|
|
20
|
+
export abstract class OverrideField extends SettingField<OverrideEntry> {
|
|
21
|
+
/** Field type: "input" for free-text, "finite" for fixed options. */
|
|
22
|
+
abstract readonly type: FieldType;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Canonical display label for this field — the single source of truth
|
|
26
|
+
* for both the field rows' `currentValue` and the post-commit refresh
|
|
27
|
+
* of an open field submenu, so the two can't drift.
|
|
28
|
+
*/
|
|
29
|
+
abstract displayValue(entry: OverrideEntry): string;
|
|
30
|
+
|
|
31
|
+
/** Current value shown in the row — same rendering as the post-commit
|
|
32
|
+
* refresh of an open field submenu, so the two can't drift. */
|
|
33
|
+
currentValue(entry: OverrideEntry): string {
|
|
34
|
+
return this.displayValue(entry);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** For "input" fields: validation function for committed values. */
|
|
38
|
+
abstract readonly validate: InputValidator;
|
|
39
|
+
|
|
40
|
+
/** For "finite" fields: the fixed options (undefined for "input"). */
|
|
41
|
+
readonly options: readonly string[] | undefined = undefined;
|
|
42
|
+
|
|
43
|
+
/** Applies a committed value to the override object. The pattern key
|
|
44
|
+
* rename is handled separately by the mutator. */
|
|
45
|
+
abstract apply(override: ModelOverride, value: string): void;
|
|
46
|
+
|
|
47
|
+
/** One `term: value` part of the entry-row summary (see
|
|
48
|
+
* {@link OverrideSummary.of}), or `null` when the field shouldn't
|
|
49
|
+
* appear (unset/zero values — each field decides). The pattern is the
|
|
50
|
+
* row label, so it never contributes a part; the default is `null`. */
|
|
51
|
+
summaryPart(_override: ModelOverride): string | null {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Parses a non-negative number; blank input means zero (unset fields
|
|
56
|
+
* default to zero at read time). Returns `null` when invalid. Shared
|
|
57
|
+
* by the cost and numeric fields so their validation can't drift. */
|
|
58
|
+
protected static parseNonNegative(raw: string): number | null {
|
|
59
|
+
const n = Number(raw.trim());
|
|
60
|
+
return isFinite(n) && n >= 0 ? n : null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The entry's pattern key. The rename itself is handled by the mutator
|
|
66
|
+
* (`OverrideEntryMutator`); {@link apply} is a no-op.
|
|
67
|
+
*/
|
|
68
|
+
class PatternField extends OverrideField {
|
|
69
|
+
readonly id = "pattern";
|
|
70
|
+
readonly type = "input";
|
|
71
|
+
readonly field = FIELDS.pattern;
|
|
72
|
+
|
|
73
|
+
displayValue(entry: OverrideEntry): string {
|
|
74
|
+
return entry.pattern;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
readonly validate: InputValidator = (raw) => {
|
|
78
|
+
const trimmed = raw.trim();
|
|
79
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
apply(): void {
|
|
83
|
+
// The pattern IS the map key — renames go through the mutator.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* One `ModelCostRates` key of the override's `cost` object (absorbs the
|
|
89
|
+
* former `CostFieldDefFactory` and the standalone `parseCostValue` /
|
|
90
|
+
* `applyCostFieldValue` helpers): owns the cost parse and immutable
|
|
91
|
+
* apply in the same place as its display format.
|
|
92
|
+
*/
|
|
93
|
+
class CostField extends OverrideField {
|
|
94
|
+
readonly type = "input";
|
|
95
|
+
override readonly field: Field;
|
|
96
|
+
|
|
97
|
+
constructor(
|
|
98
|
+
readonly id: string,
|
|
99
|
+
private readonly key: keyof ModelCostRates,
|
|
100
|
+
field: Field,
|
|
101
|
+
/** Short term used in the entry-row summary (e.g. `cache read`). */
|
|
102
|
+
private readonly summaryTerm: string,
|
|
103
|
+
) {
|
|
104
|
+
super();
|
|
105
|
+
this.field = field;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
displayValue(entry: OverrideEntry): string {
|
|
109
|
+
return String(entry.override.cost?.[this.key] ?? 0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
readonly validate: InputValidator = (raw) => {
|
|
113
|
+
const parsed = OverrideField.parseNonNegative(raw);
|
|
114
|
+
return parsed === null ? null : String(parsed);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
apply(override: ModelOverride, value: string): void {
|
|
118
|
+
const nextCost = this.applyCost(override.cost, Number(value));
|
|
119
|
+
if (nextCost) override.cost = nextCost;
|
|
120
|
+
else delete override.cost;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Returns a copy of `cost` with this field set to `value`. A `0` (or
|
|
124
|
+
* non-finite) value removes the field — unset cost fields default to
|
|
125
|
+
* zero at read time — and when no fields remain, returns `undefined`
|
|
126
|
+
* so the whole `cost` object can be dropped. Immutable. */
|
|
127
|
+
private applyCost(
|
|
128
|
+
cost: ModelOverride["cost"],
|
|
129
|
+
value: number,
|
|
130
|
+
): Partial<ModelCost> | undefined {
|
|
131
|
+
const next = { ...cost };
|
|
132
|
+
if (isFinite(value) && value > 0) next[this.key] = value;
|
|
133
|
+
else delete next[this.key];
|
|
134
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Appears in the summary only when a non-zero cost is set. */
|
|
138
|
+
override summaryPart(override: ModelOverride): string | null {
|
|
139
|
+
const value = override.cost?.[this.key];
|
|
140
|
+
return value ? `${this.summaryTerm}: $${value}` : null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Non-negative numeric field (`contextSize`, `maxTokens`): keeps the
|
|
146
|
+
* display format, validator and apply logic in a single place so the
|
|
147
|
+
* three can't drift (e.g. the validator accepting `0` while the apply
|
|
148
|
+
* drops it — unset fields default to zero at read time, so all render
|
|
149
|
+
* as `0`).
|
|
150
|
+
*/
|
|
151
|
+
class NumericField extends OverrideField {
|
|
152
|
+
readonly type = "input";
|
|
153
|
+
override readonly field: Field;
|
|
154
|
+
|
|
155
|
+
constructor(
|
|
156
|
+
readonly id: "maxTokens" | "contextSize",
|
|
157
|
+
field: Field,
|
|
158
|
+
) {
|
|
159
|
+
super();
|
|
160
|
+
this.field = field;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
displayValue(entry: OverrideEntry): string {
|
|
164
|
+
return String(entry.override[this.id] ?? 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Accepts finite, non-negative numbers (blank means zero). */
|
|
168
|
+
readonly validate: InputValidator = (raw) => {
|
|
169
|
+
const n = OverrideField.parseNonNegative(raw);
|
|
170
|
+
return n === null ? null : String(n);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** Applies the committed value; non-positive values remove the key. */
|
|
174
|
+
apply(override: ModelOverride, value: string): void {
|
|
175
|
+
const n = Number(value);
|
|
176
|
+
if (isFinite(n) && n > 0) override[this.id] = n;
|
|
177
|
+
else delete override[this.id];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Appears in the summary only when set. */
|
|
181
|
+
override summaryPart(override: ModelOverride): string | null {
|
|
182
|
+
const value = override[this.id];
|
|
183
|
+
return value === undefined ? null : `${this.id}: ${value}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The override's `capabilities` list (finite: fixed label options). */
|
|
188
|
+
class CapabilitiesField extends OverrideField {
|
|
189
|
+
readonly id = "capabilities";
|
|
190
|
+
readonly type = "finite";
|
|
191
|
+
readonly field = FIELDS.capabilities;
|
|
192
|
+
override readonly options = ["text", "text | image"];
|
|
193
|
+
|
|
194
|
+
displayValue(entry: OverrideEntry): string {
|
|
195
|
+
return entry.override.capabilities?.join(" | ") ?? "text";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
readonly validate: InputValidator = (raw) =>
|
|
199
|
+
this.options?.includes(raw) ? raw : null;
|
|
200
|
+
|
|
201
|
+
apply(override: ModelOverride, value: string): void {
|
|
202
|
+
override.capabilities =
|
|
203
|
+
value === "text | image"
|
|
204
|
+
? ["text", "image"]
|
|
205
|
+
: value === "text"
|
|
206
|
+
? ["text"]
|
|
207
|
+
: undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Appears in the summary only when non-empty. */
|
|
211
|
+
override summaryPart(override: ModelOverride): string | null {
|
|
212
|
+
return override.capabilities?.length
|
|
213
|
+
? `capabilities: ${override.capabilities.join(",")}`
|
|
214
|
+
: null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The override's `reasoning` flag (finite: true/false, default true). */
|
|
219
|
+
class ReasoningField extends OverrideField {
|
|
220
|
+
readonly id = "reasoning";
|
|
221
|
+
readonly type = "finite";
|
|
222
|
+
readonly field = FIELDS.reasoning;
|
|
223
|
+
override readonly options = ["true", "false"];
|
|
224
|
+
|
|
225
|
+
displayValue(entry: OverrideEntry): string {
|
|
226
|
+
return entry.override.reasoning === false ? "false" : "true";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
readonly validate: InputValidator = (raw) =>
|
|
230
|
+
this.options?.includes(raw) ? raw : null;
|
|
231
|
+
|
|
232
|
+
apply(override: ModelOverride, value: string): void {
|
|
233
|
+
if (value === "true") override.reasoning = true;
|
|
234
|
+
else if (value === "false") override.reasoning = false;
|
|
235
|
+
else delete override.reasoning;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Appears in the summary only when explicitly set. */
|
|
239
|
+
override summaryPart(override: ModelOverride): string | null {
|
|
240
|
+
return override.reasoning === undefined
|
|
241
|
+
? null
|
|
242
|
+
: `reasoning: ${override.reasoning}`;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Cost field specs: [ModelCostRates key, FIELDS entry, summary term] */
|
|
247
|
+
const COST_FIELDS: [keyof ModelCostRates, Field, string][] = [
|
|
248
|
+
["input", FIELDS.inputCost, "input"],
|
|
249
|
+
["output", FIELDS.outputCost, "output"],
|
|
250
|
+
["cacheRead", FIELDS.cacheReadCost, "cache read"],
|
|
251
|
+
["cacheWrite", FIELDS.cacheWriteCost, "cache write"],
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The field registry for model override entries: the complete list of
|
|
256
|
+
* editable fields (order defines the row order in the settings list)
|
|
257
|
+
* plus id-based lookup. Throws on an unknown id — the registry is
|
|
258
|
+
* static, so a miss is a programming error.
|
|
259
|
+
*/
|
|
260
|
+
export class OverrideFields {
|
|
261
|
+
static readonly all: OverrideField[] = [
|
|
262
|
+
new PatternField(),
|
|
263
|
+
...COST_FIELDS.map(
|
|
264
|
+
([key, field, term]) => new CostField(`cost.${key}`, key, field, term),
|
|
265
|
+
),
|
|
266
|
+
new CapabilitiesField(),
|
|
267
|
+
new ReasoningField(),
|
|
268
|
+
new NumericField("contextSize", FIELDS.contextSize),
|
|
269
|
+
new NumericField("maxTokens", FIELDS.maxTokens),
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
static byId(id: string): OverrideField {
|
|
273
|
+
const field = this.all.find((f) => f.id === id);
|
|
274
|
+
if (!field) throw new Error(`Unknown override field: ${id}`);
|
|
275
|
+
return field;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Builds the entry-row summary shown in the override entry list
|
|
281
|
+
* (`input: $0.2, output: $0.6, cache read: $0.01, …`), composed from the
|
|
282
|
+
* {@link OverrideFields} registry — adding a field to the registry makes
|
|
283
|
+
* it show up here too, so the summary can't lag the edit menu. Fields
|
|
284
|
+
* with unset/zero values are omitted for brevity (each field decides via
|
|
285
|
+
* {@link OverrideField.summaryPart}); `—` when nothing is set.
|
|
286
|
+
*/
|
|
287
|
+
export class OverrideSummary {
|
|
288
|
+
static of(override: ModelOverride): string {
|
|
289
|
+
const parts = OverrideFields.all
|
|
290
|
+
.map((field) => field.summaryPart(override))
|
|
291
|
+
.filter((part): part is string => part !== null);
|
|
292
|
+
return parts.length > 0 ? parts.join(", ") : "—";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { LlamaServer, ModelOverride } from "../../../interfaces/settings";
|
|
2
|
+
import { OverrideEntry } from "./entry";
|
|
3
|
+
import { OverrideFields } from "./fields";
|
|
4
|
+
|
|
5
|
+
/** Result of applying a field change: the next server list and the
|
|
6
|
+
* updated override for UI refresh. `null` when the entry doesn't exist
|
|
7
|
+
* (stale index — a no-op). */
|
|
8
|
+
export interface MutationResult {
|
|
9
|
+
next: LlamaServer[];
|
|
10
|
+
updatedOverride: ModelOverride;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Applies edits (field commits, entry adds/renames/removals) to the
|
|
15
|
+
* override entries of a single server, immutably. Binds the server list
|
|
16
|
+
* and server index once in the constructor so call sites don't juggle
|
|
17
|
+
* long positional lists.
|
|
18
|
+
*
|
|
19
|
+
* Per-field commit behavior lives in the {@link OverrideFields} registry
|
|
20
|
+
* (`apply` on each field instance) — this class only resolves the field
|
|
21
|
+
* and the entry, and handles the pattern rename specially.
|
|
22
|
+
*/
|
|
23
|
+
export class OverrideEntryMutator {
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly servers: LlamaServer[],
|
|
26
|
+
private readonly serverIndex: number,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
/** The server's override entries, in map iteration order. */
|
|
30
|
+
entries(): OverrideEntry[] {
|
|
31
|
+
return OverrideEntry.listFrom(this.servers[this.serverIndex]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Handles a field commit from an override entry's submenu. Returns
|
|
35
|
+
* `null` when the entry doesn't exist (stale index — no-op). */
|
|
36
|
+
applyFieldChange(
|
|
37
|
+
entryIndex: number,
|
|
38
|
+
field: string,
|
|
39
|
+
value: string,
|
|
40
|
+
): MutationResult | null {
|
|
41
|
+
const entry = this.entries()[entryIndex];
|
|
42
|
+
if (!entry) return null;
|
|
43
|
+
|
|
44
|
+
// Pattern field: rename the pattern key, keep the override object
|
|
45
|
+
if (field === "pattern") {
|
|
46
|
+
return {
|
|
47
|
+
next: this.updateEntry(entryIndex, value, entry.override),
|
|
48
|
+
updatedOverride: entry.override,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Other fields: let the field definition apply itself
|
|
53
|
+
const updatedOverride: ModelOverride = { ...entry.override };
|
|
54
|
+
OverrideFields.byId(field).apply(updatedOverride, value);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
next: this.updateEntry(entryIndex, entry.pattern, updatedOverride),
|
|
58
|
+
updatedOverride,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Adds a new override entry with a uniquified pattern. */
|
|
63
|
+
addEntry(pattern: string, override: ModelOverride = {}): LlamaServer[] {
|
|
64
|
+
const server = this.servers[this.serverIndex];
|
|
65
|
+
if (!server) return this.servers;
|
|
66
|
+
|
|
67
|
+
const existing = new Set(Object.keys(server.overrides ?? {}));
|
|
68
|
+
let uniquePattern = pattern;
|
|
69
|
+
let n = 2;
|
|
70
|
+
while (existing.has(uniquePattern)) {
|
|
71
|
+
uniquePattern = `${pattern}-${n++}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return this.withServer((s) => ({
|
|
75
|
+
...s,
|
|
76
|
+
overrides: { ...s.overrides, [uniquePattern]: override },
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Removes the entry at `entryIndex`. */
|
|
81
|
+
removeEntry(entryIndex: number): LlamaServer[] {
|
|
82
|
+
return this.withServer((s) => ({
|
|
83
|
+
...s,
|
|
84
|
+
overrides: Object.fromEntries(
|
|
85
|
+
Object.entries(s.overrides ?? {}).filter((_, j) => j !== entryIndex),
|
|
86
|
+
),
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// -- internal helpers ------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/** Returns the servers with the target server replaced by
|
|
93
|
+
* `fn(server)`. Immutable. */
|
|
94
|
+
private withServer(fn: (server: LlamaServer) => LlamaServer): LlamaServer[] {
|
|
95
|
+
return this.servers.map((server, i) =>
|
|
96
|
+
i === this.serverIndex ? fn(server) : server,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Replaces the entry at `entryIndex` with `pattern → override` in a
|
|
101
|
+
* single mutation — used both for renaming the pattern and for editing
|
|
102
|
+
* its fields. The entry keeps its position in the map's iteration
|
|
103
|
+
* order. Immutable. */
|
|
104
|
+
private updateEntry(
|
|
105
|
+
entryIndex: number,
|
|
106
|
+
pattern: string,
|
|
107
|
+
override: ModelOverride,
|
|
108
|
+
): LlamaServer[] {
|
|
109
|
+
return this.withServer((s) => ({
|
|
110
|
+
...s,
|
|
111
|
+
overrides: Object.fromEntries(
|
|
112
|
+
Object.entries(s.overrides ?? {}).map(([k, v], j) =>
|
|
113
|
+
j === entryIndex ? [pattern, override] : [k, v],
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
2
|
+
import { FieldMessages, TITLES } from "../../strings";
|
|
3
|
+
import { ItemBuilder } from "../itemBuilder";
|
|
4
|
+
import { OverrideEntry } from "./entry";
|
|
5
|
+
import { OverrideFields, type OverrideField } from "./fields";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds `SettingItem` objects for override entry field submenus.
|
|
9
|
+
*/
|
|
10
|
+
export class OverrideItemBuilder extends ItemBuilder<
|
|
11
|
+
OverrideEntry,
|
|
12
|
+
OverrideField
|
|
13
|
+
> {
|
|
14
|
+
protected get fields(): readonly OverrideField[] {
|
|
15
|
+
return OverrideFields.all;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Adds field-type-specific columns: fixed `values` for "finite" fields,
|
|
20
|
+
* an `InputDialog` submenu (validating against the field definition)
|
|
21
|
+
* for "input" fields.
|
|
22
|
+
*/
|
|
23
|
+
protected decorate(
|
|
24
|
+
def: OverrideField,
|
|
25
|
+
entry: OverrideEntry,
|
|
26
|
+
base: SettingItem,
|
|
27
|
+
): SettingItem {
|
|
28
|
+
if (def.type === "finite") {
|
|
29
|
+
return { ...base, values: [...(def.options ?? [])] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
...base,
|
|
34
|
+
submenu: this.dialogs.inputSubmenu(
|
|
35
|
+
TITLES.edit(def.label),
|
|
36
|
+
FieldMessages.of(def.field),
|
|
37
|
+
def.placeholder,
|
|
38
|
+
def.validate,
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type {
|
|
3
|
+
Component,
|
|
4
|
+
Focusable,
|
|
5
|
+
SettingItem,
|
|
6
|
+
SettingsList,
|
|
7
|
+
} from "@earendil-works/pi-tui";
|
|
8
|
+
import type { LlamaServer } from "../../../interfaces/settings";
|
|
9
|
+
import type { LlamaSettingsManager } from "../../../managers/settings";
|
|
10
|
+
import { HINTS } from "../../strings";
|
|
11
|
+
import type { OverrideSettingsListOptions } from "../editorOptions";
|
|
12
|
+
import { SettingsListFactory } from "../settingsListFactory";
|
|
13
|
+
import { OverrideEntryListEditor } from "./entryEditor";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Top-level overrides editor: one row per server, Enter drills into
|
|
17
|
+
* override entries.
|
|
18
|
+
*
|
|
19
|
+
* Unlike `ServerSettingsList`, this has no add/delete — just drill-down.
|
|
20
|
+
*
|
|
21
|
+
* Within a server's entry list: Enter drills into the field-edit submenu;
|
|
22
|
+
* a adds, d deletes (after confirmation). Fields use a mix of finite
|
|
23
|
+
* (Enter to cycle) and infinite (Enter to type) editing:
|
|
24
|
+
*
|
|
25
|
+
* - Pattern / costs (input, output, cacheRead, cacheWrite): infinite —
|
|
26
|
+
* Enter opens an Input for typing.
|
|
27
|
+
* - Capabilities: finite — Enter cycles between `text` and `text | image`.
|
|
28
|
+
* - Reasoning: finite — Enter cycles between `true` and `false`.
|
|
29
|
+
*
|
|
30
|
+
* Servers themselves are not managed here — use `/models servers`.
|
|
31
|
+
*/
|
|
32
|
+
export class OverrideSettingsList implements Component, Focusable {
|
|
33
|
+
private settingsList: SettingsList | null = null;
|
|
34
|
+
private isFocused = false;
|
|
35
|
+
|
|
36
|
+
constructor(private readonly options: OverrideSettingsListOptions) {
|
|
37
|
+
this.settingsList = this.buildSettingsList();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Opens the editor in a modal `ui.custom` dialog seeded with the
|
|
42
|
+
* current `llamaSettings.servers` and resolves when the user closes
|
|
43
|
+
* it (Esc). Writes go through `settings.setLlamaSetting()`; write
|
|
44
|
+
* errors are notified via `ui` and the editor stays open with the
|
|
45
|
+
* pre-mutation list.
|
|
46
|
+
*/
|
|
47
|
+
static async show(
|
|
48
|
+
ui: ExtensionUIContext,
|
|
49
|
+
settings: LlamaSettingsManager,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const servers = await settings.getLlamaServers();
|
|
52
|
+
|
|
53
|
+
await ui.custom<void>(
|
|
54
|
+
(tui, theme, keybindings, done) =>
|
|
55
|
+
new OverrideSettingsList({
|
|
56
|
+
tui,
|
|
57
|
+
theme,
|
|
58
|
+
keybindings,
|
|
59
|
+
servers,
|
|
60
|
+
persist: (next) => settings.setLlamaSetting("servers", next),
|
|
61
|
+
done: () => done(undefined),
|
|
62
|
+
onError: (message) => ui.notify(message, "error"),
|
|
63
|
+
onChanged: () => {}, // no per-change notification needed
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// -- Component -------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
invalidate(): void {
|
|
71
|
+
this.settingsList?.invalidate();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
handleInput(data: string): void {
|
|
75
|
+
this.settingsList?.handleInput(data);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
render(width: number): string[] {
|
|
79
|
+
if (!this.settingsList) return ["Loading..."];
|
|
80
|
+
return this.settingsList.render(width);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// -- Focusable -------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
get focused(): boolean {
|
|
86
|
+
return this.isFocused;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
set focused(value: boolean) {
|
|
90
|
+
// `SettingsList` has no `focused` flag — the flag is ours alone
|
|
91
|
+
this.isFocused = value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// -- internal --------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
private buildSettingsList(): SettingsList {
|
|
97
|
+
const serverItems: SettingItem[] = this.options.servers.map(
|
|
98
|
+
(server, i) => ({
|
|
99
|
+
id: `server-${i}`,
|
|
100
|
+
label: server.url,
|
|
101
|
+
description: HINTS.overrideServerRow,
|
|
102
|
+
currentValue: OverrideSettingsList.entriesLabel(server),
|
|
103
|
+
submenu: (_cv, done) =>
|
|
104
|
+
new OverrideEntryListEditor(this.options, i, () => {
|
|
105
|
+
// Entry count may have changed (add/delete/pattern rename)
|
|
106
|
+
const current = this.options.servers[i];
|
|
107
|
+
serverList?.updateValue(
|
|
108
|
+
`server-${i}`,
|
|
109
|
+
OverrideSettingsList.entriesLabel(current),
|
|
110
|
+
);
|
|
111
|
+
done();
|
|
112
|
+
}),
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
let serverList: SettingsList | null = null;
|
|
117
|
+
serverList = SettingsListFactory.create(serverItems, () => {
|
|
118
|
+
this.options.done();
|
|
119
|
+
});
|
|
120
|
+
return serverList;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Row summary for a server in the list: its override entry count. */
|
|
124
|
+
private static entriesLabel(server: LlamaServer | undefined): string {
|
|
125
|
+
return `${Object.keys(server?.overrides ?? {}).length} entries`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { LlamaServer } from "../../../interfaces/settings";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builder for `LlamaServer` objects, centralizing the "empty string means
|
|
5
|
+
* omit the key" rule for the optional `id` and `name` fields — the single
|
|
6
|
+
* place where that invariant lives, shared by the add-server wizard's
|
|
7
|
+
* final step and by field commits on existing servers.
|
|
8
|
+
*
|
|
9
|
+
* Start from an existing server with {@link from}, chain the setters,
|
|
10
|
+
* then call {@link build}. Immutable with respect to the input: the
|
|
11
|
+
* source server is never mutated.
|
|
12
|
+
*/
|
|
13
|
+
export class LlamaServerBuilder {
|
|
14
|
+
private constructor(private data: LlamaServer) {}
|
|
15
|
+
|
|
16
|
+
/** Starts a builder from an existing server (also used to start a
|
|
17
|
+
* fresh `{ url }` server in the wizard flow). */
|
|
18
|
+
static from(server: LlamaServer): LlamaServerBuilder {
|
|
19
|
+
return new LlamaServerBuilder({ ...server });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Sets the URL (always present, never omitted). */
|
|
23
|
+
url(url: string): this {
|
|
24
|
+
this.data = { ...this.data, url };
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Sets the custom provider ID. An empty value omits the key — when
|
|
30
|
+
* editing an existing server this removes it, restoring the
|
|
31
|
+
* URL-based auto-detected ID.
|
|
32
|
+
*/
|
|
33
|
+
optionalId(id: string): this {
|
|
34
|
+
this.data = this.withOptional("id", id);
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sets the display name. An empty value omits the key — when editing
|
|
40
|
+
* an existing server this removes it.
|
|
41
|
+
*/
|
|
42
|
+
optionalName(name: string): this {
|
|
43
|
+
this.data = this.withOptional("name", name);
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Returns the built server. */
|
|
48
|
+
build(): LlamaServer {
|
|
49
|
+
return this.data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Copies `data`, setting `key` to `value` when non-empty and
|
|
53
|
+
* removing the key otherwise. */
|
|
54
|
+
private withOptional(key: "id" | "name", value: string): LlamaServer {
|
|
55
|
+
const next = { ...this.data };
|
|
56
|
+
if (value.length > 0) next[key] = value;
|
|
57
|
+
else delete next[key];
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
}
|