Skip to content

[3/n] Billing & usage dispatcher refactor#11118

Open
IsaiahWitzke wants to merge 19 commits into
masterfrom
iw/billing-dispatcher
Open

[3/n] Billing & usage dispatcher refactor#11118
IsaiahWitzke wants to merge 19 commits into
masterfrom
iw/billing-dispatcher

Conversation

@IsaiahWitzke
Copy link
Copy Markdown
Contributor

@IsaiahWitzke IsaiahWitzke commented May 16, 2026

There were some issues with the v1/v2 split + the settings framework. searching for the usage and billing page wasnt working when the FF was on / we didnt show the old usage page for legacy plans / we couldnt hot-switch between the pages depending on the billing tier.

this pr wraps the 2 billing and usage pages with a dispatcher... that dispatcher is now the "SettingsWidget" that you can search for / filter out / etc...

Demo: https://www.loom.com/share/9ae48a10492142868e86cc5c96895ffa

^ i noticed that the "Billing and Usage" header is missing in the V2 page... im also added that back in this PR too.

IsaiahWitzke and others added 9 commits May 16, 2026 14:12
Adds freeAvailableModels back to the client query allowlist in
crates/warp_graphql_schema/api/client-schema.ts so it survives the
filtered codegen pass, then regenerates crates/warp_graphql_schema/api/schema.graphql
against staging. The regeneration also pulls in unrelated staging drift
that's been accumulating, including:

- New UsageVisibilityGranularity enum, UsageVisibilityPolicy type, and
  Tier.usageVisibilityPolicy field
- New AGGREGATE sentinel values on AICreditsUsageAndCostType,
  AICreditsUsageBucket, and AICreditsUsageSource
- Workspace.billingCycleUsageHistory and friends
- Harness*, AgentHarnessesPolicy, AgentVisibility, and harness settings
- enterpriseAnalyticsPolicy, enterpriseSpendingLimitsPolicy,
  enterpriseUsageThresholdsPolicy
- VOYAGE_4_512 EmbeddingConfig variant

Follow-up commits will resolve the resulting cynic compile errors in
crates/graphql.

Co-Authored-By: Oz <oz-agent@warp.dev>
After regenerating crates/warp_graphql_schema/api/schema.graphql against
staging, the warp_graphql crate failed to compile due to two
schema-drift issues that were unrelated to the freeAvailableModels
restoration:

- harnessAuthSecrets was missing from clientQueries in
  crates/warp_graphql_schema/api/client-schema.ts, so the filtered
  client schema lost the root query field plus its
  ListHarnessAuthSecretsInput, HarnessAuthSecretsOutput, and
  HarnessAuthSecretsResult types. Cynic then failed to compile the
  ListHarnessAuthSecrets fragment in
  crates/graphql/src/api/queries/list_harness_auth_secrets.rs. Adding
  the field to the allowlist and regenerating brings the types back.
- The EmbeddingConfig enum in
  crates/graphql/src/api/full_source_code_embedding.rs lacked the new
  VOYAGE_4_512 variant introduced server-side, so cynic refused to
  derive cynic::Enum on it. Added the variant with the matching
  #[cynic(rename = ...)] mapping.

cargo build -p warp_graphql and cargo clippy -p warp_graphql --tests
both succeed after these changes.

Co-Authored-By: Oz <oz-agent@warp.dev>
Adding VOYAGE_4_512 to the cynic EmbeddingConfig enum in the previous
commit broke the exhaustive From / TryFrom matches in
crates/ai/src/index/full_source_code_embedding/mod.rs.

Mirrors the new variant on the ai-side EmbeddingConfig enum and wires
both conversion match arms through to the new graphql variant, keeping
the workspace build green.

cargo build -p ai and cargo clippy -p ai --all-targets --all-features
--tests are both green after this change.

Co-Authored-By: Oz <oz-agent@warp.dev>
Adds the Rust shapes for the new server-side surface from the
tiered-usage-visibility spec, all in crates/graphql/src/api/billing.rs:

