casabot 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tui/app.js +88 -44
- package/package.json +4 -1
- package/src/tui/app.tsx +272 -98
- package/src/tui/marked-terminal.d.ts +5 -0
package/dist/tui/app.js
CHANGED
|
@@ -1,53 +1,108 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback } from "react";
|
|
3
|
-
import { render, Box, Text, useInput, useApp } from "ink";
|
|
2
|
+
import { useState, useCallback, useEffect } from "react";
|
|
3
|
+
import { render, Box, Text, useInput, useApp, useStdout } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import Spinner from "ink-spinner";
|
|
6
|
+
import Gradient from "ink-gradient";
|
|
7
|
+
import { marked } from "marked";
|
|
8
|
+
import { markedTerminal } from "marked-terminal";
|
|
4
9
|
import { runAgent } from "../agent/base.js";
|
|
5
|
-
|
|
10
|
+
marked.use(markedTerminal());
|
|
11
|
+
function renderMarkdown(content) {
|
|
12
|
+
const result = marked.parse(content);
|
|
13
|
+
if (typeof result === "string")
|
|
14
|
+
return result.trimEnd();
|
|
15
|
+
return content;
|
|
16
|
+
}
|
|
17
|
+
function truncateOutput(content, maxLines = 8) {
|
|
6
18
|
const lines = content.split("\n");
|
|
7
19
|
if (lines.length <= maxLines)
|
|
8
20
|
return content;
|
|
9
|
-
return lines.slice(0, maxLines).join("\n") +
|
|
21
|
+
return (lines.slice(0, maxLines).join("\n") +
|
|
22
|
+
`\n … ${lines.length - maxLines} more lines`);
|
|
23
|
+
}
|
|
24
|
+
function HRule({ width }) {
|
|
25
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(width - 4, 10)) }) }));
|
|
26
|
+
}
|
|
27
|
+
function Header() {
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingTop: 1, children: [_jsx(Gradient, { name: "vice", children: _jsx(Text, { bold: true, children: "✦ CasAbot" }) }), _jsx(Text, { dimColor: true, children: "Cassiopeia A — Freely creates everything, like a supernova explosion." })] }));
|
|
29
|
+
}
|
|
30
|
+
function UserMessageView({ content }) {
|
|
31
|
+
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
|
+
}
|
|
33
|
+
function AssistantMessageView({ content, }) {
|
|
34
|
+
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) }) })] }));
|
|
10
35
|
}
|
|
11
|
-
function
|
|
36
|
+
function ToolCallsView({ message, }) {
|
|
37
|
+
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
|
+
let display = tc.arguments;
|
|
39
|
+
try {
|
|
40
|
+
const args = JSON.parse(tc.arguments);
|
|
41
|
+
if (typeof args.command === "string")
|
|
42
|
+
display = args.command;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* keep raw */
|
|
46
|
+
}
|
|
47
|
+
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: tc.name }), _jsx(Text, { children: " → " }), _jsx(Text, { color: "white", children: display })] }, i));
|
|
48
|
+
})] })] }));
|
|
49
|
+
}
|
|
50
|
+
function ToolResultView({ content, }) {
|
|
51
|
+
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
|
+
}
|
|
53
|
+
function MessageView({ message, }) {
|
|
12
54
|
if (message.role === "user") {
|
|
13
|
-
return
|
|
55
|
+
return _jsx(UserMessageView, { content: message.content });
|
|
14
56
|
}
|
|
15
57
|
if (message.role === "tool") {
|
|
16
|
-
return
|
|
58
|
+
return _jsx(ToolResultView, { content: message.content });
|
|
59
|
+
}
|
|
60
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
61
|
+
return _jsx(ToolCallsView, { message: message });
|
|
17
62
|
}
|
|
18
63
|
if (message.role === "assistant") {
|
|
19
|
-
|
|
20
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "CasAbot: " }), message.content ? _jsx(Text, { color: "cyan", children: message.content }) : null, message.toolCalls.map((tc, i) => {
|
|
21
|
-
const args = (() => {
|
|
22
|
-
try {
|
|
23
|
-
return JSON.parse(tc.arguments);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
})();
|
|
29
|
-
return (_jsxs(Text, { dimColor: true, children: [" ⚡ ", tc.name, ": ", args.command ?? tc.arguments] }, i));
|
|
30
|
-
})] }));
|
|
31
|
-
}
|
|
32
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "CasAbot: " }), _jsx(Text, { color: "cyan", children: message.content })] }));
|
|
64
|
+
return _jsx(AssistantMessageView, { content: message.content });
|
|
33
65
|
}
|
|
34
66
|
return _jsx(Text, { children: message.content });
|
|
35
67
|
}
|
|
36
|
-
function
|
|
68
|
+
function WelcomeHint() {
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 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." })] }));
|
|
70
|
+
}
|
|
71
|
+
function ProcessingIndicator() {
|
|
72
|
+
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…" })] }));
|
|
73
|
+
}
|
|
74
|
+
function App({ provider, conversation, skills, }) {
|
|
37
75
|
const [messages, setMessages] = useState([]);
|
|
38
76
|
const [input, setInput] = useState("");
|
|
39
77
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
40
78
|
const { exit } = useApp();
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
const { stdout } = useStdout();
|
|
80
|
+
const [termSize, setTermSize] = useState({
|
|
81
|
+
columns: stdout.columns ?? 80,
|
|
82
|
+
rows: stdout.rows ?? 24,
|
|
83
|
+
});
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const onResize = () => {
|
|
86
|
+
setTermSize({
|
|
87
|
+
columns: stdout.columns ?? 80,
|
|
88
|
+
rows: stdout.rows ?? 24,
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
stdout.on("resize", onResize);
|
|
92
|
+
return () => {
|
|
93
|
+
stdout.off("resize", onResize);
|
|
94
|
+
};
|
|
95
|
+
}, [stdout]);
|
|
96
|
+
const handleSubmit = useCallback(async (text) => {
|
|
97
|
+
const trimmed = text.trim();
|
|
98
|
+
if (!trimmed || isProcessing)
|
|
44
99
|
return;
|
|
45
100
|
setInput("");
|
|
46
101
|
setIsProcessing(true);
|
|
47
|
-
const userMsg = { role: "user", content:
|
|
102
|
+
const userMsg = { role: "user", content: trimmed };
|
|
48
103
|
setMessages((prev) => [...prev, userMsg]);
|
|
49
104
|
try {
|
|
50
|
-
const generator = runAgent(provider,
|
|
105
|
+
const generator = runAgent(provider, trimmed, conversation, skills);
|
|
51
106
|
for await (const msg of generator) {
|
|
52
107
|
setMessages((prev) => [...prev, msg]);
|
|
53
108
|
}
|
|
@@ -56,31 +111,20 @@ function App({ provider, conversation, skills }) {
|
|
|
56
111
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
57
112
|
setMessages((prev) => [
|
|
58
113
|
...prev,
|
|
59
|
-
{ role: "assistant", content: `❌ Error
|
|
114
|
+
{ role: "assistant", content: `❌ Error: ${errorMsg}` },
|
|
60
115
|
]);
|
|
61
116
|
}
|
|
62
117
|
setIsProcessing(false);
|
|
63
|
-
}, [
|
|
118
|
+
}, [isProcessing, provider, conversation, skills]);
|
|
64
119
|
useInput((ch, key) => {
|
|
65
120
|
if (key.ctrl && ch === "c") {
|
|
66
121
|
exit();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (isProcessing)
|
|
70
|
-
return;
|
|
71
|
-
if (key.return) {
|
|
72
|
-
handleSubmit().catch(() => { });
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (key.backspace || key.delete) {
|
|
76
|
-
setInput((prev) => prev.slice(0, -1));
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (ch && !key.ctrl && !key.meta) {
|
|
80
|
-
setInput((prev) => prev + ch);
|
|
81
122
|
}
|
|
82
123
|
});
|
|
83
|
-
|
|
124
|
+
const userCount = messages.filter((m) => m.role === "user").length;
|
|
125
|
+
return (_jsxs(Box, { flexDirection: "column", width: termSize.columns, height: termSize.rows, borderStyle: "round", borderColor: "gray", children: [_jsx(Header, {}), _jsx(HRule, { width: termSize.columns }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflowY: "hidden", justifyContent: messages.length === 0 && !isProcessing ? "center" : "flex-end", paddingBottom: 1, children: [messages.length === 0 && !isProcessing ? (_jsx(WelcomeHint, {})) : (messages.map((msg, i) => (_jsx(MessageView, { message: msg }, i)))), isProcessing && _jsx(ProcessingIndicator, {})] }), _jsx(HRule, { width: termSize.columns }), _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) => {
|
|
126
|
+
handleSubmit(val).catch(() => { });
|
|
127
|
+
}, 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"] })] })] }));
|
|
84
128
|
}
|
|
85
129
|
export function startTUI(provider, conversation, skills) {
|
|
86
130
|
render(_jsx(App, { provider: provider, conversation: conversation, skills: skills }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "casabot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "CasAbot — Skill-driven multi-agent orchestrator system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"gray-matter": "^4.0.3",
|
|
34
34
|
"ink": "^6.6.0",
|
|
35
|
+
"ink-gradient": "^4.0.0",
|
|
36
|
+
"ink-spinner": "^5.0.0",
|
|
37
|
+
"ink-text-input": "^6.0.0",
|
|
35
38
|
"marked": "^15.0.12",
|
|
36
39
|
"marked-terminal": "^7.3.0",
|
|
37
40
|
"openai": "^5.1.0",
|
package/src/tui/app.tsx
CHANGED
|
@@ -1,147 +1,321 @@
|
|
|
1
|
-
import React, { useState, useCallback } from "react";
|
|
2
|
-
import { render, Box, Text, useInput, useApp } from "ink";
|
|
1
|
+
import React, { useState, useCallback, useEffect } from "react";
|
|
2
|
+
import { render, Box, Text, useInput, useApp, useStdout } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import Gradient from "ink-gradient";
|
|
6
|
+
import { marked } from "marked";
|
|
7
|
+
import { markedTerminal } from "marked-terminal";
|
|
3
8
|
import type { ChatProvider } from "../providers/base.js";
|
|
4
9
|
import type { ConversationHistory, Message, Skill } from "../config/types.js";
|
|
5
10
|
import { runAgent } from "../agent/base.js";
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
marked.use(markedTerminal());
|
|
13
|
+
|
|
14
|
+
function renderMarkdown(content: string): string {
|
|
15
|
+
const result = marked.parse(content);
|
|
16
|
+
if (typeof result === "string") return result.trimEnd();
|
|
17
|
+
return content;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function truncateOutput(content: string, maxLines = 8): string {
|
|
8
21
|
const lines = content.split("\n");
|
|
9
22
|
if (lines.length <= maxLines) return content;
|
|
10
|
-
return
|
|
23
|
+
return (
|
|
24
|
+
lines.slice(0, maxLines).join("\n") +
|
|
25
|
+
`\n … ${lines.length - maxLines} more lines`
|
|
26
|
+
);
|
|
11
27
|
}
|
|
12
28
|
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
function HRule({ width }: { width: number }): React.ReactElement {
|
|
30
|
+
return (
|
|
31
|
+
<Box paddingX={1}>
|
|
32
|
+
<Text dimColor>{"─".repeat(Math.max(width - 4, 10))}</Text>
|
|
33
|
+
</Box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Header(): React.ReactElement {
|
|
38
|
+
return (
|
|
39
|
+
<Box flexDirection="column" paddingX={2} paddingTop={1}>
|
|
40
|
+
<Gradient name="vice">
|
|
41
|
+
<Text bold>{"✦ CasAbot"}</Text>
|
|
42
|
+
</Gradient>
|
|
43
|
+
<Text dimColor>
|
|
44
|
+
{"Cassiopeia A — Freely creates everything, like a supernova explosion."}
|
|
45
|
+
</Text>
|
|
46
|
+
</Box>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function UserMessageView({ content }: { content: string }): React.ReactElement {
|
|
51
|
+
return (
|
|
52
|
+
<Box flexDirection="column" paddingX={2} marginTop={1}>
|
|
53
|
+
<Text color="green" bold>
|
|
54
|
+
{"▶ You"}
|
|
55
|
+
</Text>
|
|
56
|
+
<Box marginLeft={2}>
|
|
57
|
+
<Text>{content}</Text>
|
|
19
58
|
</Box>
|
|
20
|
-
|
|
21
|
-
|
|
59
|
+
</Box>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
22
62
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
63
|
+
function AssistantMessageView({
|
|
64
|
+
content,
|
|
65
|
+
}: {
|
|
66
|
+
content: string;
|
|
67
|
+
}): React.ReactElement {
|
|
68
|
+
return (
|
|
69
|
+
<Box flexDirection="column" paddingX={2} marginTop={1}>
|
|
70
|
+
<Text color="cyan" bold>
|
|
71
|
+
{"✦ CasAbot"}
|
|
72
|
+
</Text>
|
|
73
|
+
<Box marginLeft={2}>
|
|
74
|
+
<Text>{renderMarkdown(content)}</Text>
|
|
28
75
|
</Box>
|
|
29
|
-
|
|
30
|
-
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
31
79
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
</Text>
|
|
46
|
-
);
|
|
47
|
-
})}
|
|
80
|
+
function ToolCallsView({
|
|
81
|
+
message,
|
|
82
|
+
}: {
|
|
83
|
+
message: Message;
|
|
84
|
+
}): React.ReactElement {
|
|
85
|
+
return (
|
|
86
|
+
<Box flexDirection="column" paddingX={2} marginTop={1}>
|
|
87
|
+
<Text color="cyan" bold>
|
|
88
|
+
{"✦ CasAbot"}
|
|
89
|
+
</Text>
|
|
90
|
+
{message.content ? (
|
|
91
|
+
<Box marginLeft={2}>
|
|
92
|
+
<Text>{renderMarkdown(message.content)}</Text>
|
|
48
93
|
</Box>
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
94
|
+
) : null}
|
|
95
|
+
<Box
|
|
96
|
+
flexDirection="column"
|
|
97
|
+
marginLeft={2}
|
|
98
|
+
marginTop={1}
|
|
99
|
+
borderStyle="round"
|
|
100
|
+
borderColor="yellow"
|
|
101
|
+
paddingX={1}
|
|
102
|
+
>
|
|
103
|
+
<Text color="yellow" bold>
|
|
104
|
+
{"⚡ Tool Calls"}
|
|
105
|
+
</Text>
|
|
106
|
+
{message.toolCalls?.map((tc, i) => {
|
|
107
|
+
let display = tc.arguments;
|
|
108
|
+
try {
|
|
109
|
+
const args = JSON.parse(tc.arguments) as Record<string, unknown>;
|
|
110
|
+
if (typeof args.command === "string") display = args.command;
|
|
111
|
+
} catch {
|
|
112
|
+
/* keep raw */
|
|
113
|
+
}
|
|
114
|
+
return (
|
|
115
|
+
<Box key={i}>
|
|
116
|
+
<Text dimColor>{tc.name}</Text>
|
|
117
|
+
<Text>{" → "}</Text>
|
|
118
|
+
<Text color="white">{display}</Text>
|
|
119
|
+
</Box>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
56
122
|
</Box>
|
|
57
|
-
|
|
58
|
-
|
|
123
|
+
</Box>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ToolResultView({
|
|
128
|
+
content,
|
|
129
|
+
}: {
|
|
130
|
+
content: string;
|
|
131
|
+
}): React.ReactElement {
|
|
132
|
+
return (
|
|
133
|
+
<Box
|
|
134
|
+
flexDirection="column"
|
|
135
|
+
marginLeft={4}
|
|
136
|
+
marginRight={2}
|
|
137
|
+
borderStyle="round"
|
|
138
|
+
borderColor="gray"
|
|
139
|
+
paddingX={1}
|
|
140
|
+
>
|
|
141
|
+
<Text dimColor bold>
|
|
142
|
+
{"📋 Result"}
|
|
143
|
+
</Text>
|
|
144
|
+
<Text dimColor>{truncateOutput(content)}</Text>
|
|
145
|
+
</Box>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
59
148
|
|
|
149
|
+
function MessageView({
|
|
150
|
+
message,
|
|
151
|
+
}: {
|
|
152
|
+
message: Message;
|
|
153
|
+
}): React.ReactElement {
|
|
154
|
+
if (message.role === "user") {
|
|
155
|
+
return <UserMessageView content={message.content} />;
|
|
156
|
+
}
|
|
157
|
+
if (message.role === "tool") {
|
|
158
|
+
return <ToolResultView content={message.content} />;
|
|
159
|
+
}
|
|
160
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
161
|
+
return <ToolCallsView message={message} />;
|
|
162
|
+
}
|
|
163
|
+
if (message.role === "assistant") {
|
|
164
|
+
return <AssistantMessageView content={message.content} />;
|
|
165
|
+
}
|
|
60
166
|
return <Text>{message.content}</Text>;
|
|
61
167
|
}
|
|
62
168
|
|
|
169
|
+
function WelcomeHint(): React.ReactElement {
|
|
170
|
+
return (
|
|
171
|
+
<Box flexDirection="column" alignItems="center" justifyContent="center" flexGrow={1}>
|
|
172
|
+
<Text dimColor>{"Type a message below to get started."}</Text>
|
|
173
|
+
<Text dimColor>{"CasAbot will orchestrate agents to help you."}</Text>
|
|
174
|
+
</Box>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function ProcessingIndicator(): React.ReactElement {
|
|
179
|
+
return (
|
|
180
|
+
<Box paddingX={2} marginTop={1} gap={1}>
|
|
181
|
+
<Text color="yellow">
|
|
182
|
+
<Spinner type="dots" />
|
|
183
|
+
</Text>
|
|
184
|
+
<Text color="yellow">{"Thinking…"}</Text>
|
|
185
|
+
</Box>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
63
189
|
interface AppProps {
|
|
64
190
|
provider: ChatProvider;
|
|
65
191
|
conversation: ConversationHistory;
|
|
66
192
|
skills: Skill[];
|
|
67
193
|
}
|
|
68
194
|
|
|
69
|
-
function App({
|
|
195
|
+
function App({
|
|
196
|
+
provider,
|
|
197
|
+
conversation,
|
|
198
|
+
skills,
|
|
199
|
+
}: AppProps): React.ReactElement {
|
|
70
200
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
71
201
|
const [input, setInput] = useState("");
|
|
72
202
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
73
203
|
const { exit } = useApp();
|
|
204
|
+
const { stdout } = useStdout();
|
|
205
|
+
|
|
206
|
+
const [termSize, setTermSize] = useState({
|
|
207
|
+
columns: stdout.columns ?? 80,
|
|
208
|
+
rows: stdout.rows ?? 24,
|
|
209
|
+
});
|
|
74
210
|
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const onResize = () => {
|
|
213
|
+
setTermSize({
|
|
214
|
+
columns: stdout.columns ?? 80,
|
|
215
|
+
rows: stdout.rows ?? 24,
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
stdout.on("resize", onResize);
|
|
219
|
+
return () => {
|
|
220
|
+
stdout.off("resize", onResize);
|
|
221
|
+
};
|
|
222
|
+
}, [stdout]);
|
|
78
223
|
|
|
79
|
-
|
|
80
|
-
|
|
224
|
+
const handleSubmit = useCallback(
|
|
225
|
+
async (text: string) => {
|
|
226
|
+
const trimmed = text.trim();
|
|
227
|
+
if (!trimmed || isProcessing) return;
|
|
81
228
|
|
|
82
|
-
|
|
83
|
-
|
|
229
|
+
setInput("");
|
|
230
|
+
setIsProcessing(true);
|
|
84
231
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
232
|
+
const userMsg: Message = { role: "user", content: trimmed };
|
|
233
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const generator = runAgent(provider, trimmed, conversation, skills);
|
|
237
|
+
for await (const msg of generator) {
|
|
238
|
+
setMessages((prev) => [...prev, msg]);
|
|
239
|
+
}
|
|
240
|
+
} catch (err: unknown) {
|
|
241
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
242
|
+
setMessages((prev) => [
|
|
243
|
+
...prev,
|
|
244
|
+
{ role: "assistant", content: `❌ Error: ${errorMsg}` },
|
|
245
|
+
]);
|
|
89
246
|
}
|
|
90
|
-
} catch (err: unknown) {
|
|
91
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
92
|
-
setMessages((prev) => [
|
|
93
|
-
...prev,
|
|
94
|
-
{ role: "assistant", content: `❌ Error occurred: ${errorMsg}` },
|
|
95
|
-
]);
|
|
96
|
-
}
|
|
97
247
|
|
|
98
|
-
|
|
99
|
-
|
|
248
|
+
setIsProcessing(false);
|
|
249
|
+
},
|
|
250
|
+
[isProcessing, provider, conversation, skills],
|
|
251
|
+
);
|
|
100
252
|
|
|
101
253
|
useInput((ch, key) => {
|
|
102
254
|
if (key.ctrl && ch === "c") {
|
|
103
255
|
exit();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (isProcessing) return;
|
|
108
|
-
|
|
109
|
-
if (key.return) {
|
|
110
|
-
handleSubmit().catch(() => {});
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (key.backspace || key.delete) {
|
|
115
|
-
setInput((prev) => prev.slice(0, -1));
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (ch && !key.ctrl && !key.meta) {
|
|
120
|
-
setInput((prev) => prev + ch);
|
|
121
256
|
}
|
|
122
257
|
});
|
|
123
258
|
|
|
259
|
+
const userCount = messages.filter((m) => m.role === "user").length;
|
|
260
|
+
|
|
124
261
|
return (
|
|
125
|
-
<Box
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
262
|
+
<Box
|
|
263
|
+
flexDirection="column"
|
|
264
|
+
width={termSize.columns}
|
|
265
|
+
height={termSize.rows}
|
|
266
|
+
borderStyle="round"
|
|
267
|
+
borderColor="gray"
|
|
268
|
+
>
|
|
269
|
+
<Header />
|
|
270
|
+
<HRule width={termSize.columns} />
|
|
271
|
+
|
|
272
|
+
<Box
|
|
273
|
+
flexDirection="column"
|
|
274
|
+
flexGrow={1}
|
|
275
|
+
overflowY="hidden"
|
|
276
|
+
justifyContent={messages.length === 0 && !isProcessing ? "center" : "flex-end"}
|
|
277
|
+
paddingBottom={1}
|
|
278
|
+
>
|
|
279
|
+
{messages.length === 0 && !isProcessing ? (
|
|
280
|
+
<WelcomeHint />
|
|
281
|
+
) : (
|
|
282
|
+
messages.map((msg, i) => (
|
|
283
|
+
<MessageView key={i} message={msg} />
|
|
284
|
+
))
|
|
285
|
+
)}
|
|
286
|
+
{isProcessing && <ProcessingIndicator />}
|
|
131
287
|
</Box>
|
|
132
288
|
|
|
133
|
-
{
|
|
134
|
-
<MessageView key={i} message={msg} />
|
|
135
|
-
))}
|
|
289
|
+
<HRule width={termSize.columns} />
|
|
136
290
|
|
|
137
|
-
{
|
|
138
|
-
<
|
|
139
|
-
|
|
291
|
+
<Box paddingX={1}>
|
|
292
|
+
<Box
|
|
293
|
+
borderStyle="round"
|
|
294
|
+
borderColor={isProcessing ? "gray" : "cyan"}
|
|
295
|
+
paddingX={1}
|
|
296
|
+
width="100%"
|
|
297
|
+
>
|
|
298
|
+
<Text color="cyan" bold>
|
|
299
|
+
{"❯ "}
|
|
300
|
+
</Text>
|
|
301
|
+
<TextInput
|
|
302
|
+
value={input}
|
|
303
|
+
onChange={setInput}
|
|
304
|
+
onSubmit={(val: string) => {
|
|
305
|
+
handleSubmit(val).catch(() => {});
|
|
306
|
+
}}
|
|
307
|
+
placeholder="Type your message…"
|
|
308
|
+
focus={!isProcessing}
|
|
309
|
+
showCursor
|
|
310
|
+
/>
|
|
311
|
+
</Box>
|
|
312
|
+
</Box>
|
|
140
313
|
|
|
141
|
-
<Box
|
|
142
|
-
<Text
|
|
143
|
-
<Text>
|
|
144
|
-
|
|
314
|
+
<Box paddingX={2} justifyContent="space-between">
|
|
315
|
+
<Text dimColor>{"Ctrl+C exit"}</Text>
|
|
316
|
+
<Text dimColor>
|
|
317
|
+
{userCount} {userCount === 1 ? "message" : "messages"}
|
|
318
|
+
</Text>
|
|
145
319
|
</Box>
|
|
146
320
|
</Box>
|
|
147
321
|
);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
declare module "marked-terminal" {
|
|
2
|
+
import type { MarkedExtension } from "marked";
|
|
3
|
+
export function markedTerminal(options?: Record<string, unknown>): MarkedExtension;
|
|
4
|
+
export default function Renderer(options?: Record<string, unknown>, highlightOptions?: Record<string, unknown>): void;
|
|
5
|
+
}
|