@rejacky/opencode-insights 0.1.4 → 0.1.6
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 +80 -94
- package/dist/{capture-DkASdFpu.d.ts → capture-Dy799i3Z.d.ts} +18 -8
- package/dist/{chunk-Q5ROKOOJ.js → chunk-XCHFXQIL.js} +167 -49
- package/dist/cli.d.ts +7 -1
- package/dist/cli.js +622 -150
- package/dist/index.d.ts +1 -2
- package/dist/index.js +66 -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-XCHFXQIL.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));
|
|
@@ -184,14 +259,21 @@ function buildRequestHistory(records) {
|
|
|
184
259
|
requests: requests.sort((a, b) => b.timestamp - a.timestamp)
|
|
185
260
|
};
|
|
186
261
|
}
|
|
262
|
+
function ensureEventTypeColumn(db) {
|
|
263
|
+
const existing = db.all("select name from pragma_table_info('captures') where name = 'event_type'");
|
|
264
|
+
if (existing.length === 0) {
|
|
265
|
+
db.run(`alter table captures add column event_type text`);
|
|
266
|
+
}
|
|
267
|
+
db.run(`update captures set event_type = json_extract(payload_json, '$.event.type') where kind = 'event' and event_type is null`);
|
|
268
|
+
}
|
|
187
269
|
async function readSqliteCaptures(path, limit) {
|
|
188
270
|
if (!existsSync(path)) return void 0;
|
|
189
271
|
try {
|
|
190
|
-
const
|
|
191
|
-
if (!
|
|
192
|
-
const db = new mod.Database(path, { readonly: true });
|
|
272
|
+
const db = await openDatabase(path);
|
|
273
|
+
if (!db) return readSqliteCapturesWithCli(path, limit);
|
|
193
274
|
try {
|
|
194
|
-
|
|
275
|
+
ensureEventTypeColumn(db);
|
|
276
|
+
const rows = db.all(recentCaptureSql(Math.max(1, limit)));
|
|
195
277
|
return dedupeRows(rows).map(rowToCapture);
|
|
196
278
|
} finally {
|
|
197
279
|
db.close();
|
|
@@ -200,30 +282,86 @@ async function readSqliteCaptures(path, limit) {
|
|
|
200
282
|
return readSqliteCapturesWithCli(path, limit);
|
|
201
283
|
}
|
|
202
284
|
}
|
|
203
|
-
async function
|
|
204
|
-
if (!existsSync(path)) return void 0;
|
|
285
|
+
async function runSqlite3Json(path, sql) {
|
|
205
286
|
try {
|
|
206
|
-
const { stdout } = await execFileAsync("sqlite3", ["-json", path,
|
|
287
|
+
const { stdout } = await execFileAsync("sqlite3", ["-json", path, sql], {
|
|
207
288
|
maxBuffer: 128 * 1024 * 1024
|
|
208
289
|
});
|
|
209
|
-
|
|
210
|
-
return dedupeRows(JSON.parse(stdout)).map(rowToCapture);
|
|
290
|
+
return stdout;
|
|
211
291
|
} catch {
|
|
212
292
|
return void 0;
|
|
213
293
|
}
|
|
214
294
|
}
|
|
295
|
+
async function readSqliteCapturesWithCli(path, limit) {
|
|
296
|
+
if (!existsSync(path)) return void 0;
|
|
297
|
+
await runSqlite3Json(path, "alter table captures add column event_type text");
|
|
298
|
+
await runSqlite3Json(path, "update captures set event_type = json_extract(payload_json, '$.event.type') where kind = 'event' and event_type is null");
|
|
299
|
+
const stdout = await runSqlite3Json(path, recentCaptureSql(Math.max(1, Math.trunc(limit))));
|
|
300
|
+
if (!stdout?.trim()) return [];
|
|
301
|
+
return dedupeRows(JSON.parse(stdout)).map(rowToCapture);
|
|
302
|
+
}
|
|
303
|
+
async function readSqliteViewerCaptures(path, limit) {
|
|
304
|
+
if (!existsSync(path)) return void 0;
|
|
305
|
+
try {
|
|
306
|
+
const db = await openDatabase(path);
|
|
307
|
+
if (!db) return readSqliteViewerCapturesWithCli(path, limit);
|
|
308
|
+
try {
|
|
309
|
+
ensureEventTypeColumn(db);
|
|
310
|
+
const rows = db.all(viewerCaptureSql(Math.max(1, limit)));
|
|
311
|
+
return dedupeRows(rows).map(rowToCapture);
|
|
312
|
+
} finally {
|
|
313
|
+
db.close();
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
return readSqliteViewerCapturesWithCli(path, limit);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function readSqliteViewerCapturesWithCli(path, limit) {
|
|
320
|
+
if (!existsSync(path)) return void 0;
|
|
321
|
+
await runSqlite3Json(path, "alter table captures add column event_type text");
|
|
322
|
+
await runSqlite3Json(path, "update captures set event_type = json_extract(payload_json, '$.event.type') where kind = 'event' and event_type is null");
|
|
323
|
+
const stdout = await runSqlite3Json(path, viewerCaptureSql(Math.max(1, Math.trunc(limit))));
|
|
324
|
+
if (!stdout?.trim()) return [];
|
|
325
|
+
return dedupeRows(JSON.parse(stdout)).map(rowToCapture);
|
|
326
|
+
}
|
|
215
327
|
function recentCaptureSql(limit) {
|
|
216
328
|
return `select id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
|
|
217
329
|
from captures
|
|
218
330
|
where id in (
|
|
219
|
-
select id from captures
|
|
331
|
+
select id from captures
|
|
332
|
+
where kind in (
|
|
333
|
+
'chat.params',
|
|
334
|
+
'chat.message',
|
|
335
|
+
'chat.headers',
|
|
336
|
+
'experimental.chat.messages.transform',
|
|
337
|
+
'experimental.chat.system.transform'
|
|
338
|
+
)
|
|
339
|
+
order by timestamp desc
|
|
340
|
+
limit ${limit}
|
|
220
341
|
)
|
|
221
342
|
or id in (
|
|
343
|
+
select id from captures
|
|
344
|
+
where kind = 'event'
|
|
345
|
+
and event_type in (
|
|
346
|
+
'message.updated',
|
|
347
|
+
'message.part.updated',
|
|
348
|
+
'message.part.delta',
|
|
349
|
+
'session.updated',
|
|
350
|
+
'session.created'
|
|
351
|
+
)
|
|
352
|
+
order by timestamp desc
|
|
353
|
+
limit ${limit}
|
|
354
|
+
)
|
|
355
|
+
order by timestamp desc`;
|
|
356
|
+
}
|
|
357
|
+
function viewerCaptureSql(limit) {
|
|
358
|
+
return `select id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
|
|
359
|
+
from captures
|
|
360
|
+
where id in (
|
|
222
361
|
select id from captures
|
|
223
362
|
where kind in (
|
|
224
363
|
'chat.params',
|
|
225
364
|
'chat.message',
|
|
226
|
-
'experimental.chat.messages.transform',
|
|
227
365
|
'experimental.chat.system.transform'
|
|
228
366
|
)
|
|
229
367
|
order by timestamp desc
|
|
@@ -232,12 +370,21 @@ function recentCaptureSql(limit) {
|
|
|
232
370
|
or id in (
|
|
233
371
|
select id from captures
|
|
234
372
|
where kind = 'event'
|
|
235
|
-
and
|
|
373
|
+
and event_type in (
|
|
374
|
+
'message.updated',
|
|
375
|
+
'message.part.updated',
|
|
376
|
+
'message.part.delta',
|
|
377
|
+
'session.updated',
|
|
378
|
+
'session.created'
|
|
379
|
+
)
|
|
236
380
|
order by timestamp desc
|
|
237
381
|
limit ${limit}
|
|
238
382
|
)
|
|
239
383
|
order by timestamp desc`;
|
|
240
384
|
}
|
|
385
|
+
function isViewerCaptureKind(kind) {
|
|
386
|
+
return kind === "chat.params" || kind === "chat.message" || kind === "experimental.chat.system.transform" || kind === "event";
|
|
387
|
+
}
|
|
241
388
|
function dedupeRows(rows) {
|
|
242
389
|
const seen = /* @__PURE__ */ new Set();
|
|
243
390
|
return rows.filter((row) => {
|
|
@@ -297,18 +444,21 @@ function agentFromCapture(record, input) {
|
|
|
297
444
|
return optionalString(input.agent);
|
|
298
445
|
}
|
|
299
446
|
function messageIDForCapture(record, input) {
|
|
300
|
-
if (record.kind === "experimental.chat.messages.transform") {
|
|
301
|
-
return latestUserMessageIDFromTransform(record.payload.output) ?? record.messageID;
|
|
302
|
-
}
|
|
303
447
|
return record.messageID ?? messageIDFromPayload(input.message);
|
|
304
448
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
for (
|
|
308
|
-
|
|
309
|
-
if (
|
|
449
|
+
function attachPendingSystemTransform(pendingSystemTransforms, request) {
|
|
450
|
+
let index = -1;
|
|
451
|
+
for (let candidateIndex = pendingSystemTransforms.length - 1; candidateIndex >= 0; candidateIndex -= 1) {
|
|
452
|
+
const candidate = pendingSystemTransforms[candidateIndex];
|
|
453
|
+
if (candidate && candidate.sessionID === request.sessionID && candidate.providerID === request.providerID && candidate.modelID === request.modelID && candidate.timestamp <= request.timestamp && request.timestamp - candidate.timestamp <= 5e3) {
|
|
454
|
+
index = candidateIndex;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
310
457
|
}
|
|
311
|
-
|
|
458
|
+
if (index < 0) return;
|
|
459
|
+
const [system] = pendingSystemTransforms.splice(index, 1);
|
|
460
|
+
if (!system) return;
|
|
461
|
+
request.system = { id: system.id, timestamp: system.timestamp, payload: system.payload };
|
|
312
462
|
}
|
|
313
463
|
function latestUserMessageBefore(messages, sessionID, timestamp) {
|
|
314
464
|
let latest;
|
|
@@ -373,6 +523,12 @@ function findFirstString(value, keys) {
|
|
|
373
523
|
}
|
|
374
524
|
return void 0;
|
|
375
525
|
}
|
|
526
|
+
function projectName(path) {
|
|
527
|
+
if (!path) return void 0;
|
|
528
|
+
const normalized = path.replace(/\/+$/, "");
|
|
529
|
+
if (!normalized) return void 0;
|
|
530
|
+
return normalized.split("/").filter(Boolean).at(-1) ?? normalized;
|
|
531
|
+
}
|
|
376
532
|
function numberFromPath(value, key) {
|
|
377
533
|
if (!isRecord(value)) return void 0;
|
|
378
534
|
const item = value[key];
|
|
@@ -381,6 +537,7 @@ function numberFromPath(value, key) {
|
|
|
381
537
|
|
|
382
538
|
// src/viewer.ts
|
|
383
539
|
import { createServer } from "http";
|
|
540
|
+
var REQUEST_PATH_RE = /^\/api\/request\/(.+)$/;
|
|
384
541
|
async function serveViewer(options = {}) {
|
|
385
542
|
const host = options.host ?? "127.0.0.1";
|
|
386
543
|
const port = options.port ?? 8765;
|
|
@@ -391,6 +548,21 @@ async function serveViewer(options = {}) {
|
|
|
391
548
|
sendJson(response, history);
|
|
392
549
|
return;
|
|
393
550
|
}
|
|
551
|
+
const requestMatch = REQUEST_PATH_RE.exec(url.pathname);
|
|
552
|
+
if (requestMatch) {
|
|
553
|
+
const requestId = requestMatch[1];
|
|
554
|
+
if (!requestId) {
|
|
555
|
+
sendJson(response, { error: "not found" });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const record = await readCaptureRecord(requestId, options);
|
|
559
|
+
if (!record) {
|
|
560
|
+
sendJson(response, { error: "not found" });
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
sendJson(response, record.payload);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
394
566
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
395
567
|
sendHtml(response, renderViewerHtml(resolveCapturePath(options)));
|
|
396
568
|
return;
|
|
@@ -416,8 +588,152 @@ async function readHistory(options = {}) {
|
|
|
416
588
|
dbPath: options.dbPath,
|
|
417
589
|
limit: options.limit ?? 5e3
|
|
418
590
|
};
|
|
419
|
-
const records = await
|
|
420
|
-
|
|
591
|
+
const records = await readViewerCaptures(readOptions);
|
|
592
|
+
const history = buildRequestHistory(records);
|
|
593
|
+
prepareHistoryForViewer(history);
|
|
594
|
+
stripPayloadsForViewer(history);
|
|
595
|
+
return history;
|
|
596
|
+
}
|
|
597
|
+
function prepareHistoryForViewer(history) {
|
|
598
|
+
for (const session of history.sessions) {
|
|
599
|
+
for (const message of session.messages) {
|
|
600
|
+
const viewerMessage = message;
|
|
601
|
+
viewerMessage.visibleSteps = buildViewerVisibleSteps(message);
|
|
602
|
+
viewerMessage.hiddenContexts = buildViewerHiddenContexts(message);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function stripPayloadsForViewer(history) {
|
|
607
|
+
history.requests = history.requests.map(viewerRequestSummary);
|
|
608
|
+
for (const session of history.sessions) {
|
|
609
|
+
session.requests = [];
|
|
610
|
+
for (const message of session.messages) {
|
|
611
|
+
message.requests = [];
|
|
612
|
+
message.response = void 0;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function buildViewerVisibleSteps(message) {
|
|
617
|
+
const steps = [];
|
|
618
|
+
for (const request of (message.requests || []).filter((item) => item.agent !== "title" && item.agent !== "messages.transform")) {
|
|
619
|
+
const label = [request.agent || "agent", request.providerID, request.modelID].filter(Boolean).join(" \xB7 ");
|
|
620
|
+
const response = request.response;
|
|
621
|
+
const reasoning = response?.reasoning;
|
|
622
|
+
const text = response?.text;
|
|
623
|
+
if (reasoning) {
|
|
624
|
+
steps.push({ label: `${label} thinking`, text: reasoning });
|
|
625
|
+
}
|
|
626
|
+
for (const tool of viewerToolSteps(response)) {
|
|
627
|
+
steps.push({ label: `${label} tool`, text: tool });
|
|
628
|
+
}
|
|
629
|
+
if (text && normalizeDisplayText(text) !== normalizeDisplayText(reasoning)) {
|
|
630
|
+
steps.push({ label: `${label} response`, text });
|
|
631
|
+
}
|
|
632
|
+
if (!reasoning && !text && viewerToolSteps(response).length === 0) {
|
|
633
|
+
steps.push({ label, text: "Model step captured, but no visible thinking or response text was recorded." });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return steps;
|
|
637
|
+
}
|
|
638
|
+
function buildViewerHiddenContexts(message) {
|
|
639
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
640
|
+
for (const request of message.requests || []) {
|
|
641
|
+
const step = [request.agent || "agent", request.providerID, request.modelID].filter(Boolean).join(" \xB7 ");
|
|
642
|
+
if (request.system?.payload?.output) {
|
|
643
|
+
addHiddenContext(contexts, "System Transform Output", step, request.system.payload.output);
|
|
644
|
+
}
|
|
645
|
+
if (request.agent === "messages.transform" && request.payload) {
|
|
646
|
+
addHiddenContext(contexts, "Messages Transform Output", step, request.payload.output || request.payload);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return [...contexts.values()];
|
|
650
|
+
}
|
|
651
|
+
function addHiddenContext(contexts, title, step, value) {
|
|
652
|
+
const text = hiddenContextText(value);
|
|
653
|
+
if (!text) return;
|
|
654
|
+
const key = `${title}:${normalizeDisplayText(text)}`;
|
|
655
|
+
const existing = contexts.get(key);
|
|
656
|
+
if (existing) {
|
|
657
|
+
existing.count += 1;
|
|
658
|
+
if (!existing.step.split(", ").includes(step)) existing.step = `${existing.step}, ${step}`;
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
contexts.set(key, {
|
|
662
|
+
title,
|
|
663
|
+
step,
|
|
664
|
+
preview: previewText(text, 180),
|
|
665
|
+
text,
|
|
666
|
+
count: 1
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function hiddenContextText(value) {
|
|
670
|
+
if (typeof value === "string") return value;
|
|
671
|
+
if (Array.isArray(value)) return value.map(hiddenContextText).filter(Boolean).join("\n\n");
|
|
672
|
+
if (!isRecord2(value)) return value === void 0 || value === null ? "" : String(value);
|
|
673
|
+
if (value.system !== void 0) return hiddenContextText(value.system);
|
|
674
|
+
if (Array.isArray(value.messages)) {
|
|
675
|
+
return value.messages.map(messageContextText).filter(Boolean).join("\n\n");
|
|
676
|
+
}
|
|
677
|
+
const strings = collectStrings(value);
|
|
678
|
+
return strings.length ? strings.join("\n\n") : JSON.stringify(value, null, 2);
|
|
679
|
+
}
|
|
680
|
+
function messageContextText(value) {
|
|
681
|
+
if (!isRecord2(value)) return hiddenContextText(value);
|
|
682
|
+
const info = isRecord2(value.info) ? value.info : {};
|
|
683
|
+
const role = typeof info.role === "string" ? info.role : "message";
|
|
684
|
+
const parts = Array.isArray(value.parts) ? value.parts : [];
|
|
685
|
+
const text = parts.map(partText).filter(Boolean).join("\n");
|
|
686
|
+
return text ? `${role}: ${text}` : `${role}: ${hiddenContextText(value)}`;
|
|
687
|
+
}
|
|
688
|
+
function partText(value) {
|
|
689
|
+
if (!isRecord2(value)) return "";
|
|
690
|
+
return typeof value.text === "string" ? value.text : typeof value.content === "string" ? value.content : "";
|
|
691
|
+
}
|
|
692
|
+
function viewerToolSteps(response) {
|
|
693
|
+
const tools = [];
|
|
694
|
+
const seen = /* @__PURE__ */ new Set();
|
|
695
|
+
for (const wrapper of response?.events || []) {
|
|
696
|
+
const event = isRecord2(wrapper.event) ? wrapper.event : void 0;
|
|
697
|
+
const properties = isRecord2(event?.properties) ? event.properties : void 0;
|
|
698
|
+
const part = isRecord2(properties?.part) ? properties.part : void 0;
|
|
699
|
+
if (event?.type !== "message.part.updated" || part?.type !== "tool") continue;
|
|
700
|
+
const state = isRecord2(part.state) ? part.state : void 0;
|
|
701
|
+
const key = [part.id, part.tool, state?.status].filter(Boolean).join(":");
|
|
702
|
+
if (seen.has(key)) continue;
|
|
703
|
+
seen.add(key);
|
|
704
|
+
tools.push([part.tool || "tool", state?.status].filter(Boolean).join(" \xB7 "));
|
|
705
|
+
}
|
|
706
|
+
return tools;
|
|
707
|
+
}
|
|
708
|
+
function collectStrings(value) {
|
|
709
|
+
if (typeof value === "string") return [value];
|
|
710
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
711
|
+
if (!isRecord2(value)) return [];
|
|
712
|
+
return Object.values(value).flatMap(collectStrings);
|
|
713
|
+
}
|
|
714
|
+
function previewText(value, size) {
|
|
715
|
+
const text = normalizeDisplayText(value);
|
|
716
|
+
return text.length > size ? `${text.slice(0, size - 1)}...` : text || "(empty)";
|
|
717
|
+
}
|
|
718
|
+
function normalizeDisplayText(value) {
|
|
719
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
720
|
+
}
|
|
721
|
+
function isRecord2(value) {
|
|
722
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
723
|
+
}
|
|
724
|
+
function viewerRequestSummary(request) {
|
|
725
|
+
return {
|
|
726
|
+
id: request.id,
|
|
727
|
+
sessionID: request.sessionID,
|
|
728
|
+
messageID: request.messageID,
|
|
729
|
+
timestamp: request.timestamp,
|
|
730
|
+
agent: request.agent,
|
|
731
|
+
purpose: request.purpose,
|
|
732
|
+
providerID: request.providerID,
|
|
733
|
+
modelID: request.modelID,
|
|
734
|
+
summary: request.summary,
|
|
735
|
+
payload: {}
|
|
736
|
+
};
|
|
421
737
|
}
|
|
422
738
|
function sendJson(response, value) {
|
|
423
739
|
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
@@ -439,8 +755,11 @@ function renderViewerHtml(dbPath) {
|
|
|
439
755
|
:root {
|
|
440
756
|
color-scheme: dark;
|
|
441
757
|
--bg: #101214;
|
|
758
|
+
--header: #0d0f11;
|
|
442
759
|
--panel: #171a1d;
|
|
443
760
|
--panel-2: #20242a;
|
|
761
|
+
--surface: #0b0d0f;
|
|
762
|
+
--field: #0d0f11;
|
|
444
763
|
--line: #30363d;
|
|
445
764
|
--text: #eef2f5;
|
|
446
765
|
--muted: #9aa6b2;
|
|
@@ -448,6 +767,32 @@ function renderViewerHtml(dbPath) {
|
|
|
448
767
|
--ok: #86efac;
|
|
449
768
|
--warn: #fbbf24;
|
|
450
769
|
--bad: #fca5a5;
|
|
770
|
+
--pill-text: #0d0f11;
|
|
771
|
+
--json-key: #bae6fd;
|
|
772
|
+
--json-string: #bbf7d0;
|
|
773
|
+
--json-number: #fde68a;
|
|
774
|
+
--json-boolean: #f0abfc;
|
|
775
|
+
}
|
|
776
|
+
body[data-theme="light"] {
|
|
777
|
+
color-scheme: light;
|
|
778
|
+
--bg: #f6f7f9;
|
|
779
|
+
--header: #ffffff;
|
|
780
|
+
--panel: #ffffff;
|
|
781
|
+
--panel-2: #edf2f7;
|
|
782
|
+
--surface: #ffffff;
|
|
783
|
+
--field: #ffffff;
|
|
784
|
+
--line: #d7dde5;
|
|
785
|
+
--text: #18212f;
|
|
786
|
+
--muted: #667085;
|
|
787
|
+
--accent: #2563eb;
|
|
788
|
+
--ok: #16a34a;
|
|
789
|
+
--warn: #b45309;
|
|
790
|
+
--bad: #dc2626;
|
|
791
|
+
--pill-text: #ffffff;
|
|
792
|
+
--json-key: #1d4ed8;
|
|
793
|
+
--json-string: #15803d;
|
|
794
|
+
--json-number: #a16207;
|
|
795
|
+
--json-boolean: #9333ea;
|
|
451
796
|
}
|
|
452
797
|
* { box-sizing: border-box; }
|
|
453
798
|
body {
|
|
@@ -465,9 +810,35 @@ function renderViewerHtml(dbPath) {
|
|
|
465
810
|
gap: 16px;
|
|
466
811
|
padding: 0 18px;
|
|
467
812
|
border-bottom: 1px solid var(--line);
|
|
468
|
-
background:
|
|
813
|
+
background: var(--header);
|
|
469
814
|
}
|
|
470
815
|
h1 { margin: 0; font-size: 15px; font-weight: 700; }
|
|
816
|
+
.brand, .header-side {
|
|
817
|
+
display: flex;
|
|
818
|
+
align-items: center;
|
|
819
|
+
gap: 14px;
|
|
820
|
+
min-width: 0;
|
|
821
|
+
}
|
|
822
|
+
.header-meta { min-width: 0; }
|
|
823
|
+
.theme-toggle {
|
|
824
|
+
display: inline-flex;
|
|
825
|
+
gap: 2px;
|
|
826
|
+
padding: 3px;
|
|
827
|
+
border: 1px solid var(--line);
|
|
828
|
+
border-radius: 8px;
|
|
829
|
+
background: var(--panel);
|
|
830
|
+
}
|
|
831
|
+
.theme-toggle button {
|
|
832
|
+
padding: 5px 8px;
|
|
833
|
+
border: 0;
|
|
834
|
+
border-radius: 5px;
|
|
835
|
+
background: transparent;
|
|
836
|
+
color: var(--muted);
|
|
837
|
+
}
|
|
838
|
+
.theme-toggle button.active {
|
|
839
|
+
background: var(--panel-2);
|
|
840
|
+
color: var(--text);
|
|
841
|
+
}
|
|
471
842
|
main {
|
|
472
843
|
display: grid;
|
|
473
844
|
grid-template-columns: 300px minmax(340px, 0.95fr) minmax(460px, 1.25fr);
|
|
@@ -486,15 +857,17 @@ function renderViewerHtml(dbPath) {
|
|
|
486
857
|
border-bottom: 1px solid var(--line);
|
|
487
858
|
z-index: 2;
|
|
488
859
|
}
|
|
489
|
-
input {
|
|
860
|
+
input, select {
|
|
490
861
|
width: 100%;
|
|
491
862
|
border: 1px solid var(--line);
|
|
492
|
-
background:
|
|
863
|
+
background: var(--field);
|
|
493
864
|
color: var(--text);
|
|
494
865
|
border-radius: 6px;
|
|
495
866
|
padding: 8px 10px;
|
|
496
867
|
font: inherit;
|
|
497
868
|
}
|
|
869
|
+
select { cursor: pointer; }
|
|
870
|
+
.toolbar.stack { flex-direction: column; }
|
|
498
871
|
button {
|
|
499
872
|
border: 1px solid var(--line);
|
|
500
873
|
background: var(--panel-2);
|
|
@@ -517,7 +890,7 @@ function renderViewerHtml(dbPath) {
|
|
|
517
890
|
padding: 11px 12px;
|
|
518
891
|
}
|
|
519
892
|
.item:hover, .item.active { background: var(--panel-2); }
|
|
520
|
-
.
|
|
893
|
+
.session-child { padding-left: 28px; }
|
|
521
894
|
.title { color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
522
895
|
.meta {
|
|
523
896
|
margin-top: 4px;
|
|
@@ -528,7 +901,7 @@ function renderViewerHtml(dbPath) {
|
|
|
528
901
|
}
|
|
529
902
|
.muted { color: var(--muted); }
|
|
530
903
|
.pill {
|
|
531
|
-
color:
|
|
904
|
+
color: var(--pill-text);
|
|
532
905
|
background: var(--accent);
|
|
533
906
|
border-radius: 999px;
|
|
534
907
|
padding: 1px 6px;
|
|
@@ -548,7 +921,7 @@ function renderViewerHtml(dbPath) {
|
|
|
548
921
|
overflow: auto;
|
|
549
922
|
white-space: pre-wrap;
|
|
550
923
|
word-break: break-word;
|
|
551
|
-
background:
|
|
924
|
+
background: var(--surface);
|
|
552
925
|
border: 1px solid var(--line);
|
|
553
926
|
border-radius: 8px;
|
|
554
927
|
min-height: 220px;
|
|
@@ -567,14 +940,45 @@ function renderViewerHtml(dbPath) {
|
|
|
567
940
|
color: var(--text);
|
|
568
941
|
font-weight: 700;
|
|
569
942
|
}
|
|
943
|
+
.step {
|
|
944
|
+
margin: 0 0 10px;
|
|
945
|
+
padding: 12px;
|
|
946
|
+
border: 1px solid var(--line);
|
|
947
|
+
border-radius: 8px;
|
|
948
|
+
background: var(--surface);
|
|
949
|
+
}
|
|
950
|
+
.step-label {
|
|
951
|
+
color: var(--accent);
|
|
952
|
+
font-weight: 700;
|
|
953
|
+
margin-bottom: 6px;
|
|
954
|
+
}
|
|
955
|
+
.step-text { white-space: pre-wrap; word-break: break-word; }
|
|
956
|
+
details.hidden-context {
|
|
957
|
+
margin: 10px 0;
|
|
958
|
+
border: 1px solid var(--line);
|
|
959
|
+
border-radius: 8px;
|
|
960
|
+
background: var(--surface);
|
|
961
|
+
}
|
|
962
|
+
details.hidden-context summary {
|
|
963
|
+
cursor: pointer;
|
|
964
|
+
padding: 10px 12px;
|
|
965
|
+
color: var(--accent);
|
|
966
|
+
}
|
|
967
|
+
.hidden-body { padding: 0 12px 12px; }
|
|
968
|
+
.hidden-text {
|
|
969
|
+
margin: 0;
|
|
970
|
+
white-space: pre-wrap;
|
|
971
|
+
word-break: break-word;
|
|
972
|
+
color: var(--text);
|
|
973
|
+
}
|
|
570
974
|
.json-tree { font-size: 12px; line-height: 1.55; }
|
|
571
975
|
.json-tree details { margin-left: 14px; }
|
|
572
976
|
.json-tree summary { cursor: pointer; color: var(--accent); }
|
|
573
977
|
.json-tree .leaf { margin-left: 14px; }
|
|
574
|
-
.json-key { color:
|
|
575
|
-
.json-string { color:
|
|
576
|
-
.json-number { color:
|
|
577
|
-
.json-boolean { color:
|
|
978
|
+
.json-key { color: var(--json-key); }
|
|
979
|
+
.json-string { color: var(--json-string); }
|
|
980
|
+
.json-number { color: var(--json-number); }
|
|
981
|
+
.json-boolean { color: var(--json-boolean); }
|
|
578
982
|
.json-null { color: var(--muted); }
|
|
579
983
|
@media (max-width: 1000px) {
|
|
580
984
|
main { grid-template-columns: 1fr; height: auto; }
|
|
@@ -582,38 +986,44 @@ function renderViewerHtml(dbPath) {
|
|
|
582
986
|
}
|
|
583
987
|
</style>
|
|
584
988
|
</head>
|
|
585
|
-
<body>
|
|
989
|
+
<body data-theme="dark">
|
|
586
990
|
<header>
|
|
587
|
-
<
|
|
588
|
-
|
|
589
|
-
<div class="
|
|
590
|
-
|
|
991
|
+
<div class="brand">
|
|
992
|
+
<h1>OpenCode Insights</h1>
|
|
993
|
+
<div id="theme-toggle" class="theme-toggle" aria-label="Theme">
|
|
994
|
+
<button type="button" data-theme-option="dark">Dark</button>
|
|
995
|
+
<button type="button" data-theme-option="light">Light</button>
|
|
996
|
+
</div>
|
|
997
|
+
</div>
|
|
998
|
+
<div class="header-side">
|
|
999
|
+
<div class="header-meta">
|
|
1000
|
+
<div class="meta">${escapedDbPath}</div>
|
|
1001
|
+
<div id="status" class="status">Loading history...</div>
|
|
1002
|
+
</div>
|
|
591
1003
|
</div>
|
|
592
1004
|
</header>
|
|
593
1005
|
<main>
|
|
594
1006
|
<section>
|
|
595
|
-
<div class="toolbar
|
|
1007
|
+
<div class="toolbar stack">
|
|
1008
|
+
<select id="project-filter"><option value="">All projects</option></select>
|
|
1009
|
+
<input id="session-filter" placeholder="Filter sessions">
|
|
1010
|
+
</div>
|
|
596
1011
|
<div id="sessions"></div>
|
|
597
1012
|
</section>
|
|
598
1013
|
<section>
|
|
599
|
-
<div class="toolbar"><input id="timeline-filter" placeholder="Filter
|
|
1014
|
+
<div class="toolbar"><input id="timeline-filter" placeholder="Filter conversation"></div>
|
|
600
1015
|
<div id="timeline"></div>
|
|
601
1016
|
</section>
|
|
602
1017
|
<section>
|
|
603
1018
|
<div class="toolbar">
|
|
604
|
-
<button id="copy">Copy
|
|
1019
|
+
<button id="copy">Copy Summary</button>
|
|
605
1020
|
<button id="refresh">Refresh</button>
|
|
606
1021
|
</div>
|
|
607
1022
|
<div class="detail">
|
|
608
1023
|
<div class="tabs">
|
|
609
1024
|
<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
1025
|
</div>
|
|
616
|
-
<div id="detail" class="panel">Select a message
|
|
1026
|
+
<div id="detail" class="panel">Select a user message.</div>
|
|
617
1027
|
</div>
|
|
618
1028
|
</section>
|
|
619
1029
|
</main>
|
|
@@ -624,11 +1034,14 @@ function renderViewerHtml(dbPath) {
|
|
|
624
1034
|
messageID: null,
|
|
625
1035
|
requestID: null,
|
|
626
1036
|
tab: "summary",
|
|
1037
|
+
project: "",
|
|
627
1038
|
loading: true,
|
|
628
1039
|
error: null,
|
|
629
|
-
loadedMs: 0
|
|
1040
|
+
loadedMs: 0,
|
|
1041
|
+
payloadCache: {}
|
|
630
1042
|
};
|
|
631
1043
|
|
|
1044
|
+
const THEME_KEY = "opencode-insights-theme";
|
|
632
1045
|
const qs = (id) => document.getElementById(id);
|
|
633
1046
|
const fmt = (ms) => ms ? new Date(ms).toLocaleString() : "-";
|
|
634
1047
|
const short = (value, size = 90) => {
|
|
@@ -636,11 +1049,20 @@ function renderViewerHtml(dbPath) {
|
|
|
636
1049
|
return text.length > size ? text.slice(0, size - 1) + "..." : text;
|
|
637
1050
|
};
|
|
638
1051
|
|
|
1052
|
+
function applyTheme(theme) {
|
|
1053
|
+
const next = theme === "light" ? "light" : "dark";
|
|
1054
|
+
document.body.dataset.theme = next;
|
|
1055
|
+
localStorage.setItem(THEME_KEY, next);
|
|
1056
|
+
for (const item of document.querySelectorAll("[data-theme-option]")) {
|
|
1057
|
+
item.classList.toggle("active", item.dataset.themeOption === next);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
639
1061
|
async function load() {
|
|
640
1062
|
state.loading = true;
|
|
641
1063
|
state.error = null;
|
|
642
1064
|
renderStatus();
|
|
643
|
-
qs("sessions").innerHTML = '<div class="empty">Loading sessions
|
|
1065
|
+
qs("sessions").innerHTML = '<div class="empty">Loading sessions...</div>';
|
|
644
1066
|
qs("timeline").innerHTML = '<div class="empty">Waiting for history data...</div>';
|
|
645
1067
|
try {
|
|
646
1068
|
const started = performance.now();
|
|
@@ -654,8 +1076,9 @@ function renderViewerHtml(dbPath) {
|
|
|
654
1076
|
state.messageID = null;
|
|
655
1077
|
state.requestID = null;
|
|
656
1078
|
}
|
|
1079
|
+
renderProjectOptions();
|
|
657
1080
|
const session = selectedSession();
|
|
658
|
-
if (session && !state.messageID
|
|
1081
|
+
if (session && !state.messageID) state.messageID = firstUserMessage(session)?.id || null;
|
|
659
1082
|
} catch (error) {
|
|
660
1083
|
state.error = error instanceof Error ? error.message : String(error);
|
|
661
1084
|
} finally {
|
|
@@ -673,17 +1096,8 @@ function renderViewerHtml(dbPath) {
|
|
|
673
1096
|
return session?.messages.find((message) => message.id === state.messageID) || null;
|
|
674
1097
|
}
|
|
675
1098
|
|
|
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 };
|
|
1099
|
+
function firstUserMessage(session) {
|
|
1100
|
+
return session?.messages.find((message) => message.role === "user") || session?.messages[0] || null;
|
|
687
1101
|
}
|
|
688
1102
|
|
|
689
1103
|
function render() {
|
|
@@ -704,33 +1118,67 @@ function renderViewerHtml(dbPath) {
|
|
|
704
1118
|
status.classList.toggle("error", Boolean(state.error));
|
|
705
1119
|
if (state.error) status.textContent = "Load failed";
|
|
706
1120
|
else if (state.loading) status.textContent = "Loading history...";
|
|
707
|
-
else status.textContent = state.history.sessions.length + " sessions \xB7 " + state.history.requests.length + "
|
|
1121
|
+
else status.textContent = state.history.sessions.length + " sessions \xB7 " + state.history.requests.length + " model steps \xB7 " + state.loadedMs + "ms";
|
|
708
1122
|
qs("refresh").disabled = state.loading;
|
|
709
1123
|
qs("refresh").textContent = state.loading ? "Loading..." : "Refresh";
|
|
710
1124
|
}
|
|
711
1125
|
|
|
1126
|
+
function renderProjectOptions() {
|
|
1127
|
+
const select = qs("project-filter");
|
|
1128
|
+
const current = state.project;
|
|
1129
|
+
const projects = [...new Set(state.history.sessions.map((session) => session.project || session.cwd || "Unknown").filter(Boolean))].sort();
|
|
1130
|
+
select.innerHTML = '<option value="">All projects</option>' + projects.map((project) =>
|
|
1131
|
+
'<option value="' + escapeAttr(project) + '">' + escapeHtml(project) + '</option>'
|
|
1132
|
+
).join("");
|
|
1133
|
+
if (projects.includes(current)) select.value = current;
|
|
1134
|
+
else state.project = "";
|
|
1135
|
+
}
|
|
1136
|
+
|
|
712
1137
|
function renderSessions() {
|
|
713
1138
|
const filter = qs("session-filter").value.toLowerCase();
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1139
|
+
const project = state.project;
|
|
1140
|
+
const all = state.history.sessions;
|
|
1141
|
+
const byParent = new Map();
|
|
1142
|
+
const byId = new Map(all.map((session) => [session.id, session]));
|
|
1143
|
+
for (const session of all) {
|
|
1144
|
+
const parent = session.parentID && byId.has(session.parentID) ? session.parentID : "";
|
|
1145
|
+
const list = byParent.get(parent) || [];
|
|
1146
|
+
list.push(session);
|
|
1147
|
+
byParent.set(parent, list);
|
|
1148
|
+
}
|
|
1149
|
+
for (const list of byParent.values()) list.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
1150
|
+
const matches = (session) => {
|
|
1151
|
+
const projectMatch = !project || (session.project || session.cwd || "Unknown") === project;
|
|
1152
|
+
const text = [session.id, session.title, session.project, session.cwd].filter(Boolean).join(" ").toLowerCase();
|
|
1153
|
+
return projectMatch && (!filter || text.includes(filter));
|
|
1154
|
+
};
|
|
1155
|
+
const renderNode = (session, depth = 0) => {
|
|
1156
|
+
const children = byParent.get(session.id) || [];
|
|
1157
|
+
const childHtml = children.map((child) => renderNode(child, depth + 1)).join("");
|
|
1158
|
+
if (!matches(session) && !childHtml) return "";
|
|
1159
|
+
const cls = "item" + (depth > 0 ? " session-child" : "") + (session.id === state.sessionID ? " active" : "");
|
|
1160
|
+
return '<button class="' + cls + '" data-session="' + escapeAttr(session.id) + '">' +
|
|
1161
|
+
'<div class="title">' + (depth > 0 ? "sub: " : "") + escapeHtml(session.title || session.id) + '</div>' +
|
|
1162
|
+
'<div class="meta">' + escapeHtml(session.project || session.cwd || "Unknown project") + ' \xB7 ' + userMessages(session).length + ' messages \xB7 ' + fmt(session.updatedAt) + '</div>' +
|
|
1163
|
+
'</button>' + childHtml;
|
|
1164
|
+
};
|
|
1165
|
+
const html = (byParent.get("") || []).map((session) => renderNode(session)).join("");
|
|
1166
|
+
qs("sessions").innerHTML = html || '<div class="empty">No sessions found.</div>';
|
|
723
1167
|
for (const item of document.querySelectorAll("[data-session]")) {
|
|
724
1168
|
item.onclick = () => {
|
|
725
1169
|
state.sessionID = item.dataset.session;
|
|
726
1170
|
const session = selectedSession();
|
|
727
|
-
state.messageID = session?.
|
|
1171
|
+
state.messageID = firstUserMessage(session)?.id || null;
|
|
728
1172
|
state.requestID = null;
|
|
729
1173
|
render();
|
|
730
1174
|
};
|
|
731
1175
|
}
|
|
732
1176
|
}
|
|
733
1177
|
|
|
1178
|
+
function userMessages(session) {
|
|
1179
|
+
return (session?.messages || []).filter((message) => message.role === "user");
|
|
1180
|
+
}
|
|
1181
|
+
|
|
734
1182
|
function renderTimeline() {
|
|
735
1183
|
const session = selectedSession();
|
|
736
1184
|
if (!session) {
|
|
@@ -739,79 +1187,71 @@ function renderViewerHtml(dbPath) {
|
|
|
739
1187
|
}
|
|
740
1188
|
const filter = qs("timeline-filter").value.toLowerCase();
|
|
741
1189
|
const blocks = [];
|
|
742
|
-
for (const message of session
|
|
1190
|
+
for (const message of userMessages(session)) {
|
|
743
1191
|
const searchable = JSON.stringify(message).toLowerCase();
|
|
744
1192
|
if (filter && !searchable.includes(filter)) continue;
|
|
745
1193
|
blocks.push(
|
|
746
|
-
'<button class="item ' + (message.id === state.messageID
|
|
747
|
-
'<div class="title"><span class="pill msg">
|
|
748
|
-
'<div class="meta">' + message.
|
|
1194
|
+
'<button class="item ' + (message.id === state.messageID ? "active" : "") + '" data-message="' + escapeAttr(message.id) + '">' +
|
|
1195
|
+
'<div class="title"><span class="pill msg">USER</span> ' + escapeHtml(short(message.text || message.id, 130)) + '</div>' +
|
|
1196
|
+
'<div class="meta">' + visibleSteps(message).length + ' visible steps \xB7 ' + hiddenContexts(message).length + ' hidden context items \xB7 ' + fmt(message.createdAt) + '</div>' +
|
|
749
1197
|
'</button>'
|
|
750
1198
|
);
|
|
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
1199
|
}
|
|
760
|
-
qs("timeline").innerHTML = blocks.length ? blocks.join("") : '<div class="empty">No messages
|
|
1200
|
+
qs("timeline").innerHTML = blocks.length ? blocks.join("") : '<div class="empty">No user messages found.</div>';
|
|
761
1201
|
for (const item of document.querySelectorAll("[data-message]")) {
|
|
762
1202
|
item.onclick = () => {
|
|
763
1203
|
state.messageID = item.dataset.message;
|
|
764
|
-
state.requestID =
|
|
1204
|
+
state.requestID = null;
|
|
765
1205
|
render();
|
|
766
1206
|
};
|
|
767
1207
|
}
|
|
768
1208
|
}
|
|
769
1209
|
|
|
770
1210
|
function renderDetail() {
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
qs("detail").textContent = "Select a message or request.";
|
|
1211
|
+
const message = selectedMessage();
|
|
1212
|
+
if (!message) {
|
|
1213
|
+
qs("detail").textContent = "Select a user message.";
|
|
775
1214
|
return;
|
|
776
1215
|
}
|
|
777
1216
|
|
|
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
|
-
}
|
|
1217
|
+
const session = selectedSession();
|
|
1218
|
+
const steps = visibleSteps(message);
|
|
1219
|
+
const hidden = hiddenContexts(message);
|
|
1220
|
+
qs("detail").innerHTML =
|
|
1221
|
+
'<div class="kv">' +
|
|
1222
|
+
kv("Session", session?.title || session?.id || "-") +
|
|
1223
|
+
kv("Project", session?.project || session?.cwd || "Unknown") +
|
|
1224
|
+
kv("User message", message.text || "(no user text captured)") +
|
|
1225
|
+
kv("Visible steps", String(steps.length)) +
|
|
1226
|
+
kv("Hidden context", hidden.length ? hidden.length + " item(s)" : "none captured") +
|
|
1227
|
+
'</div>' +
|
|
1228
|
+
'<div class="subhead">Agent Thinking / Response Sequence</div>' +
|
|
1229
|
+
(steps.length ? steps.map(renderStep).join("") : '<div class="empty">No assistant thinking or response text captured for this message.</div>') +
|
|
1230
|
+
'<div class="subhead">Hidden Context</div>' +
|
|
1231
|
+
(hidden.length ? hidden.map(renderHiddenContext).join("") : '<div class="empty">No system prompt or hidden prompt-like context captured.</div>');
|
|
1232
|
+
}
|
|
797
1233
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
1234
|
+
function visibleSteps(message) {
|
|
1235
|
+
return message.visibleSteps || [];
|
|
1236
|
+
}
|
|
802
1237
|
|
|
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
|
-
}
|
|
1238
|
+
function hiddenContexts(message) {
|
|
1239
|
+
return message.hiddenContexts || [];
|
|
1240
|
+
}
|
|
813
1241
|
|
|
814
|
-
|
|
1242
|
+
function renderStep(step) {
|
|
1243
|
+
return '<div class="step">' +
|
|
1244
|
+
'<div class="step-label">' + escapeHtml(step.label) + '</div>' +
|
|
1245
|
+
'<div class="step-text">' + escapeHtml(step.text) + '</div>' +
|
|
1246
|
+
'</div>';
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function renderHiddenContext(item) {
|
|
1250
|
+
const count = item.count > 1 ? ' \xB7 used by ' + item.count + ' model steps' : "";
|
|
1251
|
+
return '<details class="hidden-context">' +
|
|
1252
|
+
'<summary>' + escapeHtml(item.title) + ' \xB7 ' + escapeHtml(item.step || "-") + count + ' \xB7 ' + escapeHtml(item.preview) + '</summary>' +
|
|
1253
|
+
'<div class="hidden-body"><pre class="hidden-text">' + escapeHtml(item.text) + '</pre></div>' +
|
|
1254
|
+
'</details>';
|
|
815
1255
|
}
|
|
816
1256
|
|
|
817
1257
|
function kv(key, value) {
|
|
@@ -827,12 +1267,22 @@ function renderViewerHtml(dbPath) {
|
|
|
827
1267
|
renderJson({ status: "missing", note: "Select a hook row to inspect hook details." });
|
|
828
1268
|
return;
|
|
829
1269
|
}
|
|
830
|
-
const
|
|
1270
|
+
const cached = state.payloadCache[request.id];
|
|
1271
|
+
if (cached === undefined || cached === null) {
|
|
1272
|
+
qs("detail").innerHTML = '<div class="empty">Loading hook payload...</div>';
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (cached.error) {
|
|
1276
|
+
renderJson({ error: cached.error });
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const payload = cached;
|
|
831
1280
|
const hookInput = payload.input || {};
|
|
832
1281
|
const hookOutput = payload.output || {};
|
|
1282
|
+
const systemOutput = request.system?.payload?.output || null;
|
|
833
1283
|
const headerOutput = request.headers?.payload?.output || null;
|
|
834
1284
|
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>' +
|
|
1285
|
+
'<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
1286
|
'<div class="kv">' +
|
|
837
1287
|
kv("Hook id", request.id) +
|
|
838
1288
|
kv("Agent", request.agent || "-") +
|
|
@@ -845,10 +1295,12 @@ function renderViewerHtml(dbPath) {
|
|
|
845
1295
|
'<div class="json-tree">' + jsonNode(summarizeHookInput(hookInput), "hookInput", true) + '</div>' +
|
|
846
1296
|
'<div class="subhead">Hook Output: model-call settings</div>' +
|
|
847
1297
|
'<div class="json-tree">' + jsonNode(hookOutput, "hookOutput", true) + '</div>' +
|
|
1298
|
+
'<div class="subhead">System Transform Output</div>' +
|
|
1299
|
+
'<div class="json-tree">' + jsonNode(systemOutput, "systemOutput", true) + '</div>' +
|
|
848
1300
|
'<div class="subhead">Headers Hook Output</div>' +
|
|
849
1301
|
'<div class="json-tree">' + jsonNode(headerOutput, "headersOutput", true) + '</div>' +
|
|
850
1302
|
'<div class="subhead">Raw Full-Fidelity Payload</div>' +
|
|
851
|
-
'<div class="json-tree">' + jsonNode({
|
|
1303
|
+
'<div class="json-tree">' + jsonNode({ system: request.system?.payload || null, params: payload, headers: request.headers?.payload || null }, "raw", false) + '</div>';
|
|
852
1304
|
}
|
|
853
1305
|
|
|
854
1306
|
function summarizeHookInput(input) {
|
|
@@ -915,16 +1367,23 @@ function renderViewerHtml(dbPath) {
|
|
|
915
1367
|
}
|
|
916
1368
|
|
|
917
1369
|
qs("session-filter").oninput = render;
|
|
1370
|
+
qs("project-filter").onchange = () => {
|
|
1371
|
+
state.project = qs("project-filter").value;
|
|
1372
|
+
render();
|
|
1373
|
+
};
|
|
918
1374
|
qs("timeline-filter").oninput = render;
|
|
919
1375
|
qs("refresh").onclick = load;
|
|
920
|
-
|
|
921
|
-
|
|
1376
|
+
for (const item of document.querySelectorAll("[data-theme-option]")) {
|
|
1377
|
+
item.onclick = () => applyTheme(item.dataset.themeOption);
|
|
1378
|
+
}
|
|
922
1379
|
qs("copy").onclick = async () => {
|
|
923
|
-
const
|
|
924
|
-
const value =
|
|
925
|
-
:
|
|
926
|
-
:
|
|
927
|
-
:
|
|
1380
|
+
const message = selectedMessage();
|
|
1381
|
+
const value = message ? {
|
|
1382
|
+
messageID: message.id,
|
|
1383
|
+
text: message.text,
|
|
1384
|
+
steps: visibleSteps(message),
|
|
1385
|
+
hiddenContext: hiddenContexts(message)
|
|
1386
|
+
} : null;
|
|
928
1387
|
await navigator.clipboard.writeText(JSON.stringify(value, null, 2));
|
|
929
1388
|
};
|
|
930
1389
|
for (const tab of document.querySelectorAll("[data-tab]")) {
|
|
@@ -934,6 +1393,7 @@ function renderViewerHtml(dbPath) {
|
|
|
934
1393
|
renderDetail();
|
|
935
1394
|
};
|
|
936
1395
|
}
|
|
1396
|
+
applyTheme(localStorage.getItem(THEME_KEY));
|
|
937
1397
|
load();
|
|
938
1398
|
</script>
|
|
939
1399
|
</body>
|
|
@@ -1090,10 +1550,16 @@ function parseOptions(args) {
|
|
|
1090
1550
|
const value = args[index + 1];
|
|
1091
1551
|
if (value) options.configDir = value;
|
|
1092
1552
|
index += 1;
|
|
1553
|
+
} else if (arg === "--retention-days") {
|
|
1554
|
+
options.retentionDays = Number.parseFloat(args[index + 1] ?? "");
|
|
1555
|
+
index += 1;
|
|
1093
1556
|
}
|
|
1094
1557
|
}
|
|
1095
1558
|
if (!Number.isFinite(options.limit) || options.limit < 1) options.limit = DEFAULT_RECENT_LIMIT;
|
|
1096
1559
|
if (options.port !== void 0 && (!Number.isFinite(options.port) || options.port < 1)) options.port = 8765;
|
|
1560
|
+
if (options.retentionDays !== void 0 && (!Number.isFinite(options.retentionDays) || options.retentionDays < 0)) {
|
|
1561
|
+
options.retentionDays = void 0;
|
|
1562
|
+
}
|
|
1097
1563
|
return options;
|
|
1098
1564
|
}
|
|
1099
1565
|
function parsePositionals(args) {
|
|
@@ -1102,7 +1568,7 @@ function parsePositionals(args) {
|
|
|
1102
1568
|
const arg = args[index];
|
|
1103
1569
|
if (!arg) continue;
|
|
1104
1570
|
if (arg.startsWith("--")) {
|
|
1105
|
-
if (["--db", "--data-dir", "--limit", "--host", "--port", "--output", "--config-dir"].includes(arg)) index += 1;
|
|
1571
|
+
if (["--db", "--data-dir", "--limit", "--host", "--port", "--output", "--config-dir", "--retention-days"].includes(arg)) index += 1;
|
|
1106
1572
|
continue;
|
|
1107
1573
|
}
|
|
1108
1574
|
if (arg === "-o") {
|
|
@@ -1234,7 +1700,7 @@ async function configureOpenCodeDebug(options) {
|
|
|
1234
1700
|
}
|
|
1235
1701
|
const opencodeConfig = await readJsonConfig(opencodePath, { plugin: [] });
|
|
1236
1702
|
const tuiConfig = await readJsonConfig(tuiPath, { plugin: [] });
|
|
1237
|
-
setSinglePluginSpec(opencodeConfig, SERVER_PLUGIN_SPEC, localServerEntry);
|
|
1703
|
+
setSinglePluginSpec(opencodeConfig, SERVER_PLUGIN_SPEC, [localServerEntry, debugServerOptions(options)], localServerEntry);
|
|
1238
1704
|
setSinglePluginSpec(tuiConfig, TUI_PLUGIN_SPEC, localTuiEntry);
|
|
1239
1705
|
removePlugin(tuiConfig, SUBPATH_TUI_PLUGIN_SPEC);
|
|
1240
1706
|
const lines = [
|
|
@@ -1323,6 +1789,11 @@ function stripJsonCommentsAndTrailingCommas(input) {
|
|
|
1323
1789
|
function isJsonObject(value) {
|
|
1324
1790
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1325
1791
|
}
|
|
1792
|
+
function debugServerOptions(options) {
|
|
1793
|
+
const serverOptions = {};
|
|
1794
|
+
if (options.retentionDays !== void 0) serverOptions.retentionDays = options.retentionDays;
|
|
1795
|
+
return serverOptions;
|
|
1796
|
+
}
|
|
1326
1797
|
function addUniquePlugin(config, plugin) {
|
|
1327
1798
|
const current = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1328
1799
|
if (current.includes(plugin)) {
|
|
@@ -1341,9 +1812,10 @@ function removePlugin(config, plugin) {
|
|
|
1341
1812
|
function isPluginEntry(entry, plugin) {
|
|
1342
1813
|
return entry === plugin || Array.isArray(entry) && entry[0] === plugin;
|
|
1343
1814
|
}
|
|
1344
|
-
function setSinglePluginSpec(config, previousPlugin, nextPlugin) {
|
|
1815
|
+
function setSinglePluginSpec(config, previousPlugin, nextPlugin, nextPluginSpec) {
|
|
1345
1816
|
const current = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1346
|
-
const
|
|
1817
|
+
const localPlugin = nextPluginSpec ?? (typeof nextPlugin === "string" ? nextPlugin : "");
|
|
1818
|
+
const next = current.filter((entry) => !isInsightsPluginEntry(entry, previousPlugin, localPlugin));
|
|
1347
1819
|
config.plugin = [...next, nextPlugin];
|
|
1348
1820
|
}
|
|
1349
1821
|
function isInsightsPluginEntry(entry, packagePlugin, localPlugin) {
|
|
@@ -1411,7 +1883,7 @@ async function writeJsonConfig(path, config) {
|
|
|
1411
1883
|
function usage() {
|
|
1412
1884
|
return [
|
|
1413
1885
|
"Usage:",
|
|
1414
|
-
" opencode-insights debug [--config-dir DIR] [--dry-run]",
|
|
1886
|
+
" opencode-insights debug [--config-dir DIR] [--retention-days DAYS] [--dry-run]",
|
|
1415
1887
|
" opencode-insights uninstall [--config-dir DIR] [--db PATH] [--data-dir DIR] [--keep-data] [--dry-run]",
|
|
1416
1888
|
" opencode-insights recent [--db PATH] [--data-dir DIR] [--limit N] [--json]",
|
|
1417
1889
|
" opencode-insights sessions [--db PATH] [--data-dir DIR] [--limit N] [--json]",
|