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 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
- const result = marked.parse(content);
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 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;
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 { 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]);
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
- setScrollOffset((prev) => Math.min(prev + 1, maxScrollOffset));
177
+ scrollUp();
180
178
  }
181
179
  if (key.downArrow) {
182
- setScrollOffset((prev) => Math.max(prev - 1, 0));
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: [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) => {
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 ↑↓ scroll" }), _jsxs(Text, { dimColor: true, children: [userCount, " ", userCount === 1 ? "message" : "messages"] })] })] }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "casabot",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
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,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(markedTerminal());
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
- const result = marked.parse(content);
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 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
- );
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
- 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;
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 { 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]);
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
- setScrollOffset((prev) => Math.min(prev + 1, maxScrollOffset));
332
+ scrollUp();
340
333
  }
341
334
  if (key.downArrow) {
342
- setScrollOffset((prev) => Math.max(prev - 1, 0));
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={hiddenAbove + i} message={msg} />
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 ↑↓ scroll"}</Text>
396
+ <Text dimColor>{"Ctrl+C exit ↑↓/wheel scroll"}</Text>
410
397
  <Text dimColor>
411
398
  {userCount} {userCount === 1 ? "message" : "messages"}
412
399
  </Text>