osai-agent 4.0.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 (86) hide show
  1. package/LICENSE +7 -0
  2. package/package.json +72 -0
  3. package/src/agent/context.js +141 -0
  4. package/src/agent/loop/context-summary.js +196 -0
  5. package/src/agent/loop/directory-utils.js +102 -0
  6. package/src/agent/loop/local.js +196 -0
  7. package/src/agent/loop/loop-detection.js +288 -0
  8. package/src/agent/loop/stream-parser.js +515 -0
  9. package/src/agent/loop/tool-executor.js +470 -0
  10. package/src/agent/loop/verification.js +263 -0
  11. package/src/agent/loop/websocket.js +80 -0
  12. package/src/agent/prompt.js +259 -0
  13. package/src/agent/react-loop.js +697 -0
  14. package/src/agent/subagent.js +263 -0
  15. package/src/commands/config.js +53 -0
  16. package/src/commands/connect.js +190 -0
  17. package/src/commands/devices.js +121 -0
  18. package/src/commands/login.js +77 -0
  19. package/src/commands/logout.js +31 -0
  20. package/src/commands/mcp.js +258 -0
  21. package/src/commands/provider.js +633 -0
  22. package/src/commands/register.js +74 -0
  23. package/src/commands/run.js +150 -0
  24. package/src/commands/search.js +64 -0
  25. package/src/commands/session.js +57 -0
  26. package/src/commands/skills.js +54 -0
  27. package/src/commands/stop-subagent.js +58 -0
  28. package/src/index.js +208 -0
  29. package/src/llm/direct.js +317 -0
  30. package/src/memory/store.js +215 -0
  31. package/src/mock-readline.js +27 -0
  32. package/src/parser/dependencies.js +71 -0
  33. package/src/parser/markdown.js +505 -0
  34. package/src/parser/stream.js +96 -0
  35. package/src/prompts/modes/CODING.js +160 -0
  36. package/src/prompts/modes/GENERAL.js +105 -0
  37. package/src/prompts/modes/NETWORK.js +69 -0
  38. package/src/prompts/modes/SSH.js +53 -0
  39. package/src/prompts/systemPrompt.js +85 -0
  40. package/src/safety/check.js +210 -0
  41. package/src/services/crypto.js +78 -0
  42. package/src/services/executor.js +68 -0
  43. package/src/services/history.js +58 -0
  44. package/src/services/server-url.js +11 -0
  45. package/src/services/session.js +194 -0
  46. package/src/services/ssh.js +176 -0
  47. package/src/services/websocket.js +112 -0
  48. package/src/skills/loader.js +231 -0
  49. package/src/tools/browser.js +434 -0
  50. package/src/tools/local.js +1254 -0
  51. package/src/tools/mcp-client.js +209 -0
  52. package/src/tools/registry.js +132 -0
  53. package/src/tools/search-providers.js +237 -0
  54. package/src/tools/ssh.js +74 -0
  55. package/src/ui/App.js +2031 -0
  56. package/src/ui/animation.js +47 -0
  57. package/src/ui/components/AskUserDialog.js +33 -0
  58. package/src/ui/components/ConfirmationDialog.js +45 -0
  59. package/src/ui/components/DiffView.js +201 -0
  60. package/src/ui/components/Header.js +157 -0
  61. package/src/ui/components/HistoryPicker.js +130 -0
  62. package/src/ui/components/InputShell.js +22 -0
  63. package/src/ui/components/MessageHistory.js +1200 -0
  64. package/src/ui/components/ModalPanel.js +40 -0
  65. package/src/ui/components/ModePicker.js +161 -0
  66. package/src/ui/components/PlanDialog.js +48 -0
  67. package/src/ui/components/ProviderMenu.js +1095 -0
  68. package/src/ui/components/SavePicker.js +106 -0
  69. package/src/ui/components/SelectMenu.js +194 -0
  70. package/src/ui/components/SlashMenu.js +168 -0
  71. package/src/ui/components/SubagentPanel.js +138 -0
  72. package/src/ui/components/TextInputSafe.js +117 -0
  73. package/src/ui/components/TodoPanel.js +54 -0
  74. package/src/ui/components/ToolExecution.js +261 -0
  75. package/src/ui/components/TranscriptViewport.js +99 -0
  76. package/src/ui/diff.js +249 -0
  77. package/src/ui/h.js +7 -0
  78. package/src/ui/mouse-scroll.js +63 -0
  79. package/src/ui/slash-picker.js +58 -0
  80. package/src/ui/terminal.js +41 -0
  81. package/src/ui/theme.js +5 -0
  82. package/src/ui/welcome.js +12 -0
  83. package/src/utils/constants.js +231 -0
  84. package/src/utils/helpers.js +154 -0
  85. package/src/utils/logger.js +81 -0
  86. package/src/utils/sound.js +33 -0
