casabot 1.1.0 → 1.1.2
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 +66 -5
- package/package.json +1 -1
- package/src/tui/app.tsx +104 -10
- package/src/tui/marked-terminal.d.ts +2 -2
package/dist/tui/app.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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 } 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";
|
|
6
6
|
import Gradient from "ink-gradient";
|
|
7
7
|
import { marked } from "marked";
|
|
8
|
-
import markedTerminal from "marked-terminal";
|
|
8
|
+
import { markedTerminal } from "marked-terminal";
|
|
9
9
|
import { runAgent } from "../agent/base.js";
|
|
10
10
|
marked.use(markedTerminal());
|
|
11
11
|
function renderMarkdown(content) {
|
|
@@ -21,6 +21,27 @@ function truncateOutput(content, maxLines = 8) {
|
|
|
21
21
|
return (lines.slice(0, maxLines).join("\n") +
|
|
22
22
|
`\n … ${lines.length - maxLines} more lines`);
|
|
23
23
|
}
|
|
24
|
+
function estimateMessageLines(message, width) {
|
|
25
|
+
const contentWidth = Math.max(width - 10, 20);
|
|
26
|
+
const countLines = (text) => text.split("\n").reduce((sum, line) => sum + Math.max(1, Math.ceil((line.length || 1) / contentWidth)), 0);
|
|
27
|
+
if (message.role === "user") {
|
|
28
|
+
return 2 + countLines(message.content);
|
|
29
|
+
}
|
|
30
|
+
if (message.role === "tool") {
|
|
31
|
+
return 3 + countLines(truncateOutput(message.content));
|
|
32
|
+
}
|
|
33
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
34
|
+
let lines = 2;
|
|
35
|
+
if (message.content)
|
|
36
|
+
lines += countLines(message.content);
|
|
37
|
+
lines += 4 + (message.toolCalls?.length ?? 0);
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
40
|
+
if (message.role === "assistant") {
|
|
41
|
+
return 2 + countLines(message.content);
|
|
42
|
+
}
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
24
45
|
function HRule({ width }) {
|
|
25
46
|
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(width - 4, 10)) }) }));
|
|
26
47
|
}
|
|
@@ -71,10 +92,17 @@ function WelcomeHint() {
|
|
|
71
92
|
function ProcessingIndicator() {
|
|
72
93
|
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
94
|
}
|
|
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
|
+
// border(2) + header(3) + hrules(2) + input(3) + status(1) = 11
|
|
100
|
+
const CHROME_HEIGHT = 11;
|
|
74
101
|
function App({ provider, conversation, skills, }) {
|
|
75
102
|
const [messages, setMessages] = useState([]);
|
|
76
103
|
const [input, setInput] = useState("");
|
|
77
104
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
105
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
78
106
|
const { exit } = useApp();
|
|
79
107
|
const { stdout } = useStdout();
|
|
80
108
|
const [termSize, setTermSize] = useState({
|
|
@@ -93,6 +121,33 @@ function App({ provider, conversation, skills, }) {
|
|
|
93
121
|
stdout.off("resize", onResize);
|
|
94
122
|
};
|
|
95
123
|
}, [stdout]);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
setScrollOffset((prev) => (prev === 0 ? 0 : prev + 1));
|
|
126
|
+
}, [messages.length]);
|
|
127
|
+
const messagesHeight = Math.max(termSize.rows - CHROME_HEIGHT, 4);
|
|
128
|
+
const { visibleMessages, hiddenAbove, hiddenBelow } = useMemo(() => {
|
|
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]);
|
|
148
|
+
const maxScrollOffset = useMemo(() => {
|
|
149
|
+
return Math.max(0, messages.length - 1);
|
|
150
|
+
}, [messages.length]);
|
|
96
151
|
const handleSubmit = useCallback(async (text) => {
|
|
97
152
|
const trimmed = text.trim();
|
|
98
153
|
if (!trimmed || isProcessing)
|
|
@@ -120,11 +175,17 @@ function App({ provider, conversation, skills, }) {
|
|
|
120
175
|
if (key.ctrl && ch === "c") {
|
|
121
176
|
exit();
|
|
122
177
|
}
|
|
178
|
+
if (key.upArrow) {
|
|
179
|
+
setScrollOffset((prev) => Math.min(prev + 1, maxScrollOffset));
|
|
180
|
+
}
|
|
181
|
+
if (key.downArrow) {
|
|
182
|
+
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
183
|
+
}
|
|
123
184
|
});
|
|
124
185
|
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 }),
|
|
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: [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
187
|
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"] })] })] }));
|
|
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 ↑↓ scroll" }), _jsxs(Text, { dimColor: true, children: [userCount, " ", userCount === 1 ? "message" : "messages"] })] })] }));
|
|
128
189
|
}
|
|
129
190
|
export function startTUI(provider, conversation, skills) {
|
|
130
191
|
render(_jsx(App, { provider: provider, conversation: conversation, skills: skills }));
|
package/package.json
CHANGED
package/src/tui/app.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import React, { useState, useCallback, useEffect } from "react";
|
|
1
|
+
import React, { useState, useCallback, useEffect, useMemo } 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";
|
|
5
5
|
import Gradient from "ink-gradient";
|
|
6
6
|
import { marked } from "marked";
|
|
7
|
-
import markedTerminal from "marked-terminal";
|
|
7
|
+
import { markedTerminal } from "marked-terminal";
|
|
8
8
|
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";
|
|
@@ -26,6 +26,33 @@ function truncateOutput(content: string, maxLines = 8): string {
|
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function estimateMessageLines(message: Message, width: number): number {
|
|
30
|
+
const contentWidth = Math.max(width - 10, 20);
|
|
31
|
+
const countLines = (text: string): number =>
|
|
32
|
+
text.split("\n").reduce(
|
|
33
|
+
(sum, line) =>
|
|
34
|
+
sum + Math.max(1, Math.ceil((line.length || 1) / contentWidth)),
|
|
35
|
+
0,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (message.role === "user") {
|
|
39
|
+
return 2 + countLines(message.content);
|
|
40
|
+
}
|
|
41
|
+
if (message.role === "tool") {
|
|
42
|
+
return 3 + countLines(truncateOutput(message.content));
|
|
43
|
+
}
|
|
44
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
45
|
+
let lines = 2;
|
|
46
|
+
if (message.content) lines += countLines(message.content);
|
|
47
|
+
lines += 4 + (message.toolCalls?.length ?? 0);
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
|
50
|
+
if (message.role === "assistant") {
|
|
51
|
+
return 2 + countLines(message.content);
|
|
52
|
+
}
|
|
53
|
+
return 2;
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
function HRule({ width }: { width: number }): React.ReactElement {
|
|
30
57
|
return (
|
|
31
58
|
<Box paddingX={1}>
|
|
@@ -186,12 +213,32 @@ function ProcessingIndicator(): React.ReactElement {
|
|
|
186
213
|
);
|
|
187
214
|
}
|
|
188
215
|
|
|
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
|
+
|
|
189
233
|
interface AppProps {
|
|
190
234
|
provider: ChatProvider;
|
|
191
235
|
conversation: ConversationHistory;
|
|
192
236
|
skills: Skill[];
|
|
193
237
|
}
|
|
194
238
|
|
|
239
|
+
// border(2) + header(3) + hrules(2) + input(3) + status(1) = 11
|
|
240
|
+
const CHROME_HEIGHT = 11;
|
|
241
|
+
|
|
195
242
|
function App({
|
|
196
243
|
provider,
|
|
197
244
|
conversation,
|
|
@@ -200,6 +247,7 @@ function App({
|
|
|
200
247
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
201
248
|
const [input, setInput] = useState("");
|
|
202
249
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
250
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
203
251
|
const { exit } = useApp();
|
|
204
252
|
const { stdout } = useStdout();
|
|
205
253
|
|
|
@@ -221,6 +269,39 @@ function App({
|
|
|
221
269
|
};
|
|
222
270
|
}, [stdout]);
|
|
223
271
|
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
setScrollOffset((prev) => (prev === 0 ? 0 : prev + 1));
|
|
274
|
+
}, [messages.length]);
|
|
275
|
+
|
|
276
|
+
const messagesHeight = Math.max(termSize.rows - CHROME_HEIGHT, 4);
|
|
277
|
+
|
|
278
|
+
const { visibleMessages, hiddenAbove, hiddenBelow } = useMemo(() => {
|
|
279
|
+
if (messages.length === 0) {
|
|
280
|
+
return { visibleMessages: [] as Message[], hiddenAbove: 0, hiddenBelow: 0 };
|
|
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]);
|
|
300
|
+
|
|
301
|
+
const maxScrollOffset = useMemo(() => {
|
|
302
|
+
return Math.max(0, messages.length - 1);
|
|
303
|
+
}, [messages.length]);
|
|
304
|
+
|
|
224
305
|
const handleSubmit = useCallback(
|
|
225
306
|
async (text: string) => {
|
|
226
307
|
const trimmed = text.trim();
|
|
@@ -254,6 +335,12 @@ function App({
|
|
|
254
335
|
if (key.ctrl && ch === "c") {
|
|
255
336
|
exit();
|
|
256
337
|
}
|
|
338
|
+
if (key.upArrow) {
|
|
339
|
+
setScrollOffset((prev) => Math.min(prev + 1, maxScrollOffset));
|
|
340
|
+
}
|
|
341
|
+
if (key.downArrow) {
|
|
342
|
+
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
343
|
+
}
|
|
257
344
|
});
|
|
258
345
|
|
|
259
346
|
const userCount = messages.filter((m) => m.role === "user").length;
|
|
@@ -271,19 +358,26 @@ function App({
|
|
|
271
358
|
|
|
272
359
|
<Box
|
|
273
360
|
flexDirection="column"
|
|
274
|
-
|
|
361
|
+
height={messagesHeight}
|
|
275
362
|
overflowY="hidden"
|
|
276
|
-
justifyContent=
|
|
277
|
-
paddingBottom={1}
|
|
363
|
+
justifyContent="flex-end"
|
|
278
364
|
>
|
|
279
365
|
{messages.length === 0 && !isProcessing ? (
|
|
280
366
|
<WelcomeHint />
|
|
281
367
|
) : (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
368
|
+
<>
|
|
369
|
+
{hiddenAbove > 0 && (
|
|
370
|
+
<ScrollIndicator direction="above" count={hiddenAbove} />
|
|
371
|
+
)}
|
|
372
|
+
{visibleMessages.map((msg, i) => (
|
|
373
|
+
<MessageView key={hiddenAbove + i} message={msg} />
|
|
374
|
+
))}
|
|
375
|
+
{isProcessing && <ProcessingIndicator />}
|
|
376
|
+
{hiddenBelow > 0 && (
|
|
377
|
+
<ScrollIndicator direction="below" count={hiddenBelow} />
|
|
378
|
+
)}
|
|
379
|
+
</>
|
|
285
380
|
)}
|
|
286
|
-
{isProcessing && <ProcessingIndicator />}
|
|
287
381
|
</Box>
|
|
288
382
|
|
|
289
383
|
<HRule width={termSize.columns} />
|
|
@@ -312,7 +406,7 @@ function App({
|
|
|
312
406
|
</Box>
|
|
313
407
|
|
|
314
408
|
<Box paddingX={2} justifyContent="space-between">
|
|
315
|
-
<Text dimColor>{"Ctrl+C exit"}</Text>
|
|
409
|
+
<Text dimColor>{"Ctrl+C exit ↑↓ scroll"}</Text>
|
|
316
410
|
<Text dimColor>
|
|
317
411
|
{userCount} {userCount === 1 ? "message" : "messages"}
|
|
318
412
|
</Text>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare module "marked-terminal" {
|
|
2
2
|
import type { MarkedExtension } from "marked";
|
|
3
|
-
function markedTerminal(options?: Record<string, unknown>): MarkedExtension;
|
|
4
|
-
export default
|
|
3
|
+
export function markedTerminal(options?: Record<string, unknown>): MarkedExtension;
|
|
4
|
+
export default function Renderer(options?: Record<string, unknown>, highlightOptions?: Record<string, unknown>): void;
|
|
5
5
|
}
|