pi-better-openai 0.1.18 → 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/index.ts CHANGED
@@ -4,37 +4,37 @@
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";
7
8
  import {
8
9
  getSettingsListTheme,
9
10
  type ExtensionAPI,
10
11
  type ExtensionContext,
11
- } from "@mariozechner/pi-coding-agent";
12
- import {
13
- calculateImageRows,
14
- Container,
15
- getCapabilities,
16
- getCellDimensions,
17
- Key,
18
- matchesKey,
19
- SettingsList,
20
- } from "@mariozechner/pi-tui";
12
+ } from "@earendil-works/pi-coding-agent";
13
+ import { Container, Key, matchesKey, SettingsList } from "@earendil-works/pi-tui";
21
14
  import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
22
- import { formatTokens, sanitizeStatusText, truncateToWidth, visibleWidth } from "./src/format.ts";
15
+ import {
16
+ formatTokens,
17
+ redactDiagnosticValue,
18
+ sanitizeStatusText,
19
+ truncateToWidth,
20
+ visibleWidth,
21
+ } from "./src/format.ts";
23
22
  import {
24
23
  DEFAULT_CONFIG,
25
24
  DEFAULT_IMAGE_CONFIG,
26
25
  DEFAULT_PET_CONFIG,
27
26
  DEFAULT_SUPPORTED_MODELS,
28
- FOOTER_MODES,
29
- IMAGE_OUTPUT_FORMATS,
30
- IMAGE_SAVE_MODES,
31
- PET_PLACEMENTS,
32
27
  PET_STATES,
28
+ FOOTER_SETTING_DESCRIPTORS,
29
+ USAGE_SETTING_DESCRIPTORS,
30
+ IMAGE_SETTING_DESCRIPTORS,
31
+ PET_SETTING_DESCRIPTORS,
32
+ FAST_SETTING_DESCRIPTORS,
33
+ type SettingsOptionDescriptor,
33
34
  configPaths,
34
35
  type ResolvedConfig,
35
36
  type PetPlacement,
36
37
  type PetState,
37
- type SupportedModel,
38
38
  isRecord,
39
39
  parseModelKey,
40
40
  normalizeModelKeys,
@@ -42,42 +42,34 @@ import {
42
42
  readRawConfig,
43
43
  resolveConfig,
44
44
  writeConfig,
45
+ applySettingToRawConfig,
45
46
  } from "./src/config.ts";
46
47
  import {
47
- AUTH_FILE,
48
- type UsageSnapshot,
49
48
  formatPercent,
50
- formatResetCountdown,
51
49
  formatUsageSnapshot,
52
50
  parseUsageSnapshot,
53
51
  readCodexAuth,
54
- requestCodexUsage,
55
52
  } from "./src/usage.ts";
56
53
  import { registerOpenAIImage, _imageTest } from "./src/image.ts";
57
54
  import {
58
55
  type CodexPetPackage,
59
- type LoadedCodexPet,
60
- loadCodexPet,
61
- animationFrameAt,
62
56
  codexHome,
63
57
  describeCodexPetSelectionIssue,
58
+ findCodexPet,
64
59
  findReadyCodexPet,
65
60
  formatNoReadyCodexPetsMessage,
66
61
  listCodexPets,
67
- nextAnimationFrameDelayMs,
68
- PET_ANIMATION_ROWS,
69
- CodexPetKittyManager,
70
62
  registerOpenAIPets,
71
- renderCodexPetFrame,
72
63
  _petsTest,
73
64
  } from "./src/pets.ts";
65
+ import { FastController, modelList, supportsFast } from "./src/fast-controller.ts";
66
+ import { UsageController } from "./src/usage-controller.ts";
67
+ import { PetFooterController } from "./src/pet-footer-controller.ts";
74
68
 
75
69
  const COMMAND = "fast";
76
70
  const OPENAI_STATUS_COMMAND = "openai-usage";
77
71
  const OPENAI_SETTINGS_COMMAND = "openai-settings";
78
72
  const FLAG = "fast";
79
- const PET_RESIZE_FREEZE_MS = 120;
80
- const PET_RENDER_CACHE_LIMIT = 48;
81
73
  const PET_EMPTY_VALUE = "not selected";
82
74
  const SERVICE_TIER = "priority";
83
75
  type SettingsPickerItem = {
@@ -92,53 +84,24 @@ type SettingsPickerItem = {
92
84
  ) => { render(width: number): string[]; invalidate(): void; handleInput?(data: string): void };
93
85
  };
94
86
 
95
- function currentModelKey(ctx: ExtensionContext): string {
96
- return ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none";
97
- }
98
-
99
- function supportsFast(ctx: ExtensionContext, supportedModels: SupportedModel[]): boolean {
100
- const current = ctx.model;
101
- if (!current) return false;
102
- return supportedModels.some(
103
- (model) => model.provider === current.provider && model.id === current.id,
104
- );
105
- }
106
-
107
- function modelList(supportedModels: SupportedModel[]): string {
108
- return supportedModels.length > 0
109
- ? supportedModels.map((model) => `${model.provider}/${model.id}`).join(", ")
110
- : "none configured";
111
- }
112
-
113
- function stateText(
114
- ctx: ExtensionContext,
115
- desiredActive: boolean,
116
- active: boolean,
117
- supportedModels: SupportedModel[],
87
+ function abbreviateHomePath(
88
+ path: string,
89
+ home = process.env.HOME || process.env.USERPROFILE,
118
90
  ): string {
119
- const model = currentModelKey(ctx);
120
- if (active) return `Fast mode is on for ${model}.`;
121
- if (desiredActive) {
122
- return `Fast mode is requested, but inactive for unsupported model ${model}. Supported models: ${modelList(supportedModels)}.`;
123
- }
124
- return `Fast mode is off. Current model: ${model}.`;
125
- }
126
-
127
- function isOpenAISubscriptionModel(ctx: ExtensionContext, cfg: ResolvedConfig): boolean {
128
- if (!ctx.model || (ctx.model.provider !== "openai" && ctx.model.provider !== "openai-codex"))
129
- return false;
130
- return !cfg.usage.showOnlyOnSubscriptionModels || ctx.modelRegistry.isUsingOAuth(ctx.model);
131
- }
132
-
133
- function randomIdleEmoteState(idleState: PetState, random = Math.random): PetState {
134
- const candidates = (["waving", "jumping"] as const).filter((state) => state !== idleState);
135
- return candidates[Math.floor(random() * candidates.length)] ?? "waving";
91
+ if (!home) return path;
92
+ if (path === home) return "~";
93
+ const homePrefix = home.endsWith(sep) ? home : `${home}${sep}`;
94
+ return path.startsWith(homePrefix) ? `~/${path.slice(homePrefix.length)}` : path;
136
95
  }
137
96
 
138
97
  function isInlinePetPlacement(placement: PetPlacement): boolean {
139
98
  return placement === "inline-left" || placement === "inline-right" || placement === "badge";
140
99
  }
141
100
 
101
+ function hasTerminalUI(ctx: ExtensionContext): boolean {
102
+ return ctx.mode === "tui" || (ctx.mode === undefined && ctx.hasUI);
103
+ }
104
+
142
105
  function petSizeCellsForPlacement(placement: PetPlacement, sizeCells: number): number {
143
106
  return placement === "badge" ? Math.min(6, sizeCells) : sizeCells;
144
107
  }
@@ -156,19 +119,10 @@ function petSlugFromPickerValue(value: string): string {
156
119
  return value === PET_EMPTY_VALUE ? "" : value;
157
120
  }
158
121
 
159
- function petPickerLookupKey(value: string): string {
160
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
161
- }
162
-
163
122
  function findPickerPet(value: string, pets: CodexPetPackage[]): CodexPetPackage | undefined {
164
- const requested = petPickerLookupKey(value.trim());
165
- if (!requested) return undefined;
166
- return pets.find(
167
- (pet) =>
168
- pet.hasSpritesheet &&
169
- (petPickerLookupKey(pet.slug) === requested ||
170
- petPickerLookupKey(pet.name) === requested ||
171
- (pet.id !== undefined && petPickerLookupKey(pet.id) === requested)),
123
+ return findCodexPet(
124
+ pets.filter((pet) => pet.hasSpritesheet),
125
+ value,
172
126
  );
173
127
  }
174
128
 
@@ -293,109 +247,34 @@ function combineInlinePetFooter(
293
247
  return lines;
294
248
  }
295
249
 
296
- function petFrameInfo(pet: LoadedCodexPet, state: PetState, elapsedMs: number) {
297
- const frames = pet.states[state] ?? pet.states.idle;
298
- const frame = animationFrameAt(frames, elapsedMs);
299
- return { frame, frameIndex: frame ? frames.indexOf(frame) : -1 };
300
- }
301
-
302
- function petFrameRows(
303
- pet: LoadedCodexPet,
304
- state: PetState,
305
- elapsedMs: number,
306
- width: number,
307
- sizeCells: number,
308
- ): number {
309
- const { frame } = petFrameInfo(pet, state, elapsedMs);
310
- if (!frame || !getCapabilities().images) return 0;
311
- const columns = Math.max(1, Math.min(Math.max(1, width - 2), sizeCells));
312
- return calculateImageRows(
313
- { widthPx: frame.widthPx, heightPx: frame.heightPx },
314
- columns,
315
- getCellDimensions(),
316
- );
317
- }
318
-
319
- function petPlaceholderLines(
320
- pet: LoadedCodexPet,
321
- state: PetState,
322
- elapsedMs: number,
323
- width: number,
324
- sizeCells: number,
325
- ): string[] {
326
- return Array.from({ length: petFrameRows(pet, state, elapsedMs, width, sizeCells) }, () => "");
327
- }
328
-
329
- function petRenderCacheKey(
330
- pet: LoadedCodexPet,
331
- state: PetState,
332
- frameIndex: number,
333
- placement: PetPlacement,
334
- width: number,
335
- sizeCells: number,
336
- ): string {
337
- const cellDimensions = getCellDimensions();
338
- const imageProtocol = getCapabilities().images ?? "none";
339
- return [
340
- pet.pet.slug,
341
- state,
342
- frameIndex,
343
- placement,
344
- width,
345
- sizeCells,
346
- imageProtocol,
347
- cellDimensions.widthPx,
348
- cellDimensions.heightPx,
349
- ].join(":");
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
+ };
350
261
  }
351
262
 
352
263
  export default function betterOpenAI(pi: ExtensionAPI): void {
353
- let desiredActive = false;
354
- let active = false;
264
+ const fastController = new FastController(SERVICE_TIER);
355
265
  let cachedConfig: ResolvedConfig | undefined;
356
- let usageSnapshot: UsageSnapshot | undefined;
357
- let usageUpdatedAt: number | undefined;
358
- let usageError: string | undefined;
359
- let usageLastFetchAt: number | undefined;
360
- let usageTimer: ReturnType<typeof setInterval> | undefined;
361
266
  let footerTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
362
- let usageRefreshInFlight = false;
363
- let queuedUsageRefresh: { ctx: ExtensionContext; modelId?: string; notify?: boolean } | undefined;
364
- let shuttingDown = false;
365
- let usageAbortController: AbortController | undefined;
366
267
  let footerInstalled = false;
367
268
  let statusInstalled = false;
368
- let requestFooterRender: (() => void) | undefined;
369
- let requestSettingsRender: (() => void) | undefined;
370
- let lastInjectedAt: number | undefined;
371
- let lastInjectedModel: string | undefined;
372
- let lastInjectedTier: string | undefined;
373
- let pet: LoadedCodexPet | undefined;
374
- let petError: string | undefined;
375
- let petLoadKey: string | undefined;
376
- let petLoadInFlight = false;
377
- let petLoadingKey: string | undefined;
378
- let petLoadNotify = false;
379
- let queuedPetRefresh: { ctx: ExtensionContext; notify: boolean } | undefined;
380
- let petTimer: ReturnType<typeof setTimeout> | undefined;
381
- let petRenderRequestTimer: ReturnType<typeof setTimeout> | undefined;
382
- let petRuntimeState: PetState = "idle";
383
- let petPreviewState: PetState | undefined;
384
- let petSettingsPreviewActive = false;
385
- let petSettingsPets: CodexPetPackage[] = [];
386
- let petFlashState: PetState | undefined;
387
- let petFlashUntil: number | undefined;
388
- let petFlashTimer: ReturnType<typeof setTimeout> | undefined;
389
- let petIdleEmoteTimer: ReturnType<typeof setTimeout> | undefined;
390
- let petResizeTimer: ReturnType<typeof setTimeout> | undefined;
391
- let petResizeFreezeUntil = 0;
392
- let stdoutResizeHandler: (() => void) | undefined;
393
- let petAnimationState: PetState | undefined;
394
- let petAnimationStartedAt = Date.now();
395
- const petRenderCache = new Map<string, string[]>();
396
- const activeToolCallIds = new Set<string>();
397
- const petImageId = 0x70657401;
398
- const petKittyManager = new CodexPetKittyManager(petImageId);
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;
276
+ const usageController = new UsageController(config, updateFooter);
277
+ const petController = new PetFooterController(config, updateFooter, () => footerInstalled);
399
278
 
400
279
  function refresh(ctx: ExtensionContext): ResolvedConfig {
401
280
  cachedConfig = resolveConfig(ctx.cwd || process.cwd());
@@ -407,413 +286,38 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
407
286
  }
408
287
 
409
288
  function persist(nextConfig: ResolvedConfig): void {
410
- cachedConfig = { ...nextConfig, active, desiredActive };
289
+ cachedConfig = {
290
+ ...nextConfig,
291
+ active: fastController.active,
292
+ desiredActive: fastController.desiredActive,
293
+ };
411
294
  if (!nextConfig.persistState) return;
412
295
  writeConfig(nextConfig.configPath, {
413
296
  ...readRawConfig(nextConfig.configPath),
414
- active,
415
- desiredActive,
297
+ active: fastController.active,
298
+ desiredActive: fastController.desiredActive,
416
299
  });
417
300
  }
418
301
 
419
- function shouldLoadPetForConfig(cfg: ResolvedConfig): boolean {
420
- return cfg.pets.enabled || petSettingsPreviewActive;
421
- }
422
-
423
- function shouldRenderPetInFooter(cfg: ResolvedConfig): boolean {
424
- return cfg.pets.enabled || (petSettingsPreviewActive && footerInstalled);
425
- }
426
-
427
- function setPetSettingsPreviewActive(ctx: ExtensionContext, next: boolean): void {
428
- if (petSettingsPreviewActive === next) return;
429
- petSettingsPreviewActive = next;
430
- if (!config(ctx).pets.enabled) petLoadKey = undefined;
431
- void refreshPet(ctx, config(ctx)).then(() => {
432
- if (!shouldRenderPetInFooter(config(ctx))) flushPetKittyCleanupNow();
433
- requestSettingsRender?.();
434
- });
435
- updateFooter(ctx);
436
- }
437
-
438
- function petStateAnimationDurationMs(state: PetState): number {
439
- return PET_ANIMATION_ROWS[state].durations.reduce((sum, duration) => sum + duration, 0);
440
- }
441
-
442
- function clearPetFlash(): void {
443
- if (petFlashTimer) clearTimeout(petFlashTimer);
444
- petFlashTimer = undefined;
445
- petFlashState = undefined;
446
- petFlashUntil = undefined;
447
- petAnimationState = undefined;
448
- }
449
-
450
- function stopPetIdleEmotes(): void {
451
- if (petIdleEmoteTimer) clearTimeout(petIdleEmoteTimer);
452
- petIdleEmoteTimer = undefined;
453
- }
454
-
455
- function resetPetRenderCache(): void {
456
- petRenderCache.clear();
457
- }
458
-
459
- function queuePetKittyCleanup(target = pet): void {
460
- petKittyManager.invalidate(target);
461
- }
462
-
463
- function takePetKittyCleanupSequence(): string {
464
- return petKittyManager.takeCleanupSequence();
465
- }
466
-
467
- function withPendingPetKittyCleanup(lines: string[]): string[] {
468
- const sequence = takePetKittyCleanupSequence();
469
- if (!sequence) return lines;
470
- if (lines.length === 0) return [sequence];
471
- return [`${sequence}\x1b[0m${lines[0]}\x1b[0m`, ...lines.slice(1)];
472
- }
473
-
474
- function writePetKittySequence(sequence: string): void {
475
- if (sequence && process.stdout.isTTY) process.stdout.write(sequence);
476
- }
477
-
478
- function flushPetKittyCleanupNow(): void {
479
- writePetKittySequence(takePetKittyCleanupSequence());
480
- }
481
-
482
- function disposePetKittyNow(target = pet): void {
483
- writePetKittySequence(petKittyManager.dispose(target));
484
- }
485
-
486
- function rememberPetRender(key: string, lines: string[]): void {
487
- petRenderCache.set(key, lines);
488
- while (petRenderCache.size > PET_RENDER_CACHE_LIMIT) {
489
- const firstKey = petRenderCache.keys().next().value;
490
- if (firstKey === undefined) break;
491
- petRenderCache.delete(firstKey);
492
- }
493
- }
494
-
495
- function petResizeFrozen(now = Date.now()): boolean {
496
- return now < petResizeFreezeUntil;
497
- }
498
-
499
- function clearPetResizeFreeze(): void {
500
- if (petResizeTimer) clearTimeout(petResizeTimer);
501
- petResizeTimer = undefined;
502
- petResizeFreezeUntil = 0;
503
- }
504
-
505
- function freezePetForResize(ctx: ExtensionContext, now = Date.now()): void {
506
- petResizeFreezeUntil = now + PET_RESIZE_FREEZE_MS;
507
- queuePetKittyCleanup();
508
- stopPetAnimation();
509
- stopPetIdleEmotes();
510
- stopPendingPetRenderRequest();
511
- if (petResizeTimer) clearTimeout(petResizeTimer);
512
- petResizeTimer = setTimeout(() => {
513
- petResizeTimer = undefined;
514
- petResizeFreezeUntil = 0;
515
- petKittyManager.resetForResize(pet);
516
- resetPetRenderCache();
517
- updateFooter(ctx);
518
- }, PET_RESIZE_FREEZE_MS);
519
- petResizeTimer.unref?.();
520
- }
521
-
522
- function installPetResizeGuard(ctx: ExtensionContext): void {
523
- if (stdoutResizeHandler || !process.stdout.isTTY) return;
524
- stdoutResizeHandler = () => freezePetForResize(ctx);
525
- process.stdout.on("resize", stdoutResizeHandler);
526
- }
527
-
528
- function uninstallPetResizeGuard(): void {
529
- if (stdoutResizeHandler) process.stdout.off("resize", stdoutResizeHandler);
530
- stdoutResizeHandler = undefined;
531
- clearPetResizeFreeze();
532
- }
533
-
534
- function currentPetState(ctx: ExtensionContext, cfg = config(ctx)): PetState {
535
- if (petPreviewState) return petPreviewState;
536
- const now = Date.now();
537
- if (petFlashState && petFlashUntil !== undefined && now < petFlashUntil) return petFlashState;
538
- if (petFlashState) clearPetFlash();
539
- return petRuntimeState === "idle" ? cfg.pets.state : petRuntimeState;
540
- }
541
-
542
- function currentPetAnimation(ctx: ExtensionContext, cfg = config(ctx)) {
543
- const state = currentPetState(ctx, cfg);
544
- const now = Date.now();
545
- if (state !== petAnimationState) {
546
- petAnimationState = state;
547
- petAnimationStartedAt = now;
548
- }
549
- return { state, elapsedMs: now - petAnimationStartedAt };
550
- }
551
-
552
- function stopPetAnimation(): void {
553
- if (petTimer) clearTimeout(petTimer);
554
- petTimer = undefined;
555
- }
556
-
557
- function stopPendingPetRenderRequest(): void {
558
- if (petRenderRequestTimer) clearTimeout(petRenderRequestTimer);
559
- petRenderRequestTimer = undefined;
560
- }
561
-
562
- function requestPetFooterRender(): void {
563
- if (petRenderRequestTimer) return;
564
- petRenderRequestTimer = setTimeout(() => {
565
- petRenderRequestTimer = undefined;
566
- requestFooterRender?.();
567
- requestSettingsRender?.();
568
- }, 16);
569
- petRenderRequestTimer.unref?.();
570
- }
571
-
572
- function schedulePetAnimation(ctx: ExtensionContext): void {
573
- if (!pet || petTimer || petResizeFrozen() || !getCapabilities().images) return;
574
- const { state, elapsedMs } = currentPetAnimation(ctx);
575
- const frames = pet.states[state] ?? pet.states.idle;
576
- petTimer = setTimeout(
577
- () => {
578
- petTimer = undefined;
579
- if (petResizeFrozen()) return;
580
- requestPetFooterRender();
581
- schedulePetAnimation(ctx);
582
- },
583
- nextAnimationFrameDelayMs(frames, elapsedMs),
584
- );
585
- petTimer.unref?.();
586
- }
587
-
588
- function startPetAnimation(ctx: ExtensionContext): void {
589
- schedulePetAnimation(ctx);
590
- void refreshPet(ctx);
591
- }
592
-
593
- function shouldRunIdleEmotes(ctx: ExtensionContext, cfg = config(ctx)): boolean {
594
- return (
595
- cfg.pets.enabled &&
596
- cfg.pets.idleEmotes &&
597
- getCapabilities().images !== null &&
598
- pet !== undefined &&
599
- petRuntimeState === "idle" &&
600
- activeToolCallIds.size === 0 &&
601
- petPreviewState === undefined &&
602
- !petSettingsPreviewActive &&
603
- petFlashState === undefined &&
604
- !petResizeFrozen()
605
- );
606
- }
607
-
608
- function playPetFlash(ctx: ExtensionContext, state: PetState, cfg = config(ctx)): void {
609
- if (!cfg.pets.enabled || !getCapabilities().images) return;
610
- clearPetFlash();
611
- const durationMs = petStateAnimationDurationMs(state);
612
- petFlashState = state;
613
- petFlashUntil = Date.now() + durationMs;
614
- petFlashTimer = setTimeout(() => {
615
- petFlashTimer = undefined;
616
- petFlashState = undefined;
617
- petFlashUntil = undefined;
618
- petAnimationState = undefined;
619
- updateFooter(ctx);
620
- }, durationMs);
621
- petFlashTimer.unref?.();
622
- updateFooter(ctx);
623
- }
624
-
625
- function schedulePetIdleEmote(ctx: ExtensionContext, cfg = config(ctx)): void {
626
- if (petIdleEmoteTimer || !shouldRunIdleEmotes(ctx, cfg)) return;
627
- const delayMs = Math.round(cfg.pets.idleEmoteIntervalMs * (0.75 + Math.random() * 0.75));
628
- petIdleEmoteTimer = setTimeout(() => {
629
- petIdleEmoteTimer = undefined;
630
- const nextConfig = config(ctx);
631
- if (shouldRunIdleEmotes(ctx, nextConfig)) {
632
- playPetFlash(ctx, randomIdleEmoteState(nextConfig.pets.state), nextConfig);
633
- }
634
- schedulePetIdleEmote(ctx, nextConfig);
635
- }, delayMs);
636
- petIdleEmoteTimer.unref?.();
637
- }
638
-
639
- function petLoadKeyForConfig(cfg: ResolvedConfig): string {
640
- const cellDimensions = getCellDimensions();
641
- return [
642
- cfg.pets.slug || "__first__",
643
- cfg.pets.sizeCells,
644
- getCapabilities().images ?? "none",
645
- cellDimensions.widthPx,
646
- cellDimensions.heightPx,
647
- ].join(":");
648
- }
649
-
650
- async function refreshPet(
651
- ctx: ExtensionContext,
652
- cfg = config(ctx),
653
- notify = false,
654
- ): Promise<void> {
655
- if (shuttingDown) return;
656
- if (!shouldLoadPetForConfig(cfg)) {
657
- queuedPetRefresh = undefined;
658
- queuePetKittyCleanup();
659
- pet = undefined;
660
- petError = undefined;
661
- petLoadKey = undefined;
662
- resetPetRenderCache();
663
- clearPetFlash();
664
- stopPetIdleEmotes();
665
- stopPetAnimation();
666
- return;
667
- }
668
-
669
- const key = petLoadKeyForConfig(cfg);
670
- if (petLoadKey === key) return;
671
- if (petLoadInFlight) {
672
- if (petLoadingKey === key) petLoadNotify ||= notify;
673
- else queuedPetRefresh = { ctx, notify: queuedPetRefresh?.notify || notify };
674
- return;
675
- }
676
-
677
- petLoadInFlight = true;
678
- petLoadingKey = key;
679
- petLoadNotify = notify;
680
- petError = undefined;
681
- const previousPet = pet;
682
-
683
- function shouldApplyLoadResult(): boolean {
684
- if (shuttingDown || queuedPetRefresh) return false;
685
- const latestConfig = config(ctx);
686
- return shouldLoadPetForConfig(latestConfig) && petLoadKeyForConfig(latestConfig) === key;
687
- }
688
-
689
- try {
690
- const loadedPet = await loadCodexPet(cfg.pets.slug || undefined, undefined, {
691
- sizeCells: cfg.pets.sizeCells,
692
- });
693
- if (!shouldApplyLoadResult()) return;
694
- if (previousPet) queuePetKittyCleanup(previousPet);
695
- pet = loadedPet;
696
- const petIssue = pet
697
- ? undefined
698
- : describeCodexPetSelectionIssue(await listCodexPets(), cfg.pets.slug || undefined);
699
- petAnimationState = undefined;
700
- resetPetRenderCache();
701
- petLoadKey = key;
702
- if (!pet && petIssue) petError = petIssue.short;
703
- if (petLoadNotify) {
704
- ctx.ui.notify(
705
- pet
706
- ? `Rendering ${pet.pet.name} in the Better OpenAI footer.`
707
- : (petIssue?.message ?? "No ready custom Codex pet found."),
708
- pet ? "info" : "warning",
709
- );
710
- }
711
- } catch (error) {
712
- if (!shouldApplyLoadResult()) return;
713
- if (previousPet) queuePetKittyCleanup(previousPet);
714
- pet = undefined;
715
- petLoadKey = key;
716
- petError = error instanceof Error ? error.message : String(error);
717
- if (petLoadNotify) ctx.ui.notify(`Could not render Codex pet: ${petError}`, "warning");
718
- } finally {
719
- petLoadInFlight = false;
720
- petLoadingKey = undefined;
721
- petLoadNotify = false;
722
- const queued = queuedPetRefresh;
723
- queuedPetRefresh = undefined;
724
- if (queued && !shuttingDown) void refreshPet(queued.ctx, config(queued.ctx), queued.notify);
725
- if (!shuttingDown) updateFooter(ctx);
726
- }
727
- }
728
-
729
302
  function writePetConfig(ctx: ExtensionContext, patch: Record<string, unknown>): ResolvedConfig {
730
303
  const cfg = refresh(ctx);
731
304
  const current = readRawConfig(cfg.configPath);
732
305
  const pets = isRecord(current.pets) ? current.pets : {};
733
306
  writeConfig(cfg.configPath, { ...current, pets: { ...pets, ...patch } });
734
- petLoadKey = undefined;
307
+ petController.invalidateLoadKey();
735
308
  return refresh(ctx);
736
309
  }
737
310
 
738
- function applyDesiredFastState(ctx: ExtensionContext, cfg = config(ctx)): void {
739
- active = desiredActive && supportsFast(ctx, cfg.supportedModels);
740
- }
741
-
742
311
  function setActive(ctx: ExtensionContext, next: boolean): void {
743
312
  const nextConfig = refresh(ctx);
744
- desiredActive = next;
745
- applyDesiredFastState(ctx, nextConfig);
313
+ fastController.setDesired(ctx, nextConfig, next);
746
314
  persist(nextConfig);
747
315
  updateFooter(ctx);
748
- if (next && !active) {
749
- ctx.ui.notify(
750
- `Fast mode requested, but ${currentModelKey(ctx)} is unsupported. It will activate automatically when you switch to a supported model: ${modelList(nextConfig.supportedModels)}.`,
751
- "warning",
752
- );
753
- return;
754
- }
755
- ctx.ui.notify(stateText(ctx, desiredActive, active, nextConfig.supportedModels), "info");
756
- }
757
-
758
- async function refreshUsage(
759
- ctx: ExtensionContext,
760
- modelId = ctx.model?.id,
761
- options?: { notify?: boolean },
762
- ): Promise<void> {
763
- if (shuttingDown || !ctx.hasUI) return;
764
- if (usageRefreshInFlight) {
765
- queuedUsageRefresh = { ctx, modelId, notify: queuedUsageRefresh?.notify || options?.notify };
316
+ if (next && !fastController.active) {
317
+ ctx.ui.notify(fastController.unsupportedRequestMessage(ctx, nextConfig), "warning");
766
318
  return;
767
319
  }
768
- usageRefreshInFlight = true;
769
- const cfg = config(ctx);
770
- try {
771
- if (!cfg.usage.enabled) {
772
- usageSnapshot = undefined;
773
- usageError = "Usage display is disabled.";
774
- if (!shuttingDown) updateFooter(ctx);
775
- return;
776
- }
777
- if (!isOpenAISubscriptionModel(ctx, cfg)) {
778
- if (!shuttingDown) updateFooter(ctx);
779
- return;
780
- }
781
- usageAbortController = new AbortController();
782
- const timeoutSignal = AbortSignal.timeout(10_000);
783
- const signal = ctx.signal
784
- ? AbortSignal.any([ctx.signal, timeoutSignal, usageAbortController.signal])
785
- : AbortSignal.any([timeoutSignal, usageAbortController.signal]);
786
- const data = await requestCodexUsage(signal);
787
- usageLastFetchAt = Date.now();
788
- usageSnapshot = data ? parseUsageSnapshot(data, modelId) : undefined;
789
- usageUpdatedAt = usageSnapshot ? Date.now() : undefined;
790
- usageError = data ? undefined : `Missing openai-codex OAuth credentials in ${AUTH_FILE}.`;
791
- if (!shuttingDown) updateFooter(ctx);
792
- if (!shuttingDown && options?.notify)
793
- ctx.ui.notify(formatUsageStatus(ctx), usageSnapshot ? "info" : "warning");
794
- } catch (error) {
795
- if (shuttingDown) return;
796
- usageError = error instanceof Error ? error.message : String(error);
797
- updateFooter(ctx);
798
- if (options?.notify) ctx.ui.notify(formatUsageStatus(ctx), "warning");
799
- } finally {
800
- usageAbortController = undefined;
801
- usageRefreshInFlight = false;
802
- if (!shuttingDown && queuedUsageRefresh) {
803
- const next = queuedUsageRefresh;
804
- queuedUsageRefresh = undefined;
805
- void refreshUsage(next.ctx, next.modelId, { notify: next.notify });
806
- }
807
- }
808
- }
809
-
810
- function startUsageRefresh(ctx: ExtensionContext): void {
811
- if (usageTimer) clearInterval(usageTimer);
812
- const cfg = config(ctx);
813
- if (!cfg.usage.enabled) return;
814
- void refreshUsage(ctx);
815
- usageTimer = setInterval(() => void refreshUsage(ctx), cfg.usage.refreshIntervalMs);
816
- usageTimer.unref?.();
320
+ ctx.ui.notify(fastController.stateText(ctx, nextConfig), "info");
817
321
  }
818
322
 
819
323
  function refreshFooterTotals(ctx: ExtensionContext): void {
@@ -828,35 +332,39 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
828
332
  }
829
333
  }
830
334
 
831
- function formatUsageDebug(ctx: ExtensionContext): string {
832
- const cfg = config(ctx);
833
- const auth = readCodexAuth();
834
- return [
835
- `Usage enabled: ${cfg.usage.enabled}`,
836
- `Current model: ${currentModelKey(ctx)}`,
837
- `Current model eligible: ${isOpenAISubscriptionModel(ctx, cfg)}`,
838
- `Requires subscription model: ${cfg.usage.showOnlyOnSubscriptionModels}`,
839
- `Auth: ${auth ? "found" : "missing"}`,
840
- `Account ID: ${auth?.accountId ?? "none"}`,
841
- `Last fetch: ${usageLastFetchAt ? new Date(usageLastFetchAt).toLocaleTimeString() : "never"}`,
842
- `Last successful update: ${usageUpdatedAt ? new Date(usageUpdatedAt).toLocaleTimeString() : "never"}`,
843
- `Last error: ${usageError ?? "none"}`,
844
- `Refresh interval: ${cfg.usage.refreshIntervalMs}ms`,
845
- `Endpoint: https://chatgpt.com/backend-api/wham/usage`,
846
- ].join("\n");
335
+ function invalidateContextUsage(): void {
336
+ contextUsageCached = false;
337
+ cachedContextUsage = undefined;
338
+ cachedContextLeafId = undefined;
339
+ cachedContextModel = undefined;
847
340
  }
848
341
 
849
- function formatUsageStatus(ctx: ExtensionContext): string {
850
- const cfg = config(ctx);
851
- if (!cfg.usage.enabled) return "Usage display is disabled.";
852
- if (!isOpenAISubscriptionModel(ctx, cfg))
853
- return "Usage hidden: current model is not an OpenAI subscription model.";
854
- if (!usageSnapshot) return `Usage unavailable${usageError ? `: ${usageError}` : "."}`;
855
- const stale =
856
- usageUpdatedAt && Date.now() - usageUpdatedAt > cfg.usage.refreshIntervalMs * 2
857
- ? ` | stale ${formatResetCountdown((Date.now() - usageUpdatedAt) / 1000)}`
858
- : "";
859
- return `${formatUsageSnapshot(usageSnapshot, cfg.usage)}${stale}`;
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;
860
368
  }
861
369
 
862
370
  pi.registerFlag(FLAG, {
@@ -868,15 +376,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
868
376
  function formatDebugStatus(ctx: ExtensionContext): string {
869
377
  const cfg = config(ctx);
870
378
  return [
871
- `Fast desired: ${desiredActive}`,
872
- `Fast active: ${active}`,
873
- `Current model: ${currentModelKey(ctx)}`,
874
- `Supported model: ${supportsFast(ctx, cfg.supportedModels)}`,
875
- `Configured service_tier: ${SERVICE_TIER}`,
876
- `Last injected: ${lastInjectedAt ? `${new Date(lastInjectedAt).toLocaleTimeString()} (${lastInjectedModel}, ${lastInjectedTier})` : "never"}`,
379
+ ...fastController.debugLines(ctx, cfg),
877
380
  `Footer mode: ${cfg.footer.mode}`,
878
381
  "",
879
- formatUsageDebug(ctx),
382
+ usageController.formatDebug(ctx),
880
383
  "",
881
384
  `Image enabled: ${cfg.image.enabled}`,
882
385
  `Image default save: ${cfg.image.defaultSave}`,
@@ -885,22 +388,17 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
885
388
  `Pet placement: ${cfg.pets.placement}`,
886
389
  `Pet failed tool state: ${cfg.pets.failedToolState}`,
887
390
  `Pet idle emotes: ${cfg.pets.idleEmotes} (${cfg.pets.idleEmoteIntervalMs}ms)`,
888
- `Pet loaded: ${pet?.pet.name ?? "none"}`,
889
- `Pet error: ${petError ?? "none"}`,
391
+ `Pet loaded: ${petController.loadedPet?.pet.name ?? "none"}`,
392
+ `Pet error: ${petController.error ?? "none"}`,
890
393
  `Config: ${cfg.configPath}`,
891
394
  ].join("\n");
892
395
  }
893
396
 
894
- function formatOpenAIStatus(ctx: ExtensionContext): string {
895
- refresh(ctx);
896
- return formatUsageStatus(ctx);
897
- }
898
-
899
397
  pi.registerCommand(COMMAND, {
900
398
  description: "Toggle OpenAI fast mode",
901
399
  handler: async (args, ctx) => {
902
400
  const arg = args.trim().toLowerCase();
903
- if (!arg) return setActive(ctx, !desiredActive);
401
+ if (!arg) return setActive(ctx, !fastController.desiredActive);
904
402
  ctx.ui.notify("Usage: /fast", "error");
905
403
  },
906
404
  });
@@ -908,98 +406,18 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
908
406
  pi.registerCommand(OPENAI_STATUS_COMMAND, {
909
407
  description: "Show OpenAI subscription usage status",
910
408
  handler: async (_args, ctx) => {
911
- ctx.ui.notify(formatOpenAIStatus(ctx), "info");
409
+ await usageController.refresh(ctx, ctx.model?.id, { notify: true, force: true });
912
410
  },
913
411
  });
914
412
 
915
- function textPanel(title: string, lines: string[], done: () => void) {
916
- return {
917
- render(width: number) {
918
- const clipped = lines.map((line) => truncateToWidth(line, width, "..."));
919
- return [title, "", ...clipped, "", "Esc/q to go back"];
920
- },
921
- invalidate() {},
922
- handleInput(data: string) {
923
- if (data.includes("\x1b") || data === "escape" || data === "q" || data === "\x03") done();
924
- },
925
- };
926
- }
927
-
928
413
  function buildPetSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
929
- return [
930
- {
931
- id: "pets.enabled",
932
- label: "Enabled",
933
- currentValue: String(cfg.pets.enabled),
934
- values: ["true", "false"],
935
- description:
936
- "Render a custom Codex pet from ${CODEX_HOME:-~/.codex}/pets in the Better OpenAI footer.",
937
- },
938
- {
939
- id: "pets.slug",
940
- label: "Pet",
414
+ return settingsItemsFromDescriptors(PET_SETTING_DESCRIPTORS, cfg, {
415
+ "pets.slug": {
941
416
  currentValue: petConfigPickerValue(cfg),
942
- values: readyPetPickerValues(petSettingsPets),
943
- description: petPickerDescription(cfg, petSettingsPets),
944
- },
945
- {
946
- id: "pets.placement",
947
- label: "Placement",
948
- currentValue: cfg.pets.placement,
949
- values: [...PET_PLACEMENTS],
950
- description:
951
- "Footer layout: stacked, inline-left, inline-right, badge, or habitat divider.",
952
- },
953
- {
954
- id: "pets.state",
955
- label: "Idle state",
956
- currentValue: cfg.pets.state,
957
- values: [...PET_STATES],
958
- description: "Animation row to show when pi is idle.",
959
- },
960
- {
961
- id: "pets.thinkingState",
962
- label: "Thinking state",
963
- currentValue: cfg.pets.thinkingState,
964
- values: [...PET_STATES],
965
- description: "Animation row to show while the model is thinking or streaming.",
966
- },
967
- {
968
- id: "pets.toolState",
969
- label: "Tool state",
970
- currentValue: cfg.pets.toolState,
971
- values: [...PET_STATES],
972
- description: "Animation row to show during tool execution.",
973
- },
974
- {
975
- id: "pets.failedToolState",
976
- label: "Failed tool state",
977
- currentValue: cfg.pets.failedToolState,
978
- values: [...PET_STATES],
979
- description: "Animation row to flash after any tool call returns an error.",
980
- },
981
- {
982
- id: "pets.idleEmotes",
983
- label: "Random idle emotes",
984
- currentValue: String(cfg.pets.idleEmotes),
985
- values: ["true", "false"],
986
- description: "Occasionally flash a wave or jump while pi is idle.",
987
- },
988
- {
989
- id: "pets.idleEmoteIntervalMs",
990
- label: "Idle emote interval",
991
- currentValue: String(cfg.pets.idleEmoteIntervalMs),
992
- values: ["5000", "15000", "30000", "60000", "120000", "300000"],
993
- description: "Average delay between random idle pet emotes in milliseconds.",
417
+ values: readyPetPickerValues(petController.settingsPets),
418
+ description: petPickerDescription(cfg, petController.settingsPets),
994
419
  },
995
- {
996
- id: "pets.sizeCells",
997
- label: "Size",
998
- currentValue: String(cfg.pets.sizeCells),
999
- values: ["4", "6", "8", "10", "12", "16"],
1000
- description: "Pet image width in terminal cells.",
1001
- },
1002
- ];
420
+ });
1003
421
  }
1004
422
 
1005
423
  function petPreviewFromItem(item: SettingsPickerItem | undefined): PetState | undefined {
@@ -1015,23 +433,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1015
433
  }
1016
434
 
1017
435
  function renderPetSettingsPreview(ctx: ExtensionContext, width: number): string[] {
1018
- if (!petSettingsPreviewActive || shouldRenderPetInFooter(config(ctx))) return [];
1019
- if (!pet) {
1020
- return [petError ? `Preview unavailable: ${petError}` : "Preview: loading pet…"];
1021
- }
1022
- const cfg = config(ctx);
1023
- const { state, elapsedMs } = currentPetAnimation(ctx, cfg);
1024
- const plainTheme = { fg: (_color: string, value: string) => value };
1025
- return [
1026
- `Preview: ${pet.pet.name}`,
1027
- ...renderCodexPetFrame(pet, state, width, plainTheme, {
1028
- sizeCells: cfg.pets.sizeCells,
1029
- imageId: petImageId,
1030
- now: elapsedMs,
1031
- durationMultiplier: 1,
1032
- kittyManager: petKittyManager,
1033
- }),
1034
- ];
436
+ return petController.renderSettingsPreview(ctx, width);
1035
437
  }
1036
438
 
1037
439
  function settingsSubmenu(
@@ -1206,10 +608,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1206
608
  }
1207
609
 
1208
610
  function fastSettingsSummary(ctx: ExtensionContext, cfg: ResolvedConfig): string {
1209
- if (active) return "on";
1210
- if (desiredActive)
1211
- return supportsFast(ctx, cfg.supportedModels) ? "requested" : "requested inactive";
1212
- return "off";
611
+ return fastController.settingsSummary(ctx, cfg);
1213
612
  }
1214
613
 
1215
614
  function usageSettingsSummary(cfg: ResolvedConfig): string {
@@ -1230,113 +629,52 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1230
629
  return `${status} · ${selected} · ${cfg.pets.placement}`;
1231
630
  }
1232
631
 
1233
- function buildFastSettingsItems(
1234
- ctx: ExtensionContext,
632
+ type SettingsItemOverrides = Record<
633
+ string,
634
+ Partial<Pick<SettingsPickerItem, "currentValue" | "description" | "values">>
635
+ >;
636
+
637
+ function settingsItemsFromDescriptors(
638
+ descriptors: readonly SettingsOptionDescriptor[],
1235
639
  cfg: ResolvedConfig,
640
+ overrides: SettingsItemOverrides = {},
1236
641
  ): SettingsPickerItem[] {
642
+ return descriptors.map((descriptor) => {
643
+ const override = overrides[descriptor.id] ?? {};
644
+ const values = override.values ?? descriptor.values;
645
+ return {
646
+ id: descriptor.id,
647
+ label: descriptor.label,
648
+ currentValue: override.currentValue ?? descriptor.currentValue(cfg),
649
+ values: values ? [...values] : undefined,
650
+ description: override.description ?? descriptor.description,
651
+ };
652
+ });
653
+ }
654
+
655
+ function buildFastSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
1237
656
  return [
1238
657
  {
1239
658
  id: "fast.enabled",
1240
659
  label: "Fast mode",
1241
- currentValue: String(desiredActive),
660
+ currentValue: String(fastController.desiredActive),
1242
661
  values: ["true", "false"],
1243
662
  description: `Request OpenAI fast mode. Activates for supported models: ${modelList(cfg.supportedModels)}.`,
1244
663
  },
1245
- {
1246
- id: "persistState",
1247
- label: "Persist fast state",
1248
- currentValue: String(cfg.persistState),
1249
- values: ["true", "false"],
1250
- description: "Remember fast-mode state across sessions.",
1251
- },
664
+ ...settingsItemsFromDescriptors(FAST_SETTING_DESCRIPTORS, cfg),
1252
665
  ];
1253
666
  }
1254
667
 
1255
668
  function buildFooterSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
1256
- return [
1257
- {
1258
- id: "footer.mode",
1259
- label: "Footer mode",
1260
- currentValue: cfg.footer.mode,
1261
- values: [...FOOTER_MODES],
1262
- description:
1263
- "replace = custom footer, status = pi footer plus status line, off = no Better OpenAI footer/status unless Footer pet is enabled.",
1264
- },
1265
- ];
669
+ return settingsItemsFromDescriptors(FOOTER_SETTING_DESCRIPTORS, cfg);
1266
670
  }
1267
671
 
1268
672
  function buildUsageSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
1269
- return [
1270
- {
1271
- id: "usage.enabled",
1272
- label: "Usage display",
1273
- currentValue: String(cfg.usage.enabled),
1274
- values: ["true", "false"],
1275
- description: "Fetch and display OpenAI subscription usage windows.",
1276
- },
1277
- {
1278
- id: "usage.refreshIntervalMs",
1279
- label: "Usage refresh",
1280
- currentValue: String(cfg.usage.refreshIntervalMs),
1281
- values: ["15000", "30000", "60000", "120000", "300000", "600000"],
1282
- description: "Usage refresh interval in milliseconds.",
1283
- },
1284
- {
1285
- id: "usage.showOnlyOnSubscriptionModels",
1286
- label: "Usage only on OAuth",
1287
- currentValue: String(cfg.usage.showOnlyOnSubscriptionModels),
1288
- values: ["true", "false"],
1289
- description: "Only show usage when the current OpenAI model uses subscription/OAuth auth.",
1290
- },
1291
- {
1292
- id: "usage.showResetTimes",
1293
- label: "Usage reset times",
1294
- currentValue: String(cfg.usage.showResetTimes),
1295
- values: ["true", "false"],
1296
- description: "Include compact reset countdowns and local reset times.",
1297
- },
1298
- ];
673
+ return settingsItemsFromDescriptors(USAGE_SETTING_DESCRIPTORS, cfg);
1299
674
  }
1300
675
 
1301
676
  function buildImageSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
1302
- return [
1303
- {
1304
- id: "image.enabled",
1305
- label: "Image tool",
1306
- currentValue: String(cfg.image.enabled),
1307
- values: ["true", "false"],
1308
- description: "Allow the openai_image tool to make image requests.",
1309
- },
1310
- {
1311
- id: "image.defaultModel",
1312
- label: "Image model",
1313
- currentValue: cfg.image.defaultModel,
1314
- values: ["gpt-5.5", "gpt-5.4", "gpt-5.2", "gpt-5"],
1315
- description:
1316
- "Mainline model used for image generation when current model is not openai-codex.",
1317
- },
1318
- {
1319
- id: "image.defaultSave",
1320
- label: "Image save",
1321
- currentValue: cfg.image.defaultSave,
1322
- values: [...IMAGE_SAVE_MODES],
1323
- description: "Where generated images are saved by default.",
1324
- },
1325
- {
1326
- id: "image.outputFormat",
1327
- label: "Image format",
1328
- currentValue: cfg.image.outputFormat,
1329
- values: [...IMAGE_OUTPUT_FORMATS],
1330
- description: "Generated image file format.",
1331
- },
1332
- {
1333
- id: "image.timeoutMs",
1334
- label: "Image timeout",
1335
- currentValue: String(cfg.image.timeoutMs),
1336
- values: ["30000", "60000", "120000", "180000", "300000"],
1337
- description: "Image request timeout in milliseconds.",
1338
- },
1339
- ];
677
+ return settingsItemsFromDescriptors(IMAGE_SETTING_DESCRIPTORS, cfg);
1340
678
  }
1341
679
 
1342
680
  function buildDiagnosticsSettingsItems(
@@ -1362,11 +700,13 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1362
700
  id: "config.print",
1363
701
  label: "Print config",
1364
702
  currentValue: "open",
1365
- description: "Show the selected raw config JSON.",
703
+ description: "Show the selected config JSON with sensitive fields redacted.",
1366
704
  submenu: (_value, done) =>
1367
705
  textPanel(
1368
706
  "Config",
1369
- JSON.stringify(readRawConfig(cfg.configPath), null, 2).split("\n"),
707
+ JSON.stringify(redactDiagnosticValue(readRawConfig(cfg.configPath)), null, 2).split(
708
+ "\n",
709
+ ),
1370
710
  () => done(),
1371
711
  ),
1372
712
  },
@@ -1383,7 +723,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1383
723
  submenu: (_value, done) =>
1384
724
  settingsSubmenu(
1385
725
  "Fast mode settings",
1386
- () => buildFastSettingsItems(ctx, config(ctx)),
726
+ () => buildFastSettingsItems(config(ctx)),
1387
727
  ctx,
1388
728
  () => done(fastSettingsSummary(ctx, config(ctx))),
1389
729
  ),
@@ -1433,7 +773,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1433
773
  currentValue: petSettingsSummary(cfg),
1434
774
  description: "Configure footer pet visibility, animation-state mapping, and size.",
1435
775
  submenu: (_value, done) => {
1436
- setPetSettingsPreviewActive(ctx, true);
776
+ petController.setSettingsPreviewActive(ctx, true);
1437
777
  return settingsSubmenu(
1438
778
  "Footer pet settings",
1439
779
  () => buildPetSettingsItems(config(ctx)),
@@ -1442,14 +782,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1442
782
  {
1443
783
  onSelection: (item) => {
1444
784
  const previewState = petPreviewFromItem(item);
1445
- if (previewState !== petPreviewState) {
1446
- petPreviewState = previewState;
785
+ if (previewState !== petController.previewState) {
786
+ petController.setPreviewState(previewState);
1447
787
  updateFooter(ctx);
1448
788
  }
1449
789
  },
1450
790
  onClose: () => {
1451
- petPreviewState = undefined;
1452
- setPetSettingsPreviewActive(ctx, false);
791
+ petController.setPreviewState(undefined);
792
+ petController.setSettingsPreviewActive(ctx, false);
1453
793
  updateFooter(ctx);
1454
794
  },
1455
795
  renderExtra: (width) => renderPetSettingsPreview(ctx, width),
@@ -1477,79 +817,47 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1477
817
  const cfg = refresh(ctx);
1478
818
  const current = readRawConfig(cfg.configPath);
1479
819
  const bool = rawValue === "true";
1480
- const num = Number(rawValue);
1481
820
  if (id === "fast.enabled") {
1482
- desiredActive = bool;
1483
- applyDesiredFastState(ctx, cfg);
1484
- if (cfg.persistState) {
1485
- current.active = active;
1486
- current.desiredActive = desiredActive;
1487
- }
1488
- } else if (id === "persistState") current.persistState = bool;
1489
- else if (id.startsWith("usage.")) {
1490
- const usage = isRecord(current.usage) ? current.usage : {};
1491
- const key = id.slice("usage.".length);
1492
- usage[key] = key === "refreshIntervalMs" ? num : bool;
1493
- current.usage = usage;
1494
- } else if (id === "footer.mode") {
1495
- const footer = isRecord(current.footer) ? current.footer : {};
1496
- footer.mode = rawValue;
1497
- current.footer = footer;
1498
- } else if (id.startsWith("pets.")) {
1499
- const pets = isRecord(current.pets) ? current.pets : {};
1500
- const key = id.slice("pets.".length);
1501
- pets[key] =
1502
- key === "sizeCells" || key === "idleEmoteIntervalMs"
1503
- ? num
1504
- : key === "slug"
1505
- ? petSlugFromPickerValue(rawValue)
1506
- : rawValue === "true"
1507
- ? true
1508
- : rawValue === "false"
1509
- ? false
1510
- : rawValue;
1511
- current.pets = pets;
1512
- if (key === "enabled" || key === "sizeCells" || key === "slug") petLoadKey = undefined;
1513
- if (key === "placement" || key === "sizeCells" || key === "slug") resetPetRenderCache();
1514
- if (key === "idleEmotes" || key === "idleEmoteIntervalMs") stopPetIdleEmotes();
1515
- } else if (id.startsWith("image.")) {
1516
- const image = isRecord(current.image) ? current.image : {};
1517
- const key = id.slice("image.".length);
1518
- image[key] =
1519
- key === "timeoutMs"
1520
- ? num
1521
- : rawValue === "true"
1522
- ? true
1523
- : rawValue === "false"
1524
- ? false
1525
- : rawValue;
1526
- current.image = image;
821
+ fastController.setDesired(ctx, cfg, bool);
822
+ }
823
+ const petKey = id.startsWith("pets.") ? id.slice("pets.".length) : undefined;
824
+ const nextRawConfig = applySettingToRawConfig(current, id, rawValue, {
825
+ persistState: cfg.persistState,
826
+ active: fastController.active,
827
+ desiredActive: fastController.desiredActive,
828
+ petEmptyValue: PET_EMPTY_VALUE,
829
+ });
830
+ if (petKey) {
831
+ if (petKey === "enabled" || petKey === "sizeCells" || petKey === "slug")
832
+ petController.invalidateLoadKey();
833
+ if (petKey === "placement" || petKey === "sizeCells" || petKey === "slug")
834
+ petController.resetRenderCache();
835
+ if (petKey === "idleEmotes" || petKey === "idleEmoteIntervalMs")
836
+ petController.stopIdleEmotes();
1527
837
  }
1528
- writeConfig(cfg.configPath, current);
838
+ writeConfig(cfg.configPath, nextRawConfig);
1529
839
  const next = refresh(ctx);
1530
840
  if (id === "pets.enabled" || id === "pets.sizeCells" || id === "pets.slug")
1531
- void refreshPet(ctx, next);
841
+ void petController.refresh(ctx, next);
1532
842
  if (id.startsWith("usage.")) {
1533
- if (usageTimer) clearInterval(usageTimer);
1534
- usageTimer = undefined;
1535
- if (next.usage.enabled) startUsageRefresh(ctx);
1536
- else {
1537
- usageSnapshot = undefined;
1538
- usageError = "Usage display is disabled.";
1539
- }
843
+ usageController.restartAfterSettingsChange(ctx, next);
1540
844
  }
1541
845
  updateFooter(ctx);
1542
846
  }
1543
847
 
1544
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
+ }
1545
853
  try {
1546
- petSettingsPets = await listCodexPets();
854
+ petController.settingsPets = await listCodexPets();
1547
855
  } catch {
1548
- petSettingsPets = [];
856
+ petController.settingsPets = [];
1549
857
  }
1550
858
  try {
1551
859
  await ctx.ui.custom((tui, theme, _kb, done) => {
1552
- requestSettingsRender = () => tui.requestRender();
860
+ petController.setSettingsRenderRequest(() => tui.requestRender());
1553
861
  const container = new Container();
1554
862
  container.addChild(
1555
863
  new (class {
@@ -1595,12 +903,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1595
903
  };
1596
904
  });
1597
905
  } finally {
1598
- requestSettingsRender = undefined;
1599
- if (petPreviewState !== undefined || petSettingsPreviewActive) {
1600
- petPreviewState = undefined;
1601
- setPetSettingsPreviewActive(ctx, false);
1602
- updateFooter(ctx);
1603
- }
906
+ petController.setSettingsRenderRequest(undefined);
907
+ petController.setPreviewState(undefined);
908
+ petController.setSettingsPreviewActive(ctx, false);
909
+ updateFooter(ctx);
1604
910
  }
1605
911
  }
1606
912
 
@@ -1628,17 +934,16 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1628
934
  slug: selectedPet.slug,
1629
935
  });
1630
936
  updateFooter(ctx);
1631
- await refreshPet(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
+ );
1632
943
  },
1633
944
  tuck: (ctx) => {
1634
945
  writePetConfig(ctx, { enabled: false });
1635
- queuePetKittyCleanup();
1636
- pet = undefined;
1637
- petError = undefined;
1638
- resetPetRenderCache();
1639
- clearPetFlash();
1640
- stopPetIdleEmotes();
1641
- stopPetAnimation();
946
+ petController.tuck();
1642
947
  updateFooter(ctx);
1643
948
  ctx.ui.notify("Footer pet tucked away.", "info");
1644
949
  },
@@ -1659,8 +964,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1659
964
 
1660
965
  const next = writePetConfig(ctx, { slug: selectedPet.slug });
1661
966
  updateFooter(ctx);
1662
- if (shouldLoadPetForConfig(next)) await refreshPet(ctx, next, true);
1663
- else {
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 {
1664
975
  ctx.ui.notify(
1665
976
  `Selected ${selectedPet.name} (${selectedPet.slug}) for the footer pet. Use /pets wake to show it.`,
1666
977
  "info",
@@ -1671,36 +982,36 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1671
982
 
1672
983
  function installFooter(ctx: ExtensionContext): void {
1673
984
  if (footerInstalled) {
1674
- requestFooterRender?.();
985
+ petController.requestFooterRenderNow();
1675
986
  return;
1676
987
  }
1677
988
  footerInstalled = true;
1678
989
  ctx.ui.setFooter((tui, theme, footerData) => {
1679
- requestFooterRender = () => tui.requestRender();
990
+ petController.setFooterRenderRequest(() => tui.requestRender());
1680
991
  const unsubscribe = footerData.onBranchChange?.(() => tui.requestRender());
1681
992
  let lastFooterSizeKey: string | undefined;
1682
993
  return {
1683
994
  dispose: () => {
1684
995
  unsubscribe?.();
1685
- stopPetIdleEmotes();
1686
- stopPetAnimation();
1687
- stopPendingPetRenderRequest();
1688
- disposePetKittyNow();
996
+ petController.stopIdleEmotes();
997
+ petController.stopAnimation();
998
+ petController.stopPendingRenderRequest();
999
+ petController.disposeKittyNow();
1689
1000
  footerInstalled = false;
1690
- requestFooterRender = undefined;
1001
+ petController.setFooterRenderRequest(undefined);
1691
1002
  },
1692
1003
  invalidate() {
1693
- queuePetKittyCleanup();
1694
- resetPetRenderCache();
1004
+ petController.queueKittyCleanup();
1005
+ petController.resetRenderCache();
1695
1006
  },
1696
1007
  render(width: number): string[] {
1697
1008
  const now = Date.now();
1698
1009
  const footerSizeKey = `${width}:${process.stdout.rows ?? 0}`;
1699
1010
  if (lastFooterSizeKey !== undefined && lastFooterSizeKey !== footerSizeKey) {
1700
- freezePetForResize(ctx, now);
1011
+ petController.freezeForResize(ctx, now);
1701
1012
  }
1702
1013
  lastFooterSizeKey = footerSizeKey;
1703
- const freezePetFrame = petResizeFrozen(now);
1014
+ const freezePetFrame = petController.isResizeFrozen(now);
1704
1015
 
1705
1016
  const totalInput = footerTotals.input;
1706
1017
  const totalOutput = footerTotals.output;
@@ -1708,15 +1019,13 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1708
1019
  const totalCacheWrite = footerTotals.cacheWrite;
1709
1020
  const totalCost = footerTotals.cost;
1710
1021
 
1711
- let pwd = ctx.sessionManager.getCwd();
1712
- const home = process.env.HOME || process.env.USERPROFILE;
1713
- if (home && pwd.startsWith(home)) pwd = `~${pwd.slice(home.length)}`;
1022
+ let pwd = abbreviateHomePath(ctx.sessionManager.getCwd());
1714
1023
 
1715
1024
  const branch = footerData.getGitBranch?.();
1716
1025
  if (branch) pwd = `${pwd} (${branch})`;
1717
1026
 
1718
- const sessionName = ctx.sessionManager.getSessionName();
1719
- if (sessionName) pwd = `${pwd} • ${sessionName}`;
1027
+ const currentSessionName = sessionName(ctx);
1028
+ if (currentSessionName) pwd = `${pwd} • ${currentSessionName}`;
1720
1029
 
1721
1030
  const parts: string[] = [];
1722
1031
  if (totalInput) parts.push(`↑${formatTokens(totalInput)}`);
@@ -1728,11 +1037,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1728
1037
  if (totalCost || usingSubscription)
1729
1038
  parts.push(`$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`);
1730
1039
 
1731
- const contextUsage = ctx.getContextUsage();
1732
- const contextWindow = contextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
1733
- const contextPercentValue = contextUsage?.percent ?? 0;
1040
+ const currentContextUsage = contextUsage(ctx);
1041
+ const contextWindow = currentContextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
1042
+ const contextPercentValue = currentContextUsage?.percent ?? 0;
1734
1043
  const contextPercent =
1735
- contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
1044
+ currentContextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
1736
1045
  const contextDisplay =
1737
1046
  contextPercent === "?"
1738
1047
  ? `?/${formatTokens(contextWindow)} (auto)`
@@ -1746,27 +1055,24 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1746
1055
  parts.push(contextText);
1747
1056
 
1748
1057
  const cfg = config(ctx);
1749
- const cfgForPets = cfg;
1750
- const shouldRenderPet = shouldRenderPetInFooter(cfgForPets);
1751
- const requestedPetPlacement = cfgForPets.pets.placement;
1058
+ const shouldRenderPet = petController.shouldRenderInFooter(cfg);
1059
+ const requestedPetPlacement = cfg.pets.placement;
1752
1060
  const requestedPetSizeCells = petSizeCellsForPlacement(
1753
1061
  requestedPetPlacement,
1754
- cfgForPets.pets.sizeCells,
1062
+ cfg.pets.sizeCells,
1755
1063
  );
1756
1064
  const inlinePet = Boolean(
1757
1065
  shouldRenderPet &&
1758
- pet &&
1066
+ petController.loadedPet &&
1759
1067
  isInlinePetPlacement(requestedPetPlacement) &&
1760
1068
  width >= requestedPetSizeCells + 32,
1761
1069
  );
1762
- const petRenderSizeCells = inlinePet ? requestedPetSizeCells : cfgForPets.pets.sizeCells;
1070
+ const petRenderSizeCells = inlinePet ? requestedPetSizeCells : cfg.pets.sizeCells;
1763
1071
  const petColumnWidth = Math.min(petRenderSizeCells, Math.max(1, width - 1));
1764
1072
  const footerTextWidth = inlinePet ? Math.max(1, width - petColumnWidth - 2) : width;
1765
1073
 
1766
- let usageLine: string | undefined;
1767
- if (usageSnapshot && cfg.usage.enabled && isOpenAISubscriptionModel(ctx, cfg)) {
1768
- usageLine = theme.fg("dim", formatUsageSnapshot(usageSnapshot, cfg.usage));
1769
- }
1074
+ const usageStatusLine = usageController.statusLine(ctx, cfg, usingSubscription);
1075
+ const usageLine = usageStatusLine ? theme.fg("dim", usageStatusLine) : undefined;
1770
1076
 
1771
1077
  let statsLeft = parts.join(" ");
1772
1078
  let statsLeftWidth = visibleWidth(statsLeft);
@@ -1778,7 +1084,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1778
1084
  const modelName = ctx.model?.id || "no-model";
1779
1085
  const thinkingLevel = pi.getThinkingLevel();
1780
1086
  const fastSuffix =
1781
- active && supportsFast(ctx, config(ctx).supportedModels) ? " fast" : "";
1087
+ fastController.active && supportsFast(ctx, cfg.supportedModels) ? " fast" : "";
1782
1088
  let rightWithoutProvider = modelName;
1783
1089
  if (ctx.model?.reasoning) {
1784
1090
  rightWithoutProvider =
@@ -1835,65 +1141,21 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1835
1141
  textLines.push(truncateToWidth(statusLine, footerTextWidth, theme.fg("dim", "...")));
1836
1142
  }
1837
1143
 
1838
- const petLines: string[] = [];
1839
- if (shouldRenderPet) {
1840
- if (pet) {
1841
- const { state: petState, elapsedMs } = currentPetAnimation(ctx, cfgForPets);
1842
- if (freezePetFrame) {
1843
- petLines.push(
1844
- ...petPlaceholderLines(
1845
- pet,
1846
- petState,
1847
- elapsedMs,
1848
- petColumnWidth,
1849
- petRenderSizeCells,
1850
- ),
1851
- );
1852
- } else {
1853
- const { frameIndex } = petFrameInfo(pet, petState, elapsedMs);
1854
- const cacheKey = petRenderCacheKey(
1855
- pet,
1856
- petState,
1857
- frameIndex,
1858
- requestedPetPlacement,
1859
- petColumnWidth,
1860
- petRenderSizeCells,
1861
- );
1862
- const cachedPetLines = petRenderCache.get(cacheKey);
1863
- const imageProtocol = getCapabilities().images;
1864
- if (cachedPetLines) {
1865
- petLines.push(...cachedPetLines);
1866
- } else {
1867
- const renderedPetLines = renderCodexPetFrame(
1868
- pet,
1869
- petState,
1870
- petColumnWidth,
1871
- theme,
1872
- {
1873
- sizeCells: petRenderSizeCells,
1874
- imageId: petImageId,
1875
- now: elapsedMs,
1876
- durationMultiplier: 1,
1877
- kittyManager: petKittyManager,
1878
- },
1879
- );
1880
- petLines.push(...renderedPetLines);
1881
- if (renderedPetLines.length > 0) {
1882
- if (imageProtocol !== null && imageProtocol !== "kitty")
1883
- rememberPetRender(cacheKey, renderedPetLines);
1884
- }
1885
- }
1886
- }
1887
- } else if (petError) {
1888
- petLines.push(truncateToWidth(theme.fg("warning", `pet: ${petError}`), width, "..."));
1889
- }
1890
- }
1144
+ const petLines = petController.renderPetLines(ctx, cfg, {
1145
+ shouldRenderPet,
1146
+ freezePetFrame,
1147
+ requestedPetPlacement,
1148
+ petColumnWidth,
1149
+ petRenderSizeCells,
1150
+ width,
1151
+ theme,
1152
+ });
1891
1153
 
1892
1154
  if (!shouldRenderPet || petLines.length === 0)
1893
- return withPendingPetKittyCleanup(textLines);
1155
+ return petController.withPendingKittyCleanup(textLines);
1894
1156
 
1895
1157
  if (inlinePet) {
1896
- return withPendingPetKittyCleanup(
1158
+ return petController.withPendingKittyCleanup(
1897
1159
  combineInlinePetFooter(
1898
1160
  petLines,
1899
1161
  textLines,
@@ -1904,16 +1166,16 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1904
1166
  );
1905
1167
  }
1906
1168
 
1907
- if (requestedPetPlacement === "habitat" && pet) {
1908
- const label = ` ${pet.pet.name} `;
1169
+ if (requestedPetPlacement === "habitat" && petController.loadedPet) {
1170
+ const label = ` ${petController.loadedPet.pet.name} `;
1909
1171
  const divider = theme.fg(
1910
1172
  "dim",
1911
1173
  truncateToWidth(`─${label}${"─".repeat(width)}`, width, ""),
1912
1174
  );
1913
- return withPendingPetKittyCleanup([divider, ...petLines, ...textLines]);
1175
+ return petController.withPendingKittyCleanup([divider, ...petLines, ...textLines]);
1914
1176
  }
1915
1177
 
1916
- return withPendingPetKittyCleanup([...petLines, ...textLines]);
1178
+ return petController.withPendingKittyCleanup([...petLines, ...textLines]);
1917
1179
  },
1918
1180
  };
1919
1181
  });
@@ -1921,10 +1183,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1921
1183
 
1922
1184
  function clearFooter(ctx: ExtensionContext): void {
1923
1185
  if (!footerInstalled) return;
1924
- disposePetKittyNow();
1186
+ petController.disposeKittyNow();
1925
1187
  ctx.ui.setFooter(undefined);
1926
1188
  footerInstalled = false;
1927
- requestFooterRender = undefined;
1189
+ petController.setFooterRenderRequest(undefined);
1928
1190
  }
1929
1191
 
1930
1192
  function setStatus(ctx: ExtensionContext, text: string | undefined): void {
@@ -1935,13 +1197,20 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1935
1197
 
1936
1198
  function updateFooter(ctx: ExtensionContext): void {
1937
1199
  const cfg = config(ctx);
1938
- const resizeFrozen = petResizeFrozen();
1939
- const shouldAnimatePet = shouldLoadPetForConfig(cfg);
1940
- const shouldRenderPet = shouldRenderPetInFooter(cfg);
1941
- stopPetAnimation();
1942
- if (shouldAnimatePet && !resizeFrozen) startPetAnimation(ctx);
1943
- if (!resizeFrozen && shouldRunIdleEmotes(ctx, cfg)) schedulePetIdleEmote(ctx, cfg);
1944
- else stopPetIdleEmotes();
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
+
1212
+ petController.updateActivity(ctx, cfg);
1213
+ const shouldRenderPet = petController.shouldRenderInFooter(cfg);
1945
1214
 
1946
1215
  if (cfg.footer.mode === "replace" || shouldRenderPet) {
1947
1216
  setStatus(ctx, undefined);
@@ -1956,135 +1225,114 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1956
1225
  return;
1957
1226
  }
1958
1227
 
1959
- const fast =
1960
- active && supportsFast(ctx, cfg.supportedModels)
1961
- ? `${ctx.model?.id ?? "model"} fast`
1962
- : undefined;
1963
- const usage =
1964
- usageSnapshot && cfg.usage.enabled && isOpenAISubscriptionModel(ctx, cfg)
1965
- ? formatUsageSnapshot(usageSnapshot, cfg.usage)
1966
- : undefined;
1228
+ const fast = fastController.statusSegment(ctx, cfg);
1229
+ const usage = usageController.statusLine(ctx, cfg);
1967
1230
  setStatus(ctx, [fast, usage].filter(Boolean).join(" | ") || undefined);
1968
1231
  }
1969
1232
 
1970
1233
  pi.on("session_start", (_event, ctx) => {
1234
+ invalidateContextUsage();
1235
+ invalidateSessionName();
1971
1236
  const nextConfig = refresh(ctx);
1972
- desiredActive = nextConfig.persistState ? nextConfig.desiredActive : false;
1973
- if (pi.getFlag(FLAG) === true) desiredActive = true;
1974
- applyDesiredFastState(ctx, nextConfig);
1975
- if (desiredActive !== nextConfig.desiredActive || active !== nextConfig.active)
1237
+ fastController.initializeForSession(ctx, nextConfig, pi.getFlag(FLAG) === true);
1238
+ if (
1239
+ fastController.desiredActive !== nextConfig.desiredActive ||
1240
+ fastController.active !== nextConfig.active
1241
+ )
1976
1242
  persist(nextConfig);
1977
- if (desiredActive && !active) {
1978
- ctx.ui.notify(
1979
- `Fast mode requested, but ${currentModelKey(ctx)} is unsupported. It will activate automatically when you switch to a supported model: ${modelList(nextConfig.supportedModels)}.`,
1980
- "warning",
1981
- );
1243
+ if (fastController.desiredActive && !fastController.active) {
1244
+ ctx.ui.notify(fastController.unsupportedRequestMessage(ctx, nextConfig), "warning");
1982
1245
  }
1983
- installPetResizeGuard(ctx);
1246
+ if (hasTerminalUI(ctx)) petController.installResizeGuard(ctx);
1984
1247
  refreshFooterTotals(ctx);
1985
1248
  updateFooter(ctx);
1986
- if (nextConfig.pets.enabled) void refreshPet(ctx, nextConfig);
1987
- startUsageRefresh(ctx);
1988
- if (active)
1989
- ctx.ui.notify(stateText(ctx, desiredActive, active, nextConfig.supportedModels), "info");
1249
+ if (hasTerminalUI(ctx) && nextConfig.pets.enabled) void petController.refresh(ctx, nextConfig);
1250
+ usageController.start(ctx);
1251
+ if (fastController.active) ctx.ui.notify(fastController.stateText(ctx, nextConfig), "info");
1990
1252
  });
1991
1253
 
1992
1254
  pi.on("agent_start", (_event, ctx) => {
1993
- activeToolCallIds.clear();
1994
- clearPetFlash();
1995
- petRuntimeState = config(ctx).pets.thinkingState;
1255
+ invalidateContextUsage();
1256
+ petController.agentStart(ctx);
1996
1257
  updateFooter(ctx);
1997
1258
  });
1998
1259
 
1999
1260
  pi.on("tool_execution_start", (event, ctx) => {
2000
- activeToolCallIds.add(event.toolCallId);
2001
- petRuntimeState = config(ctx).pets.toolState;
1261
+ petController.toolStart(ctx, event.toolCallId);
2002
1262
  updateFooter(ctx);
2003
1263
  });
2004
1264
 
2005
1265
  pi.on("tool_execution_end", (event, ctx) => {
2006
- activeToolCallIds.delete(event.toolCallId);
2007
- const cfg = config(ctx);
2008
- petRuntimeState = activeToolCallIds.size > 0 ? cfg.pets.toolState : cfg.pets.thinkingState;
2009
- if (event.isError) {
2010
- playPetFlash(ctx, cfg.pets.failedToolState, cfg);
2011
- return;
2012
- }
2013
- updateFooter(ctx);
1266
+ petController.toolEnd(ctx, event.toolCallId, event.isError);
1267
+ if (!event.isError) updateFooter(ctx);
2014
1268
  });
2015
1269
 
2016
1270
  pi.on("agent_end", (_event, ctx) => {
2017
- activeToolCallIds.clear();
2018
- petRuntimeState = "idle";
1271
+ petController.agentEnd();
2019
1272
  updateFooter(ctx);
2020
1273
  });
2021
1274
 
2022
- pi.on("turn_end", (_event, ctx) => {
2023
- refreshFooterTotals(ctx);
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);
2024
1284
  updateFooter(ctx);
2025
- void refreshUsage(ctx);
1285
+ void usageController.refresh(ctx);
2026
1286
  });
2027
1287
 
2028
1288
  pi.on("session_compact", (_event, ctx) => {
1289
+ invalidateContextUsage();
2029
1290
  refreshFooterTotals(ctx);
2030
- queuePetKittyCleanup();
2031
- resetPetRenderCache();
1291
+ petController.queueKittyCleanup();
1292
+ petController.resetRenderCache();
2032
1293
  updateFooter(ctx);
2033
1294
  });
2034
1295
 
2035
1296
  pi.on("session_tree", (_event, ctx) => {
1297
+ invalidateContextUsage();
2036
1298
  refreshFooterTotals(ctx);
2037
- queuePetKittyCleanup();
2038
- resetPetRenderCache();
1299
+ petController.queueKittyCleanup();
1300
+ petController.resetRenderCache();
2039
1301
  updateFooter(ctx);
2040
1302
  });
2041
1303
 
2042
1304
  pi.on("model_select", (event, ctx) => {
1305
+ invalidateContextUsage();
2043
1306
  const cfg = config(ctx);
2044
- const wasActive = active;
2045
- applyDesiredFastState(ctx, cfg);
2046
- if (active !== wasActive) {
1307
+ const wasActive = fastController.active;
1308
+ fastController.applyDesiredState(ctx, cfg);
1309
+ if (fastController.active !== wasActive) {
2047
1310
  persist(cfg);
2048
1311
  ctx.ui.notify(
2049
- active
2050
- ? stateText(ctx, desiredActive, active, cfg.supportedModels)
2051
- : `Fast mode inactive for unsupported model ${currentModelKey(ctx)}.`,
2052
- active ? "info" : "warning",
1312
+ fastController.active
1313
+ ? fastController.stateText(ctx, cfg)
1314
+ : fastController.inactiveForModelMessage(ctx),
1315
+ fastController.active ? "info" : "warning",
2053
1316
  );
2054
1317
  }
2055
1318
  updateFooter(ctx);
2056
- void refreshUsage(ctx, event.model.id);
1319
+ void usageController.refresh(ctx, event.model.id, { force: true });
2057
1320
  });
2058
1321
 
2059
1322
  pi.on("session_shutdown", () => {
2060
- shuttingDown = true;
2061
- queuedUsageRefresh = undefined;
2062
- queuedPetRefresh = undefined;
2063
- petLoadingKey = undefined;
2064
- petLoadNotify = false;
2065
- usageAbortController?.abort();
2066
- usageAbortController = undefined;
2067
- if (usageTimer) clearInterval(usageTimer);
2068
- usageTimer = undefined;
2069
- activeToolCallIds.clear();
2070
- uninstallPetResizeGuard();
2071
- disposePetKittyNow();
2072
- resetPetRenderCache();
2073
- clearPetFlash();
2074
- stopPetIdleEmotes();
2075
- stopPetAnimation();
2076
- stopPendingPetRenderRequest();
1323
+ invalidateContextUsage();
1324
+ invalidateSessionName();
1325
+ usageController.shutdown();
1326
+ petController.shutdown();
2077
1327
  });
2078
1328
 
2079
1329
  pi.on("before_provider_request", (event, ctx) => {
2080
- const nextConfig = config(ctx);
2081
- if (!active || !supportsFast(ctx, nextConfig.supportedModels) || !isRecord(event.payload))
2082
- return;
2083
- lastInjectedAt = Date.now();
2084
- lastInjectedModel = currentModelKey(ctx);
2085
- lastInjectedTier = SERVICE_TIER;
2086
- return { ...event.payload, service_tier: SERVICE_TIER };
1330
+ return fastController.injectProviderPayload(event, ctx, config(ctx));
2087
1331
  });
1332
+
1333
+ pi.on("message_start", invalidateContextUsage);
1334
+ pi.on("message_update", invalidateContextUsage);
1335
+ pi.on("message_end", invalidateContextUsage);
2088
1336
  }
2089
1337
 
2090
1338
  export const _test = {
@@ -2095,6 +1343,7 @@ export const _test = {
2095
1343
  DEFAULT_PET_CONFIG,
2096
1344
  SERVICE_TIER,
2097
1345
  configPaths,
1346
+ abbreviateHomePath,
2098
1347
  parseModelKey,
2099
1348
  normalizeModelKeys,
2100
1349
  parseModels,
@@ -2112,6 +1361,7 @@ export const _test = {
2112
1361
  formatPercent,
2113
1362
  formatUsageSnapshot,
2114
1363
  readCodexAuth,
1364
+ textPanel,
2115
1365
  imageTest: _imageTest,
2116
1366
  petsTest: _petsTest,
2117
1367
  };