From 646198a50df548bf5205f9c5bc1cca46b49ab385 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 27 May 2026 15:09:52 -0300 Subject: [PATCH] skill_creator: Use the status toast for confirming creation (#57855) This PR uses the status toast for the Skill Creator confirming action as opposed the regular message notification. Aside from it looking a bit better, it's also auto-dismissed, which is preferred in this case. Release Notes: - Improved skill creation toast confirmation by making it auto-dismissed. --------- Co-authored-by: Martin Ye --- Cargo.lock | 1 + assets/keymaps/default-linux.json | 2 + assets/keymaps/default-macos.json | 2 + assets/keymaps/default-windows.json | 2 + crates/skill_creator/Cargo.toml | 1 + crates/skill_creator/src/skill_creator.rs | 144 +++++++++++++--------- 6 files changed, 93 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0ced45dfc..18e1162b1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17229,6 +17229,7 @@ dependencies = [ "http_client", "language", "menu", + "notifications", "platform_title_bar", "release_channel", "serde_json", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index c66d20f13f..ba1ed2fcd8 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1563,6 +1563,7 @@ "context": "SkillCreator", "bindings": { "ctrl-w": "workspace::CloseWindow", + "ctrl-enter": "skill_creator::SaveSkill", "tab": "skill_creator::FocusNextField", "shift-tab": "skill_creator::FocusPreviousField", }, @@ -1571,6 +1572,7 @@ "context": "SkillCreator > Editor", "bindings": { "ctrl-w": "workspace::CloseWindow", + "ctrl-enter": "skill_creator::SaveSkill", "tab": "skill_creator::FocusNextField", "shift-tab": "skill_creator::FocusPreviousField", }, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 61e719d712..efb2ecd01e 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1657,6 +1657,7 @@ "use_key_equivalents": true, "bindings": { "cmd-w": "workspace::CloseWindow", + "cmd-enter": "skill_creator::SaveSkill", "tab": "skill_creator::FocusNextField", "shift-tab": "skill_creator::FocusPreviousField", }, @@ -1666,6 +1667,7 @@ "use_key_equivalents": true, "bindings": { "cmd-w": "workspace::CloseWindow", + "cmd-enter": "skill_creator::SaveSkill", "tab": "skill_creator::FocusNextField", "shift-tab": "skill_creator::FocusPreviousField", }, diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 56a3bc4972..ce8b775229 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -1583,6 +1583,7 @@ "use_key_equivalents": true, "bindings": { "ctrl-w": "workspace::CloseWindow", + "ctrl-enter": "skill_creator::SaveSkill", "tab": "skill_creator::FocusNextField", "shift-tab": "skill_creator::FocusPreviousField", }, @@ -1592,6 +1593,7 @@ "use_key_equivalents": true, "bindings": { "ctrl-w": "workspace::CloseWindow", + "ctrl-enter": "skill_creator::SaveSkill", "tab": "skill_creator::FocusNextField", "shift-tab": "skill_creator::FocusPreviousField", }, diff --git a/crates/skill_creator/Cargo.toml b/crates/skill_creator/Cargo.toml index 1bda7bfb6e..8512c4d50b 100644 --- a/crates/skill_creator/Cargo.toml +++ b/crates/skill_creator/Cargo.toml @@ -21,6 +21,7 @@ gpui.workspace = true http_client.workspace = true language.workspace = true menu.workspace = true +notifications.workspace = true platform_title_bar.workspace = true release_channel.workspace = true serde_yaml_ng.workspace = true diff --git a/crates/skill_creator/src/skill_creator.rs b/crates/skill_creator/src/skill_creator.rs index 599764e4c7..3ed3ee49b3 100644 --- a/crates/skill_creator/src/skill_creator.rs +++ b/crates/skill_creator/src/skill_creator.rs @@ -13,6 +13,7 @@ use gpui::{ }; use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request, StatusCode, Url}; use language::{Buffer, LanguageRegistry, language_settings::SoftWrap}; +use notifications::status_toast::StatusToast; use platform_title_bar::PlatformTitleBar; use release_channel::ReleaseChannel; use settings::{ActionSequence, Settings}; @@ -22,14 +23,12 @@ use std::sync::Arc; use std::time::Duration; use theme_settings::ThemeSettings; use ui::{ - ContextMenu, Divider, DropdownMenu, DropdownStyle, Headline, HeadlineSize, SwitchField, + Banner, ContextMenu, Divider, DropdownMenu, DropdownStyle, Headline, HeadlineSize, SwitchField, WithScrollbar, prelude::*, }; use ui_input::{ErasedEditorEvent, InputField}; use util::ResultExt; -use workspace::{ - Toast, Workspace, WorkspaceSettings, client_side_decorations, notifications::NotificationId, -}; +use workspace::{Workspace, WorkspaceSettings, client_side_decorations}; use worktree::WorktreeId; actions!( @@ -43,6 +42,8 @@ const DESCRIPTION_FIELD_TAB_INDEX: isize = 3; const DISABLE_MODEL_INVOCATION_TAB_INDEX: isize = 4; const SCOPE_FIELD_TAB_INDEX: isize = 5; const BODY_FIELD_TAB_INDEX: isize = 6; +const CANCEL_BUTTON_TAB_INDEX: isize = 7; +const SAVE_BUTTON_TAB_INDEX: isize = 8; const URL_IMPORT_DEBOUNCE: Duration = Duration::from_millis(100); const URL_IMPORT_ERROR_BODY_MAX_LEN: usize = 2048; @@ -83,13 +84,6 @@ enum ScopeChoice { } impl ScopeChoice { - fn label(&self) -> SharedString { - match self { - ScopeChoice::Global => "Global".into(), - ScopeChoice::Project { root_name, .. } => root_name.clone(), - } - } - fn key(&self) -> SharedString { match self { ScopeChoice::Global => "global".into(), @@ -256,6 +250,8 @@ pub struct SkillCreator { // Held so replacing it or switching back to the form cancels an in-flight import. url_import_task: Option>, scroll_handle: ScrollHandle, + cancel_button_focus_handle: FocusHandle, + save_button_focus_handle: FocusHandle, _subscriptions: Vec, } @@ -432,6 +428,8 @@ impl SkillCreator { url_import_debounce_task: None, url_import_task: None, scroll_handle: ScrollHandle::new(), + cancel_button_focus_handle: cx.focus_handle(), + save_button_focus_handle: cx.focus_handle(), _subscriptions: subscriptions, } } @@ -728,7 +726,10 @@ impl SkillCreator { let disable_model_invocation = self.disable_model_invocation; let fs = self.fs.clone(); let workspace = self.workspace.clone(); - let scope_label = scope.label(); + let scope_description: SharedString = match &scope { + ScopeChoice::Global => "your global skills".into(), + ScopeChoice::Project { root_name, .. } => root_name.clone(), + }; self.saving = true; self.save_error = None; @@ -749,22 +750,23 @@ impl SkillCreator { this.saving = false; this.save_task = None; match result { - Ok(path) => { + Ok(_) => { if let Some(on_saved) = &this.on_saved { on_saved(cx); } if let Some(workspace) = workspace.as_ref().and_then(|w| w.upgrade()) { workspace.update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - format!( - "Saved skill \"{name}\" to {scope_label} ({})", - path.display() - ), - ), - cx, - ); + let message = + format!("Saved skill \"{name}\" to {scope_description}"); + let status_toast = StatusToast::new(message, cx, |this, _cx| { + this.icon( + Icon::new(IconName::Check) + .size(IconSize::Small) + .color(Color::Success), + ) + .dismiss_button(true) + }); + workspace.toggle_status_toast(status_toast, cx); }); } window.remove_window(); @@ -1016,45 +1018,77 @@ impl SkillCreator { )) } - fn render_action_bar(&self, cx: &mut Context) -> impl IntoElement { + fn render_footer(&self, window: &Window, cx: &mut Context) -> impl IntoElement { let valid = self.is_valid(cx); let saving = self.saving; let main_action = if saving { "Saving…" } else { "Save Skill" }; - h_flex() + // Draw a faint outline around whichever button currently holds + // keyboard focus, so tabbing to Cancel/Save is clearly visible. The + // ring border is always present (transparent when unfocused) so + // focusing a button never shifts the surrounding layout. + let focus_ring = |focus_handle: &FocusHandle| { + let focused = focus_handle.is_focused(window) && window.last_input_was_keyboard(); + let border_color = if focused { + cx.theme().colors().border_focused + } else { + cx.theme().colors().border_transparent + }; + div().rounded_sm().border_1().border_color(border_color) + }; + + v_flex() .w_full() - .map(|this| { - if self.save_error.is_some() { - this.justify_between() - } else { - this.justify_end() - } + .p_2p5() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().panel_background) + .when(self.save_error.is_some(), |this| { + this.gap_2().child( + Banner::new() + .severity(Severity::Error) + .children(self.save_error.clone().map(|err| Label::new(err))), + ) }) - .gap_2() - .children( - self.save_error - .clone() - .map(|err| Label::new(err).size(LabelSize::Small).color(Color::Error)), - ) .child( h_flex() + .w_full() .gap_1() + .justify_end() .child( - Button::new("cancel-skill", "Cancel") - .disabled(saving) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(Cancel), cx); - }), + focus_ring(&self.cancel_button_focus_handle).child( + Button::new("cancel-skill", "Cancel") + .track_focus( + &self + .cancel_button_focus_handle + .clone() + .tab_index(CANCEL_BUTTON_TAB_INDEX) + .tab_stop(true), + ) + .disabled(saving) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(Cancel), cx); + }), + ), ) .child( - Button::new("save-skill", main_action) - .style(ButtonStyle::Filled) - .layer(ui::ElevationIndex::ModalSurface) - .disabled(!valid || saving) - .loading(saving) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(SaveSkill), cx); - }), + focus_ring(&self.save_button_focus_handle).child( + Button::new("save-skill", main_action) + .track_focus( + &self + .save_button_focus_handle + .clone() + .tab_index(SAVE_BUTTON_TAB_INDEX) + .tab_stop(true), + ) + .style(ButtonStyle::Filled) + .layer(ui::ElevationIndex::ModalSurface) + .disabled(!valid || saving) + .loading(saving) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(SaveSkill), cx); + }), + ), ), ) } @@ -1165,15 +1199,7 @@ impl Render for SkillCreator { .child(self.render_form_fields(window, cx)), ), ) - .child( - h_flex() - .w_full() - .p_2p5() - .border_t_1() - .border_color(theme.colors().border_variant) - .bg(theme.colors().panel_background) - .child(self.render_action_bar(cx)), - ), + .child(self.render_footer(window, cx)), window, cx, Tiling::default(),