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
@@ -1,54 +1,16 @@
1
- import {
2
- getSettingsListTheme,
3
- type ExtensionAPI,
4
- type ExtensionCommandContext,
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
5
4
  } from "@earendil-works/pi-coding-agent";
6
- import {
7
- AutocompleteItem,
8
- SettingsList,
9
- type SettingItem,
10
- } from "@earendil-works/pi-tui";
5
+ import { AutocompleteItem } from "@earendil-works/pi-tui";
11
6
  import { PROVIDER_NAME } from "../constants";
12
- import { Action } from "../enums/action";
13
- import { Mode } from "../enums/mode";
14
- import { Status } from "../enums/status";
15
- import { LlamaSettings } from "../interfaces/settings";
16
- import { BaseModel } from "../models/baseModel";
17
- import { createOverrideSettingsList } from "../ui/overrideSettingsList";
18
- import { ServerSettingsList } from "../ui/serverSettingsList";
19
- import { errorMessage } from "../utils/errors";
20
- import { EventManager } from "./events";
7
+ import { OverrideSettingsList } from "../ui/editors/override/overrideList";
8
+ import { ServerSettingsList } from "../ui/editors/server/serverEditor";
9
+ import { SettingsEditor } from "../ui/settings";
10
+ import { ModelsMenu } from "./command/models";
21
11
  import { ServerManager } from "./server";
22
12
  import type { LlamaSettingsManager } from "./settings";
23
13
 
24
- /**
25
- * Identifiers of the editable fields shown in `/models settings`.
26
- * Values match the scalar `LlamaSettings` keys.
27
- */
28
- export enum Options {
29
- REACT_TO_MODEL_SELECT = "reactToModelSelect",
30
- AUTOLOAD_ON_MESSAGE = "autoloadOnMessage",
31
- SORT_BY = "sortBy",
32
- POLLING_TIMEOUT = "pollingTimeout",
33
- SERVER_TIMEOUT = "serverTimeout",
34
- }
35
-
36
- type SortByValue = NonNullable<LlamaSettings["sortBy"]>;
37
-
38
- const SORT_VALUES: SortByValue[] = [
39
- "asc",
40
- "desc",
41
- "asc-name",
42
- "desc-name",
43
- "api",
44
- ];
45
-
46
- /** Presets (ms) for `pollingTimeout` */
47
- const POLLING_PRESETS = [15000, 30000, 60000, 120000, 300000];
48
-
49
- /** Presets (ms) for `serverTimeout` */
50
- const SERVER_PRESETS = [500, 1000, 2000, 5000, 10000];
51
-
52
14
  /**
53
15
  * `/models` subcommand completions. Module-level so
54
16
  * {@link CommandManager.getArgumentCompletions} doesn't rebuild the
@@ -82,105 +44,15 @@ const ARGUMENT_COMPLETIONS: AutocompleteItem[] = [
82
44
  },
83
45
  ];
84
46
 
85
- /**
86
- * Formats milliseconds compactly for display (e.g. `500 -> "500ms"`,
87
- * `60000 -> "60s"`).
88
- */
89
- export const formatMs = (ms: number): string =>
90
- ms % 1000 === 0 ? `${ms / 1000}s` : `${ms}ms`;
91
-
92
- /**
93
- * Parses a value produced by `formatMs()` back to milliseconds.
94
- * Only ever called with values from the preset lists.
95
- */
96
- const parseMs = (value: string): number =>
97
- value.endsWith("ms")
98
- ? Number(value.slice(0, -2))
99
- : Number(value.slice(0, -1)) * 1000;
100
-
101
- /**
102
- * Builds the `SettingsList` items for `/models settings` from the current
103
- * (merged) values of the scalar `llamaSettings` fields.
104
- */
105
- export const buildSettingsItems = async (
106
- settings: LlamaSettingsManager,
107
- ): Promise<SettingItem[]> => {
108
- const { pollingTimeout, serverTimeout } = await settings.resolveTimeouts();
109
-
110
- return [
111
- {
112
- id: Options.REACT_TO_MODEL_SELECT,
113
- label: "React to model selection",
114
- description: "Load the model when you pick it in Pi (immediate)",
115
- currentValue: (await settings.resolveReactToModelSelect()) ? "on" : "off",
116
- values: ["on", "off"],
117
- },
118
- {
119
- id: Options.AUTOLOAD_ON_MESSAGE,
120
- label: "Autoload on message",
121
- description:
122
- "Auto-load the selected model when you send a message (immediate)",
123
- currentValue: (await settings.resolveAutoloadOnMessage()) ? "on" : "off",
124
- values: ["on", "off"],
125
- },
126
- {
127
- id: Options.SORT_BY,
128
- label: "Sort models by",
129
- description: "Order of models in /models (next open)",
130
- currentValue: await settings.resolveSortBy(),
131
- values: [...SORT_VALUES],
132
- },
133
- {
134
- id: Options.POLLING_TIMEOUT,
135
- label: "Polling timeout",
136
- description: "Max model-load wait (next model load)",
137
- currentValue: formatMs(pollingTimeout),
138
- values: POLLING_PRESETS.map(formatMs),
139
- },
140
- {
141
- id: Options.SERVER_TIMEOUT,
142
- label: "Server timeout",
143
- description: "Health check / SSE probe timeout (next model load)",
144
- currentValue: formatMs(serverTimeout),
145
- values: SERVER_PRESETS.map(formatMs),
146
- },
147
- ];
148
- };
149
-
150
- /**
151
- * Persists a change made in the settings menu.
152
- * Maps the `SettingsList` id/value pair to the matching `llamaSettings`
153
- * key and writes it via `LlamaSettingsManager.setLlamaSetting()`.
154
- */
155
- export const applySettingChange = async (
156
- id: string,
157
- newValue: string,
158
- settings: LlamaSettingsManager,
159
- ): Promise<void> => {
160
- switch (id) {
161
- case Options.REACT_TO_MODEL_SELECT:
162
- await settings.setLlamaSetting("reactToModelSelect", newValue === "on");
163
- return;
164
- case Options.AUTOLOAD_ON_MESSAGE:
165
- await settings.setLlamaSetting("autoloadOnMessage", newValue === "on");
166
- return;
167
- case Options.SORT_BY:
168
- await settings.setLlamaSetting("sortBy", newValue as SortByValue);
169
- return;
170
- case Options.POLLING_TIMEOUT:
171
- await settings.setLlamaSetting("pollingTimeout", parseMs(newValue));
172
- return;
173
- case Options.SERVER_TIMEOUT:
174
- await settings.setLlamaSetting("serverTimeout", parseMs(newValue));
175
- return;
176
- }
177
- };
178
-
179
47
  export class CommandManager {
48
+ private readonly modelsMenu: ModelsMenu;
49
+
180
50
  constructor(
181
51
  private readonly serverManager: ServerManager,
182
52
  private readonly settings: LlamaSettingsManager,
183
- ) {}
53
+ ) {
54
+ this.modelsMenu = new ModelsMenu(serverManager);
55
+ }
184
56
 
185
57
  /**
186
58
  * Sets up the argument completions for the `/models` command
@@ -207,8 +79,8 @@ export class CommandManager {
207
79
  ctx: ExtensionCommandContext,
208
80
  pi: ExtensionAPI,
209
81
  ) {
210
- // Settings menu: no network round-trip needed, handle before any
211
- // server updates / unreachable-server notifications
82
+ // Settings menu: no provider re-registration needed (sortBy, timeouts,
83
+ // etc. don't affect the model registry)
212
84
  if (args === "settings") {
213
85
  await this.runSettingsMenu(ctx);
214
86
  return;
@@ -251,18 +123,12 @@ export class CommandManager {
251
123
  }
252
124
 
253
125
  // Interactive menu: show <name> (<server_url>)
254
- await this.runModelsMenu(ctx, pi);
126
+ await this.modelsMenu.show(ctx, pi);
255
127
  }
256
128
 
257
129
  /**
258
130
  * Runs the interactive settings menu for the scalar `llamaSettings`
259
131
  * fields. Enter/Space cycles the value under the cursor; Esc closes.
260
- *
261
- * Writes go to the global `~/.pi/agent/settings.json` via
262
- * `LlamaSettingsManager.setLlamaSetting()`; write errors are notified
263
- * and leave the dialog open with values unchanged. These settings
264
- * (reactToModelSelect, autoloadOnMessage, sortBy, timeouts) do not
265
- * require provider re-registration.
266
132
  */
