@cortexkit/opencode-anthropic-auth 2.2.0 → 2.2.2

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.
@@ -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
- const width = appearance.barWidth
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
- // Quota window row: muted label left, tone-colored bar + shared figure right,
296
- // with an optional muted reset suffix. When pacing data is present, the bar
297
- // gains a pace segment and an off-pace window adds a muted subline with the
298
- // reserve/deficit delta and the projected runout.
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 reset = () => props.part?.resetIn
311
- const paceLine = () => {
312
- const pacing = props.pacing
313
- if (!pacing || pacing.state === 'on-pace') return null
314
- const pct = Math.round(Math.abs(pacing.deltaPercent))
315
- if (pacing.state === 'reserve') return `reserve ${pct}% \u00b7 lasts`
316
- return pacing.runsOutAt
317
- ? `deficit ${pct}% \u00b7 out in ${formatResetIn(pacing.runsOutAt, props.now)}`
318
- : `deficit ${pct}% \u00b7 lasts`
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
- {/* Left group (label · bar · figure) stays left-aligned in fixed columns so
333
- bars and figures line up across rows; the reset time is pushed to
334
- the right edge so reset times align in their own right column. */}
335
- <box width='100%' flexDirection='row' justifyContent='space-between'>
336
- <box flexDirection='row'>
337
- <text
338
- fg={props.theme.textMuted}
339
- >{`${formatQuotaRowLabel(props.label)} `}</text>
340
- {/* Index: the clock rebuilds the segments, which must not remount. */}
341
- <Index
342
- each={quotaBarSegments(used(), props.appearance, props.pacing)}
343
- >
344
- {(segment) => (
345
- <text fg={toneColor(props.theme, segment().tone)}>
346
- {segment().text}
347
- </text>
348
- )}
349
- </Index>
350
- <text
351
- fg={toneColor(props.theme, usageTone(used(), props.appearance))}
352
- >
353
- {` ${props.part?.figure}${props.binding ? ' •' : ''}`}
354
- </text>
355
- </box>
356
- <Show when={reset()}>
357
- <text fg={props.theme.textMuted}>{reset()}</text>
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
- <box width='100%' flexDirection='row'>
362
- <text fg={props.theme.textMuted}>{formatQuotaRowLabel('', 11)}</text>
363
- <text
364
- fg={toneColor(
365
- props.theme,
366
- props.pacing?.state === 'deficit' ? 'warn' : 'muted',
367
- )}
368
- >
369
- {paceLine()}
370
- </text>
371
- </box>
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 width='100%' flexDirection='column' marginTop={props.marginTop ?? 0}>
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={partFor(window.title)?.name ?? window.title}
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
- // The line box's laid-out width, read when it mounts and on each
496
- // resize. It is undefined (amounts keep their cents) until layout.
497
- const [width, setWidth] = createSignal<number>()
498
- let line: BoxRenderable | undefined
499
- return (
500
- <box
501
- width='100%'
502
- ref={(element: BoxRenderable) => {
503
- line = element
504
- setWidth(element.width || undefined)
505
- }}
506
- onSizeChange={() => setWidth(line?.width || undefined)}
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
- <text
509
- fg={toneColor(
510
- props.theme,
511
- props.quota?.extraUsage?.severity === 'critical'
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
+ quotaWindowParts,
135
136
  } from '@cortexkit/anthropic-auth-core'
136
137
 
137
138
  const STATE_FILE_ENV = 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE'
@@ -951,6 +952,122 @@ 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
+ * each window part's `short`, the same label common-auth's narrow one-line
958
+ * form uses (`Fa` for Fable; models whose short names would collide are
959
+ * extended until they differ, as `Fabl` and `Fabr`), so the expanded rows and
960
+ * the collapsed line agree. Short names are worked out over every model window
961
+ * in the reading, so all scoped windows are projected together. A part is
962
+ * matched to its window by the window's title, which is the name a
963
+ * model-scoped part carries; a window with no part keeps its title.
964
+ */
965
+ export function scopedQuotaShortNames(
966
+ quota: Pick<AccountQuota, 'scoped'> | null | undefined,
967
+ now = Date.now(),
968
+ ): string[] {
969
+ const scoped = quota?.scoped ?? []
970
+ if (!scoped.length) return []
971
+ const { windows } = quotaWindowParts(
972
+ quotaDisplayProjection({ scoped }, now),
973
+ { now },
974
+ )
975
+ return scoped.map(
976
+ (window) =>
977
+ windows.find(
978
+ (part) => part.kind === 'window' && part.name === window.title,
979
+ )?.short || window.title,
980
+ )
981
+ }
982
+
983
+ /**
984
+ * The width of the quota rows' label column: the longest label any of these
985
+ * accounts shows. `5h` and `7d` always show, so it is at least 2.
986
+ */
987
+ export function quotaLabelColumnWidth(
988
+ quotas: readonly (AccountQuota | null | undefined)[],
989
+ now = Date.now(),
990
+ ): number {
991
+ return Math.max(
992
+ 2,
993
+ ...quotas.flatMap((quota) =>
994
+ scopedQuotaShortNames(quota, now).map((name) => Bun.stringWidth(name)),
995
+ ),
996
+ )
997
+ }
998
+
999
+ /** The narrowest a quota row's bar shrinks to. */
1000
+ export const QUOTA_BAR_MIN_CELLS = 3
1001
+
1002
+ export interface QuotaRowFit {
1003
+ /** Bar cells to draw. */
1004
+ barCells: number
1005
+ /** The reset text to draw at the right edge, or undefined for none. */
1006
+ reset?: string
1007
+ }
1008
+
1009
+ /**
1010
+ * Fits one quota row into `width` cells. The row reads `label bar figure` on
1011
+ * the left and the reset at the right edge, with a one-cell gap before the
1012
+ * reset that the caller always draws. When the full row is too wide, the
1013
+ * reset is dropped first, so the figure (the percent left) is never cut to
1014
+ * keep it; then the bar shrinks, down to `QUOTA_BAR_MIN_CELLS`. A row still
1015
+ * too wide at that point keeps the minimum bar and is cut at the right edge
1016
+ * by the caller's no-wrap text, which cuts the figure's end. Without a
1017
+ * measured width the full row is used.
1018
+ */
1019
+ export function fitQuotaRow(input: {
1020
+ width: number | undefined
1021
+ labelWidth: number
1022
+ barWidth: number
1023
+ /** The figure with any binding marker, such as `97% left •`. */
1024
+ figure: string
1025
+ reset?: string
1026
+ }): QuotaRowFit {
1027
+ const { width, labelWidth, barWidth, figure, reset } = input
1028
+ const full: QuotaRowFit = { barCells: barWidth, reset: reset || undefined }
1029
+ if (width === undefined) return full
1030
+ const cells = (fit: QuotaRowFit) =>
1031
+ labelWidth +
1032
+ 1 +
1033
+ (fit.barCells > 0 ? fit.barCells + 1 : 0) +
1034
+ Bun.stringWidth(figure) +
1035
+ (fit.reset ? 1 + Bun.stringWidth(fit.reset) : 0)
1036
+ const minimum = Math.min(QUOTA_BAR_MIN_CELLS, barWidth)
1037
+ const candidates: QuotaRowFit[] = [full]
1038
+ for (let barCells = barWidth; barCells >= minimum; barCells--) {
1039
+ candidates.push({ barCells })
1040
+ }
1041
+ return candidates.find((fit) => cells(fit) <= width) ?? { barCells: minimum }
1042
+ }
1043
+
1044
+ /**
1045
+ * The pacing subline for a window that is off pace: `deficit 12% · out in
1046
+ * 1d23h` for a deficit that runs out before the reset, otherwise the delta
1047
+ * and `lasts to reset` (`reserve 4% · lasts to reset`). When it is wider than
1048
+ * `width` cells it is cut to fit and ends in `…` rather than wrapping. Null
1049
+ * when there is no pacing or the window is on pace.
1050
+ */
1051
+ export function fitPaceLine(
1052
+ pacing: QuotaPacing | null,
1053
+ width: number | undefined,
1054
+ runsOutIn: string | undefined,
1055
+ ): string | null {
1056
+ if (!pacing || pacing.state === 'on-pace') return null
1057
+ const head = `${pacing.state} ${Math.round(Math.abs(pacing.deltaPercent))}%`
1058
+ const line = runsOutIn
1059
+ ? `${head} \u00b7 out in ${runsOutIn}`
1060
+ : `${head} \u00b7 lasts to reset`
1061
+ if (width === undefined || Bun.stringWidth(line) <= width) return line
1062
+ if (width <= 0) return ''
1063
+ let cut = ''
1064
+ for (const character of line) {
1065
+ if (Bun.stringWidth(cut + character) > width - 1) break
1066
+ cut += character
1067
+ }
1068
+ return `${cut.trimEnd()}\u2026`
1069
+ }
1070
+
954
1071
  export function getCollapsedQuotaSummary(
955
1072
  quota: AccountQuota | null,
956
1073
  now = Date.now(),