import * as plugins from './plugins.js'; import type * as interfaces from '../ts_interfaces/index.js'; import type { ControllerSocketClient } from './classes.controllersocketclient.js'; type TAccount = interfaces.TAuthSwitchAccounts['harnesses'][number]['accounts'][number]; type TAccountStatus = TAccount['status']; type TMutationRequest = Extract; /** The request an operation belongs to; `get` and `cancel` observe that operation and never replace it. */ type TOperationRequest = Extract; type TUsageWindow = interfaces.TAuthSwitchUsageWindow; type TUsageAlert = Exclude, 'normal'>; const remaining = (date: string | null | undefined): string => { if (!date || !Number.isFinite(Date.parse(date))) return 'Unknown'; let minutes = Math.max(0, Math.ceil((Date.parse(date) - Date.now()) / 60_000)); if (minutes === 0) return 'Due now'; const days = Math.floor(minutes / 1440); minutes %= 1440; const hours = Math.floor(minutes / 60); minutes %= 60; return [days ? `${days}d` : '', hours ? `${hours}h` : '', minutes ? `${minutes}min` : ''].filter(Boolean).join(' '); }; /** The provider's own pick for a single-value summary; the first marked window leads. */ const headlineUsageWindow = (windows: readonly TUsageWindow[]): TUsageWindow | undefined => windows.find(window => window.headline === true); /** The window the Usage column shows: the provider's headline, otherwise the first general window of at least six days. */ const summaryUsageWindow = (windows: readonly TUsageWindow[]): TUsageWindow | undefined => headlineUsageWindow(windows) ?? windows.find(window => window.scope !== 'feature' && window.durationSeconds >= 6 * 86400); /** The provider's reading worth emphasising; a normal or absent reading is not marked. */ const usageWindowAlert = (window: TUsageWindow): TUsageAlert | undefined => window.severity === 'warning' || window.severity === 'critical' ? window.severity : undefined; /** * Whether a fact repeats a summary field this view shows in full, which the fact contract lets a view omit. * Billing is shown only in part and reset details not at all, so their facts stay. */ const statusShowsFact = (status: TAccountStatus, fact: TAccountStatus['facts'][number]): boolean => { switch (fact.summaryKey) { case 'subscription': return status.summary?.subscription !== undefined; case 'usageWindows': return status.summary?.usageWindows !== undefined; case 'resets': return status.summary?.resets !== undefined; case 'billing': case 'resetDetails': case undefined: return false; } }; @plugins.deesElement.customElement('harness-authswitch') export class HarnessAuthSwitch extends plugins.deesElement.DeesElement { @plugins.deesElement.property({ attribute: false }) accessor client: ControllerSocketClient | undefined; @plugins.deesElement.state() private accessor accounts: interfaces.TAuthSwitchAccounts | undefined; @plugins.deesElement.state() private accessor capabilities: NonNullable = []; @plugins.deesElement.state() private accessor harnessBehaviour: interfaces.IControllerAuthSwitchHarness[] = []; @plugins.deesElement.state() private accessor harnessId = 'opencode'; @plugins.deesElement.state() private accessor operation: interfaces.TAuthSwitchOperation | undefined; @plugins.deesElement.state() private accessor loading = false; @plugins.deesElement.state() private accessor uncertain = false; @plugins.deesElement.state() private accessor error = ''; @plugins.deesElement.state() private accessor notice: string[] = []; @plugins.deesElement.state() private accessor selectedAccountId = ''; @plugins.deesElement.state() private accessor confirmation: { text: string; mutation: interfaces.TAuthSwitchMutation } | undefined; @plugins.deesElement.state() private accessor waitingRequest: TMutationRequest | undefined; private contextId: string | undefined; /** The request the shown operation belongs to; a poll keeps it, so it names the work in flight. */ private operationRequest: TOperationRequest | undefined; private pollTimer: ReturnType | undefined; private requestGeneration = 0; public static styles = [plugins.deesElement.cssManager.defaultStyles, plugins.deesElement.css` :host { display: block; min-width: 0; } .stack { display: grid; gap: 10px; } .actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } .notice, .error, .confirmation { padding: 10px; border: 1px solid var(--dees-color-border); border-radius: 8px; font-size: 13px; } .error, dd.warning { color: var(--dees-color-text-warning); } dd.critical { color: var(--dees-color-text-error); font-weight: 600; } .muted { opacity: .72; font-size: 12px; } dl { display: grid; grid-template-columns: minmax(100px, 1fr) minmax(0, 2fr); gap: 6px 16px; font-size: 13px; margin: 0; } dt { opacity: .72; } dd { margin: 0; overflow-wrap: anywhere; } h4 { margin: 8px 0; } ul { padding-left: 20px; margin: 0; } code { font-size: 18px; user-select: all; } a { color: var(--dees-color-accent-primary); } `]; public firstUpdated(): void { void this.refresh(); } public async connectedCallback(): Promise { await super.connectedCallback(); if (this.operation?.state === 'pending') this.schedulePoll(); } public async disconnectedCallback(): Promise { if (this.pollTimer) clearTimeout(this.pollTimer); this.pollTimer = undefined; await super.disconnectedCallback(); } public updated(changes: Map): void { super.updated(changes); if (changes.has('client') && changes.get('client') !== undefined) { this.requestGeneration++; if (this.pollTimer) clearTimeout(this.pollTimer); this.operation = undefined; this.loading = false; this.uncertain = false; this.contextId = undefined; this.accounts = undefined; this.capabilities = []; this.confirmation = undefined; this.waitingRequest = undefined; this.harnessBehaviour = []; void this.refresh(); } } private selectedHarness() { return this.accounts?.harnesses.find(harness => harness.id === this.harnessId); } /** How a switch behaves for the selected harness; the controller reports it per harness. */ private selectedBehaviour() { return this.harnessBehaviour.find(harness => harness.id === this.harnessId); } /** * What replacing the active login means for the sessions that are running right now. A live-swap * harness needs no restart at all, AGL restarts the runtime it manages for a harness once the * work is idle, and for every other harness the running sessions keep the login they loaded. */ private runningSessionsSentence(): string { const label = this.selectedHarness()?.label ?? this.harnessId; const behaviour = this.selectedBehaviour(); if (behaviour?.liveSwap) return `Running ${label} sessions use it from their next request; nothing is restarted.`; if (behaviour?.restartsManagedRuntime) return `AGL restarts managed ${label} when it is idle.`; return `Running ${label} sessions keep the login they loaded until they are restarted.`; } /** * What the operation in flight means for running work. Only a change of the active login reaches * it, so the three harness cases are stated for those changes alone; reading accounts, saving a * copy of the active login, removing a saved copy and signing a new account in say what they do * and leave every running session out of it. */ private busySentence(): string { const request = this.operationRequest; // The sentence names the harness the request went to, so it stays right even if the selection moved. const harnessId = request?.action === 'mutate' ? request.coordination.mutation.harnessId : request?.action === 'login' ? request.harnessId : this.harnessId; const label = this.accounts?.harnesses.find(harness => harness.id === harnessId)?.label ?? harnessId; if (request?.action === 'mutate') { const mutation = request.coordination.mutation; if (!plugins.authswitchMutation.authSwitchMutationReplacesLogin(mutation)) { return mutation.action === 'save' ? `Working… Saving a copy of the active ${label} login; it stays active and nothing is restarted.` : `Working… Removing the saved copy; the active ${label} login is unchanged.`; } const behaviour = this.harnessBehaviour.find(harness => harness.id === harnessId); if (behaviour?.liveSwap) return `Working… Running ${label} sessions use a new login from their next request.`; if (behaviour?.restartsManagedRuntime) return `Working… Active work is allowed to finish before ${label} restarts.`; return `Working… Running ${label} sessions keep the login they loaded until they are restarted.`; } if (request?.action === 'login') return `Working… A completed sign-in saves a new ${label} account without activating it.`; // The panel is busy only for a request it sent, and the first one it sends is the account list. return 'Working… Reading accounts; no login is changed.'; } private isBusy(): boolean { return this.loading || this.operation?.state === 'pending'; } private async refresh(): Promise { if (this.isBusy()) return; this.waitingRequest = undefined; this.confirmation = undefined; await this.submit({ protocolVersion: 1, action: 'list' }); } private async recoverAccounts(): Promise { if (this.loading) return; if (this.pollTimer) clearTimeout(this.pollTimer); this.pollTimer = undefined; this.uncertain = true; this.operation = undefined; this.operationRequest = undefined; await this.refresh(); } private async submit(request: interfaces.TAuthSwitchRequest): Promise { if (!this.client || this.loading) return; if (this.uncertain && (request.action === 'mutate' || request.action === 'login')) return; const client = this.client; const generation = ++this.requestGeneration; this.loading = true; this.error = ''; if (request.action === 'list' || request.action === 'mutate' || request.action === 'login') { this.operationRequest = request; this.operation = undefined; if (request.action !== 'list') this.notice = []; } try { const { operation, harnesses } = await client.fire( 'controller.authswitch.request', request, { maxRetries: 0 }, ); if (generation !== this.requestGeneration) return; this.operation = operation; this.contextId = operation.contextId; this.harnessBehaviour = harnesses; this.loading = false; if (operation.state === 'pending') this.schedulePoll(); else this.completed(operation); } catch { if (generation !== this.requestGeneration) return; if (request.action === 'mutate' || request.action === 'login') this.uncertain = true; this.error = this.operation?.state === 'pending' ? 'Connection lost. Check progress to resume observing this operation.' : request.action === 'list' ? 'Accounts could not be loaded. Refresh to try again.' : 'The result could not be confirmed. Refresh and check the accounts before trying another change.'; } finally { if (generation === this.requestGeneration) this.loading = false; } } private schedulePoll(): void { if (this.pollTimer) clearTimeout(this.pollTimer); if (!this.isConnected) return; this.pollTimer = setTimeout(() => { this.pollTimer = undefined; if (this.operation?.state === 'pending') void this.submit({ protocolVersion: 1, action: 'get', operationId: this.operation.id }); }, 600); } private completed(operation: interfaces.TAuthSwitchOperation): void { if (operation.state === 'failed') { if (this.operationRequest?.action === 'mutate' || this.operationRequest?.action === 'login') this.uncertain = true; this.error = operation.error ?? 'The account operation failed.'; return; } if (operation.accounts) { this.uncertain = false; this.accounts = operation.accounts; this.capabilities = operation.loginCapabilities ?? []; if (!this.selectedHarness()) this.harnessId = operation.accounts.harnesses[0]?.id ?? ''; return; } if (operation.result?.status === 'busy') { this.notice = [operation.result.message]; this.waitingRequest = this.operationRequest?.action === 'mutate' ? this.operationRequest : undefined; return; } if (operation.result?.status === 'complete') { this.notice = operation.result.outcome.lines; this.error = operation.result.outcome.problems.join(' '); if (operation.result.outcome.problems.length) this.uncertain = true; } if (operation.account) { this.selectedAccountId = operation.account.id; this.notice = [`Saved ${operation.account.label}. Select Switch to use this account.`]; } this.dispatchEvent(new CustomEvent('authswitch-changed', { bubbles: true, composed: true })); // Refresh is read-only. Preserve the operation result while loading the new snapshot. const operationError = this.error; void this.refresh().then(() => { if (operationError) this.error = operationError; }); } private async mutate(mutation: interfaces.TAuthSwitchMutation, waitForIdle = false): Promise { if (!this.contextId || this.isBusy() || this.uncertain) return; this.confirmation = undefined; this.waitingRequest = undefined; await this.submit({ protocolVersion: 1, action: 'mutate', coordination: { protocolVersion: 1, contextId: this.contextId, mutation, waitForIdle, } }); } private offerSwitch(account: TAccount): void { if (this.isBusy() || this.uncertain) return; const unsaved = this.selectedHarness()?.accounts.find(item => item.isActive && !item.isStashed && item.slotId === account.slotId); this.confirmation = { text: unsaved ? `${unsaved.label} is not saved yet. Save it, then switch to ${account.label}?` : `Switch to ${account.label}? ${this.runningSessionsSentence()}`, mutation: { harnessId: this.harnessId, action: 'switch', accountId: account.id }, }; } private readonly columns: plugins.deesCatalog.Column[] = [ { key: 'label', header: 'Account' }, { key: 'state', header: 'State', value: account => [account.isActive ? 'Active' : '', account.isStashed ? 'Saved' : 'Not saved'].filter(Boolean).join(' · ') }, { key: 'subscription', header: 'Subscription', value: account => { const subscription = account.status.summary?.subscription; return subscription ? `${subscription.plan}${subscription.source === 'stored' ? ' (stored)' : ''}` : 'Unknown'; } }, { key: 'usage', header: 'Usage', value: account => { const window = summaryUsageWindow(account.status.summary?.usageWindows ?? []); return window ? `${window.label}: ${window.usedPercent}% · resets in ${remaining(window.resetAt)}` : 'Unknown'; } }, { key: 'billing', header: 'Renewal / cancellation', value: account => { const billing = account.status.summary?.billing; if (billing?.cancelsAt) return `Cancels ${new Date(billing.cancelsAt).toLocaleDateString()}`; if (billing?.renewsAt) return `${billing.autoRenew === true ? 'Auto-renews' : 'Renews'} ${new Date(billing.renewsAt).toLocaleDateString()}`; if (billing?.expiresAt) return `Expires ${new Date(billing.expiresAt).toLocaleDateString()}`; return billing?.autoRenew === false ? 'Auto-renew off · date unknown' : 'Unknown'; } }, ]; private tableActions(): plugins.deesCatalog.ITableAction[] { return [ { name: 'Refresh', iconName: 'lucide:RefreshCw', type: ['header'], actionFunc: async () => this.refresh() }, ...this.capabilities.find(item => item.harnessId === this.harnessId)?.providers.flatMap(provider => provider.flows.map(flow => ({ name: `Sign in · ${provider.label}`, iconName: 'lucide:LogIn' as const, type: ['header'] as ['header'], actionFunc: async () => { if (!this.isBusy() && !this.uncertain) await this.submit({ protocolVersion: 1, action: 'login', harnessId: this.harnessId, providerId: provider.providerId, flow }); }, }))) ?? [], { name: 'Details', iconName: 'lucide:PanelRight', type: ['inRow', 'contextmenu', 'doubleClick'], actionFunc: async ({ item }) => { this.selectedAccountId = item.id; } }, { name: 'Switch', iconName: 'lucide:ArrowRight', type: ['inRow', 'contextmenu'], actionRelevancyCheckFunc: item => item.isStashed && !item.isActive && !this.isBusy(), actionFunc: async ({ item }) => this.offerSwitch(item) }, { name: 'Save', iconName: 'lucide:Save', type: ['inRow', 'contextmenu'], actionRelevancyCheckFunc: item => item.isActive && !this.isBusy() && !this.selectedHarness()?.saveUnavailableReason, actionFunc: async ({ item }) => this.mutate({ harnessId: this.harnessId, action: 'save', accountId: item.id, keepActive: true }) }, { name: 'Remove saved copy', iconName: 'lucide:Trash2', type: ['contextmenu'], actionRelevancyCheckFunc: item => item.isStashed && !this.isBusy(), actionFunc: async ({ item }) => { this.confirmation = { text: `Remove the saved copy of ${item.label}? The active login is preserved.`, mutation: { harnessId: this.harnessId, action: 'remove', accountId: item.id } }; } }, ]; } public render(): plugins.deesElement.TemplateResult { const harness = this.selectedHarness(); const selected = harness?.accounts.find(account => account.id === this.selectedAccountId); const selectedWindows = selected?.status.summary?.usageWindows ?? []; const selectedHeadline = headlineUsageWindow(selectedWindows); const prompt = this.operation?.state === 'pending' ? this.operation.prompt : undefined; const url = prompt?.flow === 'device' ? prompt.verificationUrl : prompt?.flow === 'browser' ? prompt.authUrl : undefined; const safeUrl = url && URL.canParse(url) && new URL(url).protocol === 'https:' ? url : undefined; return plugins.deesElement.html`
${(this.accounts?.harnesses ?? []).map(item => plugins.deesElement.html` { this.harnessId = item.id; this.selectedAccountId = ''; this.confirmation = undefined; }}>`)} ${!this.accounts ? plugins.deesElement.html` this.refresh()}>` : ''}
${this.accounts ? plugins.deesElement.html`
Updated ${new Date(this.accounts.generatedAt).toLocaleString()}${this.accounts.complete ? '' : ' · Some information is unavailable'}
` : ''} ${harness?.saveUnavailableReason ? plugins.deesElement.html`
${harness.saveUnavailableReason}
` : ''} ${harness?.problems.length ? plugins.deesElement.html`
    ${harness.problems.map(problem => plugins.deesElement.html`
  • ${problem}
  • `)}
` : ''} ${this.isBusy() ? plugins.deesElement.html`
${prompt ? 'Complete the provider sign-in.' : this.busySentence()}
` : ''} ${prompt ? plugins.deesElement.html`
${safeUrl ? plugins.deesElement.html`Open provider sign-in` : ''} ${prompt.flow === 'device' ? plugins.deesElement.html`Enter this code: ${prompt.userCode}` : ''} this.submit({ protocolVersion: 1, action: 'cancel', operationId: this.operation!.id })}>
` : ''} ${this.error ? plugins.deesElement.html`` : ''} ${this.error && this.operation?.state === 'pending' ? plugins.deesElement.html`
this.submit({ protocolVersion: 1, action: 'get', operationId: this.operation!.id })}> this.recoverAccounts()}>
` : ''} ${this.notice.length ? plugins.deesElement.html`
    ${this.notice.map(line => plugins.deesElement.html`
  • ${line}
  • `)}
` : ''} ${this.waitingRequest ? plugins.deesElement.html`
this.mutate(this.waitingRequest!.coordination.mutation, true)}> { this.waitingRequest = undefined; this.notice = []; }}>
` : ''} ${this.confirmation ? plugins.deesElement.html`
${this.confirmation.text}
this.mutate(this.confirmation!.mutation)}> { this.confirmation = undefined; }}>
` : ''} ${selected ? plugins.deesElement.html`

${selected.label}

${selectedWindows.map(window => { const alert = usageWindowAlert(window); const reading = [`${window.usedPercent}% used`, `resets in ${remaining(window.resetAt)}`, alert, window === selectedHeadline ? 'headline' : undefined]; return plugins.deesElement.html`
${window.label}
${reading.filter(Boolean).join(' · ')}
`; })} ${selected.status.summary?.resets ? plugins.deesElement.html`
Available resets
${selected.status.summary.resets.available}
` : ''} ${selected.status.facts.filter(fact => !statusShowsFact(selected.status, fact)).map(fact => plugins.deesElement.html`
${fact.section ? `${fact.section} · ` : ''}${fact.label}
${fact.value}
`)}
${selected.status.problems.length ? plugins.deesElement.html`
    ${selected.status.problems.map(problem => plugins.deesElement.html`
  • ${problem}
  • `)}
` : ''}
` : ''} ${harness && !this.capabilities.find(item => item.harnessId === harness.id)?.providers.length ? plugins.deesElement.html`
${harness.loginHint}
` : ''}
`; } }