otherwise-cli 0.1.0
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 +193 -0
- package/bin/otherwise.js +5 -0
- package/frontend/404.html +84 -0
- package/frontend/assets/OpenDyslexic3-Bold-CDyRs55Y.ttf +0 -0
- package/frontend/assets/OpenDyslexic3-Regular-CIBXa4WE.ttf +0 -0
- package/frontend/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/frontend/assets/conversational-worker-CeKiciGk.js +2929 -0
- package/frontend/assets/dictation-worker-D0aYfq8b.js +29 -0
- package/frontend/assets/gemini-color-CgSQmmva.png +0 -0
- package/frontend/assets/index-BLux5ps4.js +21 -0
- package/frontend/assets/index-Blh8_TEM.js +5272 -0
- package/frontend/assets/index-BpQ1PuKu.js +18 -0
- package/frontend/assets/index-Df737c8w.css +1 -0
- package/frontend/assets/index-xaYHL6wb.js +113 -0
- package/frontend/assets/ort-wasm-simd-threaded.asyncify-BynIiDiv.wasm +0 -0
- package/frontend/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm +0 -0
- package/frontend/assets/transformers-tULNc5V3.js +31 -0
- package/frontend/assets/tts-worker-DPJWqT7N.js +2899 -0
- package/frontend/assets/voice-mode-worker-GzvIE_uh.js +2927 -0
- package/frontend/assets/worker-2d5ABSLU.js +31 -0
- package/frontend/banner.png +0 -0
- package/frontend/favicon.svg +3 -0
- package/frontend/google55e5ec47ee14a5f8.html +1 -0
- package/frontend/index.html +234 -0
- package/frontend/manifest.json +17 -0
- package/frontend/pdf.worker.min.mjs +21 -0
- package/frontend/robots.txt +5 -0
- package/frontend/sitemap.xml +27 -0
- package/package.json +81 -0
- package/src/agent/index.js +1066 -0
- package/src/agent/location.js +51 -0
- package/src/agent/prompt.js +548 -0
- package/src/agent/tools.js +4372 -0
- package/src/browser/detect.js +68 -0
- package/src/browser/session.js +1109 -0
- package/src/config.js +137 -0
- package/src/email/client.js +503 -0
- package/src/index.js +557 -0
- package/src/inference/anthropic.js +113 -0
- package/src/inference/google.js +373 -0
- package/src/inference/index.js +81 -0
- package/src/inference/ollama.js +383 -0
- package/src/inference/openai.js +140 -0
- package/src/inference/openrouter.js +378 -0
- package/src/inference/xai.js +200 -0
- package/src/logBridge.js +9 -0
- package/src/models.js +146 -0
- package/src/remote/client.js +225 -0
- package/src/scheduler/cron.js +243 -0
- package/src/server.js +3876 -0
- package/src/storage/db.js +1135 -0
- package/src/storage/supabase.js +364 -0
- package/src/tunnel/cloudflare.js +241 -0
- package/src/ui/components/App.jsx +687 -0
- package/src/ui/components/BrowserSelect.jsx +111 -0
- package/src/ui/components/FilePicker.jsx +472 -0
- package/src/ui/components/Header.jsx +444 -0
- package/src/ui/components/HelpPanel.jsx +173 -0
- package/src/ui/components/HistoryPanel.jsx +158 -0
- package/src/ui/components/MessageList.jsx +235 -0
- package/src/ui/components/ModelSelector.jsx +304 -0
- package/src/ui/components/PromptInput.jsx +515 -0
- package/src/ui/components/StreamingResponse.jsx +134 -0
- package/src/ui/components/ThinkingIndicator.jsx +365 -0
- package/src/ui/components/ToolExecution.jsx +714 -0
- package/src/ui/components/index.js +82 -0
- package/src/ui/context/TerminalContext.jsx +150 -0
- package/src/ui/context/index.js +13 -0
- package/src/ui/hooks/index.js +16 -0
- package/src/ui/hooks/useChatState.js +675 -0
- package/src/ui/hooks/useCommands.js +280 -0
- package/src/ui/hooks/useFileAttachments.js +216 -0
- package/src/ui/hooks/useKeyboardShortcuts.js +173 -0
- package/src/ui/hooks/useNotifications.js +185 -0
- package/src/ui/hooks/useTerminalSize.js +151 -0
- package/src/ui/hooks/useWebSocket.js +273 -0
- package/src/ui/index.js +94 -0
- package/src/ui/ink-runner.js +22 -0
- package/src/ui/utils/formatters.js +424 -0
- package/src/ui/utils/index.js +6 -0
- package/src/ui/utils/markdown.js +166 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolExecution components
|
|
3
|
+
* Display tool calls with their status, progress, and results
|
|
4
|
+
* Enhanced with animations, diff views, and compact/expanded modes
|
|
5
|
+
*
|
|
6
|
+
* Responsive: adapts borders and content width to terminal size
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
10
|
+
import { Box, Text } from 'ink';
|
|
11
|
+
import {
|
|
12
|
+
getToolIcon,
|
|
13
|
+
formatToolName,
|
|
14
|
+
getPrimaryArg,
|
|
15
|
+
isStreamingTool,
|
|
16
|
+
isHiddenTool,
|
|
17
|
+
formatDuration,
|
|
18
|
+
formatSize,
|
|
19
|
+
responsiveTruncate,
|
|
20
|
+
} from '../utils/formatters.js';
|
|
21
|
+
import { ToolState } from '../hooks/useChatState.js';
|
|
22
|
+
import { useTerminal } from '../context/TerminalContext.jsx';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gradient colors for animated spinner
|
|
26
|
+
*/
|
|
27
|
+
const SPINNER_GRADIENT = ['#f59e0b', '#fbbf24', '#fcd34d', '#fbbf24'];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Spinner frames
|
|
31
|
+
*/
|
|
32
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Animated tool status icon
|
|
36
|
+
*/
|
|
37
|
+
function ToolStatusIcon({ status, animate = true }) {
|
|
38
|
+
const [frame, setFrame] = useState(0);
|
|
39
|
+
const [colorIndex, setColorIndex] = useState(0);
|
|
40
|
+
const [successAnim, setSuccessAnim] = useState(0);
|
|
41
|
+
|
|
42
|
+
const isRunning = status === ToolState.RUNNING || status === ToolState.STREAMING;
|
|
43
|
+
|
|
44
|
+
// Spinner animation
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!isRunning || !animate) return;
|
|
47
|
+
|
|
48
|
+
const spinTimer = setInterval(() => {
|
|
49
|
+
setFrame(f => (f + 1) % SPINNER_FRAMES.length);
|
|
50
|
+
}, 80);
|
|
51
|
+
|
|
52
|
+
const colorTimer = setInterval(() => {
|
|
53
|
+
setColorIndex(c => (c + 1) % SPINNER_GRADIENT.length);
|
|
54
|
+
}, 150);
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
clearInterval(spinTimer);
|
|
58
|
+
clearInterval(colorTimer);
|
|
59
|
+
};
|
|
60
|
+
}, [isRunning, animate]);
|
|
61
|
+
|
|
62
|
+
// Success animation (checkmark appears with brief highlight)
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (status !== ToolState.COMPLETE) return;
|
|
65
|
+
|
|
66
|
+
setSuccessAnim(0);
|
|
67
|
+
const steps = [1, 2, 3];
|
|
68
|
+
let step = 0;
|
|
69
|
+
|
|
70
|
+
const timer = setInterval(() => {
|
|
71
|
+
if (step < steps.length) {
|
|
72
|
+
setSuccessAnim(steps[step]);
|
|
73
|
+
step++;
|
|
74
|
+
} else {
|
|
75
|
+
clearInterval(timer);
|
|
76
|
+
}
|
|
77
|
+
}, 100);
|
|
78
|
+
|
|
79
|
+
return () => clearInterval(timer);
|
|
80
|
+
}, [status]);
|
|
81
|
+
|
|
82
|
+
switch (status) {
|
|
83
|
+
case ToolState.RUNNING:
|
|
84
|
+
case ToolState.STREAMING:
|
|
85
|
+
return (
|
|
86
|
+
<Text color={SPINNER_GRADIENT[colorIndex]} bold>
|
|
87
|
+
{SPINNER_FRAMES[frame]}
|
|
88
|
+
</Text>
|
|
89
|
+
);
|
|
90
|
+
case ToolState.COMPLETE:
|
|
91
|
+
const successColor = successAnim < 3 ? '#4ade80' : '#22c55e';
|
|
92
|
+
return <Text color={successColor} bold>✓</Text>;
|
|
93
|
+
case ToolState.ERROR:
|
|
94
|
+
return <Text color="#ef4444" bold>✗</Text>;
|
|
95
|
+
case ToolState.PREPARING:
|
|
96
|
+
return <Text color="#f59e0b">◐</Text>;
|
|
97
|
+
default:
|
|
98
|
+
return <Text dimColor>○</Text>;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Tool header line (icon + name + path + status) with animation
|
|
104
|
+
* Responsive: truncates path based on available width
|
|
105
|
+
*/
|
|
106
|
+
export function ToolHeader({ name, args, status, duration, compact = false, maxArgWidth = null }) {
|
|
107
|
+
const icon = getToolIcon(name);
|
|
108
|
+
const displayName = formatToolName(name);
|
|
109
|
+
const rawPrimaryArg = getPrimaryArg(name, args);
|
|
110
|
+
|
|
111
|
+
// Truncate argument based on max width if provided
|
|
112
|
+
const primaryArg = maxArgWidth && rawPrimaryArg
|
|
113
|
+
? responsiveTruncate(rawPrimaryArg, maxArgWidth)
|
|
114
|
+
: rawPrimaryArg;
|
|
115
|
+
|
|
116
|
+
const isRunning = status === ToolState.RUNNING || status === ToolState.STREAMING;
|
|
117
|
+
|
|
118
|
+
if (compact) {
|
|
119
|
+
return (
|
|
120
|
+
<Box>
|
|
121
|
+
<ToolStatusIcon status={status} />
|
|
122
|
+
<Text> {icon} </Text>
|
|
123
|
+
<Text color="#06b6d4">{displayName}</Text>
|
|
124
|
+
{duration != null && duration > 0 && <Text dimColor> {duration}ms</Text>}
|
|
125
|
+
</Box>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Box>
|
|
131
|
+
<Text color="#374151">│</Text>
|
|
132
|
+
<Text> {icon} </Text>
|
|
133
|
+
<Text color="#06b6d4" bold>{displayName}</Text>
|
|
134
|
+
{primaryArg && (
|
|
135
|
+
<Text color="#9ca3af" wrap="truncate"> {primaryArg}</Text>
|
|
136
|
+
)}
|
|
137
|
+
<Text> </Text>
|
|
138
|
+
<ToolStatusIcon status={status} />
|
|
139
|
+
{isRunning && isStreamingTool(name) && (
|
|
140
|
+
<Text color="#f59e0b"> writing...</Text>
|
|
141
|
+
)}
|
|
142
|
+
{duration && duration > 50 && (
|
|
143
|
+
<Text dimColor> {duration}ms</Text>
|
|
144
|
+
)}
|
|
145
|
+
</Box>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Animated progress bar with gradient
|
|
151
|
+
*/
|
|
152
|
+
export function AnimatedProgressBar({
|
|
153
|
+
progress, // 0-100
|
|
154
|
+
width = 20,
|
|
155
|
+
showPercent = true,
|
|
156
|
+
color = '#22c55e',
|
|
157
|
+
animate = true,
|
|
158
|
+
}) {
|
|
159
|
+
const [shimmerOffset, setShimmerOffset] = useState(0);
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!animate || progress >= 100) return;
|
|
163
|
+
|
|
164
|
+
const timer = setInterval(() => {
|
|
165
|
+
setShimmerOffset(o => (o + 1) % width);
|
|
166
|
+
}, 100);
|
|
167
|
+
|
|
168
|
+
return () => clearInterval(timer);
|
|
169
|
+
}, [animate, progress, width]);
|
|
170
|
+
|
|
171
|
+
const filled = Math.round((progress / 100) * width);
|
|
172
|
+
const empty = width - filled;
|
|
173
|
+
|
|
174
|
+
// Create bar with shimmer effect
|
|
175
|
+
const bar = [];
|
|
176
|
+
for (let i = 0; i < filled; i++) {
|
|
177
|
+
const isShimmer = animate && i === shimmerOffset && progress < 100;
|
|
178
|
+
bar.push(
|
|
179
|
+
<Text key={i} color={isShimmer ? '#86efac' : color}>█</Text>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
for (let i = 0; i < empty; i++) {
|
|
183
|
+
bar.push(<Text key={filled + i} color="#374151">░</Text>);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Box>
|
|
188
|
+
{bar}
|
|
189
|
+
{showPercent && (
|
|
190
|
+
<Text dimColor> {Math.round(progress)}%</Text>
|
|
191
|
+
)}
|
|
192
|
+
</Box>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Streaming progress bar for file writes
|
|
198
|
+
*/
|
|
199
|
+
export function StreamingProgress({ content, maxWidth = 25, targetLines = 50 }) {
|
|
200
|
+
if (!content) return null;
|
|
201
|
+
|
|
202
|
+
const lineCount = (content.match(/\n/g) || []).length + 1;
|
|
203
|
+
const progress = Math.min((lineCount / targetLines) * 100, 100);
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<Box marginLeft={3}>
|
|
207
|
+
<Text color="#374151">├─ </Text>
|
|
208
|
+
<AnimatedProgressBar progress={progress} width={maxWidth} showPercent={false} />
|
|
209
|
+
<Text dimColor> {lineCount} lines</Text>
|
|
210
|
+
</Box>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* File diff view for edit operations
|
|
216
|
+
*/
|
|
217
|
+
export function DiffView({ oldContent, newContent, maxLines = 10 }) {
|
|
218
|
+
const lines = useMemo(() => {
|
|
219
|
+
if (!oldContent && !newContent) return [];
|
|
220
|
+
|
|
221
|
+
const oldLines = (oldContent || '').split('\n');
|
|
222
|
+
const newLines = (newContent || '').split('\n');
|
|
223
|
+
|
|
224
|
+
const result = [];
|
|
225
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
226
|
+
|
|
227
|
+
for (let i = 0; i < Math.min(maxLen, maxLines); i++) {
|
|
228
|
+
const oldLine = oldLines[i];
|
|
229
|
+
const newLine = newLines[i];
|
|
230
|
+
|
|
231
|
+
if (oldLine === newLine) {
|
|
232
|
+
result.push({ type: 'same', content: newLine || '' });
|
|
233
|
+
} else if (oldLine && !newLine) {
|
|
234
|
+
result.push({ type: 'removed', content: oldLine });
|
|
235
|
+
} else if (!oldLine && newLine) {
|
|
236
|
+
result.push({ type: 'added', content: newLine });
|
|
237
|
+
} else {
|
|
238
|
+
result.push({ type: 'removed', content: oldLine });
|
|
239
|
+
result.push({ type: 'added', content: newLine });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (maxLen > maxLines) {
|
|
244
|
+
result.push({ type: 'info', content: `... ${maxLen - maxLines} more lines` });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return result;
|
|
248
|
+
}, [oldContent, newContent, maxLines]);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<Box flexDirection="column" marginLeft={3}>
|
|
252
|
+
{lines.map((line, i) => {
|
|
253
|
+
let prefix, color;
|
|
254
|
+
switch (line.type) {
|
|
255
|
+
case 'added':
|
|
256
|
+
prefix = '+';
|
|
257
|
+
color = '#22c55e';
|
|
258
|
+
break;
|
|
259
|
+
case 'removed':
|
|
260
|
+
prefix = '-';
|
|
261
|
+
color = '#ef4444';
|
|
262
|
+
break;
|
|
263
|
+
case 'info':
|
|
264
|
+
prefix = ' ';
|
|
265
|
+
color = '#6b7280';
|
|
266
|
+
break;
|
|
267
|
+
default:
|
|
268
|
+
prefix = ' ';
|
|
269
|
+
color = '#9ca3af';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<Box key={i}>
|
|
274
|
+
<Text color={color}>{prefix} {line.content}</Text>
|
|
275
|
+
</Box>
|
|
276
|
+
);
|
|
277
|
+
})}
|
|
278
|
+
</Box>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Directory tree view for list_dir results
|
|
284
|
+
*/
|
|
285
|
+
export function DirectoryTree({ items, maxItems = 15 }) {
|
|
286
|
+
if (!items || !Array.isArray(items)) return null;
|
|
287
|
+
|
|
288
|
+
const displayItems = items.slice(0, maxItems);
|
|
289
|
+
const hasMore = items.length > maxItems;
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<Box flexDirection="column" marginLeft={3}>
|
|
293
|
+
{displayItems.map((item, i) => {
|
|
294
|
+
const isLast = i === displayItems.length - 1 && !hasMore;
|
|
295
|
+
const prefix = isLast ? '└─' : '├─';
|
|
296
|
+
const icon = item.isDirectory ? '📁' : '📄';
|
|
297
|
+
const color = item.isDirectory ? '#fbbf24' : '#9ca3af';
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<Box key={i}>
|
|
301
|
+
<Text color="#374151">{prefix} </Text>
|
|
302
|
+
<Text>{icon} </Text>
|
|
303
|
+
<Text color={color}>{item.name}</Text>
|
|
304
|
+
{item.size != null && item.size > 0 && !item.isDirectory && (
|
|
305
|
+
<Text dimColor> ({formatSize(item.size)})</Text>
|
|
306
|
+
)}
|
|
307
|
+
</Box>
|
|
308
|
+
);
|
|
309
|
+
})}
|
|
310
|
+
{hasMore && (
|
|
311
|
+
<Box>
|
|
312
|
+
<Text color="#374151">└─ </Text>
|
|
313
|
+
<Text dimColor>... {items.length - maxItems} more items</Text>
|
|
314
|
+
</Box>
|
|
315
|
+
)}
|
|
316
|
+
</Box>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Shell output with syntax highlighting
|
|
322
|
+
*/
|
|
323
|
+
export function ShellOutput({ output, exitCode, maxLines = 15 }) {
|
|
324
|
+
const lines = (output || '').split('\n').filter(l => l.trim());
|
|
325
|
+
const displayLines = lines.slice(0, maxLines);
|
|
326
|
+
const hasMore = lines.length > maxLines;
|
|
327
|
+
|
|
328
|
+
const isError = exitCode && exitCode !== 0;
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<Box flexDirection="column" marginLeft={3}>
|
|
332
|
+
{displayLines.map((line, i) => {
|
|
333
|
+
// Highlight errors
|
|
334
|
+
const isErrorLine = line.toLowerCase().includes('error') ||
|
|
335
|
+
line.toLowerCase().includes('failed') ||
|
|
336
|
+
line.startsWith('!');
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<Box key={i}>
|
|
340
|
+
<Text color="#374151">│ </Text>
|
|
341
|
+
<Text color={isErrorLine ? '#ef4444' : '#9ca3af'}>{line}</Text>
|
|
342
|
+
</Box>
|
|
343
|
+
);
|
|
344
|
+
})}
|
|
345
|
+
{hasMore && (
|
|
346
|
+
<Box>
|
|
347
|
+
<Text color="#374151">│ </Text>
|
|
348
|
+
<Text dimColor>... {lines.length - maxLines} more lines</Text>
|
|
349
|
+
</Box>
|
|
350
|
+
)}
|
|
351
|
+
{exitCode !== undefined && (
|
|
352
|
+
<Box>
|
|
353
|
+
<Text color="#374151">└─ </Text>
|
|
354
|
+
<Text color={isError ? '#ef4444' : '#22c55e'}>
|
|
355
|
+
exit {exitCode}
|
|
356
|
+
</Text>
|
|
357
|
+
</Box>
|
|
358
|
+
)}
|
|
359
|
+
</Box>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Tool result display with intelligent formatting
|
|
365
|
+
*/
|
|
366
|
+
export function ToolResult({ name, result, maxLines = 10, expanded = false }) {
|
|
367
|
+
const [isExpanded, setIsExpanded] = useState(expanded);
|
|
368
|
+
|
|
369
|
+
if (!result) {
|
|
370
|
+
return (
|
|
371
|
+
<Box marginLeft={3}>
|
|
372
|
+
<Text color="#374151">└─ </Text>
|
|
373
|
+
<Text dimColor>(no output)</Text>
|
|
374
|
+
</Box>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Handle specific tool types
|
|
379
|
+
if (name === 'list_dir' || name === 'list_directory' || name === 'read_dir') {
|
|
380
|
+
if (Array.isArray(result) || (result.items && Array.isArray(result.items))) {
|
|
381
|
+
return <DirectoryTree items={result.items || result} maxItems={maxLines} />;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (name === 'shell' || name === 'run_command' || name === 'execute_command') {
|
|
386
|
+
return <ShellOutput output={result.output || result} exitCode={result.exitCode} maxLines={maxLines} />;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Handle string results
|
|
390
|
+
if (typeof result === 'string') {
|
|
391
|
+
const lines = result.split('\n');
|
|
392
|
+
const displayLines = isExpanded ? lines : lines.slice(0, maxLines);
|
|
393
|
+
const hasMore = lines.length > maxLines;
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<Box flexDirection="column" marginLeft={3}>
|
|
397
|
+
{displayLines.map((line, i) => {
|
|
398
|
+
const isLast = i === displayLines.length - 1 && !hasMore;
|
|
399
|
+
return (
|
|
400
|
+
<Box key={i}>
|
|
401
|
+
<Text color="#374151">{isLast ? '└─' : '├─'} </Text>
|
|
402
|
+
<Text color="#9ca3af">{line}</Text>
|
|
403
|
+
</Box>
|
|
404
|
+
);
|
|
405
|
+
})}
|
|
406
|
+
{hasMore && !isExpanded && (
|
|
407
|
+
<Box>
|
|
408
|
+
<Text color="#374151">└─ </Text>
|
|
409
|
+
<Text dimColor>... {lines.length - maxLines} more lines</Text>
|
|
410
|
+
</Box>
|
|
411
|
+
)}
|
|
412
|
+
</Box>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Handle object results
|
|
417
|
+
if (typeof result === 'object') {
|
|
418
|
+
if (result.error) {
|
|
419
|
+
return (
|
|
420
|
+
<Box marginLeft={3}>
|
|
421
|
+
<Text color="#374151">└─ </Text>
|
|
422
|
+
<Text color="#ef4444">Error: {result.error}</Text>
|
|
423
|
+
</Box>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (result.success !== undefined) {
|
|
428
|
+
return (
|
|
429
|
+
<Box marginLeft={3}>
|
|
430
|
+
<Text color="#374151">└─ </Text>
|
|
431
|
+
<Text color={result.success ? '#22c55e' : '#ef4444'}>
|
|
432
|
+
{result.success ? '✓ Success' : '✗ Failed'}
|
|
433
|
+
</Text>
|
|
434
|
+
{result.message && <Text color="#9ca3af">: {result.message}</Text>}
|
|
435
|
+
</Box>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Handle file content
|
|
440
|
+
if (result.content !== undefined) {
|
|
441
|
+
const lines = result.content.split('\n');
|
|
442
|
+
const displayLines = lines.slice(0, maxLines);
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
<Box flexDirection="column" marginLeft={3}>
|
|
446
|
+
{result.path && (
|
|
447
|
+
<Box>
|
|
448
|
+
<Text color="#374151">├─ </Text>
|
|
449
|
+
<Text dimColor>{result.path}</Text>
|
|
450
|
+
{result.lineCount != null && result.lineCount > 0 && <Text dimColor> ({result.lineCount} lines)</Text>}
|
|
451
|
+
</Box>
|
|
452
|
+
)}
|
|
453
|
+
{displayLines.map((line, i) => {
|
|
454
|
+
const lineNum = String(i + 1).padStart(3, ' ');
|
|
455
|
+
return (
|
|
456
|
+
<Box key={i}>
|
|
457
|
+
<Text color="#374151">│ </Text>
|
|
458
|
+
<Text color="#4b5563">{lineNum} │ </Text>
|
|
459
|
+
<Text color="#9ca3af">{line}</Text>
|
|
460
|
+
</Box>
|
|
461
|
+
);
|
|
462
|
+
})}
|
|
463
|
+
{lines.length > maxLines && (
|
|
464
|
+
<Box>
|
|
465
|
+
<Text color="#374151">└─ </Text>
|
|
466
|
+
<Text dimColor>... {lines.length - maxLines} more lines</Text>
|
|
467
|
+
</Box>
|
|
468
|
+
)}
|
|
469
|
+
</Box>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Generic object display
|
|
474
|
+
const json = JSON.stringify(result, null, 2);
|
|
475
|
+
const lines = json.split('\n').slice(0, maxLines);
|
|
476
|
+
|
|
477
|
+
return (
|
|
478
|
+
<Box flexDirection="column" marginLeft={3}>
|
|
479
|
+
{lines.map((line, i) => (
|
|
480
|
+
<Box key={i}>
|
|
481
|
+
<Text color="#374151">{i === lines.length - 1 ? '└─' : '├─'} </Text>
|
|
482
|
+
<Text color="#9ca3af">{line}</Text>
|
|
483
|
+
</Box>
|
|
484
|
+
))}
|
|
485
|
+
</Box>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Tool error display with styling
|
|
494
|
+
*/
|
|
495
|
+
export function ToolError({ error, suggestion = '' }) {
|
|
496
|
+
const errorMsg = error?.startsWith?.('Error:')
|
|
497
|
+
? error.slice(6).trim()
|
|
498
|
+
: error;
|
|
499
|
+
|
|
500
|
+
return (
|
|
501
|
+
<Box flexDirection="column" marginLeft={3}>
|
|
502
|
+
<Box>
|
|
503
|
+
<Text color="#374151">└─ </Text>
|
|
504
|
+
<Text color="#ef4444" bold>✗ </Text>
|
|
505
|
+
<Text color="#ef4444">{errorMsg}</Text>
|
|
506
|
+
</Box>
|
|
507
|
+
{suggestion && (
|
|
508
|
+
<Box marginLeft={3}>
|
|
509
|
+
<Text dimColor>💡 {suggestion}</Text>
|
|
510
|
+
</Box>
|
|
511
|
+
)}
|
|
512
|
+
</Box>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Collapsible tool execution component
|
|
518
|
+
*/
|
|
519
|
+
export function CollapsibleToolExecution({ tool, defaultExpanded = true }) {
|
|
520
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
|
521
|
+
|
|
522
|
+
// Skip hidden tools
|
|
523
|
+
if (isHiddenTool(tool.name)) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const duration = tool.endTime && tool.startTime
|
|
528
|
+
? tool.endTime - tool.startTime
|
|
529
|
+
: null;
|
|
530
|
+
|
|
531
|
+
const isStreaming = tool.status === ToolState.STREAMING;
|
|
532
|
+
const isComplete = tool.status === ToolState.COMPLETE;
|
|
533
|
+
const hasError = tool.status === ToolState.ERROR;
|
|
534
|
+
const hasOutput = isComplete && tool.result;
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
538
|
+
<Box>
|
|
539
|
+
<Text color="#374151">{isExpanded ? '▼' : '▶'} </Text>
|
|
540
|
+
<ToolHeader
|
|
541
|
+
name={tool.name}
|
|
542
|
+
args={tool.args}
|
|
543
|
+
status={tool.status}
|
|
544
|
+
duration={isComplete ? duration : null}
|
|
545
|
+
compact={!isExpanded}
|
|
546
|
+
/>
|
|
547
|
+
</Box>
|
|
548
|
+
|
|
549
|
+
{isExpanded && (
|
|
550
|
+
<>
|
|
551
|
+
{/* Streaming progress for file writes */}
|
|
552
|
+
{isStreaming && isStreamingTool(tool.name) && (
|
|
553
|
+
<StreamingProgress content={tool.streamingContent} />
|
|
554
|
+
)}
|
|
555
|
+
|
|
556
|
+
{/* Result display */}
|
|
557
|
+
{hasOutput && (
|
|
558
|
+
<ToolResult name={tool.name} result={tool.result} />
|
|
559
|
+
)}
|
|
560
|
+
|
|
561
|
+
{/* Error display */}
|
|
562
|
+
{hasError && tool.error && (
|
|
563
|
+
<ToolError error={tool.error} />
|
|
564
|
+
)}
|
|
565
|
+
</>
|
|
566
|
+
)}
|
|
567
|
+
</Box>
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Complete tool execution component
|
|
573
|
+
* Responsive: adapts border width and content to terminal size
|
|
574
|
+
*/
|
|
575
|
+
export function ToolExecution({ tool, compact = false, showBorder = true }) {
|
|
576
|
+
const { uiWidth, contentWidth, isNarrow, useCompactMode } = useTerminal();
|
|
577
|
+
|
|
578
|
+
// Skip hidden tools
|
|
579
|
+
if (isHiddenTool(tool.name)) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const duration = tool.endTime && tool.startTime
|
|
584
|
+
? tool.endTime - tool.startTime
|
|
585
|
+
: null;
|
|
586
|
+
|
|
587
|
+
const isStreaming = tool.status === ToolState.STREAMING;
|
|
588
|
+
const isComplete = tool.status === ToolState.COMPLETE;
|
|
589
|
+
const hasError = tool.status === ToolState.ERROR;
|
|
590
|
+
|
|
591
|
+
// Use compact mode on very narrow terminals or when explicitly requested
|
|
592
|
+
if (compact || useCompactMode) {
|
|
593
|
+
return (
|
|
594
|
+
<Box>
|
|
595
|
+
<ToolHeader
|
|
596
|
+
name={tool.name}
|
|
597
|
+
args={tool.args}
|
|
598
|
+
status={tool.status}
|
|
599
|
+
duration={isComplete ? duration : null}
|
|
600
|
+
compact={true}
|
|
601
|
+
/>
|
|
602
|
+
</Box>
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Calculate responsive border width
|
|
607
|
+
const borderWidth = Math.min(uiWidth, 50);
|
|
608
|
+
|
|
609
|
+
// Calculate max arg width (leave room for icon, name, status)
|
|
610
|
+
const maxArgWidth = isNarrow ? 20 : Math.max(20, contentWidth - 40);
|
|
611
|
+
|
|
612
|
+
return (
|
|
613
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
614
|
+
{showBorder && (
|
|
615
|
+
<Box>
|
|
616
|
+
<Text color="#374151">┌{'─'.repeat(borderWidth)}</Text>
|
|
617
|
+
</Box>
|
|
618
|
+
)}
|
|
619
|
+
|
|
620
|
+
<ToolHeader
|
|
621
|
+
name={tool.name}
|
|
622
|
+
args={tool.args}
|
|
623
|
+
status={tool.status}
|
|
624
|
+
duration={isComplete ? duration : null}
|
|
625
|
+
maxArgWidth={maxArgWidth}
|
|
626
|
+
/>
|
|
627
|
+
|
|
628
|
+
{/* Streaming progress for file writes */}
|
|
629
|
+
{isStreaming && isStreamingTool(tool.name) && (
|
|
630
|
+
<StreamingProgress content={tool.streamingContent} maxWidth={Math.min(25, borderWidth - 10)} />
|
|
631
|
+
)}
|
|
632
|
+
|
|
633
|
+
{/* Result display */}
|
|
634
|
+
{isComplete && tool.result && (
|
|
635
|
+
<ToolResult name={tool.name} result={tool.result} maxLines={isNarrow ? 5 : 10} />
|
|
636
|
+
)}
|
|
637
|
+
|
|
638
|
+
{/* Error display */}
|
|
639
|
+
{hasError && tool.error && (
|
|
640
|
+
<ToolError error={tool.error} />
|
|
641
|
+
)}
|
|
642
|
+
|
|
643
|
+
{showBorder && (
|
|
644
|
+
<Box>
|
|
645
|
+
<Text color="#374151">└{'─'.repeat(borderWidth)}</Text>
|
|
646
|
+
</Box>
|
|
647
|
+
)}
|
|
648
|
+
</Box>
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Tool list summary with icons
|
|
654
|
+
*/
|
|
655
|
+
export function ToolSummary({ tools, showDetails = false }) {
|
|
656
|
+
const toolList = Object.values(tools);
|
|
657
|
+
if (toolList.length === 0) return null;
|
|
658
|
+
|
|
659
|
+
const completed = toolList.filter(t => t.status === ToolState.COMPLETE).length;
|
|
660
|
+
const errored = toolList.filter(t => t.status === ToolState.ERROR).length;
|
|
661
|
+
const running = toolList.filter(t => t.status === ToolState.RUNNING || t.status === ToolState.STREAMING).length;
|
|
662
|
+
|
|
663
|
+
return (
|
|
664
|
+
<Box flexDirection="column">
|
|
665
|
+
<Box>
|
|
666
|
+
<Text dimColor>🔧 </Text>
|
|
667
|
+
<Text color="#9ca3af">
|
|
668
|
+
{toolList.length} tool{toolList.length !== 1 ? 's' : ''}
|
|
669
|
+
</Text>
|
|
670
|
+
{completed > 0 && (
|
|
671
|
+
<Text color="#22c55e"> ({completed} ✓)</Text>
|
|
672
|
+
)}
|
|
673
|
+
{running > 0 && (
|
|
674
|
+
<Text color="#f59e0b"> ({running} running)</Text>
|
|
675
|
+
)}
|
|
676
|
+
{errored > 0 && (
|
|
677
|
+
<Text color="#ef4444"> ({errored} failed)</Text>
|
|
678
|
+
)}
|
|
679
|
+
</Box>
|
|
680
|
+
|
|
681
|
+
{showDetails && (
|
|
682
|
+
<Box flexDirection="column" marginLeft={3}>
|
|
683
|
+
{toolList.map((tool, i) => (
|
|
684
|
+
<Box key={tool.id || i}>
|
|
685
|
+
<ToolStatusIcon status={tool.status} />
|
|
686
|
+
<Text dimColor> {getToolIcon(tool.name)} {formatToolName(tool.name)}</Text>
|
|
687
|
+
</Box>
|
|
688
|
+
))}
|
|
689
|
+
</Box>
|
|
690
|
+
)}
|
|
691
|
+
</Box>
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Compact tool list (all on one line)
|
|
697
|
+
*/
|
|
698
|
+
export function CompactToolList({ tools }) {
|
|
699
|
+
const toolList = Object.values(tools);
|
|
700
|
+
if (toolList.length === 0) return null;
|
|
701
|
+
|
|
702
|
+
return (
|
|
703
|
+
<Box>
|
|
704
|
+
{toolList.map((tool, i) => (
|
|
705
|
+
<Box key={tool.id || i} marginRight={1}>
|
|
706
|
+
<ToolStatusIcon status={tool.status} />
|
|
707
|
+
<Text> {getToolIcon(tool.name)}</Text>
|
|
708
|
+
</Box>
|
|
709
|
+
))}
|
|
710
|
+
</Box>
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export default ToolExecution;
|