@blockrun/franklin 3.15.26 → 3.15.27

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.
Files changed (2) hide show
  1. package/dist/ui/app.js +50 -12
  2. package/package.json +1 -1
package/dist/ui/app.js CHANGED
@@ -56,18 +56,35 @@ function useTerminalSize() {
56
56
  }, [stdout]);
57
57
  return size;
58
58
  }
59
- function InputBox({ input, setInput, onSubmit, model, balance, chain, walletTail, sessionCost, queued, queuedCount, focused, busy, contextPct, vimMode, onVimModeChange }) {
59
+ function InputBox({ input, setInput, onSubmit, model, balance, chain, walletTail, sessionCost, queued, queuedCount, focused, busy, awaitingApproval, awaitingAnswer, contextPct, vimMode, onVimModeChange }) {
60
60
  const { cols } = useTerminalSize();
61
61
  // Avoid drawing right up to the terminal edge. Several terminals auto-wrap
62
62
  // a full-width border glyph onto the next row, which leaves "ghost" top
63
63
  // borders behind on re-render after errors / status changes.
64
64
  const boxWidth = Math.max(20, cols - 2);
65
- const placeholder = busy
66
- ? (queued
67
- ? `⏎ ${queuedCount ?? 1} queued: ${queued.slice(0, 40)}`
68
- : 'Working...')
69
- : 'Type a message...';
70
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { borderStyle: "round", borderDimColor: true, paddingX: 1, width: boxWidth, children: [busy && !input ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsx(Box, { flexGrow: 1, children: vimMode ? (_jsx(VimInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false, showMode: true, onModeChange: onVimModeChange })) : (_jsx(TextInput, { 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 ", balance, chain ? _jsxs(Text, { children: [" \u00B7 ", _jsx(Text, { color: "magenta", children: chain }), walletTail ? _jsxs(Text, { dimColor: true, children: [":", walletTail] }) : ''] }) : '', sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', contextPct !== undefined && contextPct > 0 ? (() => {
65
+ // Awaiting-input states beat "Working..." — the agent isn't busy, it's
66
+ // blocked on the user. Saying "Working..." here while a permission dialog
67
+ // sits in the scrollback above is exactly how users miss it (verified
68
+ // 2026-05-04 from a real screenshot — "Working..." spinner kept turning
69
+ // while the agent waited on a Bash approval).
70
+ const placeholder = awaitingApproval
71
+ ? '⚠ Approval needed — press [y]/[a]/[n] in the prompt above'
72
+ : awaitingAnswer
73
+ ? '⚠ Question above — type your answer'
74
+ : busy
75
+ ? (queued
76
+ ? `⏎ ${queuedCount ?? 1} queued: ${queued.slice(0, 40)}`
77
+ : 'Working...')
78
+ : 'Type a message...';
79
+ // Color the input-box border to match the urgency. Awaiting-user states
80
+ // get a bright yellow border so the focal point physically moves down to
81
+ // the input field, even peripheral vision picks it up.
82
+ const borderColor = awaitingApproval || awaitingAnswer ? 'yellow' : undefined;
83
+ const showSpinner = busy && !input && !awaitingApproval && !awaitingAnswer;
84
+ const leadingGlyph = (awaitingApproval || awaitingAnswer)
85
+ ? _jsx(Text, { color: "yellow", bold: true, children: "\u26A0 " })
86
+ : (showSpinner ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null);
87
+ 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(TextInput, { 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 ", balance, chain ? _jsxs(Text, { children: [" \u00B7 ", _jsx(Text, { color: "magenta", children: chain }), walletTail ? _jsxs(Text, { dimColor: true, children: [":", walletTail] }) : ''] }) : '', sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', contextPct !== undefined && contextPct > 0 ? (() => {
71
88
  // Visual context bar: ▓▓▓▓▓▓░░░░ 75%
72
89
  const filled = Math.round(contextPct / 10);
73
90
  const empty = 10 - filled;
@@ -148,6 +165,27 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
148
165
  const [permissionRequest, setPermissionRequest] = useState(null);
149
166
  const [askUserRequest, setAskUserRequest] = useState(null);
150
167
  const [askUserInput, setAskUserInput] = useState('');
168
+ // Ring the terminal bell exactly once when a permission/askUser dialog
169
+ // first appears. Helpful when the user has Franklin in a background
170
+ // tab and the agent stops to ask for approval — verified 2026-05-04
171
+ // from a real screenshot where the user missed the dialog because the
172
+ // input box still read "Working...". Opt-out via FRANKLIN_NO_BELL=1.
173
+ const bellPlayedRef = useRef(false);
174
+ useEffect(() => {
175
+ const dialogActive = !!permissionRequest || !!askUserRequest;
176
+ if (dialogActive && !bellPlayedRef.current) {
177
+ bellPlayedRef.current = true;
178
+ if (process.env.FRANKLIN_NO_BELL !== '1') {
179
+ try {
180
+ process.stderr.write('\x07');
181
+ }
182
+ catch { /* swallow — never break the UI on a TTY without bell */ }
183
+ }
184
+ }
185
+ else if (!dialogActive) {
186
+ bellPlayedRef.current = false;
187
+ }
188
+ }, [permissionRequest, askUserRequest]);
151
189
  // Messages queued while agent is busy — auto-submitted FIFO when turns complete.
152
190
  const [queuedInputs, setQueuedInputs] = useState([]);
153
191
  const turnDoneCallbackRef = useRef(null);
@@ -771,7 +809,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
771
809
  : `${formatTokens(r.tokens.input)} in / ${formatTokens(r.tokens.output)} out`, r.cost > 0 ? ` · $${r.cost.toFixed(4)}` : '', r.savings !== undefined && r.savings > 0 ? _jsxs(Text, { color: "green", children: [" saved ", Math.round(r.savings * 100), "%"] }) : '', r.ctxPct !== undefined && r.ctxPct >= 5
772
810
  ? _jsxs(Text, { color: r.ctxPct >= 80 ? 'red' : r.ctxPct >= 50 ? 'yellow' : undefined, dimColor: r.ctxPct < 50, children: [" \u00B7 ctx ", r.ctxPct, "%"] })
773
811
  : ''] }) }))] }, r.key));
