@jameslovespancakes/pi-plus 1.0.11 → 1.0.12

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.
Files changed (120) hide show
  1. package/README.md +44 -20
  2. package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
  3. package/package.json +6 -3
  4. package/src/core/accounts/oauth-pool.ts +102 -0
  5. package/src/core/accounts/registry.ts +8 -25
  6. package/src/core/accounts/routing.ts +122 -0
  7. package/src/core/anthropic/client-identity.ts +9 -64
  8. package/src/core/anthropic/quota.ts +8 -53
  9. package/src/core/anthropic/routing.ts +40 -115
  10. package/src/core/anthropic/store.ts +7 -9
  11. package/src/core/catalog/quality.ts +33 -15
  12. package/src/core/codex/quota.ts +8 -2
  13. package/src/core/codex/store.ts +12 -9
  14. package/src/core/config.ts +4 -17
  15. package/src/core/store.ts +13 -12
  16. package/src/domains/models/provider-picker.ts +1 -1
  17. package/src/domains/setup/index.ts +23 -23
  18. package/src/domains/subscriptions/accounts-picker.ts +18 -37
  19. package/src/domains/subscriptions/accounts.ts +23 -29
  20. package/src/domains/subscriptions/footer.ts +48 -29
  21. package/src/domains/subscriptions/index.ts +11 -27
  22. package/src/domains/subscriptions/provider.ts +38 -103
  23. package/src/domains/subscriptions/providers/anthropic.ts +5 -17
  24. package/src/domains/subscriptions/providers/codex.ts +171 -4
  25. package/src/domains/subscriptions/providers/hosted.ts +18 -0
  26. package/src/domains/subscriptions/providers/oauth-pool.ts +273 -0
  27. package/src/domains/subscriptions/routing.ts +19 -11
  28. package/src/domains/workflows/LICENSE.md +21 -0
  29. package/src/domains/workflows/index.ts +836 -0
  30. package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
  31. package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
  32. package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
  33. package/src/domains/workflows/runtime/agent-attempt.ts +147 -0
  34. package/src/domains/workflows/runtime/agent-limits.ts +43 -0
  35. package/src/domains/workflows/runtime/agent-replay.ts +399 -0
  36. package/src/domains/workflows/runtime/agent-retry.ts +116 -0
  37. package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
  38. package/src/domains/workflows/runtime/agent-runner.ts +203 -0
  39. package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
  40. package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
  41. package/src/domains/workflows/runtime/agent-session.ts +382 -0
  42. package/src/domains/workflows/runtime/agent-skills.ts +270 -0
  43. package/src/domains/workflows/runtime/agent-workspace.ts +103 -0
  44. package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
  45. package/src/domains/workflows/runtime/background-workflows.ts +492 -0
  46. package/src/domains/workflows/runtime/budget.ts +53 -0
  47. package/src/domains/workflows/runtime/cancellation.ts +87 -0
  48. package/src/domains/workflows/runtime/command-completions.ts +36 -0
  49. package/src/domains/workflows/runtime/concurrency.ts +403 -0
  50. package/src/domains/workflows/runtime/debug.ts +3 -0
  51. package/src/domains/workflows/runtime/diff-capture.ts +81 -0
  52. package/src/domains/workflows/runtime/discovery.ts +137 -0
  53. package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
  54. package/src/domains/workflows/runtime/dynamax.ts +330 -0
  55. package/src/domains/workflows/runtime/engine.ts +543 -0
  56. package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
  57. package/src/domains/workflows/runtime/finalizers.ts +66 -0
  58. package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
  59. package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
  60. package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
  61. package/src/domains/workflows/runtime/journal.ts +313 -0
  62. package/src/domains/workflows/runtime/model-profiles.ts +310 -0
  63. package/src/domains/workflows/runtime/options.ts +157 -0
  64. package/src/domains/workflows/runtime/perf.ts +146 -0
  65. package/src/domains/workflows/runtime/pi-compat.ts +32 -0
  66. package/src/domains/workflows/runtime/process-runner.ts +260 -0
  67. package/src/domains/workflows/runtime/progress-types.ts +59 -0
  68. package/src/domains/workflows/runtime/progress.ts +400 -0
  69. package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
  70. package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
  71. package/src/domains/workflows/runtime/research-contract.ts +89 -0
  72. package/src/domains/workflows/runtime/research-evidence.ts +289 -0
  73. package/src/domains/workflows/runtime/resume-context.ts +751 -0
  74. package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
  75. package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
  76. package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
  77. package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
  78. package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
  79. package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
  80. package/src/domains/workflows/runtime/review/review-format.ts +133 -0
  81. package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
  82. package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
  83. package/src/domains/workflows/runtime/review/review-report.ts +42 -0
  84. package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
  85. package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
  86. package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
  87. package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
  88. package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
  89. package/src/domains/workflows/runtime/session-identity.ts +6 -0
  90. package/src/domains/workflows/runtime/structured-output.ts +32 -0
  91. package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
  92. package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
  93. package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
  94. package/src/domains/workflows/runtime/types.ts +255 -0
  95. package/src/domains/workflows/runtime/ui/display-text.ts +13 -0
  96. package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
  97. package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
  98. package/src/domains/workflows/runtime/ui/workflow-inspector.ts +398 -0
  99. package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
  100. package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
  101. package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
  102. package/src/domains/workflows/runtime/unknown-error.ts +8 -0
  103. package/src/domains/workflows/runtime/usage.ts +341 -0
  104. package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
  105. package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
  106. package/src/domains/workflows/runtime/workflow-module.ts +75 -0
  107. package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
  108. package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
  109. package/src/domains/workflows/runtime/workflow-run-history.ts +162 -0
  110. package/src/domains/workflows/runtime/workflow-run-record.ts +635 -0
  111. package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
  112. package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
  113. package/src/domains/workflows/runtime/workflows.ts +40 -0
  114. package/src/domains/workflows/runtime/worktree.ts +581 -0
  115. package/src/domains/workflows/workflows/code-review.ts +232 -0
  116. package/src/domains/workflows/workflows/diagnose.ts +154 -0
  117. package/src/domains/workflows/workflows/perf-review.ts +149 -0
  118. package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
  119. package/src/domains/workflows/workflows/research.ts +169 -0
  120. package/src/services/usage-service.ts +6 -30
