botholomew 0.15.6 → 0.16.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/README.md +6 -6
- package/package.json +2 -1
- package/src/chat/agent.ts +1 -1
- package/src/chat/usage.ts +1 -1
- package/src/cli.ts +2 -0
- package/src/commands/prompts.ts +333 -0
- package/src/constants.ts +1 -1
- package/src/context/capabilities.ts +4 -0
- package/src/init/index.ts +4 -4
- package/src/init/templates.ts +10 -16
- package/src/tools/prompt/create.ts +136 -0
- package/src/tools/prompt/delete.ts +103 -0
- package/src/tools/prompt/edit.ts +34 -13
- package/src/tools/prompt/list.ts +109 -0
- package/src/tools/prompt/read.ts +46 -14
- package/src/tools/registry.ts +6 -0
- package/src/tui/App.tsx +91 -109
- package/src/tui/components/ContextPanel.tsx +7 -5
- package/src/tui/components/MessageList.tsx +44 -21
- package/src/tui/components/SchedulePanel.tsx +7 -5
- package/src/tui/components/TaskPanel.tsx +7 -5
- package/src/tui/components/ThreadPanel.tsx +7 -5
- package/src/tui/components/ToolCall.tsx +3 -2
- package/src/tui/components/ToolPanel.tsx +9 -5
- package/src/tui/restoreMessages.ts +106 -0
- package/src/tui/useTerminalSize.ts +26 -0
- package/src/tui/wrapDetail.ts +15 -0
- package/src/utils/frontmatter.ts +93 -4
- package/src/worker/prompt.ts +29 -23
- package/src/worker/tick.ts +16 -7
package/src/tui/App.tsx
CHANGED
|
@@ -14,8 +14,7 @@ import {
|
|
|
14
14
|
handleSlashCommand,
|
|
15
15
|
type SlashCommand,
|
|
16
16
|
} from "../skills/commands.ts";
|
|
17
|
-
import { getThread
|
|
18
|
-
import { MAX_INLINE_CHARS, PAGE_SIZE_CHARS } from "../worker/large-results.ts";
|
|
17
|
+
import { getThread } from "../threads/store.ts";
|
|
19
18
|
import { ContextPanel } from "./components/ContextPanel.tsx";
|
|
20
19
|
import { HelpPanel } from "./components/HelpPanel.tsx";
|
|
21
20
|
import { InputBar } from "./components/InputBar.tsx";
|
|
@@ -35,6 +34,7 @@ import type { ToolCallData } from "./components/ToolCall.tsx";
|
|
|
35
34
|
import { ToolPanel } from "./components/ToolPanel.tsx";
|
|
36
35
|
import { WorkerPanel } from "./components/WorkerPanel.tsx";
|
|
37
36
|
import { IdleProvider, useIdle } from "./idle.tsx";
|
|
37
|
+
import { restoreMessagesFromInteractions } from "./restoreMessages.ts";
|
|
38
38
|
import { buildSlashCommands, getSlashMatches } from "./slashCompletion.ts";
|
|
39
39
|
import { ansi } from "./theme.ts";
|
|
40
40
|
|
|
@@ -50,6 +50,14 @@ function msgId(): string {
|
|
|
50
50
|
return `msg-${++nextMsgId}`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// Conservative line reservation for the bottom chrome — StatusBar (1) +
|
|
54
|
+
// bordered InputBar (3) + multiline hint (1) + TabBar (1) + slack for the
|
|
55
|
+
// SlashCommandPopup or QueuePanel (~4). The chat-tab body's `maxHeight` and
|
|
56
|
+
// the panel boxes' `height` both subtract this from `rows` so the dynamic
|
|
57
|
+
// frame's total output stays strictly below the viewport — see the comment
|
|
58
|
+
// on the `rows` state in `AppInner` for why that matters.
|
|
59
|
+
const FOOTER_RESERVE = 10;
|
|
60
|
+
|
|
53
61
|
// Tab routing: Ctrl+<letter> jumps to a tab. Chosen for memorability — first
|
|
54
62
|
// available letter that doesn't collide with other Ctrl bindings (Ctrl+C exit,
|
|
55
63
|
// Ctrl+J/K/X/E queue ops on Chat).
|
|
@@ -73,78 +81,6 @@ const TAB_BY_CTRL_KEY: Record<string, TabId> = {
|
|
|
73
81
|
_: 8, // help (terminals that send Ctrl+/ as 0x1F)
|
|
74
82
|
};
|
|
75
83
|
|
|
76
|
-
function detectToolError(output: string | undefined): boolean {
|
|
77
|
-
if (!output) return false;
|
|
78
|
-
try {
|
|
79
|
-
const parsed = JSON.parse(output);
|
|
80
|
-
if (typeof parsed === "object" && parsed?.is_error === true) return true;
|
|
81
|
-
} catch {
|
|
82
|
-
/* not JSON */
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function restoreMessagesFromInteractions(
|
|
88
|
-
interactions: Interaction[],
|
|
89
|
-
): ChatMessage[] {
|
|
90
|
-
const result: ChatMessage[] = [];
|
|
91
|
-
let pendingTools: ToolCallData[] = [];
|
|
92
|
-
|
|
93
|
-
let restoredIdx = 0;
|
|
94
|
-
for (const ix of interactions) {
|
|
95
|
-
if (ix.kind === "tool_use") {
|
|
96
|
-
pendingTools.push({
|
|
97
|
-
id: `restored-${restoredIdx++}`,
|
|
98
|
-
name: ix.tool_name ?? "unknown",
|
|
99
|
-
input: ix.tool_input ?? "{}",
|
|
100
|
-
running: false,
|
|
101
|
-
timestamp: ix.created_at,
|
|
102
|
-
});
|
|
103
|
-
} else if (ix.kind === "tool_result") {
|
|
104
|
-
const tc = pendingTools.find((t) => t.name === ix.tool_name && !t.output);
|
|
105
|
-
if (tc) {
|
|
106
|
-
tc.output = ix.content;
|
|
107
|
-
tc.isError = detectToolError(ix.content);
|
|
108
|
-
if (ix.content.length > MAX_INLINE_CHARS) {
|
|
109
|
-
tc.largeResult = {
|
|
110
|
-
id: "(restored)",
|
|
111
|
-
chars: ix.content.length,
|
|
112
|
-
pages: Math.ceil(ix.content.length / PAGE_SIZE_CHARS),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} else if (ix.kind === "message" && ix.role === "user") {
|
|
117
|
-
result.push({
|
|
118
|
-
id: msgId(),
|
|
119
|
-
role: "user",
|
|
120
|
-
content: ix.content,
|
|
121
|
-
timestamp: ix.created_at,
|
|
122
|
-
});
|
|
123
|
-
} else if (ix.kind === "message" && ix.role === "assistant") {
|
|
124
|
-
result.push({
|
|
125
|
-
id: msgId(),
|
|
126
|
-
role: "assistant",
|
|
127
|
-
content: ix.content,
|
|
128
|
-
timestamp: ix.created_at,
|
|
129
|
-
toolCalls: pendingTools.length > 0 ? [...pendingTools] : undefined,
|
|
130
|
-
});
|
|
131
|
-
pendingTools = [];
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (pendingTools.length > 0) {
|
|
136
|
-
result.push({
|
|
137
|
-
id: msgId(),
|
|
138
|
-
role: "assistant",
|
|
139
|
-
content: "",
|
|
140
|
-
timestamp: new Date(),
|
|
141
|
-
toolCalls: [...pendingTools],
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
84
|
export function App({
|
|
149
85
|
projectDir,
|
|
150
86
|
threadId: resumeThreadId,
|
|
@@ -176,14 +112,13 @@ function AppInner({
|
|
|
176
112
|
const { exit } = useApp();
|
|
177
113
|
const { stdout } = useStdout();
|
|
178
114
|
const { markActivity } = useIdle();
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
// `
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// — we have to pass the explicit row count and re-read it on resize.
|
|
115
|
+
// Track the terminal's row count so we can cap the dynamic frame strictly
|
|
116
|
+
// below fullscreen. Ink 7 wipes scrollback (`shouldClearTerminalForFrame`
|
|
117
|
+
// → `ansiEscapes.clearTerminal`) whenever the dynamic frame is overflowing
|
|
118
|
+
// or transitions out of fullscreen — so as long as the rendered output
|
|
119
|
+
// height stays < `rows` on every render, scrollback is preserved. The
|
|
120
|
+
// chat-tab body and the seven panel boxes use this value to set explicit
|
|
121
|
+
// height/maxHeight constraints.
|
|
187
122
|
const [rows, setRows] = useState(stdout?.rows ?? 24);
|
|
188
123
|
useEffect(() => {
|
|
189
124
|
if (!stdout) return;
|
|
@@ -207,6 +142,7 @@ function AppInner({
|
|
|
207
142
|
const [isLoading, setIsLoading] = useState(false);
|
|
208
143
|
const [streamingText, setStreamingText] = useState("");
|
|
209
144
|
const [activeToolCalls, setActiveToolCalls] = useState<ToolCallData[]>([]);
|
|
145
|
+
const [streamStartedAt, setStreamStartedAt] = useState<Date | null>(null);
|
|
210
146
|
const [preparingTool, setPreparingTool] = useState<{
|
|
211
147
|
id: string;
|
|
212
148
|
name: string;
|
|
@@ -245,7 +181,10 @@ function AppInner({
|
|
|
245
181
|
}
|
|
246
182
|
sessionRef.current = session;
|
|
247
183
|
|
|
248
|
-
if (
|
|
184
|
+
if (resumeThreadId) {
|
|
185
|
+
// Always hydrate on resume so the Tools tab and chat history
|
|
186
|
+
// pick up prior tool_use/tool_result rows from the CSV — even if
|
|
187
|
+
// the thread has no plain message-kind interactions yet.
|
|
249
188
|
const threadData = await getThread(
|
|
250
189
|
session.projectDir,
|
|
251
190
|
session.threadId,
|
|
@@ -474,6 +413,7 @@ function AppInner({
|
|
|
474
413
|
setStreamingText("");
|
|
475
414
|
setActiveToolCalls([]);
|
|
476
415
|
setPreparingTool(null);
|
|
416
|
+
setStreamStartedAt(new Date());
|
|
477
417
|
|
|
478
418
|
const userMsg: ChatMessage = {
|
|
479
419
|
id: msgId(),
|
|
@@ -501,6 +441,7 @@ function AppInner({
|
|
|
501
441
|
pendingToolCalls = [];
|
|
502
442
|
setStreamingText("");
|
|
503
443
|
setActiveToolCalls([]);
|
|
444
|
+
setStreamStartedAt(new Date());
|
|
504
445
|
}
|
|
505
446
|
};
|
|
506
447
|
|
|
@@ -532,30 +473,39 @@ function AppInner({
|
|
|
532
473
|
running: true,
|
|
533
474
|
timestamp: new Date(),
|
|
534
475
|
};
|
|
535
|
-
pendingToolCalls
|
|
536
|
-
setActiveToolCalls(
|
|
476
|
+
pendingToolCalls = [...pendingToolCalls, tc];
|
|
477
|
+
setActiveToolCalls(pendingToolCalls);
|
|
537
478
|
setPreparingTool(null);
|
|
538
479
|
},
|
|
539
480
|
onToolEnd: (id, _name, output, isError, meta) => {
|
|
540
481
|
markActivityRef.current();
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
482
|
+
// Replace the matched entry with a new object so its identity
|
|
483
|
+
// changes (memoized ToolCall children rely on this); other entries
|
|
484
|
+
// keep their reference and skip re-render.
|
|
485
|
+
pendingToolCalls = pendingToolCalls.map((t) =>
|
|
486
|
+
t.id === id
|
|
487
|
+
? {
|
|
488
|
+
...t,
|
|
489
|
+
running: false,
|
|
490
|
+
output,
|
|
491
|
+
isError,
|
|
492
|
+
...(meta?.largeResult
|
|
493
|
+
? { largeResult: meta.largeResult }
|
|
494
|
+
: {}),
|
|
495
|
+
}
|
|
496
|
+
: t,
|
|
497
|
+
);
|
|
498
|
+
setActiveToolCalls(pendingToolCalls);
|
|
551
499
|
},
|
|
552
500
|
onToolNotify: (id, message) => {
|
|
553
501
|
markActivityRef.current();
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
502
|
+
let touched = false;
|
|
503
|
+
pendingToolCalls = pendingToolCalls.map((t) => {
|
|
504
|
+
if (t.id !== id) return t;
|
|
505
|
+
touched = true;
|
|
506
|
+
return { ...t, notes: [...(t.notes ?? []), message] };
|
|
507
|
+
});
|
|
508
|
+
if (touched) setActiveToolCalls(pendingToolCalls);
|
|
559
509
|
},
|
|
560
510
|
onUsage: (info) => {
|
|
561
511
|
setUsage(info);
|
|
@@ -602,6 +552,7 @@ function AppInner({
|
|
|
602
552
|
setStreamingText("");
|
|
603
553
|
setActiveToolCalls([]);
|
|
604
554
|
setPreparingTool(null);
|
|
555
|
+
setStreamStartedAt(null);
|
|
605
556
|
}
|
|
606
557
|
}
|
|
607
558
|
|
|
@@ -757,6 +708,7 @@ function AppInner({
|
|
|
757
708
|
setStreamingText("");
|
|
758
709
|
setActiveToolCalls([]);
|
|
759
710
|
setPreparingTool(null);
|
|
711
|
+
setStreamStartedAt(null);
|
|
760
712
|
setUsage(null);
|
|
761
713
|
} catch (err) {
|
|
762
714
|
setMessages((prev) => [
|
|
@@ -848,8 +800,23 @@ function AppInner({
|
|
|
848
800
|
const _dbPath = sessionRef.current.dbPath;
|
|
849
801
|
const threadId = sessionRef.current.threadId;
|
|
850
802
|
|
|
803
|
+
const panelHeight = Math.max(1, rows - FOOTER_RESERVE);
|
|
804
|
+
const onChatTab = activeTab === 1;
|
|
805
|
+
|
|
851
806
|
return (
|
|
852
|
-
|
|
807
|
+
// The root box is auto-sized on the chat tab so the dynamic frame stays
|
|
808
|
+
// small and the static-rendered chat history (in scrollback above the
|
|
809
|
+
// frame) flows directly into the streaming reply with no blank pad.
|
|
810
|
+
//
|
|
811
|
+
// On every other tab we pin the root to `height={rows}` so the dynamic
|
|
812
|
+
// frame fills the entire viewport — without that, the panel + footer
|
|
813
|
+
// are shorter than the terminal and the bottom of the chat scrollback
|
|
814
|
+
// bleeds through above the active panel. Switching chat→panel goes
|
|
815
|
+
// small→rows (no wipe, since `nextOutputHeight === viewportRows` is
|
|
816
|
+
// not "overflowing"). Switching panel→chat goes rows→small, which
|
|
817
|
+
// does trip Ink's `isLeavingFullscreen` clear, but Ink immediately
|
|
818
|
+
// re-emits `fullStaticOutput` so chat history is preserved.
|
|
819
|
+
<Box flexDirection="column" {...(onChatTab ? {} : { height: rows })}>
|
|
853
820
|
{/* Completed messages — rendered once to terminal scrollback.
|
|
854
821
|
Must live outside the display="none" tab wrappers so the <Static>
|
|
855
822
|
node always has proper terminal width in its Yoga layout.
|
|
@@ -862,16 +829,23 @@ function AppInner({
|
|
|
862
829
|
{/* Tab content area — all panels stay mounted to avoid expensive
|
|
863
830
|
remount cycles. display="none" hides inactive panels from
|
|
864
831
|
layout without destroying them.
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
832
|
+
|
|
833
|
+
Chat tab: `maxHeight={panelHeight}` (not `height`) so the box
|
|
834
|
+
shrinks to its content when streaming is short or absent. When
|
|
835
|
+
streaming overflows, the box stops at `panelHeight`;
|
|
836
|
+
`justifyContent="flex-end"` + `overflow="hidden"` clip the *top*
|
|
837
|
+
so the most-recent tokens stay visible above the input bar.
|
|
838
|
+
The frame stays strictly below `rows`, so Ink never wipes
|
|
839
|
+
scrollback during a turn.
|
|
840
|
+
|
|
841
|
+
Other tabs: `flexGrow={1}` fills the root (which is pinned to
|
|
842
|
+
`rows` on those tabs) minus the footer's actual height, so the
|
|
843
|
+
panel always reaches the top of the viewport — no scrollback
|
|
844
|
+
leak above the panel regardless of footer height. */}
|
|
871
845
|
<Box
|
|
872
|
-
display={
|
|
846
|
+
display={onChatTab ? "flex" : "none"}
|
|
873
847
|
flexDirection="column"
|
|
874
|
-
|
|
848
|
+
maxHeight={panelHeight}
|
|
875
849
|
overflow="hidden"
|
|
876
850
|
justifyContent="flex-end"
|
|
877
851
|
>
|
|
@@ -880,12 +854,14 @@ function AppInner({
|
|
|
880
854
|
isLoading={isLoading}
|
|
881
855
|
activeToolCalls={activeToolCalls}
|
|
882
856
|
preparingTool={preparingTool}
|
|
857
|
+
streamStartedAt={streamStartedAt}
|
|
883
858
|
/>
|
|
884
859
|
</Box>
|
|
885
860
|
<Box
|
|
886
861
|
display={activeTab === 2 ? "flex" : "none"}
|
|
887
862
|
flexDirection="column"
|
|
888
863
|
flexGrow={1}
|
|
864
|
+
overflow="hidden"
|
|
889
865
|
>
|
|
890
866
|
<ToolPanel toolCalls={allToolCalls} isActive={activeTab === 2} />
|
|
891
867
|
</Box>
|
|
@@ -893,6 +869,7 @@ function AppInner({
|
|
|
893
869
|
display={activeTab === 3 ? "flex" : "none"}
|
|
894
870
|
flexDirection="column"
|
|
895
871
|
flexGrow={1}
|
|
872
|
+
overflow="hidden"
|
|
896
873
|
>
|
|
897
874
|
<ContextPanel projectDir={projectDir} isActive={activeTab === 3} />
|
|
898
875
|
</Box>
|
|
@@ -900,6 +877,7 @@ function AppInner({
|
|
|
900
877
|
display={activeTab === 4 ? "flex" : "none"}
|
|
901
878
|
flexDirection="column"
|
|
902
879
|
flexGrow={1}
|
|
880
|
+
overflow="hidden"
|
|
903
881
|
>
|
|
904
882
|
<TaskPanel projectDir={projectDir} isActive={activeTab === 4} />
|
|
905
883
|
</Box>
|
|
@@ -907,6 +885,7 @@ function AppInner({
|
|
|
907
885
|
display={activeTab === 5 ? "flex" : "none"}
|
|
908
886
|
flexDirection="column"
|
|
909
887
|
flexGrow={1}
|
|
888
|
+
overflow="hidden"
|
|
910
889
|
>
|
|
911
890
|
<ThreadPanel
|
|
912
891
|
projectDir={projectDir}
|
|
@@ -918,6 +897,7 @@ function AppInner({
|
|
|
918
897
|
display={activeTab === 6 ? "flex" : "none"}
|
|
919
898
|
flexDirection="column"
|
|
920
899
|
flexGrow={1}
|
|
900
|
+
overflow="hidden"
|
|
921
901
|
>
|
|
922
902
|
<SchedulePanel projectDir={projectDir} isActive={activeTab === 6} />
|
|
923
903
|
</Box>
|
|
@@ -925,6 +905,7 @@ function AppInner({
|
|
|
925
905
|
display={activeTab === 7 ? "flex" : "none"}
|
|
926
906
|
flexDirection="column"
|
|
927
907
|
flexGrow={1}
|
|
908
|
+
overflow="hidden"
|
|
928
909
|
>
|
|
929
910
|
<WorkerPanel projectDir={projectDir} isActive={activeTab === 7} />
|
|
930
911
|
</Box>
|
|
@@ -932,6 +913,7 @@ function AppInner({
|
|
|
932
913
|
display={activeTab === 8 ? "flex" : "none"}
|
|
933
914
|
flexDirection="column"
|
|
934
915
|
flexGrow={1}
|
|
916
|
+
overflow="hidden"
|
|
935
917
|
>
|
|
936
918
|
<HelpPanel
|
|
937
919
|
projectDir={projectDir}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Text, useInput
|
|
1
|
+
import { Box, Text, useInput } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { getDbPath } from "../../constants.ts";
|
|
4
4
|
import {
|
|
@@ -24,6 +24,8 @@ import { isMarkdownPath, renderMarkdown } from "../markdown.ts";
|
|
|
24
24
|
import { theme } from "../theme.ts";
|
|
25
25
|
import { useDeleteConfirm } from "../useDeleteConfirm.ts";
|
|
26
26
|
import { useLatestRef } from "../useLatestRef.ts";
|
|
27
|
+
import { useTerminalSize } from "../useTerminalSize.ts";
|
|
28
|
+
import { wrapDetailLines } from "../wrapDetail.ts";
|
|
27
29
|
import { DeleteArmedBanner } from "./DeleteArmedBanner.tsx";
|
|
28
30
|
import { Scrollbar } from "./Scrollbar.tsx";
|
|
29
31
|
|
|
@@ -39,8 +41,8 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
39
41
|
projectDir,
|
|
40
42
|
isActive,
|
|
41
43
|
}: ContextPanelProps) {
|
|
42
|
-
const {
|
|
43
|
-
const
|
|
44
|
+
const { rows: termRows, cols: termCols } = useTerminalSize();
|
|
45
|
+
const detailWidth = Math.max(1, termCols - SIDEBAR_WIDTH - 5);
|
|
44
46
|
|
|
45
47
|
const [currentPath, setCurrentPath] = useState("");
|
|
46
48
|
const [entries, setEntries] = useState<ContextEntry[]>([]);
|
|
@@ -150,8 +152,8 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
150
152
|
const body = isMarkdownPath(fileContent.path)
|
|
151
153
|
? renderMarkdown(fileContent.content)
|
|
152
154
|
: fileContent.content;
|
|
153
|
-
return body
|
|
154
|
-
}, [fileContent, selectedEntry]);
|
|
155
|
+
return wrapDetailLines(body, detailWidth);
|
|
156
|
+
}, [fileContent, selectedEntry, detailWidth]);
|
|
155
157
|
|
|
156
158
|
const visibleDetailRows = Math.max(1, visibleRows - 2);
|
|
157
159
|
const maxDetailScroll = Math.max(0, detailLines.length - visibleDetailRows);
|
|
@@ -18,6 +18,9 @@ interface MessageListProps {
|
|
|
18
18
|
isLoading: boolean;
|
|
19
19
|
activeToolCalls: ToolCallData[];
|
|
20
20
|
preparingTool: { id: string; name: string } | null;
|
|
21
|
+
/** Timestamp the current streaming bubble started. Stable across token flushes
|
|
22
|
+
* so the displayed time doesn't flicker on every re-render. */
|
|
23
|
+
streamStartedAt: Date | null;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
function formatTime(date: Date): string {
|
|
@@ -124,11 +127,46 @@ export const MessageBubble = memo(function MessageBubble({
|
|
|
124
127
|
);
|
|
125
128
|
});
|
|
126
129
|
|
|
130
|
+
const ActiveToolsBox = memo(function ActiveToolsBox({
|
|
131
|
+
toolCalls,
|
|
132
|
+
}: {
|
|
133
|
+
toolCalls: ToolCallData[];
|
|
134
|
+
}) {
|
|
135
|
+
if (toolCalls.length === 0) return null;
|
|
136
|
+
return (
|
|
137
|
+
<Box
|
|
138
|
+
flexDirection="column"
|
|
139
|
+
marginLeft={1}
|
|
140
|
+
borderStyle="round"
|
|
141
|
+
borderColor={theme.accentBorder}
|
|
142
|
+
paddingX={1}
|
|
143
|
+
>
|
|
144
|
+
{toolCalls.map((tc) => (
|
|
145
|
+
<ToolCall key={tc.id} tool={tc} />
|
|
146
|
+
))}
|
|
147
|
+
</Box>
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const StreamingMarkdown = memo(function StreamingMarkdown({
|
|
152
|
+
text,
|
|
153
|
+
}: {
|
|
154
|
+
text: string;
|
|
155
|
+
}) {
|
|
156
|
+
const rendered = useMemo(() => renderMarkdown(text), [text]);
|
|
157
|
+
return (
|
|
158
|
+
<Box marginLeft={1}>
|
|
159
|
+
<Text>{rendered}</Text>
|
|
160
|
+
</Box>
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
127
164
|
export function MessageList({
|
|
128
165
|
streamingText,
|
|
129
166
|
isLoading,
|
|
130
167
|
activeToolCalls,
|
|
131
168
|
preparingTool,
|
|
169
|
+
streamStartedAt,
|
|
132
170
|
}: MessageListProps) {
|
|
133
171
|
return (
|
|
134
172
|
<>
|
|
@@ -139,26 +177,10 @@ export function MessageList({
|
|
|
139
177
|
<Text bold color="green">
|
|
140
178
|
Botholomew
|
|
141
179
|
</Text>
|
|
142
|
-
<Text dimColor> {formatTime(new Date())}</Text>
|
|
180
|
+
<Text dimColor> {formatTime(streamStartedAt ?? new Date())}</Text>
|
|
143
181
|
</Box>
|
|
144
|
-
{activeToolCalls
|
|
145
|
-
|
|
146
|
-
flexDirection="column"
|
|
147
|
-
marginLeft={1}
|
|
148
|
-
borderStyle="round"
|
|
149
|
-
borderColor={theme.accentBorder}
|
|
150
|
-
paddingX={1}
|
|
151
|
-
>
|
|
152
|
-
{activeToolCalls.map((tc) => (
|
|
153
|
-
<ToolCall key={tc.id} tool={tc} />
|
|
154
|
-
))}
|
|
155
|
-
</Box>
|
|
156
|
-
)}
|
|
157
|
-
{streamingText && (
|
|
158
|
-
<Box marginLeft={1}>
|
|
159
|
-
<Text>{renderMarkdown(streamingText)}</Text>
|
|
160
|
-
</Box>
|
|
161
|
-
)}
|
|
182
|
+
<ActiveToolsBox toolCalls={activeToolCalls} />
|
|
183
|
+
{streamingText && <StreamingMarkdown text={streamingText} />}
|
|
162
184
|
</Box>
|
|
163
185
|
)}
|
|
164
186
|
|
|
@@ -173,14 +195,15 @@ export function MessageList({
|
|
|
173
195
|
|
|
174
196
|
{isLoading &&
|
|
175
197
|
!preparingTool &&
|
|
176
|
-
!streamingText &&
|
|
177
198
|
(activeToolCalls.length === 0 ||
|
|
178
199
|
activeToolCalls.every((tc) => !tc.running)) && (
|
|
179
200
|
<Box marginTop={1}>
|
|
180
201
|
<Text color={theme.accent}>
|
|
181
202
|
<Spinner type="dots" />
|
|
182
203
|
</Text>
|
|
183
|
-
<Text dimColor>
|
|
204
|
+
<Text dimColor>
|
|
205
|
+
{streamingText ? " Streaming..." : " Thinking..."}
|
|
206
|
+
</Text>
|
|
184
207
|
</Box>
|
|
185
208
|
)}
|
|
186
209
|
</>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Text, useInput
|
|
1
|
+
import { Box, Text, useInput } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import type { Schedule } from "../../schedules/schema.ts";
|
|
4
4
|
import {
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
import { ansi, theme } from "../theme.ts";
|
|
15
15
|
import { useDeleteConfirm } from "../useDeleteConfirm.ts";
|
|
16
16
|
import { useLatestRef } from "../useLatestRef.ts";
|
|
17
|
+
import { useTerminalSize } from "../useTerminalSize.ts";
|
|
18
|
+
import { wrapDetailLines } from "../wrapDetail.ts";
|
|
17
19
|
import { DeleteArmedBanner } from "./DeleteArmedBanner.tsx";
|
|
18
20
|
import { Scrollbar } from "./Scrollbar.tsx";
|
|
19
21
|
|
|
@@ -88,8 +90,8 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
88
90
|
projectDir,
|
|
89
91
|
isActive,
|
|
90
92
|
}: SchedulePanelProps) {
|
|
91
|
-
const {
|
|
92
|
-
const
|
|
93
|
+
const { rows: termRows, cols: termCols } = useTerminalSize();
|
|
94
|
+
const detailWidth = Math.max(1, termCols - SIDEBAR_WIDTH - 5);
|
|
93
95
|
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
|
94
96
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
95
97
|
const [detailScroll, setDetailScroll] = useState(0);
|
|
@@ -133,8 +135,8 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
133
135
|
}, [selectedSchedule]);
|
|
134
136
|
|
|
135
137
|
const detailLines = useMemo(
|
|
136
|
-
() => renderedDetail
|
|
137
|
-
[renderedDetail],
|
|
138
|
+
() => wrapDetailLines(renderedDetail, detailWidth),
|
|
139
|
+
[renderedDetail, detailWidth],
|
|
138
140
|
);
|
|
139
141
|
|
|
140
142
|
const visibleRows = Math.max(1, termRows - 6);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Text, useInput
|
|
1
|
+
import { Box, Text, useInput } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import {
|
|
4
4
|
TASK_PRIORITIES,
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
import { ansi, theme } from "../theme.ts";
|
|
15
15
|
import { useDeleteConfirm } from "../useDeleteConfirm.ts";
|
|
16
16
|
import { useLatestRef } from "../useLatestRef.ts";
|
|
17
|
+
import { useTerminalSize } from "../useTerminalSize.ts";
|
|
18
|
+
import { wrapDetailLines } from "../wrapDetail.ts";
|
|
17
19
|
import { DeleteArmedBanner } from "./DeleteArmedBanner.tsx";
|
|
18
20
|
import { Scrollbar } from "./Scrollbar.tsx";
|
|
19
21
|
|
|
@@ -121,8 +123,8 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
121
123
|
projectDir,
|
|
122
124
|
isActive,
|
|
123
125
|
}: TaskPanelProps) {
|
|
124
|
-
const {
|
|
125
|
-
const
|
|
126
|
+
const { rows: termRows, cols: termCols } = useTerminalSize();
|
|
127
|
+
const detailWidth = Math.max(1, termCols - SIDEBAR_WIDTH - 5);
|
|
126
128
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
127
129
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
128
130
|
const [detailScroll, setDetailScroll] = useState(0);
|
|
@@ -173,8 +175,8 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
173
175
|
}, [selectedTask]);
|
|
174
176
|
|
|
175
177
|
const detailLines = useMemo(
|
|
176
|
-
() => renderedDetail
|
|
177
|
-
[renderedDetail],
|
|
178
|
+
() => wrapDetailLines(renderedDetail, detailWidth),
|
|
179
|
+
[renderedDetail, detailWidth],
|
|
178
180
|
);
|
|
179
181
|
|
|
180
182
|
const visibleRows = Math.max(1, termRows - 6);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Text, useInput
|
|
1
|
+
import { Box, Text, useInput } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import {
|
|
4
4
|
deleteThread,
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
import { ansi, theme } from "../theme.ts";
|
|
18
18
|
import { useDeleteConfirm } from "../useDeleteConfirm.ts";
|
|
19
19
|
import { useLatestRef } from "../useLatestRef.ts";
|
|
20
|
+
import { useTerminalSize } from "../useTerminalSize.ts";
|
|
21
|
+
import { wrapDetailLines } from "../wrapDetail.ts";
|
|
20
22
|
import { DeleteArmedBanner } from "./DeleteArmedBanner.tsx";
|
|
21
23
|
import { Scrollbar } from "./Scrollbar.tsx";
|
|
22
24
|
|
|
@@ -162,8 +164,8 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
162
164
|
activeThreadId,
|
|
163
165
|
isActive,
|
|
164
166
|
}: ThreadPanelProps) {
|
|
165
|
-
const {
|
|
166
|
-
const
|
|
167
|
+
const { rows: termRows, cols: termCols } = useTerminalSize();
|
|
168
|
+
const detailWidth = Math.max(1, termCols - SIDEBAR_WIDTH - 5);
|
|
167
169
|
const [threads, setThreads] = useState<Thread[]>([]);
|
|
168
170
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
169
171
|
const [detailScroll, setDetailScroll] = useState(0);
|
|
@@ -299,8 +301,8 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
299
301
|
}, [selectedDetail, activeThreadId]);
|
|
300
302
|
|
|
301
303
|
const detailLines = useMemo(
|
|
302
|
-
() => renderedDetail
|
|
303
|
-
[renderedDetail],
|
|
304
|
+
() => wrapDetailLines(renderedDetail, detailWidth),
|
|
305
|
+
[renderedDetail, detailWidth],
|
|
304
306
|
);
|
|
305
307
|
|
|
306
308
|
const visibleRows = Math.max(1, termRows - 6);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Box, Text } from "ink";
|
|
2
|
+
import { memo } from "react";
|
|
2
3
|
import { theme } from "../theme.ts";
|
|
3
4
|
import { parseSleepInput, SleepProgress } from "./SleepProgress.tsx";
|
|
4
5
|
|
|
@@ -48,7 +49,7 @@ interface ToolCallProps {
|
|
|
48
49
|
tool: ToolCallData;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
export function ToolCall({ tool }: ToolCallProps) {
|
|
52
|
+
export const ToolCall = memo(function ToolCall({ tool }: ToolCallProps) {
|
|
52
53
|
const { displayName, displayInput } = resolveToolDisplay(
|
|
53
54
|
tool.name,
|
|
54
55
|
tool.input,
|
|
@@ -122,4 +123,4 @@ export function ToolCall({ tool }: ToolCallProps) {
|
|
|
122
123
|
))}
|
|
123
124
|
</Box>
|
|
124
125
|
);
|
|
125
|
-
}
|
|
126
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Text, useInput
|
|
1
|
+
import { Box, Text, useInput } from "ink";
|
|
2
2
|
import { memo, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import {
|
|
4
4
|
detailPaneBorderProps,
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
} from "../listDetailKeys.ts";
|
|
8
8
|
import { ansi, theme } from "../theme.ts";
|
|
9
9
|
import { useLatestRef } from "../useLatestRef.ts";
|
|
10
|
+
import { useTerminalSize } from "../useTerminalSize.ts";
|
|
11
|
+
import { wrapDetailLines } from "../wrapDetail.ts";
|
|
10
12
|
import { Scrollbar } from "./Scrollbar.tsx";
|
|
11
13
|
import { resolveToolDisplay, type ToolCallData } from "./ToolCall.tsx";
|
|
12
14
|
|
|
@@ -109,8 +111,10 @@ export const ToolPanel = memo(function ToolPanel({
|
|
|
109
111
|
toolCalls,
|
|
110
112
|
isActive,
|
|
111
113
|
}: ToolPanelProps) {
|
|
112
|
-
const {
|
|
113
|
-
|
|
114
|
+
const { rows: termRows, cols: termCols } = useTerminalSize();
|
|
115
|
+
// Detail-pane content width: total cols minus sidebar, minus 4 chars of
|
|
116
|
+
// border+padding on the right pane, minus 1 col for the scrollbar.
|
|
117
|
+
const detailWidth = Math.max(1, termCols - SIDEBAR_WIDTH - 5);
|
|
114
118
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
115
119
|
const [detailScroll, setDetailScroll] = useState(0);
|
|
116
120
|
const [focus, setFocus] = useState<FocusState>("list");
|
|
@@ -133,8 +137,8 @@ export const ToolPanel = memo(function ToolPanel({
|
|
|
133
137
|
}, [selectedTool]);
|
|
134
138
|
|
|
135
139
|
const detailLines = useMemo(
|
|
136
|
-
() => renderedDetail
|
|
137
|
-
[renderedDetail],
|
|
140
|
+
() => wrapDetailLines(renderedDetail, detailWidth),
|
|
141
|
+
[renderedDetail, detailWidth],
|
|
138
142
|
);
|
|
139
143
|
|
|
140
144
|
// Visible area for sidebar and detail
|