@trenchwork/erosolar 1.1.41 → 1.1.43
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/LICENSE +16 -21
- package/README.md +236 -236
- package/agents/erosolar-code.rules.json +199 -199
- package/dist/bin/erosolar.js +0 -0
- package/dist/capabilities/enhancedGitCapability.js +63 -63
- package/dist/config.js +12 -12
- package/dist/contracts/models.schema.json +9 -9
- package/dist/contracts/module-schema.json +367 -367
- package/dist/contracts/schemas/agent-profile.schema.json +157 -157
- package/dist/contracts/schemas/agent-rules.schema.json +238 -238
- package/dist/contracts/schemas/agent-schemas.schema.json +528 -528
- package/dist/contracts/schemas/agent.schema.json +90 -90
- package/dist/contracts/schemas/tool-selection.schema.json +174 -174
- package/dist/contracts/tools.schema.json +42 -42
- package/dist/core/constants.js +7 -7
- package/dist/core/contextManager.js +16 -16
- package/dist/core/hitl.d.ts.map +1 -1
- package/dist/core/hitl.js +17 -16
- package/dist/core/hitl.js.map +1 -1
- package/dist/core/permissionMode.d.ts +40 -0
- package/dist/core/permissionMode.d.ts.map +1 -0
- package/dist/core/permissionMode.js +86 -0
- package/dist/core/permissionMode.js.map +1 -0
- package/dist/core/secretStore.js +1 -1
- package/dist/core/taskCompletionDetector.js +17 -17
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +21 -2
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/headless/interactiveShell.d.ts +7 -5
- package/dist/headless/interactiveShell.d.ts.map +1 -1
- package/dist/headless/interactiveShell.js +105 -167
- package/dist/headless/interactiveShell.js.map +1 -1
- package/dist/leanAgent.js +38 -38
- package/dist/runtime/agentSession.js +4 -4
- package/dist/shell/commandRegistry.js +6 -6
- package/dist/shell/commandRegistry.js.map +1 -1
- package/dist/shell/toolPresentation.d.ts +47 -0
- package/dist/shell/toolPresentation.d.ts.map +1 -0
- package/dist/shell/toolPresentation.js +260 -0
- package/dist/shell/toolPresentation.js.map +1 -0
- package/dist/shell/vimMode.js +29 -29
- package/dist/tools/bashTools.js +2 -2
- package/dist/tools/bashTools.js.map +1 -1
- package/dist/tools/hitlTools.js +18 -18
- package/dist/tools/webTools.d.ts.map +1 -1
- package/dist/tools/webTools.js +75 -3
- package/dist/tools/webTools.js.map +1 -1
- package/dist/ui/ink/App.d.ts +2 -0
- package/dist/ui/ink/App.d.ts.map +1 -1
- package/dist/ui/ink/App.js +2 -2
- package/dist/ui/ink/App.js.map +1 -1
- package/dist/ui/ink/ChatStatic.d.ts +6 -5
- package/dist/ui/ink/ChatStatic.d.ts.map +1 -1
- package/dist/ui/ink/ChatStatic.js +35 -10
- package/dist/ui/ink/ChatStatic.js.map +1 -1
- package/dist/ui/ink/InkPromptController.d.ts +11 -0
- package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
- package/dist/ui/ink/InkPromptController.js +50 -11
- package/dist/ui/ink/InkPromptController.js.map +1 -1
- package/dist/ui/ink/Prompt.d.ts +2 -0
- package/dist/ui/ink/Prompt.d.ts.map +1 -1
- package/dist/ui/ink/Prompt.js +31 -2
- package/dist/ui/ink/Prompt.js.map +1 -1
- package/dist/ui/ink/StatusLine.d.ts +16 -8
- package/dist/ui/ink/StatusLine.d.ts.map +1 -1
- package/dist/ui/ink/StatusLine.js +45 -4
- package/dist/ui/ink/StatusLine.js.map +1 -1
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +4 -6
- package/dist/ui/theme.js.map +1 -1
- package/package.json +116 -116
- package/scripts/postinstall.cjs +57 -57
|
@@ -43,6 +43,9 @@ import { startNewRun } from '../tools/fileChangeTracker.js';
|
|
|
43
43
|
import { onSudoPasswordNeeded, offSudoPasswordNeeded, provideSudoPassword } from '../core/sudoPasswordManager.js';
|
|
44
44
|
import { reportStatus, setStatusSink } from '../utils/statusReporter.js';
|
|
45
45
|
import { isSafetyRefusal } from '../core/refusalDetection.js';
|
|
46
|
+
import { formatToolCall, toolActivityLabel, formatToolResult, formatToolError } from '../shell/toolPresentation.js';
|
|
47
|
+
// Tool-result display (ANSI stripping, summarisation, the `⎿` block) now lives
|
|
48
|
+
// in ../shell/toolPresentation.ts — the shell just emits the formatted strings.
|
|
46
49
|
// Timeout constants for regular prompt processing (reasoning models like DeepSeek)
|
|
47
50
|
const PROMPT_REASONING_TIMEOUT_MS = 60 * 1000; // 60 seconds max for reasoning-only without action
|
|
48
51
|
// Per-step timeout: how long we'll wait for the *next* event before
|
|
@@ -128,24 +131,46 @@ function getVersion() {
|
|
|
128
131
|
return '0.0.0';
|
|
129
132
|
}
|
|
130
133
|
}
|
|
134
|
+
/** Inner content of the welcome box (plain, no border/colour). */
|
|
135
|
+
function welcomeBodyLines(input) {
|
|
136
|
+
const body = ['✻ Welcome to Erosolar Coder', ''];
|
|
137
|
+
if (!input.hasApiKey) {
|
|
138
|
+
body.push('⚠ No API key configured', '', 'Get your key: https://platform.deepseek.com/', 'Set your key: /key YOUR_API_KEY');
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
body.push(`${input.model} · ${input.provider}`, `Key: ${input.maskedKey} · /help for commands`);
|
|
142
|
+
}
|
|
143
|
+
if (input.cwd)
|
|
144
|
+
body.push(`cwd: ${input.cwd}`);
|
|
145
|
+
return body;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Wrap content lines in a Claude-Code-style rounded box (╭╮╰╯). `paint`
|
|
149
|
+
* colours an already-padded content cell; `border` colours the frame. Both
|
|
150
|
+
* default to identity so the pure version stays ANSI-free.
|
|
151
|
+
*/
|
|
152
|
+
function roundedBox(content, paint = (s) => s, border = (s) => s) {
|
|
153
|
+
const width = Math.min(content.reduce((m, c) => Math.max(m, c.length), 0), 72);
|
|
154
|
+
const pad = (c) => c + ' '.repeat(Math.max(0, width - c.length));
|
|
155
|
+
const rule = '─'.repeat(width + 2);
|
|
156
|
+
return [
|
|
157
|
+
border(`╭${rule}╮`),
|
|
158
|
+
...content.map((c) => `${border('│')} ${paint(pad(c))} ${border('│')}`),
|
|
159
|
+
border(`╰${rule}╯`),
|
|
160
|
+
];
|
|
161
|
+
}
|
|
131
162
|
/**
|
|
132
163
|
* Compose the lines shown when the interactive shell opens. Deliberately NOT a
|
|
133
164
|
* marketing splash — bare `erosolar` opens straight into the chat (like
|
|
134
|
-
* `claude`); this is
|
|
135
|
-
* the active model + masked key
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
165
|
+
* `claude`); this is the load-bearing welcome: a sparkle, the name, and either
|
|
166
|
+
* how to set a key or the active model + masked key, inside a rounded box that
|
|
167
|
+
* mirrors Claude Code's. Pure (no chalk/ANSI, no I/O) so the "no marketing
|
|
168
|
+
* splash, key guidance kept" contract is unit-testable without a PTY. The live
|
|
169
|
+
* renderer colourises equivalent content; this is the source of truth for
|
|
170
|
+
* WHICH lines appear.
|
|
139
171
|
*/
|
|
140
172
|
export function composeWelcomeLines(input) {
|
|
141
|
-
|
|
142
|
-
if (!input.hasApiKey) {
|
|
143
|
-
lines.push(' ⚠ No API key configured', '', ' Get your key: https://platform.deepseek.com/', ' Set your key: /key YOUR_API_KEY', '');
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
lines.push(` ${input.model} · ${input.provider}`, ` Key: ${input.maskedKey} · /help for commands`, '');
|
|
147
|
-
}
|
|
148
|
-
return lines;
|
|
173
|
+
return ['', ...(input.updateLines ?? []), ...roundedBox(welcomeBodyLines(input)), ''];
|
|
149
174
|
}
|
|
150
175
|
/**
|
|
151
176
|
* Run the fully interactive shell with rich UI.
|
|
@@ -292,6 +317,7 @@ class InteractiveShell {
|
|
|
292
317
|
onCtrlC: (info) => this.handleCtrlC(info),
|
|
293
318
|
onToggleAutoContinue: () => this.handleAutoContinueToggle(),
|
|
294
319
|
onToggleHITL: () => this.handleHITLToggle(),
|
|
320
|
+
onCyclePermissionMode: (mode) => this.handlePermissionModeChange(mode),
|
|
295
321
|
});
|
|
296
322
|
// Register cleanup callback for graceful shutdown
|
|
297
323
|
onShutdown(() => {
|
|
@@ -395,21 +421,20 @@ class InteractiveShell {
|
|
|
395
421
|
chalk.dim(' · installing in background…'));
|
|
396
422
|
this.runBackgroundUpdate(updateInfo);
|
|
397
423
|
}
|
|
398
|
-
// Clean, minimal welcome
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const welcomeContent = welcomeLines.join('\n');
|
|
424
|
+
// Clean, minimal welcome — a sparkle + the essentials in a rounded box,
|
|
425
|
+
// mirroring Claude Code. The pure composeWelcomeLines() is the contract for
|
|
426
|
+
// WHICH lines appear; here we draw the same box with brand colour.
|
|
427
|
+
const flare = chalk.hex('#ff6a1f');
|
|
428
|
+
const wire = chalk.hex('#3a362e');
|
|
429
|
+
const body = welcomeBodyLines({
|
|
430
|
+
hasApiKey,
|
|
431
|
+
maskedKey: hasApiKey ? maskApiKey(apiKey) : '',
|
|
432
|
+
model: this.profileConfig.model,
|
|
433
|
+
provider: this.profileConfig.provider,
|
|
434
|
+
cwd: this.workingDir,
|
|
435
|
+
});
|
|
436
|
+
const boxed = roundedBox(body, (cell) => cell.replace('✻', flare('✻')), (s) => wire(s));
|
|
437
|
+
const welcomeContent = ['', ...updateLines, ...boxed, ''].join('\n');
|
|
413
438
|
// Use renderer event system instead of direct stdout writes
|
|
414
439
|
renderer.addEvent('banner', welcomeContent);
|
|
415
440
|
// Update renderer meta with model info
|
|
@@ -455,7 +480,7 @@ class InteractiveShell {
|
|
|
455
480
|
}
|
|
456
481
|
try {
|
|
457
482
|
// Show password prompt
|
|
458
|
-
renderer.addEvent('system', chalk.yellow('
|
|
483
|
+
renderer.addEvent('system', chalk.yellow('Sudo password required'));
|
|
459
484
|
renderer.setSecretMode(true);
|
|
460
485
|
renderer.clearBuffer();
|
|
461
486
|
// Capture password input
|
|
@@ -804,11 +829,6 @@ class InteractiveShell {
|
|
|
804
829
|
this.promptController?.getRenderer()?.addEvent('response', 'Email is not handled by the CLI.');
|
|
805
830
|
return true;
|
|
806
831
|
}
|
|
807
|
-
// Session stats
|
|
808
|
-
if (lower === '/stats' || lower === '/status') {
|
|
809
|
-
this.showSessionStats();
|
|
810
|
-
return true;
|
|
811
|
-
}
|
|
812
832
|
return false;
|
|
813
833
|
}
|
|
814
834
|
/**
|
|
@@ -1054,7 +1074,7 @@ class InteractiveShell {
|
|
|
1054
1074
|
// Clear loading message
|
|
1055
1075
|
this.promptController?.setStatusMessage(null);
|
|
1056
1076
|
// Show the interactive menu
|
|
1057
|
-
this.promptController?.setMenu(menuItems, { title: '
|
|
1077
|
+
this.promptController?.setMenu(menuItems, { title: 'Select Model' }, (selected) => {
|
|
1058
1078
|
if (selected) {
|
|
1059
1079
|
// Parse provider:model format
|
|
1060
1080
|
const [providerId, ...modelParts] = selected.id.split(':');
|
|
@@ -1115,7 +1135,7 @@ class InteractiveShell {
|
|
|
1115
1135
|
};
|
|
1116
1136
|
});
|
|
1117
1137
|
// Show the interactive menu
|
|
1118
|
-
this.promptController.setMenu(menuItems, { title: '
|
|
1138
|
+
this.promptController.setMenu(menuItems, { title: 'API Keys — Select to Configure' }, (selected) => {
|
|
1119
1139
|
if (selected) {
|
|
1120
1140
|
// Start secret input for selected key
|
|
1121
1141
|
this.promptForSecret(selected.id);
|
|
@@ -1229,7 +1249,7 @@ class InteractiveShell {
|
|
|
1229
1249
|
}
|
|
1230
1250
|
showHelp() {
|
|
1231
1251
|
if (!this.promptController?.supportsInlinePanel()) {
|
|
1232
|
-
this.promptController?.setStatusMessage('Help: /model /secrets /auto /
|
|
1252
|
+
this.promptController?.setStatusMessage('Help: /model /secrets /auto /keys /clear /exit');
|
|
1233
1253
|
setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
|
|
1234
1254
|
return;
|
|
1235
1255
|
}
|
|
@@ -1248,11 +1268,14 @@ class InteractiveShell {
|
|
|
1248
1268
|
cmd('/debug') + dim(' Toggle debug mode'),
|
|
1249
1269
|
cmd('/adversarial') + dim(' Toggle the adversarial verifier (default on)'),
|
|
1250
1270
|
cmd('/ultracode') + dim(' Toggle ultracode operating mode (default on)'),
|
|
1251
|
-
cmd('/stats') + dim(' Show session token + cost stats'),
|
|
1252
1271
|
cmd('/keys') + dim(' Show keyboard shortcuts'),
|
|
1253
1272
|
cmd('/clear') + dim(' Clear the screen'),
|
|
1254
1273
|
cmd('/exit') + dim(' Quit'),
|
|
1255
1274
|
'',
|
|
1275
|
+
heading('Modes'),
|
|
1276
|
+
cmd('Shift+Tab') + dim(' Cycle permission mode: default → accept edits → plan'),
|
|
1277
|
+
dim(' plan mode is read-only — the agent investigates, then proposes a plan'),
|
|
1278
|
+
'',
|
|
1256
1279
|
heading('Quick start'),
|
|
1257
1280
|
dim(' 1. /key sk-… (or set DEEPSEEK_API_KEY)'),
|
|
1258
1281
|
dim(' 2. Type any prompt; the agent reads files, edits, runs commands'),
|
|
@@ -1272,67 +1295,32 @@ class InteractiveShell {
|
|
|
1272
1295
|
}
|
|
1273
1296
|
const kb = (key) => chalk.hex('#ffb142')(key);
|
|
1274
1297
|
const desc = (text) => chalk.dim(text);
|
|
1298
|
+
// Only shortcuts the Ink Prompt (src/ui/ink/Prompt.tsx) actually
|
|
1299
|
+
// implements are listed — advertising keys the input handler ignores
|
|
1300
|
+
// would be a deceptive panel (Glasswing transparency).
|
|
1275
1301
|
const lines = [
|
|
1276
1302
|
chalk.bold.hex('#ece6da')('Keyboard Shortcuts') + chalk.dim(' (press any key to dismiss)'),
|
|
1277
1303
|
'',
|
|
1278
1304
|
chalk.hex('#cbf24e')('Navigation'),
|
|
1279
1305
|
` ${kb('Ctrl+A')} / ${kb('Home')} ${desc('Move to start of line')}`,
|
|
1280
1306
|
` ${kb('Ctrl+E')} / ${kb('End')} ${desc('Move to end of line')}`,
|
|
1281
|
-
` ${kb('
|
|
1307
|
+
` ${kb('←')} / ${kb('→')} ${desc('Move cursor')}`,
|
|
1282
1308
|
'',
|
|
1283
1309
|
chalk.hex('#cbf24e')('Editing'),
|
|
1284
|
-
` ${kb('Ctrl+U')} ${desc('
|
|
1285
|
-
` ${kb('Ctrl+W')}
|
|
1310
|
+
` ${kb('Ctrl+U')} ${desc('Delete to start of line')}`,
|
|
1311
|
+
` ${kb('Ctrl+W')} ${desc('Delete word backward')}`,
|
|
1286
1312
|
` ${kb('Ctrl+K')} ${desc('Delete to end of line')}`,
|
|
1287
1313
|
'',
|
|
1288
|
-
chalk.hex('#cbf24e')('
|
|
1289
|
-
` ${kb('
|
|
1290
|
-
` ${kb('Ctrl+O')} ${desc('Expand last tool result')}`,
|
|
1314
|
+
chalk.hex('#cbf24e')('Modes'),
|
|
1315
|
+
` ${kb('Shift+Tab')} ${desc('Cycle permission mode (default · accept edits · plan)')}`,
|
|
1291
1316
|
'',
|
|
1292
1317
|
chalk.hex('#cbf24e')('Control'),
|
|
1293
|
-
` ${kb('Ctrl+C')} ${desc('
|
|
1318
|
+
` ${kb('Ctrl+C')} ${desc('Clear input / interrupt')}`,
|
|
1294
1319
|
` ${kb('Ctrl+D')} ${desc('Exit (when empty)')}`,
|
|
1295
|
-
` ${kb('Esc')} ${desc('Interrupt AI response')}`,
|
|
1296
1320
|
];
|
|
1297
1321
|
this.promptController.setInlinePanel(lines);
|
|
1298
1322
|
this.scheduleInlinePanelDismiss();
|
|
1299
1323
|
}
|
|
1300
|
-
showSessionStats() {
|
|
1301
|
-
if (!this.promptController?.supportsInlinePanel()) {
|
|
1302
|
-
this.promptController?.setStatusMessage('Use /stats in interactive mode');
|
|
1303
|
-
setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
const history = this.controller.getHistory();
|
|
1307
|
-
const messageCount = history.length;
|
|
1308
|
-
const userMessages = history.filter(m => m.role === 'user').length;
|
|
1309
|
-
const assistantMessages = history.filter(m => m.role === 'assistant').length;
|
|
1310
|
-
// Calculate approximate token usage from history
|
|
1311
|
-
let totalChars = 0;
|
|
1312
|
-
for (const msg of history) {
|
|
1313
|
-
if (typeof msg.content === 'string') {
|
|
1314
|
-
totalChars += msg.content.length;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
const approxTokens = Math.round(totalChars / 4); // Rough estimate
|
|
1318
|
-
const collapsedCount = this.promptController?.getRenderer?.()?.getCollapsedResultCount?.() ?? 0;
|
|
1319
|
-
const lines = [
|
|
1320
|
-
chalk.bold.hex('#ece6da')('Session Stats') + chalk.dim(' (press any key to dismiss)'),
|
|
1321
|
-
'',
|
|
1322
|
-
chalk.hex('#cbf24e')('Conversation'),
|
|
1323
|
-
` ${chalk.white(messageCount.toString())} messages (${userMessages} user, ${assistantMessages} assistant)`,
|
|
1324
|
-
` ${chalk.dim('~')}${chalk.white(approxTokens.toLocaleString())} ${chalk.dim('tokens (estimate)')}`,
|
|
1325
|
-
'',
|
|
1326
|
-
chalk.hex('#cbf24e')('Model'),
|
|
1327
|
-
` ${chalk.white(this.profileConfig.model)} ${chalk.dim('on')} ${chalk.hex('#ff9352')(this.profileConfig.provider)}`,
|
|
1328
|
-
collapsedCount > 0 ? ` ${chalk.white(collapsedCount.toString())} collapsed results` : '',
|
|
1329
|
-
'',
|
|
1330
|
-
chalk.hex('#cbf24e')('Settings'),
|
|
1331
|
-
` Debug: ${this.debugEnabled ? chalk.green('on') : chalk.dim('off')}`,
|
|
1332
|
-
].filter(line => line !== '');
|
|
1333
|
-
this.promptController.setInlinePanel(lines);
|
|
1334
|
-
this.scheduleInlinePanelDismiss();
|
|
1335
|
-
}
|
|
1336
1324
|
/**
|
|
1337
1325
|
* Auto-dismiss inline panel after timeout or on next input.
|
|
1338
1326
|
*/
|
|
@@ -1428,11 +1416,11 @@ class InteractiveShell {
|
|
|
1428
1416
|
// Check for timeout marker
|
|
1429
1417
|
if (eventOrTimeout && typeof eventOrTimeout === 'object' && '__timeout' in eventOrTimeout) {
|
|
1430
1418
|
if (hitlDepth > 0) {
|
|
1431
|
-
this.promptController?.setStatusMessage('
|
|
1419
|
+
this.promptController?.setStatusMessage('Waiting for human decision…');
|
|
1432
1420
|
continue;
|
|
1433
1421
|
}
|
|
1434
1422
|
stepTimedOut = true;
|
|
1435
|
-
this.promptController?.setStatusMessage(
|
|
1423
|
+
this.promptController?.setStatusMessage(`Step timeout (${PROMPT_STEP_TIMEOUT_MS / 1000}s) — completing response`);
|
|
1436
1424
|
// Cancel the controller so the underlying agent stops generating
|
|
1437
1425
|
// events that would never be consumed. Without this the spinner
|
|
1438
1426
|
// can keep ticking against a "ghost" run after the for-await
|
|
@@ -1448,7 +1436,7 @@ class InteractiveShell {
|
|
|
1448
1436
|
const totalElapsed = Date.now() - promptStartTime;
|
|
1449
1437
|
if (!hasReceivedMeaningfulContent && totalElapsed > TOTAL_PROMPT_TIMEOUT_MS) {
|
|
1450
1438
|
if (renderer) {
|
|
1451
|
-
renderer.addEvent('response', chalk.yellow(`\
|
|
1439
|
+
renderer.addEvent('response', chalk.yellow(`\nResponse timeout (${Math.round(totalElapsed / 1000)}s) — completing\n`));
|
|
1452
1440
|
}
|
|
1453
1441
|
reasoningTimedOut = true;
|
|
1454
1442
|
try {
|
|
@@ -1542,7 +1530,6 @@ class InteractiveShell {
|
|
|
1542
1530
|
case 'tool.start': {
|
|
1543
1531
|
const toolName = event.toolName;
|
|
1544
1532
|
const args = event.parameters;
|
|
1545
|
-
let toolDisplay = `[${toolName}]`;
|
|
1546
1533
|
if (isHitlToolName(toolName)) {
|
|
1547
1534
|
hitlDepth += 1;
|
|
1548
1535
|
}
|
|
@@ -1552,98 +1539,34 @@ class InteractiveShell {
|
|
|
1552
1539
|
if (!toolsUsed.includes(toolName)) {
|
|
1553
1540
|
toolsUsed.push(toolName);
|
|
1554
1541
|
}
|
|
1555
|
-
const filePath = args?.['file_path'];
|
|
1556
|
-
if (filePath && (toolName
|
|
1542
|
+
const filePath = (args?.['file_path'] ?? args?.['path']);
|
|
1543
|
+
if (filePath && /edit|write|create|update/i.test(toolName)) {
|
|
1557
1544
|
if (!filesModified.includes(filePath)) {
|
|
1558
1545
|
filesModified.push(filePath);
|
|
1559
1546
|
}
|
|
1560
1547
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
}
|
|
1564
|
-
else if (toolName === 'Read' && args?.['file_path']) {
|
|
1565
|
-
toolDisplay += ` ${args['file_path']}`;
|
|
1566
|
-
}
|
|
1567
|
-
else if (toolName === 'Write' && args?.['file_path']) {
|
|
1568
|
-
toolDisplay += ` ${args['file_path']}`;
|
|
1569
|
-
}
|
|
1570
|
-
else if (toolName === 'Edit' && args?.['file_path']) {
|
|
1571
|
-
toolDisplay += ` ${args['file_path']}`;
|
|
1572
|
-
}
|
|
1573
|
-
else if (toolName === 'Search' && args?.['pattern']) {
|
|
1574
|
-
toolDisplay += ` ${args['pattern']}`;
|
|
1575
|
-
}
|
|
1576
|
-
else if (toolName === 'Grep' && args?.['pattern']) {
|
|
1577
|
-
toolDisplay += ` ${args['pattern']}`;
|
|
1578
|
-
}
|
|
1579
|
-
else if (toolName === 'WebSearch' && args?.['query']) {
|
|
1580
|
-
// Surface the query so the user can see exactly what the
|
|
1581
|
-
// agent is searching for. Without this, every web-search
|
|
1582
|
-
// turn looked like an opaque "[WebSearch]" in scrollback.
|
|
1583
|
-
toolDisplay = `🌐 WebSearch: "${String(args['query']).slice(0, 80)}"`;
|
|
1584
|
-
}
|
|
1585
|
-
else if (toolName === 'WebExtract') {
|
|
1586
|
-
const urlsArg = args?.['urls'];
|
|
1587
|
-
const urls = Array.isArray(urlsArg)
|
|
1588
|
-
? urlsArg.filter((u) => typeof u === 'string')
|
|
1589
|
-
: typeof args?.['url'] === 'string'
|
|
1590
|
-
? [args['url']]
|
|
1591
|
-
: [];
|
|
1592
|
-
const display = urls.length > 0
|
|
1593
|
-
? urls.length === 1 ? urls[0] : `${urls[0]} (+${urls.length - 1} more)`
|
|
1594
|
-
: '...';
|
|
1595
|
-
toolDisplay = `🌐 WebExtract: ${display}`;
|
|
1596
|
-
}
|
|
1548
|
+
// Claude-Code action line: `⏺ ToolName(primaryArg)`. The dim
|
|
1549
|
+
// present-tense label drives the working spinner above the prompt.
|
|
1597
1550
|
if (renderer) {
|
|
1598
|
-
renderer.addEvent('tool',
|
|
1599
|
-
}
|
|
1600
|
-
// Provide explanatory status messages for different tool types
|
|
1601
|
-
let statusMsg = '';
|
|
1602
|
-
if (toolName === 'Bash') {
|
|
1603
|
-
statusMsg = `Running: ${args?.['command'] ? String(args['command']).slice(0, 40) : '...'}`;
|
|
1551
|
+
renderer.addEvent('tool', formatToolCall(toolName, args, this.workingDir));
|
|
1604
1552
|
}
|
|
1605
|
-
|
|
1606
|
-
statusMsg = `📝 Editing file: ${args?.['file_path'] || '...'}`;
|
|
1607
|
-
}
|
|
1608
|
-
else if (toolName === 'Read') {
|
|
1609
|
-
statusMsg = `📖 Reading file: ${args?.['file_path'] || '...'}`;
|
|
1610
|
-
}
|
|
1611
|
-
else if (toolName === 'Search' || toolName === 'Grep') {
|
|
1612
|
-
statusMsg = `🔍 Searching: ${args?.['pattern'] ? String(args['pattern']).slice(0, 30) : '...'}`;
|
|
1613
|
-
}
|
|
1614
|
-
else if (toolName === 'WebSearch') {
|
|
1615
|
-
statusMsg = `🌐 Searching web: ${args?.['query'] ? String(args['query']).slice(0, 40) : '...'}`;
|
|
1616
|
-
}
|
|
1617
|
-
else if (toolName === 'WebExtract') {
|
|
1618
|
-
const urlsArg = args?.['urls'];
|
|
1619
|
-
const firstUrl = Array.isArray(urlsArg)
|
|
1620
|
-
? urlsArg.find((u) => typeof u === 'string')
|
|
1621
|
-
: typeof args?.['url'] === 'string' ? args['url'] : '...';
|
|
1622
|
-
statusMsg = `🌐 Extracting: ${String(firstUrl ?? '...').slice(0, 50)}`;
|
|
1623
|
-
}
|
|
1624
|
-
else {
|
|
1625
|
-
statusMsg = `🔧 Running ${toolName}...`;
|
|
1626
|
-
}
|
|
1627
|
-
this.promptController?.setStatusMessage(statusMsg);
|
|
1553
|
+
this.promptController?.setStatusMessage(toolActivityLabel(toolName, args, this.workingDir));
|
|
1628
1554
|
break;
|
|
1629
1555
|
}
|
|
1630
1556
|
case 'tool.complete': {
|
|
1631
1557
|
if (isHitlToolName(event.toolName)) {
|
|
1632
1558
|
hitlDepth = Math.max(0, hitlDepth - 1);
|
|
1633
1559
|
}
|
|
1634
|
-
// Clear the
|
|
1635
|
-
this.promptController?.setStatusMessage('Thinking
|
|
1560
|
+
// Clear the activity label; the agent is thinking again.
|
|
1561
|
+
this.promptController?.setStatusMessage('Thinking…');
|
|
1636
1562
|
// Reset reasoning timer after tool completes
|
|
1637
1563
|
reasoningOnlyStartTime = null;
|
|
1638
|
-
//
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
renderer.addEvent('tool', 'Done:');
|
|
1642
|
-
}
|
|
1643
|
-
// Pass full result to renderer - it handles display truncation
|
|
1644
|
-
// and stores full content for Ctrl+O expansion
|
|
1564
|
+
// Render the result as a dim ` ⎿ …` block (summarised, never a
|
|
1565
|
+
// raw multi-KB dump). Pre-formatted ⏺ blocks (editTools) pass
|
|
1566
|
+
// through with just their duplicate header stripped.
|
|
1645
1567
|
if (event.result && typeof event.result === 'string' && event.result.trim() && renderer) {
|
|
1646
|
-
|
|
1568
|
+
const params = event.parameters;
|
|
1569
|
+
renderer.addEvent('tool-result', formatToolResult(event.toolName, event.result, params));
|
|
1647
1570
|
}
|
|
1648
1571
|
break;
|
|
1649
1572
|
}
|
|
@@ -1651,10 +1574,10 @@ class InteractiveShell {
|
|
|
1651
1574
|
if (isHitlToolName(event.toolName)) {
|
|
1652
1575
|
hitlDepth = Math.max(0, hitlDepth - 1);
|
|
1653
1576
|
}
|
|
1654
|
-
|
|
1655
|
-
this.promptController?.setStatusMessage('Thinking...');
|
|
1577
|
+
this.promptController?.setStatusMessage('Thinking…');
|
|
1656
1578
|
if (renderer) {
|
|
1657
|
-
|
|
1579
|
+
// Red ` ⎿ Error: …` line, mirroring a failed tool result.
|
|
1580
|
+
renderer.addEvent('error', formatToolError(event.error));
|
|
1658
1581
|
}
|
|
1659
1582
|
break;
|
|
1660
1583
|
case 'error':
|
|
@@ -1706,7 +1629,7 @@ class InteractiveShell {
|
|
|
1706
1629
|
const reasoningElapsed = Date.now() - reasoningOnlyStartTime;
|
|
1707
1630
|
if (reasoningElapsed > PROMPT_REASONING_TIMEOUT_MS) {
|
|
1708
1631
|
if (renderer) {
|
|
1709
|
-
renderer.addEvent('response', chalk.yellow(`\
|
|
1632
|
+
renderer.addEvent('response', chalk.yellow(`\nReasoning timeout (${Math.round(reasoningElapsed / 1000)}s)\n`));
|
|
1710
1633
|
}
|
|
1711
1634
|
reasoningTimedOut = true;
|
|
1712
1635
|
}
|
|
@@ -1935,6 +1858,21 @@ class InteractiveShell {
|
|
|
1935
1858
|
this.promptController?.setStatusMessage(`HITL: ${mode}`);
|
|
1936
1859
|
setTimeout(() => this.promptController?.setStatusMessage(null), 1500);
|
|
1937
1860
|
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Shift+Tab cycled the permission mode. The hint line under the input box
|
|
1863
|
+
* already shows the active mode; this surfaces a brief one-line note in
|
|
1864
|
+
* the chat so the change is unmistakable, matching how Claude Code echoes
|
|
1865
|
+
* a mode switch.
|
|
1866
|
+
*/
|
|
1867
|
+
handlePermissionModeChange(mode) {
|
|
1868
|
+
const note = mode === 'plan'
|
|
1869
|
+
? 'plan mode — read-only; I won’t edit files or run commands until you approve a plan'
|
|
1870
|
+
: mode === 'acceptEdits'
|
|
1871
|
+
? 'accept edits on — file edits apply without the adversarial pre-flight'
|
|
1872
|
+
: 'default mode';
|
|
1873
|
+
this.promptController?.setStatusMessage(note);
|
|
1874
|
+
setTimeout(() => this.promptController?.setStatusMessage(null), 2500);
|
|
1875
|
+
}
|
|
1938
1876
|
handleCtrlC(info) {
|
|
1939
1877
|
const now = Date.now();
|
|
1940
1878
|
// Reset count if more than 2 seconds since last Ctrl+C
|