@narumitw/pi-usage 0.62.0 → 0.63.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/docs/providers.md CHANGED
@@ -25,10 +25,10 @@ The README contains the capability overview and shared security requirements.
25
25
  ### OpenAI native ChatGPT OAuth
26
26
 
27
27
  - Provider ID: `openai`
28
- - Default semantics/source: verified native authentication through fresh Pi OAuth resolution and an exact complete credential match; no usage HTTP endpoint
29
- - Default display: **Connected (native OAuth)** and [ChatGPT usage settings](https://chatgpt.com/settings/usage)
30
- - Default statusline: `chatgpt usage: web only`
31
- - Opt-in source: experimental same-account Codex companion OAuth; distinct shared plan and app limits
28
+ - Source: verified native OAuth plus automatic same-account Codex companion OAuth; distinct shared plan and app windows
29
+ - Display: plan percentages and plan/app resets; app used/remaining percentages are hidden
30
+ - Missing-companion fallback: **Connected (native OAuth)**, same-account login guidance, and [ChatGPT usage settings](https://chatgpt.com/settings/usage)
31
+ - Fallback statusline: `chatgpt usage: web only`
32
32
 
33
33
  Use `/login openai` in a Pi runtime that supports native Sign in with ChatGPT, then select an official `https://api.openai.com` model.
34
34
  Authentication requires Pi's freshly resolved provider source to be `OAuth`, matching effective model and provider authorization, and a complete matching credential with `clientId` and the `chatgpt.tokens.use.direct` scope.
@@ -37,8 +37,8 @@ Stored OAuth alone and Pi's cached OAuth flag do not establish the active runtim
37
37
  Runtime/API-key overrides, model-level Authorization replacements or removals, and custom/proxy origins cannot inherit native subscription status.
38
38
  OpenAI API-key usage reporting remains unsupported.
39
39
 
40
- The default report verifies authentication, not the plan tier, allowance, remaining quota, or reset time.
41
- Default automatic refresh only revalidates auth; it does not poll a usage endpoint.
40
+ Without companion credentials, the report verifies authentication, not the plan tier, allowance, remaining quota, or reset time.
41
+ With valid companion credentials, queries and automatic refreshes read both companion endpoints; otherwise no usage request is made.
42
42
  The settings link is guidance, not an automatically opened browser or a promise that the browser is signed into Pi's account.
43
43
  Native tokens are never sent to `/backend-api/wham`, and opaque native metadata is not decoded as a legacy Codex account ID.
44
44
  Legacy Fast payload rewriting, pricing correction, and earned reset redemption do not apply to native `openai`.
@@ -46,11 +46,12 @@ Existing `openai-codex` behavior below is unchanged.
46
46
 
47
47
  The boundary is verified against Pi's [`openai-chatgpt.ts`](https://github.com/earendil-works/pi/blob/d86654abb8862e201933517d6f1fce9f88dd117f/packages/ai/src/auth/oauth/openai-chatgpt.ts) at `d86654abb8862e201933517d6f1fce9f88dd117f` and OpenAI's [models and inference](https://developers.openai.com/siwc/token-sharing-open-source/models-and-inference), [token reference](https://developers.openai.com/siwc/token-sharing-open-source/token-reference), and [UI/UX guidelines](https://developers.openai.com/siwc/ui-ux-guidelines).
48
48
  These contracts establish the public inference and authentication boundary, not a numerical usage API.
49
- Verification uses synthetic credentials with Pi's real auth resolver; live browser login and account-specific quota retrieval have not been verified.
49
+ Deterministic verification uses synthetic credentials with Pi's real auth resolver. A user reported one plan weekly remaining percentage and reset time matching the web overview; browser login, app numerical agreement, and broader live coverage remain unverified.
50
50
 
51
- #### Experimental companion quotas
51
+ #### Automatic companion quotas
52
52
 
53
- Follow the [opt-in setup](../README.md#experimental-chatgpt-companion-usage): `openaiCompanionUsage` defaults to `false` and requires a same-account/workspace `/login openai-codex`.
53
+ Follow the [companion setup](../README.md#chatgpt-companion-usage): a same-account/workspace `/login openai-codex` is required.
54
+ The former opt-in setting and confirmation are removed; stored `openaiCompanionUsage` values, including `false`, no longer disable requests and are preserved without rewriting on reads.
54
55
  Fresh companion OAuth must match a complete grant with its own account ID; custom origins, API-key overrides, conflicting grants, and effective Authorization overrides fail closed.
55
56
  Only the companion bearer and matching `ChatGPT-Account-Id` reach these fixed, redirect-rejecting GET routes:
56
57
 
@@ -63,11 +64,12 @@ Only plan primary/secondary windows are used; Codex model-specific limits are no
63
64
  Each window requires a finite 0–100 used percentage and positive whole-second duration; optional remaining percentages must agree within one percentage point, and reset timestamps must be valid nonnegative epoch seconds.
64
65
  `allowed_usage_percent` is **App allowance**, a cap on shared plan usage, not remaining quota.
65
66
  Plan/app windows and reset boundaries stay separate in reports and the statusline; Codex display preferences do not apply.
67
+ App used/remaining values are validated internally but not displayed; app reset/window information and the separately labeled allowance cap remain visible.
66
68
 
67
- Disabled or missing-companion paths remain web-only without requests. Other enabled-mode failures invalidate numerical data and remain observable.
69
+ Missing-companion paths remain web-only without requests. Other companion failures invalidate numerical data and remain observable.
68
70
  No native token, refresh token, app name, or account ID is rendered or persisted by this feature.
69
71
  An exact registration match is not independent identity proof; users must choose the same account/workspace for both logins.
70
- The endpoints are undocumented; live availability and numerical agreement remain unverified.
72
+ The endpoints remain an experimental, undocumented source despite automatic querying. One user-reported plan weekly comparison matched the web; app numerical agreement and broader live availability remain unverified.
71
73
  Contract fixtures derive from [Specode's pinned implementation](https://github.com/specode/pi-subscription-usage/blob/1015f9f419dfef9ac27c9845ec2318394928fa38/src/providers/openai.ts) and [OpenAI's shared-plan/app-limit explanation](https://help.openai.com/en/articles/20001542-using-your-chatgpt-plan-in-other-apps-and-sites), not a public native quota API.
72
74
 
73
75
  ### OpenAI Codex
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-usage",
3
- "version": "0.62.0",
3
+ "version": "0.63.0",
4
4
  "description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/format.ts CHANGED
@@ -68,7 +68,9 @@ export function formatUsageStatusline(
68
68
  .filter((bucket) => bucket.groupId === group)
69
69
  .map((bucket) => {
70
70
  const reset = formatResetCountdown(bucket.resetsAt, now);
71
- return `${bucket.remaining === undefined ? "unavailable" : `${bucket.remaining.toFixed(0)}%`} ${reset ? `↻ ${reset}` : formatWindowLabel(bucket.windowMinutes, "weekly", true)}`;
71
+ const window = reset ? `↻ ${reset}` : formatWindowLabel(bucket.windowMinutes, "weekly", true);
72
+ if (group === "chatgpt-app") return window;
73
+ return `${bucket.remaining === undefined ? "unavailable" : `${bucket.remaining.toFixed(0)}%`} ${window}`;
72
74
  });
73
75
  return `${group === "chatgpt-plan" ? "plan" : "app"} ${windows.join(" ")}`;
74
76
  })
@@ -540,9 +542,13 @@ function formatOpenAICompanionReport(lines: string[], report: UsageReport): void
540
542
  for (const group of ["chatgpt-plan", "chatgpt-app"]) {
541
543
  lines.push(group === "chatgpt-plan" ? "Plan limits:" : "App limits:");
542
544
  for (const bucket of report.buckets.filter((bucket) => bucket.groupId === group)) {
543
- lines.push(
544
- `${formatWindowLabel(bucket.windowMinutes, "weekly", false).padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`,
545
- );
545
+ const value =
546
+ group === "chatgpt-app"
547
+ ? bucket.resetsAt === undefined
548
+ ? "Reset time unavailable"
549
+ : `resets ${formatReset(bucket.resetsAt)}`
550
+ : formatPercentBucket(bucket);
551
+ lines.push(`${formatWindowLabel(bucket.windowMinutes, "weekly", false).padEnd(VALUE_COLUMN)}${value}`);
546
552
  }
547
553
  }
548
554
  for (const metric of report.metrics)
@@ -6,7 +6,7 @@ import { adapterForProvider, resolveUsageAuth } from "./query.js";
6
6
  import type { ResolvedUsageAuth } from "./types.js";
7
7
 
8
8
  // A native app registration is routing metadata, not cryptographic same-account proof.
9
- // Keep this opt-in boundary separate from legacy Codex querying and reset mutation policy.
9
+ // Keep this read-only boundary separate from legacy Codex querying and reset mutation policy.
10
10
  export async function resolveOpenAICompanionAuth(
11
11
  ctx: ExtensionContext,
12
12
  native: ResolvedUsageAuth,
@@ -16,9 +16,8 @@ export async function resolveOpenAICompanionAuth(
16
16
  ): Promise<ResolvedUsageAuth> {
17
17
  const unavailable = () => ({
18
18
  ...native,
19
- openaiCompanionEnabled: true,
20
19
  fingerprint: fingerprintResolvedAuth(
21
- { apiKey: JSON.stringify([native.fingerprint, native.openaiClientId, true, null]) },
20
+ { apiKey: JSON.stringify([native.fingerprint, native.openaiClientId, null]) },
22
21
  salt,
23
22
  ),
24
23
  });
@@ -72,10 +71,9 @@ export async function resolveOpenAICompanionAuth(
72
71
  const headers = { Authorization: `Bearer ${access}`, "ChatGPT-Account-Id": accountId };
73
72
  return {
74
73
  ...native,
75
- openaiCompanionEnabled: true,
76
74
  openaiCompanion: { headers },
77
75
  fingerprint: fingerprintResolvedAuth(
78
- { apiKey: JSON.stringify([native.fingerprint, native.openaiClientId, true, companion.fingerprint, headers]) },
76
+ { apiKey: JSON.stringify([native.fingerprint, native.openaiClientId, companion.fingerprint, headers]) },
79
77
  salt,
80
78
  ),
81
79
  secrets: [
@@ -44,13 +44,9 @@ export const OPENAI_CHATGPT_ADAPTER: UsageProviderAdapter = {
44
44
  buckets: [],
45
45
  metrics: [{ id: "plan-auth", label: "ChatGPT plan authentication", value: "Connected (native OAuth)" }],
46
46
  notes: [
47
- "Numerical usage and reset times are unavailable in pi-usage for native OpenAI OAuth.",
47
+ "Numerical usage requires a companion /login openai-codex with the same ChatGPT account/workspace; native inference stays on openai.",
48
48
  `Manage usage: ${CHATGPT_USAGE_SETTINGS_URL}`,
49
- ...(auth.openaiCompanionEnabled
50
- ? [
51
- "Experimental companion usage requires /login openai-codex with the same ChatGPT account/workspace; native inference stays on openai.",
52
- ]
53
- : []),
49
+ "Companion usage uses undocumented ChatGPT endpoints; registration matching is not independent identity proof.",
54
50
  "Fast mode and earned reset redemption remain legacy openai-codex features.",
55
51
  ],
56
52
  };
package/src/query.ts CHANGED
@@ -340,7 +340,8 @@ export async function resolveUsageAuth(
340
340
  salt: Uint8Array = AUTH_FINGERPRINT_SALT,
341
341
  credentialReader: StoredCredentialReader = readStoredCredential,
342
342
  candidateReader?: OAuthCredentialCandidateReader,
343
- openaiCompanionUsage = false,
343
+ /** @deprecated Companion resolution is automatic; this argument is ignored. */
344
+ _openaiCompanionUsage?: boolean,
344
345
  ): Promise<ResolvedUsageAuth | undefined> {
345
346
  if (ctx.model?.provider === adapter.id && !hasOfficialOrigin(ctx.model, adapter.id)) {
346
347
  throw new Error(
@@ -441,7 +442,6 @@ export async function resolveUsageAuth(
441
442
  const native = finalize(
442
443
  resolveOpenAIChatGPTAuth(auth, providerResult.auth, providerResult.source, model, offered.candidates),
443
444
  );
444
- if (!openaiCompanionUsage) return native;
445
445
  const { resolveOpenAICompanionAuth } = await import("./openai-companion-auth.js");
446
446
  return resolveOpenAICompanionAuth(ctx, native, salt, credentialReader, candidateReader);
447
447
  }
package/src/settings.ts CHANGED
@@ -12,8 +12,6 @@ export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
12
12
  export type CodexStatusPercentage = "remaining" | "used";
13
13
 
14
14
  export interface UsageSettings {
15
- /** Omitted values preserve default-off compatibility for injected settings runtimes. */
16
- openaiCompanionUsage?: boolean;
17
15
  codexFastMode: boolean;
18
16
  codexStatusResetCountdown: boolean;
19
17
  codexStatusPercentage: CodexStatusPercentage;
@@ -21,7 +19,6 @@ export interface UsageSettings {
21
19
  }
22
20
 
23
21
  export const DEFAULT_USAGE_SETTINGS: Readonly<UsageSettings> = Object.freeze({
24
- openaiCompanionUsage: false,
25
22
  codexFastMode: false,
26
23
  codexStatusResetCountdown: true,
27
24
  codexStatusPercentage: "remaining",
@@ -67,7 +64,6 @@ export function usageSettingsPath(): string {
67
64
 
68
65
  export function normalizeUsageSettings(value: unknown): UsageSettings | undefined {
69
66
  if (!isRecord(value)) return undefined;
70
- if (Object.hasOwn(value, "openaiCompanionUsage") && typeof value.openaiCompanionUsage !== "boolean") return undefined;
71
67
  if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
72
68
  return undefined;
73
69
  }
@@ -91,7 +87,6 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
91
87
  effectiveTargets.fireworks = value.fireworksAccountId;
92
88
  }
93
89
  return {
94
- openaiCompanionUsage: value.openaiCompanionUsage === true,
95
90
  codexFastMode:
96
91
  typeof value.codexFastMode === "boolean" ? value.codexFastMode : DEFAULT_USAGE_SETTINGS.codexFastMode,
97
92
  codexStatusResetCountdown:
package/src/types.ts CHANGED
@@ -50,7 +50,6 @@ export interface UsageReport {
50
50
  export interface ResolvedUsageAuth {
51
51
  /** Native registration metadata stays local; only companion headers reach ChatGPT. */
52
52
  openaiClientId?: string;
53
- openaiCompanionEnabled?: boolean;
54
53
  openaiCompanion?: { headers: Record<string, string> };
55
54
  apiKey?: string;
56
55
  headers: Record<string, string>;
@@ -9,7 +9,7 @@ const ON = "On";
9
9
  const REMAINING = "Remaining";
10
10
  const USED = "Used";
11
11
 
12
- type UsageSettingId = "codexFastMode" | "codexStatusResetCountdown" | "codexStatusPercentage" | "openaiCompanionUsage";
12
+ type UsageSettingId = "codexFastMode" | "codexStatusResetCountdown" | "codexStatusPercentage";
13
13
 
14
14
  function settingValueLabel(id: UsageSettingId, value: UsageSettings[UsageSettingId]): string {
15
15
  if (id === "codexStatusPercentage") return value === "used" ? USED : REMAINING;
@@ -30,165 +30,125 @@ export async function showUsageSettings(
30
30
  const { HorizontalRule, renderBoundedFrame } = await import("@narumitw/pi-tui-kit");
31
31
  if (parentSignal.aborted || !isCurrent()) return false;
32
32
  let changed = false;
33
- let selectedId: UsageSettingId | undefined;
34
- for (;;) {
35
- const result = await ctx.ui.custom<boolean | "consent">((tui, theme, _keybindings, done) => {
36
- const localController = new AbortController();
37
- const signal = AbortSignal.any([parentSignal, localController.signal]);
38
- let closing = false;
39
- let saveQueue = Promise.resolve();
40
- const state = settingsRuntime.get();
41
- const items: SettingItem[] = [
42
- {
43
- id: "codexFastMode",
44
- label: "Codex Fast mode",
45
- description: "Use faster Codex routing at increased plan allowance consumption.",
46
- currentValue: state.settings.codexFastMode ? ON : OFF,
47
- values: [OFF, ON],
48
- },
49
- {
50
- id: "codexStatusResetCountdown",
51
- label: "Codex reset countdown",
52
- description: "Show time remaining until each Codex usage limit resets.",
53
- currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
54
- values: [OFF, ON],
55
- },
56
- {
57
- id: "codexStatusPercentage",
58
- label: "Codex percentage",
59
- description: "Show remaining or used Codex quota in the statusline.",
60
- currentValue: settingValueLabel("codexStatusPercentage", state.settings.codexStatusPercentage),
61
- values: [REMAINING, USED],
62
- },
63
- {
64
- id: "openaiCompanionUsage",
65
- label: "Experimental ChatGPT companion usage",
66
- description:
67
- "Read undocumented plan/app usage using a same-account Codex login; matching registration does not prove identity.",
68
- currentValue: state.settings.openaiCompanionUsage ? ON : OFF,
69
- values: [OFF, ON],
70
- },
71
- ];
72
- const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
33
+ return ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
34
+ const localController = new AbortController();
35
+ const signal = AbortSignal.any([parentSignal, localController.signal]);
36
+ let closing = false;
37
+ let saveQueue = Promise.resolve();
38
+ const state = settingsRuntime.get();
39
+ const items: SettingItem[] = [
40
+ {
41
+ id: "codexFastMode",
42
+ label: "Codex Fast mode",
43
+ description: "Use faster Codex routing at increased plan allowance consumption.",
44
+ currentValue: state.settings.codexFastMode ? ON : OFF,
45
+ values: [OFF, ON],
46
+ },
47
+ {
48
+ id: "codexStatusResetCountdown",
49
+ label: "Codex reset countdown",
50
+ description: "Show time remaining until each Codex usage limit resets.",
51
+ currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
52
+ values: [OFF, ON],
53
+ },
54
+ {
55
+ id: "codexStatusPercentage",
56
+ label: "Codex percentage",
57
+ description: "Show remaining or used Codex quota in the statusline.",
58
+ currentValue: settingValueLabel("codexStatusPercentage", state.settings.codexStatusPercentage),
59
+ values: [REMAINING, USED],
60
+ },
61
+ ];
62
+ const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
73
63
 
74
- let settingsList: SettingsList;
75
- const close = (result: boolean | "consent") => {
76
- if (closing) return;
77
- closing = true;
78
- localController.abort();
79
- done(result);
80
- };
81
- const cancel = () => close(changed);
82
- const queueUpdate = <Id extends UsageSettingId>(id: Id, requested: UsageSettings[Id], display: string) => {
83
- saveQueue = saveQueue.then(async () => {
84
- if (signal.aborted || !isCurrent()) return;
85
- const previous = settingsRuntime.get().settings[id];
86
- if (settingsRuntime.get().kind === "invalid") {
87
- settingsList.updateValue(id, settingValueLabel(id, previous));
88
- if (!signal.aborted && isCurrent()) {
89
- ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
90
- tui.requestRender();
91
- }
92
- return;
93
- }
94
- try {
95
- if (id === "openaiCompanionUsage" && requested === true && previous !== true) {
96
- // Pi's confirm dialog replaces a non-overlay custom screen and restores
97
- // the editor, not that screen. Complete/dispose Settings first; after
98
- // consent, mount a fresh instance with the initiating row selected.
99
- selectedId = id;
100
- close("consent");
101
- return;
102
- }
103
- const patch: Partial<UsageSettings> = {};
104
- patch[id] = requested;
105
- await settingsRuntime.update(patch, signal);
106
- } catch (error) {
107
- if (signal.aborted || !isCurrent()) return;
108
- settingsList.updateValue(id, settingValueLabel(id, previous));
109
- ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
64
+ let settingsList: SettingsList;
65
+ const close = (result: boolean) => {
66
+ if (closing) return;
67
+ closing = true;
68
+ localController.abort();
69
+ done(result);
70
+ };
71
+ const cancel = () => close(changed);
72
+ const queueUpdate = <Id extends UsageSettingId>(id: Id, requested: UsageSettings[Id], display: string) => {
73
+ saveQueue = saveQueue.then(async () => {
74
+ if (signal.aborted || !isCurrent()) return;
75
+ const previous = settingsRuntime.get().settings[id];
76
+ if (settingsRuntime.get().kind === "invalid") {
77
+ settingsList.updateValue(id, settingValueLabel(id, previous));
78
+ if (!signal.aborted && isCurrent()) {
79
+ ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
110
80
  tui.requestRender();
111
- return;
112
- }
113
- // A rename can win a cancellation race. Apply owned lifecycle cleanup after a
114
- // durable save, but let the owner guard its session before touching UI.
115
- if (previous !== requested) {
116
- changed = true;
117
- onApplied(id);
118
81
  }
82
+ return;
83
+ }
84
+ try {
85
+ const patch: Partial<UsageSettings> = {};
86
+ patch[id] = requested;
87
+ await settingsRuntime.update(patch, signal);
88
+ } catch (error) {
119
89
  if (signal.aborted || !isCurrent()) return;
120
- settingsList.updateValue(id, display);
90
+ settingsList.updateValue(id, settingValueLabel(id, previous));
91
+ ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
121
92
  tui.requestRender();
122
- });
123
- };
124
- settingsList = new SettingsList(
125
- items,
126
- items.length + 2,
127
- getSettingsListTheme(),
128
- (id, value) => {
129
- if (closing || signal.aborted || !isCurrent()) return;
130
- selectedId = id as UsageSettingId;
131
- if (id === "codexStatusPercentage") {
132
- queueUpdate(id, value === USED ? "used" : "remaining", value);
133
- } else if (id === "codexFastMode" || id === "codexStatusResetCountdown" || id === "openaiCompanionUsage") {
134
- queueUpdate(id, value === ON, value);
135
- }
136
- },
137
- cancel,
138
- );
93
+ return;
94
+ }
95
+ // A rename can win a cancellation race. Apply owned lifecycle cleanup after a
96
+ // durable save, but let the owner guard its session before touching UI.
97
+ if (previous !== requested) {
98
+ changed = true;
99
+ onApplied(id);
100
+ }
101
+ if (signal.aborted || !isCurrent()) return;
102
+ settingsList.updateValue(id, display);
103
+ tui.requestRender();
104
+ });
105
+ };
106
+ settingsList = new SettingsList(
107
+ items,
108
+ items.length + 2,
109
+ getSettingsListTheme(),
110
+ (id, value) => {
111
+ if (closing || signal.aborted || !isCurrent()) return;
112
+ if (id === "codexStatusPercentage") {
113
+ queueUpdate(id, value === USED ? "used" : "remaining", value);
114
+ } else if (id === "codexFastMode" || id === "codexStatusResetCountdown") {
115
+ queueUpdate(id, value === ON, value);
116
+ }
117
+ },
118
+ cancel,
119
+ );
139
120
 
140
- if (selectedId) settingsList.selectItem(selectedId);
141
- parentSignal.addEventListener("abort", cancel, { once: true });
142
- return {
143
- render(width: number) {
144
- // Compatibility: Kit puts the title directly below the top rule and keeps compact
145
- // rules when at least five rows fit; both replace the legacy wrapper layout.
146
- const title = new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 0).render(width);
147
- const content = settingsList.render(width);
148
- const focusedRow = content.findIndex((line) => /^[→›]\s/u.test(stripVTControlCharacters(line)));
149
- const terminalRows = Number.isFinite(tui.terminal?.rows) ? Math.floor(tui.terminal.rows) : 24;
150
- const [ruleLine = ""] = rule.render(width);
151
- return renderBoundedFrame({
152
- width,
153
- maxRows: Math.max(1, terminalRows - 3),
154
- rule: ruleLine,
155
- title,
156
- content,
157
- priorityRows: focusedRow < 0 ? [] : [focusedRow],
158
- focusedRow,
159
- });
160
- },
161
- invalidate: () => settingsList.invalidate(),
162
- handleInput(data: string) {
163
- if (closing) return;
164
- if (matchesKey(data, Key.ctrl("c"))) cancel();
165
- else settingsList.handleInput(data);
166
- tui.requestRender();
167
- },
168
- dispose() {
169
- localController.abort();
170
- parentSignal.removeEventListener("abort", cancel);
171
- },
172
- };
173
- });
174
- if (result !== "consent" || parentSignal.aborted || !isCurrent()) return changed;
175
- try {
176
- const accepted = await ctx.ui.confirm(
177
- "Enable experimental ChatGPT companion usage?",
178
- "Requires /login openai-codex with the same ChatGPT account/workspace as native OpenAI. Only the companion token and its matching account ID are sent to undocumented ChatGPT usage endpoints. Registration matching is not independent proof of the same user/workspace. No reset or allowance mutations are performed.",
179
- { signal: parentSignal },
180
- );
181
- if (parentSignal.aborted || !isCurrent()) return changed;
182
- if (accepted && !settingsRuntime.get().settings.openaiCompanionUsage) {
183
- await settingsRuntime.update({ openaiCompanionUsage: true }, parentSignal);
184
- changed = true;
185
- // A durable rename may win cancellation; the owner guards lifecycle cleanup.
186
- onApplied("openaiCompanionUsage");
187
- }
188
- } catch (error) {
189
- if (parentSignal.aborted || !isCurrent()) return changed;
190
- ctx.ui.notify(`Could not enable ChatGPT companion usage: ${errorMessage(error)}`, "error");
191
- }
192
- if (parentSignal.aborted || !isCurrent()) return changed;
193
- }
121
+ parentSignal.addEventListener("abort", cancel, { once: true });
122
+ return {
123
+ render(width: number) {
124
+ // Compatibility: Kit puts the title directly below the top rule and keeps compact
125
+ // rules when at least five rows fit; both replace the legacy wrapper layout.
126
+ const title = new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 0).render(width);
127
+ const content = settingsList.render(width);
128
+ const focusedRow = content.findIndex((line) => /^[→›]\s/u.test(stripVTControlCharacters(line)));
129
+ const terminalRows = Number.isFinite(tui.terminal?.rows) ? Math.floor(tui.terminal.rows) : 24;
130
+ const [ruleLine = ""] = rule.render(width);
131
+ return renderBoundedFrame({
132
+ width,
133
+ maxRows: Math.max(1, terminalRows - 3),
134
+ rule: ruleLine,
135
+ title,
136
+ content,
137
+ priorityRows: focusedRow < 0 ? [] : [focusedRow],
138
+ focusedRow,
139
+ });
140
+ },
141
+ invalidate: () => settingsList.invalidate(),
142
+ handleInput(data: string) {
143
+ if (closing) return;
144
+ if (matchesKey(data, Key.ctrl("c"))) cancel();
145
+ else settingsList.handleInput(data);
146
+ tui.requestRender();
147
+ },
148
+ dispose() {
149
+ localController.abort();
150
+ parentSignal.removeEventListener("abort", cancel);
151
+ },
152
+ };
153
+ });
194
154
  }
package/src/usage.ts CHANGED
@@ -89,15 +89,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
89
89
  const credentialCandidates = createOAuthCredentialCandidateReader(pi, credentialReader);
90
90
  const createRedemptionId = dependencies.createRedemptionId ?? randomUUID;
91
91
  const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
92
- const resolveUsageAuth: typeof resolveRuntimeUsageAuth = (ctx, adapter, salt, reader, candidates) =>
93
- resolveRuntimeUsageAuth(
94
- ctx,
95
- adapter,
96
- salt,
97
- reader,
98
- candidates,
99
- settingsRuntime.get().settings.openaiCompanionUsage,
100
- );
92
+ const resolveUsageAuth = resolveRuntimeUsageAuth;
101
93
  const cache = new UsageCache(CACHE_TTL_MS);
102
94
  const failureBackoff = new Map<string, { until: number; message: string }>();
103
95
  const latestQueries = new Map<string, number>();
@@ -248,7 +240,6 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
248
240
  const expectedSessionId = ctx.sessionManager.getSessionId();
249
241
  const expectedModelIdentity = modelIdentity(ctx.model);
250
242
  const expectedTargetId = adapter.targets ? settingsRuntime.get().settings.selectedTargets[adapter.id] : undefined;
251
- const expectedCompanionUsage = settingsRuntime.get().settings.openaiCompanionUsage;
252
243
  const providerName = providerDisplayName(ctx, adapter.id);
253
244
  let auth: ResolvedUsageAuth | undefined;
254
245
  try {
@@ -301,7 +292,6 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
301
292
  expectedSessionGeneration !== sessionGeneration ||
302
293
  ctx.sessionManager.getSessionId() !== expectedSessionId ||
303
294
  modelIdentity(ctx.model) !== expectedModelIdentity ||
304
- (adapter.id === "openai" && settingsRuntime.get().settings.openaiCompanionUsage !== expectedCompanionUsage) ||
305
295
  (adapter.targets !== undefined &&
306
296
  settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId);
307
297
  if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
@@ -1090,20 +1080,6 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
1090
1080
  controller.signal,
1091
1081
  () => statusGeneration === menuGeneration && !controller.signal.aborted,
1092
1082
  (id) => {
1093
- if (
1094
- id === "openaiCompanionUsage" &&
1095
- statusGeneration === menuGeneration &&
1096
- !controller.signal.aborted
1097
- ) {
1098
- for (const active of nativeQueryControllers) active.abort();
1099
- invalidateProviderState("openai");
1100
- if (ctx.model?.provider === "openai") {
1101
- statusController?.abort();
1102
- statusController = undefined;
1103
- clearStatusTimers();
1104
- safeSetStatus(ctx, undefined);
1105
- }
1106
- }
1107
1083
  if (
1108
1084
  (id === "codexStatusResetCountdown" || id === "codexStatusPercentage") &&
1109
1085
  stableCurrent &&