@rejacky/opencode-insights 0.1.8 → 0.1.10

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 CHANGED
@@ -91,12 +91,30 @@ The `uninstall` command removes plugin config entries and local Insights data; i
91
91
 
92
92
  ## What You Get
93
93
 
94
- - Live TPS, average TPS, and average TTFT in the OpenCode session prompt zone.
94
+ - Configurable live metrics in the OpenCode session prompt zone.
95
95
  - Subagent status (running, done, failed, elapsed time, and token/context usage) in the sidebar.
96
96
  - Local capture of OpenCode hook/event data without redaction.
97
97
  - A local web viewer for reconstructed sessions, user turns, hidden request context, system/messages transforms, and assistant thinking/response sequences.
98
98
  - Native OpenCode footer components (project directory and version) remain visible — the plugin does not override `sidebar_footer` or `home_prompt_right` slots.
99
99
 
100
+ ## TUI Metrics Configuration
101
+
102
+ On TUI startup, Insights creates a configuration file beside its database:
103
+
104
+ ```text
105
+ ~/.opencode-insights/config.json
106
+ ```
107
+
108
+ With a custom database path, the configuration file is created in that database's directory. The default keeps the prompt-right display compact:
109
+
110
+ ```json
111
+ {
112
+ "promptRightMetrics": ["tps", "avg", "used", "cache"]
113
+ }
114
+ ```
115
+
116
+ `promptRightMetrics` controls both the fields and their order. Supported values are `tps`, `avg`, `ttft`, `used`, `cache`, `input`, `output`, and `reasoning`. Values that are not recognized are ignored; an empty or invalid configuration uses the default. Restart OpenCode after editing this file.
117
+
100
118
  ## Open The Viewer
101
119
 
102
120
  Start the local web viewer and open it in your browser:
