@blockrun/franklin 3.5.1 → 3.6.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 CHANGED
@@ -8,12 +8,14 @@ import { useState, useEffect, useCallback, useRef } from 'react';
8
8
  import { render, Static, Box, Text, useApp, useInput, useStdout } from 'ink';
9
9
  import Spinner from 'ink-spinner';
10
10
  import TextInput from 'ink-text-input';
11
+ import VimInput from './vim-input.js';
11
12
  import { renderMarkdown } from './markdown.js';
12
13
  import { resolveModel, PICKER_CATEGORIES, PICKER_MODELS_FLAT, } from './model-picker.js';
13
14
  import { estimateCost } from '../pricing.js';
14
15
  import { formatTokens, shortModelName } from '../stats/format.js';
16
+ import { mouse } from './mouse.js';
15
17
  // ─── Full-width input box ──────────────────────────────────────────────────
16
- function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queued, queuedCount, focused, busy, contextPct }) {
18
+ function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queued, queuedCount, focused, busy, contextPct, vimMode, onVimModeChange }) {
17
19
  const { stdout } = useStdout();
18
20
  const cols = stdout?.columns ?? 80;
19
21
  const innerWidth = Math.min(Math.max(30, cols - 4), cols - 2);
@@ -22,7 +24,13 @@ function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queu
22
24
  ? `⏎ ${queuedCount ?? 1} queued: ${queued.slice(0, 40)}`
23
25
  : 'Working...')
24
26
  : 'Type a message...';
25
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: '╭' + '─'.repeat(cols - 2) + '╮' }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u2502 " }), busy && !input ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsx(Box, { width: busy && !input ? innerWidth - 4 : innerWidth, children: _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false }) }), _jsxs(Text, { dimColor: true, children: [' '.repeat(Math.max(0, cols - innerWidth - 4)), "\u2502"] })] }), _jsx(Text, { dimColor: true, children: '╰' + '─'.repeat(cols - 2) + '╯' }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', model, " \u00B7 ", balance, sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', contextPct !== undefined && contextPct > 0 ? (_jsxs(Text, { color: contextPct > 85 ? 'red' : contextPct > 70 ? 'yellow' : undefined, children: [' · ctx ', contextPct, '%'] })) : null, (queuedCount ?? 0) > 0 ? _jsxs(Text, { color: "cyan", children: [" \u00B7 ", queuedCount, " queued"] }) : null, ' · esc'] }) })] }));
27
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: '╭' + '─'.repeat(cols - 2) + '╮' }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u2502 " }), busy && !input ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsx(Box, { width: busy && !input ? innerWidth - 4 : innerWidth, 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 })) }), _jsxs(Text, { dimColor: true, children: [' '.repeat(Math.max(0, cols - innerWidth - 4)), "\u2502"] })] }), _jsx(Text, { dimColor: true, children: '╰' + '─'.repeat(cols - 2) + '╯' }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', shortModelName(model), " \u00B7 ", balance, sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', contextPct !== undefined && contextPct > 0 ? (() => {
28
+ // Visual context bar: ▓▓▓▓▓▓░░░░ 75%
29
+ const filled = Math.round(contextPct / 10);
30
+ const empty = 10 - filled;
31
+ const barColor = contextPct > 85 ? 'red' : contextPct > 70 ? 'yellow' : 'green';
32
+ return (_jsxs(Text, { children: [' ', _jsx(Text, { color: barColor, children: '▓'.repeat(filled) }), _jsx(Text, { dimColor: true, children: '░'.repeat(empty) }), _jsxs(Text, { color: barColor, children: [' ', contextPct, "%"] })] }));
33
+ })() : null, (queuedCount ?? 0) > 0 ? _jsxs(Text, { color: "cyan", children: [" \u00B7 ", queuedCount, " queued"] }) : null, ' · esc'] }) })] }));
26
34
  }
