Install any skill in seconds. Free to start, no credit card required.
Get Started Free →GUI desktop app only. Create a one-time launch modal in the Warp client (feature announcement, onboarding, etc.). Use when adding a new modal that should appear exactly once per user on startup, gated by a feature flag, with colors sourced from Warp theme tokens and terminal theme colors.
.claude/skills/warpdotdev-gui-create-launch-modal/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 113% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 78% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 143% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 108% | 0% |
Scope — GUI desktop app only. This skill applies to Warp's GUI desktop front-end (the app/ crate on the WarpUI pixel/GPU framework). It does not apply to the headless TUI front-end (crates/warp_tui; cell-grid TuiElement library under crates/warpui_core/src/elements/tui), which has its own components, tests, and change-verification workflow. For TUI work, see the tui-ui-guidelines, tui-testing, and tui-verify-change skills instead.
Create a one-time launch modal — the feature-announcement design used for launches like "Orchestrate any agent, anywhere" or "Warp is now open-source."
app/src/workspace/view/orchestration_launch_modal/ — the canonical, most up-to-date example of this pattern.
warp_features/src/lib.rsapp/src/settings/ai.rsapp/src/workspace/one_time_modal_model.rsapp/src/workspace/view/<name>_launch_modal/app/src/workspace/view.rs and app/src/workspace/mod.rsapp/src/workspace/action.rsapp/assets/async/png/onboarding/<name>_launch_banner.pngcrates/warp_core/src/ui/icons.rs + SVG in app/assets/bundled/svg/If the modal uses icons not yet in the Icon enum, add them before writing the view.
In crates/warp_core/src/ui/icons.rs:
rust// Add to enum YourIconName, // Add to From<Icon> for &'static str match Icon::YourIconName => "bundled/svg/your-icon-name.svg",
Drop the SVG file at app/assets/bundled/svg/your-icon-name.svg. Use the same 24×24 viewBox format as existing icons.
Add to crates/warp_features/src/lib.rs:
rust/// Enables the <name> launch modal. <YourModalName>LaunchModal,
Enable for dogfood:
rustpub const DOGFOOD_FLAGS: &[FeatureFlag] = &[ FeatureFlag::<YourModalName>LaunchModal, // ... ];
Add to app/src/settings/ai.rs inside define_settings_group!(AISettings, ...). Pattern: one boolean field per modal, globally synced (not respecting user sync), private.
rust// This is not a user-visible setting - it's merely a one-time flag to track if the // <name> launch modal has been shown to the user. // // We model it as a setting so it's only shown once to a given user regardless of the number of // devices they use. did_check_to_trigger_<name>_launch_modal: DidShow<Name>LaunchModal { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: true, }
File: app/src/workspace/one_time_modal_model.rs
rustis_<name>_launch_modal_open: bool,
new()rustis_<name>_launch_modal_open: false,
In the AuthComplete → !is_existing_user branch, add to the AISettings::handle update block alongside the other pre-dismissals. Without this, new users see the modal on their second startup after onboarding.
rustif let Err(e) = settings .did_check_to_trigger_<name>_launch_modal .set_value(true, ctx) { log::warn!("Failed to mark <name> launch modal as dismissed: {e}"); }
rustpub fn is_<name>_launch_modal_open(&self) -> bool { self.is_<name>_launch_modal_open && self.target_window_id.is_some() } pub fn mark_<name>_launch_modal_dismissed(&mut self, ctx: &mut ModelContext<Self>) { self.set_<name>_launch_modal_open(false, ctx); } #[cfg(debug_assertions)] pub fn force_open_<name>_launch_modal(&mut self, ctx: &mut ModelContext<Self>) { self.set_<name>_launch_modal_open(true, ctx); }
rustfn set_<name>_launch_modal_open(&mut self, is_open: bool, ctx: &mut ModelContext<Self>) -> bool { if self.is_<name>_launch_modal_open != is_open { self.is_<name>_launch_modal_open = is_open; ctx.emit(OneTimeModalEvent::VisibilityChanged { is_open }); return true; } false }
is_any_modal_openrust|| self.is_<name>_launch_modal_open
rustfn check_and_trigger_<name>_launch_modal(&mut self, ctx: &mut ModelContext<Self>) -> bool { if !FeatureFlag::<Name>LaunchModal.is_enabled() { return false; } let ai_settings = AISettings::as_ref(ctx); if *ai_settings.did_check_to_trigger_<name>_launch_modal { return false; } AISettings::handle(ctx).update(ctx, |settings, ctx| { if let Err(e) = settings .did_check_to_trigger_<name>_launch_modal .set_value(true, ctx) { log::warn!("Failed to mark <name> launch modal as dismissed: {e}"); } }); let should_show = !matches!(ChannelState::channel(), Channel::Integration); self.set_<name>_launch_modal_open(should_show, ctx); should_show }
check_and_trigger_all_modalsInsert before check_and_trigger_hoa_onboarding:
rustif self.check_and_trigger_<name>_launch_modal(ctx) { return; }
Create app/src/workspace/view/<name>_launch_modal/mod.rs:
rustmod view; pub use view::{init, <Name>LaunchModal, <Name>LaunchModalEvent};
Create app/src/workspace/view/<name>_launch_modal/view.rs. Copy from orchestration_launch_modal/view.rs and adapt. Key details:
appearance.theme().surface_3() (or another surface_* token when needed)appearance.theme().main_text_color(...) and appearance.theme().sub_text_color(...)appearance.theme().surface_overlay_1() / surface_overlay_2()appearance.theme().outline()appearance.theme().terminal_colors().normal.magentaappearance.theme().ansi_overlay_1(magenta) for low-alpha backgroundsapp/assets/async/png/onboarding/<name>_launch_banner.pngMODAL_WIDTH/HERO_HEIGHT (420/92 ≈ 4.57), wrap the hero ConstrainedBox in Clipped::new(...) to prevent horizontal bleed when cover() scales itClipped; images only taller (aspect ratio < 4.57) are fine without itrustconst MODAL_WIDTH: f32 = 420.; const HERO_HEIGHT: f32 = 92.; const HERO_IMAGE_PATH: &str = "async/png/onboarding/<name>_launch_banner.png"; fn render_hero(&self) -> Box<dyn Element> { let hero = Clipped::new( // only needed if image ratio > 4.57 ConstrainedBox::new( Image::new(AssetSource::Bundled { path: HERO_IMAGE_PATH }, CacheOption::Original) .with_corner_radius(CornerRadius::with_top(Radius::Pixels(8.))) .cover() .top_aligned() .finish(), ) .with_width(MODAL_WIDTH) .with_height(HERO_HEIGHT) .finish(), ) .finish(); // ... close button overlay via Stack + add_positioned_child }
Use the standard badge — 24 px tall, 8 px horizontal padding, 14 px font, pill corners, with magenta sourced from terminal theme colors:
rustfn render_badge(appearance: &Appearance) -> Box<dyn Element> { let magenta = appearance.theme().terminal_colors().normal.magenta; let text = Text::new_inline("New".to_string(), appearance.ui_font_family(), 14.) .with_color(magenta.into()) .finish(); ConstrainedBox::new( Container::new( Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_main_axis_size(MainAxisSize::Min) .with_child(text) .finish(), ) .with_horizontal_padding(8.) .with_background(Fill::Solid(appearance.theme().ansi_overlay_1(magenta))) .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.))) .finish(), ) .with_height(24.) .finish() }
Always use https://, not http://:
rustconst LEARN_MORE_URL: &str = "https://warp.dev/your-blog-link";
app/src/workspace/view.rsrust// Module declaration (top) pub(crate) mod <name>_launch_modal; // Import use crate::workspace::view::<name>_launch_modal::{<Name>LaunchModal, <Name>LaunchModalEvent}; // Struct field <name>_launch_modal: ViewHandle<<Name>LaunchModal>, // In Workspace::new() let <name>_launch_view = ctx.add_typed_action_view(<Name>LaunchModal::new); ctx.subscribe_to_view(&<name>_launch_view, |me, _, event, ctx| { me.handle_<name>_launch_modal_event(event, ctx); }); // In struct initialization <name>_launch_modal: <name>_launch_view, // In OneTimeModalModel subscription handler } else if model_ref.is_<name>_launch_modal_open() { me.focus_<name>_launch_modal(ctx); // In View::render (inside the should_show_modal block) if should_show_modal && one_time_modal_model.is_<name>_launch_modal_open() { stack.add_child(ChildView::new(&self.<name>_launch_modal).finish()); }
Add event handler and focus helper:
rustfn handle_<name>_launch_modal_event(&mut self, event: &<Name>LaunchModalEvent, ctx: &mut ViewContext<Self>) { match event { <Name>LaunchModalEvent::Close => { OneTimeModalModel::handle(ctx).update(ctx, |model, ctx| { model.mark_<name>_launch_modal_dismissed(ctx); }); self.focus_active_tab(ctx); ctx.notify(); } } } fn focus_<name>_launch_modal(&mut self, ctx: &mut ViewContext<Self>) { ctx.focus(&self.<name>_launch_modal); }
app/src/workspace/mod.rsrust// In pub fn init() view::<name>_launch_modal::init(app); // In debug bindings block EditableBinding::new( "workspace:open_<name>_launch_modal", "[Debug] Open <Name> Launch Modal", WorkspaceAction::Open<Name>LaunchModal, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:reset_<name>_launch_modal_state", "[Debug] Reset <Name> Launch Modal State", WorkspaceAction::Reset<Name>LaunchModalState, ) .with_context_predicate(id!("Workspace")),
In app/src/workspace/action.rs:
rust/// Open the <Name> Launch Modal (for debugging) #[cfg(debug_assertions)] Open<Name>LaunchModal, /// Reset the <name> launch modal dismissed state (for debugging) #[cfg(debug_assertions)] Reset<Name>LaunchModalState,
Add both variants to the is_visible_in_command_palette false arm.
In app/src/workspace/view.rs TypedActionView::handle_action:
rust#[cfg(debug_assertions)] Open<Name>LaunchModal => { OneTimeModalModel::handle(ctx).update(ctx, |model, ctx| { model.force_open_<name>_launch_modal(ctx); }); ctx.notify(); } #[cfg(debug_assertions)] Reset<Name>LaunchModalState => { AISettings::handle(ctx).update(ctx, |settings, ctx| { if let Err(e) = settings .did_check_to_trigger_<name>_launch_modal .set_value(false, ctx) { log::warn!("Failed to reset <name> launch modal state: {e}"); } }); }
| User type | Sees modal? | |---|---| | New signup | No — pre-dismissed in AuthComplete new-user branch | | Not signed in | No — trigger never fires without AuthComplete | | Existing user, flag enabled | Yes — on first startup after cloud prefs load | | Integration channel | No — suppressed by Channel::Integration check | | Already seen it | No — setting persists globally across devices |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 23,971 | 7,030 | -71% | 1 | 1 | 0% | 4,703 | 3,962 | -16% | 0 | 0 | — |
case-02 | fail→pass | 25,077 | 40,737 | +62% | 1 | 1 | 0% | 4,828 | 10,280 | +113% | 0 | 0 | — |
case-03 | fail→fail | 20,481 | 25,285 | +23% | 1 | 1 | 0% | 4,100 | 9,308 | +127% | 0 | 0 | — |
case-04 | pass→fail | 16,014 | 7,013 | -56% | 1 | 1 | 0% | 2,790 | 4,814 | +73% | 0 | 0 | — |
case-05 | pass→pass | 17,754 | 8,421 | -53% | 1 | 1 | 0% | 3,262 | 5,088 | +56% | 0 | 0 | — |
case-06 | pass→pass | 13,185 | 11,140 | -16% | 1 | 1 | 0% | 2,069 | 5,358 | +159% | 0 | 0 | — |
case-07 | fail→pass | 14,863 | 5,971 | -60% | 1 | 1 | 0% | 2,593 | 4,613 | +78% | 0 | 0 | — |
case-08 | fail→pass | 15,018 | 4,317 | -71% | 1 | 1 | 0% | 2,424 | 4,448 | +83% | 0 | 0 | — |
case-09 | fail→pass | 12,489 | 5,901 | -53% | 1 | 1 | 0% | 1,949 | 4,736 | +143% | 0 | 0 | — |
case-10 | pass→pass | 24,694 | 8,145 | -67% | 1 | 1 | 0% | 2,119 | 5,122 | +142% | 0 | 0 | — |
case-11 | fail→pass | 13,722 | 4,506 | -67% | 1 | 1 | 0% | 2,142 | 4,448 | +108% | 0 | 0 | — |
case-12 | fail→pass | 13,574 | 5,068 | -63% | 1 | 1 | 0% | 2,032 | 4,547 | +124% | 0 | 0 | — |
case-13 | fail→pass | 11,878 | 3,862 | -67% | 1 | 1 | 0% | 1,591 | 4,286 | +169% | 0 | 0 | — |
case-14 | fail→pass | 15,005 | 7,134 | -52% | 1 | 1 | 0% | 2,473 | 5,184 | +110% | 0 | 0 | — |
case-15 | fail→pass | 13,501 | 5,903 | -56% | 1 | 1 | 0% | 2,078 | 4,799 | +131% | 0 | 0 | — |
case-16 | pass→pass | 7,778 | 3,342 | -57% | 1 | 1 | 0% | 1,137 | 4,029 | +254% | 0 | 0 | — |
case-17 | fail→pass | 11,232 | 4,287 | -62% | 1 | 1 | 0% | 1,733 | 4,469 | +158% | 0 | 0 | — |
case-18 | fail→pass | 12,608 | 4,905 | -61% | 1 | 1 | 0% | 2,158 | 4,642 | +115% | 0 | 0 | — |
case-19 | fail→pass | 13,728 | 5,230 | -62% | 1 | 1 | 0% | 1,892 | 4,651 | +146% | 0 | 0 | — |
case-20 | fail→pass | 26,734 | 4,317 | -84% | 1 | 1 | 0% | 1,935 | 4,414 | +128% | 0 | 0 | — |
case-21 | fail→pass | 14,138 | 7,193 | -49% | 1 | 1 | 0% | 2,207 | 5,003 | +127% | 0 | 0 | — |
case-22 | fail→fail | 11,706 | 4,059 | -65% | 1 | 1 | 0% | 1,794 | 4,216 | +135% | 0 | 0 | — |
case-23 | fail→pass | 12,670 | 5,912 | -53% | 1 | 1 | 0% | 1,927 | 4,536 | +135% | 0 | 0 | — |
case-24 | fail→pass | 9,815 | 3,708 | -62% | 1 | 1 | 0% | 1,444 | 4,173 | +189% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted, and 23 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +63 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.