pi-better-openai 0.1.13 → 0.1.15
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 +214 -74
- package/package.json +1 -1
- package/src/pets.ts +211 -68
package/index.ts
CHANGED
|
@@ -66,10 +66,9 @@ import {
|
|
|
66
66
|
listCodexPets,
|
|
67
67
|
nextAnimationFrameDelayMs,
|
|
68
68
|
PET_ANIMATION_ROWS,
|
|
69
|
-
|
|
69
|
+
CodexPetKittyManager,
|
|
70
70
|
registerOpenAIPets,
|
|
71
71
|
renderCodexPetFrame,
|
|
72
|
-
resetCodexPetKittyCache,
|
|
73
72
|
_petsTest,
|
|
74
73
|
} from "./src/pets.ts";
|
|
75
74
|
|
|
@@ -207,7 +206,10 @@ function formatPetSelectPrompt(
|
|
|
207
206
|
lines.push(
|
|
208
207
|
"",
|
|
209
208
|
"Not ready:",
|
|
210
|
-
...brokenPets.map(
|
|
209
|
+
...brokenPets.map(
|
|
210
|
+
(pet) =>
|
|
211
|
+
`- ${pet.slug} (${pet.name}) — ${pet.spritesheetIssue ?? `missing ${pet.spritesheetPath}`}`,
|
|
212
|
+
),
|
|
211
213
|
);
|
|
212
214
|
}
|
|
213
215
|
return { message: lines.join("\n"), level: "info" };
|
|
@@ -391,9 +393,9 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
391
393
|
let petAnimationState: PetState | undefined;
|
|
392
394
|
let petAnimationStartedAt = Date.now();
|
|
393
395
|
const petRenderCache = new Map<string, string[]>();
|
|
394
|
-
const pendingPetKittyCleanupImageIds = new Set<number>();
|
|
395
396
|
const activeToolCallIds = new Set<string>();
|
|
396
397
|
const petImageId = 0x70657401;
|
|
398
|
+
const petKittyManager = new CodexPetKittyManager(petImageId);
|
|
397
399
|
|
|
398
400
|
function refresh(ctx: ExtensionContext): ResolvedConfig {
|
|
399
401
|
cachedConfig = resolveConfig(ctx.cwd || process.cwd());
|
|
@@ -455,22 +457,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
455
457
|
}
|
|
456
458
|
|
|
457
459
|
function queuePetKittyCleanup(target = pet): void {
|
|
458
|
-
|
|
459
|
-
for (const frames of Object.values(target.states)) {
|
|
460
|
-
for (const frame of frames) {
|
|
461
|
-
if (frame.kittyImageId !== undefined)
|
|
462
|
-
pendingPetKittyCleanupImageIds.add(frame.kittyImageId);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
460
|
+
petKittyManager.invalidate(target);
|
|
465
461
|
}
|
|
466
462
|
|
|
467
463
|
function takePetKittyCleanupSequence(): string {
|
|
468
|
-
|
|
469
|
-
const sequence = Array.from(pendingPetKittyCleanupImageIds)
|
|
470
|
-
.map((imageId) => deleteCodexPetKittyPlacement(imageId))
|
|
471
|
-
.join("");
|
|
472
|
-
pendingPetKittyCleanupImageIds.clear();
|
|
473
|
-
return sequence;
|
|
464
|
+
return petKittyManager.takeCleanupSequence();
|
|
474
465
|
}
|
|
475
466
|
|
|
476
467
|
function withPendingPetKittyCleanup(lines: string[]): string[] {
|
|
@@ -480,11 +471,18 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
480
471
|
return [`${sequence}\x1b[0m${lines[0]}\x1b[0m`, ...lines.slice(1)];
|
|
481
472
|
}
|
|
482
473
|
|
|
483
|
-
function
|
|
484
|
-
const sequence = takePetKittyCleanupSequence();
|
|
474
|
+
function writePetKittySequence(sequence: string): void {
|
|
485
475
|
if (sequence && process.stdout.isTTY) process.stdout.write(sequence);
|
|
486
476
|
}
|
|
487
477
|
|
|
478
|
+
function flushPetKittyCleanupNow(): void {
|
|
479
|
+
writePetKittySequence(takePetKittyCleanupSequence());
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function disposePetKittyNow(target = pet): void {
|
|
483
|
+
writePetKittySequence(petKittyManager.dispose(target));
|
|
484
|
+
}
|
|
485
|
+
|
|
488
486
|
function rememberPetRender(key: string, lines: string[]): void {
|
|
489
487
|
petRenderCache.set(key, lines);
|
|
490
488
|
while (petRenderCache.size > PET_RENDER_CACHE_LIMIT) {
|
|
@@ -514,7 +512,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
514
512
|
petResizeTimer = setTimeout(() => {
|
|
515
513
|
petResizeTimer = undefined;
|
|
516
514
|
petResizeFreezeUntil = 0;
|
|
517
|
-
|
|
515
|
+
petKittyManager.resetForResize(pet);
|
|
518
516
|
resetPetRenderCache();
|
|
519
517
|
updateFooter(ctx);
|
|
520
518
|
}, PET_RESIZE_FREEZE_MS);
|
|
@@ -658,7 +656,6 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
658
656
|
if (!shouldLoadPetForConfig(cfg)) {
|
|
659
657
|
queuedPetRefresh = undefined;
|
|
660
658
|
queuePetKittyCleanup();
|
|
661
|
-
resetCodexPetKittyCache(pet, petImageId);
|
|
662
659
|
pet = undefined;
|
|
663
660
|
petError = undefined;
|
|
664
661
|
petLoadKey = undefined;
|
|
@@ -694,10 +691,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
694
691
|
sizeCells: cfg.pets.sizeCells,
|
|
695
692
|
});
|
|
696
693
|
if (!shouldApplyLoadResult()) return;
|
|
697
|
-
if (previousPet)
|
|
698
|
-
queuePetKittyCleanup(previousPet);
|
|
699
|
-
resetCodexPetKittyCache(previousPet, petImageId);
|
|
700
|
-
}
|
|
694
|
+
if (previousPet) queuePetKittyCleanup(previousPet);
|
|
701
695
|
pet = loadedPet;
|
|
702
696
|
const petIssue = pet
|
|
703
697
|
? undefined
|
|
@@ -716,10 +710,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
716
710
|
}
|
|
717
711
|
} catch (error) {
|
|
718
712
|
if (!shouldApplyLoadResult()) return;
|
|
719
|
-
if (previousPet)
|
|
720
|
-
queuePetKittyCleanup(previousPet);
|
|
721
|
-
resetCodexPetKittyCache(previousPet, petImageId);
|
|
722
|
-
}
|
|
713
|
+
if (previousPet) queuePetKittyCleanup(previousPet);
|
|
723
714
|
pet = undefined;
|
|
724
715
|
petLoadKey = key;
|
|
725
716
|
petError = error instanceof Error ? error.message : String(error);
|
|
@@ -1038,6 +1029,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1038
1029
|
imageId: petImageId,
|
|
1039
1030
|
now: elapsedMs,
|
|
1040
1031
|
durationMultiplier: 1,
|
|
1032
|
+
kittyManager: petKittyManager,
|
|
1041
1033
|
}),
|
|
1042
1034
|
];
|
|
1043
1035
|
}
|
|
@@ -1057,6 +1049,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1057
1049
|
let selectedIndex = 0;
|
|
1058
1050
|
let searchQuery = "";
|
|
1059
1051
|
let closed = false;
|
|
1052
|
+
let submenuComponent: ReturnType<NonNullable<SettingsPickerItem["submenu"]>> | undefined;
|
|
1053
|
+
let submenuItemIndex: number | undefined;
|
|
1060
1054
|
|
|
1061
1055
|
function currentItems(): SettingsPickerItem[] {
|
|
1062
1056
|
const allItems = items();
|
|
@@ -1078,6 +1072,13 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1078
1072
|
done();
|
|
1079
1073
|
}
|
|
1080
1074
|
|
|
1075
|
+
function closeNestedSubmenu(): void {
|
|
1076
|
+
submenuComponent = undefined;
|
|
1077
|
+
if (submenuItemIndex !== undefined) selectedIndex = submenuItemIndex;
|
|
1078
|
+
submenuItemIndex = undefined;
|
|
1079
|
+
options?.onSelection?.(selectedItem());
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1081
1082
|
function cycleSelected(direction: 1 | -1 = 1): void {
|
|
1082
1083
|
const item = selectedItem();
|
|
1083
1084
|
if (!item?.values?.length) return;
|
|
@@ -1090,8 +1091,23 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1090
1091
|
options?.onSelection?.(selectedItem());
|
|
1091
1092
|
}
|
|
1092
1093
|
|
|
1094
|
+
function activateSelected(): void {
|
|
1095
|
+
const item = selectedItem();
|
|
1096
|
+
if (!item) return;
|
|
1097
|
+
if (item.submenu) {
|
|
1098
|
+
submenuItemIndex = selectedIndex;
|
|
1099
|
+
submenuComponent = item.submenu(item.currentValue, (selectedValue?: string) => {
|
|
1100
|
+
if (selectedValue !== undefined) writeSetting(ctx, item.id, selectedValue);
|
|
1101
|
+
closeNestedSubmenu();
|
|
1102
|
+
});
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
cycleSelected(1);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1093
1108
|
return {
|
|
1094
1109
|
render(width: number) {
|
|
1110
|
+
if (submenuComponent) return submenuComponent.render(width);
|
|
1095
1111
|
const current = currentItems();
|
|
1096
1112
|
const selected = selectedItem();
|
|
1097
1113
|
if (!closed) options?.onSelection?.(selected);
|
|
@@ -1149,8 +1165,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1149
1165
|
);
|
|
1150
1166
|
return lines;
|
|
1151
1167
|
},
|
|
1152
|
-
invalidate() {
|
|
1168
|
+
invalidate() {
|
|
1169
|
+
submenuComponent?.invalidate();
|
|
1170
|
+
},
|
|
1153
1171
|
handleInput(data: string) {
|
|
1172
|
+
if (submenuComponent) {
|
|
1173
|
+
submenuComponent.handleInput?.(data);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1154
1176
|
const current = currentItems();
|
|
1155
1177
|
if (current.length === 0) {
|
|
1156
1178
|
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) close();
|
|
@@ -1168,7 +1190,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1168
1190
|
matchesKey(data, Key.space) ||
|
|
1169
1191
|
data === " "
|
|
1170
1192
|
)
|
|
1171
|
-
|
|
1193
|
+
activateSelected();
|
|
1172
1194
|
else if (matchesKey(data, Key.left)) cycleSelected(-1);
|
|
1173
1195
|
else if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) close();
|
|
1174
1196
|
else if (matchesKey(data, Key.backspace)) {
|
|
@@ -1183,7 +1205,35 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1183
1205
|
};
|
|
1184
1206
|
}
|
|
1185
1207
|
|
|
1186
|
-
function
|
|
1208
|
+
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";
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function usageSettingsSummary(cfg: ResolvedConfig): string {
|
|
1216
|
+
return cfg.usage.enabled
|
|
1217
|
+
? `enabled · ${Math.round(cfg.usage.refreshIntervalMs / 1000)}s`
|
|
1218
|
+
: "disabled";
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function imageSettingsSummary(cfg: ResolvedConfig): string {
|
|
1222
|
+
return cfg.image.enabled
|
|
1223
|
+
? `enabled · ${cfg.image.defaultModel} · ${cfg.image.defaultSave}/${cfg.image.outputFormat}`
|
|
1224
|
+
: "disabled";
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function petSettingsSummary(cfg: ResolvedConfig): string {
|
|
1228
|
+
const selected = cfg.pets.slug || (cfg.pets.enabled ? "first ready" : PET_EMPTY_VALUE);
|
|
1229
|
+
const status = cfg.pets.enabled ? "enabled" : "disabled";
|
|
1230
|
+
return `${status} · ${selected} · ${cfg.pets.placement}`;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function buildFastSettingsItems(
|
|
1234
|
+
ctx: ExtensionContext,
|
|
1235
|
+
cfg: ResolvedConfig,
|
|
1236
|
+
): SettingsPickerItem[] {
|
|
1187
1237
|
return [
|
|
1188
1238
|
{
|
|
1189
1239
|
id: "fast.enabled",
|
|
@@ -1199,6 +1249,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1199
1249
|
values: ["true", "false"],
|
|
1200
1250
|
description: "Remember fast-mode state across sessions.",
|
|
1201
1251
|
},
|
|
1252
|
+
];
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function buildFooterSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
|
|
1256
|
+
return [
|
|
1202
1257
|
{
|
|
1203
1258
|
id: "footer.mode",
|
|
1204
1259
|
label: "Footer mode",
|
|
@@ -1207,36 +1262,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1207
1262
|
description:
|
|
1208
1263
|
"replace = custom footer, status = pi footer plus status line, off = no Better OpenAI footer/status unless Footer pet is enabled.",
|
|
1209
1264
|
},
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
submenu: (_value, done) => {
|
|
1216
|
-
setPetSettingsPreviewActive(ctx, true);
|
|
1217
|
-
return settingsSubmenu(
|
|
1218
|
-
"Footer pet settings",
|
|
1219
|
-
() => buildPetSettingsItems(config(ctx)),
|
|
1220
|
-
ctx,
|
|
1221
|
-
() => done(),
|
|
1222
|
-
{
|
|
1223
|
-
onSelection: (item) => {
|
|
1224
|
-
const previewState = petPreviewFromItem(item);
|
|
1225
|
-
if (previewState !== petPreviewState) {
|
|
1226
|
-
petPreviewState = previewState;
|
|
1227
|
-
updateFooter(ctx);
|
|
1228
|
-
}
|
|
1229
|
-
},
|
|
1230
|
-
onClose: () => {
|
|
1231
|
-
petPreviewState = undefined;
|
|
1232
|
-
setPetSettingsPreviewActive(ctx, false);
|
|
1233
|
-
updateFooter(ctx);
|
|
1234
|
-
},
|
|
1235
|
-
renderExtra: (width) => renderPetSettingsPreview(ctx, width),
|
|
1236
|
-
},
|
|
1237
|
-
);
|
|
1238
|
-
},
|
|
1239
|
-
},
|
|
1265
|
+
];
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function buildUsageSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
|
|
1269
|
+
return [
|
|
1240
1270
|
{
|
|
1241
1271
|
id: "usage.enabled",
|
|
1242
1272
|
label: "Usage display",
|
|
@@ -1265,6 +1295,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1265
1295
|
values: ["true", "false"],
|
|
1266
1296
|
description: "Include compact reset countdowns and local reset times.",
|
|
1267
1297
|
},
|
|
1298
|
+
];
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function buildImageSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
|
|
1302
|
+
return [
|
|
1268
1303
|
{
|
|
1269
1304
|
id: "image.enabled",
|
|
1270
1305
|
label: "Image tool",
|
|
@@ -1301,6 +1336,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1301
1336
|
values: ["30000", "60000", "120000", "180000", "300000"],
|
|
1302
1337
|
description: "Image request timeout in milliseconds.",
|
|
1303
1338
|
},
|
|
1339
|
+
];
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function buildDiagnosticsSettingsItems(
|
|
1343
|
+
ctx: ExtensionContext,
|
|
1344
|
+
cfg: ResolvedConfig,
|
|
1345
|
+
): SettingsPickerItem[] {
|
|
1346
|
+
return [
|
|
1304
1347
|
{
|
|
1305
1348
|
id: "debug",
|
|
1306
1349
|
label: "Debug info",
|
|
@@ -1330,6 +1373,106 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1330
1373
|
];
|
|
1331
1374
|
}
|
|
1332
1375
|
|
|
1376
|
+
function buildSettingsSections(ctx: ExtensionContext, cfg: ResolvedConfig): SettingsPickerItem[] {
|
|
1377
|
+
return [
|
|
1378
|
+
{
|
|
1379
|
+
id: "section.fast",
|
|
1380
|
+
label: "Fast mode",
|
|
1381
|
+
currentValue: fastSettingsSummary(ctx, cfg),
|
|
1382
|
+
description: "Configure OpenAI fast mode and persistence.",
|
|
1383
|
+
submenu: (_value, done) =>
|
|
1384
|
+
settingsSubmenu(
|
|
1385
|
+
"Fast mode settings",
|
|
1386
|
+
() => buildFastSettingsItems(ctx, config(ctx)),
|
|
1387
|
+
ctx,
|
|
1388
|
+
() => done(fastSettingsSummary(ctx, config(ctx))),
|
|
1389
|
+
),
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
id: "section.footer",
|
|
1393
|
+
label: "Footer",
|
|
1394
|
+
currentValue: cfg.footer.mode,
|
|
1395
|
+
description: "Configure Better OpenAI footer ownership.",
|
|
1396
|
+
submenu: (_value, done) =>
|
|
1397
|
+
settingsSubmenu(
|
|
1398
|
+
"Footer settings",
|
|
1399
|
+
() => buildFooterSettingsItems(config(ctx)),
|
|
1400
|
+
ctx,
|
|
1401
|
+
() => done(config(ctx).footer.mode),
|
|
1402
|
+
),
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
id: "section.usage",
|
|
1406
|
+
label: "Usage",
|
|
1407
|
+
currentValue: usageSettingsSummary(cfg),
|
|
1408
|
+
description: "Configure subscription usage fetching and display.",
|
|
1409
|
+
submenu: (_value, done) =>
|
|
1410
|
+
settingsSubmenu(
|
|
1411
|
+
"Usage settings",
|
|
1412
|
+
() => buildUsageSettingsItems(config(ctx)),
|
|
1413
|
+
ctx,
|
|
1414
|
+
() => done(usageSettingsSummary(config(ctx))),
|
|
1415
|
+
),
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
id: "section.image",
|
|
1419
|
+
label: "Image tool",
|
|
1420
|
+
currentValue: imageSettingsSummary(cfg),
|
|
1421
|
+
description: "Configure OpenAI image generation defaults.",
|
|
1422
|
+
submenu: (_value, done) =>
|
|
1423
|
+
settingsSubmenu(
|
|
1424
|
+
"Image tool settings",
|
|
1425
|
+
() => buildImageSettingsItems(config(ctx)),
|
|
1426
|
+
ctx,
|
|
1427
|
+
() => done(imageSettingsSummary(config(ctx))),
|
|
1428
|
+
),
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
id: "section.pets",
|
|
1432
|
+
label: "Footer pet",
|
|
1433
|
+
currentValue: petSettingsSummary(cfg),
|
|
1434
|
+
description: "Configure footer pet visibility, animation-state mapping, and size.",
|
|
1435
|
+
submenu: (_value, done) => {
|
|
1436
|
+
setPetSettingsPreviewActive(ctx, true);
|
|
1437
|
+
return settingsSubmenu(
|
|
1438
|
+
"Footer pet settings",
|
|
1439
|
+
() => buildPetSettingsItems(config(ctx)),
|
|
1440
|
+
ctx,
|
|
1441
|
+
() => done(petSettingsSummary(config(ctx))),
|
|
1442
|
+
{
|
|
1443
|
+
onSelection: (item) => {
|
|
1444
|
+
const previewState = petPreviewFromItem(item);
|
|
1445
|
+
if (previewState !== petPreviewState) {
|
|
1446
|
+
petPreviewState = previewState;
|
|
1447
|
+
updateFooter(ctx);
|
|
1448
|
+
}
|
|
1449
|
+
},
|
|
1450
|
+
onClose: () => {
|
|
1451
|
+
petPreviewState = undefined;
|
|
1452
|
+
setPetSettingsPreviewActive(ctx, false);
|
|
1453
|
+
updateFooter(ctx);
|
|
1454
|
+
},
|
|
1455
|
+
renderExtra: (width) => renderPetSettingsPreview(ctx, width),
|
|
1456
|
+
},
|
|
1457
|
+
);
|
|
1458
|
+
},
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
id: "section.diagnostics",
|
|
1462
|
+
label: "Diagnostics",
|
|
1463
|
+
currentValue: "debug / config",
|
|
1464
|
+
description: "Show Better OpenAI diagnostics and raw config details.",
|
|
1465
|
+
submenu: (_value, done) =>
|
|
1466
|
+
settingsSubmenu(
|
|
1467
|
+
"Diagnostics",
|
|
1468
|
+
() => buildDiagnosticsSettingsItems(ctx, config(ctx)),
|
|
1469
|
+
ctx,
|
|
1470
|
+
() => done("debug / config"),
|
|
1471
|
+
),
|
|
1472
|
+
},
|
|
1473
|
+
];
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1333
1476
|
function writeSetting(ctx: ExtensionContext, id: string, rawValue: string): void {
|
|
1334
1477
|
const cfg = refresh(ctx);
|
|
1335
1478
|
const current = readRawConfig(cfg.configPath);
|
|
@@ -1422,15 +1565,15 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1422
1565
|
})(),
|
|
1423
1566
|
);
|
|
1424
1567
|
const settingsList = new SettingsList(
|
|
1425
|
-
|
|
1426
|
-
|
|
1568
|
+
buildSettingsSections(ctx, refresh(ctx)),
|
|
1569
|
+
8,
|
|
1427
1570
|
getSettingsListTheme(),
|
|
1428
1571
|
(id, newValue) => {
|
|
1429
|
-
writeSetting(ctx, id, newValue);
|
|
1572
|
+
if (!id.startsWith("section.")) writeSetting(ctx, id, newValue);
|
|
1430
1573
|
settingsList.updateValue(
|
|
1431
1574
|
id,
|
|
1432
|
-
|
|
1433
|
-
newValue,
|
|
1575
|
+
buildSettingsSections(ctx, config(ctx)).find((item) => item.id === id)
|
|
1576
|
+
?.currentValue ?? newValue,
|
|
1434
1577
|
);
|
|
1435
1578
|
tui.requestRender();
|
|
1436
1579
|
},
|
|
@@ -1490,7 +1633,6 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1490
1633
|
tuck: (ctx) => {
|
|
1491
1634
|
writePetConfig(ctx, { enabled: false });
|
|
1492
1635
|
queuePetKittyCleanup();
|
|
1493
|
-
resetCodexPetKittyCache(pet, petImageId);
|
|
1494
1636
|
pet = undefined;
|
|
1495
1637
|
petError = undefined;
|
|
1496
1638
|
resetPetRenderCache();
|
|
@@ -1543,12 +1685,12 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1543
1685
|
stopPetIdleEmotes();
|
|
1544
1686
|
stopPetAnimation();
|
|
1545
1687
|
stopPendingPetRenderRequest();
|
|
1688
|
+
disposePetKittyNow();
|
|
1546
1689
|
footerInstalled = false;
|
|
1547
1690
|
requestFooterRender = undefined;
|
|
1548
1691
|
},
|
|
1549
1692
|
invalidate() {
|
|
1550
1693
|
queuePetKittyCleanup();
|
|
1551
|
-
resetCodexPetKittyCache(pet, petImageId);
|
|
1552
1694
|
resetPetRenderCache();
|
|
1553
1695
|
},
|
|
1554
1696
|
render(width: number): string[] {
|
|
@@ -1732,6 +1874,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1732
1874
|
imageId: petImageId,
|
|
1733
1875
|
now: elapsedMs,
|
|
1734
1876
|
durationMultiplier: 1,
|
|
1877
|
+
kittyManager: petKittyManager,
|
|
1735
1878
|
},
|
|
1736
1879
|
);
|
|
1737
1880
|
petLines.push(...renderedPetLines);
|
|
@@ -1778,7 +1921,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1778
1921
|
|
|
1779
1922
|
function clearFooter(ctx: ExtensionContext): void {
|
|
1780
1923
|
if (!footerInstalled) return;
|
|
1781
|
-
|
|
1924
|
+
disposePetKittyNow();
|
|
1782
1925
|
ctx.ui.setFooter(undefined);
|
|
1783
1926
|
footerInstalled = false;
|
|
1784
1927
|
requestFooterRender = undefined;
|
|
@@ -1885,7 +2028,6 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1885
2028
|
pi.on("session_compact", (_event, ctx) => {
|
|
1886
2029
|
refreshFooterTotals(ctx);
|
|
1887
2030
|
queuePetKittyCleanup();
|
|
1888
|
-
resetCodexPetKittyCache(pet, petImageId);
|
|
1889
2031
|
resetPetRenderCache();
|
|
1890
2032
|
updateFooter(ctx);
|
|
1891
2033
|
});
|
|
@@ -1893,7 +2035,6 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1893
2035
|
pi.on("session_tree", (_event, ctx) => {
|
|
1894
2036
|
refreshFooterTotals(ctx);
|
|
1895
2037
|
queuePetKittyCleanup();
|
|
1896
|
-
resetCodexPetKittyCache(pet, petImageId);
|
|
1897
2038
|
resetPetRenderCache();
|
|
1898
2039
|
updateFooter(ctx);
|
|
1899
2040
|
});
|
|
@@ -1927,8 +2068,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1927
2068
|
usageTimer = undefined;
|
|
1928
2069
|
activeToolCallIds.clear();
|
|
1929
2070
|
uninstallPetResizeGuard();
|
|
1930
|
-
|
|
1931
|
-
flushPetKittyCleanupNow();
|
|
2071
|
+
disposePetKittyNow();
|
|
1932
2072
|
resetPetRenderCache();
|
|
1933
2073
|
clearPetFlash();
|
|
1934
2074
|
stopPetIdleEmotes();
|
package/package.json
CHANGED
package/src/pets.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { access, readdir, readFile } from "node:fs/promises";
|
|
1
|
+
import { constants, type Dirent } from "node:fs";
|
|
2
|
+
import { access, readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, resolve, sep } from "node:path";
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
@@ -21,6 +21,8 @@ const PET_COLUMNS = 8;
|
|
|
21
21
|
const PET_ROWS = 9;
|
|
22
22
|
const DEFAULT_CELL_WIDTH = 192;
|
|
23
23
|
const DEFAULT_CELL_HEIGHT = 208;
|
|
24
|
+
const EXPECTED_ATLAS_WIDTH = DEFAULT_CELL_WIDTH * PET_COLUMNS;
|
|
25
|
+
const EXPECTED_ATLAS_HEIGHT = DEFAULT_CELL_HEIGHT * PET_ROWS;
|
|
24
26
|
|
|
25
27
|
export const PET_ANIMATION_ROWS: Record<PetState, { row: number; durations: number[] }> = {
|
|
26
28
|
idle: { row: 0, durations: [280, 110, 110, 140, 140, 320] },
|
|
@@ -42,6 +44,7 @@ export type CodexPetPackage = {
|
|
|
42
44
|
dir: string;
|
|
43
45
|
spritesheetPath: string;
|
|
44
46
|
hasSpritesheet: boolean;
|
|
47
|
+
spritesheetIssue?: string;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
export type PetFrame = {
|
|
@@ -70,6 +73,29 @@ type ThemeLike = {
|
|
|
70
73
|
fg(color: string, value: string): string;
|
|
71
74
|
};
|
|
72
75
|
|
|
76
|
+
const TERMINAL_ESCAPE_REGEXP = new RegExp(
|
|
77
|
+
String.raw`\u001b(?:\][\s\S]*?(?:\u0007|\u001b\\)|[_PX^][\s\S]*?\u001b\\|\[[0-?]*[ -/]*[@-~]|[@-_][0-?]*[ -/]*[@-~])`,
|
|
78
|
+
"g",
|
|
79
|
+
);
|
|
80
|
+
const CONTROL_CHAR_REGEXP = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]+`, "g");
|
|
81
|
+
|
|
82
|
+
function stripTerminalEscapes(value: string): string {
|
|
83
|
+
return value.replace(TERMINAL_ESCAPE_REGEXP, "");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sanitizePetDisplayText(value: string): string | undefined {
|
|
87
|
+
const sanitized = stripTerminalEscapes(value)
|
|
88
|
+
.replace(CONTROL_CHAR_REGEXP, " ")
|
|
89
|
+
.replace(/\s+/g, " ")
|
|
90
|
+
.trim();
|
|
91
|
+
return sanitized || undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sanitizePetAssetPathText(value: string): string | undefined {
|
|
95
|
+
const sanitized = stripTerminalEscapes(value).replace(CONTROL_CHAR_REGEXP, "").trim();
|
|
96
|
+
return sanitized || undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
73
99
|
export function codexHome(env = process.env, home = homedir()): string {
|
|
74
100
|
return env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
75
101
|
}
|
|
@@ -82,24 +108,21 @@ function petInfoFromJson(
|
|
|
82
108
|
value: unknown,
|
|
83
109
|
fallback: string,
|
|
84
110
|
): { id?: string; name: string; description?: string; spritesheetPath: string } {
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
const fallbackName = sanitizePetDisplayText(fallback) ?? "Unnamed pet";
|
|
112
|
+
if (!isRecord(value)) return { name: fallbackName, spritesheetPath: "spritesheet.webp" };
|
|
113
|
+
const id = typeof value.id === "string" ? sanitizePetDisplayText(value.id) : undefined;
|
|
87
114
|
const displayName =
|
|
88
|
-
typeof value.displayName === "string"
|
|
89
|
-
? value.displayName.trim()
|
|
90
|
-
: undefined;
|
|
115
|
+
typeof value.displayName === "string" ? sanitizePetDisplayText(value.displayName) : undefined;
|
|
91
116
|
const name =
|
|
92
117
|
displayName ??
|
|
93
|
-
(typeof value.name === "string"
|
|
118
|
+
(typeof value.name === "string" ? sanitizePetDisplayText(value.name) : undefined) ??
|
|
94
119
|
id ??
|
|
95
|
-
|
|
120
|
+
fallbackName;
|
|
96
121
|
const description =
|
|
97
|
-
typeof value.description === "string"
|
|
98
|
-
? value.description.trim()
|
|
99
|
-
: undefined;
|
|
122
|
+
typeof value.description === "string" ? sanitizePetDisplayText(value.description) : undefined;
|
|
100
123
|
const spritesheetPath =
|
|
101
|
-
typeof value.spritesheetPath === "string"
|
|
102
|
-
? value.spritesheetPath.
|
|
124
|
+
typeof value.spritesheetPath === "string"
|
|
125
|
+
? (sanitizePetAssetPathText(value.spritesheetPath) ?? "spritesheet.webp")
|
|
103
126
|
: "spritesheet.webp";
|
|
104
127
|
return { id, name, description, spritesheetPath };
|
|
105
128
|
}
|
|
@@ -111,6 +134,48 @@ function resolvePetAssetPath(petDir: string, path: string): string | undefined {
|
|
|
111
134
|
return resolved === resolvedPetDir || resolved.startsWith(prefix) ? resolved : undefined;
|
|
112
135
|
}
|
|
113
136
|
|
|
137
|
+
function formatSharpError(error: unknown): string {
|
|
138
|
+
return (
|
|
139
|
+
sanitizePetDisplayText(error instanceof Error ? error.message : String(error)) ??
|
|
140
|
+
"unknown error"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function validatePetSpritesheet(
|
|
145
|
+
petDir: string,
|
|
146
|
+
spritesheetPath: string,
|
|
147
|
+
): Promise<string | undefined> {
|
|
148
|
+
const resolvedSpritesheetPath = resolvePetAssetPath(petDir, spritesheetPath);
|
|
149
|
+
if (!resolvedSpritesheetPath)
|
|
150
|
+
return `invalid spritesheetPath outside pet folder: ${spritesheetPath}`;
|
|
151
|
+
|
|
152
|
+
let fileStat;
|
|
153
|
+
try {
|
|
154
|
+
fileStat = await stat(resolvedSpritesheetPath);
|
|
155
|
+
} catch {
|
|
156
|
+
return `missing ${spritesheetPath}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!fileStat.isFile()) return `${spritesheetPath} is not a file`;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await access(resolvedSpritesheetPath, constants.R_OK);
|
|
163
|
+
} catch {
|
|
164
|
+
return `${spritesheetPath} is not readable`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const metadata = await sharp(resolvedSpritesheetPath, { animated: false }).metadata();
|
|
169
|
+
if (metadata.width !== EXPECTED_ATLAS_WIDTH || metadata.height !== EXPECTED_ATLAS_HEIGHT) {
|
|
170
|
+
return `invalid atlas dimensions: ${metadata.width ?? "?"}x${metadata.height ?? "?"}; expected ${EXPECTED_ATLAS_WIDTH}x${EXPECTED_ATLAS_HEIGHT}`;
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return `could not read ${spritesheetPath}: ${formatSharpError(error)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
114
179
|
export async function listCodexPets(home = codexHome()): Promise<CodexPetPackage[]> {
|
|
115
180
|
const dir = codexPetsDir(home);
|
|
116
181
|
let entries: Dirent[];
|
|
@@ -131,16 +196,7 @@ export async function listCodexPets(home = codexHome()): Promise<CodexPetPackage
|
|
|
131
196
|
continue;
|
|
132
197
|
}
|
|
133
198
|
const { id, name, description, spritesheetPath } = petInfoFromJson(parsed, entry.name);
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const resolvedSpritesheetPath = resolvePetAssetPath(petDir, spritesheetPath);
|
|
137
|
-
if (resolvedSpritesheetPath) {
|
|
138
|
-
await access(resolvedSpritesheetPath);
|
|
139
|
-
hasSpritesheet = true;
|
|
140
|
-
}
|
|
141
|
-
} catch {
|
|
142
|
-
hasSpritesheet = false;
|
|
143
|
-
}
|
|
199
|
+
const spritesheetIssue = await validatePetSpritesheet(petDir, spritesheetPath);
|
|
144
200
|
pets.push({
|
|
145
201
|
slug: entry.name,
|
|
146
202
|
id,
|
|
@@ -148,7 +204,8 @@ export async function listCodexPets(home = codexHome()): Promise<CodexPetPackage
|
|
|
148
204
|
description,
|
|
149
205
|
dir: petDir,
|
|
150
206
|
spritesheetPath,
|
|
151
|
-
hasSpritesheet,
|
|
207
|
+
hasSpritesheet: spritesheetIssue === undefined,
|
|
208
|
+
spritesheetIssue,
|
|
152
209
|
});
|
|
153
210
|
}
|
|
154
211
|
return pets.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -222,7 +279,7 @@ export function describeCodexPetSelectionIssue(
|
|
|
222
279
|
short: `Pet "${matchingPet.name}" is not ready.`,
|
|
223
280
|
message: [
|
|
224
281
|
`Pet "${matchingPet.name}" (${matchingPet.slug}) exists but is not ready.`,
|
|
225
|
-
`
|
|
282
|
+
`Problem: ${matchingPet.spritesheetIssue ?? `missing ${matchingPet.spritesheetPath}`}`,
|
|
226
283
|
"",
|
|
227
284
|
"Run /pets list for diagnostics, then fix the pet folder:",
|
|
228
285
|
` ${matchingPet.dir}/`,
|
|
@@ -266,7 +323,7 @@ export function formatNoReadyCodexPetsMessage(pets: CodexPetPackage[], home = co
|
|
|
266
323
|
return [
|
|
267
324
|
"Found custom Codex pets, but none are ready.",
|
|
268
325
|
"",
|
|
269
|
-
|
|
326
|
+
`A ready pet needs pet.json and a readable ${EXPECTED_ATLAS_WIDTH}x${EXPECTED_ATLAS_HEIGHT} spritesheet.webp atlas in its pet folder.`,
|
|
270
327
|
"Run /pets list to see what needs fixing, or create a new pet:",
|
|
271
328
|
"",
|
|
272
329
|
formatCodexPetSetupInstructions(home),
|
|
@@ -292,12 +349,9 @@ export async function loadCodexPet(
|
|
|
292
349
|
throw new Error(`Invalid spritesheetPath outside pet folder: ${pet.spritesheetPath}`);
|
|
293
350
|
const source = sharp(spritesheetPath, { animated: false });
|
|
294
351
|
const metadata = await source.metadata();
|
|
295
|
-
if (
|
|
296
|
-
metadata.width !== DEFAULT_CELL_WIDTH * PET_COLUMNS ||
|
|
297
|
-
metadata.height !== DEFAULT_CELL_HEIGHT * PET_ROWS
|
|
298
|
-
) {
|
|
352
|
+
if (metadata.width !== EXPECTED_ATLAS_WIDTH || metadata.height !== EXPECTED_ATLAS_HEIGHT) {
|
|
299
353
|
throw new Error(
|
|
300
|
-
`Invalid Codex pet atlas dimensions: ${metadata.width ?? "?"}x${metadata.height ?? "?"}; expected ${
|
|
354
|
+
`Invalid Codex pet atlas dimensions: ${metadata.width ?? "?"}x${metadata.height ?? "?"}; expected ${EXPECTED_ATLAS_WIDTH}x${EXPECTED_ATLAS_HEIGHT}.`,
|
|
301
355
|
);
|
|
302
356
|
}
|
|
303
357
|
const cellWidth = DEFAULT_CELL_WIDTH;
|
|
@@ -401,20 +455,32 @@ export function nextAnimationFrameDelayMs(
|
|
|
401
455
|
return Math.max(1, Math.ceil(frames[0]?.durationMs ?? 120));
|
|
402
456
|
}
|
|
403
457
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
export function resetCodexPetKittyCache(pet?: LoadedCodexPet, placementImageId?: number): void {
|
|
407
|
-
if (placementImageId !== undefined) previousKittyFrameByPlacement.delete(placementImageId);
|
|
458
|
+
function markCodexPetKittyFramesUnuploaded(pet?: LoadedCodexPet): void {
|
|
408
459
|
if (!pet) return;
|
|
409
460
|
for (const frames of Object.values(pet.states)) {
|
|
410
461
|
for (const frame of frames) frame.kittyUploaded = false;
|
|
411
462
|
}
|
|
412
463
|
}
|
|
413
464
|
|
|
465
|
+
function codexPetKittyImageIds(pet?: LoadedCodexPet): number[] {
|
|
466
|
+
if (!pet) return [];
|
|
467
|
+
const imageIds = new Set<number>();
|
|
468
|
+
for (const frames of Object.values(pet.states)) {
|
|
469
|
+
for (const frame of frames) {
|
|
470
|
+
if (frame.kittyImageId !== undefined) imageIds.add(frame.kittyImageId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return Array.from(imageIds);
|
|
474
|
+
}
|
|
475
|
+
|
|
414
476
|
export function deleteCodexPetKittyPlacement(imageId: number): string {
|
|
415
477
|
return `\x1b_Ga=d,d=i,i=${imageId},p=1,q=2\x1b\\`;
|
|
416
478
|
}
|
|
417
479
|
|
|
480
|
+
export function deleteCodexPetKittyImage(imageId: number): string {
|
|
481
|
+
return `\x1b_Ga=d,d=I,i=${imageId},q=2\x1b\\`;
|
|
482
|
+
}
|
|
483
|
+
|
|
418
484
|
function placeKittyImage(imageId: number, columns: number, rows: number): string {
|
|
419
485
|
return `\x1b_Ga=p,i=${imageId},p=1,c=${columns},r=${rows},q=2,C=1\x1b\\`;
|
|
420
486
|
}
|
|
@@ -446,34 +512,95 @@ function encodeKittyRawRgba(frame: PetFrame, imageId: number): string {
|
|
|
446
512
|
return chunks.join("");
|
|
447
513
|
}
|
|
448
514
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
515
|
+
export class CodexPetKittyManager {
|
|
516
|
+
private previousFrameImageId: number | undefined;
|
|
517
|
+
private readonly uploadedFrameImageIds = new Set<number>();
|
|
518
|
+
private readonly pendingCleanupImageIds = new Set<number>();
|
|
519
|
+
|
|
520
|
+
constructor(readonly placementImageId: number) {}
|
|
521
|
+
|
|
522
|
+
queueCleanup(pet?: LoadedCodexPet): void {
|
|
523
|
+
for (const imageId of codexPetKittyImageIds(pet)) this.pendingCleanupImageIds.add(imageId);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
invalidate(pet?: LoadedCodexPet): void {
|
|
527
|
+
this.queueCleanup(pet);
|
|
528
|
+
this.previousFrameImageId = undefined;
|
|
529
|
+
this.uploadedFrameImageIds.clear();
|
|
530
|
+
markCodexPetKittyFramesUnuploaded(pet);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
resetForResize(pet?: LoadedCodexPet): void {
|
|
534
|
+
this.invalidate(pet);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
dispose(pet?: LoadedCodexPet): string {
|
|
538
|
+
this.invalidate(pet);
|
|
539
|
+
return this.takeCleanupSequence();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
takeCleanupSequence(): string {
|
|
543
|
+
if (this.pendingCleanupImageIds.size === 0) return "";
|
|
544
|
+
const sequence = `${deleteCodexPetKittyPlacement(this.placementImageId)}${Array.from(
|
|
545
|
+
this.pendingCleanupImageIds,
|
|
546
|
+
)
|
|
547
|
+
.map((imageId) => deleteCodexPetKittyImage(imageId))
|
|
548
|
+
.join("")}`;
|
|
549
|
+
this.pendingCleanupImageIds.clear();
|
|
550
|
+
return sequence;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
renderFrame(frame: PetFrame, width: number, options: { sizeCells: number }): string[] {
|
|
554
|
+
const columns = Math.max(1, Math.min(Math.max(1, width - 2), options.sizeCells));
|
|
555
|
+
const rows = calculateImageRows(
|
|
556
|
+
{ widthPx: frame.widthPx, heightPx: frame.heightPx },
|
|
557
|
+
columns,
|
|
558
|
+
getCellDimensions(),
|
|
559
|
+
);
|
|
560
|
+
const frameImageId = frame.kittyImageId ?? this.placementImageId;
|
|
561
|
+
const deletePrevious =
|
|
562
|
+
this.previousFrameImageId !== undefined && this.previousFrameImageId !== frameImageId
|
|
563
|
+
? deleteCodexPetKittyPlacement(this.previousFrameImageId)
|
|
564
|
+
: "";
|
|
565
|
+
const deleteCurrent = deleteCodexPetKittyPlacement(frameImageId);
|
|
566
|
+
const upload = this.uploadedFrameImageIds.has(frameImageId)
|
|
567
|
+
? ""
|
|
568
|
+
: encodeKittyRawRgba(frame, frameImageId);
|
|
569
|
+
this.uploadedFrameImageIds.add(frameImageId);
|
|
570
|
+
frame.kittyUploaded = true;
|
|
571
|
+
this.previousFrameImageId = frameImageId;
|
|
572
|
+
// Recent pi-tui versions automatically free Kitty image IDs they own on changed lines.
|
|
573
|
+
// Pet frames are managed here and intentionally reused across animation loops, so keep
|
|
574
|
+
// the first Kitty command pointed at a harmless footer-placement ID, not a frame ID.
|
|
575
|
+
const hostCleanupSentinel =
|
|
576
|
+
frameImageId !== this.placementImageId
|
|
577
|
+
? deleteCodexPetKittyPlacement(this.placementImageId)
|
|
578
|
+
: "";
|
|
579
|
+
const sequence = `${hostCleanupSentinel}${deletePrevious}${deleteCurrent}${upload}${placeKittyImage(frameImageId, columns, rows)}`;
|
|
580
|
+
const lines: string[] = [];
|
|
581
|
+
for (let i = 0; i < rows - 1; i++) lines.push("");
|
|
582
|
+
const moveUp = rows > 1 ? `\x1b[${rows - 1}A` : "";
|
|
583
|
+
const moveDown = rows > 1 ? `\x1b[${rows - 1}B` : "";
|
|
584
|
+
lines.push(moveUp + sequence + moveDown);
|
|
585
|
+
return lines;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const defaultKittyManagers = new Map<number, CodexPetKittyManager>();
|
|
590
|
+
|
|
591
|
+
function defaultKittyManager(placementImageId: number): CodexPetKittyManager {
|
|
592
|
+
let manager = defaultKittyManagers.get(placementImageId);
|
|
593
|
+
if (!manager) {
|
|
594
|
+
manager = new CodexPetKittyManager(placementImageId);
|
|
595
|
+
defaultKittyManagers.set(placementImageId, manager);
|
|
596
|
+
}
|
|
597
|
+
return manager;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export function resetCodexPetKittyCache(pet?: LoadedCodexPet, placementImageId?: number): void {
|
|
601
|
+
if (placementImageId !== undefined) defaultKittyManagers.get(placementImageId)?.invalidate(pet);
|
|
602
|
+
else for (const manager of defaultKittyManagers.values()) manager.invalidate(pet);
|
|
603
|
+
markCodexPetKittyFramesUnuploaded(pet);
|
|
477
604
|
}
|
|
478
605
|
|
|
479
606
|
export function renderCodexPetFrame(
|
|
@@ -481,7 +608,13 @@ export function renderCodexPetFrame(
|
|
|
481
608
|
state: PetState,
|
|
482
609
|
width: number,
|
|
483
610
|
theme: ThemeLike,
|
|
484
|
-
options: {
|
|
611
|
+
options: {
|
|
612
|
+
sizeCells: number;
|
|
613
|
+
imageId: number;
|
|
614
|
+
now?: number;
|
|
615
|
+
durationMultiplier?: number;
|
|
616
|
+
kittyManager?: CodexPetKittyManager;
|
|
617
|
+
},
|
|
485
618
|
): string[] {
|
|
486
619
|
const frame = animationFrameAt(
|
|
487
620
|
pet.states[state] ?? pet.states.idle,
|
|
@@ -490,7 +623,15 @@ export function renderCodexPetFrame(
|
|
|
490
623
|
);
|
|
491
624
|
if (!frame) return [];
|
|
492
625
|
const imageProtocol = getCapabilities().images;
|
|
493
|
-
if (imageProtocol === "kitty")
|
|
626
|
+
if (imageProtocol === "kitty") {
|
|
627
|
+
return (options.kittyManager ?? defaultKittyManager(options.imageId)).renderFrame(
|
|
628
|
+
frame,
|
|
629
|
+
width,
|
|
630
|
+
{
|
|
631
|
+
sizeCells: options.sizeCells,
|
|
632
|
+
},
|
|
633
|
+
);
|
|
634
|
+
}
|
|
494
635
|
if (!imageProtocol) {
|
|
495
636
|
const fallback = imageFallback(frame.mimeType, {
|
|
496
637
|
widthPx: frame.widthPx,
|
|
@@ -540,7 +681,7 @@ const PET_SUBCOMMANDS: AutocompleteItem[] = [
|
|
|
540
681
|
];
|
|
541
682
|
|
|
542
683
|
function petCompletionValue(subcommand: string, pet: CodexPetPackage): string {
|
|
543
|
-
return `${subcommand} ${pet.
|
|
684
|
+
return `${subcommand} ${pet.slug}`;
|
|
544
685
|
}
|
|
545
686
|
|
|
546
687
|
export async function openAIPetsArgumentCompletions(
|
|
@@ -585,7 +726,9 @@ export function formatCodexPetsListMessage(pets: CodexPetPackage[], home = codex
|
|
|
585
726
|
|
|
586
727
|
const petLines = pets
|
|
587
728
|
.map((pet) => {
|
|
588
|
-
const status = pet.hasSpritesheet
|
|
729
|
+
const status = pet.hasSpritesheet
|
|
730
|
+
? "ready"
|
|
731
|
+
: (pet.spritesheetIssue ?? `missing ${pet.spritesheetPath}`);
|
|
589
732
|
return `${pet.name} (${pet.slug}) — ${status}${pet.description ? `\n ${pet.description}` : ""}`;
|
|
590
733
|
})
|
|
591
734
|
.join("\n");
|