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.
- package/LICENSE +7 -0
- package/package.json +72 -0
- package/src/agent/context.js +141 -0
- package/src/agent/loop/context-summary.js +196 -0
- package/src/agent/loop/directory-utils.js +102 -0
- package/src/agent/loop/local.js +196 -0
- package/src/agent/loop/loop-detection.js +288 -0
- package/src/agent/loop/stream-parser.js +515 -0
- package/src/agent/loop/tool-executor.js +470 -0
- package/src/agent/loop/verification.js +263 -0
- package/src/agent/loop/websocket.js +80 -0
- package/src/agent/prompt.js +259 -0
- package/src/agent/react-loop.js +697 -0
- package/src/agent/subagent.js +263 -0
- package/src/commands/config.js +53 -0
- package/src/commands/connect.js +190 -0
- package/src/commands/devices.js +121 -0
- package/src/commands/login.js +77 -0
- package/src/commands/logout.js +31 -0
- package/src/commands/mcp.js +258 -0
- package/src/commands/provider.js +633 -0
- package/src/commands/register.js +74 -0
- package/src/commands/run.js +150 -0
- package/src/commands/search.js +64 -0
- package/src/commands/session.js +57 -0
- package/src/commands/skills.js +54 -0
- package/src/commands/stop-subagent.js +58 -0
- package/src/index.js +208 -0
- package/src/llm/direct.js +317 -0
- package/src/memory/store.js +215 -0
- package/src/mock-readline.js +27 -0
- package/src/parser/dependencies.js +71 -0
- package/src/parser/markdown.js +505 -0
- package/src/parser/stream.js +96 -0
- package/src/prompts/modes/CODING.js +160 -0
- package/src/prompts/modes/GENERAL.js +105 -0
- package/src/prompts/modes/NETWORK.js +69 -0
- package/src/prompts/modes/SSH.js +53 -0
- package/src/prompts/systemPrompt.js +85 -0
- package/src/safety/check.js +210 -0
- package/src/services/crypto.js +78 -0
- package/src/services/executor.js +68 -0
- package/src/services/history.js +58 -0
- package/src/services/server-url.js +11 -0
- package/src/services/session.js +194 -0
- package/src/services/ssh.js +176 -0
- package/src/services/websocket.js +112 -0
- package/src/skills/loader.js +231 -0
- package/src/tools/browser.js +434 -0
- package/src/tools/local.js +1254 -0
- package/src/tools/mcp-client.js +209 -0
- package/src/tools/registry.js +132 -0
- package/src/tools/search-providers.js +237 -0
- package/src/tools/ssh.js +74 -0
- package/src/ui/App.js +2031 -0
- package/src/ui/animation.js +47 -0
- package/src/ui/components/AskUserDialog.js +33 -0
- package/src/ui/components/ConfirmationDialog.js +45 -0
- package/src/ui/components/DiffView.js +201 -0
- package/src/ui/components/Header.js +157 -0
- package/src/ui/components/HistoryPicker.js +130 -0
- package/src/ui/components/InputShell.js +22 -0
- package/src/ui/components/MessageHistory.js +1200 -0
- package/src/ui/components/ModalPanel.js +40 -0
- package/src/ui/components/ModePicker.js +161 -0
- package/src/ui/components/PlanDialog.js +48 -0
- package/src/ui/components/ProviderMenu.js +1095 -0
- package/src/ui/components/SavePicker.js +106 -0
- package/src/ui/components/SelectMenu.js +194 -0
- package/src/ui/components/SlashMenu.js +168 -0
- package/src/ui/components/SubagentPanel.js +138 -0
- package/src/ui/components/TextInputSafe.js +117 -0
- package/src/ui/components/TodoPanel.js +54 -0
- package/src/ui/components/ToolExecution.js +261 -0
- package/src/ui/components/TranscriptViewport.js +99 -0
- package/src/ui/diff.js +249 -0
- package/src/ui/h.js +7 -0
- package/src/ui/mouse-scroll.js +63 -0
- package/src/ui/slash-picker.js +58 -0
- package/src/ui/terminal.js +41 -0
- package/src/ui/theme.js +5 -0
- package/src/ui/welcome.js +12 -0
- package/src/utils/constants.js +231 -0
- package/src/utils/helpers.js +154 -0
- package/src/utils/logger.js +81 -0
- 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
|
+
}
|