@blockrun/franklin 3.15.2 → 3.15.3

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 +46 -13
  2. package/package.json +1 -1
package/dist/ui/app.js CHANGED
@@ -99,6 +99,11 @@ function formatAgentErrorForDisplay(error) {
99
99
  }
100
100
  function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }) {
101
101
  const { exit } = useApp();
102
+ // Track terminal rows so we can cap the dynamic-region height. Ink wipes the
103
+ // terminal scrollback (via ansiEscapes.clearTerminal → \x1b[3J) whenever the
104
+ // dynamic output exceeds rows, so any tall live region (streaming text,
105
+ // model picker) must be windowed to preserve "scroll to the start" history.
106
+ const { rows: termRows } = useTerminalSize();
102
107
  const [input, setInput] = useState('');
103
108
  const [streamText, setStreamText] = useState('');
104
109
  const [thinking, setThinking] = useState(false);
@@ -761,7 +766,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
761
766
  setAskUserRequest(null);
762
767
  setAskUserInput('');
763
768
  r(answer);
764
- }, focus: true })] })] })), expandableTool && (() => {
769
+ }, focus: true })] })] })), expandableTool && !permissionRequest && !askUserRequest && (() => {
765
770
  const tool = expandableTool;
766
771
  const elapsedFmt = tool.elapsed >= 1000
767
772
  ? `${(tool.elapsed / 1000).toFixed(1)}s`
@@ -776,18 +781,46 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
776
781
  const lines = thinkingText.split('\n').filter(Boolean).slice(-3);
777
782
  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))) }));
778
783
  })()] })), 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 && (() => {
779
- const { rendered, partial } = renderMarkdownStreaming(streamText);
780
- return (_jsx(Box, { marginTop: 0, marginBottom: 0, marginLeft: 2, children: _jsxs(Text, { wrap: "wrap", children: [rendered, rendered && partial ? '\n' : '', partial] }) }));
781
- })(), responsePreview && !streamText && (_jsx(Box, { flexDirection: "column", marginBottom: 0, marginLeft: 2, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(responsePreview) }) })), inPicker && (() => {
782
- let flatIdx = 0;
783
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "Select a model " }), _jsx(Text, { dimColor: true, children: "(\u2191\u2193 navigate, Enter select, Esc cancel)" })] }), PICKER_CATEGORIES.map((cat) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2500\u2500 ", cat.category, " \u2500\u2500"] }) }), cat.models.map((m) => {
784
- const myIdx = flatIdx++;
785
- const isSelected = myIdx === pickerIdx;
786
- const isCurrent = m.id === currentModel;
787
- const isHighlight = m.highlight === true;
788
- 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));
789
- })] }, cat.category))), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsx(Text, { dimColor: true, children: "Your conversation stays above \u2014 picking a model keeps all history intact." }) })] }));
790
- })(), !inPicker && (_jsx(InputBox, { input: (permissionRequest || askUserRequest) ? '' : input, setInput: (permissionRequest || askUserRequest) ? () => { } : setInput, onSubmit: (permissionRequest || askUserRequest) ? () => { } : 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 }))] }));
784
+ const maxLines = Math.max(8, termRows - 12);
785
+ const lines = streamText.split('\n');
786
+ const truncated = lines.length > maxLines;
787
+ const visible = truncated ? lines.slice(-maxLines).join('\n') : streamText;
788
+ const { rendered, partial } = renderMarkdownStreaming(visible);
789
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 0, marginBottom: 0, marginLeft: 2, children: [truncated && (_jsxs(Text, { dimColor: true, children: ["\u2191 ", lines.length - maxLines, " earlier line", lines.length - maxLines === 1 ? '' : 's', " \u2014 full response will appear in scrollback when this turn finishes"] })), _jsxs(Text, { wrap: "wrap", children: [rendered, rendered && partial ? '\n' : '', partial] })] }));
790
+ })(), responsePreview && !streamText && !permissionRequest && !askUserRequest && (_jsx(Box, { flexDirection: "column", marginBottom: 0, marginLeft: 2, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(responsePreview) }) })), inPicker && (() => {
791
+ const totalModels = PICKER_MODELS_FLAT.length;
792
+ const maxModels = Math.max(6, termRows - 12);
793
+ let start = Math.max(0, pickerIdx - Math.floor(maxModels / 2));
794
+ let end = Math.min(totalModels, start + maxModels);
795
+ // Expand window backward if we hit the bottom of the list, so we
796
+ // always fill `maxModels` rows when the list is long enough.
797
+ if (end - start < maxModels)
798
+ start = Math.max(0, end - maxModels);
799
+ const hiddenAbove = start;
800
+ const hiddenBelow = totalModels - end;
801
+ // Pre-compute each category's base offset into the flat model list so
802
+ // we can map (cat, localIdx) → globalIdx in one pass without re-walking.
803
+ let cursor = 0;
804
+ const catBases = PICKER_CATEGORIES.map((cat) => {
805
+ const base = cursor;
806
+ cursor += cat.models.length;
807
+ return base;
808
+ });
809
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "Select a model " }), _jsx(Text, { dimColor: true, children: "(\u2191\u2193 navigate, Enter select, Esc cancel)" })] }), hiddenAbove > 0 && (_jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["\u2191 ", hiddenAbove, " more above"] }) })), PICKER_CATEGORIES.map((cat, catIdx) => {
810
+ const base = catBases[catIdx];
811
+ const visible = cat.models
812
+ .map((m, localIdx) => ({ m, globalIdx: base + localIdx }))
813
+ .filter(({ globalIdx }) => globalIdx >= start && globalIdx < end);
814
+ if (visible.length === 0)
815
+ return null;
816
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2500\u2500 ", cat.category, " \u2500\u2500"] }) }), visible.map(({ m, globalIdx }) => {
817
+ const isSelected = globalIdx === pickerIdx;
818
+ const isCurrent = m.id === currentModel;
819
+ const isHighlight = m.highlight === true;
820
+ 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));
821
+ })] }, cat.category));
822
+ }), 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." }) })] }));
823
+ })(), !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 }))] }));
791
824
  }
792
825
  export function launchInkUI(opts) {
793
826
  let resolveInput = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.2",
3
+ "version": "3.15.3",
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": {