pi-better-model-select 0.1.3 → 0.2.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 CHANGED
@@ -1,12 +1,6 @@
1
1
  # pi-better-model-select
2
2
 
3
- A searchable, usage-ranked model picker for [pi](https://pi.dev).
4
-
5
- `/bms` opens the effective model list from `models.json` plus the optional
6
- `models.local.json` overlay, sorted by how often you pick each model. Start
7
- typing to filter; press enter to switch the session to the highlighted model.
8
- Add, update and delete live on ctrl-chords so plain keystrokes always go to the
9
- search box.
3
+ A searchable model picker for [Pi](https://pi.dev). Switch models, edit model definitions, and save default models and thinking effort. Models you pick most often appear first.
10
4
 
11
5
  ## Install
12
6
 
@@ -14,108 +8,82 @@ search box.
14
8
  pi install npm:pi-better-model-select
15
9
  ```
16
10
 
17
- If you installed the package under its old name, migrate with:
11
+ Run `/bms`, type to filter by name, model ID, or provider, then press Enter to switch models for the current session.
18
12
 
19
- ```bash
20
- pi remove npm:@msenebald/better-model-select
21
- pi install npm:pi-better-model-select
22
- ```
13
+ ## Commands and shortcuts
14
+
15
+ | Command | Action |
16
+ | --- | --- |
17
+ | `/bms` | Open the model picker |
18
+ | `/bms-add` | Add a model |
19
+ | `/bms-defaults` | Configure Pi and subagent defaults |
23
20
 
24
- ## Commands
21
+ Inside `/bms`:
25
22
 
26
- | Command | What it does |
27
- | ---------- | --------------------------------------------------- |
28
- | `/bms` | Open the searchable, usage-ranked picker |
29
- | `/bms-add` | Jump straight to the add-a-model flow |
23
+ | Key | Action |
24
+ | --- | --- |
25
+ | ↑ / ↓ | Move through models |
26
+ | Enter | Use the selected model for this session |
27
+ | Ctrl+D | Choose effort, save as the Pi default, and use it now |
28
+ | Ctrl+G | Open the defaults menu |
29
+ | Ctrl+A / Ctrl+U / Ctrl+X | Add / update / delete a model |
30
+ | Ctrl+O | Set write mode and provider filters |
31
+ | Ctrl+R | Reset usage ranking |
32
+ | Esc | Close |
30
33
 
31
- ## Keys
34
+ The picker marks the active model with `[current]` and the saved startup model with `[default]`. Only Enter selections count toward usage ranking.
32
35
 
33
- | Key | Action |
34
- | ---------- | --------------------------------------------------- |
35
- | `↑` `↓` | Move through the list |
36
- | *type* | Fuzzy filter by display name, model id or provider |
37
- | `enter` | Load the highlighted model |
38
- | `ctrl+a` | Add a model |
39
- | `ctrl+u` | Update the highlighted model |
40
- | `ctrl+x` | Delete the highlighted model |
41
- | `ctrl+r` | Reset usage statistics |
42
- | `esc` | Close |
36
+ ## Default models and effort
43
37
 
44
- ## Ranking
38
+ New in BMS `0.2.0`: save a model and thinking effort for Pi, a subagent fallback, or an individual agent role.
45
39
 
46
- Every model loaded through `/bms` increments a counter in
47
- `<agent-dir>/bms-usage.json`. The list sorts by count, then by most recent use,
48
- then alphabetically — so an unused config starts in plain alphabetical order and
49
- drifts toward your habits.
40
+ - Use Ctrl+D in `/bms` for the main Pi default.
41
+ - Use `/bms-defaults` for all defaults. The menu starts in user scope; select the scope row to switch to a trusted project's settings.
42
+ - Agent model and effort overrides are independent. You can inherit the parent model while choosing a different effort.
50
43
 
51
- Only `/bms` selections count. Switching via `/model`, `ctrl+p` cycling, or a
52
- session restore does not, which keeps the ranking a record of deliberate choices
53
- rather than of how often you restart pi.
44
+ Removing an override restores the agent's own definition and fallback rules. Effort `off` is explicit, not inheritance. The menu shows saved settings and their sources, not running-child state.
54
45
 
55
- Stats for models that disappear from the effective configuration are pruned
56
- when the picker opens. `ctrl+r` clears everything.
46
+ Run `/reload` after changing agent defaults or per-model effort preferences. Existing children stay unchanged. Test Pi startup defaults in a fresh session, since resumed sessions restore their recorded model and effort.
57
47
 
58
- ## Metadata discovery
48
+ ### Optional pi-subagents integration
59
49
 
60
- When adding or updating, the extension tries to fill in context window, output
61
- cap, reasoning support and input modalities from, in order:
50
+ [pi-subagents](https://github.com/nicobailon/pi-subagents) is a separate extension. Install it only if you want subagents:
62
51
 
63
- 1. `GET {baseUrl}/model/info` — LiteLLM proxies
64
- 2. `GET {baseUrl}/models/{id}` — OpenAI-style, plus proxy-specific fields
65
- 3. The public LiteLLM model cost map on GitHub
52
+ ```bash
53
+ pi install npm:pi-subagents
54
+ ```
66
55
 
67
- Each step is best-effort. A plain OpenAI-compatible endpoint usually yields
68
- nothing from steps 1 and 2, and the flow falls back to name-based heuristics
69
- with editable defaults. Before a write, BMS asks you to choose
70
- `models.local.json` or `models.json`. The local file is listed first and acts as
71
- a personal BMS overlay. The selected file is re-read immediately before the
72
- write so a concurrent edit in another pi session is not clobbered.
56
+ 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.
73
57
 
74
- ## Paths
58
+ Discovery has now been checked with **pi-subagents `0.82.1`**, updated from the original `0.79.0` test. BMS handles the newer bundled entry-point layout. The adapter still uses internal modules, so future package changes may require an update. If discovery fails, **Agent discovery details** shows the error. Pi defaults work without this integration.
75
59
 
76
- All files resolve through pi's `getAgentDir()`, so `PI_CODING_AGENT_DIR` and
77
- rebranded distributions work without special handling:
60
+ ### Supported effort levels
78
61
 
79
- - `<agent-dir>/models.json` is Pi's documented compatible-endpoint configuration.
80
- - `<agent-dir>/models.local.json` is an optional personal overlay managed by BMS.
81
- - `<agent-dir>/bms-usage.json` stores selection counters.
62
+ BMS uses provider-reported effort lists when available and Pi's capability restrictions where supplied. Otherwise it offers Pi's standard levels, marked as provider support unverified:
82
63
 
83
- Pi does not currently document or load `models.local.json` itself. BMS merges it
84
- over `models.json` by provider and model id, then registers the merged providers
85
- through Pi's extension API at session start and whenever `/bms` opens. If the
86
- same model exists in both files, fields from `models.local.json` win.
64
+ ```text
65
+ off · minimal · low · medium · high · xhigh · max
66
+ ```
87
67
 
88
- Deleting a model that exists only in `models.json` leaves that file untouched.
89
- BMS writes `{ "id": "...", "bmsDeleted": true }` to the local overlay instead.
90
- This BMS-owned tombstone keeps generated or externally managed model entries
91
- hidden across restarts. Delete the tombstone from `models.local.json` to restore
92
- the inherited model.
68
+ Add/update also lets you edit supported levels and set a per-model preferred effort. These hints do not change Pi's provider adapters. If Pi clamps an effort, BMS asks before saving the applied level. No paid model probes are made.
93
69
 
94
- Writes are atomic using a temporary file and rename, so an interrupted write
95
- cannot truncate either configuration file.
70
+ ## Where changes are saved
96
71
 
97
- ## Development
72
+ Ctrl+O selects the model write mode:
98
73
 
99
- ```bash
100
- npm install
101
- npm run typecheck
102
- node --experimental-strip-types --test extensions/lib/picker.test.ts
103
- pi -e . -p "say OK" # load the extension without installing it
104
- ```
74
+ | Mode | Behavior |
75
+ | --- | --- |
76
+ | Overlay, the default | Save model edits in `models.bms.json`; leave `models.json` unchanged |
77
+ | Direct | Write model edits to Pi's `models.json` |
105
78
 
106
- ## Publishing
79
+ Defaults always go into native Pi `settings.json`, regardless of write mode. User files normally live in `~/.pi/agent`; project defaults use `.pi/settings.json`. BMS respects `PI_CODING_AGENT_DIR` and shows the selected settings paths.
107
80
 
108
- Set the new version in `package.json` and `package-lock.json`, then run:
81
+ Provider filters hide entries only in `/bms`, not Pi's `/model` picker. Default-assignment menus also include authenticated Pi registry models.
109
82
 
110
- ```bash
111
- make publish
112
- ```
83
+ ## Further documentation
113
84
 
114
- The target runs the full package gate, checks npm authentication, opens npm's
115
- browser login when needed, skips the upload if that exact version already
116
- exists, publishes the public scoped package, and waits for npm to expose the
117
- new version. npm can take several minutes to process a package. If verification
118
- times out, rerun `make verify-publish`; do not publish the same version again.
85
+ - [Configuration reference](https://g.han.zone/msenebald/pi-better-model-select/src/branch/main/docs/configuration.md): file paths, precedence, overlays, metadata, and integration limits.
86
+ - [Development and testing](https://g.han.zone/msenebald/pi-better-model-select/src/branch/main/docs/development.md): local loading, isolated tests, checks, and publishing.
119
87
 
120
88
  ## License
121
89
 
@@ -0,0 +1,105 @@
1
+ # Configuration reference
2
+
3
+ See the [README](../README.md) for installation and shortcuts.
4
+
5
+ ## Files
6
+
7
+ `<agent-dir>` is normally `~/.pi/agent`. BMS uses Pi's `getAgentDir()`, so it respects `PI_CODING_AGENT_DIR`.
8
+
9
+ | File | Contents |
10
+ | --- | --- |
11
+ | `<agent-dir>/models.json` | Pi model definitions |
12
+ | `<agent-dir>/models.bms.json` | BMS model overlay |
13
+ | `<agent-dir>/bms-settings.json` | Write mode and provider filters |
14
+ | `<agent-dir>/bms-usage.json` | Selection counts and recency |
15
+ | `<agent-dir>/bms-model-metadata.json` | Effort capability hints and their source |
16
+ | `<agent-dir>/settings.json` | User defaults and per-model effort preferences |
17
+ | `<project>/.pi/settings.json` | Project defaults |
18
+
19
+ ## Model write modes
20
+
21
+ Choose the write mode through Ctrl+O in `/bms`.
22
+
23
+ - Overlay mode, the default, leaves `models.json` unchanged and writes edits to `models.bms.json`.
24
+ - Direct mode writes add, update, and delete operations to `models.json`.
25
+
26
+ Pi does not load the overlay itself. BMS merges it over `models.json` by provider and model ID, then registers the merged providers through Pi's extension API. Overlay fields win for duplicate models. BMS automatically renames its old `models.local.json` overlay to `models.bms.json`.
27
+
28
+ Deleting an inherited model in overlay mode writes a tombstone:
29
+
30
+ ```json
31
+ { "id": "model-id", "bmsDeleted": true }
32
+ ```
33
+
34
+ Remove that entry from `models.bms.json` to restore the inherited model. Model files are re-read before writes and saved through a temporary file and rename. This does not provide a shared lock against other applications.
35
+
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
+
38
+ ## Defaults and precedence
39
+
40
+ Default settings are independent of the model write mode.
41
+
42
+ | Setting | Native Pi settings field |
43
+ | --- | --- |
44
+ | Pi model | `defaultProvider`, `defaultModel` |
45
+ | Pi effort | `defaultThinkingLevel` |
46
+ | Per-model preferred effort | `modelThinkingLevels["provider/model"]` |
47
+ | Subagent fallback | `subagents.defaultModel`, `subagents.defaultThinking` |
48
+ | Role override | `subagents.agentOverrides.<name>.model` and `.thinking` |
49
+
50
+ The defaults menu starts in user scope. Project scope requires a trusted project. Pi defaults use the working directory's `.pi/settings.json`. Agent defaults use the project root resolved by `pi-subagents`, which may differ in nested repositories. The menu shows the paths.
51
+
52
+ Project settings and provider-specific role overrides may take precedence. BMS shows override sources and warns when a saved Pi default would be shadowed. Saving a Pi default removes the selected model's per-model effort preference in the same scope, so it cannot override the new default.
53
+
54
+ For agent roles:
55
+
56
+ - **Inherit parent model** writes `model: "inherit"`.
57
+ - **Remove model override** restores agent-definition and fallback rules.
58
+ - **Automatic** or **Remove effort override** removes that scope's effort preference. It does not force parent effort inheritance. `pi-subagents` has no separate persistent setting for that.
59
+ - **Off** is an explicit effort level, not inheritance.
60
+
61
+ Changing the Pi default also switches the main session. Subagent assignments do not switch it or increment usage counts. Cancelled or failed model switches leave saved defaults unchanged. If Pi clamps an effort, BMS asks before saving the applied level.
62
+
63
+ Run `/reload` after changing agent defaults or per-model effort preferences. Running children keep their existing configuration, and subagent model-scope policies still apply at launch. Resumed Pi sessions restore their recorded model and effort. Test startup defaults in a fresh session without CLI model or effort overrides.
64
+
65
+ Settings writes preserve unrelated fields, reject malformed JSON, and use atomic rename. A `.bms-lock` file serializes BMS settings writers only. After a crash, remove a stale lock only after checking that no BMS writer is active. Updates across model definitions, settings, and capability metadata are not a multi-file transaction.
66
+
67
+ ## Effort levels and metadata
68
+
69
+ BMS uses explicit provider effort lists when supplied, then native Pi capability restrictions where available. Otherwise it offers Pi's standard levels and labels provider support as unverified:
70
+
71
+ ```text
72
+ off, minimal, low, medium, high, xhigh, max
73
+ ```
74
+
75
+ The metadata adapter recognizes `supported_reasoning_efforts`, `supported_thinking_levels`, and `reasoning_efforts` containing Pi effort names. A generic reasoning flag or `supported_parameters: ["reasoning_effort"]` does not specify supported levels.
76
+
77
+ Add/update lets you edit the available levels and choose an optional per-model preferred effort. Non-reasoning models offer only `off`; correct the reasoning flag first if the endpoint metadata is wrong. BMS makes no paid model probes.
78
+
79
+ Capability hints guide menus. They do not change provider adapters or prove an endpoint honors a level. Pi may clamp effort or map it to a token budget. Provider errors remain visible rather than triggering a model fallback.
80
+
81
+ BMS blocks capability edits that conflict with directly pinned defaults in the inspected user/project settings. Update those defaults first. This check does not audit every inherited agent definition or settings file elsewhere on disk.
82
+
83
+ When adding or updating models, BMS tries these metadata sources in order:
84
+
85
+ 1. `GET {baseUrl}/model/info` for LiteLLM proxies.
86
+ 2. `GET {baseUrl}/models/{id}` for OpenAI-style and proxy-specific fields.
87
+ 3. The public LiteLLM model cost map on GitHub.
88
+
89
+ These requests are best-effort. Missing metadata falls back to name-based heuristics and editable defaults for context window, output limit, reasoning support, and input types.
90
+
91
+ ## Optional pi-subagents integration
92
+
93
+ [pi-subagents](https://github.com/nicobailon/pi-subagents) is a separate Pi extension. BMS configures its saved model and effort defaults; it does not install it or launch agents to discover roles.
94
+
95
+ Discovery was originally tested with `0.79.0` and has now been checked against `0.82.1`. BMS supports both the older root entry point and the newer `bundle/index.js` package layout. Discovery still uses internal discovery/runtime modules because the public agents API does not expose role discovery. Future incompatible versions may need an adapter update.
96
+
97
+ The package must be resolvable from BMS or `<agent-dir>/npm/node_modules`. Discovery includes built-in, package, user, project, and runtime roles when supported by the installed API. Untrusted project agents and settings are excluded. External runner roles are shown read-only; configure their models through the runner's own settings.
98
+
99
+ If discovery fails, the menu offers **Agent discovery details** with the error. Main Pi defaults remain usable.
100
+
101
+ ## Usage ranking
102
+
103
+ Only models loaded with Enter in `/bms` count toward ranking. BMS sorts by selection count, then recency, then alphabetically. Default assignments, `/model`, model cycling, and session restores do not count.
104
+
105
+ BMS prunes statistics for models missing from the effective configuration when the picker opens. Ctrl+R clears all usage statistics.
@@ -0,0 +1,104 @@
1
+ # Development and testing
2
+
3
+ Run these commands from the repository checkout:
4
+
5
+ ```bash
6
+ npm ci
7
+ npm run gate
8
+ npm run test:smoke # optional: installed Pi, Python 3 and a POSIX PTY required
9
+ pi --no-extensions -e ./extensions/bms.ts # temporary load; no install or publish
10
+ ```
11
+
12
+ ## Temporary local testing
13
+
14
+ Use a fresh terminal in this checkout. The command above disables installed
15
+ extensions to avoid loading two copies of BMS. It still uses your real Pi settings,
16
+ so default changes persist. For an isolated test, use a scratch agent directory
17
+ and project directory instead:
18
+
19
+ ```bash
20
+ REPO="$PWD"
21
+ SANDBOX="$(mktemp -d -t bms-local)"
22
+ mkdir -p "$SANDBOX/agent" "$SANDBOX/project/.pi"
23
+ printf '{}\n' > "$SANDBOX/project/.pi/settings.json"
24
+ # Optional: copy your model definitions. Review literal credentials in this file.
25
+ cp "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}/models.json" "$SANDBOX/agent/models.json"
26
+ cd "$SANDBOX/project"
27
+ PI_CODING_AGENT_DIR="$SANDBOX/agent" pi --no-extensions -e "$REPO/extensions/bms.ts"
28
+ ```
29
+
30
+ Provider API-key environment variables remain available. The isolated directory
31
+ has no login credentials; use `/login` there if needed. Do not commit credentials
32
+ or scratch settings. To test agent roles, load your installed pi-subagents entry
33
+ explicitly with another `-e` and make the package resolvable in the scratch agent's
34
+ `npm/node_modules`, or use your real agent directory with backups. For the standard
35
+ Pi npm install layout with pi-subagents 0.82.1, before launching:
36
+
37
+ ```bash
38
+ REAL_AGENT="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
39
+ ln -s "$REAL_AGENT/npm" "$SANDBOX/agent/npm"
40
+ PI_CODING_AGENT_DIR="$SANDBOX/agent" pi --no-extensions \
41
+ -e "$REPO/extensions/bms.ts" -e "$REAL_AGENT/npm/node_modules/pi-subagents/bundle/index.js"
42
+ ```
43
+
44
+ This symlinks installed package code, not your settings or auth files. Do not run
45
+ package installation/update commands in this sandbox, since they would modify the
46
+ shared package directory.
47
+
48
+ The automated `npm run test:smoke` uses an unreachable dummy endpoint and
49
+ throwaway settings. It checks command registration, the real TUI default-save
50
+ flow, the defaults shortcut, and restoration in a fresh Pi process. No model
51
+ prompts are sent. The regular test suite uses mocked provider metadata and UI.
52
+
53
+ Manual checks:
54
+
55
+ 1. `/bms`: filter and use Enter. Only the current session should change.
56
+ 2. Reopen `/bms`, press Ctrl+D, choose effort and confirm. Check both markers,
57
+ the footer effort, and the scratch `agent/settings.json`.
58
+ 3. Cancel the effort dialog and confirmation. Neither should change defaults.
59
+ 4. `/bms-defaults`: select a Pi model, then select the same model again to change
60
+ only its default effort. Test user and trusted-project scope.
61
+ 5. Configure a subagent fallback and a role model/effort. Confirm the parent
62
+ session stays unchanged. Test explicit model inheritance and removal of each
63
+ override, then `/reload` and inspect `/subagents-models reviewer`.
64
+ 6. Add/update a model. Check provider-reported effort lists, the unverified Pi
65
+ fallback, manual levels, and the preferred effort. Try removing a level that
66
+ a directly pinned default uses; BMS should block the conflicting edit.
67
+ 7. Exit and start a fresh Pi session with the same scratch paths. Confirm startup
68
+ model and effort. Do not use `--continue`, `--resume`, or CLI model/effort overrides.
69
+ 8. Test a narrow terminal and verify the list, wrapped hints, and search still work.
70
+
71
+ Quit Pi before deleting the sandbox with `rm -rf "$SANDBOX"`, then return to the
72
+ checkout with `cd "$REPO"`. The test commands do not publish or install BMS.
73
+
74
+ ## Publishing
75
+
76
+ Publish from `main` after the feature PR is reviewed and merged. Do not publish from the unmerged feature branch.
77
+
78
+ For the current `0.2.0` release:
79
+
80
+ ```bash
81
+ git switch main
82
+ git pull --ff-only origin main
83
+ npm ci
84
+ npm run gate
85
+ npm run test:smoke
86
+ npm pack --dry-run
87
+ ```
88
+
89
+ Check that the working tree is clean, both package manifests say `0.2.0`, and the package contains `README.md`, `docs/configuration.md`, and `docs/development.md`. Tests and the internal PR draft must not be included. The README links point to `main`, so merge the documentation before publishing.
90
+
91
+ Then publish to the public npm registry:
92
+
93
+ ```bash
94
+ make publish
95
+ ```
96
+
97
+ For later releases, update both manifests with `npm version <version> --no-git-tag-version`, and include that change in a reviewed PR before publishing.
98
+
99
+ The target runs the full package gate, checks npm authentication, opens npm's
100
+ browser login when needed, skips the upload if that exact version already
101
+ exists, publishes the public package, and waits for npm to expose the
102
+ new version. npm can take several minutes to process a package. If verification
103
+ times out, rerun `make verify-publish`; do not publish the same version again.
104
+
package/extensions/bms.ts CHANGED
@@ -4,15 +4,19 @@
4
4
  // with add / update / delete wired to ctrl-chords so plain typing always goes
5
5
  // to the search box.
6
6
 
7
- import { DynamicBorder, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
- import { Container, Text } from "@earendil-works/pi-tui";
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import { defaultsFlow, piPreference, setMainDefault } from "./lib/defaults.ts";
9
+ import { readSettings, settingsPath } from "./lib/defaults-store.ts";
10
+ import { pickerDialog } from "./lib/picker-dialog.ts";
9
11
  import { addFlow, deleteFlow, updateFlow, type FlowCtx } from "./lib/flows.ts";
10
- import { ModelPicker, rankModels, type PickerAction } from "./lib/picker.ts";
12
+ import { rankModels } from "./lib/picker.ts";
11
13
  import {
12
14
  loadConfig,
13
15
  loadConfigFile,
16
+ loadSettings,
14
17
  pruneUsage,
15
18
  recordUsage,
19
+ saveSettings,
16
20
  saveUsage,
17
21
  type ModelEntry,
18
22
  type ModelsConfig,
@@ -47,11 +51,13 @@ function providerConfig(
47
51
 
48
52
  /** Apply the personal BMS overlay as extension providers. */
49
53
  function syncLocalConfig(pi: ExtensionAPI, ctx: any): ModelsConfig | null {
50
- const local = loadConfigFile(ctx, "models.local.json", true);
54
+ const settings = loadSettings();
55
+ const overlay =
56
+ settings.writeMode === "overlay" ? loadConfigFile(ctx, "models.bms.json", true) : { providers: {} };
51
57
  const effective = loadConfig(ctx);
52
- if (!local || !effective) return null;
58
+ if (!overlay || !effective) return null;
53
59
 
54
- const next = new Set(Object.keys(local.providers ?? {}));
60
+ const next = new Set(Object.keys(overlay.providers ?? {}));
55
61
  for (const providerName of registeredLocalProviders) {
56
62
  if (!next.has(providerName)) pi.unregisterProvider(providerName);
57
63
  }
@@ -62,7 +68,7 @@ function syncLocalConfig(pi: ExtensionAPI, ctx: any): ModelsConfig | null {
62
68
  try {
63
69
  pi.registerProvider(providerName, providerConfig(ctx, providerName, provider) as any);
64
70
  } catch (e) {
65
- ctx.ui.notify(`Cannot apply ${providerName} from models.local.json: ${e}`, "error");
71
+ ctx.ui.notify(`Cannot apply ${providerName} from models.bms.json: ${e}`, "error");
66
72
  return null;
67
73
  }
68
74
  }
@@ -73,6 +79,52 @@ function syncLocalConfig(pi: ExtensionAPI, ctx: any): ModelsConfig | null {
73
79
  }
74
80
 
75
81
  /** Re-read model configuration from disk and switch the session to the given model. */
82
+ async function optionsFlow(ctx: any, config: ModelsConfig): Promise<void> {
83
+ for (;;) {
84
+ const settings = loadSettings();
85
+ const writeLabel =
86
+ settings.writeMode === "overlay" ? "BMS overlay (models.bms.json)" : "Pi config (models.json)";
87
+ const hidden = settings.disabledProviders.length;
88
+ const pick = await ctx.ui.select("BMS options", [
89
+ `Write mode: ${writeLabel}`,
90
+ `Provider filters: ${hidden} hidden`,
91
+ "Done",
92
+ ]);
93
+ if (!pick || pick === "Done") return;
94
+
95
+ if (pick.startsWith("Write mode:")) {
96
+ const mode = await ctx.ui.select("Where should BMS write model changes?", [
97
+ "BMS overlay (models.bms.json)",
98
+ "Pi config (models.json)",
99
+ ]);
100
+ if (!mode) continue;
101
+ settings.writeMode = mode.startsWith("BMS overlay") ? "overlay" : "direct";
102
+ saveSettings(settings);
103
+ ctx.ui.notify(`BMS write mode: ${settings.writeMode}.`, "info");
104
+ continue;
105
+ }
106
+
107
+ const providers = Object.keys(config.providers ?? {}).sort();
108
+ for (;;) {
109
+ const current = loadSettings();
110
+ const options = providers.map((provider) =>
111
+ `${current.disabledProviders.includes(provider) ? "[ ]" : "[x]"} ${provider}`,
112
+ );
113
+ const choice = await ctx.ui.select(
114
+ "Providers shown in /bms (select to toggle, ESC when done)",
115
+ options,
116
+ );
117
+ if (!choice) break;
118
+ const provider = choice.slice(4);
119
+ const disabled = new Set(current.disabledProviders);
120
+ if (disabled.has(provider)) disabled.delete(provider);
121
+ else disabled.add(provider);
122
+ current.disabledProviders = [...disabled].sort();
123
+ saveSettings(current);
124
+ }
125
+ }
126
+ }
127
+
76
128
  async function loadModel(
77
129
  pi: ExtensionAPI,
78
130
  ctx: any,
@@ -115,6 +167,23 @@ export default function (pi: ExtensionAPI) {
115
167
  syncLocalConfig(pi, ctx);
116
168
  });
117
169
 
170
+ const report = async (ctx: any, action: () => Promise<void>) => {
171
+ try { await action(); } catch (error) { ctx.ui.notify(`BMS: ${error}`, "error"); }
172
+ };
173
+ const addModel = async (ctx: any) => addFlow(ctx, async (provider, modelId) => {
174
+ if (syncLocalConfig(pi, ctx)) await report(ctx, () => setMainDefault(pi, ctx, provider, modelId));
175
+ });
176
+ pi.registerCommand("bms-defaults", {
177
+ description: "Configure Pi and subagent default models and effort levels",
178
+ handler: async (_args, ctx: any) => {
179
+ if (ctx.mode !== "tui") { ctx.ui.notify("/bms-defaults needs an interactive terminal.", "warning"); return; }
180
+ await report(ctx, async () => {
181
+ const config = syncLocalConfig(pi, ctx);
182
+ if (config) await defaultsFlow(pi, ctx, config);
183
+ });
184
+ },
185
+ });
186
+
118
187
  pi.registerCommand("bms", {
119
188
  description: "Better model select — searchable, usage-ranked model picker",
120
189
  handler: async (_args, ctx: any) => {
@@ -129,48 +198,52 @@ export default function (pi: ExtensionAPI) {
129
198
  if (!config) return;
130
199
  pruneUsage(config);
131
200
 
132
- const rows = rankModels(config);
201
+ const settings = loadSettings();
202
+ const rows = rankModels(config, settings.disabledProviders);
133
203
  if (rows.length === 0) {
204
+ const total = Object.values(config.providers ?? {}).reduce(
205
+ (sum, provider) => sum + (provider.models?.length ?? 0),
206
+ 0,
207
+ );
208
+ if (total > 0) {
209
+ const open = await ctx.ui.confirm(
210
+ "No models visible",
211
+ "All providers are hidden by BMS filters. Open options?",
212
+ );
213
+ if (!open) return;
214
+ await optionsFlow(ctx, config);
215
+ continue;
216
+ }
134
217
  const add = await ctx.ui.confirm(
135
218
  "No models configured",
136
219
  "The effective model configuration has no models. Add one now?",
137
220
  );
138
221
  if (!add) return;
139
- await addFlow(ctx as FlowCtx);
222
+ await report(ctx, () => addModel(ctx));
140
223
  continue;
141
224
  }
142
225
 
143
- const action: PickerAction | undefined = await ctx.ui.custom((tui: any, theme: any, _kb: any, done: any) => {
144
- const container = new Container();
145
- const accent = (s: string) =>
146
- typeof theme?.fg === "function" ? theme.fg("accent", s) : s;
147
-
148
- container.addChild(new DynamicBorder(accent));
149
- container.addChild(new Text(accent("Better Model Select"), 1, 0));
150
-
151
- const picker = new ModelPicker(
152
- rows,
153
- theme,
154
- () => tui.requestRender(),
155
- (a: PickerAction) => done(a),
156
- );
157
- picker.focused = true;
158
- container.addChild(picker);
159
- container.addChild(new DynamicBorder(accent));
160
-
161
- return {
162
- render: (w: number) => container.render(w),
163
- invalidate: () => container.invalidate(),
164
- handleInput: (data: string) => picker.handleInput(data),
165
- };
166
- });
226
+ try {
227
+ const preference = piPreference(readSettings(settingsPath(ctx.cwd, "user")), ctx.isProjectTrusted?.() ? readSettings(settingsPath(ctx.cwd, "project")) : {});
228
+ for (const row of rows) {
229
+ row.current = ctx.model?.provider === row.provider && ctx.model?.id === row.modelId;
230
+ row.default = preference.provider === row.provider && preference.model === row.modelId;
231
+ }
232
+ } catch (error) { ctx.ui.notify(`Cannot read defaults: ${error}`, "warning"); }
233
+ const action = await pickerDialog(ctx, rows);
167
234
 
168
235
  switch (action?.kind) {
169
236
  case "load":
170
237
  await loadModel(pi, ctx, action.provider, action.modelId);
171
238
  return;
239
+ case "set-default":
240
+ await report(ctx, () => setMainDefault(pi, ctx, action.provider, action.modelId));
241
+ break;
242
+ case "defaults":
243
+ await report(ctx, () => defaultsFlow(pi, ctx, config));
244
+ break;
172
245
  case "add":
173
- await addFlow(ctx as FlowCtx);
246
+ await report(ctx, () => addModel(ctx));
174
247
  break;
175
248
  case "update":
176
249
  await updateFlow(
@@ -188,6 +261,9 @@ export default function (pi: ExtensionAPI) {
188
261
  : undefined,
189
262
  );
190
263
  break;
264
+ case "options":
265
+ await optionsFlow(ctx, config);
266
+ break;
191
267
  case "reset": {
192
268
  const ok = await ctx.ui.confirm(
193
269
  "Reset usage statistics?",
@@ -208,7 +284,7 @@ export default function (pi: ExtensionAPI) {
208
284
 
209
285
  // Standalone add, kept for muscle memory and non-interactive convenience.
210
286
  pi.registerCommand("bms-add", {
211
- description: "Add a model from a provider's endpoint to a selected config file",
212
- handler: async (_args, ctx: any) => addFlow(ctx as FlowCtx),
287
+ description: "Add a model from a provider's endpoint using the configured write mode",
288
+ handler: async (_args, ctx: any) => report(ctx, () => addModel(ctx)),
213
289
  });
214
290
  }