pi-better-model-select 0.1.3 → 0.1.4
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 +27 -14
- package/extensions/bms.ts +73 -6
- package/extensions/lib/flows.ts +51 -57
- package/extensions/lib/picker.ts +9 -2
- package/extensions/lib/store.ts +59 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A searchable, usage-ranked model picker for [pi](https://pi.dev).
|
|
4
4
|
|
|
5
|
-
`/bms` opens the effective model list from `models.json` plus the optional
|
|
6
|
-
`models.
|
|
5
|
+
`/bms` opens the effective model list from `models.json` plus the optional BMS-owned
|
|
6
|
+
`models.bms.json` overlay, sorted by how often you pick each model. Start
|
|
7
7
|
typing to filter; press enter to switch the session to the highlighted model.
|
|
8
8
|
Add, update and delete live on ctrl-chords so plain keystrokes always go to the
|
|
9
9
|
search box.
|
|
@@ -38,9 +38,22 @@ pi install npm:pi-better-model-select
|
|
|
38
38
|
| `ctrl+a` | Add a model |
|
|
39
39
|
| `ctrl+u` | Update the highlighted model |
|
|
40
40
|
| `ctrl+x` | Delete the highlighted model |
|
|
41
|
+
| `ctrl+o` | Open BMS options |
|
|
41
42
|
| `ctrl+r` | Reset usage statistics |
|
|
42
43
|
| `esc` | Close |
|
|
43
44
|
|
|
45
|
+
## Options
|
|
46
|
+
|
|
47
|
+
Press `ctrl+o` in `/bms` to configure:
|
|
48
|
+
|
|
49
|
+
- **Write mode.** Overlay mode keeps Pi's `models.json` unchanged and stores
|
|
50
|
+
BMS edits in `models.bms.json`. Direct mode writes add, update and delete
|
|
51
|
+
operations to `models.json`.
|
|
52
|
+
- **Provider filters.** Enable or disable providers in the `/bms` model list.
|
|
53
|
+
These filters do not change Pi's `/model` picker or either model file.
|
|
54
|
+
|
|
55
|
+
BMS stores these choices in `<agent-dir>/bms-settings.json`.
|
|
56
|
+
|
|
44
57
|
## Ranking
|
|
45
58
|
|
|
46
59
|
Every model loaded through `/bms` increments a counter in
|
|
@@ -66,9 +79,8 @@ cap, reasoning support and input modalities from, in order:
|
|
|
66
79
|
|
|
67
80
|
Each step is best-effort. A plain OpenAI-compatible endpoint usually yields
|
|
68
81
|
nothing from steps 1 and 2, and the flow falls back to name-based heuristics
|
|
69
|
-
with editable defaults.
|
|
70
|
-
`models.
|
|
71
|
-
a personal BMS overlay. The selected file is re-read immediately before the
|
|
82
|
+
with editable defaults. Choose the default write mode from `ctrl+o` in `/bms`. Overlay mode writes to
|
|
83
|
+
`models.bms.json`; direct mode edits Pi's `models.json`. The selected file is re-read immediately before the
|
|
72
84
|
write so a concurrent edit in another pi session is not clobbered.
|
|
73
85
|
|
|
74
86
|
## Paths
|
|
@@ -77,19 +89,20 @@ All files resolve through pi's `getAgentDir()`, so `PI_CODING_AGENT_DIR` and
|
|
|
77
89
|
rebranded distributions work without special handling:
|
|
78
90
|
|
|
79
91
|
- `<agent-dir>/models.json` is Pi's documented compatible-endpoint configuration.
|
|
80
|
-
- `<agent-dir>/models.
|
|
92
|
+
- `<agent-dir>/models.bms.json` is the optional overlay owned and loaded by BMS.
|
|
93
|
+
- `<agent-dir>/bms-settings.json` stores the write mode and provider filters.
|
|
81
94
|
- `<agent-dir>/bms-usage.json` stores selection counters.
|
|
82
95
|
|
|
83
|
-
Pi does not
|
|
84
|
-
|
|
85
|
-
through Pi's extension API
|
|
86
|
-
|
|
96
|
+
Pi does not load `models.bms.json` itself. In overlay mode, BMS merges it over
|
|
97
|
+
`models.json` by provider and model id, then registers the merged providers
|
|
98
|
+
through Pi's extension API. If the same model exists in both files, fields from
|
|
99
|
+
`models.bms.json` win. Existing `models.local.json` overlays from BMS 0.1.x are
|
|
100
|
+
renamed to `models.bms.json` automatically.
|
|
87
101
|
|
|
88
102
|
Deleting a model that exists only in `models.json` leaves that file untouched.
|
|
89
|
-
BMS writes `{ "id": "...", "bmsDeleted": true }` to the
|
|
90
|
-
This
|
|
91
|
-
|
|
92
|
-
the inherited model.
|
|
103
|
+
BMS writes `{ "id": "...", "bmsDeleted": true }` to the overlay instead.
|
|
104
|
+
This tombstone keeps generated or externally managed model entries hidden across
|
|
105
|
+
restarts. Delete it from `models.bms.json` to restore the inherited model.
|
|
93
106
|
|
|
94
107
|
Writes are atomic using a temporary file and rename, so an interrupted write
|
|
95
108
|
cannot truncate either configuration file.
|
package/extensions/bms.ts
CHANGED
|
@@ -11,8 +11,10 @@ import { ModelPicker, rankModels, type PickerAction } from "./lib/picker.ts";
|
|
|
11
11
|
import {
|
|
12
12
|
loadConfig,
|
|
13
13
|
loadConfigFile,
|
|
14
|
+
loadSettings,
|
|
14
15
|
pruneUsage,
|
|
15
16
|
recordUsage,
|
|
17
|
+
saveSettings,
|
|
16
18
|
saveUsage,
|
|
17
19
|
type ModelEntry,
|
|
18
20
|
type ModelsConfig,
|
|
@@ -47,11 +49,13 @@ function providerConfig(
|
|
|
47
49
|
|
|
48
50
|
/** Apply the personal BMS overlay as extension providers. */
|
|
49
51
|
function syncLocalConfig(pi: ExtensionAPI, ctx: any): ModelsConfig | null {
|
|
50
|
-
const
|
|
52
|
+
const settings = loadSettings();
|
|
53
|
+
const overlay =
|
|
54
|
+
settings.writeMode === "overlay" ? loadConfigFile(ctx, "models.bms.json", true) : { providers: {} };
|
|
51
55
|
const effective = loadConfig(ctx);
|
|
52
|
-
if (!
|
|
56
|
+
if (!overlay || !effective) return null;
|
|
53
57
|
|
|
54
|
-
const next = new Set(Object.keys(
|
|
58
|
+
const next = new Set(Object.keys(overlay.providers ?? {}));
|
|
55
59
|
for (const providerName of registeredLocalProviders) {
|
|
56
60
|
if (!next.has(providerName)) pi.unregisterProvider(providerName);
|
|
57
61
|
}
|
|
@@ -62,7 +66,7 @@ function syncLocalConfig(pi: ExtensionAPI, ctx: any): ModelsConfig | null {
|
|
|
62
66
|
try {
|
|
63
67
|
pi.registerProvider(providerName, providerConfig(ctx, providerName, provider) as any);
|
|
64
68
|
} catch (e) {
|
|
65
|
-
ctx.ui.notify(`Cannot apply ${providerName} from models.
|
|
69
|
+
ctx.ui.notify(`Cannot apply ${providerName} from models.bms.json: ${e}`, "error");
|
|
66
70
|
return null;
|
|
67
71
|
}
|
|
68
72
|
}
|
|
@@ -73,6 +77,52 @@ function syncLocalConfig(pi: ExtensionAPI, ctx: any): ModelsConfig | null {
|
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/** Re-read model configuration from disk and switch the session to the given model. */
|
|
80
|
+
async function optionsFlow(ctx: any, config: ModelsConfig): Promise<void> {
|
|
81
|
+
for (;;) {
|
|
82
|
+
const settings = loadSettings();
|
|
83
|
+
const writeLabel =
|
|
84
|
+
settings.writeMode === "overlay" ? "BMS overlay (models.bms.json)" : "Pi config (models.json)";
|
|
85
|
+
const hidden = settings.disabledProviders.length;
|
|
86
|
+
const pick = await ctx.ui.select("BMS options", [
|
|
87
|
+
`Write mode: ${writeLabel}`,
|
|
88
|
+
`Provider filters: ${hidden} hidden`,
|
|
89
|
+
"Done",
|
|
90
|
+
]);
|
|
91
|
+
if (!pick || pick === "Done") return;
|
|
92
|
+
|
|
93
|
+
if (pick.startsWith("Write mode:")) {
|
|
94
|
+
const mode = await ctx.ui.select("Where should BMS write model changes?", [
|
|
95
|
+
"BMS overlay (models.bms.json)",
|
|
96
|
+
"Pi config (models.json)",
|
|
97
|
+
]);
|
|
98
|
+
if (!mode) continue;
|
|
99
|
+
settings.writeMode = mode.startsWith("BMS overlay") ? "overlay" : "direct";
|
|
100
|
+
saveSettings(settings);
|
|
101
|
+
ctx.ui.notify(`BMS write mode: ${settings.writeMode}.`, "info");
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const providers = Object.keys(config.providers ?? {}).sort();
|
|
106
|
+
for (;;) {
|
|
107
|
+
const current = loadSettings();
|
|
108
|
+
const options = providers.map((provider) =>
|
|
109
|
+
`${current.disabledProviders.includes(provider) ? "[ ]" : "[x]"} ${provider}`,
|
|
110
|
+
);
|
|
111
|
+
const choice = await ctx.ui.select(
|
|
112
|
+
"Providers shown in /bms (select to toggle, ESC when done)",
|
|
113
|
+
options,
|
|
114
|
+
);
|
|
115
|
+
if (!choice) break;
|
|
116
|
+
const provider = choice.slice(4);
|
|
117
|
+
const disabled = new Set(current.disabledProviders);
|
|
118
|
+
if (disabled.has(provider)) disabled.delete(provider);
|
|
119
|
+
else disabled.add(provider);
|
|
120
|
+
current.disabledProviders = [...disabled].sort();
|
|
121
|
+
saveSettings(current);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
76
126
|
async function loadModel(
|
|
77
127
|
pi: ExtensionAPI,
|
|
78
128
|
ctx: any,
|
|
@@ -129,8 +179,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
129
179
|
if (!config) return;
|
|
130
180
|
pruneUsage(config);
|
|
131
181
|
|
|
132
|
-
const
|
|
182
|
+
const settings = loadSettings();
|
|
183
|
+
const rows = rankModels(config, settings.disabledProviders);
|
|
133
184
|
if (rows.length === 0) {
|
|
185
|
+
const total = Object.values(config.providers ?? {}).reduce(
|
|
186
|
+
(sum, provider) => sum + (provider.models?.length ?? 0),
|
|
187
|
+
0,
|
|
188
|
+
);
|
|
189
|
+
if (total > 0) {
|
|
190
|
+
const open = await ctx.ui.confirm(
|
|
191
|
+
"No models visible",
|
|
192
|
+
"All providers are hidden by BMS filters. Open options?",
|
|
193
|
+
);
|
|
194
|
+
if (!open) return;
|
|
195
|
+
await optionsFlow(ctx, config);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
134
198
|
const add = await ctx.ui.confirm(
|
|
135
199
|
"No models configured",
|
|
136
200
|
"The effective model configuration has no models. Add one now?",
|
|
@@ -188,6 +252,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
188
252
|
: undefined,
|
|
189
253
|
);
|
|
190
254
|
break;
|
|
255
|
+
case "options":
|
|
256
|
+
await optionsFlow(ctx, config);
|
|
257
|
+
break;
|
|
191
258
|
case "reset": {
|
|
192
259
|
const ok = await ctx.ui.confirm(
|
|
193
260
|
"Reset usage statistics?",
|
|
@@ -208,7 +275,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
208
275
|
|
|
209
276
|
// Standalone add, kept for muscle memory and non-interactive convenience.
|
|
210
277
|
pi.registerCommand("bms-add", {
|
|
211
|
-
description: "Add a model from a provider's endpoint
|
|
278
|
+
description: "Add a model from a provider's endpoint using the configured write mode",
|
|
212
279
|
handler: async (_args, ctx: any) => addFlow(ctx as FlowCtx),
|
|
213
280
|
});
|
|
214
281
|
}
|
package/extensions/lib/flows.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Add / update / delete flows against the effective model configuration.
|
|
2
2
|
//
|
|
3
|
-
// Each mutation
|
|
3
|
+
// Each mutation follows the saved write mode, then re-reads the target file
|
|
4
4
|
// before saving so another pi session's edit is not silently clobbered.
|
|
5
5
|
|
|
6
6
|
import {
|
|
@@ -17,9 +17,11 @@ import {
|
|
|
17
17
|
configContainsModel,
|
|
18
18
|
loadConfig,
|
|
19
19
|
loadConfigFile,
|
|
20
|
+
loadSettings,
|
|
20
21
|
saveConfig,
|
|
21
22
|
tombstoneModel,
|
|
22
23
|
upsertModel,
|
|
24
|
+
writeTarget,
|
|
23
25
|
type ConfigTarget,
|
|
24
26
|
type ModelEntry,
|
|
25
27
|
type ModelsConfig,
|
|
@@ -37,20 +39,8 @@ export interface FlowCtx {
|
|
|
37
39
|
|
|
38
40
|
const INPUT_OPTIONS: string[][] = [["text"], ["text", "image"]];
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"models.json": "models.json (Pi configuration)",
|
|
43
|
-
} satisfies Record<ConfigTarget, string>;
|
|
44
|
-
|
|
45
|
-
async function selectWriteTarget(
|
|
46
|
-
ctx: FlowCtx,
|
|
47
|
-
message = "Write changes to which file?",
|
|
48
|
-
): Promise<ConfigTarget | undefined> {
|
|
49
|
-
const options = [TARGET_LABELS["models.local.json"], TARGET_LABELS["models.json"]];
|
|
50
|
-
const pick = await ctx.ui.select(message, options);
|
|
51
|
-
if (pick === TARGET_LABELS["models.local.json"]) return "models.local.json";
|
|
52
|
-
if (pick === TARGET_LABELS["models.json"]) return "models.json";
|
|
53
|
-
return undefined;
|
|
42
|
+
function selectedWriteTarget(): ConfigTarget {
|
|
43
|
+
return writeTarget(loadSettings());
|
|
54
44
|
}
|
|
55
45
|
|
|
56
46
|
function providerOverlay(config: ModelsConfig, providerName: string): Record<string, unknown> {
|
|
@@ -187,11 +177,10 @@ export async function addFlow(ctx: FlowCtx): Promise<void> {
|
|
|
187
177
|
if (input === undefined) return;
|
|
188
178
|
crumbs.push(`Input: ${input.join("+")}`);
|
|
189
179
|
|
|
190
|
-
const
|
|
191
|
-
if (!writeTarget) return;
|
|
180
|
+
const targetFile = selectedWriteTarget();
|
|
192
181
|
const ok = await ctx.ui.confirm(
|
|
193
182
|
`${header()}Add this model to "${providerName}"?`,
|
|
194
|
-
`Write the entry to ${
|
|
183
|
+
`Write the entry to ${targetFile}?`,
|
|
195
184
|
);
|
|
196
185
|
if (!ok) {
|
|
197
186
|
ctx.ui.notify("Cancelled. Nothing written.", "info");
|
|
@@ -199,7 +188,7 @@ export async function addFlow(ctx: FlowCtx): Promise<void> {
|
|
|
199
188
|
}
|
|
200
189
|
|
|
201
190
|
const freshEffective = loadConfig(ctx);
|
|
202
|
-
const freshTarget = loadConfigFile(ctx,
|
|
191
|
+
const freshTarget = loadConfigFile(ctx, targetFile, targetFile === "models.bms.json");
|
|
203
192
|
if (!freshEffective || !freshTarget) return;
|
|
204
193
|
if (!freshEffective.providers?.[providerName]) {
|
|
205
194
|
ctx.ui.notify(`Provider "${providerName}" no longer exists.`, "error");
|
|
@@ -218,8 +207,8 @@ export async function addFlow(ctx: FlowCtx): Promise<void> {
|
|
|
218
207
|
contextWindow,
|
|
219
208
|
maxTokens,
|
|
220
209
|
});
|
|
221
|
-
saveConfig(freshTarget,
|
|
222
|
-
ctx.ui.notify(`Added "${modelId}" to ${providerName} in ${
|
|
210
|
+
saveConfig(freshTarget, targetFile);
|
|
211
|
+
ctx.ui.notify(`Added "${modelId}" to ${providerName} in ${targetFile}.`, "info");
|
|
223
212
|
}
|
|
224
213
|
|
|
225
214
|
// ----- update --------------------------------------------------------------
|
|
@@ -381,11 +370,10 @@ export async function updateFlow(
|
|
|
381
370
|
return;
|
|
382
371
|
}
|
|
383
372
|
|
|
384
|
-
const
|
|
385
|
-
if (!writeTarget) return;
|
|
373
|
+
const targetFile = selectedWriteTarget();
|
|
386
374
|
const ok = await ctx.ui.confirm(
|
|
387
375
|
`Update ${existing.id}?`,
|
|
388
|
-
`Write ${[...changes.keys()].join(", ")} to "${providerName}" in ${
|
|
376
|
+
`Write ${[...changes.keys()].join(", ")} to "${providerName}" in ${targetFile}?`,
|
|
389
377
|
);
|
|
390
378
|
if (!ok) {
|
|
391
379
|
ctx.ui.notify("Cancelled. Nothing updated.", "info");
|
|
@@ -393,7 +381,7 @@ export async function updateFlow(
|
|
|
393
381
|
}
|
|
394
382
|
|
|
395
383
|
const freshEffective = loadConfig(ctx);
|
|
396
|
-
const freshTarget = loadConfigFile(ctx,
|
|
384
|
+
const freshTarget = loadConfigFile(ctx, targetFile, targetFile === "models.bms.json");
|
|
397
385
|
if (!freshEffective || !freshTarget) return;
|
|
398
386
|
const modelEntry = completeModel(freshEffective, providerName, existing.id);
|
|
399
387
|
if (!modelEntry) {
|
|
@@ -403,9 +391,9 @@ export async function updateFlow(
|
|
|
403
391
|
Object.assign(modelEntry, Object.fromEntries(changes));
|
|
404
392
|
ensureProviderSettings(freshTarget, freshEffective, providerName);
|
|
405
393
|
upsertModel(freshTarget, providerName, modelEntry);
|
|
406
|
-
saveConfig(freshTarget,
|
|
394
|
+
saveConfig(freshTarget, targetFile);
|
|
407
395
|
ctx.ui.notify(
|
|
408
|
-
`Updated "${existing.id}" in ${providerName} via ${
|
|
396
|
+
`Updated "${existing.id}" in ${providerName} via ${targetFile}: ${[...changes.entries()]
|
|
409
397
|
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join("+") : String(v)}`)
|
|
410
398
|
.join(", ")}.`,
|
|
411
399
|
"info",
|
|
@@ -444,60 +432,66 @@ export async function deleteFlow(
|
|
|
444
432
|
({ provider, id } = map.get(label)!);
|
|
445
433
|
}
|
|
446
434
|
|
|
435
|
+
const settings = loadSettings();
|
|
436
|
+
const targetFile = writeTarget(settings);
|
|
447
437
|
const base = loadConfigFile(ctx, "models.json");
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
438
|
+
if (!base) return;
|
|
439
|
+
|
|
440
|
+
if (settings.writeMode === "overlay") {
|
|
441
|
+
const overlay = loadConfigFile(ctx, "models.bms.json", true);
|
|
442
|
+
if (!overlay) return;
|
|
443
|
+
const inherited = configContainsModel(base, provider, id);
|
|
444
|
+
const local = configContainsModel(overlay, provider, id);
|
|
445
|
+
if (!inherited && !local) {
|
|
446
|
+
ctx.ui.notify(`Model "${id}" is no longer present in either config file.`, "warning");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
458
449
|
|
|
459
|
-
if (candidates.length === 1 && candidates[0] === "models.json") {
|
|
460
450
|
const ok = await ctx.ui.confirm(
|
|
461
|
-
|
|
462
|
-
|
|
451
|
+
`${inherited && !local ? "Hide inherited model" : "Delete"} ${id}?`,
|
|
452
|
+
inherited && !local
|
|
453
|
+
? "Keep models.json unchanged and add a BMS tombstone to models.bms.json?"
|
|
454
|
+
: `Remove it from "${provider}" in models.bms.json?`,
|
|
463
455
|
);
|
|
464
456
|
if (!ok) {
|
|
465
457
|
ctx.ui.notify("Cancelled. Nothing deleted.", "info");
|
|
466
458
|
return;
|
|
467
459
|
}
|
|
468
460
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
461
|
+
if (inherited) tombstoneModel(overlay, provider, id);
|
|
462
|
+
else {
|
|
463
|
+
const target = overlay.providers?.[provider];
|
|
464
|
+
if (target?.models) target.models = target.models.filter((model) => model.id !== id);
|
|
465
|
+
}
|
|
466
|
+
saveConfig(overlay, "models.bms.json");
|
|
467
|
+
ctx.ui.notify(
|
|
468
|
+
inherited
|
|
469
|
+
? `Hidden inherited model "${id}" via models.bms.json.`
|
|
470
|
+
: `Deleted "${id}" from ${provider} in models.bms.json.`,
|
|
471
|
+
"info",
|
|
472
|
+
);
|
|
474
473
|
return;
|
|
475
474
|
}
|
|
476
475
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
} else {
|
|
481
|
-
const selected = await selectWriteTarget(ctx, "Remove the model from which file?");
|
|
482
|
-
if (!selected) return;
|
|
483
|
-
writeTarget = selected;
|
|
476
|
+
if (!configContainsModel(base, provider, id)) {
|
|
477
|
+
ctx.ui.notify(`Model "${id}" is no longer present in models.json.`, "warning");
|
|
478
|
+
return;
|
|
484
479
|
}
|
|
485
|
-
|
|
486
480
|
const ok = await ctx.ui.confirm(
|
|
487
481
|
`Delete ${id}?`,
|
|
488
|
-
`Remove it from "${provider}" in ${
|
|
482
|
+
`Remove it from "${provider}" in ${targetFile}?`,
|
|
489
483
|
);
|
|
490
484
|
if (!ok) {
|
|
491
485
|
ctx.ui.notify("Cancelled. Nothing deleted.", "info");
|
|
492
486
|
return;
|
|
493
487
|
}
|
|
494
488
|
|
|
495
|
-
const fresh = loadConfigFile(ctx,
|
|
489
|
+
const fresh = loadConfigFile(ctx, targetFile, targetFile === "models.bms.json");
|
|
496
490
|
if (!fresh) return;
|
|
497
491
|
const target = fresh.providers?.[provider];
|
|
498
492
|
if (target?.models) {
|
|
499
493
|
target.models = target.models.filter((m) => m.id !== id);
|
|
500
|
-
saveConfig(fresh,
|
|
501
|
-
ctx.ui.notify(`Deleted "${id}" from ${provider} in ${
|
|
494
|
+
saveConfig(fresh, targetFile);
|
|
495
|
+
ctx.ui.notify(`Deleted "${id}" from ${provider} in ${targetFile}.`, "info");
|
|
502
496
|
}
|
|
503
497
|
}
|
package/extensions/lib/picker.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type PickerAction =
|
|
|
22
22
|
| { kind: "add" }
|
|
23
23
|
| { kind: "update"; provider?: string; modelId?: string }
|
|
24
24
|
| { kind: "delete"; provider?: string; modelId?: string }
|
|
25
|
+
| { kind: "options" }
|
|
25
26
|
| { kind: "reset" }
|
|
26
27
|
| { kind: "close" };
|
|
27
28
|
|
|
@@ -39,10 +40,15 @@ export interface RankedModel {
|
|
|
39
40
|
* Rank models by usage: most-selected first, ties broken by most-recent, then
|
|
40
41
|
* alphabetically so the order is stable for never-used models.
|
|
41
42
|
*/
|
|
42
|
-
export function rankModels(
|
|
43
|
+
export function rankModels(
|
|
44
|
+
config: ModelsConfig,
|
|
45
|
+
disabledProviders: readonly string[] = [],
|
|
46
|
+
): RankedModel[] {
|
|
43
47
|
const stats = loadUsage();
|
|
48
|
+
const disabled = new Set(disabledProviders);
|
|
44
49
|
const rows: RankedModel[] = [];
|
|
45
50
|
for (const [provider, entry] of Object.entries(config.providers ?? {})) {
|
|
51
|
+
if (disabled.has(provider)) continue;
|
|
46
52
|
for (const m of entry.models ?? []) {
|
|
47
53
|
const key = usageKey(provider, m.id);
|
|
48
54
|
rows.push({
|
|
@@ -103,7 +109,7 @@ export function selectListTheme(theme: any): SelectListTheme {
|
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
const HINT =
|
|
106
|
-
"↑↓ move • enter load • ctrl+a add • ctrl+u update • ctrl+x delete • ctrl+r reset
|
|
112
|
+
"↑↓ move • enter load • ctrl+a add • ctrl+u update • ctrl+x delete • ctrl+o options • ctrl+r reset • esc close";
|
|
107
113
|
|
|
108
114
|
export class ModelPicker extends Container implements Focusable {
|
|
109
115
|
private search: Input;
|
|
@@ -229,6 +235,7 @@ export class ModelPicker extends Container implements Focusable {
|
|
|
229
235
|
handleInput(data: string): void {
|
|
230
236
|
if (matchesKey(data, "escape")) return this.done({ kind: "close" });
|
|
231
237
|
if (matchesKey(data, "ctrl+a")) return this.done({ kind: "add" });
|
|
238
|
+
if (matchesKey(data, "ctrl+o")) return this.done({ kind: "options" });
|
|
232
239
|
if (matchesKey(data, "ctrl+r")) return this.done({ kind: "reset" });
|
|
233
240
|
|
|
234
241
|
if (matchesKey(data, "ctrl+u")) {
|
package/extensions/lib/store.ts
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
// Model configuration access + usage statistics store.
|
|
2
2
|
//
|
|
3
|
-
// Pi reads models.json itself. Better Model Select also
|
|
4
|
-
// models.
|
|
5
|
-
//
|
|
3
|
+
// Pi reads models.json itself. Better Model Select can also use a BMS-owned
|
|
4
|
+
// models.bms.json overlay. The extension merges and registers that overlay at
|
|
5
|
+
// runtime, so Pi never needs to understand the extra file.
|
|
6
6
|
|
|
7
7
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
|
-
export type ConfigTarget = "models.json" | "models.
|
|
11
|
+
export type ConfigTarget = "models.json" | "models.bms.json";
|
|
12
|
+
export type WriteMode = "overlay" | "direct";
|
|
12
13
|
|
|
13
14
|
export const MODELS_PATH = (): string => join(getAgentDir(), "models.json");
|
|
14
|
-
export const
|
|
15
|
+
export const BMS_MODELS_PATH = (): string => join(getAgentDir(), "models.bms.json");
|
|
16
|
+
export const LEGACY_LOCAL_MODELS_PATH = (): string => join(getAgentDir(), "models.local.json");
|
|
17
|
+
export const SETTINGS_PATH = (): string => join(getAgentDir(), "bms-settings.json");
|
|
15
18
|
export const USAGE_PATH = (): string => join(getAgentDir(), "bms-usage.json");
|
|
16
19
|
|
|
20
|
+
export interface BmsSettings {
|
|
21
|
+
version: 1;
|
|
22
|
+
writeMode: WriteMode;
|
|
23
|
+
disabledProviders: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_SETTINGS: BmsSettings = {
|
|
27
|
+
version: 1,
|
|
28
|
+
writeMode: "overlay",
|
|
29
|
+
disabledProviders: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
17
32
|
export interface ModelEntry {
|
|
18
33
|
id: string;
|
|
19
34
|
/** BMS overlay tombstone. Hides an inherited model without editing models.json. */
|
|
@@ -41,7 +56,37 @@ export interface ModelsConfig {
|
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
export function configPath(target: ConfigTarget): string {
|
|
44
|
-
return target === "models.
|
|
59
|
+
return target === "models.bms.json" ? BMS_MODELS_PATH() : MODELS_PATH();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Move the old BMS overlay name to the explicit BMS-owned filename. */
|
|
63
|
+
export function migrateLegacyOverlay(): void {
|
|
64
|
+
if (!existsSync(BMS_MODELS_PATH()) && existsSync(LEGACY_LOCAL_MODELS_PATH())) {
|
|
65
|
+
renameSync(LEGACY_LOCAL_MODELS_PATH(), BMS_MODELS_PATH());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function loadSettings(): BmsSettings {
|
|
70
|
+
try {
|
|
71
|
+
const raw = JSON.parse(readFileSync(SETTINGS_PATH(), "utf8")) as Partial<BmsSettings>;
|
|
72
|
+
return {
|
|
73
|
+
version: 1,
|
|
74
|
+
writeMode: raw.writeMode === "direct" ? "direct" : "overlay",
|
|
75
|
+
disabledProviders: Array.isArray(raw.disabledProviders)
|
|
76
|
+
? raw.disabledProviders.filter((value): value is string => typeof value === "string")
|
|
77
|
+
: [],
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return structuredClone(DEFAULT_SETTINGS);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function saveSettings(settings: BmsSettings): void {
|
|
85
|
+
writeJsonAtomic(SETTINGS_PATH(), settings);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function writeTarget(settings = loadSettings()): ConfigTarget {
|
|
89
|
+
return settings.writeMode === "direct" ? "models.json" : "models.bms.json";
|
|
45
90
|
}
|
|
46
91
|
|
|
47
92
|
/** Write JSON atomically so a crash mid-write cannot truncate the target file. */
|
|
@@ -60,6 +105,7 @@ export function loadConfigFile(
|
|
|
60
105
|
target: ConfigTarget,
|
|
61
106
|
missingIsEmpty = false,
|
|
62
107
|
): ModelsConfig | null {
|
|
108
|
+
if (target === "models.bms.json") migrateLegacyOverlay();
|
|
63
109
|
const path = configPath(target);
|
|
64
110
|
if (missingIsEmpty && !existsSync(path)) return { providers: {} };
|
|
65
111
|
try {
|
|
@@ -70,7 +116,7 @@ export function loadConfigFile(
|
|
|
70
116
|
}
|
|
71
117
|
}
|
|
72
118
|
|
|
73
|
-
/** Merge models.
|
|
119
|
+
/** Merge models.bms.json over models.json. Model entries merge by id. */
|
|
74
120
|
export function mergeConfigs(base: ModelsConfig, local: ModelsConfig): ModelsConfig {
|
|
75
121
|
const merged = structuredClone(base);
|
|
76
122
|
merged.providers = merged.providers ?? {};
|
|
@@ -102,9 +148,11 @@ export function loadConfig(ctx: {
|
|
|
102
148
|
}): ModelsConfig | null {
|
|
103
149
|
const base = loadConfigFile(ctx, "models.json");
|
|
104
150
|
if (!base) return null;
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
151
|
+
if (loadSettings().writeMode === "direct") return base;
|
|
152
|
+
migrateLegacyOverlay();
|
|
153
|
+
if (!existsSync(BMS_MODELS_PATH())) return base;
|
|
154
|
+
const overlay = loadConfigFile(ctx, "models.bms.json");
|
|
155
|
+
return overlay ? mergeConfigs(base, overlay) : null;
|
|
108
156
|
}
|
|
109
157
|
|
|
110
158
|
export function saveConfig(config: ModelsConfig, target: ConfigTarget): void {
|
|
@@ -119,7 +167,7 @@ export function configContainsModel(
|
|
|
119
167
|
return Boolean(config.providers?.[provider]?.models?.some((model) => model.id === modelId));
|
|
120
168
|
}
|
|
121
169
|
|
|
122
|
-
/** Hide an inherited model through the BMS
|
|
170
|
+
/** Hide an inherited model through the BMS overlay. */
|
|
123
171
|
export function tombstoneModel(config: ModelsConfig, provider: string, modelId: string): void {
|
|
124
172
|
upsertModel(config, provider, { id: modelId, bmsDeleted: true });
|
|
125
173
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-model-select",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Better model select for pi — /bms opens a searchable model picker sorted by how often you actually use each model, with inline add, update and delete.",
|
|
6
6
|
"keywords": [
|