warpdotdev/warp · Archived

gui-create-launch-modal

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.

Installation

$ npx skills add warpdotdev/warp --skill gui-create-launch-modal

Summary

  • 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.

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from warpdotdev/warp · top by installs.

npx skills add warpdotdev/warp

Browse all from warpdotdev/warp

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 64.9K
License LICENSE-AGPL
Default branch master
Open issues 3,847
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 12,506 B
  • docs SUMMARY.md 320 B

History

  1. First recorded snapshot · 1 installs

SKILL.md

gui-create-launch-modal

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/warptui; cell-grid TuiElement library under crates/warpuicore/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."

Reference implementation

app/src/workspace/view/orchestrationlaunchmodal/ — the canonical, most up-to-date example of this pattern.

Checklist

  • Feature flag in warp_features/src/lib.rs
  • Settings field in app/src/settings/ai.rs
  • Trigger logic in app/src/workspace/onetimemodal_model.rs
  • View files under app/src/workspace/view/<name>launchmodal/
  • Workspace wiring in app/src/workspace/view.rs and app/src/workspace/mod.rs
  • Debug actions in app/src/workspace/action.rs
  • Hero image at app/assets/async/png/onboarding/<name>launchbanner.png
  • Any custom icons added to crates/warp_core/src/ui/icons.rs + SVG in app/assets/bundled/svg/

Step 0 – Custom icons (if needed)

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:

// 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.


Step 1 – Feature flag

Add to crates/warp_features/src/lib.rs:

/// Enables the <name> launch modal.
<YourModalName>LaunchModal,

Enable for dogfood:

pub const DOGFOOD_FLAGS: &[FeatureFlag] = &[
    FeatureFlag::<YourModalName>LaunchModal,
    // ...
];

Step 2 – Settings field

Add to app/src/settings/ai.rs inside definesettingsgroup!(AISettings, ...). Pattern: one boolean field per modal, globally synced (not respecting user sync), private.

// 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,
}

Step 3 – OneTimeModalModel

File: app/src/workspace/onetimemodal_model.rs

3a. Add field to struct

is_<name>_launch_modal_open: bool,

3b. Initialize to false in new()

is_<name>_launch_modal_open: false,

3c. Pre-dismiss for new users (critical)

In the AuthComplete!isexistinguser 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.

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}");
}

3d. Public API methods

pub 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);
}

3e. Private setter

fn 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
}

3f. Add to isanymodal_open

|| self.is_<name>_launch_modal_open

3g. Trigger function

fn 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
}

3h. Call from checkandtriggerallmodals

Insert before checkandtriggerhoaonboarding:

if self.check_and_trigger_<name>_launch_modal(ctx) {
    return;
}

Step 4 – View

Create app/src/workspace/view/<name>launchmodal/mod.rs:

mod view;
pub use view::{init, <Name>LaunchModal, <Name>LaunchModalEvent};

Create app/src/workspace/view/<name>launchmodal/view.rs. Copy from orchestrationlaunchmodal/view.rs and adapt. Key details:

Color sources (important)

  • Prefer Warp theme tokens for modal backgrounds, text, overlays, and borders:

- background surfaces: appearance.theme().surface3() (or another surface* token when needed) - primary/subtext: appearance.theme().maintextcolor(...) and appearance.theme().subtextcolor(...) - overlays/hover fills: appearance.theme().surfaceoverlay1() / surfaceoverlay2() - subtle borders: appearance.theme().outline()

  • Use terminal theme colors for terminal-color accents (for example, magenta launch badge accents):

- appearance.theme().terminalcolors().normal.magenta - appearance.theme().ansioverlay_1(magenta) for low-alpha backgrounds

  • Avoid hardcoded hex colors.

Hero image

  • Store at app/assets/async/png/onboarding/<name>launchbanner.png
  • Aspect ratio matters: if the image is wider than MODALWIDTH/HEROHEIGHT (420/92 ≈ 4.57), wrap the hero ConstrainedBox in Clipped::new(...) to prevent horizontal bleed when cover() scales it
  • Images pre-sized to exactly 420×92 need no Clipped; images only taller (aspect ratio < 4.57) are fine without it
const 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
}

"New" badge

Use the standard badge — 24 px tall, 8 px horizontal padding, 14 px font, pill corners, with magenta sourced from terminal theme colors:

fn 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()
}

URLs

Always use https://, not http://:

const LEARN_MORE_URL: &str = "https://warp.dev/your-blog-link";

Step 5 – Workspace wiring

app/src/workspace/view.rs

// 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:

fn 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.rs

// 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")),

Step 6 – Debug actions

In app/src/workspace/action.rs:

/// 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 isvisibleincommandpalette false arm.

In app/src/workspace/view.rs TypedActionView::handle_action:

#[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}");
        }
    });
}

Behavior summary

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