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.
@@ -0,0 +1,56 @@
1
+ // Shared types for the /cliproxy three-panel picker.
2
+
3
+ import type { Api } from "@earendil-works/pi-ai";
4
+
5
+ export interface Theme {
6
+ fg(name: string, s: string): string;
7
+ bold(s: string): string;
8
+ }
9
+
10
+ export interface OverlayTui {
11
+ requestRender(): void;
12
+ rows?: number;
13
+ cols?: number;
14
+ }
15
+
16
+ export type PanelId = "providers" | "assigned" | "pool";
17
+
18
+ export interface ProviderEntry {
19
+ kind: "builtin" | "custom";
20
+ name: string;
21
+ /** API the provider expects. Used for compat warnings on pool rows. */
22
+ api: Api;
23
+ /** Subtitle shown in the providers panel. */
24
+ subtitle: string;
25
+ }
26
+
27
+ export interface ModelEntry {
28
+ id: string;
29
+ name: string;
30
+ /** Suggested API for this model (from server hint / catalog). */
31
+ suggestedApi: Api;
32
+ /** Subtitle to render. */
33
+ subtitle?: string;
34
+ /** Origin label for grouping (e.g. "lproxy-glm"); empty for built-in. */
35
+ origin?: string;
36
+ /** Raw upstream owned_by tag, for grouping/UX. */
37
+ ownedBy?: string;
38
+ reasoning: boolean;
39
+ contextWindow: number;
40
+ maxTokens: number;
41
+ cost?: {
42
+ input: number;
43
+ output: number;
44
+ cacheRead: number;
45
+ cacheWrite: number;
46
+ };
47
+ }
48
+
49
+ export interface CatalogIndex {
50
+ /** All models indexed by id, regardless of provider scope. */
51
+ byId: Map<string, ModelEntry>;
52
+ /** For each built-in provider name, ordered model ids that it offers on the proxy. */
53
+ builtinModelIds: Map<string, string[]>;
54
+ /** Ordered list of all custom-pool model ids. */
55
+ customPoolIds: string[];
56
+ }
package/src/ui-setup.ts CHANGED
@@ -14,7 +14,6 @@ import {
14
14
  getKeybindings,
15
15
  Input,
16
16
  matchesKey,
17
- visibleWidth,
18
17
  } from "@earendil-works/pi-tui";
19
18
 
