@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 +19 -1
- package/dist/capture-D7z7hS72.d.ts +132 -0
- package/dist/{chunk-36ZNCLI3.js → chunk-O5FFLPEQ.js} +9 -152
- package/dist/{chunk-FGTKNB7T.js → chunk-TGCIHODI.js} +258 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +37 -8
- package/dist/index.d.ts +3 -48
- package/dist/index.js +20 -8
- package/dist/tui.js +66 -18
- package/package.json +1 -1
- package/dist/capture-BMWWI5GR.d.ts +0 -61
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
|
-
-
|
|
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
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
openDatabase,
|
|
4
4
|
resolveCapturePath
|
|
5
|
-
} from "./chunk-
|
|
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 `
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
371
|
-
select id
|
|
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,
|
|
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 {
|
|
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-
|
|
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-
|
|
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
|
-
|
|
10
|
-
} from "./chunk-
|
|
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
|
|
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 ?
|
|
30
|
+
text.height = content.length > 0 ? 1 : 0;
|
|
27
31
|
props.api.renderer.requestRender();
|
|
28
32
|
};
|
|
29
33
|
const unsubscribe = props.subscribe(sync);
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 };
|