Skip to content

Add targeted Factories launch feature-intro popover (APP-5648) - #15588

Draft
warp-agent-staging[bot] wants to merge 10 commits into
masterfrom
factory/factories-launch-modal
Draft

Add targeted Factories launch feature-intro popover (APP-5648)#15588
warp-agent-staging[bot] wants to merge 10 commits into
masterfrom
factory/factories-launch-modal

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Targeted Factories launch popover for a narrow, server-managed cohort of existing Warp users (APP-5648). Drives qualified users to a sales/booking flow instead of the PLG /request-access waitlist. Companion server-side change: warpdotdev/warp-server#16154.

Reuses the existing bottom-right, non-blocking FeatureIntroModal (data-driven via FEATURE_INTROS) rather than any of the centered one-time launch modals, per the design direction in the originating thread.

Changes

  • New FeatureIntro entry for the Factories launch, with copy from a designer pass on the earlier captures: a title, a one-paragraph description of what a factory on Warp is, and a separate promotional offer block ("Get hands-on implementation support and up to $10K in Factory usage during Early Access.") with "up to $10K" as the strongest visual emphasis in that line.
  • New FeatureIntroOffer (text + emphasis), an optional field on FeatureIntro. FeatureIntroModal::render_offer renders it as its own accent-tinted, rounded container distinct from the plain description text above it, with emphasis highlighted via Text::with_single_highlight (bold + the theme's accent color) rather than faked with capitalization or symbols. offer: None on CustomModelRouter keeps that entry rendering exactly as before.
  • A follow-up spacing pass reworked the card's whole vertical rhythm (it read tight): badge-to-title 8→12px, heading-group-to-description-to-offer 8→16px each, body padding top/bottom 16/20→20/24px, the offer block's own padding 8/10→12px uniform, footer padding 12→16px, and description/offer line height 1.2→1.4. Every new value reuses spacing already established elsewhere in this crate's launch modals (openwarp_launch_modal, orchestration_launch_modal, auto_handoff_sleep_modal all use 8/16/32-based scales) rather than inventing one-off pixel values, and is gated on offer.is_some() so CustomModelRouter renders pixel-identical to before — nesting its badge+title pair one level deeper doesn't change any gap when every level uses the same 8px spacing. Card height grew from ~408px to ~456px in an 800px window (57%, well under the two-thirds ceiling I was asked to stay under).
  • FeatureIntroModal::render_description splits description on \n and renders each line as its own Text element in a tightly-spaced column (4px gap, not a full paragraph gap); this entry's current copy is a single paragraph and doesn't use it, but the capability stays available for a future multi-line description.
  • The CTA button uses the shared, unmodified PrimaryTheme (app/src/view_components/action_button.rs) — the same theme every other primary button in the app uses, including CustomModelRouter's CTA, which reuses this exact same button view. Its color is the active theme's accent color, not a value chosen for this entry; per gui-ui-guidelines, shared button themes shouldn't be modified unilaterally for one feature. The same accent token is what the offer's emphasized text now uses too, so the two are visually consistent with each other and with the rest of the app.
  • The CTA now opens the server-configured booking URL (User.factoriesLaunchModalCtaUrl) with the signed-in user's email appended as id=, Chili Piper's documented smart parameter for identifying/prefilling a guest on a Round-Robin scheduling link (FeatureIntroModal::with_email_id_prefill). Falls back to the untouched URL when no email is available (anonymous user) or the URL doesn't parse.
  • New FeatureIntro.eligible gate: an intro a user isn't currently targeted for is skipped without consuming its one-time-seen slot, so it can still show later once the user becomes eligible. OneTimeModalModel re-runs this check whenever server experiments update or a higher-priority launch modal / the free-AI-removal notice is dismissed, instead of only at startup.
  • New FeatureFlag::FactoriesLaunchModal, set by a new ServerExperiment (FactoriesLaunchModalControl/Experiment) — enrollment happens entirely server-side via the paired warp-server PR's dedicated admin tooling.
  • FactoriesLaunch eligibility is purely server-driven: the feature flag plus a server-configured CTA URL that is a well-formed absolute https URL and doesn't resolve to a reserved destination — /contact-sales (this validation's own fallback) or /request-access — checked on the normalized path regardless of host, query string, or fragment (UserWorkspaces::has_validated_factories_launch_modal_cta_url). The generic "AI enabled" gate that used to apply to every feature intro now lives only on CustomModelRouter's own eligibility.
  • FeatureIntro.requires_server_claim (true only for FactoriesLaunch): before actually showing such an intro, the client wins an atomic, cross-device claim (AuthClient::claim_feature_intro_impression, backed by the companion server PR) so two devices can't both show it. The globally-synced seen marker is written only once the claim outcome is known — on a win, or a genuine loss to another device — never on a request error, so an offline device or a transient failure retries later instead of permanently losing its only impression. An in-flight claim is tracked so a recheck firing mid-request can't start a second concurrent claim.
  • Added FeatureIntroModal telemetry (Shown/Dismissed/CtaClicked).
  • Included a placeholder gradient hero banner — no marketing asset was provided; a real one should replace it before this ships broadly.

