@monotykamary/pi-better-openai 0.1.23 → 0.1.25
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/package.json +2 -5
- package/src/config.ts +13 -5
- package/src/image.ts +136 -170
- package/src/live/controller.ts +8 -1
- package/src/live/focus.ts +92 -0
- package/src/live/index.ts +192 -40
- package/src/live/queue.ts +388 -0
- package/src/live/visualizer.ts +13 -3
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Example config:
|
|
|
85
85
|
},
|
|
86
86
|
"image": {
|
|
87
87
|
"enabled": true,
|
|
88
|
-
"defaultModel": "gpt-
|
|
88
|
+
"defaultModel": "gpt-image-2",
|
|
89
89
|
"defaultSave": "project",
|
|
90
90
|
"outputFormat": "png",
|
|
91
91
|
"timeoutMs": 180000
|
|
@@ -135,10 +135,10 @@ Use the command for quick generation:
|
|
|
135
135
|
Agents can call the `openai_image` tool directly. Supported parameters:
|
|
136
136
|
|
|
137
137
|
- `prompt` (required): pass the user's image wording verbatim.
|
|
138
|
-
- `action`: `auto`, `generate`, or `edit`.
|
|
138
|
+
- `action`: `auto`, `generate`, or `edit`. `auto` uses the edit endpoint when `images` are supplied; explicit `edit` requires images, while explicit `generate` does not accept them.
|
|
139
139
|
- `images`: up to five distinct project-local reference/edit image paths. Paths must stay inside the current workspace and point to readable PNG, JPEG, WebP, or GIF files; each file is limited to 20 MB and the combined input to 50 MB.
|
|
140
|
-
- `model`:
|
|
141
|
-
- `outputFormat`: `png`, `jpeg`, or `webp`.
|
|
140
|
+
- `model`: GPT Image model override for the standalone Codex Images API, for example `gpt-image-2`.
|
|
141
|
+
- `outputFormat`: `png`, `jpeg`, or `webp`. Codex returns PNG and the extension converts other formats locally.
|
|
142
142
|
- `save`: `project`, `global`, `custom`, or `none`.
|
|
143
143
|
- `saveDir`: required for `save: "custom"` unless `PI_IMAGE_SAVE_DIR` is set.
|
|
144
144
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monotykamary/pi-better-openai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"description": "Improve OpenAI in pi with fast mode, usage stats, realtime voice, image generation, and footer polish.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fast",
|
|
@@ -71,9 +71,6 @@
|
|
|
71
71
|
"@oh-my-pi/pi-natives-linux-x64": "17.2.9",
|
|
72
72
|
"@oh-my-pi/pi-natives-win32-x64": "17.2.9"
|
|
73
73
|
},
|
|
74
|
-
"overrides": {
|
|
75
|
-
"postcss": "8.5.25"
|
|
76
|
-
},
|
|
77
74
|
"engines": {
|
|
78
75
|
"node": ">=22.19.0"
|
|
79
76
|
},
|
|
@@ -83,7 +80,7 @@
|
|
|
83
80
|
]
|
|
84
81
|
},
|
|
85
82
|
"scripts": {
|
|
86
|
-
"check": "
|
|
83
|
+
"check": "pnpm run typecheck && pnpm run lint && pnpm run format:check && pnpm test",
|
|
87
84
|
"format": "oxfmt .",
|
|
88
85
|
"format:check": "oxfmt --check .",
|
|
89
86
|
"lint": "oxlint --deny-warnings index.ts src/*.ts src/*/*.ts tests/*.ts",
|
package/src/config.ts
CHANGED
|
@@ -130,9 +130,11 @@ export const DEFAULT_FOOTER_CONFIG: Required<FooterConfig> = {
|
|
|
130
130
|
mode: "replace",
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
+
export const DEFAULT_IMAGE_MODEL = "gpt-image-2";
|
|
134
|
+
|
|
133
135
|
export const DEFAULT_IMAGE_CONFIG: Required<ImageConfig> = {
|
|
134
136
|
enabled: true,
|
|
135
|
-
defaultModel:
|
|
137
|
+
defaultModel: DEFAULT_IMAGE_MODEL,
|
|
136
138
|
defaultSave: "project",
|
|
137
139
|
outputFormat: "png",
|
|
138
140
|
timeoutMs: 180_000,
|
|
@@ -276,8 +278,8 @@ export const IMAGE_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
|
|
|
276
278
|
key: "defaultModel",
|
|
277
279
|
label: "Image model",
|
|
278
280
|
currentValue: (cfg) => cfg.image.defaultModel,
|
|
279
|
-
values: ["gpt-
|
|
280
|
-
description: "
|
|
281
|
+
values: ["gpt-image-2"],
|
|
282
|
+
description: "GPT Image model used by the standalone Codex Images API.",
|
|
281
283
|
parse: stringSetting,
|
|
282
284
|
},
|
|
283
285
|
{
|
|
@@ -297,7 +299,7 @@ export const IMAGE_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
|
|
|
297
299
|
label: "Image format",
|
|
298
300
|
currentValue: (cfg) => cfg.image.outputFormat,
|
|
299
301
|
values: IMAGE_OUTPUT_FORMATS,
|
|
300
|
-
description: "
|
|
302
|
+
description: "Returned image format (converted locally from the Codex PNG response).",
|
|
301
303
|
parse: stringSetting,
|
|
302
304
|
},
|
|
303
305
|
{
|
|
@@ -502,6 +504,12 @@ export function readRawConfig(path: string): Record<string, unknown> {
|
|
|
502
504
|
}
|
|
503
505
|
}
|
|
504
506
|
|
|
507
|
+
export function normalizeImageModel(value: string): string {
|
|
508
|
+
const trimmed = value.trim();
|
|
509
|
+
const model = trimmed.includes("/") ? trimmed.split("/").pop() || trimmed : trimmed;
|
|
510
|
+
return model.startsWith("gpt-5") ? DEFAULT_IMAGE_MODEL : model;
|
|
511
|
+
}
|
|
512
|
+
|
|
505
513
|
export function readConfig(path: string): ConfigFile | undefined {
|
|
506
514
|
if (!existsSync(path)) return undefined;
|
|
507
515
|
const parsed = readRawConfig(path);
|
|
@@ -532,7 +540,7 @@ export function readConfig(path: string): ConfigFile | undefined {
|
|
|
532
540
|
config.image = {};
|
|
533
541
|
if (typeof parsed.image.enabled === "boolean") config.image.enabled = parsed.image.enabled;
|
|
534
542
|
if (typeof parsed.image.defaultModel === "string" && parsed.image.defaultModel.trim())
|
|
535
|
-
config.image.defaultModel = parsed.image.defaultModel
|
|
543
|
+
config.image.defaultModel = normalizeImageModel(parsed.image.defaultModel);
|
|
536
544
|
if (
|
|
537
545
|
typeof parsed.image.defaultSave === "string" &&
|
|
538
546
|
(IMAGE_SAVE_MODES as readonly string[]).includes(parsed.image.defaultSave)
|
package/src/image.ts
CHANGED
|
@@ -5,8 +5,7 @@ import { extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Box, Container, Image, Text } from "@earendil-works/pi-tui";
|
|
7
7
|
import sharp from "sharp";
|
|
8
|
-
import type
|
|
9
|
-
import { isRecord } from "./config.ts";
|
|
8
|
+
import { isRecord, normalizeImageModel, type ResolvedConfig } from "./config.ts";
|
|
10
9
|
import {
|
|
11
10
|
extractAccountIdFromJwt,
|
|
12
11
|
getCodexCredentials,
|
|
@@ -17,13 +16,14 @@ import { piAgentDir, resolveUserPath } from "./paths.ts";
|
|
|
17
16
|
|
|
18
17
|
const OPENAI_IMAGE_TOOL = "openai_image";
|
|
19
18
|
const OPENAI_IMAGE_COMMAND = "openai-image";
|
|
20
|
-
const
|
|
19
|
+
const CODEX_IMAGES_BASE_URL = "https://chatgpt.com/backend-api/codex/images";
|
|
20
|
+
const CODEX_IMAGE_GENERATIONS_URL = `${CODEX_IMAGES_BASE_URL}/generations`;
|
|
21
|
+
const CODEX_IMAGE_EDITS_URL = `${CODEX_IMAGES_BASE_URL}/edits`;
|
|
21
22
|
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
22
23
|
const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
23
24
|
const MAX_IMAGE_INPUTS = 5;
|
|
24
25
|
const MAX_TOTAL_IMAGE_INPUT_BYTES = 50 * 1024 * 1024;
|
|
25
26
|
const SUPPORTED_INPUT_IMAGE_FORMATS = new Set(["png", "jpeg", "jpg", "webp", "gif"]);
|
|
26
|
-
const SSE_EVENT_BOUNDARY = /\r?\n\r?\n/;
|
|
27
27
|
|
|
28
28
|
export const IMAGE_SAVE_MODES = ["none", "project", "global", "custom"] as const;
|
|
29
29
|
export const IMAGE_ACTIONS = ["auto", "generate", "edit"] as const;
|
|
@@ -56,12 +56,12 @@ const TOOL_PARAMS = {
|
|
|
56
56
|
model: {
|
|
57
57
|
type: "string",
|
|
58
58
|
description:
|
|
59
|
-
"
|
|
59
|
+
"GPT Image model for the standalone Codex Images API. Defaults to the configured image model.",
|
|
60
60
|
},
|
|
61
61
|
outputFormat: {
|
|
62
62
|
type: "string",
|
|
63
63
|
enum: IMAGE_OUTPUT_FORMATS,
|
|
64
|
-
description: "
|
|
64
|
+
description: "Returned image format; JPEG and WebP are converted locally from Codex PNG.",
|
|
65
65
|
},
|
|
66
66
|
save: {
|
|
67
67
|
type: "string",
|
|
@@ -131,15 +131,9 @@ async function getCredentials(
|
|
|
131
131
|
throw new Error("Missing openai-codex OAuth credentials. Run /login openai-codex.");
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
function resolveModel(
|
|
135
|
-
params: Pick<ToolParams, "model">,
|
|
136
|
-
ctx: ExtensionContext,
|
|
137
|
-
cfg: ResolvedConfig,
|
|
138
|
-
): string {
|
|
134
|
+
function resolveModel(params: Pick<ToolParams, "model">, cfg: ResolvedConfig): string {
|
|
139
135
|
const model = params.model?.trim();
|
|
140
|
-
|
|
141
|
-
if (ctx.model?.provider === "openai-codex") return ctx.model.id;
|
|
142
|
-
return cfg.image.defaultModel;
|
|
136
|
+
return normalizeImageModel(model || cfg.image.defaultModel);
|
|
143
137
|
}
|
|
144
138
|
|
|
145
139
|
function resolveImageConfig(cfg: ResolvedConfig, params: ToolParams) {
|
|
@@ -263,35 +257,48 @@ async function saveImage(
|
|
|
263
257
|
return path;
|
|
264
258
|
}
|
|
265
259
|
|
|
260
|
+
type ConcreteImageAction = Exclude<ImageAction, "auto">;
|
|
261
|
+
|
|
262
|
+
type StandaloneImageRequest = {
|
|
263
|
+
action: ConcreteImageAction;
|
|
264
|
+
endpoint: string;
|
|
265
|
+
body: Record<string, unknown>;
|
|
266
|
+
};
|
|
267
|
+
|
|
266
268
|
function buildRequest(
|
|
267
269
|
params: ToolParams,
|
|
268
270
|
model: string,
|
|
269
271
|
cfg: ResolvedConfig,
|
|
270
272
|
images: ImageInput[],
|
|
271
|
-
) {
|
|
272
|
-
const { action
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return {
|
|
273
|
+
): StandaloneImageRequest {
|
|
274
|
+
const { action: requestedAction } = resolveImageConfig(cfg, params);
|
|
275
|
+
const action: ConcreteImageAction =
|
|
276
|
+
requestedAction === "auto" ? (images.length > 0 ? "edit" : "generate") : requestedAction;
|
|
277
|
+
if (action === "edit" && images.length === 0)
|
|
278
|
+
throw new Error("action=edit requires at least one image.");
|
|
279
|
+
if (action === "generate" && images.length > 0)
|
|
280
|
+
throw new Error("action=generate does not accept images; use action=auto or action=edit.");
|
|
281
|
+
|
|
282
|
+
const common = {
|
|
283
|
+
prompt: params.prompt,
|
|
284
|
+
background: "auto",
|
|
284
285
|
model,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
tools: [tool],
|
|
288
|
-
tool_choice: { type: "image_generation" },
|
|
289
|
-
parallel_tool_calls: false,
|
|
290
|
-
store: false,
|
|
291
|
-
stream: true,
|
|
292
|
-
include: [],
|
|
293
|
-
client_metadata: { "x-codex-installation-id": "pi-better-openai" },
|
|
286
|
+
quality: "auto",
|
|
287
|
+
size: "auto",
|
|
294
288
|
};
|
|
289
|
+
if (action === "edit") {
|
|
290
|
+
return {
|
|
291
|
+
action,
|
|
292
|
+
endpoint: CODEX_IMAGE_EDITS_URL,
|
|
293
|
+
body: {
|
|
294
|
+
images: images.map((image) => ({
|
|
295
|
+
image_url: `data:${image.mimeType};base64,${image.data}`,
|
|
296
|
+
})),
|
|
297
|
+
...common,
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return { action, endpoint: CODEX_IMAGE_GENERATIONS_URL, body: common };
|
|
295
302
|
}
|
|
296
303
|
|
|
297
304
|
function dataUrlParts(value: string, fallbackMimeType: string): { data: string; mimeType: string } {
|
|
@@ -304,21 +311,72 @@ function dataUrlParts(value: string, fallbackMimeType: string): { data: string;
|
|
|
304
311
|
return { data: value.trim(), mimeType: fallbackMimeType };
|
|
305
312
|
}
|
|
306
313
|
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
| undefined
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
function parseImageResponsePayload(payload: unknown, requestId: string): ExtractedImageResult {
|
|
315
|
+
if (!isRecord(payload) || !Array.isArray(payload.data))
|
|
316
|
+
throw new Error("Invalid response from Codex Images API.");
|
|
317
|
+
|
|
318
|
+
let image: Record<string, unknown> | undefined;
|
|
319
|
+
for (const item of payload.data) {
|
|
320
|
+
if (isRecord(item) && typeof item.b64_json === "string" && item.b64_json.trim()) {
|
|
321
|
+
image = item;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!image) throw new Error("No image data returned by Codex Images API.");
|
|
326
|
+
|
|
327
|
+
const responseFormat =
|
|
328
|
+
typeof payload.output_format === "string" &&
|
|
329
|
+
(IMAGE_OUTPUT_FORMATS as readonly string[]).includes(payload.output_format)
|
|
330
|
+
? (payload.output_format as ImageOutputFormat)
|
|
331
|
+
: "png";
|
|
332
|
+
const { data, mimeType } = dataUrlParts(
|
|
333
|
+
image.b64_json as string,
|
|
334
|
+
imageMimeType(`image.${responseFormat}`, responseFormat),
|
|
335
|
+
);
|
|
336
|
+
const safeRequestId = requestId.replace(/[^a-zA-Z0-9_-]/g, "_") || randomUUID().slice(0, 8);
|
|
337
|
+
return {
|
|
338
|
+
id: typeof image.id === "string" ? image.id : `ig_${safeRequestId}`,
|
|
339
|
+
status: "completed",
|
|
340
|
+
revisedPrompt: typeof image.revised_prompt === "string" ? image.revised_prompt : undefined,
|
|
341
|
+
data,
|
|
342
|
+
mimeType,
|
|
319
343
|
};
|
|
320
344
|
}
|
|
321
345
|
|
|
346
|
+
async function parseImageResponse(
|
|
347
|
+
response: Response,
|
|
348
|
+
requestId: string,
|
|
349
|
+
): Promise<ExtractedImageResult> {
|
|
350
|
+
let payload: unknown;
|
|
351
|
+
try {
|
|
352
|
+
payload = await response.json();
|
|
353
|
+
} catch {
|
|
354
|
+
throw new Error("Invalid response from Codex Images API.");
|
|
355
|
+
}
|
|
356
|
+
return parseImageResponsePayload(payload, requestId);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function convertImageOutput(
|
|
360
|
+
image: ExtractedImageResult,
|
|
361
|
+
outputFormat: ImageOutputFormat,
|
|
362
|
+
): Promise<ExtractedImageResult> {
|
|
363
|
+
const targetMimeType = imageMimeType(`image.${outputFormat}`, outputFormat);
|
|
364
|
+
if (image.mimeType === targetMimeType) return image;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const input = sharp(Buffer.from(image.data, "base64"), { animated: false });
|
|
368
|
+
const output =
|
|
369
|
+
outputFormat === "jpeg"
|
|
370
|
+
? await input.jpeg().toBuffer()
|
|
371
|
+
: outputFormat === "webp"
|
|
372
|
+
? await input.webp().toBuffer()
|
|
373
|
+
: await input.png().toBuffer();
|
|
374
|
+
return { ...image, data: output.toString("base64"), mimeType: targetMimeType };
|
|
375
|
+
} catch {
|
|
376
|
+
throw new Error(`Failed to convert Codex image output to ${outputFormat}.`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
322
380
|
function isImageContent(
|
|
323
381
|
value: unknown,
|
|
324
382
|
): value is { type: "image"; data: string; mimeType: string } {
|
|
@@ -330,106 +388,6 @@ function isImageContent(
|
|
|
330
388
|
);
|
|
331
389
|
}
|
|
332
390
|
|
|
333
|
-
function extractImageFromEvent(
|
|
334
|
-
event: unknown,
|
|
335
|
-
fallbackMimeType: string,
|
|
336
|
-
): ExtractedImageResult | undefined {
|
|
337
|
-
if (!isRecord(event)) return undefined;
|
|
338
|
-
const item = asImageResultItem(event.item) ?? asImageResultItem(event);
|
|
339
|
-
if (item) {
|
|
340
|
-
const raw =
|
|
341
|
-
typeof item.result === "string" && item.result.trim()
|
|
342
|
-
? item.result
|
|
343
|
-
: typeof item.b64_json === "string"
|
|
344
|
-
? item.b64_json
|
|
345
|
-
: undefined;
|
|
346
|
-
if (!raw) return undefined;
|
|
347
|
-
const { data, mimeType } = dataUrlParts(raw, fallbackMimeType);
|
|
348
|
-
return {
|
|
349
|
-
id: typeof item.id === "string" ? item.id : `ig_${randomUUID().slice(0, 8)}`,
|
|
350
|
-
status: typeof item.status === "string" ? item.status : "completed",
|
|
351
|
-
revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : undefined,
|
|
352
|
-
data,
|
|
353
|
-
mimeType,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
const partial =
|
|
357
|
-
typeof event.partial_image_b64 === "string"
|
|
358
|
-
? event.partial_image_b64
|
|
359
|
-
: typeof event.b64_json === "string"
|
|
360
|
-
? event.b64_json
|
|
361
|
-
: undefined;
|
|
362
|
-
if (typeof partial === "string" && partial.trim()) {
|
|
363
|
-
const { data, mimeType } = dataUrlParts(partial, fallbackMimeType);
|
|
364
|
-
return { id: `ig_${randomUUID().slice(0, 8)}`, status: "partial", data, mimeType };
|
|
365
|
-
}
|
|
366
|
-
return undefined;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async function parseSseForImage(
|
|
370
|
-
response: Response,
|
|
371
|
-
fallbackMimeType: string,
|
|
372
|
-
signal?: AbortSignal,
|
|
373
|
-
): Promise<ExtractedImageResult> {
|
|
374
|
-
if (!response.body) throw new Error("No response body from Codex image request.");
|
|
375
|
-
const reader = response.body.getReader();
|
|
376
|
-
const decoder = new TextDecoder();
|
|
377
|
-
let buffer = "";
|
|
378
|
-
try {
|
|
379
|
-
while (true) {
|
|
380
|
-
if (signal?.aborted) throw new Error("Image request was aborted.");
|
|
381
|
-
const { done, value } = await reader.read();
|
|
382
|
-
if (done) break;
|
|
383
|
-
buffer += decoder.decode(value, { stream: true });
|
|
384
|
-
let boundary = SSE_EVENT_BOUNDARY.exec(buffer);
|
|
385
|
-
while (boundary) {
|
|
386
|
-
const idx = boundary.index;
|
|
387
|
-
const chunk = buffer.slice(0, idx);
|
|
388
|
-
buffer = buffer.slice(idx + boundary[0].length);
|
|
389
|
-
const data = chunk
|
|
390
|
-
.split(/\r?\n/)
|
|
391
|
-
.filter((line) => line.startsWith("data:"))
|
|
392
|
-
.map((line) => line.slice(5).trim())
|
|
393
|
-
.join("\n")
|
|
394
|
-
.trim();
|
|
395
|
-
if (data && data !== "[DONE]") {
|
|
396
|
-
let event: unknown;
|
|
397
|
-
try {
|
|
398
|
-
event = JSON.parse(data);
|
|
399
|
-
} catch {
|
|
400
|
-
event = undefined;
|
|
401
|
-
}
|
|
402
|
-
const image = extractImageFromEvent(event, fallbackMimeType);
|
|
403
|
-
if (image?.data && image.status === "completed") {
|
|
404
|
-
await reader.cancel().catch(() => undefined);
|
|
405
|
-
return image;
|
|
406
|
-
}
|
|
407
|
-
if (isRecord(event) && event.type === "response.failed") {
|
|
408
|
-
const error =
|
|
409
|
-
isRecord(event.response) && isRecord(event.response.error)
|
|
410
|
-
? event.response.error
|
|
411
|
-
: undefined;
|
|
412
|
-
const message = sanitizeDiagnosticError(
|
|
413
|
-
typeof error?.message === "string" ? error.message : "Codex image request failed.",
|
|
414
|
-
);
|
|
415
|
-
throw new Error(message);
|
|
416
|
-
}
|
|
417
|
-
if (isRecord(event) && event.type === "error") {
|
|
418
|
-
const message = sanitizeDiagnosticError(
|
|
419
|
-
typeof event.message === "string" ? event.message : JSON.stringify(event),
|
|
420
|
-
);
|
|
421
|
-
throw new Error(`Codex image error: ${message}`);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
boundary = SSE_EVENT_BOUNDARY.exec(buffer);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
} finally {
|
|
428
|
-
reader.releaseLock();
|
|
429
|
-
}
|
|
430
|
-
throw new Error("No completed image_generation_call result returned by Codex.");
|
|
431
|
-
}
|
|
432
|
-
|
|
433
391
|
async function requestCodexImage(
|
|
434
392
|
params: ToolParams,
|
|
435
393
|
ctx: ExtensionContext,
|
|
@@ -437,9 +395,10 @@ async function requestCodexImage(
|
|
|
437
395
|
requestSignal?: AbortSignal,
|
|
438
396
|
): Promise<CodexImageResult> {
|
|
439
397
|
if (!cfg.image.enabled) throw new Error("OpenAI image generation is disabled in config.");
|
|
398
|
+
if (!params.prompt.trim()) throw new Error("Image prompt must not be empty.");
|
|
440
399
|
const cwd = ctx.cwd || process.cwd();
|
|
441
|
-
const model = resolveModel(params,
|
|
442
|
-
const {
|
|
400
|
+
const model = resolveModel(params, cfg);
|
|
401
|
+
const { outputFormat, save } = resolveImageConfig(cfg, params);
|
|
443
402
|
const saveDir = resolveSaveDir(save, params, cwd);
|
|
444
403
|
const timeoutSignal = AbortSignal.timeout(cfg.image.timeoutMs);
|
|
445
404
|
const baseSignal = requestSignal ?? ctx.signal;
|
|
@@ -447,18 +406,19 @@ async function requestCodexImage(
|
|
|
447
406
|
const credentials = await getCredentials(ctx, signal);
|
|
448
407
|
const images = await readImageInputs(params.images, cwd);
|
|
449
408
|
const request = buildRequest(params, model, cfg, images);
|
|
450
|
-
const
|
|
409
|
+
const requestId = randomUUID();
|
|
410
|
+
const response = await fetch(request.endpoint, {
|
|
451
411
|
method: "POST",
|
|
452
412
|
headers: {
|
|
453
413
|
authorization: `Bearer ${credentials.accessToken}`,
|
|
454
414
|
"chatgpt-account-id": credentials.accountId,
|
|
455
|
-
|
|
456
|
-
accept: "text/event-stream",
|
|
415
|
+
accept: "application/json",
|
|
457
416
|
"content-type": "application/json",
|
|
458
417
|
originator: "codex_cli_rs",
|
|
459
418
|
"User-Agent": "codex_cli_rs/0.0.0 (pi-better-openai)",
|
|
419
|
+
"x-codex-image-turn-id": requestId,
|
|
460
420
|
},
|
|
461
|
-
body: JSON.stringify(request),
|
|
421
|
+
body: JSON.stringify(request.body),
|
|
462
422
|
signal,
|
|
463
423
|
});
|
|
464
424
|
if (!response.ok) {
|
|
@@ -467,15 +427,19 @@ async function requestCodexImage(
|
|
|
467
427
|
: "";
|
|
468
428
|
throw new Error(`Codex image request failed (${response.status}${statusText}).`);
|
|
469
429
|
}
|
|
470
|
-
const parsed = await
|
|
471
|
-
|
|
472
|
-
imageMimeType(`image.${outputFormat}`, outputFormat),
|
|
473
|
-
signal,
|
|
474
|
-
);
|
|
430
|
+
const parsed = await parseImageResponse(response, requestId);
|
|
431
|
+
const converted = await convertImageOutput(parsed, outputFormat);
|
|
475
432
|
const savedPath = saveDir
|
|
476
|
-
? await saveImage(
|
|
433
|
+
? await saveImage(converted.data, outputFormat, saveDir, converted.id)
|
|
477
434
|
: undefined;
|
|
478
|
-
return {
|
|
435
|
+
return {
|
|
436
|
+
...converted,
|
|
437
|
+
prompt: params.prompt,
|
|
438
|
+
savedPath,
|
|
439
|
+
model,
|
|
440
|
+
action: request.action,
|
|
441
|
+
outputFormat,
|
|
442
|
+
};
|
|
479
443
|
}
|
|
480
444
|
|
|
481
445
|
function displayPath(path: string): string {
|
|
@@ -488,7 +452,7 @@ function displayPath(path: string): string {
|
|
|
488
452
|
|
|
489
453
|
function resultText(result: CodexImageResult): string {
|
|
490
454
|
const parts = [
|
|
491
|
-
`Generated image using OpenAI
|
|
455
|
+
`Generated image using the OpenAI Codex Images API with ${result.model}.`,
|
|
492
456
|
`Action: ${result.action}.`,
|
|
493
457
|
`Prompt: ${result.prompt}`,
|
|
494
458
|
];
|
|
@@ -534,8 +498,8 @@ export function registerOpenAIImage(
|
|
|
534
498
|
authFound: credentials !== undefined,
|
|
535
499
|
authSource: credentials?.source,
|
|
536
500
|
accountId: maskIdentifier(credentials?.accountId),
|
|
537
|
-
endpoint:
|
|
538
|
-
defaultModel:
|
|
501
|
+
endpoint: CODEX_IMAGES_BASE_URL,
|
|
502
|
+
defaultModel: cfg.image.defaultModel,
|
|
539
503
|
defaultSave: cfg.image.defaultSave,
|
|
540
504
|
enabled: cfg.image.enabled,
|
|
541
505
|
lastStatus,
|
|
@@ -596,7 +560,7 @@ export function registerOpenAIImage(
|
|
|
596
560
|
});
|
|
597
561
|
|
|
598
562
|
pi.registerCommand(OPENAI_IMAGE_COMMAND, {
|
|
599
|
-
description: "Generate an image with OpenAI Codex
|
|
563
|
+
description: "Generate an image with the standalone OpenAI Codex Images API",
|
|
600
564
|
handler: async (args, ctx) => {
|
|
601
565
|
const prompt = args.trim();
|
|
602
566
|
if (!prompt) {
|
|
@@ -621,7 +585,7 @@ export function registerOpenAIImage(
|
|
|
621
585
|
name: OPENAI_IMAGE_TOOL,
|
|
622
586
|
label: "OpenAI image",
|
|
623
587
|
description:
|
|
624
|
-
"Generate or edit images through
|
|
588
|
+
"Generate or edit images through the standalone Codex Images API using OpenAI subscription auth. Supports local reference/edit images and saves to the project by default.",
|
|
625
589
|
promptSnippet: "Generate or edit raster images via OpenAI Codex subscription auth.",
|
|
626
590
|
promptGuidelines: [
|
|
627
591
|
"Use openai_image when the user asks to generate or edit a raster image, photo, illustration, mockup, texture, sprite, or bitmap asset.",
|
|
@@ -631,12 +595,12 @@ export function registerOpenAIImage(
|
|
|
631
595
|
parameters: TOOL_PARAMS,
|
|
632
596
|
async execute(_toolCallId, params: ToolParams, signal, onUpdate, ctx) {
|
|
633
597
|
const cfg = getConfig(ctx);
|
|
634
|
-
const model = resolveModel(params,
|
|
598
|
+
const model = resolveModel(params, cfg);
|
|
635
599
|
onUpdate?.({
|
|
636
600
|
content: [
|
|
637
601
|
{
|
|
638
602
|
type: "text",
|
|
639
|
-
text: `Requesting OpenAI
|
|
603
|
+
text: `Requesting OpenAI Codex image via ${model}...`,
|
|
640
604
|
},
|
|
641
605
|
],
|
|
642
606
|
details: undefined,
|
|
@@ -656,7 +620,9 @@ export function registerOpenAIImage(
|
|
|
656
620
|
}
|
|
657
621
|
|
|
658
622
|
export const _imageTest = {
|
|
659
|
-
|
|
623
|
+
CODEX_IMAGES_BASE_URL,
|
|
624
|
+
CODEX_IMAGE_GENERATIONS_URL,
|
|
625
|
+
CODEX_IMAGE_EDITS_URL,
|
|
660
626
|
DEFAULT_TIMEOUT_MS,
|
|
661
627
|
OPENAI_IMAGE_TOOL,
|
|
662
628
|
OPENAI_IMAGE_COMMAND,
|
|
@@ -666,7 +632,7 @@ export const _imageTest = {
|
|
|
666
632
|
extractAccountIdFromJwt,
|
|
667
633
|
imageMimeType,
|
|
668
634
|
dataUrlParts,
|
|
669
|
-
|
|
635
|
+
parseImageResponsePayload,
|
|
670
636
|
displayPath,
|
|
671
637
|
buildRequest,
|
|
672
638
|
};
|
package/src/live/controller.ts
CHANGED
|
@@ -25,7 +25,14 @@ Treat delegation context as your own internal progress and result. Never mention
|
|
|
25
25
|
|
|
26
26
|
For greetings, clarification, or ordinary conversation that needs no tools, answer directly without delegating. Preserve one-assistant continuity: converse here, delegate execution, then communicate the returned result as your own.`;
|
|
27
27
|
|
|
28
|
-
export type LivePhase =
|
|
28
|
+
export type LivePhase =
|
|
29
|
+
| "standby"
|
|
30
|
+
| "connecting"
|
|
31
|
+
| "listening"
|
|
32
|
+
| "working"
|
|
33
|
+
| "speaking"
|
|
34
|
+
| "muted"
|
|
35
|
+
| "error";
|
|
29
36
|
|
|
30
37
|
export interface LiveTranscript {
|
|
31
38
|
role: "user" | "assistant";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Terminal focus tracking via private mode 1004 (xterm focus-event reporting).
|
|
2
|
+
// Enabling `CSI ? 1004 h` makes the terminal emit `CSI I` on focus-in and
|
|
3
|
+
// `CSI O` on focus-out as input sequences; pi-tui's stdin buffer frames them
|
|
4
|
+
// as generic CSI and forwards them to input listeners unmodified, so an
|
|
5
|
+
// extension can observe them without any upstream keymap changes. Support is
|
|
6
|
+
// detected with a DECRQM probe (`CSI ? 1004 $ p` -> `CSI ? 1004 ; 1 $ y` set /
|
|
7
|
+
// `; 2 $ y` reset); terminals that ignore both sequences simply fall back to
|
|
8
|
+
// the FIFO floor policy. Supported by iTerm2, kitty, WezTerm, Alacritty, foot,
|
|
9
|
+
// Windows Terminal, xterm.js, and modern tmux.
|
|
10
|
+
|
|
11
|
+
export const FOCUS_REPORTING_ENABLE = "\x1b[?1004h";
|
|
12
|
+
export const FOCUS_REPORTING_DISABLE = "\x1b[?1004l";
|
|
13
|
+
export const FOCUS_IN_SEQUENCE = "\x1b[I";
|
|
14
|
+
export const FOCUS_OUT_SEQUENCE = "\x1b[O";
|
|
15
|
+
export const DECRQM_FOCUS_QUERY = "\x1b[?1004$p";
|
|
16
|
+
|
|
17
|
+
export type FocusInputListener = (data: string) => { consume?: boolean; data?: string } | undefined;
|
|
18
|
+
|
|
19
|
+
export interface FocusTerminalHandle {
|
|
20
|
+
write(data: string): void;
|
|
21
|
+
addInputListener(listener: FocusInputListener): () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseFocusSequence(data: string): boolean | undefined {
|
|
25
|
+
if (data === FOCUS_IN_SEQUENCE) return true;
|
|
26
|
+
if (data === FOCUS_OUT_SEQUENCE) return false;
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseDecrqmFocusResponse(data: string): boolean | undefined {
|
|
31
|
+
if (data === "\x1b[?1004;1$y" || data === "\x1b[?1004;3$y") return true;
|
|
32
|
+
if (data === "\x1b[?1004;2$y" || data === "\x1b[?1004;4$y") return false;
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function probeFocusReporting(
|
|
37
|
+
handle: FocusTerminalHandle,
|
|
38
|
+
timeoutMs = 400,
|
|
39
|
+
): Promise<boolean> {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
let settled = false;
|
|
42
|
+
let removeListener: (() => void) | undefined;
|
|
43
|
+
const done = (supported: boolean) => {
|
|
44
|
+
if (settled) return;
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
removeListener?.();
|
|
48
|
+
resolve(supported);
|
|
49
|
+
};
|
|
50
|
+
const timer = setTimeout(() => done(false), timeoutMs);
|
|
51
|
+
timer.unref?.();
|
|
52
|
+
try {
|
|
53
|
+
removeListener = handle.addInputListener((data) => {
|
|
54
|
+
const state = parseDecrqmFocusResponse(data);
|
|
55
|
+
if (state === undefined) return undefined;
|
|
56
|
+
done(true);
|
|
57
|
+
return { consume: true };
|
|
58
|
+
});
|
|
59
|
+
handle.write(DECRQM_FOCUS_QUERY);
|
|
60
|
+
} catch {
|
|
61
|
+
done(false);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function attachFocusReporting(
|
|
67
|
+
handle: FocusTerminalHandle,
|
|
68
|
+
onFocus: (focused: boolean) => void,
|
|
69
|
+
): () => void {
|
|
70
|
+
const removeListener = handle.addInputListener((data) => {
|
|
71
|
+
const focused = parseFocusSequence(data);
|
|
72
|
+
if (focused === undefined) return undefined;
|
|
73
|
+
onFocus(focused);
|
|
74
|
+
return { consume: true };
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
handle.write(FOCUS_REPORTING_ENABLE);
|
|
78
|
+
} catch {
|
|
79
|
+
// The terminal write path is best-effort; focus events simply never arrive.
|
|
80
|
+
}
|
|
81
|
+
let disposed = false;
|
|
82
|
+
return () => {
|
|
83
|
+
if (disposed) return;
|
|
84
|
+
disposed = true;
|
|
85
|
+
removeListener();
|
|
86
|
+
try {
|
|
87
|
+
handle.write(FOCUS_REPORTING_DISABLE);
|
|
88
|
+
} catch {
|
|
89
|
+
// Nothing to undo on a torn-down tty.
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|