- UsageVisibilityPolicy and UsageVisibilityGranularity, with the new
  field plumbed through the Tier fragment.
- BillingCycleUsageHistory, BillingCycleUsageSummary, UsageEntry.
- AICreditsUsageAndCostSubjectType, AICreditsUsageAndCostType (with
  AGGREGATE sentinel), AICreditsUsageBucket (with AGGREGATE), and
  AICreditsUsageSource (with AGGREGATE).

All new enums carry a #[cynic(fallback)] Other(String) variant so the
client stays forward-compatible with future server-side additions.
Nothing in the client queries these fields yet; that comes in
subsequent PRs.

Co-Authored-By: Oz <oz-agent@warp.dev>
Wires the new cynic types into the workspace metadata query so the
client actually fetches the per-cycle usage breakdown and the tier's
visibility policy. No consumers yet — model conversion and rendering
land in subsequent PRs.

Co-Authored-By: Oz <oz-agent@warp.dev>
Adds the Rust-side mirrors of the new GraphQL types:

* BillingCycleUsageEntry / Summary / Data — the redacted per-cycle usage
  data returned by Workspace.billingCycleUsageHistory.
* UsageVisibilityGranularity / UsageVisibilityPolicy / UsageVisibility —
  the tier's visibility policy and the resolved per-viewer view of it.
  MaxPriorCycles is its own enum so consumers never have to handle the
  -1 sentinel from the wire format.

Tier.usage_visibility_policy and Workspace.billing_cycle_usage are
threaded through so the data has a place to land. Real From conversions
land in the next commit; for now gql_convert sets both to defaults so the
crate continues to compile.

UsageVisibility carries a temporary #[allow(dead_code)] because its
first consumer (resolve_usage_visibility) lands in the commit after gql
conversion. That annotation goes away once the resolver is added.

Co-Authored-By: Oz <oz-agent@warp.dev>
* From<GqlUsageVisibilityGranularity> for UsageVisibilityGranularity:
  maps each variant through; Other(_) is logged and falls back to OwnOnly
  (fail-closed).
* from_gql_max_prior_cycles: i32 -> MaxPriorCycles, turning the wire
  format's -1 sentinel into Unlimited and 0/N>0 into None/Limited(n).
  Defensively maps any other negative value to Unlimited with a logged
  error.
* From<GqlUsageVisibilityPolicy> for UsageVisibilityPolicy: composes
  the above two.
* convert_billing_cycle_usage: maps the nullable
  GqlBillingCycleUsageHistory into BillingCycleUsageData, flattening
  Time -> chrono::DateTime<Utc> and preserving entry shape.

Tier and Workspace conversions now use these instead of the temporary
defaults stubbed in the previous commit.

