@blockrun/franklin 3.3.0 → 3.3.1
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/README.md +216 -233
- package/dist/agent/commands.js +24 -12
- package/dist/agent/context.js +18 -1
- package/dist/agent/loop.js +48 -19
- package/dist/banner.js +40 -27
- package/dist/commands/migrate.d.ts +13 -0
- package/dist/commands/migrate.js +389 -0
- package/dist/commands/panel.d.ts +6 -0
- package/dist/commands/panel.js +29 -0
- package/dist/commands/start.js +12 -4
- package/dist/index.js +15 -0
- package/dist/mcp/client.js +9 -2
- package/dist/panel/html.d.ts +5 -0
- package/dist/panel/html.js +341 -0
- package/dist/panel/server.d.ts +7 -0
- package/dist/panel/server.js +152 -0
- package/dist/session/storage.js +4 -2
- package/dist/stats/tracker.d.ts +1 -0
- package/dist/stats/tracker.js +59 -13
- package/dist/tools/bash.js +6 -1
- package/dist/tools/index.js +0 -4
- package/dist/tools/webfetch.js +19 -9
- package/dist/tools/write.js +2 -0
- package/dist/ui/app.js +73 -44
- package/dist/ui/markdown.d.ts +9 -0
- package/dist/ui/markdown.js +86 -0
- package/package.json +1 -1
package/dist/tools/webfetch.js
CHANGED
|
@@ -9,27 +9,30 @@ const MAX_BODY_BYTES = 256 * 1024; // 256KB
|
|
|
9
9
|
const CACHE_TTL_MS = 15 * 60 * 1000;
|
|
10
10
|
const MAX_CACHE_ENTRIES = 50;
|
|
11
11
|
const fetchCache = new Map();
|
|
12
|
-
function
|
|
13
|
-
|
|
12
|
+
function cacheKey(url, maxLength) {
|
|
13
|
+
return `${url}::${maxLength}`;
|
|
14
|
+
}
|
|
15
|
+
function getCached(key) {
|
|
16
|
+
const entry = fetchCache.get(key);
|
|
14
17
|
if (!entry)
|
|
15
18
|
return null;
|
|
16
19
|
if (Date.now() > entry.expiresAt) {
|
|
17
|
-
fetchCache.delete(
|
|
20
|
+
fetchCache.delete(key);
|
|
18
21
|
return null;
|
|
19
22
|
}
|
|
20
23
|
return entry.output;
|
|
21
24
|
}
|
|
22
|
-
function setCached(
|
|
25
|
+
function setCached(key, output) {
|
|
23
26
|
// Evict oldest entry if at capacity
|
|
24
27
|
if (fetchCache.size >= MAX_CACHE_ENTRIES) {
|
|
25
28
|
const firstKey = fetchCache.keys().next().value;
|
|
26
29
|
if (firstKey)
|
|
27
30
|
fetchCache.delete(firstKey);
|
|
28
31
|
}
|
|
29
|
-
fetchCache.set(
|
|
32
|
+
fetchCache.set(key, { output, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
30
33
|
}
|
|
31
34
|
// ─── Execute ────────────────────────────────────────────────────────────────
|
|
32
|
-
async function execute(input,
|
|
35
|
+
async function execute(input, ctx) {
|
|
33
36
|
const { url, max_length } = input;
|
|
34
37
|
if (!url) {
|
|
35
38
|
return { output: 'Error: url is required', isError: true };
|
|
@@ -45,13 +48,17 @@ async function execute(input, _ctx) {
|
|
|
45
48
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
46
49
|
return { output: `Error: only http/https URLs are supported`, isError: true };
|
|
47
50
|
}
|
|
51
|
+
const maxLen = Math.min(max_length ?? MAX_BODY_BYTES, MAX_BODY_BYTES);
|
|
52
|
+
const key = cacheKey(url, maxLen);
|
|
48
53
|
// Check cache first
|
|
49
|
-
const cached = getCached(
|
|
54
|
+
const cached = getCached(key);
|
|
50
55
|
if (cached) {
|
|
51
56
|
return { output: cached + '\n\n(cached)' };
|
|
52
57
|
}
|
|
53
58
|
const controller = new AbortController();
|
|
54
59
|
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
60
|
+
const onAbort = () => controller.abort();
|
|
61
|
+
ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
55
62
|
try {
|
|
56
63
|
const response = await fetch(url, {
|
|
57
64
|
signal: controller.signal,
|
|
@@ -68,7 +75,6 @@ async function execute(input, _ctx) {
|
|
|
68
75
|
};
|
|
69
76
|
}
|
|
70
77
|
const contentType = response.headers.get('content-type') || '';
|
|
71
|
-
const maxLen = Math.min(max_length ?? MAX_BODY_BYTES, MAX_BODY_BYTES);
|
|
72
78
|
// Read body with size limit
|
|
73
79
|
const reader = response.body?.getReader();
|
|
74
80
|
if (!reader) {
|
|
@@ -106,11 +112,14 @@ async function execute(input, _ctx) {
|
|
|
106
112
|
output += '\n\n... (content truncated)';
|
|
107
113
|
}
|
|
108
114
|
// Cache successful responses
|
|
109
|
-
setCached(
|
|
115
|
+
setCached(key, output);
|
|
110
116
|
return { output };
|
|
111
117
|
}
|
|
112
118
|
catch (err) {
|
|
113
119
|
const msg = err instanceof Error ? err.message : String(err);
|
|
120
|
+
if (ctx.abortSignal.aborted) {
|
|
121
|
+
return { output: `Error: request aborted for ${url}`, isError: true };
|
|
122
|
+
}
|
|
114
123
|
if (msg.includes('abort')) {
|
|
115
124
|
return { output: `Error: request timed out after 30s for ${url}`, isError: true };
|
|
116
125
|
}
|
|
@@ -118,6 +127,7 @@ async function execute(input, _ctx) {
|
|
|
118
127
|
}
|
|
119
128
|
finally {
|
|
120
129
|
clearTimeout(timeout);
|
|
130
|
+
ctx.abortSignal.removeEventListener('abort', onAbort);
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
function stripHtml(html) {
|
package/dist/tools/write.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
|
+
import { partiallyReadFiles } from './read.js';
|
|
7
8
|
function withTrailingSep(value) {
|
|
8
9
|
return value.endsWith(path.sep) ? value : value + path.sep;
|
|
9
10
|
}
|
|
@@ -84,6 +85,7 @@ async function execute(input, ctx) {
|
|
|
84
85
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
85
86
|
const existed = fs.existsSync(resolved);
|
|
86
87
|
fs.writeFileSync(resolved, content, 'utf-8');
|
|
88
|
+
partiallyReadFiles.delete(resolved);
|
|
87
89
|
const lineCount = content.split('\n').length;
|
|
88
90
|
const byteCount = Buffer.byteLength(content, 'utf-8');
|
|
89
91
|
const sizeStr = byteCount >= 1024 ? `${(byteCount / 1024).toFixed(1)}KB` : `${byteCount}B`;
|
package/dist/ui/app.js
CHANGED
|
@@ -3,21 +3,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
* RunCode ink-based terminal UI.
|
|
4
4
|
* Real-time streaming, thinking animation, tool progress, slash commands.
|
|
5
5
|
*/
|
|
6
|
+
import chalk from 'chalk';
|
|
6
7
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
7
8
|
import { render, Static, Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
8
9
|
import Spinner from 'ink-spinner';
|
|
9
10
|
import TextInput from 'ink-text-input';
|
|
11
|
+
import { renderMarkdown } from './markdown.js';
|
|
10
12
|
import { resolveModel, PICKER_CATEGORIES, PICKER_MODELS_FLAT, } from './model-picker.js';
|
|
11
13
|
import { estimateCost } from '../pricing.js';
|
|
12
14
|
// ─── Full-width input box ──────────────────────────────────────────────────
|
|
13
|
-
function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queued, focused, busy }) {
|
|
15
|
+
function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queued, queuedCount, focused, busy }) {
|
|
14
16
|
const { stdout } = useStdout();
|
|
15
17
|
const cols = stdout?.columns ?? 80;
|
|
16
18
|
const innerWidth = Math.min(Math.max(30, cols - 4), cols - 2);
|
|
17
19
|
const placeholder = busy
|
|
18
|
-
? (queued
|
|
20
|
+
? (queued
|
|
21
|
+
? `⏎ ${queuedCount ?? 1} queued: ${queued.slice(0, 40)}`
|
|
22
|
+
: 'Working...')
|
|
19
23
|
: 'Type a message...';
|
|
20
|
-
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)] }) : '', ' · esc to abort/quit'] }) })] }));
|
|
24
|
+
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)] }) : '', (queuedCount ?? 0) > 0 ? _jsxs(Text, { color: "cyan", children: [" \u00B7 ", queuedCount, " queued"] }) : null, ' · esc to abort/quit'] }) })] }));
|
|
21
25
|
}
|
|
22
26
|
function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }) {
|
|
23
27
|
const { exit } = useApp();
|
|
@@ -37,6 +41,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
37
41
|
const [mode, setMode] = useState(startWithPicker ? 'model-picker' : 'input');
|
|
38
42
|
const [pickerIdx, setPickerIdx] = useState(0);
|
|
39
43
|
const [statusMsg, setStatusMsg] = useState('');
|
|
44
|
+
const [statusTone, setStatusTone] = useState('success');
|
|
40
45
|
const [turnTokens, setTurnTokens] = useState({ input: 0, output: 0, calls: 0 });
|
|
41
46
|
const [totalCost, setTotalCost] = useState(0);
|
|
42
47
|
const [showHelp, setShowHelp] = useState(false);
|
|
@@ -59,26 +64,50 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
59
64
|
const [permissionRequest, setPermissionRequest] = useState(null);
|
|
60
65
|
const [askUserRequest, setAskUserRequest] = useState(null);
|
|
61
66
|
const [askUserInput, setAskUserInput] = useState('');
|
|
62
|
-
//
|
|
63
|
-
const [
|
|
67
|
+
// Messages queued while agent is busy — auto-submitted FIFO when turns complete.
|
|
68
|
+
const [queuedInputs, setQueuedInputs] = useState([]);
|
|
64
69
|
const turnDoneCallbackRef = useRef(null);
|
|
65
70
|
// Refs to read current state values inside memoized event handlers (avoids stale closures)
|
|
66
71
|
const streamTextRef = useRef('');
|
|
67
72
|
const turnTokensRef = useRef({ input: 0, output: 0, calls: 0 });
|
|
68
73
|
const totalCostRef = useRef(0);
|
|
69
74
|
const turnCostRef = useRef(0); // per-turn cost (reset each turn)
|
|
70
|
-
const
|
|
75
|
+
const queuedInputsRef = useRef([]);
|
|
71
76
|
// Keep refs in sync so memoized event handlers can read current values
|
|
72
77
|
streamTextRef.current = streamText;
|
|
73
78
|
turnTokensRef.current = turnTokens;
|
|
74
79
|
totalCostRef.current = totalCost;
|
|
75
|
-
|
|
80
|
+
queuedInputsRef.current = queuedInputs;
|
|
76
81
|
costAtLastFetchRef.current = costAtLastFetch;
|
|
77
82
|
baseBalanceNumRef.current = baseBalanceNum;
|
|
78
83
|
// Compute live balance = fetchedBalance - spend_since_last_fetch
|
|
79
84
|
const liveBalance = baseBalanceNum !== null
|
|
80
85
|
? `$${Math.max(0, baseBalanceNum - (totalCost - costAtLastFetch)).toFixed(2)} USDC`
|
|
81
86
|
: balance;
|
|
87
|
+
const showStatus = useCallback((text, tone = 'success', durationMs = 3000) => {
|
|
88
|
+
setStatusTone(tone);
|
|
89
|
+
setStatusMsg(text);
|
|
90
|
+
if (durationMs > 0) {
|
|
91
|
+
setTimeout(() => setStatusMsg(''), durationMs);
|
|
92
|
+
}
|
|
93
|
+
}, []);
|
|
94
|
+
const commitResponse = useCallback((text, tokens = turnTokensRef.current, cost = turnCostRef.current) => {
|
|
95
|
+
if (!text.trim())
|
|
96
|
+
return;
|
|
97
|
+
setCommittedResponses((rs) => [...rs, {
|
|
98
|
+
key: String(Date.now() + Math.random()),
|
|
99
|
+
text,
|
|
100
|
+
tokens,
|
|
101
|
+
cost,
|
|
102
|
+
}]);
|
|
103
|
+
const allLines = text.split('\n');
|
|
104
|
+
if (allLines.length > 20) {
|
|
105
|
+
setResponsePreview(' ↑ scroll to see full reply\n' + allLines.slice(-20).join('\n'));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
setResponsePreview('');
|
|
109
|
+
}
|
|
110
|
+
}, []);
|
|
82
111
|
// Permission dialog key handler — captures y/n/a when dialog is visible.
|
|
83
112
|
// ink 6.x: useInput handlers all fire regardless of TextInput focus prop,
|
|
84
113
|
// so we handle here AND block TextInput onChange (see focused prop below).
|
|
@@ -110,11 +139,10 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
110
139
|
// Escape during generation → abort current turn (skip if permission dialog open)
|
|
111
140
|
if (key.escape && !ready && !permissionRequest) {
|
|
112
141
|
onAbort();
|
|
113
|
-
|
|
142
|
+
showStatus('Aborted', 'warning', 3000);
|
|
114
143
|
setReady(true);
|
|
115
144
|
setWaiting(false);
|
|
116
145
|
setThinking(false);
|
|
117
|
-
setTimeout(() => setStatusMsg(''), 3000);
|
|
118
146
|
return;
|
|
119
147
|
}
|
|
120
148
|
// Esc to quit (only when input is empty and in input mode)
|
|
@@ -134,10 +162,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
134
162
|
const selected = PICKER_MODELS_FLAT[pickerIdx];
|
|
135
163
|
setCurrentModel(selected.id);
|
|
136
164
|
onModelChange(selected.id);
|
|
137
|
-
|
|
165
|
+
showStatus(`Model → ${selected.label}`, 'success', 3000);
|
|
138
166
|
setMode('input');
|
|
139
167
|
setReady(true);
|
|
140
|
-
setTimeout(() => setStatusMsg(''), 3000);
|
|
141
168
|
}
|
|
142
169
|
else if (key.escape) {
|
|
143
170
|
setMode('input');
|
|
@@ -169,8 +196,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
169
196
|
return;
|
|
170
197
|
// If agent is busy, queue the message — it will be auto-submitted when the turn finishes
|
|
171
198
|
if (!ready) {
|
|
172
|
-
|
|
199
|
+
setQueuedInputs(prev => [...prev, trimmed]);
|
|
173
200
|
setInput('');
|
|
201
|
+
showStatus(`Queued message (${queuedInputsRef.current.length + 1} pending)`, 'warning', 1500);
|
|
174
202
|
return;
|
|
175
203
|
}
|
|
176
204
|
// Bare exit/quit (no slash needed)
|
|
@@ -199,8 +227,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
199
227
|
const resolved = resolveModel(parts[1]);
|
|
200
228
|
setCurrentModel(resolved);
|
|
201
229
|
onModelChange(resolved);
|
|
202
|
-
|
|
203
|
-
setTimeout(() => setStatusMsg(''), 3000);
|
|
230
|
+
showStatus(`Model → ${resolved}`, 'success', 3000);
|
|
204
231
|
}
|
|
205
232
|
else {
|
|
206
233
|
const idx = PICKER_MODELS_FLAT.findIndex(m => m.id === currentModel);
|
|
@@ -215,8 +242,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
215
242
|
return;
|
|
216
243
|
case '/cost':
|
|
217
244
|
case '/usage':
|
|
218
|
-
|
|
219
|
-
setTimeout(() => setStatusMsg(''), 4000);
|
|
245
|
+
showStatus(`Cost: $${totalCost.toFixed(4)} this session`, 'success', 4000);
|
|
220
246
|
return;
|
|
221
247
|
case '/help':
|
|
222
248
|
setShowHelp(true);
|
|
@@ -234,8 +260,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
234
260
|
return;
|
|
235
261
|
case '/retry':
|
|
236
262
|
if (!lastPrompt) {
|
|
237
|
-
|
|
238
|
-
setTimeout(() => setStatusMsg(''), 3000);
|
|
263
|
+
showStatus('No previous prompt to retry', 'warning', 3000);
|
|
239
264
|
return;
|
|
240
265
|
}
|
|
241
266
|
setStreamText('');
|
|
@@ -261,6 +286,13 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
261
286
|
}
|
|
262
287
|
}
|
|
263
288
|
// ── Normal prompt ──
|
|
289
|
+
// Show user message in scrollback so the conversation is readable
|
|
290
|
+
setCommittedResponses(rs => [...rs, {
|
|
291
|
+
key: `user-${Date.now()}`,
|
|
292
|
+
text: chalk.cyan('❯') + ' ' + trimmed,
|
|
293
|
+
tokens: { input: 0, output: 0, calls: 0 },
|
|
294
|
+
cost: 0,
|
|
295
|
+
}]);
|
|
264
296
|
setResponsePreview('');
|
|
265
297
|
setLastPrompt(trimmed);
|
|
266
298
|
setInputHistory(prev => [...prev.slice(-49), trimmed]); // Keep last 50
|
|
@@ -279,7 +311,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
279
311
|
setTurnTokens({ input: 0, output: 0, calls: 0 });
|
|
280
312
|
turnCostRef.current = 0;
|
|
281
313
|
onSubmit(trimmed);
|
|
282
|
-
}, [ready, currentModel, totalCost, onSubmit, onModelChange, onAbort, onExit, exit, lastPrompt, inputHistory]);
|
|
314
|
+
}, [ready, currentModel, totalCost, onSubmit, onModelChange, onAbort, onExit, exit, lastPrompt, inputHistory, showStatus]);
|
|
283
315
|
// Expose event handler, balance updater, and permission bridge
|
|
284
316
|
useEffect(() => {
|
|
285
317
|
globalThis.__runcode_ui = {
|
|
@@ -387,27 +419,24 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
387
419
|
break;
|
|
388
420
|
}
|
|
389
421
|
case 'turn_done': {
|
|
390
|
-
// Commit full response to Static immediately — enters terminal scrollback like Claude Code.
|
|
391
|
-
// Also keep a short preview (last 5 lines) visible in the dynamic area.
|
|
392
422
|
const text = streamTextRef.current;
|
|
393
423
|
if (text.trim()) {
|
|
394
|
-
|
|
395
|
-
key: String(Date.now()),
|
|
396
|
-
text,
|
|
397
|
-
tokens: turnTokensRef.current,
|
|
398
|
-
cost: turnCostRef.current, // per-turn cost, not cumulative
|
|
399
|
-
}]);
|
|
400
|
-
// Preview = only show when response is long enough to scroll off screen
|
|
401
|
-
const allLines = text.split('\n');
|
|
402
|
-
if (allLines.length > 20) {
|
|
403
|
-
setResponsePreview(' ↑ scroll to see full reply\n' + allLines.slice(-20).join('\n'));
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
// Short response: already fully visible in scrollback, no preview needed
|
|
407
|
-
setResponsePreview('');
|
|
408
|
-
}
|
|
424
|
+
commitResponse(text, turnTokensRef.current, turnCostRef.current);
|
|
409
425
|
setStreamText('');
|
|
410
426
|
}
|
|
427
|
+
if (event.reason === 'error' && event.error) {
|
|
428
|
+
commitResponse(`Error: ${event.error}`, turnTokensRef.current, turnCostRef.current);
|
|
429
|
+
showStatus('Turn failed', 'error', 5000);
|
|
430
|
+
}
|
|
431
|
+
else if (event.reason === 'aborted') {
|
|
432
|
+
showStatus('Aborted', 'warning', 3000);
|
|
433
|
+
}
|
|
434
|
+
else if (event.reason === 'max_turns') {
|
|
435
|
+
showStatus('Stopped after reaching max turns', 'warning', 5000);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
setStatusMsg('');
|
|
439
|
+
}
|
|
411
440
|
setReady(true);
|
|
412
441
|
setWaiting(false);
|
|
413
442
|
setThinking(false);
|
|
@@ -417,10 +446,10 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
417
446
|
// Ring the terminal bell so the user knows the AI finished
|
|
418
447
|
// (shows notification badge in iTerm2/Terminal.app when tabbed away)
|
|
419
448
|
process.stderr.write('\x07');
|
|
420
|
-
// Auto-submit any message
|
|
421
|
-
const queued =
|
|
449
|
+
// Auto-submit any queued message while agent was busy
|
|
450
|
+
const queued = queuedInputsRef.current[0];
|
|
422
451
|
if (queued) {
|
|
423
|
-
|
|
452
|
+
setQueuedInputs((prev) => prev.slice(1));
|
|
424
453
|
// Small delay so React can flush the ready=true state first
|
|
425
454
|
setTimeout(() => {
|
|
426
455
|
const fn = globalThis.__runcode_submit;
|
|
@@ -440,7 +469,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
440
469
|
delete globalThis.__runcode_ui;
|
|
441
470
|
delete globalThis.__runcode_submit;
|
|
442
471
|
};
|
|
443
|
-
}, [handleSubmit]);
|
|
472
|
+
}, [handleSubmit, commitResponse, showStatus]);
|
|
444
473
|
// ── Render ──
|
|
445
474
|
// Note: the tree is ALWAYS the same shape across mode changes. Static
|
|
446
475
|
// components (completedTools, committedResponses) stay mounted so Ink
|
|
@@ -448,9 +477,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
448
477
|
// opens/closes. The picker is rendered inline below scrollback, and the
|
|
449
478
|
// InputBox is hidden while it's active.
|
|
450
479
|
const inPicker = mode === 'model-picker';
|
|
451
|
-
return (_jsxs(Box, { flexDirection: "column", children: [statusMsg && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color:
|
|
480
|
+
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
|
|
452
481
|
? _jsxs(Text, { color: "red", children: [" \u2717 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms", tool.preview ? ` — ${tool.preview}` : ''] })] })
|
|
453
|
-
: _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: r.text }), (r.tokens.input > 0 || r.tokens.output > 0) && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [r.tokens.calls > 0 && r.tokens.input === 0
|
|
482
|
+
: _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.tokens.calls > 0 && r.tokens.input === 0
|
|
454
483
|
? `${r.tokens.calls} calls`
|
|
455
484
|
: `${r.tokens.input.toLocaleString()} in / ${r.tokens.output.toLocaleString()} out${r.tokens.calls > 0 ? ` / ${r.tokens.calls} calls` : ''}`, r.cost > 0 ? ` · $${r.cost.toFixed(4)}` : ''] }) }))] }, 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) => {
|
|
456
485
|
const answer = val.trim() || '(no response)';
|
|
@@ -458,7 +487,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
458
487
|
setAskUserRequest(null);
|
|
459
488
|
setAskUserInput('');
|
|
460
489
|
r(answer);
|
|
461
|
-
}, 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] }) : null, _jsx(Text, { dimColor: true, children: (() => { const s = Math.round((Date.now() - tool.startTime) / 1000); return s > 0 ? ` ${s}s` : ''; })() })] }), tool.liveOutput ? (_jsxs(Text, { dimColor: true, children: [" \u2514 ", tool.liveOutput] })) : null] }, id))), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { color: "magenta", children: [" ", _jsx(Spinner, { type: "dots" }), " thinking..."] }), thinkingText && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", thinkingText.split('\n').pop()?.slice(0, 80)] }))] })), waiting && !thinking && tools.size === 0 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "yellow", children: [" ", _jsx(Spinner, { type: "dots" }), " ", _jsx(Text, { dimColor: true, children: currentModel })] }) })), streamText && (_jsx(Box, { marginTop: 0, marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: streamText }) })), responsePreview && !streamText && (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: responsePreview }) })), inPicker && (() => {
|
|
490
|
+
}, 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] }) : null, _jsx(Text, { dimColor: true, children: (() => { const s = Math.round((Date.now() - tool.startTime) / 1000); return s > 0 ? ` ${s}s` : ''; })() })] }), tool.liveOutput ? (_jsxs(Text, { dimColor: true, children: [" \u2514 ", tool.liveOutput] })) : null] }, id))), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { color: "magenta", children: [" ", _jsx(Spinner, { type: "dots" }), " thinking..."] }), thinkingText && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", thinkingText.split('\n').pop()?.slice(0, 80)] }))] })), waiting && !thinking && tools.size === 0 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "yellow", children: [" ", _jsx(Spinner, { type: "dots" }), " ", _jsx(Text, { dimColor: true, children: currentModel })] }) })), 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 && (() => {
|
|
462
491
|
let flatIdx = 0;
|
|
463
492
|
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) => {
|
|
464
493
|
const myIdx = flatIdx++;
|
|
@@ -467,7 +496,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
467
496
|
const isHighlight = m.highlight === true;
|
|
468
497
|
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));
|
|
469
498
|
})] }, 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." }) })] }));
|
|
470
|
-
})(), !inPicker && (_jsx(InputBox, { input: (permissionRequest || askUserRequest) ? '' : input, setInput: (permissionRequest || askUserRequest) ? () => { } : setInput, onSubmit: (permissionRequest || askUserRequest) ? () => { } : handleSubmit, model: currentModel, balance: liveBalance, sessionCost: totalCost, queued:
|
|
499
|
+
})(), !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) }))] }));
|
|
471
500
|
}
|
|
472
501
|
export function launchInkUI(opts) {
|
|
473
502
|
let resolveInput = null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown renderer for terminal output.
|
|
3
|
+
* Converts markdown to ANSI-formatted text using chalk.
|
|
4
|
+
* Shared between Ink UI and basic terminal UI.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Render a complete markdown string to ANSI-colored terminal output.
|
|
8
|
+
*/
|
|
9
|
+
export declare function renderMarkdown(text: string): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown renderer for terminal output.
|
|
3
|
+
* Converts markdown to ANSI-formatted text using chalk.
|
|
4
|
+
* Shared between Ink UI and basic terminal UI.
|
|
5
|
+
*/
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
/**
|
|
8
|
+
* Render a complete markdown string to ANSI-colored terminal output.
|
|
9
|
+
*/
|
|
10
|
+
export function renderMarkdown(text) {
|
|
11
|
+
const lines = text.split('\n');
|
|
12
|
+
const out = [];
|
|
13
|
+
let inCodeBlock = false;
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
// Code block toggle
|
|
16
|
+
if (line.startsWith('```')) {
|
|
17
|
+
inCodeBlock = !inCodeBlock;
|
|
18
|
+
out.push(chalk.dim(line));
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (inCodeBlock) {
|
|
22
|
+
out.push(chalk.cyan(line));
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
// Headers
|
|
26
|
+
if (line.startsWith('### ')) {
|
|
27
|
+
out.push(chalk.bold(line.slice(4)));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (line.startsWith('## ')) {
|
|
31
|
+
out.push(chalk.bold.underline(line.slice(3)));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (line.startsWith('# ')) {
|
|
35
|
+
out.push(chalk.bold.underline(line.slice(2)));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// Horizontal rule
|
|
39
|
+
if (/^[-=─]{3,}$/.test(line.trim())) {
|
|
40
|
+
out.push(chalk.dim('─'.repeat(40)));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Blockquotes
|
|
44
|
+
if (line.startsWith('> ')) {
|
|
45
|
+
out.push(chalk.dim('│ ') + chalk.italic(renderInline(line.slice(2))));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// Bullet points
|
|
49
|
+
if (line.match(/^(\s*)[-*] /)) {
|
|
50
|
+
out.push(line.replace(/^(\s*)[-*] /, '$1• ').replace(/^(\s*• )(.*)/, (_, prefix, rest) => prefix + renderInline(rest)));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// Table rows — render with dim separators
|
|
54
|
+
if (line.includes('|') && line.trim().startsWith('|')) {
|
|
55
|
+
// Separator row (|---|---|)
|
|
56
|
+
if (/^\s*\|[\s-:]+\|/.test(line) && !line.match(/[a-zA-Z]/)) {
|
|
57
|
+
out.push(chalk.dim(line));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Data row — bold headers in first row, dim pipes
|
|
61
|
+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
62
|
+
const formatted = cells.map(c => renderInline(c)).join(chalk.dim(' │ '));
|
|
63
|
+
out.push(chalk.dim('│ ') + formatted + chalk.dim(' │'));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Everything else — inline formatting
|
|
67
|
+
out.push(renderInline(line));
|
|
68
|
+
}
|
|
69
|
+
return out.join('\n');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Render inline markdown formatting (bold, italic, code, links).
|
|
73
|
+
*/
|
|
74
|
+
function renderInline(text) {
|
|
75
|
+
return text
|
|
76
|
+
// Inline code (process first to protect contents from other formatting)
|
|
77
|
+
.replace(/`([^`]+)`/g, (_, t) => chalk.cyan(t))
|
|
78
|
+
// Bold
|
|
79
|
+
.replace(/\*\*([^*]+)\*\*/g, (_, t) => chalk.bold(t))
|
|
80
|
+
// Italic
|
|
81
|
+
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, t) => chalk.italic(t))
|
|
82
|
+
// Strikethrough
|
|
83
|
+
.replace(/~~([^~]+)~~/g, (_, t) => chalk.strikethrough(t))
|
|
84
|
+
// Links — show label in blue, URL dimmed
|
|
85
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => chalk.blue.underline(label) + chalk.dim(` (${url})`));
|
|
86
|
+
}
|
package/package.json
CHANGED