267
133
  private async runSettingsMenu(ctx: ExtensionCommandContext): Promise<void> {
268
134
  if (ctx.mode !== "tui") {
@@ -273,36 +139,12 @@ export class CommandManager {
273
139
  return;
274
140
  }
275
141
 
276
- const items = await buildSettingsItems(this.settings);
277
-
278
- await ctx.ui.custom<void>(
279
- (_tui, _theme, _kb, done) =>
280
- new SettingsList(
281
- items,
282
- Math.min(items.length + 2, 15),
283
- getSettingsListTheme(),
284
- (id, newValue) => {
285
- applySettingChange(id, newValue, this.settings).catch(
286
- (err: unknown) => {
287
- const message = errorMessage(err);
288
- ctx.ui.notify(message, "error");
289
- },
290
- );
291
- },
292
- () => done(undefined),
293
- ),
294
- );
142
+ await SettingsEditor.show(ctx.ui, this.settings);
295
143
  }
296
144
 
297
145
  /**
298
- * Runs the interactive servers editor for `llamaSettings.servers`.
299
- * Enter on a server row drills into its field-edit submenu (URL/id/name);
300
- * a adds a new server (inline Input), d deletes (after confirmation);
301
- * Esc closes.
302
- *
303
- * Writes go to the global `~/.pi/agent/settings.json` via
304
- * `LlamaSettingsManager.setLlamaSetting()`; write errors are notified and
305
- * the editor stays open with the pre-mutation list. After closing,
146
+ * Runs the interactive servers editor for `llamaSettings.servers`
147
+ * (see `ServerSettingsList` for the editing semantics). After closing,
306
148
  * providers are re-registered so server changes apply immediately.
307
149
  */
308
150
  private async runServersEditor(
@@ -317,49 +159,17 @@ export class CommandManager {
317
159
  return;
318
160
  }
319
161
 
320
- const servers = await this.settings.getLlamaServers();
321
- const { serverTimeout } = await this.settings.resolveTimeouts();
162
+ await ServerSettingsList.show(ctx.ui, this.settings);
322
163
 
323
- await ctx.ui.custom<void>(
324
- (tui, theme, keybindings, done) =>
325
- new ServerSettingsList({
326
- tui,
327
- theme,
328
- keybindings,
329
- servers,
330
- persist: (next) => this.settings.setLlamaSetting("servers", next),
331
- done: () => {
332
- done(undefined);
333
- // Re-register providers so the updated server list takes effect
334
- this.serverManager.update(pi);
335
- },
336
- onError: (message) => ctx.ui.notify(message, "error"),
337
- serverTimeout,
338
- }),
339
- );
164
+ // Re-register providers so the updated server list takes effect
165
+ await this.serverManager.update(pi);
340
166
  }
341
167
 
342
168
  /**
343
169
  * Runs the interactive overrides editor for
344
- * `llamaSettings.servers[].overrides`: a SettingsList of servers drilling
345
- * down into each server's override entries (one row per pattern, with
346
- * add/delete support). Within a server's entry list: Enter drills into
347
- * the field-edit submenu; a adds, d deletes (after confirmation).
348
- *
349
- * Fields use a mix of finite (Enter to cycle) and infinite (Enter to
350
- * type) editing:
351
- *
352
- * - Pattern / costs (input, output, cacheRead, cacheWrite): infinite —
353
- * Enter opens an Input for typing.
354
- * - Capabilities: finite — Enter cycles between `text` and `text | image`.
355
- * - Reasoning: finite — Enter cycles between `true` and `false`.
356
- *
357
- * Servers themselves are not managed here — use `/models servers`.
358
- *
359
- * Writes go to the global `~/.pi/agent/settings.json` via
360
- * `LlamaSettingsManager.setLlamaSetting()`; write errors are notified and
361
- * leave the values unchanged. After closing, providers are
362
- * re-registered so new overrides take effect on the next request.
170
+ * `llamaSettings.servers[].overrides` (see `OverrideSettingsList` for
171
+ * the editing semantics). After closing, providers are re-registered
172
+ * so new overrides take effect on the next request.
363
173
  */
364
174
  private async runOverridesEditor(
365
175
  ctx: ExtensionCommandContext,
@@ -373,23 +183,10 @@ export class CommandManager {
373
183
  return;
374
184
  }
375
185
 
376
- const servers = await this.settings.getLlamaServers();
377
- await ctx.ui.custom<void>((tui, theme, keybindings, done) =>
378
- createOverrideSettingsList({
379
- tui,
380
- theme,
381
- keybindings,
382
- servers,
383
- persist: (next) => this.settings.setLlamaSetting("servers", next),
384
- done: () => {
385
- done(undefined);
386
- // Re-register providers so the updated overrides take effect
387
- this.serverManager.update(pi);
388
- },
389
- onError: (message) => ctx.ui.notify(message, "error"),
390
- onChanged: () => {}, // no per-change notification needed
391
- }),
392
- );
186
+ await OverrideSettingsList.show(ctx.ui, this.settings);
187
+
188
+ // Re-register providers so the updated overrides take effect
189
+ await this.serverManager.update(pi);
393
190
  }
394
191
 
395
192
  /**
@@ -398,233 +195,4 @@ export class CommandManager {
398
195
  private notifyNotFound(ctx: ExtensionCommandContext, url: string): void {
399
196
  ctx.ui.notify(`${PROVIDER_NAME} unreachable at ${url}`, "error");
400
197
  }
401
-
402
- /**
403
- * Runs the interactive model selection menu.
404
- */
405
- private async runModelsMenu(
406
- ctx: ExtensionCommandContext,
407
- pi: ExtensionAPI,
408
- ): Promise<void> {
409
- const event = await this.modelSelectionHandler(
410
- ctx,
411
- await this.serverManager.getAllModels(),
412
- );
413
-
414
- if (!event) return;
415
- const { action, model } = event;
416
-
417
- // Action: Cancel
418
- if (!action || action === Action.CANCEL) return;
419
-
420
- // Action: Info
421
- if (action === Action.INFO) {
422
- const info = await model.getInfo();
423
- ctx.ui.notify(`${info}`, "info");
424
- return;
425
- }
426
-
427
- // Action: Unload
428
- if (action === Action.UNLOAD) {
429
- await model.unload();
430
- ctx.ui.notify(`Unloaded ${model.name}`, "info");
431
- return;
432
- }
433
-
434
- // Action: Switch
435
- if (action === Action.SWITCH) {
436
- const { serverId } = model;
437
- const piModel = ctx.modelRegistry.find(serverId, model.id);
438
- if (!piModel)
439
- throw new Error(`Cannot find model ${model.name} in pi registry`);
440
-
441
- await pi.setModel(piModel);
442
- ctx.ui.notify(`Model ${model.name} ready`, "info");
443
- return;
444
- }
445
-
446
- // Actions: Load / Load & Switch / Retry
447
- const loadActions = [Action.LOAD, Action.LOAD_AND_SWITCH, Action.RETRY];
448
- if (loadActions.includes(action)) {
449
- ctx.ui.notify(`Loading ${model.name}...`, "info");
450
- // Mark the load as in-flight so session_before_switch can warn about
451
- // it (see EventManager.inflightModel for the coupling rationale)
452
- EventManager.inflightModel = model;
453
-
454
- // Subscribe to progress events; skip when the server is gone
455
- // (removed/edited away mid-load → getServer returns undefined)
456
- const server = this.serverManager.getServer(model);
457
- const cleanupProgress =
458
- server?.sseManager.subscribeToProgress(
459
- model.id,
460
- (percentage, stage) => {
461
- const stageText = stage ? ` (${stage})` : "";
462
- ctx.ui.notify(
463
- `Loading ${model.name}... [${percentage}%${stageText}]`,
464
- "info",
465
- );
466
- },
467
- ) ?? (() => {});
468
-
469
- const onSuccess = async () => {
470
- const { serverId } = model;
471
- const piModel = ctx.modelRegistry.find(serverId, model.id);
472
- if (!piModel)
473
- throw new Error(`Cannot find model ${model.name} in pi registry`);
474
-
475
- // Verify auth
476
- if ((await model.getStatus()) === Status.UNAUTHORIZED)
477
- throw new Error(
478
- `Unauthorized for ${model.name}. Use /login and add your API key.`,
479
- );
480
-
481
- // Verify failure
482
- if ((await model.getStatus()) === Status.FAILED)
483
- throw new Error(`Failed to load model ${model.name}`);
484
-
485
- // Select the model if asked
486
- if (action === Action.LOAD_AND_SWITCH) await pi.setModel(piModel);
487
-
488
- ctx.ui.notify(`Model ${model.name} ready`, "info");
489
- };
490
-
491
- const onFailure = (err: any) => {
492
- const message = errorMessage(err);
493
-
494
- try {
495
- ctx.ui.notify(message, "error");
496
- } catch {
497
- // ctx went stale between error and notification
498
- }
499
- };
500
-
501
- const onFinished = async () => {
502
- cleanupProgress();
503
- EventManager.resetInflightModel();
504
-
505
- // Re-scan providers to ensure accuracy of loaded models
506
- await this.serverManager.update(pi);
507
-
508
- // Force TUI refresh so Pi picks up the updated model states
509
- ctx.ui.setStatus(PROVIDER_NAME, " ");
510
- ctx.ui.setStatus(PROVIDER_NAME, undefined);
511
- };
512
-
513
- // Load the model without blocking the UI
514
- model.load().then(onSuccess).catch(onFailure).finally(onFinished);
515
- }
516
- }
517
-
518
- /**
519
- * Handles the menu for model selection.
520
- * Loops: select model → select action → handle action.
521
- *
522
- * Escape on actions menu goes back to model selection.
523
- * Escape on model selection exits.
524
- *
525
- * @returns The selected action and model
526
- */
527
- private async modelSelectionHandler(
528
- ctx: ExtensionCommandContext,
529
- models: BaseModel[],
530
- ): Promise<{ action: Action; model: BaseModel } | null> {
531
- while (true) {
532
- // Select the model
533
- const model = await this.selectModel(ctx, models);
534
- if (!model) return null;
535
-
536
- // Select the action
537
- const actions = await this.getActionsForModel(model);
538
- const action = await this.selectAction(ctx, model, actions);
539
- if (action === null) {
540
- // Escape key pressed => back to model selection
541
- continue;
542
- }
543
-
544
- // Return the selected action and model
545
- return { action, model };
546
- }
547
- }
548
-
549
- /**
550
- * Select a model from the list. Returns null if user cancels.
551
- *
552
- * @returns The model selected by the user
553
- */
554
- private async selectModel(
555
- ctx: ExtensionCommandContext,
556
- models: BaseModel[],
557
- ): Promise<BaseModel | null> {
558
- const labels = await Promise.all(
559
- models.map(async (model) => ({
560
- label: (await model.getLabel()).trim(),
561
- serverUrl: model.serverUrl,
562
- })),
563
- );
564
-
565
- // Count grapheme clusters (not UTF-16 code units) so emoji padding aligns visually
566
- const graphemeLength = (str: string) =>
567
- [...new Intl.Segmenter().segment(str)].length;
568
-
569
- // Decorate the label so the spacing makes it seem more like a table
570
- const maxLength = Math.max(
571
- ...labels.map(({ label }) => graphemeLength(label)),
572
- );
573
- const choices = labels.map(({ label, serverUrl }) => {
574
- const extraPadding = 2;
575
- const padLen = maxLength - graphemeLength(label) + extraPadding;
576
- return `${label}${" ".repeat(padLen)} [Server: ${serverUrl}]`;
577
- });
578
-
579
- const choice = await ctx.ui.select(`${PROVIDER_NAME} models:`, choices);
580
- if (!choice) return null;
581
- const idx = choices.indexOf(choice);
582
-
583
- return models[idx];
584
- }
585
-
586
- /**
587
- * Get available actions for a model based on its mode and status.
588
- *
589
- * @returns A mapping of actions for each status
590
- */
591
- private async getActionsForModel(model: BaseModel): Promise<Array<Action>> {
592
- const base = [Action.INFO, Action.CANCEL];
593
-
594
- const actions: Record<Status, Array<Action>> = {
595
- [Status.LOADED]:
596
- model.mode === Mode.ROUTER
597
- ? [Action.SWITCH, Action.UNLOAD, ...base]
598
- : [Action.SWITCH, ...base],
599
- [Status.LOADING]: [...base],
600
- [Status.FAILED]: [Action.RETRY, ...base],
601
- [Status.SLEEPING]:
602
- model.mode === Mode.ROUTER
603
- ? [Action.SWITCH, Action.UNLOAD, ...base]
604
- : [Action.SWITCH, ...base],
605
- [Status.UNLOADED]: [Action.LOAD_AND_SWITCH, Action.LOAD, ...base],
606
- [Status.UNAUTHORIZED]: [...base],
607
- };
608
-
609
- const status = await model.getStatus();
610
- return actions[status];
611
- }
612
-
613
- /**
614
- * Selects an action for a model.
615
- *
616
- * @returns The selected action
617
- */
618
- private async selectAction(
619
- ctx: ExtensionCommandContext,
620
- model: BaseModel,
621
- actions: Array<Action>,
622
- ): Promise<Action | null> {
623
- const labels = actions.map((a) => String(a));
624
- const choice = await ctx.ui.select(`${model.name}`, labels);
625
- if (!choice) return null;
626
-
627
- const idx = labels.indexOf(choice);
628
- return actions[idx];
629
- }
630
198
  }
