pi-llama-cpp 0.11.0 → 0.12.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.
@@ -14,7 +14,8 @@ import { Mode } from "../enums/mode";
14
14
  import { Status } from "../enums/status";
15
15
  import { LlamaSettings } from "../interfaces/settings";
16
16
  import { BaseModel } from "../models/baseModel";
17
- import { ServerListEditor } from "../ui/serverListEditor";
17
+ import { createOverrideSettingsList } from "../ui/overrideSettingsList";
18
+ import { ServerSettingsList } from "../ui/serverSettingsList";
18
19
  import { errorMessage } from "../utils/errors";
19
20
  import { EventManager } from "./events";
20
21
  import { ServerManager } from "./server";
@@ -64,15 +65,20 @@ const ARGUMENT_COMPLETIONS: AutocompleteItem[] = [
64
65
  label: "unload",
65
66
  description: "Unload all models",
66
67
  },
68
+ {
69
+ value: "settings",
70
+ label: "settings",
71
+ description: "Configure llamaSettings",
72
+ },
67
73
  {
68
74
  value: "servers",
69
75
  label: "servers",
70
76
  description: "Manage llama.cpp server URLs",
71
77
  },
72
78
  {
73
- value: "settings",
74
- label: "settings",
75
- description: "Configure llamaSettings",
79
+ value: "overrides",
80
+ label: "overrides",
81
+ description: "Manage llama.cpp model overrides",
76
82
  },
77
83
  ];
78
84
 
@@ -96,17 +102,17 @@ const parseMs = (value: string): number =>
96
102
  * Builds the `SettingsList` items for `/models settings` from the current
97
103
  * (merged) values of the scalar `llamaSettings` fields.
98
104
  */
