opencode-top 3.1.2 → 3.2.1
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/bin/octop.js +2 -9
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +22432 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +22572 -0
- package/dist/core/agents.d.ts +11 -0
- package/dist/core/agents.d.ts.map +1 -0
- package/dist/core/agents.js +58 -0
- package/dist/core/agents.js.map +1 -0
- package/dist/core/session.d.ts +19 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +261 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/types.d.ts +140 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +29 -0
- package/dist/core/types.js.map +1 -0
- package/dist/data/pricing.d.ts +4 -0
- package/dist/data/pricing.d.ts.map +1 -0
- package/dist/data/pricing.js +76 -0
- package/dist/data/pricing.js.map +1 -0
- package/dist/data/sqlite.d.ts +5 -0
- package/dist/data/sqlite.d.ts.map +1 -0
- package/dist/data/sqlite.js +222 -0
- package/dist/data/sqlite.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/{src/index.ts → dist/index.js} +1 -0
- package/dist/index.js.map +1 -0
- package/dist/ui/App.d.ts +6 -0
- package/dist/ui/App.d.ts.map +1 -0
- package/dist/ui/App.js +101 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/components/AgentChainGraph.d.ts +9 -0
- package/dist/ui/components/AgentChainGraph.d.ts.map +1 -0
- package/dist/ui/components/AgentChainGraph.js +41 -0
- package/dist/ui/components/AgentChainGraph.js.map +1 -0
- package/dist/ui/components/AgentTree.d.ts +13 -0
- package/dist/ui/components/AgentTree.d.ts.map +1 -0
- package/dist/ui/components/AgentTree.js +50 -0
- package/dist/ui/components/AgentTree.js.map +1 -0
- package/dist/ui/components/DetailsPanel.d.ts +9 -0
- package/dist/ui/components/DetailsPanel.d.ts.map +1 -0
- package/dist/ui/components/DetailsPanel.js +82 -0
- package/dist/ui/components/DetailsPanel.js.map +1 -0
- package/dist/ui/components/MessagesPanel.d.ts +12 -0
- package/dist/ui/components/MessagesPanel.d.ts.map +1 -0
- package/dist/ui/components/MessagesPanel.js +107 -0
- package/dist/ui/components/MessagesPanel.js.map +1 -0
- package/dist/ui/components/SparkLine.d.ts +10 -0
- package/dist/ui/components/SparkLine.d.ts.map +1 -0
- package/dist/ui/components/SparkLine.js +12 -0
- package/dist/ui/components/SparkLine.js.map +1 -0
- package/dist/ui/components/StatusBar.d.ts +9 -0
- package/dist/ui/components/StatusBar.d.ts.map +1 -0
- package/dist/ui/components/StatusBar.js +9 -0
- package/dist/ui/components/StatusBar.js.map +1 -0
- package/dist/ui/components/TabBar.d.ts +10 -0
- package/dist/ui/components/TabBar.d.ts.map +1 -0
- package/dist/ui/components/TabBar.js +17 -0
- package/dist/ui/components/TabBar.js.map +1 -0
- package/dist/ui/screens/OverviewScreen.d.ts +12 -0
- package/dist/ui/screens/OverviewScreen.d.ts.map +1 -0
- package/dist/ui/screens/OverviewScreen.js +94 -0
- package/dist/ui/screens/OverviewScreen.js.map +1 -0
- package/dist/ui/screens/SessionsScreen.d.ts +12 -0
- package/dist/ui/screens/SessionsScreen.d.ts.map +1 -0
- package/dist/ui/screens/SessionsScreen.js +98 -0
- package/dist/ui/screens/SessionsScreen.js.map +1 -0
- package/dist/ui/screens/TimelineScreen.d.ts +11 -0
- package/dist/ui/screens/TimelineScreen.d.ts.map +1 -0
- package/dist/ui/screens/TimelineScreen.js +128 -0
- package/dist/ui/screens/TimelineScreen.js.map +1 -0
- package/dist/ui/screens/ToolsScreen.d.ts +12 -0
- package/dist/ui/screens/ToolsScreen.d.ts.map +1 -0
- package/dist/ui/screens/ToolsScreen.js +113 -0
- package/dist/ui/screens/ToolsScreen.js.map +1 -0
- package/dist/ui/theme.d.ts +21 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +21 -0
- package/dist/ui/theme.js.map +1 -0
- package/package.json +3 -1
- package/patches/ink+5.2.1.patch +13 -0
- package/bin/octop.mjs +0 -13
- package/src/cli.ts +0 -60
- package/src/core/agents.ts +0 -78
- package/src/core/session.ts +0 -315
- package/src/core/types.ts +0 -156
- package/src/data/pricing.ts +0 -82
- package/src/data/sqlite.ts +0 -347
- package/src/ui/App.tsx +0 -154
- package/src/ui/components/AgentChainGraph.tsx +0 -95
- package/src/ui/components/AgentTree.tsx +0 -101
- package/src/ui/components/DetailsPanel.tsx +0 -211
- package/src/ui/components/MessagesPanel.tsx +0 -323
- package/src/ui/components/SparkLine.tsx +0 -18
- package/src/ui/components/StatusBar.tsx +0 -24
- package/src/ui/components/TabBar.tsx +0 -42
- package/src/ui/screens/OverviewScreen.tsx +0 -327
- package/src/ui/screens/SessionsScreen.tsx +0 -168
- package/src/ui/screens/TimelineScreen.tsx +0 -222
- package/src/ui/screens/ToolsScreen.tsx +0 -260
- package/src/ui/theme.ts +0 -21
|
@@ -1,222 +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 type { Workflow, Interaction, MessagePart } from "../../core/types";
|
|
6
|
-
|
|
7
|
-
interface TimelineScreenProps {
|
|
8
|
-
workflows: Workflow[];
|
|
9
|
-
isActive: boolean;
|
|
10
|
-
contentHeight: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type TimelineLine =
|
|
14
|
-
| { kind: "session-header"; sessionId: string; title: string; agent: string | null; time: number | null }
|
|
15
|
-
| { kind: "interaction-header"; interactionId: string; modelId: string; time: number | null; agent: string | null }
|
|
16
|
-
| { kind: "tool-call"; part: MessagePart & { type: "tool" }; interactionId: string }
|
|
17
|
-
| { kind: "text-snippet"; text: string; interactionId: string }
|
|
18
|
-
| { kind: "reasoning-snippet"; text: string; interactionId: string }
|
|
19
|
-
| { kind: "spacer" };
|
|
20
|
-
|
|
21
|
-
function formatTime(ts: number | null): string {
|
|
22
|
-
if (!ts) return "??:??";
|
|
23
|
-
return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function buildTimeline(workflows: Workflow[]): TimelineLine[] {
|
|
27
|
-
const lines: TimelineLine[] = [];
|
|
28
|
-
|
|
29
|
-
const allSessions = workflows.flatMap((w) => [
|
|
30
|
-
w.mainSession,
|
|
31
|
-
...w.subAgentSessions,
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
// Sort by timeCreated
|
|
35
|
-
allSessions.sort((a, b) => (a.timeCreated ?? 0) - (b.timeCreated ?? 0));
|
|
36
|
-
|
|
37
|
-
for (const session of allSessions) {
|
|
38
|
-
const agentName = session.interactions[0]?.agent ?? null;
|
|
39
|
-
lines.push({
|
|
40
|
-
kind: "session-header",
|
|
41
|
-
sessionId: session.id,
|
|
42
|
-
title: session.title ?? session.id.slice(0, 12),
|
|
43
|
-
agent: agentName,
|
|
44
|
-
time: session.timeCreated,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
for (const interaction of session.interactions) {
|
|
48
|
-
if (interaction.role !== "assistant") continue;
|
|
49
|
-
|
|
50
|
-
lines.push({
|
|
51
|
-
kind: "interaction-header",
|
|
52
|
-
interactionId: interaction.id,
|
|
53
|
-
modelId: interaction.modelId,
|
|
54
|
-
time: interaction.time.created,
|
|
55
|
-
agent: interaction.agent,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
for (const part of interaction.parts) {
|
|
59
|
-
if (part.type === "tool") {
|
|
60
|
-
lines.push({
|
|
61
|
-
kind: "tool-call",
|
|
62
|
-
part: part as MessagePart & { type: "tool" },
|
|
63
|
-
interactionId: interaction.id,
|
|
64
|
-
});
|
|
65
|
-
} else if (part.type === "text" && part.text.trim().length > 0) {
|
|
66
|
-
lines.push({
|
|
67
|
-
kind: "text-snippet",
|
|
68
|
-
text: part.text.slice(0, 120).replace(/\n/g, " "),
|
|
69
|
-
interactionId: interaction.id,
|
|
70
|
-
});
|
|
71
|
-
} else if (part.type === "reasoning" && part.text.trim().length > 0) {
|
|
72
|
-
lines.push({
|
|
73
|
-
kind: "reasoning-snippet",
|
|
74
|
-
text: part.text.slice(0, 100).replace(/\n/g, " "),
|
|
75
|
-
interactionId: interaction.id,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
lines.push({ kind: "spacer" });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return lines;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function renderLine(line: TimelineLine, idx: number): React.ReactElement {
|
|
88
|
-
switch (line.kind) {
|
|
89
|
-
case "session-header":
|
|
90
|
-
return (
|
|
91
|
-
<Box key={idx} flexDirection="row">
|
|
92
|
-
<Text color={colors.accent} bold>
|
|
93
|
-
▶ {truncate(line.title, 40)}
|
|
94
|
-
</Text>
|
|
95
|
-
{line.agent && <Text color={colors.cyan}> [{line.agent}]</Text>}
|
|
96
|
-
<Box flexGrow={1} />
|
|
97
|
-
<Text color={colors.textDim}>{formatTime(line.time)}</Text>
|
|
98
|
-
</Box>
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
case "interaction-header":
|
|
102
|
-
return (
|
|
103
|
-
<Box key={idx} flexDirection="row">
|
|
104
|
-
<Text color={colors.textDim}> </Text>
|
|
105
|
-
<Text color={colors.purple}>◆ </Text>
|
|
106
|
-
<Text color={colors.info}>{truncate(line.modelId, 30)}</Text>
|
|
107
|
-
{line.agent && <Text color={colors.cyan}> [{line.agent}]</Text>}
|
|
108
|
-
<Box flexGrow={1} />
|
|
109
|
-
<Text color={colors.textDim}>{formatTime(line.time)}</Text>
|
|
110
|
-
</Box>
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
case "tool-call": {
|
|
114
|
-
const p = line.part;
|
|
115
|
-
const statusIcon = p.status === "completed" ? "✓" : p.status === "error" ? "✗" : "◌";
|
|
116
|
-
const statusColor =
|
|
117
|
-
p.status === "completed" ? colors.success : p.status === "error" ? colors.error : colors.warning;
|
|
118
|
-
const durationMs = p.timeEnd > 0 && p.timeStart > 0 ? p.timeEnd - p.timeStart : 0;
|
|
119
|
-
const durationStr = durationMs > 0 ? ` ${(durationMs / 1000).toFixed(1)}s` : "";
|
|
120
|
-
return (
|
|
121
|
-
<Box key={idx} flexDirection="row">
|
|
122
|
-
<Text color={colors.textDim}> </Text>
|
|
123
|
-
<Text color={statusColor}>{statusIcon} </Text>
|
|
124
|
-
<Text color={colors.text}>{truncate(p.toolName, 20)}</Text>
|
|
125
|
-
{p.title && <Text color={colors.textDim}> {truncate(p.title, 25)}</Text>}
|
|
126
|
-
<Box flexGrow={1} />
|
|
127
|
-
<Text color={colors.textDim}>{durationStr}</Text>
|
|
128
|
-
</Box>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
case "text-snippet":
|
|
133
|
-
return (
|
|
134
|
-
<Box key={idx} flexDirection="row">
|
|
135
|
-
<Text color={colors.textDim}> │ </Text>
|
|
136
|
-
<Text color={colors.text}>{truncate(line.text, 80)}</Text>
|
|
137
|
-
</Box>
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
case "reasoning-snippet":
|
|
141
|
-
return (
|
|
142
|
-
<Box key={idx} flexDirection="row">
|
|
143
|
-
<Text color={colors.textDim}> ⚡ </Text>
|
|
144
|
-
<Text color={colors.accentDim}>{truncate(line.text, 80)}</Text>
|
|
145
|
-
</Box>
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
case "spacer":
|
|
149
|
-
return <Box key={idx} />;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function TimelineScreenInner({ workflows, isActive, contentHeight }: TimelineScreenProps) {
|
|
154
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
155
|
-
|
|
156
|
-
const allLines = useMemo(() => buildTimeline(workflows), [workflows]);
|
|
157
|
-
|
|
158
|
-
const visibleHeight = contentHeight - 4; // header row + status bar + borders
|
|
159
|
-
|
|
160
|
-
const clampOffset = useCallback(
|
|
161
|
-
(offset: number) => Math.max(0, Math.min(offset, Math.max(0, allLines.length - visibleHeight))),
|
|
162
|
-
[allLines.length, visibleHeight]
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
useInput(
|
|
166
|
-
(input, key) => {
|
|
167
|
-
if (key.upArrow || input === "k") {
|
|
168
|
-
setScrollOffset((o) => clampOffset(o - 1));
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (key.downArrow || input === "j") {
|
|
172
|
-
setScrollOffset((o) => clampOffset(o + 1));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (input === "g") {
|
|
176
|
-
setScrollOffset(0);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (input === "G") {
|
|
180
|
-
setScrollOffset(clampOffset(allLines.length));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (key.pageUp) {
|
|
184
|
-
setScrollOffset((o) => clampOffset(o - visibleHeight));
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (key.pageDown) {
|
|
188
|
-
setScrollOffset((o) => clampOffset(o + visibleHeight));
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
{ isActive }
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
const visibleLines = allLines.slice(scrollOffset, scrollOffset + visibleHeight);
|
|
196
|
-
|
|
197
|
-
return (
|
|
198
|
-
<Box flexDirection="column" height={contentHeight}>
|
|
199
|
-
<Box paddingX={1} flexDirection="row">
|
|
200
|
-
<Text color={colors.accent} bold>
|
|
201
|
-
Timeline
|
|
202
|
-
</Text>
|
|
203
|
-
<Box flexGrow={1} />
|
|
204
|
-
<Text color={colors.textDim}>
|
|
205
|
-
{scrollOffset + 1}-{Math.min(scrollOffset + visibleHeight, allLines.length)}/
|
|
206
|
-
{allLines.length}
|
|
207
|
-
</Text>
|
|
208
|
-
</Box>
|
|
209
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
210
|
-
{visibleLines.map((line, i) => renderLine(line, scrollOffset + i))}
|
|
211
|
-
</Box>
|
|
212
|
-
<StatusBar hints="j/k:scroll g/G:top/bottom PgUp/PgDn:page 1:sessions 3:tools 4:overview q:quit" />
|
|
213
|
-
</Box>
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export const TimelineScreen = memo(TimelineScreenInner);
|
|
218
|
-
|
|
219
|
-
function truncate(s: string, max: number): string {
|
|
220
|
-
if (s.length <= max) return s;
|
|
221
|
-
return s.slice(0, max - 1) + "…";
|
|
222
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import React, { useState, useMemo, memo } from "react";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import { colors } from "../theme";
|
|
4
|
-
import { StatusBar } from "../components/StatusBar";
|
|
5
|
-
import type { Workflow, ToolUsage } from "../../core/types";
|
|
6
|
-
import { getToolUsage } from "../../core/session";
|
|
7
|
-
|
|
8
|
-
interface ToolsScreenProps {
|
|
9
|
-
workflows: Workflow[];
|
|
10
|
-
isActive: boolean;
|
|
11
|
-
contentHeight: number;
|
|
12
|
-
terminalWidth: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type SortKey = "calls" | "failures" | "avgTime";
|
|
16
|
-
|
|
17
|
-
function aggregateToolUsage(workflows: Workflow[]): ToolUsage[] {
|
|
18
|
-
const merged = new Map<
|
|
19
|
-
string,
|
|
20
|
-
{ calls: number; successes: number; failures: number; totalDurationMs: number; recentErrors: string[] }
|
|
21
|
-
>();
|
|
22
|
-
|
|
23
|
-
for (const workflow of workflows) {
|
|
24
|
-
const allSessions = [workflow.mainSession, ...workflow.subAgentSessions];
|
|
25
|
-
for (const session of allSessions) {
|
|
26
|
-
for (const tool of getToolUsage(session)) {
|
|
27
|
-
const existing = merged.get(tool.name) ?? {
|
|
28
|
-
calls: 0,
|
|
29
|
-
successes: 0,
|
|
30
|
-
failures: 0,
|
|
31
|
-
totalDurationMs: 0,
|
|
32
|
-
recentErrors: [],
|
|
33
|
-
};
|
|
34
|
-
existing.calls += tool.calls;
|
|
35
|
-
existing.successes += tool.successes;
|
|
36
|
-
existing.failures += tool.failures;
|
|
37
|
-
existing.totalDurationMs += tool.totalDurationMs;
|
|
38
|
-
for (const err of tool.recentErrors) {
|
|
39
|
-
if (existing.recentErrors.length < 3) existing.recentErrors.push(err);
|
|
40
|
-
}
|
|
41
|
-
merged.set(tool.name, existing);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return Array.from(merged.entries()).map(([name, s]) => ({
|
|
47
|
-
name,
|
|
48
|
-
calls: s.calls,
|
|
49
|
-
successes: s.successes,
|
|
50
|
-
failures: s.failures,
|
|
51
|
-
totalDurationMs: s.totalDurationMs,
|
|
52
|
-
avgDurationMs: s.calls > 0 ? s.totalDurationMs / s.calls : 0,
|
|
53
|
-
recentErrors: s.recentErrors,
|
|
54
|
-
}));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function SuccessBar({ successes, calls, width = 12 }: { successes: number; calls: number; width?: number }) {
|
|
58
|
-
const pct = calls > 0 ? successes / calls : 0;
|
|
59
|
-
const filled = Math.round(pct * width);
|
|
60
|
-
const empty = width - filled;
|
|
61
|
-
const color = pct >= 0.9 ? colors.success : pct >= 0.7 ? colors.warning : colors.error;
|
|
62
|
-
return (
|
|
63
|
-
<Text>
|
|
64
|
-
<Text color={color}>{"█".repeat(filled)}</Text>
|
|
65
|
-
<Text color={colors.border}>{"░".repeat(empty)}</Text>
|
|
66
|
-
<Text color={colors.textDim}> {Math.round(pct * 100)}%</Text>
|
|
67
|
-
</Text>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function formatDuration(ms: number): string {
|
|
72
|
-
if (ms === 0) return "—";
|
|
73
|
-
if (ms < 1000) return `${ms.toFixed(0)}ms`;
|
|
74
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function ToolsScreenInner({ workflows, isActive, contentHeight, terminalWidth }: ToolsScreenProps) {
|
|
78
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
79
|
-
const [sortKey, setSortKey] = useState<SortKey>("calls");
|
|
80
|
-
|
|
81
|
-
const allTools = useMemo(() => aggregateToolUsage(workflows), [workflows]);
|
|
82
|
-
|
|
83
|
-
const sortedTools = useMemo(() => {
|
|
84
|
-
const copy = [...allTools];
|
|
85
|
-
switch (sortKey) {
|
|
86
|
-
case "calls":
|
|
87
|
-
return copy.sort((a, b) => b.calls - a.calls);
|
|
88
|
-
case "failures":
|
|
89
|
-
return copy.sort((a, b) => b.failures - a.failures);
|
|
90
|
-
case "avgTime":
|
|
91
|
-
return copy.sort((a, b) => b.avgDurationMs - a.avgDurationMs);
|
|
92
|
-
}
|
|
93
|
-
}, [allTools, sortKey]);
|
|
94
|
-
|
|
95
|
-
const listHeight = contentHeight - 4; // header + status bar + padding
|
|
96
|
-
const clampedIndex = Math.min(selectedIndex, Math.max(0, sortedTools.length - 1));
|
|
97
|
-
const selectedTool = sortedTools[clampedIndex] ?? null;
|
|
98
|
-
|
|
99
|
-
// Pagination
|
|
100
|
-
const startIndex = clampedIndex >= listHeight ? clampedIndex - listHeight + 1 : 0;
|
|
101
|
-
const visibleTools = sortedTools.slice(startIndex, startIndex + listHeight);
|
|
102
|
-
|
|
103
|
-
useInput(
|
|
104
|
-
(input, key) => {
|
|
105
|
-
if (key.upArrow || input === "k") {
|
|
106
|
-
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (key.downArrow || input === "j") {
|
|
110
|
-
setSelectedIndex((i) => Math.min(sortedTools.length - 1, i + 1));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (key.tab) {
|
|
114
|
-
setSortKey((k) => {
|
|
115
|
-
if (k === "calls") return "failures";
|
|
116
|
-
if (k === "failures") return "avgTime";
|
|
117
|
-
return "calls";
|
|
118
|
-
});
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
{ isActive }
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const sortLabels: Record<SortKey, string> = {
|
|
126
|
-
calls: "Calls",
|
|
127
|
-
failures: "Failures",
|
|
128
|
-
avgTime: "Avg Time",
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<Box flexDirection="column" width={terminalWidth} height={contentHeight}>
|
|
133
|
-
<Box paddingX={1} flexDirection="row">
|
|
134
|
-
<Text color={colors.accent} bold>
|
|
135
|
-
Tools
|
|
136
|
-
</Text>
|
|
137
|
-
<Box flexGrow={1} />
|
|
138
|
-
<Text color={colors.textDim}>
|
|
139
|
-
sort:{" "}
|
|
140
|
-
{(["calls", "failures", "avgTime"] as SortKey[]).map((k) => (
|
|
141
|
-
<Text key={k} color={sortKey === k ? colors.accent : colors.textDim}>
|
|
142
|
-
[{k === sortKey ? sortLabels[k] : k}]{" "}
|
|
143
|
-
</Text>
|
|
144
|
-
))}
|
|
145
|
-
Tab:cycle
|
|
146
|
-
</Text>
|
|
147
|
-
</Box>
|
|
148
|
-
|
|
149
|
-
<Box flexDirection="row" flexGrow={1}>
|
|
150
|
-
{/* Left: tool list */}
|
|
151
|
-
<Box width={36} flexDirection="column" borderStyle="single" borderColor={colors.border}>
|
|
152
|
-
<Box paddingX={1} flexDirection="row">
|
|
153
|
-
<Text color={colors.textDim} bold>
|
|
154
|
-
Tool
|
|
155
|
-
</Text>
|
|
156
|
-
<Box flexGrow={1} />
|
|
157
|
-
<Text color={colors.textDim}>calls </Text>
|
|
158
|
-
<Text color={colors.textDim}>err</Text>
|
|
159
|
-
</Box>
|
|
160
|
-
{visibleTools.map((tool, i) => {
|
|
161
|
-
const isSelected = tool.name === selectedTool?.name;
|
|
162
|
-
return (
|
|
163
|
-
<Box key={tool.name} flexDirection="row" paddingX={1}>
|
|
164
|
-
<Text
|
|
165
|
-
color={isSelected ? colors.accent : colors.textDim}
|
|
166
|
-
bold={isSelected}
|
|
167
|
-
>
|
|
168
|
-
{isSelected ? "▶ " : " "}{truncate(tool.name, 20)}
|
|
169
|
-
</Text>
|
|
170
|
-
<Box flexGrow={1} />
|
|
171
|
-
<Text color={colors.info}>{tool.calls}</Text>
|
|
172
|
-
<Text color={colors.textDim}> </Text>
|
|
173
|
-
<Text color={tool.failures > 0 ? colors.error : colors.textDim}>{tool.failures}</Text>
|
|
174
|
-
</Box>
|
|
175
|
-
);
|
|
176
|
-
})}
|
|
177
|
-
{allTools.length === 0 && (
|
|
178
|
-
<Box paddingX={1}>
|
|
179
|
-
<Text color={colors.textDim}>No tool data yet</Text>
|
|
180
|
-
</Box>
|
|
181
|
-
)}
|
|
182
|
-
</Box>
|
|
183
|
-
|
|
184
|
-
{/* Right: detail panel */}
|
|
185
|
-
<Box flexGrow={1} flexDirection="column" borderStyle="single" borderColor={colors.border} paddingX={1}>
|
|
186
|
-
{selectedTool ? (
|
|
187
|
-
<>
|
|
188
|
-
<Text color={colors.cyan} bold>
|
|
189
|
-
{selectedTool.name}
|
|
190
|
-
</Text>
|
|
191
|
-
<Box marginTop={1} flexDirection="column">
|
|
192
|
-
<Box flexDirection="row">
|
|
193
|
-
<Box width={14}>
|
|
194
|
-
<Text color={colors.textDim}>Total calls</Text>
|
|
195
|
-
</Box>
|
|
196
|
-
<Text color={colors.text}>{selectedTool.calls}</Text>
|
|
197
|
-
</Box>
|
|
198
|
-
<Box flexDirection="row">
|
|
199
|
-
<Box width={14}>
|
|
200
|
-
<Text color={colors.textDim}>Successes</Text>
|
|
201
|
-
</Box>
|
|
202
|
-
<Text color={colors.success}>{selectedTool.successes}</Text>
|
|
203
|
-
</Box>
|
|
204
|
-
<Box flexDirection="row">
|
|
205
|
-
<Box width={14}>
|
|
206
|
-
<Text color={colors.textDim}>Failures</Text>
|
|
207
|
-
</Box>
|
|
208
|
-
<Text color={selectedTool.failures > 0 ? colors.error : colors.textDim}>
|
|
209
|
-
{selectedTool.failures}
|
|
210
|
-
</Text>
|
|
211
|
-
</Box>
|
|
212
|
-
<Box flexDirection="row">
|
|
213
|
-
<Box width={14}>
|
|
214
|
-
<Text color={colors.textDim}>Avg time</Text>
|
|
215
|
-
</Box>
|
|
216
|
-
<Text color={colors.info}>{formatDuration(selectedTool.avgDurationMs)}</Text>
|
|
217
|
-
</Box>
|
|
218
|
-
</Box>
|
|
219
|
-
|
|
220
|
-
<Box marginTop={1}>
|
|
221
|
-
<Text color={colors.textDim}>Success rate </Text>
|
|
222
|
-
<SuccessBar successes={selectedTool.successes} calls={selectedTool.calls} />
|
|
223
|
-
</Box>
|
|
224
|
-
|
|
225
|
-
{selectedTool.recentErrors.length > 0 && (
|
|
226
|
-
<>
|
|
227
|
-
<Box marginTop={1}>
|
|
228
|
-
<Text color={colors.warning} bold>
|
|
229
|
-
Recent Errors
|
|
230
|
-
</Text>
|
|
231
|
-
</Box>
|
|
232
|
-
{selectedTool.recentErrors.map((err, i) => (
|
|
233
|
-
<Box key={i} flexDirection="row">
|
|
234
|
-
<Text color={colors.error}>• </Text>
|
|
235
|
-
<Text color={colors.text}>{truncate(err, 60)}</Text>
|
|
236
|
-
</Box>
|
|
237
|
-
))}
|
|
238
|
-
</>
|
|
239
|
-
)}
|
|
240
|
-
</>
|
|
241
|
-
) : (
|
|
242
|
-
<Text color={colors.textDim}>No tools found. Run some OpenCode sessions first.</Text>
|
|
243
|
-
)}
|
|
244
|
-
</Box>
|
|
245
|
-
</Box>
|
|
246
|
-
|
|
247
|
-
<StatusBar
|
|
248
|
-
hints="j/k:nav Tab:cycle-sort 1:sessions 3:overview q:quit"
|
|
249
|
-
info={`${allTools.length} tools`}
|
|
250
|
-
/>
|
|
251
|
-
</Box>
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export const ToolsScreen = memo(ToolsScreenInner);
|
|
256
|
-
|
|
257
|
-
function truncate(s: string, max: number): string {
|
|
258
|
-
if (s.length <= max) return s;
|
|
259
|
-
return s.slice(0, max - 1) + "…";
|
|
260
|
-
}
|
package/src/ui/theme.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export const colors = {
|
|
2
|
-
bg: "#1a1a2e",
|
|
3
|
-
bgSecondary: "#16213e",
|
|
4
|
-
border: "#0f3460",
|
|
5
|
-
accent: "#e94560",
|
|
6
|
-
accentDim: "#533483",
|
|
7
|
-
text: "#eaeaea",
|
|
8
|
-
textDim: "#888",
|
|
9
|
-
success: "#4ade80",
|
|
10
|
-
warning: "#fbbf24",
|
|
11
|
-
error: "#f87171",
|
|
12
|
-
info: "#60a5fa",
|
|
13
|
-
purple: "#a855f7",
|
|
14
|
-
cyan: "#22d3ee",
|
|
15
|
-
} as const;
|
|
16
|
-
|
|
17
|
-
export const tokens = {
|
|
18
|
-
radius: 1,
|
|
19
|
-
paddingX: 1,
|
|
20
|
-
paddingY: 0,
|
|
21
|
-
} as const;
|