@@ -11,8 +11,8 @@ import { ServerManager } from "./server";
11
11
 
12
12
  export class EventManager {
13
13
  /**
14
- * Model with a load currently in flight. Deliberately a class static
15
- * (REFACTOR.md §3.3): the load is started by CommandManager
14
+ * Model with a load currently in flight. Deliberately a class static:
15
+ * the load is started by CommandManager
16
16
  * (fire-and-forget from the /models editor) while the "session switched
17
17
  * mid-load" warning must be emitted here, from the session_before_switch
18
18
  * hook — a shared static is the least-plumbing bridge between the two
@@ -1,6 +1,8 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { API_TYPE, PROVIDER_NAME, type SortBy } from "../constants";
2
+ import { ApiError } from "../api/client";
3
+ import { API_TYPE, PROVIDER_NAME } from "../constants";
3
4
  import { ServerStatus } from "../enums/serverStatus";
5
+ import type { SortBy } from "../interfaces/sortBy";
4
6
  import { BaseModel } from "../models/baseModel";
5
7
  import { Server } from "../server";
6
8
  import type { LlamaSettingsManager } from "./settings";
@@ -80,7 +82,28 @@ export class ServerManager {
80
82
  try {
81
83
  await server.initialize();
82
84
  await this.registerProvider(server, pi);
83
- } catch {
85
+ } catch (err) {
86
+ if (err instanceof ApiError && err.type === "authentication") {
87
+ // Register the provider with an empty model list so the user can
88
+ // still configure the API key via `/login` or `auth.json`. On the
89
+ // next scan the provider will re-initialize and discover models.
90
+ // Don't add to `failedUrls` — the server IS reachable, auth just
91
+ // isn't configured yet, so the health indicator should stay green.
92
+ const message = [
93
+ "[pi-llama-cpp]",
94
+ `Server at '${server.baseUrl}' requires a valid API key.`,
95
+ "Configure the key via `/login` or in `~/.pi/agent/auth.json`.",
96
+ ].join("\n");
97
+ this.warnings.push(message);
98
+ pi.registerProvider(server.providerId, {
99
+ name: server.providerName,
100
+ baseUrl: server.apiBaseUrl,
101
+ api: API_TYPE,
102
+ apiKey: server.getApiKey(),
103
+ models: [],
104
+ });
105
+ continue;
106
+ }
84
107
  this.failedUrls.push(server.baseUrl);
85
108
  continue;
86
109
  }
@@ -128,9 +151,10 @@ export class ServerManager {
128
151
  }
129
152
 
130
153
  /**
131
- * Creates a Pi provider for the given server
154
+ * Creates a Pi provider for the given server.
132
155
  *
133
156
  * @param server The server
157
+ * @param pi The Pi API
134
158
  */
135
159
  private async registerProvider(server: Server, pi: ExtensionAPI) {
136
160
  const { apiBaseUrl, models, providerId, providerName } = server;