casabot 1.1.4 → 1.1.6

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