Cargo.lock 🔗
@@ -1251,6 +1251,7 @@ dependencies = [
"fs",
"gpui",
"markdown_preview",
+ "notifications",
"release_channel",
"semver",
"serde",
Danilo Leal created
- Adds a status toast to the announcement banner for surfacing the
layout revert option
- Removes the agent panel banner
A good chunk of the diff here was because I touched up the status toast
component API a little bit.
Release Notes:
- N/A
Cargo.lock | 1
crates/agent_ui/src/agent_configuration.rs | 68
crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs | 10
crates/agent_ui/src/agent_panel.rs | 101
crates/agent_ui/src/thread_import.rs | 22
crates/agent_ui/src/ui/undo_reject_toast.rs | 24
crates/ai_onboarding/src/ai_onboarding.rs | 132
crates/auto_update_ui/Cargo.toml | 1
crates/auto_update_ui/src/auto_update_ui.rs | 41
crates/component_preview/src/component_preview.rs | 14
crates/debugger_ui/src/session/running/memory_view.rs | 4
crates/git_ui/src/clone.rs | 12
crates/git_ui/src/git_panel.rs | 44
crates/keymap_editor/src/keymap_editor.rs | 10
crates/notifications/src/status_toast.rs | 90
crates/onboarding/src/onboarding.rs | 24
crates/project_panel/src/project_panel.rs | 10
crates/ui/src/components/ai/parallel_agents_illustration.rs | 199
crates/ui/src/components/icon.rs | 3
crates/workspace/src/toast_layer.rs | 9
20 files changed, 409 insertions(+), 410 deletions(-)
@@ -1251,6 +1251,7 @@ dependencies = [
"fs",
"gpui",
"markdown_preview",
+ "notifications",
"release_channel",
"semver",
"serde",
@@ -26,7 +26,7 @@ use language_model::{
ZED_CLOUD_PROVIDER_ID,
};
use language_models::AllLanguageModelSettings;
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use project::{
agent_server_store::{AgentId, AgentServerStore, ExternalAgentSource},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
@@ -1330,40 +1330,44 @@ fn show_unable_to_uninstall_extension_with_context_server(
move |this, _cx| {
let workspace_handle = workspace_handle.clone();
- this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
- .dismiss_button(true)
- .action("Uninstall", move |_, _cx| {
- if let Some((extension_id, _)) =
- resolve_extension_for_context_server(&context_server_id, _cx)
- {
- ExtensionStore::global(_cx).update(_cx, |store, cx| {
- store
- .uninstall_extension(extension_id, cx)
- .detach_and_log_err(cx);
- });
+ this.icon(
+ Icon::new(IconName::Warning)
+ .size(IconSize::Small)
+ .color(Color::Warning),
+ )
+ .dismiss_button(true)
+ .action("Uninstall", move |_, _cx| {
+ if let Some((extension_id, _)) =
+ resolve_extension_for_context_server(&context_server_id, _cx)
+ {
+ ExtensionStore::global(_cx).update(_cx, |store, cx| {
+ store
+ .uninstall_extension(extension_id, cx)
+ .detach_and_log_err(cx);
+ });
- workspace_handle
- .update(_cx, |workspace, cx| {
- let fs = workspace.app_state().fs.clone();
- cx.spawn({
- let context_server_id = context_server_id.clone();
- async move |_workspace_handle, cx| {
- cx.update(|cx| {
- update_settings_file(fs, cx, move |settings, _| {
- settings
- .project
- .context_servers
- .remove(&context_server_id.0);
- });
+ workspace_handle
+ .update(_cx, |workspace, cx| {
+ let fs = workspace.app_state().fs.clone();
+ cx.spawn({
+ let context_server_id = context_server_id.clone();
+ async move |_workspace_handle, cx| {
+ cx.update(|cx| {
+ update_settings_file(fs, cx, move |settings, _| {
+ settings
+ .project
+ .context_servers
+ .remove(&context_server_id.0);
});
- anyhow::Ok(())
- }
- })
- .detach_and_log_err(cx);
+ });
+ anyhow::Ok(())
+ }
})
- .log_err();
- }
- })
+ .detach_and_log_err(cx);
+ })
+ .log_err();
+ }
+ })
},
);
@@ -9,7 +9,7 @@ use gpui::{
};
use language::{Language, LanguageRegistry};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use parking_lot::Mutex;
use project::{
context_server_store::{
@@ -631,8 +631,12 @@ impl ConfigureContextServerModal {
format!("{} configured successfully.", id.0),
cx,
|this, _cx| {
- this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted))
- .action("Dismiss", |_, _| {})
+ this.icon(
+ Icon::new(IconName::ToolHammer)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .action("Dismiss", |_, _| {})
},
);
@@ -49,7 +49,7 @@ use crate::{
use crate::{ExpandMessageEditor, ThreadHistoryView};
use crate::{ManageProfiles, ThreadHistoryViewEvent};
use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore};
-use agent_settings::{AgentSettings, WindowLayout};
+use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding;
use anyhow::{Context as _, Result, anyhow};
use client::UserStore;
@@ -760,8 +760,6 @@ pub struct AgentPanel {
pending_serialization: Option<Task<Result<()>>>,
new_user_onboarding: Entity<AgentPanelOnboarding>,
new_user_onboarding_upsell_dismissed: AtomicBool,
- agent_layout_onboarding: Entity<ai_onboarding::AgentLayoutOnboarding>,
- agent_layout_onboarding_dismissed: AtomicBool,
selected_agent: Agent,
pending_thread_loads: usize,
worktree_creation_status: Option<(EntityId, WorktreeCreationStatus)>,
@@ -1065,46 +1063,6 @@ impl AgentPanel {
)
});
- let weak_panel = cx.entity().downgrade();
-
- let layout = AgentSettings::get_layout(cx);
- let is_agent_layout = matches!(layout, WindowLayout::Agent(_));
-
- let agent_layout_onboarding = cx.new(|_cx| ai_onboarding::AgentLayoutOnboarding {
- use_agent_layout: Arc::new({
- let fs = fs.clone();
- let weak_panel = weak_panel.clone();
- move |_window, cx| {
- let _ = AgentSettings::set_layout(WindowLayout::Agent(None), fs.clone(), cx);
- weak_panel
- .update(cx, |panel, cx| {
- panel.dismiss_agent_layout_onboarding(cx);
- })
- .ok();
- }
- }),
- revert_to_editor_layout: Arc::new({
- let fs = fs.clone();
- let weak_panel = weak_panel.clone();
- move |_window, cx| {
- let _ = AgentSettings::set_layout(WindowLayout::Editor(None), fs.clone(), cx);
- weak_panel
- .update(cx, |panel, cx| {
- panel.dismiss_agent_layout_onboarding(cx);
- })
- .ok();
- }
- }),
- dismissed: Arc::new(move |_window, cx| {
- weak_panel
- .update(cx, |panel, cx| {
- panel.dismiss_agent_layout_onboarding(cx);
- })
- .ok();
- }),
- is_agent_layout,
- });
-
// Subscribe to extension events to sync agent servers when extensions change
let extension_subscription = if let Some(extension_events) = ExtensionEvents::try_global(cx)
{
@@ -1169,7 +1127,6 @@ impl AgentPanel {
zoomed: false,
pending_serialization: None,
new_user_onboarding: onboarding,
- agent_layout_onboarding,
thread_store,
selected_agent: Agent::default(),
pending_thread_loads: 0,
@@ -1179,9 +1136,6 @@ impl AgentPanel {
_worktree_creation_task: None,
show_trust_workspace_message: false,
new_user_onboarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)),
- agent_layout_onboarding_dismissed: AtomicBool::new(AgentLayoutOnboarding::dismissed(
- cx,
- )),
_base_view_observation: None,
_draft_editor_observation: None,
};
@@ -4676,56 +4630,10 @@ impl AgentPanel {
plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
}
- fn should_render_agent_layout_onboarding(&self, cx: &mut Context<Self>) -> bool {
- // We only want to show this for existing users: those who
- // have used the agent panel before the sidebar was introduced.
- // We can infer that state by users having seen the onboarding
- // at one point, but not the agent layout onboarding.
-
- let has_messages = self.active_thread_has_messages(cx);
- let is_dismissed = self
- .agent_layout_onboarding_dismissed
- .load(Ordering::Acquire);
-
- if is_dismissed || has_messages {
- return false;
- }
-
- match &self.base_view {
- BaseView::Uninitialized => false,
- BaseView::AgentThread { .. } => {
- let existing_user = self
- .new_user_onboarding_upsell_dismissed
- .load(Ordering::Acquire);
- existing_user
- }
- }
- }
-
- fn render_agent_layout_onboarding(
- &self,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Option<impl IntoElement> {
- if !self.should_render_agent_layout_onboarding(cx) {
- return None;
- }
-
- Some(div().child(self.agent_layout_onboarding.clone()))
- }
-
- fn dismiss_agent_layout_onboarding(&mut self, cx: &mut Context<Self>) {
- self.agent_layout_onboarding_dismissed
- .store(true, Ordering::Release);
- AgentLayoutOnboarding::set_dismissed(true, cx);
- cx.notify();
- }
-
fn dismiss_ai_onboarding(&mut self, cx: &mut Context<Self>) {
self.new_user_onboarding_upsell_dismissed
.store(true, Ordering::Release);
OnboardingUpsell::set_dismissed(true, cx);
- self.dismiss_agent_layout_onboarding(cx);
cx.notify();
}
@@ -4987,7 +4895,6 @@ impl Render for AgentPanel {
.child(self.render_toolbar(window, cx))
.children(self.render_workspace_trust_message(cx))
.children(self.render_new_user_onboarding(window, cx))
- .children(self.render_agent_layout_onboarding(window, cx))
.map(|parent| match self.visible_surface() {
VisibleSurface::Uninitialized => parent,
VisibleSurface::AgentThread(conversation_view) => parent
@@ -5078,12 +4985,6 @@ impl Dismissable for OnboardingUpsell {
const KEY: &'static str = "dismissed-trial-upsell";
}
-struct AgentLayoutOnboarding;
-
-impl Dismissable for AgentLayoutOnboarding {
- const KEY: &'static str = "dismissed-agent-layout-onboarding";
-}
-
struct TrialEndUpsell;
impl Dismissable for TrialEndUpsell {
@@ -11,7 +11,7 @@ use gpui::{
App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent,
Render, SharedString, Task, WeakEntity, Window,
};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use project::{AgentId, AgentRegistryStore, AgentServerStore};
use release_channel::ReleaseChannel;
use remote::RemoteConnectionOptions;
@@ -275,8 +275,12 @@ impl ThreadImportModal {
fn show_imported_threads_toast(&self, imported_count: usize, cx: &mut App) {
let status_toast = if imported_count == 0 {
StatusToast::new("No threads found to import.", cx, |this, _cx| {
- this.icon(ToastIcon::new(IconName::Info).color(Color::Muted))
- .dismiss_button(true)
+ this.icon(
+ Icon::new(IconName::Info)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .dismiss_button(true)
})
} else {
let message = if imported_count == 1 {
@@ -285,8 +289,12 @@ impl ThreadImportModal {
format!("Imported {imported_count} threads.")
};
StatusToast::new(message, cx, |this, _cx| {
- this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
- .dismiss_button(true)
+ this.icon(
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Success),
+ )
+ .dismiss_button(true)
})
};
@@ -660,7 +668,7 @@ fn show_cross_channel_import_toast(
) {
let status_toast = if imported_count == 0 {
StatusToast::new("No new threads found to import.", cx, |this, _cx| {
- this.icon(ToastIcon::new(IconName::Info).color(Color::Muted))
+ this.icon(Icon::new(IconName::Info).color(Color::Muted))
.dismiss_button(true)
})
} else {
@@ -670,7 +678,7 @@ fn show_cross_channel_import_toast(
format!("Imported {imported_count} threads from other channels.")
};
StatusToast::new(message, cx, |this, _cx| {
- this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
+ this.icon(Icon::new(IconName::Check).color(Color::Success))
.dismiss_button(true)
})
};
@@ -1,6 +1,6 @@
use action_log::ActionLog;
use gpui::{App, Entity};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use ui::prelude::*;
use workspace::Workspace;
@@ -11,15 +11,19 @@ pub fn show_undo_reject_toast(
) {
let action_log_weak = action_log.downgrade();
let status_toast = StatusToast::new("Agent Changes Rejected", cx, move |this, _cx| {
- this.icon(ToastIcon::new(IconName::Undo).color(Color::Muted))
- .action("Undo", move |_window, cx| {
- if let Some(action_log) = action_log_weak.upgrade() {
- action_log
- .update(cx, |action_log, cx| action_log.undo_last_reject(cx))
- .detach();
- }
- })
- .dismiss_button(true)
+ this.icon(
+ Icon::new(IconName::Undo)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .action("Undo", move |_window, cx| {
+ if let Some(action_log) = action_log_weak.upgrade() {
+ action_log
+ .update(cx, |action_log, cx| action_log.undo_last_reject(cx))
+ .detach();
+ }
+ })
+ .dismiss_button(true)
});
workspace.toggle_status_toast(status_toast, cx);
}
@@ -17,9 +17,7 @@ use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement};
-use ui::{
- Divider, List, ListBulletItem, RegisterComponent, Tooltip, Vector, VectorName, prelude::*,
-};
+use ui::{Divider, RegisterComponent, Tooltip, Vector, VectorName, prelude::*};
#[derive(PartialEq)]
pub enum SignInStatus {
@@ -442,131 +440,3 @@ impl Component for ZedAiOnboarding {
)
}
}
-
-#[derive(RegisterComponent)]
-pub struct AgentLayoutOnboarding {
- pub use_agent_layout: Arc<dyn Fn(&mut Window, &mut App)>,
- pub revert_to_editor_layout: Arc<dyn Fn(&mut Window, &mut App)>,
- pub dismissed: Arc<dyn Fn(&mut Window, &mut App)>,
- pub is_agent_layout: bool,
-}
-
-impl Render for AgentLayoutOnboarding {
- fn render(&mut self, _window: &mut ui::Window, _cx: &mut Context<Self>) -> impl IntoElement {
- let description = "With the new Threads Sidebar, you can manage multiple agents across several projects, all in one window.";
-
- let dismiss_button = div().absolute().top_0().right_0().child(
- IconButton::new("dismiss", IconName::Close)
- .icon_size(IconSize::Small)
- .on_click({
- let dismiss = self.dismissed.clone();
- move |_, window, cx| {
- telemetry::event!("Agentic Layout Onboarding Dismissed");
- dismiss(window, cx)
- }
- }),
- );
-
- let primary_button = if self.is_agent_layout {
- Button::new("revert", "Use Previous Layout")
- .label_size(LabelSize::Small)
- .style(ButtonStyle::Outlined)
- .on_click({
- let revert = self.revert_to_editor_layout.clone();
- let dismiss = self.dismissed.clone();
- move |_, window, cx| {
- telemetry::event!("Clicked to Use Previous Layout");
- revert(window, cx);
- dismiss(window, cx);
- }
- })
- } else {
- Button::new("start", "Use New Layout")
- .label_size(LabelSize::Small)
- .style(ButtonStyle::Outlined)
- .on_click({
- let use_layout = self.use_agent_layout.clone();
- let dismiss = self.dismissed.clone();
- move |_, window, cx| {
- telemetry::event!("Clicked to Use New Layout");
- use_layout(window, cx);
- dismiss(window, cx);
- }
- })
- };
-
- let content = v_flex()
- .min_w_0()
- .w_full()
- .relative()
- .gap_1()
- .child(Label::new("A new workspace layout for agentic workflows"))
- .child(Label::new(description).color(Color::Muted).mb_2())
- .child(
- List::new()
- .child(ListBulletItem::new(
- "The Sidebar and Agent Panel are on the left by default",
- ))
- .child(ListBulletItem::new(
- "The Project Panel and all other panels shift to the right",
- ))
- .child(ListBulletItem::new(
- "You can always customize your workspace layout in your Settings",
- )),
- )
- .child(
- h_flex()
- .w_full()
- .gap_1()
- .flex_wrap()
- .justify_end()
- .child(
- Button::new("learn", "Learn More")
- .label_size(LabelSize::Small)
- .style(ButtonStyle::OutlinedGhost)
- .on_click(move |_, _, cx| {
- cx.open_url(&zed_urls::parallel_agents_blog(cx))
- }),
- )
- .child(primary_button),
- )
- .child(dismiss_button);
-
- AgentPanelOnboardingCard::new().child(content)
- }
-}
-
-impl Component for AgentLayoutOnboarding {
- fn scope() -> ComponentScope {
- ComponentScope::Onboarding
- }
-
- fn name() -> &'static str {
- "Agent Layout Onboarding"
- }
-
- fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
- let onboarding = cx.new(|_cx| AgentLayoutOnboarding {
- use_agent_layout: Arc::new(|_, _| {}),
- revert_to_editor_layout: Arc::new(|_, _| {}),
- dismissed: Arc::new(|_, _| {}),
- is_agent_layout: false,
- });
-
- Some(
- v_flex()
- .min_w_0()
- .gap_4()
- .child(single_example(
- "Agent Layout Onboarding",
- div()
- .w_full()
- .min_w_40()
- .max_w(px(1100.))
- .child(onboarding)
- .into_any_element(),
- ))
- .into_any_element(),
- )
- }
-}
@@ -19,6 +19,7 @@ client.workspace = true
db.workspace = true
fs.workspace = true
editor.workspace = true
+notifications.workspace = true
gpui.workspace = true
markdown_preview.workspace = true
release_channel.workspace = true
@@ -9,6 +9,7 @@ use gpui::{
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window, actions, prelude::*,
};
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
+use notifications::status_toast::StatusToast;
use release_channel::{AppVersion, ReleaseChannel};
use semver::Version;
use serde::Deserialize;
@@ -207,17 +208,17 @@ fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementC
let fs = <dyn Fs>::global(cx);
Some(AnnouncementContent {
heading: "Introducing Parallel Agents".into(),
- description: "Run multiple agent threads simultaneously across projects.".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 Now".into(),
+ primary_action_label: "Try Agentic Layout".into(),
primary_action_url: None,
primary_action_callback: Some(Arc::new(move |window, cx| {
- let already_agent_layout =
- matches!(AgentSettings::get_layout(cx), WindowLayout::Agent(_));
+ let get_layout = AgentSettings::get_layout(cx);
+ let already_agent_layout = matches!(get_layout, WindowLayout::Agent(_));
let update;
if !already_agent_layout {
@@ -230,6 +231,7 @@ fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementC
update = None;
}
+ let revert_fs = fs.clone();
window
.spawn(cx, async move |cx| {
if let Some(update) = update {
@@ -237,6 +239,35 @@ fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementC
}
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);
})
@@ -381,8 +412,10 @@ pub fn notify_if_app_was_updated(cx: &mut App) {
}
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
+
cx.spawn(async move |cx| {
let should_show_notification = should_show_notification.await?;
+
if should_show_notification {
cx.update(|cx| {
show_update_notification(cx);
@@ -8,7 +8,7 @@ use gpui::{
};
use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
use language::LanguageRegistry;
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use persistence::ComponentPreviewDb;
use project::Project;
use std::{iter::Iterator, ops::Range, sync::Arc};
@@ -561,10 +561,14 @@ impl ComponentPreview {
workspace.update(cx, |workspace, cx| {
let status_toast =
StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
- this.icon(ToastIcon::new(IconName::GitBranch).color(Color::Muted))
- .action("Open Pull Request", |_, cx| {
- cx.open_url("https://github.com/")
- })
+ this.icon(
+ Icon::new(IconName::GitBranch)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .action("Open Pull Request", |_, cx| {
+ cx.open_url("https://github.com/")
+ })
});
workspace.toggle_status_toast(status_toast, cx)
});
@@ -14,7 +14,7 @@ use gpui::{
Subscription, Task, TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions,
anchored, deferred, uniform_list,
};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
use settings::Settings;
use theme_settings::ThemeSettings;
@@ -480,7 +480,7 @@ impl MemoryView {
cx.emit(DismissEvent)
});
}).detach();
- this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
+ this.icon(Icon::new(IconName::XCircle).size(IconSize::Small).color(Color::Error))
}),
cx,
);
@@ -1,7 +1,7 @@
use gpui::{App, Context, WeakEntity, Window};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use std::sync::Arc;
-use ui::{Color, IconName, SharedString};
+use ui::{Color, Icon, IconName, IconSize, SharedString};
use util::ResultExt;
use workspace::{self, Workspace};
@@ -48,8 +48,12 @@ pub fn clone_and_open(
workspace
.update(cx, |workspace, cx| {
let toast = StatusToast::new(error.to_string(), cx, |this, _| {
- this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
- .dismiss_button(true)
+ this.icon(
+ Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error),
+ )
+ .dismiss_button(true)
});
workspace.toggle_status_toast(toast, cx);
})
@@ -50,7 +50,7 @@ use language_model::{
};
use menu;
use multi_buffer::ExcerptBoundaryInfo;
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use panel::{PanelHeader, panel_button, panel_filled_button, panel_icon_button};
use project::{
Fs, Project, ProjectPath,
@@ -3864,9 +3864,17 @@ impl GitPanel {
let status_toast = StatusToast::new(message, cx, move |this, _cx| {
use remote_output::SuccessStyle::*;
match style {
- Toast => this.icon(ToastIcon::new(IconName::GitBranch).color(Color::Muted)),
+ Toast => this.icon(
+ Icon::new(IconName::GitBranch)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ ),
ToastWithLog { output } => this
- .icon(ToastIcon::new(IconName::GitBranch).color(Color::Muted))
+ .icon(
+ Icon::new(IconName::GitBranch)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
.action("View Log", move |window, cx| {
let output = output.clone();
let output =
@@ -3878,7 +3886,11 @@ impl GitPanel {
.ok();
}),
PushPrLink { text, link } => this
- .icon(ToastIcon::new(IconName::GitBranch).color(Color::Muted))
+ .icon(
+ Icon::new(IconName::GitBranch)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
.action(text, move |_, cx| cx.open_url(&link)),
}
.dismiss_button(true)
@@ -6479,16 +6491,20 @@ pub(crate) fn show_error_toast(
workspace.update(cx, |workspace, cx| {
let workspace_weak = cx.weak_entity();
let toast = StatusToast::new(format!("git {} failed", action), cx, |this, _cx| {
- this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
- .action("View Log", move |window, cx| {
- let message = message.clone();
- let action = action.clone();
- workspace_weak
- .update(cx, move |workspace, cx| {
- open_output(action, workspace, &message, window, cx)
- })
- .ok();
- })
+ this.icon(
+ Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error),
+ )
+ .action("View Log", move |window, cx| {
+ let message = message.clone();
+ let action = action.clone();
+ workspace_weak
+ .update(cx, move |workspace, cx| {
+ open_output(action, workspace, &message, window, cx)
+ })
+ .ok();
+ })
});
workspace.toggle_status_toast(toast, cx)
});
@@ -25,7 +25,7 @@ use gpui::{
};
use language::{Language, LanguageConfig, ToOffset as _};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use project::{CompletionDisplayOptions, Project};
use settings::{
BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets, infer_json_indent_size,
@@ -2883,8 +2883,12 @@ impl KeybindingEditorModal {
format!("Saved edits to the {} action.", humanized_action_name),
cx,
move |this, _cx| {
- this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
- .dismiss_button(true)
+ this.icon(
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Success),
+ )
+ .dismiss_button(true)
// .action("Undo", f) todo: wire the undo functionality
},
);
@@ -5,41 +5,13 @@ use ui::{Tooltip, prelude::*};
use workspace::{ToastAction, ToastView};
use zed_actions::toast;
-#[derive(Clone, Copy)]
-pub struct ToastIcon {
- icon: IconName,
- color: Color,
-}
-
-impl ToastIcon {
- pub fn new(icon: IconName) -> Self {
- Self {
- icon,
- color: Color::default(),
- }
- }
-
- pub fn color(mut self, color: Color) -> Self {
- self.color = color;
- self
- }
-}
-
-impl From<IconName> for ToastIcon {
- fn from(icon: IconName) -> Self {
- Self {
- icon,
- color: Color::default(),
- }
- }
-}
-
#[derive(RegisterComponent)]
pub struct StatusToast {
- icon: Option<ToastIcon>,
+ icon: Option<Icon>,
text: SharedString,
action: Option<ToastAction>,
show_dismiss: bool,
+ auto_dismiss: bool,
this_handle: Entity<Self>,
focus_handle: FocusHandle,
}
@@ -59,6 +31,7 @@ impl StatusToast {
icon: None,
action: None,
show_dismiss: false,
+ auto_dismiss: true,
this_handle: cx.entity(),
focus_handle,
},
@@ -67,11 +40,16 @@ impl StatusToast {
})
}
- pub fn icon(mut self, icon: ToastIcon) -> Self {
+ pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
+ pub fn auto_dismiss(mut self, auto_dismiss: bool) -> Self {
+ self.auto_dismiss = auto_dismiss;
+ self
+ }
+
pub fn action(
mut self,
label: impl Into<SharedString>,
@@ -116,9 +94,7 @@ impl Render for StatusToast {
.flex_none()
.bg(cx.theme().colors().surface_background)
.shadow_lg()
- .when_some(self.icon.as_ref(), |this, icon| {
- this.child(Icon::new(icon.icon).color(icon.color))
- })
+ .when_some(self.icon.clone(), |this, icon| this.child(icon))
.child(Label::new(self.text.clone()).color(Color::Default))
.when_some(self.action.as_ref(), |this, action| {
this.child(
@@ -155,6 +131,10 @@ impl ToastView for StatusToast {
fn action(&self) -> Option<ToastAction> {
self.action.clone()
}
+
+ fn auto_dismiss(&self) -> bool {
+ self.auto_dismiss
+ }
}
impl Focusable for StatusToast {
@@ -183,33 +163,55 @@ impl Component for StatusToast {
let icon_example = StatusToast::new(
"Nathan Sobo accepted your contact request",
cx,
- |this, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)),
+ |this, _| {
+ this.icon(
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ },
);
let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| {
- this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
+ this.icon(
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Success),
+ )
});
let error_example = StatusToast::new(
"git push: Couldn't find remote origin `iamnbutler/zed`",
cx,
|this, _cx| {
- this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
- .action("More Info", |_, _| {})
+ this.icon(
+ Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error),
+ )
+ .action("More Info", |_, _| {})
},
);
let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| {
- this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
- .action("More Info", |_, _| {})
+ this.icon(
+ Icon::new(IconName::Warning)
+ .size(IconSize::Small)
+ .color(Color::Warning),
+ )
+ .action("More Info", |_, _| {})
});
let pr_example =
StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
- this.icon(ToastIcon::new(IconName::GitBranch).color(Color::Muted))
- .action("Open Pull Request", |_, cx| {
- cx.open_url("https://github.com/")
- })
+ this.icon(
+ Icon::new(IconName::GitBranch)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .action("Open Pull Request", |_, cx| {
+ cx.open_url("https://github.com/")
+ })
});
Some(
@@ -7,7 +7,7 @@ use gpui::{
FocusHandle, Focusable, Global, IntoElement, KeyContext, Render, ScrollHandle, SharedString,
Subscription, Task, WeakEntity, Window, actions,
};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::{SettingsStore, VsCodeSettingsSource};
@@ -495,8 +495,12 @@ pub async fn handle_import_vscode_settings(
format!("Your {} settings were successfully imported.", source),
cx,
|this, _| {
- this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
- .dismiss_button(true)
+ this.icon(
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Success),
+ )
+ .dismiss_button(true)
},
);
SettingsImportState::update(cx, |state, _| match source {
@@ -514,11 +518,15 @@ pub async fn handle_import_vscode_settings(
"Failed to import settings. See log for details",
cx,
|this, _| {
- this.icon(ToastIcon::new(IconName::Close).color(Color::Error))
- .action("Open Log", |window, cx| {
- window.dispatch_action(workspace::OpenLog.boxed_clone(), cx)
- })
- .dismiss_button(true)
+ this.icon(
+ Icon::new(IconName::Close)
+ .size(IconSize::Small)
+ .color(Color::Error),
+ )
+ .action("Open Log", |window, cx| {
+ window.dispatch_action(workspace::OpenLog.boxed_clone(), cx)
+ })
+ .dismiss_button(true)
},
);
workspace.toggle_status_toast(error_toast, cx);
@@ -31,7 +31,7 @@ use gpui::{
};
use language::DiagnosticSeverity;
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
use project::{
Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
ProjectPath, Worktree, WorktreeId,
@@ -2275,8 +2275,12 @@ impl ProjectPanel {
.update(cx, |panel, cx| {
let message = format!("Failed to restore {}: {}", file_name, e);
let toast = StatusToast::new(message, cx, |this, _| {
- this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
- .dismiss_button(true)
+ this.icon(
+ Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error),
+ )
+ .dismiss_button(true)
});
panel
.workspace
@@ -15,32 +15,39 @@ 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 title_bar = |id: &'static str, width: DefiniteLength, duration_ms: u64| {
+ let loading_bar = |id: &'static str, width: DefiniteLength, duration_ms: u64| {
div()
- .h_2()
+ .h(rems_from_px(5.))
.w(width)
.rounded_full()
- .debug_bg_blue()
.bg(cx.theme().colors().element_selected)
.with_animation(
id,
Animation::new(Duration::from_millis(duration_ms))
.repeat()
- .with_easing(pulsating_between(0.4, 0.8)),
+ .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_1()
+ .gap_0p5()
.child(
Icon::new(IconName::GitWorktree)
.color(Color::Muted)
- .size(IconSize::XSmall),
+ .size(IconSize::Indicator),
)
.child(
Label::new(worktree)
@@ -56,51 +63,53 @@ impl RenderOnce for ParallelAgentsIllustration {
.alpha(0.5)
};
- let agent = |id: &'static str,
- icon: IconName,
- width: DefiniteLength,
- duration_ms: u64,
- data: Vec<AnyElement>| {
+ let agent = |title: SharedString, icon: IconName, selected: bool, data: Vec<AnyElement>| {
v_flex()
- .p_2()
+ .when(selected, |this| {
+ this.bg(cx.theme().colors().element_active.opacity(0.2))
+ })
+ .p_1()
.child(
h_flex()
.w_full()
- .gap_2()
+ .gap_1()
.child(
icon_container()
- .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)),
+ .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted)),
)
- .child(title_bar(id, width, duration_ms)),
+ .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_2()
+ .gap_1()
.child(icon_container())
.children(data),
)
};
let agents = v_flex()
- .absolute()
- .w(rems_from_px(380.))
- .top_8()
- .rounded_t_sm()
- .border_1()
- .border_color(cx.theme().colors().border.opacity(0.5))
+ .col_span(3)
.bg(cx.theme().colors().elevated_surface_background)
- .shadow_md()
.child(agent(
- "zed-agent-bar",
+ "Fix branch label".into(),
IconName::ZedAgent,
- relative(0.7),
- 1800,
+ true,
vec![
- worktree("happy-tree".into()).into_any_element(),
+ worktree("bug-fix".into()).into_any_element(),
dot_separator().into_any_element(),
- DiffStat::new("ds", 23, 13)
+ DiffStat::new("ds", 5, 2)
.label_size(LabelSize::XSmall)
.into_any_element(),
dot_separator().into_any_element(),
@@ -109,10 +118,9 @@ impl RenderOnce for ParallelAgentsIllustration {
))
.child(Divider::horizontal())
.child(agent(
- "claude-bar",
+ "Improve thread id".into(),
IconName::AiClaude,
- relative(0.85),
- 2400,
+ false,
vec![
DiffStat::new("ds", 120, 84)
.label_size(LabelSize::XSmall)
@@ -123,27 +131,142 @@ impl RenderOnce for ParallelAgentsIllustration {
))
.child(Divider::horizontal())
.child(agent(
- "openai-bar",
+ "Refactor archive view".into(),
IconName::AiOpenAi,
- relative(0.4),
- 3100,
+ false,
vec![
worktree("silent-forest".into()).into_any_element(),
dot_separator().into_any_element(),
time("37m".into()).into_any_element(),
],
- ))
- .child(Divider::horizontal());
+ ));
+
+ 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)
+ .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(agents)
+ .child(workspace)
}
}
@@ -113,6 +113,7 @@ impl From<IconName> for Icon {
}
/// The source of an icon.
+#[derive(Clone)]
enum IconSource {
/// An SVG embedded in the Zed binary.
Embedded(SharedString),
@@ -126,7 +127,7 @@ enum IconSource {
ExternalSvg(SharedString),
}
-#[derive(IntoElement, RegisterComponent)]
+#[derive(Clone, IntoElement, RegisterComponent)]
pub struct Icon {
source: IconSource,
color: Color,
@@ -44,6 +44,10 @@ pub fn init(cx: &mut App) {
pub trait ToastView: ManagedView {
fn action(&self) -> Option<ToastAction>;
+
+ fn auto_dismiss(&self) -> bool {
+ true
+ }
}
#[derive(Clone)]
@@ -131,6 +135,7 @@ impl ToastLayer {
V: ToastView,
{
let action = new_toast.read(cx).action();
+ let auto_dismiss = new_toast.read(cx).auto_dismiss();
let focus_handle = cx.focus_handle();
self.active_toast = Some(ActiveToast {
@@ -143,7 +148,9 @@ impl ToastLayer {
focus_handle,
});
- self.start_dismiss_timer(DEFAULT_TOAST_DURATION, cx);
+ if auto_dismiss {
+ self.start_dismiss_timer(DEFAULT_TOAST_DURATION, cx);
+ }
cx.notify();
}