@blockrun/franklin 3.25.0 → 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 +51 -28
- package/dist/ui/paste-heuristics.d.ts +19 -0
- package/dist/ui/paste-heuristics.js +40 -0
- package/package.json +1 -1
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,37 +446,59 @@ 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
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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).
|
|
458
|
+
const insertAt = currentCursorOffset;
|
|
459
|
+
const insertPastedText = (buf, baseOffset) => {
|
|
460
|
+
if (buf.length === 0)
|
|
461
|
+
return;
|
|
462
|
+
const lineCount = buf.split('\n').length;
|
|
463
|
+
const textToInsert = lineCount >= PASTE_COLLAPSE_LINE_THRESHOLD
|
|
464
|
+
? encodePasteBlock(buf)
|
|
465
|
+
: buf;
|
|
466
|
+
const cur = valueRef.current;
|
|
467
|
+
const at = Math.min(baseOffset, cur.length);
|
|
468
|
+
updateValue(cur.slice(0, at) + textToInsert + cur.slice(at), at + textToInsert.length);
|
|
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.
|
|
461
474
|
tryReadClipboardImage().then((img) => {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
injected = encodeImageBlock(img.path);
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return;
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
+
});
|
|
473
497
|
return;
|
|
474
498
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
: buffered;
|
|
499
|
+
// Genuine text paste — insert synchronously, no clipboard probe.
|
|
500
|
+
insertPastedText(buffered, currentCursorOffset);
|
|
501
|
+
return;
|
|
479
502
|
}
|
|
480
503
|
if (!text) {
|
|
481
504
|
if (hasPasteEnd)
|
|
@@ -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