@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.
- package/dist/{chunk-O5FFLPEQ.js → chunk-SGXZJVYX.js} +242 -8
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +76 -11
- package/dist/index.d.ts +24 -1
- package/dist/index.js +1 -1
- package/dist/tui.js +249 -8
- package/package.json +2 -1
|
@@ -1,6 +1,229 @@
|
|
|
1
|
+
// src/activity.ts
|
|
2
|
+
function createActivityState() {
|
|
3
|
+
return {
|
|
4
|
+
bySessionID: {},
|
|
5
|
+
childrenByParent: {},
|
|
6
|
+
titles: {},
|
|
7
|
+
hydrated: /* @__PURE__ */ new Set(),
|
|
8
|
+
seenKeys: {},
|
|
9
|
+
loading: /* @__PURE__ */ new Set()
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function emptyActivity() {
|
|
13
|
+
return { toolCalls: 0, toolBreakdown: {}, warnings: 0, warningDetails: [], skills: {}, autoCompacts: 0, steps: 0 };
|
|
14
|
+
}
|
|
15
|
+
function hasActivity(a) {
|
|
16
|
+
if (!a) return false;
|
|
17
|
+
return a.toolCalls > 0 || a.warnings > 0 || a.autoCompacts > 0 || a.steps > 0 || Object.keys(a.skills).length > 0;
|
|
18
|
+
}
|
|
19
|
+
function recordChild(state, sessionID, parentID) {
|
|
20
|
+
const children = state.childrenByParent[parentID];
|
|
21
|
+
if (!children) {
|
|
22
|
+
state.childrenByParent[parentID] = [sessionID];
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!children.includes(sessionID)) children.push(sessionID);
|
|
26
|
+
}
|
|
27
|
+
function activityFor(state, sessionID) {
|
|
28
|
+
return state.bySessionID[sessionID] ??= emptyActivity();
|
|
29
|
+
}
|
|
30
|
+
function seen(state, sessionID, key) {
|
|
31
|
+
const keys = state.seenKeys[sessionID];
|
|
32
|
+
if (!keys) {
|
|
33
|
+
state.seenKeys[sessionID] = /* @__PURE__ */ new Set([key]);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (keys.has(key)) return true;
|
|
37
|
+
keys.add(key);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
function recordToolPart(state, sessionID, part) {
|
|
41
|
+
const activity = activityFor(state, sessionID);
|
|
42
|
+
let counted = false;
|
|
43
|
+
if (part.id !== void 0) {
|
|
44
|
+
if (!seen(state, sessionID, `tool:${part.id}`)) {
|
|
45
|
+
activity.toolCalls += 1;
|
|
46
|
+
activity.toolBreakdown[part.tool] = (activity.toolBreakdown[part.tool] ?? 0) + 1;
|
|
47
|
+
counted = true;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
activity.toolCalls += 1;
|
|
51
|
+
activity.toolBreakdown[part.tool] = (activity.toolBreakdown[part.tool] ?? 0) + 1;
|
|
52
|
+
counted = true;
|
|
53
|
+
}
|
|
54
|
+
const partState = part.state;
|
|
55
|
+
if (partState?.status === "error" && part.id !== void 0) {
|
|
56
|
+
if (!seen(state, sessionID, `warning:${part.id}`)) {
|
|
57
|
+
activity.warnings += 1;
|
|
58
|
+
if (typeof partState.error === "string" && partState.error.length > 0) {
|
|
59
|
+
activity.warningDetails.push({ tool: part.tool, message: partState.error });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (part.tool === "skill" && partState?.input && typeof partState.input.name === "string" && partState.input.name.length > 0) {
|
|
64
|
+
if (part.id !== void 0) {
|
|
65
|
+
if (!seen(state, sessionID, `skill:${part.id}`)) {
|
|
66
|
+
activity.skills[partState.input.name] = (activity.skills[partState.input.name] ?? 0) + 1;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
activity.skills[partState.input.name] = (activity.skills[partState.input.name] ?? 0) + 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return counted;
|
|
73
|
+
}
|
|
74
|
+
function recordCompaction(state, sessionID, id, auto) {
|
|
75
|
+
if (!auto) return false;
|
|
76
|
+
const activity = activityFor(state, sessionID);
|
|
77
|
+
if (seen(state, sessionID, `compact:${id}`)) return false;
|
|
78
|
+
activity.autoCompacts += 1;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
function recordStep(state, sessionID, id) {
|
|
82
|
+
const activity = activityFor(state, sessionID);
|
|
83
|
+
if (seen(state, sessionID, `step:${id}`)) return false;
|
|
84
|
+
activity.steps += 1;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
function mergeActivity(...activities) {
|
|
88
|
+
const result = emptyActivity();
|
|
89
|
+
for (const activity of activities) {
|
|
90
|
+
result.toolCalls += activity.toolCalls;
|
|
91
|
+
result.warnings += activity.warnings;
|
|
92
|
+
result.autoCompacts += activity.autoCompacts;
|
|
93
|
+
result.steps += activity.steps;
|
|
94
|
+
for (const [tool, count] of Object.entries(activity.toolBreakdown)) {
|
|
95
|
+
result.toolBreakdown[tool] = (result.toolBreakdown[tool] ?? 0) + count;
|
|
96
|
+
}
|
|
97
|
+
for (const [name, count] of Object.entries(activity.skills)) {
|
|
98
|
+
result.skills[name] = (result.skills[name] ?? 0) + count;
|
|
99
|
+
}
|
|
100
|
+
for (const detail of activity.warningDetails) {
|
|
101
|
+
result.warningDetails.push(detail);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
function collectTreeSessions(state, rootSessionID) {
|
|
107
|
+
const visited = /* @__PURE__ */ new Set();
|
|
108
|
+
const result = [];
|
|
109
|
+
const stack = [rootSessionID];
|
|
110
|
+
while (stack.length > 0) {
|
|
111
|
+
const sessionID = stack.pop();
|
|
112
|
+
if (sessionID === void 0 || visited.has(sessionID)) continue;
|
|
113
|
+
visited.add(sessionID);
|
|
114
|
+
result.push(sessionID);
|
|
115
|
+
const children = state.childrenByParent[sessionID] ?? [];
|
|
116
|
+
for (const child of children) {
|
|
117
|
+
if (!visited.has(child)) stack.push(child);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function treeActivity(state, rootSessionID) {
|
|
123
|
+
const activities = [];
|
|
124
|
+
for (const sessionID of collectTreeSessions(state, rootSessionID)) {
|
|
125
|
+
const activity = state.bySessionID[sessionID];
|
|
126
|
+
if (activity) activities.push(activity);
|
|
127
|
+
}
|
|
128
|
+
return mergeActivity(...activities);
|
|
129
|
+
}
|
|
130
|
+
function treeLoading(state, rootSessionID) {
|
|
131
|
+
return collectTreeSessions(state, rootSessionID).some((sessionID) => state.loading.has(sessionID));
|
|
132
|
+
}
|
|
133
|
+
function pluralize(count, singular, plural) {
|
|
134
|
+
return `${count} ${count === 1 ? singular : plural ?? `${singular}s`}`;
|
|
135
|
+
}
|
|
136
|
+
function formatActivitySuffix(a) {
|
|
137
|
+
if (!hasActivity(a)) return "";
|
|
138
|
+
const parts = [];
|
|
139
|
+
if (a && a.toolCalls > 0) parts.push(pluralize(a.toolCalls, "tool call"));
|
|
140
|
+
if (a && a.autoCompacts > 0) parts.push(pluralize(a.autoCompacts, "auto-compact"));
|
|
141
|
+
if (a && Object.keys(a.skills).length > 0) {
|
|
142
|
+
const total = Object.values(a.skills).reduce((sum, count) => sum + count, 0);
|
|
143
|
+
parts.push(pluralize(total, "skill"));
|
|
144
|
+
}
|
|
145
|
+
if (a && a.warnings > 0) parts.push(pluralize(a.warnings, "warning"));
|
|
146
|
+
return parts.join(" \xB7 ");
|
|
147
|
+
}
|
|
148
|
+
function formatActivityBriefRows(a, subagentCount) {
|
|
149
|
+
if (!hasActivity(a) && subagentCount <= 0) return [];
|
|
150
|
+
const rows = [];
|
|
151
|
+
if (a && a.toolCalls > 0) rows.push(pluralize(a.toolCalls, "tool call"));
|
|
152
|
+
if (a && a.autoCompacts > 0) rows.push(pluralize(a.autoCompacts, "auto-compact"));
|
|
153
|
+
if (a && Object.keys(a.skills).length > 0) {
|
|
154
|
+
const total = Object.values(a.skills).reduce((sum, count) => sum + count, 0);
|
|
155
|
+
rows.push(pluralize(total, "skill"));
|
|
156
|
+
}
|
|
157
|
+
if (a && a.steps > 0) rows.push(pluralize(a.steps, "model request"));
|
|
158
|
+
if (subagentCount > 0) rows.push(pluralize(subagentCount, "subagent"));
|
|
159
|
+
if (a && a.warnings > 0) rows.push(pluralize(a.warnings, "warning"));
|
|
160
|
+
return rows;
|
|
161
|
+
}
|
|
162
|
+
function truncateMiddle(value, maxLength) {
|
|
163
|
+
if (value.length <= maxLength) return value;
|
|
164
|
+
if (maxLength <= 3) return value.slice(0, maxLength);
|
|
165
|
+
const marker = "...";
|
|
166
|
+
const left = Math.ceil((maxLength - marker.length) / 2);
|
|
167
|
+
const right = Math.floor((maxLength - marker.length) / 2);
|
|
168
|
+
return `${value.slice(0, left)}${marker}${value.slice(value.length - right)}`;
|
|
169
|
+
}
|
|
170
|
+
function sortedBreakdown(breakdown) {
|
|
171
|
+
return Object.entries(breakdown).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
172
|
+
}
|
|
173
|
+
function indent(lines, depth) {
|
|
174
|
+
return lines.map((text) => `${" ".repeat(depth)}${text}`);
|
|
175
|
+
}
|
|
176
|
+
function treeSubagentCount(state, rootSessionID) {
|
|
177
|
+
return Math.max(0, collectTreeSessions(state, rootSessionID).length - 1);
|
|
178
|
+
}
|
|
179
|
+
function buildSessionAnalysisRows(state, rootSessionID) {
|
|
180
|
+
const rows = [];
|
|
181
|
+
const children = (sessionID) => state.childrenByParent[sessionID] ?? [];
|
|
182
|
+
const activityOf = (sessionID) => state.bySessionID[sessionID] ?? emptyActivity();
|
|
183
|
+
const titleOf = (sessionID) => state.titles[sessionID] ?? sessionID;
|
|
184
|
+
const childIds = [];
|
|
185
|
+
const visit = (sessionID, depth) => {
|
|
186
|
+
for (const childID of children(sessionID)) {
|
|
187
|
+
childIds.push(childID);
|
|
188
|
+
const childActivity = activityOf(childID);
|
|
189
|
+
const suffix = formatActivitySuffix(childActivity);
|
|
190
|
+
const line = `\xB7 ${truncateMiddle(titleOf(childID), 36)}${suffix ? ` ${suffix}` : ""}`;
|
|
191
|
+
rows.push(`${" ".repeat(depth + 1)}${line}`);
|
|
192
|
+
visit(childID, depth + 1);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
visit(rootSessionID, 0);
|
|
196
|
+
const tree = treeActivity(state, rootSessionID);
|
|
197
|
+
const sections = [];
|
|
198
|
+
if (tree.toolCalls > 0) {
|
|
199
|
+
sections.push([`Tool calls (${tree.toolCalls})`, indent(sortedBreakdown(tree.toolBreakdown).map(([tool, count]) => `\xB7 ${tool} ${count}`), 1)]);
|
|
200
|
+
}
|
|
201
|
+
if (Object.keys(tree.skills).length > 0) {
|
|
202
|
+
const total = Object.values(tree.skills).reduce((sum, count) => sum + count, 0);
|
|
203
|
+
sections.push([`Skills (${total})`, indent(sortedBreakdown(tree.skills).map(([name, count]) => `\xB7 ${name} ${count}`), 1)]);
|
|
204
|
+
}
|
|
205
|
+
if (tree.autoCompacts > 0) {
|
|
206
|
+
sections.push([`Auto-compactions`, [` \xB7 ${tree.autoCompacts}`]]);
|
|
207
|
+
}
|
|
208
|
+
if (tree.steps > 0) {
|
|
209
|
+
sections.push([`Model requests (${tree.steps})`, [` \xB7 ${tree.steps}`]]);
|
|
210
|
+
}
|
|
211
|
+
if (childIds.length > 0 && hasActivity(tree)) {
|
|
212
|
+
sections.push([`Subagents`, rows]);
|
|
213
|
+
}
|
|
214
|
+
if (tree.warnings > 0) {
|
|
215
|
+
const details = tree.warningDetails.length > 0 ? tree.warningDetails.map((detail) => ` \xB7 ${truncateMiddle(detail.tool, 24)} \u2014 ${truncateMiddle(detail.message, 72)}`) : [` \xB7 ${pluralize(tree.warnings, "warning")}`];
|
|
216
|
+
sections.push([`Tool warnings (${tree.warnings})`, details]);
|
|
217
|
+
}
|
|
218
|
+
return sections.flatMap(([title, body]) => [
|
|
219
|
+
{ text: title, header: true },
|
|
220
|
+
...body.map((text) => ({ text, header: false }))
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
|
|
1
224
|
// src/subagents.ts
|
|
2
|
-
function createSubagentState() {
|
|
3
|
-
return { children: {}, totalExecuted: 0 };
|
|
225
|
+
function createSubagentState(activityStore) {
|
|
226
|
+
return { children: {}, totalExecuted: 0, ...activityStore ? { activityStore } : {} };
|
|
4
227
|
}
|
|
5
228
|
function applySubagentEvent(state, event) {
|
|
6
229
|
const created = extractTaskToolSubagent(event) ?? extractSubagent(event) ?? updateExistingSubagent(state, event);
|
|
@@ -20,7 +243,8 @@ function applySubagentEvent(state, event) {
|
|
|
20
243
|
updatedAt: created.updatedAt,
|
|
21
244
|
endedAt,
|
|
22
245
|
elapsedMs: elapsedMs(startedAt, endedAt ?? created.updatedAt),
|
|
23
|
-
tokens: created.tokens ?? previous?.tokens
|
|
246
|
+
tokens: created.tokens ?? previous?.tokens,
|
|
247
|
+
activity: state.activityStore ? state.activityStore.bySessionID[created.id] ??= emptyActivity() : void 0
|
|
24
248
|
};
|
|
25
249
|
if (!previous) state.totalExecuted += 1;
|
|
26
250
|
state.children[created.id] = next;
|
|
@@ -78,7 +302,7 @@ function getSubagentSidebarModel(state, parentID, options = {}) {
|
|
|
78
302
|
rows: children.map((child) => ({
|
|
79
303
|
id: child.id,
|
|
80
304
|
title: formatSubagentTitle(child.title),
|
|
81
|
-
subtitle: [formatSubagentDuration(child, options.now), formatUsage(child)].filter(Boolean).join(" \xB7 "),
|
|
305
|
+
subtitle: [formatSubagentDuration(child, options.now), formatUsage(child), formatActivitySuffix(child.activity)].filter(Boolean).join(" \xB7 "),
|
|
82
306
|
status: child.status
|
|
83
307
|
}))
|
|
84
308
|
};
|
|
@@ -265,13 +489,13 @@ function statusSortRank(status) {
|
|
|
265
489
|
}
|
|
266
490
|
function formatSubagentTitle(title) {
|
|
267
491
|
const match = title.match(/^(?:[✓✗!]\s*)?([A-Za-z][\w -]*?)\s+[—-]\s+(.+)$/u);
|
|
268
|
-
if (!match) return
|
|
492
|
+
if (!match) return truncateMiddle2(title, 36);
|
|
269
493
|
const agent = match[1]?.trim();
|
|
270
494
|
const task = match[2]?.trim();
|
|
271
|
-
if (!agent || !task) return
|
|
272
|
-
return
|
|
495
|
+
if (!agent || !task) return truncateMiddle2(title, 36);
|
|
496
|
+
return truncateMiddle2(`${agent}: ${task}`, 36);
|
|
273
497
|
}
|
|
274
|
-
function
|
|
498
|
+
function truncateMiddle2(value, maxLength) {
|
|
275
499
|
if (value.length <= maxLength) return value;
|
|
276
500
|
if (maxLength <= 3) return value.slice(0, maxLength);
|
|
277
501
|
const marker = "...";
|
|
@@ -300,6 +524,16 @@ function isRecord(value) {
|
|
|
300
524
|
}
|
|
301
525
|
|
|
302
526
|
export {
|
|
527
|
+
createActivityState,
|
|
528
|
+
recordChild,
|
|
529
|
+
recordToolPart,
|
|
530
|
+
recordCompaction,
|
|
531
|
+
recordStep,
|
|
532
|
+
treeActivity,
|
|
533
|
+
treeLoading,
|
|
534
|
+
formatActivityBriefRows,
|
|
535
|
+
treeSubagentCount,
|
|
536
|
+
buildSessionAnalysisRows,
|
|
303
537
|
createSubagentState,
|
|
304
538
|
applySubagentEvent,
|
|
305
539
|
renderSubagentStatus,
|
package/dist/cli.d.ts
CHANGED
|
@@ -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,
|
|
1739
|
-
setSinglePluginSpec(tuiConfig,
|
|
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,
|
|
1910
|
+
function setSinglePluginSpec(config, nextPlugin) {
|
|
1853
1911
|
const current = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1912
|
+
const next = current.filter(
|
|
1913
|
+
(entry) => !isInsightsPluginEntry(entry) && entry !== nextPlugin && !(Array.isArray(entry) && entry[0] === nextPlugin)
|
|
1914
|
+
);
|
|
1856
1915
|
config.plugin = [...next, nextPlugin];
|
|
1857
1916
|
}
|
|
1858
|
-
function isInsightsPluginEntry(entry
|
|
1859
|
-
if (isPluginEntry(entry, packagePlugin) || isPluginEntry(entry, localPlugin) || isPluginEntry(entry, SUBPATH_TUI_PLUGIN_SPEC)) {
|
|
1860
|
-
return true;
|
|
1861
|
-
}
|
|
1917
|
+
function isInsightsPluginEntry(entry) {
|
|
1862
1918
|
const spec = Array.isArray(entry) ? entry[0] : entry;
|
|
1863
1919
|
if (typeof spec !== "string") return false;
|
|
1864
|
-
return
|
|
1920
|
+
return isInsightsSpec(spec);
|
|
1921
|
+
}
|
|
1922
|
+
function isInsightsSpec(spec) {
|
|
1923
|
+
if (spec === SERVER_PLUGIN_SPEC || spec === SUBPATH_TUI_PLUGIN_SPEC) return true;
|
|
1924
|
+
if (spec.startsWith("npm:")) return isInsightsSpec(spec.slice(4));
|
|
1925
|
+
if (spec.startsWith(`${SERVER_PLUGIN_SPEC}@`)) return true;
|
|
1926
|
+
const normalized = spec.replaceAll("\\", "/");
|
|
1927
|
+
return /(?:^|[/@-])opencode-insights.*\.tgz$/u.test(normalized) || /\/opencode-insights\/dist\/(?:index|tui)\.js$/u.test(normalized);
|
|
1865
1928
|
}
|
|
1866
1929
|
async function uninstallOpenCode(options) {
|
|
1867
1930
|
const configDir = options.configDir ?? defaultOpenCodeConfigDir();
|
|
@@ -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
package/dist/tui.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applySubagentEvent,
|
|
3
|
+
buildSessionAnalysisRows,
|
|
4
|
+
createActivityState,
|
|
3
5
|
createSubagentState,
|
|
6
|
+
formatActivityBriefRows,
|
|
4
7
|
getSubagentSidebarModel,
|
|
5
|
-
getSubagentSidebarRowAtLine
|
|
6
|
-
|
|
8
|
+
getSubagentSidebarRowAtLine,
|
|
9
|
+
recordChild,
|
|
10
|
+
recordCompaction,
|
|
11
|
+
recordStep,
|
|
12
|
+
recordToolPart,
|
|
13
|
+
treeActivity,
|
|
14
|
+
treeLoading,
|
|
15
|
+
treeSubagentCount
|
|
16
|
+
} from "./chunk-SGXZJVYX.js";
|
|
7
17
|
import {
|
|
8
18
|
createMetricsState,
|
|
9
19
|
readInsightsConfig,
|
|
@@ -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
|
|
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
|
|
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 (!
|
|
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,
|
|
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 (!
|
|
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.
|
|
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",
|