casabot 1.1.5 → 1.1.7
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 +50 -106
- package/package.json +1 -1
- package/src/tui/app.tsx +98 -166
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,21 @@ 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
|
|
26
|
-
|
|
27
|
-
const scrollUpRef = useRef(onScrollUp);
|
|
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 }) {
|
|
60
|
-
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(width - 4, 10)) }) }));
|
|
23
|
+
function HRule({ columns }) {
|
|
24
|
+
return (_jsx(Box, { paddingX: 1, width: columns, children: _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(columns - 4, 1)) }) }));
|
|
61
25
|
}
|
|
62
|
-
function
|
|
63
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
26
|
+
function HeaderBlock({ columns }) {
|
|
27
|
+
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, width: columns, children: [_jsx(Box, { paddingX: 2, children: _jsx(Gradient, { name: "vice", children: _jsx(Text, { bold: true, children: "✦ CasAbot" }) }) }), _jsx(Box, { paddingX: 2, children: _jsx(Text, { wrap: "wrap", dimColor: true, children: "Cassiopeia A — Freely creates everything, like a supernova explosion." }) }), _jsx(HRule, { columns: columns })] }));
|
|
64
28
|
}
|
|
65
|
-
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 }) })] }));
|
|
29
|
+
function UserMessageView({ content, columns }) {
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, width: columns, children: [_jsx(Text, { color: "green", bold: true, children: "▶ You" }), _jsx(Box, { marginLeft: 2, width: Math.max(columns - 6, 10), children: _jsx(Text, { wrap: "wrap", children: content }) })] }));
|
|
67
31
|
}
|
|
68
|
-
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) }) })] }));
|
|
32
|
+
function AssistantMessageView({ content, columns, }) {
|
|
33
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, width: columns, children: [_jsx(Text, { color: "cyan", bold: true, children: "✦ CasAbot" }), _jsx(Box, { marginLeft: 2, width: Math.max(columns - 6, 10), children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(content) }) })] }));
|
|
70
34
|
}
|
|
71
|
-
function ToolCallsView({ message, }) {
|
|
72
|
-
|
|
35
|
+
function ToolCallsView({ message, columns, }) {
|
|
36
|
+
const boxWidth = Math.max(columns - 6, 10);
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, width: columns, children: [_jsx(Text, { color: "cyan", bold: true, children: "✦ CasAbot" }), message.content ? (_jsx(Box, { marginLeft: 2, width: boxWidth, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(message.content) }) })) : null, _jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, width: boxWidth, overflow: "hidden", children: [_jsx(Text, { color: "yellow", bold: true, children: "⚡ Tool Calls" }), message.toolCalls?.map((tc, i) => {
|
|
73
38
|
let display = tc.arguments;
|
|
74
39
|
try {
|
|
75
40
|
const args = JSON.parse(tc.arguments);
|
|
@@ -79,66 +44,45 @@ function ToolCallsView({ message, }) {
|
|
|
79
44
|
catch {
|
|
80
45
|
/* keep raw */
|
|
81
46
|
}
|
|
82
|
-
|
|
47
|
+
const maxArgLen = Math.max(boxWidth - tc.name.length - 8, 20);
|
|
48
|
+
if (display.length > maxArgLen) {
|
|
49
|
+
display = display.slice(0, maxArgLen - 1) + "…";
|
|
50
|
+
}
|
|
51
|
+
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: tc.name }), _jsx(Text, { children: " → " }), _jsx(Text, { color: "white", wrap: "wrap", children: display })] }, i));
|
|
83
52
|
})] })] }));
|
|
84
53
|
}
|
|
85
|
-
function ToolResultView({ content, }) {
|
|
86
|
-
|
|
54
|
+
function ToolResultView({ content, columns, }) {
|
|
55
|
+
const boxWidth = Math.max(columns - 6, 10);
|
|
56
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 4, marginRight: 2, borderStyle: "round", borderColor: "gray", paddingX: 1, width: boxWidth, overflow: "hidden", children: [_jsx(Text, { dimColor: true, bold: true, children: "📋 Result" }), _jsx(Text, { dimColor: true, wrap: "wrap", children: truncateOutput(content) })] }));
|
|
87
57
|
}
|
|
88
|
-
function MessageView({ message, }) {
|
|
58
|
+
function MessageView({ message, columns, }) {
|
|
89
59
|
if (message.role === "user") {
|
|
90
|
-
return _jsx(UserMessageView, { content: message.content });
|
|
60
|
+
return _jsx(UserMessageView, { content: message.content, columns: columns });
|
|
91
61
|
}
|
|
92
62
|
if (message.role === "tool") {
|
|
93
|
-
return _jsx(ToolResultView, { content: message.content });
|
|
63
|
+
return _jsx(ToolResultView, { content: message.content, columns: columns });
|
|
94
64
|
}
|
|
95
65
|
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
96
|
-
return _jsx(ToolCallsView, { message: message });
|
|
66
|
+
return _jsx(ToolCallsView, { message: message, columns: columns });
|
|
97
67
|
}
|
|
98
68
|
if (message.role === "assistant") {
|
|
99
|
-
return _jsx(AssistantMessageView, { content: message.content });
|
|
69
|
+
return _jsx(AssistantMessageView, { content: message.content, columns: columns });
|
|
100
70
|
}
|
|
101
|
-
return _jsx(Text, { children: message.content });
|
|
71
|
+
return _jsx(Text, { wrap: "wrap", children: message.content });
|
|
102
72
|
}
|
|
103
|
-
function WelcomeHint() {
|
|
104
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
73
|
+
function WelcomeHint({ columns }) {
|
|
74
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, marginBottom: 1, width: columns, 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
75
|
}
|
|
106
|
-
function ProcessingIndicator() {
|
|
107
|
-
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…" })] }));
|
|
76
|
+
function ProcessingIndicator({ columns }) {
|
|
77
|
+
return (_jsxs(Box, { paddingX: 2, marginTop: 1, gap: 1, width: columns, children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "yellow", children: "Thinking…" })] }));
|
|
108
78
|
}
|
|
109
|
-
// border(2) + header(3) + hrules(2) + input(3) + status(1) = 11
|
|
110
|
-
const CHROME_HEIGHT = 11;
|
|
111
79
|
function App({ provider, conversation, skills, }) {
|
|
112
80
|
const [messages, setMessages] = useState([]);
|
|
113
81
|
const [input, setInput] = useState("");
|
|
114
82
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
115
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
116
83
|
const { exit } = useApp();
|
|
117
84
|
const { stdout } = useStdout();
|
|
118
|
-
const
|
|
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]);
|
|
85
|
+
const columns = stdout.columns ?? 80;
|
|
142
86
|
const handleSubmit = useCallback(async (text) => {
|
|
143
87
|
const trimmed = text.trim();
|
|
144
88
|
if (!trimmed || isProcessing)
|
|
@@ -162,28 +106,28 @@ function App({ provider, conversation, skills, }) {
|
|
|
162
106
|
}
|
|
163
107
|
setIsProcessing(false);
|
|
164
108
|
}, [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
109
|
useInput((ch, key) => {
|
|
173
110
|
if (key.ctrl && ch === "c") {
|
|
174
111
|
exit();
|
|
175
112
|
}
|
|
176
|
-
if (key.upArrow) {
|
|
177
|
-
scrollUp();
|
|
178
|
-
}
|
|
179
|
-
if (key.downArrow) {
|
|
180
|
-
scrollDown();
|
|
181
|
-
}
|
|
182
113
|
});
|
|
183
114
|
const userCount = messages.filter((m) => m.role === "user").length;
|
|
184
|
-
|
|
115
|
+
const items = useMemo(() => [
|
|
116
|
+
{ key: "header", type: "header" },
|
|
117
|
+
...messages.map((msg, i) => ({
|
|
118
|
+
key: `msg-${i}`,
|
|
119
|
+
type: "message",
|
|
120
|
+
message: msg,
|
|
121
|
+
})),
|
|
122
|
+
], [messages]);
|
|
123
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: items, children: (item) => {
|
|
124
|
+
if (item.type === "header") {
|
|
125
|
+
return (_jsx(Box, { flexDirection: "column", width: columns, children: _jsx(HeaderBlock, { columns: columns }) }, item.key));
|
|
126
|
+
}
|
|
127
|
+
return (_jsx(Box, { flexDirection: "column", width: columns, children: _jsx(MessageView, { message: item.message, columns: columns }) }, item.key));
|
|
128
|
+
} }), messages.length === 0 && !isProcessing && _jsx(WelcomeHint, { columns: columns }), isProcessing && _jsx(ProcessingIndicator, { columns: columns }), _jsx(HRule, { columns: columns }), _jsx(Box, { paddingX: 1, width: columns, children: _jsxs(Box, { borderStyle: "round", borderColor: isProcessing ? "gray" : "cyan", paddingX: 1, width: Math.max(columns - 2, 10), overflow: "hidden", children: [_jsx(Text, { color: "cyan", bold: true, children: "❯ " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: (val) => {
|
|
185
129
|
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
|
|
130
|
+
}, placeholder: "Type your message\u2026", focus: !isProcessing, showCursor: true })] }) }), _jsxs(Box, { paddingX: 2, width: columns, justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: "Ctrl+C exit" }), _jsxs(Text, { dimColor: true, children: [userCount, " ", userCount === 1 ? "message" : "messages"] })] })] }));
|
|
187
131
|
}
|
|
188
132
|
export function startTUI(provider, conversation, skills) {
|
|
189
133
|
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,76 +26,40 @@ function truncateOutput(content: string, maxLines = 8): string {
|
|
|
30
26
|
);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
function
|
|
34
|
-
onScrollUp: () => void,
|
|
35
|
-
onScrollDown: () => void,
|
|
36
|
-
): void {
|
|
37
|
-
const { stdout } = useStdout();
|
|
38
|
-
const scrollUpRef = useRef(onScrollUp);
|
|
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 {
|
|
29
|
+
function HRule({ columns }: { columns: number }): React.ReactElement {
|
|
75
30
|
return (
|
|
76
|
-
<Box paddingX={1}>
|
|
77
|
-
<Text dimColor>{"─".repeat(Math.max(
|
|
31
|
+
<Box paddingX={1} width={columns}>
|
|
32
|
+
<Text dimColor>{"─".repeat(Math.max(columns - 4, 1))}</Text>
|
|
78
33
|
</Box>
|
|
79
34
|
);
|
|
80
35
|
}
|
|
81
36
|
|
|
82
|
-
function
|
|
37
|
+
function HeaderBlock({ columns }: { columns: number }): React.ReactElement {
|
|
83
38
|
return (
|
|
84
|
-
<Box flexDirection="column"
|
|
85
|
-
<
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
39
|
+
<Box flexDirection="column" paddingTop={1} width={columns}>
|
|
40
|
+
<Box paddingX={2}>
|
|
41
|
+
<Gradient name="vice">
|
|
42
|
+
<Text bold>{"✦ CasAbot"}</Text>
|
|
43
|
+
</Gradient>
|
|
44
|
+
</Box>
|
|
45
|
+
<Box paddingX={2}>
|
|
46
|
+
<Text wrap="wrap" dimColor>
|
|
47
|
+
{"Cassiopeia A — Freely creates everything, like a supernova explosion."}
|
|
48
|
+
</Text>
|
|
49
|
+
</Box>
|
|
50
|
+
<HRule columns={columns} />
|
|
91
51
|
</Box>
|
|
92
52
|
);
|
|
93
53
|
}
|
|
94
54
|
|
|
95
|
-
function UserMessageView({ content }: { content: string }): React.ReactElement {
|
|
55
|
+
function UserMessageView({ content, columns }: { content: string; columns: number }): React.ReactElement {
|
|
96
56
|
return (
|
|
97
|
-
<Box flexDirection="column" paddingX={2} marginTop={1}>
|
|
57
|
+
<Box flexDirection="column" paddingX={2} marginTop={1} width={columns}>
|
|
98
58
|
<Text color="green" bold>
|
|
99
59
|
{"▶ You"}
|
|
100
60
|
</Text>
|
|
101
|
-
<Box marginLeft={2}>
|
|
102
|
-
<Text>{content}</Text>
|
|
61
|
+
<Box marginLeft={2} width={Math.max(columns - 6, 10)}>
|
|
62
|
+
<Text wrap="wrap">{content}</Text>
|
|
103
63
|
</Box>
|
|
104
64
|
</Box>
|
|
105
65
|
);
|
|
@@ -107,16 +67,18 @@ function UserMessageView({ content }: { content: string }): React.ReactElement {
|
|
|
107
67
|
|
|
108
68
|
function AssistantMessageView({
|
|
109
69
|
content,
|
|
70
|
+
columns,
|
|
110
71
|
}: {
|
|
111
72
|
content: string;
|
|
73
|
+
columns: number;
|
|
112
74
|
}): React.ReactElement {
|
|
113
75
|
return (
|
|
114
|
-
<Box flexDirection="column" paddingX={2} marginTop={1}>
|
|
76
|
+
<Box flexDirection="column" paddingX={2} marginTop={1} width={columns}>
|
|
115
77
|
<Text color="cyan" bold>
|
|
116
78
|
{"✦ CasAbot"}
|
|
117
79
|
</Text>
|
|
118
|
-
<Box marginLeft={2}>
|
|
119
|
-
<Text>{renderMarkdown(content)}</Text>
|
|
80
|
+
<Box marginLeft={2} width={Math.max(columns - 6, 10)}>
|
|
81
|
+
<Text wrap="wrap">{renderMarkdown(content)}</Text>
|
|
120
82
|
</Box>
|
|
121
83
|
</Box>
|
|
122
84
|
);
|
|
@@ -124,17 +86,20 @@ function AssistantMessageView({
|
|
|
124
86
|
|
|
125
87
|
function ToolCallsView({
|
|
126
88
|
message,
|
|
89
|
+
columns,
|
|
127
90
|
}: {
|
|
128
91
|
message: Message;
|
|
92
|
+
columns: number;
|
|
129
93
|
}): React.ReactElement {
|
|
94
|
+
const boxWidth = Math.max(columns - 6, 10);
|
|
130
95
|
return (
|
|
131
|
-
<Box flexDirection="column" paddingX={2} marginTop={1}>
|
|
96
|
+
<Box flexDirection="column" paddingX={2} marginTop={1} width={columns}>
|
|
132
97
|
<Text color="cyan" bold>
|
|
133
98
|
{"✦ CasAbot"}
|
|
134
99
|
</Text>
|
|
135
100
|
{message.content ? (
|
|
136
|
-
<Box marginLeft={2}>
|
|
137
|
-
<Text>{renderMarkdown(message.content)}</Text>
|
|
101
|
+
<Box marginLeft={2} width={boxWidth}>
|
|
102
|
+
<Text wrap="wrap">{renderMarkdown(message.content)}</Text>
|
|
138
103
|
</Box>
|
|
139
104
|
) : null}
|
|
140
105
|
<Box
|
|
@@ -144,6 +109,8 @@ function ToolCallsView({
|
|
|
144
109
|
borderStyle="round"
|
|
145
110
|
borderColor="yellow"
|
|
146
111
|
paddingX={1}
|
|
112
|
+
width={boxWidth}
|
|
113
|
+
overflow="hidden"
|
|
147
114
|
>
|
|
148
115
|
<Text color="yellow" bold>
|
|
149
116
|
{"⚡ Tool Calls"}
|
|
@@ -156,11 +123,15 @@ function ToolCallsView({
|
|
|
156
123
|
} catch {
|
|
157
124
|
/* keep raw */
|
|
158
125
|
}
|
|
126
|
+
const maxArgLen = Math.max(boxWidth - tc.name.length - 8, 20);
|
|
127
|
+
if (display.length > maxArgLen) {
|
|
128
|
+
display = display.slice(0, maxArgLen - 1) + "…";
|
|
129
|
+
}
|
|
159
130
|
return (
|
|
160
131
|
<Box key={i}>
|
|
161
132
|
<Text dimColor>{tc.name}</Text>
|
|
162
133
|
<Text>{" → "}</Text>
|
|
163
|
-
<Text color="white">{display}</Text>
|
|
134
|
+
<Text color="white" wrap="wrap">{display}</Text>
|
|
164
135
|
</Box>
|
|
165
136
|
);
|
|
166
137
|
})}
|
|
@@ -171,9 +142,12 @@ function ToolCallsView({
|
|
|
171
142
|
|
|
172
143
|
function ToolResultView({
|
|
173
144
|
content,
|
|
145
|
+
columns,
|
|
174
146
|
}: {
|
|
175
147
|
content: string;
|
|
148
|
+
columns: number;
|
|
176
149
|
}): React.ReactElement {
|
|
150
|
+
const boxWidth = Math.max(columns - 6, 10);
|
|
177
151
|
return (
|
|
178
152
|
<Box
|
|
179
153
|
flexDirection="column"
|
|
@@ -182,47 +156,51 @@ function ToolResultView({
|
|
|
182
156
|
borderStyle="round"
|
|
183
157
|
borderColor="gray"
|
|
184
158
|
paddingX={1}
|
|
159
|
+
width={boxWidth}
|
|
160
|
+
overflow="hidden"
|
|
185
161
|
>
|
|
186
162
|
<Text dimColor bold>
|
|
187
163
|
{"📋 Result"}
|
|
188
164
|
</Text>
|
|
189
|
-
<Text dimColor>{truncateOutput(content)}</Text>
|
|
165
|
+
<Text dimColor wrap="wrap">{truncateOutput(content)}</Text>
|
|
190
166
|
</Box>
|
|
191
167
|
);
|
|
192
168
|
}
|
|
193
169
|
|
|
194
170
|
function MessageView({
|
|
195
171
|
message,
|
|
172
|
+
columns,
|
|
196
173
|
}: {
|
|
197
174
|
message: Message;
|
|
175
|
+
columns: number;
|
|
198
176
|
}): React.ReactElement {
|
|
199
177
|
if (message.role === "user") {
|
|
200
|
-
return <UserMessageView content={message.content} />;
|
|
178
|
+
return <UserMessageView content={message.content} columns={columns} />;
|
|
201
179
|
}
|
|
202
180
|
if (message.role === "tool") {
|
|
203
|
-
return <ToolResultView content={message.content} />;
|
|
181
|
+
return <ToolResultView content={message.content} columns={columns} />;
|
|
204
182
|
}
|
|
205
183
|
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
206
|
-
return <ToolCallsView message={message} />;
|
|
184
|
+
return <ToolCallsView message={message} columns={columns} />;
|
|
207
185
|
}
|
|
208
186
|
if (message.role === "assistant") {
|
|
209
|
-
return <AssistantMessageView content={message.content} />;
|
|
187
|
+
return <AssistantMessageView content={message.content} columns={columns} />;
|
|
210
188
|
}
|
|
211
|
-
return <Text>{message.content}</Text>;
|
|
189
|
+
return <Text wrap="wrap">{message.content}</Text>;
|
|
212
190
|
}
|
|
213
191
|
|
|
214
|
-
function WelcomeHint(): React.ReactElement {
|
|
192
|
+
function WelcomeHint({ columns }: { columns: number }): React.ReactElement {
|
|
215
193
|
return (
|
|
216
|
-
<Box flexDirection="column"
|
|
194
|
+
<Box flexDirection="column" paddingX={2} marginTop={1} marginBottom={1} width={columns}>
|
|
217
195
|
<Text dimColor>{"Type a message below to get started."}</Text>
|
|
218
196
|
<Text dimColor>{"CasAbot will orchestrate agents to help you."}</Text>
|
|
219
197
|
</Box>
|
|
220
198
|
);
|
|
221
199
|
}
|
|
222
200
|
|
|
223
|
-
function ProcessingIndicator(): React.ReactElement {
|
|
201
|
+
function ProcessingIndicator({ columns }: { columns: number }): React.ReactElement {
|
|
224
202
|
return (
|
|
225
|
-
<Box paddingX={2} marginTop={1} gap={1}>
|
|
203
|
+
<Box paddingX={2} marginTop={1} gap={1} width={columns}>
|
|
226
204
|
<Text color="yellow">
|
|
227
205
|
<Spinner type="dots" />
|
|
228
206
|
</Text>
|
|
@@ -231,15 +209,16 @@ function ProcessingIndicator(): React.ReactElement {
|
|
|
231
209
|
);
|
|
232
210
|
}
|
|
233
211
|
|
|
212
|
+
type DisplayItem =
|
|
213
|
+
| { key: string; type: "header" }
|
|
214
|
+
| { key: string; type: "message"; message: Message };
|
|
215
|
+
|
|
234
216
|
interface AppProps {
|
|
235
217
|
provider: ChatProvider;
|
|
236
218
|
conversation: ConversationHistory;
|
|
237
219
|
skills: Skill[];
|
|
238
220
|
}
|
|
239
221
|
|
|
240
|
-
// border(2) + header(3) + hrules(2) + input(3) + status(1) = 11
|
|
241
|
-
const CHROME_HEIGHT = 11;
|
|
242
|
-
|
|
243
222
|
function App({
|
|
244
223
|
provider,
|
|
245
224
|
conversation,
|
|
@@ -248,42 +227,9 @@ function App({
|
|
|
248
227
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
249
228
|
const [input, setInput] = useState("");
|
|
250
229
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
251
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
252
230
|
const { exit } = useApp();
|
|
253
231
|
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]);
|
|
232
|
+
const columns = stdout.columns ?? 80;
|
|
287
233
|
|
|
288
234
|
const handleSubmit = useCallback(
|
|
289
235
|
async (text: string) => {
|
|
@@ -314,68 +260,54 @@ function App({
|
|
|
314
260
|
[isProcessing, provider, conversation, skills],
|
|
315
261
|
);
|
|
316
262
|
|
|
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
263
|
useInput((ch, key) => {
|
|
328
264
|
if (key.ctrl && ch === "c") {
|
|
329
265
|
exit();
|
|
330
266
|
}
|
|
331
|
-
if (key.upArrow) {
|
|
332
|
-
scrollUp();
|
|
333
|
-
}
|
|
334
|
-
if (key.downArrow) {
|
|
335
|
-
scrollDown();
|
|
336
|
-
}
|
|
337
267
|
});
|
|
338
268
|
|
|
339
269
|
const userCount = messages.filter((m) => m.role === "user").length;
|
|
340
270
|
|
|
271
|
+
const items = useMemo((): DisplayItem[] => [
|
|
272
|
+
{ key: "header", type: "header" },
|
|
273
|
+
...messages.map((msg, i): DisplayItem => ({
|
|
274
|
+
key: `msg-${i}`,
|
|
275
|
+
type: "message",
|
|
276
|
+
message: msg,
|
|
277
|
+
})),
|
|
278
|
+
], [messages]);
|
|
279
|
+
|
|
341
280
|
return (
|
|
342
|
-
<Box
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
281
|
+
<Box flexDirection="column" width={columns}>
|
|
282
|
+
<Static items={items}>
|
|
283
|
+
{(item) => {
|
|
284
|
+
if (item.type === "header") {
|
|
285
|
+
return (
|
|
286
|
+
<Box key={item.key} flexDirection="column" width={columns}>
|
|
287
|
+
<HeaderBlock columns={columns} />
|
|
288
|
+
</Box>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return (
|
|
292
|
+
<Box key={item.key} flexDirection="column" width={columns}>
|
|
293
|
+
<MessageView message={item.message} columns={columns} />
|
|
294
|
+
</Box>
|
|
295
|
+
);
|
|
296
|
+
}}
|
|
297
|
+
</Static>
|
|
351
298
|
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
height={messagesHeight}
|
|
355
|
-
overflowY="hidden"
|
|
356
|
-
justifyContent="flex-end"
|
|
357
|
-
>
|
|
358
|
-
{messages.length === 0 && !isProcessing ? (
|
|
359
|
-
<WelcomeHint />
|
|
360
|
-
) : (
|
|
361
|
-
<>
|
|
362
|
-
<Box minHeight={messagesHeight} />
|
|
363
|
-
{visibleMessages.map((msg, i) => (
|
|
364
|
-
<MessageView key={i} message={msg} />
|
|
365
|
-
))}
|
|
366
|
-
{isProcessing && <ProcessingIndicator />}
|
|
367
|
-
</>
|
|
368
|
-
)}
|
|
369
|
-
</Box>
|
|
299
|
+
{messages.length === 0 && !isProcessing && <WelcomeHint columns={columns} />}
|
|
300
|
+
{isProcessing && <ProcessingIndicator columns={columns} />}
|
|
370
301
|
|
|
371
|
-
<HRule
|
|
302
|
+
<HRule columns={columns} />
|
|
372
303
|
|
|
373
|
-
<Box paddingX={1}>
|
|
304
|
+
<Box paddingX={1} width={columns}>
|
|
374
305
|
<Box
|
|
375
306
|
borderStyle="round"
|
|
376
307
|
borderColor={isProcessing ? "gray" : "cyan"}
|
|
377
308
|
paddingX={1}
|
|
378
|
-
width=
|
|
309
|
+
width={Math.max(columns - 2, 10)}
|
|
310
|
+
overflow="hidden"
|
|
379
311
|
>
|
|
380
312
|
<Text color="cyan" bold>
|
|
381
313
|
{"❯ "}
|
|
@@ -393,8 +325,8 @@ function App({
|
|
|
393
325
|
</Box>
|
|
394
326
|
</Box>
|
|
395
327
|
|
|
396
|
-
<Box paddingX={2} justifyContent="space-between">
|
|
397
|
-
<Text dimColor>{"Ctrl+C exit
|
|
328
|
+
<Box paddingX={2} width={columns} justifyContent="space-between">
|
|
329
|
+
<Text dimColor>{"Ctrl+C exit"}</Text>
|
|
398
330
|
<Text dimColor>
|
|
399
331
|
{userCount} {userCount === 1 ? "message" : "messages"}
|
|
400
332
|
</Text>
|