erosolar-cli 2.1.172 → 2.1.173

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 (213) 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/askUserCapability.js +1 -1
  5. package/dist/capabilities/askUserCapability.js.map +1 -1
  6. package/dist/capabilities/statusCapability.js +2 -2
  7. package/dist/capabilities/statusCapability.js.map +1 -1
  8. package/dist/codex/capabilities/codexCoreCapability.d.ts +6 -0
  9. package/dist/codex/capabilities/codexCoreCapability.d.ts.map +1 -0
  10. package/dist/codex/capabilities/codexCoreCapability.js +516 -0
  11. package/dist/codex/capabilities/codexCoreCapability.js.map +1 -0
  12. package/dist/codex/fs.d.ts +4 -0
  13. package/dist/codex/fs.d.ts.map +1 -0
  14. package/dist/codex/fs.js +25 -0
  15. package/dist/codex/fs.js.map +1 -0
  16. package/dist/codex/persistence/planStore.d.ts +4 -0
  17. package/dist/codex/persistence/planStore.d.ts.map +1 -0
  18. package/dist/codex/persistence/planStore.js +59 -0
  19. package/dist/codex/persistence/planStore.js.map +1 -0
  20. package/dist/codex/pluginAllowlist.d.ts +4 -0
  21. package/dist/codex/pluginAllowlist.d.ts.map +1 -0
  22. package/dist/codex/pluginAllowlist.js +14 -0
  23. package/dist/codex/pluginAllowlist.js.map +1 -0
  24. package/dist/codex/types.d.ts +21 -0
  25. package/dist/codex/types.d.ts.map +1 -0
  26. package/dist/codex/types.js +62 -0
  27. package/dist/codex/types.js.map +1 -0
  28. package/dist/contracts/agent-schemas.json +5 -5
  29. package/dist/core/agent.d.ts +83 -24
  30. package/dist/core/agent.d.ts.map +1 -1
  31. package/dist/core/agent.js +499 -248
  32. package/dist/core/agent.js.map +1 -1
  33. package/dist/core/preferences.d.ts +1 -0
  34. package/dist/core/preferences.d.ts.map +1 -1
  35. package/dist/core/preferences.js +8 -1
  36. package/dist/core/preferences.js.map +1 -1
  37. package/dist/core/reliabilityPrompt.d.ts +9 -0
  38. package/dist/core/reliabilityPrompt.d.ts.map +1 -0
  39. package/dist/core/reliabilityPrompt.js +31 -0
  40. package/dist/core/reliabilityPrompt.js.map +1 -0
  41. package/dist/core/schemaValidator.js +3 -3
  42. package/dist/core/schemaValidator.js.map +1 -1
  43. package/dist/core/toolPreconditions.d.ts +0 -11
  44. package/dist/core/toolPreconditions.d.ts.map +1 -1
  45. package/dist/core/toolPreconditions.js +33 -164
  46. package/dist/core/toolPreconditions.js.map +1 -1
  47. package/dist/core/toolRuntime.d.ts.map +1 -1
  48. package/dist/core/toolRuntime.js +9 -114
  49. package/dist/core/toolRuntime.js.map +1 -1
  50. package/dist/core/updateChecker.d.ts +61 -1
  51. package/dist/core/updateChecker.d.ts.map +1 -1
  52. package/dist/core/updateChecker.js +147 -3
  53. package/dist/core/updateChecker.js.map +1 -1
  54. package/dist/headless/evalMode.d.ts.map +1 -1
  55. package/dist/headless/evalMode.js +6 -0
  56. package/dist/headless/evalMode.js.map +1 -1
  57. package/dist/headless/headlessApp.d.ts.map +1 -1
  58. package/dist/headless/headlessApp.js +6 -39
  59. package/dist/headless/headlessApp.js.map +1 -1
  60. package/dist/mcp/sseClient.d.ts +4 -1
  61. package/dist/mcp/sseClient.d.ts.map +1 -1
  62. package/dist/mcp/sseClient.js +36 -2
  63. package/dist/mcp/sseClient.js.map +1 -1
  64. package/dist/mcp/stdioClient.d.ts +4 -1
  65. package/dist/mcp/stdioClient.d.ts.map +1 -1
  66. package/dist/mcp/stdioClient.js +41 -1
  67. package/dist/mcp/stdioClient.js.map +1 -1
  68. package/dist/mcp/toolBridge.d.ts +3 -0
  69. package/dist/mcp/toolBridge.d.ts.map +1 -1
  70. package/dist/mcp/toolBridge.js +2 -2
  71. package/dist/mcp/toolBridge.js.map +1 -1
  72. package/dist/mcp/types.d.ts +18 -0
  73. package/dist/mcp/types.d.ts.map +1 -1
  74. package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
  75. package/dist/plugins/tools/nodeDefaults.js +0 -2
  76. package/dist/plugins/tools/nodeDefaults.js.map +1 -1
  77. package/dist/providers/openaiResponsesProvider.d.ts.map +1 -1
  78. package/dist/providers/openaiResponsesProvider.js +79 -74
  79. package/dist/providers/openaiResponsesProvider.js.map +1 -1
  80. package/dist/runtime/agentController.d.ts.map +1 -1
  81. package/dist/runtime/agentController.js +6 -3
  82. package/dist/runtime/agentController.js.map +1 -1
  83. package/dist/runtime/agentSession.d.ts +0 -2
  84. package/dist/runtime/agentSession.d.ts.map +1 -1
  85. package/dist/runtime/agentSession.js +2 -2
  86. package/dist/runtime/agentSession.js.map +1 -1
  87. package/dist/shell/interactiveShell.d.ts +25 -18
  88. package/dist/shell/interactiveShell.d.ts.map +1 -1
  89. package/dist/shell/interactiveShell.js +345 -291
  90. package/dist/shell/interactiveShell.js.map +1 -1
  91. package/dist/shell/shellApp.d.ts.map +1 -1
  92. package/dist/shell/shellApp.js +15 -8
  93. package/dist/shell/shellApp.js.map +1 -1
  94. package/dist/shell/systemPrompt.d.ts.map +1 -1
  95. package/dist/shell/systemPrompt.js +4 -15
  96. package/dist/shell/systemPrompt.js.map +1 -1
  97. package/dist/subagents/taskRunner.js +2 -1
  98. package/dist/subagents/taskRunner.js.map +1 -1
  99. package/dist/tools/bashTools.d.ts.map +1 -1
  100. package/dist/tools/bashTools.js +101 -8
  101. package/dist/tools/bashTools.js.map +1 -1
  102. package/dist/tools/diffUtils.d.ts +8 -2
  103. package/dist/tools/diffUtils.d.ts.map +1 -1
  104. package/dist/tools/diffUtils.js +72 -13
  105. package/dist/tools/diffUtils.js.map +1 -1
  106. package/dist/tools/grepTools.d.ts.map +1 -1
  107. package/dist/tools/grepTools.js +10 -2
  108. package/dist/tools/grepTools.js.map +1 -1
  109. package/dist/tools/planningTools.d.ts +0 -10
  110. package/dist/tools/planningTools.d.ts.map +1 -1
  111. package/dist/tools/planningTools.js +0 -16
  112. package/dist/tools/planningTools.js.map +1 -1
  113. package/dist/tools/searchTools.d.ts.map +1 -1
  114. package/dist/tools/searchTools.js +4 -2
  115. package/dist/tools/searchTools.js.map +1 -1
  116. package/dist/ui/PromptController.d.ts +7 -4
  117. package/dist/ui/PromptController.d.ts.map +1 -1
  118. package/dist/ui/PromptController.js +4 -7
  119. package/dist/ui/PromptController.js.map +1 -1
  120. package/dist/ui/ShellUIAdapter.d.ts +286 -28
  121. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  122. package/dist/ui/ShellUIAdapter.js +1485 -121
  123. package/dist/ui/ShellUIAdapter.js.map +1 -1
  124. package/dist/ui/UnifiedUIController.d.ts +80 -0
  125. package/dist/ui/UnifiedUIController.d.ts.map +1 -0
  126. package/dist/ui/UnifiedUIController.js +211 -0
  127. package/dist/ui/UnifiedUIController.js.map +1 -0
  128. package/dist/ui/UnifiedUIRenderer.d.ts +102 -46
  129. package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
  130. package/dist/ui/UnifiedUIRenderer.js +680 -610
  131. package/dist/ui/UnifiedUIRenderer.js.map +1 -1
  132. package/dist/ui/animatedStatus.d.ts +128 -6
  133. package/dist/ui/animatedStatus.d.ts.map +1 -1
  134. package/dist/ui/animatedStatus.js +383 -50
  135. package/dist/ui/animatedStatus.js.map +1 -1
  136. package/dist/ui/animation/AnimationScheduler.d.ts +192 -0
  137. package/dist/ui/animation/AnimationScheduler.d.ts.map +1 -0
  138. package/dist/ui/animation/AnimationScheduler.js +432 -0
  139. package/dist/ui/animation/AnimationScheduler.js.map +1 -0
  140. package/dist/ui/display.d.ts +179 -25
  141. package/dist/ui/display.d.ts.map +1 -1
  142. package/dist/ui/display.js +678 -96
  143. package/dist/ui/display.js.map +1 -1
  144. package/dist/ui/inPlaceUpdater.d.ts +181 -0
  145. package/dist/ui/inPlaceUpdater.d.ts.map +1 -0
  146. package/dist/ui/inPlaceUpdater.js +515 -0
  147. package/dist/ui/inPlaceUpdater.js.map +1 -0
  148. package/dist/ui/interrupts/InterruptManager.d.ts +142 -0
  149. package/dist/ui/interrupts/InterruptManager.d.ts.map +1 -0
  150. package/dist/ui/interrupts/InterruptManager.js +439 -0
  151. package/dist/ui/interrupts/InterruptManager.js.map +1 -0
  152. package/dist/ui/layout.d.ts +0 -1
  153. package/dist/ui/layout.d.ts.map +1 -1
  154. package/dist/ui/layout.js +0 -12
  155. package/dist/ui/layout.js.map +1 -1
  156. package/dist/ui/orchestration/StatusOrchestrator.d.ts +1 -1
  157. package/dist/ui/orchestration/StatusOrchestrator.js +1 -1
  158. package/dist/ui/orchestration/UIUpdateCoordinator.d.ts +61 -7
  159. package/dist/ui/orchestration/UIUpdateCoordinator.d.ts.map +1 -1
  160. package/dist/ui/orchestration/UIUpdateCoordinator.js +232 -20
  161. package/dist/ui/orchestration/UIUpdateCoordinator.js.map +1 -1
  162. package/dist/ui/shortcutsHelp.d.ts.map +1 -1
  163. package/dist/ui/shortcutsHelp.js +0 -1
  164. package/dist/ui/shortcutsHelp.js.map +1 -1
  165. package/dist/ui/telemetry/ResponseTracker.d.ts +22 -0
  166. package/dist/ui/telemetry/ResponseTracker.d.ts.map +1 -0
  167. package/dist/ui/telemetry/ResponseTracker.js +60 -0
  168. package/dist/ui/telemetry/ResponseTracker.js.map +1 -0
  169. package/dist/ui/telemetry/UITelemetry.d.ts +181 -0
  170. package/dist/ui/telemetry/UITelemetry.d.ts.map +1 -0
  171. package/dist/ui/telemetry/UITelemetry.js +446 -0
  172. package/dist/ui/telemetry/UITelemetry.js.map +1 -0
  173. package/dist/ui/unified/index.d.ts +28 -1
  174. package/dist/ui/unified/index.d.ts.map +1 -1
  175. package/dist/ui/unified/index.js +41 -2
  176. package/dist/ui/unified/index.js.map +1 -1
  177. package/dist/ui/unified/layout.d.ts +12 -0
  178. package/dist/ui/unified/layout.d.ts.map +1 -0
  179. package/dist/ui/unified/layout.js +96 -0
  180. package/dist/ui/unified/layout.js.map +1 -0
  181. package/package.json +1 -2
  182. package/dist/StringUtils.d.ts +0 -8
  183. package/dist/StringUtils.d.ts.map +0 -1
  184. package/dist/StringUtils.js +0 -11
  185. package/dist/StringUtils.js.map +0 -1
  186. package/dist/core/aiFlowSupervisor.d.ts +0 -44
  187. package/dist/core/aiFlowSupervisor.d.ts.map +0 -1
  188. package/dist/core/aiFlowSupervisor.js +0 -299
  189. package/dist/core/aiFlowSupervisor.js.map +0 -1
  190. package/dist/core/cliTestHarness.d.ts +0 -200
  191. package/dist/core/cliTestHarness.d.ts.map +0 -1
  192. package/dist/core/cliTestHarness.js +0 -549
  193. package/dist/core/cliTestHarness.js.map +0 -1
  194. package/dist/core/testUtils.d.ts +0 -121
  195. package/dist/core/testUtils.d.ts.map +0 -1
  196. package/dist/core/testUtils.js +0 -235
  197. package/dist/core/testUtils.js.map +0 -1
  198. package/dist/core/toolValidation.d.ts +0 -116
  199. package/dist/core/toolValidation.d.ts.map +0 -1
  200. package/dist/core/toolValidation.js +0 -282
  201. package/dist/core/toolValidation.js.map +0 -1
  202. package/dist/ui/planOverlay.d.ts +0 -28
  203. package/dist/ui/planOverlay.d.ts.map +0 -1
  204. package/dist/ui/planOverlay.js +0 -156
  205. package/dist/ui/planOverlay.js.map +0 -1
  206. package/dist/ui/streamingFormatter.d.ts +0 -30
  207. package/dist/ui/streamingFormatter.d.ts.map +0 -1
  208. package/dist/ui/streamingFormatter.js +0 -91
  209. package/dist/ui/streamingFormatter.js.map +0 -1
  210. package/dist/utils/errorUtils.d.ts +0 -16
  211. package/dist/utils/errorUtils.d.ts.map +0 -1
  212. package/dist/utils/errorUtils.js +0 -66
  213. package/dist/utils/errorUtils.js.map +0 -1
