From b0da653ac827e24362efc4e5b052457c2177018d Mon Sep 17 00:00:00 2001 From: Siddharth Balyan <52913345+alt-glitch@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:20:25 +0530 Subject: [PATCH] fix(billing): rename user-facing "terminal billing" copy to Remote Spending (#68355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(billing): rename user-facing "terminal billing" copy to Remote Spending The capability was renamed Remote Spending on the portal (consent CTA: "Allow Remote Spending"; per-terminal states Granted/Stopped), but the terminal, desktop, and docs still said "terminal billing" everywhere. - Feature name: Remote Spending in titles/labels, lowercase mid-sentence. - Step-up action verb is now "allow", matching the portal consent CTA. - Kill-switch-off recovery copy points at the actual control ("a billing admin can turn it on from the portal's Hermes Agent page") instead of the dead-end "manage it on the portal". - Per-terminal revoke copy uses the portal vocabulary ("stopped"). - Wire identifiers (cli_billing_enabled, cli_billing_disabled, ...) are unchanged; copy, comments, docs, and test expectations only. * fix(billing): correct the post-step-up denial diagnosis + finish the desktop rename Adversarial review findings: (1) a repeated insufficient_scope after a successful step-up is a per-terminal authorization failure, but the copy blamed the org kill-switch and pointed at the wrong recovery control — now: "Remote Spending still isn't active for this terminal — the authorization didn't take. Retry, or make this change on the portal." (2) the desktop step-up flow started in Remote Spending vocabulary but finished in "billing management access" — renamed both end states. (3) prettier formatting on the touched files (matches the post-merge fmt bot). --- agent/billing_view.py | 2 +- .../src/app/settings/billing/errors.ts | 15 ++-- .../src/app/settings/billing/index.test.tsx | 10 +-- .../billing/use-billing-state.test.ts | 2 +- .../app/settings/billing/use-billing-state.ts | 2 +- .../src/app/settings/billing/use-step-up.ts | 4 +- apps/shared/src/billing-types.ts | 4 +- docs/billing-lifecycle.md | 17 ++-- gateway/slash_commands.py | 6 +- hermes_cli/auth.py | 2 +- hermes_cli/cli_billing_mixin.py | 86 ++++++++++--------- hermes_cli/nous_billing.py | 12 +-- tests/agent/test_billing_view.py | 2 +- tests/hermes_cli/test_billing_cli.py | 6 +- tests/hermes_cli/test_subscription_cli.py | 15 ++-- tui_gateway/server.py | 2 +- ui-tui/README.md | 6 +- ui-tui/scripts/billing-fixtures.tsx | 2 +- ui-tui/src/__tests__/billingStepUp.test.tsx | 8 +- .../__tests__/subscriptionOverlay.test.tsx | 6 +- ui-tui/src/__tests__/topupCommand.test.ts | 6 +- ui-tui/src/app/createGatewayEventHandler.ts | 2 +- ui-tui/src/app/interfaces.ts | 14 +-- ui-tui/src/app/slash/commands/topup.ts | 16 ++-- ui-tui/src/components/billingOverlay.tsx | 28 +++--- ui-tui/src/components/subscriptionOverlay.tsx | 22 ++--- ui-tui/src/gatewayTypes.ts | 2 +- website/docs/reference/slash-commands.md | 2 +- 28 files changed, 159 insertions(+), 142 deletions(-) diff --git a/agent/billing_view.py b/agent/billing_view.py index 0e9930fd3ea6..a535aee1f142 100644 --- a/agent/billing_view.py +++ b/agent/billing_view.py @@ -1,4 +1,4 @@ -"""Surface-agnostic core for the Phase 2b terminal-billing screens. +"""Surface-agnostic core for the Phase 2b Remote Spending screens. One fetch/parse per concern, consumed identically by the CLI handler (``cli.py::_show_billing``), the TUI JSON-RPC methods diff --git a/apps/desktop/src/app/settings/billing/errors.ts b/apps/desktop/src/app/settings/billing/errors.ts index f368a9e0abc1..0357bf424421 100644 --- a/apps/desktop/src/app/settings/billing/errors.ts +++ b/apps/desktop/src/app/settings/billing/errors.ts @@ -32,19 +32,19 @@ export const resolveRefusal = (refusal: BillingRefusal): BillingRefusalPresentat case 'insufficient_scope': return { action: { type: 'step_up' }, - message: 'This needs terminal billing enabled. Start a top-up to enable it, then retry.', - title: 'Terminal billing needs approval' + message: 'This needs Remote Spending allowed. Start a top-up to allow it, then retry.', + title: 'Remote Spending needs approval' } case 'remote_spending_revoked': { const who = refusal.actor === 'admin' - ? 'An admin turned off terminal billing for this terminal.' - : 'You turned off terminal billing for this terminal.' + ? 'An admin stopped remote spending for this terminal.' + : 'You stopped remote spending for this terminal.' return { action: portalAction(refusal.portalUrl), message: `${who} Reconnect from Settings → Gateway to re-authorize this device.`, - title: 'Terminal billing was turned off' + title: 'Remote spending was stopped' } } @@ -60,8 +60,9 @@ export const resolveRefusal = (refusal: BillingRefusal): BillingRefusalPresentat case 'remote_spending_disabled': return { action: portalAction(refusal.portalUrl), - message: 'Terminal billing is off for this account — an admin must enable it on the portal.', - title: 'Terminal billing is off' + message: + "Remote spending is off for this account — a billing admin can turn it on from the portal's Hermes Agent page.", + title: 'Remote spending is off' } case 'role_required': diff --git a/apps/desktop/src/app/settings/billing/index.test.tsx b/apps/desktop/src/app/settings/billing/index.test.tsx index 27b702d99d34..29b8f95e82ab 100644 --- a/apps/desktop/src/app/settings/billing/index.test.tsx +++ b/apps/desktop/src/app/settings/billing/index.test.tsx @@ -74,7 +74,9 @@ describe('BillingSettings', () => { expect(screen.getByText('Ultra · $200/mo')).toBeTruthy() expect(screen.getByText('Visa •••• 3206')).toBeTruthy() expect( - screen.getByText('Terminal billing is off for this account — an admin must enable it on the portal.') + screen.getByText( + "Remote spending is off for this account — a billing admin can turn it on from the portal's Hermes Agent page." + ) ).toBeTruthy() expect(screen.queryByRole('button', { name: '$100' })).toBeNull() expect(screen.getByText('Refill $10 when balance falls below $5')).toBeTruthy() @@ -197,10 +199,8 @@ describe('BillingSettings', () => { }) fireEvent.click(screen.getByRole('button', { name: 'Save' })) - expect(await screen.findByText('Terminal billing needs approval:')).toBeTruthy() - expect( - screen.getByText('This needs terminal billing enabled. Start a top-up to enable it, then retry.') - ).toBeTruthy() + expect(await screen.findByText('Remote Spending needs approval:')).toBeTruthy() + expect(screen.getByText('This needs Remote Spending allowed. Start a top-up to allow it, then retry.')).toBeTruthy() expect(screen.getByRole('button', { name: 'Verify to continue' })).toBeTruthy() }) diff --git a/apps/desktop/src/app/settings/billing/use-billing-state.test.ts b/apps/desktop/src/app/settings/billing/use-billing-state.test.ts index cd7c74507a98..f87d9f4fe108 100644 --- a/apps/desktop/src/app/settings/billing/use-billing-state.test.ts +++ b/apps/desktop/src/app/settings/billing/use-billing-state.test.ts @@ -65,7 +65,7 @@ describe('deriveBillingView', () => { const buyCredits = view.accountRows.find(row => row.id === 'buy_credits') expect(buyCredits?.description).toBe( - 'Terminal billing is off for this account — an admin must enable it on the portal.' + "Remote spending is off for this account — a billing admin can turn it on from the portal's Hermes Agent page." ) expect(buyCredits?.chips).toBeUndefined() expect(view.accountRows.find(row => row.id === 'auto_reload')).toMatchObject({ diff --git a/apps/desktop/src/app/settings/billing/use-billing-state.ts b/apps/desktop/src/app/settings/billing/use-billing-state.ts index 33ee75e77fb9..870af163670f 100644 --- a/apps/desktop/src/app/settings/billing/use-billing-state.ts +++ b/apps/desktop/src/app/settings/billing/use-billing-state.ts @@ -458,7 +458,7 @@ function deriveUsageRows( value: clamp01(usedFraction) } : undefined, - caption: cap.is_default_ceiling ? 'Default ceiling' : 'Monthly terminal billing spend', + caption: cap.is_default_ceiling ? 'Default ceiling' : 'Monthly remote spending', id: 'monthly_cap', title: 'Monthly spend cap', value diff --git a/apps/desktop/src/app/settings/billing/use-step-up.ts b/apps/desktop/src/app/settings/billing/use-step-up.ts index 7658dc917567..2966f602a41a 100644 --- a/apps/desktop/src/app/settings/billing/use-step-up.ts +++ b/apps/desktop/src/app/settings/billing/use-step-up.ts @@ -121,7 +121,7 @@ export function useStepUpFlow() { if (!result.data.granted) { setMessage({ kind: 'error', - text: 'Verification finished without granting billing management access.', + text: 'Verification finished without allowing Remote Spending for this terminal.', title: 'Verification was not approved' }) @@ -134,7 +134,7 @@ export function useStepUpFlow() { ]) setMessage({ kind: 'success', - text: 'Billing management access was verified.', + text: 'Remote Spending is allowed for this terminal.', title: 'Verification complete' }) }, [api, gateway, queryClient, unsubscribe]) diff --git a/apps/shared/src/billing-types.ts b/apps/shared/src/billing-types.ts index d06e24bcba50..33004746c81b 100644 --- a/apps/shared/src/billing-types.ts +++ b/apps/shared/src/billing-types.ts @@ -1,12 +1,12 @@ /** - * Shared terminal-billing wire contracts. + * Shared Remote Spending wire contracts. * * These shapes round-trip between the Python tui_gateway and TypeScript clients * such as the TUI and desktop app. Keep rendering state, client logic, and the * gateway event union out of this runtime-free module. */ -// ── Terminal billing (Phase 2b) ────────────────────────────────────── +// ── Remote Spending (Phase 2b) ─────────────────────────────────────── /** One serialized usage bar (mirrors server `_serialize_usage_bar`). */ export interface UsageBarData { diff --git a/docs/billing-lifecycle.md b/docs/billing-lifecycle.md index 53151684214b..8562c90e8368 100644 --- a/docs/billing-lifecycle.md +++ b/docs/billing-lifecycle.md @@ -30,7 +30,7 @@ Source: `ui-tui/src/components/billingOverlay.tsx` (`OverviewScreen`, | `monthly_cap` present, `limit_usd != null` | `{spent_display} of {limit_display} used this month` (+ ` (default ceiling)` iff `is_default_ceiling`). | | `monthly_cap` absent or `limit_usd == null` | `No monthly cap visible (managed on the portal).` | | Role without billing capability (`!is_admin`, menu collapses) | Note: `Billing actions need someone with billing permissions (owner, admin, or finance admin).` Menu collapses to `Manage on portal` / `Cancel`. | -| Org kill-switch off (`is_admin` but `!cli_billing_enabled`) | Note: `Terminal billing is off for this org — manage it on the portal.` Same collapsed menu. | +| Org kill-switch off (`is_admin` but `!cli_billing_enabled`) | Note: `Remote spending is off for this org — a billing admin can turn it on from the portal's Hermes Agent page.` Same collapsed menu. | Note: `full = s.is_admin && s.cli_billing_enabled` gates the **org-level** switch, not the per-terminal `billing:manage` scope — that's discovered @@ -44,10 +44,10 @@ Source: `renderBillingError` in `ui-tui/src/app/slash/commands/topup.ts:37-149`. | `error` code | Copy | Portal URL | `retry_after` | |---|---|:-:|:-:| -| `insufficient_scope` | `This needs terminal billing enabled. Start a top-up to enable it, then retry.` | if present | — | -| `remote_spending_revoked` (CF-4) | `{An admin turned off terminal billing for this terminal. \| You turned off terminal billing for this terminal.}` (by `actor`) `Reconnect to restore — run /portal to re-authorize this terminal.` Also clears `billing` overlay state immediately (doesn't wait for token refresh). | if present | — | +| `insufficient_scope` | `This needs Remote Spending allowed. Start a top-up to allow it, then retry.` | if present | — | +| `remote_spending_revoked` (CF-4) | `{An admin stopped remote spending for this terminal. \| You stopped remote spending for this terminal.}` (by `actor`) `Reconnect to restore — run /portal to re-authorize this terminal.` Also clears `billing` overlay state immediately (doesn't wait for token refresh). | if present | — | | `session_revoked` | `Your session was logged out. Run /portal to log in again.` Also clears `billing` overlay state. | if present | — | -| `cli_billing_disabled` / `remote_spending_disabled` (dual-emitted) | `Terminal billing is off for this account — an admin must enable it on the portal.` | if present | — | +| `cli_billing_disabled` / `remote_spending_disabled` (dual-emitted) | `Remote spending is off for this account — a billing admin can turn it on from the portal's Hermes Agent page.` | if present | — | | `role_required` | `Adding funds needs someone with billing permissions (owner, admin, or finance admin), or manage this on the portal.` | if present | — | | `consent_required` | `This action needs a one-time card confirmation and consent step on the portal before it can proceed.` | if present | — | | `org_access_denied` | `This token isn't bound to an org you can manage. Sign in with the right org, or manage this on the portal.` | if present | — | @@ -136,14 +136,15 @@ just because NAS hasn't caught up yet. | `error` | Copy | |---|---| | `session_revoked` | `Your session expired — run /portal to log in again, then retry the change.` | -| `remote_spending_revoked` | `{message}` or `Terminal spending was turned off for this session — reconnect from the portal, then retry.` | +| `remote_spending_revoked` | `{message}` or `Remote spending was stopped for this terminal — reconnect from the portal, then retry.` | | `rate_limited` | `Too many attempts — wait a moment, then try again.` | -| other/unknown | `{message}` or `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.` | +| other/unknown | `{message}` or `Remote Spending was not allowed — someone with billing permissions (owner, admin, or finance admin) must approve it. You can also make this change on the portal.` | A **repeat** scope denial during a post-grant replay never re-enters the step-up screen (it's already mounted there — re-patching would freeze it); -`allowStepUp=false` instead surfaces a terminal result: `Terminal billing -still isn't enabled for this org — enable it on the portal, then retry.` +`allowStepUp=false` instead surfaces a terminal result: `Remote Spending still +isn’t active for this terminal — the authorization didn’t take. Retry, or make +this change on the portal.` ## Text-mode (CLI) parity diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index a2da10556c8e..f041f45d8b5d 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -4131,9 +4131,9 @@ class GatewaySlashCommandsMixin: """Handle /topup -- show the Nous balance and hand off to the portal. Renders the balance block + identity line + a tappable portal URL that - opens the billing page. Terminal billing is managed on the portal: the - terminal does NOT charge, confirm, or track payment here — everything - happens in the browser and the next /topup shows the new balance. The + opens the billing page. Remote spending is managed on the portal: this + messaging command does NOT charge, confirm, or track payment here — + everything happens in the browser and the next /topup shows the new balance. The tappable URL is the affordance and works on every platform (button-capable or plain text like SMS/email). Fetched off the event loop; fail-open. """ diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 13f7cf362f6b..7a6db664bc16 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -8154,7 +8154,7 @@ def step_up_nous_billing_scope( The lazy step-up (plan D-A): triggered when a billing endpoint returns ``403 insufficient_scope``. Runs a fresh device-connect with ``inference:invoke tool:invoke billing:manage`` on the scope. The user must be - an ADMIN/OWNER and tick "Allow terminal billing" in the portal for the minted + an ADMIN/OWNER and select "Allow Remote Spending" in the portal for the minted token to actually carry the scope; otherwise the server silently downscopes and this returns False. diff --git a/hermes_cli/cli_billing_mixin.py b/hermes_cli/cli_billing_mixin.py index 49b89fa77b93..bb9edd95267a 100644 --- a/hermes_cli/cli_billing_mixin.py +++ b/hermes_cli/cli_billing_mixin.py @@ -382,7 +382,7 @@ class CLIBillingMixin: if allow_stepup: self._subscription_handle_scope_required(state, retry=("preview", tier_id)) else: - print(" Terminal billing still isn't enabled for this org — enable it on the portal, then retry.") + print(" Remote Spending still isn't active for this terminal — the authorization didn't take. Retry, or make this change on the portal.") return except BillingError as exc: self._subscription_render_error(state, exc) @@ -562,12 +562,12 @@ class CLIBillingMixin: if allow_stepup: self._subscription_handle_scope_required(state, retry=action, idempotency_key=key) else: - print(" Terminal billing still isn't enabled for this org — enable it on the portal, then retry.") + print(" Remote Spending still isn't active for this terminal — the authorization didn't take. Retry, or make this change on the portal.") except BillingError as exc: self._subscription_render_error(state, exc) def _subscription_handle_scope_required(self, state, *, retry, idempotency_key=None): - """insufficient_scope → grant terminal billing (step-up), then replay `retry`. + """insufficient_scope → allow remote spending (step-up), then replay `retry`. Mirrors _billing_handle_scope_required: the classic CLI calls step_up_nous_billing_scope directly (it opens the browser + blocks), then @@ -577,34 +577,34 @@ class CLIBillingMixin: print() print(" ! One-time setup") - _cprint(f" {_d('To change your plan from the terminal, enable terminal billing once. It opens your browser to authorize, then your change picks up right here.')}") + _cprint(f" {_d('To change your plan from the terminal, allow Remote Spending once. It opens your browser to authorize, then your change picks up right here.')}") if not getattr(self, "_app", None): - print(" Run `hermes portal` and enable terminal billing, then re-run /subscription.") + print(" Run `hermes portal` and allow Remote Spending, then re-run /subscription.") return confirm_choices = [ - ("yes", "Enable terminal billing", "open your browser to authorize"), + ("yes", "Allow Remote Spending", "open your browser to authorize"), ("no", "Not now", "cancel"), ] raw = self._prompt_text_input_modal( - title="Enable terminal billing", + title="Allow Remote Spending", detail="Opens your browser to authorize this terminal.", choices=confirm_choices, ) if self._normalize_slash_confirm_choice(raw, confirm_choices) != "yes": - print(" No change made. Enable terminal billing when you're ready.") + print(" No change made. Allow Remote Spending when you're ready.") return - print(" Opening your browser to enable terminal billing…") + print(" Opening your browser to allow Remote Spending…") try: from hermes_cli.auth import step_up_nous_billing_scope granted = step_up_nous_billing_scope(open_browser=True) except Exception as exc: - print(f" Couldn't enable terminal billing: {exc}") + print(f" Couldn't allow Remote Spending: {exc}") return if not granted: - print(" Couldn't enable terminal billing — an org admin or owner has to approve it for this org.") + print(" Couldn't allow Remote Spending — an org admin or owner has to approve it for this org.") return - _cprint(f" {_DIM}✓ Terminal billing enabled.{_RST}") + _cprint(f" {_DIM}✓ Remote Spending allowed.{_RST}") # Bust the 30s token cache so the replay uses the freshly-scoped token. The # cache still holds the pre-grant unscoped token, and _request only busts it # on a 401 (not a 403 scope denial) — without this, the replay would 403 @@ -637,7 +637,7 @@ class CLIBillingMixin: msg = str(exc) or "Something went wrong." if code == "insufficient_scope": # Defensive: the flow routes scope to the step-up before reaching here. - _cprint(" 🟡 Terminal billing isn't enabled. Enable it, then retry.") + _cprint(" 🟡 Remote Spending isn't allowed yet. Allow it, then retry.") elif code in ("subscription_mutation_rejected", "preview_rejected"): _cprint(f" 🟡 {msg}") else: @@ -661,11 +661,11 @@ class CLIBillingMixin: _cprint(f" Portal: {_url}") # ------------------------------------------------------------------ - # /billing — Phase 2b terminal billing (CLI surface, all 5 screens) + # /billing — Phase 2b Remote Spending (CLI surface, all 5 screens) # ------------------------------------------------------------------ def _show_billing(self, command: str = "/topup"): - """`/topup` — terminal billing for Nous (one interactive modal). + """`/topup` — Remote Spending for Nous (one interactive modal). ZERO sub-commands: any argument is ignored. Bare ``/topup`` always opens the Overview (Screen 1), whose numbered menu is the *only* way to @@ -710,7 +710,7 @@ class CLIBillingMixin: Dollars-only (no "credits") — mirrors the TUI /topup overlay: balance leads in the title, the shared plan + top-up bars render below, then the - reordered menu (Add funds first). No scope preflight — terminal billing + reordered menu (Add funds first). No scope preflight — remote spending is discovered reactively when a charge 403s insufficient_scope. """ from cli import _cprint, _b, _d @@ -762,8 +762,11 @@ class CLIBillingMixin: self._billing_portal_hint(state) return if not state.cli_billing_enabled: - _cprint(f" {_d('Terminal billing is turned off for this org.')}") - self._billing_portal_hint(state, reason="Enable it on the portal to add funds here.") + _cprint(f" {_d('Remote spending is off for this org.')}") + self._billing_portal_hint( + state, + reason="A billing admin can turn it on from the portal's Hermes Agent page to add funds here.", + ) return # A missing card does NOT gate the whole overview — the org may already have @@ -778,7 +781,7 @@ class CLIBillingMixin: return # Add funds first, then settings, then the scopeless browser handoff. - # No "Enable terminal billing" item — that's discovered at pay time. + # No "Allow Remote Spending" item — that's discovered at pay time. # "Add funds" charges in-terminal against the org's portal-saved card # (server-held via POST /charge — no card ref leaves the client). A # missing card is NOT gated here: the buy flow reacts to the server's @@ -856,8 +859,11 @@ class CLIBillingMixin: return False if not state.cli_billing_enabled: print() - _cprint(f" 💳 {_d('Terminal billing is turned off for this org.')}") - self._billing_portal_hint(state, reason="Enable it on the portal first.") + _cprint(f" 💳 {_d('Remote spending is off for this org.')}") + self._billing_portal_hint( + state, + reason="A billing admin can turn it on from the portal's Hermes Agent page before adding funds.", + ) return False return True @@ -1033,7 +1039,7 @@ class CLIBillingMixin: try: result = post_charge(amount_usd=amount, idempotency_key=key) except BillingScopeRequired: - # In-flight reauth: enable terminal billing, then resume THIS charge + # In-flight reauth: allow remote spending, then resume THIS charge # (press-Enter beat) — no command re-run. Reuses the same idem key. self._billing_handle_scope_required(state, amount=amount, idempotency_key=key) return @@ -1129,7 +1135,7 @@ class CLIBillingMixin: print(" 💳 No card on file — top up and manage billing on the portal.") elif code in ("cli_billing_disabled", "remote_spending_disabled") or \ getattr(exc, "code", None) == "remote_spending_disabled": - print(" Terminal billing is off for this account — an admin must enable it on the portal.") + print(" Remote spending is off for this account — a billing admin can turn it on from the portal's Hermes Agent page.") elif code == "role_required": print(" Adding funds needs an org admin/owner. Ask an admin, or manage on the portal.") elif code == "idempotency_conflict": @@ -1146,8 +1152,8 @@ class CLIBillingMixin: print(f" 🟡 Too many charges right now{mins}. This isn't a payment failure.") elif code == "insufficient_scope": # Never leak the raw billing:manage scope (the post-grant replay can - # re-raise it if the grant raced) — the concept is "terminal billing". - print(" 🔴 Terminal billing needs approval — run /topup to enable it, then retry.") + # re-raise it if the grant raced) — the concept is "Remote Spending". + print(" 🔴 Remote Spending needs approval — run /topup to allow it, then retry.") else: print(f" 🔴 {exc}") if portal_url: @@ -1156,9 +1162,9 @@ class CLIBillingMixin: def _billing_handle_scope_required(self, state, *, amount=None, idempotency_key=None): """403 insufficient_scope → in-flight reauth, then resume the held charge. - The buy path discovers terminal billing isn't enabled only when the - charge 403s — there is no preflight. We enable it in-flight ("Enable - terminal billing" → browser device-flow), then on return ask the user to + The buy path discovers remote spending isn't allowed only when the + charge 403s — there is no preflight. We allow it in-flight ("Allow + Remote Spending" → browser device-flow), then on return ask the user to press Enter to resume the held ``amount`` (reusing ``idempotency_key`` so the resumed charge collapses with the original). Never leaks the raw billing:manage scope. @@ -1170,33 +1176,33 @@ class CLIBillingMixin: amount_str = format_money(amount) if amount is not None else "your top-up" print() print(" ! One-time setup") - _cprint(f" {_d(f'To charge this terminal, enable terminal billing once. It opens your browser to authorize, then {amount_str} picks up right here.')}") + _cprint(f" {_d(f'To charge from this terminal, allow Remote Spending once. It opens your browser to authorize, then {amount_str} picks up right here.')}") if not getattr(self, "_app", None): - print(" Run `hermes portal` and enable terminal billing, then retry.") + print(" Run `hermes portal` and allow Remote Spending, then retry.") return confirm_choices = [ - ("yes", "Enable terminal billing", "open your browser to authorize"), + ("yes", "Allow Remote Spending", "open your browser to authorize"), ("no", "Not now", "cancel"), ] raw = self._prompt_text_input_modal( - title="Enable terminal billing", + title="Allow Remote Spending", detail="Opens your browser to authorize this terminal.", choices=confirm_choices, ) choice = self._normalize_slash_confirm_choice(raw, confirm_choices) if choice != "yes": - print(" No charge made. Run /topup when you want to enable terminal billing.") + print(" No charge made. Run /topup when you want to allow Remote Spending.") return - print(" Opening your browser to enable terminal billing…") + print(" Opening your browser to allow Remote Spending…") try: from hermes_cli.auth import step_up_nous_billing_scope granted = step_up_nous_billing_scope(open_browser=True) except Exception as exc: - print(f" Couldn't enable terminal billing: {exc}") + print(f" Couldn't allow Remote Spending: {exc}") return if not granted: - print(" Couldn't enable terminal billing — an org admin or owner has to approve it. Your card was not charged.") + print(" Couldn't allow Remote Spending — an org admin or owner has to approve it. Your card was not charged.") return # Granted. The token now carries the scope, but the ORG kill-switch @@ -1206,7 +1212,7 @@ class CLIBillingMixin: fresh = build_billing_state() if not (fresh.logged_in and fresh.cli_billing_enabled): - print(" Terminal billing was enabled for this terminal, but it's still turned off for this org. Enable it in the portal, then run /topup again.") + print(" Remote Spending is allowed for this terminal, but it's still off for this org. A billing admin can turn it on from the portal's Hermes Agent page, then run /topup again.") self._billing_portal_hint(fresh) return @@ -1214,7 +1220,7 @@ class CLIBillingMixin: # file. If there's none, this is a half-done state: say so and route to the # portal to top up / manage billing, rather than a bare "✓ enabled" that reads as done. if fresh.card is None: - print(" ✓ Terminal billing enabled — but there's no card on file yet.") + print(" ✓ Remote Spending allowed — but there's no card on file yet.") _cprint(f" {_d('Top up and manage billing on the portal to continue.')}") self._billing_portal_hint(fresh) return @@ -1222,12 +1228,12 @@ class CLIBillingMixin: # Nothing to resume (scope-required hit outside a charge, e.g. auto-reload # config) → just tell the user it's ready. if amount is None: - print(" ✓ Terminal billing enabled. Run /topup to continue.") + print(" ✓ Remote Spending allowed. Run /topup to continue.") return # Press-Enter beat: the user is back from the browser; resume the held # purchase on an explicit confirm (reassuring, not silent). - print(" ✓ Terminal billing enabled.") + print(" ✓ Remote Spending allowed.") resume_choices = [ ("resume", f"Resume {format_money(amount)} top-up", "finish the held purchase"), ("cancel", "Cancel", "do not charge"), diff --git a/hermes_cli/nous_billing.py b/hermes_cli/nous_billing.py index a2c59a317f8d..0ad745dcb96b 100644 --- a/hermes_cli/nous_billing.py +++ b/hermes_cli/nous_billing.py @@ -1,4 +1,4 @@ -"""Nous Portal terminal-billing HTTP client (Phase 2b). +"""Nous Portal Remote Spending HTTP client (Phase 2b). Thin, fail-loud client for the four ``/api/billing/*`` endpoints the terminal billing screens drive. Companion to ``hermes_cli/nous_account.py`` (which owns @@ -90,8 +90,8 @@ class BillingScopeRequired(BillingError): """``403 insufficient_scope`` — the held token lacks ``billing:manage``. The lazy step-up trigger: catching this kicks off a fresh device-connect that - requests ``billing:manage`` (and tells the user an ADMIN must tick "Allow - terminal billing"). Also fires mid-session if the scope is stripped on refresh + requests ``billing:manage`` (and tells the user an ADMIN must select "Allow + Remote Spending"). Also fires mid-session if the scope is stripped on refresh after the user loses ADMIN. """ @@ -363,11 +363,11 @@ def _raise_for_error( ) raise BillingAuthError(message or "Authentication required.", **common) if status == 403: - # This terminal's spending was revoked (NOT the same as never having the - # scope). Disable spend UI immediately; recovery is reconnect. + # Remote spending was stopped for this terminal (NOT the same as never + # having the scope). Disable spend UI immediately; recovery is reconnect. if error == "remote_spending_revoked": raise BillingRemoteSpendingRevoked( - message or "Remote Spending was revoked for this terminal.", **common + message or "Remote spending was stopped for this terminal.", **common ) if error == "insufficient_scope": raise BillingScopeRequired( diff --git a/tests/agent/test_billing_view.py b/tests/agent/test_billing_view.py index 596570f0f574..89824fa32610 100644 --- a/tests/agent/test_billing_view.py +++ b/tests/agent/test_billing_view.py @@ -1,4 +1,4 @@ -"""Unit tests for the Phase 2b terminal-billing core + HTTP client. +"""Unit tests for the Phase 2b Remote Spending core + HTTP client. Covers: - Decimal money parsing/formatting (server emits decimal strings, not 2dp). diff --git a/tests/hermes_cli/test_billing_cli.py b/tests/hermes_cli/test_billing_cli.py index 6c1a7c3b6b08..d90dfb9c26bc 100644 --- a/tests/hermes_cli/test_billing_cli.py +++ b/tests/hermes_cli/test_billing_cli.py @@ -81,7 +81,11 @@ def test_billing_killswitch_off_blocks(cli, monkeypatch, capsys): monkeypatch.setattr(bv, "build_billing_state", lambda *a, **kw: state) cli._show_billing("/billing") out = capsys.readouterr().out - assert "turned off for this org" in out + assert "Remote spending is off for this org." in out + assert ( + "A billing admin can turn it on from the portal's Hermes Agent page " + "to add funds here." + ) in out def test_billing_limit_screen_readonly(cli, monkeypatch, capsys): diff --git a/tests/hermes_cli/test_subscription_cli.py b/tests/hermes_cli/test_subscription_cli.py index 7a5b2babb59b..deb13648a881 100644 --- a/tests/hermes_cli/test_subscription_cli.py +++ b/tests/hermes_cli/test_subscription_cli.py @@ -1,7 +1,7 @@ """Tests for the /subscription CLI change flow (cli.py::_show_subscription). Parity with the TUI overlay: the classic CLI now previews + applies a plan change -in-terminal (picker → preview → confirm → apply), grants terminal billing inline on +in-terminal (picker → preview → confirm → apply), allows remote spending inline on insufficient_scope, and leads a scheduled downgrade/cancel with a prominent banner. Interactive screens are driven by mocking `_prompt_text_input_modal`. """ @@ -158,7 +158,7 @@ def test_insufficient_scope_triggers_stepup_then_replays(cli, monkeypatch, capsy def _put(**kw): calls["n"] += 1 if calls["n"] == 1: - raise nb.BillingScopeRequired("terminal billing required") + raise nb.BillingScopeRequired("remote spending required") return {"message": "Scheduled."} monkeypatch.setattr(nb, "put_subscription_pending_change", _put) @@ -171,7 +171,7 @@ def test_insufficient_scope_triggers_stepup_then_replays(cli, monkeypatch, capsy # applied once (scope-denied), granted, replayed → applied again assert calls["n"] == 2 - assert "Terminal billing enabled" in out + assert "Remote Spending allowed" in out def test_stepup_declined_grant_does_not_replay(cli, monkeypatch, capsys): @@ -183,7 +183,7 @@ def test_stepup_declined_grant_does_not_replay(cli, monkeypatch, capsys): def _put(**kw): calls["n"] += 1 - raise nb.BillingScopeRequired("terminal billing required") + raise nb.BillingScopeRequired("remote spending required") monkeypatch.setattr(nb, "put_subscription_pending_change", _put) import hermes_cli.auth as auth @@ -194,7 +194,7 @@ def test_stepup_declined_grant_does_not_replay(cli, monkeypatch, capsys): out = capsys.readouterr().out assert calls["n"] == 1 # applied once, grant denied, no replay - assert "Couldn't enable terminal billing" in out + assert "Couldn't allow Remote Spending" in out def test_unknown_preview_effect_fails_safe(cli, monkeypatch, capsys): @@ -235,7 +235,10 @@ def test_bounded_stepup_does_not_loop_on_repeat_denial(cli, monkeypatch, capsys) out = capsys.readouterr().out assert calls["n"] == 2 # applied, granted, replayed once — no third attempt - assert "still isn't enabled" in out + assert ( + "Remote Spending still isn't active for this terminal — the authorization " + "didn't take. Retry, or make this change on the portal." + ) in out def test_upgrade_transport_failure_is_ambiguous_not_flat_failure(cli, monkeypatch, capsys): diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 847479d46bd8..49b97fd8f3e8 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -8136,7 +8136,7 @@ def _(rid, params: dict) -> dict: # =========================================================================== -# Phase 2b terminal billing RPC methods +# Phase 2b Remote Spending RPC methods # =========================================================================== # # These return STRUCTURED success envelopes (result.ok / result.error) rather diff --git a/ui-tui/README.md b/ui-tui/README.md index fe5ab7c8db1d..5a2094a3184f 100644 --- a/ui-tui/README.md +++ b/ui-tui/README.md @@ -96,7 +96,7 @@ npm run test:watch - `types.ts` — `SlashCommand` interface and `SlashRunCtx` execution context (gateway rpc, transcript helpers, session refs, stale-guard) - `registry.ts` — assembles `SLASH_COMMANDS` from all command files in registration order (core → billing → credits → session → ops → setup → debug) and exposes `findSlashCommand(name)` for case-insensitive lookup - `commands/core.ts` — general TUI commands -- `commands/billing.ts` — `/billing`: manage Nous terminal billing — buy credits, auto-reload, limits +- `commands/billing.ts` — `/billing`: manage Nous remote spending — buy credits, auto-reload, limits - `commands/credits.ts` — `/credits` - `commands/session.ts` — session and agent commands - `commands/ops.ts` — operations commands @@ -231,7 +231,7 @@ The following commands are handled directly by the TUI client. Unrecognized comm `/status`, `/title`, `/fortune`, `/redraw`, `/terminal-setup` ### Billing (`billing.ts`) -`/billing` — manage Nous terminal billing — buy credits, auto-reload, limits +`/billing` — manage Nous remote spending — buy credits, auto-reload, limits ### Session (`session.ts`) `/model`, `/sessions` (aliases `/switch`, `/session`, `/resume`), @@ -366,7 +366,7 @@ ui-tui/ types.ts SlashCommand interface and SlashRunCtx execution context registry.ts SLASH_COMMANDS assembly and findSlashCommand lookup commands/ - billing.ts /billing — manage Nous terminal billing + billing.ts /billing — manage Nous remote spending core.ts general TUI commands credits.ts /credits debug.ts /heapdump, /mem diff --git a/ui-tui/scripts/billing-fixtures.tsx b/ui-tui/scripts/billing-fixtures.tsx index f55eb725db14..7f6edac01220 100644 --- a/ui-tui/scripts/billing-fixtures.tsx +++ b/ui-tui/scripts/billing-fixtures.tsx @@ -205,7 +205,7 @@ const FIXTURES: Record = { node: billEl(billState({ is_admin: false })) }, 'topup-disabled': { - desc: '/topup overview — terminal billing OFF for org', + desc: '/topup overview — remote spending OFF for org', node: billEl(billState({ cli_billing_enabled: false })) }, 'topup-buy': { diff --git a/ui-tui/src/__tests__/billingStepUp.test.tsx b/ui-tui/src/__tests__/billingStepUp.test.tsx index 8c4ee1542dfb..4c54c9949541 100644 --- a/ui-tui/src/__tests__/billingStepUp.test.tsx +++ b/ui-tui/src/__tests__/billingStepUp.test.tsx @@ -93,11 +93,11 @@ const overlay = (screen: BillingOverlayState['screen']): BillingOverlayState => state: billState() }) -describe('BillingOverlay — step-up screen (Enable terminal billing)', () => { +describe('BillingOverlay — step-up screen (Allow Remote Spending)', () => { it('renders the one-time-setup prompt with the held amount, never leaking the raw scope', () => { const out = render(overlay('stepup')) expect(out).toContain('One-time setup') - expect(out).toContain('Enable terminal billing') + expect(out).toContain('Allow Remote Spending') expect(out).toContain('$100') // resumes the held purchase expect(out).toContain('Not now') expect(out).not.toContain('billing:manage') @@ -112,8 +112,8 @@ describe('BillingOverlay — overview (reordered, dollars)', () => { expect(out).toContain('Auto-reload') expect(out).toContain('Manage on portal') 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') + // No standalone "Allow Remote Spending" item — discovered at pay time. + expect(out).not.toContain('Allow Remote Spending') }) it('renders the two-bar dollar usage when a usage model is present', () => { diff --git a/ui-tui/src/__tests__/subscriptionOverlay.test.tsx b/ui-tui/src/__tests__/subscriptionOverlay.test.tsx index eaf22e847f68..0569f64164a9 100644 --- a/ui-tui/src/__tests__/subscriptionOverlay.test.tsx +++ b/ui-tui/src/__tests__/subscriptionOverlay.test.tsx @@ -353,11 +353,11 @@ describe('SubscriptionOverlay — overview actions', () => { }) describe('SubscriptionOverlay — step-up', () => { - it('prompts to enable terminal billing (never leaks the raw scope)', () => { + it('prompts to allow Remote Spending (never leaks the raw scope)', () => { const out = render(at('stepup', subscriber(), { stepUpRetry: { kind: 'preview', tierId: 'ultra' } })) - expect(out).toContain('Terminal billing') - expect(out).toContain('Enable terminal billing') + expect(out).toContain('Remote Spending') + expect(out).toContain('Allow Remote Spending') expect(out).not.toContain('billing:manage') }) }) diff --git a/ui-tui/src/__tests__/topupCommand.test.ts b/ui-tui/src/__tests__/topupCommand.test.ts index 636aba932fcd..809355582de1 100644 --- a/ui-tui/src/__tests__/topupCommand.test.ts +++ b/ui-tui/src/__tests__/topupCommand.test.ts @@ -311,8 +311,8 @@ describe('/billing slash command (overlay-driven)', () => { // ── CF-4: revoked-terminal UX (kill the "15-minute zombie button") ── it.each([ - ['admin', 'An admin turned off terminal billing for this terminal'], - ['self', 'You turned off terminal billing for this terminal'] + ['admin', 'An admin stopped remote spending for this terminal'], + ['self', 'You stopped remote spending for this terminal'] ])( 'ctx.charge remote_spending_revoked (%s) → clears the overlay (no zombie button) + actor copy', async (actor, copy) => { @@ -387,7 +387,7 @@ describe('/billing slash command (overlay-driven)', () => { await Promise.resolve() await Promise.resolve() const out = printed(sys) - expect(out).toContain('Terminal billing is off for this account') + expect(out).toContain('Remote spending is off for this account') // Account-wide switch is NOT a per-terminal revoke — overlay stays open. expect(getOverlayState().billing).toBeTruthy() }) diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index f1c87665f286..4906290ac4c0 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -551,7 +551,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: return } - sys('💳 Open this link to grant terminal billing access:') + sys('💳 Open this link to allow Remote Spending:') sys(url) if (code) { diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts index c29d722cf452..dddad408c8e1 100644 --- a/ui-tui/src/app/interfaces.ts +++ b/ui-tui/src/app/interfaces.ts @@ -127,7 +127,7 @@ export interface BillingOverlayCtx { */ charge: (amount: string, idempotencyKey?: string) => Promise /** - * Run the `billing.step_up` device flow (grant Remote Spending). Resolves + * Run the `billing.step_up` device flow (allow Remote Spending). Resolves * `true` when the grant lands. The browser opens via the gateway's * out-of-band `billing.step_up.verification` event — the overlay just awaits. */ @@ -176,15 +176,15 @@ export interface BillingOverlayState { // scheduled at date / no-op / blocked) + the apply action. // result — the outcome, including an SCA/decline upgrade handed off to the // portal. -// stepup — reached when a mutation returns insufficient_scope: grants the -// terminal-billing scope in place, then auto-replays the held action. +// stepup — reached when a mutation returns insufficient_scope: allows remote +// spending in place, then auto-replays the held action. export type SubscriptionScreen = 'confirm' | 'overview' | 'picker' | 'result' | 'stepup' -// 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. +// The action held while the stepup screen allows remote spending, replayed after +// approval: re-preview a tier, re-apply the confirmed pending change, or re-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). */ +/** Outcome of a remote-spending step-up: granted, plus the typed denial (for copy). */ export interface StepUpResult { granted: boolean error?: string @@ -215,7 +215,7 @@ export interface SubscriptionOverlayCtx { /** POST /upgrade: charge the card on the subscription + flip the plan now. */ upgrade: (tierId: string, idempotencyKey?: string) => Promise /** - * Run the `billing.step_up` device flow (grant terminal billing / "Remote + * Run the `billing.step_up` device flow (allow remote spending / "Remote * Spending"). Resolves `{granted}` plus the typed denial (`error`/`message`) so * the stepup screen shows the right recovery. The browser opens via the * gateway's out-of-band verification event — the stepup screen just awaits. diff --git a/ui-tui/src/app/slash/commands/topup.ts b/ui-tui/src/app/slash/commands/topup.ts index 77222e9e0812..b3ab23ccd388 100644 --- a/ui-tui/src/app/slash/commands/topup.ts +++ b/ui-tui/src/app/slash/commands/topup.ts @@ -37,9 +37,9 @@ const renderBillingError = ( switch (env.error) { case 'insufficient_scope': // Reached by non-charge mutations (e.g. auto-reload config) that need - // terminal billing enabled. The resumable step-up lives on the buy/charge + // Remote Spending allowed. The resumable step-up lives on the buy/charge // path; point the user there rather than leaking the raw scope name. - sys('This needs terminal billing enabled. Start a top-up to enable it, then retry.') + sys('This needs Remote Spending allowed. Start a top-up to allow it, then retry.') break case 'remote_spending_revoked': { @@ -49,8 +49,8 @@ const renderBillingError = ( const who = env.actor === 'admin' - ? 'An admin turned off terminal billing for this terminal.' - : 'You turned off terminal billing for this terminal.' + ? 'An admin stopped remote spending for this terminal.' + : 'You stopped remote spending for this terminal.' sys(`${who} Reconnect to restore — run /portal to re-authorize this terminal.`) @@ -67,9 +67,11 @@ const renderBillingError = ( case 'cli_billing_disabled': case 'remote_spending_disabled': - // Account-wide switch is OFF (dual-emitted error/code). An admin must flip - // it on the portal; this is NOT a per-terminal revoke. - sys('Terminal billing is off for this account — an admin must enable it on the portal.') + // Account-wide switch is OFF (dual-emitted error/code). A billing admin can + // turn it on from the portal's Hermes Agent page; this is NOT a per-terminal stop. + sys( + "Remote spending is off for this account — a billing admin can turn it on from the portal's Hermes Agent page." + ) break diff --git a/ui-tui/src/components/billingOverlay.tsx b/ui-tui/src/components/billingOverlay.tsx index 984915f79075..c5577a5c6cf1 100644 --- a/ui-tui/src/components/billingOverlay.tsx +++ b/ui-tui/src/components/billingOverlay.tsx @@ -83,7 +83,7 @@ interface ScreenProps { function OverviewScreen({ ctx, onClose, onPatch, s, t }: ScreenProps) { // Full charge menu only for an admin with the org kill-switch on; otherwise it // collapses to Manage-on-portal / Close + a one-line note. NOTE: this is the - // ORG-level gate (cli_billing_enabled), NOT the per-terminal billing scope — + // ORG-level gate (cli_billing_enabled), NOT the per-terminal remote spending scope — // that's discovered reactively at pay time (a charge 403s insufficient_scope // and the confirm screen routes into the resumable step-up). We deliberately // do NOT preflight the scope here. @@ -92,7 +92,7 @@ function OverviewScreen({ ctx, onClose, onPatch, s, t }: ScreenProps) { const note = !s.is_admin ? 'Billing actions need someone with billing permissions (owner, admin, or finance admin).' : !s.cli_billing_enabled - ? 'Terminal billing is off for this org — manage it on the portal.' + ? "Remote spending is off for this org — a billing admin can turn it on from the portal's Hermes Agent page." : null // Always show the full billing menu for an admin/billing-on org — a missing @@ -495,14 +495,14 @@ function ConfirmScreen({ ) } -// ── Screen: Step-up (resumable "Enable terminal billing") ──────────── +// ── Screen: Step-up (resumable "Allow Remote Spending") ───────────── // Reached ONLY when a charge returns insufficient_scope — there is no preflight // or scope check anywhere; the buy path discovers it reactively. The modal stays // MOUNTED through the browser device-flow: // prompt (heads-up) → waiting (browser authorize) → granted (press Enter to // resume) → replay the held charge (pendingCharge.amount) → settle → close. // Never leaks the raw billing:manage scope — the user-facing concept is -// "terminal billing". +// "Remote Spending". function StepUpScreen({ amount, @@ -526,12 +526,12 @@ function StepUpScreen({ } setPhase('waiting') - ctx.sys('Opening your browser to enable terminal billing…') + ctx.sys('Opening your browser to allow Remote Spending…') void ctx.requestRemoteSpending().then(granted => { if (!granted) { ctx.sys( - "! Couldn't enable terminal billing — someone with billing permissions (owner, admin, or finance admin) has to approve it. Your card was not charged." + "! Couldn't allow Remote Spending — someone with billing permissions (owner, admin, or finance admin) has to approve it. Your card was not charged." ) onClose() @@ -550,12 +550,12 @@ function StepUpScreen({ } setPhase('resuming') - ctx.sys('✓ Terminal billing enabled — resuming your purchase.') + ctx.sys('✓ Remote Spending allowed — resuming your purchase.') void ctx.charge(amount, idempotencyKey).then(outcome => { // If the replay STILL can't spend (grant raced/expired or downscoped), // say so — don't close on a reassuring line with no charge made. if (outcome === 'needs_remote_spending') { - ctx.sys('! Terminal billing still needs approval — run /topup to try again. Your card was not charged.') + ctx.sys('! Remote Spending still needs approval — run /topup to try again. Your card was not charged.') } onClose() @@ -563,7 +563,7 @@ function StepUpScreen({ } const decline = () => { - ctx.sys('No charge made. Run /topup when you want to enable terminal billing.') + ctx.sys('No charge made. Run /topup when you want to allow Remote Spending.') onClose() } @@ -622,7 +622,7 @@ function StepUpScreen({ return ( - Enable terminal billing + Allow Remote Spending Waiting for your browser… Approve in the page that just opened. @@ -637,7 +637,7 @@ function StepUpScreen({ return ( - Terminal billing enabled + Remote Spending allowed Your ${amount} top-up is ready to finish. @@ -652,7 +652,7 @@ function StepUpScreen({ return ( - Enable terminal billing + Allow Remote Spending Resuming your ${amount} top-up… @@ -667,12 +667,12 @@ function StepUpScreen({ One-time setup - To charge this terminal, enable terminal billing once. + To charge from this terminal, allow Remote Spending once. It opens your browser to authorize, then your ${amount} top-up picks up right here. - + {footer('↑/↓ select · Enter confirm · Y/N quick · Esc cancel', t)} diff --git a/ui-tui/src/components/subscriptionOverlay.tsx b/ui-tui/src/components/subscriptionOverlay.tsx index 6de7ceeaa6f4..3a75ddab21d4 100644 --- a/ui-tui/src/components/subscriptionOverlay.tsx +++ b/ui-tui/src/components/subscriptionOverlay.tsx @@ -29,7 +29,7 @@ interface SubscriptionOverlayProps { /** * The /subscription modal — an in-terminal plan-change flow (V3). A small state * machine: overview → picker → confirm → result, with a stepup screen spliced in - * when a mutation needs terminal billing. Downgrades / cancellations / resume are + * when a mutation needs remote spending. Downgrades / cancellations / resume are * chargeless; an upgrade charges the card on the subscription, and an SCA/decline * is handed off to the portal. Starting a NEW subscription still deep-links (needs * a fresh card). All RPCs live in subscription.ts, reached via `overlay.ctx`. @@ -162,7 +162,7 @@ function upgradeResult(r: null | SubscriptionUpgradeResponse, pendingTierId?: nu return errorResult(r) } -/** Map a failed terminal-billing step-up to the right recovery copy (typed). */ +/** Map a failed remote-spending step-up to the right recovery copy (typed). */ function stepUpDenialResult(res: { error?: string; message?: string }): SubscriptionResult { if (res.error === 'session_revoked') { return { message: 'Your session expired — run /portal to log in again, then retry the change.', ok: false } @@ -170,8 +170,7 @@ 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.', + message: res.message || 'Remote spending was stopped for this terminal — reconnect from the portal, then retry.', ok: false } } @@ -183,7 +182,7 @@ 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.', + 'Remote Spending was not allowed — someone with billing permissions (owner, admin, or finance admin) must approve it. You can also make this change on the portal.', ok: false } } @@ -196,7 +195,8 @@ function stepUpDenialResult(res: { error?: string; message?: string }): Subscrip // Post-grant replays pass allowStepUp=false and surface this instead (mirrors the // CLI's allow_stepup=False cap). const scopeStillDeniedResult: SubscriptionResult = { - message: 'Terminal billing still isn’t enabled for this org — enable it on the portal, then retry.', + message: + 'Remote Spending still isn’t active for this terminal — the authorization didn’t take. Retry, or make this change on the portal.', ok: false } @@ -818,7 +818,7 @@ function ResultScreen({ onClose, overlay, t }: Omit) { ) } -// ── Screen: Step-up (grant terminal billing inline, then replay) ────── +// ── Screen: Step-up (allow remote spending inline, then replay) ─────── function StepUpScreen({ onPatch, overlay, t }: ScreenProps) { const { ctx } = overlay @@ -908,7 +908,7 @@ function StepUpScreen({ onPatch, overlay, t }: ScreenProps) { ] : phase === 'prompt' ? [ - { color: t.color.ok, label: 'Enable terminal billing', run: enable }, + { color: t.color.ok, label: 'Allow Remote Spending', run: enable }, { label: 'Cancel', run: back } ] : [] @@ -918,12 +918,12 @@ function StepUpScreen({ onPatch, overlay, t }: ScreenProps) { return ( - Terminal billing + Remote Spending {phase === 'prompt' && ( <> - Changing your plan needs terminal billing enabled for this org. Enable it here, then continue. + Changing your plan needs Remote Spending allowed for this terminal. Allow it here, then continue. Someone with billing permissions (owner, admin, or finance admin) approves it once in the browser. @@ -935,7 +935,7 @@ function StepUpScreen({ onPatch, overlay, t }: ScreenProps) { 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 === 'granted' && Remote Spending allowed. Continue to finish your change.} {phase === 'resuming' && Applying your change…} {rows.map((row, i) => ( diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index 7fa2c34b37ae..49d1a6e8b00c 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -45,7 +45,7 @@ export interface SlashExecResponse { warning?: string } -// ── Terminal billing (Phase 2b) ────────────────────────────────────── +// ── Remote Spending (Phase 2b) ─────────────────────────────────────── // Wire shapes now live in @hermes/shared for reuse by TypeScript clients. export type { diff --git a/website/docs/reference/slash-commands.md b/website/docs/reference/slash-commands.md index 8b5786de5a55..0ca2b811fde6 100644 --- a/website/docs/reference/slash-commands.md +++ b/website/docs/reference/slash-commands.md @@ -113,7 +113,7 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in | `/version` | Show Hermes Agent version, build, and environment info. | | `/usage` | Show token usage, cost breakdown, session duration, and — when available from the active provider — an **Account limits** section with remaining quota / credits / plan usage pulled live from the provider's API. | | `/credits` | Show your Nous credit balance and a top-up handoff link. | -| `/billing` | CLI terminal-billing flow for Nous — view balance, buy credits, and manage auto-reload / monthly limits. | +| `/billing` | CLI Remote Spending flow for Nous — view balance, buy credits, and manage auto-reload / monthly limits. | | `/insights` | Show usage insights and analytics (last 30 days) | | `/platforms` (alias: `/gateway`) | Show gateway/messaging platform status (CLI-only summary view). | | `/paste` | Attach a clipboard image |