opencode-top 3.1.2 → 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 -154
  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,347 +0,0 @@
1
- import Database from "better-sqlite3";
2
- import { TokenUsage, type Session, type Interaction, type MessagePart } from "../core/types";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
-
6
- interface DbSession {
7
- id: string;
8
- parent_id: string | null;
9
- project_id: string | null;
10
- title: string | null;
11
- time_created: number | null;
12
- time_archived: number | null;
13
- project_name: string | null;
14
- }
15
-
16
- interface DbMessage {
17
- id: string;
18
- session_id: string;
19
- data: string;
20
- time_created: number;
21
- }
22
-
23
- interface DbPart {
24
- message_id: string;
25
- session_id: string;
26
- data: string;
27
- }
28
-
29
- // Real message.data schema from OpenCode
30
- interface RealMessageData {
31
- id?: string;
32
- parentID?: string;
33
- role?: "assistant" | "user";
34
- agent?: string;
35
- mode?: string;
36
- modelID?: string;
37
- providerID?: string;
38
- time?: {
39
- created?: number;
40
- completed?: number;
41
- };
42
- tokens?: {
43
- input?: number;
44
- output?: number;
45
- reasoning?: number;
46
- cache?: {
47
- read?: number;
48
- write?: number;
49
- };
50
- };
51
- cost?: number;
52
- finish?: string;
53
- // Legacy fields (older messages may still have these)
54
- usage?: {
55
- input_tokens?: number;
56
- output_tokens?: number;
57
- cache_read_input_tokens?: number;
58
- cache_write_input_tokens?: number;
59
- };
60
- model?: string;
61
- stop_reason?: string;
62
- }
63
-
64
- // Real part.data schema from OpenCode
65
- interface RealPartData {
66
- type?: string;
67
- text?: string;
68
- time?: {
69
- start?: number;
70
- end?: number;
71
- };
72
- // tool call fields
73
- callID?: string;
74
- tool?: string;
75
- state?: {
76
- status?: string;
77
- input?: Record<string, unknown>;
78
- output?: string;
79
- title?: string;
80
- time?: {
81
- start?: number;
82
- end?: number;
83
- };
84
- metadata?: {
85
- exit?: number;
86
- exitCode?: number;
87
- truncated?: boolean;
88
- };
89
- };
90
- // patch fields
91
- hash?: string;
92
- files?: string[];
93
- }
94
-
95
- export function getDbPath(): string {
96
- return path.join(os.homedir(), ".local", "share", "opencode", "opencode.db");
97
- }
98
-
99
- export function loadSessions(dbPath: string = getDbPath()): Session[] {
100
- const db = new Database(dbPath, { readonly: true });
101
-
102
- const sessions = db
103
- .prepare(`
104
- SELECT
105
- s.id, s.parent_id, s.project_id, s.title,
106
- s.time_created, s.time_archived,
107
- p.name as project_name
108
- FROM session s
109
- LEFT JOIN project p ON s.project_id = p.id
110
- ORDER BY s.time_created DESC
111
- `)
112
- .all() as DbSession[];
113
-
114
- const sessionIds = sessions.map((s) => s.id);
115
- const interactions = loadInteractions(db, sessionIds);
116
- db.close();
117
-
118
- return sessions.map((s) => ({
119
- id: s.id,
120
- parentId: s.parent_id,
121
- projectId: s.project_id,
122
- projectName: s.project_name,
123
- title: s.title,
124
- timeCreated: s.time_created,
125
- timeArchived: s.time_archived,
126
- interactions: interactions.get(s.id) ?? [],
127
- source: "sqlite" as const,
128
- }));
129
- }
130
-
131
- function loadInteractions(
132
- db: Database.Database,
133
- sessionIds: string[]
134
- ): Map<string, Interaction[]> {
135
- const result = new Map<string, Interaction[]>();
136
-
137
- if (sessionIds.length === 0) return result;
138
-
139
- const placeholders = sessionIds.map(() => "?").join(",");
140
-
141
- const messages = db
142
- .prepare(`
143
- SELECT id, session_id, data, time_created
144
- FROM message
145
- WHERE session_id IN (${placeholders})
146
- ORDER BY time_created ASC
147
- `)
148
- .all(...sessionIds) as DbMessage[];
149
-
150
- if (messages.length === 0) return result;
151
-
152
- // Load all parts for these messages in one batch
153
- const messageIds = messages.map((m) => m.id);
154
- const partsByMessageId = loadParts(db, messageIds);
155
-
156
- for (const msg of messages) {
157
- if (!result.has(msg.session_id)) {
158
- result.set(msg.session_id, []);
159
- }
160
-
161
- const parts = partsByMessageId.get(msg.id) ?? [];
162
- const parsed = parseMessageData(msg.data, msg.id, msg.session_id, msg.time_created, parts);
163
- if (parsed) {
164
- result.get(msg.session_id)!.push(parsed);
165
- }
166
- }
167
-
168
- return result;
169
- }
170
-
171
- function loadParts(db: Database.Database, messageIds: string[]): Map<string, MessagePart[]> {
172
- const result = new Map<string, MessagePart[]>();
173
-
174
- if (messageIds.length === 0) return result;
175
-
176
- // Check if part table exists
177
- const tableExists = db
178
- .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='part'")
179
- .get();
180
- if (!tableExists) return result;
181
-
182
- const placeholders = messageIds.map(() => "?").join(",");
183
- let rows: DbPart[] = [];
184
-
185
- try {
186
- rows = db
187
- .prepare(`
188
- SELECT message_id, session_id, data
189
- FROM part
190
- WHERE message_id IN (${placeholders})
191
- ORDER BY rowid ASC
192
- `)
193
- .all(...messageIds) as DbPart[];
194
- } catch {
195
- // part table may have different schema
196
- return result;
197
- }
198
-
199
- for (const row of rows) {
200
- if (!result.has(row.message_id)) {
201
- result.set(row.message_id, []);
202
- }
203
- const part = parsePart(row.data);
204
- if (part) {
205
- result.get(row.message_id)!.push(part);
206
- }
207
- }
208
-
209
- return result;
210
- }
211
-
212
- function parsePart(data: string): MessagePart | null {
213
- try {
214
- const json = JSON.parse(data) as RealPartData;
215
- const timeStart = json.time?.start ?? 0;
216
- const timeEnd = json.time?.end ?? 0;
217
-
218
- switch (json.type) {
219
- case "text":
220
- return {
221
- type: "text",
222
- text: json.text ?? "",
223
- timeStart,
224
- timeEnd,
225
- };
226
-
227
- case "tool": {
228
- const state = json.state ?? {};
229
- const status = normalizeToolStatus(state.status);
230
- // Timing lives in state.time, not top-level time
231
- const toolTimeStart = state.time?.start ?? timeStart;
232
- const toolTimeEnd = state.time?.end ?? timeEnd;
233
- return {
234
- type: "tool",
235
- callId: json.callID ?? "",
236
- toolName: json.tool ?? "unknown",
237
- status,
238
- input: state.input ?? {},
239
- output: state.output ?? "",
240
- title: state.title ?? null,
241
- exitCode: state.metadata?.exit ?? state.metadata?.exitCode ?? null,
242
- truncated: state.metadata?.truncated ?? false,
243
- timeStart: toolTimeStart,
244
- timeEnd: toolTimeEnd,
245
- };
246
- }
247
-
248
- case "reasoning":
249
- return {
250
- type: "reasoning",
251
- text: json.text ?? "",
252
- timeStart,
253
- timeEnd,
254
- };
255
-
256
- case "patch":
257
- return {
258
- type: "patch",
259
- hash: json.hash ?? "",
260
- files: json.files ?? [],
261
- };
262
-
263
- default:
264
- return null;
265
- }
266
- } catch {
267
- return null;
268
- }
269
- }
270
-
271
- function normalizeToolStatus(status: string | undefined): "completed" | "pending" | "error" {
272
- if (status === "completed") return "completed";
273
- if (status === "error") return "error";
274
- return "pending";
275
- }
276
-
277
- function parseMessageData(
278
- data: string,
279
- messageId: string,
280
- sessionId: string,
281
- timeCreated: number,
282
- parts: MessagePart[]
283
- ): Interaction | null {
284
- try {
285
- const json = JSON.parse(data) as RealMessageData;
286
-
287
- // Support both new schema (tokens.*) and legacy schema (usage.*)
288
- const newTokens = json.tokens;
289
- const legacyUsage = json.usage ?? {};
290
-
291
- const input = newTokens?.input ?? legacyUsage.input_tokens ?? 0;
292
- const output = newTokens?.output ?? legacyUsage.output_tokens ?? 0;
293
- const cacheRead = newTokens?.cache?.read ?? legacyUsage.cache_read_input_tokens ?? 0;
294
- const cacheWrite = newTokens?.cache?.write ?? legacyUsage.cache_write_input_tokens ?? 0;
295
- const reasoning = newTokens?.reasoning ?? 0;
296
-
297
- const timeCompleted = json.time?.completed ?? null;
298
- const timeDelta =
299
- timeCompleted && json.time?.created
300
- ? (timeCompleted - json.time.created) / 1000
301
- : null;
302
- const outputRate =
303
- output > 0 && timeDelta && timeDelta > 0 ? output / timeDelta : 0;
304
-
305
- const role = json.role ?? "assistant";
306
-
307
- // Only include interactions that have meaningful data (assistant messages with tokens)
308
- if (role !== "assistant" && input === 0 && output === 0) return null;
309
-
310
- return {
311
- id: messageId,
312
- sessionId,
313
- modelId: normalizeModelName(json.modelID ?? json.model ?? "unknown"),
314
- providerId: json.providerID ?? null,
315
- role,
316
- tokens: new TokenUsage(input, output, cacheRead, cacheWrite, reasoning),
317
- time: {
318
- created: json.time?.created ?? timeCreated,
319
- completed: timeCompleted,
320
- },
321
- agent: json.agent ?? null,
322
- finishReason: json.finish ?? json.stop_reason ?? null,
323
- outputRate,
324
- parts,
325
- };
326
- } catch {
327
- return null;
328
- }
329
- }
330
-
331
- function normalizeModelName(model: string): string {
332
- return model
333
- .replace(/-\d{8}$/, "")
334
- .replace(/:/g, "-")
335
- .toLowerCase();
336
- }
337
-
338
- export function sessionExists(dbPath: string = getDbPath()): boolean {
339
- try {
340
- const db = new Database(dbPath, { readonly: true });
341
- db.prepare("SELECT 1 FROM session LIMIT 1").get();
342
- db.close();
343
- return true;
344
- } catch {
345
- return false;
346
- }
347
- }
package/src/ui/App.tsx DELETED
@@ -1,154 +0,0 @@
1
- import React, { useState, useEffect, useCallback, useRef } from "react";
2
- import { Box, Text, useApp, useInput, useStdout } from "ink";
3
- import { colors } from "./theme";
4
- import { TabBar } from "./components/TabBar";
5
- import { SessionsScreen } from "./screens/SessionsScreen";
6
- import { ToolsScreen } from "./screens/ToolsScreen";
7
- import { OverviewScreen } from "./screens/OverviewScreen";
8
- import type { Workflow, ScreenId } from "../core/types";
9
- import { loadSessions, sessionExists } from "../data/sqlite";
10
- import { groupSessionsToWorkflows } from "../core/agents";
11
-
12
- interface AppProps {
13
- refreshInterval?: number;
14
- }
15
-
16
- function workflowsEqual(a: Workflow[], b: Workflow[]): boolean {
17
- if (a.length !== b.length) return false;
18
- for (let i = 0; i < a.length; i++) {
19
- if (a[i].id !== b[i].id) return false;
20
- if (a[i].mainSession.interactions.length !== b[i].mainSession.interactions.length) return false;
21
- if (a[i].subAgentSessions.length !== b[i].subAgentSessions.length) return false;
22
- }
23
- return true;
24
- }
25
-
26
- export function App({ refreshInterval = 2000 }: AppProps) {
27
- const { exit } = useApp();
28
- const { stdout } = useStdout();
29
- const terminalHeight = stdout?.rows ?? 24;
30
- const terminalWidth = stdout?.columns ?? 80;
31
-
32
- // Enter alternate screen buffer on mount — prevents Ink's clearTerminal flash.
33
- // Ink triggers \x1b[2J\x1b[3J\x1b[H whenever outputHeight >= stdout.rows.
34
- // Alt screen (\x1b[?1049h) moves rendering to a separate buffer so the main
35
- // screen is never touched, eliminating the black flash entirely.
36
- useEffect(() => {
37
- stdout?.write("\x1b[?1049h"); // enter alt screen
38
- stdout?.write("\x1b[?25l"); // hide cursor
39
- return () => {
40
- stdout?.write("\x1b[?25h"); // restore cursor
41
- stdout?.write("\x1b[?1049l"); // leave alt screen
42
- };
43
- }, [stdout]);
44
-
45
- const [workflows, setWorkflows] = useState<Workflow[]>([]);
46
- const [screen, setScreen] = useState<ScreenId>("sessions");
47
- const [error, setError] = useState<string | null>(null);
48
- const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
49
-
50
- const workflowsRef = useRef<Workflow[]>([]);
51
- const mountedRef = useRef(false);
52
-
53
- const loadData = useCallback(() => {
54
- if (!sessionExists()) {
55
- if (mountedRef.current) {
56
- setError("No OpenCode database found. Run OpenCode first.");
57
- }
58
- return;
59
- }
60
-
61
- try {
62
- const sessions = loadSessions();
63
- const grouped = groupSessionsToWorkflows(sessions);
64
-
65
- if (!workflowsEqual(workflowsRef.current, grouped)) {
66
- workflowsRef.current = grouped;
67
- if (mountedRef.current) {
68
- setWorkflows(grouped);
69
- setLastRefresh(new Date());
70
- }
71
- }
72
-
73
- if (mountedRef.current) {
74
- setError(null);
75
- }
76
- } catch (e) {
77
- if (mountedRef.current) {
78
- setError(e instanceof Error ? e.message : "Failed to load sessions");
79
- }
80
- }
81
- }, []);
82
-
83
- useEffect(() => {
84
- mountedRef.current = true;
85
- loadData();
86
- const interval = setInterval(loadData, refreshInterval);
87
- return () => {
88
- mountedRef.current = false;
89
- clearInterval(interval);
90
- };
91
- }, [loadData, refreshInterval]);
92
-
93
- // Global: q quit, r refresh, 1-3 tabs
94
- useInput((input, key) => {
95
- if (input === "q" || key.escape) {
96
- exit();
97
- return;
98
- }
99
- if (input === "r") {
100
- loadData();
101
- return;
102
- }
103
- if (input === "1") { setScreen("sessions"); return; }
104
- if (input === "2") { setScreen("tools"); return; }
105
- if (input === "3") { setScreen("overview"); return; }
106
- });
107
-
108
- if (error) {
109
- return (
110
- <Box flexDirection="column" padding={2}>
111
- <Text color={colors.error} bold>Error</Text>
112
- <Text color={colors.text}>{error}</Text>
113
- <Box marginTop={1}>
114
- <Text color={colors.textDim}>Press q to quit</Text>
115
- </Box>
116
- </Box>
117
- );
118
- }
119
-
120
- // Tab bar is 3 rows (border top + content + border bottom)
121
- const contentHeight = terminalHeight - 3;
122
-
123
- return (
124
- <Box flexDirection="column" width={terminalWidth} height={terminalHeight - 1}>
125
- <TabBar activeScreen={screen} lastRefresh={lastRefresh} />
126
- <Box width={terminalWidth} height={contentHeight}>
127
- {screen === "sessions" && (
128
- <SessionsScreen
129
- workflows={workflows}
130
- isActive={true}
131
- contentHeight={contentHeight}
132
- terminalWidth={terminalWidth}
133
- />
134
- )}
135
- {screen === "tools" && (
136
- <ToolsScreen
137
- workflows={workflows}
138
- isActive={true}
139
- contentHeight={contentHeight}
140
- terminalWidth={terminalWidth}
141
- />
142
- )}
143
- {screen === "overview" && (
144
- <OverviewScreen
145
- workflows={workflows}
146
- isActive={true}
147
- contentHeight={contentHeight}
148
- terminalWidth={terminalWidth}
149
- />
150
- )}
151
- </Box>
152
- </Box>
153
- );
154
- }
@@ -1,95 +0,0 @@
1
- import React, { memo } from "react";
2
- import { Box, Text } from "ink";
3
- import { colors } from "../theme";
4
- import type { AgentNode } from "../../core/types";
5
- import { getSessionTokens, getSessionCostSingle } from "../../core/session";
6
- import { getPricing } from "../../data/pricing";
7
-
8
- function formatTokens(n: number): string {
9
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
10
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
11
- return n.toString();
12
- }
13
-
14
- interface AgentNodeRowProps {
15
- node: AgentNode;
16
- isLast: boolean;
17
- prefix: string;
18
- }
19
-
20
- function AgentNodeRow({ node, isLast, prefix }: AgentNodeRowProps) {
21
- const { session } = node;
22
- const tokens = getSessionTokens(session);
23
- const pricing = getPricing(session.interactions[0]?.modelId ?? "");
24
- const cost = getSessionCostSingle(session, pricing);
25
- const agentName = session.interactions[0]?.agent ?? session.interactions[0]?.role ?? "main";
26
-
27
- const connector = isLast ? "└─ " : "├─ ";
28
- const childPrefix = prefix + (isLast ? " " : "│ ");
29
-
30
- return (
31
- <>
32
- <Box flexDirection="row">
33
- <Text color={colors.textDim}>{prefix}{connector}</Text>
34
- <Text color={colors.cyan}>[{agentName}]</Text>
35
- <Text color={colors.text}> {truncate(session.title ?? session.id.slice(0, 8), 20)}</Text>
36
- <Box flexGrow={1} />
37
- <Text color={colors.textDim}>{formatTokens(tokens.total)}</Text>
38
- <Text color={colors.textDim}> </Text>
39
- <Text color={colors.success}>${cost.toFixed(3)}</Text>
40
- </Box>
41
- {node.children.map((child, i) => (
42
- <AgentNodeRow
43
- key={child.session.id}
44
- node={child}
45
- isLast={i === node.children.length - 1}
46
- prefix={childPrefix}
47
- />
48
- ))}
49
- </>
50
- );
51
- }
52
-
53
- interface AgentChainGraphProps {
54
- agentTree: AgentNode;
55
- }
56
-
57
- function AgentChainGraphInner({ agentTree }: AgentChainGraphProps) {
58
- const { session } = agentTree;
59
- const tokens = getSessionTokens(session);
60
- const pricing = getPricing(session.interactions[0]?.modelId ?? "");
61
- const cost = getSessionCostSingle(session, pricing);
62
- const agentName = session.interactions[0]?.agent ?? "main";
63
-
64
- if (agentTree.children.length === 0) {
65
- return null;
66
- }
67
-
68
- return (
69
- <Box flexDirection="column">
70
- <Box flexDirection="row">
71
- <Text color={colors.cyan} bold>[{agentName}]</Text>
72
- <Text color={colors.text}> {truncate(session.title ?? "root", 20)}</Text>
73
- <Box flexGrow={1} />
74
- <Text color={colors.textDim}>{formatTokens(tokens.total)}</Text>
75
- <Text color={colors.textDim}> </Text>
76
- <Text color={colors.success}>${cost.toFixed(3)}</Text>
77
- </Box>
78
- {agentTree.children.map((child, i) => (
79
- <AgentNodeRow
80
- key={child.session.id}
81
- node={child}
82
- isLast={i === agentTree.children.length - 1}
83
- prefix=""
84
- />
85
- ))}
86
- </Box>
87
- );
88
- }
89
-
90
- export const AgentChainGraph = memo(AgentChainGraphInner);
91
-
92
- function truncate(s: string, max: number): string {
93
- if (s.length <= max) return s;
94
- return s.slice(0, max - 1) + "…";
95
- }
@@ -1,101 +0,0 @@
1
- import React, { memo } from "react";
2
- import { Box, Text } from "ink";
3
- import { colors } from "../theme";
4
- import type { Workflow, FlatNode } from "../../core/types";
5
- import { getSessionTokens, getSessionCostSingle } from "../../core/session";
6
- import { getPricing } from "../../data/pricing";
7
- import Decimal from "decimal.js";
8
-
9
- interface AgentTreeProps {
10
- workflows: Workflow[];
11
- selectedId: string | null;
12
- flatNodes: FlatNode[];
13
- onSelect: (id: string) => void;
14
- maxHeight?: 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 formatCost(d: Decimal): string {
24
- if (d.lessThan(0.01)) return `$${d.toFixed(4)}`;
25
- return `$${d.toFixed(2)}`;
26
- }
27
-
28
- function AgentTreeInner({ workflows, selectedId, flatNodes, maxHeight = 20 }: AgentTreeProps) {
29
- const headerHeight = 2;
30
- const visibleCount = Math.max(1, maxHeight - headerHeight);
31
- const selectedIndex = flatNodes.findIndex((n) => n.id === selectedId);
32
-
33
- // Keep selected item visible with some context above
34
- let startIndex = 0;
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));
39
- }
40
- const visibleNodes = flatNodes.slice(startIndex, startIndex + visibleCount);
41
-
42
- return (
43
- <Box flexDirection="column" paddingX={1} height={maxHeight} overflow="hidden">
44
- <Box marginBottom={1} flexDirection="row">
45
- <Text color={colors.accent} bold>Sessions</Text>
46
- <Box flexGrow={1} />
47
- <Text color={colors.textDim}>{workflows.length}</Text>
48
- </Box>
49
-
50
- {visibleNodes.map((node) => {
51
- const isSelected = node.id === selectedId;
52
- const tokens = getSessionTokens(node.session);
53
- const pricing = getPricing(node.session.interactions[0]?.modelId ?? "");
54
- const cost = getSessionCostSingle(node.session, pricing);
55
- const agentName = node.session.interactions[0]?.agent ?? null;
56
-
57
- const indent = " ".repeat(node.depth);
58
- const prefix = node.depth === 0
59
- ? (node.hasChildren ? "▶ " : "● ")
60
- : (node.hasChildren ? "├▶ " : "├─ ");
61
-
62
- const label = node.depth === 0
63
- ? truncate(node.session.title ?? node.session.projectName ?? "Untitled", 22)
64
- : truncate(`[${agentName ?? "?"}] ${node.session.title ?? ""}`, 20);
65
-
66
- return (
67
- <Box key={node.id} flexDirection="row">
68
- <Box width={1} />
69
- <Text
70
- color={isSelected ? undefined : colors.text}
71
- backgroundColor={isSelected ? colors.accent : undefined}
72
- bold={isSelected}
73
- >
74
- {indent}{prefix}{label}
75
- </Text>
76
- <Box flexGrow={1} />
77
- <Text color={colors.textDim} dimColor>{formatTokens(tokens.total)}</Text>
78
- <Box width={1} />
79
- <Text color={colors.success}>{formatCost(cost)}</Text>
80
- <Box width={1} />
81
- </Box>
82
- );
83
- })}
84
-
85
- {flatNodes.length > visibleCount && (
86
- <Text color={colors.textDim}>
87
- {startIndex > 0 ? "↑ " : " "}
88
- {startIndex + visibleCount}/{flatNodes.length}
89
- {startIndex + visibleCount < flatNodes.length ? " ↓" : " "}
90
- </Text>
91
- )}
92
- </Box>
93
- );
94
- }
95
-
96
- export const AgentTree = memo(AgentTreeInner);
97
-
98
- function truncate(label: string, maxLen: number): string {
99
- if (label.length <= maxLen) return label;
100
- return label.slice(0, maxLen - 1) + "…";
101
- }