@slkiser/opencode-quota 3.10.1 → 3.11.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 +57 -815
- package/dist/lib/config.js +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/entries.d.ts +5 -0
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/google-agy-companion.d.ts +29 -0
- package/dist/lib/google-agy-companion.d.ts.map +1 -0
- package/dist/lib/google-agy-companion.js +289 -0
- package/dist/lib/google-agy-companion.js.map +1 -0
- package/dist/lib/google-agy.d.ts +52 -0
- package/dist/lib/google-agy.d.ts.map +1 -0
- package/dist/lib/google-agy.js +461 -0
- package/dist/lib/google-agy.js.map +1 -0
- package/dist/lib/google-antigravity-companion.d.ts.map +1 -1
- package/dist/lib/google-antigravity-companion.js +3 -1
- package/dist/lib/google-antigravity-companion.js.map +1 -1
- package/dist/lib/google-gemini-cli.js +4 -4
- package/dist/lib/google-gemini-cli.js.map +1 -1
- package/dist/lib/google.d.ts.map +1 -1
- package/dist/lib/google.js +55 -5
- package/dist/lib/google.js.map +1 -1
- package/dist/lib/grouped-entry-normalization.js +1 -1
- package/dist/lib/grouped-entry-normalization.js.map +1 -1
- package/dist/lib/init-installer.js +3 -3
- package/dist/lib/init-installer.js.map +1 -1
- package/dist/lib/maintainer-announcements.js +2 -2
- package/dist/lib/maintainer-announcements.js.map +1 -1
- package/dist/lib/provider-metadata.d.ts +1 -1
- package/dist/lib/provider-metadata.d.ts.map +1 -1
- package/dist/lib/provider-metadata.js +15 -0
- package/dist/lib/provider-metadata.js.map +1 -1
- package/dist/lib/quota-dialog-commands.js +1 -1
- package/dist/lib/quota-dialog-commands.js.map +1 -1
- package/dist/lib/quota-render-data.d.ts.map +1 -1
- package/dist/lib/quota-render-data.js +13 -0
- package/dist/lib/quota-render-data.js.map +1 -1
- package/dist/lib/quota-state.js +1 -1
- package/dist/lib/quota-state.js.map +1 -1
- package/dist/lib/quota-status.d.ts +1 -0
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +29 -0
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/toast-format-grouped.js +1 -1
- package/dist/lib/toast-format-grouped.js.map +1 -1
- package/dist/lib/tui-runtime.d.ts.map +1 -1
- package/dist/lib/tui-runtime.js +8 -6
- package/dist/lib/tui-runtime.js.map +1 -1
- package/dist/lib/types.d.ts +22 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +12 -3
- package/dist/lib/types.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +42 -4
- package/dist/plugin.js.map +1 -1
- package/dist/providers/google-agy.d.ts +3 -0
- package/dist/providers/google-agy.d.ts.map +1 -0
- package/dist/providers/google-agy.js +90 -0
- package/dist/providers/google-agy.js.map +1 -0
- package/dist/providers/google-antigravity.d.ts.map +1 -1
- package/dist/providers/google-antigravity.js +3 -1
- package/dist/providers/google-antigravity.js.map +1 -1
- package/dist/providers/minimax-coding-plan.d.ts +1 -1
- package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
- package/dist/providers/minimax-coding-plan.js +7 -4
- package/dist/providers/minimax-coding-plan.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +2 -0
- package/dist/providers/registry.js.map +1 -1
- package/dist/tui.js +782 -0
- package/package.json +8 -5
package/dist/tui.js
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
2
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
3
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
4
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
5
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
6
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
7
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
8
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
9
|
+
/** @jsxImportSource @opentui/solid */
|
|
10
|
+
|
|
11
|
+
import { Show, createEffect, createSignal, onCleanup } from "solid-js";
|
|
12
|
+
import { getCompactStatusText, getHomeBottomAnnouncementText, getSidebarPanelLines, getSidebarPanelLinesExpanded, shouldRenderCompactStatus, shouldRenderHomeBottom, shouldRenderSidebarPanel } from "./lib/tui-panel-state.js";
|
|
13
|
+
import { getSidebarBodyLineColor } from "./lib/tui-line-style.js";
|
|
14
|
+
import { createTuiQuotaClient, getTuiRuntimeRootHints, getTuiSessionModelMeta, loadTuiHomeBottomStatus, normalizeTuiSessionID, loadTuiSessionQuotaSurfaces, resolveTuiSurfaceRegistration, writeTuiQuotaExportIfEnabled } from "./lib/tui-runtime.js";
|
|
15
|
+
import { QUOTA_DIALOG_COMMANDS, buildQuotaDialogCommandOutput } from "./lib/quota-dialog-commands.js";
|
|
16
|
+
const id = "@slkiser/opencode-quota";
|
|
17
|
+
// Place Quota near the top so variable-height built-in sections
|
|
18
|
+
// (MCP/LSP/Todo/Files) do not push it below the visible fold.
|
|
19
|
+
const SIDEBAR_ORDER = 150;
|
|
20
|
+
const COMPACT_ORDER = 90;
|
|
21
|
+
const REFRESH_INTERVAL_MS = 60_000;
|
|
22
|
+
const EVENT_REFRESH_DELAYS_MS = [150, 600];
|
|
23
|
+
const MOUNT_RECOVERY_DELAYS_MS = [500, 1_500, 4_000];
|
|
24
|
+
const sessionResources = new WeakMap();
|
|
25
|
+
const homeResources = new WeakMap();
|
|
26
|
+
function getSessionResourceMap(api) {
|
|
27
|
+
const existing = sessionResources.get(api);
|
|
28
|
+
if (existing) return existing;
|
|
29
|
+
const next = new Map();
|
|
30
|
+
sessionResources.set(api, next);
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
function createSessionQuotaResource(api, sessionID) {
|
|
34
|
+
const [sidebar, setSidebar] = createSignal({
|
|
35
|
+
status: "loading",
|
|
36
|
+
lines: []
|
|
37
|
+
});
|
|
38
|
+
const [compact, setCompact] = createSignal({
|
|
39
|
+
status: "loading"
|
|
40
|
+
});
|
|
41
|
+
let refCount = 0;
|
|
42
|
+
let disposed = false;
|
|
43
|
+
let loadVersion = 0;
|
|
44
|
+
let inFlight = false;
|
|
45
|
+
let queued = false;
|
|
46
|
+
const timers = new Set();
|
|
47
|
+
const reload = () => {
|
|
48
|
+
if (disposed) return;
|
|
49
|
+
if (inFlight) {
|
|
50
|
+
queued = true;
|
|
51
|
+
loadVersion += 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
inFlight = true;
|
|
55
|
+
const currentVersion = ++loadVersion;
|
|
56
|
+
void loadTuiSessionQuotaSurfaces({
|
|
57
|
+
api,
|
|
58
|
+
sessionID
|
|
59
|
+
}).then(next => {
|
|
60
|
+
if (disposed || currentVersion !== loadVersion) return;
|
|
61
|
+
setSidebar(next.sidebar);
|
|
62
|
+
setCompact(next.compact);
|
|
63
|
+
}).catch(() => {
|
|
64
|
+
if (disposed || currentVersion !== loadVersion) return;
|
|
65
|
+
}).finally(() => {
|
|
66
|
+
if (disposed) return;
|
|
67
|
+
inFlight = false;
|
|
68
|
+
if (queued) {
|
|
69
|
+
queued = false;
|
|
70
|
+
reload();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const queueRefresh = delay => {
|
|
75
|
+
if (disposed) return;
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
timers.delete(timer);
|
|
78
|
+
reload();
|
|
79
|
+
}, delay);
|
|
80
|
+
timers.add(timer);
|
|
81
|
+
};
|
|
82
|
+
const scheduleRefresh = () => {
|
|
83
|
+
for (const delay of EVENT_REFRESH_DELAYS_MS) queueRefresh(delay);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// TUI/session state can hydrate asynchronously after mount or session switch,
|
|
87
|
+
// so retry a few times to recover from empty first-load reads.
|
|
88
|
+
const scheduleMountRecovery = () => {
|
|
89
|
+
for (const delay of MOUNT_RECOVERY_DELAYS_MS) queueRefresh(delay);
|
|
90
|
+
};
|
|
91
|
+
const interval = setInterval(reload, REFRESH_INTERVAL_MS);
|
|
92
|
+
const unsubscribers = [api.event.on("session.updated", event => {
|
|
93
|
+
if (event.properties?.info?.id === sessionID) {
|
|
94
|
+
scheduleRefresh();
|
|
95
|
+
}
|
|
96
|
+
}), api.event.on("message.updated", event => {
|
|
97
|
+
if (event.properties?.info?.sessionID === sessionID) {
|
|
98
|
+
scheduleRefresh();
|
|
99
|
+
}
|
|
100
|
+
}), api.event.on("message.removed", event => {
|
|
101
|
+
if (event.properties?.sessionID === sessionID) {
|
|
102
|
+
scheduleRefresh();
|
|
103
|
+
}
|
|
104
|
+
}), api.event.on("tui.session.select", event => {
|
|
105
|
+
if (event.properties?.sessionID === sessionID) {
|
|
106
|
+
scheduleRefresh();
|
|
107
|
+
}
|
|
108
|
+
})];
|
|
109
|
+
const dispose = () => {
|
|
110
|
+
if (disposed) return;
|
|
111
|
+
disposed = true;
|
|
112
|
+
clearInterval(interval);
|
|
113
|
+
for (const timer of timers) clearTimeout(timer);
|
|
114
|
+
timers.clear();
|
|
115
|
+
for (const unsubscribe of unsubscribers) unsubscribe();
|
|
116
|
+
getSessionResourceMap(api).delete(sessionID);
|
|
117
|
+
};
|
|
118
|
+
const resource = {
|
|
119
|
+
sessionID,
|
|
120
|
+
sidebar,
|
|
121
|
+
compact,
|
|
122
|
+
retain: () => {
|
|
123
|
+
refCount += 1;
|
|
124
|
+
return resource;
|
|
125
|
+
},
|
|
126
|
+
release: () => {
|
|
127
|
+
refCount -= 1;
|
|
128
|
+
if (refCount <= 0) dispose();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
reload();
|
|
132
|
+
scheduleMountRecovery();
|
|
133
|
+
return resource;
|
|
134
|
+
}
|
|
135
|
+
function acquireSessionQuotaResource(api, sessionID) {
|
|
136
|
+
const resources = getSessionResourceMap(api);
|
|
137
|
+
const existing = resources.get(sessionID);
|
|
138
|
+
if (existing) return existing.retain();
|
|
139
|
+
const next = createSessionQuotaResource(api, sessionID).retain();
|
|
140
|
+
resources.set(sessionID, next);
|
|
141
|
+
return next;
|
|
142
|
+
}
|
|
143
|
+
function createHomeBottomResource(api) {
|
|
144
|
+
const [bottom, setBottom] = createSignal({
|
|
145
|
+
status: "loading",
|
|
146
|
+
compact: {
|
|
147
|
+
status: "loading"
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
let refCount = 0;
|
|
151
|
+
let disposed = false;
|
|
152
|
+
let loadVersion = 0;
|
|
153
|
+
let inFlight = false;
|
|
154
|
+
let queued = false;
|
|
155
|
+
const timers = new Set();
|
|
156
|
+
const reload = () => {
|
|
157
|
+
if (disposed) return;
|
|
158
|
+
if (inFlight) {
|
|
159
|
+
queued = true;
|
|
160
|
+
loadVersion += 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
inFlight = true;
|
|
164
|
+
const currentVersion = ++loadVersion;
|
|
165
|
+
void loadTuiHomeBottomStatus({
|
|
166
|
+
api
|
|
167
|
+
}).then(next => {
|
|
168
|
+
if (disposed || currentVersion !== loadVersion) return;
|
|
169
|
+
setBottom(next);
|
|
170
|
+
// Fire-and-forget: write export file if enabled. A failed write must
|
|
171
|
+
// never affect TUI rendering, so log a warning and continue.
|
|
172
|
+
void writeTuiQuotaExportIfEnabled({
|
|
173
|
+
api
|
|
174
|
+
}).catch(err => {
|
|
175
|
+
console.warn(`[opencode-quota] quota export write failed: ${String(err)}`);
|
|
176
|
+
});
|
|
177
|
+
}).catch(() => {
|
|
178
|
+
if (disposed || currentVersion !== loadVersion) return;
|
|
179
|
+
}).finally(() => {
|
|
180
|
+
if (disposed) return;
|
|
181
|
+
inFlight = false;
|
|
182
|
+
if (queued) {
|
|
183
|
+
queued = false;
|
|
184
|
+
reload();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
const queueRefresh = delay => {
|
|
189
|
+
if (disposed) return;
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
timers.delete(timer);
|
|
192
|
+
reload();
|
|
193
|
+
}, delay);
|
|
194
|
+
timers.add(timer);
|
|
195
|
+
};
|
|
196
|
+
const scheduleRefresh = () => {
|
|
197
|
+
for (const delay of EVENT_REFRESH_DELAYS_MS) queueRefresh(delay);
|
|
198
|
+
};
|
|
199
|
+
const interval = setInterval(reload, REFRESH_INTERVAL_MS);
|
|
200
|
+
const unsubscribers = [api.event.on("session.updated", scheduleRefresh), api.event.on("message.updated", scheduleRefresh), api.event.on("message.removed", scheduleRefresh), api.event.on("tui.session.select", scheduleRefresh)];
|
|
201
|
+
const dispose = () => {
|
|
202
|
+
if (disposed) return;
|
|
203
|
+
disposed = true;
|
|
204
|
+
clearInterval(interval);
|
|
205
|
+
for (const timer of timers) clearTimeout(timer);
|
|
206
|
+
timers.clear();
|
|
207
|
+
for (const unsubscribe of unsubscribers) unsubscribe();
|
|
208
|
+
homeResources.delete(api);
|
|
209
|
+
};
|
|
210
|
+
const resource = {
|
|
211
|
+
bottom,
|
|
212
|
+
retain: () => {
|
|
213
|
+
refCount += 1;
|
|
214
|
+
return resource;
|
|
215
|
+
},
|
|
216
|
+
release: () => {
|
|
217
|
+
refCount -= 1;
|
|
218
|
+
if (refCount <= 0) dispose();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
reload();
|
|
222
|
+
return resource;
|
|
223
|
+
}
|
|
224
|
+
function acquireHomeBottomResource(api) {
|
|
225
|
+
const existing = homeResources.get(api);
|
|
226
|
+
if (existing) return existing.retain();
|
|
227
|
+
const next = createHomeBottomResource(api).retain();
|
|
228
|
+
homeResources.set(api, next);
|
|
229
|
+
return next;
|
|
230
|
+
}
|
|
231
|
+
function useSessionQuotaResource(api, sessionID) {
|
|
232
|
+
let current = acquireSessionQuotaResource(api, sessionID());
|
|
233
|
+
const [resource, setResource] = createSignal(current);
|
|
234
|
+
createEffect(() => {
|
|
235
|
+
const nextSessionID = sessionID();
|
|
236
|
+
if (current.sessionID === nextSessionID) return;
|
|
237
|
+
const previous = current;
|
|
238
|
+
current = acquireSessionQuotaResource(api, nextSessionID);
|
|
239
|
+
setResource(current);
|
|
240
|
+
previous.release();
|
|
241
|
+
});
|
|
242
|
+
onCleanup(() => {
|
|
243
|
+
current.release();
|
|
244
|
+
});
|
|
245
|
+
return resource;
|
|
246
|
+
}
|
|
247
|
+
function SidebarContentView(props) {
|
|
248
|
+
const resource = useSessionQuotaResource(props.api, () => props.sessionID);
|
|
249
|
+
const panel = () => resource().sidebar();
|
|
250
|
+
const lines = () => getSidebarPanelLines(panel());
|
|
251
|
+
const hasDetailLines = () => Boolean(panel().linesExpanded?.length);
|
|
252
|
+
const [collapsed, setCollapsed] = createSignal(props.api.kv?.get("quota-sidebar-collapsed", true) ?? true);
|
|
253
|
+
const toggleCollapsed = () => {
|
|
254
|
+
if (!hasDetailLines()) return;
|
|
255
|
+
const next = !collapsed();
|
|
256
|
+
setCollapsed(next);
|
|
257
|
+
props.api.kv?.set("quota-sidebar-collapsed", next);
|
|
258
|
+
};
|
|
259
|
+
const displayLines = () => {
|
|
260
|
+
if (!hasDetailLines()) return lines();
|
|
261
|
+
return collapsed() ? lines() : getSidebarPanelLinesExpanded(panel());
|
|
262
|
+
};
|
|
263
|
+
const toggleIcon = () => collapsed() ? "▶" : "▼";
|
|
264
|
+
const providerCount = () => panel().providerCount ?? 0;
|
|
265
|
+
return _$createComponent(Show, {
|
|
266
|
+
get when() {
|
|
267
|
+
return shouldRenderSidebarPanel(panel());
|
|
268
|
+
},
|
|
269
|
+
get children() {
|
|
270
|
+
var _el$ = _$createElement("box"),
|
|
271
|
+
_el$2 = _$createElement("box"),
|
|
272
|
+
_el$3 = _$createElement("text"),
|
|
273
|
+
_el$4 = _$createElement("b"),
|
|
274
|
+
_el$8 = _$createElement("box");
|
|
275
|
+
_$insertNode(_el$, _el$2);
|
|
276
|
+
_$insertNode(_el$, _el$8);
|
|
277
|
+
_$setProp(_el$, "gap", 0);
|
|
278
|
+
_$insertNode(_el$2, _el$3);
|
|
279
|
+
_$setProp(_el$2, "flexDirection", "row");
|
|
280
|
+
_$insertNode(_el$3, _el$4);
|
|
281
|
+
_$setProp(_el$3, "onMouseDown", toggleCollapsed);
|
|
282
|
+
_$insert(_el$4, (() => {
|
|
283
|
+
var _c$ = _$memo(() => !!hasDetailLines());
|
|
284
|
+
return () => _c$() ? `${toggleIcon()} Quota` : "Quota";
|
|
285
|
+
})());
|
|
286
|
+
_$insert(_el$2, _$createComponent(Show, {
|
|
287
|
+
get when() {
|
|
288
|
+
return _$memo(() => !!collapsed())() && providerCount() > 0;
|
|
289
|
+
},
|
|
290
|
+
get children() {
|
|
291
|
+
var _el$5 = _$createElement("text"),
|
|
292
|
+
_el$6 = _$createTextNode(` (`),
|
|
293
|
+
_el$7 = _$createTextNode(` providers)`);
|
|
294
|
+
_$insertNode(_el$5, _el$6);
|
|
295
|
+
_$insertNode(_el$5, _el$7);
|
|
296
|
+
_$insert(_el$5, providerCount, _el$7);
|
|
297
|
+
_$effect(_$p => _$setProp(_el$5, "fg", props.api.theme.current.textMuted, _$p));
|
|
298
|
+
return _el$5;
|
|
299
|
+
}
|
|
300
|
+
}), null);
|
|
301
|
+
_$setProp(_el$8, "gap", 0);
|
|
302
|
+
_$insert(_el$8, () => displayLines().map(line => (() => {
|
|
303
|
+
var _el$9 = _$createElement("text");
|
|
304
|
+
_$setProp(_el$9, "wrapMode", "none");
|
|
305
|
+
_$insert(_el$9, line || " ");
|
|
306
|
+
_$effect(_$p => _$setProp(_el$9, "fg", getSidebarBodyLineColor(line, props.api.theme.current), _$p));
|
|
307
|
+
return _el$9;
|
|
308
|
+
})()));
|
|
309
|
+
_$effect(_$p => _$setProp(_el$3, "fg", props.api.theme.current.text, _$p));
|
|
310
|
+
return _el$;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function CompactStatusLine(props) {
|
|
315
|
+
const text = () => {
|
|
316
|
+
const panel = props.panel();
|
|
317
|
+
if (!shouldRenderCompactStatus(panel)) return "";
|
|
318
|
+
return getCompactStatusText(panel);
|
|
319
|
+
};
|
|
320
|
+
const line = () => (() => {
|
|
321
|
+
var _el$0 = _$createElement("box"),
|
|
322
|
+
_el$1 = _$createElement("text");
|
|
323
|
+
_$insertNode(_el$0, _el$1);
|
|
324
|
+
_$setProp(_el$0, "flexDirection", "row");
|
|
325
|
+
_$setProp(_el$1, "wrapMode", "none");
|
|
326
|
+
_$insert(_el$1, text);
|
|
327
|
+
_$effect(_p$ => {
|
|
328
|
+
var _v$ = props.justifyContent,
|
|
329
|
+
_v$2 = props.api.theme.current.textMuted;
|
|
330
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$0, "justifyContent", _v$, _p$.e));
|
|
331
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$1, "fg", _v$2, _p$.t));
|
|
332
|
+
return _p$;
|
|
333
|
+
}, {
|
|
334
|
+
e: undefined,
|
|
335
|
+
t: undefined
|
|
336
|
+
});
|
|
337
|
+
return _el$0;
|
|
338
|
+
})();
|
|
339
|
+
return _$createComponent(Show, {
|
|
340
|
+
get when() {
|
|
341
|
+
return text();
|
|
342
|
+
},
|
|
343
|
+
get children() {
|
|
344
|
+
return _$createComponent(Show, {
|
|
345
|
+
get when() {
|
|
346
|
+
return props.blankLineBefore;
|
|
347
|
+
},
|
|
348
|
+
get fallback() {
|
|
349
|
+
return line();
|
|
350
|
+
},
|
|
351
|
+
get children() {
|
|
352
|
+
var _el$10 = _$createElement("box"),
|
|
353
|
+
_el$11 = _$createElement("text");
|
|
354
|
+
_$insertNode(_el$10, _el$11);
|
|
355
|
+
_$setProp(_el$10, "gap", 0);
|
|
356
|
+
_$insertNode(_el$11, _$createTextNode(` `));
|
|
357
|
+
_$insert(_el$10, line, null);
|
|
358
|
+
return _el$10;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function SessionPromptWithCompactStatus(props) {
|
|
365
|
+
const resource = useSessionQuotaResource(props.api, () => props.sessionID);
|
|
366
|
+
const panel = () => resource().compact();
|
|
367
|
+
return (() => {
|
|
368
|
+
var _el$13 = _$createElement("box");
|
|
369
|
+
_$setProp(_el$13, "gap", 0);
|
|
370
|
+
_$insert(_el$13, _$createComponent(props.api.ui.Prompt, {
|
|
371
|
+
get sessionID() {
|
|
372
|
+
return props.sessionID;
|
|
373
|
+
},
|
|
374
|
+
get visible() {
|
|
375
|
+
return props.visible;
|
|
376
|
+
},
|
|
377
|
+
get disabled() {
|
|
378
|
+
return props.disabled;
|
|
379
|
+
},
|
|
380
|
+
get onSubmit() {
|
|
381
|
+
return props.onSubmit;
|
|
382
|
+
},
|
|
383
|
+
ref(r$) {
|
|
384
|
+
var _ref$ = props.promptRef;
|
|
385
|
+
typeof _ref$ === "function" ? _ref$(r$) : props.promptRef = r$;
|
|
386
|
+
}
|
|
387
|
+
}), null);
|
|
388
|
+
_$insert(_el$13, _$createComponent(CompactStatusLine, {
|
|
389
|
+
get api() {
|
|
390
|
+
return props.api;
|
|
391
|
+
},
|
|
392
|
+
panel: panel,
|
|
393
|
+
justifyContent: "flex-end"
|
|
394
|
+
}), null);
|
|
395
|
+
return _el$13;
|
|
396
|
+
})();
|
|
397
|
+
}
|
|
398
|
+
function HomeBottomView(props) {
|
|
399
|
+
const resource = acquireHomeBottomResource(props.api);
|
|
400
|
+
onCleanup(() => resource.release());
|
|
401
|
+
const announcement = () => getHomeBottomAnnouncementText(resource.bottom());
|
|
402
|
+
const compact = () => resource.bottom().compact;
|
|
403
|
+
return _$createComponent(Show, {
|
|
404
|
+
get when() {
|
|
405
|
+
return shouldRenderHomeBottom(resource.bottom());
|
|
406
|
+
},
|
|
407
|
+
get children() {
|
|
408
|
+
var _el$14 = _$createElement("box"),
|
|
409
|
+
_el$15 = _$createElement("text");
|
|
410
|
+
_$insertNode(_el$14, _el$15);
|
|
411
|
+
_$setProp(_el$14, "gap", 0);
|
|
412
|
+
_$insertNode(_el$15, _$createTextNode(` `));
|
|
413
|
+
_$insert(_el$14, _$createComponent(Show, {
|
|
414
|
+
get when() {
|
|
415
|
+
return announcement();
|
|
416
|
+
},
|
|
417
|
+
get children() {
|
|
418
|
+
var _el$17 = _$createElement("box"),
|
|
419
|
+
_el$18 = _$createElement("text");
|
|
420
|
+
_$insertNode(_el$17, _el$18);
|
|
421
|
+
_$setProp(_el$17, "flexDirection", "row");
|
|
422
|
+
_$setProp(_el$17, "justifyContent", "center");
|
|
423
|
+
_$setProp(_el$18, "wrapMode", "none");
|
|
424
|
+
_$insert(_el$18, announcement);
|
|
425
|
+
_$effect(_$p => _$setProp(_el$18, "fg", props.api.theme.current.textMuted, _$p));
|
|
426
|
+
return _el$17;
|
|
427
|
+
}
|
|
428
|
+
}), null);
|
|
429
|
+
_$insert(_el$14, _$createComponent(CompactStatusLine, {
|
|
430
|
+
get api() {
|
|
431
|
+
return props.api;
|
|
432
|
+
},
|
|
433
|
+
panel: compact,
|
|
434
|
+
justifyContent: "center"
|
|
435
|
+
}), null);
|
|
436
|
+
return _el$14;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function getActiveTuiSessionID(api) {
|
|
441
|
+
const route = api.route?.current;
|
|
442
|
+
if (route?.name !== "session" && route?.type !== "session") return undefined;
|
|
443
|
+
return normalizeTuiSessionID(route.params?.sessionID ?? route.params?.session_id ?? route.params?.id ?? route.sessionID);
|
|
444
|
+
}
|
|
445
|
+
function getTuiCommandArguments(input) {
|
|
446
|
+
if (!input || typeof input !== "object") return undefined;
|
|
447
|
+
const record = input;
|
|
448
|
+
for (const key of ["arguments", "args", "query"]) {
|
|
449
|
+
const value = record[key];
|
|
450
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
451
|
+
}
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
function CommandLoadingDialog(props) {
|
|
455
|
+
return (() => {
|
|
456
|
+
var _el$19 = _$createElement("box"),
|
|
457
|
+
_el$20 = _$createElement("text"),
|
|
458
|
+
_el$21 = _$createElement("b"),
|
|
459
|
+
_el$22 = _$createElement("text");
|
|
460
|
+
_$insertNode(_el$19, _el$20);
|
|
461
|
+
_$insertNode(_el$19, _el$22);
|
|
462
|
+
_$setProp(_el$19, "gap", 1);
|
|
463
|
+
_$insertNode(_el$20, _el$21);
|
|
464
|
+
_$insert(_el$21, () => props.title);
|
|
465
|
+
_$insertNode(_el$22, _$createTextNode(`Loading deterministic local output…`));
|
|
466
|
+
_$effect(_p$ => {
|
|
467
|
+
var _v$3 = props.api.theme.current.text,
|
|
468
|
+
_v$4 = props.api.theme.current.textMuted;
|
|
469
|
+
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$20, "fg", _v$3, _p$.e));
|
|
470
|
+
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$22, "fg", _v$4, _p$.t));
|
|
471
|
+
return _p$;
|
|
472
|
+
}, {
|
|
473
|
+
e: undefined,
|
|
474
|
+
t: undefined
|
|
475
|
+
});
|
|
476
|
+
return _el$19;
|
|
477
|
+
})();
|
|
478
|
+
}
|
|
479
|
+
function CommandOutputDialog(props) {
|
|
480
|
+
const lines = () => props.output.split("\n");
|
|
481
|
+
const bodyHeight = () => Math.min(28, Math.max(6, lines().length));
|
|
482
|
+
return (() => {
|
|
483
|
+
var _el$24 = _$createElement("box"),
|
|
484
|
+
_el$25 = _$createElement("text"),
|
|
485
|
+
_el$26 = _$createElement("b"),
|
|
486
|
+
_el$27 = _$createElement("scrollbox"),
|
|
487
|
+
_el$28 = _$createElement("box"),
|
|
488
|
+
_el$29 = _$createElement("text");
|
|
489
|
+
_$insertNode(_el$24, _el$25);
|
|
490
|
+
_$insertNode(_el$24, _el$27);
|
|
491
|
+
_$insertNode(_el$24, _el$29);
|
|
492
|
+
_$setProp(_el$24, "gap", 1);
|
|
493
|
+
_$setProp(_el$24, "width", "100%");
|
|
494
|
+
_$setProp(_el$24, "flexGrow", 1);
|
|
495
|
+
_$setProp(_el$24, "paddingLeft", 2);
|
|
496
|
+
_$setProp(_el$24, "paddingRight", 2);
|
|
497
|
+
_$setProp(_el$24, "paddingBottom", 1);
|
|
498
|
+
_$insertNode(_el$25, _el$26);
|
|
499
|
+
_$insert(_el$26, () => props.title);
|
|
500
|
+
_$insertNode(_el$27, _el$28);
|
|
501
|
+
_$setProp(_el$27, "width", "100%");
|
|
502
|
+
_$setProp(_el$27, "flexGrow", 1);
|
|
503
|
+
_$setProp(_el$27, "maxHeight", 28);
|
|
504
|
+
_$setProp(_el$28, "gap", 0);
|
|
505
|
+
_$setProp(_el$28, "width", "100%");
|
|
506
|
+
_$setProp(_el$28, "minWidth", 0);
|
|
507
|
+
_$insert(_el$28, () => lines().map(line => (() => {
|
|
508
|
+
var _el$31 = _$createElement("text");
|
|
509
|
+
_$setProp(_el$31, "wrapMode", "word");
|
|
510
|
+
_$setProp(_el$31, "width", "100%");
|
|
511
|
+
_$insert(_el$31, line || " ");
|
|
512
|
+
_$effect(_$p => _$setProp(_el$31, "fg", props.api.theme.current.text, _$p));
|
|
513
|
+
return _el$31;
|
|
514
|
+
})()));
|
|
515
|
+
_$insertNode(_el$29, _$createTextNode(`esc closes`));
|
|
516
|
+
_$effect(_p$ => {
|
|
517
|
+
var _v$5 = props.api.theme.current.text,
|
|
518
|
+
_v$6 = bodyHeight(),
|
|
519
|
+
_v$7 = props.api.theme.current.textMuted;
|
|
520
|
+
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$25, "fg", _v$5, _p$.e));
|
|
521
|
+
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$27, "minHeight", _v$6, _p$.t));
|
|
522
|
+
_v$7 !== _p$.a && (_p$.a = _$setProp(_el$29, "fg", _v$7, _p$.a));
|
|
523
|
+
return _p$;
|
|
524
|
+
}, {
|
|
525
|
+
e: undefined,
|
|
526
|
+
t: undefined,
|
|
527
|
+
a: undefined
|
|
528
|
+
});
|
|
529
|
+
return _el$24;
|
|
530
|
+
})();
|
|
531
|
+
}
|
|
532
|
+
function CommandErrorDialog(props) {
|
|
533
|
+
const message = props.error instanceof Error ? props.error.message : String(props.error);
|
|
534
|
+
return (() => {
|
|
535
|
+
var _el$32 = _$createElement("box"),
|
|
536
|
+
_el$33 = _$createElement("text"),
|
|
537
|
+
_el$34 = _$createElement("b"),
|
|
538
|
+
_el$35 = _$createElement("text"),
|
|
539
|
+
_el$37 = _$createElement("text"),
|
|
540
|
+
_el$38 = _$createElement("text");
|
|
541
|
+
_$insertNode(_el$32, _el$33);
|
|
542
|
+
_$insertNode(_el$32, _el$35);
|
|
543
|
+
_$insertNode(_el$32, _el$37);
|
|
544
|
+
_$insertNode(_el$32, _el$38);
|
|
545
|
+
_$setProp(_el$32, "gap", 1);
|
|
546
|
+
_$insertNode(_el$33, _el$34);
|
|
547
|
+
_$insert(_el$34, () => props.title);
|
|
548
|
+
_$insertNode(_el$35, _$createTextNode(`OpenCode Quota command failed.`));
|
|
549
|
+
_$setProp(_el$37, "wrapMode", "none");
|
|
550
|
+
_$insert(_el$37, message || "Unknown error");
|
|
551
|
+
_$insertNode(_el$38, _$createTextNode(`esc closes`));
|
|
552
|
+
_$effect(_p$ => {
|
|
553
|
+
var _v$8 = props.api.theme.current.text,
|
|
554
|
+
_v$9 = props.api.theme.current.text,
|
|
555
|
+
_v$0 = props.api.theme.current.textMuted,
|
|
556
|
+
_v$1 = props.api.theme.current.textMuted;
|
|
557
|
+
_v$8 !== _p$.e && (_p$.e = _$setProp(_el$33, "fg", _v$8, _p$.e));
|
|
558
|
+
_v$9 !== _p$.t && (_p$.t = _$setProp(_el$35, "fg", _v$9, _p$.t));
|
|
559
|
+
_v$0 !== _p$.a && (_p$.a = _$setProp(_el$37, "fg", _v$0, _p$.a));
|
|
560
|
+
_v$1 !== _p$.o && (_p$.o = _$setProp(_el$38, "fg", _v$1, _p$.o));
|
|
561
|
+
return _p$;
|
|
562
|
+
}, {
|
|
563
|
+
e: undefined,
|
|
564
|
+
t: undefined,
|
|
565
|
+
a: undefined,
|
|
566
|
+
o: undefined
|
|
567
|
+
});
|
|
568
|
+
return _el$32;
|
|
569
|
+
})();
|
|
570
|
+
}
|
|
571
|
+
function TokensBetweenPromptDialog(props) {
|
|
572
|
+
const DialogPrompt = props.api.ui?.DialogPrompt;
|
|
573
|
+
if (DialogPrompt) {
|
|
574
|
+
return _$createComponent(DialogPrompt, {
|
|
575
|
+
title: "OpenCode Quota Token Range",
|
|
576
|
+
placeholder: "YYYY-MM-DD YYYY-MM-DD",
|
|
577
|
+
description: () => (() => {
|
|
578
|
+
var _el$40 = _$createElement("text");
|
|
579
|
+
_$insertNode(_el$40, _$createTextNode(`Enter start and end dates, for example: 2026-01-01 2026-01-15`));
|
|
580
|
+
_$setProp(_el$40, "wrapMode", "none");
|
|
581
|
+
_$effect(_$p => _$setProp(_el$40, "fg", props.api.theme.current.textMuted, _$p));
|
|
582
|
+
return _el$40;
|
|
583
|
+
})(),
|
|
584
|
+
get onConfirm() {
|
|
585
|
+
return props.onConfirm;
|
|
586
|
+
},
|
|
587
|
+
get onCancel() {
|
|
588
|
+
return props.onCancel;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return _$createComponent(CommandOutputDialog, {
|
|
593
|
+
get api() {
|
|
594
|
+
return props.api;
|
|
595
|
+
},
|
|
596
|
+
title: "OpenCode Quota Token Range",
|
|
597
|
+
output: "Missing arguments for /tokens_between\n\nExpected: /tokens_between YYYY-MM-DD YYYY-MM-DD\nExample: /tokens_between 2026-01-01 2026-01-15"
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function replaceDialog(api, size, render) {
|
|
601
|
+
const dialog = api.ui?.dialog;
|
|
602
|
+
dialog?.replace?.(render);
|
|
603
|
+
// OpenCode's dialog.replace() resets size to medium; xlarge is the widest supported dialog.
|
|
604
|
+
dialog?.setSize?.(size);
|
|
605
|
+
}
|
|
606
|
+
function clearDialog(api) {
|
|
607
|
+
api.ui?.dialog?.clear?.();
|
|
608
|
+
}
|
|
609
|
+
async function runQuotaDialogCommandAsync(api, command, rawInput, state) {
|
|
610
|
+
const spec = QUOTA_DIALOG_COMMANDS.find(item => item.id === command);
|
|
611
|
+
const args = getTuiCommandArguments(rawInput);
|
|
612
|
+
const sessionID = getActiveTuiSessionID(api);
|
|
613
|
+
if (command === "tokens_between" && !args) {
|
|
614
|
+
replaceDialog(api, "medium", () => _$createComponent(TokensBetweenPromptDialog, {
|
|
615
|
+
api: api,
|
|
616
|
+
onCancel: () => clearDialog(api),
|
|
617
|
+
onConfirm: value => {
|
|
618
|
+
const nextArgs = value.trim();
|
|
619
|
+
if (!nextArgs) {
|
|
620
|
+
replaceDialog(api, "large", () => _$createComponent(CommandOutputDialog, {
|
|
621
|
+
api: api,
|
|
622
|
+
get title() {
|
|
623
|
+
return spec.title;
|
|
624
|
+
},
|
|
625
|
+
output: "Missing arguments for /tokens_between\n\nExpected: /tokens_between YYYY-MM-DD YYYY-MM-DD\nExample: /tokens_between 2026-01-01 2026-01-15"
|
|
626
|
+
}));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
runQuotaDialogCommand(api, command, {
|
|
630
|
+
arguments: nextArgs
|
|
631
|
+
}, state);
|
|
632
|
+
}
|
|
633
|
+
}));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
replaceDialog(api, spec.dialogSize, () => _$createComponent(CommandLoadingDialog, {
|
|
637
|
+
api: api,
|
|
638
|
+
get title() {
|
|
639
|
+
return spec.title;
|
|
640
|
+
}
|
|
641
|
+
}));
|
|
642
|
+
try {
|
|
643
|
+
const result = await buildQuotaDialogCommandOutput({
|
|
644
|
+
command,
|
|
645
|
+
arguments: args,
|
|
646
|
+
client: createTuiQuotaClient(api),
|
|
647
|
+
roots: getTuiRuntimeRootHints(api),
|
|
648
|
+
sessionID,
|
|
649
|
+
resolveSessionMeta: id => getTuiSessionModelMeta(api, id),
|
|
650
|
+
lastSessionTokenError: state?.lastSessionTokenError,
|
|
651
|
+
setLastSessionTokenError: state ? error => {
|
|
652
|
+
state.lastSessionTokenError = error;
|
|
653
|
+
} : undefined,
|
|
654
|
+
log: async (message, extra) => {
|
|
655
|
+
await api.client?.app?.log?.({
|
|
656
|
+
body: {
|
|
657
|
+
service: "quota-toast",
|
|
658
|
+
level: "debug",
|
|
659
|
+
message,
|
|
660
|
+
extra
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
if (result.state === "noop") {
|
|
666
|
+
clearDialog(api);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
replaceDialog(api, result.dialogSize, () => _$createComponent(CommandOutputDialog, {
|
|
670
|
+
api: api,
|
|
671
|
+
get title() {
|
|
672
|
+
return result.title;
|
|
673
|
+
},
|
|
674
|
+
get output() {
|
|
675
|
+
return result.output;
|
|
676
|
+
}
|
|
677
|
+
}));
|
|
678
|
+
} catch (error) {
|
|
679
|
+
replaceDialog(api, "large", () => _$createComponent(CommandErrorDialog, {
|
|
680
|
+
api: api,
|
|
681
|
+
get title() {
|
|
682
|
+
return spec.title;
|
|
683
|
+
},
|
|
684
|
+
error: error
|
|
685
|
+
}));
|
|
686
|
+
api.ui?.toast?.({
|
|
687
|
+
variant: "error",
|
|
688
|
+
message: "OpenCode Quota command failed"
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function runQuotaDialogCommand(api, command, rawInput, state) {
|
|
693
|
+
void runQuotaDialogCommandAsync(api, command, rawInput, state);
|
|
694
|
+
}
|
|
695
|
+
function registerQuotaDialogCommands(api) {
|
|
696
|
+
const keymap = api.keymap;
|
|
697
|
+
if (!keymap?.registerLayer) return;
|
|
698
|
+
const commandState = {};
|
|
699
|
+
const dispose = keymap.registerLayer({
|
|
700
|
+
commands: QUOTA_DIALOG_COMMANDS.map(spec => ({
|
|
701
|
+
namespace: "palette",
|
|
702
|
+
name: `opencode-quota.${spec.id}`,
|
|
703
|
+
title: spec.title,
|
|
704
|
+
desc: spec.description,
|
|
705
|
+
category: "OpenCode Quota",
|
|
706
|
+
slashName: spec.slashName,
|
|
707
|
+
run(input) {
|
|
708
|
+
runQuotaDialogCommand(api, spec.id, input, commandState);
|
|
709
|
+
}
|
|
710
|
+
}))
|
|
711
|
+
});
|
|
712
|
+
if (typeof dispose === "function") {
|
|
713
|
+
api.lifecycle.onDispose(dispose);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function registerSidebarSlots(api) {
|
|
717
|
+
api.slots.register({
|
|
718
|
+
order: SIDEBAR_ORDER,
|
|
719
|
+
slots: {
|
|
720
|
+
sidebar_content(_ctx, props) {
|
|
721
|
+
return _$createComponent(SidebarContentView, {
|
|
722
|
+
api: api,
|
|
723
|
+
get sessionID() {
|
|
724
|
+
return props.session_id;
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
const tui = async api => {
|
|
732
|
+
registerQuotaDialogCommands(api);
|
|
733
|
+
let surfaceRegistration;
|
|
734
|
+
try {
|
|
735
|
+
surfaceRegistration = await resolveTuiSurfaceRegistration(api);
|
|
736
|
+
} catch {
|
|
737
|
+
registerSidebarSlots(api);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (surfaceRegistration.sidebar.enabled) {
|
|
741
|
+
registerSidebarSlots(api);
|
|
742
|
+
}
|
|
743
|
+
const compactRegistration = surfaceRegistration.compact;
|
|
744
|
+
if (!compactRegistration.enabled && !surfaceRegistration.homeBottom) return;
|
|
745
|
+
const compactSlots = {};
|
|
746
|
+
if (compactRegistration.sessionPrompt) {
|
|
747
|
+
compactSlots.session_prompt = (_ctx, props) => _$createComponent(SessionPromptWithCompactStatus, {
|
|
748
|
+
api: api,
|
|
749
|
+
get sessionID() {
|
|
750
|
+
return props.session_id;
|
|
751
|
+
},
|
|
752
|
+
get visible() {
|
|
753
|
+
return props.visible;
|
|
754
|
+
},
|
|
755
|
+
get disabled() {
|
|
756
|
+
return props.disabled;
|
|
757
|
+
},
|
|
758
|
+
get onSubmit() {
|
|
759
|
+
return props.on_submit;
|
|
760
|
+
},
|
|
761
|
+
get promptRef() {
|
|
762
|
+
return props.ref;
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
if (surfaceRegistration.homeBottom) {
|
|
767
|
+
compactSlots.home_bottom = () => _$createComponent(HomeBottomView, {
|
|
768
|
+
api: api
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
if (Object.keys(compactSlots).length > 0) {
|
|
772
|
+
api.slots.register({
|
|
773
|
+
order: COMPACT_ORDER,
|
|
774
|
+
slots: compactSlots
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
const pluginModule = {
|
|
779
|
+
id,
|
|
780
|
+
tui
|
|
781
|
+
};
|
|
782
|
+
export default pluginModule;
|