botholomew 0.15.2 → 0.15.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/package.json +1 -1
- package/src/chat/agent.ts +12 -1
- package/src/commands/chat.ts +2 -1
- package/src/tools/schedule/create.ts +3 -1
- package/src/tools/task/create.ts +3 -1
- package/src/tools/task/delete.ts +3 -1
- package/src/tools/task/update.ts +3 -1
- package/src/tools/tool.ts +7 -0
- package/src/tui/App.tsx +20 -3
- package/src/tui/components/ContextPanel.tsx +4 -6
- package/src/tui/components/HelpPanel.tsx +9 -7
- package/src/tui/components/SchedulePanel.tsx +3 -6
- package/src/tui/components/TaskPanel.tsx +3 -6
- package/src/tui/components/ThreadPanel.tsx +3 -6
- package/src/tui/components/ToolCall.tsx +14 -0
- package/src/tui/components/WorkerPanel.tsx +15 -4
- package/src/tui/theme.ts +1 -0
package/package.json
CHANGED
package/src/chat/agent.ts
CHANGED
|
@@ -35,11 +35,13 @@ import {
|
|
|
35
35
|
|
|
36
36
|
registerAllTools();
|
|
37
37
|
|
|
38
|
-
/** Tools available in chat mode — no worker terminal tools (complete/fail/wait), no bulk-destructive file tools (
|
|
38
|
+
/** Tools available in chat mode — no worker terminal tools (complete/fail/wait), no bulk-destructive file tools (copy/move, dir ops) */
|
|
39
39
|
const CHAT_TOOL_NAMES = new Set([
|
|
40
40
|
"create_task",
|
|
41
41
|
"list_tasks",
|
|
42
42
|
"view_task",
|
|
43
|
+
"update_task",
|
|
44
|
+
"delete_task",
|
|
43
45
|
"context_info",
|
|
44
46
|
"context_tree",
|
|
45
47
|
"context_read",
|
|
@@ -156,6 +158,11 @@ export interface ChatTurnCallbacks {
|
|
|
156
158
|
isError: boolean,
|
|
157
159
|
meta?: ToolEndMeta,
|
|
158
160
|
) => void;
|
|
161
|
+
/** Side-effect notification from inside a tool ("Created subtask: …"). The
|
|
162
|
+
* TUI renders these inside the tool-call card so they stay anchored to the
|
|
163
|
+
* tool that produced them. Workers don't supply this; tools fall back to
|
|
164
|
+
* `logger.info`. */
|
|
165
|
+
onToolNotify?: (toolUseId: string, message: string) => void;
|
|
159
166
|
/** Called between LLM turns. The TUI returns any queued user messages so
|
|
160
167
|
* the agent can inject them into the running turn instead of waiting for
|
|
161
168
|
* the entire tool loop to finish. Each returned message is logged + pushed
|
|
@@ -406,6 +413,9 @@ export async function runChatTurn(input: {
|
|
|
406
413
|
config,
|
|
407
414
|
mcpxClient,
|
|
408
415
|
shouldAbort: session ? () => session.aborted : undefined,
|
|
416
|
+
notify: callbacks.onToolNotify
|
|
417
|
+
? (msg) => callbacks.onToolNotify?.(toolUse.id, msg)
|
|
418
|
+
: undefined,
|
|
409
419
|
});
|
|
410
420
|
const durationMs = Date.now() - start;
|
|
411
421
|
const stored = maybeStoreResult(toolUse.name, result.output);
|
|
@@ -454,6 +464,7 @@ interface ChatToolCallCtx {
|
|
|
454
464
|
config: Required<BotholomewConfig>;
|
|
455
465
|
mcpxClient: McpxClient | null;
|
|
456
466
|
shouldAbort?: () => boolean;
|
|
467
|
+
notify?: (message: string) => void;
|
|
457
468
|
}
|
|
458
469
|
|
|
459
470
|
async function executeChatToolCall(
|
package/src/commands/chat.ts
CHANGED
|
@@ -8,8 +8,9 @@ export function registerChatCommand(program: Command) {
|
|
|
8
8
|
"Open the interactive chat TUI\n\n" +
|
|
9
9
|
" Tab navigation (Ctrl+<letter> from any tab):\n" +
|
|
10
10
|
" Ctrl+a Chat Ctrl+t Tasks Ctrl+w Workers\n" +
|
|
11
|
-
" Ctrl+o Tools Ctrl+
|
|
11
|
+
" Ctrl+o Tools Ctrl+e Threads Ctrl+g Help\n" +
|
|
12
12
|
" Ctrl+n Context Ctrl+s Schedules Esc Return to Chat\n\n" +
|
|
13
|
+
" Refresh: Ctrl+R refreshes Context · Tasks · Threads · Schedules · Workers\n\n" +
|
|
13
14
|
" Chat input:\n" +
|
|
14
15
|
" Enter Send message\n" +
|
|
15
16
|
" ⌥+Enter Insert newline\n" +
|
|
@@ -33,7 +33,9 @@ export const createScheduleTool = {
|
|
|
33
33
|
description: input.description,
|
|
34
34
|
frequency: input.frequency,
|
|
35
35
|
});
|
|
36
|
-
|
|
36
|
+
const msg = `Created schedule: ${schedule.name} (${schedule.id})`;
|
|
37
|
+
if (ctx.notify) ctx.notify(msg);
|
|
38
|
+
else logger.info(msg);
|
|
37
39
|
return {
|
|
38
40
|
id: schedule.id,
|
|
39
41
|
name: schedule.name,
|
package/src/tools/task/create.ts
CHANGED
|
@@ -55,7 +55,9 @@ export const createTaskTool = {
|
|
|
55
55
|
blocked_by: input.blocked_by,
|
|
56
56
|
context_paths: input.context_paths,
|
|
57
57
|
});
|
|
58
|
-
|
|
58
|
+
const msg = `Created subtask: ${newTask.name} (${newTask.id})`;
|
|
59
|
+
if (ctx.notify) ctx.notify(msg);
|
|
60
|
+
else logger.info(msg);
|
|
59
61
|
return {
|
|
60
62
|
id: newTask.id,
|
|
61
63
|
name: newTask.name,
|
package/src/tools/task/delete.ts
CHANGED
|
@@ -44,7 +44,9 @@ export const deleteTaskTool = {
|
|
|
44
44
|
is_error: true,
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
const msg = `Deleted task: ${existing.name} (${existing.id})`;
|
|
48
|
+
if (ctx.notify) ctx.notify(msg);
|
|
49
|
+
else logger.info(msg);
|
|
48
50
|
return {
|
|
49
51
|
deleted_id: existing.id,
|
|
50
52
|
message: `Deleted task "${existing.name}" (${existing.id})`,
|
package/src/tools/task/update.ts
CHANGED
|
@@ -91,7 +91,9 @@ export const updateTaskTool = {
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
const msg = `Updated task: ${updated.name} (${updated.id})`;
|
|
95
|
+
if (ctx.notify) ctx.notify(msg);
|
|
96
|
+
else logger.info(msg);
|
|
95
97
|
return {
|
|
96
98
|
task: {
|
|
97
99
|
id: updated.id,
|
package/src/tools/tool.ts
CHANGED
|
@@ -22,6 +22,13 @@ export interface ToolContext {
|
|
|
22
22
|
* Esc-to-abort by reading `session.aborted`. Workers leave this `undefined`.
|
|
23
23
|
*/
|
|
24
24
|
shouldAbort?: () => boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Chat-mode only. Tools call this to surface a short human-readable
|
|
27
|
+
* side-effect message (e.g. "Created subtask: …") that the TUI renders
|
|
28
|
+
* inside the tool-call card. Workers leave this `undefined`; tools fall
|
|
29
|
+
* back to `logger.info` so worker logs are unchanged.
|
|
30
|
+
*/
|
|
31
|
+
notify?: (message: string) => void;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
type ToolOutputBase = { is_error: z.ZodBoolean };
|
package/src/tui/App.tsx
CHANGED
|
@@ -65,7 +65,7 @@ const TAB_BY_CTRL_KEY: Record<string, TabId> = {
|
|
|
65
65
|
o: 2, // t[o]ols
|
|
66
66
|
n: 3, // co[n]text
|
|
67
67
|
t: 4, // [t]asks
|
|
68
|
-
|
|
68
|
+
e: 5, // thr[e]ads
|
|
69
69
|
s: 6, // [s]chedules
|
|
70
70
|
w: 7, // [w]orkers
|
|
71
71
|
g: 8, // help (also catches Ctrl+/ on terminals that map it to BEL)
|
|
@@ -328,9 +328,18 @@ function AppInner({
|
|
|
328
328
|
slashCommandsRef.current,
|
|
329
329
|
);
|
|
330
330
|
if (popupOpen) return;
|
|
331
|
+
// Ctrl+E edits a queued message when one is selected; only
|
|
332
|
+
// fall through to the Threads tab-jump when the queue is empty.
|
|
333
|
+
if (input === "e" && queuedMessagesRef.current.length > 0) {
|
|
334
|
+
// handled by the queue keybindings block below
|
|
335
|
+
} else {
|
|
336
|
+
setActiveTab(tabForKey);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
setActiveTab(tabForKey);
|
|
341
|
+
return;
|
|
331
342
|
}
|
|
332
|
-
setActiveTab(tabForKey);
|
|
333
|
-
return;
|
|
334
343
|
}
|
|
335
344
|
}
|
|
336
345
|
|
|
@@ -476,6 +485,14 @@ function AppInner({
|
|
|
476
485
|
}
|
|
477
486
|
setActiveToolCalls([...pendingToolCalls]);
|
|
478
487
|
},
|
|
488
|
+
onToolNotify: (id, message) => {
|
|
489
|
+
markActivityRef.current();
|
|
490
|
+
const tc = pendingToolCalls.find((t) => t.id === id);
|
|
491
|
+
if (tc) {
|
|
492
|
+
tc.notes = [...(tc.notes ?? []), message];
|
|
493
|
+
setActiveToolCalls([...pendingToolCalls]);
|
|
494
|
+
}
|
|
495
|
+
},
|
|
479
496
|
onUsage: (info) => {
|
|
480
497
|
setUsage(info);
|
|
481
498
|
},
|
|
@@ -236,8 +236,9 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
236
236
|
deleteConfirm.pressDelete(entry.path);
|
|
237
237
|
return;
|
|
238
238
|
}
|
|
239
|
-
if (input === "r") {
|
|
239
|
+
if (key.ctrl && (input === "r" || input === "R")) {
|
|
240
240
|
refresh(currentPathRef.current);
|
|
241
|
+
return;
|
|
241
242
|
}
|
|
242
243
|
},
|
|
243
244
|
{ isActive },
|
|
@@ -367,7 +368,7 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
367
368
|
<Text dimColor>
|
|
368
369
|
{focus === "detail"
|
|
369
370
|
? "↑↓ scroll · ⇧↑↓ page · g/G top/bot · ← back to list"
|
|
370
|
-
: "↑↓ select · → drill in/enter detail · ← up · d delete (×2) ·
|
|
371
|
+
: "↑↓ select · → drill in/enter detail · ← up · d delete (×2) · ^R refresh"}
|
|
371
372
|
</Text>
|
|
372
373
|
</Box>
|
|
373
374
|
</Box>
|
|
@@ -400,7 +401,7 @@ function ContextDetailHeader({
|
|
|
400
401
|
indexLoaded: boolean;
|
|
401
402
|
}) {
|
|
402
403
|
return (
|
|
403
|
-
<Box flexDirection="column">
|
|
404
|
+
<Box flexDirection="column" width="100%" backgroundColor={theme.headerBg}>
|
|
404
405
|
<Box>
|
|
405
406
|
<Text bold color="cyan" wrap="truncate-end">
|
|
406
407
|
{entry.is_directory ? "📁" : "📄"} context/{entry.path}
|
|
@@ -441,9 +442,6 @@ function ContextDetailHeader({
|
|
|
441
442
|
</Box>
|
|
442
443
|
</>
|
|
443
444
|
)}
|
|
444
|
-
<Box>
|
|
445
|
-
<Text dimColor>{"─".repeat(2)}</Text>
|
|
446
|
-
</Box>
|
|
447
445
|
</Box>
|
|
448
446
|
);
|
|
449
447
|
}
|
|
@@ -56,7 +56,7 @@ export const HelpPanel = memo(function HelpPanel({
|
|
|
56
56
|
{" "}Ctrl+t{" "}Tasks
|
|
57
57
|
</Text>
|
|
58
58
|
<Text>
|
|
59
|
-
{" "}Ctrl+
|
|
59
|
+
{" "}Ctrl+e{" "}Threads
|
|
60
60
|
</Text>
|
|
61
61
|
<Text>
|
|
62
62
|
{" "}Ctrl+s{" "}Schedules
|
|
@@ -142,19 +142,21 @@ export const HelpPanel = memo(function HelpPanel({
|
|
|
142
142
|
(cancels on any other key or after 3s)
|
|
143
143
|
</Text>
|
|
144
144
|
<Text>
|
|
145
|
-
{" "}
|
|
146
|
-
|
|
145
|
+
{" "}Ctrl+R{" "}Refresh (Context · Tasks · Threads ·
|
|
146
|
+
Schedules · Workers)
|
|
147
|
+
</Text>
|
|
148
|
+
<Text>
|
|
149
|
+
{" "}Tasks{" "}f filter · p priority · d delete (×2)
|
|
147
150
|
</Text>
|
|
148
151
|
<Text>
|
|
149
152
|
{" "}Threads{" "}f filter · s/ search · w follow · d delete
|
|
150
|
-
(×2)
|
|
153
|
+
(×2)
|
|
151
154
|
</Text>
|
|
152
155
|
<Text>
|
|
153
|
-
{" "}Schedules{" "}f filter · e toggle · d delete (×2)
|
|
154
|
-
refresh
|
|
156
|
+
{" "}Schedules{" "}f filter · e toggle · d delete (×2)
|
|
155
157
|
</Text>
|
|
156
158
|
<Text>
|
|
157
|
-
{" "}Context{" "}d delete (×2)
|
|
159
|
+
{" "}Context{" "}d delete (×2)
|
|
158
160
|
</Text>
|
|
159
161
|
<Text>
|
|
160
162
|
{" "}Workers{" "}f filter · l toggle log/detail · d delete log
|
|
@@ -207,7 +207,7 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
207
207
|
deleteConfirm.pressDelete(s.name);
|
|
208
208
|
return;
|
|
209
209
|
}
|
|
210
|
-
if (input === "r") {
|
|
210
|
+
if (key.ctrl && (input === "r" || input === "R")) {
|
|
211
211
|
forceRefresh();
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
@@ -338,7 +338,7 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
338
338
|
<Text dimColor>
|
|
339
339
|
{focus === "detail"
|
|
340
340
|
? "↑↓ scroll · ⇧↑↓ page · g/G top/bot · ← back to list"
|
|
341
|
-
: "↑↓ select · → enter detail · f filter · e toggle · d delete (×2) ·
|
|
341
|
+
: "↑↓ select · → enter detail · f filter · e toggle · d delete (×2) · ^R refresh"}
|
|
342
342
|
</Text>
|
|
343
343
|
</Box>
|
|
344
344
|
</Box>
|
|
@@ -348,7 +348,7 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
348
348
|
function ScheduleDetailHeader({ schedule }: { schedule: Schedule }) {
|
|
349
349
|
const enabledKey = String(schedule.enabled);
|
|
350
350
|
return (
|
|
351
|
-
<Box flexDirection="column">
|
|
351
|
+
<Box flexDirection="column" width="100%" backgroundColor={theme.headerBg}>
|
|
352
352
|
<Box>
|
|
353
353
|
<Text bold color={theme.info} wrap="truncate-end">
|
|
354
354
|
{schedule.name}
|
|
@@ -367,9 +367,6 @@ function ScheduleDetailHeader({ schedule }: { schedule: Schedule }) {
|
|
|
367
367
|
</Text>
|
|
368
368
|
</Text>
|
|
369
369
|
</Box>
|
|
370
|
-
<Box>
|
|
371
|
-
<Text dimColor>{"─".repeat(2)}</Text>
|
|
372
|
-
</Box>
|
|
373
370
|
</Box>
|
|
374
371
|
);
|
|
375
372
|
}
|
|
@@ -241,7 +241,7 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
241
241
|
deleteConfirm.pressDelete(t.name || t.id);
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
|
-
if (input === "r") {
|
|
244
|
+
if (key.ctrl && (input === "r" || input === "R")) {
|
|
245
245
|
forceRefresh();
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
@@ -376,7 +376,7 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
376
376
|
<Text dimColor>
|
|
377
377
|
{focus === "detail"
|
|
378
378
|
? "↑↓ scroll · ⇧↑↓ page · g/G top/bot · ← back to list"
|
|
379
|
-
: "↑↓ select · → enter detail · f filter · p priority · d delete (×2) ·
|
|
379
|
+
: "↑↓ select · → enter detail · f filter · p priority · d delete (×2) · ^R refresh"}
|
|
380
380
|
</Text>
|
|
381
381
|
</Box>
|
|
382
382
|
</Box>
|
|
@@ -385,7 +385,7 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
385
385
|
|
|
386
386
|
function TaskDetailHeader({ task }: { task: Task }) {
|
|
387
387
|
return (
|
|
388
|
-
<Box flexDirection="column">
|
|
388
|
+
<Box flexDirection="column" width="100%" backgroundColor={theme.headerBg}>
|
|
389
389
|
<Box>
|
|
390
390
|
<Text bold color={theme.info} wrap="truncate-end">
|
|
391
391
|
{task.name}
|
|
@@ -408,9 +408,6 @@ function TaskDetailHeader({ task }: { task: Task }) {
|
|
|
408
408
|
</Text>
|
|
409
409
|
</Text>
|
|
410
410
|
</Box>
|
|
411
|
-
<Box>
|
|
412
|
-
<Text dimColor>{"─".repeat(2)}</Text>
|
|
413
|
-
</Box>
|
|
414
411
|
</Box>
|
|
415
412
|
);
|
|
416
413
|
}
|
|
@@ -394,7 +394,7 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
394
394
|
deleteConfirm.pressDelete(t.title || "(untitled)");
|
|
395
395
|
return;
|
|
396
396
|
}
|
|
397
|
-
if (input === "r") {
|
|
397
|
+
if (key.ctrl && (input === "r" || input === "R")) {
|
|
398
398
|
forceRefresh();
|
|
399
399
|
return;
|
|
400
400
|
}
|
|
@@ -575,7 +575,7 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
575
575
|
<Text dimColor>
|
|
576
576
|
{focus === "detail"
|
|
577
577
|
? "↑↓ scroll · ⇧↑↓ page · g/G top/bot · ← back to list"
|
|
578
|
-
: `↑↓ select · → enter detail · s search · f filter · d delete (×2)${selectedThread && !selectedThread.ended_at ? " · w follow" : ""} ·
|
|
578
|
+
: `↑↓ select · → enter detail · s search · f filter · d delete (×2)${selectedThread && !selectedThread.ended_at ? " · w follow" : ""} · ^R refresh`}
|
|
579
579
|
</Text>
|
|
580
580
|
</Box>
|
|
581
581
|
</Box>
|
|
@@ -591,7 +591,7 @@ function ThreadDetailHeader({
|
|
|
591
591
|
isActiveThread: boolean;
|
|
592
592
|
}) {
|
|
593
593
|
return (
|
|
594
|
-
<Box flexDirection="column">
|
|
594
|
+
<Box flexDirection="column" width="100%" backgroundColor={theme.headerBg}>
|
|
595
595
|
<Box>
|
|
596
596
|
<Text wrap="truncate-end">
|
|
597
597
|
<Text bold italic color={theme.info}>
|
|
@@ -625,9 +625,6 @@ function ThreadDetailHeader({
|
|
|
625
625
|
</Text>
|
|
626
626
|
</Text>
|
|
627
627
|
</Box>
|
|
628
|
-
<Box>
|
|
629
|
-
<Text dimColor>{"─".repeat(2)}</Text>
|
|
630
|
-
</Box>
|
|
631
628
|
</Box>
|
|
632
629
|
);
|
|
633
630
|
}
|
|
@@ -40,6 +40,8 @@ export interface ToolCallData {
|
|
|
40
40
|
timestamp: Date;
|
|
41
41
|
largeResult?: LargeResultMeta;
|
|
42
42
|
isError?: boolean;
|
|
43
|
+
/** Side-effect notes emitted by the tool (e.g. "Created subtask: …"). */
|
|
44
|
+
notes?: string[];
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
interface ToolCallProps {
|
|
@@ -106,6 +108,18 @@ export function ToolCall({ tool }: ToolCallProps) {
|
|
|
106
108
|
{tool.largeResult.pages}pg]
|
|
107
109
|
</Text>
|
|
108
110
|
)}
|
|
111
|
+
{tool.notes?.map((note, i) => (
|
|
112
|
+
<Text
|
|
113
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: notes are append-only
|
|
114
|
+
key={i}
|
|
115
|
+
color={theme.accent}
|
|
116
|
+
dimColor
|
|
117
|
+
wrap="truncate-end"
|
|
118
|
+
>
|
|
119
|
+
{" ℹ "}
|
|
120
|
+
{note}
|
|
121
|
+
</Text>
|
|
122
|
+
))}
|
|
109
123
|
</Box>
|
|
110
124
|
);
|
|
111
125
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
-
import { memo, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { readLogTail } from "../../worker/log-reader.ts";
|
|
5
5
|
import {
|
|
6
6
|
deleteWorkerLog,
|
|
@@ -70,6 +70,7 @@ export const WorkerPanel = memo(function WorkerPanel({
|
|
|
70
70
|
const [workers, setWorkers] = useState<Worker[]>([]);
|
|
71
71
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
72
72
|
const [filterIdx, setFilterIdx] = useState(0);
|
|
73
|
+
const [refreshTick, setRefreshTick] = useState(0);
|
|
73
74
|
const [now, setNow] = useState(() => new Date());
|
|
74
75
|
const [viewMode, setViewMode] = useState<"detail" | "log">("detail");
|
|
75
76
|
const [logContent, setLogContent] = useState("");
|
|
@@ -79,6 +80,7 @@ export const WorkerPanel = memo(function WorkerPanel({
|
|
|
79
80
|
const [logFollow, setLogFollow] = useState(true);
|
|
80
81
|
const [focus, setFocus] = useState<FocusState>("list");
|
|
81
82
|
|
|
83
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: refreshTick triggers manual refresh
|
|
82
84
|
useEffect(() => {
|
|
83
85
|
let mounted = true;
|
|
84
86
|
|
|
@@ -104,7 +106,7 @@ export const WorkerPanel = memo(function WorkerPanel({
|
|
|
104
106
|
mounted = false;
|
|
105
107
|
clearInterval(interval);
|
|
106
108
|
};
|
|
107
|
-
}, [projectDir, filterIdx]);
|
|
109
|
+
}, [projectDir, filterIdx, refreshTick]);
|
|
108
110
|
|
|
109
111
|
const selected = workers[selectedIndex];
|
|
110
112
|
const selectedLogPath = selected?.log_path ?? null;
|
|
@@ -171,6 +173,10 @@ export const WorkerPanel = memo(function WorkerPanel({
|
|
|
171
173
|
const viewModeRef = useLatestRef(viewMode);
|
|
172
174
|
const selectedLogPathRef = useLatestRef(selectedLogPath);
|
|
173
175
|
|
|
176
|
+
const forceRefresh = useCallback(() => {
|
|
177
|
+
setRefreshTick((t) => t + 1);
|
|
178
|
+
}, []);
|
|
179
|
+
|
|
174
180
|
const deleteConfirm = useDeleteConfirm(() => {
|
|
175
181
|
const path = selectedLogPathRef.current;
|
|
176
182
|
if (!path) return;
|
|
@@ -238,6 +244,11 @@ export const WorkerPanel = memo(function WorkerPanel({
|
|
|
238
244
|
deleteConfirm.pressDelete(`worker log: ${basename(path)}`);
|
|
239
245
|
return;
|
|
240
246
|
}
|
|
247
|
+
|
|
248
|
+
if (key.ctrl && (input === "r" || input === "R")) {
|
|
249
|
+
forceRefresh();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
241
252
|
},
|
|
242
253
|
{ isActive },
|
|
243
254
|
);
|
|
@@ -257,8 +268,8 @@ export const WorkerPanel = memo(function WorkerPanel({
|
|
|
257
268
|
{focus === "detail"
|
|
258
269
|
? " · ↑↓ scroll ⇧↑↓ page g/G top/bot ← back to list l toggle"
|
|
259
270
|
: viewMode === "log"
|
|
260
|
-
? " · ↑↓ select → enter log l detail f filter d delete log (×2)"
|
|
261
|
-
: " · ↑↓ select → enter detail l view log f filter"}
|
|
271
|
+
? " · ↑↓ select → enter log l detail f filter d delete log (×2) ^R refresh"
|
|
272
|
+
: " · ↑↓ select → enter detail l view log f filter ^R refresh"}
|
|
262
273
|
</Text>
|
|
263
274
|
</Box>
|
|
264
275
|
<DeleteArmedBanner
|
package/src/tui/theme.ts
CHANGED