@benvargas/pi-model-sort 1.0.0 → 1.0.2
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 +28 -0
- package/README.md +3 -1
- package/extensions/index.ts +26 -6
- package/extensions/sort.ts +30 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.2] - 2026-09-11
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Scoped Ctrl+P / Ctrl+Shift+P cycling no longer fails with "Cannot read
|
|
14
|
+
properties of undefined (reading 'persist')" on pi 0.84.3 and later. pi
|
|
15
|
+
0.84.3 added a second `options` parameter to `AgentSession._cycleScopedModel`
|
|
16
|
+
and reads `options.persist` once a next model is selected; the patch
|
|
17
|
+
forwarded only the direction, so every cycle that actually changed model (two
|
|
18
|
+
or more available scoped models) threw after pi had already swapped the model
|
|
19
|
+
(no `model_select`, so last-used and thinking-level memory were not updated).
|
|
20
|
+
A single-model scope was unaffected (pi returns "Only one model in scope"
|
|
21
|
+
before reading the options). The patch now forwards every argument
|
|
22
|
+
untouched. Validated against pi 0.85.1, including a regression test that
|
|
23
|
+
drives pi's real `AgentSession` through the patched cycle.
|
|
24
|
+
|
|
25
|
+
## [1.0.1] - 2026-09-02
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- An explicit `--model` on the command line now wins over the MRU startup
|
|
29
|
+
override. Previously a fresh start such as `pi --model provider/id` (or
|
|
30
|
+
bb's Pi provider, which launches `pi --mode rpc --model provider/id` and
|
|
31
|
+
aborts the thread with "Pi did not start with model ..." when pi reports a
|
|
32
|
+
different model back) was silently switched to the most recently used
|
|
33
|
+
model. The explicitly requested model is recorded as last-used on startup,
|
|
34
|
+
since pi sets it during construction without emitting `model_select`.
|
|
35
|
+
Detection inspects `process.argv` for `--model <value>` / `--model=value`;
|
|
36
|
+
`--models` (scope) is not treated as a selection.
|
|
37
|
+
|
|
10
38
|
## [1.0.0] - 2026-08-28
|
|
11
39
|
|
|
12
40
|
### Added
|
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ Sorts pi's model picker by last usage and starts fresh sessions on your most rec
|
|
|
5
5
|
- `/model` picker — both "Scope: all" and "Scope: scoped" views, including fuzzy-search results — is sorted by recency: current model first → most recently used → provider/id alphabetical
|
|
6
6
|
- Ctrl+P / Ctrl+Shift+P **scoped** cycling follows last-used order (scoped models come from `enabledModels` or `--models`)
|
|
7
7
|
- Fresh starts and `/new` begin on your most recently used model instead of `enabledModels[0]` or the hardcoded provider default
|
|
8
|
+
- An explicit `--model` on the command line always wins over MRU (so `pi --model provider/id`, scripts, and bb's Pi provider get the model they asked for); that model is recorded as last-used
|
|
8
9
|
- Continued sessions (`pi -c`, `--session`, `/resume`, forks) keep the model saved in the session file, and that restored model is recorded as last-used (fresh `/new` sessions and `/reload` are excluded from that recording)
|
|
9
10
|
- Remembers the thinking level you last used on each model and restores it on every switch, clamped to what each model supports
|
|
10
11
|
- No configuration needed — tracking starts on first use and degrades to the default alphabetical order with no history
|
|
@@ -31,6 +32,7 @@ The extension works automatically — there are no commands to learn.
|
|
|
31
32
|
/model # Most recently used models appear at the top
|
|
32
33
|
Ctrl+P / Ctrl+Shift+P # Cycle through scoped models in last-used order
|
|
33
34
|
pi # Fresh starts use MRU
|
|
35
|
+
pi --model provider/id # Explicit model wins over MRU
|
|
34
36
|
pi -c # Continuations keep the session's model
|
|
35
37
|
```
|
|
36
38
|
|
|
@@ -38,7 +40,7 @@ pi -c # Continuations keep the session's model
|
|
|
38
40
|
|
|
39
41
|
- Tracking uses pi's documented extension events: `/model` switches (`model_select`) and thinking-level changes (`thinking_level_select`) are timestamped into `~/.pi/agent/extensions/pi-model-sort.json`. Continued sessions restore their model during construction without emitting `model_select` (pi 0.84.3), so the extension records the restored model at `session_start` itself.
|
|
40
42
|
- Sorting has no SDK hook, so the extension wraps (monkey-patches) internal methods: `ModelSelectorComponent.sortModels`, its scoped loader and `filterModels`, and `AgentSession._cycleScopedModel` for scoped cycling. All original methods are preserved and restored on shutdown/reload; the patches survive `modelRegistry.refresh()`.
|
|
41
|
-
- The MRU startup override calls `pi.setModel()` on `session_start` for fresh starts and `/new` only. A continued session is detected by projecting its branch through pi's own context-message rules (`message`, `custom_message`, non-empty `branch_summary`, and `compaction` entries — exactly what `buildSessionContext()` counts) — pi seeds every new session with `model_change` + `thinking_level_change` entries before `session_start` fires, so raw branch length cannot distinguish fresh from continued.
|
|
43
|
+
- The MRU startup override calls `pi.setModel()` on `session_start` for fresh starts and `/new` only, and never when the process was launched with `--model` (pi exposes no "model came from the CLI" signal to extensions, so the extension inspects `process.argv`). A continued session is detected by projecting its branch through pi's own context-message rules (`message`, `custom_message`, non-empty `branch_summary`, and `compaction` entries — exactly what `buildSessionContext()` counts) — pi seeds every new session with `model_change` + `thinking_level_change` entries before `session_start` fires, so raw branch length cannot distinguish fresh from continued.
|
|
42
44
|
|
|
43
45
|
### Known limitations on pi 0.84.x
|
|
44
46
|
|
package/extensions/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
findMruModel,
|
|
46
46
|
handleModelSelect,
|
|
47
47
|
hasContextMessages,
|
|
48
|
+
hasExplicitModelArg,
|
|
48
49
|
type ModelSortConfig,
|
|
49
50
|
parseConfig,
|
|
50
51
|
recordThinkingSelect,
|
|
@@ -315,18 +316,24 @@ function unpatchRegistry(registry: PatchedRegistry): void {
|
|
|
315
316
|
// before cycling so Ctrl+P / Ctrl+Shift+P follows last-used order instead
|
|
316
317
|
// of the configured order. Non-destructive: the session's stored order is
|
|
317
318
|
// temporarily swapped and restored after the cycle lookup.
|
|
319
|
+
//
|
|
320
|
+
// Every argument is forwarded untouched: pi 0.84.3 added a second
|
|
321
|
+
// `options: ModelMutationOptions` parameter and reads `options.persist` once
|
|
322
|
+
// a next model is selected, so dropping it made every model-changing scoped
|
|
323
|
+
// cycle (two or more available scoped models) throw. A single-model scope
|
|
324
|
+
// returns early ("Only one model in scope") and never reached the crash.
|
|
318
325
|
|
|
319
326
|
type ScopedModelEntry = { model: { provider: string; id: string }; thinkingLevel?: string };
|
|
320
327
|
|
|
321
|
-
let origCycleScopedModel: ((
|
|
328
|
+
let origCycleScopedModel: ((...args: unknown[]) => Promise<unknown>) | null = null;
|
|
322
329
|
|
|
323
330
|
function patchCycleScopedModel(getLastUsed: () => Record<string, number>): void {
|
|
324
331
|
if (origCycleScopedModel !== null) return;
|
|
325
332
|
|
|
326
333
|
const proto = AgentSession.prototype as unknown as Record<string, unknown>;
|
|
327
|
-
origCycleScopedModel = proto._cycleScopedModel as (
|
|
334
|
+
origCycleScopedModel = proto._cycleScopedModel as (...args: unknown[]) => Promise<unknown>;
|
|
328
335
|
|
|
329
|
-
proto._cycleScopedModel = async function (this: Record<string, unknown>,
|
|
336
|
+
proto._cycleScopedModel = async function (this: Record<string, unknown>, ...args: unknown[]) {
|
|
330
337
|
const orig = origCycleScopedModel;
|
|
331
338
|
if (!orig) return undefined;
|
|
332
339
|
|
|
@@ -334,7 +341,7 @@ function patchCycleScopedModel(getLastUsed: () => Record<string, number>): void
|
|
|
334
341
|
const origScoped = this._scopedModels as ScopedModelEntry[] | undefined;
|
|
335
342
|
|
|
336
343
|
if (!origScoped || origScoped.length <= 1) {
|
|
337
|
-
return orig.
|
|
344
|
+
return orig.apply(this, args);
|
|
338
345
|
}
|
|
339
346
|
|
|
340
347
|
// Sort by last-used without mutating the session's stored order.
|
|
@@ -350,7 +357,7 @@ function patchCycleScopedModel(getLastUsed: () => Record<string, number>): void
|
|
|
350
357
|
// Temporarily swap for the cycle lookup, restore afterward.
|
|
351
358
|
this._scopedModels = sorted;
|
|
352
359
|
try {
|
|
353
|
-
return await orig.
|
|
360
|
+
return await orig.apply(this, args);
|
|
354
361
|
} finally {
|
|
355
362
|
this._scopedModels = origScoped;
|
|
356
363
|
}
|
|
@@ -398,8 +405,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
398
405
|
// branch_summary, compaction entries) — the same predicate pi core uses
|
|
399
406
|
// for its continuation check, not raw branch length and not literal
|
|
400
407
|
// message entries alone.
|
|
408
|
+
//
|
|
409
|
+
// An explicit `--model` on the command line always wins. Pi resolved
|
|
410
|
+
// that model during construction without emitting model_select, so on
|
|
411
|
+
// the initial startup record it as last-used instead of overriding it.
|
|
412
|
+
// This is what bb's Pi provider relies on: it launches
|
|
413
|
+
// `pi --mode rpc --model provider/id` and aborts the thread if pi
|
|
414
|
+
// reports a different model back.
|
|
401
415
|
const hasSessionMessages = hasContextMessages(ctx.sessionManager.buildContextEntries());
|
|
402
|
-
|
|
416
|
+
const explicitModel = hasExplicitModelArg(process.argv);
|
|
417
|
+
if (explicitModel && shouldApplyMruOverride(event.reason, hasSessionMessages)) {
|
|
418
|
+
if (event.reason === "startup" && ctx.model) {
|
|
419
|
+
lastUsed[buildModelKey(ctx.model.provider, ctx.model.id)] = Date.now();
|
|
420
|
+
writeConfig({ lastUsed, thinking: tracker.thinking });
|
|
421
|
+
}
|
|
422
|
+
} else if (shouldApplyMruOverride(event.reason, hasSessionMessages) && Object.keys(lastUsed).length > 0) {
|
|
403
423
|
const mruModel = findMruModel(lastUsed, ctx.modelRegistry);
|
|
404
424
|
const currentModel = ctx.model as { provider: string; id: string } | undefined;
|
|
405
425
|
if (
|
package/extensions/sort.ts
CHANGED
|
@@ -101,6 +101,36 @@ export function shouldApplyMruOverride(reason: SessionStartReason, hasSessionMes
|
|
|
101
101
|
return reason === "startup" || reason === "new";
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Whether the process was launched with an explicit `--model` argument.
|
|
106
|
+
*
|
|
107
|
+
* Pi does not tell extensions whether the starting model came from the CLI,
|
|
108
|
+
* so this inspects the argv pi was started with. An explicit model must win
|
|
109
|
+
* over the MRU override: callers such as bb's Pi provider (`pi --mode rpc
|
|
110
|
+
* --model provider/id`), scripts, and anyone typing `pi --model ...` expect
|
|
111
|
+
* the model they asked for, and bb aborts the thread when the model pi
|
|
112
|
+
* reports back differs from the one it requested.
|
|
113
|
+
*
|
|
114
|
+
* Only pi's own spelling counts: `--model <value>` (pi 0.84.x has no `-m`
|
|
115
|
+
* alias). `--model=value` is accepted too in case pi adds it. `--models` is
|
|
116
|
+
* a scope list, not a selection, and is ignored. A trailing `--model` with no
|
|
117
|
+
* value is ignored.
|
|
118
|
+
*
|
|
119
|
+
* @param argv process arguments to inspect (normally `process.argv`)
|
|
120
|
+
*/
|
|
121
|
+
export function hasExplicitModelArg(argv: readonly string[]): boolean {
|
|
122
|
+
for (let i = 0; i < argv.length; i++) {
|
|
123
|
+
const arg = argv[i];
|
|
124
|
+
if (arg === "--model") {
|
|
125
|
+
const value = argv[i + 1];
|
|
126
|
+
if (value !== undefined && value !== "" && !value.startsWith("-")) return true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (arg.startsWith("--model=") && arg.length > "--model=".length) return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
104
134
|
/**
|
|
105
135
|
* Whether the model pi restored for this session start should be recorded as
|
|
106
136
|
* last-used. Pi 0.84.3 restores a continued session's model during
|