agent: Add new thread start buttons to the empty state (#34829)

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-07-21 20:39:29 -03:00 committed by GitHub
parent 19ab1eb792
commit 8515487bbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 297 additions and 38 deletions

View file

@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
use crate::NewExternalAgentThread;
use crate::agent_diff::AgentDiffThread;
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
use crate::ui::NewThreadButton;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@ -66,8 +67,8 @@ use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
use ui::{
Banner, Callout, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
ProgressBar, Tab, Tooltip, prelude::*,
Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu,
PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::{
@ -1906,16 +1907,39 @@ impl AgentPanel {
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.header("Zed Agent")
})
.action("New Thread", NewThread::default().boxed_clone())
.action("New Text Thread", NewTextThread.boxed_clone())
.item(
ContextMenuEntry::new("New Thread")
.icon(IconName::NewThread)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(NewThread::default().boxed_clone(), cx);
}),
)
.item(
ContextMenuEntry::new("New Text Thread")
.icon(IconName::NewTextThread)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(NewTextThread.boxed_clone(), cx);
}),
)
.when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx);
if !thread.is_empty() {
this.action(
"New From Summary",
Box::new(NewThread {
from_thread_id: Some(thread.id().clone()),
}),
let thread_id = thread.id().clone();
this.item(
ContextMenuEntry::new("New From Summary")
.icon(IconName::NewFromSummary)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(
Box::new(NewThread {
from_thread_id: Some(thread_id.clone()),
}),
cx,
);
}),
)
} else {
this
@ -1924,19 +1948,33 @@ impl AgentPanel {
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.separator()
.header("External Agents")
.action(
"New Gemini Thread",
NewExternalAgentThread {
agent: Some(crate::ExternalAgent::Gemini),
}
.boxed_clone(),
.item(
ContextMenuEntry::new("New Gemini Thread")
.icon(IconName::AiGemini)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(crate::ExternalAgent::Gemini),
}
.boxed_clone(),
cx,
);
}),
)
.action(
"New Claude Code Thread",
NewExternalAgentThread {
agent: Some(crate::ExternalAgent::ClaudeCode),
}
.boxed_clone(),
.item(
ContextMenuEntry::new("New Claude Code Thread")
.icon(IconName::AiClaude)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(crate::ExternalAgent::ClaudeCode),
}
.boxed_clone(),
cx,
);
}),
)
});
menu
@ -2285,6 +2323,28 @@ impl AgentPanel {
})))
}
fn render_empty_state_section_header(
&self,
label: impl Into<SharedString>,
action_slot: Option<AnyElement>,
cx: &mut Context<Self>,
) -> impl IntoElement {
h_flex()
.mt_2()
.pl_1p5()
.pb_1()
.w_full()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
Label::new(label.into())
.size(LabelSize::Small)
.color(Color::Muted),
)
.children(action_slot)
}
fn render_thread_empty_state(
&self,
window: &mut Window,
@ -2407,19 +2467,9 @@ impl AgentPanel {
.justify_end()
.gap_1()
.child(
h_flex()
.pl_1p5()
.pb_1()
.w_full()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
Label::new("Recent")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
self.render_empty_state_section_header(
"Recent",
Some(
Button::new("view-history", "View All")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
@ -2434,8 +2484,11 @@ impl AgentPanel {
)
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
})
.into_any_element(),
),
cx,
),
)
.child(
v_flex()
@ -2463,6 +2516,113 @@ impl AgentPanel {
},
)),
)
.child(self.render_empty_state_section_header("Start", None, cx))
.child(
v_flex()
.p_1()
.gap_2()
.child(
h_flex()
.w_full()
.gap_2()
.child(
NewThreadButton::new(
"new-thread-btn",
"New Thread",
IconName::NewThread,
)
.keybinding(KeyBinding::for_action_in(
&NewThread::default(),
&self.focus_handle(cx),
window,
cx,
))
.on_click(
|window, cx| {
window.dispatch_action(
NewThread::default().boxed_clone(),
cx,
)
},
),
)
.child(
NewThreadButton::new(
"new-text-thread-btn",
"New Text Thread",
IconName::NewTextThread,
)
.keybinding(KeyBinding::for_action_in(
&NewTextThread,
&self.focus_handle(cx),
window,
cx,
))
.on_click(
|window, cx| {
window.dispatch_action(Box::new(NewTextThread), cx)
},
),
),
)
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.child(
h_flex()
.w_full()
.gap_2()
.child(
NewThreadButton::new(
"new-gemini-thread-btn",
"New Gemini Thread",
IconName::AiGemini,
)
// .keybinding(KeyBinding::for_action_in(
// &OpenHistory,
// &self.focus_handle(cx),
// window,
// cx,
// ))
.on_click(
|window, cx| {
window.dispatch_action(
Box::new(NewExternalAgentThread {
agent: Some(
crate::ExternalAgent::Gemini,
),
}),
cx,
)
},
),
)
.child(
NewThreadButton::new(
"new-claude-thread-btn",
"New Claude Code Thread",
IconName::AiClaude,
)
// .keybinding(KeyBinding::for_action_in(
// &OpenHistory,
// &self.focus_handle(cx),
// window,
// cx,
// ))
.on_click(
|window, cx| {
window.dispatch_action(
Box::new(NewExternalAgentThread {
agent: Some(
crate::ExternalAgent::ClaudeCode,
),
}),
cx,
)
},
),
),
)
}),
)
.when_some(configuration_error.as_ref(), |this, err| {
this.child(self.render_configuration_error(err, &focus_handle, window, cx))
})

View file

@ -2,6 +2,7 @@ mod agent_notification;
mod burn_mode_tooltip;
mod context_pill;
mod end_trial_upsell;
mod new_thread_button;
mod onboarding_modal;
pub mod preview;
mod upsell;
@ -10,4 +11,5 @@ pub use agent_notification::*;
pub use burn_mode_tooltip::*;
pub use context_pill::*;
pub use end_trial_upsell::*;
pub use new_thread_button::*;
pub use onboarding_modal::*;

View file

@ -0,0 +1,75 @@
use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled};
use ui::prelude::*;
#[derive(IntoElement)]
pub struct NewThreadButton {
id: ElementId,
label: SharedString,
icon: IconName,
keybinding: Option<ui::KeyBinding>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
impl NewThreadButton {
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self {
Self {
id: id.into(),
label: label.into(),
icon,
keybinding: None,
on_click: None,
}
}
pub fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self {
self.keybinding = keybinding;
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn(&mut Window, &mut App) + 'static,
{
self.on_click = Some(Box::new(
move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx),
));
self
}
}
impl RenderOnce for NewThreadButton {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.id(self.id)
.w_full()
.py_1p5()
.px_2()
.gap_1()
.justify_between()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border.opacity(0.4))
.bg(cx.theme().colors().element_active.opacity(0.2))
.hover(|style| {
style
.bg(cx.theme().colors().element_hover)
.border_color(cx.theme().colors().border)
})
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(self.icon)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(self.label).size(LabelSize::Small)),
)
.when_some(self.keybinding, |this, keybinding| {
this.child(keybinding.size(rems_from_px(10.)))
})
.when_some(self.on_click, |this, on_click| {
this.on_click(move |event, window, cx| on_click(event, window, cx))
})
}
}