@rejacky/opencode-insights 0.2.0 → 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.
@@ -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
@@ -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, unsupportedFlagWarning };
99
+ export { addUniquePlugin, configureOpenCodeDebug, defaultOpenCodeConfigDir, formatSessionSummary, parseOptions, removePlugin, resolveOpenCodeConfigPath, revertOpenCodeDebug, stripJsonCommentsAndTrailingCommas, summarizeSessions, uninstallOpenCode, unsupportedFlagWarning };
package/dist/cli.js CHANGED
@@ -1544,6 +1544,11 @@ async function main(argv) {
1544
1544
  }
1545
1545
  if (command === "debug") {
1546
1546
  process.stdout.write(`${await configureOpenCodeDebug(options)}
1547
+ `);
1548
+ return;
1549
+ }
1550
+ if (command === "revert") {
1551
+ process.stdout.write(`${await revertOpenCodeDebug(options)}
1547
1552
  `);
1548
1553
  return;
1549
1554
  }
@@ -1735,9 +1740,8 @@ async function configureOpenCodeDebug(options) {
1735
1740
  const tuiSource = await readJsonConfigSource(tuiPath);
1736
1741
  const opencodeConfig = await readJsonConfig(opencodePath, { plugin: [] }, opencodeSource);
1737
1742
  const tuiConfig = await readJsonConfig(tuiPath, { plugin: [] }, tuiSource);
1738
- setSinglePluginSpec(opencodeConfig, SERVER_PLUGIN_SPEC, localServerEntry);
1739
- setSinglePluginSpec(tuiConfig, TUI_PLUGIN_SPEC, localTuiEntry);
1740
- removePlugin(tuiConfig, SUBPATH_TUI_PLUGIN_SPEC);
1743
+ setSinglePluginSpec(opencodeConfig, localServerEntry);
1744
+ setSinglePluginSpec(tuiConfig, localTuiEntry);
1741
1745
  const lines = [
1742
1746
  `OpenCode config: ${opencodePath}`,
1743
1747
  `TUI config: ${tuiPath}`,
@@ -1758,6 +1762,60 @@ async function configureOpenCodeDebug(options) {
1758
1762
  lines.push("Debug configuration written. Restart OpenCode to load the local build.");
1759
1763
  return lines.join("\n");
1760
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
+ }
1761
1819
  function defaultOpenCodeConfigDir() {
1762
1820
  const override = process.env.OPENCODE_CONFIG_DIR;
1763
1821
  if (override) return override;
@@ -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();
@@ -1929,6 +1992,7 @@ function usage() {
1929
1992
  return [
1930
1993
  "Usage:",
1931
1994
  " opencode-insights debug [--config-dir DIR] [--dry-run]",
1995
+ " opencode-insights revert [--config-dir DIR] [--dry-run]",
1932
1996
  " opencode-insights uninstall [--config-dir DIR] [--keep-data] [--dry-run]",
1933
1997
  " opencode-insights recent [--limit N] [--json]",
1934
1998
  " opencode-insights sessions [--limit N] [--json]",
@@ -1965,6 +2029,7 @@ export {
1965
2029
  parseOptions,
1966
2030
  removePlugin,
1967
2031
  resolveOpenCodeConfigPath,
2032
+ revertOpenCodeDebug,
1968
2033
  stripJsonCommentsAndTrailingCommas,
1969
2034
  summarizeSessions,
1970
2035
  uninstallOpenCode,
package/dist/index.d.ts CHANGED
@@ -2,6 +2,27 @@ import { Plugin } from '@opencode-ai/plugin';
2
2
  import { TuiPlugin } from '@opencode-ai/plugin/tui';
3
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
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
+ };
25
+
5
26
  type SubagentStatus = "running" | "done" | "error";
6
27
  type SubagentInfo = {
7
28
  id: string;
@@ -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,
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,
@@ -16,7 +26,7 @@ import {
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() {
@@ -161,9 +171,97 @@ function createGoUsageRefresher(config, fetchImpl = fetch) {
161
171
  return { state, refresh };
162
172
  }
163
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
+
164
261
  // src/tui.tsx
262
+ import { useTerminalDimensions } from "@opentui/solid";
165
263
  import { Fragment, jsx, jsxs } from "@opentui/solid/jsx-runtime";
166
- function isSessionID(value) {
264
+ function isSessionID2(value) {
167
265
  return typeof value === "string" && value.startsWith("ses");
168
266
  }
169
267
  function PromptRightMetrics(props) {
@@ -403,6 +501,119 @@ function renderSubagentStyledSidebar(model, api, titleAttributes, collapsed, hov
403
501
  }
404
502
  return new StyledText(chunks);
405
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
+ }
406
617
  function renderTokenUsageSidebar(content, api, titleAttributes, collapsed) {
407
618
  const [title, ...details] = content.split("\n");
408
619
  const visibleDetails = collapsed ? details.slice(0, 1) : details;
@@ -427,7 +638,9 @@ function textChunk(text, fg, attributes, bg) {
427
638
  var tui = async (api, options) => {
428
639
  const config = await readInsightsConfig(options ?? {});
429
640
  const metrics = createMetricsState();
430
- const subagents = createSubagentState();
641
+ const activity = createActivityState();
642
+ const activityListeners = createListenerRegistry();
643
+ const subagents = createSubagentState(activity);
431
644
  const metricListeners = createListenerRegistry();
432
645
  const subagentListeners = createListenerRegistry();
433
646
  const goUsageListeners = createListenerRegistry();
@@ -438,7 +651,7 @@ var tui = async (api, options) => {
438
651
  if (await goUsage.refresh()) goUsageListeners.notify();
439
652
  };
440
653
  const hydrateSessionMetrics = async (sessionID) => {
441
- if (!isSessionID(sessionID) || hydratedSessions.has(sessionID)) return;
654
+ if (!isSessionID2(sessionID) || hydratedSessions.has(sessionID)) return;
442
655
  hydratedSessions.add(sessionID);
443
656
  try {
444
657
  const response = await api.client.session.messages({ sessionID });
@@ -500,19 +713,37 @@ var tui = async (api, options) => {
500
713
  });
501
714
  const offPart = api.event.on("message.part.updated", (evt) => {
502
715
  const part = evt.properties.part;
716
+ const sessionID = part.sessionID ?? evt.properties.sessionID;
503
717
  if (part.type === "tool") {
504
- 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);
505
724
  }
506
725
  if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
507
726
  metricListeners.notify();
727
+ activityListeners.notify();
508
728
  });
509
729
  const offSessionCreated = api.event.on("session.created", (evt) => {
510
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
+ }
511
737
  });
512
738
  const offSessionUpdated = api.event.on("session.updated", (evt) => {
513
739
  if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
514
740
  const info = evt.properties.info;
515
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
+ }
516
747
  const providerID = info?.model?.providerID;
517
748
  if (sessionID && typeof providerID === "string") {
518
749
  goProviderTracker.record(sessionID, providerID);
@@ -537,7 +768,7 @@ var tui = async (api, options) => {
537
768
  sessionID: props.session_id,
538
769
  subscribe: metricListeners.subscribe,
539
770
  text: () => {
540
- if (!isSessionID(props.session_id)) return "";
771
+ if (!isSessionID2(props.session_id)) return "";
541
772
  const status = api.state.session.status(props.session_id);
542
773
  return renderPromptRightMetricsText(metrics, props.session_id, {
543
774
  idle: status?.type === "idle",
@@ -547,6 +778,16 @@ var tui = async (api, options) => {
547
778
  }
548
779
  ),
549
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
+ ),
550
791
  /* @__PURE__ */ jsx(
551
792
  TokenUsageSidebar,
552
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.2.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",