pi-llama-cpp 0.14.0 → 0.16.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 +52 -13
- package/package.json +3 -3
- package/src/api/client.ts +59 -22
- package/src/constants.ts +5 -0
- package/src/enums/mode.ts +1 -0
- package/src/index.ts +2 -1
- package/src/interfaces/endpoints/models.ts +6 -0
- package/src/interfaces/endpoints/props.ts +14 -0
- package/src/interfaces/settings.ts +6 -0
- package/src/managers/command/models.ts +12 -6
- package/src/managers/command.ts +6 -1
- package/src/managers/server.ts +30 -38
- package/src/managers/settings.ts +23 -3
- package/src/models/legacyModel.ts +3 -3
- package/src/models/llamaSwapModel.ts +90 -0
- package/src/models/routerModel.ts +2 -2
- package/src/models/singleModel.ts +2 -2
- package/src/server.ts +42 -15
- package/src/sse/manager.ts +2 -2
- package/src/ui/dialog/confirm.ts +1 -1
- package/src/ui/dialog/input.ts +1 -1
- package/src/ui/editors/editorOptions.ts +8 -1
- package/src/ui/editors/itemBuilder.ts +2 -6
- package/src/ui/editors/listEditor.ts +24 -15
- package/src/ui/editors/override/entryEditor.ts +105 -23
- package/src/ui/editors/override/fields/base.ts +53 -0
- package/src/ui/editors/override/fields/capabilities.ts +36 -0
- package/src/ui/editors/override/fields/cost.ts +64 -0
- package/src/ui/editors/override/fields/index.ts +68 -0
- package/src/ui/editors/override/fields/numeric.ts +55 -0
- package/src/ui/editors/override/fields/pattern.ts +24 -0
- package/src/ui/editors/override/fields/reasoning.ts +33 -0
- package/src/ui/editors/override/itemBuilder.ts +4 -4
- package/src/ui/editors/server/fields.ts +2 -2
- package/src/ui/editors/server/itemBuilder.ts +2 -3
- package/src/ui/editors/server/serverEditor.ts +58 -22
- package/src/ui/editors/server/utils.ts +50 -0
- package/src/ui/settings/index.ts +11 -0
- package/src/ui/strings.ts +17 -0
- package/src/utils/credentialResolver.ts +110 -0
- package/src/utils/health.ts +2 -2
- package/src/utils/urlResolver.ts +1 -1
- package/tests/{commandManager.test.ts → command/commandManager.test.ts} +80 -9
- package/tests/{events.test.ts → events/events.test.ts} +6 -6
- package/tests/mocks.ts +4 -0
- package/tests/{legacyModel.test.ts → models/legacyModel.test.ts} +5 -5
- package/tests/models/llamaSwapModel.test.ts +327 -0
- package/tests/{routerModel.test.ts → models/routerModel.test.ts} +5 -5
- package/tests/{singleModel.test.ts → models/singleModel.test.ts} +5 -5
- package/tests/{health.test.ts → server/health.test.ts} +4 -4
- package/tests/{server.test.ts → server/server.test.ts} +57 -7
- package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
- package/tests/{settings.test.ts → settings/settings.test.ts} +198 -355
- package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
- package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +2 -2
- package/tests/{dialog.test.ts → ui/dialog.test.ts} +5 -4
- package/tests/{overrides.test.ts → ui/overrides.test.ts} +9 -42
- package/tests/utils/credentialResolver.test.ts +66 -0
- package/tests/utils/urlResolver.test.ts +116 -0
- package/src/ui/editors/override/fields.ts +0 -294
- package/src/ui/editors/override/handlers.ts +0 -118
- package/src/ui/editors/server/handlers.ts +0 -32
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { FALLBACK_CTX } from "../constants";
|
|
2
|
+
import { Mode } from "../enums/mode";
|
|
3
|
+
import { Status } from "../enums/status";
|
|
4
|
+
import { BaseModel } from "./baseModel";
|
|
5
|
+
|
|
6
|
+
export class LlamaSwapModel extends BaseModel {
|
|
7
|
+
get mode(): Mode {
|
|
8
|
+
return Mode.LLAMASWAP;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
override get name(): string {
|
|
12
|
+
// llama-swap adds a `llamaswap` key inside `meta` with aliases;
|
|
13
|
+
// fall back to the standard aliases array, then to id
|
|
14
|
+
return (
|
|
15
|
+
this.model.meta?.llamaswap?.aliases?.[0] ??
|
|
16
|
+
this.model.aliases?.[0] ??
|
|
17
|
+
this.model.id
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Retrieves the context size of the model.
|
|
23
|
+
* An override's `contextSize` takes precedence; otherwise reads
|
|
24
|
+
* `meta.n_ctx` from the llama-swap `/v1/models` response,
|
|
25
|
+
* falling back to `FALLBACK_CTX`.
|
|
26
|
+
*
|
|
27
|
+
* @returns The context size
|
|
28
|
+
*/
|
|
29
|
+
protected override async getContextSize(): Promise<number> {
|
|
30
|
+
const overridden = this.server.findOverrideForModel(this.id)?.contextSize;
|
|
31
|
+
if (overridden && overridden > 0) return overridden;
|
|
32
|
+
|
|
33
|
+
const { data } = await this.server.fetchModels();
|
|
34
|
+
const model = data.find((m) => m.id === this.id);
|
|
35
|
+
|
|
36
|
+
return model?.meta?.n_ctx ?? FALLBACK_CTX;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detects the capabilities of the model.
|
|
41
|
+
* An override's `capabilities` fully replaces detection.
|
|
42
|
+
*
|
|
43
|
+
* @returns An array of capabilities, as expected by Pi
|
|
44
|
+
*/
|
|
45
|
+
override async getCapabilities(): Promise<("text" | "image")[]> {
|
|
46
|
+
const overridden = this.server.findOverrideForModel(this.id)?.capabilities;
|
|
47
|
+
if (overridden) return overridden;
|
|
48
|
+
|
|
49
|
+
const { data } = await this.server.fetchModels();
|
|
50
|
+
const model = data.find((m) => m.id === this.id);
|
|
51
|
+
|
|
52
|
+
const inputModalities = model?.architecture?.input_modalities ?? [];
|
|
53
|
+
return inputModalities.includes("image") ? ["text", "image"] : ["text"];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detects the load status of a model.
|
|
58
|
+
* For simplicity, we'll only handle loaded/unloaded
|
|
59
|
+
*
|
|
60
|
+
* @returns The current {@link Status}
|
|
61
|
+
*/
|
|
62
|
+
override async getStatus(): Promise<Status> {
|
|
63
|
+
const { data } = await this.server.fetchModels();
|
|
64
|
+
const model = data.find((m) => m.id === this.id);
|
|
65
|
+
|
|
66
|
+
return model?.status?.value === "loaded" ? Status.LOADED : Status.UNLOADED;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Loads the model in the llama-swap server
|
|
71
|
+
*/
|
|
72
|
+
override async load(): Promise<void> {
|
|
73
|
+
const status = await this.getStatus();
|
|
74
|
+
if (status === Status.LOADED) return;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await this.server.llamaSwapLoad(this.id);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.warn({ err });
|
|
80
|
+
throw new Error(`Model loading failed: ${this.id}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Unloads the model in the llama-swap server
|
|
86
|
+
*/
|
|
87
|
+
override async unload(): Promise<void> {
|
|
88
|
+
await this.server.llamaSwapUnload(this.id);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -26,7 +26,7 @@ export class RouterModel extends BaseModel {
|
|
|
26
26
|
*
|
|
27
27
|
* In exchange, it will allow unloaded models to be correctly shown as "unloaded".
|
|
28
28
|
*/
|
|
29
|
-
protected async pollStatus(startTime = Date.now()): Promise<void> {
|
|
29
|
+
protected override async pollStatus(startTime = Date.now()): Promise<void> {
|
|
30
30
|
let elapsed = 0;
|
|
31
31
|
const limit = 5000;
|
|
32
32
|
|
|
@@ -52,7 +52,7 @@ export class RouterModel extends BaseModel {
|
|
|
52
52
|
*
|
|
53
53
|
* @returns The context size in tokens
|
|
54
54
|
*/
|
|
55
|
-
protected async getContextSize(): Promise<number> {
|
|
55
|
+
protected override async getContextSize(): Promise<number> {
|
|
56
56
|
// We can get a more accurate context size if the model is already loaded
|
|
57
57
|
if ((await this.getStatus()) === Status.LOADED) {
|
|
58
58
|
return super.getContextSize();
|
|
@@ -2,11 +2,11 @@ import { Mode } from "../enums/mode";
|
|
|
2
2
|
import { BaseModel } from "./baseModel";
|
|
3
3
|
|
|
4
4
|
export class SingleModel extends BaseModel {
|
|
5
|
-
get mode(): Mode {
|
|
5
|
+
override get mode(): Mode {
|
|
6
6
|
return Mode.SINGLE;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
protected async getCapabilities(): Promise<("text" | "image")[]> {
|
|
9
|
+
protected override async getCapabilities(): Promise<("text" | "image")[]> {
|
|
10
10
|
try {
|
|
11
11
|
return await super.getCapabilities();
|
|
12
12
|
} catch {
|
package/src/server.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { Mode } from "./enums/mode";
|
|
|
8
8
|
import { ServerStatus } from "./enums/serverStatus";
|
|
9
9
|
import { ModelsEndpoint } from "./interfaces/endpoints/models";
|
|
10
10
|
import {
|
|
11
|
+
LlamaSwapPropsError,
|
|
11
12
|
PropsEndpoint,
|
|
12
13
|
PropsModelEndpoint,
|
|
13
14
|
} from "./interfaces/endpoints/props";
|
|
@@ -16,6 +17,7 @@ import type { ModelOverride } from "./interfaces/settings";
|
|
|
16
17
|
import type { LlamaSettingsManager } from "./managers/settings";
|
|
17
18
|
import { BaseModel } from "./models/baseModel";
|
|
18
19
|
import { LegacyModel } from "./models/legacyModel";
|
|
20
|
+
import { LlamaSwapModel } from "./models/llamaSwapModel";
|
|
19
21
|
import { RouterModel } from "./models/routerModel";
|
|
20
22
|
import { SingleModel } from "./models/singleModel";
|
|
21
23
|
import { SSEManager } from "./sse/manager";
|
|
@@ -52,8 +54,8 @@ export class Server {
|
|
|
52
54
|
// in ServerManager), so no lazy fallback is needed. initialize()
|
|
53
55
|
// rebuilds the client to re-resolve the API key.
|
|
54
56
|
this.apiClient =
|
|
55
|
-
deps.createApiClient?.(this.
|
|
56
|
-
new ApiClient(options.baseUrl, this.
|
|
57
|
+
deps.createApiClient?.(this.apiKey) ??
|
|
58
|
+
new ApiClient(options.baseUrl, this.apiKey);
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/** Base URL of this server endpoint. */
|
|
@@ -119,10 +121,8 @@ export class Server {
|
|
|
119
121
|
/**
|
|
120
122
|
* Retrieves the API key from the config resolver.
|
|
121
123
|
* Tries custom ID first, then falls back to URL-based ID.
|
|
122
|
-
*
|
|
123
|
-
* @returns The API key
|
|
124
124
|
*/
|
|
125
|
-
|
|
125
|
+
get apiKey(): string {
|
|
126
126
|
// Try custom ID first
|
|
127
127
|
if (this.options.customId) {
|
|
128
128
|
const key = this.settings.resolveApiKey(this.options.customId);
|
|
@@ -137,10 +137,9 @@ export class Server {
|
|
|
137
137
|
* Clears the cache first so we always fetch fresh data.
|
|
138
138
|
*/
|
|
139
139
|
async initialize() {
|
|
140
|
-
const apiKey = this.getApiKey();
|
|
141
140
|
this.apiClient =
|
|
142
|
-
this.deps.createApiClient?.(apiKey) ??
|
|
143
|
-
new ApiClient(this.baseUrl, apiKey);
|
|
141
|
+
this.deps.createApiClient?.(this.apiKey) ??
|
|
142
|
+
new ApiClient(this.baseUrl, this.apiKey);
|
|
144
143
|
this.sse = this.deps.createSSEManager?.(this) ?? new SSEManager(this);
|
|
145
144
|
const { data } = await this.fetchModels();
|
|
146
145
|
const mode = await this.detectServerMode(data);
|
|
@@ -150,6 +149,7 @@ export class Server {
|
|
|
150
149
|
[Mode.ROUTER]: RouterModel,
|
|
151
150
|
[Mode.LEGACY]: LegacyModel,
|
|
152
151
|
[Mode.SINGLE]: SingleModel,
|
|
152
|
+
[Mode.LLAMASWAP]: LlamaSwapModel,
|
|
153
153
|
}[mode];
|
|
154
154
|
|
|
155
155
|
const models: BaseModel[] = data.map((m) => new modelCtor(m, this));
|
|
@@ -166,9 +166,12 @@ export class Server {
|
|
|
166
166
|
* @returns The detected mode
|
|
167
167
|
*/
|
|
168
168
|
private async detectServerMode(data: ModelsEndpoint["data"]): Promise<Mode> {
|
|
169
|
-
const
|
|
169
|
+
const serverProps = await this.fetchServerProps();
|
|
170
170
|
|
|
171
|
-
if (
|
|
171
|
+
if ("src" in serverProps && serverProps.src === "llama-swap")
|
|
172
|
+
return Mode.LLAMASWAP;
|
|
173
|
+
if ("role" in serverProps && serverProps.role === "router")
|
|
174
|
+
return Mode.ROUTER;
|
|
172
175
|
if ("max_model_len" in data[0]) return Mode.LEGACY;
|
|
173
176
|
return Mode.SINGLE;
|
|
174
177
|
}
|
|
@@ -185,7 +188,7 @@ export class Server {
|
|
|
185
188
|
* @returns The server status
|
|
186
189
|
*/
|
|
187
190
|
async isReady(timeout: number): Promise<ServerStatus> {
|
|
188
|
-
return checkServerHealth(this.baseUrl, timeout, this.
|
|
191
|
+
return checkServerHealth(this.baseUrl, timeout, this.apiKey);
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
/**
|
|
@@ -200,12 +203,16 @@ export class Server {
|
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
/**
|
|
203
|
-
* Fetches general properties of the server
|
|
206
|
+
* Fetches general properties of the server.
|
|
207
|
+
* llama-server returns {@link PropsEndpoint}; llama-swap returns
|
|
208
|
+
* {@link LlamaSwapPropsError} (an error response with a `src` discriminator).
|
|
204
209
|
*
|
|
205
|
-
* @return The properties
|
|
210
|
+
* @return The server properties
|
|
206
211
|
*/
|
|
207
|
-
async fetchServerProps(): Promise<PropsEndpoint> {
|
|
208
|
-
return await this.apiClient.get<PropsEndpoint>(
|
|
212
|
+
async fetchServerProps(): Promise<PropsEndpoint | LlamaSwapPropsError> {
|
|
213
|
+
return await this.apiClient.get<PropsEndpoint | LlamaSwapPropsError>(
|
|
214
|
+
"/props?autoload=false",
|
|
215
|
+
);
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
/**
|
|
@@ -261,4 +268,24 @@ export class Server {
|
|
|
261
268
|
model,
|
|
262
269
|
});
|
|
263
270
|
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Loads a model on a llama-swap server (GET /upstream/{id}).
|
|
274
|
+
*
|
|
275
|
+
* @param model The model ID to load
|
|
276
|
+
*/
|
|
277
|
+
async llamaSwapLoad(model: string): Promise<void> {
|
|
278
|
+
this.apiClient.clearCache();
|
|
279
|
+
await this.apiClient.rawGet(`/upstream/${model}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Unloads a model on a llama-swap server (POST /api/models/unload/{id}).
|
|
284
|
+
*
|
|
285
|
+
* @param model The model ID to unload
|
|
286
|
+
*/
|
|
287
|
+
async llamaSwapUnload(model: string): Promise<void> {
|
|
288
|
+
this.apiClient.clearCache();
|
|
289
|
+
await this.apiClient.rawPost(`/api/models/unload/${model}`);
|
|
290
|
+
}
|
|
264
291
|
}
|
package/src/sse/manager.ts
CHANGED
|
@@ -46,7 +46,7 @@ export class SSEManager {
|
|
|
46
46
|
try {
|
|
47
47
|
const response = await fetch(this.sseEndpoint, {
|
|
48
48
|
method: "GET",
|
|
49
|
-
headers: buildAuthHeaders(this.server.
|
|
49
|
+
headers: buildAuthHeaders(this.server.apiKey),
|
|
50
50
|
signal: AbortSignal.timeout(await this.server.getServerTimeout()),
|
|
51
51
|
});
|
|
52
52
|
this.sseSupported =
|
|
@@ -78,7 +78,7 @@ export class SSEManager {
|
|
|
78
78
|
this.modelCallbacks.set(modelId, callbacks);
|
|
79
79
|
|
|
80
80
|
// Create SSE client if not already created
|
|
81
|
-
this.sseClient ??= new SSEClient(this.sseEndpoint, this.server.
|
|
81
|
+
this.sseClient ??= new SSEClient(this.sseEndpoint, this.server.apiKey);
|
|
82
82
|
|
|
83
83
|
// Subscribe a single dispatching callback to the SSE client
|
|
84
84
|
if (!this.sseSubscribers.has(modelId)) {
|
package/src/ui/dialog/confirm.ts
CHANGED
package/src/ui/dialog/input.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionUIContext,
|
|
3
|
+
Theme,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
5
|
import type { KeybindingsManager, TUI } from "@earendil-works/pi-tui";
|
|
3
6
|
import type { LlamaServer } from "../../interfaces/settings";
|
|
4
7
|
|
|
@@ -32,6 +35,10 @@ export interface SettingsListEditorOptions {
|
|
|
32
35
|
export interface ServerSettingsListOptions extends SettingsListEditorOptions {
|
|
33
36
|
/** Timeout (ms) for health checks. Defaults to 1000ms if not provided. */
|
|
34
37
|
serverTimeout?: number;
|
|
38
|
+
/** Resolves the API key for a server (used for auth probing). */
|
|
39
|
+
authResolver?: (server: LlamaServer) => Promise<string>;
|
|
40
|
+
/** UI context for notifications (used to warn about auth-required servers). */
|
|
41
|
+
ui?: ExtensionUIContext;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
/**
|
|
@@ -30,7 +30,7 @@ export abstract class ItemBuilder<TEntry, TField extends SettingField<TEntry>> {
|
|
|
30
30
|
description: def.description,
|
|
31
31
|
currentValue: def.currentValue(entry),
|
|
32
32
|
};
|
|
33
|
-
return this.decorate(def,
|
|
33
|
+
return this.decorate(def, base);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** Builds the `SettingItem` array for one entry's editable fields. */
|
|
@@ -39,9 +39,5 @@ export abstract class ItemBuilder<TEntry, TField extends SettingField<TEntry>> {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** Adds field-type-specific columns (e.g. submenu or fixed values). */
|
|
42
|
-
protected abstract decorate(
|
|
43
|
-
def: TField,
|
|
44
|
-
entry: TEntry,
|
|
45
|
-
base: SettingItem,
|
|
46
|
-
): SettingItem;
|
|
42
|
+
protected abstract decorate(def: TField, base: SettingItem): SettingItem;
|
|
47
43
|
}
|
|
@@ -66,9 +66,6 @@ export abstract class ListEditor<
|
|
|
66
66
|
| "emptyServers"
|
|
67
67
|
| "emptyOverrideEntries";
|
|
68
68
|
|
|
69
|
-
/** Total item count (entries or servers) for selection clamping. */
|
|
70
|
-
protected abstract getCurrentCount(): number;
|
|
71
|
-
|
|
72
69
|
/** Returns the id for a row at the given index. */
|
|
73
70
|
protected abstract getRowId(index: number): string;
|
|
74
71
|
|
|
@@ -78,6 +75,12 @@ export abstract class ListEditor<
|
|
|
78
75
|
/** Title for the delete confirmation dialog (e.g. "Delete server"). */
|
|
79
76
|
protected abstract get deleteTitle(): string;
|
|
80
77
|
|
|
78
|
+
/** Returns the number of items in the current list. Override when the
|
|
79
|
+
* list items don't directly correspond to `options.servers`. */
|
|
80
|
+
protected getCount(): number {
|
|
81
|
+
return this.options.servers.length;
|
|
82
|
+
}
|
|
83
|
+
|
|
81
84
|
// -- hooks -----------------------------------------------------------------
|
|
82
85
|
|
|
83
86
|
/**
|
|
@@ -111,10 +114,7 @@ export abstract class ListEditor<
|
|
|
111
114
|
|
|
112
115
|
handleInput(data: string): void {
|
|
113
116
|
// Delegate to the open modal dialog, if any
|
|
114
|
-
if (this.
|
|
115
|
-
this.activeDialog.handleInput(data);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
117
|
+
if (this.delegateToDialogInput(data)) return;
|
|
118
118
|
|
|
119
119
|
// Delegate to settingsList if a submenu is open
|
|
120
120
|
if (this.submenuOpen && this.settingsList) {
|
|
@@ -134,15 +134,11 @@ export abstract class ListEditor<
|
|
|
134
134
|
}
|
|
135
135
|
if (kb.matches(data, "tui.select.up")) {
|
|
136
136
|
this.selectedIndex =
|
|
137
|
-
this.selectedIndex === 0
|
|
138
|
-
? this.getCurrentCount() - 1
|
|
139
|
-
: this.selectedIndex - 1;
|
|
137
|
+
this.selectedIndex === 0 ? this.getCount() - 1 : this.selectedIndex - 1;
|
|
140
138
|
}
|
|
141
139
|
if (kb.matches(data, "tui.select.down")) {
|
|
142
140
|
this.selectedIndex =
|
|
143
|
-
this.selectedIndex === this.
|
|
144
|
-
? 0
|
|
145
|
-
: this.selectedIndex + 1;
|
|
141
|
+
this.selectedIndex === this.getCount() - 1 ? 0 : this.selectedIndex + 1;
|
|
146
142
|
}
|
|
147
143
|
if (data === "a") {
|
|
148
144
|
this.beginAdd();
|
|
@@ -164,7 +160,7 @@ export abstract class ListEditor<
|
|
|
164
160
|
// Empty list hint
|
|
165
161
|
if (!this.settingsList) return ["Loading..."];
|
|
166
162
|
const lines = this.settingsList.render(width);
|
|
167
|
-
if (this.
|
|
163
|
+
if (this.getCount() === 0) {
|
|
168
164
|
lines[lines.length - 1] = getSettingsListTheme().hint(
|
|
169
165
|
HINTS[this.emptyHintKey],
|
|
170
166
|
);
|
|
@@ -174,6 +170,18 @@ export abstract class ListEditor<
|
|
|
174
170
|
|
|
175
171
|
// -- helpers ---------------------------------------------------------------
|
|
176
172
|
|
|
173
|
+
/**
|
|
174
|
+
* If a dialog is open, delegates input to it and returns true;
|
|
175
|
+
* otherwise returns false. Used to short-circuit handleInput.
|
|
176
|
+
*/
|
|
177
|
+
private delegateToDialogInput(data: string): boolean {
|
|
178
|
+
if (this.activeDialog) {
|
|
179
|
+
this.activeDialog.handleInput(data);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
177
185
|
/**
|
|
178
186
|
* Rebuilds the list and places the cursor on `targetIndex`.
|
|
179
187
|
* Subclasses call this after add/delete/persist to refresh the UI.
|
|
@@ -181,9 +189,10 @@ export abstract class ListEditor<
|
|
|
181
189
|
protected async rebuildList(targetIndex: number): Promise<void> {
|
|
182
190
|
const list = await this.buildSettingsList();
|
|
183
191
|
this.settingsList = list;
|
|
184
|
-
const count = this.
|
|
192
|
+
const count = this.getCount();
|
|
185
193
|
if (count === 0) {
|
|
186
194
|
this.selectedIndex = 0;
|
|
195
|
+
this.options.tui.requestRender();
|
|
187
196
|
return;
|
|
188
197
|
}
|
|
189
198
|
this.selectedIndex = Math.min(targetIndex, count - 1);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { SettingItem, SettingsList } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { LlamaServer, ModelOverride } from "../../../interfaces/settings";
|
|
2
3
|
import { FIELDS, FieldMessages, HINTS, TITLES } from "../../strings";
|
|
3
4
|
import type { OverrideSettingsListOptions } from "../editorOptions";
|
|
4
5
|
import { ListEditor } from "../listEditor";
|
|
5
6
|
import { SettingsListFactory } from "../settingsListFactory";
|
|
6
7
|
import { OverrideEntry } from "./entry";
|
|
7
|
-
import { OverrideFields, OverrideSummary } from "./fields";
|
|
8
|
-
import { OverrideEntryMutator } from "./handlers";
|
|
8
|
+
import { OverrideFields, OverrideSummary } from "./fields/index";
|
|
9
9
|
import { OverrideItemBuilder } from "./itemBuilder";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -31,21 +31,21 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
31
31
|
|
|
32
32
|
/** Esc steps back to the server list, not out of the whole dialog —
|
|
33
33
|
* `options.done` is the top-level close (shared options object). */
|
|
34
|
-
protected close(): void {
|
|
34
|
+
protected override close(): void {
|
|
35
35
|
this.done();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// -- abstract hooks -------------------------------------------------------
|
|
39
39
|
|
|
40
|
-
protected buildSettingsList(): SettingsList {
|
|
40
|
+
protected override buildSettingsList(): SettingsList {
|
|
41
41
|
const builder = new OverrideItemBuilder(this.dialogs);
|
|
42
|
-
const items: SettingItem[] = this.
|
|
42
|
+
const items: SettingItem[] = this.entries().map((entry, i) => ({
|
|
43
43
|
id: `entry-${i}`,
|
|
44
44
|
label: entry.pattern,
|
|
45
45
|
description: HINTS.overrideEntryRow,
|
|
46
46
|
currentValue: OverrideSummary.of(entry.override),
|
|
47
47
|
submenu: (_cv, done) => {
|
|
48
|
-
const current = this.
|
|
48
|
+
const current = this.entries()[i] ?? entry;
|
|
49
49
|
const fieldItems = builder.buildFieldItems(current);
|
|
50
50
|
const fieldList = SettingsListFactory.create(
|
|
51
51
|
fieldItems,
|
|
@@ -75,7 +75,7 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
75
75
|
field: string,
|
|
76
76
|
value: string,
|
|
77
77
|
): void {
|
|
78
|
-
const result = this.
|
|
78
|
+
const result = this.applyFieldChange(entryIndex, field, value);
|
|
79
79
|
if (!result) return;
|
|
80
80
|
|
|
81
81
|
this.commitFieldChange(
|
|
@@ -103,7 +103,7 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
103
103
|
);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
protected beginAdd(): void {
|
|
106
|
+
protected override beginAdd(): void {
|
|
107
107
|
this.openDialog(
|
|
108
108
|
this.dialogs.input({
|
|
109
109
|
title: TITLES.addOverride,
|
|
@@ -119,9 +119,9 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
protected deleteSelected(): void {
|
|
122
|
+
protected override deleteSelected(): void {
|
|
123
123
|
const idx = this.selectedIndex;
|
|
124
|
-
const next = this.
|
|
124
|
+
const next = this.removeEntry(idx);
|
|
125
125
|
void this.persistSnapshot(next, () => {
|
|
126
126
|
this.rebuildList(idx);
|
|
127
127
|
this.options.onChanged?.();
|
|
@@ -129,36 +129,118 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
protected readonly emptyHintKey = "emptyOverrideEntries" as const;
|
|
132
|
+
protected override readonly emptyHintKey = "emptyOverrideEntries" as const;
|
|
133
133
|
|
|
134
|
-
protected
|
|
135
|
-
return this.
|
|
134
|
+
protected override getCount(): number {
|
|
135
|
+
return this.entries().length;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
protected getRowId(index: number): string {
|
|
138
|
+
protected override getRowId(index: number): string {
|
|
139
139
|
return `entry-${index}`;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
protected getRowLabel(index: number): string {
|
|
143
|
-
return this.
|
|
142
|
+
protected override getRowLabel(index: number): string {
|
|
143
|
+
return this.entries()[index]?.pattern ?? "";
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
protected get deleteTitle(): string {
|
|
146
|
+
protected override get deleteTitle(): string {
|
|
147
147
|
return TITLES.deleteOverride;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// -- helpers ---------------------------------------------------------------
|
|
151
151
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
/** The server's override entries, in map iteration order. */
|
|
153
|
+
private entries(): OverrideEntry[] {
|
|
154
|
+
return OverrideEntry.listFrom(this.options.servers[this.serverIndex]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Handles a field commit from an override entry's submenu. Returns
|
|
158
|
+
* `null` when the entry doesn't exist (stale index — no-op). */
|
|
159
|
+
private applyFieldChange(
|
|
160
|
+
entryIndex: number,
|
|
161
|
+
field: string,
|
|
162
|
+
value: string,
|
|
163
|
+
): { next: LlamaServer[]; updatedOverride: ModelOverride } | null {
|
|
164
|
+
const entry = this.entries()[entryIndex];
|
|
165
|
+
if (!entry) return null;
|
|
166
|
+
|
|
167
|
+
// Pattern field: rename the pattern key, keep the override object
|
|
168
|
+
if (field === "pattern") {
|
|
169
|
+
return {
|
|
170
|
+
next: this.updateEntry(entryIndex, value, entry.override),
|
|
171
|
+
updatedOverride: entry.override,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Other fields: let the field definition apply itself
|
|
176
|
+
const updatedOverride: ModelOverride = { ...entry.override };
|
|
177
|
+
OverrideFields.byId(field).apply(updatedOverride, value);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
next: this.updateEntry(entryIndex, entry.pattern, updatedOverride),
|
|
181
|
+
updatedOverride,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Adds a new override entry with a uniquified pattern. */
|
|
186
|
+
private addEntry(pattern: string): LlamaServer[] {
|
|
187
|
+
const server = this.options.servers[this.serverIndex];
|
|
188
|
+
if (!server) return this.options.servers;
|
|
189
|
+
|
|
190
|
+
const existing = new Set(Object.keys(server.overrides ?? {}));
|
|
191
|
+
let uniquePattern = pattern;
|
|
192
|
+
let n = 2;
|
|
193
|
+
while (existing.has(uniquePattern)) {
|
|
194
|
+
uniquePattern = `${pattern}-${n++}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return this.withServer((s) => ({
|
|
198
|
+
...s,
|
|
199
|
+
overrides: { ...s.overrides, [uniquePattern]: {} },
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Removes the entry at `entryIndex`. */
|
|
204
|
+
private removeEntry(entryIndex: number): LlamaServer[] {
|
|
205
|
+
return this.withServer((s) => ({
|
|
206
|
+
...s,
|
|
207
|
+
overrides: Object.fromEntries(
|
|
208
|
+
Object.entries(s.overrides ?? {}).filter((_, j) => j !== entryIndex),
|
|
209
|
+
),
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Returns the servers with the target server replaced by
|
|
214
|
+
* `fn(server)`. Immutable. */
|
|
215
|
+
private withServer(fn: (server: LlamaServer) => LlamaServer): LlamaServer[] {
|
|
216
|
+
return this.options.servers.map((server, i) =>
|
|
217
|
+
i === this.serverIndex ? fn(server) : server,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Replaces the entry at `entryIndex` with `pattern → override` in a
|
|
222
|
+
* single mutation — used both for renaming the pattern and for editing
|
|
223
|
+
* its fields. The entry keeps its position in the map's iteration
|
|
224
|
+
* order. Immutable. */
|
|
225
|
+
private updateEntry(
|
|
226
|
+
entryIndex: number,
|
|
227
|
+
pattern: string,
|
|
228
|
+
override: ModelOverride,
|
|
229
|
+
): LlamaServer[] {
|
|
230
|
+
return this.withServer((s) => ({
|
|
231
|
+
...s,
|
|
232
|
+
overrides: Object.fromEntries(
|
|
233
|
+
Object.entries(s.overrides ?? {}).map(([k, v], j) =>
|
|
234
|
+
j === entryIndex ? [pattern, override] : [k, v],
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
}));
|
|
156
238
|
}
|
|
157
239
|
|
|
158
240
|
private async saveAdd(pattern: string): Promise<void> {
|
|
159
|
-
const next = this.
|
|
241
|
+
const next = this.addEntry(pattern);
|
|
160
242
|
await this.persistSnapshot(next, () => {
|
|
161
|
-
this.rebuildList(this.
|
|
243
|
+
this.rebuildList(this.entries().length - 1);
|
|
162
244
|
this.options.onChanged?.();
|
|
163
245
|
this.options.tui.requestRender();
|
|
164
246
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ModelOverride } from "../../../../interfaces/settings";
|
|
2
|
+
import { SettingField } from "../../settingField";
|
|
3
|
+
import { OverrideEntry } from "../entry";
|
|
4
|
+
|
|
5
|
+
/** Type of a field: "input" for free-text, "finite" for fixed options */
|
|
6
|
+
export type FieldType = "input" | "finite";
|
|
7
|
+
|
|
8
|
+
/** Validation function for input fields */
|
|
9
|
+
export type InputValidator = (raw: string) => string | null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Abstract base class for one editable field of a model override entry
|
|
13
|
+
* (Template Method): each subclass owns its display format, validation
|
|
14
|
+
* and apply logic in a single place, so the parse/format round-trips
|
|
15
|
+
* can't drift. Extends the shared {@link SettingField} base (id, label,
|
|
16
|
+
* description, placeholder from the FIELDS registry).
|
|
17
|
+
*/
|
|
18
|
+
export abstract class OverrideField extends SettingField<OverrideEntry> {
|
|
19
|
+
/** Field type: "input" for free-text, "finite" for fixed options. */
|
|
20
|
+
abstract readonly type: FieldType;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Canonical display label for this field — the single source of truth
|
|
24
|
+
* for both the field rows' `currentValue` and the post-commit refresh
|
|
25
|
+
* of an open field submenu, so the two can't drift.
|
|
26
|
+
*/
|
|
27
|
+
abstract displayValue(entry: OverrideEntry): string;
|
|
28
|
+
|
|
29
|
+
/** Current value shown in the row — defaults to `displayValue`. */
|
|
30
|
+
currentValue(entry: OverrideEntry): string {
|
|
31
|
+
return this.displayValue(entry);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** For "input" fields: validation function for committed values. */
|
|
35
|
+
abstract readonly validate: InputValidator;
|
|
36
|
+
|
|
37
|
+
/** For "finite" fields: the fixed options (undefined for "input"). */
|
|
38
|
+
readonly options: readonly string[] | undefined = undefined;
|
|
39
|
+
|
|
40
|
+
/** Applies a committed value to the override object. The pattern key
|
|
41
|
+
* rename is handled separately by the mutator (default: no-op). */
|
|
42
|
+
apply(_override: ModelOverride, _value: string): void {
|
|
43
|
+
// no-op — override in subclasses that need to mutate the override
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One `term: value` part of the entry-row summary (see
|
|
47
|
+
* {@link OverrideSummary.of}), or `null` when the field shouldn't
|
|
48
|
+
* appear (unset/zero values — each field decides). The pattern is the
|
|
49
|
+
* row label, so it never contributes a part; the default is `null`. */
|
|
50
|
+
summaryPart(_override: ModelOverride): string | null {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|