pi-better-openai 0.1.19 → 0.1.21
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 +4 -4
- package/index.ts +145 -56
- package/package.json +6 -4
- package/src/codex-auth.ts +38 -7
- package/src/config.ts +3 -2
- package/src/fast-controller.ts +1 -1
- package/src/format.ts +45 -20
- package/src/image.ts +116 -127
- package/src/paths.ts +17 -0
- package/src/pet-footer-controller.ts +37 -21
- package/src/pets.ts +104 -47
- package/src/usage-controller.ts +133 -48
- package/src/usage.ts +91 -23
package/src/pets.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { constants, type Dirent } from "node:fs";
|
|
|
2
2
|
import { access, lstat, readdir, readFile, realpath } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, resolve, sep } from "node:path";
|
|
5
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import {
|
|
7
7
|
calculateImageRows,
|
|
8
8
|
getCapabilities,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
truncateToWidth,
|
|
13
13
|
type AutocompleteItem,
|
|
14
14
|
type ImageTheme,
|
|
15
|
-
} from "@
|
|
15
|
+
} from "@earendil-works/pi-tui";
|
|
16
16
|
import sharp from "sharp";
|
|
17
17
|
import { isRecord, type PetState } from "./config.ts";
|
|
18
18
|
|
|
@@ -25,6 +25,7 @@ const DEFAULT_SPRITESHEET_PATH = "spritesheet.webp";
|
|
|
25
25
|
const EXPECTED_ATLAS_WIDTH = DEFAULT_CELL_WIDTH * PET_COLUMNS;
|
|
26
26
|
const EXPECTED_ATLAS_HEIGHT = DEFAULT_CELL_HEIGHT * PET_ROWS;
|
|
27
27
|
const PET_CATALOG_CACHE_TTL_MS = 1500;
|
|
28
|
+
const PET_CATALOG_READ_CONCURRENCY = 4;
|
|
28
29
|
|
|
29
30
|
type ListCodexPetsOptions = { refresh?: boolean };
|
|
30
31
|
|
|
@@ -61,8 +62,8 @@ export type CodexPetPackage = {
|
|
|
61
62
|
export type PetFrame = {
|
|
62
63
|
data?: string;
|
|
63
64
|
rawRgbaData?: string;
|
|
65
|
+
kittyUpload?: string;
|
|
64
66
|
kittyImageId?: number;
|
|
65
|
-
kittyUploaded?: boolean;
|
|
66
67
|
mimeType: "image/png";
|
|
67
68
|
durationMs: number;
|
|
68
69
|
widthPx: number;
|
|
@@ -265,6 +266,25 @@ async function readCodexPetPackage(
|
|
|
265
266
|
};
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
async function readCodexPetPackages(
|
|
270
|
+
petsDir: string,
|
|
271
|
+
entries: Dirent[],
|
|
272
|
+
options: { validateSpritesheet: boolean },
|
|
273
|
+
): Promise<CodexPetPackage[]> {
|
|
274
|
+
const pets: Array<CodexPetPackage | undefined> = Array.from({ length: entries.length });
|
|
275
|
+
let nextIndex = 0;
|
|
276
|
+
const readNext = async (): Promise<void> => {
|
|
277
|
+
while (nextIndex < entries.length) {
|
|
278
|
+
const index = nextIndex++;
|
|
279
|
+
pets[index] = await readCodexPetPackage(petsDir, entries[index], options);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
await Promise.all(
|
|
283
|
+
Array.from({ length: Math.min(PET_CATALOG_READ_CONCURRENCY, entries.length) }, readNext),
|
|
284
|
+
);
|
|
285
|
+
return pets.filter((pet): pet is CodexPetPackage => pet !== undefined);
|
|
286
|
+
}
|
|
287
|
+
|
|
268
288
|
export async function listCodexPets(
|
|
269
289
|
home = codexHome(),
|
|
270
290
|
options: ListCodexPetsOptions = {},
|
|
@@ -274,22 +294,22 @@ export async function listCodexPets(
|
|
|
274
294
|
if (!options.refresh && cached && cached.expiresAt > Date.now()) return cached.pets;
|
|
275
295
|
|
|
276
296
|
const { dir, entries } = await readPetDirectoryEntries(home);
|
|
277
|
-
const pets:
|
|
278
|
-
for (const entry of entries) {
|
|
279
|
-
const pet = await readCodexPetPackage(dir, entry, { validateSpritesheet: true });
|
|
280
|
-
if (pet) pets.push(pet);
|
|
281
|
-
}
|
|
297
|
+
const pets = await readCodexPetPackages(dir, entries, { validateSpritesheet: true });
|
|
282
298
|
pets.sort((a, b) => a.name.localeCompare(b.name));
|
|
283
299
|
petCatalogCache.set(cacheKey, { expiresAt: Date.now() + PET_CATALOG_CACHE_TTL_MS, pets });
|
|
284
300
|
return pets;
|
|
285
301
|
}
|
|
286
302
|
|
|
287
|
-
function petLookupKey(value: string): string {
|
|
288
|
-
return value
|
|
303
|
+
export function petLookupKey(value: string): string {
|
|
304
|
+
return value
|
|
305
|
+
.normalize("NFKC")
|
|
306
|
+
.toLowerCase()
|
|
307
|
+
.replace(/[^\p{L}\p{N}\p{M}]+/gu, "");
|
|
289
308
|
}
|
|
290
309
|
|
|
291
310
|
type PetLookupRequest = {
|
|
292
311
|
hasText: boolean;
|
|
312
|
+
exact?: string;
|
|
293
313
|
key?: string;
|
|
294
314
|
};
|
|
295
315
|
|
|
@@ -297,7 +317,11 @@ function petLookupRequest(value?: string): PetLookupRequest {
|
|
|
297
317
|
const requestedText = value?.trim();
|
|
298
318
|
if (!requestedText) return { hasText: false };
|
|
299
319
|
|
|
300
|
-
return {
|
|
320
|
+
return {
|
|
321
|
+
hasText: true,
|
|
322
|
+
exact: requestedText.normalize("NFKC").toLowerCase(),
|
|
323
|
+
key: petLookupKey(requestedText) || undefined,
|
|
324
|
+
};
|
|
301
325
|
}
|
|
302
326
|
|
|
303
327
|
function petMatchesLookup(pet: CodexPetPackage, requested: string): boolean {
|
|
@@ -308,11 +332,35 @@ function petMatchesLookup(pet: CodexPetPackage, requested: string): boolean {
|
|
|
308
332
|
);
|
|
309
333
|
}
|
|
310
334
|
|
|
335
|
+
function exactPetMatches(
|
|
336
|
+
pets: CodexPetPackage[],
|
|
337
|
+
requested: string,
|
|
338
|
+
): CodexPetPackage[] | undefined {
|
|
339
|
+
const fields: Array<(pet: CodexPetPackage) => string | undefined> = [
|
|
340
|
+
(pet) => pet.slug,
|
|
341
|
+
(pet) => pet.id,
|
|
342
|
+
(pet) => pet.name,
|
|
343
|
+
];
|
|
344
|
+
for (const field of fields) {
|
|
345
|
+
const matches = pets.filter((pet) => field(pet)?.normalize("NFKC").toLowerCase() === requested);
|
|
346
|
+
if (matches.length > 0) return matches;
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function uniquePetMatch(pets: CodexPetPackage[]): CodexPetPackage | undefined {
|
|
352
|
+
return pets.length === 1 ? pets[0] : undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
311
355
|
export function findCodexPet(pets: CodexPetPackage[], value?: string): CodexPetPackage | undefined {
|
|
312
356
|
const request = petLookupRequest(value);
|
|
313
357
|
const { key } = request;
|
|
314
358
|
if (!key) return undefined;
|
|
315
|
-
|
|
359
|
+
if (request.exact) {
|
|
360
|
+
const exactMatches = exactPetMatches(pets, request.exact);
|
|
361
|
+
if (exactMatches) return uniquePetMatch(exactMatches);
|
|
362
|
+
}
|
|
363
|
+
return uniquePetMatch(pets.filter((pet) => petMatchesLookup(pet, key)));
|
|
316
364
|
}
|
|
317
365
|
|
|
318
366
|
export function findReadyCodexPet(
|
|
@@ -324,7 +372,14 @@ export function findReadyCodexPet(
|
|
|
324
372
|
|
|
325
373
|
const { key } = request;
|
|
326
374
|
if (!key) return undefined;
|
|
327
|
-
|
|
375
|
+
if (request.exact) {
|
|
376
|
+
const exactMatches = exactPetMatches(pets, request.exact);
|
|
377
|
+
if (exactMatches) {
|
|
378
|
+
const exact = uniquePetMatch(exactMatches);
|
|
379
|
+
return exact?.hasSpritesheet ? exact : undefined;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return uniquePetMatch(pets.filter((pet) => pet.hasSpritesheet && petMatchesLookup(pet, key)));
|
|
328
383
|
}
|
|
329
384
|
|
|
330
385
|
function selectPet(pets: CodexPetPackage[], slug?: string): CodexPetPackage | undefined {
|
|
@@ -431,18 +486,15 @@ async function findCodexPetDirect(
|
|
|
431
486
|
const request = petLookupRequest(slug);
|
|
432
487
|
if (!request.key) return undefined;
|
|
433
488
|
const { dir, entries } = await readPetDirectoryEntries(home);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
return undefined;
|
|
489
|
+
const candidates = await readCodexPetPackages(dir, entries, { validateSpritesheet: false });
|
|
490
|
+
const candidate = findCodexPet(candidates, slug);
|
|
491
|
+
if (!candidate || candidate.spritesheetIssue) return candidate;
|
|
492
|
+
const spritesheetIssue = await validatePetSpritesheet(candidate.dir, candidate.spritesheetPath);
|
|
493
|
+
return {
|
|
494
|
+
...candidate,
|
|
495
|
+
hasSpritesheet: spritesheetIssue === undefined,
|
|
496
|
+
spritesheetIssue,
|
|
497
|
+
};
|
|
446
498
|
}
|
|
447
499
|
|
|
448
500
|
export async function loadCodexPet(
|
|
@@ -513,12 +565,20 @@ export async function loadCodexPet(
|
|
|
513
565
|
kernel: sharp.kernel.nearest,
|
|
514
566
|
});
|
|
515
567
|
}
|
|
568
|
+
const kittyImageId = useKitty ? kittyImageBase + kittyFrameOffset++ : undefined;
|
|
516
569
|
const encoded = useKitty
|
|
517
|
-
? {
|
|
518
|
-
|
|
570
|
+
? {
|
|
571
|
+
kittyUpload: encodeKittyRawRgbaData(
|
|
572
|
+
(await frame.ensureAlpha().raw().toBuffer()).toString("base64"),
|
|
573
|
+
outputWidthPx,
|
|
574
|
+
outputHeightPx,
|
|
575
|
+
kittyImageId!,
|
|
576
|
+
),
|
|
577
|
+
}
|
|
578
|
+
: { data: (await frame.png().toBuffer()).toString("base64") };
|
|
519
579
|
states[state].push({
|
|
520
580
|
...encoded,
|
|
521
|
-
kittyImageId
|
|
581
|
+
kittyImageId,
|
|
522
582
|
mimeType: "image/png",
|
|
523
583
|
durationMs,
|
|
524
584
|
widthPx: outputWidthPx,
|
|
@@ -575,12 +635,6 @@ function forEachCodexPetFrame(
|
|
|
575
635
|
}
|
|
576
636
|
}
|
|
577
637
|
|
|
578
|
-
function markCodexPetKittyFramesUnuploaded(pet?: LoadedCodexPet): void {
|
|
579
|
-
forEachCodexPetFrame(pet, (frame) => {
|
|
580
|
-
frame.kittyUploaded = false;
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
|
|
584
638
|
function codexPetKittyImageIds(pet?: LoadedCodexPet): number[] {
|
|
585
639
|
const imageIds = new Set<number>();
|
|
586
640
|
forEachCodexPetFrame(pet, (frame) => {
|
|
@@ -601,18 +655,15 @@ function placeKittyImage(imageId: number, columns: number, rows: number): string
|
|
|
601
655
|
return `\x1b_Ga=p,i=${imageId},p=1,c=${columns},r=${rows},q=2,C=1\x1b\\`;
|
|
602
656
|
}
|
|
603
657
|
|
|
604
|
-
function
|
|
605
|
-
|
|
658
|
+
function encodeKittyRawRgbaData(
|
|
659
|
+
rawRgbaData: string,
|
|
660
|
+
widthPx: number,
|
|
661
|
+
heightPx: number,
|
|
662
|
+
imageId: number,
|
|
663
|
+
): string {
|
|
606
664
|
if (!rawRgbaData) return "";
|
|
607
665
|
const chunkSize = 4096;
|
|
608
|
-
const params = [
|
|
609
|
-
"a=t",
|
|
610
|
-
"f=32",
|
|
611
|
-
`s=${frame.widthPx}`,
|
|
612
|
-
`v=${frame.heightPx}`,
|
|
613
|
-
`i=${imageId}`,
|
|
614
|
-
"q=2",
|
|
615
|
-
].join(",");
|
|
666
|
+
const params = ["a=t", "f=32", `s=${widthPx}`, `v=${heightPx}`, `i=${imageId}`, "q=2"].join(",");
|
|
616
667
|
if (rawRgbaData.length <= chunkSize) {
|
|
617
668
|
return `\x1b_G${params};${rawRgbaData}\x1b\\`;
|
|
618
669
|
}
|
|
@@ -628,6 +679,15 @@ function encodeKittyRawRgba(frame: PetFrame, imageId: number): string {
|
|
|
628
679
|
return chunks.join("");
|
|
629
680
|
}
|
|
630
681
|
|
|
682
|
+
function encodeKittyRawRgba(frame: PetFrame, imageId: number): string {
|
|
683
|
+
return (
|
|
684
|
+
frame.kittyUpload ??
|
|
685
|
+
(frame.rawRgbaData
|
|
686
|
+
? encodeKittyRawRgbaData(frame.rawRgbaData, frame.widthPx, frame.heightPx, imageId)
|
|
687
|
+
: "")
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
631
691
|
export class CodexPetKittyManager {
|
|
632
692
|
private previousFrameImageId: number | undefined;
|
|
633
693
|
private readonly pendingCleanupImageIds = new Set<number>();
|
|
@@ -641,7 +701,6 @@ export class CodexPetKittyManager {
|
|
|
641
701
|
invalidate(pet?: LoadedCodexPet): void {
|
|
642
702
|
this.queueCleanup(pet);
|
|
643
703
|
this.previousFrameImageId = undefined;
|
|
644
|
-
markCodexPetKittyFramesUnuploaded(pet);
|
|
645
704
|
}
|
|
646
705
|
|
|
647
706
|
resetForResize(pet?: LoadedCodexPet): void {
|
|
@@ -683,7 +742,6 @@ export class CodexPetKittyManager {
|
|
|
683
742
|
// occasionally invisible pet. The TUI diff still avoids writing this payload
|
|
684
743
|
// again when the rendered line is unchanged.
|
|
685
744
|
const upload = encodeKittyRawRgba(frame, frameImageId);
|
|
686
|
-
frame.kittyUploaded = true;
|
|
687
745
|
this.previousFrameImageId = frameImageId;
|
|
688
746
|
// Recent pi-tui versions automatically free Kitty image IDs they own on changed lines.
|
|
689
747
|
// Pet frames are managed here and intentionally reused across animation loops, so keep
|
|
@@ -716,7 +774,6 @@ function defaultKittyManager(placementImageId: number): CodexPetKittyManager {
|
|
|
716
774
|
export function resetCodexPetKittyCache(pet?: LoadedCodexPet, placementImageId?: number): void {
|
|
717
775
|
if (placementImageId !== undefined) defaultKittyManagers.get(placementImageId)?.invalidate(pet);
|
|
718
776
|
else for (const manager of defaultKittyManagers.values()) manager.invalidate(pet);
|
|
719
|
-
markCodexPetKittyFramesUnuploaded(pet);
|
|
720
777
|
}
|
|
721
778
|
|
|
722
779
|
export function renderCodexPetFrame(
|
package/src/usage-controller.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ResolvedConfig } from "./config.ts";
|
|
3
|
-
import { maskIdentifier } from "./format.ts";
|
|
3
|
+
import { maskIdentifier, sanitizeDiagnosticError } from "./format.ts";
|
|
4
4
|
import {
|
|
5
5
|
AUTH_FILE,
|
|
6
6
|
type UsageSnapshot,
|
|
@@ -9,19 +9,34 @@ import {
|
|
|
9
9
|
parseUsageSnapshot,
|
|
10
10
|
readCodexAuth,
|
|
11
11
|
requestCodexUsage,
|
|
12
|
+
usageScopeForModel,
|
|
12
13
|
} from "./usage.ts";
|
|
13
14
|
import { currentModelKey } from "./fast-controller.ts";
|
|
14
15
|
|
|
15
|
-
export function isOpenAISubscriptionModel(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
export function isOpenAISubscriptionModel(
|
|
17
|
+
ctx: ExtensionContext,
|
|
18
|
+
cfg: ResolvedConfig,
|
|
19
|
+
isUsingOAuth?: boolean,
|
|
20
|
+
): boolean {
|
|
21
|
+
const model = ctx.model;
|
|
22
|
+
if (!model || (model.provider !== "openai" && model.provider !== "openai-codex")) return false;
|
|
23
|
+
return (
|
|
24
|
+
!cfg.usage.showOnlyOnSubscriptionModels ||
|
|
25
|
+
(isUsingOAuth ?? ctx.modelRegistry.isUsingOAuth(model))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const STALE_EXTENSION_CONTEXT_MESSAGE = "This extension ctx is stale";
|
|
30
|
+
|
|
31
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
32
|
+
return error instanceof Error && error.message.includes(STALE_EXTENSION_CONTEXT_MESSAGE);
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
type UsageRefreshOptions = { notify?: boolean; force?: boolean };
|
|
22
36
|
|
|
23
37
|
type QueuedUsageRefresh = {
|
|
24
38
|
ctx: ExtensionContext;
|
|
39
|
+
generation: number;
|
|
25
40
|
modelId?: string;
|
|
26
41
|
notify?: boolean;
|
|
27
42
|
force?: boolean;
|
|
@@ -39,6 +54,7 @@ export class UsageController {
|
|
|
39
54
|
private usageAbortController: AbortController | undefined;
|
|
40
55
|
private sessionAbortSignal: AbortSignal | undefined;
|
|
41
56
|
private sessionAbortHandler: (() => void) | undefined;
|
|
57
|
+
private sessionGeneration = 0;
|
|
42
58
|
|
|
43
59
|
constructor(
|
|
44
60
|
private readonly getConfig: (ctx: ExtensionContext) => ResolvedConfig,
|
|
@@ -49,8 +65,16 @@ export class UsageController {
|
|
|
49
65
|
return this.usageSnapshot;
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
statusLine(
|
|
53
|
-
|
|
68
|
+
statusLine(
|
|
69
|
+
ctx: ExtensionContext,
|
|
70
|
+
cfg = this.getConfig(ctx),
|
|
71
|
+
isUsingOAuth?: boolean,
|
|
72
|
+
): string | undefined {
|
|
73
|
+
return this.usageSnapshot &&
|
|
74
|
+
!this.usageError &&
|
|
75
|
+
this.usageSnapshot.scope === usageScopeForModel(ctx.model?.id) &&
|
|
76
|
+
cfg.usage.enabled &&
|
|
77
|
+
isOpenAISubscriptionModel(ctx, cfg, isUsingOAuth)
|
|
54
78
|
? formatUsageSnapshot(this.usageSnapshot, cfg.usage)
|
|
55
79
|
: undefined;
|
|
56
80
|
}
|
|
@@ -60,8 +84,9 @@ export class UsageController {
|
|
|
60
84
|
if (!cfg.usage.enabled) return "Usage display is disabled.";
|
|
61
85
|
if (!isOpenAISubscriptionModel(ctx, cfg))
|
|
62
86
|
return "Usage hidden: current model is not an OpenAI subscription model.";
|
|
63
|
-
if (
|
|
64
|
-
|
|
87
|
+
if (this.usageError) return `Usage unavailable: ${this.usageError}`;
|
|
88
|
+
if (!this.usageSnapshot || this.usageSnapshot.scope !== usageScopeForModel(ctx.model?.id))
|
|
89
|
+
return "Usage unavailable.";
|
|
65
90
|
const stale =
|
|
66
91
|
this.usageUpdatedAt && Date.now() - this.usageUpdatedAt > cfg.usage.refreshIntervalMs * 2
|
|
67
92
|
? ` | stale ${formatResetCountdown((Date.now() - this.usageUpdatedAt) / 1000)}`
|
|
@@ -87,74 +112,124 @@ export class UsageController {
|
|
|
87
112
|
].join("\n");
|
|
88
113
|
}
|
|
89
114
|
|
|
115
|
+
private isGenerationCurrent(generation: number): boolean {
|
|
116
|
+
return !this.shuttingDown && generation === this.sessionGeneration;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private deactivateGeneration(generation: number): void {
|
|
120
|
+
if (generation !== this.sessionGeneration) return;
|
|
121
|
+
this.shuttingDown = true;
|
|
122
|
+
this.sessionGeneration++;
|
|
123
|
+
this.queuedUsageRefresh = undefined;
|
|
124
|
+
this.usageAbortController?.abort();
|
|
125
|
+
this.usageAbortController = undefined;
|
|
126
|
+
this.stopTimer();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private handleStaleContextError(error: unknown, generation: number): boolean {
|
|
130
|
+
if (!isStaleExtensionContextError(error)) return false;
|
|
131
|
+
this.deactivateGeneration(generation);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
90
135
|
async refresh(
|
|
91
136
|
ctx: ExtensionContext,
|
|
92
|
-
modelId
|
|
137
|
+
modelId?: string,
|
|
93
138
|
options?: UsageRefreshOptions,
|
|
139
|
+
generation = this.sessionGeneration,
|
|
94
140
|
): Promise<void> {
|
|
95
|
-
if (this.
|
|
141
|
+
if (!this.isGenerationCurrent(generation)) return;
|
|
142
|
+
|
|
143
|
+
let resolvedModelId = modelId;
|
|
144
|
+
try {
|
|
145
|
+
if (!ctx.hasUI) return;
|
|
146
|
+
resolvedModelId ??= ctx.model?.id;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.handleStaleContextError(error, generation);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
96
152
|
if (this.usageRefreshInFlight) {
|
|
153
|
+
const queued =
|
|
154
|
+
this.queuedUsageRefresh?.generation === generation ? this.queuedUsageRefresh : undefined;
|
|
97
155
|
this.queuedUsageRefresh = {
|
|
98
156
|
ctx,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
157
|
+
generation,
|
|
158
|
+
modelId: resolvedModelId,
|
|
159
|
+
notify: queued?.notify || options?.notify,
|
|
160
|
+
force: queued?.force || options?.force,
|
|
102
161
|
};
|
|
103
162
|
return;
|
|
104
163
|
}
|
|
164
|
+
|
|
105
165
|
this.usageRefreshInFlight = true;
|
|
106
|
-
const cfg = this.getConfig(ctx);
|
|
107
166
|
try {
|
|
167
|
+
const cfg = this.getConfig(ctx);
|
|
168
|
+
if (!this.isGenerationCurrent(generation)) return;
|
|
169
|
+
|
|
108
170
|
if (!cfg.usage.enabled) {
|
|
109
171
|
this.usageSnapshot = undefined;
|
|
110
172
|
this.usageError = "Usage display is disabled.";
|
|
111
|
-
|
|
112
|
-
if (
|
|
173
|
+
this.updateFooter(ctx);
|
|
174
|
+
if (options?.notify) ctx.ui.notify(this.formatStatus(ctx), "warning");
|
|
113
175
|
return;
|
|
114
176
|
}
|
|
115
177
|
if (!isOpenAISubscriptionModel(ctx, cfg)) {
|
|
116
|
-
|
|
117
|
-
if (
|
|
178
|
+
this.updateFooter(ctx);
|
|
179
|
+
if (options?.notify) ctx.ui.notify(this.formatStatus(ctx), "warning");
|
|
118
180
|
return;
|
|
119
181
|
}
|
|
120
182
|
const shouldThrottle =
|
|
121
183
|
!options?.force &&
|
|
122
184
|
!options?.notify &&
|
|
123
185
|
this.usageLastFetchAt !== undefined &&
|
|
124
|
-
Date.now() - this.usageLastFetchAt < cfg.usage.refreshIntervalMs
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (shouldThrottle) {
|
|
128
|
-
if (!this.shuttingDown) this.updateFooter(ctx);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
186
|
+
Date.now() - this.usageLastFetchAt < cfg.usage.refreshIntervalMs;
|
|
187
|
+
if (shouldThrottle) return;
|
|
188
|
+
this.usageLastFetchAt = Date.now();
|
|
131
189
|
this.usageAbortController = new AbortController();
|
|
132
190
|
const timeoutSignal = AbortSignal.timeout(10_000);
|
|
133
191
|
const signal = ctx.signal
|
|
134
192
|
? AbortSignal.any([ctx.signal, timeoutSignal, this.usageAbortController.signal])
|
|
135
193
|
: AbortSignal.any([timeoutSignal, this.usageAbortController.signal]);
|
|
136
194
|
const data = await requestCodexUsage(ctx, signal);
|
|
137
|
-
this.
|
|
138
|
-
|
|
195
|
+
if (!this.isGenerationCurrent(generation)) return;
|
|
196
|
+
|
|
197
|
+
this.usageSnapshot = data ? parseUsageSnapshot(data, resolvedModelId) : undefined;
|
|
139
198
|
this.usageUpdatedAt = this.usageSnapshot ? Date.now() : undefined;
|
|
140
199
|
this.usageError = data
|
|
141
200
|
? undefined
|
|
142
201
|
: `Missing openai-codex OAuth credentials in ${AUTH_FILE}.`;
|
|
143
|
-
|
|
144
|
-
if (
|
|
202
|
+
this.updateFooter(ctx);
|
|
203
|
+
if (options?.notify)
|
|
145
204
|
ctx.ui.notify(this.formatStatus(ctx), this.usageSnapshot ? "info" : "warning");
|
|
146
205
|
} catch (error) {
|
|
147
|
-
if (this.
|
|
148
|
-
|
|
149
|
-
this.
|
|
150
|
-
|
|
206
|
+
if (this.handleStaleContextError(error, generation) || !this.isGenerationCurrent(generation))
|
|
207
|
+
return;
|
|
208
|
+
this.usageError = sanitizeDiagnosticError(
|
|
209
|
+
error instanceof Error ? error.message : String(error),
|
|
210
|
+
);
|
|
211
|
+
try {
|
|
212
|
+
this.updateFooter(ctx);
|
|
213
|
+
if (options?.notify) ctx.ui.notify(this.formatStatus(ctx), "warning");
|
|
214
|
+
} catch (secondaryError) {
|
|
215
|
+
if (!this.handleStaleContextError(secondaryError, generation)) {
|
|
216
|
+
this.usageError = sanitizeDiagnosticError(
|
|
217
|
+
secondaryError instanceof Error ? secondaryError.message : String(secondaryError),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
151
221
|
} finally {
|
|
152
222
|
this.usageAbortController = undefined;
|
|
153
223
|
this.usageRefreshInFlight = false;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
void this.refresh(
|
|
224
|
+
const next = this.queuedUsageRefresh;
|
|
225
|
+
this.queuedUsageRefresh = undefined;
|
|
226
|
+
if (next && !this.shuttingDown && next.generation === this.sessionGeneration) {
|
|
227
|
+
void this.refresh(
|
|
228
|
+
next.ctx,
|
|
229
|
+
next.modelId,
|
|
230
|
+
{ notify: next.notify, force: next.force },
|
|
231
|
+
next.generation,
|
|
232
|
+
);
|
|
158
233
|
}
|
|
159
234
|
}
|
|
160
235
|
}
|
|
@@ -170,33 +245,42 @@ export class UsageController {
|
|
|
170
245
|
}
|
|
171
246
|
|
|
172
247
|
start(ctx: ExtensionContext): void {
|
|
173
|
-
this.
|
|
248
|
+
this.usageAbortController?.abort();
|
|
249
|
+
this.queuedUsageRefresh = undefined;
|
|
174
250
|
this.stopTimer();
|
|
251
|
+
const generation = ++this.sessionGeneration;
|
|
252
|
+
this.shuttingDown = false;
|
|
253
|
+
|
|
175
254
|
const cfg = this.getConfig(ctx);
|
|
176
255
|
if (!cfg.usage.enabled) return;
|
|
177
256
|
const sessionSignal = ctx.signal;
|
|
178
|
-
if (sessionSignal?.aborted)
|
|
257
|
+
if (sessionSignal?.aborted) {
|
|
258
|
+
this.deactivateGeneration(generation);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
179
261
|
this.sessionAbortSignal = sessionSignal;
|
|
180
262
|
this.sessionAbortHandler = () => {
|
|
181
|
-
this.
|
|
182
|
-
this.usageAbortController?.abort();
|
|
183
|
-
this.usageAbortController = undefined;
|
|
184
|
-
this.queuedUsageRefresh = undefined;
|
|
263
|
+
this.deactivateGeneration(generation);
|
|
185
264
|
};
|
|
186
265
|
sessionSignal?.addEventListener("abort", this.sessionAbortHandler, { once: true });
|
|
187
|
-
void this.refresh(ctx, undefined, { force: true });
|
|
266
|
+
void this.refresh(ctx, undefined, { force: true }, generation);
|
|
188
267
|
this.usageTimer = setInterval(() => {
|
|
268
|
+
if (!this.isGenerationCurrent(generation)) return;
|
|
189
269
|
if (sessionSignal?.aborted) {
|
|
190
|
-
this.
|
|
270
|
+
this.deactivateGeneration(generation);
|
|
191
271
|
return;
|
|
192
272
|
}
|
|
193
|
-
void this.refresh(ctx);
|
|
273
|
+
void this.refresh(ctx, undefined, undefined, generation);
|
|
194
274
|
}, cfg.usage.refreshIntervalMs);
|
|
195
275
|
this.usageTimer.unref?.();
|
|
196
276
|
}
|
|
197
277
|
|
|
198
278
|
restartAfterSettingsChange(ctx: ExtensionContext, cfg: ResolvedConfig): void {
|
|
279
|
+
this.usageAbortController?.abort();
|
|
280
|
+
this.queuedUsageRefresh = undefined;
|
|
199
281
|
this.stopTimer();
|
|
282
|
+
this.sessionGeneration++;
|
|
283
|
+
this.shuttingDown = false;
|
|
200
284
|
if (cfg.usage.enabled) this.start(ctx);
|
|
201
285
|
else {
|
|
202
286
|
this.usageSnapshot = undefined;
|
|
@@ -206,6 +290,7 @@ export class UsageController {
|
|
|
206
290
|
|
|
207
291
|
shutdown(): void {
|
|
208
292
|
this.shuttingDown = true;
|
|
293
|
+
this.sessionGeneration++;
|
|
209
294
|
this.queuedUsageRefresh = undefined;
|
|
210
295
|
this.usageAbortController?.abort();
|
|
211
296
|
this.usageAbortController = undefined;
|