@trenchwork/erosolar 1.1.41 → 1.1.42

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.
Files changed (72) hide show
  1. package/LICENSE +16 -21
  2. package/README.md +236 -236
  3. package/agents/erosolar-code.rules.json +199 -199
  4. package/dist/bin/erosolar.js +0 -0
  5. package/dist/capabilities/enhancedGitCapability.js +63 -63
  6. package/dist/config.js +12 -12
  7. package/dist/contracts/models.schema.json +9 -9
  8. package/dist/contracts/module-schema.json +367 -367
  9. package/dist/contracts/schemas/agent-profile.schema.json +157 -157
  10. package/dist/contracts/schemas/agent-rules.schema.json +238 -238
  11. package/dist/contracts/schemas/agent-schemas.schema.json +528 -528
  12. package/dist/contracts/schemas/agent.schema.json +90 -90
  13. package/dist/contracts/schemas/tool-selection.schema.json +174 -174
  14. package/dist/contracts/tools.schema.json +42 -42
  15. package/dist/core/constants.js +7 -7
  16. package/dist/core/contextManager.js +16 -16
  17. package/dist/core/hitl.d.ts.map +1 -1
  18. package/dist/core/hitl.js +17 -16
  19. package/dist/core/hitl.js.map +1 -1
  20. package/dist/core/permissionMode.d.ts +40 -0
  21. package/dist/core/permissionMode.d.ts.map +1 -0
  22. package/dist/core/permissionMode.js +86 -0
  23. package/dist/core/permissionMode.js.map +1 -0
  24. package/dist/core/secretStore.js +1 -1
  25. package/dist/core/taskCompletionDetector.js +17 -17
  26. package/dist/core/toolRuntime.d.ts.map +1 -1
  27. package/dist/core/toolRuntime.js +21 -2
  28. package/dist/core/toolRuntime.js.map +1 -1
  29. package/dist/headless/interactiveShell.d.ts +7 -5
  30. package/dist/headless/interactiveShell.d.ts.map +1 -1
  31. package/dist/headless/interactiveShell.js +92 -159
  32. package/dist/headless/interactiveShell.js.map +1 -1
  33. package/dist/leanAgent.js +38 -38
  34. package/dist/runtime/agentSession.js +4 -4
  35. package/dist/shell/commandRegistry.js +6 -6
  36. package/dist/shell/commandRegistry.js.map +1 -1
  37. package/dist/shell/toolPresentation.d.ts +47 -0
  38. package/dist/shell/toolPresentation.d.ts.map +1 -0
  39. package/dist/shell/toolPresentation.js +260 -0
  40. package/dist/shell/toolPresentation.js.map +1 -0
  41. package/dist/shell/vimMode.js +29 -29
  42. package/dist/tools/bashTools.js +2 -2
  43. package/dist/tools/bashTools.js.map +1 -1
  44. package/dist/tools/hitlTools.js +18 -18
  45. package/dist/tools/webTools.d.ts.map +1 -1
  46. package/dist/tools/webTools.js +75 -3
  47. package/dist/tools/webTools.js.map +1 -1
  48. package/dist/ui/ink/App.d.ts +2 -0
  49. package/dist/ui/ink/App.d.ts.map +1 -1
  50. package/dist/ui/ink/App.js +2 -2
  51. package/dist/ui/ink/App.js.map +1 -1
  52. package/dist/ui/ink/ChatStatic.d.ts +6 -5
  53. package/dist/ui/ink/ChatStatic.d.ts.map +1 -1
  54. package/dist/ui/ink/ChatStatic.js +35 -10
  55. package/dist/ui/ink/ChatStatic.js.map +1 -1
  56. package/dist/ui/ink/InkPromptController.d.ts +11 -0
  57. package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
  58. package/dist/ui/ink/InkPromptController.js +50 -11
  59. package/dist/ui/ink/InkPromptController.js.map +1 -1
  60. package/dist/ui/ink/Prompt.d.ts +2 -0
  61. package/dist/ui/ink/Prompt.d.ts.map +1 -1
  62. package/dist/ui/ink/Prompt.js +8 -2
  63. package/dist/ui/ink/Prompt.js.map +1 -1
  64. package/dist/ui/ink/StatusLine.d.ts +16 -8
  65. package/dist/ui/ink/StatusLine.d.ts.map +1 -1
  66. package/dist/ui/ink/StatusLine.js +45 -4
  67. package/dist/ui/ink/StatusLine.js.map +1 -1
  68. package/dist/ui/theme.d.ts.map +1 -1
  69. package/dist/ui/theme.js +4 -6
  70. package/dist/ui/theme.js.map +1 -1
  71. package/package.json +116 -116
  72. 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 just the load-bearing status: either how to set a key, or
