opencode-top 3.0.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/LICENSE +21 -0
- package/README.md +35 -0
- package/bin/octop.js +13 -0
- package/bin/octop.mjs +13 -0
- package/package.json +48 -0
- package/src/cli.ts +60 -0
- package/src/core/agents.ts +78 -0
- package/src/core/session.ts +315 -0
- package/src/core/types.ts +156 -0
- package/src/data/pricing.ts +82 -0
- package/src/data/sqlite.ts +347 -0
- package/src/index.ts +6 -0
- package/src/ui/App.tsx +141 -0
- package/src/ui/components/AgentChainGraph.tsx +95 -0
- package/src/ui/components/AgentTree.tsx +98 -0
- package/src/ui/components/DetailsPanel.tsx +210 -0
- package/src/ui/components/MessagesPanel.tsx +188 -0
- package/src/ui/components/SparkLine.tsx +18 -0
- package/src/ui/components/StatusBar.tsx +24 -0
- package/src/ui/components/TabBar.tsx +42 -0
- package/src/ui/screens/OverviewScreen.tsx +327 -0
- package/src/ui/screens/SessionsScreen.tsx +191 -0
- package/src/ui/screens/TimelineScreen.tsx +222 -0
- package/src/ui/screens/ToolsScreen.tsx +260 -0
- package/src/ui/theme.ts +21 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import Decimal from "decimal.js";
|
|
2
|
+
|
|
3
|
+
export class TokenUsage {
|
|
4
|
+
constructor(
|
|
5
|
+
readonly input: number = 0,
|
|
6
|
+
readonly output: number = 0,
|
|
7
|
+
readonly cacheRead: number = 0,
|
|
8
|
+
readonly cacheWrite: number = 0,
|
|
9
|
+
readonly reasoning: number = 0
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
get total(): number {
|
|
13
|
+
return this.input + this.output + this.cacheRead + this.cacheWrite;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
add(other: TokenUsage): TokenUsage {
|
|
17
|
+
return new TokenUsage(
|
|
18
|
+
this.input + other.input,
|
|
19
|
+
this.output + other.output,
|
|
20
|
+
this.cacheRead + other.cacheRead,
|
|
21
|
+
this.cacheWrite + other.cacheWrite,
|
|
22
|
+
this.reasoning + other.reasoning
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
calculateCost(pricing: ModelPricing): Decimal {
|
|
27
|
+
const inputCost = new Decimal(this.input).mul(pricing.input).div(1_000_000);
|
|
28
|
+
const outputCost = new Decimal(this.output).mul(pricing.output).div(1_000_000);
|
|
29
|
+
const cacheReadCost = new Decimal(this.cacheRead).mul(pricing.cacheRead).div(1_000_000);
|
|
30
|
+
const cacheWriteCost = new Decimal(this.cacheWrite).mul(pricing.cacheWrite).div(1_000_000);
|
|
31
|
+
return inputCost.plus(outputCost).plus(cacheReadCost).plus(cacheWriteCost);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TimeData {
|
|
36
|
+
created: number | null;
|
|
37
|
+
completed: number | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Part types from the `part` table
|
|
41
|
+
export type MessagePart =
|
|
42
|
+
| {
|
|
43
|
+
type: "text";
|
|
44
|
+
text: string;
|
|
45
|
+
timeStart: number;
|
|
46
|
+
timeEnd: number;
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
type: "tool";
|
|
50
|
+
callId: string;
|
|
51
|
+
toolName: string;
|
|
52
|
+
status: "completed" | "pending" | "error";
|
|
53
|
+
input: Record<string, unknown>;
|
|
54
|
+
output: string;
|
|
55
|
+
title: string | null;
|
|
56
|
+
exitCode: number | null;
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
timeStart: number;
|
|
59
|
+
timeEnd: number;
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
type: "reasoning";
|
|
63
|
+
text: string;
|
|
64
|
+
timeStart: number;
|
|
65
|
+
timeEnd: number;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
type: "patch";
|
|
69
|
+
hash: string;
|
|
70
|
+
files: string[];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export interface Interaction {
|
|
74
|
+
id: string;
|
|
75
|
+
sessionId: string;
|
|
76
|
+
modelId: string;
|
|
77
|
+
providerId: string | null;
|
|
78
|
+
role: "assistant" | "user";
|
|
79
|
+
tokens: TokenUsage;
|
|
80
|
+
time: TimeData;
|
|
81
|
+
agent: string | null;
|
|
82
|
+
finishReason: string | null;
|
|
83
|
+
outputRate: number;
|
|
84
|
+
parts: MessagePart[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface Session {
|
|
88
|
+
id: string;
|
|
89
|
+
parentId: string | null;
|
|
90
|
+
projectId: string | null;
|
|
91
|
+
projectName: string | null;
|
|
92
|
+
title: string | null;
|
|
93
|
+
timeCreated: number | null;
|
|
94
|
+
timeArchived: number | null;
|
|
95
|
+
interactions: Interaction[];
|
|
96
|
+
source: "sqlite" | "files";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface AgentNode {
|
|
100
|
+
session: Session;
|
|
101
|
+
children: AgentNode[];
|
|
102
|
+
depth: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface FlatNode {
|
|
106
|
+
id: string;
|
|
107
|
+
session: Session;
|
|
108
|
+
workflowIndex: number;
|
|
109
|
+
depth: number;
|
|
110
|
+
hasChildren: boolean;
|
|
111
|
+
agentNode: AgentNode;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface Workflow {
|
|
115
|
+
id: string;
|
|
116
|
+
mainSession: Session;
|
|
117
|
+
subAgentSessions: Session[];
|
|
118
|
+
agentTree: AgentNode;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ModelPricing {
|
|
122
|
+
input: Decimal;
|
|
123
|
+
output: Decimal;
|
|
124
|
+
cacheRead: Decimal;
|
|
125
|
+
cacheWrite: Decimal;
|
|
126
|
+
contextWindow: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface ToolUsage {
|
|
130
|
+
name: string;
|
|
131
|
+
calls: number;
|
|
132
|
+
successes: number;
|
|
133
|
+
failures: number;
|
|
134
|
+
totalDurationMs: number;
|
|
135
|
+
avgDurationMs: number;
|
|
136
|
+
recentErrors: string[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface OverviewStats {
|
|
140
|
+
totalCost: Decimal;
|
|
141
|
+
totalTokens: TokenUsage;
|
|
142
|
+
modelBreakdown: Map<string, { cost: Decimal; tokens: number; calls: number }>;
|
|
143
|
+
projectBreakdown: Map<string, { cost: Decimal; sessions: number }>;
|
|
144
|
+
agentBreakdown: Map<string, { cost: Decimal; calls: number }>;
|
|
145
|
+
agentToolErrors: Map<string, { calls: number; errors: number }>;
|
|
146
|
+
toolCallCounts: Map<string, { calls: number; errors: number; totalDurationMs: number }>;
|
|
147
|
+
// 7-day daily data
|
|
148
|
+
weeklyTokens: { date: string; tokens: number }[];
|
|
149
|
+
weeklySessions: { date: string; sessions: number }[];
|
|
150
|
+
// 24-hour activity pattern (interactions per hour, all-time)
|
|
151
|
+
hourlyActivity: number[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type ScreenId = "sessions" | "tools" | "overview";
|
|
155
|
+
|
|
156
|
+
export type { Decimal };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import Decimal from "decimal.js";
|
|
2
|
+
import type { ModelPricing } from "../core/types";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PRICING: ModelPricing = {
|
|
5
|
+
input: new Decimal(0),
|
|
6
|
+
output: new Decimal(0),
|
|
7
|
+
cacheRead: new Decimal(0),
|
|
8
|
+
cacheWrite: new Decimal(0),
|
|
9
|
+
contextWindow: 128000,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const KNOWN_PRICING: Record<string, Partial<ModelPricing>> = {
|
|
13
|
+
"claude-sonnet-4-20250514": {
|
|
14
|
+
input: new Decimal(3),
|
|
15
|
+
output: new Decimal(15),
|
|
16
|
+
cacheRead: new Decimal(0.3),
|
|
17
|
+
cacheWrite: new Decimal(3.75),
|
|
18
|
+
contextWindow: 200000,
|
|
19
|
+
},
|
|
20
|
+
"claude-3-5-sonnet-20241022": {
|
|
21
|
+
input: new Decimal(3),
|
|
22
|
+
output: new Decimal(15),
|
|
23
|
+
cacheRead: new Decimal(0.3),
|
|
24
|
+
cacheWrite: new Decimal(3.75),
|
|
25
|
+
contextWindow: 200000,
|
|
26
|
+
},
|
|
27
|
+
"claude-3-5-sonnet-20240620": {
|
|
28
|
+
input: new Decimal(3),
|
|
29
|
+
output: new Decimal(15),
|
|
30
|
+
cacheRead: new Decimal(0.3),
|
|
31
|
+
cacheWrite: new Decimal(3.75),
|
|
32
|
+
contextWindow: 200000,
|
|
33
|
+
},
|
|
34
|
+
"claude-3-5-haiku-20241022": {
|
|
35
|
+
input: new Decimal(1),
|
|
36
|
+
output: new Decimal(5),
|
|
37
|
+
cacheRead: new Decimal(0.1),
|
|
38
|
+
cacheWrite: new Decimal(1.25),
|
|
39
|
+
contextWindow: 200000,
|
|
40
|
+
},
|
|
41
|
+
"claude-3-haiku-20240307": {
|
|
42
|
+
input: new Decimal(0.25),
|
|
43
|
+
output: new Decimal(1.25),
|
|
44
|
+
cacheRead: new Decimal(0.03),
|
|
45
|
+
cacheWrite: new Decimal(0.3),
|
|
46
|
+
contextWindow: 200000,
|
|
47
|
+
},
|
|
48
|
+
"claude-3-opus-20240229": {
|
|
49
|
+
input: new Decimal(15),
|
|
50
|
+
output: new Decimal(75),
|
|
51
|
+
cacheRead: new Decimal(1.5),
|
|
52
|
+
cacheWrite: new Decimal(18.75),
|
|
53
|
+
contextWindow: 200000,
|
|
54
|
+
},
|
|
55
|
+
"claude-opus-4-20250514": {
|
|
56
|
+
input: new Decimal(15),
|
|
57
|
+
output: new Decimal(75),
|
|
58
|
+
cacheRead: new Decimal(1.5),
|
|
59
|
+
cacheWrite: new Decimal(18.75),
|
|
60
|
+
contextWindow: 200000,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function getPricing(modelId: string): ModelPricing {
|
|
65
|
+
const normalized = modelId.toLowerCase();
|
|
66
|
+
|
|
67
|
+
for (const [key, pricing] of Object.entries(KNOWN_PRICING)) {
|
|
68
|
+
if (normalized.includes(key.toLowerCase()) || key.toLowerCase().includes(normalized)) {
|
|
69
|
+
return { ...DEFAULT_PRICING, ...pricing };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return DEFAULT_PRICING;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getAllPricing(): Map<string, ModelPricing> {
|
|
77
|
+
const result = new Map<string, ModelPricing>();
|
|
78
|
+
for (const [key, pricing] of Object.entries(KNOWN_PRICING)) {
|
|
79
|
+
result.set(key.toLowerCase(), { ...DEFAULT_PRICING, ...pricing });
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
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
|
+
}
|