casabot 1.1.2 → 1.1.4
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 +52 -54
- package/package.json +1 -1
- package/src/tui/app.tsx +65 -78
package/dist/tui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
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,26 +22,39 @@ function truncateOutput(content, maxLines = 8) {
|
|
|
21
22
|
return (lines.slice(0, maxLines).join("\n") +
|
|
22
23
|
`\n … ${lines.length - maxLines} more lines`);
|
|
23
24
|
}
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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]);
|
|
44
58
|
}
|
|
45
59
|
function HRule({ width }) {
|
|
46
60
|
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(width - 4, 10)) }) }));
|
|
@@ -92,10 +106,6 @@ function WelcomeHint() {
|
|
|
92
106
|
function ProcessingIndicator() {
|
|
93
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…" })] }));
|
|
94
108
|
}
|
|
95
|
-
function ScrollIndicator({ direction, count, }) {
|
|
96
|
-
const arrow = direction === "above" ? "▲" : "▼";
|
|
97
|
-
return (_jsx(Box, { justifyContent: "center", paddingX: 2, children: _jsx(Text, { dimColor: true, children: `${arrow} ${count} more ${count === 1 ? "message" : "messages"} ${direction}` }) }));
|
|
98
|
-
}
|
|
99
109
|
// border(2) + header(3) + hrules(2) + input(3) + status(1) = 11
|
|
100
110
|
const CHROME_HEIGHT = 11;
|
|
101
111
|
function App({ provider, conversation, skills, }) {
|
|
@@ -125,26 +135,7 @@ function App({ provider, conversation, skills, }) {
|
|
|
125
135
|
setScrollOffset((prev) => (prev === 0 ? 0 : prev + 1));
|
|
126
136
|
}, [messages.length]);
|
|
127
137
|
const messagesHeight = Math.max(termSize.rows - CHROME_HEIGHT, 4);
|
|
128
|
-
const
|
|
129
|
-
if (messages.length === 0) {
|
|
130
|
-
return { visibleMessages: [], hiddenAbove: 0, hiddenBelow: 0 };
|
|
131
|
-
}
|
|
132
|
-
const endIndex = messages.length - scrollOffset;
|
|
133
|
-
let usedLines = isProcessing ? 2 : 0;
|
|
134
|
-
let startIndex = endIndex;
|
|
135
|
-
for (let i = endIndex - 1; i >= 0; i--) {
|
|
136
|
-
const lines = estimateMessageLines(messages[i], termSize.columns);
|
|
137
|
-
if (usedLines + lines > messagesHeight && startIndex < endIndex)
|
|
138
|
-
break;
|
|
139
|
-
usedLines += lines;
|
|
140
|
-
startIndex = i;
|
|
141
|
-
}
|
|
142
|
-
return {
|
|
143
|
-
visibleMessages: messages.slice(startIndex, endIndex),
|
|
144
|
-
hiddenAbove: startIndex,
|
|
145
|
-
hiddenBelow: scrollOffset,
|
|
146
|
-
};
|
|
147
|
-
}, [messages, scrollOffset, messagesHeight, termSize.columns, isProcessing]);
|
|
138
|
+
const visibleMessages = useMemo(() => messages.slice(0, messages.length - scrollOffset), [messages, scrollOffset]);
|
|
148
139
|
const maxScrollOffset = useMemo(() => {
|
|
149
140
|
return Math.max(0, messages.length - 1);
|
|
150
141
|
}, [messages.length]);
|
|
@@ -171,21 +162,28 @@ function App({ provider, conversation, skills, }) {
|
|
|
171
162
|
}
|
|
172
163
|
setIsProcessing(false);
|
|
173
164
|
}, [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);
|
|
174
172
|
useInput((ch, key) => {
|
|
175
173
|
if (key.ctrl && ch === "c") {
|
|
176
174
|
exit();
|
|
177
175
|
}
|
|
178
176
|
if (key.upArrow) {
|
|
179
|
-
|
|
177
|
+
scrollUp();
|
|
180
178
|
}
|
|
181
179
|
if (key.downArrow) {
|
|
182
|
-
|
|
180
|
+
scrollDown();
|
|
183
181
|
}
|
|
184
182
|
});
|
|
185
183
|
const userCount = messages.filter((m) => m.role === "user").length;
|
|
186
|
-
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: [
|
|
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) => {
|
|
187
185
|
handleSubmit(val).catch(() => { });
|
|
188
|
-
}, 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
|
|
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"] })] })] }));
|
|
189
187
|
}
|
|
190
188
|
export function startTUI(provider, conversation, skills) {
|
|
191
189
|
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, useMemo } 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,31 +30,45 @@ function truncateOutput(content: string, maxLines = 8): string {
|
|
|
26
30
|
);
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
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);
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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]);
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
function HRule({ width }: { width: number }): React.ReactElement {
|
|
@@ -213,23 +231,6 @@ function ProcessingIndicator(): React.ReactElement {
|
|
|
213
231
|
);
|
|
214
232
|
}
|
|
215
233
|
|
|
216
|
-
function ScrollIndicator({
|
|
217
|
-
direction,
|
|
218
|
-
count,
|
|
219
|
-
}: {
|
|
220
|
-
direction: "above" | "below";
|
|
221
|
-
count: number;
|
|
222
|
-
}): React.ReactElement {
|
|
223
|
-
const arrow = direction === "above" ? "▲" : "▼";
|
|
224
|
-
return (
|
|
225
|
-
<Box justifyContent="center" paddingX={2}>
|
|
226
|
-
<Text dimColor>
|
|
227
|
-
{`${arrow} ${count} more ${count === 1 ? "message" : "messages"} ${direction}`}
|
|
228
|
-
</Text>
|
|
229
|
-
</Box>
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
234
|
interface AppProps {
|
|
234
235
|
provider: ChatProvider;
|
|
235
236
|
conversation: ConversationHistory;
|
|
@@ -275,28 +276,10 @@ function App({
|
|
|
275
276
|
|
|
276
277
|
const messagesHeight = Math.max(termSize.rows - CHROME_HEIGHT, 4);
|
|
277
278
|
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const endIndex = messages.length - scrollOffset;
|
|
284
|
-
let usedLines = isProcessing ? 2 : 0;
|
|
285
|
-
let startIndex = endIndex;
|
|
286
|
-
|
|
287
|
-
for (let i = endIndex - 1; i >= 0; i--) {
|
|
288
|
-
const lines = estimateMessageLines(messages[i], termSize.columns);
|
|
289
|
-
if (usedLines + lines > messagesHeight && startIndex < endIndex) break;
|
|
290
|
-
usedLines += lines;
|
|
291
|
-
startIndex = i;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
visibleMessages: messages.slice(startIndex, endIndex),
|
|
296
|
-
hiddenAbove: startIndex,
|
|
297
|
-
hiddenBelow: scrollOffset,
|
|
298
|
-
};
|
|
299
|
-
}, [messages, scrollOffset, messagesHeight, termSize.columns, isProcessing]);
|
|
279
|
+
const visibleMessages = useMemo(
|
|
280
|
+
() => messages.slice(0, messages.length - scrollOffset),
|
|
281
|
+
[messages, scrollOffset],
|
|
282
|
+
);
|
|
300
283
|
|
|
301
284
|
const maxScrollOffset = useMemo(() => {
|
|
302
285
|
return Math.max(0, messages.length - 1);
|
|
@@ -331,15 +314,25 @@ function App({
|
|
|
331
314
|
[isProcessing, provider, conversation, skills],
|
|
332
315
|
);
|
|
333
316
|
|
|
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
|
+
|
|
334
327
|
useInput((ch, key) => {
|
|
335
328
|
if (key.ctrl && ch === "c") {
|
|
336
329
|
exit();
|
|
337
330
|
}
|
|
338
331
|
if (key.upArrow) {
|
|
339
|
-
|
|
332
|
+
scrollUp();
|
|
340
333
|
}
|
|
341
334
|
if (key.downArrow) {
|
|
342
|
-
|
|
335
|
+
scrollDown();
|
|
343
336
|
}
|
|
344
337
|
});
|
|
345
338
|
|
|
@@ -366,16 +359,10 @@ function App({
|
|
|
366
359
|
<WelcomeHint />
|
|
367
360
|
) : (
|
|
368
361
|
<>
|
|
369
|
-
{hiddenAbove > 0 && (
|
|
370
|
-
<ScrollIndicator direction="above" count={hiddenAbove} />
|
|
371
|
-
)}
|
|
372
362
|
{visibleMessages.map((msg, i) => (
|
|
373
|
-
<MessageView key={
|
|
363
|
+
<MessageView key={i} message={msg} />
|
|
374
364
|
))}
|
|
375
365
|
{isProcessing && <ProcessingIndicator />}
|
|
376
|
-
{hiddenBelow > 0 && (
|
|
377
|
-
<ScrollIndicator direction="below" count={hiddenBelow} />
|
|
378
|
-
)}
|
|
379
366
|
</>
|
|
380
367
|
)}
|
|
381
368
|
</Box>
|
|
@@ -406,7 +393,7 @@ function App({
|
|
|
406
393
|
</Box>
|
|
407
394
|
|
|
408
395
|
<Box paddingX={2} justifyContent="space-between">
|
|
409
|
-
<Text dimColor>{"Ctrl+C exit
|
|
396
|
+
<Text dimColor>{"Ctrl+C exit ↑↓/wheel scroll"}</Text>
|
|
410
397
|
<Text dimColor>
|
|
411
398
|
{userCount} {userCount === 1 ? "message" : "messages"}
|
|
412
399
|
</Text>
|