Merge branch 'main' into ollama-inline-completions
This commit is contained in:
commit
62b69ffdca
59 changed files with 1899 additions and 1050 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -13538,6 +13538,7 @@ dependencies = [
|
|||
"smol",
|
||||
"sysinfo",
|
||||
"telemetry_events",
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.8.20",
|
||||
"unindent",
|
||||
"util",
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12.286 6H7.048C6.469 6 6 6.469 6 7.048v5.238c0 .578.469 1.047 1.048 1.047h5.238c.578 0 1.047-.469 1.047-1.047V7.048c0-.579-.469-1.048-1.047-1.048Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.714 10a1.05 1.05 0 0 1-1.047-1.048V3.714a1.05 1.05 0 0 1 1.047-1.047h5.238A1.05 1.05 0 0 1 10 3.714"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.486 6.2H7.24795C6.66895 6.2 6.19995 6.669 6.19995 7.248V12.486C6.19995 13.064 6.66895 13.533 7.24795 13.533H12.486C13.064 13.533 13.533 13.064 13.533 12.486V7.248C13.533 6.669 13.064 6.2 12.486 6.2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.91712 10.203C3.63951 10.2022 3.37351 10.0915 3.1773 9.89511C2.98109 9.69872 2.87064 9.43261 2.87012 9.155V3.917C2.87091 3.63956 2.98147 3.37371 3.17765 3.17753C3.37383 2.98135 3.63968 2.87079 3.91712 2.87H9.15512C9.43273 2.87053 9.69883 2.98097 9.89523 3.17718C10.0916 3.37339 10.2023 3.63939 10.2031 3.917" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 515 B After Width: | Height: | Size: 802 B |
|
@ -162,6 +162,12 @@
|
|||
// 2. Always quit the application
|
||||
// "on_last_window_closed": "quit_app",
|
||||
"on_last_window_closed": "platform_default",
|
||||
// Whether to show padding for zoomed panels.
|
||||
// When enabled, zoomed center panels (e.g. code editor) will have padding all around,
|
||||
// while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
|
||||
//
|
||||
// Default: true
|
||||
"zoomed_padding": true,
|
||||
// Whether to use the system provided dialogs for Open and Save As.
|
||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||
"use_system_path_prompts": true,
|
||||
|
@ -1629,6 +1635,9 @@
|
|||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Kotlin": {
|
||||
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
|
||||
},
|
||||
"LaTeX": {
|
||||
"formatter": "language_server",
|
||||
"language_servers": ["texlab", "..."],
|
||||
|
|
|
@ -183,16 +183,15 @@ impl ToolCall {
|
|||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
|
||||
first_line.to_owned() + "…"
|
||||
} else {
|
||||
tool_call.title
|
||||
};
|
||||
Self {
|
||||
id: tool_call.id,
|
||||
label: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
tool_call.title.into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
label: cx
|
||||
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
|
||||
kind: tool_call.kind,
|
||||
content: tool_call
|
||||
.content
|
||||
|
@ -233,7 +232,11 @@ impl ToolCall {
|
|||
|
||||
if let Some(title) = title {
|
||||
self.label.update(cx, |label, cx| {
|
||||
label.replace(title, cx);
|
||||
if let Some((first_line, _)) = title.split_once("\n") {
|
||||
label.replace(first_line.to_owned() + "…", cx)
|
||||
} else {
|
||||
label.replace(title, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -756,6 +759,8 @@ pub struct AcpThread {
|
|||
connection: Rc<dyn AgentConnection>,
|
||||
session_id: acp::SessionId,
|
||||
token_usage: Option<TokenUsage>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -770,11 +775,12 @@ pub enum AcpThreadEvent {
|
|||
Stopped,
|
||||
Error,
|
||||
LoadError(LoadError),
|
||||
PromptCapabilitiesUpdated,
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum ThreadStatus {
|
||||
Idle,
|
||||
WaitingForToolConfirmation,
|
||||
|
@ -821,7 +827,20 @@ impl AcpThread {
|
|||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
session_id: acp::SessionId,
|
||||
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let prompt_capabilities = *prompt_capabilities_rx.borrow();
|
||||
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
|
||||
loop {
|
||||
let caps = prompt_capabilities_rx.recv().await?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.prompt_capabilities = caps;
|
||||
cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated);
|
||||
})?;
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
|
@ -833,9 +852,15 @@ impl AcpThread {
|
|||
connection,
|
||||
session_id,
|
||||
token_usage: None,
|
||||
prompt_capabilities,
|
||||
_observe_prompt_capabilities: task,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
self.prompt_capabilities
|
||||
}
|
||||
|
||||
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
||||
&self.connection
|
||||
}
|
||||
|
@ -1373,6 +1398,10 @@ impl AcpThread {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn can_resume(&self, cx: &App) -> bool {
|
||||
self.connection.resume(&self.session_id, cx).is_some()
|
||||
}
|
||||
|
||||
pub fn resume(&mut self, cx: &mut Context<Self>) -> BoxFuture<'static, Result<()>> {
|
||||
self.run_turn(cx, async move |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
|
@ -2595,13 +2624,19 @@ mod tests {
|
|||
.into(),
|
||||
);
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let thread = cx.new(|_cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
AcpThread::new(
|
||||
"Test",
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.sessions.lock().insert(session_id, thread.downgrade());
|
||||
|
@ -2635,14 +2670,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
let sessions = self.sessions.lock();
|
||||
let thread = sessions.get(session_id).unwrap().clone();
|
||||
|
@ -2659,7 +2686,7 @@ mod tests {
|
|||
fn truncate(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||
Some(Rc::new(FakeAgentSessionEditor {
|
||||
_session_id: session_id.clone(),
|
||||
|
|
|
@ -38,12 +38,10 @@ pub trait AgentConnection {
|
|||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>>;
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities;
|
||||
|
||||
fn resume(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionResume>> {
|
||||
None
|
||||
}
|
||||
|
@ -53,7 +51,7 @@ pub trait AgentConnection {
|
|||
fn truncate(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||
None
|
||||
}
|
||||
|
@ -61,7 +59,7 @@ pub trait AgentConnection {
|
|||
fn set_title(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionSetTitle>> {
|
||||
None
|
||||
}
|
||||
|
@ -329,13 +327,19 @@ mod test_support {
|
|||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let thread = cx.new(|_cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
AcpThread::new(
|
||||
"Test",
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.sessions.lock().insert(
|
||||
|
@ -348,14 +352,6 @@ mod test_support {
|
|||
Task::ready(Ok(thread))
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn authenticate(
|
||||
&self,
|
||||
_method_id: acp::AuthMethodId,
|
||||
|
@ -439,7 +435,7 @@ mod test_support {
|
|||
fn truncate(
|
||||
&self,
|
||||
_session_id: &agent_client_protocol::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||
Some(Rc::new(StubAgentSessionEditor))
|
||||
}
|
||||
|
|
|
@ -21,12 +21,12 @@ use ui::prelude::*;
|
|||
use util::ResultExt as _;
|
||||
use workspace::{Item, Workspace};
|
||||
|
||||
actions!(acp, [OpenDebugTools]);
|
||||
actions!(dev, [OpenAcpLogs]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(
|
||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| {
|
||||
workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
|
||||
let acp_tools =
|
||||
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
|
||||
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
|
||||
|
|
|
@ -180,7 +180,7 @@ impl NativeAgent {
|
|||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<NativeAgent>> {
|
||||
log::info!("Creating new NativeAgent");
|
||||
log::debug!("Creating new NativeAgent");
|
||||
|
||||
let project_context = cx
|
||||
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
|
||||
|
@ -240,13 +240,16 @@ impl NativeAgent {
|
|||
let title = thread.title();
|
||||
let project = thread.project.clone();
|
||||
let action_log = thread.action_log.clone();
|
||||
let acp_thread = cx.new(|_cx| {
|
||||
let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone();
|
||||
let acp_thread = cx.new(|cx| {
|
||||
acp_thread::AcpThread::new(
|
||||
title,
|
||||
connection,
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
session_id.clone(),
|
||||
prompt_capabilities_rx,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let subscriptions = vec![
|
||||
|
@ -756,7 +759,7 @@ impl NativeAgentConnection {
|
|||
}
|
||||
}
|
||||
|
||||
log::info!("Response stream completed");
|
||||
log::debug!("Response stream completed");
|
||||
anyhow::Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
})
|
||||
|
@ -781,7 +784,7 @@ impl AgentModelSelector for NativeAgentConnection {
|
|||
model_id: acp_thread::AgentModelId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
log::info!("Setting model for session {}: {}", session_id, model_id);
|
||||
log::debug!("Setting model for session {}: {}", session_id, model_id);
|
||||
let Some(thread) = self
|
||||
.0
|
||||
.read(cx)
|
||||
|
@ -852,7 +855,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
cx: &mut App,
|
||||
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
|
||||
let agent = self.0.clone();
|
||||
log::info!("Creating new thread for project at: {:?}", cwd);
|
||||
log::debug!("Creating new thread for project at: {:?}", cwd);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
log::debug!("Starting thread creation in async context");
|
||||
|
@ -917,7 +920,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>();
|
||||
log::info!("Converted prompt to message: {} chars", content.len());
|
||||
log::debug!("Converted prompt to message: {} chars", content.len());
|
||||
log::debug!("Message id: {:?}", id);
|
||||
log::debug!("Message content: {:?}", content);
|
||||
|
||||
|
@ -925,18 +928,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
})
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn resume(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
|
||||
Some(Rc::new(NativeAgentSessionResume {
|
||||
connection: self.clone(),
|
||||
|
@ -956,9 +951,9 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
fn truncate(
|
||||
&self,
|
||||
session_id: &agent_client_protocol::SessionId,
|
||||
cx: &mut App,
|
||||
cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
|
||||
self.0.update(cx, |agent, _cx| {
|
||||
self.0.read_with(cx, |agent, _cx| {
|
||||
agent.sessions.get(session_id).map(|session| {
|
||||
Rc::new(NativeAgentSessionEditor {
|
||||
thread: session.thread.clone(),
|
||||
|
@ -971,7 +966,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
fn set_title(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> {
|
||||
Some(Rc::new(NativeAgentSessionSetTitle {
|
||||
connection: self.clone(),
|
||||
|
|
|
@ -22,6 +22,10 @@ impl NativeAgentServer {
|
|||
}
|
||||
|
||||
impl AgentServer for NativeAgentServer {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"zed"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Zed Agent".into()
|
||||
}
|
||||
|
@ -44,7 +48,7 @@ impl AgentServer for NativeAgentServer {
|
|||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"NativeAgentServer::connect called for path: {:?}",
|
||||
_root_dir
|
||||
);
|
||||
|
@ -63,7 +67,7 @@ impl AgentServer for NativeAgentServer {
|
|||
|
||||
// Create the connection wrapper
|
||||
let connection = NativeAgentConnection(agent);
|
||||
log::info!("NativeAgentServer connection established successfully");
|
||||
log::debug!("NativeAgentServer connection established successfully");
|
||||
|
||||
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
|
||||
})
|
||||
|
|
|
@ -4,6 +4,8 @@ use agent_client_protocol::{self as acp};
|
|||
use agent_settings::AgentProfileId;
|
||||
use anyhow::Result;
|
||||
use client::{Client, UserStore};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::IndexMap;
|
||||
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
|
||||
use fs::{FakeFs, Fs};
|
||||
use futures::{
|
||||
|
@ -672,15 +674,6 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||
"}
|
||||
)
|
||||
});
|
||||
|
||||
// Ensure we error if calling resume when tool use limit was *not* reached.
|
||||
let error = thread
|
||||
.update(cx, |thread, cx| thread.resume(cx))
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"can only resume after tool use limit is reached"
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -1692,6 +1685,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_title_generation(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
@ -1737,6 +1731,81 @@ async fn test_title_generation(cx: &mut TestAppContext) {
|
|||
thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let _events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(ToolRequiringPermission);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(UserMessageId::new(), ["Hey!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let permission_tool_use = LanguageModelToolUse {
|
||||
id: "tool_id_1".into(),
|
||||
name: ToolRequiringPermission::name().into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
is_input_complete: true,
|
||||
};
|
||||
let echo_tool_use = LanguageModelToolUse {
|
||||
id: "tool_id_2".into(),
|
||||
name: EchoTool::name().into(),
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
is_input_complete: true,
|
||||
};
|
||||
fake_model.send_last_completion_stream_text_chunk("Hi!");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
permission_tool_use,
|
||||
));
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
echo_tool_use.clone(),
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Ensure pending tools are skipped when building a request.
|
||||
let request = thread
|
||||
.read_with(cx, |thread, cx| {
|
||||
thread.build_completion_request(CompletionIntent::EditFile, cx)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
request.messages[1..],
|
||||
vec![
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec!["Hey!".into()],
|
||||
cache: true
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![
|
||||
MessageContent::Text("Hi!".into()),
|
||||
MessageContent::ToolUse(echo_tool_use.clone())
|
||||
],
|
||||
cache: false
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::ToolResult(LanguageModelToolResult {
|
||||
tool_use_id: echo_tool_use.id.clone(),
|
||||
tool_name: echo_tool_use.name,
|
||||
is_error: false,
|
||||
content: "test".into(),
|
||||
output: Some("test".into())
|
||||
})],
|
||||
cache: false
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
cx.update(settings::init);
|
||||
|
@ -2029,6 +2098,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
|||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
fake_model.send_last_completion_stream_text_chunk("Hey,");
|
||||
fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
|
||||
provider: LanguageModelProviderName::new("Anthropic"),
|
||||
retry_after: Some(Duration::from_secs(3)),
|
||||
|
@ -2038,8 +2108,9 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
|||
cx.executor().advance_clock(Duration::from_secs(3));
|
||||
cx.run_until_parked();
|
||||
|
||||
fake_model.send_last_completion_stream_text_chunk("Hey!");
|
||||
fake_model.send_last_completion_stream_text_chunk("there!");
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
let mut retry_events = Vec::new();
|
||||
while let Some(Ok(event)) = events.next().await {
|
||||
|
@ -2067,12 +2138,94 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
|||
|
||||
## Assistant
|
||||
|
||||
Hey!
|
||||
Hey,
|
||||
|
||||
[resume]
|
||||
|
||||
## Assistant
|
||||
|
||||
there!
|
||||
"}
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
||||
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(UserMessageId::new(), ["Call the echo tool!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let tool_use_1 = LanguageModelToolUse {
|
||||
id: "tool_1".into(),
|
||||
name: EchoTool::name().into(),
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
is_input_complete: true,
|
||||
};
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
tool_use_1.clone(),
|
||||
));
|
||||
fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
|
||||
provider: LanguageModelProviderName::new("Anthropic"),
|
||||
retry_after: Some(Duration::from_secs(3)),
|
||||
});
|
||||
fake_model.end_last_completion_stream();
|
||||
|
||||
cx.executor().advance_clock(Duration::from_secs(3));
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
assert_eq!(
|
||||
completion.messages[1..],
|
||||
vec![
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec!["Call the echo tool!".into()],
|
||||
cache: false
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
|
||||
cache: false
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![language_model::MessageContent::ToolResult(
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_1.id.clone(),
|
||||
tool_name: tool_use_1.name.clone(),
|
||||
is_error: false,
|
||||
content: "test".into(),
|
||||
output: Some("test".into())
|
||||
}
|
||||
)],
|
||||
cache: true
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
fake_model.send_last_completion_stream_text_chunk("Done");
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
events.collect::<Vec<_>>().await;
|
||||
thread.read_with(cx, |thread, _cx| {
|
||||
assert_eq!(
|
||||
thread.last_message(),
|
||||
Some(Message::Agent(AgentMessage {
|
||||
content: vec![AgentMessageContent::Text("Done".into())],
|
||||
tool_results: IndexMap::default()
|
||||
}))
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
|
||||
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
|
||||
|
|
|
@ -123,7 +123,7 @@ impl Message {
|
|||
match self {
|
||||
Message::User(message) => message.to_markdown(),
|
||||
Message::Agent(message) => message.to_markdown(),
|
||||
Message::Resume => "[resumed after tool use limit was reached]".into(),
|
||||
Message::Resume => "[resume]\n".into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -448,24 +448,33 @@ impl AgentMessage {
|
|||
cache: false,
|
||||
};
|
||||
for chunk in &self.content {
|
||||
let chunk = match chunk {
|
||||
match chunk {
|
||||
AgentMessageContent::Text(text) => {
|
||||
language_model::MessageContent::Text(text.clone())
|
||||
assistant_message
|
||||
.content
|
||||
.push(language_model::MessageContent::Text(text.clone()));
|
||||
}
|
||||
AgentMessageContent::Thinking { text, signature } => {
|
||||
language_model::MessageContent::Thinking {
|
||||
text: text.clone(),
|
||||
signature: signature.clone(),
|
||||
}
|
||||
assistant_message
|
||||
.content
|
||||
.push(language_model::MessageContent::Thinking {
|
||||
text: text.clone(),
|
||||
signature: signature.clone(),
|
||||
});
|
||||
}
|
||||
AgentMessageContent::RedactedThinking(value) => {
|
||||
language_model::MessageContent::RedactedThinking(value.clone())
|
||||
assistant_message.content.push(
|
||||
language_model::MessageContent::RedactedThinking(value.clone()),
|
||||
);
|
||||
}
|
||||
AgentMessageContent::ToolUse(value) => {
|
||||
language_model::MessageContent::ToolUse(value.clone())
|
||||
AgentMessageContent::ToolUse(tool_use) => {
|
||||
if self.tool_results.contains_key(&tool_use.id) {
|
||||
assistant_message
|
||||
.content
|
||||
.push(language_model::MessageContent::ToolUse(tool_use.clone()));
|
||||
}
|
||||
}
|
||||
};
|
||||
assistant_message.content.push(chunk);
|
||||
}
|
||||
|
||||
let mut user_message = LanguageModelRequestMessage {
|
||||
|
@ -566,11 +575,22 @@ pub struct Thread {
|
|||
templates: Arc<Templates>,
|
||||
model: Option<Arc<dyn LanguageModel>>,
|
||||
summarization_model: Option<Arc<dyn LanguageModel>>,
|
||||
prompt_capabilities_tx: watch::Sender<acp::PromptCapabilities>,
|
||||
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
pub(crate) project: Entity<Project>,
|
||||
pub(crate) action_log: Entity<ActionLog>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
|
||||
let image = model.map_or(true, |model| model.supports_images());
|
||||
acp::PromptCapabilities {
|
||||
image,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
project_context: Entity<ProjectContext>,
|
||||
|
@ -581,6 +601,8 @@ impl Thread {
|
|||
) -> Self {
|
||||
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
||||
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
||||
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
||||
Self {
|
||||
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
|
||||
prompt_id: PromptId::new(),
|
||||
|
@ -608,6 +630,8 @@ impl Thread {
|
|||
templates,
|
||||
model,
|
||||
summarization_model: None,
|
||||
prompt_capabilities_tx,
|
||||
prompt_capabilities_rx,
|
||||
project,
|
||||
action_log,
|
||||
}
|
||||
|
@ -741,6 +765,8 @@ impl Thread {
|
|||
.or_else(|| registry.default_model())
|
||||
.map(|model| model.model)
|
||||
});
|
||||
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
||||
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
||||
|
||||
Self {
|
||||
id,
|
||||
|
@ -770,6 +796,8 @@ impl Thread {
|
|||
project,
|
||||
action_log,
|
||||
updated_at: db_thread.updated_at,
|
||||
prompt_capabilities_tx,
|
||||
prompt_capabilities_rx,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -937,10 +965,12 @@ impl Thread {
|
|||
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
||||
let old_usage = self.latest_token_usage();
|
||||
self.model = Some(model);
|
||||
let new_caps = Self::prompt_capabilities(self.model.as_deref());
|
||||
let new_usage = self.latest_token_usage();
|
||||
if old_usage != new_usage {
|
||||
cx.emit(TokenUsageUpdated(new_usage));
|
||||
}
|
||||
self.prompt_capabilities_tx.send(new_caps).log_err();
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
|
@ -1076,15 +1106,10 @@ impl Thread {
|
|||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
|
||||
anyhow::ensure!(
|
||||
self.tool_use_limit_reached,
|
||||
"can only resume after tool use limit is reached"
|
||||
);
|
||||
|
||||
self.messages.push(Message::Resume);
|
||||
cx.notify();
|
||||
|
||||
log::info!("Total messages in thread: {}", self.messages.len());
|
||||
log::debug!("Total messages in thread: {}", self.messages.len());
|
||||
self.run_turn(cx)
|
||||
}
|
||||
|
||||
|
@ -1102,7 +1127,7 @@ impl Thread {
|
|||
{
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
log::info!("Thread::send called with model: {:?}", model.name());
|
||||
log::info!("Thread::send called with model: {}", model.name().0);
|
||||
self.advance_prompt_id();
|
||||
|
||||
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
|
@ -1112,7 +1137,7 @@ impl Thread {
|
|||
.push(Message::User(UserMessage { id, content }));
|
||||
cx.notify();
|
||||
|
||||
log::info!("Total messages in thread: {}", self.messages.len());
|
||||
log::debug!("Total messages in thread: {}", self.messages.len());
|
||||
self.run_turn(cx)
|
||||
}
|
||||
|
||||
|
@ -1136,44 +1161,14 @@ impl Thread {
|
|||
event_stream: event_stream.clone(),
|
||||
tools: self.enabled_tools(profile, &model, cx),
|
||||
_task: cx.spawn(async move |this, cx| {
|
||||
log::info!("Starting agent turn execution");
|
||||
log::debug!("Starting agent turn execution");
|
||||
|
||||
let turn_result: Result<()> = async {
|
||||
let mut intent = CompletionIntent::UserPrompt;
|
||||
loop {
|
||||
Self::stream_completion(&this, &model, intent, &event_stream, cx).await?;
|
||||
|
||||
let mut end_turn = true;
|
||||
this.update(cx, |this, cx| {
|
||||
// Generate title if needed.
|
||||
if this.title.is_none() && this.pending_title_generation.is_none() {
|
||||
this.generate_title(cx);
|
||||
}
|
||||
|
||||
// End the turn if the model didn't use tools.
|
||||
let message = this.pending_message.as_ref();
|
||||
end_turn =
|
||||
message.map_or(true, |message| message.tool_results.is_empty());
|
||||
this.flush_pending_message(cx);
|
||||
})?;
|
||||
|
||||
if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
|
||||
log::info!("Tool use limit reached, completing turn");
|
||||
return Err(language_model::ToolUseLimitReachedError.into());
|
||||
} else if end_turn {
|
||||
log::info!("No tool uses found, completing turn");
|
||||
return Ok(());
|
||||
} else {
|
||||
intent = CompletionIntent::ToolResults;
|
||||
}
|
||||
}
|
||||
}
|
||||
.await;
|
||||
let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await;
|
||||
_ = this.update(cx, |this, cx| this.flush_pending_message(cx));
|
||||
|
||||
match turn_result {
|
||||
Ok(()) => {
|
||||
log::info!("Turn execution completed");
|
||||
log::debug!("Turn execution completed");
|
||||
event_stream.send_stop(acp::StopReason::EndTurn);
|
||||
}
|
||||
Err(error) => {
|
||||
|
@ -1199,20 +1194,18 @@ impl Thread {
|
|||
Ok(events_rx)
|
||||
}
|
||||
|
||||
async fn stream_completion(
|
||||
async fn run_turn_internal(
|
||||
this: &WeakEntity<Self>,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
completion_intent: CompletionIntent,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
event_stream: &ThreadEventStream,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
log::debug!("Stream completion started successfully");
|
||||
let request = this.update(cx, |this, cx| {
|
||||
this.build_completion_request(completion_intent, cx)
|
||||
})??;
|
||||
let mut attempt = 0;
|
||||
let mut intent = CompletionIntent::UserPrompt;
|
||||
loop {
|
||||
let request =
|
||||
this.update(cx, |this, cx| this.build_completion_request(intent, cx))??;
|
||||
|
||||
let mut attempt = None;
|
||||
'retry: loop {
|
||||
telemetry::event!(
|
||||
"Agent Thread Completion",
|
||||
thread_id = this.read_with(cx, |this, _| this.id.to_string())?,
|
||||
|
@ -1222,75 +1215,31 @@ impl Thread {
|
|||
attempt
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"Calling model.stream_completion, attempt {}",
|
||||
attempt.unwrap_or(0)
|
||||
);
|
||||
log::debug!("Calling model.stream_completion, attempt {}", attempt);
|
||||
let mut events = model
|
||||
.stream_completion(request.clone(), cx)
|
||||
.stream_completion(request, cx)
|
||||
.await
|
||||
.map_err(|error| anyhow!(error))?;
|
||||
let mut tool_results = FuturesUnordered::new();
|
||||
|
||||
let mut error = None;
|
||||
while let Some(event) = events.next().await {
|
||||
log::trace!("Received completion event: {:?}", event);
|
||||
match event {
|
||||
Ok(event) => {
|
||||
log::trace!("Received completion event: {:?}", event);
|
||||
tool_results.extend(this.update(cx, |this, cx| {
|
||||
this.handle_streamed_completion_event(event, event_stream, cx)
|
||||
this.handle_completion_event(event, event_stream, cx)
|
||||
})??);
|
||||
}
|
||||
Err(error) => {
|
||||
let completion_mode =
|
||||
this.read_with(cx, |thread, _cx| thread.completion_mode())?;
|
||||
if completion_mode == CompletionMode::Normal {
|
||||
return Err(anyhow!(error))?;
|
||||
}
|
||||
|
||||
let Some(strategy) = Self::retry_strategy_for(&error) else {
|
||||
return Err(anyhow!(error))?;
|
||||
};
|
||||
|
||||
let max_attempts = match &strategy {
|
||||
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
|
||||
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
|
||||
};
|
||||
|
||||
let attempt = attempt.get_or_insert(0u8);
|
||||
|
||||
*attempt += 1;
|
||||
|
||||
let attempt = *attempt;
|
||||
if attempt > max_attempts {
|
||||
return Err(anyhow!(error))?;
|
||||
}
|
||||
|
||||
let delay = match &strategy {
|
||||
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
|
||||
let delay_secs =
|
||||
initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
|
||||
Duration::from_secs(delay_secs)
|
||||
}
|
||||
RetryStrategy::Fixed { delay, .. } => *delay,
|
||||
};
|
||||
log::debug!("Retry attempt {attempt} with delay {delay:?}");
|
||||
|
||||
event_stream.send_retry(acp_thread::RetryStatus {
|
||||
last_error: error.to_string().into(),
|
||||
attempt: attempt as usize,
|
||||
max_attempts: max_attempts as usize,
|
||||
started_at: Instant::now(),
|
||||
duration: delay,
|
||||
});
|
||||
|
||||
cx.background_executor().timer(delay).await;
|
||||
continue 'retry;
|
||||
Err(err) => {
|
||||
error = Some(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let end_turn = tool_results.is_empty();
|
||||
while let Some(tool_result) = tool_results.next().await {
|
||||
log::info!("Tool finished {:?}", tool_result);
|
||||
log::debug!("Tool finished {:?}", tool_result);
|
||||
|
||||
event_stream.update_tool_call_fields(
|
||||
&tool_result.tool_use_id,
|
||||
|
@ -1311,31 +1260,83 @@ impl Thread {
|
|||
})?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
this.update(cx, |this, cx| {
|
||||
this.flush_pending_message(cx);
|
||||
if this.title.is_none() && this.pending_title_generation.is_none() {
|
||||
this.generate_title(cx);
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Some(error) = error {
|
||||
attempt += 1;
|
||||
let retry =
|
||||
this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
|
||||
let timer = cx.background_executor().timer(retry.duration);
|
||||
event_stream.send_retry(retry);
|
||||
timer.await;
|
||||
this.update(cx, |this, _cx| {
|
||||
if let Some(Message::Agent(message)) = this.messages.last() {
|
||||
if message.tool_results.is_empty() {
|
||||
intent = CompletionIntent::UserPrompt;
|
||||
this.messages.push(Message::Resume);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
} else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
|
||||
return Err(language_model::ToolUseLimitReachedError.into());
|
||||
} else if end_turn {
|
||||
return Ok(());
|
||||
} else {
|
||||
intent = CompletionIntent::ToolResults;
|
||||
attempt = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage {
|
||||
log::debug!("Building system message");
|
||||
let prompt = SystemPromptTemplate {
|
||||
project: self.project_context.read(cx),
|
||||
available_tools: self.tools.keys().cloned().collect(),
|
||||
fn handle_completion_error(
|
||||
&mut self,
|
||||
error: LanguageModelCompletionError,
|
||||
attempt: u8,
|
||||
) -> Result<acp_thread::RetryStatus> {
|
||||
if self.completion_mode == CompletionMode::Normal {
|
||||
return Err(anyhow!(error));
|
||||
}
|
||||
.render(&self.templates)
|
||||
.context("failed to build system prompt")
|
||||
.expect("Invalid template");
|
||||
log::debug!("System message built");
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![prompt.into()],
|
||||
cache: true,
|
||||
|
||||
let Some(strategy) = Self::retry_strategy_for(&error) else {
|
||||
return Err(anyhow!(error));
|
||||
};
|
||||
|
||||
let max_attempts = match &strategy {
|
||||
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
|
||||
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
|
||||
};
|
||||
|
||||
if attempt > max_attempts {
|
||||
return Err(anyhow!(error));
|
||||
}
|
||||
|
||||
let delay = match &strategy {
|
||||
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
|
||||
let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
|
||||
Duration::from_secs(delay_secs)
|
||||
}
|
||||
RetryStrategy::Fixed { delay, .. } => *delay,
|
||||
};
|
||||
log::debug!("Retry attempt {attempt} with delay {delay:?}");
|
||||
|
||||
Ok(acp_thread::RetryStatus {
|
||||
last_error: error.to_string().into(),
|
||||
attempt: attempt as usize,
|
||||
max_attempts: max_attempts as usize,
|
||||
started_at: Instant::now(),
|
||||
duration: delay,
|
||||
})
|
||||
}
|
||||
|
||||
/// A helper method that's called on every streamed completion event.
|
||||
/// Returns an optional tool result task, which the main agentic loop will
|
||||
/// send back to the model when it resolves.
|
||||
fn handle_streamed_completion_event(
|
||||
fn handle_completion_event(
|
||||
&mut self,
|
||||
event: LanguageModelCompletionEvent,
|
||||
event_stream: &ThreadEventStream,
|
||||
|
@ -1530,7 +1531,7 @@ impl Thread {
|
|||
});
|
||||
let supports_images = self.model().is_some_and(|model| model.supports_images());
|
||||
let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
|
||||
log::info!("Running tool {}", tool_use.name);
|
||||
log::debug!("Running tool {}", tool_use.name);
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
let tool_result = tool_result.await.and_then(|output| {
|
||||
if let LanguageModelToolResultContent::Image(_) = &output.llm_output
|
||||
|
@ -1642,7 +1643,7 @@ impl Thread {
|
|||
summary.extend(lines.next());
|
||||
}
|
||||
|
||||
log::info!("Setting summary: {}", summary);
|
||||
log::debug!("Setting summary: {}", summary);
|
||||
let summary = SharedString::from(summary);
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
|
@ -1659,7 +1660,7 @@ impl Thread {
|
|||
return;
|
||||
};
|
||||
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"Generating title with model: {:?}",
|
||||
self.summarization_model.as_ref().map(|model| model.name())
|
||||
);
|
||||
|
@ -1745,6 +1746,10 @@ impl Thread {
|
|||
return;
|
||||
};
|
||||
|
||||
if message.content.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for content in &message.content {
|
||||
let AgentMessageContent::ToolUse(tool_use) = content else {
|
||||
continue;
|
||||
|
@ -1773,7 +1778,7 @@ impl Thread {
|
|||
pub(crate) fn build_completion_request(
|
||||
&self,
|
||||
completion_intent: CompletionIntent,
|
||||
cx: &mut App,
|
||||
cx: &App,
|
||||
) -> Result<LanguageModelRequest> {
|
||||
let model = self.model().context("No language model configured")?;
|
||||
let tools = if let Some(turn) = self.running_turn.as_ref() {
|
||||
|
@ -1797,8 +1802,8 @@ impl Thread {
|
|||
log::debug!("Completion mode: {:?}", self.completion_mode);
|
||||
|
||||
let messages = self.build_request_messages(cx);
|
||||
log::info!("Request will include {} messages", messages.len());
|
||||
log::info!("Request includes {} tools", tools.len());
|
||||
log::debug!("Request will include {} messages", messages.len());
|
||||
log::debug!("Request includes {} tools", tools.len());
|
||||
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: Some(self.id.to_string()),
|
||||
|
@ -1894,21 +1899,29 @@ impl Thread {
|
|||
"Building request messages from {} thread messages",
|
||||
self.messages.len()
|
||||
);
|
||||
let mut messages = vec![self.build_system_message(cx)];
|
||||
|
||||
let system_prompt = SystemPromptTemplate {
|
||||
project: self.project_context.read(cx),
|
||||
available_tools: self.tools.keys().cloned().collect(),
|
||||
}
|
||||
.render(&self.templates)
|
||||
.context("failed to build system prompt")
|
||||
.expect("Invalid template");
|
||||
let mut messages = vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![system_prompt.into()],
|
||||
cache: false,
|
||||
}];
|
||||
for message in &self.messages {
|
||||
messages.extend(message.to_request());
|
||||
}
|
||||
|
||||
if let Some(message) = self.pending_message.as_ref() {
|
||||
messages.extend(message.to_request());
|
||||
if let Some(last_message) = messages.last_mut() {
|
||||
last_message.cache = true;
|
||||
}
|
||||
|
||||
if let Some(last_user_message) = messages
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find(|message| message.role == Role::User)
|
||||
{
|
||||
last_user_message.cache = true;
|
||||
if let Some(message) = self.pending_message.as_ref() {
|
||||
messages.extend(message.to_request());
|
||||
}
|
||||
|
||||
messages
|
||||
|
|
|
@ -136,12 +136,17 @@ impl AgentTool for FetchTool {
|
|||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
_event_stream: ToolCallEventStream,
|
||||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
let authorize = event_stream.authorize(input.url.clone(), cx);
|
||||
|
||||
let text = cx.background_spawn({
|
||||
let http_client = self.http_client.clone();
|
||||
async move { Self::build_message(http_client, &input.url).await }
|
||||
async move {
|
||||
authorize.await?;
|
||||
Self::build_message(http_client, &input.url).await
|
||||
}
|
||||
});
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
|
|
|
@ -165,16 +165,17 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
|
|||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
Ok(snapshots
|
||||
.iter()
|
||||
.flat_map(|snapshot| {
|
||||
let mut results = Vec::new();
|
||||
for snapshot in snapshots {
|
||||
for entry in snapshot.entries(false, 0) {
|
||||
let root_name = PathBuf::from(snapshot.root_name());
|
||||
snapshot
|
||||
.entries(false, 0)
|
||||
.map(move |entry| root_name.join(&entry.path))
|
||||
.filter(|path| path_matcher.is_match(&path))
|
||||
})
|
||||
.collect())
|
||||
if path_matcher.is_match(root_name.join(&entry.path)) {
|
||||
results.push(snapshot.abs_path().join(entry.path.as_ref()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -215,8 +216,8 @@ mod test {
|
|||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
PathBuf::from(path!("/root/apple/banana/carrot")),
|
||||
PathBuf::from(path!("/root/apple/bandana/carbonara"))
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -227,8 +228,8 @@ mod test {
|
|||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
PathBuf::from(path!("/root/apple/banana/carrot")),
|
||||
PathBuf::from(path!("/root/apple/bandana/carbonara"))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
|
|||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream};
|
||||
|
||||
|
@ -68,27 +69,12 @@ impl AgentTool for ReadFileTool {
|
|||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
let path = &input.path;
|
||||
match (input.start_line, input.end_line) {
|
||||
(Some(start), Some(end)) => {
|
||||
format!(
|
||||
"[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
|
||||
path, start, end, path, start, end
|
||||
)
|
||||
}
|
||||
(Some(start), None) => {
|
||||
format!(
|
||||
"[Read file `{}` (from line {})](@selection:{}:({}-{}))",
|
||||
path, start, path, start, start
|
||||
)
|
||||
}
|
||||
_ => format!("[Read file `{}`](@file:{})", path, path),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
"Read file".into()
|
||||
}
|
||||
input
|
||||
.ok()
|
||||
.as_ref()
|
||||
.and_then(|input| Path::new(&input.path).file_name())
|
||||
.map(|file_name| file_name.to_string_lossy().to_string().into())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn run(
|
||||
|
@ -258,6 +244,19 @@ impl AgentTool for ReadFileTool {
|
|||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
|
||||
let markdown = MarkdownCodeBlock {
|
||||
tag: &input.path,
|
||||
text,
|
||||
}
|
||||
.to_string();
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
content: Some(vec![acp::ToolCallContent::Content {
|
||||
content: markdown.into(),
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
|
|
|
@ -185,13 +185,16 @@ impl AgentConnection for AcpConnection {
|
|||
|
||||
let session_id = response.session_id;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|_cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
AcpThread::new(
|
||||
self.server_name.clone(),
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
|
||||
watch::Receiver::constant(self.prompt_capabilities),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
|
@ -263,7 +266,9 @@ impl AgentConnection for AcpConnection {
|
|||
|
||||
match serde_json::from_value(data.clone()) {
|
||||
Ok(ErrorDetails { details }) => {
|
||||
if suppress_abort_err && details.contains("This operation was aborted")
|
||||
if suppress_abort_err
|
||||
&& (details.contains("This operation was aborted")
|
||||
|| details.contains("The user aborted a request"))
|
||||
{
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
|
@ -279,10 +284,6 @@ impl AgentConnection for AcpConnection {
|
|||
})
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
self.prompt_capabilities
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
||||
session.suppress_abort_err = true;
|
||||
|
|
|
@ -36,6 +36,7 @@ pub trait AgentServer: Send {
|
|||
fn name(&self) -> SharedString;
|
||||
fn empty_state_headline(&self) -> SharedString;
|
||||
fn empty_state_message(&self) -> SharedString;
|
||||
fn telemetry_id(&self) -> &'static str;
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
|
@ -97,7 +98,7 @@ pub struct AgentServerCommand {
|
|||
}
|
||||
|
||||
impl AgentServerCommand {
|
||||
pub(crate) async fn resolve(
|
||||
pub async fn resolve(
|
||||
path_bin_name: &'static str,
|
||||
extra_args: &[&'static str],
|
||||
fallback_path: Option<&Path>,
|
||||
|
|
|
@ -43,6 +43,10 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
|
|||
pub struct ClaudeCode;
|
||||
|
||||
impl AgentServer for ClaudeCode {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"claude-code"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Claude Code".into()
|
||||
}
|
||||
|
@ -249,13 +253,19 @@ impl AgentConnection for ClaudeAgentConnection {
|
|||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|_cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
AcpThread::new(
|
||||
"Claude Code",
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
|
@ -319,14 +329,6 @@ impl AgentConnection for ClaudeAgentConnection {
|
|||
cx.foreground_executor().spawn(async move { end_rx.await? })
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
||||
let sessions = self.sessions.borrow();
|
||||
let Some(session) = sessions.get(session_id) else {
|
||||
|
|
|
@ -22,6 +22,10 @@ impl CustomAgentServer {
|
|||
}
|
||||
|
||||
impl crate::AgentServer for CustomAgentServer {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"custom"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
|
|
@ -17,6 +17,10 @@ pub struct Gemini;
|
|||
const ACP_ARG: &str = "--experimental-acp";
|
||||
|
||||
impl AgentServer for Gemini {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"gemini-cli"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Gemini CLI".into()
|
||||
}
|
||||
|
@ -53,7 +57,7 @@ impl AgentServer for Gemini {
|
|||
return Err(LoadError::NotInstalled {
|
||||
error_message: "Failed to find Gemini CLI binary".into(),
|
||||
install_message: "Install Gemini CLI".into(),
|
||||
install_command: "npm install -g @google/gemini-cli@preview".into()
|
||||
install_command: Self::install_command().into(),
|
||||
}.into());
|
||||
};
|
||||
|
||||
|
@ -88,7 +92,7 @@ impl AgentServer for Gemini {
|
|||
current_version
|
||||
).into(),
|
||||
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
||||
upgrade_command: "npm install -g @google/gemini-cli@preview".into(),
|
||||
upgrade_command: Self::upgrade_command().into(),
|
||||
}.into())
|
||||
}
|
||||
}
|
||||
|
@ -101,6 +105,20 @@ impl AgentServer for Gemini {
|
|||
}
|
||||
}
|
||||
|
||||
impl Gemini {
|
||||
pub fn binary_name() -> &'static str {
|
||||
"gemini"
|
||||
}
|
||||
|
||||
pub fn install_command() -> &'static str {
|
||||
"npm install -g @google/gemini-cli@preview"
|
||||
}
|
||||
|
||||
pub fn upgrade_command() -> &'static str {
|
||||
"npm install -g @google/gemini-cli@preview"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -373,7 +373,7 @@ impl MessageEditor {
|
|||
|
||||
if Img::extensions().contains(&extension) && !extension.contains("svg") {
|
||||
if !self.prompt_capabilities.get().image {
|
||||
return Task::ready(Err(anyhow!("This agent does not support images yet")));
|
||||
return Task::ready(Err(anyhow!("This model does not support images yet")));
|
||||
}
|
||||
let task = self
|
||||
.project
|
||||
|
|
|
@ -462,7 +462,7 @@ impl AcpThreadHistory {
|
|||
|
||||
cx.notify();
|
||||
}))
|
||||
.end_slot::<IconButton>(if hovered || selected {
|
||||
.end_slot::<IconButton>(if hovered {
|
||||
Some(
|
||||
IconButton::new("delete", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
|
|
|
@ -35,6 +35,7 @@ use prompt_store::{PromptId, PromptStore};
|
|||
use rope::Point;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::cell::Cell;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::{collections::BTreeMap, rc::Rc, time::Duration};
|
||||
|
@ -274,6 +275,7 @@ pub struct AcpThreadView {
|
|||
edits_expanded: bool,
|
||||
plan_expanded: bool,
|
||||
editor_expanded: bool,
|
||||
should_be_following: bool,
|
||||
editing_message: Option<usize>,
|
||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||
is_loading_contents: bool,
|
||||
|
@ -385,6 +387,7 @@ impl AcpThreadView {
|
|||
edits_expanded: false,
|
||||
plan_expanded: false,
|
||||
editor_expanded: false,
|
||||
should_be_following: false,
|
||||
history_store,
|
||||
hovered_recent_history_item: None,
|
||||
prompt_capabilities,
|
||||
|
@ -472,7 +475,7 @@ impl AcpThreadView {
|
|||
let action_log = thread.read(cx).action_log().clone();
|
||||
|
||||
this.prompt_capabilities
|
||||
.set(connection.prompt_capabilities());
|
||||
.set(thread.read(cx).prompt_capabilities());
|
||||
|
||||
let count = thread.read(cx).entries().len();
|
||||
this.list_state.splice(0..0, count);
|
||||
|
@ -820,6 +823,9 @@ impl AcpThreadView {
|
|||
let Some(thread) = self.thread() else {
|
||||
return;
|
||||
};
|
||||
if !thread.read(cx).can_resume(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let task = thread.update(cx, |thread, cx| thread.resume(cx));
|
||||
cx.spawn(async move |this, cx| {
|
||||
|
@ -887,6 +893,8 @@ impl AcpThreadView {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let agent_telemetry_id = self.agent.telemetry_id();
|
||||
|
||||
self.thread_error.take();
|
||||
self.editing_message.take();
|
||||
self.thread_feedback.clear();
|
||||
|
@ -894,6 +902,13 @@ impl AcpThreadView {
|
|||
let Some(thread) = self.thread().cloned() else {
|
||||
return;
|
||||
};
|
||||
if self.should_be_following {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
self.is_loading_contents = true;
|
||||
let guard = cx.new(|_| ());
|
||||
|
@ -924,6 +939,9 @@ impl AcpThreadView {
|
|||
}
|
||||
});
|
||||
drop(guard);
|
||||
|
||||
telemetry::event!("Agent Message Sent", agent = agent_telemetry_id);
|
||||
|
||||
thread.send(contents, cx)
|
||||
})?;
|
||||
send.await
|
||||
|
@ -935,6 +953,16 @@ impl AcpThreadView {
|
|||
this.handle_thread_error(err, cx);
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.should_be_following = this
|
||||
.workspace
|
||||
.update(cx, |workspace, _| {
|
||||
workspace.is_being_followed(CollaboratorId::Agent)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
@ -1141,6 +1169,10 @@ impl AcpThreadView {
|
|||
});
|
||||
}
|
||||
}
|
||||
AcpThreadEvent::PromptCapabilitiesUpdated => {
|
||||
self.prompt_capabilities
|
||||
.set(thread.read(cx).prompt_capabilities());
|
||||
}
|
||||
AcpThreadEvent::TokenUsageUpdated => {}
|
||||
}
|
||||
cx.notify();
|
||||
|
@ -1220,30 +1252,44 @@ impl AcpThreadView {
|
|||
pending_auth_method.replace(method.clone());
|
||||
let authenticate = connection.authenticate(method, cx);
|
||||
cx.notify();
|
||||
self.auth_task = Some(cx.spawn_in(window, {
|
||||
let project = self.project.clone();
|
||||
let agent = self.agent.clone();
|
||||
async move |this, cx| {
|
||||
let result = authenticate.await;
|
||||
self.auth_task =
|
||||
Some(cx.spawn_in(window, {
|
||||
let project = self.project.clone();
|
||||
let agent = self.agent.clone();
|
||||
async move |this, cx| {
|
||||
let result = authenticate.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if let Err(err) = result {
|
||||
this.handle_thread_error(err, cx);
|
||||
} else {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
match &result {
|
||||
Ok(_) => telemetry::event!(
|
||||
"Authenticate Agent Succeeded",
|
||||
agent = agent.telemetry_id()
|
||||
),
|
||||
Err(_) => {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Failed",
|
||||
agent = agent.telemetry_id(),
|
||||
)
|
||||
}
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if let Err(err) = result {
|
||||
this.handle_thread_error(err, cx);
|
||||
} else {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn authorize_tool_call(
|
||||
|
@ -1251,6 +1297,7 @@ impl AcpThreadView {
|
|||
tool_call_id: acp::ToolCallId,
|
||||
option_id: acp::PermissionOptionId,
|
||||
option_kind: acp::PermissionOptionKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(thread) = self.thread() else {
|
||||
|
@ -1259,6 +1306,13 @@ impl AcpThreadView {
|
|||
thread.update(cx, |thread, cx| {
|
||||
thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
|
||||
});
|
||||
if self.should_be_following {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -1302,11 +1356,24 @@ impl AcpThreadView {
|
|||
None
|
||||
};
|
||||
|
||||
let has_checkpoint_button = message
|
||||
.checkpoint
|
||||
.as_ref()
|
||||
.is_some_and(|checkpoint| checkpoint.show);
|
||||
|
||||
let agent_name = self.agent.name();
|
||||
|
||||
v_flex()
|
||||
.id(("user_message", entry_ix))
|
||||
.pt_2()
|
||||
.map(|this| {
|
||||
if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() {
|
||||
this.pt_4()
|
||||
} else if rules_item.is_some() {
|
||||
this.pt_3()
|
||||
} else {
|
||||
this.pt_2()
|
||||
}
|
||||
})
|
||||
.pb_4()
|
||||
.px_2()
|
||||
.gap_1p5()
|
||||
|
@ -1315,6 +1382,7 @@ impl AcpThreadView {
|
|||
.children(message.id.clone().and_then(|message_id| {
|
||||
message.checkpoint.as_ref()?.show.then(|| {
|
||||
h_flex()
|
||||
.px_3()
|
||||
.gap_2()
|
||||
.child(Divider::horizontal())
|
||||
.child(
|
||||
|
@ -1484,17 +1552,14 @@ impl AcpThreadView {
|
|||
return primary;
|
||||
};
|
||||
|
||||
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
||||
let primary = if entry_ix == total_entries - 1 && !is_generating {
|
||||
let primary = if entry_ix == total_entries - 1 {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(primary)
|
||||
.child(self.render_thread_controls(cx))
|
||||
.child(self.render_thread_controls(&thread, cx))
|
||||
.when_some(
|
||||
self.thread_feedback.comments_editor.clone(),
|
||||
|this, editor| {
|
||||
this.child(Self::render_feedback_feedback_editor(editor, window, cx))
|
||||
},
|
||||
|this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
|
@ -1633,15 +1698,16 @@ impl AcpThreadView {
|
|||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_tool_call_icon(
|
||||
fn render_tool_call(
|
||||
&self,
|
||||
group_name: SharedString,
|
||||
entry_ix: usize,
|
||||
is_collapsible: bool,
|
||||
is_open: bool,
|
||||
tool_call: &ToolCall,
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
|
||||
let card_header_id = SharedString::from("inner-tool-call-header");
|
||||
|
||||
let tool_icon =
|
||||
if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 {
|
||||
FileIcons::get_icon(&tool_call.locations[0].path, cx)
|
||||
|
@ -1649,7 +1715,7 @@ impl AcpThreadView {
|
|||
.unwrap_or(Icon::new(IconName::ToolPencil))
|
||||
} else {
|
||||
Icon::new(match tool_call.kind {
|
||||
acp::ToolKind::Read => IconName::ToolRead,
|
||||
acp::ToolKind::Read => IconName::ToolSearch,
|
||||
acp::ToolKind::Edit => IconName::ToolPencil,
|
||||
acp::ToolKind::Delete => IconName::ToolDeleteFile,
|
||||
acp::ToolKind::Move => IconName::ArrowRightLeft,
|
||||
|
@ -1663,59 +1729,6 @@ impl AcpThreadView {
|
|||
.size(IconSize::Small)
|
||||
.color(Color::Muted);
|
||||
|
||||
let base_container = h_flex().flex_shrink_0().size_4().justify_center();
|
||||
|
||||
if is_collapsible {
|
||||
base_container
|
||||
.child(
|
||||
div()
|
||||
.group_hover(&group_name, |s| s.invisible().w_0())
|
||||
.child(tool_icon),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.invisible()
|
||||
.justify_center()
|
||||
.group_hover(&group_name, |s| s.visible())
|
||||
.child(
|
||||
Disclosure::new(("expand", entry_ix), is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronRight)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
if is_open {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
base_container.child(tool_icon)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tool_call(
|
||||
&self,
|
||||
entry_ix: usize,
|
||||
tool_call: &ToolCall,
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
|
||||
let card_header_id = SharedString::from("inner-tool-call-header");
|
||||
|
||||
let in_progress = match &tool_call.status {
|
||||
ToolCallStatus::InProgress => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let failed_or_canceled = match &tool_call.status {
|
||||
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
|
||||
_ => false,
|
||||
|
@ -1725,6 +1738,7 @@ impl AcpThreadView {
|
|||
tool_call.status,
|
||||
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
|
||||
);
|
||||
|
||||
let needs_confirmation = matches!(
|
||||
tool_call.status,
|
||||
ToolCallStatus::WaitingForConfirmation { .. }
|
||||
|
@ -1742,7 +1756,7 @@ impl AcpThreadView {
|
|||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.w_16()
|
||||
.w_12()
|
||||
.h_full()
|
||||
.bg(linear_gradient(
|
||||
90.,
|
||||
|
@ -1814,6 +1828,7 @@ impl AcpThreadView {
|
|||
.child(
|
||||
h_flex()
|
||||
.id(header_id)
|
||||
.group(&card_header_id)
|
||||
.relative()
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
|
@ -1831,19 +1846,11 @@ impl AcpThreadView {
|
|||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.group(&card_header_id)
|
||||
.relative()
|
||||
.w_full()
|
||||
.h(window.line_height() - px(2.))
|
||||
.text_size(self.tool_name_font_size())
|
||||
.child(self.render_tool_call_icon(
|
||||
card_header_id,
|
||||
entry_ix,
|
||||
is_collapsible,
|
||||
is_open,
|
||||
tool_call,
|
||||
cx,
|
||||
))
|
||||
.child(tool_icon)
|
||||
.child(if tool_call.locations.len() == 1 {
|
||||
let name = tool_call.locations[0]
|
||||
.path
|
||||
|
@ -1871,13 +1878,13 @@ impl AcpThreadView {
|
|||
})
|
||||
.child(name)
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.cursor(gpui::CursorStyle::PointingHand)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
h_flex()
|
||||
.id("non-card-label-container")
|
||||
.relative()
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
|
@ -1888,47 +1895,39 @@ impl AcpThreadView {
|
|||
default_markdown_style(false, true, window, cx),
|
||||
)))
|
||||
.child(gradient_overlay(gradient_color))
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
if is_open {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.into_any()
|
||||
}),
|
||||
)
|
||||
.when(in_progress && use_card_layout, |this| {
|
||||
this.child(
|
||||
div().absolute().right_2().child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"running",
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(
|
||||
delta,
|
||||
)))
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(failed_or_canceled, |this| {
|
||||
this.child(
|
||||
div().absolute().right_2().child(
|
||||
Icon::new(IconName::Close)
|
||||
.color(Color::Error)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
)
|
||||
}),
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_px()
|
||||
.when(is_collapsible, |this| {
|
||||
this.child(
|
||||
Disclosure::new(("expand", entry_ix), is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&card_header_id)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
if is_open {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(failed_or_canceled, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Close)
|
||||
.color(Color::Error)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.children(tool_output_display)
|
||||
}
|
||||
|
@ -1998,9 +1997,27 @@ impl AcpThreadView {
|
|||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let uri: SharedString = resource_link.uri.clone().into();
|
||||
let is_file = resource_link.uri.strip_prefix("file://");
|
||||
|
||||
let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") {
|
||||
path.to_string().into()
|
||||
let label: SharedString = if let Some(abs_path) = is_file {
|
||||
if let Some(project_path) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(&Path::new(abs_path), cx)
|
||||
&& let Some(worktree) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
{
|
||||
worktree
|
||||
.read(cx)
|
||||
.full_path(&project_path.path)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into()
|
||||
} else {
|
||||
abs_path.to_string().into()
|
||||
}
|
||||
} else {
|
||||
uri.clone()
|
||||
};
|
||||
|
@ -2017,10 +2034,12 @@ impl AcpThreadView {
|
|||
Button::new(button_id, label)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.truncate(true)
|
||||
.when(is_file.is_none(), |this| {
|
||||
this.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let workspace = self.workspace.clone();
|
||||
move |_, _, window, cx: &mut Context<Self>| {
|
||||
|
@ -2079,11 +2098,12 @@ impl AcpThreadView {
|
|||
let tool_call_id = tool_call_id.clone();
|
||||
let option_id = option.id.clone();
|
||||
let option_kind = option.kind;
|
||||
move |this, _, _, cx| {
|
||||
move |this, _, window, cx| {
|
||||
this.authorize_tool_call(
|
||||
tool_call_id.clone(),
|
||||
option_id.clone(),
|
||||
option_kind,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
@ -2460,7 +2480,6 @@ impl AcpThreadView {
|
|||
Some(
|
||||
h_flex()
|
||||
.px_2p5()
|
||||
.pb_1()
|
||||
.child(
|
||||
Icon::new(IconName::Attach)
|
||||
.size(IconSize::XSmall)
|
||||
|
@ -2476,8 +2495,7 @@ impl AcpThreadView {
|
|||
Label::new(user_rules_text)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.truncate()
|
||||
.buffer_font(cx),
|
||||
.truncate(),
|
||||
)
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.tooltip(Tooltip::text("View User Rules"))
|
||||
|
@ -2491,7 +2509,13 @@ impl AcpThreadView {
|
|||
}),
|
||||
)
|
||||
})
|
||||
.when(has_both, |this| this.child(Divider::vertical()))
|
||||
.when(has_both, |this| {
|
||||
this.child(
|
||||
Label::new("•")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
})
|
||||
.when_some(rules_file_text, |parent, rules_file_text| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
|
@ -2500,8 +2524,7 @@ impl AcpThreadView {
|
|||
.child(
|
||||
Label::new(rules_file_text)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.tooltip(Tooltip::text("View Project Rules"))
|
||||
|
@ -2725,6 +2748,12 @@ impl AcpThreadView {
|
|||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = this.agent.telemetry_id(),
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(method_id.clone(), window, cx)
|
||||
})
|
||||
})
|
||||
|
@ -2753,6 +2782,8 @@ impl AcpThreadView {
|
|||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id());
|
||||
|
||||
let task = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
|
@ -2760,7 +2791,7 @@ impl AcpThreadView {
|
|||
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("install".to_string()),
|
||||
id: task::TaskId(install_command.clone()),
|
||||
full_label: install_command.clone(),
|
||||
label: install_command.clone(),
|
||||
command: Some(install_command.clone()),
|
||||
|
@ -2810,6 +2841,8 @@ impl AcpThreadView {
|
|||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id());
|
||||
|
||||
let task = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
|
@ -2817,7 +2850,7 @@ impl AcpThreadView {
|
|||
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()),
|
||||
id: task::TaskId(upgrade_command.to_string()),
|
||||
full_label: upgrade_command.clone(),
|
||||
label: upgrade_command.clone(),
|
||||
command: Some(upgrade_command.clone()),
|
||||
|
@ -3078,13 +3111,13 @@ impl AcpThreadView {
|
|||
h_flex()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.when(expanded, |this| {
|
||||
this.border_b_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("edits-container")
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Disclosure::new("edits-disclosure", expanded))
|
||||
.map(|this| {
|
||||
|
@ -3633,13 +3666,53 @@ impl AcpThreadView {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_following(&self, cx: &App) -> bool {
|
||||
match self.thread().map(|thread| thread.read(cx).status()) {
|
||||
Some(ThreadStatus::Generating) => self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.is_being_followed(CollaboratorId::Agent)
|
||||
})
|
||||
.unwrap_or(false),
|
||||
_ => self.should_be_following,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_following(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let following = self.is_following(cx);
|
||||
|
||||
self.should_be_following = !following;
|
||||
if self.thread().map(|thread| thread.read(cx).status()) == Some(ThreadStatus::Generating) {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if following {
|
||||
workspace.unfollow(CollaboratorId::Agent, window, cx);
|
||||
} else {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
telemetry::event!("Follow Agent Selected", following = !following);
|
||||
}
|
||||
|
||||
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let following = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.is_being_followed(CollaboratorId::Agent)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let following = self.is_following(cx);
|
||||
|
||||
let tooltip_label = if following {
|
||||
if self.agent.name() == "Zed Agent" {
|
||||
format!("Stop Following the {}", self.agent.name())
|
||||
} else {
|
||||
format!("Stop Following {}", self.agent.name())
|
||||
}
|
||||
} else {
|
||||
if self.agent.name() == "Zed Agent" {
|
||||
format!("Follow the {}", self.agent.name())
|
||||
} else {
|
||||
format!("Follow {}", self.agent.name())
|
||||
}
|
||||
};
|
||||
|
||||
IconButton::new("follow-agent", IconName::Crosshair)
|
||||
.icon_size(IconSize::Small)
|
||||
|
@ -3648,10 +3721,10 @@ impl AcpThreadView {
|
|||
.selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
|
||||
.tooltip(move |window, cx| {
|
||||
if following {
|
||||
Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
|
||||
Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx)
|
||||
} else {
|
||||
Tooltip::with_meta(
|
||||
"Follow Agent",
|
||||
tooltip_label.clone(),
|
||||
Some(&Follow),
|
||||
"Track the agent's location as it reads and edits files.",
|
||||
window,
|
||||
|
@ -3660,15 +3733,7 @@ impl AcpThreadView {
|
|||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if following {
|
||||
workspace.unfollow(CollaboratorId::Agent, window, cx);
|
||||
} else {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
this.toggle_following(window, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -4080,7 +4145,20 @@ impl AcpThreadView {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_thread_controls(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
fn render_thread_controls(
|
||||
&self,
|
||||
thread: &Entity<AcpThread>,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
||||
if is_generating {
|
||||
return h_flex().id("thread-controls-container").ml_1().child(
|
||||
div()
|
||||
.py_2()
|
||||
.px(rems_from_px(22.))
|
||||
.child(SpinnerLabel::new().size(LabelSize::Small)),
|
||||
);
|
||||
}
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
|
@ -4177,13 +4255,8 @@ impl AcpThreadView {
|
|||
container.child(open_as_markdown).child(scroll_to_top)
|
||||
}
|
||||
|
||||
fn render_feedback_feedback_editor(
|
||||
editor: Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
v_flex()
|
||||
fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
|
||||
h_flex()
|
||||
.key_context("AgentFeedbackMessageEditor")
|
||||
.on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
|
||||
this.thread_feedback.dismiss_comments();
|
||||
|
@ -4192,43 +4265,31 @@ impl AcpThreadView {
|
|||
.on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| {
|
||||
this.submit_feedback_message(cx);
|
||||
}))
|
||||
.mb_2()
|
||||
.mx_4()
|
||||
.p_2()
|
||||
.mb_2()
|
||||
.mx_5()
|
||||
.gap_1()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(editor)
|
||||
.child(div().w_full().child(editor))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.justify_end()
|
||||
.child(
|
||||
Button::new("dismiss-feedback-message", "Cancel")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
IconButton::new("dismiss-feedback-message", IconName::Close)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.thread_feedback.dismiss_comments();
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("submit-feedback-message", "Share Feedback")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
IconButton::new("submit-feedback-message", IconName::Return)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.submit_feedback_message(cx);
|
||||
})),
|
||||
|
@ -4469,12 +4530,53 @@ impl AcpThreadView {
|
|||
}
|
||||
|
||||
fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
|
||||
let can_resume = self
|
||||
.thread()
|
||||
.map_or(false, |thread| thread.read(cx).can_resume(cx));
|
||||
|
||||
let can_enable_burn_mode = self.as_native_thread(cx).map_or(false, |thread| {
|
||||
let thread = thread.read(cx);
|
||||
let supports_burn_mode = thread
|
||||
.model()
|
||||
.map_or(false, |model| model.supports_burn_mode());
|
||||
supports_burn_mode && thread.completion_mode() == CompletionMode::Normal
|
||||
});
|
||||
|
||||
Callout::new()
|
||||
.severity(Severity::Error)
|
||||
.title("Error")
|
||||
.icon(IconName::XCircle)
|
||||
.description(error.clone())
|
||||
.actions_slot(self.create_copy_button(error.to_string()))
|
||||
.actions_slot(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.when(can_resume && can_enable_burn_mode, |this| {
|
||||
this.child(
|
||||
Button::new("enable-burn-mode-and-retry", "Enable Burn Mode and Retry")
|
||||
.icon(IconName::ZedBurnMode)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_burn_mode(&ToggleBurnMode, window, cx);
|
||||
this.resume_chat(cx);
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(can_resume, |this| {
|
||||
this.child(
|
||||
Button::new("retry", "Retry")
|
||||
.icon(IconName::RotateCw)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.resume_chat(cx);
|
||||
})),
|
||||
)
|
||||
})
|
||||
.child(self.create_copy_button(error.to_string())),
|
||||
)
|
||||
.dismiss_action(self.dismiss_error_button(cx))
|
||||
}
|
||||
|
||||
|
@ -4662,6 +4764,24 @@ impl AcpThreadView {
|
|||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let agent = self.agent.clone();
|
||||
let ThreadState::Ready { thread, .. } = &self.thread_state else {
|
||||
return;
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
self.clear_thread_error(cx);
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
Button::new("upgrade", "Upgrade")
|
||||
.label_size(LabelSize::Small)
|
||||
|
@ -4762,45 +4882,30 @@ impl Render for AcpThreadView {
|
|||
.items_center()
|
||||
.justify_end()
|
||||
.child(self.render_load_error(e, cx)),
|
||||
ThreadState::Ready { thread, .. } => {
|
||||
let thread_clone = thread.clone();
|
||||
|
||||
v_flex().flex_1().map(|this| {
|
||||
if has_messages {
|
||||
this.child(
|
||||
list(
|
||||
self.list_state.clone(),
|
||||
cx.processor(|this, index: usize, window, cx| {
|
||||
let Some((entry, len)) = this.thread().and_then(|thread| {
|
||||
let entries = &thread.read(cx).entries();
|
||||
Some((entries.get(index)?, entries.len()))
|
||||
}) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
this.render_entry(index, len, entry, window, cx)
|
||||
}),
|
||||
)
|
||||
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
|
||||
.flex_grow()
|
||||
.into_any(),
|
||||
ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
|
||||
if has_messages {
|
||||
this.child(
|
||||
list(
|
||||
self.list_state.clone(),
|
||||
cx.processor(|this, index: usize, window, cx| {
|
||||
let Some((entry, len)) = this.thread().and_then(|thread| {
|
||||
let entries = &thread.read(cx).entries();
|
||||
Some((entries.get(index)?, entries.len()))
|
||||
}) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
this.render_entry(index, len, entry, window, cx)
|
||||
}),
|
||||
)
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
.children(
|
||||
match thread_clone.read(cx).status() {
|
||||
ThreadStatus::Idle
|
||||
| ThreadStatus::WaitingForToolConfirmation => None,
|
||||
ThreadStatus::Generating => div()
|
||||
.py_2()
|
||||
.px(rems_from_px(22.))
|
||||
.child(SpinnerLabel::new().size(LabelSize::Small))
|
||||
.into(),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
this.child(self.render_recent_history(window, cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
|
||||
.flex_grow()
|
||||
.into_any(),
|
||||
)
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
} else {
|
||||
this.child(self.render_recent_history(window, cx))
|
||||
}
|
||||
}),
|
||||
})
|
||||
// The activity bar is intentionally rendered outside of the ThreadState::Ready match
|
||||
// above so that the scrollbar doesn't render behind it. The current setup allows
|
||||
|
@ -5217,6 +5322,10 @@ pub(crate) mod tests {
|
|||
where
|
||||
C: 'static + AgentConnection + Send + Clone,
|
||||
{
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"test"
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::Ai
|
||||
}
|
||||
|
@ -5265,6 +5374,12 @@ pub(crate) mod tests {
|
|||
project,
|
||||
action_log,
|
||||
SessionId("test".into()),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
}
|
||||
|
@ -5273,14 +5388,6 @@ pub(crate) mod tests {
|
|||
&[]
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn authenticate(
|
||||
&self,
|
||||
_method_id: acp::AuthMethodId,
|
||||
|
|
|
@ -5,6 +5,7 @@ mod tool_picker;
|
|||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::Plan;
|
||||
|
@ -15,7 +16,7 @@ use extension_host::ExtensionStore;
|
|||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||
Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
|
@ -23,10 +24,11 @@ use language_model::{
|
|||
};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
Project,
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||
|
@ -39,7 +41,7 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
|||
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
|
||||
use crate::{
|
||||
AddContextServer,
|
||||
AddContextServer, ExternalAgent, NewExternalAgentThread,
|
||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||
};
|
||||
|
||||
|
@ -47,6 +49,7 @@ pub struct AgentConfiguration {
|
|||
fs: Arc<dyn Fs>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
focus_handle: FocusHandle,
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
|
@ -56,6 +59,8 @@ pub struct AgentConfiguration {
|
|||
_registry_subscription: Subscription,
|
||||
scroll_handle: ScrollHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
gemini_is_installed: bool,
|
||||
_check_for_gemini: Task<()>,
|
||||
}
|
||||
|
||||
impl AgentConfiguration {
|
||||
|
@ -65,6 +70,7 @@ impl AgentConfiguration {
|
|||
tools: Entity<ToolWorkingSet>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
|
@ -89,6 +95,11 @@ impl AgentConfiguration {
|
|||
|
||||
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
|
||||
.detach();
|
||||
cx.observe_global_in::<SettingsStore>(window, |this, _, cx| {
|
||||
this.check_for_gemini(cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
let scroll_handle = ScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
@ -97,6 +108,7 @@ impl AgentConfiguration {
|
|||
fs,
|
||||
language_registry,
|
||||
workspace,
|
||||
project,
|
||||
focus_handle,
|
||||
configuration_views_by_provider: HashMap::default(),
|
||||
context_server_store,
|
||||
|
@ -106,8 +118,11 @@ impl AgentConfiguration {
|
|||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
scrollbar_state,
|
||||
gemini_is_installed: false,
|
||||
_check_for_gemini: Task::ready(()),
|
||||
};
|
||||
this.build_provider_configuration_views(window, cx);
|
||||
this.check_for_gemini(cx);
|
||||
this
|
||||
}
|
||||
|
||||
|
@ -137,6 +152,34 @@ impl AgentConfiguration {
|
|||
self.configuration_views_by_provider
|
||||
.insert(provider.id(), configuration_view);
|
||||
}
|
||||
|
||||
fn check_for_gemini(&mut self, cx: &mut Context<Self>) {
|
||||
let project = self.project.clone();
|
||||
let settings = AllAgentServersSettings::get_global(cx).clone();
|
||||
self._check_for_gemini = cx.spawn({
|
||||
async move |this, cx| {
|
||||
let Some(project) = project.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let gemini_is_installed = AgentServerCommand::resolve(
|
||||
Gemini::binary_name(),
|
||||
&[],
|
||||
// TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
|
||||
None,
|
||||
settings.gemini,
|
||||
&project,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.is_some();
|
||||
this.update(cx, |this, cx| {
|
||||
this.gemini_is_installed = gemini_is_installed;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentConfiguration {
|
||||
|
@ -211,7 +254,6 @@ impl AgentConfiguration {
|
|||
.child(
|
||||
h_flex()
|
||||
.id(provider_id_string.clone())
|
||||
.cursor_pointer()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.w_full()
|
||||
|
@ -231,10 +273,7 @@ impl AgentConfiguration {
|
|||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(provider_name.clone())
|
||||
.size(LabelSize::Large),
|
||||
)
|
||||
.child(Label::new(provider_name.clone()))
|
||||
.map(|this| {
|
||||
if is_zed_provider && is_signed_in {
|
||||
this.child(
|
||||
|
@ -279,7 +318,7 @@ impl AgentConfiguration {
|
|||
"Start New Thread",
|
||||
)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon(IconName::Thread)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small)
|
||||
|
@ -378,7 +417,7 @@ impl AgentConfiguration {
|
|||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
|
@ -519,6 +558,14 @@ impl AgentConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
|
||||
cx.theme().colors().background.opacity(0.25)
|
||||
}
|
||||
|
||||
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
|
||||
cx.theme().colors().border.opacity(0.6)
|
||||
}
|
||||
|
||||
fn render_context_servers_section(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
|
@ -536,7 +583,12 @@ impl AgentConfiguration {
|
|||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||
.child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)),
|
||||
.child(
|
||||
Label::new(
|
||||
"All context servers connected through the Model Context Protocol.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
context_server_ids.into_iter().map(|context_server_id| {
|
||||
|
@ -546,7 +598,7 @@ impl AgentConfiguration {
|
|||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
h_flex().w_full().child(
|
||||
Button::new("add-context-server", "Add Custom Server")
|
||||
|
@ -637,8 +689,6 @@ impl AgentConfiguration {
|
|||
.map_or([].as_slice(), |tools| tools.as_slice());
|
||||
let tool_count = tools.len();
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
|
||||
let (source_icon, source_tooltip) = if is_from_extension {
|
||||
(
|
||||
IconName::ZedMcpExtension,
|
||||
|
@ -781,8 +831,8 @@ impl AgentConfiguration {
|
|||
.id(item_id.clone())
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().background.opacity(0.2))
|
||||
.border_color(self.card_item_border_color(cx))
|
||||
.bg(self.card_item_bg_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
|
@ -790,7 +840,11 @@ impl AgentConfiguration {
|
|||
.justify_between()
|
||||
.when(
|
||||
error.is_some() || are_tools_expanded && tool_count >= 1,
|
||||
|element| element.border_b_1().border_color(border_color),
|
||||
|element| {
|
||||
element
|
||||
.border_b_1()
|
||||
.border_color(self.card_item_border_color(cx))
|
||||
},
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
@ -972,6 +1026,166 @@ impl AgentConfiguration {
|
|||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AllAgentServersSettings::get_global(cx).clone();
|
||||
let user_defined_agents = settings
|
||||
.custom
|
||||
.iter()
|
||||
.map(|(name, settings)| {
|
||||
self.render_agent_server(
|
||||
IconName::Ai,
|
||||
name.clone(),
|
||||
ExternalAgent::Custom {
|
||||
name: name.clone(),
|
||||
settings: settings.clone(),
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
v_flex()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("External Agents"))
|
||||
.child(
|
||||
Label::new(
|
||||
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiGemini,
|
||||
"Gemini CLI",
|
||||
ExternalAgent::Gemini,
|
||||
(!self.gemini_is_installed).then_some(Gemini::install_command().into()),
|
||||
cx,
|
||||
))
|
||||
// TODO add CC
|
||||
.children(user_defined_agents),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_agent_server(
|
||||
&self,
|
||||
icon: IconName,
|
||||
name: impl Into<SharedString>,
|
||||
agent: ExternalAgent,
|
||||
install_command: Option<SharedString>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let name = name.into();
|
||||
h_flex()
|
||||
.p_1()
|
||||
.pl_2()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(self.card_item_border_color(cx))
|
||||
.bg(self.card_item_bg_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||
.child(Label::new(name.clone())),
|
||||
)
|
||||
.map(|this| {
|
||||
if let Some(install_command) = install_command {
|
||||
this.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("install_external_agent-{name}")),
|
||||
"Install Agent",
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text(install_command.clone()))
|
||||
.on_click(cx.listener(
|
||||
move |this, _, window, cx| {
|
||||
let Some(project) = this.project.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(workspace) = this.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let cwd = project.read(cx).first_project_directory(cx);
|
||||
let shell =
|
||||
project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||
let spawn_in_terminal = task::SpawnInTerminal {
|
||||
id: task::TaskId(install_command.to_string()),
|
||||
full_label: install_command.to_string(),
|
||||
label: install_command.to_string(),
|
||||
command: Some(install_command.to_string()),
|
||||
args: Vec::new(),
|
||||
command_label: install_command.to_string(),
|
||||
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,
|
||||
};
|
||||
let task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.check_for_gemini(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
},
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
h_flex().gap_1().child(
|
||||
Button::new(
|
||||
SharedString::from(format!("start_acp_thread-{name}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Thread)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(agent.clone()),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentConfiguration {
|
||||
|
@ -991,6 +1205,7 @@ impl Render for AgentConfiguration {
|
|||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_general_settings_section(cx))
|
||||
.child(self.render_agent_servers_section(cx))
|
||||
.child(self.render_context_servers_section(window, cx))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
)
|
||||
|
|
|
@ -1529,6 +1529,7 @@ impl AgentDiff {
|
|||
| AcpThreadEvent::TokenUsageUpdated
|
||||
| AcpThreadEvent::EntriesRemoved(_)
|
||||
| AcpThreadEvent::ToolAuthorizationRequired
|
||||
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||
| AcpThreadEvent::Retry(_) => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ use agent_servers::AgentServerSettings;
|
|||
use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_actions::agent::ReauthenticateAgent;
|
||||
|
||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
|
@ -240,6 +242,7 @@ enum WhichFontSize {
|
|||
None,
|
||||
}
|
||||
|
||||
// TODO unify this with ExternalAgent
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum AgentType {
|
||||
#[default]
|
||||
|
@ -1024,6 +1027,8 @@ impl AgentPanel {
|
|||
}
|
||||
|
||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
telemetry::event!("Agent Thread Started", agent = "zed-text");
|
||||
|
||||
let context = self
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| context_store.create(cx));
|
||||
|
@ -1116,6 +1121,8 @@ impl AgentPanel {
|
|||
}
|
||||
};
|
||||
|
||||
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
|
@ -1473,6 +1480,7 @@ impl AgentPanel {
|
|||
tools,
|
||||
self.language_registry.clone(),
|
||||
self.workspace.clone(),
|
||||
self.project.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
@ -2204,6 +2212,8 @@ impl AgentPanel {
|
|||
"Enable Full Screen"
|
||||
};
|
||||
|
||||
let selected_agent = self.selected_agent.clone();
|
||||
|
||||
PopoverMenu::new("agent-options-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||
|
@ -2283,6 +2293,11 @@ impl AgentPanel {
|
|||
.action("Settings", Box::new(OpenSettings))
|
||||
.separator()
|
||||
.action(full_screen_label, Box::new(ToggleZoom));
|
||||
|
||||
if selected_agent == AgentType::Gemini {
|
||||
menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
|
||||
}
|
||||
|
||||
menu
|
||||
}))
|
||||
}
|
||||
|
@ -2317,6 +2332,8 @@ impl AgentPanel {
|
|||
.menu({
|
||||
let menu = self.assistant_navigation_menu.clone();
|
||||
move |window, cx| {
|
||||
telemetry::event!("View Thread History Clicked");
|
||||
|
||||
if let Some(menu) = menu.as_ref() {
|
||||
menu.update(cx, |_, cx| {
|
||||
cx.defer_in(window, |menu, window, cx| {
|
||||
|
@ -2495,6 +2512,8 @@ impl AgentPanel {
|
|||
let workspace = self.workspace.clone();
|
||||
|
||||
move |window, cx| {
|
||||
telemetry::event!("New Thread Clicked");
|
||||
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
menu = menu
|
||||
|
@ -2671,6 +2690,15 @@ impl AgentPanel {
|
|||
}
|
||||
|
||||
menu
|
||||
})
|
||||
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
|
||||
menu.separator().link(
|
||||
"Add Your Own Agent",
|
||||
OpenBrowser {
|
||||
url: "https://agentclientprotocol.com/".into(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
});
|
||||
menu
|
||||
}))
|
||||
|
@ -3751,6 +3779,11 @@ impl Render for AgentPanel {
|
|||
}
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_burn_mode))
|
||||
.on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
|
||||
if let Some(thread_view) = this.active_thread_view() {
|
||||
thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
|
||||
}
|
||||
}))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_onboarding(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
|
|
|
@ -160,6 +160,7 @@ pub struct NewNativeAgentThreadFromSummary {
|
|||
from_session_id: agent_client_protocol::SessionId,
|
||||
}
|
||||
|
||||
// TODO unify this with AgentType
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ExternalAgent {
|
||||
|
@ -174,6 +175,15 @@ enum ExternalAgent {
|
|||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::NativeAgent => "zed",
|
||||
Self::Gemini => "gemini-cli",
|
||||
Self::ClaudeCode => "claude-code",
|
||||
Self::Custom { .. } => "custom",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server(
|
||||
&self,
|
||||
fs: Arc<dyn fs::Fs>,
|
||||
|
|
|
@ -361,6 +361,7 @@ impl TextThreadEditor {
|
|||
if self.sending_disabled(cx) {
|
||||
return;
|
||||
}
|
||||
telemetry::event!("Agent Message Sent", agent = "zed-text");
|
||||
self.send_to_model(window, cx);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}
|
|||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct AiUpsellCard {
|
||||
pub sign_in_status: SignInStatus,
|
||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub account_too_young: bool,
|
||||
pub user_plan: Option<Plan>,
|
||||
pub tab_index: Option<isize>,
|
||||
sign_in_status: SignInStatus,
|
||||
sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
account_too_young: bool,
|
||||
user_plan: Option<Plan>,
|
||||
tab_index: Option<isize>,
|
||||
}
|
||||
|
||||
impl AiUpsellCard {
|
||||
|
@ -43,6 +43,11 @@ impl AiUpsellCard {
|
|||
tab_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
|
||||
self.tab_index = tab_index;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AiUpsellCard {
|
||||
|
|
|
@ -118,7 +118,7 @@ impl Tool for FetchTool {
|
|||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
fn may_perform_edits(&self) -> bool {
|
||||
|
|
|
@ -435,8 +435,8 @@ mod test {
|
|||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
PathBuf::from(path!("root/apple/banana/carrot")),
|
||||
PathBuf::from(path!("root/apple/bandana/carbonara"))
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -447,8 +447,8 @@ mod test {
|
|||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
PathBuf::from(path!("root/apple/banana/carrot")),
|
||||
PathBuf::from(path!("root/apple/bandana/carbonara"))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ impl Tool for ReadFileTool {
|
|||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ToolRead
|
||||
IconName::ToolSearch
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
|
|
@ -74,7 +74,7 @@ use std::{
|
|||
fmt::{self, Write},
|
||||
iter, mem,
|
||||
ops::{Deref, Range},
|
||||
path::Path,
|
||||
path::{self, Path},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
|
@ -90,8 +90,8 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||
use util::post_inc;
|
||||
use util::{RangeExt, ResultExt, debug_panic};
|
||||
use workspace::{
|
||||
CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item,
|
||||
notifications::NotifyTaskExt,
|
||||
CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace,
|
||||
item::Item, notifications::NotifyTaskExt,
|
||||
};
|
||||
|
||||
/// Determines what kinds of highlights should be applied to a lines background.
|
||||
|
@ -3603,176 +3603,187 @@ impl EditorElement {
|
|||
let focus_handle = editor.focus_handle(cx);
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
let header =
|
||||
div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_sm()
|
||||
.when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
colors.border_focused
|
||||
} else {
|
||||
colors.border
|
||||
};
|
||||
div.border_color(border_color)
|
||||
})
|
||||
.bg(colors.editor_subheader_background)
|
||||
.hover(|style| style.bg(colors.element_hover))
|
||||
.map(|header| {
|
||||
let editor = self.editor.clone();
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
let toggle_chevron_icon =
|
||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
.height(px(28.).into())
|
||||
.width(px(28.))
|
||||
.children(toggle_chevron_icon)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
"Toggle Excerpt Fold",
|
||||
Some(&ToggleFold),
|
||||
"Alt+click to toggle all",
|
||||
&focus_handle,
|
||||
let header = div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_sm()
|
||||
.when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
colors.border_focused
|
||||
} else {
|
||||
colors.border
|
||||
};
|
||||
div.border_color(border_color)
|
||||
})
|
||||
.bg(colors.editor_subheader_background)
|
||||
.hover(|style| style.bg(colors.element_hover))
|
||||
.map(|header| {
|
||||
let editor = self.editor.clone();
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
let toggle_chevron_icon =
|
||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
.height(px(28.).into())
|
||||
.width(px(28.))
|
||||
.children(toggle_chevron_icon)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
"Toggle Excerpt Fold",
|
||||
Some(&ToggleFold),
|
||||
"Alt+click to toggle all",
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |event, window, cx| {
|
||||
if event.modifiers().alt {
|
||||
// Alt+click toggles all buffers
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_fold_all(
|
||||
&ToggleFoldAll,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |event, window, cx| {
|
||||
if event.modifiers().alt {
|
||||
// Alt+click toggles all buffers
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Regular click toggles single buffer
|
||||
if is_folded {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_fold_all(
|
||||
&ToggleFoldAll,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
});
|
||||
} else {
|
||||
// Regular click toggles single buffer
|
||||
if is_folded {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
});
|
||||
} else {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
});
|
||||
}
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.children(
|
||||
editor
|
||||
.addons
|
||||
.values()
|
||||
.filter_map(|addon| {
|
||||
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
||||
})
|
||||
.take(1),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.size(Pixels(12.0))
|
||||
.justify_center()
|
||||
.children(indicator),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(
|
||||
filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into()),
|
||||
)
|
||||
.single_line()
|
||||
.when_some(file_status, |el, status| {
|
||||
el.color(if status.is_conflicted() {
|
||||
Color::Conflict
|
||||
} else if status.is_modified() {
|
||||
Color::Modified
|
||||
} else if status.is_deleted() {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Created
|
||||
})
|
||||
.when(status.is_deleted(), |el| el.strikethrough())
|
||||
}),
|
||||
)
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||
colors.text_disabled
|
||||
} else {
|
||||
colors.text_muted
|
||||
})
|
||||
.children(
|
||||
editor
|
||||
.addons
|
||||
.values()
|
||||
.filter_map(|addon| {
|
||||
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
||||
})
|
||||
.take(1),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.size(Pixels(12.0))
|
||||
.justify_center()
|
||||
.children(indicator),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.map(|path_header| {
|
||||
let filename = filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into());
|
||||
|
||||
path_header
|
||||
.when(ItemSettings::get_global(cx).file_icons, |el| {
|
||||
let path = path::Path::new(filename.as_str());
|
||||
let icon = FileIcons::get_icon(path, cx)
|
||||
.unwrap_or_default();
|
||||
let icon =
|
||||
Icon::from_path(icon).color(Color::Muted);
|
||||
el.child(icon)
|
||||
})
|
||||
.child(Label::new(filename).single_line().when_some(
|
||||
file_status,
|
||||
|el, status| {
|
||||
el.color(if status.is_conflicted() {
|
||||
Color::Conflict
|
||||
} else if status.is_modified() {
|
||||
Color::Modified
|
||||
} else if status.is_deleted() {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Created
|
||||
})
|
||||
.when(status.is_deleted(), |el| {
|
||||
el.strikethrough()
|
||||
})
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||
|el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.id("jump-to-file-button")
|
||||
.gap_2p5()
|
||||
.child(Label::new("Jump To File"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenExcerpts,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
);
|
||||
})
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||
colors.text_disabled
|
||||
} else {
|
||||
colors.text_muted
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||
|el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.id("jump-to-file-button")
|
||||
.gap_2p5()
|
||||
.child(Label::new("Jump To File"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenExcerpts,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
let file = for_excerpt.buffer.file().cloned();
|
||||
let editor = self.editor.clone();
|
||||
|
|
|
@ -1404,7 +1404,7 @@ impl ProjectItem for Editor {
|
|||
}
|
||||
|
||||
fn for_broken_project_item(
|
||||
abs_path: PathBuf,
|
||||
abs_path: &Path,
|
||||
is_local: bool,
|
||||
e: &anyhow::Error,
|
||||
window: &mut Window,
|
||||
|
|
|
@ -1401,13 +1401,16 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
#[cfg(windows)]
|
||||
let raw_query = raw_query.trim().to_owned().replace("/", "\\");
|
||||
#[cfg(not(windows))]
|
||||
let raw_query = raw_query.trim().to_owned();
|
||||
let raw_query = raw_query.trim();
|
||||
|
||||
let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query {
|
||||
let raw_query = raw_query.trim_end_matches(':').to_owned();
|
||||
let path = path_position.path.to_str();
|
||||
let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':');
|
||||
let file_query_end = if path_trimmed == raw_query {
|
||||
None
|
||||
} else {
|
||||
// Safe to unwrap as we won't get here when the unwrap in if fails
|
||||
Some(path_position.path.to_str().unwrap().len())
|
||||
Some(path.unwrap().len())
|
||||
};
|
||||
|
||||
let query = FileSearchQuery {
|
||||
|
|
|
@ -218,6 +218,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
|
|||
" ndan ",
|
||||
" band ",
|
||||
"a bandana",
|
||||
"bandana:",
|
||||
] {
|
||||
picker
|
||||
.update_in(cx, |picker, window, cx| {
|
||||
|
@ -252,6 +253,53 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
|
|||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a": {
|
||||
"foo:bar.rs": "",
|
||||
"foo.rs": "",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, _, cx) = build_find_picker(project, cx);
|
||||
|
||||
// 'foo:' matches both files
|
||||
cx.simulate_input("foo:");
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(picker.delegate.matches.len(), 3);
|
||||
assert_match_at_position(picker, 0, "foo.rs");
|
||||
assert_match_at_position(picker, 1, "foo:bar.rs");
|
||||
});
|
||||
|
||||
// 'foo:b' matches one of the files
|
||||
cx.simulate_input("b");
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(picker.delegate.matches.len(), 2);
|
||||
assert_match_at_position(picker, 0, "foo:bar.rs");
|
||||
});
|
||||
|
||||
cx.dispatch_action(editor::actions::Backspace);
|
||||
|
||||
// 'foo:1' matches both files, specifying which row to jump to
|
||||
cx.simulate_input("1");
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(picker.delegate.matches.len(), 3);
|
||||
assert_match_at_position(picker, 0, "foo.rs");
|
||||
assert_match_at_position(picker, 1, "foo:bar.rs");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_unicode_paths(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
|
|
@ -9,10 +9,8 @@ use parking::Parker;
|
|||
use parking_lot::Mutex;
|
||||
use util::ResultExt;
|
||||
use windows::{
|
||||
Foundation::TimeSpan,
|
||||
System::Threading::{
|
||||
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions,
|
||||
WorkItemPriority,
|
||||
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority,
|
||||
},
|
||||
Win32::{
|
||||
Foundation::{LPARAM, WPARAM},
|
||||
|
@ -56,12 +54,7 @@ impl WindowsDispatcher {
|
|||
Ok(())
|
||||
})
|
||||
};
|
||||
ThreadPool::RunWithPriorityAndOptionsAsync(
|
||||
&handler,
|
||||
WorkItemPriority::High,
|
||||
WorkItemOptions::TimeSliced,
|
||||
)
|
||||
.log_err();
|
||||
ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err();
|
||||
}
|
||||
|
||||
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
|
||||
|
@ -72,12 +65,7 @@ impl WindowsDispatcher {
|
|||
Ok(())
|
||||
})
|
||||
};
|
||||
let delay = TimeSpan {
|
||||
// A time period expressed in 100-nanosecond units.
|
||||
// 10,000,000 ticks per second
|
||||
Duration: (duration.as_nanos() / 100) as i64,
|
||||
};
|
||||
ThreadPoolTimer::CreateTimer(&handler, delay).log_err();
|
||||
ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1569,11 +1569,21 @@ impl Buffer {
|
|||
self.send_operation(op, true, cx);
|
||||
}
|
||||
|
||||
pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> {
|
||||
let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else {
|
||||
return None;
|
||||
};
|
||||
Some(&self.diagnostics[idx].1)
|
||||
pub fn buffer_diagnostics(
|
||||
&self,
|
||||
for_server: Option<LanguageServerId>,
|
||||
) -> Vec<&DiagnosticEntry<Anchor>> {
|
||||
match for_server {
|
||||
Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) {
|
||||
Ok(idx) => self.diagnostics[idx].1.iter().collect(),
|
||||
Err(_) => Vec::new(),
|
||||
},
|
||||
None => self
|
||||
.diagnostics
|
||||
.iter()
|
||||
.flat_map(|(_, diagnostic_set)| diagnostic_set.iter())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn request_autoindent(&mut self, cx: &mut Context<Self>) {
|
||||
|
|
|
@ -1743,6 +1743,5 @@ pub enum Event {
|
|||
}
|
||||
|
||||
impl EventEmitter<Event> for LogStore {}
|
||||
impl EventEmitter<Event> for LspLogView {}
|
||||
impl EventEmitter<EditorEvent> for LspLogView {}
|
||||
impl EventEmitter<SearchEvent> for LspLogView {}
|
||||
|
|
|
@ -11,6 +11,21 @@
|
|||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (member_expression
|
||||
object: (identifier) @_obj (#eq? @_obj "styled")
|
||||
property: (property_identifier))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (call_expression
|
||||
function: (identifier) @_name (#eq? @_name "styled"))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @_name (#eq? @_name "html")
|
||||
arguments: (template_string) @injection.content
|
||||
|
|
|
@ -6,9 +6,6 @@
|
|||
(self) @variable.special
|
||||
(field_identifier) @property
|
||||
|
||||
(shorthand_field_initializer
|
||||
(identifier) @property)
|
||||
|
||||
(trait_item name: (type_identifier) @type.interface)
|
||||
(impl_item trait: (type_identifier) @type.interface)
|
||||
(abstract_type trait: (type_identifier) @type.interface)
|
||||
|
@ -41,20 +38,11 @@
|
|||
(identifier) @function.special
|
||||
(scoped_identifier
|
||||
name: (identifier) @function.special)
|
||||
]
|
||||
"!" @function.special)
|
||||
])
|
||||
|
||||
(macro_definition
|
||||
name: (identifier) @function.special.definition)
|
||||
|
||||
(mod_item
|
||||
name: (identifier) @module)
|
||||
|
||||
(visibility_modifier [
|
||||
(crate) @keyword
|
||||
(super) @keyword
|
||||
])
|
||||
|
||||
; Identifier conventions
|
||||
|
||||
; Assume uppercase names are types/enum-constructors
|
||||
|
@ -127,7 +115,9 @@
|
|||
"where"
|
||||
"while"
|
||||
"yield"
|
||||
(crate)
|
||||
(mutable_specifier)
|
||||
(super)
|
||||
] @keyword
|
||||
|
||||
[
|
||||
|
@ -199,7 +189,6 @@
|
|||
operator: "/" @operator
|
||||
|
||||
(lifetime) @lifetime
|
||||
(lifetime (identifier) @lifetime)
|
||||
|
||||
(parameter (identifier) @variable.parameter)
|
||||
|
||||
|
|
|
@ -11,6 +11,21 @@
|
|||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (member_expression
|
||||
object: (identifier) @_obj (#eq? @_obj "styled")
|
||||
property: (property_identifier))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (call_expression
|
||||
function: (identifier) @_name (#eq? @_name "styled"))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @_name (#eq? @_name "html")
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
|
|
|
@ -15,6 +15,21 @@
|
|||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (member_expression
|
||||
object: (identifier) @_obj (#eq? @_obj "styled")
|
||||
property: (property_identifier))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (call_expression
|
||||
function: (identifier) @_name (#eq? @_name "styled"))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @_name (#eq? @_name "html")
|
||||
arguments: (template_string) @injection.content
|
||||
|
|
|
@ -1085,10 +1085,10 @@ impl Element for MarkdownElement {
|
|||
);
|
||||
el.child(
|
||||
h_flex()
|
||||
.w_5()
|
||||
.w_4()
|
||||
.absolute()
|
||||
.top_1()
|
||||
.right_1()
|
||||
.top_1p5()
|
||||
.right_1p5()
|
||||
.justify_end()
|
||||
.child(codeblock),
|
||||
)
|
||||
|
@ -1115,11 +1115,12 @@ impl Element for MarkdownElement {
|
|||
cx,
|
||||
);
|
||||
el.child(
|
||||
div()
|
||||
h_flex()
|
||||
.w_4()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.w_5()
|
||||
.justify_end()
|
||||
.visible_on_hover("code_block")
|
||||
.child(codeblock),
|
||||
)
|
||||
|
|
|
@ -283,17 +283,13 @@ pub(crate) fn render_ai_setup_page(
|
|||
v_flex()
|
||||
.mt_2()
|
||||
.gap_6()
|
||||
.child({
|
||||
let mut ai_upsell_card =
|
||||
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx);
|
||||
|
||||
ai_upsell_card.tab_index = Some({
|
||||
tab_index += 1;
|
||||
tab_index - 1
|
||||
});
|
||||
|
||||
ai_upsell_card
|
||||
})
|
||||
.child(
|
||||
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
|
||||
.tab_index(Some({
|
||||
tab_index += 1;
|
||||
tab_index - 1
|
||||
})),
|
||||
)
|
||||
.child(render_llm_provider_section(
|
||||
&mut tab_index,
|
||||
workspace,
|
||||
|
|
|
@ -446,7 +446,6 @@ pub enum ResponseStreamResult {
|
|||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ResponseStreamEvent {
|
||||
pub model: String,
|
||||
pub choices: Vec<ChoiceDelta>,
|
||||
pub usage: Option<Usage>,
|
||||
}
|
||||
|
|
|
@ -7588,19 +7588,16 @@ impl LspStore {
|
|||
let snapshot = buffer_handle.read(cx).snapshot();
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let reused_diagnostics = buffer
|
||||
.get_diagnostics(server_id)
|
||||
.into_iter()
|
||||
.flat_map(|diag| {
|
||||
diag.iter()
|
||||
.filter(|v| merge(buffer, &v.diagnostic, cx))
|
||||
.map(|v| {
|
||||
let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
|
||||
let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
|
||||
DiagnosticEntry {
|
||||
range: start..end,
|
||||
diagnostic: v.diagnostic.clone(),
|
||||
}
|
||||
})
|
||||
.buffer_diagnostics(Some(server_id))
|
||||
.iter()
|
||||
.filter(|v| merge(buffer, &v.diagnostic, cx))
|
||||
.map(|v| {
|
||||
let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
|
||||
let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
|
||||
DiagnosticEntry {
|
||||
range: start..end,
|
||||
diagnostic: v.diagnostic.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
@ -11706,12 +11703,11 @@ impl LspStore {
|
|||
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
|
||||
}
|
||||
"workspace/symbol" => {
|
||||
if let Some(options) = parse_register_capabilities(reg)? {
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.workspace_symbol_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.workspace_symbol_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"workspace/fileOperations" => {
|
||||
if let Some(options) = reg.register_options {
|
||||
|
@ -11735,12 +11731,11 @@ impl LspStore {
|
|||
}
|
||||
}
|
||||
"textDocument/rangeFormatting" => {
|
||||
if let Some(options) = parse_register_capabilities(reg)? {
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_range_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_range_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/onTypeFormatting" => {
|
||||
if let Some(options) = reg
|
||||
|
@ -11755,36 +11750,32 @@ impl LspStore {
|
|||
}
|
||||
}
|
||||
"textDocument/formatting" => {
|
||||
if let Some(options) = parse_register_capabilities(reg)? {
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/rename" => {
|
||||
if let Some(options) = parse_register_capabilities(reg)? {
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.rename_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.rename_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/inlayHint" => {
|
||||
if let Some(options) = parse_register_capabilities(reg)? {
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.inlay_hint_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.inlay_hint_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/documentSymbol" => {
|
||||
if let Some(options) = parse_register_capabilities(reg)? {
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_symbol_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_symbol_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/codeAction" => {
|
||||
if let Some(options) = reg
|
||||
|
@ -11800,12 +11791,11 @@ impl LspStore {
|
|||
}
|
||||
}
|
||||
"textDocument/definition" => {
|
||||
if let Some(options) = parse_register_capabilities(reg)? {
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.definition_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.definition_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/completion" => {
|
||||
if let Some(caps) = reg
|
||||
|
@ -12184,10 +12174,10 @@ impl LspStore {
|
|||
// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133
|
||||
fn parse_register_capabilities<T: serde::de::DeserializeOwned>(
|
||||
reg: lsp::Registration,
|
||||
) -> anyhow::Result<Option<OneOf<bool, T>>> {
|
||||
) -> Result<OneOf<bool, T>> {
|
||||
Ok(match reg.register_options {
|
||||
Some(options) => Some(OneOf::Right(serde_json::from_value::<T>(options)?)),
|
||||
None => Some(OneOf::Left(true)),
|
||||
Some(options) => OneOf::Right(serde_json::from_value::<T>(options)?),
|
||||
None => OneOf::Left(true),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -65,6 +65,7 @@ telemetry_events.workspace = true
|
|||
util.workspace = true
|
||||
watch.workspace = true
|
||||
worktree.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
crashes.workspace = true
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::Parser;
|
||||
use remote_server::Commands;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
|
@ -21,105 +22,34 @@ struct Cli {
|
|||
printenv: bool,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
Run {
|
||||
#[arg(long)]
|
||||
log_file: PathBuf,
|
||||
#[arg(long)]
|
||||
pid_file: PathBuf,
|
||||
#[arg(long)]
|
||||
stdin_socket: PathBuf,
|
||||
#[arg(long)]
|
||||
stdout_socket: PathBuf,
|
||||
#[arg(long)]
|
||||
stderr_socket: PathBuf,
|
||||
},
|
||||
Proxy {
|
||||
#[arg(long)]
|
||||
reconnect: bool,
|
||||
#[arg(long)]
|
||||
identifier: String,
|
||||
},
|
||||
Version,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() {
|
||||
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
|
||||
use remote::proxy::ProxyLaunchError;
|
||||
use remote_server::unix::{execute_proxy, execute_run};
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if let Some(socket_path) = &cli.askpass {
|
||||
askpass::main(socket_path);
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(socket) = &cli.crash_handler {
|
||||
crashes::crash_server(socket.as_path());
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if cli.printenv {
|
||||
util::shell_env::print_env();
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let result = match cli.command {
|
||||
Some(Commands::Run {
|
||||
log_file,
|
||||
pid_file,
|
||||
stdin_socket,
|
||||
stdout_socket,
|
||||
stderr_socket,
|
||||
}) => execute_run(
|
||||
log_file,
|
||||
pid_file,
|
||||
stdin_socket,
|
||||
stdout_socket,
|
||||
stderr_socket,
|
||||
),
|
||||
Some(Commands::Proxy {
|
||||
identifier,
|
||||
reconnect,
|
||||
}) => match execute_proxy(identifier, reconnect) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
if let Some(err) = err.downcast_ref::<ProxyLaunchError>() {
|
||||
std::process::exit(err.to_exit_code());
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
},
|
||||
Some(Commands::Version) => {
|
||||
let release_channel = *RELEASE_CHANNEL;
|
||||
match release_channel {
|
||||
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
||||
println!("{}", env!("ZED_PKG_VERSION"))
|
||||
}
|
||||
ReleaseChannel::Nightly | ReleaseChannel::Dev => {
|
||||
println!(
|
||||
"{}",
|
||||
option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name())
|
||||
)
|
||||
}
|
||||
};
|
||||
std::process::exit(0);
|
||||
}
|
||||
None => {
|
||||
eprintln!("usage: remote <run|proxy|version>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
if let Err(error) = result {
|
||||
log::error!("exiting due to error: {}", error);
|
||||
if let Some(command) = cli.command {
|
||||
remote_server::run(command)
|
||||
} else {
|
||||
eprintln!("usage: remote <run|proxy|version>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,4 +6,78 @@ pub mod unix;
|
|||
#[cfg(test)]
|
||||
mod remote_editing_tests;
|
||||
|
||||
use clap::Subcommand;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use headless_project::{HeadlessAppState, HeadlessProject};
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
Run {
|
||||
#[arg(long)]
|
||||
log_file: PathBuf,
|
||||
#[arg(long)]
|
||||
pid_file: PathBuf,
|
||||
#[arg(long)]
|
||||
stdin_socket: PathBuf,
|
||||
#[arg(long)]
|
||||
stdout_socket: PathBuf,
|
||||
#[arg(long)]
|
||||
stderr_socket: PathBuf,
|
||||
},
|
||||
Proxy {
|
||||
#[arg(long)]
|
||||
reconnect: bool,
|
||||
#[arg(long)]
|
||||
identifier: String,
|
||||
},
|
||||
Version,
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn run(command: Commands) -> anyhow::Result<()> {
|
||||
use anyhow::Context;
|
||||
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
|
||||
use unix::{ExecuteProxyError, execute_proxy, execute_run};
|
||||
|
||||
match command {
|
||||
Commands::Run {
|
||||
log_file,
|
||||
pid_file,
|
||||
stdin_socket,
|
||||
stdout_socket,
|
||||
stderr_socket,
|
||||
} => execute_run(
|
||||
log_file,
|
||||
pid_file,
|
||||
stdin_socket,
|
||||
stdout_socket,
|
||||
stderr_socket,
|
||||
),
|
||||
Commands::Proxy {
|
||||
identifier,
|
||||
reconnect,
|
||||
} => execute_proxy(identifier, reconnect)
|
||||
.inspect_err(|err| {
|
||||
if let ExecuteProxyError::ServerNotRunning(err) = err {
|
||||
std::process::exit(err.to_exit_code());
|
||||
}
|
||||
})
|
||||
.context("running proxy on the remote server"),
|
||||
Commands::Version => {
|
||||
let release_channel = *RELEASE_CHANNEL;
|
||||
match release_channel {
|
||||
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
||||
println!("{}", env!("ZED_PKG_VERSION"))
|
||||
}
|
||||
ReleaseChannel::Nightly | ReleaseChannel::Dev => {
|
||||
println!(
|
||||
"{}",
|
||||
option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name())
|
||||
)
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ use smol::Async;
|
|||
use smol::{net::unix::UnixListener, stream::StreamExt as _};
|
||||
use std::ffi::OsStr;
|
||||
use std::ops::ControlFlow;
|
||||
use std::process::ExitStatus;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
use std::{env, thread};
|
||||
|
@ -46,6 +47,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
use telemetry_events::LocationData;
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
||||
pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL {
|
||||
|
@ -526,7 +528,23 @@ pub fn execute_run(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum ServerPathError {
|
||||
#[error("Failed to create server_dir `{path}`")]
|
||||
CreateServerDir {
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
#[error("Failed to create logs_dir `{path}`")]
|
||||
CreateLogsDir {
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ServerPaths {
|
||||
log_file: PathBuf,
|
||||
pid_file: PathBuf,
|
||||
|
@ -536,10 +554,19 @@ struct ServerPaths {
|
|||
}
|
||||
|
||||
impl ServerPaths {
|
||||
fn new(identifier: &str) -> Result<Self> {
|
||||
fn new(identifier: &str) -> Result<Self, ServerPathError> {
|
||||
let server_dir = paths::remote_server_state_dir().join(identifier);
|
||||
std::fs::create_dir_all(&server_dir)?;
|
||||
std::fs::create_dir_all(&logs_dir())?;
|
||||
std::fs::create_dir_all(&server_dir).map_err(|source| {
|
||||
ServerPathError::CreateServerDir {
|
||||
source,
|
||||
path: server_dir.clone(),
|
||||
}
|
||||
})?;
|
||||
let log_dir = logs_dir();
|
||||
std::fs::create_dir_all(log_dir).map_err(|source| ServerPathError::CreateLogsDir {
|
||||
source: source,
|
||||
path: log_dir.clone(),
|
||||
})?;
|
||||
|
||||
let pid_file = server_dir.join("server.pid");
|
||||
let stdin_socket = server_dir.join("stdin.sock");
|
||||
|
@ -557,7 +584,43 @@ impl ServerPaths {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum ExecuteProxyError {
|
||||
#[error("Failed to init server paths")]
|
||||
ServerPath(#[from] ServerPathError),
|
||||
|
||||
#[error(transparent)]
|
||||
ServerNotRunning(#[from] ProxyLaunchError),
|
||||
|
||||
#[error("Failed to check PidFile '{path}'")]
|
||||
CheckPidFile {
|
||||
#[source]
|
||||
source: CheckPidError,
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
#[error("Failed to kill existing server with pid '{pid}'")]
|
||||
KillRunningServer {
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
pid: u32,
|
||||
},
|
||||
|
||||
#[error("failed to spawn server")]
|
||||
SpawnServer(#[source] SpawnServerError),
|
||||
|
||||
#[error("stdin_task failed")]
|
||||
StdinTask(#[source] anyhow::Error),
|
||||
#[error("stdout_task failed")]
|
||||
StdoutTask(#[source] anyhow::Error),
|
||||
#[error("stderr_task failed")]
|
||||
StderrTask(#[source] anyhow::Error),
|
||||
}
|
||||
|
||||
pub(crate) fn execute_proxy(
|
||||
identifier: String,
|
||||
is_reconnecting: bool,
|
||||
) -> Result<(), ExecuteProxyError> {
|
||||
init_logging_proxy();
|
||||
|
||||
let server_paths = ServerPaths::new(&identifier)?;
|
||||
|
@ -574,12 +637,19 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
|||
|
||||
log::info!("starting proxy process. PID: {}", std::process::id());
|
||||
|
||||
let server_pid = check_pid_file(&server_paths.pid_file)?;
|
||||
let server_pid = check_pid_file(&server_paths.pid_file).map_err(|source| {
|
||||
ExecuteProxyError::CheckPidFile {
|
||||
source,
|
||||
path: server_paths.pid_file.clone(),
|
||||
}
|
||||
})?;
|
||||
let server_running = server_pid.is_some();
|
||||
if is_reconnecting {
|
||||
if !server_running {
|
||||
log::error!("attempted to reconnect, but no server running");
|
||||
anyhow::bail!(ProxyLaunchError::ServerNotRunning);
|
||||
return Err(ExecuteProxyError::ServerNotRunning(
|
||||
ProxyLaunchError::ServerNotRunning,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if let Some(pid) = server_pid {
|
||||
|
@ -590,7 +660,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
|||
kill_running_server(pid, &server_paths)?;
|
||||
}
|
||||
|
||||
spawn_server(&server_paths)?;
|
||||
spawn_server(&server_paths).map_err(ExecuteProxyError::SpawnServer)?;
|
||||
};
|
||||
|
||||
let stdin_task = smol::spawn(async move {
|
||||
|
@ -630,9 +700,9 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
|||
|
||||
if let Err(forwarding_result) = smol::block_on(async move {
|
||||
futures::select! {
|
||||
result = stdin_task.fuse() => result.context("stdin_task failed"),
|
||||
result = stdout_task.fuse() => result.context("stdout_task failed"),
|
||||
result = stderr_task.fuse() => result.context("stderr_task failed"),
|
||||
result = stdin_task.fuse() => result.map_err(ExecuteProxyError::StdinTask),
|
||||
result = stdout_task.fuse() => result.map_err(ExecuteProxyError::StdoutTask),
|
||||
result = stderr_task.fuse() => result.map_err(ExecuteProxyError::StderrTask),
|
||||
}
|
||||
}) {
|
||||
log::error!(
|
||||
|
@ -645,12 +715,12 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> {
|
||||
fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> {
|
||||
log::info!("killing existing server with PID {}", pid);
|
||||
std::process::Command::new("kill")
|
||||
.arg(pid.to_string())
|
||||
.output()
|
||||
.context("failed to kill existing server")?;
|
||||
.map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?;
|
||||
|
||||
for file in [
|
||||
&paths.pid_file,
|
||||
|
@ -664,18 +734,39 @@ fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_server(paths: &ServerPaths) -> Result<()> {
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum SpawnServerError {
|
||||
#[error("failed to remove stdin socket")]
|
||||
RemoveStdinSocket(#[source] std::io::Error),
|
||||
|
||||
#[error("failed to remove stdout socket")]
|
||||
RemoveStdoutSocket(#[source] std::io::Error),
|
||||
|
||||
#[error("failed to remove stderr socket")]
|
||||
RemoveStderrSocket(#[source] std::io::Error),
|
||||
|
||||
#[error("failed to get current_exe")]
|
||||
CurrentExe(#[source] std::io::Error),
|
||||
|
||||
#[error("failed to launch server process")]
|
||||
ProcessStatus(#[source] std::io::Error),
|
||||
|
||||
#[error("failed to launch and detach server process: {status}\n{paths}")]
|
||||
LaunchStatus { status: ExitStatus, paths: String },
|
||||
}
|
||||
|
||||
fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> {
|
||||
if paths.stdin_socket.exists() {
|
||||
std::fs::remove_file(&paths.stdin_socket)?;
|
||||
std::fs::remove_file(&paths.stdin_socket).map_err(SpawnServerError::RemoveStdinSocket)?;
|
||||
}
|
||||
if paths.stdout_socket.exists() {
|
||||
std::fs::remove_file(&paths.stdout_socket)?;
|
||||
std::fs::remove_file(&paths.stdout_socket).map_err(SpawnServerError::RemoveStdoutSocket)?;
|
||||
}
|
||||
if paths.stderr_socket.exists() {
|
||||
std::fs::remove_file(&paths.stderr_socket)?;
|
||||
std::fs::remove_file(&paths.stderr_socket).map_err(SpawnServerError::RemoveStderrSocket)?;
|
||||
}
|
||||
|
||||
let binary_name = std::env::current_exe()?;
|
||||
let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?;
|
||||
let mut server_process = std::process::Command::new(binary_name);
|
||||
server_process
|
||||
.arg("run")
|
||||
|
@ -692,11 +783,17 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
|
|||
|
||||
let status = server_process
|
||||
.status()
|
||||
.context("failed to launch server process")?;
|
||||
anyhow::ensure!(
|
||||
status.success(),
|
||||
"failed to launch and detach server process"
|
||||
);
|
||||
.map_err(SpawnServerError::ProcessStatus)?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(SpawnServerError::LaunchStatus {
|
||||
status,
|
||||
paths: format!(
|
||||
"log file: {:?}, pid file: {:?}",
|
||||
paths.log_file, paths.pid_file,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let mut total_time_waited = std::time::Duration::from_secs(0);
|
||||
let wait_duration = std::time::Duration::from_millis(20);
|
||||
|
@ -717,7 +814,15 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn check_pid_file(path: &Path) -> Result<Option<u32>> {
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Failed to remove PID file for missing process (pid `{pid}`")]
|
||||
pub(crate) struct CheckPidError {
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
pid: u32,
|
||||
}
|
||||
|
||||
fn check_pid_file(path: &Path) -> Result<Option<u32>, CheckPidError> {
|
||||
let Some(pid) = std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.and_then(|contents| contents.parse::<u32>().ok())
|
||||
|
@ -742,7 +847,7 @@ fn check_pid_file(path: &Path) -> Result<Option<u32>> {
|
|||
log::debug!(
|
||||
"Found PID file, but process with that PID does not exist. Removing PID file."
|
||||
);
|
||||
std::fs::remove_file(&path).context("Failed to remove PID file")?;
|
||||
std::fs::remove_file(&path).map_err(|source| CheckPidError { source, pid })?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
mod tab_switcher_tests;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::items::entry_git_aware_label_color;
|
||||
use editor::items::{
|
||||
entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color,
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Render,
|
||||
Styled, Task, WeakEntity, Window, actions, rems,
|
||||
Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point,
|
||||
Render, Styled, Task, WeakEntity, Window, actions, rems,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::Project;
|
||||
|
@ -15,11 +17,14 @@ use schemars::JsonSchema;
|
|||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{cmp::Reverse, sync::Arc};
|
||||
use ui::{ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
use ui::{
|
||||
DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip,
|
||||
prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
ModalView, Pane, SaveIntent, Workspace,
|
||||
item::{ItemHandle, ItemSettings, TabContentParams},
|
||||
item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams},
|
||||
pane::{Event as PaneEvent, render_item_indicator, tab_details},
|
||||
};
|
||||
|
||||
|
@ -233,6 +238,77 @@ pub struct TabSwitcherDelegate {
|
|||
restored_items: bool,
|
||||
}
|
||||
|
||||
impl TabMatch {
|
||||
fn icon(
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
selected: bool,
|
||||
window: &Window,
|
||||
cx: &App,
|
||||
) -> Option<DecoratedIcon> {
|
||||
let icon = self.item.tab_icon(window, cx)?;
|
||||
let item_settings = ItemSettings::get_global(cx);
|
||||
let show_diagnostics = item_settings.show_diagnostics;
|
||||
let git_status_color = item_settings
|
||||
.git_status
|
||||
.then(|| {
|
||||
let path = self.item.project_path(cx)?;
|
||||
let project = project.read(cx);
|
||||
let entry = project.entry_for_path(&path, cx)?;
|
||||
let git_status = project
|
||||
.project_path_git_status(&path, cx)
|
||||
.map(|status| status.summary())
|
||||
.unwrap_or_default();
|
||||
Some(entry_git_aware_label_color(
|
||||
git_status,
|
||||
entry.is_ignored,
|
||||
selected,
|
||||
))
|
||||
})
|
||||
.flatten();
|
||||
let colored_icon = icon.color(git_status_color.unwrap_or_default());
|
||||
|
||||
let most_sever_diagostic_level = if show_diagnostics == ShowDiagnostics::Off {
|
||||
None
|
||||
} else {
|
||||
let buffer_store = project.read(cx).buffer_store().read(cx);
|
||||
let buffer = self
|
||||
.item
|
||||
.project_path(cx)
|
||||
.and_then(|path| buffer_store.get_by_path(&path))
|
||||
.map(|buffer| buffer.read(cx));
|
||||
buffer.and_then(|buffer| {
|
||||
buffer
|
||||
.buffer_diagnostics(None)
|
||||
.iter()
|
||||
.map(|diagnostic_entry| diagnostic_entry.diagnostic.severity)
|
||||
.min()
|
||||
})
|
||||
};
|
||||
|
||||
let decorations =
|
||||
entry_diagnostic_aware_icon_decoration_and_color(most_sever_diagostic_level)
|
||||
.filter(|(d, _)| {
|
||||
*d != IconDecorationKind::Triangle
|
||||
|| show_diagnostics != ShowDiagnostics::Errors
|
||||
})
|
||||
.map(|(icon, color)| {
|
||||
let knockout_item_color = if selected {
|
||||
cx.theme().colors().element_selected
|
||||
} else {
|
||||
cx.theme().colors().element_background
|
||||
};
|
||||
IconDecoration::new(icon, knockout_item_color, cx)
|
||||
.color(color.color(cx))
|
||||
.position(Point {
|
||||
x: px(-2.),
|
||||
y: px(-2.),
|
||||
})
|
||||
});
|
||||
Some(DecoratedIcon::new(colored_icon, decorations))
|
||||
}
|
||||
}
|
||||
|
||||
impl TabSwitcherDelegate {
|
||||
#[allow(clippy::complexity)]
|
||||
fn new(
|
||||
|
@ -574,31 +650,7 @@ impl PickerDelegate for TabSwitcherDelegate {
|
|||
};
|
||||
let label = tab_match.item.tab_content(params, window, cx);
|
||||
|
||||
let icon = tab_match.item.tab_icon(window, cx).map(|icon| {
|
||||
let git_status_color = ItemSettings::get_global(cx)
|
||||
.git_status
|
||||
.then(|| {
|
||||
tab_match
|
||||
.item
|
||||
.project_path(cx)
|
||||
.as_ref()
|
||||
.and_then(|path| {
|
||||
let project = self.project.read(cx);
|
||||
let entry = project.entry_for_path(path, cx)?;
|
||||
let git_status = project
|
||||
.project_path_git_status(path, cx)
|
||||
.map(|status| status.summary())
|
||||
.unwrap_or_default();
|
||||
Some((entry, git_status))
|
||||
})
|
||||
.map(|(entry, git_status)| {
|
||||
entry_git_aware_label_color(git_status, entry.is_ignored, selected)
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
|
||||
icon.color(git_status_color.unwrap_or_default())
|
||||
});
|
||||
let icon = tab_match.icon(&self.project, selected, window, cx);
|
||||
|
||||
let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
|
||||
let indicator_color = if let Some(ref indicator) = indicator {
|
||||
|
@ -640,7 +692,7 @@ impl PickerDelegate for TabSwitcherDelegate {
|
|||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(h_flex().w_full().child(label))
|
||||
.start_slot::<Icon>(icon)
|
||||
.start_slot::<DecoratedIcon>(icon)
|
||||
.map(|el| {
|
||||
if self.selected_index == ix {
|
||||
el.end_slot::<AnyElement>(close_button)
|
||||
|
|
|
@ -561,7 +561,7 @@ impl ContextMenu {
|
|||
action: Some(action.boxed_clone()),
|
||||
handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
|
||||
icon: Some(IconName::ArrowUpRight),
|
||||
icon_size: IconSize::Small,
|
||||
icon_size: IconSize::XSmall,
|
||||
icon_position: IconPosition::End,
|
||||
icon_color: None,
|
||||
disabled: false,
|
||||
|
|
|
@ -162,6 +162,19 @@ impl<T> Receiver<T> {
|
|||
pending_waker_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Receiver`] holding an initial value that will never change.
|
||||
pub fn constant(value: T) -> Self {
|
||||
let state = Arc::new(RwLock::new(State {
|
||||
value,
|
||||
wakers: BTreeMap::new(),
|
||||
next_waker_id: WakerId::default(),
|
||||
version: 0,
|
||||
closed: false,
|
||||
}));
|
||||
|
||||
Self { state, version: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Receiver<T> {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use gpui::{EventEmitter, FocusHandle, Focusable};
|
||||
use ui::{
|
||||
|
@ -12,7 +12,7 @@ use crate::Item;
|
|||
/// A view to display when a certain buffer fails to open.
|
||||
pub struct InvalidBufferView {
|
||||
/// Which path was attempted to open.
|
||||
pub abs_path: Arc<PathBuf>,
|
||||
pub abs_path: Arc<Path>,
|
||||
/// An error message, happened when opening the buffer.
|
||||
pub error: SharedString,
|
||||
is_local: bool,
|
||||
|
@ -21,7 +21,7 @@ pub struct InvalidBufferView {
|
|||
|
||||
impl InvalidBufferView {
|
||||
pub fn new(
|
||||
abs_path: PathBuf,
|
||||
abs_path: &Path,
|
||||
is_local: bool,
|
||||
e: &anyhow::Error,
|
||||
_: &mut Window,
|
||||
|
@ -29,7 +29,7 @@ impl InvalidBufferView {
|
|||
) -> Self {
|
||||
Self {
|
||||
is_local,
|
||||
abs_path: Arc::new(abs_path),
|
||||
abs_path: Arc::from(abs_path),
|
||||
error: format!("{e}").into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ impl Item for InvalidBufferView {
|
|||
// Ensure we always render at least the filename.
|
||||
detail += 1;
|
||||
|
||||
let path = self.abs_path.as_path();
|
||||
let path = self.abs_path.as_ref();
|
||||
|
||||
let mut prefix = path;
|
||||
while detail > 0 {
|
||||
|
|
|
@ -23,7 +23,7 @@ use std::{
|
|||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
|
@ -1168,7 +1168,7 @@ pub trait ProjectItem: Item {
|
|||
/// with the error from that failure as an argument.
|
||||
/// Allows to open an item that can gracefully display and handle errors.
|
||||
fn for_broken_project_item(
|
||||
_abs_path: PathBuf,
|
||||
_abs_path: &Path,
|
||||
_is_local: bool,
|
||||
_e: &anyhow::Error,
|
||||
_window: &mut Window,
|
||||
|
|
|
@ -613,48 +613,59 @@ impl ProjectItemRegistry {
|
|||
self.build_project_item_for_path_fns
|
||||
.push(|project, project_path, window, cx| {
|
||||
let project_path = project_path.clone();
|
||||
let abs_path = project.read(cx).absolute_path(&project_path, cx);
|
||||
let is_file = project
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)
|
||||
.is_some_and(|entry| entry.is_file());
|
||||
let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
|
||||
let is_local = project.read(cx).is_local();
|
||||
let project_item =
|
||||
<T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
|
||||
let project = project.clone();
|
||||
Some(window.spawn(cx, async move |cx| match project_item.await {
|
||||
Ok(project_item) => {
|
||||
let project_item = project_item;
|
||||
let project_entry_id: Option<ProjectEntryId> =
|
||||
project_item.read_with(cx, project::ProjectItem::entry_id)?;
|
||||
let build_workspace_item = Box::new(
|
||||
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
|
||||
Box::new(cx.new(|cx| {
|
||||
T::for_project_item(
|
||||
project,
|
||||
Some(pane),
|
||||
project_item,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})) as Box<dyn ItemHandle>
|
||||
},
|
||||
) as Box<_>;
|
||||
Ok((project_entry_id, build_workspace_item))
|
||||
}
|
||||
Err(e) => match abs_path {
|
||||
Some(abs_path) => match cx.update(|window, cx| {
|
||||
T::for_broken_project_item(abs_path, is_local, &e, window, cx)
|
||||
})? {
|
||||
Some(broken_project_item_view) => {
|
||||
let build_workspace_item = Box::new(
|
||||
Some(window.spawn(cx, async move |cx| {
|
||||
match project_item.await.with_context(|| {
|
||||
format!(
|
||||
"opening project path {:?}",
|
||||
entry_abs_path.as_deref().unwrap_or(&project_path.path)
|
||||
)
|
||||
}) {
|
||||
Ok(project_item) => {
|
||||
let project_item = project_item;
|
||||
let project_entry_id: Option<ProjectEntryId> =
|
||||
project_item.read_with(cx, project::ProjectItem::entry_id)?;
|
||||
let build_workspace_item = Box::new(
|
||||
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
|
||||
Box::new(cx.new(|cx| {
|
||||
T::for_project_item(
|
||||
project,
|
||||
Some(pane),
|
||||
project_item,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})) as Box<dyn ItemHandle>
|
||||
},
|
||||
) as Box<_>;
|
||||
Ok((project_entry_id, build_workspace_item))
|
||||
}
|
||||
Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) {
|
||||
Some(abs_path) => match cx.update(|window, cx| {
|
||||
T::for_broken_project_item(abs_path, is_local, &e, window, cx)
|
||||
})? {
|
||||
Some(broken_project_item_view) => {
|
||||
let build_workspace_item = Box::new(
|
||||
move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
|
||||
cx.new(|_| broken_project_item_view).boxed_clone()
|
||||
},
|
||||
)
|
||||
as Box<_>;
|
||||
Ok((None, build_workspace_item))
|
||||
}
|
||||
Ok((None, build_workspace_item))
|
||||
}
|
||||
None => Err(e)?,
|
||||
},
|
||||
None => Err(e)?,
|
||||
},
|
||||
None => Err(e)?,
|
||||
},
|
||||
}
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
@ -4011,52 +4022,6 @@ impl Workspace {
|
|||
maybe_pane_handle
|
||||
}
|
||||
|
||||
pub fn split_pane_with_item(
|
||||
&mut self,
|
||||
pane_to_split: WeakEntity<Pane>,
|
||||
split_direction: SplitDirection,
|
||||
from: WeakEntity<Pane>,
|
||||
item_id_to_move: EntityId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(pane_to_split) = pane_to_split.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(from) = from.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_pane = self.add_pane(window, cx);
|
||||
move_item(&from, &new_pane, item_id_to_move, 0, true, window, cx);
|
||||
self.center
|
||||
.split(&pane_to_split, &new_pane, split_direction)
|
||||
.unwrap();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn split_pane_with_project_entry(
|
||||
&mut self,
|
||||
pane_to_split: WeakEntity<Pane>,
|
||||
split_direction: SplitDirection,
|
||||
project_entry: ProjectEntryId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let pane_to_split = pane_to_split.upgrade()?;
|
||||
let new_pane = self.add_pane(window, cx);
|
||||
self.center
|
||||
.split(&pane_to_split, &new_pane, split_direction)
|
||||
.unwrap();
|
||||
|
||||
let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
|
||||
let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let active_item = self.active_pane.read(cx).active_item();
|
||||
for pane in &self.panes {
|
||||
|
@ -6622,15 +6587,29 @@ impl Render for Workspace {
|
|||
}
|
||||
})
|
||||
.children(self.zoomed.as_ref().and_then(|view| {
|
||||
Some(div()
|
||||
let zoomed_view = view.upgrade()?;
|
||||
let div = div()
|
||||
.occlude()
|
||||
.absolute()
|
||||
.overflow_hidden()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.background)
|
||||
.child(view.upgrade()?)
|
||||
.child(zoomed_view)
|
||||
.inset_0()
|
||||
.shadow_lg())
|
||||
.shadow_lg();
|
||||
|
||||
if !WorkspaceSettings::get_global(cx).zoomed_padding {
|
||||
return Some(div);
|
||||
}
|
||||
|
||||
Some(match self.zoomed_position {
|
||||
Some(DockPosition::Left) => div.right_2().border_r_1(),
|
||||
Some(DockPosition::Right) => div.left_2().border_l_1(),
|
||||
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
|
||||
None => {
|
||||
div.top_2().bottom_2().left_2().right_2().border_1()
|
||||
}
|
||||
})
|
||||
}))
|
||||
.children(self.render_notifications(window, cx)),
|
||||
)
|
||||
|
|
|
@ -29,6 +29,7 @@ pub struct WorkspaceSettings {
|
|||
pub on_last_window_closed: OnLastWindowClosed,
|
||||
pub resize_all_panels_in_dock: Vec<DockPosition>,
|
||||
pub close_on_file_delete: bool,
|
||||
pub zoomed_padding: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
|
@ -202,6 +203,12 @@ pub struct WorkspaceSettingsContent {
|
|||
///
|
||||
/// Default: false
|
||||
pub close_on_file_delete: Option<bool>,
|
||||
/// Whether to show padding for zoomed panels.
|
||||
/// When enabled, zoomed bottom panels will have some top padding,
|
||||
/// while zoomed left/right panels will have padding to the right/left (respectively).
|
||||
///
|
||||
/// Default: true
|
||||
pub zoomed_padding: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
4
crates/zed/resources/info/SupportedPlatforms.plist
Normal file
4
crates/zed/resources/info/SupportedPlatforms.plist
Normal file
|
@ -0,0 +1,4 @@
|
|||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
|
@ -4434,7 +4434,6 @@ mod tests {
|
|||
assert_eq!(actions_without_namespace, Vec::<&str>::new());
|
||||
|
||||
let expected_namespaces = vec![
|
||||
"acp",
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
|
|
@ -290,7 +290,9 @@ pub mod agent {
|
|||
Chat,
|
||||
/// Toggles the language model selector dropdown.
|
||||
#[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
|
||||
ToggleModelSelector
|
||||
ToggleModelSelector,
|
||||
/// Triggers re-authentication on Gemini
|
||||
ReauthenticateAgent
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue