pi-banana 2.5.2 → 2.5.3

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.
@@ -30,7 +30,7 @@ import {
30
30
  Text,
31
31
  } from "@earendil-works/pi-tui";
32
32
  import { GoogleGenAI } from "@google/genai";
33
- import { existsSync } from "node:fs";
33
+ import { existsSync, readFileSync } from "node:fs";
34
34
  import { homedir } from "node:os";
35
35
  import { mkdir, readFile, writeFile } from "node:fs/promises";
36
36
  import { extname, isAbsolute, resolve } from "node:path";
@@ -222,6 +222,19 @@ async function loadReferenceImage(
222
222
  return { data: buf.toString("base64"), mimeType };
223
223
  }
224
224
 
225
+ // Inline preview bytes, loaded at render time from the file the tool wrote.
226
+ // Nothing image-sized is kept on the tool result itself: content goes to the
227
+ // model and details go into the saved session, and a base64 PNG in either one
228
+ // is counted against the context by request guards for the rest of the session.
229
+ function readPreview(outputPath?: string): string | undefined {
230
+ if (typeof outputPath !== "string" || !outputPath) return undefined;
231
+ try {
232
+ return readFileSync(outputPath).toString("base64");
233
+ } catch {
234
+ return undefined; // moved, deleted, or unreadable: show the rest of the card
235
+ }
236
+ }
237
+
225
238
  async function resolveOutputPath(
226
239
  cwd: string,
227
240
  prompt: string,
@@ -389,7 +402,7 @@ export default function (pi: ExtensionAPI) {
389
402
  details: {
390
403
  prompt: params.prompt, model: oaiModel, quality, aspectRatio, imageSize,
391
404
  mimeType: "image/png", outputPath: outPath, editing,
392
- referenceImages: params.referenceImages, imageBase64: buf.toString("base64"),
405
+ referenceImages: params.referenceImages,
393
406
  },
394
407
  };
395
408
  }
@@ -510,9 +523,9 @@ export default function (pi: ExtensionAPI) {
510
523
  : `Generated → ${outPath}`;
511
524
 
512
525
  return {
513
- // Only text in content — the base64 image goes into details
514
- // so it does not bloat conversation history sent to the LLM.
515
- // renderResult reads the image from details for inline display.
526
+ // Only text in content, and no image bytes in details either:
527
+ // details are persisted in the session and counted by request
528
+ // guards. renderResult loads the preview from outputPath.
516
529
  content: [{ type: "text", text: summary }],
517
530
  details: {
518
531
  prompt: params.prompt,
@@ -524,14 +537,15 @@ export default function (pi: ExtensionAPI) {
524
537
  outputPath: outPath,
525
538
  editing,
526
539
  referenceImages: params.referenceImages,
527
- imageBase64: base64,
528
540
  },
529
541
  };
530
542
  },
531
543
 
532
544
  renderResult(result, _options, theme) {
533
- // Image is in details (not content) so base64 does not bloat
534
- // conversation history sent to the LLM on subsequent turns.
545
+ // No image bytes travel with the result: content stays text so it
546
+ // does not bloat LLM history, and details stay small so the saved
547
+ // session does not carry a megabyte per picture. The preview is
548
+ // read back from the file that was just written.
535
549
  const { details, content } = result;
536
550
  const container = new Container();
537
551
 
@@ -542,11 +556,12 @@ export default function (pi: ExtensionAPI) {
542
556
  container.addChild(new Text(theme.fg("success", "✔ " + summaryText), 1, 0));
543
557
  }
544
558
 
545
- // 2. Image from details
546
- if (details && (details as any).imageBase64) {
559
+ // 2. Image, read from the saved file
560
+ const preview = readPreview((details as any)?.outputPath);
561
+ if (preview) {
547
562
  container.addChild(new Spacer(1));
548
563
  container.addChild(
549
- new Image((details as any).imageBase64, (details as any).mimeType ?? "image/png", {
564
+ new Image(preview, (details as any).mimeType ?? "image/png", {
550
565
  fallbackColor: (s: string) => theme.fg("muted", s),
551
566
  }, {
552
567
  maxWidthCells: 80,
@@ -69,7 +69,11 @@ export async function resolveProvider(ctx: any): Promise<Provider> {
69
69
 
70
70
  // ─── OpenAI-compat calls ─────────────────────────────────────────────
71
71
 
72
- /** Map aspect ratio + size tier to a generic WxH string. */
72
+ /** Round to the nearest multiple of 16 (never below 16): the OpenAI-compatible
73
+ * image upstream rejects sizes whose width or height is not divisible by 16. */
74
+ const round16 = (n: number): number => Math.max(16, Math.round(n / 16) * 16);
75
+
76
+ /** Map aspect ratio + size tier to a generic WxH string (multiples of 16). */
73
77
  export function sizeFor(aspectRatio: string, imageSize: string): string {
74
78
  const base = imageSize === "4K" ? 4096 : imageSize === "2K" ? 2048 : 1024;
75
79
  const ratio = (() => {
@@ -77,8 +81,8 @@ export function sizeFor(aspectRatio: string, imageSize: string): string {
77
81
  return w && h ? w / h : 1;
78
82
  })();
79
83
  if (Math.abs(ratio - 1) < 0.01) return `${base}x${base}`;
80
- if (ratio > 1) return `${base}x${Math.round(base / ratio)}`;
81
- return `${Math.round(base * ratio)}x${base}`;
84
+ if (ratio > 1) return `${round16(base)}x${round16(Math.round(base / ratio))}`;
85
+ return `${round16(Math.round(base * ratio))}x${round16(base)}`;
82
86
  }
83
87
 
84
88
  async function asBuffer(data: any): Promise<Buffer> {
@@ -121,10 +125,15 @@ export async function openaiEditImage(
121
125
  form.append("model", opts.model);
122
126
  form.append("prompt", opts.prompt);
123
127
  form.append("size", opts.size);
128
+ // A single reference image is the plain "image" file part. Multiple
129
+ // references must be appended as repeated "image[]" parts: the
130
+ // /images/edits upstream rejects duplicate "image" fields with
131
+ // 400 "Duplicate parameter image".
132
+ const field = opts.images.length > 1 ? "image[]" : "image";
124
133
  for (const img of opts.images) {
125
134
  const bytes = Buffer.from(img.data, "base64");
126
135
  const ext = img.mimeType.split("/")[1] ?? "png";
127
- form.append("image", new Blob([bytes], { type: img.mimeType }), `ref.${ext}`);
136
+ form.append(field, new Blob([bytes], { type: img.mimeType }), `ref.${ext}`);
128
137
  }
129
138
  const res = await fetch(`${config.baseUrl}/images/edits`, {
130
139
  method: "POST",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-banana",
3
- "version": "2.5.2",
3
+ "version": "2.5.3",
4
4
  "description": "Generate, edit, and analyze images in pi using Google Nano Banana (image gen) and Gemini Vision (analysis). Inline terminal preview, reference-image editing, auto-save.",
5
5
  "author": "Francesco Frapporti <effedue@gmail.com>",
6
6
  "license": "MIT",