Co-Authored-By: Oz <oz-agent@warp.dev>
The helper folds the tier's UsageVisibilityPolicy and the viewer's admin
status into a single UsageVisibility value that downstream UI code can
match on. Admins (Owner or Admin, per Workspace::is_workspace_admin,
which matches the server's HasAdminLevelPermissions) get the policy's
admin_granularity; everyone else collapses to OwnOnly. max_prior_cycles
is plan-wide and applies to all viewers. Missing policy or viewer email
fails closed to the default {OwnOnly, MaxPriorCycles::None} \u2014 matching
the server's default-deny posture.

Tests cover the matrix: missing policy, missing viewer email, non-member
email, free user, Build admin, Build non-admin, Build Business admin,
Build Business non-admin, Enterprise admin, and an Admin-role-no-Owner
viewer.

UsageVisibility until the UI scaffold PR consumes them; the lint only
counts non-test usage.

Includes downstream fixups for test-only Workspace literals in
user_workspaces.rs (cfg(test) block), user_workspaces_tests.rs,
update_environment_form_tests.rs, and integration_testing/assertions.rs
\u2014 each gets billing_cycle_usage: Default::default() so the new
required field compiles in every test build.

Co-Authored-By: Oz <oz-agent@warp.dev>
…bounds

Mirror the GraphQL nullability exactly: the outer BillingCycleUsageHistory
is nullable, but currentPeriodStart/currentPeriodEnd are non-null when it
is present. Previously the Rust mirror flipped this — the outer field was
non-Optional and the two period bounds were independently Optional, which
admitted impossible states (one bound present and the other absent) and
required convert_billing_cycle_usage to synthesize a sentinel default.

Now:
- Workspace.billing_cycle_usage: Option<BillingCycleUsageData>
- BillingCycleUsageData.current_period_start/end are non-Optional DateTime
- convert_billing_cycle_usage takes the non-Optional GQL type directly; the
  outer Option is mapped at the call site

Co-Authored-By: Oz <oz-agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label May 16, 2026
@IsaiahWitzke IsaiahWitzke changed the base branch from master to iw/usage-visibility-model May 16, 2026 23:11
@IsaiahWitzke IsaiahWitzke force-pushed the iw/billing-dispatcher branch from 61b51c6 to 6a51a86 Compare May 16, 2026 23:33
addon_credit_denomination_buttons: Default::default(),
purchase_addon_credits_loading: false,
prorated_request_limits_info_mouse_states: Default::default(),
upgrade_link: MouseStateHandle::default(),
Copy link
Copy Markdown
Contributor Author

@IsaiahWitzke IsaiahWitzke May 16, 2026

Choose a reason for hiding this comment

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

wow... thats a bunch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Thoughts on grouping them into nested structs?

i.e.

struct PlanHeaderMouseHandles {
   ...
}

and put all the structs in there

@IsaiahWitzke IsaiahWitzke marked this pull request as ready for review May 16, 2026 23:49
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 16, 2026

@IsaiahWitzke

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@IsaiahWitzke IsaiahWitzke changed the title [3/n] dispatcher refactor [3/n] Billing & usage dispatcher refactor May 16, 2026
@IsaiahWitzke IsaiahWitzke requested a review from jefflloyd May 16, 2026 23:49
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

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

Overview

This PR wraps the legacy and v2 Billing and Usage pages behind a dispatcher so settings search and page selection target one SettingsWidget, and it moves the v2 page layout under the shared settings-page chrome.

Concerns

  • The dispatcher computes the v1/v2 route from workspace billing metadata but does not subscribe to workspace updates itself, so hot-switching can stay on the previously rendered child until some unrelated parent render occurs.
  • The new page visibility check no longer matches the old logged-out/anonymous behavior and can expose the Billing and Usage nav item while auth user data is missing or logged out.

Verdict

Found: 0 critical, 2 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/settings_view/billing_and_usage_dispatch.rs
Comment thread app/src/settings_view/billing_and_usage_dispatch.rs Outdated
IsaiahWitzke and others added 8 commits May 16, 2026 21:34
Hoists the helper from the standalone usage_visibility module onto the
Workspace struct and decouples it from the email-based admin lookup. The
function now takes the resolved is_admin bool directly, which:
- separates 'is the viewer an admin?' (caller's concern) from 'what
  visibility does that imply?' (policy's concern)
- makes the unit tests simpler (no need to construct workspace member
  lists)
- lets non-email callers (service accounts, future programmatic flows)
  resolve visibility without going through an email lookup

Deletes the now-empty usage_visibility / usage_visibility_tests modules
and folds the tests into the standard workspace_tests.rs sibling.

Co-Authored-By: Oz <oz-agent@warp.dev>
Wraps the existing legacy BillingAndUsagePageView (v1) and the new
BillingAndUsagePageV2View (v2) behind a BillingAndUsageDispatchView that
the Settings framework treats as the 'Billing and Usage' page. The
dispatcher owns a PageType::Monolith containing a single SettingsWidget
whose render() picks between v1 and v2 at render time via ChildView,
based on the BillingAndUsagePageV2 feature flag plus the current
workspace's plan (Build / Build Max / Build Business / Enterprise → v2,
everything else → v1).

