@adeildo/pi-providers 5.7.2 → 5.9.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
@@ -81,3 +81,12 @@ The Claude plan billing comes from [pi-claude-max](https://github.com/bradennss/
81
81
  Several accounts per provider, with a switch to the next one when a limit hits, follow [pi-multiprovider](https://github.com/monotykamary/pi-multiprovider) by monotykamary, also MIT. This one keeps them inside pi's own login, with no OAuth of its own.
82
82
 
83
83
  Part of [pi-harness](https://github.com/felipeadeildo/pi-harness), which brings every piece in one install.
84
+
85
+ ## License
86
+
87
+ MIT
88
+
89
+ <p align="center">
90
+ <a href="https://adeildo.dev"><img src="https://adeildo.dev/assets/images/cat.svg" width="42" height="42" alt="adeildo.dev"></a><br>
91
+ Brought to you by <a href="https://adeildo.dev">adeildo.dev</a>
92
+ </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adeildo/pi-providers",
3
- "version": "5.7.2",
3
+ "version": "5.9.0",
4
4
  "description": "Provider features for Pi. Bills Anthropic OAuth requests to the Claude Pro or Max plan instead of extra usage.",
5
5
  "keywords": [
6
6
  "anthropic",
@@ -37,7 +37,7 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@adeildo/pi-kit": "5.7.2"
40
+ "@adeildo/pi-kit": "5.9.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@earendil-works/pi-ai": "*",
@@ -1,6 +1,7 @@
1
1
  // The accounts of one provider in our frame: which one is in use, what each plan has left, and the
2
2
  // keys to switch, rename, remove or add one. Renaming and removing happen in place, so the list stays
3
3
  // where it is and says what changed.
4
+ import { frame, frameInner, type FrameLine } from "@adeildo/pi-kit";
4
5
  import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
5
6
  import {
6
7
  type Component,
@@ -15,7 +16,6 @@ import {
15
16
  } from "@earendil-works/pi-tui";
16
17
 
17
18
  import type { UsageWindow } from "../usage/types.ts";
18
- import { frameBottom, frameInner, frameRow, frameTop } from "./frame.ts";
19
19
  import { quotaWindows } from "./quota.ts";
20
20
 
21
21
  /** What a row says past the account's name: its plan, or the trouble it is in. */
@@ -157,24 +157,25 @@ export class AccountsView implements Component, Focusable {
157
157
  const states = new Map(rows.map((row) => [row.id, describeState(this.theme, row.state)]));
158
158
  const columns = this.columns(rows, [...states.values()], inner);
159
159
 
160
- const lines: string[] = [];
160
+ const lines: FrameLine[] = [];
161
161
  if (rows.length === 0) lines.push(this.theme.fg("muted", this.listing.empty), "");
162
162
  for (const item of items.slice(start, end)) {
163
163
  const row = rows.find((candidate) => candidate.id === item);
164
- if (row === undefined) lines.push(this.addLine());
165
- else lines.push(this.accountLine(row, columns, states.get(row.id) ?? "", inner));
164
+ const text =
165
+ row === undefined
166
+ ? this.addLine()
167
+ : this.accountLine(row, columns, states.get(row.id) ?? "", inner);
168
+ lines.push({ text, selected: item === this.cursor });
166
169
  }
167
- lines.push("");
168
170
  const status = this.statusLine(rows);
169
- if (status !== undefined) lines.push(status);
171
+ if (status !== undefined) lines.push("", status);
170
172
  const position = start > 0 || end < items.length ? `${at + 1}/${items.length}` : undefined;
171
- lines.push(this.hint(rows, position));
172
-
173
- return [
174
- frameTop(this.theme, `${this.listing.provider} accounts`, width),
175
- ...lines.map((line) => frameRow(this.theme, line, inner)),
176
- frameBottom(this.theme, width),
177
- ];
173
+ const row = rows.find((candidate) => candidate.id === this.cursor);
174
+ return frame(this.theme, width, lines, {
175
+ title: `${this.listing.provider} accounts`,
176
+ keys: this.keys(row),
177
+ footnote: position === undefined ? "" : this.theme.fg("dim", position),
178
+ });
178
179
  }
179
180
 
180
181
  private browsing(data: string): void {
@@ -287,7 +288,7 @@ export class AccountsView implements Component, Focusable {
287
288
  const editing = this.mode.kind !== "browse" && this.mode.id === row.id;
288
289
  // Pi's own field draws the cursor where it is and scrolls a name longer than the column.
289
290
  if (editing && this.mode.kind === "rename") return this.input.render(width)[0] ?? "";
290
- const label = truncateToWidth(row.label, NAME_LIMIT, "…");
291
+ const label = truncateToWidth(row.label, NAME_LIMIT, "...");
291
292
  if (editing) return this.theme.fg("error", label);
292
293
  return chosen ? this.theme.bold(label) : label;
293
294
  }
@@ -308,15 +309,6 @@ export class AccountsView implements Component, Focusable {
308
309
  return this.theme.fg(this.note.tone, this.note.text);
309
310
  }
310
311
 
311
- private hint(rows: readonly AccountRow[], position: string | undefined): string {
312
- const row = rows.find((candidate) => candidate.id === this.cursor);
313
- const text = this.keys(row).map(
314
- ([key, action]) => `${this.theme.fg("muted", key)} ${this.theme.fg("dim", action)}`,
315
- );
316
- if (position !== undefined) text.push(this.theme.fg("dim", position));
317
- return text.join(" ");
318
- }
319
-
320
312
  /** The keys that do something right now, and what each one does. */
321
313
  private keys(row: AccountRow | undefined): [string, string][] {
322
314
  if (this.mode.kind === "rename") {
@@ -368,7 +360,7 @@ export function describeState(theme: Theme, state: AccountState): string {
368
360
  case "quota":
369
361
  return quotaWindows(theme, state.windows);
370
362
  case "reading":
371
- return theme.fg("dim", "reading the plan…");
363
+ return theme.fg("dim", "reading the plan...");
372
364
  case "signIn":
373
365
  return theme.fg("error", "sign in again");
374
366
  case "spent":
package/src/ui/login.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // it waits for. The provider drives it through the interaction pi would hand it.
3
3
  import { spawn } from "node:child_process";
4
4
 
5
+ import { frame, frameInner, type KeyHint } from "@adeildo/pi-kit";
5
6
  import type { AuthEvent, AuthPrompt, ProviderAuthInteraction } from "@earendil-works/pi-ai";
6
7
  import {
7
8
  copyToClipboard,
@@ -21,8 +22,6 @@ import {
21
22
  truncateToWidth,
22
23
  } from "@earendil-works/pi-tui";
23
24
 
24
- import { frameBottom, frameInner, frameRow, frameTop } from "./frame.ts";
25
-
26
25
  const MAX_LOG = 5;
27
26
 
28
27
  /** Pi's own Text wraps the URL and the instructions into the rows the frame draws. */
@@ -139,12 +138,11 @@ export class LoginView implements Component, Focusable {
139
138
  rows.push(this.linkHint());
140
139
  }
141
140
  if (this.code !== undefined) rows.push(this.theme.bold(this.code));
142
- rows.push("", ...this.promptRows(inner), "", this.hint());
143
- return [
144
- frameTop(this.theme, `Entrar · ${this.label}`, width),
145
- ...rows.map((row) => frameRow(this.theme, row, inner)),
146
- frameBottom(this.theme, width),
147
- ];
141
+ rows.push("", ...this.promptRows(inner));
142
+ return frame(this.theme, width, rows, {
143
+ title: `Entrar · ${this.label}`,
144
+ keys: this.hint(),
145
+ });
148
146
  }
149
147
 
150
148
  /** Ends the screen; the login it was drawing ended one way or another. */
@@ -237,7 +235,7 @@ export class LoginView implements Component, Focusable {
237
235
 
238
236
  private promptRows(inner: number): string[] {
239
237
  const pending = this.pending;
240
- if (pending === undefined) return [this.theme.fg("dim", "waiting…")];
238
+ if (pending === undefined) return [this.theme.fg("dim", "waiting...")];
241
239
  const room = Math.max(1, inner - 2);
242
240
  const asked = this.theme.fg("text", truncateToWidth(pending.prompt.message, inner));
243
241
  if (pending.prompt.type === "select") {
@@ -259,20 +257,16 @@ export class LoginView implements Component, Focusable {
259
257
  return [asked, `${this.theme.fg("accent", "❯ ")}${field}`];
260
258
  }
261
259
 
262
- private hint(): string {
263
- const keys: [string, string][] =
264
- this.pending?.prompt.type === "select"
265
- ? [
266
- ["↑↓", "move"],
267
- ["enter", "choose"],
268
- ["esc", "cancel"],
269
- ]
270
- : [
271
- ["enter", "submit"],
272
- ["esc", "cancel"],
273
- ];
274
- return keys
275
- .map(([key, action]) => `${this.theme.fg("muted", key)} ${this.theme.fg("dim", action)}`)
276
- .join(" ");
260
+ private hint(): KeyHint[] {
261
+ return this.pending?.prompt.type === "select"
262
+ ? [
263
+ ["↑↓", "move"],
264
+ ["enter", "choose"],
265
+ ["esc", "cancel"],
266
+ ]
267
+ : [
268
+ ["enter", "submit"],
269
+ ["esc", "cancel"],
270
+ ];
277
271
  }
278
272
  }
package/src/ui/picker.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Our dialogs wear the same frame as the rest of the harness. A host that cannot draw a component,
2
2
  // like RPC, falls back to pi's own select.
3
- import { oneAtATime } from "@adeildo/pi-kit";
3
+ import { frame, frameInner, type KeyHint, oneAtATime } from "@adeildo/pi-kit";
4
4
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import type { Theme } from "@earendil-works/pi-coding-agent";
6
6
  import {
@@ -12,8 +12,6 @@ import {
12
12
  visibleWidth,
13
13
  } from "@earendil-works/pi-tui";
14
14
 
15
- import { frameBottom, frameInner, frameRow, frameTop } from "./frame.ts";
16
-
17
15
  /** One row of a picker: a label, and a second column on the right. */
18
16
  export interface PickOption {
19
17
  label: string;
@@ -21,6 +19,11 @@ export interface PickOption {
21
19
  }
22
20
 
23
21
  const POINTER = "\u276f ";
22
+ const KEYS: KeyHint[] = [
23
+ ["↑↓", "move"],
24
+ ["enter", "select"],
25
+ ["esc", "cancel"],
26
+ ];
24
27
  const MAX_VISIBLE = 10;
25
28
  const GAP = 2;
26
29
  const MIN_DESCRIPTION = 12;
@@ -109,15 +112,15 @@ export class FramedSelect implements Component {
109
112
  start > 0 || end < this.options.length
110
113
  ? `${this.selected + 1}/${this.options.length}`
111
114
  : undefined;
112
- return [
113
- frameTop(this.theme, this.title, width),
114
- ...this.options
115
- .slice(start, end)
116
- .map((option, index) => this.line(this.row(option, start + index, inner), inner)),
117
- this.line("", inner),
118
- this.line(this.hint(position), inner),
119
- frameBottom(this.theme, width),
120
- ];
115
+ const rows = this.options.slice(start, end).map((option, index) => ({
116
+ text: this.row(option, start + index, inner),
117
+ selected: start + index === this.selected,
118
+ }));
119
+ return frame(this.theme, width, rows, {
120
+ title: this.title,
121
+ keys: KEYS,
122
+ footnote: position === undefined ? "" : this.theme.fg("dim", position),
123
+ });
121
124
  }
122
125
 
123
126
  /** One line: the pointer, the label, and the description in a column shared by every row. */
@@ -161,18 +164,4 @@ export class FramedSelect implements Component {
161
164
  const option = this.options[this.selected];
162
165
  if (option !== undefined) this.done(labelOf(option));
163
166
  }
164
-
165
- private hint(position: string | undefined): string {
166
- const keys = [
167
- `${this.theme.fg("muted", "↑↓")} ${this.theme.fg("dim", "move")}`,
168
- `${this.theme.fg("muted", "enter")} ${this.theme.fg("dim", "select")}`,
169
- `${this.theme.fg("muted", "esc")} ${this.theme.fg("dim", "cancel")}`,
170
- ];
171
- if (position !== undefined) keys.push(this.theme.fg("dim", position));
172
- return keys.join(" ");
173
- }
174
-
175
- private line(text: string, inner: number): string {
176
- return frameRow(this.theme, text, inner);
177
- }
178
167
  }
package/src/ui/frame.ts DELETED
@@ -1,27 +0,0 @@
1
- // The rounded frame every dialog of this package wears.
2
- import type { Theme } from "@earendil-works/pi-coding-agent";
3
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
-
5
- /** The columns a row's text may use, between the frame's sides and their padding. */
6
- export function frameInner(width: number): number {
7
- return Math.max(1, width - 4);
8
- }
9
-
10
- export function frameTop(theme: Theme, title: string, width: number): string {
11
- const border = (text: string) => theme.fg("border", text);
12
- const room = Math.max(0, width - 5);
13
- const label = theme.fg("accent", truncateToWidth(title, Math.max(0, room - 3), "…"));
14
- const dashes = Math.max(0, room - visibleWidth(label));
15
- return `${border("╭─ ")}${label}${border(` ${"─".repeat(dashes)}╮`)}`;
16
- }
17
-
18
- export function frameRow(theme: Theme, text: string, inner: number): string {
19
- const border = theme.fg("border", "│");
20
- const clipped = truncateToWidth(text, inner);
21
- const pad = " ".repeat(Math.max(0, inner - visibleWidth(clipped)));
22
- return `${border} ${clipped}${pad} ${border}`;
23
- }
24
-
25
- export function frameBottom(theme: Theme, width: number): string {
26
- return theme.fg("border", `╰${"─".repeat(Math.max(0, width - 2))}╯`);
27
- }