casabot 1.1.1 → 1.1.3
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 +112 -9
- package/package.json +1 -1
- package/src/tui/app.tsx +162 -13
package/dist/tui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback, useEffect } from "react";
|
|
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
3
|
import { render, Box, Text, useInput, useApp, useStdout } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import Spinner from "ink-spinner";
|
|
@@ -7,12 +7,13 @@ 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(
|
|
10
|
+
marked.use(markedTerminal({
|
|
11
|
+
showSectionPrefix: false,
|
|
12
|
+
tab: 2,
|
|
13
|
+
}));
|
|
14
|
+
marked.use({ gfm: true });
|
|
11
15
|
function renderMarkdown(content) {
|
|
12
|
-
|
|
13
|
-
if (typeof result === "string")
|
|
14
|
-
return result.trimEnd();
|
|
15
|
-
return content;
|
|
16
|
+
return marked.parse(content, { async: false }).trimEnd();
|
|
16
17
|
}
|
|
17
18
|
function truncateOutput(content, maxLines = 8) {
|
|
18
19
|
const lines = content.split("\n");
|
|
@@ -21,6 +22,61 @@ function truncateOutput(content, maxLines = 8) {
|
|
|
21
22
|
return (lines.slice(0, maxLines).join("\n") +
|
|
22
23
|
`\n … ${lines.length - maxLines} more lines`);
|
|
23
24
|
}
|
|
25
|
+
function estimateMessageLines(message, width) {
|
|
26
|
+
const contentWidth = Math.max(width - 10, 20);
|
|
27
|
+
const countLines = (text) => text.split("\n").reduce((sum, line) => sum + Math.max(1, Math.ceil((line.length || 1) / contentWidth)), 0);
|
|
28
|
+
if (message.role === "user") {
|
|
29
|
+
return 2 + countLines(message.content);
|
|
30
|
+
}
|
|
31
|
+
if (message.role === "tool") {
|
|
32
|
+
return 3 + countLines(truncateOutput(message.content));
|
|
33
|
+
}
|
|
34
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
35
|
+
let lines = 2;
|
|
36
|
+
if (message.content)
|
|
37
|
+
lines += countLines(message.content);
|
|
38
|
+
lines += 4 + (message.toolCalls?.length ?? 0);
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
if (message.role === "assistant") {
|
|
42
|
+
return 2 + countLines(message.content);
|
|
43
|
+
}
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
function useMouseWheel(onScrollUp, onScrollDown) {
|
|
47
|
+
const { stdout } = useStdout();
|
|
48
|
+
const scrollUpRef = useRef(onScrollUp);
|
|
49
|
+
const scrollDownRef = useRef(onScrollDown);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
scrollUpRef.current = onScrollUp;
|
|
52
|
+
scrollDownRef.current = onScrollDown;
|
|
53
|
+
});
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!process.stdin.isTTY)
|
|
56
|
+
return;
|
|
57
|
+
const ENABLE_MOUSE = "\x1b[?1000h\x1b[?1006h";
|
|
58
|
+
const DISABLE_MOUSE = "\x1b[?1000l\x1b[?1006l";
|
|
59
|
+
stdout.write(ENABLE_MOUSE);
|
|
60
|
+
const sgrRegex = /\x1b\[<(\d+);\d+;\d+M/g;
|
|
61
|
+
const handleData = (data) => {
|
|
62
|
+
const str = data.toString("utf8");
|
|
63
|
+
let match;
|
|
64
|
+
while ((match = sgrRegex.exec(str)) !== null) {
|
|
65
|
+
const button = parseInt(match[1], 10);
|
|
66
|
+
if (button === 64)
|
|
67
|
+
scrollUpRef.current();
|
|
68
|
+
if (button === 65)
|
|
69
|
+
scrollDownRef.current();
|
|
70
|
+
}
|
|
71
|
+
sgrRegex.lastIndex = 0;
|
|
72
|
+
};
|
|
73
|
+
process.stdin.on("data", handleData);
|
|
74
|
+
return () => {
|
|
75
|
+
process.stdin.off("data", handleData);
|
|
76
|
+
stdout.write(DISABLE_MOUSE);
|
|
77
|
+
};
|
|
78
|
+
}, [stdout]);
|
|
79
|
+
}
|
|
24
80
|
function HRule({ width }) {
|
|
25
81
|
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(width - 4, 10)) }) }));
|
|
26
82
|
}
|
|
@@ -71,10 +127,17 @@ function WelcomeHint() {
|
|
|
71
127
|
function ProcessingIndicator() {
|
|
72
128
|
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
129
|
}
|
|
130
|
+
function ScrollIndicator({ direction, count, }) {
|
|
131
|
+
const arrow = direction === "above" ? "▲" : "▼";
|
|
132
|
+
return (_jsx(Box, { justifyContent: "center", paddingX: 2, children: _jsx(Text, { dimColor: true, children: `${arrow} ${count} more ${count === 1 ? "message" : "messages"} ${direction}` }) }));
|
|
133
|
+
}
|
|
134
|
+
// border(2) + header(3) + hrules(2) + input(3) + status(1) = 11
|
|
135
|
+
const CHROME_HEIGHT = 11;
|
|
74
136
|
function App({ provider, conversation, skills, }) {
|
|
75
137
|
const [messages, setMessages] = useState([]);
|
|
76
138
|
const [input, setInput] = useState("");
|
|
77
139
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
140
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
78
141
|
const { exit } = useApp();
|
|
79
142
|
const { stdout } = useStdout();
|
|
80
143
|
const [termSize, setTermSize] = useState({
|
|
@@ -93,6 +156,33 @@ function App({ provider, conversation, skills, }) {
|
|
|
93
156
|
stdout.off("resize", onResize);
|
|
94
157
|
};
|
|
95
158
|
}, [stdout]);
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
setScrollOffset((prev) => (prev === 0 ? 0 : prev + 1));
|
|
161
|
+
}, [messages.length]);
|
|
162
|
+
const messagesHeight = Math.max(termSize.rows - CHROME_HEIGHT, 4);
|
|
163
|
+
const { visibleMessages, hiddenAbove, hiddenBelow } = useMemo(() => {
|
|
164
|
+
if (messages.length === 0) {
|
|
165
|
+
return { visibleMessages: [], hiddenAbove: 0, hiddenBelow: 0 };
|
|
166
|
+
}
|
|
167
|
+
const endIndex = messages.length - scrollOffset;
|
|
168
|
+
let usedLines = isProcessing ? 2 : 0;
|
|
169
|
+
let startIndex = endIndex;
|
|
170
|
+
for (let i = endIndex - 1; i >= 0; i--) {
|
|
171
|
+
const lines = estimateMessageLines(messages[i], termSize.columns);
|
|
172
|
+
if (usedLines + lines > messagesHeight && startIndex < endIndex)
|
|
173
|
+
break;
|
|
174
|
+
usedLines += lines;
|
|
175
|
+
startIndex = i;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
visibleMessages: messages.slice(startIndex, endIndex),
|
|
179
|
+
hiddenAbove: startIndex,
|
|
180
|
+
hiddenBelow: scrollOffset,
|
|
181
|
+
};
|
|
182
|
+
}, [messages, scrollOffset, messagesHeight, termSize.columns, isProcessing]);
|
|
183
|
+
const maxScrollOffset = useMemo(() => {
|
|
184
|
+
return Math.max(0, messages.length - 1);
|
|
185
|
+
}, [messages.length]);
|
|
96
186
|
const handleSubmit = useCallback(async (text) => {
|
|
97
187
|
const trimmed = text.trim();
|
|
98
188
|
if (!trimmed || isProcessing)
|
|
@@ -116,15 +206,28 @@ function App({ provider, conversation, skills, }) {
|
|
|
116
206
|
}
|
|
117
207
|
setIsProcessing(false);
|
|
118
208
|
}, [isProcessing, provider, conversation, skills]);
|
|
209
|
+
const scrollUp = useCallback(() => {
|
|
210
|
+
setScrollOffset((prev) => Math.min(prev + 1, maxScrollOffset));
|
|
211
|
+
}, [maxScrollOffset]);
|
|
212
|
+
const scrollDown = useCallback(() => {
|
|
213
|
+
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
214
|
+
}, []);
|
|
215
|
+
useMouseWheel(scrollUp, scrollDown);
|
|
119
216
|
useInput((ch, key) => {
|
|
120
217
|
if (key.ctrl && ch === "c") {
|
|
121
218
|
exit();
|
|
122
219
|
}
|
|
220
|
+
if (key.upArrow) {
|
|
221
|
+
scrollUp();
|
|
222
|
+
}
|
|
223
|
+
if (key.downArrow) {
|
|
224
|
+
scrollDown();
|
|
225
|
+
}
|
|
123
226
|
});
|
|
124
227
|
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 }),
|
|
228
|
+
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: [hiddenAbove > 0 && (_jsx(ScrollIndicator, { direction: "above", count: hiddenAbove })), visibleMessages.map((msg, i) => (_jsx(MessageView, { message: msg }, hiddenAbove + i))), isProcessing && _jsx(ProcessingIndicator, {}), hiddenBelow > 0 && (_jsx(ScrollIndicator, { direction: "below", count: hiddenBelow }))] })) }), _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
229
|
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"] })] })] }));
|
|
230
|
+
}, 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"] })] })] }));
|
|
128
231
|
}
|
|
129
232
|
export function startTUI(provider, conversation, skills) {
|
|
130
233
|
render(_jsx(App, { provider: provider, conversation: conversation, skills: skills }));
|
package/package.json
CHANGED
package/src/tui/app.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useCallback, useEffect } from "react";
|
|
1
|
+
import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { render, Box, Text, useInput, useApp, useStdout } from "ink";
|
|
3
3
|
import TextInput from "ink-text-input";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
@@ -9,12 +9,16 @@ 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(
|
|
12
|
+
marked.use(
|
|
13
|
+
markedTerminal({
|
|
14
|
+
showSectionPrefix: false,
|
|
15
|
+
tab: 2,
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
marked.use({ gfm: true });
|
|
13
19
|
|
|
14
20
|
function renderMarkdown(content: string): string {
|
|
15
|
-
|
|
16
|
-
if (typeof result === "string") return result.trimEnd();
|
|
17
|
-
return content;
|
|
21
|
+
return (marked.parse(content, { async: false }) as string).trimEnd();
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
function truncateOutput(content: string, maxLines = 8): string {
|
|
@@ -26,6 +30,74 @@ function truncateOutput(content: string, maxLines = 8): string {
|
|
|
26
30
|
);
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
function estimateMessageLines(message: Message, width: number): number {
|
|
34
|
+
const contentWidth = Math.max(width - 10, 20);
|
|
35
|
+
const countLines = (text: string): number =>
|
|
36
|
+
text.split("\n").reduce(
|
|
37
|
+
(sum, line) =>
|
|
38
|
+
sum + Math.max(1, Math.ceil((line.length || 1) / contentWidth)),
|
|
39
|
+
0,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (message.role === "user") {
|
|
43
|
+
return 2 + countLines(message.content);
|
|
44
|
+
}
|
|
45
|
+
if (message.role === "tool") {
|
|
46
|
+
return 3 + countLines(truncateOutput(message.content));
|
|
47
|
+
}
|
|
48
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
49
|
+
let lines = 2;
|
|
50
|
+
if (message.content) lines += countLines(message.content);
|
|
51
|
+
lines += 4 + (message.toolCalls?.length ?? 0);
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
if (message.role === "assistant") {
|
|
55
|
+
return 2 + countLines(message.content);
|
|
56
|
+
}
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function useMouseWheel(
|
|
61
|
+
onScrollUp: () => void,
|
|
62
|
+
onScrollDown: () => void,
|
|
63
|
+
): void {
|
|
64
|
+
const { stdout } = useStdout();
|
|
65
|
+
const scrollUpRef = useRef(onScrollUp);
|
|
66
|
+
const scrollDownRef = useRef(onScrollDown);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
scrollUpRef.current = onScrollUp;
|
|
70
|
+
scrollDownRef.current = onScrollDown;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!process.stdin.isTTY) return;
|
|
75
|
+
|
|
76
|
+
const ENABLE_MOUSE = "\x1b[?1000h\x1b[?1006h";
|
|
77
|
+
const DISABLE_MOUSE = "\x1b[?1000l\x1b[?1006l";
|
|
78
|
+
stdout.write(ENABLE_MOUSE);
|
|
79
|
+
|
|
80
|
+
const sgrRegex = /\x1b\[<(\d+);\d+;\d+M/g;
|
|
81
|
+
|
|
82
|
+
const handleData = (data: Buffer): void => {
|
|
83
|
+
const str = data.toString("utf8");
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = sgrRegex.exec(str)) !== null) {
|
|
86
|
+
const button = parseInt(match[1], 10);
|
|
87
|
+
if (button === 64) scrollUpRef.current();
|
|
88
|
+
if (button === 65) scrollDownRef.current();
|
|
89
|
+
}
|
|
90
|
+
sgrRegex.lastIndex = 0;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
process.stdin.on("data", handleData);
|
|
94
|
+
return () => {
|
|
95
|
+
process.stdin.off("data", handleData);
|
|
96
|
+
stdout.write(DISABLE_MOUSE);
|
|
97
|
+
};
|
|
98
|
+
}, [stdout]);
|
|
99
|
+
}
|
|
100
|
+
|
|
29
101
|
function HRule({ width }: { width: number }): React.ReactElement {
|
|
30
102
|
return (
|
|
31
103
|
<Box paddingX={1}>
|
|
@@ -186,12 +258,32 @@ function ProcessingIndicator(): React.ReactElement {
|
|
|
186
258
|
);
|
|
187
259
|
}
|
|
188
260
|
|
|
261
|
+
function ScrollIndicator({
|
|
262
|
+
direction,
|
|
263
|
+
count,
|
|
264
|
+
}: {
|
|
265
|
+
direction: "above" | "below";
|
|
266
|
+
count: number;
|
|
267
|
+
}): React.ReactElement {
|
|
268
|
+
const arrow = direction === "above" ? "▲" : "▼";
|
|
269
|
+
return (
|
|
270
|
+
<Box justifyContent="center" paddingX={2}>
|
|
271
|
+
<Text dimColor>
|
|
272
|
+
{`${arrow} ${count} more ${count === 1 ? "message" : "messages"} ${direction}`}
|
|
273
|
+
</Text>
|
|
274
|
+
</Box>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
189
278
|
interface AppProps {
|
|
190
279
|
provider: ChatProvider;
|
|
191
280
|
conversation: ConversationHistory;
|
|
192
281
|
skills: Skill[];
|
|
193
282
|
}
|
|
194
283
|
|
|
284
|
+
// border(2) + header(3) + hrules(2) + input(3) + status(1) = 11
|
|
285
|
+
const CHROME_HEIGHT = 11;
|
|
286
|
+
|
|
195
287
|
function App({
|
|
196
288
|
provider,
|
|
197
289
|
conversation,
|
|
@@ -200,6 +292,7 @@ function App({
|
|
|
200
292
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
201
293
|
const [input, setInput] = useState("");
|
|
202
294
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
295
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
203
296
|
const { exit } = useApp();
|
|
204
297
|
const { stdout } = useStdout();
|
|
205
298
|
|
|
@@ -221,6 +314,39 @@ function App({
|
|
|
221
314
|
};
|
|
222
315
|
}, [stdout]);
|
|
223
316
|
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
setScrollOffset((prev) => (prev === 0 ? 0 : prev + 1));
|
|
319
|
+
}, [messages.length]);
|
|
320
|
+
|
|
321
|
+
const messagesHeight = Math.max(termSize.rows - CHROME_HEIGHT, 4);
|
|
322
|
+
|
|
323
|
+
const { visibleMessages, hiddenAbove, hiddenBelow } = useMemo(() => {
|
|
324
|
+
if (messages.length === 0) {
|
|
325
|
+
return { visibleMessages: [] as Message[], hiddenAbove: 0, hiddenBelow: 0 };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const endIndex = messages.length - scrollOffset;
|
|
329
|
+
let usedLines = isProcessing ? 2 : 0;
|
|
330
|
+
let startIndex = endIndex;
|
|
331
|
+
|
|
332
|
+
for (let i = endIndex - 1; i >= 0; i--) {
|
|
333
|
+
const lines = estimateMessageLines(messages[i], termSize.columns);
|
|
334
|
+
if (usedLines + lines > messagesHeight && startIndex < endIndex) break;
|
|
335
|
+
usedLines += lines;
|
|
336
|
+
startIndex = i;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
visibleMessages: messages.slice(startIndex, endIndex),
|
|
341
|
+
hiddenAbove: startIndex,
|
|
342
|
+
hiddenBelow: scrollOffset,
|
|
343
|
+
};
|
|
344
|
+
}, [messages, scrollOffset, messagesHeight, termSize.columns, isProcessing]);
|
|
345
|
+
|
|
346
|
+
const maxScrollOffset = useMemo(() => {
|
|
347
|
+
return Math.max(0, messages.length - 1);
|
|
348
|
+
}, [messages.length]);
|
|
349
|
+
|
|
224
350
|
const handleSubmit = useCallback(
|
|
225
351
|
async (text: string) => {
|
|
226
352
|
const trimmed = text.trim();
|
|
@@ -250,10 +376,26 @@ function App({
|
|
|
250
376
|
[isProcessing, provider, conversation, skills],
|
|
251
377
|
);
|
|
252
378
|
|
|
379
|
+
const scrollUp = useCallback(() => {
|
|
380
|
+
setScrollOffset((prev) => Math.min(prev + 1, maxScrollOffset));
|
|
381
|
+
}, [maxScrollOffset]);
|
|
382
|
+
|
|
383
|
+
const scrollDown = useCallback(() => {
|
|
384
|
+
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
385
|
+
}, []);
|
|
386
|
+
|
|
387
|
+
useMouseWheel(scrollUp, scrollDown);
|
|
388
|
+
|
|
253
389
|
useInput((ch, key) => {
|
|
254
390
|
if (key.ctrl && ch === "c") {
|
|
255
391
|
exit();
|
|
256
392
|
}
|
|
393
|
+
if (key.upArrow) {
|
|
394
|
+
scrollUp();
|
|
395
|
+
}
|
|
396
|
+
if (key.downArrow) {
|
|
397
|
+
scrollDown();
|
|
398
|
+
}
|
|
257
399
|
});
|
|
258
400
|
|
|
259
401
|
const userCount = messages.filter((m) => m.role === "user").length;
|
|
@@ -271,19 +413,26 @@ function App({
|
|
|
271
413
|
|
|
272
414
|
<Box
|
|
273
415
|
flexDirection="column"
|
|
274
|
-
|
|
416
|
+
height={messagesHeight}
|
|
275
417
|
overflowY="hidden"
|
|
276
|
-
justifyContent=
|
|
277
|
-
paddingBottom={1}
|
|
418
|
+
justifyContent="flex-end"
|
|
278
419
|
>
|
|
279
420
|
{messages.length === 0 && !isProcessing ? (
|
|
280
421
|
<WelcomeHint />
|
|
281
422
|
) : (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
423
|
+
<>
|
|
424
|
+
{hiddenAbove > 0 && (
|
|
425
|
+
<ScrollIndicator direction="above" count={hiddenAbove} />
|
|
426
|
+
)}
|
|
427
|
+
{visibleMessages.map((msg, i) => (
|
|
428
|
+
<MessageView key={hiddenAbove + i} message={msg} />
|
|
429
|
+
))}
|
|
430
|
+
{isProcessing && <ProcessingIndicator />}
|
|
431
|
+
{hiddenBelow > 0 && (
|
|
432
|
+
<ScrollIndicator direction="below" count={hiddenBelow} />
|
|
433
|
+
)}
|
|
434
|
+
</>
|
|
285
435
|
)}
|
|
286
|
-
{isProcessing && <ProcessingIndicator />}
|
|
287
436
|
</Box>
|
|
288
437
|
|
|
289
438
|
<HRule width={termSize.columns} />
|
|
@@ -312,7 +461,7 @@ function App({
|
|
|
312
461
|
</Box>
|
|
313
462
|
|
|
314
463
|
<Box paddingX={2} justifyContent="space-between">
|
|
315
|
-
<Text dimColor>{"Ctrl+C exit"}</Text>
|
|
464
|
+
<Text dimColor>{"Ctrl+C exit ↑↓/wheel scroll"}</Text>
|
|
316
465
|
<Text dimColor>
|
|
317
466
|
{userCount} {userCount === 1 ? "message" : "messages"}
|
|
318
467
|
</Text>
|