@rejacky/opencode-insights 0.2.0 → 0.3.1
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 +2 -1
- 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 +248 -8
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -95,11 +95,12 @@ The `uninstall` command removes plugin config entries and local Insights data; i
|
|
|
95
95
|
- A collapsible session-wide `Token Usage` sidebar showing total tokens, response count, input/output/reasoning usage, cache read/write usage, and aggregate cache rate. It loads completed responses already present in the session and continues updating live.
|
|
96
96
|
- An opt-in `Go Usage` sidebar showing OpenCode Go rolling/weekly/monthly usage limits for sessions that use the `opencode-go` provider.
|
|
97
97
|
- Subagent status (running, done, failed, elapsed time, and token/context usage) in the sidebar.
|
|
98
|
+
- A collapsible `Session Analysis` sidebar showing the active session's aggregated activity (tool calls, skills, auto-compactions, warnings, model requests, subagent tree). Click it to open a detail dialog; its vertical scrollbar appears only when the content overflows the dialog.
|
|
98
99
|
- Local capture of OpenCode hook/event data without redaction.
|
|
99
100
|
- A local web viewer for reconstructed sessions, user turns, hidden request context, system/messages transforms, and assistant thinking/response sequences.
|
|
100
101
|
- Native OpenCode footer components (project directory and version) remain visible — the plugin does not override `sidebar_footer` or `home_prompt_right` slots.
|
|
101
102
|
|
|
102
|
-
The right sidebar contains the plugin sections: `Token Usage`, `Go Usage` (when enabled and the session uses `opencode-go`), and `
|
|
103
|
+
The right sidebar contains the plugin sections: `Token Usage`, `Go Usage` (when enabled and the session uses `opencode-go`), `Subagents`, and `Session Analysis`. Click any section header to collapse or expand it. Token usage is aggregated across the full session; prompt-right `used` and `cache` values continue to represent the latest completed assistant response.
|
|
103
104
|
|
|
104
105
|
## TUI Metrics Configuration
|
|
105
106
|
|
|
@@ -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,118 @@ 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
|
+
maxHeight,
|
|
550
|
+
flexGrow: 1,
|
|
551
|
+
paddingTop: 1,
|
|
552
|
+
children: [
|
|
553
|
+
/* @__PURE__ */ jsx(For, { each: visibleRows, children: (row) => /* @__PURE__ */ jsx(
|
|
554
|
+
"text",
|
|
555
|
+
{
|
|
556
|
+
fg: row.header ? props.api.theme.current.text : props.api.theme.current.textMuted,
|
|
557
|
+
...row.header ? { attributes: titleAttributes } : {},
|
|
558
|
+
...row.header && row.key ? { onMouseUp: () => toggleSection(row.key ?? "") } : {},
|
|
559
|
+
children: row.text
|
|
560
|
+
}
|
|
561
|
+
) }),
|
|
562
|
+
/* @__PURE__ */ jsx(For, { each: loading ? [true] : [], children: () => /* @__PURE__ */ jsx("text", { fg: props.api.theme.current.textMuted, children: "loading\u2026" }) })
|
|
563
|
+
]
|
|
564
|
+
}
|
|
565
|
+
)
|
|
566
|
+
] });
|
|
567
|
+
}
|
|
568
|
+
function SessionAnalysisSidebar(props) {
|
|
569
|
+
let text;
|
|
570
|
+
const titleAttributes = createTextAttributes({ bold: true });
|
|
571
|
+
let previous;
|
|
572
|
+
const openDialog = (event) => {
|
|
573
|
+
if (!text) return;
|
|
574
|
+
props.api.ui.dialog.replace(() => /* @__PURE__ */ jsx(SessionAnalysisDialog, { api: props.api, state: props.state, rootSessionID: props.sessionID }));
|
|
575
|
+
};
|
|
576
|
+
const sync = () => {
|
|
577
|
+
if (!text) return;
|
|
578
|
+
const tree = treeActivity(props.state, props.sessionID);
|
|
579
|
+
const loading = treeLoading(props.state, props.sessionID);
|
|
580
|
+
const rows = formatActivityBriefRows(tree, treeSubagentCount(props.state, props.sessionID));
|
|
581
|
+
const lines = loading ? rows.length > 0 ? [...rows, "loading\u2026"] : ["loading\u2026"] : rows;
|
|
582
|
+
const visible = lines.length > 0;
|
|
583
|
+
const content = lines.join("\n");
|
|
584
|
+
const next = {
|
|
585
|
+
content,
|
|
586
|
+
visible,
|
|
587
|
+
height: visible ? "auto" : 0
|
|
588
|
+
};
|
|
589
|
+
if (!hasRenderStateChanged(previous, next)) return;
|
|
590
|
+
previous = next;
|
|
591
|
+
text.visible = next.visible;
|
|
592
|
+
text.height = next.height;
|
|
593
|
+
text.content = visible ? renderSessionAnalysisSidebar(lines, props.api, titleAttributes) : "";
|
|
594
|
+
props.api.renderer.requestRender();
|
|
595
|
+
};
|
|
596
|
+
const unsubscribe = props.subscribe(sync);
|
|
597
|
+
const timer = setInterval(sync, 1e3);
|
|
598
|
+
onCleanup(() => {
|
|
599
|
+
unsubscribe();
|
|
600
|
+
clearInterval(timer);
|
|
601
|
+
});
|
|
602
|
+
props.hydrate();
|
|
603
|
+
return /* @__PURE__ */ jsx(
|
|
604
|
+
"text",
|
|
605
|
+
{
|
|
606
|
+
ref: (ref) => {
|
|
607
|
+
text = ref;
|
|
608
|
+
sync();
|
|
609
|
+
},
|
|
610
|
+
onMouseUp: openDialog,
|
|
611
|
+
fg: props.api.theme.current.textMuted,
|
|
612
|
+
children: ""
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
}
|
|
406
616
|
function renderTokenUsageSidebar(content, api, titleAttributes, collapsed) {
|
|
407
617
|
const [title, ...details] = content.split("\n");
|
|
408
618
|
const visibleDetails = collapsed ? details.slice(0, 1) : details;
|
|
@@ -427,7 +637,9 @@ function textChunk(text, fg, attributes, bg) {
|
|
|
427
637
|
var tui = async (api, options) => {
|
|
428
638
|
const config = await readInsightsConfig(options ?? {});
|
|
429
639
|
const metrics = createMetricsState();
|
|
430
|
-
const
|
|
640
|
+
const activity = createActivityState();
|
|
641
|
+
const activityListeners = createListenerRegistry();
|
|
642
|
+
const subagents = createSubagentState(activity);
|
|
431
643
|
const metricListeners = createListenerRegistry();
|
|
432
644
|
const subagentListeners = createListenerRegistry();
|
|
433
645
|
const goUsageListeners = createListenerRegistry();
|
|
@@ -438,7 +650,7 @@ var tui = async (api, options) => {
|
|
|
438
650
|
if (await goUsage.refresh()) goUsageListeners.notify();
|
|
439
651
|
};
|
|
440
652
|
const hydrateSessionMetrics = async (sessionID) => {
|
|
441
|
-
if (!
|
|
653
|
+
if (!isSessionID2(sessionID) || hydratedSessions.has(sessionID)) return;
|
|
442
654
|
hydratedSessions.add(sessionID);
|
|
443
655
|
try {
|
|
444
656
|
const response = await api.client.session.messages({ sessionID });
|
|
@@ -500,19 +712,37 @@ var tui = async (api, options) => {
|
|
|
500
712
|
});
|
|
501
713
|
const offPart = api.event.on("message.part.updated", (evt) => {
|
|
502
714
|
const part = evt.properties.part;
|
|
715
|
+
const sessionID = part.sessionID ?? evt.properties.sessionID;
|
|
503
716
|
if (part.type === "tool") {
|
|
504
|
-
recordToolActivity(metrics,
|
|
717
|
+
recordToolActivity(metrics, sessionID, part.messageID, Date.now());
|
|
718
|
+
recordToolPart(activity, sessionID, part);
|
|
719
|
+
} else if (part.type === "compaction") {
|
|
720
|
+
recordCompaction(activity, sessionID, part.id, part.auto === true);
|
|
721
|
+
} else if (part.type === "step-finish") {
|
|
722
|
+
recordStep(activity, sessionID, part.id);
|
|
505
723
|
}
|
|
506
724
|
if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
|
|
507
725
|
metricListeners.notify();
|
|
726
|
+
activityListeners.notify();
|
|
508
727
|
});
|
|
509
728
|
const offSessionCreated = api.event.on("session.created", (evt) => {
|
|
510
729
|
if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
|
|
730
|
+
const info = evt.properties.info;
|
|
731
|
+
if (info && typeof info.id === "string") {
|
|
732
|
+
if (typeof info.title === "string") activity.titles[info.id] = info.title;
|
|
733
|
+
if (typeof info.parentID === "string") recordChild(activity, info.id, info.parentID);
|
|
734
|
+
activityListeners.notify();
|
|
735
|
+
}
|
|
511
736
|
});
|
|
512
737
|
const offSessionUpdated = api.event.on("session.updated", (evt) => {
|
|
513
738
|
if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
|
|
514
739
|
const info = evt.properties.info;
|
|
515
740
|
const sessionID = typeof info?.id === "string" ? info.id : void 0;
|
|
741
|
+
if (sessionID) {
|
|
742
|
+
if (typeof info?.title === "string") activity.titles[sessionID] = info.title;
|
|
743
|
+
if (typeof info?.parentID === "string") recordChild(activity, sessionID, info.parentID);
|
|
744
|
+
activityListeners.notify();
|
|
745
|
+
}
|
|
516
746
|
const providerID = info?.model?.providerID;
|
|
517
747
|
if (sessionID && typeof providerID === "string") {
|
|
518
748
|
goProviderTracker.record(sessionID, providerID);
|
|
@@ -537,7 +767,7 @@ var tui = async (api, options) => {
|
|
|
537
767
|
sessionID: props.session_id,
|
|
538
768
|
subscribe: metricListeners.subscribe,
|
|
539
769
|
text: () => {
|
|
540
|
-
if (!
|
|
770
|
+
if (!isSessionID2(props.session_id)) return "";
|
|
541
771
|
const status = api.state.session.status(props.session_id);
|
|
542
772
|
return renderPromptRightMetricsText(metrics, props.session_id, {
|
|
543
773
|
idle: status?.type === "idle",
|
|
@@ -547,6 +777,16 @@ var tui = async (api, options) => {
|
|
|
547
777
|
}
|
|
548
778
|
),
|
|
549
779
|
sidebar_content: (_ctx, props) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
780
|
+
/* @__PURE__ */ jsx(
|
|
781
|
+
SessionAnalysisSidebar,
|
|
782
|
+
{
|
|
783
|
+
api,
|
|
784
|
+
sessionID: props.session_id,
|
|
785
|
+
state: activity,
|
|
786
|
+
subscribe: activityListeners.subscribe,
|
|
787
|
+
hydrate: () => void hydrateActivity(api.client, activity, props.session_id).then(() => activityListeners.notify())
|
|
788
|
+
}
|
|
789
|
+
),
|
|
550
790
|
/* @__PURE__ */ jsx(
|
|
551
791
|
TokenUsageSidebar,
|
|
552
792
|
{
|
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.1",
|
|
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",
|