@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 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 startup and after new captures. Set `retentionDays` to another number of days, or `0` to disable auto-cleaning.
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
- You can override storage and retention in `opencode.json` or `opencode.jsonc`:
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
- ```json
250
+ ```jsonc
247
251
  {
248
- "plugin": [
249
- [
250
- "@rejacky/opencode-insights",
251
- {
252
- "dbPath": "/absolute/path/to/insights.sqlite",
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, renderMetricsText as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, renderPromptRightMetricsText as E, renderResponseMetricsText as F, type GoUsageConfig as G, renderSessionTokenUsage as H, type InsightsConfig as I, JsonlCaptureStore as J, resolveCapturePath as K, resolveInsightsConfigPath as L, type MessageTiming as M, resolveRetentionDays as N, type PromptRightMetric as P, 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, normalizeChatHeadersCapture as o, normalizeChatMessageCapture as p, normalizeChatParamsCapture as q, normalizeEventCapture as r, normalizeExperimentalChatMessagesTransformCapture as s, normalizeExperimentalChatSystemTransformCapture as t, normalizeToolCapture as u, openDatabase as v, readInsightsConfig as w, recordAssistantDelta as x, recordAssistantMessage as y, recordToolActivity as z };
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(dirname(resolveCapturePath(options)), "config.json");
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 path = resolveInsightsConfigPath(options);
335
- if (!existsSync(path)) {
336
- try {
337
- await mkdir(dirname(path), { recursive: true });
338
- await writeFile(path, `${JSON.stringify(defaultInsightsConfig(), null, 2)}
339
- `, "utf8");
340
- } catch {
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
- return insightsConfigFrom(JSON.parse(await readFile(path, "utf8")));
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 truncateMiddle(title, 36);
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 truncateMiddle(title, 36);
272
- return truncateMiddle(`${agent}: ${task}`, 36);
495
+ if (!agent || !task) return truncateMiddle2(title, 36);
496
+ return truncateMiddle2(`${agent}: ${task}`, 36);
273
497
  }
274
- function truncateMiddle(value, maxLength) {
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-BpktoFGh.js';
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
- resolveCapturePath
5
- } from "./chunk-RWPY5QOE.js";
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, SERVER_PLUGIN_SPEC, [localServerEntry, debugServerOptions(options)], localServerEntry);
1736
- setSinglePluginSpec(tuiConfig, TUI_PLUGIN_SPEC, localTuiEntry);
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, previousPlugin, nextPlugin, nextPluginSpec) {
1910
+ function setSinglePluginSpec(config, nextPlugin) {
1853
1911
  const current = Array.isArray(config.plugin) ? config.plugin : [];
1854
- const localPlugin = nextPluginSpec ?? (typeof nextPlugin === "string" ? nextPlugin : "");
1855
- const next = current.filter((entry) => !isInsightsPluginEntry(entry, previousPlugin, localPlugin));
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, packagePlugin, localPlugin) {
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 /(?:^|[/@-])opencode-insights.*\.tgz$/u.test(spec) || /\/opencode-insights\/dist\/(?:index|tui)\.js$/u.test(spec);
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] [--retention-days DAYS] [--dry-run]",
1932
- " opencode-insights uninstall [--config-dir DIR] [--db PATH] [--data-dir DIR] [--keep-data] [--dry-run]",
1933
- " opencode-insights recent [--db PATH] [--data-dir DIR] [--limit N] [--json]",
1934
- " opencode-insights sessions [--db PATH] [--data-dir DIR] [--limit N] [--json]",
1935
- " opencode-insights history [--db PATH] [--data-dir DIR] [--limit N]",
1936
- " opencode-insights show <session-id> [--db PATH] [--data-dir DIR] [--limit N]",
1937
- " opencode-insights export <session-id> [--output PATH] [--db PATH] [--data-dir DIR] [--limit N]",
1938
- " opencode-insights serve [--db PATH] [--data-dir DIR] [--limit N] [--host HOST] [--port PORT]",
1939
- " opencode-insights open [--db PATH] [--data-dir DIR] [--limit N] [--host HOST] [--port PORT]",
1940
- " opencode-insights doctor [--db PATH] [--data-dir DIR]",
1941
- " opencode-insights vacuum [--db PATH] [--data-dir DIR]"
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 normalizeChatHeadersCapture, p as normalizeChatMessageCapture, q as normalizeChatParamsCapture, r as normalizeEventCapture, s as normalizeExperimentalChatMessagesTransformCapture, t as normalizeExperimentalChatSystemTransformCapture, u as normalizeToolCapture, v as openDatabase, w as readInsightsConfig, x as recordAssistantDelta, y as recordAssistantMessage, z as recordToolActivity, B as renderMetricsText, E as renderPromptRightMetricsText, F as renderResponseMetricsText, H as renderSessionTokenUsage, K as resolveCapturePath, L as resolveInsightsConfigPath, N as resolveRetentionDays } from './capture-BpktoFGh.js';
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-O5FFLPEQ.js";
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-RWPY5QOE.js";
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 store = createCaptureStore(options);
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
- } from "./chunk-O5FFLPEQ.js";
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-RWPY5QOE.js";
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 fill = Math.min(4, Math.ceil(row.usagePercent / 25));
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 isSessionID(value) {
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 subagents = createSubagentState();
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 (!isSessionID(sessionID) || hydratedSessions.has(sessionID)) return;
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, part.sessionID ?? evt.properties.sessionID, part.messageID, Date.now());
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 (!isSessionID(props.session_id)) return "";
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.1.13",
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",