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 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
- ["openai/gpt-5.4", "openai/gpt-5.5", "openai-codex/gpt-5.4", "openai-codex/gpt-5.5"]
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 "@mariozechner/pi-coding-agent";
13
- import { Container, Key, matchesKey, SettingsList } from "@mariozechner/pi-tui";
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 { formatTokens, sanitizeStatusText, truncateToWidth, visibleWidth } from "./src/format.ts";
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 abbreviateHomePath(
81
- path: string,
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
- const requested = petPickerLookupKey(value.trim());
117
- if (!requested) return undefined;
118
- return pets.find(
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 spaces(width: number): string {
171
- return " ".repeat(Math.max(0, width));
172
- }
173
-
174
- function padTextToWidth(value: string, width: number): string {
175
- return value + spaces(width - visibleWidth(value));
176
- }
177
-
178
- function isTerminalImageLine(line: string): boolean {
179
- return line.includes("\x1b_G") || line.includes("\x1b]1337;File=");
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 raw config JSON.",
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("\n"),
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(ctx, config(ctx)),
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)) await petController.refresh(ctx, next, true);
916
- else {
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 sessionName = ctx.sessionManager.getSessionName();
970
- if (sessionName) pwd = `${pwd} • ${sessionName}`;
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 contextUsage = ctx.getContextUsage();
983
- const contextWindow = contextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
984
- const contextPercentValue = contextUsage?.percent ?? 0;
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
- contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
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 cfgForPets = cfg;
1001
- const shouldRenderPet = petController.shouldRenderInFooter(cfgForPets);
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
- cfgForPets.pets.sizeCells,
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 : cfgForPets.pets.sizeCells;
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, config(ctx).supportedModels) ? " fast" : "";
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, cfgForPets, {
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", (_event, ctx) => {
1204
- refreshFooterTotals(ctx);
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.19",
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
- "@types/node": "^24.13.2",
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
- "@mariozechner/pi-coding-agent": ">=0.57.0",
52
- "@mariozechner/pi-tui": "*"
53
+ "@earendil-works/pi-coding-agent": "*",
54
+ "@earendil-works/pi-tui": "*"
53
55
  },
54
56
  "engines": {
55
- "node": ">=20.6.0"
57
+ "node": ">=22.19.0"
56
58
  },
57
59
  "pi": {
58
60
  "extensions": [