@cortexkit/opencode-anthropic-auth 2.2.7 → 2.3.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.
Files changed (39) hide show
  1. package/dist/cli.js +43 -42
  2. package/dist/index-2jsr8wvj.js +43 -0
  3. package/dist/index-46e2gk3c.js +2 -0
  4. package/dist/{index-62x1p34p.js → index-71temqvq.js} +1 -1
  5. package/dist/{index-ppdrzs41.js → index-bcp23wwb.js} +1 -1
  6. package/dist/{index-hazrk5c3.js → index-c8ehrd0p.js} +1 -1
  7. package/dist/index-dpnhamzw.js +2 -0
  8. package/dist/{index-0kxy8k2c.js → index-m4dazzyf.js} +1 -1
  9. package/dist/{index-b1dvkp15.js → index-naff9h33.js} +1 -1
  10. package/dist/{index-6chd6jnr.js → index-sg0knfec.js} +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/oauth-destination.d.ts +35 -0
  13. package/dist/plugin-test-handles.d.ts +26 -0
  14. package/dist/rpc/port-file.js +1 -1
  15. package/dist/rpc/protocol.js +1 -1
  16. package/dist/rpc/rpc-client.js +1 -1
  17. package/dist/rpc/rpc-dir.js +1 -1
  18. package/dist/sidebar-state.d.ts +6 -3
  19. package/dist/sidebar-state.js +1 -1
  20. package/dist/tui/claude-sidebar.d.ts +58 -0
  21. package/dist/tui/menu-status.d.ts +70 -0
  22. package/dist/tui-preferences.js +2 -2
  23. package/dist/v2/adapter.d.ts +13 -8
  24. package/dist/v2/server.js +10 -10
  25. package/dist/v2/tui.d.ts +12 -0
  26. package/dist/v2/tui.js +4 -5
  27. package/package.json +2 -2
  28. package/src/sidebar-state.ts +55 -46
  29. package/src/tui/claude-sidebar.tsx +155 -15
  30. package/src/tui/entry.mjs +13 -0
  31. package/src/tui/menu-status.tsx +300 -0
  32. package/src/tui-compiled/sidebar-state.ts +55 -46
  33. package/src/tui-compiled/tui/claude-sidebar.tsx +112 -12
  34. package/src/tui-compiled/tui/menu-status.tsx +388 -0
  35. package/src/tui-compiled/tui-preferences.ts +32 -3
  36. package/src/tui-preferences.ts +32 -3
  37. package/dist/index-20d6rmjj.js +0 -2
  38. package/dist/index-cjjjgewq.js +0 -43
  39. package/dist/index-jvhs8sxv.js +0 -2
