pi-cliproxyapi 0.2.0 → 0.3.0

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.
@@ -1,247 +0,0 @@
1
- // Three-panel picker: state, navigation, render, input dispatch.
2
- //
3
- // Layout (drawn by render()):
4
- // \u250c\u2500 providers \u2500\u252c\u2500 assigned to <prov> \u2500\u2510
5
- // \u2502 list ... \u2502 list ... \u2502
6
- // \u2502 \u251c\u2500 available pool \u2500\u2500\u2500\u2500\u2500\u2524
7
- // \u2502 \u2502 grouped by owned_by \u2502
8
- // \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
9
-
10
- import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
11
- import {
12
- type Component,
13
- getKeybindings,
14
- matchesKey,
15
- visibleWidth,
16
- } from "@earendil-works/pi-tui";
17
-
18
- import type { ProxyConfig } from "../config.ts";
19
- import type { Discovery } from "../fetch-models.ts";
20
- import { buildCatalog } from "./catalog.ts";
21
- import {
22
- apiCompatible,
23
- assignedIdsFor,
24
- attachModel,
25
- detachModel,
26
- groupPoolByOwnedBy,
27
- poolFor,
28
- } from "./mutate.ts";
29
- import { collectProviders } from "./providers.ts";
30
- import { confirmRemoveProvider } from "./prompt-confirm.ts";
31
- import { promptNewProviderName } from "./prompt-name.ts";
32
- import { pad } from "./render-text.ts";
33
- import {
34
- renderEmpty,
35
- renderModelRow,
36
- renderNewProviderRow,
37
- renderPanelHeader,
38
- renderProviderRow,
39
- renderSubheader,
40
- } from "./rows.ts";
41
- import type {
42
- CatalogIndex,
43
- OverlayTui,
44
- PanelId,
45
- ProviderEntry,
46
- Theme,
47
- } from "./types.ts";
48
-
49
- interface RightRow {
50
- kind: "header" | "sub" | "model" | "empty";
51
- label?: string;
52
- // model fields
53
- id?: string;
54
- side?: "assigned" | "pool";
55
- compatWarn?: boolean;
56
- /** index into the linear cursor list (skipping non-cursor rows). */
57
- cursorIdx?: number;
58
- }
59
-
60
- export function buildPicker(
61
- tui: OverlayTui,
62
- theme: Theme,
63
- cfg: ProxyConfig,
64
- discovery: Discovery,
65
- ctx: ExtensionCommandContext,
66
- done: (v: ProxyConfig | null) => void,
67
- ): Component & { handleInput(data: string): void } {
68
- const catalog: CatalogIndex = buildCatalog(discovery);
69
- let providers: ProviderEntry[] = collectProviders(cfg, catalog);
70
-
71
- let focus: PanelId = "providers";
72
- let providerCursor = 0;
73
- let assignedCursor = 0;
74
- let poolCursor = 0;
75
- let providerScroll = 0;
76
- let assignedScroll = 0;
77
- let poolScroll = 0;
78
- const finish = (result: ProxyConfig | null): void => {
79
- done(result);
80
- };
81
-
82
- const selectedProvider = (): ProviderEntry | null => {
83
- if (providers.length === 0) return null;
84
- const idx = Math.max(0, Math.min(providerCursor, providers.length - 1));
85
- return providers[idx] ?? null;
86
- };
87
-
88
- const refresh = (): void => {
89
- providers = collectProviders(cfg, catalog);
90
- const maxProv = Math.max(0, providers.length); // +1 for "+ new" pseudo row
91
- if (providerCursor > maxProv) providerCursor = maxProv;
92
- const prov = selectedProvider();
93
- if (prov) {
94
- const aLen = assignedIdsFor(cfg, prov).length;
95
- const pLen = poolFor(cfg, prov, catalog).length;
96
- if (assignedCursor >= aLen) assignedCursor = Math.max(0, aLen - 1);
97
- if (poolCursor >= pLen) poolCursor = Math.max(0, pLen - 1);
98
- }
99
- };
100
-
101
- const ensureVisible = (
102
- cursor: number,
103
- scroll: number,
104
- visible: number,
105
- ): number => {
106
- if (cursor < scroll) return cursor;
107
- if (cursor >= scroll + visible) return cursor - visible + 1;
108
- return Math.max(0, scroll);
109
- };
110
-
111
- const onTab = (back: boolean): void => {
112
- const order: PanelId[] = ["providers", "assigned", "pool"];
113
- const i = order.indexOf(focus);
114
- focus = back
115
- ? order[(i - 1 + order.length) % order.length]!
116
- : order[(i + 1) % order.length]!;
117
- };
118
-
119
- const moveCursor = (delta: number): void => {
120
- if (focus === "providers") {
121
- const total = providers.length + 1; // +1 for "+ new" pseudo row
122
- providerCursor = Math.max(0, Math.min(providerCursor + delta, total - 1));
123
- return;
124
- }
125
- const prov = selectedProvider();
126
- if (!prov) return;
127
- if (focus === "assigned") {
128
- const total = assignedIdsFor(cfg, prov).length;
129
- if (total === 0) return;
130
- assignedCursor = Math.max(0, Math.min(assignedCursor + delta, total - 1));
131
- return;
132
- }
133
- const total = poolFor(cfg, prov, catalog).length;
134
- if (total === 0) return;
135
- poolCursor = Math.max(0, Math.min(poolCursor + delta, total - 1));
136
- };
137
-
138
- const onActivate = async (): Promise<void> => {
139
- if (focus === "providers") {
140
- if (providerCursor === providers.length) {
141
- const name = await promptNewProviderName(ctx, cfg.proxy.providerPrefix);
142
- if (name && !cfg.customProviders[name]) {
143
- cfg.customProviders[name] = { api: "openai-completions", models: [] };
144
- refresh();
145
- providerCursor = providers.findIndex(
146
- (p) => p.kind === "custom" && p.name === name,
147
- );
148
- if (providerCursor < 0) providerCursor = providers.length - 1;
149
- focus = "pool";
150
- }
151
- tui.requestRender();
152
- return;
153
- }
154
- focus = "pool";
155
- return;
156
- }
157
- const prov = selectedProvider();
158
- if (!prov) return;
159
- if (focus === "assigned") {
160
- const ids = assignedIdsFor(cfg, prov);
161
- const id = ids[assignedCursor];
162
- if (!id) return;
163
- detachModel(cfg, prov, id);
164
- refresh();
165
- return;
166
- }
167
- const ids = poolFor(cfg, prov, catalog);
168
- const id = ids[poolCursor];
169
- if (!id) return;
170
- const m = catalog.byId.get(id);
171
- if (!m) return;
172
- attachModel(cfg, prov, m);
173
- refresh();
174
- };
175
-
176
- const onDelete = async (): Promise<void> => {
177
- if (focus !== "providers") return;
178
- const prov = selectedProvider();
179
- if (!prov || prov.kind !== "custom") return;
180
- const ok = await confirmRemoveProvider(ctx, prov.name);
181
- if (!ok) {
182
- tui.requestRender();
183
- return;
184
- }
185
- delete cfg.customProviders[prov.name];
186
- refresh();
187
- if (providerCursor >= providers.length)
188
- providerCursor = Math.max(0, providers.length - 1);
189
- tui.requestRender();
190
- };
191
-
192
- // Render and input split into the second half of the file.
193
- return assembleComponent({
194
- tui,
195
- theme,
196
- cfg,
197
- catalog,
198
- getProviders: () => providers,
199
- getFocus: () => focus,
200
- setFocus: (f: PanelId) => {
201
- focus = f;
202
- },
203
- getProviderCursor: () => providerCursor,
204
- getAssignedCursor: () => assignedCursor,
205
- getPoolCursor: () => poolCursor,
206
- getProviderScroll: () => providerScroll,
207
- setProviderScroll: (v: number) => {
208
- providerScroll = v;
209
- },
210
- getAssignedScroll: () => assignedScroll,
211
- setAssignedScroll: (v: number) => {
212
- assignedScroll = v;
213
- },
214
- getPoolScroll: () => poolScroll,
215
- setPoolScroll: (v: number) => {
216
- poolScroll = v;
217
- },
218
- selectedProvider,
219
- moveCursor,
220
- onTab,
221
- onActivate,
222
- onDelete,
223
- ensureVisible,
224
- finish,
225
- apiCompatible,
226
- poolGrouper: (ids: string[]) => groupPoolByOwnedBy(ids, catalog),
227
- assignedIdsFor: (p: ProviderEntry) => assignedIdsFor(cfg, p),
228
- poolFor: (p: ProviderEntry) => poolFor(cfg, p, catalog),
229
- });
230
- }
231
-
232
- // The component assembly (render + input) lives next-door to keep this file
233
- // readable. We import it lazily to dodge circular imports.
234
- import { assembleComponent } from "./picker-component.ts";
235
- // Re-export helpers used by the component file (avoid duplicate imports).
236
- export {
237
- pad,
238
- renderEmpty,
239
- renderModelRow,
240
- renderNewProviderRow,
241
- renderPanelHeader,
242
- renderProviderRow,
243
- renderSubheader,
244
- visibleWidth,
245
- };
246
- export type { RightRow };
247
- export { matchesKey, getKeybindings };