pi-cliproxyapi 0.1.2 → 0.2.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.
package/README.md CHANGED
@@ -19,13 +19,22 @@ One `(endpoint, apiKey)` pair — every provider and model inherits it automatic
19
19
 
20
20
  | Command | Description |
21
21
  | --- | --- |
22
- | `/cliproxy` | Interactive overlayenable providers, toggle models, create custom groups |
22
+ | `/cliproxy` | Three-panel picker — providers, assigned models, available pool |
23
23
  | `/cliproxy-setup` | Configure endpoint, API key, provider prefix, usage key |
24
24
  | `/cliproxy-refresh` | Re-fetch upstream models, re-register providers |
25
- | `/cliproxy-list` | Read-only view of current configuration |
26
25
  | `/cliproxy-usage` | Per-account quota windows with progress bars (`d` = show disabled, `v` = verbose) |
27
26
  | `/cliproxy-doctor` | Connectivity, key resolution, discovery diagnostics |
28
27
 
28
+ ### `/cliproxy` picker
29
+
30
+ The picker has three panels you cycle through with `Tab` / arrow keys:
31
+
32
+ - **left** — every provider (built-in + custom). `+ new custom group…` is the last row.
33
+ - **right top** — models currently assigned to the focused provider. `Enter` / `Space` removes one.
34
+ - **right bottom** — available model pool, grouped by upstream `owned_by`. `Enter` / `Space` attaches a model to the focused provider.
35
+
36
+ Extra keys: `d` removes a custom group (with confirmation), `s` saves, `q` / `Esc` cancels. A `⚠` marker shows when a model's recommended API differs from the provider's API — attach is allowed anyway.
37
+
29
38
  ## Prerequisites
30
39
 