@@ -30,7 +30,9 @@ export interface AccountQuota {
30
30
 
31
31
  export interface SidebarAccountState {
32
32
  id: string
33
- label: string | undefined
33
+ // Absent, rather than present and `undefined`, when the account has no
34
+ // label: the OpenCode 2 RPC boundary refuses a value carrying `undefined`.
35
+ label?: string
34
36
  quota: AccountQuota | null
35
37
  enabled: boolean
36
38
  stateLine?: string
@@ -91,7 +93,9 @@ export interface SidebarState {
91
93
  refreshBackoffUntil?: number
92
94
  }
93
95
  fallbacks: SidebarAccountState[]
94
- activeId: string | undefined
96
+ // Absent rather than `undefined`: `normalizeSidebarState` never writes the
97
+ // key, and the OpenCode 2 RPC boundary refuses a value carrying `undefined`.
98
+ activeId?: string
95
99
  route: string
96
100
  relay: { enabled: boolean; transport: string } | null
97
101
  fastMode: boolean
@@ -146,7 +150,6 @@ export function getSidebarStateFile(): string {
146
150
  export const DEFAULT_SIDEBAR_STATE: SidebarState = {
147
151
  main: { quota: null },
148
152
  fallbacks: [],
149
- activeId: undefined,
150
153
  route: 'main',
151
154
  relay: null,
152
155
  fastMode: false,
@@ -350,40 +353,46 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
350
353
  }
351
354
  if (typeof m.quotaBackedOff === 'boolean')
352
355
  main.quotaBackedOff = m.quotaBackedOff
353
- if (typeof m.quotaBackoffUntil === 'number')
356
+ // Only finite numbers are display data: the OpenCode 2 RPC boundary
357
+ // refuses NaN and Infinity, so they are dropped like any wrong type.
358
+ if (isFiniteNumber(m.quotaBackoffUntil)) {
354
359
  main.quotaBackoffUntil = m.quotaBackoffUntil
360
+ }
355
361
  if (typeof m.refreshBackedOff === 'boolean')
356
362
  main.refreshBackedOff = m.refreshBackedOff
357
- if (typeof m.refreshBackoffUntil === 'number')
363
+ if (isFiniteNumber(m.refreshBackoffUntil)) {
358
364
  main.refreshBackoffUntil = m.refreshBackoffUntil
365
+ }
359
366
  }
360
367
 
361
368
  const fallbacks: SidebarAccountState[] = Array.isArray(raw.fallbacks)
362
369
  ? raw.fallbacks
363
370
  .filter(isRecord)
364
371
  .filter((entry) => typeof entry.id === 'string')
365
- .map((entry) => ({
366
- id: entry.id as string,
367
- label: typeof entry.label === 'string' ? entry.label : undefined,
368
- quota: normalizeAccountQuota(entry.quota),
369
- enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
370
- ...(typeof entry.stateLine === 'string' && {
371
- stateLine: entry.stateLine,
372
- }),
373
- ...(entry.quotaDated === true && { quotaDated: true }),
374
- needsReauth:
375
- typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
376
- ...(entry.vaultReauth === true && { vaultReauth: true }),
377
- ...(entry.vaultServed === true && { vaultServed: true }),
378
- ...(typeof entry.custodyState === 'string' && {
379
- custodyState:
380
- entry.custodyState as SidebarAccountState['custodyState'],
372
+ .map(
373
+ (entry): SidebarAccountState => ({
374
+ id: entry.id as string,
375
+ ...(typeof entry.label === 'string' && { label: entry.label }),
376
+ quota: normalizeAccountQuota(entry.quota),
377
+ enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
378
+ ...(typeof entry.stateLine === 'string' && {
379
+ stateLine: entry.stateLine,
380
+ }),
381
+ ...(entry.quotaDated === true && { quotaDated: true }),
382
+ needsReauth:
383
+ typeof entry.needsReauth === 'boolean'
384
+ ? entry.needsReauth
385
+ : false,
386
+ ...(entry.vaultReauth === true && { vaultReauth: true }),
387
+ ...(entry.vaultServed === true && { vaultServed: true }),
388
+ ...(typeof entry.custodyState === 'string' && {
389
+ custodyState:
390
+ entry.custodyState as SidebarAccountState['custodyState'],
391
+ }),
392
+ ...(typeof entry.tierLabel === 'string' &&
393
+ entry.tierLabel.trim() && { tierLabel: entry.tierLabel.trim() }),
381
394
  }),
382
- tierLabel:
383
- typeof entry.tierLabel === 'string' && entry.tierLabel.trim()
384
- ? entry.tierLabel.trim()
385
- : undefined,
386
- }))
395
+ )
387
396
  : []
388
397
 
389
398
  let relay: SidebarState['relay'] = null
@@ -400,11 +409,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
400
409
  if (typeof ck.enabled === 'boolean') {
401
410
  cacheKeep = {
402
411
  enabled: ck.enabled,
403
- window: typeof ck.window === 'string' ? ck.window : undefined,
404
- trackedSessions:
405
- typeof ck.trackedSessions === 'number'
406
- ? ck.trackedSessions
407
- : undefined,
412
+ ...(typeof ck.window === 'string' && { window: ck.window }),
413
+ ...(isFiniteNumber(ck.trackedSessions) && {
414
+ trackedSessions: ck.trackedSessions,
415
+ }),
408
416
  }
409
417
  }
410
418
  }
@@ -432,23 +440,23 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
432
440
  mode: recovery.mode,
433
441
  remaining: Math.max(0, Math.floor(recovery.remaining)),
434
442
  changedAt: recovery.changedAt,
435
- requestedModelId:
436
- typeof recovery.requestedModelId === 'string'
437
- ? recovery.requestedModelId
438
- : undefined,
439
- targetModelId:
440
- typeof recovery.targetModelId === 'string'
441
- ? recovery.targetModelId
442
- : undefined,
443
+ ...(typeof recovery.requestedModelId === 'string' && {
444
+ requestedModelId: recovery.requestedModelId,
445
+ }),
446
+ ...(typeof recovery.targetModelId === 'string' && {
447
+ targetModelId: recovery.targetModelId,
448
+ }),
443
449
  },
444
450
  ]
445
451
  })