27
35
  function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }) {
28
36
  const { exit } = useApp();
@@ -33,6 +41,8 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
33
41
  const [tools, setTools] = useState(new Map());
34
42
  // Completed tool results committed to Static (permanent scrollback — no re-render artifacts)
35
43
  const [completedTools, setCompletedTools] = useState([]);
44
+ // Last completed tool — shown in dynamic area so it can be expanded/collapsed with Tab
45
+ const [expandableTool, setExpandableTool] = useState(null);
36
46
  // Full responses committed to Static immediately — goes into terminal scrollback like Claude Code
37
47
  const [committedResponses, setCommittedResponses] = useState([]);
38
48
  // Short preview of latest response shown in dynamic area (last ~5 lines, cleared on next turn)
@@ -48,6 +58,8 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
48
58
  const [totalCost, setTotalCost] = useState(0);
49
59
  const [showHelp, setShowHelp] = useState(false);
50
60
  const [showWallet, setShowWallet] = useState(false);
61
+ const [vimEnabled, setVimEnabled] = useState(false);
62
+ const [currentVimMode, setCurrentVimMode] = useState('insert');
51
63
  const [balance, setBalance] = useState(walletBalance);
52
64
  // Parse the fetched balance to a number so we can compute live balance = fetchedBalance - sessionCost.
53
65
  // costAtLastFetch tracks totalCost when balance was last fetched, to avoid double-subtracting.
@@ -69,6 +81,37 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
69
81
  // Messages queued while agent is busy — auto-submitted FIFO when turns complete.
70
82
  const [queuedInputs, setQueuedInputs] = useState([]);
71
83
  const turnDoneCallbackRef = useRef(null);
84
+ // ── Render throttling: batch rapid text_delta/thinking_delta into 50ms frames ──
85
+ // Without this, each delta (20-100/sec) triggers a full React re-render.
86
+ // With this, we accumulate in refs and flush at ~20fps — smooth and efficient.
87
+ const pendingTextRef = useRef('');
88
+ const pendingThinkingRef = useRef('');
89
+ const flushTimerRef = useRef(null);
90
+ const flushPendingText = useCallback(() => {
91
+ flushTimerRef.current = null;
92
+ const text = pendingTextRef.current;
93
+ const thinking = pendingThinkingRef.current;
94
+ if (text) {
95
+ pendingTextRef.current = '';
96
+ setWaiting(false);
97
+ setThinking(false);
98
+ setStreamText(prev => prev + text);
99
+ }
100
+ if (thinking) {
101
+ pendingThinkingRef.current = '';
102
+ setWaiting(false);
103
+ setThinking(true);
104
+ setThinkingText(prev => {
105
+ const updated = prev + thinking;
106
+ return updated.length > 500 ? updated.slice(-500) : updated;
107
+ });
108
+ }
109
+ }, []);
110
+ const scheduleFlush = useCallback(() => {
111
+ if (!flushTimerRef.current) {
112
+ flushTimerRef.current = setTimeout(flushPendingText, 50);
113
+ }
114
+ }, [flushPendingText]);
72
115
  // Refs to read current state values inside memoized event handlers (avoids stale closures)
73
116
  const streamTextRef = useRef('');
74
117
  const turnTokensRef = useRef({ input: 0, output: 0, calls: 0 });
@@ -99,15 +142,19 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
99
142
  const commitResponse = useCallback((text, tokens = turnTokensRef.current, cost = turnCostRef.current) => {
100
143
  if (!text.trim())
101
144
  return;
102
- setCommittedResponses((rs) => [...rs, {
103
- key: String(Date.now() + Math.random()),
104
- text,
105
- tokens,
106
- cost,
107
- model: turnModelRef.current,
108
- tier: turnTierRef.current,
109
- savings: turnSavingsRef.current,
110
- }]);
145
+ setCommittedResponses((rs) => {
146
+ const next = [...rs, {
147
+ key: String(Date.now() + Math.random()),
148
+ text,
149
+ tokens,
150
+ cost,
151
+ model: turnModelRef.current,
152
+ tier: turnTierRef.current,
153
+ savings: turnSavingsRef.current,
154
+ }];
155
+ // Cap at 300 items — older items are already in terminal scrollback
156
+ return next.length > 300 ? next.slice(-300) : next;
157
+ });
111
158
  const allLines = text.split('\n');
112
159
  if (allLines.length > 20) {
113
160
  setResponsePreview(' ↑ scroll to see full reply\n' + allLines.slice(-20).join('\n'));
@@ -154,7 +201,10 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
154
201
  return;
155
202
  }
156
203
  // Esc to quit (only when input is empty and in input mode)
204
+ // In Vim mode: Esc goes to normal mode (handled by VimInput), only quit on Esc in normal mode with empty input
157
205
  if (key.escape && mode === 'input' && ready && !input) {
206
+ if (vimEnabled && currentVimMode === 'insert')
207
+ return; // Let VimInput handle Esc → normal
158
208
  onExit();
159
209
  exit();
160
210
  return;
@@ -179,6 +229,12 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
179
229
  setReady(true);
180
230
  }
181
231
  }, { isActive: isPickerOrEsc });
232
+ // Tab key: toggle expand/collapse on the last completed tool
233
+ useInput((_ch, key) => {
234
+ if (key.tab && expandableTool) {
235
+ setExpandableTool(prev => prev ? { ...prev, expanded: !prev.expanded } : null);
236
+ }
237
+ }, { isActive: mode === 'input' && !permissionRequest && !askUserRequest });
182
238
  // Input history: Up/Down arrow when in ready input mode
183
239
  useInput((_ch, key) => {
184
240
  if (key.upArrow && inputHistory.length > 0) {
@@ -256,6 +312,10 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
256
312
  setShowHelp(true);
257
313
  setShowWallet(false);
258
314
  return;
315
+ case '/vim':
316
+ setVimEnabled(prev => !prev);
317
+ showStatus(vimEnabled ? 'Vim mode OFF' : 'Vim mode ON — Esc for normal, i for insert', 'success', 3000);
318
+ return;
259
319
  case '/clear':
260
320
  setStreamText('');
261
321
  setTools(new Map());
@@ -316,6 +376,12 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
316
376
  setThinking(false);
317
377
  setThinkingText('');
318
378
  setTools(new Map());
379
+ // Flush expandable tool to Static before clearing
380
+ setExpandableTool(prev => {
381
+ if (prev)
382
+ setCompletedTools(prev2 => [...prev2, { ...prev, expanded: false }]);
383
+ return null;
384
+ });
319
385
  setCompletedTools([]);
320
386
  setReady(false);
321
387
  setWaiting(true);
@@ -329,6 +395,22 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
329
395
  turnSavingsRef.current = undefined;
330
396
  onSubmit(trimmed);
331
397
  }, [ready, currentModel, totalCost, onSubmit, onModelChange, onAbort, onExit, exit, lastPrompt, inputHistory, showStatus]);
398
+ // Mouse support — enable tracking and handle clicks on tool results
399
+ useEffect(() => {
400
+ const cleanup = mouse.enable();
401
+ const handleClick = (_event) => {
402
+ // Click anywhere toggles the expandable tool (if one exists)
403
+ // This is intentionally simple — we don't track exact coordinates of components.
404
+ // The expandable tool is always the most recent tool result, so any click is a
405
+ // reasonable toggle target. Tab key remains the precise alternative.
406
+ setExpandableTool(prev => prev ? { ...prev, expanded: !prev.expanded } : null);
407
+ };
408
+ mouse.on('click', handleClick);
409
+ return () => {
410
+ mouse.removeListener('click', handleClick);
411
+ cleanup();
412
+ };
413
+ }, []);
332
414
  // Expose event handler, balance updater, and permission bridge
333
415
  useEffect(() => {
334
416
  globalThis.__runcode_ui = {
@@ -360,18 +442,14 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
360
442
  handleEvent: (event) => {
361
443
  switch (event.kind) {
362
444
  case 'text_delta':
363
- setWaiting(false);
364
- setThinking(false);
365
- setStreamText(prev => prev + event.text);
445
+ // Throttled: accumulate in ref, flush every 50ms (~20fps)
446
+ pendingTextRef.current += event.text;
447
+ scheduleFlush();
366
448
  break;
367
449
  case 'thinking_delta':
368
- setWaiting(false);
369
- setThinking(true);
370
- setThinkingText(prev => {
371
- // Keep last 500 chars of thinking for display
372
- const updated = prev + event.text;
373
- return updated.length > 500 ? updated.slice(-500) : updated;
374
- });
450
+ // Throttled: accumulate in ref, flush every 50ms
451
+ pendingThinkingRef.current += event.text;
452
+ scheduleFlush();
375
453
  break;
376
454
  case 'capability_start':
377
455
  setWaiting(false);
@@ -382,6 +460,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
382
460
  done: false, error: false,
383
461
  preview: event.preview || '',
384
462
  liveOutput: '',
463
+ liveLines: [],
464
+ fullOutput: '',
465
+ expanded: false,
385
466
  elapsed: 0,
386
467
  });
387
468
  return next;
@@ -393,7 +474,13 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
393
474
  if (!t || t.done)
394
475
  return prev;
395
476
  const next = new Map(prev);
396
- next.set(event.id, { ...t, liveOutput: event.text });
477
+ // Accumulate output lines for multi-line display (keep last 5)
478
+ const newLines = [...t.liveLines];
479
+ const incoming = event.text.split('\n').filter(Boolean);
480
+ newLines.push(...incoming);
481
+ while (newLines.length > 5)
482
+ newLines.shift();
483
+ next.set(event.id, { ...t, liveOutput: event.text, liveLines: newLines });
397
484
  return next;
398
485
  });
399
486
  break;
@@ -413,10 +500,19 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
413
500
  error: !!event.result.isError,
414
501
  preview: resultPreview,
415
502
  liveOutput: '',
503
+ liveLines: [],
504
+ fullOutput: event.result.output || '',
505
+ diff: event.result.diff,
506
+ expanded: false,
416
507
  elapsed: Date.now() - t.startTime,
417
508
  };
418
- // Move to Static (permanent scrollback) prevents re-render artifacts
419
- setCompletedTools(prev2 => [...prev2, completed]);
509
+ // Move previous expandable tool to Static, set new one as expandable
510
+ setExpandableTool(prevExpTool => {
511
+ if (prevExpTool) {
512
+ setCompletedTools(prev2 => [...prev2, { ...prevExpTool, expanded: false }]);
513
+ }
514
+ return completed;
515
+ });
420
516
  next.delete(event.id);
421
517
  }
422
518
  return next;
@@ -444,6 +540,23 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
444
540
  break;
445
541
  }