Still needs to be set before enabling for real users

  • The initial cohort. The allowlist ships empty (nobody enrolled), which is the safe default. The real booking URL is now configured (see the companion PR) — enrolling a domain via the admin tooling is the only remaining step to show this to real users.

Verification

  • cargo check -p warp --lib, cargo build -p warp --lib — clean.
  • cargo test -p warp --lib workspace:: server::server_api:: workspaces::user_workspaces:: update_manager:: — all pass (the one unrelated pre-existing failure, ambient_agent_headers_for_task_overrides_existing_cloud_agent_header, fails identically on develop due to no network access to the nsc credential tool in this sandbox).
  • ./script/format and cargo clippy -p warp --lib --tests -- -D warnings (also warp_server_client, warp_graphql) — clean.
  • New/updated unit tests: the offline/network-error claim path, the claim-won and claim-lost-to-another-device paths, the ExperimentsUpdated/higher-priority-dismissal recheck triggers, the CTA-URL validation (malformed values, reserved-destination regression cases for /contact-sales and /request-access with query-string/fragment/case variants), the AI-enabled gate scoped to CustomModelRouter only, and with_email_id_prefill (appends id= to a bare or already-querystring'd URL, leaves the URL untouched for no/empty email or an unparseable URL).
  • Positive-path coverage for the real booking URL (previously only exercised via rejections): a client test asserts has_validated_factories_launch_modal_cta_url() and FactoriesLaunch.eligible both flip true with the exact https://warp-dev.chilipiper.com/round-robin/factories-warp-intro value now configured server-side (see the companion PR for the matching server-side positive-path test and the test that reads the actual shipped YAML configs).
  • Ran gitleaks (v8.30.1) locally against this round's changed files — no leaks.
  • Visual verification: re-captured a full-window screenshot, a close-up, and a flow video after each round of design feedback (copy shortened, then line-broken, then the designer pass with the offer block and emphasis, then the spacing pass) to confirm the PR media reflects the shipped copy and layout, not a superseded version. The spacing pass's close-up specifically confirms the badge no longer sits on the title, the title doesn't run into the body, the offer block's text has comfortable padding within its container, and there are visible gaps between the offer block, the divider, and the CTA button. Also captured the card over a populated terminal and over an active agent conversation (rather than only the empty "New session" pane shown in earlier rounds), since that's closer to what a real user's window looks like and shows the card clipping real content at its right edge.
  • Confirmed FeatureIntro.offer: None on CustomModelRouter is unaffected by the spacing pass by building this PR's merge-base commit in a separate worktree and comparing its CustomModelRouter card against the current branch's: identical title, description, icon, CTA, no offer block, and the same badge/title/description/footer proportions in both.
  • Could not verify the id= email prefill by observing an actual opened browser: this sandbox has no browser or xdg-open handler installed, so ctx.open_url has nowhere to hand off to. Verified the URL-construction logic itself via the unit tests above instead.
  • What did not run in CI: with the PR in draft, only CodeQL and the CLA check run; there is no Rust build/test/clippy CI job configured for draft PRs on this repo. I ran the equivalent commands locally instead (see above).
Computer-use video recordings (1)

Opening the Command Palette, running "[Debug] Open Feature Intro Modal", observing the card's spacing, clicking its "Get Early Access" CTA button, and confirming it disappears.
Factories launch popover flow (spacing pass): Opening the Command Palette, running "[Debug] Open Feature Intro Modal", observing the card with the reworked vertical rhythm, clicking its "Get Early Access" CTA, and confirming it disappears and does not reappear.

Computer-use screenshots (4)

Warp terminal window (1280x800) showing the Factories launch popover card anchored to the bottom-right, overlapping terminal content behind it including git status output, cargo --version output, pwd output, and git log --oneline -5 commit history.
The card over a populated terminal pane (real command output scrolled up behind it), clipping the right edge of that content — not the empty "New session" pane shown in earlier rounds.

Warp window (1280x800) showing the Factories launch popover card overlapping an active agent conversation, with the user prompt "what does the ls command do?" and the agent's full response visible behind it.
The same card overlapping an active agent conversation, clipping the right edge of the agent's response — the surface a lot of users actually live in.

Close-up zoomed view of the Feature Intro Modal popover card showing the NEW badge, title "Build your software factory on Warp", body paragraph, blue offer block with "up to $10K" highlighted, divider line, and "Get Early Access" CTA button with visible spacing between each section.
Close-up confirming the reworked rhythm: badge separated from the title, title separated from the body, the offer block's comfortable internal padding, and visible gaps between the offer block, the divider, and the CTA button.

After clicking the X close button, the feature intro popover card is fully dismissed; the terminal window returns to its normal appearance with an empty "New session" terminal and blinking cursor, with no overlay remaining.
After clicking the close button, the popover is fully dismissed and does not return.

Reuses the existing bottom-right, non-blocking FeatureIntroModal
infrastructure (data-driven via FEATURE_INTROS) to announce Warp
Factories to a narrow, server-managed cohort and drive them to a
sales/booking flow instead of the PLG waitlist.

- New FeatureIntro entry gated by a new eligibility check
  (FeatureIntro.eligible), so an intro a user isn't yet targeted for
  is skipped without consuming its one-time-seen slot. Backed by a
  new FeatureFlag::FactoriesLaunchModal set by the corresponding new
  ServerExperiment (Control/Experiment arms), which the server admin
  tooling can enroll by individual email or company domain with no
  client release (see the paired warp-server change).
- The 'Get Early Access' CTA opens a server-configured booking URL
  (User.factoriesLaunchModalCtaUrl, fetched alongside experiments in
  the workspaces-metadata poll and cached on UserWorkspaces), never
  a client-compiled URL, and never /request-access. Falls back to
  Warp's Contact Sales page until fetched.
- Adds FeatureIntroModal telemetry (Shown/Dismissed/CtaClicked),
  which this popover previously emitted none of.
- Show-once-ever is inherited unchanged from the existing
  cloud-synced seen_feature_intro_ids setting.

Placeholder hero banner included; a real marketing asset should
replace app/assets/async/png/onboarding/factories_launch_intro_banner.png
before this ships broadly.
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

This PR was generated with Warp.

Comment @warp-factory on this PR to send it follow-up work.

View run View conversation View on Slack

…ed CTA, scoped AI gate

- Finding 2: Re-run the feature-intro check when server experiments update
  (subscribe to ServerExperimentsEvent::ExperimentsUpdated) and whenever a
  higher-priority launch modal or the free-AI-removal notice is dismissed
  (maybe_check_and_trigger_feature_intro_modal), so a server-targeted intro
  isn't stuck unseen until the next full app restart.
- Finding 3: FactoriesLaunch now requires winning an atomic, cross-device
  server claim (AuthClient::claim_feature_intro_impression) before it is
  actually shown. The seen marker is still set synchronously so it only
  shows at most once per device even while the claim is in flight; a lost
  claim resumes the lower-priority modal chain instead.
- Finding 4: UserWorkspaces::has_validated_factories_launch_modal_cta_url()
  fails closed until the server delivers a real (non-fallback) CTA URL;
  FactoriesLaunch's eligibility now requires it alongside the feature flag.
- Finding 5: Removed the blanket is_any_ai_enabled gate from the shared
  feature-intro check. It now lives only on CustomModelRouter's own
  eligibility; FactoriesLaunch is purely server-driven.

Adds unit tests for the recheck triggers, the server-claim gating (mocked
AuthClient), and the CTA-URL / AI-enabled eligibility gates.
…im, strict CTA URL validation

- Finding 1: The globally-synced seen marker for a requires_server_claim
  intro is now written only once the claim outcome is known (won, or
  genuinely lost to another device via Ok(false)), never on a request
  error. A network failure or offline device no longer permanently
  suppresses the modal everywhere; it simply retries on the next recheck.
  Tracks the in-flight claim (pending_claim_intro_id) so a recheck that
  fires mid-request doesn't start a second concurrent claim. Adds a
  regression test for the error/offline path plus a successful retry.
- Finding 2: UserWorkspaces::has_validated_factories_launch_modal_cta_url
  now requires a well-formed absolute https URL with a non-empty host, and
  rejects the Contact Sales fallback even with a trailing slash or
  surrounding whitespace, instead of a weak byte-inequality check in front
  of ctx.open_url. Covered by new empty/malformed/fallback-variant test
  cases.
…est-access)

