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

- Skill: `liueggy/gui-create-launch-modal` (Agent Skill)
- Install (CLI): `npx skillmds@latest add liueggy/gui-create-launch-modal`
- Raw SKILL.md: https://api.skillmd.com/api/skills/liueggy/gui-create-launch-modal/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: liueggy (https://skillmd.com/u/liueggy)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/liueggy/gui-create-launch-modal

---


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

## Reference implementation

`app/src/workspace/view/orchestration_launch_modal/` — 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/one_time_modal_model.rs`
- [ ] View files under `app/src/workspace/view/<name>_launch_modal/`
- [ ] 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>_launch_banner.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`:

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

---

## Step 1 – Feature flag

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

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

Enable for dogfood:

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

---

## Step 2 – Settings field

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

---

## Step 3 – OneTimeModalModel

File: `app/src/workspace/one_time_modal_model.rs`

### 3a. Add field to struct

```rust
is_<name>_launch_modal_open: bool,
```

### 3b. Initialize to false in `new()`

```rust
is_<name>_launch_modal_open: false,
```

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

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

```rust
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

```rust
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

```rust
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 `is_any_modal_open`

```rust
|| self.is_<name>_launch_modal_open
```

### 3g. Trigger function

```rust
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 `check_and_trigger_all_modals`

Insert before `check_and_trigger_hoa_onboarding`:

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

---

## Step 4 – View

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

```rust
mod 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:

### Color sources (important)

- Prefer Warp theme tokens for modal backgrounds, text, overlays, and borders:
  - background surfaces: `appearance.theme().surface_3()` (or another `surface_*` token when needed)
  - primary/subtext: `appearance.theme().main_text_color(...)` and `appearance.theme().sub_text_color(...)`
  - overlays/hover fills: `appearance.theme().surface_overlay_1()` / `surface_overlay_2()`
  - subtle borders: `appearance.theme().outline()`
- Use terminal theme colors for terminal-color accents (for example, magenta launch badge accents):
  - `appearance.theme().terminal_colors().normal.magenta`
  - `appearance.theme().ansi_overlay_1(magenta)` for low-alpha backgrounds
- Avoid hardcoded hex colors.

### Hero image

- Store at `app/assets/async/png/onboarding/<name>_launch_banner.png`
- **Aspect ratio matters**: if the image is wider than `MODAL_WIDTH/HERO_HEIGHT` (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

```rust
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:

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

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

---

## Step 5 – Workspace wiring

### `app/src/workspace/view.rs`

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

```rust
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`

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

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

---

## 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 |

