erosolar-cli 2.1.168 → 2.1.170

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 (133) hide show
  1. package/README.md +1 -1
  2. package/agents/erosolar-code.rules.json +2 -2
  3. package/agents/general.rules.json +21 -3
  4. package/dist/capabilities/statusCapability.js +2 -2
  5. package/dist/capabilities/statusCapability.js.map +1 -1
  6. package/dist/contracts/agent-schemas.json +5 -5
  7. package/dist/core/agent.d.ts +83 -24
  8. package/dist/core/agent.d.ts.map +1 -1
  9. package/dist/core/agent.js +499 -248
  10. package/dist/core/agent.js.map +1 -1
  11. package/dist/core/preferences.d.ts +1 -0
  12. package/dist/core/preferences.d.ts.map +1 -1
  13. package/dist/core/preferences.js +8 -1
  14. package/dist/core/preferences.js.map +1 -1
  15. package/dist/core/reliabilityPrompt.d.ts +9 -0
  16. package/dist/core/reliabilityPrompt.d.ts.map +1 -0
  17. package/dist/core/reliabilityPrompt.js +31 -0
  18. package/dist/core/reliabilityPrompt.js.map +1 -0
  19. package/dist/core/schemaValidator.js +3 -3
  20. package/dist/core/schemaValidator.js.map +1 -1
  21. package/dist/core/toolPreconditions.d.ts +0 -11
  22. package/dist/core/toolPreconditions.d.ts.map +1 -1
  23. package/dist/core/toolPreconditions.js +33 -164
  24. package/dist/core/toolPreconditions.js.map +1 -1
  25. package/dist/core/toolRuntime.d.ts.map +1 -1
  26. package/dist/core/toolRuntime.js +9 -114
  27. package/dist/core/toolRuntime.js.map +1 -1
  28. package/dist/core/updateChecker.d.ts +61 -1
  29. package/dist/core/updateChecker.d.ts.map +1 -1
  30. package/dist/core/updateChecker.js +147 -3
  31. package/dist/core/updateChecker.js.map +1 -1
  32. package/dist/headless/headlessApp.d.ts.map +1 -1
  33. package/dist/headless/headlessApp.js +0 -39
  34. package/dist/headless/headlessApp.js.map +1 -1
  35. package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
  36. package/dist/plugins/tools/nodeDefaults.js +0 -2
  37. package/dist/plugins/tools/nodeDefaults.js.map +1 -1
  38. package/dist/providers/openaiResponsesProvider.d.ts.map +1 -1
  39. package/dist/providers/openaiResponsesProvider.js +79 -74
  40. package/dist/providers/openaiResponsesProvider.js.map +1 -1
  41. package/dist/runtime/agentController.d.ts.map +1 -1
  42. package/dist/runtime/agentController.js +6 -3
  43. package/dist/runtime/agentController.js.map +1 -1
  44. package/dist/runtime/agentSession.d.ts +0 -2
  45. package/dist/runtime/agentSession.d.ts.map +1 -1
  46. package/dist/runtime/agentSession.js +2 -2
  47. package/dist/runtime/agentSession.js.map +1 -1
  48. package/dist/shell/interactiveShell.d.ts +11 -12
  49. package/dist/shell/interactiveShell.d.ts.map +1 -1
  50. package/dist/shell/interactiveShell.js +269 -193
  51. package/dist/shell/interactiveShell.js.map +1 -1
  52. package/dist/shell/systemPrompt.d.ts.map +1 -1
  53. package/dist/shell/systemPrompt.js +4 -15
  54. package/dist/shell/systemPrompt.js.map +1 -1
  55. package/dist/subagents/taskRunner.js +2 -1
  56. package/dist/subagents/taskRunner.js.map +1 -1
  57. package/dist/tools/bashTools.d.ts.map +1 -1
  58. package/dist/tools/bashTools.js +101 -8
  59. package/dist/tools/bashTools.js.map +1 -1
  60. package/dist/tools/diffUtils.d.ts +8 -2
  61. package/dist/tools/diffUtils.d.ts.map +1 -1
  62. package/dist/tools/diffUtils.js +72 -13
  63. package/dist/tools/diffUtils.js.map +1 -1
  64. package/dist/tools/grepTools.d.ts.map +1 -1
  65. package/dist/tools/grepTools.js +10 -2
  66. package/dist/tools/grepTools.js.map +1 -1
  67. package/dist/tools/searchTools.d.ts.map +1 -1
  68. package/dist/tools/searchTools.js +4 -2
  69. package/dist/tools/searchTools.js.map +1 -1
  70. package/dist/ui/PromptController.d.ts +2 -3
  71. package/dist/ui/PromptController.d.ts.map +1 -1
  72. package/dist/ui/PromptController.js +2 -3
  73. package/dist/ui/PromptController.js.map +1 -1
  74. package/dist/ui/ShellUIAdapter.d.ts +71 -18
  75. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  76. package/dist/ui/ShellUIAdapter.js +237 -139
  77. package/dist/ui/ShellUIAdapter.js.map +1 -1
  78. package/dist/ui/UnifiedUIController.d.ts +0 -1
  79. package/dist/ui/UnifiedUIController.d.ts.map +1 -1
  80. package/dist/ui/UnifiedUIController.js +0 -1
  81. package/dist/ui/UnifiedUIController.js.map +1 -1
  82. package/dist/ui/UnifiedUIRenderer.d.ts +122 -7
  83. package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
  84. package/dist/ui/UnifiedUIRenderer.js +823 -130
  85. package/dist/ui/UnifiedUIRenderer.js.map +1 -1
  86. package/dist/ui/animatedStatus.d.ts +129 -0
  87. package/dist/ui/animatedStatus.d.ts.map +1 -0
  88. package/dist/ui/animatedStatus.js +384 -0
  89. package/dist/ui/animatedStatus.js.map +1 -0
  90. package/dist/ui/display.d.ts +13 -48
  91. package/dist/ui/display.d.ts.map +1 -1
  92. package/dist/ui/display.js +22 -105
  93. package/dist/ui/display.js.map +1 -1
  94. package/dist/ui/shortcutsHelp.d.ts.map +1 -1
  95. package/dist/ui/shortcutsHelp.js +0 -1
  96. package/dist/ui/shortcutsHelp.js.map +1 -1
  97. package/dist/ui/unified/index.d.ts +1 -1
  98. package/dist/ui/unified/index.d.ts.map +1 -1
  99. package/dist/ui/unified/index.js +0 -2
  100. package/dist/ui/unified/index.js.map +1 -1
  101. package/package.json +1 -2
  102. package/dist/StringUtils.d.ts +0 -8
  103. package/dist/StringUtils.d.ts.map +0 -1
  104. package/dist/StringUtils.js +0 -11
  105. package/dist/StringUtils.js.map +0 -1
  106. package/dist/core/aiFlowSupervisor.d.ts +0 -44
  107. package/dist/core/aiFlowSupervisor.d.ts.map +0 -1
  108. package/dist/core/aiFlowSupervisor.js +0 -299
  109. package/dist/core/aiFlowSupervisor.js.map +0 -1
  110. package/dist/core/cliTestHarness.d.ts +0 -200
  111. package/dist/core/cliTestHarness.d.ts.map +0 -1
  112. package/dist/core/cliTestHarness.js +0 -549
  113. package/dist/core/cliTestHarness.js.map +0 -1
  114. package/dist/core/testUtils.d.ts +0 -121
  115. package/dist/core/testUtils.d.ts.map +0 -1
  116. package/dist/core/testUtils.js +0 -235
  117. package/dist/core/testUtils.js.map +0 -1
  118. package/dist/core/toolValidation.d.ts +0 -116
  119. package/dist/core/toolValidation.d.ts.map +0 -1
  120. package/dist/core/toolValidation.js +0 -282
  121. package/dist/core/toolValidation.js.map +0 -1
  122. package/dist/ui/compactRenderer.d.ts +0 -139
  123. package/dist/ui/compactRenderer.d.ts.map +0 -1
  124. package/dist/ui/compactRenderer.js +0 -398
  125. package/dist/ui/compactRenderer.js.map +0 -1
  126. package/dist/ui/streamingFormatter.d.ts +0 -30
  127. package/dist/ui/streamingFormatter.d.ts.map +0 -1
  128. package/dist/ui/streamingFormatter.js +0 -91
  129. package/dist/ui/streamingFormatter.js.map +0 -1
  130. package/dist/utils/errorUtils.d.ts +0 -16
  131. package/dist/utils/errorUtils.d.ts.map +0 -1
  132. package/dist/utils/errorUtils.js +0 -66
  133. package/dist/utils/errorUtils.js.map +0 -1
