From 33300cdc7f649a2a9ccfdfe8303517ef44ff3152 Mon Sep 17 00:00:00 2001 From: nousbot-eng Date: Sat, 18 Jul 2026 05:06:55 -0400 Subject: [PATCH] fmt(js): `npm run fix` on merge (#66834) Co-authored-by: github-actions[bot] --- ui-tui/src/__tests__/billingStepUp.test.tsx | 24 ++- .../__tests__/subscriptionOverlay.test.tsx | 185 ++++++++++++++---- ui-tui/src/__tests__/topupCommand.test.ts | 37 ++-- .../__tests__/turnControllerNotice.test.ts | 7 +- ui-tui/src/__tests__/usageCommand.test.ts | 18 +- ui-tui/src/app/interfaces.ts | 22 ++- ui-tui/src/app/overlayStore.ts | 28 +-- ui-tui/src/app/slash/commands/session.ts | 4 +- ui-tui/src/app/slash/commands/subscription.ts | 5 +- ui-tui/src/app/slash/commands/topup.ts | 17 +- ui-tui/src/app/useInputHandlers.ts | 3 +- ui-tui/src/components/appOverlays.tsx | 4 +- ui-tui/src/components/billingOverlay.tsx | 25 ++- ui-tui/src/components/overlayPrimitives.tsx | 6 +- ui-tui/src/components/subscriptionOverlay.tsx | 141 +++++++++---- ui-tui/src/gatewayTypes.ts | 30 +-- 16 files changed, 403 insertions(+), 153 deletions(-) diff --git a/ui-tui/src/__tests__/billingStepUp.test.tsx b/ui-tui/src/__tests__/billingStepUp.test.tsx index f59b1ef2ac88..8c4ee1542dfb 100644 --- a/ui-tui/src/__tests__/billingStepUp.test.tsx +++ b/ui-tui/src/__tests__/billingStepUp.test.tsx @@ -107,11 +107,11 @@ describe('BillingOverlay — step-up screen (Enable terminal billing)', () => { describe('BillingOverlay — overview (reordered, dollars)', () => { it('leads with balance in the title, Add funds first, no "credits"', () => { const out = render(overlay('overview')) - expect(out).toContain('Top up · balance $12.00') // balance in the title - expect(out).toContain('Add funds') // buy action, renamed + expect(out).toContain('Top up · balance $12.00') // balance in the title + expect(out).toContain('Add funds') // buy action, renamed expect(out).toContain('Auto-reload') expect(out).toContain('Manage on portal') - expect(out.toLowerCase()).not.toContain('credits') // dollars only + expect(out.toLowerCase()).not.toContain('credits') // dollars only // No standalone "Enable terminal billing" item — discovered at pay time. expect(out).not.toContain('Enable terminal billing') }) @@ -126,8 +126,22 @@ describe('BillingOverlay — overview (reordered, dollars)', () => { status: 'healthy', plan_name: 'Plus', has_topup: true, - plan_bar: { kind: 'plan', remaining_display: '$14.00', total_display: '$20.00', spent_display: '$6.00', pct_used: 30, fill_fraction: 0.7 }, - topup_bar: { kind: 'topup', remaining_display: '$12.00', total_display: '$12.00', spent_display: '$0.00', pct_used: null, fill_fraction: 1 } + plan_bar: { + kind: 'plan', + remaining_display: '$14.00', + total_display: '$20.00', + spent_display: '$6.00', + pct_used: 30, + fill_fraction: 0.7 + }, + topup_bar: { + kind: 'topup', + remaining_display: '$12.00', + total_display: '$12.00', + spent_display: '$0.00', + pct_used: null, + fill_fraction: 1 + } } } } diff --git a/ui-tui/src/__tests__/subscriptionOverlay.test.tsx b/ui-tui/src/__tests__/subscriptionOverlay.test.tsx index 22cfec4e77e9..eaf22e847f68 100644 --- a/ui-tui/src/__tests__/subscriptionOverlay.test.tsx +++ b/ui-tui/src/__tests__/subscriptionOverlay.test.tsx @@ -30,7 +30,10 @@ import { DEFAULT_THEME } from '../theme.js' const t = DEFAULT_THEME /** Mount a SubscriptionOverlay via renderSync + PassThrough. */ -function mount(overlay: SubscriptionOverlayState, onPatch: (next: Partial) => void = () => {}) { +function mount( + overlay: SubscriptionOverlayState, + onPatch: (next: Partial) => void = () => {} +) { const stdout = new PassThrough() const stdin = new PassThrough() const stderr = new PassThrough() @@ -46,15 +49,13 @@ function mount(overlay: SubscriptionOverlayState, onPatch: (next: Partial {}, onPatch, overlay, t }) - const instance = renderSync( - element, - { - patchConsole: false, - stderr: stderr as NodeJS.WriteStream, - stdin: stdin as NodeJS.ReadStream, - stdout: stdout as NodeJS.WriteStream - } - ) + + const instance = renderSync(element, { + patchConsole: false, + stderr: stderr as NodeJS.WriteStream, + stdin: stdin as NodeJS.ReadStream, + stdout: stdout as NodeJS.WriteStream + }) return { cleanup: () => { @@ -76,9 +77,33 @@ function render(overlay: SubscriptionOverlayState): string { } const TIERS = [ - { tier_id: 'free', name: 'Free', tier_order: 0, dollars_per_month_display: '$0', monthly_credits: '0', is_current: false, is_enabled: true }, - { tier_id: 'plus', name: 'Plus', tier_order: 1, dollars_per_month_display: '$20', monthly_credits: '1000', is_current: true, is_enabled: true }, - { tier_id: 'ultra', name: 'Ultra', tier_order: 2, dollars_per_month_display: '$40', monthly_credits: '3000', is_current: false, is_enabled: true } + { + tier_id: 'free', + name: 'Free', + tier_order: 0, + dollars_per_month_display: '$0', + monthly_credits: '0', + is_current: false, + is_enabled: true + }, + { + tier_id: 'plus', + name: 'Plus', + tier_order: 1, + dollars_per_month_display: '$20', + monthly_credits: '1000', + is_current: true, + is_enabled: true + }, + { + tier_id: 'ultra', + name: 'Ultra', + tier_order: 2, + dollars_per_month_display: '$40', + monthly_credits: '3000', + is_current: false, + is_enabled: true + } ] const state = (overrides: Partial = {}): SubscriptionStateResponse => ({ @@ -128,7 +153,15 @@ describe('SubscriptionOverlay — overview', () => { const out = render( overlay( state({ - current: { tier_id: 'pro', tier_name: 'Pro', monthly_credits: '1000', credits_remaining: '700', cycle_ends_at: '2026-07-01', pending_downgrade_tier_name: null, pending_downgrade_at: null }, + current: { + tier_id: 'pro', + tier_name: 'Pro', + monthly_credits: '1000', + credits_remaining: '700', + cycle_ends_at: '2026-07-01', + pending_downgrade_tier_name: null, + pending_downgrade_at: null + }, usage: { available: true, status: 'healthy', @@ -136,8 +169,22 @@ describe('SubscriptionOverlay — overview', () => { renews_display: 'Jul 1, 2026', total_spendable_display: '$26.00', has_topup: true, - plan_bar: { kind: 'plan', remaining_display: '$14.00', total_display: '$20.00', spent_display: '$6.00', pct_used: 30, fill_fraction: 0.7 }, - topup_bar: { kind: 'topup', remaining_display: '$12.00', total_display: '$12.00', spent_display: '$0.00', pct_used: null, fill_fraction: 1 } + plan_bar: { + kind: 'plan', + remaining_display: '$14.00', + total_display: '$20.00', + spent_display: '$6.00', + pct_used: 30, + fill_fraction: 0.7 + }, + topup_bar: { + kind: 'topup', + remaining_display: '$12.00', + total_display: '$12.00', + spent_display: '$0.00', + pct_used: null, + fill_fraction: 1 + } } }) ) @@ -155,8 +202,29 @@ describe('SubscriptionOverlay — overview', () => { const out = render( overlay( state({ - current: { tier_id: 'pro', tier_name: 'Pro', monthly_credits: '1000', credits_remaining: '170', cycle_ends_at: '2026-07-01', pending_downgrade_tier_name: null, pending_downgrade_at: null }, - usage: { available: true, status: 'low', plan_name: 'Pro', total_spendable_display: '$3.40', plan_bar: { kind: 'plan', remaining_display: '$3.40', total_display: '$20.00', spent_display: '$16.60', pct_used: 83, fill_fraction: 0.17 } } + current: { + tier_id: 'pro', + tier_name: 'Pro', + monthly_credits: '1000', + credits_remaining: '170', + cycle_ends_at: '2026-07-01', + pending_downgrade_tier_name: null, + pending_downgrade_at: null + }, + usage: { + available: true, + status: 'low', + plan_name: 'Pro', + total_spendable_display: '$3.40', + plan_bar: { + kind: 'plan', + remaining_display: '$3.40', + total_display: '$20.00', + spent_display: '$16.60', + pct_used: 83, + fill_fraction: 0.17 + } + } }) ) ) @@ -172,7 +240,15 @@ describe('SubscriptionOverlay — overview', () => { is_admin: false, can_change_plan: false, role: 'MEMBER', - current: { tier_id: 'pro', tier_name: 'Pro', monthly_credits: '1000', credits_remaining: '500', cycle_ends_at: '2026-07-01', pending_downgrade_tier_name: null, pending_downgrade_at: null }, + current: { + tier_id: 'pro', + tier_name: 'Pro', + monthly_credits: '1000', + credits_remaining: '500', + cycle_ends_at: '2026-07-01', + pending_downgrade_tier_name: null, + pending_downgrade_at: null + }, usage: { available: true, status: 'healthy', plan_name: 'Pro' } }) ) @@ -186,7 +262,16 @@ describe('SubscriptionOverlay — overview', () => { const out = render( overlay( state({ - current: { tier_id: 'pro', tier_name: 'Pro', monthly_credits: '1000', credits_remaining: '500', cycle_ends_at: '2026-07-01', pending_downgrade_tier_name: 'Free', pending_downgrade_at: '2026-07-15', pending_downgrade_display: 'Jul 15, 2026' }, + current: { + tier_id: 'pro', + tier_name: 'Pro', + monthly_credits: '1000', + credits_remaining: '500', + cycle_ends_at: '2026-07-01', + pending_downgrade_tier_name: 'Free', + pending_downgrade_at: '2026-07-15', + pending_downgrade_display: 'Jul 15, 2026' + }, usage: { available: true, status: 'healthy', plan_name: 'Pro' } }) ) @@ -296,7 +381,13 @@ describe('SubscriptionOverlay — confirm', () => { pending: { kind: 'upgrade', targetTierId: 'ultra', - preview: { ok: true, effect: 'charge_now', target_tier_name: 'Ultra', amount_due_now_cents: 1234, monthly_credits_delta: '2000' } + preview: { + ok: true, + effect: 'charge_now', + target_tier_name: 'Ultra', + amount_due_now_cents: 1234, + monthly_credits_delta: '2000' + } } }) ) @@ -311,7 +402,13 @@ describe('SubscriptionOverlay — confirm', () => { pending: { kind: 'tier_change', targetTierId: 'plus', - preview: { ok: true, effect: 'scheduled', target_tier_name: 'Plus', effective_at: '2026-08-01T00:00:00Z', amount_due_now_cents: null } + preview: { + ok: true, + effect: 'scheduled', + target_tier_name: 'Plus', + effective_at: '2026-08-01T00:00:00Z', + amount_due_now_cents: null + } } }) ) @@ -322,7 +419,9 @@ describe('SubscriptionOverlay — confirm', () => { }) it('cancellation: shows cancel-at-period-end copy', () => { - const out = render(at('confirm', subscriber(), { pending: { kind: 'cancellation', targetTierId: null, preview: null } })) + const out = render( + at('confirm', subscriber(), { pending: { kind: 'cancellation', targetTierId: null, preview: null } }) + ) expect(out).toContain('Confirm cancellation') expect(out).toContain('will not renew') @@ -331,7 +430,11 @@ describe('SubscriptionOverlay — confirm', () => { it('blocked: shows the reason + Manage on portal', () => { const out = render( at('confirm', subscriber(), { - pending: { kind: 'tier_change', targetTierId: 'ultra', preview: { ok: true, effect: 'blocked', reason: 'Retract the cancellation before upgrading.' } } + pending: { + kind: 'tier_change', + targetTierId: 'ultra', + preview: { ok: true, effect: 'blocked', reason: 'Retract the cancellation before upgrading.' } + } }) ) @@ -352,7 +455,11 @@ describe('SubscriptionOverlay — result', () => { it('error with recovery: shows the message + Open the portal', () => { const out = render( at('result', subscriber(), { - result: { ok: false, message: 'This upgrade needs extra verification (3DS).', recoveryUrl: 'https://portal.example/x' } + result: { + ok: false, + message: 'This upgrade needs extra verification (3DS).', + recoveryUrl: 'https://portal.example/x' + } }) ) @@ -366,6 +473,7 @@ describe('SubscriptionOverlay — upgrade response mapping', () => { const applyUpgrade = async (response: unknown) => { const onPatch = vi.fn() const upgrade = vi.fn(() => Promise.resolve(response)) + const mounted = mount( at('confirm', subscriber(), { ctx: { ...ctx, upgrade } as SubscriptionOverlayState['ctx'], @@ -433,17 +541,22 @@ describe('SubscriptionOverlay — upgrade response mapping', () => { vi.useFakeTimers() try { - const refreshState = vi.fn(() => Promise.resolve(subscriber({ - current: { - tier_id: 'ultra', - tier_name: 'Ultra', - monthly_credits: '3000', - credits_remaining: '3000', - cycle_ends_at: '2026-08-01', - pending_downgrade_tier_name: null, - pending_downgrade_at: null - } - }))) + const refreshState = vi.fn(() => + Promise.resolve( + subscriber({ + current: { + tier_id: 'ultra', + tier_name: 'Ultra', + monthly_credits: '3000', + credits_remaining: '3000', + cycle_ends_at: '2026-08-01', + pending_downgrade_tier_name: null, + pending_downgrade_at: null + } + }) + ) + ) + const result = { message: 'Upgraded to Ultra.', ok: true, pendingTierId: 'ultra' } const mounted = mount(at('result', subscriber(), { ctx: { ...ctx, refreshState }, result })) diff --git a/ui-tui/src/__tests__/topupCommand.test.ts b/ui-tui/src/__tests__/topupCommand.test.ts index 63b967b26d1f..636aba932fcd 100644 --- a/ui-tui/src/__tests__/topupCommand.test.ts +++ b/ui-tui/src/__tests__/topupCommand.test.ts @@ -313,19 +313,28 @@ describe('/billing slash command (overlay-driven)', () => { it.each([ ['admin', 'An admin turned off terminal billing for this terminal'], ['self', 'You turned off terminal billing for this terminal'] - ])('ctx.charge remote_spending_revoked (%s) → clears the overlay (no zombie button) + actor copy', async (actor, copy) => { - const { run, sys } = buildCtx({ - 'billing.state': ownerState(), - 'billing.charge': { ok: false, error: 'remote_spending_revoked', actor, recovery: 'reconnect', idempotency_key: 'k' } - }) + ])( + 'ctx.charge remote_spending_revoked (%s) → clears the overlay (no zombie button) + actor copy', + async (actor, copy) => { + const { run, sys } = buildCtx({ + 'billing.state': ownerState(), + 'billing.charge': { + ok: false, + error: 'remote_spending_revoked', + actor, + recovery: 'reconnect', + idempotency_key: 'k' + } + }) - await run('') - getOverlayState().billing!.ctx.charge('100') - await Promise.resolve() - await Promise.resolve() - expect(printed(sys)).toContain(copy) - expect(getOverlayState().billing).toBeNull() - }) + await run('') + getOverlayState().billing!.ctx.charge('100') + await Promise.resolve() + await Promise.resolve() + expect(printed(sys)).toContain(copy) + expect(getOverlayState().billing).toBeNull() + } + ) it('ctx.charge session_revoked → clears overlay + re-login (not reconnect) copy', async () => { const { run, sys } = buildCtx({ @@ -353,9 +362,7 @@ describe('/billing slash command (overlay-driven)', () => { return Promise.reject(new Error('socket closed')) } - return Promise.resolve( - method === 'billing.charge' ? { ok: true, charge_id: 'ch_1', idempotency_key: 'k' } : null - ) + return Promise.resolve(method === 'billing.charge' ? { ok: true, charge_id: 'ch_1', idempotency_key: 'k' } : null) }) await getOverlayState().billing!.ctx.charge('100') await vi.waitFor(() => expect(printed(sys)).toContain('outcome is unconfirmed')) diff --git a/ui-tui/src/__tests__/turnControllerNotice.test.ts b/ui-tui/src/__tests__/turnControllerNotice.test.ts index c3531cb8db58..0e4a7566d32e 100644 --- a/ui-tui/src/__tests__/turnControllerNotice.test.ts +++ b/ui-tui/src/__tests__/turnControllerNotice.test.ts @@ -35,7 +35,12 @@ describe('turnController.startMessage — flash-and-yield notices clear on next it('leaves a sticky credits.depleted notice across a new turn', () => { patchUiState({ - notice: { key: 'credits.depleted', kind: 'sticky', level: 'error', text: '✕ Credit access paused · run /topup to top up' } + notice: { + key: 'credits.depleted', + kind: 'sticky', + level: 'error', + text: '✕ Credit access paused · run /topup to top up' + } }) turnController.startMessage() expect(getUiState().notice?.key).toBe('credits.depleted') diff --git a/ui-tui/src/__tests__/usageCommand.test.ts b/ui-tui/src/__tests__/usageCommand.test.ts index 312fc28d9c48..6634c05441b4 100644 --- a/ui-tui/src/__tests__/usageCommand.test.ts +++ b/ui-tui/src/__tests__/usageCommand.test.ts @@ -79,8 +79,22 @@ describe('/usage slash command', () => { renews_display: 'Jul 1, 2026', total_spendable_display: '$26.00', has_topup: true, - plan_bar: { kind: 'plan', remaining_display: '$14.00', total_display: '$20.00', spent_display: '$6.00', pct_used: 30, fill_fraction: 0.7 }, - topup_bar: { kind: 'topup', remaining_display: '$12.00', total_display: '$12.00', spent_display: '$0.00', pct_used: null, fill_fraction: 1 } + plan_bar: { + kind: 'plan', + remaining_display: '$14.00', + total_display: '$20.00', + spent_display: '$6.00', + pct_used: 30, + fill_fraction: 0.7 + }, + topup_bar: { + kind: 'topup', + remaining_display: '$12.00', + total_display: '$12.00', + spent_display: '$0.00', + pct_used: null, + fill_fraction: 1 + } } }) }) diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts index 655f50d11fe0..c29d722cf452 100644 --- a/ui-tui/src/app/interfaces.ts +++ b/ui-tui/src/app/interfaces.ts @@ -3,7 +3,16 @@ import type { MutableRefObject, ReactNode, RefObject, SetStateAction } from 'rea import type { PasteEvent } from '../components/textInput.js' import type { GatewayClient } from '../gatewayClient.js' -import type { BillingCardInfo, BillingMutationResponse, BillingStateResponse, ImageAttachResponse, SessionCloseResponse, SubscriptionPreviewResponse, SubscriptionStateResponse, SubscriptionUpgradeResponse } from '../gatewayTypes.js' +import type { + BillingCardInfo, + BillingMutationResponse, + BillingStateResponse, + ImageAttachResponse, + SessionCloseResponse, + SubscriptionPreviewResponse, + SubscriptionStateResponse, + SubscriptionUpgradeResponse +} from '../gatewayTypes.js' import type { ParsedVoiceRecordKey } from '../lib/platform.js' import type { RpcResult } from '../lib/rpc.js' import type { Theme } from '../theme.js' @@ -96,9 +105,9 @@ export type BillingScreen = 'autoreload' | 'buy' | 'confirm' | 'limit' | 'overvi /** Outcome of a charge attempt — lets the overlay route without tearing down. */ export type BillingChargeOutcome = - | 'submitted' // 202 accepted; settlement is reported via transcript lines - | 'needs_remote_spending' // insufficient_scope → route to the stepup screen - | 'error' // any other failure (already surfaced via sys) + | 'submitted' // 202 accepted; settlement is reported via transcript lines + | 'needs_remote_spending' // insufficient_scope → route to the stepup screen + | 'error' // any other failure (already surfaced via sys) /** * The functions the overlay needs to talk to the gateway and emit @@ -173,10 +182,7 @@ export type SubscriptionScreen = 'confirm' | 'overview' | 'picker' | 'result' | // The action held while the stepup screen grants terminal billing, replayed on // grant: re-preview a tier, re-apply the confirmed pending change, or re-resume. -export type SubscriptionStepUpRetry = - | { kind: 'apply' } - | { kind: 'preview'; tierId: string } - | { kind: 'resume' } +export type SubscriptionStepUpRetry = { kind: 'apply' } | { kind: 'preview'; tierId: string } | { kind: 'resume' } /** Outcome of a terminal-billing step-up: granted, plus the typed denial (for copy). */ export interface StepUpResult { diff --git a/ui-tui/src/app/overlayStore.ts b/ui-tui/src/app/overlayStore.ts index 9273a138e95b..bff021f31530 100644 --- a/ui-tui/src/app/overlayStore.ts +++ b/ui-tui/src/app/overlayStore.ts @@ -44,20 +44,20 @@ export const $isBlocked = computed( }) => Boolean( agents || - approval || - billing || - clarify || - confirm || - journey || - modelPicker || - pager || - petPicker || - pluginsHub || - secret || - sessions || - skillsHub || - subscription || - sudo + approval || + billing || + clarify || + confirm || + journey || + modelPicker || + pager || + petPicker || + pluginsHub || + secret || + sessions || + skillsHub || + subscription || + sudo ) ) diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index 03e8cd1d9870..47922c5f7add 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -598,7 +598,9 @@ export const sessionCommands: SlashCommand[] = [ const plan = usageModel.plan_name ?? (usageModel.status === 'free' ? 'Free' : null) if (plan) { - sections.push({ text: `Plan: ${plan}${usageModel.renews_display ? ` · renews ${usageModel.renews_display}` : ''}` }) + sections.push({ + text: `Plan: ${plan}${usageModel.renews_display ? ` · renews ${usageModel.renews_display}` : ''}` + }) } if (barLines.length) { diff --git a/ui-tui/src/app/slash/commands/subscription.ts b/ui-tui/src/app/slash/commands/subscription.ts index 98c8bdb0d2f3..d9c12fe20f64 100644 --- a/ui-tui/src/app/slash/commands/subscription.ts +++ b/ui-tui/src/app/slash/commands/subscription.ts @@ -106,7 +106,10 @@ const buildSubscriptionCtx = ( // Carry the typed denial (session_revoked / remote_spending_revoked / // rate_limited / …) so the stepup screen shows the right recovery. .then(r => ({ error: r?.error, granted: !!(r && r.ok && r.granted), message: r?.message })) - .catch(() => ({ granted: false, message: 'Could not reach the billing service — check your connection, then retry.' })), + .catch(() => ({ + granted: false, + message: 'Could not reach the billing service — check your connection, then retry.' + })), resume: () => ctx.gateway .rpc('subscription.resume', {}) diff --git a/ui-tui/src/app/slash/commands/topup.ts b/ui-tui/src/app/slash/commands/topup.ts index ae0b7b9f3130..679143a17746 100644 --- a/ui-tui/src/app/slash/commands/topup.ts +++ b/ui-tui/src/app/slash/commands/topup.ts @@ -13,7 +13,8 @@ import type { SlashCommand, SlashRunCtx } from '../types.js' // Poll cadence (plan §5, frozen): 2s interval, 5-minute cap. const POLL_INTERVAL_MS = 2000 const POLL_CAP_MS = 5 * 60 * 1000 -const UNCONFIRMED_CHARGE_MESSAGE = '🟡 Your last charge’s outcome is unconfirmed — check your balance/history before retrying.' +const UNCONFIRMED_CHARGE_MESSAGE = + '🟡 Your last charge’s outcome is unconfirmed — check your balance/history before retrying.' type Sys = (text: string) => void @@ -74,7 +75,9 @@ const renderBillingError = ( break case 'role_required': - sys('Adding funds needs someone with billing permissions (owner, admin, or finance admin), or manage this on the portal.') + sys( + 'Adding funds needs someone with billing permissions (owner, admin, or finance admin), or manage this on the portal.' + ) break @@ -94,7 +97,9 @@ const renderBillingError = ( break case 'auto_top_up_disabled_failures': - sys('Auto-reload was turned off after repeated charge failures. Fix the card issue, then re-enable it from /topup → Auto-reload.') + sys( + 'Auto-reload was turned off after repeated charge failures. Fix the card issue, then re-enable it from /topup → Auto-reload.' + ) break @@ -199,7 +204,11 @@ const pollCharge = (sys: Sys, ctx: SlashRunCtx, chargeId: string, portalUrl?: st ctx.guarded(r => { if (!r.ok) { // 429/503 while polling = retry-after, NOT a failure. Back off + continue. - if (r.error === 'rate_limited' || r.error === 'temporarily_unavailable' || r.error === 'stripe_unavailable') { + if ( + r.error === 'rate_limited' || + r.error === 'temporarily_unavailable' || + r.error === 'stripe_unavailable' + ) { if (timedOut()) { return } diff --git a/ui-tui/src/app/useInputHandlers.ts b/ui-tui/src/app/useInputHandlers.ts index e65a2e3ed8e8..7ec10c420dbf 100644 --- a/ui-tui/src/app/useInputHandlers.ts +++ b/ui-tui/src/app/useInputHandlers.ts @@ -328,7 +328,8 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult { // answering felt like the prompt had locked the entire UI. Explicitly // skip the prompt-overlay early-return for scroll keys so they fall // through to the wheel / PageUp / Shift+arrow handlers below. - const promptOverlay = overlay.approval || overlay.billing || overlay.clarify || overlay.confirm || overlay.subscription + const promptOverlay = + overlay.approval || overlay.billing || overlay.clarify || overlay.confirm || overlay.subscription const fallThroughForScroll = promptOverlay && shouldFallThroughForScroll(key) if (promptOverlay && !fallThroughForScroll) { diff --git a/ui-tui/src/components/appOverlays.tsx b/ui-tui/src/components/appOverlays.tsx index b58a114f6f23..80e040e4fe9a 100644 --- a/ui-tui/src/components/appOverlays.tsx +++ b/ui-tui/src/components/appOverlays.tsx @@ -57,7 +57,9 @@ export function PromptZone({ const current = overlay.subscription const onPatch = (next: Partial) => - patchOverlayState(prev => (prev.subscription ? { ...prev, subscription: { ...prev.subscription, ...next } } : prev)) + patchOverlayState(prev => + prev.subscription ? { ...prev, subscription: { ...prev.subscription, ...next } } : prev + ) const onClose = () => patchOverlayState({ subscription: null }) diff --git a/ui-tui/src/components/billingOverlay.tsx b/ui-tui/src/components/billingOverlay.tsx index 326f159db698..984915f79075 100644 --- a/ui-tui/src/components/billingOverlay.tsx +++ b/ui-tui/src/components/billingOverlay.tsx @@ -157,7 +157,9 @@ function OverviewScreen({ ctx, onClose, onPatch, s, t }: ScreenProps) { full menu — members/billing-off get the portal note instead. */} {full && ( - {s.card ? `Card: ${s.card.display ?? s.card.masked}` : 'No saved card on file — “Add funds” walks you through adding one.'} + {s.card + ? `Card: ${s.card.display ?? s.card.masked}` + : 'No saved card on file — “Add funds” walks you through adding one.'} )} {note && ( @@ -354,14 +356,19 @@ function BuyScreen({ ctx, onPatch, s, t }: ScreenProps) { Add funds No saved card on file. - Add a card once on the portal billing page — after that you can top up right from the terminal. + + Add a card once on the portal billing page — after that you can top up right from the terminal. + {rows.map((label, i) => ( ))} {error && {error}} - {footer(checking ? 'Checking for a card…' : `↑/↓ select · 1-${rows.length} quick pick · Enter confirm · Esc back`, t)} + {footer( + checking ? 'Checking for a card…' : `↑/↓ select · 1-${rows.length} quick pick · Enter confirm · Esc back`, + t + )} ) } @@ -475,7 +482,9 @@ function ConfirmScreen({ {payLine} {/* Provenance-less payloads (older NAS) keep the generic line; when the resolver says WHY this card, payLine already carries it. */} - {s.card && !s.card.resolved_via && Your card saved on the portal will be charged.} + {s.card && !s.card.resolved_via && ( + Your card saved on the portal will be charged. + )} By confirming, you allow Nous Research to charge your card. @@ -677,9 +686,12 @@ function AutoReloadScreen({ ctx, onClose, onPatch, s, t }: ScreenProps) { const ar = s.auto_reload const enabled = Boolean(ar?.enabled) const distinctCard = ar?.card.kind === 'distinct' ? ar.card : null + const distinctCardName = distinctCard - ? [distinctCard.brand, distinctCard.last4 ? `••${distinctCard.last4}` : null].filter(Boolean).join(' ') || 'a different card' + ? [distinctCard.brand, distinctCard.last4 ? `••${distinctCard.last4}` : null].filter(Boolean).join(' ') || + 'a different card' : null + const manageCardLabel = 'Use your card on file — manage on portal' // Prefill from state (strip the $ from the *_usd raw fields if present). @@ -690,14 +702,17 @@ function AutoReloadScreen({ ctx, onClose, onPatch, s, t }: ScreenProps) { const [error, setError] = useState(null) // focusRow: 0=threshold field, 1=reloadTo field, 2=Agree, 3=Turn off (if enabled), last=Cancel const manageCardRows = distinctCard && s.portal_url ? [manageCardLabel] : [] + const actionRows = enabled ? ['Agree and turn on', 'Turn off', ...manageCardRows, 'Cancel'] : ['Agree and turn on', ...manageCardRows, 'Cancel'] + const actionColors: Record = { 'Agree and turn on': t.color.ok, 'Turn off': t.color.warn, [manageCardLabel]: t.color.accent } + const FIELD_ROWS = 2 const [row, setRow] = useState(0) diff --git a/ui-tui/src/components/overlayPrimitives.tsx b/ui-tui/src/components/overlayPrimitives.tsx index 8a3b23f83428..995a4d85c207 100644 --- a/ui-tui/src/components/overlayPrimitives.tsx +++ b/ui-tui/src/components/overlayPrimitives.tsx @@ -17,11 +17,7 @@ export interface MenuRowSpec { * the key fully handled and skip the default escape/arrow/enter/number * handling for that keypress (e.g. a screen with a text-input sub-mode). */ -export function useMenu( - rows: MenuRowSpec[], - onEscape: () => void, - onKey?: (ch: string, key: Key) => boolean -): number { +export function useMenu(rows: MenuRowSpec[], onEscape: () => void, onKey?: (ch: string, key: Key) => boolean): number { const [sel, setSel] = useState(0) useInput((ch, key) => { diff --git a/ui-tui/src/components/subscriptionOverlay.tsx b/ui-tui/src/components/subscriptionOverlay.tsx index 22443f7ec7f5..24d45ed3758c 100644 --- a/ui-tui/src/components/subscriptionOverlay.tsx +++ b/ui-tui/src/components/subscriptionOverlay.tsx @@ -9,11 +9,7 @@ import type { SubscriptionResult, SubscriptionStepUpRetry } from '../app/interfaces.js' -import type { - SubscriptionStateResponse, - SubscriptionTierOption, - SubscriptionUpgradeResponse -} from '../gatewayTypes.js' +import type { SubscriptionStateResponse, SubscriptionTierOption, SubscriptionUpgradeResponse } from '../gatewayTypes.js' import type { Theme } from '../theme.js' import { ActionRow, footer, MenuRow, type MenuRowSpec, UsageBars, useMenu } from './overlayPrimitives.js' @@ -173,7 +169,11 @@ function stepUpDenialResult(res: { error?: string; message?: string }): Subscrip } if (res.error === 'remote_spending_revoked') { - return { message: res.message || 'Terminal spending was turned off for this session — reconnect from the portal, then retry.', ok: false } + return { + message: + res.message || 'Terminal spending was turned off for this session — reconnect from the portal, then retry.', + ok: false + } } if (res.error === 'rate_limited') { @@ -182,7 +182,8 @@ function stepUpDenialResult(res: { error?: string; message?: string }): Subscrip return { message: - res.message || 'Terminal billing was not enabled — someone with billing permissions (owner, admin, or finance admin) must allow it for this org. You can also make this change on the portal.', + res.message || + 'Terminal billing was not enabled — someone with billing permissions (owner, admin, or finance admin) must allow it for this org. You can also make this change on the portal.', ok: false } } @@ -259,28 +260,46 @@ function applyPendingAndRoute( const finish = (result: SubscriptionResult) => onPatch({ result, screen: 'result' }) if (pending.kind === 'cancellation') { - return ctx.scheduleCancellation().then(r => - isScopeDenial(r) - ? toStepUp() - : finish(mutationResult(r, 'Scheduled — your plan stays active until the end of the billing period, then it cancels. Nothing changes today.')) - ) + return ctx + .scheduleCancellation() + .then(r => + isScopeDenial(r) + ? toStepUp() + : finish( + mutationResult( + r, + 'Scheduled — your plan stays active until the end of the billing period, then it cancels. Nothing changes today.' + ) + ) + ) } if (pending.kind === 'upgrade') { - return ctx.upgrade(pending.targetTierId ?? '', pending.idempotencyKey).then(r => - isScopeDenial(r) ? toStepUp() : finish(upgradeResult(r, pending.targetTierId)) - ) + return ctx + .upgrade(pending.targetTierId ?? '', pending.idempotencyKey) + .then(r => (isScopeDenial(r) ? toStepUp() : finish(upgradeResult(r, pending.targetTierId)))) } - return ctx.scheduleChange(pending.targetTierId ?? '').then(r => - isScopeDenial(r) - ? toStepUp() - : finish(mutationResult(r, 'Scheduled — your plan doesn’t change today. You keep your current plan until the end of the billing period, then it switches.')) - ) + return ctx + .scheduleChange(pending.targetTierId ?? '') + .then(r => + isScopeDenial(r) + ? toStepUp() + : finish( + mutationResult( + r, + 'Scheduled — your plan doesn’t change today. You keep your current plan until the end of the billing period, then it switches.' + ) + ) + ) } /** Resume (undo the pending change) and route: result (ok/err) or stepup (scope). */ -function resumeAndRoute(ctx: SubscriptionOverlayState['ctx'], onPatch: ScreenProps['onPatch'], allowStepUp = true): Promise { +function resumeAndRoute( + ctx: SubscriptionOverlayState['ctx'], + onPatch: ScreenProps['onPatch'], + allowStepUp = true +): Promise { return ctx.resume().then(r => { if (isScopeDenial(r)) { return allowStepUp @@ -288,7 +307,10 @@ function resumeAndRoute(ctx: SubscriptionOverlayState['ctx'], onPatch: ScreenPro : onPatch({ result: scopeStillDeniedResult, screen: 'result' }) } - return onPatch({ result: mutationResult(r, 'Your pending change was undone — you stay on your current plan.'), screen: 'result' }) + return onPatch({ + result: mutationResult(r, 'Your pending change was undone — you stay on your current plan.'), + screen: 'result' + }) }) } @@ -594,7 +616,11 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) { if (isCancellation) { primary = { color: t.color.warn, label: 'Cancel subscription', run: apply } } else if (effect === 'charge_now') { - primary = { color: t.color.ok, label: amount ? `Pay ${amount} & upgrade now` : 'Upgrade now (prorated charge)', run: apply } + primary = { + color: t.color.ok, + label: amount ? `Pay ${amount} & upgrade now` : 'Upgrade now (prorated charge)', + run: apply + } } else if (effect === 'scheduled') { primary = { color: t.color.ok, label: `Schedule change to ${targetName}`, run: apply } } else if (effect === 'blocked') { @@ -604,7 +630,12 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) { const rows: MenuRowSpec[] = primary ? [primary, { label: 'Back', run: back }] : [{ label: 'Back', run: back }] const sel = useMenu(rows, back) // Chip contrasts an immediate charge vs a period-end schedule at a glance. - const chip = effect === 'charge_now' ? { color: t.color.ok, label: 'charged now' } : effect === 'scheduled' ? { color: t.color.warn, label: 'scheduled · not today' } : null + const chip = + effect === 'charge_now' + ? { color: t.color.ok, label: 'charged now' } + : effect === 'scheduled' + ? { color: t.color.warn, label: 'scheduled · not today' } + : null return ( @@ -619,10 +650,12 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) { {isCancellation && ( <> - Cancel {s.current?.tier_name ?? 'your plan'} — it stays active until {shortDate(s.current?.cycle_ends_at)}, then - will not renew. + Cancel {s.current?.tier_name ?? 'your plan'} — it stays active until {shortDate(s.current?.cycle_ends_at)}, + then will not renew. + + + You keep your remaining credits for this period. You can resume before it ends. - You keep your remaining credits for this period. You can resume before it ends. )} @@ -636,7 +669,9 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) { Monthly credits change: {preview.monthly_credits_delta}. )} - {chargeCard ? `${chargeCard} — the card on your subscription — will be charged.` : 'The card on your subscription will be charged.'} + {chargeCard + ? `${chargeCard} — the card on your subscription — will be charged.` + : 'The card on your subscription will be charged.'} )} @@ -644,8 +679,8 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) { {effect === 'scheduled' && !isCancellation && ( <> - Change to {targetName} — takes effect {shortDate(preview?.effective_at)}. No charge now; you keep your current - plan until then. + Change to {targetName} — takes effect {shortDate(preview?.effective_at)}. No charge now; you keep your + current plan until then. {preview?.monthly_credits_delta && ( Monthly credits change: {preview.monthly_credits_delta}. @@ -658,7 +693,9 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) { )} {effect === 'blocked' && !isCancellation && ( - {preview?.reason ?? 'That change cannot be made here — manage it on the portal.'} + + {preview?.reason ?? 'That change cannot be made here — manage it on the portal.'} + )} @@ -678,6 +715,7 @@ function ResultScreen({ onClose, overlay, t }: Omit) { const result = overlay.result ?? null const recoveryUrl = result?.recoveryUrl ?? null const pendingTierId = result?.pendingTierId ?? null + const [applyState, setApplyState] = useState<'applying' | 'confirmed' | 'timed_out'>( pendingTierId ? 'applying' : 'confirmed' ) @@ -738,6 +776,7 @@ function ResultScreen({ onClose, overlay, t }: Omit) { const applying = result?.ok && applyState === 'applying' const timedOut = result?.ok && applyState === 'timed_out' + const message = timedOut ? 'Your upgrade succeeded and is still applying — refresh in a moment.' : (result?.message ?? '') @@ -751,7 +790,10 @@ function ResultScreen({ onClose, overlay, t }: Omit) { } const rows: MenuRowSpec[] = recoveryUrl - ? [{ color: t.color.accent, label: 'Open the portal to finish', run: openRecovery }, { label: 'Close', run: onClose }] + ? [ + { color: t.color.accent, label: 'Open the portal to finish', run: openRecovery }, + { label: 'Close', run: onClose } + ] : [{ label: 'Close', run: onClose }] const sel = useMenu(rows, onClose) @@ -762,7 +804,9 @@ function ResultScreen({ onClose, overlay, t }: Omit) { {applying ? 'Applying…' : timedOut ? 'Still applying' : result?.ok ? 'Done' : 'Could not complete'} {message} - {result?.ok && !applying && !timedOut && Re-run /subscription anytime to review it.} + {result?.ok && !applying && !timedOut && ( + Re-run /subscription anytime to review it. + )} {rows.map((row, i) => ( @@ -857,9 +901,15 @@ function StepUpScreen({ onPatch, overlay, t }: ScreenProps) { const rows: MenuRowSpec[] = phase === 'granted' - ? [{ color: t.color.ok, label: retry?.kind === 'apply' ? 'Continue the change' : 'Continue', run: resume }, { label: 'Cancel', run: back }] + ? [ + { color: t.color.ok, label: retry?.kind === 'apply' ? 'Continue the change' : 'Continue', run: resume }, + { label: 'Cancel', run: back } + ] : phase === 'prompt' - ? [{ color: t.color.ok, label: 'Enable terminal billing', run: enable }, { label: 'Cancel', run: back }] + ? [ + { color: t.color.ok, label: 'Enable terminal billing', run: enable }, + { label: 'Cancel', run: back } + ] : [] const sel = useMenu(rows, back) @@ -871,12 +921,18 @@ function StepUpScreen({ onPatch, overlay, t }: ScreenProps) { {phase === 'prompt' && ( <> - Changing your plan needs terminal billing enabled for this org. Enable it here, then continue. - Someone with billing permissions (owner, admin, or finance admin) approves it once in the browser. + + Changing your plan needs terminal billing enabled for this org. Enable it here, then continue. + + + Someone with billing permissions (owner, admin, or finance admin) approves it once in the browser. + )} {phase === 'waiting' && ( - Opening your browser to approve… finish there, then come back — nothing is charged until you continue. + + Opening your browser to approve… finish there, then come back — nothing is charged until you continue. + )} {phase === 'granted' && Terminal billing enabled. Continue to finish your change.} {phase === 'resuming' && Applying your change…} @@ -885,7 +941,14 @@ function StepUpScreen({ onPatch, overlay, t }: ScreenProps) { ))} - {footer(phase === 'waiting' ? 'Waiting for approval… · Esc to cancel' : phase === 'resuming' ? 'Working…' : '↑/↓ select · Enter · Esc back', t)} + {footer( + phase === 'waiting' + ? 'Waiting for approval… · Esc to cancel' + : phase === 'resuming' + ? 'Working…' + : '↑/↓ select · Enter · Esc back', + t + )} ) } diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index 312385b7c6eb..b79de83222d6 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -157,36 +157,36 @@ export interface BillingMutationResponse { export interface SubscriptionTierOption { tier_id: string name: string - tier_order: number // sorts the picker + upgrade/downgrade hint - dollars_per_month_display: string // pre-formatted ($X / $X.YY) + tier_order: number // sorts the picker + upgrade/downgrade hint + dollars_per_month_display: string // pre-formatted ($X / $X.YY) monthly_credits: string | null - is_current: boolean // the active plan: shown, not selectable - is_enabled: boolean // false = grandfathered current tier + is_current: boolean // the active plan: shown, not selectable + is_enabled: boolean // false = grandfathered current tier } export interface SubscriptionStateResponse { ok: boolean logged_in: boolean is_admin: boolean - can_change_plan: boolean // role gate (ADMIN/OWNER), from NAS + can_change_plan: boolean // role gate (ADMIN/OWNER), from NAS org_name: string | null - org_id: string | null // org.id from the NAS response + org_id: string | null // org.id from the NAS response role: string | null - context: 'personal' | 'team' // personal account vs team/org terminal + context: 'personal' | 'team' // personal account vs team/org terminal current: { - tier_id: string | null // null = free (no active sub) + tier_id: string | null // null = free (no active sub) tier_name: string | null monthly_credits: string | null credits_remaining: string | null - cycle_ends_at: string | null // ISO + cycle_ends_at: string | null // ISO pending_downgrade_tier_name: string | null pending_downgrade_at: string | null - pending_downgrade_display: string | null // formatted pending_downgrade_at + pending_downgrade_display: string | null // formatted pending_downgrade_at cancel_at_period_end: boolean // subscription scheduled to cancel at period end - cancellation_effective_at: string | null // ISO when cancellation takes effect - cancellation_effective_display: string | null // formatted cancellation_effective_at + cancellation_effective_at: string | null // ISO when cancellation takes effect + cancellation_effective_display: string | null // formatted cancellation_effective_at } | null - tiers: SubscriptionTierOption[] // selectable catalog for the in-terminal picker + tiers: SubscriptionTierOption[] // selectable catalog for the in-terminal picker portal_url: string | null error?: string | null // Shared dollar usage model (two-bar view), embedded by the gateway so the @@ -206,8 +206,8 @@ export interface SubscriptionPreviewResponse { target_tier_id?: string | null target_tier_name?: string | null monthly_credits_delta?: string | null - amount_due_now_cents?: number | null // the prorated upfront charge for an upgrade - effective_at?: string | null // ISO, when a scheduled change lands + amount_due_now_cents?: number | null // the prorated upfront charge for an upgrade + effective_at?: string | null // ISO, when a scheduled change lands // typed-error envelope (present when ok=false) error?: string message?: string