casabot 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- function truncateOutput(content, maxLines = 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") + `\n ... (${lines.length - maxLines} more lines)`;
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 MessageView({ message }) {
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 (_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: "User: " }), _jsx(Text, { children: message.content })] }));
55
+ return _jsx(UserMessageView, { content: message.content });
14
56
  }
15
57
  if (message.role === "tool") {
16
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, bold: true, children: "[Tool Result]" }), _jsx(Text, { dimColor: true, children: truncateOutput(message.content) })] }));
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
- if (message.toolCalls?.length) {
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 App({ provider, conversation, skills }) {
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 handleSubmit = useCallback(async () => {
42
- const text = input.trim();
43
- if (!text || isProcessing)
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: text };
102
+ const userMsg = { role: "user", content: trimmed };
48
103
  setMessages((prev) => [...prev, userMsg]);
49
104
  try {
50
- const generator = runAgent(provider, text, conversation, skills);
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 occurred: ${errorMsg}` },
114
+ { role: "assistant", content: `❌ Error: ${errorMsg}` },
60
115
  ]);
61
116
  }
62
117
  setIsProcessing(false);
63
- }, [input, isProcessing, provider, conversation, skills]);
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
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "🌟 CasAbot > " }), _jsx(Text, { dimColor: true, children: "Cassiopeia A \u2014 Freely creates everything, like a supernova explosion." })] }), messages.map((msg, i) => (_jsx(MessageView, { message: msg }, i))), isProcessing && (_jsx(Text, { color: "yellow", children: "⏳ Processing..." })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", bold: true, children: "❯ " }), _jsx(Text, { children: input }), !isProcessing && _jsx(Text, { dimColor: true, children: "█" })] })] }));
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.0.2",
3
+ "version": "1.1.0",
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
- function truncateOutput(content: string, maxLines = 5): string {
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 lines.slice(0, maxLines).join("\n") + `\n ... (${lines.length - maxLines} more lines)`;
23
+ return (
24
+ lines.slice(0, maxLines).join("\n") +
25
+ `\n … ${lines.length - maxLines} more lines`
26
+ );
11
27
  }
12
28
 
13
- function MessageView({ message }: { message: Message }): React.ReactElement {
14
- if (message.role === "user") {
15
- return (
16
- <Box>
17
- <Text color="green" bold>{"User: "}</Text>
18
- <Text>{message.content}</Text>
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
- if (message.role === "tool") {
24
- return (
25
- <Box flexDirection="column">
26
- <Text dimColor bold>{"[Tool Result]"}</Text>
27
- <Text dimColor>{truncateOutput(message.content)}</Text>
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
- if (message.role === "assistant") {
33
- if (message.toolCalls?.length) {
34
- return (
35
- <Box flexDirection="column">
36
- <Text color="cyan" bold>{"CasAbot: "}</Text>
37
- {message.content ? <Text color="cyan">{message.content}</Text> : null}
38
- {message.toolCalls.map((tc, i) => {
39
- const args = (() => {
40
- try { return JSON.parse(tc.arguments) as { command?: string }; } catch { return {}; }
41
- })();
42
- return (
43
- <Text key={i} dimColor>
44
- {" ⚡ "}{tc.name}: {args.command ?? tc.arguments}
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
- return (
53
- <Box>
54
- <Text color="cyan" bold>{"CasAbot: "}</Text>
55
- <Text color="cyan">{message.content}</Text>
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({ provider, conversation, skills }: AppProps): React.ReactElement {
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
- const handleSubmit = useCallback(async () => {
76
- const text = input.trim();
77
- if (!text || isProcessing) return;
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
- setInput("");
80
- setIsProcessing(true);
224
+ const handleSubmit = useCallback(
225
+ async (text: string) => {
226
+ const trimmed = text.trim();
227
+ if (!trimmed || isProcessing) return;
81
228
 
82
- const userMsg: Message = { role: "user", content: text };
83
- setMessages((prev) => [...prev, userMsg]);
229
+ setInput("");
230
+ setIsProcessing(true);
84
231
 
85
- try {
86
- const generator = runAgent(provider, text, conversation, skills);
87
- for await (const msg of generator) {
88
- setMessages((prev) => [...prev, msg]);
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
- setIsProcessing(false);
99
- }, [input, isProcessing, provider, conversation, skills]);
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 flexDirection="column">
126
- <Box marginBottom={1}>
127
- <Text bold color="cyan">
128
- {"🌟 CasAbot > "}
129
- </Text>
130
- <Text dimColor>Cassiopeia A — Freely creates everything, like a supernova explosion.</Text>
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
- {messages.map((msg, i) => (
134
- <MessageView key={i} message={msg} />
135
- ))}
289
+ <HRule width={termSize.columns} />
136
290
 
137
- {isProcessing && (
138
- <Text color="yellow">{"⏳ Processing..."}</Text>
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 marginTop={1}>
142
- <Text color="green" bold>{" "}</Text>
143
- <Text>{input}</Text>
144
- {!isProcessing && <Text dimColor>{""}</Text>}
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
+ function markedTerminal(options?: Record<string, unknown>): MarkedExtension;
4
+ export default markedTerminal;
5
+ }