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.
Files changed (65) hide show
  1. package/README.md +7 -8
  2. package/package.json +2 -2
  3. package/src/api/client.ts +25 -0
  4. package/src/constants.ts +0 -5
  5. package/src/enums/status.ts +0 -1
  6. package/src/interfaces/endpoints/models.ts +1 -1
  7. package/src/interfaces/settings.ts +1 -1
  8. package/src/interfaces/sortBy.ts +4 -0
  9. package/src/managers/command/models.ts +247 -0
  10. package/src/managers/command.ts +29 -461
  11. package/src/managers/events.ts +2 -2
  12. package/src/managers/server.ts +27 -3
  13. package/src/managers/settings.ts +12 -88
  14. package/src/models/baseModel.ts +27 -10
  15. package/src/models/legacyModel.ts +2 -2
  16. package/src/models/routerModel.ts +2 -2
  17. package/src/models/singleModel.ts +1 -1
  18. package/src/server.ts +11 -29
  19. package/src/sse/client.ts +113 -59
  20. package/src/sse/fetch.ts +43 -0
  21. package/src/sse/manager.ts +9 -27
  22. package/src/sse/types.ts +0 -4
  23. package/src/ui/dialog/base.ts +118 -0
  24. package/src/ui/dialog/confirm.ts +45 -0
  25. package/src/ui/dialog/factory.ts +111 -0
  26. package/src/ui/dialog/input.ts +63 -0
  27. package/src/ui/dialog/options.ts +23 -0
  28. package/src/ui/editors/editorOptions.ts +43 -0
  29. package/src/ui/editors/itemBuilder.ts +47 -0
  30. package/src/ui/editors/listEditor.ts +291 -0
  31. package/src/ui/editors/override/entry.ts +24 -0
  32. package/src/ui/editors/override/entryEditor.ts +166 -0
  33. package/src/ui/editors/override/fields.ts +294 -0
  34. package/src/ui/editors/override/handlers.ts +118 -0
  35. package/src/ui/editors/override/itemBuilder.ts +42 -0
  36. package/src/ui/editors/override/overrideList.ts +127 -0
  37. package/src/ui/editors/server/builder.ts +60 -0
  38. package/src/ui/editors/server/fields.ts +84 -0
  39. package/src/ui/editors/server/handlers.ts +32 -0
  40. package/src/ui/editors/server/itemBuilder.ts +68 -0
  41. package/src/ui/editors/server/serverEditor.ts +161 -0
  42. package/src/ui/editors/server/utils.ts +45 -0
  43. package/src/ui/editors/server/wizard.ts +110 -0
  44. package/src/ui/editors/settingField.ts +37 -0
  45. package/src/ui/editors/settingsListFactory.ts +33 -0
  46. package/src/ui/settings/index.ts +237 -0
  47. package/src/ui/strings.ts +8 -4
  48. package/src/utils/serverIds.ts +21 -0
  49. package/src/utils/settingsStore.ts +1 -1
  50. package/src/utils/urlResolver.ts +129 -0
  51. package/src/utils/urls.ts +33 -13
  52. package/tests/commandManager.test.ts +14 -9
  53. package/tests/dialog.test.ts +55 -4
  54. package/tests/legacyModel.test.ts +34 -28
  55. package/tests/overrides.test.ts +111 -88
  56. package/tests/routerModel.test.ts +67 -68
  57. package/tests/server.test.ts +0 -12
  58. package/tests/settings.test.ts +10 -8
  59. package/tests/singleModel.test.ts +12 -12
  60. package/tests/sseManager.test.ts +6 -24
  61. package/src/ui/dialog.ts +0 -290
  62. package/src/ui/overrideEntryEditor.ts +0 -119
  63. package/src/ui/overrideSettingsList.ts +0 -710
  64. package/src/ui/serverListEditor.ts +0 -59
  65. 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
+ }