@@ -0,0 +1,132 @@
1
+ type StreamSample = {
2
+ at: number;
3
+ tokens: number;
4
+ };
5
+ type MessageTiming = {
6
+ sessionID: string;
7
+ requestStartAt: number;
8
+ firstResponseAt?: number | undefined;
9
+ firstTokenAt?: number | undefined;
10
+ lastTokenAt?: number | undefined;
11
+ lastToolCallAt?: number | undefined;
12
+ };
13
+ type SessionAverage = {
14
+ totalTokens: number;
15
+ totalDurationMs: number;
16
+ totalTtftMs: number;
17
+ messageCount: number;
18
+ };
19
+ type AssistantResponseUsage = {
20
+ inputTokens?: number | undefined;
21
+ outputTokens?: number | undefined;
22
+ reasoningTokens?: number | undefined;
23
+ cacheReadTokens?: number | undefined;
24
+ cacheWriteTokens?: number | undefined;
25
+ finish?: string | undefined;
26
+ };
27
+ type PromptRightMetric = "tps" | "avg" | "ttft" | "used" | "cache" | "input" | "output" | "reasoning";
28
+ declare const DEFAULT_PROMPT_RIGHT_METRICS: PromptRightMetric[];
29
+ type MetricsState = {
30
+ streamSamplesBySession: Record<string, StreamSample[]>;
31
+ messageTimingByID: Record<string, MessageTiming>;
32
+ sessionAverageByID: Record<string, SessionAverage>;
33
+ latestResponseUsageBySession: Record<string, AssistantResponseUsage>;
34
+ };
35
+ declare function createMetricsState(): MetricsState;
36
+ declare function estimateStreamTokens(delta: string): number;
37
+ declare function recordAssistantMessage(state: MetricsState, input: {
38
+ sessionID: string;
39
+ messageID: string;
40
+ createdAt: number;
41
+ completedAt?: number;
42
+ outputTokens?: number;
43
+ reasoningTokens?: number;
44
+ inputTokens?: number;
45
+ cacheReadTokens?: number;
46
+ cacheWriteTokens?: number;
47
+ finish?: string;
48
+ }): void;
49
+ declare function recordAssistantDelta(state: MetricsState, input: {
50
+ sessionID: string;
51
+ messageID: string;
52
+ delta: string;
53
+ at: number;
54
+ }): void;
55
+ declare function recordToolActivity(state: MetricsState, sessionID: string, messageID: string, at?: number): void;
56
+ declare function renderMetricsText(state: MetricsState, sessionID: string, options?: {
57
+ now?: number;
58
+ idle?: boolean;
59
+ }): string;
60
+ declare function renderResponseMetricsText(state: MetricsState, sessionID: string): string;
61
+ declare function renderPromptRightMetricsText(state: MetricsState, sessionID: string, options?: {
62
+ now?: number;
63
+ idle?: boolean;
64
+ metrics?: PromptRightMetric[];
65
+ }): string;
66
+
67
+ type CaptureKind = "chat.message" | "chat.params" | "chat.headers" | "experimental.chat.messages.transform" | "experimental.chat.system.transform" | "event" | "tool.execute.before" | "tool.execute.after";
68
+ type CaptureRecord = {
69
+ id: string;
70
+ kind: CaptureKind;
71
+ timestamp: number;
72
+ sessionID?: string | undefined;
73
+ messageID?: string | undefined;
74
+ providerID?: string | undefined;
75
+ modelID?: string | undefined;
76
+ payload: Record<string, unknown>;
77
+ };
78
+ type CaptureStore = {
79
+ initialize?(): Promise<void>;
80
+ append(record: CaptureRecord): Promise<void>;
81
+ close?(): Promise<void>;
82
+ };
83
+ type InsightsOptions = {
84
+ dataDir?: unknown;
85
+ dbPath?: unknown;
86
+ retentionDays?: unknown;
87
+ };
88
+ type InsightsConfig = {
89
+ promptRightMetrics: PromptRightMetric[];
90
+ };
91
+ declare function defaultDataDir(): string;
92
+ declare function resolveCapturePath(options?: InsightsOptions): string;
93
+ declare function resolveInsightsConfigPath(options?: InsightsOptions): string;
94
+ declare function readInsightsConfig(options?: InsightsOptions): Promise<InsightsConfig>;
95
+ declare function normalizeChatMessageCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
96
+ declare function normalizeChatParamsCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
97
+ declare function normalizeChatHeadersCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
98
+ declare function normalizeExperimentalChatMessagesTransformCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
99
+ declare function normalizeExperimentalChatSystemTransformCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
100
+ declare function normalizeEventCapture(event: unknown, timestamp?: number): CaptureRecord;
101
+ declare function normalizeToolCapture(kind: "tool.execute.before" | "tool.execute.after", input: unknown, output: unknown, timestamp?: number): CaptureRecord;
102
+ declare class JsonlCaptureStore implements CaptureStore {
103
+ private readonly path;
104
+ private readonly retentionMs;
105
+ constructor(path: string, retentionMs?: number | undefined);
106
+ initialize(): Promise<void>;
107
+ append(record: CaptureRecord): Promise<void>;
108
+ private pruneExpired;
109
+ }
110
+ interface SqliteDb {
111
+ all(sql: string, ...params: unknown[]): Record<string, unknown>[];
112
+ run(sql: string, ...params: unknown[]): void;
113
+ sync(): void;
114
+ close(): void;
115
+ }
116
+ declare function openDatabase(path: string, readonly?: boolean): Promise<SqliteDb | undefined>;
117
+ declare function extractEventType(payload: Record<string, unknown>): string | null;
118
+ declare class SqliteCaptureStore implements CaptureStore {
119
+ private readonly path;
120
+ private readonly retentionMs;
121
+ private db;
122
+ private fallbackStore;
123
+ constructor(path: string, retentionMs?: number | undefined);
124
+ initialize(): Promise<void>;
125
+ append(record: CaptureRecord): Promise<void>;
126
+ close(): Promise<void>;
127
+ private pruneExpired;
128
+ }
129
+ declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
130
+ declare function resolveRetentionDays(value: unknown): number;
131
+
132
+ export { type AssistantResponseUsage as A, renderResponseMetricsText as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, resolveCapturePath as E, resolveInsightsConfigPath as F, resolveRetentionDays as G, type InsightsConfig as I, JsonlCaptureStore as J, type MessageTiming as M, type PromptRightMetric as P, type SessionAverage as S, type CaptureKind as a, type CaptureStore as b, type InsightsOptions as c, type MetricsState as d, SqliteCaptureStore as e, type SqliteDb as f, type StreamSample as g, createCaptureStore as h, createMetricsState as i, defaultDataDir as j, estimateStreamTokens as k, extractEventType as l, normalizeChatMessageCapture as m, normalizeChatHeadersCapture as n, normalizeChatParamsCapture as o, normalizeEventCapture as p, normalizeExperimentalChatMessagesTransformCapture as q, normalizeExperimentalChatSystemTransformCapture as r, normalizeToolCapture as s, openDatabase as t, readInsightsConfig as u, recordAssistantDelta as v, recordAssistantMessage as w, recordToolActivity as x, renderMetricsText as y, renderPromptRightMetricsText as z };
@@ -1,149 +1,3 @@
1
- // src/metrics.ts
2
- var STREAM_WINDOW_MS = 5e3;
3
- var LIVE_STALE_MS = 1500;
4
- var SINGLE_SAMPLE_MS = 1e3;
5
- function createMetricsState() {
6
- return {
7
- streamSamplesBySession: {},
8
- messageTimingByID: {},
9
- sessionAverageByID: {}
10
- };
11
- }
12
- function estimateStreamTokens(delta) {
13
- return Math.max(1, Math.ceil(Buffer.byteLength(delta, "utf8") / 5));
14
- }
15
- function recordAssistantMessage(state, input) {
16
- if (typeof input.completedAt !== "number") {
17
- const existing = state.messageTimingByID[input.messageID];
18
- state.messageTimingByID[input.messageID] = {
19
- sessionID: input.sessionID,
20
- requestStartAt: input.createdAt,
21
- firstResponseAt: existing?.firstResponseAt,
22
- firstTokenAt: existing?.firstTokenAt,
23
- lastTokenAt: existing?.lastTokenAt,
24
- lastToolCallAt: existing?.lastToolCallAt
25
- };
26
- return;
27
- }
28
- const timing = state.messageTimingByID[input.messageID];
29
- if (timing?.sessionID === input.sessionID && typeof timing.firstResponseAt === "number") {
30
- const totalTokens = (input.outputTokens ?? 0) + (input.reasoningTokens ?? 0);
31
- const endAt = input.finish === "tool-calls" ? timing.lastToolCallAt : input.completedAt;
32
- const durationMs = typeof endAt === "number" ? Math.max(endAt - timing.firstResponseAt, 1) : void 0;
33
- const ttftMs = Math.max(timing.firstResponseAt - timing.requestStartAt, 0);
34
- if (totalTokens > 0 && durationMs) {
35
- const totals = state.sessionAverageByID[input.sessionID] ?? {
36
- totalTokens: 0,
37
- totalDurationMs: 0,
38
- totalTtftMs: 0,
39
- messageCount: 0
40
- };
41
- state.sessionAverageByID[input.sessionID] = {
42
- totalTokens: totals.totalTokens + totalTokens,
43
- totalDurationMs: totals.totalDurationMs + durationMs,
44
- totalTtftMs: totals.totalTtftMs + ttftMs,
45
- messageCount: totals.messageCount + 1
46
- };
47
- }
48
- }
49
- delete state.messageTimingByID[input.messageID];
50
- pruneSamples(state, input.completedAt);
51
- }
52
- function recordAssistantDelta(state, input) {
53
- const sample = {
54
- at: input.at,
55
- tokens: estimateStreamTokens(input.delta)
56
- };
57
- state.streamSamplesBySession[input.sessionID] = [
58
- ...(state.streamSamplesBySession[input.sessionID] ?? []).filter((item) => input.at - item.at <= STREAM_WINDOW_MS),
59
- sample
60
- ];
61
- const timing = state.messageTimingByID[input.messageID];
62
- if (timing) {
63
- state.messageTimingByID[input.messageID] = timing.firstTokenAt ? { ...timing, lastTokenAt: input.at } : {
64
- ...timing,
65
- firstResponseAt: timing.firstResponseAt ?? input.at,
66
- firstTokenAt: input.at,
67
- lastTokenAt: input.at
68
- };
69
- }
70
- }
71
- function recordToolActivity(state, sessionID, messageID, at = Date.now()) {
72
- if (state.streamSamplesBySession[sessionID]?.length) {
73
- delete state.streamSamplesBySession[sessionID];
74
- }
75
- const timing = state.messageTimingByID[messageID];
76
- if (timing) {
77
- state.messageTimingByID[messageID] = {
78
- ...timing,
79
- lastToolCallAt: at,
80
- firstResponseAt: timing.firstResponseAt ?? at
81
- };
82
- }
83
- }
84
- function renderMetricsText(state, sessionID, options = {}) {
85
- const live = liveTps(state, sessionID, options) ?? "-";
86
- const avg = sessionAverage(state, sessionID) ?? "-";
87
- const ttft = sessionTtft(state, sessionID) ?? "-";
88
- return `TPS ${live} | AVG ${avg} | TTFT ${ttft}`;
89
- }
90
- function pruneSamples(state, now = Date.now()) {
91
- for (const [sessionID, samples] of Object.entries(state.streamSamplesBySession)) {
92
- const next = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
93
- if (next.length > 0) state.streamSamplesBySession[sessionID] = next;
94
- else delete state.streamSamplesBySession[sessionID];
95
- }
96
- }
97
- function sessionAverage(state, sessionID) {
98
- const totals = state.sessionAverageByID[sessionID];
99
- if (!totals || totals.totalTokens <= 0 || totals.totalDurationMs <= 0) return void 0;
100
- return formatRate(totals.totalTokens / (totals.totalDurationMs / 1e3), "AVG");
101
- }
102
- function sessionTtft(state, sessionID) {
103
- const totals = state.sessionAverageByID[sessionID];
104
- if (!totals || totals.messageCount <= 0 || totals.totalTtftMs < 0) return void 0;
105
- return formatTtft(totals.totalTtftMs / totals.messageCount / 1e3);
106
- }
107
- function liveTps(state, sessionID, options = {}) {
108
- if (options.idle) return void 0;
109
- const now = options.now ?? Date.now();
110
- const samples = state.streamSamplesBySession[sessionID] ?? [];
111
- const relevant = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
112
- if (relevant.length === 0) return void 0;
113
- const lastSample = relevant.at(-1);
114
- if (!lastSample || now - lastSample.at > LIVE_STALE_MS) return void 0;
115
- const total = relevant.reduce((sum, sample) => sum + sample.tokens, 0);
116
- const durationSeconds = activeDurationMs(relevant, now) / 1e3;
117
- if (durationSeconds <= 0) return void 0;
118
- return formatRate(total / durationSeconds, "TPS");
119
- }
120
- function activeDurationMs(samples, tailAt) {
121
- if (samples.length === 0) return 0;
122
- if (samples.length === 1) {
123
- const tailDuration = tailAt ? Math.max(0, tailAt - samples[0].at) : SINGLE_SAMPLE_MS;
124
- return Math.min(Math.max(tailDuration, 250), SINGLE_SAMPLE_MS);
125
- }
126
- let duration = 0;
127
- for (let index = 1; index < samples.length; index++) {
128
- duration += Math.max(0, samples[index].at - samples[index - 1].at);
129
- }
130
- if (tailAt) {
131
- duration += Math.max(0, tailAt - samples.at(-1).at);
132
- }
133
- return Math.max(duration, SINGLE_SAMPLE_MS);
134
- }
135
- function formatRate(value, label) {
136
- if (!Number.isFinite(value) || value <= 0) return void 0;
137
- const suffix = label === "TPS" ? " TPS" : "";
138
- if (value >= 100) return `${Math.round(value)}${suffix}`;
139
- if (value >= 10) return `${value.toFixed(1)}${suffix}`;
140
- return `${value.toFixed(2)}${suffix}`;
141
- }
142
- function formatTtft(value) {
143
- if (!Number.isFinite(value) || value < 0) return void 0;
144
- return `${value.toFixed(1)}s`;
145
- }
146
-
147
1
  // src/subagents.ts
