@blockrun/runcode 2.5.8 → 2.5.10

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.
@@ -33,7 +33,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
33
33
  const toolDefs = config.capabilities.map((c) => c.spec);
34
34
  const maxTurns = config.maxTurns ?? 100;
35
35
  const workDir = config.workingDir ?? process.cwd();
36
- const permissions = new PermissionManager(config.permissionMode ?? 'default');
36
+ const permissions = new PermissionManager(config.permissionMode ?? 'default', config.permissionPromptFn);
37
37
  const history = [];
38
38
  let lastUserInput = ''; // For /retry
39
39
  // Session persistence
@@ -17,16 +17,19 @@ export declare class PermissionManager {
17
17
  private rules;
18
18
  private mode;
19
19
  private sessionAllowed;
20
- constructor(mode?: PermissionMode);
20
+ private promptFn?;
21
+ constructor(mode?: PermissionMode, promptFn?: (toolName: string, description: string) => Promise<'yes' | 'no' | 'always'>);
21
22
  /**
22
23
  * Check if a tool can be used. Returns the decision.
23
24
  */
24
25
  check(toolName: string, input: Record<string, unknown>): Promise<PermissionDecision>;
25
26
  /**
26
27
  * Prompt the user interactively for permission.
28
+ * Uses injected promptFn (Ink UI) when available, falls back to readline.
29
+ * pendingCount: how many more operations of this type are waiting (including this one).
27
30
  * Returns true if allowed, false if denied.
28
31
  */
29
- promptUser(toolName: string, input: Record<string, unknown>): Promise<boolean>;
32
+ promptUser(toolName: string, input: Record<string, unknown>, pendingCount?: number): Promise<boolean>;
30
33
  private loadRules;
31
34
  private matchesRule;
32
35
  private getPrimaryInputValue;
@@ -20,9 +20,11 @@ export class PermissionManager {
20
20
  rules;
21
21
  mode;
22
22
  sessionAllowed = new Set(); // "always allow" for this session
23
- constructor(mode = 'default') {
23
+ promptFn;
24
+ constructor(mode = 'default', promptFn) {
24
25
  this.mode = mode;
25
26
  this.rules = this.loadRules();
27
+ this.promptFn = promptFn;
26
28
  }
27
29
  /**
28
30
  * Check if a tool can be used. Returns the decision.
@@ -71,14 +73,34 @@ export class PermissionManager {
71
73
  }
72
74
  /**
73
75
  * Prompt the user interactively for permission.
76
+ * Uses injected promptFn (Ink UI) when available, falls back to readline.
77
+ * pendingCount: how many more operations of this type are waiting (including this one).
74
78
  * Returns true if allowed, false if denied.
75
79
  */
76
- async promptUser(toolName, input) {
80
+ async promptUser(toolName, input, pendingCount = 1) {
77
81
  const description = this.describeAction(toolName, input);
82
+ // Append pending-count hint so user knows to press [a] to skip all
83
+ const hint = pendingCount > 1
84
+ ? `${description}\n │ \x1b[33m${pendingCount} pending — press [a] to allow all\x1b[0m`
85
+ : description;
86
+ // Ink UI path: use injected prompt function to avoid stdin conflict.
87
+ // Ink owns stdin in raw mode; a second readline would get EOF immediately.
88
+ if (this.promptFn) {
89
+ const result = await this.promptFn(toolName, hint);
90
+ if (result === 'always') {
91
+ this.sessionAllowed.add(toolName);
92
+ return true;
93
+ }
94
+ return result === 'yes';
95
+ }
96
+ // Readline fallback (basic terminal / piped mode)
78
97
  console.error('');
79
98
  console.error(chalk.yellow(' ╭─ Permission required ─────────────────'));
80
99
  console.error(chalk.yellow(` │ ${toolName}`));
81
100
  console.error(chalk.dim(` │ ${description}`));
101
+ if (pendingCount > 1) {
102
+ console.error(chalk.yellow(` │ ${pendingCount} pending — press [a] to allow all`));
103
+ }
82
104
  console.error(chalk.yellow(' ╰─────────────────────────────────────'));
83
105
  const answer = await askQuestion(chalk.bold(' Allow? ') + chalk.dim('[Y/n/a]lways: '));
84
106
  const normalized = answer.trim().toLowerCase();
@@ -41,6 +41,15 @@ export class StreamingExecutor {
41
41
  const alreadyStarted = new Set(this.pending.map(p => p.invocation.id));
42
42
  const pendingSnapshot = [...this.pending];
43
43
  this.pending = []; // Clear immediately so errors don't leave stale state
44
+ // Pre-count how many sequential invocations of each tool type are pending.
45
+ // Passed to promptUser so the dialog can show "N pending — press [a] to allow all".
46
+ const pendingCounts = new Map();
47
+ for (const inv of allInvocations) {
48
+ if (!alreadyStarted.has(inv.id)) {
49
+ pendingCounts.set(inv.name, (pendingCounts.get(inv.name) || 0) + 1);
50
+ }
51
+ }
52
+ const remainingCounts = new Map(pendingCounts);
44
53
  try {
45
54
  // Wait for concurrent results that were started during streaming
46
55
  for (const p of pendingSnapshot) {
@@ -51,8 +60,10 @@ export class StreamingExecutor {
51
60
  for (const inv of allInvocations) {
52
61
  if (alreadyStarted.has(inv.id))
53
62
  continue;
63
+ const remaining = remainingCounts.get(inv.name) ?? 1;
64
+ remainingCounts.set(inv.name, remaining - 1);
54
65
  this.onStart(inv.id, inv.name);
55
- const result = await this.executeWithPermissions(inv);
66
+ const result = await this.executeWithPermissions(inv, remaining);
56
67
  results.push([inv, result]);
57
68
  }
58
69
  }
@@ -62,21 +73,21 @@ export class StreamingExecutor {
62
73
  }
63
74
  return results;
64
75
  }
65
- async executeWithPermissions(invocation) {
76
+ async executeWithPermissions(invocation, pendingCount = 1) {
66
77
  // Permission check
67
78
  if (this.permissions) {
68
79
  const decision = await this.permissions.check(invocation.name, invocation.input);
69
80
  if (decision.behavior === 'deny') {
70
81
  return {
71
- output: `Permission denied for ${invocation.name}: ${decision.reason || 'denied by policy'}`,
82
+ output: `Permission denied for ${invocation.name}: ${decision.reason || 'denied by policy'}. Do not retry — explain to the user what you were trying to do and ask how they'd like to proceed.`,
72
83
  isError: true,
73
84
  };
74
85
  }
75
86
  if (decision.behavior === 'ask') {
76
- const allowed = await this.permissions.promptUser(invocation.name, invocation.input);
87
+ const allowed = await this.permissions.promptUser(invocation.name, invocation.input, pendingCount);
77
88
  if (!allowed) {
78
89
  return {
79
- output: `User denied permission for ${invocation.name}`,
90
+ output: `User denied permission for ${invocation.name}. Do not retry — ask the user what they'd like to do instead.`,
80
91
  isError: true,
81
92
  };
82
93
  }
@@ -103,4 +103,10 @@ export interface AgentConfig {
103
103
  debug?: boolean;
104
104
  /** Ultrathink mode: inject deep-reasoning instruction into every prompt */
105
105
  ultrathink?: boolean;
106
+ /**
107
+ * Permission prompt function — injected by Ink UI to avoid stdin conflict.
108
+ * Replaces the readline-based askQuestion() when running in interactive mode.
109
+ * Returns 'yes' | 'no' | 'always' (always = allow for rest of session).
110
+ */
111
+ permissionPromptFn?: (toolName: string, description: string) => Promise<'yes' | 'no' | 'always'>;
106
112
  }
@@ -128,7 +128,7 @@ export async function startCommand(options) {
128
128
  permissionMode: (options.trust || !process.stdin.isTTY) ? 'trust' : 'default',
129
129
  debug: options.debug,
130
130
  };
131
- // Use ink UI if TTY, fallback to basic readline for piped input
131
+ // Use Ink UI if TTY, fallback to basic readline for piped input
132
132
  if (process.stdin.isTTY) {
133
133
  await runWithInkUI(agentConfig, model, workDir, version, walletInfo, (cb) => {
134
134
  onBalanceFetched = cb;
@@ -151,6 +151,10 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
151
151
  agentConfig.model = newModel;
152
152
  },
153
153
  });
154
+ // Wire permission prompts through Ink UI to avoid stdin/readline conflict.
155
+ // Ink owns stdin in raw mode; the old readline-based askQuestion() got EOF
156
+ // immediately and auto-denied every permission. Now y/n/a goes through useInput.
157
+ agentConfig.permissionPromptFn = (toolName, description) => ui.requestPermission(toolName, description);
154
158
  // Wire up background balance fetch to UI
155
159
  onBalanceReady?.((bal) => ui.updateBalance(bal));
156
160
  try {
package/dist/ui/app.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface InkUIHandle {
9
9
  waitForInput: () => Promise<string | null>;
10
10
  onAbort: (cb: () => void) => void;
11
11
  cleanup: () => void;
12
+ requestPermission: (toolName: string, description: string) => Promise<'yes' | 'no' | 'always'>;
12
13
  }
13
14
  export declare function launchInkUI(opts: {
14
15
  model: string;
package/dist/ui/app.js CHANGED
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * Real-time streaming, thinking animation, tool progress, slash commands.
5
5
  */
6
6
  import { useState, useEffect, useCallback } from 'react';
7
- import { render, Box, Text, useApp, useInput, useStdout } from 'ink';
7
+ import { render, Static, Box, Text, useApp, useInput, useStdout } from 'ink';
8
8
  import Spinner from 'ink-spinner';
9
9
  import TextInput from 'ink-text-input';
10
10
  import { resolveModel } from './model-picker.js';
@@ -41,6 +41,8 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
41
41
  const [thinking, setThinking] = useState(false);
42
42
  const [waiting, setWaiting] = useState(false);
43
43
  const [tools, setTools] = useState(new Map());
44
+ // Completed tool results committed to Static (permanent scrollback — no re-render artifacts)
45
+ const [completedTools, setCompletedTools] = useState([]);
44
46
  const [currentModel, setCurrentModel] = useState(initialModel || PICKER_MODELS[0].id);
45
47
  const [ready, setReady] = useState(!startWithPicker);
46
48
  const [mode, setMode] = useState(startWithPicker ? 'model-picker' : 'input');
@@ -55,11 +57,34 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
55
57
  const [lastPrompt, setLastPrompt] = useState('');
56
58
  const [inputHistory, setInputHistory] = useState([]);
57
59
  const [historyIdx, setHistoryIdx] = useState(-1);
60
+ const [permissionRequest, setPermissionRequest] = useState(null);
61
+ // Permission dialog key handler — captures y/n/a when dialog is visible.
62
+ // Must be registered before other handlers so it takes priority.
63
+ useInput((ch, _key) => {
64
+ if (!permissionRequest)
65
+ return;
66
+ const c = ch.toLowerCase();
67
+ if (c === 'y') {
68
+ const r = permissionRequest.resolve;
69
+ setPermissionRequest(null);
70
+ r('yes');
71
+ }
72
+ else if (c === 'n') {
73
+ const r = permissionRequest.resolve;
74
+ setPermissionRequest(null);
75
+ r('no');
76
+ }
77
+ else if (c === 'a') {
78
+ const r = permissionRequest.resolve;
79
+ setPermissionRequest(null);
80
+ r('always');
81
+ }
82
+ }, { isActive: !!permissionRequest });
58
83
  // Key handler for picker + esc + abort
59
84
  const isPickerOrEsc = mode === 'model-picker' || (mode === 'input' && ready && !input) || !ready;
60
85
  useInput((ch, key) => {
61
- // Escape during generation → abort current turn
62
- if (key.escape && !ready) {
86
+ // Escape during generation → abort current turn (skip if permission dialog open)
87
+ if (key.escape && !ready && !permissionRequest) {
63
88
  onAbort();
64
89
  setStatusMsg('Aborted');
65
90
  setReady(true);
@@ -212,6 +237,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
212
237
  setThinking(false);
213
238
  setThinkingText('');
214
239
  setTools(new Map());
240
+ setCompletedTools([]);
215
241
  setReady(false);
216
242
  setWaiting(true);
217
243
  setStatusMsg('');
@@ -220,10 +246,17 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
220
246
  setTurnTokens({ input: 0, output: 0 });
221
247
  onSubmit(trimmed);
222
248
  }, [currentModel, totalCost, onSubmit, onModelChange, onAbort, onExit, exit, lastPrompt, inputHistory]);
223
- // Expose event handler + balance updater
249
+ // Expose event handler, balance updater, and permission bridge
224
250
  useEffect(() => {
225
251
  globalThis.__runcode_ui = {
226
252
  updateBalance: (bal) => setBalance(bal),
253
+ requestPermission: (toolName, description) => {
254
+ return new Promise((resolve) => {
255
+ // Ring the terminal bell — causes tab to show notification badge in iTerm2/Terminal.app
256
+ process.stderr.write('\x07');
257
+ setPermissionRequest({ toolName, description, resolve });
258
+ });
259
+ },
227
260
  handleEvent: (event) => {
228
261
  switch (event.kind) {
229
262
  case 'text_delta':
@@ -251,21 +284,27 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
251
284
  return next;
252
285
  });
253
286
  break;
254
- case 'capability_done':
287
+ case 'capability_done': {
255
288
  setTools(prev => {
256
289
  const next = new Map(prev);
257
290
  const t = next.get(event.id);
258
291
  if (t) {
259
- next.set(event.id, {
260
- ...t, done: true,
292
+ const completed = {
293
+ ...t,
294
+ key: event.id,
295
+ done: true,
261
296
  error: !!event.result.isError,
262
297
  preview: event.result.output.replace(/\n/g, ' ').slice(0, 200),
263
298
  elapsed: Date.now() - t.startTime,
264
- });
299
+ };
300
+ // Move to Static (permanent scrollback) — prevents re-render artifacts
301
+ setCompletedTools(prev2 => [...prev2, completed]);
302
+ next.delete(event.id);
265
303
  }
266
304
  return next;
267
305
  });
268
306
  break;
307
+ }
269
308
  case 'usage':
270
309
  setCurrentModel(event.model);
271
310
  setTurnTokens(prev => ({
@@ -295,9 +334,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
295
334
  }), _jsx(Text, { children: " " })] }));
296
335
  }
297
336
  // ── 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.entries()).map(([id, tool]) => (_jsx(Box, { marginLeft: 1, children: tool.done ? (tool.error
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` : ''; })() })] })) }, 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' }))] }));
337
+ 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 })] })] })), _jsx(Static, { items: completedTools, children: (tool) => (_jsx(Box, { marginLeft: 1, children: tool.error
338
+ ? _jsxs(Text, { color: "red", children: [" \u2717 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms"] })] })
339
+ : _jsxs(Text, { color: "green", children: [" \u2713 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms \u2014 ", tool.preview.slice(0, 120), tool.preview.length > 120 ? '...' : ''] })] }) }, tool.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: [" ", 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: 2, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always allow this session" })] }) })] })), Array.from(tools.entries()).map(([id, tool]) => (_jsx(Box, { marginLeft: 1, children: _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` : ''] }) })), _jsx(InputBox, { input: ready ? input : '', setInput: ready ? setInput : () => { }, onSubmit: ready ? handleSubmit : () => { }, model: currentModel, balance: balance, focused: ready && !permissionRequest })] }));
301
340
  }
302
341
  export function launchInkUI(opts) {
303
342
  let resolveInput = null;
@@ -331,5 +370,9 @@ export function launchInkUI(opts) {
331
370
  },
332
371
  onAbort: (cb) => { abortCallback = cb; },
333
372
  cleanup: () => { instance.unmount(); },
373
+ requestPermission: (toolName, description) => {
374
+ const ui = globalThis.__runcode_ui;
375
+ return ui?.requestPermission(toolName, description) ?? Promise.resolve('no');
376
+ },
334
377
  };
335
378
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/runcode",
3
- "version": "2.5.8",
3
+ "version": "2.5.10",
4
4
  "description": "RunCode — AI coding agent powered by 41+ models. Pay per use with USDC.",
5
5
  "type": "module",
6
6
  "bin": {