Round 3 review: the CTA URL gate compared only against the exact Contact
Sales URL with query equality required, so query-string variants of
Contact Sales and /request-access outright slipped through. Replace the
byte-comparison against a single fallback constant with a reserved-path
check on the normalized (trailing slash trimmed, lowercased) path,
independent of host, query string, or fragment. Also trims the doc
comment on the validation helper down to its own behavior instead of
naming its caller.
No client-side behavior change (trim_end_matches('/') already strips
every trailing slash); adds //-suffixed cases to the reserved-path test
tables to match the server-side fix.

@warp-agent-staging warp-agent-staging Bot left a comment

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.

Overview

Adds the targeted Factories launch modal (bottom-right feature intro, server-gated cohort, once-ever impression claim, view/click/dismiss telemetry) across this PR and warpdotdev/warp-server#16154. Implementation and review are complete; posting the items that need a human decision rather than an approval or rejection.

Concerns

  • The booking URL is not set, and nothing ships until it is. No booking link was supplied and there is no Calendly/Chili-Piper integration in either repo, so the CTA destination is a server-configured value (features.factories_launch_modal_cta_url) and eligibility fails closed against the Contact Sales fallback and /request-access. The modal shows to nobody until a real https booking URL is configured — decide the destination.
  • The initial cohort is not loaded. The ~325 paid domains live in a Google Sheet outside this repo, so the allowlist ships empty. Load them through the new admin page in the server PR when the list is ready.
  • Lead recording and booked-call attribution are deferred. CTA clicks are logged with the authenticated user ID, but no Factory lead is created in a sales-actionable system and no booking attribution exists. Decide the lead sink and whether booked-call tracking lands before launch or after.
  • Neither repo runs build/test coverage on an ordinary PR. warp-server skips those jobs until merge queue (.github/workflows/ci.yml:104-105,138-139,153-154) and this repo has no Rust build/test workflow, so all suites were run locally. The new atomicity integration test executes for the first time in merge-queue CI.

