@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.
Files changed (149) hide show
  1. package/README.md +278 -112
  2. package/bin/moxxy +10 -0
  3. package/package.json +36 -53
  4. package/src/api-client.js +286 -0
  5. package/src/cli.js +349 -0
  6. package/src/commands/agent.js +413 -0
  7. package/src/commands/auth.js +326 -0
  8. package/src/commands/channel.js +285 -0
  9. package/src/commands/doctor.js +261 -0
  10. package/src/commands/events.js +80 -0
  11. package/src/commands/gateway.js +428 -0
  12. package/src/commands/heartbeat.js +145 -0
  13. package/src/commands/init.js +954 -0
  14. package/src/commands/mcp.js +278 -0
  15. package/src/commands/plugin.js +583 -0
  16. package/src/commands/provider.js +1934 -0
  17. package/src/commands/settings.js +224 -0
  18. package/src/commands/skill.js +125 -0
  19. package/src/commands/template.js +237 -0
  20. package/src/commands/uninstall.js +196 -0
  21. package/src/commands/update.js +406 -0
  22. package/src/commands/vault.js +219 -0
  23. package/src/help.js +392 -0
  24. package/src/lib/plugin-registry.js +98 -0
  25. package/src/platform.js +40 -0
  26. package/src/sse-client.js +79 -0
  27. package/src/tui/action-wizards.js +130 -0
  28. package/src/tui/app.jsx +859 -0
  29. package/src/tui/components/action-picker.jsx +86 -0
  30. package/src/tui/components/chat-panel.jsx +120 -0
  31. package/src/tui/components/footer.jsx +13 -0
  32. package/src/tui/components/header.jsx +45 -0
  33. package/src/tui/components/input-area.jsx +384 -0
  34. package/src/tui/components/messages/ask-message.jsx +13 -0
  35. package/src/tui/components/messages/assistant-message.jsx +165 -0
  36. package/src/tui/components/messages/channel-message.jsx +18 -0
  37. package/src/tui/components/messages/event-message.jsx +22 -0
  38. package/src/tui/components/messages/hive-status.jsx +34 -0
  39. package/src/tui/components/messages/skill-message.jsx +31 -0
  40. package/src/tui/components/messages/system-message.jsx +12 -0
  41. package/src/tui/components/messages/thinking.jsx +25 -0
  42. package/src/tui/components/messages/tool-group.jsx +62 -0
  43. package/src/tui/components/messages/tool-message.jsx +66 -0
  44. package/src/tui/components/messages/user-message.jsx +12 -0
  45. package/src/tui/components/model-picker.jsx +138 -0
  46. package/src/tui/components/multiline-input.jsx +72 -0
  47. package/src/tui/events-handler.js +730 -0
  48. package/src/tui/helpers.js +59 -0
  49. package/src/tui/hooks/use-command-handler.js +451 -0
  50. package/src/tui/index.jsx +55 -0
  51. package/src/tui/input-utils.js +26 -0
  52. package/src/tui/markdown-renderer.js +66 -0
  53. package/src/tui/mcp-wizard.js +136 -0
  54. package/src/tui/model-picker.js +174 -0
  55. package/src/tui/slash-commands.js +26 -0
  56. package/src/tui/store.js +12 -0
  57. package/src/tui/theme.js +17 -0
  58. package/src/ui.js +109 -0
  59. package/bin/moxxy.js +0 -2
  60. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  61. package/dist/chunk-2FZEA3NG.mjs +0 -457
  62. package/dist/chunk-3KDPLS22.mjs +0 -1131
  63. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  64. package/dist/chunk-6DZX6EAA.mjs +0 -37
  65. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  66. package/dist/chunk-C46NSEKG.mjs +0 -211
  67. package/dist/chunk-CAUXONEF.mjs +0 -1131
  68. package/dist/chunk-CPL5V56X.mjs +0 -1131
  69. package/dist/chunk-CTBVTTBG.mjs +0 -440
  70. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  71. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  72. package/dist/chunk-GSNMMI3H.mjs +0 -530
  73. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  74. package/dist/chunk-ITBO7BKI.mjs +0 -1243
  75. package/dist/chunk-J33O35WX.mjs +0 -532
  76. package/dist/chunk-N5JTPB6U.mjs +0 -820
  77. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  78. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  79. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  80. package/dist/chunk-QO2JONHP.mjs +0 -1131
  81. package/dist/chunk-RVAPILHA.mjs +0 -1242
  82. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  83. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  84. package/dist/chunk-SOFST2PV.mjs +0 -1242
  85. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  86. package/dist/chunk-TMZWETMH.mjs +0 -1242
  87. package/dist/chunk-TYD7NMMI.mjs +0 -581
  88. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  89. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  90. package/dist/chunk-UQZKODNW.mjs +0 -1124
  91. package/dist/chunk-USC6R2ON.mjs +0 -1242
  92. package/dist/chunk-W32EQCVC.mjs +0 -823
  93. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  94. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  95. package/dist/cli-2AIWTL6F.mjs +0 -8
  96. package/dist/cli-2QKJ5UUL.mjs +0 -8
  97. package/dist/cli-4RIS6DQX.mjs +0 -8
  98. package/dist/cli-5RH4VBBL.mjs +0 -7
  99. package/dist/cli-7MK4YGOP.mjs +0 -7
  100. package/dist/cli-B4KH6MZI.mjs +0 -8
  101. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  102. package/dist/cli-CVP26EL2.mjs +0 -8
  103. package/dist/cli-DDRVVNAV.mjs +0 -8
  104. package/dist/cli-E7U56QVQ.mjs +0 -8
  105. package/dist/cli-EQNRMLL3.mjs +0 -8
  106. package/dist/cli-F5RUHHH4.mjs +0 -8
  107. package/dist/cli-LX6FFSEF.mjs +0 -8
  108. package/dist/cli-LY74GWKR.mjs +0 -6
  109. package/dist/cli-MAT3ZJHI.mjs +0 -8
  110. package/dist/cli-NJXXTQYF.mjs +0 -8
  111. package/dist/cli-O4ZGFAZG.mjs +0 -8
  112. package/dist/cli-ORVLI3UQ.mjs +0 -8
  113. package/dist/cli-PV43ZVKA.mjs +0 -8
  114. package/dist/cli-REVD6ISM.mjs +0 -8
  115. package/dist/cli-TBX76KQX.mjs +0 -8
  116. package/dist/cli-THCGF7SQ.mjs +0 -8
  117. package/dist/cli-TLX5ENVM.mjs +0 -8
  118. package/dist/cli-TMNI5ZYE.mjs +0 -8
  119. package/dist/cli-TNJHCBQA.mjs +0 -6
  120. package/dist/cli-TUX22CZP.mjs +0 -8
  121. package/dist/cli-XJVH7EEP.mjs +0 -8
  122. package/dist/cli-XXOW4VXJ.mjs +0 -8
  123. package/dist/cli-XZ5RESNB.mjs +0 -6
  124. package/dist/cli-YCBYZ76Q.mjs +0 -8
  125. package/dist/cli-ZLMQCU7X.mjs +0 -8
  126. package/dist/dist-2VGKJRBH.mjs +0 -6820
  127. package/dist/dist-37BNX4QG.mjs +0 -7081
  128. package/dist/dist-7LTHRYKA.mjs +0 -11569
  129. package/dist/dist-7XJPQW5C.mjs +0 -6950
  130. package/dist/dist-AYMVOW7T.mjs +0 -7123
  131. package/dist/dist-BHUWCDRS.mjs +0 -7132
  132. package/dist/dist-FAXRJMEN.mjs +0 -6812
  133. package/dist/dist-HQGANM3P.mjs +0 -6976
  134. package/dist/dist-KATLOZQV.mjs +0 -7054
  135. package/dist/dist-KLSB6YHV.mjs +0 -6964
  136. package/dist/dist-LKIOZQ42.mjs +0 -17
  137. package/dist/dist-UYA4RJUH.mjs +0 -2792
  138. package/dist/dist-ZYHCBILM.mjs +0 -6993
  139. package/dist/index.d.mts +0 -23
  140. package/dist/index.d.ts +0 -23
  141. package/dist/index.js +0 -25531
  142. package/dist/index.mjs +0 -18
  143. package/dist/src-APP5P3UD.mjs +0 -1386
  144. package/dist/src-D5HMDDVE.mjs +0 -1324
  145. package/dist/src-EK3WD4AU.mjs +0 -1327
  146. package/dist/src-LSZFLMFN.mjs +0 -1400
  147. package/dist/src-T77DFTFP.mjs +0 -1407
  148. package/dist/src-WIOCZRAC.mjs +0 -1397
  149. 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
+ }