opencode-top 3.0.0 → 3.1.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
CHANGED
package/src/ui/App.tsx
CHANGED
|
@@ -108,7 +108,7 @@ export function App({ refreshInterval = 2000 }: AppProps) {
|
|
|
108
108
|
const contentHeight = terminalHeight - 3;
|
|
109
109
|
|
|
110
110
|
return (
|
|
111
|
-
<Box flexDirection="column" width={terminalWidth}>
|
|
111
|
+
<Box flexDirection="column" width={terminalWidth} height={terminalHeight}>
|
|
112
112
|
<TabBar activeScreen={screen} lastRefresh={lastRefresh} />
|
|
113
113
|
<Box width={terminalWidth} height={contentHeight}>
|
|
114
114
|
{screen === "sessions" && (
|
|
@@ -30,14 +30,17 @@ function AgentTreeInner({ workflows, selectedId, flatNodes, maxHeight = 20 }: Ag
|
|
|
30
30
|
const visibleCount = Math.max(1, maxHeight - headerHeight);
|
|
31
31
|
const selectedIndex = flatNodes.findIndex((n) => n.id === selectedId);
|
|
32
32
|
|
|
33
|
+
// Keep selected item visible with some context above
|
|
33
34
|
let startIndex = 0;
|
|
34
|
-
if (selectedIndex >=
|
|
35
|
-
|
|
35
|
+
if (selectedIndex >= 0) {
|
|
36
|
+
// Try to show 2 items above the selected one
|
|
37
|
+
const ideal = selectedIndex - 2;
|
|
38
|
+
startIndex = Math.max(0, Math.min(ideal, flatNodes.length - visibleCount));
|
|
36
39
|
}
|
|
37
40
|
const visibleNodes = flatNodes.slice(startIndex, startIndex + visibleCount);
|
|
38
41
|
|
|
39
42
|
return (
|
|
40
|
-
<Box flexDirection="column" paddingX={1}>
|
|
43
|
+
<Box flexDirection="column" paddingX={1} height={maxHeight} overflow="hidden">
|
|
41
44
|
<Box marginBottom={1} flexDirection="row">
|
|
42
45
|
<Text color={colors.accent} bold>Sessions</Text>
|
|
43
46
|
<Box flexGrow={1} />
|
|
@@ -14,6 +14,7 @@ import { AgentChainGraph } from "./AgentChainGraph";
|
|
|
14
14
|
|
|
15
15
|
interface DetailsPanelProps {
|
|
16
16
|
workflow: Workflow | null;
|
|
17
|
+
height?: number;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function StatRow({
|
|
@@ -73,7 +74,7 @@ function formatTokens(n: number): string {
|
|
|
73
74
|
return n.toString();
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
function DetailsPanelInner({ workflow }: DetailsPanelProps) {
|
|
77
|
+
function DetailsPanelInner({ workflow, height }: DetailsPanelProps) {
|
|
77
78
|
const data = useMemo(() => {
|
|
78
79
|
if (!workflow) return null;
|
|
79
80
|
|
|
@@ -121,14 +122,14 @@ function DetailsPanelInner({ workflow }: DetailsPanelProps) {
|
|
|
121
122
|
|
|
122
123
|
if (!data) {
|
|
123
124
|
return (
|
|
124
|
-
<Box flexDirection="column" paddingX={1}>
|
|
125
|
+
<Box flexDirection="column" paddingX={1} height={height}>
|
|
125
126
|
<Text color={colors.textDim}>Select a session</Text>
|
|
126
127
|
</Box>
|
|
127
128
|
);
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
return (
|
|
131
|
-
<Box flexDirection="column" paddingX={1}>
|
|
132
|
+
<Box flexDirection="column" paddingX={1} height={height} overflow="hidden">
|
|
132
133
|
<Box marginBottom={1}>
|
|
133
134
|
<Text color={colors.accent} bold>
|
|
134
135
|
Details
|
|
@@ -1,59 +1,106 @@
|
|
|
1
|
-
import React, { useMemo, memo } from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
1
|
+
import React, { useMemo, memo, useEffect, useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import { colors } from "../theme";
|
|
4
4
|
import type { Session, Interaction, MessagePart } from "../../core/types";
|
|
5
5
|
|
|
6
6
|
interface MessagesPanelProps {
|
|
7
7
|
session: Session | null;
|
|
8
|
-
scrollOffset: number;
|
|
9
|
-
onScrollOffsetChange: (offset: number) => void;
|
|
10
8
|
maxHeight: number;
|
|
9
|
+
isActive: boolean;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
type MsgLine =
|
|
14
|
-
| { kind: "
|
|
15
|
-
| { kind: "tool
|
|
13
|
+
| { kind: "header"; modelId: string; agent: string | null; duration: string; time: string }
|
|
14
|
+
| { kind: "tool"; callId: string; icon: string; iconColor: string; name: string; title: string; right: string; expanded: boolean }
|
|
15
|
+
| { kind: "tool-detail"; label: string; value: string; isSection: boolean }
|
|
16
16
|
| { kind: "text"; text: string }
|
|
17
|
-
| { kind: "reasoning"; text: string }
|
|
18
|
-
| { kind: "spacer" };
|
|
17
|
+
| { kind: "reasoning"; text: string };
|
|
19
18
|
|
|
20
|
-
function buildLines(session: Session): MsgLine[] {
|
|
19
|
+
function buildLines(session: Session, contentWidth: number, expandedIds: Set<string>): MsgLine[] {
|
|
21
20
|
const lines: MsgLine[] = [];
|
|
22
21
|
for (const interaction of session.interactions) {
|
|
23
22
|
if (interaction.role !== "assistant") continue;
|
|
24
|
-
|
|
23
|
+
|
|
24
|
+
const dur =
|
|
25
|
+
interaction.time.completed && interaction.time.created
|
|
26
|
+
? formatDuration(interaction.time.completed - interaction.time.created)
|
|
27
|
+
: "";
|
|
28
|
+
|
|
29
|
+
lines.push({
|
|
30
|
+
kind: "header",
|
|
31
|
+
modelId: interaction.modelId,
|
|
32
|
+
agent: interaction.agent ?? null,
|
|
33
|
+
duration: dur,
|
|
34
|
+
time: formatTime(interaction.time.created),
|
|
35
|
+
});
|
|
36
|
+
|
|
25
37
|
for (const part of interaction.parts) {
|
|
26
38
|
if (part.type === "tool") {
|
|
27
|
-
|
|
39
|
+
const p = part as MessagePart & { type: "tool" };
|
|
40
|
+
const icon = p.status === "completed" ? "✓" : p.status === "error" ? "✗" : "◌";
|
|
41
|
+
const iconColor =
|
|
42
|
+
p.status === "completed" ? colors.success : p.status === "error" ? colors.error : colors.warning;
|
|
43
|
+
const dur2 = p.timeEnd > 0 && p.timeStart > 0 ? formatDuration(p.timeEnd - p.timeStart) : "";
|
|
44
|
+
const exitStr = p.exitCode !== null && p.exitCode !== 0 ? `exit:${p.exitCode} ` : "";
|
|
45
|
+
const expanded = expandedIds.has(p.callId);
|
|
46
|
+
|
|
47
|
+
lines.push({
|
|
48
|
+
kind: "tool",
|
|
49
|
+
callId: p.callId,
|
|
50
|
+
icon,
|
|
51
|
+
iconColor,
|
|
52
|
+
name: truncate(p.toolName, 18),
|
|
53
|
+
title: p.title ? truncate(p.title, 28) : "",
|
|
54
|
+
right: exitStr + dur2,
|
|
55
|
+
expanded,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (expanded) {
|
|
59
|
+
const inputKeys = Object.keys(p.input);
|
|
60
|
+
if (inputKeys.length > 0) {
|
|
61
|
+
lines.push({ kind: "tool-detail", label: "input", value: "", isSection: true });
|
|
62
|
+
for (const key of inputKeys) {
|
|
63
|
+
const val = formatParamValue(p.input[key], contentWidth - key.length - 6);
|
|
64
|
+
lines.push({ kind: "tool-detail", label: key, value: val, isSection: false });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (p.output?.trim()) {
|
|
68
|
+
lines.push({ kind: "tool-detail", label: "output", value: "", isSection: true });
|
|
69
|
+
const outLines = p.output.trim().split("\n").slice(0, 40);
|
|
70
|
+
for (const ol of outLines) {
|
|
71
|
+
for (const wrapped of wrapText(ol === "" ? " " : ol, contentWidth - 5)) {
|
|
72
|
+
lines.push({ kind: "tool-detail", label: "", value: wrapped, isSection: false });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
28
77
|
} else if (part.type === "text" && part.text.trim()) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for (const line of wrapped) {
|
|
32
|
-
lines.push({ kind: "text", text: line });
|
|
78
|
+
for (const row of wrapText(part.text.trim(), contentWidth - 3)) {
|
|
79
|
+
lines.push({ kind: "text", text: row });
|
|
33
80
|
}
|
|
34
81
|
} else if (part.type === "reasoning" && part.text.trim()) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
lines.push({ kind: "reasoning", text: line });
|
|
82
|
+
for (const row of wrapText(part.text.trim(), contentWidth - 5)) {
|
|
83
|
+
lines.push({ kind: "reasoning", text: row });
|
|
38
84
|
}
|
|
39
85
|
}
|
|
40
86
|
}
|
|
41
|
-
lines.push({ kind: "spacer" });
|
|
42
87
|
}
|
|
43
88
|
return lines;
|
|
44
89
|
}
|
|
45
90
|
|
|
91
|
+
function formatParamValue(val: unknown, maxLen: number): string {
|
|
92
|
+
if (val === null || val === undefined) return "null";
|
|
93
|
+
if (typeof val === "string") return truncate(val.split("\n")[0], Math.max(20, maxLen));
|
|
94
|
+
if (typeof val === "number" || typeof val === "boolean") return String(val);
|
|
95
|
+
try { return truncate(JSON.stringify(val), Math.max(20, maxLen)); }
|
|
96
|
+
catch { return String(val); }
|
|
97
|
+
}
|
|
98
|
+
|
|
46
99
|
function wrapText(text: string, maxLen: number): string[] {
|
|
47
|
-
const rawLines = text.split("\n");
|
|
48
100
|
const out: string[] = [];
|
|
49
|
-
for (const raw of
|
|
50
|
-
if (raw.length <= maxLen) {
|
|
51
|
-
|
|
52
|
-
} else {
|
|
53
|
-
for (let i = 0; i < raw.length; i += maxLen) {
|
|
54
|
-
out.push(raw.slice(i, i + maxLen));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
101
|
+
for (const raw of text.split("\n")) {
|
|
102
|
+
if (raw.length <= maxLen) { out.push(raw); }
|
|
103
|
+
else { for (let i = 0; i < raw.length; i += maxLen) out.push(raw.slice(i, i + maxLen)); }
|
|
57
104
|
}
|
|
58
105
|
return out;
|
|
59
106
|
}
|
|
@@ -69,87 +116,104 @@ function formatDuration(ms: number): string {
|
|
|
69
116
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
70
117
|
}
|
|
71
118
|
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const dur =
|
|
77
|
-
i.time.completed && i.time.created
|
|
78
|
-
? i.time.completed - i.time.created
|
|
79
|
-
: 0;
|
|
80
|
-
return (
|
|
81
|
-
<Box key={idx} flexDirection="row" marginTop={1}>
|
|
82
|
-
<Text color={colors.purple} bold>◆ </Text>
|
|
83
|
-
<Text color={colors.info}>{truncate(i.modelId, 28)}</Text>
|
|
84
|
-
{i.agent && <Text color={colors.cyan}> [{i.agent}]</Text>}
|
|
85
|
-
<Box flexGrow={1} />
|
|
86
|
-
{dur > 0 && <Text color={colors.textDim}>{formatDuration(dur)} </Text>}
|
|
87
|
-
<Text color={colors.textDim}>{formatTime(i.time.created)}</Text>
|
|
88
|
-
</Box>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
119
|
+
function truncate(s: string, max: number): string {
|
|
120
|
+
if (!s || s.length <= max) return s;
|
|
121
|
+
return s.slice(0, max - 1) + "…";
|
|
122
|
+
}
|
|
91
123
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
p.status === "completed" ? colors.success : p.status === "error" ? colors.error : colors.warning;
|
|
97
|
-
const dur = p.timeEnd > 0 && p.timeStart > 0 ? p.timeEnd - p.timeStart : 0;
|
|
98
|
-
const exitStr = p.exitCode !== null && p.exitCode !== 0 ? ` exit:${p.exitCode}` : "";
|
|
99
|
-
return (
|
|
100
|
-
<Box key={idx} flexDirection="column" paddingLeft={2}>
|
|
101
|
-
<Box flexDirection="row">
|
|
102
|
-
<Text color={statusColor}>{statusIcon} </Text>
|
|
103
|
-
<Text color={colors.text} bold>{truncate(p.toolName, 20)}</Text>
|
|
104
|
-
{p.title && <Text color={colors.textDim}> {truncate(p.title, 30)}</Text>}
|
|
105
|
-
<Box flexGrow={1} />
|
|
106
|
-
{exitStr && <Text color={colors.error}>{exitStr} </Text>}
|
|
107
|
-
{dur > 0 && <Text color={colors.textDim}>{formatDuration(dur)}</Text>}
|
|
108
|
-
</Box>
|
|
109
|
-
{/* Show output preview for errors or short outputs */}
|
|
110
|
-
{(p.status === "error" || (p.output && p.output.length < 200 && p.output.trim())) && (
|
|
111
|
-
<Box paddingLeft={2}>
|
|
112
|
-
<Text color={p.status === "error" ? colors.error : colors.textDim}>
|
|
113
|
-
{truncate(p.output.trim(), 120)}
|
|
114
|
-
</Text>
|
|
115
|
-
</Box>
|
|
116
|
-
)}
|
|
117
|
-
</Box>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
124
|
+
function MessagesPanelInner({ session, maxHeight, isActive }: MessagesPanelProps) {
|
|
125
|
+
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
|
126
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
127
|
+
const [cursor, setCursor] = useState(0);
|
|
120
128
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
129
|
+
// Reset all state when session changes
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
setExpandedIds(new Set());
|
|
132
|
+
setScrollOffset(0);
|
|
133
|
+
setCursor(0);
|
|
134
|
+
}, [session?.id]);
|
|
135
|
+
|
|
136
|
+
const contentWidth = 80;
|
|
137
|
+
const viewHeight = maxHeight - 1; // minus the counter row
|
|
138
|
+
|
|
139
|
+
const allLines = useMemo(
|
|
140
|
+
() => (session ? buildLines(session, contentWidth, expandedIds) : []),
|
|
141
|
+
[session, expandedIds]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const maxOffset = Math.max(0, allLines.length - viewHeight);
|
|
145
|
+
|
|
146
|
+
// Keep cursor in view: scroll to follow cursor
|
|
147
|
+
const clampedCursor = Math.min(cursor, Math.max(0, allLines.length - 1));
|
|
148
|
+
const clampedOffset = Math.max(
|
|
149
|
+
0,
|
|
150
|
+
Math.min(
|
|
151
|
+
scrollOffset,
|
|
152
|
+
Math.min(maxOffset, Math.max(scrollOffset, clampedCursor - viewHeight + 1))
|
|
153
|
+
)
|
|
154
|
+
);
|
|
139
155
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
useInput((input, key) => {
|
|
157
|
+
if (key.downArrow || input === "j") {
|
|
158
|
+
setCursor((c) => Math.min(allLines.length - 1, c + 1));
|
|
159
|
+
setScrollOffset((o) => {
|
|
160
|
+
const newCursor = Math.min(allLines.length - 1, clampedCursor + 1);
|
|
161
|
+
// Scroll down if cursor goes below view
|
|
162
|
+
if (newCursor >= o + viewHeight) return Math.min(maxOffset, o + 1);
|
|
163
|
+
return o;
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (key.upArrow || input === "k") {
|
|
168
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
169
|
+
setScrollOffset((o) => {
|
|
170
|
+
const newCursor = Math.max(0, clampedCursor - 1);
|
|
171
|
+
// Scroll up if cursor goes above view
|
|
172
|
+
if (newCursor < o) return Math.max(0, o - 1);
|
|
173
|
+
return o;
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (key.pageDown || input === "d") {
|
|
178
|
+
const half = Math.floor(viewHeight / 2);
|
|
179
|
+
setScrollOffset((o) => Math.min(maxOffset, o + half));
|
|
180
|
+
setCursor((c) => Math.min(allLines.length - 1, c + half));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (key.pageUp || input === "u") {
|
|
184
|
+
const half = Math.floor(viewHeight / 2);
|
|
185
|
+
setScrollOffset((o) => Math.max(0, o - half));
|
|
186
|
+
setCursor((c) => Math.max(0, c - half));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (input === "g") {
|
|
190
|
+
setScrollOffset(0);
|
|
191
|
+
setCursor(0);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (input === "G") {
|
|
195
|
+
setScrollOffset(maxOffset);
|
|
196
|
+
setCursor(allLines.length - 1);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (key.return) {
|
|
200
|
+
const line = allLines[clampedCursor];
|
|
201
|
+
if (line?.kind === "tool") {
|
|
202
|
+
const id = line.callId;
|
|
203
|
+
setExpandedIds((prev) => {
|
|
204
|
+
const next = new Set(prev);
|
|
205
|
+
if (next.has(id)) next.delete(id);
|
|
206
|
+
else next.add(id);
|
|
207
|
+
return next;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}, { isActive });
|
|
149
213
|
|
|
150
214
|
if (!session) {
|
|
151
215
|
return (
|
|
152
|
-
<Box paddingX={1}>
|
|
216
|
+
<Box height={maxHeight} paddingX={1}>
|
|
153
217
|
<Text color={colors.textDim}>Select a session</Text>
|
|
154
218
|
</Box>
|
|
155
219
|
);
|
|
@@ -157,32 +221,98 @@ function MessagesPanelInner({
|
|
|
157
221
|
|
|
158
222
|
if (allLines.length === 0) {
|
|
159
223
|
return (
|
|
160
|
-
<Box paddingX={1}>
|
|
224
|
+
<Box height={maxHeight} paddingX={1}>
|
|
161
225
|
<Text color={colors.textDim}>No messages</Text>
|
|
162
226
|
</Box>
|
|
163
227
|
);
|
|
164
228
|
}
|
|
165
229
|
|
|
166
|
-
const
|
|
167
|
-
const visibleLines = allLines.slice(clampedOffset, clampedOffset + maxHeight);
|
|
230
|
+
const visibleLines = allLines.slice(clampedOffset, clampedOffset + viewHeight);
|
|
168
231
|
|
|
169
232
|
return (
|
|
170
|
-
<Box flexDirection="column" paddingX={1}
|
|
171
|
-
|
|
233
|
+
<Box flexDirection="column" height={maxHeight} paddingX={1}>
|
|
234
|
+
{/* Counter row */}
|
|
235
|
+
<Box flexDirection="row" height={1}>
|
|
172
236
|
<Text color={colors.textDim}>
|
|
173
|
-
{clampedOffset + 1}–{Math.min(clampedOffset +
|
|
237
|
+
{clampedOffset + 1}–{Math.min(clampedOffset + viewHeight, allLines.length)}/{allLines.length}
|
|
174
238
|
</Text>
|
|
175
239
|
<Box flexGrow={1} />
|
|
176
|
-
<Text color={colors.textDim}>
|
|
240
|
+
<Text color={colors.textDim}>
|
|
241
|
+
{session.interactions.filter((i) => i.role === "assistant").length} turns
|
|
242
|
+
</Text>
|
|
243
|
+
{clampedOffset > 0 && <Text color={colors.textDim}> ↑</Text>}
|
|
244
|
+
{clampedOffset < maxOffset && <Text color={colors.textDim}> ↓</Text>}
|
|
177
245
|
</Box>
|
|
178
|
-
|
|
246
|
+
|
|
247
|
+
{visibleLines.map((line, i) => {
|
|
248
|
+
const absIdx = clampedOffset + i;
|
|
249
|
+
const isCursor = absIdx === clampedCursor && isActive;
|
|
250
|
+
|
|
251
|
+
switch (line.kind) {
|
|
252
|
+
case "header":
|
|
253
|
+
return (
|
|
254
|
+
<Box key={absIdx} flexDirection="row" height={1}>
|
|
255
|
+
<Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
|
|
256
|
+
<Text color={isCursor ? colors.accent : colors.purple} bold>◆ </Text>
|
|
257
|
+
<Text color={isCursor ? colors.accent : colors.info}>{truncate(line.modelId, 26)}</Text>
|
|
258
|
+
{line.agent && <Text color={colors.cyan}> [{line.agent}]</Text>}
|
|
259
|
+
<Box flexGrow={1} />
|
|
260
|
+
{line.duration && <Text color={colors.textDim}>{line.duration} </Text>}
|
|
261
|
+
<Text color={colors.textDim}>{line.time}</Text>
|
|
262
|
+
</Box>
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
case "tool":
|
|
266
|
+
return (
|
|
267
|
+
<Box key={absIdx} flexDirection="row" height={1} paddingLeft={1}>
|
|
268
|
+
<Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
|
|
269
|
+
<Text color={line.iconColor}>{line.icon} </Text>
|
|
270
|
+
<Text color={isCursor ? colors.accent : colors.text} bold={isCursor}>{line.name}</Text>
|
|
271
|
+
{line.title && <Text color={colors.textDim}> {line.title}</Text>}
|
|
272
|
+
<Box flexGrow={1} />
|
|
273
|
+
<Text color={colors.textDim}>{line.expanded ? "▼ " : "▶ "}</Text>
|
|
274
|
+
{line.right && <Text color={colors.textDim}>{line.right}</Text>}
|
|
275
|
+
</Box>
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
case "tool-detail":
|
|
279
|
+
if (line.isSection) {
|
|
280
|
+
return (
|
|
281
|
+
<Box key={absIdx} height={1} paddingLeft={3}>
|
|
282
|
+
<Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
|
|
283
|
+
<Text color={colors.purple}>── {line.label} </Text>
|
|
284
|
+
</Box>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return (
|
|
288
|
+
<Box key={absIdx} height={1} paddingLeft={4}>
|
|
289
|
+
<Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
|
|
290
|
+
{line.label
|
|
291
|
+
? <><Text color={colors.cyan}>{line.label}</Text><Text color={colors.textDim}>: </Text><Text color={isCursor ? colors.accent : colors.text}>{line.value}</Text></>
|
|
292
|
+
: <Text color={isCursor ? colors.accent : colors.textDim}>{line.value}</Text>
|
|
293
|
+
}
|
|
294
|
+
</Box>
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
case "text":
|
|
298
|
+
return (
|
|
299
|
+
<Box key={absIdx} flexDirection="row" height={1} paddingLeft={1}>
|
|
300
|
+
<Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
|
|
301
|
+
<Text color={isCursor ? colors.accent : colors.text}> {line.text}</Text>
|
|
302
|
+
</Box>
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
case "reasoning":
|
|
306
|
+
return (
|
|
307
|
+
<Box key={absIdx} flexDirection="row" height={1} paddingLeft={1}>
|
|
308
|
+
<Text color={isCursor ? colors.accent : colors.textDim}>{isCursor ? "›" : " "}</Text>
|
|
309
|
+
<Text color={isCursor ? colors.accent : colors.accentDim}> ⚡ {line.text}</Text>
|
|
310
|
+
</Box>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
})}
|
|
179
314
|
</Box>
|
|
180
315
|
);
|
|
181
316
|
}
|
|
182
317
|
|
|
183
318
|
export const MessagesPanel = memo(MessagesPanelInner);
|
|
184
|
-
|
|
185
|
-
function truncate(s: string, max: number): string {
|
|
186
|
-
if (s.length <= max) return s;
|
|
187
|
-
return s.slice(0, max - 1) + "…";
|
|
188
|
-
}
|
|
@@ -5,7 +5,7 @@ import { StatusBar } from "../components/StatusBar";
|
|
|
5
5
|
import { AgentTree } from "../components/AgentTree";
|
|
6
6
|
import { DetailsPanel } from "../components/DetailsPanel";
|
|
7
7
|
import { MessagesPanel } from "../components/MessagesPanel";
|
|
8
|
-
import type { Workflow,
|
|
8
|
+
import type { Workflow, AgentNode, FlatNode } from "../../core/types";
|
|
9
9
|
|
|
10
10
|
interface SessionsScreenProps {
|
|
11
11
|
workflows: Workflow[];
|
|
@@ -18,7 +18,6 @@ type RightMode = "stats" | "messages";
|
|
|
18
18
|
|
|
19
19
|
function flattenWorkflow(workflow: Workflow, workflowIndex: number): FlatNode[] {
|
|
20
20
|
const nodes: FlatNode[] = [];
|
|
21
|
-
|
|
22
21
|
function walk(node: AgentNode) {
|
|
23
22
|
nodes.push({
|
|
24
23
|
id: node.session.id,
|
|
@@ -28,11 +27,8 @@ function flattenWorkflow(workflow: Workflow, workflowIndex: number): FlatNode[]
|
|
|
28
27
|
hasChildren: node.children.length > 0,
|
|
29
28
|
agentNode: node,
|
|
30
29
|
});
|
|
31
|
-
for (const child of node.children)
|
|
32
|
-
walk(child);
|
|
33
|
-
}
|
|
30
|
+
for (const child of node.children) walk(child);
|
|
34
31
|
}
|
|
35
|
-
|
|
36
32
|
walk(workflow.agentTree);
|
|
37
33
|
return nodes;
|
|
38
34
|
}
|
|
@@ -45,9 +41,7 @@ function SessionsScreenInner({
|
|
|
45
41
|
}: SessionsScreenProps) {
|
|
46
42
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
47
43
|
const [rightMode, setRightMode] = useState<RightMode>("stats");
|
|
48
|
-
const [msgScrollOffset, setMsgScrollOffset] = useState(0);
|
|
49
44
|
|
|
50
|
-
// Flat list of all nodes (root workflows + sub-agents in tree order)
|
|
51
45
|
const flatNodes = useMemo(() => {
|
|
52
46
|
return workflows.flatMap((w, i) => flattenWorkflow(w, i));
|
|
53
47
|
}, [workflows]);
|
|
@@ -55,12 +49,10 @@ function SessionsScreenInner({
|
|
|
55
49
|
const clampedIndex = Math.min(selectedIndex, Math.max(0, flatNodes.length - 1));
|
|
56
50
|
const selectedNode = flatNodes[clampedIndex] ?? null;
|
|
57
51
|
|
|
58
|
-
// For DetailsPanel we need the full Workflow — use the root workflow of the selected node
|
|
59
52
|
const selectedWorkflow = useMemo(() => {
|
|
60
53
|
if (!selectedNode) return null;
|
|
61
54
|
const w = workflows[selectedNode.workflowIndex];
|
|
62
55
|
if (!w) return null;
|
|
63
|
-
// If we selected a sub-agent, wrap it as a single-session workflow for DetailsPanel
|
|
64
56
|
if (selectedNode.session.id !== w.mainSession.id) {
|
|
65
57
|
return {
|
|
66
58
|
id: selectedNode.session.id,
|
|
@@ -73,56 +65,49 @@ function SessionsScreenInner({
|
|
|
73
65
|
}, [selectedNode, workflows]);
|
|
74
66
|
|
|
75
67
|
const leftWidth = Math.floor(terminalWidth * 0.35);
|
|
76
|
-
const rightWidth = terminalWidth - leftWidth - 2;
|
|
68
|
+
const rightWidth = terminalWidth - leftWidth - 2;
|
|
69
|
+
|
|
70
|
+
const statusBarHeight = 1;
|
|
71
|
+
const borderRows = 2;
|
|
72
|
+
const innerHeight = contentHeight - statusBarHeight - borderRows;
|
|
73
|
+
const panelHeight = contentHeight - statusBarHeight;
|
|
74
|
+
const msgHeight = innerHeight - 1; // minus tab header row
|
|
77
75
|
|
|
78
|
-
// Message scroll: reset when selection changes
|
|
79
76
|
const handleSelect = useCallback((index: number) => {
|
|
80
77
|
setSelectedIndex(index);
|
|
81
|
-
setMsgScrollOffset(0);
|
|
82
78
|
}, []);
|
|
83
79
|
|
|
80
|
+
// SessionsScreen only handles: tab switch, tree nav (stats mode), session switch (messages mode)
|
|
84
81
|
useInput(
|
|
85
82
|
(input, key) => {
|
|
86
|
-
if (key.upArrow || input === "k") {
|
|
87
|
-
handleSelect(Math.max(0, clampedIndex - 1));
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
if (key.downArrow || input === "j") {
|
|
91
|
-
handleSelect(Math.min(flatNodes.length - 1, clampedIndex + 1));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
83
|
if (key.tab) {
|
|
95
84
|
setRightMode((m) => (m === "stats" ? "messages" : "stats"));
|
|
96
|
-
setMsgScrollOffset(0);
|
|
97
85
|
return;
|
|
98
86
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (input === "
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
87
|
+
if (rightMode === "stats") {
|
|
88
|
+
if (key.upArrow || input === "k") { handleSelect(Math.max(0, clampedIndex - 1)); return; }
|
|
89
|
+
if (key.downArrow || input === "j") { handleSelect(Math.min(flatNodes.length - 1, clampedIndex + 1)); return; }
|
|
90
|
+
if (input === "g") { handleSelect(0); return; }
|
|
91
|
+
if (input === "G") { handleSelect(flatNodes.length - 1); return; }
|
|
92
|
+
} else {
|
|
93
|
+
// In messages mode, [ and ] switch session
|
|
94
|
+
if (input === "[") { handleSelect(Math.max(0, clampedIndex - 1)); return; }
|
|
95
|
+
if (input === "]") { handleSelect(Math.min(flatNodes.length - 1, clampedIndex + 1)); return; }
|
|
109
96
|
}
|
|
110
97
|
},
|
|
111
98
|
{ isActive }
|
|
112
99
|
);
|
|
113
100
|
|
|
114
|
-
const panelHeight = contentHeight - 2; // leave room for status bar
|
|
115
|
-
|
|
116
101
|
return (
|
|
117
102
|
<Box flexDirection="column" width={terminalWidth} height={contentHeight}>
|
|
118
|
-
<Box flexDirection="row"
|
|
103
|
+
<Box flexDirection="row" height={panelHeight}>
|
|
119
104
|
{/* Left: agent/session tree */}
|
|
120
105
|
<Box
|
|
121
106
|
width={leftWidth}
|
|
107
|
+
height={panelHeight}
|
|
122
108
|
borderStyle="single"
|
|
123
109
|
borderColor={colors.border}
|
|
124
110
|
flexDirection="column"
|
|
125
|
-
overflow="hidden"
|
|
126
111
|
>
|
|
127
112
|
<AgentTree
|
|
128
113
|
workflows={workflows}
|
|
@@ -132,32 +117,25 @@ function SessionsScreenInner({
|
|
|
132
117
|
const idx = flatNodes.findIndex((n) => n.id === id);
|
|
133
118
|
if (idx >= 0) handleSelect(idx);
|
|
134
119
|
}}
|
|
135
|
-
maxHeight={
|
|
120
|
+
maxHeight={innerHeight}
|
|
136
121
|
/>
|
|
137
122
|
</Box>
|
|
138
123
|
|
|
139
124
|
{/* Right: details or messages */}
|
|
140
125
|
<Box
|
|
141
|
-
flexGrow={1}
|
|
142
126
|
width={rightWidth}
|
|
127
|
+
height={panelHeight}
|
|
143
128
|
borderStyle="single"
|
|
144
129
|
borderColor={colors.border}
|
|
145
130
|
flexDirection="column"
|
|
146
|
-
overflow="hidden"
|
|
147
131
|
>
|
|
148
|
-
{/*
|
|
149
|
-
<Box paddingX={1} flexDirection="row">
|
|
150
|
-
<Text
|
|
151
|
-
color={rightMode === "stats" ? colors.accent : colors.textDim}
|
|
152
|
-
bold={rightMode === "stats"}
|
|
153
|
-
>
|
|
132
|
+
{/* Tab header: 1 row */}
|
|
133
|
+
<Box paddingX={1} height={1} flexDirection="row">
|
|
134
|
+
<Text color={rightMode === "stats" ? colors.accent : colors.textDim} bold={rightMode === "stats"}>
|
|
154
135
|
[Stats]
|
|
155
136
|
</Text>
|
|
156
137
|
<Text color={colors.textDim}> </Text>
|
|
157
|
-
<Text
|
|
158
|
-
color={rightMode === "messages" ? colors.accent : colors.textDim}
|
|
159
|
-
bold={rightMode === "messages"}
|
|
160
|
-
>
|
|
138
|
+
<Text color={rightMode === "messages" ? colors.accent : colors.textDim} bold={rightMode === "messages"}>
|
|
161
139
|
[Messages]
|
|
162
140
|
</Text>
|
|
163
141
|
<Box flexGrow={1} />
|
|
@@ -165,13 +143,12 @@ function SessionsScreenInner({
|
|
|
165
143
|
</Box>
|
|
166
144
|
|
|
167
145
|
{rightMode === "stats" ? (
|
|
168
|
-
<DetailsPanel workflow={selectedWorkflow} />
|
|
146
|
+
<DetailsPanel workflow={selectedWorkflow} height={innerHeight - 1} />
|
|
169
147
|
) : (
|
|
170
148
|
<MessagesPanel
|
|
171
149
|
session={selectedNode?.session ?? null}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
maxHeight={panelHeight - 2}
|
|
150
|
+
maxHeight={msgHeight}
|
|
151
|
+
isActive={isActive && rightMode === "messages"}
|
|
175
152
|
/>
|
|
176
153
|
)}
|
|
177
154
|
</Box>
|
|
@@ -180,8 +157,8 @@ function SessionsScreenInner({
|
|
|
180
157
|
<StatusBar
|
|
181
158
|
hints={
|
|
182
159
|
rightMode === "messages"
|
|
183
|
-
? "j/k:
|
|
184
|
-
: "j/k:nav Tab:messages 2:tools 3:overview r:refresh q:quit"
|
|
160
|
+
? "j/k:scroll d/u:½page g/G:top/bot Enter:expand [:prev ]:next Tab:stats q:quit"
|
|
161
|
+
: "j/k:nav g/G:top/bot Tab:messages 2:tools 3:overview r:refresh q:quit"
|
|
185
162
|
}
|
|
186
163
|
/>
|
|
187
164
|
</Box>
|