pi-better-model-select 0.2.0 → 0.3.1
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/CHANGELOG.md +63 -0
- package/README.md +17 -7
- package/docs/configuration.md +8 -2
- package/docs/demo/showcase.gif +0 -0
- package/extensions/bms.ts +62 -50
- package/extensions/lib/defaults.ts +146 -52
- package/extensions/lib/effort-editor.ts +35 -20
- package/extensions/lib/flows.ts +126 -496
- package/extensions/lib/picker-dialog.ts +11 -19
- package/extensions/lib/picker.ts +90 -162
- package/extensions/lib/store.ts +3 -0
- package/extensions/lib/tui.ts +320 -0
- package/package.json +3 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Included the README's showcase GIF in the npm package without shipping the other demo assets.
|
|
8
|
+
- Restored the Ctrl+D set-default hint on the main model search screen, including at 80-column widths.
|
|
9
|
+
- Accepting "Save and use anyway" now saves without asking for confirmation again. Default-save messages show the settings path and identify any startup override that remains.
|
|
10
|
+
|
|
11
|
+
## 0.3.0
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `/bms-options` for direct access to result placement, write mode, and provider visibility.
|
|
16
|
+
- Above-prompt result placement, with the best match nearest search. Below-prompt placement remains the default.
|
|
17
|
+
- Ctrl+O Actions menu with access to model management, defaults, options, and usage reset.
|
|
18
|
+
- Visible **Save configuration** and **Back** actions in forms, alongside Ctrl+S and Esc.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Replaced the model list with aligned MODEL, PROVIDER, CONTEXT, USES, and STATUS columns, a full-row selection highlight, and up to six visible results.
|
|
23
|
+
- Moved search into a prompt-style field in Pi's editor area. Shortcut hints and the result count follow result placement.
|
|
24
|
+
- Replaced model-edit questionnaires with draft forms. Changes are saved explicitly; leaving a modified form asks whether to discard them.
|
|
25
|
+
- Organized Pi and subagent defaults into a table with model, effort, and source information.
|
|
26
|
+
- Preserved search and selection across submenus. Returning from an Actions submenu restores the selected action.
|
|
27
|
+
- Kept non-search menus top-to-bottom regardless of result placement.
|
|
28
|
+
- Styled shortcut keys separately from action descriptions and omitted whole hints at narrow widths.
|
|
29
|
+
- Made destructive confirmations cancel-first and explicit about write scope.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Restored the same form draft and selection after closing a child dialog, without losing it when Pi restores its editor.
|
|
34
|
+
|
|
35
|
+
## 0.2.0
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Default model and thinking effort configuration for Pi, the subagent fallback, and individual agent roles.
|
|
40
|
+
- `/bms-defaults`, Ctrl+D for the Pi default, and Ctrl+G for the defaults menu.
|
|
41
|
+
- User/project scope selection, effective-value sources, and warnings about higher-priority overrides.
|
|
42
|
+
- Provider-reported effort capability hints, editable supported levels, and per-model preferred effort.
|
|
43
|
+
- Active-model and saved-default markers in the picker.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Discovery of pi-subagents roles with the newer bundled entry-point layout, checked against pi-subagents 0.82.1 after the original 0.79.0 integration.
|
|
48
|
+
|
|
49
|
+
## 0.1.4
|
|
50
|
+
|
|
51
|
+
- Added BMS options and provider filters.
|
|
52
|
+
|
|
53
|
+
## 0.1.3
|
|
54
|
+
|
|
55
|
+
- Renamed the package to `pi-better-model-select`.
|
|
56
|
+
|
|
57
|
+
## 0.1.2
|
|
58
|
+
|
|
59
|
+
- Persisted deletion of inherited models in the personal overlay.
|
|
60
|
+
|
|
61
|
+
## 0.1.1
|
|
62
|
+
|
|
63
|
+
- Added a personal model overlay so local edits can remain separate from Pi's model configuration.
|
package/README.md
CHANGED
|
@@ -10,12 +10,21 @@ pi install npm:pi-better-model-select
|
|
|
10
10
|
|
|
11
11
|
Run `/bms`, type to filter by name, model ID, or provider, then press Enter to switch models for the current session.
|
|
12
12
|
|
|
13
|
+
## Compact TUI
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
The picker uses aligned model, provider, context, usage, and status columns. Type in the search field in Pi's editor area. Results appear below search by default, matching Pi command completion. Choose **Options → Result placement → Above prompt** for bottom-up results with the best match nearest the search line.
|
|
18
|
+
|
|
19
|
+
Add and edit use one form. Review the fields, press Enter to change a value, then choose **Save configuration** or press Ctrl+S to save. Text and numeric fields currently open a Pi input dialog; choices use the compact table. Esc goes back; unsaved changes require a discard confirmation. Options also stay in a draft until saved.
|
|
20
|
+
|
|
13
21
|
## Commands and shortcuts
|
|
14
22
|
|
|
15
23
|
| Command | Action |
|
|
16
24
|
| --- | --- |
|
|
17
25
|
| `/bms` | Open the model picker |
|
|
18
26
|
| `/bms-add` | Add a model |
|
|
27
|
+
| `/bms-options` | Open result placement, write mode, and provider options |
|
|
19
28
|
| `/bms-defaults` | Configure Pi and subagent defaults |
|
|
20
29
|
|
|
21
30
|
Inside `/bms`:
|
|
@@ -27,15 +36,15 @@ Inside `/bms`:
|
|
|
27
36
|
| Ctrl+D | Choose effort, save as the Pi default, and use it now |
|
|
28
37
|
| Ctrl+G | Open the defaults menu |
|
|
29
38
|
| Ctrl+A / Ctrl+U / Ctrl+X | Add / update / delete a model |
|
|
30
|
-
| Ctrl+O |
|
|
39
|
+
| Ctrl+O | Open actions, including options and defaults |
|
|
31
40
|
| Ctrl+R | Reset usage ranking |
|
|
32
41
|
| Esc | Close |
|
|
33
42
|
|
|
34
|
-
The
|
|
43
|
+
The status column identifies the active model and saved default. Only Enter selections count toward usage ranking. Returning from a submenu preserves your search and selection. Plain menus always browse top-to-bottom; only searchable result lists reverse in above-prompt mode. Esc goes back one level, including back to Actions when that is where you entered a submenu.
|
|
35
44
|
|
|
36
45
|
## Default models and effort
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
Save a model and thinking effort for Pi, a subagent fallback, or an individual agent role.
|
|
39
48
|
|
|
40
49
|
- Use Ctrl+D in `/bms` for the main Pi default.
|
|
41
50
|
- Use `/bms-defaults` for all defaults. The menu starts in user scope; select the scope row to switch to a trusted project's settings.
|
|
@@ -55,7 +64,7 @@ pi install npm:pi-subagents
|
|
|
55
64
|
|
|
56
65
|
BMS discovers its agent roles and edits their saved model and effort defaults. It does not install `pi-subagents` automatically. External runner roles are read-only because their model settings belong to the runner.
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
If discovery fails, **Agent discovery details** shows the error. Pi defaults work without this integration. See the configuration reference for compatibility details.
|
|
59
68
|
|
|
60
69
|
### Supported effort levels
|
|
61
70
|
|
|
@@ -69,7 +78,7 @@ Add/update also lets you edit supported levels and set a per-model preferred eff
|
|
|
69
78
|
|
|
70
79
|
## Where changes are saved
|
|
71
80
|
|
|
72
|
-
Ctrl+O
|
|
81
|
+
Open Ctrl+O → Options to select the model write mode:
|
|
73
82
|
|
|
74
83
|
| Mode | Behavior |
|
|
75
84
|
| --- | --- |
|
|
@@ -82,8 +91,9 @@ Provider filters hide entries only in `/bms`, not Pi's `/model` picker. Default-
|
|
|
82
91
|
|
|
83
92
|
## Further documentation
|
|
84
93
|
|
|
85
|
-
- [
|
|
86
|
-
- [
|
|
94
|
+
- [Changelog](CHANGELOG.md): changes by version.
|
|
95
|
+
- [Configuration reference](docs/configuration.md): file paths, precedence, overlays, metadata, and integration limits.
|
|
96
|
+
- [Development and testing](docs/development.md): local loading, isolated tests, checks, and publishing.
|
|
87
97
|
|
|
88
98
|
## License
|
|
89
99
|
|
package/docs/configuration.md
CHANGED
|
@@ -10,7 +10,7 @@ See the [README](../README.md) for installation and shortcuts.
|
|
|
10
10
|
| --- | --- |
|
|
11
11
|
| `<agent-dir>/models.json` | Pi model definitions |
|
|
12
12
|
| `<agent-dir>/models.bms.json` | BMS model overlay |
|
|
13
|
-
| `<agent-dir>/bms-settings.json` | Write mode and provider filters |
|
|
13
|
+
| `<agent-dir>/bms-settings.json` | Write mode, result placement, and provider filters |
|
|
14
14
|
| `<agent-dir>/bms-usage.json` | Selection counts and recency |
|
|
15
15
|
| `<agent-dir>/bms-model-metadata.json` | Effort capability hints and their source |
|
|
16
16
|
| `<agent-dir>/settings.json` | User defaults and per-model effort preferences |
|
|
@@ -18,7 +18,7 @@ See the [README](../README.md) for installation and shortcuts.
|
|
|
18
18
|
|
|
19
19
|
## Model write modes
|
|
20
20
|
|
|
21
|
-
Choose the write mode through Ctrl+O in `/bms
|
|
21
|
+
Choose the write mode through Ctrl+O → Options in `/bms`, then Ctrl+S to save.
|
|
22
22
|
|
|
23
23
|
- Overlay mode, the default, leaves `models.json` unchanged and writes edits to `models.bms.json`.
|
|
24
24
|
- Direct mode writes add, update, and delete operations to `models.json`.
|
|
@@ -35,6 +35,12 @@ Remove that entry from `models.bms.json` to restore the inherited model. Model f
|
|
|
35
35
|
|
|
36
36
|
Provider filters affect only `/bms`. They do not change Pi's `/model` picker or model definitions. Default-assignment menus ignore the filters and also include authenticated Pi registry models.
|
|
37
37
|
|
|
38
|
+
## Picker layout
|
|
39
|
+
|
|
40
|
+
`bms-settings.json` accepts `resultPlacement: "below"` or `"above"`. Missing or invalid values use `"below"`, matching Pi command completion. In above mode the best match is the bottom row, immediately above search; arrow keys follow physical screen direction. Searchable assignment menus use the same preference. Forms retain their normal top-to-bottom field order.
|
|
41
|
+
|
|
42
|
+
The search field is part of a temporary custom interaction in Pi's editor area. BMS does not replace the normal editor factory. Enter loads the selected model; Esc returns to the editor. Up to six results are shown at a time, with scrolling for the rest.
|
|
43
|
+
|
|
38
44
|
## Defaults and precedence
|
|
39
45
|
|
|
40
46
|
Default settings are independent of the model write mode.
|
|
Binary file
|
package/extensions/bms.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
8
8
|
import { defaultsFlow, piPreference, setMainDefault } from "./lib/defaults.ts";
|
|
9
9
|
import { readSettings, settingsPath } from "./lib/defaults-store.ts";
|
|
10
10
|
import { pickerDialog } from "./lib/picker-dialog.ts";
|
|
11
|
+
import { confirmAction, editForm, tableSelect } from "./lib/tui.ts";
|
|
11
12
|
import { addFlow, deleteFlow, updateFlow, type FlowCtx } from "./lib/flows.ts";
|
|
12
13
|
import { rankModels } from "./lib/picker.ts";
|
|
13
14
|
import {
|
|
@@ -78,51 +79,50 @@ function syncLocalConfig(pi: ExtensionAPI, ctx: any): ModelsConfig | null {
|
|
|
78
79
|
return effective;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
/**
|
|
82
|
+
/** Stage option changes until the user explicitly saves. */
|
|
82
83
|
async function optionsFlow(ctx: any, config: ModelsConfig): Promise<void> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
84
|
+
const settings = loadSettings();
|
|
85
|
+
const providers = Object.keys(config.providers ?? {}).sort();
|
|
86
|
+
const writeLabel = () => settings.writeMode === "overlay" ? "BMS overlay (models.bms.json)" : "Pi config (models.json)";
|
|
87
|
+
await editForm(ctx, {
|
|
88
|
+
title: "Models / Options",
|
|
89
|
+
fields: [
|
|
90
|
+
{ key: "placement", label: "Result placement", value: settings.resultPlacement === "above" ? "Above prompt" : "Below prompt",
|
|
91
|
+
description: "The best match stays nearest the search line. Arrow keys follow the displayed direction.",
|
|
92
|
+
edit: async () => {
|
|
93
|
+
const value = await tableSelect(ctx, { title: "Models / Options / Result placement", initialValue: settings.resultPlacement,
|
|
94
|
+
rows: [{ value: "below", label: "Below prompt", description: "Pi-style results. Down moves into the list." }, { value: "above", label: "Above prompt", description: "Results grow upward. Up moves into the list." }] });
|
|
95
|
+
if (!value) return;
|
|
96
|
+
settings.resultPlacement = value as "above" | "below";
|
|
97
|
+
return value === "above" ? "Above prompt" : "Below prompt";
|
|
98
|
+
} },
|
|
99
|
+
{ key: "write", label: "Write mode", value: writeLabel(),
|
|
100
|
+
description: "Overlay keeps BMS edits separate from models.json. Direct mode edits Pi's model configuration.",
|
|
101
|
+
edit: async () => {
|
|
102
|
+
const value = await tableSelect(ctx, { title: "Models / Options / Write mode", initialValue: settings.writeMode,
|
|
103
|
+
rows: [{ value: "overlay", label: "BMS overlay", description: "models.bms.json" }, { value: "direct", label: "Pi config", description: "models.json" }] });
|
|
104
|
+
if (!value) return;
|
|
105
|
+
settings.writeMode = value as "overlay" | "direct";
|
|
106
|
+
return writeLabel();
|
|
107
|
+
} },
|
|
108
|
+
{ key: "providers", label: "Providers", value: `${providers.filter(p => !settings.disabledProviders.includes(p)).length} enabled`,
|
|
109
|
+
description: "Choose a provider to toggle visibility. Esc returns to options; Ctrl+S saves the complete draft.",
|
|
110
|
+
edit: async () => {
|
|
111
|
+
let selected: string | undefined;
|
|
112
|
+
for (;;) {
|
|
113
|
+
selected = await tableSelect(ctx, { title: "Models / Options / Providers", initialValue: selected, search: true, placement: settings.resultPlacement,
|
|
114
|
+
rows: providers.map(provider => ({ value: provider, label: `${settings.disabledProviders.includes(provider) ? "[ ]" : "[x]"} ${provider}` })),
|
|
115
|
+
hint: "↑↓ move Enter toggle Esc back" });
|
|
116
|
+
if (!selected) break;
|
|
117
|
+
const disabled = new Set(settings.disabledProviders);
|
|
118
|
+
if (disabled.has(selected)) disabled.delete(selected); else disabled.add(selected);
|
|
119
|
+
settings.disabledProviders = [...disabled].sort();
|
|
120
|
+
}
|
|
121
|
+
return `${providers.filter(p => !settings.disabledProviders.includes(p)).length} enabled`;
|
|
122
|
+
} },
|
|
123
|
+
],
|
|
124
|
+
onSave: async () => { saveSettings(settings); ctx.ui.notify("BMS options saved.", "info"); return true; },
|
|
125
|
+
});
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
async function loadModel(
|
|
@@ -173,6 +173,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
173
173
|
const addModel = async (ctx: any) => addFlow(ctx, async (provider, modelId) => {
|
|
174
174
|
if (syncLocalConfig(pi, ctx)) await report(ctx, () => setMainDefault(pi, ctx, provider, modelId));
|
|
175
175
|
});
|
|
176
|
+
pi.registerCommand("bms-options", {
|
|
177
|
+
description: "Configure BMS result placement, write mode, and provider visibility",
|
|
178
|
+
handler: async (_args, ctx: any) => {
|
|
179
|
+
if (ctx.mode !== "tui") { ctx.ui.notify("/bms-options needs an interactive terminal.", "warning"); return; }
|
|
180
|
+
await report(ctx, async () => {
|
|
181
|
+
const config = syncLocalConfig(pi, ctx);
|
|
182
|
+
if (config) await optionsFlow(ctx, config);
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
});
|
|
176
186
|
pi.registerCommand("bms-defaults", {
|
|
177
187
|
description: "Configure Pi and subagent default models and effort levels",
|
|
178
188
|
handler: async (_args, ctx: any) => {
|
|
@@ -192,7 +202,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
192
202
|
return;
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
//
|
|
205
|
+
// Preserve search and selection while opening submenus.
|
|
206
|
+
const pickerState: { query?: string; selectedValue?: string } = {};
|
|
196
207
|
for (;;) {
|
|
197
208
|
const config = syncLocalConfig(pi, ctx);
|
|
198
209
|
if (!config) return;
|
|
@@ -230,7 +241,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
230
241
|
row.default = preference.provider === row.provider && preference.model === row.modelId;
|
|
231
242
|
}
|
|
232
243
|
} catch (error) { ctx.ui.notify(`Cannot read defaults: ${error}`, "warning"); }
|
|
233
|
-
const action = await pickerDialog(ctx, rows);
|
|
244
|
+
const action = await pickerDialog(ctx, rows, "Models", false, { placement: settings.resultPlacement, state: pickerState });
|
|
234
245
|
|
|
235
246
|
switch (action?.kind) {
|
|
236
247
|
case "load":
|
|
@@ -265,10 +276,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
265
276
|
await optionsFlow(ctx, config);
|
|
266
277
|
break;
|
|
267
278
|
case "reset": {
|
|
268
|
-
const ok = await ctx
|
|
269
|
-
"Reset usage
|
|
270
|
-
"
|
|
271
|
-
|
|
279
|
+
const ok = await confirmAction(ctx, {
|
|
280
|
+
title: "Models / Reset usage",
|
|
281
|
+
lines: ["Clear all BMS model selection counts and last-used times.", "Ranking returns to alphabetical order. Models and defaults are unchanged."],
|
|
282
|
+
confirmLabel: "Reset usage",
|
|
283
|
+
});
|
|
272
284
|
if (ok) {
|
|
273
285
|
saveUsage({ version: 1, counts: {}, lastUsed: {} });
|
|
274
286
|
ctx.ui.notify("Usage statistics cleared.", "info");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { discoverRoles, type AgentRole } from "./agents.ts";
|
|
2
2
|
import { capabilities, isEffort, patchSettings, readSettings, roleField, setPiDefault, setRoleField, settingsPath, type Effort, type JsonObject, type Scope } from "./defaults-store.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { tableSelect, editForm, confirmAction } from "./tui.ts";
|
|
4
4
|
import { rankModelsForDefaults } from "./defaults-models.ts";
|
|
5
5
|
import type { ModelsConfig } from "./store.ts";
|
|
6
6
|
|
|
@@ -8,7 +8,10 @@ export async function chooseEffort(ctx: any, key: string, model?: any, initial?:
|
|
|
8
8
|
const capability = capabilities(key, model);
|
|
9
9
|
const levels = [...capability.levels];
|
|
10
10
|
if (isEffort(initial) && levels.includes(initial)) levels.splice(levels.indexOf(initial), 1), levels.unshift(initial);
|
|
11
|
-
|
|
11
|
+
const title = `Effort for ${key}\n${capability.source}\nAutomatic removes this scope's preference. Pi/provider may clamp unsupported levels.`;
|
|
12
|
+
// Keep the public helper usable by RPC and legacy headless select mocks.
|
|
13
|
+
if (typeof ctx.ui.custom !== "function") return ctx.ui.select(title, ["automatic", ...levels]);
|
|
14
|
+
return await tableSelect(ctx, { title, initialValue: typeof initial === "string" ? initial : "automatic", rows: ["automatic", ...levels].map(value => ({ value, label: value })) }) as Effort | "automatic" | undefined;
|
|
12
15
|
}
|
|
13
16
|
export function piPreference(user: JsonObject, project: JsonObject): { provider?: string; model?: string; effort?: Effort; source: string } {
|
|
14
17
|
const provider = project.defaultProvider ?? user.defaultProvider;
|
|
@@ -21,43 +24,58 @@ function trusted(pi: any, ctx: any): boolean {
|
|
|
21
24
|
return typeof ctx.isProjectTrusted === "function" ? ctx.isProjectTrusted() : typeof pi.isProjectTrusted === "function" && pi.isProjectTrusted();
|
|
22
25
|
}
|
|
23
26
|
function projectSettings(pi: any, ctx: any, cwd: string): JsonObject { return trusted(pi, ctx) ? readSettings(settingsPath(cwd, "project")) : {}; }
|
|
27
|
+
function confirmDefault(ctx: any, title: string, message: string, confirmLabel: string): Promise<boolean> {
|
|
28
|
+
return typeof ctx.ui.custom === "function"
|
|
29
|
+
? confirmAction(ctx, { title, lines: message.split("\n"), confirmLabel })
|
|
30
|
+
: ctx.ui.confirm(title, message);
|
|
31
|
+
}
|
|
24
32
|
|
|
25
33
|
/** Switch first; never replace saved defaults when authentication/model selection fails. */
|
|
26
34
|
export async function setMainDefault(pi: any, ctx: any, provider: string, modelId: string, scope: Scope = "user"): Promise<void> {
|
|
35
|
+
await applyMainDefault(pi, ctx, provider, modelId, scope);
|
|
36
|
+
}
|
|
37
|
+
async function applyMainDefault(pi: any, ctx: any, provider: string, modelId: string, scope: Scope, selectedEffort?: Effort | "automatic"): Promise<boolean> {
|
|
27
38
|
if (scope === "project" && !trusted(pi, ctx)) throw new Error("Project is not trusted");
|
|
28
39
|
await ctx.modelRegistry.refresh();
|
|
29
40
|
const model = ctx.modelRegistry.find(provider, modelId);
|
|
30
41
|
if (!model) throw new Error(`Model ${provider}/${modelId} is not in the active registry`);
|
|
31
42
|
const user = readSettings(settingsPath(ctx.cwd, "user"));
|
|
32
43
|
const project = projectSettings(pi, ctx, ctx.cwd);
|
|
33
|
-
const choice = await chooseEffort(ctx, `${provider}/${modelId}`, model, piPreference(user, project).effort);
|
|
34
|
-
if (choice === undefined) return;
|
|
44
|
+
const choice = selectedEffort ?? await chooseEffort(ctx, `${provider}/${modelId}`, model, piPreference(user, project).effort);
|
|
45
|
+
if (choice === undefined) return false;
|
|
35
46
|
const effort = choice === "automatic" ? undefined : choice;
|
|
36
47
|
const preview = structuredClone(scope === "user" ? user : project);
|
|
37
48
|
setPiDefault(preview, provider, modelId, effort);
|
|
38
49
|
const effective = piPreference(scope === "user" ? preview : user, scope === "project" ? preview : project);
|
|
39
50
|
const shadowed = effective.provider !== provider || effective.model !== modelId || (effort !== undefined && effective.effort !== effort);
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
const path = settingsPath(ctx.cwd, scope);
|
|
52
|
+
const key = `${provider}/${modelId}`;
|
|
53
|
+
const overridePath = settingsPath(ctx.cwd, effective.provider !== provider || effective.model !== modelId || project.modelThinkingLevels?.[key] !== undefined || (scope === "user" && project.defaultThinkingLevel !== undefined) ? "project" : "user");
|
|
54
|
+
const summary = `${provider}/${modelId}, effort ${choice}\nScope: ${scope}\nSave to: ${path}`;
|
|
55
|
+
if (shadowed) {
|
|
56
|
+
const message = `${summary}\nHigher-priority settings in ${overridePath} still apply at startup.\nSave this preference and use your selection in this session anyway?`;
|
|
57
|
+
if (!(await confirmDefault(ctx, "Higher-priority settings", message, "Save and use anyway"))) return false;
|
|
58
|
+
} else if (selectedEffort === undefined && !(await confirmDefault(ctx, "Save and use Pi default?", summary, "Save and use"))) return false;
|
|
42
59
|
// Parse before changing session state. A malformed file must never be overwritten.
|
|
43
60
|
readSettings(settingsPath(ctx.cwd, scope));
|
|
44
61
|
const previousModel = ctx.model;
|
|
45
62
|
const previousEffort = pi.getThinkingLevel();
|
|
46
63
|
if (!(await pi.setModel(model))) {
|
|
47
64
|
ctx.ui.notify("Could not switch model. Defaults were not changed.", "warning");
|
|
48
|
-
return;
|
|
65
|
+
return false;
|
|
49
66
|
}
|
|
50
67
|
const requested = effort ?? effective.effort ?? "medium";
|
|
51
68
|
try {
|
|
52
69
|
pi.setThinkingLevel(requested);
|
|
53
70
|
const applied = pi.getThinkingLevel();
|
|
54
|
-
if (effort !== undefined && applied !== effort && !(await ctx
|
|
71
|
+
if (effort !== undefined && applied !== effort && !(await confirmDefault(ctx, "Pi adjusted the effort", `Requested ${effort}; Pi uses ${applied}. Save ${applied} instead?`, `Save ${applied}`))) {
|
|
55
72
|
if (previousModel) await pi.setModel(previousModel);
|
|
56
73
|
pi.setThinkingLevel(previousEffort);
|
|
57
|
-
return;
|
|
74
|
+
return false;
|
|
58
75
|
}
|
|
59
76
|
patchSettings(settingsPath(ctx.cwd, scope), data => setPiDefault(data, provider, modelId, effort === undefined ? undefined : applied));
|
|
60
|
-
ctx.ui.notify(`Using ${provider}/${modelId}, effort ${applied}. Saved ${scope} Pi default.${shadowed ?
|
|
77
|
+
ctx.ui.notify(`Using ${provider}/${modelId}, effort ${applied}. Saved ${scope} Pi default to ${path}.${shadowed ? ` Startup still uses ${effective.provider}/${effective.model}, effort ${effective.effort ?? "automatic"}, due to ${overridePath}. Edit the overriding preference to change startup behavior.` : ""}`, "info");
|
|
78
|
+
return true;
|
|
61
79
|
} catch (error) {
|
|
62
80
|
if (previousModel) await pi.setModel(previousModel);
|
|
63
81
|
pi.setThinkingLevel(previousEffort);
|
|
@@ -73,11 +91,53 @@ function display(value: unknown): string {
|
|
|
73
91
|
async function assignModel(ctx: any, config: ModelsConfig, title: string): Promise<{ provider: string; modelId: string } | undefined> {
|
|
74
92
|
const rows = rankModelsForDefaults(config, ctx);
|
|
75
93
|
if (!rows.length) { ctx.ui.notify("No models available to assign.", "warning"); return; }
|
|
76
|
-
const
|
|
77
|
-
|
|
94
|
+
const value = await tableSelect(ctx, { title, search: true, columns: [{ key: "model", label: "MODEL" }, { key: "provider", label: "PROVIDER" }], rows: rows.map((row, index) => ({ value: String(index), label: row.label, cells: { model: row.modelId, provider: row.provider } })) });
|
|
95
|
+
const row = value === undefined ? undefined : rows[Number(value)];
|
|
96
|
+
return row ? { provider: row.provider, modelId: row.modelId } : undefined;
|
|
97
|
+
}
|
|
98
|
+
async function editPiDefault(pi: any, ctx: any, config: ModelsConfig, scope: Scope, user: JsonObject, project: JsonObject): Promise<void> {
|
|
99
|
+
const current = piPreference(user, project);
|
|
100
|
+
let provider = current.provider, modelId = current.model;
|
|
101
|
+
let effort: Effort | "automatic" = current.effort ?? "automatic";
|
|
102
|
+
let reset = false;
|
|
103
|
+
const path = settingsPath(ctx.cwd, scope);
|
|
104
|
+
await editForm(ctx, {
|
|
105
|
+
title: "Pi session default",
|
|
106
|
+
hint: `Scope: ${scope}\n${path}\nSave switches the session after validating the model. Higher-priority settings still apply at startup.`,
|
|
107
|
+
fields: [
|
|
108
|
+
{ key: "model", label: "Model", value: `${provider ?? "auto"}/${modelId ?? "auto"}`, edit: async () => {
|
|
109
|
+
const action = await tableSelect(ctx, { title: "Pi default model", rows: [{ value: "choose", label: "Choose model" }, { value: "reset", label: "Remove scope defaults", description: "Remove this scope's default model and global effort. Keep other per-model preferences." }] });
|
|
110
|
+
if (action === undefined) return;
|
|
111
|
+
if (action === "reset") {
|
|
112
|
+
if (!(await confirmAction(ctx, { title: "Reset Pi defaults?", lines: [`Scope: ${scope}`, path, "Remove defaultProvider, defaultModel and defaultThinkingLevel only. Session stays unchanged."], confirmLabel: "Reset scope defaults" }))) return;
|
|
113
|
+
reset = true; return "remove scope defaults";
|
|
114
|
+
}
|
|
115
|
+
const selected = await assignModel(ctx, config, "Choose Pi default model");
|
|
116
|
+
if (!selected) return;
|
|
117
|
+
provider = selected.provider; modelId = selected.modelId; reset = false;
|
|
118
|
+
return `${provider}/${modelId}`;
|
|
119
|
+
} },
|
|
120
|
+
{ key: "effort", label: "Effort", value: effort, edit: async () => {
|
|
121
|
+
if (!provider || !modelId || reset) { ctx.ui.notify("Choose a model first.", "warning"); return; }
|
|
122
|
+
const selected = await chooseEffort(ctx, `${provider}/${modelId}`, ctx.modelRegistry.find(provider, modelId), effort);
|
|
123
|
+
if (selected === undefined) return;
|
|
124
|
+
effort = selected; return selected;
|
|
125
|
+
} },
|
|
126
|
+
],
|
|
127
|
+
onSave: async () => {
|
|
128
|
+
if (scope === "project" && !trusted(pi, ctx)) throw new Error("Project is not trusted");
|
|
129
|
+
if (reset) {
|
|
130
|
+
patchSettings(path, data => { delete data.defaultProvider; delete data.defaultModel; delete data.defaultThinkingLevel; });
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (!provider || !modelId) { ctx.ui.notify("Choose a model first.", "warning"); return false; }
|
|
134
|
+
return applyMainDefault(pi, ctx, provider, modelId, scope, effort);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
78
137
|
}
|
|
79
138
|
export async function defaultsFlow(pi: any, ctx: any, config: ModelsConfig): Promise<void> {
|
|
80
139
|
let scope: Scope = "user";
|
|
140
|
+
const state = {};
|
|
81
141
|
for (;;) {
|
|
82
142
|
const discovery = await discoverRoles(ctx.cwd, pi, ctx.model?.provider, trusted(pi, ctx));
|
|
83
143
|
const user = readSettings(settingsPath(ctx.cwd, "user"));
|
|
@@ -88,28 +148,33 @@ export async function defaultsFlow(pi: any, ctx: any, config: ModelsConfig): Pro
|
|
|
88
148
|
const roles = discovery.roles;
|
|
89
149
|
const modelFallback = roleField(agentUser, agentProject, ctx.model?.provider, undefined, "model");
|
|
90
150
|
const thinkingFallback = roleField(agentUser, agentProject, ctx.model?.provider, undefined, "thinking");
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
151
|
+
const selected = await tableSelect(ctx, {
|
|
152
|
+
title: "BMS defaults",
|
|
153
|
+
search: false,
|
|
154
|
+
state,
|
|
155
|
+
hint: `Scope: ${scope}. Saved settings, not running children. Agent changes require /reload.\nPi: ${settingsPath(ctx.cwd, scope)}\nAgents: ${settingsPath(discovery.projectRoot, scope)}`,
|
|
156
|
+
columns: [{ key: "target", label: "TARGET" }, { key: "model", label: "MODEL" }, { key: "effort", label: "EFFORT" }, { key: "source", label: "SOURCE" }],
|
|
157
|
+
rows: [
|
|
158
|
+
{ value: "0", label: `Scope: ${scope}`, cells: { target: "Scope", model: scope, effort: "", source: "Select to change" } },
|
|
159
|
+
{ value: "1", label: "Pi session", cells: { target: "Pi session", model: `${main.provider ?? "auto"}/${main.model ?? "auto"}`, effort: main.effort ?? "automatic", source: main.source } },
|
|
160
|
+
{ value: "2", label: "Subagent fallback", cells: { target: "Subagent fallback", model: display(modelFallback.value), effort: display(thinkingFallback.value), source: `${modelFallback.source}; ${thinkingFallback.source}` } },
|
|
161
|
+
...roles.map((role, index) => {
|
|
162
|
+
const model = roleField(agentUser, agentProject, ctx.model?.provider, role.name, "model");
|
|
163
|
+
const effort = roleField(agentUser, agentProject, ctx.model?.provider, role.name, "thinking");
|
|
164
|
+
return { value: String(index + 3), label: role.name, cells: { target: role.name, model: display(model.value ?? role.model), effort: display(effort.value ?? role.thinking), source: role.runner ? "External runner" : `${model.source}; ${effort.source}` } };
|
|
165
|
+
}),
|
|
166
|
+
...(discovery.warning ? [{ value: "details", label: "Agent discovery details" }] : []),
|
|
167
|
+
{ value: "done", label: "Done" },
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
if (selected === undefined || selected === "done") return;
|
|
171
|
+
const index = Number(selected);
|
|
106
172
|
if (index === 0) {
|
|
107
173
|
if (!trusted(pi, ctx)) { ctx.ui.notify("Project defaults require a trusted project.", "warning"); continue; }
|
|
108
174
|
scope = scope === "user" ? "project" : "user";
|
|
109
175
|
} else if (index === 1) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
} else if (selected === "Agent discovery details") {
|
|
176
|
+
await editPiDefault(pi, ctx, config, scope, user, project);
|
|
177
|
+
} else if (selected === "details") {
|
|
113
178
|
ctx.ui.notify(discovery.warning!, "warning");
|
|
114
179
|
} else {
|
|
115
180
|
const role = index === 2 ? undefined : roles[index - 3];
|
|
@@ -124,26 +189,55 @@ async function editRole(pi: any, ctx: any, config: ModelsConfig, scope: Scope, r
|
|
|
124
189
|
ctx.ui.notify("External runner defaults are not editable with the Pi registry picker. Use the runner's own configuration.", "warning");
|
|
125
190
|
return;
|
|
126
191
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
let
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
192
|
+
const local = scope === "user" ? user : project;
|
|
193
|
+
const entry = role ? local.subagents?.agentOverrides?.[role.name] : undefined;
|
|
194
|
+
let model: string | undefined = role ? entry?.model : local.subagents?.defaultModel;
|
|
195
|
+
let effort: string | false | undefined = role ? entry?.thinking : local.subagents?.defaultThinking;
|
|
196
|
+
let modelChanged = false, effortChanged = false;
|
|
197
|
+
await editForm(ctx, {
|
|
198
|
+
title: `${title} defaults`,
|
|
199
|
+
hint: `Scope: ${scope}\n${path}\nProvider-specific and project overrides still take precedence. Removing effort restores agent-defined/fallback behavior, not parent inheritance.`,
|
|
200
|
+
fields: [
|
|
201
|
+
{ key: "model", label: "Model", value: model ?? "remove override", edit: async () => {
|
|
202
|
+
const action = await tableSelect(ctx, { title: `Model for ${title}`, rows: [
|
|
203
|
+
{ value: "choose", label: "Choose model" },
|
|
204
|
+
{ value: "inherit", label: "Inherit parent model", description: "Explicitly use the parent session model." },
|
|
205
|
+
{ value: "remove", label: "Remove model override", description: `Remove only the ${scope} ${title} model override.` },
|
|
206
|
+
] });
|
|
207
|
+
if (action === undefined) return;
|
|
208
|
+
if (action === "choose") {
|
|
209
|
+
const selected = await assignModel(ctx, config, `Choose model for ${title}`);
|
|
210
|
+
if (!selected) return;
|
|
211
|
+
model = `${selected.provider}/${selected.modelId}`;
|
|
212
|
+
} else if (action === "inherit") model = "inherit";
|
|
213
|
+
else {
|
|
214
|
+
if (!(await confirmAction(ctx, { title: "Remove model override?", lines: [`Target: ${title}`, `Scope: ${scope}`, path], confirmLabel: "Remove override" }))) return;
|
|
215
|
+
model = undefined;
|
|
216
|
+
}
|
|
217
|
+
modelChanged = true;
|
|
218
|
+
return model ?? "remove override";
|
|
219
|
+
} },
|
|
220
|
+
{ key: "effort", label: "Effort", value: display(effort), edit: async () => {
|
|
221
|
+
const effective = modelChanged ? model : roleField(user, project, ctx.model?.provider, role?.name, "model").value ?? role?.model;
|
|
222
|
+
const ref = typeof effective === "string" && effective !== "inherit" ? effective.replace(/:(off|minimal|low|medium|high|xhigh|max)$/, "") : `${ctx.model?.provider}/${ctx.model?.id}`;
|
|
223
|
+
const split = ref.indexOf("/");
|
|
224
|
+
const selected = await chooseEffort(ctx, ref, split > 0 ? ctx.modelRegistry.find(ref.slice(0, split), ref.slice(split + 1)) : undefined, effort);
|
|
225
|
+
if (selected === undefined) return;
|
|
226
|
+
if (selected === "automatic" && !(await confirmAction(ctx, { title: "Remove effort override?", lines: [`Target: ${title}`, `Scope: ${scope}`, path, "Restores agent-defined/fallback behavior."], confirmLabel: "Remove override" }))) return;
|
|
227
|
+
effort = selected === "automatic" ? undefined : selected;
|
|
228
|
+
effortChanged = true;
|
|
229
|
+
return effort ?? "remove override";
|
|
230
|
+
} },
|
|
231
|
+
],
|
|
232
|
+
onSave: async () => {
|
|
233
|
+
if (scope === "project" && !trusted(pi, ctx)) throw new Error("Project is not trusted");
|
|
234
|
+
if (!modelChanged && !effortChanged) return true;
|
|
235
|
+
patchSettings(path, data => {
|
|
236
|
+
if (modelChanged) setRoleField(data, role?.name, "model", model);
|
|
237
|
+
if (effortChanged) setRoleField(data, role?.name, "thinking", effort);
|
|
238
|
+
});
|
|
239
|
+
ctx.ui.notify(`Saved ${title}. Run /reload to refresh subagent defaults.`, "info");
|
|
240
|
+
return true;
|
|
241
|
+
},
|
|
242
|
+
});
|
|
149
243
|
}
|