99
- export const buildSettingsItems = (
105
+ export const buildSettingsItems = async (
100
106
  settings: LlamaSettingsManager,
101
- ): SettingItem[] => {
102
- const { pollingTimeout, serverTimeout } = settings.resolveTimeouts();
107
+ ): Promise<SettingItem[]> => {
108
+ const { pollingTimeout, serverTimeout } = await settings.resolveTimeouts();
103
109
 
104
110
  return [
105
111
  {
106
112
  id: Options.REACT_TO_MODEL_SELECT,
107
113
  label: "React to model selection",
108
114
  description: "Load the model when you pick it in Pi (immediate)",
109
- currentValue: settings.resolveReactToModelSelect() ? "on" : "off",
115
+ currentValue: (await settings.resolveReactToModelSelect()) ? "on" : "off",
110
116
  values: ["on", "off"],
111
117
  },
112
118
  {
@@ -114,14 +120,14 @@ export const buildSettingsItems = (
114
120
  label: "Autoload on message",
115
121
  description:
116
122
  "Auto-load the selected model when you send a message (immediate)",
117
- currentValue: settings.resolveAutoloadOnMessage() ? "on" : "off",
123
+ currentValue: (await settings.resolveAutoloadOnMessage()) ? "on" : "off",
118
124
  values: ["on", "off"],
119
125
  },
120
126
  {
121
127
  id: Options.SORT_BY,
122
128
  label: "Sort models by",
123
129
  description: "Order of models in /models (next open)",
124
- currentValue: settings.resolveSortBy(),
130
+ currentValue: await settings.resolveSortBy(),
125
131
  values: [...SORT_VALUES],
126
132
  },
127
133
  {
@@ -208,9 +214,17 @@ export class CommandManager {
208
214
  return;
209
215
  }
210
216
 
211
- // Servers editor: same — edits are passive until the next provider scan
217
+ // Servers editor: re-registers providers after editing so changes
218
+ // (add / remove / URL / id / name) apply immediately
212
219
  if (args === "servers") {
213
- await this.runServersEditor(ctx);
220
+ await this.runServersEditor(ctx, pi);
221
+ return;
222
+ }
223
+
224
+ // Overrides editor: re-registers providers after editing so new
225
+ // overrides take effect on the next request
226
+ if (args === "overrides") {
227
+ await this.runOverridesEditor(ctx, pi);
214
228
  return;
215
229
  }
216
230
 
@@ -223,17 +237,15 @@ export class CommandManager {
223
237
  }
224
238
 
225
239
  if (args === "unload") {
226
- await Promise.all(
227
- this.serverManager.getAllModels().map((model) => model.unload()),
228
- );
240
+ const models = await this.serverManager.getAllModels();
241
+ await Promise.all(models.map((model) => model.unload()));
229
242
  ctx.ui.notify(`Unloaded all ${PROVIDER_NAME} models`, "info");
230
243
  return;
231
244
  }
232
245
 
233
246
  if (args === "info") {
234
- const infos = await Promise.all(
235
- this.serverManager.getAllModels().map((model) => model.getInfo()),
236
- );
247
+ const models = await this.serverManager.getAllModels();
248
+ const infos = await Promise.all(models.map((model) => model.getInfo()));
237
249
  ctx.ui.notify(ctx.ui.theme.fg("accent", infos.join("\n")), "info");
238
250
  return;
239
251
  }
@@ -248,7 +260,9 @@ export class CommandManager {
248
260
  *
249
261
  * Writes go to the global `~/.pi/agent/settings.json` via
250
262
  * `LlamaSettingsManager.setLlamaSetting()`; write errors are notified
251
- * and leave the dialog open with values unchanged.
263
+ * and leave the dialog open with values unchanged. These settings
264
+ * (reactToModelSelect, autoloadOnMessage, sortBy, timeouts) do not
265
+ * require provider re-registration.
252
266
  */
253
267
  private async runSettingsMenu(ctx: ExtensionCommandContext): Promise<void> {
254
268
  if (ctx.mode !== "tui") {
@@ -259,7 +273,7 @@ export class CommandManager {
259
273
  return;
260
274
  }
261
275
 
262
- const items = buildSettingsItems(this.settings);
276
+ const items = await buildSettingsItems(this.settings);
263
277
 
264
278
  await ctx.ui.custom<void>(
265
279
  (_tui, _theme, _kb, done) =>
@@ -282,16 +296,19 @@ export class CommandManager {
282
296
 
283
297
  /**
284
298
  * Runs the interactive servers editor for `llamaSettings.servers`.
285
- * Enter/e edits the selected URL, i its id, n its name, a adds,
286
- * d deletes (after a confirmation prompt); Esc closes.
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.
287
302
  *
288
303
  * Writes go to the global `~/.pi/agent/settings.json` via
289
304
  * `LlamaSettingsManager.setLlamaSetting()`; write errors are notified and
290
- * the editor stays open with the pre-mutation list. List changes (add,
291
- * remove, URL/`id`/`name` edits) apply the next time providers are
292
- * scanned — run `/models` to see them.
305
+ * the editor stays open with the pre-mutation list. After closing,
306
+ * providers are re-registered so server changes apply immediately.
293
307
  */
294
- private async runServersEditor(ctx: ExtensionCommandContext): Promise<void> {
308
+ private async runServersEditor(
309
+ ctx: ExtensionCommandContext,
310
+ pi: ExtensionAPI,
311
+ ): Promise<void> {
295
312
  if (ctx.mode !== "tui") {
296
313
  ctx.ui.notify(
297
314
  "/models servers requires an interactive session (TUI)",
@@ -300,22 +317,79 @@ export class CommandManager {
300
317
  return;
301
318
  }
302
319
 
303
- const servers = this.settings.llamaServers;
320
+ const servers = await this.settings.getLlamaServers();
304
321
 
305
322
  await ctx.ui.custom<void>(
306
323
  (tui, theme, keybindings, done) =>
307
- new ServerListEditor({
324
+ new ServerSettingsList({
308
325
  tui,
309
326
  theme,
310
327
  keybindings,
311
328
  servers,
312
329
  persist: (next) => this.settings.setLlamaSetting("servers", next),
313
- done: () => done(undefined),
330
+ done: () => {
331
+ done(undefined);
332
+ // Re-register providers so the updated server list takes effect
333
+ this.serverManager.update(pi);
334
+ },
314
335
  onError: (message) => ctx.ui.notify(message, "error"),
315
336
  }),
316
337
  );
317
338
  }
318
339
 
340
+ /**
341
+ * Runs the interactive overrides editor for
342
+ * `llamaSettings.servers[].overrides`: a SettingsList of servers drilling
343
+ * down into each server's override entries (one row per pattern, with
344
+ * add/delete support). Within a server's entry list: Enter drills into
345
+ * the field-edit submenu; a adds, d deletes (after confirmation).
346
+ *
347
+ * Fields use a mix of finite (Enter to cycle) and infinite (Enter to
348
+ * type) editing:
349
+ *
350
+ * - Pattern / costs (input, output, cacheRead, cacheWrite): infinite —
351
+ * Enter opens an Input for typing.
352
+ * - Capabilities: finite — Enter cycles between `text` and `text | image`.
353
+ * - Reasoning: finite — Enter cycles between `true` and `false`.
354
+ *
355
+ * Servers themselves are not managed here — use `/models servers`.
356
+ *
357
+ * Writes go to the global `~/.pi/agent/settings.json` via
358
+ * `LlamaSettingsManager.setLlamaSetting()`; write errors are notified and
359
+ * leave the values unchanged. After closing, providers are
360
+ * re-registered so new overrides take effect on the next request.
361
+ */
362
+ private async runOverridesEditor(
363
+ ctx: ExtensionCommandContext,
364
+ pi: ExtensionAPI,
365
+ ): Promise<void> {
366
+ if (ctx.mode !== "tui") {
367
+ ctx.ui.notify(
368
+ "/models overrides requires an interactive session (TUI)",
369
+ "warning",
370
+ );
371
+ return;
372
+ }
373
+
374
+ const servers = await this.settings.getLlamaServers();
375
+ await ctx.ui.custom<void>((tui, theme, keybindings, done) =>
376
+ createOverrideSettingsList({
377
+ tui,
378
+ theme,
379
+ keybindings,
380
+ servers,
381
+ persist: (next) => this.settings.setLlamaSetting("servers", next),
382
+ done: () => {
383
+ done(undefined);
384
+ // Re-register providers so the updated overrides take effect
385
+ this.serverManager.update(pi);
386
+ },
387
+ onError: (message) => ctx.ui.notify(message, "error"),
388
+ onChanged: () => {}, // no per-change notification needed
389
+ }),
390
+ );
391
+ }
392
+
319
393
  /**
320
394
  * Notifies the user that a server is unreachable.
321
395
  */
@@ -332,7 +406,7 @@ export class CommandManager {
332
406
  ): Promise<void> {
333
407
  const event = await this.modelSelectionHandler(
334
408
  ctx,
335
- this.serverManager.getAllModels(),
409
+ await this.serverManager.getAllModels(),
336
410
  );
337
411
 
338
412
  if (!event) return;
@@ -522,7 +596,10 @@ export class CommandManager {
522
596
  : [Action.SWITCH, ...base],
523
597
  [Status.LOADING]: [...base],
524
598
  [Status.FAILED]: [Action.RETRY, ...base],
525
- [Status.SLEEPING]: [Action.SWITCH, Action.UNLOAD, ...base],
599
+ [Status.SLEEPING]:
600
+ model.mode === Mode.ROUTER
601
+ ? [Action.SWITCH, Action.UNLOAD, ...base]
602
+ : [Action.SWITCH, ...base],
526
603
  [Status.UNLOADED]: [Action.LOAD_AND_SWITCH, Action.LOAD, ...base],
527
604
  [Status.UNAUTHORIZED]: [...base],
528
605
  };
@@ -47,7 +47,7 @@ export class EventManager {
47
47
  */
48
48
  async onModelSelect(event: ModelSelectEvent, ctx: ExtensionContext) {
49
49
  // Check if the model_select event should be used
50
- if (!this.settings.resolveReactToModelSelect()) return;
50
+ if (!(await this.settings.resolveReactToModelSelect())) return;
51
51
 
52
52
  for (const { providerId, models } of this.serverManager.servers) {
53
53
  if (event.model.provider !== providerId) continue;
@@ -72,7 +72,7 @@ export class EventManager {
72
72
  * @param model The model to potentially auto-load
73
73
  */
74
74
  private async autoLoadIfNeeded(model: BaseModel): Promise<void> {
75
- if (!this.settings.resolveAutoloadOnMessage()) return;
75
+ if (!(await this.settings.resolveAutoloadOnMessage())) return;
76
76
 
77
77
  const status = await model.getStatus();
78
78
  if (status !== Status.UNLOADED) return;
@@ -30,7 +30,7 @@ export class ServerManager {
30
30
  */
31
31
  async initialize(pi: ExtensionAPI) {
32
32
  // Register the providers with the configured server timeout
33
- const { serverTimeout } = this.settings.resolveTimeouts();
33
+ const { serverTimeout } = await this.settings.resolveTimeouts();
34
34
  await this.update(pi, serverTimeout);
35
35
  }
36
36
 
@@ -51,7 +51,7 @@ export class ServerManager {
51
51
  // (add / remove / URL / id / name) apply on the next scan
52
52
  const fresh: Server[] = [];
53
53
  const seen = new Set<string>(); // dedupe repeated URLs (same providerId)
54
- for (const server of this.settings.resolveServers()) {
54
+ for (const server of await this.settings.resolveServers()) {
55
55
  if (seen.has(server.providerId)) continue;
56
56
  seen.add(server.providerId);
57
57
  fresh.push(server);
@@ -171,16 +171,20 @@ export class ServerManager {
171
171
 
172
172
  /**
173
173
  * Returns all models from all servers, sorted by the configured sort mode.
174
+ * Servers maintain their order from `llamaSettings`; sorting only applies
175
+ * to models within each server.
174
176
  *
175
177
  * @returns Flat array of all models across all servers
176
178
  */
177
- getAllModels(): BaseModel[] {
178
- const sortBy = this.settings.resolveSortBy();
179
- const allModels = this.servers.flatMap((s) => s.models);
179
+ async getAllModels(): Promise<BaseModel[]> {
180
+ const sortBy = await this.settings.resolveSortBy();
180
181
 
181
- if (sortBy === "api") return allModels;
182
+ if (sortBy === "api") {
183
+ return this.servers.flatMap((s) => s.models);
184
+ }
182
185
 
183
- return allModels.sort(ServerManager.SORTERS[sortBy]);
186
+ const sorter = ServerManager.SORTERS[sortBy];
187
+ return this.servers.flatMap((s) => [...s.models].sort(sorter));
184
188
  }
185
189
 
186
190
  private static sortByIdAsc(a: BaseModel, b: BaseModel): number {
@@ -204,8 +208,7 @@ export class ServerManager {
204
208
  }
205
209
 
206
210
  /**
207
- * Comparators for each sort mode except "api", which preserves server
208
- * order (short-circuited in {@link ServerManager.getAllModels}).
211
+ * Comparators for sorting models within each server.
209
212
  */
210
213
  private static readonly SORTERS: Record<
211
214
  Exclude<SortBy, "api">,
@@ -1,8 +1,11 @@
1
1
  import { ApiKeyCredential, ModelThinkingLevel } from "@earendil-works/pi-ai";
2
2
  import {
3
+ getAgentDir,
3
4
  readStoredCredential,
4
5
  SettingsManager,
5
6
  } from "@earendil-works/pi-coding-agent";
7
+ import { access } from "node:fs/promises";
8
+ import { join } from "node:path";
6
9
  import {
7
10
  API_KEY_PLACEHOLDER,
8
11
  AUTOLOAD_ON_MESSAGE,
@@ -15,7 +18,11 @@ import {
15
18
  THINKING_BUDGETS,
16
19
  type SortBy,
17
20
  } from "../constants";
18
- import { LlamaServer, LlamaSettings } from "../interfaces/settings";
21
+ import {
22
+ LlamaServer,
23
+ LlamaSettings,
24
+ ModelOverride,
25
+ } from "../interfaces/settings";
19
26
  import { Server } from "../server";
20
27
  import { SettingsStore } from "../utils/settingsStore";
21
28
  import { isValidServerUrl, normalizeUrl } from "../utils/urls";
@@ -23,7 +30,22 @@ import { isValidServerUrl, normalizeUrl } from "../utils/urls";
23
30
  export class LlamaSettingsManager {
24
31
  private settingsManager = SettingsManager.create(process.cwd());
25
32
 
26
- constructor(private readonly store: SettingsStore = new SettingsStore()) {}
33
+ private globalStore = new SettingsStore(join(getAgentDir(), "settings.json"));
34
+ private projectStore = new SettingsStore(
35
+ join(process.cwd(), ".pi", "settings.json"),
36
+ );
37
+
38
+ /**
39
+ * Check if project settings file exists in the current working directory.
40
+ */
41
+ private async hasProjectSettings(): Promise<boolean> {
42
+ try {
43
+ await access(join(process.cwd(), ".pi", "settings.json"));
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
27
49
 
28
50
  /** Warnings collected during URL resolution (dropped invalid entries). */
29
51
  private warnings: string[] = [];
@@ -38,29 +60,31 @@ export class LlamaSettingsManager {
38
60
  }
39
61
 
40
62
  /**
41
- * Convenience getter for merged project/global settings
63
+ * Reloads settings from disk and returns merged project/global settings.
64
+ * Project settings override global settings.
42
65
  */
43
- private get mergedSettings(): Record<string, any> {
44
- const merged = {
66
+ private async getMergedSettings(): Promise<Record<string, any>> {
67
+ await this.settingsManager.reload();
68
+ return {
45
69
  ...this.settingsManager.getGlobalSettings(),
46
70
  ...this.settingsManager.getProjectSettings(),
47
71
  } as Record<string, any>;
48
- return merged;
49
72
  }
50
73
 
51
74
  /**
52
- * Convenience getter for the `llamaSettings` key
75
+ * Convenience method for the `llamaSettings` key.
76
+ * Reloads settings from disk before reading.
53
77
  */
54
- private get llamaSettings(): LlamaSettings {
55
- return this.mergedSettings[SETTINGS_KEY] ?? {};
78
+ async getLlamaSettings(): Promise<LlamaSettings> {
79
+ return (await this.getMergedSettings())[SETTINGS_KEY] ?? {};
56
80
  }
57
81
 
58
82
  /**
59
- * Convenience getter for the merged `servers` list (project overrides
60
- * global, per-key merge)
83
+ * Convenience method for the merged `servers` list (project overrides
84
+ * global, per-key merge). Reloads settings from disk before reading.
61
85
  */
62
- get llamaServers(): LlamaServer[] {
63
- return this.llamaSettings.servers ?? [];
86
+ async getLlamaServers(): Promise<LlamaServer[]> {
87
+ return (await this.getLlamaSettings()).servers ?? [];
64
88
  }
65
89
 
66
90
  /**
@@ -73,14 +97,14 @@ export class LlamaSettingsManager {
73
97
  *
74
98
  * @returns The list of URLs to use
75
99
  */
76
- resolveUrls(): string[] {
100
+ async resolveUrls(): Promise<string[]> {
77
101
  let response = this.resolveEnvUrls();
78
102
  if (response.length > 0) return response;
79
103
 
80
- response = this.resolveServerUrls();
104
+ response = await this.resolveServerUrls();
81
105
  if (response.length > 0) return response;
82
106
 
83
- response = this.resolveLegacyUrls();
107
+ response = await this.resolveLegacyUrls();
84
108
  if (response.length > 0) return response;
85
109
 
86
110
  return [LLAMA_SERVER_URL];
@@ -101,22 +125,24 @@ export class LlamaSettingsManager {
101
125
  /**
102
126
  * Resolves the llama-server URLs from `llamaSettings.servers`.
103
127
  * Settings are merged, prioritizing project over global settings.
128
+ * Reloads settings from disk before reading.
104
129
  *
105
130
  * @returns A list of detected URLs
106
131
  */
107
- private resolveServerUrls(): string[] {
108
- const { servers = [] } = this.llamaSettings;
132
+ private async resolveServerUrls(): Promise<string[]> {
133
+ const { servers = [] } = await this.getLlamaSettings();
109
134
  return servers.map((s) => this.parseUrls(s.url)).flat();
110
135
  }
111
136
 
112
137
  /**
113
- * Resolves the llama-server URLs from `llamaSettings.servers`.
138
+ * Resolves the llama-server URLs from `llamaServerUrl` legacy key.
114
139
  * Settings are merged, prioritizing project over global settings.
140
+ * Reloads settings from disk before reading.
115
141
  *
116
142
  * @returns A list of detected URLs
117
143
  */
118
- private resolveLegacyUrls(): string[] {
119
- const { llamaServerUrl = null } = this.mergedSettings;
144
+ private async resolveLegacyUrls(): Promise<string[]> {
145
+ const { llamaServerUrl = null } = await this.getMergedSettings();
120
146
  if (!llamaServerUrl) return [];
121
147
 
122
148
  return this.parseUrls(llamaServerUrl);
@@ -147,26 +173,53 @@ export class LlamaSettingsManager {
147
173
  });
148
174
  }
149
175
 
176
+ /**
177
+ * Resolves the override map for a given server URL.
178
+ *
179
+ * Reads the `overrides` field from the matching server config and returns
180
+ * a map of model ID → override. Returns an empty object when the server
181
+ * has no `overrides` defined.
182
+ *
183
+ * @param serverUrl - The URL of the server to resolve overrides for
184
+ * @returns A map of model ID to override configuration (partial fields,
185
+ * fallbacks applied at consumption time)
186
+ */
187
+ async resolveServerOverrides(
188
+ serverUrl: string,
189
+ ): Promise<Record<string, ModelOverride>> {
190
+ const serverConfig = (await this.getLlamaSettings()).servers?.find(
191
+ (s: { url: string }) => s.url === serverUrl,
192
+ );
193
+ return serverConfig?.overrides ?? {};
194
+ }
195
+
150
196
  /**
151
197
  * Resolves the servers that this extension will use.
152
198
  * Uses `resolveUrls()` as the source of truth for URLs (env > settings >
153
- * legacy > default), then applies `id`/`name` from `llamaSettings.servers`
154
- * as overrides when available.
199
+ * legacy > default), then applies `id`/`name`/`overrides` from
200
+ * `llamaSettings.servers` as overrides when available.
201
+ * Reloads settings from disk before reading.
155
202
  *
156
203
  * @returns A list of Server objects
157
204
  */
158
- resolveServers(): Server[] {
159
- const urls = this.resolveUrls();
160
- const serverConfigs = this.llamaSettings.servers ?? [];
205
+ async resolveServers(): Promise<Server[]> {
206
+ const urls = await this.resolveUrls();
207
+ const serverConfigs = (await this.getLlamaSettings()).servers ?? [];
161
208
 
162
- return urls.map((url) => {
209
+ const servers: Server[] = [];
210
+ for (const url of urls) {
163
211
  const config = serverConfigs.find((s) => s.url === url);
164
- return new Server(this, {
165
- baseUrl: url,
166
- customId: config?.id,
167
- customName: config?.name,
168
- });
169
- });
212
+ const overrides = await this.resolveServerOverrides(url);
213
+ servers.push(
214
+ new Server(this, {
215
+ baseUrl: url,
216
+ customId: config?.id,
217
+ customName: config?.name,
218
+ overrides,
219
+ }),
220
+ );
221
+ }
222
+ return servers;
170
223
  }
171
224
 
172
225
  /**
@@ -206,8 +259,11 @@ export class LlamaSettingsManager {
206
259
  *
207
260
  * @returns `true` if the extension should load the model on model_select
208
261
  */
209
- resolveReactToModelSelect(): boolean {
210
- return this.llamaSettings.reactToModelSelect ?? REACT_TO_MODEL_SELECT;
262
+ async resolveReactToModelSelect(): Promise<boolean> {
263
+ return (
264
+ (await this.getLlamaSettings()).reactToModelSelect ??
265
+ REACT_TO_MODEL_SELECT
266
+ );
211
267
  }
212
268
 
213
269
  /**
@@ -215,8 +271,10 @@ export class LlamaSettingsManager {
215
271
  *
216
272
  * @returns `true` if the extension should auto-load models
217
273
  */
218
- resolveAutoloadOnMessage(): boolean {
219
- return this.llamaSettings.autoloadOnMessage ?? AUTOLOAD_ON_MESSAGE;
274
+ async resolveAutoloadOnMessage(): Promise<boolean> {
275
+ return (
276
+ (await this.getLlamaSettings()).autoloadOnMessage ?? AUTOLOAD_ON_MESSAGE
277
+ );
220
278
  }
221
279
 
222
280
  /**
@@ -224,10 +282,14 @@ export class LlamaSettingsManager {
224
282
  *
225
283
  * @returns Object with polling and server timeout values
226
284
  */
227
- resolveTimeouts(): { pollingTimeout: number; serverTimeout: number } {
285
+ async resolveTimeouts(): Promise<{
286
+ pollingTimeout: number;
287
+ serverTimeout: number;
288
+ }> {
289
+ const llamaSettings = await this.getLlamaSettings();
228
290
  return {
229
- pollingTimeout: this.llamaSettings.pollingTimeout ?? POLLING_TIMEOUT,
230
- serverTimeout: this.llamaSettings.serverTimeout ?? SERVER_TIMEOUT,
291
+ pollingTimeout: llamaSettings.pollingTimeout ?? POLLING_TIMEOUT,
292
+ serverTimeout: llamaSettings.serverTimeout ?? SERVER_TIMEOUT,
231
293
  };
232
294
  }
233
295
 
@@ -236,13 +298,16 @@ export class LlamaSettingsManager {
236
298
  *
237
299
  * @returns The sort order: "asc", "desc", "asc-name", "desc-name", or "api"
238
300
  */
239
- resolveSortBy(): SortBy {
240
- return this.llamaSettings.sortBy ?? SORT_BY;
301
+ async resolveSortBy(): Promise<SortBy> {
302
+ return (await this.getLlamaSettings()).sortBy ?? SORT_BY;
241
303
  }
242
304
 
243
305
  /**
244
- * Persists one llamaSettings field to the global settings file and
245
- * reloads the in-memory settings so resolvers see the change immediately.
306
+ * Persists one llamaSettings field to settings and reloads the in-memory
307
+ * settings so resolvers see the change immediately.
308
+ *
309
+ * When `scope` is `"auto"` (default), writes to the project `.pi/settings.json`
310
+ * if it exists, otherwise to global `~/.pi/agent/settings.json`.
246
311
  *
247
312
  * Rejects if the file can't be read (e.g. invalid JSON) or written —
248
313
  * in-memory state stays consistent (reload only on success).
@@ -250,8 +315,18 @@ export class LlamaSettingsManager {
250
315
  async setLlamaSetting<K extends keyof LlamaSettings>(
251
316
  key: K,
252
317
  value: LlamaSettings[K],
318
+ scope: "auto" | "global" | "project" = "auto",
253
319
  ): Promise<void> {
254
- await this.store.updateKey(SETTINGS_KEY, (current) => {
320
+ const store =
321
+ scope === "auto"
322
+ ? (await this.hasProjectSettings())
323
+ ? this.projectStore
324
+ : this.globalStore
325
+ : scope === "project"
326
+ ? this.projectStore
327
+ : this.globalStore;
328
+
329
+ await store.updateKey(SETTINGS_KEY, (current) => {
255
330
  const merged =
256
331
  typeof current === "object" && current !== null
257
332
  ? (current as Record<string, unknown>)