446
542
  case 'turn_done': {
543
+ // Flush any pending throttled text immediately
544
+ if (flushTimerRef.current) {
545
+ clearTimeout(flushTimerRef.current);
546
+ flushTimerRef.current = null;
547
+ }
548
+ // Merge pending text into the ref so commitResponse sees the full text
549
+ if (pendingTextRef.current) {
550
+ streamTextRef.current += pendingTextRef.current;
551
+ pendingTextRef.current = '';
552
+ }
553
+ pendingThinkingRef.current = '';
554
+ // Flush expandable tool to Static before committing response
555
+ setExpandableTool(prev => {
556
+ if (prev)
557
+ setCompletedTools(prev2 => [...prev2, { ...prev, expanded: false }]);
558
+ return null;
559
+ });
447
560
  const text = streamTextRef.current;
448
561
  if (text.trim()) {
449
562
  commitResponse(text, turnTokensRef.current, turnCostRef.current);
@@ -502,17 +615,39 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
502
615
  // opens/closes. The picker is rendered inline below scrollback, and the
503
616
  // InputBox is hidden while it's active.
504
617
  const inPicker = mode === 'model-picker';
505
- return (_jsxs(Box, { flexDirection: "column", children: [statusMsg && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: statusTone === 'error' ? 'red' : statusTone === 'warning' ? 'yellow' : 'green', children: statusMsg }) })), showHelp && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Commands" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/model" }), " [name] Switch model (picker if no name)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/wallet" }), " Show wallet address & balance"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/cost" }), " Session cost & savings"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/retry" }), " Retry the last prompt"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/compact" }), " Compress conversation history"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Coding \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/test" }), " Run tests"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/fix" }), " Fix last error"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/review" }), " Code review"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/explain" }), " file Explain code"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/search" }), " query Search codebase"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/session-search" }), " q Search past sessions"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/refactor" }), " desc Refactor code"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/scaffold" }), " desc Generate boilerplate"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Git \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/commit" }), " Commit changes"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/push" }), " Push to remote"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/pr" }), " Create pull request"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/status" }), " Git status"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/diff" }), " Git diff"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/log" }), " Git log"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/branch" }), " [name] Branches"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/stash" }), " Stash changes"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/undo" }), " Undo last commit"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Analysis \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/security" }), " Security audit"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/lint" }), " Quality check"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/optimize" }), " Performance check"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/todo" }), " Find TODOs"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/deps" }), " Dependencies"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/clean" }), " Dead code removal"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/context" }), " Session info (model, tokens, mode)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/plan" }), " Enter plan mode (read-only tools)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/execute" }), " Exit plan mode (enable all tools)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/sessions" }), " List saved sessions"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/resume" }), " id Resume a saved session"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/clear" }), " Clear conversation history"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/doctor" }), " Diagnose setup issues"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/help" }), " This help"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/exit" }), " Quit"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Shortcuts: sonnet, opus, gpt, gemini, deepseek, flash, free, r1, o4, nano, mini, haiku" })] })), showWallet && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Wallet" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" Chain: ", _jsx(Text, { color: "magenta", children: chain })] }), _jsxs(Text, { children: [" Address: ", _jsx(Text, { color: "cyan", children: walletAddress })] }), _jsxs(Text, { children: [" Balance: ", _jsx(Text, { color: "green", children: balance })] })] })), _jsx(Static, { items: completedTools, children: (tool) => (_jsx(Box, { marginLeft: 1, children: tool.error
506
- ? _jsxs(Text, { color: "red", children: [" \u2717 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms", tool.preview ? ` — ${tool.preview}` : ''] })] })
507
- : _jsxs(Text, { color: "green", children: [" \u2713 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms", tool.preview ? ` — ${tool.preview}` : ''] })] }) }, tool.key)) }), _jsx(Static, { items: committedResponses, children: (r) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { wrap: "wrap", children: renderMarkdown(r.text) }), (r.tokens.input > 0 || r.tokens.output > 0) && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [r.tier && _jsxs(Text, { color: "cyan", children: [r.tier, " "] }), r.model ? shortModelName(r.model) : '', r.model ? ' · ' : '', r.tokens.calls > 0 && r.tokens.input === 0
508
- ? `${r.tokens.calls} calls`
509
- : `${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.key)) }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 1, 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: 3, 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: 1, 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: 3, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
618
+ return (_jsxs(Box, { flexDirection: "column", children: [statusMsg && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: statusTone === 'error' ? 'red' : statusTone === 'warning' ? 'yellow' : 'green', children: statusMsg }) })), showHelp && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Commands" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/model" }), " [name] Switch model (picker if no name)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/wallet" }), " Show wallet address & balance"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/cost" }), " Session cost & savings"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/retry" }), " Retry the last prompt"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/compact" }), " Compress conversation history"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Coding \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/test" }), " Run tests"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/fix" }), " Fix last error"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/review" }), " Code review"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/explain" }), " file Explain code"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/search" }), " query Search codebase"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/session-search" }), " q Search past sessions"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/refactor" }), " desc Refactor code"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/scaffold" }), " desc Generate boilerplate"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Git \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/commit" }), " Commit changes"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/push" }), " Push to remote"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/pr" }), " Create pull request"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/status" }), " Git status"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/diff" }), " Git diff"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/log" }), " Git log"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/branch" }), " [name] Branches"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/stash" }), " Stash changes"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/undo" }), " Undo last commit"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Analysis \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/security" }), " Security audit"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/lint" }), " Quality check"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/optimize" }), " Performance check"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/todo" }), " Find TODOs"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/deps" }), " Dependencies"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/clean" }), " Dead code removal"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/context" }), " Session info (model, tokens, mode)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/plan" }), " Enter plan mode (read-only tools)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/execute" }), " Exit plan mode (enable all tools)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/sessions" }), " List saved sessions"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/resume" }), " id Resume a saved session"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/clear" }), " Clear conversation history"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/doctor" }), " Diagnose setup issues"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/vim" }), " Toggle Vim input mode"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/help" }), " This help"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/exit" }), " Quit"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Shortcuts: sonnet, opus, gpt, gemini, deepseek, flash, free, r1, o4, nano, mini, haiku" })] })), showWallet && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Wallet" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" Chain: ", _jsx(Text, { color: "magenta", children: chain })] }), _jsxs(Text, { children: [" Address: ", _jsx(Text, { color: "cyan", children: walletAddress })] }), _jsxs(Text, { children: [" Balance: ", _jsx(Text, { color: "green", children: balance })] })] })), _jsx(Static, { items: completedTools, children: (tool) => {
619
+ const elapsedFmt = tool.elapsed >= 1000
620
+ ? `${(tool.elapsed / 1000).toFixed(1)}s`
621
+ : `${tool.elapsed}ms`;
622
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { children: [tool.error
623
+ ? _jsx(Text, { color: "red", children: "\u2717" })
624
+ : _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] })] }), 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.diff && !tool.error && (tool.diff.oldLines.length > 8 || tool.diff.newLines.length > 8) && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ['⎿ ', tool.diff.oldLines.length, " lines \u2192 ", tool.diff.newLines.length, " lines"] }) })), tool.error && 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))) }))] }, tool.key));
625
+ } }), _jsx(Static, { items: committedResponses, children: (r) => {
626
+ const isUserMsg = r.key.startsWith('user-');
627
+ return (_jsxs(Box, { flexDirection: "column", children: [!isUserMsg && (r.tokens.input > 0 || r.tokens.output > 0) && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(60) }) })), _jsx(Text, { wrap: "wrap", children: renderMarkdown(r.text) }), (r.tokens.input > 0 || r.tokens.output > 0) && (_jsx(Box, { marginLeft: 1, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [r.tier && _jsxs(Text, { color: "cyan", children: ["[", r.tier, "] "] }), r.model ? shortModelName(r.model) : '', r.model ? ' · ' : '', r.tokens.calls > 0 && r.tokens.input === 0
628
+ ? `${r.tokens.calls} calls`
629
+ : `${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.key));
630
+ } }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 1, 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: 3, 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: 1, 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: 3, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
510
631
  const answer = val.trim() || '(no response)';
