pi-llama-cpp 0.14.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 (47) hide show
  1. package/README.md +12 -10
  2. package/package.json +3 -3
  3. package/src/constants.ts +5 -0
  4. package/src/index.ts +2 -1
  5. package/src/interfaces/settings.ts +6 -0
  6. package/src/managers/command/models.ts +9 -2
  7. package/src/managers/command.ts +6 -1
  8. package/src/managers/server.ts +30 -38
  9. package/src/managers/settings.ts +14 -2
  10. package/src/server.ts +6 -9
  11. package/src/sse/manager.ts +2 -2
  12. package/src/ui/editors/editorOptions.ts +8 -1
  13. package/src/ui/editors/itemBuilder.ts +2 -6
  14. package/src/ui/editors/listEditor.ts +24 -15
  15. package/src/ui/editors/override/entryEditor.ts +97 -15
  16. package/src/ui/editors/override/fields/base.ts +53 -0
  17. package/src/ui/editors/override/fields/capabilities.ts +36 -0
  18. package/src/ui/editors/override/fields/cost.ts +64 -0
  19. package/src/ui/editors/override/fields/index.ts +68 -0
  20. package/src/ui/editors/override/fields/numeric.ts +55 -0
  21. package/src/ui/editors/override/fields/pattern.ts +24 -0
  22. package/src/ui/editors/override/fields/reasoning.ts +33 -0
  23. package/src/ui/editors/override/itemBuilder.ts +3 -6
  24. package/src/ui/editors/server/itemBuilder.ts +1 -5
  25. package/src/ui/editors/server/serverEditor.ts +51 -15
  26. package/src/ui/editors/server/utils.ts +50 -0
  27. package/src/ui/settings/index.ts +11 -0
  28. package/src/ui/strings.ts +17 -0
  29. package/src/utils/health.ts +2 -1
  30. package/src/utils/urlResolver.ts +1 -1
  31. package/tests/{commandManager.test.ts → command/commandManager.test.ts} +80 -9
  32. package/tests/{events.test.ts → events/events.test.ts} +6 -6
  33. package/tests/mocks.ts +2 -0
  34. package/tests/{legacyModel.test.ts → models/legacyModel.test.ts} +5 -5
  35. package/tests/{routerModel.test.ts → models/routerModel.test.ts} +5 -5
  36. package/tests/{singleModel.test.ts → models/singleModel.test.ts} +5 -5
  37. package/tests/{health.test.ts → server/health.test.ts} +2 -2
  38. package/tests/{server.test.ts → server/server.test.ts} +5 -5
  39. package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
  40. package/tests/{settings.test.ts → settings/settings.test.ts} +174 -133
  41. package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
  42. package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +2 -2
  43. package/tests/{dialog.test.ts → ui/dialog.test.ts} +5 -4
  44. package/tests/{overrides.test.ts → ui/overrides.test.ts} +9 -42
  45. package/src/ui/editors/override/fields.ts +0 -294
  46. package/src/ui/editors/override/handlers.ts +0 -118
  47. package/src/ui/editors/server/handlers.ts +0 -32
