pi-better-openai 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/index.ts +145 -56
- package/package.json +6 -4
- package/src/codex-auth.ts +38 -7
- package/src/config.ts +3 -2
- package/src/fast-controller.ts +1 -1
- package/src/format.ts +45 -20
- package/src/image.ts +116 -127
- package/src/paths.ts +17 -0
- package/src/pet-footer-controller.ts +37 -21
- package/src/pets.ts +104 -47
- package/src/usage-controller.ts +133 -48
- package/src/usage.ts +91 -23
package/src/format.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
truncateToWidth as truncateTerminalText,
|
|
3
|
+
visibleWidth as terminalVisibleWidth,
|
|
4
|
+
} from "@earendil-works/pi-tui";
|
|
5
|
+
|
|
1
6
|
const ANSI_ESCAPE_PATTERN = String.raw`\u001B\[[0-?]*[ -/]*[@-~]`;
|
|
2
7
|
const ANSI_ESCAPE_REGEXP = new RegExp(ANSI_ESCAPE_PATTERN, "g");
|
|
3
|
-
const LEADING_ANSI_ESCAPE_REGEXP = new RegExp(`^(?:${ANSI_ESCAPE_PATTERN})+`);
|
|
4
|
-
const TRAILING_ANSI_ESCAPE_REGEXP = new RegExp(`(?:${ANSI_ESCAPE_PATTERN})+$`);
|
|
5
8
|
const DIAGNOSTIC_MAX_LENGTH = 500;
|
|
9
|
+
const REDACTED = "[REDACTED]";
|
|
10
|
+
const SENSITIVE_DIAGNOSTIC_KEY_SUFFIXES = [
|
|
11
|
+
"token",
|
|
12
|
+
"apikey",
|
|
13
|
+
"accesskey",
|
|
14
|
+
"authorization",
|
|
15
|
+
"password",
|
|
16
|
+
"passwd",
|
|
17
|
+
"secret",
|
|
18
|
+
"privatekey",
|
|
19
|
+
"credential",
|
|
20
|
+
"credentials",
|
|
21
|
+
"accountid",
|
|
22
|
+
] as const;
|
|
6
23
|
|
|
7
24
|
function replaceControlCharacters(value: string): string {
|
|
8
25
|
let result = "";
|
|
@@ -18,23 +35,11 @@ export function stripAnsi(value: string): string {
|
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
export function visibleWidth(value: string): number {
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function leadingAnsi(value: string): string {
|
|
25
|
-
return value.match(LEADING_ANSI_ESCAPE_REGEXP)?.[0] ?? "";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function trailingAnsi(value: string): string {
|
|
29
|
-
return value.match(TRAILING_ANSI_ESCAPE_REGEXP)?.[0] ?? "";
|
|
38
|
+
return terminalVisibleWidth(value);
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
export function truncateToWidth(value: string, width: number, ellipsis = "..."): string {
|
|
33
|
-
|
|
34
|
-
if (width <= 0) return "";
|
|
35
|
-
const plain = stripAnsi(value);
|
|
36
|
-
if (width <= ellipsis.length) return ellipsis.slice(0, width);
|
|
37
|
-
return `${leadingAnsi(value)}${plain.slice(0, Math.max(0, width - ellipsis.length))}${ellipsis}${trailingAnsi(value)}`;
|
|
42
|
+
return truncateTerminalText(value, width, ellipsis);
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export function formatTokens(count: number): string {
|
|
@@ -46,10 +51,7 @@ export function formatTokens(count: number): string {
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
export function sanitizeStatusText(text: string): string {
|
|
49
|
-
return text
|
|
50
|
-
.replace(/[\r\n\t]/g, " ")
|
|
51
|
-
.replace(/ +/g, " ")
|
|
52
|
-
.trim();
|
|
54
|
+
return text.replace(/[ \r\n\t]+/g, " ").trim();
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export function maskIdentifier(value: string | undefined): string | undefined {
|
|
@@ -77,3 +79,26 @@ export function sanitizeDiagnosticError(
|
|
|
77
79
|
if (sanitized.length <= maxLength) return sanitized;
|
|
78
80
|
return `${sanitized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
79
81
|
}
|
|
82
|
+
|
|
83
|
+
function isSensitiveDiagnosticKey(key: string): boolean {
|
|
84
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
85
|
+
return (
|
|
86
|
+
normalized === "access" ||
|
|
87
|
+
normalized === "auth" ||
|
|
88
|
+
normalized === "refresh" ||
|
|
89
|
+
SENSITIVE_DIAGNOSTIC_KEY_SUFFIXES.some((suffix) => normalized.endsWith(suffix))
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function redactDiagnosticValue(value: unknown): unknown {
|
|
94
|
+
if (typeof value === "string") return sanitizeDiagnosticError(value);
|
|
95
|
+
if (Array.isArray(value)) return value.map(redactDiagnosticValue);
|
|
96
|
+
if (typeof value !== "object" || value === null) return value;
|
|
97
|
+
|
|
98
|
+
return Object.fromEntries(
|
|
99
|
+
Object.entries(value).map(([key, entry]) => [
|
|
100
|
+
key,
|
|
101
|
+
isSensitiveDiagnosticKey(key) ? REDACTED : redactDiagnosticValue(entry),
|
|
102
|
+
]),
|
|
103
|
+
);
|
|
104
|
+
}
|
package/src/image.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Box, Container, Image, Text } from "@earendil-works/pi-tui";
|
|
6
7
|
import sharp from "sharp";
|
|
7
8
|
import type { ResolvedConfig } from "./config.ts";
|
|
8
9
|
import { isRecord } from "./config.ts";
|
|
@@ -12,13 +13,17 @@ import {
|
|
|
12
13
|
type CodexCredentialsWithSource,
|
|
13
14
|
} from "./codex-auth.ts";
|
|
14
15
|
import { maskIdentifier, sanitizeDiagnosticError } from "./format.ts";
|
|
16
|
+
import { piAgentDir, resolveUserPath } from "./paths.ts";
|
|
15
17
|
|
|
16
18
|
const OPENAI_IMAGE_TOOL = "openai_image";
|
|
17
19
|
const OPENAI_IMAGE_COMMAND = "openai-image";
|
|
18
20
|
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
19
21
|
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
20
22
|
const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
23
|
+
const MAX_IMAGE_INPUTS = 5;
|
|
24
|
+
const MAX_TOTAL_IMAGE_INPUT_BYTES = 50 * 1024 * 1024;
|
|
21
25
|
const SUPPORTED_INPUT_IMAGE_FORMATS = new Set(["png", "jpeg", "jpg", "webp", "gif"]);
|
|
26
|
+
const SSE_EVENT_BOUNDARY = /\r?\n\r?\n/;
|
|
22
27
|
|
|
23
28
|
export const IMAGE_SAVE_MODES = ["none", "project", "global", "custom"] as const;
|
|
24
29
|
export const IMAGE_ACTIONS = ["auto", "generate", "edit"] as const;
|
|
@@ -44,6 +49,7 @@ const TOOL_PARAMS = {
|
|
|
44
49
|
},
|
|
45
50
|
images: {
|
|
46
51
|
type: "array",
|
|
52
|
+
maxItems: MAX_IMAGE_INPUTS,
|
|
47
53
|
items: { type: "string" },
|
|
48
54
|
description: "Local image paths to use as edit targets or references.",
|
|
49
55
|
},
|
|
@@ -116,8 +122,11 @@ export type ImageGenerationDebug = {
|
|
|
116
122
|
lastError?: string;
|
|
117
123
|
};
|
|
118
124
|
|
|
119
|
-
async function getCredentials(
|
|
120
|
-
|
|
125
|
+
async function getCredentials(
|
|
126
|
+
ctx: ExtensionContext,
|
|
127
|
+
signal?: AbortSignal,
|
|
128
|
+
): Promise<CodexImageCredentials> {
|
|
129
|
+
const credentials = await getCodexCredentials(ctx, signal);
|
|
121
130
|
if (credentials) return credentials;
|
|
122
131
|
throw new Error("Missing openai-codex OAuth credentials. Run /login openai-codex.");
|
|
123
132
|
}
|
|
@@ -141,12 +150,14 @@ function resolveImageConfig(cfg: ResolvedConfig, params: ToolParams) {
|
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
function imageMimeType(path: string, outputFormat?: string): string {
|
|
153
|
+
if (outputFormat === "jpeg" || outputFormat === "jpg") return "image/jpeg";
|
|
154
|
+
if (outputFormat === "webp") return "image/webp";
|
|
155
|
+
if (outputFormat === "gif") return "image/gif";
|
|
156
|
+
if (outputFormat === "png") return "image/png";
|
|
144
157
|
const ext = extname(path).toLowerCase();
|
|
145
158
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
146
159
|
if (ext === ".webp") return "image/webp";
|
|
147
160
|
if (ext === ".gif") return "image/gif";
|
|
148
|
-
if (outputFormat === "jpeg") return "image/jpeg";
|
|
149
|
-
if (outputFormat === "webp") return "image/webp";
|
|
150
161
|
return "image/png";
|
|
151
162
|
}
|
|
152
163
|
|
|
@@ -162,15 +173,17 @@ function isInsideDirectory(root: string, child: string): boolean {
|
|
|
162
173
|
);
|
|
163
174
|
}
|
|
164
175
|
|
|
165
|
-
async function validateImageInput(
|
|
166
|
-
|
|
176
|
+
async function validateImageInput(
|
|
177
|
+
path: string,
|
|
178
|
+
realWorkspaceRoot: string,
|
|
179
|
+
): Promise<{ mimeType: string; path: string; size: number }> {
|
|
167
180
|
const realInputPath = await realpath(path).catch(() => undefined);
|
|
168
181
|
if (!realInputPath || !isInsideDirectory(realWorkspaceRoot, realInputPath))
|
|
169
182
|
throw new Error(
|
|
170
183
|
`Image input must be a file inside the current workspace: ${displayPath(path)}`,
|
|
171
184
|
);
|
|
172
185
|
|
|
173
|
-
const pathStats = await stat(
|
|
186
|
+
const pathStats = await stat(realInputPath).catch(() => undefined);
|
|
174
187
|
if (!pathStats?.isFile())
|
|
175
188
|
throw new Error(
|
|
176
189
|
`Image input must be a file inside the current workspace: ${displayPath(path)}`,
|
|
@@ -178,16 +191,24 @@ async function validateImageInput(path: string, workspaceRoot: string): Promise<
|
|
|
178
191
|
if (pathStats.size > MAX_IMAGE_INPUT_BYTES)
|
|
179
192
|
throw new Error(`Image input is too large (max 20 MB): ${displayPath(path)}`);
|
|
180
193
|
|
|
181
|
-
const metadata = await sharp(
|
|
194
|
+
const metadata = await sharp(realInputPath, { animated: false })
|
|
182
195
|
.metadata()
|
|
183
196
|
.catch(() => undefined);
|
|
184
197
|
if (!metadata?.format || !SUPPORTED_INPUT_IMAGE_FORMATS.has(metadata.format))
|
|
185
198
|
throw new Error(`Image input is not a readable image: ${displayPath(path)}`);
|
|
199
|
+
return {
|
|
200
|
+
mimeType: imageMimeType(path, metadata.format),
|
|
201
|
+
path: realInputPath,
|
|
202
|
+
size: pathStats.size,
|
|
203
|
+
};
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
async function readImageInputs(paths: string[] | undefined, cwd: string): Promise<ImageInput[]> {
|
|
189
|
-
const
|
|
207
|
+
const validatedInputs: Array<{ path: string; mimeType: string }> = [];
|
|
208
|
+
const seenPaths = new Set<string>();
|
|
209
|
+
let totalBytes = 0;
|
|
190
210
|
const workspaceRoot = resolve(cwd);
|
|
211
|
+
let realWorkspaceRoot: string | undefined;
|
|
191
212
|
for (const rawPath of paths ?? []) {
|
|
192
213
|
const trimmed = rawPath.trim();
|
|
193
214
|
if (!trimmed) continue;
|
|
@@ -196,11 +217,23 @@ async function readImageInputs(paths: string[] | undefined, cwd: string): Promis
|
|
|
196
217
|
throw new Error(
|
|
197
218
|
`Image input must be a file inside the current workspace: ${displayPath(path)}`,
|
|
198
219
|
);
|
|
199
|
-
await
|
|
200
|
-
const
|
|
201
|
-
|
|
220
|
+
realWorkspaceRoot ??= await realpath(workspaceRoot).catch(() => workspaceRoot);
|
|
221
|
+
const input = await validateImageInput(path, realWorkspaceRoot);
|
|
222
|
+
if (seenPaths.has(input.path)) continue;
|
|
223
|
+
if (validatedInputs.length >= MAX_IMAGE_INPUTS)
|
|
224
|
+
throw new Error(`Too many image inputs (max ${MAX_IMAGE_INPUTS}).`);
|
|
225
|
+
totalBytes += input.size;
|
|
226
|
+
if (totalBytes > MAX_TOTAL_IMAGE_INPUT_BYTES)
|
|
227
|
+
throw new Error("Image inputs are too large in total (max 50 MB).");
|
|
228
|
+
seenPaths.add(input.path);
|
|
229
|
+
validatedInputs.push({ path: input.path, mimeType: input.mimeType });
|
|
202
230
|
}
|
|
203
|
-
return
|
|
231
|
+
return Promise.all(
|
|
232
|
+
validatedInputs.map(async (input) => ({
|
|
233
|
+
...input,
|
|
234
|
+
data: (await readFile(input.path)).toString("base64"),
|
|
235
|
+
})),
|
|
236
|
+
);
|
|
204
237
|
}
|
|
205
238
|
|
|
206
239
|
function resolveSaveDir(
|
|
@@ -210,14 +243,10 @@ function resolveSaveDir(
|
|
|
210
243
|
): string | undefined {
|
|
211
244
|
if (mode === "none") return undefined;
|
|
212
245
|
if (mode === "project") return join(cwd, ".pi", "generated-images");
|
|
213
|
-
if (mode === "global")
|
|
214
|
-
return join(
|
|
215
|
-
process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"),
|
|
216
|
-
"generated-images",
|
|
217
|
-
);
|
|
246
|
+
if (mode === "global") return join(piAgentDir(), "generated-images");
|
|
218
247
|
const dir = params.saveDir?.trim() || process.env.PI_IMAGE_SAVE_DIR?.trim();
|
|
219
248
|
if (!dir) throw new Error("save=custom requires saveDir or PI_IMAGE_SAVE_DIR.");
|
|
220
|
-
return dir;
|
|
249
|
+
return resolveUserPath(dir, cwd);
|
|
221
250
|
}
|
|
222
251
|
|
|
223
252
|
async function saveImage(
|
|
@@ -342,19 +371,19 @@ async function parseSseForImage(
|
|
|
342
371
|
const reader = response.body.getReader();
|
|
343
372
|
const decoder = new TextDecoder();
|
|
344
373
|
let buffer = "";
|
|
345
|
-
let lastImage: ExtractedImageResult | undefined;
|
|
346
374
|
try {
|
|
347
375
|
while (true) {
|
|
348
376
|
if (signal?.aborted) throw new Error("Image request was aborted.");
|
|
349
377
|
const { done, value } = await reader.read();
|
|
350
378
|
if (done) break;
|
|
351
379
|
buffer += decoder.decode(value, { stream: true });
|
|
352
|
-
let
|
|
353
|
-
while (
|
|
380
|
+
let boundary = SSE_EVENT_BOUNDARY.exec(buffer);
|
|
381
|
+
while (boundary) {
|
|
382
|
+
const idx = boundary.index;
|
|
354
383
|
const chunk = buffer.slice(0, idx);
|
|
355
|
-
buffer = buffer.slice(idx +
|
|
384
|
+
buffer = buffer.slice(idx + boundary[0].length);
|
|
356
385
|
const data = chunk
|
|
357
|
-
.split(
|
|
386
|
+
.split(/\r?\n/)
|
|
358
387
|
.filter((line) => line.startsWith("data:"))
|
|
359
388
|
.map((line) => line.slice(5).trim())
|
|
360
389
|
.join("\n")
|
|
@@ -367,12 +396,9 @@ async function parseSseForImage(
|
|
|
367
396
|
event = undefined;
|
|
368
397
|
}
|
|
369
398
|
const image = extractImageFromEvent(event, fallbackMimeType);
|
|
370
|
-
if (image?.data) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
await reader.cancel().catch(() => undefined);
|
|
374
|
-
return image;
|
|
375
|
-
}
|
|
399
|
+
if (image?.data && image.status === "completed") {
|
|
400
|
+
await reader.cancel().catch(() => undefined);
|
|
401
|
+
return image;
|
|
376
402
|
}
|
|
377
403
|
if (isRecord(event) && event.type === "response.failed") {
|
|
378
404
|
const error =
|
|
@@ -391,13 +417,12 @@ async function parseSseForImage(
|
|
|
391
417
|
throw new Error(`Codex image error: ${message}`);
|
|
392
418
|
}
|
|
393
419
|
}
|
|
394
|
-
|
|
420
|
+
boundary = SSE_EVENT_BOUNDARY.exec(buffer);
|
|
395
421
|
}
|
|
396
422
|
}
|
|
397
423
|
} finally {
|
|
398
424
|
reader.releaseLock();
|
|
399
425
|
}
|
|
400
|
-
if (lastImage?.status === "completed") return lastImage;
|
|
401
426
|
throw new Error("No completed image_generation_call result returned by Codex.");
|
|
402
427
|
}
|
|
403
428
|
|
|
@@ -408,14 +433,16 @@ async function requestCodexImage(
|
|
|
408
433
|
requestSignal?: AbortSignal,
|
|
409
434
|
): Promise<CodexImageResult> {
|
|
410
435
|
if (!cfg.image.enabled) throw new Error("OpenAI image generation is disabled in config.");
|
|
411
|
-
const
|
|
436
|
+
const cwd = ctx.cwd || process.cwd();
|
|
412
437
|
const model = resolveModel(params, ctx, cfg);
|
|
413
438
|
const { action, outputFormat, save } = resolveImageConfig(cfg, params);
|
|
414
|
-
const
|
|
415
|
-
const request = buildRequest(params, model, cfg, images);
|
|
439
|
+
const saveDir = resolveSaveDir(save, params, cwd);
|
|
416
440
|
const timeoutSignal = AbortSignal.timeout(cfg.image.timeoutMs);
|
|
417
441
|
const baseSignal = requestSignal ?? ctx.signal;
|
|
418
442
|
const signal = baseSignal ? AbortSignal.any([baseSignal, timeoutSignal]) : timeoutSignal;
|
|
443
|
+
const credentials = await getCredentials(ctx, signal);
|
|
444
|
+
const images = await readImageInputs(params.images, cwd);
|
|
445
|
+
const request = buildRequest(params, model, cfg, images);
|
|
419
446
|
const response = await fetch(CODEX_RESPONSES_URL, {
|
|
420
447
|
method: "POST",
|
|
421
448
|
headers: {
|
|
@@ -441,7 +468,6 @@ async function requestCodexImage(
|
|
|
441
468
|
imageMimeType(`image.${outputFormat}`, outputFormat),
|
|
442
469
|
signal,
|
|
443
470
|
);
|
|
444
|
-
const saveDir = resolveSaveDir(save, params, ctx.cwd || process.cwd());
|
|
445
471
|
const savedPath = saveDir
|
|
446
472
|
? await saveImage(parsed.data, outputFormat, saveDir, parsed.id)
|
|
447
473
|
: undefined;
|
|
@@ -456,37 +482,6 @@ function displayPath(path: string): string {
|
|
|
456
482
|
return path.startsWith(homePrefix) ? `~/${path.slice(homePrefix.length)}` : path;
|
|
457
483
|
}
|
|
458
484
|
|
|
459
|
-
function textFromMessageContent(content: unknown): string | undefined {
|
|
460
|
-
if (typeof content === "string") return content.trim() || undefined;
|
|
461
|
-
if (!Array.isArray(content)) return undefined;
|
|
462
|
-
const text = content
|
|
463
|
-
.filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
464
|
-
.map((part) => (part as { text: string }).text)
|
|
465
|
-
.join("\n")
|
|
466
|
-
.trim();
|
|
467
|
-
return text || undefined;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function latestUserPromptFromEntries(entries: unknown[]): string | undefined {
|
|
471
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
472
|
-
const entry = entries[i];
|
|
473
|
-
if (
|
|
474
|
-
!isRecord(entry) ||
|
|
475
|
-
entry.type !== "message" ||
|
|
476
|
-
!isRecord(entry.message) ||
|
|
477
|
-
entry.message.role !== "user"
|
|
478
|
-
)
|
|
479
|
-
continue;
|
|
480
|
-
const text = textFromMessageContent(entry.message.content);
|
|
481
|
-
if (text) return text;
|
|
482
|
-
}
|
|
483
|
-
return undefined;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function resolveToolPrompt(params: ToolParams, ctx: ExtensionContext): string {
|
|
487
|
-
return latestUserPromptFromEntries(ctx.sessionManager.getEntries()) ?? params.prompt;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
485
|
function resultText(result: CodexImageResult): string {
|
|
491
486
|
const parts = [
|
|
492
487
|
`Generated image using OpenAI image_generation tool via openai-codex/${result.model}.`,
|
|
@@ -544,63 +539,57 @@ export function registerOpenAIImage(
|
|
|
544
539
|
};
|
|
545
540
|
}
|
|
546
541
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (imagePart) image = { data: imagePart.data, mimeType: imagePart.mimeType };
|
|
575
|
-
}
|
|
542
|
+
pi.registerMessageRenderer<CodexImageResult>("openai-image", (message, _options, theme) => {
|
|
543
|
+
const result = message.details;
|
|
544
|
+
const text =
|
|
545
|
+
result && isRecord(result)
|
|
546
|
+
? resultText(result as CodexImageResult)
|
|
547
|
+
: typeof message.content === "string"
|
|
548
|
+
? message.content
|
|
549
|
+
: message.content
|
|
550
|
+
.filter((part) => part.type === "text")
|
|
551
|
+
.map((part) => part.text)
|
|
552
|
+
.join("\n");
|
|
553
|
+
let image: { data: string; mimeType: string; savedPath?: string } | undefined;
|
|
554
|
+
if (
|
|
555
|
+
result &&
|
|
556
|
+
isRecord(result) &&
|
|
557
|
+
typeof result.data === "string" &&
|
|
558
|
+
typeof result.mimeType === "string"
|
|
559
|
+
) {
|
|
560
|
+
image = {
|
|
561
|
+
data: result.data,
|
|
562
|
+
mimeType: result.mimeType,
|
|
563
|
+
savedPath: typeof result.savedPath === "string" ? result.savedPath : undefined,
|
|
564
|
+
};
|
|
565
|
+
} else if (Array.isArray(message.content)) {
|
|
566
|
+
const imagePart = message.content.find(isImageContent);
|
|
567
|
+
if (imagePart) image = { data: imagePart.data, mimeType: imagePart.mimeType };
|
|
568
|
+
}
|
|
576
569
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
return container;
|
|
601
|
-
});
|
|
602
|
-
})
|
|
603
|
-
.catch(() => undefined);
|
|
570
|
+
const container = new Container();
|
|
571
|
+
const box = new Box(1, 1, (line) => theme.bg("customMessageBg", line));
|
|
572
|
+
box.addChild(new Text(`${theme.fg("accent", theme.bold("[openai-image]"))}\n\n${text}`, 0, 0));
|
|
573
|
+
if (image) {
|
|
574
|
+
box.addChild(
|
|
575
|
+
new Image(
|
|
576
|
+
image.data,
|
|
577
|
+
image.mimeType,
|
|
578
|
+
{ fallbackColor: (line) => theme.fg("dim", line) },
|
|
579
|
+
{
|
|
580
|
+
maxWidthCells: 80,
|
|
581
|
+
maxHeightCells: 24,
|
|
582
|
+
filename:
|
|
583
|
+
"savedPath" in image && typeof image.savedPath === "string"
|
|
584
|
+
? image.savedPath
|
|
585
|
+
: undefined,
|
|
586
|
+
},
|
|
587
|
+
),
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
container.addChild(box);
|
|
591
|
+
return container;
|
|
592
|
+
});
|
|
604
593
|
|
|
605
594
|
pi.registerCommand(OPENAI_IMAGE_COMMAND, {
|
|
606
595
|
description: "Generate an image with OpenAI Codex image generation",
|
|
@@ -639,7 +628,6 @@ export function registerOpenAIImage(
|
|
|
639
628
|
async execute(_toolCallId, params: ToolParams, signal, onUpdate, ctx) {
|
|
640
629
|
const cfg = getConfig(ctx);
|
|
641
630
|
const model = resolveModel(params, ctx, cfg);
|
|
642
|
-
const requestParams = { ...params, prompt: resolveToolPrompt(params, ctx) };
|
|
643
631
|
onUpdate?.({
|
|
644
632
|
content: [
|
|
645
633
|
{
|
|
@@ -649,7 +637,7 @@ export function registerOpenAIImage(
|
|
|
649
637
|
],
|
|
650
638
|
details: undefined,
|
|
651
639
|
});
|
|
652
|
-
const result = await generate(
|
|
640
|
+
const result = await generate(params, ctx, signal);
|
|
653
641
|
return {
|
|
654
642
|
content: [
|
|
655
643
|
{ type: "text", text: resultText(result) },
|
|
@@ -669,11 +657,12 @@ export const _imageTest = {
|
|
|
669
657
|
OPENAI_IMAGE_TOOL,
|
|
670
658
|
OPENAI_IMAGE_COMMAND,
|
|
671
659
|
MAX_IMAGE_INPUT_BYTES,
|
|
660
|
+
MAX_IMAGE_INPUTS,
|
|
661
|
+
MAX_TOTAL_IMAGE_INPUT_BYTES,
|
|
672
662
|
extractAccountIdFromJwt,
|
|
673
663
|
imageMimeType,
|
|
674
664
|
dataUrlParts,
|
|
675
665
|
extractImageFromEvent,
|
|
676
666
|
displayPath,
|
|
677
|
-
latestUserPromptFromEntries,
|
|
678
667
|
buildRequest,
|
|
679
668
|
};
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function expandTildePath(path: string, home = homedir()): string {
|
|
5
|
+
if (path === "~") return home;
|
|
6
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
|
|
7
|
+
return path;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function piAgentDir(env = process.env, home = homedir()): string {
|
|
11
|
+
const configuredDir = env.PI_CODING_AGENT_DIR?.trim();
|
|
12
|
+
return configuredDir ? expandTildePath(configuredDir, home) : join(home, ".pi", "agent");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveUserPath(path: string, cwd: string, home = homedir()): string {
|
|
16
|
+
return resolve(cwd, expandTildePath(path, home));
|
|
17
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
2
|
-
import { calculateImageRows, getCapabilities, getCellDimensions } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { calculateImageRows, getCapabilities, getCellDimensions } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { PetPlacement, PetState, ResolvedConfig } from "./config.ts";
|
|
4
4
|
import {
|
|
5
5
|
animationFrameAt,
|
|
@@ -165,6 +165,7 @@ export class PetFooterController {
|
|
|
165
165
|
this.petSettingsPreviewActive = next;
|
|
166
166
|
if (!this.getConfig(ctx).pets.enabled) this.petLoadKey = undefined;
|
|
167
167
|
void this.refresh(ctx, this.getConfig(ctx)).then(() => {
|
|
168
|
+
if (this.shuttingDown) return;
|
|
168
169
|
if (!this.shouldRenderInFooter(this.getConfig(ctx))) this.flushKittyCleanupNow();
|
|
169
170
|
this.requestSettingsRender?.();
|
|
170
171
|
});
|
|
@@ -376,8 +377,12 @@ export class PetFooterController {
|
|
|
376
377
|
updateActivity(ctx: ExtensionContext, cfg: ResolvedConfig): void {
|
|
377
378
|
const resizeFrozen = this.isResizeFrozen();
|
|
378
379
|
const shouldAnimatePet = this.shouldLoadForConfig(cfg);
|
|
379
|
-
|
|
380
|
-
|
|
380
|
+
if (!shouldAnimatePet || resizeFrozen) {
|
|
381
|
+
this.stopAnimation();
|
|
382
|
+
} else {
|
|
383
|
+
if (this.currentState(ctx, cfg) !== this.petAnimationState) this.stopAnimation();
|
|
384
|
+
this.startAnimation(ctx);
|
|
385
|
+
}
|
|
381
386
|
if (!resizeFrozen && this.shouldRunIdleEmotes(ctx, cfg)) this.scheduleIdleEmote(ctx, cfg);
|
|
382
387
|
else this.stopIdleEmotes();
|
|
383
388
|
}
|
|
@@ -552,20 +557,22 @@ export class PetFooterController {
|
|
|
552
557
|
),
|
|
553
558
|
);
|
|
554
559
|
} else {
|
|
555
|
-
const { frameIndex } = petFrameInfo(this.pet, petState, elapsedMs);
|
|
556
|
-
const cacheKey = petRenderCacheKey(
|
|
557
|
-
this.pet,
|
|
558
|
-
petState,
|
|
559
|
-
frameIndex,
|
|
560
|
-
options.requestedPetPlacement,
|
|
561
|
-
options.petColumnWidth,
|
|
562
|
-
options.petRenderSizeCells,
|
|
563
|
-
);
|
|
564
|
-
const cachedPetLines = this.petRenderCache.get(cacheKey);
|
|
565
560
|
const imageProtocol = getCapabilities().images;
|
|
566
|
-
if (
|
|
567
|
-
|
|
568
|
-
|
|
561
|
+
if (imageProtocol !== null && imageProtocol !== "kitty") {
|
|
562
|
+
const { frameIndex } = petFrameInfo(this.pet, petState, elapsedMs);
|
|
563
|
+
const cacheKey = petRenderCacheKey(
|
|
564
|
+
this.pet,
|
|
565
|
+
petState,
|
|
566
|
+
frameIndex,
|
|
567
|
+
options.requestedPetPlacement,
|
|
568
|
+
options.petColumnWidth,
|
|
569
|
+
options.petRenderSizeCells,
|
|
570
|
+
);
|
|
571
|
+
const cachedPetLines = this.petRenderCache.get(cacheKey);
|
|
572
|
+
if (cachedPetLines) {
|
|
573
|
+
petLines.push(...cachedPetLines);
|
|
574
|
+
return petLines;
|
|
575
|
+
}
|
|
569
576
|
const renderedPetLines = renderCodexPetFrame(
|
|
570
577
|
this.pet,
|
|
571
578
|
petState,
|
|
@@ -580,10 +587,17 @@ export class PetFooterController {
|
|
|
580
587
|
},
|
|
581
588
|
);
|
|
582
589
|
petLines.push(...renderedPetLines);
|
|
583
|
-
if (renderedPetLines.length > 0)
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
590
|
+
if (renderedPetLines.length > 0) this.rememberRender(cacheKey, renderedPetLines);
|
|
591
|
+
} else {
|
|
592
|
+
petLines.push(
|
|
593
|
+
...renderCodexPetFrame(this.pet, petState, options.petColumnWidth, options.theme, {
|
|
594
|
+
sizeCells: options.petRenderSizeCells,
|
|
595
|
+
imageId: this.petImageId,
|
|
596
|
+
now: elapsedMs,
|
|
597
|
+
durationMultiplier: 1,
|
|
598
|
+
kittyManager: this.petKittyManager,
|
|
599
|
+
}),
|
|
600
|
+
);
|
|
587
601
|
}
|
|
588
602
|
}
|
|
589
603
|
} else if (this.petError) {
|
|
@@ -607,5 +621,7 @@ export class PetFooterController {
|
|
|
607
621
|
this.stopIdleEmotes();
|
|
608
622
|
this.stopAnimation();
|
|
609
623
|
this.stopPendingRenderRequest();
|
|
624
|
+
this.requestFooterRender = undefined;
|
|
625
|
+
this.requestSettingsRender = undefined;
|
|
610
626
|
}
|
|
611
627
|
}
|