@xynogen/pix-models 0.1.18 → 0.1.20
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 +3 -2
- package/src/extension.ts +1 -1
- package/src/models.test.ts +48 -0
- package/src/models.ts +88 -3
- package/src/once.ts +0 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-models",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Pi extension — enhanced /models picker with BenchLM ranks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@xynogen/pix-data": "^0.3.0",
|
|
42
|
-
"@xynogen/pix-pretty": "^1.7.9"
|
|
42
|
+
"@xynogen/pix-pretty": "^1.7.9",
|
|
43
|
+
"@xynogen/pix-runtime": "^0.1.1"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
46
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/extension.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { once } from "@xynogen/pix-runtime/once";
|
|
2
3
|
import modelPickerExtension from "./models.ts";
|
|
3
|
-
import { once } from "./once.ts";
|
|
4
4
|
|
|
5
5
|
export default function (pi: ExtensionAPI): void {
|
|
6
6
|
once(pi, "pix-models", () => modelPickerExtension(pi));
|
package/src/models.test.ts
CHANGED
|
@@ -7,8 +7,56 @@ import {
|
|
|
7
7
|
type ModelSearchLookup,
|
|
8
8
|
normalizeModelText,
|
|
9
9
|
sortModels,
|
|
10
|
+
stepEffectiveThinkingLevel,
|
|
11
|
+
stepThinkingLevel,
|
|
12
|
+
THINKING_LEVELS,
|
|
10
13
|
} from "./models.ts";
|
|
11
14
|
|
|
15
|
+
describe("stepThinkingLevel", () => {
|
|
16
|
+
it("steps up one notch", () => {
|
|
17
|
+
expect(stepThinkingLevel("low", 1)).toBe("medium");
|
|
18
|
+
expect(stepThinkingLevel("off", 1)).toBe("minimal");
|
|
19
|
+
});
|
|
20
|
+
it("steps down one notch", () => {
|
|
21
|
+
expect(stepThinkingLevel("medium", -1)).toBe("low");
|
|
22
|
+
expect(stepThinkingLevel("xhigh", -1)).toBe("high");
|
|
23
|
+
});
|
|
24
|
+
it("clamps at the low end (no wrap)", () => {
|
|
25
|
+
expect(stepThinkingLevel("off", -1)).toBe("off");
|
|
26
|
+
});
|
|
27
|
+
it("clamps at the high end (no wrap)", () => {
|
|
28
|
+
expect(stepThinkingLevel("xhigh", 1)).toBe("xhigh");
|
|
29
|
+
});
|
|
30
|
+
it("falls back to a medium-anchored step for unknown input", () => {
|
|
31
|
+
expect(stepThinkingLevel("bogus", 1)).toBe("high");
|
|
32
|
+
expect(stepThinkingLevel("", -1)).toBe("low");
|
|
33
|
+
});
|
|
34
|
+
it("exposes the six canonical levels ascending", () => {
|
|
35
|
+
expect(THINKING_LEVELS).toEqual(["off", "minimal", "low", "medium", "high", "xhigh"]);
|
|
36
|
+
});
|
|
37
|
+
it("can walk the full ladder up and back down", () => {
|
|
38
|
+
let lvl: string = "off";
|
|
39
|
+
for (let i = 0; i < 10; i++) lvl = stepThinkingLevel(lvl, 1);
|
|
40
|
+
expect(lvl).toBe("xhigh");
|
|
41
|
+
for (let i = 0; i < 10; i++) lvl = stepThinkingLevel(lvl, -1);
|
|
42
|
+
expect(lvl).toBe("off");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("stepEffectiveThinkingLevel", () => {
|
|
47
|
+
it("skips levels that clamp back to the current level", () => {
|
|
48
|
+
const clamp = (requested: string) => {
|
|
49
|
+
if (requested === "minimal" || requested === "low") return "off";
|
|
50
|
+
return requested;
|
|
51
|
+
};
|
|
52
|
+
expect(stepEffectiveThinkingLevel("off", 1, clamp)).toBe("medium");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("stays at an endpoint when every farther request clamps back", () => {
|
|
56
|
+
expect(stepEffectiveThinkingLevel("high", 1, () => "high")).toBe("high");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
12
60
|
describe("fmtCtx", () => {
|
|
13
61
|
it("formats 0 as 0", () => expect(fmtCtx(0)).toBe("0"));
|
|
14
62
|
it("formats small numbers as-is", () => expect(fmtCtx(512)).toBe("512"));
|
package/src/models.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
12
12
|
import {
|
|
13
13
|
fuzzyFilter,
|
|
14
14
|
Input,
|
|
15
|
+
Key,
|
|
15
16
|
matchesKey,
|
|
16
17
|
type SelectItem,
|
|
17
18
|
SelectList,
|
|
@@ -92,6 +93,49 @@ export function normalizeModelText(s: string): string {
|
|
|
92
93
|
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
// ─── Thinking level control ──────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Canonical thinking levels, ascending. Shift+←/→ in the picker steps through this
|
|
100
|
+
* list; pi.setThinkingLevel() clamps to what the active model actually supports,
|
|
101
|
+
* so visiting an unsupported rung is harmless (it lands on the nearest allowed).
|
|
102
|
+
*/
|
|
103
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
104
|
+
|
|
105
|
+
export type ThinkingLevelName = (typeof THINKING_LEVELS)[number];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Step the thinking level one notch. `dir` is -1 (←) or +1 (→). Clamps at the
|
|
109
|
+
* ends (no wraparound) so ← at "off" stays "off" and → at "xhigh" stays "xhigh".
|
|
110
|
+
* Unknown input falls back to "medium" as a neutral midpoint.
|
|
111
|
+
*/
|
|
112
|
+
export function stepThinkingLevel(current: string, dir: -1 | 1): ThinkingLevelName {
|
|
113
|
+
const idx = THINKING_LEVELS.indexOf(current as ThinkingLevelName);
|
|
114
|
+
const base = idx === -1 ? THINKING_LEVELS.indexOf("medium") : idx;
|
|
115
|
+
const next = Math.min(THINKING_LEVELS.length - 1, Math.max(0, base + dir));
|
|
116
|
+
return THINKING_LEVELS[next] as ThinkingLevelName;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Move one effective notch in a direction after the host clamps unsupported
|
|
121
|
+
* levels. Keeps trying farther rungs until the model's resolved level changes.
|
|
122
|
+
*/
|
|
123
|
+
export function stepEffectiveThinkingLevel(
|
|
124
|
+
current: string,
|
|
125
|
+
dir: -1 | 1,
|
|
126
|
+
apply: (level: ThinkingLevelName) => string,
|
|
127
|
+
): ThinkingLevelName {
|
|
128
|
+
let candidate = stepThinkingLevel(current, dir);
|
|
129
|
+
while (candidate !== current) {
|
|
130
|
+
const effective = apply(candidate) as ThinkingLevelName;
|
|
131
|
+
if (effective !== current) return effective;
|
|
132
|
+
const farther = stepThinkingLevel(candidate, dir);
|
|
133
|
+
if (farther === candidate) break;
|
|
134
|
+
candidate = farther;
|
|
135
|
+
}
|
|
136
|
+
return current as ThinkingLevelName;
|
|
137
|
+
}
|
|
138
|
+
|
|
95
139
|
export type ModelSearchLookup = {
|
|
96
140
|
/** benchlm local rank per item value (ranked models only). */
|
|
97
141
|
rankByValue: Map<string, number>;
|
|
@@ -227,7 +271,7 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
227
271
|
// items built inside the custom() factory so we have theme access for colors
|
|
228
272
|
|
|
229
273
|
const result = await ctx.ui.custom<string | null>(
|
|
230
|
-
(
|
|
274
|
+
(tui, theme, _kb, done) => {
|
|
231
275
|
const accent = "accent";
|
|
232
276
|
|
|
233
277
|
// Find max rank width across all benchmarked rows for # padding
|
|
@@ -365,6 +409,27 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
365
409
|
internal.invalidate();
|
|
366
410
|
};
|
|
367
411
|
|
|
412
|
+
// Live thinking-level readout. Shift+←/→ mutates the session immediately via
|
|
413
|
+
// pi.setThinkingLevel(); we mirror pi.getThinkingLevel() so the header
|
|
414
|
+
// reflects the clamped result (model may not support every rung).
|
|
415
|
+
//
|
|
416
|
+
// `local` shadows the level so the header updates even on builds/contexts
|
|
417
|
+
// where pi.getThinkingLevel() lags or is unavailable inside the overlay.
|
|
418
|
+
// We seed it from the getter, then advance it in lock-step with each
|
|
419
|
+
// setThinkingLevel() call and reconcile back to the getter when present.
|
|
420
|
+
let localLevel = pi.getThinkingLevel?.() ?? "";
|
|
421
|
+
const thinkLine = () => {
|
|
422
|
+
const live = pi.getThinkingLevel?.();
|
|
423
|
+
const resolved = live ?? localLevel;
|
|
424
|
+
const label = resolved || "—";
|
|
425
|
+
const coloredLabel = resolved
|
|
426
|
+
? theme.getThinkingBorderColor(resolved)(label)
|
|
427
|
+
: theme.fg("dim", label);
|
|
428
|
+
return (
|
|
429
|
+
theme.fg("muted", "Thinking: ") + coloredLabel + theme.fg("dim", " (shift+←/→ adjust)")
|
|
430
|
+
);
|
|
431
|
+
};
|
|
432
|
+
|
|
368
433
|
return {
|
|
369
434
|
render(w: number) {
|
|
370
435
|
const mw = modalWidth(w);
|
|
@@ -372,10 +437,14 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
372
437
|
const lines: string[] = [
|
|
373
438
|
theme.fg(accent, theme.bold(`${icon("picker.model")} Select model`)),
|
|
374
439
|
theme.fg("dim", "context · pricing · coding rank & score from modelgrep.com"),
|
|
440
|
+
thinkLine(),
|
|
375
441
|
theme.fg("muted", "Search:"),
|
|
376
442
|
...search.render(inner),
|
|
377
443
|
...list.render(inner),
|
|
378
|
-
theme.fg(
|
|
444
|
+
theme.fg(
|
|
445
|
+
"dim",
|
|
446
|
+
"fuzzy search · ↑↓ navigate · shift+←/→ thinking · enter select · esc cancel",
|
|
447
|
+
),
|
|
379
448
|
];
|
|
380
449
|
return frameLines({
|
|
381
450
|
width: mw,
|
|
@@ -393,7 +462,23 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
393
462
|
// SelectList uses. Arrows arrive as named keys ("up"/"down"),
|
|
394
463
|
// not raw escape sequences, so string-equality checks fail.
|
|
395
464
|
const isNav = matchesKey(data, "up") || matchesKey(data, "down");
|
|
396
|
-
|
|
465
|
+
// Shift+←/→ tunes the ACTIVE session model's thinking level without
|
|
466
|
+
// stealing plain ←/→ cursor movement from search. setThinkingLevel clamps
|
|
467
|
+
// to model capability, so unsupported rungs land on the nearest allowed.
|
|
468
|
+
let dir: -1 | 1 | 0 = 0;
|
|
469
|
+
if (matchesKey(data, Key.shift(Key.left))) dir = -1;
|
|
470
|
+
else if (matchesKey(data, Key.shift(Key.right))) dir = 1;
|
|
471
|
+
if (dir !== 0) {
|
|
472
|
+
const cur = pi.getThinkingLevel?.() || localLevel || "medium";
|
|
473
|
+
localLevel = stepEffectiveThinkingLevel(cur, dir, (candidate) => {
|
|
474
|
+
pi.setThinkingLevel(candidate);
|
|
475
|
+
return pi.getThinkingLevel();
|
|
476
|
+
});
|
|
477
|
+
// setThinkingLevel doesn't repaint this overlay, so force a render
|
|
478
|
+
// now — otherwise the header shows a stale level until the next key.
|
|
479
|
+
tui.requestRender();
|
|
480
|
+
return;
|
|
481
|
+
} else if (isNav || matchesKey(data, "enter")) {
|
|
397
482
|
list.handleInput?.(data);
|
|
398
483
|
} else if (matchesKey(data, "escape")) {
|
|
399
484
|
done(null);
|
package/src/once.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-instance idempotency guard for extension activation.
|
|
3
|
-
*
|
|
4
|
-
* pix-core (the meta-package) invokes this package's factory, and a standalone
|
|
5
|
-
* install makes Pi invoke it again — sometimes against the SAME `pi`. We must
|
|
6
|
-
* dedupe that. But Pi rebuilds the extension runtime on /new, /resume, /fork,
|
|
7
|
-
* and /reload, handing the factory a BRAND-NEW `pi`; that must re-register.
|
|
8
|
-
*
|
|
9
|
-
* Keying the registry on the `pi` instance satisfies both: same instance =>
|
|
10
|
-
* skip, new instance => run. The registry lives on globalThis because jiti
|
|
11
|
-
* (`moduleCache: false`) re-evaluates this module on every load pass, so a
|
|
12
|
-
* module-scoped WeakMap would not be shared between the aggregator pass and the
|
|
13
|
-
* standalone pass within a single session.
|
|
14
|
-
*/
|
|
15
|
-
export function once(pi: object, key: string, fn: () => void): void {
|
|
16
|
-
const g = globalThis as { __pixOnce?: WeakMap<object, Set<string>> };
|
|
17
|
-
if (!g.__pixOnce) g.__pixOnce = new WeakMap<object, Set<string>>();
|
|
18
|
-
const registry = g.__pixOnce;
|
|
19
|
-
let loaded = registry.get(pi);
|
|
20
|
-
if (!loaded) {
|
|
21
|
-
loaded = new Set<string>();
|
|
22
|
-
registry.set(pi, loaded);
|
|
23
|
-
}
|
|
24
|
-
if (loaded.has(key)) return;
|
|
25
|
-
loaded.add(key);
|
|
26
|
-
fn();
|
|
27
|
-
}
|