Verdict

Checks: build pass (local), tests pass (local), CI green on what executes, visual proof present

Found: 0 critical, 0 important, 0 suggestions, 0 nits — all 11 review findings from four rounds are resolved; the above are decisions, not defects.

Responding as wilson: Open session · View in factory

Requester feedback: the body text was too long for the 340px card. Cut
it to roughly half while keeping what a factory on Warp is and the
Early Access incentive (hands-on implementation support + up to $10K
in Factory usage).
Requester feedback: the two ideas in the body copy (what a factory on
Warp is, and the Early Access incentive) ran together into a single
paragraph. Render description as separate lines split on '\n' rather
than relying on the text-shaping engine to honor an embedded newline
within one text run, so each line renders in a tightly-spaced column
with explicit, deterministic line breaks.
…er email

Design pass (requester copy):
- New body copy and a distinct promotional offer block (FeatureIntroOffer)
  rendered as an accent-tinted rounded container below the description,
  with "up to $10K" highlighted in bold, accent-colored text via
  Text::with_single_highlight rather than faked emphasis.
- More vertical space between the offer block and the footer divider,
  applied only when an offer is present so CustomModelRouter (offer: None)
  renders exactly as before.

Real booking URL:
- The CTA now appends the signed-in user's email as Chili Piper's
  documented id= smart parameter for Round-Robin scheduling links, so the
  booking form prefills instead of asking again. Falls back to the
  untouched URL when no email is available or the URL doesn't parse.