What this gives us:
* Both v1 and v2 share identical page chrome (vertical scrollable,
  PAGE_PADDING, MAX_PAGE_WIDTH centering, narrow-pane dual-axis
  scrolling) — provided once by the dispatcher's PageType.
* One unified set of search terms (plan, billing, ai, usage, limit,
  credits, balance, overview) — typing any of those surfaces the page
  in the Settings sidebar regardless of which inner page is active.
* One stable widget_id so future scroll-to-widget callers have a target.

v1 changes:
* Collapses PlanWidget / UsageWidget / PlanWidgetStateHandles structs
  (which were only structs because of the SettingsWidget trait) directly
  into BillingAndUsagePageView. All 22 mouse-state/switch-state handles
  + the highlighted-hyperlink index now live on the page view, and the
  render methods become inherent methods (render_plan_header,
  render_page_body, plus their helpers). View::render is two children of
  a Flex::column.

v2 changes:
* Drops its own outer Container/Align/ConstrainedBox/PAGE_PADDING
  wrapper and the 'Billing and Usage' sub-header — the dispatcher's
  PageType provides them.
* Drops its inherent update_filter — the dispatcher's PageType handles
  search.

Adds the BillingAndUsagePageV2 feature flag (PR 4 will add it to
DOGFOOD_FLAGS once the new section lands).

Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Sets the Monolith PageType's title so v1 and v2 both get the standard
page-title chrome rendered above their content.

Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Adds BillingMetadata::is_free_plan for parity with is_enterprise_plan
and routes Free customers through the dispatcher's v2 branch.

Co-Authored-By: Oz <oz-agent@warp.dev>
@IsaiahWitzke IsaiahWitzke force-pushed the iw/billing-dispatcher branch from 9c8027b to 622b73d Compare May 17, 2026 01:36
Comment thread crates/warp_features/src/lib.rs Outdated
Base automatically changed from iw/usage-visibility-model to master May 17, 2026 06:17
Copy link
Copy Markdown
Contributor

@tylerlam-warp tylerlam-warp left a comment

Choose a reason for hiding this comment

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

This all looks good, I found a small bug. Also have some questions

Comment on lines +49 to +53
bm.is_on_build_plan()
|| bm.is_on_build_max_plan()
|| bm.is_on_build_business_plan()
|| bm.is_enterprise_plan()
|| bm.is_free_plan()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Goes back to your question about whether we should only enable the new page for Build. I think from my end:

I haven't done as much testing on enterprise and free tiers that I have yet. Not sure if those are ready yet for primetime?

I think in the end we probably should enable for enterprise, because we need to display the usage. Although I need to hide the reload credits panel on enterprise which I don't think happens yet

Comment on lines +46 to +47
.current_workspace()
.is_some_and(|workspace| {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happens if there is no current workspace? I guess we'll default to the old page? Should be fine?

Comment on lines +130 to +152
impl SettingsWidget for BillingAndUsageWidget {
type View = BillingAndUsageDispatchView;

fn search_terms(&self) -> &str {
"plan billing a.i. ai usage limit credits balance overview"
}

fn render(
&self,
view: &Self::View,
_appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let inner = if view.use_v2(app) {
ChildView::new(&view.v2).finish()
} else {
ChildView::new(&view.v1).finish()
};
Container::new(inner)
.with_margin_top(HEADER_PADDING)
.finish()
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Found a mini bug:

Image

Notice there are two toasts - I think both the new and old views are listening to the buy credits event. I think we need to just listen in the active one?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

goood catch

addon_credit_denomination_buttons: Default::default(),
purchase_addon_credits_loading: false,
prorated_request_limits_info_mouse_states: Default::default(),
upgrade_link: MouseStateHandle::default(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Thoughts on grouping them into nested structs?

i.e.

struct PlanHeaderMouseHandles {
   ...
}

and put all the structs in there

let bm = &workspace.billing_metadata;
bm.is_on_build_plan()
|| bm.is_on_build_max_plan()
|| bm.is_on_build_business_plan()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this legit?

Image

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants