@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/agent/bash-guard.d.ts +17 -0
- package/dist/agent/bash-guard.js +158 -0
- package/dist/agent/permissions.js +41 -2
- package/dist/agent/streaming-executor.js +32 -0
- package/dist/agent/tokens.js +1 -1
- package/dist/agent/types.d.ts +9 -0
- package/dist/mcp/client.js +36 -0
- package/dist/pricing.js +1 -1
- package/dist/tools/bash.js +56 -1
- package/dist/tools/edit.js +4 -2
- package/dist/tools/read.d.ts +2 -0
- package/dist/tools/read.js +28 -0
- package/dist/tools/write.js +2 -1
- package/dist/ui/app.js +167 -32
- package/dist/ui/markdown.d.ts +6 -0
- package/dist/ui/markdown.js +73 -6
- package/dist/ui/model-picker.js +2 -2
- package/dist/ui/mouse.d.ts +29 -0
- package/dist/ui/mouse.js +89 -0
- package/dist/ui/terminal.js +45 -28
- package/dist/ui/vim-input.d.ts +19 -0
- package/dist/ui/vim-input.js +439 -0
- package/package.json +1 -1
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 ? (
|
|
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) =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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
|
|
419
|
-
|
|
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) =>
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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 })] })] })),
|
|
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');
|
package/dist/ui/markdown.d.ts
CHANGED
|
@@ -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.
|
package/dist/ui/markdown.js
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
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 —
|
|
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
|
package/dist/ui/model-picker.js
CHANGED
|
@@ -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
|
|
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
|
|
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 {};
|