@rejacky/opencode-insights 0.1.9 → 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-7M32TU5P.js → chunk-O5FFLPEQ.js} +0 -152
- package/dist/{chunk-FGTKNB7T.js → chunk-TGCIHODI.js} +258 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +2 -48
- package/dist/index.js +18 -8
- package/dist/tui.js +29 -11
- 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 };
|
|
@@ -446,12 +300,6 @@ function isRecord(value) {
|
|
|
446
300
|
}
|
|
447
301
|
|
|
448
302
|
export {
|
|
449
|
-
createMetricsState,
|
|
450
|
-
estimateStreamTokens,
|
|
451
|
-
recordAssistantMessage,
|
|
452
|
-
recordAssistantDelta,
|
|
453
|
-
recordToolActivity,
|
|
454
|
-
renderMetricsText,
|
|
455
303
|
createSubagentState,
|
|
456
304
|
applySubagentEvent,
|
|
457
305
|
renderSubagentStatus,
|
|
@@ -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
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 = {
|
|
@@ -112,4 +66,4 @@ declare const _default: {
|
|
|
112
66
|
server: Plugin;
|
|
113
67
|
};
|
|
114
68
|
|
|
115
|
-
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,25 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applySubagentEvent,
|
|
3
|
-
createMetricsState,
|
|
4
3
|
createSubagentState,
|
|
5
|
-
estimateStreamTokens,
|
|
6
4
|
getSubagentItems,
|
|
7
5
|
getSubagentSidebarModel,
|
|
8
6
|
getSubagentSidebarRowAtLine,
|
|
9
7
|
pruneStaleSubagents,
|
|
10
|
-
recordAssistantDelta,
|
|
11
|
-
recordAssistantMessage,
|
|
12
|
-
recordToolActivity,
|
|
13
|
-
renderMetricsText,
|
|
14
8
|
renderSubagentFooter,
|
|
15
9
|
renderSubagentSidebar,
|
|
16
10
|
renderSubagentStatus
|
|
17
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-O5FFLPEQ.js";
|
|
18
12
|
import {
|
|
13
|
+
DEFAULT_PROMPT_RIGHT_METRICS,
|
|
19
14
|
JsonlCaptureStore,
|
|
20
15
|
SqliteCaptureStore,
|
|
21
16
|
createCaptureStore,
|
|
17
|
+
createMetricsState,
|
|
22
18
|
defaultDataDir,
|
|
19
|
+
estimateStreamTokens,
|
|
23
20
|
extractEventType,
|
|
24
21
|
normalizeChatHeadersCapture,
|
|
25
22
|
normalizeChatMessageCapture,
|
|
@@ -29,9 +26,17 @@ import {
|
|
|
29
26
|
normalizeExperimentalChatSystemTransformCapture,
|
|
30
27
|
normalizeToolCapture,
|
|
31
28
|
openDatabase,
|
|
29
|
+
readInsightsConfig,
|
|
30
|
+
recordAssistantDelta,
|
|
31
|
+
recordAssistantMessage,
|
|
32
|
+
recordToolActivity,
|
|
33
|
+
renderMetricsText,
|
|
34
|
+
renderPromptRightMetricsText,
|
|
35
|
+
renderResponseMetricsText,
|
|
32
36
|
resolveCapturePath,
|
|
37
|
+
resolveInsightsConfigPath,
|
|
33
38
|
resolveRetentionDays
|
|
34
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-TGCIHODI.js";
|
|
35
40
|
|
|
36
41
|
// src/cli-shim.ts
|
|
37
42
|
import { existsSync } from "fs";
|
|
@@ -133,6 +138,7 @@ var rootTui = async (...args) => {
|
|
|
133
138
|
var id = "opencode-insights";
|
|
134
139
|
var src_default = { id, server };
|
|
135
140
|
export {
|
|
141
|
+
DEFAULT_PROMPT_RIGHT_METRICS,
|
|
136
142
|
JsonlCaptureStore,
|
|
137
143
|
OpenCodeInsights,
|
|
138
144
|
SqliteCaptureStore,
|
|
@@ -157,14 +163,18 @@ export {
|
|
|
157
163
|
normalizeToolCapture,
|
|
158
164
|
openDatabase,
|
|
159
165
|
pruneStaleSubagents,
|
|
166
|
+
readInsightsConfig,
|
|
160
167
|
recordAssistantDelta,
|
|
161
168
|
recordAssistantMessage,
|
|
162
169
|
recordToolActivity,
|
|
163
170
|
renderMetricsText,
|
|
171
|
+
renderPromptRightMetricsText,
|
|
172
|
+
renderResponseMetricsText,
|
|
164
173
|
renderSubagentFooter,
|
|
165
174
|
renderSubagentSidebar,
|
|
166
175
|
renderSubagentStatus,
|
|
167
176
|
resolveCapturePath,
|
|
177
|
+
resolveInsightsConfigPath,
|
|
168
178
|
resolveRetentionDays,
|
|
169
179
|
server,
|
|
170
180
|
rootTui as tui
|
package/dist/tui.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applySubagentEvent,
|
|
3
|
-
createMetricsState,
|
|
4
3
|
createSubagentState,
|
|
5
4
|
getSubagentSidebarModel,
|
|
6
|
-
getSubagentSidebarRowAtLine
|
|
5
|
+
getSubagentSidebarRowAtLine
|
|
6
|
+
} from "./chunk-O5FFLPEQ.js";
|
|
7
|
+
import {
|
|
8
|
+
createMetricsState,
|
|
9
|
+
readInsightsConfig,
|
|
7
10
|
recordAssistantDelta,
|
|
8
11
|
recordAssistantMessage,
|
|
9
12
|
recordToolActivity,
|
|
10
|
-
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
renderPromptRightMetricsText
|
|
14
|
+
} from "./chunk-TGCIHODI.js";
|
|
12
15
|
|
|
13
16
|
// src/tui.tsx
|
|
14
17
|
import { createTextAttributes, StyledText } from "@opentui/core";
|
|
@@ -17,18 +20,22 @@ import { jsx } from "@opentui/solid/jsx-runtime";
|
|
|
17
20
|
function isSessionID(value) {
|
|
18
21
|
return typeof value === "string" && value.startsWith("ses");
|
|
19
22
|
}
|
|
20
|
-
function
|
|
23
|
+
function PromptRightMetrics(props) {
|
|
21
24
|
let text;
|
|
22
25
|
const sync = () => {
|
|
23
26
|
if (!text) return;
|
|
24
27
|
const content = props.text();
|
|
25
28
|
text.content = content;
|
|
26
29
|
text.visible = content.length > 0;
|
|
27
|
-
text.height = content.length > 0 ?
|
|
30
|
+
text.height = content.length > 0 ? 1 : 0;
|
|
28
31
|
props.api.renderer.requestRender();
|
|
29
32
|
};
|
|
30
33
|
const unsubscribe = props.subscribe(sync);
|
|
31
|
-
|
|
34
|
+
const timer = setInterval(sync, 1e3);
|
|
35
|
+
onCleanup(() => {
|
|
36
|
+
unsubscribe();
|
|
37
|
+
clearInterval(timer);
|
|
38
|
+
});
|
|
32
39
|
return /* @__PURE__ */ jsx(
|
|
33
40
|
"text",
|
|
34
41
|
{
|
|
@@ -37,6 +44,10 @@ function PromptRight(props) {
|
|
|
37
44
|
sync();
|
|
38
45
|
},
|
|
39
46
|
fg: props.api.theme.current.textMuted,
|
|
47
|
+
height: 1,
|
|
48
|
+
wrapMode: "none",
|
|
49
|
+
truncate: true,
|
|
50
|
+
overflow: "hidden",
|
|
40
51
|
children: props.text()
|
|
41
52
|
}
|
|
42
53
|
);
|
|
@@ -135,7 +146,8 @@ function textChunk(text, fg, attributes, bg) {
|
|
|
135
146
|
...bg === void 0 ? {} : { bg }
|
|
136
147
|
};
|
|
137
148
|
}
|
|
138
|
-
var tui = async (api) => {
|
|
149
|
+
var tui = async (api, options) => {
|
|
150
|
+
const config = await readInsightsConfig(options ?? {});
|
|
139
151
|
const metrics = createMetricsState();
|
|
140
152
|
const subagents = createSubagentState();
|
|
141
153
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -164,8 +176,11 @@ var tui = async (api) => {
|
|
|
164
176
|
sessionID: info.sessionID ?? evt.properties.sessionID,
|
|
165
177
|
messageID: info.id,
|
|
166
178
|
createdAt: info.time.created,
|
|
179
|
+
inputTokens: info.tokens.input,
|
|
167
180
|
outputTokens: info.tokens.output,
|
|
168
|
-
reasoningTokens: info.tokens.reasoning
|
|
181
|
+
reasoningTokens: info.tokens.reasoning,
|
|
182
|
+
cacheReadTokens: info.tokens.cache?.read,
|
|
183
|
+
cacheWriteTokens: info.tokens.cache?.write
|
|
169
184
|
};
|
|
170
185
|
if (typeof info.time.completed === "number") messageInput.completedAt = info.time.completed;
|
|
171
186
|
if (typeof info.finish === "string") messageInput.finish = info.finish;
|
|
@@ -198,7 +213,7 @@ var tui = async (api) => {
|
|
|
198
213
|
const offSlots = api.slots.register({
|
|
199
214
|
slots: {
|
|
200
215
|
session_prompt_right: (_ctx, props) => /* @__PURE__ */ jsx(
|
|
201
|
-
|
|
216
|
+
PromptRightMetrics,
|
|
202
217
|
{
|
|
203
218
|
api,
|
|
204
219
|
sessionID: props.session_id,
|
|
@@ -206,7 +221,10 @@ var tui = async (api) => {
|
|
|
206
221
|
text: () => {
|
|
207
222
|
if (!isSessionID(props.session_id)) return "";
|
|
208
223
|
const status = api.state.session.status(props.session_id);
|
|
209
|
-
return
|
|
224
|
+
return renderPromptRightMetricsText(metrics, props.session_id, {
|
|
225
|
+
idle: status?.type === "idle",
|
|
226
|
+
metrics: config.promptRightMetrics
|
|
227
|
+
});
|
|
210
228
|
}
|
|
211
229
|
}
|
|
212
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 };
|