@rejacky/opencode-insights 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
- ## Go Usage Configuration
124
+ ## Provider Usage Configuration
124
125
 
125
- The `Go Usage` sidebar shows the rolling (5 hour), weekly, and monthly usage limits of your OpenCode Go subscription for sessions that use the `opencode-go` provider. It is disabled by default and opt-in:
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
- ```json
128
+ ```jsonc
128
129
  {
130
+ "promptRightMetrics": ["tps", "avg", "used", "cache"],
129
131
  "goUsage": {
130
- "enabled": true,
131
- "cookie": "Fe26.2**...",
132
- "workspaceID": "wrk_...",
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
- - `enabled` — set to `true` to activate the section. Defaults to `false`.
139
- - `cookie` — the `auth` session cookie for `opencode.ai` (see below).
140
- - `workspaceID` your workspace id, visible in the console URL (`/workspace/<workspaceID>/go`).
141
- - `refreshMs` — how often to re-fetch usage from the console. Defaults to `300000` (5 minutes); values below 60000 are clamped.
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
- 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. Restart OpenCode after editing this file.
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
 
@@ -78,7 +78,7 @@ declare function renderPromptRightMetricsText(state: MetricsState, sessionID: st
78
78
  metrics?: PromptRightMetric[];
79
79
  }): string;
80
80
  declare function getSessionTokenUsage(state: MetricsState, sessionID: string): SessionTokenUsage | undefined;
81
- declare function renderSessionTokenUsage(state: MetricsState, sessionID: string): string;
81
+ declare function renderSessionTokenUsage(state: MetricsState, sessionID: string, subagentTokens?: number): string;
82
82
 
83
83
  type CaptureKind = "chat.message" | "chat.params" | "chat.headers" | "experimental.chat.messages.transform" | "experimental.chat.system.transform" | "event" | "tool.execute.before" | "tool.execute.after";
84
84
  type CaptureRecord = {
@@ -107,9 +107,15 @@ type GoUsageConfig = {
107
107
  workspaceID: string;
108
108
  refreshMs: number;
109
109
  };
110
+ type CopilotUsageConfig = {
111
+ enabled: boolean;
112
+ token: string;
113
+ refreshMs: number;
114
+ };
110
115
  type InsightsConfig = {
111
116
  promptRightMetrics: PromptRightMetric[];
112
117
  goUsage: GoUsageConfig;
118
+ copilotUsage: CopilotUsageConfig;
113
119
  dbPath?: string | undefined;
114
120
  retentionDays?: number | undefined;
115
121
  };
@@ -119,6 +125,7 @@ declare function resolveInsightsConfigPath(options?: InsightsOptions): string;
119
125
  declare function resolveLegacyInsightsConfigPath(options?: InsightsOptions): string;
120
126
  declare function readInsightsConfig(options?: InsightsOptions): Promise<InsightsConfig>;
121
127
  declare function insightsOptionsFromConfig(config: InsightsConfig, dataDir?: string): InsightsOptions;
128
+ declare function resolveCopilotToken(config: CopilotUsageConfig): string;
122
129
  declare function normalizeChatMessageCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
123
130
  declare function normalizeChatParamsCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
124
131
  declare function normalizeChatHeadersCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
@@ -156,4 +163,4 @@ declare class SqliteCaptureStore implements CaptureStore {
156
163
  declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
157
164
  declare function resolveRetentionDays(value: unknown): number;
158
165
 
159
- export { type AssistantResponseUsage as A, recordToolActivity as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, renderMetricsText as E, renderPromptRightMetricsText as F, type GoUsageConfig as G, renderResponseMetricsText as H, type InsightsConfig as I, JsonlCaptureStore as J, renderSessionTokenUsage as K, resolveCapturePath as L, type MessageTiming as M, resolveInsightsConfigPath as N, resolveLegacyInsightsConfigPath as O, type PromptRightMetric as P, resolveRetentionDays as Q, type SessionAverage as S, type CaptureKind as a, type CaptureStore as b, type InsightsOptions as c, type MetricsState as d, type SessionTokenUsage as e, SqliteCaptureStore as f, type SqliteDb as g, type StreamSample as h, createCaptureStore as i, createMetricsState as j, defaultDataDir as k, estimateStreamTokens as l, extractEventType as m, getSessionTokenUsage as n, insightsOptionsFromConfig as o, normalizeChatHeadersCapture as p, normalizeChatMessageCapture as q, normalizeChatParamsCapture as r, normalizeEventCapture as s, normalizeExperimentalChatMessagesTransformCapture as t, normalizeExperimentalChatSystemTransformCapture as u, normalizeToolCapture as v, openDatabase as w, readInsightsConfig as x, recordAssistantDelta as y, recordAssistantMessage as z };
166
+ export { type AssistantResponseUsage as A, recordAssistantMessage as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, recordToolActivity as E, renderMetricsText as F, type GoUsageConfig as G, renderPromptRightMetricsText as H, type InsightsConfig as I, JsonlCaptureStore as J, renderResponseMetricsText as K, renderSessionTokenUsage as L, type MessageTiming as M, resolveCapturePath as N, resolveCopilotToken as O, type PromptRightMetric as P, resolveInsightsConfigPath as Q, resolveLegacyInsightsConfigPath as R, type SessionAverage as S, resolveRetentionDays as T, type CaptureKind as a, type CaptureStore as b, type CopilotUsageConfig as c, type InsightsOptions as d, type MetricsState as e, type SessionTokenUsage as f, SqliteCaptureStore as g, type SqliteDb as h, type StreamSample as i, createCaptureStore as j, createMetricsState as k, defaultDataDir as l, estimateStreamTokens as m, extractEventType as n, getSessionTokenUsage as o, insightsOptionsFromConfig as p, normalizeChatHeadersCapture as q, normalizeChatMessageCapture as r, normalizeChatParamsCapture as s, normalizeEventCapture as t, normalizeExperimentalChatMessagesTransformCapture as u, normalizeExperimentalChatSystemTransformCapture as v, normalizeToolCapture as w, openDatabase as x, readInsightsConfig as y, recordAssistantDelta as z };
@@ -225,6 +225,20 @@ function buildSessionAnalysisRows(state, rootSessionID) {
225
225
  function createSubagentState(activityStore) {
226
226
  return { children: {}, totalExecuted: 0, ...activityStore ? { activityStore } : {} };
227
227
  }
228
+ function recordSubagentFromSessionInfo(state, session) {
229
+ if (!session.parentID || !session.id || session.id === session.parentID) return;
230
+ if (state.children[session.id]) return;
231
+ const now = (/* @__PURE__ */ new Date()).toISOString();
232
+ state.children[session.id] = {
233
+ id: session.id,
234
+ parentID: session.parentID,
235
+ title: session.title ?? "subagent",
236
+ status: "done",
237
+ startedAt: now,
238
+ updatedAt: now
239
+ };
240
+ state.totalExecuted += 1;
241
+ }
228
242
  function applySubagentEvent(state, event) {
229
243
  const created = extractTaskToolSubagent(event) ?? extractSubagent(event) ?? updateExistingSubagent(state, event);
230
244
  if (!created) return false;
@@ -275,6 +289,9 @@ function getSubagentItems(state, parentID) {
275
289
  return b.startedAt.localeCompare(a.startedAt);
276
290
  });
277
291
  }
292
+ function sumSubagentTokens(state, parentID) {
293
+ return getSubagentItems(state, parentID).reduce((sum, child) => sum + (child.tokens?.total ?? 0), 0);
294
+ }
278
295
  function pruneStaleSubagents(state, options = {}) {
279
296
  const now = options.now ?? Date.now();
280
297
  const staleMs = options.staleMs ?? 18e4;
@@ -535,9 +552,11 @@ export {
535
552
  treeSubagentCount,
536
553
  buildSessionAnalysisRows,
537
554
  createSubagentState,
555
+ recordSubagentFromSessionInfo,
538
556
  applySubagentEvent,
539
557
  renderSubagentStatus,
540
558
  getSubagentItems,
559
+ sumSubagentTokens,
541
560
  pruneStaleSubagents,
542
561
  getSubagentSidebarModel,
543
562
  getSubagentSidebarRowAtLine,
@@ -136,21 +136,28 @@ function renderPromptRightMetricsText(state, sessionID, options = {}) {
136
136
  function getSessionTokenUsage(state, sessionID) {
137
137
  return state.sessionTokenUsageByID[sessionID];
138
138
  }
139
- function renderSessionTokenUsage(state, sessionID) {
139
+ function renderSessionTokenUsage(state, sessionID, subagentTokens = 0) {
140
140
  const usage = getSessionTokenUsage(state, sessionID);
141
141
  if (!usage) return "";
142
+ const grandTotal = usage.totalTokens + subagentTokens;
142
143
  const cachePromptTokens = usage.inputTokens + usage.cacheReadTokens;
143
144
  const cacheRate = cachePromptTokens > 0 ? usage.cacheReadTokens / cachePromptTokens * 100 : void 0;
144
- return [
145
+ const lines = [
145
146
  "Token Usage",
146
- `${formatTokenCount(usage.totalTokens)} total \xB7 ${usage.responseCount} responses`,
147
+ `${formatTokenCount(grandTotal)} total \xB7 ${usage.responseCount} responses`
148
+ ];
149
+ if (subagentTokens > 0) {
150
+ lines.push(`${formatTokenCount(subagentTokens)} used by subagents`);
151
+ }
152
+ lines.push(
147
153
  `${formatTokenCount(usage.inputTokens)} input`,
148
154
  `${formatTokenCount(usage.outputTokens)} output`,
149
155
  `${formatTokenCount(usage.reasoningTokens)} reasoning`,
150
156
  `${formatTokenCount(usage.cacheReadTokens)} cache read`,
151
157
  `${formatTokenCount(usage.cacheWriteTokens)} cache write`,
152
158
  `${cacheRate === void 0 ? "-" : formatPercent(cacheRate)} cache rate`
153
- ].join("\n");
159
+ );
160
+ return lines.join("\n");
154
161
  }
155
162
  function pruneSamples(state, now = Date.now()) {
156
163
  for (const [sessionID, samples] of Object.entries(state.streamSamplesBySession)) {
@@ -270,7 +277,9 @@ import { parse } from "jsonc-parser";
270
277
  var DEFAULT_RETENTION_DAYS = 1;
271
278
  var DAY_MS = 24 * 60 * 60 * 1e3;
272
279
  var DEFAULT_GO_USAGE_REFRESH_MS = 3e5;
280
+ var DEFAULT_COPILOT_USAGE_REFRESH_MS = 3e5;
273
281
  var MIN_GO_USAGE_REFRESH_MS = 6e4;
282
+ var MIN_COPILOT_USAGE_REFRESH_MS = 6e4;
274
283
  var sequence = 0;
275
284
  function nextID(timestamp) {
276
285
  sequence += 1;
@@ -370,7 +379,8 @@ function defaultInsightsConfigJsonc() {
370
379
  ' // "dbPath": "/absolute/path/to/insights.sqlite",',
371
380
  ` "retentionDays": ${DEFAULT_RETENTION_DAYS},`,
372
381
  ` "promptRightMetrics": ${JSON.stringify(DEFAULT_PROMPT_RIGHT_METRICS)},`,
373
- ` "goUsage": ${JSON.stringify(defaultGoUsageConfig())}`,
382
+ ` "goUsage": ${JSON.stringify(defaultGoUsageConfig())},`,
383
+ ` "copilotUsage": ${JSON.stringify(defaultCopilotUsageConfig())}`,
374
384
  "}",
375
385
  ""
376
386
  ].join("\n");
@@ -379,11 +389,18 @@ function insightsOptionsFromConfig(config, dataDir) {
379
389
  return compactUndefined({ dataDir, dbPath: config.dbPath, retentionDays: config.retentionDays });
380
390
  }
381
391
  function defaultInsightsConfig() {
382
- return { promptRightMetrics: [...DEFAULT_PROMPT_RIGHT_METRICS], goUsage: defaultGoUsageConfig() };
392
+ return {
393
+ promptRightMetrics: [...DEFAULT_PROMPT_RIGHT_METRICS],
394
+ goUsage: defaultGoUsageConfig(),
395
+ copilotUsage: defaultCopilotUsageConfig()
396
+ };
383
397
  }
384
398
  function defaultGoUsageConfig() {
385
399
  return { enabled: false, cookie: "", workspaceID: "", refreshMs: DEFAULT_GO_USAGE_REFRESH_MS };
386
400
  }
401
+ function defaultCopilotUsageConfig() {
402
+ return { enabled: false, token: "", refreshMs: DEFAULT_COPILOT_USAGE_REFRESH_MS };
403
+ }
387
404
  function insightsConfigFrom(value) {
388
405
  const record = isRecord(value) ? value : {};
389
406
  const metrics = Array.isArray(record.promptRightMetrics) ? record.promptRightMetrics.filter(isPromptRightMetric) : [];
@@ -393,6 +410,7 @@ function insightsConfigFrom(value) {
393
410
  return {
394
411
  promptRightMetrics: metrics.length ? metrics : [...DEFAULT_PROMPT_RIGHT_METRICS],
395
412
  goUsage: goUsageConfigFrom(record.goUsage),
413
+ copilotUsage: copilotUsageConfigFrom(record.copilotUsage),
396
414
  dbPath,
397
415
  retentionDays
398
416
  };
@@ -405,9 +423,32 @@ function goUsageConfigFrom(value) {
405
423
  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;
406
424
  return { enabled, cookie, workspaceID, refreshMs };
407
425
  }
426
+ function copilotUsageConfigFrom(value) {
427
+ const record = isRecord(value) ? value : {};
428
+ const enabled = record.enabled === true;
429
+ const token = typeof record.token === "string" && record.token.length > 0 ? record.token : "";
430
+ 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;
431
+ return { enabled, token, refreshMs };
432
+ }
408
433
  function isPromptRightMetric(value) {
409
434
  return value === "tps" || value === "avg" || value === "ttft" || value === "used" || value === "cache" || value === "input" || value === "output" || value === "reasoning";
410
435
  }
436
+ function resolveCopilotToken(config) {
437
+ if (config.token.length > 0) return config.token;
438
+ try {
439
+ const authPath = join(homedir(), ".local/share/opencode/auth.json");
440
+ if (!existsSync(authPath)) return "";
441
+ const raw = readFileSync(authPath, "utf8");
442
+ const auth = JSON.parse(raw);
443
+ const copilot = isRecord(auth?.["github-copilot"]) ? auth["github-copilot"] : void 0;
444
+ if (!copilot) return "";
445
+ const access = typeof copilot.access === "string" ? copilot.access : "";
446
+ const refresh = typeof copilot.refresh === "string" ? copilot.refresh : "";
447
+ return access || refresh;
448
+ } catch {
449
+ return "";
450
+ }
451
+ }
411
452
  function normalizeChatMessageCapture(input, output, timestamp = Date.now()) {
412
453
  const inputRecord = isRecord(input) ? input : {};
413
454
  const model = isRecord(inputRecord.model) ? inputRecord.model : void 0;
@@ -747,6 +788,7 @@ export {
747
788
  resolveLegacyInsightsConfigPath,
748
789
  readInsightsConfig,
749
790
  insightsOptionsFromConfig,
791
+ resolveCopilotToken,
750
792
  normalizeChatMessageCapture,
751
793
  normalizeChatParamsCapture,
752
794
  normalizeChatHeadersCapture,
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as CaptureRecord } from './capture-CwesXqmX.js';
2
+ import { C as CaptureRecord } from './capture-DJVWBRum.js';
3
3
 
4
4
  type HistoryMessage = {
5
5
  id: string;
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  readInsightsConfig,
5
5
  resolveCapturePath,
6
6
  resolveInsightsConfigPath
7
- } from "./chunk-RZGCLQ2M.js";
7
+ } from "./chunk-ROQFQXIH.js";
8
8
 
9
9
  // src/cli.ts
10
10
  import { execFile as execFile2 } from "child_process";
@@ -217,21 +217,19 @@ function buildRequestHistory(records) {
217
217
  continue;
218
218
  }
219
219
  if (type === "message.part.updated" || type === "message.part.delta") {
220
+ if (type === "message.part.delta") continue;
220
221
  const part = isRecord(properties.part) ? properties.part : {};
221
222
  const sessionID = optionalString(part.sessionID) ?? optionalString(properties.sessionID);
222
223
  const messageID = optionalString(part.messageID) ?? optionalString(properties.messageID);
223
224
  if (!sessionID || !messageID) continue;
224
225
  const partType = optionalString(part.type);
225
- const field = optionalString(properties.field);
226
- const delta = optionalString(properties.delta);
227
226
  const text = optionalString(part.text);
228
227
  const reasonText = optionalString(part.text) ?? optionalString(part.markdown);
229
228
  const targetResponse = responses.get(`${sessionID}:${messageID}`);
230
229
  if (targetResponse) {
231
- targetResponse.events.push(record.payload);
230
+ if (partType === "tool") targetResponse.events.push(record.payload);
232
231
  if (partType === "text" && text !== void 0) targetResponse.text = text;
233
232
  if (partType === "reasoning" && reasonText !== void 0) targetResponse.reasoning = reasonText;
234
- if (type === "message.part.delta" && field === "text" && delta !== void 0) targetResponse.text += delta;
235
233
  continue;
236
234
  }
237
235
  if (partType !== "text" && partType !== "reasoning") continue;
@@ -376,7 +374,6 @@ function viewerCaptureSql(limit) {
376
374
  and event_type in (
377
375
  'message.updated',
378
376
  'message.part.updated',
379
- 'message.part.delta',
380
377
  'session.updated',
381
378
  'session.created'
382
379
  )
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, c as InsightsOptions, J as JsonlCaptureStore, M as MessageTiming, d as MetricsState, P as PromptRightMetric, S as SessionAverage, e as SessionTokenUsage, f as SqliteCaptureStore, g as SqliteDb, h as StreamSample, i as createCaptureStore, j as createMetricsState, k as defaultDataDir, l as estimateStreamTokens, m as extractEventType, n as getSessionTokenUsage, o as insightsOptionsFromConfig, p as normalizeChatHeadersCapture, q as normalizeChatMessageCapture, r as normalizeChatParamsCapture, s as normalizeEventCapture, t as normalizeExperimentalChatMessagesTransformCapture, u as normalizeExperimentalChatSystemTransformCapture, v as normalizeToolCapture, w as openDatabase, x as readInsightsConfig, y as recordAssistantDelta, z as recordAssistantMessage, B as recordToolActivity, E as renderMetricsText, F as renderPromptRightMetricsText, H as renderResponseMetricsText, K as renderSessionTokenUsage, L as resolveCapturePath, N as resolveInsightsConfigPath, O as resolveLegacyInsightsConfigPath, Q as resolveRetentionDays } from './capture-CwesXqmX.js';
3
+ export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, c as CopilotUsageConfig, D as DEFAULT_PROMPT_RIGHT_METRICS, G as GoUsageConfig, I as InsightsConfig, d as InsightsOptions, J as JsonlCaptureStore, M as MessageTiming, e as MetricsState, P as PromptRightMetric, S as SessionAverage, f as SessionTokenUsage, g as SqliteCaptureStore, h as SqliteDb, i as StreamSample, j as createCaptureStore, k as createMetricsState, l as defaultDataDir, m as estimateStreamTokens, n as extractEventType, o as getSessionTokenUsage, p as insightsOptionsFromConfig, q as normalizeChatHeadersCapture, r as normalizeChatMessageCapture, s as normalizeChatParamsCapture, t as normalizeEventCapture, u as normalizeExperimentalChatMessagesTransformCapture, v as normalizeExperimentalChatSystemTransformCapture, w as normalizeToolCapture, x as openDatabase, y as readInsightsConfig, z as recordAssistantDelta, B as recordAssistantMessage, E as recordToolActivity, F as renderMetricsText, H as renderPromptRightMetricsText, K as renderResponseMetricsText, L as renderSessionTokenUsage, N as resolveCapturePath, O as resolveCopilotToken, Q as resolveInsightsConfigPath, R as resolveLegacyInsightsConfigPath, T as resolveRetentionDays } from './capture-DJVWBRum.js';
4
4
 
5
5
  type SessionActivity = {
6
6
  toolCalls: number;
@@ -58,11 +58,17 @@ type SubagentSidebarModel = {
58
58
  rows: SubagentSidebarRow[];
59
59
  };
60
60
  declare function createSubagentState(activityStore?: ActivityState): SubagentState;
61
+ declare function recordSubagentFromSessionInfo(state: SubagentState, session: {
62
+ id: string;
63
+ parentID?: string;
64
+ title?: string;
65
+ }): void;
61
66
  declare function applySubagentEvent(state: SubagentState, event: unknown): boolean;
62
67
  declare function renderSubagentStatus(state: SubagentState, options?: {
63
68
  now?: number;
64
69
  }): string;
65
70
  declare function getSubagentItems(state: SubagentState, parentID?: string): SubagentInfo[];
71
+ declare function sumSubagentTokens(state: SubagentState, parentID: string): number;
66
72
  declare function pruneStaleSubagents(state: SubagentState, options?: {
67
73
  now?: number;
68
74
  staleMs?: number;
@@ -89,4 +95,4 @@ declare const _default: {
89
95
  server: Plugin;
90
96
  };
91
97
 
92
- 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 };
98
+ export { OpenCodeInsights, type SubagentInfo, type SubagentSidebarModel, type SubagentSidebarRow, type SubagentState, type SubagentStatus, applySubagentEvent, createSubagentState, _default as default, getSubagentItems, getSubagentSidebarModel, getSubagentSidebarRowAtLine, id, pruneStaleSubagents, recordSubagentFromSessionInfo, renderSubagentFooter, renderSubagentSidebar, renderSubagentStatus, server, sumSubagentTokens, rootTui as tui };
package/dist/index.js CHANGED
@@ -5,10 +5,12 @@ import {
5
5
  getSubagentSidebarModel,
6
6
  getSubagentSidebarRowAtLine,
7
7
  pruneStaleSubagents,
8
+ recordSubagentFromSessionInfo,
8
9
  renderSubagentFooter,
9
10
  renderSubagentSidebar,
10
- renderSubagentStatus
11
- } from "./chunk-SGXZJVYX.js";
11
+ renderSubagentStatus,
12
+ sumSubagentTokens
13
+ } from "./chunk-ODCIUSCV.js";
12
14
  import {
13
15
  DEFAULT_PROMPT_RIGHT_METRICS,
14
16
  JsonlCaptureStore,
@@ -37,10 +39,11 @@ import {
37
39
  renderResponseMetricsText,
38
40
  renderSessionTokenUsage,
39
41
  resolveCapturePath,
42
+ resolveCopilotToken,
40
43
  resolveInsightsConfigPath,
41
44
  resolveLegacyInsightsConfigPath,
42
45
  resolveRetentionDays
43
- } from "./chunk-RZGCLQ2M.js";
46
+ } from "./chunk-ROQFQXIH.js";
44
47
 
45
48
  // src/cli-shim.ts
46
49
  import { existsSync } from "fs";
@@ -173,6 +176,7 @@ export {
173
176
  readInsightsConfig,
174
177
  recordAssistantDelta,
175
178
  recordAssistantMessage,
179
+ recordSubagentFromSessionInfo,
176
180
  recordToolActivity,
177
181
  renderMetricsText,
178
182
  renderPromptRightMetricsText,
@@ -182,9 +186,11 @@ export {
182
186
  renderSubagentSidebar,
183
187
  renderSubagentStatus,
184
188
  resolveCapturePath,
189
+ resolveCopilotToken,
185
190
  resolveInsightsConfigPath,
186
191
  resolveLegacyInsightsConfigPath,
187
192
  resolveRetentionDays,
188
193
  server,
194
+ sumSubagentTokens,
189
195
  rootTui as tui
190
196
  };
package/dist/tui.js CHANGED
@@ -10,10 +10,11 @@ import {
10
10
  recordCompaction,
11
11
  recordStep,
12
12
  recordToolPart,
13
+ sumSubagentTokens,
13
14
  treeActivity,
14
15
  treeLoading,
15
16
  treeSubagentCount
16
- } from "./chunk-SGXZJVYX.js";
17
+ } from "./chunk-ODCIUSCV.js";
17
18
  import {
18
19
  createMetricsState,
19
20
  readInsightsConfig,
@@ -21,8 +22,9 @@ import {
21
22
  recordAssistantMessage,
22
23
  recordToolActivity,
23
24
  renderPromptRightMetricsText,
24
- renderSessionTokenUsage
25
- } from "./chunk-RZGCLQ2M.js";
25
+ renderSessionTokenUsage,
26
+ resolveCopilotToken
27
+ } from "./chunk-ROQFQXIH.js";
26
28
 
27
29
  // src/tui.tsx
28
30
  import { createTextAttributes, StyledText } from "@opentui/core";
@@ -108,7 +110,8 @@ function formatUsageBar(usagePercent, width = 10) {
108
110
  }
109
111
  function formatGoUsageRow(row) {
110
112
  const bar = formatUsageBar(row.usagePercent);
111
- return `${row.label.padEnd(9)}${`${row.usagePercent}%`.padEnd(3)} ${bar} ${row.reset}`;
113
+ const pct = `${Math.round(row.usagePercent)}%`;
114
+ return `${row.label.padEnd(9)}${pct.padEnd(4)} ${bar} ${row.reset}`;
112
115
  }
113
116
  function goUsageSectionVisible(config, usesGoProvider) {
114
117
  return usesGoProvider && config.goUsage.enabled && config.goUsage.cookie.length > 0 && config.goUsage.workspaceID.length > 0;
@@ -171,6 +174,87 @@ function createGoUsageRefresher(config, fetchImpl = fetch) {
171
174
  return { state, refresh };
172
175
  }
173
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
+
174
258
  // src/activity-hydrate.ts
175
259
  var CONCURRENCY_LIMIT = 4;
176
260
  var LIST_LIMIT = 1e3;
@@ -312,7 +396,8 @@ function TokenUsageSidebar(props) {
312
396
  };
313
397
  const sync = () => {
314
398
  if (!text) return;
315
- const content = renderSessionTokenUsage(props.state, props.sessionID);
399
+ const subagentTokens = sumSubagentTokens(props.subagentState, props.sessionID);
400
+ const content = renderSessionTokenUsage(props.state, props.sessionID, subagentTokens);
316
401
  const next = {
317
402
  content: `${collapsed()}|${content}`,
318
403
  visible: content.length > 0,
@@ -409,6 +494,75 @@ function renderGoUsageSidebar(rows, error, api, titleAttributes, collapsed) {
409
494
  }
410
495
  return new StyledText(chunks);
411
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
+ }
412
566
  function SubagentSidebar(props) {
413
567
  let text;
414
568
  const [collapsed, setCollapsed] = createSignal(false);
@@ -643,12 +797,19 @@ var tui = async (api, options) => {
643
797
  const metricListeners = createListenerRegistry();
644
798
  const subagentListeners = createListenerRegistry();
645
799
  const goUsageListeners = createListenerRegistry();
800
+ const copilotUsageListeners = createListenerRegistry();
646
801
  const hydratedSessions = /* @__PURE__ */ new Set();
647
802
  const goProviderTracker = createGoProviderTracker();
803
+ const copilotProviderTracker = createCopilotProviderTracker();
648
804
  const goUsage = createGoUsageRefresher(config.goUsage);
805
+ const copilotToken = resolveCopilotToken(config.copilotUsage);
806
+ const copilotUsage = createCopilotUsageRefresher(config.copilotUsage, copilotToken);
649
807
  const refreshGoUsage = async () => {
650
808
  if (await goUsage.refresh()) goUsageListeners.notify();
651
809
  };
810
+ const refreshCopilotUsage = async () => {
811
+ if (await copilotUsage.refresh()) copilotUsageListeners.notify();
812
+ };
652
813
  const hydrateSessionMetrics = async (sessionID) => {
653
814
  if (!isSessionID2(sessionID) || hydratedSessions.has(sessionID)) return;
654
815
  hydratedSessions.add(sessionID);
@@ -659,6 +820,7 @@ var tui = async (api, options) => {
659
820
  const info = message.info;
660
821
  const providerID = info.providerID;
661
822
  goProviderTracker.record(sessionID, typeof providerID === "string" ? providerID : void 0);
823
+ copilotProviderTracker.record(sessionID, typeof providerID === "string" ? providerID : void 0);
662
824
  if (info.role !== "assistant" || typeof info.time.completed !== "number") continue;
663
825
  const input = {
664
826
  sessionID: info.sessionID,
@@ -694,6 +856,7 @@ var tui = async (api, options) => {
694
856
  const sessionID = info.sessionID ?? evt.properties.sessionID;
695
857
  const providerID = info.providerID ?? info.model?.providerID;
696
858
  goProviderTracker.record(sessionID, typeof providerID === "string" ? providerID : void 0);
859
+ copilotProviderTracker.record(sessionID, typeof providerID === "string" ? providerID : void 0);
697
860
  if (info.role !== "assistant") return;
698
861
  const messageInput = {
699
862
  sessionID: info.sessionID ?? evt.properties.sessionID,
@@ -793,6 +956,7 @@ var tui = async (api, options) => {
793
956
  api,
794
957
  sessionID: props.session_id,
795
958
  state: metrics,
959
+ subagentState: subagents,
796
960
  subscribe: metricListeners.subscribe,
797
961
  hydrate: () => void hydrateSessionMetrics(props.session_id)
798
962
  }
@@ -810,6 +974,20 @@ var tui = async (api, options) => {
810
974
  refresh: () => void refreshGoUsage()
811
975
  }
812
976
  ),
977
+ /* @__PURE__ */ jsx(
978
+ CopilotUsageSidebar,
979
+ {
980
+ api,
981
+ state: copilotUsage.state,
982
+ config,
983
+ token: copilotToken,
984
+ tracker: copilotProviderTracker,
985
+ sessionID: props.session_id,
986
+ subscribe: metricListeners.subscribe,
987
+ copilotUsageSubscribe: copilotUsageListeners.subscribe,
988
+ refresh: () => void refreshCopilotUsage()
989
+ }
990
+ ),
813
991
  /* @__PURE__ */ jsx(
814
992
  SubagentSidebar,
815
993
  {
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.3.1",
4
+ "version": "0.4.0",
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",