pi-llama-cpp 0.9.2 → 0.11.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 +163 -27
- package/package.json +4 -3
- package/src/api/client.ts +76 -32
- package/src/constants.ts +28 -3
- package/src/index.ts +4 -12
- package/src/interfaces/events.ts +14 -3
- package/src/interfaces/server.ts +20 -0
- package/src/interfaces/settings.ts +69 -0
- package/src/managers/command.ts +285 -43
- package/src/managers/events.ts +50 -12
- package/src/managers/server.ts +90 -17
- package/src/managers/settings.ts +268 -0
- package/src/models/baseModel.ts +32 -17
- package/src/models/legacyModel.ts +2 -2
- package/src/models/routerModel.ts +3 -3
- package/src/server.ts +86 -21
- package/src/sse/client.ts +28 -16
- package/src/sse/manager.ts +23 -10
- package/src/ui/serverListEditor.ts +481 -0
- package/src/utils/errors.ts +5 -0
- package/src/utils/settingsStore.ts +60 -0
- package/src/utils/urls.ts +16 -0
- package/tests/commandManager.test.ts +256 -11
- package/tests/events.test.ts +229 -64
- package/tests/mocks.ts +145 -32
- package/tests/server.test.ts +54 -6
- package/tests/serverListEditor.test.ts +637 -0
- package/tests/serverManager.test.ts +282 -39
- package/tests/settings.test.ts +793 -0
- package/tests/settingsStore.test.ts +209 -0
- package/tests/sseManager.test.ts +98 -0
- package/src/interfaces/auth.ts +0 -6
- package/src/resolver.ts +0 -149
- package/src/utils/cache.ts +0 -39
- package/src/utils/mutex.ts +0 -24
- package/tests/resolver.test.ts +0 -184
|
@@ -1,12 +1,23 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { KeybindingsManager, TUI } from "@earendil-works/pi-tui";
|
|
1
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
4
|
import { Action } from "../src/enums/action";
|
|
3
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
applySettingChange,
|
|
7
|
+
buildSettingsItems,
|
|
8
|
+
CommandManager,
|
|
9
|
+
formatMs,
|
|
10
|
+
} from "../src/managers/command";
|
|
4
11
|
import { ServerManager } from "../src/managers/server";
|
|
12
|
+
import type { LlamaSettingsManager } from "../src/managers/settings";
|
|
13
|
+
import type { Server } from "../src/server";
|
|
14
|
+
import { ServerListEditor } from "../src/ui/serverListEditor";
|
|
5
15
|
import {
|
|
6
16
|
createMockCtx,
|
|
7
17
|
createMockModel,
|
|
8
18
|
createMockPi,
|
|
9
19
|
createMockServer,
|
|
20
|
+
makeSettingsStub,
|
|
10
21
|
mockRpc,
|
|
11
22
|
} from "./mocks";
|
|
12
23
|
|
|
@@ -18,19 +29,26 @@ beforeEach(() => {
|
|
|
18
29
|
describe("CommandManager", () => {
|
|
19
30
|
let serverManager: ServerManager;
|
|
20
31
|
let commandManager: CommandManager;
|
|
32
|
+
let settingsStub: LlamaSettingsManager;
|
|
21
33
|
let mockPi: ReturnType<typeof createMockPi>;
|
|
22
34
|
|
|
23
35
|
beforeEach(() => {
|
|
24
36
|
mockPi = createMockPi();
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
settingsStub = makeSettingsStub();
|
|
38
|
+
serverManager = new ServerManager(settingsStub);
|
|
39
|
+
commandManager = new CommandManager(serverManager, settingsStub);
|
|
27
40
|
});
|
|
28
41
|
|
|
29
42
|
describe("getArgumentCompletions", () => {
|
|
30
43
|
it("should provide completions for /models", () => {
|
|
31
44
|
const completions = commandManager.getArgumentCompletions("");
|
|
32
|
-
expect(completions).toHaveLength(
|
|
33
|
-
expect(completions?.map((c) => c.value)).toEqual([
|
|
45
|
+
expect(completions).toHaveLength(4);
|
|
46
|
+
expect(completions?.map((c) => c.value)).toEqual([
|
|
47
|
+
"info",
|
|
48
|
+
"unload",
|
|
49
|
+
"servers",
|
|
50
|
+
"settings",
|
|
51
|
+
]);
|
|
34
52
|
});
|
|
35
53
|
|
|
36
54
|
it("should filter completions by prefix", () => {
|
|
@@ -43,6 +61,11 @@ describe("CommandManager", () => {
|
|
|
43
61
|
const completions = commandManager.getArgumentCompletions("zzz");
|
|
44
62
|
expect(completions).toBeNull();
|
|
45
63
|
});
|
|
64
|
+
|
|
65
|
+
it("should provide the server/settings completions by prefix", () => {
|
|
66
|
+
const completions = commandManager.getArgumentCompletions("s");
|
|
67
|
+
expect(completions?.map((c) => c.value)).toEqual(["servers", "settings"]);
|
|
68
|
+
});
|
|
46
69
|
});
|
|
47
70
|
|
|
48
71
|
describe("handleCommand", () => {
|
|
@@ -53,8 +76,11 @@ describe("CommandManager", () => {
|
|
|
53
76
|
baseUrl: "http://127.0.0.1:8080",
|
|
54
77
|
models: [model1, model2],
|
|
55
78
|
});
|
|
56
|
-
|
|
57
|
-
|
|
79
|
+
const unloadSettings = makeSettingsStub({
|
|
80
|
+
resolveServers: vi.fn((): Server[] => [server]),
|
|
81
|
+
});
|
|
82
|
+
serverManager = new ServerManager(unloadSettings);
|
|
83
|
+
commandManager = new CommandManager(serverManager, unloadSettings);
|
|
58
84
|
|
|
59
85
|
const ctx = {
|
|
60
86
|
ui: {
|
|
@@ -80,8 +106,11 @@ describe("CommandManager", () => {
|
|
|
80
106
|
baseUrl: "http://127.0.0.1:8080",
|
|
81
107
|
models: [model1, model2],
|
|
82
108
|
});
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
const infoSettings = makeSettingsStub({
|
|
110
|
+
resolveServers: vi.fn((): Server[] => [server]),
|
|
111
|
+
});
|
|
112
|
+
serverManager = new ServerManager(infoSettings);
|
|
113
|
+
commandManager = new CommandManager(serverManager, infoSettings);
|
|
85
114
|
|
|
86
115
|
const ctx = {
|
|
87
116
|
ui: {
|
|
@@ -97,6 +126,184 @@ describe("CommandManager", () => {
|
|
|
97
126
|
});
|
|
98
127
|
});
|
|
99
128
|
|
|
129
|
+
describe("settings menu helpers", () => {
|
|
130
|
+
it("should format milliseconds compactly", () => {
|
|
131
|
+
expect(formatMs(500)).toBe("500ms");
|
|
132
|
+
expect(formatMs(60000)).toBe("60s");
|
|
133
|
+
expect(formatMs(1500)).toBe("1500ms");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should build one item per editable scalar field", () => {
|
|
137
|
+
const items = buildSettingsItems(settingsStub);
|
|
138
|
+
expect(items.map((i) => i.id)).toEqual([
|
|
139
|
+
"reactToModelSelect",
|
|
140
|
+
"autoloadOnMessage",
|
|
141
|
+
"sortBy",
|
|
142
|
+
"pollingTimeout",
|
|
143
|
+
"serverTimeout",
|
|
144
|
+
]);
|
|
145
|
+
// Booleans are displayed as on/off
|
|
146
|
+
expect(items[0].values).toEqual(["on", "off"]);
|
|
147
|
+
// Timeouts cycle through formatted presets
|
|
148
|
+
expect(items[3].values).toEqual(["15s", "30s", "60s", "120s", "300s"]);
|
|
149
|
+
expect(items[4].values).toEqual(["500ms", "1s", "2s", "5s", "10s"]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should map changes to the right key and type", async () => {
|
|
153
|
+
const settingsStub = makeSettingsStub();
|
|
154
|
+
|
|
155
|
+
await applySettingChange("reactToModelSelect", "on", settingsStub);
|
|
156
|
+
expect(settingsStub.setLlamaSetting).toHaveBeenCalledWith(
|
|
157
|
+
"reactToModelSelect",
|
|
158
|
+
true,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
await applySettingChange("autoloadOnMessage", "off", settingsStub);
|
|
162
|
+
expect(settingsStub.setLlamaSetting).toHaveBeenCalledWith(
|
|
163
|
+
"autoloadOnMessage",
|
|
164
|
+
false,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
await applySettingChange("sortBy", "desc-name", settingsStub);
|
|
168
|
+
expect(settingsStub.setLlamaSetting).toHaveBeenCalledWith(
|
|
169
|
+
"sortBy",
|
|
170
|
+
"desc-name",
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await applySettingChange("pollingTimeout", "120s", settingsStub);
|
|
174
|
+
expect(settingsStub.setLlamaSetting).toHaveBeenCalledWith(
|
|
175
|
+
"pollingTimeout",
|
|
176
|
+
120000,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await applySettingChange("serverTimeout", "500ms", settingsStub);
|
|
180
|
+
expect(settingsStub.setLlamaSetting).toHaveBeenCalledWith(
|
|
181
|
+
"serverTimeout",
|
|
182
|
+
500,
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("/models settings menu", () => {
|
|
188
|
+
it("should open the settings menu without touching servers", async () => {
|
|
189
|
+
const updateSpy = vi
|
|
190
|
+
.spyOn(serverManager, "update")
|
|
191
|
+
.mockResolvedValue(undefined);
|
|
192
|
+
const ctx = createMockCtx(() => null);
|
|
193
|
+
|
|
194
|
+
await commandManager.handleCommand("settings", ctx as any, mockPi as any);
|
|
195
|
+
|
|
196
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
197
|
+
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
|
198
|
+
expect(ctx.ui.custom).toHaveBeenCalledTimes(1);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should notify instead of opening the menu outside the TUI", async () => {
|
|
202
|
+
const ctx = { ...createMockCtx(() => null), mode: "rpc" } as any;
|
|
203
|
+
|
|
204
|
+
await commandManager.handleCommand("settings", ctx, mockPi as any);
|
|
205
|
+
|
|
206
|
+
expect(ctx.ui.custom).not.toHaveBeenCalled();
|
|
207
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
208
|
+
"/models settings requires an interactive session (TUI)",
|
|
209
|
+
"warning",
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("/models servers editor", () => {
|
|
215
|
+
const ESC = "\x1b";
|
|
216
|
+
const ENTER = "\r";
|
|
217
|
+
|
|
218
|
+
const createMockKeybindings = (): KeybindingsManager =>
|
|
219
|
+
({
|
|
220
|
+
matches: vi.fn(
|
|
221
|
+
(data: string, name: string) =>
|
|
222
|
+
(data === ENTER && name === "tui.select.confirm") ||
|
|
223
|
+
(data === ESC && name === "tui.select.cancel"),
|
|
224
|
+
),
|
|
225
|
+
}) as unknown as KeybindingsManager;
|
|
226
|
+
|
|
227
|
+
const createMockTheme = (): Theme =>
|
|
228
|
+
({
|
|
229
|
+
fg: (_color: string, text: string) => text,
|
|
230
|
+
bold: (text: string) => text,
|
|
231
|
+
}) as unknown as Theme;
|
|
232
|
+
|
|
233
|
+
it("should open the editor without touching servers", async () => {
|
|
234
|
+
const updateSpy = vi
|
|
235
|
+
.spyOn(serverManager, "update")
|
|
236
|
+
.mockResolvedValue(undefined);
|
|
237
|
+
const ctx = createMockCtx(() => null);
|
|
238
|
+
|
|
239
|
+
await commandManager.handleCommand("servers", ctx as any, mockPi as any);
|
|
240
|
+
|
|
241
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
242
|
+
expect(ctx.ui.custom).toHaveBeenCalledTimes(1);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should notify instead of opening the editor outside the TUI", async () => {
|
|
246
|
+
const ctx = { ...createMockCtx(() => null), mode: "rpc" } as any;
|
|
247
|
+
|
|
248
|
+
await commandManager.handleCommand("servers", ctx, mockPi as any);
|
|
249
|
+
|
|
250
|
+
expect(ctx.ui.custom).not.toHaveBeenCalled();
|
|
251
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
252
|
+
"/models servers requires an interactive session (TUI)",
|
|
253
|
+
"warning",
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should wire the editor to the merged servers and the write path", async () => {
|
|
258
|
+
const editorSettings = makeSettingsStub({
|
|
259
|
+
llamaServers: [{ url: "http://seed:1" }],
|
|
260
|
+
});
|
|
261
|
+
commandManager = new CommandManager(serverManager, editorSettings);
|
|
262
|
+
const ctx = createMockCtx(() => null);
|
|
263
|
+
|
|
264
|
+
await commandManager.handleCommand("servers", ctx as any, mockPi as any);
|
|
265
|
+
|
|
266
|
+
// Invoke the factory captured from ctx.ui.custom
|
|
267
|
+
const factory = vi.mocked(ctx.ui.custom).mock.calls[0][0] as (
|
|
268
|
+
tui: TUI,
|
|
269
|
+
theme: Theme,
|
|
270
|
+
kb: KeybindingsManager,
|
|
271
|
+
done: (result: undefined) => void,
|
|
272
|
+
) => ServerListEditor;
|
|
273
|
+
const done = vi.fn();
|
|
274
|
+
const editor = factory(
|
|
275
|
+
{ requestRender: vi.fn() } as unknown as TUI,
|
|
276
|
+
createMockTheme(),
|
|
277
|
+
{
|
|
278
|
+
matches: vi.fn(
|
|
279
|
+
(data: string, name: string) =>
|
|
280
|
+
(data === ENTER && name === "tui.select.confirm") ||
|
|
281
|
+
(data === ESC && name === "tui.select.cancel"),
|
|
282
|
+
),
|
|
283
|
+
} as unknown as KeybindingsManager,
|
|
284
|
+
done,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
expect(editor).toBeInstanceOf(ServerListEditor);
|
|
288
|
+
// Seeded with the merged snapshot
|
|
289
|
+
expect(editor.render(80).join("\n")).toContain("http://seed:1");
|
|
290
|
+
|
|
291
|
+
// Add a server through the editor → setLlamaSetting("servers", …)
|
|
292
|
+
editor.handleInput("a");
|
|
293
|
+
for (const ch of "http://new:2") editor.handleInput(ch);
|
|
294
|
+
editor.handleInput(ENTER);
|
|
295
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
296
|
+
expect(editorSettings.setLlamaSetting).toHaveBeenCalledWith("servers", [
|
|
297
|
+
{ url: "http://seed:1" },
|
|
298
|
+
{ url: "http://new:2" },
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
// Esc closes the editor
|
|
302
|
+
editor.handleInput(ESC);
|
|
303
|
+
expect(done).toHaveBeenCalledTimes(1);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
100
307
|
describe("/models interactive menu", () => {
|
|
101
308
|
const CHOICE = "model-a [Server: http://127.0.0.1:8080]";
|
|
102
309
|
|
|
@@ -113,10 +320,15 @@ describe("CommandManager", () => {
|
|
|
113
320
|
models: [model],
|
|
114
321
|
}),
|
|
115
322
|
);
|
|
116
|
-
const
|
|
323
|
+
const settingsStub = makeSettingsStub({
|
|
324
|
+
resolveServers: vi.fn((): Server[] => servers),
|
|
325
|
+
});
|
|
326
|
+
const serverManager = new ServerManager(settingsStub);
|
|
117
327
|
return {
|
|
118
|
-
commandManager: new CommandManager(serverManager),
|
|
328
|
+
commandManager: new CommandManager(serverManager, settingsStub),
|
|
119
329
|
serverManager,
|
|
330
|
+
settingsStub,
|
|
331
|
+
servers,
|
|
120
332
|
mockPi,
|
|
121
333
|
};
|
|
122
334
|
};
|
|
@@ -198,5 +410,38 @@ describe("CommandManager", () => {
|
|
|
198
410
|
expect(ctx.ui.select).toHaveBeenCalledTimes(3);
|
|
199
411
|
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
|
200
412
|
});
|
|
413
|
+
|
|
414
|
+
it("should pick up servers added via the editor on the next /models scan", async () => {
|
|
415
|
+
const { commandManager, settingsStub, servers } = createCommandManager([
|
|
416
|
+
createMockModel("model-a"),
|
|
417
|
+
]);
|
|
418
|
+
|
|
419
|
+
const ctx = createMockCtx(() => null);
|
|
420
|
+
await commandManager.handleCommand("", ctx as any, mockPi as any);
|
|
421
|
+
|
|
422
|
+
// First scan: only model-a is offered
|
|
423
|
+
let choices = vi.mocked(ctx.ui.select).mock.calls[0][1] as string[];
|
|
424
|
+
expect(choices).toHaveLength(1);
|
|
425
|
+
expect(choices[0]).toContain("model-a");
|
|
426
|
+
|
|
427
|
+
// Editor persist → settings change → resolveServers now returns both
|
|
428
|
+
const modelB = createMockModel("model-b", {
|
|
429
|
+
serverUrl: "http://127.0.0.1:8081",
|
|
430
|
+
});
|
|
431
|
+
const serverB = createMockServer({
|
|
432
|
+
baseUrl: "http://127.0.0.1:8081",
|
|
433
|
+
models: [modelB],
|
|
434
|
+
});
|
|
435
|
+
vi.mocked(settingsStub.resolveServers).mockReturnValue([
|
|
436
|
+
...servers,
|
|
437
|
+
serverB,
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
await commandManager.handleCommand("", ctx as any, mockPi as any);
|
|
441
|
+
|
|
442
|
+
choices = vi.mocked(ctx.ui.select).mock.calls[1][1] as string[];
|
|
443
|
+
expect(choices).toHaveLength(2);
|
|
444
|
+
expect(choices[1]).toContain("model-b");
|
|
445
|
+
});
|
|
201
446
|
});
|
|
202
447
|
});
|