pi-llama-cpp 0.13.0 → 0.15.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 (77) hide show
  1. package/README.md +19 -18
  2. package/package.json +3 -3
  3. package/src/api/client.ts +25 -0
  4. package/src/constants.ts +2 -2
  5. package/src/enums/status.ts +0 -1
  6. package/src/index.ts +2 -1
  7. package/src/interfaces/endpoints/models.ts +1 -1
  8. package/src/interfaces/settings.ts +7 -1
  9. package/src/interfaces/sortBy.ts +4 -0
  10. package/src/managers/command/models.ts +254 -0
  11. package/src/managers/command.ts +34 -461
  12. package/src/managers/events.ts +2 -2
  13. package/src/managers/server.ts +34 -18
  14. package/src/managers/settings.ts +26 -90
  15. package/src/models/baseModel.ts +27 -10
  16. package/src/models/legacyModel.ts +2 -2
  17. package/src/models/routerModel.ts +2 -2
  18. package/src/models/singleModel.ts +1 -1
  19. package/src/server.ts +17 -38
  20. package/src/sse/client.ts +113 -59
  21. package/src/sse/fetch.ts +43 -0
  22. package/src/sse/manager.ts +9 -27
  23. package/src/sse/types.ts +0 -4
  24. package/src/ui/dialog/base.ts +118 -0
  25. package/src/ui/dialog/confirm.ts +45 -0
  26. package/src/ui/dialog/factory.ts +111 -0
  27. package/src/ui/dialog/input.ts +63 -0
  28. package/src/ui/dialog/options.ts +23 -0
  29. package/src/ui/editors/editorOptions.ts +50 -0
  30. package/src/ui/editors/itemBuilder.ts +43 -0
  31. package/src/ui/editors/listEditor.ts +300 -0
  32. package/src/ui/editors/override/entry.ts +24 -0
  33. package/src/ui/editors/override/entryEditor.ts +248 -0
  34. package/src/ui/editors/override/fields/base.ts +53 -0
  35. package/src/ui/editors/override/fields/capabilities.ts +36 -0
  36. package/src/ui/editors/override/fields/cost.ts +64 -0
  37. package/src/ui/editors/override/fields/index.ts +68 -0
  38. package/src/ui/editors/override/fields/numeric.ts +55 -0
  39. package/src/ui/editors/override/fields/pattern.ts +24 -0
  40. package/src/ui/editors/override/fields/reasoning.ts +33 -0
  41. package/src/ui/editors/override/itemBuilder.ts +39 -0
  42. package/src/ui/editors/override/overrideList.ts +127 -0
  43. package/src/ui/editors/server/builder.ts +60 -0
  44. package/src/ui/editors/server/fields.ts +84 -0
  45. package/src/ui/editors/server/itemBuilder.ts +64 -0
  46. package/src/ui/editors/server/serverEditor.ts +197 -0
  47. package/src/ui/editors/server/utils.ts +95 -0
  48. package/src/ui/editors/server/wizard.ts +110 -0
  49. package/src/ui/editors/settingField.ts +37 -0
  50. package/src/ui/editors/settingsListFactory.ts +33 -0
  51. package/src/ui/settings/index.ts +248 -0
  52. package/src/ui/strings.ts +25 -4
  53. package/src/utils/health.ts +2 -1
  54. package/src/utils/serverIds.ts +21 -0
  55. package/src/utils/settingsStore.ts +1 -1
  56. package/src/utils/urlResolver.ts +129 -0
  57. package/src/utils/urls.ts +33 -13
  58. package/tests/{commandManager.test.ts → command/commandManager.test.ts} +88 -12
  59. package/tests/{events.test.ts → events/events.test.ts} +6 -6
  60. package/tests/mocks.ts +2 -0
  61. package/tests/models/legacyModel.test.ts +103 -0
  62. package/tests/{routerModel.test.ts → models/routerModel.test.ts} +71 -72
  63. package/tests/{singleModel.test.ts → models/singleModel.test.ts} +17 -17
  64. package/tests/{health.test.ts → server/health.test.ts} +2 -2
  65. package/tests/{server.test.ts → server/server.test.ts} +5 -17
  66. package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
  67. package/tests/{settings.test.ts → settings/settings.test.ts} +184 -141
  68. package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
  69. package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +8 -26
  70. package/tests/{dialog.test.ts → ui/dialog.test.ts} +56 -4
  71. package/tests/{overrides.test.ts → ui/overrides.test.ts} +101 -111
  72. package/src/ui/dialog.ts +0 -290
  73. package/src/ui/overrideEntryEditor.ts +0 -119
  74. package/src/ui/overrideSettingsList.ts +0 -710
  75. package/src/ui/serverListEditor.ts +0 -59
  76. package/src/ui/serverSettingsList.ts +0 -513
  77. package/tests/legacyModel.test.ts +0 -97
