@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/README.md +21 -26
- package/dist/chunks/{chunk-WEXCFYZX.ts → chunk-ENCC7IBP.ts} +6 -9
- package/dist/chunks/chunk-ENCC7IBP.ts.map +7 -0
- package/dist/chunks/{openai-companion-auth-Q456G46T.ts → openai-companion-auth-6VLPFVHF.ts} +4 -6
- package/dist/chunks/openai-companion-auth-6VLPFVHF.ts.map +7 -0
- package/dist/chunks/{openai-companion-usage-Y3WTMUB2.ts → openai-companion-usage-4NOBHGP7.ts} +2 -2
- package/dist/index.ts +117 -172
- package/dist/index.ts.map +3 -3
- package/docs/providers.md +13 -11
- package/package.json +1 -1
- package/src/format.ts +10 -4
- package/src/openai-companion-auth.ts +3 -5
- package/src/providers/openai-chatgpt.ts +2 -6
- package/src/query.ts +2 -2
- package/src/settings.ts +0 -5
- package/src/types.ts +0 -1
- package/src/usage-settings-ui.ts +116 -156
- package/src/usage.ts +1 -25
- package/dist/chunks/chunk-WEXCFYZX.ts.map +0 -7
- package/dist/chunks/openai-companion-auth-Q456G46T.ts.map +0 -7
- /package/dist/chunks/{openai-companion-usage-Y3WTMUB2.ts.map → openai-companion-usage-4NOBHGP7.ts.map} +0 -0
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
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
####
|
|
51
|
+
#### Automatic companion quotas
|
|
52
52
|
|
|
53
|
-
Follow the [
|
|
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
|
-
|
|
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
|
|
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
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
|
-
|
|
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
|
-
|
|
544
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|
package/src/usage-settings-ui.ts
CHANGED
|
@@ -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"
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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,
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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 &&
|