@rejacky/opencode-insights 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -13
- package/dist/{capture-0knBP9m2.d.ts → capture-An0IKDqr.d.ts} +28 -1
- package/dist/{chunk-IXXHI7ZM.js → chunk-2HZVJIOC.js} +122 -15
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/tui.js +183 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,13 +94,14 @@ The `uninstall` command removes plugin config entries and local Insights data; i
|
|
|
94
94
|
- Configurable live metrics in the OpenCode session prompt zone.
|
|
95
95
|
- A collapsible session-wide `Token Usage` sidebar showing total tokens, response count, input/output/reasoning usage, cache read/write usage, and aggregate cache rate. It loads completed responses already present in the session and continues updating live.
|
|
96
96
|
- An opt-in `Go Usage` sidebar showing OpenCode Go rolling/weekly/monthly usage limits for sessions that use the `opencode-go` provider.
|
|
97
|
+
- An opt-in `Copilot Usage` sidebar showing GitHub Copilot premium-interaction quota, usage bar, and days until reset for sessions that use the `github-copilot` provider.
|
|
97
98
|
- Subagent status (running, done, failed, elapsed time, and token/context usage) in the sidebar.
|
|
98
99
|
- A collapsible `Session Analysis` sidebar showing the active session's aggregated activity (tool calls, skills, auto-compactions, warnings, model requests, subagent tree). Click it to open a detail dialog; its vertical scrollbar appears only when the content overflows the dialog.
|
|
99
100
|
- Local capture of OpenCode hook/event data without redaction.
|
|
100
101
|
- A local web viewer for reconstructed sessions, user turns, hidden request context, system/messages transforms, and assistant thinking/response sequences.
|
|
101
102
|
- Native OpenCode footer components (project directory and version) remain visible — the plugin does not override `sidebar_footer` or `home_prompt_right` slots.
|
|
102
103
|
|
|
103
|
-
The right sidebar contains the plugin sections: `Token Usage`, `Go Usage` (when enabled and the session uses `opencode-go`), `Subagents`, and `Session Analysis`. Click any section header to collapse or expand it. Token usage is aggregated across the full session; prompt-right `used` and `cache` values continue to represent the latest completed assistant response.
|
|
104
|
+
The right sidebar contains the plugin sections: `Token Usage`, `Go Usage` (when enabled and the session uses `opencode-go`), `Copilot Usage` (when enabled and the session uses `github-copilot`), `Subagents`, and `Session Analysis`. Click any section header to collapse or expand it. Token usage is aggregated across the full session; prompt-right `used` and `cache` values continue to represent the latest completed assistant response.
|
|
104
105
|
|
|
105
106
|
## TUI Metrics Configuration
|
|
106
107
|
|
|
@@ -120,27 +121,67 @@ With a custom database path, the configuration file is created in that database'
|
|
|
120
121
|
|
|
121
122
|
`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.
|
|
122
123
|
|
|
123
|
-
##
|
|
124
|
+
## Provider Usage Configuration
|
|
124
125
|
|
|
125
|
-
The
|
|
126
|
+
The plugin can show usage sidebar sections for the AI provider used by the current session. Each section is opt-in, disabled by default, and only appears when the session uses the matching provider.
|
|
126
127
|
|
|
127
|
-
```
|
|
128
|
+
```jsonc
|
|
128
129
|
{
|
|
130
|
+
"promptRightMetrics": ["tps", "avg", "used", "cache"],
|
|
129
131
|
"goUsage": {
|
|
130
|
-
"enabled":
|
|
131
|
-
"cookie": "
|
|
132
|
-
"workspaceID": "
|
|
132
|
+
"enabled": false,
|
|
133
|
+
"cookie": "",
|
|
134
|
+
"workspaceID": "",
|
|
135
|
+
"refreshMs": 300000
|
|
136
|
+
},
|
|
137
|
+
"copilotUsage": {
|
|
138
|
+
"enabled": false,
|
|
139
|
+
"token": "",
|
|
133
140
|
"refreshMs": 300000
|
|
134
141
|
}
|
|
135
142
|
}
|
|
136
143
|
```
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
### Go Usage
|
|
146
|
+
|
|
147
|
+
Shows rolling (5 hour), weekly, and monthly usage limits for OpenCode Go subscriptions. Only visible when the session uses the `opencode-go` provider.
|
|
148
|
+
|
|
149
|
+
```jsonc
|
|
150
|
+
"goUsage": {
|
|
151
|
+
"enabled": true, // set to true to activate
|
|
152
|
+
"cookie": "Fe26.2**...", // auth session cookie from opencode.ai
|
|
153
|
+
"workspaceID": "wrk_...", // visible in the console URL
|
|
154
|
+
"refreshMs": 300000 // poll interval (min 60000)
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
To get the cookie, log in to `https://opencode.ai`, open the workspace `/go` page, then copy the `auth` cookie value from your browser's DevTools (Application → Cookies → `https://opencode.ai`). The cookie lasts up to a year; if the section shows an error, copy it again.
|
|
159
|
+
|
|
160
|
+
### Copilot Usage
|
|
161
|
+
|
|
162
|
+
Shows GitHub Copilot premium-interaction quota (used/total, progress bar, days until reset). Only visible when the session uses the `github-copilot` provider.
|
|
163
|
+
|
|
164
|
+
```jsonc
|
|
165
|
+
"copilotUsage": {
|
|
166
|
+
"enabled": true, // set to true to activate
|
|
167
|
+
"token": "", // optional: manual token override
|
|
168
|
+
"refreshMs": 300000 // poll interval (min 60000)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The `token` field is optional. If empty, the plugin reads the token from OpenCode's auth store (`~/.local/share/opencode/auth.json` → `github-copilot.access`). No manual setup is needed if you authenticate with Copilot through OpenCode.
|
|
173
|
+
|
|
174
|
+
The section displays:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
▼ Copilot
|
|
178
|
+
Premium 84% ████████░░ 7d
|
|
179
|
+
2942 / 3500
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
When the quota is exhausted and overage is permitted, the bar fills to 100% and the percentage reflects actual usage.
|
|
142
183
|
|
|
143
|
-
|
|
184
|
+
Restart OpenCode after editing this file.
|
|
144
185
|
|
|
145
186
|
## Open The Viewer
|
|
146
187
|
|
|
@@ -254,7 +295,8 @@ comments allowed). The plugin creates it on first run with `dbPath` commented ou
|
|
|
254
295
|
// "dbPath": "/absolute/path/to/insights.sqlite",
|
|
255
296
|
"retentionDays": 1,
|
|
256
297
|
"promptRightMetrics": ["tps", "avg", "used", "cache"],
|
|
257
|
-
"goUsage": { "enabled": false, "cookie": "", "workspaceID": "", "refreshMs": 300000 }
|
|
298
|
+
"goUsage": { "enabled": false, "cookie": "", "workspaceID": "", "refreshMs": 300000 },
|
|
299
|
+
"copilotUsage": { "enabled": false, "token": "", "refreshMs": 300000 }
|
|
258
300
|
}
|
|
259
301
|
```
|
|
260
302
|
|
|
@@ -16,6 +16,19 @@ type SessionAverage = {
|
|
|
16
16
|
totalTtftMs: number;
|
|
17
17
|
messageCount: number;
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Per-message metrics for input-box (current round).
|
|
21
|
+
* AVG = totalTokens(=output+reasoning)/duration where duration = firstTokenAt→endAt (fallback completed-created if hydrated).
|
|
22
|
+
* TTFT = firstTokenAt - requestStartAt. TPS = live estimated tokens / active burst duration (5s window) for this messageID only.
|
|
23
|
+
* Session totals (Token Usage sidebar) remain in SessionTokenUsage/sessionAverageByID (deprecated for prompt-right).
|
|
24
|
+
*/
|
|
25
|
+
type MessageMetrics = {
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
ttftMs: number | undefined;
|
|
29
|
+
createdAt: number;
|
|
30
|
+
completedAt: number;
|
|
31
|
+
};
|
|
19
32
|
type AssistantResponseUsage = {
|
|
20
33
|
inputTokens?: number | undefined;
|
|
21
34
|
outputTokens?: number | undefined;
|
|
@@ -37,7 +50,10 @@ type PromptRightMetric = "tps" | "avg" | "ttft" | "used" | "cache" | "input" | "
|
|
|
37
50
|
declare const DEFAULT_PROMPT_RIGHT_METRICS: PromptRightMetric[];
|
|
38
51
|
type MetricsState = {
|
|
39
52
|
streamSamplesBySession: Record<string, StreamSample[]>;
|
|
53
|
+
streamSamplesByMessageID: Record<string, StreamSample[]>;
|
|
40
54
|
messageTimingByID: Record<string, MessageTiming>;
|
|
55
|
+
messageMetricsByID: Record<string, MessageMetrics>;
|
|
56
|
+
latestMessageIDBySession: Record<string, string>;
|
|
41
57
|
sessionAverageByID: Record<string, SessionAverage>;
|
|
42
58
|
latestResponseUsageBySession: Record<string, AssistantResponseUsage>;
|
|
43
59
|
responseUsageByMessageID: Record<string, {
|
|
@@ -47,6 +63,10 @@ type MetricsState = {
|
|
|
47
63
|
sessionTokenUsageByID: Record<string, SessionTokenUsage>;
|
|
48
64
|
};
|
|
49
65
|
declare function createMetricsState(): MetricsState;
|
|
66
|
+
/**
|
|
67
|
+
* Estimated tokens for live TPS: ceil(bytes/5). Differs from per-message AVG which uses real output+reasoning tokens.
|
|
68
|
+
* Multibyte (CJK/emoji) inflates bytes but may over/undercount vs real tokenizer.
|
|
69
|
+
*/
|
|
50
70
|
declare function estimateStreamTokens(delta: string): number;
|
|
51
71
|
declare function recordAssistantMessage(state: MetricsState, input: {
|
|
52
72
|
sessionID: string;
|
|
@@ -107,9 +127,15 @@ type GoUsageConfig = {
|
|
|
107
127
|
workspaceID: string;
|
|
108
128
|
refreshMs: number;
|
|
109
129
|
};
|
|
130
|
+
type CopilotUsageConfig = {
|
|
131
|
+
enabled: boolean;
|
|
132
|
+
token: string;
|
|
133
|
+
refreshMs: number;
|
|
134
|
+
};
|
|
110
135
|
type InsightsConfig = {
|
|
111
136
|
promptRightMetrics: PromptRightMetric[];
|
|
112
137
|
goUsage: GoUsageConfig;
|
|
138
|
+
copilotUsage: CopilotUsageConfig;
|
|
113
139
|
dbPath?: string | undefined;
|
|
114
140
|
retentionDays?: number | undefined;
|
|
115
141
|
};
|
|
@@ -119,6 +145,7 @@ declare function resolveInsightsConfigPath(options?: InsightsOptions): string;
|
|
|
119
145
|
declare function resolveLegacyInsightsConfigPath(options?: InsightsOptions): string;
|
|
120
146
|
declare function readInsightsConfig(options?: InsightsOptions): Promise<InsightsConfig>;
|
|
121
147
|
declare function insightsOptionsFromConfig(config: InsightsConfig, dataDir?: string): InsightsOptions;
|
|
148
|
+
declare function resolveCopilotToken(config: CopilotUsageConfig): string;
|
|
122
149
|
declare function normalizeChatMessageCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
|
|
123
150
|
declare function normalizeChatParamsCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
|
|
124
151
|
declare function normalizeChatHeadersCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
|
|
@@ -156,4 +183,4 @@ declare class SqliteCaptureStore implements CaptureStore {
|
|
|
156
183
|
declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
|
|
157
184
|
declare function resolveRetentionDays(value: unknown): number;
|
|
158
185
|
|
|
159
|
-
export { type AssistantResponseUsage as A,
|
|
186
|
+
export { type AssistantResponseUsage as A, recordAssistantDelta as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, recordAssistantMessage as E, recordToolActivity as F, type GoUsageConfig as G, renderMetricsText as H, type InsightsConfig as I, JsonlCaptureStore as J, renderPromptRightMetricsText as K, renderResponseMetricsText as L, type MessageMetrics as M, renderSessionTokenUsage as N, resolveCapturePath as O, type PromptRightMetric as P, resolveCopilotToken as Q, resolveInsightsConfigPath as R, type SessionAverage as S, resolveLegacyInsightsConfigPath as T, resolveRetentionDays as U, type CaptureKind as a, type CaptureStore as b, type CopilotUsageConfig as c, type InsightsOptions as d, type MessageTiming as e, type MetricsState as f, type SessionTokenUsage as g, SqliteCaptureStore as h, type SqliteDb as i, type StreamSample as j, createCaptureStore as k, createMetricsState as l, defaultDataDir as m, estimateStreamTokens as n, extractEventType as o, getSessionTokenUsage as p, insightsOptionsFromConfig as q, normalizeChatHeadersCapture as r, normalizeChatMessageCapture as s, normalizeChatParamsCapture as t, normalizeEventCapture as u, normalizeExperimentalChatMessagesTransformCapture as v, normalizeExperimentalChatSystemTransformCapture as w, normalizeToolCapture as x, openDatabase as y, readInsightsConfig as z };
|
|
@@ -6,7 +6,10 @@ var SINGLE_SAMPLE_MS = 1e3;
|
|
|
6
6
|
function createMetricsState() {
|
|
7
7
|
return {
|
|
8
8
|
streamSamplesBySession: {},
|
|
9
|
+
streamSamplesByMessageID: {},
|
|
9
10
|
messageTimingByID: {},
|
|
11
|
+
messageMetricsByID: {},
|
|
12
|
+
latestMessageIDBySession: {},
|
|
10
13
|
sessionAverageByID: {},
|
|
11
14
|
latestResponseUsageBySession: {},
|
|
12
15
|
responseUsageByMessageID: {},
|
|
@@ -27,6 +30,7 @@ function recordAssistantMessage(state, input) {
|
|
|
27
30
|
lastTokenAt: existing?.lastTokenAt,
|
|
28
31
|
lastToolCallAt: existing?.lastToolCallAt
|
|
29
32
|
};
|
|
33
|
+
state.latestMessageIDBySession[input.sessionID] = input.messageID;
|
|
30
34
|
return;
|
|
31
35
|
}
|
|
32
36
|
const usage = compactUsage({
|
|
@@ -42,13 +46,33 @@ function recordAssistantMessage(state, input) {
|
|
|
42
46
|
state.responseUsageByMessageID[input.messageID] = { sessionID: input.sessionID, usage };
|
|
43
47
|
rebuildSessionTokenUsage(state, input.sessionID);
|
|
44
48
|
}
|
|
49
|
+
state.latestMessageIDBySession[input.sessionID] = input.messageID;
|
|
45
50
|
const timing = state.messageTimingByID[input.messageID];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
51
|
+
const totalTokens = (input.outputTokens ?? 0) + (input.reasoningTokens ?? 0);
|
|
52
|
+
let durationMs;
|
|
53
|
+
let ttftMs;
|
|
54
|
+
if (timing?.sessionID === input.sessionID && typeof (timing.firstTokenAt ?? timing.firstResponseAt) === "number") {
|
|
55
|
+
const first = timing.firstTokenAt ?? timing.firstResponseAt;
|
|
56
|
+
const endAt = input.finish === "tool-calls" ? timing.lastToolCallAt ?? input.completedAt : input.completedAt;
|
|
57
|
+
durationMs = typeof endAt === "number" ? Math.max(endAt - first, 1) : void 0;
|
|
58
|
+
ttftMs = Math.max(first - timing.requestStartAt, 0);
|
|
59
|
+
} else {
|
|
60
|
+
durationMs = Math.max(input.completedAt - input.createdAt, 1);
|
|
61
|
+
ttftMs = void 0;
|
|
62
|
+
}
|
|
63
|
+
if (totalTokens > 0 && durationMs) {
|
|
64
|
+
state.messageMetricsByID[input.messageID] = {
|
|
65
|
+
totalTokens,
|
|
66
|
+
durationMs,
|
|
67
|
+
ttftMs,
|
|
68
|
+
createdAt: input.createdAt,
|
|
69
|
+
completedAt: input.completedAt
|
|
70
|
+
};
|
|
71
|
+
if (ttftMs !== void 0) {
|
|
72
|
+
const firstForSession = timing?.firstTokenAt ?? timing?.firstResponseAt;
|
|
73
|
+
const endForSession = timing ? input.finish === "tool-calls" ? timing.lastToolCallAt ?? input.completedAt : input.completedAt : input.completedAt;
|
|
74
|
+
const sessionDuration = timing && firstForSession && typeof endForSession === "number" ? Math.max(endForSession - firstForSession, 1) : durationMs;
|
|
75
|
+
const sessionTtft2 = ttftMs;
|
|
52
76
|
const totals = state.sessionAverageByID[input.sessionID] ?? {
|
|
53
77
|
totalTokens: 0,
|
|
54
78
|
totalDurationMs: 0,
|
|
@@ -57,13 +81,27 @@ function recordAssistantMessage(state, input) {
|
|
|
57
81
|
};
|
|
58
82
|
state.sessionAverageByID[input.sessionID] = {
|
|
59
83
|
totalTokens: totals.totalTokens + totalTokens,
|
|
60
|
-
totalDurationMs: totals.totalDurationMs +
|
|
61
|
-
totalTtftMs: totals.totalTtftMs +
|
|
84
|
+
totalDurationMs: totals.totalDurationMs + sessionDuration,
|
|
85
|
+
totalTtftMs: totals.totalTtftMs + sessionTtft2,
|
|
62
86
|
messageCount: totals.messageCount + 1
|
|
63
87
|
};
|
|
88
|
+
} else {
|
|
89
|
+
const totals = state.sessionAverageByID[input.sessionID] ?? {
|
|
90
|
+
totalTokens: 0,
|
|
91
|
+
totalDurationMs: 0,
|
|
92
|
+
totalTtftMs: 0,
|
|
93
|
+
messageCount: 0
|
|
94
|
+
};
|
|
95
|
+
state.sessionAverageByID[input.sessionID] = {
|
|
96
|
+
totalTokens: totals.totalTokens + totalTokens,
|
|
97
|
+
totalDurationMs: totals.totalDurationMs + durationMs,
|
|
98
|
+
totalTtftMs: totals.totalTtftMs,
|
|
99
|
+
messageCount: totals.messageCount
|
|
100
|
+
};
|
|
64
101
|
}
|
|
65
102
|
}
|
|
66
103
|
delete state.messageTimingByID[input.messageID];
|
|
104
|
+
delete state.streamSamplesByMessageID[input.messageID];
|
|
67
105
|
pruneSamples(state, input.completedAt);
|
|
68
106
|
}
|
|
69
107
|
function recordAssistantDelta(state, input) {
|
|
@@ -71,10 +109,15 @@ function recordAssistantDelta(state, input) {
|
|
|
71
109
|
at: input.at,
|
|
72
110
|
tokens: estimateStreamTokens(input.delta)
|
|
73
111
|
};
|
|
112
|
+
state.streamSamplesByMessageID[input.messageID] = [
|
|
113
|
+
...(state.streamSamplesByMessageID[input.messageID] ?? []).filter((item) => input.at - item.at <= STREAM_WINDOW_MS),
|
|
114
|
+
sample
|
|
115
|
+
];
|
|
74
116
|
state.streamSamplesBySession[input.sessionID] = [
|
|
75
117
|
...(state.streamSamplesBySession[input.sessionID] ?? []).filter((item) => input.at - item.at <= STREAM_WINDOW_MS),
|
|
76
118
|
sample
|
|
77
119
|
];
|
|
120
|
+
state.latestMessageIDBySession[input.sessionID] = input.messageID;
|
|
78
121
|
const timing = state.messageTimingByID[input.messageID];
|
|
79
122
|
if (timing) {
|
|
80
123
|
state.messageTimingByID[input.messageID] = timing.firstTokenAt ? { ...timing, lastTokenAt: input.at } : {
|
|
@@ -86,6 +129,9 @@ function recordAssistantDelta(state, input) {
|
|
|
86
129
|
}
|
|
87
130
|
}
|
|
88
131
|
function recordToolActivity(state, sessionID, messageID, at = Date.now()) {
|
|
132
|
+
if (state.streamSamplesByMessageID[messageID]?.length) {
|
|
133
|
+
delete state.streamSamplesByMessageID[messageID];
|
|
134
|
+
}
|
|
89
135
|
if (state.streamSamplesBySession[sessionID]?.length) {
|
|
90
136
|
delete state.streamSamplesBySession[sessionID];
|
|
91
137
|
}
|
|
@@ -100,8 +146,10 @@ function recordToolActivity(state, sessionID, messageID, at = Date.now()) {
|
|
|
100
146
|
}
|
|
101
147
|
function renderMetricsText(state, sessionID, options = {}) {
|
|
102
148
|
const live = liveTps(state, sessionID, options) ?? "-";
|
|
103
|
-
const
|
|
104
|
-
const
|
|
149
|
+
const msgID = state.latestMessageIDBySession[sessionID];
|
|
150
|
+
const hasPerMessage = !!msgID && !!state.messageMetricsByID[msgID];
|
|
151
|
+
const avg = hasPerMessage ? messageAverage(state, sessionID) ?? "-" : sessionAverage(state, sessionID) ?? "-";
|
|
152
|
+
const ttft = hasPerMessage ? messageTtft(state, sessionID) ?? "-" : sessionTtft(state, sessionID) ?? "-";
|
|
105
153
|
return `TPS ${live} | AVG ${avg} | TTFT ${ttft}`;
|
|
106
154
|
}
|
|
107
155
|
function renderResponseMetricsText(state, sessionID) {
|
|
@@ -121,10 +169,14 @@ function renderPromptRightMetricsText(state, sessionID, options = {}) {
|
|
|
121
169
|
const usage = state.latestResponseUsageBySession[sessionID];
|
|
122
170
|
const used = usage ? sumTokens(usage) : void 0;
|
|
123
171
|
const cacheRate = usage ? cacheReadRate(usage) : void 0;
|
|
172
|
+
const msgID = state.latestMessageIDBySession[sessionID];
|
|
173
|
+
const hasPerMessage = !!msgID && !!state.messageMetricsByID[msgID];
|
|
174
|
+
const avgVal = hasPerMessage ? messageAverage(state, sessionID) : sessionAverage(state, sessionID);
|
|
175
|
+
const ttftVal = hasPerMessage ? messageTtft(state, sessionID) : sessionTtft(state, sessionID);
|
|
124
176
|
const values = {
|
|
125
177
|
tps: `TPS ${liveTps(state, sessionID, options) ?? "-"}`,
|
|
126
|
-
avg: `AVG ${
|
|
127
|
-
ttft: `TTFT ${
|
|
178
|
+
avg: `AVG ${avgVal ?? "-"}`,
|
|
179
|
+
ttft: `TTFT ${ttftVal ?? "-"}`,
|
|
128
180
|
used: `${used === void 0 ? "-" : formatTokenCount(used)} used`,
|
|
129
181
|
cache: `${cacheRate === void 0 ? "-" : formatPercent(cacheRate)} cache`,
|
|
130
182
|
input: `${usage?.inputTokens === void 0 ? "-" : formatTokenCount(usage.inputTokens)} in`,
|
|
@@ -160,6 +212,11 @@ function renderSessionTokenUsage(state, sessionID, subagentTokens = 0) {
|
|
|
160
212
|
return lines.join("\n");
|
|
161
213
|
}
|
|
162
214
|
function pruneSamples(state, now = Date.now()) {
|
|
215
|
+
for (const [messageID, samples] of Object.entries(state.streamSamplesByMessageID)) {
|
|
216
|
+
const next = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
|
|
217
|
+
if (next.length > 0) state.streamSamplesByMessageID[messageID] = next;
|
|
218
|
+
else delete state.streamSamplesByMessageID[messageID];
|
|
219
|
+
}
|
|
163
220
|
for (const [sessionID, samples] of Object.entries(state.streamSamplesBySession)) {
|
|
164
221
|
const next = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
|
|
165
222
|
if (next.length > 0) state.streamSamplesBySession[sessionID] = next;
|
|
@@ -188,6 +245,20 @@ function rebuildSessionTokenUsage(state, sessionID) {
|
|
|
188
245
|
usage.totalTokens = usage.inputTokens + usage.outputTokens + usage.reasoningTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
189
246
|
state.sessionTokenUsageByID[sessionID] = usage;
|
|
190
247
|
}
|
|
248
|
+
function messageAverage(state, sessionID) {
|
|
249
|
+
const msgID = state.latestMessageIDBySession[sessionID];
|
|
250
|
+
if (!msgID) return void 0;
|
|
251
|
+
const m = state.messageMetricsByID[msgID];
|
|
252
|
+
if (!m || m.totalTokens <= 0 || m.durationMs <= 0) return void 0;
|
|
253
|
+
return formatRate(m.totalTokens / (m.durationMs / 1e3), "AVG");
|
|
254
|
+
}
|
|
255
|
+
function messageTtft(state, sessionID) {
|
|
256
|
+
const msgID = state.latestMessageIDBySession[sessionID];
|
|
257
|
+
if (!msgID) return void 0;
|
|
258
|
+
const m = state.messageMetricsByID[msgID];
|
|
259
|
+
if (!m || m.ttftMs === void 0 || m.ttftMs < 0) return void 0;
|
|
260
|
+
return formatTtft(m.ttftMs / 1e3);
|
|
261
|
+
}
|
|
191
262
|
function sessionAverage(state, sessionID) {
|
|
192
263
|
const totals = state.sessionAverageByID[sessionID];
|
|
193
264
|
if (!totals || totals.totalTokens <= 0 || totals.totalDurationMs <= 0) return void 0;
|
|
@@ -201,7 +272,8 @@ function sessionTtft(state, sessionID) {
|
|
|
201
272
|
function liveTps(state, sessionID, options = {}) {
|
|
202
273
|
if (options.idle) return void 0;
|
|
203
274
|
const now = options.now ?? Date.now();
|
|
204
|
-
const
|
|
275
|
+
const msgID = state.latestMessageIDBySession[sessionID];
|
|
276
|
+
const samples = (msgID ? state.streamSamplesByMessageID[msgID] : void 0) ?? state.streamSamplesBySession[sessionID] ?? [];
|
|
205
277
|
const relevant = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
|
|
206
278
|
if (relevant.length === 0) return void 0;
|
|
207
279
|
const lastSample = relevant.at(-1);
|
|
@@ -277,7 +349,9 @@ import { parse } from "jsonc-parser";
|
|
|
277
349
|
var DEFAULT_RETENTION_DAYS = 1;
|
|
278
350
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
279
351
|
var DEFAULT_GO_USAGE_REFRESH_MS = 3e5;
|
|
352
|
+
var DEFAULT_COPILOT_USAGE_REFRESH_MS = 3e5;
|
|
280
353
|
var MIN_GO_USAGE_REFRESH_MS = 6e4;
|
|
354
|
+
var MIN_COPILOT_USAGE_REFRESH_MS = 6e4;
|
|
281
355
|
var sequence = 0;
|
|
282
356
|
function nextID(timestamp) {
|
|
283
357
|
sequence += 1;
|
|
@@ -377,7 +451,8 @@ function defaultInsightsConfigJsonc() {
|
|
|
377
451
|
' // "dbPath": "/absolute/path/to/insights.sqlite",',
|
|
378
452
|
` "retentionDays": ${DEFAULT_RETENTION_DAYS},`,
|
|
379
453
|
` "promptRightMetrics": ${JSON.stringify(DEFAULT_PROMPT_RIGHT_METRICS)},`,
|
|
380
|
-
` "goUsage": ${JSON.stringify(defaultGoUsageConfig())}
|
|
454
|
+
` "goUsage": ${JSON.stringify(defaultGoUsageConfig())},`,
|
|
455
|
+
` "copilotUsage": ${JSON.stringify(defaultCopilotUsageConfig())}`,
|
|
381
456
|
"}",
|
|
382
457
|
""
|
|
383
458
|
].join("\n");
|
|
@@ -386,11 +461,18 @@ function insightsOptionsFromConfig(config, dataDir) {
|
|
|
386
461
|
return compactUndefined({ dataDir, dbPath: config.dbPath, retentionDays: config.retentionDays });
|
|
387
462
|
}
|
|
388
463
|
function defaultInsightsConfig() {
|
|
389
|
-
return {
|
|
464
|
+
return {
|
|
465
|
+
promptRightMetrics: [...DEFAULT_PROMPT_RIGHT_METRICS],
|
|
466
|
+
goUsage: defaultGoUsageConfig(),
|
|
467
|
+
copilotUsage: defaultCopilotUsageConfig()
|
|
468
|
+
};
|
|
390
469
|
}
|
|
391
470
|
function defaultGoUsageConfig() {
|
|
392
471
|
return { enabled: false, cookie: "", workspaceID: "", refreshMs: DEFAULT_GO_USAGE_REFRESH_MS };
|
|
393
472
|
}
|
|
473
|
+
function defaultCopilotUsageConfig() {
|
|
474
|
+
return { enabled: false, token: "", refreshMs: DEFAULT_COPILOT_USAGE_REFRESH_MS };
|
|
475
|
+
}
|
|
394
476
|
function insightsConfigFrom(value) {
|
|
395
477
|
const record = isRecord(value) ? value : {};
|
|
396
478
|
const metrics = Array.isArray(record.promptRightMetrics) ? record.promptRightMetrics.filter(isPromptRightMetric) : [];
|
|
@@ -400,6 +482,7 @@ function insightsConfigFrom(value) {
|
|
|
400
482
|
return {
|
|
401
483
|
promptRightMetrics: metrics.length ? metrics : [...DEFAULT_PROMPT_RIGHT_METRICS],
|
|
402
484
|
goUsage: goUsageConfigFrom(record.goUsage),
|
|
485
|
+
copilotUsage: copilotUsageConfigFrom(record.copilotUsage),
|
|
403
486
|
dbPath,
|
|
404
487
|
retentionDays
|
|
405
488
|
};
|
|
@@ -412,9 +495,32 @@ function goUsageConfigFrom(value) {
|
|
|
412
495
|
const refreshMs = typeof record.refreshMs === "number" && Number.isFinite(record.refreshMs) ? Math.max(MIN_GO_USAGE_REFRESH_MS, Math.trunc(record.refreshMs)) : DEFAULT_GO_USAGE_REFRESH_MS;
|
|
413
496
|
return { enabled, cookie, workspaceID, refreshMs };
|
|
414
497
|
}
|
|
498
|
+
function copilotUsageConfigFrom(value) {
|
|
499
|
+
const record = isRecord(value) ? value : {};
|
|
500
|
+
const enabled = record.enabled === true;
|
|
501
|
+
const token = typeof record.token === "string" && record.token.length > 0 ? record.token : "";
|
|
502
|
+
const refreshMs = typeof record.refreshMs === "number" && Number.isFinite(record.refreshMs) ? Math.max(MIN_COPILOT_USAGE_REFRESH_MS, Math.trunc(record.refreshMs)) : DEFAULT_COPILOT_USAGE_REFRESH_MS;
|
|
503
|
+
return { enabled, token, refreshMs };
|
|
504
|
+
}
|
|
415
505
|
function isPromptRightMetric(value) {
|
|
416
506
|
return value === "tps" || value === "avg" || value === "ttft" || value === "used" || value === "cache" || value === "input" || value === "output" || value === "reasoning";
|
|
417
507
|
}
|
|
508
|
+
function resolveCopilotToken(config) {
|
|
509
|
+
if (config.token.length > 0) return config.token;
|
|
510
|
+
try {
|
|
511
|
+
const authPath = join(homedir(), ".local/share/opencode/auth.json");
|
|
512
|
+
if (!existsSync(authPath)) return "";
|
|
513
|
+
const raw = readFileSync(authPath, "utf8");
|
|
514
|
+
const auth = JSON.parse(raw);
|
|
515
|
+
const copilot = isRecord(auth?.["github-copilot"]) ? auth["github-copilot"] : void 0;
|
|
516
|
+
if (!copilot) return "";
|
|
517
|
+
const access = typeof copilot.access === "string" ? copilot.access : "";
|
|
518
|
+
const refresh = typeof copilot.refresh === "string" ? copilot.refresh : "";
|
|
519
|
+
return access || refresh;
|
|
520
|
+
} catch {
|
|
521
|
+
return "";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
418
524
|
function normalizeChatMessageCapture(input, output, timestamp = Date.now()) {
|
|
419
525
|
const inputRecord = isRecord(input) ? input : {};
|
|
420
526
|
const model = isRecord(inputRecord.model) ? inputRecord.model : void 0;
|
|
@@ -754,6 +860,7 @@ export {
|
|
|
754
860
|
resolveLegacyInsightsConfigPath,
|
|
755
861
|
readInsightsConfig,
|
|
756
862
|
insightsOptionsFromConfig,
|
|
863
|
+
resolveCopilotToken,
|
|
757
864
|
normalizeChatMessageCapture,
|
|
758
865
|
normalizeChatParamsCapture,
|
|
759
866
|
normalizeChatHeadersCapture,
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from '@opencode-ai/plugin';
|
|
2
2
|
import { TuiPlugin } from '@opencode-ai/plugin/tui';
|
|
3
|
-
export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, D as DEFAULT_PROMPT_RIGHT_METRICS, G as GoUsageConfig, I as InsightsConfig,
|
|
3
|
+
export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, c as CopilotUsageConfig, D as DEFAULT_PROMPT_RIGHT_METRICS, G as GoUsageConfig, I as InsightsConfig, d as InsightsOptions, J as JsonlCaptureStore, M as MessageMetrics, e as MessageTiming, f as MetricsState, P as PromptRightMetric, S as SessionAverage, g as SessionTokenUsage, h as SqliteCaptureStore, i as SqliteDb, j as StreamSample, k as createCaptureStore, l as createMetricsState, m as defaultDataDir, n as estimateStreamTokens, o as extractEventType, p as getSessionTokenUsage, q as insightsOptionsFromConfig, r as normalizeChatHeadersCapture, s as normalizeChatMessageCapture, t as normalizeChatParamsCapture, u as normalizeEventCapture, v as normalizeExperimentalChatMessagesTransformCapture, w as normalizeExperimentalChatSystemTransformCapture, x as normalizeToolCapture, y as openDatabase, z as readInsightsConfig, B as recordAssistantDelta, E as recordAssistantMessage, F as recordToolActivity, H as renderMetricsText, K as renderPromptRightMetricsText, L as renderResponseMetricsText, N as renderSessionTokenUsage, O as resolveCapturePath, Q as resolveCopilotToken, R as resolveInsightsConfigPath, T as resolveLegacyInsightsConfigPath, U as resolveRetentionDays } from './capture-An0IKDqr.js';
|
|
4
4
|
|
|
5
5
|
type SessionActivity = {
|
|
6
6
|
toolCalls: number;
|
package/dist/index.js
CHANGED
|
@@ -39,10 +39,11 @@ import {
|
|
|
39
39
|
renderResponseMetricsText,
|
|
40
40
|
renderSessionTokenUsage,
|
|
41
41
|
resolveCapturePath,
|
|
42
|
+
resolveCopilotToken,
|
|
42
43
|
resolveInsightsConfigPath,
|
|
43
44
|
resolveLegacyInsightsConfigPath,
|
|
44
45
|
resolveRetentionDays
|
|
45
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-2HZVJIOC.js";
|
|
46
47
|
|
|
47
48
|
// src/cli-shim.ts
|
|
48
49
|
import { existsSync } from "fs";
|
|
@@ -185,6 +186,7 @@ export {
|
|
|
185
186
|
renderSubagentSidebar,
|
|
186
187
|
renderSubagentStatus,
|
|
187
188
|
resolveCapturePath,
|
|
189
|
+
resolveCopilotToken,
|
|
188
190
|
resolveInsightsConfigPath,
|
|
189
191
|
resolveLegacyInsightsConfigPath,
|
|
190
192
|
resolveRetentionDays,
|
package/dist/tui.js
CHANGED
|
@@ -22,8 +22,9 @@ import {
|
|
|
22
22
|
recordAssistantMessage,
|
|
23
23
|
recordToolActivity,
|
|
24
24
|
renderPromptRightMetricsText,
|
|
25
|
-
renderSessionTokenUsage
|
|
26
|
-
|
|
25
|
+
renderSessionTokenUsage,
|
|
26
|
+
resolveCopilotToken
|
|
27
|
+
} from "./chunk-2HZVJIOC.js";
|
|
27
28
|
|
|
28
29
|
// src/tui.tsx
|
|
29
30
|
import { createTextAttributes, StyledText } from "@opentui/core";
|
|
@@ -109,7 +110,8 @@ function formatUsageBar(usagePercent, width = 10) {
|
|
|
109
110
|
}
|
|
110
111
|
function formatGoUsageRow(row) {
|
|
111
112
|
const bar = formatUsageBar(row.usagePercent);
|
|
112
|
-
|
|
113
|
+
const pct = `${Math.round(row.usagePercent)}%`;
|
|
114
|
+
return `${row.label.padEnd(9)}${pct.padEnd(4)} ${bar} ${row.reset}`;
|
|
113
115
|
}
|
|
114
116
|
function goUsageSectionVisible(config, usesGoProvider) {
|
|
115
117
|
return usesGoProvider && config.goUsage.enabled && config.goUsage.cookie.length > 0 && config.goUsage.workspaceID.length > 0;
|
|
@@ -172,6 +174,87 @@ function createGoUsageRefresher(config, fetchImpl = fetch) {
|
|
|
172
174
|
return { state, refresh };
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
// src/copilot-usage.ts
|
|
178
|
+
var CopilotUsageError = class extends Error {
|
|
179
|
+
};
|
|
180
|
+
async function fetchCopilotUsage(token, fetchImpl = fetch) {
|
|
181
|
+
const response = await fetchImpl("https://api.github.com/copilot_internal/user", {
|
|
182
|
+
headers: {
|
|
183
|
+
Authorization: `Bearer ${token}`,
|
|
184
|
+
Accept: "application/json"
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
if (response.status === 401 || response.status === 403) {
|
|
188
|
+
throw new CopilotUsageError("token expired or invalid; update token in config or re-authenticate");
|
|
189
|
+
}
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw new CopilotUsageError(`copilot API request failed with status ${response.status}`);
|
|
192
|
+
}
|
|
193
|
+
let body;
|
|
194
|
+
try {
|
|
195
|
+
body = await response.json();
|
|
196
|
+
} catch {
|
|
197
|
+
throw new CopilotUsageError("could not parse copilot usage response");
|
|
198
|
+
}
|
|
199
|
+
return body;
|
|
200
|
+
}
|
|
201
|
+
function copilotUsageRow(data, now) {
|
|
202
|
+
const pi = data.quota_snapshots?.premium_interactions;
|
|
203
|
+
if (!pi || pi.unlimited) return void 0;
|
|
204
|
+
const used = pi.entitlement - pi.remaining;
|
|
205
|
+
const percentUsed = 100 - pi.percent_remaining;
|
|
206
|
+
const resetDate = Date.parse(data.quota_reset_date_utc);
|
|
207
|
+
const daysRemaining = Number.isFinite(resetDate) ? Math.max(0, Math.ceil((resetDate - now) / 864e5)) : 0;
|
|
208
|
+
return { used, total: pi.entitlement, percentUsed, daysRemaining, creditsUsed: pi.credits_used };
|
|
209
|
+
}
|
|
210
|
+
function formatCopilotUsageRow(row) {
|
|
211
|
+
const bar = formatUsageBar(row.percentUsed);
|
|
212
|
+
const pct = `${Math.round(row.percentUsed)}%`;
|
|
213
|
+
const main = `${"Premium".padEnd(9)}${pct.padEnd(4)} ${bar} ${row.daysRemaining}d`;
|
|
214
|
+
const detail = `${"".padEnd(9)}${String(row.used).padStart(4)} / ${row.total}`;
|
|
215
|
+
return `${main}
|
|
216
|
+
${detail}`;
|
|
217
|
+
}
|
|
218
|
+
function copilotUsageSectionVisible(config, token, usesCopilot) {
|
|
219
|
+
return usesCopilot && config.copilotUsage.enabled && token.length > 0;
|
|
220
|
+
}
|
|
221
|
+
function createCopilotProviderTracker() {
|
|
222
|
+
const providers = /* @__PURE__ */ new Map();
|
|
223
|
+
return {
|
|
224
|
+
record(sessionID, providerID) {
|
|
225
|
+
if (providerID) providers.set(sessionID, providerID);
|
|
226
|
+
},
|
|
227
|
+
usesCopilot(sessionID) {
|
|
228
|
+
return providers.get(sessionID) === "github-copilot";
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function createCopilotUsageRefresher(config, token, fetchImpl = fetch) {
|
|
233
|
+
const state = {};
|
|
234
|
+
let inflight;
|
|
235
|
+
async function refresh(now = Date.now()) {
|
|
236
|
+
if (inflight) {
|
|
237
|
+
await inflight;
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
if (state.lastFetchAt !== void 0 && now - state.lastFetchAt < config.refreshMs) return false;
|
|
241
|
+
inflight = (async () => {
|
|
242
|
+
try {
|
|
243
|
+
state.data = await fetchCopilotUsage(token, fetchImpl);
|
|
244
|
+
state.error = void 0;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
247
|
+
} finally {
|
|
248
|
+
state.lastFetchAt = now;
|
|
249
|
+
inflight = void 0;
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
await inflight;
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
return { state, refresh };
|
|
256
|
+
}
|
|
257
|
+
|
|
175
258
|
// src/activity-hydrate.ts
|
|
176
259
|
var CONCURRENCY_LIMIT = 4;
|
|
177
260
|
var LIST_LIMIT = 1e3;
|
|
@@ -411,6 +494,75 @@ function renderGoUsageSidebar(rows, error, api, titleAttributes, collapsed) {
|
|
|
411
494
|
}
|
|
412
495
|
return new StyledText(chunks);
|
|
413
496
|
}
|
|
497
|
+
function CopilotUsageSidebar(props) {
|
|
498
|
+
let text;
|
|
499
|
+
const [collapsed, setCollapsed] = createSignal(false);
|
|
500
|
+
const titleAttributes = createTextAttributes({ bold: true });
|
|
501
|
+
let previous;
|
|
502
|
+
const toggle = (event) => {
|
|
503
|
+
if (!text || event.y !== text.y) return;
|
|
504
|
+
setCollapsed((prev) => !prev);
|
|
505
|
+
sync();
|
|
506
|
+
};
|
|
507
|
+
const sync = () => {
|
|
508
|
+
if (!text) return;
|
|
509
|
+
const visible = copilotUsageSectionVisible(props.config, props.token, props.tracker.usesCopilot(props.sessionID));
|
|
510
|
+
if (visible) props.refresh();
|
|
511
|
+
const row = visible ? copilotUsageRow(props.state.data ?? {}, Date.now()) : void 0;
|
|
512
|
+
const error = props.state.error;
|
|
513
|
+
const showContent = visible && (row || error);
|
|
514
|
+
const signature = showContent ? JSON.stringify({ collapsed: collapsed(), row, error }) : "";
|
|
515
|
+
const next = {
|
|
516
|
+
content: signature,
|
|
517
|
+
visible: signature.length > 0,
|
|
518
|
+
height: signature.length > 0 ? "auto" : 0
|
|
519
|
+
};
|
|
520
|
+
if (!hasRenderStateChanged(previous, next)) return;
|
|
521
|
+
previous = next;
|
|
522
|
+
text.visible = next.visible;
|
|
523
|
+
text.height = next.height;
|
|
524
|
+
text.content = showContent ? renderCopilotUsageSidebar(row, error, props.api, titleAttributes, collapsed()) : "";
|
|
525
|
+
props.api.renderer.requestRender();
|
|
526
|
+
};
|
|
527
|
+
const unsubscribe = props.subscribe(sync);
|
|
528
|
+
const unsubscribeCopilot = props.copilotUsageSubscribe(sync);
|
|
529
|
+
const timer = setInterval(sync, 1e3);
|
|
530
|
+
onCleanup(() => {
|
|
531
|
+
unsubscribe();
|
|
532
|
+
unsubscribeCopilot();
|
|
533
|
+
clearInterval(timer);
|
|
534
|
+
});
|
|
535
|
+
return /* @__PURE__ */ jsx(
|
|
536
|
+
"text",
|
|
537
|
+
{
|
|
538
|
+
ref: (ref) => {
|
|
539
|
+
text = ref;
|
|
540
|
+
sync();
|
|
541
|
+
},
|
|
542
|
+
onMouseDown: toggle,
|
|
543
|
+
fg: props.api.theme.current.textMuted,
|
|
544
|
+
children: ""
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
function renderCopilotUsageSidebar(row, error, api, titleAttributes, collapsed) {
|
|
549
|
+
const chunks = [
|
|
550
|
+
textChunk(`${collapsed ? "\u25B6" : "\u25BC"} Copilot
|
|
551
|
+
`, api.theme.current.text, titleAttributes)
|
|
552
|
+
];
|
|
553
|
+
if (!collapsed) {
|
|
554
|
+
if (row) {
|
|
555
|
+
const lines = formatCopilotUsageRow(row).split("\n");
|
|
556
|
+
for (const [index, line] of lines.entries()) {
|
|
557
|
+
if (index > 0) chunks.push(textChunk("\n"));
|
|
558
|
+
chunks.push(textChunk(line, api.theme.current.textMuted));
|
|
559
|
+
}
|
|
560
|
+
} else if (error) {
|
|
561
|
+
chunks.push(textChunk(`Copilot: ${error}`, api.theme.current.error));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return new StyledText(chunks);
|
|
565
|
+
}
|
|
414
566
|
function SubagentSidebar(props) {
|
|
415
567
|
let text;
|
|
416
568
|
const [collapsed, setCollapsed] = createSignal(false);
|
|
@@ -645,22 +797,34 @@ var tui = async (api, options) => {
|
|
|
645
797
|
const metricListeners = createListenerRegistry();
|
|
646
798
|
const subagentListeners = createListenerRegistry();
|
|
647
799
|
const goUsageListeners = createListenerRegistry();
|
|
800
|
+
const copilotUsageListeners = createListenerRegistry();
|
|
648
801
|
const hydratedSessions = /* @__PURE__ */ new Set();
|
|
649
802
|
const goProviderTracker = createGoProviderTracker();
|
|
803
|
+
const copilotProviderTracker = createCopilotProviderTracker();
|
|
650
804
|
const goUsage = createGoUsageRefresher(config.goUsage);
|
|
805
|
+
const copilotToken = resolveCopilotToken(config.copilotUsage);
|
|
806
|
+
const copilotUsage = createCopilotUsageRefresher(config.copilotUsage, copilotToken);
|
|
651
807
|
const refreshGoUsage = async () => {
|
|
652
808
|
if (await goUsage.refresh()) goUsageListeners.notify();
|
|
653
809
|
};
|
|
810
|
+
const refreshCopilotUsage = async () => {
|
|
811
|
+
if (await copilotUsage.refresh()) copilotUsageListeners.notify();
|
|
812
|
+
};
|
|
654
813
|
const hydrateSessionMetrics = async (sessionID) => {
|
|
655
814
|
if (!isSessionID2(sessionID) || hydratedSessions.has(sessionID)) return;
|
|
656
815
|
hydratedSessions.add(sessionID);
|
|
657
816
|
try {
|
|
658
817
|
const response = await api.client.session.messages({ sessionID });
|
|
659
|
-
const messages = response.data ?? []
|
|
818
|
+
const messages = (response.data ?? []).slice().sort((a, b) => {
|
|
819
|
+
const aCreated = a.info.time?.created;
|
|
820
|
+
const bCreated = b.info.time?.created;
|
|
821
|
+
return (typeof aCreated === "number" ? aCreated : 0) - (typeof bCreated === "number" ? bCreated : 0);
|
|
822
|
+
});
|
|
660
823
|
for (const message of messages) {
|
|
661
824
|
const info = message.info;
|
|
662
825
|
const providerID = info.providerID;
|
|
663
826
|
goProviderTracker.record(sessionID, typeof providerID === "string" ? providerID : void 0);
|
|
827
|
+
copilotProviderTracker.record(sessionID, typeof providerID === "string" ? providerID : void 0);
|
|
664
828
|
if (info.role !== "assistant" || typeof info.time.completed !== "number") continue;
|
|
665
829
|
const input = {
|
|
666
830
|
sessionID: info.sessionID,
|
|
@@ -696,6 +860,7 @@ var tui = async (api, options) => {
|
|
|
696
860
|
const sessionID = info.sessionID ?? evt.properties.sessionID;
|
|
697
861
|
const providerID = info.providerID ?? info.model?.providerID;
|
|
698
862
|
goProviderTracker.record(sessionID, typeof providerID === "string" ? providerID : void 0);
|
|
863
|
+
copilotProviderTracker.record(sessionID, typeof providerID === "string" ? providerID : void 0);
|
|
699
864
|
if (info.role !== "assistant") return;
|
|
700
865
|
const messageInput = {
|
|
701
866
|
sessionID: info.sessionID ?? evt.properties.sessionID,
|
|
@@ -813,6 +978,20 @@ var tui = async (api, options) => {
|
|
|
813
978
|
refresh: () => void refreshGoUsage()
|
|
814
979
|
}
|
|
815
980
|
),
|
|
981
|
+
/* @__PURE__ */ jsx(
|
|
982
|
+
CopilotUsageSidebar,
|
|
983
|
+
{
|
|
984
|
+
api,
|
|
985
|
+
state: copilotUsage.state,
|
|
986
|
+
config,
|
|
987
|
+
token: copilotToken,
|
|
988
|
+
tracker: copilotProviderTracker,
|
|
989
|
+
sessionID: props.session_id,
|
|
990
|
+
subscribe: metricListeners.subscribe,
|
|
991
|
+
copilotUsageSubscribe: copilotUsageListeners.subscribe,
|
|
992
|
+
refresh: () => void refreshCopilotUsage()
|
|
993
|
+
}
|
|
994
|
+
),
|
|
816
995
|
/* @__PURE__ */ jsx(
|
|
817
996
|
SubagentSidebar,
|
|
818
997
|
{
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@rejacky/opencode-insights",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"description": "OpenCode plugin for local request capture, TPS metrics, and subagent status visibility.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "opencode-insights contributors",
|