@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 CHANGED
@@ -85,7 +85,7 @@ Example config:
85
85
  },
86
86
  "image": {
87
87
  "enabled": true,
88
- "defaultModel": "gpt-5.5",
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`: Codex image model override, for example `openai-codex/gpt-5.5`.
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.23",
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": "npm run typecheck && npm run lint && npm run format:check && npm test",
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: "gpt-5.5",
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-5.5", "gpt-5.4", "gpt-5.2", "gpt-5"],
280
- description: "Mainline model used for image generation when current model is not openai-codex.",
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: "Generated image file format.",
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.trim();
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 { ResolvedConfig } from "./config.ts";
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 CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
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
- "OpenAI Codex model to drive the hosted image_generation tool. Defaults to current openai-codex model or config default.",
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: "Generated image format.",
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
- if (model) return model.includes("/") ? model.split("/").pop() || model : model;
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, outputFormat } = resolveImageConfig(cfg, params);
273
- const content: Array<Record<string, unknown>> = [{ type: "input_text", text: params.prompt }];
274
- for (const image of images) {
275
- content.push({
276
- type: "input_image",
277
- detail: "auto",
278
- image_url: `data:${image.mimeType};base64,${image.data}`,
279
- });
280
- }
281
- const tool: Record<string, unknown> = { type: "image_generation", output_format: outputFormat };
282
- if (action !== "auto") tool.action = action;
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
- instructions: "",
286
- input: [{ role: "user", content }],
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 asImageResultItem(
308
- value: unknown,
309
- ):
310
- | { id?: string; status?: string; revised_prompt?: string; result?: string; b64_json?: string }
311
- | undefined {
312
- if (!isRecord(value) || value.type !== "image_generation_call") return undefined;
313
- return value as {
314
- id?: string;
315
- status?: string;
316
- revised_prompt?: string;
317
- result?: string;
318
- b64_json?: string;
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, ctx, cfg);
442
- const { action, outputFormat, save } = resolveImageConfig(cfg, params);
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 response = await fetch(CODEX_RESPONSES_URL, {
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
- "OpenAI-Beta": "responses=experimental",
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 parseSseForImage(
471
- response,
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(parsed.data, outputFormat, saveDir, parsed.id)
433
+ ? await saveImage(converted.data, outputFormat, saveDir, converted.id)
477
434
  : undefined;
478
- return { ...parsed, prompt: params.prompt, savedPath, model, action, outputFormat };
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 image_generation tool via openai-codex/${result.model}.`,
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: CODEX_RESPONSES_URL,
538
- defaultModel: ctx.model?.provider === "openai-codex" ? ctx.model.id : cfg.image.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 image generation",
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 OpenAI Codex subscription auth using the hosted image_generation tool. Supports local reference/edit images and saves to the project by default.",
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, ctx, cfg);
598
+ const model = resolveModel(params, cfg);
635
599
  onUpdate?.({
636
600
  content: [
637
601
  {
638
602
  type: "text",
639
- text: `Requesting OpenAI image_generation via openai-codex/${model}...`,
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
- CODEX_RESPONSES_URL,
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
- extractImageFromEvent,
635
+ parseImageResponsePayload,
670
636
  displayPath,
671
637
  buildRequest,
672
638
  };
@@ -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 = "connecting" | "listening" | "working" | "speaking" | "muted" | "error";
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
+ }