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 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
- origFilterModels!.call(this, query);
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 || !query) return;
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
- // Re-render the original filterModels already called updateList() before
173
- // we re-sorted, so the UI is stale until the next input event.
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 (_event, ctx) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-model-sort",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Sort models in pi's /model selector by last usage — most recently used models appear first",
5
5
  "type": "module",
6
6
  "author": "Tom X Nguyen",
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}`;