erosolar-cli 2.1.171 → 2.1.172

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