@cortexkit/opencode-anthropic-auth 2.2.0 → 2.2.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/cli.js +1 -1
- package/dist/{index-f2ne65w4.js → index-62x1p34p.js} +1 -1
- package/dist/{index-pden7a7y.js → index-88zg9ztr.js} +1 -1
- package/dist/{index-ccrq4mra.js → index-8ysjtqek.js} +1 -1
- package/dist/{index-ah5s0bew.js → index-ppdrzs41.js} +1 -1
- package/dist/index-xbwa4x53.js +2 -0
- package/dist/index.js +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.js +1 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state.d.ts +53 -0
- package/dist/sidebar-state.js +1 -1
- package/dist/tui/claude-sidebar.d.ts +5 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/v2/server.js +1 -1
- package/dist/v2/tui.js +1 -1
- package/package.json +2 -2
- package/src/sidebar-state.ts +125 -0
- package/src/tui/claude-sidebar.tsx +164 -93
- package/src/tui-compiled/sidebar-state.ts +125 -0
- package/src/tui-compiled/tui/claude-sidebar.tsx +288 -231
- package/dist/index-czyrqpxc.js +0 -2
|
@@ -29,15 +29,19 @@ import {
|
|
|
29
29
|
computeQuotaPacing,
|
|
30
30
|
DEFAULT_SIDEBAR_STATE,
|
|
31
31
|
FIVE_HOUR_MS,
|
|
32
|
+
fitPaceLine,
|
|
33
|
+
fitQuotaRow,
|
|
32
34
|
formatPrimeAccountValue,
|
|
33
35
|
formatQuotaRowLabel,
|
|
34
36
|
getCollapsedAccounts,
|
|
35
37
|
getCollapsedQuotaSummary,
|
|
36
38
|
getFableRecoverySummary,
|
|
37
39
|
type QuotaPacing,
|
|
40
|
+
quotaLabelColumnWidth,
|
|
38
41
|
resolveActiveAccount,
|
|
39
42
|
SEVEN_DAY_MS,
|
|
40
43
|
type SidebarState,
|
|
44
|
+
scopedQuotaShortNames,
|
|
41
45
|
} from '../sidebar-state.js'
|
|
42
46
|
import {
|
|
43
47
|
type AnthropicAuthTuiPrefs,
|
|
@@ -131,8 +135,9 @@ function quotaBarSegments(
|
|
|
131
135
|
usedPct: number,
|
|
132
136
|
appearance: AppearancePrefs,
|
|
133
137
|
pacing: QuotaPacing | null,
|
|
138
|
+
width: number,
|
|
134
139
|
): BarSegment[] {
|
|
135
|
-
|
|
140
|
+
if (width <= 0) return []
|
|
136
141
|
const cells = (pct: number) =>
|
|
137
142
|
Math.max(0, Math.min(Math.round((pct / 100) * width), width))
|
|
138
143
|
const usedCells = cells(usedPct)
|
|
@@ -292,14 +297,35 @@ function CollapsedAccountRow(props: {
|
|
|
292
297
|
)
|
|
293
298
|
}
|
|
294
299
|
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
300
|
+
// One cell run of a single-line row: drawn at its own width, never shrunk and
|
|
301
|
+
// never wrapped, so the row's layout is exactly what `fitQuotaRow` measured.
|
|
302
|
+
function Cell(props: { fg: SidebarColor; text: string }) {
|
|
303
|
+
return (
|
|
304
|
+
<text
|
|
305
|
+
width={Bun.stringWidth(props.text)}
|
|
306
|
+
flexShrink={0}
|
|
307
|
+
wrapMode='none'
|
|
308
|
+
fg={props.fg}
|
|
309
|
+
>
|
|
310
|
+
{props.text}
|
|
311
|
+
</text>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Quota window row: muted label left in a column `labelWidth` wide, then the
|
|
316
|
+
// tone-colored bar and the shared figure, and the muted reset at the right
|
|
317
|
+
// edge. The row is one line: `fitQuotaRow` drops detail to fit `width`
|
|
318
|
+
// instead of letting it wrap. When pacing data is present, the bar gains a
|
|
319
|
+
// pace segment and an off-pace window adds a one-line subline under the bar
|
|
320
|
+
// with the reserve/deficit delta and either the projected runout or
|
|
321
|
+
// `lasts to reset`.
|
|
299
322
|
function QuotaRow(props: {
|
|
300
323
|
theme: SidebarTheme
|
|
301
324
|
appearance: AppearancePrefs
|
|
302
325
|
label: string
|
|
326
|
+
labelWidth: number
|
|
327
|
+
/** The row's laid-out width, undefined until the first layout. */
|
|
328
|
+
width: number | undefined
|
|
303
329
|
part: QuotaWindowPart | undefined
|
|
304
330
|
usedPercent: number | undefined
|
|
305
331
|
pacing: QuotaPacing | null
|
|
@@ -307,68 +333,86 @@ function QuotaRow(props: {
|
|
|
307
333
|
now: number
|
|
308
334
|
}) {
|
|
309
335
|
const used = () => props.usedPercent ?? 0
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
:
|
|
319
|
-
|
|
336
|
+
const labelCell = () =>
|
|
337
|
+
`${props.label}${' '.repeat(Math.max(0, props.labelWidth - Bun.stringWidth(props.label)))} `
|
|
338
|
+
const figure = () =>
|
|
339
|
+
`${props.part?.figure ?? ''}${props.binding ? ' \u2022' : ''}`
|
|
340
|
+
const fit = () =>
|
|
341
|
+
fitQuotaRow({
|
|
342
|
+
width: props.width,
|
|
343
|
+
labelWidth: props.labelWidth,
|
|
344
|
+
barWidth: props.appearance.barWidth,
|
|
345
|
+
figure: figure(),
|
|
346
|
+
reset: props.part?.resetIn,
|
|
347
|
+
})
|
|
348
|
+
const paceLine = () =>
|
|
349
|
+
fitPaceLine(
|
|
350
|
+
props.pacing,
|
|
351
|
+
props.width === undefined
|
|
352
|
+
? undefined
|
|
353
|
+
: props.width - props.labelWidth - 1,
|
|
354
|
+
props.pacing?.runsOutAt
|
|
355
|
+
? formatResetIn(props.pacing.runsOutAt, props.now)
|
|
356
|
+
: undefined,
|
|
357
|
+
)
|
|
320
358
|
return (
|
|
321
359
|
<Show
|
|
322
360
|
when={props.part}
|
|
323
361
|
fallback={
|
|
324
|
-
<box width='100%' flexDirection='row'>
|
|
325
|
-
<text
|
|
326
|
-
fg={props.theme.textMuted}
|
|
327
|
-
>{`${formatQuotaRowLabel(props.label)} `}</text>
|
|
328
|
-
<text fg={props.theme.textMuted}>{'\u2014'}</text>
|
|
362
|
+
<box width='100%' height={1} flexDirection='row'>
|
|
363
|
+
<Cell fg={props.theme.textMuted} text={`${labelCell()}\u2014`} />
|
|
329
364
|
</box>
|
|
330
365
|
}
|
|
331
366
|
>
|
|
332
|
-
{/*
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
{
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
367
|
+
{/* The label column is the same width in every row, so bars start in
|
|
368
|
+
one column; a bar shrinks only when its row needs the room. The
|
|
369
|
+
reset is pushed to the right edge, always at least one space after
|
|
370
|
+
the figure. */}
|
|
371
|
+
<box width='100%' height={1} flexDirection='row'>
|
|
372
|
+
<Cell fg={props.theme.textMuted} text={labelCell()} />
|
|
373
|
+
{/* Index: the clock rebuilds the segments, which must not remount. */}
|
|
374
|
+
<Index
|
|
375
|
+
each={quotaBarSegments(
|
|
376
|
+
used(),
|
|
377
|
+
props.appearance,
|
|
378
|
+
props.pacing,
|
|
379
|
+
fit().barCells,
|
|
380
|
+
)}
|
|
381
|
+
>
|
|
382
|
+
{(segment) => (
|
|
383
|
+
<Cell
|
|
384
|
+
fg={toneColor(props.theme, segment().tone)}
|
|
385
|
+
text={segment().text}
|
|
386
|
+
/>
|
|
387
|
+
)}
|
|
388
|
+
</Index>
|
|
389
|
+
<Cell
|
|
390
|
+
fg={toneColor(props.theme, usageTone(used(), props.appearance))}
|
|
391
|
+
text={`${fit().barCells > 0 ? ' ' : ''}${figure()}`}
|
|
392
|
+
/>
|
|
393
|
+
<box flexGrow={1} />
|
|
394
|
+
<Show when={fit().reset}>
|
|
395
|
+
{(reset: () => string) => (
|
|
396
|
+
<Cell fg={props.theme.textMuted} text={` ${reset()}`} />
|
|
397
|
+
)}
|
|
358
398
|
</Show>
|
|
359
399
|
</box>
|
|
360
400
|
<Show when={paceLine()}>
|
|
361
|
-
|
|
362
|
-
<
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
props.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
401
|
+
{(line: () => string) => (
|
|
402
|
+
<box width='100%' height={1} flexDirection='row'>
|
|
403
|
+
<Cell
|
|
404
|
+
fg={props.theme.textMuted}
|
|
405
|
+
text={' '.repeat(props.labelWidth + 1)}
|
|
406
|
+
/>
|
|
407
|
+
<Cell
|
|
408
|
+
fg={toneColor(
|
|
409
|
+
props.theme,
|
|
410
|
+
props.pacing?.state === 'deficit' ? 'warn' : 'muted',
|
|
411
|
+
)}
|
|
412
|
+
text={line()}
|
|
413
|
+
/>
|
|
414
|
+
</box>
|
|
415
|
+
)}
|
|
372
416
|
</Show>
|
|
373
417
|
</Show>
|
|
374
418
|
)
|
|
@@ -405,6 +449,11 @@ export function AccountBlock(props: {
|
|
|
405
449
|
stateLine?: string
|
|
406
450
|
quotaDated?: boolean
|
|
407
451
|
tierLabel?: string
|
|
452
|
+
/**
|
|
453
|
+
* The quota rows' label column width, shared across the sidebar's accounts
|
|
454
|
+
* so their bars line up; defaults to this account's own longest label.
|
|
455
|
+
*/
|
|
456
|
+
labelWidth?: number
|
|
408
457
|
marginTop?: number
|
|
409
458
|
now: number
|
|
410
459
|
}) {
|
|
@@ -439,8 +488,25 @@ export function AccountBlock(props: {
|
|
|
439
488
|
})
|
|
440
489
|
const partFor = (name: string) =>
|
|
441
490
|
parts().windows.find((part) => part.name === name)
|
|
491
|
+
const scopedLabels = () => scopedQuotaShortNames(props.quota, props.now)
|
|
492
|
+
const labelWidth = () =>
|
|
493
|
+
props.labelWidth ?? quotaLabelColumnWidth([props.quota], props.now)
|
|
494
|
+
// The block's laid-out width, read when it mounts and on each resize. Quota
|
|
495
|
+
// rows drop or shrink detail to fit it (and are cut at the edge when even
|
|
496
|
+
// that is not enough); pacing sublines are cut to fit. Undefined until layout.
|
|
497
|
+
const [width, setWidth] = createSignal<number>()
|
|
498
|
+
let block: BoxRenderable | undefined
|
|
442
499
|
return (
|
|
443
|
-
<box
|
|
500
|
+
<box
|
|
501
|
+
width='100%'
|
|
502
|
+
flexDirection='column'
|
|
503
|
+
marginTop={props.marginTop ?? 0}
|
|
504
|
+
ref={(element: BoxRenderable) => {
|
|
505
|
+
block = element
|
|
506
|
+
setWidth(element.width || undefined)
|
|
507
|
+
}}
|
|
508
|
+
onSizeChange={() => setWidth(block?.width || undefined)}
|
|
509
|
+
>
|
|
444
510
|
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
445
511
|
<text fg={props.theme.text}>
|
|
446
512
|
<b>{`${props.name}${props.tierLabel ? ` · ${props.tierLabel}` : ''}`}</b>
|
|
@@ -460,6 +526,8 @@ export function AccountBlock(props: {
|
|
|
460
526
|
theme={props.theme}
|
|
461
527
|
appearance={props.appearance}
|
|
462
528
|
label='5h'
|
|
529
|
+
labelWidth={labelWidth()}
|
|
530
|
+
width={width()}
|
|
463
531
|
part={partFor('5h')}
|
|
464
532
|
usedPercent={props.quota?.five_hour?.usedPercent}
|
|
465
533
|
pacing={pacingFor(props.quota?.five_hour, FIVE_HOUR_MS)}
|
|
@@ -470,6 +538,8 @@ export function AccountBlock(props: {
|
|
|
470
538
|
theme={props.theme}
|
|
471
539
|
appearance={props.appearance}
|
|
472
540
|
label='7d'
|
|
541
|
+
labelWidth={labelWidth()}
|
|
542
|
+
width={width()}
|
|
473
543
|
part={partFor('7d')}
|
|
474
544
|
usedPercent={props.quota?.seven_day?.usedPercent}
|
|
475
545
|
pacing={pacingFor(props.quota?.seven_day, SEVEN_DAY_MS)}
|
|
@@ -477,11 +547,13 @@ export function AccountBlock(props: {
|
|
|
477
547
|
now={props.now}
|
|
478
548
|
/>
|
|
479
549
|
<For each={props.quota?.scoped ?? []}>
|
|
480
|
-
{(window) => (
|
|
550
|
+
{(window, index) => (
|
|
481
551
|
<QuotaRow
|
|
482
552
|
theme={props.theme}
|
|
483
553
|
appearance={props.appearance}
|
|
484
|
-
label={
|
|
554
|
+
label={scopedLabels()[index()] ?? window.title}
|
|
555
|
+
labelWidth={labelWidth()}
|
|
556
|
+
width={width()}
|
|
485
557
|
part={partFor(window.title)}
|
|
486
558
|
usedPercent={window.usedPercent}
|
|
487
559
|
pacing={pacingFor(window, SEVEN_DAY_MS)}
|
|
@@ -491,40 +563,26 @@ export function AccountBlock(props: {
|
|
|
491
563
|
)}
|
|
492
564
|
</For>
|
|
493
565
|
<Show when={partFor('credits')}>
|
|
494
|
-
{(budget: () => QuotaWindowPart) =>
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
566
|
+
{(budget: () => QuotaWindowPart) => (
|
|
567
|
+
// One line in the block's width: amounts drop their cents rather
|
|
568
|
+
// than wrap, and keep them until the first layout.
|
|
569
|
+
<box width='100%' height={1} flexDirection='row'>
|
|
570
|
+
<text
|
|
571
|
+
wrapMode='none'
|
|
572
|
+
fg={toneColor(
|
|
573
|
+
props.theme,
|
|
574
|
+
props.quota?.extraUsage?.severity === 'critical'
|
|
575
|
+
? 'err'
|
|
576
|
+
: props.quota?.extraUsage?.severity === 'warning' ||
|
|
577
|
+
props.quota?.extraUsage?.exhausted
|
|
578
|
+
? 'warn'
|
|
579
|
+
: 'muted',
|
|
580
|
+
)}
|
|
507
581
|
>
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
? 'err'
|
|
513
|
-
: props.quota?.extraUsage?.severity === 'warning' ||
|
|
514
|
-
props.quota?.extraUsage?.exhausted
|
|
515
|
-
? 'warn'
|
|
516
|
-
: 'muted',
|
|
517
|
-
)}
|
|
518
|
-
>
|
|
519
|
-
{creditLine(
|
|
520
|
-
budget().figure,
|
|
521
|
-
props.quota?.extraUsage,
|
|
522
|
-
width(),
|
|
523
|
-
)}
|
|
524
|
-
</text>
|
|
525
|
-
</box>
|
|
526
|
-
)
|
|
527
|
-
}}
|
|
582
|
+
{creditLine(budget().figure, props.quota?.extraUsage, width())}
|
|
583
|
+
</text>
|
|
584
|
+
</box>
|
|
585
|
+
)}
|
|
528
586
|
</Show>
|
|
529
587
|
<Show when={parts().age}>
|
|
530
588
|
<text fg={props.theme.textMuted}>{parts().age}</text>
|
|
@@ -634,6 +692,17 @@ export function ClaudeSidebar(props: {
|
|
|
634
692
|
state().main?.quota != null ||
|
|
635
693
|
state().main?.stateLine != null ||
|
|
636
694
|
enabledFallbacks().length > 0
|
|
695
|
+
// One label column for every expanded account, so all their bars line up.
|
|
696
|
+
const quotaLabelWidth = () =>
|
|
697
|
+
quotaLabelColumnWidth(
|
|
698
|
+
[
|
|
699
|
+
state().main?.quota,
|
|
700
|
+
...(prefs().sections.fallbackAccounts
|
|
701
|
+
? enabledFallbacks().map((fallback) => fallback.quota)
|
|
702
|
+
: []),
|
|
703
|
+
],
|
|
704
|
+
now(),
|
|
705
|
+
)
|
|
637
706
|
|
|
638
707
|
const headerLabel = () => {
|
|
639
708
|
const name = prefs().header.label
|
|
@@ -821,6 +890,7 @@ export function ClaudeSidebar(props: {
|
|
|
821
890
|
active={state().activeId === 'main'}
|
|
822
891
|
pacingEnabled={prefs().sections.pacing}
|
|
823
892
|
tierLabel={state().main?.tierLabel}
|
|
893
|
+
labelWidth={quotaLabelWidth()}
|
|
824
894
|
stateLine={state().main?.stateLine}
|
|
825
895
|
quotaDated={state().main?.quotaDated}
|
|
826
896
|
now={now()}
|
|
@@ -836,6 +906,7 @@ export function ClaudeSidebar(props: {
|
|
|
836
906
|
active={state().activeId === fb.id}
|
|
837
907
|
pacingEnabled={prefs().sections.pacing}
|
|
838
908
|
tierLabel={fb.tierLabel}
|
|
909
|
+
labelWidth={quotaLabelWidth()}
|
|
839
910
|
needsReauth={fb.needsReauth}
|
|
840
911
|
vaultReauth={fb.vaultReauth}
|
|
841
912
|
stateLine={fb.stateLine}
|
|
@@ -132,6 +132,7 @@ import {
|
|
|
132
132
|
isQuotaStale,
|
|
133
133
|
logger,
|
|
134
134
|
quotaDisplayProjection,
|
|
135
|
+
quotaTextParts,
|
|
135
136
|
} from '@cortexkit/anthropic-auth-core'
|
|
136
137
|
|
|
137
138
|
const STATE_FILE_ENV = 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE'
|
|
@@ -951,6 +952,130 @@ export function formatQuotaRowLabel(name: string, width = 10): string {
|
|
|
951
952
|
return label + ' '.repeat(Math.max(0, width - Bun.stringWidth(label)))
|
|
952
953
|
}
|
|
953
954
|
|
|
955
|
+
/**
|
|
956
|
+
* The short names model-scoped rows read under, in the order of `quota.scoped`:
|
|
957
|
+
* the names common-auth's narrow one-line form gives them (`Fa` for Fable;
|
|
958
|
+
* models whose short names would collide are extended until they differ, as
|
|
959
|
+
* `Fabl` and `Fabr`), so the expanded rows and the collapsed line agree.
|
|
960
|
+
*
|
|
961
|
+
* common-auth exposes that short name only inside the narrow text, and the
|
|
962
|
+
* narrow line shows just one model window (the tightest). So each window's
|
|
963
|
+
* name is read from a narrow line of a copy of the reading in which that
|
|
964
|
+
* window alone is spent: every model window stays in, so collisions resolve
|
|
965
|
+
* exactly as they do in the real line, and the one piece the line shows is
|
|
966
|
+
* `<short name> 0% left`. The figures in the copy are never displayed.
|
|
967
|
+
*/
|
|
968
|
+
export function scopedQuotaShortNames(
|
|
969
|
+
quota: Pick<AccountQuota, 'scoped'> | null | undefined,
|
|
970
|
+
now = Date.now(),
|
|
971
|
+
): string[] {
|
|
972
|
+
const scoped = quota?.scoped ?? []
|
|
973
|
+
return scoped.map((window, index) => {
|
|
974
|
+
const probe = quotaDisplayProjection(
|
|
975
|
+
{
|
|
976
|
+
scoped: scoped.map((other, at) => ({
|
|
977
|
+
...other,
|
|
978
|
+
usedPercent: at === index ? 100 : 0,
|
|
979
|
+
remainingPercent: at === index ? 0 : 100,
|
|
980
|
+
checkedAt: now,
|
|
981
|
+
})),
|
|
982
|
+
},
|
|
983
|
+
now,
|
|
984
|
+
)
|
|
985
|
+
const piece = quotaTextParts(probe, { now, form: 'narrow' })[0]
|
|
986
|
+
const name = piece?.replace(/ 0% left$/, '')
|
|
987
|
+
return name && name !== piece ? name : window.title
|
|
988
|
+
})
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* The width of the quota rows' label column: the longest label any of these
|
|
993
|
+
* accounts shows. `5h` and `7d` always show, so it is at least 2.
|
|
994
|
+
*/
|
|
995
|
+
export function quotaLabelColumnWidth(
|
|
996
|
+
quotas: readonly (AccountQuota | null | undefined)[],
|
|
997
|
+
now = Date.now(),
|
|
998
|
+
): number {
|
|
999
|
+
return Math.max(
|
|
1000
|
+
2,
|
|
1001
|
+
...quotas.flatMap((quota) =>
|
|
1002
|
+
scopedQuotaShortNames(quota, now).map((name) => Bun.stringWidth(name)),
|
|
1003
|
+
),
|
|
1004
|
+
)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** The narrowest a quota row's bar shrinks to. */
|
|
1008
|
+
export const QUOTA_BAR_MIN_CELLS = 3
|
|
1009
|
+
|
|
1010
|
+
export interface QuotaRowFit {
|
|
1011
|
+
/** Bar cells to draw. */
|
|
1012
|
+
barCells: number
|
|
1013
|
+
/** The reset text to draw at the right edge, or undefined for none. */
|
|
1014
|
+
reset?: string
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Fits one quota row into `width` cells. The row reads `label bar figure` on
|
|
1019
|
+
* the left and the reset at the right edge, with a one-cell gap before the
|
|
1020
|
+
* reset that the caller always draws. When the full row is too wide, the
|
|
1021
|
+
* reset is dropped first, so the figure (the percent left) is never cut to
|
|
1022
|
+
* keep it; then the bar shrinks, down to `QUOTA_BAR_MIN_CELLS`. A row still
|
|
1023
|
+
* too wide at that point keeps the minimum bar and is cut at the right edge
|
|
1024
|
+
* by the caller's no-wrap text, which cuts the figure's end. Without a
|
|
1025
|
+
* measured width the full row is used.
|
|
1026
|
+
*/
|
|
1027
|
+
export function fitQuotaRow(input: {
|
|
1028
|
+
width: number | undefined
|
|
1029
|
+
labelWidth: number
|
|
1030
|
+
barWidth: number
|
|
1031
|
+
/** The figure with any binding marker, such as `97% left •`. */
|
|
1032
|
+
figure: string
|
|
1033
|
+
reset?: string
|
|
1034
|
+
}): QuotaRowFit {
|
|
1035
|
+
const { width, labelWidth, barWidth, figure, reset } = input
|
|
1036
|
+
const full: QuotaRowFit = { barCells: barWidth, reset: reset || undefined }
|
|
1037
|
+
if (width === undefined) return full
|
|
1038
|
+
const cells = (fit: QuotaRowFit) =>
|
|
1039
|
+
labelWidth +
|
|
1040
|
+
1 +
|
|
1041
|
+
(fit.barCells > 0 ? fit.barCells + 1 : 0) +
|
|
1042
|
+
Bun.stringWidth(figure) +
|
|
1043
|
+
(fit.reset ? 1 + Bun.stringWidth(fit.reset) : 0)
|
|
1044
|
+
const minimum = Math.min(QUOTA_BAR_MIN_CELLS, barWidth)
|
|
1045
|
+
const candidates: QuotaRowFit[] = [full]
|
|
1046
|
+
for (let barCells = barWidth; barCells >= minimum; barCells--) {
|
|
1047
|
+
candidates.push({ barCells })
|
|
1048
|
+
}
|
|
1049
|
+
return candidates.find((fit) => cells(fit) <= width) ?? { barCells: minimum }
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* The pacing subline for a window that is off pace: `deficit 12% · out in
|
|
1054
|
+
* 1d23h` for a deficit that runs out before the reset, otherwise the delta
|
|
1055
|
+
* and `lasts to reset` (`reserve 4% · lasts to reset`). When it is wider than
|
|
1056
|
+
* `width` cells it is cut to fit and ends in `…` rather than wrapping. Null
|
|
1057
|
+
* when there is no pacing or the window is on pace.
|
|
1058
|
+
*/
|
|
1059
|
+
export function fitPaceLine(
|
|
1060
|
+
pacing: QuotaPacing | null,
|
|
1061
|
+
width: number | undefined,
|
|
1062
|
+
runsOutIn: string | undefined,
|
|
1063
|
+
): string | null {
|
|
1064
|
+
if (!pacing || pacing.state === 'on-pace') return null
|
|
1065
|
+
const head = `${pacing.state} ${Math.round(Math.abs(pacing.deltaPercent))}%`
|
|
1066
|
+
const line = runsOutIn
|
|
1067
|
+
? `${head} \u00b7 out in ${runsOutIn}`
|
|
1068
|
+
: `${head} \u00b7 lasts to reset`
|
|
1069
|
+
if (width === undefined || Bun.stringWidth(line) <= width) return line
|
|
1070
|
+
if (width <= 0) return ''
|
|
1071
|
+
let cut = ''
|
|
1072
|
+
for (const character of line) {
|
|
1073
|
+
if (Bun.stringWidth(cut + character) > width - 1) break
|
|
1074
|
+
cut += character
|
|
1075
|
+
}
|
|
1076
|
+
return `${cut.trimEnd()}\u2026`
|
|
1077
|
+
}
|
|
1078
|
+
|
|
954
1079
|
export function getCollapsedQuotaSummary(
|
|
955
1080
|
quota: AccountQuota | null,
|
|
956
1081
|
now = Date.now(),
|