@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 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(decodePromptValue(currentValue));
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 `❯ ${decodePromptValue(value)}`;
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
@@ -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 {};
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.25.3",
3
+ "version": "3.25.4",
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": {