usage-panel-opencode 0.0.0-stage → 0.1.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/dist/tui.js ADDED
@@ -0,0 +1,960 @@
1
+ // @bun
2
+ // src/tui.tsx
3
+ import { createComponent as _$createComponent3 } from "@opentui/solid";
4
+ import { Plugin } from "@opencode/plugin/tui";
5
+ import { createSignal } from "solid-js";
6
+
7
+ // src/config.ts
8
+ var DEFAULT_PREFERENCES = Object.freeze({
9
+ enabled: true,
10
+ intervalSeconds: 60,
11
+ visible: true
12
+ });
13
+
14
+ // src/rpc.ts
15
+ import { Rpc } from "@opencode/plugin/rpc";
16
+ var emptyObjectSchema = {
17
+ type: "object",
18
+ properties: {},
19
+ additionalProperties: false
20
+ };
21
+ var instanceInputSchema = {
22
+ type: "object",
23
+ properties: { instanceId: { type: "string", minLength: 1, maxLength: 128 } },
24
+ required: ["instanceId"],
25
+ additionalProperties: false
26
+ };
27
+ var nullableStringSchema = {
28
+ anyOf: [{ type: "string" }, { type: "null" }]
29
+ };
30
+ var nullableNumberSchema = {
31
+ anyOf: [{ type: "number" }, { type: "null" }]
32
+ };
33
+ var panelSnapshotSchema = {
34
+ type: "object",
35
+ properties: {
36
+ state: {
37
+ type: "string",
38
+ enum: ["loading", "ready", "stale", "no_subscription", "needs_auth", "error"]
39
+ },
40
+ snapshot: {
41
+ anyOf: [
42
+ {
43
+ type: "object",
44
+ properties: {
45
+ account: {
46
+ type: "object",
47
+ properties: {
48
+ credentialKind: { type: "string", enum: ["oauth", "key", "none"] },
49
+ label: nullableStringSchema,
50
+ accountId: {
51
+ anyOf: [{ type: "string" }, { type: "null" }]
52
+ },
53
+ planHint: nullableStringSchema,
54
+ organization: {
55
+ anyOf: [{ type: "boolean" }, { type: "null" }]
56
+ },
57
+ expiresAt: nullableNumberSchema
58
+ },
59
+ required: ["credentialKind"],
60
+ additionalProperties: false
61
+ },
62
+ plan: {
63
+ anyOf: [
64
+ {
65
+ type: "object",
66
+ properties: {
67
+ type: { type: "string" },
68
+ label: { type: "string" }
69
+ },
70
+ required: ["type", "label"],
71
+ additionalProperties: false
72
+ },
73
+ { type: "null" }
74
+ ]
75
+ },
76
+ windows: {
77
+ type: "array",
78
+ minItems: 0,
79
+ maxItems: 8,
80
+ items: {
81
+ type: "object",
82
+ properties: {
83
+ kind: { type: "string", enum: ["five_hour", "weekly", "other"] },
84
+ label: { type: "string", minLength: 1 },
85
+ applicable: { type: "boolean" },
86
+ availability: {
87
+ type: "string",
88
+ enum: ["applicable", "not_applicable", "unknown"]
89
+ },
90
+ usedPercent: {
91
+ anyOf: [{ type: "number", minimum: 0, maximum: 100 }, { type: "null" }]
92
+ },
93
+ remainingPercent: {
94
+ anyOf: [{ type: "number", minimum: 0, maximum: 100 }, { type: "null" }]
95
+ },
96
+ resetsAt: nullableNumberSchema,
97
+ source: { type: "string", enum: ["service", "headers"] }
98
+ },
99
+ required: [
100
+ "kind",
101
+ "label",
102
+ "applicable",
103
+ "usedPercent",
104
+ "remainingPercent",
105
+ "resetsAt",
106
+ "source"
107
+ ],
108
+ additionalProperties: false
109
+ }
110
+ },
111
+ origin: {
112
+ type: "object",
113
+ properties: {
114
+ kind: { type: "string", enum: ["service", "headers", "mixed", "none"] },
115
+ fetchedAt: nullableNumberSchema,
116
+ detail: nullableStringSchema
117
+ },
118
+ required: ["kind", "fetchedAt", "detail"],
119
+ additionalProperties: false
120
+ }
121
+ },
122
+ required: ["account", "plan", "windows", "origin"],
123
+ additionalProperties: false
124
+ },
125
+ { type: "null" }
126
+ ]
127
+ },
128
+ error: {
129
+ anyOf: [
130
+ {
131
+ type: "object",
132
+ properties: {
133
+ kind: {
134
+ type: "string",
135
+ enum: [
136
+ "network",
137
+ "rate_limited",
138
+ "unauthorized",
139
+ "forbidden",
140
+ "malformed",
141
+ "unknown"
142
+ ]
143
+ },
144
+ message: { type: "string" },
145
+ action: nullableStringSchema
146
+ },
147
+ required: ["kind", "message"],
148
+ additionalProperties: false
149
+ },
150
+ { type: "null" }
151
+ ]
152
+ },
153
+ refreshNotice: {
154
+ anyOf: [
155
+ {
156
+ type: "object",
157
+ properties: {
158
+ kind: { type: "string", enum: ["limited", "in_progress"] },
159
+ retryAt: { type: "number", minimum: 0 },
160
+ message: { type: "string" }
161
+ },
162
+ required: ["kind", "retryAt", "message"],
163
+ additionalProperties: false
164
+ },
165
+ { type: "null" }
166
+ ]
167
+ },
168
+ preferences: {
169
+ type: "object",
170
+ properties: {
171
+ enabled: { type: "boolean" },
172
+ intervalSeconds: { type: "integer", minimum: 60 },
173
+ visible: { type: "boolean" }
174
+ },
175
+ required: ["enabled", "intervalSeconds", "visible"],
176
+ additionalProperties: false
177
+ },
178
+ ageMs: { type: "number", minimum: 0 },
179
+ retryAt: { type: "number", minimum: 0 }
180
+ },
181
+ required: ["state", "snapshot", "preferences"],
182
+ additionalProperties: false
183
+ };
184
+ var preferenceUpdateSchema = {
185
+ type: "object",
186
+ properties: {
187
+ enabled: { type: "boolean" },
188
+ intervalSeconds: { type: "integer" },
189
+ visible: { type: "boolean" }
190
+ },
191
+ additionalProperties: false
192
+ };
193
+ var usageRpcDefinition = {
194
+ id: "openai-usage",
195
+ methods: {
196
+ snapshot: {
197
+ input: instanceInputSchema,
198
+ output: panelSnapshotSchema
199
+ },
200
+ refresh: {
201
+ input: instanceInputSchema,
202
+ output: panelSnapshotSchema
203
+ },
204
+ setPreferences: {
205
+ input: preferenceUpdateSchema,
206
+ output: panelSnapshotSchema
207
+ },
208
+ start: {
209
+ input: instanceInputSchema,
210
+ output: emptyObjectSchema
211
+ },
212
+ stop: {
213
+ input: instanceInputSchema,
214
+ output: emptyObjectSchema
215
+ }
216
+ },
217
+ events: {
218
+ updated: {
219
+ schema: {
220
+ type: "object",
221
+ properties: {
222
+ instanceId: { type: "string", minLength: 1, maxLength: 128 },
223
+ data: panelSnapshotSchema
224
+ },
225
+ required: ["instanceId", "data"],
226
+ additionalProperties: false
227
+ }
228
+ }
229
+ }
230
+ };
231
+ var Usage = Rpc.define(usageRpcDefinition);
232
+
233
+ // src/ui/detail.tsx
234
+ import { effect as _$effect } from "@opentui/solid";
235
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
236
+ import { insertNode as _$insertNode } from "@opentui/solid";
237
+ import { memo as _$memo } from "@opentui/solid";
238
+ import { createComponent as _$createComponent } from "@opentui/solid";
239
+ import { insert as _$insert } from "@opentui/solid";
240
+ import { setProp as _$setProp } from "@opentui/solid";
241
+ import { createElement as _$createElement } from "@opentui/solid";
242
+ import { TextAttributes } from "@opentui/core";
243
+ import { For, Show } from "solid-js";
244
+
245
+ // src/core/redact.ts
246
+ var DEFAULT_MAX_LENGTH = 240;
247
+ var TOKEN_ASSIGNMENT = /\b((?:access|refresh|id)[_-]?token|api[_-]?key|token)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;}\]]+)/gi;
248
+ var AUTHORIZATION_HEADER = /\b(authorization\s*[:=]\s*)(?:bearer\s+)?(?:"[^"]*"|'[^']*'|[^\s,;}\]]+)/gi;
249
+ var ACCOUNT_ID_FIELD = /(\b(?:chatgpt[-_]?account[-_]?id|account[-_]?id)\s*[:=]\s*["']?)([A-Za-z0-9_-]{4,64})/gi;
250
+ function safeString(value) {
251
+ return typeof value === "string" ? value : "";
252
+ }
253
+ function validLimit(value) {
254
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : DEFAULT_MAX_LENGTH;
255
+ }
256
+ function stripTerminalSequences(text) {
257
+ let result = "";
258
+ for (let index = 0;index < text.length; index += 1) {
259
+ if (text.charCodeAt(index) !== 27) {
260
+ result += text[index];
261
+ continue;
262
+ }
263
+ const next = text.charCodeAt(index + 1);
264
+ if (next === 91) {
265
+ index += 2;
266
+ while (index < text.length && !(text.charCodeAt(index) >= 64 && text.charCodeAt(index) <= 126)) {
267
+ index += 1;
268
+ }
269
+ } else if (next === 93) {
270
+ index += 2;
271
+ while (index < text.length) {
272
+ const code = text.charCodeAt(index);
273
+ if (code === 7)
274
+ break;
275
+ if (code === 27 && text.charCodeAt(index + 1) === 92) {
276
+ index += 1;
277
+ break;
278
+ }
279
+ index += 1;
280
+ }
281
+ } else {
282
+ index += 1;
283
+ }
284
+ }
285
+ return result;
286
+ }
287
+ function stripUnsafeCharacters(text) {
288
+ return Array.from(text, (character) => {
289
+ const codePoint = character.codePointAt(0) ?? 0;
290
+ if (codePoint === 9)
291
+ return " ";
292
+ if (codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297) {
293
+ return "";
294
+ }
295
+ return character;
296
+ }).join("");
297
+ }
298
+ function replaceExactValues(text, values) {
299
+ let result = text;
300
+ for (const value of values ?? []) {
301
+ if (typeof value === "string" && value.length > 0)
302
+ result = result.split(value).join("[redacted]");
303
+ }
304
+ return result;
305
+ }
306
+ function maskIdentifier(value) {
307
+ return maskAccountId(value) ?? "[redacted]";
308
+ }
309
+ function maskAccountId(value) {
310
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{4,64}$/.test(value) || value.length <= 4) {
311
+ return null;
312
+ }
313
+ return `\u2026${value.slice(-4)}`;
314
+ }
315
+ function sanitizeText(value, maxLength = DEFAULT_MAX_LENGTH) {
316
+ const limit = validLimit(maxLength);
317
+ const text = stripUnsafeCharacters(stripTerminalSequences(safeString(value)).replace(/\r\n?|\n/g, " ")).replace(/\s{2,}/g, " ").trim();
318
+ if (text.length <= limit)
319
+ return text;
320
+ return `${text.slice(0, limit - 1)}\u2026`;
321
+ }
322
+ function redactText(value, options = {}) {
323
+ let text = safeString(value);
324
+ text = replaceExactValues(text, options.secrets);
325
+ for (const accountId of options.accountIds ?? []) {
326
+ if (typeof accountId === "string" && accountId.length > 0) {
327
+ text = text.split(accountId).join(maskIdentifier(accountId));
328
+ }
329
+ }
330
+ text = text.replace(AUTHORIZATION_HEADER, "$1[redacted]").replace(/\bbearer\s+[^\s,;}\]]+/gi, "Bearer [redacted]").replace(TOKEN_ASSIGNMENT, "$1=[redacted]").replace(/\bsk-[A-Za-z0-9_-]{8,}/gi, "[redacted]").replace(/\beyJ[A-Za-z0-9_-]{10,}(?:\.[A-Za-z0-9_-]+){1,2}\b/g, "[redacted]").replace(ACCOUNT_ID_FIELD, (_match, prefix, accountId) => {
331
+ return `${prefix}${maskIdentifier(accountId)}`;
332
+ });
333
+ return sanitizeText(text, options.maxLength);
334
+ }
335
+
336
+ // src/ui/format.ts
337
+ function formatPercent(value) {
338
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100)
339
+ return "\u2014";
340
+ return `${Math.round(value)}%`;
341
+ }
342
+ function formatResetTime(timestamp) {
343
+ if (typeof timestamp !== "number" || !Number.isFinite(timestamp) || timestamp <= 0) {
344
+ return "no disponible";
345
+ }
346
+ const date = new Date(timestamp);
347
+ if (Number.isNaN(date.getTime()))
348
+ return "no disponible";
349
+ return new Intl.DateTimeFormat("es", { dateStyle: "medium", timeStyle: "short" }).format(date);
350
+ }
351
+
352
+ // src/ui/detail.tsx
353
+ function safeText(value, fallback = "no disponible") {
354
+ if (typeof value !== "string")
355
+ return fallback;
356
+ return sanitizeText(redactText(value), 120) || fallback;
357
+ }
358
+ function maskedAccount(panel) {
359
+ const account = panel.snapshot?.account;
360
+ if (!account)
361
+ return "Cuenta no disponible";
362
+ if (typeof account.accountId === "string" && /^\u2026[A-Za-z0-9_-]{4}$/.test(account.accountId)) {
363
+ return `Cuenta ${account.accountId}`;
364
+ }
365
+ const label = typeof account.label === "string" ? safeText(account.label, "") : "";
366
+ const firstCharacter = Array.from(label)[0];
367
+ if (firstCharacter)
368
+ return `Cuenta ${firstCharacter}\u2026`;
369
+ return account.credentialKind === "none" ? "Sin cuenta conectada" : "Cuenta conectada";
370
+ }
371
+ function originLabel(kind) {
372
+ switch (kind) {
373
+ case "service":
374
+ return "servicio de ChatGPT";
375
+ case "headers":
376
+ return "cabeceras de OpenAI";
377
+ case "mixed":
378
+ return "servicio y cabeceras de OpenAI";
379
+ default:
380
+ return "no disponible";
381
+ }
382
+ }
383
+ function safeErrorMessage(kind) {
384
+ switch (kind) {
385
+ case "network":
386
+ return "No se pudo consultar el consumo por un problema de conexi\xF3n.";
387
+ case "rate_limited":
388
+ return "OpenAI limit\xF3 temporalmente las consultas de consumo.";
389
+ case "unauthorized":
390
+ return "La conexi\xF3n de OpenAI necesita volver a autenticarse.";
391
+ case "forbidden":
392
+ return "OpenAI no permiti\xF3 consultar el consumo de esta cuenta.";
393
+ case "malformed":
394
+ return "La respuesta de consumo no tiene un formato reconocido.";
395
+ default:
396
+ return "No se pudo obtener el consumo de OpenAI.";
397
+ }
398
+ }
399
+ function safeErrorAction(kind) {
400
+ switch (kind) {
401
+ case "unauthorized":
402
+ return "/connect \u2192 OpenAI";
403
+ case "forbidden":
404
+ return "Comprueba que la cuenta tenga acceso a la suscripci\xF3n.";
405
+ case "malformed":
406
+ return "Actualiza el plugin y vuelve a intentarlo.";
407
+ default:
408
+ return "Comprueba la conexi\xF3n y vuelve a intentarlo.";
409
+ }
410
+ }
411
+ function activateDetailRefresh(button, onRefresh) {
412
+ if (button === 0)
413
+ onRefresh();
414
+ }
415
+ function detailWindow(window) {
416
+ const availability = window.availability ?? (window.applicable ? "applicable" : "not_applicable");
417
+ if (availability === "not_applicable") {
418
+ return {
419
+ label: safeText(window.label),
420
+ applicable: false,
421
+ availability,
422
+ remaining: "no aplica",
423
+ used: "no aplica",
424
+ reset: "no disponible"
425
+ };
426
+ }
427
+ if (availability === "unknown") {
428
+ return {
429
+ label: safeText(window.label),
430
+ applicable: false,
431
+ availability,
432
+ remaining: "sin datos",
433
+ used: "sin datos",
434
+ reset: "no disponible"
435
+ };
436
+ }
437
+ const percent = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100 ? formatPercent(value) : "no disponible";
438
+ return {
439
+ label: safeText(window.label),
440
+ applicable: true,
441
+ availability,
442
+ remaining: percent(window.remainingPercent),
443
+ used: percent(window.usedPercent),
444
+ reset: formatResetTime(window.resetsAt)
445
+ };
446
+ }
447
+ function retryCountdown(retryAt, now) {
448
+ const remaining = Math.max(0, retryAt - now);
449
+ if (remaining < 1000)
450
+ return "menos de 1 s";
451
+ const seconds = Math.ceil(remaining / 1000);
452
+ if (seconds < 60)
453
+ return `${seconds} s`;
454
+ return `${Math.ceil(seconds / 60)} min`;
455
+ }
456
+ function buildDetailViewModel(panel, now = Date.now(), refreshing = false) {
457
+ const snapshot = panel.snapshot;
458
+ const safeError = panel.error ? safeErrorMessage(panel.error.kind) : null;
459
+ const rawMessage = panel.state === "stale" ? `Se muestra la \xFAltima lectura disponible.${safeError ? ` ${safeError}` : ""}` : safeError ?? (panel.state === "loading" ? "Cargando consumo\u2026" : panel.state === "no_subscription" ? "Esta conexi\xF3n no tiene cuotas de suscripci\xF3n que mostrar." : panel.state === "needs_auth" ? "Vuelve a conectar OpenAI para consultar el consumo." : panel.state === "error" ? "No se pudo obtener el consumo." : null);
460
+ const message = rawMessage ? safeText(rawMessage) : null;
461
+ const action = (panel.error ? safeErrorAction(panel.error.kind) : null) ?? (panel.state === "needs_auth" ? "/connect \u2192 OpenAI" : panel.state === "error" ? "Vuelve a intentarlo." : null);
462
+ const refreshNotice = panel.refreshNotice;
463
+ return {
464
+ title: "Detalle del consumo de OpenAI",
465
+ plan: safeText(snapshot?.plan?.label ?? snapshot?.account.planHint),
466
+ account: maskedAccount(panel),
467
+ scope: "Alcance de cuota: desconocido (no verificado para esta fuente y cuenta).",
468
+ origin: originLabel(snapshot?.origin.kind),
469
+ windows: snapshot?.windows.map(detailWindow) ?? [],
470
+ message,
471
+ action: action ? safeText(action) : null,
472
+ refreshMessage: refreshNotice ? refreshNotice.kind === "limited" ? `Se conserva el dato actual. Podr\xE1s reintentar en ${retryCountdown(refreshNotice.retryAt, now)}.` : `Ya hay una consulta en curso. Podr\xE1s reintentar en ${retryCountdown(refreshNotice.retryAt, now)}.` : null,
473
+ refreshing
474
+ };
475
+ }
476
+ function Detail(props) {
477
+ const base = props.theme.text?.base ?? "white";
478
+ const muted = props.theme.text?.muted ?? base;
479
+ const view = () => buildDetailViewModel(props.panel, Date.now(), props.refreshing);
480
+ return (() => {
481
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$6 = _$createElement("text"), _el$7 = _$createTextNode(`Plan: `), _el$8 = _$createElement("text"), _el$9 = _$createElement("text"), _el$0 = _$createElement("text"), _el$1 = _$createTextNode(`Origen: `), _el$13 = _$createElement("text"), _el$14 = _$createElement("text");
482
+ _$insertNode(_el$, _el$2);
483
+ _$insertNode(_el$, _el$6);
484
+ _$insertNode(_el$, _el$8);
485
+ _$insertNode(_el$, _el$9);
486
+ _$insertNode(_el$, _el$0);
487
+ _$insertNode(_el$, _el$13);
488
+ _$insertNode(_el$, _el$14);
489
+ _$setProp(_el$, "flexDirection", "column");
490
+ _$setProp(_el$, "padding", 1);
491
+ _$setProp(_el$2, "fg", base);
492
+ _$insert(_el$2, () => view().title);
493
+ _$insert(_el$, _$createComponent(Show, {
494
+ get when() {
495
+ return view().message;
496
+ },
497
+ get children() {
498
+ var _el$3 = _$createElement("text");
499
+ _$setProp(_el$3, "fg", muted);
500
+ _$insert(_el$3, () => view().message);
501
+ return _el$3;
502
+ }
503
+ }), _el$6);
504
+ _$insert(_el$, _$createComponent(For, {
505
+ get each() {
506
+ return view().windows;
507
+ },
508
+ children: (window) => (() => {
509
+ var _el$16 = _$createElement("box"), _el$17 = _$createElement("text"), _el$18 = _$createElement("text"), _el$19 = _$createTextNode(`Restante: `), _el$20 = _$createTextNode(` \xB7 Consumido: `), _el$21 = _$createElement("text"), _el$22 = _$createTextNode(`Reinicio: `);
510
+ _$insertNode(_el$16, _el$17);
511
+ _$insertNode(_el$16, _el$18);
512
+ _$insertNode(_el$16, _el$21);
513
+ _$setProp(_el$16, "flexDirection", "column");
514
+ _$setProp(_el$16, "paddingTop", 1);
515
+ _$setProp(_el$17, "fg", base);
516
+ _$insert(_el$17, () => window.label, null);
517
+ _$insert(_el$17, () => window.availability === "not_applicable" ? " \xB7 no aplica" : "", null);
518
+ _$insertNode(_el$18, _el$19);
519
+ _$insertNode(_el$18, _el$20);
520
+ _$setProp(_el$18, "fg", muted);
521
+ _$insert(_el$18, () => window.remaining, _el$20);
522
+ _$insert(_el$18, () => window.used, null);
523
+ _$insertNode(_el$21, _el$22);
524
+ _$setProp(_el$21, "fg", muted);
525
+ _$insert(_el$21, () => window.reset, null);
526
+ _$effect((_$p) => _$setProp(_el$17, "attributes", TextAttributes.BOLD, _$p));
527
+ return _el$16;
528
+ })()
529
+ }), _el$6);
530
+ _$insert(_el$, _$createComponent(Show, {
531
+ get when() {
532
+ return _$memo(() => !!props.panel.snapshot)() && props.panel.snapshot.windows.length === 0;
533
+ },
534
+ get children() {
535
+ var _el$4 = _$createElement("text");
536
+ _$insertNode(_el$4, _$createTextNode(`Sin ventanas de cuota aplicables.`));
537
+ _$setProp(_el$4, "fg", muted);
538
+ return _el$4;
539
+ }
540
+ }), _el$6);
541
+ _$insertNode(_el$6, _el$7);
542
+ _$setProp(_el$6, "fg", muted);
543
+ _$insert(_el$6, () => view().plan, null);
544
+ _$setProp(_el$8, "fg", muted);
545
+ _$insert(_el$8, () => view().account);
546
+ _$setProp(_el$9, "fg", muted);
547
+ _$insert(_el$9, () => view().scope);
548
+ _$insertNode(_el$0, _el$1);
549
+ _$setProp(_el$0, "fg", muted);
550
+ _$insert(_el$0, () => view().origin, null);
551
+ _$insert(_el$, _$createComponent(Show, {
552
+ get when() {
553
+ return view().action;
554
+ },
555
+ get children() {
556
+ var _el$10 = _$createElement("text"), _el$11 = _$createTextNode(`Acci\xF3n: `);
557
+ _$insertNode(_el$10, _el$11);
558
+ _$setProp(_el$10, "fg", muted);
559
+ _$insert(_el$10, () => view().action, null);
560
+ return _el$10;
561
+ }
562
+ }), _el$13);
563
+ _$insert(_el$, _$createComponent(Show, {
564
+ get when() {
565
+ return view().refreshMessage;
566
+ },
567
+ get children() {
568
+ var _el$12 = _$createElement("text");
569
+ _$setProp(_el$12, "fg", muted);
570
+ _$insert(_el$12, () => view().refreshMessage);
571
+ return _el$12;
572
+ }
573
+ }), _el$13);
574
+ _$setProp(_el$13, "fg", base);
575
+ _$setProp(_el$13, "onMouseUp", (event) => activateDetailRefresh(event.button, props.onRefresh));
576
+ _$insert(_el$13, () => view().refreshing ? "Actualizando\u2026" : "Actualizar consumo");
577
+ _$insertNode(_el$14, _$createTextNode(`Volver`));
578
+ _$setProp(_el$14, "fg", muted);
579
+ _$setProp(_el$14, "onMouseUp", (event) => activateDetailRefresh(event.button, props.onBack));
580
+ _$effect((_$p) => _$setProp(_el$2, "attributes", TextAttributes.BOLD, _$p));
581
+ return _el$;
582
+ })();
583
+ }
584
+
585
+ // src/ui/sidebar.tsx
586
+ import { insert as _$insert2 } from "@opentui/solid";
587
+ import { createComponent as _$createComponent2 } from "@opentui/solid";
588
+ import { effect as _$effect2 } from "@opentui/solid";
589
+ import { createTextNode as _$createTextNode2 } from "@opentui/solid";
590
+ import { insertNode as _$insertNode2 } from "@opentui/solid";
591
+ import { memo as _$memo2 } from "@opentui/solid";
592
+ import { setProp as _$setProp2 } from "@opentui/solid";
593
+ import { createElement as _$createElement2 } from "@opentui/solid";
594
+ import { TextAttributes as TextAttributes2 } from "@opentui/core";
595
+ import { For as For2, Show as Show2 } from "solid-js";
596
+
597
+ // src/ui/theme.ts
598
+ function usageTheme(theme) {
599
+ return {
600
+ text: {
601
+ base: theme.text?.base ?? "white",
602
+ muted: theme.text?.muted ?? theme.text?.base ?? "white"
603
+ }
604
+ };
605
+ }
606
+
607
+ // src/ui/sidebar.tsx
608
+ var SIDEBAR_WIDTH_CAP = 28;
609
+ var RESERVED_MAIN_CONTENT_WIDTH = 64;
610
+ var MINIMUM_USEFUL_WIDTH = 14;
611
+ var RESERVED_HOST_CONTENT_HEIGHT = 7;
612
+ function sidebarWidthForTerminal(terminalWidth, terminalHeight = Number.POSITIVE_INFINITY, requiredRows = 5) {
613
+ if (!Number.isFinite(terminalWidth) || !Number.isFinite(requiredRows) || requiredRows < 0 || Number.isFinite(terminalHeight) && Math.floor(terminalHeight) - RESERVED_HOST_CONTENT_HEIGHT < requiredRows) {
614
+ return null;
615
+ }
616
+ const available = Math.min(SIDEBAR_WIDTH_CAP, Math.floor(terminalWidth) - RESERVED_MAIN_CONTENT_WIDTH);
617
+ return available >= MINIMUM_USEFUL_WIDTH ? available : null;
618
+ }
619
+ function compactLabel(window) {
620
+ if (window.kind === "five_hour")
621
+ return "5h";
622
+ if (window.kind === "weekly")
623
+ return "Sem";
624
+ return window.label;
625
+ }
626
+ function retrySuffix(panel, now) {
627
+ const retryAt = panel.retryAt ?? panel.refreshNotice?.retryAt;
628
+ if (typeof retryAt !== "number" || !Number.isFinite(retryAt))
629
+ return "";
630
+ const seconds = Math.max(0, Math.ceil((retryAt - now) / 1000));
631
+ return ` \xB7 reintento en ${seconds}s`;
632
+ }
633
+ function initialFailureLines(panel, width, now, requiresAuth) {
634
+ const retryAt = panel.retryAt ?? panel.refreshNotice?.retryAt;
635
+ const retrySeconds = typeof retryAt === "number" && Number.isFinite(retryAt) ? Math.max(0, Math.ceil((retryAt - now) / 1000)) : null;
636
+ if (width < MINIMUM_USEFUL_WIDTH) {
637
+ const action = requiresAuth ? "/connect" : "/usage-detail";
638
+ return [`Consumo \xB7 ${action}${retrySuffix(panel, now)}`.slice(0, width)];
639
+ }
640
+ if (width < 27) {
641
+ return ["5h \xB7 \u2014", "Sem \xB7 \u2014", "Sin lectura", ...requiresAuth ? ["Reconecta OpenAI", "/connect"] : ["/usage-detail"], ...!requiresAuth && retrySeconds !== null ? [`reintento en ${retrySeconds}s`] : []].map((line) => line.slice(0, width));
642
+ }
643
+ return ["5 h \xB7 \u2014", "Semanal \xB7 \u2014", requiresAuth ? "Sin lectura \xB7 Requiere acceso" : "Sin lectura \xB7 Fallo inicial", requiresAuth ? "Reconecta OpenAI \xB7 /connect" : "Acci\xF3n: /usage-detail", ...!requiresAuth && retrySeconds !== null ? [`reintento en ${retrySeconds}s`] : []].map((line) => line.slice(0, width));
644
+ }
645
+ function progressBar(remaining, size) {
646
+ const filled = Math.max(0, Math.min(size, Math.round(remaining / 100 * size)));
647
+ return `[${"\u2501".repeat(filled)}${"\u2500".repeat(size - filled)}]`;
648
+ }
649
+ function activateSidebarDetail(button, onOpenDetail) {
650
+ if (button === 0)
651
+ onOpenDetail?.();
652
+ }
653
+ function buildWindowLine(window, width, labelWidth) {
654
+ const compact = width < 27;
655
+ const label = (compact ? compactLabel(window) : window.label).slice(0, labelWidth);
656
+ const alignedLabel = label.padEnd(labelWidth);
657
+ const availability = window.availability ?? (window.applicable ? "applicable" : "not_applicable");
658
+ if (availability === "unknown") {
659
+ const line = compact ? `${alignedLabel} sin datos`.trim() : `${alignedLabel} sin datos`;
660
+ return line.slice(0, width);
661
+ }
662
+ if (availability === "not_applicable") {
663
+ const line = compact ? `${alignedLabel} n/a`.trim() : `${alignedLabel} no aplica`;
664
+ return line.slice(0, width);
665
+ }
666
+ if (window.remainingPercent === null) {
667
+ const line = compact ? `${alignedLabel} sin datos`.trim() : `${alignedLabel} sin datos`;
668
+ return line.slice(0, width);
669
+ }
670
+ if (compact) {
671
+ const percent = formatPercent(window.remainingPercent);
672
+ const smallBar = width >= 14 ? progressBar(window.remainingPercent, 1) : "";
673
+ const line = `${alignedLabel}${smallBar ? ` ${smallBar}` : ""} ${percent}`;
674
+ return line.slice(0, width);
675
+ }
676
+ const percent = formatPercent(window.remainingPercent);
677
+ const fixedLength = labelWidth + 2 + 2 + 2 + 4;
678
+ const barSize = Math.max(2, Math.min(12, width - fixedLength));
679
+ const line = `${alignedLabel} ${progressBar(window.remainingPercent, barSize)} ${percent.padStart(4)}`;
680
+ return line.length <= width ? line : line.slice(0, width);
681
+ }
682
+ function buildSidebarLines(panel, options = {}) {
683
+ if (!panel.preferences.enabled || !panel.preferences.visible)
684
+ return [];
685
+ const width = Math.max(1, Math.floor(options.width ?? 80));
686
+ const now = options.now ?? Date.now();
687
+ if (panel.state === "loading")
688
+ return ["5 h \xB7 cargando\u2026".slice(0, width), "Semanal \xB7 cargando\u2026".slice(0, width)];
689
+ if (panel.state === "no_subscription")
690
+ return ["Sin cuota de suscripci\xF3n".slice(0, width)];
691
+ if (panel.state === "needs_auth" || panel.state === "error" && !panel.snapshot && panel.error?.kind === "unauthorized") {
692
+ return initialFailureLines(panel, width, now, true);
693
+ }
694
+ if (panel.state === "error" && !panel.snapshot) {
695
+ return initialFailureLines(panel, width, now, false);
696
+ }
697
+ if (!panel.snapshot)
698
+ return ["Consumo no disponible".slice(0, width)];
699
+ const windows = panel.snapshot.windows;
700
+ if (windows.length === 0)
701
+ return ["Sin ventanas de cuota".slice(0, width)];
702
+ const compactWindows = windows.filter((window) => window.kind === "five_hour" || window.kind === "weekly");
703
+ if (compactWindows.length === 0) {
704
+ return ["5 h \xB7 sin datos", "Semanal \xB7 sin datos"].map((line) => line.slice(0, width));
705
+ }
706
+ const compact = width < 27;
707
+ const labelWidth = Math.min(compact ? 3 : 10, Math.max(...compactWindows.map((window) => (compact ? compactLabel(window) : window.label).length)));
708
+ return compactWindows.map((window) => buildWindowLine(window, width, labelWidth));
709
+ }
710
+ function sidebarRequiredRows(panel, refreshing = false, width = 80) {
711
+ if (!panel.preferences.enabled || !panel.preferences.visible)
712
+ return 0;
713
+ const lines = buildSidebarLines(panel, {
714
+ width
715
+ });
716
+ if (lines.length === 0)
717
+ return 0;
718
+ const contentRows = 1 + lines.length * 2;
719
+ return contentRows + (refreshing ? 2 : 0);
720
+ }
721
+ function Sidebar(props) {
722
+ const width = props.width;
723
+ if (typeof width !== "number" || !Number.isFinite(width) || width < MINIMUM_USEFUL_WIDTH) {
724
+ return null;
725
+ }
726
+ const tokens = usageTheme(props.theme);
727
+ const lines = () => buildSidebarLines(props.panel, {
728
+ width
729
+ });
730
+ return (() => {
731
+ var _el$ = _$createElement2("box");
732
+ _$setProp2(_el$, "width", width);
733
+ _$setProp2(_el$, "flexDirection", "column");
734
+ _$setProp2(_el$, "onMouseUp", (event) => activateSidebarDetail(event.button, props.onOpenDetail));
735
+ _$insert2(_el$, _$createComponent2(Show2, {
736
+ get when() {
737
+ return _$memo2(() => !!props.panel.preferences.enabled)() && props.panel.preferences.visible;
738
+ },
739
+ get children() {
740
+ var _el$2 = _$createElement2("text");
741
+ _$insertNode2(_el$2, _$createTextNode2(`Uso OpenAI`));
742
+ _$setProp2(_el$2, "marginBottom", 1);
743
+ _$effect2((_p$) => {
744
+ var _v$ = tokens.text.base, _v$2 = TextAttributes2.BOLD;
745
+ _v$ !== _p$.e && (_p$.e = _$setProp2(_el$2, "fg", _v$, _p$.e));
746
+ _v$2 !== _p$.t && (_p$.t = _$setProp2(_el$2, "attributes", _v$2, _p$.t));
747
+ return _p$;
748
+ }, {
749
+ e: undefined,
750
+ t: undefined
751
+ });
752
+ return _el$2;
753
+ }
754
+ }), null);
755
+ _$insert2(_el$, _$createComponent2(For2, {
756
+ get each() {
757
+ return lines();
758
+ },
759
+ children: (line, index) => (() => {
760
+ var _el$6 = _$createElement2("text");
761
+ _$insert2(_el$6, line);
762
+ _$effect2((_p$) => {
763
+ var _v$3 = tokens.text.base, _v$4 = index() < lines().length - 1 ? 1 : 0;
764
+ _v$3 !== _p$.e && (_p$.e = _$setProp2(_el$6, "fg", _v$3, _p$.e));
765
+ _v$4 !== _p$.t && (_p$.t = _$setProp2(_el$6, "marginBottom", _v$4, _p$.t));
766
+ return _p$;
767
+ }, {
768
+ e: undefined,
769
+ t: undefined
770
+ });
771
+ return _el$6;
772
+ })()
773
+ }), null);
774
+ _$insert2(_el$, _$createComponent2(Show2, {
775
+ get when() {
776
+ return _$memo2(() => !!(props.panel.preferences.enabled && props.panel.preferences.visible))() && props.refreshing;
777
+ },
778
+ get children() {
779
+ var _el$4 = _$createElement2("text");
780
+ _$insertNode2(_el$4, _$createTextNode2(`Actualizando consumo\u2026`));
781
+ _$setProp2(_el$4, "marginTop", 1);
782
+ _$effect2((_$p) => _$setProp2(_el$4, "fg", tokens.text.muted, _$p));
783
+ return _el$4;
784
+ }
785
+ }), null);
786
+ return _el$;
787
+ })();
788
+ }
789
+
790
+ // src/tui.tsx
791
+ var DETAIL_ROUTE_NAME = "openai-usage-detail";
792
+ var initialPanel = {
793
+ state: "loading",
794
+ snapshot: null,
795
+ preferences: {
796
+ ...DEFAULT_PREFERENCES
797
+ }
798
+ };
799
+ function describeRefreshError(_error) {
800
+ return {
801
+ kind: "unknown",
802
+ message: "No se pudo actualizar el consumo de OpenAI.",
803
+ action: "Comprueba la conexi\xF3n y vuelve a intentarlo."
804
+ };
805
+ }
806
+ function locationKey(directory) {
807
+ const normalized = directory.replaceAll("\\", "/").replace(/\/+$/, "") || "/";
808
+ return /^[A-Za-z]:\//.test(normalized) ? normalized.toLowerCase() : normalized;
809
+ }
810
+ var tui_default = Plugin.define({
811
+ id: "openai-usage.tui",
812
+ setup(context) {
813
+ const instanceId = globalThis.crypto.randomUUID();
814
+ const [panel, setPanel] = createSignal(initialPanel);
815
+ const [refreshing, setRefreshing] = createSignal(false);
816
+ const usage = context.client.rpc(Usage);
817
+ const location = context.location ?? context.data.location.default();
818
+ const rpcOptions = {
819
+ location
820
+ };
821
+ const unsubscribe = usage.events.on("updated", (event) => {
822
+ if (locationKey(event.location.directory) !== locationKey(location.directory))
823
+ return;
824
+ const update = event.data;
825
+ if (update.instanceId !== instanceId || !update.data)
826
+ return;
827
+ setPanel(update.data);
828
+ });
829
+ const openDetail = () => {
830
+ context.ui.router.navigate({
831
+ type: "plugin",
832
+ name: DETAIL_ROUTE_NAME
833
+ });
834
+ };
835
+ const sidebarWidth = () => {
836
+ const terminalWidth = context.renderer.terminalWidth;
837
+ const terminalHeight = context.renderer.terminalHeight;
838
+ const availableWidth = sidebarWidthForTerminal(terminalWidth, terminalHeight, 0);
839
+ if (availableWidth === null)
840
+ return null;
841
+ return sidebarWidthForTerminal(terminalWidth, terminalHeight, sidebarRequiredRows(panel(), refreshing(), availableWidth));
842
+ };
843
+ const refreshNow = async () => {
844
+ if (refreshing())
845
+ return;
846
+ setRefreshing(true);
847
+ try {
848
+ const snapshot = await usage.refresh({
849
+ instanceId
850
+ }, rpcOptions);
851
+ setPanel(snapshot);
852
+ } catch (error) {
853
+ setPanel((current) => ({
854
+ ...current,
855
+ state: current.snapshot ? "stale" : "error",
856
+ error: describeRefreshError(error)
857
+ }));
858
+ } finally {
859
+ setRefreshing(false);
860
+ }
861
+ };
862
+ const unregisterRoute = context.ui.router.register({
863
+ name: DETAIL_ROUTE_NAME,
864
+ render: () => _$createComponent3(Detail, {
865
+ get panel() {
866
+ return panel();
867
+ },
868
+ get refreshing() {
869
+ return refreshing();
870
+ },
871
+ onRefresh: () => void refreshNow(),
872
+ onBack: () => context.ui.router.navigate({
873
+ type: "home"
874
+ }),
875
+ get theme() {
876
+ return context.theme;
877
+ }
878
+ })
879
+ });
880
+ context.keymap.layer(() => ({
881
+ mode: "global",
882
+ commands: [{
883
+ id: "openai-usage.detail",
884
+ title: "Abrir detalle del consumo de OpenAI",
885
+ description: "Muestra el desglose y el origen de las cuotas.",
886
+ group: "Consumo de OpenAI",
887
+ bind: false,
888
+ palette: true,
889
+ slash: {
890
+ name: "usage-detail",
891
+ aliases: ["openai-usage-detail"]
892
+ },
893
+ enabled: () => panel().preferences.enabled,
894
+ run: openDetail
895
+ }, {
896
+ id: "openai-usage.refresh",
897
+ title: "Actualizar consumo de OpenAI",
898
+ description: "Consulta ahora el consumo respetando el intervalo m\xEDnimo.",
899
+ group: "Consumo de OpenAI",
900
+ bind: false,
901
+ palette: true,
902
+ slash: {
903
+ name: "usage-refresh",
904
+ aliases: ["openai-usage-refresh"]
905
+ },
906
+ enabled: () => panel().preferences.enabled,
907
+ run: refreshNow
908
+ }]
909
+ }));
910
+ const unregisterSlot = context.ui.slot({
911
+ append: "sidebar.content",
912
+ render: () => _$createComponent3(Sidebar, {
913
+ get panel() {
914
+ return panel();
915
+ },
916
+ get refreshing() {
917
+ return refreshing();
918
+ },
919
+ get theme() {
920
+ return context.theme;
921
+ },
922
+ get width() {
923
+ return sidebarWidth();
924
+ },
925
+ onOpenDetail: openDetail
926
+ })
927
+ });
928
+ usage.start({
929
+ instanceId
930
+ }, rpcOptions).then(() => usage.snapshot({
931
+ instanceId
932
+ }, rpcOptions)).then((snapshot) => {
933
+ setPanel(snapshot);
934
+ }).catch(() => {
935
+ setPanel((current) => ({
936
+ ...current,
937
+ state: current.snapshot ? "stale" : "error",
938
+ error: {
939
+ kind: "unknown",
940
+ message: "No se pudo iniciar el panel de consumo.",
941
+ action: "Comprueba que el plugin de servidor est\xE9 cargado."
942
+ }
943
+ }));
944
+ });
945
+ return () => {
946
+ unsubscribe();
947
+ unregisterRoute();
948
+ unregisterSlot();
949
+ usage.stop({
950
+ instanceId
951
+ }, rpcOptions).catch(() => {
952
+ return;
953
+ });
954
+ };
955
+ }
956
+ });
957
+ export {
958
+ tui_default as default,
959
+ describeRefreshError
960
+ };