20
19
  import {
@@ -23,6 +22,7 @@ import {
23
22
  resolveConfigValue,
24
23
  saveConfig,
25
24
  } from "./config.ts";
25
+ import { frame, frameInner } from "./ui-frame.ts";
26
26
 
27
27
  interface Theme {
28
28
  fg(name: string, s: string): string;
@@ -104,13 +104,9 @@ export async function runSetup(
104
104
  const prefill = forceAll
105
105
  ? (values[step.label] ?? step.initialValue ?? "")
106
106
  : (values[step.label] ?? "");
107
- // Skip already-filled non-empty fields when called for first-run setup
108
- // (we still want to ask for missing pieces, but not re-prompt for
109
- // what's already saved).
110
107
  if (!forceAll && prefill) {
111
108
  continue;
112
109
  }
113
-
114
110
  const result = await promptStep(
115
111
  ctx,
116
112
  step,
@@ -123,7 +119,7 @@ export async function runSetup(
123
119
  const trimmed = result.trim();
124
120
  if (!trimmed) {
125
121
  if (step.required) {
126
- ctx.ui.notify(`${step.label} is required aborted`, "warning");
122
+ ctx.ui.notify(`${step.label} is required \u2014 aborted`, "warning");
127
123
  cancelled = true;
128
124
  break;
129
125
  }
@@ -153,10 +149,6 @@ export async function runSetup(
153
149
  return true;
154
150
  }
155
151
 
156
- /**
157
- * If the user typed a bare ~/path or /abs/path, wrap it in `!cat <path>`
158
- * so resolveConfigValue executes it. Otherwise return as-is.
159
- */
160
152
  function normalizeValue(raw: string): string {
161
153
  if (raw.startsWith("!") || raw.startsWith("$")) return raw;
162
154
  if (raw.startsWith("~/")) {
@@ -189,10 +181,7 @@ async function promptStep(
189
181
  prefill,
190
182
  done,
191
183
  ),
192
- {
193
- overlay: true,
194
- overlayOptions: { width: 100, maxHeight: "60%" },
195
- },
184
+ { overlay: true, overlayOptions: { width: 100, maxHeight: "60%" } },
196
185
  );
197
186
  }
198
187
 
@@ -220,8 +209,6 @@ function buildStepOverlay(
220
209
  }
221
210
  if (step.validate) {
222
211
  const trimmed = raw.trim();
223
- // Only validate the literal form. Indirect ("!", "$") values are
224
- // validated at apply time, not here.
225
212
  if (trimmed && !trimmed.startsWith("!") && !trimmed.startsWith("$")) {
226
213
  const err = step.validate(trimmed);
227
214
  if (err) {
@@ -239,38 +226,30 @@ function buildStepOverlay(
239
226
 
240
227
  return {
241
228
  render(width: number): string[] {
242
- const inner = Math.max(40, width - 2);
243
- const top = theme.fg(
244
- "borderAccent",
245
- `\u256d\u2500 ${theme.bold(theme.fg("accent", `setup: ${step.label}`))} ${"\u2500".repeat(Math.max(0, inner - visibleWidth(`setup: ${step.label}`) - 4))}\u256e`,
246
- );
247
- const hintLine = pad(theme.fg("dim", step.hint), inner - 2);
229
+ const inner = frameInner(width);
248
230
  const inputLines = input.render(inner - 4);
249
- const errLine = error
250
- ? pad(theme.fg("error", `! ${error}`), inner - 2)
251
- : pad(theme.fg("dim", "enter = save · esc = cancel"), inner - 2);
252
- const side = theme.fg("borderAccent", "\u2502");
253
- const out: string[] = [top];
254
- out.push(`${side} ${hintLine} ${side}`);
255
- out.push(`${side} ${pad("", inner - 2)} ${side}`);
256
- for (const ln of inputLines) {
257
- out.push(
258
- `${side} ${pad(theme.fg("accent", `> ${ln}`), inner - 2)} ${side}`,
259
- );
260
- }
261
- out.push(`${side} ${pad("", inner - 2)} ${side}`);
262
- out.push(`${side} ${errLine} ${side}`);
263
- out.push(
264
- theme.fg("borderAccent", `\u2570${"\u2500".repeat(inner)}\u256f`),
265
- );
266
- return out;
231
+ const lines: string[] = [
232
+ "",
233
+ ` ${theme.fg("dim", step.hint)}`,
234
+ "",
235
+ ...inputLines.map((ln) => ` ${theme.fg("accent", `\u276f ${ln}`)}`),
236
+ "",
237
+ error
238
+ ? ` ${theme.fg("error", `! ${error}`)}`
239
+ : ` ${theme.fg("dim", "enter = save \u00b7 esc = cancel")}`,
240
+ ];
241
+ return frame(theme, {
242
+ width,
243
+ title: ` setup: ${step.label} `,
244
+ lines,
245
+ footer: { hint: " enter = save \u00b7 esc = cancel " },
246
+ });
267
247
  },
268
248
  invalidate(): void {
269
249
  input.invalidate();
270
250
  },
271
251
  handleInput(data: string): void {
272
252
  const kb = getKeybindings();
273
- // Plain Esc cancels even if Input doesn't dispatch it.
274
253
  if (kb.matches(data, "tui.select.cancel") || matchesKey(data, "escape")) {
275
254
  done(undefined);
276
255
  return;
@@ -281,9 +260,3 @@ function buildStepOverlay(
281
260
  },
282
261
  };
283
262
  }
284
-
285
- function pad(s: string, width: number): string {
286
- const w = visibleWidth(s);
287
- if (w >= width) return s;
288
- return s + " ".repeat(width - w);
289
- }