pi-model-sort 0.1.0 → 0.1.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/README.md +9 -2
- package/model-sort.ts +65 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,8 @@ The extension works automatically — there are no commands to learn.
|
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
# Install, then just use pi normally
|
|
39
|
-
/model # Most recently used models
|
|
39
|
+
/model # Most recently used models appear at the top
|
|
40
|
+
Ctrl+P / Ctrl+Shift+P # Cycle through models in last-used order
|
|
40
41
|
pi --list-models # CLI output is also sorted by last usage
|
|
41
42
|
```
|
|
42
43
|
|
|
@@ -104,19 +105,25 @@ Session starts
|
|
|
104
105
|
sortModels — sorts "Scope: all" view
|
|
105
106
|
loadModels — sorts "Scope: scoped" scopedModelItems after load
|
|
106
107
|
→ Monkey-patches ModelRegistry.prototype.getAvailable/getAll
|
|
108
|
+
→ Monkey-patches AgentSession.prototype._cycleScopedModel
|
|
107
109
|
→ Sort order: current model first → most recent → provider/id alphabetical
|
|
108
110
|
→ Patches survive modelRegistry.refresh()
|
|
109
111
|
```
|
|
110
112
|
|
|
111
|
-
**
|
|
113
|
+
**Five patches, full coverage:**
|
|
112
114
|
|
|
113
115
|
| Patch | What it affects |
|
|
114
116
|
|-------|----------------|
|
|
115
117
|
| `ModelSelectorComponent.prototype.sortModels` | `/model` TUI picker — "Scope: all" view |
|
|
116
118
|
| `ModelSelectorComponent.prototype.loadModels` | `/model` TUI picker — "Scope: scoped" view (configured cycling models) |
|
|
119
|
+
| `AgentSession.prototype._cycleScopedModel` | `Ctrl+P` / `Ctrl+Shift+P` cycling order (non-destructive swap, cycling does not update last-used to avoid feedback loop) |
|
|
117
120
|
| `ModelRegistry.prototype.getAvailable()` | `/scoped-models` config selector, model resolution |
|
|
118
121
|
| `ModelRegistry.prototype.getAll()` | `--list-models` CLI output |
|
|
119
122
|
|
|
123
|
+
When no scoped models are configured, Ctrl+P falls through to `_cycleAvailableModel` which calls `getAvailable()` — already sorted by the registry patch.
|
|
124
|
+
|
|
125
|
+
> **Why cycling doesn't update last-used:** Updating timestamps during Ctrl+P cycling creates a feedback loop — each cycle makes the selected model most-recent, re-sorts it to position 0, and `(currentIndex + 1) % len` always lands on the second model. Models would toggle forever between the top 2. Manual model selection (`/model`, session restore) still updates last-used.
|
|
126
|
+
|
|
120
127
|
The SDK doesn't expose a sort order for model lists. Monkey-patching the component and registry methods is the only way to control ordering without rebuilding the entire picker UI.
|
|
121
128
|
|
|
122
129
|
The patches survive `modelRegistry.refresh()` because they wrap the original methods. On reload, the extension detects the prototypes are already patched and just updates the last-used data source.
|
package/model-sort.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-model-sort — sort models in pi by last usage (descending).
|
|
3
3
|
*
|
|
4
|
-
* Strategy: monkey-patches
|
|
5
|
-
* loadModels
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Strategy: monkey-patches three areas:
|
|
5
|
+
* ModelSelectorComponent.prototype.sortModels and loadModels — sorts both
|
|
6
|
+
* "Scope: all" and "Scope: scoped" views in the /model TUI picker.
|
|
7
|
+
* ModelRegistry.prototype.getAvailable/getAll — sorts --list-models CLI
|
|
8
|
+
* and the /scoped-models config selector.
|
|
9
|
+
* AgentSession.prototype._cycleScopedModel — sorts the Ctrl+P / Ctrl+Shift+P
|
|
10
|
+
* cycling order (non-destructively — the configured order is preserved).
|
|
9
11
|
*
|
|
10
12
|
* Usage tracking is automatic — every model selection (manual, Ctrl+P cycle,
|
|
11
13
|
* or session restore) updates the last-used timestamp. Data persists to
|
|
@@ -16,7 +18,7 @@
|
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import { ModelRegistry, ModelSelectorComponent } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { AgentSession, ModelRegistry, ModelSelectorComponent } from "@earendil-works/pi-coding-agent";
|
|
20
22
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
21
23
|
import { homedir } from "node:os";
|
|
22
24
|
import { join } from "node:path";
|
|
@@ -188,6 +190,55 @@ function unpatchRegistry(registry: PatchedRegistry): void {
|
|
|
188
190
|
delete raw.__model_sort_orig_getAll;
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
// AgentSession _cycleScopedModel patch — sorts the scoped models list
|
|
194
|
+
// before cycling so Ctrl+P / Ctrl+Shift+P follows last-used order instead
|
|
195
|
+
// of the configured order. Non-destructive: the session's stored order is
|
|
196
|
+
// temporarily swapped and restored after the cycle lookup.
|
|
197
|
+
|
|
198
|
+
type ScopedModelEntry = { model: { provider: string; id: string }; thinkingLevel?: string };
|
|
199
|
+
|
|
200
|
+
let origCycleScopedModel: ((direction: string) => Promise<unknown>) | null = null;
|
|
201
|
+
|
|
202
|
+
function patchCycleScopedModel(getLastUsed: () => Record<string, number>): void {
|
|
203
|
+
if (origCycleScopedModel !== null) return;
|
|
204
|
+
|
|
205
|
+
const proto = AgentSession.prototype as unknown as Record<string, unknown>;
|
|
206
|
+
origCycleScopedModel = proto._cycleScopedModel as (direction: string) => Promise<unknown>;
|
|
207
|
+
|
|
208
|
+
proto._cycleScopedModel = async function (this: Record<string, unknown>, direction: string) {
|
|
209
|
+
const lastUsed = getLastUsed();
|
|
210
|
+
const origScoped = this._scopedModels as ScopedModelEntry[] | undefined;
|
|
211
|
+
|
|
212
|
+
if (!origScoped || origScoped.length <= 1) {
|
|
213
|
+
return origCycleScopedModel!.call(this, direction);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Sort by last-used without mutating the session's stored order.
|
|
217
|
+
const sorted = [...origScoped].sort((a, b) => {
|
|
218
|
+
const aKey = buildModelKey(a.model.provider, a.model.id);
|
|
219
|
+
const bKey = buildModelKey(b.model.provider, b.model.id);
|
|
220
|
+
const aLast = lastUsed[aKey] ?? 0;
|
|
221
|
+
const bLast = lastUsed[bKey] ?? 0;
|
|
222
|
+
if (aLast !== bLast) return bLast - aLast;
|
|
223
|
+
return a.model.provider.localeCompare(b.model.provider) || a.model.id.localeCompare(b.model.id);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Temporarily swap for the cycle lookup, restore afterward.
|
|
227
|
+
this._scopedModels = sorted;
|
|
228
|
+
try {
|
|
229
|
+
return await origCycleScopedModel!.call(this, direction);
|
|
230
|
+
} finally {
|
|
231
|
+
this._scopedModels = origScoped;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function unpatchCycleScopedModel(): void {
|
|
237
|
+
if (origCycleScopedModel === null) return;
|
|
238
|
+
(AgentSession.prototype as unknown as Record<string, unknown>)._cycleScopedModel = origCycleScopedModel;
|
|
239
|
+
origCycleScopedModel = null;
|
|
240
|
+
}
|
|
241
|
+
|
|
191
242
|
// Extension
|
|
192
243
|
|
|
193
244
|
export default function (pi: ExtensionAPI) {
|
|
@@ -200,6 +251,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
200
251
|
patchRegistry(ctx.modelRegistry as unknown as PatchedRegistry, () => lastUsed);
|
|
201
252
|
patchSortModels(() => lastUsed);
|
|
202
253
|
patchLoadModels(() => lastUsed);
|
|
254
|
+
patchCycleScopedModel(() => lastUsed);
|
|
203
255
|
|
|
204
256
|
if (ctx.hasUI) {
|
|
205
257
|
const count = Object.keys(lastUsed).length;
|
|
@@ -212,8 +264,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
212
264
|
}
|
|
213
265
|
});
|
|
214
266
|
|
|
215
|
-
// Track
|
|
267
|
+
// Track model selections (manual, session restore).
|
|
268
|
+
// Skip "cycle" events — updating lastUsed during Ctrl+P cycling creates
|
|
269
|
+
// a feedback loop: each cycle step makes the selected model most-recent,
|
|
270
|
+
// re-sorts it to position 0, then (currentIndex + 1) % len always hits
|
|
271
|
+
// position 1 — toggling forever between the top 2.
|
|
216
272
|
pi.on("model_select", async (event, _ctx) => {
|
|
273
|
+
if (event.source === "cycle") return;
|
|
217
274
|
const key = buildModelKey(event.model.provider, event.model.id);
|
|
218
275
|
lastUsed[key] = Date.now();
|
|
219
276
|
writeConfig({ lastUsed });
|
|
@@ -223,5 +280,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
223
280
|
pi.on("session_shutdown", () => {
|
|
224
281
|
unpatchSortModels();
|
|
225
282
|
unpatchLoadModels();
|
|
283
|
+
unpatchCycleScopedModel();
|
|
226
284
|
});
|
|
227
285
|
}
|