@blockrun/franklin 3.25.1 → 3.25.2

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/dist/ui/app.js CHANGED
@@ -19,6 +19,7 @@ import { estimateCost } from '../pricing.js';
19
19
  import { formatTokens, shortModelName } from '../stats/format.js';
20
20
  import { mouse, forceDisableMouseTracking } from './mouse.js';
21
21
  import { resolveAskUserAnswer } from './ask-user-answer.js';
22
+ import { looksLikeImagePasteStub } from './paste-heuristics.js';
22
23
  // ─── Full-width input box ──────────────────────────────────────────────────
23
24
  const BRACKETED_PASTE_START = '[200~';
24
25
  const BRACKETED_PASTE_END = '[201~';
@@ -445,53 +446,58 @@ function PromptTextInput({ value, onChange, onSubmit, placeholder = '', focus =
445
446
  const buffered = pasteBufferRef.current;
446
447
  pasteBufferRef.current = '';
447
448
  pasteActiveRef.current = false;
448
- // Image-paste detection. Original heuristic (3.25.0) only probed the
449
- // clipboard when the bracketed-paste buffer was empty, assuming Cmd+V
450
- // on an image-only clipboard always arrived as an empty paste. That
451
- // assumption holds for macOS Terminal/iTerm2 but NOT for several Linux
452
- // terminals some send a filename, a `file://` URI, or a short binary
453
- // header alongside the image, which made the gate skip the probe and
454
- // the image silently dropped (user-reported on Kali: the clipboard
455
- // image was readable by other tools but Franklin couldn't paste it).
456
- // Verified in a Lima Ubuntu VM with xclip non-empty buffer triggered
457
- // the skip.
458
- //
459
- // Fix: ALWAYS probe the clipboard on a paste-end. If an image is there,
460
- // it wins; the bracketed-paste buffer text is treated as terminal noise
461
- // and dropped. If no image, fall through to the normal text-paste path
462
- // (collapse-to-block or inline) so text pastes are unaffected. The
463
- // probe is async (osascript / xclip / wl-paste shell-out, 30-100 ms),
464
- // so the handler returns immediately and updateValue happens when the
465
- // Promise resolves.
449
+ // Image-paste detection. Cmd+V on a clipboard image arrives as an empty
450
+ // bracketed paste on macOS Terminal/iTerm2; several Linux terminals
451
+ // instead emit a filename, a `file://` URI, or the raw image header
452
+ // alongside it (3.25.0 only probed on an empty buffer, so those Linux
453
+ // shapes silently dropped the image fixed in #77). We probe the system
454
+ // clipboard for an image only when the buffer *looks* like one of those
455
+ // stubs; genuine text is inserted synchronously below so the common
456
+ // paste path never waits on the async osascript / xclip / wl-paste
457
+ // shell-out (30-100 ms, but a cold spawn can be more).
466
458
  const insertAt = currentCursorOffset;
467
- tryReadClipboardImage().then((img) => {
468
- if (img && 'path' in img) {
469
- // Image wins — drop the bracketed-paste buffer (likely a stub the
470
- // terminal sent for the image).
471
- const injected = encodeImageBlock(img.path);
472
- const cur = valueRef.current;
473
- const at = Math.min(insertAt, cur.length);
474
- updateValue(cur.slice(0, at) + injected + cur.slice(at), at + injected.length);
459
+ const insertPastedText = (buf, baseOffset) => {
460
+ if (buf.length === 0)
475
461
  return;
476
- }
477
- if (img && 'error' in img) {
478
- const injected = `[Image rejected: ${img.error}] `;
479
- const cur = valueRef.current;
480
- const at = Math.min(insertAt, cur.length);
481
- updateValue(cur.slice(0, at) + injected + cur.slice(at), at + injected.length);
482
- return;
483
- }
484
- // No image on the clipboard — treat as a normal text paste.
485
- if (buffered.length === 0)
486
- return;
487
- const lineCount = buffered.split('\n').length;
462
+ const lineCount = buf.split('\n').length;
488
463
  const textToInsert = lineCount >= PASTE_COLLAPSE_LINE_THRESHOLD
489
- ? encodePasteBlock(buffered)
490
- : buffered;
464
+ ? encodePasteBlock(buf)
465
+ : buf;
491
466
  const cur = valueRef.current;
492
- const at = Math.min(insertAt, cur.length);
467
+ const at = Math.min(baseOffset, cur.length);
493
468
  updateValue(cur.slice(0, at) + textToInsert + cur.slice(at), at + textToInsert.length);
494
- }).catch(() => { });
469
+ };
470
+ if (looksLikeImagePasteStub(buffered)) {
471
+ // The probe is async; the handler returns now and updateValue happens
472
+ // when the Promise resolves. insertAt (captured above) pins the result
473
+ // to where the user pasted even if the cursor moved meanwhile.
474
+ tryReadClipboardImage().then((img) => {
475
+ if (img && 'path' in img) {
476
+ // Image wins — drop the bracketed-paste buffer (the terminal stub).
477
+ const injected = encodeImageBlock(img.path);
478
+ const cur = valueRef.current;
479
+ const at = Math.min(insertAt, cur.length);
480
+ updateValue(cur.slice(0, at) + injected + cur.slice(at), at + injected.length);
481
+ return;
482
+ }
483
+ if (img && 'error' in img) {
484
+ const injected = `[Image rejected: ${img.error}] `;
485
+ const cur = valueRef.current;
486
+ const at = Math.min(insertAt, cur.length);
487
+ updateValue(cur.slice(0, at) + injected + cur.slice(at), at + injected.length);
488
+ return;
489
+ }
490
+ // No image after all — the stub was literal text (e.g. a lone
491
+ // "photo.png" the user actually typed). Insert it as text.
492
+ insertPastedText(buffered, insertAt);
493
+ }).catch(() => {
494
+ // Probe failed unexpectedly — don't lose the paste; insert as text.
495
+ insertPastedText(buffered, insertAt);
496
+ });
497
+ return;
498
+ }
499
+ // Genuine text paste — insert synchronously, no clipboard probe.
500
+ insertPastedText(buffered, currentCursorOffset);
495
501
  return;
496
502
  }
497
503
  if (!text) {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pure heuristics for the bracketed-paste handler in app.tsx, split out so they
3
+ * can be unit-tested without pulling in Ink/React.
4
+ */
5
+ /**
6
+ * Does a bracketed-paste buffer look like a terminal's stand-in for an image
7
+ * paste rather than genuine pasted text?
8
+ *
9
+ * Cmd+V on a clipboard image yields an *empty* buffer on macOS Terminal/iTerm2,
10
+ * but several Linux terminals instead emit a filename, a `file://` URI, or the
11
+ * raw image header alongside the paste. Those shapes — and only those — warrant
12
+ * the (async, 30-100 ms) clipboard probe in app.tsx. Substantial text returns
13
+ * `false` so it can be inserted synchronously, keeping the common paste path
14
+ * instant instead of waiting on an osascript / xclip / wl-paste shell-out.
15
+ *
16
+ * False positives are harmless: a literal text paste of "photo.png" probes the
17
+ * clipboard, finds no image, and falls through to the text path anyway.
18
+ */
19
+ export declare function looksLikeImagePasteStub(buffered: string): boolean;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Pure heuristics for the bracketed-paste handler in app.tsx, split out so they
3
+ * can be unit-tested without pulling in Ink/React.
4
+ */
5
+ // Image filenames a terminal might substitute for a pasted image. Anchored, so
6
+ // only a string that *is* such a path matches — not prose that mentions one.
7
+ const IMAGE_FILE_EXT = /\.(png|jpe?g|gif|webp|bmp|tiff?|heic|heif|avif)$/i;
8
+ /**
9
+ * Does a bracketed-paste buffer look like a terminal's stand-in for an image
10
+ * paste rather than genuine pasted text?
11
+ *
12
+ * Cmd+V on a clipboard image yields an *empty* buffer on macOS Terminal/iTerm2,
13
+ * but several Linux terminals instead emit a filename, a `file://` URI, or the
14
+ * raw image header alongside the paste. Those shapes — and only those — warrant
15
+ * the (async, 30-100 ms) clipboard probe in app.tsx. Substantial text returns
16
+ * `false` so it can be inserted synchronously, keeping the common paste path
17
+ * instant instead of waiting on an osascript / xclip / wl-paste shell-out.
18
+ *
19
+ * False positives are harmless: a literal text paste of "photo.png" probes the
20
+ * clipboard, finds no image, and falls through to the text path anyway.
21
+ */
22
+ export function looksLikeImagePasteStub(buffered) {
23
+ // macOS image paste: empty / whitespace-only bracketed paste.
24
+ if (buffered.trim().length === 0)
25
+ return true;
26
+ // Raw binary the terminal dumped (e.g. PNG's \x1a, or bytes that failed
27
+ // UTF-8 decoding into U+FFFD). Genuine text never carries control chars
28
+ // other than tab / newline / carriage return.
29
+ if (/[\x00-\x08\x0b\x0c\x0e-\x1f�]/.test(buffered))
30
+ return true;
31
+ // A single-line file reference the terminal sent instead of the image bytes.
32
+ const trimmed = buffered.trim();
33
+ if (!trimmed.includes('\n')) {
34
+ if (/^file:\/\//i.test(trimmed))
35
+ return true; // file:// URI
36
+ if (IMAGE_FILE_EXT.test(trimmed))
37
+ return true; // bare filename / path
38
+ }
39
+ return false;
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.25.1",
3
+ "version": "3.25.2",
4
4
  "description": "Franklin Agent — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {