oc-go-usage-display 1.0.2 → 2.0.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 +249 -68
- package/bin/lib.js +433 -29
- package/bin/oc-go-usage-display-init.js +42 -13
- package/bin/oc-go-usage-display-remove.js +38 -0
- package/bin/oc-go-usage-display-show.js +47 -39
- package/bin/oc-go-usage-display-status.js +33 -42
- package/bin/oc-go-usage-display-update.js +27 -14
- package/dist/helpers.d.ts +116 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +568 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +5 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +258 -278
- package/dist/index.js.map +1 -1
- package/dist/plugins/oc-go-usage-display.kilo.ts +678 -0
- package/dist/plugins/oc-go-usage-display.kilo.tsx +1272 -0
- package/dist/plugins/oc-go-usage-display.ts +678 -0
- package/dist/plugins/oc-go-usage-display.tsx +1018 -0
- package/dist/shared.d.ts +96 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +575 -0
- package/dist/shared.js.map +1 -0
- package/dist/tui-shared.d.ts +109 -0
- package/dist/tui-shared.d.ts.map +1 -0
- package/dist/tui-shared.js +372 -0
- package/dist/tui-shared.js.map +1 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +226 -377
- package/dist/tui.js.map +1 -1
- package/dist/tui.kilo.d.ts +8 -0
- package/dist/tui.kilo.d.ts.map +1 -0
- package/dist/tui.kilo.js +502 -0
- package/dist/tui.kilo.js.map +1 -0
- package/package.json +47 -13
- package/src/helpers.ts +733 -0
- package/src/index.ts +331 -304
- package/src/shared.ts +696 -0
- package/src/tui-shared.tsx +705 -0
- package/src/tui.kilo.tsx +816 -0
- package/src/tui.tsx +356 -465
package/dist/tui.js
CHANGED
|
@@ -1,419 +1,268 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/solid/jsx-runtime";
|
|
2
|
-
import { For, Show,
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
const
|
|
10
|
-
const POLL_INTERVAL_MS = 60_000;
|
|
11
|
-
const FETCH_TIMEOUT_MS = 10_000;
|
|
2
|
+
import { For, Show, createMemo, createSignal } from "solid-js";
|
|
3
|
+
import { buildGoModelFooters, buildModelMixSummary, relevantReset, formatPercentCell, meterSeverityForPercent, modelDisplayName, shortModelName, weightGoModels, } from "./helpers.js";
|
|
4
|
+
import { aggregateModelUsageFromMessages, errorMessage, MODELS_EMPTY_LABEL, OPENCODE_MODEL_NAME_MAX_CHARS, TOP_GO_MODELS_LABEL, } from "./shared.js";
|
|
5
|
+
import { CollapsibleSection, GoStatusline, GoUsageBlock, MeterBar, createCollapseState, createUsageStore, isGoUsageProvider, logUsageError, makeProviderResolver, readModelDisplayName, reactiveChild, resolveSurfaceSelection, } from "./tui-shared.js";
|
|
6
|
+
import { EVENT_TTL_MS, POLL_INTERVAL_MS } from "./tui-shared.js";
|
|
7
|
+
// This entry is the opencode build, so it resolves `OPENCODE_OC_GO_*` and never
|
|
8
|
+
// reads a Kilo-prefixed name.
|
|
9
|
+
const HOST = "opencode";
|
|
12
10
|
const SLOT_ORDER = 50;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
if (xdgDataHome)
|
|
22
|
-
return path.join(xdgDataHome, "opencode", "auth.json");
|
|
23
|
-
return path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
|
|
24
|
-
}
|
|
25
|
-
function authJsonPaths() {
|
|
26
|
-
return [dataShareAuthPath(), path.join(CONFIG_DIR, "auth.json")];
|
|
27
|
-
}
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Small pure helpers
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
function isRecord(value) {
|
|
32
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
-
}
|
|
34
|
-
function toFiniteNumber(value) {
|
|
35
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
36
|
-
return null;
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
function toNonEmptyString(value) {
|
|
40
|
-
if (typeof value !== "string")
|
|
41
|
-
return null;
|
|
42
|
-
const trimmed = value.trim();
|
|
43
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
44
|
-
}
|
|
45
|
-
function isDisplayMode(value) {
|
|
46
|
-
return value === "sidebar" || value === "statusline" || value === "both";
|
|
47
|
-
}
|
|
48
|
-
function surfaceSelectionFromDisplayMode(mode) {
|
|
49
|
-
return { sidebar: mode !== "statusline", statusline: mode !== "sidebar" };
|
|
50
|
-
}
|
|
51
|
-
function parseBooleanFlag(value) {
|
|
52
|
-
if (typeof value === "boolean")
|
|
53
|
-
return value;
|
|
54
|
-
if (typeof value === "number") {
|
|
55
|
-
if (value === 1)
|
|
56
|
-
return true;
|
|
57
|
-
if (value === 0)
|
|
58
|
-
return false;
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
if (typeof value !== "string")
|
|
62
|
-
return null;
|
|
63
|
-
const normalized = value.trim().toLowerCase();
|
|
64
|
-
if (normalized === "1" || normalized === "true")
|
|
65
|
-
return true;
|
|
66
|
-
if (normalized === "0" || normalized === "false")
|
|
67
|
-
return false;
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
function formatResetDuration(totalSec) {
|
|
71
|
-
if (totalSec === null || !Number.isFinite(totalSec) || totalSec < 0)
|
|
72
|
-
return null;
|
|
73
|
-
const sec = Math.floor(totalSec);
|
|
74
|
-
const hours = Math.floor(sec / 3600);
|
|
75
|
-
const minutes = Math.floor((sec % 3600) / 60);
|
|
76
|
-
if (hours > 0)
|
|
77
|
-
return `${hours}h${minutes}m`;
|
|
78
|
-
if (minutes > 0)
|
|
79
|
-
return `${minutes}m`;
|
|
80
|
-
return `${sec}s`;
|
|
81
|
-
}
|
|
82
|
-
function isSnapshotEmpty(snapshot) {
|
|
83
|
-
return snapshot.rolling === null && snapshot.weekly === null && snapshot.monthly === null;
|
|
84
|
-
}
|
|
85
|
-
function formatCompactLine(snapshot) {
|
|
86
|
-
const rolling = snapshot.rolling === null ? "5h n/a" : `5h ${snapshot.rolling.percent}%`;
|
|
87
|
-
const weekly = snapshot.weekly === null ? "7d n/a" : `7d ${snapshot.weekly.percent}%`;
|
|
88
|
-
return `Go ${rolling} | ${weekly}`;
|
|
89
|
-
}
|
|
90
|
-
function buildUsageRows(snapshot) {
|
|
91
|
-
const rows = [];
|
|
92
|
-
if (snapshot.rolling !== null) {
|
|
93
|
-
const reset = formatResetDuration(snapshot.rolling.resetInSec) ?? snapshot.rolling.resetText;
|
|
94
|
-
rows.push({ label: "5h", value: `${snapshot.rolling.percent}%${reset ? ` · resets ${reset}` : ""}` });
|
|
95
|
-
}
|
|
96
|
-
if (snapshot.weekly !== null)
|
|
97
|
-
rows.push({ label: "7d", value: `${snapshot.weekly.percent}%` });
|
|
98
|
-
if (snapshot.monthly !== null)
|
|
99
|
-
rows.push({ label: "30d", value: `${snapshot.monthly.percent}%` });
|
|
100
|
-
return rows;
|
|
101
|
-
}
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// Settings boundary (new toggles > legacy display; tui.json options > env >
|
|
104
|
-
// api.kv > default both on)
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
function resolveSurfaceSelection(options, api) {
|
|
107
|
-
if (options !== undefined) {
|
|
108
|
-
const sidebarOption = parseBooleanFlag(options.sidebar);
|
|
109
|
-
const statuslineOption = parseBooleanFlag(options.statusline);
|
|
110
|
-
if (sidebarOption !== null || statuslineOption !== null) {
|
|
111
|
-
return { sidebar: sidebarOption ?? true, statusline: statuslineOption ?? true };
|
|
112
|
-
}
|
|
113
|
-
if (isDisplayMode(options.display))
|
|
114
|
-
return surfaceSelectionFromDisplayMode(options.display);
|
|
115
|
-
}
|
|
116
|
-
const sidebarEnv = parseBooleanFlag(process.env.OPENCODE_GO_SIDEBAR);
|
|
117
|
-
const statuslineEnv = parseBooleanFlag(process.env.OPENCODE_GO_STATUSLINE);
|
|
118
|
-
if (sidebarEnv !== null || statuslineEnv !== null) {
|
|
119
|
-
return { sidebar: sidebarEnv ?? true, statusline: statuslineEnv ?? true };
|
|
120
|
-
}
|
|
121
|
-
const displayEnv = toNonEmptyString(process.env.OPENCODE_GO_DISPLAY);
|
|
122
|
-
if (displayEnv !== null && isDisplayMode(displayEnv)) {
|
|
123
|
-
return surfaceSelectionFromDisplayMode(displayEnv);
|
|
124
|
-
}
|
|
11
|
+
// The factory body lives here so the exported `goUsageTui` can wrap the whole
|
|
12
|
+
// initialization in a single fail-safe boundary. A throwing factory would
|
|
13
|
+
// destabilize the host's plugin load, so it must never reject.
|
|
14
|
+
async function initializeTui(api, options) {
|
|
15
|
+
const surfaces = resolveSurfaceSelection(options, api, HOST);
|
|
16
|
+
const usageStore = createUsageStore(api, HOST);
|
|
17
|
+
const collapse = createCollapseState(api);
|
|
18
|
+
const [activeProviderId, setActiveProviderId] = createSignal(undefined);
|
|
125
19
|
try {
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
return { sidebar: true, statusline: true };
|
|
134
|
-
}
|
|
135
|
-
function readCollapsedFlag(api, key) {
|
|
136
|
-
try {
|
|
137
|
-
return api.kv.get(key, false) === true;
|
|
20
|
+
const modelString = api.state?.config?.model;
|
|
21
|
+
if (typeof modelString === "string" && modelString.length > 0) {
|
|
22
|
+
const providerPart = modelString.split("/")[0];
|
|
23
|
+
if (providerPart !== undefined && providerPart.length > 0)
|
|
24
|
+
setActiveProviderId(providerPart);
|
|
25
|
+
}
|
|
138
26
|
}
|
|
139
27
|
catch {
|
|
140
|
-
|
|
28
|
+
// State may not be ready; fall through to the event signal.
|
|
141
29
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
30
|
+
const resolveActiveProviderId = makeProviderResolver(api.state, activeProviderId);
|
|
31
|
+
// The host's message store is where the per-model mix comes from, and it
|
|
32
|
+
// changes without the plan changing. Bumping this token on the same events
|
|
33
|
+
// that refresh the plan keeps the mix from going stale behind a host that
|
|
34
|
+
// reuses the element it was handed instead of re-invoking this subtree (see
|
|
35
|
+
// the reactivity note in `tui-shared`).
|
|
36
|
+
const [stateToken, setStateToken] = createSignal(0);
|
|
37
|
+
function readSessionMessages(sessionId) {
|
|
38
|
+
if (sessionId.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
try {
|
|
41
|
+
return api.state.session.messages(sessionId);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
147
46
|
}
|
|
148
|
-
|
|
149
|
-
|
|
47
|
+
// One model row: the display name the catalog gives it, then its weight bar
|
|
48
|
+
// and percent pushed to the right edge so the bars line up down the block.
|
|
49
|
+
function WeightedModelRow(props) {
|
|
50
|
+
const rounded = createMemo(() => Math.round(props.model.share));
|
|
51
|
+
const name = createMemo(() => modelDisplayName(readModelDisplayName(api, props.model.providerID, props.model.modelID), props.model.modelID, OPENCODE_MODEL_NAME_MAX_CHARS));
|
|
52
|
+
return (_jsxs("box", { flexDirection: "row", gap: 1, flexShrink: 0, children: [_jsx("text", { fg: props.theme.current.text, wrapMode: "none", flexShrink: 0, children: name() }), _jsx(MeterBar, { theme: props.theme, percent: props.model.share, severity: meterSeverityForPercent(rounded()) }), _jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", flexShrink: 0, children: formatPercentCell(rounded()) })] }));
|
|
150
53
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
54
|
+
// The model mix. Collapsed by default: the one-line summary is the cheap
|
|
55
|
+
// answer, and this block already leads a sidebar that the host's own Context
|
|
56
|
+
// panel follows. The count in the header is every Go model the session used,
|
|
57
|
+
// not the three rows below it, so a truncated ranking says so.
|
|
58
|
+
function TopGoModelsSection(props) {
|
|
59
|
+
const [isExpanded, setIsExpanded] = createSignal(false);
|
|
60
|
+
const weights = createMemo(() => weightGoModels(props.usage()?.models ?? []));
|
|
61
|
+
const rows = () => weights().models;
|
|
62
|
+
// The collapsed line names the same models the expanded rows do, in as few
|
|
63
|
+
// cells as the sidebar has.
|
|
64
|
+
const mix = createMemo(() => buildModelMixSummary(rows().map((model) => ({
|
|
65
|
+
name: shortModelName(modelDisplayName(readModelDisplayName(api, model.providerID, model.modelID), model.modelID, OPENCODE_MODEL_NAME_MAX_CHARS)),
|
|
66
|
+
share: model.share,
|
|
67
|
+
}))));
|
|
68
|
+
const body = createMemo(() => (_jsxs("box", { flexDirection: "column", gap: 1, paddingTop: 1, paddingLeft: 2, children: [_jsx(For, { each: rows(), children: (model) => _jsx(WeightedModelRow, { theme: props.theme, model: model }) }), _jsx(For, { each: buildGoModelFooters(weights()), children: (line) => (_jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: line })) })] })));
|
|
69
|
+
// The whole section is one function child, because two rules apply here and
|
|
70
|
+
// both are about the same thing: the host reuses the element it was handed,
|
|
71
|
+
// so a plain value is read once at mount. That is why the conditions below
|
|
72
|
+
// are read INSIDE this memo (re-running it is what re-reads them) and why
|
|
73
|
+
// `Show` is given a value rather than an accessor -- solid's `Show` memoizes
|
|
74
|
+
// `when` and never calls it, so an accessor here is a function object, and a
|
|
75
|
+
// function object is always truthy. The real host caught exactly that: a
|
|
76
|
+
// session with no assistant messages rendered `Top Go models (0)` with a
|
|
77
|
+
// caret that opened onto nothing.
|
|
78
|
+
const section = createMemo(() => {
|
|
79
|
+
// No models yet is not a fold: there is nothing to fold, and a caret that
|
|
80
|
+
// opens onto an empty box reads as a broken section.
|
|
81
|
+
if (weights().goModels === 0) {
|
|
82
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: props.theme.current.text, wrapMode: "none", children: _jsx("b", { children: TOP_GO_MODELS_LABEL }) }), _jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: MODELS_EMPTY_LABEL })] }));
|
|
83
|
+
}
|
|
84
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx(CollapsibleSection, { theme: props.theme, label: TOP_GO_MODELS_LABEL, count: () => weights().goModels, expanded: isExpanded, onToggle: () => setIsExpanded((current) => !current), body: body }), _jsx(Show, { when: !isExpanded(), children: _jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", paddingLeft: 2, children: reactiveChild(mix) }) })] }));
|
|
85
|
+
});
|
|
86
|
+
return _jsx("box", { flexDirection: "column", children: reactiveChild(section) });
|
|
157
87
|
}
|
|
158
|
-
|
|
159
|
-
|
|
88
|
+
function GoSidebarPanel(props) {
|
|
89
|
+
const planResetLine = createMemo(() => {
|
|
90
|
+
const snapshot = usageStore.snapshot();
|
|
91
|
+
if (snapshot === null || snapshot.source === "unavailable")
|
|
92
|
+
return null;
|
|
93
|
+
return relevantReset(snapshot);
|
|
94
|
+
});
|
|
95
|
+
const sessionUsage = createMemo(() => {
|
|
96
|
+
stateToken();
|
|
97
|
+
return aggregateModelUsageFromMessages(readSessionMessages(props.sessionId));
|
|
98
|
+
});
|
|
99
|
+
return (_jsxs("box", { flexDirection: "column", gap: 1, children: [_jsx(GoUsageBlock, { api: api, theme: props.theme, snapshot: usageStore.snapshot, resetLine: planResetLine }), _jsx(TopGoModelsSection, { theme: props.theme, usage: sessionUsage })] }));
|
|
160
100
|
}
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
for (const authPath of authJsonPaths()) {
|
|
167
|
-
let raw;
|
|
101
|
+
if (surfaces.sidebar) {
|
|
102
|
+
// Host-owned slot: `register` returns an id but the SDK exposes no
|
|
103
|
+
// unregister API, so there is nothing to dispose here (the slot dies
|
|
104
|
+
// with the host). Interval/event/command teardown below is the full
|
|
105
|
+
// dispose path.
|
|
168
106
|
try {
|
|
169
|
-
|
|
107
|
+
api.slots.register({
|
|
108
|
+
order: SLOT_ORDER,
|
|
109
|
+
slots: {
|
|
110
|
+
sidebar_content(ctx, props) {
|
|
111
|
+
// Individually guarded: a later render must never throw into the host.
|
|
112
|
+
try {
|
|
113
|
+
if (props.session_id.length === 0)
|
|
114
|
+
return null;
|
|
115
|
+
if (!isGoUsageProvider(resolveActiveProviderId(props.session_id)))
|
|
116
|
+
return null;
|
|
117
|
+
if (api.route.current.name !== "session")
|
|
118
|
+
return null;
|
|
119
|
+
if (collapse.isSidebarCollapsed())
|
|
120
|
+
return null;
|
|
121
|
+
return _jsx(GoSidebarPanel, { theme: ctx.theme, sessionId: props.session_id });
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
170
129
|
}
|
|
171
130
|
catch {
|
|
172
|
-
|
|
131
|
+
// Additive slot: a registration failure must not abort the plugin.
|
|
173
132
|
}
|
|
174
|
-
|
|
133
|
+
}
|
|
134
|
+
if (surfaces.statusline) {
|
|
135
|
+
// Host-owned slot (see sidebar note above): no unregister API exists,
|
|
136
|
+
// so disposal is a no-op for slots.
|
|
175
137
|
try {
|
|
176
|
-
|
|
138
|
+
api.slots.register({
|
|
139
|
+
order: SLOT_ORDER,
|
|
140
|
+
slots: {
|
|
141
|
+
session_prompt_right(_ctx, props) {
|
|
142
|
+
// Individually guarded: a later render must never throw into the host.
|
|
143
|
+
try {
|
|
144
|
+
if (props.session_id.length === 0)
|
|
145
|
+
return null;
|
|
146
|
+
if (!isGoUsageProvider(resolveActiveProviderId(props.session_id)))
|
|
147
|
+
return null;
|
|
148
|
+
if (collapse.isStatuslineCollapsed())
|
|
149
|
+
return null;
|
|
150
|
+
return _jsx(GoStatusline, { snapshot: usageStore.snapshot });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
});
|
|
177
158
|
}
|
|
178
159
|
catch {
|
|
179
|
-
|
|
160
|
+
// Additive slot: a registration failure must not abort the plugin.
|
|
180
161
|
}
|
|
181
|
-
if (!isRecord(parsed))
|
|
182
|
-
continue;
|
|
183
|
-
const goEntry = parsed["opencode-go"];
|
|
184
|
-
const fallbackEntry = parsed["opencode"];
|
|
185
|
-
const goKey = isRecord(goEntry) ? toNonEmptyString(goEntry.key) : null;
|
|
186
|
-
if (goKey)
|
|
187
|
-
return goKey;
|
|
188
|
-
const fallbackKey = isRecord(fallbackEntry) ? toNonEmptyString(fallbackEntry.key) : null;
|
|
189
|
-
if (fallbackKey)
|
|
190
|
-
return fallbackKey;
|
|
191
162
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return null;
|
|
213
|
-
const containers = [payload];
|
|
214
|
-
for (const key of ["usage", "data", "go"]) {
|
|
215
|
-
if (isRecord(payload[key]))
|
|
216
|
-
containers.push(payload[key]);
|
|
163
|
+
// `api.command` is a deprecated legacy shim that hosts may omit; guard so
|
|
164
|
+
// the plugin still initializes and disposes safely without it.
|
|
165
|
+
let unregisterToggleCommand = () => { };
|
|
166
|
+
try {
|
|
167
|
+
const unregister = api.command?.register(() => [
|
|
168
|
+
{
|
|
169
|
+
title: "Go usage: toggle sidebar",
|
|
170
|
+
value: "oc-go-usage-display.toggle-sidebar",
|
|
171
|
+
category: "Go",
|
|
172
|
+
onSelect: () => collapse.toggleSidebar(),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
title: "Go usage: toggle statusline",
|
|
176
|
+
value: "oc-go-usage-display.toggle-statusline",
|
|
177
|
+
category: "Go",
|
|
178
|
+
onSelect: () => collapse.toggleStatusline(),
|
|
179
|
+
},
|
|
180
|
+
]);
|
|
181
|
+
if (typeof unregister === "function")
|
|
182
|
+
unregisterToggleCommand = unregister;
|
|
217
183
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
continue;
|
|
221
|
-
const rolling = extractWindow(container.rolling ?? container.rollingUsage ?? container["5h"]);
|
|
222
|
-
const weekly = extractWindow(container.weekly ?? container.weeklyUsage ?? container["7d"]);
|
|
223
|
-
const monthly = extractWindow(container.monthly ?? container.monthlyUsage ?? container["30d"]);
|
|
224
|
-
if (rolling !== null || weekly !== null || monthly !== null) {
|
|
225
|
-
return { rolling, weekly, monthly, source: "api", fetchedAt: Date.now() };
|
|
226
|
-
}
|
|
184
|
+
catch {
|
|
185
|
+
// Legacy command registration is optional; ignore failures.
|
|
227
186
|
}
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
async function fetchJsonWithTimeout(url, apiKey) {
|
|
231
|
-
const controller = new AbortController();
|
|
232
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
187
|
+
let unsubscribeSession = () => { };
|
|
188
|
+
let unsubscribeMessage = () => { };
|
|
233
189
|
try {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
190
|
+
const unsubscribe = api.event.on("session.updated", (event) => {
|
|
191
|
+
const providerId = event?.properties?.info?.model?.providerID;
|
|
192
|
+
if (providerId !== undefined)
|
|
193
|
+
setActiveProviderId(providerId);
|
|
194
|
+
usageStore.refreshSafely(0);
|
|
237
195
|
});
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
if (!response.ok)
|
|
241
|
-
return null;
|
|
242
|
-
try {
|
|
243
|
-
return (await response.json());
|
|
244
|
-
}
|
|
245
|
-
catch {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
196
|
+
if (typeof unsubscribe === "function")
|
|
197
|
+
unsubscribeSession = unsubscribe;
|
|
248
198
|
}
|
|
249
199
|
catch {
|
|
250
|
-
|
|
200
|
+
// Event subscription is additive; a failure must not abort the plugin.
|
|
251
201
|
}
|
|
252
|
-
|
|
253
|
-
|
|
202
|
+
try {
|
|
203
|
+
const unsubscribe = api.event.on("message.updated", () => {
|
|
204
|
+
usageStore.refreshSafely(EVENT_TTL_MS);
|
|
205
|
+
// The model's share of the session changes with every finished message,
|
|
206
|
+
// not only when the plan does.
|
|
207
|
+
setStateToken((current) => current + 1);
|
|
208
|
+
});
|
|
209
|
+
if (typeof unsubscribe === "function")
|
|
210
|
+
unsubscribeMessage = unsubscribe;
|
|
254
211
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
weekly: { percent: 15, resetInSec: null, resetText: null },
|
|
260
|
-
monthly: { percent: 61, resetInSec: null, resetText: null },
|
|
261
|
-
source: "mock",
|
|
262
|
-
fetchedAt: Date.now(),
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
async function loadUsageSnapshot() {
|
|
266
|
-
if (process.env.OPENCODE_GO_MOCK === "1")
|
|
267
|
-
return mockSnapshot();
|
|
268
|
-
const apiKey = toNonEmptyString(process.env.OPENCODE_GO_API_KEY) ?? readAuthJsonApiKey();
|
|
269
|
-
if (apiKey === null)
|
|
270
|
-
return null;
|
|
271
|
-
const payload = await fetchJsonWithTimeout(API_USAGE_URL, apiKey);
|
|
272
|
-
if (payload === null)
|
|
273
|
-
return null;
|
|
274
|
-
if (isRecord(payload) && payload.__rejected === true)
|
|
275
|
-
return null;
|
|
276
|
-
return extractSnapshotFromApiPayload(payload);
|
|
277
|
-
}
|
|
278
|
-
async function logUsageError(api, message) {
|
|
212
|
+
catch {
|
|
213
|
+
// Event subscription is additive; a failure must not abort the plugin.
|
|
214
|
+
}
|
|
215
|
+
let pollTimer = null;
|
|
279
216
|
try {
|
|
280
|
-
|
|
217
|
+
pollTimer = setInterval(() => usageStore.refreshSafely(), POLL_INTERVAL_MS);
|
|
281
218
|
}
|
|
282
219
|
catch {
|
|
283
|
-
//
|
|
220
|
+
// No poll timer: the on-demand refresh below still runs.
|
|
284
221
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
let cachedAt = 0;
|
|
296
|
-
let refreshInFlight = false;
|
|
297
|
-
async function refreshUsage() {
|
|
298
|
-
if (refreshInFlight)
|
|
299
|
-
return;
|
|
300
|
-
if (Date.now() - cachedAt < POLL_INTERVAL_MS && usageSnapshot() !== null)
|
|
301
|
-
return;
|
|
302
|
-
refreshInFlight = true;
|
|
303
|
-
try {
|
|
304
|
-
const snapshot = await loadUsageSnapshot();
|
|
305
|
-
if (snapshot === null) {
|
|
306
|
-
if (usageSnapshot() === null) {
|
|
307
|
-
await logUsageError(api, "Go usage unavailable (not configured or request failed)");
|
|
222
|
+
try {
|
|
223
|
+
api.lifecycle.onDispose(() => {
|
|
224
|
+
// Each teardown step is isolated: one throwing unsubscribe must not
|
|
225
|
+
// prevent the rest (or leak into the host's dispose pass).
|
|
226
|
+
if (pollTimer !== null) {
|
|
227
|
+
try {
|
|
228
|
+
clearInterval(pollTimer);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// noop
|
|
308
232
|
}
|
|
309
|
-
return;
|
|
310
233
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
refreshInFlight = false;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
function toggleSidebarCollapsed() {
|
|
322
|
-
const next = !isSidebarCollapsed();
|
|
323
|
-
setIsSidebarCollapsed(next);
|
|
324
|
-
try {
|
|
325
|
-
api.kv.set(KV_COLLAPSED_SIDEBAR_KEY, next);
|
|
326
|
-
}
|
|
327
|
-
catch {
|
|
328
|
-
// Collapse state is best-effort persistence only.
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
function toggleStatuslineCollapsed() {
|
|
332
|
-
const next = !isStatuslineCollapsed();
|
|
333
|
-
setIsStatuslineCollapsed(next);
|
|
334
|
-
try {
|
|
335
|
-
api.kv.set(KV_COLLAPSED_STATUSLINE_KEY, next);
|
|
336
|
-
}
|
|
337
|
-
catch {
|
|
338
|
-
// Collapse state is best-effort persistence only.
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
function GoSidebarPanel(props) {
|
|
342
|
-
createEffect(() => {
|
|
343
|
-
const snapshot = usageSnapshot();
|
|
344
|
-
if (snapshot !== null && snapshot.source === "unavailable") {
|
|
345
|
-
void logUsageError(api, "Go usage snapshot unavailable");
|
|
234
|
+
for (const teardown of [unsubscribeSession, unsubscribeMessage, unregisterToggleCommand]) {
|
|
235
|
+
try {
|
|
236
|
+
teardown();
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// noop
|
|
240
|
+
}
|
|
346
241
|
}
|
|
347
242
|
});
|
|
348
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: props.theme.current.text, children: _jsx("b", { children: "Go Usage" }) }), _jsx(Show, { when: usageSnapshot(), fallback: _jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: "Go loading\u2026" }), children: (snapshot) => (_jsx(For, { each: buildUsageRows(snapshot()), children: (row) => (_jsxs("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: [row.label, " ", row.value] })) })) })] }));
|
|
349
243
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
244
|
+
catch {
|
|
245
|
+
// Lifecycle registration is best-effort; never leak a timer into the host.
|
|
246
|
+
if (pollTimer !== null) {
|
|
247
|
+
try {
|
|
248
|
+
clearInterval(pollTimer);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// noop
|
|
252
|
+
}
|
|
253
|
+
}
|
|
356
254
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (props.session_id.length === 0)
|
|
363
|
-
return null;
|
|
364
|
-
if (api.route.current.name !== "session")
|
|
365
|
-
return null;
|
|
366
|
-
if (isSidebarCollapsed())
|
|
367
|
-
return null;
|
|
368
|
-
return _jsx(GoSidebarPanel, { theme: ctx.theme });
|
|
369
|
-
},
|
|
370
|
-
},
|
|
371
|
-
});
|
|
255
|
+
await usageStore.refresh();
|
|
256
|
+
}
|
|
257
|
+
const goUsageTui = async (api, options) => {
|
|
258
|
+
try {
|
|
259
|
+
await initializeTui(api, options);
|
|
372
260
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
session_prompt_right(_ctx, props) {
|
|
378
|
-
if (props.session_id.length === 0)
|
|
379
|
-
return null;
|
|
380
|
-
if (isStatuslineCollapsed())
|
|
381
|
-
return null;
|
|
382
|
-
return _jsx(GoStatusline, {});
|
|
383
|
-
},
|
|
384
|
-
},
|
|
385
|
-
});
|
|
261
|
+
catch (error) {
|
|
262
|
+
// Best-effort and non-blocking: never delay plugin resolution on the host
|
|
263
|
+
// log and never let an initialization error reject into the host.
|
|
264
|
+
void logUsageError(api, `Go usage TUI failed to initialize: ${errorMessage(error)}; continuing without display`);
|
|
386
265
|
}
|
|
387
|
-
const unregisterToggleCommand = api.command.register(() => [
|
|
388
|
-
{
|
|
389
|
-
title: "Go usage: toggle sidebar",
|
|
390
|
-
value: "oc-go-usage-display.toggle-sidebar",
|
|
391
|
-
category: "Go",
|
|
392
|
-
onSelect: () => toggleSidebarCollapsed(),
|
|
393
|
-
},
|
|
394
|
-
{
|
|
395
|
-
title: "Go usage: toggle statusline",
|
|
396
|
-
value: "oc-go-usage-display.toggle-statusline",
|
|
397
|
-
category: "Go",
|
|
398
|
-
onSelect: () => toggleStatuslineCollapsed(),
|
|
399
|
-
},
|
|
400
|
-
]);
|
|
401
|
-
const unsubscribeSession = api.event.on("session.updated", () => {
|
|
402
|
-
void refreshUsage();
|
|
403
|
-
});
|
|
404
|
-
const unsubscribeMessage = api.event.on("message.updated", () => {
|
|
405
|
-
void refreshUsage();
|
|
406
|
-
});
|
|
407
|
-
const pollTimer = setInterval(() => {
|
|
408
|
-
void refreshUsage();
|
|
409
|
-
}, POLL_INTERVAL_MS);
|
|
410
|
-
api.lifecycle.onDispose(() => {
|
|
411
|
-
clearInterval(pollTimer);
|
|
412
|
-
unsubscribeSession();
|
|
413
|
-
unsubscribeMessage();
|
|
414
|
-
unregisterToggleCommand();
|
|
415
|
-
});
|
|
416
|
-
await refreshUsage();
|
|
417
266
|
};
|
|
418
267
|
export default { id: "oc-go-usage-display", tui: goUsageTui };
|
|
419
268
|
//# sourceMappingURL=tui.js.map
|