@moxxy/cli 0.0.12 → 0.1.1
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 +278 -112
- package/bin/moxxy +10 -0
- package/package.json +36 -53
- package/src/api-client.js +286 -0
- package/src/cli.js +349 -0
- package/src/commands/agent.js +413 -0
- package/src/commands/auth.js +326 -0
- package/src/commands/channel.js +285 -0
- package/src/commands/doctor.js +261 -0
- package/src/commands/events.js +80 -0
- package/src/commands/gateway.js +428 -0
- package/src/commands/heartbeat.js +145 -0
- package/src/commands/init.js +954 -0
- package/src/commands/mcp.js +278 -0
- package/src/commands/plugin.js +583 -0
- package/src/commands/provider.js +1934 -0
- package/src/commands/settings.js +224 -0
- package/src/commands/skill.js +125 -0
- package/src/commands/template.js +237 -0
- package/src/commands/uninstall.js +196 -0
- package/src/commands/update.js +406 -0
- package/src/commands/vault.js +219 -0
- package/src/help.js +392 -0
- package/src/lib/plugin-registry.js +98 -0
- package/src/platform.js +40 -0
- package/src/sse-client.js +79 -0
- package/src/tui/action-wizards.js +130 -0
- package/src/tui/app.jsx +859 -0
- package/src/tui/components/action-picker.jsx +86 -0
- package/src/tui/components/chat-panel.jsx +120 -0
- package/src/tui/components/footer.jsx +13 -0
- package/src/tui/components/header.jsx +45 -0
- package/src/tui/components/input-area.jsx +384 -0
- package/src/tui/components/messages/ask-message.jsx +13 -0
- package/src/tui/components/messages/assistant-message.jsx +165 -0
- package/src/tui/components/messages/channel-message.jsx +18 -0
- package/src/tui/components/messages/event-message.jsx +22 -0
- package/src/tui/components/messages/hive-status.jsx +34 -0
- package/src/tui/components/messages/skill-message.jsx +31 -0
- package/src/tui/components/messages/system-message.jsx +12 -0
- package/src/tui/components/messages/thinking.jsx +25 -0
- package/src/tui/components/messages/tool-group.jsx +62 -0
- package/src/tui/components/messages/tool-message.jsx +66 -0
- package/src/tui/components/messages/user-message.jsx +12 -0
- package/src/tui/components/model-picker.jsx +138 -0
- package/src/tui/components/multiline-input.jsx +72 -0
- package/src/tui/events-handler.js +730 -0
- package/src/tui/helpers.js +59 -0
- package/src/tui/hooks/use-command-handler.js +451 -0
- package/src/tui/index.jsx +55 -0
- package/src/tui/input-utils.js +26 -0
- package/src/tui/markdown-renderer.js +66 -0
- package/src/tui/mcp-wizard.js +136 -0
- package/src/tui/model-picker.js +174 -0
- package/src/tui/slash-commands.js +26 -0
- package/src/tui/store.js +12 -0
- package/src/tui/theme.js +17 -0
- package/src/ui.js +109 -0
- package/bin/moxxy.js +0 -2
- package/dist/chunk-23LZYKQ6.mjs +0 -1131
- package/dist/chunk-2FZEA3NG.mjs +0 -457
- package/dist/chunk-3KDPLS22.mjs +0 -1131
- package/dist/chunk-3QRJTRBT.mjs +0 -1102
- package/dist/chunk-6DZX6EAA.mjs +0 -37
- package/dist/chunk-A4WRDUNY.mjs +0 -1242
- package/dist/chunk-C46NSEKG.mjs +0 -211
- package/dist/chunk-CAUXONEF.mjs +0 -1131
- package/dist/chunk-CPL5V56X.mjs +0 -1131
- package/dist/chunk-CTBVTTBG.mjs +0 -440
- package/dist/chunk-FHHLXTEZ.mjs +0 -1121
- package/dist/chunk-FXY3GPVA.mjs +0 -1126
- package/dist/chunk-GSNMMI3H.mjs +0 -530
- package/dist/chunk-HHOAOGUS.mjs +0 -1242
- package/dist/chunk-ITBO7BKI.mjs +0 -1243
- package/dist/chunk-J33O35WX.mjs +0 -532
- package/dist/chunk-N5JTPB6U.mjs +0 -820
- package/dist/chunk-NGVL4Q5C.mjs +0 -1102
- package/dist/chunk-Q2OCMNYI.mjs +0 -1131
- package/dist/chunk-QDVRLN6D.mjs +0 -1121
- package/dist/chunk-QO2JONHP.mjs +0 -1131
- package/dist/chunk-RVAPILHA.mjs +0 -1242
- package/dist/chunk-S7YBOV7E.mjs +0 -1131
- package/dist/chunk-SHIG6Y5L.mjs +0 -1074
- package/dist/chunk-SOFST2PV.mjs +0 -1242
- package/dist/chunk-SUNUYS6G.mjs +0 -1243
- package/dist/chunk-TMZWETMH.mjs +0 -1242
- package/dist/chunk-TYD7NMMI.mjs +0 -581
- package/dist/chunk-TYQ3YS42.mjs +0 -1068
- package/dist/chunk-UALWCJ7F.mjs +0 -1131
- package/dist/chunk-UQZKODNW.mjs +0 -1124
- package/dist/chunk-USC6R2ON.mjs +0 -1242
- package/dist/chunk-W32EQCVC.mjs +0 -823
- package/dist/chunk-WMB5ENMC.mjs +0 -1242
- package/dist/chunk-WNHA5JAP.mjs +0 -1242
- package/dist/cli-2AIWTL6F.mjs +0 -8
- package/dist/cli-2QKJ5UUL.mjs +0 -8
- package/dist/cli-4RIS6DQX.mjs +0 -8
- package/dist/cli-5RH4VBBL.mjs +0 -7
- package/dist/cli-7MK4YGOP.mjs +0 -7
- package/dist/cli-B4KH6MZI.mjs +0 -8
- package/dist/cli-CGO2LZ6Z.mjs +0 -8
- package/dist/cli-CVP26EL2.mjs +0 -8
- package/dist/cli-DDRVVNAV.mjs +0 -8
- package/dist/cli-E7U56QVQ.mjs +0 -8
- package/dist/cli-EQNRMLL3.mjs +0 -8
- package/dist/cli-F5RUHHH4.mjs +0 -8
- package/dist/cli-LX6FFSEF.mjs +0 -8
- package/dist/cli-LY74GWKR.mjs +0 -6
- package/dist/cli-MAT3ZJHI.mjs +0 -8
- package/dist/cli-NJXXTQYF.mjs +0 -8
- package/dist/cli-O4ZGFAZG.mjs +0 -8
- package/dist/cli-ORVLI3UQ.mjs +0 -8
- package/dist/cli-PV43ZVKA.mjs +0 -8
- package/dist/cli-REVD6ISM.mjs +0 -8
- package/dist/cli-TBX76KQX.mjs +0 -8
- package/dist/cli-THCGF7SQ.mjs +0 -8
- package/dist/cli-TLX5ENVM.mjs +0 -8
- package/dist/cli-TMNI5ZYE.mjs +0 -8
- package/dist/cli-TNJHCBQA.mjs +0 -6
- package/dist/cli-TUX22CZP.mjs +0 -8
- package/dist/cli-XJVH7EEP.mjs +0 -8
- package/dist/cli-XXOW4VXJ.mjs +0 -8
- package/dist/cli-XZ5RESNB.mjs +0 -6
- package/dist/cli-YCBYZ76Q.mjs +0 -8
- package/dist/cli-ZLMQCU7X.mjs +0 -8
- package/dist/dist-2VGKJRBH.mjs +0 -6820
- package/dist/dist-37BNX4QG.mjs +0 -7081
- package/dist/dist-7LTHRYKA.mjs +0 -11569
- package/dist/dist-7XJPQW5C.mjs +0 -6950
- package/dist/dist-AYMVOW7T.mjs +0 -7123
- package/dist/dist-BHUWCDRS.mjs +0 -7132
- package/dist/dist-FAXRJMEN.mjs +0 -6812
- package/dist/dist-HQGANM3P.mjs +0 -6976
- package/dist/dist-KATLOZQV.mjs +0 -7054
- package/dist/dist-KLSB6YHV.mjs +0 -6964
- package/dist/dist-LKIOZQ42.mjs +0 -17
- package/dist/dist-UYA4RJUH.mjs +0 -2792
- package/dist/dist-ZYHCBILM.mjs +0 -6993
- package/dist/index.d.mts +0 -23
- package/dist/index.d.ts +0 -23
- package/dist/index.js +0 -25531
- package/dist/index.mjs +0 -18
- package/dist/src-APP5P3UD.mjs +0 -1386
- package/dist/src-D5HMDDVE.mjs +0 -1324
- package/dist/src-EK3WD4AU.mjs +0 -1327
- package/dist/src-LSZFLMFN.mjs +0 -1400
- package/dist/src-T77DFTFP.mjs +0 -1407
- package/dist/src-WIOCZRAC.mjs +0 -1397
- package/dist/src-YK6CHCMW.mjs +0 -1400
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
import { THEME } from '../../theme.js';
|
|
5
|
+
|
|
6
|
+
const C = {
|
|
7
|
+
text: '#CCCCCC',
|
|
8
|
+
bold: '#FFFFFF',
|
|
9
|
+
italic: '#B0B0B0',
|
|
10
|
+
code: '#E5C07B',
|
|
11
|
+
link: '#61AFEF',
|
|
12
|
+
linkUrl: '#666666',
|
|
13
|
+
heading12: '#61AFEF',
|
|
14
|
+
heading3plus: '#98C379',
|
|
15
|
+
quote: '#777777',
|
|
16
|
+
bullet: '#61AFEF',
|
|
17
|
+
dim: '#555555',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function renderInline(tokens) {
|
|
21
|
+
if (!tokens) return null;
|
|
22
|
+
return tokens.map((tok, i) => {
|
|
23
|
+
switch (tok.type) {
|
|
24
|
+
case 'text':
|
|
25
|
+
if (tok.tokens && tok.tokens.length > 0) {
|
|
26
|
+
return <Text key={i}>{renderInline(tok.tokens)}</Text>;
|
|
27
|
+
}
|
|
28
|
+
return tok.text;
|
|
29
|
+
case 'strong':
|
|
30
|
+
return <Text key={i} bold color={C.bold}>{renderInline(tok.tokens)}</Text>;
|
|
31
|
+
case 'em':
|
|
32
|
+
return <Text key={i} italic color={C.italic}>{renderInline(tok.tokens)}</Text>;
|
|
33
|
+
case 'codespan':
|
|
34
|
+
return <Text key={i} color={C.code}>{tok.text}</Text>;
|
|
35
|
+
case 'del':
|
|
36
|
+
return <Text key={i} color={C.dim} dimColor strikethrough>{renderInline(tok.tokens)}</Text>;
|
|
37
|
+
case 'link':
|
|
38
|
+
return <Text key={i}><Text underline color={C.link}>{renderInline(tok.tokens)}</Text><Text color={C.linkUrl} dimColor> ({tok.href})</Text></Text>;
|
|
39
|
+
case 'image':
|
|
40
|
+
return <Text key={i} color={C.link}>{tok.text || 'image'}</Text>;
|
|
41
|
+
case 'br':
|
|
42
|
+
return '\n';
|
|
43
|
+
case 'escape':
|
|
44
|
+
return tok.text;
|
|
45
|
+
default:
|
|
46
|
+
if (tok.tokens) return <Text key={i}>{renderInline(tok.tokens)}</Text>;
|
|
47
|
+
return tok.raw || tok.text || '';
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderListItem(item, bullet) {
|
|
53
|
+
const children = item.tokens || [];
|
|
54
|
+
if (children.length === 0) {
|
|
55
|
+
return <Text><Text color={C.bullet}>{bullet}</Text></Text>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return children.map((child, k) => {
|
|
59
|
+
if (k === 0) {
|
|
60
|
+
const inlineTokens = child.tokens || [];
|
|
61
|
+
return (
|
|
62
|
+
<Text key={k}>
|
|
63
|
+
<Text color={C.bullet}>{bullet}</Text>
|
|
64
|
+
{renderInline(inlineTokens)}
|
|
65
|
+
</Text>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (child.type === 'list') {
|
|
69
|
+
return renderBlock(child, k, false);
|
|
70
|
+
}
|
|
71
|
+
const inlineTokens = child.tokens || [];
|
|
72
|
+
return (
|
|
73
|
+
<Text key={k}>{' '}{renderInline(inlineTokens)}</Text>
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderBlock(token, key, isFirst) {
|
|
79
|
+
const gap = isFirst ? 0 : 1;
|
|
80
|
+
|
|
81
|
+
switch (token.type) {
|
|
82
|
+
case 'heading': {
|
|
83
|
+
const hColor = token.depth <= 2 ? C.heading12 : C.heading3plus;
|
|
84
|
+
return (
|
|
85
|
+
<Box key={key} marginTop={gap}>
|
|
86
|
+
<Text bold color={hColor}>
|
|
87
|
+
{renderInline(token.tokens)}
|
|
88
|
+
</Text>
|
|
89
|
+
</Box>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
case 'paragraph':
|
|
93
|
+
return (
|
|
94
|
+
<Box key={key} marginTop={gap}>
|
|
95
|
+
<Text>{renderInline(token.tokens)}</Text>
|
|
96
|
+
</Box>
|
|
97
|
+
);
|
|
98
|
+
case 'list':
|
|
99
|
+
return (
|
|
100
|
+
<Box key={key} marginTop={gap} flexDirection="column">
|
|
101
|
+
{token.items.map((item, j) => {
|
|
102
|
+
const bullet = token.ordered ? `${(token.start || 1) + j}. ` : '- ';
|
|
103
|
+
return (
|
|
104
|
+
<Box key={j} flexDirection="column">
|
|
105
|
+
{renderListItem(item, bullet)}
|
|
106
|
+
</Box>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</Box>
|
|
110
|
+
);
|
|
111
|
+
case 'code':
|
|
112
|
+
return (
|
|
113
|
+
<Box key={key} marginTop={1} marginBottom={1} borderStyle="round" borderColor={C.dim} paddingX={1}>
|
|
114
|
+
<Text color={C.code}>{token.text}</Text>
|
|
115
|
+
</Box>
|
|
116
|
+
);
|
|
117
|
+
case 'blockquote': {
|
|
118
|
+
const innerTokens = token.tokens || [];
|
|
119
|
+
return (
|
|
120
|
+
<Box key={key} marginTop={gap} flexDirection="row">
|
|
121
|
+
<Text color={C.quote}>│ </Text>
|
|
122
|
+
<Box flexDirection="column">
|
|
123
|
+
{innerTokens.map((t, j) => renderBlock(t, j, j === 0))}
|
|
124
|
+
</Box>
|
|
125
|
+
</Box>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
case 'hr':
|
|
129
|
+
return <Text key={key} color={C.dim}>{'─'.repeat(40)}</Text>;
|
|
130
|
+
case 'space':
|
|
131
|
+
return null;
|
|
132
|
+
default:
|
|
133
|
+
if (token.raw) return <Text key={key}>{token.raw.trimEnd()}</Text>;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function MarkdownBody({ content }) {
|
|
139
|
+
if (!content) return null;
|
|
140
|
+
|
|
141
|
+
let tokens;
|
|
142
|
+
try {
|
|
143
|
+
tokens = marked.lexer(content, { gfm: true });
|
|
144
|
+
} catch {
|
|
145
|
+
return <Text>{content}</Text>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const blocks = tokens.map((token, i) => renderBlock(token, i, i === 0)).filter(Boolean);
|
|
149
|
+
if (blocks.length === 0) return <Text>{content}</Text>;
|
|
150
|
+
return <Box flexDirection="column">{blocks}</Box>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function AssistantMessage({ msg, agentName }) {
|
|
154
|
+
const name = agentName || 'Moxxy';
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Box flexDirection="column" marginTop={1}>
|
|
158
|
+
<Text>
|
|
159
|
+
<Text bold color={THEME.assistant}>{name}</Text>
|
|
160
|
+
{msg.streaming && <Text color={THEME.dim}> typing…</Text>}
|
|
161
|
+
</Text>
|
|
162
|
+
<MarkdownBody content={msg.content || ''} />
|
|
163
|
+
</Box>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../../theme.js';
|
|
4
|
+
|
|
5
|
+
export function ChannelMessage({ msg }) {
|
|
6
|
+
const channel = msg.channel ? msg.channel.charAt(0).toUpperCase() + msg.channel.slice(1) : 'Channel';
|
|
7
|
+
const sender = msg.sender || 'User';
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Box flexDirection="column" marginTop={1}>
|
|
11
|
+
<Text>
|
|
12
|
+
<Text bold color={THEME.user}>{sender}</Text>
|
|
13
|
+
<Text color={THEME.dim}> via {channel}</Text>
|
|
14
|
+
</Text>
|
|
15
|
+
<Text wrap="wrap">{msg.content || ''}</Text>
|
|
16
|
+
</Box>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../../theme.js';
|
|
4
|
+
|
|
5
|
+
export function EventMessage({ msg }) {
|
|
6
|
+
const isError = msg.eventType?.includes('failed') || msg.eventType?.includes('violation') || msg.eventType?.includes('denied');
|
|
7
|
+
const color = isError ? THEME.error : THEME.dim;
|
|
8
|
+
|
|
9
|
+
if (msg.type === 'hive-event') {
|
|
10
|
+
return (
|
|
11
|
+
<Box marginTop={1}>
|
|
12
|
+
<Text color={THEME.dim}>{msg.content || ''}</Text>
|
|
13
|
+
</Box>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box marginTop={1}>
|
|
19
|
+
<Text color={color}>[{msg.eventType}] {JSON.stringify(msg.payload || {}).slice(0, 100)}</Text>
|
|
20
|
+
</Box>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../../theme.js';
|
|
4
|
+
|
|
5
|
+
export function HiveStatus({ msg }) {
|
|
6
|
+
const total = msg.totalTasks || 0;
|
|
7
|
+
const completed = msg.completedTasks || 0;
|
|
8
|
+
const inProgress = msg.inProgressTasks || 0;
|
|
9
|
+
const workers = msg.workers || 0;
|
|
10
|
+
const pct = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
11
|
+
|
|
12
|
+
const barWidth = 20;
|
|
13
|
+
const filled = total > 0 ? Math.round((completed / total) * barWidth) : 0;
|
|
14
|
+
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
15
|
+
|
|
16
|
+
const statusParts = [];
|
|
17
|
+
if (workers > 0) statusParts.push(`${workers} workers`);
|
|
18
|
+
if (inProgress > 0) statusParts.push(`${inProgress} active`);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Box flexDirection="column" marginTop={1}>
|
|
22
|
+
<Text>
|
|
23
|
+
<Text bold color={THEME.warning}>Hive</Text>
|
|
24
|
+
<Text color={THEME.dim}> [{bar}] {completed}/{total} ({pct}%)</Text>
|
|
25
|
+
</Text>
|
|
26
|
+
{statusParts.length > 0 && (
|
|
27
|
+
<Text color={THEME.dim}>{statusParts.join(' · ')}</Text>
|
|
28
|
+
)}
|
|
29
|
+
{(msg.recentEvents || []).map((evt, i) => (
|
|
30
|
+
<Text key={i} color={THEME.dim}>{evt}</Text>
|
|
31
|
+
))}
|
|
32
|
+
</Box>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../../theme.js';
|
|
4
|
+
|
|
5
|
+
export function SkillMessage({ msg, showDetails = false }) {
|
|
6
|
+
let icon = '⚡';
|
|
7
|
+
let color = THEME.warning;
|
|
8
|
+
|
|
9
|
+
if (msg.status === 'completed') {
|
|
10
|
+
icon = '✓';
|
|
11
|
+
color = THEME.success;
|
|
12
|
+
} else if (msg.status === 'error') {
|
|
13
|
+
icon = '✗';
|
|
14
|
+
color = THEME.error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box flexDirection="column">
|
|
19
|
+
<Text>
|
|
20
|
+
<Text color={color}>{icon}</Text>
|
|
21
|
+
<Text bold color={color}> {msg.name}</Text>
|
|
22
|
+
<Text color={THEME.dim}> skill</Text>
|
|
23
|
+
{msg.status === 'running' && <Text color={THEME.dim}> …</Text>}
|
|
24
|
+
{msg.error ? <Text color={THEME.error}> - {msg.error}</Text> : null}
|
|
25
|
+
</Text>
|
|
26
|
+
{showDetails && msg.description ? (
|
|
27
|
+
<Text color={THEME.dim}> {msg.description}</Text>
|
|
28
|
+
) : null}
|
|
29
|
+
</Box>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../../theme.js';
|
|
4
|
+
|
|
5
|
+
export function SystemMessage({ msg }) {
|
|
6
|
+
return (
|
|
7
|
+
<Box flexDirection="column" marginTop={1}>
|
|
8
|
+
<Text bold color={THEME.dim}>System</Text>
|
|
9
|
+
<Text color={THEME.dim}>{msg.content || ''}</Text>
|
|
10
|
+
</Box>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../../theme.js';
|
|
4
|
+
|
|
5
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
6
|
+
|
|
7
|
+
export function ThinkingIndicator() {
|
|
8
|
+
const [frame, setFrame] = useState(0);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const timer = setInterval(() => {
|
|
12
|
+
setFrame(f => (f + 1) % FRAMES.length);
|
|
13
|
+
}, 100);
|
|
14
|
+
return () => clearInterval(timer);
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box marginTop={1}>
|
|
19
|
+
<Text>
|
|
20
|
+
<Text color={THEME.assistant}>{FRAMES[frame]}</Text>
|
|
21
|
+
<Text color={THEME.dim}> thinking...</Text>
|
|
22
|
+
</Text>
|
|
23
|
+
</Box>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../../theme.js';
|
|
4
|
+
import { ToolMessage } from './tool-message.jsx';
|
|
5
|
+
import { SkillMessage } from './skill-message.jsx';
|
|
6
|
+
|
|
7
|
+
export function ToolGroup({ messages, expanded = false }) {
|
|
8
|
+
const completed = messages.filter(m => m.status === 'completed').length;
|
|
9
|
+
const errors = messages.filter(m => m.status === 'error').length;
|
|
10
|
+
const running = messages.filter(m => m.status === 'invoked' || m.status === 'running').length;
|
|
11
|
+
const allDone = running === 0;
|
|
12
|
+
|
|
13
|
+
// While running, show all tools individually
|
|
14
|
+
if (!allDone) {
|
|
15
|
+
return (
|
|
16
|
+
<Box flexDirection="column" marginTop={1}>
|
|
17
|
+
{messages.map((msg, i) => {
|
|
18
|
+
if (msg.type === 'skill') return <SkillMessage key={i} msg={msg} />;
|
|
19
|
+
return <ToolMessage key={i} msg={msg} />;
|
|
20
|
+
})}
|
|
21
|
+
</Box>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const icon = errors > 0 ? '✗' : '✓';
|
|
26
|
+
const color = errors > 0 ? THEME.error : THEME.success;
|
|
27
|
+
const skills = messages.filter(m => m.type === 'skill').length;
|
|
28
|
+
const tools = messages.filter(m => m.type === 'tool').length;
|
|
29
|
+
const parts = [];
|
|
30
|
+
if (tools > 0) parts.push(`${tools} tool${tools > 1 ? 's' : ''}`);
|
|
31
|
+
if (skills > 0) parts.push(`${skills} skill${skills > 1 ? 's' : ''}`);
|
|
32
|
+
const label = parts.join(', ');
|
|
33
|
+
const detail = errors > 0 ? `${completed} done, ${errors} failed` : `${completed} done`;
|
|
34
|
+
|
|
35
|
+
if (!expanded) {
|
|
36
|
+
return (
|
|
37
|
+
<Box marginTop={1}>
|
|
38
|
+
<Text>
|
|
39
|
+
<Text color={color}>{icon}</Text>
|
|
40
|
+
<Text color={THEME.dim}> {label} ({detail})</Text>
|
|
41
|
+
</Text>
|
|
42
|
+
</Box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Box flexDirection="column" marginTop={1}>
|
|
48
|
+
<Box>
|
|
49
|
+
<Text>
|
|
50
|
+
<Text color={color}>{icon}</Text>
|
|
51
|
+
<Text color={THEME.dim}> {label} ({detail})</Text>
|
|
52
|
+
</Text>
|
|
53
|
+
</Box>
|
|
54
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
55
|
+
{messages.map((msg, i) => {
|
|
56
|
+
if (msg.type === 'skill') return <SkillMessage key={i} msg={msg} showDetails={true} />;
|
|
57
|
+
return <ToolMessage key={i} msg={msg} showDetails={true} />;
|
|
58
|
+
})}
|
|
59
|
+
</Box>
|
|
60
|
+
</Box>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../../theme.js';
|
|
4
|
+
|
|
5
|
+
const RESULT_MAX = 500;
|
|
6
|
+
|
|
7
|
+
function formatRaw(value) {
|
|
8
|
+
if (value == null) return null;
|
|
9
|
+
if (typeof value === 'string') return value;
|
|
10
|
+
try { return JSON.stringify(value, null, 2); } catch { return String(value); }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function truncateResult(text) {
|
|
14
|
+
if (!text || text.length <= RESULT_MAX) return text;
|
|
15
|
+
return text.slice(0, RESULT_MAX) + '\n… (truncated)';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ToolMessage({ msg, showDetails = false }) {
|
|
19
|
+
let icon = '⚙';
|
|
20
|
+
let color = THEME.tool;
|
|
21
|
+
|
|
22
|
+
if (msg.status === 'completed') {
|
|
23
|
+
icon = '✓';
|
|
24
|
+
color = THEME.success;
|
|
25
|
+
} else if (msg.status === 'error') {
|
|
26
|
+
icon = '✗';
|
|
27
|
+
color = THEME.error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rawArgs = showDetails ? formatRaw(msg.rawArguments) : null;
|
|
31
|
+
const rawResult = showDetails ? truncateResult(formatRaw(msg.rawResult)) : null;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Box flexDirection="column">
|
|
35
|
+
<Text>
|
|
36
|
+
<Text color={color}>{icon}</Text>
|
|
37
|
+
<Text bold color={color}> {msg.name}</Text>
|
|
38
|
+
{msg.error ? <Text color={THEME.error}> - {msg.error}</Text> : null}
|
|
39
|
+
</Text>
|
|
40
|
+
{rawArgs && (
|
|
41
|
+
<Box flexDirection="column" marginLeft={2} marginTop={0}>
|
|
42
|
+
<Text bold color={THEME.dim}>input</Text>
|
|
43
|
+
<Box marginLeft={2}>
|
|
44
|
+
<Text color={THEME.dim} wrap="wrap">{rawArgs}</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
</Box>
|
|
47
|
+
)}
|
|
48
|
+
{rawResult && (
|
|
49
|
+
<Box flexDirection="column" marginLeft={2} marginTop={0}>
|
|
50
|
+
<Text bold color={THEME.dim}>output</Text>
|
|
51
|
+
<Box marginLeft={2}>
|
|
52
|
+
<Text color={THEME.dim} wrap="wrap">{rawResult}</Text>
|
|
53
|
+
</Box>
|
|
54
|
+
</Box>
|
|
55
|
+
)}
|
|
56
|
+
{showDetails && msg.error && (
|
|
57
|
+
<Box flexDirection="column" marginLeft={2} marginTop={0}>
|
|
58
|
+
<Text bold color={THEME.error}>error</Text>
|
|
59
|
+
<Box marginLeft={2}>
|
|
60
|
+
<Text color={THEME.error} wrap="wrap">{msg.error}</Text>
|
|
61
|
+
</Box>
|
|
62
|
+
</Box>
|
|
63
|
+
)}
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../../theme.js';
|
|
4
|
+
|
|
5
|
+
export function UserMessage({ msg }) {
|
|
6
|
+
return (
|
|
7
|
+
<Box flexDirection="column" marginTop={1}>
|
|
8
|
+
<Text bold color={THEME.user}>You</Text>
|
|
9
|
+
<Text wrap="wrap">{msg.content || ''}</Text>
|
|
10
|
+
</Box>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../theme.js';
|
|
4
|
+
|
|
5
|
+
const MAX_VISIBLE_ROWS = 10;
|
|
6
|
+
|
|
7
|
+
function renderBrowseEntry(entry, isSelected) {
|
|
8
|
+
if (entry.type === 'section') {
|
|
9
|
+
return (
|
|
10
|
+
<Text bold color="magenta">
|
|
11
|
+
{entry.label}
|
|
12
|
+
</Text>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (entry.type === 'custom') {
|
|
17
|
+
const marker = entry.is_current ? '●' : '+';
|
|
18
|
+
return (
|
|
19
|
+
<Text
|
|
20
|
+
backgroundColor={isSelected ? THEME.primary : undefined}
|
|
21
|
+
color={isSelected ? 'black' : 'yellow'}
|
|
22
|
+
>
|
|
23
|
+
{` ${marker} Custom model…`}
|
|
24
|
+
<Text color={isSelected ? 'black' : THEME.dim}>
|
|
25
|
+
{entry.current_model_id ? ` current: ${entry.current_model_id}` : ''}
|
|
26
|
+
</Text>
|
|
27
|
+
<Text color={isSelected ? 'black' : THEME.dim}>
|
|
28
|
+
{` ${entry.provider_name}`}
|
|
29
|
+
</Text>
|
|
30
|
+
</Text>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const marker = entry.is_current ? '●' : ' ';
|
|
35
|
+
const badge = entry.deployment === 'local'
|
|
36
|
+
? '[Local] '
|
|
37
|
+
: entry.deployment === 'cloud'
|
|
38
|
+
? '[Cloud] '
|
|
39
|
+
: '';
|
|
40
|
+
const badgeColor = isSelected
|
|
41
|
+
? 'black'
|
|
42
|
+
: entry.deployment === 'cloud'
|
|
43
|
+
? 'blue'
|
|
44
|
+
: entry.deployment === 'local'
|
|
45
|
+
? 'green'
|
|
46
|
+
: THEME.dim;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Text
|
|
50
|
+
backgroundColor={isSelected ? THEME.primary : undefined}
|
|
51
|
+
color={isSelected ? 'black' : THEME.text}
|
|
52
|
+
>
|
|
53
|
+
{` ${marker} `}
|
|
54
|
+
<Text color={badgeColor}>{badge}</Text>
|
|
55
|
+
{entry.model_name}
|
|
56
|
+
<Text color={isSelected ? 'black' : THEME.dim}>
|
|
57
|
+
{` ${entry.provider_name}`}
|
|
58
|
+
</Text>
|
|
59
|
+
</Text>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ModelPicker({ picker, termHeight = 40 }) {
|
|
64
|
+
if (!picker) return null;
|
|
65
|
+
|
|
66
|
+
const visibleRows = Math.max(4, Math.min(MAX_VISIBLE_ROWS, termHeight - 18));
|
|
67
|
+
|
|
68
|
+
if (picker.mode === 'custom') {
|
|
69
|
+
return (
|
|
70
|
+
<Box justifyContent="center" paddingX={2} marginBottom={1}>
|
|
71
|
+
<Box
|
|
72
|
+
width={72}
|
|
73
|
+
flexDirection="column"
|
|
74
|
+
borderStyle="round"
|
|
75
|
+
borderColor={THEME.primary}
|
|
76
|
+
paddingX={1}
|
|
77
|
+
paddingY={1}
|
|
78
|
+
>
|
|
79
|
+
<Text bold color={THEME.primary}>Select model</Text>
|
|
80
|
+
<Text color={THEME.dim}>
|
|
81
|
+
Provider: <Text color={THEME.text}>{picker.providerName}</Text>
|
|
82
|
+
<Text color={THEME.dim}>{` (${picker.providerId})`}</Text>
|
|
83
|
+
</Text>
|
|
84
|
+
<Text> </Text>
|
|
85
|
+
<Text>
|
|
86
|
+
<Text color={THEME.dim}>Model ID: </Text>
|
|
87
|
+
<Text color={THEME.text}>{picker.value}</Text>
|
|
88
|
+
<Text color={THEME.accent}>█</Text>
|
|
89
|
+
</Text>
|
|
90
|
+
<Text> </Text>
|
|
91
|
+
<Text color={THEME.dim}>
|
|
92
|
+
{picker.status || 'Enter confirms • Esc cancels'}
|
|
93
|
+
</Text>
|
|
94
|
+
</Box>
|
|
95
|
+
</Box>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const start = picker.scroll;
|
|
100
|
+
const end = Math.min(picker.entries.length, start + visibleRows);
|
|
101
|
+
const visibleEntries = picker.entries.slice(start, end);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Box justifyContent="center" paddingX={2} marginBottom={1}>
|
|
105
|
+
<Box
|
|
106
|
+
width={72}
|
|
107
|
+
flexDirection="column"
|
|
108
|
+
borderStyle="round"
|
|
109
|
+
borderColor={THEME.primary}
|
|
110
|
+
paddingX={1}
|
|
111
|
+
paddingY={1}
|
|
112
|
+
>
|
|
113
|
+
<Text bold color={THEME.primary}>Select model</Text>
|
|
114
|
+
<Text color={picker.focus === 'search' ? THEME.primary : THEME.dim}>
|
|
115
|
+
Search: <Text color={THEME.text}>{picker.query}</Text>
|
|
116
|
+
{picker.focus === 'search' && <Text color={THEME.accent}>█</Text>}
|
|
117
|
+
</Text>
|
|
118
|
+
<Text> </Text>
|
|
119
|
+
{visibleEntries.length === 0 ? (
|
|
120
|
+
<Text color={THEME.dim}>No models available.</Text>
|
|
121
|
+
) : (
|
|
122
|
+
visibleEntries.map((entry, index) => {
|
|
123
|
+
const absoluteIndex = start + index;
|
|
124
|
+
return (
|
|
125
|
+
<Box key={`${entry.type}:${entry.provider_id || entry.model_id || entry.label}:${absoluteIndex}`}>
|
|
126
|
+
{renderBrowseEntry(entry, picker.selected === absoluteIndex)}
|
|
127
|
+
</Box>
|
|
128
|
+
);
|
|
129
|
+
})
|
|
130
|
+
)}
|
|
131
|
+
<Text> </Text>
|
|
132
|
+
<Text color={THEME.dim}>
|
|
133
|
+
{picker.status || '↑↓ navigate • Tab switch • Enter select • Esc close'}
|
|
134
|
+
</Text>
|
|
135
|
+
</Box>
|
|
136
|
+
</Box>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { THEME } from '../theme.js';
|
|
4
|
+
|
|
5
|
+
export function MultilineInput({ onSubmit, placeholder, prompt }) {
|
|
6
|
+
const [value, setValue] = useState('');
|
|
7
|
+
const [cursorCol, setCursorCol] = useState(0);
|
|
8
|
+
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
// Enter without shift → submit
|
|
11
|
+
if (key.return && !key.shift) {
|
|
12
|
+
if (value.trim()) {
|
|
13
|
+
onSubmit(value);
|
|
14
|
+
setValue('');
|
|
15
|
+
setCursorCol(0);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Shift+Enter → newline
|
|
21
|
+
if (key.return && key.shift) {
|
|
22
|
+
setValue(prev => prev + '\n');
|
|
23
|
+
setCursorCol(0);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Backspace
|
|
28
|
+
if (key.backspace || key.delete) {
|
|
29
|
+
if (value.length > 0) {
|
|
30
|
+
setValue(prev => prev.slice(0, -1));
|
|
31
|
+
setCursorCol(prev => Math.max(0, prev - 1));
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Ignore other control keys
|
|
37
|
+
if (key.ctrl || key.meta || key.escape) return;
|
|
38
|
+
|
|
39
|
+
// Tab → insert spaces
|
|
40
|
+
if (key.tab) {
|
|
41
|
+
setValue(prev => prev + ' ');
|
|
42
|
+
setCursorCol(prev => prev + 2);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Regular character input
|
|
47
|
+
if (input && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow) {
|
|
48
|
+
setValue(prev => prev + input);
|
|
49
|
+
setCursorCol(prev => prev + input.length);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const displayValue = value || '';
|
|
54
|
+
const lines = displayValue.split('\n');
|
|
55
|
+
const showPlaceholder = !displayValue && placeholder;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Box flexDirection="column">
|
|
59
|
+
{showPlaceholder ? (
|
|
60
|
+
<Text color={THEME.dim}>{prompt}{placeholder}</Text>
|
|
61
|
+
) : (
|
|
62
|
+
lines.map((line, i) => (
|
|
63
|
+
<Text key={i}>
|
|
64
|
+
{i === 0 ? <Text color={THEME.accent} bold>{prompt}</Text> : <Text color={THEME.dim}> </Text>}
|
|
65
|
+
<Text>{line}</Text>
|
|
66
|
+
{i === lines.length - 1 ? <Text color={THEME.accent}>█</Text> : null}
|
|
67
|
+
</Text>
|
|
68
|
+
))
|
|
69
|
+
)}
|
|
70
|
+
</Box>
|
|
71
|
+
);
|
|
72
|
+
}
|