opencode-auth-load-balancer 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -12
- package/dist/accounts.d.ts +22 -2
- package/dist/index.d.ts +7 -12
- package/dist/index.js +46026 -14563
- package/dist/legacy.d.ts +25 -0
- package/dist/login-health.d.ts +12 -0
- package/dist/notify.d.ts +6 -0
- package/dist/pairing.d.ts +22 -0
- package/dist/pending/coordinator.d.ts +5 -1
- package/dist/pending/v2.d.ts +3 -0
- package/dist/pool/paths.d.ts +8 -0
- package/dist/providers/anthropic/constants.d.ts +21 -0
- package/dist/providers/anthropic/oauth.d.ts +9 -0
- package/dist/providers/anthropic/setup-token.d.ts +14 -0
- package/dist/providers/anthropic/usage.d.ts +13 -0
- package/dist/providers/types.d.ts +27 -0
- package/dist/refresh.d.ts +12 -0
- package/dist/status.d.ts +6 -0
- package/dist/sync/apply.d.ts +66 -0
- package/dist/sync/crypto.d.ts +19 -0
- package/dist/sync/cycle.d.ts +19 -0
- package/dist/sync/engine.d.ts +44 -0
- package/dist/sync/errors.d.ts +9 -0
- package/dist/sync/gist.d.ts +33 -0
- package/dist/sync/github-auth.d.ts +15 -0
- package/dist/sync/intent.d.ts +29 -0
- package/dist/sync/loop.d.ts +40 -0
- package/dist/sync/merge.d.ts +49 -0
- package/dist/sync/origin.d.ts +2 -0
- package/dist/sync/own.d.ts +31 -0
- package/dist/sync/payload.d.ts +50 -0
- package/dist/sync/start.d.ts +5 -0
- package/dist/sync/state.d.ts +79 -0
- package/dist/sync/timing.d.ts +4 -0
- package/dist/token-mint.d.ts +25 -0
- package/dist/types.d.ts +75 -0
- package/dist/util.d.ts +7 -0
- package/dist/v1.d.ts +1 -0
- package/dist/v1.js +18393 -0
- package/dist/v2-rpc.d.ts +62 -0
- package/dist/v2.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +46 -8
- package/tui/auth-load-balancer-rpc.ts +60 -0
- package/tui/auth-load-balancer-tui.logic.ts +363 -7
- package/tui/auth-load-balancer-tui.view.tsx +299 -107
- package/tui/auth-load-balancer-v2.ts +10 -0
- package/tui/auth-load-balancer-v2.view.tsx +242 -0
- package/tui.ts +2 -0
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* per provider, sorted by the scheduler's score (highest = use next), showing
|
|
13
13
|
* the score + use-order, usage + reset countdowns, in-use marker, and state.
|
|
14
14
|
* Click any account row for a menu to Rename (prompt), Disable/Enable
|
|
15
|
-
* (toggle), Re-login (provider OAuth),
|
|
15
|
+
* (toggle), Re-login (provider OAuth), pair a Claude row's other login
|
|
16
|
+
* (setup-token for inference / OAuth for usage), or Delete (confirm) it.
|
|
16
17
|
*
|
|
17
18
|
* The ranking is computed by the SAME code the server scheduler uses — imported from a
|
|
18
19
|
* byte-identical copy of src/scheduler/score-core.ts installed alongside this file
|
|
@@ -30,25 +31,40 @@ import {
|
|
|
30
31
|
utilOf,
|
|
31
32
|
} from './auth-load-balancer-scoring'
|
|
32
33
|
import {
|
|
34
|
+
awaitSyncResult,
|
|
33
35
|
cfg,
|
|
36
|
+
cleanLabel,
|
|
34
37
|
clearReloginTargetInPool,
|
|
35
38
|
compareAscii,
|
|
39
|
+
credentialTag,
|
|
36
40
|
deleteFromPool,
|
|
41
|
+
discardSyncIntent,
|
|
42
|
+
hasLostLogin,
|
|
43
|
+
isGistLinkShape,
|
|
44
|
+
lostLoginLines,
|
|
37
45
|
MANUAL_DISABLED_REASON,
|
|
38
46
|
pct,
|
|
39
47
|
pickAuthMethodIndex,
|
|
40
48
|
type PoolShape,
|
|
41
49
|
readPool,
|
|
50
|
+
readShareLink,
|
|
51
|
+
readSyncView,
|
|
42
52
|
renameInPool,
|
|
53
|
+
rowWarnings,
|
|
43
54
|
sessionAccountId,
|
|
44
55
|
sessionFallback,
|
|
45
56
|
setDisabledInPool,
|
|
46
57
|
setReloginTargetInPool,
|
|
47
58
|
stateOf,
|
|
59
|
+
type SyncAction,
|
|
60
|
+
syncLines,
|
|
61
|
+
syncMenu,
|
|
62
|
+
type SyncMenuId,
|
|
48
63
|
tierResets,
|
|
49
64
|
toScore,
|
|
50
65
|
until,
|
|
51
66
|
winPct,
|
|
67
|
+
writeSyncIntent,
|
|
52
68
|
} from './auth-load-balancer-tui.logic'
|
|
53
69
|
|
|
54
70
|
// NOTE: ships its OWN copy of `src/status.ts`'s `PROVIDER_NAMES` by design —
|
|
@@ -113,6 +129,14 @@ function usePool() {
|
|
|
113
129
|
return sample
|
|
114
130
|
}
|
|
115
131
|
|
|
132
|
+
/** One poll sample of the sync state: role, last sync, latest outcome (never the key). */
|
|
133
|
+
function useSync() {
|
|
134
|
+
const [view, setView] = createSignal(readSyncView())
|
|
135
|
+
const timer = setInterval(() => setView(readSyncView()), POLL_MS)
|
|
136
|
+
onCleanup(() => clearInterval(timer))
|
|
137
|
+
return view
|
|
138
|
+
}
|
|
139
|
+
|
|
116
140
|
/** Shared theme-color accessor used identically by `BottomBar` and `SidebarPanel`. */
|
|
117
141
|
function themeColor(api: TuiPluginApi) {
|
|
118
142
|
return () => api.theme.current
|
|
@@ -131,6 +155,8 @@ interface Chip extends WindowDisplay {
|
|
|
131
155
|
label: string
|
|
132
156
|
/** Raw model ids (requested → served) when this session runs on a fallback model, else undefined. */
|
|
133
157
|
fallback?: { from: string; to: string }
|
|
158
|
+
/** `<label> <warning>` for each login warning on this provider's rows (`rowWarnings`). */
|
|
159
|
+
warnings: string[]
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
/** Always-visible bottom bar (app_bottom): the in-use account per provider. */
|
|
@@ -159,7 +185,7 @@ function BottomBar(props: { api: TuiPluginApi }) {
|
|
|
159
185
|
if (!a) continue
|
|
160
186
|
out.push({
|
|
161
187
|
name: providerLabel(providerID),
|
|
162
|
-
label: a.label,
|
|
188
|
+
label: cleanLabel(a.label),
|
|
163
189
|
weeklyPct: winPct(a.usage?.weekly, now),
|
|
164
190
|
weeklyReset: until(a.usage?.weekly?.resetAt, now),
|
|
165
191
|
hourlyPct: winPct(a.usage?.hourly, now),
|
|
@@ -167,6 +193,9 @@ function BottomBar(props: { api: TuiPluginApi }) {
|
|
|
167
193
|
// Only THIS session's fallback (keyed by sid); the `lastSelected` home-screen
|
|
168
194
|
// fallback path has no session, so no downgrade to surface there.
|
|
169
195
|
fallback: sessionFallback(p, providerID, sid),
|
|
196
|
+
warnings: accounts
|
|
197
|
+
.filter((x) => x.providerID === providerID)
|
|
198
|
+
.flatMap((x) => rowWarnings(x, now).map((w) => `${x.label} ${w}`)),
|
|
170
199
|
})
|
|
171
200
|
}
|
|
172
201
|
return out
|
|
@@ -186,6 +215,11 @@ function BottomBar(props: { api: TuiPluginApi }) {
|
|
|
186
215
|
</span>
|
|
187
216
|
) : null}
|
|
188
217
|
{` wk ${a.weeklyPct} (${a.weeklyReset}) · 5h ${a.hourlyPct} (${a.hourlyReset})`}
|
|
218
|
+
{a.warnings.length > 0 ? (
|
|
219
|
+
<span style={{ fg: color().warning }}>
|
|
220
|
+
{` ! ${a.warnings.join(' · ')}`}
|
|
221
|
+
</span>
|
|
222
|
+
) : null}
|
|
189
223
|
</text>
|
|
190
224
|
)}
|
|
191
225
|
</For>
|
|
@@ -202,8 +236,15 @@ interface Row extends WindowDisplay {
|
|
|
202
236
|
rank: number | null
|
|
203
237
|
state: string
|
|
204
238
|
manuallyDisabled: boolean
|
|
205
|
-
/** A static API
|
|
239
|
+
/** A static-credential row (API key, setup-token): re-login asks for a new one, not an OAuth sign-in. */
|
|
206
240
|
apiKey: boolean
|
|
241
|
+
/** `credentialTag`: `token` / `token+oauth` on a setup-token row, else ''. */
|
|
242
|
+
login: string
|
|
243
|
+
/** A login of this row was refused for good: its menu offers to re-login that one. */
|
|
244
|
+
oauthLost: boolean
|
|
245
|
+
tokenLost: boolean
|
|
246
|
+
/** Why each lost login stopped working (`lostLoginLines`). */
|
|
247
|
+
lostLines: string[]
|
|
207
248
|
}
|
|
208
249
|
interface Group {
|
|
209
250
|
provider: string
|
|
@@ -216,6 +257,7 @@ function SidebarPanel(props: {
|
|
|
216
257
|
sessionId: string | undefined
|
|
217
258
|
}) {
|
|
218
259
|
const sample = usePool()
|
|
260
|
+
const sync = useSync()
|
|
219
261
|
const color = themeColor(props.api)
|
|
220
262
|
const groups = createMemo<Group[]>(() => {
|
|
221
263
|
const { pool: p, now } = sample()
|
|
@@ -249,7 +291,7 @@ function SidebarPanel(props: {
|
|
|
249
291
|
if (available) rank += 1
|
|
250
292
|
return {
|
|
251
293
|
id: a.id,
|
|
252
|
-
label: a.label,
|
|
294
|
+
label: cleanLabel(a.label),
|
|
253
295
|
providerID,
|
|
254
296
|
current: sessionAcct === a.id,
|
|
255
297
|
score: available ? score : null,
|
|
@@ -266,9 +308,15 @@ function SidebarPanel(props: {
|
|
|
266
308
|
// Raw reads are finiteness-guarded inside tierResets: a hand-edited
|
|
267
309
|
// `1e999` entry (Infinity via JSON.parse) would otherwise render its
|
|
268
310
|
// tier annotation forever until the server heals the file.
|
|
269
|
-
state: stateOf(sa, tierResets(a, now), now),
|
|
311
|
+
state: [stateOf(sa, tierResets(a, now), now), ...rowWarnings(a, now)]
|
|
312
|
+
.filter(Boolean)
|
|
313
|
+
.join(' · '),
|
|
270
314
|
manuallyDisabled: a.disabledReason === MANUAL_DISABLED_REASON,
|
|
271
315
|
apiKey: a.refresh === '',
|
|
316
|
+
login: credentialTag(a),
|
|
317
|
+
oauthLost: hasLostLogin(a, 'oauth'),
|
|
318
|
+
tokenLost: hasLostLogin(a, 'token'),
|
|
319
|
+
lostLines: lostLoginLines(a, now),
|
|
272
320
|
}
|
|
273
321
|
})
|
|
274
322
|
return { provider: providerLabel(providerID), rows }
|
|
@@ -307,17 +355,22 @@ function SidebarPanel(props: {
|
|
|
307
355
|
)
|
|
308
356
|
}
|
|
309
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Run one of the provider's pooled logins onto the clicked row:
|
|
360
|
+
* `staticLogin` picks the setup-token / API-key login, else the OAuth one.
|
|
361
|
+
* `action` names the step in the dialog and toasts ("Re-login", …).
|
|
362
|
+
*/
|
|
310
363
|
async function openRelogin(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
apiKey: boolean,
|
|
364
|
+
row: Row,
|
|
365
|
+
staticLogin: boolean,
|
|
366
|
+
action: string,
|
|
315
367
|
): Promise<void> {
|
|
368
|
+
const { id, label, providerID } = row
|
|
316
369
|
try {
|
|
317
370
|
const methods = (await props.api.client.provider.auth()).data?.[
|
|
318
371
|
providerID
|
|
319
372
|
]
|
|
320
|
-
const method = pickAuthMethodIndex(methods,
|
|
373
|
+
const method = pickAuthMethodIndex(methods, staticLogin)
|
|
321
374
|
if (method === null) {
|
|
322
375
|
props.api.ui.toast({
|
|
323
376
|
variant: 'error',
|
|
@@ -355,7 +408,7 @@ function SidebarPanel(props: {
|
|
|
355
408
|
if (completed) {
|
|
356
409
|
props.api.ui.toast({
|
|
357
410
|
variant: 'success',
|
|
358
|
-
message:
|
|
411
|
+
message: `${action} complete for ${label}.`,
|
|
359
412
|
})
|
|
360
413
|
// The existing 3s usePool poll observes the consumed intent and
|
|
361
414
|
// cleared disabledReason; no manual state refresh is needed.
|
|
@@ -363,7 +416,7 @@ function SidebarPanel(props: {
|
|
|
363
416
|
clearReloginTargetInPool()
|
|
364
417
|
props.api.ui.toast({
|
|
365
418
|
variant: 'error',
|
|
366
|
-
message:
|
|
419
|
+
message: `${action} failed for ${label}.`,
|
|
367
420
|
})
|
|
368
421
|
}
|
|
369
422
|
dialog().clear()
|
|
@@ -372,7 +425,7 @@ function SidebarPanel(props: {
|
|
|
372
425
|
dialog().clear()
|
|
373
426
|
props.api.ui.toast({
|
|
374
427
|
variant: 'error',
|
|
375
|
-
message:
|
|
428
|
+
message: `${action} failed for ${label}.`,
|
|
376
429
|
})
|
|
377
430
|
}
|
|
378
431
|
}
|
|
@@ -382,7 +435,7 @@ function SidebarPanel(props: {
|
|
|
382
435
|
// to approve it for as long as the callback waits.
|
|
383
436
|
dialog().replace(() =>
|
|
384
437
|
props.api.ui.DialogAlert({
|
|
385
|
-
title:
|
|
438
|
+
title: `${action} "${label}"`,
|
|
386
439
|
message: `${auth.instructions}\n\n${auth.url}`,
|
|
387
440
|
onConfirm: () => dialog().clear(),
|
|
388
441
|
}),
|
|
@@ -393,7 +446,7 @@ function SidebarPanel(props: {
|
|
|
393
446
|
|
|
394
447
|
dialog().replace(() =>
|
|
395
448
|
props.api.ui.DialogPrompt({
|
|
396
|
-
title:
|
|
449
|
+
title: `${action} "${label}"`,
|
|
397
450
|
description: () => (
|
|
398
451
|
<box>
|
|
399
452
|
<text>{auth.instructions}</text>
|
|
@@ -421,46 +474,15 @@ function SidebarPanel(props: {
|
|
|
421
474
|
dialog().clear()
|
|
422
475
|
props.api.ui.toast({
|
|
423
476
|
variant: 'error',
|
|
424
|
-
message:
|
|
477
|
+
message: `${action} failed for ${label}.`,
|
|
425
478
|
})
|
|
426
479
|
}
|
|
427
480
|
}
|
|
428
481
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
// Esc (to blur itself) — so the menu needed TWO Esc presses to close. A plain
|
|
433
|
-
// clickable list has no input, so the dialog stack's own Esc binding closes it
|
|
434
|
-
// in ONE press. The menu is opened by a mouse click on the row, so mouse-driven
|
|
435
|
-
// options stay consistent (there is no keyboard path that opens it).
|
|
436
|
-
function openMenu(
|
|
437
|
-
id: string,
|
|
438
|
-
label: string,
|
|
439
|
-
providerID: string,
|
|
440
|
-
manuallyDisabled: boolean,
|
|
441
|
-
apiKey: boolean,
|
|
482
|
+
function showMenu(
|
|
483
|
+
title: string,
|
|
484
|
+
items: readonly { title: string; run: () => void }[],
|
|
442
485
|
): void {
|
|
443
|
-
const items: { title: string; run: () => void }[] = [
|
|
444
|
-
{ title: 'Rename', run: () => openRename(id, label) },
|
|
445
|
-
{
|
|
446
|
-
// Reversible (just a scheduler skip), so no confirm step — flip the
|
|
447
|
-
// pool flag and close, unlike Delete which drops the tokens.
|
|
448
|
-
title: manuallyDisabled
|
|
449
|
-
? 'Enable — include in selection'
|
|
450
|
-
: 'Disable — exclude from selection',
|
|
451
|
-
run: () => {
|
|
452
|
-
setDisabledInPool(id, !manuallyDisabled)
|
|
453
|
-
dialog().clear()
|
|
454
|
-
},
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
title: 'Re-login — re-authorize with the provider',
|
|
458
|
-
run: () => {
|
|
459
|
-
void openRelogin(id, label, providerID, apiKey)
|
|
460
|
-
},
|
|
461
|
-
},
|
|
462
|
-
{ title: 'Delete — remove from pool', run: () => openDelete(id, label) },
|
|
463
|
-
]
|
|
464
486
|
dialog().replace(() => {
|
|
465
487
|
const c = color()
|
|
466
488
|
const [hovered, setHovered] = createSignal(-1)
|
|
@@ -474,7 +496,7 @@ function SidebarPanel(props: {
|
|
|
474
496
|
>
|
|
475
497
|
<box flexDirection="row" justifyContent="space-between">
|
|
476
498
|
<text fg={c.text}>
|
|
477
|
-
<b>{
|
|
499
|
+
<b>{title}</b>
|
|
478
500
|
</text>
|
|
479
501
|
<text fg={c.textMuted} onMouseUp={() => dialog().clear()}>
|
|
480
502
|
esc
|
|
@@ -501,68 +523,238 @@ function SidebarPanel(props: {
|
|
|
501
523
|
})
|
|
502
524
|
}
|
|
503
525
|
|
|
526
|
+
// Click an account -> a small menu so Rename, Disable/Enable, Re-login, pairing, and
|
|
527
|
+
// Delete are all reachable. Deliberately NOT api.ui.DialogSelect: that always renders an
|
|
528
|
+
// auto-focused filter <input>, and a focused opentui input swallows the FIRST
|
|
529
|
+
// Esc (to blur itself) — so the menu needed TWO Esc presses to close. A plain
|
|
530
|
+
// clickable list has no input, so the dialog stack's own Esc binding closes it
|
|
531
|
+
// in ONE press. The menu is opened by a mouse click on the row, so mouse-driven
|
|
532
|
+
// options stay consistent (there is no keyboard path that opens it).
|
|
533
|
+
function openMenu(row: Row): void {
|
|
534
|
+
const { id, label, manuallyDisabled } = row
|
|
535
|
+
// A Claude row holds two logins — a setup-token serving inference, an
|
|
536
|
+
// OAuth login measuring usage (and serving inference without a token) —
|
|
537
|
+
// so it gets one item per login instead of an ambiguous "Re-login".
|
|
538
|
+
const hasOAuth = !row.apiKey
|
|
539
|
+
const hasToken = row.login !== ''
|
|
540
|
+
const logins =
|
|
541
|
+
row.providerID === 'anthropic'
|
|
542
|
+
? [
|
|
543
|
+
{
|
|
544
|
+
action:
|
|
545
|
+
hasOAuth || row.oauthLost
|
|
546
|
+
? 'Re-login OAuth'
|
|
547
|
+
: 'Add OAuth login',
|
|
548
|
+
role: hasToken ? 'usage' : 'inference + usage',
|
|
549
|
+
staticLogin: false,
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
action: hasToken
|
|
553
|
+
? 'Replace setup-token'
|
|
554
|
+
: row.tokenLost
|
|
555
|
+
? 'Re-login setup-token'
|
|
556
|
+
: 'Add setup-token',
|
|
557
|
+
role: 'inference',
|
|
558
|
+
staticLogin: true,
|
|
559
|
+
},
|
|
560
|
+
]
|
|
561
|
+
: [
|
|
562
|
+
{
|
|
563
|
+
action: 'Re-login',
|
|
564
|
+
role: 're-authorize with the provider',
|
|
565
|
+
staticLogin: row.apiKey,
|
|
566
|
+
},
|
|
567
|
+
]
|
|
568
|
+
const items: { title: string; run: () => void }[] = [
|
|
569
|
+
{ title: 'Rename', run: () => openRename(id, label) },
|
|
570
|
+
{
|
|
571
|
+
// Reversible (just a scheduler skip), so no confirm step — flip the
|
|
572
|
+
// pool flag and close, unlike Delete which drops the tokens.
|
|
573
|
+
title: manuallyDisabled
|
|
574
|
+
? 'Enable — include in selection'
|
|
575
|
+
: 'Disable — exclude from selection',
|
|
576
|
+
run: () => {
|
|
577
|
+
setDisabledInPool(id, !manuallyDisabled)
|
|
578
|
+
dialog().clear()
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
...logins.map(({ action, role, staticLogin }) => ({
|
|
582
|
+
title: `${action} — ${role}`,
|
|
583
|
+
run: () => {
|
|
584
|
+
void openRelogin(row, staticLogin, action)
|
|
585
|
+
},
|
|
586
|
+
})),
|
|
587
|
+
{ title: 'Delete — remove from pool', run: () => openDelete(id, label) },
|
|
588
|
+
]
|
|
589
|
+
showMenu(label, items)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Ask the server for a sync action and report its outcome. The toast carries
|
|
594
|
+
* the server's own message, which never holds the link; only `showLink`,
|
|
595
|
+
* which the user chose, opens the link in a dialog.
|
|
596
|
+
*/
|
|
597
|
+
async function runSync(
|
|
598
|
+
action: SyncAction,
|
|
599
|
+
link?: string,
|
|
600
|
+
showLink = false,
|
|
601
|
+
): Promise<void> {
|
|
602
|
+
dialog().clear()
|
|
603
|
+
const at = writeSyncIntent(action, link)
|
|
604
|
+
if (action === 'forget') return
|
|
605
|
+
const status = await awaitSyncResult(at)
|
|
606
|
+
if (!status) discardSyncIntent(at)
|
|
607
|
+
props.api.ui.toast({
|
|
608
|
+
variant: status?.ok ? 'success' : 'error',
|
|
609
|
+
message: status?.message ?? 'Sync is taking longer than expected.',
|
|
610
|
+
})
|
|
611
|
+
const shared = status?.ok && showLink ? readShareLink() : undefined
|
|
612
|
+
if (shared) openLink(shared)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function openLink(link: string): void {
|
|
616
|
+
dialog().replace(() =>
|
|
617
|
+
props.api.ui.DialogAlert({
|
|
618
|
+
title: 'Share link',
|
|
619
|
+
message: `${link}\n\nOpen opencode on another machine, choose Sync here, and paste this. Anyone holding it can read these credentials: if it leaks, delete the gist and upload again for a new link.`,
|
|
620
|
+
onConfirm: () => dialog().clear(),
|
|
621
|
+
}),
|
|
622
|
+
)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function openSubscribe(): void {
|
|
626
|
+
dialog().replace(() =>
|
|
627
|
+
props.api.ui.DialogPrompt({
|
|
628
|
+
title: 'Follow a gist link',
|
|
629
|
+
placeholder: 'https://gist.github.com/<user>/<id>#<key>',
|
|
630
|
+
onConfirm: (text: string) => {
|
|
631
|
+
if (isGistLinkShape(text)) void runSync('subscribe', text)
|
|
632
|
+
else {
|
|
633
|
+
dialog().clear()
|
|
634
|
+
props.api.ui.toast({
|
|
635
|
+
variant: 'error',
|
|
636
|
+
message: 'That is not a gist link with a key.',
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
onCancel: () => dialog().clear(),
|
|
641
|
+
}),
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function openForget(): void {
|
|
646
|
+
dialog().replace(() =>
|
|
647
|
+
props.api.ui.DialogConfirm({
|
|
648
|
+
title: 'Stop syncing',
|
|
649
|
+
message:
|
|
650
|
+
'Stop syncing with the gist? Accounts already imported stay in the pool; the gist itself is not deleted.',
|
|
651
|
+
onConfirm: () => void runSync('forget'),
|
|
652
|
+
onCancel: () => dialog().clear(),
|
|
653
|
+
}),
|
|
654
|
+
)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const syncRuns: Record<SyncMenuId, () => void> = {
|
|
658
|
+
'show-link': () => {
|
|
659
|
+
const link = readShareLink()
|
|
660
|
+
if (link) openLink(link)
|
|
661
|
+
},
|
|
662
|
+
upload: () => void runSync('upload'),
|
|
663
|
+
'upload-new': () => void runSync('upload-new', undefined, true),
|
|
664
|
+
sync: () => void runSync('sync'),
|
|
665
|
+
subscribe: openSubscribe,
|
|
666
|
+
forget: openForget,
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function openSyncMenu(): void {
|
|
670
|
+
showMenu(
|
|
671
|
+
'Gist sync',
|
|
672
|
+
syncMenu(sync()).map((item) => ({
|
|
673
|
+
title: item.title,
|
|
674
|
+
run: syncRuns[item.id],
|
|
675
|
+
})),
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
|
|
504
679
|
return (
|
|
505
|
-
<
|
|
506
|
-
<
|
|
680
|
+
<box>
|
|
681
|
+
<Show when={groups().length > 0}>
|
|
682
|
+
<box>
|
|
683
|
+
<text fg={color().text}>
|
|
684
|
+
<b>Auth accounts</b>
|
|
685
|
+
<span style={{ fg: color().textMuted }}>
|
|
686
|
+
{' '}
|
|
687
|
+
(click: rename / disable / re-login / delete)
|
|
688
|
+
</span>
|
|
689
|
+
</text>
|
|
690
|
+
<For each={groups()}>
|
|
691
|
+
{(g) => (
|
|
692
|
+
<box>
|
|
693
|
+
<text fg={color().textMuted}>{g.provider}</text>
|
|
694
|
+
<For each={g.rows}>
|
|
695
|
+
{(r) => (
|
|
696
|
+
<box onMouseUp={() => openMenu(r)}>
|
|
697
|
+
<text
|
|
698
|
+
fg={
|
|
699
|
+
r.manuallyDisabled
|
|
700
|
+
? color().textMuted
|
|
701
|
+
: r.current
|
|
702
|
+
? color().primary
|
|
703
|
+
: color().text
|
|
704
|
+
}
|
|
705
|
+
wrapMode="word"
|
|
706
|
+
>
|
|
707
|
+
{(r.current ? '▶ ' : ' ') +
|
|
708
|
+
(r.rank ? `${r.rank}. ` : ' ') +
|
|
709
|
+
r.label}
|
|
710
|
+
<Show when={r.login}>
|
|
711
|
+
<span style={{ fg: color().textMuted }}>
|
|
712
|
+
{` [${r.login}]`}
|
|
713
|
+
</span>
|
|
714
|
+
</Show>
|
|
715
|
+
<Show when={r.score !== null}>
|
|
716
|
+
<span style={{ fg: color().secondary }}>
|
|
717
|
+
{` ${(r.score ?? 0).toFixed(2)}`}
|
|
718
|
+
</span>
|
|
719
|
+
</Show>
|
|
720
|
+
<Show when={r.state}>
|
|
721
|
+
<span
|
|
722
|
+
style={{ fg: color().warning }}
|
|
723
|
+
>{` (${r.state})`}</span>
|
|
724
|
+
</Show>
|
|
725
|
+
</text>
|
|
726
|
+
<text fg={color().textMuted}>
|
|
727
|
+
{` wk ${r.weeklyPct} (${r.weeklyReset}) · 5h ${r.hourlyPct} (${r.hourlyReset})`}
|
|
728
|
+
</text>
|
|
729
|
+
<For each={r.lostLines}>
|
|
730
|
+
{(line) => (
|
|
731
|
+
<text fg={color().textMuted} wrapMode="word">
|
|
732
|
+
{` ${line}`}
|
|
733
|
+
</text>
|
|
734
|
+
)}
|
|
735
|
+
</For>
|
|
736
|
+
</box>
|
|
737
|
+
)}
|
|
738
|
+
</For>
|
|
739
|
+
</box>
|
|
740
|
+
)}
|
|
741
|
+
</For>
|
|
742
|
+
</box>
|
|
743
|
+
</Show>
|
|
744
|
+
<box onMouseUp={openSyncMenu}>
|
|
507
745
|
<text fg={color().text}>
|
|
508
|
-
<b>
|
|
509
|
-
<span style={{ fg: color().textMuted }}>
|
|
510
|
-
{' '}
|
|
511
|
-
(click: rename / disable / re-login / delete)
|
|
512
|
-
</span>
|
|
746
|
+
<b>Gist sync</b>
|
|
747
|
+
<span style={{ fg: color().textMuted }}> (click)</span>
|
|
513
748
|
</text>
|
|
514
|
-
<For each={
|
|
515
|
-
{(
|
|
516
|
-
<
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
{(r) => (
|
|
520
|
-
<box
|
|
521
|
-
onMouseUp={() =>
|
|
522
|
-
openMenu(
|
|
523
|
-
r.id,
|
|
524
|
-
r.label,
|
|
525
|
-
r.providerID,
|
|
526
|
-
r.manuallyDisabled,
|
|
527
|
-
r.apiKey,
|
|
528
|
-
)
|
|
529
|
-
}
|
|
530
|
-
>
|
|
531
|
-
<text
|
|
532
|
-
fg={
|
|
533
|
-
r.manuallyDisabled
|
|
534
|
-
? color().textMuted
|
|
535
|
-
: r.current
|
|
536
|
-
? color().primary
|
|
537
|
-
: color().text
|
|
538
|
-
}
|
|
539
|
-
wrapMode="word"
|
|
540
|
-
>
|
|
541
|
-
{(r.current ? '▶ ' : ' ') +
|
|
542
|
-
(r.rank ? `${r.rank}. ` : ' ') +
|
|
543
|
-
r.label}
|
|
544
|
-
<Show when={r.score !== null}>
|
|
545
|
-
<span style={{ fg: color().secondary }}>
|
|
546
|
-
{` ${(r.score ?? 0).toFixed(2)}`}
|
|
547
|
-
</span>
|
|
548
|
-
</Show>
|
|
549
|
-
<Show when={r.state}>
|
|
550
|
-
<span
|
|
551
|
-
style={{ fg: color().warning }}
|
|
552
|
-
>{` (${r.state})`}</span>
|
|
553
|
-
</Show>
|
|
554
|
-
</text>
|
|
555
|
-
<text fg={color().textMuted}>
|
|
556
|
-
{` wk ${r.weeklyPct} (${r.weeklyReset}) · 5h ${r.hourlyPct} (${r.hourlyReset})`}
|
|
557
|
-
</text>
|
|
558
|
-
</box>
|
|
559
|
-
)}
|
|
560
|
-
</For>
|
|
561
|
-
</box>
|
|
749
|
+
<For each={syncLines(sync(), sample().now)}>
|
|
750
|
+
{(line) => (
|
|
751
|
+
<text fg={color().textMuted} wrapMode="word">
|
|
752
|
+
{` ${line}`}
|
|
753
|
+
</text>
|
|
562
754
|
)}
|
|
563
755
|
</For>
|
|
564
756
|
</box>
|
|
565
|
-
</
|
|
757
|
+
</box>
|
|
566
758
|
)
|
|
567
759
|
}
|
|
568
760
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Plugin } from '@opencode/plugin/tui'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
id: 'auth-load-balancer-tui',
|
|
5
|
+
async setup(ctx: Plugin.Context) {
|
|
6
|
+
// The host installs its Solid transform before evaluating this view.
|
|
7
|
+
const { registerV2Slots } = await import('./auth-load-balancer-v2.view.tsx')
|
|
8
|
+
return registerV2Slots(ctx)
|
|
9
|
+
},
|
|
10
|
+
}
|