@narumitw/pi-usage 0.61.2 → 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 +34 -7
- package/dist/chunks/chunk-ENCC7IBP.ts +3002 -0
- package/dist/chunks/chunk-ENCC7IBP.ts.map +7 -0
- package/dist/chunks/openai-companion-auth-6VLPFVHF.ts +83 -0
- package/dist/chunks/openai-companion-auth-6VLPFVHF.ts.map +7 -0
- package/dist/chunks/openai-companion-usage-4NOBHGP7.ts +120 -0
- package/dist/chunks/openai-companion-usage-4NOBHGP7.ts.map +7 -0
- package/dist/index.ts +382 -3202
- package/dist/index.ts.map +4 -4
- package/docs/providers.md +32 -8
- package/package.json +4 -4
- package/src/format.ts +35 -1
- package/src/openai-companion-auth.ts +101 -0
- package/src/providers/openai-chatgpt.ts +21 -10
- package/src/providers/openai-companion-usage.ts +146 -0
- package/src/query.ts +5 -1
- package/src/types.ts +3 -0
- package/src/usage-settings-ui.ts +116 -114
- package/src/usage.ts +62 -4
package/src/usage-settings-ui.ts
CHANGED
|
@@ -29,124 +29,126 @@ export async function showUsageSettings(
|
|
|
29
29
|
}
|
|
30
30
|
const { HorizontalRule, renderBoundedFrame } = await import("@narumitw/pi-tui-kit");
|
|
31
31
|
if (parentSignal.aborted || !isCurrent()) return false;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
|
|
32
|
+
let changed = false;
|
|
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) });
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
const patch: Partial<UsageSettings> = {};
|
|
85
|
-
patch[id] = requested;
|
|
86
|
-
await settingsRuntime.update(patch, signal);
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (signal.aborted || !isCurrent()) return;
|
|
89
|
-
settingsList.updateValue(id, settingValueLabel(id, previous));
|
|
90
|
-
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");
|
|
91
80
|
tui.requestRender();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (previous !== requested) {
|
|
95
|
-
changed = true;
|
|
96
|
-
onApplied(id);
|
|
97
81
|
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const patch: Partial<UsageSettings> = {};
|
|
86
|
+
patch[id] = requested;
|
|
87
|
+
await settingsRuntime.update(patch, signal);
|
|
88
|
+
} catch (error) {
|
|
98
89
|
if (signal.aborted || !isCurrent()) return;
|
|
99
|
-
settingsList.updateValue(id,
|
|
90
|
+
settingsList.updateValue(id, settingValueLabel(id, previous));
|
|
91
|
+
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
100
92
|
tui.requestRender();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
);
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
);
|
|
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
|
+
});
|
|
152
154
|
}
|
package/src/usage.ts
CHANGED
|
@@ -22,7 +22,12 @@ import { abortError, awaitWithDeadline, errorMessage, runWithConcurrency, UsageC
|
|
|
22
22
|
import { formatProviderStates, formatUsageStatusline } from "./format.js";
|
|
23
23
|
import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
|
|
24
24
|
import { UnsupportedOpenAIUsageAuthError } from "./providers/openai-chatgpt.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
adapterForProvider,
|
|
27
|
+
isStaleExtensionContextError,
|
|
28
|
+
queryProviderUsage,
|
|
29
|
+
resolveUsageAuth as resolveRuntimeUsageAuth,
|
|
30
|
+
} from "./query.js";
|
|
26
31
|
import { createUsageSettingsRuntime, type UsageSettingsRuntime, type UsageSettingsState } from "./settings.js";
|
|
27
32
|
import type {
|
|
28
33
|
PiModel,
|
|
@@ -84,10 +89,12 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
84
89
|
const credentialCandidates = createOAuthCredentialCandidateReader(pi, credentialReader);
|
|
85
90
|
const createRedemptionId = dependencies.createRedemptionId ?? randomUUID;
|
|
86
91
|
const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
|
|
92
|
+
const resolveUsageAuth = resolveRuntimeUsageAuth;
|
|
87
93
|
const cache = new UsageCache(CACHE_TTL_MS);
|
|
88
94
|
const failureBackoff = new Map<string, { until: number; message: string }>();
|
|
89
95
|
const latestQueries = new Map<string, number>();
|
|
90
96
|
const activeControllers = new Set<AbortController>();
|
|
97
|
+
const nativeQueryControllers = new Set<AbortController>();
|
|
91
98
|
let querySequence = 0;
|
|
92
99
|
let activeCurrentIdentity: string | undefined;
|
|
93
100
|
let sessionActive = false;
|
|
@@ -183,7 +190,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
183
190
|
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
184
191
|
if (
|
|
185
192
|
sessionActive &&
|
|
186
|
-
showCodexResetCountdown &&
|
|
193
|
+
(showCodexResetCountdown || outcome.state.report.source === "openai-chatgpt-companion") &&
|
|
187
194
|
outcome.state.report.buckets.some(
|
|
188
195
|
(bucket) => bucket.resetsAt !== undefined && Number.isFinite(bucket.resetsAt) && bucket.resetsAt * 1_000 > now,
|
|
189
196
|
)
|
|
@@ -220,7 +227,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
220
227
|
activeCurrentIdentity = nextIdentity;
|
|
221
228
|
};
|
|
222
229
|
|
|
223
|
-
const
|
|
230
|
+
const queryAdapterStateInternal = async (
|
|
224
231
|
ctx: ExtensionContext,
|
|
225
232
|
adapter: UsageProviderAdapter,
|
|
226
233
|
displayState: UsageDisplayState,
|
|
@@ -244,6 +251,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
244
251
|
);
|
|
245
252
|
} catch (error) {
|
|
246
253
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
254
|
+
if (adapter.id === "openai") invalidateProviderState(adapter.id);
|
|
247
255
|
const authState =
|
|
248
256
|
error instanceof UnsupportedOpenAIUsageAuthError
|
|
249
257
|
? "unsupported"
|
|
@@ -288,6 +296,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
288
296
|
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId);
|
|
289
297
|
if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
|
|
290
298
|
if (!auth) {
|
|
299
|
+
if (adapter.id === "openai") invalidateProviderState(adapter.id);
|
|
291
300
|
if (displayState === "current") {
|
|
292
301
|
transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
|
|
293
302
|
}
|
|
@@ -302,6 +311,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
302
311
|
authState: "unavailable",
|
|
303
312
|
};
|
|
304
313
|
}
|
|
314
|
+
if (adapter.id === "openai" && !auth.openaiCompanion) invalidateProviderState(adapter.id);
|
|
305
315
|
let retryableAuthChanged = false;
|
|
306
316
|
const guard = async () => {
|
|
307
317
|
if (signal.aborted || requestContextChanged()) throw abortError();
|
|
@@ -466,6 +476,25 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
466
476
|
}
|
|
467
477
|
};
|
|
468
478
|
|
|
479
|
+
const queryAdapterState = async (...args: Parameters<typeof queryAdapterStateInternal>): Promise<QueryOutcome> => {
|
|
480
|
+
const [ctx, adapter, displayState, force, signal, ...rest] = args;
|
|
481
|
+
if (adapter.id !== "openai") return queryAdapterStateInternal(...args);
|
|
482
|
+
const controller = new AbortController();
|
|
483
|
+
nativeQueryControllers.add(controller);
|
|
484
|
+
try {
|
|
485
|
+
return await queryAdapterStateInternal(
|
|
486
|
+
ctx,
|
|
487
|
+
adapter,
|
|
488
|
+
displayState,
|
|
489
|
+
force,
|
|
490
|
+
AbortSignal.any([signal, controller.signal]),
|
|
491
|
+
...rest,
|
|
492
|
+
);
|
|
493
|
+
} finally {
|
|
494
|
+
nativeQueryControllers.delete(controller);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
469
498
|
const loadTargetChoices = async (ctx: ExtensionContext, adapter: UsageProviderAdapter, signal: AbortSignal) => {
|
|
470
499
|
if (!adapter.targets) throw new Error("Provider does not support usage targets.");
|
|
471
500
|
const expectedSessionGeneration = sessionGeneration;
|
|
@@ -686,7 +715,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
686
715
|
const generation = statusGeneration;
|
|
687
716
|
const outcome = await queryAdapterState(ctx, adapter, displayState, force, signal);
|
|
688
717
|
if (signal.aborted) throw abortError();
|
|
689
|
-
if (
|
|
718
|
+
if (await outcomeStillCurrent(ctx, model, generation, outcome, signal)) return outcome;
|
|
690
719
|
force = false;
|
|
691
720
|
}
|
|
692
721
|
throw new Error("OpenAI runtime authentication kept changing; reopen /usage to retry.");
|
|
@@ -1253,6 +1282,17 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
1253
1282
|
}
|
|
1254
1283
|
const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
|
|
1255
1284
|
if (!revalidated) return { kind: "back" };
|
|
1285
|
+
// Current-provider revalidation can await unrelated work after the configured
|
|
1286
|
+
// native report was checked. Refresh its identity at the publication boundary.
|
|
1287
|
+
if (adapter.id === "openai") {
|
|
1288
|
+
outcome = await runMenuOperation(
|
|
1289
|
+
ctx,
|
|
1290
|
+
"Revalidating ChatGPT companion usage…",
|
|
1291
|
+
controller.signal,
|
|
1292
|
+
(signal) => queryStableAdapterState(ctx, adapter, "configured", false, signal),
|
|
1293
|
+
);
|
|
1294
|
+
if (!outcome) return { kind: "back" };
|
|
1295
|
+
}
|
|
1256
1296
|
stableCurrent = revalidated;
|
|
1257
1297
|
current = revalidated.outcome;
|
|
1258
1298
|
visibleStates = [
|
|
@@ -1300,6 +1340,22 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
1300
1340
|
});
|
|
1301
1341
|
const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
|
|
1302
1342
|
if (!revalidated) return { kind: "stay" };
|
|
1343
|
+
const nativeIndex = adapters.findIndex(
|
|
1344
|
+
(adapter) => adapter.id === "openai" && adapter.id !== currentProviderId,
|
|
1345
|
+
);
|
|
1346
|
+
if (nativeIndex >= 0) {
|
|
1347
|
+
const nativeAdapter = adapters[nativeIndex];
|
|
1348
|
+
if (nativeAdapter) {
|
|
1349
|
+
const native = await runMenuOperation(
|
|
1350
|
+
ctx,
|
|
1351
|
+
"Revalidating ChatGPT companion usage…",
|
|
1352
|
+
controller.signal,
|
|
1353
|
+
(signal) => queryStableAdapterState(ctx, nativeAdapter, "configured", false, signal),
|
|
1354
|
+
);
|
|
1355
|
+
if (!native) return { kind: "stay" };
|
|
1356
|
+
queriedStates[nativeIndex] = native.state;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1303
1359
|
stableCurrent = revalidated;
|
|
1304
1360
|
current = revalidated.outcome;
|
|
1305
1361
|
visibleStates = [
|
|
@@ -1356,6 +1412,8 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
1356
1412
|
startStatusRefresh(ctx, ctx.model, false);
|
|
1357
1413
|
});
|
|
1358
1414
|
pi.on("model_select", (event, ctx) => {
|
|
1415
|
+
// Cancel read-only native queries, not confirmed legacy reset mutations.
|
|
1416
|
+
for (const controller of nativeQueryControllers) controller.abort();
|
|
1359
1417
|
startStatusRefresh(ctx, event.model, false);
|
|
1360
1418
|
});
|
|
1361
1419
|
pi.on("turn_start", (_event, ctx) => {
|