@xynogen/pix-models 0.1.18 → 0.1.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-models",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Pi extension — enhanced /models picker with BenchLM ranks",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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
- (_tui, theme, _kb, done) => {
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("dim", "fuzzy search · ↑↓ navigate · enter select · esc cancel"),
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
- if (isNav || matchesKey(data, "enter")) {
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);