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 CHANGED
@@ -1,18 +1,16 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState, useCallback, useEffect, useMemo, useRef } from "react";
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 useMouseWheel(onScrollUp, onScrollDown) {
26
- const { stdout } = useStdout();
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 Header() {
63
- 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." })] }));
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
- 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) => {
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
- return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: tc.name }), _jsx(Text, { children: " → " }), _jsx(Text, { color: "white", children: display })] }, i));
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
- 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) })] }));
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", 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." })] }));
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 [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]);
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
- return (_jsxs(Box, { flexDirection: "column", width: termSize.columns, height: termSize.rows, borderStyle: "round", borderColor: "gray", children: [_jsx(Header, {}), _jsx(HRule, { width: termSize.columns }), _jsx(Box, { flexDirection: "column", height: messagesHeight, overflowY: "hidden", justifyContent: "flex-end", children: messages.length === 0 && !isProcessing ? (_jsx(WelcomeHint, {})) : (_jsxs(_Fragment, { children: [_jsx(Box, { minHeight: messagesHeight }), visibleMessages.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) => {
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 ↑↓/wheel scroll" }), _jsxs(Text, { dimColor: true, children: [userCount, " ", userCount === 1 ? "message" : "messages"] })] })] }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "casabot",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "CasAbot — Skill-driven multi-agent orchestrator system",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/tui/app.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
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 useMouseWheel(
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(width - 4, 10))}</Text>
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 Header(): React.ReactElement {
37
+ function HeaderBlock({ columns }: { columns: number }): React.ReactElement {
83
38
  return (
84
- <Box flexDirection="column" paddingX={2} paddingTop={1}>
85
- <Gradient name="vice">
86
- <Text bold>{"✦ CasAbot"}</Text>
87
- </Gradient>
88
- <Text dimColor>
89
- {"Cassiopeia A — Freely creates everything, like a supernova explosion."}
90
- </Text>
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" alignItems="center" justifyContent="center" flexGrow={1}>
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
- flexDirection="column"
344
- width={termSize.columns}
345
- height={termSize.rows}
346
- borderStyle="round"
347
- borderColor="gray"
348
- >
349
- <Header />
350
- <HRule width={termSize.columns} />
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
- <Box
353
- flexDirection="column"
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 width={termSize.columns} />
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="100%"
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 ↑↓/wheel scroll"}</Text>
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>