511
632
  const r = askUserRequest.resolve;
512
633
  setAskUserRequest(null);
513
634
  setAskUserInput('');
514
635
  r(answer);
515
- }, focus: true })] })] })), Array.from(tools.entries()).map(([id, tool]) => (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { color: "cyan", children: [' ', _jsx(Spinner, { type: "dots" }), ' ', tool.name, tool.preview ? _jsxs(Text, { dimColor: true, children: [": ", tool.preview.slice(0, 60)] }) : null, _jsx(Text, { dimColor: true, children: (() => { const s = Math.round((Date.now() - tool.startTime) / 1000); return s > 0 ? ` ${s}s` : ''; })() })] }), tool.liveOutput ? (_jsxs(Text, { color: "yellow", children: [' ', tool.liveOutput.slice(0, 100)] })) : null] }, id))), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { color: "magenta", children: [" ", _jsx(Spinner, { type: "dots" }), " thinking", completedTools.length > 0 ? _jsxs(Text, { dimColor: true, children: [' ', "(step ", completedTools.length + 1, ")"] }) : null] }), thinkingText && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [' ', thinkingText.split('\n').pop()?.slice(0, 100)] }))] })), waiting && !thinking && tools.size === 0 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "yellow", children: [" ", _jsx(Spinner, { type: "dots" }), " ", _jsxs(Text, { dimColor: true, children: [currentModel, completedTools.length > 0 ? ` · step ${completedTools.length + 1}` : ''] })] }) })), streamText && (_jsx(Box, { marginTop: 0, marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(streamText) }) })), responsePreview && !streamText && (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(responsePreview) }) })), inPicker && (() => {
636
+ }, focus: true })] })] })), expandableTool && (() => {
637
+ const tool = expandableTool;
638
+ const elapsedFmt = tool.elapsed >= 1000
639
+ ? `${(tool.elapsed / 1000).toFixed(1)}s`
640
+ : `${tool.elapsed}ms`;
641
+ const hasExpandableContent = !!(tool.diff || (tool.fullOutput && tool.fullOutput.split('\n').length > 1));
642
+ 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))) }))] }));
643
+ })(), Array.from(tools.entries()).map(([id, tool]) => {
644
+ const elapsed = Math.round((Date.now() - tool.startTime) / 1000);
645
+ const elapsedStr = elapsed > 0 ? ` ${elapsed}s` : '';
646
+ 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));
647
+ }), 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] }), thinkingText && (() => {
648
+ const lines = thinkingText.split('\n').filter(Boolean).slice(-3);
649
+ 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))) }));
650
+ })()] })), 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 && (_jsx(Box, { marginTop: 0, marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(streamText) }) })), responsePreview && !streamText && (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(responsePreview) }) })), inPicker && (() => {
516
651
  let flatIdx = 0;
517
652
  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) => {
518
653
  const myIdx = flatIdx++;
@@ -521,7 +656,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
521
656
  const isHighlight = m.highlight === true;
522
657
  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));