package/README.md CHANGED
@@ -35,11 +35,12 @@ A [Pi Coding Agent](https://pi.dev/) extension that integrates with running [lla
35
35
 
36
36
  When browsing servers via `/models servers`, each server URL is prefixed with a health indicator:
37
37
 
38
- | Icon | Status | Description |
39
- | ---- | ----------- | --------------------------------------------- |
40
- | 🟢 | Healthy | Server responded successfully to health check |
41
- | 🟡 | Timeout | Server health check timed out |
42
- | 🔴 | Unreachable | Server could not be reached |
38
+ | Icon | Status | Description |
39
+ | ---- | ------------ | --------------------------------------------- |
40
+ | 🟢 | Healthy | Server responded successfully to health check |
41
+ | 🟡 | Timeout | Server health check timed out |
42
+ | 🔴 | Unreachable | Server could not be reached |
43
+ | ⛔ | Unauthorized | Server requires an API key |
43
44
 
44
45
  ## Installation
45
46
 
@@ -127,6 +128,7 @@ With this config, the servers will appear in Pi as **Llama.cpp (Local Server)**
127
128
  | `sortBy` | string | `"asc"` | Sort order for models (see below) |
128
129
  | `pollingTimeout` | number | `60000` | Max time (ms) to wait for model loading before giving up |
129
130
  | `serverTimeout` | number | `1000` | Timeout (ms) for server health checks and SSE probes |
131
+ | `showServerUrls` | boolean | `true` | Show `[Server: <url>]` suffix in the /models model list |
130
132
 
131
133
  > **Note:** `serverTimeout` controls individual HTTP request timeouts (health checks, SSE probe). `pollingTimeout` controls the total wait time for a model to finish loading. Increase `serverTimeout` for slow/high-latency servers, and `pollingTimeout` for large models or slow hardware.
132
134
 
@@ -137,11 +139,11 @@ Run `/models settings` to edit the scalar settings above without hand-editing JS
137
139
  #### Server list editor
138
140
 
139
141
  Run `/models servers` to add, edit or remove entries of `llamaSettings.servers`
140
- without hand-editing JSON. Each server URL is prefixed with a health indicator
141
- (🟢 healthy, 🟡 timeout, 🔴 unreachable) that reflects the result of a
142
- health check against the server. Each change is written immediately to the **project**
143
- `.pi/settings.json` if it exists, otherwise to **global**
144
- `~/.pi/agent/settings.json`.
142
+ without hand-editing JSON. Each server URL is prefixed with a status indicator
143
+ (🟢 healthy, 🟡 timeout, 🔴 unreachable, ⛔ unauthorized) that reflects the
144
+ result of a health check and an auth probe against the server. Each change is
145
+ written immediately to the **project** `.pi/settings.json` if it exists,
146
+ otherwise to **global** `~/.pi/agent/settings.json`.
145
147
 
146
148
  Changes take effect immediately after closing the editor: new servers
147
149
  register their providers, removed ones leave pi's registry right away,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-llama-cpp",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Pi extension for llama.cpp integration. Supports router, single and legacy models. Supports multiple servers.",
5
5
  "keywords": [
6
6
  "pi",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  "type": "module",
40
40
  "devDependencies": {
41
- "@types/node": "^26.6.2",
41
+ "@types/node": "^26.6.3",
42
42
  "prettier-plugin-organize-imports": "^4.3.0",
43
- "vitest": "^5.0.1"
43
+ "vitest": "^5.0.2"
44
44
  }
45
45
  }
package/src/constants.ts CHANGED
@@ -76,6 +76,11 @@ export const AUTOLOAD_ON_MESSAGE = false;
76
76
  */
77
77
  export const SORT_BY = "asc";
78
78
 
79
+ /**
80
+ * Default for showServerUrls — show server URLs in /models by default.
81
+ */
82
+ export const SHOW_SERVER_URLS = true;
83
+
79
84
  /**
80
85
  * Thinking budgets to send to the server, depending on user-selected level in Pi.
81
86
  */
package/src/index.ts CHANGED
@@ -11,9 +11,10 @@ import { ModelSelectEvent } from "./interfaces/events";
11
11
  import { CommandManager } from "./managers/command";
12
12
  import { EventManager } from "./managers/events";
13
13
  import { ServerManager } from "./managers/server";
14
- import { settings } from "./managers/settings";
14
+ import { createSettingsManager } from "./managers/settings";
15
15
 
16
16
  export default async function (pi: ExtensionAPI) {
17
+ const settings = createSettingsManager();
17
18
  const serverManager = new ServerManager(settings);
18
19
  const eventManager = new EventManager(serverManager, settings);
19
20
  const commandManager = new CommandManager(serverManager, settings);
@@ -132,4 +132,10 @@ export interface LlamaSettings {
132
132
  * @default "asc"
133
133
  */
134
134
  sortBy?: SortBy;
135
+ /**
136
+ * Whether to show the `[Server: <url>]` suffix in the /models model list.
137
+ * When `false`, only model names are shown without server annotations.
138
+ * @default true
139
+ */
140
+ showServerUrls?: boolean;
135
141
  }
@@ -10,6 +10,7 @@ import { BaseModel } from "../../models/baseModel";
10
10
  import { errorMessage } from "../../utils/errors";
11
11
  import { EventManager } from "../events";
12
12
  import type { ServerManager } from "../server";
13
+ import type { LlamaSettingsManager } from "../settings";
13
14
 
14
15
  /**
15
16
  * Interactive model selection/action flow for the `/models` command.
@@ -24,7 +25,10 @@ import type { ServerManager } from "../server";
24
25
  * server registry without coupling the menu to command routing.
25
26
  */
26
27
  export class ModelsMenu {
27
- constructor(private readonly serverManager: ServerManager) {}
28
+ constructor(
29
+ private readonly serverManager: ServerManager,
30
+ private readonly settings: LlamaSettingsManager,
31
+ ) {}
28
32
 
29
33
  /**
30
34
  * Runs the interactive model selection menu.
@@ -173,6 +177,8 @@ export class ModelsMenu {
173
177
  ctx: ExtensionCommandContext,
174
178
  models: BaseModel[],
175
179
  ): Promise<BaseModel | null> {
180
+ const showServerUrls = await this.settings.resolveShowServerUrls();
181
+
176
182
  const labels = await Promise.all(
177
183
  models.map(async (model) => ({
178
184
  label: (await model.getLabel()).trim(),
@@ -191,7 +197,8 @@ export class ModelsMenu {
191
197
  const choices = labels.map(({ label, serverUrl }) => {
192
198
  const extraPadding = 2;
193
199
  const padLen = maxLength - graphemeLength(label) + extraPadding;
194
- return `${label}${" ".repeat(padLen)} [Server: ${serverUrl}]`;
200
+ const serverSuffix = showServerUrls ? ` [Server: ${serverUrl}]` : "";
201
+ return `${label}${" ".repeat(padLen)}${serverSuffix}`;
195
202
  });
196
203
 
197
204
  const choice = await ctx.ui.select(`${PROVIDER_NAME} models:`, choices);
@@ -51,7 +51,7 @@ export class CommandManager {
51
51
  private readonly serverManager: ServerManager,
52
52
  private readonly settings: LlamaSettingsManager,
53
53
  ) {
54
- this.modelsMenu = new ModelsMenu(serverManager);
54
+ this.modelsMenu = new ModelsMenu(serverManager, settings);
55
55
  }
56
56
 
57
57
  /**
@@ -108,6 +108,11 @@ export class CommandManager {
108
108
  this.notifyNotFound(ctx, url);
109
109
  }
110
110
 
111
+ // Notify about other warnings (e.g. unauthorized servers)
112
+ for (const warning of this.serverManager.getWarnings()) {
113
+ ctx.ui.notify(warning, "warning");
114
+ }
115
+
111
116
  if (args === "unload") {
112
117
  const models = await this.serverManager.getAllModels();
113
118
  await Promise.all(models.map((model) => model.unload()));
@@ -1,10 +1,14 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import type {
2
+ ExtensionAPI,
3
+ ProviderModelConfig,
4
+ } from "@earendil-works/pi-coding-agent";
2
5
  import { ApiError } from "../api/client";
3
6
  import { API_TYPE, PROVIDER_NAME } from "../constants";
4
7
  import { ServerStatus } from "../enums/serverStatus";
5
8
  import type { SortBy } from "../interfaces/sortBy";
6
9
  import { BaseModel } from "../models/baseModel";
7
10
  import { Server } from "../server";
11
+ import { authRequiredMessage } from "../ui/strings";
8
12
  import type { LlamaSettingsManager } from "./settings";
9
13
 
10
14
  /** Model-list comparator: negative if a sorts first, positive if b does. */
@@ -45,6 +49,7 @@ export class ServerManager {
45
49
  */
46
50
  async update(pi: ExtensionAPI, timeout?: number) {
47
51
  this.failedUrls.length = 0;
52
+ this.warnings.length = 0;
48
53
 
49
54
  // Surface warnings from strict URL parsing (dropped invalid entries)
50
55
  this.warnings.push(...this.settings.takeWarnings());
@@ -60,9 +65,9 @@ export class ServerManager {
60
65
  }
61
66
 
62
67
  // Unregister providers that disappeared (removed or edited away);
63
- // no-op for providers that were never registered
68
+ // `seen` tracks all kept providerIds, so we skip those still present.
64
69
  for (const old of this.servers) {
65
- if (fresh.some((f) => f.providerId === old.providerId)) continue;
70
+ if (seen.has(old.providerId)) continue;
66
71
  pi.unregisterProvider(old.providerId);
67
72
  // Optional chain is intentional despite the non-optional type: `sse`
68
73
  // is undefined until initialize() runs (async-constructor hack — see Server)
@@ -79,34 +84,7 @@ export class ServerManager {
79
84
 
80
85
  // Initialization and registration
81
86
  for (const server of registrableServers) {
82
- try {
83
- await server.initialize();
84
- await this.registerProvider(server, pi);
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
- }
107
- this.failedUrls.push(server.baseUrl);
108
- continue;
109
- }
87
+ await this.registerProvider(server, pi);
110
88
  }
111
89
  }
112
90
 
@@ -151,23 +129,37 @@ export class ServerManager {
151
129
  }
152
130
 
153
131
  /**
154
- * Creates a Pi provider for the given server.
132
+ * Initializes the server and creates a Pi provider.
133
+ * Handles auth errors by registering with an empty model list so the user
134
+ * can still configure the API key via `/login` or `auth.json`.
155
135
  *
156
136
  * @param server The server
157
137
  * @param pi The Pi API
158
138
  */
159
139
  private async registerProvider(server: Server, pi: ExtensionAPI) {
160
- const { apiBaseUrl, models, providerId, providerName } = server;
161
- const apiKey = server.getApiKey();
162
- const modelConfigs = await Promise.all(
163
- models.map((m) => m.toProviderConfig()),
164
- );
140
+ const { apiBaseUrl, apiKey, baseUrl, models, providerId, providerName } =
141
+ server;
142
+ let modelConfigs: ProviderModelConfig[] = [];
143
+
144
+ try {
145
+ await server.initialize();
146
+ modelConfigs = await Promise.all(models.map((m) => m.toProviderConfig()));
147
+ } catch (err) {
148
+ if (err instanceof ApiError && err.type === "authentication") {
149
+ // Don't add to `failedUrls` — the server IS reachable, auth just
150
+ // isn't configured yet, so the health indicator should stay green.
151
+ this.warnings.push(authRequiredMessage(baseUrl, providerId));
152
+ } else {
153
+ this.failedUrls.push(baseUrl);
154
+ return;
155
+ }
156
+ }
165
157
 
166
158
  pi.registerProvider(providerId, {
167
159
  name: providerName,
168
160
  baseUrl: apiBaseUrl,
169
161
  api: API_TYPE,
170
- apiKey: apiKey,
162
+ apiKey,
171
163
  models: modelConfigs,
172
164
  });
173
165
  }
@@ -13,6 +13,7 @@ import {
13
13
  REACT_TO_MODEL_SELECT,
14
14
  SERVER_TIMEOUT,
15
15
  SETTINGS_KEY,
16
+ SHOW_SERVER_URLS,
16
17
  SORT_BY,
17
18
  THINKING_BUDGETS,
18
19
  } from "../constants";
@@ -226,6 +227,15 @@ export class LlamaSettingsManager {
226
227
  return (await this.getLlamaSettings()).sortBy ?? SORT_BY;
227
228
  }
228
229
 
230
+ /**
231
+ * Resolves whether to show server URLs in the /models model list.
232
+ *
233
+ * @returns `true` if server URLs should be shown
234
+ */
235
+ async resolveShowServerUrls(): Promise<boolean> {
236
+ return (await this.getLlamaSettings()).showServerUrls ?? SHOW_SERVER_URLS;
237
+ }
238
+
229
239
  /**
230
240
  * Persists one llamaSettings field to settings and reloads the in-memory
231
241
  * settings so resolvers see the change immediately.
@@ -262,6 +272,8 @@ export class LlamaSettingsManager {
262
272
  }
263
273
 
264
274
  /**
265
- * Shared singleton instance used across the extension.
275
+ * Creates a new LlamaSettingsManager instance.
266
276
  */
267
- export const settings = new LlamaSettingsManager();
277
+ export function createSettingsManager(): LlamaSettingsManager {
278
+ return new LlamaSettingsManager();
279
+ }
package/src/server.ts CHANGED
@@ -52,8 +52,8 @@ export class Server {
52
52
  // in ServerManager), so no lazy fallback is needed. initialize()
53
53
  // rebuilds the client to re-resolve the API key.
54
54
  this.apiClient =
55
- deps.createApiClient?.(this.getApiKey()) ??
56
- new ApiClient(options.baseUrl, this.getApiKey());
55
+ deps.createApiClient?.(this.apiKey) ??
56
+ new ApiClient(options.baseUrl, this.apiKey);
57
57
  }
58
58
 
59
59
  /** Base URL of this server endpoint. */
@@ -119,10 +119,8 @@ export class Server {
119
119
  /**
120
120
  * Retrieves the API key from the config resolver.
121
121
  * Tries custom ID first, then falls back to URL-based ID.
122
- *
123
- * @returns The API key
124
122
  */
125
- getApiKey(): string {
123
+ get apiKey(): string {
126
124
  // Try custom ID first
127
125
  if (this.options.customId) {
128
126
  const key = this.settings.resolveApiKey(this.options.customId);
@@ -137,10 +135,9 @@ export class Server {
137
135
  * Clears the cache first so we always fetch fresh data.
138
136
  */
139
137
  async initialize() {
140
- const apiKey = this.getApiKey();
141
138
  this.apiClient =
142
- this.deps.createApiClient?.(apiKey) ??
143
- new ApiClient(this.baseUrl, apiKey);
139
+ this.deps.createApiClient?.(this.apiKey) ??
140
+ new ApiClient(this.baseUrl, this.apiKey);
144
141
  this.sse = this.deps.createSSEManager?.(this) ?? new SSEManager(this);
145
142
  const { data } = await this.fetchModels();
146
143
  const mode = await this.detectServerMode(data);
@@ -185,7 +182,7 @@ export class Server {
185
182
  * @returns The server status
186
183
  */
187
184
  async isReady(timeout: number): Promise<ServerStatus> {
188
- return checkServerHealth(this.baseUrl, timeout, this.getApiKey());
185
+ return checkServerHealth(this.baseUrl, timeout, this.apiKey);
189
186
  }
190
187
 
191
188
  /**
@@ -46,7 +46,7 @@ export class SSEManager {
46
46
  try {
47
47
  const response = await fetch(this.sseEndpoint, {
48
48
  method: "GET",
49
- headers: buildAuthHeaders(this.server.getApiKey()),
49
+ headers: buildAuthHeaders(this.server.apiKey),
50
50
  signal: AbortSignal.timeout(await this.server.getServerTimeout()),
51
51
  });
52
52
  this.sseSupported =
@@ -78,7 +78,7 @@ export class SSEManager {
78
78
  this.modelCallbacks.set(modelId, callbacks);
79
79
 
80
80
  // Create SSE client if not already created
81
- this.sseClient ??= new SSEClient(this.sseEndpoint, this.server.getApiKey());
81
+ this.sseClient ??= new SSEClient(this.sseEndpoint, this.server.apiKey);
82
82
 
83
83
  // Subscribe a single dispatching callback to the SSE client
84
84
  if (!this.sseSubscribers.has(modelId)) {
@@ -1,4 +1,7 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
1
+ import type {
2
+ ExtensionUIContext,
3
+ Theme,
4
+ } from "@earendil-works/pi-coding-agent";
2
5
  import type { KeybindingsManager, TUI } from "@earendil-works/pi-tui";
3
6
  import type { LlamaServer } from "../../interfaces/settings";
4
7
 
@@ -32,6 +35,10 @@ export interface SettingsListEditorOptions {
32
35
  export interface ServerSettingsListOptions extends SettingsListEditorOptions {
33
36
  /** Timeout (ms) for health checks. Defaults to 1000ms if not provided. */
34
37
  serverTimeout?: number;
38
+ /** Resolves the API key for a server (used for auth probing). */
39
+ authResolver?: (server: LlamaServer) => Promise<string>;
40
+ /** UI context for notifications (used to warn about auth-required servers). */
41
+ ui?: ExtensionUIContext;
35
42
  }
36
43
 
37
44
  /**
@@ -30,7 +30,7 @@ export abstract class ItemBuilder<TEntry, TField extends SettingField<TEntry>> {
30
30
  description: def.description,
31
31
  currentValue: def.currentValue(entry),
32
32
  };
33
- return this.decorate(def, entry, base);
33
+ return this.decorate(def, base);
34
34
  }
35
35
 
36
36
  /** Builds the `SettingItem` array for one entry's editable fields. */
@@ -39,9 +39,5 @@ export abstract class ItemBuilder<TEntry, TField extends SettingField<TEntry>> {
39
39
  }
40
40
 
41
41
  /** Adds field-type-specific columns (e.g. submenu or fixed values). */
42
- protected abstract decorate(
43
- def: TField,
44
- entry: TEntry,
45
- base: SettingItem,
46
- ): SettingItem;
42
+ protected abstract decorate(def: TField, base: SettingItem): SettingItem;
47
43
  }
@@ -66,9 +66,6 @@ export abstract class ListEditor<
66
66
  | "emptyServers"
67
67
  | "emptyOverrideEntries";
68
68
 
69
- /** Total item count (entries or servers) for selection clamping. */
70
- protected abstract getCurrentCount(): number;
71
-
72
69
  /** Returns the id for a row at the given index. */
73
70
  protected abstract getRowId(index: number): string;
74
71
 
@@ -78,6 +75,12 @@ export abstract class ListEditor<
78
75
  /** Title for the delete confirmation dialog (e.g. "Delete server"). */
79
76
  protected abstract get deleteTitle(): string;
80
77
 
78
+ /** Returns the number of items in the current list. Override when the
79
+ * list items don't directly correspond to `options.servers`. */
80
+ protected getCount(): number {
81
+ return this.options.servers.length;
82
+ }
83
+
81
84
  // -- hooks -----------------------------------------------------------------
82
85
 
83
86
  /**
@@ -111,10 +114,7 @@ export abstract class ListEditor<
111
114
 
112
115
  handleInput(data: string): void {
113
116
  // Delegate to the open modal dialog, if any
114
- if (this.activeDialog) {
115
- this.activeDialog.handleInput(data);
116
- return;
117
- }
117
+ if (this.delegateToDialogInput(data)) return;
118
118
 
119
119
  // Delegate to settingsList if a submenu is open
120
120
  if (this.submenuOpen && this.settingsList) {
@@ -134,15 +134,11 @@ export abstract class ListEditor<
134
134
  }
135
135
  if (kb.matches(data, "tui.select.up")) {
136
136
  this.selectedIndex =
137
- this.selectedIndex === 0
138
- ? this.getCurrentCount() - 1
139
- : this.selectedIndex - 1;
137
+ this.selectedIndex === 0 ? this.getCount() - 1 : this.selectedIndex - 1;
140
138
  }
141
139
  if (kb.matches(data, "tui.select.down")) {
142
140
  this.selectedIndex =
143
- this.selectedIndex === this.getCurrentCount() - 1
144
- ? 0
145
- : this.selectedIndex + 1;
141
+ this.selectedIndex === this.getCount() - 1 ? 0 : this.selectedIndex + 1;
146
142
  }
147
143
  if (data === "a") {
148
144
  this.beginAdd();
@@ -164,7 +160,7 @@ export abstract class ListEditor<
164
160
  // Empty list hint
165
161
  if (!this.settingsList) return ["Loading..."];
166
162
  const lines = this.settingsList.render(width);
167
- if (this.getCurrentCount() === 0) {
163
+ if (this.getCount() === 0) {
168
164
  lines[lines.length - 1] = getSettingsListTheme().hint(
169
165
  HINTS[this.emptyHintKey],
170
166
  );
@@ -174,6 +170,18 @@ export abstract class ListEditor<
174
170
 
175
171
  // -- helpers ---------------------------------------------------------------
176
172
 
173
+ /**
174
+ * If a dialog is open, delegates input to it and returns true;
175
+ * otherwise returns false. Used to short-circuit handleInput.
176
+ */
177
+ private delegateToDialogInput(data: string): boolean {
178
+ if (this.activeDialog) {
179
+ this.activeDialog.handleInput(data);
180
+ return true;
181
+ }
182
+ return false;
183
+ }
184
+
177
185
  /**
178
186
  * Rebuilds the list and places the cursor on `targetIndex`.
179
187
  * Subclasses call this after add/delete/persist to refresh the UI.
@@ -181,9 +189,10 @@ export abstract class ListEditor<
181
189
  protected async rebuildList(targetIndex: number): Promise<void> {
182
190
  const list = await this.buildSettingsList();
183
191
  this.settingsList = list;
184
- const count = this.getCurrentCount();
192
+ const count = this.getCount();
185
193
  if (count === 0) {
186
194
  this.selectedIndex = 0;
195
+ this.options.tui.requestRender();
187
196
  return;
188
197
  }
189
198
  this.selectedIndex = Math.min(targetIndex, count - 1);