@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.
@@ -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
+ 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(),