@victor-software-house/pi-openai-proxy 4.2.3 → 4.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.
Files changed (2) hide show
  1. package/extensions/proxy.ts +72 -151
  2. package/package.json +1 -1
@@ -33,9 +33,6 @@ import {
33
33
  import {
34
34
  type Component,
35
35
  Container,
36
- fuzzyFilter,
37
- getKeybindings,
38
- Input,
39
36
  type SettingItem,
40
37
  SettingsList,
41
38
  Text,
@@ -553,180 +550,104 @@ export default function proxyExtension(pi: ExtensionAPI): void {
553
550
  _currentValue: string,
554
551
  done: (selectedValue?: string) => void,
555
552
  ): Component {
556
- interface ModelEntry {
557
- canonical: string;
558
- provider: string;
559
- }
553
+ const models = getAvailableModels();
554
+ const selected = new Set(config.customModels);
560
555
 
561
- const models: ModelEntry[] = getAvailableModels().map((m) => ({
562
- canonical: `${m.provider}/${m.id}`,
563
- provider: m.provider,
564
- }));
556
+ // Build provider order from sorted model list
557
+ const providerOrder: string[] = [];
558
+ for (const m of models) {
559
+ if (!providerOrder.includes(m.provider)) providerOrder.push(m.provider);
560
+ }
561
+
562
+ const items: SettingItem[] = models.map((m) => {
563
+ const canonical = `${m.provider}/${m.id}`;
564
+ return {
565
+ id: canonical,
566
+ label: canonical,
567
+ description: `Provider: ${m.provider} | Left/Right: jump provider`,
568
+ currentValue: selected.has(canonical) ? "[x]" : "[ ]",
569
+ values: ["[x]", "[ ]"],
570
+ };
571
+ });
565
572
 
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
- }
573
+ const list = new SettingsList(
574
+ items,
575
+ Math.min(items.length + 2, 20),
576
+ getSettingsListTheme(),
577
+ (id: string, newValue: string) => {
578
+ if (newValue === "[x]") {
579
+ selected.add(id);
580
+ } else {
581
+ selected.delete(id);
582
+ }
583
+ config = { ...config, customModels: [...selected] };
584
+ saveConfigToFile(config);
585
+ config = loadConfigFromFile();
586
+ },
587
+ () => done(`${String(selected.size)} selected`),
588
+ { enableSearch: true },
589
+ );
587
590
 
591
+ // SettingsList has no public selectedIndex setter.
592
+ // Access internals via bracket notation for provider jumping.
588
593
  function jumpProvider(direction: "prev" | "next"): void {
589
- if (filtered.length === 0) return;
590
- const { firstOf, lastOf } = providerBoundaries(filtered);
591
- const current = filtered[selectedIndex];
594
+ const sl = list as unknown as Record<string, unknown>;
595
+ const idx = sl["selectedIndex"] as number;
596
+ const display = (
597
+ (sl["searchEnabled"] as boolean) ? sl["filteredItems"] : sl["items"]
598
+ ) as SettingItem[];
599
+ if (display.length === 0) return;
600
+
601
+ const current = display[idx];
592
602
  if (current === undefined) return;
603
+ const currentProv = current.id.split("/")[0] ?? "";
604
+ const provIdx = providerOrder.indexOf(currentProv);
593
605
 
594
- const providers = [...firstOf.keys()]; // insertion-order = list order
595
- const provIdx = providers.indexOf(current.provider);
596
-
606
+ let target: number;
597
607
  if (direction === "prev") {
598
608
  if (provIdx <= 0) {
599
- selectedIndex = 0;
609
+ target = 0;
600
610
  } else {
601
- const prevProvider = providers[provIdx - 1];
602
- if (prevProvider !== undefined) {
603
- selectedIndex = lastOf.get(prevProvider) ?? 0;
611
+ const prev = providerOrder[provIdx - 1] ?? "";
612
+ target = 0;
613
+ for (let i = display.length - 1; i >= 0; i--) {
614
+ if (display[i]?.id.startsWith(`${prev}/`) === true) {
615
+ target = i;
616
+ break;
617
+ }
604
618
  }
605
619
  }
606
620
  } else {
607
- if (provIdx >= providers.length - 1) {
608
- selectedIndex = filtered.length - 1;
621
+ if (provIdx >= providerOrder.length - 1) {
622
+ target = display.length - 1;
609
623
  } else {
610
- const nextProvider = providers[provIdx + 1];
611
- if (nextProvider !== undefined) {
612
- selectedIndex = firstOf.get(nextProvider) ?? filtered.length - 1;
624
+ const next = providerOrder[provIdx + 1] ?? "";
625
+ target = display.length - 1;
626
+ for (let i = 0; i < display.length; i++) {
627
+ if (display[i]?.id.startsWith(`${next}/`) === true) {
628
+ target = i;
629
+ break;
630
+ }
613
631
  }
614
632
  }
615
633
  }
634
+ sl["selectedIndex"] = target;
616
635
  }
617
636
 
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();
641
-
642
637
  return {
643
638
  render(width: number): string[] {
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;
639
+ return list.render(width);
640
+ },
641
+ invalidate(): void {
642
+ list.invalidate();
694
643
  },
695
- invalidate(): void {},
696
644
  handleInput(data: string): void {
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
645
+ if (data === "\x1B[D") {
718
646
  jumpProvider("prev");
719
647
  } else if (data === "\x1B[C") {
720
- // Right arrow: next provider
721
648
  jumpProvider("next");
722
- } else if (kb.matches(data, "tui.select.confirm") || data === " ") {
723
- toggle(selectedIndex);
724
649
  } else {
725
- const sanitized = data.replace(/ /g, "");
726
- if (sanitized !== "") {
727
- searchInput.handleInput(sanitized);
728
- applyFilter(searchInput.getValue());
729
- }
650
+ list.handleInput(data);
730
651
  }
731
652
  },
732
653
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-openai-proxy",
3
- "version": "4.2.3",
3
+ "version": "4.2.4",
4
4
  "description": "OpenAI-compatible HTTP proxy for pi's multi-provider model registry",
5
5
  "license": "MIT",
6
6
  "author": "Victor Software House",