@@ -13,15 +13,13 @@ import { InterruptPriority } from './interrupts/InterruptManager.js';
13
13
  import { getTerminalColumns } from './layout.js';
14
14
  import { theme, icons } from './theme.js';
15
15
  import { UIUpdateCoordinator } from './orchestration/UIUpdateCoordinator.js';
16
- import { formatRichContent } from './richText.js';
17
16
  export class ShellUIAdapter {
18
17
  uiController;
19
18
  display;
20
19
  config;
21
- isProcessing = false;
22
- contextUsage = 0;
23
20
  fileChangeCallback;
24
21
  _toolStatusCallback;
22
+ _activityCallback;
25
23
  profileSwitcherTimeout;
26
24
  updateCoordinator;
27
25
  // Track tool operations for compact rendering
@@ -38,6 +36,16 @@ export class ShellUIAdapter {
38
36
  activeToolStatus = null;
39
37
  activeToolHeartbeatId = null;
40
38
  statusSpinnerIndex = 0;
39
+ // Store last tool result for expansion with ctrl+o
40
+ lastToolResult = null;
41
+ // Scrolling output buffer - shows only the last N lines of tool output in-place
42
+ maxScrollingLines = 3;
43
+ scrollingOutputBuffer = new Map();
44
+ scrollingPanelOwner = null;
45
+ // Scrolling tool call display - shows only the last N tool calls, scrolling older ones up
46
+ maxVisibleToolCalls = 3;
47
+ recentToolCalls = [];
48
+ toolCallDisplayedCount = 0;
41
49
  constructor(writeStream, display, config = {}, updateCoordinator) {
42
50
  this.display = display;
43
51
  this.config = {
@@ -138,6 +146,12 @@ export class ShellUIAdapter {
138
146
  else {
139
147
  this.logToolProgress(call, progress);
140
148
  }
149
+ // Update activity line with streaming output from bash commands
150
+ if (this._activityCallback && progress.message) {
151
+ // Show the streaming line as the activity (e.g., npm install progress)
152
+ const activityText = progress.message.slice(0, 60);
153
+ this._activityCallback(activityText);
154
+ }
141
155
  // Update status bar with progress - use in-place update for supported terminals
142
156
  if (this._toolStatusCallback) {
143
157
  const description = this.getToolActionDescription(call, progress);
@@ -167,12 +181,10 @@ export class ShellUIAdapter {
167
181
  const now = Date.now();
168
182
  // Complete the operation tracking
169
183
  const op = this.pendingOperations.get(call.id);
170
- let durationMs;
171
184
  if (op) {
172
185
  op.status = 'success';
173
186
  op.completedAt = now;
174
187
  op.summary = this.extractResultSummary(call, output);
175
- durationMs = op.startedAt ? Math.max(0, now - op.startedAt) : undefined;
176
188
  this.pendingOperations.delete(call.id);
177
189
  this.completedOperations.push(op);
178
190
  // Keep only recent completed operations
@@ -191,19 +203,22 @@ export class ShellUIAdapter {
191
203
  this.fileChangeCallback(fileChange.path, fileChange.type, fileChange.additions, fileChange.removals);
192
204
  }
193
205
  }
194
- const renderedRich = this.maybeRenderRichToolResult(call, output, true, durationMs);
195
- // Use compact display for quick single-result tools and structured edit output
196
- if (!renderedRich) {
197
- if (call.name === 'Edit' || call.name === 'edit_file') {
198
- this.displayEditResult(call, output, true);
199
- }
200
- else if (this.compactDisplayMode && this.isCompactTool(call.name)) {
201
- this.displayCompactResult(call, output, true);
202
- }
203
- else {
204
- // Erosolar-CLI style: Display tool result with ⎿ prefix
205
- this.displayToolResultSummary(call, output, true);
206
- }
206
+ // Store result for expansion with ctrl+o
207
+ this.lastToolResult = {
208
+ toolName: call.name,
209
+ output,
210
+ timestamp: Date.now(),
211
+ };
212
+ // Claude Code style: compact display for all tools
213
+ if (call.name === 'Edit' || call.name === 'edit_file') {
214
+ this.displayEditResult(call, output, true);
215
+ }
216
+ else if (this.compactDisplayMode && this.isCompactTool(call.name)) {
217
+ this.displayCompactResult(call, output, true);
218
+ }
219
+ else {
220
+ // Display tool result with ⎿ prefix
221
+ this.displayToolResultSummary(call, output, true);
207
222
  }
208
223
  // Surface any captured preflight warnings in a structured block
209
224
  this.flushToolWarnings(call.id, call.name);
@@ -217,12 +232,10 @@ export class ShellUIAdapter {
217
232
  onToolError: (call, message) => {
218
233
  // Complete the operation tracking with error
219
234
  const op = this.pendingOperations.get(call.id);
220
- let durationMs;
221
235
  if (op) {
222
236
  op.status = 'error';
223
237
  op.completedAt = Date.now();
224
238
  op.summary = message;
225
- durationMs = op.startedAt ? Math.max(0, (op.completedAt ?? Date.now()) - op.startedAt) : undefined;
226
239
  this.pendingOperations.delete(call.id);
227
240
  this.completedOperations.push(op);
228
241
  }
@@ -230,19 +243,16 @@ export class ShellUIAdapter {
230
243
  this.logExploreCompletion(call.id, false);
231
244
  }
232
245
  this.clearToolState(call.id);
233
- const renderedRich = this.maybeRenderRichToolResult(call, message, false, durationMs);
234
- // Use compact display for errors too
235
- if (!renderedRich) {
236
- if (call.name === 'Edit' || call.name === 'edit_file') {
237
- this.displayEditResult(call, message, false);
238
- }
239
- else if (this.compactDisplayMode && this.isCompactTool(call.name)) {
240
- this.displayCompactResult(call, message, false);
241
- }
242
- else {
243
- // Erosolar-CLI style: Display error with ⎿ prefix (error color)
244
- this.displayToolResultSummary(call, message, false);
245
- }
246
+ // Claude Code style: compact display for errors too
247
+ if (call.name === 'Edit' || call.name === 'edit_file') {
248
+ this.displayEditResult(call, message, false);
249
+ }
250
+ else if (this.compactDisplayMode && this.isCompactTool(call.name)) {
251
+ this.displayCompactResult(call, message, false);
252
+ }
253
+ else {
254
+ // Display error with ⎿ prefix (error color)
255
+ this.displayToolResultSummary(call, message, false);
246
256
  }
247
257
  this.flushToolWarnings(call.id, call.name);
248
258
  // Clear status bar after showing error
@@ -310,17 +320,27 @@ export class ShellUIAdapter {
310
320
  * Shows the top two tools and how many more were used.
311
321
  */
312
322
  showToolUsageSummary() {
323
+ // Compute summary for metadata but don't display it
324
+ // The tool calls are already visible in the conversation flow
313
325
  this.lastToolUsageSummary = this.formatToolUsageSummary();
314
326
  this.lastToolUsageSummaryPlain = this.formatToolUsageSummary({ plain: true });
315
- const summary = this.lastToolUsageSummary;
316
- if (!summary) {
317
- return;
318
- }
319
- this.display.stream(`\n${summary}\n`);
327
+ // Don't stream the summary - it's redundant after the assistant response
320
328
  }
321
329
  getToolUsageSummary(options = {}) {
322
330
  return options.plain ? this.lastToolUsageSummaryPlain : this.lastToolUsageSummary;
323
331
  }
332
+ /**
333
+ * Get the last tool result for expansion with ctrl+o
334
+ */
335
+ getLastToolResult() {
336
+ return this.lastToolResult;
337
+ }
338
+ /**
339
+ * Clear the stored tool result (e.g., after expansion)
340
+ */
341
+ clearLastToolResult() {
342
+ this.lastToolResult = null;
343
+ }
324
344
  formatToolUsageSummary(options = {}) {
325
345
  const plain = options.plain === true;
326
346
  if (this.toolUsageCounts.size === 0) {
@@ -439,7 +459,6 @@ export class ShellUIAdapter {
439
459
  case 'Write':
440
460
  case 'write_file': {
441
461
  const path = this.extractPath(args, ['file_path', 'path']);
442
- const lines = this.countLines(output);
443
462
  return path ? `${this.truncatePath(path, 40)} written` : 'file written';
444
463
  }
445
464
  case 'Edit':
@@ -504,8 +523,8 @@ export class ShellUIAdapter {
504
523
  return;
505
524
  }
506
525
  const prefix = success ? theme.success(icons.subaction) : theme.error(icons.subaction);
507
- const toolLabel = theme.tool(call.name);
508
- this.display.stream(` ${prefix} ${toolLabel} ${summary}\n\n`);
526
+ // Just show the result summary - tool name was already shown on the call line
527
+ this.display.stream(` ${prefix} ${summary}\n`);
509
528
  }
510
529
  /**
511
530
  * Display edit results with full colored diff output.
@@ -523,12 +542,10 @@ export class ShellUIAdapter {
523
542
  if (success) {
524
543
  const diffSection = this.extractEditDiffSection(output);
525
544
  if (diffSection) {
526
- this.display.stream(`${diffSection}\n\n`);
527
- return;
545
+ // Show diff with single trailing newline
546
+ this.display.stream(`${diffSection}\n`);
528
547
  }
529
548
  }
530
- // Add spacing when no diff section is available
531
- this.display.stream('\n');
532
549
  }
533
550
  logToolProgress(call, progress) {
534
551
  const description = this.getToolActionDescription(call, progress);
@@ -551,12 +568,97 @@ export class ShellUIAdapter {
551
568
  }
552
569
  const separator = theme.ui.muted(' — ');
553
570
  const line = parts.filter(Boolean).join(separator);
554
- const last = this.toolProgressSnapshots.get(call.id);
555
- if (line && line !== last) {
556
- // Single trailing newline - no leading newline to avoid gaps
557
- this.display.stream(`${line}\n`);
558
- this.toolProgressSnapshots.set(call.id, line);
571
+ if (!line)
572
+ return;
573
+ // Use scrolling output for ALL tools on TTY - show only last N lines, updating in place
574
+ // This prevents flooding the terminal with hundreds of progress lines
575
+ if (process.stdout.isTTY) {
576
+ this.appendScrollingOutput(call.id, line);
577
+ }
578
+ else {
579
+ // Non-TTY: original streaming behavior (for log files, pipes, etc.)
580
+ const last = this.toolProgressSnapshots.get(call.id);
581
+ if (line !== last) {
582
+ this.display.stream(`${line}\n`);
583
+ this.toolProgressSnapshots.set(call.id, line);
584
+ }
585
+ }
586
+ }
587
+ /**
588
+ * Append a line to scrolling output buffer and update display in-place
589
+ * Shows only the last maxScrollingLines, replacing previous output
590
+ */
591
+ appendScrollingOutput(callId, line) {
592
+ let buffer = this.scrollingOutputBuffer.get(callId);
593
+ if (!buffer) {
594
+ buffer = { lines: [], displayedLineCount: 0 };
595
+ this.scrollingOutputBuffer.set(callId, buffer);
559
596
  }
597
+ // Add line to buffer
598
+ buffer.lines.push(line);
599
+ // Keep only the last maxScrollingLines
600
+ while (buffer.lines.length > this.maxScrollingLines) {
601
+ buffer.lines.shift();
602
+ }
603
+ // Prefer inline panel (scroll box) when unified renderer is active
604
+ if (this.shouldUseInlineScrollBox()) {
605
+ this.scrollingPanelOwner = callId;
606
+ const panelLines = this.buildScrollingPanelLines(buffer.lines);
607
+ this.display.showInlinePanel(panelLines);
608
+ buffer.displayedLineCount = 0;
609
+ return;
610
+ }
611
+ // Move cursor up to overwrite previous lines (if any were displayed)
612
+ if (buffer.displayedLineCount > 0) {
613
+ process.stdout.write(`\x1b[${buffer.displayedLineCount}A`);
614
+ }
615
+ // Clear and rewrite the scrolling region
616
+ const linesToShow = buffer.lines;
617
+ for (let i = 0; i < linesToShow.length; i++) {
618
+ process.stdout.write(`\r\x1b[K${linesToShow[i]}\n`);
619
+ }
620
+ // Track how many lines we displayed for next update
621
+ buffer.displayedLineCount = linesToShow.length;
622
+ }
623
+ /**
624
+ * Build the pinned scroll-box contents for live tool output.
625
+ */
626
+ buildScrollingPanelLines(lines) {
627
+ if (!lines.length) {
628
+ return [];
629
+ }
630
+ const header = theme.ui.muted(`${icons.action} live output (latest ${Math.min(lines.length, this.maxScrollingLines)})`);
631
+ const indented = lines.map(text => ` ${text}`);
632
+ return [header, ...indented];
633
+ }
634
+ /**
635
+ * Use inline scroll box only when an interactive renderer is active.
636
+ */
637
+ shouldUseInlineScrollBox() {
638
+ return this.display.hasRenderer() && process.stdout.isTTY && !process.env['CI'];
639
+ }
640
+ /**
641
+ * Finalize scrolling output - called when tool completes
642
+ * Clears the scrolling progress lines so only the final result is shown
643
+ */
644
+ finalizeScrollingOutput(callId) {
645
+ if (this.scrollingPanelOwner === callId && this.shouldUseInlineScrollBox()) {
646
+ this.display.clearInlinePanel();
647
+ this.scrollingPanelOwner = null;
648
+ this.scrollingOutputBuffer.delete(callId);
649
+ return;
650
+ }
651
+ const buffer = this.scrollingOutputBuffer.get(callId);
652
+ if (buffer && buffer.displayedLineCount > 0 && process.stdout.isTTY) {
653
+ // Move cursor up and clear all the progress lines
654
+ process.stdout.write(`\x1b[${buffer.displayedLineCount}A`);
655
+ for (let i = 0; i < buffer.displayedLineCount; i++) {
656
+ process.stdout.write(`\r\x1b[K\n`);
657
+ }
658
+ // Move cursor back up to where progress started
659
+ process.stdout.write(`\x1b[${buffer.displayedLineCount}A`);
660
+ }
661
+ this.scrollingOutputBuffer.delete(callId);
560
662
  }
561
663
  logExploreProgress(call, progress) {
562
664
  const previous = this.exploreProgressState.get(call.id) ?? { lastCurrent: 0 };
@@ -570,8 +672,14 @@ export class ShellUIAdapter {
570
672
  }
571
673
  const line = parts.join(theme.ui.muted(' • '));
572
674
  if (line && line !== previous.lastLine) {
573
- // Single trailing newline - no leading newline to avoid gaps
574
- this.display.stream(`${line}\n`);
675
+ // Use in-place update for TTY - single line that updates without scrolling
676
+ if (process.stdout.isTTY && previous.lastLine) {
677
+ // Move cursor up one line and clear it, then write new line
678
+ process.stdout.write(`\x1b[1A\r\x1b[K${line}\n`);
679
+ }
680
+ else {
681
+ this.display.stream(`${line}\n`);
682
+ }
575
683
  }
576
684
  this.exploreProgressState.set(call.id, {
577
685
  total,
@@ -585,6 +693,10 @@ export class ShellUIAdapter {
585
693
  const summary = success
586
694
  ? `${theme.success('✓')} Indexed ${total} file${total === 1 ? '' : 's'}`
587
695
  : `${theme.error('✗')} Explore interrupted`;
696
+ // Clear the progress line before showing summary (if TTY and had progress)
697
+ if (process.stdout.isTTY && state?.lastLine) {
698
+ process.stdout.write(`\x1b[1A\r\x1b[K`);
699
+ }
588
700
  this.display.stream(`${summary}\n`);
589
701
  }
590
702
  /**
@@ -594,12 +706,13 @@ export class ShellUIAdapter {
594
706
  clearToolState(callId) {
595
707
  this.toolProgressSnapshots.delete(callId);
596
708
  this.exploreProgressState.delete(callId);
709
+ this.finalizeScrollingOutput(callId);
597
710
  }
598
711
  /**
599
712
  * Display tool call start with Erosolar-CLI style prefix
600
713
  * - ✢ for Task/agent tools (active task indicator)
601
714
  * - ⏺ for other tools
602
- * Routes through display module to work with scroll regions
715
+ * Uses scrolling display to show only the last N tool calls
603
716
  */
604
717
  displayToolCallStart(call, isCacheHit = false) {
605
718
  const args = call.arguments || {};
@@ -622,8 +735,41 @@ export class ShellUIAdapter {
622
735
  }
623
736
  // Erosolar-CLI style: bullet + optional task marker + ToolName(args)
624
737
  const line = `${bullet}${taskMarker ? taskMarker : ''} ${displayName}${argsStr}${cacheHint}`;
625
- // Stream with spacing to keep it as a distinct event
626
- this.display.stream(`\n${line}\n`);
738
+ // Use scrolling display for tool calls - shows only recent calls
739
+ this.appendToolCallDisplay(call.id, line);
740
+ }
741
+ /**
742
+ * Append a tool call to the scrolling display, keeping only the last N visible.
743
+ * Older tool calls scroll up and disappear, showing only recent activity.
744
+ */
745
+ appendToolCallDisplay(callId, line) {
746
+ // Add to recent tool calls
747
+ this.recentToolCalls.push({ line, id: callId });
748
+ // Keep only the last maxVisibleToolCalls
749
+ while (this.recentToolCalls.length > this.maxVisibleToolCalls) {
750
+ this.recentToolCalls.shift();
751
+ }
752
+ // On TTY, use in-place updating to show only recent tool calls
753
+ if (process.stdout.isTTY && this.toolCallDisplayedCount > 0) {
754
+ // Move cursor up to overwrite previous tool call lines
755
+ const moveUp = `\x1b[${this.toolCallDisplayedCount}A`;
756
+ // Clear from cursor to end of screen
757
+ const clearDown = '\x1b[J';
758
+ process.stdout.write(moveUp + clearDown);
759
+ }
760
+ // Display all recent tool calls
761
+ for (const toolCall of this.recentToolCalls) {
762
+ this.display.stream(`${toolCall.line}\n`);
763
+ }
764
+ // Track how many lines we displayed
765
+ this.toolCallDisplayedCount = this.recentToolCalls.length;
766
+ }
767
+ /**
768
+ * Clear tool call display state when processing completes
769
+ */
770
+ clearToolCallDisplay() {
771
+ this.recentToolCalls = [];
772
+ this.toolCallDisplayedCount = 0;
627
773
  }
628
774
  /**
629
775
  * Format tool arguments inline for display (Erosolar-CLI style)
@@ -772,8 +918,8 @@ export class ShellUIAdapter {
772
918
  * Start processing a request
773
919
  */
774
920
  startProcessing(message = 'Working on your request') {
775
- this.isProcessing = true;
776
921
  this.resetToolUsageSummary();
922
+ this.clearToolCallDisplay(); // Clear previous tool call display
777
923
  this.uiController.startProcessing();
778
924
  this.uiController.setBaseStatus(message, 'info');
779
925
  }
@@ -781,20 +927,17 @@ export class ShellUIAdapter {
781
927
  * End processing
782
928
  */
783
929
  endProcessing(message = 'Ready for prompts') {
784
- this.isProcessing = false;
785
930
  this.uiController.endProcessing();
786
931
  this.showToolUsageSummary();
787
932
  this.uiController.setBaseStatus(message, 'success');
933
+ // Clear tool call scrolling state for next request
934
+ this.clearToolCallDisplay();
788
935
  }
789
936
  /**
790
- * Update context usage
791
- * NOTE: This only tracks the value internally. The actual context display
792
- * is handled by display.showStatusLine() which shows "Session Xm • Context Y% used • Ready"
793
- * We don't push a status override here to avoid duplicate context lines.
937
+ * Update context usage (no-op - status handled by display.showStatusLine)
794
938
  */
795
- updateContextUsage(percentage) {
796
- this.contextUsage = percentage;
797
- // Status updates are internal; avoid forcing overlay rerender
939
+ updateContextUsage(_percentage) {
940
+ // No-op - context display is handled by display.showStatusLine()
798
941
  }
799
942
  /**
800
943
  * Show a user interrupt
@@ -814,7 +957,7 @@ export class ShellUIAdapter {
814
957
  /**
815
958
  * Show profile switcher (Shift+Tab)
816
959
  */
817
- showProfileSwitcher(profiles, currentProfile) {
960
+ showProfileSwitcher(profiles, _currentProfile) {
818
961
  this.updateCoordinator.enqueue({
819
962
  lane: 'overlay',
820
963
  description: 'profile-switcher',
@@ -1026,6 +1169,12 @@ export class ShellUIAdapter {
1026
1169
  callback(this.buildRunningStatusLine(this.activeToolStatus.description, this.activeToolStatus.startedAt));
1027
1170
  }
1028
1171
  }
1172
+ /**
1173
+ * Set activity callback - called during tool progress to update activity line with streaming output
1174
+ */
1175
+ setActivityCallback(callback) {
1176
+ this._activityCallback = callback;
1177
+ }
1029
1178
  /**
1030
1179
  * Keep a live status line ticking while an SSE tool or long-running tool is active.
1031
1180
  * Uses a heartbeat to animate the spinner and update elapsed time without spamming writes.
@@ -1146,79 +1295,10 @@ export class ShellUIAdapter {
1146
1295
  const prefix = success
1147
1296
  ? theme.success(icons.subaction)
1148
1297
  : theme.error(icons.subaction);
1149
- // Add newline after result for visual separation
1150
- this.display.stream(` ${prefix} ${summary}\n\n`);
1298
+ // Single newline - next tool call adds its own spacing
1299
+ this.display.stream(` ${prefix} ${summary}\n`);
1151
1300
  }
1152
1301
  }
1153
- /**
1154
- * Render a rich block for non-streamed SSE-style tool results so they feel like a "completed event".
1155
- * Returns true if a block was rendered and downstream compact formatting should be skipped.
1156
- */
1157
- maybeRenderRichToolResult(call, output, success, durationMs) {
1158
- if (!this.shouldRenderRichToolBlock(call, output)) {
1159
- return false;
1160
- }
1161
- const block = this.renderRichToolBlock(call, output, success, durationMs);
1162
- if (!block.trim()) {
1163
- return false;
1164
- }
1165
- this.display.stream(`\n${block}\n\n`);
1166
- return true;
1167
- }
1168
- shouldRenderRichToolBlock(call, output) {
1169
- const trimmed = output?.trim() ?? '';
1170
- if (!trimmed)
1171
- return false;
1172
- // Skip tools that already have bespoke rendering
1173
- if (call.name === 'Edit' || call.name === 'edit_file' || call.name === 'list_files') {
1174
- return false;
1175
- }
1176
- // Read operations should stay compact (single-line summary), not boxed
1177
- if (call.name === 'Read' || call.name === 'read_file') {
1178
- return false;
1179
- }
1180
- const isMcpTool = call.name.startsWith('mcp__');
1181
- const isMultiline = trimmed.includes('\n');
1182
- const isLong = trimmed.length > 160;
1183
- return isMcpTool || isMultiline || isLong;
1184
- }
1185
- renderRichToolBlock(call, output, success, durationMs) {
1186
- const columns = Math.max(60, Math.min(getTerminalColumns(), 120));
1187
- const innerWidth = Math.max(40, columns - 4);
1188
- const statusIcon = success ? theme.success(icons.success) : theme.error(icons.error);
1189
- const labels = [
1190
- `${statusIcon} ${theme.tool(call.name)}`,
1191
- call.name.startsWith('mcp__') ? theme.ui.muted('SSE result') : theme.ui.muted('Tool result'),
1192
- ];
1193
- const elapsed = this.formatElapsedMs(durationMs);
1194
- if (elapsed) {
1195
- labels.push(`${theme.metrics.elapsedLabel('elapsed')} ${theme.metrics.elapsedValue(elapsed)}`);
1196
- }
1197
- const title = labels.filter(Boolean).join(theme.ui.muted(' • '));
1198
- const titlePadding = Math.max(0, innerWidth - this.visibleLength(title) - 2);
1199
- const top = theme.ui.muted(`╭─ ${title} ${'─'.repeat(titlePadding)}╮`);
1200
- const bottom = theme.ui.muted(`╰${'─'.repeat(innerWidth + 2)}╯`);
1201
- const bodyLines = formatRichContent(output.trim(), innerWidth);
1202
- const boxed = bodyLines.length ? bodyLines : [''];
1203
- const content = boxed.map(line => {
1204
- const truncated = this.visibleLength(line) > innerWidth
1205
- ? `${line.slice(0, Math.max(0, innerWidth - 1))}…`
1206
- : line;
1207
- const pad = Math.max(0, innerWidth - this.visibleLength(truncated));
1208
- return `${theme.ui.muted('│')} ${truncated}${' '.repeat(pad)} ${theme.ui.muted('│')}`;
1209
- });
1210
- return [top, ...content, bottom].join('\n');
1211
- }
1212
- visibleLength(value) {
1213
- if (!value)
1214
- return 0;
1215
- return this.stripAnsi(value).length;
1216
- }
1217
- stripAnsi(value) {
1218
- if (!value)
1219
- return '';
1220
- return value.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, '');
1221
- }
1222
1302
  /**
1223
1303
  * Format tool result summary in Erosolar-CLI style
1224
1304
  * Returns compact summary like "Found 4 files" or "Read 50 lines"
@@ -1331,14 +1411,32 @@ export class ShellUIAdapter {
1331
1411
  }
1332
1412
  return 'Todos updated';
1333
1413
  }
1414
+ case 'grep_search':
1415
+ case 'search_files':
1416
+ case 'Grep': {
1417
+ // Count matches in output - look for file paths or line numbers
1418
+ const lines = output.trim().split('\n').filter(l => l.trim());
1419
+ if (lines.length === 0 || output.includes('No matches found') || output.includes('no matches')) {
1420
+ return theme.ui.muted('No matches');
1421
+ }
1422
+ // Count unique files or match lines
1423
+ const fileMatches = output.match(/^[^\n:]+:\d+:/gm);
1424
+ if (fileMatches) {
1425
+ const uniqueFiles = new Set(fileMatches.map(m => m.split(':')[0])).size;
1426
+ return `${theme.info(String(fileMatches.length))} matches in ${uniqueFiles} file${uniqueFiles === 1 ? '' : 's'}`;
1427
+ }
1428
+ return `${theme.info(String(lines.length))} matches`;
1429
+ }
1334
1430
  default: {
1335
- // Generic result
1431
+ // Generic result - show more info for better context
1336
1432
  const lines = output.trim().split('\n').filter(l => l).length;
1337
1433
  if (lines === 0) {
1338
- return 'Completed';
1434
+ return theme.ui.muted('No output');
1339
1435
  }
1340
1436
  if (lines <= 3) {
1341
- return 'Completed';
1437
+ // Show the actual output if short
1438
+ const shortOutput = output.trim().substring(0, 60);
1439
+ return shortOutput.length < output.trim().length ? `${shortOutput}…` : shortOutput;
1342
1440
  }
1343
1441
  return `${theme.info(String(lines))} lines`;
1344
1442
  }