@@ -35,37 +35,37 @@ async function inspect(ctx: any): Promise<Feature[]> {
35
35
 
36
36
  /* subscriptions */
37
37
  const providers = accountProviders();
38
- const accountSummary: string[] = [];
39
- let anySignedIn = false;
40
- for (const provider of providers) {
41
- // pi holds the primary credential itself; the adapter only knows about the
42
- // extra pooled accounts. Being signed in at all is what makes this usable.
43
- let primary = false;
38
+ const accountStates = await Promise.all(providers.map(async (provider) => {
39
+ const primaryAuth = Promise.resolve()
40
+ .then(() => ctx.modelRegistry.getProviderAuth(provider.id))
41
+ .then((result: any) => !!result?.auth?.apiKey)
42
+ .catch(() => false);
44
43
  try {
45
- primary = !!(await ctx.modelRegistry.getProviderAuth(provider.id))?.auth?.apiKey;
46
- } catch { /* provider not configured */ }
47
-
48
- try {
49
- const accounts = await provider.list();
50
- const routing = provider.routing ? await provider.routing.get() : "n/a";
44
+ const [primary, accounts, routing] = await Promise.all([
45
+ primaryAuth,
46
+ provider.list(),
47
+ provider.routing?.get(),
48
+ ]);
49
+ const signedIn = primary || accounts.length > 0;
51
50
  const pooled = accounts.length + (primary ? 1 : 0);
52
- anySignedIn ||= primary || accounts.length > 0;
53
- accountSummary.push(
54
- primary || accounts.length > 0
55
- ? `${provider.id}: ${pooled} account(s), routing=${routing}`
51
+ return {
52
+ signedIn,
53
+ summary: signedIn
54
+ ? `${provider.id}: ${pooled} account(s), routing=${routing ?? "n/a"}`
56
55
  : `${provider.id}: not signed in`,
57
- );
56
+ };
58
57
  } catch {
59
- accountSummary.push(`${provider.id}: unreadable`);
58
+ return { signedIn: await primaryAuth, summary: `${provider.id}: unreadable` };
60
59
  }
61
- }
60
+ }));
61
+ const anySignedIn = accountStates.some((state) => state.signedIn);
62
62
  features.push({
63
63
  name: "Subscriptions",
64
64
  ready: anySignedIn,
65
- detail: accountSummary.join("; ") || "no account providers registered",
66
- commands: ["/account", "/account <provider> add", "/routing standard|optimal", "/usage"],
67
- setup: anySignedIn ? undefined : "/account anthropic add",
68
- open: "/account",
65
+ detail: accountStates.map((state) => state.summary).join("; ") || "no account providers registered",
66
+ commands: ["/accounts", "/accounts add <provider>", "/routing sequential|quota-aware", "/usage"],
67
+ setup: anySignedIn ? undefined : "/accounts add anthropic",
68
+ open: "/accounts",
69
69
  });
70
70
 
71
71
  /* benchmarks */
@@ -2,14 +2,7 @@ import type { Component } from "@earendil-works/pi-tui";
2
2
  import { hasTruecolor, levelColor } from "../../ui/format.ts";
3
3
  import type { AccountProvider, ManagedAccount } from "../../core/accounts/registry.ts";
4
4
 
5
- /**
6
- * The `/accounts` list.
7
- *
8
- * Mirrors the provider picker: an inline SettingsList inside pi's own rule
9
- * chrome, toggled in place so nothing redraws the screen. Rows are grouped by
10
- * provider, because an account label alone ("Personal") does not say which
11
- * subscription it belongs to.
12
- */
5
+ /** Inline account picker grouped by provider. */
13
6
 
14
7
  export type AccountState = "enabled" | "disabled";
15
8
 
@@ -36,28 +29,25 @@ export interface AccountRow {
36
29
  detail?: string;
37
30
  }
38
31
 
39
- /** Flattens every provider's accounts into one ordered list. */
32
+ /** Flattens provider accounts while loading providers in parallel. */
40
33
  export async function accountRows(providers: AccountProvider[]): Promise<AccountRow[]> {
41
- const rows: AccountRow[] = [];
42
- for (const provider of providers) {
43
- let accounts: ManagedAccount[] = [];
34
+ const groups = await Promise.all(providers.map(async (provider) => {
35
+ let accounts: ManagedAccount[];
44
36
  try {
45
37
  accounts = await provider.list();
46
38
  } catch {
47
- continue; // A provider that cannot enumerate is simply not shown.
39
+ return [];
48
40
  }
49
- for (const account of accounts) {
50
- rows.push({
51
- id: `${provider.id}:${account.id}`,
52
- providerId: provider.id,
53
- providerLabel: provider.label,
54
- label: account.label,
55
- state: account.enabled ? "enabled" : "disabled",
56
- detail: account.primary ? "primary" : undefined,
57
- });
58
- }
59
- }
60
- return rows;
41
+ return accounts.map((account) => ({
42
+ id: `${provider.id}:${account.id}`,
43
+ providerId: provider.id,
44
+ providerLabel: provider.label,
45
+ label: account.label,
46
+ state: account.enabled ? "enabled" as const : "disabled" as const,
47
+ detail: account.primary ? "primary" : undefined,
48
+ }));
49
+ }));
50
+ return groups.flat();
61
51
  }
62
52
 
63
53
  function labelFor(theme: any, row: AccountRow): string {
@@ -79,13 +69,7 @@ function framed(theme: any, list: any, title: string): Component {
79
69
  } as Component;
80
70
  }
81
71
 
82
- /**
83
- * What closing the picker asked for.
84
- *
85
- * The wizards run AFTER the picker closes rather than inside it, so they can
86
- * use pi's ordinary select/input prompts instead of being reimplemented as
87
- * nested TUI components.
88
- */
72
+ /** Wizard requested when the picker closes. */
89
73
  export type PickerAction = { kind: "add" } | { kind: "rename" } | undefined;
90
74
 
91
75
  /** Action row ids are namespaced so they cannot collide with an account id. */
@@ -94,8 +78,7 @@ const RENAME_ID = "__action_rename";
94
78
 
95
79
  export interface AccountPickerDeps {
96
80
  rows: () => Promise<AccountRow[]>;
97
- /** Applies a toggle and returns the resulting state. Synchronous so the
98
- * label, dot and value all change in one render. */
81
+ /** Applies a toggle synchronously for one-render updates. */
99
82
  toggle: (providerId: string, accountId: string) => AccountState;
100
83
  }
101
84
 
@@ -118,9 +101,7 @@ export async function openAccountsPicker(ctx: any, deps: AccountPickerDeps): Pro
118
101
  description: row.detail,
119
102
  }));
