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.
Files changed (81) hide show
  1. package/README.md +193 -0
  2. package/bin/otherwise.js +5 -0
  3. package/frontend/404.html +84 -0
  4. package/frontend/assets/OpenDyslexic3-Bold-CDyRs55Y.ttf +0 -0
  5. package/frontend/assets/OpenDyslexic3-Regular-CIBXa4WE.ttf +0 -0
  6. package/frontend/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  7. package/frontend/assets/conversational-worker-CeKiciGk.js +2929 -0
  8. package/frontend/assets/dictation-worker-D0aYfq8b.js +29 -0
  9. package/frontend/assets/gemini-color-CgSQmmva.png +0 -0
  10. package/frontend/assets/index-BLux5ps4.js +21 -0
  11. package/frontend/assets/index-Blh8_TEM.js +5272 -0
  12. package/frontend/assets/index-BpQ1PuKu.js +18 -0
  13. package/frontend/assets/index-Df737c8w.css +1 -0
  14. package/frontend/assets/index-xaYHL6wb.js +113 -0
  15. package/frontend/assets/ort-wasm-simd-threaded.asyncify-BynIiDiv.wasm +0 -0
  16. package/frontend/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm +0 -0
  17. package/frontend/assets/transformers-tULNc5V3.js +31 -0
  18. package/frontend/assets/tts-worker-DPJWqT7N.js +2899 -0
  19. package/frontend/assets/voice-mode-worker-GzvIE_uh.js +2927 -0
  20. package/frontend/assets/worker-2d5ABSLU.js +31 -0
  21. package/frontend/banner.png +0 -0
  22. package/frontend/favicon.svg +3 -0
  23. package/frontend/google55e5ec47ee14a5f8.html +1 -0
  24. package/frontend/index.html +234 -0
  25. package/frontend/manifest.json +17 -0
  26. package/frontend/pdf.worker.min.mjs +21 -0
  27. package/frontend/robots.txt +5 -0
  28. package/frontend/sitemap.xml +27 -0
  29. package/package.json +81 -0
  30. package/src/agent/index.js +1066 -0
  31. package/src/agent/location.js +51 -0
  32. package/src/agent/prompt.js +548 -0
  33. package/src/agent/tools.js +4372 -0
  34. package/src/browser/detect.js +68 -0
  35. package/src/browser/session.js +1109 -0
  36. package/src/config.js +137 -0
  37. package/src/email/client.js +503 -0
  38. package/src/index.js +557 -0
  39. package/src/inference/anthropic.js +113 -0
  40. package/src/inference/google.js +373 -0
  41. package/src/inference/index.js +81 -0
  42. package/src/inference/ollama.js +383 -0
  43. package/src/inference/openai.js +140 -0
  44. package/src/inference/openrouter.js +378 -0
  45. package/src/inference/xai.js +200 -0
  46. package/src/logBridge.js +9 -0
  47. package/src/models.js +146 -0
  48. package/src/remote/client.js +225 -0
  49. package/src/scheduler/cron.js +243 -0
  50. package/src/server.js +3876 -0
  51. package/src/storage/db.js +1135 -0
  52. package/src/storage/supabase.js +364 -0
  53. package/src/tunnel/cloudflare.js +241 -0
  54. package/src/ui/components/App.jsx +687 -0
  55. package/src/ui/components/BrowserSelect.jsx +111 -0
  56. package/src/ui/components/FilePicker.jsx +472 -0
  57. package/src/ui/components/Header.jsx +444 -0
  58. package/src/ui/components/HelpPanel.jsx +173 -0
  59. package/src/ui/components/HistoryPanel.jsx +158 -0
  60. package/src/ui/components/MessageList.jsx +235 -0
  61. package/src/ui/components/ModelSelector.jsx +304 -0
  62. package/src/ui/components/PromptInput.jsx +515 -0
  63. package/src/ui/components/StreamingResponse.jsx +134 -0
  64. package/src/ui/components/ThinkingIndicator.jsx +365 -0
  65. package/src/ui/components/ToolExecution.jsx +714 -0
  66. package/src/ui/components/index.js +82 -0
  67. package/src/ui/context/TerminalContext.jsx +150 -0
  68. package/src/ui/context/index.js +13 -0
  69. package/src/ui/hooks/index.js +16 -0
  70. package/src/ui/hooks/useChatState.js +675 -0
  71. package/src/ui/hooks/useCommands.js +280 -0
  72. package/src/ui/hooks/useFileAttachments.js +216 -0
  73. package/src/ui/hooks/useKeyboardShortcuts.js +173 -0
  74. package/src/ui/hooks/useNotifications.js +185 -0
  75. package/src/ui/hooks/useTerminalSize.js +151 -0
  76. package/src/ui/hooks/useWebSocket.js +273 -0
  77. package/src/ui/index.js +94 -0
  78. package/src/ui/ink-runner.js +22 -0
  79. package/src/ui/utils/formatters.js +424 -0
  80. package/src/ui/utils/index.js +6 -0
  81. 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;