148
2
  function createSubagentState() {
149
3
  return { children: {}, totalExecuted: 0 };
@@ -229,6 +83,14 @@ function getSubagentSidebarModel(state, parentID, options = {}) {
229
83
  }))
230
84
  };
231
85
  }
86
+ function getSubagentSidebarRowAtLine(model, line) {
87
+ let rowStart = 2;
88
+ for (const [index, row] of model.rows.entries()) {
89
+ if (index > 0) rowStart += 1;
90
+ if (line === rowStart || line === rowStart + 1) return row;
91
+ rowStart += 2;
92
+ }
93
+ }
232
94
  function renderSubagentSidebar(state, parentID, options = {}) {
233
95
  const model = getSubagentSidebarModel(state, parentID, options);
234
96
  if (!model) return "";
@@ -438,18 +300,13 @@ function isRecord(value) {
438
300
  }
439
301
 
440
302
  export {
441
- createMetricsState,
442
- estimateStreamTokens,
443
- recordAssistantMessage,
444
- recordAssistantDelta,
445
- recordToolActivity,
446
- renderMetricsText,
447
303
  createSubagentState,
448
304
  applySubagentEvent,
449
305
  renderSubagentStatus,
450
306
  getSubagentItems,
451
307
  pruneStaleSubagents,
452
308
  getSubagentSidebarModel,
309
+ getSubagentSidebarRowAtLine,
453
310
  renderSubagentSidebar,
454
311
  renderSubagentFooter
455
312
  };
@@ -1,3 +1,219 @@
1
+ // src/metrics.ts
2
+ var DEFAULT_PROMPT_RIGHT_METRICS = ["tps", "avg", "used", "cache"];
3
+ var STREAM_WINDOW_MS = 5e3;
4
+ var LIVE_STALE_MS = 1500;
5
+ var SINGLE_SAMPLE_MS = 1e3;
6
+ function createMetricsState() {
7
+ return {
8
+ streamSamplesBySession: {},
9
+ messageTimingByID: {},
10
+ sessionAverageByID: {},
11
+ latestResponseUsageBySession: {}
12
+ };
13
+ }
14
+ function estimateStreamTokens(delta) {
15
+ return Math.max(1, Math.ceil(Buffer.byteLength(delta, "utf8") / 5));
16
+ }
17
+ function recordAssistantMessage(state, input) {
18
+ if (typeof input.completedAt !== "number") {
19
+ const existing = state.messageTimingByID[input.messageID];
20
+ state.messageTimingByID[input.messageID] = {
21
+ sessionID: input.sessionID,
22
+ requestStartAt: input.createdAt,
23
+ firstResponseAt: existing?.firstResponseAt,
24
+ firstTokenAt: existing?.firstTokenAt,
25
+ lastTokenAt: existing?.lastTokenAt,
26
+ lastToolCallAt: existing?.lastToolCallAt
27
+ };
28
+ return;
29
+ }
30
+ const usage = compactUsage({
31
+ inputTokens: input.inputTokens,
32
+ outputTokens: input.outputTokens,
33
+ reasoningTokens: input.reasoningTokens,
34
+ cacheReadTokens: input.cacheReadTokens,
35
+ cacheWriteTokens: input.cacheWriteTokens,
36
+ finish: input.finish
37
+ });
38
+ if (hasTokenUsage(usage)) state.latestResponseUsageBySession[input.sessionID] = usage;
39
+ const timing = state.messageTimingByID[input.messageID];
40
+ if (timing?.sessionID === input.sessionID && typeof timing.firstResponseAt === "number") {
41
+ const totalTokens = (input.outputTokens ?? 0) + (input.reasoningTokens ?? 0);
42
+ const endAt = input.finish === "tool-calls" ? timing.lastToolCallAt : input.completedAt;
43
+ const durationMs = typeof endAt === "number" ? Math.max(endAt - timing.firstResponseAt, 1) : void 0;
44
+ const ttftMs = Math.max(timing.firstResponseAt - timing.requestStartAt, 0);
45
+ if (totalTokens > 0 && durationMs) {
46
+ const totals = state.sessionAverageByID[input.sessionID] ?? {
47
+ totalTokens: 0,
48
+ totalDurationMs: 0,
49
+ totalTtftMs: 0,
50
+ messageCount: 0
51
+ };
52
+ state.sessionAverageByID[input.sessionID] = {
53
+ totalTokens: totals.totalTokens + totalTokens,
54
+ totalDurationMs: totals.totalDurationMs + durationMs,
55
+ totalTtftMs: totals.totalTtftMs + ttftMs,
56
+ messageCount: totals.messageCount + 1
57
+ };
58
+ }
59
+ }
60
+ delete state.messageTimingByID[input.messageID];
61
+ pruneSamples(state, input.completedAt);
62
+ }
63
+ function recordAssistantDelta(state, input) {
64
+ const sample = {
65
+ at: input.at,
66
+ tokens: estimateStreamTokens(input.delta)
67
+ };
68
+ state.streamSamplesBySession[input.sessionID] = [
69
+ ...(state.streamSamplesBySession[input.sessionID] ?? []).filter((item) => input.at - item.at <= STREAM_WINDOW_MS),
70
+ sample
71
+ ];
72
+ const timing = state.messageTimingByID[input.messageID];
73
+ if (timing) {
74
+ state.messageTimingByID[input.messageID] = timing.firstTokenAt ? { ...timing, lastTokenAt: input.at } : {
75
+ ...timing,
76
+ firstResponseAt: timing.firstResponseAt ?? input.at,
77
+ firstTokenAt: input.at,
78
+ lastTokenAt: input.at
79
+ };
80
+ }
81
+ }
82
+ function recordToolActivity(state, sessionID, messageID, at = Date.now()) {
83
+ if (state.streamSamplesBySession[sessionID]?.length) {
84
+ delete state.streamSamplesBySession[sessionID];
85
+ }
86
+ const timing = state.messageTimingByID[messageID];
87
+ if (timing) {
88
+ state.messageTimingByID[messageID] = {
89
+ ...timing,
90
+ lastToolCallAt: at,
91
+ firstResponseAt: timing.firstResponseAt ?? at
92
+ };
93
+ }
94
+ }
95
+ function renderMetricsText(state, sessionID, options = {}) {
96
+ const live = liveTps(state, sessionID, options) ?? "-";
97
+ const avg = sessionAverage(state, sessionID) ?? "-";
98
+ const ttft = sessionTtft(state, sessionID) ?? "-";
99
+ return `TPS ${live} | AVG ${avg} | TTFT ${ttft}`;
100
+ }
101
+ function renderResponseMetricsText(state, sessionID) {
102
+ const usage = state.latestResponseUsageBySession[sessionID];
103
+ if (!usage || !hasTokenUsage(usage)) return "";
104
+ const used = sumTokens(usage);
105
+ const cacheRate = cacheReadRate(usage);
106
+ const parts = [
107
+ used === void 0 ? void 0 : `${formatTokenCount(used)} used`,
108
+ cacheRate === void 0 ? void 0 : `${formatPercent(cacheRate)} cache`,
109
+ usage.outputTokens === void 0 ? void 0 : `${formatTokenCount(usage.outputTokens)} out`,
110
+ usage.reasoningTokens === void 0 ? void 0 : `${formatTokenCount(usage.reasoningTokens)} think`
111
+ ].filter((part) => !!part);
112
+ return parts.join(" | ");
113
+ }
114
+ function renderPromptRightMetricsText(state, sessionID, options = {}) {
115
+ const usage = state.latestResponseUsageBySession[sessionID];
116
+ const used = usage ? sumTokens(usage) : void 0;
117
+ const cacheRate = usage ? cacheReadRate(usage) : void 0;
118
+ const values = {
119
+ tps: `TPS ${liveTps(state, sessionID, options) ?? "-"}`,
120
+ avg: `AVG ${sessionAverage(state, sessionID) ?? "-"}`,
121
+ ttft: `TTFT ${sessionTtft(state, sessionID) ?? "-"}`,
122
+ used: `${used === void 0 ? "-" : formatTokenCount(used)} used`,
123
+ cache: `${cacheRate === void 0 ? "-" : formatPercent(cacheRate)} cache`,
124
+ input: `${usage?.inputTokens === void 0 ? "-" : formatTokenCount(usage.inputTokens)} in`,
125
+ output: `${usage?.outputTokens === void 0 ? "-" : formatTokenCount(usage.outputTokens)} out`,
126
+ reasoning: `${usage?.reasoningTokens === void 0 ? "-" : formatTokenCount(usage.reasoningTokens)} think`
127
+ };
128
+ return (options.metrics?.length ? options.metrics : DEFAULT_PROMPT_RIGHT_METRICS).map((metric) => values[metric]).join(" | ");
129
+ }
130
+ function pruneSamples(state, now = Date.now()) {
131
+ for (const [sessionID, samples] of Object.entries(state.streamSamplesBySession)) {
132
+ const next = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
133
+ if (next.length > 0) state.streamSamplesBySession[sessionID] = next;
134
+ else delete state.streamSamplesBySession[sessionID];
135
+ }
136
+ }
137
+ function sessionAverage(state, sessionID) {
138
+ const totals = state.sessionAverageByID[sessionID];
139
+ if (!totals || totals.totalTokens <= 0 || totals.totalDurationMs <= 0) return void 0;
140
+ return formatRate(totals.totalTokens / (totals.totalDurationMs / 1e3), "AVG");
141
+ }
142
+ function sessionTtft(state, sessionID) {
143
+ const totals = state.sessionAverageByID[sessionID];
144
+ if (!totals || totals.messageCount <= 0 || totals.totalTtftMs < 0) return void 0;
145
+ return formatTtft(totals.totalTtftMs / totals.messageCount / 1e3);
146
+ }
147
+ function liveTps(state, sessionID, options = {}) {
148
+ if (options.idle) return void 0;
149
+ const now = options.now ?? Date.now();
150
+ const samples = state.streamSamplesBySession[sessionID] ?? [];
151
+ const relevant = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
152
+ if (relevant.length === 0) return void 0;
153
+ const lastSample = relevant.at(-1);
154
+ if (!lastSample || now - lastSample.at > LIVE_STALE_MS) return void 0;
155
+ const total = relevant.reduce((sum, sample) => sum + sample.tokens, 0);
156
+ const durationSeconds = activeDurationMs(relevant, now) / 1e3;
157
+ if (durationSeconds <= 0) return void 0;
158
+ return formatRate(total / durationSeconds, "TPS");
159
+ }
160
+ function activeDurationMs(samples, tailAt) {
161
+ if (samples.length === 0) return 0;
162
+ if (samples.length === 1) {
163
+ const tailDuration = tailAt ? Math.max(0, tailAt - samples[0].at) : SINGLE_SAMPLE_MS;
164
+ return Math.min(Math.max(tailDuration, 250), SINGLE_SAMPLE_MS);
165
+ }
166
+ let duration = 0;
167
+ for (let index = 1; index < samples.length; index++) {
168
+ duration += Math.max(0, samples[index].at - samples[index - 1].at);
169
+ }
170
+ if (tailAt) {
171
+ duration += Math.max(0, tailAt - samples.at(-1).at);
172
+ }
173
+ return Math.max(duration, SINGLE_SAMPLE_MS);
174
+ }
175
+ function formatRate(value, label) {
176
+ if (!Number.isFinite(value) || value <= 0) return void 0;
177
+ const suffix = label === "TPS" ? " TPS" : "";
178
+ if (value >= 100) return `${Math.round(value)}${suffix}`;
179
+ if (value >= 10) return `${value.toFixed(1)}${suffix}`;
180
+ return `${value.toFixed(2)}${suffix}`;
181
+ }
182
+ function formatTtft(value) {
183
+ if (!Number.isFinite(value) || value < 0) return void 0;
184
+ return `${value.toFixed(1)}s`;
185
+ }
186
+ function compactUsage(usage) {
187
+ return Object.fromEntries(Object.entries(usage).filter(([, value]) => value !== void 0));
188
+ }
189
+ function hasTokenUsage(usage) {
190
+ return [usage.inputTokens, usage.outputTokens, usage.reasoningTokens, usage.cacheReadTokens, usage.cacheWriteTokens].some(
191
+ (value) => typeof value === "number"
192
+ );
193
+ }
194
+ function sumTokens(usage) {
195
+ const values = [usage.inputTokens, usage.outputTokens, usage.reasoningTokens, usage.cacheReadTokens, usage.cacheWriteTokens].filter(
196
+ (value) => typeof value === "number"
197
+ );
198
+ return values.length ? values.reduce((sum, value) => sum + value, 0) : void 0;
199
+ }
200
+ function cacheReadRate(usage) {
201
+ if (typeof usage.cacheReadTokens !== "number") return void 0;
202
+ const promptTokens = (usage.inputTokens ?? 0) + usage.cacheReadTokens;
203
+ return promptTokens > 0 ? usage.cacheReadTokens / promptTokens * 100 : void 0;
204
+ }
205
+ function formatTokenCount(value) {
206
+ if (Math.abs(value) >= 1e6) return `${formatAbbreviatedTokenCount(value, 1e6)}m`;
207
+ if (Math.abs(value) >= 1e3) return `${formatAbbreviatedTokenCount(value, 1e3)}k`;
208
+ return String(Math.round(value));
209
+ }
210
+ function formatPercent(value) {
211
+ return `${(Math.round(value * 10) / 10).toFixed(1).replace(/\.0$/u, "")}%`;
212
+ }
213
+ function formatAbbreviatedTokenCount(value, divisor) {
214
+ return (Math.round(value / divisor * 10) / 10).toFixed(1);
215
+ }
216
+
1
217
  // src/capture.ts
2
218
  import { mkdir, appendFile, readFile, writeFile } from "fs/promises";
3
219
  import { existsSync, readFileSync } from "fs";
@@ -62,6 +278,37 @@ function resolveCapturePath(options = {}) {
62
278
  const dataDir = typeof options.dataDir === "string" && options.dataDir.length > 0 ? options.dataDir : defaultDataDir();
63
279
  return join(dataDir, "insights.sqlite");
64
280
  }
281
+ function resolveInsightsConfigPath(options = {}) {
282
+ return join(dirname(resolveCapturePath(options)), "config.json");
283
+ }
284
+ async function readInsightsConfig(options = {}) {
285
+ const path = resolveInsightsConfigPath(options);
286
+ if (!existsSync(path)) {
287
+ try {
288
+ await mkdir(dirname(path), { recursive: true });
289
+ await writeFile(path, `${JSON.stringify(defaultInsightsConfig(), null, 2)}
290
+ `, "utf8");
291
+ } catch {
292
+ return defaultInsightsConfig();
293
+ }
294
+ }
295
+ try {
296
+ return insightsConfigFrom(JSON.parse(await readFile(path, "utf8")));
297
+ } catch {
298
+ return defaultInsightsConfig();
299
+ }
300
+ }
301
+ function defaultInsightsConfig() {
302
+ return { promptRightMetrics: [...DEFAULT_PROMPT_RIGHT_METRICS] };
303
+ }
304
+ function insightsConfigFrom(value) {
305
+ if (!isRecord(value) || !Array.isArray(value.promptRightMetrics)) return defaultInsightsConfig();
306
+ const metrics = value.promptRightMetrics.filter(isPromptRightMetric);
307
+ return metrics.length ? { promptRightMetrics: metrics } : defaultInsightsConfig();
308
+ }
309
+ function isPromptRightMetric(value) {
310
+ return value === "tps" || value === "avg" || value === "ttft" || value === "used" || value === "cache" || value === "input" || value === "output" || value === "reasoning";
311
+ }
65
312
  function normalizeChatMessageCapture(input, output, timestamp = Date.now()) {
66
313
  const inputRecord = isRecord(input) ? input : {};
67
314
  const model = isRecord(inputRecord.model) ? inputRecord.model : void 0;
@@ -384,8 +631,19 @@ function retentionCutoff(now, retentionMs) {
384
631
  }
385
632
 
386
633
  export {
634
+ DEFAULT_PROMPT_RIGHT_METRICS,
635
+ createMetricsState,
636
+ estimateStreamTokens,
637
+ recordAssistantMessage,
638
+ recordAssistantDelta,
639
+ recordToolActivity,
640
+ renderMetricsText,
641
+ renderResponseMetricsText,
642
+ renderPromptRightMetricsText,
387
643
  defaultDataDir,
388
644
  resolveCapturePath,
645
+ resolveInsightsConfigPath,
646
+ readInsightsConfig,
389
647
  normalizeChatMessageCapture,
390
648
  normalizeChatParamsCapture,
391
649
  normalizeChatHeadersCapture,
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as CaptureRecord } from './capture-BMWWI5GR.js';
2
+ import { C as CaptureRecord } from './capture-D7z7hS72.js';
3
3
 
4
4
  type HistoryMessage = {
5
5
  id: string;
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-TGCIHODI.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { execFile as execFile2 } from "child_process";
@@ -355,10 +355,9 @@ function recentCaptureSql(limit) {
355
355
  order by timestamp desc`;
356
356
  }
357
357
  function viewerCaptureSql(limit) {
358
- return `select id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
359
- from captures
360
- where id in (
361
- select id from captures
358
+ return `with recent_model as (
359
+ select id, session_id
360
+ from captures
362
361
  where kind in (
363
362
  'chat.params',
364
363
  'chat.message',
@@ -366,9 +365,10 @@ function viewerCaptureSql(limit) {
366
365
  )
367
366
  order by timestamp desc
368
367
  limit ${limit}
369
- )
370
- or id in (
371
- select id from captures
368
+ ),
369
+ recent_events as (
370
+ select id, session_id, payload_json
371
+ from captures
372
372
  where kind = 'event'
373
373
  and event_type in (
374
374
  'message.updated',
@@ -379,7 +379,36 @@ function viewerCaptureSql(limit) {
379
379
  )
380
380
  order by timestamp desc
381
381
  limit ${limit}
382
+ ),
383
+ recent_sessions as (
384
+ select session_id from recent_model where session_id is not null
385
+ union
386
+ select session_id from recent_events where session_id is not null
387
+ union
388
+ select json_extract(payload_json, '$.event.properties.sessionID') from recent_events where json_extract(payload_json, '$.event.properties.sessionID') is not null
389
+ union
390
+ select json_extract(payload_json, '$.event.properties.info.sessionID') from recent_events where json_extract(payload_json, '$.event.properties.info.sessionID') is not null
391
+ ),
392
+ metadata_events as (
393
+ select id
394
+ from captures
395
+ where kind = 'event'
396
+ and event_type in ('message.updated', 'session.updated', 'session.created')
397
+ and coalesce(
398
+ session_id,
399
+ json_extract(payload_json, '$.event.properties.sessionID'),
400
+ json_extract(payload_json, '$.event.properties.info.sessionID')
401
+ ) in (select session_id from recent_sessions)
402
+ and (
403
+ json_extract(payload_json, '$.event.properties.info.path.cwd') is not null
404
+ or json_extract(payload_json, '$.event.properties.info.path.root') is not null
405
+ )
382
406
  )
407
+ select id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
408
+ from captures
409
+ where id in (select id from recent_model)
410
+ or id in (select id from recent_events)
411
+ or id in (select id from metadata_events)
383
412
  order by timestamp desc`;
384
413
  }
385
414
  function isViewerCaptureKind(kind) {
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 SqliteCaptureStore, f as SqliteDb, g as StreamSample, h as createCaptureStore, i as createMetricsState, j as defaultDataDir, k as estimateStreamTokens, l as extractEventType, n as normalizeChatHeadersCapture, m as normalizeChatMessageCapture, o as normalizeChatParamsCapture, p as normalizeEventCapture, q as normalizeExperimentalChatMessagesTransformCapture, r as normalizeExperimentalChatSystemTransformCapture, s as normalizeToolCapture, t as openDatabase, u as readInsightsConfig, v as recordAssistantDelta, w as recordAssistantMessage, x as recordToolActivity, y as renderMetricsText, z as renderPromptRightMetricsText, B as renderResponseMetricsText, E as resolveCapturePath, F as resolveInsightsConfigPath, G as resolveRetentionDays } from './capture-D7z7hS72.js';
50
4
 
51
5
  type SubagentStatus = "running" | "done" | "error";
52
6
  type SubagentInfo = {
@@ -94,6 +48,7 @@ declare function getSubagentSidebarModel(state: SubagentState, parentID: string,
94
48
  now?: number;
95
49
  staleMs?: number;
96
50
  }): SubagentSidebarModel | undefined;
51
+ declare function getSubagentSidebarRowAtLine(model: SubagentSidebarModel, line: number): SubagentSidebarRow | undefined;
97
52
  declare function renderSubagentSidebar(state: SubagentState, parentID: string, options?: {
98
53
  now?: number;
99
54
  }): string;
@@ -111,4 +66,4 @@ declare const _default: {
111
66
  server: Plugin;
112
67
  };
113
68
 
114
- 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, 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,24 +1,22 @@
1
1
  import {
2
2
  applySubagentEvent,
3
- createMetricsState,
4
3
  createSubagentState,
5
- estimateStreamTokens,
6
4
  getSubagentItems,
7
5
  getSubagentSidebarModel,
6
+ getSubagentSidebarRowAtLine,
8
7
  pruneStaleSubagents,
9
- recordAssistantDelta,
10
- recordAssistantMessage,
11
- recordToolActivity,
12
- renderMetricsText,
13
8
  renderSubagentFooter,
14
9
  renderSubagentSidebar,
15
10
  renderSubagentStatus
16
- } from "./chunk-36ZNCLI3.js";
11
+ } from "./chunk-O5FFLPEQ.js";
17
12
  import {
13
+ DEFAULT_PROMPT_RIGHT_METRICS,
18
14
  JsonlCaptureStore,
19
15
  SqliteCaptureStore,
20
16
  createCaptureStore,
17
+ createMetricsState,
21
18
  defaultDataDir,
19
+ estimateStreamTokens,
22
20
  extractEventType,
23
21
  normalizeChatHeadersCapture,
24
22
  normalizeChatMessageCapture,
@@ -28,9 +26,17 @@ import {
28
26
  normalizeExperimentalChatSystemTransformCapture,
29
27
  normalizeToolCapture,
30
28
  openDatabase,
29
+ readInsightsConfig,
30
+ recordAssistantDelta,
31
+ recordAssistantMessage,
32
+ recordToolActivity,
33
+ renderMetricsText,
34
+ renderPromptRightMetricsText,
35
+ renderResponseMetricsText,
31
36
  resolveCapturePath,
37
+ resolveInsightsConfigPath,
32
38
  resolveRetentionDays
33
- } from "./chunk-FGTKNB7T.js";
39
+ } from "./chunk-TGCIHODI.js";
34
40
 
35
41
  // src/cli-shim.ts
36
42
  import { existsSync } from "fs";
@@ -132,6 +138,7 @@ var rootTui = async (...args) => {
132
138
  var id = "opencode-insights";
133
139
  var src_default = { id, server };
134
140
  export {
141
+ DEFAULT_PROMPT_RIGHT_METRICS,
135
142
  JsonlCaptureStore,
136
143
  OpenCodeInsights,
137
144
  SqliteCaptureStore,
@@ -145,6 +152,7 @@ export {
145
152
  extractEventType,
146
153
  getSubagentItems,
147
154
  getSubagentSidebarModel,
155
+ getSubagentSidebarRowAtLine,
148
156
  id,
149
157
  normalizeChatHeadersCapture,
150
158
  normalizeChatMessageCapture,
@@ -155,14 +163,18 @@ export {
155
163
  normalizeToolCapture,
156
164
  openDatabase,
157
165
  pruneStaleSubagents,
166
+ readInsightsConfig,
158
167
  recordAssistantDelta,
159
168
  recordAssistantMessage,
160
169
  recordToolActivity,
161
170
  renderMetricsText,
171
+ renderPromptRightMetricsText,
172
+ renderResponseMetricsText,
162
173
  renderSubagentFooter,
163
174
  renderSubagentSidebar,
164
175
  renderSubagentStatus,
165
176
  resolveCapturePath,
177
+ resolveInsightsConfigPath,
166
178
  resolveRetentionDays,
167
179
  server,
168
180
  rootTui as tui
package/dist/tui.js CHANGED
@@ -1,13 +1,17 @@
1
1
  import {
2
2
  applySubagentEvent,
3
- createMetricsState,
4
3
  createSubagentState,
5
4
  getSubagentSidebarModel,
5
+ getSubagentSidebarRowAtLine
6
+ } from "./chunk-O5FFLPEQ.js";
7
+ import {
8
+ createMetricsState,
9
+ readInsightsConfig,
6
10
  recordAssistantDelta,
7
11
  recordAssistantMessage,
8
12
  recordToolActivity,
9
- renderMetricsText
10
- } from "./chunk-36ZNCLI3.js";
13
+ renderPromptRightMetricsText
14
+ } from "./chunk-TGCIHODI.js";
11
15
 
12
16
  // src/tui.tsx
13
17
  import { createTextAttributes, StyledText } from "@opentui/core";
@@ -16,18 +20,22 @@ import { jsx } from "@opentui/solid/jsx-runtime";
16
20
  function isSessionID(value) {
17
21
  return typeof value === "string" && value.startsWith("ses");
18
22
  }
19
- function PromptRight(props) {
23
+ function PromptRightMetrics(props) {
20
24
  let text;
21
25
  const sync = () => {
22
26
  if (!text) return;
23
27
  const content = props.text();
24
28
  text.content = content;
25
29
  text.visible = content.length > 0;
26
- text.height = content.length > 0 ? "auto" : 0;
30
+ text.height = content.length > 0 ? 1 : 0;
27
31
  props.api.renderer.requestRender();
28
32
  };
29
33
  const unsubscribe = props.subscribe(sync);
30
- onCleanup(unsubscribe);
34
+ const timer = setInterval(sync, 1e3);
35
+ onCleanup(() => {
36
+ unsubscribe();
37
+ clearInterval(timer);
38
+ });
31
39
  return /* @__PURE__ */ jsx(
32
40
  "text",
33
41
  {
@@ -36,6 +44,10 @@ function PromptRight(props) {
36
44
  sync();
37
45
  },
38
46
  fg: props.api.theme.current.textMuted,
47
+ height: 1,
48
+ wrapMode: "none",
49
+ truncate: true,
50
+ overflow: "hidden",
39
51
  children: props.text()
40
52
  }
41
53
  );
@@ -43,17 +55,41 @@ function PromptRight(props) {
43
55
  function SubagentSidebar(props) {
44
56
  let text;
45
57
  const [collapsed, setCollapsed] = createSignal(false);
58
+ const [hoveredRowID, setHoveredRowID] = createSignal();
46
59
  const titleAttributes = createTextAttributes({ bold: true });
47
- const toggle = () => {
60
+ const toggle = (event) => {
61
+ if (!text || event.y !== text.y) return;
48
62
  setCollapsed((prev) => !prev);
49
63
  props.api.renderer.requestRender();
50
64
  };
65
+ const openSubagent = (event) => {
66
+ if (!text || collapsed()) return;
67
+ const model = getSubagentSidebarModel(props.state, props.sessionID);
68
+ if (!model) return;
69
+ const row = getSubagentSidebarRowAtLine(model, event.y - text.y);
70
+ if (!row) return;
71
+ props.api.route.navigate("session", { sessionID: row.id });
72
+ };
73
+ const hoverSubagent = (event) => {
74
+ if (!text || collapsed()) return;
75
+ const model = getSubagentSidebarModel(props.state, props.sessionID);
76
+ const row = model && getSubagentSidebarRowAtLine(model, event.y - text.y);
77
+ const nextRowID = row?.id;
78
+ if (nextRowID === hoveredRowID()) return;
79
+ setHoveredRowID(nextRowID);
80
+ sync();
81
+ };
82
+ const clearHoveredSubagent = () => {
83
+ if (!hoveredRowID()) return;
84
+ setHoveredRowID(void 0);
85
+ sync();
86
+ };
51
87
  const sync = () => {
52
88
  if (!text) return;
53
89
  const model = getSubagentSidebarModel(props.state, props.sessionID);
54
90
  text.visible = !!model;
55
91
  text.height = model ? "auto" : 0;
56
- text.content = model ? renderSubagentStyledSidebar(props.state, props.sessionID, props.api, titleAttributes, collapsed()) : "";
92
+ text.content = model ? renderSubagentStyledSidebar(props.state, props.sessionID, props.api, titleAttributes, collapsed(), hoveredRowID()) : "";
57
93
  props.api.renderer.requestRender();
58
94
  };
59
95
  const unsubscribe = props.subscribe(sync);
@@ -70,12 +106,15 @@ function SubagentSidebar(props) {
70
106
  sync();
71
107
  },
72
108
  onMouseDown: toggle,
109
+ onMouseUp: openSubagent,
110
+ onMouseMove: hoverSubagent,
111
+ onMouseOut: clearHoveredSubagent,
73
112
  fg: props.api.theme.current.textMuted,
74
113
  children: ""
75
114
  }
76
115
  );
77
116
  }
78
- function renderSubagentStyledSidebar(state, sessionID, api, titleAttributes, collapsed) {
117
+ function renderSubagentStyledSidebar(state, sessionID, api, titleAttributes, collapsed, hoveredRowID) {
79
118
  const model = getSubagentSidebarModel(state, sessionID);
80
119
  if (!model) return "";
81
120
  const indicator = collapsed ? "\u25B6 " : "\u25BC ";
@@ -89,23 +128,26 @@ function renderSubagentStyledSidebar(state, sessionID, api, titleAttributes, col
89
128
  for (const [index, row] of model.rows.entries()) {
90
129
  if (index > 0) chunks.push(textChunk("\n"));
91
130
  const dotColor = row.status === "running" ? api.theme.current.success : row.status === "error" ? api.theme.current.error : api.theme.current.textMuted;
92
- chunks.push(textChunk("\u2022 ", dotColor));
131
+ const background = row.id === hoveredRowID ? api.theme.current.backgroundElement : void 0;
132
+ chunks.push(textChunk("\u2022 ", dotColor, void 0, background));
93
133
  chunks.push(textChunk(`${row.title}
94
- `, api.theme.current.text));
95
- chunks.push(textChunk(row.subtitle, api.theme.current.textMuted));
134
+ `, api.theme.current.text, void 0, background));
135
+ chunks.push(textChunk(row.subtitle, api.theme.current.textMuted, void 0, background));
96
136
  }
97
137
  }
98
138
  return new StyledText(chunks);
99
139
  }
100
- function textChunk(text, fg, attributes) {
140
+ function textChunk(text, fg, attributes, bg) {
101
141
  return {
102
142
  __isChunk: true,
103
143
  text,
104
144
  ...fg === void 0 ? {} : { fg },
105
- ...attributes === void 0 ? {} : { attributes }
145
+ ...attributes === void 0 ? {} : { attributes },
146
+ ...bg === void 0 ? {} : { bg }
106
147
  };
107
148
  }
108
- var tui = async (api) => {
149
+ var tui = async (api, options) => {
150
+ const config = await readInsightsConfig(options ?? {});
109
151
  const metrics = createMetricsState();
110
152
  const subagents = createSubagentState();
111
153
  const listeners = /* @__PURE__ */ new Set();
@@ -134,8 +176,11 @@ var tui = async (api) => {
134
176
  sessionID: info.sessionID ?? evt.properties.sessionID,
135
177
  messageID: info.id,
136
178
  createdAt: info.time.created,
179
+ inputTokens: info.tokens.input,
137
180
  outputTokens: info.tokens.output,
138
- reasoningTokens: info.tokens.reasoning
181
+ reasoningTokens: info.tokens.reasoning,
182
+ cacheReadTokens: info.tokens.cache?.read,
183
+ cacheWriteTokens: info.tokens.cache?.write
139
184
  };
140
185
  if (typeof info.time.completed === "number") messageInput.completedAt = info.time.completed;
141
186
  if (typeof info.finish === "string") messageInput.finish = info.finish;
@@ -168,7 +213,7 @@ var tui = async (api) => {
168
213
  const offSlots = api.slots.register({
169
214
  slots: {
170
215
  session_prompt_right: (_ctx, props) => /* @__PURE__ */ jsx(
171
- PromptRight,
216
+ PromptRightMetrics,
172
217
  {
173
218
  api,
174
219
  sessionID: props.session_id,
@@ -176,7 +221,10 @@ var tui = async (api) => {
176
221
  text: () => {
177
222
  if (!isSessionID(props.session_id)) return "";
178
223
  const status = api.state.session.status(props.session_id);
179
- return renderMetricsText(metrics, props.session_id, { idle: status?.type === "idle" });
224
+ return renderPromptRightMetricsText(metrics, props.session_id, {
225
+ idle: status?.type === "idle",
226
+ metrics: config.promptRightMetrics
227
+ });
180
228
  }
181
229
  }
182
230
  ),
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.8",
4
+ "version": "0.1.10",
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",
@@ -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 };