120
103
 
121
- // Actions live as rows rather than hidden keystrokes, so they are
122
- // discoverable. A single-entry `values` makes Enter fire onChange without
123
- // the row appearing to cycle through anything.
104
+ // Single-value action rows trigger onChange without cycling.
124
105
  items.push({
125
106
  id: ADD_ID,
126
107
  label: theme.fg("accent", "+ Add account"),
@@ -35,8 +35,10 @@ async function openBrowser(pi: ExtensionAPI, url: string): Promise<void> {
35
35
  function bridge(pi: ExtensionAPI, ctx: any): AccountContext {
36
36
  return {
37
37
  hasUI: ctx.hasUI,
38
+ signal: ctx.signal,
38
39
  ui: {
39
40
  input: (title, placeholder) => ctx.ui.input(title, placeholder),
41
+ select: (title, options) => ctx.ui.select(title, options),
40
42
  confirm: (title, message) => ctx.ui.confirm(title, message),
41
43
  notify: (message, type) => ctx.ui.notify(message, type ?? "info"),
42
44
  },
@@ -58,34 +60,34 @@ async function listAll(ctx: any): Promise<void> {
58
60
  return;
59
61
  }
60
62
 
61
- const blocks: string[] = [];
62
- for (const provider of providers) {
63
+ const blocks = await Promise.all(providers.map(async (provider): Promise<string[]> => {
63
64
  try {
64
- const accounts = await provider.list();
65
- const routing = provider.routing ? ` · routing: ${await provider.routing.get()}` : "";
66
- blocks.push(
65
+ const [accounts, mode] = await Promise.all([
66
+ provider.list(),
67
+ provider.routing?.get(),
68
+ ]);
69
+ const routing = mode ? ` · routing: ${mode}` : "";
70
+ return [
67
71
  `${provider.label} (${provider.id})${routing}`,
68
72
  ...(accounts.length > 0
69
73
  ? accounts.map((account) => ` ${describe(account)}`)
70
- : [` no additional accounts, add one with /account ${provider.id} add`]),
71
- );
74
+ : [` no additional accounts, add one with /accounts add ${provider.id}`]),
75
+ ];
72
76
  } catch (error) {
73
- blocks.push(`${provider.label} (${provider.id}): ${error instanceof Error ? error.message : String(error)}`);
77
+ return [`${provider.label} (${provider.id}): ${error instanceof Error ? error.message : String(error)}`];
74
78
  }
75
- }
76
- ctx.ui.notify(blocks.join("\n"), "info");
79
+ }));
80
+ ctx.ui.notify(blocks.flat().join("\n"), "info");
77
81
  }
78
82
 
79
-
80
-
81
83
  async function pickAccount(ctx: any, provider: AccountProvider): Promise<string | undefined> {
82
84
  const accounts = await provider.list();
83
85
  if (accounts.length === 0) {
84
- ctx.ui.notify(`No ${provider.label} accounts yet. Add one with /account ${provider.id} add.`, "info");
86
+ ctx.ui.notify(`No ${provider.label} accounts yet. Add one with /accounts add ${provider.id}.`, "info");
85
87
  return undefined;
86
88
  }
87
89
  if (!ctx.hasUI) {
88
- ctx.ui.notify(`Specify an account: /account ${provider.id} reauth <label>`, "warning");
90
+ ctx.ui.notify(`Choose an account with /accounts reauth ${provider.id}.`, "warning");
89
91
  return undefined;
90
92
  }
91
93
 
@@ -96,19 +98,15 @@ async function pickAccount(ctx: any, provider: AccountProvider): Promise<string
96
98
  }
97
99
 
98
100
  export function registerAccountCommands(pi: ExtensionAPI): void {
99
- /** Toggles one account and returns the state it ended in. */
100
101
  const toggle = (providerId: string, accountId: string): AccountState => {
101
102
  const provider = accountProvider(providerId);
102
103
  if (!provider?.setEnabled) throw new Error(`${providerId} accounts cannot be disabled.`);
103
104
  const next = pendingState.get(`${providerId}:${accountId}`) === "enabled" ? "disabled" : "enabled";
104
105
  pendingState.set(`${providerId}:${accountId}`, next);
105
- // Adapters persist asynchronously; the picker needs the answer now, so the
106
- // write is fired off and failures surface as a notification.
107
106
  void provider.setEnabled(accountId, next === "enabled").catch(() => {});
108
107
  return next;
109
108
  };
110
109
 
111
- /** Mirror of on-disk state, so the synchronous toggle can answer instantly. */
112
110
  const pendingState = new Map<string, AccountState>();
113
111
 
114
112
  const rows = async () => {
@@ -118,8 +116,7 @@ export function registerAccountCommands(pi: ExtensionAPI): void {
118
116
  return list;
119
117
  };
120
118
 
121
- /** Adds an account: choose provider, name it, authorize. */
122
- async function addAccount(ctx: any, providerId?: string): Promise<void> {
119
+ async function addAccount(ctx: any, providerId?: string, requestedLabel?: string): Promise<void> {
123
120
  if (!ctx.hasUI) {
124
121
  ctx.ui.notify("Adding an account requires interactive Pi mode.", "error");
125
122
  return;
@@ -144,7 +141,8 @@ export function registerAccountCommands(pi: ExtensionAPI): void {
144
141
  }
145
142
  if (!provider) return;
146
143
 
147
- const label = await ctx.ui.input(`${provider.label} account name`, "Work, Personal, etc.");
144
+ const label = requestedLabel?.trim()
145
+ || await ctx.ui.input(`${provider.label} account name`, "Work, Personal, etc.");
148
146
  if (!label?.trim()) return;
149
147
 
150
148
  try {
@@ -155,7 +153,6 @@ export function registerAccountCommands(pi: ExtensionAPI): void {
155
153
  }
156
154
  }
157
155
 
158
- /** Rename wizard: pick an account, type a new name. */
159
156
  async function renameAccount(ctx: any): Promise<void> {
160
157
  if (!ctx.hasUI) {
161
158
  ctx.ui.notify("Renaming an account requires interactive Pi mode.", "error");
@@ -179,7 +176,6 @@ export function registerAccountCommands(pi: ExtensionAPI): void {
179
176
 
180
177
  const provider = accountProvider(row.providerId);
181
178
  try {
182
- // Row ids are "<providerId>:<accountId>"; strip the prefix.
183
179
  await provider!.rename!(row.id.slice(row.providerId.length + 1), next.trim());
184
180
  ctx.ui.notify(`Renamed to “${next.trim()}”.`, "info");
185
181
  } catch (error) {
@@ -202,15 +198,15 @@ export function registerAccountCommands(pi: ExtensionAPI): void {
202
198
  .map((id) => ({ value: `${parts[0]} ${id}`, label: id }));
203
199
  },
204
200
  handler: async (args, ctx) => {
205
- const [action, providerId] = args.trim().split(/\s+/).filter(Boolean);
201
+ const [action, providerId, ...rest] = args.trim().split(/\s+/).filter(Boolean);
206
202
 
207
- if (action === "add") return addAccount(ctx, providerId);
203
+ if (action === "add") return addAccount(ctx, providerId, rest.join(" ") || undefined);
208
204
  if (action === "rename") return renameAccount(ctx);
209
205
 
210
206
  if (action === "reauth") {
211
207
  const provider = providerId ? accountProvider(providerId) : accountProviders()[0];
212
208
  if (!provider) { ctx.ui.notify("No subscription providers are registered.", "error"); return; }
213
- const target = await pickAccount(ctx, provider);
209
+ const target = rest.join(" ") || await pickAccount(ctx, provider);
214
210
  if (!target) return;
215
211
  try {
216
212
  const done = await provider.reauth(bridge(pi, ctx), target);
@@ -226,11 +222,9 @@ export function registerAccountCommands(pi: ExtensionAPI): void {
226
222
  return;
227
223
  }
228
224
 
229
- // Bare /accounts: the picker interactively, plain text headless.
230
225
  if (!ctx.hasUI) return listAll(ctx);
231
226
 
232
- // The picker closes to run a wizard, then reopens so the result is
233
- // visible immediately. Bounded so a misbehaving action cannot spin.
227
+ // Reopen after each wizard so changes appear immediately.
234
228
  for (let step = 0; step < 24; step++) {
235
229
  const requested = await openAccountsPicker(ctx, { rows, toggle });
236
230
  if (!requested) return;
@@ -4,20 +4,55 @@ import { refreshUsage, startPolling, stopPolling, subscribe, usageState } from "
4
4
  import { renderUsageLines, usageSummaryText } from "../../ui/usage-bars.ts";
5
5
  import { formatTokens, sanitize } from "../../ui/format.ts";
6
6
 
7
- /**
8
- * Single-line session footer (no working-directory line) with the subscription
9
- * bars rendered directly underneath it.
10
- *
11
- * The footer no longer owns the poll loop; it subscribes to usage-service and
12
- * re-renders on change. That is what lets non-UI consumers get fresh data.
13
- */
7
+ /** Compact session footer with shared subscription usage bars. */
14
8
 
15
- const SUBSCRIPTION_PROVIDERS = new Set(["anthropic", "openai-codex", "kimi-coding"]);
9
+ const SUBSCRIPTION_PROVIDERS = new Set(["anthropic", "openai-codex", "kimi-coding", "xai"]);
10
+
11
+ interface SessionTotals {
12
+ input: number;
13
+ output: number;
14
+ cacheRead: number;
15
+ cacheWrite: number;
16
+ cost: number;
17
+ latestHitRate?: number;
18
+ }
19
+
20
+ function usageFor(entry: any): any {
21
+ if (entry.type === "message") {
22
+ return entry.message.role === "assistant" || entry.message.role === "toolResult" ? entry.message.usage : undefined;
23
+ }
24
+ return entry.type === "branch_summary" || entry.type === "compaction" ? entry.usage : undefined;
25
+ }
26
+
27
+ function sumSessionUsage(entries: any[]): SessionTotals {
28
+ const totals: SessionTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
29
+ for (const entry of entries) {
30
+ const usage = usageFor(entry);
31
+ if (!usage) continue;
32
+ totals.input += usage.input ?? 0;
33
+ totals.output += usage.output ?? 0;
34
+ totals.cacheRead += usage.cacheRead ?? 0;
35
+ totals.cacheWrite += usage.cacheWrite ?? 0;
36
+ totals.cost += usage.cost?.total ?? 0;
37
+ if (entry.type === "message" && entry.message.role === "assistant") {
38
+ const prompt = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
39
+ totals.latestHitRate = prompt > 0 ? ((usage.cacheRead ?? 0) / prompt) * 100 : undefined;
40
+ }
41
+ }
42
+ return totals;
43
+ }
44
+
45
+ function usageSignature(entries: any[]): string {
46
+ const last = entries.at(-1);
47
+ const usage = last ? usageFor(last) : undefined;
48
+ return [entries.length, last?.id, usage?.input, usage?.output, usage?.cacheRead, usage?.cacheWrite, usage?.cost?.total].join(":");
49
+ }
16
50
 
17
51
  export function registerFooter(pi: ExtensionAPI): void {
18
52
  let showUsage = true;
19
53
  let requestRender: (() => void) | undefined;
20
54
  let unsubscribe: (() => void) | undefined;
55
+ let cachedUsage: { signature: string; totals: SessionTotals } | undefined;
21
56
 
22
57
  const apply = (ctx: any) => {
23
58
  if (!ctx.hasUI) return;
@@ -34,28 +69,12 @@ export function registerFooter(pi: ExtensionAPI): void {
34
69
  const width = Math.max(1, Math.floor(Number(rawWidth) || 0));
35
70
  const lines: string[] = [];
36
71
 
37
- let input = 0;
38
- let output = 0;
39
- let cacheRead = 0;
40
- let cacheWrite = 0;
41
- let cost = 0;
42
- let latestHitRate: number | undefined;
43
-
44
- for (const entry of ctx.sessionManager.getEntries()) {
45
- const usage = entry.type === "message"
46
- ? (entry.message.role === "assistant" || entry.message.role === "toolResult" ? entry.message.usage : undefined)
47
- : (entry.type === "branch_summary" || entry.type === "compaction" ? entry.usage : undefined);
48
- if (!usage) continue;
49
- input += usage.input ?? 0;
50
- output += usage.output ?? 0;
51
- cacheRead += usage.cacheRead ?? 0;
52
- cacheWrite += usage.cacheWrite ?? 0;
53
- cost += usage.cost?.total ?? 0;
54
- if (entry.type === "message" && entry.message.role === "assistant") {
55
- const prompt = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
56
- latestHitRate = prompt > 0 ? ((usage.cacheRead ?? 0) / prompt) * 100 : undefined;
57
- }
72
+ const entries = ctx.sessionManager.getEntries();
73
+ const signature = usageSignature(entries);
74
+ if (cachedUsage?.signature !== signature) {
75
+ cachedUsage = { signature, totals: sumSessionUsage(entries) };
58
76
  }
77
+ const { input, output, cacheRead, cacheWrite, cost, latestHitRate } = cachedUsage.totals;
59
78
 
60
79
  const parts: string[] = [];
61
80
  if (input) parts.push(`↑${formatTokens(input)}`);
@@ -1,40 +1,24 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { registerAccountProvider } from "../../core/accounts/registry.ts";
3
3
  import { anthropicAccounts } from "./providers/anthropic.ts";
4
- import { codexAccounts } from "./providers/codex.ts";
4
+ import { codexAccounts, registerCodexProvider } from "./providers/codex.ts";
5
+ import { kimiAccounts, KIMI_SPEC, xaiAccounts, XAI_SPEC } from "./providers/hosted.ts";
6
+ import { registerPooledOAuthProvider } from "./providers/oauth-pool.ts";
5
7
  import { registerAnthropicProvider } from "./provider.ts";
6
8
  import { registerAccountCommands } from "./accounts.ts";
7
9
  import { registerRoutingCommands } from "./routing.ts";
8
10
  import { registerFooter } from "./footer.ts";
9
11
 
10
- /**
11
- * Subscriptions domain: Claude OAuth accounts, routing mode, and the quota HUD.
12
- *
13
- * This replaces the old `claude-multi-account.ts` + `compact-footer.ts` pair.
14
- * Account and routing commands are provider-agnostic: `/account <provider>` and
15
- * `/routing` dispatch through core/accounts/registry.ts, so adding a second
16
- * provider means writing one adapter, not new commands.
17
- * Authentication and model catalogue are separate concerns here; they were only
18
- * ever colocated because they shared an OAuth token.
19
- *
20
- * Relationship to @cortexkit/pi-anthropic-auth:
21
- * That package stays installed and keeps ownership of the Anthropic provider,
22
- * its stream implementation, and its own commands (/claude-account,
23
- * /claude-routing, /claude-quota, ...). Absorbing it fully was evaluated and
24
- * rejected, because its `dist/commands.js` exports only `registerCommands(pi)` as a
25
- * unit, so taking over individual commands would mean copying ~130 lines of
26
- * provider/model specs that drift on every vendor upgrade. This domain adds
27
- * the multi-account surface the vendor does not provide, and everything it
28
- * touches goes through src/vendor/anthropic.ts.
29
- */
12
+ /** Registers subscription providers, pooled accounts, routing, and usage UI. */
30
13
  export default function subscriptions(pi: ExtensionAPI) {
31
- // Adapters register first so the generic commands can see them.
32
- registerAccountProvider(anthropicAccounts);
33
- registerAccountProvider(codexAccounts);
14
+ for (const provider of [anthropicAccounts, codexAccounts, kimiAccounts, xaiAccounts]) {
15
+ registerAccountProvider(provider);
16
+ }
34
17
 
35
- // Own the Anthropic provider unless explicitly told to defer to cortexkit.
36
- // PI_PLUS_VENDOR_ANTHROPIC=1 restores the vendored package.
37
- if (process.env.PI_PLUS_VENDOR_ANTHROPIC !== "1") registerAnthropicProvider(pi);
18
+ registerAnthropicProvider(pi);
19
+ registerCodexProvider(pi);
20
+ registerPooledOAuthProvider(pi, KIMI_SPEC);
21
+ registerPooledOAuthProvider(pi, XAI_SPEC);
38
22
 
39
23
  registerAccountCommands(pi);
40
24
  registerRoutingCommands(pi);