@cortexkit/opencode-anthropic-auth 2.1.2 → 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/src/tui/entry.mjs CHANGED
@@ -37,6 +37,17 @@ async function loadLegacyTui() {
37
37
  return mod.default
38
38
  }
39
39
 
40
+ // The quota sidebar renderer shared with OpenCode 1, loaded the same way.
41
+ async function loadSidebar() {
42
+ try {
43
+ await import(runtimeProbe)
44
+ } catch (error) {
45
+ if (!isMissingRuntimeRegistry(error)) throw error
46
+ return import('./claude-sidebar.tsx')
47
+ }
48
+ return import('../tui-compiled/tui/claude-sidebar.tsx')
49
+ }
50
+
40
51
  let legacyModule
41
52
 
42
53
  // OpenCode 1 reads id and tui from the default definition; OpenCode 2 reads
@@ -57,7 +68,10 @@ export const setup = async (context) => {
57
68
  // TUI must not load the OpenCode 1 renderer or start that renderer's separate
58
69
  // loopback notification poller.
59
70
  const { setupAnthropicTui } = await import('../../dist/v2/tui.js')
60
- return setupAnthropicTui(context)
71
+ return setupAnthropicTui(context, {
72
+ mountSidebar: async (ctx, read) =>
73
+ (await loadSidebar()).mountClaudeSidebar(ctx, read),
74
+ })
61
75
  }
62
76
 
63
77
  export default { id, tui, setup }
@@ -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'
@@ -888,7 +889,10 @@ export function formatPrimeCost(value: number): string {
888
889
  return value.toFixed(Math.min(6, Math.max(0, 4)))
889
890
  }
890
891
 
891
- export function formatPrimeAccountValue(account: PrimeSidebarAccountState): {
892
+ export function formatPrimeAccountValue(
893
+ account: PrimeSidebarAccountState,
894
+ now = Date.now(),
895
+ ): {
892
896
  text: string
893
897
  hasError: boolean
894
898
  } {
@@ -898,7 +902,7 @@ export function formatPrimeAccountValue(account: PrimeSidebarAccountState): {
898
902
  if (account.lastResult === 'skipped') {
899
903
  return { text: 'skip', hasError: false }
900
904
  }
901
- if (account.nextDueAt && account.nextDueAt > Date.now()) {
905
+ if (account.nextDueAt && account.nextDueAt > now) {
902
906
  return { text: formatPrimeTime(account.nextDueAt), hasError: false }
903
907
  }
904
908
  if (account.lastPrimedAt) {
@@ -948,6 +952,130 @@ export function formatQuotaRowLabel(name: string, width = 10): string {
948
952
  return label + ' '.repeat(Math.max(0, width - Bun.stringWidth(label)))
949
953
  }
950
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
+
951
1079
  export function getCollapsedQuotaSummary(
952
1080
  quota: AccountQuota | null,
953
1081
  now = Date.now(),
@@ -994,7 +1122,9 @@ export function getCollapsedAccounts(
994
1122
  state: SidebarState,
995
1123
  showFallbacks = true,
996
1124
  ) {
997
- const activeId = resolveActiveAccount(state).id
1125
+ // Before any request no account is active; do not mark main as serving.
1126
+ const activeId =
1127
+ state.activeId === undefined ? undefined : resolveActiveAccount(state).id
998
1128
  return [
999
1129
  {
1000
1130
  id: 'main',