Skip to main content

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.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
warpdotdev/warp
آخر نشاط في المصدر
١٠ يوليو ٢٠٢٦ في ٢٣:٢٣
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٦٥٬٠٦٤
التفرعات
٥٬٥٦٣

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
gui-create-launch-modal
description
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.
# 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);
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub