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/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Usage display and image generation require pi's `openai-codex` OAuth credentials
|
|
|
23
23
|
1. In pi, run `/login openai-codex`.
|
|
24
24
|
2. Verify subscription usage with `/openai-usage`, or open `/openai-settings` and check **Diagnostics**.
|
|
25
25
|
3. The extension reads auth from pi's agent auth store, normally `~/.pi/agent/auth.json`. Do not copy, paste, or commit values from this file.
|
|
26
|
-
4. If `PI_CODING_AGENT_DIR` is set, the auth store and global generated-image directory use that agent directory instead of `~/.pi/agent`.
|
|
26
|
+
4. If `PI_CODING_AGENT_DIR` is set, the auth store, global extension config, and global generated-image directory use that agent directory instead of `~/.pi/agent`. A leading `~/` is expanded to your home directory.
|
|
27
27
|
|
|
28
28
|
## Features
|
|
29
29
|
|
|
@@ -45,7 +45,7 @@ Usage display and image generation require pi's `openai-codex` OAuth credentials
|
|
|
45
45
|
The extension reads JSON config from two locations:
|
|
46
46
|
|
|
47
47
|
- Project config: `.pi/extensions/pi-better-openai.json`
|
|
48
|
-
- Global config: `~/.pi/agent/extensions/pi-better-openai.json`
|
|
48
|
+
- Global config: `$PI_CODING_AGENT_DIR/extensions/pi-better-openai.json`, defaulting to `~/.pi/agent/extensions/pi-better-openai.json`
|
|
49
49
|
|
|
50
50
|
Project overrides global. Global values fill fields omitted by the project file. Invalid enum values are ignored, and numeric settings are clamped to safe ranges.
|
|
51
51
|
|
|
@@ -105,7 +105,7 @@ Agents can call the `openai_image` tool directly. Supported parameters:
|
|
|
105
105
|
|
|
106
106
|
- `prompt` (required): pass the user's image wording verbatim.
|
|
107
107
|
- `action`: `auto`, `generate`, or `edit`.
|
|
108
|
-
- `images`: project-local reference/edit image paths. Paths must stay inside the current workspace and point to readable PNG, JPEG, WebP, or GIF files.
|
|
108
|
+
- `images`: up to five distinct project-local reference/edit image paths. Paths must stay inside the current workspace and point to readable PNG, JPEG, WebP, or GIF files; each file is limited to 20 MB and the combined input to 50 MB.
|
|
109
109
|
- `model`: Codex image model override, for example `openai-codex/gpt-5.5`.
|
|
110
110
|
- `outputFormat`: `png`, `jpeg`, or `webp`.
|
|
111
111
|
- `save`: `project`, `global`, `custom`, or `none`.
|
|
@@ -115,7 +115,7 @@ Save modes:
|
|
|
115
115
|
|
|
116
116
|
- `project` writes to `.pi/generated-images/` in the current project.
|
|
117
117
|
- `global` writes to the agent `generated-images` directory, normally `~/.pi/agent/generated-images/` or `$PI_CODING_AGENT_DIR/generated-images/`.
|
|
118
|
-
- `custom` writes to `saveDir` or `PI_IMAGE_SAVE_DIR
|
|
118
|
+
- `custom` writes to `saveDir` or `PI_IMAGE_SAVE_DIR`; relative paths are resolved from the current project.
|
|
119
119
|
- `none` returns the image without saving it.
|
|
120
120
|
|
|
121
121
|
The repository ignores `.pi/`, so generated images and local config should not be committed.
|
package/index.ts
CHANGED
|
@@ -9,10 +9,16 @@ import {
|
|
|
9
9
|
getSettingsListTheme,
|
|
10
10
|
type ExtensionAPI,
|
|
11
11
|
type ExtensionContext,
|
|
12
|
-
} from "@
|
|
13
|
-
import { Container, Key, matchesKey, SettingsList } from "@
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { Container, Key, matchesKey, SettingsList } from "@earendil-works/pi-tui";
|
|
14
14
|
import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
formatTokens,
|
|
17
|
+
redactDiagnosticValue,
|
|
18
|
+
sanitizeStatusText,
|
|
19
|
+
truncateToWidth,
|
|
20
|
+
visibleWidth,
|
|
21
|
+
} from "./src/format.ts";
|
|
16
22
|
import {
|
|
17
23
|
DEFAULT_CONFIG,
|
|
18
24
|
DEFAULT_IMAGE_CONFIG,
|
|
@@ -49,6 +55,7 @@ import {
|
|
|
49
55
|
type CodexPetPackage,
|
|
50
56
|
codexHome,
|
|
51
57
|
describeCodexPetSelectionIssue,
|
|
58
|
+
findCodexPet,
|
|
52
59
|
findReadyCodexPet,
|
|
53
60
|
formatNoReadyCodexPetsMessage,
|
|
54
61
|
listCodexPets,
|
|
@@ -91,6 +98,10 @@ function isInlinePetPlacement(placement: PetPlacement): boolean {
|
|
|
91
98
|
return placement === "inline-left" || placement === "inline-right" || placement === "badge";
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
function hasTerminalUI(ctx: ExtensionContext): boolean {
|
|
102
|
+
return ctx.mode === "tui" || (ctx.mode === undefined && ctx.hasUI);
|
|
103
|
+
}
|
|
104
|
+
|
|
94
105
|
function petSizeCellsForPlacement(placement: PetPlacement, sizeCells: number): number {
|
|
95
106
|
return placement === "badge" ? Math.min(6, sizeCells) : sizeCells;
|
|
96
107
|
}
|
|
@@ -108,19 +119,10 @@ function petSlugFromPickerValue(value: string): string {
|
|
|
108
119
|
return value === PET_EMPTY_VALUE ? "" : value;
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
function petPickerLookupKey(value: string): string {
|
|
112
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
122
|
function findPickerPet(value: string, pets: CodexPetPackage[]): CodexPetPackage | undefined {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
(pet) =>
|
|
120
|
-
pet.hasSpritesheet &&
|
|
121
|
-
(petPickerLookupKey(pet.slug) === requested ||
|
|
122
|
-
petPickerLookupKey(pet.name) === requested ||
|
|
123
|
-
(pet.id !== undefined && petPickerLookupKey(pet.id) === requested)),
|
|
123
|
+
return findCodexPet(
|
|
124
|
+
pets.filter((pet) => pet.hasSpritesheet),
|
|
125
|
+
value,
|
|
124
126
|
);
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -245,12 +247,32 @@ function combineInlinePetFooter(
|
|
|
245
247
|
return lines;
|
|
246
248
|
}
|
|
247
249
|
|
|
250
|
+
function textPanel(title: string, lines: string[], done: () => void) {
|
|
251
|
+
return {
|
|
252
|
+
render(width: number) {
|
|
253
|
+
const clipped = lines.map((line) => truncateToWidth(line, width, "..."));
|
|
254
|
+
return [title, "", ...clipped, "", "Esc/q to go back"];
|
|
255
|
+
},
|
|
256
|
+
invalidate() {},
|
|
257
|
+
handleInput(data: string) {
|
|
258
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c")) || data === "q") done();
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
248
263
|
export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
249
264
|
const fastController = new FastController(SERVICE_TIER);
|
|
250
265
|
let cachedConfig: ResolvedConfig | undefined;
|
|
251
266
|
let footerTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
252
267
|
let footerInstalled = false;
|
|
253
268
|
let statusInstalled = false;
|
|
269
|
+
let contextUsageCached = false;
|
|
270
|
+
let cachedContextUsage: ReturnType<ExtensionContext["getContextUsage"]>;
|
|
271
|
+
let cachedContextLeafId: string | null | undefined;
|
|
272
|
+
let cachedContextModel: ExtensionContext["model"];
|
|
273
|
+
let sessionNameCached = false;
|
|
274
|
+
let cachedSessionNameLeafId: string | null | undefined;
|
|
275
|
+
let cachedSessionName: string | undefined;
|
|
254
276
|
const usageController = new UsageController(config, updateFooter);
|
|
255
277
|
const petController = new PetFooterController(config, updateFooter, () => footerInstalled);
|
|
256
278
|
|
|
@@ -310,6 +332,41 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
310
332
|
}
|
|
311
333
|
}
|
|
312
334
|
|
|
335
|
+
function invalidateContextUsage(): void {
|
|
336
|
+
contextUsageCached = false;
|
|
337
|
+
cachedContextUsage = undefined;
|
|
338
|
+
cachedContextLeafId = undefined;
|
|
339
|
+
cachedContextModel = undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function contextUsage(ctx: ExtensionContext): ReturnType<ExtensionContext["getContextUsage"]> {
|
|
343
|
+
const leafId = ctx.sessionManager.getLeafId();
|
|
344
|
+
const model = ctx.model;
|
|
345
|
+
if (!contextUsageCached || leafId !== cachedContextLeafId || model !== cachedContextModel) {
|
|
346
|
+
cachedContextUsage = ctx.getContextUsage();
|
|
347
|
+
contextUsageCached = true;
|
|
348
|
+
cachedContextLeafId = leafId;
|
|
349
|
+
cachedContextModel = model;
|
|
350
|
+
}
|
|
351
|
+
return cachedContextUsage;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function sessionName(ctx: ExtensionContext): string | undefined {
|
|
355
|
+
const leafId = ctx.sessionManager.getLeafId();
|
|
356
|
+
if (!sessionNameCached || leafId !== cachedSessionNameLeafId) {
|
|
357
|
+
cachedSessionName = ctx.sessionManager.getSessionName();
|
|
358
|
+
cachedSessionNameLeafId = leafId;
|
|
359
|
+
sessionNameCached = true;
|
|
360
|
+
}
|
|
361
|
+
return cachedSessionName;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function invalidateSessionName(): void {
|
|
365
|
+
sessionNameCached = false;
|
|
366
|
+
cachedSessionNameLeafId = undefined;
|
|
367
|
+
cachedSessionName = undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
313
370
|
pi.registerFlag(FLAG, {
|
|
314
371
|
description: "Start with OpenAI fast mode enabled (service_tier=priority)",
|
|
315
372
|
type: "boolean",
|
|
@@ -353,19 +410,6 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
353
410
|
},
|
|
354
411
|
});
|
|
355
412
|
|
|
356
|
-
function textPanel(title: string, lines: string[], done: () => void) {
|
|
357
|
-
return {
|
|
358
|
-
render(width: number) {
|
|
359
|
-
const clipped = lines.map((line) => truncateToWidth(line, width, "..."));
|
|
360
|
-
return [title, "", ...clipped, "", "Esc/q to go back"];
|
|
361
|
-
},
|
|
362
|
-
invalidate() {},
|
|
363
|
-
handleInput(data: string) {
|
|
364
|
-
if (data.includes("\x1b") || data === "escape" || data === "q" || data === "\x03") done();
|
|
365
|
-
},
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
413
|
function buildPetSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
|
|
370
414
|
return settingsItemsFromDescriptors(PET_SETTING_DESCRIPTORS, cfg, {
|
|
371
415
|
"pets.slug": {
|
|
@@ -608,10 +652,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
608
652
|
});
|
|
609
653
|
}
|
|
610
654
|
|
|
611
|
-
function buildFastSettingsItems(
|
|
612
|
-
ctx: ExtensionContext,
|
|
613
|
-
cfg: ResolvedConfig,
|
|
614
|
-
): SettingsPickerItem[] {
|
|
655
|
+
function buildFastSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
|
|
615
656
|
return [
|
|
616
657
|
{
|
|
617
658
|
id: "fast.enabled",
|
|
@@ -659,11 +700,13 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
659
700
|
id: "config.print",
|
|
660
701
|
label: "Print config",
|
|
661
702
|
currentValue: "open",
|
|
662
|
-
description: "Show the selected
|
|
703
|
+
description: "Show the selected config JSON with sensitive fields redacted.",
|
|
663
704
|
submenu: (_value, done) =>
|
|
664
705
|
textPanel(
|
|
665
706
|
"Config",
|
|
666
|
-
JSON.stringify(readRawConfig(cfg.configPath), null, 2).split(
|
|
707
|
+
JSON.stringify(redactDiagnosticValue(readRawConfig(cfg.configPath)), null, 2).split(
|
|
708
|
+
"\n",
|
|
709
|
+
),
|
|
667
710
|
() => done(),
|
|
668
711
|
),
|
|
669
712
|
},
|
|
@@ -680,7 +723,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
680
723
|
submenu: (_value, done) =>
|
|
681
724
|
settingsSubmenu(
|
|
682
725
|
"Fast mode settings",
|
|
683
|
-
() => buildFastSettingsItems(
|
|
726
|
+
() => buildFastSettingsItems(config(ctx)),
|
|
684
727
|
ctx,
|
|
685
728
|
() => done(fastSettingsSummary(ctx, config(ctx))),
|
|
686
729
|
),
|
|
@@ -803,6 +846,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
803
846
|
}
|
|
804
847
|
|
|
805
848
|
async function showSettingsPicker(ctx: ExtensionContext): Promise<void> {
|
|
849
|
+
if (!hasTerminalUI(ctx)) {
|
|
850
|
+
ctx.ui.notify("Better OpenAI settings require interactive TUI mode.", "warning");
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
806
853
|
try {
|
|
807
854
|
petController.settingsPets = await listCodexPets();
|
|
808
855
|
} catch {
|
|
@@ -887,7 +934,12 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
887
934
|
slug: selectedPet.slug,
|
|
888
935
|
});
|
|
889
936
|
updateFooter(ctx);
|
|
890
|
-
await petController.refresh(ctx, next, true);
|
|
937
|
+
if (hasTerminalUI(ctx)) await petController.refresh(ctx, next, true);
|
|
938
|
+
else
|
|
939
|
+
ctx.ui.notify(
|
|
940
|
+
`Enabled ${selectedPet.name} (${selectedPet.slug}); footer pets render in interactive TUI mode.`,
|
|
941
|
+
"info",
|
|
942
|
+
);
|
|
891
943
|
},
|
|
892
944
|
tuck: (ctx) => {
|
|
893
945
|
writePetConfig(ctx, { enabled: false });
|
|
@@ -912,8 +964,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
912
964
|
|
|
913
965
|
const next = writePetConfig(ctx, { slug: selectedPet.slug });
|
|
914
966
|
updateFooter(ctx);
|
|
915
|
-
if (petController.shouldLoadForConfig(next))
|
|
916
|
-
|
|
967
|
+
if (petController.shouldLoadForConfig(next)) {
|
|
968
|
+
if (hasTerminalUI(ctx)) await petController.refresh(ctx, next, true);
|
|
969
|
+
else
|
|
970
|
+
ctx.ui.notify(
|
|
971
|
+
`Selected ${selectedPet.name} (${selectedPet.slug}); footer pets render in interactive TUI mode.`,
|
|
972
|
+
"info",
|
|
973
|
+
);
|
|
974
|
+
} else {
|
|
917
975
|
ctx.ui.notify(
|
|
918
976
|
`Selected ${selectedPet.name} (${selectedPet.slug}) for the footer pet. Use /pets wake to show it.`,
|
|
919
977
|
"info",
|
|
@@ -966,8 +1024,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
966
1024
|
const branch = footerData.getGitBranch?.();
|
|
967
1025
|
if (branch) pwd = `${pwd} (${branch})`;
|
|
968
1026
|
|
|
969
|
-
const
|
|
970
|
-
if (
|
|
1027
|
+
const currentSessionName = sessionName(ctx);
|
|
1028
|
+
if (currentSessionName) pwd = `${pwd} • ${currentSessionName}`;
|
|
971
1029
|
|
|
972
1030
|
const parts: string[] = [];
|
|
973
1031
|
if (totalInput) parts.push(`↑${formatTokens(totalInput)}`);
|
|
@@ -979,11 +1037,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
979
1037
|
if (totalCost || usingSubscription)
|
|
980
1038
|
parts.push(`$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`);
|
|
981
1039
|
|
|
982
|
-
const
|
|
983
|
-
const contextWindow =
|
|
984
|
-
const contextPercentValue =
|
|
1040
|
+
const currentContextUsage = contextUsage(ctx);
|
|
1041
|
+
const contextWindow = currentContextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
1042
|
+
const contextPercentValue = currentContextUsage?.percent ?? 0;
|
|
985
1043
|
const contextPercent =
|
|
986
|
-
|
|
1044
|
+
currentContextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
|
987
1045
|
const contextDisplay =
|
|
988
1046
|
contextPercent === "?"
|
|
989
1047
|
? `?/${formatTokens(contextWindow)} (auto)`
|
|
@@ -997,12 +1055,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
997
1055
|
parts.push(contextText);
|
|
998
1056
|
|
|
999
1057
|
const cfg = config(ctx);
|
|
1000
|
-
const
|
|
1001
|
-
const
|
|
1002
|
-
const requestedPetPlacement = cfgForPets.pets.placement;
|
|
1058
|
+
const shouldRenderPet = petController.shouldRenderInFooter(cfg);
|
|
1059
|
+
const requestedPetPlacement = cfg.pets.placement;
|
|
1003
1060
|
const requestedPetSizeCells = petSizeCellsForPlacement(
|
|
1004
1061
|
requestedPetPlacement,
|
|
1005
|
-
|
|
1062
|
+
cfg.pets.sizeCells,
|
|
1006
1063
|
);
|
|
1007
1064
|
const inlinePet = Boolean(
|
|
1008
1065
|
shouldRenderPet &&
|
|
@@ -1010,11 +1067,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1010
1067
|
isInlinePetPlacement(requestedPetPlacement) &&
|
|
1011
1068
|
width >= requestedPetSizeCells + 32,
|
|
1012
1069
|
);
|
|
1013
|
-
const petRenderSizeCells = inlinePet ? requestedPetSizeCells :
|
|
1070
|
+
const petRenderSizeCells = inlinePet ? requestedPetSizeCells : cfg.pets.sizeCells;
|
|
1014
1071
|
const petColumnWidth = Math.min(petRenderSizeCells, Math.max(1, width - 1));
|
|
1015
1072
|
const footerTextWidth = inlinePet ? Math.max(1, width - petColumnWidth - 2) : width;
|
|
1016
1073
|
|
|
1017
|
-
const usageStatusLine = usageController.statusLine(ctx, cfg);
|
|
1074
|
+
const usageStatusLine = usageController.statusLine(ctx, cfg, usingSubscription);
|
|
1018
1075
|
const usageLine = usageStatusLine ? theme.fg("dim", usageStatusLine) : undefined;
|
|
1019
1076
|
|
|
1020
1077
|
let statsLeft = parts.join(" ");
|
|
@@ -1027,7 +1084,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1027
1084
|
const modelName = ctx.model?.id || "no-model";
|
|
1028
1085
|
const thinkingLevel = pi.getThinkingLevel();
|
|
1029
1086
|
const fastSuffix =
|
|
1030
|
-
fastController.active && supportsFast(ctx,
|
|
1087
|
+
fastController.active && supportsFast(ctx, cfg.supportedModels) ? " fast" : "";
|
|
1031
1088
|
let rightWithoutProvider = modelName;
|
|
1032
1089
|
if (ctx.model?.reasoning) {
|
|
1033
1090
|
rightWithoutProvider =
|
|
@@ -1084,7 +1141,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1084
1141
|
textLines.push(truncateToWidth(statusLine, footerTextWidth, theme.fg("dim", "...")));
|
|
1085
1142
|
}
|
|
1086
1143
|
|
|
1087
|
-
const petLines = petController.renderPetLines(ctx,
|
|
1144
|
+
const petLines = petController.renderPetLines(ctx, cfg, {
|
|
1088
1145
|
shouldRenderPet,
|
|
1089
1146
|
freezePetFrame,
|
|
1090
1147
|
requestedPetPlacement,
|
|
@@ -1140,6 +1197,18 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1140
1197
|
|
|
1141
1198
|
function updateFooter(ctx: ExtensionContext): void {
|
|
1142
1199
|
const cfg = config(ctx);
|
|
1200
|
+
|
|
1201
|
+
if (!hasTerminalUI(ctx)) {
|
|
1202
|
+
if (cfg.footer.mode === "off") {
|
|
1203
|
+
setStatus(ctx, undefined);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const fast = fastController.statusSegment(ctx, cfg);
|
|
1207
|
+
const usage = usageController.statusLine(ctx, cfg);
|
|
1208
|
+
setStatus(ctx, [fast, usage].filter(Boolean).join(" | ") || undefined);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1143
1212
|
petController.updateActivity(ctx, cfg);
|
|
1144
1213
|
const shouldRenderPet = petController.shouldRenderInFooter(cfg);
|
|
1145
1214
|
|
|
@@ -1162,6 +1231,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1162
1231
|
}
|
|
1163
1232
|
|
|
1164
1233
|
pi.on("session_start", (_event, ctx) => {
|
|
1234
|
+
invalidateContextUsage();
|
|
1235
|
+
invalidateSessionName();
|
|
1165
1236
|
const nextConfig = refresh(ctx);
|
|
1166
1237
|
fastController.initializeForSession(ctx, nextConfig, pi.getFlag(FLAG) === true);
|
|
1167
1238
|
if (
|
|
@@ -1172,15 +1243,16 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1172
1243
|
if (fastController.desiredActive && !fastController.active) {
|
|
1173
1244
|
ctx.ui.notify(fastController.unsupportedRequestMessage(ctx, nextConfig), "warning");
|
|
1174
1245
|
}
|
|
1175
|
-
petController.installResizeGuard(ctx);
|
|
1246
|
+
if (hasTerminalUI(ctx)) petController.installResizeGuard(ctx);
|
|
1176
1247
|
refreshFooterTotals(ctx);
|
|
1177
1248
|
updateFooter(ctx);
|
|
1178
|
-
if (nextConfig.pets.enabled) void petController.refresh(ctx, nextConfig);
|
|
1249
|
+
if (hasTerminalUI(ctx) && nextConfig.pets.enabled) void petController.refresh(ctx, nextConfig);
|
|
1179
1250
|
usageController.start(ctx);
|
|
1180
1251
|
if (fastController.active) ctx.ui.notify(fastController.stateText(ctx, nextConfig), "info");
|
|
1181
1252
|
});
|
|
1182
1253
|
|
|
1183
1254
|
pi.on("agent_start", (_event, ctx) => {
|
|
1255
|
+
invalidateContextUsage();
|
|
1184
1256
|
petController.agentStart(ctx);
|
|
1185
1257
|
updateFooter(ctx);
|
|
1186
1258
|
});
|
|
@@ -1200,13 +1272,21 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1200
1272
|
updateFooter(ctx);
|
|
1201
1273
|
});
|
|
1202
1274
|
|
|
1203
|
-
pi.on("turn_end", (
|
|
1204
|
-
|
|
1275
|
+
pi.on("turn_end", (event, ctx) => {
|
|
1276
|
+
invalidateContextUsage();
|
|
1277
|
+
if (event.message?.role === "assistant") {
|
|
1278
|
+
footerTotals.input += event.message.usage.input;
|
|
1279
|
+
footerTotals.output += event.message.usage.output;
|
|
1280
|
+
footerTotals.cacheRead += event.message.usage.cacheRead;
|
|
1281
|
+
footerTotals.cacheWrite += event.message.usage.cacheWrite;
|
|
1282
|
+
footerTotals.cost += event.message.usage.cost.total;
|
|
1283
|
+
} else refreshFooterTotals(ctx);
|
|
1205
1284
|
updateFooter(ctx);
|
|
1206
1285
|
void usageController.refresh(ctx);
|
|
1207
1286
|
});
|
|
1208
1287
|
|
|
1209
1288
|
pi.on("session_compact", (_event, ctx) => {
|
|
1289
|
+
invalidateContextUsage();
|
|
1210
1290
|
refreshFooterTotals(ctx);
|
|
1211
1291
|
petController.queueKittyCleanup();
|
|
1212
1292
|
petController.resetRenderCache();
|
|
@@ -1214,6 +1294,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1214
1294
|
});
|
|
1215
1295
|
|
|
1216
1296
|
pi.on("session_tree", (_event, ctx) => {
|
|
1297
|
+
invalidateContextUsage();
|
|
1217
1298
|
refreshFooterTotals(ctx);
|
|
1218
1299
|
petController.queueKittyCleanup();
|
|
1219
1300
|
petController.resetRenderCache();
|
|
@@ -1221,6 +1302,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1221
1302
|
});
|
|
1222
1303
|
|
|
1223
1304
|
pi.on("model_select", (event, ctx) => {
|
|
1305
|
+
invalidateContextUsage();
|
|
1224
1306
|
const cfg = config(ctx);
|
|
1225
1307
|
const wasActive = fastController.active;
|
|
1226
1308
|
fastController.applyDesiredState(ctx, cfg);
|
|
@@ -1238,6 +1320,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1238
1320
|
});
|
|
1239
1321
|
|
|
1240
1322
|
pi.on("session_shutdown", () => {
|
|
1323
|
+
invalidateContextUsage();
|
|
1324
|
+
invalidateSessionName();
|
|
1241
1325
|
usageController.shutdown();
|
|
1242
1326
|
petController.shutdown();
|
|
1243
1327
|
});
|
|
@@ -1245,6 +1329,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1245
1329
|
pi.on("before_provider_request", (event, ctx) => {
|
|
1246
1330
|
return fastController.injectProviderPayload(event, ctx, config(ctx));
|
|
1247
1331
|
});
|
|
1332
|
+
|
|
1333
|
+
pi.on("message_start", invalidateContextUsage);
|
|
1334
|
+
pi.on("message_update", invalidateContextUsage);
|
|
1335
|
+
pi.on("message_end", invalidateContextUsage);
|
|
1248
1336
|
}
|
|
1249
1337
|
|
|
1250
1338
|
export const _test = {
|
|
@@ -1273,6 +1361,7 @@ export const _test = {
|
|
|
1273
1361
|
formatPercent,
|
|
1274
1362
|
formatUsageSnapshot,
|
|
1275
1363
|
readCodexAuth,
|
|
1364
|
+
textPanel,
|
|
1276
1365
|
imageTest: _imageTest,
|
|
1277
1366
|
petsTest: _petsTest,
|
|
1278
1367
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-openai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Personal pi extension that improves OpenAI with fast mode, usage stats, and footer polish.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fast",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"sharp": "^0.34.5"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
+
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
45
|
+
"@earendil-works/pi-tui": "0.80.6",
|
|
44
46
|
"@types/node": "^24.13.2",
|
|
45
47
|
"oxfmt": "^0.47.0",
|
|
46
48
|
"oxlint": "^1.62.0",
|
|
@@ -48,11 +50,11 @@
|
|
|
48
50
|
"vitest": "^4.1.5"
|
|
49
51
|
},
|
|
50
52
|
"peerDependencies": {
|
|
51
|
-
"@
|
|
52
|
-
"@
|
|
53
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
54
|
+
"@earendil-works/pi-tui": "*"
|
|
53
55
|
},
|
|
54
56
|
"engines": {
|
|
55
|
-
"node": ">=
|
|
57
|
+
"node": ">=22.19.0"
|
|
56
58
|
},
|
|
57
59
|
"pi": {
|
|
58
60
|
"extensions": [
|
package/src/codex-auth.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { join } from "node:path";
|
|
4
|
-
import type { ExtensionContext } from "@
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { piAgentDir } from "./paths.ts";
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
export const AUTH_FILE = join(AGENT_DIR, "auth.json");
|
|
6
|
+
export const AUTH_FILE = join(piAgentDir(), "auth.json");
|
|
8
7
|
|
|
9
8
|
export type CodexCredentials = {
|
|
10
9
|
accessToken: string;
|
|
@@ -15,6 +14,30 @@ export type CodexCredentialsWithSource = CodexCredentials & {
|
|
|
15
14
|
source: "modelRegistry" | "authFile";
|
|
16
15
|
};
|
|
17
16
|
|
|
17
|
+
function waitForSignal<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
18
|
+
if (!signal) return operation;
|
|
19
|
+
if (signal.aborted) return Promise.reject(signal.reason ?? new Error("Operation was aborted."));
|
|
20
|
+
|
|
21
|
+
return new Promise<T>((resolve, reject) => {
|
|
22
|
+
const onAbort = () => {
|
|
23
|
+
cleanup();
|
|
24
|
+
reject(signal.reason ?? new Error("Operation was aborted."));
|
|
25
|
+
};
|
|
26
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
27
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
28
|
+
void operation.then(
|
|
29
|
+
(value) => {
|
|
30
|
+
cleanup();
|
|
31
|
+
resolve(value);
|
|
32
|
+
},
|
|
33
|
+
(error: unknown) => {
|
|
34
|
+
cleanup();
|
|
35
|
+
reject(error);
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
function decodeBase64Url(value: string): string {
|
|
19
42
|
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
20
43
|
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
@@ -79,11 +102,13 @@ export function readCodexAuth(): CodexCredentials | undefined {
|
|
|
79
102
|
access?: string | null;
|
|
80
103
|
accountId?: string | null;
|
|
81
104
|
account_id?: string | null;
|
|
105
|
+
expires?: number | null;
|
|
82
106
|
}
|
|
83
107
|
| undefined
|
|
84
108
|
>;
|
|
85
109
|
const entry = auth["openai-codex"];
|
|
86
110
|
if (entry?.type !== "oauth") return undefined;
|
|
111
|
+
if (typeof entry.expires === "number" && Date.now() >= entry.expires) return undefined;
|
|
87
112
|
const accessToken = entry.access?.trim();
|
|
88
113
|
const accountId = (entry.accountId ?? entry.account_id)?.trim();
|
|
89
114
|
return accessToken && accountId ? { accessToken, accountId } : undefined;
|
|
@@ -94,10 +119,16 @@ export function readCodexAuth(): CodexCredentials | undefined {
|
|
|
94
119
|
|
|
95
120
|
export async function getCodexCredentials(
|
|
96
121
|
ctx?: Pick<ExtensionContext, "modelRegistry">,
|
|
122
|
+
signal?: AbortSignal,
|
|
97
123
|
): Promise<CodexCredentialsWithSource | undefined> {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
if (signal?.aborted) throw signal.reason ?? new Error("Operation was aborted.");
|
|
125
|
+
const registryRequest = ctx?.modelRegistry?.getApiKeyForProvider("openai-codex");
|
|
126
|
+
const registryToken = registryRequest
|
|
127
|
+
? await waitForSignal(
|
|
128
|
+
registryRequest.catch(() => undefined),
|
|
129
|
+
signal,
|
|
130
|
+
)
|
|
131
|
+
: undefined;
|
|
101
132
|
const registryCredentials = parseCodexRegistryCredentials(registryToken);
|
|
102
133
|
if (registryCredentials) return { ...registryCredentials, source: "modelRegistry" };
|
|
103
134
|
const auth = readCodexAuth();
|
package/src/config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { CONFIG_BASENAME, logPrefix } from "./identity.ts";
|
|
5
|
+
import { piAgentDir } from "./paths.ts";
|
|
5
6
|
|
|
6
7
|
export const FOOTER_MODES = ["replace", "status", "off"] as const;
|
|
7
8
|
export const IMAGE_SAVE_MODES = ["none", "project", "global", "custom"] as const;
|
|
@@ -409,10 +410,10 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
409
410
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
410
411
|
}
|
|
411
412
|
|
|
412
|
-
export function configPaths(cwd: string, home = homedir()) {
|
|
413
|
+
export function configPaths(cwd: string, home = homedir(), env = process.env) {
|
|
413
414
|
return {
|
|
414
415
|
project: join(cwd, ".pi", "extensions", CONFIG_BASENAME),
|
|
415
|
-
global: join(
|
|
416
|
+
global: join(piAgentDir(env, home), "extensions", CONFIG_BASENAME),
|
|
416
417
|
};
|
|
417
418
|
}
|
|
418
419
|
|
package/src/fast-controller.ts
CHANGED