@rejacky/opencode-insights 0.1.4 → 0.1.5
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/README.md +57 -43
- package/dist/{capture-DkASdFpu.d.ts → capture-B6sM1QQA.d.ts} +17 -8
- package/dist/{chunk-Q5ROKOOJ.js → chunk-PBX4AJCR.js} +141 -47
- package/dist/cli.d.ts +7 -1
- package/dist/cli.js +606 -145
- package/dist/index.d.ts +1 -2
- package/dist/index.js +64 -14
- package/package.json +7 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
openDatabase,
|
|
3
4
|
resolveCapturePath
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-PBX4AJCR.js";
|
|
5
6
|
|
|
6
7
|
// src/cli.ts
|
|
7
8
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -48,12 +49,55 @@ async function readRecentCaptures(options = {}) {
|
|
|
48
49
|
const records = parseJsonlRecords(await readFile(jsonlPath, "utf8"));
|
|
49
50
|
return records.slice(-Math.max(1, options.limit ?? 20)).reverse();
|
|
50
51
|
}
|
|
52
|
+
async function readViewerCaptures(options = {}) {
|
|
53
|
+
const dbPath = resolveCapturePath(options);
|
|
54
|
+
const sqliteRecords = await readSqliteViewerCaptures(dbPath, options.limit ?? 5e3);
|
|
55
|
+
if (sqliteRecords) return sqliteRecords;
|
|
56
|
+
const jsonlPath = dbPath.endsWith(".sqlite") ? `${dbPath}.jsonl` : dbPath;
|
|
57
|
+
if (!existsSync(jsonlPath)) return [];
|
|
58
|
+
const records = parseJsonlRecords(await readFile(jsonlPath, "utf8"));
|
|
59
|
+
return records.filter((record) => isViewerCaptureKind(record.kind)).slice(-Math.max(1, options.limit ?? 5e3)).reverse();
|
|
60
|
+
}
|
|
61
|
+
async function readCaptureRecord(id, options = {}) {
|
|
62
|
+
const dbPath = resolveCapturePath(options);
|
|
63
|
+
if (!existsSync(dbPath)) return void 0;
|
|
64
|
+
try {
|
|
65
|
+
const db = await openDatabase(dbPath);
|
|
66
|
+
if (db) {
|
|
67
|
+
try {
|
|
68
|
+
const rows = db.all("select id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json from captures where id = ?", id);
|
|
69
|
+
const row = rows[0];
|
|
70
|
+
if (!row) return void 0;
|
|
71
|
+
return rowToCapture(row);
|
|
72
|
+
} finally {
|
|
73
|
+
db.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const escapedId = id.replace(/'/g, "'\\''");
|
|
80
|
+
const { stdout } = await execFileAsync("sqlite3", [
|
|
81
|
+
"-json",
|
|
82
|
+
dbPath,
|
|
83
|
+
`select id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json from captures where id = '${escapedId}'`
|
|
84
|
+
], { maxBuffer: 128 * 1024 * 1024 });
|
|
85
|
+
if (!stdout.trim()) return void 0;
|
|
86
|
+
const rows = JSON.parse(stdout);
|
|
87
|
+
const row = rows[0];
|
|
88
|
+
if (!row) return void 0;
|
|
89
|
+
return rowToCapture(row);
|
|
90
|
+
} catch {
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
51
94
|
function buildRequestHistory(records) {
|
|
52
95
|
const sessions = /* @__PURE__ */ new Map();
|
|
53
96
|
const messages = /* @__PURE__ */ new Map();
|
|
54
97
|
const responses = /* @__PURE__ */ new Map();
|
|
55
|
-
const
|
|
98
|
+
const responsesByParent = /* @__PURE__ */ new Map();
|
|
56
99
|
const requests = [];
|
|
100
|
+
const pendingSystemTransforms = [];
|
|
57
101
|
const getSession = (sessionID) => {
|
|
58
102
|
const existing = sessions.get(sessionID);
|
|
59
103
|
if (existing) return existing;
|
|
@@ -84,7 +128,27 @@ function buildRequestHistory(records) {
|
|
|
84
128
|
responses.set(key, created);
|
|
85
129
|
return created;
|
|
86
130
|
};
|
|
87
|
-
|
|
131
|
+
const addRequest = (request) => {
|
|
132
|
+
requests.push(request);
|
|
133
|
+
if (request.sessionID) {
|
|
134
|
+
getSession(request.sessionID).requests.push(request);
|
|
135
|
+
if (request.messageID) getMessage(request.sessionID, request.messageID, "user").requests.push(request);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const addResponseByParent = (response) => {
|
|
139
|
+
if (!response.parentID) return;
|
|
140
|
+
const key = `${response.sessionID}:${response.parentID}`;
|
|
141
|
+
const parentResponses = responsesByParent.get(key) ?? [];
|
|
142
|
+
if (!parentResponses.includes(response)) parentResponses.push(response);
|
|
143
|
+
responsesByParent.set(key, parentResponses);
|
|
144
|
+
};
|
|
145
|
+
const updateSessionPath = (session, path) => {
|
|
146
|
+
if (!isRecord(path)) return;
|
|
147
|
+
session.cwd = optionalString(path.cwd) ?? session.cwd;
|
|
148
|
+
session.root = optionalString(path.root) ?? session.root;
|
|
149
|
+
session.project = projectName(session.root) ?? projectName(session.cwd) ?? session.project;
|
|
150
|
+
};
|
|
151
|
+
for (const record of records.slice().sort((a, b) => a.timestamp - b.timestamp || a.id.localeCompare(b.id))) {
|
|
88
152
|
if (record.sessionID) getSession(record.sessionID);
|
|
89
153
|
if (record.kind === "chat.message") {
|
|
90
154
|
const message = historyMessageFromChatMessage(record);
|
|
@@ -99,10 +163,11 @@ function buildRequestHistory(records) {
|
|
|
99
163
|
if (!request.messageID && request.sessionID) {
|
|
100
164
|
request.messageID = latestUserMessageBefore(messages, request.sessionID, request.timestamp)?.id;
|
|
101
165
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
166
|
+
if (record.kind === "experimental.chat.system.transform") {
|
|
167
|
+
pendingSystemTransforms.push(request);
|
|
168
|
+
} else {
|
|
169
|
+
attachPendingSystemTransform(pendingSystemTransforms, request);
|
|
170
|
+
addRequest(request);
|
|
106
171
|
}
|
|
107
172
|
}
|
|
108
173
|
if (record.kind === "chat.headers") {
|
|
@@ -118,8 +183,10 @@ function buildRequestHistory(records) {
|
|
|
118
183
|
const sessionID = optionalString(info.id) ?? optionalString(properties.sessionID);
|
|
119
184
|
if (!sessionID) continue;
|
|
120
185
|
const session = getSession(sessionID);
|
|
186
|
+
session.parentID = optionalString(info.parentID) ?? session.parentID;
|
|
121
187
|
session.title = optionalString(info.title) ?? session.title;
|
|
122
188
|
session.updatedAt = numberFromPath(info.time, "updated") ?? session.updatedAt;
|
|
189
|
+
updateSessionPath(session, info.path);
|
|
123
190
|
continue;
|
|
124
191
|
}
|
|
125
192
|
if (type === "message.updated") {
|
|
@@ -128,6 +195,7 @@ function buildRequestHistory(records) {
|
|
|
128
195
|
const messageID = optionalString(info.id);
|
|
129
196
|
if (!sessionID || !messageID) continue;
|
|
130
197
|
const role = optionalString(info.role) ?? "unknown";
|
|
198
|
+
updateSessionPath(getSession(sessionID), info.path);
|
|
131
199
|
if (role === "assistant") {
|
|
132
200
|
const response = getResponse(sessionID, messageID, role);
|
|
133
201
|
response.createdAt = numberFromPath(info.time, "created") ?? response.createdAt;
|
|
@@ -137,7 +205,7 @@ function buildRequestHistory(records) {
|
|
|
137
205
|
response.cost = typeof info.cost === "number" ? info.cost : response.cost;
|
|
138
206
|
response.finish = optionalString(info.finish) ?? response.finish;
|
|
139
207
|
response.events.push(record.payload);
|
|
140
|
-
|
|
208
|
+
addResponseByParent(response);
|
|
141
209
|
continue;
|
|
142
210
|
}
|
|
143
211
|
const message = getMessage(sessionID, messageID, role);
|
|
@@ -169,11 +237,18 @@ function buildRequestHistory(records) {
|
|
|
169
237
|
message.text = text;
|
|
170
238
|
}
|
|
171
239
|
}
|
|
240
|
+
for (const request of pendingSystemTransforms) addRequest(request);
|
|
172
241
|
for (const session of sessions.values()) {
|
|
173
242
|
for (const message of session.messages) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
243
|
+
const messageResponses = (responsesByParent.get(`${message.sessionID}:${message.id}`) ?? []).sort(
|
|
244
|
+
(a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0)
|
|
245
|
+
);
|
|
246
|
+
message.response = messageResponses.at(-1);
|
|
247
|
+
let responseIndex = 0;
|
|
248
|
+
for (const request of message.requests.slice().sort((a, b) => a.timestamp - b.timestamp)) {
|
|
249
|
+
if (!requestShouldOwnAssistantResponse(request)) continue;
|
|
250
|
+
request.response = messageResponses[responseIndex] ?? message.response;
|
|
251
|
+
if (responseIndex < messageResponses.length) responseIndex += 1;
|
|
177
252
|
}
|
|
178
253
|
}
|
|
179
254
|
session.messages.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
|
@@ -187,11 +262,10 @@ function buildRequestHistory(records) {
|
|
|
187
262
|
async function readSqliteCaptures(path, limit) {
|
|
188
263
|
if (!existsSync(path)) return void 0;
|
|
189
264
|
try {
|
|
190
|
-
const
|
|
191
|
-
if (!
|
|
192
|
-
const db = new mod.Database(path, { readonly: true });
|
|
265
|
+
const db = await openDatabase(path);
|
|
266
|
+
if (!db) return readSqliteCapturesWithCli(path, limit);
|
|
193
267
|
try {
|
|
194
|
-
const rows = db.
|
|
268
|
+
const rows = db.all(recentCaptureSql(Math.max(1, limit)));
|
|
195
269
|
return dedupeRows(rows).map(rowToCapture);
|
|
196
270
|
} finally {
|
|
197
271
|
db.close();
|
|
@@ -212,18 +286,71 @@ async function readSqliteCapturesWithCli(path, limit) {
|
|
|
212
286
|
return void 0;
|
|
213
287
|
}
|
|
214
288
|
}
|
|
289
|
+
async function readSqliteViewerCaptures(path, limit) {
|
|
290
|
+
if (!existsSync(path)) return void 0;
|
|
291
|
+
try {
|
|
292
|
+
const db = await openDatabase(path);
|
|
293
|
+
if (!db) return readSqliteViewerCapturesWithCli(path, limit);
|
|
294
|
+
try {
|
|
295
|
+
const rows = db.all(viewerCaptureSql(Math.max(1, limit)));
|
|
296
|
+
return dedupeRows(rows).map(rowToCapture);
|
|
297
|
+
} finally {
|
|
298
|
+
db.close();
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
return readSqliteViewerCapturesWithCli(path, limit);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function readSqliteViewerCapturesWithCli(path, limit) {
|
|
305
|
+
if (!existsSync(path)) return void 0;
|
|
306
|
+
try {
|
|
307
|
+
const { stdout } = await execFileAsync("sqlite3", ["-json", path, viewerCaptureSql(Math.max(1, Math.trunc(limit)))], {
|
|
308
|
+
maxBuffer: 128 * 1024 * 1024
|
|
309
|
+
});
|
|
310
|
+
if (!stdout.trim()) return [];
|
|
311
|
+
return dedupeRows(JSON.parse(stdout)).map(rowToCapture);
|
|
312
|
+
} catch {
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
215
316
|
function recentCaptureSql(limit) {
|
|
216
317
|
return `select id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
|
|
217
318
|
from captures
|
|
218
319
|
where id in (
|
|
219
|
-
select id from captures
|
|
320
|
+
select id from captures
|
|
321
|
+
where kind in (
|
|
322
|
+
'chat.params',
|
|
323
|
+
'chat.message',
|
|
324
|
+
'chat.headers',
|
|
325
|
+
'experimental.chat.messages.transform',
|
|
326
|
+
'experimental.chat.system.transform'
|
|
327
|
+
)
|
|
328
|
+
order by timestamp desc
|
|
329
|
+
limit ${limit}
|
|
220
330
|
)
|
|
221
331
|
or id in (
|
|
332
|
+
select id from captures
|
|
333
|
+
where kind = 'event'
|
|
334
|
+
and json_extract(payload_json, '$.event.type') in (
|
|
335
|
+
'message.updated',
|
|
336
|
+
'message.part.updated',
|
|
337
|
+
'message.part.delta',
|
|
338
|
+
'session.updated',
|
|
339
|
+
'session.created'
|
|
340
|
+
)
|
|
341
|
+
order by timestamp desc
|
|
342
|
+
limit ${limit}
|
|
343
|
+
)
|
|
344
|
+
order by timestamp desc`;
|
|
345
|
+
}
|
|
346
|
+
function viewerCaptureSql(limit) {
|
|
347
|
+
return `select id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
|
|
348
|
+
from captures
|
|
349
|
+
where id in (
|
|
222
350
|
select id from captures
|
|
223
351
|
where kind in (
|
|
224
352
|
'chat.params',
|
|
225
353
|
'chat.message',
|
|
226
|
-
'experimental.chat.messages.transform',
|
|
227
354
|
'experimental.chat.system.transform'
|
|
228
355
|
)
|
|
229
356
|
order by timestamp desc
|
|
@@ -232,12 +359,21 @@ function recentCaptureSql(limit) {
|
|
|
232
359
|
or id in (
|
|
233
360
|
select id from captures
|
|
234
361
|
where kind = 'event'
|
|
235
|
-
and json_extract(payload_json, '$.event.type') in (
|
|
362
|
+
and json_extract(payload_json, '$.event.type') in (
|
|
363
|
+
'message.updated',
|
|
364
|
+
'message.part.updated',
|
|
365
|
+
'message.part.delta',
|
|
366
|
+
'session.updated',
|
|
367
|
+
'session.created'
|
|
368
|
+
)
|
|
236
369
|
order by timestamp desc
|
|
237
370
|
limit ${limit}
|
|
238
371
|
)
|
|
239
372
|
order by timestamp desc`;
|
|
240
373
|
}
|
|
374
|
+
function isViewerCaptureKind(kind) {
|
|
375
|
+
return kind === "chat.params" || kind === "chat.message" || kind === "experimental.chat.system.transform" || kind === "event";
|
|
376
|
+
}
|
|
241
377
|
function dedupeRows(rows) {
|
|
242
378
|
const seen = /* @__PURE__ */ new Set();
|
|
243
379
|
return rows.filter((row) => {
|
|
@@ -297,18 +433,21 @@ function agentFromCapture(record, input) {
|
|
|
297
433
|
return optionalString(input.agent);
|
|
298
434
|
}
|
|
299
435
|
function messageIDForCapture(record, input) {
|
|
300
|
-
if (record.kind === "experimental.chat.messages.transform") {
|
|
301
|
-
return latestUserMessageIDFromTransform(record.payload.output) ?? record.messageID;
|
|
302
|
-
}
|
|
303
436
|
return record.messageID ?? messageIDFromPayload(input.message);
|
|
304
437
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
for (
|
|
308
|
-
|
|
309
|
-
if (
|
|
438
|
+
function attachPendingSystemTransform(pendingSystemTransforms, request) {
|
|
439
|
+
let index = -1;
|
|
440
|
+
for (let candidateIndex = pendingSystemTransforms.length - 1; candidateIndex >= 0; candidateIndex -= 1) {
|
|
441
|
+
const candidate = pendingSystemTransforms[candidateIndex];
|
|
442
|
+
if (candidate && candidate.sessionID === request.sessionID && candidate.providerID === request.providerID && candidate.modelID === request.modelID && candidate.timestamp <= request.timestamp && request.timestamp - candidate.timestamp <= 5e3) {
|
|
443
|
+
index = candidateIndex;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
310
446
|
}
|
|
311
|
-
|
|
447
|
+
if (index < 0) return;
|
|
448
|
+
const [system] = pendingSystemTransforms.splice(index, 1);
|
|
449
|
+
if (!system) return;
|
|
450
|
+
request.system = { id: system.id, timestamp: system.timestamp, payload: system.payload };
|
|
312
451
|
}
|
|
313
452
|
function latestUserMessageBefore(messages, sessionID, timestamp) {
|
|
314
453
|
let latest;
|
|
@@ -373,6 +512,12 @@ function findFirstString(value, keys) {
|
|
|
373
512
|
}
|
|
374
513
|
return void 0;
|
|
375
514
|
}
|
|
515
|
+
function projectName(path) {
|
|
516
|
+
if (!path) return void 0;
|
|
517
|
+
const normalized = path.replace(/\/+$/, "");
|
|
518
|
+
if (!normalized) return void 0;
|
|
519
|
+
return normalized.split("/").filter(Boolean).at(-1) ?? normalized;
|
|
520
|
+
}
|
|
376
521
|
function numberFromPath(value, key) {
|
|
377
522
|
if (!isRecord(value)) return void 0;
|
|
378
523
|
const item = value[key];
|
|
@@ -381,6 +526,7 @@ function numberFromPath(value, key) {
|
|
|
381
526
|
|
|
382
527
|
// src/viewer.ts
|
|
383
528
|
import { createServer } from "http";
|
|
529
|
+
var REQUEST_PATH_RE = /^\/api\/request\/(.+)$/;
|
|
384
530
|
async function serveViewer(options = {}) {
|
|
385
531
|
const host = options.host ?? "127.0.0.1";
|
|
386
532
|
const port = options.port ?? 8765;
|
|
@@ -391,6 +537,21 @@ async function serveViewer(options = {}) {
|
|
|
391
537
|
sendJson(response, history);
|
|
392
538
|
return;
|
|
393
539
|
}
|
|
540
|
+
const requestMatch = REQUEST_PATH_RE.exec(url.pathname);
|
|
541
|
+
if (requestMatch) {
|
|
542
|
+
const requestId = requestMatch[1];
|
|
543
|
+
if (!requestId) {
|
|
544
|
+
sendJson(response, { error: "not found" });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const record = await readCaptureRecord(requestId, options);
|
|
548
|
+
if (!record) {
|
|
549
|
+
sendJson(response, { error: "not found" });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
sendJson(response, record.payload);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
394
555
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
395
556
|
sendHtml(response, renderViewerHtml(resolveCapturePath(options)));
|
|
396
557
|
return;
|
|
@@ -416,8 +577,152 @@ async function readHistory(options = {}) {
|
|
|
416
577
|
dbPath: options.dbPath,
|
|
417
578
|
limit: options.limit ?? 5e3
|
|
418
579
|
};
|
|
419
|
-
const records = await
|
|
420
|
-
|
|
580
|
+
const records = await readViewerCaptures(readOptions);
|
|
581
|
+
const history = buildRequestHistory(records);
|
|
582
|
+
prepareHistoryForViewer(history);
|
|
583
|
+
stripPayloadsForViewer(history);
|
|
584
|
+
return history;
|
|
585
|
+
}
|
|
586
|
+
function prepareHistoryForViewer(history) {
|
|
587
|
+
for (const session of history.sessions) {
|
|
588
|
+
for (const message of session.messages) {
|
|
589
|
+
const viewerMessage = message;
|
|
590
|
+
viewerMessage.visibleSteps = buildViewerVisibleSteps(message);
|
|
591
|
+
viewerMessage.hiddenContexts = buildViewerHiddenContexts(message);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function stripPayloadsForViewer(history) {
|
|
596
|
+
history.requests = history.requests.map(viewerRequestSummary);
|
|
597
|
+
for (const session of history.sessions) {
|
|
598
|
+
session.requests = [];
|
|
599
|
+
for (const message of session.messages) {
|
|
600
|
+
message.requests = [];
|
|
601
|
+
message.response = void 0;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function buildViewerVisibleSteps(message) {
|
|
606
|
+
const steps = [];
|
|
607
|
+
for (const request of (message.requests || []).filter((item) => item.agent !== "title" && item.agent !== "messages.transform")) {
|
|
608
|
+
const label = [request.agent || "agent", request.providerID, request.modelID].filter(Boolean).join(" \xB7 ");
|
|
609
|
+
const response = request.response;
|
|
610
|
+
const reasoning = response?.reasoning;
|
|
611
|
+
const text = response?.text;
|
|
612
|
+
if (reasoning) {
|
|
613
|
+
steps.push({ label: `${label} thinking`, text: reasoning });
|
|
614
|
+
}
|
|
615
|
+
for (const tool of viewerToolSteps(response)) {
|
|
616
|
+
steps.push({ label: `${label} tool`, text: tool });
|
|
617
|
+
}
|
|
618
|
+
if (text && normalizeDisplayText(text) !== normalizeDisplayText(reasoning)) {
|
|
619
|
+
steps.push({ label: `${label} response`, text });
|
|
620
|
+
}
|
|
621
|
+
if (!reasoning && !text && viewerToolSteps(response).length === 0) {
|
|
622
|
+
steps.push({ label, text: "Model step captured, but no visible thinking or response text was recorded." });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return steps;
|
|
626
|
+
}
|
|
627
|
+
function buildViewerHiddenContexts(message) {
|
|
628
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
629
|
+
for (const request of message.requests || []) {
|
|
630
|
+
const step = [request.agent || "agent", request.providerID, request.modelID].filter(Boolean).join(" \xB7 ");
|
|
631
|
+
if (request.system?.payload?.output) {
|
|
632
|
+
addHiddenContext(contexts, "System Transform Output", step, request.system.payload.output);
|
|
633
|
+
}
|
|
634
|
+
if (request.agent === "messages.transform" && request.payload) {
|
|
635
|
+
addHiddenContext(contexts, "Messages Transform Output", step, request.payload.output || request.payload);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return [...contexts.values()];
|
|
639
|
+
}
|
|
640
|
+
function addHiddenContext(contexts, title, step, value) {
|
|
641
|
+
const text = hiddenContextText(value);
|
|
642
|
+
if (!text) return;
|
|
643
|
+
const key = `${title}:${normalizeDisplayText(text)}`;
|
|
644
|
+
const existing = contexts.get(key);
|
|
645
|
+
if (existing) {
|
|
646
|
+
existing.count += 1;
|
|
647
|
+
if (!existing.step.split(", ").includes(step)) existing.step = `${existing.step}, ${step}`;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
contexts.set(key, {
|
|
651
|
+
title,
|
|
652
|
+
step,
|
|
653
|
+
preview: previewText(text, 180),
|
|
654
|
+
text,
|
|
655
|
+
count: 1
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
function hiddenContextText(value) {
|
|
659
|
+
if (typeof value === "string") return value;
|
|
660
|
+
if (Array.isArray(value)) return value.map(hiddenContextText).filter(Boolean).join("\n\n");
|
|
661
|
+
if (!isRecord2(value)) return value === void 0 || value === null ? "" : String(value);
|
|
662
|
+
if (value.system !== void 0) return hiddenContextText(value.system);
|
|
663
|
+
if (Array.isArray(value.messages)) {
|
|
664
|
+
return value.messages.map(messageContextText).filter(Boolean).join("\n\n");
|
|
665
|
+
}
|
|
666
|
+
const strings = collectStrings(value);
|
|
667
|
+
return strings.length ? strings.join("\n\n") : JSON.stringify(value, null, 2);
|
|
668
|
+
}
|
|
669
|
+
function messageContextText(value) {
|
|
670
|
+
if (!isRecord2(value)) return hiddenContextText(value);
|
|
671
|
+
const info = isRecord2(value.info) ? value.info : {};
|
|
672
|
+
const role = typeof info.role === "string" ? info.role : "message";
|
|
673
|
+
const parts = Array.isArray(value.parts) ? value.parts : [];
|
|
674
|
+
const text = parts.map(partText).filter(Boolean).join("\n");
|
|
675
|
+
return text ? `${role}: ${text}` : `${role}: ${hiddenContextText(value)}`;
|
|
676
|
+
}
|
|
677
|
+
function partText(value) {
|
|
678
|
+
if (!isRecord2(value)) return "";
|
|
679
|
+
return typeof value.text === "string" ? value.text : typeof value.content === "string" ? value.content : "";
|
|
680
|
+
}
|
|
681
|
+
function viewerToolSteps(response) {
|
|
682
|
+
const tools = [];
|
|
683
|
+
const seen = /* @__PURE__ */ new Set();
|
|
684
|
+
for (const wrapper of response?.events || []) {
|
|
685
|
+
const event = isRecord2(wrapper.event) ? wrapper.event : void 0;
|
|
686
|
+
const properties = isRecord2(event?.properties) ? event.properties : void 0;
|
|
687
|
+
const part = isRecord2(properties?.part) ? properties.part : void 0;
|
|
688
|
+
if (event?.type !== "message.part.updated" || part?.type !== "tool") continue;
|
|
689
|
+
const state = isRecord2(part.state) ? part.state : void 0;
|
|
690
|
+
const key = [part.id, part.tool, state?.status].filter(Boolean).join(":");
|
|
691
|
+
if (seen.has(key)) continue;
|
|
692
|
+
seen.add(key);
|
|
693
|
+
tools.push([part.tool || "tool", state?.status].filter(Boolean).join(" \xB7 "));
|
|
694
|
+
}
|
|
695
|
+
return tools;
|
|
696
|
+
}
|
|
697
|
+
function collectStrings(value) {
|
|
698
|
+
if (typeof value === "string") return [value];
|
|
699
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
700
|
+
if (!isRecord2(value)) return [];
|
|
701
|
+
return Object.values(value).flatMap(collectStrings);
|
|
702
|
+
}
|
|
703
|
+
function previewText(value, size) {
|
|
704
|
+
const text = normalizeDisplayText(value);
|
|
705
|
+
return text.length > size ? `${text.slice(0, size - 1)}...` : text || "(empty)";
|
|
706
|
+
}
|
|
707
|
+
function normalizeDisplayText(value) {
|
|
708
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
709
|
+
}
|
|
710
|
+
function isRecord2(value) {
|
|
711
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
712
|
+
}
|
|
713
|
+
function viewerRequestSummary(request) {
|
|
714
|
+
return {
|
|
715
|
+
id: request.id,
|
|
716
|
+
sessionID: request.sessionID,
|
|
717
|
+
messageID: request.messageID,
|
|
718
|
+
timestamp: request.timestamp,
|
|
719
|
+
agent: request.agent,
|
|
720
|
+
purpose: request.purpose,
|
|
721
|
+
providerID: request.providerID,
|
|
722
|
+
modelID: request.modelID,
|
|
723
|
+
summary: request.summary,
|
|
724
|
+
payload: {}
|
|
725
|
+
};
|
|
421
726
|
}
|
|
422
727
|
function sendJson(response, value) {
|
|
423
728
|
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
@@ -439,8 +744,11 @@ function renderViewerHtml(dbPath) {
|
|
|
439
744
|
:root {
|
|
440
745
|
color-scheme: dark;
|
|
441
746
|
--bg: #101214;
|
|
747
|
+
--header: #0d0f11;
|
|
442
748
|
--panel: #171a1d;
|
|
443
749
|
--panel-2: #20242a;
|
|
750
|
+
--surface: #0b0d0f;
|
|
751
|
+
--field: #0d0f11;
|
|
444
752
|
--line: #30363d;
|
|
445
753
|
--text: #eef2f5;
|
|
446
754
|
--muted: #9aa6b2;
|
|
@@ -448,6 +756,32 @@ function renderViewerHtml(dbPath) {
|
|
|
448
756
|
--ok: #86efac;
|
|
449
757
|
--warn: #fbbf24;
|
|
450
758
|
--bad: #fca5a5;
|
|
759
|
+
--pill-text: #0d0f11;
|
|
760
|
+
--json-key: #bae6fd;
|
|
761
|
+
--json-string: #bbf7d0;
|
|
762
|
+
--json-number: #fde68a;
|
|
763
|
+
--json-boolean: #f0abfc;
|
|
764
|
+
}
|
|
765
|
+
body[data-theme="light"] {
|
|
766
|
+
color-scheme: light;
|
|
767
|
+
--bg: #f6f7f9;
|
|
768
|
+
--header: #ffffff;
|
|
769
|
+
--panel: #ffffff;
|
|
770
|
+
--panel-2: #edf2f7;
|
|
771
|
+
--surface: #ffffff;
|
|
772
|
+
--field: #ffffff;
|
|
773
|
+
--line: #d7dde5;
|
|
774
|
+
--text: #18212f;
|
|
775
|
+
--muted: #667085;
|
|
776
|
+
--accent: #2563eb;
|
|
777
|
+
--ok: #16a34a;
|
|
778
|
+
--warn: #b45309;
|
|
779
|
+
--bad: #dc2626;
|
|
780
|
+
--pill-text: #ffffff;
|
|
781
|
+
--json-key: #1d4ed8;
|
|
782
|
+
--json-string: #15803d;
|
|
783
|
+
--json-number: #a16207;
|
|
784
|
+
--json-boolean: #9333ea;
|
|
451
785
|
}
|
|
452
786
|
* { box-sizing: border-box; }
|
|
453
787
|
body {
|
|
@@ -465,9 +799,35 @@ function renderViewerHtml(dbPath) {
|
|
|
465
799
|
gap: 16px;
|
|
466
800
|
padding: 0 18px;
|
|
467
801
|
border-bottom: 1px solid var(--line);
|
|
468
|
-
background:
|
|
802
|
+
background: var(--header);
|
|
469
803
|
}
|
|
470
804
|
h1 { margin: 0; font-size: 15px; font-weight: 700; }
|
|
805
|
+
.brand, .header-side {
|
|
806
|
+
display: flex;
|
|
807
|
+
align-items: center;
|
|
808
|
+
gap: 14px;
|
|
809
|
+
min-width: 0;
|
|
810
|
+
}
|
|
811
|
+
.header-meta { min-width: 0; }
|
|
812
|
+
.theme-toggle {
|
|
813
|
+
display: inline-flex;
|
|
814
|
+
gap: 2px;
|
|
815
|
+
padding: 3px;
|
|
816
|
+
border: 1px solid var(--line);
|
|
817
|
+
border-radius: 8px;
|
|
818
|
+
background: var(--panel);
|
|
819
|
+
}
|
|
820
|
+
.theme-toggle button {
|
|
821
|
+
padding: 5px 8px;
|
|
822
|
+
border: 0;
|
|
823
|
+
border-radius: 5px;
|
|
824
|
+
background: transparent;
|
|
825
|
+
color: var(--muted);
|
|
826
|
+
}
|
|
827
|
+
.theme-toggle button.active {
|
|
828
|
+
background: var(--panel-2);
|
|
829
|
+
color: var(--text);
|
|
830
|
+
}
|
|
471
831
|
main {
|
|
472
832
|
display: grid;
|
|
473
833
|
grid-template-columns: 300px minmax(340px, 0.95fr) minmax(460px, 1.25fr);
|
|
@@ -486,15 +846,17 @@ function renderViewerHtml(dbPath) {
|
|
|
486
846
|
border-bottom: 1px solid var(--line);
|
|
487
847
|
z-index: 2;
|
|
488
848
|
}
|
|
489
|
-
input {
|
|
849
|
+
input, select {
|
|
490
850
|
width: 100%;
|
|
491
851
|
border: 1px solid var(--line);
|
|
492
|
-
background:
|
|
852
|
+
background: var(--field);
|
|
493
853
|
color: var(--text);
|
|
494
854
|
border-radius: 6px;
|
|
495
855
|
padding: 8px 10px;
|
|
496
856
|
font: inherit;
|
|
497
857
|
}
|
|
858
|
+
select { cursor: pointer; }
|
|
859
|
+
.toolbar.stack { flex-direction: column; }
|
|
498
860
|
button {
|
|
499
861
|
border: 1px solid var(--line);
|
|
500
862
|
background: var(--panel-2);
|
|
@@ -517,7 +879,7 @@ function renderViewerHtml(dbPath) {
|
|
|
517
879
|
padding: 11px 12px;
|
|
518
880
|
}
|
|
519
881
|
.item:hover, .item.active { background: var(--panel-2); }
|
|
520
|
-
.
|
|
882
|
+
.session-child { padding-left: 28px; }
|
|
521
883
|
.title { color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
522
884
|
.meta {
|
|
523
885
|
margin-top: 4px;
|
|
@@ -528,7 +890,7 @@ function renderViewerHtml(dbPath) {
|
|
|
528
890
|
}
|
|
529
891
|
.muted { color: var(--muted); }
|
|
530
892
|
.pill {
|
|
531
|
-
color:
|
|
893
|
+
color: var(--pill-text);
|
|
532
894
|
background: var(--accent);
|
|
533
895
|
border-radius: 999px;
|
|
534
896
|
padding: 1px 6px;
|
|
@@ -548,7 +910,7 @@ function renderViewerHtml(dbPath) {
|
|
|
548
910
|
overflow: auto;
|
|
549
911
|
white-space: pre-wrap;
|
|
550
912
|
word-break: break-word;
|
|
551
|
-
background:
|
|
913
|
+
background: var(--surface);
|
|
552
914
|
border: 1px solid var(--line);
|
|
553
915
|
border-radius: 8px;
|
|
554
916
|
min-height: 220px;
|
|
@@ -567,14 +929,45 @@ function renderViewerHtml(dbPath) {
|
|
|
567
929
|
color: var(--text);
|
|
568
930
|
font-weight: 700;
|
|
569
931
|
}
|
|
932
|
+
.step {
|
|
933
|
+
margin: 0 0 10px;
|
|
934
|
+
padding: 12px;
|
|
935
|
+
border: 1px solid var(--line);
|
|
936
|
+
border-radius: 8px;
|
|
937
|
+
background: var(--surface);
|
|
938
|
+
}
|
|
939
|
+
.step-label {
|
|
940
|
+
color: var(--accent);
|
|
941
|
+
font-weight: 700;
|
|
942
|
+
margin-bottom: 6px;
|
|
943
|
+
}
|
|
944
|
+
.step-text { white-space: pre-wrap; word-break: break-word; }
|
|
945
|
+
details.hidden-context {
|
|
946
|
+
margin: 10px 0;
|
|
947
|
+
border: 1px solid var(--line);
|
|
948
|
+
border-radius: 8px;
|
|
949
|
+
background: var(--surface);
|
|
950
|
+
}
|
|
951
|
+
details.hidden-context summary {
|
|
952
|
+
cursor: pointer;
|
|
953
|
+
padding: 10px 12px;
|
|
954
|
+
color: var(--accent);
|
|
955
|
+
}
|
|
956
|
+
.hidden-body { padding: 0 12px 12px; }
|
|
957
|
+
.hidden-text {
|
|
958
|
+
margin: 0;
|
|
959
|
+
white-space: pre-wrap;
|
|
960
|
+
word-break: break-word;
|
|
961
|
+
color: var(--text);
|
|
962
|
+
}
|
|
570
963
|
.json-tree { font-size: 12px; line-height: 1.55; }
|
|
571
964
|
.json-tree details { margin-left: 14px; }
|
|
572
965
|
.json-tree summary { cursor: pointer; color: var(--accent); }
|
|
573
966
|
.json-tree .leaf { margin-left: 14px; }
|
|
574
|
-
.json-key { color:
|
|
575
|
-
.json-string { color:
|
|
576
|
-
.json-number { color:
|
|
577
|
-
.json-boolean { color:
|
|
967
|
+
.json-key { color: var(--json-key); }
|
|
968
|
+
.json-string { color: var(--json-string); }
|
|
969
|
+
.json-number { color: var(--json-number); }
|
|
970
|
+
.json-boolean { color: var(--json-boolean); }
|
|
578
971
|
.json-null { color: var(--muted); }
|
|
579
972
|
@media (max-width: 1000px) {
|
|
580
973
|
main { grid-template-columns: 1fr; height: auto; }
|
|
@@ -582,38 +975,44 @@ function renderViewerHtml(dbPath) {
|
|
|
582
975
|
}
|
|
583
976
|
</style>
|
|
584
977
|
</head>
|
|
585
|
-
<body>
|
|
978
|
+
<body data-theme="dark">
|
|
586
979
|
<header>
|
|
587
|
-
<
|
|
588
|
-
|
|
589
|
-
<div class="
|
|
590
|
-
|
|
980
|
+
<div class="brand">
|
|
981
|
+
<h1>OpenCode Insights</h1>
|
|
982
|
+
<div id="theme-toggle" class="theme-toggle" aria-label="Theme">
|
|
983
|
+
<button type="button" data-theme-option="dark">Dark</button>
|
|
984
|
+
<button type="button" data-theme-option="light">Light</button>
|
|
985
|
+
</div>
|
|
986
|
+
</div>
|
|
987
|
+
<div class="header-side">
|
|
988
|
+
<div class="header-meta">
|
|
989
|
+
<div class="meta">${escapedDbPath}</div>
|
|
990
|
+
<div id="status" class="status">Loading history...</div>
|
|
991
|
+
</div>
|
|
591
992
|
</div>
|
|
592
993
|
</header>
|
|
593
994
|
<main>
|
|
594
995
|
<section>
|
|
595
|
-
<div class="toolbar
|
|
996
|
+
<div class="toolbar stack">
|
|
997
|
+
<select id="project-filter"><option value="">All projects</option></select>
|
|
998
|
+
<input id="session-filter" placeholder="Filter sessions">
|
|
999
|
+
</div>
|
|
596
1000
|
<div id="sessions"></div>
|
|
597
1001
|
</section>
|
|
598
1002
|
<section>
|
|
599
|
-
<div class="toolbar"><input id="timeline-filter" placeholder="Filter
|
|
1003
|
+
<div class="toolbar"><input id="timeline-filter" placeholder="Filter conversation"></div>
|
|
600
1004
|
<div id="timeline"></div>
|
|
601
1005
|
</section>
|
|
602
1006
|
<section>
|
|
603
1007
|
<div class="toolbar">
|
|
604
|
-
<button id="copy">Copy
|
|
1008
|
+
<button id="copy">Copy Summary</button>
|
|
605
1009
|
<button id="refresh">Refresh</button>
|
|
606
1010
|
</div>
|
|
607
1011
|
<div class="detail">
|
|
608
1012
|
<div class="tabs">
|
|
609
1013
|
<button data-tab="summary" class="active">Summary</button>
|
|
610
|
-
<button data-tab="request">Request</button>
|
|
611
|
-
<button data-tab="response">Response</button>
|
|
612
|
-
<button data-tab="raw">Raw</button>
|
|
613
|
-
<button id="expand-json" class="json-control">Expand All</button>
|
|
614
|
-
<button id="collapse-json" class="json-control">Collapse All</button>
|
|
615
1014
|
</div>
|
|
616
|
-
<div id="detail" class="panel">Select a message
|
|
1015
|
+
<div id="detail" class="panel">Select a user message.</div>
|
|
617
1016
|
</div>
|
|
618
1017
|
</section>
|
|
619
1018
|
</main>
|
|
@@ -624,11 +1023,14 @@ function renderViewerHtml(dbPath) {
|
|
|
624
1023
|
messageID: null,
|
|
625
1024
|
requestID: null,
|
|
626
1025
|
tab: "summary",
|
|
1026
|
+
project: "",
|
|
627
1027
|
loading: true,
|
|
628
1028
|
error: null,
|
|
629
|
-
loadedMs: 0
|
|
1029
|
+
loadedMs: 0,
|
|
1030
|
+
payloadCache: {}
|
|
630
1031
|
};
|
|
631
1032
|
|
|
1033
|
+
const THEME_KEY = "opencode-insights-theme";
|
|
632
1034
|
const qs = (id) => document.getElementById(id);
|
|
633
1035
|
const fmt = (ms) => ms ? new Date(ms).toLocaleString() : "-";
|
|
634
1036
|
const short = (value, size = 90) => {
|
|
@@ -636,11 +1038,20 @@ function renderViewerHtml(dbPath) {
|
|
|
636
1038
|
return text.length > size ? text.slice(0, size - 1) + "..." : text;
|
|
637
1039
|
};
|
|
638
1040
|
|
|
1041
|
+
function applyTheme(theme) {
|
|
1042
|
+
const next = theme === "light" ? "light" : "dark";
|
|
1043
|
+
document.body.dataset.theme = next;
|
|
1044
|
+
localStorage.setItem(THEME_KEY, next);
|
|
1045
|
+
for (const item of document.querySelectorAll("[data-theme-option]")) {
|
|
1046
|
+
item.classList.toggle("active", item.dataset.themeOption === next);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
639
1050
|
async function load() {
|
|
640
1051
|
state.loading = true;
|
|
641
1052
|
state.error = null;
|
|
642
1053
|
renderStatus();
|
|
643
|
-
qs("sessions").innerHTML = '<div class="empty">Loading sessions
|
|
1054
|
+
qs("sessions").innerHTML = '<div class="empty">Loading sessions...</div>';
|
|
644
1055
|
qs("timeline").innerHTML = '<div class="empty">Waiting for history data...</div>';
|
|
645
1056
|
try {
|
|
646
1057
|
const started = performance.now();
|
|
@@ -654,8 +1065,9 @@ function renderViewerHtml(dbPath) {
|
|
|
654
1065
|
state.messageID = null;
|
|
655
1066
|
state.requestID = null;
|
|
656
1067
|
}
|
|
1068
|
+
renderProjectOptions();
|
|
657
1069
|
const session = selectedSession();
|
|
658
|
-
if (session && !state.messageID
|
|
1070
|
+
if (session && !state.messageID) state.messageID = firstUserMessage(session)?.id || null;
|
|
659
1071
|
} catch (error) {
|
|
660
1072
|
state.error = error instanceof Error ? error.message : String(error);
|
|
661
1073
|
} finally {
|
|
@@ -673,17 +1085,8 @@ function renderViewerHtml(dbPath) {
|
|
|
673
1085
|
return session?.messages.find((message) => message.id === state.messageID) || null;
|
|
674
1086
|
}
|
|
675
1087
|
|
|
676
|
-
function
|
|
677
|
-
|
|
678
|
-
return message?.requests.find((request) => request.id === state.requestID)
|
|
679
|
-
|| state.history.requests.find((request) => request.id === state.requestID)
|
|
680
|
-
|| null;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function activeContext() {
|
|
684
|
-
const message = selectedMessage();
|
|
685
|
-
const request = selectedRequest();
|
|
686
|
-
return { message, request: request || message?.requests[0] || null };
|
|
1088
|
+
function firstUserMessage(session) {
|
|
1089
|
+
return session?.messages.find((message) => message.role === "user") || session?.messages[0] || null;
|
|
687
1090
|
}
|
|
688
1091
|
|
|
689
1092
|
function render() {
|
|
@@ -704,33 +1107,67 @@ function renderViewerHtml(dbPath) {
|
|
|
704
1107
|
status.classList.toggle("error", Boolean(state.error));
|
|
705
1108
|
if (state.error) status.textContent = "Load failed";
|
|
706
1109
|
else if (state.loading) status.textContent = "Loading history...";
|
|
707
|
-
else status.textContent = state.history.sessions.length + " sessions \xB7 " + state.history.requests.length + "
|
|
1110
|
+
else status.textContent = state.history.sessions.length + " sessions \xB7 " + state.history.requests.length + " model steps \xB7 " + state.loadedMs + "ms";
|
|
708
1111
|
qs("refresh").disabled = state.loading;
|
|
709
1112
|
qs("refresh").textContent = state.loading ? "Loading..." : "Refresh";
|
|
710
1113
|
}
|
|
711
1114
|
|
|
1115
|
+
function renderProjectOptions() {
|
|
1116
|
+
const select = qs("project-filter");
|
|
1117
|
+
const current = state.project;
|
|
1118
|
+
const projects = [...new Set(state.history.sessions.map((session) => session.project || session.cwd || "Unknown").filter(Boolean))].sort();
|
|
1119
|
+
select.innerHTML = '<option value="">All projects</option>' + projects.map((project) =>
|
|
1120
|
+
'<option value="' + escapeAttr(project) + '">' + escapeHtml(project) + '</option>'
|
|
1121
|
+
).join("");
|
|
1122
|
+
if (projects.includes(current)) select.value = current;
|
|
1123
|
+
else state.project = "";
|
|
1124
|
+
}
|
|
1125
|
+
|
|
712
1126
|
function renderSessions() {
|
|
713
1127
|
const filter = qs("session-filter").value.toLowerCase();
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1128
|
+
const project = state.project;
|
|
1129
|
+
const all = state.history.sessions;
|
|
1130
|
+
const byParent = new Map();
|
|
1131
|
+
const byId = new Map(all.map((session) => [session.id, session]));
|
|
1132
|
+
for (const session of all) {
|
|
1133
|
+
const parent = session.parentID && byId.has(session.parentID) ? session.parentID : "";
|
|
1134
|
+
const list = byParent.get(parent) || [];
|
|
1135
|
+
list.push(session);
|
|
1136
|
+
byParent.set(parent, list);
|
|
1137
|
+
}
|
|
1138
|
+
for (const list of byParent.values()) list.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
1139
|
+
const matches = (session) => {
|
|
1140
|
+
const projectMatch = !project || (session.project || session.cwd || "Unknown") === project;
|
|
1141
|
+
const text = [session.id, session.title, session.project, session.cwd].filter(Boolean).join(" ").toLowerCase();
|
|
1142
|
+
return projectMatch && (!filter || text.includes(filter));
|
|
1143
|
+
};
|
|
1144
|
+
const renderNode = (session, depth = 0) => {
|
|
1145
|
+
const children = byParent.get(session.id) || [];
|
|
1146
|
+
const childHtml = children.map((child) => renderNode(child, depth + 1)).join("");
|
|
1147
|
+
if (!matches(session) && !childHtml) return "";
|
|
1148
|
+
const cls = "item" + (depth > 0 ? " session-child" : "") + (session.id === state.sessionID ? " active" : "");
|
|
1149
|
+
return '<button class="' + cls + '" data-session="' + escapeAttr(session.id) + '">' +
|
|
1150
|
+
'<div class="title">' + (depth > 0 ? "sub: " : "") + escapeHtml(session.title || session.id) + '</div>' +
|
|
1151
|
+
'<div class="meta">' + escapeHtml(session.project || session.cwd || "Unknown project") + ' \xB7 ' + userMessages(session).length + ' messages \xB7 ' + fmt(session.updatedAt) + '</div>' +
|
|
1152
|
+
'</button>' + childHtml;
|
|
1153
|
+
};
|
|
1154
|
+
const html = (byParent.get("") || []).map((session) => renderNode(session)).join("");
|
|
1155
|
+
qs("sessions").innerHTML = html || '<div class="empty">No sessions found.</div>';
|
|
723
1156
|
for (const item of document.querySelectorAll("[data-session]")) {
|
|
724
1157
|
item.onclick = () => {
|
|
725
1158
|
state.sessionID = item.dataset.session;
|
|
726
1159
|
const session = selectedSession();
|
|
727
|
-
state.messageID = session?.
|
|
1160
|
+
state.messageID = firstUserMessage(session)?.id || null;
|
|
728
1161
|
state.requestID = null;
|
|
729
1162
|
render();
|
|
730
1163
|
};
|
|
731
1164
|
}
|
|
732
1165
|
}
|
|
733
1166
|
|
|
1167
|
+
function userMessages(session) {
|
|
1168
|
+
return (session?.messages || []).filter((message) => message.role === "user");
|
|
1169
|
+
}
|
|
1170
|
+
|
|
734
1171
|
function renderTimeline() {
|
|
735
1172
|
const session = selectedSession();
|
|
736
1173
|
if (!session) {
|
|
@@ -739,79 +1176,71 @@ function renderViewerHtml(dbPath) {
|
|
|
739
1176
|
}
|
|
740
1177
|
const filter = qs("timeline-filter").value.toLowerCase();
|
|
741
1178
|
const blocks = [];
|
|
742
|
-
for (const message of session
|
|
1179
|
+
for (const message of userMessages(session)) {
|
|
743
1180
|
const searchable = JSON.stringify(message).toLowerCase();
|
|
744
1181
|
if (filter && !searchable.includes(filter)) continue;
|
|
745
1182
|
blocks.push(
|
|
746
|
-
'<button class="item ' + (message.id === state.messageID
|
|
747
|
-
'<div class="title"><span class="pill msg">
|
|
748
|
-
'<div class="meta">' + message.
|
|
1183
|
+
'<button class="item ' + (message.id === state.messageID ? "active" : "") + '" data-message="' + escapeAttr(message.id) + '">' +
|
|
1184
|
+
'<div class="title"><span class="pill msg">USER</span> ' + escapeHtml(short(message.text || message.id, 130)) + '</div>' +
|
|
1185
|
+
'<div class="meta">' + visibleSteps(message).length + ' visible steps \xB7 ' + hiddenContexts(message).length + ' hidden context items \xB7 ' + fmt(message.createdAt) + '</div>' +
|
|
749
1186
|
'</button>'
|
|
750
1187
|
);
|
|
751
|
-
for (const request of message.requests) {
|
|
752
|
-
blocks.push(
|
|
753
|
-
'<button class="item request-item ' + (request.id === state.requestID ? "active" : "") + '" data-message="' + escapeAttr(message.id) + '" data-request="' + escapeAttr(request.id) + '">' +
|
|
754
|
-
'<div class="title"><span class="pill">HOOK</span> ' + escapeHtml(request.agent || "agent") + ' \xB7 ' + escapeHtml([request.providerID, request.modelID].filter(Boolean).join("/") || "-") + '</div>' +
|
|
755
|
-
'<div class="meta">' + escapeHtml(short(request.summary, 120)) + ' \xB7 ' + (request.headers ? "headers" : "no headers") + ' \xB7 ' + (request.response?.text ? "response" : "no response") + '</div>' +
|
|
756
|
-
'</button>'
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
1188
|
}
|
|
760
|
-
qs("timeline").innerHTML = blocks.length ? blocks.join("") : '<div class="empty">No messages
|
|
1189
|
+
qs("timeline").innerHTML = blocks.length ? blocks.join("") : '<div class="empty">No user messages found.</div>';
|
|
761
1190
|
for (const item of document.querySelectorAll("[data-message]")) {
|
|
762
1191
|
item.onclick = () => {
|
|
763
1192
|
state.messageID = item.dataset.message;
|
|
764
|
-
state.requestID =
|
|
1193
|
+
state.requestID = null;
|
|
765
1194
|
render();
|
|
766
1195
|
};
|
|
767
1196
|
}
|
|
768
1197
|
}
|
|
769
1198
|
|
|
770
1199
|
function renderDetail() {
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
qs("detail").textContent = "Select a message or request.";
|
|
1200
|
+
const message = selectedMessage();
|
|
1201
|
+
if (!message) {
|
|
1202
|
+
qs("detail").textContent = "Select a user message.";
|
|
775
1203
|
return;
|
|
776
1204
|
}
|
|
777
1205
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
kv("
|
|
784
|
-
kv("
|
|
785
|
-
kv("
|
|
786
|
-
kv("
|
|
787
|
-
kv("
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
'</div>';
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
1206
|
+
const session = selectedSession();
|
|
1207
|
+
const steps = visibleSteps(message);
|
|
1208
|
+
const hidden = hiddenContexts(message);
|
|
1209
|
+
qs("detail").innerHTML =
|
|
1210
|
+
'<div class="kv">' +
|
|
1211
|
+
kv("Session", session?.title || session?.id || "-") +
|
|
1212
|
+
kv("Project", session?.project || session?.cwd || "Unknown") +
|
|
1213
|
+
kv("User message", message.text || "(no user text captured)") +
|
|
1214
|
+
kv("Visible steps", String(steps.length)) +
|
|
1215
|
+
kv("Hidden context", hidden.length ? hidden.length + " item(s)" : "none captured") +
|
|
1216
|
+
'</div>' +
|
|
1217
|
+
'<div class="subhead">Agent Thinking / Response Sequence</div>' +
|
|
1218
|
+
(steps.length ? steps.map(renderStep).join("") : '<div class="empty">No assistant thinking or response text captured for this message.</div>') +
|
|
1219
|
+
'<div class="subhead">Hidden Context</div>' +
|
|
1220
|
+
(hidden.length ? hidden.map(renderHiddenContext).join("") : '<div class="empty">No system prompt or hidden prompt-like context captured.</div>');
|
|
1221
|
+
}
|
|
797
1222
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
1223
|
+
function visibleSteps(message) {
|
|
1224
|
+
return message.visibleSteps || [];
|
|
1225
|
+
}
|
|
802
1226
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
status: "missing",
|
|
807
|
-
note: request?.agent === "title"
|
|
808
|
-
? "The title request does not produce the assistant reply. Select the build request or MSG row to inspect the conversation response."
|
|
809
|
-
: "No assistant response events were captured for this message yet."
|
|
810
|
-
});
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
1227
|
+
function hiddenContexts(message) {
|
|
1228
|
+
return message.hiddenContexts || [];
|
|
1229
|
+
}
|
|
813
1230
|
|
|
814
|
-
|
|
1231
|
+
function renderStep(step) {
|
|
1232
|
+
return '<div class="step">' +
|
|
1233
|
+
'<div class="step-label">' + escapeHtml(step.label) + '</div>' +
|
|
1234
|
+
'<div class="step-text">' + escapeHtml(step.text) + '</div>' +
|
|
1235
|
+
'</div>';
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function renderHiddenContext(item) {
|
|
1239
|
+
const count = item.count > 1 ? ' \xB7 used by ' + item.count + ' model steps' : "";
|
|
1240
|
+
return '<details class="hidden-context">' +
|
|
1241
|
+
'<summary>' + escapeHtml(item.title) + ' \xB7 ' + escapeHtml(item.step || "-") + count + ' \xB7 ' + escapeHtml(item.preview) + '</summary>' +
|
|
1242
|
+
'<div class="hidden-body"><pre class="hidden-text">' + escapeHtml(item.text) + '</pre></div>' +
|
|
1243
|
+
'</details>';
|
|
815
1244
|
}
|
|
816
1245
|
|
|
817
1246
|
function kv(key, value) {
|
|
@@ -827,12 +1256,22 @@ function renderViewerHtml(dbPath) {
|
|
|
827
1256
|
renderJson({ status: "missing", note: "Select a hook row to inspect hook details." });
|
|
828
1257
|
return;
|
|
829
1258
|
}
|
|
830
|
-
const
|
|
1259
|
+
const cached = state.payloadCache[request.id];
|
|
1260
|
+
if (cached === undefined || cached === null) {
|
|
1261
|
+
qs("detail").innerHTML = '<div class="empty">Loading hook payload...</div>';
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (cached.error) {
|
|
1265
|
+
renderJson({ error: cached.error });
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const payload = cached;
|
|
831
1269
|
const hookInput = payload.input || {};
|
|
832
1270
|
const hookOutput = payload.output || {};
|
|
1271
|
+
const systemOutput = request.system?.payload?.output || null;
|
|
833
1272
|
const headerOutput = request.headers?.payload?.output || null;
|
|
834
1273
|
qs("detail").innerHTML =
|
|
835
|
-
'<p class="explain">These are OpenCode plugin hook values, not a raw HTTP request. <b>Hook input</b> is the context OpenCode passed to the plugin before the model call. <b>Hook output</b> is the model settings returned by the plugin hook. <b>Headers output</b> is the provider headers hook result.</p>' +
|
|
1274
|
+
'<p class="explain">These are OpenCode plugin hook values, not a raw HTTP request. <b>Hook input</b> is the context OpenCode passed to the plugin before the model call. <b>Hook output</b> is the model settings returned by the plugin hook. <b>System transform</b> is the system prompt OpenCode prepared for this same call. <b>Headers output</b> is the provider headers hook result.</p>' +
|
|
836
1275
|
'<div class="kv">' +
|
|
837
1276
|
kv("Hook id", request.id) +
|
|
838
1277
|
kv("Agent", request.agent || "-") +
|
|
@@ -845,10 +1284,12 @@ function renderViewerHtml(dbPath) {
|
|
|
845
1284
|
'<div class="json-tree">' + jsonNode(summarizeHookInput(hookInput), "hookInput", true) + '</div>' +
|
|
846
1285
|
'<div class="subhead">Hook Output: model-call settings</div>' +
|
|
847
1286
|
'<div class="json-tree">' + jsonNode(hookOutput, "hookOutput", true) + '</div>' +
|
|
1287
|
+
'<div class="subhead">System Transform Output</div>' +
|
|
1288
|
+
'<div class="json-tree">' + jsonNode(systemOutput, "systemOutput", true) + '</div>' +
|
|
848
1289
|
'<div class="subhead">Headers Hook Output</div>' +
|
|
849
1290
|
'<div class="json-tree">' + jsonNode(headerOutput, "headersOutput", true) + '</div>' +
|
|
850
1291
|
'<div class="subhead">Raw Full-Fidelity Payload</div>' +
|
|
851
|
-
'<div class="json-tree">' + jsonNode({
|
|
1292
|
+
'<div class="json-tree">' + jsonNode({ system: request.system?.payload || null, params: payload, headers: request.headers?.payload || null }, "raw", false) + '</div>';
|
|
852
1293
|
}
|
|
853
1294
|
|
|
854
1295
|
function summarizeHookInput(input) {
|
|
@@ -915,16 +1356,23 @@ function renderViewerHtml(dbPath) {
|
|
|
915
1356
|
}
|
|
916
1357
|
|
|
917
1358
|
qs("session-filter").oninput = render;
|
|
1359
|
+
qs("project-filter").onchange = () => {
|
|
1360
|
+
state.project = qs("project-filter").value;
|
|
1361
|
+
render();
|
|
1362
|
+
};
|
|
918
1363
|
qs("timeline-filter").oninput = render;
|
|
919
1364
|
qs("refresh").onclick = load;
|
|
920
|
-
|
|
921
|
-
|
|
1365
|
+
for (const item of document.querySelectorAll("[data-theme-option]")) {
|
|
1366
|
+
item.onclick = () => applyTheme(item.dataset.themeOption);
|
|
1367
|
+
}
|
|
922
1368
|
qs("copy").onclick = async () => {
|
|
923
|
-
const
|
|
924
|
-
const value =
|
|
925
|
-
:
|
|
926
|
-
:
|
|
927
|
-
:
|
|
1369
|
+
const message = selectedMessage();
|
|
1370
|
+
const value = message ? {
|
|
1371
|
+
messageID: message.id,
|
|
1372
|
+
text: message.text,
|
|
1373
|
+
steps: visibleSteps(message),
|
|
1374
|
+
hiddenContext: hiddenContexts(message)
|
|
1375
|
+
} : null;
|
|
928
1376
|
await navigator.clipboard.writeText(JSON.stringify(value, null, 2));
|
|
929
1377
|
};
|
|
930
1378
|
for (const tab of document.querySelectorAll("[data-tab]")) {
|
|
@@ -934,6 +1382,7 @@ function renderViewerHtml(dbPath) {
|
|
|
934
1382
|
renderDetail();
|
|
935
1383
|
};
|
|
936
1384
|
}
|
|
1385
|
+
applyTheme(localStorage.getItem(THEME_KEY));
|
|
937
1386
|
load();
|
|
938
1387
|
</script>
|
|
939
1388
|
</body>
|
|
@@ -1090,10 +1539,16 @@ function parseOptions(args) {
|
|
|
1090
1539
|
const value = args[index + 1];
|
|
1091
1540
|
if (value) options.configDir = value;
|
|
1092
1541
|
index += 1;
|
|
1542
|
+
} else if (arg === "--retention-days") {
|
|
1543
|
+
options.retentionDays = Number.parseFloat(args[index + 1] ?? "");
|
|
1544
|
+
index += 1;
|
|
1093
1545
|
}
|
|
1094
1546
|
}
|
|
1095
1547
|
if (!Number.isFinite(options.limit) || options.limit < 1) options.limit = DEFAULT_RECENT_LIMIT;
|
|
1096
1548
|
if (options.port !== void 0 && (!Number.isFinite(options.port) || options.port < 1)) options.port = 8765;
|
|
1549
|
+
if (options.retentionDays !== void 0 && (!Number.isFinite(options.retentionDays) || options.retentionDays < 0)) {
|
|
1550
|
+
options.retentionDays = void 0;
|
|
1551
|
+
}
|
|
1097
1552
|
return options;
|
|
1098
1553
|
}
|
|
1099
1554
|
function parsePositionals(args) {
|
|
@@ -1102,7 +1557,7 @@ function parsePositionals(args) {
|
|
|
1102
1557
|
const arg = args[index];
|
|
1103
1558
|
if (!arg) continue;
|
|
1104
1559
|
if (arg.startsWith("--")) {
|
|
1105
|
-
if (["--db", "--data-dir", "--limit", "--host", "--port", "--output", "--config-dir"].includes(arg)) index += 1;
|
|
1560
|
+
if (["--db", "--data-dir", "--limit", "--host", "--port", "--output", "--config-dir", "--retention-days"].includes(arg)) index += 1;
|
|
1106
1561
|
continue;
|
|
1107
1562
|
}
|
|
1108
1563
|
if (arg === "-o") {
|
|
@@ -1234,7 +1689,7 @@ async function configureOpenCodeDebug(options) {
|
|
|
1234
1689
|
}
|
|
1235
1690
|
const opencodeConfig = await readJsonConfig(opencodePath, { plugin: [] });
|
|
1236
1691
|
const tuiConfig = await readJsonConfig(tuiPath, { plugin: [] });
|
|
1237
|
-
setSinglePluginSpec(opencodeConfig, SERVER_PLUGIN_SPEC, localServerEntry);
|
|
1692
|
+
setSinglePluginSpec(opencodeConfig, SERVER_PLUGIN_SPEC, [localServerEntry, debugServerOptions(options)], localServerEntry);
|
|
1238
1693
|
setSinglePluginSpec(tuiConfig, TUI_PLUGIN_SPEC, localTuiEntry);
|
|
1239
1694
|
removePlugin(tuiConfig, SUBPATH_TUI_PLUGIN_SPEC);
|
|
1240
1695
|
const lines = [
|
|
@@ -1323,6 +1778,11 @@ function stripJsonCommentsAndTrailingCommas(input) {
|
|
|
1323
1778
|
function isJsonObject(value) {
|
|
1324
1779
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1325
1780
|
}
|
|
1781
|
+
function debugServerOptions(options) {
|
|
1782
|
+
const serverOptions = {};
|
|
1783
|
+
if (options.retentionDays !== void 0) serverOptions.retentionDays = options.retentionDays;
|
|
1784
|
+
return serverOptions;
|
|
1785
|
+
}
|
|
1326
1786
|
function addUniquePlugin(config, plugin) {
|
|
1327
1787
|
const current = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1328
1788
|
if (current.includes(plugin)) {
|
|
@@ -1341,9 +1801,10 @@ function removePlugin(config, plugin) {
|
|
|
1341
1801
|
function isPluginEntry(entry, plugin) {
|
|
1342
1802
|
return entry === plugin || Array.isArray(entry) && entry[0] === plugin;
|
|
1343
1803
|
}
|
|
1344
|
-
function setSinglePluginSpec(config, previousPlugin, nextPlugin) {
|
|
1804
|
+
function setSinglePluginSpec(config, previousPlugin, nextPlugin, nextPluginSpec) {
|
|
1345
1805
|
const current = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1346
|
-
const
|
|
1806
|
+
const localPlugin = nextPluginSpec ?? (typeof nextPlugin === "string" ? nextPlugin : "");
|
|
1807
|
+
const next = current.filter((entry) => !isInsightsPluginEntry(entry, previousPlugin, localPlugin));
|
|
1347
1808
|
config.plugin = [...next, nextPlugin];
|
|
1348
1809
|
}
|
|
1349
1810
|
function isInsightsPluginEntry(entry, packagePlugin, localPlugin) {
|
|
@@ -1411,7 +1872,7 @@ async function writeJsonConfig(path, config) {
|
|
|
1411
1872
|
function usage() {
|
|
1412
1873
|
return [
|
|
1413
1874
|
"Usage:",
|
|
1414
|
-
" opencode-insights debug [--config-dir DIR] [--dry-run]",
|
|
1875
|
+
" opencode-insights debug [--config-dir DIR] [--retention-days DAYS] [--dry-run]",
|
|
1415
1876
|
" opencode-insights uninstall [--config-dir DIR] [--db PATH] [--data-dir DIR] [--keep-data] [--dry-run]",
|
|
1416
1877
|
" opencode-insights recent [--db PATH] [--data-dir DIR] [--limit N] [--json]",
|
|
1417
1878
|
" opencode-insights sessions [--db PATH] [--data-dir DIR] [--limit N] [--json]",
|