774
- } }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "yellow", children: "\u256D\u2500 Permission required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: ["\u2502 ", _jsx(Text, { bold: true, children: permissionRequest.toolName })] }), permissionRequest.description.split('\n').map((line, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", line] }, i))), _jsx(Text, { color: "yellow", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no" })] }) })] })), askUserRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "cyan", children: "\u256D\u2500 Question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "cyan", children: ["\u2502 ", _jsx(Text, { bold: true, children: askUserRequest.question })] }), askUserRequest.options && askUserRequest.options.length > 0 && (askUserRequest.options.map((opt, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", i + 1, ". ", opt] }, i)))), _jsx(Text, { color: "cyan", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
812
+ } }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "red", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0 ACTION REQUIRED \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "yellow", children: "\u256D\u2500 Permission required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: ["\u2502 ", _jsx(Text, { bold: true, children: permissionRequest.toolName })] }), permissionRequest.description.split('\n').map((line, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", line] }, i))), _jsx(Text, { color: "yellow", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no" })] }) })] })), askUserRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "magenta", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0 ANSWER REQUIRED \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "cyan", children: "\u256D\u2500 Question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "cyan", children: ["\u2502 ", _jsx(Text, { bold: true, children: askUserRequest.question })] }), askUserRequest.options && askUserRequest.options.length > 0 && (askUserRequest.options.map((opt, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", i + 1, ". ", opt] }, i)))), _jsx(Text, { color: "cyan", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
775
813
  const answer = val.trim() || '(no response)';
776
814
  const r = askUserRequest.resolve;
777
815
  setAskUserRequest(null);
@@ -784,14 +822,14 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
784
822
  : `${tool.elapsed}ms`;
785
823
  const hasExpandableContent = !!(tool.diff || (tool.fullOutput && tool.fullOutput.split('\n').length > 1));
786
824
  return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { children: [tool.error ? _jsx(Text, { color: "red", children: "\u2717" }) : _jsx(Text, { color: "green", children: "\u2713" }), ' ', _jsx(Text, { bold: true, children: tool.name }), tool.preview ? _jsxs(Text, { dimColor: true, children: ["(", tool.preview.slice(0, 80), ")"] }) : null, _jsxs(Text, { dimColor: true, children: [" ", elapsedFmt] }), hasExpandableContent && (_jsxs(Text, { dimColor: true, children: [" ", tool.expanded ? '(tab to collapse)' : '(tab to expand)'] }))] }), !tool.expanded && tool.diff && !tool.error && tool.diff.oldLines.length <= 8 && tool.diff.newLines.length <= 8 && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [tool.diff.oldLines.map((line, i) => (_jsxs(Text, { color: "red", wrap: "truncate-end", children: ['⎿ ', "- ", line.slice(0, 120)] }, `old-${i}`))), tool.diff.newLines.map((line, i) => (_jsxs(Text, { color: "green", wrap: "truncate-end", children: ['⎿ ', "+ ", line.slice(0, 120)] }, `new-${i}`)))] })), tool.expanded && tool.fullOutput && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [tool.fullOutput.split('\n').slice(0, 30).map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['⎿ ', line.slice(0, 120)] }, i))), tool.fullOutput.split('\n').length > 30 && (_jsxs(Text, { dimColor: true, children: ['⎿ ', "... ", tool.fullOutput.split('\n').length - 30, " more lines"] }))] })), tool.error && !tool.expanded && tool.fullOutput && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: tool.fullOutput.split('\n').filter(Boolean).slice(0, 3).map((line, i) => (_jsxs(Text, { color: "red", wrap: "truncate-end", children: ['⎿ ', line.slice(0, 120)] }, i))) }))] }));