523
658
  })] }, 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." }) })] }));
524
- })(), !inPicker && (_jsx(InputBox, { input: (permissionRequest || askUserRequest) ? '' : input, setInput: (permissionRequest || askUserRequest) ? () => { } : setInput, onSubmit: (permissionRequest || askUserRequest) ? () => { } : handleSubmit, model: currentModel, balance: liveBalance, sessionCost: totalCost, queued: queuedInputs[0] || undefined, queuedCount: queuedInputs.length, focused: !permissionRequest && !askUserRequest, busy: !askUserRequest && (waiting || thinking || tools.size > 0), contextPct: contextPct }))] }));
659
+ })(), !inPicker && (_jsx(InputBox, { input: (permissionRequest || askUserRequest) ? '' : input, setInput: (permissionRequest || askUserRequest) ? () => { } : setInput, onSubmit: (permissionRequest || askUserRequest) ? () => { } : handleSubmit, model: currentModel, balance: liveBalance, 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 }))] }));
525
660
  }
526
661
  export function launchInkUI(opts) {
527
662
  let resolveInput = null;
@@ -573,7 +708,7 @@ export function launchInkUI(opts) {
573
708
  return new Promise((resolve) => { resolveInput = resolve; });
574
709
  },
575
710
  onAbort: (cb) => { abortCallback = cb; },
576
- cleanup: () => { instance.unmount(); },
711
+ cleanup: () => { mouse.disable(); instance.unmount(); },
577
712
  requestPermission: (toolName, description) => {
578
713
  const ui = globalThis.__runcode_ui;
579
714
  return ui?.requestPermission(toolName, description) ?? Promise.resolve('no');
@@ -2,6 +2,12 @@
2
2
  * Markdown renderer for terminal output.
3
3
  * Converts markdown to ANSI-formatted text using chalk.
4
4
  * Shared between Ink UI and basic terminal UI.
5
+ *
6
+ * Features beyond basic markdown:
7
+ * - Language labels on code blocks (```ts → TS)
8
+ * - Numbered list support
9
+ * - Nested blockquotes
10
+ * - Task lists (- [x] done, - [ ] todo)
5
11
  */
6
12
  /**
7
13
  * Render a complete markdown string to ANSI-colored terminal output.
@@ -2,8 +2,30 @@
2
2
  * Markdown renderer for terminal output.
3
3
  * Converts markdown to ANSI-formatted text using chalk.
4
4
  * Shared between Ink UI and basic terminal UI.
5
+ *
6
+ * Features beyond basic markdown:
7
+ * - Language labels on code blocks (```ts → TS)
8
+ * - Numbered list support
9
+ * - Nested blockquotes
10
+ * - Task lists (- [x] done, - [ ] todo)
5
11
  */
6
12
  import chalk from 'chalk';
13
+ /** Short language label for code block headers. */
14
+ const LANG_LABELS = {
15
+ ts: 'TypeScript', typescript: 'TypeScript',
16
+ js: 'JavaScript', javascript: 'JavaScript',
17
+ py: 'Python', python: 'Python',
18
+ rs: 'Rust', rust: 'Rust',
19
+ go: 'Go', golang: 'Go',
20
+ sh: 'Shell', bash: 'Shell', zsh: 'Shell', shell: 'Shell',
21
+ json: 'JSON', yaml: 'YAML', yml: 'YAML', toml: 'TOML',
22
+ sql: 'SQL', html: 'HTML', css: 'CSS', xml: 'XML',
23
+ md: 'Markdown', markdown: 'Markdown',
24
+ diff: 'Diff', dockerfile: 'Dockerfile',
25
+ c: 'C', cpp: 'C++', java: 'Java', rb: 'Ruby', ruby: 'Ruby',
26
+ swift: 'Swift', kt: 'Kotlin', kotlin: 'Kotlin',
27
+ tsx: 'TSX', jsx: 'JSX',
28
+ };
7
29
  /**
8
30
  * Render a complete markdown string to ANSI-colored terminal output.
9
31
  */
@@ -11,14 +33,41 @@ export function renderMarkdown(text) {
11
33
  const lines = text.split('\n');
12
34
  const out = [];
13
35
  let inCodeBlock = false;
36
+ let codeBlockLang = '';
14
37
  for (const line of lines) {
15
38
  // Code block toggle
16
39
  if (line.startsWith('```')) {
40
+ if (!inCodeBlock) {
41
+ // Opening — extract language
42
+ const lang = line.slice(3).trim().split(/\s/)[0].toLowerCase();
43
+ codeBlockLang = lang;
44
+ const label = LANG_LABELS[lang] || (lang ? lang.toUpperCase() : '');
45
+ out.push(chalk.dim('```') + (label ? chalk.dim.italic(` ${label}`) : ''));
46
+ }
47
+ else {
48
+ // Closing
49
+ out.push(chalk.dim('```'));
50
+ codeBlockLang = '';
51
+ }
17
52
  inCodeBlock = !inCodeBlock;
18
- out.push(chalk.dim(line));
19
53
  continue;
20
54
  }
21
55
  if (inCodeBlock) {
56
+ // Diff-style highlighting inside code blocks
57
+ if (codeBlockLang === 'diff') {
58
+ if (line.startsWith('+')) {
59
+ out.push(chalk.green(line));
60
+ continue;
61
+ }
62
+ if (line.startsWith('-')) {
63
+ out.push(chalk.red(line));
64
+ continue;
65
+ }
66
+ if (line.startsWith('@@')) {
67
+ out.push(chalk.cyan(line));
68
+ continue;
69
+ }
70
+ }
22
71
  out.push(chalk.cyan(line));
23
72
  continue;
24
73
  }
@@ -40,9 +89,21 @@ export function renderMarkdown(text) {
40
89
  out.push(chalk.dim('─'.repeat(40)));
41
90
  continue;
42
91
  }
43
- // Blockquotes
44
- if (line.startsWith('> ')) {
45
- out.push(chalk.dim('│ ') + chalk.italic(renderInline(line.slice(2))));
92
+ // Blockquotes (support nesting: >> , >>> )
93
+ const bqMatch = line.match(/^((?:>\s*)+)(.*)/);
94
+ if (bqMatch) {
95
+ const depth = (bqMatch[1].match(/>/g) || []).length;
96
+ const prefix = chalk.dim('│ '.repeat(depth));
97
+ out.push(prefix + chalk.italic(renderInline(bqMatch[2].trim())));
98
+ continue;
99
+ }
100
+ // Task lists: - [x] done, - [ ] todo
101
+ const taskMatch = line.match(/^(\s*)[-*] \[([ xX])\] (.*)/);
102
+ if (taskMatch) {
103
+ const indent = taskMatch[1];
104
+ const checked = taskMatch[2] !== ' ';
105
+ const label = taskMatch[3];
106
+ out.push(indent + (checked ? chalk.green('✓') : chalk.dim('○')) + ' ' + renderInline(label));
46
107
  continue;
47
108
  }
48
109
  // Bullet points
@@ -50,6 +111,12 @@ export function renderMarkdown(text) {
50
111
  out.push(line.replace(/^(\s*)[-*] /, '$1• ').replace(/^(\s*• )(.*)/, (_, prefix, rest) => prefix + renderInline(rest)));
51
112
  continue;
52
113
  }
114
+ // Numbered lists: 1. , 2. , etc.
115
+ const numMatch = line.match(/^(\s*)(\d+)\. (.*)/);
116
+ if (numMatch) {
117
+ out.push(numMatch[1] + chalk.dim(numMatch[2] + '.') + ' ' + renderInline(numMatch[3]));
118
+ continue;
119
+ }
53
120
  // Table rows — render with dim separators
54
121
  if (line.includes('|') && line.trim().startsWith('|')) {
55
122
  // Separator row (|---|---|)
@@ -57,7 +124,7 @@ export function renderMarkdown(text) {
57
124
  out.push(chalk.dim(line));
58
125
  continue;
59
126
  }
60
- // Data row — bold headers in first row, dim pipes
127
+ // Data row — dim pipes
61
128
  const cells = line.split('|').map(c => c.trim()).filter(Boolean);
62
129
  const formatted = cells.map(c => renderInline(c)).join(chalk.dim(' │ '));
63
130
  out.push(chalk.dim('│ ') + formatted + chalk.dim(' │'));
@@ -69,7 +136,7 @@ export function renderMarkdown(text) {
69
136
  return out.join('\n');
70
137
  }
71
138
  /**
72
- * Render inline markdown formatting (bold, italic, code, links).
139
+ * Render inline markdown formatting (bold, italic, code, links, strikethrough).
73
140
  */
74
141
  function renderInline(text) {
75
142
  return text
@@ -54,7 +54,7 @@ export const MODEL_SHORTCUTS = {
54
54
  // Others
55
55
  minimax: 'minimax/minimax-m2.7',
56
56
  glm: 'zai/glm-5.1',
57
- 'glm-turbo': 'zai/glm-5.1-turbo',
57
+ 'glm-turbo': 'zai/glm-5-turbo',
58
58
  'glm5': 'zai/glm-5.1',
59
59
  kimi: 'moonshot/kimi-k2.5',
60
60
  };
@@ -79,7 +79,7 @@ export const PICKER_CATEGORIES = [
79
79
  category: '🔥 Promo (flat $0.001/call)',
80
80
  models: [
81
81
  { id: 'zai/glm-5.1', shortcut: 'glm', label: 'GLM-5.1', price: '$0.001/call', highlight: true },
82
- { id: 'zai/glm-5.1-turbo', shortcut: 'glm-turbo', label: 'GLM-5.1 Turbo', price: '$0.001/call', highlight: true },
82
+ { id: 'zai/glm-5-turbo', shortcut: 'glm-turbo', label: 'GLM-5 Turbo', price: '$0.001/call', highlight: true },
83
83
  ],
84
84
  },
85
85
  {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Mouse event support for Ink terminal UI.
3
+ * Enables SGR extended mouse tracking (DECSET 1000+1006) and parses events from stdin.
4
+ * Lightweight — only handles clicks, not drag/hover/selection.
5
+ */
6
+ import { EventEmitter } from 'node:events';
7
+ export interface MouseEvent {
8
+ button: 'left' | 'middle' | 'right' | 'wheel-up' | 'wheel-down';
9
+ action: 'press' | 'release';
10
+ col: number;
11
+ row: number;
12
+ }
13
+ declare class MouseManager extends EventEmitter {
14
+ private enabled;
15
+ private stdinListener;
16
+ /**
17
+ * Enable mouse tracking. Call once on app startup.
18
+ * Returns cleanup function to call on unmount.
19
+ */
20
+ enable(): () => void;
21
+ /**
22
+ * Disable mouse tracking and clean up.
23
+ */
24
+ disable(): void;
25
+ isEnabled(): boolean;
26
+ }
27
+ /** Singleton mouse manager. */
28
+ export declare const mouse: MouseManager;
29
+ export {};