thread_view: Adjust empty state and error displays (#36774)
Also changes the message editor placeholder depending on the agent. Release Notes: - N/A --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
896a35f7be
commit
639417c2bc
4 changed files with 254 additions and 267 deletions
|
@ -23,11 +23,11 @@ impl NativeAgentServer {
|
||||||
|
|
||||||
impl AgentServer for NativeAgentServer {
|
impl AgentServer for NativeAgentServer {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"Native Agent"
|
"Zed Agent"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty_state_headline(&self) -> &'static str {
|
fn empty_state_headline(&self) -> &'static str {
|
||||||
"Welcome to the Agent Panel"
|
self.name()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty_state_message(&self) -> &'static str {
|
fn empty_state_message(&self) -> &'static str {
|
||||||
|
|
|
@ -258,6 +258,7 @@ pub struct AcpThreadView {
|
||||||
hovered_recent_history_item: Option<usize>,
|
hovered_recent_history_item: Option<usize>,
|
||||||
entry_view_state: Entity<EntryViewState>,
|
entry_view_state: Entity<EntryViewState>,
|
||||||
message_editor: Entity<MessageEditor>,
|
message_editor: Entity<MessageEditor>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
model_selector: Option<Entity<AcpModelSelectorPopover>>,
|
model_selector: Option<Entity<AcpModelSelectorPopover>>,
|
||||||
profile_selector: Option<Entity<ProfileSelector>>,
|
profile_selector: Option<Entity<ProfileSelector>>,
|
||||||
notifications: Vec<WindowHandle<AgentNotification>>,
|
notifications: Vec<WindowHandle<AgentNotification>>,
|
||||||
|
@ -312,6 +313,13 @@ impl AcpThreadView {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
|
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
|
||||||
let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some();
|
let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some();
|
||||||
|
|
||||||
|
let placeholder = if agent.name() == "Zed Agent" {
|
||||||
|
format!("Message the {} — @ to include context", agent.name())
|
||||||
|
} else {
|
||||||
|
format!("Message {} — @ to include context", agent.name())
|
||||||
|
};
|
||||||
|
|
||||||
let message_editor = cx.new(|cx| {
|
let message_editor = cx.new(|cx| {
|
||||||
let mut editor = MessageEditor::new(
|
let mut editor = MessageEditor::new(
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
|
@ -319,7 +327,7 @@ impl AcpThreadView {
|
||||||
history_store.clone(),
|
history_store.clone(),
|
||||||
prompt_store.clone(),
|
prompt_store.clone(),
|
||||||
prompt_capabilities.clone(),
|
prompt_capabilities.clone(),
|
||||||
"Message the agent — @ to include context",
|
placeholder,
|
||||||
prevent_slash_commands,
|
prevent_slash_commands,
|
||||||
editor::EditorMode::AutoHeight {
|
editor::EditorMode::AutoHeight {
|
||||||
min_lines: MIN_EDITOR_LINES,
|
min_lines: MIN_EDITOR_LINES,
|
||||||
|
@ -381,6 +389,7 @@ impl AcpThreadView {
|
||||||
prompt_capabilities,
|
prompt_capabilities,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
_cancel_task: None,
|
_cancel_task: None,
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,8 +413,12 @@ impl AcpThreadView {
|
||||||
let connection = match connect_task.await {
|
let connection = match connect_task.await {
|
||||||
Ok(connection) => connection,
|
Ok(connection) => connection,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.handle_load_error(err, cx);
|
if err.downcast_ref::<LoadError>().is_some() {
|
||||||
|
this.handle_load_error(err, window, cx);
|
||||||
|
} else {
|
||||||
|
this.handle_thread_error(err, cx);
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
|
@ -522,6 +535,7 @@ impl AcpThreadView {
|
||||||
title_editor,
|
title_editor,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
};
|
};
|
||||||
|
this.message_editor.focus_handle(cx).focus(window);
|
||||||
|
|
||||||
this.profile_selector = this.as_native_thread(cx).map(|thread| {
|
this.profile_selector = this.as_native_thread(cx).map(|thread| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
|
@ -537,7 +551,7 @@ impl AcpThreadView {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
this.handle_load_error(err, cx);
|
this.handle_load_error(err, window, cx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
@ -606,17 +620,28 @@ impl AcpThreadView {
|
||||||
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
|
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
};
|
};
|
||||||
|
if this.message_editor.focus_handle(cx).is_focused(window) {
|
||||||
|
this.focus_handle.focus(window)
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) {
|
fn handle_load_error(
|
||||||
|
&mut self,
|
||||||
|
err: anyhow::Error,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
if let Some(load_err) = err.downcast_ref::<LoadError>() {
|
if let Some(load_err) = err.downcast_ref::<LoadError>() {
|
||||||
self.thread_state = ThreadState::LoadError(load_err.clone());
|
self.thread_state = ThreadState::LoadError(load_err.clone());
|
||||||
} else {
|
} else {
|
||||||
self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
|
self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
|
||||||
}
|
}
|
||||||
|
if self.message_editor.focus_handle(cx).is_focused(window) {
|
||||||
|
self.focus_handle.focus(window)
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -633,12 +658,11 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn title(&self, cx: &App) -> SharedString {
|
pub fn title(&self) -> SharedString {
|
||||||
match &self.thread_state {
|
match &self.thread_state {
|
||||||
ThreadState::Ready { thread, .. } => thread.read(cx).title(),
|
ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
|
||||||
ThreadState::Loading { .. } => "Loading…".into(),
|
ThreadState::Loading { .. } => "Loading…".into(),
|
||||||
ThreadState::LoadError(_) => "Failed to load".into(),
|
ThreadState::LoadError(_) => "Failed to load".into(),
|
||||||
ThreadState::Unauthenticated { .. } => "Authentication Required".into(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1069,6 +1093,9 @@ impl AcpThreadView {
|
||||||
AcpThreadEvent::LoadError(error) => {
|
AcpThreadEvent::LoadError(error) => {
|
||||||
self.thread_retry_status.take();
|
self.thread_retry_status.take();
|
||||||
self.thread_state = ThreadState::LoadError(error.clone());
|
self.thread_state = ThreadState::LoadError(error.clone());
|
||||||
|
if self.message_editor.focus_handle(cx).is_focused(window) {
|
||||||
|
self.focus_handle.focus(window)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AcpThreadEvent::TitleUpdated => {
|
AcpThreadEvent::TitleUpdated => {
|
||||||
let title = thread.read(cx).title();
|
let title = thread.read(cx).title();
|
||||||
|
@ -2338,33 +2365,6 @@ impl AcpThreadView {
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_agent_logo(&self) -> AnyElement {
|
|
||||||
Icon::new(self.agent.logo())
|
|
||||||
.color(Color::Muted)
|
|
||||||
.size(IconSize::XLarge)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_error_agent_logo(&self) -> AnyElement {
|
|
||||||
let logo = Icon::new(self.agent.logo())
|
|
||||||
.color(Color::Muted)
|
|
||||||
.size(IconSize::XLarge)
|
|
||||||
.into_any_element();
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.relative()
|
|
||||||
.justify_center()
|
|
||||||
.child(div().opacity(0.3).child(logo))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.absolute()
|
|
||||||
.right_1()
|
|
||||||
.bottom_0()
|
|
||||||
.child(Icon::new(IconName::XCircleFilled).color(Color::Error)),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
||||||
let project_context = self
|
let project_context = self
|
||||||
.as_native_thread(cx)?
|
.as_native_thread(cx)?
|
||||||
|
@ -2493,8 +2493,7 @@ impl AcpThreadView {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_empty_state(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
fn render_recent_history(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
|
|
||||||
let render_history = self
|
let render_history = self
|
||||||
.agent
|
.agent
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -2506,38 +2505,6 @@ impl AcpThreadView {
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.when(!render_history, |this| {
|
|
||||||
this.child(
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.child(if loading {
|
|
||||||
h_flex()
|
|
||||||
.justify_center()
|
|
||||||
.child(self.render_agent_logo())
|
|
||||||
.with_animation(
|
|
||||||
"pulsating_icon",
|
|
||||||
Animation::new(Duration::from_secs(2))
|
|
||||||
.repeat()
|
|
||||||
.with_easing(pulsating_between(0.4, 1.0)),
|
|
||||||
|icon, delta| icon.opacity(delta),
|
|
||||||
)
|
|
||||||
.into_any()
|
|
||||||
} else {
|
|
||||||
self.render_agent_logo().into_any_element()
|
|
||||||
})
|
|
||||||
.child(h_flex().mt_4().mb_2().justify_center().child(if loading {
|
|
||||||
div()
|
|
||||||
.child(LoadingLabel::new("").size(LabelSize::Large))
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
Headline::new(self.agent.empty_state_headline())
|
|
||||||
.size(HeadlineSize::Medium)
|
|
||||||
.into_any_element()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(render_history, |this| {
|
.when(render_history, |this| {
|
||||||
let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| {
|
let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| {
|
||||||
history_store.entries().take(3).collect()
|
history_store.entries().take(3).collect()
|
||||||
|
@ -2612,196 +2579,118 @@ impl AcpThreadView {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> Div {
|
) -> Div {
|
||||||
v_flex()
|
v_flex().flex_1().size_full().justify_end().child(
|
||||||
.p_2()
|
v_flex()
|
||||||
.gap_2()
|
.p_2()
|
||||||
.flex_1()
|
.pr_3()
|
||||||
.items_center()
|
.w_full()
|
||||||
.justify_center()
|
.border_t_1()
|
||||||
.child(
|
.border_color(cx.theme().colors().border)
|
||||||
v_flex()
|
.bg(cx.theme().status().warning.opacity(0.04))
|
||||||
.items_center()
|
.child(
|
||||||
.justify_center()
|
h_flex()
|
||||||
.child(self.render_error_agent_logo())
|
.gap_1p5()
|
||||||
.child(
|
.child(
|
||||||
h_flex().mt_4().mb_1().justify_center().child(
|
Icon::new(IconName::Warning)
|
||||||
Headline::new("Authentication Required").size(HeadlineSize::Medium),
|
.color(Color::Warning)
|
||||||
|
.size(IconSize::Small),
|
||||||
|
)
|
||||||
|
.child(Label::new("Authentication Required")),
|
||||||
|
)
|
||||||
|
.children(description.map(|desc| {
|
||||||
|
div().text_ui(cx).child(self.render_markdown(
|
||||||
|
desc.clone(),
|
||||||
|
default_markdown_style(false, false, window, cx),
|
||||||
|
))
|
||||||
|
}))
|
||||||
|
.children(
|
||||||
|
configuration_view
|
||||||
|
.cloned()
|
||||||
|
.map(|view| div().w_full().child(view)),
|
||||||
|
)
|
||||||
|
.when(
|
||||||
|
configuration_view.is_none()
|
||||||
|
&& description.is_none()
|
||||||
|
&& pending_auth_method.is_none(),
|
||||||
|
|el| {
|
||||||
|
el.child(
|
||||||
|
Label::new(format!(
|
||||||
|
"You are not currently authenticated with {}. Please choose one of the following options:",
|
||||||
|
self.agent.name()
|
||||||
|
))
|
||||||
|
.color(Color::Muted)
|
||||||
|
.mb_1()
|
||||||
|
.ml_5(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.when(!connection.auth_methods().is_empty(), |this| {
|
||||||
|
this.child(
|
||||||
|
h_flex().justify_end().flex_wrap().gap_1().children(
|
||||||
|
connection.auth_methods().iter().enumerate().rev().map(
|
||||||
|
|(ix, method)| {
|
||||||
|
Button::new(
|
||||||
|
SharedString::from(method.id.0.clone()),
|
||||||
|
method.name.clone(),
|
||||||
|
)
|
||||||
|
.when(ix == 0, |el| {
|
||||||
|
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||||
|
})
|
||||||
|
.on_click({
|
||||||
|
let method_id = method.id.clone();
|
||||||
|
cx.listener(move |this, _, window, cx| {
|
||||||
|
this.authenticate(method_id.clone(), window, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.into_any(),
|
})
|
||||||
)
|
.when_some(pending_auth_method, |el, _| {
|
||||||
.children(description.map(|desc| {
|
|
||||||
div().text_ui(cx).text_center().child(self.render_markdown(
|
|
||||||
desc.clone(),
|
|
||||||
default_markdown_style(false, false, window, cx),
|
|
||||||
))
|
|
||||||
}))
|
|
||||||
.children(
|
|
||||||
configuration_view
|
|
||||||
.cloned()
|
|
||||||
.map(|view| div().px_4().w_full().max_w_128().child(view)),
|
|
||||||
)
|
|
||||||
.when(
|
|
||||||
configuration_view.is_none()
|
|
||||||
&& description.is_none()
|
|
||||||
&& pending_auth_method.is_none(),
|
|
||||||
|el| {
|
|
||||||
el.child(
|
el.child(
|
||||||
div()
|
h_flex()
|
||||||
.text_ui(cx)
|
.py_4()
|
||||||
.text_center()
|
|
||||||
.px_4()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.max_w_128()
|
.justify_center()
|
||||||
.child(Label::new("Authentication required")),
|
.gap_1()
|
||||||
)
|
.child(
|
||||||
},
|
Icon::new(IconName::ArrowCircle)
|
||||||
)
|
.size(IconSize::Small)
|
||||||
.when_some(pending_auth_method, |el, _| {
|
.color(Color::Muted)
|
||||||
let spinner_icon = div()
|
.with_animation(
|
||||||
.px_0p5()
|
"arrow-circle",
|
||||||
.id("generating")
|
Animation::new(Duration::from_secs(2)).repeat(),
|
||||||
.tooltip(Tooltip::text("Generating Changes…"))
|
|icon, delta| {
|
||||||
.child(
|
icon.transform(Transformation::rotate(percentage(
|
||||||
Icon::new(IconName::ArrowCircle)
|
delta,
|
||||||
.size(IconSize::Small)
|
)))
|
||||||
.with_animation(
|
},
|
||||||
"arrow-circle",
|
)
|
||||||
Animation::new(Duration::from_secs(2)).repeat(),
|
.into_any_element(),
|
||||||
|icon, delta| {
|
|
||||||
icon.transform(Transformation::rotate(percentage(delta)))
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.into_any_element(),
|
.child(Label::new("Authenticating…")),
|
||||||
)
|
)
|
||||||
.into_any();
|
}),
|
||||||
el.child(
|
)
|
||||||
h_flex()
|
|
||||||
.text_ui(cx)
|
|
||||||
.text_center()
|
|
||||||
.justify_center()
|
|
||||||
.gap_2()
|
|
||||||
.px_4()
|
|
||||||
.w_full()
|
|
||||||
.max_w_128()
|
|
||||||
.child(Label::new("Authenticating..."))
|
|
||||||
.child(spinner_icon),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.mt_1p5()
|
|
||||||
.gap_1()
|
|
||||||
.flex_wrap()
|
|
||||||
.justify_center()
|
|
||||||
.children(connection.auth_methods().iter().enumerate().rev().map(
|
|
||||||
|(ix, method)| {
|
|
||||||
Button::new(
|
|
||||||
SharedString::from(method.id.0.clone()),
|
|
||||||
method.name.clone(),
|
|
||||||
)
|
|
||||||
.style(ButtonStyle::Outlined)
|
|
||||||
.when(ix == 0, |el| {
|
|
||||||
el.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
|
||||||
})
|
|
||||||
.size(ButtonSize::Medium)
|
|
||||||
.label_size(LabelSize::Small)
|
|
||||||
.on_click({
|
|
||||||
let method_id = method.id.clone();
|
|
||||||
cx.listener(move |this, _, window, cx| {
|
|
||||||
this.authenticate(method_id.clone(), window, cx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
|
fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
|
||||||
let mut container = v_flex()
|
let (message, action_slot) = match e {
|
||||||
.items_center()
|
LoadError::NotInstalled {
|
||||||
.justify_center()
|
error_message,
|
||||||
.child(self.render_error_agent_logo())
|
install_message,
|
||||||
.child(
|
install_command,
|
||||||
v_flex()
|
} => {
|
||||||
.mt_4()
|
let install_command = install_command.clone();
|
||||||
.mb_2()
|
let button = Button::new("install", install_message)
|
||||||
.gap_0p5()
|
|
||||||
.text_center()
|
|
||||||
.items_center()
|
|
||||||
.child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
|
|
||||||
.child(
|
|
||||||
Label::new(e.to_string())
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let LoadError::Unsupported {
|
|
||||||
upgrade_message,
|
|
||||||
upgrade_command,
|
|
||||||
..
|
|
||||||
} = &e
|
|
||||||
{
|
|
||||||
let upgrade_message = upgrade_message.clone();
|
|
||||||
let upgrade_command = upgrade_command.clone();
|
|
||||||
container = container.child(
|
|
||||||
Button::new("upgrade", upgrade_message)
|
|
||||||
.tooltip(Tooltip::text(upgrade_command.clone()))
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
let task = this
|
|
||||||
.workspace
|
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
let project = workspace.project().read(cx);
|
|
||||||
let cwd = project.first_project_directory(cx);
|
|
||||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
|
||||||
let spawn_in_terminal = task::SpawnInTerminal {
|
|
||||||
id: task::TaskId("upgrade".to_string()),
|
|
||||||
full_label: upgrade_command.clone(),
|
|
||||||
label: upgrade_command.clone(),
|
|
||||||
command: Some(upgrade_command.clone()),
|
|
||||||
args: Vec::new(),
|
|
||||||
command_label: upgrade_command.clone(),
|
|
||||||
cwd,
|
|
||||||
env: Default::default(),
|
|
||||||
use_new_terminal: true,
|
|
||||||
allow_concurrent_runs: true,
|
|
||||||
reveal: Default::default(),
|
|
||||||
reveal_target: Default::default(),
|
|
||||||
hide: Default::default(),
|
|
||||||
shell,
|
|
||||||
show_summary: true,
|
|
||||||
show_command: true,
|
|
||||||
show_rerun: false,
|
|
||||||
};
|
|
||||||
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
let Some(task) = task else { return };
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
if let Some(Ok(_)) = task.await {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.reset(window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach()
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
} else if let LoadError::NotInstalled {
|
|
||||||
install_message,
|
|
||||||
install_command,
|
|
||||||
..
|
|
||||||
} = e
|
|
||||||
{
|
|
||||||
let install_message = install_message.clone();
|
|
||||||
let install_command = install_command.clone();
|
|
||||||
container = container.child(
|
|
||||||
Button::new("install", install_message)
|
|
||||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
|
||||||
.size(ButtonSize::Medium)
|
|
||||||
.tooltip(Tooltip::text(install_command.clone()))
|
.tooltip(Tooltip::text(install_command.clone()))
|
||||||
|
.style(ButtonStyle::Outlined)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.icon(IconName::Download)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
let task = this
|
let task = this
|
||||||
.workspace
|
.workspace
|
||||||
|
@ -2841,11 +2730,81 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach()
|
.detach()
|
||||||
})),
|
}));
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
container.into_any()
|
(error_message.clone(), Some(button.into_any_element()))
|
||||||
|
}
|
||||||
|
LoadError::Unsupported {
|
||||||
|
error_message,
|
||||||
|
upgrade_message,
|
||||||
|
upgrade_command,
|
||||||
|
} => {
|
||||||
|
let upgrade_command = upgrade_command.clone();
|
||||||
|
let button = Button::new("upgrade", upgrade_message)
|
||||||
|
.tooltip(Tooltip::text(upgrade_command.clone()))
|
||||||
|
.style(ButtonStyle::Outlined)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.icon(IconName::Download)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
let task = this
|
||||||
|
.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let project = workspace.project().read(cx);
|
||||||
|
let cwd = project.first_project_directory(cx);
|
||||||
|
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||||
|
let spawn_in_terminal = task::SpawnInTerminal {
|
||||||
|
id: task::TaskId("upgrade".to_string()),
|
||||||
|
full_label: upgrade_command.clone(),
|
||||||
|
label: upgrade_command.clone(),
|
||||||
|
command: Some(upgrade_command.clone()),
|
||||||
|
args: Vec::new(),
|
||||||
|
command_label: upgrade_command.clone(),
|
||||||
|
cwd,
|
||||||
|
env: Default::default(),
|
||||||
|
use_new_terminal: true,
|
||||||
|
allow_concurrent_runs: true,
|
||||||
|
reveal: Default::default(),
|
||||||
|
reveal_target: Default::default(),
|
||||||
|
hide: Default::default(),
|
||||||
|
shell,
|
||||||
|
show_summary: true,
|
||||||
|
show_command: true,
|
||||||
|
show_rerun: false,
|
||||||
|
};
|
||||||
|
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
let Some(task) = task else { return };
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
if let Some(Ok(_)) = task.await {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.reset(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach()
|
||||||
|
}));
|
||||||
|
|
||||||
|
(error_message.clone(), Some(button.into_any_element()))
|
||||||
|
}
|
||||||
|
LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
|
||||||
|
LoadError::Other(msg) => (
|
||||||
|
msg.into(),
|
||||||
|
Some(self.create_copy_button(msg.to_string()).into_any_element()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Callout::new()
|
||||||
|
.severity(Severity::Error)
|
||||||
|
.icon(IconName::XCircleFilled)
|
||||||
|
.title("Failed to Launch")
|
||||||
|
.description(message)
|
||||||
|
.actions_slot(div().children(action_slot))
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_activity_bar(
|
fn render_activity_bar(
|
||||||
|
@ -3336,6 +3295,19 @@ impl AcpThreadView {
|
||||||
(IconName::Maximize, "Expand Message Editor")
|
(IconName::Maximize, "Expand Message Editor")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let backdrop = div()
|
||||||
|
.size_full()
|
||||||
|
.absolute()
|
||||||
|
.inset_0()
|
||||||
|
.bg(cx.theme().colors().panel_background)
|
||||||
|
.opacity(0.8)
|
||||||
|
.block_mouse_except_scroll();
|
||||||
|
|
||||||
|
let enable_editor = match self.thread_state {
|
||||||
|
ThreadState::Loading { .. } | ThreadState::Ready { .. } => true,
|
||||||
|
ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false,
|
||||||
|
};
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.on_action(cx.listener(Self::expand_message_editor))
|
.on_action(cx.listener(Self::expand_message_editor))
|
||||||
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
|
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
|
||||||
|
@ -3411,6 +3383,7 @@ impl AcpThreadView {
|
||||||
.child(self.render_send_button(cx)),
|
.child(self.render_send_button(cx)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.when(!enable_editor, |this| this.child(backdrop))
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3913,18 +3886,19 @@ impl AcpThreadView {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = self.title(cx);
|
// TODO: Change this once we have title summarization for external agents.
|
||||||
|
let title = self.agent.name();
|
||||||
|
|
||||||
match AgentSettings::get_global(cx).notify_when_agent_waiting {
|
match AgentSettings::get_global(cx).notify_when_agent_waiting {
|
||||||
NotifyWhenAgentWaiting::PrimaryScreen => {
|
NotifyWhenAgentWaiting::PrimaryScreen => {
|
||||||
if let Some(primary) = cx.primary_display() {
|
if let Some(primary) = cx.primary_display() {
|
||||||
self.pop_up(icon, caption.into(), title, window, primary, cx);
|
self.pop_up(icon, caption.into(), title.into(), window, primary, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NotifyWhenAgentWaiting::AllScreens => {
|
NotifyWhenAgentWaiting::AllScreens => {
|
||||||
let caption = caption.into();
|
let caption = caption.into();
|
||||||
for screen in cx.displays() {
|
for screen in cx.displays() {
|
||||||
self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
|
self.pop_up(icon, caption.clone(), title.into(), window, screen, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NotifyWhenAgentWaiting::Never => {
|
NotifyWhenAgentWaiting::Never => {
|
||||||
|
@ -4423,6 +4397,7 @@ impl AcpThreadView {
|
||||||
Callout::new()
|
Callout::new()
|
||||||
.severity(Severity::Error)
|
.severity(Severity::Error)
|
||||||
.title("Error")
|
.title("Error")
|
||||||
|
.icon(IconName::XCircle)
|
||||||
.description(error.clone())
|
.description(error.clone())
|
||||||
.actions_slot(self.create_copy_button(error.to_string()))
|
.actions_slot(self.create_copy_button(error.to_string()))
|
||||||
.dismiss_action(self.dismiss_error_button(cx))
|
.dismiss_action(self.dismiss_error_button(cx))
|
||||||
|
@ -4434,6 +4409,7 @@ impl AcpThreadView {
|
||||||
|
|
||||||
Callout::new()
|
Callout::new()
|
||||||
.severity(Severity::Error)
|
.severity(Severity::Error)
|
||||||
|
.icon(IconName::XCircle)
|
||||||
.title("Free Usage Exceeded")
|
.title("Free Usage Exceeded")
|
||||||
.description(ERROR_MESSAGE)
|
.description(ERROR_MESSAGE)
|
||||||
.actions_slot(
|
.actions_slot(
|
||||||
|
@ -4453,6 +4429,7 @@ impl AcpThreadView {
|
||||||
Callout::new()
|
Callout::new()
|
||||||
.severity(Severity::Error)
|
.severity(Severity::Error)
|
||||||
.title("Authentication Required")
|
.title("Authentication Required")
|
||||||
|
.icon(IconName::XCircle)
|
||||||
.description(error.clone())
|
.description(error.clone())
|
||||||
.actions_slot(
|
.actions_slot(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -4478,6 +4455,7 @@ impl AcpThreadView {
|
||||||
Callout::new()
|
Callout::new()
|
||||||
.severity(Severity::Error)
|
.severity(Severity::Error)
|
||||||
.title("Model Prompt Limit Reached")
|
.title("Model Prompt Limit Reached")
|
||||||
|
.icon(IconName::XCircle)
|
||||||
.description(error_message)
|
.description(error_message)
|
||||||
.actions_slot(
|
.actions_slot(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -4648,7 +4626,14 @@ impl AcpThreadView {
|
||||||
|
|
||||||
impl Focusable for AcpThreadView {
|
impl Focusable for AcpThreadView {
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
self.message_editor.focus_handle(cx)
|
match self.thread_state {
|
||||||
|
ThreadState::Loading { .. } | ThreadState::Ready { .. } => {
|
||||||
|
self.message_editor.focus_handle(cx)
|
||||||
|
}
|
||||||
|
ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4664,6 +4649,7 @@ impl Render for AcpThreadView {
|
||||||
.on_action(cx.listener(Self::toggle_burn_mode))
|
.on_action(cx.listener(Self::toggle_burn_mode))
|
||||||
.on_action(cx.listener(Self::keep_all))
|
.on_action(cx.listener(Self::keep_all))
|
||||||
.on_action(cx.listener(Self::reject_all))
|
.on_action(cx.listener(Self::reject_all))
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
.bg(cx.theme().colors().panel_background)
|
.bg(cx.theme().colors().panel_background)
|
||||||
.child(match &self.thread_state {
|
.child(match &self.thread_state {
|
||||||
ThreadState::Unauthenticated {
|
ThreadState::Unauthenticated {
|
||||||
|
@ -4680,14 +4666,14 @@ impl Render for AcpThreadView {
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
ThreadState::Loading { .. } => {
|
ThreadState::Loading { .. } => v_flex()
|
||||||
v_flex().flex_1().child(self.render_empty_state(window, cx))
|
|
||||||
}
|
|
||||||
ThreadState::LoadError(e) => v_flex()
|
|
||||||
.p_2()
|
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
.child(self.render_recent_history(window, cx)),
|
||||||
|
ThreadState::LoadError(e) => v_flex()
|
||||||
|
.flex_1()
|
||||||
|
.size_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_end()
|
||||||
.child(self.render_load_error(e, cx)),
|
.child(self.render_load_error(e, cx)),
|
||||||
ThreadState::Ready { thread, .. } => {
|
ThreadState::Ready { thread, .. } => {
|
||||||
let thread_clone = thread.clone();
|
let thread_clone = thread.clone();
|
||||||
|
@ -4724,7 +4710,7 @@ impl Render for AcpThreadView {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.child(self.render_empty_state(window, cx))
|
this.child(self.render_recent_history(window, cx))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2097,7 +2097,7 @@ impl AgentPanel {
|
||||||
.child(title_editor)
|
.child(title_editor)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
Label::new(thread_view.read(cx).title(cx))
|
Label::new(thread_view.read(cx).title())
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.truncate()
|
.truncate()
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
|
|
@ -132,6 +132,7 @@ impl RenderOnce for Callout {
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
|
.w_full()
|
||||||
.p_2()
|
.p_2()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.items_start()
|
.items_start()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue