@narumitw/pi-codex-usage 0.4.1 → 0.4.3
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/package.json +1 -1
- package/src/codex-usage.ts +128 -62
package/package.json
CHANGED
package/src/codex-usage.ts
CHANGED
|
@@ -143,6 +143,8 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
143
143
|
let statuslineClearTimer: ReturnType<typeof setTimeout> | undefined;
|
|
144
144
|
let statuslineRefreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
145
145
|
let statuslineRequestId = 0;
|
|
146
|
+
let sessionActive = false;
|
|
147
|
+
let activeStatuslineContext: ExtensionContext | undefined;
|
|
146
148
|
|
|
147
149
|
const clearStatuslineTimers = () => {
|
|
148
150
|
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
@@ -151,25 +153,57 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
151
153
|
statuslineRefreshTimer = undefined;
|
|
152
154
|
};
|
|
153
155
|
|
|
156
|
+
const handleStaleContextError = (ctx: ExtensionContext, error: unknown): boolean => {
|
|
157
|
+
if (!isStaleExtensionContextError(error)) return false;
|
|
158
|
+
if (ctx === activeStatuslineContext) {
|
|
159
|
+
statuslineRequestId += 1;
|
|
160
|
+
clearStatuslineTimers();
|
|
161
|
+
activeStatuslineContext = undefined;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const rethrowUnlessStaleContextError = (ctx: ExtensionContext) => (error: unknown) => {
|
|
167
|
+
if (!handleStaleContextError(ctx, error)) throw error;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const setStatuslineValue = (ctx: ExtensionContext, value: string | undefined): boolean => {
|
|
171
|
+
try {
|
|
172
|
+
ctx.ui.setStatus(STATUS_KEY, value);
|
|
173
|
+
return true;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (handleStaleContextError(ctx, error)) return false;
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
154
180
|
const clearUsageStatusline = (ctx: ExtensionContext) => {
|
|
155
181
|
statuslineRequestId += 1;
|
|
156
182
|
clearStatuslineTimers();
|
|
157
|
-
|
|
183
|
+
activeStatuslineContext = undefined;
|
|
184
|
+
setStatuslineValue(ctx, undefined);
|
|
158
185
|
};
|
|
159
186
|
|
|
160
187
|
const scheduleTemporaryStatuslineClear = (ctx: ExtensionContext) => {
|
|
161
188
|
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
189
|
+
const requestId = statuslineRequestId;
|
|
162
190
|
statuslineClearTimer = setTimeout(() => {
|
|
163
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
164
191
|
statuslineClearTimer = undefined;
|
|
192
|
+
if (!sessionActive || requestId !== statuslineRequestId) return;
|
|
193
|
+
setStatuslineValue(ctx, undefined);
|
|
165
194
|
}, CACHE_TTL_MS);
|
|
166
195
|
statuslineClearTimer.unref?.();
|
|
167
196
|
};
|
|
168
197
|
|
|
169
|
-
const scheduleStatuslineRefresh = (ctx: ExtensionContext) => {
|
|
198
|
+
const scheduleStatuslineRefresh = (ctx: ExtensionContext, model: CodexUsageModel | undefined) => {
|
|
170
199
|
if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
|
|
200
|
+
const requestId = statuslineRequestId;
|
|
171
201
|
statuslineRefreshTimer = setTimeout(() => {
|
|
172
|
-
|
|
202
|
+
statuslineRefreshTimer = undefined;
|
|
203
|
+
if (!sessionActive || requestId !== statuslineRequestId) return;
|
|
204
|
+
void refreshCurrentCodexUsageStatusline(ctx, true, model).catch(
|
|
205
|
+
rethrowUnlessStaleContextError(ctx),
|
|
206
|
+
);
|
|
173
207
|
}, CACHE_TTL_MS);
|
|
174
208
|
statuslineRefreshTimer.unref?.();
|
|
175
209
|
};
|
|
@@ -179,19 +213,23 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
179
213
|
report: CodexUsageReport,
|
|
180
214
|
options: { autoRefresh: boolean; model: CodexUsageModel | undefined },
|
|
181
215
|
) => {
|
|
216
|
+
if (!setStatuslineValue(ctx, formatCodexUsageStatusline(report, options.model))) return;
|
|
217
|
+
activeStatuslineContext = ctx;
|
|
182
218
|
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
183
219
|
statuslineClearTimer = undefined;
|
|
184
|
-
|
|
185
|
-
if (options.autoRefresh) scheduleStatuslineRefresh(ctx);
|
|
220
|
+
if (options.autoRefresh) scheduleStatuslineRefresh(ctx, options.model);
|
|
186
221
|
else scheduleTemporaryStatuslineClear(ctx);
|
|
187
222
|
};
|
|
188
223
|
|
|
189
224
|
const refreshCurrentCodexUsageStatusline = async (
|
|
190
225
|
ctx: ExtensionContext,
|
|
191
226
|
force: boolean,
|
|
192
|
-
model
|
|
227
|
+
model?: CodexUsageModel,
|
|
193
228
|
) => {
|
|
194
|
-
if (!
|
|
229
|
+
if (!sessionActive) return;
|
|
230
|
+
activeStatuslineContext = ctx;
|
|
231
|
+
const selectedModel = model ?? ctx.model;
|
|
232
|
+
if (!isOpenAICodexModel(selectedModel)) {
|
|
195
233
|
clearUsageStatusline(ctx);
|
|
196
234
|
return;
|
|
197
235
|
}
|
|
@@ -200,101 +238,120 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
200
238
|
statuslineRequestId = requestId;
|
|
201
239
|
const cached = cache && Date.now() - cache.createdAt < CACHE_TTL_MS ? cache : undefined;
|
|
202
240
|
if (cached && !force) {
|
|
203
|
-
setUsageStatusline(ctx, cached.report, { autoRefresh: true, model });
|
|
241
|
+
setUsageStatusline(ctx, cached.report, { autoRefresh: true, model: selectedModel });
|
|
204
242
|
return;
|
|
205
243
|
}
|
|
206
244
|
|
|
207
|
-
ctx
|
|
245
|
+
if (!setStatuslineValue(ctx, "📊 checking")) return;
|
|
208
246
|
const result = await queryUsage(ctx, { timeoutMs: DEFAULT_TIMEOUT_MS });
|
|
209
|
-
if (requestId !== statuslineRequestId) return;
|
|
210
|
-
if (!isOpenAICodexModel(ctx.model)) {
|
|
211
|
-
clearUsageStatusline(ctx);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
247
|
+
if (!sessionActive || requestId !== statuslineRequestId) return;
|
|
214
248
|
|
|
215
249
|
if (!result.ok) {
|
|
216
|
-
ctx
|
|
217
|
-
|
|
250
|
+
if (setStatuslineValue(ctx, "📊 usage error")) {
|
|
251
|
+
scheduleStatuslineRefresh(ctx, selectedModel);
|
|
252
|
+
}
|
|
218
253
|
return;
|
|
219
254
|
}
|
|
220
255
|
|
|
221
256
|
cache = { createdAt: Date.now(), report: result.report };
|
|
222
|
-
setUsageStatusline(ctx, result.report, { autoRefresh: true, model });
|
|
257
|
+
setUsageStatusline(ctx, result.report, { autoRefresh: true, model: selectedModel });
|
|
223
258
|
};
|
|
224
259
|
|
|
225
260
|
pi.registerCommand(COMMAND_NAME, {
|
|
226
261
|
description: "Show Codex ChatGPT subscription usage and rate-limit windows",
|
|
227
262
|
handler: async (args, ctx) => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (options.value.clearStatusline) {
|
|
235
|
-
clearUsageStatusline(ctx);
|
|
236
|
-
ctx.ui.notify("Codex usage statusline cleared.", "info");
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
263
|
+
try {
|
|
264
|
+
const options = parseArgs(args);
|
|
265
|
+
if (!options.ok) {
|
|
266
|
+
ctx.ui.notify(options.error, "warning");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
239
269
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
autoRefresh: isOpenAICodexModel(ctx.model),
|
|
245
|
-
model: ctx.model,
|
|
246
|
-
});
|
|
270
|
+
if (options.value.clearStatusline) {
|
|
271
|
+
clearUsageStatusline(ctx);
|
|
272
|
+
ctx.ui.notify("Codex usage statusline cleared.", "info");
|
|
273
|
+
return;
|
|
247
274
|
}
|
|
248
|
-
showReport(ctx, cached.report, true);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
275
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
276
|
+
const cached = cache && Date.now() - cache.createdAt < CACHE_TTL_MS ? cache : undefined;
|
|
277
|
+
if (cached && !options.value.refresh) {
|
|
278
|
+
if (options.value.statusline) {
|
|
279
|
+
setUsageStatusline(ctx, cached.report, {
|
|
280
|
+
autoRefresh: isOpenAICodexModel(ctx.model),
|
|
281
|
+
model: ctx.model,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
showReport(ctx, cached.report, true);
|
|
258
285
|
return;
|
|
259
286
|
}
|
|
260
287
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
288
|
+
let keepStatusline = false;
|
|
289
|
+
const statuslineStarted =
|
|
290
|
+
options.value.statusline && setStatuslineValue(ctx, "📊 checking");
|
|
291
|
+
try {
|
|
292
|
+
const result = await queryUsage(ctx, options.value);
|
|
293
|
+
if (!result.ok) {
|
|
294
|
+
ctx.ui.notify(formatQueryErrors(result.errors), "error");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
cache = { createdAt: Date.now(), report: result.report };
|
|
299
|
+
if (options.value.statusline) {
|
|
300
|
+
setUsageStatusline(ctx, result.report, {
|
|
301
|
+
autoRefresh: isOpenAICodexModel(ctx.model),
|
|
302
|
+
model: ctx.model,
|
|
303
|
+
});
|
|
304
|
+
keepStatusline = true;
|
|
305
|
+
}
|
|
306
|
+
showReport(ctx, result.report, false);
|
|
307
|
+
} finally {
|
|
308
|
+
if (statuslineStarted && !keepStatusline) setStatuslineValue(ctx, undefined);
|
|
268
309
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (handleStaleContextError(ctx, error)) return;
|
|
312
|
+
throw error;
|
|
272
313
|
}
|
|
273
314
|
},
|
|
274
315
|
});
|
|
275
316
|
|
|
276
317
|
pi.on("session_start", (_event, ctx) => {
|
|
277
|
-
|
|
278
|
-
|
|
318
|
+
sessionActive = true;
|
|
319
|
+
if (isOpenAICodexModel(ctx.model)) {
|
|
320
|
+
void refreshCurrentCodexUsageStatusline(ctx, false, ctx.model).catch(
|
|
321
|
+
rethrowUnlessStaleContextError(ctx),
|
|
322
|
+
);
|
|
323
|
+
} else {
|
|
324
|
+
clearUsageStatusline(ctx);
|
|
325
|
+
}
|
|
279
326
|
});
|
|
280
327
|
|
|
281
328
|
pi.on("session_tree", (_event, ctx) => {
|
|
282
|
-
if (isOpenAICodexModel(ctx.model))
|
|
283
|
-
|
|
329
|
+
if (isOpenAICodexModel(ctx.model)) {
|
|
330
|
+
void refreshCurrentCodexUsageStatusline(ctx, false, ctx.model).catch(
|
|
331
|
+
rethrowUnlessStaleContextError(ctx),
|
|
332
|
+
);
|
|
333
|
+
} else {
|
|
334
|
+
clearUsageStatusline(ctx);
|
|
335
|
+
}
|
|
284
336
|
});
|
|
285
337
|
|
|
286
338
|
pi.on("model_select", (event, ctx) => {
|
|
287
339
|
if (isOpenAICodexModel(event.model)) {
|
|
288
|
-
void refreshCurrentCodexUsageStatusline(ctx, false, event.model)
|
|
340
|
+
void refreshCurrentCodexUsageStatusline(ctx, false, event.model).catch(
|
|
341
|
+
rethrowUnlessStaleContextError(ctx),
|
|
342
|
+
);
|
|
289
343
|
} else {
|
|
290
344
|
clearUsageStatusline(ctx);
|
|
291
345
|
}
|
|
292
346
|
});
|
|
293
347
|
|
|
294
|
-
pi.on("session_shutdown", (_event, ctx) =>
|
|
348
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
349
|
+
sessionActive = false;
|
|
350
|
+
clearUsageStatusline(ctx);
|
|
351
|
+
});
|
|
295
352
|
}
|
|
296
353
|
|
|
297
|
-
function parseArgs(
|
|
354
|
+
export function parseArgs(
|
|
298
355
|
args: string,
|
|
299
356
|
): { ok: true; value: QueryUsageOptions } | { ok: false; error: string } {
|
|
300
357
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
@@ -342,6 +399,13 @@ function isOpenAICodexModel(model: Pick<PiModel, "provider"> | undefined): boole
|
|
|
342
399
|
return model?.provider === CODEX_PROVIDER_ID;
|
|
343
400
|
}
|
|
344
401
|
|
|
402
|
+
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
403
|
+
return (
|
|
404
|
+
error instanceof Error &&
|
|
405
|
+
error.message.includes("This extension ctx is stale after session replacement or reload")
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
345
409
|
async function queryUsage(
|
|
346
410
|
ctx: ExtensionContext,
|
|
347
411
|
options: Pick<QueryUsageOptions, "timeoutMs">,
|
|
@@ -352,6 +416,7 @@ async function queryUsage(
|
|
|
352
416
|
const report = await queryViaPiAuth(ctx, options.timeoutMs);
|
|
353
417
|
return { ok: true, report };
|
|
354
418
|
} catch (cause) {
|
|
419
|
+
if (isStaleExtensionContextError(cause)) throw cause;
|
|
355
420
|
errors.push({ source: "pi-auth", message: errorMessage(cause), cause });
|
|
356
421
|
}
|
|
357
422
|
|
|
@@ -359,6 +424,7 @@ async function queryUsage(
|
|
|
359
424
|
const report = await queryViaCodexAppServer(options.timeoutMs);
|
|
360
425
|
return { ok: true, report };
|
|
361
426
|
} catch (cause) {
|
|
427
|
+
if (isStaleExtensionContextError(cause)) throw cause;
|
|
362
428
|
errors.push({ source: "codex-app-server", message: errorMessage(cause), cause });
|
|
363
429
|
}
|
|
364
430
|
|