446
452
  : []
447
453
 
454
+ const prime = normalizePrimeSection(raw.prime)
455
+
448
456
  return {
449
457
  main,
450
458
  fallbacks,
451
- activeId: typeof raw.activeId === 'string' ? raw.activeId : undefined,
459
+ ...(typeof raw.activeId === 'string' && { activeId: raw.activeId }),
452
460
  route:
453
461
  typeof raw.route === 'string' ? raw.route : DEFAULT_SIDEBAR_STATE.route,
454
462
  relay,
@@ -457,9 +465,9 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
457
465
  ? raw.fastMode
458
466
  : DEFAULT_SIDEBAR_STATE.fastMode,
459
467
  cacheKeep,
460
- prime: normalizePrimeSection(raw.prime),
461
- fableRecoveries: fableRecoveries.length > 0 ? fableRecoveries : undefined,
462
- lastUpdated: typeof raw.lastUpdated === 'number' ? raw.lastUpdated : 0,
468
+ ...(prime && { prime }),
469
+ ...(fableRecoveries.length > 0 && { fableRecoveries }),
470
+ lastUpdated: isFiniteNumber(raw.lastUpdated) ? raw.lastUpdated : 0,
463
471
  }
464
472
  }
465
473
 
@@ -692,9 +700,10 @@ export interface SidebarStateWriteOptions {
692
700
  })
693
701
  | undefined
694
702
  >
695
- onRoutingResolved?: (
696
- routing: Pick<SidebarState, 'activeId' | 'route'>,
697
- ) => void
703
+ onRoutingResolved?: (routing: {
704
+ activeId: string | undefined
705
+ route: string
706
+ }) => void
698
707
  }
699
708
 
700
709
  export async function setSidebarState(
@@ -601,6 +601,12 @@ export interface SidebarController {
601
601
  toggleCollapsed: () => void
602
602
  /** Stops the preference watcher; OpenCode 1 keeps it for the process. */
603
603
  dispose: () => void
604
+ /**
605
+ * However many refreshes in a row have failed, a retry never waits longer
606
+ * than this. Every attempt costs the server a full runtime read, so a
607
+ * failing sidebar must not keep trying at the poll rate forever.
608
+ */
609
+ retryMaxMs: number
604
610
  }
605
611
 
606
612
  // The TUI may unmount and remount sidebar_content when the user switches
@@ -658,7 +664,7 @@ export function createSidebarController(
658
664
  }
659
665
  }
660
666
 
661
- return { prefs, collapsed, toggleCollapsed, dispose }
667
+ return { prefs, collapsed, toggleCollapsed, dispose, retryMaxMs: 30_000 }
662
668
  }
663
669
 
664
670
  /** The sidebar panel for one session; the host owns polling and the theme. */
@@ -1070,40 +1076,174 @@ export type ReadSidebarState = (
1070
1076
  signal: AbortSignal,
1071
1077
  ) => Promise<SidebarState>
1072
1078
 
