pi-model-sort 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 +7 -2
- package/model-sort.ts +65 -6
- package/package.json +1 -1
- package/src/index.ts +7 -0
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Pi's `/model` selector sorts models alphabetically by provider. If you have Anth
|
|
|
23
23
|
|
|
24
24
|
- **Automatic tracking** — every `/model` switch, `Ctrl+P` cycle, and session restore is recorded with a Unix timestamp
|
|
25
25
|
- **Sort order** — current model first → most recently used descending → provider/id alphabetical fallback
|
|
26
|
+
- **MRU on startup** — new sessions start on your most recently used model instead of `scopedModels[0]` or the hardcoded provider default order
|
|
26
27
|
- **Persistent** — usage data lives in `~/.pi/agent/extensions/pi-model-sort.json`, survives restarts
|
|
27
28
|
- **No config needed** — install and forget; the extension starts tracking on first use
|
|
28
29
|
- **Zero setup** — with no recorded usage, models fall back to the default alphabetical order
|
|
@@ -99,7 +100,7 @@ model_select event fires
|
|
|
99
100
|
→ Writes to pi-model-sort.json
|
|
100
101
|
→ Next /model opens with updated sort
|
|
101
102
|
|
|
102
|
-
Session starts
|
|
103
|
+
Session starts (startup / new)
|
|
103
104
|
→ Extension reads pi-model-sort.json
|
|
104
105
|
→ Monkey-patches ModelSelectorComponent.prototype:
|
|
105
106
|
sortModels — sorts "Scope: all" view
|
|
@@ -108,9 +109,12 @@ Session starts
|
|
|
108
109
|
→ Monkey-patches AgentSession.prototype._cycleScopedModel
|
|
109
110
|
→ Sort order: current model first → most recent → provider/id alphabetical
|
|
110
111
|
→ Patches survive modelRegistry.refresh()
|
|
112
|
+
→ Overrides initial model to MRU via pi.setModel()
|
|
113
|
+
if pi core chose a different model (scopedModels[0], defaultModelPerProvider)
|
|
114
|
+
only on startup and new session starts — not resume, reload, or fork
|
|
111
115
|
```
|
|
112
116
|
|
|
113
|
-
**Five patches, full coverage:**
|
|
117
|
+
**Five patches + MRU startup override, full coverage:**
|
|
114
118
|
|
|
115
119
|
| Patch | What it affects |
|
|
116
120
|
|-------|----------------|
|
|
@@ -119,6 +123,7 @@ Session starts
|
|
|
119
123
|
| `AgentSession.prototype._cycleScopedModel` | `Ctrl+P` / `Ctrl+Shift+P` cycling order (non-destructive swap, cycling does not update last-used to avoid feedback loop) |
|
|
120
124
|
| `ModelRegistry.prototype.getAvailable()` | `/scoped-models` config selector, model resolution |
|
|
121
125
|
| `ModelRegistry.prototype.getAll()` | `--list-models` CLI output |
|
|
126
|
+
| `pi.setModel()` on `session_start` | MRU model selection on startup and `/new` — overrides pi core's default |
|
|
122
127
|
|
|
123
128
|
When no scoped models are configured, Ctrl+P falls through to `_cycleAvailableModel` which calls `getAvailable()` — already sorted by the registry patch.
|
|
124
129
|
|
package/model-sort.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
buildModelKey,
|
|
27
27
|
CONFIG_FILENAME,
|
|
28
28
|
type ModelSortConfig,
|
|
29
|
+
parseModelKey,
|
|
29
30
|
sortByLastUsed,
|
|
30
31
|
} from "./src/index.js";
|
|
31
32
|
|
|
@@ -149,10 +150,29 @@ function patchFilterModels(getLastUsed: () => Record<string, number>): void {
|
|
|
149
150
|
origFilterModels = proto.filterModels as (query: string) => void;
|
|
150
151
|
|
|
151
152
|
proto.filterModels = function (this: Record<string, unknown>, query: string) {
|
|
152
|
-
|
|
153
|
+
// Suppress the original's updateList() call — we'll call it once after
|
|
154
|
+
// re-sorting to avoid a double-render.
|
|
155
|
+
const origUpdateList = this.updateList as () => void;
|
|
156
|
+
this.updateList = (() => {}) as unknown as () => void;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
origFilterModels!.call(this, query);
|
|
160
|
+
} finally {
|
|
161
|
+
this.updateList = origUpdateList as unknown as () => void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Empty query: nothing to re-sort (activeModels is already sorted by our
|
|
165
|
+
// sortModels/loadModels patches), just render the original result.
|
|
166
|
+
if (!query) {
|
|
167
|
+
origUpdateList.call(this);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
153
170
|
|
|
154
171
|
const filtered = this.filteredModels as Array<{ provider: string; id: string; model: unknown }> | undefined;
|
|
155
|
-
if (!filtered || filtered.length <= 1
|
|
172
|
+
if (!filtered || filtered.length <= 1) {
|
|
173
|
+
origUpdateList.call(this);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
156
176
|
|
|
157
177
|
const lastUsed = getLastUsed();
|
|
158
178
|
this.filteredModels = sortByLastUsed(filtered, lastUsed, buildCurrentModelKey(this));
|
|
@@ -169,9 +189,8 @@ function patchFilterModels(getLastUsed: () => Record<string, number>): void {
|
|
|
169
189
|
}
|
|
170
190
|
}
|
|
171
191
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
(this.updateList as () => void)();
|
|
192
|
+
// Render once with the final sorted list.
|
|
193
|
+
origUpdateList.call(this);
|
|
175
194
|
};
|
|
176
195
|
}
|
|
177
196
|
|
|
@@ -284,12 +303,32 @@ function unpatchCycleScopedModel(): void {
|
|
|
284
303
|
origCycleScopedModel = null;
|
|
285
304
|
}
|
|
286
305
|
|
|
306
|
+
// MRU model lookup — finds the most recently used model that exists in the
|
|
307
|
+
// registry and has auth configured. Returns undefined if no usable model found.
|
|
308
|
+
|
|
309
|
+
function findMruModel(
|
|
310
|
+
lastUsed: Record<string, number>,
|
|
311
|
+
registry: { find(provider: string, modelId: string): unknown; hasConfiguredAuth(model: unknown): boolean },
|
|
312
|
+
): unknown | undefined {
|
|
313
|
+
const sorted = Object.entries(lastUsed).sort(([, a], [, b]) => b - a);
|
|
314
|
+
for (const [key] of sorted) {
|
|
315
|
+
const parsed = parseModelKey(key);
|
|
316
|
+
if (!parsed) continue;
|
|
317
|
+
const [provider, modelId] = parsed;
|
|
318
|
+
const model = registry.find(provider, modelId);
|
|
319
|
+
if (model && registry.hasConfiguredAuth(model)) {
|
|
320
|
+
return model;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
287
326
|
// Extension
|
|
288
327
|
|
|
289
328
|
export default function (pi: ExtensionAPI) {
|
|
290
329
|
let lastUsed: Record<string, number> = {};
|
|
291
330
|
|
|
292
|
-
pi.on("session_start", async (
|
|
331
|
+
pi.on("session_start", async (event, ctx) => {
|
|
293
332
|
const config = readConfig();
|
|
294
333
|
lastUsed = config.lastUsed;
|
|
295
334
|
|
|
@@ -299,6 +338,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
299
338
|
patchFilterModels(() => lastUsed);
|
|
300
339
|
patchCycleScopedModel(() => lastUsed);
|
|
301
340
|
|
|
341
|
+
// Override initial model to MRU on new sessions.
|
|
342
|
+
// Pi core picks the saved default if in scope, otherwise scopedModels[0].
|
|
343
|
+
// This hijack switches to the most recently used model instead, so your
|
|
344
|
+
// actual usage history determines the default — not alphabetical scope order.
|
|
345
|
+
if (
|
|
346
|
+
(event.reason === "startup" || event.reason === "new") &&
|
|
347
|
+
Object.keys(lastUsed).length > 0
|
|
348
|
+
) {
|
|
349
|
+
const mruModel = findMruModel(lastUsed, ctx.modelRegistry);
|
|
350
|
+
const currentModel = ctx.model as { provider: string; id: string } | undefined;
|
|
351
|
+
if (
|
|
352
|
+
mruModel &&
|
|
353
|
+
(!currentModel ||
|
|
354
|
+
currentModel.provider !== (mruModel as { provider: string }).provider ||
|
|
355
|
+
currentModel.id !== (mruModel as { id: string }).id)
|
|
356
|
+
) {
|
|
357
|
+
await pi.setModel(mruModel as Parameters<typeof pi.setModel>[0]);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
302
361
|
if (ctx.hasUI) {
|
|
303
362
|
const count = Object.keys(lastUsed).length;
|
|
304
363
|
ctx.ui.notify(
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface ModelSortConfig {
|
|
|
10
10
|
lastUsed: Record<string, number>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** Parse a model key into [provider, modelId]. Returns undefined if malformed. */
|
|
14
|
+
export function parseModelKey(key: string): [provider: string, modelId: string] | undefined {
|
|
15
|
+
const idx = key.indexOf("/");
|
|
16
|
+
if (idx === -1) return undefined;
|
|
17
|
+
return [key.substring(0, idx), key.substring(idx + 1)];
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
/** Build a stable model key from provider and model id. */
|
|
14
21
|
export function buildModelKey(provider: string, modelId: string): string {
|
|
15
22
|
return `${provider}/${modelId}`;
|