@rejacky/opencode-insights 0.1.9 → 0.1.11

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/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  openDatabase,
4
4
  resolveCapturePath
5
- } from "./chunk-FGTKNB7T.js";
5
+ } from "./chunk-3YLLHABZ.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { execFile as execFile2 } from "child_process";
@@ -12,6 +12,7 @@ import { dirname, join, resolve } from "path";
12
12
  import { homedir } from "os";
13
13
  import { pathToFileURL } from "url";
14
14
  import { promisify as promisify2 } from "util";
15
+ import { applyEdits, modify, parse } from "jsonc-parser";
15
16
 
16
17
  // src/inspect.ts
17
18
  import { readFile } from "fs/promises";
@@ -1727,8 +1728,10 @@ async function configureOpenCodeDebug(options) {
1727
1728
  if (!existsSync2(localServerEntry) || !existsSync2(localTuiEntry)) {
1728
1729
  throw new Error("Missing dist output. Run npm run build before opencode-insights debug.");
1729
1730
  }
1730
- const opencodeConfig = await readJsonConfig(opencodePath, { plugin: [] });
1731
- const tuiConfig = await readJsonConfig(tuiPath, { plugin: [] });
1731
+ const opencodeSource = await readJsonConfigSource(opencodePath);
1732
+ const tuiSource = await readJsonConfigSource(tuiPath);
1733
+ const opencodeConfig = await readJsonConfig(opencodePath, { plugin: [] }, opencodeSource);
1734
+ const tuiConfig = await readJsonConfig(tuiPath, { plugin: [] }, tuiSource);
1732
1735
  setSinglePluginSpec(opencodeConfig, SERVER_PLUGIN_SPEC, [localServerEntry, debugServerOptions(options)], localServerEntry);
1733
1736
  setSinglePluginSpec(tuiConfig, TUI_PLUGIN_SPEC, localTuiEntry);
1734
1737
  removePlugin(tuiConfig, SUBPATH_TUI_PLUGIN_SPEC);
@@ -1745,8 +1748,8 @@ async function configureOpenCodeDebug(options) {
1745
1748
  return lines.join("\n");
1746
1749
  }
1747
1750
  await mkdir(configDir, { recursive: true });
1748
- await writeJsonConfig(opencodePath, opencodeConfig);
1749
- await writeJsonConfig(tuiPath, tuiConfig);
1751
+ await writeJsonConfig(opencodePath, opencodeConfig, opencodeSource);
1752
+ await writeJsonConfig(tuiPath, tuiConfig, tuiSource);
1750
1753
  lines.push("Debug configuration written. Restart OpenCode to load the local build.");
1751
1754
  return lines.join("\n");
1752
1755
  }
@@ -1763,18 +1766,23 @@ function resolveOpenCodeConfigPath(configDir) {
1763
1766
  if (existsSync2(jsonPath)) return jsonPath;
1764
1767
  return jsonPath;
1765
1768
  }
