@victor-software-house/pi-openai-proxy 4.2.2 → 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 +168 -30
  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,46 +553,181 @@ 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);
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
+ }
555
587
 
556
- const items: SettingItem[] = models.map((m) => {
557
- const canonical = `${m.provider}/${m.id}`;
558
- return {
559
- id: canonical,
560
- label: canonical,
561
- currentValue: selected.has(canonical) ? "[x]" : "[ ]",
562
- values: ["[x]", "[ ]"],
563
- };
564
- });
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);
565
596
 
566
- const list = new SettingsList(
567
- items,
568
- Math.min(items.length + 2, 20),
569
- getSettingsListTheme(),
570
- (id: string, newValue: string) => {
571
- if (newValue === "[x]") {
572
- selected.add(id);
597
+ if (direction === "prev") {
598
+ if (provIdx <= 0) {
599
+ selectedIndex = 0;
573
600
  } else {
574
- selected.delete(id);
601
+ const prevProvider = providers[provIdx - 1];
602
+ if (prevProvider !== undefined) {
603
+ selectedIndex = lastOf.get(prevProvider) ?? 0;
604
+ }
575
605
  }
576
- config = { ...config, customModels: [...selected] };
577
- saveConfigToFile(config);
578
- config = loadConfigFromFile();
579
- },
580
- () => done(`${String(selected.size)} selected`),
581
- { enableSearch: true },
582
- );
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
+ }
617
+
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);
623
+ } else {
624
+ selected.add(entry.canonical);
625
+ }
626
+ config = { ...config, customModels: [...selected] };
627
+ saveConfigToFile(config);
628
+ config = loadConfigFromFile();
629
+ }
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();
583
641
 
584
642
  return {
585
643
  render(width: number): string[] {
586
- return list.render(width);
587
- },
588
- invalidate(): void {
589
- list.invalidate();
644
+ const lines: string[] = [];
645
+ lines.push(...searchInput.render(width));
646
+ lines.push("");
647
+
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"));
652
+ return lines;
653
+ }
654
+
655
+ const start = Math.max(
656
+ 0,
657
+ Math.min(selectedIndex - Math.floor(maxVisible / 2), filtered.length - maxVisible),
658
+ );
659
+ const end = Math.min(start + maxVisible, filtered.length);
660
+
661
+ let lastProvider = "";
662
+ for (let i = start; i < end; i++) {
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)})`));
683
+ }
684
+
685
+ lines.push("");
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
+ );
693
+ return lines;
590
694
  },
695
+ invalidate(): void {},
591
696
  handleInput(data: string): void {
592
- list.handleInput(data);
697
+ const kb = getKeybindings();
698
+
699
+ if (kb.matches(data, "tui.select.cancel")) {
700
+ done(`${String(selected.size)} selected`);
701
+ return;
702
+ }
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());
729
+ }
730
+ }
593
731
  },
594
732
  };
595
733
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-openai-proxy",
3
- "version": "4.2.2",
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",