[3/n] Billing & usage dispatcher refactor#11118
Conversation
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>
61b51c6 to
6a51a86
Compare
| addon_credit_denomination_buttons: Default::default(), | ||
| purchase_addon_credits_loading: false, | ||
| prorated_request_limits_info_mouse_states: Default::default(), | ||
| upgrade_link: MouseStateHandle::default(), |
There was a problem hiding this comment.
wow... thats a bunch
There was a problem hiding this comment.
nit: Thoughts on grouping them into nested structs?
i.e.
struct PlanHeaderMouseHandles {
...
}
and put all the structs in there
|
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 Powered by Oz |
There was a problem hiding this comment.
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
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>
…t users can still see cta
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>
9c8027b to
622b73d
Compare
tylerlam-warp
left a comment
There was a problem hiding this comment.
This all looks good, I found a small bug. Also have some questions
| 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() |
There was a problem hiding this comment.
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
| .current_workspace() | ||
| .is_some_and(|workspace| { |
There was a problem hiding this comment.
What happens if there is no current workspace? I guess we'll default to the old page? Should be fine?
| 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() | ||
| } | ||
| } |
There was a problem hiding this comment.
Oh I think you'll need to deal with it here: https://github.com/warpdotdev/warp/pull/11118/changes#diff-71ee90ed716843f228033fa5cedbb0fa7d86a36d27729eb6a7e458e78c7eb982R29
| addon_credit_denomination_buttons: Default::default(), | ||
| purchase_addon_credits_loading: false, | ||
| prorated_request_limits_info_mouse_states: Default::default(), | ||
| upgrade_link: MouseStateHandle::default(), |
There was a problem hiding this comment.
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() |


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.