1766
- async function readJsonConfig(path, fallback) {
1767
- if (!existsSync2(path)) return { ...fallback };
1768
- const content = await readFile2(path, "utf8");
1769
+ async function readJsonConfig(path, fallback, source) {
1770
+ const content = source ?? (existsSync2(path) ? await readFile2(path, "utf8") : void 0);
1771
+ if (content === void 0) return { ...fallback };
1769
1772
  const trimmed = content.trim();
1770
1773
  if (!trimmed) return { ...fallback };
1771
1774
  try {
1772
- const parsed = JSON.parse(stripJsonCommentsAndTrailingCommas(trimmed));
1775
+ const parseErrors = [];
1776
+ const parsed = parse(trimmed, parseErrors, { allowTrailingComma: true });
1777
+ if (parseErrors.length > 0) throw new Error("invalid JSONC syntax");
1773
1778
  return isJsonObject(parsed) ? parsed : { ...fallback };
1774
1779
  } catch (error) {
1775
1780
  throw new Error(`Could not parse ${path}: ${error instanceof Error ? error.message : String(error)}`);
1776
1781
  }
1777
1782
  }
1783
+ async function readJsonConfigSource(path) {
1784
+ return existsSync2(path) ? readFile2(path, "utf8") : void 0;
1785
+ }
1778
1786
  function stripJsonCommentsAndTrailingCommas(input) {
1779
1787
  let output = "";
1780
1788
  let inString = false;
@@ -1888,11 +1896,12 @@ async function uninstallOpenCode(options) {
1888
1896
  }
1889
1897
  async function removePluginFromConfig(path, plugin, options) {
1890
1898
  if (!existsSync2(path)) return "config not found";
1891
- const config = await readJsonConfig(path, { plugin: [] });
1899
+ const source = await readJsonConfigSource(path);
1900
+ const config = await readJsonConfig(path, { plugin: [] }, source);
1892
1901
  const changed = removePlugin(config, plugin);
1893
1902
  if (!changed) return `not present (${plugin})`;
1894
1903
  if (options.dryRun) return `would remove (${plugin})`;
1895
- await writeJsonConfig(path, config);
1904
+ await writeJsonConfig(path, config, source);
1896
1905
  return `removed (${plugin})`;
1897
1906
  }
1898
1907
  async function removeDataFiles(paths, options) {
@@ -1904,8 +1913,15 @@ async function removeDataFiles(paths, options) {
1904
1913
  }
1905
1914
  return existing;
1906
1915
  }
1907
- async function writeJsonConfig(path, config) {
1916
+ async function writeJsonConfig(path, config, source) {
1908
1917
  await mkdir(dirname(path), { recursive: true });
1918
+ if (source !== void 0) {
1919
+ const edits = modify(source, ["plugin"], config.plugin, {
1920
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" }
1921
+ });
1922
+ await writeFile(path, applyEdits(source, edits), "utf8");
1923
+ return;
1924
+ }
1909
1925
  await writeFile(path, `${JSON.stringify(config, null, 2)}
1910
1926
  `, "utf8");
1911
1927
  }
package/dist/index.d.ts CHANGED
@@ -1,52 +1,6 @@
1
1
  import { Plugin } from '@opencode-ai/plugin';
2
2
  import { TuiPlugin } from '@opencode-ai/plugin/tui';
3
- export { a as CaptureKind, C as CaptureRecord, b as CaptureStore, I as InsightsOptions, J as JsonlCaptureStore, S as SqliteCaptureStore, c as SqliteDb, d as createCaptureStore, e as defaultDataDir, f as extractEventType, n as normalizeChatHeadersCapture, g as normalizeChatMessageCapture, h as normalizeChatParamsCapture, i as normalizeEventCapture, j as normalizeExperimentalChatMessagesTransformCapture, k as normalizeExperimentalChatSystemTransformCapture, l as normalizeToolCapture, o as openDatabase, r as resolveCapturePath, m as resolveRetentionDays } from './capture-BMWWI5GR.js';
4
-
5
- type StreamSample = {
6
- at: number;
7
- tokens: number;
8
- };
9
- type MessageTiming = {
10
- sessionID: string;
11
- requestStartAt: number;
12
- firstResponseAt?: number | undefined;
13
- firstTokenAt?: number | undefined;
14
- lastTokenAt?: number | undefined;
15
- lastToolCallAt?: number | undefined;
16
- };
17
- type SessionAverage = {
18
- totalTokens: number;
19
- totalDurationMs: number;
20
- totalTtftMs: number;
21
- messageCount: number;
22
- };
23
- type MetricsState = {
24
- streamSamplesBySession: Record<string, StreamSample[]>;
25
- messageTimingByID: Record<string, MessageTiming>;
26
- sessionAverageByID: Record<string, SessionAverage>;
27
- };
28
- declare function createMetricsState(): MetricsState;
29
- declare function estimateStreamTokens(delta: string): number;
30
- declare function recordAssistantMessage(state: MetricsState, input: {
31
- sessionID: string;
32
- messageID: string;
33
- createdAt: number;
34
- completedAt?: number;
35
- outputTokens?: number;
36
- reasoningTokens?: number;
37
- finish?: string;
38
- }): void;
39
- declare function recordAssistantDelta(state: MetricsState, input: {
40
- sessionID: string;
41
- messageID: string;
42
- delta: string;
43
- at: number;
44
- }): void;
45
- declare function recordToolActivity(state: MetricsState, sessionID: string, messageID: string, at?: number): void;
46
- declare function renderMetricsText(state: MetricsState, sessionID: string, options?: {
47
- now?: number;
48
- idle?: boolean;
49
- }): string;
3
+ export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, D as DEFAULT_PROMPT_RIGHT_METRICS, I as InsightsConfig, c as InsightsOptions, J as JsonlCaptureStore, M as MessageTiming, d as MetricsState, P as PromptRightMetric, S as SessionAverage, e as SessionTokenUsage, f as SqliteCaptureStore, g as SqliteDb, h as StreamSample, i as createCaptureStore, j as createMetricsState, k as defaultDataDir, l as estimateStreamTokens, m as extractEventType, n as getSessionTokenUsage, o as normalizeChatHeadersCapture, p as normalizeChatMessageCapture, q as normalizeChatParamsCapture, r as normalizeEventCapture, s as normalizeExperimentalChatMessagesTransformCapture, t as normalizeExperimentalChatSystemTransformCapture, u as normalizeToolCapture, v as openDatabase, w as readInsightsConfig, x as recordAssistantDelta, y as recordAssistantMessage, z as recordToolActivity, B as renderMetricsText, E as renderPromptRightMetricsText, F as renderResponseMetricsText, G as renderSessionTokenUsage, H as resolveCapturePath, K as resolveInsightsConfigPath, L as resolveRetentionDays } from './capture-BIiGg2nW.js';
50
4
 
51
5
  type SubagentStatus = "running" | "done" | "error";
52
6
  type SubagentInfo = {
@@ -112,4 +66,4 @@ declare const _default: {
112
66
  server: Plugin;
113
67
  };
114
68
 
115
- export { type MessageTiming, type MetricsState, OpenCodeInsights, type SessionAverage, type StreamSample, type SubagentInfo, type SubagentSidebarModel, type SubagentSidebarRow, type SubagentState, type SubagentStatus, applySubagentEvent, createMetricsState, createSubagentState, _default as default, estimateStreamTokens, getSubagentItems, getSubagentSidebarModel, getSubagentSidebarRowAtLine, id, pruneStaleSubagents, recordAssistantDelta, recordAssistantMessage, recordToolActivity, renderMetricsText, renderSubagentFooter, renderSubagentSidebar, renderSubagentStatus, server, rootTui as tui };
69
+ export { OpenCodeInsights, type SubagentInfo, type SubagentSidebarModel, type SubagentSidebarRow, type SubagentState, type SubagentStatus, applySubagentEvent, createSubagentState, _default as default, getSubagentItems, getSubagentSidebarModel, getSubagentSidebarRowAtLine, id, pruneStaleSubagents, renderSubagentFooter, renderSubagentSidebar, renderSubagentStatus, server, rootTui as tui };
package/dist/index.js CHANGED
@@ -1,26 +1,24 @@
1
1
  import {
2
2
  applySubagentEvent,
3
- createMetricsState,
4
3
  createSubagentState,
5
- estimateStreamTokens,
6
4
  getSubagentItems,
7
5
  getSubagentSidebarModel,
8
6
  getSubagentSidebarRowAtLine,
9
7
  pruneStaleSubagents,
10
- recordAssistantDelta,
11
- recordAssistantMessage,
12
- recordToolActivity,
13
- renderMetricsText,
14
8
  renderSubagentFooter,
15
9
  renderSubagentSidebar,
16
10
  renderSubagentStatus
17
- } from "./chunk-7M32TU5P.js";
11
+ } from "./chunk-O5FFLPEQ.js";
18
12
  import {
13
+ DEFAULT_PROMPT_RIGHT_METRICS,
19
14
  JsonlCaptureStore,
20
15
  SqliteCaptureStore,
21
16
  createCaptureStore,
17
+ createMetricsState,
22
18
  defaultDataDir,
19
+ estimateStreamTokens,
23
20
  extractEventType,
21
+ getSessionTokenUsage,
24
22
  normalizeChatHeadersCapture,
25
23
  normalizeChatMessageCapture,
26
24
  normalizeChatParamsCapture,
@@ -29,9 +27,18 @@ import {
29
27
  normalizeExperimentalChatSystemTransformCapture,
30
28
  normalizeToolCapture,
31
29
  openDatabase,
30
+ readInsightsConfig,
31
+ recordAssistantDelta,
32
+ recordAssistantMessage,
33
+ recordToolActivity,
34
+ renderMetricsText,
35
+ renderPromptRightMetricsText,
36
+ renderResponseMetricsText,
37
+ renderSessionTokenUsage,
32
38
  resolveCapturePath,
39
+ resolveInsightsConfigPath,
33
40
  resolveRetentionDays
34
- } from "./chunk-FGTKNB7T.js";
41
+ } from "./chunk-3YLLHABZ.js";
35
42
 
36
43
  // src/cli-shim.ts
37
44
  import { existsSync } from "fs";
@@ -133,6 +140,7 @@ var rootTui = async (...args) => {
133
140
  var id = "opencode-insights";
134
141
  var src_default = { id, server };
135
142
  export {
143
+ DEFAULT_PROMPT_RIGHT_METRICS,
136
144
  JsonlCaptureStore,
137
145
  OpenCodeInsights,
138
146
  SqliteCaptureStore,
@@ -144,6 +152,7 @@ export {
144
152
  defaultDataDir,
145
153
  estimateStreamTokens,
146
154
  extractEventType,
155
+ getSessionTokenUsage,
147
156
  getSubagentItems,
148
157
  getSubagentSidebarModel,
149
158
  getSubagentSidebarRowAtLine,
@@ -157,14 +166,19 @@ export {
157
166
  normalizeToolCapture,
158
167
  openDatabase,
159
168
  pruneStaleSubagents,
169
+ readInsightsConfig,
160
170
  recordAssistantDelta,
161
171
  recordAssistantMessage,
162
172
  recordToolActivity,
163
173
  renderMetricsText,
174
+ renderPromptRightMetricsText,
175
+ renderResponseMetricsText,
176
+ renderSessionTokenUsage,
164
177
  renderSubagentFooter,
165
178
  renderSubagentSidebar,
166
179
  renderSubagentStatus,
167
180
  resolveCapturePath,
181
+ resolveInsightsConfigPath,
168
182
  resolveRetentionDays,
169
183
  server,
170
184
  rootTui as tui
package/dist/tui.js CHANGED
@@ -1,34 +1,67 @@
1
1
  import {
2
2
  applySubagentEvent,
3
- createMetricsState,
4
3
  createSubagentState,
5
4
  getSubagentSidebarModel,
6
- getSubagentSidebarRowAtLine,
5
+ getSubagentSidebarRowAtLine
6
+ } from "./chunk-O5FFLPEQ.js";
7
+ import {
8
+ createMetricsState,
9
+ readInsightsConfig,
7
10
  recordAssistantDelta,
8
11
  recordAssistantMessage,
9
12
  recordToolActivity,
10
- renderMetricsText
11
- } from "./chunk-7M32TU5P.js";
13
+ renderPromptRightMetricsText,
14
+ renderSessionTokenUsage
15
+ } from "./chunk-3YLLHABZ.js";
12
16
 
13
17
  // src/tui.tsx
14
18
  import { createTextAttributes, StyledText } from "@opentui/core";
15
19
  import { createSignal, onCleanup } from "solid-js";
16
- import { jsx } from "@opentui/solid/jsx-runtime";
20
+
21
+ // src/listeners.ts
22
+ function createListenerRegistry() {
23
+ const listeners = /* @__PURE__ */ new Set();
24
+ return {
25
+ notify() {
26
+ for (const listener of listeners) listener();
27
+ },
28
+ subscribe(listener) {
29
+ listeners.add(listener);
30
+ return () => listeners.delete(listener);
31
+ }
32
+ };
33
+ }
34
+
35
+ // src/render-state.ts
36
+ function hasRenderStateChanged(previous, next) {
37
+ return !previous || previous.content !== next.content || previous.visible !== next.visible || previous.height !== next.height;
38
+ }
39
+
40
+ // src/tui.tsx
41
+ import { Fragment, jsx, jsxs } from "@opentui/solid/jsx-runtime";
17
42
  function isSessionID(value) {
18
43
  return typeof value === "string" && value.startsWith("ses");
19
44
  }
20
- function PromptRight(props) {
45
+ function PromptRightMetrics(props) {
21
46
  let text;
47
+ let previous;
22
48
  const sync = () => {
23
49
  if (!text) return;
24
50
  const content = props.text();
25
- text.content = content;
26
- text.visible = content.length > 0;
27
- text.height = content.length > 0 ? "auto" : 0;
51
+ const next = { content, visible: content.length > 0, height: content.length > 0 ? 1 : 0 };
52
+ if (!hasRenderStateChanged(previous, next)) return;
53
+ previous = next;
54
+ text.content = next.content;
55
+ text.visible = next.visible;
56
+ text.height = next.height;
28
57
  props.api.renderer.requestRender();
29
58
  };
30
59
  const unsubscribe = props.subscribe(sync);
31
- onCleanup(unsubscribe);
60
+ const timer = setInterval(sync, 1e3);
61
+ onCleanup(() => {
62
+ unsubscribe();
63
+ clearInterval(timer);
64
+ });
32
65
  return /* @__PURE__ */ jsx(
33
66
  "text",
34
67
  {
@@ -37,19 +70,65 @@ function PromptRight(props) {
37
70
  sync();
38
71
  },
39
72
  fg: props.api.theme.current.textMuted,
73
+ height: 1,
74
+ wrapMode: "none",
75
+ truncate: true,
76
+ overflow: "hidden",
40
77
  children: props.text()
41
78
  }
42
79
  );
43
80
  }
81
+ function TokenUsageSidebar(props) {
82
+ let text;
83
+ let previous;
84
+ const [collapsed, setCollapsed] = createSignal(false);
85
+ const titleAttributes = createTextAttributes({ bold: true });
86
+ const toggleTokenUsage = (event) => {
87
+ if (!text || event.y !== text.y) return;
88
+ setCollapsed((prev) => !prev);
89
+ sync();
90
+ };
91
+ const sync = () => {
92
+ if (!text) return;
93
+ const content = renderSessionTokenUsage(props.state, props.sessionID);
94
+ const next = {
95
+ content: `${collapsed()}|${content}`,
96
+ visible: content.length > 0,
97
+ height: content.length > 0 ? "auto" : 0
98
+ };
99
+ if (!hasRenderStateChanged(previous, next)) return;
100
+ previous = next;
101
+ text.visible = next.visible;
102
+ text.height = next.height;
103
+ text.content = content ? renderTokenUsageSidebar(content, props.api, titleAttributes, collapsed()) : "";
104
+ props.api.renderer.requestRender();
105
+ };
106
+ const unsubscribe = props.subscribe(sync);
107
+ onCleanup(unsubscribe);
108
+ props.hydrate();
109
+ return /* @__PURE__ */ jsx(
110
+ "text",
111
+ {
112
+ ref: (ref) => {
113
+ text = ref;
114
+ sync();
115
+ },
116
+ onMouseDown: toggleTokenUsage,
117
+ fg: props.api.theme.current.textMuted,
118
+ children: ""
119
+ }
120
+ );
121
+ }
44
122
  function SubagentSidebar(props) {
45
123
  let text;
46
124
  const [collapsed, setCollapsed] = createSignal(false);
47
125
  const [hoveredRowID, setHoveredRowID] = createSignal();
48
126
  const titleAttributes = createTextAttributes({ bold: true });
127
+ let previous;
49
128
  const toggle = (event) => {
50
129
  if (!text || event.y !== text.y) return;
51
130
  setCollapsed((prev) => !prev);
52
- props.api.renderer.requestRender();
131
+ sync();
53
132
  };
54
133
  const openSubagent = (event) => {
55
134
  if (!text || collapsed()) return;
@@ -76,9 +155,17 @@ function SubagentSidebar(props) {
76
155
  const sync = () => {
77
156
  if (!text) return;
78
157
  const model = getSubagentSidebarModel(props.state, props.sessionID);
79
- text.visible = !!model;
80
- text.height = model ? "auto" : 0;
81
- text.content = model ? renderSubagentStyledSidebar(props.state, props.sessionID, props.api, titleAttributes, collapsed(), hoveredRowID()) : "";
158
+ const content = model ? renderSubagentStyledSidebar(model, props.api, titleAttributes, collapsed(), hoveredRowID()) : "";
159
+ const next = {
160
+ content: model ? contentSignature(model, collapsed(), hoveredRowID()) : "",
161
+ visible: !!model,
162
+ height: model ? "auto" : 0
163
+ };
164
+ if (!hasRenderStateChanged(previous, next)) return;
165
+ previous = next;
166
+ text.visible = next.visible;
167
+ text.height = next.height;
168
+ text.content = model ? content : "";
82
169
  props.api.renderer.requestRender();
83
170
  };
84
171
  const unsubscribe = props.subscribe(sync);
@@ -103,9 +190,7 @@ function SubagentSidebar(props) {
103
190
  }
104
191
  );
105
192
  }
106
- function renderSubagentStyledSidebar(state, sessionID, api, titleAttributes, collapsed, hoveredRowID) {
107
- const model = getSubagentSidebarModel(state, sessionID);
108
- if (!model) return "";
193
+ function renderSubagentStyledSidebar(model, api, titleAttributes, collapsed, hoveredRowID) {
109
194
  const indicator = collapsed ? "\u25B6 " : "\u25BC ";
110
195
  const chunks = [
111
196
  textChunk(`${indicator}${model.title}
@@ -126,6 +211,18 @@ function renderSubagentStyledSidebar(state, sessionID, api, titleAttributes, col
126
211
  }
127
212
  return new StyledText(chunks);
128
213
  }
214
+ function renderTokenUsageSidebar(content, api, titleAttributes, collapsed) {
215
+ const [title, ...details] = content.split("\n");
216
+ const visibleDetails = collapsed ? details.slice(0, 1) : details;
217
+ return new StyledText([
218
+ textChunk(`${collapsed ? "\u25B6" : "\u25BC"} ${title}
219
+ `, api.theme.current.text, titleAttributes),
220
+ textChunk(visibleDetails.join("\n"), api.theme.current.textMuted)
221
+ ]);
222
+ }
223
+ function contentSignature(model, collapsed, hoveredRowID) {
224
+ return JSON.stringify({ collapsed, hoveredRowID, title: model.title, summary: model.summary, rows: model.rows });
225
+ }
129
226
  function textChunk(text, fg, attributes, bg) {
130
227
  return {
131
228
  __isChunk: true,
@@ -135,16 +232,39 @@ function textChunk(text, fg, attributes, bg) {
135
232
  ...bg === void 0 ? {} : { bg }
136
233
  };
137
234
  }
138
- var tui = async (api) => {
235
+ var tui = async (api, options) => {
236
+ const config = await readInsightsConfig(options ?? {});
139
237
  const metrics = createMetricsState();
140
238
  const subagents = createSubagentState();
141
- const listeners = /* @__PURE__ */ new Set();
142
- const bump = () => {
143
- for (const listener of listeners) listener();
144
- };
145
- const subscribe = (listener) => {
146
- listeners.add(listener);
147
- return () => listeners.delete(listener);
239
+ const metricListeners = createListenerRegistry();
240
+ const subagentListeners = createListenerRegistry();
241
+ const hydratedSessions = /* @__PURE__ */ new Set();
242
+ const hydrateSessionMetrics = async (sessionID) => {
243
+ if (!isSessionID(sessionID) || hydratedSessions.has(sessionID)) return;
244
+ hydratedSessions.add(sessionID);
245
+ try {
246
+ const response = await api.client.session.messages({ sessionID });
247
+ const messages = response.data ?? [];
248
+ for (const message of messages) {
249
+ const info = message.info;
250
+ if (info.role !== "assistant" || typeof info.time.completed !== "number") continue;
251
+ const input = {
252
+ sessionID: info.sessionID,
253
+ messageID: info.id,
254
+ createdAt: info.time.created,
255
+ completedAt: info.time.completed,
256
+ inputTokens: info.tokens.input,
257
+ outputTokens: info.tokens.output,
258
+ reasoningTokens: info.tokens.reasoning,
259
+ cacheReadTokens: info.tokens.cache.read,
260
+ cacheWriteTokens: info.tokens.cache.write
261
+ };
262
+ recordAssistantMessage(metrics, typeof info.finish === "string" ? { ...input, finish: info.finish } : input);
263
+ }
264
+ metricListeners.notify();
265
+ } catch {
266
+ hydratedSessions.delete(sessionID);
267
+ }
148
268
  };
149
269
  const offDelta = api.event.on("message.part.delta", (evt) => {
150
270
  if (evt.properties.field !== "text") return;
@@ -154,71 +274,89 @@ var tui = async (api) => {
154
274
  delta: evt.properties.delta,
155
275
  at: Date.now()
156
276
  });
157
- bump();
277
+ metricListeners.notify();
158
278
  });
159
279
  const offMessage = api.event.on("message.updated", (evt) => {
160
280
  const info = evt.properties.info;
161
- if (applySubagentEvent(subagents, evt)) bump();
281
+ if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
162
282
  if (info.role !== "assistant") return;
163
283
  const messageInput = {
164
284
  sessionID: info.sessionID ?? evt.properties.sessionID,
165
285
  messageID: info.id,
166
286
  createdAt: info.time.created,
287
+ inputTokens: info.tokens.input,
167
288
  outputTokens: info.tokens.output,
168
- reasoningTokens: info.tokens.reasoning
289
+ reasoningTokens: info.tokens.reasoning,
290
+ cacheReadTokens: info.tokens.cache?.read,
291
+ cacheWriteTokens: info.tokens.cache?.write
169
292
  };
170
293
  if (typeof info.time.completed === "number") messageInput.completedAt = info.time.completed;
171
294
  if (typeof info.finish === "string") messageInput.finish = info.finish;
172
295
  recordAssistantMessage(metrics, messageInput);
173
- bump();
296
+ metricListeners.notify();
174
297
  });
175
298
  const offPart = api.event.on("message.part.updated", (evt) => {
176
299
  const part = evt.properties.part;
177
300
  if (part.type === "tool") {
178
301
  recordToolActivity(metrics, part.sessionID ?? evt.properties.sessionID, part.messageID, Date.now());
179
302
  }
180
- applySubagentEvent(subagents, evt);
181
- bump();
303
+ if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
304
+ metricListeners.notify();
182
305
  });
183
306
  const offSessionCreated = api.event.on("session.created", (evt) => {
184
- if (applySubagentEvent(subagents, evt)) bump();
307
+ if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
185
308
  });
186
309
  const offSessionUpdated = api.event.on("session.updated", (evt) => {
187
- if (applySubagentEvent(subagents, evt)) bump();
310
+ if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
188
311
  });
189
312
  const offSessionStatus = api.event.on("session.status", (evt) => {
190
- if (applySubagentEvent(subagents, evt)) bump();
313
+ if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
191
314
  });
192
315
  const offSessionIdle = api.event.on("session.idle", (evt) => {
193
- if (applySubagentEvent(subagents, evt)) bump();
316
+ if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
194
317
  });
195
318
  const offSessionError = api.event.on("session.error", (evt) => {
196
- if (applySubagentEvent(subagents, evt)) bump();
319
+ if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
197
320
  });
198
321
  const offSlots = api.slots.register({
199
322
  slots: {
200
323
  session_prompt_right: (_ctx, props) => /* @__PURE__ */ jsx(
201
- PromptRight,
324
+ PromptRightMetrics,
202
325
  {
203
326
  api,
204
327
  sessionID: props.session_id,
205
- subscribe,
328
+ subscribe: metricListeners.subscribe,
206
329
  text: () => {
207
330
  if (!isSessionID(props.session_id)) return "";
208
331
  const status = api.state.session.status(props.session_id);
209
- return renderMetricsText(metrics, props.session_id, { idle: status?.type === "idle" });
332
+ return renderPromptRightMetricsText(metrics, props.session_id, {
333
+ idle: status?.type === "idle",
334
+ metrics: config.promptRightMetrics
335
+ });
210
336
  }
211
337
  }
212
338
  ),
213
- sidebar_content: (_ctx, props) => /* @__PURE__ */ jsx(
214
- SubagentSidebar,
215
- {
216
- api,
217
- sessionID: props.session_id,
218
- state: subagents,
219
- subscribe
220
- }
221
- )
339
+ sidebar_content: (_ctx, props) => /* @__PURE__ */ jsxs(Fragment, { children: [
340
+ /* @__PURE__ */ jsx(
341
+ TokenUsageSidebar,
342
+ {
343
+ api,
344
+ sessionID: props.session_id,
345
+ state: metrics,
346
+ subscribe: metricListeners.subscribe,
347
+ hydrate: () => void hydrateSessionMetrics(props.session_id)
348
+ }
349
+ ),
350
+ /* @__PURE__ */ jsx(
351
+ SubagentSidebar,
352
+ {
353
+ api,
354
+ sessionID: props.session_id,
355
+ state: subagents,
356
+ subscribe: subagentListeners.subscribe
357
+ }
358
+ )
359
+ ] })
222
360
  }
223
361
  });
224
362
  api.lifecycle.onDispose(() => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@rejacky/opencode-insights",
4
- "version": "0.1.9",
4
+ "version": "0.1.11",
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",
@@ -86,6 +86,7 @@
86
86
  },
87
87
  "dependencies": {
88
88
  "better-sqlite3": "^12.11.1",
89
+ "jsonc-parser": "^3.3.1",
89
90
  "sql.js": "^1.14.1"
90
91
  }
91
92
  }
@@ -1,61 +0,0 @@
1
- type CaptureKind = "chat.message" | "chat.params" | "chat.headers" | "experimental.chat.messages.transform" | "experimental.chat.system.transform" | "event" | "tool.execute.before" | "tool.execute.after";
2
- type CaptureRecord = {
3
- id: string;
4
- kind: CaptureKind;
5
- timestamp: number;
6
- sessionID?: string | undefined;
7
- messageID?: string | undefined;
8
- providerID?: string | undefined;
9
- modelID?: string | undefined;
10
- payload: Record<string, unknown>;
11
- };
12
- type CaptureStore = {
13
- initialize?(): Promise<void>;
14
- append(record: CaptureRecord): Promise<void>;
15
- close?(): Promise<void>;
16
- };
17
- type InsightsOptions = {
18
- dataDir?: unknown;
19
- dbPath?: unknown;
20
- retentionDays?: unknown;
21
- };
22
- declare function defaultDataDir(): string;
23
- declare function resolveCapturePath(options?: InsightsOptions): string;
24
- declare function normalizeChatMessageCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
25
- declare function normalizeChatParamsCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
26
- declare function normalizeChatHeadersCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
27
- declare function normalizeExperimentalChatMessagesTransformCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
28
- declare function normalizeExperimentalChatSystemTransformCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
29
- declare function normalizeEventCapture(event: unknown, timestamp?: number): CaptureRecord;
30
- declare function normalizeToolCapture(kind: "tool.execute.before" | "tool.execute.after", input: unknown, output: unknown, timestamp?: number): CaptureRecord;
31
- declare class JsonlCaptureStore implements CaptureStore {
32
- private readonly path;
33
- private readonly retentionMs;
34
- constructor(path: string, retentionMs?: number | undefined);
35
- initialize(): Promise<void>;
36
- append(record: CaptureRecord): Promise<void>;
37
- private pruneExpired;
38
- }
39
- interface SqliteDb {
40
- all(sql: string, ...params: unknown[]): Record<string, unknown>[];
41
- run(sql: string, ...params: unknown[]): void;
42
- sync(): void;
43
- close(): void;
44
- }
45
- declare function openDatabase(path: string, readonly?: boolean): Promise<SqliteDb | undefined>;
46
- declare function extractEventType(payload: Record<string, unknown>): string | null;
47
- declare class SqliteCaptureStore implements CaptureStore {
48
- private readonly path;
49
- private readonly retentionMs;
50
- private db;
51
- private fallbackStore;
52
- constructor(path: string, retentionMs?: number | undefined);
53
- initialize(): Promise<void>;
54
- append(record: CaptureRecord): Promise<void>;
55
- close(): Promise<void>;
56
- private pruneExpired;
57
- }
58
- declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
59
- declare function resolveRetentionDays(value: unknown): number;
60
-
61
- export { type CaptureRecord as C, type InsightsOptions as I, JsonlCaptureStore as J, SqliteCaptureStore as S, type CaptureKind as a, type CaptureStore as b, type SqliteDb as c, createCaptureStore as d, defaultDataDir as e, extractEventType as f, normalizeChatMessageCapture as g, normalizeChatParamsCapture as h, normalizeEventCapture as i, normalizeExperimentalChatMessagesTransformCapture as j, normalizeExperimentalChatSystemTransformCapture as k, normalizeToolCapture as l, resolveRetentionDays as m, normalizeChatHeadersCapture as n, openDatabase as o, resolveCapturePath as r };