1073
- function HostSidebar(props: {
1074
- host: ClaudeSidebarHost
1079
+ /**
1080
+ * What the sidebar reads and draws with, independent of the host chrome.
1081
+ * The timer functions default to the global ones; a test passes its own so it
1082
+ * can drive the refresh schedule without touching the shared globals.
1083
+ */
1084
+ export interface HostSidebarPropsState {
1075
1085
  read: ReadSidebarState
1076
1086
  controller: SidebarController
1077
1087
  sessionId: string
1078
- }) {
1079
- const [state, setState] = createSignal<SidebarState>(DEFAULT_SIDEBAR_STATE)
1088
+ setTimeoutImpl?: typeof globalThis.setTimeout
1089
+ /** Clears a handle `setTimeoutImpl` returned, whatever type that was. */
1090
+ clearTimeoutImpl?: (handle: unknown) => void
1091
+ }
1092
+
1093
+ interface HostSidebarProps extends HostSidebarPropsState {
1094
+ host: ClaudeSidebarHost
1095
+ }
1096
+
1097
+ /** The refresh loop's own timers, so a test can drive its schedule. */
1098
+ export interface SidebarScheduleTimers {
1099
+ setTimeoutImpl?: typeof globalThis.setTimeout
1100
+ clearTimeoutImpl?: (handle: unknown) => void
1101
+ }
1102
+
1103
+ /**
1104
+ * Delay before the next refresh after `failures` failed reads in a row, each
1105
+ * one longer than the last until the controller's limit. A successful read
1106
+ * resets the count, so a healthy server is polled at the configured rate. A
1107
+ * failed read still costs the server a full runtime read, so retrying at the
1108
+ * poll rate keeps that cost alive for as long as the failure lasts.
1109
+ */
1110
+ export function sidebarRetryDelay(
1111
+ pollMs: number,
1112
+ failures: number,
1113
+ retryMaxMs: number,
1114
+ ): number {
1115
+ if (failures <= 0) return pollMs
1116
+ return Math.min(pollMs * 2 ** failures, retryMaxMs)
1117
+ }
1118
+
1119
+ /** Starts and stops the refresh schedule the sidebar lives on. */
1120
+ export interface SidebarSchedule {
1121
+ /**
1122
+ * Reads once and then keeps the retry timer running. Safe to call more than
1123
+ * once: a running schedule is left alone.
1124
+ */
1125
+ start: () => void
1126
+ /**
1127
+ * Stops the schedule for good: clears the waiting timer and aborts a read
1128
+ * still in flight, so it cannot queue another one. The item that unmounts a
1129
+ * sidebar calls this; without it, a failing refresh would keep polling the
1130
+ * server from a sidebar nobody is looking at.
1131
+ */
1132
+ stop: () => void
1133
+ }
1134
+
1135
+ // One read on start, then a timer that backs off after failures and resets
1136
+ // after a success. Lives outside Solid: the component drives it from an
1137
+ // effect, and tests drive it with their own timers.
1138
+ export function createSidebarSchedule(
1139
+ options: {
1140
+ read: ReadSidebarState
1141
+ sessionId: string
1142
+ pollRate: () => { pollMs: number; retryMaxMs: number }
1143
+ /** Receives each answer that differs from the one before it. */
1144
+ onState: (state: SidebarState) => void
1145
+ } & SidebarScheduleTimers,
1146
+ ): SidebarSchedule {
1147
+ const {
1148
+ read,
1149
+ sessionId,
1150
+ pollRate,
1151
+ onState,
1152
+ setTimeoutImpl = setTimeout,
1153
+ clearTimeoutImpl = clearTimeout as (handle: unknown) => void,
1154
+ } = options
1080
1155
  const abort = new AbortController()
1156
+ // The timer waiting to run. Every schedule replaces it, so stopping always
1157
+ // clears the one that is actually queued.
1158
+ let timer: unknown
1159
+ let running = false
1081
1160
  let inFlight = false
1082
1161
  let shown = ''
1083
- const refresh = async () => {
1084
- if (inFlight || abort.signal.aborted) return
1162
+ let failures = 0
1163
+ // `undefined` means the previous read was still running, which is not a
1164
+ // failure: counting it as one would back off before the server answered.
1165
+ const refresh = async (): Promise<boolean | undefined> => {
1166
+ if (inFlight || abort.signal.aborted) return undefined
1085
1167
  inFlight = true
1086
1168
  try {
1087
- const next = await props.read(props.sessionId, abort.signal)
1088
- // Every answer is stamped now; redraw only when its content changed.
1089
- // Countdowns and ages advance on the sidebar's own clock.
1169
+ const next = await read(sessionId, abort.signal)
1170
+ // Every answer is stamped by the server; redraw only when its content
1171
+ // changed. Countdowns and ages advance on the sidebar's own clock.
1090
1172
  const content = JSON.stringify({ ...next, lastUpdated: 0 })
1091
1173
  if (!abort.signal.aborted && content !== shown) {
1092
1174
  shown = content
1093
- setState(next)
1175
+ onState(next)
1094
1176
  }
1177
+ return true
1095
1178
  } catch {
1096
1179
  // Keep the last state; an older server without `sidebar` keeps waiting.
1180
+ return false
1097
1181
  } finally {
1098
1182
  inFlight = false
1099
1183
  }
1100
1184
  }
1185
+ const schedule = () => {
1186
+ if (!running) return
1187
+ const { pollMs, retryMaxMs } = pollRate()
1188
+ const wait = sidebarRetryDelay(pollMs, failures, retryMaxMs)
1189
+ timer = setTimeoutImpl(async () => {
1190
+ timer = undefined
1191
+ if (!running) return
1192
+ const ok = await refresh()
1193
+ if (!running) return
1194
+ if (ok === true) failures = 0
1195
+ else if (ok === false) failures += 1
1196
+ schedule()
1197
+ }, wait)
1198
+ }
1199
+ return {
1200
+ start() {
1201
+ if (running || abort.signal.aborted) return
1202
+ running = true
1203
+ // The first read happens here rather than in the timer, so a caller that
1204
+ // starts before any draw still has an answer waiting for it. The loop
1205
+ // then continues on the retry timer below.
1206
+ void refresh().then((ok) => {
1207
+ if (running && ok === false) failures += 1
1208
+ })
1209
+ schedule()
1210
+ },
1211
+ stop() {
1212
+ running = false
1213
+ if (timer !== undefined) clearTimeoutImpl(timer)
1214
+ timer = undefined
1215
+ abort.abort()
1216
+ },
1217
+ }
1218
+ }
1219
+
1220
+ // The OpenCode 2 sidebar's state: its refresh schedule, redrawn through a
1221
+ // signal the component renders from.
1222
+ export function useHostSidebarState(
1223
+ props: HostSidebarPropsState,
1224
+ ): () => SidebarState {
1225
+ const [state, setState] = createSignal<SidebarState>(DEFAULT_SIDEBAR_STATE)
1101
1226
  createEffect(() => {
1102
- const timer = setInterval(refresh, props.controller.prefs().pollMs)
1103
- onCleanup(() => clearInterval(timer))
1227
+ // Reading the poll rate here re-runs this effect when it changes. A
1228
+ // stopped schedule never restarts, so each run builds a fresh one and the
1229
+ // cleanup stops the run it belongs to.
1230
+ const pollMs = props.controller.prefs().pollMs
1231
+ const schedule = createSidebarSchedule({
1232
+ read: props.read,
1233
+ sessionId: props.sessionId,
1234
+ pollRate: () => ({ pollMs, retryMaxMs: props.controller.retryMaxMs }),
1235
+ onState: setState,
1236
+ setTimeoutImpl: props.setTimeoutImpl,
1237
+ clearTimeoutImpl: props.clearTimeoutImpl,
1238
+ })
1239
+ schedule.start()
1240
+ onCleanup(() => schedule.stop())
1104
1241
  })
1105
- void refresh()
1106
- onCleanup(() => abort.abort())
1242
+ return state
1243
+ }
1244
+
1245
+ function HostSidebar(props: HostSidebarProps) {
1246
+ const state = useHostSidebarState(props)
1107
1247
  return (
1108
1248
  <ClaudeSidebar
1109
1249
  state={state}
package/src/tui/entry.mjs CHANGED
@@ -59,6 +59,17 @@ async function loadSignInLink() {
59
59
  return import('../tui-compiled/tui/sign-in-link.tsx')
60
60
  }
61
61
 
62
+ // The /claude menu's status view, loaded the same way as the sidebar.
63
+ async function loadMenuStatus() {
64
+ try {
65
+ await import(runtimeProbe)
66
+ } catch (error) {
67
+ if (!isMissingRuntimeRegistry(error)) throw error
68
+ return import('./menu-status.tsx')
69
+ }
70
+ return import('../tui-compiled/tui/menu-status.tsx')
71
+ }
72
+
62
73
  let legacyModule
63
74
 
64
75
  // OpenCode 1 reads id and tui from the default definition; OpenCode 2 reads
@@ -84,6 +95,8 @@ export const setup = async (context) => {
84
95
  (await loadSidebar()).mountClaudeSidebar(ctx, read),
85
96
  showSignInLink: async (ctx, url, signal) =>
86
97
  (await loadSignInLink()).showSignInLinkDialog(ctx, url, signal),
98
+ showStatus: async (ctx, view, signal) =>
99
+ (await loadMenuStatus()).showMenuStatusDialog(ctx, view, signal),
87
100
  })
88
101
  }
89
102