Skip to content

Add annual Pro pricing - #9234

Merged
lawrencecchen merged 57 commits into
mainfrom
feat-annual-pricing
Aug 1, 2026
Merged

lawrencecchen merged 57 commits into
mainfrom
feat-annual-pricing

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add cmux Pro annual billing at $288/year, shown as $24/month with 20% savings
  • toggle Monthly and Annual in place with shared React state across public, embedded, and dashboard pricing
  • route the selected interval through Stripe checkout metadata and preserve the legacy $240 annual subscription key
  • provision and validate the new annual Stripe lookup key without changing grandfathered subscriptions

Validation

  • bun run test (931 passed, 128 skipped)
  • bun run typecheck
  • focused ESLint
  • bun run build
  • tagged macOS build ann288, verified in the embedded Upgrade flow

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Adds annual Pro ($288/year, $24/month) and Team ($336/year, $28/user/month) with a shared Monthly/Annual toggle across public, in‑app, and dashboard pricing. Secures checkout with an HMAC‑signed, same‑origin app relay and authenticated native return, and syncs the app theme to web surfaces with contrast‑safe product blue.

  • New Features

    • Pricing: centralized in services/billing/plans; shared PricingIntervalProvider/PricingIntervalSelector and PricingCheckoutButton (toggle without navigation); shows monthly equivalents and savings; checkout links include interval; CTA/checkout analytics via lib/posthog-client.
    • Checkout/Stripe: Pro yearly cmux-pro-yearly-288; Team yearly cmux-team-yearly-336; resolves month/year for both; envs STRIPE_PRO_YEARLY_288_PRICE_ID, STRIPE_TEAM_YEARLY_PRICE_ID; unified web/scripts/stripe/provision-catalog.sh for test/live with authenticated retries.
    • App/web: pricing and Pro Welcome share BrowserAppTheme; contrast‑adjusted --cmux-product-blue tokens for light/dark; theme injection pinned to the trusted origin; explicit external‑browser intent via cmux_external_browser=1 from trusted sources; updated ghostty submodule and recorded GhosttyKit artifact for a working theme picker.
  • Bug Fixes

    • Checkout safety: require a valid interval; same‑origin, HMAC‑signed app checkout relay with expiry/signature; authenticate and preserve tagged native callback schemes through relay, completion, success, and after‑sign‑in; billingInvalidRelay banner; guard Stripe pagination.
    • External intents/URL safety: trust‑gate external‑browser intents and destinations; shared loopback host validation in the URL resolver; tests cover app‑web origin pinning, relay signing, annual pricing behavior, and Stripe catalog provisioning.
    • Build/UI/Copy: run Pagefind before next build; clean up embedded pricing layout; update macOS copy to $288/year.
    • Restore: return validated portable agent restore arguments.

Written for commit 3afa9eb. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added monthly and annual Pro and Team billing options across pricing, checkout, and dashboard billing.
    • Annual Pro pricing now displays $288/year ($24/month) with 20% savings.
    • Billing selections persist through checkout and cancellation links.
    • App web pages now synchronize with the desktop app’s theme.
    • Supported links can open directly in an external browser.
  • Bug Fixes

    • Improved billing price validation.
    • Preserved legacy annual pricing for grandfathered subscriptions.
  • Tests

    • Added coverage for pricing, checkout, theme synchronization, and legacy subscriptions.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Pro billing now supports monthly and annual intervals across pricing pages, dashboard upsells, checkout URLs, Stripe metadata, analytics, and pricing display. Embedded app pages receive Ghostty-derived themes and generalized external-browser handling.

Changes

Pro billing intervals

