opencode-top 3.1.1 → 3.2.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.
Files changed (103) hide show
  1. package/bin/octop.js +2 -9
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +22432 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/cli.mjs +22570 -0
  7. package/dist/core/agents.d.ts +11 -0
  8. package/dist/core/agents.d.ts.map +1 -0
  9. package/dist/core/agents.js +58 -0
  10. package/dist/core/agents.js.map +1 -0
  11. package/dist/core/session.d.ts +19 -0
  12. package/dist/core/session.d.ts.map +1 -0
  13. package/dist/core/session.js +261 -0
  14. package/dist/core/session.js.map +1 -0
  15. package/dist/core/types.d.ts +140 -0
  16. package/dist/core/types.d.ts.map +1 -0
  17. package/dist/core/types.js +29 -0
  18. package/dist/core/types.js.map +1 -0
  19. package/dist/data/pricing.d.ts +4 -0
  20. package/dist/data/pricing.d.ts.map +1 -0
  21. package/dist/data/pricing.js +76 -0
  22. package/dist/data/pricing.js.map +1 -0
  23. package/dist/data/sqlite.d.ts +5 -0
  24. package/dist/data/sqlite.d.ts.map +1 -0
  25. package/dist/data/sqlite.js +222 -0
  26. package/dist/data/sqlite.js.map +1 -0
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/{src/index.ts → dist/index.js} +1 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/ui/App.d.ts +6 -0
  32. package/dist/ui/App.d.ts.map +1 -0
  33. package/dist/ui/App.js +101 -0
  34. package/dist/ui/App.js.map +1 -0
  35. package/dist/ui/components/AgentChainGraph.d.ts +9 -0
  36. package/dist/ui/components/AgentChainGraph.d.ts.map +1 -0
  37. package/dist/ui/components/AgentChainGraph.js +41 -0
  38. package/dist/ui/components/AgentChainGraph.js.map +1 -0
  39. package/dist/ui/components/AgentTree.d.ts +13 -0
  40. package/dist/ui/components/AgentTree.d.ts.map +1 -0
  41. package/dist/ui/components/AgentTree.js +50 -0
  42. package/dist/ui/components/AgentTree.js.map +1 -0
  43. package/dist/ui/components/DetailsPanel.d.ts +9 -0
  44. package/dist/ui/components/DetailsPanel.d.ts.map +1 -0
  45. package/dist/ui/components/DetailsPanel.js +82 -0
  46. package/dist/ui/components/DetailsPanel.js.map +1 -0
  47. package/dist/ui/components/MessagesPanel.d.ts +12 -0
  48. package/dist/ui/components/MessagesPanel.d.ts.map +1 -0
  49. package/dist/ui/components/MessagesPanel.js +107 -0
  50. package/dist/ui/components/MessagesPanel.js.map +1 -0
  51. package/dist/ui/components/SparkLine.d.ts +10 -0
  52. package/dist/ui/components/SparkLine.d.ts.map +1 -0
  53. package/dist/ui/components/SparkLine.js +12 -0
  54. package/dist/ui/components/SparkLine.js.map +1 -0
  55. package/dist/ui/components/StatusBar.d.ts +9 -0
  56. package/dist/ui/components/StatusBar.d.ts.map +1 -0
  57. package/dist/ui/components/StatusBar.js +9 -0
  58. package/dist/ui/components/StatusBar.js.map +1 -0
  59. package/dist/ui/components/TabBar.d.ts +10 -0
  60. package/dist/ui/components/TabBar.d.ts.map +1 -0
  61. package/dist/ui/components/TabBar.js +17 -0
  62. package/dist/ui/components/TabBar.js.map +1 -0
  63. package/dist/ui/screens/OverviewScreen.d.ts +12 -0
  64. package/dist/ui/screens/OverviewScreen.d.ts.map +1 -0
  65. package/dist/ui/screens/OverviewScreen.js +94 -0
  66. package/dist/ui/screens/OverviewScreen.js.map +1 -0
  67. package/dist/ui/screens/SessionsScreen.d.ts +12 -0
  68. package/dist/ui/screens/SessionsScreen.d.ts.map +1 -0
  69. package/dist/ui/screens/SessionsScreen.js +98 -0
  70. package/dist/ui/screens/SessionsScreen.js.map +1 -0
  71. package/dist/ui/screens/TimelineScreen.d.ts +11 -0
  72. package/dist/ui/screens/TimelineScreen.d.ts.map +1 -0
  73. package/dist/ui/screens/TimelineScreen.js +128 -0
  74. package/dist/ui/screens/TimelineScreen.js.map +1 -0
  75. package/dist/ui/screens/ToolsScreen.d.ts +12 -0
  76. package/dist/ui/screens/ToolsScreen.d.ts.map +1 -0
  77. package/dist/ui/screens/ToolsScreen.js +113 -0
  78. package/dist/ui/screens/ToolsScreen.js.map +1 -0
  79. package/dist/ui/theme.d.ts +21 -0
  80. package/dist/ui/theme.d.ts.map +1 -0
  81. package/dist/ui/theme.js +21 -0
  82. package/dist/ui/theme.js.map +1 -0
  83. package/package.json +2 -1
  84. package/bin/octop.mjs +0 -13
  85. package/src/cli.ts +0 -60
  86. package/src/core/agents.ts +0 -78
  87. package/src/core/session.ts +0 -315
  88. package/src/core/types.ts +0 -156
  89. package/src/data/pricing.ts +0 -82
  90. package/src/data/sqlite.ts +0 -347
  91. package/src/ui/App.tsx +0 -141
  92. package/src/ui/components/AgentChainGraph.tsx +0 -95
  93. package/src/ui/components/AgentTree.tsx +0 -101
  94. package/src/ui/components/DetailsPanel.tsx +0 -211
  95. package/src/ui/components/MessagesPanel.tsx +0 -323
  96. package/src/ui/components/SparkLine.tsx +0 -18
  97. package/src/ui/components/StatusBar.tsx +0 -24
  98. package/src/ui/components/TabBar.tsx +0 -42
  99. package/src/ui/screens/OverviewScreen.tsx +0 -327
  100. package/src/ui/screens/SessionsScreen.tsx +0 -168
  101. package/src/ui/screens/TimelineScreen.tsx +0 -222
  102. package/src/ui/screens/ToolsScreen.tsx +0 -260
  103. package/src/ui/theme.ts +0 -21
