botholomew 0.14.2 → 0.15.0
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 +1 -1
- package/src/chat/session.ts +4 -0
- package/src/commands/chat.ts +14 -6
- package/src/tui/App.tsx +75 -86
- package/src/tui/components/ContextPanel.tsx +325 -151
- package/src/tui/components/HelpPanel.tsx +42 -43
- package/src/tui/components/SchedulePanel.tsx +98 -97
- package/src/tui/components/Scrollbar.tsx +73 -0
- package/src/tui/components/TabBar.tsx +13 -13
- package/src/tui/components/TaskPanel.tsx +86 -95
- package/src/tui/components/ThreadPanel.tsx +133 -120
- package/src/tui/components/ToolPanel.tsx +84 -85
- package/src/tui/components/WorkerPanel.tsx +77 -77
- package/src/tui/listDetailKeys.ts +124 -0
- package/src/tui/useLatestRef.ts +18 -0
- package/src/worker/prompt.ts +10 -1
package/package.json
CHANGED
package/src/chat/session.ts
CHANGED
|
@@ -180,6 +180,10 @@ export async function endChatSession(session: ChatSession): Promise<void> {
|
|
|
180
180
|
export async function clearChatSession(
|
|
181
181
|
session: ChatSession,
|
|
182
182
|
): Promise<{ previousThreadId: string; newThreadId: string }> {
|
|
183
|
+
// Abort any in-flight stream up front so its callbacks don't continue to
|
|
184
|
+
// fire into the new thread (caused #190 — old messages reappearing on the
|
|
185
|
+
// next user submission).
|
|
186
|
+
abortActiveStream(session);
|
|
183
187
|
const previousThreadId = session.threadId;
|
|
184
188
|
await endThread(session.projectDir, previousThreadId);
|
|
185
189
|
const newThreadId = await createThread(
|
package/src/commands/chat.ts
CHANGED
|
@@ -5,13 +5,21 @@ export function registerChatCommand(program: Command) {
|
|
|
5
5
|
.command("chat")
|
|
6
6
|
.description(
|
|
7
7
|
"Open the interactive chat TUI\n\n" +
|
|
8
|
-
"
|
|
8
|
+
" Tab navigation (Ctrl+<letter> from any tab):\n" +
|
|
9
|
+
" Ctrl+a Chat Ctrl+t Tasks Ctrl+w Workers\n" +
|
|
10
|
+
" Ctrl+o Tools Ctrl+r Threads ? Help (non-chat)\n" +
|
|
11
|
+
" Ctrl+n Context Ctrl+s Schedules Esc Return to Chat\n\n" +
|
|
12
|
+
" Chat input:\n" +
|
|
9
13
|
" Enter Send message\n" +
|
|
10
|
-
" ⌥+Enter Insert newline
|
|
11
|
-
" ↑/↓ Browse input history\n
|
|
12
|
-
"
|
|
13
|
-
" /
|
|
14
|
-
" /
|
|
14
|
+
" ⌥+Enter Insert newline\n" +
|
|
15
|
+
" ↑/↓ Browse input history\n" +
|
|
16
|
+
" Esc Steer / abort an in-flight turn\n" +
|
|
17
|
+
" Ctrl+J/K Navigate queued messages\n" +
|
|
18
|
+
" Ctrl+E/X Edit / remove the selected queued message\n\n" +
|
|
19
|
+
" Slash commands:\n" +
|
|
20
|
+
" /help Show chat-command reference (Help tab has the full keymap)\n" +
|
|
21
|
+
" /skills List available skills\n" +
|
|
22
|
+
" /clear End current thread and start a new one\n" +
|
|
15
23
|
" /exit End the chat session",
|
|
16
24
|
)
|
|
17
25
|
.option("--thread-id <id>", "Resume an existing chat thread")
|
package/src/tui/App.tsx
CHANGED
|
@@ -47,6 +47,21 @@ function msgId(): string {
|
|
|
47
47
|
return `msg-${++nextMsgId}`;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Tab routing: Ctrl+<letter> jumps to a tab. Chosen for memorability — first
|
|
51
|
+
// available letter that doesn't collide with other Ctrl bindings (Ctrl+C exit,
|
|
52
|
+
// Ctrl+J/K/X/E queue ops on Chat). Help is bound to `?` instead of Ctrl+H
|
|
53
|
+
// because most terminals send Ctrl+H as ASCII 0x08 (backspace), which Ink
|
|
54
|
+
// reports as `key.backspace=true`, not `input='h'`.
|
|
55
|
+
const TAB_BY_CTRL_KEY: Record<string, TabId> = {
|
|
56
|
+
a: 1, // ch[a]t
|
|
57
|
+
o: 2, // t[o]ols
|
|
58
|
+
n: 3, // co[n]text
|
|
59
|
+
t: 4, // [t]asks
|
|
60
|
+
r: 5, // th[r]eads
|
|
61
|
+
s: 6, // [s]chedules
|
|
62
|
+
w: 7, // [w]orkers
|
|
63
|
+
};
|
|
64
|
+
|
|
50
65
|
function detectToolError(output: string | undefined): boolean {
|
|
51
66
|
if (!output) return false;
|
|
52
67
|
try {
|
|
@@ -187,7 +202,7 @@ export function App({
|
|
|
187
202
|
id: msgId(),
|
|
188
203
|
role: "system" as const,
|
|
189
204
|
content:
|
|
190
|
-
"
|
|
205
|
+
"Switch panels with Ctrl+<letter> (^a chat · ^o tools · ^n context · ^t tasks · ^r threads · ^s schedules · ^w workers) — `?` for help. Type /help for commands.",
|
|
191
206
|
timestamp: new Date(),
|
|
192
207
|
},
|
|
193
208
|
]);
|
|
@@ -259,17 +274,30 @@ export function App({
|
|
|
259
274
|
return;
|
|
260
275
|
}
|
|
261
276
|
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
270
|
-
|
|
277
|
+
// Ctrl+<letter> jumps directly to a tab from any tab. On Chat, only
|
|
278
|
+
// suppress these if the slash-autocomplete popup needs the keystroke
|
|
279
|
+
// (Ctrl combos don't drive the popup, but keep the guard symmetric
|
|
280
|
+
// with the previous Tab-cycle behavior).
|
|
281
|
+
if (key.ctrl) {
|
|
282
|
+
const tabForKey = TAB_BY_CTRL_KEY[input];
|
|
283
|
+
if (tabForKey !== undefined) {
|
|
284
|
+
if (activeTabRef.current === 1) {
|
|
285
|
+
const popupOpen = getSlashMatches(
|
|
286
|
+
inputValueRef.current,
|
|
287
|
+
slashCommandsRef.current,
|
|
288
|
+
);
|
|
289
|
+
if (popupOpen) return;
|
|
290
|
+
}
|
|
291
|
+
setActiveTab(tabForKey);
|
|
292
|
+
return;
|
|
271
293
|
}
|
|
272
|
-
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// `?` jumps to the Help tab from any non-chat tab (on Chat, `?` is
|
|
297
|
+
// a regular character). Acts as the Help shortcut since Ctrl+H is
|
|
298
|
+
// unusable — most terminals send it as backspace.
|
|
299
|
+
if (input === "?" && activeTabRef.current !== 1) {
|
|
300
|
+
setActiveTab(8);
|
|
273
301
|
return;
|
|
274
302
|
}
|
|
275
303
|
|
|
@@ -316,12 +344,6 @@ export function App({
|
|
|
316
344
|
}
|
|
317
345
|
|
|
318
346
|
if (tab !== 1) {
|
|
319
|
-
// Number keys jump to tab on non-chat tabs
|
|
320
|
-
const num = Number.parseInt(input, 10);
|
|
321
|
-
if (num >= 1 && num <= 8) {
|
|
322
|
-
setActiveTab(num as TabId);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
347
|
// Escape returns to chat
|
|
326
348
|
if (key.escape) {
|
|
327
349
|
setActiveTab(1);
|
|
@@ -512,81 +534,30 @@ export function App({
|
|
|
512
534
|
|
|
513
535
|
if (trimmed === "/help") {
|
|
514
536
|
const skills = sessionRef.current.skills;
|
|
515
|
-
const
|
|
537
|
+
const lines: string[] = [
|
|
538
|
+
"For the full keyboard reference, switch to the Help tab (`?` from any non-chat tab) — this message lists chat commands only.",
|
|
539
|
+
"",
|
|
540
|
+
"Slash commands:",
|
|
541
|
+
" /help Show this message",
|
|
542
|
+
" /skills List available skills",
|
|
543
|
+
" /clear End current thread and start a new one",
|
|
544
|
+
" /exit End the chat session",
|
|
545
|
+
];
|
|
516
546
|
if (skills.size > 0) {
|
|
517
|
-
|
|
547
|
+
lines.push("", "Skills:");
|
|
518
548
|
for (const [skillName, skill] of skills) {
|
|
519
|
-
|
|
549
|
+
lines.push(
|
|
520
550
|
` /${skillName.padEnd(14)} ${skill.description || "(no description)"}`,
|
|
521
551
|
);
|
|
522
552
|
}
|
|
523
553
|
} else {
|
|
524
|
-
|
|
554
|
+
lines.push("", "Skills:", " (none — add .md files to skills/)");
|
|
525
555
|
}
|
|
526
556
|
|
|
527
557
|
const helpMsg: ChatMessage = {
|
|
528
558
|
id: msgId(),
|
|
529
559
|
role: "system",
|
|
530
|
-
content:
|
|
531
|
-
"Navigation:",
|
|
532
|
-
" Tab Cycle between panels",
|
|
533
|
-
" 1-7 Jump to panel (when not in Chat)",
|
|
534
|
-
" Escape Return to Chat",
|
|
535
|
-
"",
|
|
536
|
-
"Chat (Tab 1):",
|
|
537
|
-
" Enter Send message",
|
|
538
|
-
" ⌥+Enter Insert newline",
|
|
539
|
-
" ↑/↓ Browse input history",
|
|
540
|
-
" / Open slash-command autocomplete",
|
|
541
|
-
" Enter Run highlighted command / insert if it takes args (popup open)",
|
|
542
|
-
" Tab Insert highlighted command without submitting (popup open)",
|
|
543
|
-
" ↑/↓ Move highlight (popup open)",
|
|
544
|
-
" Esc Close popup",
|
|
545
|
-
"",
|
|
546
|
-
"Tools (Tab 2):",
|
|
547
|
-
" ↑/↓ Select tool call",
|
|
548
|
-
" Shift+↑/↓ Scroll detail pane",
|
|
549
|
-
" j/k Scroll detail pane",
|
|
550
|
-
"",
|
|
551
|
-
"Context (Tab 3):",
|
|
552
|
-
" ↑/↓ Navigate items",
|
|
553
|
-
" Enter Expand directory / preview file",
|
|
554
|
-
" Backspace Go up one directory",
|
|
555
|
-
" / Search context",
|
|
556
|
-
" d Delete selected item",
|
|
557
|
-
"",
|
|
558
|
-
"Tasks (Tab 4):",
|
|
559
|
-
" ↑/↓ Navigate task list",
|
|
560
|
-
" Shift+↑/↓ Scroll detail pane",
|
|
561
|
-
" j/k Scroll detail pane",
|
|
562
|
-
" f Cycle status filter",
|
|
563
|
-
" p Cycle priority filter",
|
|
564
|
-
" r Refresh tasks",
|
|
565
|
-
"",
|
|
566
|
-
"Threads (Tab 5):",
|
|
567
|
-
" ↑/↓ Navigate thread list",
|
|
568
|
-
" Shift+↑/↓ Scroll detail pane",
|
|
569
|
-
" j/k Scroll detail pane",
|
|
570
|
-
" f Cycle type filter",
|
|
571
|
-
" d Delete thread (with confirmation)",
|
|
572
|
-
" r Refresh threads",
|
|
573
|
-
"",
|
|
574
|
-
"Schedules (Tab 6):",
|
|
575
|
-
" ↑/↓ Navigate schedule list",
|
|
576
|
-
" Shift+↑/↓ Scroll detail pane",
|
|
577
|
-
" j/k Scroll detail pane",
|
|
578
|
-
" f Cycle enabled/disabled filter",
|
|
579
|
-
" e Toggle enable/disable",
|
|
580
|
-
" d Delete schedule (with confirmation)",
|
|
581
|
-
" r Refresh schedules",
|
|
582
|
-
"",
|
|
583
|
-
"Commands:",
|
|
584
|
-
" /help Show this help",
|
|
585
|
-
" /skills List available skills",
|
|
586
|
-
" /clear End current thread and start a new one",
|
|
587
|
-
" /exit End the chat session",
|
|
588
|
-
...skillLines,
|
|
589
|
-
].join("\n"),
|
|
560
|
+
content: lines.join("\n"),
|
|
590
561
|
timestamp: new Date(),
|
|
591
562
|
};
|
|
592
563
|
setMessages((prev) => [...prev, helpMsg]);
|
|
@@ -622,8 +593,23 @@ export function App({
|
|
|
622
593
|
// Drain any queued messages so they don't leak into the new thread.
|
|
623
594
|
queueRef.current.length = 0;
|
|
624
595
|
syncQueue();
|
|
625
|
-
|
|
626
|
-
|
|
596
|
+
// Abort any in-flight stream synchronously so its callbacks stop
|
|
597
|
+
// firing before we reset UI state. clearChatSession also calls
|
|
598
|
+
// this, but doing it here lets us start the wait-for-quiesce
|
|
599
|
+
// poll below immediately rather than waiting on the
|
|
600
|
+
// createThread/endThread round trip first.
|
|
601
|
+
abortActiveStream(session);
|
|
602
|
+
void (async () => {
|
|
603
|
+
// Wait for any in-flight processQueue iteration to finish so
|
|
604
|
+
// its trailing `finalizeSegment` can't race our state reset
|
|
605
|
+
// and re-add the previous thread's assistant message after
|
|
606
|
+
// the UI has been cleared. (Issue #190.)
|
|
607
|
+
while (processingRef.current) {
|
|
608
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
const { previousThreadId, newThreadId } =
|
|
612
|
+
await clearChatSession(session);
|
|
627
613
|
// Ink's <Static> writes messages to terminal scrollback and
|
|
628
614
|
// can't un-write them, so setMessages alone leaves the old
|
|
629
615
|
// lines visible. Clear the terminal (including scrollback)
|
|
@@ -639,8 +625,10 @@ export function App({
|
|
|
639
625
|
]);
|
|
640
626
|
setMessagesEpoch((n) => n + 1);
|
|
641
627
|
setChatTitle(undefined);
|
|
642
|
-
|
|
643
|
-
|
|
628
|
+
setStreamingText("");
|
|
629
|
+
setActiveToolCalls([]);
|
|
630
|
+
setPreparingTool(null);
|
|
631
|
+
} catch (err) {
|
|
644
632
|
setMessages((prev) => [
|
|
645
633
|
...prev,
|
|
646
634
|
{
|
|
@@ -650,7 +638,8 @@ export function App({
|
|
|
650
638
|
timestamp: new Date(),
|
|
651
639
|
},
|
|
652
640
|
]);
|
|
653
|
-
}
|
|
641
|
+
}
|
|
642
|
+
})();
|
|
654
643
|
},
|
|
655
644
|
});
|
|
656
645
|
if (handled) return;
|