@astrosheep/pi-quota 0.8.0 → 0.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
@@ -18,7 +18,7 @@ Reload Pi after installing or changing settings. Use `/usage` for a persistent,
18
18
 
19
19
  A percentage always means **remaining**, not used. Unknown is `[?] ?`; an expired reset is `↺ due` with the stale percentage hidden until the next successful query. A wallet balance never becomes a percentage. Historical spend is not a quota.
20
20
 
21
- The footer shows at most two windows and `+N` for additional ones, for the current provider only. `/usage` queries all configured supported providers and shows successful results with all windows, grouped by provider; it does not open a modal or send messages to the model. Snapshots remain frozen at capture time. Query failures replace stale values in the footer and appear in `/usage --all`.
21
+ The footer shows at most two windows — set `quotaUsage.footerWindows` to a positive integer or `"all"` to show more — and `+N` for additional ones, for the current provider only. `/usage` queries all configured supported providers and shows successful results with all windows, grouped by provider; it does not open a modal or send messages to the model. Snapshots remain frozen at capture time. Query failures replace stale values in the footer and appear in `/usage --all`.
22
22
 
23
23
  ## Configure providers
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/pi-quota",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/bar/quota.ts CHANGED
@@ -24,14 +24,18 @@ function windowSpans(window: QuotaWindow, now: number): Span[] {
24
24
  ];
25
25
  }
26
26
 
