pi-llama-cpp 0.12.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 +20 -9
- package/package.json +3 -3
- package/src/api/client.ts +25 -0
- package/src/constants.ts +8 -5
- package/src/enums/status.ts +0 -1
- package/src/interfaces/endpoints/models.ts +1 -1
- package/src/interfaces/settings.ts +3 -2
- package/src/interfaces/sortBy.ts +4 -0
- package/src/managers/command/models.ts +247 -0
- package/src/managers/command.ts +29 -459
- package/src/managers/events.ts +2 -2
- package/src/managers/server.ts +29 -5
- 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 +35 -48
- 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/health.ts +48 -0
- 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 +38 -7
- package/tests/dialog.test.ts +95 -2
- package/tests/health.test.ts +116 -0
- package/tests/legacyModel.test.ts +34 -28
- package/tests/overrides.test.ts +123 -68
- package/tests/routerModel.test.ts +67 -68
- package/tests/server.test.ts +47 -16
- package/tests/serverManager.test.ts +4 -4
- 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 -287
- package/src/ui/overrideEntryEditor.ts +0 -119
- package/src/ui/overrideSettingsList.ts +0 -682
- package/src/ui/serverListEditor.ts +0 -32
- package/src/ui/serverSettingsList.ts +0 -466
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import { SettingsList } from "@earendil-works/pi-tui";
|
|
4
|
+
import type { LlamaServer } from "../../interfaces/settings";
|
|
5
|
+
import { BaseDialog } from "../dialog/base";
|
|
6
|
+
import { DialogFactory } from "../dialog/factory";
|
|
7
|
+
import { HINTS } from "../strings";
|
|
8
|
+
import type { SettingsListEditorOptions } from "./editorOptions";
|
|
9
|
+
|
|
10
|
+
// ─── Abstract base class ──────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base class for SettingsList-based editors that manage a list of servers
|
|
14
|
+
* with add/delete support at the top level.
|
|
15
|
+
*
|
|
16
|
+
* Subclasses must implement:
|
|
17
|
+
* - {@link buildSettingsList} — how to build the top-level list items
|
|
18
|
+
* - {@link beginAdd} — how to start the add wizard
|
|
19
|
+
* - {@link deleteSelected} — how to delete the selected entry
|
|
20
|
+
* - {@link getRowLabel} — display name for a row at the given index
|
|
21
|
+
* - {@link emptyHintKey} — which hint to show when the list is empty
|
|
22
|
+
* - {@link deleteTitle} — dialog title for the delete confirmation
|
|
23
|
+
*/
|
|
24
|
+
export abstract class ListEditor<
|
|
25
|
+
O extends SettingsListEditorOptions = SettingsListEditorOptions,
|
|
26
|
+
> implements Component {
|
|
27
|
+
protected settingsList: SettingsList | null = null;
|
|
28
|
+
protected isFocused = false;
|
|
29
|
+
/** The currently open modal dialog (add wizard, input, or confirm),
|
|
30
|
+
* when one is up: input/render/invalidate/focus all delegate to it
|
|
31
|
+
* until it is cleared via {@link closeDialog}. */
|
|
32
|
+
protected activeDialog: BaseDialog | undefined;
|
|
33
|
+
/** Dialog factory holding `theme`/`tui` once for all dialog construction. */
|
|
34
|
+
protected readonly dialogs: DialogFactory;
|
|
35
|
+
/** Whether a row's field submenu is open. While open, input is delegated
|
|
36
|
+
* to the settingsList and list rebuilds are deferred: rebuilding would
|
|
37
|
+
* discard the submenu and kick the user back to the list. */
|
|
38
|
+
protected submenuOpen = false;
|
|
39
|
+
/** Row index of a rebuild deferred by {@link commitFieldChange} while a
|
|
40
|
+
* submenu was open; flushed by {@link trackSubmenu} on close. */
|
|
41
|
+
private deferredRebuild: number | null = null;
|
|
42
|
+
/** Selected row index. Mirrors the `SettingsList`'s selection, which is
|
|
43
|
+
* `private` in pi-tui's typings and thus unreachable — so this copy is
|
|
44
|
+
* advanced by the same wrap-around logic the list applies internally
|
|
45
|
+
* (both handle the same up/down keypress). Kept in sync explicitly in
|
|
46
|
+
* {@link rebuildList} via `selectItem`. */
|
|
47
|
+
protected selectedIndex = 0;
|
|
48
|
+
|
|
49
|
+
constructor(protected readonly options: O) {
|
|
50
|
+
this.dialogs = new DialogFactory(options.theme, options.tui);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// -- abstract hooks -------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/** Build the top-level SettingsList from the current data snapshot. */
|
|
56
|
+
protected abstract buildSettingsList(): SettingsList | Promise<SettingsList>;
|
|
57
|
+
|
|
58
|
+
/** Start the add wizard (called when user presses "a"). */
|
|
59
|
+
protected abstract beginAdd(): void;
|
|
60
|
+
|
|
61
|
+
/** Delete the currently selected entry (called after confirmation). */
|
|
62
|
+
protected abstract deleteSelected(): void;
|
|
63
|
+
|
|
64
|
+
/** Hint line key for the empty-list state. */
|
|
65
|
+
protected abstract readonly emptyHintKey:
|
|
66
|
+
| "emptyServers"
|
|
67
|
+
| "emptyOverrideEntries";
|
|
68
|
+
|
|
69
|
+
/** Total item count (entries or servers) for selection clamping. */
|
|
70
|
+
protected abstract getCurrentCount(): number;
|
|
71
|
+
|
|
72
|
+
/** Returns the id for a row at the given index. */
|
|
73
|
+
protected abstract getRowId(index: number): string;
|
|
74
|
+
|
|
75
|
+
/** Returns the display label for a row at the given index. */
|
|
76
|
+
protected abstract getRowLabel(index: number): string;
|
|
77
|
+
|
|
78
|
+
/** Title for the delete confirmation dialog (e.g. "Delete server"). */
|
|
79
|
+
protected abstract get deleteTitle(): string;
|
|
80
|
+
|
|
81
|
+
// -- hooks -----------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Closes this editor on Esc. The base implementation ends the whole
|
|
85
|
+
* dialog (`options.done`) — correct for top-level editors. Drill-down
|
|
86
|
+
* subclasses that live inside another editor's submenu override this
|
|
87
|
+
* to step back one level instead (their `options.done` still points
|
|
88
|
+
* at the top-level close inherited through the shared options).
|
|
89
|
+
*/
|
|
90
|
+
protected close(): void {
|
|
91
|
+
this.options.done();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// -- Focusable ------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
get focused(): boolean {
|
|
97
|
+
return this.isFocused;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
set focused(value: boolean) {
|
|
101
|
+
this.isFocused = value;
|
|
102
|
+
if (this.activeDialog) this.activeDialog.focused = value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// -- Component ------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
invalidate(): void {
|
|
108
|
+
this.settingsList?.invalidate();
|
|
109
|
+
this.activeDialog?.invalidate();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
handleInput(data: string): void {
|
|
113
|
+
// Delegate to the open modal dialog, if any
|
|
114
|
+
if (this.activeDialog) {
|
|
115
|
+
this.activeDialog.handleInput(data);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Delegate to settingsList if a submenu is open
|
|
120
|
+
if (this.submenuOpen && this.settingsList) {
|
|
121
|
+
this.settingsList.handleInput(data);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Top-level keybindings. Up/down mirror the wrap-around the
|
|
126
|
+
// `SettingsList` applies internally to the same keypress (its
|
|
127
|
+
// selection is private — see the {@link selectedIndex} doc); the
|
|
128
|
+
// trailing forward to `settingsList.handleInput` is what actually
|
|
129
|
+
// moves the rendered cursor.
|
|
130
|
+
const kb = this.options.keybindings;
|
|
131
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
132
|
+
this.close();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (kb.matches(data, "tui.select.up")) {
|
|
136
|
+
this.selectedIndex =
|
|
137
|
+
this.selectedIndex === 0
|
|
138
|
+
? this.getCurrentCount() - 1
|
|
139
|
+
: this.selectedIndex - 1;
|
|
140
|
+
}
|
|
141
|
+
if (kb.matches(data, "tui.select.down")) {
|
|
142
|
+
this.selectedIndex =
|
|
143
|
+
this.selectedIndex === this.getCurrentCount() - 1
|
|
144
|
+
? 0
|
|
145
|
+
: this.selectedIndex + 1;
|
|
146
|
+
}
|
|
147
|
+
if (data === "a") {
|
|
148
|
+
this.beginAdd();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (data === "d") {
|
|
152
|
+
this.beginConfirm();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (this.settingsList) {
|
|
156
|
+
this.settingsList.handleInput(data);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
render(width: number): string[] {
|
|
161
|
+
// Delegate to the open modal dialog, if any
|
|
162
|
+
if (this.activeDialog) return this.activeDialog.render(width);
|
|
163
|
+
|
|
164
|
+
// Empty list hint
|
|
165
|
+
if (!this.settingsList) return ["Loading..."];
|
|
166
|
+
const lines = this.settingsList.render(width);
|
|
167
|
+
if (this.getCurrentCount() === 0) {
|
|
168
|
+
lines[lines.length - 1] = getSettingsListTheme().hint(
|
|
169
|
+
HINTS[this.emptyHintKey],
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return lines;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// -- helpers ---------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Rebuilds the list and places the cursor on `targetIndex`.
|
|
179
|
+
* Subclasses call this after add/delete/persist to refresh the UI.
|
|
180
|
+
*/
|
|
181
|
+
protected async rebuildList(targetIndex: number): Promise<void> {
|
|
182
|
+
const list = await this.buildSettingsList();
|
|
183
|
+
this.settingsList = list;
|
|
184
|
+
const count = this.getCurrentCount();
|
|
185
|
+
if (count === 0) {
|
|
186
|
+
this.selectedIndex = 0;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.selectedIndex = Math.min(targetIndex, count - 1);
|
|
190
|
+
this.settingsList.selectItem(this.getRowId(this.selectedIndex));
|
|
191
|
+
// Rebuilding is async (health checks); request a render only once the
|
|
192
|
+
// new list is in place, otherwise the old frame stays on screen
|
|
193
|
+
this.options.tui.requestRender();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Tracks a row's field submenu opening or closing. While a submenu is
|
|
198
|
+
* open, {@link rebuildList} would discard it and kick the user back to
|
|
199
|
+
* the list — so a rebuild deferred by {@link commitFieldChange} is
|
|
200
|
+
* flushed here on close instead of running mid-edit.
|
|
201
|
+
*/
|
|
202
|
+
protected trackSubmenu(open: boolean): void {
|
|
203
|
+
this.submenuOpen = open;
|
|
204
|
+
if (open) return;
|
|
205
|
+
const index = this.deferredRebuild;
|
|
206
|
+
this.deferredRebuild = null;
|
|
207
|
+
if (index !== null) void this.rebuildList(index);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Opens a modal dialog: input/render delegate to it until
|
|
212
|
+
* {@link closeDialog}. Focus follows the editor's.
|
|
213
|
+
*/
|
|
214
|
+
protected openDialog(dialog: BaseDialog): void {
|
|
215
|
+
this.activeDialog = dialog;
|
|
216
|
+
dialog.focused = this.isFocused;
|
|
217
|
+
this.options.tui.requestRender();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Closes the open modal dialog and returns to the list. */
|
|
221
|
+
protected closeDialog(): void {
|
|
222
|
+
this.activeDialog = undefined;
|
|
223
|
+
this.options.tui.requestRender();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Opens the delete confirmation dialog.
|
|
228
|
+
*/
|
|
229
|
+
protected beginConfirm(): void {
|
|
230
|
+
const idx = this.selectedIndex;
|
|
231
|
+
const name = this.getRowLabel(idx);
|
|
232
|
+
if (!name) return;
|
|
233
|
+
|
|
234
|
+
this.openDialog(
|
|
235
|
+
this.dialogs.confirmDelete(
|
|
236
|
+
this.deleteTitle,
|
|
237
|
+
name,
|
|
238
|
+
() => {
|
|
239
|
+
this.closeDialog();
|
|
240
|
+
void this.deleteSelected();
|
|
241
|
+
},
|
|
242
|
+
() => this.closeDialog(),
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Persists `next`; on success adopts it and runs `onSuccess`;
|
|
249
|
+
* on failure notifies and stays in list mode.
|
|
250
|
+
*
|
|
251
|
+
* Callers must close any open dialog *before* the async persist:
|
|
252
|
+
* render() returns the list once `activeDialog` is cleared, so
|
|
253
|
+
* leaving the dialog open would blank the screen mid-save.
|
|
254
|
+
*/
|
|
255
|
+
protected async persistSnapshot(
|
|
256
|
+
next: LlamaServer[],
|
|
257
|
+
onSuccess: () => void,
|
|
258
|
+
): Promise<void> {
|
|
259
|
+
try {
|
|
260
|
+
await this.options.persist(next);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
this.options.onError(String(err));
|
|
263
|
+
this.options.tui.requestRender();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this.options.servers = next;
|
|
267
|
+
onSuccess();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Handles a field commit from an open row submenu: persists `next` and,
|
|
272
|
+
* on success, refreshes the open submenu UI in place via `refresh`, then
|
|
273
|
+
* defers the full list rebuild of row `deferIndex` until the submenu
|
|
274
|
+
* closes (rebuilding now would discard the submenu and kick the user
|
|
275
|
+
* back to the list — see {@link trackSubmenu}). No-op when
|
|
276
|
+
* `next` is the current list (unchanged, callers compare by reference).
|
|
277
|
+
*/
|
|
278
|
+
protected commitFieldChange(
|
|
279
|
+
next: LlamaServer[],
|
|
280
|
+
deferIndex: number | null,
|
|
281
|
+
refresh: () => void,
|
|
282
|
+
): void {
|
|
283
|
+
if (next === this.options.servers) return;
|
|
284
|
+
void this.persistSnapshot(next, () => {
|
|
285
|
+
refresh();
|
|
286
|
+
if (deferIndex !== null && this.submenuOpen)
|
|
287
|
+
this.deferredRebuild = deferIndex;
|
|
288
|
+
this.options.tui.requestRender();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LlamaServer, ModelOverride } from "../../../interfaces/settings";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One override entry: the pattern key and its override object. Owns the
|
|
5
|
+
* canonical construction from a server's overrides map, so the editor
|
|
6
|
+
* and the mutator share a single entry representation. Row rendering
|
|
7
|
+
* (summary) is registry-driven — see `OverrideSummary`.
|
|
8
|
+
*/
|
|
9
|
+
export class OverrideEntry {
|
|
10
|
+
constructor(
|
|
11
|
+
readonly pattern: string,
|
|
12
|
+
readonly override: ModelOverride,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Entries of a server's overrides map, in map iteration order (the
|
|
17
|
+
* order the entry list renders and mutates by).
|
|
18
|
+
*/
|
|
19
|
+
static listFrom(server: LlamaServer | undefined): OverrideEntry[] {
|
|
20
|
+
return Object.entries(server?.overrides ?? {}).map(
|
|
21
|
+
([pattern, override]) => new OverrideEntry(pattern, override),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { SettingItem, SettingsList } from "@earendil-works/pi-tui";
|
|
2
|
+
import { FIELDS, FieldMessages, HINTS, TITLES } from "../../strings";
|
|
3
|
+
import type { OverrideSettingsListOptions } from "../editorOptions";
|
|
4
|
+
import { ListEditor } from "../listEditor";
|
|
5
|
+
import { SettingsListFactory } from "../settingsListFactory";
|
|
6
|
+
import { OverrideEntry } from "./entry";
|
|
7
|
+
import { OverrideFields, OverrideSummary } from "./fields";
|
|
8
|
+
import { OverrideEntryMutator } from "./handlers";
|
|
9
|
+
import { OverrideItemBuilder } from "./itemBuilder";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The override entries of a single server, as a list view with add/delete.
|
|
13
|
+
*/
|
|
14
|
+
export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOptions> {
|
|
15
|
+
/** Currently open field submenu, so commits can refresh its rows */
|
|
16
|
+
private fieldList: SettingsList | null = null;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
/** Shared options object — `persistSnapshot` updates `options.servers`
|
|
20
|
+
* in place so later drill-downs (and sibling editors) see the change */
|
|
21
|
+
options: OverrideSettingsListOptions,
|
|
22
|
+
private readonly serverIndex: number,
|
|
23
|
+
/** Closes this editor (called on Esc in the entry list) */
|
|
24
|
+
private readonly done: () => void,
|
|
25
|
+
) {
|
|
26
|
+
super(options);
|
|
27
|
+
this.settingsList = this.buildSettingsList();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// -- hooks ---------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/** Esc steps back to the server list, not out of the whole dialog —
|
|
33
|
+
* `options.done` is the top-level close (shared options object). */
|
|
34
|
+
protected close(): void {
|
|
35
|
+
this.done();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// -- abstract hooks -------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
protected buildSettingsList(): SettingsList {
|
|
41
|
+
const builder = new OverrideItemBuilder(this.dialogs);
|
|
42
|
+
const items: SettingItem[] = this.mutator.entries().map((entry, i) => ({
|
|
43
|
+
id: `entry-${i}`,
|
|
44
|
+
label: entry.pattern,
|
|
45
|
+
description: HINTS.overrideEntryRow,
|
|
46
|
+
currentValue: OverrideSummary.of(entry.override),
|
|
47
|
+
submenu: (_cv, done) => {
|
|
48
|
+
const current = this.mutator.entries()[i] ?? entry;
|
|
49
|
+
const fieldItems = builder.buildFieldItems(current);
|
|
50
|
+
const fieldList = SettingsListFactory.create(
|
|
51
|
+
fieldItems,
|
|
52
|
+
() => {
|
|
53
|
+
this.fieldList = null;
|
|
54
|
+
done();
|
|
55
|
+
// Flushes any rebuild deferred by a pattern rename
|
|
56
|
+
// (see commitFieldChange)
|
|
57
|
+
this.trackSubmenu(false);
|
|
58
|
+
},
|
|
59
|
+
(field, value) => void this.handleFieldChange(i, field, value),
|
|
60
|
+
);
|
|
61
|
+
this.fieldList = fieldList;
|
|
62
|
+
this.trackSubmenu(true);
|
|
63
|
+
return fieldList;
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
return SettingsListFactory.create(items, () => {
|
|
68
|
+
this.done();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Handle a field commit from a row's submenu. */
|
|
73
|
+
private handleFieldChange(
|
|
74
|
+
entryIndex: number,
|
|
75
|
+
field: string,
|
|
76
|
+
value: string,
|
|
77
|
+
): void {
|
|
78
|
+
const result = this.mutator.applyFieldChange(entryIndex, field, value);
|
|
79
|
+
if (!result) return;
|
|
80
|
+
|
|
81
|
+
this.commitFieldChange(
|
|
82
|
+
result.next,
|
|
83
|
+
// The entry row's label is the pattern, and SettingsList labels
|
|
84
|
+
// can't be patched in place — defer the rebuild until the submenu
|
|
85
|
+
// closes so the user stays inside it (see commitFieldChange)
|
|
86
|
+
field === "pattern" ? entryIndex : null,
|
|
87
|
+
() => {
|
|
88
|
+
// Refresh the entry row's summary in place
|
|
89
|
+
this.settingsList?.updateValue(
|
|
90
|
+
`entry-${entryIndex}`,
|
|
91
|
+
OverrideSummary.of(result.updatedOverride),
|
|
92
|
+
);
|
|
93
|
+
// Refresh the committed row in the open field submenu. Passing
|
|
94
|
+
// `value` as the pattern makes the pattern field display the new
|
|
95
|
+
// pattern; other fields only read the override.
|
|
96
|
+
this.fieldList?.updateValue(
|
|
97
|
+
field,
|
|
98
|
+
OverrideFields.byId(field).displayValue(
|
|
99
|
+
new OverrideEntry(value, result.updatedOverride),
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected beginAdd(): void {
|
|
107
|
+
this.openDialog(
|
|
108
|
+
this.dialogs.input({
|
|
109
|
+
title: TITLES.addOverride,
|
|
110
|
+
message: FieldMessages.of(FIELDS.pattern),
|
|
111
|
+
placeholder: FIELDS.pattern.placeholder,
|
|
112
|
+
validate: OverrideFields.byId("pattern").validate,
|
|
113
|
+
onSubmit: (value) => {
|
|
114
|
+
this.closeDialog();
|
|
115
|
+
void this.saveAdd(value);
|
|
116
|
+
},
|
|
117
|
+
onCancel: () => this.closeDialog(),
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected deleteSelected(): void {
|
|
123
|
+
const idx = this.selectedIndex;
|
|
124
|
+
const next = this.mutator.removeEntry(idx);
|
|
125
|
+
void this.persistSnapshot(next, () => {
|
|
126
|
+
this.rebuildList(idx);
|
|
127
|
+
this.options.onChanged?.();
|
|
128
|
+
this.options.tui.requestRender();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected readonly emptyHintKey = "emptyOverrideEntries" as const;
|
|
133
|
+
|
|
134
|
+
protected getCurrentCount(): number {
|
|
135
|
+
return this.mutator.entries().length;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected getRowId(index: number): string {
|
|
139
|
+
return `entry-${index}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
protected getRowLabel(index: number): string {
|
|
143
|
+
return this.mutator.entries()[index]?.pattern ?? "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
protected get deleteTitle(): string {
|
|
147
|
+
return TITLES.deleteOverride;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// -- helpers ---------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/** Fresh mutator over the current servers snapshot — `persistSnapshot`
|
|
153
|
+
* replaces `options.servers` in place, so instances can't be cached. */
|
|
154
|
+
private get mutator(): OverrideEntryMutator {
|
|
155
|
+
return new OverrideEntryMutator(this.options.servers, this.serverIndex);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async saveAdd(pattern: string): Promise<void> {
|
|
159
|
+
const next = this.mutator.addEntry(pattern);
|
|
160
|
+
await this.persistSnapshot(next, () => {
|
|
161
|
+
this.rebuildList(this.mutator.entries().length - 1);
|
|
162
|
+
this.options.onChanged?.();
|
|
163
|
+
this.options.tui.requestRender();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|