import * as plugins from './plugins.js'; import type * as interfaces from '../ts_interfaces/index.js'; import type { ControllerSocketClient } from './classes.controllersocketclient.js'; import { AccountLimitsReadModel, accountLimitsSummaryWindow, resolveAccountLimitsCurrent, type IAccountLimitsReadState, type TAccountLimitsCodexAccountRequest, type TAccountLimitsRequest, } from './classes.accountlimitsreadmodel.js'; type TAccount = interfaces.TAuthSwitchAccounts['harnesses'][number]['accounts'][number]; type THarness = interfaces.TAuthSwitchAccounts['harnesses'][number]; type TBilling = NonNullable['billing']>; export interface IAccountLimitsOpenRequestDetail { anchor: HTMLElement; } export type TAccountLimitsCloseReason = 'trigger' | 'popover' | 'button'; export interface IAccountLimitsCloseDetail { reason: TAccountLimitsCloseReason; } const timestamp = (valueArg: string | null | undefined): string => { if (!valueArg) return 'Not reported'; const parsed = Date.parse(valueArg); return Number.isFinite(parsed) ? new Date(parsed).toLocaleString() : valueArg; }; const duration = (secondsArg: number): string => { if (secondsArg % 86400 === 0) return `${secondsArg / 86400}d`; if (secondsArg % 3600 === 0) return `${secondsArg / 3600}h`; if (secondsArg % 60 === 0) return `${secondsArg / 60}min`; return `${secondsArg}s`; }; const billingRows = (billingArg: TBilling | undefined): [string, string][] => { if (!billingArg) return []; return [ billingArg.hasActiveSubscription === undefined ? undefined : ['Active subscription', billingArg.hasActiveSubscription ? 'Yes' : 'No'], billingArg.autoRenew === undefined ? undefined : ['Automatic renewal', billingArg.autoRenew ? 'On' : 'Off'], billingArg.renewsAt === undefined ? undefined : ['Renews', timestamp(billingArg.renewsAt)], billingArg.cancelsAt === undefined ? undefined : ['Cancels', timestamp(billingArg.cancelsAt)], billingArg.expiresAt === undefined ? undefined : ['Expires', timestamp(billingArg.expiresAt)], ].filter((rowArg): rowArg is [string, string] => rowArg !== undefined); }; @plugins.deesElement.customElement('agl-account-limits') export class AglAccountLimits extends plugins.deesElement.DeesElement { @plugins.deesElement.property({ attribute: false }) accessor client: ControllerSocketClient | undefined; @plugins.deesElement.property({ attribute: false }) accessor context: interfaces.TControllerAuthSwitchLimitsContext | undefined; @plugins.deesElement.property({ attribute: false }) accessor contextKey = ''; @plugins.deesElement.property({ type: Boolean, reflect: true }) accessor open = false; @plugins.deesElement.state() private accessor readState: IAccountLimitsReadState = { harnesses: [], loadingCurrent: false, loadingAll: false, staleHarnessIds: [], }; @plugins.deesElement.state() private accessor triggerElement: HTMLElement | null = null; private configuredClient: ControllerSocketClient | undefined; private readonly readModel = new AccountLimitsReadModel((stateArg) => { this.readState = stateArg; }); public static styles = [ plugins.deesElement.cssManager.defaultStyles, plugins.deesElement.css` :host { display: block; width: 188px; min-width: 188px; height: 100%; min-height: 0; color: var(--dees-color-text-primary); font-family: var(--dees-font-family); } .trigger { box-sizing: border-box; display: grid; grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); width: 100%; height: 100%; min-height: 30px; padding: 3px 10px; border: 0; border-left: 1px solid var(--dees-color-border-subtle); border-right: 1px solid var(--dees-color-border-subtle); background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; } .trigger:hover, .trigger:focus-visible, :host([open]) .trigger { background: var(--dees-color-fill-secondary); } .trigger:focus-visible { outline: 2px solid var(--dees-color-accent-primary); outline-offset: -2px; } .primary, .secondary { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .primary { align-self: end; font-size: 11px; font-weight: 600; line-height: 13px; } .secondary { align-self: start; color: var(--dees-color-text-secondary); font-size: 10px; line-height: 12px; } .trigger.stale .secondary, .banner.error, .problemList, .staleLabel { color: var(--dees-color-text-warning); } .limitsPanel { box-sizing: border-box; display: grid; gap: 12px; padding: 14px; color: var(--dees-color-text-primary); font: 12px/1.45 system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } .panelHeader, .accountHeader, .harnessHeader { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } .panelHeader h3, .harnessHeader h4, .accountHeader h5 { margin: 0; overflow-wrap: anywhere; } .panelHeader h3 { font-size: 16px; } .harnessHeader h4 { font-size: 14px; } .accountHeader h5 { font-size: 13px; } .accountHeader { display: grid; justify-items: start; gap: 6px; } .accountHeader h5 { width: 100%; min-width: 0; } .accountHeader .badges { justify-content: flex-start; } .panelActions { display: flex; flex: 0 0 auto; gap: 6px; } .banner, .harness, .account, .empty { border: 1px solid var(--dees-color-border-subtle); border-radius: 9px; } .banner, .empty { padding: 9px 10px; font-size: 12px; } .harness { display: grid; gap: 10px; padding: 11px; } .account { display: grid; gap: 9px; padding: 10px; background: var(--dees-color-bg-secondary); } .metadata, .muted, .freshness, .badge { color: var(--dees-color-text-secondary); font-size: 11px; } .badges { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px; } .badge { padding: 2px 6px; border: 1px solid var(--dees-color-border-subtle); border-radius: 999px; white-space: nowrap; } .badge.current { border-color: color-mix(in srgb, var(--dees-color-accent-primary) 55%, transparent); background: color-mix(in srgb, var(--dees-color-accent-primary) 12%, transparent); color: var(--dees-color-accent-primary); font-weight: 700; } .detailGrid { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 4px 12px; margin: 0; font-size: 12px; } .detailGrid dt { color: var(--dees-color-text-secondary); } .detailGrid dd { margin: 0; overflow-wrap: anywhere; } .sectionTitle { margin: 2px 0 0; color: var(--dees-color-text-secondary); font-size: 11px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase; } .problemList, .factList { margin: 0; padding-left: 18px; font-size: 12px; } .problemList li + li, .factList li + li { margin-top: 4px; } @media (max-width: 600px) { :host { width: 160px; min-width: 160px; } .limitsPanel { padding: 11px; } .panelHeader { align-items: stretch; flex-direction: column; } .panelActions { justify-content: flex-end; } .detailGrid { grid-template-columns: 1fr; gap: 1px; } .detailGrid dd { margin-bottom: 5px; } } `, ]; public async connectedCallback(): Promise { await super.connectedCallback(); this.syncInputs(); this.readModel.start(); } public async disconnectedCallback(): Promise { this.readModel.stop(); await super.disconnectedCallback(); } public firstUpdated(): void { this.triggerElement = this.shadowRoot?.querySelector('.trigger') ?? null; } public updated(changesArg: Map): void { super.updated(changesArg); if (changesArg.has('client') || changesArg.has('context') || changesArg.has('contextKey')) { this.syncInputs(); } if (changesArg.has('open') && this.open && !changesArg.get('open')) { void this.readModel.refreshAll(); } } /** Refresh the current authoritative account only. */ public async refresh(): Promise { await this.readModel.refreshCurrent(); } /** Fence prior current-account reads and refresh changed authswitch state. */ public invalidate(): void { this.readModel.invalidate(); if (this.open) void this.readModel.refreshAll(); } private syncInputs(): void { if (this.configuredClient !== this.client) { this.configuredClient = this.client; let request: TAccountLimitsRequest | undefined; let codexAccountRequest: TAccountLimitsCodexAccountRequest | undefined; if (this.client) { const client = this.client; request = (requestArg, signalArg) => client.fire( 'controller.authswitch.request', requestArg, { maxRetries: 0, abortSignal: signalArg }, ); codexAccountRequest = async (profileIdArg, signalArg) => ( await client.fire( 'controller.codex.account.get', { profileId: profileIdArg }, { maxRetries: 0, abortSignal: signalArg }, ) ).account; } this.readModel.setRequest(request); this.readModel.setCodexAccountRequest(codexAccountRequest); } this.readModel.setContext(this.context, this.contextKey); } private triggerClick = (): void => { if (this.open) { this.emitClose('trigger'); return; } if (!this.triggerElement) return; this.dispatchEvent(new CustomEvent( 'account-limits-open-request', { detail: { anchor: this.triggerElement }, bubbles: true, composed: true }, )); }; private emitClose(reasonArg: TAccountLimitsCloseReason): void { this.dispatchEvent(new CustomEvent('account-limits-close', { detail: { reason: reasonArg }, bubbles: true, composed: true, })); } private handlePopoverClose = (): void => { // DeesPopover owns outside/Escape/anchor-loss dismissal. Mirror that state immediately so // the controlled trigger cannot remain aria-expanded while the inner popover is closed. this.open = false; this.emitClose('popover'); }; private renderTrigger(): plugins.deesElement.TemplateResult { const current = resolveAccountLimitsCurrent( this.readState.accounts, this.context, this.readState.runtimeIdentity, ); let stale = false; let primary: string; let secondary: string; let title: string; let ariaLabel: string; if (!current.available) { const message = this.readState.currentError ?? current.message; const provider = this.context?.available ? this.context.harnessId === 'opencode' ? 'OpenCode' : this.context.harnessId : this.context?.reason === 'remote_codex_profile' || this.context?.reason === 'codex_runtime_account_unresolved' ? 'Codex' : this.context?.reason === 'flex_account_unavailable' ? 'Flex' : this.context?.reason === 'opencode_provider_unresolved' ? 'OpenCode' : undefined; stale = Boolean(this.readState.currentError); primary = provider ? `${provider} limits` : 'Account limits'; secondary = 'Current account unavailable'; title = message; ariaLabel = `Account limits. ${message}`; } else { const summaryWindow = accountLimitsSummaryWindow( current.account.status.summary?.usageWindows ?? [], ); stale = this.readState.staleHarnessIds.includes(current.harness.id) || Boolean(this.readState.currentError); secondary = summaryWindow ? `${summaryWindow.label} · ${summaryWindow.usedPercent}% used` : current.account.status.rateLimit ? 'Provider rate limited · usage unknown' : 'No usage window reported'; primary = `${current.harness.label} · ${current.account.label}`; const staleSuffix = stale ? ' · Last reading is stale' : ''; title = `${primary} · ${secondary}${staleSuffix}`; ariaLabel = `Account limits. ${primary}. ${secondary}${stale ? '. Last reading is stale.' : ''}`; } // Keep this template site and button node stable while current-account identity refreshes. // DeesPopover holds the node as its anchor, so replacing it would detach an open popover. return plugins.deesElement.html` `; } private renderAccount( harnessArg: THarness, accountArg: TAccount, currentArg: ReturnType, ): plugins.deesElement.TemplateResult { const summary = accountArg.status.summary; const billing = billingRows(summary?.billing); const windows = summary?.usageWindows ?? []; const resets = summary?.resets; return plugins.deesElement.html` `; } private renderHarness( harnessArg: THarness, currentArg: ReturnType, ): plugins.deesElement.TemplateResult { const behaviour = this.readState.harnesses.find((itemArg) => itemArg.id === harnessArg.id); const stale = this.readState.staleHarnessIds.includes(harnessArg.id); const orderedAccounts = currentArg.available && currentArg.harness === harnessArg ? [currentArg.account, ...harnessArg.accounts.filter( (accountArg) => accountArg !== currentArg.account, )] : harnessArg.accounts; return plugins.deesElement.html`

${harnessArg.label}

${stale ? plugins.deesElement.html`
Showing the last available reading for this provider.
` : ''} ${behaviour?.renewalUnavailableReason ? plugins.deesElement.html` ` : ''} ${harnessArg.saveUnavailableReason ? plugins.deesElement.html` ` : ''} ${harnessArg.problems.length ? plugins.deesElement.html`
    ${harnessArg.problems.map((problemArg) => plugins.deesElement.html`
  • ${problemArg}
  • `)}
` : ''} ${orderedAccounts.length ? orderedAccounts.map((accountArg) => this.renderAccount(harnessArg, accountArg, currentArg)) : plugins.deesElement.html`
No accounts were reported for this provider.
`} ${harnessArg.credentialDrift.length ? plugins.deesElement.html`
Credential drift
${harnessArg.credentialDrift.map((driftArg) => plugins.deesElement.html`
Expected account
${driftArg.expectedLabel ?? driftArg.expectedAccountId}
Current account
${driftArg.currentLabel ?? driftArg.currentAccountId ?? 'No active login'}
Changed since
${timestamp(driftArg.switchedAt)}
${driftArg.slotId ? plugins.deesElement.html`
Slot
${driftArg.slotId}
` : ''}
`)} ` : ''}
${harnessArg.loginHint}
`; } private renderPanel(): plugins.deesElement.TemplateResult { const accounts = this.readState.accounts; const current = resolveAccountLimitsCurrent( accounts, this.context, this.readState.runtimeIdentity, ); const orderedHarnesses = accounts && current.available ? [current.harness, ...accounts.harnesses.filter( (harnessArg) => harnessArg !== current.harness, )] : accounts?.harnesses; return plugins.deesElement.html`

Account limits

${this.readState.allUpdatedAt ? `All accounts refreshed ${new Date(this.readState.allUpdatedAt).toLocaleTimeString()}` : 'All accounts have not been refreshed in this panel yet.'}
void this.readModel.refreshAll()}> this.emitClose('button')}>
Current session
${current.available ? `${current.harness.label} · ${current.account.label}` : current.message}
${this.readState.allError ? plugins.deesElement.html` ` : ''} ${accounts && !accounts.complete ? plugins.deesElement.html` ` : ''} ${this.readState.loadingAll && !accounts ? plugins.deesElement.html`
Loading all account limits…
` : ''} ${orderedHarnesses ? orderedHarnesses.map((harnessArg) => this.renderHarness(harnessArg, current)) : !this.readState.loadingAll ? plugins.deesElement.html`
No account snapshot is available.
` : ''}
`; } public render(): plugins.deesElement.TemplateResult { return plugins.deesElement.html` ${this.renderTrigger()} ${this.renderPanel()} `; } } declare global { interface HTMLElementTagNameMap { 'agl-account-limits': AglAccountLimits; } }