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.
- package/README.md +1 -1
- package/agents/erosolar-code.rules.json +2 -2
- package/agents/general.rules.json +21 -3
- package/dist/capabilities/askUserCapability.js +1 -1
- package/dist/capabilities/askUserCapability.js.map +1 -1
- package/dist/capabilities/statusCapability.js +2 -2
- package/dist/capabilities/statusCapability.js.map +1 -1
- package/dist/codex/capabilities/codexCoreCapability.d.ts +6 -0
- package/dist/codex/capabilities/codexCoreCapability.d.ts.map +1 -0
- package/dist/codex/capabilities/codexCoreCapability.js +516 -0
- package/dist/codex/capabilities/codexCoreCapability.js.map +1 -0
- package/dist/codex/fs.d.ts +4 -0
- package/dist/codex/fs.d.ts.map +1 -0
- package/dist/codex/fs.js +25 -0
- package/dist/codex/fs.js.map +1 -0
- package/dist/codex/persistence/planStore.d.ts +4 -0
- package/dist/codex/persistence/planStore.d.ts.map +1 -0
- package/dist/codex/persistence/planStore.js +59 -0
- package/dist/codex/persistence/planStore.js.map +1 -0
- package/dist/codex/pluginAllowlist.d.ts +4 -0
- package/dist/codex/pluginAllowlist.d.ts.map +1 -0
- package/dist/codex/pluginAllowlist.js +14 -0
- package/dist/codex/pluginAllowlist.js.map +1 -0
- package/dist/codex/types.d.ts +21 -0
- package/dist/codex/types.d.ts.map +1 -0
- package/dist/codex/types.js +62 -0
- package/dist/codex/types.js.map +1 -0
- package/dist/contracts/agent-schemas.json +5 -5
- package/dist/core/agent.d.ts +83 -24
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +499 -248
- package/dist/core/agent.js.map +1 -1
- package/dist/core/preferences.d.ts +1 -0
- package/dist/core/preferences.d.ts.map +1 -1
- package/dist/core/preferences.js +8 -1
- package/dist/core/preferences.js.map +1 -1
- package/dist/core/reliabilityPrompt.d.ts +9 -0
- package/dist/core/reliabilityPrompt.d.ts.map +1 -0
- package/dist/core/reliabilityPrompt.js +31 -0
- package/dist/core/reliabilityPrompt.js.map +1 -0
- package/dist/core/schemaValidator.js +3 -3
- package/dist/core/schemaValidator.js.map +1 -1
- package/dist/core/toolPreconditions.d.ts +0 -11
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +33 -164
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +9 -114
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/updateChecker.d.ts +61 -1
- package/dist/core/updateChecker.d.ts.map +1 -1
- package/dist/core/updateChecker.js +147 -3
- package/dist/core/updateChecker.js.map +1 -1
- package/dist/headless/evalMode.d.ts.map +1 -1
- package/dist/headless/evalMode.js +6 -0
- package/dist/headless/evalMode.js.map +1 -1
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +6 -39
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/mcp/sseClient.d.ts +4 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +36 -2
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/mcp/stdioClient.d.ts +4 -1
- package/dist/mcp/stdioClient.d.ts.map +1 -1
- package/dist/mcp/stdioClient.js +41 -1
- package/dist/mcp/stdioClient.js.map +1 -1
- package/dist/mcp/toolBridge.d.ts +3 -0
- package/dist/mcp/toolBridge.d.ts.map +1 -1
- package/dist/mcp/toolBridge.js +2 -2
- package/dist/mcp/toolBridge.js.map +1 -1
- package/dist/mcp/types.d.ts +18 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
- package/dist/plugins/tools/nodeDefaults.js +0 -2
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- package/dist/providers/openaiResponsesProvider.d.ts.map +1 -1
- package/dist/providers/openaiResponsesProvider.js +79 -74
- package/dist/providers/openaiResponsesProvider.js.map +1 -1
- package/dist/runtime/agentController.d.ts.map +1 -1
- package/dist/runtime/agentController.js +6 -3
- package/dist/runtime/agentController.js.map +1 -1
- package/dist/runtime/agentSession.d.ts +0 -2
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +2 -2
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +25 -18
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +345 -291
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +15 -8
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +4 -15
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/subagents/taskRunner.js +2 -1
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/tools/bashTools.d.ts.map +1 -1
- package/dist/tools/bashTools.js +101 -8
- package/dist/tools/bashTools.js.map +1 -1
- package/dist/tools/diffUtils.d.ts +8 -2
- package/dist/tools/diffUtils.d.ts.map +1 -1
- package/dist/tools/diffUtils.js +72 -13
- package/dist/tools/diffUtils.js.map +1 -1
- package/dist/tools/grepTools.d.ts.map +1 -1
- package/dist/tools/grepTools.js +10 -2
- package/dist/tools/grepTools.js.map +1 -1
- package/dist/tools/planningTools.d.ts +0 -10
- package/dist/tools/planningTools.d.ts.map +1 -1
- package/dist/tools/planningTools.js +0 -16
- package/dist/tools/planningTools.js.map +1 -1
- package/dist/tools/searchTools.d.ts.map +1 -1
- package/dist/tools/searchTools.js +4 -2
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/ui/PromptController.d.ts +7 -4
- package/dist/ui/PromptController.d.ts.map +1 -1
- package/dist/ui/PromptController.js +4 -7
- package/dist/ui/PromptController.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +286 -28
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +1485 -121
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/UnifiedUIController.d.ts +80 -0
- package/dist/ui/UnifiedUIController.d.ts.map +1 -0
- package/dist/ui/UnifiedUIController.js +211 -0
- package/dist/ui/UnifiedUIController.js.map +1 -0
- package/dist/ui/UnifiedUIRenderer.d.ts +102 -46
- package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
- package/dist/ui/UnifiedUIRenderer.js +680 -610
- package/dist/ui/UnifiedUIRenderer.js.map +1 -1
- package/dist/ui/animatedStatus.d.ts +128 -6
- package/dist/ui/animatedStatus.d.ts.map +1 -1
- package/dist/ui/animatedStatus.js +383 -50
- package/dist/ui/animatedStatus.js.map +1 -1
- package/dist/ui/animation/AnimationScheduler.d.ts +192 -0
- package/dist/ui/animation/AnimationScheduler.d.ts.map +1 -0
- package/dist/ui/animation/AnimationScheduler.js +432 -0
- package/dist/ui/animation/AnimationScheduler.js.map +1 -0
- package/dist/ui/display.d.ts +179 -25
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +678 -96
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/inPlaceUpdater.d.ts +181 -0
- package/dist/ui/inPlaceUpdater.d.ts.map +1 -0
- package/dist/ui/inPlaceUpdater.js +515 -0
- package/dist/ui/inPlaceUpdater.js.map +1 -0
- package/dist/ui/interrupts/InterruptManager.d.ts +142 -0
- package/dist/ui/interrupts/InterruptManager.d.ts.map +1 -0
- package/dist/ui/interrupts/InterruptManager.js +439 -0
- package/dist/ui/interrupts/InterruptManager.js.map +1 -0
- package/dist/ui/layout.d.ts +0 -1
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +0 -12
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/orchestration/StatusOrchestrator.d.ts +1 -1
- package/dist/ui/orchestration/StatusOrchestrator.js +1 -1
- package/dist/ui/orchestration/UIUpdateCoordinator.d.ts +61 -7
- package/dist/ui/orchestration/UIUpdateCoordinator.d.ts.map +1 -1
- package/dist/ui/orchestration/UIUpdateCoordinator.js +232 -20
- package/dist/ui/orchestration/UIUpdateCoordinator.js.map +1 -1
- package/dist/ui/shortcutsHelp.d.ts.map +1 -1
- package/dist/ui/shortcutsHelp.js +0 -1
- package/dist/ui/shortcutsHelp.js.map +1 -1
- package/dist/ui/telemetry/ResponseTracker.d.ts +22 -0
- package/dist/ui/telemetry/ResponseTracker.d.ts.map +1 -0
- package/dist/ui/telemetry/ResponseTracker.js +60 -0
- package/dist/ui/telemetry/ResponseTracker.js.map +1 -0
- package/dist/ui/telemetry/UITelemetry.d.ts +181 -0
- package/dist/ui/telemetry/UITelemetry.d.ts.map +1 -0
- package/dist/ui/telemetry/UITelemetry.js +446 -0
- package/dist/ui/telemetry/UITelemetry.js.map +1 -0
- package/dist/ui/unified/index.d.ts +28 -1
- package/dist/ui/unified/index.d.ts.map +1 -1
- package/dist/ui/unified/index.js +41 -2
- package/dist/ui/unified/index.js.map +1 -1
- package/dist/ui/unified/layout.d.ts +12 -0
- package/dist/ui/unified/layout.d.ts.map +1 -0
- package/dist/ui/unified/layout.js +96 -0
- package/dist/ui/unified/layout.js.map +1 -0
- package/package.json +1 -2
- package/dist/StringUtils.d.ts +0 -8
- package/dist/StringUtils.d.ts.map +0 -1
- package/dist/StringUtils.js +0 -11
- package/dist/StringUtils.js.map +0 -1
- package/dist/core/aiFlowSupervisor.d.ts +0 -44
- package/dist/core/aiFlowSupervisor.d.ts.map +0 -1
- package/dist/core/aiFlowSupervisor.js +0 -299
- package/dist/core/aiFlowSupervisor.js.map +0 -1
- package/dist/core/cliTestHarness.d.ts +0 -200
- package/dist/core/cliTestHarness.d.ts.map +0 -1
- package/dist/core/cliTestHarness.js +0 -549
- package/dist/core/cliTestHarness.js.map +0 -1
- package/dist/core/testUtils.d.ts +0 -121
- package/dist/core/testUtils.d.ts.map +0 -1
- package/dist/core/testUtils.js +0 -235
- package/dist/core/testUtils.js.map +0 -1
- package/dist/core/toolValidation.d.ts +0 -116
- package/dist/core/toolValidation.d.ts.map +0 -1
- package/dist/core/toolValidation.js +0 -282
- package/dist/core/toolValidation.js.map +0 -1
- package/dist/ui/planOverlay.d.ts +0 -28
- package/dist/ui/planOverlay.d.ts.map +0 -1
- package/dist/ui/planOverlay.js +0 -156
- package/dist/ui/planOverlay.js.map +0 -1
- package/dist/ui/streamingFormatter.d.ts +0 -30
- package/dist/ui/streamingFormatter.d.ts.map +0 -1
- package/dist/ui/streamingFormatter.js +0 -91
- package/dist/ui/streamingFormatter.js.map +0 -1
- package/dist/utils/errorUtils.d.ts +0 -16
- package/dist/utils/errorUtils.d.ts.map +0 -1
- package/dist/utils/errorUtils.js +0 -66
- package/dist/utils/errorUtils.js.map +0 -1
|
@@ -1,160 +1,1524 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
uiController;
|
|
17
|
+
display;
|
|
18
|
+
config;
|
|
14
19
|
fileChangeCallback;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
this.
|
|
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
|
|
61
|
-
|
|
62
|
-
this.
|
|
63
|
-
|
|
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,
|
|
66
|
-
|
|
67
|
-
this.
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
return `${
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
378
|
+
const code = warning.code ? `[${warning.code}] ` : '';
|
|
379
|
+
const suggestion = warning.suggestion ? ` — ${warning.suggestion}` : '';
|
|
380
|
+
return `${code}${warning.message}${suggestion}`;
|
|
110
381
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
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
|