@nicknisi/pi-model-switch 0.2.0 → 0.2.1
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/dist/section-picker.d.ts
CHANGED
|
@@ -16,7 +16,10 @@ export declare class SectionPicker implements Component {
|
|
|
16
16
|
private readonly selectTheme;
|
|
17
17
|
constructor(sections: PickerSection[], theme: Theme, done: (value: string | null) => void);
|
|
18
18
|
private renderTabBar;
|
|
19
|
+
private wireSelectList;
|
|
19
20
|
private switchSection;
|
|
21
|
+
private filteredItems;
|
|
22
|
+
private rebuildList;
|
|
20
23
|
render(width: number): string[];
|
|
21
24
|
invalidate(): void;
|
|
22
25
|
handleInput(data: string): void;
|
package/dist/section-picker.js
CHANGED
|
@@ -21,8 +21,7 @@ export class SectionPicker {
|
|
|
21
21
|
noMatch: (text) => theme.fg('warning', text),
|
|
22
22
|
};
|
|
23
23
|
this.selectList = new SelectList(sections[0]?.items ?? [], 10, this.selectTheme);
|
|
24
|
-
this.
|
|
25
|
-
this.selectList.onCancel = () => this.done(null);
|
|
24
|
+
this.wireSelectList();
|
|
26
25
|
this.container.addChild(this.tabBar);
|
|
27
26
|
this.container.addChild(this.searchInput);
|
|
28
27
|
this.container.addChild(this.selectList);
|
|
@@ -37,12 +36,42 @@ export class SectionPicker {
|
|
|
37
36
|
});
|
|
38
37
|
this.tabBar.setText(parts.join(''));
|
|
39
38
|
}
|
|
39
|
+
wireSelectList() {
|
|
40
|
+
this.selectList.onSelect = (item) => this.done(item.value);
|
|
41
|
+
this.selectList.onCancel = () => this.done(null);
|
|
42
|
+
}
|
|
40
43
|
switchSection(direction) {
|
|
41
44
|
this.activeSectionIndex = (this.activeSectionIndex + direction + this.sections.length) % this.sections.length;
|
|
42
45
|
this.searchInput.setValue('');
|
|
43
|
-
this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
this.rebuildList();
|
|
47
|
+
}
|
|
48
|
+
filteredItems() {
|
|
49
|
+
const query = this.searchInput.getValue().trim().toLowerCase();
|
|
50
|
+
if (query.length === 0)
|
|
51
|
+
return this.sections[this.activeSectionIndex]?.items ?? [];
|
|
52
|
+
// SelectList.setFilter only matches value.startsWith(query), which can never
|
|
53
|
+
// match a model name inside "provider/modelId" — filter here instead, with a
|
|
54
|
+
// substring match across every section.
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
const matches = [];
|
|
57
|
+
for (const section of this.sections) {
|
|
58
|
+
for (const item of section.items) {
|
|
59
|
+
if (seen.has(item.value) || !item.value.toLowerCase().includes(query))
|
|
60
|
+
continue;
|
|
61
|
+
seen.add(item.value);
|
|
62
|
+
matches.push({ ...item, description: section.name });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return matches;
|
|
66
|
+
}
|
|
67
|
+
rebuildList() {
|
|
68
|
+
// Size the primary column to the content so section descriptions never
|
|
69
|
+
// truncate long provider/modelId labels (SelectList caps it at 32 otherwise).
|
|
70
|
+
this.selectList = new SelectList(this.filteredItems(), 10, this.selectTheme, {
|
|
71
|
+
minPrimaryColumnWidth: 1,
|
|
72
|
+
maxPrimaryColumnWidth: Number.MAX_SAFE_INTEGER,
|
|
73
|
+
});
|
|
74
|
+
this.wireSelectList();
|
|
46
75
|
this.container.clear();
|
|
47
76
|
this.container.addChild(this.tabBar);
|
|
48
77
|
this.container.addChild(this.searchInput);
|
|
@@ -69,7 +98,7 @@ export class SectionPicker {
|
|
|
69
98
|
}
|
|
70
99
|
else {
|
|
71
100
|
this.searchInput.handleInput(data);
|
|
72
|
-
this.
|
|
101
|
+
this.rebuildList();
|
|
73
102
|
}
|
|
74
103
|
}
|
|
75
104
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { SectionPicker } from './section-picker.js';
|
|
3
|
+
const theme = {
|
|
4
|
+
fg: (_color, text) => text,
|
|
5
|
+
bold: (text) => text,
|
|
6
|
+
};
|
|
7
|
+
function sections() {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
name: 'work',
|
|
11
|
+
items: [
|
|
12
|
+
{ value: 'cloudflare-ai-gateway/gpt-5.6-sol', label: ' cloudflare-ai-gateway/gpt-5.6-sol' },
|
|
13
|
+
{ value: 'cloudflare-ai-gateway/claude-opus-5', label: ' cloudflare-ai-gateway/claude-opus-5' },
|
|
14
|
+
{
|
|
15
|
+
value: 'fireworks/accounts/fireworks/models/kimi-k3',
|
|
16
|
+
label: ' fireworks/accounts/fireworks/models/kimi-k3',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'personal',
|
|
22
|
+
items: [
|
|
23
|
+
{
|
|
24
|
+
value: 'fireworks/accounts/fireworks/models/kimi-k3',
|
|
25
|
+
label: ' fireworks/accounts/fireworks/models/kimi-k3',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
function render(picker) {
|
|
32
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escape codes
|
|
33
|
+
return picker
|
|
34
|
+
.render(240)
|
|
35
|
+
.join('\n')
|
|
36
|
+
.replace(/\x1b\[[0-9;]*m/g, '');
|
|
37
|
+
}
|
|
38
|
+
function type(picker, text) {
|
|
39
|
+
for (const char of text)
|
|
40
|
+
picker.handleInput(char);
|
|
41
|
+
}
|
|
42
|
+
describe('SectionPicker filtering', () => {
|
|
43
|
+
it('matches a model name substring inside provider/modelId', () => {
|
|
44
|
+
const picker = new SectionPicker(sections(), theme, vi.fn());
|
|
45
|
+
type(picker, 'claude');
|
|
46
|
+
const output = render(picker);
|
|
47
|
+
expect(output).toContain('cloudflare-ai-gateway/claude-opus-5');
|
|
48
|
+
expect(output).not.toContain('gpt-5.6-sol');
|
|
49
|
+
expect(output).not.toContain('kimi-k3');
|
|
50
|
+
});
|
|
51
|
+
it('searches across all sections and dedupes models present in multiple sections', () => {
|
|
52
|
+
const picker = new SectionPicker(sections(), theme, vi.fn());
|
|
53
|
+
type(picker, 'kimi');
|
|
54
|
+
const output = render(picker);
|
|
55
|
+
const occurrences = output.split('kimi-k3').length - 1;
|
|
56
|
+
expect(occurrences).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
it('shows only the active section when the query is empty', () => {
|
|
59
|
+
const picker = new SectionPicker(sections(), theme, vi.fn());
|
|
60
|
+
type(picker, 'claude');
|
|
61
|
+
type(picker, '\x7f'.repeat(7)); // backspace away the query
|
|
62
|
+
const output = render(picker);
|
|
63
|
+
expect(output).toContain('cloudflare-ai-gateway/claude-opus-5');
|
|
64
|
+
expect(output).toContain('cloudflare-ai-gateway/gpt-5.6-sol');
|
|
65
|
+
});
|
|
66
|
+
it('confirms the highlighted filtered match on enter', () => {
|
|
67
|
+
const done = vi.fn();
|
|
68
|
+
const picker = new SectionPicker(sections(), theme, done);
|
|
69
|
+
type(picker, 'kimi');
|
|
70
|
+
picker.handleInput('\r');
|
|
71
|
+
expect(done).toHaveBeenCalledWith('fireworks/accounts/fireworks/models/kimi-k3');
|
|
72
|
+
});
|
|
73
|
+
});
|
package/package.json
CHANGED
package/section-picker.ts
CHANGED
|
@@ -38,8 +38,7 @@ export class SectionPicker implements Component {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
this.selectList = new SelectList(sections[0]?.items ?? [], 10, this.selectTheme);
|
|
41
|
-
this.
|
|
42
|
-
this.selectList.onCancel = () => this.done(null);
|
|
41
|
+
this.wireSelectList();
|
|
43
42
|
|
|
44
43
|
this.container.addChild(this.tabBar);
|
|
45
44
|
this.container.addChild(this.searchInput);
|
|
@@ -57,12 +56,44 @@ export class SectionPicker implements Component {
|
|
|
57
56
|
this.tabBar.setText(parts.join(''));
|
|
58
57
|
}
|
|
59
58
|
|
|
59
|
+
private wireSelectList(): void {
|
|
60
|
+
this.selectList.onSelect = (item) => this.done(item.value);
|
|
61
|
+
this.selectList.onCancel = () => this.done(null);
|
|
62
|
+
}
|
|
63
|
+
|
|
60
64
|
private switchSection(direction: 1 | -1): void {
|
|
61
65
|
this.activeSectionIndex = (this.activeSectionIndex + direction + this.sections.length) % this.sections.length;
|
|
62
66
|
this.searchInput.setValue('');
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
this.rebuildList();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private filteredItems(): SelectItem[] {
|
|
71
|
+
const query = this.searchInput.getValue().trim().toLowerCase();
|
|
72
|
+
if (query.length === 0) return this.sections[this.activeSectionIndex]?.items ?? [];
|
|
73
|
+
|
|
74
|
+
// SelectList.setFilter only matches value.startsWith(query), which can never
|
|
75
|
+
// match a model name inside "provider/modelId" — filter here instead, with a
|
|
76
|
+
// substring match across every section.
|
|
77
|
+
const seen = new Set<string>();
|
|
78
|
+
const matches: SelectItem[] = [];
|
|
79
|
+
for (const section of this.sections) {
|
|
80
|
+
for (const item of section.items) {
|
|
81
|
+
if (seen.has(item.value) || !item.value.toLowerCase().includes(query)) continue;
|
|
82
|
+
seen.add(item.value);
|
|
83
|
+
matches.push({ ...item, description: section.name });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return matches;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private rebuildList(): void {
|
|
90
|
+
// Size the primary column to the content so section descriptions never
|
|
91
|
+
// truncate long provider/modelId labels (SelectList caps it at 32 otherwise).
|
|
92
|
+
this.selectList = new SelectList(this.filteredItems(), 10, this.selectTheme, {
|
|
93
|
+
minPrimaryColumnWidth: 1,
|
|
94
|
+
maxPrimaryColumnWidth: Number.MAX_SAFE_INTEGER,
|
|
95
|
+
});
|
|
96
|
+
this.wireSelectList();
|
|
66
97
|
this.container.clear();
|
|
67
98
|
this.container.addChild(this.tabBar);
|
|
68
99
|
this.container.addChild(this.searchInput);
|
|
@@ -94,7 +125,7 @@ export class SectionPicker implements Component {
|
|
|
94
125
|
this.selectList.handleInput(data);
|
|
95
126
|
} else {
|
|
96
127
|
this.searchInput.handleInput(data);
|
|
97
|
-
this.
|
|
128
|
+
this.rebuildList();
|
|
98
129
|
}
|
|
99
130
|
}
|
|
100
131
|
}
|