casabot 1.1.4 → 1.1.6
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/dist/tui/app.js +30 -92
- package/package.json +1 -1
- package/src/tui/app.tsx +59 -145
package/dist/tui/app.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
-
import { useState, useCallback,
|
|
3
|
-
import { render, Box, Text, useInput, useApp, useStdout } from "ink";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useMemo } from "react";
|
|
3
|
+
import { render, Box, Text, Static, useInput, useApp, useStdout } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import Spinner from "ink-spinner";
|
|
6
6
|
import Gradient from "ink-gradient";
|
|
7
7
|
import { marked } from "marked";
|
|
8
8
|
import { markedTerminal } from "marked-terminal";
|
|
9
9
|
import { runAgent } from "../agent/base.js";
|
|
10
|
-
marked.use(markedTerminal({
|
|
11
|
-
showSectionPrefix: false,
|
|
12
|
-
tab: 2,
|
|
13
|
-
}));
|
|
14
10
|
marked.use({ gfm: true });
|
|
15
11
|
function renderMarkdown(content) {
|
|
12
|
+
const width = Math.max((process.stdout.columns ?? 80) - 8, 40);
|
|
13
|
+
marked.use(markedTerminal({ showSectionPrefix: false, tab: 2, width }));
|
|
16
14
|
return marked.parse(content, { async: false }).trimEnd();
|
|
17
15
|
}
|
|
18
16
|
function truncateOutput(content, maxLines = 8) {
|
|
@@ -22,54 +20,22 @@ function truncateOutput(content, maxLines = 8) {
|
|
|
22
20
|
return (lines.slice(0, maxLines).join("\n") +
|
|
23
21
|
`\n … ${lines.length - maxLines} more lines`);
|
|
24
22
|
}
|
|
25
|
-
function
|
|
23
|
+
function HRule() {
|
|
26
24
|
const { stdout } = useStdout();
|
|
27
|
-
const
|
|
28
|
-
const scrollDownRef = useRef(onScrollDown);
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
scrollUpRef.current = onScrollUp;
|
|
31
|
-
scrollDownRef.current = onScrollDown;
|
|
32
|
-
});
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (!process.stdin.isTTY)
|
|
35
|
-
return;
|
|
36
|
-
const ENABLE_MOUSE = "\x1b[?1000h\x1b[?1006h";
|
|
37
|
-
const DISABLE_MOUSE = "\x1b[?1000l\x1b[?1006l";
|
|
38
|
-
stdout.write(ENABLE_MOUSE);
|
|
39
|
-
const sgrRegex = /\x1b\[<(\d+);\d+;\d+M/g;
|
|
40
|
-
const handleData = (data) => {
|
|
41
|
-
const str = data.toString("utf8");
|
|
42
|
-
let match;
|
|
43
|
-
while ((match = sgrRegex.exec(str)) !== null) {
|
|
44
|
-
const button = parseInt(match[1], 10);
|
|
45
|
-
if (button === 64)
|
|
46
|
-
scrollUpRef.current();
|
|
47
|
-
if (button === 65)
|
|
48
|
-
scrollDownRef.current();
|
|
49
|
-
}
|
|
50
|
-
sgrRegex.lastIndex = 0;
|
|
51
|
-
};
|
|
52
|
-
process.stdin.on("data", handleData);
|
|
53
|
-
return () => {
|
|
54
|
-
process.stdin.off("data", handleData);
|
|
55
|
-
stdout.write(DISABLE_MOUSE);
|
|
56
|
-
};
|
|
57
|
-
}, [stdout]);
|
|
58
|
-
}
|
|
59
|
-
function HRule({ width }) {
|
|
25
|
+
const width = stdout.columns ?? 80;
|
|
60
26
|
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(width - 4, 10)) }) }));
|
|
61
27
|
}
|
|
62
|
-
function
|
|
63
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
28
|
+
function HeaderBlock() {
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsx(Box, { paddingX: 2, children: _jsx(Gradient, { name: "vice", children: _jsx(Text, { bold: true, children: "✦ CasAbot" }) }) }), _jsx(Box, { paddingX: 2, children: _jsx(Text, { dimColor: true, children: "Cassiopeia A — Freely creates everything, like a supernova explosion." }) }), _jsx(HRule, {})] }));
|
|
64
30
|
}
|
|
65
31
|
function UserMessageView({ content }) {
|
|
66
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, children: [_jsx(Text, { color: "green", bold: true, children: "▶ You" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { children: content }) })] }));
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, children: [_jsx(Text, { color: "green", bold: true, children: "▶ You" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { wrap: "wrap", children: content }) })] }));
|
|
67
33
|
}
|
|
68
34
|
function AssistantMessageView({ content, }) {
|
|
69
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "✦ CasAbot" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { children: renderMarkdown(content) }) })] }));
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "✦ CasAbot" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(content) }) })] }));
|
|
70
36
|
}
|
|
71
37
|
function ToolCallsView({ message, }) {
|
|
72
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "✦ CasAbot" }), message.content ? (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { children: renderMarkdown(message.content) }) })) : null, _jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "⚡ Tool Calls" }), message.toolCalls?.map((tc, i) => {
|
|
38
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "✦ CasAbot" }), message.content ? (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(message.content) }) })) : null, _jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "⚡ Tool Calls" }), message.toolCalls?.map((tc, i) => {
|
|
73
39
|
let display = tc.arguments;
|
|
74
40
|
try {
|
|
75
41
|
const args = JSON.parse(tc.arguments);
|
|
@@ -79,11 +45,11 @@ function ToolCallsView({ message, }) {
|
|
|
79
45
|
catch {
|
|
80
46
|
/* keep raw */
|
|
81
47
|
}
|
|
82
|
-
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: tc.name }), _jsx(Text, { children: " → " }), _jsx(Text, { color: "white", children: display })] }, i));
|
|
48
|
+
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: tc.name }), _jsx(Text, { children: " → " }), _jsx(Text, { color: "white", wrap: "wrap", children: display })] }, i));
|
|
83
49
|
})] })] }));
|
|
84
50
|
}
|
|
85
51
|
function ToolResultView({ content, }) {
|
|
86
|
-
return (_jsxs(Box, { flexDirection: "column", marginLeft: 4, marginRight: 2, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "📋 Result" }), _jsx(Text, { dimColor: true, children: truncateOutput(content) })] }));
|
|
52
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 4, marginRight: 2, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "📋 Result" }), _jsx(Text, { dimColor: true, wrap: "wrap", children: truncateOutput(content) })] }));
|
|
87
53
|
}
|
|
88
54
|
function MessageView({ message, }) {
|
|
89
55
|
if (message.role === "user") {
|
|
@@ -101,44 +67,16 @@ function MessageView({ message, }) {
|
|
|
101
67
|
return _jsx(Text, { children: message.content });
|
|
102
68
|
}
|
|
103
69
|
function WelcomeHint() {
|
|
104
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
70
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Type a message below to get started." }), _jsx(Text, { dimColor: true, children: "CasAbot will orchestrate agents to help you." })] }));
|
|
105
71
|
}
|
|
106
72
|
function ProcessingIndicator() {
|
|
107
73
|
return (_jsxs(Box, { paddingX: 2, marginTop: 1, gap: 1, children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "yellow", children: "Thinking…" })] }));
|
|
108
74
|
}
|
|
109
|
-
// border(2) + header(3) + hrules(2) + input(3) + status(1) = 11
|
|
110
|
-
const CHROME_HEIGHT = 11;
|
|
111
75
|
function App({ provider, conversation, skills, }) {
|
|
112
76
|
const [messages, setMessages] = useState([]);
|
|
113
77
|
const [input, setInput] = useState("");
|
|
114
78
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
115
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
116
79
|
const { exit } = useApp();
|
|
117
|
-
const { stdout } = useStdout();
|
|
118
|
-
const [termSize, setTermSize] = useState({
|
|
119
|
-
columns: stdout.columns ?? 80,
|
|
120
|
-
rows: stdout.rows ?? 24,
|
|
121
|
-
});
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
const onResize = () => {
|
|
124
|
-
setTermSize({
|
|
125
|
-
columns: stdout.columns ?? 80,
|
|
126
|
-
rows: stdout.rows ?? 24,
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
stdout.on("resize", onResize);
|
|
130
|
-
return () => {
|
|
131
|
-
stdout.off("resize", onResize);
|
|
132
|
-
};
|
|
133
|
-
}, [stdout]);
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
setScrollOffset((prev) => (prev === 0 ? 0 : prev + 1));
|
|
136
|
-
}, [messages.length]);
|
|
137
|
-
const messagesHeight = Math.max(termSize.rows - CHROME_HEIGHT, 4);
|
|
138
|
-
const visibleMessages = useMemo(() => messages.slice(0, messages.length - scrollOffset), [messages, scrollOffset]);
|
|
139
|
-
const maxScrollOffset = useMemo(() => {
|
|
140
|
-
return Math.max(0, messages.length - 1);
|
|
141
|
-
}, [messages.length]);
|
|
142
80
|
const handleSubmit = useCallback(async (text) => {
|
|
143
81
|
const trimmed = text.trim();
|
|
144
82
|
if (!trimmed || isProcessing)
|
|
@@ -162,28 +100,28 @@ function App({ provider, conversation, skills, }) {
|
|
|
162
100
|
}
|
|
163
101
|
setIsProcessing(false);
|
|
164
102
|
}, [isProcessing, provider, conversation, skills]);
|
|
165
|
-
const scrollUp = useCallback(() => {
|
|
166
|
-
setScrollOffset((prev) => Math.min(prev + 1, maxScrollOffset));
|
|
167
|
-
}, [maxScrollOffset]);
|
|
168
|
-
const scrollDown = useCallback(() => {
|
|
169
|
-
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
170
|
-
}, []);
|
|
171
|
-
useMouseWheel(scrollUp, scrollDown);
|
|
172
103
|
useInput((ch, key) => {
|
|
173
104
|
if (key.ctrl && ch === "c") {
|
|
174
105
|
exit();
|
|
175
106
|
}
|
|
176
|
-
if (key.upArrow) {
|
|
177
|
-
scrollUp();
|
|
178
|
-
}
|
|
179
|
-
if (key.downArrow) {
|
|
180
|
-
scrollDown();
|
|
181
|
-
}
|
|
182
107
|
});
|
|
183
108
|
const userCount = messages.filter((m) => m.role === "user").length;
|
|
184
|
-
|
|
109
|
+
const items = useMemo(() => [
|
|
110
|
+
{ key: "header", type: "header" },
|
|
111
|
+
...messages.map((msg, i) => ({
|
|
112
|
+
key: `msg-${i}`,
|
|
113
|
+
type: "message",
|
|
114
|
+
message: msg,
|
|
115
|
+
})),
|
|
116
|
+
], [messages]);
|
|
117
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item) => {
|
|
118
|
+
if (item.type === "header") {
|
|
119
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(HeaderBlock, {}) }, item.key));
|
|
120
|
+
}
|
|
121
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(MessageView, { message: item.message }) }, item.key));
|
|
122
|
+
} }), messages.length === 0 && !isProcessing && _jsx(WelcomeHint, {}), isProcessing && _jsx(ProcessingIndicator, {}), _jsx(HRule, {}), _jsx(Box, { paddingX: 1, children: _jsxs(Box, { borderStyle: "round", borderColor: isProcessing ? "gray" : "cyan", paddingX: 1, width: "100%", children: [_jsx(Text, { color: "cyan", bold: true, children: "❯ " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: (val) => {
|
|
185
123
|
handleSubmit(val).catch(() => { });
|
|
186
|
-
}, placeholder: "Type your message\u2026", focus: !isProcessing, showCursor: true })] }) }), _jsxs(Box, { paddingX: 2, justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: "Ctrl+C exit
|
|
124
|
+
}, placeholder: "Type your message\u2026", focus: !isProcessing, showCursor: true })] }) }), _jsxs(Box, { paddingX: 2, justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: "Ctrl+C exit" }), _jsxs(Text, { dimColor: true, children: [userCount, " ", userCount === 1 ? "message" : "messages"] })] })] }));
|
|
187
125
|
}
|
|
188
126
|
export function startTUI(provider, conversation, skills) {
|
|
189
127
|
render(_jsx(App, { provider: provider, conversation: conversation, skills: skills }));
|
package/package.json
CHANGED
package/src/tui/app.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState, useCallback,
|
|
2
|
-
import { render, Box, Text, useInput, useApp, useStdout } from "ink";
|
|
1
|
+
import React, { useState, useCallback, useMemo } from "react";
|
|
2
|
+
import { render, Box, Text, Static, useInput, useApp, useStdout } from "ink";
|
|
3
3
|
import TextInput from "ink-text-input";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
5
|
import Gradient from "ink-gradient";
|
|
@@ -9,15 +9,11 @@ import type { ChatProvider } from "../providers/base.js";
|
|
|
9
9
|
import type { ConversationHistory, Message, Skill } from "../config/types.js";
|
|
10
10
|
import { runAgent } from "../agent/base.js";
|
|
11
11
|
|
|
12
|
-
marked.use(
|
|
13
|
-
markedTerminal({
|
|
14
|
-
showSectionPrefix: false,
|
|
15
|
-
tab: 2,
|
|
16
|
-
}),
|
|
17
|
-
);
|
|
18
12
|
marked.use({ gfm: true });
|
|
19
13
|
|
|
20
14
|
function renderMarkdown(content: string): string {
|
|
15
|
+
const width = Math.max((process.stdout.columns ?? 80) - 8, 40);
|
|
16
|
+
marked.use(markedTerminal({ showSectionPrefix: false, tab: 2, width }));
|
|
21
17
|
return (marked.parse(content, { async: false }) as string).trimEnd();
|
|
22
18
|
}
|
|
23
19
|
|
|
@@ -30,48 +26,9 @@ function truncateOutput(content: string, maxLines = 8): string {
|
|
|
30
26
|
);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
function
|
|
34
|
-
onScrollUp: () => void,
|
|
35
|
-
onScrollDown: () => void,
|
|
36
|
-
): void {
|
|
29
|
+
function HRule(): React.ReactElement {
|
|
37
30
|
const { stdout } = useStdout();
|
|
38
|
-
const
|
|
39
|
-
const scrollDownRef = useRef(onScrollDown);
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
scrollUpRef.current = onScrollUp;
|
|
43
|
-
scrollDownRef.current = onScrollDown;
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (!process.stdin.isTTY) return;
|
|
48
|
-
|
|
49
|
-
const ENABLE_MOUSE = "\x1b[?1000h\x1b[?1006h";
|
|
50
|
-
const DISABLE_MOUSE = "\x1b[?1000l\x1b[?1006l";
|
|
51
|
-
stdout.write(ENABLE_MOUSE);
|
|
52
|
-
|
|
53
|
-
const sgrRegex = /\x1b\[<(\d+);\d+;\d+M/g;
|
|
54
|
-
|
|
55
|
-
const handleData = (data: Buffer): void => {
|
|
56
|
-
const str = data.toString("utf8");
|
|
57
|
-
let match;
|
|
58
|
-
while ((match = sgrRegex.exec(str)) !== null) {
|
|
59
|
-
const button = parseInt(match[1], 10);
|
|
60
|
-
if (button === 64) scrollUpRef.current();
|
|
61
|
-
if (button === 65) scrollDownRef.current();
|
|
62
|
-
}
|
|
63
|
-
sgrRegex.lastIndex = 0;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
process.stdin.on("data", handleData);
|
|
67
|
-
return () => {
|
|
68
|
-
process.stdin.off("data", handleData);
|
|
69
|
-
stdout.write(DISABLE_MOUSE);
|
|
70
|
-
};
|
|
71
|
-
}, [stdout]);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function HRule({ width }: { width: number }): React.ReactElement {
|
|
31
|
+
const width = stdout.columns ?? 80;
|
|
75
32
|
return (
|
|
76
33
|
<Box paddingX={1}>
|
|
77
34
|
<Text dimColor>{"─".repeat(Math.max(width - 4, 10))}</Text>
|
|
@@ -79,15 +36,20 @@ function HRule({ width }: { width: number }): React.ReactElement {
|
|
|
79
36
|
);
|
|
80
37
|
}
|
|
81
38
|
|
|
82
|
-
function
|
|
39
|
+
function HeaderBlock(): React.ReactElement {
|
|
83
40
|
return (
|
|
84
|
-
<Box flexDirection="column"
|
|
85
|
-
<
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
41
|
+
<Box flexDirection="column" paddingTop={1}>
|
|
42
|
+
<Box paddingX={2}>
|
|
43
|
+
<Gradient name="vice">
|
|
44
|
+
<Text bold>{"✦ CasAbot"}</Text>
|
|
45
|
+
</Gradient>
|
|
46
|
+
</Box>
|
|
47
|
+
<Box paddingX={2}>
|
|
48
|
+
<Text dimColor>
|
|
49
|
+
{"Cassiopeia A — Freely creates everything, like a supernova explosion."}
|
|
50
|
+
</Text>
|
|
51
|
+
</Box>
|
|
52
|
+
<HRule />
|
|
91
53
|
</Box>
|
|
92
54
|
);
|
|
93
55
|
}
|
|
@@ -99,7 +61,7 @@ function UserMessageView({ content }: { content: string }): React.ReactElement {
|
|
|
99
61
|
{"▶ You"}
|
|
100
62
|
</Text>
|
|
101
63
|
<Box marginLeft={2}>
|
|
102
|
-
<Text>{content}</Text>
|
|
64
|
+
<Text wrap="wrap">{content}</Text>
|
|
103
65
|
</Box>
|
|
104
66
|
</Box>
|
|
105
67
|
);
|
|
@@ -116,7 +78,7 @@ function AssistantMessageView({
|
|
|
116
78
|
{"✦ CasAbot"}
|
|
117
79
|
</Text>
|
|
118
80
|
<Box marginLeft={2}>
|
|
119
|
-
<Text>{renderMarkdown(content)}</Text>
|
|
81
|
+
<Text wrap="wrap">{renderMarkdown(content)}</Text>
|
|
120
82
|
</Box>
|
|
121
83
|
</Box>
|
|
122
84
|
);
|
|
@@ -134,7 +96,7 @@ function ToolCallsView({
|
|
|
134
96
|
</Text>
|
|
135
97
|
{message.content ? (
|
|
136
98
|
<Box marginLeft={2}>
|
|
137
|
-
<Text>{renderMarkdown(message.content)}</Text>
|
|
99
|
+
<Text wrap="wrap">{renderMarkdown(message.content)}</Text>
|
|
138
100
|
</Box>
|
|
139
101
|
) : null}
|
|
140
102
|
<Box
|
|
@@ -160,7 +122,7 @@ function ToolCallsView({
|
|
|
160
122
|
<Box key={i}>
|
|
161
123
|
<Text dimColor>{tc.name}</Text>
|
|
162
124
|
<Text>{" → "}</Text>
|
|
163
|
-
<Text color="white">{display}</Text>
|
|
125
|
+
<Text color="white" wrap="wrap">{display}</Text>
|
|
164
126
|
</Box>
|
|
165
127
|
);
|
|
166
128
|
})}
|
|
@@ -186,7 +148,7 @@ function ToolResultView({
|
|
|
186
148
|
<Text dimColor bold>
|
|
187
149
|
{"📋 Result"}
|
|
188
150
|
</Text>
|
|
189
|
-
<Text dimColor>{truncateOutput(content)}</Text>
|
|
151
|
+
<Text dimColor wrap="wrap">{truncateOutput(content)}</Text>
|
|
190
152
|
</Box>
|
|
191
153
|
);
|
|
192
154
|
}
|
|
@@ -213,7 +175,7 @@ function MessageView({
|
|
|
213
175
|
|
|
214
176
|
function WelcomeHint(): React.ReactElement {
|
|
215
177
|
return (
|
|
216
|
-
<Box flexDirection="column"
|
|
178
|
+
<Box flexDirection="column" paddingX={2} marginTop={1} marginBottom={1}>
|
|
217
179
|
<Text dimColor>{"Type a message below to get started."}</Text>
|
|
218
180
|
<Text dimColor>{"CasAbot will orchestrate agents to help you."}</Text>
|
|
219
181
|
</Box>
|
|
@@ -231,15 +193,16 @@ function ProcessingIndicator(): React.ReactElement {
|
|
|
231
193
|
);
|
|
232
194
|
}
|
|
233
195
|
|
|
196
|
+
type DisplayItem =
|
|
197
|
+
| { key: string; type: "header" }
|
|
198
|
+
| { key: string; type: "message"; message: Message };
|
|
199
|
+
|
|
234
200
|
interface AppProps {
|
|
235
201
|
provider: ChatProvider;
|
|
236
202
|
conversation: ConversationHistory;
|
|
237
203
|
skills: Skill[];
|
|
238
204
|
}
|
|
239
205
|
|
|
240
|
-
// border(2) + header(3) + hrules(2) + input(3) + status(1) = 11
|
|
241
|
-
const CHROME_HEIGHT = 11;
|
|
242
|
-
|
|
243
206
|
function App({
|
|
244
207
|
provider,
|
|
245
208
|
conversation,
|
|
@@ -248,42 +211,7 @@ function App({
|
|
|
248
211
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
249
212
|
const [input, setInput] = useState("");
|
|
250
213
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
251
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
252
214
|
const { exit } = useApp();
|
|
253
|
-
const { stdout } = useStdout();
|
|
254
|
-
|
|
255
|
-
const [termSize, setTermSize] = useState({
|
|
256
|
-
columns: stdout.columns ?? 80,
|
|
257
|
-
rows: stdout.rows ?? 24,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
useEffect(() => {
|
|
261
|
-
const onResize = () => {
|
|
262
|
-
setTermSize({
|
|
263
|
-
columns: stdout.columns ?? 80,
|
|
264
|
-
rows: stdout.rows ?? 24,
|
|
265
|
-
});
|
|
266
|
-
};
|
|
267
|
-
stdout.on("resize", onResize);
|
|
268
|
-
return () => {
|
|
269
|
-
stdout.off("resize", onResize);
|
|
270
|
-
};
|
|
271
|
-
}, [stdout]);
|
|
272
|
-
|
|
273
|
-
useEffect(() => {
|
|
274
|
-
setScrollOffset((prev) => (prev === 0 ? 0 : prev + 1));
|
|
275
|
-
}, [messages.length]);
|
|
276
|
-
|
|
277
|
-
const messagesHeight = Math.max(termSize.rows - CHROME_HEIGHT, 4);
|
|
278
|
-
|
|
279
|
-
const visibleMessages = useMemo(
|
|
280
|
-
() => messages.slice(0, messages.length - scrollOffset),
|
|
281
|
-
[messages, scrollOffset],
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
const maxScrollOffset = useMemo(() => {
|
|
285
|
-
return Math.max(0, messages.length - 1);
|
|
286
|
-
}, [messages.length]);
|
|
287
215
|
|
|
288
216
|
const handleSubmit = useCallback(
|
|
289
217
|
async (text: string) => {
|
|
@@ -314,60 +242,46 @@ function App({
|
|
|
314
242
|
[isProcessing, provider, conversation, skills],
|
|
315
243
|
);
|
|
316
244
|
|
|
317
|
-
const scrollUp = useCallback(() => {
|
|
318
|
-
setScrollOffset((prev) => Math.min(prev + 1, maxScrollOffset));
|
|
319
|
-
}, [maxScrollOffset]);
|
|
320
|
-
|
|
321
|
-
const scrollDown = useCallback(() => {
|
|
322
|
-
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
323
|
-
}, []);
|
|
324
|
-
|
|
325
|
-
useMouseWheel(scrollUp, scrollDown);
|
|
326
|
-
|
|
327
245
|
useInput((ch, key) => {
|
|
328
246
|
if (key.ctrl && ch === "c") {
|
|
329
247
|
exit();
|
|
330
248
|
}
|
|
331
|
-
if (key.upArrow) {
|
|
332
|
-
scrollUp();
|
|
333
|
-
}
|
|
334
|
-
if (key.downArrow) {
|
|
335
|
-
scrollDown();
|
|
336
|
-
}
|
|
337
249
|
});
|
|
338
250
|
|
|
339
251
|
const userCount = messages.filter((m) => m.role === "user").length;
|
|
340
252
|
|
|
253
|
+
const items = useMemo((): DisplayItem[] => [
|
|
254
|
+
{ key: "header", type: "header" },
|
|
255
|
+
...messages.map((msg, i): DisplayItem => ({
|
|
256
|
+
key: `msg-${i}`,
|
|
257
|
+
type: "message",
|
|
258
|
+
message: msg,
|
|
259
|
+
})),
|
|
260
|
+
], [messages]);
|
|
261
|
+
|
|
341
262
|
return (
|
|
342
|
-
<Box
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
263
|
+
<Box flexDirection="column">
|
|
264
|
+
<Static items={items}>
|
|
265
|
+
{(item) => {
|
|
266
|
+
if (item.type === "header") {
|
|
267
|
+
return (
|
|
268
|
+
<Box key={item.key} flexDirection="column">
|
|
269
|
+
<HeaderBlock />
|
|
270
|
+
</Box>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return (
|
|
274
|
+
<Box key={item.key} flexDirection="column">
|
|
275
|
+
<MessageView message={item.message} />
|
|
276
|
+
</Box>
|
|
277
|
+
);
|
|
278
|
+
}}
|
|
279
|
+
</Static>
|
|
351
280
|
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
height={messagesHeight}
|
|
355
|
-
overflowY="hidden"
|
|
356
|
-
justifyContent="flex-end"
|
|
357
|
-
>
|
|
358
|
-
{messages.length === 0 && !isProcessing ? (
|
|
359
|
-
<WelcomeHint />
|
|
360
|
-
) : (
|
|
361
|
-
<>
|
|
362
|
-
{visibleMessages.map((msg, i) => (
|
|
363
|
-
<MessageView key={i} message={msg} />
|
|
364
|
-
))}
|
|
365
|
-
{isProcessing && <ProcessingIndicator />}
|
|
366
|
-
</>
|
|
367
|
-
)}
|
|
368
|
-
</Box>
|
|
281
|
+
{messages.length === 0 && !isProcessing && <WelcomeHint />}
|
|
282
|
+
{isProcessing && <ProcessingIndicator />}
|
|
369
283
|
|
|
370
|
-
<HRule
|
|
284
|
+
<HRule />
|
|
371
285
|
|
|
372
286
|
<Box paddingX={1}>
|
|
373
287
|
<Box
|
|
@@ -393,7 +307,7 @@ function App({
|
|
|
393
307
|
</Box>
|
|
394
308
|
|
|
395
309
|
<Box paddingX={2} justifyContent="space-between">
|
|
396
|
-
<Text dimColor>{"Ctrl+C exit
|
|
310
|
+
<Text dimColor>{"Ctrl+C exit"}</Text>
|
|
397
311
|
<Text dimColor>
|
|
398
312
|
{userCount} {userCount === 1 ? "message" : "messages"}
|
|
399
313
|
</Text>
|