@@ -0,0 +1,106 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { h } from '../h.js';
4
+ import { isOnlySgrMouseInput } from '../mouse-scroll.js';
5
+
6
+ const OPTIONS = [
7
+ { name: 'Local', desc: 'Save to local storage', value: 'local' },
8
+ { name: 'Cloud', desc: 'Save to cloud storage', value: 'cloud' },
9
+ { name: 'Discard', desc: 'Discard conversation and start fresh', value: 'discard' },
10
+ ];
11
+
12
+ export function SavePicker({ visible, onSelect, onCancel, hasCloud }) {
13
+ const [cursor, setCursor] = useState(0);
14
+ const [query, setQuery] = useState('');
15
+
16
+ const filtered = useMemo(() => {
17
+ let opts = OPTIONS.filter(o => o.value !== 'cloud' || hasCloud);
18
+ if (!query) return opts;
19
+ const q = query.toLowerCase();
20
+ return opts.filter(o => o.name.toLowerCase().includes(q) || o.desc.toLowerCase().includes(q));
21
+ }, [hasCloud, query]);
22
+
23
+ useEffect(() => {
24
+ if (cursor >= filtered.length) setCursor(Math.max(0, filtered.length - 1));
25
+ }, [filtered.length]);
26
+
27
+ useInput((input, key) => {
28
+ if (!visible) return;
29
+
30
+ if (key.escape) {
31
+ setQuery('');
32
+ setCursor(0);
33
+ onCancel();
34
+ return;
35
+ }
36
+
37
+ if (key.upArrow) {
38
+ setCursor(c => (c > 0 ? c - 1 : filtered.length - 1));
39
+ return;
40
+ }
41
+
42
+ if (key.downArrow) {
43
+ setCursor(c => (c < filtered.length - 1 ? c + 1 : 0));
44
+ return;
45
+ }
46
+
47
+ if (key.return) {
48
+ if (filtered.length > 0) {
49
+ onSelect(filtered[cursor].value);
50
+ }
51
+ setQuery('');
52
+ setCursor(0);
53
+ return;
54
+ }
55
+
56
+ if (key.backspace || key.delete) {
57
+ setQuery(q => q.slice(0, -1));
58
+ setCursor(0);
59
+ return;
60
+ }
61
+
62
+ if (isOnlySgrMouseInput(input)) return;
63
+ if (input && !key.ctrl && !key.meta) {
64
+ setQuery(q => q + input);
65
+ setCursor(0);
66
+ }
67
+ });
68
+
69
+ if (!visible) return null;
70
+
71
+ const separator = '─'.repeat(44);
72
+
73
+ return h(
74
+ Box,
75
+ { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
76
+
77
+ h(Text, { color: '#7aa2f7', bold: true }, ' Save session'),
78
+ h(Text, { color: '#3b3f52' }, separator),
79
+
80
+ // Items
81
+ ...filtered.map((o, i) => {
82
+ const isHL = i === cursor;
83
+ const prefix = isHL ? h(Text, { color: '#9ece6a' }, ' ▸ ') : h(Text, { color: '#3b3f52' }, ' ');
84
+ const icon = o.value === 'local' ? ' ' : o.value === 'cloud' ? '☁ ' : '✕ ';
85
+
86
+ return h(Box, { key: i },
87
+ prefix,
88
+ h(Text, isHL ? { color: '#ffffff', bold: true, backgroundColor: '#2a3a5c' } : { color: '#9aa5ce' },
89
+ ` ${icon}${o.name.padEnd(10)} ${isHL ? o.desc : ''}`
90
+ )
91
+ );
92
+ }),
93
+
94
+ !hasCloud ? h(Text, { color: '#565f89', dimColor: true }, ' Cloud unavailable - not connected to server') : null,
95
+
96
+ h(Text, { color: '#3b3f52' }, separator),
97
+
98
+ h(
99
+ Box,
100
+ { flexDirection: 'row' },
101
+ h(Text, { color: '#9ece6a' }, ' ↑↓'), h(Text, { color: '#565f89' }, ' Navigate'),
102
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Enter'), h(Text, { color: '#565f89' }, ' Select'),
103
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Cancel')
104
+ )
105
+ );
106
+ }
@@ -0,0 +1,194 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { Box, Text, useInput, useWindowSize } from 'ink';
3
+ import { h } from '../h.js';
4
+ import { InputShell } from './InputShell.js';
5
+ import { isOnlySgrMouseInput } from '../mouse-scroll.js';
6
+
7
+ /**
8
+ * Reusable interactive menu with:
9
+ * - Arrow-up/down navigation with visual highlight
10
+ * - Optional fuzzy-search / filter
11
+ * - Smart positioning (above input if space below is tight, else below)
12
+ * - Escape to cancel
13
+ *
14
+ * Props:
15
+ * items: [{ label, desc?, value, … }] — list of options
16
+ * title?: string — heading text
17
+ * searchable?: boolean — enable type-to-filter (default true)
18
+ * placeholder?: string — search placeholder
19
+ * onSelect: (item) => void
20
+ * onCancel: () => void
21
+ * visible: boolean
22
+ * position?: 'top' | 'bottom' | 'auto' — force position or auto-detect
23
+ */
24
+ export function SelectMenu({
25
+ items = [],
26
+ title = 'Menu',
27
+ searchable = true,
28
+ placeholder = 'Type to search…',
29
+ onSelect,
30
+ onCancel,
31
+ visible,
32
+ position = 'auto',
33
+ }) {
34
+ const [query, setQuery] = useState('');
35
+ const [cursor, setCursor] = useState(0);
36
+
37
+ // Filter items based on query
38
+ const filtered = useMemo(() => {
39
+ if (!searchable || !query) return items;
40
+ const q = query.toLowerCase();
41
+ return items.filter(
42
+ (it) =>
43
+ (it.label || '').toLowerCase().includes(q) ||
44
+ (it.desc || '').toLowerCase().includes(q) ||
45
+ (it.cmd || '').toLowerCase().includes(q) ||
46
+ (it.name || '').toLowerCase().includes(q)
47
+ );
48
+ }, [items, query, searchable]);
49
+
50
+ // Clamp cursor when filtered list changes
51
+ useMemo(() => {
52
+ if (cursor >= filtered.length) setCursor(Math.max(0, filtered.length - 1));
53
+ }, [filtered.length]);
54
+
55
+ useInput((input, key) => {
56
+ if (!visible) return;
57
+
58
+ // Escape → cancel
59
+ if (key.escape) {
60
+ setQuery('');
61
+ setCursor(0);
62
+ onCancel();
63
+ return;
64
+ }
65
+
66
+ // Arrow up
67
+ if (key.upArrow) {
68
+ setCursor((c) => (c > 0 ? c - 1 : filtered.length - 1));
69
+ return;
70
+ }
71
+
72
+ // Arrow down
73
+ if (key.downArrow) {
74
+ setCursor((c) => (c < filtered.length - 1 ? c + 1 : 0));
75
+ return;
76
+ }
77
+
78
+ // Enter → select current item
79
+ if (key.return) {
80
+ if (filtered.length > 0) {
81
+ const item = filtered[cursor];
82
+ setQuery('');
83
+ setCursor(0);
84
+ onSelect(item);
85
+ } else {
86
+ setQuery('');
87
+ setCursor(0);
88
+ onCancel();
89
+ }
90
+ return;
91
+ }
92
+
93
+ // Backspace → delete last char from query
94
+ if (key.backspace || key.delete) {
95
+ setQuery((q) => q.slice(0, -1));
96
+ setCursor(0);
97
+ return;
98
+ }
99
+
100
+ // Any printable character → append to search query
101
+ if (isOnlySgrMouseInput(input)) return;
102
+ if (input && !key.ctrl && !key.meta) {
103
+ setQuery((q) => q + input);
104
+ setCursor(0);
105
+ }
106
+ });
107
+
108
+ if (!visible) return null;
109
+
110
+ const { rows } = useWindowSize();
111
+ const maxVisibleItems = Math.min(filtered.length, Math.max(5, (rows || 24) - 8));
112
+
113
+ // Determine scroll window around cursor
114
+ let startIdx = 0;
115
+ if (filtered.length > maxVisibleItems) {
116
+ const half = Math.floor(maxVisibleItems / 2);
117
+ startIdx = Math.max(0, Math.min(cursor - half, filtered.length - maxVisibleItems));
118
+ }
119
+ const visibleItems = filtered.slice(startIdx, startIdx + maxVisibleItems);
120
+
121
+ const separator = '─'.repeat(50);
122
+
123
+ return h(
124
+ Box,
125
+ { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
126
+
127
+ // Title
128
+ h(Box, { paddingBottom: 0 },
129
+ h(Text, { color: '#7aa2f7', bold: true }, ` ${title}`)
130
+ ),
131
+ h(Text, { color: '#3b3f52' }, separator),
132
+
133
+ // Search bar (if searchable)
134
+ searchable
135
+ ? h(InputShell, { flexDirection: 'row', paddingY: 0, marginY: 0 },
136
+ h(Text, { color: '#e0af68' }, ' Search: '),
137
+ h(Text, { color: '#c0caf5' }, query || placeholder),
138
+ h(Text, { color: '#565f89' }, query ? '' : ' ')
139
+ )
140
+ : null,
141
+
142
+ // Items
143
+ ...visibleItems.map((item, i) => {
144
+ const realIdx = startIdx + i;
145
+ const isHighlighted = realIdx === cursor;
146
+ const icon = isHighlighted ? h(Text, { color: '#9ece6a' }, ' ▸ ') : h(Text, { color: '#3b3f52' }, ' ');
147
+ const label = item.label || item.cmd || item.name || '';
148
+ const desc = item.desc ? ` ${item.desc}` : '';
149
+ const mark = item.active ? ' ◀' : '';
150
+
151
+ return h(
152
+ Box,
153
+ { key: realIdx },
154
+ icon,
155
+ h(
156
+ Text,
157
+ isHighlighted
158
+ ? { color: '#ffffff', bold: true, backgroundColor: '#2a3a5c' }
159
+ : { color: '#9aa5ce' },
160
+ `${label.padEnd(16)}${isHighlighted ? desc : ''}${mark}`
161
+ )
162
+ );
163
+ }),
164
+
165
+ // Scroll indicator
166
+ filtered.length > maxVisibleItems
167
+ ? h(
168
+ Box,
169
+ { paddingTop: 0 },
170
+ h(
171
+ Text,
172
+ { color: '#565f89', dimColor: true },
173
+ ` Showing ${startIdx + 1}-${startIdx + visibleItems.length} of ${filtered.length}`
174
+ )
175
+ )
176
+ : null,
177
+
178
+ h(Text, { color: '#3b3f52' }, separator),
179
+
180
+ // Footer help
181
+ h(
182
+ Box,
183
+ { flexDirection: 'row' },
184
+ h(Text, { color: '#565f89' }, ' ↑↓ Navigate'),
185
+ h(Text, { color: '#3b3f52' }, ' · '),
186
+ h(Text, { color: '#565f89' }, 'Enter Select'),
187
+ h(Text, { color: '#3b3f52' }, ' · '),
188
+ h(Text, { color: '#565f89' }, 'Esc Cancel'),
189
+ searchable
190
+ ? [h(Text, { color: '#3b3f52' }, ' · '), h(Text, { color: '#565f89' }, 'Type to search')]
191
+ : []
192
+ )
193
+ );
194
+ }
@@ -0,0 +1,168 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { Box, Text, useInput, useWindowSize } from 'ink';
3
+ import { h } from '../h.js';
4
+ import { InputShell } from './InputShell.js';
5
+ import { isOnlySgrMouseInput } from '../mouse-scroll.js';
6
+
7
+ const COMMANDS = [
8
+ { cmd: '/clear', desc: 'Clear displayed history', action: 'clear' },
9
+ { cmd: '/help', desc: 'Show available commands', action: 'help' },
10
+ { cmd: '/new', desc: 'Start a new section (saves current)', action: 'new' },
11
+ { cmd: '/status', desc: 'Show OS, mode and session info', action: 'status' },
12
+ { cmd: '/stats', desc: 'Iterations, tokens and timing', action: 'stats' },
13
+ { cmd: '/history', desc: 'Saved sessions - resume a past conversation', action: 'history' },
14
+ { cmd: '/save', desc: 'Save current session to disk', action: 'save' },
15
+ { cmd: '/context', desc: 'Show conversation context size', action: 'context' },
16
+ { cmd: '/devices', desc: 'List configured devices', action: 'devices' },
17
+ { cmd: '/provider', desc: 'Manage AI providers and API keys', action: 'provider' },
18
+ { cmd: '/plan', desc: 'Switch to PLAN mode (read-only, no modifications)', action: 'plan' },
19
+ { cmd: '/exec', desc: 'Switch to EXEC mode (modifications allowed)', action: 'exec' },
20
+ { cmd: '/mode', desc: 'Switch GENERAL / CODING / NETWORK / SSH mode', action: 'mode' },
21
+ { cmd: '/todos', desc: 'Show current todo list', action: 'todos' },
22
+ { cmd: '/skills', desc: 'List available agent skills', action: 'skills' },
23
+ { cmd: '/exit', desc: 'Exit the agent', action: 'exit' },
24
+ { cmd: '/logout', desc: 'Log out and clear credentials', action: 'logout' },
25
+ ];
26
+
27
+ export { COMMANDS as SLASH_COMMANDS };
28
+
29
+ export function SlashMenu({ visible, onSelect, onCancel }) {
30
+ const [query, setQuery] = useState('');
31
+ const [cursor, setCursor] = useState(0);
32
+
33
+ // Filter commands based on search query
34
+ const filtered = useMemo(() => {
35
+ if (!query) return COMMANDS;
36
+ const q = query.toLowerCase();
37
+ return COMMANDS.filter(
38
+ (c) => c.cmd.toLowerCase().includes(q) || c.desc.toLowerCase().includes(q)
39
+ );
40
+ }, [query]);
41
+
42
+ // Clamp cursor
43
+ useEffect(() => {
44
+ if (cursor >= filtered.length) setCursor(Math.max(0, filtered.length - 1));
45
+ }, [filtered.length]);
46
+
47
+ useInput((input, key) => {
48
+ if (!visible) return;
49
+
50
+ if (key.escape) {
51
+ setQuery('');
52
+ setCursor(0);
53
+ onCancel();
54
+ return;
55
+ }
56
+
57
+ if (key.upArrow) {
58
+ setCursor((c) => (c > 0 ? c - 1 : filtered.length - 1));
59
+ return;
60
+ }
61
+
62
+ if (key.downArrow) {
63
+ setCursor((c) => (c < filtered.length - 1 ? c + 1 : 0));
64
+ return;
65
+ }
66
+
67
+ if (key.return) {
68
+ if (filtered.length > 0) {
69
+ onSelect(filtered[cursor]);
70
+ }
71
+ setQuery('');
72
+ setCursor(0);
73
+ return;
74
+ }
75
+
76
+ if (key.backspace || key.delete) {
77
+ setQuery((q) => q.slice(0, -1));
78
+ setCursor(0);
79
+ return;
80
+ }
81
+
82
+ if (isOnlySgrMouseInput(input)) return;
83
+ if (input && !key.ctrl && !key.meta) {
84
+ setQuery((q) => q + input);
85
+ setCursor(0);
86
+ }
87
+ });
88
+
89
+ if (!visible) return null;
90
+
91
+ const { rows } = useWindowSize();
92
+ const maxVisible = Math.min(filtered.length, Math.max(6, (rows || 24) - 10));
93
+ let startIdx = 0;
94
+ if (filtered.length > maxVisible) {
95
+ const half = Math.floor(maxVisible / 2);
96
+ startIdx = Math.max(0, Math.min(cursor - half, filtered.length - maxVisible));
97
+ }
98
+ const visibleItems = filtered.slice(startIdx, startIdx + maxVisible);
99
+
100
+ const separator = '─'.repeat(52);
101
+
102
+ return h(
103
+ Box,
104
+ { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
105
+
106
+ // Header
107
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
108
+ h(Text, { color: '#7aa2f7', bold: true }, ' Commands'),
109
+ h(Text, { color: '#3b3f52' }, ' '),
110
+ h(Text, { color: '#565f89', dimColor: true }, `${filtered.length}/${COMMANDS.length}`)
111
+ ),
112
+ h(Text, { color: '#3b3f52' }, separator),
113
+
114
+ // Search bar
115
+ h(InputShell, { flexDirection: 'row', paddingY: 0, marginY: 0 },
116
+ h(Text, { color: '#e0af68' }, ' Search: '),
117
+ h(Text, { color: '#c0caf5', bold: true }, query || ' '),
118
+ h(Text, { color: '#565f89' }, query ? '' : ' type to filter commands')
119
+ ),
120
+ h(Text, { color: '#3b3f52' }, separator),
121
+
122
+ // Items
123
+ ...visibleItems.map((cmd, i) => {
124
+ const realIdx = startIdx + i;
125
+ const isHighlighted = realIdx === cursor;
126
+ const prefix = isHighlighted ? h(Text, { color: '#9ece6a' }, ' ▸ ') : h(Text, { color: '#3b3f52' }, ' ');
127
+
128
+ return h(
129
+ Box,
130
+ { key: realIdx },
131
+ prefix,
132
+ h(
133
+ Text,
134
+ isHighlighted
135
+ ? { color: '#ffffff', bold: true, backgroundColor: '#2a3a5c' }
136
+ : { color: '#9aa5ce' },
137
+ isHighlighted
138
+ ? `${cmd.cmd.padEnd(14)} ${cmd.desc}`
139
+ : `${cmd.cmd.padEnd(14)}`
140
+ )
141
+ );
142
+ }),
143
+
144
+ // Scroll indicator
145
+ filtered.length > maxVisible
146
+ ? h(Text, { color: '#565f89', dimColor: true }, ` Showing ${startIdx + 1}-${startIdx + visibleItems.length} of ${filtered.length}`)
147
+ : null,
148
+
149
+ h(Text, { color: '#3b3f52' }, separator),
150
+
151
+ // Footer
152
+ h(
153
+ Box,
154
+ { flexDirection: 'row' },
155
+ h(Text, { color: '#9ece6a' }, ' ↑↓'),
156
+ h(Text, { color: '#565f89' }, ' Navigate'),
157
+ h(Text, { color: '#3b3f52' }, ' '),
158
+ h(Text, { color: '#9ece6a' }, ' Enter'),
159
+ h(Text, { color: '#565f89' }, ' Select'),
160
+ h(Text, { color: '#3b3f52' }, ' '),
161
+ h(Text, { color: '#9ece6a' }, ' Esc'),
162
+ h(Text, { color: '#565f89' }, ' Cancel'),
163
+ h(Text, { color: '#3b3f52' }, ' '),
164
+ h(Text, { color: '#9ece6a' }, ' Type'),
165
+ h(Text, { color: '#565f89' }, ' Search')
166
+ )
167
+ );
168
+ }
@@ -0,0 +1,138 @@
1
+ import { Box, Text } from 'ink';
2
+ import { h } from '../h.js';
3
+ import { ENABLE_UI_ANIMATIONS, useAnimationFrame } from '../animation.js';
4
+
5
+ const SPINNER = ['◐', '◓', '◑', '◒'];
6
+
7
+ function formatDuration(ms) {
8
+ if (!ms || ms < 0) return '0s';
9
+ const sec = Math.round(ms / 1000);
10
+ if (sec < 60) return `${sec}s`;
11
+ return `${Math.floor(sec / 60)}m ${sec % 60}s`;
12
+ }
13
+
14
+ function truncate(value, max = 50) {
15
+ const s = String(value || '');
16
+ if (s.length <= max) return s;
17
+ return s.slice(0, max - 1) + '…';
18
+ }
19
+
20
+ function statusLabel(state) {
21
+ if (!state) return 'Idle';
22
+ if (state.status === 'running') return 'Exploring';
23
+ if (state.status === 'failed') return 'Failed';
24
+ return 'Completed';
25
+ }
26
+
27
+ function compactFindings(state) {
28
+ return state?.findingsPreview || state?.findings || '';
29
+ }
30
+
31
+ function formatTokens(tokens) {
32
+ const n = Number(tokens || 0);
33
+ if (!n) return null;
34
+ if (n >= 1000) return `~${(n / 1000).toFixed(1)}k tok`;
35
+ return `~${n} tok`;
36
+ }
37
+
38
+ const TOOL_COLORS = {
39
+ READ_FILE: '#9ece6a', WRITE_FILE: '#e0af68', EDIT_FILE: '#e0af68',
40
+ APPEND_FILE: '#e0af68', DELETE_FILE: '#f7768e', FETCH_URL: '#7dcfff',
41
+ WEB_SEARCH: '#bb9af7', LOCAL_CMD: '#f7768e', GREP: '#73daca',
42
+ GLOB: '#73daca', SEARCH_FILE: '#73daca', LIST_DIR: '#9ece6a',
43
+ TREE_VIEW: '#9ece6a', FILE_INFO: '#9ece6a', GIT: '#f7768e',
44
+ BROWSE: '#7dcfff', BROWSE_SEARCH: '#bb9af7', BROWSE_EXTRACT: '#7dcfff',
45
+ };
46
+ const DEFAULT_COLOR = '#e0af68';
47
+ const getToolColor = (name) => TOOL_COLORS[name] || DEFAULT_COLOR;
48
+
49
+ function formatTarget(ev) {
50
+ const tc = ev?.toolCall;
51
+ if (!tc) return '';
52
+ return tc.path || tc.cmd || tc.url || tc.query || tc.pattern || '';
53
+ }
54
+
55
+ export function SubagentPanel({ state, events, isExpanded }) {
56
+ const frame = useAnimationFrame(state?.status === 'running');
57
+
58
+ if (!state) return null;
59
+
60
+ const isRunning = state.status === 'running';
61
+ const isFailed = state.status === 'failed';
62
+ const spinner = ENABLE_UI_ANIMATIONS && isRunning ? SPINNER[frame % SPINNER.length] : '◎';
63
+ const icon = isRunning ? spinner : isFailed ? '✗' : '✓';
64
+ const iconColor = isRunning ? '#7dcfff' : isFailed ? '#f7768e' : '#9ece6a';
65
+ const elapsed = state.elapsed || (state.startedAt ? Date.now() - state.startedAt : 0);
66
+ const desc = truncate(state.description || 'Exploration task', 50);
67
+ const visibleEvents = (events || []).slice(-8);
68
+ const hiddenEvents = Math.max(0, (events || []).length - visibleEvents.length);
69
+ const findings = compactFindings(state);
70
+ const tokenLabel = formatTokens(state.estimatedTokens);
71
+
72
+ return h(Box, { flexDirection: 'column' },
73
+ h(Box, { flexDirection: 'row' },
74
+ h(Text, { color: iconColor }, `${icon} `),
75
+ h(Text, { color: '#c0caf5', bold: true }, `Subagent: ${desc}`),
76
+ h(Text, { color: '#565f89' }, ` · ${statusLabel(state)} · ${state.toolsUsed || 0} tools · ${formatDuration(elapsed)}`),
77
+ tokenLabel
78
+ ? h(Text, { color: '#e0af68' }, ` · ${tokenLabel}`)
79
+ : null,
80
+ state.iterations != null
81
+ ? h(Text, { color: '#565f89' }, ` · ${state.iterations} iter`)
82
+ : null,
83
+ state.completionSignal
84
+ ? h(Text, { color: '#565f89' }, ` · ${state.completionSignal}`)
85
+ : null,
86
+ isFailed && state.error
87
+ ? h(Text, { color: '#f7768e' }, ` · Error: ${truncate(state.error, 40)}`)
88
+ : null,
89
+ ),
90
+ isRunning && state.lastTool
91
+ ? h(Box, { paddingLeft: 2 },
92
+ h(Text, { color: '#565f89' }, '⎿ '),
93
+ h(Text, { color: getToolColor(state.lastTool), bold: true }, state.lastTool),
94
+ state.lastTarget ? h(Text, { color: '#565f89' }, `: ${truncate(state.lastTarget, 80)}`) : null,
95
+ )
96
+ : null,
97
+ isExpanded
98
+ ? h(Box, { flexDirection: 'column', paddingLeft: 2 },
99
+ hiddenEvents > 0
100
+ ? h(Text, { color: '#565f89', dimColor: true }, ` … ${hiddenEvents} earlier subagent events hidden`)
101
+ : null,
102
+ visibleEvents.length > 0
103
+ ? visibleEvents.map((ev, i) => {
104
+ if (ev.type === 'tool_start') {
105
+ return h(Box, { key: i },
106
+ h(Text, { color: '#7aa2f7' }, ' ├─ '),
107
+ h(Text, { color: getToolColor(ev.name), bold: true }, ev.name),
108
+ formatTarget(ev) ? h(Text, { color: '#565f89' }, `: ${truncate(formatTarget(ev), 80)}`) : null,
109
+ h(Text, { color: '#565f89' }, ' ◐'),
110
+ );
111
+ }
112
+ if (ev.type === 'tool_end') {
113
+ return h(Box, { key: i },
114
+ h(Text, { color: ev.success ? '#9ece6a' : '#f7768e' }, ' ├─ '),
115
+ h(Text, { color: getToolColor(ev.name), bold: true }, ev.name),
116
+ formatTarget(ev) ? h(Text, { color: '#565f89' }, `: ${truncate(formatTarget(ev), 80)}`) : null,
117
+ h(Text, { color: '#565f89' }, ev.success ? ' ✓' : ' ✗'),
118
+ );
119
+ }
120
+ return null;
121
+ })
122
+ : null,
123
+ state.findings
124
+ ? h(Box, { key: 'findings', marginTop: 1, flexDirection: 'column' },
125
+ h(Text, { color: '#565f89', bold: true }, ' Findings:'),
126
+ h(Text, { color: '#9aa5ce' }, ` ${truncate(findings, 300)}`),
127
+ )
128
+ : null,
129
+ !state.findings && state.findingsPreview
130
+ ? h(Box, { key: 'findings', marginTop: 1, flexDirection: 'column' },
131
+ h(Text, { color: '#565f89', bold: true }, ' Findings:'),
132
+ h(Text, { color: '#9aa5ce' }, ` ${truncate(state.findingsPreview, 300)}`),
133
+ )
134
+ : null,
135
+ )
136
+ : null,
137
+ );
138
+ }