usage-panel-opencode 0.1.0 → 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/README.md +1 -1
- package/dist/tui.js +960 -0
- package/package.json +6 -3
- package/src/core/usage-client.ts +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Instala las dependencias (`bun install`). Para una instalación publicada, añad
|
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Para desarrollo en este repositorio, usa `"plugins": ["./"]
|
|
25
|
+
Para desarrollo en este repositorio, ejecuta `bun run build` y usa `"plugins": ["./"]`; vuelve a ejecutar el build tras cambiar el TUI. El paquete incluye piezas separadas de servidor y TUI. `prepack` compila el TUI con el transform de Solid antes de publicar, sin depender del `bunfig.toml` de la aplicación consumidora.
|
|
26
26
|
|
|
27
27
|
## Preferencias
|
|
28
28
|
|
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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-panel-opencode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenCode v2 plugin for viewing ChatGPT subscription usage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"bun": ">=1.2.0"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"src"
|
|
11
|
+
"src",
|
|
12
|
+
"dist"
|
|
12
13
|
],
|
|
13
14
|
"exports": {
|
|
14
15
|
".": {
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
},
|
|
22
23
|
"./tui": {
|
|
23
24
|
"types": "./src/tui.tsx",
|
|
24
|
-
"import": "./
|
|
25
|
+
"import": "./dist/tui.js"
|
|
25
26
|
},
|
|
26
27
|
"./rpc": {
|
|
27
28
|
"types": "./src/rpc.ts",
|
|
@@ -29,6 +30,8 @@
|
|
|
29
30
|
}
|
|
30
31
|
},
|
|
31
32
|
"scripts": {
|
|
33
|
+
"build": "bun run scripts/build.ts",
|
|
34
|
+
"prepack": "bun run build",
|
|
32
35
|
"test": "bun test",
|
|
33
36
|
"typecheck": "tsc --noEmit",
|
|
34
37
|
"lint": "biome check ."
|
package/src/core/usage-client.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface FetchUsageOptions {
|
|
|
37
37
|
|
|
38
38
|
const USAGE_ENDPOINT = 'https://chatgpt.com/backend-api/wham/usage';
|
|
39
39
|
const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9_-]{4,64}$/;
|
|
40
|
-
const USER_AGENT = 'usage-panel-opencode/0.1.
|
|
40
|
+
const USER_AGENT = 'usage-panel-opencode/0.1.1';
|
|
41
41
|
|
|
42
42
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
43
43
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|