- Added a positive-path test with the exact real booking URL, since the
  eligibility gate had only ever been exercised against rejections.
with_email_id_prefill appended id= unconditionally, so a configured CTA
URL that already carried an id pair ended up with two, leaving it
ambiguous (and consumer-dependent) which one identified the guest.
Strip any existing id pair before appending the signed-in user's email,
preserving every other query pair.

@warp-agent-staging warp-agent-staging Bot left a comment

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.

Overview

Design pass, the real Chili Piper booking URL, and CTA email prefill are in; the implement-review loop is closed. Posting the decisions that are yours, superseding the booking-URL item from my earlier review.

Concerns

  • The CTA discloses a signed-in user's email to a third party in a URL. The prefill appends id=<email> so users land on the booking form filled in, which is what was asked for, but that address becomes URL data subject to browser history and Chili Piper's request, logging and analytics chain. Nothing in this change establishes that the disclosure and its retention are approved for this use — get an explicit privacy call, or switch to an opaque one-time token or a vendor-side integration.
  • The prefilled flow has never been opened in a real browser. The sandbox has no browser, so unit tests prove the URL is constructed correctly but nothing proves Chili Piper honours id or that the form actually prefills. Someone should click it once on staging before this goes to real users.
  • The card grew from ~342px to ~408px, about +19%. That is the requested longer copy plus the offer block's chrome, and it is roughly half the window height. Compact was a stated constraint, so confirm the new footprint is acceptable or say what should give.
  • Still unresolved from before: the ~325 paid domains are not loaded, so the allowlist is empty and this shows to nobody; and CTA clicks are telemetried with the user ID but no sales-actionable lead record or booked-call attribution exists.

Verdict

Checks: build pass (local), tests pass (local), CI green on what executes, visual proof present

Found: 0 critical, 0 important, 0 suggestions, 0 nits — every review finding across five rounds is resolved. The above are decisions and outstanding validation, not defects. Confirmed safe to merge without exposure: the config change adds no domain, email, or default arm, so the modal cannot reach anyone while the allowlist stays empty.

Responding as wilson: Open session · View in factory

Requester feedback: the card read tight vertically. Reworked the whole
stack rather than the two gaps named previously:
- badge-to-title: 8px -> 12px
- title-group-to-description-to-offer: 8px -> 16px each
- body container top/bottom padding: 16/20px -> 20/24px
- offer block's own padding: 8/10px -> 12px uniform
- footer vertical padding: 12px -> 16px
- description/offer line height: 1.2 -> 1.4

Every new value is gated on FeatureIntro.offer being Some, and reuses
spacing already established elsewhere in this crate's launch modals
(openwarp_launch_modal, orchestration_launch_modal,
auto_handoff_sleep_modal all use 8/16/32-based scales), rather than
inventing one-off pixel values. CustomModelRouter (offer: None) renders
pixel-identical to before: nesting its badge+title pair one level deeper
doesn't change any gap when every level uses the same 8px spacing.

Card height grows from ~408px to ~456px in an 800px window (57%), well
under the two-thirds ceiling.
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.

0 participants