27
- export function quotaElement(state: QuotaState, now = Date.now()): BarElement {
27
+ // Footer window cap: a positive count, or 'all' for every shared window.
28
+ export type FooterWindows = number | 'all';
29
+
30
+ export function quotaElement(state: QuotaState, now = Date.now(), maxWindows: FooterWindows = 2): BarElement {
28
31
  const element: BarElement = { id: 'quota', spans: [] };
29
32
  if (state.kind === 'hidden') return element;
30
33
  if (state.kind === 'loading') return { ...element, spans: [{ text: 'Quota …', tone: 'dim' }] };
31
34
  if (state.kind === 'error') return { ...element, spans: [{ text: `Quota ! ${errorText[state.code]}`, tone: 'warning' }] };
32
35
  const windows = state.snapshot.windows;
33
36
  const shared = windows.filter(window => !window.scope);
34
- const shown = (shared.length ? shared : windows).slice(0, 2);
37
+ const eligible = shared.length ? shared : windows;
38
+ const shown = maxWindows === 'all' ? eligible : eligible.slice(0, maxWindows);
35
39
  const spans: Span[] = state.snapshot.balance ? balanceSpans(state.snapshot.balance)
36
40
  : state.snapshot.allowance ? allowanceSpans(state.snapshot.allowance) : [];
37
41
  for (const window of shown) {
@@ -42,8 +46,8 @@ export function quotaElement(state: QuotaState, now = Date.now()): BarElement {
42
46
  return { ...element, spans };
43
47
  }
44
48
 
45
- export function renderFooter(state: QuotaState, paint: Paint = plain, now = Date.now()): string {
46
- return renderBar([quotaElement(state, now)], paint);
49
+ export function renderFooter(state: QuotaState, paint: Paint = plain, now = Date.now(), maxWindows: FooterWindows = 2): string {
50
+ return renderBar([quotaElement(state, now, maxWindows)], paint);
47
51
  }
48
52
 
49
53
  export function renderUsage(state: QuotaState, paint: Paint = plain, now = Date.now(), barWidth = 16): string[] {
package/src/config.ts CHANGED
@@ -6,12 +6,13 @@ import { createYesCodeAdapter, validateYesCodeOptions } from './query/yescode.ts
6
6
  import type { NewApiOptions } from './query/new-api.ts';
7
7
  import type { YesCodeOptions } from './query/yescode.ts';
8
8
  import type { QuotaAdapter } from './query/types.ts';
9
+ import type { FooterWindows } from './bar/quota.ts';
9
10
 
10
11
  export type ProviderQuotaConfig =
11
12
  | ({ adapter: 'new-api'; quotaPerUnit: number; currency: string } & Pick<NewApiOptions, 'dashboardAccessToken' | 'dashboardUserId'>)
12
13
  | ({ adapter: 'yescode' } & YesCodeOptions)
13
14
  | { adapter: 'deepseek' | 'sub2api' | 'opencode-go' };
14
- export interface QuotaConfig { providers: Record<string, ProviderQuotaConfig> }
15
+ export interface QuotaConfig { providers: Record<string, ProviderQuotaConfig>; footerWindows?: FooterWindows }
15
16
 
16
17
  const DASHBOARD_KEYS = ['dashboardAccessToken', 'dashboardUserId'] as const;
17
18
 
@@ -29,10 +30,16 @@ function parseDashboardOptions(item: Record<string, unknown>): Pick<NewApiOption
29
30
  return result;
30
31
  }
31
32
 
33
+ function parseFooterWindows(value: unknown): FooterWindows {
34
+ if (value === 'all') return value;
35
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 1) return value;
36
+ throw new QuotaConfigError();
37
+ }
38
+
32
39
  export class QuotaConfigError extends Error {
33
40
  constructor() {
34
41
  // Settings may accidentally contain secrets. Never echo content/values.
35
- super('Invalid settings.json quotaUsage. Use providers with adapter "new-api", "deepseek", "sub2api", "opencode-go" or "yescode".');
42
+ super('Invalid settings.json quotaUsage. Use providers with adapter "new-api", "deepseek", "sub2api", "opencode-go" or "yescode", and optionally footerWindows as a positive integer or "all".');
36
43
  }
37
44
  }
38
45
 
@@ -41,7 +48,7 @@ function record(value: unknown): value is Record<string, unknown> {
41
48
  }
42
49
 
43
50
  export function parseQuotaConfig(value: unknown): QuotaConfig {
44
- if (!record(value) || Object.keys(value).some(key => key !== 'providers')
51
+ if (!record(value) || Object.keys(value).some(key => key !== 'providers' && key !== 'footerWindows')
45
52
  || !record(value.providers) || Object.keys(value.providers).length > 64) throw new QuotaConfigError();
46
53
  const providers: Record<string, ProviderQuotaConfig> = Object.create(null);
47
54
  for (const [provider, item] of Object.entries(value.providers)) {
@@ -76,7 +83,14 @@ export function parseQuotaConfig(value: unknown): QuotaConfig {
76
83
  providers[provider] = { adapter: 'new-api', ...settings, ...parseDashboardOptions(item) };
77
84
  } catch { throw new QuotaConfigError(); }
78
85
  }
79
- return { providers };
86
+ const config: QuotaConfig = { providers };
87
+ if (value.footerWindows !== undefined) config.footerWindows = parseFooterWindows(value.footerWindows);
88
+ return config;
89
+ }
90
+
91
+ export function loadQuotaFooterWindows(settings: unknown): FooterWindows | undefined {
92
+ if (!record(settings) || settings.quotaUsage === undefined) return undefined;
93
+ return parseQuotaConfig(settings.quotaUsage).footerWindows;
80
94
  }
81
95
 
82
96
  export function loadQuotaAdaptersFromSettings(settings: unknown): QuotaAdapter[] {
package/src/extension.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
2
2
  import { renderFooter } from './bar/quota.ts';
3
+ import type { FooterWindows } from './bar/quota.ts';
3
4
  import { captureAllUsage, USAGE_ENTRY, usageCardComponent, usageLines, compactUsageLines } from './bar/card.ts';
4
5
  import type { UsageCard } from './bar/card.ts';
5
6
  import { codexAdapter } from './query/codex.ts';
@@ -23,6 +24,7 @@ export interface QuotaExtensionOptions {
23
24
  adapters?: readonly QuotaAdapter[];
24
25
  providers?: readonly string[]; // Extra configured quota providers to include in /usage
25
26
  footer?: boolean; // false: query + /usage + structured events only
27
+ footerWindows?: FooterWindows; // footer window cap; default 2, 'all' shows every window
26
28
  intervalMs?: number;
27
29
  timeoutMs?: number;
28
30
  }
@@ -57,7 +59,7 @@ export function createQuotaExtension(options: QuotaExtensionOptions = {}) {
57
59
  const ctx = current;
58
60
  const paint = process.env.NO_COLOR !== undefined ? undefined
59
61
  : (tone: Parameters<typeof ctx.ui.theme.fg>[0], text: string) => ctx.ui.theme.fg(tone, text);
60
- const text = renderFooter(controller.state, paint) || undefined;
62
+ const text = renderFooter(controller.state, paint, Date.now(), options.footerWindows) || undefined;
61
63
  if (text !== lastStatus) {
62
64
  ctx.ui.setStatus(STATUS_KEY, text);
63
65
  lastStatus = text;
@@ -236,10 +238,11 @@ export function createQuotaExtension(options: QuotaExtensionOptions = {}) {
236
238
  hasUsageSnapshot = true;
237
239
  pi.appendEntry<UsageCard>(USAGE_ENTRY, card);
238
240
  // Custom entry renderers are TUI-only; RPC clients still get readable output.
239
- // Fenced so clients that render markdown use a monospace font; compact
240
- // layout because narrow clients (phones) wrap long grid lines anyway.
241
+ // No markdown fence: paseo routes notify to a plain-text Notification
242
+ // component (no markdown), so fence markers would show literally. The
243
+ // compact layout works as preserved-whitespace plain text on its own.
241
244
  if (ctx.mode !== 'tui') {
242
- ctx.ui.notify(`\`\`\`\n${compactUsageLines(card, undefined).join('\n')}\n\`\`\``, 'info');
245
+ ctx.ui.notify(compactUsageLines(card, undefined).join('\n'), 'info');
243
246
  }
244
247
  } finally {
245
248
  if (spinner) { clearInterval(spinner); spinner = undefined; }
package/src/index.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { getAgentDir, SettingsManager } from '@earendil-works/pi-coding-agent';
2
2
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
3
3
  import { createQuotaExtension } from './extension.ts';
4
- import { loadQuotaAdaptersFromSettings, QuotaConfigError } from './config.ts';
4
+ import { loadQuotaAdaptersFromSettings, loadQuotaFooterWindows, QuotaConfigError } from './config.ts';
5
+ import type { FooterWindows } from './bar/quota.ts';
5
6
  import type { QuotaAdapter } from './query/types.ts';
6
7
 
7
8
  export default function (pi: ExtensionAPI): void {
8
9
  let adapters: QuotaAdapter[] = [];
10
+ let footerWindows: FooterWindows | undefined;
9
11
  try {
10
12
  // Quota provider bindings belong to the user's global Pi settings. Project
11
13
  // settings are intentionally not read here: a project must not redirect a
@@ -14,11 +16,12 @@ export default function (pi: ExtensionAPI): void {
14
16
  projectTrusted: false,
15
17
  }).getGlobalSettings() as unknown;
16
18
  adapters = loadQuotaAdaptersFromSettings(settings);
19
+ footerWindows = loadQuotaFooterWindows(settings);
17
20
  } catch {
18
21
  pi.on('session_start', (_event, ctx) => {
19
22
  if (ctx.hasUI) ctx.ui.notify(new QuotaConfigError().message, 'warning');
20
23
  });
21
24
  }
22
25
  // Invalid optional settings never disable the built-in Codex/Kimi/OpenCode Go adapters.
23
- createQuotaExtension({ adapters })(pi);
26
+ createQuotaExtension({ adapters, footerWindows })(pi);
24
27
  }