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,84 @@
|
|
|
1
|
+
import type { LlamaServer } from "../../../interfaces/settings";
|
|
2
|
+
import { ServerUrl } from "../../../utils/urls";
|
|
3
|
+
import { FIELDS, type Field } from "../../strings";
|
|
4
|
+
import { SettingField } from "../settingField";
|
|
5
|
+
import { LlamaServerBuilder } from "./builder";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base class for one editable field of a server row. `apply` delegates
|
|
9
|
+
* to {@link LlamaServerBuilder} so the unset-on-empty rule for `id`/
|
|
10
|
+
* `name` stays in one place. Mirrors the override side's
|
|
11
|
+
* `OverrideField`.
|
|
12
|
+
*/
|
|
13
|
+
export abstract class ServerField extends SettingField<LlamaServer> {
|
|
14
|
+
/** Validation function for committed values (`undefined` = accept
|
|
15
|
+
* any). */
|
|
16
|
+
readonly validate: ((raw: string) => string | null) | undefined = undefined;
|
|
17
|
+
|
|
18
|
+
/** Applies a committed value to the server, immutably (returns the
|
|
19
|
+
* updated copy). */
|
|
20
|
+
abstract apply(server: LlamaServer, value: string): LlamaServer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The server URL (required, validated). */
|
|
24
|
+
class UrlField extends ServerField {
|
|
25
|
+
readonly id = "url";
|
|
26
|
+
readonly field = FIELDS.serverUrl;
|
|
27
|
+
readonly validate = ServerUrl.parse;
|
|
28
|
+
|
|
29
|
+
currentValue(server: LlamaServer): string {
|
|
30
|
+
return server.url;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
apply(server: LlamaServer, value: string): LlamaServer {
|
|
34
|
+
return LlamaServerBuilder.from(server).url(value).build();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An optional string field (`id`, `name`). Empty input omits the key —
|
|
40
|
+
* when editing an existing server this removes it, restoring the
|
|
41
|
+
* URL-based auto-detected id for `id`.
|
|
42
|
+
*/
|
|
43
|
+
class OptionalTextField extends ServerField {
|
|
44
|
+
readonly field: Field;
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
readonly id: "id" | "name",
|
|
48
|
+
field: Field,
|
|
49
|
+
) {
|
|
50
|
+
super();
|
|
51
|
+
this.field = field;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
currentValue(server: LlamaServer): string {
|
|
55
|
+
return server[this.id] ?? "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
apply(server: LlamaServer, value: string): LlamaServer {
|
|
59
|
+
const builder = LlamaServerBuilder.from(server);
|
|
60
|
+
return this.id === "id"
|
|
61
|
+
? builder.optionalId(value).build()
|
|
62
|
+
: builder.optionalName(value).build();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The field registry for servers: the complete list of editable fields
|
|
68
|
+
* (order defines the row order in the settings list) plus id-based
|
|
69
|
+
* lookup. Throws on an unknown id — the registry is static, so a miss is
|
|
70
|
+
* a programming error.
|
|
71
|
+
*/
|
|
72
|
+
export class ServerFields {
|
|
73
|
+
static readonly all: readonly ServerField[] = [
|
|
74
|
+
new UrlField(),
|
|
75
|
+
new OptionalTextField("id", FIELDS.providerId),
|
|
76
|
+
new OptionalTextField("name", FIELDS.displayName),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
static byId(id: string): ServerField {
|
|
80
|
+
const field = this.all.find((f) => f.id === id);
|
|
81
|
+
if (!field) throw new Error(`Unknown server field: ${id}`);
|
|
82
|
+
return field;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LlamaServer } from "../../../interfaces/settings";
|
|
2
|
+
import { ServerFields } from "./fields";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Applies field commits to a single server, immutably. Binds the server
|
|
6
|
+
* list and server index once in the constructor so call sites don't
|
|
7
|
+
* juggle long positional lists. Mirrors the override side's
|
|
8
|
+
* `OverrideEntryMutator`.
|
|
9
|
+
*
|
|
10
|
+
* Per-field commit behavior lives in the `ServerFields` registry
|
|
11
|
+
* (`apply` on each field class, backed by `LlamaServerBuilder`) — this
|
|
12
|
+
* class only resolves the field definition and produces the next list.
|
|
13
|
+
*/
|
|
14
|
+
export class ServerEntryMutator {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly servers: LlamaServer[],
|
|
17
|
+
private readonly serverIndex: number,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handles a field commit from a server row's submenu. Returns the
|
|
22
|
+
* next server list, or the same list when the server is missing
|
|
23
|
+
* (no-op — callers can compare by reference).
|
|
24
|
+
*/
|
|
25
|
+
applyFieldChange(field: string, value: string): LlamaServer[] {
|
|
26
|
+
const server = this.servers[this.serverIndex];
|
|
27
|
+
if (!server) return this.servers;
|
|
28
|
+
|
|
29
|
+
const updated = ServerFields.byId(field).apply(server, value);
|
|
30
|
+
return this.servers.map((s, i) => (i === this.serverIndex ? updated : s));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { LlamaServer } from "../../../interfaces/settings";
|
|
3
|
+
import { FieldMessages, HINTS, TITLES } from "../../strings";
|
|
4
|
+
import { ItemBuilder } from "../itemBuilder";
|
|
5
|
+
import { SettingsListFactory } from "../settingsListFactory";
|
|
6
|
+
import { ServerFields, type ServerField } from "./fields";
|
|
7
|
+
import { ServerDisplay } from "./utils";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds `SettingItem` objects for server rows and their field-edit submenus.
|
|
11
|
+
*/
|
|
12
|
+
export class ServerItemBuilder extends ItemBuilder<LlamaServer, ServerField> {
|
|
13
|
+
protected get fields(): readonly ServerField[] {
|
|
14
|
+
return ServerFields.all;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Adds the field-edit submenu: an `InputDialog` validating against the
|
|
19
|
+
* field definition.
|
|
20
|
+
*/
|
|
21
|
+
protected decorate(
|
|
22
|
+
def: ServerField,
|
|
23
|
+
server: LlamaServer,
|
|
24
|
+
base: SettingItem,
|
|
25
|
+
): SettingItem {
|
|
26
|
+
return {
|
|
27
|
+
...base,
|
|
28
|
+
submenu: this.dialogs.inputSubmenu(
|
|
29
|
+
TITLES.edit(def.label),
|
|
30
|
+
FieldMessages.of(def.field),
|
|
31
|
+
def.placeholder,
|
|
32
|
+
def.validate,
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds the `SettingItem` for one server row in the top-level list.
|
|
39
|
+
* Enter drills into the server's field-edit submenu.
|
|
40
|
+
*/
|
|
41
|
+
buildRow(
|
|
42
|
+
server: LlamaServer,
|
|
43
|
+
index: number,
|
|
44
|
+
onChange: (field: string, value: string) => void,
|
|
45
|
+
healthEmoji: string,
|
|
46
|
+
onSubmenuChange: (open: boolean) => void,
|
|
47
|
+
): SettingItem {
|
|
48
|
+
return {
|
|
49
|
+
id: `server-${index}`,
|
|
50
|
+
label: `${healthEmoji} ${server.url}`,
|
|
51
|
+
description: HINTS.serverRow,
|
|
52
|
+
currentValue: ServerDisplay.suffix(server),
|
|
53
|
+
submenu: (_cv, done) => {
|
|
54
|
+
// Rebuild the field items on open so they prefill with current values
|
|
55
|
+
const items = this.buildFieldItems(server);
|
|
56
|
+
onSubmenuChange(true);
|
|
57
|
+
return SettingsListFactory.create(
|
|
58
|
+
items,
|
|
59
|
+
() => {
|
|
60
|
+
onSubmenuChange(false);
|
|
61
|
+
done();
|
|
62
|
+
},
|
|
63
|
+
onChange,
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { SettingItem, SettingsList } from "@earendil-works/pi-tui";
|
|
3
|
+
import { SERVER_TIMEOUT } from "../../../constants";
|
|
4
|
+
import type { LlamaServer } from "../../../interfaces/settings";
|
|
5
|
+
import type { LlamaSettingsManager } from "../../../managers/settings";
|
|
6
|
+
import { TITLES } from "../../strings";
|
|
7
|
+
import type { ServerSettingsListOptions } from "../editorOptions";
|
|
8
|
+
import { ListEditor } from "../listEditor";
|
|
9
|
+
import { SettingsListFactory } from "../settingsListFactory";
|
|
10
|
+
import { ServerEntryMutator } from "./handlers";
|
|
11
|
+
import { ServerItemBuilder } from "./itemBuilder";
|
|
12
|
+
import { ServerDisplay } from "./utils";
|
|
13
|
+
import { ServerWizard } from "./wizard";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wrapper around a `SettingsList` of servers that adds `a` (add) and `d`
|
|
17
|
+
* (delete) support at the list level.
|
|
18
|
+
*
|
|
19
|
+
* - **a** opens the add wizard — a sequence of framed `InputDialog`s
|
|
20
|
+
* (URL → optional ID → optional name, mirroring `/login`'s sequential
|
|
21
|
+
* prompts). Esc at any step aborts without persisting; the server is
|
|
22
|
+
* saved only after the final step.
|
|
23
|
+
* - **d** opens a `ConfirmDialog` (Delete/Cancel select list).
|
|
24
|
+
* - **Enter** on a server row drills into its field-edit submenu.
|
|
25
|
+
* - **Esc** closes the editor.
|
|
26
|
+
*/
|
|
27
|
+
export class ServerSettingsList extends ListEditor<ServerSettingsListOptions> {
|
|
28
|
+
constructor(options: ServerSettingsListOptions) {
|
|
29
|
+
super(options);
|
|
30
|
+
void this.buildSettingsList().then((list) => {
|
|
31
|
+
this.settingsList = list;
|
|
32
|
+
this.options.tui.requestRender();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Opens the editor in a modal `ui.custom` dialog seeded with the
|
|
38
|
+
* current `llamaSettings.servers` and resolves when the user closes
|
|
39
|
+
* it (Esc). Writes go through `settings.setLlamaSetting()`; write
|
|
40
|
+
* errors are notified via `ui` and the editor stays open with the
|
|
41
|
+
* pre-mutation list.
|
|
42
|
+
*/
|
|
43
|
+
static async show(
|
|
44
|
+
ui: ExtensionUIContext,
|
|
45
|
+
settings: LlamaSettingsManager,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
const servers = await settings.getLlamaServers();
|
|
48
|
+
const { serverTimeout } = await settings.resolveTimeouts();
|
|
49
|
+
|
|
50
|
+
await ui.custom<void>(
|
|
51
|
+
(tui, theme, keybindings, done) =>
|
|
52
|
+
new ServerSettingsList({
|
|
53
|
+
tui,
|
|
54
|
+
theme,
|
|
55
|
+
keybindings,
|
|
56
|
+
servers,
|
|
57
|
+
persist: (next) => settings.setLlamaSetting("servers", next),
|
|
58
|
+
done: () => done(undefined),
|
|
59
|
+
onError: (message) => ui.notify(message, "error"),
|
|
60
|
+
serverTimeout,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// -- abstract hooks -------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
protected async buildSettingsList(): Promise<SettingsList> {
|
|
68
|
+
const builder = new ServerItemBuilder(this.dialogs);
|
|
69
|
+
const serverTimeout = this.options.serverTimeout ?? SERVER_TIMEOUT;
|
|
70
|
+
const healthEmojis = await Promise.all(
|
|
71
|
+
this.options.servers.map((server) =>
|
|
72
|
+
ServerDisplay.healthEmoji(server.url, serverTimeout),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
const items: SettingItem[] = this.options.servers.map((server, i) =>
|
|
76
|
+
builder.buildRow(
|
|
77
|
+
server,
|
|
78
|
+
i,
|
|
79
|
+
(field, value) => this.handleFieldChange(field, value),
|
|
80
|
+
healthEmojis[i],
|
|
81
|
+
// Opening delegates input to the submenu; closing flushes a
|
|
82
|
+
// rebuild deferred by a field commit (see commitFieldChange)
|
|
83
|
+
(open) => this.trackSubmenu(open),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return SettingsListFactory.create(items, () => {
|
|
88
|
+
this.options.done();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Handle a field commit from a server row's submenu. */
|
|
93
|
+
private handleFieldChange(field: string, value: string): void {
|
|
94
|
+
const idx = this.selectedIndex;
|
|
95
|
+
const next = new ServerEntryMutator(
|
|
96
|
+
this.options.servers,
|
|
97
|
+
idx,
|
|
98
|
+
).applyFieldChange(field, value);
|
|
99
|
+
|
|
100
|
+
this.commitFieldChange(next, idx, () => {
|
|
101
|
+
// Refresh the row's suffix in place; the full rebuild (row labels
|
|
102
|
+
// with health emoji, fresh server snapshots for the field submenu)
|
|
103
|
+
// is deferred until the submenu closes (see commitFieldChange)
|
|
104
|
+
const updated = next[idx];
|
|
105
|
+
if (updated) {
|
|
106
|
+
this.settingsList?.updateValue(
|
|
107
|
+
`server-${idx}`,
|
|
108
|
+
ServerDisplay.suffix(updated),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected beginAdd(): void {
|
|
115
|
+
const wizard = new ServerWizard(this.dialogs, (dialog) =>
|
|
116
|
+
this.openDialog(dialog),
|
|
117
|
+
);
|
|
118
|
+
wizard.start(
|
|
119
|
+
(server) => {
|
|
120
|
+
this.closeDialog();
|
|
121
|
+
void this.saveNewServer(server);
|
|
122
|
+
},
|
|
123
|
+
() => this.closeDialog(),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
protected deleteSelected(): void {
|
|
128
|
+
const idx = this.selectedIndex;
|
|
129
|
+
const next = this.options.servers.filter((_, i) => i !== idx);
|
|
130
|
+
void this.persistSnapshot(next, () => {
|
|
131
|
+
void this.rebuildList(idx);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
protected readonly emptyHintKey = "emptyServers" as const;
|
|
136
|
+
|
|
137
|
+
protected getCurrentCount(): number {
|
|
138
|
+
return this.options.servers.length;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
protected getRowId(index: number): string {
|
|
142
|
+
return `server-${index}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected getRowLabel(index: number): string {
|
|
146
|
+
return this.options.servers[index]?.url ?? "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected get deleteTitle(): string {
|
|
150
|
+
return TITLES.deleteServer;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// -- wizard helpers --------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
private async saveNewServer(server: LlamaServer): Promise<void> {
|
|
156
|
+
const next = [...this.options.servers, server];
|
|
157
|
+
await this.persistSnapshot(next, () => {
|
|
158
|
+
void this.rebuildList(next.length - 1);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ServerStatus } from "../../../enums/serverStatus";
|
|
2
|
+
import type { LlamaServer } from "../../../interfaces/settings";
|
|
3
|
+
import { checkServerHealth } from "../../../utils/health";
|
|
4
|
+
import { ServerIds } from "../../../utils/serverIds";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Display concerns for a server row in the top-level list: the dim
|
|
8
|
+
* `(...)` suffix after the URL and the health-status icon.
|
|
9
|
+
*/
|
|
10
|
+
export class ServerDisplay {
|
|
11
|
+
/**
|
|
12
|
+
* Formats the dim `(...)` suffix shown after a server's URL:
|
|
13
|
+
* `(<id> - <name>)`, with the auto-detected URL-based id used when no
|
|
14
|
+
* custom id override exists (mirrors `Server.providerId`). Returns the
|
|
15
|
+
* empty string when there is nothing to show.
|
|
16
|
+
*/
|
|
17
|
+
static suffix(server: LlamaServer): string {
|
|
18
|
+
if (!server.id && !server.name) return "";
|
|
19
|
+
const id = ServerIds.resolve(server.url, server.id);
|
|
20
|
+
return server.name ? `(${id} - ${server.name})` : `(${server.id})`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks the health of a server and returns the corresponding emoji.
|
|
25
|
+
*
|
|
26
|
+
* Delegates the probe/classification to the shared `checkServerHealth`
|
|
27
|
+
* (`utils/health`) — this wrapper only maps the status to its icon.
|
|
28
|
+
*
|
|
29
|
+
* @param url - The server URL to check
|
|
30
|
+
* @param timeout - Maximum time (ms) to wait for the health check
|
|
31
|
+
* @returns The health emoji for the server status
|
|
32
|
+
*/
|
|
33
|
+
static async healthEmoji(url: string, timeout: number): Promise<string> {
|
|
34
|
+
return SERVER_STATUS_ICONS[await checkServerHealth(url, timeout)];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Emoji indicators for server health status.
|
|
40
|
+
*/
|
|
41
|
+
const SERVER_STATUS_ICONS: Record<ServerStatus, string> = {
|
|
42
|
+
[ServerStatus.READY]: "🟢",
|
|
43
|
+
[ServerStatus.TIMEOUT]: "🟡",
|
|
44
|
+
[ServerStatus.UNREACHABLE]: "🔴",
|
|
45
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { LlamaServer } from "../../../interfaces/settings";
|
|
2
|
+
import type { DialogFactory } from "../../dialog/factory";
|
|
3
|
+
import type { InputDialog } from "../../dialog/input";
|
|
4
|
+
import { FieldMessages, TITLES } from "../../strings";
|
|
5
|
+
import { ServerFields, type ServerField } from "./fields";
|
|
6
|
+
|
|
7
|
+
/** Wizard step spec: which registry field this step prompts for, plus an
|
|
8
|
+
* optional note override for the prompt. */
|
|
9
|
+
interface WizardStepSpec {
|
|
10
|
+
def: ServerField;
|
|
11
|
+
note?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* State machine for the add-server wizard (URL → optional ID → optional name).
|
|
16
|
+
*
|
|
17
|
+
* The wizard state is just a growing `LlamaServer`: each step applies its
|
|
18
|
+
* committed value through the field definition's `apply` (backed by
|
|
19
|
+
* {@link LlamaServerBuilder}, so the unset-on-empty rule for `id`/`name`
|
|
20
|
+
* is shared with field edits).
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* ```ts
|
|
24
|
+
* const wizard = new ServerWizard(dialogs);
|
|
25
|
+
* wizard.start(onComplete, onCancel);
|
|
26
|
+
* wizard.next(value); // advances through the steps
|
|
27
|
+
* wizard.cancel();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class ServerWizard {
|
|
31
|
+
/**
|
|
32
|
+
* Ordered pipeline of wizard steps: URL (required, validated) → optional
|
|
33
|
+
* ID → optional name. Steps reference the shared `ServerFields` registry
|
|
34
|
+
* — the single source of truth for each field's label, placeholder,
|
|
35
|
+
* validator and apply logic — so the wizard can't drift from field edits
|
|
36
|
+
* on existing servers.
|
|
37
|
+
*/
|
|
38
|
+
private static readonly STEPS: WizardStepSpec[] = [
|
|
39
|
+
{ def: ServerFields.byId("url") },
|
|
40
|
+
{ def: ServerFields.byId("id"), note: "(optional)" },
|
|
41
|
+
{ def: ServerFields.byId("name"), note: "(optional)" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
private server: LlamaServer = { url: "" };
|
|
45
|
+
private stepIndex = 0;
|
|
46
|
+
private dialog: InputDialog | null = null;
|
|
47
|
+
private onComplete: ((server: LlamaServer) => void) | null = null;
|
|
48
|
+
private onCancel: (() => void) | null = null;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
private readonly dialogs: DialogFactory,
|
|
52
|
+
/** Called each time a step dialog is created — the containing editor
|
|
53
|
+
* re-opens it via `openDialog` so input keeps flowing to the current
|
|
54
|
+
* step. */
|
|
55
|
+
private readonly onDialog: (dialog: InputDialog) => void,
|
|
56
|
+
) {}
|
|
57
|
+
|
|
58
|
+
/** Start the wizard at the first step (URL). Focus is managed by the
|
|
59
|
+
* caller (the containing editor's `openDialog`). */
|
|
60
|
+
start(onComplete: (server: LlamaServer) => void, onCancel: () => void): void {
|
|
61
|
+
this.onComplete = onComplete;
|
|
62
|
+
this.onCancel = onCancel;
|
|
63
|
+
this.server = { url: "" };
|
|
64
|
+
this.stepIndex = 0;
|
|
65
|
+
this.showStep();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Commit the current step's value and advance. When the final step
|
|
70
|
+
* completes, hands the built server to the completion callback.
|
|
71
|
+
*/
|
|
72
|
+
next(value: string): void {
|
|
73
|
+
const spec = ServerWizard.STEPS[this.stepIndex];
|
|
74
|
+
if (!spec) return;
|
|
75
|
+
|
|
76
|
+
this.server = spec.def.apply(this.server, value);
|
|
77
|
+
this.stepIndex += 1;
|
|
78
|
+
|
|
79
|
+
if (this.stepIndex >= ServerWizard.STEPS.length) {
|
|
80
|
+
this.dialog = null;
|
|
81
|
+
this.onComplete?.(this.server);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.showStep();
|
|
86
|
+
this.dialogs.requestRender();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Abort the wizard and call the cancel callback. */
|
|
90
|
+
cancel(): void {
|
|
91
|
+
this.dialog = null;
|
|
92
|
+
this.stepIndex = 0;
|
|
93
|
+
this.onCancel?.();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// -- internal helpers ------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
private showStep(): void {
|
|
99
|
+
const spec = ServerWizard.STEPS[this.stepIndex];
|
|
100
|
+
this.dialog = this.dialogs.input({
|
|
101
|
+
title: TITLES.addServerStep((this.stepIndex + 1) as 1 | 2 | 3),
|
|
102
|
+
message: FieldMessages.of(spec.def.field, spec.note),
|
|
103
|
+
placeholder: spec.def.placeholder,
|
|
104
|
+
validate: spec.def.validate,
|
|
105
|
+
onSubmit: (v) => this.next(v),
|
|
106
|
+
onCancel: () => this.cancel(),
|
|
107
|
+
});
|
|
108
|
+
this.onDialog(this.dialog);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Field } from "../strings";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for one editable field of an entry (Template Method): the
|
|
5
|
+
* user-facing strings come from the shared `FIELDS` registry
|
|
6
|
+
* (`strings.ts`), and the `ItemBuilder` template fills the common
|
|
7
|
+
* `SettingItem` columns from this class while domain subclasses own the
|
|
8
|
+
* value/apply behavior.
|
|
9
|
+
*
|
|
10
|
+
* Both field registries (`ServerFields`, `OverrideFields`) extend this
|
|
11
|
+
* class, so the two editors share one field idiom instead of parallel
|
|
12
|
+
* interface/class shapes.
|
|
13
|
+
*/
|
|
14
|
+
export abstract class SettingField<TEntry> {
|
|
15
|
+
/** Unique id used as `SettingItem.id`. */
|
|
16
|
+
abstract readonly id: string;
|
|
17
|
+
/** User-facing strings from the shared FIELDS registry. */
|
|
18
|
+
abstract readonly field: Field;
|
|
19
|
+
|
|
20
|
+
/** Row label (from the shared FIELDS registry). */
|
|
21
|
+
get label(): string {
|
|
22
|
+
return this.field.label;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Row description in the containing `SettingsList`. */
|
|
26
|
+
get description(): string {
|
|
27
|
+
return this.field.description;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Example value shown dim as `e.g., <placeholder>` in input dialogs. */
|
|
31
|
+
get placeholder(): string | undefined {
|
|
32
|
+
return this.field.placeholder;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Current value shown in the entry's field row. */
|
|
36
|
+
abstract currentValue(entry: TEntry): string;
|
|
37
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { SettingsList, type SettingItem } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Factory for creating `SettingsList` instances with the standard
|
|
6
|
+
* geometry formula and theme.
|
|
7
|
+
*
|
|
8
|
+
* All call sites use this factory instead of constructing `SettingsList`
|
|
9
|
+
* directly, so the height formula (`Math.min(n + 2, 15)`) and theme
|
|
10
|
+
* lookup live in a single place and can't drift apart.
|
|
11
|
+
*/
|
|
12
|
+
export class SettingsListFactory {
|
|
13
|
+
/**
|
|
14
|
+
* Creates a `SettingsList` with the standard geometry formula and theme.
|
|
15
|
+
*
|
|
16
|
+
* @param items — The list items to display.
|
|
17
|
+
* @param onEscape — Called when the user presses Esc to close the list.
|
|
18
|
+
* @param onSelect — Optional callback for field commits (defaults to a no-op).
|
|
19
|
+
*/
|
|
20
|
+
static create(
|
|
21
|
+
items: SettingItem[],
|
|
22
|
+
onEscape: () => void,
|
|
23
|
+
onSelect?: (field: string, value: string) => void,
|
|
24
|
+
): SettingsList {
|
|
25
|
+
return new SettingsList(
|
|
26
|
+
items,
|
|
27
|
+
Math.min(items.length + 2, 15),
|
|
28
|
+
getSettingsListTheme(),
|
|
29
|
+
onSelect ?? (() => {}),
|
|
30
|
+
onEscape,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|