@@ -1,327 +0,0 @@
1
- import React, { useMemo, memo } from "react";
2
- import { Box, Text } from "ink";
3
- import { colors } from "../theme";
4
- import { StatusBar } from "../components/StatusBar";
5
- import { SparkLine } from "../components/SparkLine";
6
- import type { Workflow } from "../../core/types";
7
- import { computeOverviewStats, buildSparkSeries } from "../../core/session";
8
- import { getAllPricing } from "../../data/pricing";
9
-
10
- interface OverviewScreenProps {
11
- workflows: Workflow[];
12
- isActive: boolean;
13
- contentHeight: number;
14
- terminalWidth: number;
15
- }
16
-
17
- function formatTokens(n: number): string {
18
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
19
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
20
- return n.toString();
21
- }
22
-
23
- function StatRow({ label, value, color = colors.text }: { label: string; value: string; color?: string }) {
24
- return (
25
- <Box flexDirection="row">
26
- <Box width={18}>
27
- <Text color={colors.textDim}>{label}</Text>
28
- </Box>
29
- <Text color={color}>{value}</Text>
30
- </Box>
31
- );
32
- }
33
-
34
- function SectionHeader({ title }: { title: string }) {
35
- return (
36
- <Box marginTop={1}>
37
- <Text color={colors.purple} bold>{title}</Text>
38
- </Box>
39
- );
40
- }
41
-
42
- /** Horizontal bar: filled █ proportional to value/max, with label and count */
43
- function HBar({
44
- label,
45
- value,
46
- max,
47
- total,
48
- width = 16,
49
- barColor = colors.info,
50
- labelWidth = 10,
51
- showPct = false,
52
- }: {
53
- label: string;
54
- value: number;
55
- max: number;
56
- total?: number;
57
- width?: number;
58
- barColor?: string;
59
- labelWidth?: number;
60
- showPct?: boolean;
61
- }) {
62
- const pct = max > 0 ? value / max : 0;
63
- const filled = Math.max(0, Math.round(pct * width));
64
- const empty = width - filled;
65
- const pctStr = showPct && total && total > 0 ? ` ${Math.round((value / total) * 100)}%` : "";
66
- return (
67
- <Box flexDirection="row">
68
- <Box width={labelWidth}>
69
- <Text color={colors.text}>{truncate(label, labelWidth - 1)}</Text>
70
- </Box>
71
- <Text color={barColor}>{"█".repeat(filled)}</Text>
72
- <Text color={colors.border}>{"░".repeat(empty)}</Text>
73
- <Text color={colors.textDim}> {value}{pctStr}</Text>
74
- </Box>
75
- );
76
- }
77
-
78
- /** Error rate bar: shows ok + error segments */
79
- function ErrorBar({
80
- label,
81
- calls,
82
- errors,
83
- width = 16,
84
- labelWidth = 8,
85
- }: {
86
- label: string;
87
- calls: number;
88
- errors: number;
89
- width: number;
90
- labelWidth: number;
91
- }) {
92
- const okCount = calls - errors;
93
- const okFilled = calls > 0 ? Math.round((okCount / calls) * width) : width;
94
- const errFilled = width - okFilled;
95
- const errPct = calls > 0 ? Math.round((errors / calls) * 100) : 0;
96
- return (
97
- <Box flexDirection="row">
98
- <Box width={labelWidth}>
99
- <Text color={colors.text}>{truncate(label, labelWidth - 1)}</Text>
100
- </Box>
101
- <Text color={colors.success}>{"█".repeat(okFilled)}</Text>
102
- <Text color={errors > 0 ? colors.error : colors.border}>{"█".repeat(errFilled)}</Text>
103
- <Text color={colors.textDim}> {calls} calls</Text>
104
- {errors > 0 && <Text color={colors.error}> {errors} err ({errPct}%)</Text>}
105
- </Box>
106
- );
107
- }
108
-
109
- function OverviewScreenInner({ workflows, isActive, contentHeight, terminalWidth }: OverviewScreenProps) {
110
- const pricing = useMemo(() => getAllPricing(), []);
111
- const stats = useMemo(() => computeOverviewStats(workflows, pricing), [workflows, pricing]);
112
-
113
- const topModels = useMemo(() =>
114
- Array.from(stats.modelBreakdown.entries())
115
- .sort((a, b) => b[1].calls - a[1].calls)
116
- .slice(0, 4),
117
- [stats]
118
- );
119
-
120
- const topProjects = useMemo(() =>
121
- Array.from(stats.projectBreakdown.entries())
122
- .sort((a, b) => b[1].sessions - a[1].sessions)
123
- .slice(0, 4),
124
- [stats]
125
- );
126
-
127
- const agentToolErrorList = useMemo(() =>
128
- Array.from(stats.agentToolErrors.entries())
129
- .sort((a, b) => b[1].calls - a[1].calls),
130
- [stats]
131
- );
132
-
133
- const topTools = useMemo(() =>
134
- Array.from(stats.toolCallCounts.entries())
135
- .sort((a, b) => b[1].calls - a[1].calls)
136
- .slice(0, 8),
137
- [stats]
138
- );
139
- const maxToolCalls = topTools[0]?.[1].calls ?? 1;
140
-
141
- // Weekly sparklines
142
- const weeklyTokenValues = stats.weeklyTokens.map((d) => d.tokens);
143
- const weeklySessionValues = stats.weeklySessions.map((d) => d.sessions);
144
- const maxWeeklyTokens = Math.max(...weeklyTokenValues, 1);
145
- const maxWeeklySessions = Math.max(...weeklySessionValues, 1);
146
-
147
- // Hourly heatmap — group into 4-hour buckets for compactness: 0-3,4-7,8-11,12-15,16-19,20-23
148
- const hourlyBuckets = Array.from({ length: 6 }, (_, i) =>
149
- stats.hourlyActivity.slice(i * 4, i * 4 + 4).reduce((a, b) => a + b, 0)
150
- );
151
- const hourSpark = buildSparkSeries(stats.hourlyActivity);
152
-
153
- // Column widths based on terminal
154
- const leftW = Math.max(32, Math.floor(terminalWidth * 0.38));
155
- const midW = Math.max(26, Math.floor(terminalWidth * 0.30));
156
- // right gets remaining
157
-
158
- return (
159
- <Box flexDirection="column" width={terminalWidth} height={contentHeight}>
160
- <Box paddingX={1} flexDirection="row">
161
- <Text color={colors.accent} bold>Overview</Text>
162
- <Box flexGrow={1} />
163
- <Text color={colors.textDim}>{workflows.length} workflows {stats.totalTokens.total > 0 ? formatTokens(stats.totalTokens.total) + " tokens" : ""}</Text>
164
- </Box>
165
-
166
- <Box flexDirection="row" flexGrow={1} paddingX={1}>
167
-
168
- {/* ── LEFT COLUMN ─────────────────────────────────────── */}
169
- <Box flexDirection="column" width={leftW}>
170
-
171
- <SectionHeader title="Totals" />
172
- <StatRow label="Total tokens" value={formatTokens(stats.totalTokens.total)} />
173
- <StatRow label=" input" value={formatTokens(stats.totalTokens.input)} color={colors.info} />
174
- <StatRow label=" output" value={formatTokens(stats.totalTokens.output)} color={colors.cyan} />
175
- <StatRow label=" cache r/w" value={`${formatTokens(stats.totalTokens.cacheRead)} / ${formatTokens(stats.totalTokens.cacheWrite)}`} color={colors.textDim} />
176
- <StatRow label="Total cost" value={`$${stats.totalCost.toFixed(4)}`} color={colors.success} />
177
-
178
- <SectionHeader title="Token trend (7d)" />
179
- <Box flexDirection="row">
180
- <SparkLine values={weeklyTokenValues} color={colors.cyan} />
181
- </Box>
182
- <Box flexDirection="row">
183
- <Text color={colors.textDim}>{stats.weeklyTokens[0]?.date.slice(3) ?? ""}</Text>
184
- <Box flexGrow={1} />
185
- <Text color={colors.textDim}>peak {formatTokens(maxWeeklyTokens)}</Text>
186
- <Box flexGrow={1} />
187
- <Text color={colors.textDim}>{stats.weeklyTokens[6]?.date.slice(3) ?? ""}</Text>
188
- </Box>
189
-
190
- <SectionHeader title="Sessions (7d)" />
191
- <Box flexDirection="row">
192
- <SparkLine values={weeklySessionValues} color={colors.accent} />
193
- </Box>
194
- <Box flexDirection="row">
195
- <Text color={colors.textDim}>{stats.weeklySessions[0]?.date.slice(3) ?? ""}</Text>
196
- <Box flexGrow={1} />
197
- <Text color={colors.textDim}>peak {maxWeeklySessions}/day</Text>
198
- <Box flexGrow={1} />
199
- <Text color={colors.textDim}>{stats.weeklySessions[6]?.date.slice(3) ?? ""}</Text>
200
- </Box>
201
-
202
- <SectionHeader title="Hourly activity" />
203
- <Text color={colors.warning}>{hourSpark}</Text>
204
- <Box flexDirection="row">
205
- <Text color={colors.textDim}>00</Text>
206
- <Box flexGrow={1} />
207
- <Text color={colors.textDim}>06</Text>
208
- <Box flexGrow={1} />
209
- <Text color={colors.textDim}>12</Text>
210
- <Box flexGrow={1} />
211
- <Text color={colors.textDim}>18</Text>
212
- <Box flexGrow={1} />
213
- <Text color={colors.textDim}>23</Text>
214
- </Box>
215
-
216
- </Box>
217
-
218
- {/* ── MIDDLE COLUMN ───────────────────────────────────── */}
219
- <Box flexDirection="column" width={midW} paddingLeft={2}>
220
-
221
- <SectionHeader title="Tool errors by agent" />
222
- {agentToolErrorList.length === 0
223
- ? <Text color={colors.textDim}>No tool data</Text>
224
- : agentToolErrorList.map(([agent, data]) => (
225
- <ErrorBar
226
- key={agent}
227
- label={agent}
228
- calls={data.calls}
229
- errors={data.errors}
230
- width={14}
231
- labelWidth={9}
232
- />
233
- ))
234
- }
235
-
236
- <SectionHeader title="Top tools (calls / errors)" />
237
- {topTools.length === 0
238
- ? <Text color={colors.textDim}>No tool data</Text>
239
- : topTools.map(([name, data]) => (
240
- <Box key={name} flexDirection="row">
241
- <Box width={10}>
242
- <Text color={colors.text}>{truncate(name, 9)}</Text>
243
- </Box>
244
- <Text color={data.errors > 0 ? colors.warning : colors.info}>
245
- {"█".repeat(Math.max(1, Math.round((data.calls / maxToolCalls) * 12)))}
246
- </Text>
247
- <Text color={colors.border}>
248
- {"░".repeat(Math.max(0, 12 - Math.max(1, Math.round((data.calls / maxToolCalls) * 12))))}
249
- </Text>
250
- <Text color={colors.textDim}> {data.calls}</Text>
251
- {data.errors > 0 && <Text color={colors.error}> ✗{data.errors}</Text>}
252
- </Box>
253
- ))
254
- }
255
-
256
- </Box>
257
-
258
- {/* ── RIGHT COLUMN ────────────────────────────────────── */}
259
- <Box flexDirection="column" flexGrow={1} paddingLeft={2}>
260
-
261
- <SectionHeader title="Projects" />
262
- {topProjects.length === 0
263
- ? <Text color={colors.textDim}>No project data</Text>
264
- : topProjects.map(([project, data]) => (
265
- <Box key={project} flexDirection="row">
266
- <Box width={22}>
267
- <Text color={colors.text}>{truncate(project, 20)}</Text>
268
- </Box>
269
- <Text color={colors.info}>{data.sessions} sess</Text>
270
- <Text color={colors.success}> ${data.cost.toFixed(3)}</Text>
271
- </Box>
272
- ))
273
- }
274
-
275
- <SectionHeader title="Tool avg duration (top 5)" />
276
- {(() => {
277
- const durList = Array.from(stats.toolCallCounts.entries())
278
- .filter(([, d]) => d.calls > 0 && d.totalDurationMs > 0)
279
- .map(([name, d]) => ({ name, avg: d.totalDurationMs / d.calls }))
280
- .sort((a, b) => b.avg - a.avg)
281
- .slice(0, 5);
282
- const maxAvg = durList[0]?.avg ?? 1;
283
- const barW = 10;
284
- return durList.map(({ name, avg }) => {
285
- const filled = Math.max(1, Math.round((avg / maxAvg) * barW));
286
- const durStr = avg < 1000 ? `${avg.toFixed(0)}ms` : `${(avg / 1000).toFixed(1)}s`;
287
- return (
288
- <Box key={name} flexDirection="row">
289
- <Box width={12}>
290
- <Text color={colors.text}>{truncate(name, 10)}</Text>
291
- </Box>
292
- <Text color={colors.warning}>{"█".repeat(filled)}</Text>
293
- <Text color={colors.border}>{"░".repeat(barW - filled)}</Text>
294
- <Text color={colors.textDim}> {durStr}</Text>
295
- </Box>
296
- );
297
- });
298
- })()}
299
-
300
- <SectionHeader title="Models" />
301
- {topModels.length === 0
302
- ? <Text color={colors.textDim}>No model data</Text>
303
- : topModels.map(([model, data]) => (
304
- <Box key={model} flexDirection="row">
305
- <Box width={22}>
306
- <Text color={colors.text}>{truncate(model, 20)}</Text>
307
- </Box>
308
- <Text color={colors.info}>{data.calls}</Text>
309
- <Text color={colors.textDim}> {formatTokens(data.tokens)}</Text>
310
- </Box>
311
- ))
312
- }
313
-
314
- </Box>
315
- </Box>
316
-
317
- <StatusBar hints="1:sessions 2:tools r:refresh q:quit" />
318
- </Box>
319
- );
320
- }
321
-
322
- export const OverviewScreen = memo(OverviewScreenInner);
323
-
324
- function truncate(s: string, max: number): string {
325
- if (s.length <= max) return s;
326
- return s.slice(0, max - 1) + "…";
327
- }
@@ -1,168 +0,0 @@
1
- import React, { useState, useMemo, useCallback, memo } from "react";
2
- import { Box, Text, useInput } from "ink";
3
- import { colors } from "../theme";
4
- import { StatusBar } from "../components/StatusBar";
5
- import { AgentTree } from "../components/AgentTree";
6
- import { DetailsPanel } from "../components/DetailsPanel";
7
- import { MessagesPanel } from "../components/MessagesPanel";
8
- import type { Workflow, AgentNode, FlatNode } from "../../core/types";
9
-
10
- interface SessionsScreenProps {
11
- workflows: Workflow[];
12
- isActive: boolean;
13
- contentHeight: number;
14
- terminalWidth: number;
15
- }
16
-
17
- type RightMode = "stats" | "messages";
18
-
19
- function flattenWorkflow(workflow: Workflow, workflowIndex: number): FlatNode[] {
20
- const nodes: FlatNode[] = [];
21
- function walk(node: AgentNode) {
22
- nodes.push({
23
- id: node.session.id,
24
- session: node.session,
25
- workflowIndex,
26
- depth: node.depth,
27
- hasChildren: node.children.length > 0,
28
- agentNode: node,
29
- });
30
- for (const child of node.children) walk(child);
31
- }
32
- walk(workflow.agentTree);
33
- return nodes;
34
- }
35
-
36
- function SessionsScreenInner({
37
- workflows,
38
- isActive,
39
- contentHeight,
40
- terminalWidth,
41
- }: SessionsScreenProps) {
42
- const [selectedIndex, setSelectedIndex] = useState(0);
43
- const [rightMode, setRightMode] = useState<RightMode>("stats");
44
-
45
- const flatNodes = useMemo(() => {
46
- return workflows.flatMap((w, i) => flattenWorkflow(w, i));
47
- }, [workflows]);
48
-
49
- const clampedIndex = Math.min(selectedIndex, Math.max(0, flatNodes.length - 1));
50
- const selectedNode = flatNodes[clampedIndex] ?? null;
51
-
52
- const selectedWorkflow = useMemo(() => {
53
- if (!selectedNode) return null;
54
- const w = workflows[selectedNode.workflowIndex];
55
- if (!w) return null;
56
- if (selectedNode.session.id !== w.mainSession.id) {
57
- return {
58
- id: selectedNode.session.id,
59
- mainSession: selectedNode.session,
60
- subAgentSessions: selectedNode.agentNode.children.map((c) => c.session),
61
- agentTree: selectedNode.agentNode,
62
- };
63
- }
64
- return w;
65
- }, [selectedNode, workflows]);
66
-
67
- const leftWidth = Math.floor(terminalWidth * 0.35);
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
75
-
76
- const handleSelect = useCallback((index: number) => {
77
- setSelectedIndex(index);
78
- }, []);
79
-
80
- // SessionsScreen only handles: tab switch, tree nav (stats mode), session switch (messages mode)
81
- useInput(
82
- (input, key) => {
83
- if (key.tab) {
84
- setRightMode((m) => (m === "stats" ? "messages" : "stats"));
85
- return;
86
- }
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; }
96
- }
97
- },
98
- { isActive }
99
- );
100
-
101
- return (
102
- <Box flexDirection="column" width={terminalWidth} height={contentHeight}>
103
- <Box flexDirection="row" height={panelHeight}>
104
- {/* Left: agent/session tree */}
105
- <Box
106
- width={leftWidth}
107
- height={panelHeight}
108
- borderStyle="single"
109
- borderColor={colors.border}
110
- flexDirection="column"
111
- >
112
- <AgentTree
113
- workflows={workflows}
114
- selectedId={selectedNode?.id ?? null}
115
- flatNodes={flatNodes}
116
- onSelect={(id) => {
117
- const idx = flatNodes.findIndex((n) => n.id === id);
118
- if (idx >= 0) handleSelect(idx);
119
- }}
120
- maxHeight={innerHeight}
121
- />
122
- </Box>
123
-
124
- {/* Right: details or messages */}
125
- <Box
126
- width={rightWidth}
127
- height={panelHeight}
128
- borderStyle="single"
129
- borderColor={colors.border}
130
- flexDirection="column"
131
- >
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"}>
135
- [Stats]
136
- </Text>
137
- <Text color={colors.textDim}> </Text>
138
- <Text color={rightMode === "messages" ? colors.accent : colors.textDim} bold={rightMode === "messages"}>
139
- [Messages]
140
- </Text>
141
- <Box flexGrow={1} />
142
- <Text color={colors.textDim}>Tab:switch</Text>
143
- </Box>
144
-
145
- {rightMode === "stats" ? (
146
- <DetailsPanel workflow={selectedWorkflow} height={innerHeight - 1} />
147
- ) : (
148
- <MessagesPanel
149
- session={selectedNode?.session ?? null}
150
- maxHeight={msgHeight}
151
- isActive={isActive && rightMode === "messages"}
152
- />
153
- )}
154
- </Box>
155
- </Box>
156
-
157
- <StatusBar
158
- hints={
159
- rightMode === "messages"
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"
162
- }
163
- />
164
- </Box>
165
- );
166
- }
167
-
168
- export const SessionsScreen = memo(SessionsScreenInner);