@oh-my-pi/pi-coding-agent 16.4.5 → 16.4.6
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/CHANGELOG.md +29 -0
- package/dist/cli.js +3052 -3005
- package/dist/types/cli/bench-cli.d.ts +1 -7
- package/dist/types/cli/usage-cli.d.ts +1 -0
- package/dist/types/commands/usage.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/model-browser.d.ts +3 -0
- package/dist/types/modes/components/model-hub.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/cli/bench-cli.ts +7 -26
- package/src/cli/usage-cli.ts +11 -0
- package/src/commands/usage.ts +13 -2
- package/src/config/settings-schema.ts +1 -1
- package/src/modes/components/advisor-config.ts +3 -1
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/model-browser.ts +114 -33
- package/src/modes/components/model-hub.ts +455 -108
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +20 -13
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +5 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +19 -20
- package/src/session/agent-session.ts +184 -48
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/slash-commands/builtin-registry.ts +9 -0
|
@@ -13,7 +13,7 @@ import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
|
13
13
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
14
14
|
import {
|
|
15
15
|
type Component,
|
|
16
|
-
|
|
16
|
+
fuzzyRank,
|
|
17
17
|
Input,
|
|
18
18
|
matchesKey,
|
|
19
19
|
ScrollView,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
25
25
|
import { getRoleInfo, MODEL_ROLE_IDS } from "../../config/model-roles";
|
|
26
26
|
import type { Settings } from "../../config/settings";
|
|
27
|
+
import type { ModelPerfStats } from "../../session/agent-storage";
|
|
27
28
|
import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
|
|
28
29
|
import { theme } from "../theme/theme";
|
|
29
30
|
import {
|
|
@@ -227,6 +228,18 @@ function formatContext(model: Model): string {
|
|
|
227
228
|
return `${formatNumber(ctx).toLowerCase()} ${theme.icon.context.replace(/:$/, "")}`;
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
/** `118t/s` average output speed; one decimal below 10 t/s. */
|
|
232
|
+
function formatTps(tps: number): string {
|
|
233
|
+
const value = tps >= 10 ? String(Math.round(tps)) : tps.toFixed(1);
|
|
234
|
+
return `${value}t/s`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** `0.9s` average time-to-first-token; whole seconds from 10s up. */
|
|
238
|
+
function formatTtft(ms: number): string {
|
|
239
|
+
const seconds = ms / 1000;
|
|
240
|
+
return seconds >= 10 ? `${Math.round(seconds)}s` : `${seconds.toFixed(1)}s`;
|
|
241
|
+
}
|
|
242
|
+
|
|
230
243
|
/** Pad `text` on the left to `width` terminal columns (ANSI/emoji aware). */
|
|
231
244
|
function padLeftVisible(text: string, width: number): string {
|
|
232
245
|
const missing = width - visibleWidth(text);
|
|
@@ -250,6 +263,12 @@ export interface ModelBrowserOptions {
|
|
|
250
263
|
const LIST_ROW_START = 2;
|
|
251
264
|
/** Rendered rows after the list window: blank + two detail rows. */
|
|
252
265
|
const DETAIL_ROWS = 3;
|
|
266
|
+
/** Row width from which the measured-perf column appears (TPS only). */
|
|
267
|
+
const PERF_TPS_MIN_WIDTH = 76;
|
|
268
|
+
/** Row width from which the perf column also includes TTFT. */
|
|
269
|
+
const PERF_FULL_MIN_WIDTH = 96;
|
|
270
|
+
/** What the per-row perf column shows at the current width. */
|
|
271
|
+
type PerfMode = "off" | "tps" | "full";
|
|
253
272
|
|
|
254
273
|
/**
|
|
255
274
|
* The reusable browser component. Renders a fixed-height block
|
|
@@ -263,6 +282,7 @@ export class ModelBrowser implements Component {
|
|
|
263
282
|
#visibleItems: ModelBrowserItem[] = [];
|
|
264
283
|
#roles: RoleAssignments = {};
|
|
265
284
|
#mruOrder: ReadonlyArray<string> = [];
|
|
285
|
+
#perf: ReadonlyMap<string, ModelPerfStats> = new Map();
|
|
266
286
|
#selectedIndex = 0;
|
|
267
287
|
#hoveredIndex: number | null = null;
|
|
268
288
|
#maxVisible = 10;
|
|
@@ -270,6 +290,7 @@ export class ModelBrowser implements Component {
|
|
|
270
290
|
#currentContextTokens: number;
|
|
271
291
|
#disableOverContext: boolean;
|
|
272
292
|
#emptyText?: () => string | undefined;
|
|
293
|
+
/** First visible list row; panned by the wheel, snapped to the selection on keyboard navigation. */
|
|
273
294
|
#windowStart = 0;
|
|
274
295
|
#windowCount = 0;
|
|
275
296
|
|
|
@@ -310,7 +331,14 @@ export class ModelBrowser implements Component {
|
|
|
310
331
|
this.#mruOrder = order;
|
|
311
332
|
}
|
|
312
333
|
|
|
334
|
+
/** Measured TPS/TTFT averages keyed by `provider/id` selector (see AgentStorage.getModelPerf). */
|
|
335
|
+
setPerfStats(perf: ReadonlyMap<string, ModelPerfStats>): void {
|
|
336
|
+
this.#perf = perf;
|
|
337
|
+
}
|
|
338
|
+
|
|
313
339
|
setMaxVisible(rows: number): void {
|
|
340
|
+
// No selection snap here: hosts call this on every render, and it must
|
|
341
|
+
// not undo wheel panning. render() re-clamps the window.
|
|
314
342
|
this.#maxVisible = Math.max(1, rows);
|
|
315
343
|
}
|
|
316
344
|
|
|
@@ -345,6 +373,7 @@ export class ModelBrowser implements Component {
|
|
|
345
373
|
const index = this.#visibleItems.findIndex(item => item.selector === selector);
|
|
346
374
|
if (index < 0) return false;
|
|
347
375
|
this.#selectedIndex = this.#coerceSelectedIndex(index);
|
|
376
|
+
this.#ensureSelectedVisible();
|
|
348
377
|
return true;
|
|
349
378
|
}
|
|
350
379
|
|
|
@@ -372,6 +401,21 @@ export class ModelBrowser implements Component {
|
|
|
372
401
|
return clamped;
|
|
373
402
|
}
|
|
374
403
|
|
|
404
|
+
/** Clamp a window start into `[0, total - maxVisible]`. */
|
|
405
|
+
#clampWindowStart(start: number): number {
|
|
406
|
+
return Math.max(0, Math.min(start, this.#visibleItems.length - this.#maxVisible));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Scroll just enough to keep the selected row inside the window. */
|
|
410
|
+
#ensureSelectedVisible(): void {
|
|
411
|
+
if (this.#selectedIndex < this.#windowStart) {
|
|
412
|
+
this.#windowStart = this.#selectedIndex;
|
|
413
|
+
} else if (this.#selectedIndex >= this.#windowStart + this.#maxVisible) {
|
|
414
|
+
this.#windowStart = this.#selectedIndex - this.#maxVisible + 1;
|
|
415
|
+
}
|
|
416
|
+
this.#windowStart = this.#clampWindowStart(this.#windowStart);
|
|
417
|
+
}
|
|
418
|
+
|
|
375
419
|
moveSelection(delta: number): void {
|
|
376
420
|
const count = this.#visibleItems.length;
|
|
377
421
|
if (count === 0) return;
|
|
@@ -389,6 +433,7 @@ export class ModelBrowser implements Component {
|
|
|
389
433
|
#setSelectedIndex(index: number): void {
|
|
390
434
|
if (index === this.#selectedIndex) return;
|
|
391
435
|
this.#selectedIndex = index;
|
|
436
|
+
this.#ensureSelectedVisible();
|
|
392
437
|
this.onSelectionChange?.(this.getSelected());
|
|
393
438
|
}
|
|
394
439
|
|
|
@@ -432,16 +477,26 @@ export class ModelBrowser implements Component {
|
|
|
432
477
|
if (query.trim()) {
|
|
433
478
|
// Match against the displayed "provider/id" string so the user can
|
|
434
479
|
// type what they see: bare names, provider prefixes, or scoped
|
|
435
|
-
// queries all flow through the same fuzzy matcher.
|
|
436
|
-
|
|
437
|
-
const matches =
|
|
480
|
+
// queries all flow through the same fuzzy matcher.
|
|
481
|
+
const ranked = fuzzyRank(this.#baseItems, query, ({ provider, id }) => `${provider}/${id}`);
|
|
482
|
+
const matches = ranked.map(result => result.item);
|
|
483
|
+
// Match quality is the primary key while searching: an exact
|
|
484
|
+
// "gpt-5.5" must beat the MRU (or role-assigned) "gpt-5.6", so
|
|
485
|
+
// role rank is skipped and MRU only breaks ties. Scores are
|
|
486
|
+
// bucketed so sub-point position noise (provider-name length)
|
|
487
|
+
// can't split equally good matches; within a bucket the stable
|
|
488
|
+
// sort keeps sortModelItems' MRU/version order.
|
|
438
489
|
sortModelItems(matches, { roles: this.#roles, mruOrder: this.#mruOrder, skipRoleRank: true });
|
|
490
|
+
const buckets = new Map<ModelBrowserItem, number>();
|
|
491
|
+
for (const result of ranked) buckets.set(result.item, Math.round(result.score / 10));
|
|
492
|
+
matches.sort((a, b) => (buckets.get(a) ?? 0) - (buckets.get(b) ?? 0));
|
|
439
493
|
items = matches;
|
|
440
494
|
} else {
|
|
441
495
|
items = this.#baseItems;
|
|
442
496
|
}
|
|
443
497
|
this.#visibleItems = this.#insertSeparator(items);
|
|
444
498
|
this.#selectedIndex = this.#coerceSelectedIndex(Math.min(this.#selectedIndex, this.#visibleItems.length - 1));
|
|
499
|
+
this.#ensureSelectedVisible();
|
|
445
500
|
this.onSelectionChange?.(this.getSelected());
|
|
446
501
|
}
|
|
447
502
|
|
|
@@ -499,36 +554,37 @@ export class ModelBrowser implements Component {
|
|
|
499
554
|
*/
|
|
500
555
|
routeMouse(event: SgrMouseEvent, line: number): void {
|
|
501
556
|
if (event.wheel !== null) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const listLine = line - LIST_ROW_START;
|
|
506
|
-
if (listLine < 0 || listLine >= this.#windowCount) {
|
|
507
|
-
if (event.motion && this.#hoveredIndex !== null) {
|
|
508
|
-
this.#hoveredIndex = null;
|
|
509
|
-
}
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
const index = this.#windowStart + listLine;
|
|
513
|
-
const item = this.#visibleItems[index];
|
|
514
|
-
if (!item || this.#isDisabled(item)) {
|
|
515
|
-
this.#hoveredIndex = null;
|
|
557
|
+
// Wheel pans the window; it never moves the selection and never wraps.
|
|
558
|
+
this.#windowStart = this.#clampWindowStart(this.#windowStart + event.wheel);
|
|
559
|
+
this.#hoveredIndex = this.#hoverIndexAt(line);
|
|
516
560
|
return;
|
|
517
561
|
}
|
|
518
562
|
if (event.motion) {
|
|
519
|
-
this.#hoveredIndex =
|
|
563
|
+
this.#hoveredIndex = this.#hoverIndexAt(line);
|
|
520
564
|
return;
|
|
521
565
|
}
|
|
522
|
-
if (event.leftClick)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
566
|
+
if (!event.leftClick) return;
|
|
567
|
+
const index = this.#hoverIndexAt(line);
|
|
568
|
+
const item = index !== null ? this.#visibleItems[index] : undefined;
|
|
569
|
+
if (index === null || !item) return;
|
|
570
|
+
// Settings idiom: click selects, click-again activates.
|
|
571
|
+
if (index === this.#selectedIndex) {
|
|
572
|
+
this.onActivate?.(item);
|
|
573
|
+
} else {
|
|
574
|
+
this.#setSelectedIndex(index);
|
|
529
575
|
}
|
|
530
576
|
}
|
|
531
577
|
|
|
578
|
+
/** List index under a frame-local row, or null when off-list or on a disabled row. */
|
|
579
|
+
#hoverIndexAt(line: number): number | null {
|
|
580
|
+
const listLine = line - LIST_ROW_START;
|
|
581
|
+
if (listLine < 0 || listLine >= this.#windowCount) return null;
|
|
582
|
+
const index = this.#windowStart + listLine;
|
|
583
|
+
const item = this.#visibleItems[index];
|
|
584
|
+
if (!item || this.#isDisabled(item)) return null;
|
|
585
|
+
return index;
|
|
586
|
+
}
|
|
587
|
+
|
|
532
588
|
#chipsFor(model: Model): string {
|
|
533
589
|
const parts: string[] = [];
|
|
534
590
|
const seen = new Set<string>();
|
|
@@ -545,6 +601,16 @@ export class ModelBrowser implements Component {
|
|
|
545
601
|
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
546
602
|
}
|
|
547
603
|
|
|
604
|
+
/** `0.9s 118t/s` measured-perf cell for the row's meta block; empty when unmeasured or the column is off. */
|
|
605
|
+
#perfCell(item: ModelBrowserItem, mode: PerfMode): string {
|
|
606
|
+
if (mode === "off") return "";
|
|
607
|
+
const perf = this.#perf.get(item.selector);
|
|
608
|
+
if (!perf) return "";
|
|
609
|
+
const tps = formatTps(perf.tps);
|
|
610
|
+
if (mode === "full" && perf.ttftMs !== null) return `${formatTtft(perf.ttftMs)} ${tps}`;
|
|
611
|
+
return tps;
|
|
612
|
+
}
|
|
613
|
+
|
|
548
614
|
#renderRow(
|
|
549
615
|
item: ModelBrowserItem,
|
|
550
616
|
width: number,
|
|
@@ -552,6 +618,8 @@ export class ModelBrowser implements Component {
|
|
|
552
618
|
hovered: boolean,
|
|
553
619
|
ctxWidth: number,
|
|
554
620
|
costWidth: number,
|
|
621
|
+
perfWidth: number,
|
|
622
|
+
perfMode: PerfMode,
|
|
555
623
|
): string {
|
|
556
624
|
if (item.id === "separator") {
|
|
557
625
|
const dashCount = Math.max(0, width - 4);
|
|
@@ -567,8 +635,11 @@ export class ModelBrowser implements Component {
|
|
|
567
635
|
: "";
|
|
568
636
|
let left = `${prefix}${providerPrefix}${name}${this.#chipsFor(item.model)}${overLimit}`;
|
|
569
637
|
|
|
570
|
-
|
|
571
|
-
const
|
|
638
|
+
// Perf column collapses entirely when no visible row has measurements.
|
|
639
|
+
const perfCol =
|
|
640
|
+
perfWidth > 0 ? `${theme.fg("dim", padLeftVisible(this.#perfCell(item, perfMode), perfWidth))} ` : "";
|
|
641
|
+
const meta = `${perfCol}${theme.fg("dim", padLeftVisible(formatContext(item.model), ctxWidth))} ${theme.fg("dim", padLeftVisible(formatCostPair(item.model), costWidth))}`;
|
|
642
|
+
const metaWidth = ctxWidth + costWidth + 2 + (perfWidth > 0 ? perfWidth + 2 : 0);
|
|
572
643
|
const available = Math.max(1, width - metaWidth - 1);
|
|
573
644
|
left = truncateToWidth(left, available);
|
|
574
645
|
const gap = Math.max(0, available - visibleWidth(left));
|
|
@@ -594,6 +665,11 @@ export class ModelBrowser implements Component {
|
|
|
594
665
|
facts.push(`${formatCostPair(model)} per M`);
|
|
595
666
|
if (model.reasoning) facts.push("reasoning");
|
|
596
667
|
if (model.input.includes("image")) facts.push("vision");
|
|
668
|
+
const perf = this.#perf.get(selected.selector);
|
|
669
|
+
if (perf) {
|
|
670
|
+
facts.push(`~${formatTps(perf.tps)}`);
|
|
671
|
+
if (perf.ttftMs !== null) facts.push(`${formatTtft(perf.ttftMs)} ttft`);
|
|
672
|
+
}
|
|
597
673
|
const line1 = truncateToWidth(theme.fg("muted", ` ${facts.join(" · ")}`), width);
|
|
598
674
|
|
|
599
675
|
if (this.#isDisabled(selected)) {
|
|
@@ -626,12 +702,12 @@ export class ModelBrowser implements Component {
|
|
|
626
702
|
lines.push("");
|
|
627
703
|
|
|
628
704
|
const total = this.#visibleItems.length;
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
);
|
|
705
|
+
// The window is persistent state: wheel scrolling panned it, keyboard
|
|
706
|
+
// navigation snapped it to the selection. Re-clamp here because items
|
|
707
|
+
// or maxVisible may have changed since.
|
|
708
|
+
this.#windowStart = this.#clampWindowStart(this.#windowStart);
|
|
709
|
+
const startIndex = this.#windowStart;
|
|
633
710
|
const endIndex = Math.min(startIndex + this.#maxVisible, total);
|
|
634
|
-
this.#windowStart = startIndex;
|
|
635
711
|
this.#windowCount = Math.max(0, endIndex - startIndex);
|
|
636
712
|
|
|
637
713
|
if (total === 0) {
|
|
@@ -644,11 +720,14 @@ export class ModelBrowser implements Component {
|
|
|
644
720
|
// scanning the entire catalog on every render.
|
|
645
721
|
let ctxWidth = 0;
|
|
646
722
|
let costWidth = 0;
|
|
723
|
+
const perfMode: PerfMode = width >= PERF_FULL_MIN_WIDTH ? "full" : width >= PERF_TPS_MIN_WIDTH ? "tps" : "off";
|
|
724
|
+
let perfWidth = 0;
|
|
647
725
|
for (let i = startIndex; i < endIndex; i++) {
|
|
648
726
|
const item = this.#visibleItems[i];
|
|
649
727
|
if (!item) continue;
|
|
650
728
|
ctxWidth = Math.max(ctxWidth, visibleWidth(formatContext(item.model)));
|
|
651
729
|
costWidth = Math.max(costWidth, visibleWidth(formatCostPair(item.model)));
|
|
730
|
+
perfWidth = Math.max(perfWidth, visibleWidth(this.#perfCell(item, perfMode)));
|
|
652
731
|
}
|
|
653
732
|
|
|
654
733
|
const rows: string[] = [];
|
|
@@ -663,6 +742,8 @@ export class ModelBrowser implements Component {
|
|
|
663
742
|
i === this.#hoveredIndex,
|
|
664
743
|
ctxWidth,
|
|
665
744
|
costWidth,
|
|
745
|
+
perfWidth,
|
|
746
|
+
perfMode,
|
|
666
747
|
),
|
|
667
748
|
);
|
|
668
749
|
}
|