@blockrun/runcode 2.5.7 → 2.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/context.d.ts +3 -0
- package/dist/agent/context.js +19 -2
- package/dist/agent/loop.js +1 -1
- package/dist/agent/permissions.d.ts +5 -2
- package/dist/agent/permissions.js +24 -2
- package/dist/agent/streaming-executor.js +16 -5
- package/dist/agent/types.d.ts +6 -0
- package/dist/commands/start.js +5 -1
- package/dist/tools/edit.js +36 -11
- package/dist/tools/read.d.ts +6 -0
- package/dist/tools/read.js +15 -0
- package/dist/tools/webfetch.js +36 -3
- package/dist/ui/app.d.ts +1 -0
- package/dist/ui/app.js +52 -11
- package/package.json +1 -1
package/dist/agent/context.d.ts
CHANGED
|
@@ -4,5 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Build the full system instructions array for a session.
|
|
7
|
+
* Result is memoized per workingDir for the process lifetime.
|
|
7
8
|
*/
|
|
8
9
|
export declare function assembleInstructions(workingDir: string): string[];
|
|
10
|
+
/** Invalidate cache for a workingDir (call after /clear or session reset). */
|
|
11
|
+
export declare function invalidateInstructionCache(workingDir: string): void;
|
package/dist/agent/context.js
CHANGED
|
@@ -53,10 +53,17 @@ The user can type these shortcuts: /commit, /review, /test, /fix, /debug, /expla
|
|
|
53
53
|
/log, /branch, /stash, /plan, /ultraplan, /execute, /compact, /retry, /sessions, /resume,
|
|
54
54
|
/tasks, /context, /doctor, /tokens, /model, /cost, /dump, /ultrathink [query], /clear,
|
|
55
55
|
/help, /exit.`;
|
|
56
|
+
// Cache assembled instructions per workingDir — avoids re-running git commands
|
|
57
|
+
// when sub-agents are spawned (common in parallel tool use patterns).
|
|
58
|
+
const _instructionCache = new Map();
|
|
56
59
|
/**
|
|
57
60
|
* Build the full system instructions array for a session.
|
|
61
|
+
* Result is memoized per workingDir for the process lifetime.
|
|
58
62
|
*/
|
|
59
63
|
export function assembleInstructions(workingDir) {
|
|
64
|
+
const cached = _instructionCache.get(workingDir);
|
|
65
|
+
if (cached)
|
|
66
|
+
return cached;
|
|
60
67
|
const parts = [BASE_INSTRUCTIONS];
|
|
61
68
|
// Read RUNCODE.md or CLAUDE.md from the project
|
|
62
69
|
const projectConfig = readProjectConfig(workingDir);
|
|
@@ -70,8 +77,13 @@ export function assembleInstructions(workingDir) {
|
|
|
70
77
|
if (gitInfo) {
|
|
71
78
|
parts.push(`# Git Context\n\n${gitInfo}`);
|
|
72
79
|
}
|
|
80
|
+
_instructionCache.set(workingDir, parts);
|
|
73
81
|
return parts;
|
|
74
82
|
}
|
|
83
|
+
/** Invalidate cache for a workingDir (call after /clear or session reset). */
|
|
84
|
+
export function invalidateInstructionCache(workingDir) {
|
|
85
|
+
_instructionCache.delete(workingDir);
|
|
86
|
+
}
|
|
75
87
|
// ─── Project Config ────────────────────────────────────────────────────────
|
|
76
88
|
/**
|
|
77
89
|
* Look for RUNCODE.md, then CLAUDE.md in the working directory and parents.
|
|
@@ -114,6 +126,8 @@ function buildEnvironmentSection(workingDir) {
|
|
|
114
126
|
}
|
|
115
127
|
// ─── Git Context ───────────────────────────────────────────────────────────
|
|
116
128
|
const GIT_TIMEOUT_MS = 5_000;
|
|
129
|
+
// Max chars for git log output — long commit messages can bloat the system prompt
|
|
130
|
+
const MAX_GIT_LOG_CHARS = 2_000;
|
|
117
131
|
function getGitContext(workingDir) {
|
|
118
132
|
try {
|
|
119
133
|
const isGit = execSync('git rev-parse --is-inside-work-tree', {
|
|
@@ -154,15 +168,18 @@ function getGitContext(workingDir) {
|
|
|
154
168
|
}
|
|
155
169
|
}
|
|
156
170
|
catch { /* ignore */ }
|
|
157
|
-
// Recent commits (last 5)
|
|
171
|
+
// Recent commits (last 5) — capped to prevent huge messages bloating context
|
|
158
172
|
try {
|
|
159
|
-
|
|
173
|
+
let log = execSync('git log --oneline -5', {
|
|
160
174
|
cwd: workingDir,
|
|
161
175
|
encoding: 'utf-8',
|
|
162
176
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
163
177
|
timeout: GIT_TIMEOUT_MS,
|
|
164
178
|
}).trim();
|
|
165
179
|
if (log) {
|
|
180
|
+
if (log.length > MAX_GIT_LOG_CHARS) {
|
|
181
|
+
log = log.slice(0, MAX_GIT_LOG_CHARS) + '\n... (truncated)';
|
|
182
|
+
}
|
|
166
183
|
lines.push(`\nRecent commits:\n${log}`);
|
|
167
184
|
}
|
|
168
185
|
}
|
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/commands/start.js
CHANGED
|
@@ -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
|
|
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/tools/edit.js
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { partiallyReadFiles } from './read.js';
|
|
7
|
+
/**
|
|
8
|
+
* Normalize curly/smart quotes to straight quotes.
|
|
9
|
+
* Claude Code does this to handle API-sanitized strings and editor paste artifacts.
|
|
10
|
+
*/
|
|
11
|
+
function normalizeQuotes(str) {
|
|
12
|
+
return str
|
|
13
|
+
.replace(/[\u201C\u201D]/g, '"') // " " → "
|
|
14
|
+
.replace(/[\u2018\u2019]/g, "'"); // ' ' → '
|
|
15
|
+
}
|
|
6
16
|
async function execute(input, ctx) {
|
|
7
17
|
const { file_path: filePath, old_string: oldStr, new_string: newStr, replace_all: replaceAll } = input;
|
|
8
18
|
if (!filePath) {
|
|
@@ -18,12 +28,25 @@ async function execute(input, ctx) {
|
|
|
18
28
|
return { output: 'Error: old_string and new_string are identical', isError: true };
|
|
19
29
|
}
|
|
20
30
|
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(ctx.workingDir, filePath);
|
|
31
|
+
// Warn if the file was only partially read — editing without full context risks mistakes
|
|
32
|
+
const isPartial = partiallyReadFiles.has(resolved);
|
|
21
33
|
try {
|
|
22
34
|
if (!fs.existsSync(resolved)) {
|
|
23
35
|
return { output: `Error: file not found: ${resolved}`, isError: true };
|
|
24
36
|
}
|
|
25
37
|
const content = fs.readFileSync(resolved, 'utf-8');
|
|
38
|
+
// Try exact match first, then quote-normalized fallback
|
|
39
|
+
let effectiveOldStr = oldStr;
|
|
26
40
|
if (!content.includes(oldStr)) {
|
|
41
|
+
const normalized = normalizeQuotes(oldStr);
|
|
42
|
+
const contentNormalized = normalizeQuotes(content);
|
|
43
|
+
if (normalized !== oldStr && contentNormalized.includes(normalized)) {
|
|
44
|
+
// Find the original text in content that corresponds to the normalized match
|
|
45
|
+
const idx = contentNormalized.indexOf(normalized);
|
|
46
|
+
effectiveOldStr = content.slice(idx, idx + normalized.length);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!content.includes(effectiveOldStr)) {
|
|
27
50
|
// Find lines containing fragments of old_string for helpful context
|
|
28
51
|
const lines = content.split('\n');
|
|
29
52
|
const searchTerms = oldStr.split('\n').map(l => l.trim()).filter(l => l.length > 3);
|
|
@@ -52,20 +75,17 @@ async function execute(input, ctx) {
|
|
|
52
75
|
let updated;
|
|
53
76
|
let matchCount;
|
|
54
77
|
if (replaceAll) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
updated = content.split(oldStr).join(newStr);
|
|
78
|
+
matchCount = content.split(effectiveOldStr).length - 1;
|
|
79
|
+
updated = content.split(effectiveOldStr).join(newStr);
|
|
58
80
|
}
|
|
59
81
|
else {
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const secondIdx = content.indexOf(oldStr, firstIdx + 1);
|
|
82
|
+
const firstIdx = content.indexOf(effectiveOldStr);
|
|
83
|
+
const secondIdx = content.indexOf(effectiveOldStr, firstIdx + 1);
|
|
63
84
|
if (secondIdx !== -1) {
|
|
64
|
-
// Multiple matches — show where they are
|
|
65
85
|
const positions = [];
|
|
66
86
|
let searchFrom = 0;
|
|
67
87
|
while (true) {
|
|
68
|
-
const idx = content.indexOf(
|
|
88
|
+
const idx = content.indexOf(effectiveOldStr, searchFrom);
|
|
69
89
|
if (idx === -1)
|
|
70
90
|
break;
|
|
71
91
|
const lineNum = content.slice(0, idx).split('\n').length;
|
|
@@ -79,11 +99,13 @@ async function execute(input, ctx) {
|
|
|
79
99
|
};
|
|
80
100
|
}
|
|
81
101
|
matchCount = 1;
|
|
82
|
-
updated = content.slice(0, firstIdx) + newStr + content.slice(firstIdx +
|
|
102
|
+
updated = content.slice(0, firstIdx) + newStr + content.slice(firstIdx + effectiveOldStr.length);
|
|
83
103
|
}
|
|
84
104
|
fs.writeFileSync(resolved, updated, 'utf-8');
|
|
105
|
+
// File has been modified — remove from partial-read tracking so next read is fresh
|
|
106
|
+
partiallyReadFiles.delete(resolved);
|
|
85
107
|
// Build a concise diff preview
|
|
86
|
-
const oldLines =
|
|
108
|
+
const oldLines = effectiveOldStr.split('\n');
|
|
87
109
|
const newLines = newStr.split('\n');
|
|
88
110
|
let diffPreview = '';
|
|
89
111
|
if (oldLines.length <= 5 && newLines.length <= 5) {
|
|
@@ -94,8 +116,11 @@ async function execute(input, ctx) {
|
|
|
94
116
|
else {
|
|
95
117
|
diffPreview = ` (${oldLines.length} lines → ${newLines.length} lines)`;
|
|
96
118
|
}
|
|
119
|
+
const partialWarning = isPartial
|
|
120
|
+
? '\nNote: file was only partially read before this edit.'
|
|
121
|
+
: '';
|
|
97
122
|
return {
|
|
98
|
-
output: `Updated ${resolved} — ${matchCount} replacement${matchCount > 1 ? 's' : ''} made.${diffPreview}`,
|
|
123
|
+
output: `Updated ${resolved} — ${matchCount} replacement${matchCount > 1 ? 's' : ''} made.${diffPreview}${partialWarning}`,
|
|
99
124
|
};
|
|
100
125
|
}
|
|
101
126
|
catch (err) {
|
package/dist/tools/read.d.ts
CHANGED
|
@@ -2,4 +2,10 @@
|
|
|
2
2
|
* Read capability — reads files with line numbers.
|
|
3
3
|
*/
|
|
4
4
|
import type { CapabilityHandler } from '../agent/types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Tracks files that were only partially read (offset or limit applied).
|
|
7
|
+
* Edit tool uses this to warn when editing without full context.
|
|
8
|
+
* Exported so edit.ts can check and clear entries.
|
|
9
|
+
*/
|
|
10
|
+
export declare const partiallyReadFiles: Set<string>;
|
|
5
11
|
export declare const readCapability: CapabilityHandler;
|
package/dist/tools/read.js
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* Tracks files that were only partially read (offset or limit applied).
|
|
8
|
+
* Edit tool uses this to warn when editing without full context.
|
|
9
|
+
* Exported so edit.ts can check and clear entries.
|
|
10
|
+
*/
|
|
11
|
+
export const partiallyReadFiles = new Set();
|
|
6
12
|
async function execute(input, ctx) {
|
|
7
13
|
const { file_path: filePath, offset, limit } = input;
|
|
8
14
|
if (!filePath) {
|
|
@@ -37,6 +43,15 @@ async function execute(input, ctx) {
|
|
|
37
43
|
const maxLines = limit ?? 2000;
|
|
38
44
|
const endLine = Math.min(allLines.length, startLine + maxLines);
|
|
39
45
|
const slice = allLines.slice(startLine, endLine);
|
|
46
|
+
// Track partial reads — file was not read from the beginning or was truncated
|
|
47
|
+
const isPartial = startLine > 0 || endLine < allLines.length;
|
|
48
|
+
if (isPartial) {
|
|
49
|
+
partiallyReadFiles.add(resolved);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Full read — clear any stale partial flag
|
|
53
|
+
partiallyReadFiles.delete(resolved);
|
|
54
|
+
}
|
|
40
55
|
// Format with line numbers (cat -n style)
|
|
41
56
|
const numbered = slice.map((line, i) => `${startLine + i + 1}\t${line}`);
|
|
42
57
|
let result = numbered.join('\n');
|
package/dist/tools/webfetch.js
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { VERSION } from '../config.js';
|
|
5
5
|
const MAX_BODY_BYTES = 256 * 1024; // 256KB
|
|
6
|
+
// ─── Session cache ──────────────────────────────────────────────────────────
|
|
7
|
+
// Avoids re-fetching the same URL within a session (common in research tasks).
|
|
8
|
+
// 15-min TTL, max 50 entries.
|
|
9
|
+
const CACHE_TTL_MS = 15 * 60 * 1000;
|
|
10
|
+
const MAX_CACHE_ENTRIES = 50;
|
|
11
|
+
const fetchCache = new Map();
|
|
12
|
+
function getCached(url) {
|
|
13
|
+
const entry = fetchCache.get(url);
|
|
14
|
+
if (!entry)
|
|
15
|
+
return null;
|
|
16
|
+
if (Date.now() > entry.expiresAt) {
|
|
17
|
+
fetchCache.delete(url);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return entry.output;
|
|
21
|
+
}
|
|
22
|
+
function setCached(url, output) {
|
|
23
|
+
// Evict oldest entry if at capacity
|
|
24
|
+
if (fetchCache.size >= MAX_CACHE_ENTRIES) {
|
|
25
|
+
const firstKey = fetchCache.keys().next().value;
|
|
26
|
+
if (firstKey)
|
|
27
|
+
fetchCache.delete(firstKey);
|
|
28
|
+
}
|
|
29
|
+
fetchCache.set(url, { output, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
30
|
+
}
|
|
31
|
+
// ─── Execute ────────────────────────────────────────────────────────────────
|
|
6
32
|
async function execute(input, _ctx) {
|
|
7
33
|
const { url, max_length } = input;
|
|
8
34
|
if (!url) {
|
|
@@ -19,6 +45,11 @@ async function execute(input, _ctx) {
|
|
|
19
45
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
20
46
|
return { output: `Error: only http/https URLs are supported`, isError: true };
|
|
21
47
|
}
|
|
48
|
+
// Check cache first
|
|
49
|
+
const cached = getCached(url);
|
|
50
|
+
if (cached) {
|
|
51
|
+
return { output: cached + '\n\n(cached)' };
|
|
52
|
+
}
|
|
22
53
|
const controller = new AbortController();
|
|
23
54
|
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
24
55
|
try {
|
|
@@ -62,8 +93,8 @@ async function execute(input, _ctx) {
|
|
|
62
93
|
// Format response based on content type
|
|
63
94
|
if (contentType.includes('json')) {
|
|
64
95
|
try {
|
|
65
|
-
const
|
|
66
|
-
body = JSON.stringify(
|
|
96
|
+
const parsedJson = JSON.parse(body);
|
|
97
|
+
body = JSON.stringify(parsedJson, null, 2).slice(0, maxLen);
|
|
67
98
|
}
|
|
68
99
|
catch { /* leave as-is if not valid JSON */ }
|
|
69
100
|
}
|
|
@@ -74,6 +105,8 @@ async function execute(input, _ctx) {
|
|
|
74
105
|
if (totalBytes >= maxLen) {
|
|
75
106
|
output += '\n\n... (content truncated)';
|
|
76
107
|
}
|
|
108
|
+
// Cache successful responses
|
|
109
|
+
setCached(url, output);
|
|
77
110
|
return { output };
|
|
78
111
|
}
|
|
79
112
|
catch (err) {
|
|
@@ -118,7 +151,7 @@ function stripHtml(html) {
|
|
|
118
151
|
export const webFetchCapability = {
|
|
119
152
|
spec: {
|
|
120
153
|
name: 'WebFetch',
|
|
121
|
-
description: 'Fetch a web page and return its content. HTML tags are stripped for readability.',
|
|
154
|
+
description: 'Fetch a web page and return its content. HTML tags are stripped for readability. Results are cached for 15 minutes.',
|
|
122
155
|
input_schema: {
|
|
123
156
|
type: 'object',
|
|
124
157
|
properties: {
|
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,15 @@ 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
|
|
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
|
+
setPermissionRequest({ toolName, description, resolve });
|
|
256
|
+
});
|
|
257
|
+
},
|
|
227
258
|
handleEvent: (event) => {
|
|
228
259
|
switch (event.kind) {
|
|
229
260
|
case 'text_delta':
|
|
@@ -251,21 +282,27 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
251
282
|
return next;
|
|
252
283
|
});
|
|
253
284
|
break;
|
|
254
|
-
case 'capability_done':
|
|
285
|
+
case 'capability_done': {
|
|
255
286
|
setTools(prev => {
|
|
256
287
|
const next = new Map(prev);
|
|
257
288
|
const t = next.get(event.id);
|
|
258
289
|
if (t) {
|
|
259
|
-
|
|
260
|
-
...t,
|
|
290
|
+
const completed = {
|
|
291
|
+
...t,
|
|
292
|
+
key: event.id,
|
|
293
|
+
done: true,
|
|
261
294
|
error: !!event.result.isError,
|
|
262
295
|
preview: event.result.output.replace(/\n/g, ' ').slice(0, 200),
|
|
263
296
|
elapsed: Date.now() - t.startTime,
|
|
264
|
-
}
|
|
297
|
+
};
|
|
298
|
+
// Move to Static (permanent scrollback) — prevents re-render artifacts
|
|
299
|
+
setCompletedTools(prev2 => [...prev2, completed]);
|
|
300
|
+
next.delete(event.id);
|
|
265
301
|
}
|
|
266
302
|
return next;
|
|
267
303
|
});
|
|
268
304
|
break;
|
|
305
|
+
}
|
|
269
306
|
case 'usage':
|
|
270
307
|
setCurrentModel(event.model);
|
|
271
308
|
setTurnTokens(prev => ({
|
|
@@ -295,9 +332,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
295
332
|
}), _jsx(Text, { children: " " })] }));
|
|
296
333
|
}
|
|
297
334
|
// ── 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 })] })] })),
|
|
299
|
-
|
|
300
|
-
|
|
335
|
+
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
|
|
336
|
+
? _jsxs(Text, { color: "red", children: [" \u2717 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms"] })] })
|
|
337
|
+
: _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
338
|
}
|
|
302
339
|
export function launchInkUI(opts) {
|
|
303
340
|
let resolveInput = null;
|
|
@@ -331,5 +368,9 @@ export function launchInkUI(opts) {
|
|
|
331
368
|
},
|
|
332
369
|
onAbort: (cb) => { abortCallback = cb; },
|
|
333
370
|
cleanup: () => { instance.unmount(); },
|
|
371
|
+
requestPermission: (toolName, description) => {
|
|
372
|
+
const ui = globalThis.__runcode_ui;
|
|
373
|
+
return ui?.requestPermission(toolName, description) ?? Promise.resolve('no');
|
|
374
|
+
},
|
|
334
375
|
};
|
|
335
376
|
}
|