Layer / File(s) Summary
Pricing contracts and Stripe provisioning
web/services/billing/*, web/scripts/stripe/*, web/app/env.ts, web/messages/*
Defines interval-specific prices and lookup keys, preserves grandfathered annual pricing, validates Stripe catalog data, and updates pricing copy.
Interval context, URLs, and checkout analytics
web/app/components/pricing-interval-selector.tsx, web/app/lib/billing.ts, web/app/lib/posthog-client.ts, web/app/components/checkout-navigation.tsx
Adds interval selection, interval-aware URLs, centralized PostHog setup, and checkout analytics.
Pricing and dashboard integration
web/app/[locale]/pricing/page.tsx, web/app/app-pricing/page.tsx, web/app/[locale]/dashboard/billing/page.tsx, web/app/[locale]/components/pro-cta-link.tsx, web/app/components/pricing-shared.tsx
Renders monthly or annual Pro and Team pricing and routes selected intervals through pricing and dashboard checkout flows.
Checkout persistence and validation
web/app/api/billing/checkout/route.ts, web/tests/billing-*, web/tests/pro-*, web/tests/stripe-provision-catalog.test.ts
Adds normalized intervals to cancellation URLs and Stripe metadata, with URL, session, pricing, and provisioning coverage.

Embedded web theme and navigation

Layer / File(s) Summary
Theme models and pricing appearance
Sources/ProWelcomeChecklist.swift, Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/AppSession/*, web/app/app-pricing/appearance.ts, web/app/app-pro-welcome/page.tsx, web/app/globals.css
Derives Ghostty theme values, decorates supported URLs, and applies consolidated pricing theme variables.
Web-view theme application and external navigation
Sources/Panels/BrowserPanel.swift, Sources/Panels/BrowserNavigationDelegate.swift, Packages/macOS/CmuxBrowser/Tests/*, cmuxTests/*, ghostty
Applies themes during web-view lifecycle events and recognizes validated HTTP(S) external-browser intents beyond checkout routes.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PricingPage
  participant PricingIntervalProvider
  participant PricingCheckoutButton
  participant CheckoutRoute
  participant Stripe
  PricingPage->>PricingIntervalProvider: initialize selected interval
  PricingPage->>PricingCheckoutButton: provide interval-specific checkout hrefs
  PricingCheckoutButton->>CheckoutRoute: redirect with interval parameter
  CheckoutRoute->>Stripe: create session with billingInterval metadata
  Stripe-->>CheckoutRoute: return checkout session
Loading

Possibly related PRs


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error AppWebThemeSnapshot adds a large RGB contrast algorithm in root Sources/ProWelcomeChecklist.swift; tests exercise it independently, beyond the allowed Ghostty/app-lifecycle glue. Keep Ghostty color resolution in the app, but extract the pure contrast logic to the CmuxBrowser package target as public BrowserAppThemeContrast and move its unit tests there.
Cmux User-Facing Error Privacy ❌ Error web/scripts/stripe/provision-catalog.sh adds command output containing Stripe product, price, and webhook IDs, lookup keys, and environment variable names. Use generic operator-facing output. Keep billing IDs, provider details, lookup keys, and environment names in sanitized internal logs only.
Cmux Full Internationalization ❌ Error Annual pricing adds or changes 15 user-facing message paths only in en.json and ja.json; the other 18 locales in routing.ts lack matching entries. Add translated entries for the changed pricing and FAQ paths in web/messages/{ar,bs,da,de,es,fr,it,km,ko,no,pl,pt-BR,ru,th,tr,uk,zh-CN,zh-TW}.json.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (21 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Production Swift additions use Sendable value types, while AppWebThemeSnapshot, BrowserPanel, and BrowserNavigationDelegate have explicit @MainActor boundaries; no new mutable Sendable reference or...
Cmux Swift Blocking Runtime ✅ Passed The full Swift diff adds no semaphores, blocking waits, sleeps, delayed dispatch, polling, main-queue sync, or manual locks; changes use @MainActor and nonblocking JavaScript evaluation.
Cmux Browser Automation Off-Main ✅ Passed PR base diff has no changes to TerminalController.swift, ControlCommandExecutionPolicy.swift, its tests, or browser socket routing; added evaluateJavaScript is UI theme sync, not socket automation.
Cmux Expensive Synchronous Load ✅ Passed The PR Swift diff adds no agent-history loaders or large-file parsing; theme work is bounded and existing close fallback uses SharedLiveAgentIndex with a nil-cache fallback.
Cmux Cache Substitution Correctness ✅ Passed AppWebThemeSnapshot is transient UI state: cold reads use GhosttyApp directly, updates use notifications, and no persistence/history/undo consumer trusts it; Stripe caches are outside those paths.
Cmux No Hacky Sleeps ✅ Passed The PR adds no sleep, timer, fixed delay, or wall-clock retry in non-test runtime code; setInterval is only a React state setter, and shell loops paginate Stripe API results.
Cmux Algorithmic Complexity ✅ Passed No complexity violation found: Stripe provisioning scans each paginated product/webhook list once; pricing/theme logic uses fixed-size collections or static copy, with no per-record rescans or hot-...
Cmux Swift Concurrency ✅ Passed The Swift diff adds no Dispatch, Combine, fire-and-forget Task, or new internal completion APIs; the sole completionHandler use is WebKit evaluateJavaScript at an allowed callback boundary.
Cmux Swift @Concurrent ✅ Passed The full Swift diff adds no async or nonisolated work and no @concurrent annotations; new theme and navigation work is synchronous and runs under @MainActor.
Cmux Swiftpm Lockfiles ✅ Passed The PR changes no cmux Package.swift, Package.resolved, .gitignore, workflow, or Xcode package references; its only dependency change is the documented vendored Ghostty submodule.
Cmux Swift Logging ✅ Passed The Swift diff adds no print, debugPrint, dump, NSLog, Logger, or ad hoc diagnostic logging; the only changed log text is an existing #if DEBUG cmuxDebugLog call.
Cmux Swiftui State Layout ✅ Passed The PR adds no SwiftUI state, GeometryReader, lazy/list store rows, or render-time mutations; BrowserPanel changes are AppKit/WKWebView theme callbacks, and existing ObservableObject state is touch...
Cmux Architecture Rethink ✅ Passed Swift changes use value snapshots and one BrowserPanel theme-apply path; no new timing repair, polling, locks, mutable side channel, or split MainActor owner was introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The Swift diff adds theme/navigation logic to existing browser/workspace surfaces, not a standalone window; no close shortcut ownership is introduced, and scripts/lint_auxiliary_window_close_shortc...
Cmux Source Artifacts ✅ Passed The 47 changed paths are source, tests, scripts, docs, config, localization, or documented Ghostty release metadata; no forbidden artifact directories or generated files were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Changed production Swift adds theme and navigation behavior only; no new DEBUG/test-only seam or test-named member was added, and tested helpers have production callers.
Cmux No Ambient Global State ✅ Passed Production Swift adds constructable instance-owned theme and navigation types plus private pure helpers; no new mutable global, static-only namespace, singleton, or app-delegate state appears in th...
Title check ✅ Passed The title clearly identifies the primary change: adding annual Pro pricing.
Description check ✅ Passed The description explains the changes and testing performed, but omits several template sections such as the demo video, review trigger, and checklist.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-annual-pricing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/app/`[locale]/dashboard/billing/page.tsx:
- Around line 524-530: Update priceCopy to derive the current monthly and yearly
display strings from PRO_PRICING_USD rather than hardcoding "$30/month" and
"$288/year"; preserve the existing lookup-key matching and legacy yearly
"$240/year" behavior.

In `@web/app/api/billing/checkout/route.ts`:
- Around line 103-108: Validate the raw interval query parameter before calling
proBillingInterval: allow an absent value to default to monthly, accept only
"month" or "year", and return the same invalid-checkout response used by
checkoutPlan for any explicit unrecognized value. Keep successUrl and cancelUrl
construction unchanged after validation.

In `@web/messages/en.json`:
- Around line 403-408: Add billingPeriod, monthly, annual, saveAnnual,
annualPriceDetail, and annualComparePrice to every locale file routed by
web/i18n/routing.ts, including all 18 listed non-English locales, using
appropriate translations or the established fallback convention. Keep the keys
consistent across locale files and preserve the existing English and Japanese
entries.

In `@web/tests/app-pricing-page.test.tsx`:
- Around line 113-117: Update the negative assertion in the pricing page test
around the checkout href expectation to check that the monthly interval
parameter is absent, rather than checking the unrelated appearance parameter.
Keep the existing yearly checkout assertion and active button assertion
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c79224da-5a34-44e9-93c2-2891c093ab67

📥 Commits

Reviewing files that changed from the base of the PR and between b86cce2 and a61a0ab.

📒 Files selected for processing (24)
  • skills/cmux-billing/SKILL.md
  • web/app/[locale]/components/pro-cta-link.tsx
  • web/app/[locale]/dashboard/billing/page.tsx
  • web/app/[locale]/posthog.tsx
  • web/app/[locale]/pricing/page.tsx
  • web/app/api/billing/checkout/route.ts
  • web/app/app-pricing/page.tsx
  • web/app/components/checkout-navigation.tsx
  • web/app/components/pricing-interval-selector.tsx
  • web/app/components/pricing-shared.tsx
  • web/app/lib/billing.ts
  • web/app/lib/posthog-client.ts
  • web/messages/en.json
  • web/messages/ja.json
  • web/scripts/stripe/provision-live.sh
  • web/services/billing/plans.ts
  • web/services/billing/stripe.ts
  • web/tests/app-pricing-page.test.tsx
  • web/tests/billing-checkout-route.test.ts
  • web/tests/billing-links.test.ts
  • web/tests/dashboard-billing-page.test.tsx
  • web/tests/pricing-page.test.tsx
  • web/tests/pro-cta-link.test.tsx
  • web/tests/pro-pricing.test.ts

Comment thread web/app/[locale]/dashboard/billing/page.tsx Outdated
Comment thread web/app/api/billing/checkout/route.ts Outdated
Comment thread web/messages/en.json
Comment thread web/tests/app-pricing-page.test.tsx
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/BrowserNavigationDelegate.swift`:
- Around line 5-16: Update
BrowserExternalNavigationIntent.shouldOpenInSystemBrowser to honor
cmux_external_browser only when the URL’s scheme and host match the trusted web
origins resolved by AuthEnvironment for checkout, portal, or pricing URLs;
reject non-cmux hosts before evaluating the flag. Add a test covering a non-cmux
host with the flag and asserting it returns false, while preserving acceptance
for trusted origins.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b85939d2-2e98-48cf-b5cf-4865fb3c027a

📥 Commits

Reviewing files that changed from the base of the PR and between a61a0ab and 32dfdac.

📒 Files selected for processing (18)
  • Sources/Panels/BrowserNavigationDelegate.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/ProWelcomeChecklist.swift
  • cmuxTests/AuthEnvironmentTests.swift
  • web/app/[locale]/dashboard/billing/page.tsx
  • web/app/[locale]/pricing/page.tsx
  • web/app/app-pricing/appearance.ts
  • web/app/app-pricing/page.tsx
  • web/app/app-pro-welcome/page.tsx
  • web/app/components/pricing-interval-selector.tsx
  • web/app/components/pricing-shared.tsx
  • web/app/lib/billing.ts
  • web/messages/en.json
  • web/messages/ja.json
  • web/tests/app-pricing-page.test.tsx
  • web/tests/app-pro-welcome-page.test.tsx
  • web/tests/dashboard-billing-page.test.tsx
  • web/tests/pricing-page.test.tsx

Comment thread Sources/Panels/BrowserNavigationDelegate.swift Outdated
@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghostty`:
- Line 1: Remove the invalid ghostty submodule pointer update and restore the
submodule to a reachable commit that exists in the Ghostty repository and is an
ancestor of its main branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 213b1993-b5cc-4b3a-8485-8c49e7303b26

📥 Commits

Reviewing files that changed from the base of the PR and between d1efb9c and 3b8ee71.

📒 Files selected for processing (1)
  • ghostty

Comment thread ghostty Outdated
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Current-head preview: https://cmux-kf0boh4yx-manaflow.vercel.app/pricing

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Current-head preview: https://cmux-6j97s7hzh-manaflow.vercel.app/pricing

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Live Stripe catalog verified: cmux-pro-yearly-288 is active at USD $288/year. The legacy cmux-pro-yearly price remains active for existing subscribers.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 880a08d596

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +577 to 580
if (lookupKey === PRO_PRICING_USD.year.lookupKey) {
return `$${PRO_PRICING_USD.year.billedAmount}/year`;
}
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive annual Pro copy from subscription metadata

When STRIPE_PRO_YEARLY_288_PRICE_ID references a valid operator-managed annual Price whose lookup key is absent or differs from cmux-pro-yearly-288, checkout still records billingInterval: "year", but this function falls through to null and StripePlan omits the price metric entirely. Use the persisted interval metadata as a fallback for new annual Pro subscriptions, while retaining the lookup-key check that distinguishes grandfathered $240/year subscriptions.

Useful? React with 👍 / 👎.

Comment on lines +379 to +383
#expect(script.contains("[data-cmux-app-theme]"))
#expect(script.contains("--ghostty-background"))
#expect(script.contains("--ghostty-foreground"))
#expect(script.contains("--cmux-product-blue"))
#expect(script.contains("--cmux-product-blue-on-background"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Execute the generated theme script in a runtime harness

These assertions only search the generated JavaScript for variable names, so they still pass if the script is syntactically invalid, the selector no longer matches, or the assignments never affect the DOM. Execute the script against a small DOM/WebKit harness and assert the resulting styles and dataset instead; the repository's cmux-testing guidance requires runtime behavior rather than implementation-text checks.

AGENTS.md reference: AGENTS.md:L107-L107

Useful? React with 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 18 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/app/lib/billing.ts">

<violation number="1" location="web/app/lib/billing.ts:118">
P2: Cross-origin checkout can fail for fresh relay links under ordinary clock skew because the verifier has no future-time tolerance. Allow a small bounded clock-skew window (and apply it to both expiry bounds) or rely on the HMAC-authenticated expiry without rejecting valid tokens that are slightly future-dated.</violation>

<violation number="2" location="web/app/lib/billing.ts:218">
P2: Native callback relays can silently lose their callback when `cmuxScheme` contains casing or whitespace: the URL carries the normalized scheme, while the HMAC covers the raw scheme. Signing the same normalized `scheme` that is emitted would keep generation and verification consistent.</violation>
</file>

<file name="web/app/lib/native-callback.ts">

<violation number="1" location="web/app/lib/native-callback.ts:26">
P2: Tagged development-app checkouts completed through a deployed or cross-origin relay cannot finish native handoff: this function preserves `cmux-dev-*`, but `/handler/after-sign-in` later validates that scheme against its deployed request and rejects it as non-local. The trust established in Stripe metadata needs to be carried into the handoff validation (or the flow needs a signed server-side handoff) rather than only selecting the scheme here.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread web/app/lib/billing.ts
return {
scheme,
expires,
signature: relaySignature(target, parameters, expires, secret),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Native callback relays can silently lose their callback when cmuxScheme contains casing or whitespace: the URL carries the normalized scheme, while the HMAC covers the raw scheme. Signing the same normalized scheme that is emitted would keep generation and verification consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/lib/billing.ts, line 218:

<comment>Native callback relays can silently lose their callback when `cmuxScheme` contains casing or whitespace: the URL carries the normalized scheme, while the HMAC covers the raw scheme. Signing the same normalized `scheme` that is emitted would keep generation and verification consistent.</comment>

<file context>
@@ -140,3 +197,67 @@ function configuredAppPricingCheckoutURL(): URL | null {
+  return {
+    scheme,
+    expires,
+    signature: relaySignature(target, parameters, expires, secret),
+  };
+}
</file context>
Suggested change
signature: relaySignature(target, parameters, expires, secret),
signature: relaySignature(
target,
{ ...parameters, cmuxScheme: scheme },
expires,
secret,
),

Comment thread web/app/lib/billing.ts Outdated
): string | null {
const scheme = rawScheme?.trim().toLowerCase() ?? "";
if (NATIVE_SCHEMES.has(scheme) || scheme === "cmux-dev") return scheme;
return /^cmux-dev-[a-z0-9-]+$/.test(scheme) ? scheme : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Tagged development-app checkouts completed through a deployed or cross-origin relay cannot finish native handoff: this function preserves cmux-dev-*, but /handler/after-sign-in later validates that scheme against its deployed request and rejects it as non-local. The trust established in Stripe metadata needs to be carried into the handoff validation (or the flow needs a signed server-side handoff) rather than only selecting the scheme here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/lib/native-callback.ts, line 26:

<comment>Tagged development-app checkouts completed through a deployed or cross-origin relay cannot finish native handoff: this function preserves `cmux-dev-*`, but `/handler/after-sign-in` later validates that scheme against its deployed request and rejects it as non-local. The trust established in Stripe metadata needs to be carried into the handoff validation (or the flow needs a signed server-side handoff) rather than only selecting the scheme here.</comment>

<file context>
@@ -18,6 +18,14 @@ export function validatedNativeCallbackScheme(
+): string | null {
+  const scheme = rawScheme?.trim().toLowerCase() ?? "";
+  if (NATIVE_SCHEMES.has(scheme) || scheme === "cmux-dev") return scheme;
+  return /^cmux-dev-[a-z0-9-]+$/.test(scheme) ? scheme : null;
+}
+
</file context>

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Exact current-head Vercel preview for b22f63e0b99ec1d6b876ed8651dd9387cf92db76: https://cmux-aj62unaqd-manaflow.vercel.app/app-pricing?cmux_app=1&appearance=dark&background=%23272822&interval=year

Verified with Vercel protection bypass: Annual selected, Save 20%, Pro $24/$288, Team $28/$336, and annual checkout links.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Current head 9d263fb1fa adds only the base-branch Swift compile repair. The verified web tree is unchanged: https://cmux-aj62unaqd-manaflow.vercel.app/app-pricing?cmux_app=1&appearance=dark&background=%23272822&interval=year

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Final exact-head Vercel preview for 535d274bdc: https://cmux-ef26z8mwk-manaflow.vercel.app/app-pricing?cmux_app=1&appearance=dark&background=%23272822&interval=year

Verified with Vercel protection bypass: exclusive interval control, Annual selected, Save 20%, explicit yearly billing cadence, Pro $24/$288, Team $28/$336, and annual checkout links.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 15 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/Panels/BrowserPanel.swift">

<violation number="1" location="Sources/Panels/BrowserPanel.swift:5278">
P2: Theme JavaScript is no longer origin-gated at the injection helper: any caller that passes an external `WKWebView` can mutate a page that presents `[data-cmux-app-theme]`. Keeping `supportsAppWebTheme(webView)` in this guard preserves the trusted-origin boundary at the `evaluateJavaScript` seam.</violation>
</file>

<file name="web/scripts/stripe/provision-catalog.sh">

<violation number="1" location="web/scripts/stripe/provision-catalog.sh:115">
P2: Catalog provisioning now scans inactive products too, so Stripe accounts with a large retired catalog can require many extra list requests before finding the canonical product. Passing the list endpoint's `active=true` filter would preserve the existing active-product behavior while retaining pagination.</violation>
</file>

<file name="web/app/components/pricing-interval-selector.tsx">

<violation number="1" location="web/app/components/pricing-interval-selector.tsx:102">
P2: Pricing interval now duplicates composite radio-group focus and selection behavior in this component, increasing the chance that keyboard and accessibility semantics drift from the shared UI implementation. Using Base UI's Radio Group (or native radio inputs if styling permits) would keep roving focus and checked-state behavior in a tested primitive.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic


private func applyAppWebTheme(_ theme: AppWebThemeSnapshot, to webView: WKWebView) {
let browserTheme = theme.browserTheme
guard let script = browserTheme.applyingJavaScript() else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Theme JavaScript is no longer origin-gated at the injection helper: any caller that passes an external WKWebView can mutate a page that presents [data-cmux-app-theme]. Keeping supportsAppWebTheme(webView) in this guard preserves the trusted-origin boundary at the evaluateJavaScript seam.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Panels/BrowserPanel.swift, line 5278:

<comment>Theme JavaScript is no longer origin-gated at the injection helper: any caller that passes an external `WKWebView` can mutate a page that presents `[data-cmux-app-theme]`. Keeping `supportsAppWebTheme(webView)` in this guard preserves the trusted-origin boundary at the `evaluateJavaScript` seam.</comment>

<file context>
@@ -5275,11 +5275,7 @@ final class BrowserPanel: Panel, ObservableObject {
-            trustedOrigin: AuthEnvironment.appWebOrigin
-        ),
-              let script = browserTheme.applyingJavaScript() else {
+        guard let script = browserTheme.applyingJavaScript() else {
             return
         }
</file context>
Suggested change
guard let script = browserTheme.applyingJavaScript() else {
guard supportsAppWebTheme(webView),
let script = browserTheme.applyingJavaScript() else {

Comment thread web/scripts/stripe/provision-catalog.sh Outdated
page_args+=(--data-urlencode "starting_after=${starting_after}")
fi
response="$(
stripe_get "/products" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Catalog provisioning now scans inactive products too, so Stripe accounts with a large retired catalog can require many extra list requests before finding the canonical product. Passing the list endpoint's active=true filter would preserve the existing active-product behavior while retaining pagination.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/scripts/stripe/provision-catalog.sh, line 115:

<comment>Catalog provisioning now scans inactive products too, so Stripe accounts with a large retired catalog can require many extra list requests before finding the canonical product. Passing the list endpoint's `active=true` filter would preserve the existing active-product behavior while retaining pagination.</comment>

<file context>
@@ -101,19 +101,18 @@ product_matches_catalog_identity() {
     response="$(
-      stripe_get "/products/search" \
-        --data-urlencode "query=name:'${name}' AND active:'true'" \
+      stripe_get "/products" \
         --data-urlencode "limit=100" \
         "${page_args[@]}"
</file context>
Suggested change
stripe_get "/products" \
stripe_get "/products" \
--data-urlencode "active=true" \

},
[setInterval, surface],
);
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Pricing interval now duplicates composite radio-group focus and selection behavior in this component, increasing the chance that keyboard and accessibility semantics drift from the shared UI implementation. Using Base UI's Radio Group (or native radio inputs if styling permits) would keep roving focus and checked-state behavior in a tested primitive.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/components/pricing-interval-selector.tsx, line 102:

<comment>Pricing interval now duplicates composite radio-group focus and selection behavior in this component, increasing the chance that keyboard and accessibility semantics drift from the shared UI implementation. Using Base UI's Radio Group (or native radio inputs if styling permits) would keep roving focus and checked-state behavior in a tested primitive.</comment>

<file context>
@@ -84,37 +88,58 @@ export function PricingIntervalSelector({
+    },
+    [setInterval, surface],
+  );
+  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
+    let nextInterval: BillingInterval | null = null;
+    switch (event.key) {
</file context>

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 20 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/scripts/stripe/provision-catalog.sh">

<violation number="1" location="web/scripts/stripe/provision-catalog.sh:106">
P3: This initialization is immediately overwritten at the top of the loop, so it never affects a request and makes the pagination state look initialized twice. Declaring the local array without an initial value keeps the loop as the single initialization point.</violation>

<violation number="2" location="web/scripts/stripe/provision-catalog.sh:244">
P3: This pre-loop assignment is dead setup because the loop overwrites it before the first webhook request. Removing it leaves behavior unchanged and avoids duplicate pagination initialization.</violation>
</file>

<file name="web/app/env.ts">

<violation number="1" location="web/app/env.ts:190">
P2: Retiring STRIPE_PRO_YEARLY_PRICE_ID via a hard validation error can take the whole production app down: in any non-preview runtime that still has the legacy var set (only `skipValidation` bypasses it), `env` throws 'Invalid environment variables' at boot, so the deployment crashes rather than degrading to the new key. Since the legacy key is explicitly retained for grandfathered subscriptions, consider logging a deprecation warning instead of a boot-failing error, or clearly sequencing env cleanup before deploying this version.</violation>
</file>

<file name="web/app/components/pricing-interval-selector.tsx">

<violation number="1" location="web/app/components/pricing-interval-selector.tsx:128">
P3: Screen-reader users are given a radiogroup with no reliable orientation even though the interval radios are laid out horizontally. Adding `aria-orientation="horizontal"` would expose the actual layout and align the group semantics with its left/right keyboard behavior.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread web/app/env.ts
STRIPE_PRO_YEARLY_PRICE_ID: z.string().min(1).optional(),
// Deliberately distinct from the legacy STRIPE_PRO_YEARLY_PRICE_ID,
// which can refer to the grandfathered $240/year price.
STRIPE_PRO_YEARLY_PRICE_ID: retiredEnvValue(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Retiring STRIPE_PRO_YEARLY_PRICE_ID via a hard validation error can take the whole production app down: in any non-preview runtime that still has the legacy var set (only skipValidation bypasses it), env throws 'Invalid environment variables' at boot, so the deployment crashes rather than degrading to the new key. Since the legacy key is explicitly retained for grandfathered subscriptions, consider logging a deprecation warning instead of a boot-failing error, or clearly sequencing env cleanup before deploying this version.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/env.ts, line 190:

<comment>Retiring STRIPE_PRO_YEARLY_PRICE_ID via a hard validation error can take the whole production app down: in any non-preview runtime that still has the legacy var set (only `skipValidation` bypasses it), `env` throws 'Invalid environment variables' at boot, so the deployment crashes rather than degrading to the new key. Since the legacy key is explicitly retained for grandfathered subscriptions, consider logging a deprecation warning instead of a boot-failing error, or clearly sequencing env cleanup before deploying this version.</comment>

<file context>
@@ -175,6 +187,10 @@ export const env = createEnv({
     STRIPE_PRO_MONTHLY_PRICE_ID: z.string().min(1).optional(),
     // Deliberately distinct from the legacy STRIPE_PRO_YEARLY_PRICE_ID,
     // which can refer to the grandfathered $240/year price.
+    STRIPE_PRO_YEARLY_PRICE_ID: retiredEnvValue(
+      "STRIPE_PRO_YEARLY_PRICE_ID",
+      "STRIPE_PRO_YEARLY_288_PRICE_ID",
</file context>

if [[ "$MODE" == "live" ]]; then
webhook_ids=""
starting_after=""
webhook_page_args=(--data-urlencode "limit=100")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This pre-loop assignment is dead setup because the loop overwrites it before the first webhook request. Removing it leaves behavior unchanged and avoids duplicate pagination initialization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/scripts/stripe/provision-catalog.sh, line 244:

<comment>This pre-loop assignment is dead setup because the loop overwrites it before the first webhook request. Removing it leaves behavior unchanged and avoids duplicate pagination initialization.</comment>

<file context>
@@ -247,15 +241,14 @@ ensure_price "$team_product_id" "cmux-team-yearly-336" "33600" "year" "cmux Team
 if [[ "$MODE" == "live" ]]; then
   webhook_ids=""
   starting_after=""
+  webhook_page_args=(--data-urlencode "limit=100")
   while :; do
-    webhook_page_args=()
</file context>

local plan="$2"
local response product_json product_id starting_after
local -a matching_product_ids=()
local -a page_args=(--data-urlencode "limit=100")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This initialization is immediately overwritten at the top of the loop, so it never affects a request and makes the pagination state look initialized twice. Declaring the local array without an initial value keeps the loop as the single initialization point.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/scripts/stripe/provision-catalog.sh, line 106:

<comment>This initialization is immediately overwritten at the top of the loop, so it never affects a request and makes the pagination state look initialized twice. Declaring the local array without an initial value keeps the loop as the single initialization point.</comment>

<file context>
@@ -101,21 +101,18 @@ product_matches_catalog_identity() {
+  local response product_json product_id starting_after
   local -a matching_product_ids=()
-  local -a page_args=()
+  local -a page_args=(--data-urlencode "limit=100")
 
-  next_page=""
</file context>
Suggested change
local -a page_args=(--data-urlencode "limit=100")
local -a page_args

<div
ref={captureView}
className="mx-auto mt-6 flex w-fit border border-border p-1 text-sm"
role="radiogroup"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Screen-reader users are given a radiogroup with no reliable orientation even though the interval radios are laid out horizontally. Adding aria-orientation="horizontal" would expose the actual layout and align the group semantics with its left/right keyboard behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/components/pricing-interval-selector.tsx, line 128:

<comment>Screen-reader users are given a radiogroup with no reliable orientation even though the interval radios are laid out horizontally. Adding `aria-orientation="horizontal"` would expose the actual layout and align the group semantics with its left/right keyboard behavior.</comment>

<file context>
@@ -84,37 +88,58 @@ export function PricingIntervalSelector({
       ref={captureView}
       className="mx-auto mt-6 flex w-fit border border-border p-1 text-sm"
-      role="group"
+      role="radiogroup"
       aria-label={billingPeriodLabel}
+      onKeyDown={handleKeyDown}
</file context>
Suggested change
role="radiogroup"
role="radiogroup"
aria-orientation="horizontal"

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 20 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/scripts/stripe/provision-catalog.sh">

<violation number="1" location="web/scripts/stripe/provision-catalog.sh:106">
P3: The initial `page_args` value is never observed because every loop iteration overwrites it before the request is built. Removing this initializer and the analogous `webhook_page_args` initializer keeps pagination setup in one place and avoids dead setup that can drift from the loop's actual arguments.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

local plan="$2"
local response product_json product_id starting_after
local -a matching_product_ids=()
local -a page_args=(--data-urlencode "limit=100")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The initial page_args value is never observed because every loop iteration overwrites it before the request is built. Removing this initializer and the analogous webhook_page_args initializer keeps pagination setup in one place and avoids dead setup that can drift from the loop's actual arguments.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/scripts/stripe/provision-catalog.sh, line 106:

<comment>The initial `page_args` value is never observed because every loop iteration overwrites it before the request is built. Removing this initializer and the analogous `webhook_page_args` initializer keeps pagination setup in one place and avoids dead setup that can drift from the loop's actual arguments.</comment>

<file context>
@@ -101,21 +101,18 @@ product_matches_catalog_identity() {
+  local response product_json product_id starting_after
   local -a matching_product_ids=()
-  local -a page_args=()
+  local -a page_args=(--data-urlencode "limit=100")
 
-  next_page=""
</file context>

# Conflicts:
#	docs/ghostty-fork.md
#	ghostty
#	scripts/ghosttykit-checksums.txt

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ffcb8bc76b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +12 to +13
/// Creates a complete theme after the app has resolved readable accent variants.
public init(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the new public initializer parameters

For package consumers, this six-argument public initializer leaves the meanings and invariants of the three accent variants undocumented. The package API rules require - Parameter documentation for public initializers and documentation for public property invariants, so add DocC entries for these arguments rather than only a summary sentence.

AGENTS.md reference: AGENTS.md:L99-L102

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3afa9eb13a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +304 to 306
enterprise: pricing.enterprise.price,
}}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore checkout actions in the embedded comparison

On /app-pricing, users who scroll to the comparison section no longer get its sticky Free, Pro, Team, or Enterprise actions because this PricingCompareTable call drops the entire actions prop; the public pricing page still wires interval-aware actions, and the pre-change embedded page supplied all four. Restore the actions using the shared checkout buttons while preserving the current-plan and App Store gating branches.

AGENTS.md reference: AGENTS.md:L89-L92

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant