botholomew 0.3.1 → 0.3.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/package.json +2 -2
- package/src/chat/agent.ts +62 -16
- package/src/chat/session.ts +19 -6
- package/src/cli.ts +2 -0
- package/src/commands/thread.ts +180 -0
- package/src/config/schemas.ts +3 -1
- package/src/daemon/large-results.ts +15 -3
- package/src/daemon/llm.ts +22 -7
- package/src/daemon/prompt.ts +1 -9
- package/src/daemon/tick.ts +9 -0
- package/src/db/threads.ts +17 -0
- package/src/init/templates.ts +1 -0
- package/src/tools/context/read-large-result.ts +2 -1
- package/src/tools/context/search.ts +2 -0
- package/src/tools/context/update-beliefs.ts +2 -0
- package/src/tools/context/update-goals.ts +2 -0
- package/src/tools/dir/create.ts +3 -2
- package/src/tools/dir/list.ts +2 -1
- package/src/tools/dir/size.ts +2 -1
- package/src/tools/dir/tree.ts +3 -2
- package/src/tools/file/copy.ts +2 -1
- package/src/tools/file/count-lines.ts +2 -1
- package/src/tools/file/delete.ts +3 -2
- package/src/tools/file/edit.ts +2 -1
- package/src/tools/file/exists.ts +2 -1
- package/src/tools/file/info.ts +2 -0
- package/src/tools/file/move.ts +2 -1
- package/src/tools/file/read.ts +2 -1
- package/src/tools/file/write.ts +3 -2
- package/src/tools/mcp/exec.ts +70 -3
- package/src/tools/mcp/info.ts +8 -0
- package/src/tools/mcp/list-tools.ts +18 -6
- package/src/tools/mcp/search.ts +38 -10
- package/src/tools/registry.ts +2 -0
- package/src/tools/schedule/create.ts +2 -0
- package/src/tools/schedule/list.ts +2 -0
- package/src/tools/search/grep.ts +3 -2
- package/src/tools/search/semantic.ts +2 -0
- package/src/tools/task/complete.ts +2 -0
- package/src/tools/task/create.ts +17 -4
- package/src/tools/task/fail.ts +2 -0
- package/src/tools/task/list.ts +2 -0
- package/src/tools/task/update.ts +87 -0
- package/src/tools/task/view.ts +3 -1
- package/src/tools/task/wait.ts +2 -0
- package/src/tools/thread/list.ts +2 -0
- package/src/tools/thread/view.ts +3 -1
- package/src/tools/tool.ts +5 -3
- package/src/tui/App.tsx +209 -82
- package/src/tui/components/ContextPanel.tsx +6 -3
- package/src/tui/components/HelpPanel.tsx +52 -3
- package/src/tui/components/InputBar.tsx +125 -59
- package/src/tui/components/MessageList.tsx +40 -75
- package/src/tui/components/StatusBar.tsx +9 -8
- package/src/tui/components/TabBar.tsx +4 -2
- package/src/tui/components/TaskPanel.tsx +409 -0
- package/src/tui/components/ThreadPanel.tsx +541 -0
- package/src/tui/components/ToolCall.tsx +36 -3
- package/src/tui/components/ToolPanel.tsx +40 -31
- package/src/tui/theme.ts +20 -3
- package/src/utils/title.ts +47 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
|
-
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { theme } from "../theme.ts";
|
|
2
|
+
import { memo, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { ansi, theme } from "../theme.ts";
|
|
4
4
|
import { resolveToolDisplay, type ToolCallData } from "./ToolCall.tsx";
|
|
5
5
|
|
|
6
6
|
interface ToolPanelProps {
|
|
@@ -10,16 +10,6 @@ interface ToolPanelProps {
|
|
|
10
10
|
|
|
11
11
|
const SIDEBAR_WIDTH = 42;
|
|
12
12
|
|
|
13
|
-
// ANSI escape helpers
|
|
14
|
-
const RESET = "\x1b[0m";
|
|
15
|
-
const BOLD = "\x1b[1m";
|
|
16
|
-
const DIM = "\x1b[2m";
|
|
17
|
-
const CYAN = "\x1b[36m";
|
|
18
|
-
const GREEN = "\x1b[32m";
|
|
19
|
-
const YELLOW = "\x1b[33m";
|
|
20
|
-
const MAGENTA = "\x1b[35m";
|
|
21
|
-
const BLUE = "\x1b[34m";
|
|
22
|
-
|
|
23
13
|
/** Try to parse a string as JSON; returns the parsed value or undefined on failure */
|
|
24
14
|
function tryParseJson(str: string): unknown | undefined {
|
|
25
15
|
try {
|
|
@@ -37,10 +27,10 @@ function colorizeJson(str: string): string {
|
|
|
37
27
|
}
|
|
38
28
|
|
|
39
29
|
function colorizeValue(value: unknown, indent: number): string {
|
|
40
|
-
if (value === null) return `${
|
|
30
|
+
if (value === null) return `${ansi.toolName}null${ansi.reset}`;
|
|
41
31
|
if (typeof value === "boolean")
|
|
42
|
-
return `${
|
|
43
|
-
if (typeof value === "number") return `${
|
|
32
|
+
return `${ansi.toolName}${value ? "true" : "false"}${ansi.reset}`;
|
|
33
|
+
if (typeof value === "number") return `${ansi.accent}${value}${ansi.reset}`;
|
|
44
34
|
if (typeof value === "string") {
|
|
45
35
|
// Try to unwrap stringified JSON (common in tool results)
|
|
46
36
|
const inner = tryParseJson(value);
|
|
@@ -48,7 +38,7 @@ function colorizeValue(value: unknown, indent: number): string {
|
|
|
48
38
|
return colorizeValue(inner, indent);
|
|
49
39
|
}
|
|
50
40
|
const escaped = JSON.stringify(value);
|
|
51
|
-
return `${
|
|
41
|
+
return `${ansi.success}${escaped}${ansi.reset}`;
|
|
52
42
|
}
|
|
53
43
|
|
|
54
44
|
const pad = " ".repeat(indent);
|
|
@@ -67,7 +57,7 @@ function colorizeValue(value: unknown, indent: number): string {
|
|
|
67
57
|
if (entries.length === 0) return "{}";
|
|
68
58
|
const lines = entries.map(
|
|
69
59
|
([k, v]) =>
|
|
70
|
-
`${innerPad}${
|
|
60
|
+
`${innerPad}${ansi.info}${JSON.stringify(k)}${ansi.reset}: ${colorizeValue(v, indent + 1)}`,
|
|
71
61
|
);
|
|
72
62
|
return `{\n${lines.join(",\n")}\n${pad}}`;
|
|
73
63
|
}
|
|
@@ -88,26 +78,36 @@ function buildDetailAnsi(tool: ToolCallData): string {
|
|
|
88
78
|
tool.name,
|
|
89
79
|
tool.input,
|
|
90
80
|
);
|
|
91
|
-
lines.push(`${
|
|
81
|
+
lines.push(`${ansi.bold}${ansi.info}${displayName}${ansi.reset}`);
|
|
92
82
|
if (tool.name === "mcp_exec") {
|
|
93
|
-
lines.push(`${
|
|
83
|
+
lines.push(`${ansi.dim}via mcp_exec${ansi.reset}`);
|
|
94
84
|
}
|
|
95
|
-
lines.push(`${
|
|
85
|
+
lines.push(`${ansi.dim}Time: ${time}${ansi.reset}`);
|
|
96
86
|
if (tool.running) {
|
|
97
|
-
lines.push(`${
|
|
87
|
+
lines.push(`${ansi.accent}⟳ running${ansi.reset}`);
|
|
98
88
|
}
|
|
99
89
|
lines.push("");
|
|
100
90
|
|
|
101
|
-
lines.push(`${
|
|
91
|
+
lines.push(`${ansi.bold}${ansi.primary}Input${ansi.reset}`);
|
|
102
92
|
lines.push(colorizeJson(displayInput));
|
|
103
93
|
lines.push("");
|
|
104
94
|
|
|
105
95
|
if (tool.output) {
|
|
106
|
-
|
|
107
|
-
|
|
96
|
+
if (tool.isError) {
|
|
97
|
+
lines.push(`${ansi.bold}${ansi.error}Error${ansi.reset}`);
|
|
98
|
+
lines.push(`${ansi.error}${colorizeJson(tool.output)}${ansi.reset}`);
|
|
99
|
+
} else {
|
|
100
|
+
lines.push(`${ansi.bold}${ansi.primary}Output${ansi.reset}`);
|
|
101
|
+
if (tool.largeResult) {
|
|
102
|
+
lines.push(
|
|
103
|
+
`${ansi.accent}Paginated for LLM: ${tool.largeResult.chars.toLocaleString()} chars, ${tool.largeResult.pages} page(s) — stored as ${tool.largeResult.id}${ansi.reset}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
lines.push(colorizeJson(tool.output));
|
|
107
|
+
}
|
|
108
108
|
} else if (!tool.running) {
|
|
109
|
-
lines.push(`${
|
|
110
|
-
lines.push(`${
|
|
109
|
+
lines.push(`${ansi.bold}${ansi.primary}Output${ansi.reset}`);
|
|
110
|
+
lines.push(`${ansi.dim}(no output)${ansi.reset}`);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
return lines.join("\n");
|
|
@@ -115,7 +115,10 @@ function buildDetailAnsi(tool: ToolCallData): string {
|
|
|
115
115
|
|
|
116
116
|
const PAGE_SCROLL_LINES = 10;
|
|
117
117
|
|
|
118
|
-
export
|
|
118
|
+
export const ToolPanel = memo(function ToolPanel({
|
|
119
|
+
toolCalls,
|
|
120
|
+
isActive,
|
|
121
|
+
}: ToolPanelProps) {
|
|
119
122
|
const { stdout } = useStdout();
|
|
120
123
|
const termRows = stdout?.rows ?? 24;
|
|
121
124
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -261,7 +264,7 @@ export function ToolPanel({ toolCalls, isActive }: ToolPanelProps) {
|
|
|
261
264
|
{sidebarVisible.map((tc, vi) => {
|
|
262
265
|
const i = vi + sidebarScrollOffset;
|
|
263
266
|
const isSelected = i === selectedIndex;
|
|
264
|
-
const icon = tc.running ? "⟳" : "✔";
|
|
267
|
+
const icon = tc.running ? "⟳" : tc.isError ? "✘" : "✔";
|
|
265
268
|
const time = tc.timestamp.toLocaleTimeString([], {
|
|
266
269
|
hour: "2-digit",
|
|
267
270
|
minute: "2-digit",
|
|
@@ -273,7 +276,7 @@ export function ToolPanel({ toolCalls, isActive }: ToolPanelProps) {
|
|
|
273
276
|
? `${displayName.slice(0, maxName - 1)}…`
|
|
274
277
|
: displayName;
|
|
275
278
|
return (
|
|
276
|
-
<Box key={
|
|
279
|
+
<Box key={tc.id} paddingX={1}>
|
|
277
280
|
<Text
|
|
278
281
|
backgroundColor={isSelected ? theme.selectionBg : undefined}
|
|
279
282
|
bold={isSelected}
|
|
@@ -288,7 +291,13 @@ export function ToolPanel({ toolCalls, isActive }: ToolPanelProps) {
|
|
|
288
291
|
>
|
|
289
292
|
{isSelected ? "▸" : " "}{" "}
|
|
290
293
|
<Text
|
|
291
|
-
color={
|
|
294
|
+
color={
|
|
295
|
+
tc.running
|
|
296
|
+
? theme.accent
|
|
297
|
+
: tc.isError
|
|
298
|
+
? theme.error
|
|
299
|
+
: theme.muted
|
|
300
|
+
}
|
|
292
301
|
bold={false}
|
|
293
302
|
>
|
|
294
303
|
{icon}
|
|
@@ -330,4 +339,4 @@ export function ToolPanel({ toolCalls, isActive }: ToolPanelProps) {
|
|
|
330
339
|
</Box>
|
|
331
340
|
</Box>
|
|
332
341
|
);
|
|
333
|
-
}
|
|
342
|
+
});
|
package/src/tui/theme.ts
CHANGED
|
@@ -29,8 +29,11 @@ function detectDarkBackground(): boolean {
|
|
|
29
29
|
["read", "-g", "AppleInterfaceStyle"],
|
|
30
30
|
{ encoding: "utf-8", timeout: 500 },
|
|
31
31
|
);
|
|
32
|
-
// Returns "Dark" in dark mode
|
|
33
|
-
|
|
32
|
+
// Returns "Dark" in dark mode, "Light" or exit 1 in light mode
|
|
33
|
+
// Only trust the result if the command succeeded (status 0)
|
|
34
|
+
if (result.status === 0) {
|
|
35
|
+
return result.stdout?.trim() === "Dark";
|
|
36
|
+
}
|
|
34
37
|
} catch {
|
|
35
38
|
// fall through to default
|
|
36
39
|
}
|
|
@@ -44,7 +47,7 @@ const isDark = detectDarkBackground();
|
|
|
44
47
|
export const theme = {
|
|
45
48
|
accent: isDark ? "yellow" : "#B8860B",
|
|
46
49
|
accentBorder: isDark ? "yellow" : "#B8860B",
|
|
47
|
-
userBg: isDark ? "#
|
|
50
|
+
userBg: isDark ? "#2a5a8c" : "#d0e0f0",
|
|
48
51
|
selectionBg: isDark ? "#333" : "#ddd",
|
|
49
52
|
success: "green",
|
|
50
53
|
error: "red",
|
|
@@ -54,5 +57,19 @@ export const theme = {
|
|
|
54
57
|
muted: "gray",
|
|
55
58
|
} as const;
|
|
56
59
|
|
|
60
|
+
/** ANSI escape codes for raw string building (detail panes, etc.) */
|
|
61
|
+
export const ansi = {
|
|
62
|
+
reset: "\x1b[0m",
|
|
63
|
+
bold: "\x1b[1m",
|
|
64
|
+
dim: "\x1b[2m",
|
|
65
|
+
success: "\x1b[32m",
|
|
66
|
+
error: "\x1b[31m",
|
|
67
|
+
info: "\x1b[36m",
|
|
68
|
+
primary: "\x1b[34m",
|
|
69
|
+
accent: "\x1b[33m",
|
|
70
|
+
toolName: "\x1b[35m",
|
|
71
|
+
muted: "\x1b[2m",
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
57
74
|
// Exported for testing
|
|
58
75
|
export { detectDarkBackground };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
3
|
+
import type { DbConnection } from "../db/connection.ts";
|
|
4
|
+
import { updateThreadTitle } from "../db/threads.ts";
|
|
5
|
+
import { logger } from "./logger.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate a short title for a thread using the chunker model (Haiku).
|
|
9
|
+
* Fire-and-forget — errors are logged at debug level and never propagated.
|
|
10
|
+
*/
|
|
11
|
+
export async function generateThreadTitle(
|
|
12
|
+
config: Required<BotholomewConfig>,
|
|
13
|
+
conn: DbConnection,
|
|
14
|
+
threadId: string,
|
|
15
|
+
context: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
const client = new Anthropic({
|
|
19
|
+
apiKey: config.anthropic_api_key || undefined,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const response = await client.messages.create({
|
|
23
|
+
model: config.chunker_model,
|
|
24
|
+
max_tokens: 50,
|
|
25
|
+
system:
|
|
26
|
+
"You are a title generator. The user will provide the first message from a conversation. Output a short descriptive title (5-8 words). Output ONLY the title, nothing else.",
|
|
27
|
+
messages: [
|
|
28
|
+
{
|
|
29
|
+
role: "user",
|
|
30
|
+
content: `Generate a title for this message:\n\n"${context}"`,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const title = response.content
|
|
36
|
+
.filter((b) => b.type === "text")
|
|
37
|
+
.map((b) => b.text)
|
|
38
|
+
.join("")
|
|
39
|
+
.trim();
|
|
40
|
+
|
|
41
|
+
if (title) {
|
|
42
|
+
await updateThreadTitle(conn, threadId, title);
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
logger.warn(`Failed to generate thread title: ${err}`);
|
|
46
|
+
}
|
|
47
|
+
}
|