pi-better-openai 0.1.19 → 0.1.22
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 +16 -6
- package/index.ts +150 -152
- package/package.json +7 -5
- package/src/codex-auth.ts +38 -7
- package/src/config.ts +6 -2
- package/src/fast-controller.ts +5 -2
- package/src/footer-layout.ts +97 -0
- package/src/format.ts +45 -20
- package/src/image.ts +121 -128
- package/src/paths.ts +17 -0
- package/src/pet-footer-controller.ts +56 -25
- package/src/pets.ts +110 -49
- package/src/usage-controller.ts +141 -51
- package/src/usage.ts +91 -23
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ A pi extension for OpenAI subscription workflows: fast mode, usage visibility, f
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
+
Requires Node.js 22.19.0 or newer.
|
|
8
|
+
|
|
7
9
|
Install from GitHub:
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -23,7 +25,7 @@ Usage display and image generation require pi's `openai-codex` OAuth credentials
|
|
|
23
25
|
1. In pi, run `/login openai-codex`.
|
|
24
26
|
2. Verify subscription usage with `/openai-usage`, or open `/openai-settings` and check **Diagnostics**.
|
|
25
27
|
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`.
|
|
28
|
+
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
29
|
|
|
28
30
|
## Features
|
|
29
31
|
|
|
@@ -45,14 +47,22 @@ Usage display and image generation require pi's `openai-codex` OAuth credentials
|
|
|
45
47
|
The extension reads JSON config from two locations:
|
|
46
48
|
|
|
47
49
|
- Project config: `.pi/extensions/pi-better-openai.json`
|
|
48
|
-
- Global config: `~/.pi/agent/extensions/pi-better-openai.json`
|
|
50
|
+
- Global config: `$PI_CODING_AGENT_DIR/extensions/pi-better-openai.json`, defaulting to `~/.pi/agent/extensions/pi-better-openai.json`
|
|
49
51
|
|
|
50
52
|
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
53
|
|
|
52
54
|
Default supported models:
|
|
53
55
|
|
|
54
56
|
```json
|
|
55
|
-
[
|
|
57
|
+
[
|
|
58
|
+
"openai/gpt-5.4",
|
|
59
|
+
"openai/gpt-5.5",
|
|
60
|
+
"openai-codex/gpt-5.6-sol",
|
|
61
|
+
"openai-codex/gpt-5.6-terra",
|
|
62
|
+
"openai-codex/gpt-5.6-luna",
|
|
63
|
+
"openai-codex/gpt-5.4",
|
|
64
|
+
"openai-codex/gpt-5.5"
|
|
65
|
+
]
|
|
56
66
|
```
|
|
57
67
|
|
|
58
68
|
Example config:
|
|
@@ -105,7 +115,7 @@ Agents can call the `openai_image` tool directly. Supported parameters:
|
|
|
105
115
|
|
|
106
116
|
- `prompt` (required): pass the user's image wording verbatim.
|
|
107
117
|
- `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.
|
|
118
|
+
- `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
119
|
- `model`: Codex image model override, for example `openai-codex/gpt-5.5`.
|
|
110
120
|
- `outputFormat`: `png`, `jpeg`, or `webp`.
|
|
111
121
|
- `save`: `project`, `global`, `custom`, or `none`.
|
|
@@ -115,7 +125,7 @@ Save modes:
|
|
|
115
125
|
|
|
116
126
|
- `project` writes to `.pi/generated-images/` in the current project.
|
|
117
127
|
- `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
|
|
128
|
+
- `custom` writes to `saveDir` or `PI_IMAGE_SAVE_DIR`; relative paths are resolved from the current project.
|
|
119
129
|
- `none` returns the image without saving it.
|
|
120
130
|
|
|
121
131
|
The repository ignores `.pi/`, so generated images and local config should not be committed.
|
|
@@ -146,7 +156,7 @@ Then reload Codex skills (`Cmd/Ctrl+K → Force Reload Skills`) and ask:
|
|
|
146
156
|
$hatch-pet create a new pet inspired by pi-better-openai
|
|
147
157
|
```
|
|
148
158
|
|
|
149
|
-
Custom pets should end up in `${CODEX_HOME:-~/.codex}/pets/<pet-name>/` with `pet.json` and `spritesheet.webp`. Refresh custom pets in Codex settings and toggle the overlay with `/pet`.
|
|
159
|
+
Custom pets should end up in `${CODEX_HOME:-~/.codex}/pets/<pet-name>/` with `pet.json` and `spritesheet.webp`. The spritesheet must be a 1536×1872 atlas arranged as 8 columns by 9 animation rows. Animated footer rendering also requires a terminal image protocol supported by pi. Refresh custom pets in Codex settings and toggle the overlay with `/pet`.
|
|
150
160
|
|
|
151
161
|
## Screenshots
|
|
152
162
|
|
package/index.ts
CHANGED
|
@@ -4,15 +4,20 @@
|
|
|
4
4
|
* Adds `service_tier: "priority"` to OpenAI provider payloads while fast mode is
|
|
5
5
|
* enabled and the selected model is in the configured allow-list.
|
|
6
6
|
*/
|
|
7
|
-
import { sep } from "node:path";
|
|
8
7
|
import {
|
|
9
8
|
getSettingsListTheme,
|
|
10
9
|
type ExtensionAPI,
|
|
11
10
|
type ExtensionContext,
|
|
12
|
-
} from "@
|
|
13
|
-
import { Container, Key, matchesKey, SettingsList } from "@
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { Container, Key, matchesKey, SettingsList } from "@earendil-works/pi-tui";
|
|
14
13
|
import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
|
|
15
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
formatTokens,
|
|
16
|
+
redactDiagnosticValue,
|
|
17
|
+
sanitizeStatusText,
|
|
18
|
+
truncateToWidth,
|
|
19
|
+
visibleWidth,
|
|
20
|
+
} from "./src/format.ts";
|
|
16
21
|
import {
|
|
17
22
|
DEFAULT_CONFIG,
|
|
18
23
|
DEFAULT_IMAGE_CONFIG,
|
|
@@ -27,7 +32,6 @@ import {
|
|
|
27
32
|
type SettingsOptionDescriptor,
|
|
28
33
|
configPaths,
|
|
29
34
|
type ResolvedConfig,
|
|
30
|
-
type PetPlacement,
|
|
31
35
|
type PetState,
|
|
32
36
|
isRecord,
|
|
33
37
|
parseModelKey,
|
|
@@ -49,6 +53,7 @@ import {
|
|
|
49
53
|
type CodexPetPackage,
|
|
50
54
|
codexHome,
|
|
51
55
|
describeCodexPetSelectionIssue,
|
|
56
|
+
findCodexPet,
|
|
52
57
|
findReadyCodexPet,
|
|
53
58
|
formatNoReadyCodexPetsMessage,
|
|
54
59
|
listCodexPets,
|
|
@@ -58,6 +63,13 @@ import {
|
|
|
58
63
|
import { FastController, modelList, supportsFast } from "./src/fast-controller.ts";
|
|
59
64
|
import { UsageController } from "./src/usage-controller.ts";
|
|
60
65
|
import { PetFooterController } from "./src/pet-footer-controller.ts";
|
|
66
|
+
import {
|
|
67
|
+
abbreviateHomePath,
|
|
68
|
+
combineInlinePetFooter,
|
|
69
|
+
isInlinePetPlacement,
|
|
70
|
+
isTerminalImageLine,
|
|
71
|
+
petSizeCellsForPlacement,
|
|
72
|
+
} from "./src/footer-layout.ts";
|
|
61
73
|
|
|
62
74
|
const COMMAND = "fast";
|
|
63
75
|
const OPENAI_STATUS_COMMAND = "openai-usage";
|
|
@@ -77,22 +89,8 @@ type SettingsPickerItem = {
|
|
|
77
89
|
) => { render(width: number): string[]; invalidate(): void; handleInput?(data: string): void };
|
|
78
90
|
};
|
|
79
91
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
home = process.env.HOME || process.env.USERPROFILE,
|
|
83
|
-
): string {
|
|
84
|
-
if (!home) return path;
|
|
85
|
-
if (path === home) return "~";
|
|
86
|
-
const homePrefix = home.endsWith(sep) ? home : `${home}${sep}`;
|
|
87
|
-
return path.startsWith(homePrefix) ? `~/${path.slice(homePrefix.length)}` : path;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function isInlinePetPlacement(placement: PetPlacement): boolean {
|
|
91
|
-
return placement === "inline-left" || placement === "inline-right" || placement === "badge";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function petSizeCellsForPlacement(placement: PetPlacement, sizeCells: number): number {
|
|
95
|
-
return placement === "badge" ? Math.min(6, sizeCells) : sizeCells;
|
|
92
|
+
function hasTerminalUI(ctx: ExtensionContext): boolean {
|
|
93
|
+
return ctx.mode === "tui" || (ctx.mode === undefined && ctx.hasUI);
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
function readyPetPickerValues(pets: CodexPetPackage[]): string[] | undefined {
|
|
@@ -108,19 +106,10 @@ function petSlugFromPickerValue(value: string): string {
|
|
|
108
106
|
return value === PET_EMPTY_VALUE ? "" : value;
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
function petPickerLookupKey(value: string): string {
|
|
112
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
109
|
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)),
|
|
110
|
+
return findCodexPet(
|
|
111
|
+
pets.filter((pet) => pet.hasSpritesheet),
|
|
112
|
+
value,
|
|
124
113
|
);
|
|
125
114
|
}
|
|
126
115
|
|
|
@@ -129,8 +118,8 @@ function petPickerDescription(cfg: ResolvedConfig, pets: CodexPetPackage[]): str
|
|
|
129
118
|
if (readyPets.length === 0)
|
|
130
119
|
return "No ready custom pets found. Use /pets help to create one, then return here.";
|
|
131
120
|
if (!cfg.pets.slug) {
|
|
132
|
-
const firstPet = readyPets[0]
|
|
133
|
-
const lastPet = readyPets[readyPets.length - 1]
|
|
121
|
+
const firstPet = readyPets[0]!;
|
|
122
|
+
const lastPet = readyPets[readyPets.length - 1]!;
|
|
134
123
|
return `No pet selected. Enter/Space/→ selects ${firstPet.name}; ← selects ${lastPet.name}.`;
|
|
135
124
|
}
|
|
136
125
|
const selectedPet = findPickerPet(cfg.pets.slug, readyPets);
|
|
@@ -167,82 +156,17 @@ function formatPetSelectPrompt(
|
|
|
167
156
|
return { message: lines.join("\n"), level: "info" };
|
|
168
157
|
}
|
|
169
158
|
|
|
170
|
-
function
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function petLineCell(line: string, width: number): string {
|
|
183
|
-
if (!line) return spaces(width);
|
|
184
|
-
if (isTerminalImageLine(line)) return `\x1b[0m${line}`;
|
|
185
|
-
const clipped = truncateToWidth(line, width, "");
|
|
186
|
-
return clipped + spaces(width - visibleWidth(clipped));
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function stripLeadingCursorUp(line: string): string {
|
|
190
|
-
if (!line.startsWith("\x1b[")) return line;
|
|
191
|
-
const end = line.indexOf("A", 2);
|
|
192
|
-
if (end === -1) return line;
|
|
193
|
-
return /^\d+$/.test(line.slice(2, end)) ? line.slice(end + 1) : line;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function stripTrailingCursorDown(line: string): string {
|
|
197
|
-
if (!line.endsWith("B")) return line;
|
|
198
|
-
const start = line.lastIndexOf("\x1b[");
|
|
199
|
-
if (start === -1) return line;
|
|
200
|
-
return /^\d+$/.test(line.slice(start + 2, -1)) ? line.slice(0, start) : line;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function terminalImageInlineLeftSequence(line: string, totalRows: number): string {
|
|
204
|
-
const moveUp = totalRows > 1 ? `\x1b[${totalRows - 1}A` : "";
|
|
205
|
-
const moveDown = totalRows > 1 ? `\x1b[${totalRows - 1}B` : "";
|
|
206
|
-
const balancedLine = stripTrailingCursorDown(stripLeadingCursorUp(line));
|
|
207
|
-
return `\x1b[0m\r${moveUp}${balancedLine}${moveDown}`;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function combineInlinePetFooter(
|
|
211
|
-
petLines: string[],
|
|
212
|
-
textLines: string[],
|
|
213
|
-
width: number,
|
|
214
|
-
placement: PetPlacement,
|
|
215
|
-
petWidth: number,
|
|
216
|
-
): string[] {
|
|
217
|
-
const gap = 2;
|
|
218
|
-
const textWidth = Math.max(1, width - petWidth - gap);
|
|
219
|
-
const totalRows = Math.max(petLines.length, textLines.length);
|
|
220
|
-
const petStart = 0;
|
|
221
|
-
const textStart = 0;
|
|
222
|
-
const hasTerminalImageLine = petLines.some(isTerminalImageLine);
|
|
223
|
-
const leftImageLine =
|
|
224
|
-
placement === "inline-left" ? petLines.find(isTerminalImageLine) : undefined;
|
|
225
|
-
const renderPetOnRight =
|
|
226
|
-
placement === "inline-right" || (placement !== "inline-left" && hasTerminalImageLine);
|
|
227
|
-
const lines: string[] = [];
|
|
228
|
-
|
|
229
|
-
for (let row = 0; row < totalRows; row++) {
|
|
230
|
-
const petLine = row >= petStart ? (petLines[row - petStart] ?? "") : "";
|
|
231
|
-
const textLine = row >= textStart ? (textLines[row - textStart] ?? "") : "";
|
|
232
|
-
const textPart = truncateToWidth(textLine, textWidth, "...");
|
|
233
|
-
const petPart = leftImageLine ? spaces(petWidth) : petLineCell(petLine, petWidth);
|
|
234
|
-
if (renderPetOnRight) {
|
|
235
|
-
lines.push(`${padTextToWidth(textPart, textWidth)}${spaces(gap)}${petPart}`);
|
|
236
|
-
} else {
|
|
237
|
-
lines.push(`${petPart}${spaces(gap)}${textPart}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (leftImageLine && lines.length > 0) {
|
|
242
|
-
lines[lines.length - 1] += terminalImageInlineLeftSequence(leftImageLine, totalRows);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return lines;
|
|
159
|
+
function textPanel(title: string, lines: string[], done: () => void) {
|
|
160
|
+
return {
|
|
161
|
+
render(width: number) {
|
|
162
|
+
const clipped = lines.map((line) => truncateToWidth(line, width, "..."));
|
|
163
|
+
return [title, "", ...clipped, "", "Esc/q to go back"];
|
|
164
|
+
},
|
|
165
|
+
invalidate() {},
|
|
166
|
+
handleInput(data: string) {
|
|
167
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c")) || data === "q") done();
|
|
168
|
+
},
|
|
169
|
+
};
|
|
246
170
|
}
|
|
247
171
|
|
|
248
172
|
export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
@@ -251,6 +175,13 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
251
175
|
let footerTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
252
176
|
let footerInstalled = false;
|
|
253
177
|
let statusInstalled = false;
|
|
178
|
+
let contextUsageCached = false;
|
|
179
|
+
let cachedContextUsage: ReturnType<ExtensionContext["getContextUsage"]>;
|
|
180
|
+
let cachedContextLeafId: string | null | undefined;
|
|
181
|
+
let cachedContextModel: ExtensionContext["model"];
|
|
182
|
+
let sessionNameCached = false;
|
|
183
|
+
let cachedSessionNameLeafId: string | null | undefined;
|
|
184
|
+
let cachedSessionName: string | undefined;
|
|
254
185
|
const usageController = new UsageController(config, updateFooter);
|
|
255
186
|
const petController = new PetFooterController(config, updateFooter, () => footerInstalled);
|
|
256
187
|
|
|
@@ -310,6 +241,41 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
310
241
|
}
|
|
311
242
|
}
|
|
312
243
|
|
|
244
|
+
function invalidateContextUsage(): void {
|
|
245
|
+
contextUsageCached = false;
|
|
246
|
+
cachedContextUsage = undefined;
|
|
247
|
+
cachedContextLeafId = undefined;
|
|
248
|
+
cachedContextModel = undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function contextUsage(ctx: ExtensionContext): ReturnType<ExtensionContext["getContextUsage"]> {
|
|
252
|
+
const leafId = ctx.sessionManager.getLeafId();
|
|
253
|
+
const model = ctx.model;
|
|
254
|
+
if (!contextUsageCached || leafId !== cachedContextLeafId || model !== cachedContextModel) {
|
|
255
|
+
cachedContextUsage = ctx.getContextUsage();
|
|
256
|
+
contextUsageCached = true;
|
|
257
|
+
cachedContextLeafId = leafId;
|
|
258
|
+
cachedContextModel = model;
|
|
259
|
+
}
|
|
260
|
+
return cachedContextUsage;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function sessionName(ctx: ExtensionContext): string | undefined {
|
|
264
|
+
const leafId = ctx.sessionManager.getLeafId();
|
|
265
|
+
if (!sessionNameCached || leafId !== cachedSessionNameLeafId) {
|
|
266
|
+
cachedSessionName = ctx.sessionManager.getSessionName();
|
|
267
|
+
cachedSessionNameLeafId = leafId;
|
|
268
|
+
sessionNameCached = true;
|
|
269
|
+
}
|
|
270
|
+
return cachedSessionName;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function invalidateSessionName(): void {
|
|
274
|
+
sessionNameCached = false;
|
|
275
|
+
cachedSessionNameLeafId = undefined;
|
|
276
|
+
cachedSessionName = undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
313
279
|
pi.registerFlag(FLAG, {
|
|
314
280
|
description: "Start with OpenAI fast mode enabled (service_tier=priority)",
|
|
315
281
|
type: "boolean",
|
|
@@ -353,19 +319,6 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
353
319
|
},
|
|
354
320
|
});
|
|
355
321
|
|
|
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
322
|
function buildPetSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
|
|
370
323
|
return settingsItemsFromDescriptors(PET_SETTING_DESCRIPTORS, cfg, {
|
|
371
324
|
"pets.slug": {
|
|
@@ -608,10 +561,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
608
561
|
});
|
|
609
562
|
}
|
|
610
563
|
|
|
611
|
-
function buildFastSettingsItems(
|
|
612
|
-
ctx: ExtensionContext,
|
|
613
|
-
cfg: ResolvedConfig,
|
|
614
|
-
): SettingsPickerItem[] {
|
|
564
|
+
function buildFastSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
|
|
615
565
|
return [
|
|
616
566
|
{
|
|
617
567
|
id: "fast.enabled",
|
|
@@ -659,11 +609,13 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
659
609
|
id: "config.print",
|
|
660
610
|
label: "Print config",
|
|
661
611
|
currentValue: "open",
|
|
662
|
-
description: "Show the selected
|
|
612
|
+
description: "Show the selected config JSON with sensitive fields redacted.",
|
|
663
613
|
submenu: (_value, done) =>
|
|
664
614
|
textPanel(
|
|
665
615
|
"Config",
|
|
666
|
-
JSON.stringify(readRawConfig(cfg.configPath), null, 2).split(
|
|
616
|
+
JSON.stringify(redactDiagnosticValue(readRawConfig(cfg.configPath)), null, 2).split(
|
|
617
|
+
"\n",
|
|
618
|
+
),
|
|
667
619
|
() => done(),
|
|
668
620
|
),
|
|
669
621
|
},
|
|
@@ -680,7 +632,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
680
632
|
submenu: (_value, done) =>
|
|
681
633
|
settingsSubmenu(
|
|
682
634
|
"Fast mode settings",
|
|
683
|
-
() => buildFastSettingsItems(
|
|
635
|
+
() => buildFastSettingsItems(config(ctx)),
|
|
684
636
|
ctx,
|
|
685
637
|
() => done(fastSettingsSummary(ctx, config(ctx))),
|
|
686
638
|
),
|
|
@@ -803,6 +755,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
803
755
|
}
|
|
804
756
|
|
|
805
757
|
async function showSettingsPicker(ctx: ExtensionContext): Promise<void> {
|
|
758
|
+
if (!hasTerminalUI(ctx)) {
|
|
759
|
+
ctx.ui.notify("Better OpenAI settings require interactive TUI mode.", "warning");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
806
762
|
try {
|
|
807
763
|
petController.settingsPets = await listCodexPets();
|
|
808
764
|
} catch {
|
|
@@ -887,7 +843,12 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
887
843
|
slug: selectedPet.slug,
|
|
888
844
|
});
|
|
889
845
|
updateFooter(ctx);
|
|
890
|
-
await petController.refresh(ctx, next, true);
|
|
846
|
+
if (hasTerminalUI(ctx)) await petController.refresh(ctx, next, true);
|
|
847
|
+
else
|
|
848
|
+
ctx.ui.notify(
|
|
849
|
+
`Enabled ${selectedPet.name} (${selectedPet.slug}); footer pets render in interactive TUI mode.`,
|
|
850
|
+
"info",
|
|
851
|
+
);
|
|
891
852
|
},
|
|
892
853
|
tuck: (ctx) => {
|
|
893
854
|
writePetConfig(ctx, { enabled: false });
|
|
@@ -912,8 +873,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
912
873
|
|
|
913
874
|
const next = writePetConfig(ctx, { slug: selectedPet.slug });
|
|
914
875
|
updateFooter(ctx);
|
|
915
|
-
if (petController.shouldLoadForConfig(next))
|
|
916
|
-
|
|
876
|
+
if (petController.shouldLoadForConfig(next)) {
|
|
877
|
+
if (hasTerminalUI(ctx)) await petController.refresh(ctx, next, true);
|
|
878
|
+
else
|
|
879
|
+
ctx.ui.notify(
|
|
880
|
+
`Selected ${selectedPet.name} (${selectedPet.slug}); footer pets render in interactive TUI mode.`,
|
|
881
|
+
"info",
|
|
882
|
+
);
|
|
883
|
+
} else {
|
|
917
884
|
ctx.ui.notify(
|
|
918
885
|
`Selected ${selectedPet.name} (${selectedPet.slug}) for the footer pet. Use /pets wake to show it.`,
|
|
919
886
|
"info",
|
|
@@ -966,8 +933,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
966
933
|
const branch = footerData.getGitBranch?.();
|
|
967
934
|
if (branch) pwd = `${pwd} (${branch})`;
|
|
968
935
|
|
|
969
|
-
const
|
|
970
|
-
if (
|
|
936
|
+
const currentSessionName = sessionName(ctx);
|
|
937
|
+
if (currentSessionName) pwd = `${pwd} • ${currentSessionName}`;
|
|
971
938
|
|
|
972
939
|
const parts: string[] = [];
|
|
973
940
|
if (totalInput) parts.push(`↑${formatTokens(totalInput)}`);
|
|
@@ -979,11 +946,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
979
946
|
if (totalCost || usingSubscription)
|
|
980
947
|
parts.push(`$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`);
|
|
981
948
|
|
|
982
|
-
const
|
|
983
|
-
const contextWindow =
|
|
984
|
-
const contextPercentValue =
|
|
949
|
+
const currentContextUsage = contextUsage(ctx);
|
|
950
|
+
const contextWindow = currentContextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
951
|
+
const contextPercentValue = currentContextUsage?.percent ?? 0;
|
|
985
952
|
const contextPercent =
|
|
986
|
-
|
|
953
|
+
currentContextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
|
987
954
|
const contextDisplay =
|
|
988
955
|
contextPercent === "?"
|
|
989
956
|
? `?/${formatTokens(contextWindow)} (auto)`
|
|
@@ -997,12 +964,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
997
964
|
parts.push(contextText);
|
|
998
965
|
|
|
999
966
|
const cfg = config(ctx);
|
|
1000
|
-
const
|
|
1001
|
-
const
|
|
1002
|
-
const requestedPetPlacement = cfgForPets.pets.placement;
|
|
967
|
+
const shouldRenderPet = petController.shouldRenderInFooter(cfg);
|
|
968
|
+
const requestedPetPlacement = cfg.pets.placement;
|
|
1003
969
|
const requestedPetSizeCells = petSizeCellsForPlacement(
|
|
1004
970
|
requestedPetPlacement,
|
|
1005
|
-
|
|
971
|
+
cfg.pets.sizeCells,
|
|
1006
972
|
);
|
|
1007
973
|
const inlinePet = Boolean(
|
|
1008
974
|
shouldRenderPet &&
|
|
@@ -1010,11 +976,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1010
976
|
isInlinePetPlacement(requestedPetPlacement) &&
|
|
1011
977
|
width >= requestedPetSizeCells + 32,
|
|
1012
978
|
);
|
|
1013
|
-
const petRenderSizeCells = inlinePet ? requestedPetSizeCells :
|
|
979
|
+
const petRenderSizeCells = inlinePet ? requestedPetSizeCells : cfg.pets.sizeCells;
|
|
1014
980
|
const petColumnWidth = Math.min(petRenderSizeCells, Math.max(1, width - 1));
|
|
1015
981
|
const footerTextWidth = inlinePet ? Math.max(1, width - petColumnWidth - 2) : width;
|
|
1016
982
|
|
|
1017
|
-
const usageStatusLine = usageController.statusLine(ctx, cfg);
|
|
983
|
+
const usageStatusLine = usageController.statusLine(ctx, cfg, usingSubscription);
|
|
1018
984
|
const usageLine = usageStatusLine ? theme.fg("dim", usageStatusLine) : undefined;
|
|
1019
985
|
|
|
1020
986
|
let statsLeft = parts.join(" ");
|
|
@@ -1027,7 +993,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1027
993
|
const modelName = ctx.model?.id || "no-model";
|
|
1028
994
|
const thinkingLevel = pi.getThinkingLevel();
|
|
1029
995
|
const fastSuffix =
|
|
1030
|
-
fastController.active && supportsFast(ctx,
|
|
996
|
+
fastController.active && supportsFast(ctx, cfg.supportedModels) ? " fast" : "";
|
|
1031
997
|
let rightWithoutProvider = modelName;
|
|
1032
998
|
if (ctx.model?.reasoning) {
|
|
1033
999
|
rightWithoutProvider =
|
|
@@ -1084,7 +1050,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1084
1050
|
textLines.push(truncateToWidth(statusLine, footerTextWidth, theme.fg("dim", "...")));
|
|
1085
1051
|
}
|
|
1086
1052
|
|
|
1087
|
-
const petLines = petController.renderPetLines(ctx,
|
|
1053
|
+
const petLines = petController.renderPetLines(ctx, cfg, {
|
|
1088
1054
|
shouldRenderPet,
|
|
1089
1055
|
freezePetFrame,
|
|
1090
1056
|
requestedPetPlacement,
|
|
@@ -1140,6 +1106,18 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1140
1106
|
|
|
1141
1107
|
function updateFooter(ctx: ExtensionContext): void {
|
|
1142
1108
|
const cfg = config(ctx);
|
|
1109
|
+
|
|
1110
|
+
if (!hasTerminalUI(ctx)) {
|
|
1111
|
+
if (cfg.footer.mode === "off") {
|
|
1112
|
+
setStatus(ctx, undefined);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const fast = fastController.statusSegment(ctx, cfg);
|
|
1116
|
+
const usage = usageController.statusLine(ctx, cfg);
|
|
1117
|
+
setStatus(ctx, [fast, usage].filter(Boolean).join(" | ") || undefined);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1143
1121
|
petController.updateActivity(ctx, cfg);
|
|
1144
1122
|
const shouldRenderPet = petController.shouldRenderInFooter(cfg);
|
|
1145
1123
|
|
|
@@ -1162,6 +1140,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1162
1140
|
}
|
|
1163
1141
|
|
|
1164
1142
|
pi.on("session_start", (_event, ctx) => {
|
|
1143
|
+
invalidateContextUsage();
|
|
1144
|
+
invalidateSessionName();
|
|
1165
1145
|
const nextConfig = refresh(ctx);
|
|
1166
1146
|
fastController.initializeForSession(ctx, nextConfig, pi.getFlag(FLAG) === true);
|
|
1167
1147
|
if (
|
|
@@ -1172,15 +1152,16 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1172
1152
|
if (fastController.desiredActive && !fastController.active) {
|
|
1173
1153
|
ctx.ui.notify(fastController.unsupportedRequestMessage(ctx, nextConfig), "warning");
|
|
1174
1154
|
}
|
|
1175
|
-
petController.installResizeGuard(ctx);
|
|
1155
|
+
if (hasTerminalUI(ctx)) petController.installResizeGuard(ctx);
|
|
1176
1156
|
refreshFooterTotals(ctx);
|
|
1177
1157
|
updateFooter(ctx);
|
|
1178
|
-
if (nextConfig.pets.enabled) void petController.refresh(ctx, nextConfig);
|
|
1158
|
+
if (hasTerminalUI(ctx) && nextConfig.pets.enabled) void petController.refresh(ctx, nextConfig);
|
|
1179
1159
|
usageController.start(ctx);
|
|
1180
1160
|
if (fastController.active) ctx.ui.notify(fastController.stateText(ctx, nextConfig), "info");
|
|
1181
1161
|
});
|
|
1182
1162
|
|
|
1183
1163
|
pi.on("agent_start", (_event, ctx) => {
|
|
1164
|
+
invalidateContextUsage();
|
|
1184
1165
|
petController.agentStart(ctx);
|
|
1185
1166
|
updateFooter(ctx);
|
|
1186
1167
|
});
|
|
@@ -1200,13 +1181,21 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1200
1181
|
updateFooter(ctx);
|
|
1201
1182
|
});
|
|
1202
1183
|
|
|
1203
|
-
pi.on("turn_end", (
|
|
1204
|
-
|
|
1184
|
+
pi.on("turn_end", (event, ctx) => {
|
|
1185
|
+
invalidateContextUsage();
|
|
1186
|
+
if (event.message?.role === "assistant") {
|
|
1187
|
+
footerTotals.input += event.message.usage.input;
|
|
1188
|
+
footerTotals.output += event.message.usage.output;
|
|
1189
|
+
footerTotals.cacheRead += event.message.usage.cacheRead;
|
|
1190
|
+
footerTotals.cacheWrite += event.message.usage.cacheWrite;
|
|
1191
|
+
footerTotals.cost += event.message.usage.cost.total;
|
|
1192
|
+
} else refreshFooterTotals(ctx);
|
|
1205
1193
|
updateFooter(ctx);
|
|
1206
1194
|
void usageController.refresh(ctx);
|
|
1207
1195
|
});
|
|
1208
1196
|
|
|
1209
1197
|
pi.on("session_compact", (_event, ctx) => {
|
|
1198
|
+
invalidateContextUsage();
|
|
1210
1199
|
refreshFooterTotals(ctx);
|
|
1211
1200
|
petController.queueKittyCleanup();
|
|
1212
1201
|
petController.resetRenderCache();
|
|
@@ -1214,6 +1203,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1214
1203
|
});
|
|
1215
1204
|
|
|
1216
1205
|
pi.on("session_tree", (_event, ctx) => {
|
|
1206
|
+
invalidateContextUsage();
|
|
1217
1207
|
refreshFooterTotals(ctx);
|
|
1218
1208
|
petController.queueKittyCleanup();
|
|
1219
1209
|
petController.resetRenderCache();
|
|
@@ -1221,6 +1211,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1221
1211
|
});
|
|
1222
1212
|
|
|
1223
1213
|
pi.on("model_select", (event, ctx) => {
|
|
1214
|
+
invalidateContextUsage();
|
|
1224
1215
|
const cfg = config(ctx);
|
|
1225
1216
|
const wasActive = fastController.active;
|
|
1226
1217
|
fastController.applyDesiredState(ctx, cfg);
|
|
@@ -1238,6 +1229,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1238
1229
|
});
|
|
1239
1230
|
|
|
1240
1231
|
pi.on("session_shutdown", () => {
|
|
1232
|
+
invalidateContextUsage();
|
|
1233
|
+
invalidateSessionName();
|
|
1241
1234
|
usageController.shutdown();
|
|
1242
1235
|
petController.shutdown();
|
|
1243
1236
|
});
|
|
@@ -1245,6 +1238,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1245
1238
|
pi.on("before_provider_request", (event, ctx) => {
|
|
1246
1239
|
return fastController.injectProviderPayload(event, ctx, config(ctx));
|
|
1247
1240
|
});
|
|
1241
|
+
|
|
1242
|
+
pi.on("message_start", invalidateContextUsage);
|
|
1243
|
+
pi.on("message_update", invalidateContextUsage);
|
|
1244
|
+
pi.on("message_end", invalidateContextUsage);
|
|
1248
1245
|
}
|
|
1249
1246
|
|
|
1250
1247
|
export const _test = {
|
|
@@ -1273,6 +1270,7 @@ export const _test = {
|
|
|
1273
1270
|
formatPercent,
|
|
1274
1271
|
formatUsageSnapshot,
|
|
1275
1272
|
readCodexAuth,
|
|
1273
|
+
textPanel,
|
|
1276
1274
|
imageTest: _imageTest,
|
|
1277
1275
|
petsTest: _petsTest,
|
|
1278
1276
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-openai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "Personal pi extension that improves OpenAI with fast mode, usage stats, and footer polish.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fast",
|
|
@@ -41,18 +41,20 @@
|
|
|
41
41
|
"sharp": "^0.34.5"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@
|
|
44
|
+
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
45
|
+
"@earendil-works/pi-tui": "0.80.6",
|
|
46
|
+
"@types/node": "~22.19.0",
|
|
45
47
|
"oxfmt": "^0.47.0",
|
|
46
48
|
"oxlint": "^1.62.0",
|
|
47
49
|
"typescript": "^6.0.3",
|
|
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": [
|