@blockrun/runcode 2.5.2 → 2.5.4
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/commands.js +117 -14
- package/dist/agent/compact.js +17 -0
- package/dist/agent/loop.d.ts +0 -5
- package/dist/agent/loop.js +20 -183
- package/dist/tools/askuser.js +10 -2
- package/dist/ui/app.js +6 -4
- package/dist/ui/terminal.d.ts +1 -0
- package/dist/ui/terminal.js +14 -3
- package/package.json +1 -1
package/dist/agent/commands.js
CHANGED
|
@@ -12,6 +12,8 @@ import { execSync } from 'node:child_process';
|
|
|
12
12
|
import { BLOCKRUN_DIR, VERSION } from '../config.js';
|
|
13
13
|
import { estimateHistoryTokens, getAnchoredTokenCount, getContextWindow, resetTokenAnchor } from './tokens.js';
|
|
14
14
|
import { forceCompact } from './compact.js';
|
|
15
|
+
import { getStatsSummary } from '../stats/tracker.js';
|
|
16
|
+
import { resolveModel } from '../ui/model-picker.js';
|
|
15
17
|
import { listSessions, loadSessionHistory, } from '../session/storage.js';
|
|
16
18
|
// ─── Git helpers ──────────────────────────────────────────────────────────
|
|
17
19
|
function gitExec(cmd, cwd, timeout = 5000, maxBuffer) {
|
|
@@ -28,7 +30,12 @@ function gitCmd(ctx, cmd, timeout, maxBuffer) {
|
|
|
28
30
|
return gitExec(cmd, ctx.config.workingDir || process.cwd(), timeout, maxBuffer);
|
|
29
31
|
}
|
|
30
32
|
catch (e) {
|
|
31
|
-
|
|
33
|
+
// Prefer stderr (actual git error message) over the noisy "Command failed: ..." header
|
|
34
|
+
const errObj = e;
|
|
35
|
+
const stderr = errObj.stderr ? String(errObj.stderr).trim() : '';
|
|
36
|
+
// Take only the first meaningful line (git sometimes dumps full usage on errors)
|
|
37
|
+
const firstLine = (stderr || errObj.message || 'unknown').split('\n')[0].trim();
|
|
38
|
+
ctx.onEvent({ kind: 'text_delta', text: `Git: ${firstLine}\n` });
|
|
32
39
|
return null;
|
|
33
40
|
}
|
|
34
41
|
}
|
|
@@ -41,34 +48,51 @@ const DIRECT_COMMANDS = {
|
|
|
41
48
|
'/stash': (ctx) => {
|
|
42
49
|
const r = gitCmd(ctx, 'git stash push -m "runcode auto-stash"', 10000);
|
|
43
50
|
if (r !== null)
|
|
44
|
-
ctx.onEvent({ kind: 'text_delta', text: r
|
|
51
|
+
ctx.onEvent({ kind: 'text_delta', text: r ? `${r}\n` : 'No changes to stash.\n' });
|
|
45
52
|
emitDone(ctx);
|
|
46
53
|
},
|
|
47
54
|
'/unstash': (ctx) => {
|
|
48
55
|
const r = gitCmd(ctx, 'git stash pop', 10000);
|
|
49
56
|
if (r !== null)
|
|
50
|
-
ctx.onEvent({ kind: 'text_delta', text: r
|
|
57
|
+
ctx.onEvent({ kind: 'text_delta', text: r ? `${r}\n` : 'Stash applied.\n' });
|
|
51
58
|
emitDone(ctx);
|
|
52
59
|
},
|
|
53
60
|
'/log': (ctx) => {
|
|
54
61
|
const r = gitCmd(ctx, 'git log --oneline -15 --no-color');
|
|
55
|
-
|
|
62
|
+
if (r !== null)
|
|
63
|
+
ctx.onEvent({ kind: 'text_delta', text: r ? `\`\`\`\n${r}\n\`\`\`\n` : 'No commits yet.\n' });
|
|
56
64
|
emitDone(ctx);
|
|
57
65
|
},
|
|
58
66
|
'/status': (ctx) => {
|
|
59
67
|
const r = gitCmd(ctx, 'git status --short --branch');
|
|
60
|
-
|
|
68
|
+
if (r !== null)
|
|
69
|
+
ctx.onEvent({ kind: 'text_delta', text: r ? `\`\`\`\n${r}\n\`\`\`\n` : 'Working tree clean.\n' });
|
|
61
70
|
emitDone(ctx);
|
|
62
71
|
},
|
|
63
72
|
'/diff': (ctx) => {
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
// git diff with stat header then full diff
|
|
74
|
+
const stat = gitCmd(ctx, 'git diff --stat --no-color');
|
|
75
|
+
if (stat === null) {
|
|
76
|
+
emitDone(ctx);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const full = gitCmd(ctx, 'git diff --no-color');
|
|
80
|
+
if (full === null) {
|
|
81
|
+
emitDone(ctx);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!stat && !full) {
|
|
85
|
+
ctx.onEvent({ kind: 'text_delta', text: 'No unstaged changes.\n' });
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
ctx.onEvent({ kind: 'text_delta', text: `\`\`\`diff\n${[stat, full].filter(Boolean).join('\n---\n')}\n\`\`\`\n` });
|
|
89
|
+
}
|
|
66
90
|
emitDone(ctx);
|
|
67
91
|
},
|
|
68
92
|
'/undo': (ctx) => {
|
|
69
93
|
const r = gitCmd(ctx, 'git reset --soft HEAD~1');
|
|
70
94
|
if (r !== null)
|
|
71
|
-
ctx.onEvent({ kind: 'text_delta', text:
|
|
95
|
+
ctx.onEvent({ kind: 'text_delta', text: `Last commit undone. Changes preserved in staging.\n` });
|
|
72
96
|
emitDone(ctx);
|
|
73
97
|
},
|
|
74
98
|
'/tokens': (ctx) => {
|
|
@@ -256,6 +280,65 @@ const DIRECT_COMMANDS = {
|
|
|
256
280
|
}
|
|
257
281
|
emitDone(ctx);
|
|
258
282
|
},
|
|
283
|
+
'/cost': async (ctx) => {
|
|
284
|
+
const { stats, saved } = getStatsSummary();
|
|
285
|
+
ctx.onEvent({ kind: 'text_delta', text: `**Session Cost**\n` +
|
|
286
|
+
` Requests: ${stats.totalRequests}\n` +
|
|
287
|
+
` Cost: $${stats.totalCostUsd.toFixed(4)} USDC\n` +
|
|
288
|
+
` Saved: $${saved.toFixed(2)} vs Claude Opus\n` +
|
|
289
|
+
` Tokens: ${stats.totalInputTokens.toLocaleString()} in / ${stats.totalOutputTokens.toLocaleString()} out\n`
|
|
290
|
+
});
|
|
291
|
+
emitDone(ctx);
|
|
292
|
+
},
|
|
293
|
+
'/wallet': async (ctx) => {
|
|
294
|
+
const chain = (await import('../config.js')).loadChain();
|
|
295
|
+
try {
|
|
296
|
+
let address;
|
|
297
|
+
let balance;
|
|
298
|
+
const fetchTimeout = (ms) => new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms));
|
|
299
|
+
if (chain === 'solana') {
|
|
300
|
+
const { getOrCreateSolanaWallet, setupAgentSolanaWallet } = await import('@blockrun/llm');
|
|
301
|
+
const w = await getOrCreateSolanaWallet();
|
|
302
|
+
address = w.address;
|
|
303
|
+
try {
|
|
304
|
+
const client = await setupAgentSolanaWallet({ silent: true });
|
|
305
|
+
const bal = await Promise.race([client.getBalance(), fetchTimeout(5000)]);
|
|
306
|
+
balance = `$${bal.toFixed(2)} USDC`;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
balance = '(unavailable)';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
const { getOrCreateWallet, setupAgentWallet } = await import('@blockrun/llm');
|
|
314
|
+
const w = getOrCreateWallet();
|
|
315
|
+
address = w.address;
|
|
316
|
+
try {
|
|
317
|
+
const client = setupAgentWallet({ silent: true });
|
|
318
|
+
const bal = await Promise.race([client.getBalance(), fetchTimeout(5000)]);
|
|
319
|
+
balance = `$${bal.toFixed(2)} USDC`;
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
balance = '(unavailable)';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
ctx.onEvent({ kind: 'text_delta', text: `**Wallet**\n` +
|
|
326
|
+
` Chain: ${chain}\n` +
|
|
327
|
+
` Address: ${address}\n` +
|
|
328
|
+
` Balance: ${balance}\n`
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
ctx.onEvent({ kind: 'text_delta', text: `Wallet error: ${err.message}\n` });
|
|
333
|
+
}
|
|
334
|
+
emitDone(ctx);
|
|
335
|
+
},
|
|
336
|
+
'/clear': (ctx) => {
|
|
337
|
+
ctx.history.length = 0;
|
|
338
|
+
resetTokenAnchor();
|
|
339
|
+
ctx.onEvent({ kind: 'text_delta', text: 'Conversation history cleared.\n' });
|
|
340
|
+
emitDone(ctx);
|
|
341
|
+
},
|
|
259
342
|
'/compact': async (ctx) => {
|
|
260
343
|
const beforeTokens = estimateHistoryTokens(ctx.history);
|
|
261
344
|
const { history: compacted, compacted: didCompact } = await forceCompact(ctx.history, ctx.config.model, ctx.client, ctx.config.debug);
|
|
@@ -263,12 +346,16 @@ const DIRECT_COMMANDS = {
|
|
|
263
346
|
ctx.history.length = 0;
|
|
264
347
|
ctx.history.push(...compacted);
|
|
265
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
|
+
});
|
|
266
358
|
}
|
|
267
|
-
const afterTokens = estimateHistoryTokens(ctx.history);
|
|
268
|
-
ctx.onEvent({ kind: 'text_delta', text: didCompact
|
|
269
|
-
? `Compacted: ~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens\n`
|
|
270
|
-
: `History too short to compact (${beforeTokens.toLocaleString()} tokens, ${ctx.history.length} messages).\n`
|
|
271
|
-
});
|
|
272
359
|
emitDone(ctx);
|
|
273
360
|
},
|
|
274
361
|
};
|
|
@@ -320,12 +407,28 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
320
407
|
await DIRECT_COMMANDS[input](ctx);
|
|
321
408
|
return { handled: true };
|
|
322
409
|
}
|
|
410
|
+
// /model — show current model or switch with /model <name>
|
|
411
|
+
if (input === '/model' || input.startsWith('/model ')) {
|
|
412
|
+
if (input === '/model') {
|
|
413
|
+
ctx.onEvent({ kind: 'text_delta', text: `Current model: **${ctx.config.model}**\n` +
|
|
414
|
+
`Switch with: \`/model <name>\` (e.g. \`/model sonnet\`, \`/model free\`, \`/model gemini\`)\n`
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const newModel = resolveModel(input.slice(7).trim());
|
|
419
|
+
ctx.config.model = newModel;
|
|
420
|
+
ctx.onEvent({ kind: 'text_delta', text: `Model → **${newModel}**\n` });
|
|
421
|
+
}
|
|
422
|
+
emitDone(ctx);
|
|
423
|
+
return { handled: true };
|
|
424
|
+
}
|
|
323
425
|
// /branch has both no-arg and with-arg forms
|
|
324
426
|
if (input === '/branch' || input.startsWith('/branch ')) {
|
|
325
427
|
const cwd = ctx.config.workingDir || process.cwd();
|
|
326
428
|
if (input === '/branch') {
|
|
327
429
|
const r = gitCmd(ctx, 'git branch -v --no-color');
|
|
328
|
-
|
|
430
|
+
if (r !== null)
|
|
431
|
+
ctx.onEvent({ kind: 'text_delta', text: r ? `\`\`\`\n${r}\n\`\`\`\n` : 'No branches yet.\n' });
|
|
329
432
|
}
|
|
330
433
|
else {
|
|
331
434
|
const branchName = input.slice(8).trim();
|
package/dist/agent/compact.js
CHANGED
|
@@ -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) {
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -4,11 +4,6 @@
|
|
|
4
4
|
* Original implementation with different architecture from any reference codebase.
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentConfig, Dialogue, StreamEvent } from './types.js';
|
|
7
|
-
/**
|
|
8
|
-
* Run the agent loop.
|
|
9
|
-
* Yields StreamEvents for the UI to render. Returns when the conversation ends.
|
|
10
|
-
*/
|
|
11
|
-
export declare function runAgent(config: AgentConfig, initialPrompt: string): AsyncGenerator<StreamEvent, void>;
|
|
12
7
|
/**
|
|
13
8
|
* Run a multi-turn interactive session.
|
|
14
9
|
* Each user message triggers a full agent loop.
|
package/dist/agent/loop.js
CHANGED
|
@@ -14,182 +14,6 @@ import { optimizeHistory, CAPPED_MAX_TOKENS, ESCALATED_MAX_TOKENS, getMaxOutputT
|
|
|
14
14
|
import { recordUsage } from '../stats/tracker.js';
|
|
15
15
|
import { estimateCost } from '../pricing.js';
|
|
16
16
|
import { createSessionId, appendToSession, updateSessionMeta, pruneOldSessions, } from '../session/storage.js';
|
|
17
|
-
// ─── Main Entry Point ──────────────────────────────────────────────────────
|
|
18
|
-
/**
|
|
19
|
-
* Run the agent loop.
|
|
20
|
-
* Yields StreamEvents for the UI to render. Returns when the conversation ends.
|
|
21
|
-
*/
|
|
22
|
-
export async function* runAgent(config, initialPrompt) {
|
|
23
|
-
const client = new ModelClient({
|
|
24
|
-
apiUrl: config.apiUrl,
|
|
25
|
-
chain: config.chain,
|
|
26
|
-
debug: config.debug,
|
|
27
|
-
});
|
|
28
|
-
const capabilityMap = new Map();
|
|
29
|
-
for (const cap of config.capabilities) {
|
|
30
|
-
capabilityMap.set(cap.spec.name, cap);
|
|
31
|
-
}
|
|
32
|
-
const toolDefs = config.capabilities.map((c) => c.spec);
|
|
33
|
-
const maxTurns = config.maxTurns ?? 100;
|
|
34
|
-
const workDir = config.workingDir ?? process.cwd();
|
|
35
|
-
const state = {
|
|
36
|
-
history: [
|
|
37
|
-
{ role: 'user', content: initialPrompt },
|
|
38
|
-
],
|
|
39
|
-
turnIndex: 0,
|
|
40
|
-
abort: new AbortController(),
|
|
41
|
-
};
|
|
42
|
-
// ─── Reasoning-Action Cycle ────────────────────────────────────────────
|
|
43
|
-
while (state.turnIndex < maxTurns) {
|
|
44
|
-
state.turnIndex++;
|
|
45
|
-
// 1. Call model
|
|
46
|
-
const { content: responseParts, usage } = await callModel(client, config, state, toolDefs);
|
|
47
|
-
// Emit usage
|
|
48
|
-
yield {
|
|
49
|
-
kind: 'usage',
|
|
50
|
-
inputTokens: usage.inputTokens,
|
|
51
|
-
outputTokens: usage.outputTokens,
|
|
52
|
-
model: config.model,
|
|
53
|
-
};
|
|
54
|
-
// 2. Classify response parts
|
|
55
|
-
const textParts = [];
|
|
56
|
-
const invocations = [];
|
|
57
|
-
for (const part of responseParts) {
|
|
58
|
-
if (part.type === 'text') {
|
|
59
|
-
textParts.push(part.text);
|
|
60
|
-
yield { kind: 'text_delta', text: part.text };
|
|
61
|
-
}
|
|
62
|
-
else if (part.type === 'tool_use') {
|
|
63
|
-
invocations.push(part);
|
|
64
|
-
}
|
|
65
|
-
else if (part.type === 'thinking') {
|
|
66
|
-
yield { kind: 'thinking_delta', text: part.thinking };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// 3. Append assistant response to history
|
|
70
|
-
state.history.push({
|
|
71
|
-
role: 'assistant',
|
|
72
|
-
content: responseParts,
|
|
73
|
-
});
|
|
74
|
-
// 4. If no capability invocations, the agent is done
|
|
75
|
-
if (invocations.length === 0) {
|
|
76
|
-
yield { kind: 'turn_done', reason: 'completed' };
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
// 5. Execute capabilities
|
|
80
|
-
const outcomes = await executeCapabilities(invocations, capabilityMap, workDir, state.abort, (evt) => { config.onEvent?.(evt); });
|
|
81
|
-
// Emit capability results
|
|
82
|
-
for (const [invocation, result] of outcomes) {
|
|
83
|
-
yield {
|
|
84
|
-
kind: 'capability_done',
|
|
85
|
-
id: invocation.id,
|
|
86
|
-
result,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
// 6. Append capability outcomes as user message
|
|
90
|
-
const outcomeContent = outcomes.map(([invocation, result]) => ({
|
|
91
|
-
type: 'tool_result',
|
|
92
|
-
tool_use_id: invocation.id,
|
|
93
|
-
content: result.output,
|
|
94
|
-
is_error: result.isError,
|
|
95
|
-
}));
|
|
96
|
-
state.history.push({
|
|
97
|
-
role: 'user',
|
|
98
|
-
content: outcomeContent,
|
|
99
|
-
});
|
|
100
|
-
// Continue to next cycle...
|
|
101
|
-
}
|
|
102
|
-
yield { kind: 'turn_done', reason: 'max_turns' };
|
|
103
|
-
}
|
|
104
|
-
// ─── Model Call ────────────────────────────────────────────────────────────
|
|
105
|
-
async function callModel(client, config, state, tools) {
|
|
106
|
-
const systemPrompt = config.systemInstructions.join('\n\n');
|
|
107
|
-
return client.complete({
|
|
108
|
-
model: config.model,
|
|
109
|
-
messages: state.history,
|
|
110
|
-
system: systemPrompt,
|
|
111
|
-
tools,
|
|
112
|
-
max_tokens: 16384,
|
|
113
|
-
stream: true,
|
|
114
|
-
}, state.abort.signal);
|
|
115
|
-
}
|
|
116
|
-
// ─── Capability Execution ──────────────────────────────────────────────────
|
|
117
|
-
async function executeCapabilities(invocations, handlers, workDir, abort, emitEvent, permissions) {
|
|
118
|
-
// Partition into concurrent-safe and sequential
|
|
119
|
-
const concurrent = [];
|
|
120
|
-
const sequential = [];
|
|
121
|
-
for (const inv of invocations) {
|
|
122
|
-
const handler = handlers.get(inv.name);
|
|
123
|
-
if (handler?.concurrent) {
|
|
124
|
-
concurrent.push(inv);
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
sequential.push(inv);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const results = [];
|
|
131
|
-
const scope = {
|
|
132
|
-
workingDir: workDir,
|
|
133
|
-
abortSignal: abort.signal,
|
|
134
|
-
};
|
|
135
|
-
// Run concurrent capabilities in parallel
|
|
136
|
-
if (concurrent.length > 0) {
|
|
137
|
-
const batch = concurrent.map(async (inv) => {
|
|
138
|
-
const result = await checkAndRun(inv, handlers, scope, permissions, emitEvent);
|
|
139
|
-
return [inv, result];
|
|
140
|
-
});
|
|
141
|
-
const batchResults = await Promise.all(batch);
|
|
142
|
-
results.push(...batchResults);
|
|
143
|
-
}
|
|
144
|
-
// Run sequential capabilities one at a time
|
|
145
|
-
for (const inv of sequential) {
|
|
146
|
-
const result = await checkAndRun(inv, handlers, scope, permissions, emitEvent);
|
|
147
|
-
results.push([inv, result]);
|
|
148
|
-
}
|
|
149
|
-
return results;
|
|
150
|
-
}
|
|
151
|
-
async function checkAndRun(invocation, handlers, scope, permissions, emitEvent) {
|
|
152
|
-
// Permission check
|
|
153
|
-
if (permissions) {
|
|
154
|
-
const decision = await permissions.check(invocation.name, invocation.input);
|
|
155
|
-
if (decision.behavior === 'deny') {
|
|
156
|
-
return {
|
|
157
|
-
output: `Permission denied for ${invocation.name}: ${decision.reason || 'denied by policy'}`,
|
|
158
|
-
isError: true,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
if (decision.behavior === 'ask') {
|
|
162
|
-
const allowed = await permissions.promptUser(invocation.name, invocation.input);
|
|
163
|
-
if (!allowed) {
|
|
164
|
-
return {
|
|
165
|
-
output: `User denied permission for ${invocation.name}`,
|
|
166
|
-
isError: true,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
emitEvent({ kind: 'capability_start', id: invocation.id, name: invocation.name });
|
|
172
|
-
return runSingleCapability(invocation, handlers, scope);
|
|
173
|
-
}
|
|
174
|
-
async function runSingleCapability(invocation, handlers, scope) {
|
|
175
|
-
const handler = handlers.get(invocation.name);
|
|
176
|
-
if (!handler) {
|
|
177
|
-
return {
|
|
178
|
-
output: `Unknown capability: ${invocation.name}`,
|
|
179
|
-
isError: true,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
return await handler.execute(invocation.input, scope);
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
187
|
-
return {
|
|
188
|
-
output: `Error executing ${invocation.name}: ${message}`,
|
|
189
|
-
isError: true,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
17
|
// ─── Interactive Session ───────────────────────────────────────────────────
|
|
194
18
|
/**
|
|
195
19
|
* Run a multi-turn interactive session.
|
|
@@ -211,6 +35,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
211
35
|
const workDir = config.workingDir ?? process.cwd();
|
|
212
36
|
const permissions = new PermissionManager(config.permissionMode ?? 'default');
|
|
213
37
|
const history = [];
|
|
38
|
+
let lastUserInput = ''; // For /retry
|
|
214
39
|
// Session persistence
|
|
215
40
|
const sessionId = createSessionId();
|
|
216
41
|
let turnCount = 0;
|
|
@@ -224,14 +49,26 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
224
49
|
continue; // Empty input → re-prompt
|
|
225
50
|
// ── Slash command dispatch ──
|
|
226
51
|
if (input.startsWith('/')) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
52
|
+
// /retry re-sends the last user message
|
|
53
|
+
if (input === '/retry') {
|
|
54
|
+
if (!lastUserInput) {
|
|
55
|
+
onEvent({ kind: 'text_delta', text: 'No previous message to retry.\n' });
|
|
56
|
+
onEvent({ kind: 'turn_done', reason: 'completed' });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
input = lastUserInput;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const cmdResult = await handleSlashCommand(input, {
|
|
63
|
+
history, config, client, sessionId, onEvent,
|
|
64
|
+
});
|
|
65
|
+
if (cmdResult.handled)
|
|
66
|
+
continue;
|
|
67
|
+
if (cmdResult.rewritten)
|
|
68
|
+
input = cmdResult.rewritten;
|
|
69
|
+
}
|
|
234
70
|
}
|
|
71
|
+
lastUserInput = input;
|
|
235
72
|
history.push({ role: 'user', content: input });
|
|
236
73
|
appendToSession(sessionId, { role: 'user', content: input });
|
|
237
74
|
turnCount++;
|
package/dist/tools/askuser.js
CHANGED
|
@@ -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:
|
|
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
|
|
43
|
+
resolve({ output: 'User closed input without responding.', isError: false });
|
|
36
44
|
});
|
|
37
45
|
});
|
|
38
46
|
}
|
package/dist/ui/app.js
CHANGED
|
@@ -171,8 +171,10 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
171
171
|
setStreamText('');
|
|
172
172
|
setTools(new Map());
|
|
173
173
|
setTurnTokens({ input: 0, output: 0 });
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
setWaiting(true);
|
|
175
|
+
setReady(false);
|
|
176
|
+
// Pass through to agent loop to clear the actual conversation history
|
|
177
|
+
onSubmit('/clear');
|
|
176
178
|
return;
|
|
177
179
|
case '/retry':
|
|
178
180
|
if (!lastPrompt) {
|
|
@@ -293,9 +295,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
293
295
|
}), _jsx(Text, { children: " " })] }));
|
|
294
296
|
}
|
|
295
297
|
// ── Normal Mode ──
|
|
296
|
-
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.
|
|
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
|
|
297
299
|
? _jsxs(Text, { color: "red", children: [" \u2717 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms"] })] })
|
|
298
|
-
: _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` : ''; })() })] })) },
|
|
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' }))] }));
|
|
299
301
|
}
|
|
300
302
|
export function launchInkUI(opts) {
|
|
301
303
|
let resolveInput = null;
|
package/dist/ui/terminal.d.ts
CHANGED
package/dist/ui/terminal.js
CHANGED
|
@@ -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 = [];
|
|
@@ -154,7 +156,9 @@ export class TerminalUI {
|
|
|
154
156
|
});
|
|
155
157
|
rl.on('close', () => {
|
|
156
158
|
this.stdinEOF = true;
|
|
157
|
-
|
|
159
|
+
// Keep lineQueue intact — buffered lines should still drain before signaling EOF.
|
|
160
|
+
// If there are active waiters, queue is already empty (nextLine checks queue first),
|
|
161
|
+
// so it's safe to resolve them with null now.
|
|
158
162
|
for (const waiter of this.lineWaiters)
|
|
159
163
|
waiter(null);
|
|
160
164
|
this.lineWaiters = [];
|
|
@@ -267,6 +271,8 @@ export class TerminalUI {
|
|
|
267
271
|
case 'usage':
|
|
268
272
|
this.totalInputTokens += event.inputTokens;
|
|
269
273
|
this.totalOutputTokens += event.outputTokens;
|
|
274
|
+
if (event.model)
|
|
275
|
+
this.sessionModel = event.model;
|
|
270
276
|
break;
|
|
271
277
|
case 'turn_done': {
|
|
272
278
|
this.spinner.stop();
|
|
@@ -293,9 +299,14 @@ export class TerminalUI {
|
|
|
293
299
|
const cmd = parts[0].toLowerCase();
|
|
294
300
|
switch (cmd) {
|
|
295
301
|
case '/cost':
|
|
296
|
-
case '/usage':
|
|
297
|
-
|
|
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`));
|
|
298
308
|
return true;
|
|
309
|
+
}
|
|
299
310
|
default:
|
|
300
311
|
// All other slash commands pass through to the agent loop (commands.ts handles them)
|
|
301
312
|
return false;
|