@@ -1,160 +1,1524 @@
1
- import { display as defaultDisplay } from './display.js';
2
- class MinimalUIController {
3
- recordAssistantThought(thought, _meta) {
4
- return thought ? Date.now() : null;
5
- }
6
- recordAssistantResponse(response, _meta) {
7
- return response ? Date.now() : null;
8
- }
9
- }
1
+ /**
2
+ * ShellUIAdapter - Bridges the UnifiedUIController with the existing shell infrastructure
3
+ * Provides compatibility layer and migration path from old to new UI system
4
+ *
5
+ * Advanced UI Features:
6
+ * - Compact same-line tool displays
7
+ * - In-place progress updates
8
+ * - Grouped operation summaries
9
+ * - Dynamic status badges
10
+ */
11
+ import { UnifiedUIController } from './UnifiedUIController.js';
12
+ import { InterruptPriority } from './interrupts/InterruptManager.js';
13
+ import { theme, icons } from './theme.js';
14
+ import { UIUpdateCoordinator } from './orchestration/UIUpdateCoordinator.js';
10
15
  export class ShellUIAdapter {
11
- outputDisplay;
12
- renderer = null;
13
- controller = new MinimalUIController();
16
+ uiController;
17
+ display;
18
+ config;
14
19
  fileChangeCallback;
15
- toolStatusCallback;
16
- toolUsage = new Map();
17
- constructor(outputDisplay = defaultDisplay, _config = {}) {
18
- this.outputDisplay = outputDisplay;
19
- }
20
- attachRenderer(renderer) {
21
- this.renderer = renderer;
22
- }
23
- getController() {
24
- return this.controller;
25
- }
26
- setFileChangeCallback(callback) {
27
- this.fileChangeCallback = callback;
28
- }
29
- setToolStatusCallback(callback) {
30
- this.toolStatusCallback = callback;
20
+ _toolStatusCallback;
21
+ _activityCallback;
22
+ updateCoordinator;
23
+ // Track tool operations for compact rendering
24
+ pendingOperations = new Map();
25
+ completedOperations = [];
26
+ maxCompletedOps = 8;
27
+ compactDisplayMode = true; // Enable compact same-line displays
28
+ toolWarnings = new Map();
29
+ toolProgressSnapshots = new Map();
30
+ exploreProgressState = new Map();
31
+ toolUsageCounts = new Map();
32
+ lastToolUsageSummary = null;
33
+ lastToolUsageSummaryPlain = null;
34
+ activeToolStatus = null;
35
+ activeToolHeartbeatId = null;
36
+ statusSpinnerIndex = 0;
37
+ // Store last tool result for expansion with ctrl+o
38
+ lastToolResult = null;
39
+ // Scrolling output buffer - shows only the last N lines of tool output in-place
40
+ maxScrollingLines = 3;
41
+ scrollingOutputBuffer = new Map();
42
+ scrollingPanelOwner = null;
43
+ // Scrolling tool call display - shows only the last N tool calls, scrolling older ones up
44
+ maxVisibleToolCalls = 3;
45
+ recentToolCalls = [];
46
+ toolCallDisplayedCount = 0;
47
+ constructor(writeStream, display, config = {}, updateCoordinator) {
48
+ this.display = display;
49
+ this.config = {
50
+ enableTelemetry: true,
51
+ debugMode: false,
52
+ ...config,
53
+ };
54
+ this.updateCoordinator = updateCoordinator ?? new UIUpdateCoordinator();
55
+ // Initialize unified UI controller
56
+ this.uiController = new UnifiedUIController(writeStream, {
57
+ enableAnimations: true,
58
+ enableTelemetry: this.config.enableTelemetry,
59
+ adaptivePerformance: true,
60
+ debugMode: this.config.debugMode,
61
+ });
62
+ // Legacy components are optional and should be passed in if needed
63
+ // They require a readline.Interface which we don't have here
64
+ this.setupDisplayIntegration();
31
65
  }
32
- updateContextUsage(percentage) {
33
- if (!Number.isFinite(percentage))
34
- return;
35
- const percent = Math.max(0, Math.min(100, Math.round(percentage)));
36
- this.renderer?.updateStatusMeta({ contextPercent: percent }, { render: false });
66
+ /**
67
+ * Setup display integration
68
+ */
69
+ setupDisplayIntegration() {
70
+ // Register output interceptor to coordinate prompt repainting
71
+ this.display.registerOutputInterceptor({
72
+ beforeWrite: () => {
73
+ this.uiController.beginOutput();
74
+ },
75
+ afterWrite: () => {
76
+ this.uiController.endOutput();
77
+ },
78
+ });
37
79
  }
38
- startProcessing(message) {
39
- this.resetToolUsage();
40
- if (message) {
41
- this.renderer?.updateStatusBundle({ main: message }, { render: false });
42
- }
80
+ /**
81
+ * Expose the shared update coordinator so other components can align with UI mode.
82
+ */
83
+ getUpdateCoordinator() {
84
+ return this.updateCoordinator;
43
85
  }
44
- endProcessing(message) {
45
- this.renderer?.updateStatusBundle({ main: message ?? null, streaming: null }, { render: false });
86
+ /**
87
+ * Expose the underlying unified UI controller for orchestration/telemetry.
88
+ */
89
+ getController() {
90
+ return this.uiController;
46
91
  }
92
+ /**
93
+ * Create a tool observer for the agent
94
+ * Shows tool results in clean panels (status bar shows active tool execution)
95
+ * Uses compact rendering for same-line displays when appropriate
96
+ */
47
97
  createToolObserver() {
48
98
  return {
49
99
  onToolStart: (call) => {
50
100
  this.recordToolUsage(call.name);
51
- const summary = this.formatToolCall(call);
52
- this.outputDisplay.emit('tool-call', summary);
53
- this.toolStatusCallback?.(`Running ${call.name}`);
101
+ this.clearToolState(call.id);
102
+ // Track operation for compact rendering
103
+ const operation = {
104
+ id: call.id,
105
+ name: call.name,
106
+ status: 'running',
107
+ summary: this.getToolSummary(call),
108
+ args: call.arguments,
109
+ startedAt: Date.now(),
110
+ };
111
+ this.pendingOperations.set(call.id, operation);
112
+ // Erosolar-CLI style: Show tool call start for ALL tools
113
+ // This provides transparency about which tools are being used
114
+ this.displayToolCallStart(call);
115
+ // Update status bar to show active tool
116
+ if (this._toolStatusCallback) {
117
+ const actionDesc = this.getToolActionDescription(call);
118
+ this.activeToolStatus = {
119
+ id: call.id,
120
+ description: actionDesc,
121
+ startedAt: Date.now(),
122
+ };
123
+ this._toolStatusCallback(this.buildRunningStatusLine(actionDesc, this.activeToolStatus.startedAt));
124
+ this.startToolStatusHeartbeat(call.id);
125
+ }
126
+ this.uiController.onToolStart(call);
54
127
  },
55
128
  onToolProgress: (call, progress) => {
56
- const label = progress.message || `${progress.current}/${progress.total ?? progress.current}`;
57
- this.toolStatusCallback?.(`${call.name}: ${label}`);
129
+ // Update pending operation
130
+ const op = this.pendingOperations.get(call.id);
131
+ if (op) {
132
+ op.detail = progress.message;
133
+ }
134
+ this.uiController.onToolProgress(call.id, {
135
+ current: progress.current,
136
+ total: progress.total ?? progress.current,
137
+ message: progress.message,
138
+ });
139
+ if (call.name === 'explore') {
140
+ this.logExploreProgress(call, progress);
141
+ }
142
+ else {
143
+ this.logToolProgress(call, progress);
144
+ }
145
+ // Update activity line with streaming output from bash commands
146
+ if (this._activityCallback && progress.message) {
147
+ // Show the streaming line as the activity (e.g., npm install progress)
148
+ const activityText = progress.message.slice(0, 60);
149
+ this._activityCallback(activityText);
150
+ }
151
+ // Update status bar with progress - use in-place update for supported terminals
152
+ if (this._toolStatusCallback) {
153
+ const description = this.getToolActionDescription(call, progress);
154
+ if (description) {
155
+ if (this.activeToolStatus && this.activeToolStatus.id === call.id) {
156
+ this.activeToolStatus.description = description;
157
+ }
158
+ else {
159
+ this.activeToolStatus = {
160
+ id: call.id,
161
+ description: description,
162
+ startedAt: Date.now(),
163
+ };
164
+ }
165
+ this._toolStatusCallback(this.buildRunningStatusLine(description, this.activeToolStatus.startedAt));
166
+ }
167
+ }
168
+ },
169
+ onToolWarning: (call, warning) => {
170
+ const warningText = this.formatToolWarning(warning);
171
+ this.recordToolWarning(call.id, warningText);
172
+ if (this._toolStatusCallback) {
173
+ this._toolStatusCallback(`Warning: ${this.getToolActionDescription(call)}`);
174
+ }
58
175
  },
59
176
  onToolResult: (call, output) => {
60
- const result = this.formatToolResult(call, output, true);
61
- this.outputDisplay.rememberToolResult(result.full, result.summary);
62
- this.outputDisplay.emit('tool-result', result.summary, { compact: true });
63
- this.toolStatusCallback?.(null);
177
+ const now = Date.now();
178
+ // Complete the operation tracking
179
+ const op = this.pendingOperations.get(call.id);
180
+ if (op) {
181
+ op.status = 'success';
182
+ op.completedAt = now;
183
+ op.summary = this.extractResultSummary(call, output);
184
+ this.pendingOperations.delete(call.id);
185
+ this.completedOperations.push(op);
186
+ // Keep only recent completed operations
187
+ while (this.completedOperations.length > this.maxCompletedOps) {
188
+ this.completedOperations.shift();
189
+ }
190
+ }
191
+ if (call.name === 'explore') {
192
+ this.logExploreCompletion(call.id, true);
193
+ }
194
+ this.clearToolState(call.id);
195
+ // Track file changes for Edit/Write/NotebookEdit tools
196
+ if (this.fileChangeCallback && (call.name === 'Edit' || call.name === 'Write' || call.name === 'NotebookEdit')) {
197
+ const fileChange = this.parseFileChange(call, output);
198
+ if (fileChange) {
199
+ this.fileChangeCallback(fileChange.path, fileChange.type, fileChange.additions, fileChange.removals);
200
+ }
201
+ }
202
+ // Store result for expansion with ctrl+o
203
+ this.lastToolResult = {
204
+ toolName: call.name,
205
+ output,
206
+ timestamp: Date.now(),
207
+ };
208
+ // Claude Code style: compact display for all tools
209
+ if (call.name === 'Edit' || call.name === 'edit_file') {
210
+ this.displayEditResult(call, output, true);
211
+ }
212
+ else if (this.compactDisplayMode && this.isCompactTool(call.name)) {
213
+ this.displayCompactResult(call, output, true);
214
+ }
215
+ else {
216
+ // Display tool result with ⎿ prefix
217
+ this.displayToolResultSummary(call, output, true);
218
+ }
219
+ // Surface any captured preflight warnings in a structured block
220
+ this.flushToolWarnings(call.id, call.name);
221
+ // Clear status bar after showing result
222
+ this.stopToolStatusHeartbeat();
223
+ if (this._toolStatusCallback) {
224
+ this._toolStatusCallback(null);
225
+ }
226
+ this.uiController.onToolComplete(call.id, output);
64
227
  },
65
- onToolError: (call, error) => {
66
- const result = this.formatToolResult(call, error, false);
67
- this.outputDisplay.rememberToolResult(result.full, result.summary);
68
- this.outputDisplay.emit('tool-result', result.summary, { compact: true });
69
- this.toolStatusCallback?.(null);
228
+ onToolError: (call, message) => {
229
+ // Complete the operation tracking with error
230
+ const op = this.pendingOperations.get(call.id);
231
+ if (op) {
232
+ op.status = 'error';
233
+ op.completedAt = Date.now();
234
+ op.summary = message;
235
+ this.pendingOperations.delete(call.id);
236
+ this.completedOperations.push(op);
237
+ }
238
+ if (call.name === 'explore') {
239
+ this.logExploreCompletion(call.id, false);
240
+ }
241
+ this.clearToolState(call.id);
242
+ // Claude Code style: compact display for errors too
243
+ if (call.name === 'Edit' || call.name === 'edit_file') {
244
+ this.displayEditResult(call, message, false);
245
+ }
246
+ else if (this.compactDisplayMode && this.isCompactTool(call.name)) {
247
+ this.displayCompactResult(call, message, false);
248
+ }
249
+ else {
250
+ // Display error with ⎿ prefix (error color)
251
+ this.displayToolResultSummary(call, message, false);
252
+ }
253
+ this.flushToolWarnings(call.id, call.name);
254
+ // Clear status bar after showing error
255
+ this.stopToolStatusHeartbeat();
256
+ if (this._toolStatusCallback) {
257
+ this._toolStatusCallback(null);
258
+ }
259
+ this.uiController.onToolError(call.id, { message });
70
260
  },
71
261
  onCacheHit: (call) => {
72
- this.recordToolUsage(call.name);
73
- const summary = `${call.name} (cached)`;
74
- this.outputDisplay.emit('tool-call', summary);
75
- this.outputDisplay.emit('tool-result', `⎿ ${summary}`, { compact: true });
76
- },
77
- onToolWarning: (_call, warning) => {
78
- const message = typeof warning === 'string'
79
- ? warning
80
- : typeof warning === 'object' && warning !== null && 'message' in warning
81
- ? String(warning.message ?? '')
82
- : '';
83
- if (message) {
84
- this.outputDisplay.showWarning(message);
262
+ // Track as cached operation
263
+ const operation = {
264
+ id: call.id,
265
+ name: call.name,
266
+ status: 'cached',
267
+ summary: `${this.getToolSummary(call)} (cached)`,
268
+ args: call.arguments,
269
+ startedAt: Date.now(),
270
+ completedAt: Date.now(),
271
+ };
272
+ this.completedOperations.push(operation);
273
+ this.clearToolState(call.id);
274
+ // Erosolar-CLI style: Show tool call with (cached) indicator
275
+ this.displayToolCallStart(call, true);
276
+ this.flushToolWarnings(call.id, call.name);
277
+ this.stopToolStatusHeartbeat();
278
+ if (this._toolStatusCallback) {
279
+ this._toolStatusCallback(null);
85
280
  }
281
+ this.uiController.onToolStart(call);
282
+ this.uiController.onToolComplete(call.id, 'cache-hit');
86
283
  },
87
284
  };
88
285
  }
286
+ /**
287
+ * Check if a tool should use compact (single-line) display
288
+ */
289
+ isCompactTool(toolName) {
290
+ const compactTools = new Set([
291
+ 'Read', 'read_file',
292
+ 'Glob', 'glob',
293
+ 'TodoWrite', 'todo_write',
294
+ 'WebFetch', 'web_fetch',
295
+ 'list_files',
296
+ ]);
297
+ return compactTools.has(toolName);
298
+ }
299
+ /**
300
+ * Track tool usage for per-request summaries.
301
+ */
302
+ recordToolUsage(toolName) {
303
+ const name = toolName.trim();
304
+ if (!name)
305
+ return;
306
+ const current = this.toolUsageCounts.get(name) ?? 0;
307
+ this.toolUsageCounts.set(name, current + 1);
308
+ }
309
+ resetToolUsageSummary() {
310
+ this.toolUsageCounts.clear();
311
+ this.lastToolUsageSummary = null;
312
+ this.lastToolUsageSummaryPlain = null;
313
+ }
314
+ /**
315
+ * Render a compact "tools used" line mirroring Claude Code.
316
+ * Shows the top two tools and how many more were used.
317
+ */
318
+ showToolUsageSummary() {
319
+ // Compute summary for metadata but don't display it
320
+ // The tool calls are already visible in the conversation flow
321
+ this.lastToolUsageSummary = this.formatToolUsageSummary();
322
+ this.lastToolUsageSummaryPlain = this.formatToolUsageSummary({ plain: true });
323
+ // Don't stream the summary - it's redundant after the assistant response
324
+ }
89
325
  getToolUsageSummary(options = {}) {
90
- if (this.toolUsage.size === 0) {
326
+ return options.plain ? this.lastToolUsageSummaryPlain : this.lastToolUsageSummary;
327
+ }
328
+ /**
329
+ * Get the last tool result for expansion with ctrl+o
330
+ */
331
+ getLastToolResult() {
332
+ return this.lastToolResult;
333
+ }
334
+ /**
335
+ * Clear the stored tool result (e.g., after expansion)
336
+ */
337
+ clearLastToolResult() {
338
+ this.lastToolResult = null;
339
+ }
340
+ formatToolUsageSummary(options = {}) {
341
+ const plain = options.plain === true;
342
+ if (this.toolUsageCounts.size === 0) {
91
343
  return null;
92
344
  }
93
- const entries = Array.from(this.toolUsage.entries()).sort((a, b) => {
345
+ const entries = Array.from(this.toolUsageCounts.entries()).sort((a, b) => {
94
346
  if (b[1] === a[1]) {
95
347
  return a[0].localeCompare(b[0]);
96
348
  }
97
349
  return b[1] - a[1];
98
350
  });
351
+ const totalCalls = entries.reduce((sum, [, count]) => sum + count, 0);
99
352
  const top = entries.slice(0, 2);
100
- const remaining = entries.length - top.length;
101
- const joiner = options.plain ? ' · ' : ' ';
102
- const formatted = top.map(([name, count]) => {
103
- const label = options.plain ? name : name;
104
- return `${label} x${count}`;
353
+ const remaining = entries.slice(2);
354
+ const formattedTop = top.map(([name, count]) => {
355
+ const countLabel = count > 1 ? plain ? `×${count}` : theme.ui.muted(`×${count}`) : '';
356
+ const formattedName = plain ? name : theme.tool(name);
357
+ return `${formattedName}${countLabel}`;
105
358
  });
106
- if (remaining > 0) {
107
- formatted.push(`+${remaining} more`);
359
+ const remainderLabel = remaining.length
360
+ ? plain
361
+ ? ` +${remaining.length} more`
362
+ : theme.ui.muted(` +${remaining.length} more`)
363
+ : '';
364
+ const totalLabel = plain
365
+ ? ` · ${totalCalls} call${totalCalls === 1 ? '' : 's'}`
366
+ : theme.ui.muted(` · ${totalCalls} call${totalCalls === 1 ? '' : 's'}`);
367
+ const separator = plain ? ', ' : theme.ui.muted(', ');
368
+ const prefix = plain ? 'tools ' : `${theme.info(icons.action)} tools `;
369
+ return `${prefix}${formattedTop.join(separator)}${remainderLabel}${totalLabel}`.trim();
370
+ }
371
+ /**
372
+ * Normalize preflight warnings for consistent display.
373
+ */
374
+ formatToolWarning(warning) {
375
+ if (typeof warning === 'string') {
376
+ return warning;
108
377
  }
109
- return formatted.join(joiner);
378
+ const code = warning.code ? `[${warning.code}] ` : '';
379
+ const suggestion = warning.suggestion ? ` — ${warning.suggestion}` : '';
380
+ return `${code}${warning.message}${suggestion}`;
110
381
  }
111
- dispose() {
112
- this.toolUsage.clear();
113
- this.toolStatusCallback = undefined;
114
- this.fileChangeCallback = undefined;
115
- this.renderer = null;
382
+ /**
383
+ * Track warnings for a tool call so we can show them after the result too.
384
+ */
385
+ recordToolWarning(callId, warningText) {
386
+ const existing = this.toolWarnings.get(callId) ?? [];
387
+ if (!existing.includes(warningText)) {
388
+ existing.push(warningText);
389
+ this.toolWarnings.set(callId, existing);
390
+ }
116
391
  }
117
- resetToolUsage() {
118
- this.toolUsage.clear();
392
+ /**
393
+ * Render any captured warnings for the given tool call as a structured block.
394
+ */
395
+ flushToolWarnings(callId, toolName) {
396
+ const warnings = this.toolWarnings.get(callId);
397
+ if (!warnings?.length) {
398
+ return;
399
+ }
400
+ this.toolWarnings.delete(callId);
401
+ const header = `${theme.warning(icons.warning)} ${theme.ui.muted('preflight')} warnings for ${theme.tool(toolName)}`;
402
+ const bullet = theme.ui.muted(icons.bullet);
403
+ const body = warnings.map(text => ` ${bullet} ${text}`).join('\n');
404
+ this.display.stream(`\n${header}\n${body}\n`);
119
405
  }
120
- recordToolUsage(toolName) {
121
- if (!toolName)
406
+ /**
407
+ * Get a short summary for tool operation
408
+ */
409
+ getToolSummary(call) {
410
+ const args = call.arguments;
411
+ switch (call.name) {
412
+ case 'Read':
413
+ case 'read_file': {
414
+ const path = this.extractPath(args, ['file_path', 'path']);
415
+ return path ? this.truncatePath(path, 30) : '';
416
+ }
417
+ case 'Edit':
418
+ case 'edit_file': {
419
+ const path = this.extractPath(args, ['file_path', 'path']);
420
+ return path ? this.truncatePath(path, 30) : '';
421
+ }
422
+ case 'Grep':
423
+ case 'grep': {
424
+ const pattern = args?.['pattern'];
425
+ return pattern ? `"${pattern.slice(0, 20)}"` : '';
426
+ }
427
+ case 'Glob':
428
+ case 'glob': {
429
+ const pattern = args?.['pattern'];
430
+ return pattern || '';
431
+ }
432
+ case 'Task':
433
+ case 'task': {
434
+ const desc = args?.['description'];
435
+ return desc ? desc.slice(0, 25) : '';
436
+ }
437
+ default:
438
+ return '';
439
+ }
440
+ }
441
+ /**
442
+ * Extract result summary from tool output
443
+ * Includes file paths for file operations so users can see which files were accessed
444
+ */
445
+ extractResultSummary(call, output) {
446
+ const args = call.arguments;
447
+ switch (call.name) {
448
+ case 'Read':
449
+ case 'read_file': {
450
+ const path = this.extractPath(args, ['file_path', 'path']);
451
+ const lines = this.countLines(output);
452
+ // Show file path so users see which file was read
453
+ return path ? `${this.truncatePath(path, 40)} (${lines} lines)` : `${lines} lines`;
454
+ }
455
+ case 'Write':
456
+ case 'write_file': {
457
+ const path = this.extractPath(args, ['file_path', 'path']);
458
+ return path ? `${this.truncatePath(path, 40)} written` : 'file written';
459
+ }
460
+ case 'Edit':
461
+ case 'edit_file': {
462
+ const path = this.extractPath(args, ['file_path', 'path']);
463
+ // Parse additions/removals from output if available
464
+ const editMatch = output.match(/\+(\d+).*-(\d+)/);
465
+ const changes = editMatch ? `+${editMatch[1]}/-${editMatch[2]}` : 'edited';
466
+ return path ? `${this.truncatePath(path, 35)} ${changes}` : changes;
467
+ }
468
+ case 'Grep':
469
+ case 'grep': {
470
+ const pattern = args?.['pattern'];
471
+ const matches = output.trim().split('\n').filter(l => l).length;
472
+ const matchText = `${matches} match${matches === 1 ? '' : 'es'}`;
473
+ // Show pattern for context
474
+ if (pattern && pattern.length <= 20) {
475
+ return `"${pattern}" → ${matchText}`;
476
+ }
477
+ return matchText;
478
+ }
479
+ case 'Glob':
480
+ case 'glob': {
481
+ const pattern = args?.['pattern'];
482
+ const files = output.trim().split('\n').filter(f => f).length;
483
+ const fileText = `${files} file${files === 1 ? '' : 's'}`;
484
+ // Show pattern for context
485
+ if (pattern && pattern.length <= 25) {
486
+ return `${pattern} → ${fileText}`;
487
+ }
488
+ return fileText;
489
+ }
490
+ case 'list_files': {
491
+ const path = this.extractPath(args, ['path']) || '.';
492
+ const files = output.trim().split('\n').filter(f => f).length;
493
+ return `${this.truncatePath(path, 40)} → ${files} item${files === 1 ? '' : 's'}`;
494
+ }
495
+ case 'Bash':
496
+ case 'bash': {
497
+ const cmd = args?.['command'];
498
+ if (cmd) {
499
+ const shortCmd = cmd.split('\n')[0]?.trim() || cmd;
500
+ const truncated = shortCmd.length > 30 ? `${shortCmd.slice(0, 27)}...` : shortCmd;
501
+ return `\`${truncated}\` done`;
502
+ }
503
+ return 'done';
504
+ }
505
+ default:
506
+ return 'done';
507
+ }
508
+ }
509
+ /**
510
+ * Display compact single-line result (Erosolar-CLI style)
511
+ * Shows the tool name with a concise summary (no raw payload dumps)
512
+ */
513
+ displayCompactResult(call, output, success) {
514
+ const args = call.arguments || {};
515
+ const summary = success
516
+ ? this.extractResultSummary(call, output) || this.formatClaudeCodeResultSummary(call.name, args, output, success)
517
+ : this.formatClaudeCodeResultSummary(call.name, args, output, success);
518
+ if (!summary) {
519
+ return;
520
+ }
521
+ const prefix = success ? theme.success(icons.subaction) : theme.error(icons.subaction);
522
+ // Just show the result summary - tool name was already shown on the call line
523
+ this.display.stream(` ${prefix} ${summary}\n`);
524
+ }
525
+ /**
526
+ * Display edit results with full colored diff output.
527
+ * - Summary line with additions/removals
528
+ * - Diff lines rendered in-place (red for removals, green for additions)
529
+ */
530
+ displayEditResult(call, output, success) {
531
+ const args = call.arguments || {};
532
+ const summary = this.formatClaudeCodeResultSummary(call.name, args, output, success);
533
+ if (!summary) {
534
+ return;
535
+ }
536
+ const prefix = success ? theme.success(icons.subaction) : theme.error(icons.subaction);
537
+ this.display.stream(` ${prefix} ${summary}\n`);
538
+ if (success) {
539
+ const diffSection = this.extractEditDiffSection(output);
540
+ if (diffSection) {
541
+ // Show diff with single trailing newline
542
+ this.display.stream(`${diffSection}\n`);
543
+ }
544
+ }
545
+ }
546
+ logToolProgress(call, progress) {
547
+ const description = this.getToolActionDescription(call, progress);
548
+ const pct = progress.total
549
+ ? Math.max(0, Math.min(100, Math.round((progress.current / progress.total) * 100)))
550
+ : null;
551
+ const parts = [];
552
+ if (description) {
553
+ parts.push(theme.ui.muted(description));
554
+ }
555
+ const details = [];
556
+ if (pct !== null) {
557
+ details.push(`${pct}%`);
558
+ }
559
+ if (progress.message?.trim()) {
560
+ details.push(progress.message.trim());
561
+ }
562
+ if (details.length) {
563
+ parts.push(theme.ui.muted(details.join(' · ')));
564
+ }
565
+ const separator = theme.ui.muted(' — ');
566
+ const line = parts.filter(Boolean).join(separator);
567
+ if (!line)
568
+ return;
569
+ // Use scrolling output for ALL tools on TTY - show only last N lines, updating in place
570
+ // This prevents flooding the terminal with hundreds of progress lines
571
+ if (process.stdout.isTTY) {
572
+ this.appendScrollingOutput(call.id, line);
573
+ }
574
+ else {
575
+ // Non-TTY: original streaming behavior (for log files, pipes, etc.)
576
+ const last = this.toolProgressSnapshots.get(call.id);
577
+ if (line !== last) {
578
+ this.display.stream(`${line}\n`);
579
+ this.toolProgressSnapshots.set(call.id, line);
580
+ }
581
+ }
582
+ }
583
+ /**
584
+ * Append a line to scrolling output buffer and update display in-place
585
+ * Shows only the last maxScrollingLines, replacing previous output
586
+ */
587
+ appendScrollingOutput(callId, line) {
588
+ let buffer = this.scrollingOutputBuffer.get(callId);
589
+ if (!buffer) {
590
+ buffer = { lines: [], displayedLineCount: 0 };
591
+ this.scrollingOutputBuffer.set(callId, buffer);
592
+ }
593
+ // Add line to buffer
594
+ buffer.lines.push(line);
595
+ // Keep only the last maxScrollingLines
596
+ while (buffer.lines.length > this.maxScrollingLines) {
597
+ buffer.lines.shift();
598
+ }
599
+ // Prefer inline panel (scroll box) when unified renderer is active
600
+ if (this.shouldUseInlineScrollBox()) {
601
+ this.scrollingPanelOwner = callId;
602
+ const panelLines = this.buildScrollingPanelLines(buffer.lines);
603
+ this.display.showInlinePanel(panelLines);
604
+ buffer.displayedLineCount = 0;
605
+ return;
606
+ }
607
+ // Move cursor up to overwrite previous lines (if any were displayed)
608
+ if (buffer.displayedLineCount > 0) {
609
+ process.stdout.write(`\x1b[${buffer.displayedLineCount}A`);
610
+ }
611
+ // Clear and rewrite the scrolling region
612
+ const linesToShow = buffer.lines;
613
+ for (let i = 0; i < linesToShow.length; i++) {
614
+ process.stdout.write(`\r\x1b[K${linesToShow[i]}\n`);
615
+ }
616
+ // Track how many lines we displayed for next update
617
+ buffer.displayedLineCount = linesToShow.length;
618
+ }
619
+ /**
620
+ * Build the pinned scroll-box contents for live tool output.
621
+ */
622
+ buildScrollingPanelLines(lines) {
623
+ if (!lines.length) {
624
+ return [];
625
+ }
626
+ const header = theme.ui.muted(`${icons.action} live output (latest ${Math.min(lines.length, this.maxScrollingLines)})`);
627
+ const indented = lines.map(text => ` ${text}`);
628
+ return [header, ...indented];
629
+ }
630
+ /**
631
+ * Use inline scroll box only when an interactive renderer is active.
632
+ */
633
+ shouldUseInlineScrollBox() {
634
+ return this.display.hasRenderer() && process.stdout.isTTY && !process.env['CI'];
635
+ }
636
+ /**
637
+ * Finalize scrolling output - called when tool completes
638
+ * Clears the scrolling progress lines so only the final result is shown
639
+ */
640
+ finalizeScrollingOutput(callId) {
641
+ if (this.scrollingPanelOwner === callId && this.shouldUseInlineScrollBox()) {
642
+ this.display.clearInlinePanel();
643
+ this.scrollingPanelOwner = null;
644
+ this.scrollingOutputBuffer.delete(callId);
645
+ return;
646
+ }
647
+ const buffer = this.scrollingOutputBuffer.get(callId);
648
+ if (buffer && buffer.displayedLineCount > 0 && process.stdout.isTTY) {
649
+ // Move cursor up and clear all the progress lines
650
+ process.stdout.write(`\x1b[${buffer.displayedLineCount}A`);
651
+ for (let i = 0; i < buffer.displayedLineCount; i++) {
652
+ process.stdout.write(`\r\x1b[K\n`);
653
+ }
654
+ // Move cursor back up to where progress started
655
+ process.stdout.write(`\x1b[${buffer.displayedLineCount}A`);
656
+ }
657
+ this.scrollingOutputBuffer.delete(callId);
658
+ }
659
+ logExploreProgress(call, progress) {
660
+ const previous = this.exploreProgressState.get(call.id) ?? { lastCurrent: 0 };
661
+ const total = progress.total ?? previous.total;
662
+ const current = Math.max(progress.current, previous.lastCurrent);
663
+ const path = progress.message ? this.truncatePath(String(progress.message), 60) : null;
664
+ const header = total ? `${current}/${total}` : `${current}`;
665
+ const parts = [`${theme.info('⇢')} Exploring ${header}`];
666
+ if (path) {
667
+ parts.push(theme.ui.muted(path));
668
+ }
669
+ const line = parts.join(theme.ui.muted(' • '));
670
+ if (line && line !== previous.lastLine) {
671
+ // Use in-place update for TTY - single line that updates without scrolling
672
+ if (process.stdout.isTTY && previous.lastLine) {
673
+ // Move cursor up one line and clear it, then write new line
674
+ process.stdout.write(`\x1b[1A\r\x1b[K${line}\n`);
675
+ }
676
+ else {
677
+ this.display.stream(`${line}\n`);
678
+ }
679
+ }
680
+ this.exploreProgressState.set(call.id, {
681
+ total,
682
+ lastCurrent: current,
683
+ lastLine: line,
684
+ });
685
+ }
686
+ logExploreCompletion(callId, success) {
687
+ const state = this.exploreProgressState.get(callId);
688
+ const total = state?.total ?? state?.lastCurrent ?? 0;
689
+ const summary = success
690
+ ? `${theme.success('✓')} Indexed ${total} file${total === 1 ? '' : 's'}`
691
+ : `${theme.error('✗')} Explore interrupted`;
692
+ // Clear the progress line before showing summary (if TTY and had progress)
693
+ if (process.stdout.isTTY && state?.lastLine) {
694
+ process.stdout.write(`\x1b[1A\r\x1b[K`);
695
+ }
696
+ this.display.stream(`${summary}\n`);
697
+ }
698
+ /**
699
+ * Centralized cleanup for tool state
700
+ * Called from onToolResult, onToolError, and onCacheHit
701
+ */
702
+ clearToolState(callId) {
703
+ this.toolProgressSnapshots.delete(callId);
704
+ this.exploreProgressState.delete(callId);
705
+ this.finalizeScrollingOutput(callId);
706
+ }
707
+ /**
708
+ * Display tool call start with Erosolar-CLI style prefix
709
+ * - ✢ for Task/agent tools (active task indicator)
710
+ * - ⏺ for other tools
711
+ * Uses scrolling display to show only the last N tool calls
712
+ */
713
+ displayToolCallStart(call, isCacheHit = false) {
714
+ const args = call.arguments || {};
715
+ const toolName = theme.tool(call.name);
716
+ // Format args inline (compact)
717
+ const argsStr = this.formatToolArgsInline(call.name, args);
718
+ const cacheHint = isCacheHit ? theme.ui.muted(' (cached)') : '';
719
+ // Use ✢ for Task tools (shows active task), ⏺ for others
720
+ const isTaskTool = call.name === 'Task' || call.name === 'task';
721
+ const bullet = theme.info(icons.action);
722
+ const taskMarker = isTaskTool ? ` ${theme.info('✢')}` : '';
723
+ // For Task tools, show the subagent type if available
724
+ let displayName = toolName;
725
+ if (isTaskTool) {
726
+ const subagentType = args['subagent_type'];
727
+ if (subagentType) {
728
+ // Capitalize first letter
729
+ displayName = theme.tool(subagentType.charAt(0).toUpperCase() + subagentType.slice(1));
730
+ }
731
+ }
732
+ // Erosolar-CLI style: bullet + optional task marker + ToolName(args)
733
+ const line = `${bullet}${taskMarker ? taskMarker : ''} ${displayName}${argsStr}${cacheHint}`;
734
+ // Use scrolling display for tool calls - shows only recent calls
735
+ this.appendToolCallDisplay(call.id, line);
736
+ }
737
+ /**
738
+ * Append a tool call to the scrolling display, keeping only the last N visible.
739
+ * Older tool calls scroll up and disappear, showing only recent activity.
740
+ */
741
+ appendToolCallDisplay(callId, line) {
742
+ // Add to recent tool calls
743
+ this.recentToolCalls.push({ line, id: callId });
744
+ // Keep only the last maxVisibleToolCalls
745
+ while (this.recentToolCalls.length > this.maxVisibleToolCalls) {
746
+ this.recentToolCalls.shift();
747
+ }
748
+ // On TTY, use in-place updating to show only recent tool calls
749
+ if (process.stdout.isTTY && this.toolCallDisplayedCount > 0) {
750
+ // Move cursor up to overwrite previous tool call lines
751
+ const moveUp = `\x1b[${this.toolCallDisplayedCount}A`;
752
+ // Clear from cursor to end of screen
753
+ const clearDown = '\x1b[J';
754
+ process.stdout.write(moveUp + clearDown);
755
+ }
756
+ // Display all recent tool calls
757
+ for (const toolCall of this.recentToolCalls) {
758
+ this.display.stream(`${toolCall.line}\n`);
759
+ }
760
+ // Track how many lines we displayed
761
+ this.toolCallDisplayedCount = this.recentToolCalls.length;
762
+ }
763
+ /**
764
+ * Clear tool call display state when processing completes
765
+ */
766
+ clearToolCallDisplay() {
767
+ this.recentToolCalls = [];
768
+ this.toolCallDisplayedCount = 0;
769
+ }
770
+ /**
771
+ * Format tool arguments inline for display (Erosolar-CLI style)
772
+ * Format: ToolName(arg1: "value1", arg2: "value2")
773
+ */
774
+ formatToolArgsInline(toolName, args) {
775
+ const parts = [];
776
+ // Priority order for different argument types
777
+ const priorityArgs = [
778
+ 'file_path', 'path', 'pattern', 'command', 'query', 'url',
779
+ 'output_mode', 'glob', 'type', 'head_limit',
780
+ ];
781
+ // Skip these arguments in display
782
+ const skipArgs = new Set([
783
+ 'dangerouslyDisableSandbox', 'run_in_background', 'description',
784
+ 'timeout', 'encoding', '-n', // defaults that don't add info
785
+ ]);
786
+ // Build args display based on tool type
787
+ switch (toolName) {
788
+ case 'Read':
789
+ case 'read_file': {
790
+ const filePath = args['file_path'] || args['path'];
791
+ if (filePath)
792
+ parts.push(this.truncatePath(String(filePath), 50));
793
+ break;
794
+ }
795
+ case 'Write':
796
+ case 'write_file':
797
+ case 'Edit':
798
+ case 'edit_file': {
799
+ const filePath = args['file_path'] || args['path'];
800
+ if (filePath)
801
+ parts.push(this.truncatePath(String(filePath), 50));
802
+ break;
803
+ }
804
+ case 'Bash':
805
+ case 'bash': {
806
+ const cmd = args['command'];
807
+ if (cmd) {
808
+ const shortCmd = String(cmd).split('\n')[0]?.trim() || String(cmd);
809
+ parts.push(shortCmd.length > 50 ? `${shortCmd.slice(0, 47)}...` : shortCmd);
810
+ }
811
+ break;
812
+ }
813
+ case 'Grep':
814
+ case 'grep': {
815
+ const pattern = args['pattern'];
816
+ if (pattern) {
817
+ const truncPattern = String(pattern).length > 35
818
+ ? `${String(pattern).slice(0, 32)}...`
819
+ : String(pattern);
820
+ parts.push(`pattern: "${truncPattern}"`);
821
+ }
822
+ const path = args['path'];
823
+ if (path && path !== '.') {
824
+ parts.push(`path: "${this.truncatePath(String(path), 30)}"`);
825
+ }
826
+ const outputMode = args['output_mode'];
827
+ if (outputMode && outputMode !== 'files_with_matches') {
828
+ parts.push(`output_mode: "${outputMode}"`);
829
+ }
830
+ break;
831
+ }
832
+ case 'Glob':
833
+ case 'glob': {
834
+ const pattern = args['pattern'];
835
+ if (pattern)
836
+ parts.push(`pattern: "${pattern}"`);
837
+ const path = args['path'];
838
+ if (path && path !== '.') {
839
+ parts.push(`path: "${this.truncatePath(String(path), 30)}"`);
840
+ }
841
+ break;
842
+ }
843
+ case 'list_files': {
844
+ const path = args['path'];
845
+ if (path && path !== '.') {
846
+ parts.push(this.truncatePath(String(path), 50));
847
+ }
848
+ break;
849
+ }
850
+ case 'WebFetch':
851
+ case 'web_fetch': {
852
+ const url = args['url'];
853
+ if (url) {
854
+ try {
855
+ parts.push(new URL(String(url)).hostname);
856
+ }
857
+ catch {
858
+ parts.push(String(url).slice(0, 40));
859
+ }
860
+ }
861
+ break;
862
+ }
863
+ case 'WebSearch':
864
+ case 'web_search': {
865
+ const query = args['query'];
866
+ if (query) {
867
+ const truncQuery = String(query).length > 40
868
+ ? `${String(query).slice(0, 37)}...`
869
+ : String(query);
870
+ parts.push(`"${truncQuery}"`);
871
+ }
872
+ break;
873
+ }
874
+ case 'Task':
875
+ case 'task': {
876
+ const desc = args['description'];
877
+ if (desc) {
878
+ const truncDesc = String(desc).length > 40
879
+ ? `${String(desc).slice(0, 37)}...`
880
+ : String(desc);
881
+ parts.push(`"${truncDesc}"`);
882
+ }
883
+ break;
884
+ }
885
+ case 'TodoWrite':
886
+ case 'todo_write': {
887
+ // Erosolar-CLI shows parameter names for complex types
888
+ parts.push('todos');
889
+ break;
890
+ }
891
+ default: {
892
+ // For unknown tools, show key: value pairs for priority args
893
+ for (const key of priorityArgs) {
894
+ if (skipArgs.has(key))
895
+ continue;
896
+ const value = args[key];
897
+ if (value === undefined || value === null || value === '')
898
+ continue;
899
+ if (typeof value === 'string') {
900
+ const truncVal = value.length > 30 ? `${value.slice(0, 27)}...` : value;
901
+ parts.push(`${key}: "${truncVal}"`);
902
+ }
903
+ else if (typeof value === 'number' || typeof value === 'boolean') {
904
+ parts.push(`${key}: ${value}`);
905
+ }
906
+ if (parts.length >= 3)
907
+ break; // Max 3 args for display
908
+ }
909
+ }
910
+ }
911
+ return parts.length > 0 ? `(${parts.join(', ')})` : '';
912
+ }
913
+ /**
914
+ * Start processing a request
915
+ */
916
+ startProcessing(message = 'Working on your request') {
917
+ this.resetToolUsageSummary();
918
+ this.clearToolCallDisplay(); // Clear previous tool call display
919
+ this.uiController.startProcessing();
920
+ this.uiController.setBaseStatus(message, 'info');
921
+ }
922
+ /**
923
+ * End processing
924
+ */
925
+ endProcessing(message = 'Ready for prompts') {
926
+ this.uiController.endProcessing();
927
+ this.showToolUsageSummary();
928
+ this.uiController.setBaseStatus(message, 'success');
929
+ // Clear tool call scrolling state for next request
930
+ this.clearToolCallDisplay();
931
+ }
932
+ /**
933
+ * Update context usage (no-op - status handled by display.showStatusLine)
934
+ */
935
+ updateContextUsage(_percentage) {
936
+ // No-op - context display is handled by display.showStatusLine()
937
+ }
938
+ /**
939
+ * Show a user interrupt
940
+ */
941
+ showInterrupt(message, type = 'alert', handler) {
942
+ const priority = type === 'alert'
943
+ ? InterruptPriority.HIGH
944
+ : InterruptPriority.NORMAL;
945
+ return this.uiController.queueInterrupt(type, message, priority, handler ? async () => handler() : undefined);
946
+ }
947
+ /**
948
+ * Complete an interrupt
949
+ */
950
+ completeInterrupt(id) {
951
+ this.uiController.completeInterrupt(id);
952
+ }
953
+ /**
954
+ * Show profile switcher (Shift+Tab)
955
+ */
956
+ showProfileSwitcher(profiles, _currentProfile) {
957
+ if (!profiles?.length) {
958
+ return;
959
+ }
960
+ const commands = profiles.map(profile => ({
961
+ command: profile.command,
962
+ description: profile.description,
963
+ category: 'profiles',
964
+ }));
965
+ this.display.showCommandPalette(commands, {
966
+ title: 'Switch Profile',
967
+ intro: 'Select a profile or type a command:',
968
+ });
969
+ }
970
+ /**
971
+ * Get telemetry data (only available in unified mode)
972
+ */
973
+ getTelemetry() {
974
+ return {
975
+ snapshot: this.uiController.getTelemetrySnapshot(),
976
+ performance: this.uiController.getPerformanceSummary(),
977
+ };
978
+ }
979
+ /**
980
+ * Get action description for tool execution (Erosolar-CLI style)
981
+ * Returns a human-readable description of what the tool is doing
982
+ * Includes the actual file path, command, or search target when available
983
+ */
984
+ getToolActionDescription(call, progress) {
985
+ const args = call.arguments;
986
+ let description;
987
+ switch (call.name) {
988
+ case 'Read':
989
+ case 'read_file': {
990
+ const filePath = this.extractPath(args, ['file_path', 'path']);
991
+ description = filePath ? `Reading ${this.truncatePath(filePath)}` : 'Reading file...';
992
+ break;
993
+ }
994
+ case 'Write':
995
+ case 'write_file': {
996
+ const filePath = this.extractPath(args, ['file_path', 'path']);
997
+ description = filePath ? `Writing ${this.truncatePath(filePath)}` : 'Writing file...';
998
+ break;
999
+ }
1000
+ case 'Edit':
1001
+ case 'edit_file': {
1002
+ const filePath = this.extractPath(args, ['file_path', 'path']);
1003
+ description = filePath ? `Editing ${this.truncatePath(filePath)}` : 'Editing file...';
1004
+ break;
1005
+ }
1006
+ case 'Bash':
1007
+ case 'bash': {
1008
+ const command = args?.['command'];
1009
+ if (command) {
1010
+ // Show first meaningful part of command (truncated)
1011
+ const shortCmd = command.split('\n')[0]?.trim() || command;
1012
+ const truncated = shortCmd.length > 40 ? `${shortCmd.slice(0, 37)}...` : shortCmd;
1013
+ description = `Running: ${truncated}`;
1014
+ }
1015
+ else {
1016
+ description = 'Executing command...';
1017
+ }
1018
+ break;
1019
+ }
1020
+ case 'Grep':
1021
+ case 'grep': {
1022
+ const pattern = args?.['pattern'];
1023
+ const path = this.extractPath(args, ['path', 'directory']);
1024
+ if (pattern) {
1025
+ const truncPattern = pattern.length > 20 ? `${pattern.slice(0, 17)}...` : pattern;
1026
+ description = path
1027
+ ? `Searching "${truncPattern}" in ${this.truncatePath(path)}`
1028
+ : `Searching for "${truncPattern}"`;
1029
+ }
1030
+ else {
1031
+ description = 'Searching code...';
1032
+ }
1033
+ break;
1034
+ }
1035
+ case 'Glob':
1036
+ case 'glob': {
1037
+ const pattern = args?.['pattern'];
1038
+ description = pattern ? `Finding files: ${pattern}` : 'Finding files...';
1039
+ break;
1040
+ }
1041
+ case 'list_files': {
1042
+ const path = this.extractPath(args, ['path']);
1043
+ description = path ? `Listing ${this.truncatePath(path)}` : 'Listing files...';
1044
+ break;
1045
+ }
1046
+ case 'WebFetch':
1047
+ case 'web_fetch': {
1048
+ const url = args?.['url'];
1049
+ if (url) {
1050
+ try {
1051
+ const hostname = new URL(url).hostname;
1052
+ description = `Fetching ${hostname}`;
1053
+ }
1054
+ catch {
1055
+ description = 'Fetching web content...';
1056
+ }
1057
+ }
1058
+ else {
1059
+ description = 'Fetching web content...';
1060
+ }
1061
+ break;
1062
+ }
1063
+ case 'WebSearch':
1064
+ case 'web_search': {
1065
+ const query = args?.['query'];
1066
+ if (query) {
1067
+ const truncQuery = query.length > 30 ? `${query.slice(0, 27)}...` : query;
1068
+ description = `Searching: "${truncQuery}"`;
1069
+ }
1070
+ else {
1071
+ description = 'Searching web...';
1072
+ }
1073
+ break;
1074
+ }
1075
+ case 'Task':
1076
+ case 'task': {
1077
+ const taskDesc = args?.['description'];
1078
+ description = taskDesc ? `Task: ${taskDesc}` : 'Running task...';
1079
+ break;
1080
+ }
1081
+ default:
1082
+ description = `Executing ${call.name}...`;
1083
+ }
1084
+ if (progress) {
1085
+ const details = [];
1086
+ if (progress.message?.trim()) {
1087
+ details.push(progress.message.trim());
1088
+ }
1089
+ if (typeof progress.total === 'number' && progress.total > 0) {
1090
+ const pct = Math.max(0, Math.min(100, Math.round((progress.current / progress.total) * 100)));
1091
+ details.push(`${pct}%`);
1092
+ }
1093
+ if (details.length) {
1094
+ return `${description} (${details.join(' ')})`;
1095
+ }
1096
+ }
1097
+ return description;
1098
+ }
1099
+ /**
1100
+ * Extract a path from tool arguments, checking multiple possible keys
1101
+ */
1102
+ extractPath(args, keys) {
1103
+ if (!args)
1104
+ return undefined;
1105
+ for (const key of keys) {
1106
+ const value = args[key];
1107
+ if (typeof value === 'string' && value.trim()) {
1108
+ return value.trim();
1109
+ }
1110
+ }
1111
+ return undefined;
1112
+ }
1113
+ /**
1114
+ * Truncate a file path to show just the filename or last path segments
1115
+ * e.g., "/Users/bo/GitHub/project/src/ui/display.ts" -> "src/ui/display.ts"
1116
+ */
1117
+ truncatePath(fullPath, maxLen = 50) {
1118
+ if (fullPath.length <= maxLen)
1119
+ return fullPath;
1120
+ const parts = fullPath.split('/').filter(Boolean);
1121
+ if (parts.length <= 2)
1122
+ return fullPath;
1123
+ // Start from the end and build up until we exceed maxLen
1124
+ let result = parts[parts.length - 1] || '';
1125
+ for (let i = parts.length - 2; i >= 0; i--) {
1126
+ const candidate = `${parts[i]}/${result}`;
1127
+ if (candidate.length > maxLen - 3) {
1128
+ return `.../${result}`;
1129
+ }
1130
+ result = candidate;
1131
+ }
1132
+ return result;
1133
+ }
1134
+ /**
1135
+ * Set file change tracking callback
1136
+ */
1137
+ setFileChangeCallback(callback) {
1138
+ this.fileChangeCallback = callback;
1139
+ }
1140
+ /**
1141
+ * Set tool status callback - called when tools start/complete to update dynamic status
1142
+ */
1143
+ setToolStatusCallback(callback) {
1144
+ this._toolStatusCallback = callback;
1145
+ if (callback && this.activeToolStatus) {
1146
+ callback(this.buildRunningStatusLine(this.activeToolStatus.description, this.activeToolStatus.startedAt));
1147
+ }
1148
+ }
1149
+ /**
1150
+ * Set activity callback - called during tool progress to update activity line with streaming output
1151
+ */
1152
+ setActivityCallback(callback) {
1153
+ this._activityCallback = callback;
1154
+ }
1155
+ /**
1156
+ * Keep a live status line ticking while an SSE tool or long-running tool is active.
1157
+ * Uses a heartbeat to animate the spinner and update elapsed time without spamming writes.
1158
+ */
1159
+ startToolStatusHeartbeat(callId) {
1160
+ if (!this._toolStatusCallback || !this.activeToolStatus) {
122
1161
  return;
123
- const current = this.toolUsage.get(toolName) ?? 0;
124
- this.toolUsage.set(toolName, current + 1);
125
- }
126
- formatToolCall(call) {
127
- const args = this.summarizeArgs(call.arguments ?? {});
128
- return args ? `${call.name}(${args})` : call.name;
129
- }
130
- summarizeArgs(args) {
131
- if (!args || typeof args !== 'object') {
132
- return '';
133
- }
134
- const entries = Object.entries(args);
135
- if (entries.length === 0)
136
- return '';
137
- const summary = entries.slice(0, 3).map(([key, value]) => `${key}: ${this.limitValue(value)}`).join(', ');
138
- const suffix = entries.length > 3 ? ' …' : '';
139
- return `${summary}${suffix}`;
140
- }
141
- limitValue(value) {
142
- const text = typeof value === 'string'
143
- ? value
144
- : value === null || value === undefined
145
- ? ''
146
- : JSON.stringify(value);
147
- if (!text)
148
- return '';
149
- const normalized = text.replace(/\s+/g, ' ').trim();
150
- return normalized.length > 48 ? `${normalized.slice(0, 45)}…` : normalized;
151
- }
152
- formatToolResult(call, output, ok) {
153
- const full = (output ?? '').toString().trim();
154
- const firstLine = full.split('\n').find(line => line.trim().length > 0) ?? (ok ? 'Completed' : 'Error');
155
- const needsHint = full.includes('\n') || full.length > 120;
156
- const summary = `⎿ ${call.name}: ${firstLine}${needsHint ? ' (ctrl+o to expand)' : ''}`;
157
- return { summary, full: full || firstLine };
1162
+ }
1163
+ // Stop any previous heartbeat to avoid overlapping timers
1164
+ this.stopToolStatusHeartbeat();
1165
+ const heartbeatId = `tool-status:${callId}`;
1166
+ this.activeToolHeartbeatId = heartbeatId;
1167
+ this.updateCoordinator.startHeartbeat(heartbeatId, {
1168
+ intervalMs: 1000,
1169
+ description: 'live tool status ticker',
1170
+ lane: 'status',
1171
+ priority: 'low',
1172
+ mode: ['processing', 'tooling', 'streaming', 'mcp'],
1173
+ run: () => {
1174
+ if (!this._toolStatusCallback || !this.activeToolStatus) {
1175
+ this.stopToolStatusHeartbeat();
1176
+ return;
1177
+ }
1178
+ const statusLine = this.buildRunningStatusLine(this.activeToolStatus.description, this.activeToolStatus.startedAt);
1179
+ this._toolStatusCallback(statusLine);
1180
+ },
1181
+ });
1182
+ }
1183
+ stopToolStatusHeartbeat() {
1184
+ if (this.activeToolHeartbeatId) {
1185
+ this.updateCoordinator.stopHeartbeat(this.activeToolHeartbeatId);
1186
+ }
1187
+ this.activeToolHeartbeatId = null;
1188
+ this.activeToolStatus = null;
1189
+ this.statusSpinnerIndex = 0;
1190
+ }
1191
+ buildRunningStatusLine(description, startedAt) {
1192
+ const frames = Array.isArray(icons.spinner) ? icons.spinner : [icons.loading];
1193
+ const frame = frames[this.statusSpinnerIndex % frames.length] ?? frames[0] ?? icons.bullet;
1194
+ this.statusSpinnerIndex += 1;
1195
+ const elapsed = this.formatElapsedMs(Date.now() - startedAt);
1196
+ const metaParts = [];
1197
+ if (elapsed) {
1198
+ metaParts.push(`${theme.metrics.elapsedLabel('elapsed')} ${theme.metrics.elapsedValue(elapsed)}`);
1199
+ }
1200
+ if (process.stdout.isTTY) {
1201
+ metaParts.push(theme.ui.muted('esc to interrupt'));
1202
+ }
1203
+ const meta = metaParts.length ? ` (${metaParts.join(theme.ui.muted(' • '))})` : '';
1204
+ return `${theme.info(frame)} ${description}${meta}`;
1205
+ }
1206
+ formatElapsedMs(elapsedMs) {
1207
+ if (typeof elapsedMs !== 'number' || !Number.isFinite(elapsedMs) || elapsedMs < 0) {
1208
+ return null;
1209
+ }
1210
+ const totalSeconds = Math.max(0, Math.round(elapsedMs / 1000));
1211
+ const minutes = Math.floor(totalSeconds / 60);
1212
+ const seconds = totalSeconds % 60;
1213
+ if (minutes > 0) {
1214
+ return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
1215
+ }
1216
+ return `${seconds}s`;
1217
+ }
1218
+ /**
1219
+ * Parse file changes from Edit/Write/NotebookEdit tool output
1220
+ */
1221
+ parseFileChange(call, output) {
1222
+ const args = call.arguments;
1223
+ // Handle different path parameter names: file_path (Edit/Write), notebook_path (NotebookEdit)
1224
+ const path = args.file_path || args.notebook_path || args.path;
1225
+ if (!path)
1226
+ return null;
1227
+ // Parse additions and removals from output
1228
+ let additions = 0;
1229
+ let removals = 0;
1230
+ // For Edit tool: parse "Lines changed: +X / -Y" or "+X addition -Y removal"
1231
+ const editMatch = output.match(/\+(\d+).*-(\d+)/);
1232
+ if (editMatch) {
1233
+ additions = parseInt(editMatch[1] || '0', 10);
1234
+ removals = parseInt(editMatch[2] || '0', 10);
1235
+ }
1236
+ // Alternative format: "X addition" and "Y removal"
1237
+ const addMatch = output.match(/(\d+)\s+addition/);
1238
+ const remMatch = output.match(/(\d+)\s+removal/);
1239
+ if (addMatch)
1240
+ additions = parseInt(addMatch[1] || '0', 10);
1241
+ if (remMatch)
1242
+ removals = parseInt(remMatch[1] || '0', 10);
1243
+ // For Write tool: estimate as all additions (new file or full rewrite)
1244
+ if (call.name === 'Write' && additions === 0 && removals === 0) {
1245
+ const lines = (args.content || '').split('\n').length;
1246
+ additions = lines;
1247
+ }
1248
+ // For NotebookEdit: estimate based on new_source content
1249
+ if (call.name === 'NotebookEdit' && additions === 0 && removals === 0) {
1250
+ const lines = (args.new_source || '').split('\n').length;
1251
+ additions = lines;
1252
+ removals = lines; // Assume cell replacement
1253
+ }
1254
+ return {
1255
+ path,
1256
+ type: call.name === 'Edit' || call.name === 'NotebookEdit' ? 'edit' : 'write',
1257
+ additions,
1258
+ removals
1259
+ };
1260
+ }
1261
+ /**
1262
+ * Display tool result summary using Erosolar-CLI style.
1263
+ * Format: ⎿ Summary text
1264
+ * Routes through display module to work with scroll regions
1265
+ */
1266
+ displayToolResultSummary(call, output, success) {
1267
+ const args = call.arguments || {};
1268
+ // Get compact summary for the result
1269
+ const summary = this.formatClaudeCodeResultSummary(call.name, args, output, success);
1270
+ if (summary) {
1271
+ // Erosolar-CLI style: ⎿ Summary (indented under tool call)
1272
+ const prefix = success
1273
+ ? theme.success(icons.subaction)
1274
+ : theme.error(icons.subaction);
1275
+ // Single newline - next tool call adds its own spacing
1276
+ this.display.stream(` ${prefix} ${summary}\n`);
1277
+ }
1278
+ }
1279
+ /**
1280
+ * Format tool result summary in Erosolar-CLI style
1281
+ * Returns compact summary like "Found 4 files" or "Read 50 lines"
1282
+ */
1283
+ formatClaudeCodeResultSummary(toolName, args, output, success) {
1284
+ if (!success) {
1285
+ // Error case - show error message
1286
+ const errorMsg = output.length > 60 ? `${output.slice(0, 57)}...` : output;
1287
+ return theme.error(errorMsg);
1288
+ }
1289
+ switch (toolName) {
1290
+ case 'Read':
1291
+ case 'read_file': {
1292
+ const lines = this.countLines(output);
1293
+ const path = args['file_path'] || args['path'];
1294
+ if (path) {
1295
+ return `${this.truncatePath(String(path), 40)} · ${theme.info(String(lines))} lines`;
1296
+ }
1297
+ return `Read ${theme.info(String(lines))} lines`;
1298
+ }
1299
+ case 'Write':
1300
+ case 'write_file': {
1301
+ const path = args['file_path'] || args['path'];
1302
+ const lines = (args['content'] || '').split('\n').length;
1303
+ if (path) {
1304
+ return `Wrote ${theme.info(String(lines))} lines to ${this.truncatePath(String(path), 40)}`;
1305
+ }
1306
+ return `Wrote ${theme.info(String(lines))} lines`;
1307
+ }
1308
+ case 'Edit':
1309
+ case 'edit_file': {
1310
+ // Parse additions/removals from output
1311
+ const editMatch = output.match(/\+(\d+).*-(\d+)/);
1312
+ if (editMatch) {
1313
+ const adds = editMatch[1];
1314
+ const removes = editMatch[2];
1315
+ return `${theme.success(`+${adds}`)} ${theme.error(`-${removes}`)} lines`;
1316
+ }
1317
+ return 'Updated file';
1318
+ }
1319
+ case 'Grep':
1320
+ case 'grep':
1321
+ case 'Search': {
1322
+ const outputLines = output.trim().split('\n').filter(l => l.trim());
1323
+ const count = outputLines.length;
1324
+ const outputMode = args['output_mode'];
1325
+ if (count === 0) {
1326
+ return theme.ui.muted('No matches found');
1327
+ }
1328
+ if (outputMode === 'content') {
1329
+ return `Found ${theme.info(String(count))} lines`;
1330
+ }
1331
+ return `Found ${theme.info(String(count))} files`;
1332
+ }
1333
+ case 'Glob':
1334
+ case 'glob': {
1335
+ const files = output.trim().split('\n').filter(f => f.trim());
1336
+ const count = files.length;
1337
+ if (count === 0) {
1338
+ return theme.ui.muted('No files found');
1339
+ }
1340
+ return `Found ${theme.info(String(count))} file${count === 1 ? '' : 's'}`;
1341
+ }
1342
+ case 'list_files': {
1343
+ const files = output.trim().split('\n').filter(f => f.trim());
1344
+ const count = files.length;
1345
+ const path = args['path'] || '.';
1346
+ return `${this.truncatePath(String(path), 40)} · ${theme.info(String(count))} item${count === 1 ? '' : 's'}`;
1347
+ }
1348
+ case 'Bash':
1349
+ case 'bash': {
1350
+ const lines = output.trim().split('\n').filter(l => l).length;
1351
+ if (lines === 0) {
1352
+ return theme.ui.muted('Completed (no output)');
1353
+ }
1354
+ return `Output: ${theme.info(String(lines))} lines`;
1355
+ }
1356
+ case 'WebFetch':
1357
+ case 'web_fetch': {
1358
+ const size = output.length;
1359
+ const sizeStr = size > 1024 ? `${(size / 1024).toFixed(1)}KB` : `${size}B`;
1360
+ return `Fetched ${theme.info(sizeStr)}`;
1361
+ }
1362
+ case 'WebSearch':
1363
+ case 'web_search': {
1364
+ const resultMatches = output.match(/^\d+\./gm);
1365
+ const count = resultMatches ? resultMatches.length : 'multiple';
1366
+ return `Found ${theme.info(String(count))} results`;
1367
+ }
1368
+ case 'Task':
1369
+ case 'task': {
1370
+ // Parse agent output to show nested tool summaries
1371
+ return this.formatAgentResultSummary(output);
1372
+ }
1373
+ case 'TodoWrite':
1374
+ case 'todo_write': {
1375
+ const todos = args['todos'];
1376
+ if (todos && Array.isArray(todos)) {
1377
+ const completed = todos.filter(t => t.status === 'completed').length;
1378
+ const inProgress = todos.filter(t => t.status === 'in_progress').length;
1379
+ const pending = todos.length - completed - inProgress;
1380
+ const parts = [];
1381
+ if (completed > 0)
1382
+ parts.push(theme.success(`${completed} done`));
1383
+ if (inProgress > 0)
1384
+ parts.push(theme.warning(`${inProgress} active`));
1385
+ if (pending > 0)
1386
+ parts.push(theme.ui.muted(`${pending} pending`));
1387
+ return `Todos: ${parts.join(', ')}`;
1388
+ }
1389
+ return 'Todos updated';
1390
+ }
1391
+ case 'grep_search':
1392
+ case 'search_files':
1393
+ case 'Grep': {
1394
+ // Count matches in output - look for file paths or line numbers
1395
+ const lines = output.trim().split('\n').filter(l => l.trim());
1396
+ if (lines.length === 0 || output.includes('No matches found') || output.includes('no matches')) {
1397
+ return theme.ui.muted('No matches');
1398
+ }
1399
+ // Count unique files or match lines
1400
+ const fileMatches = output.match(/^[^\n:]+:\d+:/gm);
1401
+ if (fileMatches) {
1402
+ const uniqueFiles = new Set(fileMatches.map(m => m.split(':')[0])).size;
1403
+ return `${theme.info(String(fileMatches.length))} matches in ${uniqueFiles} file${uniqueFiles === 1 ? '' : 's'}`;
1404
+ }
1405
+ return `${theme.info(String(lines.length))} matches`;
1406
+ }
1407
+ default: {
1408
+ // Generic result - show more info for better context
1409
+ const lines = output.trim().split('\n').filter(l => l).length;
1410
+ if (lines === 0) {
1411
+ return theme.ui.muted('No output');
1412
+ }
1413
+ if (lines <= 3) {
1414
+ // Show the actual output if short
1415
+ const shortOutput = output.trim().substring(0, 60);
1416
+ return shortOutput.length < output.trim().length ? `${shortOutput}…` : shortOutput;
1417
+ }
1418
+ return `${theme.info(String(lines))} lines`;
1419
+ }
1420
+ }
1421
+ }
1422
+ /**
1423
+ * Format agent/task result summary showing nested tool uses
1424
+ * Example output:
1425
+ * Read 100 lines
1426
+ * Found 4 lines
1427
+ * +31 more tool uses
1428
+ */
1429
+ formatAgentResultSummary(output) {
1430
+ const lines = [];
1431
+ // Parse output for tool use summaries
1432
+ // Look for patterns like "Read X lines", "Found X files", "Output: X lines", etc.
1433
+ const toolPatterns = [
1434
+ /Read\s+(\d+)\s+lines?/gi,
1435
+ /Found\s+(\d+)\s+(?:files?|lines?|results?|matches?)/gi,
1436
+ /Output:\s+(\d+)\s+lines?/gi,
1437
+ /Wrote\s+(\d+)\s+lines?/gi,
1438
+ /Fetched\s+[\d.]+[KMB]?B?/gi,
1439
+ /Completed/gi,
1440
+ /Updated\s+file/gi,
1441
+ /\+\d+\s+-\d+\s+lines?/gi,
1442
+ ];
1443
+ const toolUses = [];
1444
+ const seenPatterns = new Set();
1445
+ for (const pattern of toolPatterns) {
1446
+ const matches = output.matchAll(pattern);
1447
+ for (const match of matches) {
1448
+ const text = match[0];
1449
+ if (!seenPatterns.has(text.toLowerCase())) {
1450
+ seenPatterns.add(text.toLowerCase());
1451
+ toolUses.push(text);
1452
+ }
1453
+ }
1454
+ }
1455
+ // Show first 2 tool uses, then "+N more"
1456
+ const maxShown = 2;
1457
+ const shown = toolUses.slice(0, maxShown);
1458
+ const remaining = toolUses.length - maxShown;
1459
+ for (const use of shown) {
1460
+ lines.push(use);
1461
+ }
1462
+ if (remaining > 0) {
1463
+ lines.push(`+${remaining} more tool uses`);
1464
+ }
1465
+ else if (lines.length === 0) {
1466
+ // Fallback if no tool uses found
1467
+ const outputLines = output.trim().split('\n').filter(l => l.trim()).length;
1468
+ if (outputLines > 0) {
1469
+ return `${theme.info(String(outputLines))} lines`;
1470
+ }
1471
+ return 'Completed';
1472
+ }
1473
+ return lines.join('\n ');
1474
+ }
1475
+ /**
1476
+ * Extract the diff section from Edit tool output, removing repeated headers.
1477
+ */
1478
+ extractEditDiffSection(output) {
1479
+ if (!output.trim()) {
1480
+ return null;
1481
+ }
1482
+ const lines = output.split('\n');
1483
+ let startIndex = 0;
1484
+ if (lines[startIndex]?.trim().startsWith('⏺')) {
1485
+ startIndex += 1;
1486
+ }
1487
+ if (lines[startIndex]?.trim().startsWith('⎿')) {
1488
+ startIndex += 1;
1489
+ }
1490
+ const remaining = lines
1491
+ .slice(startIndex)
1492
+ .filter(line => line.trim().length > 0);
1493
+ if (!remaining.length) {
1494
+ return null;
1495
+ }
1496
+ return remaining.join('\n');
1497
+ }
1498
+ /**
1499
+ * Count lines in output while ignoring trailing blank lines.
1500
+ */
1501
+ countLines(output) {
1502
+ if (!output) {
1503
+ return 0;
1504
+ }
1505
+ const normalized = output.replace(/\r\n/g, '\n').replace(/\n+$/g, '');
1506
+ if (!normalized) {
1507
+ return 0;
1508
+ }
1509
+ return normalized.split('\n').length;
1510
+ }
1511
+ /**
1512
+ * Get UI state
1513
+ */
1514
+ getState() {
1515
+ return this.uiController.getState();
1516
+ }
1517
+ /**
1518
+ * Dispose of resources
1519
+ */
1520
+ dispose() {
1521
+ this.uiController.dispose();
158
1522
  }
159
1523
  }
160
1524
  //# sourceMappingURL=ShellUIAdapter.js.map