@rejacky/opencode-insights 0.1.13 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -12
- package/dist/{capture-BpktoFGh.d.ts → capture-CwesXqmX.d.ts} +5 -1
- package/dist/{chunk-RWPY5QOE.js → chunk-RZGCLQ2M.js} +50 -12
- package/dist/{chunk-O5FFLPEQ.js → chunk-SGXZJVYX.js} +242 -8
- package/dist/cli.d.ts +4 -3
- package/dist/cli.js +110 -44
- package/dist/index.d.ts +25 -2
- package/dist/index.js +8 -3
- package/dist/tui.js +262 -11
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -239,24 +239,30 @@ If SQLite is unavailable in the plugin runtime, the fallback path is:
|
|
|
239
239
|
~/.opencode-insights/insights.sqlite.jsonl
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
The database keeps one day of captures by default and auto-cleans older rows on
|
|
242
|
+
The database keeps one day of captures by default and auto-cleans older rows on
|
|
243
|
+
startup and after new captures; `retentionDays` sets how many days to keep (`0`
|
|
244
|
+
disables auto-cleaning).
|
|
243
245
|
|
|
244
|
-
|
|
246
|
+
Storage settings live in the config file `~/.opencode-insights/config.jsonc` (JSONC —
|
|
247
|
+
comments allowed). The plugin creates it on first run with `dbPath` commented out
|
|
248
|
+
(uncomment to relocate the database) and `retentionDays` defaulting to 1:
|
|
245
249
|
|
|
246
|
-
```
|
|
250
|
+
```jsonc
|
|
247
251
|
{
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
"retentionDays": 1
|
|
254
|
-
}
|
|
255
|
-
]
|
|
256
|
-
]
|
|
252
|
+
// Database file. Default: ~/.opencode-insights/insights.sqlite
|
|
253
|
+
// "dbPath": "/absolute/path/to/insights.sqlite",
|
|
254
|
+
"retentionDays": 1,
|
|
255
|
+
"promptRightMetrics": ["tps", "avg", "used", "cache"],
|
|
256
|
+
"goUsage": { "enabled": false, "cookie": "", "workspaceID": "", "refreshMs": 300000 }
|
|
257
257
|
}
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
+
A legacy `config.json` is still honored when `config.jsonc` does not exist. Plugin
|
|
261
|
+
params such as `{ "dbPath": … }` in `opencode.json` are ignored after the upgrade —
|
|
262
|
+
if you previously set `dbPath` or `retentionDays` there, copy those values into
|
|
263
|
+
`~/.opencode-insights/config.jsonc`. CLI commands read the configured database path
|
|
264
|
+
from this file; the former `--db`/`--data-dir`/`--retention-days` flags are removed.
|
|
265
|
+
|
|
260
266
|
## Privacy Model
|
|
261
267
|
|
|
262
268
|
This plugin intentionally does not redact anything. It stores data locally exactly as OpenCode exposes it to plugin hooks and events.
|
|
@@ -110,11 +110,15 @@ type GoUsageConfig = {
|
|
|
110
110
|
type InsightsConfig = {
|
|
111
111
|
promptRightMetrics: PromptRightMetric[];
|
|
112
112
|
goUsage: GoUsageConfig;
|
|
113
|
+
dbPath?: string | undefined;
|
|
114
|
+
retentionDays?: number | undefined;
|
|
113
115
|
};
|
|
114
116
|
declare function defaultDataDir(): string;
|
|
115
117
|
declare function resolveCapturePath(options?: InsightsOptions): string;
|
|
116
118
|
declare function resolveInsightsConfigPath(options?: InsightsOptions): string;
|
|
119
|
+
declare function resolveLegacyInsightsConfigPath(options?: InsightsOptions): string;
|
|
117
120
|
declare function readInsightsConfig(options?: InsightsOptions): Promise<InsightsConfig>;
|
|
121
|
+
declare function insightsOptionsFromConfig(config: InsightsConfig, dataDir?: string): InsightsOptions;
|
|
118
122
|
declare function normalizeChatMessageCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
|
|
119
123
|
declare function normalizeChatParamsCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
|
|
120
124
|
declare function normalizeChatHeadersCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
|
|
@@ -152,4 +156,4 @@ declare class SqliteCaptureStore implements CaptureStore {
|
|
|
152
156
|
declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
|
|
153
157
|
declare function resolveRetentionDays(value: unknown): number;
|
|
154
158
|
|
|
155
|
-
export { type AssistantResponseUsage as A,
|
|
159
|
+
export { type AssistantResponseUsage as A, recordToolActivity as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, renderMetricsText as E, renderPromptRightMetricsText as F, type GoUsageConfig as G, renderResponseMetricsText as H, type InsightsConfig as I, JsonlCaptureStore as J, renderSessionTokenUsage as K, resolveCapturePath as L, type MessageTiming as M, resolveInsightsConfigPath as N, resolveLegacyInsightsConfigPath as O, type PromptRightMetric as P, resolveRetentionDays as Q, type SessionAverage as S, type CaptureKind as a, type CaptureStore as b, type InsightsOptions as c, type MetricsState as d, type SessionTokenUsage as e, SqliteCaptureStore as f, type SqliteDb as g, type StreamSample as h, createCaptureStore as i, createMetricsState as j, defaultDataDir as k, estimateStreamTokens as l, extractEventType as m, getSessionTokenUsage as n, insightsOptionsFromConfig as o, normalizeChatHeadersCapture as p, normalizeChatMessageCapture as q, normalizeChatParamsCapture as r, normalizeEventCapture as s, normalizeExperimentalChatMessagesTransformCapture as t, normalizeExperimentalChatSystemTransformCapture as u, normalizeToolCapture as v, openDatabase as w, readInsightsConfig as x, recordAssistantDelta as y, recordAssistantMessage as z };
|
|
@@ -266,6 +266,7 @@ import { mkdir, appendFile, readFile, writeFile } from "fs/promises";
|
|
|
266
266
|
import { existsSync, readFileSync } from "fs";
|
|
267
267
|
import { dirname, join } from "path";
|
|
268
268
|
import { homedir } from "os";
|
|
269
|
+
import { parse } from "jsonc-parser";
|
|
269
270
|
var DEFAULT_RETENTION_DAYS = 1;
|
|
270
271
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
271
272
|
var DEFAULT_GO_USAGE_REFRESH_MS = 3e5;
|
|
@@ -328,25 +329,55 @@ function resolveCapturePath(options = {}) {
|
|
|
328
329
|
return join(dataDir, "insights.sqlite");
|
|
329
330
|
}
|
|
330
331
|
function resolveInsightsConfigPath(options = {}) {
|
|
331
|
-
return join(
|
|
332
|
+
return join(resolveConfigDataDir(options), "config.jsonc");
|
|
333
|
+
}
|
|
334
|
+
function resolveLegacyInsightsConfigPath(options = {}) {
|
|
335
|
+
return join(resolveConfigDataDir(options), "config.json");
|
|
336
|
+
}
|
|
337
|
+
function resolveConfigDataDir(options = {}) {
|
|
338
|
+
return typeof options.dataDir === "string" && options.dataDir.length > 0 ? options.dataDir : defaultDataDir();
|
|
332
339
|
}
|
|
333
340
|
async function readInsightsConfig(options = {}) {
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
return defaultInsightsConfig();
|
|
342
|
-
}
|
|
341
|
+
const jsoncPath = resolveInsightsConfigPath(options);
|
|
342
|
+
if (existsSync(jsoncPath)) {
|
|
343
|
+
return parseInsightsConfigFile(jsoncPath);
|
|
344
|
+
}
|
|
345
|
+
const legacyPath = resolveLegacyInsightsConfigPath(options);
|
|
346
|
+
if (existsSync(legacyPath)) {
|
|
347
|
+
return parseInsightsConfigFile(legacyPath);
|
|
343
348
|
}
|
|
344
349
|
try {
|
|
345
|
-
|
|
350
|
+
await mkdir(dirname(jsoncPath), { recursive: true });
|
|
351
|
+
await writeFile(jsoncPath, defaultInsightsConfigJsonc(), "utf8");
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
return defaultInsightsConfig();
|
|
355
|
+
}
|
|
356
|
+
async function parseInsightsConfigFile(path) {
|
|
357
|
+
try {
|
|
358
|
+
const parseErrors = [];
|
|
359
|
+
const parsed = parse(await readFile(path, "utf8"), parseErrors, { allowTrailingComma: true });
|
|
360
|
+
if (parseErrors.length > 0) return defaultInsightsConfig();
|
|
361
|
+
return insightsConfigFrom(parsed);
|
|
346
362
|
} catch {
|
|
347
363
|
return defaultInsightsConfig();
|
|
348
364
|
}
|
|
349
365
|
}
|
|
366
|
+
function defaultInsightsConfigJsonc() {
|
|
367
|
+
return [
|
|
368
|
+
"{",
|
|
369
|
+
" // Database file. Default: ~/.opencode-insights/insights.sqlite",
|
|
370
|
+
' // "dbPath": "/absolute/path/to/insights.sqlite",',
|
|
371
|
+
` "retentionDays": ${DEFAULT_RETENTION_DAYS},`,
|
|
372
|
+
` "promptRightMetrics": ${JSON.stringify(DEFAULT_PROMPT_RIGHT_METRICS)},`,
|
|
373
|
+
` "goUsage": ${JSON.stringify(defaultGoUsageConfig())}`,
|
|
374
|
+
"}",
|
|
375
|
+
""
|
|
376
|
+
].join("\n");
|
|
377
|
+
}
|
|
378
|
+
function insightsOptionsFromConfig(config, dataDir) {
|
|
379
|
+
return compactUndefined({ dataDir, dbPath: config.dbPath, retentionDays: config.retentionDays });
|
|
380
|
+
}
|
|
350
381
|
function defaultInsightsConfig() {
|
|
351
382
|
return { promptRightMetrics: [...DEFAULT_PROMPT_RIGHT_METRICS], goUsage: defaultGoUsageConfig() };
|
|
352
383
|
}
|
|
@@ -356,9 +387,14 @@ function defaultGoUsageConfig() {
|
|
|
356
387
|
function insightsConfigFrom(value) {
|
|
357
388
|
const record = isRecord(value) ? value : {};
|
|
358
389
|
const metrics = Array.isArray(record.promptRightMetrics) ? record.promptRightMetrics.filter(isPromptRightMetric) : [];
|
|
390
|
+
const dbPath = typeof record.dbPath === "string" && record.dbPath.trim().length > 0 ? record.dbPath.trim() : void 0;
|
|
391
|
+
const rawRetention = record.retentionDays;
|
|
392
|
+
const retentionDays = rawRetention === void 0 || rawRetention === null || rawRetention === "" ? void 0 : resolveRetentionDays(rawRetention);
|
|
359
393
|
return {
|
|
360
394
|
promptRightMetrics: metrics.length ? metrics : [...DEFAULT_PROMPT_RIGHT_METRICS],
|
|
361
|
-
goUsage: goUsageConfigFrom(record.goUsage)
|
|
395
|
+
goUsage: goUsageConfigFrom(record.goUsage),
|
|
396
|
+
dbPath,
|
|
397
|
+
retentionDays
|
|
362
398
|
};
|
|
363
399
|
}
|
|
364
400
|
function goUsageConfigFrom(value) {
|
|
@@ -708,7 +744,9 @@ export {
|
|
|
708
744
|
defaultDataDir,
|
|
709
745
|
resolveCapturePath,
|
|
710
746
|
resolveInsightsConfigPath,
|
|
747
|
+
resolveLegacyInsightsConfigPath,
|
|
711
748
|
readInsightsConfig,
|
|
749
|
+
insightsOptionsFromConfig,
|
|
712
750
|
normalizeChatMessageCapture,
|
|
713
751
|
normalizeChatParamsCapture,
|
|
714
752
|
normalizeChatHeadersCapture,
|
|
@@ -1,6 +1,229 @@
|
|
|
1
|
+
// src/activity.ts
|
|
2
|
+
function createActivityState() {
|
|
3
|
+
return {
|
|
4
|
+
bySessionID: {},
|
|
5
|
+
childrenByParent: {},
|
|
6
|
+
titles: {},
|
|
7
|
+
hydrated: /* @__PURE__ */ new Set(),
|
|
8
|
+
seenKeys: {},
|
|
9
|
+
loading: /* @__PURE__ */ new Set()
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function emptyActivity() {
|
|
13
|
+
return { toolCalls: 0, toolBreakdown: {}, warnings: 0, warningDetails: [], skills: {}, autoCompacts: 0, steps: 0 };
|
|
14
|
+
}
|
|
15
|
+
function hasActivity(a) {
|
|
16
|
+
if (!a) return false;
|
|
17
|
+
return a.toolCalls > 0 || a.warnings > 0 || a.autoCompacts > 0 || a.steps > 0 || Object.keys(a.skills).length > 0;
|
|
18
|
+
}
|
|
19
|
+
function recordChild(state, sessionID, parentID) {
|
|
20
|
+
const children = state.childrenByParent[parentID];
|
|
21
|
+
if (!children) {
|
|
22
|
+
state.childrenByParent[parentID] = [sessionID];
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!children.includes(sessionID)) children.push(sessionID);
|
|
26
|
+
}
|
|
27
|
+
function activityFor(state, sessionID) {
|
|
28
|
+
return state.bySessionID[sessionID] ??= emptyActivity();
|
|
29
|
+
}
|
|
30
|
+
function seen(state, sessionID, key) {
|
|
31
|
+
const keys = state.seenKeys[sessionID];
|
|
32
|
+
if (!keys) {
|
|
33
|
+
state.seenKeys[sessionID] = /* @__PURE__ */ new Set([key]);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (keys.has(key)) return true;
|
|
37
|
+
keys.add(key);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
function recordToolPart(state, sessionID, part) {
|
|
41
|
+
const activity = activityFor(state, sessionID);
|
|
42
|
+
let counted = false;
|
|
43
|
+
if (part.id !== void 0) {
|
|
44
|
+
if (!seen(state, sessionID, `tool:${part.id}`)) {
|
|
45
|
+
activity.toolCalls += 1;
|
|
46
|
+
activity.toolBreakdown[part.tool] = (activity.toolBreakdown[part.tool] ?? 0) + 1;
|
|
47
|
+
counted = true;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
activity.toolCalls += 1;
|
|
51
|
+
activity.toolBreakdown[part.tool] = (activity.toolBreakdown[part.tool] ?? 0) + 1;
|
|
52
|
+
counted = true;
|
|
53
|
+
}
|
|
54
|
+
const partState = part.state;
|
|
55
|
+
if (partState?.status === "error" && part.id !== void 0) {
|
|
56
|
+
if (!seen(state, sessionID, `warning:${part.id}`)) {
|
|
57
|
+
activity.warnings += 1;
|
|
58
|
+
if (typeof partState.error === "string" && partState.error.length > 0) {
|
|
59
|
+
activity.warningDetails.push({ tool: part.tool, message: partState.error });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (part.tool === "skill" && partState?.input && typeof partState.input.name === "string" && partState.input.name.length > 0) {
|
|
64
|
+
if (part.id !== void 0) {
|
|
65
|
+
if (!seen(state, sessionID, `skill:${part.id}`)) {
|
|
66
|
+
activity.skills[partState.input.name] = (activity.skills[partState.input.name] ?? 0) + 1;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
activity.skills[partState.input.name] = (activity.skills[partState.input.name] ?? 0) + 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return counted;
|
|
73
|
+
}
|
|
74
|
+
function recordCompaction(state, sessionID, id, auto) {
|
|
75
|
+
if (!auto) return false;
|
|
76
|
+
const activity = activityFor(state, sessionID);
|
|
77
|
+
if (seen(state, sessionID, `compact:${id}`)) return false;
|
|
78
|
+
activity.autoCompacts += 1;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
function recordStep(state, sessionID, id) {
|
|
82
|
+
const activity = activityFor(state, sessionID);
|
|
83
|
+
if (seen(state, sessionID, `step:${id}`)) return false;
|
|
84
|
+
activity.steps += 1;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
function mergeActivity(...activities) {
|
|
88
|
+
const result = emptyActivity();
|
|
89
|
+
for (const activity of activities) {
|
|
90
|
+
result.toolCalls += activity.toolCalls;
|
|
91
|
+
result.warnings += activity.warnings;
|
|
92
|
+
result.autoCompacts += activity.autoCompacts;
|
|
93
|
+
result.steps += activity.steps;
|
|
94
|
+
for (const [tool, count] of Object.entries(activity.toolBreakdown)) {
|
|
95
|
+
result.toolBreakdown[tool] = (result.toolBreakdown[tool] ?? 0) + count;
|
|
96
|
+
}
|
|
97
|
+
for (const [name, count] of Object.entries(activity.skills)) {
|
|
98
|
+
result.skills[name] = (result.skills[name] ?? 0) + count;
|
|
99
|
+
}
|
|
100
|
+
for (const detail of activity.warningDetails) {
|
|
101
|
+
result.warningDetails.push(detail);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
function collectTreeSessions(state, rootSessionID) {
|
|
107
|
+
const visited = /* @__PURE__ */ new Set();
|
|
108
|
+
const result = [];
|
|
109
|
+
const stack = [rootSessionID];
|
|
110
|
+
while (stack.length > 0) {
|
|
111
|
+
const sessionID = stack.pop();
|
|
112
|
+
if (sessionID === void 0 || visited.has(sessionID)) continue;
|
|
113
|
+
visited.add(sessionID);
|
|
114
|
+
result.push(sessionID);
|
|
115
|
+
const children = state.childrenByParent[sessionID] ?? [];
|
|
116
|
+
for (const child of children) {
|
|
117
|
+
if (!visited.has(child)) stack.push(child);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function treeActivity(state, rootSessionID) {
|
|
123
|
+
const activities = [];
|
|
124
|
+
for (const sessionID of collectTreeSessions(state, rootSessionID)) {
|
|
125
|
+
const activity = state.bySessionID[sessionID];
|
|
126
|
+
if (activity) activities.push(activity);
|
|
127
|
+
}
|
|
128
|
+
return mergeActivity(...activities);
|
|
129
|
+
}
|
|
130
|
+
function treeLoading(state, rootSessionID) {
|
|
131
|
+
return collectTreeSessions(state, rootSessionID).some((sessionID) => state.loading.has(sessionID));
|
|
132
|
+
}
|
|
133
|
+
function pluralize(count, singular, plural) {
|
|
134
|
+
return `${count} ${count === 1 ? singular : plural ?? `${singular}s`}`;
|
|
135
|
+
}
|
|
136
|
+
function formatActivitySuffix(a) {
|
|
137
|
+
if (!hasActivity(a)) return "";
|
|
138
|
+
const parts = [];
|
|
139
|
+
if (a && a.toolCalls > 0) parts.push(pluralize(a.toolCalls, "tool call"));
|
|
140
|
+
if (a && a.autoCompacts > 0) parts.push(pluralize(a.autoCompacts, "auto-compact"));
|
|
141
|
+
if (a && Object.keys(a.skills).length > 0) {
|
|
142
|
+
const total = Object.values(a.skills).reduce((sum, count) => sum + count, 0);
|
|
143
|
+
parts.push(pluralize(total, "skill"));
|
|
144
|
+
}
|
|
145
|
+
if (a && a.warnings > 0) parts.push(pluralize(a.warnings, "warning"));
|
|
146
|
+
return parts.join(" \xB7 ");
|
|
147
|
+
}
|
|
148
|
+
function formatActivityBriefRows(a, subagentCount) {
|
|
149
|
+
if (!hasActivity(a) && subagentCount <= 0) return [];
|
|
150
|
+
const rows = [];
|
|
151
|
+
if (a && a.toolCalls > 0) rows.push(pluralize(a.toolCalls, "tool call"));
|
|
152
|
+
if (a && a.autoCompacts > 0) rows.push(pluralize(a.autoCompacts, "auto-compact"));
|
|
153
|
+
if (a && Object.keys(a.skills).length > 0) {
|
|
154
|
+
const total = Object.values(a.skills).reduce((sum, count) => sum + count, 0);
|
|
155
|
+
rows.push(pluralize(total, "skill"));
|
|
156
|
+
}
|
|
157
|
+
if (a && a.steps > 0) rows.push(pluralize(a.steps, "model request"));
|
|
158
|
+
if (subagentCount > 0) rows.push(pluralize(subagentCount, "subagent"));
|
|
159
|
+
if (a && a.warnings > 0) rows.push(pluralize(a.warnings, "warning"));
|
|
160
|
+
return rows;
|
|
161
|
+
}
|
|
162
|
+
function truncateMiddle(value, maxLength) {
|
|
163
|
+
if (value.length <= maxLength) return value;
|
|
164
|
+
if (maxLength <= 3) return value.slice(0, maxLength);
|
|
165
|
+
const marker = "...";
|
|
166
|
+
const left = Math.ceil((maxLength - marker.length) / 2);
|
|
167
|
+
const right = Math.floor((maxLength - marker.length) / 2);
|
|
168
|
+
return `${value.slice(0, left)}${marker}${value.slice(value.length - right)}`;
|
|
169
|
+
}
|
|
170
|
+
function sortedBreakdown(breakdown) {
|
|
171
|
+
return Object.entries(breakdown).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
172
|
+
}
|
|
173
|
+
function indent(lines, depth) {
|
|
174
|
+
return lines.map((text) => `${" ".repeat(depth)}${text}`);
|
|
175
|
+
}
|
|
176
|
+
function treeSubagentCount(state, rootSessionID) {
|
|
177
|
+
return Math.max(0, collectTreeSessions(state, rootSessionID).length - 1);
|
|
178
|
+
}
|
|
179
|
+
function buildSessionAnalysisRows(state, rootSessionID) {
|
|
180
|
+
const rows = [];
|
|
181
|
+
const children = (sessionID) => state.childrenByParent[sessionID] ?? [];
|
|
182
|
+
const activityOf = (sessionID) => state.bySessionID[sessionID] ?? emptyActivity();
|
|
183
|
+
const titleOf = (sessionID) => state.titles[sessionID] ?? sessionID;
|
|
184
|
+
const childIds = [];
|
|
185
|
+
const visit = (sessionID, depth) => {
|
|
186
|
+
for (const childID of children(sessionID)) {
|
|
187
|
+
childIds.push(childID);
|
|
188
|
+
const childActivity = activityOf(childID);
|
|
189
|
+
const suffix = formatActivitySuffix(childActivity);
|
|
190
|
+
const line = `\xB7 ${truncateMiddle(titleOf(childID), 36)}${suffix ? ` ${suffix}` : ""}`;
|
|
191
|
+
rows.push(`${" ".repeat(depth + 1)}${line}`);
|
|
192
|
+
visit(childID, depth + 1);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
visit(rootSessionID, 0);
|
|
196
|
+
const tree = treeActivity(state, rootSessionID);
|
|
197
|
+
const sections = [];
|
|
198
|
+
if (tree.toolCalls > 0) {
|
|
199
|
+
sections.push([`Tool calls (${tree.toolCalls})`, indent(sortedBreakdown(tree.toolBreakdown).map(([tool, count]) => `\xB7 ${tool} ${count}`), 1)]);
|
|
200
|
+
}
|
|
201
|
+
if (Object.keys(tree.skills).length > 0) {
|
|
202
|
+
const total = Object.values(tree.skills).reduce((sum, count) => sum + count, 0);
|
|
203
|
+
sections.push([`Skills (${total})`, indent(sortedBreakdown(tree.skills).map(([name, count]) => `\xB7 ${name} ${count}`), 1)]);
|
|
204
|
+
}
|
|
205
|
+
if (tree.autoCompacts > 0) {
|
|
206
|
+
sections.push([`Auto-compactions`, [` \xB7 ${tree.autoCompacts}`]]);
|
|
207
|
+
}
|
|
208
|
+
if (tree.steps > 0) {
|
|
209
|
+
sections.push([`Model requests (${tree.steps})`, [` \xB7 ${tree.steps}`]]);
|
|
210
|
+
}
|
|
211
|
+
if (childIds.length > 0 && hasActivity(tree)) {
|
|
212
|
+
sections.push([`Subagents`, rows]);
|
|
213
|
+
}
|
|
214
|
+
if (tree.warnings > 0) {
|
|
215
|
+
const details = tree.warningDetails.length > 0 ? tree.warningDetails.map((detail) => ` \xB7 ${truncateMiddle(detail.tool, 24)} \u2014 ${truncateMiddle(detail.message, 72)}`) : [` \xB7 ${pluralize(tree.warnings, "warning")}`];
|
|
216
|
+
sections.push([`Tool warnings (${tree.warnings})`, details]);
|
|
217
|
+
}
|
|
218
|
+
return sections.flatMap(([title, body]) => [
|
|
219
|
+
{ text: title, header: true },
|
|
220
|
+
...body.map((text) => ({ text, header: false }))
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
|
|
1
224
|
// src/subagents.ts
|
|
2
|
-
function createSubagentState() {
|
|
3
|
-
return { children: {}, totalExecuted: 0 };
|
|
225
|
+
function createSubagentState(activityStore) {
|
|
226
|
+
return { children: {}, totalExecuted: 0, ...activityStore ? { activityStore } : {} };
|
|
4
227
|
}
|
|
5
228
|
function applySubagentEvent(state, event) {
|
|
6
229
|
const created = extractTaskToolSubagent(event) ?? extractSubagent(event) ?? updateExistingSubagent(state, event);
|
|
@@ -20,7 +243,8 @@ function applySubagentEvent(state, event) {
|
|
|
20
243
|
updatedAt: created.updatedAt,
|
|
21
244
|
endedAt,
|
|
22
245
|
elapsedMs: elapsedMs(startedAt, endedAt ?? created.updatedAt),
|
|
23
|
-
tokens: created.tokens ?? previous?.tokens
|
|
246
|
+
tokens: created.tokens ?? previous?.tokens,
|
|
247
|
+
activity: state.activityStore ? state.activityStore.bySessionID[created.id] ??= emptyActivity() : void 0
|
|
24
248
|
};
|
|
25
249
|
if (!previous) state.totalExecuted += 1;
|
|
26
250
|
state.children[created.id] = next;
|
|
@@ -78,7 +302,7 @@ function getSubagentSidebarModel(state, parentID, options = {}) {
|
|
|
78
302
|
rows: children.map((child) => ({
|
|
79
303
|
id: child.id,
|
|
80
304
|
title: formatSubagentTitle(child.title),
|
|
81
|
-
subtitle: [formatSubagentDuration(child, options.now), formatUsage(child)].filter(Boolean).join(" \xB7 "),
|
|
305
|
+
subtitle: [formatSubagentDuration(child, options.now), formatUsage(child), formatActivitySuffix(child.activity)].filter(Boolean).join(" \xB7 "),
|
|
82
306
|
status: child.status
|
|
83
307
|
}))
|
|
84
308
|
};
|
|
@@ -265,13 +489,13 @@ function statusSortRank(status) {
|
|
|
265
489
|
}
|
|
266
490
|
function formatSubagentTitle(title) {
|
|
267
491
|
const match = title.match(/^(?:[✓✗!]\s*)?([A-Za-z][\w -]*?)\s+[—-]\s+(.+)$/u);
|
|
268
|
-
if (!match) return
|
|
492
|
+
if (!match) return truncateMiddle2(title, 36);
|
|
269
493
|
const agent = match[1]?.trim();
|
|
270
494
|
const task = match[2]?.trim();
|
|
271
|
-
if (!agent || !task) return
|
|
272
|
-
return
|
|
495
|
+
if (!agent || !task) return truncateMiddle2(title, 36);
|
|
496
|
+
return truncateMiddle2(`${agent}: ${task}`, 36);
|
|
273
497
|
}
|
|
274
|
-
function
|
|
498
|
+
function truncateMiddle2(value, maxLength) {
|
|
275
499
|
if (value.length <= maxLength) return value;
|
|
276
500
|
if (maxLength <= 3) return value.slice(0, maxLength);
|
|
277
501
|
const marker = "...";
|
|
@@ -300,6 +524,16 @@ function isRecord(value) {
|
|
|
300
524
|
}
|
|
301
525
|
|
|
302
526
|
export {
|
|
527
|
+
createActivityState,
|
|
528
|
+
recordChild,
|
|
529
|
+
recordToolPart,
|
|
530
|
+
recordCompaction,
|
|
531
|
+
recordStep,
|
|
532
|
+
treeActivity,
|
|
533
|
+
treeLoading,
|
|
534
|
+
formatActivityBriefRows,
|
|
535
|
+
treeSubagentCount,
|
|
536
|
+
buildSessionAnalysisRows,
|
|
303
537
|
createSubagentState,
|
|
304
538
|
applySubagentEvent,
|
|
305
539
|
renderSubagentStatus,
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as CaptureRecord } from './capture-
|
|
2
|
+
import { C as CaptureRecord } from './capture-CwesXqmX.js';
|
|
3
3
|
|
|
4
4
|
type HistoryMessage = {
|
|
5
5
|
id: string;
|
|
@@ -72,10 +72,10 @@ type CliOptions = {
|
|
|
72
72
|
port?: number | undefined;
|
|
73
73
|
output?: string | undefined;
|
|
74
74
|
configDir?: string | undefined;
|
|
75
|
-
retentionDays?: number | undefined;
|
|
76
75
|
dryRun: boolean;
|
|
77
76
|
keepData: boolean;
|
|
78
77
|
};
|
|
78
|
+
declare function unsupportedFlagWarning(arg: string): string | undefined;
|
|
79
79
|
declare function parseOptions(args: string[]): CliOptions;
|
|
80
80
|
declare function summarizeSessions(sessions: ReturnType<typeof buildRequestHistory>["sessions"]): {
|
|
81
81
|
id: string;
|
|
@@ -88,6 +88,7 @@ declare function summarizeSessions(sessions: ReturnType<typeof buildRequestHisto
|
|
|
88
88
|
declare function formatSessionSummary(rows: ReturnType<typeof summarizeSessions>): string;
|
|
89
89
|
type JsonObject = Record<string, unknown>;
|
|
90
90
|
declare function configureOpenCodeDebug(options: CliOptions): Promise<string>;
|
|
91
|
+
declare function revertOpenCodeDebug(options: CliOptions): Promise<string>;
|
|
91
92
|
declare function defaultOpenCodeConfigDir(): string;
|
|
92
93
|
declare function resolveOpenCodeConfigPath(configDir: string): string;
|
|
93
94
|
declare function stripJsonCommentsAndTrailingCommas(input: string): string;
|
|
@@ -95,4 +96,4 @@ declare function addUniquePlugin(config: JsonObject, plugin: string): boolean;
|
|
|
95
96
|
declare function removePlugin(config: JsonObject, plugin: string): boolean;
|
|
96
97
|
declare function uninstallOpenCode(options: CliOptions): Promise<string>;
|
|
97
98
|
|
|
98
|
-
export { addUniquePlugin, configureOpenCodeDebug, defaultOpenCodeConfigDir, formatSessionSummary, parseOptions, removePlugin, resolveOpenCodeConfigPath, stripJsonCommentsAndTrailingCommas, summarizeSessions, uninstallOpenCode };
|
|
99
|
+
export { addUniquePlugin, configureOpenCodeDebug, defaultOpenCodeConfigDir, formatSessionSummary, parseOptions, removePlugin, resolveOpenCodeConfigPath, revertOpenCodeDebug, stripJsonCommentsAndTrailingCommas, summarizeSessions, uninstallOpenCode, unsupportedFlagWarning };
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
openDatabase,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
readInsightsConfig,
|
|
5
|
+
resolveCapturePath,
|
|
6
|
+
resolveInsightsConfigPath
|
|
7
|
+
} from "./chunk-RZGCLQ2M.js";
|
|
6
8
|
|
|
7
9
|
// src/cli.ts
|
|
8
10
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -1446,10 +1448,25 @@ var DEFAULT_HISTORY_LIMIT = 5e3;
|
|
|
1446
1448
|
var SERVER_PLUGIN_SPEC = "@rejacky/opencode-insights";
|
|
1447
1449
|
var TUI_PLUGIN_SPEC = SERVER_PLUGIN_SPEC;
|
|
1448
1450
|
var SUBPATH_TUI_PLUGIN_SPEC = "@rejacky/opencode-insights/tui";
|
|
1451
|
+
function unsupportedFlagWarning(arg) {
|
|
1452
|
+
if (arg === "--data-dir") {
|
|
1453
|
+
return "warning: --data-dir is no longer supported; the CLI reads the configured database path from ~/.opencode-insights/config.jsonc";
|
|
1454
|
+
}
|
|
1455
|
+
const setting = arg === "--db" ? "dbPath" : arg === "--retention-days" ? "retentionDays" : void 0;
|
|
1456
|
+
if (!setting) return void 0;
|
|
1457
|
+
return `warning: ${arg} is no longer supported; set ${setting} in ~/.opencode-insights/config.jsonc`;
|
|
1458
|
+
}
|
|
1449
1459
|
async function main(argv) {
|
|
1450
1460
|
const command = argv[2] ?? "recent";
|
|
1461
|
+
for (const arg of argv.slice(3)) {
|
|
1462
|
+
const warning = unsupportedFlagWarning(arg);
|
|
1463
|
+
if (warning) process.stderr.write(`${warning}
|
|
1464
|
+
`);
|
|
1465
|
+
}
|
|
1451
1466
|
const options = parseOptions(argv.slice(3));
|
|
1452
1467
|
const positionals = parsePositionals(argv.slice(3));
|
|
1468
|
+
const config = await readInsightsConfig();
|
|
1469
|
+
options.dbPath = config.dbPath;
|
|
1453
1470
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
1454
1471
|
process.stdout.write(`${usage()}
|
|
1455
1472
|
`);
|
|
@@ -1527,6 +1544,11 @@ async function main(argv) {
|
|
|
1527
1544
|
}
|
|
1528
1545
|
if (command === "debug") {
|
|
1529
1546
|
process.stdout.write(`${await configureOpenCodeDebug(options)}
|
|
1547
|
+
`);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (command === "revert") {
|
|
1551
|
+
process.stdout.write(`${await revertOpenCodeDebug(options)}
|
|
1530
1552
|
`);
|
|
1531
1553
|
return;
|
|
1532
1554
|
}
|
|
@@ -1553,14 +1575,6 @@ function parseOptions(args) {
|
|
|
1553
1575
|
options.dryRun = true;
|
|
1554
1576
|
} else if (arg === "--keep-data") {
|
|
1555
1577
|
options.keepData = true;
|
|
1556
|
-
} else if (arg === "--db") {
|
|
1557
|
-
const value = args[index + 1];
|
|
1558
|
-
if (value) options.dbPath = value;
|
|
1559
|
-
index += 1;
|
|
1560
|
-
} else if (arg === "--data-dir") {
|
|
1561
|
-
const value = args[index + 1];
|
|
1562
|
-
if (value) options.dataDir = value;
|
|
1563
|
-
index += 1;
|
|
1564
1578
|
} else if (arg === "--limit") {
|
|
1565
1579
|
options.limit = Number.parseInt(args[index + 1] ?? "20", 10);
|
|
1566
1580
|
options.limitProvided = true;
|
|
@@ -1580,16 +1594,10 @@ function parseOptions(args) {
|
|
|
1580
1594
|
const value = args[index + 1];
|
|
1581
1595
|
if (value) options.configDir = value;
|
|
1582
1596
|
index += 1;
|
|
1583
|
-
} else if (arg === "--retention-days") {
|
|
1584
|
-
options.retentionDays = Number.parseFloat(args[index + 1] ?? "");
|
|
1585
|
-
index += 1;
|
|
1586
1597
|
}
|
|
1587
1598
|
}
|
|
1588
1599
|
if (!Number.isFinite(options.limit) || options.limit < 1) options.limit = DEFAULT_RECENT_LIMIT;
|
|
1589
1600
|
if (options.port !== void 0 && (!Number.isFinite(options.port) || options.port < 1)) options.port = 8765;
|
|
1590
|
-
if (options.retentionDays !== void 0 && (!Number.isFinite(options.retentionDays) || options.retentionDays < 0)) {
|
|
1591
|
-
options.retentionDays = void 0;
|
|
1592
|
-
}
|
|
1593
1601
|
return options;
|
|
1594
1602
|
}
|
|
1595
1603
|
function parsePositionals(args) {
|
|
@@ -1732,14 +1740,14 @@ async function configureOpenCodeDebug(options) {
|
|
|
1732
1740
|
const tuiSource = await readJsonConfigSource(tuiPath);
|
|
1733
1741
|
const opencodeConfig = await readJsonConfig(opencodePath, { plugin: [] }, opencodeSource);
|
|
1734
1742
|
const tuiConfig = await readJsonConfig(tuiPath, { plugin: [] }, tuiSource);
|
|
1735
|
-
setSinglePluginSpec(opencodeConfig,
|
|
1736
|
-
setSinglePluginSpec(tuiConfig,
|
|
1737
|
-
removePlugin(tuiConfig, SUBPATH_TUI_PLUGIN_SPEC);
|
|
1743
|
+
setSinglePluginSpec(opencodeConfig, localServerEntry);
|
|
1744
|
+
setSinglePluginSpec(tuiConfig, localTuiEntry);
|
|
1738
1745
|
const lines = [
|
|
1739
1746
|
`OpenCode config: ${opencodePath}`,
|
|
1740
1747
|
`TUI config: ${tuiPath}`,
|
|
1741
1748
|
`Local server plugin: ${localServerEntry}`,
|
|
1742
1749
|
`Local TUI plugin: ${localTuiEntry}`,
|
|
1750
|
+
`Insights config: ${resolveInsightsConfigPath({ dataDir: options.dataDir })}`,
|
|
1743
1751
|
`Server plugin: set local build output`,
|
|
1744
1752
|
`TUI plugin: set local build output`
|
|
1745
1753
|
];
|
|
@@ -1747,12 +1755,67 @@ async function configureOpenCodeDebug(options) {
|
|
|
1747
1755
|
lines.push("Dry run: no files written.");
|
|
1748
1756
|
return lines.join("\n");
|
|
1749
1757
|
}
|
|
1758
|
+
await readInsightsConfig({ dataDir: options.dataDir });
|
|
1750
1759
|
await mkdir(configDir, { recursive: true });
|
|
1751
1760
|
await writeJsonConfig(opencodePath, opencodeConfig, opencodeSource);
|
|
1752
1761
|
await writeJsonConfig(tuiPath, tuiConfig, tuiSource);
|
|
1753
1762
|
lines.push("Debug configuration written. Restart OpenCode to load the local build.");
|
|
1754
1763
|
return lines.join("\n");
|
|
1755
1764
|
}
|
|
1765
|
+
async function revertOpenCodeDebug(options) {
|
|
1766
|
+
const configDir = options.configDir ?? defaultOpenCodeConfigDir();
|
|
1767
|
+
const opencodePath = resolveOpenCodeConfigPath(configDir);
|
|
1768
|
+
const tuiPath = join(configDir, "tui.json");
|
|
1769
|
+
const officialSpec = `${SERVER_PLUGIN_SPEC}@latest`;
|
|
1770
|
+
const serverResult = await revertPluginToOfficial(opencodePath, officialSpec, options);
|
|
1771
|
+
const tuiResult = await revertPluginToOfficial(tuiPath, officialSpec, options);
|
|
1772
|
+
const changed = serverResult.startsWith("replaced") || tuiResult.startsWith("replaced");
|
|
1773
|
+
const lines = [
|
|
1774
|
+
`OpenCode config: ${opencodePath}`,
|
|
1775
|
+
`TUI config: ${tuiPath}`,
|
|
1776
|
+
`Official plugin spec: ${officialSpec}`,
|
|
1777
|
+
`Server plugin: ${serverResult}`,
|
|
1778
|
+
`TUI plugin: ${tuiResult}`
|
|
1779
|
+
];
|
|
1780
|
+
if (options.dryRun) {
|
|
1781
|
+
lines.push("Dry run: no files written.");
|
|
1782
|
+
} else if (changed) {
|
|
1783
|
+
lines.push("Reverted to the official package. Restart OpenCode to load it.");
|
|
1784
|
+
} else {
|
|
1785
|
+
lines.push("No local build output found; nothing to revert.");
|
|
1786
|
+
}
|
|
1787
|
+
return lines.join("\n");
|
|
1788
|
+
}
|
|
1789
|
+
async function revertPluginToOfficial(path, officialSpec, options) {
|
|
1790
|
+
if (!existsSync2(path)) return "config not found";
|
|
1791
|
+
const source = await readJsonConfigSource(path);
|
|
1792
|
+
const config = await readJsonConfig(path, { plugin: [] }, source);
|
|
1793
|
+
const current = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1794
|
+
let changed = false;
|
|
1795
|
+
const next = current.map((entry) => {
|
|
1796
|
+
if (!isLocalDistEntry(entry)) return entry;
|
|
1797
|
+
changed = true;
|
|
1798
|
+
return officialSpec;
|
|
1799
|
+
});
|
|
1800
|
+
if (!changed) return "not present (local build output)";
|
|
1801
|
+
config.plugin = dedupeStrings(next);
|
|
1802
|
+
if (options.dryRun) return `would replace local build with ${officialSpec}`;
|
|
1803
|
+
await writeJsonConfig(path, config, source);
|
|
1804
|
+
return `replaced local build with ${officialSpec}`;
|
|
1805
|
+
}
|
|
1806
|
+
function isLocalDistEntry(entry) {
|
|
1807
|
+
const spec = Array.isArray(entry) ? entry[0] : entry;
|
|
1808
|
+
return typeof spec === "string" && /\/opencode-insights\/dist\/(?:index|tui)\.js$/u.test(spec.replaceAll("\\", "/"));
|
|
1809
|
+
}
|
|
1810
|
+
function dedupeStrings(values) {
|
|
1811
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1812
|
+
return values.filter((value) => {
|
|
1813
|
+
if (typeof value !== "string") return true;
|
|
1814
|
+
if (seen.has(value)) return false;
|
|
1815
|
+
seen.add(value);
|
|
1816
|
+
return true;
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1756
1819
|
function defaultOpenCodeConfigDir() {
|
|
1757
1820
|
const override = process.env.OPENCODE_CONFIG_DIR;
|
|
1758
1821
|
if (override) return override;
|
|
@@ -1826,11 +1889,6 @@ function stripJsonCommentsAndTrailingCommas(input) {
|
|
|
1826
1889
|
function isJsonObject(value) {
|
|
1827
1890
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1828
1891
|
}
|
|
1829
|
-
function debugServerOptions(options) {
|
|
1830
|
-
const serverOptions = {};
|
|
1831
|
-
if (options.retentionDays !== void 0) serverOptions.retentionDays = options.retentionDays;
|
|
1832
|
-
return serverOptions;
|
|
1833
|
-
}
|
|
1834
1892
|
function addUniquePlugin(config, plugin) {
|
|
1835
1893
|
const current = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1836
1894
|
if (current.includes(plugin)) {
|
|
@@ -1849,19 +1907,24 @@ function removePlugin(config, plugin) {
|
|
|
1849
1907
|
function isPluginEntry(entry, plugin) {
|
|
1850
1908
|
return entry === plugin || Array.isArray(entry) && entry[0] === plugin;
|
|
1851
1909
|
}
|
|
1852
|
-
function setSinglePluginSpec(config,
|
|
1910
|
+
function setSinglePluginSpec(config, nextPlugin) {
|
|
1853
1911
|
const current = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1912
|
+
const next = current.filter(
|
|
1913
|
+
(entry) => !isInsightsPluginEntry(entry) && entry !== nextPlugin && !(Array.isArray(entry) && entry[0] === nextPlugin)
|
|
1914
|
+
);
|
|
1856
1915
|
config.plugin = [...next, nextPlugin];
|
|
1857
1916
|
}
|
|
1858
|
-
function isInsightsPluginEntry(entry
|
|
1859
|
-
if (isPluginEntry(entry, packagePlugin) || isPluginEntry(entry, localPlugin) || isPluginEntry(entry, SUBPATH_TUI_PLUGIN_SPEC)) {
|
|
1860
|
-
return true;
|
|
1861
|
-
}
|
|
1917
|
+
function isInsightsPluginEntry(entry) {
|
|
1862
1918
|
const spec = Array.isArray(entry) ? entry[0] : entry;
|
|
1863
1919
|
if (typeof spec !== "string") return false;
|
|
1864
|
-
return
|
|
1920
|
+
return isInsightsSpec(spec);
|
|
1921
|
+
}
|
|
1922
|
+
function isInsightsSpec(spec) {
|
|
1923
|
+
if (spec === SERVER_PLUGIN_SPEC || spec === SUBPATH_TUI_PLUGIN_SPEC) return true;
|
|
1924
|
+
if (spec.startsWith("npm:")) return isInsightsSpec(spec.slice(4));
|
|
1925
|
+
if (spec.startsWith(`${SERVER_PLUGIN_SPEC}@`)) return true;
|
|
1926
|
+
const normalized = spec.replaceAll("\\", "/");
|
|
1927
|
+
return /(?:^|[/@-])opencode-insights.*\.tgz$/u.test(normalized) || /\/opencode-insights\/dist\/(?:index|tui)\.js$/u.test(normalized);
|
|
1865
1928
|
}
|
|
1866
1929
|
async function uninstallOpenCode(options) {
|
|
1867
1930
|
const configDir = options.configDir ?? defaultOpenCodeConfigDir();
|
|
@@ -1928,17 +1991,18 @@ async function writeJsonConfig(path, config, source) {
|
|
|
1928
1991
|
function usage() {
|
|
1929
1992
|
return [
|
|
1930
1993
|
"Usage:",
|
|
1931
|
-
" opencode-insights debug [--config-dir DIR] [--
|
|
1932
|
-
" opencode-insights
|
|
1933
|
-
" opencode-insights
|
|
1934
|
-
" opencode-insights
|
|
1935
|
-
" opencode-insights
|
|
1936
|
-
" opencode-insights
|
|
1937
|
-
" opencode-insights
|
|
1938
|
-
" opencode-insights
|
|
1939
|
-
" opencode-insights
|
|
1940
|
-
" opencode-insights
|
|
1941
|
-
" opencode-insights
|
|
1994
|
+
" opencode-insights debug [--config-dir DIR] [--dry-run]",
|
|
1995
|
+
" opencode-insights revert [--config-dir DIR] [--dry-run]",
|
|
1996
|
+
" opencode-insights uninstall [--config-dir DIR] [--keep-data] [--dry-run]",
|
|
1997
|
+
" opencode-insights recent [--limit N] [--json]",
|
|
1998
|
+
" opencode-insights sessions [--limit N] [--json]",
|
|
1999
|
+
" opencode-insights history [--limit N]",
|
|
2000
|
+
" opencode-insights show <session-id> [--limit N]",
|
|
2001
|
+
" opencode-insights export <session-id> [--output PATH] [--limit N]",
|
|
2002
|
+
" opencode-insights serve [--limit N] [--host HOST] [--port PORT]",
|
|
2003
|
+
" opencode-insights open [--limit N] [--host HOST] [--port PORT]",
|
|
2004
|
+
" opencode-insights doctor",
|
|
2005
|
+
" opencode-insights vacuum"
|
|
1942
2006
|
].join("\n");
|
|
1943
2007
|
}
|
|
1944
2008
|
function isDirectRun() {
|
|
@@ -1965,7 +2029,9 @@ export {
|
|
|
1965
2029
|
parseOptions,
|
|
1966
2030
|
removePlugin,
|
|
1967
2031
|
resolveOpenCodeConfigPath,
|
|
2032
|
+
revertOpenCodeDebug,
|
|
1968
2033
|
stripJsonCommentsAndTrailingCommas,
|
|
1969
2034
|
summarizeSessions,
|
|
1970
|
-
uninstallOpenCode
|
|
2035
|
+
uninstallOpenCode,
|
|
2036
|
+
unsupportedFlagWarning
|
|
1971
2037
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { Plugin } from '@opencode-ai/plugin';
|
|
2
2
|
import { TuiPlugin } from '@opencode-ai/plugin/tui';
|
|
3
|
-
export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, D as DEFAULT_PROMPT_RIGHT_METRICS, G as GoUsageConfig, I as InsightsConfig, c as InsightsOptions, J as JsonlCaptureStore, M as MessageTiming, d as MetricsState, P as PromptRightMetric, S as SessionAverage, e as SessionTokenUsage, f as SqliteCaptureStore, g as SqliteDb, h as StreamSample, i as createCaptureStore, j as createMetricsState, k as defaultDataDir, l as estimateStreamTokens, m as extractEventType, n as getSessionTokenUsage, o as
|
|
3
|
+
export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, D as DEFAULT_PROMPT_RIGHT_METRICS, G as GoUsageConfig, I as InsightsConfig, c as InsightsOptions, J as JsonlCaptureStore, M as MessageTiming, d as MetricsState, P as PromptRightMetric, S as SessionAverage, e as SessionTokenUsage, f as SqliteCaptureStore, g as SqliteDb, h as StreamSample, i as createCaptureStore, j as createMetricsState, k as defaultDataDir, l as estimateStreamTokens, m as extractEventType, n as getSessionTokenUsage, o as insightsOptionsFromConfig, p as normalizeChatHeadersCapture, q as normalizeChatMessageCapture, r as normalizeChatParamsCapture, s as normalizeEventCapture, t as normalizeExperimentalChatMessagesTransformCapture, u as normalizeExperimentalChatSystemTransformCapture, v as normalizeToolCapture, w as openDatabase, x as readInsightsConfig, y as recordAssistantDelta, z as recordAssistantMessage, B as recordToolActivity, E as renderMetricsText, F as renderPromptRightMetricsText, H as renderResponseMetricsText, K as renderSessionTokenUsage, L as resolveCapturePath, N as resolveInsightsConfigPath, O as resolveLegacyInsightsConfigPath, Q as resolveRetentionDays } from './capture-CwesXqmX.js';
|
|
4
|
+
|
|
5
|
+
type SessionActivity = {
|
|
6
|
+
toolCalls: number;
|
|
7
|
+
toolBreakdown: Record<string, number>;
|
|
8
|
+
warnings: number;
|
|
9
|
+
warningDetails: Array<{
|
|
10
|
+
tool: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}>;
|
|
13
|
+
skills: Record<string, number>;
|
|
14
|
+
autoCompacts: number;
|
|
15
|
+
steps: number;
|
|
16
|
+
};
|
|
17
|
+
type ActivityState = {
|
|
18
|
+
bySessionID: Record<string, SessionActivity>;
|
|
19
|
+
childrenByParent: Record<string, string[]>;
|
|
20
|
+
titles: Record<string, string>;
|
|
21
|
+
hydrated: Set<string>;
|
|
22
|
+
seenKeys: Record<string, Set<string>>;
|
|
23
|
+
loading: Set<string>;
|
|
24
|
+
};
|
|
4
25
|
|
|
5
26
|
type SubagentStatus = "running" | "done" | "error";
|
|
6
27
|
type SubagentInfo = {
|
|
@@ -18,10 +39,12 @@ type SubagentInfo = {
|
|
|
18
39
|
total?: number | undefined;
|
|
19
40
|
contextPercent?: number | undefined;
|
|
20
41
|
} | undefined;
|
|
42
|
+
activity?: SessionActivity | undefined;
|
|
21
43
|
};
|
|
22
44
|
type SubagentState = {
|
|
23
45
|
children: Record<string, SubagentInfo>;
|
|
24
46
|
totalExecuted: number;
|
|
47
|
+
activityStore?: ActivityState | undefined;
|
|
25
48
|
};
|
|
26
49
|
type SubagentSidebarRow = {
|
|
27
50
|
id: string;
|
|
@@ -34,7 +57,7 @@ type SubagentSidebarModel = {
|
|
|
34
57
|
summary: string;
|
|
35
58
|
rows: SubagentSidebarRow[];
|
|
36
59
|
};
|
|
37
|
-
declare function createSubagentState(): SubagentState;
|
|
60
|
+
declare function createSubagentState(activityStore?: ActivityState): SubagentState;
|
|
38
61
|
declare function applySubagentEvent(state: SubagentState, event: unknown): boolean;
|
|
39
62
|
declare function renderSubagentStatus(state: SubagentState, options?: {
|
|
40
63
|
now?: number;
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
renderSubagentFooter,
|
|
9
9
|
renderSubagentSidebar,
|
|
10
10
|
renderSubagentStatus
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-SGXZJVYX.js";
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_PROMPT_RIGHT_METRICS,
|
|
14
14
|
JsonlCaptureStore,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
estimateStreamTokens,
|
|
20
20
|
extractEventType,
|
|
21
21
|
getSessionTokenUsage,
|
|
22
|
+
insightsOptionsFromConfig,
|
|
22
23
|
normalizeChatHeadersCapture,
|
|
23
24
|
normalizeChatMessageCapture,
|
|
24
25
|
normalizeChatParamsCapture,
|
|
@@ -37,8 +38,9 @@ import {
|
|
|
37
38
|
renderSessionTokenUsage,
|
|
38
39
|
resolveCapturePath,
|
|
39
40
|
resolveInsightsConfigPath,
|
|
41
|
+
resolveLegacyInsightsConfigPath,
|
|
40
42
|
resolveRetentionDays
|
|
41
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-RZGCLQ2M.js";
|
|
42
44
|
|
|
43
45
|
// src/cli-shim.ts
|
|
44
46
|
import { existsSync } from "fs";
|
|
@@ -91,7 +93,8 @@ var OpenCodeInsights = async (_input, options) => {
|
|
|
91
93
|
if (options?.cliShim !== false) {
|
|
92
94
|
void ensureCliShim().catch(() => void 0);
|
|
93
95
|
}
|
|
94
|
-
const
|
|
96
|
+
const config = await readInsightsConfig(options);
|
|
97
|
+
const store = createCaptureStore(insightsOptionsFromConfig(config, options?.dataDir));
|
|
95
98
|
try {
|
|
96
99
|
await store.initialize?.();
|
|
97
100
|
} catch {
|
|
@@ -157,6 +160,7 @@ export {
|
|
|
157
160
|
getSubagentSidebarModel,
|
|
158
161
|
getSubagentSidebarRowAtLine,
|
|
159
162
|
id,
|
|
163
|
+
insightsOptionsFromConfig,
|
|
160
164
|
normalizeChatHeadersCapture,
|
|
161
165
|
normalizeChatMessageCapture,
|
|
162
166
|
normalizeChatParamsCapture,
|
|
@@ -179,6 +183,7 @@ export {
|
|
|
179
183
|
renderSubagentStatus,
|
|
180
184
|
resolveCapturePath,
|
|
181
185
|
resolveInsightsConfigPath,
|
|
186
|
+
resolveLegacyInsightsConfigPath,
|
|
182
187
|
resolveRetentionDays,
|
|
183
188
|
server,
|
|
184
189
|
rootTui as tui
|
package/dist/tui.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applySubagentEvent,
|
|
3
|
+
buildSessionAnalysisRows,
|
|
4
|
+
createActivityState,
|
|
3
5
|
createSubagentState,
|
|
6
|
+
formatActivityBriefRows,
|
|
4
7
|
getSubagentSidebarModel,
|
|
5
|
-
getSubagentSidebarRowAtLine
|
|
6
|
-
|
|
8
|
+
getSubagentSidebarRowAtLine,
|
|
9
|
+
recordChild,
|
|
10
|
+
recordCompaction,
|
|
11
|
+
recordStep,
|
|
12
|
+
recordToolPart,
|
|
13
|
+
treeActivity,
|
|
14
|
+
treeLoading,
|
|
15
|
+
treeSubagentCount
|
|
16
|
+
} from "./chunk-SGXZJVYX.js";
|
|
7
17
|
import {
|
|
8
18
|
createMetricsState,
|
|
9
19
|
readInsightsConfig,
|
|
@@ -12,11 +22,11 @@ import {
|
|
|
12
22
|
recordToolActivity,
|
|
13
23
|
renderPromptRightMetricsText,
|
|
14
24
|
renderSessionTokenUsage
|
|
15
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-RZGCLQ2M.js";
|
|
16
26
|
|
|
17
27
|
// src/tui.tsx
|
|
18
28
|
import { createTextAttributes, StyledText } from "@opentui/core";
|
|
19
|
-
import { createSignal, onCleanup } from "solid-js";
|
|
29
|
+
import { createSignal, For, onCleanup, onMount } from "solid-js";
|
|
20
30
|
|
|
21
31
|
// src/listeners.ts
|
|
22
32
|
function createListenerRegistry() {
|
|
@@ -85,9 +95,19 @@ function formatReset(seconds) {
|
|
|
85
95
|
if (hours > 0) return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
86
96
|
return `${minutes}m`;
|
|
87
97
|
}
|
|
98
|
+
var BLOCK_PARTIALS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
|
|
99
|
+
function formatUsageBar(usagePercent, width = 10) {
|
|
100
|
+
const percent = Math.max(0, Math.min(100, usagePercent));
|
|
101
|
+
const steps = width * 8;
|
|
102
|
+
const filled = Math.min(steps, Math.max(0, Math.round(percent / 100 * steps)));
|
|
103
|
+
const full = Math.floor(filled / 8);
|
|
104
|
+
const remainder = filled % 8;
|
|
105
|
+
const partial = BLOCK_PARTIALS[remainder] ?? "";
|
|
106
|
+
const empty = Math.max(0, width - full - (partial.length > 0 ? 1 : 0));
|
|
107
|
+
return "\u2588".repeat(full) + partial + "\u2591".repeat(empty);
|
|
108
|
+
}
|
|
88
109
|
function formatGoUsageRow(row) {
|
|
89
|
-
const
|
|
90
|
-
const bar = "\u2588".repeat(fill) + "\u2591".repeat(4 - fill);
|
|
110
|
+
const bar = formatUsageBar(row.usagePercent);
|
|
91
111
|
return `${row.label.padEnd(9)}${`${row.usagePercent}%`.padEnd(3)} ${bar} ${row.reset}`;
|
|
92
112
|
}
|
|
93
113
|
function goUsageSectionVisible(config, usesGoProvider) {
|
|
@@ -151,9 +171,97 @@ function createGoUsageRefresher(config, fetchImpl = fetch) {
|
|
|
151
171
|
return { state, refresh };
|
|
152
172
|
}
|
|
153
173
|
|
|
174
|
+
// src/activity-hydrate.ts
|
|
175
|
+
var CONCURRENCY_LIMIT = 4;
|
|
176
|
+
var LIST_LIMIT = 1e3;
|
|
177
|
+
function isSessionID(value) {
|
|
178
|
+
return value.startsWith("ses");
|
|
179
|
+
}
|
|
180
|
+
async function mapConcurrent(items, limit, fn) {
|
|
181
|
+
const results = new Array(items.length);
|
|
182
|
+
let next = 0;
|
|
183
|
+
async function worker() {
|
|
184
|
+
while (true) {
|
|
185
|
+
const index = next;
|
|
186
|
+
next += 1;
|
|
187
|
+
if (index >= items.length) return;
|
|
188
|
+
results[index] = await fn(items[index]);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
function collectUnhydrated(state, rootSessionID) {
|
|
195
|
+
const visited = /* @__PURE__ */ new Set();
|
|
196
|
+
const result = [];
|
|
197
|
+
const stack = [rootSessionID];
|
|
198
|
+
while (stack.length > 0) {
|
|
199
|
+
const sessionID = stack.pop();
|
|
200
|
+
if (sessionID === void 0 || visited.has(sessionID)) continue;
|
|
201
|
+
visited.add(sessionID);
|
|
202
|
+
if (!state.hydrated.has(sessionID) && !state.loading.has(sessionID)) result.push(sessionID);
|
|
203
|
+
const children = state.childrenByParent[sessionID] ?? [];
|
|
204
|
+
for (const child of children) {
|
|
205
|
+
if (!visited.has(child)) stack.push(child);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
function applyParts(state, sessionID, parts) {
|
|
211
|
+
for (const part of parts) {
|
|
212
|
+
const id2 = typeof part.id === "string" ? part.id : void 0;
|
|
213
|
+
const type = part.type;
|
|
214
|
+
if (type === "tool" && typeof part.tool === "string") {
|
|
215
|
+
recordToolPart(state, sessionID, {
|
|
216
|
+
...id2 !== void 0 ? { id: id2 } : {},
|
|
217
|
+
tool: part.tool,
|
|
218
|
+
...typeof part.state === "object" && part.state !== null && !Array.isArray(part.state) ? { state: part.state } : {}
|
|
219
|
+
});
|
|
220
|
+
} else if (type === "compaction" && id2 !== void 0) {
|
|
221
|
+
recordCompaction(state, sessionID, id2, part.auto === true);
|
|
222
|
+
} else if (type === "step-finish" && id2 !== void 0) {
|
|
223
|
+
recordStep(state, sessionID, id2);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function hydrateActivity(client, state, rootSessionID) {
|
|
228
|
+
if (!isSessionID(rootSessionID)) return;
|
|
229
|
+
let sessions = [];
|
|
230
|
+
try {
|
|
231
|
+
const response = await client.session.list({ limit: LIST_LIMIT });
|
|
232
|
+
sessions = response.data ?? [];
|
|
233
|
+
} catch {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
for (const session of sessions) {
|
|
237
|
+
if (session.id) {
|
|
238
|
+
if (session.title) state.titles[session.id] = session.title;
|
|
239
|
+
if (session.parentID) recordChild(state, session.id, session.parentID);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const toHydrate = collectUnhydrated(state, rootSessionID);
|
|
243
|
+
for (const sessionID of toHydrate) state.loading.add(sessionID);
|
|
244
|
+
await mapConcurrent(toHydrate, CONCURRENCY_LIMIT, async (sessionID) => {
|
|
245
|
+
try {
|
|
246
|
+
const response = await client.session.messages({ sessionID });
|
|
247
|
+
const messages = response.data ?? [];
|
|
248
|
+
for (const message of messages) {
|
|
249
|
+
if (message.parts && message.parts.length > 0) {
|
|
250
|
+
applyParts(state, sessionID, message.parts);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
state.hydrated.add(sessionID);
|
|
254
|
+
} catch {
|
|
255
|
+
} finally {
|
|
256
|
+
state.loading.delete(sessionID);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
154
261
|
// src/tui.tsx
|
|
262
|
+
import { useTerminalDimensions } from "@opentui/solid";
|
|
155
263
|
import { Fragment, jsx, jsxs } from "@opentui/solid/jsx-runtime";
|
|
156
|
-
function
|
|
264
|
+
function isSessionID2(value) {
|
|
157
265
|
return typeof value === "string" && value.startsWith("ses");
|
|
158
266
|
}
|
|
159
267
|
function PromptRightMetrics(props) {
|
|
@@ -393,6 +501,119 @@ function renderSubagentStyledSidebar(model, api, titleAttributes, collapsed, hov
|
|
|
393
501
|
}
|
|
394
502
|
return new StyledText(chunks);
|
|
395
503
|
}
|
|
504
|
+
function renderSessionAnalysisSidebar(lines, api, titleAttributes) {
|
|
505
|
+
const chunks = [
|
|
506
|
+
textChunk(`Session Analysis
|
|
507
|
+
`, api.theme.current.text, titleAttributes),
|
|
508
|
+
...lines.flatMap((line, index) => [
|
|
509
|
+
...index > 0 ? [textChunk("\n")] : [],
|
|
510
|
+
textChunk(line, api.theme.current.textMuted)
|
|
511
|
+
])
|
|
512
|
+
];
|
|
513
|
+
return new StyledText(chunks);
|
|
514
|
+
}
|
|
515
|
+
function SessionAnalysisDialog(props) {
|
|
516
|
+
const dimensions = useTerminalDimensions();
|
|
517
|
+
const titleAttributes = createTextAttributes({ bold: true });
|
|
518
|
+
const [collapsedSections, setCollapsedSections] = createSignal(/* @__PURE__ */ new Set());
|
|
519
|
+
const rows = buildSessionAnalysisRows(props.state, props.rootSessionID);
|
|
520
|
+
const loading = treeLoading(props.state, props.rootSessionID);
|
|
521
|
+
const maxHeight = Math.max(4, Math.floor(dimensions().height / 2) - 6);
|
|
522
|
+
onMount(() => props.api.ui.dialog.setSize("large"));
|
|
523
|
+
const toggleSection = (header) => {
|
|
524
|
+
setCollapsedSections((previous) => {
|
|
525
|
+
const next = new Set(previous);
|
|
526
|
+
if (next.has(header)) next.delete(header);
|
|
527
|
+
else next.add(header);
|
|
528
|
+
return next;
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
const visibleRows = [];
|
|
532
|
+
let currentHeader;
|
|
533
|
+
for (const row of rows) {
|
|
534
|
+
if (row.header) {
|
|
535
|
+
currentHeader = row.text;
|
|
536
|
+
visibleRows.push({ text: `${collapsedSections().has(row.text) ? "\u25B6" : "\u25BE"} ${row.text}`, header: true, key: row.text });
|
|
537
|
+
} else if (currentHeader === void 0 || !collapsedSections().has(currentHeader)) {
|
|
538
|
+
visibleRows.push(row);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", flexGrow: 1, paddingLeft: 4, paddingRight: 4, paddingTop: 1, children: [
|
|
542
|
+
/* @__PURE__ */ jsxs("box", { flexDirection: "row", justifyContent: "space-between", children: [
|
|
543
|
+
/* @__PURE__ */ jsx("text", { fg: props.api.theme.current.text, attributes: titleAttributes, children: "Session Analysis" }),
|
|
544
|
+
/* @__PURE__ */ jsx("text", { fg: props.api.theme.current.textMuted, onMouseUp: () => props.api.ui.dialog.clear(), children: "esc" })
|
|
545
|
+
] }),
|
|
546
|
+
/* @__PURE__ */ jsxs(
|
|
547
|
+
"scrollbox",
|
|
548
|
+
{
|
|
549
|
+
verticalScrollbarOptions: { visible: true },
|
|
550
|
+
maxHeight,
|
|
551
|
+
flexGrow: 1,
|
|
552
|
+
paddingTop: 1,
|
|
553
|
+
children: [
|
|
554
|
+
/* @__PURE__ */ jsx(For, { each: visibleRows, children: (row) => /* @__PURE__ */ jsx(
|
|
555
|
+
"text",
|
|
556
|
+
{
|
|
557
|
+
fg: row.header ? props.api.theme.current.text : props.api.theme.current.textMuted,
|
|
558
|
+
...row.header ? { attributes: titleAttributes } : {},
|
|
559
|
+
...row.header && row.key ? { onMouseUp: () => toggleSection(row.key ?? "") } : {},
|
|
560
|
+
children: row.text
|
|
561
|
+
}
|
|
562
|
+
) }),
|
|
563
|
+
/* @__PURE__ */ jsx(For, { each: loading ? [true] : [], children: () => /* @__PURE__ */ jsx("text", { fg: props.api.theme.current.textMuted, children: "loading\u2026" }) })
|
|
564
|
+
]
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
] });
|
|
568
|
+
}
|
|
569
|
+
function SessionAnalysisSidebar(props) {
|
|
570
|
+
let text;
|
|
571
|
+
const titleAttributes = createTextAttributes({ bold: true });
|
|
572
|
+
let previous;
|
|
573
|
+
const openDialog = (event) => {
|
|
574
|
+
if (!text) return;
|
|
575
|
+
props.api.ui.dialog.replace(() => /* @__PURE__ */ jsx(SessionAnalysisDialog, { api: props.api, state: props.state, rootSessionID: props.sessionID }));
|
|
576
|
+
};
|
|
577
|
+
const sync = () => {
|
|
578
|
+
if (!text) return;
|
|
579
|
+
const tree = treeActivity(props.state, props.sessionID);
|
|
580
|
+
const loading = treeLoading(props.state, props.sessionID);
|
|
581
|
+
const rows = formatActivityBriefRows(tree, treeSubagentCount(props.state, props.sessionID));
|
|
582
|
+
const lines = loading ? rows.length > 0 ? [...rows, "loading\u2026"] : ["loading\u2026"] : rows;
|
|
583
|
+
const visible = lines.length > 0;
|
|
584
|
+
const content = lines.join("\n");
|
|
585
|
+
const next = {
|
|
586
|
+
content,
|
|
587
|
+
visible,
|
|
588
|
+
height: visible ? "auto" : 0
|
|
589
|
+
};
|
|
590
|
+
if (!hasRenderStateChanged(previous, next)) return;
|
|
591
|
+
previous = next;
|
|
592
|
+
text.visible = next.visible;
|
|
593
|
+
text.height = next.height;
|
|
594
|
+
text.content = visible ? renderSessionAnalysisSidebar(lines, props.api, titleAttributes) : "";
|
|
595
|
+
props.api.renderer.requestRender();
|
|
596
|
+
};
|
|
597
|
+
const unsubscribe = props.subscribe(sync);
|
|
598
|
+
const timer = setInterval(sync, 1e3);
|
|
599
|
+
onCleanup(() => {
|
|
600
|
+
unsubscribe();
|
|
601
|
+
clearInterval(timer);
|
|
602
|
+
});
|
|
603
|
+
props.hydrate();
|
|
604
|
+
return /* @__PURE__ */ jsx(
|
|
605
|
+
"text",
|
|
606
|
+
{
|
|
607
|
+
ref: (ref) => {
|
|
608
|
+
text = ref;
|
|
609
|
+
sync();
|
|
610
|
+
},
|
|
611
|
+
onMouseUp: openDialog,
|
|
612
|
+
fg: props.api.theme.current.textMuted,
|
|
613
|
+
children: ""
|
|
614
|
+
}
|
|
615
|
+
);
|
|
616
|
+
}
|
|
396
617
|
function renderTokenUsageSidebar(content, api, titleAttributes, collapsed) {
|
|
397
618
|
const [title, ...details] = content.split("\n");
|
|
398
619
|
const visibleDetails = collapsed ? details.slice(0, 1) : details;
|
|
@@ -417,7 +638,9 @@ function textChunk(text, fg, attributes, bg) {
|
|
|
417
638
|
var tui = async (api, options) => {
|
|
418
639
|
const config = await readInsightsConfig(options ?? {});
|
|
419
640
|
const metrics = createMetricsState();
|
|
420
|
-
const
|
|
641
|
+
const activity = createActivityState();
|
|
642
|
+
const activityListeners = createListenerRegistry();
|
|
643
|
+
const subagents = createSubagentState(activity);
|
|
421
644
|
const metricListeners = createListenerRegistry();
|
|
422
645
|
const subagentListeners = createListenerRegistry();
|
|
423
646
|
const goUsageListeners = createListenerRegistry();
|
|
@@ -428,7 +651,7 @@ var tui = async (api, options) => {
|
|
|
428
651
|
if (await goUsage.refresh()) goUsageListeners.notify();
|
|
429
652
|
};
|
|
430
653
|
const hydrateSessionMetrics = async (sessionID) => {
|
|
431
|
-
if (!
|
|
654
|
+
if (!isSessionID2(sessionID) || hydratedSessions.has(sessionID)) return;
|
|
432
655
|
hydratedSessions.add(sessionID);
|
|
433
656
|
try {
|
|
434
657
|
const response = await api.client.session.messages({ sessionID });
|
|
@@ -490,19 +713,37 @@ var tui = async (api, options) => {
|
|
|
490
713
|
});
|
|
491
714
|
const offPart = api.event.on("message.part.updated", (evt) => {
|
|
492
715
|
const part = evt.properties.part;
|
|
716
|
+
const sessionID = part.sessionID ?? evt.properties.sessionID;
|
|
493
717
|
if (part.type === "tool") {
|
|
494
|
-
recordToolActivity(metrics,
|
|
718
|
+
recordToolActivity(metrics, sessionID, part.messageID, Date.now());
|
|
719
|
+
recordToolPart(activity, sessionID, part);
|
|
720
|
+
} else if (part.type === "compaction") {
|
|
721
|
+
recordCompaction(activity, sessionID, part.id, part.auto === true);
|
|
722
|
+
} else if (part.type === "step-finish") {
|
|
723
|
+
recordStep(activity, sessionID, part.id);
|
|
495
724
|
}
|
|
496
725
|
if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
|
|
497
726
|
metricListeners.notify();
|
|
727
|
+
activityListeners.notify();
|
|
498
728
|
});
|
|
499
729
|
const offSessionCreated = api.event.on("session.created", (evt) => {
|
|
500
730
|
if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
|
|
731
|
+
const info = evt.properties.info;
|
|
732
|
+
if (info && typeof info.id === "string") {
|
|
733
|
+
if (typeof info.title === "string") activity.titles[info.id] = info.title;
|
|
734
|
+
if (typeof info.parentID === "string") recordChild(activity, info.id, info.parentID);
|
|
735
|
+
activityListeners.notify();
|
|
736
|
+
}
|
|
501
737
|
});
|
|
502
738
|
const offSessionUpdated = api.event.on("session.updated", (evt) => {
|
|
503
739
|
if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
|
|
504
740
|
const info = evt.properties.info;
|
|
505
741
|
const sessionID = typeof info?.id === "string" ? info.id : void 0;
|
|
742
|
+
if (sessionID) {
|
|
743
|
+
if (typeof info?.title === "string") activity.titles[sessionID] = info.title;
|
|
744
|
+
if (typeof info?.parentID === "string") recordChild(activity, sessionID, info.parentID);
|
|
745
|
+
activityListeners.notify();
|
|
746
|
+
}
|
|
506
747
|
const providerID = info?.model?.providerID;
|
|
507
748
|
if (sessionID && typeof providerID === "string") {
|
|
508
749
|
goProviderTracker.record(sessionID, providerID);
|
|
@@ -527,7 +768,7 @@ var tui = async (api, options) => {
|
|
|
527
768
|
sessionID: props.session_id,
|
|
528
769
|
subscribe: metricListeners.subscribe,
|
|
529
770
|
text: () => {
|
|
530
|
-
if (!
|
|
771
|
+
if (!isSessionID2(props.session_id)) return "";
|
|
531
772
|
const status = api.state.session.status(props.session_id);
|
|
532
773
|
return renderPromptRightMetricsText(metrics, props.session_id, {
|
|
533
774
|
idle: status?.type === "idle",
|
|
@@ -537,6 +778,16 @@ var tui = async (api, options) => {
|
|
|
537
778
|
}
|
|
538
779
|
),
|
|
539
780
|
sidebar_content: (_ctx, props) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
781
|
+
/* @__PURE__ */ jsx(
|
|
782
|
+
SessionAnalysisSidebar,
|
|
783
|
+
{
|
|
784
|
+
api,
|
|
785
|
+
sessionID: props.session_id,
|
|
786
|
+
state: activity,
|
|
787
|
+
subscribe: activityListeners.subscribe,
|
|
788
|
+
hydrate: () => void hydrateActivity(api.client, activity, props.session_id).then(() => activityListeners.notify())
|
|
789
|
+
}
|
|
790
|
+
),
|
|
540
791
|
/* @__PURE__ */ jsx(
|
|
541
792
|
TokenUsageSidebar,
|
|
542
793
|
{
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@rejacky/opencode-insights",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "OpenCode plugin for local request capture, TPS metrics, and subagent status visibility.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "opencode-insights contributors",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsup",
|
|
49
49
|
"debug": "npm run build && node dist/cli.js debug",
|
|
50
|
+
"revert-debug": "node dist/cli.js revert",
|
|
50
51
|
"postinstall": "npm rebuild better-sqlite3",
|
|
51
52
|
"test": "vitest run",
|
|
52
53
|
"typecheck": "tsc --noEmit",
|