787
- })(), Array.from(tools.entries()).map(([id, tool]) => {
825
+ })(), !permissionRequest && !askUserRequest && Array.from(tools.entries()).map(([id, tool]) => {
788
826
  const elapsed = Math.round((Date.now() - tool.startTime) / 1000);
789
827
  const elapsedStr = elapsed > 0 ? ` ${elapsed}s` : '';
790
828
  return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), ' ', _jsx(Text, { bold: true, color: "cyan", children: tool.name }), tool.preview ? _jsxs(Text, { dimColor: true, children: ["(", tool.preview.slice(0, 70), ")"] }) : null, _jsx(Text, { dimColor: true, children: elapsedStr })] }), tool.liveLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: tool.liveLines.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['⎿ ', line.slice(0, 120)] }, i))) }))] }, id));
791
- }), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "magenta", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsx(Text, { bold: true, children: "thinking" }), completedTools.length > 0 ? _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 step ", completedTools.length + 1] }) : null] }), process.env.FRANKLIN_SHOW_THINKING === '1' && thinkingText && (() => {
829
+ }), thinking && !permissionRequest && !askUserRequest && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "magenta", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsx(Text, { bold: true, children: "thinking" }), completedTools.length > 0 ? _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 step ", completedTools.length + 1] }) : null] }), process.env.FRANKLIN_SHOW_THINKING === '1' && thinkingText && (() => {
792
830
  const lines = thinkingText.split('\n').filter(Boolean).slice(-3);
793
831
  return (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: lines.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['⎿ ', line.slice(0, 120)] }, i))) }));
794
- })()] })), waiting && !thinking && tools.size === 0 && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsxs(Text, { dimColor: true, children: [shortModelName(currentModel), completedTools.length > 0 ? ` · step ${completedTools.length + 1}` : ''] })] }) })), streamText && (() => {
832
+ })()] })), waiting && !thinking && tools.size === 0 && !permissionRequest && !askUserRequest && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsxs(Text, { dimColor: true, children: [shortModelName(currentModel), completedTools.length > 0 ? ` · step ${completedTools.length + 1}` : ''] })] }) })), streamText && !permissionRequest && !askUserRequest && (() => {
795
833
  const maxLines = Math.max(8, termRows - 12);
796
834
  const lines = streamText.split('\n');
797
835
  const truncated = lines.length > maxLines;
@@ -831,7 +869,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
831
869
  return (_jsxs(Box, { marginLeft: 2, children: [_jsxs(Text, { inverse: isSelected, color: isSelected ? 'cyan' : isHighlight ? 'yellow' : undefined, bold: isSelected || isHighlight, children: [' ', m.label.padEnd(26), ' '] }), _jsxs(Text, { dimColor: true, children: [" ", m.shortcut.padEnd(14)] }), _jsx(Text, { color: m.price === 'FREE' ? 'green' : isHighlight ? 'yellow' : undefined, dimColor: !isHighlight && m.price !== 'FREE', children: m.price }), isCurrent && _jsx(Text, { color: "green", children: " \u2190" })] }, m.id));
832
870
  })] }, cat.category));
833
871
  }), hiddenBelow > 0 && (_jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["\u2193 ", hiddenBelow, " more below"] }) })), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsx(Text, { dimColor: true, children: "Your conversation stays above \u2014 picking a model keeps all history intact." }) })] }));
834
- })(), !inPicker && !permissionRequest && !askUserRequest && (_jsx(InputBox, { input: input, setInput: setInput, onSubmit: handleSubmit, model: currentModel, balance: liveBalance, chain: chain, walletTail: walletAddress && walletAddress.length >= 4 && !walletAddress.startsWith('not set') ? walletAddress.slice(-4) : undefined, sessionCost: totalCost, queued: queuedInputs[0] || undefined, queuedCount: queuedInputs.length, focused: !permissionRequest && !askUserRequest, busy: !askUserRequest && (waiting || thinking || tools.size > 0), contextPct: contextPct, vimMode: vimEnabled, onVimModeChange: setCurrentVimMode }))] }));
872
+ })(), !inPicker && !permissionRequest && !askUserRequest && (_jsx(InputBox, { input: input, setInput: setInput, onSubmit: handleSubmit, model: currentModel, balance: liveBalance, chain: chain, walletTail: walletAddress && walletAddress.length >= 4 && !walletAddress.startsWith('not set') ? walletAddress.slice(-4) : undefined, sessionCost: totalCost, queued: queuedInputs[0] || undefined, queuedCount: queuedInputs.length, focused: !permissionRequest && !askUserRequest, busy: !askUserRequest && (waiting || thinking || tools.size > 0), awaitingApproval: !!permissionRequest, awaitingAnswer: !!askUserRequest, contextPct: contextPct, vimMode: vimEnabled, onVimModeChange: setCurrentVimMode }))] }));
835
873
  }
836
874
  export function launchInkUI(opts) {
837
875
  let resolveInput = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.26",
3
+ "version": "3.15.27",
4
4
  "description": "Franklin — 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": {