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
package/src/tools/task/fail.ts
CHANGED
|
@@ -7,6 +7,7 @@ const inputSchema = z.object({
|
|
|
7
7
|
|
|
8
8
|
const outputSchema = z.object({
|
|
9
9
|
message: z.string(),
|
|
10
|
+
is_error: z.boolean(),
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
export const failTaskTool = {
|
|
@@ -18,5 +19,6 @@ export const failTaskTool = {
|
|
|
18
19
|
outputSchema,
|
|
19
20
|
execute: async (input) => ({
|
|
20
21
|
message: `Task failed: ${input.reason}`,
|
|
22
|
+
is_error: false,
|
|
21
23
|
}),
|
|
22
24
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/task/list.ts
CHANGED
|
@@ -20,6 +20,7 @@ const outputSchema = z.object({
|
|
|
20
20
|
}),
|
|
21
21
|
),
|
|
22
22
|
count: z.number(),
|
|
23
|
+
is_error: z.boolean(),
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
export const listTasksTool = {
|
|
@@ -44,6 +45,7 @@ export const listTasksTool = {
|
|
|
44
45
|
created_at: t.created_at.toISOString(),
|
|
45
46
|
})),
|
|
46
47
|
count: tasks.length,
|
|
48
|
+
is_error: false,
|
|
47
49
|
};
|
|
48
50
|
},
|
|
49
51
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getTask, TASK_PRIORITIES, updateTask } from "../../db/tasks.ts";
|
|
3
|
+
import { logger } from "../../utils/logger.ts";
|
|
4
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
5
|
+
|
|
6
|
+
const inputSchema = z.object({
|
|
7
|
+
id: z.string().describe("ID of the task to update"),
|
|
8
|
+
name: z.string().optional().describe("Updated task name"),
|
|
9
|
+
description: z.string().optional().describe("Updated task description"),
|
|
10
|
+
priority: z.enum(TASK_PRIORITIES).optional().describe("Updated priority"),
|
|
11
|
+
blocked_by: z
|
|
12
|
+
.array(z.string())
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Replacement list of task IDs that must complete first"),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const outputSchema = z.object({
|
|
18
|
+
task: z
|
|
19
|
+
.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
name: z.string(),
|
|
22
|
+
description: z.string(),
|
|
23
|
+
status: z.string(),
|
|
24
|
+
priority: z.string(),
|
|
25
|
+
blocked_by: z.array(z.string()),
|
|
26
|
+
updated_at: z.string(),
|
|
27
|
+
})
|
|
28
|
+
.nullable(),
|
|
29
|
+
message: z.string(),
|
|
30
|
+
is_error: z.boolean(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const updateTaskTool = {
|
|
34
|
+
name: "update_task",
|
|
35
|
+
description:
|
|
36
|
+
"Update a pending task's name, description, priority, or dependencies. Only pending tasks can be updated.",
|
|
37
|
+
group: "task",
|
|
38
|
+
inputSchema,
|
|
39
|
+
outputSchema,
|
|
40
|
+
execute: async (input, ctx) => {
|
|
41
|
+
const existing = await getTask(ctx.conn, input.id);
|
|
42
|
+
if (!existing) {
|
|
43
|
+
return {
|
|
44
|
+
task: null,
|
|
45
|
+
message: `Task ${input.id} not found`,
|
|
46
|
+
is_error: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (existing.status !== "pending") {
|
|
50
|
+
return {
|
|
51
|
+
task: null,
|
|
52
|
+
message: `Cannot update task ${input.id}: only pending tasks can be updated (current status: ${existing.status})`,
|
|
53
|
+
is_error: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const updated = await updateTask(ctx.conn, input.id, {
|
|
58
|
+
name: input.name,
|
|
59
|
+
description: input.description,
|
|
60
|
+
priority: input.priority,
|
|
61
|
+
blocked_by: input.blocked_by,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!updated) {
|
|
65
|
+
return {
|
|
66
|
+
task: null,
|
|
67
|
+
message: `Failed to update task ${input.id}`,
|
|
68
|
+
is_error: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.info(`Updated task: ${updated.name} (${updated.id})`);
|
|
73
|
+
return {
|
|
74
|
+
task: {
|
|
75
|
+
id: updated.id,
|
|
76
|
+
name: updated.name,
|
|
77
|
+
description: updated.description,
|
|
78
|
+
status: updated.status,
|
|
79
|
+
priority: updated.priority,
|
|
80
|
+
blocked_by: updated.blocked_by,
|
|
81
|
+
updated_at: updated.updated_at.toISOString(),
|
|
82
|
+
},
|
|
83
|
+
message: `Updated task "${updated.name}"`,
|
|
84
|
+
is_error: false,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/task/view.ts
CHANGED
|
@@ -22,6 +22,7 @@ const outputSchema = z.object({
|
|
|
22
22
|
updated_at: z.string(),
|
|
23
23
|
})
|
|
24
24
|
.nullable(),
|
|
25
|
+
is_error: z.boolean(),
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
export const viewTaskTool = {
|
|
@@ -32,7 +33,7 @@ export const viewTaskTool = {
|
|
|
32
33
|
outputSchema,
|
|
33
34
|
execute: async (input, ctx) => {
|
|
34
35
|
const task = await getTask(ctx.conn, input.id);
|
|
35
|
-
if (!task) return { task: null };
|
|
36
|
+
if (!task) return { task: null, is_error: true };
|
|
36
37
|
return {
|
|
37
38
|
task: {
|
|
38
39
|
id: task.id,
|
|
@@ -47,6 +48,7 @@ export const viewTaskTool = {
|
|
|
47
48
|
created_at: task.created_at.toISOString(),
|
|
48
49
|
updated_at: task.updated_at.toISOString(),
|
|
49
50
|
},
|
|
51
|
+
is_error: false,
|
|
50
52
|
};
|
|
51
53
|
},
|
|
52
54
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/task/wait.ts
CHANGED
|
@@ -7,6 +7,7 @@ const inputSchema = z.object({
|
|
|
7
7
|
|
|
8
8
|
const outputSchema = z.object({
|
|
9
9
|
message: z.string(),
|
|
10
|
+
is_error: z.boolean(),
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
export const waitTaskTool = {
|
|
@@ -19,5 +20,6 @@ export const waitTaskTool = {
|
|
|
19
20
|
outputSchema,
|
|
20
21
|
execute: async (input) => ({
|
|
21
22
|
message: `Task waiting: ${input.reason}`,
|
|
23
|
+
is_error: false,
|
|
22
24
|
}),
|
|
23
25
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/thread/list.ts
CHANGED
|
@@ -22,6 +22,7 @@ const outputSchema = z.object({
|
|
|
22
22
|
}),
|
|
23
23
|
),
|
|
24
24
|
count: z.number(),
|
|
25
|
+
is_error: z.boolean(),
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
export const listThreadsTool = {
|
|
@@ -45,6 +46,7 @@ export const listThreadsTool = {
|
|
|
45
46
|
ended_at: t.ended_at?.toISOString() ?? null,
|
|
46
47
|
})),
|
|
47
48
|
count: threads.length,
|
|
49
|
+
is_error: false,
|
|
48
50
|
};
|
|
49
51
|
},
|
|
50
52
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/thread/view.ts
CHANGED
|
@@ -28,6 +28,7 @@ const outputSchema = z.object({
|
|
|
28
28
|
created_at: z.string(),
|
|
29
29
|
}),
|
|
30
30
|
),
|
|
31
|
+
is_error: z.boolean(),
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
export const viewThreadTool = {
|
|
@@ -39,7 +40,7 @@ export const viewThreadTool = {
|
|
|
39
40
|
outputSchema,
|
|
40
41
|
execute: async (input, ctx) => {
|
|
41
42
|
const result = await getThread(ctx.conn, input.id);
|
|
42
|
-
if (!result) return { thread: null, interactions: [] };
|
|
43
|
+
if (!result) return { thread: null, interactions: [], is_error: false };
|
|
43
44
|
return {
|
|
44
45
|
thread: {
|
|
45
46
|
id: result.thread.id,
|
|
@@ -58,6 +59,7 @@ export const viewThreadTool = {
|
|
|
58
59
|
tool_name: i.tool_name,
|
|
59
60
|
created_at: i.created_at.toISOString(),
|
|
60
61
|
})),
|
|
62
|
+
is_error: false,
|
|
61
63
|
};
|
|
62
64
|
},
|
|
63
65
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/tool.ts
CHANGED
|
@@ -13,9 +13,11 @@ export interface ToolContext {
|
|
|
13
13
|
embedFn?: EmbedFn;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
type ToolOutputBase = { is_error: z.ZodBoolean };
|
|
17
|
+
|
|
16
18
|
export interface ToolDefinition<
|
|
17
19
|
TInput extends z.ZodObject<z.ZodRawShape>,
|
|
18
|
-
TOutput extends z.
|
|
20
|
+
TOutput extends z.ZodObject<z.ZodRawShape & ToolOutputBase>,
|
|
19
21
|
> {
|
|
20
22
|
name: string;
|
|
21
23
|
description: string;
|
|
@@ -33,14 +35,14 @@ export interface ToolDefinition<
|
|
|
33
35
|
|
|
34
36
|
export type AnyToolDefinition = ToolDefinition<
|
|
35
37
|
z.ZodObject<z.ZodRawShape>,
|
|
36
|
-
z.
|
|
38
|
+
z.ZodObject<z.ZodRawShape & ToolOutputBase>
|
|
37
39
|
>;
|
|
38
40
|
|
|
39
41
|
const tools = new Map<string, AnyToolDefinition>();
|
|
40
42
|
|
|
41
43
|
export function registerTool<
|
|
42
44
|
TInput extends z.ZodObject<z.ZodRawShape>,
|
|
43
|
-
TOutput extends z.
|
|
45
|
+
TOutput extends z.ZodObject<z.ZodRawShape & ToolOutputBase>,
|
|
44
46
|
>(tool: ToolDefinition<TInput, TOutput>): void {
|
|
45
47
|
tools.set(tool.name, tool as unknown as AnyToolDefinition);
|
|
46
48
|
}
|
package/src/tui/App.tsx
CHANGED
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
sendMessage,
|
|
7
7
|
startChatSession,
|
|
8
8
|
} from "../chat/session.ts";
|
|
9
|
+
import { MAX_INLINE_CHARS, PAGE_SIZE_CHARS } from "../daemon/large-results.ts";
|
|
9
10
|
import type { Interaction } from "../db/threads.ts";
|
|
10
11
|
import { getThread } from "../db/threads.ts";
|
|
11
12
|
import { ContextPanel } from "./components/ContextPanel.tsx";
|
|
12
|
-
import { Divider } from "./components/Divider.tsx";
|
|
13
13
|
import { HelpPanel } from "./components/HelpPanel.tsx";
|
|
14
14
|
import { InputBar } from "./components/InputBar.tsx";
|
|
15
15
|
import { AnimatedLogo } from "./components/Logo.tsx";
|
|
@@ -17,8 +17,11 @@ import { type ChatMessage, MessageList } from "./components/MessageList.tsx";
|
|
|
17
17
|
import { QueuePanel } from "./components/QueuePanel.tsx";
|
|
18
18
|
import { StatusBar } from "./components/StatusBar.tsx";
|
|
19
19
|
import { TabBar, type TabId } from "./components/TabBar.tsx";
|
|
20
|
+
import { TaskPanel } from "./components/TaskPanel.tsx";
|
|
21
|
+
import { ThreadPanel } from "./components/ThreadPanel.tsx";
|
|
20
22
|
import type { ToolCallData } from "./components/ToolCall.tsx";
|
|
21
23
|
import { ToolPanel } from "./components/ToolPanel.tsx";
|
|
24
|
+
import { ansi } from "./theme.ts";
|
|
22
25
|
|
|
23
26
|
interface AppProps {
|
|
24
27
|
projectDir: string;
|
|
@@ -31,15 +34,28 @@ function msgId(): string {
|
|
|
31
34
|
return `msg-${++nextMsgId}`;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
function detectToolError(output: string | undefined): boolean {
|
|
38
|
+
if (!output) return false;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(output);
|
|
41
|
+
if (typeof parsed === "object" && parsed?.is_error === true) return true;
|
|
42
|
+
} catch {
|
|
43
|
+
/* not JSON */
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
function restoreMessagesFromInteractions(
|
|
35
49
|
interactions: Interaction[],
|
|
36
50
|
): ChatMessage[] {
|
|
37
51
|
const result: ChatMessage[] = [];
|
|
38
52
|
let pendingTools: ToolCallData[] = [];
|
|
39
53
|
|
|
54
|
+
let restoredIdx = 0;
|
|
40
55
|
for (const ix of interactions) {
|
|
41
56
|
if (ix.kind === "tool_use") {
|
|
42
57
|
pendingTools.push({
|
|
58
|
+
id: `restored-${restoredIdx++}`,
|
|
43
59
|
name: ix.tool_name ?? "unknown",
|
|
44
60
|
input: ix.tool_input ?? "{}",
|
|
45
61
|
running: false,
|
|
@@ -49,6 +65,14 @@ function restoreMessagesFromInteractions(
|
|
|
49
65
|
const tc = pendingTools.find((t) => t.name === ix.tool_name && !t.output);
|
|
50
66
|
if (tc) {
|
|
51
67
|
tc.output = ix.content;
|
|
68
|
+
tc.isError = detectToolError(ix.content);
|
|
69
|
+
if (ix.content.length > MAX_INLINE_CHARS) {
|
|
70
|
+
tc.largeResult = {
|
|
71
|
+
id: "(restored)",
|
|
72
|
+
chars: ix.content.length,
|
|
73
|
+
pages: Math.ceil(ix.content.length / PAGE_SIZE_CHARS),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
52
76
|
}
|
|
53
77
|
} else if (ix.kind === "message" && ix.role === "user") {
|
|
54
78
|
result.push({
|
|
@@ -95,11 +119,13 @@ export function App({
|
|
|
95
119
|
const [streamingText, setStreamingText] = useState("");
|
|
96
120
|
const [activeToolCalls, setActiveToolCalls] = useState<ToolCallData[]>([]);
|
|
97
121
|
const [ready, setReady] = useState(false);
|
|
98
|
-
const
|
|
122
|
+
const skipSplash = !!(resumeThreadId || initialPrompt);
|
|
123
|
+
const [splashDone, setSplashDone] = useState(skipSplash);
|
|
99
124
|
const [error, setError] = useState<string | null>(null);
|
|
100
125
|
const sessionRef = useRef<ChatSession | null>(null);
|
|
101
126
|
const [activeTab, setActiveTab] = useState<TabId>(1);
|
|
102
127
|
const [daemonRunning, setDaemonRunning] = useState(false);
|
|
128
|
+
const [chatTitle, setChatTitle] = useState<string | undefined>(undefined);
|
|
103
129
|
const queueRef = useRef<string[]>([]);
|
|
104
130
|
const processingRef = useRef(false);
|
|
105
131
|
const [queuedMessages, setQueuedMessages] = useState<string[]>([]);
|
|
@@ -157,7 +183,7 @@ export function App({
|
|
|
157
183
|
const threadId = sessionRef.current.threadId;
|
|
158
184
|
endChatSession(sessionRef.current);
|
|
159
185
|
process.stderr.write(
|
|
160
|
-
`\nThread: ${threadId}\nResume with:
|
|
186
|
+
`\nThread: ${threadId}\nResume with: ${ansi.success}botholomew chat --thread-id ${threadId}${ansi.reset}\n`,
|
|
161
187
|
);
|
|
162
188
|
}
|
|
163
189
|
};
|
|
@@ -169,63 +195,78 @@ export function App({
|
|
|
169
195
|
return () => clearTimeout(timer);
|
|
170
196
|
}, []);
|
|
171
197
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// Queue manipulation keybindings (only when queue has items on Chat tab)
|
|
189
|
-
if (activeTab === 1 && queuedMessages.length > 0 && key.ctrl) {
|
|
190
|
-
if (input === "j") {
|
|
191
|
-
setSelectedQueueIndex((i) =>
|
|
192
|
-
Math.min(i + 1, queuedMessages.length - 1),
|
|
193
|
-
);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (input === "k") {
|
|
197
|
-
setSelectedQueueIndex((i) => Math.max(i - 1, 0));
|
|
198
|
+
// Stable ref for App-level input handler — same pattern as InputBar to
|
|
199
|
+
// prevent Ink's useInput from re-registering stdin listeners on every render.
|
|
200
|
+
const activeTabRef = useRef(activeTab);
|
|
201
|
+
const queuedMessagesRef = useRef(queuedMessages);
|
|
202
|
+
const selectedQueueIndexRef = useRef(selectedQueueIndex);
|
|
203
|
+
activeTabRef.current = activeTab;
|
|
204
|
+
queuedMessagesRef.current = queuedMessages;
|
|
205
|
+
selectedQueueIndexRef.current = selectedQueueIndex;
|
|
206
|
+
|
|
207
|
+
const stableAppHandler = useCallback(
|
|
208
|
+
// biome-ignore lint/suspicious/noExplicitAny: Ink's Key type is not exported
|
|
209
|
+
(input: string, key: any) => {
|
|
210
|
+
// Ctrl+C exits
|
|
211
|
+
if (input === "c" && key.ctrl) {
|
|
212
|
+
exit();
|
|
198
213
|
return;
|
|
199
214
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
215
|
+
|
|
216
|
+
// Tab key cycles tabs — always active (InputBar ignores tab)
|
|
217
|
+
if (key.tab && !key.shift) {
|
|
218
|
+
setActiveTab((t) => ((t % 6) + 1) as TabId);
|
|
203
219
|
return;
|
|
204
220
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
221
|
+
|
|
222
|
+
// Queue manipulation keybindings (only when queue has items on Chat tab)
|
|
223
|
+
const tab = activeTabRef.current;
|
|
224
|
+
const queue = queuedMessagesRef.current;
|
|
225
|
+
if (tab === 1 && queue.length > 0 && key.ctrl) {
|
|
226
|
+
if (input === "j") {
|
|
227
|
+
setSelectedQueueIndex((i) => Math.min(i + 1, queue.length - 1));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (input === "k") {
|
|
231
|
+
setSelectedQueueIndex((i) => Math.max(i - 1, 0));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (input === "x") {
|
|
235
|
+
queueRef.current.splice(selectedQueueIndexRef.current, 1);
|
|
236
|
+
syncQueue();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (input === "e") {
|
|
240
|
+
const [msg] = queueRef.current.splice(
|
|
241
|
+
selectedQueueIndexRef.current,
|
|
242
|
+
1,
|
|
243
|
+
);
|
|
244
|
+
syncQueue();
|
|
245
|
+
if (msg) {
|
|
246
|
+
setInputValue(msg);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
210
249
|
}
|
|
211
|
-
return;
|
|
212
250
|
}
|
|
213
|
-
}
|
|
214
251
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
252
|
+
if (tab !== 1) {
|
|
253
|
+
// Number keys jump to tab on non-chat tabs
|
|
254
|
+
const num = Number.parseInt(input, 10);
|
|
255
|
+
if (num >= 1 && num <= 6) {
|
|
256
|
+
setActiveTab(num as TabId);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Escape returns to chat
|
|
260
|
+
if (key.escape) {
|
|
261
|
+
setActiveTab(1);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
226
264
|
}
|
|
227
|
-
}
|
|
228
|
-
|
|
265
|
+
},
|
|
266
|
+
[exit, syncQueue],
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
useInput(stableAppHandler);
|
|
229
270
|
|
|
230
271
|
const processQueue = useCallback(async () => {
|
|
231
272
|
if (processingRef.current || !sessionRef.current) return;
|
|
@@ -268,17 +309,23 @@ export function App({
|
|
|
268
309
|
}
|
|
269
310
|
};
|
|
270
311
|
|
|
312
|
+
let lastStreamFlush = 0;
|
|
271
313
|
try {
|
|
272
314
|
await sendMessage(sessionRef.current, trimmed, {
|
|
273
315
|
onToken: (token) => {
|
|
274
316
|
currentText += token;
|
|
275
|
-
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
if (now - lastStreamFlush >= 50) {
|
|
319
|
+
setStreamingText(currentText);
|
|
320
|
+
lastStreamFlush = now;
|
|
321
|
+
}
|
|
276
322
|
},
|
|
277
|
-
onToolStart: (name, input) => {
|
|
323
|
+
onToolStart: (id, name, input) => {
|
|
278
324
|
if (currentText) {
|
|
279
325
|
finalizeSegment();
|
|
280
326
|
}
|
|
281
327
|
const tc: ToolCallData = {
|
|
328
|
+
id,
|
|
282
329
|
name,
|
|
283
330
|
input,
|
|
284
331
|
running: true,
|
|
@@ -287,13 +334,15 @@ export function App({
|
|
|
287
334
|
pendingToolCalls.push(tc);
|
|
288
335
|
setActiveToolCalls([...pendingToolCalls]);
|
|
289
336
|
},
|
|
290
|
-
onToolEnd: (
|
|
291
|
-
const tc = pendingToolCalls.find(
|
|
292
|
-
(t) => t.name === name && t.running,
|
|
293
|
-
);
|
|
337
|
+
onToolEnd: (id, _name, output, isError, meta) => {
|
|
338
|
+
const tc = pendingToolCalls.find((t) => t.id === id);
|
|
294
339
|
if (tc) {
|
|
295
340
|
tc.running = false;
|
|
296
341
|
tc.output = output;
|
|
342
|
+
tc.isError = isError;
|
|
343
|
+
if (meta?.largeResult) {
|
|
344
|
+
tc.largeResult = meta.largeResult;
|
|
345
|
+
}
|
|
297
346
|
}
|
|
298
347
|
setActiveToolCalls([...pendingToolCalls]);
|
|
299
348
|
},
|
|
@@ -330,6 +379,28 @@ export function App({
|
|
|
330
379
|
}
|
|
331
380
|
}, [ready, initialPrompt, processQueue, syncQueue]);
|
|
332
381
|
|
|
382
|
+
// Poll for chat thread title updates
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (!ready || !sessionRef.current) return;
|
|
385
|
+
let mounted = true;
|
|
386
|
+
|
|
387
|
+
const refreshTitle = async () => {
|
|
388
|
+
const session = sessionRef.current;
|
|
389
|
+
if (!session) return;
|
|
390
|
+
const result = await getThread(session.conn, session.threadId);
|
|
391
|
+
if (mounted && result?.thread.title) {
|
|
392
|
+
setChatTitle(result.thread.title);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
refreshTitle();
|
|
397
|
+
const interval = setInterval(refreshTitle, 5000);
|
|
398
|
+
return () => {
|
|
399
|
+
mounted = false;
|
|
400
|
+
clearInterval(interval);
|
|
401
|
+
};
|
|
402
|
+
}, [ready]);
|
|
403
|
+
|
|
333
404
|
const handleSubmit = useCallback(
|
|
334
405
|
async (text: string) => {
|
|
335
406
|
const trimmed = text.trim();
|
|
@@ -344,14 +415,13 @@ export function App({
|
|
|
344
415
|
content: [
|
|
345
416
|
"Navigation:",
|
|
346
417
|
" Tab Cycle between panels",
|
|
347
|
-
" 1-
|
|
418
|
+
" 1-6 Jump to panel (when not in Chat)",
|
|
348
419
|
" Escape Return to Chat",
|
|
349
420
|
"",
|
|
350
421
|
"Chat (Tab 1):",
|
|
351
422
|
" Enter Send message",
|
|
352
423
|
" ⌥+Enter Insert newline",
|
|
353
424
|
" ↑/↓ Browse input history",
|
|
354
|
-
" Shift+↑/↓ Scroll chat history",
|
|
355
425
|
"",
|
|
356
426
|
"Tools (Tab 2):",
|
|
357
427
|
" ↑/↓ Select tool call",
|
|
@@ -365,6 +435,22 @@ export function App({
|
|
|
365
435
|
" / Search context",
|
|
366
436
|
" d Delete selected item",
|
|
367
437
|
"",
|
|
438
|
+
"Tasks (Tab 4):",
|
|
439
|
+
" ↑/↓ Navigate task list",
|
|
440
|
+
" Shift+↑/↓ Scroll detail pane",
|
|
441
|
+
" j/k Scroll detail pane",
|
|
442
|
+
" f Cycle status filter",
|
|
443
|
+
" p Cycle priority filter",
|
|
444
|
+
" r Refresh tasks",
|
|
445
|
+
"",
|
|
446
|
+
"Threads (Tab 5):",
|
|
447
|
+
" ↑/↓ Navigate thread list",
|
|
448
|
+
" Shift+↑/↓ Scroll detail pane",
|
|
449
|
+
" j/k Scroll detail pane",
|
|
450
|
+
" f Cycle type filter",
|
|
451
|
+
" d Delete thread (with confirmation)",
|
|
452
|
+
" r Refresh threads",
|
|
453
|
+
"",
|
|
368
454
|
"Commands:",
|
|
369
455
|
" /help Show this help",
|
|
370
456
|
" /quit, /exit End the chat session",
|
|
@@ -388,6 +474,20 @@ export function App({
|
|
|
388
474
|
[exit, processQueue, syncQueue],
|
|
389
475
|
);
|
|
390
476
|
|
|
477
|
+
const sessionConn = sessionRef.current?.conn;
|
|
478
|
+
const inputBarHeader = useMemo(
|
|
479
|
+
() =>
|
|
480
|
+
sessionConn ? (
|
|
481
|
+
<StatusBar
|
|
482
|
+
projectDir={projectDir}
|
|
483
|
+
conn={sessionConn}
|
|
484
|
+
chatTitle={chatTitle}
|
|
485
|
+
onDaemonStatusChange={setDaemonRunning}
|
|
486
|
+
/>
|
|
487
|
+
) : null,
|
|
488
|
+
[projectDir, sessionConn, chatTitle],
|
|
489
|
+
);
|
|
490
|
+
|
|
391
491
|
const allToolCalls = useMemo(
|
|
392
492
|
() => messages.flatMap((m) => m.toolCalls ?? []),
|
|
393
493
|
[messages],
|
|
@@ -420,32 +520,65 @@ export function App({
|
|
|
420
520
|
|
|
421
521
|
return (
|
|
422
522
|
<Box flexDirection="column" height="100%">
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
523
|
+
{/* Tab content area — all panels stay mounted to avoid expensive
|
|
524
|
+
remount cycles (especially <Static> in MessageList re-rendering
|
|
525
|
+
the entire history). display="none" hides inactive panels from
|
|
526
|
+
layout without destroying them. */}
|
|
527
|
+
<Box
|
|
528
|
+
display={activeTab === 1 ? "flex" : "none"}
|
|
529
|
+
flexDirection="column"
|
|
530
|
+
flexGrow={1}
|
|
531
|
+
>
|
|
428
532
|
<MessageList
|
|
429
533
|
messages={messages}
|
|
430
534
|
streamingText={streamingText}
|
|
431
535
|
isLoading={isLoading}
|
|
432
536
|
activeToolCalls={activeToolCalls}
|
|
433
|
-
isActive={activeTab === 1}
|
|
434
537
|
/>
|
|
435
|
-
|
|
436
|
-
|
|
538
|
+
</Box>
|
|
539
|
+
<Box
|
|
540
|
+
display={activeTab === 2 ? "flex" : "none"}
|
|
541
|
+
flexDirection="column"
|
|
542
|
+
flexGrow={1}
|
|
543
|
+
>
|
|
437
544
|
<ToolPanel toolCalls={allToolCalls} isActive={activeTab === 2} />
|
|
438
|
-
|
|
439
|
-
|
|
545
|
+
</Box>
|
|
546
|
+
<Box
|
|
547
|
+
display={activeTab === 3 ? "flex" : "none"}
|
|
548
|
+
flexDirection="column"
|
|
549
|
+
flexGrow={1}
|
|
550
|
+
>
|
|
440
551
|
<ContextPanel conn={conn} isActive={activeTab === 3} />
|
|
441
|
-
|
|
442
|
-
|
|
552
|
+
</Box>
|
|
553
|
+
<Box
|
|
554
|
+
display={activeTab === 4 ? "flex" : "none"}
|
|
555
|
+
flexDirection="column"
|
|
556
|
+
flexGrow={1}
|
|
557
|
+
>
|
|
558
|
+
<TaskPanel conn={conn} isActive={activeTab === 4} />
|
|
559
|
+
</Box>
|
|
560
|
+
<Box
|
|
561
|
+
display={activeTab === 5 ? "flex" : "none"}
|
|
562
|
+
flexDirection="column"
|
|
563
|
+
flexGrow={1}
|
|
564
|
+
>
|
|
565
|
+
<ThreadPanel
|
|
566
|
+
conn={conn}
|
|
567
|
+
activeThreadId={threadId}
|
|
568
|
+
isActive={activeTab === 5}
|
|
569
|
+
/>
|
|
570
|
+
</Box>
|
|
571
|
+
<Box
|
|
572
|
+
display={activeTab === 6 ? "flex" : "none"}
|
|
573
|
+
flexDirection="column"
|
|
574
|
+
flexGrow={1}
|
|
575
|
+
>
|
|
443
576
|
<HelpPanel
|
|
444
577
|
projectDir={projectDir}
|
|
445
578
|
threadId={threadId}
|
|
446
579
|
daemonRunning={daemonRunning}
|
|
447
580
|
/>
|
|
448
|
-
|
|
581
|
+
</Box>
|
|
449
582
|
|
|
450
583
|
{/* Queued messages (only on Chat tab) */}
|
|
451
584
|
{activeTab === 1 && queuedMessages.length > 0 && (
|
|
@@ -455,22 +588,16 @@ export function App({
|
|
|
455
588
|
/>
|
|
456
589
|
)}
|
|
457
590
|
|
|
458
|
-
{/* Bottom bar: StatusBar + InputBar (input only on Chat tab) */}
|
|
591
|
+
{/* Bottom bar: StatusBar + InputBar (input only on Chat tab) + TabBar */}
|
|
459
592
|
<InputBar
|
|
460
593
|
value={inputValue}
|
|
461
594
|
onChange={setInputValue}
|
|
462
595
|
onSubmit={handleSubmit}
|
|
463
596
|
disabled={activeTab !== 1}
|
|
464
597
|
history={inputHistory}
|
|
465
|
-
header={
|
|
466
|
-
<StatusBar
|
|
467
|
-
projectDir={projectDir}
|
|
468
|
-
conn={conn}
|
|
469
|
-
isLoading={isLoading}
|
|
470
|
-
onDaemonStatusChange={setDaemonRunning}
|
|
471
|
-
/>
|
|
472
|
-
}
|
|
598
|
+
header={inputBarHeader}
|
|
473
599
|
/>
|
|
600
|
+
<TabBar activeTab={activeTab} />
|
|
474
601
|
</Box>
|
|
475
602
|
);
|
|
476
603
|
}
|