@rejacky/opencode-insights 0.4.0 → 0.4.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.
@@ -16,6 +16,19 @@ type SessionAverage = {
16
16
  totalTtftMs: number;
17
17
  messageCount: number;
18
18
  };
19
+ /**
20
+ * Per-message metrics for input-box (current round).
21
+ * AVG = totalTokens(=output+reasoning)/duration where duration = firstTokenAt→endAt (fallback completed-created if hydrated).
22
+ * TTFT = firstTokenAt - requestStartAt. TPS = live estimated tokens / active burst duration (5s window) for this messageID only.
23
+ * Session totals (Token Usage sidebar) remain in SessionTokenUsage/sessionAverageByID (deprecated for prompt-right).
24
+ */
25
+ type MessageMetrics = {
26
+ totalTokens: number;
27
+ durationMs: number;
28
+ ttftMs: number | undefined;
29
+ createdAt: number;
30
+ completedAt: number;
31
+ };
19
32
  type AssistantResponseUsage = {
20
33
  inputTokens?: number | undefined;
21
34
  outputTokens?: number | undefined;
@@ -37,7 +50,10 @@ type PromptRightMetric = "tps" | "avg" | "ttft" | "used" | "cache" | "input" | "
37
50
  declare const DEFAULT_PROMPT_RIGHT_METRICS: PromptRightMetric[];
38
51
  type MetricsState = {
39
52
  streamSamplesBySession: Record<string, StreamSample[]>;
53
+ streamSamplesByMessageID: Record<string, StreamSample[]>;
40
54
  messageTimingByID: Record<string, MessageTiming>;
55
+ messageMetricsByID: Record<string, MessageMetrics>;
56
+ latestMessageIDBySession: Record<string, string>;
41
57
  sessionAverageByID: Record<string, SessionAverage>;
42
58
  latestResponseUsageBySession: Record<string, AssistantResponseUsage>;
43
59
  responseUsageByMessageID: Record<string, {
@@ -47,6 +63,10 @@ type MetricsState = {
47
63
  sessionTokenUsageByID: Record<string, SessionTokenUsage>;
48
64
  };
49
65
  declare function createMetricsState(): MetricsState;
66
+ /**
67
+ * Estimated tokens for live TPS: ceil(bytes/5). Differs from per-message AVG which uses real output+reasoning tokens.
68
+ * Multibyte (CJK/emoji) inflates bytes but may over/undercount vs real tokenizer.
69
+ */
50
70
  declare function estimateStreamTokens(delta: string): number;
51
71
  declare function recordAssistantMessage(state: MetricsState, input: {
52
72
  sessionID: string;
@@ -163,4 +183,4 @@ declare class SqliteCaptureStore implements CaptureStore {
163
183
  declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
164
184
  declare function resolveRetentionDays(value: unknown): number;
165
185
 
166
- export { type AssistantResponseUsage as A, recordAssistantMessage as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, recordToolActivity as E, renderMetricsText as F, type GoUsageConfig as G, renderPromptRightMetricsText as H, type InsightsConfig as I, JsonlCaptureStore as J, renderResponseMetricsText as K, renderSessionTokenUsage as L, type MessageTiming as M, resolveCapturePath as N, resolveCopilotToken as O, type PromptRightMetric as P, resolveInsightsConfigPath as Q, resolveLegacyInsightsConfigPath as R, type SessionAverage as S, resolveRetentionDays as T, type CaptureKind as a, type CaptureStore as b, type CopilotUsageConfig as c, type InsightsOptions as d, type MetricsState as e, type SessionTokenUsage as f, SqliteCaptureStore as g, type SqliteDb as h, type StreamSample as i, createCaptureStore as j, createMetricsState as k, defaultDataDir as l, estimateStreamTokens as m, extractEventType as n, getSessionTokenUsage as o, insightsOptionsFromConfig as p, normalizeChatHeadersCapture as q, normalizeChatMessageCapture as r, normalizeChatParamsCapture as s, normalizeEventCapture as t, normalizeExperimentalChatMessagesTransformCapture as u, normalizeExperimentalChatSystemTransformCapture as v, normalizeToolCapture as w, openDatabase as x, readInsightsConfig as y, recordAssistantDelta as z };
186
+ export { type AssistantResponseUsage as A, recordAssistantDelta as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, recordAssistantMessage as E, recordToolActivity as F, type GoUsageConfig as G, renderMetricsText as H, type InsightsConfig as I, JsonlCaptureStore as J, renderPromptRightMetricsText as K, renderResponseMetricsText as L, type MessageMetrics as M, renderSessionTokenUsage as N, resolveCapturePath as O, type PromptRightMetric as P, resolveCopilotToken as Q, resolveInsightsConfigPath as R, type SessionAverage as S, resolveLegacyInsightsConfigPath as T, resolveRetentionDays as U, type CaptureKind as a, type CaptureStore as b, type CopilotUsageConfig as c, type InsightsOptions as d, type MessageTiming as e, type MetricsState as f, type SessionTokenUsage as g, SqliteCaptureStore as h, type SqliteDb as i, type StreamSample as j, createCaptureStore as k, createMetricsState as l, defaultDataDir as m, estimateStreamTokens as n, extractEventType as o, getSessionTokenUsage as p, insightsOptionsFromConfig as q, normalizeChatHeadersCapture as r, normalizeChatMessageCapture as s, normalizeChatParamsCapture as t, normalizeEventCapture as u, normalizeExperimentalChatMessagesTransformCapture as v, normalizeExperimentalChatSystemTransformCapture as w, normalizeToolCapture as x, openDatabase as y, readInsightsConfig as z };
@@ -6,7 +6,10 @@ var SINGLE_SAMPLE_MS = 1e3;
6
6
  function createMetricsState() {
7
7
  return {
8
8
  streamSamplesBySession: {},
9
+ streamSamplesByMessageID: {},
9
10
  messageTimingByID: {},
11
+ messageMetricsByID: {},
12
+ latestMessageIDBySession: {},
10
13
  sessionAverageByID: {},
11
14
  latestResponseUsageBySession: {},
12
15
  responseUsageByMessageID: {},
@@ -27,6 +30,7 @@ function recordAssistantMessage(state, input) {
27
30
  lastTokenAt: existing?.lastTokenAt,
28
31
  lastToolCallAt: existing?.lastToolCallAt
29
32
  };
33
+ state.latestMessageIDBySession[input.sessionID] = input.messageID;
30
34
  return;
31
35
  }
32
36
  const usage = compactUsage({
@@ -42,13 +46,33 @@ function recordAssistantMessage(state, input) {
42
46
  state.responseUsageByMessageID[input.messageID] = { sessionID: input.sessionID, usage };
43
47
  rebuildSessionTokenUsage(state, input.sessionID);
44
48
  }
49
+ state.latestMessageIDBySession[input.sessionID] = input.messageID;
45
50
  const timing = state.messageTimingByID[input.messageID];
46
- if (timing?.sessionID === input.sessionID && typeof timing.firstResponseAt === "number") {
47
- const totalTokens = (input.outputTokens ?? 0) + (input.reasoningTokens ?? 0);
48
- const endAt = input.finish === "tool-calls" ? timing.lastToolCallAt : input.completedAt;
49
- const durationMs = typeof endAt === "number" ? Math.max(endAt - timing.firstResponseAt, 1) : void 0;
50
- const ttftMs = Math.max(timing.firstResponseAt - timing.requestStartAt, 0);
51
- if (totalTokens > 0 && durationMs) {
51
+ const totalTokens = (input.outputTokens ?? 0) + (input.reasoningTokens ?? 0);
52
+ let durationMs;
53
+ let ttftMs;
54
+ if (timing?.sessionID === input.sessionID && typeof (timing.firstTokenAt ?? timing.firstResponseAt) === "number") {
55
+ const first = timing.firstTokenAt ?? timing.firstResponseAt;
56
+ const endAt = input.finish === "tool-calls" ? timing.lastToolCallAt ?? input.completedAt : input.completedAt;
57
+ durationMs = typeof endAt === "number" ? Math.max(endAt - first, 1) : void 0;
58
+ ttftMs = Math.max(first - timing.requestStartAt, 0);
59
+ } else {
60
+ durationMs = Math.max(input.completedAt - input.createdAt, 1);
61
+ ttftMs = void 0;
62
+ }
63
+ if (totalTokens > 0 && durationMs) {
64
+ state.messageMetricsByID[input.messageID] = {
65
+ totalTokens,
66
+ durationMs,
67
+ ttftMs,
68
+ createdAt: input.createdAt,
69
+ completedAt: input.completedAt
70
+ };
71
+ if (ttftMs !== void 0) {
72
+ const firstForSession = timing?.firstTokenAt ?? timing?.firstResponseAt;
73
+ const endForSession = timing ? input.finish === "tool-calls" ? timing.lastToolCallAt ?? input.completedAt : input.completedAt : input.completedAt;
74
+ const sessionDuration = timing && firstForSession && typeof endForSession === "number" ? Math.max(endForSession - firstForSession, 1) : durationMs;
75
+ const sessionTtft2 = ttftMs;
52
76
  const totals = state.sessionAverageByID[input.sessionID] ?? {
53
77
  totalTokens: 0,
54
78
  totalDurationMs: 0,
@@ -57,13 +81,27 @@ function recordAssistantMessage(state, input) {
57
81
  };
58
82
  state.sessionAverageByID[input.sessionID] = {
59
83
  totalTokens: totals.totalTokens + totalTokens,
60
- totalDurationMs: totals.totalDurationMs + durationMs,
61
- totalTtftMs: totals.totalTtftMs + ttftMs,
84
+ totalDurationMs: totals.totalDurationMs + sessionDuration,
85
+ totalTtftMs: totals.totalTtftMs + sessionTtft2,
62
86
  messageCount: totals.messageCount + 1
63
87
  };
88
+ } else {
89
+ const totals = state.sessionAverageByID[input.sessionID] ?? {
90
+ totalTokens: 0,
91
+ totalDurationMs: 0,
92
+ totalTtftMs: 0,
93
+ messageCount: 0
94
+ };
95
+ state.sessionAverageByID[input.sessionID] = {
96
+ totalTokens: totals.totalTokens + totalTokens,
97
+ totalDurationMs: totals.totalDurationMs + durationMs,
98
+ totalTtftMs: totals.totalTtftMs,
99
+ messageCount: totals.messageCount
100
+ };
64
101
  }
65
102
  }
66
103
  delete state.messageTimingByID[input.messageID];
104
+ delete state.streamSamplesByMessageID[input.messageID];
67
105
  pruneSamples(state, input.completedAt);
68
106
  }
69
107
  function recordAssistantDelta(state, input) {
@@ -71,10 +109,15 @@ function recordAssistantDelta(state, input) {
71
109
  at: input.at,
72
110
  tokens: estimateStreamTokens(input.delta)
73
111
  };
112
+ state.streamSamplesByMessageID[input.messageID] = [
113
+ ...(state.streamSamplesByMessageID[input.messageID] ?? []).filter((item) => input.at - item.at <= STREAM_WINDOW_MS),
114
+ sample
115
+ ];
74
116
  state.streamSamplesBySession[input.sessionID] = [
75
117
  ...(state.streamSamplesBySession[input.sessionID] ?? []).filter((item) => input.at - item.at <= STREAM_WINDOW_MS),
76
118
  sample
77
119
  ];
120
+ state.latestMessageIDBySession[input.sessionID] = input.messageID;
78
121
  const timing = state.messageTimingByID[input.messageID];
79
122
  if (timing) {
80
123
  state.messageTimingByID[input.messageID] = timing.firstTokenAt ? { ...timing, lastTokenAt: input.at } : {
@@ -86,6 +129,9 @@ function recordAssistantDelta(state, input) {
86
129
  }
87
130
  }
88
131
  function recordToolActivity(state, sessionID, messageID, at = Date.now()) {
132
+ if (state.streamSamplesByMessageID[messageID]?.length) {
133
+ delete state.streamSamplesByMessageID[messageID];
134
+ }
89
135
  if (state.streamSamplesBySession[sessionID]?.length) {
90
136
  delete state.streamSamplesBySession[sessionID];
91
137
  }
@@ -100,8 +146,10 @@ function recordToolActivity(state, sessionID, messageID, at = Date.now()) {
100
146
  }
101
147
  function renderMetricsText(state, sessionID, options = {}) {
102
148
  const live = liveTps(state, sessionID, options) ?? "-";
103
- const avg = sessionAverage(state, sessionID) ?? "-";
104
- const ttft = sessionTtft(state, sessionID) ?? "-";
149
+ const msgID = state.latestMessageIDBySession[sessionID];
150
+ const hasPerMessage = !!msgID && !!state.messageMetricsByID[msgID];
151
+ const avg = hasPerMessage ? messageAverage(state, sessionID) ?? "-" : sessionAverage(state, sessionID) ?? "-";
152
+ const ttft = hasPerMessage ? messageTtft(state, sessionID) ?? "-" : sessionTtft(state, sessionID) ?? "-";
105
153
  return `TPS ${live} | AVG ${avg} | TTFT ${ttft}`;
106
154
  }
107
155
  function renderResponseMetricsText(state, sessionID) {
@@ -121,10 +169,14 @@ function renderPromptRightMetricsText(state, sessionID, options = {}) {
121
169
  const usage = state.latestResponseUsageBySession[sessionID];
122
170
  const used = usage ? sumTokens(usage) : void 0;
123
171
  const cacheRate = usage ? cacheReadRate(usage) : void 0;
172
+ const msgID = state.latestMessageIDBySession[sessionID];
173
+ const hasPerMessage = !!msgID && !!state.messageMetricsByID[msgID];
174
+ const avgVal = hasPerMessage ? messageAverage(state, sessionID) : sessionAverage(state, sessionID);
175
+ const ttftVal = hasPerMessage ? messageTtft(state, sessionID) : sessionTtft(state, sessionID);
124
176
  const values = {
125
177
  tps: `TPS ${liveTps(state, sessionID, options) ?? "-"}`,
126
- avg: `AVG ${sessionAverage(state, sessionID) ?? "-"}`,
127
- ttft: `TTFT ${sessionTtft(state, sessionID) ?? "-"}`,
178
+ avg: `AVG ${avgVal ?? "-"}`,
179
+ ttft: `TTFT ${ttftVal ?? "-"}`,
128
180
  used: `${used === void 0 ? "-" : formatTokenCount(used)} used`,
129
181
  cache: `${cacheRate === void 0 ? "-" : formatPercent(cacheRate)} cache`,
130
182
  input: `${usage?.inputTokens === void 0 ? "-" : formatTokenCount(usage.inputTokens)} in`,
@@ -160,6 +212,11 @@ function renderSessionTokenUsage(state, sessionID, subagentTokens = 0) {
160
212
  return lines.join("\n");
161
213
  }
162
214
  function pruneSamples(state, now = Date.now()) {
215
+ for (const [messageID, samples] of Object.entries(state.streamSamplesByMessageID)) {
216
+ const next = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
217
+ if (next.length > 0) state.streamSamplesByMessageID[messageID] = next;
218
+ else delete state.streamSamplesByMessageID[messageID];
219
+ }
163
220
  for (const [sessionID, samples] of Object.entries(state.streamSamplesBySession)) {
164
221
  const next = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
165
222
  if (next.length > 0) state.streamSamplesBySession[sessionID] = next;
@@ -188,6 +245,20 @@ function rebuildSessionTokenUsage(state, sessionID) {
188
245
  usage.totalTokens = usage.inputTokens + usage.outputTokens + usage.reasoningTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
189
246
  state.sessionTokenUsageByID[sessionID] = usage;
190
247
  }
248
+ function messageAverage(state, sessionID) {
249
+ const msgID = state.latestMessageIDBySession[sessionID];
250
+ if (!msgID) return void 0;
251
+ const m = state.messageMetricsByID[msgID];
252
+ if (!m || m.totalTokens <= 0 || m.durationMs <= 0) return void 0;
253
+ return formatRate(m.totalTokens / (m.durationMs / 1e3), "AVG");
254
+ }
255
+ function messageTtft(state, sessionID) {
256
+ const msgID = state.latestMessageIDBySession[sessionID];
257
+ if (!msgID) return void 0;
258
+ const m = state.messageMetricsByID[msgID];
259
+ if (!m || m.ttftMs === void 0 || m.ttftMs < 0) return void 0;
260
+ return formatTtft(m.ttftMs / 1e3);
261
+ }
191
262
  function sessionAverage(state, sessionID) {
192
263
  const totals = state.sessionAverageByID[sessionID];
193
264
  if (!totals || totals.totalTokens <= 0 || totals.totalDurationMs <= 0) return void 0;
@@ -201,7 +272,8 @@ function sessionTtft(state, sessionID) {
201
272
  function liveTps(state, sessionID, options = {}) {
202
273
  if (options.idle) return void 0;
203
274
  const now = options.now ?? Date.now();
204
- const samples = state.streamSamplesBySession[sessionID] ?? [];
275
+ const msgID = state.latestMessageIDBySession[sessionID];
276
+ const samples = (msgID ? state.streamSamplesByMessageID[msgID] : void 0) ?? state.streamSamplesBySession[sessionID] ?? [];
205
277
  const relevant = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
206
278
  if (relevant.length === 0) return void 0;
207
279
  const lastSample = relevant.at(-1);
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as CaptureRecord } from './capture-DJVWBRum.js';
2
+ import { C as CaptureRecord } from './capture-An0IKDqr.js';
3
3
 
4
4
  type HistoryMessage = {
5
5
  id: string;
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  readInsightsConfig,
5
5
  resolveCapturePath,
6
6
  resolveInsightsConfigPath
7
- } from "./chunk-ROQFQXIH.js";
7
+ } from "./chunk-2HZVJIOC.js";
8
8
 
9
9
  // src/cli.ts
10
10
  import { execFile as execFile2 } from "child_process";
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from '@opencode-ai/plugin';
2
2
  import { TuiPlugin } from '@opencode-ai/plugin/tui';
3
- export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, c as CopilotUsageConfig, D as DEFAULT_PROMPT_RIGHT_METRICS, G as GoUsageConfig, I as InsightsConfig, d as InsightsOptions, J as JsonlCaptureStore, M as MessageTiming, e as MetricsState, P as PromptRightMetric, S as SessionAverage, f as SessionTokenUsage, g as SqliteCaptureStore, h as SqliteDb, i as StreamSample, j as createCaptureStore, k as createMetricsState, l as defaultDataDir, m as estimateStreamTokens, n as extractEventType, o as getSessionTokenUsage, p as insightsOptionsFromConfig, q as normalizeChatHeadersCapture, r as normalizeChatMessageCapture, s as normalizeChatParamsCapture, t as normalizeEventCapture, u as normalizeExperimentalChatMessagesTransformCapture, v as normalizeExperimentalChatSystemTransformCapture, w as normalizeToolCapture, x as openDatabase, y as readInsightsConfig, z as recordAssistantDelta, B as recordAssistantMessage, E as recordToolActivity, F as renderMetricsText, H as renderPromptRightMetricsText, K as renderResponseMetricsText, L as renderSessionTokenUsage, N as resolveCapturePath, O as resolveCopilotToken, Q as resolveInsightsConfigPath, R as resolveLegacyInsightsConfigPath, T as resolveRetentionDays } from './capture-DJVWBRum.js';
3
+ export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, c as CopilotUsageConfig, D as DEFAULT_PROMPT_RIGHT_METRICS, G as GoUsageConfig, I as InsightsConfig, d as InsightsOptions, J as JsonlCaptureStore, M as MessageMetrics, e as MessageTiming, f as MetricsState, P as PromptRightMetric, S as SessionAverage, g as SessionTokenUsage, h as SqliteCaptureStore, i as SqliteDb, j as StreamSample, k as createCaptureStore, l as createMetricsState, m as defaultDataDir, n as estimateStreamTokens, o as extractEventType, p as getSessionTokenUsage, q as insightsOptionsFromConfig, r as normalizeChatHeadersCapture, s as normalizeChatMessageCapture, t as normalizeChatParamsCapture, u as normalizeEventCapture, v as normalizeExperimentalChatMessagesTransformCapture, w as normalizeExperimentalChatSystemTransformCapture, x as normalizeToolCapture, y as openDatabase, z as readInsightsConfig, B as recordAssistantDelta, E as recordAssistantMessage, F as recordToolActivity, H as renderMetricsText, K as renderPromptRightMetricsText, L as renderResponseMetricsText, N as renderSessionTokenUsage, O as resolveCapturePath, Q as resolveCopilotToken, R as resolveInsightsConfigPath, T as resolveLegacyInsightsConfigPath, U as resolveRetentionDays } from './capture-An0IKDqr.js';
4
4
 
5
5
  type SessionActivity = {
6
6
  toolCalls: number;
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ import {
43
43
  resolveInsightsConfigPath,
44
44
  resolveLegacyInsightsConfigPath,
45
45
  resolveRetentionDays
46
- } from "./chunk-ROQFQXIH.js";
46
+ } from "./chunk-2HZVJIOC.js";
47
47
 
48
48
  // src/cli-shim.ts
49
49
  import { existsSync } from "fs";
package/dist/tui.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  renderPromptRightMetricsText,
25
25
  renderSessionTokenUsage,
26
26
  resolveCopilotToken
27
- } from "./chunk-ROQFQXIH.js";
27
+ } from "./chunk-2HZVJIOC.js";
28
28
 
29
29
  // src/tui.tsx
30
30
  import { createTextAttributes, StyledText } from "@opentui/core";
@@ -815,7 +815,11 @@ var tui = async (api, options) => {
815
815
  hydratedSessions.add(sessionID);
816
816
  try {
817
817
  const response = await api.client.session.messages({ sessionID });
818
- const messages = response.data ?? [];
818
+ const messages = (response.data ?? []).slice().sort((a, b) => {
819
+ const aCreated = a.info.time?.created;
820
+ const bCreated = b.info.time?.created;
821
+ return (typeof aCreated === "number" ? aCreated : 0) - (typeof bCreated === "number" ? bCreated : 0);
822
+ });
819
823
  for (const message of messages) {
820
824
  const info = message.info;
821
825
  const providerID = info.providerID;
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.0",
4
+ "version": "0.4.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",