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.
- package/README.md +12 -10
- package/package.json +3 -3
- package/src/constants.ts +5 -0
- package/src/index.ts +2 -1
- package/src/interfaces/settings.ts +6 -0
- package/src/managers/command/models.ts +9 -2
- package/src/managers/command.ts +6 -1
- package/src/managers/server.ts +30 -38
- package/src/managers/settings.ts +14 -2
- package/src/server.ts +6 -9
- package/src/sse/manager.ts +2 -2
- package/src/ui/editors/editorOptions.ts +8 -1
- package/src/ui/editors/itemBuilder.ts +2 -6
- package/src/ui/editors/listEditor.ts +24 -15
- package/src/ui/editors/override/entryEditor.ts +97 -15
- package/src/ui/editors/override/fields/base.ts +53 -0
- package/src/ui/editors/override/fields/capabilities.ts +36 -0
- package/src/ui/editors/override/fields/cost.ts +64 -0
- package/src/ui/editors/override/fields/index.ts +68 -0
- package/src/ui/editors/override/fields/numeric.ts +55 -0
- package/src/ui/editors/override/fields/pattern.ts +24 -0
- package/src/ui/editors/override/fields/reasoning.ts +33 -0
- package/src/ui/editors/override/itemBuilder.ts +3 -6
- package/src/ui/editors/server/itemBuilder.ts +1 -5
- package/src/ui/editors/server/serverEditor.ts +51 -15
- package/src/ui/editors/server/utils.ts +50 -0
- package/src/ui/settings/index.ts +11 -0
- package/src/ui/strings.ts +17 -0
- package/src/utils/health.ts +2 -1
- package/src/utils/urlResolver.ts +1 -1
- package/tests/{commandManager.test.ts → command/commandManager.test.ts} +80 -9
- package/tests/{events.test.ts → events/events.test.ts} +6 -6
- package/tests/mocks.ts +2 -0
- package/tests/{legacyModel.test.ts → models/legacyModel.test.ts} +5 -5
- package/tests/{routerModel.test.ts → models/routerModel.test.ts} +5 -5
- package/tests/{singleModel.test.ts → models/singleModel.test.ts} +5 -5
- package/tests/{health.test.ts → server/health.test.ts} +2 -2
- package/tests/{server.test.ts → server/server.test.ts} +5 -5
- package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
- package/tests/{settings.test.ts → settings/settings.test.ts} +174 -133
- package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
- package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +2 -2
- package/tests/{dialog.test.ts → ui/dialog.test.ts} +5 -4
- package/tests/{overrides.test.ts → ui/overrides.test.ts} +9 -42
- package/src/ui/editors/override/fields.ts +0 -294
- package/src/ui/editors/override/handlers.ts +0 -118
- 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
|
|
39
|
-
| ---- |
|
|
40
|
-
| 🟢 | Healthy
|
|
41
|
-
| 🟡 | Timeout
|
|
42
|
-
| 🔴 | Unreachable
|
|
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
|
|
141
|
-
(🟢 healthy, 🟡 timeout, 🔴 unreachable) that reflects the
|
|
142
|
-
health check against the server. Each change is
|
|
143
|
-
`.pi/settings.json` if it exists,
|
|
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.
|
|
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.
|
|
41
|
+
"@types/node": "^26.6.3",
|
|
42
42
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
43
|
-
"vitest": "^5.0.
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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);
|
package/src/managers/command.ts
CHANGED
|
@@ -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()));
|
package/src/managers/server.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
//
|
|
68
|
+
// `seen` tracks all kept providerIds, so we skip those still present.
|
|
64
69
|
for (const old of this.servers) {
|
|
65
|
-
if (
|
|
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
|
-
|
|
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
|
-
*
|
|
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 } =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
162
|
+
apiKey,
|
|
171
163
|
models: modelConfigs,
|
|
172
164
|
});
|
|
173
165
|
}
|
package/src/managers/settings.ts
CHANGED
|
@@ -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
|
-
*
|
|
275
|
+
* Creates a new LlamaSettingsManager instance.
|
|
266
276
|
*/
|
|
267
|
-
export
|
|
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.
|
|
56
|
-
new ApiClient(options.baseUrl, this.
|
|
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
|
-
|
|
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.
|
|
185
|
+
return checkServerHealth(this.baseUrl, timeout, this.apiKey);
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
/**
|
package/src/sse/manager.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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 {
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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);
|