@victor-software-house/pi-openai-proxy 4.2.1 → 4.2.3

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.
Files changed (2) hide show
  1. package/extensions/proxy.ts +144 -33
  2. package/package.json +1 -1
@@ -33,6 +33,9 @@ import {
33
33
  import {
34
34
  type Component,
35
35
  Container,
36
+ fuzzyFilter,
37
+ getKeybindings,
38
+ Input,
36
39
  type SettingItem,
37
40
  SettingsList,
38
41
  Text,
@@ -550,71 +553,179 @@ export default function proxyExtension(pi: ExtensionAPI): void {
550
553
  _currentValue: string,
551
554
  done: (selectedValue?: string) => void,
552
555
  ): Component {
553
- const models = getAvailableModels();
556
+ interface ModelEntry {
557
+ canonical: string;
558
+ provider: string;
559
+ }
560
+
561
+ const models: ModelEntry[] = getAvailableModels().map((m) => ({
562
+ canonical: `${m.provider}/${m.id}`,
563
+ provider: m.provider,
564
+ }));
565
+
554
566
  const selected = new Set(config.customModels);
555
567
  let selectedIndex = 0;
568
+ const searchInput = new Input();
569
+ let filtered: ModelEntry[] = models;
570
+ const maxVisible = 20;
571
+
572
+ // Precompute provider boundary indices in the full list
573
+ function providerBoundaries(list: ModelEntry[]): {
574
+ firstOf: Map<string, number>;
575
+ lastOf: Map<string, number>;
576
+ } {
577
+ const firstOf = new Map<string, number>();
578
+ const lastOf = new Map<string, number>();
579
+ for (let i = 0; i < list.length; i++) {
580
+ const entry = list[i];
581
+ if (entry === undefined) continue;
582
+ if (!firstOf.has(entry.provider)) firstOf.set(entry.provider, i);
583
+ lastOf.set(entry.provider, i);
584
+ }
585
+ return { firstOf, lastOf };
586
+ }
587
+
588
+ function jumpProvider(direction: "prev" | "next"): void {
589
+ if (filtered.length === 0) return;
590
+ const { firstOf, lastOf } = providerBoundaries(filtered);
591
+ const current = filtered[selectedIndex];
592
+ if (current === undefined) return;
593
+
594
+ const providers = [...firstOf.keys()]; // insertion-order = list order
595
+ const provIdx = providers.indexOf(current.provider);
596
+
597
+ if (direction === "prev") {
598
+ if (provIdx <= 0) {
599
+ selectedIndex = 0;
600
+ } else {
601
+ const prevProvider = providers[provIdx - 1];
602
+ if (prevProvider !== undefined) {
603
+ selectedIndex = lastOf.get(prevProvider) ?? 0;
604
+ }
605
+ }
606
+ } else {
607
+ if (provIdx >= providers.length - 1) {
608
+ selectedIndex = filtered.length - 1;
609
+ } else {
610
+ const nextProvider = providers[provIdx + 1];
611
+ if (nextProvider !== undefined) {
612
+ selectedIndex = firstOf.get(nextProvider) ?? filtered.length - 1;
613
+ }
614
+ }
615
+ }
616
+ }
556
617
 
557
- function toggle(canonicalId: string): void {
558
- if (selected.has(canonicalId)) {
559
- selected.delete(canonicalId);
618
+ function toggle(idx: number): void {
619
+ const entry = filtered[idx];
620
+ if (entry === undefined) return;
621
+ if (selected.has(entry.canonical)) {
622
+ selected.delete(entry.canonical);
560
623
  } else {
561
- selected.add(canonicalId);
624
+ selected.add(entry.canonical);
562
625
  }
563
626
  config = { ...config, customModels: [...selected] };
564
627
  saveConfigToFile(config);
565
628
  config = loadConfigFromFile();
566
629
  }
567
630
 
631
+ function applyFilter(query: string): void {
632
+ if (query === "") {
633
+ filtered = models;
634
+ } else {
635
+ filtered = fuzzyFilter(models, query, (m) => m.canonical);
636
+ }
637
+ selectedIndex = 0;
638
+ }
639
+
640
+ const theme = getSettingsListTheme();
641
+
568
642
  return {
569
643
  render(width: number): string[] {
570
644
  const lines: string[] = [];
571
- lines.push(" Select models (Space: toggle, Esc: done)");
645
+ lines.push(...searchInput.render(width));
572
646
  lines.push("");
573
647
 
574
- if (models.length === 0) {
575
- lines.push(" No models available (no auth configured)");
648
+ if (filtered.length === 0) {
649
+ lines.push(theme.hint(" No matching models"));
650
+ lines.push("");
651
+ lines.push(theme.hint(" Type to filter | Left/Right: jump provider | Esc: done"));
576
652
  return lines;
577
653
  }
578
654
 
579
- const maxVisible = 15;
580
655
  const start = Math.max(
581
656
  0,
582
- Math.min(selectedIndex - Math.floor(maxVisible / 2), models.length - maxVisible),
657
+ Math.min(selectedIndex - Math.floor(maxVisible / 2), filtered.length - maxVisible),
583
658
  );
584
- const end = Math.min(start + maxVisible, models.length);
659
+ const end = Math.min(start + maxVisible, filtered.length);
585
660
 
661
+ let lastProvider = "";
586
662
  for (let i = start; i < end; i++) {
587
- const m = models[i];
588
- if (m === undefined) continue;
589
- const canonical = `${m.provider}/${m.id}`;
590
- const check = selected.has(canonical) ? "[x]" : "[ ]";
591
- const cursor = i === selectedIndex ? "> " : " ";
592
- const line = `${cursor}${check} ${canonical}`;
593
- const truncated = line.length > width ? line.slice(0, width - 1) : line;
594
- lines.push(truncated);
663
+ const entry = filtered[i];
664
+ if (entry === undefined) continue;
665
+
666
+ // Provider separator
667
+ if (entry.provider !== lastProvider) {
668
+ if (lastProvider !== "") lines.push("");
669
+ lastProvider = entry.provider;
670
+ }
671
+
672
+ const check = selected.has(entry.canonical) ? "[x]" : "[ ]";
673
+ const cursor = i === selectedIndex ? theme.cursor : " ";
674
+ const label =
675
+ i === selectedIndex
676
+ ? theme.label(`${check} ${entry.canonical}`, true)
677
+ : theme.label(`${check} ${entry.canonical}`, false);
678
+ lines.push(`${cursor}${label}`.slice(0, width));
679
+ }
680
+
681
+ if (start > 0 || end < filtered.length) {
682
+ lines.push(theme.hint(` (${String(selectedIndex + 1)}/${String(filtered.length)})`));
595
683
  }
596
684
 
597
685
  lines.push("");
598
- lines.push(` ${String(selected.size)} of ${String(models.length)} selected`);
686
+ lines.push(
687
+ theme.description(` ${String(selected.size)} of ${String(models.length)} selected`),
688
+ );
689
+ lines.push("");
690
+ lines.push(
691
+ theme.hint(" Type to filter | Space: toggle | Left/Right: jump provider | Esc: done"),
692
+ );
599
693
  return lines;
600
694
  },
601
- invalidate(): void {
602
- // no-op
603
- },
695
+ invalidate(): void {},
604
696
  handleInput(data: string): void {
605
- if (data === "\x1B" || data === "q") {
697
+ const kb = getKeybindings();
698
+
699
+ if (kb.matches(data, "tui.select.cancel")) {
606
700
  done(`${String(selected.size)} selected`);
607
701
  return;
608
702
  }
609
- if (models.length === 0) return;
610
- if (data === "\x1B[A") {
611
- selectedIndex = selectedIndex <= 0 ? models.length - 1 : selectedIndex - 1;
612
- } else if (data === "\x1B[B") {
613
- selectedIndex = selectedIndex >= models.length - 1 ? 0 : selectedIndex + 1;
614
- } else if (data === " " || data === "\r") {
615
- const m = models[selectedIndex];
616
- if (m !== undefined) {
617
- toggle(`${m.provider}/${m.id}`);
703
+ if (filtered.length === 0) {
704
+ // Still allow typing to refine filter
705
+ const sanitized = data.replace(/ /g, "");
706
+ if (sanitized !== "") {
707
+ searchInput.handleInput(sanitized);
708
+ applyFilter(searchInput.getValue());
709
+ }
710
+ return;
711
+ }
712
+ if (kb.matches(data, "tui.select.up")) {
713
+ selectedIndex = selectedIndex <= 0 ? filtered.length - 1 : selectedIndex - 1;
714
+ } else if (kb.matches(data, "tui.select.down")) {
715
+ selectedIndex = selectedIndex >= filtered.length - 1 ? 0 : selectedIndex + 1;
716
+ } else if (data === "\x1B[D") {
717
+ // Left arrow: previous provider
718
+ jumpProvider("prev");
719
+ } else if (data === "\x1B[C") {
720
+ // Right arrow: next provider
721
+ jumpProvider("next");
722
+ } else if (kb.matches(data, "tui.select.confirm") || data === " ") {
723
+ toggle(selectedIndex);
724
+ } else {
725
+ const sanitized = data.replace(/ /g, "");
726
+ if (sanitized !== "") {
727
+ searchInput.handleInput(sanitized);
728
+ applyFilter(searchInput.getValue());
618
729
  }
619
730
  }
620
731
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-openai-proxy",
3
- "version": "4.2.1",
3
+ "version": "4.2.3",
4
4
  "description": "OpenAI-compatible HTTP proxy for pi's multi-provider model registry",
5
5
  "license": "MIT",
6
6
  "author": "Victor Software House",