31
40
  You need a running [CliProxyAPI](https://github.com/router-for-me/CLIProxyAPI) instance — this is the corporate LLM proxy that aggregates multiple providers behind a single OpenAI-compatible endpoint.
@@ -143,7 +152,7 @@ The sidecar is **optional for basic usage** — without it the plugin falls back
143
152
  | Model discovery | Enriched from [models.dev](https://models.dev) (real context windows, costs, reasoning) | Defaults: `contextWindow=128k`, `maxTokens=16k`, `cost=0`, `reasoning=false` |
144
153
  | `/cliproxy-usage` | Works — per-account quota bars | **Does not work** (no `/api/usage` endpoint) |
145
154
  | Classification | Server-side, accurate | Local heuristics by `owned_by` |
146
- | `/cliproxy`, `/cliproxy-list`, `/cliproxy-doctor` | Work | Work |
155
+ | `/cliproxy`, `/cliproxy-doctor` | Work | Work |
147
156
 
148
157
  ## Layout
149
158
 
@@ -151,15 +160,27 @@ The sidecar is **optional for basic usage** — without it the plugin falls back
151
160
  index.ts ExtensionFactory entry point
152
161
  src/
153
162
  config.ts ~/.config/pi-cliproxyapi/config.json
154
- commands.ts 6 slash commands
163
+ commands.ts 5 slash commands
155
164
  apply.ts pi.registerProvider calls
156
165
  fetch-models.ts well-known + /v1/models fallback
157
166
  fetch-usage.ts /api/usage client with TTL cache
158
167
  compat.ts baseUrl derivation, model classification
159
168
  conflicts.ts read-only ~/.pi/{models,auth}.json scan
160
- ui-picker.ts overlay picker with collapsible provider groups
161
- ui-usage.ts ANSI-colored usage renderer
169
+ ui-frame.ts single source of truth for overlay frames
162
170
  ui-overlay.ts scrollable overlay shell with toggles
163
171
  ui-setup.ts setup wizard
172
+ ui-usage.ts ANSI-coloured usage renderer
173
+ ui-picker/ three-panel /cliproxy overlay
174
+ index.ts public runPicker entry
175
+ types.ts shared TS types
176
+ catalog.ts build a model lookup from discovery
177
+ providers.ts resolve the providers shown in the left panel
178
+ mutate.ts attach / detach / claim helpers + pool grouping
179
+ render-text.ts ANSI-aware pad / truncate
180
+ rows.ts per-row renderers for left / right panels
181
+ picker.ts state + navigation, glues catalogue to UI
182
+ picker-component.ts render + input dispatch
183
+ prompt-confirm.ts remove-group confirmation
184
+ prompt-name.ts new-group name prompt
164
185
  log.ts tagged logger
165
186
  ```
package/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * 2. fetch discovery (well-known → fall back to /v1/models)
8
8
  * 3. call pi.registerProvider for each enabled built-in + custom provider
9
9
  * 4. register slash commands /cliproxy /cliproxy-setup /cliproxy-refresh
10
- * /cliproxy-list /cliproxy-usage /cliproxy-doctor
10
+ * /cliproxy-usage /cliproxy-doctor
11
11
  *
12
12
  * All discovery + apply errors are logged but never abort extension load —
13
13
  * a missing/broken proxy must not prevent Pi from starting.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cliproxyapi",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Pi extension for corporate management of model providers via a single CliProxyAPI endpoint",
6
6
  "license": "MIT",
package/src/apply.ts CHANGED
@@ -37,6 +37,36 @@ export async function applyAll(
37
37
  }
38
38
 
39
39
  // -------- builtin providers (anthropic, openai, etc.)
40
+ const builtinByName = new Map<
41
+ string,
42
+ Array<{
43
+ id: string;
44
+ name: string;
45
+ reasoning: boolean;
46
+ contextWindow: number;
47
+ maxTokens: number;
48
+ cost: {
49
+ input: number;
50
+ output: number;
51
+ cacheRead: number;
52
+ cacheWrite: number;
53
+ };
54
+ }>
55
+ >();
56
+ for (const p of discovery.builtinProviders) {
57
+ builtinByName.set(
58
+ p.name,
59
+ p.models.map((m) => ({
60
+ id: m.id,
61
+ name: m.name,
62
+ reasoning: m.reasoning,
63
+ contextWindow: m.contextWindow,
64
+ maxTokens: m.maxTokens,
65
+ cost: m.cost,
66
+ })),
67
+ );
68
+ }
69
+
40
70
  for (const [name, p] of Object.entries(cfg.builtinProviders)) {
41
71
  if (!p?.enabled) {
42
72
  report.skipped.push({ provider: name, reason: "disabled" });
@@ -46,7 +76,7 @@ export async function applyAll(
46
76
  report.skipped.push({ provider: name, reason: "empty whitelist" });
47
77
  continue;
48
78
  }
49
- let builtin: ReadonlyArray<{
79
+ let catalog: ReadonlyArray<{
50
80
  id: string;
51
81
  name: string;
52
82
  api: Api;
@@ -56,19 +86,64 @@ export async function applyAll(
56
86
  contextWindow: number;
57
87
  maxTokens: number;
58
88
  thinkingLevelMap?: any;
59
- }>;
89
+ }> = [];
60
90
  try {
61
- builtin = getModels(name as any) as any;
91
+ catalog = getModels(name as any) as any;
62
92
  } catch {
63
- report.skipped.push({
64
- provider: name,
65
- reason: `pi-ai has no provider "${name}"`,
66
- });
67
- continue;
93
+ /* unknown provider in pi-ai catalog — keep going with proxy data */
68
94
  }
69
- const selected = builtin.filter(
70
- (m) => p.models.includes(m.id) && proxyIds.has(m.id),
95
+ const catalogById = new Map(catalog.map((m) => [m.id, m]));
96
+ const proxyById = new Map(
97
+ (builtinByName.get(name) ?? []).map((m) => [m.id, m]),
71
98
  );
99
+
100
+ interface MergedModel {
101
+ id: string;
102
+ name: string;
103
+ reasoning: boolean;
104
+ contextWindow: number;
105
+ maxTokens: number;
106
+ cost: any;
107
+ input: ("text" | "image")[];
108
+ api: Api;
109
+ thinkingLevelMap?: any;
110
+ }
111
+ const selected: MergedModel[] = [];
112
+ for (const id of p.models) {
113
+ if (!proxyIds.has(id)) continue;
114
+ const c = catalogById.get(id);
115
+ const px = proxyById.get(id);
116
+ if (c) {
117
+ selected.push({
118
+ id: c.id,
119
+ name: c.name,
120
+ reasoning: c.reasoning,
121
+ contextWindow: c.contextWindow,
122
+ maxTokens: c.maxTokens,
123
+ cost: c.cost,
124
+ input: c.input,
125
+ api: c.api,
126
+ thinkingLevelMap: c.thinkingLevelMap,
127
+ });
128
+ continue;
129
+ }
130
+ if (!px) continue;
131
+ // Catalog miss — fall back to proxy metadata. Pick API by provider name.
132
+ const api: Api =
133
+ name === "anthropic"
134
+ ? "anthropic-messages"
135
+ : ((p.apiOverride as Api | undefined) ?? "openai-responses");
136
+ selected.push({
137
+ id: px.id,
138
+ name: px.name,
139
+ reasoning: px.reasoning,
140
+ contextWindow: px.contextWindow,
141
+ maxTokens: px.maxTokens,
142
+ cost: px.cost,
143
+ input: ["text"],
144
+ api,
145
+ });
146
+ }
72
147
  if (selected.length === 0) {
73
148
  report.skipped.push({
74
149
  provider: name,
package/src/commands.ts CHANGED
@@ -2,7 +2,6 @@
2
2
  // /cliproxy — open the picker overlay
3
3
  // /cliproxy-setup — first-run / re-run setup wizard for endpoint+keys
4
4
  // /cliproxy-refresh — refetch discovery + re-apply
5
- // /cliproxy-list — show all upstream models in an overlay
6
5
  // /cliproxy-usage — fetch /api/usage and render in overlay
7
6
  // /cliproxy-doctor — connectivity + key-resolution diagnostics
8
7
 
@@ -18,7 +17,7 @@ import { fetchDiscovery, PLUGIN_USER_AGENT } from "./fetch-models.ts";
18
17
  import { clearUsageCache, fetchUsage } from "./fetch-usage.ts";
19
18
  import { log } from "./log.ts";
20
19
  import { showOverlay } from "./ui-overlay.ts";
21
- import { runPicker } from "./ui-picker.ts";
20
+ import { runPicker } from "./ui-picker/index.ts";
22
21
  import { runSetup } from "./ui-setup.ts";
23
22
  import { renderUsage } from "./ui-usage.ts";
24
23
 
@@ -41,11 +40,6 @@ export function registerCommands(pi: ExtensionAPI): void {
41
40
  handler: handleRefresh.bind(null, pi),
42
41
  });
43
42
 
44
- pi.registerCommand("cliproxy-list", {
45
- description: "Show every upstream model in a scrollable overlay",
46
- handler: handleList,
47
- });
48
-
49
43
  pi.registerCommand("cliproxy-usage", {
50
44
  description: "Show per-account quota windows from the upstream",
51
45
  handler: handleUsage,
@@ -147,27 +141,6 @@ async function handleRefresh(
147
141
  }
148
142
  }
149
143
 
150
- // --------------------------------------------------------------------------- /cliproxy-list
151
-
152
- async function handleList(
153
- _args: string,
154
- ctx: ExtensionCommandContext,
155
- ): Promise<void> {
156
- const cfg = loadConfig();
157
- const resolvedKey = resolveConfigValue(cfg.proxy.apiKey);
158
- let discovery;
159
- try {
160
- discovery = await fetchDiscovery(cfg, resolvedKey);
161
- } catch (err) {
162
- ctx.ui.notify(`list failed: ${(err as Error).message}`, "error");
163
- return;
164
- }
165
- await runPicker(ctx, cfg, discovery, {
166
- readOnly: true,
167
- title: " /cliproxy-list \u00b7 read-only ",
168
- });
169
- }
170
-
171
144
  // --------------------------------------------------------------------------- /cliproxy-usage
172
145
 
173
146
  async function handleUsage(
@@ -0,0 +1,118 @@
1
+ // Shared frame renderer for every /cliproxy overlay.
2
+ //
3
+ // Geometry contract:
4
+ // - frame() always returns lines that are EXACTLY `width` visible cells.
5
+ // - top = \u256d\u2500 <title> <fill> \u256e
6
+ // - body i = \u2502 <line padded/truncated to width-2> \u2502
7
+ // - footer = \u2570\u2500 <hint> <fill> [badge] \u256f (if footer provided)
8
+ // or \u2570\u2500\u2500\u2500\u2026\u2500\u2500\u2500\u256f (otherwise)
9
+ //
10
+ // Callers stay simple:
11
+ //
12
+ // return frame(theme, {
13
+ // width,
14
+ // title: " setup ",
15
+ // lines: [
16
+ // " hint goes here ",
17
+ // "",
18
+ // theme.fg("accent", "> input"),
19
+ // ],
20
+ // footer: { hint: "enter = save \u00b7 esc = cancel" },
21
+ // });
22
+ //
23
+ // Anything inside `lines` may already contain ANSI escapes; pad() is ANSI-aware
24
+ // (see render-text.ts). No caller should ever draw \u256d/\u256e/\u2502/\u2570/\u256f by hand again.
25
+
26
+ import { visibleWidth } from "@earendil-works/pi-tui";
27
+
28
+ import { pad } from "./ui-picker/render-text.ts";
29
+ import type { Theme } from "./ui-picker/types.ts";
30
+
31
+ export interface FrameFooter {
32
+ /** Left-aligned hint text (dimmed). */
33
+ hint?: string;
34
+ /** Right-aligned focus/state badge (muted). */
35
+ badge?: string;
36
+ }
37
+
38
+ export interface FrameOpts {
39
+ width: number;
40
+ title?: string;
41
+ titleColor?: "accent" | "error" | "success" | "warning";
42
+ lines: string[];
43
+ footer?: FrameFooter;
44
+ }
45
+
46
+ const SIDE = "\u2502";
47
+ const TL = "\u256d";
48
+ const TR = "\u256e";
49
+ const BL = "\u2570";
50
+ const BR = "\u256f";
51
+ const HR = "\u2500";
52
+
53
+ export function frame(theme: Theme, opts: FrameOpts): string[] {
54
+ const w = Math.max(4, opts.width);
55
+ return [
56
+ drawTop(theme, opts.title, opts.titleColor ?? "accent", w),
57
+ ...opts.lines.map((line) => drawBody(theme, line, w)),
58
+ drawBottom(theme, opts.footer, w),
59
+ ];
60
+ }
61
+
62
+ function drawTop(
63
+ theme: Theme,
64
+ title: string | undefined,
65
+ color: "accent" | "error" | "success" | "warning",
66
+ width: number,
67
+ ): string {
68
+ // Layout: \u256d \u2500 <title> <fill> \u256e \u2014 exactly `width` cells.
69
+ // Fixed cells = 2 (\u256d\u2500) + 1 (\u256e) = 3. The rest is title + fill.
70
+ const t = title ?? "";
71
+ const titleVis = visibleWidth(t);
72
+ const rest = Math.max(0, width - 3 - titleVis);
73
+ const titleColoured = t ? theme.bold(theme.fg(color, t)) : "";
74
+ const body = `${TL}${HR}${titleColoured}${HR.repeat(rest)}${TR}`;
75
+ return theme.fg("borderAccent", body);
76
+ }
77
+
78
+ function drawBody(theme: Theme, content: string, width: number): string {
79
+ // Layout: \u2502 <content padded to width-2> \u2502
80
+ const inner = Math.max(0, width - 2);
81
+ const side = theme.fg("borderAccent", SIDE);
82
+ return `${side}${pad(content, inner)}${side}`;
83
+ }
84
+
85
+ function drawBottom(
86
+ theme: Theme,
87
+ footer: FrameFooter | undefined,
88
+ width: number,
89
+ ): string {
90
+ const start = `${BL}${HR}`;
91
+ const end = `${BR}`;
92
+ const fixedCells = 3; // \u2570 + \u2500 + \u256f
93
+
94
+ if (!footer || (!footer.hint && !footer.badge)) {
95
+ const rest = Math.max(0, width - fixedCells);
96
+ return theme.fg("borderAccent", `${start}${HR.repeat(rest)}${end}`);
97
+ }
98
+
99
+ const hint = footer.hint ?? "";
100
+ const badge = footer.badge ?? "";
101
+ const hintVis = visibleWidth(hint);
102
+ const badgeVis = visibleWidth(badge);
103
+ const rest = Math.max(0, width - fixedCells - hintVis - badgeVis);
104
+
105
+ const left = theme.fg("dim", hint);
106
+ const right = badge ? theme.fg("muted", badge) : "";
107
+ const fill = HR.repeat(rest);
108
+
109
+ return `${theme.fg("borderAccent", start)}${left}${theme.fg("borderAccent", fill)}${right}${theme.fg("borderAccent", end)}`;
110
+ }
111
+
112
+ /**
113
+ * Convenience: number of usable content cells per body line for a given frame
114
+ * width. Use this when laying out children that need to know their width.
115
+ */
116
+ export function frameInner(width: number): number {
117
+ return Math.max(0, width - 2);
118
+ }
package/src/ui-overlay.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Read-only scrollable overlay used by /cliproxy-list, /cliproxy-usage, /cliproxy-doctor.
1
+ // Read-only scrollable overlay used by /cliproxy-usage and /cliproxy-doctor.
2
2
  //
3
3
  // Pure presentational — never touches the agent session or context. The
4
4
  // caller may supply boolean "toggles" (one keystroke each) that re-render the
@@ -23,6 +23,8 @@ import {
23
23
  visibleWidth,
24
24
  } from "@earendil-works/pi-tui";
25
25
 
26
+ import { frame } from "./ui-frame.ts";
27
+
26
28
  interface Theme {
27
29
  fg(name: string, s: string): string;
28
30
  bold(s: string): string;
@@ -76,7 +78,6 @@ function buildOverlay(
76
78
  };
77
79
 
78
80
  const renderFrame = (width: number, height: number): string[] => {
79
- const inner = Math.max(10, width - 2);
80
81
  const visible = Math.max(1, height - 2);
81
82
  const total = lines.length;
82
83
  const maxOffset = Math.max(0, total - visible);
@@ -87,26 +88,22 @@ function buildOverlay(
87
88
  total > visible
88
89
  ? Math.min(100, Math.round(((offset + visible) / total) * 100))
89
90
  : 100;
90
- const titleBar = formatTitleBar(t, props.title, inner);
91
- const footerBar = formatFooterBar(t, {
92
- from: offset + 1,
93
- to: Math.min(total, offset + visible),
94
- total,
95
- pct,
96
- width: inner,
97
- toggles: (props.toggles ?? []).map((tg) => ({
98
- hint: tg.hint,
99
- active: state[tg.key] === true,
100
- })),
91
+ const hintBase = "\u2191\u2193 \u00b7 pgUp/pgDn \u00b7 g/G";
92
+ const togglesText = (props.toggles ?? [])
93
+ .map((tg) =>
94
+ state[tg.key] === true
95
+ ? t.fg("success", `[${tg.hint}]`)
96
+ : t.fg("dim", tg.hint),
97
+ )
98
+ .join(" \u00b7 ");
99
+ const hint = ` ${hintBase}${togglesText ? " " + togglesText : ""} `;
100
+ const badge = ` ${offset + 1}\u2013${Math.min(total, offset + visible)} of ${total} ${pct}% `;
101
+ return frame(t, {
102
+ width,
103
+ title: ` ${props.title} `,
104
+ lines: slice.map((ln) => ` ${ln}`),
105
+ footer: { hint, badge },
101
106
  });
102
- const sideL = t.fg("borderAccent", "│");
103
- const sideR = t.fg("borderAccent", "│");
104
- const out: string[] = [titleBar];
105
- for (const ln of slice) {
106
- out.push(`${sideL} ${padRight(ln, inner - 2)} ${sideR}`);
107
- }
108
- out.push(footerBar);
109
- return out;
110
107
  };
111
108
 
112
109
  return {
@@ -174,60 +171,6 @@ function buildOverlay(
174
171
  };
175
172
  }
176
173
 
177
- // --------------------------------------------------------------------------- chrome helpers
178
-
179
- function formatTitleBar(theme: Theme, title: string, inner: number): string {
180
- const label = ` ${title} `;
181
- const leftSep = "╭─";
182
- const rightFill = inner - visibleWidth(label) - visibleWidth(leftSep) - 2;
183
- const fill = "─".repeat(Math.max(0, rightFill));
184
- return theme.fg(
185
- "borderAccent",
186
- `${leftSep}${theme.bold(theme.fg("accent", label))}${theme.fg("borderAccent", fill)}╮`,
187
- );
188
- }
189
-
190
- function formatFooterBar(
191
- theme: Theme,
192
- opts: {
193
- from: number;
194
- to: number;
195
- total: number;
196
- pct: number;
197
- width: number;
198
- toggles: Array<{ hint: string; active: boolean }>;
199
- },
200
- ): string {
201
- const hintBase = "↑↓ · pgUp/pgDn · g/G";
202
- const togglesText = opts.toggles
203
- .map((tg) =>
204
- tg.active
205
- ? theme.fg("success", `[${tg.hint}]`)
206
- : theme.fg("dim", tg.hint),
207
- )
208
- .join(" · ");
209
- const left = ` ${hintBase}${togglesText ? " " + togglesText : ""} `;
210
- const right = ` ${opts.from}–${opts.to} of ${opts.total} ${opts.pct}% `;
211
- const leftSep = "╰─";
212
- const rightSep = "╯";
213
- const used =
214
- visibleWidth(leftSep) +
215
- visibleWidth(rightSep) +
216
- visibleWidth(left) +
217
- visibleWidth(right);
218
- const filler = "─".repeat(Math.max(0, opts.width - used));
219
- return theme.fg(
220
- "borderAccent",
221
- `${leftSep}${theme.fg("dim", left)}${filler}${theme.fg("muted", right)}${rightSep}`,
222
- );
223
- }
224
-
225
- function padRight(s: string, width: number): string {
226
- const w = visibleWidth(s);
227
- if (w >= width) return s;
228
- return s + " ".repeat(width - w);
229
- }
230
-
231
174
  // --------------------------------------------------------------------------- public API
232
175
 
233
176
  /**
@@ -0,0 +1,52 @@
1
+ // Build a fast lookup of every model the proxy currently offers, split into
2
+ // built-in (by provider name) and the custom pool. The result is consumed by
3
+ // providers.ts and mutate.ts.
4
+
5
+ import type { Discovery } from "../fetch-models.ts";
6
+ import type { CatalogIndex, ModelEntry } from "./types.ts";
7
+
8
+ export function buildCatalog(discovery: Discovery): CatalogIndex {
9
+ const byId = new Map<string, ModelEntry>();
10
+ const builtinModelIds = new Map<string, string[]>();
11
+
12
+ for (const p of discovery.builtinProviders) {
13
+ const ids: string[] = [];
14
+ for (const m of p.models) {
15
+ byId.set(m.id, {
16
+ id: m.id,
17
+ name: m.name,
18
+ suggestedApi: p.api,
19
+ subtitle: m.name && m.name !== m.id ? m.name : undefined,
20
+ ownedBy: p.name,
21
+ reasoning: m.reasoning,
22
+ contextWindow: m.contextWindow,
23
+ maxTokens: m.maxTokens,
24
+ cost: m.cost,
25
+ });
26
+ ids.push(m.id);
27
+ }
28
+ builtinModelIds.set(p.name, ids);
29
+ }
30
+
31
+ const customPoolIds: string[] = [];
32
+ for (const m of discovery.customPool) {
33
+ // Don't overwrite a built-in entry if the same id appears in both.
34
+ if (!byId.has(m.id)) {
35
+ byId.set(m.id, {
36
+ id: m.id,
37
+ name: m.name,
38
+ suggestedApi: m.api,
39
+ subtitle: m.name && m.name !== m.id ? m.name : undefined,
40
+ origin: m.suggestedProvider,
41
+ ownedBy: m.ownedBy,
42
+ reasoning: m.reasoning,
43
+ contextWindow: m.contextWindow,
44
+ maxTokens: m.maxTokens,
45
+ cost: m.cost,
46
+ });
47
+ }
48
+ customPoolIds.push(m.id);
49
+ }
50
+
51
+ return { byId, builtinModelIds, customPoolIds };
52
+ }
@@ -0,0 +1,35 @@
1
+ // Public entry for the /cliproxy three-panel picker.
2
+
3
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
+
5
+ import type { ProxyConfig } from "../config.ts";
6
+ import type { Discovery } from "../fetch-models.ts";
7
+ import { buildPicker } from "./picker.ts";
8
+ import type { OverlayTui, Theme } from "./types.ts";
9
+
10
+ export async function runPicker(
11
+ ctx: ExtensionCommandContext,
12
+ cfg: ProxyConfig,
13
+ discovery: Discovery,
14
+ ): Promise<ProxyConfig | null> {
15
+ if (!ctx.hasUI) {
16
+ ctx.ui.notify("interactive UI required for /cliproxy", "warning");
17
+ return null;
18
+ }
19
+ const draft: ProxyConfig = structuredClone(cfg);
20
+ return ctx.ui.custom<ProxyConfig | null>(
21
+ (tui, theme, _kb, done) =>
22
+ buildPicker(
23
+ tui as unknown as OverlayTui,
24
+ theme as unknown as Theme,
25
+ draft,
26
+ discovery,
27
+ ctx,
28
+ done,
29
+ ),
30
+ {
31
+ overlay: true,
32
+ overlayOptions: { width: 170, maxHeight: "94%" },
33
+ },
34
+ );
35
+ }