package/src/ui/dialog.ts DELETED
@@ -1,290 +0,0 @@
1
- import { DynamicBorder, type Theme } from "@earendil-works/pi-coding-agent";
2
- import type {
3
- Component,
4
- Keybinding,
5
- SelectListTheme,
6
- TUI,
7
- } from "@earendil-works/pi-tui";
8
- import {
9
- Container,
10
- getKeybindings,
11
- Input,
12
- SelectList,
13
- Spacer,
14
- Text,
15
- } from "@earendil-works/pi-tui";
16
-
17
- /**
18
- * Base dialog class encapsulating shared UI utilities (frame, hints,
19
- * themes) so concrete dialogs don't duplicate code.
20
- */
21
- abstract class BaseDialog {
22
- protected theme: Theme;
23
- protected tui: TUI;
24
-
25
- constructor(theme: Theme, tui: TUI) {
26
- this.theme = theme;
27
- this.tui = tui;
28
- }
29
-
30
- // -- shared utilities ------------------------------------------------------
31
-
32
- protected selectListTheme(): SelectListTheme {
33
- return {
34
- selectedPrefix: (text) => this.theme.fg("accent", text),
35
- selectedText: (text) => this.theme.fg("accent", text),
36
- description: (text) => this.theme.fg("muted", text),
37
- scrollInfo: (text) => this.theme.fg("dim", text),
38
- noMatch: (text) => this.theme.fg("warning", text),
39
- };
40
- }
41
-
42
- protected hint(action: Keybinding, description: string): string {
43
- const keys = getKeybindings().getKeys(action).join("/");
44
- return (
45
- this.theme.fg("dim", keys) + this.theme.fg("muted", ` ${description}`)
46
- );
47
- }
48
-
49
- protected inputFooter(): string {
50
- return `${this.hint("tui.select.confirm", "save")} • ${this.hint("tui.select.cancel", "cancel")}`;
51
- }
52
-
53
- protected selectFooter(): string {
54
- return `${this.hint("tui.select.confirm", "select")} • ${this.hint("tui.select.cancel", "cancel")}`;
55
- }
56
-
57
- protected createFrame(
58
- title: string,
59
- body: Component[],
60
- footer?: string,
61
- ): Container {
62
- const container = new Container();
63
- container.addChild(
64
- new DynamicBorder((text) => this.theme.fg("accent", text)),
65
- );
66
- container.addChild(
67
- new Text(this.theme.fg("accent", this.theme.bold(title)), 1, 0),
68
- );
69
- for (const child of body) container.addChild(child);
70
- if (footer) {
71
- container.addChild(new Spacer(1));
72
- container.addChild(new Text(this.theme.fg("dim", footer), 1, 0));
73
- }
74
- container.addChild(
75
- new DynamicBorder((text) => this.theme.fg("accent", text)),
76
- );
77
- return container;
78
- }
79
- }
80
-
81
- /**
82
- * Shared `SettingItem.submenu` factory for the text fields of both editors.
83
- * Opens an `InputDialog` pre-filled with `initialValue`; Enter commits the
84
- * validated value via `done(value)`, Esc cancels via `done(undefined)`.
85
- */
86
- export class InputDialog extends BaseDialog {
87
- private readonly options: InputDialogOptions;
88
- private readonly body = new Container();
89
- private readonly container: Container;
90
- private readonly input = new Input();
91
- private errorText: Text | undefined;
92
-
93
- constructor(options: InputDialogOptions) {
94
- super(options.theme, options.tui);
95
- this.options = options;
96
-
97
- const initial = options.initialValue ?? "";
98
- this.input.setValue(initial);
99
- for (let i = 0; i < [...initial].length; i++) {
100
- this.input.handleInput("\x1b[C");
101
- }
102
- this.input.onSubmit = (value) => this.submit(value);
103
- this.input.onEscape = () => options.onCancel();
104
-
105
- this.body.addChild(new Text(this.theme.fg("text", options.message), 1, 0));
106
- if (options.placeholder) {
107
- this.body.addChild(
108
- new Text(this.theme.fg("dim", `e.g., ${options.placeholder}`), 1, 0),
109
- );
110
- }
111
- this.body.addChild(this.input);
112
-
113
- this.container = this.createFrame(
114
- options.title,
115
- [this.body],
116
- this.inputFooter(),
117
- );
118
- }
119
-
120
- // -- Component -------------------------------------------------------------
121
-
122
- invalidate(): void {
123
- this.container.invalidate();
124
- }
125
-
126
- handleInput(data: string): void {
127
- this.input.handleInput(data);
128
- this.tui.requestRender();
129
- }
130
-
131
- render(width: number): string[] {
132
- return this.container.render(width);
133
- }
134
-
135
- // -- Focusable ---------------------------------------------------------------
136
-
137
- get focused(): boolean {
138
- return this.input.focused;
139
- }
140
-
141
- set focused(value: boolean) {
142
- this.input.focused = value;
143
- }
144
-
145
- // -- helpers -------------------------------------------------------------------
146
-
147
- private submit(raw: string): void {
148
- const validated = this.options.validate ? this.options.validate(raw) : raw;
149
- if (validated === null) {
150
- this.setError(`Invalid value "${raw}"`);
151
- return;
152
- }
153
- this.options.onSubmit(validated);
154
- }
155
-
156
- private setError(message: string | undefined): void {
157
- if (this.errorText) {
158
- this.body.removeChild(this.errorText);
159
- this.errorText = undefined;
160
- }
161
- if (message) {
162
- this.errorText = new Text(this.theme.fg("error", message), 1, 0);
163
- this.body.addChild(this.errorText);
164
- }
165
- this.tui.requestRender();
166
- }
167
-
168
- /**
169
- * Builds a `SettingItem.submenu` factory that opens an `InputDialog`.
170
- * The dialog prefills from the `currentValue` the containing
171
- * `SettingsList` passes at activation time — not a value captured at
172
- * build time — so re-entering a field after a commit shows the
173
- * just-saved value.
174
- */
175
- static inputSubmenu =
176
- (
177
- theme: Theme,
178
- tui: TUI,
179
- title: string,
180
- message: string,
181
- placeholder: string | undefined,
182
- validate?: (raw: string) => string | null,
183
- ) =>
184
- (
185
- currentValue: string,
186
- done: (selectedValue?: string) => void,
187
- ): Component => {
188
- const dialog = new InputDialog({
189
- theme,
190
- tui,
191
- title,
192
- message,
193
- placeholder,
194
- initialValue: currentValue,
195
- validate,
196
- onSubmit: (value) => done(value),
197
- onCancel: () => done(undefined),
198
- });
199
- dialog.focused = true;
200
- return dialog;
201
- };
202
- }
203
-
204
- export interface InputDialogOptions {
205
- theme: Theme;
206
- tui: TUI;
207
- title: string;
208
- message: string;
209
- placeholder?: string;
210
- initialValue?: string;
211
- validate?: (raw: string) => string | null;
212
- onSubmit: (value: string) => void;
213
- onCancel: () => void;
214
- }
215
-
216
- /**
217
- * A framed confirmation dialog with a two-option `SelectList`
218
- * (Delete / Cancel), matching the confirm pattern of pi's `/llama` view.
219
- */
220
- export class ConfirmDialog extends BaseDialog {
221
- private readonly options: ConfirmDialogOptions;
222
- private readonly container: Container;
223
- private readonly list: SelectList;
224
- private isFocused = false;
225
-
226
- constructor(options: ConfirmDialogOptions) {
227
- super(options.theme, options.tui);
228
- this.options = options;
229
-
230
- this.list = new SelectList(
231
- [
232
- { value: "confirm", label: options.confirmLabel ?? "Delete" },
233
- { value: "cancel", label: options.cancelLabel ?? "Cancel" },
234
- ],
235
- 2,
236
- this.selectListTheme(),
237
- );
238
- this.list.onSelect = (item) => {
239
- if (item.value === "confirm") options.onConfirm();
240
- else options.onCancel();
241
- };
242
- this.list.onCancel = () => options.onCancel();
243
-
244
- this.container = this.createFrame(
245
- options.title,
246
- [
247
- new Text(this.theme.fg("text", options.message), 1, 0),
248
- new Spacer(1),
249
- this.list,
250
- ],
251
- this.selectFooter(),
252
- );
253
- }
254
-
255
- // -- Component -------------------------------------------------------------
256
-
257
- invalidate(): void {
258
- this.container.invalidate();
259
- }
260
-
261
- handleInput(data: string): void {
262
- this.list.handleInput(data);
263
- this.tui.requestRender();
264
- }
265
-
266
- render(width: number): string[] {
267
- return this.container.render(width);
268
- }
269
-
270
- // -- Focusable ---------------------------------------------------------------
271
-
272
- get focused(): boolean {
273
- return this.isFocused;
274
- }
275
-
276
- set focused(value: boolean) {
277
- this.isFocused = value;
278
- }
279
- }
280
-
281
- export interface ConfirmDialogOptions {
282
- theme: Theme;
283
- tui: TUI;
284
- title: string;
285
- message: string;
286
- confirmLabel?: string;
287
- cancelLabel?: string;
288
- onConfirm: () => void;
289
- onCancel: () => void;
290
- }
@@ -1,119 +0,0 @@
1
- import type { ModelCost, ModelCostRates } from "@earendil-works/pi-ai";
2
- import type { LlamaServer, ModelOverride } from "../interfaces/settings";
3
-
4
- /**
5
- * Parses a raw cost-field value. Empty input means zero (treated as unset
6
- * — the key is removed from the settings). Everything else must be a
7
- * finite, non-negative number.
8
- *
9
- * @returns The parsed value, or `null` when the input is invalid
10
- */
11
- export const parseCostValue = (raw: string): number | null => {
12
- const v = raw.trim();
13
- if (v.length === 0) return 0;
14
- const n = Number(v);
15
- if (isNaN(n) || !isFinite(n) || n < 0) return null;
16
- return n;
17
- };
18
-
19
- /**
20
- * Returns a copy of `cost` with `field` set to `value`. A `0` (or
21
- * non-finite) value removes the field — unset cost fields default to
22
- * zero at read time — and when no fields remain, returns `undefined` so
23
- * the whole `cost` object can be dropped. Immutable.
24
- */
25
- export const applyCostFieldValue = (
26
- cost: ModelOverride["cost"],
27
- field: keyof ModelCostRates,
28
- value: number,
29
- ): Partial<ModelCost> | undefined => {
30
- const next = { ...cost };
31
- if (isFinite(value) && value > 0) next[field] = value;
32
- else delete next[field];
33
- return Object.keys(next).length > 0 ? next : undefined;
34
- };
35
-
36
- /**
37
- * Returns a new list with the server at `serverIndex` updated so its
38
- * `overrides` map gains a new `pattern → override` entry. Immutable.
39
- */
40
- export const addOverrideEntry = (
41
- servers: LlamaServer[],
42
- serverIndex: number,
43
- pattern: string,
44
- override: ModelOverride = {},
45
- ): LlamaServer[] =>
46
- servers.map((server, i) => {
47
- if (i !== serverIndex) return server;
48
- const overrides = { ...server.overrides, [pattern]: override };
49
- return { ...server, overrides };
50
- });
51
-
52
- /**
53
- * Returns a new list with the override entry at `entryIndex` (within the
54
- * server's overrides map) replaced by `pattern → override` in a single
55
- * mutation — used both for renaming the pattern and for editing its
56
- * fields. The entry keeps its position in the map's iteration order.
57
- * Immutable.
58
- */
59
- export const updateOverrideEntry = (
60
- servers: LlamaServer[],
61
- serverIndex: number,
62
- entryIndex: number,
63
- pattern: string,
64
- override: ModelOverride,
65
- ): LlamaServer[] =>
66
- servers.map((server, i) => {
67
- if (i !== serverIndex) return server;
68
- const overrides = server.overrides ?? {};
69
- const entries = Object.entries(overrides).map(([k, v], j) =>
70
- j === entryIndex ? [pattern, override] : [k, v],
71
- );
72
- return { ...server, overrides: Object.fromEntries(entries) };
73
- });
74
-
75
- /**
76
- * Returns a new list with the override entry at `entryIndex` (within the
77
- * server's overrides map) removed. Immutable.
78
- */
79
- export const removeOverrideEntry = (
80
- servers: LlamaServer[],
81
- serverIndex: number,
82
- entryIndex: number,
83
- ): LlamaServer[] =>
84
- servers.map((server, i) => {
85
- if (i !== serverIndex) return server;
86
- const overrides = server.overrides ?? {};
87
- const entries = Object.entries(overrides).filter(
88
- (_, j) => j !== entryIndex,
89
- );
90
- return { ...server, overrides: Object.fromEntries(entries) };
91
- });
92
-
93
- /**
94
- * Formats an override as a summary shown in the entry rows:
95
- * `input: $0.2, output: $0.6, cache read: $0.01, capabilities: text,image,
96
- * reasoning: false` — cost fields that are zero or undefined are omitted
97
- * for brevity; `—` when everything is empty.
98
- */
99
- export const formatOverrideSummary = (override: ModelOverride): string => {
100
- const parts: string[] = [];
101
- const cost = override.cost ?? {};
102
- if (cost.input) parts.push(`input: $${cost.input}`);
103
- if (cost.output) parts.push(`output: $${cost.output}`);
104
- if (cost.cacheRead) parts.push(`cache read: $${cost.cacheRead}`);
105
- if (cost.cacheWrite) parts.push(`cache write: $${cost.cacheWrite}`);
106
- if (override.capabilities?.length) {
107
- parts.push(`capabilities: ${override.capabilities.join(",")}`);
108
- }
109
- if (override.reasoning !== undefined) {
110
- parts.push(`reasoning: ${override.reasoning}`);
111
- }
112
- if (override.contextSize !== undefined) {
113
- parts.push(`contextSize: ${override.contextSize}`);
114
- }
115
- if (override.maxTokens !== undefined) {
116
- parts.push(`maxTokens: ${override.maxTokens}`);
117
- }
118
- return parts.length > 0 ? parts.join(", ") : "—";
119
- };