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,59 +0,0 @@
|
|
|
1
|
-
import { PROVIDER_PREFIX } from "../constants";
|
|
2
|
-
import { ServerStatus } from "../enums/serverStatus";
|
|
3
|
-
import type { LlamaServer } from "../interfaces/settings";
|
|
4
|
-
import { checkServerHealth } from "../utils/health";
|
|
5
|
-
import { normalizeUrl } from "../utils/urls";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Validates and normalizes a user-entered server URL: trims whitespace and
|
|
9
|
-
* strips trailing slashes (shared core in `utils/urls` — the same treatment
|
|
10
|
-
* the settings parser applies), then rejects empty strings, semicolons (the
|
|
11
|
-
* settings parser splits on them — use separate entries) and values without
|
|
12
|
-
* an http(s) scheme.
|
|
13
|
-
*
|
|
14
|
-
* @returns The normalized URL, or `null` when the input is invalid
|
|
15
|
-
*/
|
|
16
|
-
export const normalizeServerUrl = (raw: string): string | null => {
|
|
17
|
-
const url = normalizeUrl(raw);
|
|
18
|
-
if (url.length === 0 || url.includes(";") || !/^https?:\/\//i.test(url)) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
return url;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Formats the dim `(...)` suffix shown after a server's URL:
|
|
26
|
-
* `(<id> - <name>)`, with the auto-detected URL-based id used when no
|
|
27
|
-
* custom id override exists (mirrors `Server.providerId`). Returns the
|
|
28
|
-
* empty string when there is nothing to show.
|
|
29
|
-
*/
|
|
30
|
-
export const formatServerSuffix = (server: LlamaServer): string => {
|
|
31
|
-
if (!server.id && !server.name) return "";
|
|
32
|
-
const id = server.id ?? `${PROVIDER_PREFIX}=${server.url}`;
|
|
33
|
-
return server.name ? `(${id} - ${server.name})` : `(${server.id})`;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Emoji indicators for server health status.
|
|
38
|
-
*/
|
|
39
|
-
const SERVER_STATUS_ICONS: Record<ServerStatus, string> = {
|
|
40
|
-
[ServerStatus.READY]: "🟢",
|
|
41
|
-
[ServerStatus.TIMEOUT]: "🟡",
|
|
42
|
-
[ServerStatus.UNREACHABLE]: "🔴",
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Checks the health of a server and returns the corresponding emoji.
|
|
47
|
-
*
|
|
48
|
-
* Delegates the probe/classification to the shared `checkServerHealth`
|
|
49
|
-
* (`utils/health`) — this wrapper only maps the status to its icon.
|
|
50
|
-
*
|
|
51
|
-
* @param url - The server URL to check
|
|
52
|
-
* @param timeout - Maximum time (ms) to wait for the health check
|
|
53
|
-
* @returns The health emoji for the server status
|
|
54
|
-
*/
|
|
55
|
-
export const getServerHealthEmoji = async (
|
|
56
|
-
url: string,
|
|
57
|
-
timeout: number,
|
|
58
|
-
): Promise<string> =>
|
|
59
|
-
SERVER_STATUS_ICONS[await checkServerHealth(url, timeout)];
|
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getSettingsListTheme,
|
|
3
|
-
type Theme,
|
|
4
|
-
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type {
|
|
6
|
-
Component,
|
|
7
|
-
Focusable,
|
|
8
|
-
KeybindingsManager,
|
|
9
|
-
TUI,
|
|
10
|
-
} from "@earendil-works/pi-tui";
|
|
11
|
-
import { SettingsList, type SettingItem } from "@earendil-works/pi-tui";
|
|
12
|
-
import type { LlamaServer } from "../interfaces/settings";
|
|
13
|
-
import { ConfirmDialog, InputDialog } from "./dialog";
|
|
14
|
-
import {
|
|
15
|
-
formatServerSuffix,
|
|
16
|
-
getServerHealthEmoji,
|
|
17
|
-
normalizeServerUrl,
|
|
18
|
-
} from "./serverListEditor";
|
|
19
|
-
import { fieldMessage, FIELDS, HINTS, TITLES } from "./strings";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Options for the SettingsList-based server editor.
|
|
23
|
-
*/
|
|
24
|
-
export interface ServerSettingsListOptions {
|
|
25
|
-
/** TUI instance, used to request re-renders after async persists */
|
|
26
|
-
tui: TUI;
|
|
27
|
-
/** Theme for dialogs (from the ctx.ui.custom factory) */
|
|
28
|
-
theme: Theme;
|
|
29
|
-
/** App keybindings manager (injected by ctx.ui.custom) */
|
|
30
|
-
keybindings: KeybindingsManager;
|
|
31
|
-
/**
|
|
32
|
-
* Snapshot of the merged `llamaSettings.servers` to edit. Replaced
|
|
33
|
-
* in place (`options.servers = next`) after every successful persist
|
|
34
|
-
* so the rebuilt rows reflect the adopted list.
|
|
35
|
-
*/
|
|
36
|
-
servers: LlamaServer[];
|
|
37
|
-
/** Persists a new server list; a rejection keeps the current list */
|
|
38
|
-
persist: (next: LlamaServer[]) => Promise<void>;
|
|
39
|
-
/** Closes the editor (called on Esc in the server list) */
|
|
40
|
-
done: () => void;
|
|
41
|
-
/** Notifies about persistence errors */
|
|
42
|
-
onError: (message: string) => void;
|
|
43
|
-
/** Timeout (ms) for health checks. Defaults to 1000ms if not provided. */
|
|
44
|
-
serverTimeout?: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Builds the SettingItem for one server's editable fields (URL, id, name).
|
|
49
|
-
* Each field is infinite → opens an `InputDialog` submenu; commits are
|
|
50
|
-
* reported by the containing `SettingsList`'s `onChange` (field id → value).
|
|
51
|
-
*/
|
|
52
|
-
const buildServerFieldItems = (
|
|
53
|
-
server: LlamaServer,
|
|
54
|
-
theme: Theme,
|
|
55
|
-
tui: TUI,
|
|
56
|
-
): SettingItem[] => [
|
|
57
|
-
{
|
|
58
|
-
id: "url",
|
|
59
|
-
label: FIELDS.serverUrl.label,
|
|
60
|
-
description: FIELDS.serverUrl.description,
|
|
61
|
-
currentValue: server.url,
|
|
62
|
-
submenu: InputDialog.inputSubmenu(
|
|
63
|
-
theme,
|
|
64
|
-
tui,
|
|
65
|
-
TITLES.edit(FIELDS.serverUrl.label),
|
|
66
|
-
fieldMessage(FIELDS.serverUrl),
|
|
67
|
-
FIELDS.serverUrl.placeholder,
|
|
68
|
-
normalizeServerUrl,
|
|
69
|
-
),
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: "id",
|
|
73
|
-
label: FIELDS.providerId.label,
|
|
74
|
-
description: FIELDS.providerId.description,
|
|
75
|
-
currentValue: server.id ?? "",
|
|
76
|
-
submenu: InputDialog.inputSubmenu(
|
|
77
|
-
theme,
|
|
78
|
-
tui,
|
|
79
|
-
TITLES.edit(FIELDS.providerId.label),
|
|
80
|
-
fieldMessage(FIELDS.providerId),
|
|
81
|
-
FIELDS.providerId.placeholder,
|
|
82
|
-
),
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: "name",
|
|
86
|
-
label: FIELDS.displayName.label,
|
|
87
|
-
description: FIELDS.displayName.description,
|
|
88
|
-
currentValue: server.name ?? "",
|
|
89
|
-
submenu: InputDialog.inputSubmenu(
|
|
90
|
-
theme,
|
|
91
|
-
tui,
|
|
92
|
-
TITLES.edit(FIELDS.displayName.label),
|
|
93
|
-
fieldMessage(FIELDS.displayName),
|
|
94
|
-
FIELDS.displayName.placeholder,
|
|
95
|
-
),
|
|
96
|
-
},
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Builds the SettingItem for one server row in the top-level list.
|
|
101
|
-
* Enter drills into the server's field-edit submenu.
|
|
102
|
-
*/
|
|
103
|
-
const buildServerRow = (
|
|
104
|
-
server: LlamaServer,
|
|
105
|
-
index: number,
|
|
106
|
-
theme: Theme,
|
|
107
|
-
tui: TUI,
|
|
108
|
-
onChange: (field: string, value: string) => void,
|
|
109
|
-
healthEmoji: string,
|
|
110
|
-
onSubmenuChange: (open: boolean) => void,
|
|
111
|
-
): SettingItem => ({
|
|
112
|
-
id: `server-${index}`,
|
|
113
|
-
label: `${healthEmoji} ${server.url}`,
|
|
114
|
-
description: HINTS.serverRow,
|
|
115
|
-
currentValue: formatServerSuffix(server),
|
|
116
|
-
submenu: (_cv, done) => {
|
|
117
|
-
// Rebuild the field items on open so they prefill with current values
|
|
118
|
-
const items = buildServerFieldItems(server, theme, tui);
|
|
119
|
-
onSubmenuChange(true);
|
|
120
|
-
return new SettingsList(
|
|
121
|
-
items,
|
|
122
|
-
Math.min(items.length + 2, 15),
|
|
123
|
-
getSettingsListTheme(),
|
|
124
|
-
// Field commits arrive here: done(value) from the input submenus
|
|
125
|
-
(field, value) => onChange(field, value),
|
|
126
|
-
() => {
|
|
127
|
-
onSubmenuChange(false);
|
|
128
|
-
done();
|
|
129
|
-
},
|
|
130
|
-
);
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Wrapper around a `SettingsList` of servers that adds `a` (add) and `d`
|
|
136
|
-
* (delete) support at the list level.
|
|
137
|
-
*
|
|
138
|
-
* - **a** opens the add wizard — a sequence of framed `InputDialog`s
|
|
139
|
-
* (URL → optional ID → optional name, mirroring `/login`'s sequential
|
|
140
|
-
* prompts). Esc at any step aborts without persisting; the server is
|
|
141
|
-
* saved only after the final step.
|
|
142
|
-
* - **d** opens a `ConfirmDialog` (Delete/Cancel select list).
|
|
143
|
-
* - **Enter** on a server row drills into its field-edit submenu.
|
|
144
|
-
* - **Esc** closes the editor.
|
|
145
|
-
*/
|
|
146
|
-
export class ServerSettingsList implements Component, Focusable {
|
|
147
|
-
private settingsList!: SettingsList;
|
|
148
|
-
private mode: "list" | "wizard" | "confirm" = "list";
|
|
149
|
-
private wizardStep = 0;
|
|
150
|
-
private wizardUrl = "";
|
|
151
|
-
private wizardId = "";
|
|
152
|
-
private wizardDialog: InputDialog | undefined;
|
|
153
|
-
private confirmDialog: ConfirmDialog | undefined;
|
|
154
|
-
private isFocused = false;
|
|
155
|
-
/** True while a server row's field-edit submenu is open */
|
|
156
|
-
private submenuOpen = false;
|
|
157
|
-
|
|
158
|
-
constructor(private readonly options: ServerSettingsListOptions) {
|
|
159
|
-
this.buildSettingsList().then((list) => {
|
|
160
|
-
this.settingsList = list;
|
|
161
|
-
this.options.tui.requestRender();
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Builds the top-level `SettingsList` from the current
|
|
167
|
-
* `options.servers`. Called on construction and rebuilt after every
|
|
168
|
-
* successful persist so added/deleted/edited servers are reflected
|
|
169
|
-
* immediately (the initial snapshot is not mutated by `persist`).
|
|
170
|
-
*/
|
|
171
|
-
private async buildSettingsList(): Promise<SettingsList> {
|
|
172
|
-
const { theme, tui } = this.options;
|
|
173
|
-
const serverTimeout = this.options.serverTimeout ?? 1000;
|
|
174
|
-
// Check health for each server in parallel
|
|
175
|
-
const healthEmojis = await Promise.all(
|
|
176
|
-
this.options.servers.map((server) =>
|
|
177
|
-
getServerHealthEmoji(server.url, serverTimeout),
|
|
178
|
-
),
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
// A fresh list never has a submenu open (also covers the rebuild
|
|
182
|
-
// paths, which discard any open field submenu)
|
|
183
|
-
this.submenuOpen = false;
|
|
184
|
-
const items: SettingItem[] = this.options.servers.map((server, i) =>
|
|
185
|
-
buildServerRow(
|
|
186
|
-
server,
|
|
187
|
-
i,
|
|
188
|
-
theme,
|
|
189
|
-
tui,
|
|
190
|
-
(field, value) => this.handleFieldChange(field, value),
|
|
191
|
-
healthEmojis[i],
|
|
192
|
-
(open) => {
|
|
193
|
-
this.submenuOpen = open;
|
|
194
|
-
},
|
|
195
|
-
),
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
return new SettingsList(
|
|
199
|
-
items,
|
|
200
|
-
Math.min(items.length + 2, 15),
|
|
201
|
-
getSettingsListTheme(),
|
|
202
|
-
() => {
|
|
203
|
-
// Top-level onChange is a no-op (servers don't change at this level)
|
|
204
|
-
},
|
|
205
|
-
() => {
|
|
206
|
-
this.options.done();
|
|
207
|
-
},
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// -- Focusable -----------------------------------------------------------
|
|
212
|
-
|
|
213
|
-
get focused(): boolean {
|
|
214
|
-
return this.isFocused;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
set focused(value: boolean) {
|
|
218
|
-
this.isFocused = value;
|
|
219
|
-
if (this.wizardDialog) this.wizardDialog.focused = value;
|
|
220
|
-
if (this.confirmDialog) this.confirmDialog.focused = value;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// -- Component -----------------------------------------------------------
|
|
224
|
-
|
|
225
|
-
invalidate(): void {
|
|
226
|
-
this.settingsList?.invalidate();
|
|
227
|
-
this.wizardDialog?.invalidate();
|
|
228
|
-
this.confirmDialog?.invalidate();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
handleInput(data: string): void {
|
|
232
|
-
if (this.mode === "wizard") {
|
|
233
|
-
this.wizardDialog?.handleInput(data);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (this.mode === "confirm") {
|
|
238
|
-
this.confirmDialog?.handleInput(data);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// A server row's field submenu is open: delegate everything (Esc,
|
|
243
|
-
// a/d, arrows) to the containing SettingsList, which forwards input
|
|
244
|
-
// to the submenu. Intercepting here would close the whole editor on
|
|
245
|
-
// Esc and trigger add/delete while typing in a field.
|
|
246
|
-
if (this.submenuOpen) {
|
|
247
|
-
this.settingsList?.handleInput(data);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const kb = this.options.keybindings;
|
|
252
|
-
if (kb.matches(data, "tui.select.cancel")) {
|
|
253
|
-
this.options.done();
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
// Track selection for add/delete
|
|
257
|
-
if (kb.matches(data, "tui.select.up")) {
|
|
258
|
-
this._selectedIndex =
|
|
259
|
-
this._selectedIndex === 0
|
|
260
|
-
? this.options.servers.length - 1
|
|
261
|
-
: this._selectedIndex - 1;
|
|
262
|
-
}
|
|
263
|
-
if (kb.matches(data, "tui.select.down")) {
|
|
264
|
-
this._selectedIndex =
|
|
265
|
-
this._selectedIndex === this.options.servers.length - 1
|
|
266
|
-
? 0
|
|
267
|
-
: this._selectedIndex + 1;
|
|
268
|
-
}
|
|
269
|
-
if (data === "a") {
|
|
270
|
-
this.beginWizard();
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (data === "d") {
|
|
274
|
-
this.beginConfirm();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
// Delegate to SettingsList (it handles up/down internally for rendering)
|
|
278
|
-
this.settingsList?.handleInput(data);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
render(width: number): string[] {
|
|
282
|
-
if (this.mode === "wizard") {
|
|
283
|
-
return this.wizardDialog?.render(width) ?? [];
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (this.mode === "confirm") {
|
|
287
|
-
return this.confirmDialog?.render(width) ?? [];
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// settingsList may not be initialized yet if buildSettingsList is still
|
|
291
|
-
// resolving health checks (async in constructor). Return empty until ready.
|
|
292
|
-
if (!this.settingsList) return [];
|
|
293
|
-
|
|
294
|
-
// List mode: the shortcuts live in the rows' description; only when the
|
|
295
|
-
// list is empty (no rows → no description) show them as the hint line
|
|
296
|
-
const settingsLines = this.settingsList.render(width);
|
|
297
|
-
if (this.options.servers.length === 0) {
|
|
298
|
-
settingsLines[settingsLines.length - 1] = getSettingsListTheme().hint(
|
|
299
|
-
HINTS.emptyServers,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
return settingsLines;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// -- helpers ---------------------------------------------------------------
|
|
306
|
-
|
|
307
|
-
private selectedIndex(): number {
|
|
308
|
-
// Manual tracker for add/delete targeting, kept in sync with the
|
|
309
|
-
// SettingsList's cursor by rebuildList (SettingsList doesn't expose
|
|
310
|
-
// a getter)
|
|
311
|
-
return this._selectedIndex;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
private _selectedIndex = 0;
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Rebuilds the server list and places the cursor on `targetIndex`
|
|
318
|
-
* (clamped to the last row). A fresh SettingsList starts its cursor
|
|
319
|
-
* at index 0, so `selectItem` is used to restore the position.
|
|
320
|
-
*/
|
|
321
|
-
private async rebuildList(targetIndex: number): Promise<void> {
|
|
322
|
-
const list = await this.buildSettingsList();
|
|
323
|
-
this.settingsList = list;
|
|
324
|
-
const count = this.options.servers.length;
|
|
325
|
-
if (count === 0) {
|
|
326
|
-
this._selectedIndex = 0;
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
const target = Math.min(targetIndex, count - 1);
|
|
330
|
-
this._selectedIndex = target;
|
|
331
|
-
this.settingsList.selectItem(`server-${target}`);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// -- add wizard ---------------------------------------------------------------
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Opens the add wizard at step 1/3 (URL). Esc at any step cancels the
|
|
338
|
-
* whole wizard; the server is persisted only after the final step.
|
|
339
|
-
*/
|
|
340
|
-
private beginWizard(): void {
|
|
341
|
-
this.mode = "wizard";
|
|
342
|
-
this.wizardStep = 0;
|
|
343
|
-
this.wizardUrl = "";
|
|
344
|
-
this.wizardId = "";
|
|
345
|
-
this.showWizardStep();
|
|
346
|
-
this.options.tui.requestRender();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private showWizardStep(): void {
|
|
350
|
-
const { theme, tui } = this.options;
|
|
351
|
-
const step = this.wizardStep;
|
|
352
|
-
|
|
353
|
-
const dialog =
|
|
354
|
-
step === 0
|
|
355
|
-
? new InputDialog({
|
|
356
|
-
theme,
|
|
357
|
-
tui,
|
|
358
|
-
title: TITLES.addServerStep(1),
|
|
359
|
-
message: fieldMessage(FIELDS.serverUrl),
|
|
360
|
-
placeholder: FIELDS.serverUrl.placeholder,
|
|
361
|
-
validate: normalizeServerUrl,
|
|
362
|
-
onSubmit: (value) => this.wizardSubmit(value),
|
|
363
|
-
onCancel: () => this.cancelWizard(),
|
|
364
|
-
})
|
|
365
|
-
: step === 1
|
|
366
|
-
? new InputDialog({
|
|
367
|
-
theme,
|
|
368
|
-
tui,
|
|
369
|
-
title: TITLES.addServerStep(2),
|
|
370
|
-
message: fieldMessage(FIELDS.providerId, "(optional)"),
|
|
371
|
-
placeholder: FIELDS.providerId.placeholder,
|
|
372
|
-
onSubmit: (value) => this.wizardSubmit(value),
|
|
373
|
-
onCancel: () => this.cancelWizard(),
|
|
374
|
-
})
|
|
375
|
-
: new InputDialog({
|
|
376
|
-
theme,
|
|
377
|
-
tui,
|
|
378
|
-
title: TITLES.addServerStep(3),
|
|
379
|
-
message: fieldMessage(FIELDS.displayName, "(optional)"),
|
|
380
|
-
placeholder: FIELDS.displayName.placeholder,
|
|
381
|
-
onSubmit: (value) => this.wizardSubmit(value),
|
|
382
|
-
onCancel: () => this.cancelWizard(),
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
this.wizardDialog = dialog;
|
|
386
|
-
dialog.focused = this.isFocused;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
private wizardSubmit(value: string): void {
|
|
390
|
-
if (this.wizardStep === 0) {
|
|
391
|
-
this.wizardUrl = value;
|
|
392
|
-
this.wizardStep = 1;
|
|
393
|
-
this.showWizardStep();
|
|
394
|
-
} else if (this.wizardStep === 1) {
|
|
395
|
-
this.wizardId = value;
|
|
396
|
-
this.wizardStep = 2;
|
|
397
|
-
this.showWizardStep();
|
|
398
|
-
} else {
|
|
399
|
-
// Final step (display name): commit the collected fields
|
|
400
|
-
this.wizardDialog = undefined;
|
|
401
|
-
void this.saveNewServer(this.wizardUrl, this.wizardId, value);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
this.options.tui.requestRender();
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
private cancelWizard(): void {
|
|
408
|
-
this.wizardDialog = undefined;
|
|
409
|
-
this.mode = "list";
|
|
410
|
-
this.options.tui.requestRender();
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
private async saveNewServer(
|
|
414
|
-
url: string,
|
|
415
|
-
id: string,
|
|
416
|
-
name: string,
|
|
417
|
-
): Promise<void> {
|
|
418
|
-
const next = [
|
|
419
|
-
...this.options.servers,
|
|
420
|
-
{
|
|
421
|
-
url,
|
|
422
|
-
...(id.length > 0 ? { id } : {}),
|
|
423
|
-
...(name.length > 0 ? { name } : {}),
|
|
424
|
-
},
|
|
425
|
-
];
|
|
426
|
-
try {
|
|
427
|
-
await this.options.persist(next);
|
|
428
|
-
// Adopt the persisted list and rebuild so the new row appears;
|
|
429
|
-
// the new server is appended last — put the cursor on it
|
|
430
|
-
this.options.servers = next;
|
|
431
|
-
await this.rebuildList(next.length - 1);
|
|
432
|
-
this.mode = "list";
|
|
433
|
-
} catch (err) {
|
|
434
|
-
this.options.onError(String(err));
|
|
435
|
-
this.mode = "list";
|
|
436
|
-
}
|
|
437
|
-
this.options.tui.requestRender();
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// -- delete ------------------------------------------------------------------
|
|
441
|
-
|
|
442
|
-
private beginConfirm(): void {
|
|
443
|
-
const server = this.options.servers[this.selectedIndex()];
|
|
444
|
-
if (!server) return;
|
|
445
|
-
|
|
446
|
-
this.mode = "confirm";
|
|
447
|
-
this.confirmDialog = new ConfirmDialog({
|
|
448
|
-
theme: this.options.theme,
|
|
449
|
-
tui: this.options.tui,
|
|
450
|
-
title: TITLES.deleteServer,
|
|
451
|
-
message: `Delete "${server.url}"?`,
|
|
452
|
-
onConfirm: () => {
|
|
453
|
-
this.confirmDialog = undefined;
|
|
454
|
-
void this.deleteSelected();
|
|
455
|
-
},
|
|
456
|
-
onCancel: () => {
|
|
457
|
-
this.confirmDialog = undefined;
|
|
458
|
-
this.mode = "list";
|
|
459
|
-
this.options.tui.requestRender();
|
|
460
|
-
},
|
|
461
|
-
});
|
|
462
|
-
this.confirmDialog.focused = this.isFocused;
|
|
463
|
-
this.options.tui.requestRender();
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
private async deleteSelected(): Promise<void> {
|
|
467
|
-
const idx = this.selectedIndex();
|
|
468
|
-
const servers = this.options.servers;
|
|
469
|
-
const next = servers.filter((_, i) => i !== idx);
|
|
470
|
-
try {
|
|
471
|
-
await this.options.persist(next);
|
|
472
|
-
// Adopt the persisted list and rebuild so the row disappears;
|
|
473
|
-
// the server that followed the deleted one now sits at the same
|
|
474
|
-
// index (rebuildList clamps when the last row was deleted)
|
|
475
|
-
this.options.servers = next;
|
|
476
|
-
await this.rebuildList(idx);
|
|
477
|
-
this.mode = "list";
|
|
478
|
-
} catch (err) {
|
|
479
|
-
this.options.onError(String(err));
|
|
480
|
-
this.mode = "list";
|
|
481
|
-
}
|
|
482
|
-
this.options.tui.requestRender();
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
private async handleFieldChange(field: string, value: string): Promise<void> {
|
|
486
|
-
const idx = this._selectedIndex;
|
|
487
|
-
const server = this.options.servers[idx];
|
|
488
|
-
if (!server) return;
|
|
489
|
-
|
|
490
|
-
const next = this.options.servers.map((s, i) => {
|
|
491
|
-
if (i !== idx) return s;
|
|
492
|
-
if (field === "url") return { ...s, url: value };
|
|
493
|
-
if (field === "id") {
|
|
494
|
-
const { id, ...rest } = s;
|
|
495
|
-
return value.length === 0 ? rest : { ...rest, id: value };
|
|
496
|
-
}
|
|
497
|
-
const { name, ...rest } = s;
|
|
498
|
-
return value.length === 0 ? rest : { ...rest, name: value };
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
try {
|
|
502
|
-
await this.options.persist(next);
|
|
503
|
-
// URL/id/name changes alter row labels → rebuild the list;
|
|
504
|
-
// keep the cursor on the edited server
|
|
505
|
-
this.options.servers = next;
|
|
506
|
-
await this.rebuildList(idx);
|
|
507
|
-
this.options.tui.requestRender();
|
|
508
|
-
} catch (err) {
|
|
509
|
-
this.options.onError(String(err));
|
|
510
|
-
this.options.tui.requestRender();
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|