mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +07:00
Use the announcement toast for skills support (#56873)
Closes AI-244 This PR changes the approach for announcing skills support. It uses the announcement toast, the same thing used for parallel agents, which observes a specific version number to be displayed. I ended up simplifying things a bit by thinking we could rely on documentation for more detailed information (with more extensive paths and whatnot). Release Notes: - N/A --------- Co-authored-by: Richard Feldman <richard@zed.dev> Co-authored-by: MartinYe1234 <52641447+MartinYe1234@users.noreply.github.com> Co-authored-by: Martin Ye <martinye022@gmail.com>
This commit is contained in:
parent
2e70059cd9
commit
1ddf7407e9
15 changed files with 150 additions and 618 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -1293,22 +1293,20 @@ dependencies = [
|
|||
name = "auto_update_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent_settings",
|
||||
"agent_skills",
|
||||
"anyhow",
|
||||
"auto_update",
|
||||
"client",
|
||||
"db",
|
||||
"editor",
|
||||
"fs",
|
||||
"gpui",
|
||||
"markdown_preview",
|
||||
"notifications",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"release_channel",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"telemetry",
|
||||
"ui",
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ acp_thread.workspace = true
|
|||
action_log.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agent.workspace = true
|
||||
agent_skills.workspace = true
|
||||
async-channel.workspace = true
|
||||
agent_servers.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_skills.workspace = true
|
||||
ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
heapless.workspace = true
|
||||
|
|
|
|||
|
|
@ -554,18 +554,6 @@ pub fn init(
|
|||
);
|
||||
})
|
||||
.detach();
|
||||
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
||||
workspace.register_action(
|
||||
|workspace: &mut Workspace,
|
||||
_: &zed_actions::agent::OpenRulesToSkillsMigrationInfo,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>| {
|
||||
crate::ui::RulesToSkillsModal::toggle(workspace, window, cx);
|
||||
},
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_new(ManageProfilesModal::register).detach();
|
||||
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
||||
workspace.register_action(
|
||||
|
|
|
|||
|
|
@ -333,8 +333,6 @@ pub struct ThreadView {
|
|||
pub show_codex_windows_warning: bool,
|
||||
pub multi_root_callout_dismissed: bool,
|
||||
pub generating_indicator_in_list: bool,
|
||||
/// Errors emitted by the agent while loading SKILL.md files. Each one
|
||||
/// renders as a clickable banner that opens the offending file.
|
||||
pub skill_loading_errors: Vec<SkillLoadingError>,
|
||||
/// Errors the user has explicitly dismissed. Each entry is matched against
|
||||
/// emitted errors by full equality; when an error no longer appears in the
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ mod end_trial_upsell;
|
|||
mod hold_for_default;
|
||||
mod mention_crease;
|
||||
mod model_selector_components;
|
||||
mod rules_to_skills_modal;
|
||||
mod undo_reject_toast;
|
||||
|
||||
pub use agent_notification::*;
|
||||
|
|
@ -11,7 +10,6 @@ pub use end_trial_upsell::*;
|
|||
pub use hold_for_default::*;
|
||||
pub use mention_crease::*;
|
||||
pub use model_selector_components::*;
|
||||
pub use rules_to_skills_modal::*;
|
||||
pub use undo_reject_toast::*;
|
||||
|
||||
/// Returns the appropriate [`DocumentationSide`] for documentation asides
|
||||
|
|
|
|||
|
|
@ -1,196 +0,0 @@
|
|||
//! Mini modal shown when the user clicks the title-bar Skills
|
||||
//! announcement banner. Renders one of two flavours depending on the
|
||||
//! persisted [`MigrationResult`]:
|
||||
//!
|
||||
//! * **No rules migrated** (new user, or an existing user who never
|
||||
//! touched Rules): a generic "Introducing Skills" intro that explains
|
||||
//! what Skills are and how to invoke them.
|
||||
//! * **Rules migrated**: a per-destination summary of exactly which
|
||||
//! Rules ended up where (Skills directory, global AGENTS.md, top of
|
||||
//! AGENTS.md for customized built-ins), capped at three names per
|
||||
//! section with an "…and N more" overflow line.
|
||||
|
||||
use agent_skills::GLOBAL_SKILLS_DIR_DISPLAY;
|
||||
use gpui::{
|
||||
DismissEvent, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled,
|
||||
};
|
||||
use paths::GLOBAL_AGENTS_FILE_DISPLAY;
|
||||
use prompt_store::rules_to_skills_migration::{self, MigrationResult};
|
||||
use ui::{
|
||||
AlertModal, Button, ButtonCommon, ButtonStyle, Clickable, KeyBinding, ListBulletItem, h_flex,
|
||||
prelude::*,
|
||||
};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
/// Maximum number of rule names to list inline in the modal before
|
||||
/// collapsing the rest into an "…and N more" line.
|
||||
const MAX_LISTED_NAMES: usize = 3;
|
||||
|
||||
pub struct RulesToSkillsModal {
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl RulesToSkillsModal {
|
||||
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for RulesToSkillsModal {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for RulesToSkillsModal {}
|
||||
|
||||
impl ModalView for RulesToSkillsModal {}
|
||||
|
||||
impl Render for RulesToSkillsModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let result = rules_to_skills_migration::migration_result().unwrap_or_default();
|
||||
|
||||
let mut modal = AlertModal::new("rules-to-skills-migration")
|
||||
.width(rems(28.))
|
||||
.key_context("RulesToSkillsModal")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.dismiss(cx)))
|
||||
.on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.dismiss(cx)));
|
||||
|
||||
if result.is_empty() {
|
||||
modal = render_introducing_skills(modal);
|
||||
} else {
|
||||
modal = render_migration_summary(modal, &result);
|
||||
}
|
||||
|
||||
// Both flavours close with the same invocation instructions.
|
||||
modal = modal.child(Label::new(
|
||||
"To include a Skill in a prompt, type /skill-name (or @-mention it).",
|
||||
));
|
||||
|
||||
modal.footer(
|
||||
h_flex().p_3().items_center().justify_end().child(
|
||||
Button::new("got-it", "Got it")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.key_binding(
|
||||
KeyBinding::for_action(&menu::Confirm, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.dismiss(cx);
|
||||
cx.stop_propagation();
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the modal body for users who had no Rules to migrate — a
|
||||
/// generic introduction to the Skills feature.
|
||||
fn render_introducing_skills(modal: AlertModal) -> AlertModal {
|
||||
modal.title("Introducing Skills").child(Label::new(format!(
|
||||
"Skills are reusable instructions for the agent, stored as Markdown files \
|
||||
under {GLOBAL_SKILLS_DIR_DISPLAY}/<name>/SKILL.md."
|
||||
)))
|
||||
}
|
||||
|
||||
/// Render the modal body for users whose Rules were migrated, listing
|
||||
/// each destination's contents (capped at [`MAX_LISTED_NAMES`] per
|
||||
/// section).
|
||||
fn render_migration_summary(mut modal: AlertModal, result: &MigrationResult) -> AlertModal {
|
||||
modal = modal.title("Skills have replaced Rules");
|
||||
|
||||
if !result.skill_names.is_empty() {
|
||||
modal = modal.child(Label::new(format!(
|
||||
"These Rules have been migrated to Skills in {GLOBAL_SKILLS_DIR_DISPLAY}:"
|
||||
)));
|
||||
modal = add_bulleted_names(modal, &result.skill_names);
|
||||
}
|
||||
|
||||
if !result.agents_md_names.is_empty() {
|
||||
modal = modal.child(Label::new(format!(
|
||||
"These Default Rules were added to {GLOBAL_AGENTS_FILE_DISPLAY}:"
|
||||
)));
|
||||
modal = add_bulleted_names(modal, &result.agents_md_names);
|
||||
}
|
||||
|
||||
if !result.customized_builtins.is_empty() {
|
||||
modal = modal.child(Label::new(customized_builtins_sentence(
|
||||
&result.customized_builtins,
|
||||
)));
|
||||
}
|
||||
|
||||
modal
|
||||
}
|
||||
|
||||
/// Append up to [`MAX_LISTED_NAMES`] bullet items naming individual
|
||||
/// rules, plus a final "…and N more" bullet if the list is longer.
|
||||
fn add_bulleted_names(mut modal: AlertModal, names: &[String]) -> AlertModal {
|
||||
for name in names.iter().take(MAX_LISTED_NAMES) {
|
||||
modal = modal.child(ListBulletItem::new(name.clone()));
|
||||
}
|
||||
if names.len() > MAX_LISTED_NAMES {
|
||||
let extras = names.len() - MAX_LISTED_NAMES;
|
||||
let label = if extras == 1 {
|
||||
"…and 1 more".to_string()
|
||||
} else {
|
||||
format!("…and {extras} more")
|
||||
};
|
||||
modal = modal.child(ListBulletItem::new(label));
|
||||
}
|
||||
modal
|
||||
}
|
||||
|
||||
/// Build the sentence describing any customized built-in prompts that
|
||||
/// were prepended to AGENTS.md. Singular wording for the common
|
||||
/// one-built-in case; comma-joined for the (currently hypothetical)
|
||||
/// multi-built-in case.
|
||||
fn customized_builtins_sentence(names: &[String]) -> String {
|
||||
debug_assert!(
|
||||
!names.is_empty(),
|
||||
"caller should only invoke this for a non-empty list"
|
||||
);
|
||||
if names.len() == 1 {
|
||||
format!(
|
||||
"Your customization of the {name} built-in prompt has been added to the top of \
|
||||
{GLOBAL_AGENTS_FILE_DISPLAY}.",
|
||||
name = names[0],
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Your customizations of these built-in prompts have been added to the top of \
|
||||
{GLOBAL_AGENTS_FILE_DISPLAY}: {joined}.",
|
||||
joined = names.join(", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn customized_builtins_sentence_uses_singular_wording_for_one_item() {
|
||||
let sentence = customized_builtins_sentence(&["Commit message".to_string()]);
|
||||
assert!(sentence.contains("Your customization of the Commit message"));
|
||||
assert!(sentence.contains("built-in prompt has been added"));
|
||||
assert!(sentence.contains(GLOBAL_AGENTS_FILE_DISPLAY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn customized_builtins_sentence_uses_plural_wording_for_multiple_items() {
|
||||
let sentence = customized_builtins_sentence(&[
|
||||
"Commit message".to_string(),
|
||||
"Future Built-in".to_string(),
|
||||
]);
|
||||
assert!(sentence.contains("customizations of these built-in prompts"));
|
||||
assert!(sentence.contains("Commit message, Future Built-in"));
|
||||
}
|
||||
}
|
||||
|
|
@ -12,22 +12,20 @@ workspace = true
|
|||
path = "src/auto_update_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
agent_skills.workspace = true
|
||||
anyhow.workspace = true
|
||||
auto_update.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
notifications.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
release_channel.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry.workspace = true
|
||||
ui.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentSettings, WindowLayout};
|
||||
use agent_skills::GLOBAL_SKILLS_DIR_DISPLAY;
|
||||
use auto_update::{AutoUpdater, release_notes_url};
|
||||
use client::zed_urls;
|
||||
use db::kvp::Dismissable;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, TaskExt, Window, actions,
|
||||
prelude::*,
|
||||
};
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use notifications::status_toast::StatusToast;
|
||||
use prompt_store::rules_to_skills_migration;
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings as _;
|
||||
use smol::io::AsyncReadExt;
|
||||
use ui::{AnnouncementToast, ListBulletItem, ParallelAgentsIllustration, prelude::*};
|
||||
use ui::{AnnouncementToast, ListBulletItem, SkillsIllustration, prelude::*};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::{
|
||||
FocusWorkspaceSidebar, Workspace,
|
||||
Workspace,
|
||||
notifications::{
|
||||
ErrorMessagePrompt, Notification, NotificationId, SuppressEvent, show_app_notification,
|
||||
simple_message_notification::MessageNotification,
|
||||
},
|
||||
};
|
||||
use zed_actions::{ShowUpdateNotification, assistant::FocusAgent};
|
||||
use zed_actions::ShowUpdateNotification;
|
||||
|
||||
actions!(
|
||||
auto_update,
|
||||
|
|
@ -186,103 +185,57 @@ struct AnnouncementContent {
|
|||
description: SharedString,
|
||||
bullet_items: Vec<SharedString>,
|
||||
primary_action_label: SharedString,
|
||||
secondary_action_label: SharedString,
|
||||
primary_action_url: Option<SharedString>,
|
||||
primary_action_callback: Option<Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>>,
|
||||
secondary_action_url: Option<SharedString>,
|
||||
on_dismiss: Option<Arc<dyn Fn(&mut App) + Send + Sync>>,
|
||||
}
|
||||
|
||||
struct ParallelAgentAnnouncement;
|
||||
struct SkillsAnnouncement;
|
||||
|
||||
impl Dismissable for ParallelAgentAnnouncement {
|
||||
const KEY: &'static str = "parallel-agent-announcement";
|
||||
impl Dismissable for SkillsAnnouncement {
|
||||
const KEY: &'static str = "skills_announcement_dismissed";
|
||||
}
|
||||
|
||||
fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementContent> {
|
||||
let version_with_parallel_agents = match ReleaseChannel::global(cx) {
|
||||
ReleaseChannel::Stable => Version::new(0, 233, 0),
|
||||
let version_with_skills = match ReleaseChannel::global(cx) {
|
||||
ReleaseChannel::Stable => Version::new(1, 4, 0),
|
||||
ReleaseChannel::Dev | ReleaseChannel::Nightly | ReleaseChannel::Preview => {
|
||||
Version::new(0, 232, 0)
|
||||
Version::new(1, 4, 0)
|
||||
}
|
||||
};
|
||||
|
||||
if *version >= version_with_parallel_agents
|
||||
&& !ParallelAgentAnnouncement::dismissed(cx)
|
||||
&& !project::DisableAiSettings::get_global(cx).disable_ai
|
||||
{
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
if *version >= version_with_skills && !SkillsAnnouncement::dismissed(cx) {
|
||||
// Only mention the Rules → Skills migration if the user actually
|
||||
// had Rules that got migrated. New users (and existing users who
|
||||
// never created a Rule) would otherwise be confused by a bullet
|
||||
// referring to "your rules" that don't exist.
|
||||
let migrated_anything =
|
||||
rules_to_skills_migration::migration_result().is_some_and(|result| !result.is_empty());
|
||||
|
||||
let mut bullet_items: Vec<SharedString> = Vec::with_capacity(3);
|
||||
bullet_items
|
||||
.push(format!("Skills live in {GLOBAL_SKILLS_DIR_DISPLAY}/<name>/SKILL.md").into());
|
||||
if migrated_anything {
|
||||
bullet_items.push(
|
||||
"Default Rules are converted into your global AGENTS.md; all other rules become skills".into(),
|
||||
);
|
||||
}
|
||||
bullet_items.push("Type / to manually invoke a skill".into());
|
||||
|
||||
Some(AnnouncementContent {
|
||||
heading: "Introducing Parallel Agents".into(),
|
||||
description: "Run multiple threads of your favorite agents simultaneously across projects in a new workspace layout, tailored for agentic workflows.".into(),
|
||||
bullet_items: vec![
|
||||
"Use your favorite agents in parallel".into(),
|
||||
"Optionally isolate agents using worktrees".into(),
|
||||
"Combine multiple projects in one window".into(),
|
||||
],
|
||||
primary_action_label: "Try Agentic Layout".into(),
|
||||
heading: "Introducing Skills Support".into(),
|
||||
description: "Extend the agent with focused instructions and domain knowledge.".into(),
|
||||
bullet_items,
|
||||
primary_action_label: "Try Now".into(),
|
||||
secondary_action_label: "Read Documentation".into(),
|
||||
primary_action_url: None,
|
||||
primary_action_callback: Some(Arc::new(move |window, cx| {
|
||||
let get_layout = AgentSettings::get_layout(cx);
|
||||
let already_agent_layout = matches!(get_layout, WindowLayout::Agent(_));
|
||||
|
||||
let update;
|
||||
if !already_agent_layout {
|
||||
update = Some(AgentSettings::set_layout(
|
||||
WindowLayout::Agent(None),
|
||||
fs.clone(),
|
||||
cx,
|
||||
));
|
||||
} else {
|
||||
update = None;
|
||||
}
|
||||
|
||||
let revert_fs = fs.clone();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
if let Some(update) = update {
|
||||
update.await.ok();
|
||||
}
|
||||
|
||||
cx.update(|window, cx| {
|
||||
if !already_agent_layout {
|
||||
if let Some(workspace) = Workspace::for_window(window, cx) {
|
||||
let toast = StatusToast::new(
|
||||
"You are in the new agentic layout!",
|
||||
cx,
|
||||
move |this, _cx| {
|
||||
this.icon(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.action("Revert", move |_window, cx| {
|
||||
let _ = AgentSettings::set_layout(
|
||||
get_layout.clone(),
|
||||
revert_fs.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.auto_dismiss(false)
|
||||
.dismiss_button(true)
|
||||
},
|
||||
);
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_status_toast(toast, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.dispatch_action(Box::new(FocusWorkspaceSidebar), cx);
|
||||
window.dispatch_action(Box::new(FocusAgent), cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
window.dispatch_action(Box::new(zed_actions::assistant::FocusAgent), cx);
|
||||
})),
|
||||
on_dismiss: Some(Arc::new(|cx| {
|
||||
ParallelAgentAnnouncement::set_dismissed(true, cx)
|
||||
})),
|
||||
secondary_action_url: Some("https://zed.dev/blog/".into()),
|
||||
on_dismiss: Some(Arc::new(|cx| SkillsAnnouncement::set_dismissed(true, cx))),
|
||||
secondary_action_url: Some(zed_urls::skills_docs(cx).into()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
@ -323,7 +276,7 @@ impl Notification for AnnouncementToastNotification {}
|
|||
impl Render for AnnouncementToastNotification {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
AnnouncementToast::new()
|
||||
.illustration(ParallelAgentsIllustration::new())
|
||||
.illustration(SkillsIllustration::new())
|
||||
.heading(self.content.heading.clone())
|
||||
.description(self.content.description.clone())
|
||||
.bullet_items(
|
||||
|
|
@ -333,11 +286,12 @@ impl Render for AnnouncementToastNotification {
|
|||
.map(|item| ListBulletItem::new(item.clone())),
|
||||
)
|
||||
.primary_action_label(self.content.primary_action_label.clone())
|
||||
.secondary_action_label(self.content.secondary_action_label.clone())
|
||||
.primary_on_click(cx.listener({
|
||||
let url = self.content.primary_action_url.clone();
|
||||
let callback = self.content.primary_action_callback.clone();
|
||||
move |this, _, window, cx| {
|
||||
telemetry::event!("Parallel Agent Announcement Main Click");
|
||||
telemetry::event!("Skills Announcement Main Click");
|
||||
if let Some(callback) = &callback {
|
||||
callback(window, cx);
|
||||
}
|
||||
|
|
@ -350,14 +304,14 @@ impl Render for AnnouncementToastNotification {
|
|||
.secondary_on_click(cx.listener({
|
||||
let url = self.content.secondary_action_url.clone();
|
||||
move |_, _, _window, cx| {
|
||||
telemetry::event!("Parallel Agent Announcement Secondary Click");
|
||||
telemetry::event!("Skills Announcement Secondary Click");
|
||||
if let Some(url) = &url {
|
||||
cx.open_url(url);
|
||||
}
|
||||
}
|
||||
}))
|
||||
.dismiss_on_click(cx.listener(|this, _, _window, cx| {
|
||||
telemetry::event!("Parallel Agent Announcement Dismiss");
|
||||
telemetry::event!("Skills Announcement Dismiss");
|
||||
this.dismiss(cx);
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ pub fn edit_prediction_docs(cx: &App) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn skills_docs(cx: &App) -> String {
|
||||
format!("{server_url}/docs/ai/skills", server_url = server_url(cx))
|
||||
}
|
||||
|
||||
/// Returns the URL to Zed's ACP registry blog post.
|
||||
pub fn acp_registry_blog(cx: &App) -> String {
|
||||
format!(
|
||||
|
|
@ -60,11 +64,6 @@ pub fn acp_registry_blog(cx: &App) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
/// Returns the URL to Zed's Parallel Agents blog post.
|
||||
pub fn parallel_agents_blog(cx: &App) -> String {
|
||||
format!("{server_url}/blog", server_url = server_url(cx))
|
||||
}
|
||||
|
||||
pub fn shared_agent_thread_url(session_id: &str) -> String {
|
||||
format!("zed://agent/shared/{}", session_id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,15 +57,15 @@ pub const MIGRATION_DONE_KEY: &str = "rules_to_skills_migration_done";
|
|||
|
||||
/// Global KVP key for the JSON-serialized [`MigrationResult`] produced by
|
||||
/// the most recent migration run — the lists of source-Rule titles that
|
||||
/// were migrated to each destination. The title-bar banner and its
|
||||
/// explainer modal read this to decide what (if anything) to tell the
|
||||
/// user about what changed.
|
||||
/// were migrated to each destination. The skills announcement toast
|
||||
/// reads this to decide whether to mention the migration in its copy.
|
||||
pub const MIGRATION_RESULT_KEY: &str = "rules_to_skills_migration_result";
|
||||
|
||||
/// A persistent record of what the rules-to-skills migration actually
|
||||
/// migrated. Persisted in [`GlobalKeyValueStore`] under
|
||||
/// [`MIGRATION_RESULT_KEY`] and read back by the announcement UI so the
|
||||
/// modal can list specific rule names instead of vaguely gesturing.
|
||||
/// [`MIGRATION_RESULT_KEY`] and read back by the skills announcement
|
||||
/// toast so it can tailor its copy to users who actually had Rules to
|
||||
/// migrate.
|
||||
///
|
||||
/// All three lists hold the *original* user-facing Rule titles, not the
|
||||
/// derived skill slug or any other transformed identifier — those are
|
||||
|
|
@ -87,10 +87,9 @@ pub struct MigrationResult {
|
|||
|
||||
impl MigrationResult {
|
||||
/// `true` if the migration didn't actually move any Rule anywhere —
|
||||
/// i.e. the user had no Rules of any kind to migrate. The
|
||||
/// announcement banner/modal uses this to switch between the
|
||||
/// "Introducing: Skills" generic intro and the "Skills have replaced
|
||||
/// Rules" migration summary.
|
||||
/// i.e. the user had no Rules of any kind to migrate. The skills
|
||||
/// announcement toast uses this to omit the migration-flavored
|
||||
/// bullet for users who never had any Rules.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.skill_names.is_empty()
|
||||
&& self.agents_md_names.is_empty()
|
||||
|
|
|
|||
|
|
@ -454,21 +454,7 @@ impl TitleBar {
|
|||
titlebar
|
||||
});
|
||||
|
||||
// The banner label stays static ("Introducing: Skills") regardless
|
||||
// of whether the user had Rules to migrate; the explainer modal
|
||||
// is where the migration-specific summary surfaces. Keeping the
|
||||
// label static avoids the rebuild-on-migration-completion plumbing
|
||||
// we'd otherwise need to dodge the title-bar-vs-migration race.
|
||||
let banner = Some(cx.new(|cx| {
|
||||
OnboardingBanner::new(
|
||||
"Skills Migration Announcement",
|
||||
IconName::Sparkle,
|
||||
"Skills",
|
||||
Some("Introducing:".into()),
|
||||
zed_actions::agent::OpenRulesToSkillsMigrationInfo.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
let banner = None;
|
||||
|
||||
let mut this = Self {
|
||||
platform_titlebar,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
mod agent_setup_button;
|
||||
mod ai_setting_item;
|
||||
mod configured_api_card;
|
||||
mod parallel_agents_illustration;
|
||||
mod skills_illustration;
|
||||
mod thread_item;
|
||||
|
||||
pub use agent_setup_button::*;
|
||||
pub use ai_setting_item::*;
|
||||
pub use configured_api_card::*;
|
||||
pub use parallel_agents_illustration::*;
|
||||
pub use skills_illustration::*;
|
||||
pub use thread_item::*;
|
||||
|
|
|
|||
|
|
@ -1,272 +0,0 @@
|
|||
use crate::{DiffStat, Divider, prelude::*};
|
||||
use gpui::{Animation, AnimationExt, pulsating_between};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ParallelAgentsIllustration;
|
||||
|
||||
impl ParallelAgentsIllustration {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ParallelAgentsIllustration {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let icon_container = || h_flex().size_4().flex_shrink_0().justify_center();
|
||||
|
||||
let loading_bar = |id: &'static str, width: DefiniteLength, duration_ms: u64| {
|
||||
div()
|
||||
.h(rems_from_px(5.))
|
||||
.w(width)
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().element_selected)
|
||||
.with_animation(
|
||||
id,
|
||||
Animation::new(Duration::from_millis(duration_ms))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.1, 0.8)),
|
||||
|label, delta| label.opacity(delta),
|
||||
)
|
||||
};
|
||||
|
||||
let skeleton_bar = |width: DefiniteLength| {
|
||||
div().h(rems_from_px(5.)).w(width).rounded_full().bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.text_muted
|
||||
.opacity(0.05))
|
||||
};
|
||||
|
||||
let time =
|
||||
|time: SharedString| Label::new(time).size(LabelSize::XSmall).color(Color::Muted);
|
||||
|
||||
let worktree = |worktree: SharedString| {
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Icon::new(IconName::GitWorktree)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Indicator),
|
||||
)
|
||||
.child(
|
||||
Label::new(worktree)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
};
|
||||
|
||||
let dot_separator = || {
|
||||
Label::new("•")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.alpha(0.5)
|
||||
};
|
||||
|
||||
let agent = |title: SharedString, icon: IconName, selected: bool, data: Vec<AnyElement>| {
|
||||
v_flex()
|
||||
.when(selected, |this| {
|
||||
this.bg(cx.theme().colors().element_active.opacity(0.2))
|
||||
})
|
||||
.p_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
icon_container()
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted)),
|
||||
)
|
||||
.map(|this| {
|
||||
if selected {
|
||||
this.child(
|
||||
Label::new(title)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
} else {
|
||||
this.child(skeleton_bar(relative(0.7)))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.opacity(0.8)
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(icon_container())
|
||||
.children(data),
|
||||
)
|
||||
};
|
||||
|
||||
let agents = v_flex()
|
||||
.col_span(3)
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.child(agent(
|
||||
"Fix branch label".into(),
|
||||
IconName::ZedAgent,
|
||||
true,
|
||||
vec![
|
||||
worktree("bug-fix".into()).into_any_element(),
|
||||
dot_separator().into_any_element(),
|
||||
DiffStat::new("ds", 5, 2)
|
||||
.label_size(LabelSize::XSmall)
|
||||
.into_any_element(),
|
||||
dot_separator().into_any_element(),
|
||||
time("2m".into()).into_any_element(),
|
||||
],
|
||||
))
|
||||
.child(Divider::horizontal())
|
||||
.child(agent(
|
||||
"Improve thread id".into(),
|
||||
IconName::AiClaude,
|
||||
false,
|
||||
vec![
|
||||
DiffStat::new("ds", 120, 84)
|
||||
.label_size(LabelSize::XSmall)
|
||||
.into_any_element(),
|
||||
dot_separator().into_any_element(),
|
||||
time("16m".into()).into_any_element(),
|
||||
],
|
||||
))
|
||||
.child(Divider::horizontal())
|
||||
.child(agent(
|
||||
"Refactor archive view".into(),
|
||||
IconName::AiOpenAi,
|
||||
false,
|
||||
vec![
|
||||
worktree("silent-forest".into()).into_any_element(),
|
||||
dot_separator().into_any_element(),
|
||||
time("37m".into()).into_any_element(),
|
||||
],
|
||||
));
|
||||
|
||||
let thread_view = v_flex()
|
||||
.col_span(3)
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.px_1p5()
|
||||
.py_0p5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.child(
|
||||
Label::new("Fix branch label")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::Plus)
|
||||
.size(IconSize::Indicator)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().p_1().child(
|
||||
v_flex()
|
||||
.px_1()
|
||||
.py_1p5()
|
||||
.gap_1()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_sm()
|
||||
.shadow_sm()
|
||||
.child(skeleton_bar(relative(0.7)))
|
||||
.child(skeleton_bar(relative(0.2))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
.gap_1()
|
||||
.child(loading_bar("a", relative(0.55), 2200))
|
||||
.child(loading_bar("b", relative(0.75), 2000))
|
||||
.child(loading_bar("c", relative(0.25), 2400)),
|
||||
);
|
||||
|
||||
let file_row = |indent: usize, is_folder: bool, bar_width: Rems| {
|
||||
let indent_px = rems_from_px((indent as f32) * 4.0);
|
||||
|
||||
h_flex()
|
||||
.px_2()
|
||||
.py_px()
|
||||
.gap_1()
|
||||
.pl(indent_px)
|
||||
.child(
|
||||
icon_container().child(
|
||||
Icon::new(if is_folder {
|
||||
IconName::FolderOpen
|
||||
} else {
|
||||
IconName::FileRust
|
||||
})
|
||||
.size(IconSize::Indicator)
|
||||
.color(Color::Custom(cx.theme().colors().icon_muted.opacity(0.2))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().h_1p5().w(bar_width).rounded_sm().bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.text
|
||||
.opacity(if is_folder { 0.15 } else { 0.1 })),
|
||||
)
|
||||
};
|
||||
|
||||
let project_panel = v_flex()
|
||||
.col_span(1)
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.child(
|
||||
v_flex()
|
||||
.child(file_row(0, true, rems_from_px(42.0)))
|
||||
.child(file_row(1, true, rems_from_px(28.0)))
|
||||
.child(file_row(2, false, rems_from_px(52.0)))
|
||||
.child(file_row(2, false, rems_from_px(36.0)))
|
||||
.child(file_row(2, false, rems_from_px(44.0)))
|
||||
.child(file_row(1, true, rems_from_px(34.0)))
|
||||
.child(file_row(2, false, rems_from_px(48.0)))
|
||||
.child(file_row(2, true, rems_from_px(26.0)))
|
||||
.child(file_row(3, false, rems_from_px(40.0)))
|
||||
.child(file_row(3, false, rems_from_px(56.0)))
|
||||
.child(file_row(1, false, rems_from_px(38.0)))
|
||||
.child(file_row(0, true, rems_from_px(30.0)))
|
||||
.child(file_row(1, false, rems_from_px(46.0)))
|
||||
.child(file_row(1, false, rems_from_px(32.0))),
|
||||
);
|
||||
|
||||
let workspace = div()
|
||||
.absolute()
|
||||
.top_8()
|
||||
.grid()
|
||||
.grid_cols(7)
|
||||
.w(rems_from_px(380.))
|
||||
.rounded_t_sm()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.shadow_md()
|
||||
.child(agents)
|
||||
.child(thread_view)
|
||||
.child(project_panel);
|
||||
|
||||
h_flex()
|
||||
.relative()
|
||||
.h(rems_from_px(180.))
|
||||
.bg(cx.theme().colors().editor_background.opacity(0.6))
|
||||
.justify_center()
|
||||
.items_end()
|
||||
.rounded_t_md()
|
||||
.overflow_hidden()
|
||||
.bg(gpui::black().opacity(0.2))
|
||||
.child(workspace)
|
||||
}
|
||||
}
|
||||
86
crates/ui/src/components/ai/skills_illustration.rs
Normal file
86
crates/ui/src/components/ai/skills_illustration.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use crate::prelude::*;
|
||||
use gpui::{linear_color_stop, linear_gradient};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct SkillsIllustration;
|
||||
|
||||
impl SkillsIllustration {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SkillsIllustration {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let skill_crease = |label: SharedString, source: SharedString| {
|
||||
h_flex()
|
||||
.py_1()
|
||||
.px_1p5()
|
||||
.gap_1p5()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().element_active.opacity(0.5))
|
||||
.justify_center()
|
||||
.rounded_md()
|
||||
.shadow_sm()
|
||||
.child(
|
||||
Icon::new(IconName::Sparkle)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(Label::new(label).size(LabelSize::XSmall).buffer_font(cx))
|
||||
.child(
|
||||
Label::new(format!("({source})"))
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
};
|
||||
|
||||
let skill_list = v_flex()
|
||||
.absolute()
|
||||
.top_8()
|
||||
.gap_2p5()
|
||||
.items_center()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2p5()
|
||||
.child(skill_crease("img-gen".into(), "studio".into()))
|
||||
.child(skill_crease("frontend-design".into(), "global".into())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2p5()
|
||||
.child(skill_crease("brainstorming".into(), "global".into()))
|
||||
.child(skill_crease("borrow-checker-expert".into(), "zed".into())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2p5()
|
||||
.child(skill_crease("grill-with-docs".into(), "global".into()))
|
||||
.child(skill_crease("video-edit".into(), "studio".into())),
|
||||
);
|
||||
|
||||
let gradient_bg = cx.theme().colors().editor_background;
|
||||
let gradient_fade = div()
|
||||
.absolute()
|
||||
.rounded_t_md()
|
||||
.inset_0()
|
||||
.bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(gradient_bg.opacity(0.8), 0.),
|
||||
linear_color_stop(gradient_bg.opacity(0.0), 1.),
|
||||
));
|
||||
|
||||
v_flex()
|
||||
.relative()
|
||||
.h(rems_from_px(150.))
|
||||
.justify_end()
|
||||
.items_center()
|
||||
.rounded_t_md()
|
||||
.overflow_hidden()
|
||||
.bg(gpui::black().opacity(0.2))
|
||||
.child(skill_list)
|
||||
.child(gradient_fade)
|
||||
}
|
||||
}
|
||||
|
|
@ -514,10 +514,6 @@ pub mod agent {
|
|||
ResetAgentZoom,
|
||||
/// Pastes clipboard content without any formatting.
|
||||
PasteRaw,
|
||||
/// Opens the "Skills have replaced Rules" explainer modal,
|
||||
/// describing the one-time migration of non-Default Rules to
|
||||
/// global Skills. Dispatched from the title-bar banner.
|
||||
OpenRulesToSkillsMigrationInfo,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue