@xynogen/pix-models 0.1.15 → 0.1.17
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/package.json +1 -1
- package/src/models.test.ts +132 -9
- package/src/models.ts +146 -128
- package/src/patch-builtin.test.ts +2 -6
- package/src/patch-builtin.ts +3 -14
package/package.json
CHANGED
package/src/models.test.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
benchStars,
|
|
4
|
+
filterModelItems,
|
|
5
|
+
fmtCost,
|
|
6
|
+
fmtCtx,
|
|
7
|
+
type ModelSearchLookup,
|
|
8
|
+
normalizeModelText,
|
|
9
|
+
sortModels,
|
|
10
|
+
} from "./models.ts";
|
|
3
11
|
|
|
4
12
|
describe("fmtCtx", () => {
|
|
5
13
|
it("formats 0 as 0", () => expect(fmtCtx(0)).toBe("0"));
|
|
@@ -16,8 +24,7 @@ describe("fmtCtx", () => {
|
|
|
16
24
|
});
|
|
17
25
|
|
|
18
26
|
describe("fmtCost", () => {
|
|
19
|
-
it("returns — for undefined entry", () =>
|
|
20
|
-
expect(fmtCost(undefined)).toBe("—"));
|
|
27
|
+
it("returns — for undefined entry", () => expect(fmtCost(undefined)).toBe("—"));
|
|
21
28
|
it("returns — when no cost field", () => expect(fmtCost({})).toBe("—"));
|
|
22
29
|
it("returns free when both 0", () => {
|
|
23
30
|
expect(fmtCost({ cost: { input: 0, output: 0 } })).toBe("free");
|
|
@@ -118,11 +125,127 @@ describe("sortModels", () => {
|
|
|
118
125
|
{ provider: "a", id: "m4", name: "Delta", score: 60, tier: 0 },
|
|
119
126
|
];
|
|
120
127
|
const sorted = sortModels(mixed);
|
|
121
|
-
expect(sorted.map((m) => m.name)).toEqual([
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
expect(sorted.map((m) => m.name)).toEqual(["Alpha", "Delta", "Beta", "Gamma"]);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ─── normalizeModelText ───────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
describe("normalizeModelText", () => {
|
|
135
|
+
it("lowercases", () => {
|
|
136
|
+
expect(normalizeModelText("GLM-5.2")).toBe("glm52");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("strips hyphens", () => {
|
|
140
|
+
expect(normalizeModelText("claude-opus-4-8")).toBe("claudeopus48");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("strips dots", () => {
|
|
144
|
+
expect(normalizeModelText("qwen3.7-max")).toBe("qwen37max");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("strips all non-alphanumeric", () => {
|
|
148
|
+
expect(normalizeModelText("a!b@c#d$e%f^g&h")).toBe("abcdefgh");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("handles plain alphanumeric", () => {
|
|
152
|
+
expect(normalizeModelText("abc123")).toBe("abc123");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("handles empty string", () => {
|
|
156
|
+
expect(normalizeModelText("")).toBe("");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── filterModelItems ─────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
describe("filterModelItems", () => {
|
|
163
|
+
// Fixture: models with ids matching the acceptance criteria.
|
|
164
|
+
// Ranks are assigned so some overlap with digit-queries.
|
|
165
|
+
const mk = (id: string, rank?: number) => ({
|
|
166
|
+
value: `p/${id}`,
|
|
167
|
+
_rank: rank,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
type Fixture = ReturnType<typeof mk>;
|
|
171
|
+
|
|
172
|
+
/** Build lookup maps exactly like production: haystack = `${id} ${name ?? ""}`. */
|
|
173
|
+
function buildLookup(items: Fixture[]): ModelSearchLookup {
|
|
174
|
+
const rankByValue = new Map<string, number>();
|
|
175
|
+
const searchTextByValue = new Map<string, string>();
|
|
176
|
+
const normalizedByValue = new Map<string, string>();
|
|
177
|
+
for (const it of items) {
|
|
178
|
+
// value is "p/glm-5.2" — extract the id part.
|
|
179
|
+
const id = it.value.split("/")[1] ?? "";
|
|
180
|
+
if (it._rank != null) rankByValue.set(it.value, it._rank);
|
|
181
|
+
const text = `${id} ${""}`; // no name in test fixture
|
|
182
|
+
searchTextByValue.set(it.value, text);
|
|
183
|
+
normalizedByValue.set(it.value, normalizeModelText(text));
|
|
184
|
+
}
|
|
185
|
+
return { rankByValue, searchTextByValue, normalizedByValue };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Full fixture with ranks: glm-5.2→3, minimax-m3→8, claude-opus-4-8→2.
|
|
189
|
+
const allItems: Fixture[] = [
|
|
190
|
+
mk("glm-5.2", 3),
|
|
191
|
+
mk("glm-5.1"),
|
|
192
|
+
mk("minimax-m3", 8),
|
|
193
|
+
mk("claude-opus-4-8", 2),
|
|
194
|
+
mk("claude-sonnet-4-6"),
|
|
195
|
+
mk("qwen3.7-max"),
|
|
196
|
+
];
|
|
197
|
+
const allLookup = buildLookup(allItems);
|
|
198
|
+
|
|
199
|
+
function values(result: Fixture[]): string[] {
|
|
200
|
+
return result.map((it) => it.value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
it("empty query returns all items in original order", () => {
|
|
204
|
+
const result = filterModelItems(allItems, "", allLookup);
|
|
205
|
+
expect(values(result)).toEqual(allItems.map((it) => it.value));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("query '52' includes glm-5.2 via normalized substring", () => {
|
|
209
|
+
const result = filterModelItems(allItems, "52", allLookup);
|
|
210
|
+
expect(values(result)).toContain("p/glm-5.2");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("query 'm3' includes minimax-m3 first (substring priority over fuzzy noise)", () => {
|
|
214
|
+
const result = filterModelItems(allItems, "m3", allLookup);
|
|
215
|
+
expect(values(result)[0]).toBe("p/minimax-m3");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("query '48' includes claude-opus-4-8", () => {
|
|
219
|
+
const result = filterModelItems(allItems, "48", allLookup);
|
|
220
|
+
expect(values(result)).toContain("p/claude-opus-4-8");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("exact id query 'glm-5.2' still matches glm-5.2", () => {
|
|
224
|
+
const result = filterModelItems(allItems, "glm-5.2", allLookup);
|
|
225
|
+
expect(values(result)).toContain("p/glm-5.2");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("digit query matching a rank returns that ranked item first", () => {
|
|
229
|
+
// glm-5.2 has rank 3 — query "3" should put it first.
|
|
230
|
+
const result = filterModelItems(allItems, "3", allLookup);
|
|
231
|
+
expect(values(result)[0]).toBe("p/glm-5.2");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("dedupe: item matching both rank and substring appears once", () => {
|
|
235
|
+
// claude-opus-4-8 has rank 2; query "2" matches by rank AND
|
|
236
|
+
// normalized substring ("2" ⊂ "claudeopus48"). Should appear once.
|
|
237
|
+
const result = filterModelItems(allItems, "2", allLookup);
|
|
238
|
+
const occurrences = values(result).filter((v) => v === "p/claude-opus-4-8");
|
|
239
|
+
expect(occurrences.length).toBe(1);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("digit query '8' (rank 8) returns minimax-m3 as rank match first", () => {
|
|
243
|
+
const result = filterModelItems(allItems, "8", allLookup);
|
|
244
|
+
expect(values(result)[0]).toBe("p/minimax-m3");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("whitespace query is treated as empty", () => {
|
|
248
|
+
const result = filterModelItems(allItems, " ", allLookup);
|
|
249
|
+
expect(values(result)).toEqual(allItems.map((it) => it.value));
|
|
127
250
|
});
|
|
128
251
|
});
|
package/src/models.ts
CHANGED
|
@@ -8,10 +8,7 @@
|
|
|
8
8
|
* Sorted by benchlm rank when available (best first), then alphabetical.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type {
|
|
12
|
-
ExtensionAPI,
|
|
13
|
-
ExtensionContext,
|
|
14
|
-
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
12
|
import {
|
|
16
13
|
fuzzyFilter,
|
|
17
14
|
Input,
|
|
@@ -20,11 +17,7 @@ import {
|
|
|
20
17
|
SelectList,
|
|
21
18
|
visibleWidth,
|
|
22
19
|
} from "@earendil-works/pi-tui";
|
|
23
|
-
import {
|
|
24
|
-
benchScoreColor,
|
|
25
|
-
lookupBenchmark,
|
|
26
|
-
lookupModelsDev,
|
|
27
|
-
} from "@xynogen/pix-data";
|
|
20
|
+
import { benchScoreColor, lookupBenchmark, lookupModelsDev } from "@xynogen/pix-data";
|
|
28
21
|
import { icon } from "@xynogen/pix-pretty/icon-catalog";
|
|
29
22
|
import { frameLines, modalWidth } from "@xynogen/pix-pretty/modal-frame";
|
|
30
23
|
import { patchOutBuiltinModelCommand } from "./patch-builtin";
|
|
@@ -40,9 +33,7 @@ export function fmtCtx(n: number): string {
|
|
|
40
33
|
return `${Math.round(n / 1_000)}k`;
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
export function fmtCost(
|
|
44
|
-
entry: { cost?: { input?: number; output?: number } } | undefined,
|
|
45
|
-
): string {
|
|
36
|
+
export function fmtCost(entry: { cost?: { input?: number; output?: number } } | undefined): string {
|
|
46
37
|
if (!entry?.cost) return "\u2014";
|
|
47
38
|
const i = entry.cost.input ?? 0;
|
|
48
39
|
const o = entry.cost.output ?? 0;
|
|
@@ -96,10 +87,69 @@ export function sortModels<T extends SortableModel>(models: T[]): T[] {
|
|
|
96
87
|
});
|
|
97
88
|
}
|
|
98
89
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
90
|
+
/** Lowercase and strip all non-alphanumerics: "glm-5.2" → "glm52". */
|
|
91
|
+
export function normalizeModelText(s: string): string {
|
|
92
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type ModelSearchLookup = {
|
|
96
|
+
/** benchlm local rank per item value (ranked models only). */
|
|
97
|
+
rankByValue: Map<string, number>;
|
|
98
|
+
/** Clean haystack per value: `${id} ${name ?? ""}` (no ANSI, no rank cell). */
|
|
99
|
+
searchTextByValue: Map<string, string>;
|
|
100
|
+
/** normalizeModelText(haystack) per value — for family+version substring matches. */
|
|
101
|
+
normalizedByValue: Map<string, string>;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Filter+order picker items for a search query.
|
|
106
|
+
* - "" → all items unchanged.
|
|
107
|
+
* - digits-only → items whose benchlm rank equals the number, followed by
|
|
108
|
+
* normalized-substring matches ("52" ⊂ "glm52") rank-sorted.
|
|
109
|
+
* - otherwise → normalized-substring matches first (rank-sorted), then
|
|
110
|
+
* fuzzy matches over the clean haystack (rank-sorted, stable), deduped.
|
|
111
|
+
*/
|
|
112
|
+
export function filterModelItems<T extends { value: string }>(
|
|
113
|
+
items: T[],
|
|
114
|
+
query: string,
|
|
115
|
+
lookup: ModelSearchLookup,
|
|
116
|
+
): T[] {
|
|
117
|
+
const q = query.trim();
|
|
118
|
+
if (q.length === 0) return items;
|
|
119
|
+
|
|
120
|
+
const rankSort = (arr: T[]): T[] => {
|
|
121
|
+
return arr
|
|
122
|
+
.map((it, i) => ({ it, i }))
|
|
123
|
+
.sort((a, b) => {
|
|
124
|
+
const ra = lookup.rankByValue.get(a.it.value) ?? Infinity;
|
|
125
|
+
const rb = lookup.rankByValue.get(b.it.value) ?? Infinity;
|
|
126
|
+
if (ra !== rb) return ra - rb;
|
|
127
|
+
return a.i - b.i;
|
|
128
|
+
})
|
|
129
|
+
.map(({ it }) => it);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const nq = normalizeModelText(q);
|
|
133
|
+
const subMatches =
|
|
134
|
+
nq.length > 0
|
|
135
|
+
? items.filter((it) => (lookup.normalizedByValue.get(it.value) ?? "").includes(nq))
|
|
136
|
+
: [];
|
|
137
|
+
|
|
138
|
+
if (/^\d+$/.test(q)) {
|
|
139
|
+
const wanted = Number(q);
|
|
140
|
+
const rankMatches = items.filter((it) => lookup.rankByValue.get(it.value) === wanted);
|
|
141
|
+
const rankValues = new Set(rankMatches.map((it) => it.value));
|
|
142
|
+
const extraSub = rankSort(subMatches).filter((it) => !rankValues.has(it.value));
|
|
143
|
+
return [...rankMatches, ...extraSub];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const fuzzy = fuzzyFilter(items, q, (it) => lookup.searchTextByValue.get(it.value) ?? "");
|
|
147
|
+
const subValues = new Set(subMatches.map((it) => it.value));
|
|
148
|
+
const extraFuzzy = rankSort(fuzzy).filter((it) => !subValues.has(it.value));
|
|
149
|
+
return [...rankSort(subMatches), ...extraFuzzy];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
103
153
|
// Mirror the built-in /model selector, which calls refresh() then awaits
|
|
104
154
|
// getAvailable() (see model-selector.js). Without refresh(), this extension
|
|
105
155
|
// reads whatever `this.models` was last loaded into — which, depending on
|
|
@@ -182,9 +232,7 @@ async function showEnhancedPicker(
|
|
|
182
232
|
|
|
183
233
|
// Find max rank width across all benchmarked rows for # padding
|
|
184
234
|
const maxRankWidth = Math.max(
|
|
185
|
-
...dedupedRows.map((r) =>
|
|
186
|
-
r.localRank ? String(r.localRank).length : 0,
|
|
187
|
-
),
|
|
235
|
+
...dedupedRows.map((r) => (r.localRank ? String(r.localRank).length : 0)),
|
|
188
236
|
1,
|
|
189
237
|
);
|
|
190
238
|
|
|
@@ -198,84 +246,85 @@ async function showEnhancedPicker(
|
|
|
198
246
|
if (localRank) rankByValue.set(`${m.provider}/${m.id}`, localRank);
|
|
199
247
|
}
|
|
200
248
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
//
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
// Clean search haystacks — labels are ANSI-laden and carry the rank cell,
|
|
250
|
+
// so matching runs against raw id+name instead (see filterModelItems).
|
|
251
|
+
const searchTextByValue = new Map<string, string>();
|
|
252
|
+
const normalizedByValue = new Map<string, string>();
|
|
253
|
+
for (const { m } of dedupedRows) {
|
|
254
|
+
const value = `${m.provider}/${m.id}`;
|
|
255
|
+
const text = `${m.id} ${m.name ?? ""}`;
|
|
256
|
+
searchTextByValue.set(value, text);
|
|
257
|
+
normalizedByValue.set(value, normalizeModelText(text));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const items: SelectItem[] = dedupedRows.map(({ m, dev, bench, localRank }) => {
|
|
261
|
+
const isCurrent = current && m.provider === current.provider && m.id === current.id;
|
|
262
|
+
|
|
263
|
+
// Label: marker + rank cell + accent-colored model name.
|
|
264
|
+
// Ranked models show muted '#' + colored rank. Unranked (no
|
|
265
|
+
// modelgrep entry) show a muted em-dash sized to the rank
|
|
266
|
+
// column, so the model name aligns across rows.
|
|
267
|
+
const marker = isCurrent ? theme.fg(accent, "▶") : " ";
|
|
268
|
+
let rankPrefix: string;
|
|
269
|
+
if (localRank) {
|
|
270
|
+
const rankStr = String(localRank).padEnd(maxRankWidth);
|
|
271
|
+
// Color rank by the model's bench score (same scale as ⚡score),
|
|
272
|
+
// not by list position — keeps the two colors consistent.
|
|
273
|
+
const rankColor = benchScoreColor(bench?.overallScore);
|
|
274
|
+
rankPrefix = mute("#") + theme.fg(rankColor, rankStr);
|
|
275
|
+
} else {
|
|
276
|
+
// Width = "#" + maxRankWidth chars (e.g. "# " or "#——" for 2-digit ranks).
|
|
277
|
+
const dash = "—".padEnd(maxRankWidth, " ");
|
|
278
|
+
rankPrefix = mute("#") + mute(dash);
|
|
279
|
+
}
|
|
280
|
+
// Display model id only; m.provider is routing provider, not part of id.
|
|
281
|
+
const idColored = theme.fg(accent, m.id);
|
|
282
|
+
const label = `${marker} ${rankPrefix} ${idColored}`;
|
|
283
|
+
|
|
284
|
+
// Description: ctx · cost · score stars
|
|
285
|
+
// Colors: ctx muted · cost success (free muted) · score+stars warning
|
|
286
|
+
const ctxRaw = fmtCtx(dev?.limit?.context ?? 0);
|
|
287
|
+
const ctxStr = mute(ctxRaw.padStart(4));
|
|
288
|
+
const rawCost = fmtCost(dev);
|
|
289
|
+
let costSeg: string;
|
|
290
|
+
if (rawCost === "—") {
|
|
291
|
+
costSeg = theme.fg("dim", "—".padEnd(10));
|
|
292
|
+
} else if (rawCost === "free") {
|
|
293
|
+
costSeg = mute("free".padEnd(10));
|
|
294
|
+
} else {
|
|
295
|
+
costSeg = theme.fg("success", rawCost.padEnd(10));
|
|
296
|
+
}
|
|
297
|
+
let benchSeg = "";
|
|
298
|
+
if (bench) {
|
|
299
|
+
const score = bench.overallScore ?? "?";
|
|
300
|
+
const s = bench.overallScore;
|
|
301
|
+
const scoreColor = benchScoreColor(s);
|
|
302
|
+
let filled = 1;
|
|
303
|
+
if (typeof s === "number") {
|
|
304
|
+
if (s >= 90) filled = 5;
|
|
305
|
+
else if (s >= 80) filled = 4;
|
|
306
|
+
else if (s >= 70) filled = 3;
|
|
307
|
+
else if (s >= 50) filled = 2;
|
|
256
308
|
}
|
|
257
|
-
const
|
|
309
|
+
const starBar = theme.fg(scoreColor, "★".repeat(filled)) + mute("☆".repeat(5 - filled));
|
|
310
|
+
benchSeg = `⚡${theme.fg(scoreColor, String(score))} ${starBar}`;
|
|
311
|
+
}
|
|
312
|
+
const desc = [ctxStr, costSeg, benchSeg].filter(Boolean).join(sep);
|
|
258
313
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
);
|
|
314
|
+
return {
|
|
315
|
+
value: `${m.provider}/${m.id}`,
|
|
316
|
+
label,
|
|
317
|
+
description: desc,
|
|
318
|
+
};
|
|
319
|
+
});
|
|
266
320
|
|
|
267
321
|
const currentIdx = current
|
|
268
|
-
? items.findIndex(
|
|
269
|
-
(it) => it.value === `${current.provider}/${current.id}`,
|
|
270
|
-
)
|
|
322
|
+
? items.findIndex((it) => it.value === `${current.provider}/${current.id}`)
|
|
271
323
|
: 0;
|
|
272
324
|
|
|
273
325
|
// Widest label (visible width, ANSI-stripped) so the model name
|
|
274
326
|
// column never truncates to "…". Add gap headroom.
|
|
275
|
-
const widestLabel = items.reduce(
|
|
276
|
-
(w, it) => Math.max(w, visibleWidth(it.label)),
|
|
277
|
-
0,
|
|
278
|
-
);
|
|
327
|
+
const widestLabel = items.reduce((w, it) => Math.max(w, visibleWidth(it.label)), 0);
|
|
279
328
|
|
|
280
329
|
const search = new Input();
|
|
281
330
|
const list = new SelectList(
|
|
@@ -306,33 +355,11 @@ async function showEnhancedPicker(
|
|
|
306
355
|
selectedIndex: number;
|
|
307
356
|
invalidate(): void;
|
|
308
357
|
};
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
// Pure number → match by benchlm rank, not name.
|
|
315
|
-
const wanted = Number(q);
|
|
316
|
-
next = internal.items.filter(
|
|
317
|
-
(it) => rankByValue.get(it.value) === wanted,
|
|
318
|
-
);
|
|
319
|
-
} else {
|
|
320
|
-
next = fuzzyFilter(
|
|
321
|
-
internal.items,
|
|
322
|
-
q,
|
|
323
|
-
(it) => `${it.label} ${it.description ?? ""}`,
|
|
324
|
-
);
|
|
325
|
-
// Stable sort: ranked models (by rank asc) before unranked.
|
|
326
|
-
next = next
|
|
327
|
-
.map((it, i) => ({ it, i }))
|
|
328
|
-
.sort((a, b) => {
|
|
329
|
-
const ra = rankByValue.get(a.it.value) ?? Infinity;
|
|
330
|
-
const rb = rankByValue.get(b.it.value) ?? Infinity;
|
|
331
|
-
if (ra !== rb) return ra - rb;
|
|
332
|
-
return a.i - b.i;
|
|
333
|
-
})
|
|
334
|
-
.map(({ it }) => it);
|
|
335
|
-
}
|
|
358
|
+
const next = filterModelItems(internal.items, query, {
|
|
359
|
+
rankByValue,
|
|
360
|
+
searchTextByValue,
|
|
361
|
+
normalizedByValue,
|
|
362
|
+
});
|
|
336
363
|
internal.filteredItems = next;
|
|
337
364
|
internal.selectedIndex = 0;
|
|
338
365
|
internal.invalidate();
|
|
@@ -343,21 +370,12 @@ async function showEnhancedPicker(
|
|
|
343
370
|
const mw = modalWidth(w);
|
|
344
371
|
const inner = mw - 4; // CHROME = 2 border + 2 padding
|
|
345
372
|
const lines: string[] = [
|
|
346
|
-
theme.fg(
|
|
347
|
-
|
|
348
|
-
theme.bold(`${icon("picker.model")} Select model`),
|
|
349
|
-
),
|
|
350
|
-
theme.fg(
|
|
351
|
-
"dim",
|
|
352
|
-
"context · pricing · coding rank & score from modelgrep.com",
|
|
353
|
-
),
|
|
373
|
+
theme.fg(accent, theme.bold(`${icon("picker.model")} Select model`)),
|
|
374
|
+
theme.fg("dim", "context · pricing · coding rank & score from modelgrep.com"),
|
|
354
375
|
theme.fg("muted", "Search:"),
|
|
355
376
|
...search.render(inner),
|
|
356
377
|
...list.render(inner),
|
|
357
|
-
theme.fg(
|
|
358
|
-
"dim",
|
|
359
|
-
"fuzzy search · ↑↓ navigate · enter select · esc cancel",
|
|
360
|
-
),
|
|
378
|
+
theme.fg("dim", "fuzzy search · ↑↓ navigate · enter select · esc cancel"),
|
|
361
379
|
];
|
|
362
380
|
return frameLines({
|
|
363
381
|
width: mw,
|
|
@@ -5,8 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
|
|
6
6
|
// Pure replacement tested in isolation (the exported fn resolves the host
|
|
7
7
|
// package, which isn't present in the test sandbox).
|
|
8
|
-
const MODEL_COMMAND_LINE =
|
|
9
|
-
'{ name: "model", description: "Select model (opens selector UI)" },';
|
|
8
|
+
const MODEL_COMMAND_LINE = '{ name: "model", description: "Select model (opens selector UI)" },';
|
|
10
9
|
|
|
11
10
|
function escapeRegExp(text: string): string {
|
|
12
11
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -14,10 +13,7 @@ function escapeRegExp(text: string): string {
|
|
|
14
13
|
|
|
15
14
|
function patchSource(source: string): string {
|
|
16
15
|
if (!source.includes(MODEL_COMMAND_LINE)) return source;
|
|
17
|
-
return source.replace(
|
|
18
|
-
new RegExp(`[ \\t]*${escapeRegExp(MODEL_COMMAND_LINE)}\\n?`),
|
|
19
|
-
"",
|
|
20
|
-
);
|
|
16
|
+
return source.replace(new RegExp(`[ \\t]*${escapeRegExp(MODEL_COMMAND_LINE)}\\n?`), "");
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
const UNPATCHED = `export const BUILTIN_SLASH_COMMANDS = [
|
package/src/patch-builtin.ts
CHANGED
|
@@ -19,8 +19,7 @@ import { createRequire } from "node:module";
|
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { dirname, join, resolve } from "node:path";
|
|
21
21
|
|
|
22
|
-
const MODEL_COMMAND_LINE =
|
|
23
|
-
'{ name: "model", description: "Select model (opens selector UI)" },';
|
|
22
|
+
const MODEL_COMMAND_LINE = '{ name: "model", description: "Select model (opens selector UI)" },';
|
|
24
23
|
|
|
25
24
|
/** Candidate slash-commands.js paths, most-specific first. */
|
|
26
25
|
function candidatePaths(): string[] {
|
|
@@ -51,14 +50,7 @@ function candidatePaths(): string[] {
|
|
|
51
50
|
];
|
|
52
51
|
for (const root of globalRoots) {
|
|
53
52
|
paths.push(
|
|
54
|
-
join(
|
|
55
|
-
root,
|
|
56
|
-
"@earendil-works",
|
|
57
|
-
"pi-coding-agent",
|
|
58
|
-
"dist",
|
|
59
|
-
"core",
|
|
60
|
-
"slash-commands.js",
|
|
61
|
-
),
|
|
53
|
+
join(root, "@earendil-works", "pi-coding-agent", "dist", "core", "slash-commands.js"),
|
|
62
54
|
);
|
|
63
55
|
}
|
|
64
56
|
|
|
@@ -99,10 +91,7 @@ export function patchOutBuiltinModelCommand(): void {
|
|
|
99
91
|
|
|
100
92
|
if (!source.includes(MODEL_COMMAND_LINE)) return; // already patched
|
|
101
93
|
|
|
102
|
-
const patched = source.replace(
|
|
103
|
-
new RegExp(`[ \\t]*${escapeRegExp(MODEL_COMMAND_LINE)}\\n?`),
|
|
104
|
-
"",
|
|
105
|
-
);
|
|
94
|
+
const patched = source.replace(new RegExp(`[ \\t]*${escapeRegExp(MODEL_COMMAND_LINE)}\\n?`), "");
|
|
106
95
|
if (patched === source) return;
|
|
107
96
|
|
|
108
97
|
try {
|