@blockrun/franklin 3.25.3 → 3.25.4
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 +50 -7
- package/dist/ui/vim-input.d.ts +6 -1
- package/dist/ui/vim-input.js +24 -1
- package/package.json +1 -1
package/dist/ui/app.js
CHANGED
|
@@ -61,6 +61,21 @@ function encodePasteBlock(content) {
|
|
|
61
61
|
function encodeImageBlock(absolutePath) {
|
|
62
62
|
return `${IMG_BLOCK_START}${Buffer.from(absolutePath, 'utf8').toString('base64')}${IMG_BLOCK_END}`;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Probe the clipboard for an image and return the input-block to splice in at
|
|
66
|
+
* the cursor — an encoded `[IMG:…]` block on success, an inline
|
|
67
|
+
* `[Image rejected: …]` notice if the image was found but unusable, or null
|
|
68
|
+
* when there's no image. Shared by PromptTextInput's Ctrl+V path and VimInput
|
|
69
|
+
* (which renders instead of PromptTextInput in vim mode).
|
|
70
|
+
*/
|
|
71
|
+
async function readClipboardImageInjection() {
|
|
72
|
+
const img = await tryReadClipboardImage();
|
|
73
|
+
if (img && 'path' in img)
|
|
74
|
+
return encodeImageBlock(img.path);
|
|
75
|
+
if (img && 'error' in img)
|
|
76
|
+
return `[Image rejected: ${img.error}] `;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
64
79
|
function decodeBlockPayload(token, startMarker, endMarker) {
|
|
65
80
|
if (!token.startsWith(startMarker) || !token.endsWith(endMarker))
|
|
66
81
|
return token;
|
|
@@ -126,6 +141,15 @@ function decodePromptValue(value) {
|
|
|
126
141
|
}
|
|
127
142
|
return decoded + value.slice(cursor);
|
|
128
143
|
}
|
|
144
|
+
function promptValueForDisplay(value) {
|
|
145
|
+
let rendered = '';
|
|
146
|
+
let cursor = 0;
|
|
147
|
+
for (const block of findPasteBlocks(value)) {
|
|
148
|
+
rendered += value.slice(cursor, block.start) + pasteSummary(block);
|
|
149
|
+
cursor = block.end;
|
|
150
|
+
}
|
|
151
|
+
return rendered + value.slice(cursor);
|
|
152
|
+
}
|
|
129
153
|
/**
|
|
130
154
|
* Read the system clipboard, and if it currently holds an image, save it to
|
|
131
155
|
* a temp file and return the absolute path. Otherwise return null.
|
|
@@ -378,6 +402,15 @@ function PromptTextInput({ value, onChange, onSubmit, placeholder = '', focus =
|
|
|
378
402
|
onChange(nextValue);
|
|
379
403
|
setCursorOffset(cursorOffsetRef.current);
|
|
380
404
|
}, [onChange]);
|
|
405
|
+
const insertClipboardImageAt = useCallback((insertAt) => {
|
|
406
|
+
readClipboardImageInjection().then((injected) => {
|
|
407
|
+
if (!injected)
|
|
408
|
+
return; // no image on clipboard — nothing to do
|
|
409
|
+
const cur = valueRef.current;
|
|
410
|
+
const at = Math.min(insertAt, cur.length);
|
|
411
|
+
updateValue(cur.slice(0, at) + injected + cur.slice(at), at + injected.length);
|
|
412
|
+
}).catch(() => { });
|
|
413
|
+
}, [updateValue]);
|
|
381
414
|
useInput((input, key) => {
|
|
382
415
|
if (!focus)
|
|
383
416
|
return;
|
|
@@ -393,7 +426,7 @@ function PromptTextInput({ value, onChange, onSubmit, placeholder = '', focus =
|
|
|
393
426
|
pasteBufferRef.current = '';
|
|
394
427
|
}
|
|
395
428
|
if (key.return && !isPasting) {
|
|
396
|
-
onSubmit(
|
|
429
|
+
onSubmit(currentValue);
|
|
397
430
|
return;
|
|
398
431
|
}
|
|
399
432
|
if (key.home || (key.ctrl && input === 'a')) {
|
|
@@ -434,6 +467,13 @@ function PromptTextInput({ value, onChange, onSubmit, placeholder = '', focus =
|
|
|
434
467
|
}
|
|
435
468
|
return;
|
|
436
469
|
}
|
|
470
|
+
// Some Linux terminals do not emit a bracketed-paste event for image-only
|
|
471
|
+
// clipboard contents. Ctrl+V gives users a raw-key fallback that probes the
|
|
472
|
+
// same clipboard image path without relying on terminal paste behavior.
|
|
473
|
+
if (key.ctrl && input === 'v') {
|
|
474
|
+
insertClipboardImageAt(currentCursorOffset);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
437
477
|
if (key.upArrow || key.downArrow || key.tab || key.ctrl || key.meta)
|
|
438
478
|
return;
|
|
439
479
|
let text = normalizeInputNewlines(stripPasteMarkers(input));
|
|
@@ -515,7 +555,7 @@ function PromptTextInput({ value, onChange, onSubmit, placeholder = '', focus =
|
|
|
515
555
|
return _jsx(Text, { children: rendered });
|
|
516
556
|
}
|
|
517
557
|
function formatUserPromptForDisplay(value) {
|
|
518
|
-
return `❯ ${
|
|
558
|
+
return `❯ ${promptValueForDisplay(value)}`;
|
|
519
559
|
}
|
|
520
560
|
function disableTerminalAutoWrap() {
|
|
521
561
|
if (!process.stdout.isTTY)
|
|
@@ -601,7 +641,7 @@ function InputBox({ input, setInput, onSubmit, model, balance, chain, walletTail
|
|
|
601
641
|
const leadingGlyph = (awaitingApproval || awaitingAnswer)
|
|
602
642
|
? _jsx(Text, { color: "yellow", bold: true, children: "\u26A0 " })
|
|
603
643
|
: (showSpinner ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null);
|
|
604
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: borderColor, borderDimColor: !borderColor, paddingX: 1, width: boxWidth, children: [leadingGlyph, _jsx(Box, { flexGrow: 1, children: vimMode ? (_jsx(VimInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false, showMode: true, onModeChange: onVimModeChange })) : (_jsx(PromptTextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false })) })] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', shortModelName(model), " \u00B7 ", (() => {
|
|
644
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: borderColor, borderDimColor: !borderColor, paddingX: 1, width: boxWidth, children: [leadingGlyph, _jsx(Box, { flexGrow: 1, children: vimMode ? (_jsx(VimInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false, showMode: true, onModeChange: onVimModeChange, onClipboardImage: readClipboardImageInjection })) : (_jsx(PromptTextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false })) })] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', shortModelName(model), " \u00B7 ", (() => {
|
|
605
645
|
// Color the balance by funding state. Real session 2026-05-04
|
|
606
646
|
// had a user staring at "$0.08 USDC" in dim text wondering
|
|
607
647
|
// whether it meant "out of money" or "wrong chain". Make
|
|
@@ -1042,17 +1082,20 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
1042
1082
|
turnTierRef.current = undefined;
|
|
1043
1083
|
turnSavingsRef.current = undefined;
|
|
1044
1084
|
turnCtxPctRef.current = undefined;
|
|
1045
|
-
onSubmit(lastPrompt);
|
|
1085
|
+
onSubmit(decodePromptValue(lastPrompt).trim());
|
|
1046
1086
|
return;
|
|
1047
1087
|
default:
|
|
1048
|
-
// All other slash commands pass through to the agent loop's command registry
|
|
1088
|
+
// All other slash commands pass through to the agent loop's command registry.
|
|
1089
|
+
// Decode here too: a slash command can carry an encoded paste/image block
|
|
1090
|
+
// as an argument, and the registry expects real text / file paths,
|
|
1091
|
+
// not the encoded block sentinels.
|
|
1049
1092
|
setStreamText('');
|
|
1050
1093
|
setThinking(false);
|
|
1051
1094
|
setThinkingText('');
|
|
1052
1095
|
setTools(new Map());
|
|
1053
1096
|
setWaiting(true);
|
|
1054
1097
|
setReady(false);
|
|
1055
|
-
onSubmit(trimmed);
|
|
1098
|
+
onSubmit(decodePromptValue(trimmed).trim());
|
|
1056
1099
|
return;
|
|
1057
1100
|
}
|
|
1058
1101
|
}
|
|
@@ -1091,7 +1134,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
1091
1134
|
turnTierRef.current = undefined;
|
|
1092
1135
|
turnSavingsRef.current = undefined;
|
|
1093
1136
|
turnCtxPctRef.current = undefined;
|
|
1094
|
-
onSubmit(trimmed);
|
|
1137
|
+
onSubmit(decodePromptValue(trimmed).trim());
|
|
1095
1138
|
}, [ready, currentModel, totalCost, onSubmit, onModelChange, requestExit, lastPrompt, inputHistory, showStatus]);
|
|
1096
1139
|
// Mouse support — OFF by default because Node stdin is shared: mouse escape
|
|
1097
1140
|
// sequences leak into Ink's input handler as typed text. Opt in with
|
package/dist/ui/vim-input.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ interface VimInputProps {
|
|
|
14
14
|
focus?: boolean;
|
|
15
15
|
showMode?: boolean;
|
|
16
16
|
onModeChange?: (mode: VimMode) => void;
|
|
17
|
+
/** Probe the clipboard for an image and return the input-block to splice in
|
|
18
|
+
* (or null if there's no image). Wired to the same path as PromptTextInput's
|
|
19
|
+
* Ctrl+V fallback so vim-mode users on terminals that don't emit a
|
|
20
|
+
* bracketed-paste event for images can still paste. */
|
|
21
|
+
onClipboardImage?: () => Promise<string | null>;
|
|
17
22
|
}
|
|
18
|
-
export default function VimInput({ value, onChange, onSubmit, placeholder, focus, showMode, onModeChange, }: VimInputProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export default function VimInput({ value, onChange, onSubmit, placeholder, focus, showMode, onModeChange, onClipboardImage, }: VimInputProps): import("react/jsx-runtime").JSX.Element;
|
|
19
24
|
export {};
|
package/dist/ui/vim-input.js
CHANGED
|
@@ -56,13 +56,18 @@ function endWord(text, pos) {
|
|
|
56
56
|
i++;
|
|
57
57
|
return Math.min(i, text.length - 1);
|
|
58
58
|
}
|
|
59
|
-
export default function VimInput({ value, onChange, onSubmit, placeholder = '', focus = true, showMode = true, onModeChange, }) {
|
|
59
|
+
export default function VimInput({ value, onChange, onSubmit, placeholder = '', focus = true, showMode = true, onModeChange, onClipboardImage, }) {
|
|
60
60
|
const [mode, setMode] = useState('insert');
|
|
61
61
|
const [cursor, setCursor] = useState(value.length);
|
|
62
62
|
const [cmdBuf, setCmdBuf] = useState(''); // accumulated command buffer (for counts + operators)
|
|
63
63
|
const [yankBuf, setYankBuf] = useState(''); // internal clipboard
|
|
64
64
|
const [undoStack, setUndoStack] = useState([]); // simple undo
|
|
65
65
|
const lastValueRef = useRef(value);
|
|
66
|
+
// Mirror the latest value prop every render so the async Ctrl+V clipboard
|
|
67
|
+
// insert (which resolves after the keypress) never splices into a stale
|
|
68
|
+
// string when the parent swaps `value` mid-probe — e.g. a submit clears the
|
|
69
|
+
// input, or another paste path writes first.
|
|
70
|
+
lastValueRef.current = value;
|
|
66
71
|
// Keep cursor in bounds when value changes externally
|
|
67
72
|
const clampedCursor = Math.min(cursor, mode === 'normal' ? Math.max(0, value.length - 1) : value.length);
|
|
68
73
|
const switchMode = useCallback((newMode) => {
|
|
@@ -155,6 +160,24 @@ export default function VimInput({ value, onChange, onSubmit, placeholder = '',
|
|
|
155
160
|
updateValue(value.slice(0, clampedCursor), clampedCursor);
|
|
156
161
|
return;
|
|
157
162
|
}
|
|
163
|
+
// Ctrl+V: clipboard-image fallback for terminals that don't emit a
|
|
164
|
+
// bracketed-paste event for image-only clipboards. Probe is async, so the
|
|
165
|
+
// handler returns now and updateValue happens when it resolves; capture
|
|
166
|
+
// the cursor offset so the block lands where the user pasted.
|
|
167
|
+
if (key.ctrl && input === 'v') {
|
|
168
|
+
if (onClipboardImage) {
|
|
169
|
+
const at = clampedCursor;
|
|
170
|
+
saveUndo();
|
|
171
|
+
onClipboardImage().then((injected) => {
|
|
172
|
+
if (!injected)
|
|
173
|
+
return;
|
|
174
|
+
const cur = lastValueRef.current;
|
|
175
|
+
const pos = Math.min(at, cur.length);
|
|
176
|
+
updateValue(cur.slice(0, pos) + injected + cur.slice(pos), pos + injected.length);
|
|
177
|
+
}).catch(() => { });
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
158
181
|
// Skip control chars and tab
|
|
159
182
|
if (key.ctrl || key.meta || key.tab)
|
|
160
183
|
return;
|
package/package.json
CHANGED