135
- * the active model + masked key. Pure (no chalk/ANSI, no I/O) so the
136
- * "no brand banner, key guidance kept" contract is unit-testable without a PTY.
137
- * The live renderer colorizes equivalent content; this is the source of truth
138
- * for WHICH lines appear.
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
- const lines = ['', ...(input.updateLines ?? [])];
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 - just the essentials
399
- const welcomeLines = [
400
- '',
401
- ...updateLines,
402
- '',
403
- ];
404
- if (!hasApiKey) {
405
- // Show API key setup instructions
406
- welcomeLines.push(chalk.yellow(' ⚠ No API key configured'), '', chalk.dim(' Get your key: ') + chalk.cyan('https://platform.deepseek.com/'), chalk.dim(' Set your key: ') + chalk.hex('#ffb142')('/key YOUR_API_KEY'), '');
407
- }
408
- else {
409
- const maskedKey = maskApiKey(apiKey);
410
- welcomeLines.push(chalk.dim(` ${this.profileConfig.model} · ${this.profileConfig.provider}`), chalk.dim(' Key: ') + chalk.green(maskedKey) + chalk.dim(' · /help for commands'), '');
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('🔐 Sudo password required'));
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: '🤖 Select Model' }, (selected) => {
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: '🔑 API Keys - Select to Configure' }, (selected) => {
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 /stats /keys /clear /exit');
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,7 +1268,6 @@ 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'),
@@ -1297,42 +1316,6 @@ class InteractiveShell {
1297
1316
  this.promptController.setInlinePanel(lines);
1298
1317
  this.scheduleInlinePanelDismiss();
1299
1318
  }
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
1319
  /**
1337
1320
  * Auto-dismiss inline panel after timeout or on next input.
1338
1321
  */
@@ -1428,11 +1411,11 @@ class InteractiveShell {
1428
1411
  // Check for timeout marker
1429
1412
  if (eventOrTimeout && typeof eventOrTimeout === 'object' && '__timeout' in eventOrTimeout) {
1430
1413
  if (hitlDepth > 0) {
1431
- this.promptController?.setStatusMessage('Waiting for human decision...');
1414
+ this.promptController?.setStatusMessage('Waiting for human decision');
1432
1415
  continue;
1433
1416
  }
1434
1417
  stepTimedOut = true;
1435
- this.promptController?.setStatusMessage(`⏱ Step timeout (${PROMPT_STEP_TIMEOUT_MS / 1000}s) - completing response`);
1418
+ this.promptController?.setStatusMessage(`Step timeout (${PROMPT_STEP_TIMEOUT_MS / 1000}s) completing response`);
1436
1419
  // Cancel the controller so the underlying agent stops generating
1437
1420
  // events that would never be consumed. Without this the spinner
1438
1421
  // can keep ticking against a "ghost" run after the for-await
@@ -1448,7 +1431,7 @@ class InteractiveShell {
1448
1431
  const totalElapsed = Date.now() - promptStartTime;
1449
1432
  if (!hasReceivedMeaningfulContent && totalElapsed > TOTAL_PROMPT_TIMEOUT_MS) {
1450
1433
  if (renderer) {
1451
- renderer.addEvent('response', chalk.yellow(`\n⏱ Response timeout (${Math.round(totalElapsed / 1000)}s) - completing\n`));
1434
+ renderer.addEvent('response', chalk.yellow(`\nResponse timeout (${Math.round(totalElapsed / 1000)}s) completing\n`));
1452
1435
  }
1453
1436
  reasoningTimedOut = true;
1454
1437
  try {
@@ -1542,7 +1525,6 @@ class InteractiveShell {
1542
1525
  case 'tool.start': {
1543
1526
  const toolName = event.toolName;
1544
1527
  const args = event.parameters;
1545
- let toolDisplay = `[${toolName}]`;
1546
1528
  if (isHitlToolName(toolName)) {
1547
1529
  hitlDepth += 1;
1548
1530
  }
@@ -1552,98 +1534,34 @@ class InteractiveShell {
1552
1534
  if (!toolsUsed.includes(toolName)) {
1553
1535
  toolsUsed.push(toolName);
1554
1536
  }
1555
- const filePath = args?.['file_path'];
1556
- if (filePath && (toolName === 'Write' || toolName === 'Edit')) {
1537
+ const filePath = (args?.['file_path'] ?? args?.['path']);
1538
+ if (filePath && /edit|write|create|update/i.test(toolName)) {
1557
1539
  if (!filesModified.includes(filePath)) {
1558
1540
  filesModified.push(filePath);
1559
1541
  }
1560
1542
  }
1561
- if (toolName === 'Bash' && args?.['command']) {
1562
- toolDisplay = `Running: $ ${args['command']}`;
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
- }
1543
+ // Claude-Code action line: `⏺ ToolName(primaryArg)`. The dim
1544
+ // present-tense label drives the working spinner above the prompt.
1597
1545
  if (renderer) {
1598
- renderer.addEvent('tool', toolDisplay);
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) : '...'}`;
1546
+ renderer.addEvent('tool', formatToolCall(toolName, args, this.workingDir));
1604
1547
  }
1605
- else if (toolName === 'Edit' || toolName === 'Write') {
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);
1548
+ this.promptController?.setStatusMessage(toolActivityLabel(toolName, args, this.workingDir));
1628
1549
  break;
1629
1550
  }
1630
1551
  case 'tool.complete': {
1631
1552
  if (isHitlToolName(event.toolName)) {
1632
1553
  hitlDepth = Math.max(0, hitlDepth - 1);
1633
1554
  }
1634
- // Clear the "Running X..." status since tool is complete
1635
- this.promptController?.setStatusMessage('Thinking...');
1555
+ // Clear the activity label; the agent is thinking again.
1556
+ this.promptController?.setStatusMessage('Thinking');
1636
1557
  // Reset reasoning timer after tool completes
1637
1558
  reasoningOnlyStartTime = null;
1638
- // Show "Done:" prefix for Bash completions
1639
- const isBash = event.toolName === 'Bash';
1640
- if (isBash && renderer) {
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
1559
+ // Render the result as a dim ` ⎿ …` block (summarised, never a
1560
+ // raw multi-KB dump). Pre-formatted ⏺ blocks (editTools) pass
1561
+ // through with just their duplicate header stripped.
1645
1562
  if (event.result && typeof event.result === 'string' && event.result.trim() && renderer) {
1646
- renderer.addEvent('tool-result', event.result);
1563
+ const params = event.parameters;
1564
+ renderer.addEvent('tool-result', formatToolResult(event.toolName, event.result, params));
1647
1565
  }
1648
1566
  break;
1649
1567
  }
@@ -1651,10 +1569,10 @@ class InteractiveShell {
1651
1569
  if (isHitlToolName(event.toolName)) {
1652
1570
  hitlDepth = Math.max(0, hitlDepth - 1);
1653
1571
  }
1654
- // Clear the "Running X..." status since tool errored
1655
- this.promptController?.setStatusMessage('Thinking...');
1572
+ this.promptController?.setStatusMessage('Thinking…');
1656
1573
  if (renderer) {
1657
- renderer.addEvent('error', event.error);
1574
+ // Red ` ⎿ Error: …` line, mirroring a failed tool result.
1575
+ renderer.addEvent('error', formatToolError(event.error));
1658
1576
  }
1659
1577
  break;
1660
1578
  case 'error':
@@ -1706,7 +1624,7 @@ class InteractiveShell {
1706
1624
  const reasoningElapsed = Date.now() - reasoningOnlyStartTime;
1707
1625
  if (reasoningElapsed > PROMPT_REASONING_TIMEOUT_MS) {
1708
1626
  if (renderer) {
1709
- renderer.addEvent('response', chalk.yellow(`\n⏱ Reasoning timeout (${Math.round(reasoningElapsed / 1000)}s)\n`));
1627
+ renderer.addEvent('response', chalk.yellow(`\nReasoning timeout (${Math.round(reasoningElapsed / 1000)}s)\n`));
1710
1628
  }
1711
1629
  reasoningTimedOut = true;
1712
1630
  }
@@ -1935,6 +1853,21 @@ class InteractiveShell {
1935
1853
  this.promptController?.setStatusMessage(`HITL: ${mode}`);
1936
1854
  setTimeout(() => this.promptController?.setStatusMessage(null), 1500);
1937
1855
  }
1856
+ /**
1857
+ * Shift+Tab cycled the permission mode. The hint line under the input box
1858
+ * already shows the active mode; this surfaces a brief one-line note in
1859
+ * the chat so the change is unmistakable, matching how Claude Code echoes
1860
+ * a mode switch.
1861
+ */
1862
+ handlePermissionModeChange(mode) {
1863
+ const note = mode === 'plan'
1864
+ ? 'plan mode — read-only; I won’t edit files or run commands until you approve a plan'
1865
+ : mode === 'acceptEdits'
1866
+ ? 'accept edits on — file edits apply without the adversarial pre-flight'
1867
+ : 'default mode';
1868
+ this.promptController?.setStatusMessage(note);
1869
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2500);
1870
+ }
1938
1871
  handleCtrlC(info) {
1939
1872
  const now = Date.now();
1940
1873
  // Reset count if more than 2 seconds since last Ctrl+C