oc-go-usage-display 1.0.2 → 1.1.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 +30 -1
- package/bin/lib.js +47 -1
- package/bin/oc-go-usage-display-remove.js +33 -0
- package/bin/oc-go-usage-display-status.js +19 -7
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -133
- package/dist/index.js.map +1 -1
- package/dist/shared.d.ts +28 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +152 -0
- package/dist/shared.js.map +1 -0
- package/dist/tui.d.ts +17 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +26 -124
- package/dist/tui.js.map +1 -1
- package/package.json +4 -2
- package/src/index.ts +34 -148
- package/src/shared.ts +179 -0
- package/src/tui.tsx +42 -137
package/src/tui.tsx
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// carries no paddingLeft/gap so `Go Usage` aligns flush left like the
|
|
11
11
|
// `Context` header.
|
|
12
12
|
// - `session_prompt_right` -> compact single line next to the context status
|
|
13
|
-
// info (e.g. `Go 5h 42% | 7d 15%`), where the `80.6K (8%) · $0.09` readout
|
|
13
|
+
// info (e.g. `Go 5h 42% | 7d 15% | 30d 61%`), where the `80.6K (8%) · $0.09` readout
|
|
14
14
|
// lives. Additive multi-render only; `sidebar_footer` (single_winner,
|
|
15
15
|
// replaces name/version) is never used.
|
|
16
16
|
//
|
|
@@ -59,9 +59,19 @@ import type {
|
|
|
59
59
|
TuiTheme,
|
|
60
60
|
} from "@opencode-ai/plugin/tui";
|
|
61
61
|
import { For, Show, createEffect, createSignal } from "solid-js";
|
|
62
|
-
import
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
import {
|
|
63
|
+
extractSnapshotFromApiPayload,
|
|
64
|
+
formatResetDuration,
|
|
65
|
+
isRecord,
|
|
66
|
+
mockSnapshot,
|
|
67
|
+
readAuthJsonApiKey,
|
|
68
|
+
toNonEmptyString,
|
|
69
|
+
unavailableSnapshot,
|
|
70
|
+
} from "./shared.js";
|
|
71
|
+
import type { UsageSnapshot } from "./shared.js";
|
|
72
|
+
|
|
73
|
+
// Re-exported so `dist/tui.js` keeps the helper surface used by tests.
|
|
74
|
+
export { formatResetDuration };
|
|
65
75
|
|
|
66
76
|
// ---------------------------------------------------------------------------
|
|
67
77
|
// Constants
|
|
@@ -77,20 +87,9 @@ const KV_COLLAPSED_STATUSLINE_KEY = "collapsed_statusline";
|
|
|
77
87
|
const KV_COLLAPSED_LEGACY_KEY = "collapsed";
|
|
78
88
|
const LOG_SERVICE = "oc-go-usage-display";
|
|
79
89
|
|
|
80
|
-
const CONFIG_DIR = path.join(os.homedir(), ".config", "opencode");
|
|
81
|
-
|
|
82
|
-
function dataShareAuthPath(): string {
|
|
83
|
-
const xdgDataHome = toNonEmptyString(process.env.XDG_DATA_HOME);
|
|
84
|
-
if (xdgDataHome) return path.join(xdgDataHome, "opencode", "auth.json");
|
|
85
|
-
return path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function authJsonPaths(): string[] {
|
|
89
|
-
return [dataShareAuthPath(), path.join(CONFIG_DIR, "auth.json")];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
90
|
// ---------------------------------------------------------------------------
|
|
93
|
-
// Trusted types (parsed at the boundary, trusted internally
|
|
91
|
+
// Trusted types (parsed at the boundary, trusted internally; usage shapes
|
|
92
|
+
// live in `./shared.js` so server and TUI parse identically)
|
|
94
93
|
// ---------------------------------------------------------------------------
|
|
95
94
|
|
|
96
95
|
type DisplayMode = "sidebar" | "statusline" | "both";
|
|
@@ -100,21 +99,6 @@ type SurfaceSelection = {
|
|
|
100
99
|
statusline: boolean;
|
|
101
100
|
};
|
|
102
101
|
|
|
103
|
-
type UsageWindow = {
|
|
104
|
-
percent: number;
|
|
105
|
-
resetInSec: number | null;
|
|
106
|
-
resetText: string | null;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
type UsageSnapshot = {
|
|
110
|
-
rolling: UsageWindow | null;
|
|
111
|
-
weekly: UsageWindow | null;
|
|
112
|
-
monthly: UsageWindow | null;
|
|
113
|
-
source: "api" | "mock" | "unavailable";
|
|
114
|
-
fetchedAt: number;
|
|
115
|
-
apiError?: string;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
102
|
type UsageRow = {
|
|
119
103
|
label: string;
|
|
120
104
|
value: string;
|
|
@@ -124,30 +108,15 @@ type UsageRow = {
|
|
|
124
108
|
// Small pure helpers
|
|
125
109
|
// ---------------------------------------------------------------------------
|
|
126
110
|
|
|
127
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
128
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function toFiniteNumber(value: unknown): number | null {
|
|
132
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
133
|
-
return value;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function toNonEmptyString(value: unknown): string | null {
|
|
137
|
-
if (typeof value !== "string") return null;
|
|
138
|
-
const trimmed = value.trim();
|
|
139
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
111
|
function isDisplayMode(value: unknown): value is DisplayMode {
|
|
143
112
|
return value === "sidebar" || value === "statusline" || value === "both";
|
|
144
113
|
}
|
|
145
114
|
|
|
146
|
-
function surfaceSelectionFromDisplayMode(mode: DisplayMode): SurfaceSelection {
|
|
115
|
+
export function surfaceSelectionFromDisplayMode(mode: DisplayMode): SurfaceSelection {
|
|
147
116
|
return { sidebar: mode !== "statusline", statusline: mode !== "sidebar" };
|
|
148
117
|
}
|
|
149
118
|
|
|
150
|
-
function parseBooleanFlag(value: unknown): boolean | null {
|
|
119
|
+
export function parseBooleanFlag(value: unknown): boolean | null {
|
|
151
120
|
if (typeof value === "boolean") return value;
|
|
152
121
|
if (typeof value === "number") {
|
|
153
122
|
if (value === 1) return true;
|
|
@@ -161,27 +130,18 @@ function parseBooleanFlag(value: unknown): boolean | null {
|
|
|
161
130
|
return null;
|
|
162
131
|
}
|
|
163
132
|
|
|
164
|
-
function
|
|
165
|
-
if (totalSec === null || !Number.isFinite(totalSec) || totalSec < 0) return null;
|
|
166
|
-
const sec = Math.floor(totalSec);
|
|
167
|
-
const hours = Math.floor(sec / 3600);
|
|
168
|
-
const minutes = Math.floor((sec % 3600) / 60);
|
|
169
|
-
if (hours > 0) return `${hours}h${minutes}m`;
|
|
170
|
-
if (minutes > 0) return `${minutes}m`;
|
|
171
|
-
return `${sec}s`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function isSnapshotEmpty(snapshot: UsageSnapshot): boolean {
|
|
133
|
+
export function isSnapshotEmpty(snapshot: UsageSnapshot): boolean {
|
|
175
134
|
return snapshot.rolling === null && snapshot.weekly === null && snapshot.monthly === null;
|
|
176
135
|
}
|
|
177
136
|
|
|
178
|
-
function formatCompactLine(snapshot: UsageSnapshot): string {
|
|
137
|
+
export function formatCompactLine(snapshot: UsageSnapshot): string {
|
|
179
138
|
const rolling = snapshot.rolling === null ? "5h n/a" : `5h ${snapshot.rolling.percent}%`;
|
|
180
139
|
const weekly = snapshot.weekly === null ? "7d n/a" : `7d ${snapshot.weekly.percent}%`;
|
|
181
|
-
|
|
140
|
+
const monthly = snapshot.monthly === null ? "30d n/a" : `30d ${snapshot.monthly.percent}%`;
|
|
141
|
+
return `Go ${rolling} | ${weekly} | ${monthly}`;
|
|
182
142
|
}
|
|
183
143
|
|
|
184
|
-
function buildUsageRows(snapshot: UsageSnapshot): UsageRow[] {
|
|
144
|
+
export function buildUsageRows(snapshot: UsageSnapshot): UsageRow[] {
|
|
185
145
|
const rows: UsageRow[] = [];
|
|
186
146
|
if (snapshot.rolling !== null) {
|
|
187
147
|
const reset = formatResetDuration(snapshot.rolling.resetInSec) ?? snapshot.rolling.resetText;
|
|
@@ -256,66 +216,6 @@ function migrateLegacyCollapsedFlag(api: TuiPluginApi): void {
|
|
|
256
216
|
// Data boundary (auth.json -> Bearer usage fetch; throws nothing)
|
|
257
217
|
// ---------------------------------------------------------------------------
|
|
258
218
|
|
|
259
|
-
function readAuthJsonApiKey(): string | null {
|
|
260
|
-
for (const authPath of authJsonPaths()) {
|
|
261
|
-
let raw: string;
|
|
262
|
-
try {
|
|
263
|
-
raw = fs.readFileSync(authPath, "utf8");
|
|
264
|
-
} catch {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
let parsed: unknown;
|
|
268
|
-
try {
|
|
269
|
-
parsed = JSON.parse(raw);
|
|
270
|
-
} catch {
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
if (!isRecord(parsed)) continue;
|
|
274
|
-
const goEntry = parsed["opencode-go"];
|
|
275
|
-
const fallbackEntry = parsed["opencode"];
|
|
276
|
-
const goKey = isRecord(goEntry) ? toNonEmptyString(goEntry.key) : null;
|
|
277
|
-
if (goKey) return goKey;
|
|
278
|
-
const fallbackKey = isRecord(fallbackEntry) ? toNonEmptyString(fallbackEntry.key) : null;
|
|
279
|
-
if (fallbackKey) return fallbackKey;
|
|
280
|
-
}
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function extractWindow(candidate: unknown): UsageWindow | null {
|
|
285
|
-
if (!isRecord(candidate)) return null;
|
|
286
|
-
const percent = toFiniteNumber(
|
|
287
|
-
candidate.percent ??
|
|
288
|
-
candidate.usagePercent ??
|
|
289
|
-
candidate.usedPercent ??
|
|
290
|
-
candidate.value ??
|
|
291
|
-
candidate.usage,
|
|
292
|
-
);
|
|
293
|
-
if (percent === null) return null;
|
|
294
|
-
return {
|
|
295
|
-
percent: Math.round(percent),
|
|
296
|
-
resetInSec: toFiniteNumber(candidate.resetInSec ?? candidate.resetInSeconds ?? null),
|
|
297
|
-
resetText: toNonEmptyString(candidate.resetText ?? candidate.reset ?? null),
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function extractSnapshotFromApiPayload(payload: unknown): UsageSnapshot | null {
|
|
302
|
-
if (!isRecord(payload)) return null;
|
|
303
|
-
const containers: unknown[] = [payload];
|
|
304
|
-
for (const key of ["usage", "data", "go"]) {
|
|
305
|
-
if (isRecord(payload[key])) containers.push(payload[key]);
|
|
306
|
-
}
|
|
307
|
-
for (const container of containers) {
|
|
308
|
-
if (!isRecord(container)) continue;
|
|
309
|
-
const rolling = extractWindow(container.rolling ?? container.rollingUsage ?? container["5h"]);
|
|
310
|
-
const weekly = extractWindow(container.weekly ?? container.weeklyUsage ?? container["7d"]);
|
|
311
|
-
const monthly = extractWindow(container.monthly ?? container.monthlyUsage ?? container["30d"]);
|
|
312
|
-
if (rolling !== null || weekly !== null || monthly !== null) {
|
|
313
|
-
return { rolling, weekly, monthly, source: "api", fetchedAt: Date.now() };
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
219
|
async function fetchJsonWithTimeout(url: string, apiKey: string): Promise<unknown | null> {
|
|
320
220
|
const controller = new AbortController();
|
|
321
221
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
@@ -338,16 +238,6 @@ async function fetchJsonWithTimeout(url: string, apiKey: string): Promise<unknow
|
|
|
338
238
|
}
|
|
339
239
|
}
|
|
340
240
|
|
|
341
|
-
function mockSnapshot(): UsageSnapshot {
|
|
342
|
-
return {
|
|
343
|
-
rolling: { percent: 42, resetInSec: 7543, resetText: null },
|
|
344
|
-
weekly: { percent: 15, resetInSec: null, resetText: null },
|
|
345
|
-
monthly: { percent: 61, resetInSec: null, resetText: null },
|
|
346
|
-
source: "mock",
|
|
347
|
-
fetchedAt: Date.now(),
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
241
|
async function loadUsageSnapshot(): Promise<UsageSnapshot | null> {
|
|
352
242
|
if (process.env.OPENCODE_GO_MOCK === "1") return mockSnapshot();
|
|
353
243
|
|
|
@@ -356,7 +246,11 @@ async function loadUsageSnapshot(): Promise<UsageSnapshot | null> {
|
|
|
356
246
|
|
|
357
247
|
const payload = await fetchJsonWithTimeout(API_USAGE_URL, apiKey);
|
|
358
248
|
if (payload === null) return null;
|
|
359
|
-
|
|
249
|
+
// Mirror the server: a rejected key is a distinct unavailable snapshot
|
|
250
|
+
// (surfaced via `apiError`) rather than a silent null.
|
|
251
|
+
if (isRecord(payload) && payload.__rejected === true) {
|
|
252
|
+
return unavailableSnapshot("API key rejected (401/403)");
|
|
253
|
+
}
|
|
360
254
|
return extractSnapshotFromApiPayload(payload);
|
|
361
255
|
}
|
|
362
256
|
|
|
@@ -432,7 +326,10 @@ const goUsageTui: TuiPlugin = async (api, options) => {
|
|
|
432
326
|
createEffect(() => {
|
|
433
327
|
const snapshot = usageSnapshot();
|
|
434
328
|
if (snapshot !== null && snapshot.source === "unavailable") {
|
|
435
|
-
void logUsageError(
|
|
329
|
+
void logUsageError(
|
|
330
|
+
api,
|
|
331
|
+
snapshot.apiError ? `Go usage unavailable (${snapshot.apiError})` : "Go usage snapshot unavailable",
|
|
332
|
+
);
|
|
436
333
|
}
|
|
437
334
|
});
|
|
438
335
|
return (
|
|
@@ -474,6 +371,10 @@ const goUsageTui: TuiPlugin = async (api, options) => {
|
|
|
474
371
|
}
|
|
475
372
|
|
|
476
373
|
if (surfaces.sidebar) {
|
|
374
|
+
// Host-owned slot: `register` returns an id but the SDK exposes no
|
|
375
|
+
// unregister API, so there is nothing to dispose here (the slot dies
|
|
376
|
+
// with the host). Interval/event/command teardown below is the full
|
|
377
|
+
// dispose path.
|
|
477
378
|
api.slots.register({
|
|
478
379
|
order: SLOT_ORDER,
|
|
479
380
|
slots: {
|
|
@@ -488,6 +389,8 @@ const goUsageTui: TuiPlugin = async (api, options) => {
|
|
|
488
389
|
}
|
|
489
390
|
|
|
490
391
|
if (surfaces.statusline) {
|
|
392
|
+
// Host-owned slot (see sidebar note above): no unregister API exists,
|
|
393
|
+
// so disposal is a no-op for slots.
|
|
491
394
|
api.slots.register({
|
|
492
395
|
order: SLOT_ORDER,
|
|
493
396
|
slots: {
|
|
@@ -500,7 +403,9 @@ const goUsageTui: TuiPlugin = async (api, options) => {
|
|
|
500
403
|
});
|
|
501
404
|
}
|
|
502
405
|
|
|
503
|
-
|
|
406
|
+
// `api.command` is a deprecated legacy shim that hosts may omit; guard so
|
|
407
|
+
// the plugin still initializes and disposes safely without it.
|
|
408
|
+
const unregisterToggleCommand = api.command?.register(() => [
|
|
504
409
|
{
|
|
505
410
|
title: "Go usage: toggle sidebar",
|
|
506
411
|
value: "oc-go-usage-display.toggle-sidebar",
|
|
@@ -513,7 +418,7 @@ const goUsageTui: TuiPlugin = async (api, options) => {
|
|
|
513
418
|
category: "Go",
|
|
514
419
|
onSelect: () => toggleStatuslineCollapsed(),
|
|
515
420
|
},
|
|
516
|
-
]);
|
|
421
|
+
]) ?? (() => {});
|
|
517
422
|
|
|
518
423
|
const unsubscribeSession = api.event.on("session.updated", () => {
|
|
519
424
|
void refreshUsage();
|