@xynogen/pix-models 0.2.2 → 0.2.4
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.ts +175 -166
package/package.json
CHANGED
package/src/models.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
terminalModalHeight,
|
|
30
30
|
} from "@xynogen/pix-pretty/modal-frame";
|
|
31
31
|
import { dotJoin } from "@xynogen/pix-pretty/utils";
|
|
32
|
+
import { SPINNER } from "@xynogen/pix-pretty/widget-format";
|
|
32
33
|
import { patchOutBuiltinModelCommand } from "./patch-builtin";
|
|
33
34
|
|
|
34
35
|
// ─── Pure logic (exported for tests) ─────────────────────────────────────────
|
|
@@ -231,12 +232,13 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
231
232
|
// ModelRegistry instance (verified in runner.js) with refresh() and an
|
|
232
233
|
// async-capable getAvailable(). Reach through the narrowed type.
|
|
233
234
|
type AvailableModels = ReturnType<typeof ctx.modelRegistry.getAvailable>;
|
|
235
|
+
// SAFETY: Runtime ModelRegistry includes refresh and async availability missing from public types.
|
|
234
236
|
const registry = ctx.modelRegistry as unknown as {
|
|
235
237
|
refresh?: () => void;
|
|
236
238
|
getAvailable(): AvailableModels | Promise<AvailableModels>;
|
|
237
239
|
};
|
|
238
240
|
registry.refresh?.();
|
|
239
|
-
|
|
241
|
+
let available = await registry.getAvailable();
|
|
240
242
|
if (available.length === 0) {
|
|
241
243
|
ctx.ui.notify("No models with configured auth.", "warning");
|
|
242
244
|
return;
|
|
@@ -254,196 +256,193 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
254
256
|
// Sort tier: 0 scored, 1 benched-but-unscored, 2 off-catalog.
|
|
255
257
|
tier: 0 | 1 | 2;
|
|
256
258
|
};
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// Local rank = position among scored available models (best pickable = #1).
|
|
287
|
-
let localRank = 0;
|
|
288
|
-
for (const r of rows) if (r.bench) r.localRank = ++localRank;
|
|
259
|
+
const buildRows = (models: Awaited<AvailableModels>): Row[] => {
|
|
260
|
+
const rows: Row[] = models.map((m) => {
|
|
261
|
+
const bench = lookupBenchmark(m.id);
|
|
262
|
+
const tier = !bench
|
|
263
|
+
? 2 // off-catalog → absolute bottom (no rank)
|
|
264
|
+
: bench.overallScore == null
|
|
265
|
+
? 1 // benched, unscored → middle
|
|
266
|
+
: 0; // scored → top
|
|
267
|
+
return {
|
|
268
|
+
m,
|
|
269
|
+
dev: lookupModelsDev(m.provider, m.id),
|
|
270
|
+
bench,
|
|
271
|
+
localRank: null,
|
|
272
|
+
tier,
|
|
273
|
+
} satisfies Row;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Mirror sortModels() — score-desc within tier 0, name-asc otherwise.
|
|
277
|
+
rows.sort((a, b) => {
|
|
278
|
+
if (a.tier !== b.tier) return a.tier - b.tier;
|
|
279
|
+
if (a.tier === 0) {
|
|
280
|
+
const sa = a.bench?.overallScore ?? -1;
|
|
281
|
+
const sb = b.bench?.overallScore ?? -1;
|
|
282
|
+
if (sa !== sb) return sb - sa;
|
|
283
|
+
}
|
|
284
|
+
return (a.m.name ?? a.m.id).localeCompare(b.m.name ?? b.m.id);
|
|
285
|
+
});
|
|
289
286
|
|
|
290
|
-
|
|
291
|
-
|
|
287
|
+
// Local rank = position among scored available models (best pickable = #1).
|
|
288
|
+
let localRank = 0;
|
|
289
|
+
for (const row of rows) if (row.bench) row.localRank = ++localRank;
|
|
290
|
+
return rows;
|
|
291
|
+
};
|
|
292
292
|
|
|
293
293
|
// items built inside the custom() factory so we have theme access for colors
|
|
294
294
|
|
|
295
295
|
const result = await ctx.ui.custom<string | null>(
|
|
296
296
|
(tui, theme, _kb, done) => {
|
|
297
297
|
const accent = "accent";
|
|
298
|
-
|
|
299
|
-
// Find max rank width across all benchmarked rows for # padding
|
|
300
|
-
const maxRankWidth = Math.max(
|
|
301
|
-
...dedupedRows.map((r) => (r.localRank ? String(r.localRank).length : 0)),
|
|
302
|
-
1,
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
// Widest cost string so the cost column pads to a common width and the
|
|
306
|
-
// following ⚡score/stars stay column-aligned (e.g. "10.00/50.00" is 11
|
|
307
|
-
// chars — a fixed pad of 10 shifted those rows right by one).
|
|
308
|
-
const maxCostWidth = Math.max(
|
|
309
|
-
...dedupedRows.map((r) => fmtCost(r.dev).length),
|
|
310
|
-
"free".length,
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
// Mute low-info parts (separators, padding, #, ☆) so the actual values pop.
|
|
314
298
|
const mute = (s: string) => theme.fg("muted", s);
|
|
315
299
|
const guide = (key: string, action: string) =>
|
|
316
|
-
theme.fg("text", key) + theme.fg("
|
|
317
|
-
const guideSep = theme.fg("
|
|
318
|
-
|
|
319
|
-
// Track rank per item value so fuzzy results can prioritize ranked models.
|
|
320
|
-
const rankByValue = new Map<string, number>();
|
|
321
|
-
for (const { m, localRank } of dedupedRows) {
|
|
322
|
-
if (localRank) rankByValue.set(`${m.provider}/${m.id}`, localRank);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Clean search haystacks — labels are ANSI-laden and carry the rank cell,
|
|
326
|
-
// so matching runs against raw id+name instead (see filterModelItems).
|
|
327
|
-
const searchTextByValue = new Map<string, string>();
|
|
328
|
-
const normalizedByValue = new Map<string, string>();
|
|
329
|
-
for (const { m } of dedupedRows) {
|
|
330
|
-
const value = `${m.provider}/${m.id}`;
|
|
331
|
-
const text = `${m.id} ${m.name ?? ""}`;
|
|
332
|
-
searchTextByValue.set(value, text);
|
|
333
|
-
normalizedByValue.set(value, normalizeModelText(text));
|
|
334
|
-
}
|
|
300
|
+
theme.fg("text", key) + theme.fg("muted", ` ${action}`);
|
|
301
|
+
const guideSep = theme.fg("muted", " · ");
|
|
335
302
|
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// Ranked models show muted '#' + colored rank. Unranked (no
|
|
341
|
-
// modelgrep entry) show a muted em-dash sized to the rank
|
|
342
|
-
// column, so the model name aligns across rows.
|
|
343
|
-
const marker = isCurrent ? theme.fg(accent, "▶") : " ";
|
|
344
|
-
let rankPrefix: string;
|
|
345
|
-
if (localRank) {
|
|
346
|
-
const rankStr = String(localRank).padEnd(maxRankWidth);
|
|
347
|
-
// Color rank by the model's bench score (same scale as ⚡score),
|
|
348
|
-
// not by list position — keeps the two colors consistent.
|
|
349
|
-
const rankColor = benchScoreColor(bench?.overallScore);
|
|
350
|
-
rankPrefix = mute("#") + theme.fg(rankColor, rankStr);
|
|
351
|
-
} else {
|
|
352
|
-
// Width = "#" + maxRankWidth chars (e.g. "# " or "#——" for 2-digit ranks).
|
|
353
|
-
const dash = "—".padEnd(maxRankWidth, " ");
|
|
354
|
-
rankPrefix = mute("#") + mute(dash);
|
|
355
|
-
}
|
|
356
|
-
// Display model id only; m.provider is routing provider, not part of id.
|
|
357
|
-
// Color the name by bench score so high-scoring models visually pop.
|
|
358
|
-
const nameColor = bench ? benchScoreColor(bench.overallScore) : accent;
|
|
359
|
-
const idColored = theme.fg(nameColor, m.id);
|
|
360
|
-
const label = `${marker} ${rankPrefix} ${idColored}`;
|
|
361
|
-
|
|
362
|
-
// Description: ctx · cost · score stars
|
|
363
|
-
// Colors: ctx muted · cost success (free muted) · score+stars warning
|
|
364
|
-
// Context: provider's `contextWindow` (source of truth) → fallback to modelgrep `dev.limit.context`.
|
|
365
|
-
const ctxRaw = fmtCtx(
|
|
366
|
-
resolveContextWindow(m as { contextWindow?: number }, dev?.limit?.context),
|
|
303
|
+
const buildPickerData = (sourceRows: Row[]) => {
|
|
304
|
+
const maxRankWidth = Math.max(
|
|
305
|
+
...sourceRows.map((row) => (row.localRank ? String(row.localRank).length : 0)),
|
|
306
|
+
1,
|
|
367
307
|
);
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const s = bench.overallScore;
|
|
382
|
-
const scoreColor = benchScoreColor(s);
|
|
383
|
-
let filled = 1;
|
|
384
|
-
if (typeof s === "number") {
|
|
385
|
-
if (s >= 90) filled = 5;
|
|
386
|
-
else if (s >= 80) filled = 4;
|
|
387
|
-
else if (s >= 70) filled = 3;
|
|
388
|
-
else if (s >= 50) filled = 2;
|
|
389
|
-
}
|
|
390
|
-
const starBar = theme.fg(scoreColor, "★".repeat(filled)) + mute("☆".repeat(5 - filled));
|
|
391
|
-
benchSeg = `⚡${theme.fg(scoreColor, String(score))} ${starBar}`;
|
|
308
|
+
const maxCostWidth = Math.max(
|
|
309
|
+
...sourceRows.map((row) => fmtCost(row.dev).length),
|
|
310
|
+
"free".length,
|
|
311
|
+
);
|
|
312
|
+
const rankByValue = new Map<string, number>();
|
|
313
|
+
const searchTextByValue = new Map<string, string>();
|
|
314
|
+
const normalizedByValue = new Map<string, string>();
|
|
315
|
+
for (const { m, localRank } of sourceRows) {
|
|
316
|
+
const value = `${m.provider}/${m.id}`;
|
|
317
|
+
const text = `${m.id} ${m.name ?? ""}`;
|
|
318
|
+
if (localRank) rankByValue.set(value, localRank);
|
|
319
|
+
searchTextByValue.set(value, text);
|
|
320
|
+
normalizedByValue.set(value, normalizeModelText(text));
|
|
392
321
|
}
|
|
393
|
-
const desc = dotJoin([ctxStr, costSeg, benchSeg], mute);
|
|
394
322
|
|
|
323
|
+
const items: SelectItem[] = sourceRows.map(({ m, dev, bench, localRank }) => {
|
|
324
|
+
const isCurrent = current && m.provider === current.provider && m.id === current.id;
|
|
325
|
+
const marker = isCurrent ? theme.fg(accent, "▶") : " ";
|
|
326
|
+
const rankPrefix = localRank
|
|
327
|
+
? mute("#") +
|
|
328
|
+
theme.fg(benchScoreColor(bench?.overallScore), String(localRank).padEnd(maxRankWidth))
|
|
329
|
+
: mute("#") + mute("—".padEnd(maxRankWidth, " "));
|
|
330
|
+
const nameColor = bench ? benchScoreColor(bench.overallScore) : accent;
|
|
331
|
+
const label = `${marker} ${rankPrefix} ${theme.fg(nameColor, m.id)}`;
|
|
332
|
+
const ctxRaw = fmtCtx(
|
|
333
|
+
resolveContextWindow(m as { contextWindow?: number }, dev?.limit?.context),
|
|
334
|
+
);
|
|
335
|
+
const rawCost = fmtCost(dev);
|
|
336
|
+
const costSeg =
|
|
337
|
+
rawCost === "—"
|
|
338
|
+
? theme.fg("muted", "—".padEnd(maxCostWidth))
|
|
339
|
+
: rawCost === "free"
|
|
340
|
+
? mute("free".padEnd(maxCostWidth))
|
|
341
|
+
: theme.fg("success", rawCost.padEnd(maxCostWidth));
|
|
342
|
+
let benchSeg = "";
|
|
343
|
+
if (bench) {
|
|
344
|
+
const score = bench.overallScore ?? "?";
|
|
345
|
+
const scoreColor = benchScoreColor(bench.overallScore);
|
|
346
|
+
const { filled, empty } = benchStars(bench.overallScore);
|
|
347
|
+
const stars = theme.fg(scoreColor, "★".repeat(filled)) + mute("☆".repeat(empty));
|
|
348
|
+
benchSeg = `⚡${theme.fg(scoreColor, String(score))} ${stars}`;
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
value: `${m.provider}/${m.id}`,
|
|
352
|
+
label,
|
|
353
|
+
description: dotJoin([mute(ctxRaw.padStart(4)), costSeg, benchSeg], mute),
|
|
354
|
+
};
|
|
355
|
+
});
|
|
395
356
|
return {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
357
|
+
items,
|
|
358
|
+
rankByValue,
|
|
359
|
+
searchTextByValue,
|
|
360
|
+
normalizedByValue,
|
|
361
|
+
widestLabel: items.reduce((width, item) => Math.max(width, visibleWidth(item.label)), 0),
|
|
399
362
|
};
|
|
400
|
-
}
|
|
363
|
+
};
|
|
401
364
|
|
|
365
|
+
let pickerData = buildPickerData(buildRows(available));
|
|
402
366
|
const currentIdx = current
|
|
403
|
-
? items.findIndex((
|
|
367
|
+
? pickerData.items.findIndex((item) => item.value === `${current.provider}/${current.id}`)
|
|
404
368
|
: 0;
|
|
405
|
-
|
|
406
|
-
// Widest label (visible width, ANSI-stripped) so the model name
|
|
407
|
-
// column never truncates to "…". Add gap headroom.
|
|
408
|
-
const widestLabel = items.reduce((w, it) => Math.max(w, visibleWidth(it.label)), 0);
|
|
409
|
-
|
|
410
369
|
const search = new Input();
|
|
411
370
|
const list = new SelectList(
|
|
412
|
-
items,
|
|
413
|
-
Math.max(1, items.length),
|
|
371
|
+
pickerData.items,
|
|
372
|
+
Math.max(1, pickerData.items.length),
|
|
414
373
|
{
|
|
415
|
-
selectedPrefix: (
|
|
416
|
-
selectedText: (
|
|
417
|
-
description: (
|
|
418
|
-
scrollInfo: (
|
|
419
|
-
noMatch: (
|
|
374
|
+
selectedPrefix: (text) => theme.fg(accent, text),
|
|
375
|
+
selectedText: (text) => theme.fg(accent, text),
|
|
376
|
+
description: (text) => text,
|
|
377
|
+
scrollInfo: (text) => theme.fg("muted", text),
|
|
378
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
420
379
|
},
|
|
421
380
|
{
|
|
422
|
-
minPrimaryColumnWidth: widestLabel + 2,
|
|
423
|
-
maxPrimaryColumnWidth: widestLabel + 2,
|
|
381
|
+
minPrimaryColumnWidth: pickerData.widestLabel + 2,
|
|
382
|
+
maxPrimaryColumnWidth: pickerData.widestLabel + 2,
|
|
424
383
|
},
|
|
425
384
|
);
|
|
426
385
|
if (currentIdx >= 0) list.setSelectedIndex(currentIdx);
|
|
427
|
-
|
|
428
386
|
list.onSelect = (item) => done(item.value);
|
|
429
387
|
list.onCancel = () => done(null);
|
|
430
388
|
search.onEscape = () => done(null);
|
|
431
389
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
390
|
+
type ListInternal = {
|
|
391
|
+
items: SelectItem[];
|
|
392
|
+
filteredItems: SelectItem[];
|
|
393
|
+
selectedIndex: number;
|
|
394
|
+
maxVisible: number;
|
|
395
|
+
layout: { minPrimaryColumnWidth: number; maxPrimaryColumnWidth: number };
|
|
396
|
+
invalidate(): void;
|
|
397
|
+
};
|
|
398
|
+
// SAFETY: SelectList has no public item-replacement API; runtime fields are stable and
|
|
399
|
+
// already used above for custom filtering. Keep this cast local to picker updates.
|
|
400
|
+
const listInternal = list as unknown as ListInternal;
|
|
401
|
+
const applyFuzzy = (query: string, selectedValue?: string) => {
|
|
402
|
+
listInternal.filteredItems = filterModelItems(listInternal.items, query, pickerData);
|
|
403
|
+
const selectedIndex = selectedValue
|
|
404
|
+
? listInternal.filteredItems.findIndex((item) => item.value === selectedValue)
|
|
405
|
+
: -1;
|
|
406
|
+
listInternal.selectedIndex = Math.max(0, selectedIndex);
|
|
407
|
+
listInternal.invalidate();
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const pager = new ModalPager();
|
|
411
|
+
let reloading = false;
|
|
412
|
+
let spinnerFrame = 0;
|
|
413
|
+
const reload = async () => {
|
|
414
|
+
if (reloading) return;
|
|
415
|
+
reloading = true;
|
|
416
|
+
const spinnerTimer = setInterval(() => {
|
|
417
|
+
spinnerFrame = (spinnerFrame + 1) % SPINNER.length;
|
|
418
|
+
tui.requestRender();
|
|
419
|
+
}, 120);
|
|
420
|
+
tui.requestRender();
|
|
421
|
+
try {
|
|
422
|
+
const selectedValue = list.getSelectedItem()?.value;
|
|
423
|
+
registry.refresh?.();
|
|
424
|
+
const refreshed = await registry.getAvailable();
|
|
425
|
+
if (refreshed.length === 0) {
|
|
426
|
+
ctx.ui.notify("No models with configured auth.", "warning");
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
available = refreshed;
|
|
430
|
+
pickerData = buildPickerData(buildRows(available));
|
|
431
|
+
listInternal.items = pickerData.items;
|
|
432
|
+
listInternal.maxVisible = Math.max(1, pickerData.items.length);
|
|
433
|
+
listInternal.layout = {
|
|
434
|
+
minPrimaryColumnWidth: pickerData.widestLabel + 2,
|
|
435
|
+
maxPrimaryColumnWidth: pickerData.widestLabel + 2,
|
|
436
|
+
};
|
|
437
|
+
applyFuzzy(search.getValue?.() ?? "", selectedValue);
|
|
438
|
+
pager.followSelection();
|
|
439
|
+
} catch (error) {
|
|
440
|
+
ctx.ui.notify(`Failed to reload models: ${String(error)}`, "error");
|
|
441
|
+
} finally {
|
|
442
|
+
clearInterval(spinnerTimer);
|
|
443
|
+
reloading = false;
|
|
444
|
+
tui.requestRender();
|
|
445
|
+
}
|
|
447
446
|
};
|
|
448
447
|
|
|
449
448
|
// Live thinking-level readout. ←/→ mutates the session immediately via
|
|
@@ -455,7 +454,6 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
455
454
|
// We seed it from the getter, then advance it in lock-step with each
|
|
456
455
|
// setThinkingLevel() call and reconcile back to the getter when present.
|
|
457
456
|
let localLevel = pi.getThinkingLevel?.() ?? "";
|
|
458
|
-
const pager = new ModalPager();
|
|
459
457
|
const thinkLine = () => {
|
|
460
458
|
const live = pi.getThinkingLevel?.();
|
|
461
459
|
const resolved = live ?? localLevel;
|
|
@@ -464,11 +462,11 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
464
462
|
? theme.getThinkingBorderColor(resolved)(label)
|
|
465
463
|
: theme.fg("dim", label);
|
|
466
464
|
return (
|
|
467
|
-
theme.fg("
|
|
465
|
+
theme.fg("dim", "Thinking: ") +
|
|
468
466
|
coloredLabel +
|
|
469
|
-
theme.fg("
|
|
467
|
+
theme.fg("muted", " (") +
|
|
470
468
|
guide("←/→", "adjust") +
|
|
471
|
-
theme.fg("
|
|
469
|
+
theme.fg("muted", ")")
|
|
472
470
|
);
|
|
473
471
|
};
|
|
474
472
|
|
|
@@ -482,13 +480,16 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
482
480
|
minHeight: MIN_MODAL_HEIGHT,
|
|
483
481
|
header: [
|
|
484
482
|
theme.fg(accent, theme.bold(`${icon("picker.model")} Select model`)),
|
|
485
|
-
|
|
483
|
+
reloading
|
|
484
|
+
? theme.fg("accent", `${SPINNER[spinnerFrame] ?? ""} Reloading models…`)
|
|
485
|
+
: theme.fg("dim", "context · pricing · coding rank & score from modelgrep.com"),
|
|
486
486
|
thinkLine(),
|
|
487
487
|
...search.render(inner),
|
|
488
488
|
"",
|
|
489
489
|
],
|
|
490
490
|
body: list.render(inner),
|
|
491
491
|
selectedBodyRange: (() => {
|
|
492
|
+
// SAFETY: SelectList tracks selectedIndex internally for pager synchronization.
|
|
492
493
|
const internal = list as unknown as { selectedIndex: number };
|
|
493
494
|
return pager.selectedRange({
|
|
494
495
|
start: internal.selectedIndex,
|
|
@@ -501,6 +502,8 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
501
502
|
guideSep +
|
|
502
503
|
guide("←/→", "thinking") +
|
|
503
504
|
guideSep +
|
|
505
|
+
guide("^r", "reload") +
|
|
506
|
+
guideSep +
|
|
504
507
|
guide("enter", "select") +
|
|
505
508
|
guideSep +
|
|
506
509
|
guide("esc", "cancel"),
|
|
@@ -524,6 +527,12 @@ async function showEnhancedPicker(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
524
527
|
const isNav = matchesKey(data, "up") || matchesKey(data, "down");
|
|
525
528
|
// ←/→ tunes the ACTIVE session model's thinking level. setThinkingLevel
|
|
526
529
|
// clamps to model capability, so unsupported rungs land on the nearest allowed.
|
|
530
|
+
// ctrl+r refreshes and replaces list data without disposing the overlay.
|
|
531
|
+
if (matchesKey(data, Key.ctrl("r"))) {
|
|
532
|
+
void reload();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (reloading) return;
|
|
527
536
|
let dir: -1 | 1 | 0 = 0;
|
|
528
537
|
if (matchesKey(data, Key.left)) dir = -1;
|
|
529
538
|
else if (matchesKey(data, Key.right)) dir = 1;
|