@moxxy/cli 0.0.12 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -112
- package/bin/moxxy +10 -0
- package/package.json +36 -53
- package/src/api-client.js +286 -0
- package/src/cli.js +341 -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 +767 -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/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 +368 -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,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../theme.js';
|
|
4
|
+
|
|
5
|
+
const MAX_VISIBLE_ROWS = 8;
|
|
6
|
+
|
|
7
|
+
export function ActionPicker({ picker, termHeight = 40 }) {
|
|
8
|
+
if (!picker) return null;
|
|
9
|
+
|
|
10
|
+
if (picker.mode === 'input') {
|
|
11
|
+
return (
|
|
12
|
+
<Box justifyContent="center" paddingX={2} marginBottom={1}>
|
|
13
|
+
<Box
|
|
14
|
+
width={72}
|
|
15
|
+
flexDirection="column"
|
|
16
|
+
borderStyle="round"
|
|
17
|
+
borderColor={THEME.primary}
|
|
18
|
+
paddingX={1}
|
|
19
|
+
paddingY={1}
|
|
20
|
+
>
|
|
21
|
+
<Text bold color={THEME.primary}>{picker.title}</Text>
|
|
22
|
+
<Text color={THEME.dim}>
|
|
23
|
+
{picker.stepLabel}
|
|
24
|
+
</Text>
|
|
25
|
+
<Text> </Text>
|
|
26
|
+
<Text>
|
|
27
|
+
<Text color={THEME.dim}>{picker.inputLabel}: </Text>
|
|
28
|
+
<Text color={THEME.text}>{picker.value}</Text>
|
|
29
|
+
<Text color={THEME.accent}>█</Text>
|
|
30
|
+
</Text>
|
|
31
|
+
{picker.placeholder ? (
|
|
32
|
+
<Text color={THEME.dim}>
|
|
33
|
+
{`e.g. ${picker.placeholder}`}
|
|
34
|
+
</Text>
|
|
35
|
+
) : null}
|
|
36
|
+
<Text> </Text>
|
|
37
|
+
<Text color={THEME.dim}>
|
|
38
|
+
{picker.status || 'Enter continues • Esc cancels'}
|
|
39
|
+
</Text>
|
|
40
|
+
</Box>
|
|
41
|
+
</Box>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const visibleRows = Math.max(4, Math.min(MAX_VISIBLE_ROWS, termHeight - 18));
|
|
46
|
+
const start = picker.scroll;
|
|
47
|
+
const end = Math.min(picker.items.length, start + visibleRows);
|
|
48
|
+
const visibleItems = picker.items.slice(start, end);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Box justifyContent="center" paddingX={2} marginBottom={1}>
|
|
52
|
+
<Box
|
|
53
|
+
width={64}
|
|
54
|
+
flexDirection="column"
|
|
55
|
+
borderStyle="round"
|
|
56
|
+
borderColor={THEME.primary}
|
|
57
|
+
paddingX={1}
|
|
58
|
+
paddingY={1}
|
|
59
|
+
>
|
|
60
|
+
<Text bold color={THEME.primary}>{picker.title}</Text>
|
|
61
|
+
<Text> </Text>
|
|
62
|
+
{visibleItems.map((item, index) => {
|
|
63
|
+
const absoluteIndex = start + index;
|
|
64
|
+
const isSelected = absoluteIndex === picker.selected;
|
|
65
|
+
return (
|
|
66
|
+
<Box key={`${item.label}:${absoluteIndex}`}>
|
|
67
|
+
<Text
|
|
68
|
+
backgroundColor={isSelected ? THEME.primary : undefined}
|
|
69
|
+
color={isSelected ? 'black' : THEME.text}
|
|
70
|
+
>
|
|
71
|
+
{` ${item.label}`}
|
|
72
|
+
<Text color={isSelected ? 'black' : THEME.dim}>
|
|
73
|
+
{item.description ? ` ${item.description}` : ''}
|
|
74
|
+
</Text>
|
|
75
|
+
</Text>
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
<Text> </Text>
|
|
80
|
+
<Text color={THEME.dim}>
|
|
81
|
+
{picker.status || '↑↓ navigate • Enter select • Esc close'}
|
|
82
|
+
</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
</Box>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../theme.js';
|
|
4
|
+
import { UserMessage } from './messages/user-message.jsx';
|
|
5
|
+
import { AssistantMessage } from './messages/assistant-message.jsx';
|
|
6
|
+
import { ToolMessage } from './messages/tool-message.jsx';
|
|
7
|
+
import { SkillMessage } from './messages/skill-message.jsx';
|
|
8
|
+
import { ToolGroup } from './messages/tool-group.jsx';
|
|
9
|
+
import { SystemMessage } from './messages/system-message.jsx';
|
|
10
|
+
import { AskMessage } from './messages/ask-message.jsx';
|
|
11
|
+
import { EventMessage } from './messages/event-message.jsx';
|
|
12
|
+
import { HiveStatus } from './messages/hive-status.jsx';
|
|
13
|
+
import { ChannelMessage } from './messages/channel-message.jsx';
|
|
14
|
+
import { ThinkingIndicator } from './messages/thinking.jsx';
|
|
15
|
+
|
|
16
|
+
function isToolish(msg) {
|
|
17
|
+
return msg.type === 'tool' || msg.type === 'skill';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function groupMessages(messages) {
|
|
21
|
+
const groups = [];
|
|
22
|
+
let currentToolGroup = null;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < messages.length; i++) {
|
|
25
|
+
const msg = messages[i];
|
|
26
|
+
if (isToolish(msg)) {
|
|
27
|
+
if (!currentToolGroup) {
|
|
28
|
+
currentToolGroup = [];
|
|
29
|
+
}
|
|
30
|
+
currentToolGroup.push(msg);
|
|
31
|
+
} else {
|
|
32
|
+
if (currentToolGroup) {
|
|
33
|
+
groups.push({ type: 'tool-group', messages: currentToolGroup, startIdx: i - currentToolGroup.length });
|
|
34
|
+
currentToolGroup = null;
|
|
35
|
+
}
|
|
36
|
+
groups.push(msg);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (currentToolGroup) {
|
|
40
|
+
groups.push({ type: 'tool-group', messages: currentToolGroup, startIdx: messages.length - currentToolGroup.length });
|
|
41
|
+
}
|
|
42
|
+
return groups;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderItem(item, index, agentName, toolsExpanded) {
|
|
46
|
+
if (item.type === 'tool-group') {
|
|
47
|
+
if (item.messages.length === 1) {
|
|
48
|
+
const msg = item.messages[0];
|
|
49
|
+
const inner = msg.type === 'skill'
|
|
50
|
+
? <SkillMessage key={`tg-${index}`} msg={msg} showDetails={toolsExpanded} />
|
|
51
|
+
: <ToolMessage key={`tg-${index}`} msg={msg} showDetails={toolsExpanded} />;
|
|
52
|
+
return <Box key={`tg-${index}`} flexDirection="column" marginTop={1}>{inner}</Box>;
|
|
53
|
+
}
|
|
54
|
+
return <ToolGroup key={`tg-${index}`} messages={item.messages} expanded={toolsExpanded} />;
|
|
55
|
+
}
|
|
56
|
+
switch (item.type) {
|
|
57
|
+
case 'user':
|
|
58
|
+
return <UserMessage key={index} msg={item} />;
|
|
59
|
+
case 'assistant':
|
|
60
|
+
return <AssistantMessage key={index} msg={item} agentName={agentName} />;
|
|
61
|
+
case 'system':
|
|
62
|
+
return <SystemMessage key={index} msg={item} />;
|
|
63
|
+
case 'ask':
|
|
64
|
+
return <AskMessage key={index} msg={item} />;
|
|
65
|
+
case 'event':
|
|
66
|
+
case 'hive-event':
|
|
67
|
+
return <EventMessage key={index} msg={item} />;
|
|
68
|
+
case 'hive-status':
|
|
69
|
+
return <HiveStatus key={index} msg={item} />;
|
|
70
|
+
case 'channel':
|
|
71
|
+
return <ChannelMessage key={index} msg={item} />;
|
|
72
|
+
default:
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Header ~9 lines, input area ~4 lines, padding ~2
|
|
78
|
+
const CHROME_LINES = 15;
|
|
79
|
+
// Rough estimate: each message group takes ~3 lines (label + content + margin)
|
|
80
|
+
const LINES_PER_GROUP = 3;
|
|
81
|
+
|
|
82
|
+
export function ChatPanel({ messages, thinking, agentName, scrollOffset = 0, toolsExpanded = false, termHeight = 40 }) {
|
|
83
|
+
const groups = groupMessages(messages);
|
|
84
|
+
const total = groups.length;
|
|
85
|
+
|
|
86
|
+
// Calculate how many groups we can fit on screen
|
|
87
|
+
const availableLines = Math.max(6, termHeight - CHROME_LINES);
|
|
88
|
+
const maxVisible = Math.max(1, Math.floor(availableLines / LINES_PER_GROUP));
|
|
89
|
+
|
|
90
|
+
// scrollOffset = number of groups from the end to skip
|
|
91
|
+
const clampedOffset = Math.min(scrollOffset, Math.max(0, total - 1));
|
|
92
|
+
const endIdx = total - clampedOffset;
|
|
93
|
+
const startIdx = Math.max(0, endIdx - maxVisible);
|
|
94
|
+
const visible = endIdx > 0 ? groups.slice(startIdx, endIdx) : [];
|
|
95
|
+
const isScrolled = clampedOffset > 0;
|
|
96
|
+
const hasOlderMessages = startIdx > 0;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Box flexDirection="column" paddingLeft={2} paddingRight={1} paddingY={1}>
|
|
100
|
+
{hasOlderMessages && (
|
|
101
|
+
<Box justifyContent="center">
|
|
102
|
+
<Text color={THEME.dim}>── {startIdx} older message{startIdx !== 1 ? 's' : ''} (Shift+↑ to scroll) ──</Text>
|
|
103
|
+
</Box>
|
|
104
|
+
)}
|
|
105
|
+
{visible.length === 0 && !hasOlderMessages ? (
|
|
106
|
+
<Box paddingTop={1}>
|
|
107
|
+
<Text color={THEME.dim}>No messages yet. Type a task or /help for commands.</Text>
|
|
108
|
+
</Box>
|
|
109
|
+
) : (
|
|
110
|
+
visible.map((item, i) => renderItem(item, i, agentName, toolsExpanded))
|
|
111
|
+
)}
|
|
112
|
+
{thinking && !isScrolled && <ThinkingIndicator />}
|
|
113
|
+
{isScrolled && (
|
|
114
|
+
<Box justifyContent="center" marginTop={1}>
|
|
115
|
+
<Text color={THEME.dim}>── {clampedOffset} newer message{clampedOffset !== 1 ? 's' : ''} (Shift+↓ to scroll) ──</Text>
|
|
116
|
+
</Box>
|
|
117
|
+
)}
|
|
118
|
+
</Box>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../theme.js';
|
|
4
|
+
|
|
5
|
+
export function Footer() {
|
|
6
|
+
return (
|
|
7
|
+
<Box width="100%" paddingX={1}>
|
|
8
|
+
<Text color={THEME.dim}>
|
|
9
|
+
{' ^X stop /help commands ^C exit'}
|
|
10
|
+
</Text>
|
|
11
|
+
</Box>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../theme.js';
|
|
4
|
+
|
|
5
|
+
const LOGO_LINES = [
|
|
6
|
+
'███╗ ███╗ ██████╗ ██╗ ██╗██╗ ██╗██╗ ██╗',
|
|
7
|
+
'████╗ ████║██╔═══██╗╚██╗██╔╝╚██╗██╔╝╚██╗ ██╔╝',
|
|
8
|
+
'██╔████╔██║██║ ██║ ╚███╔╝ ╚███╔╝ ╚████╔╝ ',
|
|
9
|
+
'██║╚██╔╝██║██║ ██║ ██╔██╗ ██╔██╗ ╚██╔╝ ',
|
|
10
|
+
'██║ ╚═╝ ██║╚██████╔╝██╔╝ ██╗██╔╝ ██╗ ██║ ',
|
|
11
|
+
'╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function computeContextUtilization(contextTokens, contextWindow) {
|
|
15
|
+
const windowSize = Number(contextWindow) || 0;
|
|
16
|
+
if (windowSize <= 0) return { hasWindow: false, percent: 0, band: 'low' };
|
|
17
|
+
const tokens = Number(contextTokens) || 0;
|
|
18
|
+
const percent = Math.min(100, Math.max(0, Math.round((tokens / windowSize) * 100)));
|
|
19
|
+
if (percent <= 60) return { hasWindow: true, percent, band: 'low' };
|
|
20
|
+
if (percent <= 80) return { hasWindow: true, percent, band: 'medium' };
|
|
21
|
+
return { hasWindow: true, percent, band: 'high' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { computeContextUtilization };
|
|
25
|
+
|
|
26
|
+
export function Header({ agent }) {
|
|
27
|
+
const model = agent ? `${agent.provider_id}/${agent.model_id}` : '';
|
|
28
|
+
const name = agent?.name || 'connecting...';
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Box width="100%" flexDirection="column" flexShrink={0}>
|
|
32
|
+
<Box flexDirection="column" paddingX={1} paddingTop={1}>
|
|
33
|
+
{LOGO_LINES.map((line, i) => (
|
|
34
|
+
<Text key={i} color={THEME.text}> {line}</Text>
|
|
35
|
+
))}
|
|
36
|
+
<Text>
|
|
37
|
+
<Text color={THEME.user}> {name}</Text>
|
|
38
|
+
<Text color={THEME.dim}> · </Text>
|
|
39
|
+
<Text color={THEME.dim}>{model || '-'}</Text>
|
|
40
|
+
</Text>
|
|
41
|
+
</Box>
|
|
42
|
+
<Box width="100%" borderStyle="single" borderBottom={true} borderTop={false} borderLeft={false} borderRight={false} borderColor={THEME.dim} />
|
|
43
|
+
</Box>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { THEME } from '../theme.js';
|
|
4
|
+
import { matchCommands } from '../slash-commands.js';
|
|
5
|
+
import { clampAutocompleteScroll, resolveAutocompleteSelection } from '../input-utils.js';
|
|
6
|
+
|
|
7
|
+
const AUTOCOMPLETE_VISIBLE_ROWS = 8;
|
|
8
|
+
|
|
9
|
+
function wordBoundaryLeft(text, pos) {
|
|
10
|
+
let i = pos - 1;
|
|
11
|
+
while (i > 0 && /\s/.test(text[i])) i--;
|
|
12
|
+
while (i > 0 && !/\s/.test(text[i - 1])) i--;
|
|
13
|
+
return Math.max(0, i);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function wordBoundaryRight(text, pos) {
|
|
17
|
+
let i = pos;
|
|
18
|
+
while (i < text.length && /\s/.test(text[i])) i++;
|
|
19
|
+
while (i < text.length && !/\s/.test(text[i])) i++;
|
|
20
|
+
return i;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function InputArea({ onSubmit, onExit, onStop, pendingAsk, agent, disabled = false }) {
|
|
24
|
+
const [inputValue, setInputValue] = useState('');
|
|
25
|
+
const [cursor, setCursor] = useState(0);
|
|
26
|
+
const [selectedMatchIndex, setSelectedMatchIndex] = useState(0);
|
|
27
|
+
const [matchScrollOffset, setMatchScrollOffset] = useState(0);
|
|
28
|
+
// anchor !== null means there's an active selection between anchor and cursor
|
|
29
|
+
const [anchor, setAnchor] = useState(null);
|
|
30
|
+
|
|
31
|
+
const valRef = useRef(inputValue);
|
|
32
|
+
const curRef = useRef(cursor);
|
|
33
|
+
const anchorRef = useRef(anchor);
|
|
34
|
+
valRef.current = inputValue;
|
|
35
|
+
curRef.current = cursor;
|
|
36
|
+
anchorRef.current = anchor;
|
|
37
|
+
|
|
38
|
+
const matches = inputValue.startsWith('/') ? matchCommands(inputValue) : [];
|
|
39
|
+
const autocompleteIndex = Math.min(selectedMatchIndex, Math.max(0, matches.length - 1));
|
|
40
|
+
|
|
41
|
+
const hasSelection = anchor !== null && anchor !== cursor;
|
|
42
|
+
const selStart = hasSelection ? Math.min(anchor, cursor) : cursor;
|
|
43
|
+
const selEnd = hasSelection ? Math.max(anchor, cursor) : cursor;
|
|
44
|
+
|
|
45
|
+
// Delete selection and return new { value, cursor }
|
|
46
|
+
const deleteSelection = useCallback(() => {
|
|
47
|
+
const a = anchorRef.current;
|
|
48
|
+
const c = curRef.current;
|
|
49
|
+
const v = valRef.current;
|
|
50
|
+
if (a === null || a === c) return null;
|
|
51
|
+
const s = Math.min(a, c);
|
|
52
|
+
const e = Math.max(a, c);
|
|
53
|
+
return { value: v.slice(0, s) + v.slice(e), cursor: s };
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const clearSelection = useCallback(() => setAnchor(null), []);
|
|
57
|
+
|
|
58
|
+
const handleSubmit = useCallback((value) => {
|
|
59
|
+
const text = typeof value === 'string' ? value : '';
|
|
60
|
+
if (text.trim()) {
|
|
61
|
+
onSubmit(text);
|
|
62
|
+
setInputValue('');
|
|
63
|
+
setCursor(0);
|
|
64
|
+
setAnchor(null);
|
|
65
|
+
}
|
|
66
|
+
}, [onSubmit]);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setSelectedMatchIndex(0);
|
|
70
|
+
setMatchScrollOffset(0);
|
|
71
|
+
}, [inputValue, matches.length]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setMatchScrollOffset(prev =>
|
|
75
|
+
clampAutocompleteScroll(autocompleteIndex, prev, AUTOCOMPLETE_VISIBLE_ROWS, matches.length)
|
|
76
|
+
);
|
|
77
|
+
}, [autocompleteIndex, matches.length]);
|
|
78
|
+
|
|
79
|
+
// Raw stdin listener for Option+Backspace (\x1b\x7f)
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const stream = process.stdin;
|
|
82
|
+
const onData = (data) => {
|
|
83
|
+
if (disabled) return;
|
|
84
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
85
|
+
if (buf.length === 2 && buf[0] === 0x1b && buf[1] === 0x7f) {
|
|
86
|
+
const val = valRef.current;
|
|
87
|
+
const c = curRef.current;
|
|
88
|
+
const a = anchorRef.current;
|
|
89
|
+
// If there's a selection, delete it
|
|
90
|
+
if (a !== null && a !== c) {
|
|
91
|
+
const s = Math.min(a, c);
|
|
92
|
+
const e = Math.max(a, c);
|
|
93
|
+
setInputValue(val.slice(0, s) + val.slice(e));
|
|
94
|
+
setCursor(s);
|
|
95
|
+
setAnchor(null);
|
|
96
|
+
} else if (c > 0) {
|
|
97
|
+
const pos = wordBoundaryLeft(val, c);
|
|
98
|
+
setInputValue(val.slice(0, pos) + val.slice(c));
|
|
99
|
+
setCursor(pos);
|
|
100
|
+
setAnchor(null);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
stream.prependListener('data', onData);
|
|
105
|
+
return () => stream.removeListener('data', onData);
|
|
106
|
+
}, [disabled]);
|
|
107
|
+
|
|
108
|
+
useInput((input, key) => {
|
|
109
|
+
if (disabled) return;
|
|
110
|
+
|
|
111
|
+
// Ctrl+C
|
|
112
|
+
if (key.ctrl && input === 'c') {
|
|
113
|
+
if (inputValue.length > 0) {
|
|
114
|
+
setInputValue('');
|
|
115
|
+
setCursor(0);
|
|
116
|
+
setAnchor(null);
|
|
117
|
+
} else {
|
|
118
|
+
onExit();
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Enter → submit
|
|
124
|
+
if (key.return && !key.shift) {
|
|
125
|
+
const selectedCommand = resolveAutocompleteSelection(inputValue, matches, autocompleteIndex);
|
|
126
|
+
if (selectedCommand) {
|
|
127
|
+
setInputValue(selectedCommand);
|
|
128
|
+
setCursor(selectedCommand.length);
|
|
129
|
+
setAnchor(null);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
handleSubmit(inputValue);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Shift+Enter → newline
|
|
138
|
+
if (key.return && key.shift) {
|
|
139
|
+
if (hasSelection) {
|
|
140
|
+
const d = deleteSelection();
|
|
141
|
+
if (d) {
|
|
142
|
+
setInputValue(d.value.slice(0, d.cursor) + '\n' + d.value.slice(d.cursor));
|
|
143
|
+
setCursor(d.cursor + 1);
|
|
144
|
+
setAnchor(null);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
setInputValue(prev => prev.slice(0, cursor) + '\n' + prev.slice(cursor));
|
|
149
|
+
setCursor(c => c + 1);
|
|
150
|
+
setAnchor(null);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Backspace
|
|
155
|
+
if (key.backspace || key.delete) {
|
|
156
|
+
if (hasSelection) {
|
|
157
|
+
const d = deleteSelection();
|
|
158
|
+
if (d) {
|
|
159
|
+
setInputValue(d.value);
|
|
160
|
+
setCursor(d.cursor);
|
|
161
|
+
setAnchor(null);
|
|
162
|
+
}
|
|
163
|
+
} else if (cursor > 0) {
|
|
164
|
+
setInputValue(prev => prev.slice(0, cursor - 1) + prev.slice(cursor));
|
|
165
|
+
setCursor(c => c - 1);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Tab → autocomplete
|
|
171
|
+
if (key.tab && matches.length > 0) {
|
|
172
|
+
const selected = matches[autocompleteIndex] || matches[0];
|
|
173
|
+
setInputValue(selected.name);
|
|
174
|
+
setCursor(selected.name.length);
|
|
175
|
+
setAnchor(null);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Shift+Left - extend selection left
|
|
180
|
+
if (key.shift && key.leftArrow) {
|
|
181
|
+
if (anchor === null) setAnchor(cursor);
|
|
182
|
+
setCursor(c => Math.max(0, c - 1));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Shift+Right - extend selection right
|
|
187
|
+
if (key.shift && key.rightArrow) {
|
|
188
|
+
if (anchor === null) setAnchor(cursor);
|
|
189
|
+
setCursor(c => Math.min(inputValue.length, c + 1));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Option+Left - jump word left
|
|
194
|
+
if (key.meta && key.leftArrow) {
|
|
195
|
+
setCursor(c => wordBoundaryLeft(inputValue, c));
|
|
196
|
+
setAnchor(null);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Option+Right - jump word right
|
|
201
|
+
if (key.meta && key.rightArrow) {
|
|
202
|
+
setCursor(c => wordBoundaryRight(inputValue, c));
|
|
203
|
+
setAnchor(null);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Left arrow - move or collapse selection
|
|
208
|
+
if (key.leftArrow) {
|
|
209
|
+
if (hasSelection) {
|
|
210
|
+
setCursor(selStart);
|
|
211
|
+
setAnchor(null);
|
|
212
|
+
} else {
|
|
213
|
+
setCursor(c => Math.max(0, c - 1));
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Right arrow - move or collapse selection
|
|
219
|
+
if (key.rightArrow) {
|
|
220
|
+
if (hasSelection) {
|
|
221
|
+
setCursor(selEnd);
|
|
222
|
+
setAnchor(null);
|
|
223
|
+
} else {
|
|
224
|
+
setCursor(c => Math.min(inputValue.length, c + 1));
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Ctrl+W - delete word before cursor
|
|
230
|
+
if (key.ctrl && input === 'w') {
|
|
231
|
+
if (hasSelection) {
|
|
232
|
+
const d = deleteSelection();
|
|
233
|
+
if (d) { setInputValue(d.value); setCursor(d.cursor); setAnchor(null); }
|
|
234
|
+
} else if (cursor > 0) {
|
|
235
|
+
const pos = wordBoundaryLeft(inputValue, cursor);
|
|
236
|
+
setInputValue(prev => prev.slice(0, pos) + prev.slice(cursor));
|
|
237
|
+
setCursor(pos);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Ctrl+A - select all or move to start
|
|
243
|
+
if (key.ctrl && input === 'a') {
|
|
244
|
+
if (inputValue.length > 0) {
|
|
245
|
+
setAnchor(0);
|
|
246
|
+
setCursor(inputValue.length);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Ctrl+E - move to end
|
|
252
|
+
if (key.ctrl && input === 'e') {
|
|
253
|
+
setCursor(inputValue.length);
|
|
254
|
+
setAnchor(null);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Ctrl+U - delete everything before cursor
|
|
259
|
+
if (key.ctrl && input === 'u') {
|
|
260
|
+
setInputValue(prev => prev.slice(cursor));
|
|
261
|
+
setCursor(0);
|
|
262
|
+
setAnchor(null);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Ctrl+K - delete everything after cursor
|
|
267
|
+
if (key.ctrl && input === 'k') {
|
|
268
|
+
setInputValue(prev => prev.slice(0, cursor));
|
|
269
|
+
setAnchor(null);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Ignore other control/meta keys
|
|
274
|
+
if (key.ctrl || key.meta || key.escape) return;
|
|
275
|
+
|
|
276
|
+
// Up/down arrows - navigate slash command suggestions
|
|
277
|
+
if (matches.length > 0 && key.upArrow) {
|
|
278
|
+
setSelectedMatchIndex(prev => Math.max(0, prev - 1));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (matches.length > 0 && key.downArrow) {
|
|
283
|
+
setSelectedMatchIndex(prev => Math.min(matches.length - 1, prev + 1));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Regular character input
|
|
288
|
+
if (input) {
|
|
289
|
+
if (hasSelection) {
|
|
290
|
+
const d = deleteSelection();
|
|
291
|
+
if (d) {
|
|
292
|
+
setInputValue(d.value.slice(0, d.cursor) + input + d.value.slice(d.cursor));
|
|
293
|
+
setCursor(d.cursor + input.length);
|
|
294
|
+
setAnchor(null);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
setInputValue(prev => prev.slice(0, cursor) + input + prev.slice(cursor));
|
|
299
|
+
setCursor(c => c + input.length);
|
|
300
|
+
setAnchor(null);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const prompt = pendingAsk ? '? ' : '› ';
|
|
305
|
+
const showPlaceholder = !inputValue;
|
|
306
|
+
const placeholderText = pendingAsk ? 'Type your answer...' : 'Type a message or /command...';
|
|
307
|
+
const visibleMatchesStart = clampAutocompleteScroll(
|
|
308
|
+
autocompleteIndex,
|
|
309
|
+
matchScrollOffset,
|
|
310
|
+
AUTOCOMPLETE_VISIBLE_ROWS,
|
|
311
|
+
matches.length
|
|
312
|
+
);
|
|
313
|
+
const visibleMatches = matches.slice(
|
|
314
|
+
visibleMatchesStart,
|
|
315
|
+
visibleMatchesStart + AUTOCOMPLETE_VISIBLE_ROWS
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Render text with selection highlighting
|
|
319
|
+
let renderedInput;
|
|
320
|
+
if (showPlaceholder) {
|
|
321
|
+
renderedInput = <Text color={THEME.dim}>{placeholderText}</Text>;
|
|
322
|
+
} else if (hasSelection) {
|
|
323
|
+
const before = inputValue.slice(0, selStart);
|
|
324
|
+
const selected = inputValue.slice(selStart, selEnd);
|
|
325
|
+
const after = inputValue.slice(selEnd);
|
|
326
|
+
renderedInput = (
|
|
327
|
+
<Text>
|
|
328
|
+
{before}
|
|
329
|
+
<Text backgroundColor="white" color="black">{selected}</Text>
|
|
330
|
+
{after}
|
|
331
|
+
</Text>
|
|
332
|
+
);
|
|
333
|
+
} else {
|
|
334
|
+
const before = inputValue.slice(0, cursor);
|
|
335
|
+
const after = inputValue.slice(cursor);
|
|
336
|
+
renderedInput = (
|
|
337
|
+
<Text>
|
|
338
|
+
{before}
|
|
339
|
+
<Text color={THEME.accent}>█</Text>
|
|
340
|
+
{after}
|
|
341
|
+
</Text>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<Box flexDirection="column" width="100%" flexShrink={0}>
|
|
347
|
+
{matches.length > 0 && (
|
|
348
|
+
<Box flexDirection="column" width="100%" paddingX={1}>
|
|
349
|
+
{visibleMatches.map((cmd, i) => {
|
|
350
|
+
const absoluteIndex = visibleMatchesStart + i;
|
|
351
|
+
return (
|
|
352
|
+
<Box key={cmd.name}>
|
|
353
|
+
<Text>
|
|
354
|
+
{absoluteIndex === autocompleteIndex
|
|
355
|
+
? <><Text bold color={THEME.primary}>{cmd.name}</Text><Text color={THEME.text}> - {cmd.description}</Text></>
|
|
356
|
+
: <><Text color={THEME.dim}>{cmd.name}</Text><Text color={THEME.dim}> - {cmd.description}</Text></>
|
|
357
|
+
}
|
|
358
|
+
</Text>
|
|
359
|
+
</Box>
|
|
360
|
+
);
|
|
361
|
+
})}
|
|
362
|
+
</Box>
|
|
363
|
+
)}
|
|
364
|
+
<Box
|
|
365
|
+
width="100%"
|
|
366
|
+
flexDirection="row"
|
|
367
|
+
borderStyle="round"
|
|
368
|
+
borderColor={matches.length > 0 ? THEME.primary : THEME.border}
|
|
369
|
+
paddingX={1}
|
|
370
|
+
>
|
|
371
|
+
<Text bold color={THEME.accent}>{prompt}</Text>
|
|
372
|
+
{renderedInput}
|
|
373
|
+
</Box>
|
|
374
|
+
<Box width="100%" paddingX={1} justifyContent="space-between">
|
|
375
|
+
<Text color={THEME.dim}> ^C exit ^X stop ^T tools ^A select all /help</Text>
|
|
376
|
+
{agent && (
|
|
377
|
+
<Text color={THEME.dim}>
|
|
378
|
+
{agent.name || agent.id}{agent.model_id ? ` · ${agent.model_id}` : ''}
|
|
379
|
+
</Text>
|
|
380
|
+
)}
|
|
381
|
+
</Box>
|
|
382
|
+
</Box>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { THEME } from '../../theme.js';
|
|
4
|
+
|
|
5
|
+
export function AskMessage({ msg }) {
|
|
6
|
+
return (
|
|
7
|
+
<Box flexDirection="column" marginTop={1}>
|
|
8
|
+
<Text bold color={THEME.warning}>Agent needs input</Text>
|
|
9
|
+
<Text color={THEME.text}>{msg.question || ''}</Text>
|
|
10
|
+
<Text color={THEME.dim}>Type your answer below and press Enter.</Text>
|
|
11
|
+
</Box>
|
|
12
|
+
);
|
|
13
|
+
}
|