@blockrun/runcode 2.5.3 → 2.5.5

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.
@@ -295,17 +295,18 @@ const DIRECT_COMMANDS = {
295
295
  try {
296
296
  let address;
297
297
  let balance;
298
+ const fetchTimeout = (ms) => new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms));
298
299
  if (chain === 'solana') {
299
300
  const { getOrCreateSolanaWallet, setupAgentSolanaWallet } = await import('@blockrun/llm');
300
301
  const w = await getOrCreateSolanaWallet();
301
302
  address = w.address;
302
303
  try {
303
304
  const client = await setupAgentSolanaWallet({ silent: true });
304
- const bal = await client.getBalance();
305
+ const bal = await Promise.race([client.getBalance(), fetchTimeout(5000)]);
305
306
  balance = `$${bal.toFixed(2)} USDC`;
306
307
  }
307
308
  catch {
308
- balance = '(fetch failed)';
309
+ balance = '(unavailable)';
309
310
  }
310
311
  }
311
312
  else {
@@ -314,11 +315,11 @@ const DIRECT_COMMANDS = {
314
315
  address = w.address;
315
316
  try {
316
317
  const client = setupAgentWallet({ silent: true });
317
- const bal = await client.getBalance();
318
+ const bal = await Promise.race([client.getBalance(), fetchTimeout(5000)]);
318
319
  balance = `$${bal.toFixed(2)} USDC`;
319
320
  }
320
321
  catch {
321
- balance = '(fetch failed)';
322
+ balance = '(unavailable)';
322
323
  }
323
324
  }
324
325
  ctx.onEvent({ kind: 'text_delta', text: `**Wallet**\n` +
@@ -345,12 +346,16 @@ const DIRECT_COMMANDS = {
345
346
  ctx.history.length = 0;
346
347
  ctx.history.push(...compacted);
347
348
  resetTokenAnchor();
349
+ const afterTokens = estimateHistoryTokens(ctx.history);
350
+ const saved = beforeTokens - afterTokens;
351
+ const pct = Math.round((saved / beforeTokens) * 100);
352
+ ctx.onEvent({ kind: 'text_delta', text: `Compacted: ~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens (saved ${pct}%)\n`
353
+ });
354
+ }
355
+ else {
356
+ ctx.onEvent({ kind: 'text_delta', text: `Nothing to compact — history is already minimal (${beforeTokens.toLocaleString()} tokens, ${ctx.history.length} messages).\n`
357
+ });
348
358
  }
349
- const afterTokens = estimateHistoryTokens(ctx.history);
350
- ctx.onEvent({ kind: 'text_delta', text: didCompact
351
- ? `Compacted: ~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens\n`
352
- : `History too short to compact (${beforeTokens.toLocaleString()} tokens, ${ctx.history.length} messages).\n`
353
- });
354
359
  emitDone(ctx);
355
360
  },
356
361
  };
@@ -38,8 +38,16 @@ export async function autoCompactIfNeeded(history, model, client, debug) {
38
38
  if (debug) {
39
39
  console.error(`[runcode] Auto-compacting: ~${currentTokens} tokens, threshold=${threshold}`);
40
40
  }
41
+ const beforeTokens = estimateHistoryTokens(history);
41
42
  try {
42
43
  const compacted = await compactHistory(history, model, client, debug);
44
+ const afterTokens = estimateHistoryTokens(compacted);
45
+ if (afterTokens >= beforeTokens) {
46
+ if (debug) {
47
+ console.error(`[runcode] Auto-compaction grew history (${beforeTokens} → ${afterTokens}) — skipping`);
48
+ }
49
+ return { history, compacted: false };
50
+ }
43
51
  return { history: compacted, compacted: true };
44
52
  }
45
53
  catch (err) {
@@ -58,8 +66,17 @@ export async function forceCompact(history, model, client, debug) {
58
66
  if (history.length <= 4) {
59
67
  return { history, compacted: false };
60
68
  }
69
+ const beforeTokens = estimateHistoryTokens(history);
61
70
  try {
62
71
  const compacted = await compactHistory(history, model, client, debug);
72
+ const afterTokens = estimateHistoryTokens(compacted);
73
+ // Only accept compaction if it actually reduces tokens
74
+ if (afterTokens >= beforeTokens) {
75
+ if (debug) {
76
+ console.error(`[runcode] Compaction produced larger history (${beforeTokens} → ${afterTokens}) — reverting`);
77
+ }
78
+ return { history, compacted: false };
79
+ }
63
80
  return { history: compacted, compacted: true };
64
81
  }
65
82
  catch (err) {
@@ -9,6 +9,14 @@ async function execute(input, _ctx) {
9
9
  if (!question) {
10
10
  return { output: 'Error: question is required', isError: true };
11
11
  }
12
+ // In non-TTY (piped/scripted) mode, creating a new readline would conflict with
13
+ // the TerminalUI's existing readline. Return a hint for the model to proceed.
14
+ if (!process.stdin.isTTY) {
15
+ return {
16
+ output: `[Non-interactive mode] Cannot prompt user. Proceed with a reasonable assumption. Question was: ${question}`,
17
+ isError: false,
18
+ };
19
+ }
12
20
  console.error('');
13
21
  console.error(chalk.yellow(' ╭─ Question ────────────────────────────'));
14
22
  console.error(chalk.yellow(` │ ${question}`));
@@ -21,7 +29,7 @@ async function execute(input, _ctx) {
21
29
  const rl = readline.createInterface({
22
30
  input: process.stdin,
23
31
  output: process.stderr,
24
- terminal: process.stdin.isTTY ?? false,
32
+ terminal: true,
25
33
  });
26
34
  return new Promise((resolve) => {
27
35
  let answered = false;
@@ -32,7 +40,7 @@ async function execute(input, _ctx) {
32
40
  });
33
41
  rl.on('close', () => {
34
42
  if (!answered)
35
- resolve({ output: 'User did not respond (EOF/piped input).', isError: true });
43
+ resolve({ output: 'User closed input without responding.', isError: false });
36
44
  });
37
45
  });
38
46
  }
@@ -2,7 +2,8 @@
2
2
  * Bash capability — execute shell commands with timeout and output capture.
3
3
  */
4
4
  import { spawn } from 'node:child_process';
5
- const MAX_OUTPUT_BYTES = 512 * 1024; // 512KB output cap
5
+ const MAX_OUTPUT_BYTES = 512 * 1024; // 512KB capture buffer (prevents OOM)
6
+ const MAX_RETURN_CHARS = 32_000; // 32KB return cap (~8,000 tokens) — prevents context bloat
6
7
  const DEFAULT_TIMEOUT_MS = 120_000; // 2 minutes
7
8
  async function execute(input, ctx) {
8
9
  const { command, timeout } = input;
@@ -99,7 +100,21 @@ async function execute(input, ctx) {
99
100
  result += stderr;
100
101
  }
101
102
  if (truncated) {
102
- result += '\n\n... (output truncated at 512KB)';
103
+ result += '\n\n... (output truncated command produced >512KB)';
104
+ }
105
+ // Cap returned output to prevent context bloat.
106
+ // Keep the LAST part (most relevant for errors/test failures/build output).
107
+ if (result.length > MAX_RETURN_CHARS) {
108
+ const lines = result.split('\n');
109
+ let trimmed = '';
110
+ for (let i = lines.length - 1; i >= 0; i--) {
111
+ const candidate = lines[i] + '\n' + trimmed;
112
+ if (candidate.length > MAX_RETURN_CHARS)
113
+ break;
114
+ trimmed = candidate;
115
+ }
116
+ const omitted = result.length - trimmed.length;
117
+ result = `... (${omitted.toLocaleString()} chars omitted from start)\n${trimmed}`;
103
118
  }
104
119
  if (killed) {
105
120
  resolve({
@@ -3,7 +3,8 @@
3
3
  */
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
- const MAX_RESULTS = 500;
6
+ const MAX_RESULTS = 200;
7
+ const MAX_OUTPUT_CHARS = 12_000; // ~3,000 tokens — prevents huge glob results from blowing up context
7
8
  /**
8
9
  * Simple glob matcher supporting *, **, and ? wildcards.
9
10
  * No external dependencies.
@@ -115,7 +116,23 @@ async function execute(input, ctx) {
115
116
  }
116
117
  let output = sorted.join('\n');
117
118
  if (sorted.length >= MAX_RESULTS) {
118
- output += `\n\n... (limited to ${MAX_RESULTS} results)`;
119
+ output += `\n\n... (limited to ${MAX_RESULTS} results. Use a more specific pattern to narrow results.)`;
120
+ }
121
+ // Cap total output length to prevent context bloat
122
+ if (output.length > MAX_OUTPUT_CHARS) {
123
+ const lines = output.split('\n');
124
+ let trimmed = '';
125
+ let count = 0;
126
+ for (const line of lines) {
127
+ if ((trimmed + line).length > MAX_OUTPUT_CHARS)
128
+ break;
129
+ trimmed += (trimmed ? '\n' : '') + line;
130
+ count++;
131
+ }
132
+ const remaining = lines.length - count;
133
+ if (remaining > 0) {
134
+ output = `${trimmed}\n... (${remaining} more paths not shown — use a more specific pattern)`;
135
+ }
119
136
  }
120
137
  return { output };
121
138
  }
@@ -4,6 +4,7 @@
4
4
  import { execSync, execFileSync } from 'node:child_process';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
+ const MAX_GREP_OUTPUT_CHARS = 16_000; // ~4,000 tokens — prevents huge grep results
7
8
  let _hasRipgrep = null;
8
9
  function hasRipgrep() {
9
10
  if (_hasRipgrep !== null)
@@ -79,6 +80,10 @@ function runRipgrep(opts, searchPath, mode, limit) {
79
80
  if (lines.length > limited.length) {
80
81
  output += `\n\n... (${lines.length - limited.length} more results, use head_limit to see more)`;
81
82
  }
83
+ // Cap total output to prevent context bloat
84
+ if (output.length > MAX_GREP_OUTPUT_CHARS) {
85
+ output = output.slice(0, MAX_GREP_OUTPUT_CHARS) + `\n... (output capped at ${MAX_GREP_OUTPUT_CHARS / 1000}KB — use more specific pattern or head_limit)`;
86
+ }
82
87
  return { output: output || 'No matches found' };
83
88
  }
84
89
  catch (err) {
@@ -127,6 +132,9 @@ function runNativeGrep(opts, searchPath, mode, limit) {
127
132
  if (lines.length > limited.length) {
128
133
  output += `\n\n... (${lines.length - limited.length} more results)`;
129
134
  }
135
+ if (output.length > MAX_GREP_OUTPUT_CHARS) {
136
+ output = output.slice(0, MAX_GREP_OUTPUT_CHARS) + `\n... (output capped at ${MAX_GREP_OUTPUT_CHARS / 1000}KB)`;
137
+ }
130
138
  return { output: output || 'No matches found' };
131
139
  }
132
140
  catch (err) {
package/dist/ui/app.js CHANGED
@@ -295,9 +295,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
295
295
  }), _jsx(Text, { children: " " })] }));
296
296
  }
297
297
  // ── Normal Mode ──
298
- return (_jsxs(Box, { flexDirection: "column", children: [statusMsg && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "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: "/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 display"] }), _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 })] })] })), Array.from(tools.values()).map((tool, i) => (_jsx(Box, { marginLeft: 1, children: tool.done ? (tool.error
298
+ return (_jsxs(Box, { flexDirection: "column", children: [statusMsg && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "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: "/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 display"] }), _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 })] })] })), Array.from(tools.entries()).map(([id, tool]) => (_jsx(Box, { marginLeft: 1, children: tool.done ? (tool.error
299
299
  ? _jsxs(Text, { color: "red", children: [" \u2717 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms"] })] })
300
- : _jsxs(Text, { color: "green", children: [" \u2713 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms \u2014 ", tool.preview.slice(0, 200), tool.preview.length > 200 ? '...' : ''] })] })) : (_jsxs(Text, { color: "cyan", children: [" ", _jsx(Spinner, { type: "dots" }), " ", tool.name, "... ", _jsx(Text, { dimColor: true, children: (() => { const s = Math.round((Date.now() - tool.startTime) / 1000); return s > 0 ? `${s}s` : ''; })() })] })) }, i))), 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, { children: streamText }) })), ready && (turnTokens.input > 0 || turnTokens.output > 0) && streamText && (_jsx(Box, { marginLeft: 1, marginTop: 0, children: _jsxs(Text, { dimColor: true, children: [turnTokens.input.toLocaleString(), " in / ", turnTokens.output.toLocaleString(), " out", totalCost > 0 ? ` · $${totalCost.toFixed(4)} session` : ''] }) })), ready && (_jsx(InputBox, { input: input, setInput: setInput, onSubmit: handleSubmit, model: currentModel, balance: balance, focused: mode === 'input' }))] }));
300
+ : _jsxs(Text, { color: "green", children: [" \u2713 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms \u2014 ", tool.preview.slice(0, 200), tool.preview.length > 200 ? '...' : ''] })] })) : (_jsxs(Text, { color: "cyan", children: [" ", _jsx(Spinner, { type: "dots" }), " ", tool.name, "... ", _jsx(Text, { dimColor: true, children: (() => { const s = Math.round((Date.now() - tool.startTime) / 1000); return s > 0 ? `${s}s` : ''; })() })] })) }, 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, { children: streamText }) })), ready && (turnTokens.input > 0 || turnTokens.output > 0) && streamText && (_jsx(Box, { marginLeft: 1, marginTop: 0, children: _jsxs(Text, { dimColor: true, children: [turnTokens.input.toLocaleString(), " in / ", turnTokens.output.toLocaleString(), " out", totalCost > 0 ? ` · $${totalCost.toFixed(4)} session` : ''] }) })), ready && (_jsx(InputBox, { input: input, setInput: setInput, onSubmit: handleSubmit, model: currentModel, balance: balance, focused: mode === 'input' }))] }));
301
301
  }
302
302
  export function launchInkUI(opts) {
303
303
  let resolveInput = null;
@@ -9,6 +9,7 @@ export declare class TerminalUI {
9
9
  private activeCapabilities;
10
10
  private totalInputTokens;
11
11
  private totalOutputTokens;
12
+ private sessionModel;
12
13
  private mdRenderer;
13
14
  private lineQueue;
14
15
  private lineWaiters;
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import readline from 'node:readline';
7
7
  import chalk from 'chalk';
8
+ import { estimateCost } from '../pricing.js';
8
9
  // ─── Spinner ───────────────────────────────────────────────────────────────
9
10
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
10
11
  class Spinner {
@@ -130,6 +131,7 @@ export class TerminalUI {
130
131
  activeCapabilities = new Map();
131
132
  totalInputTokens = 0;
132
133
  totalOutputTokens = 0;
134
+ sessionModel = '';
133
135
  mdRenderer = new MarkdownRenderer();
134
136
  // Line queue for piped (non-TTY) input — buffers all stdin lines eagerly
135
137
  lineQueue = [];
@@ -269,6 +271,8 @@ export class TerminalUI {
269
271
  case 'usage':
270
272
  this.totalInputTokens += event.inputTokens;
271
273
  this.totalOutputTokens += event.outputTokens;
274
+ if (event.model)
275
+ this.sessionModel = event.model;
272
276
  break;
273
277
  case 'turn_done': {
274
278
  this.spinner.stop();
@@ -295,9 +299,14 @@ export class TerminalUI {
295
299
  const cmd = parts[0].toLowerCase();
296
300
  switch (cmd) {
297
301
  case '/cost':
298
- case '/usage':
299
- console.error(chalk.dim(`\n Tokens: ${this.totalInputTokens.toLocaleString()} in / ${this.totalOutputTokens.toLocaleString()} out\n`));
302
+ case '/usage': {
303
+ const cost = this.sessionModel
304
+ ? estimateCost(this.sessionModel, this.totalInputTokens, this.totalOutputTokens)
305
+ : 0;
306
+ const costStr = cost > 0 ? ` · $${cost.toFixed(4)} USDC` : '';
307
+ console.error(chalk.dim(`\n Tokens: ${this.totalInputTokens.toLocaleString()} in / ${this.totalOutputTokens.toLocaleString()} out${costStr}\n`));
300
308
  return true;
309
+ }
301
310
  default:
302
311
  // All other slash commands pass through to the agent loop (commands.ts handles them)
303
312
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/runcode",
3
- "version": "2.5.3",
3
+ "version": "2.5.5",
4
4
  "description": "RunCode — AI coding agent powered by 41+ models. Pay per use with USDC.",
5
5
  "type": "module",
6
6
  "bin": {