mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
acp: Support model selection for ACP agents (#38652)
It requires the agent to implement the (still unstable) model selection API. Will allow us to test it out before stabilizing. Release Notes: - N/A
This commit is contained in:
parent
dccbb47fbc
commit
4e6e424fd7
13 changed files with 391 additions and 190 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -195,9 +195,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol"
|
name = "agent-client-protocol"
|
||||||
version = "0.4.0"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc2526e80463b9742afed4829aedd6ae5632d6db778c6cc1fecb80c960c3521b"
|
checksum = "00e33b9f4bd34d342b6f80b7156d3a37a04aeec16313f264001e52d6a9118600"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
|
|
@ -4932,7 +4932,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.0",
|
"redox_users 0.5.0",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -12677,6 +12677,7 @@ dependencies = [
|
||||||
"schemars 1.0.1",
|
"schemars 1.0.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
"workspace",
|
"workspace",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
|
|
@ -20853,7 +20854,7 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.0",
|
||||||
"winnow",
|
"winnow",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
"zvariant",
|
"zvariant",
|
||||||
|
|
|
||||||
|
|
@ -439,7 +439,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||||
# External crates
|
# External crates
|
||||||
#
|
#
|
||||||
|
|
||||||
agent-client-protocol = { version = "0.4.0", features = ["unstable"] }
|
agent-client-protocol = { version = "0.4.2", features = ["unstable"] }
|
||||||
aho-corasick = "1.1"
|
aho-corasick = "1.1"
|
||||||
alacritty_terminal = "0.25.1-rc1"
|
alacritty_terminal = "0.25.1-rc1"
|
||||||
any_vec = "0.14"
|
any_vec = "0.14"
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ pub trait AgentConnection {
|
||||||
///
|
///
|
||||||
/// If the agent does not support model selection, returns [None].
|
/// If the agent does not support model selection, returns [None].
|
||||||
/// This allows sharing the selector in UI components.
|
/// This allows sharing the selector in UI components.
|
||||||
fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
|
fn model_selector(&self, _session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,61 +177,48 @@ pub trait AgentModelSelector: 'static {
|
||||||
/// If the session doesn't exist or the model is invalid, it returns an error.
|
/// If the session doesn't exist or the model is invalid, it returns an error.
|
||||||
///
|
///
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
/// - `session_id`: The ID of the session (thread) to apply the model to.
|
|
||||||
/// - `model`: The model to select (should be one from [list_models]).
|
/// - `model`: The model to select (should be one from [list_models]).
|
||||||
/// - `cx`: The GPUI app context.
|
/// - `cx`: The GPUI app context.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A task resolving to `Ok(())` on success or an error.
|
/// A task resolving to `Ok(())` on success or an error.
|
||||||
fn select_model(
|
fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>>;
|
||||||
&self,
|
|
||||||
session_id: acp::SessionId,
|
|
||||||
model_id: AgentModelId,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Task<Result<()>>;
|
|
||||||
|
|
||||||
/// Retrieves the currently selected model for a specific session (thread).
|
/// Retrieves the currently selected model for a specific session (thread).
|
||||||
///
|
///
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
/// - `session_id`: The ID of the session (thread) to query.
|
|
||||||
/// - `cx`: The GPUI app context.
|
/// - `cx`: The GPUI app context.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A task resolving to the selected model (always set) or an error (e.g., session not found).
|
/// A task resolving to the selected model (always set) or an error (e.g., session not found).
|
||||||
fn selected_model(
|
fn selected_model(&self, cx: &mut App) -> Task<Result<AgentModelInfo>>;
|
||||||
&self,
|
|
||||||
session_id: &acp::SessionId,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Task<Result<AgentModelInfo>>;
|
|
||||||
|
|
||||||
/// Whenever the model list is updated the receiver will be notified.
|
/// Whenever the model list is updated the receiver will be notified.
|
||||||
fn watch(&self, cx: &mut App) -> watch::Receiver<()>;
|
/// Optional for agents that don't update their model list.
|
||||||
}
|
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||||
|
None
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct AgentModelId(pub SharedString);
|
|
||||||
|
|
||||||
impl std::ops::Deref for AgentModelId {
|
|
||||||
type Target = SharedString;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for AgentModelId {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct AgentModelInfo {
|
pub struct AgentModelInfo {
|
||||||
pub id: AgentModelId,
|
pub id: acp::ModelId,
|
||||||
pub name: SharedString,
|
pub name: SharedString,
|
||||||
|
pub description: Option<SharedString>,
|
||||||
pub icon: Option<IconName>,
|
pub icon: Option<IconName>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<acp::ModelInfo> for AgentModelInfo {
|
||||||
|
fn from(info: acp::ModelInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
id: info.model_id,
|
||||||
|
name: info.name.into(),
|
||||||
|
description: info.description.map(|desc| desc.into()),
|
||||||
|
icon: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct AgentModelGroupName(pub SharedString);
|
pub struct AgentModelGroupName(pub SharedString);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ struct Session {
|
||||||
|
|
||||||
pub struct LanguageModels {
|
pub struct LanguageModels {
|
||||||
/// Access language model by ID
|
/// Access language model by ID
|
||||||
models: HashMap<acp_thread::AgentModelId, Arc<dyn LanguageModel>>,
|
models: HashMap<acp::ModelId, Arc<dyn LanguageModel>>,
|
||||||
/// Cached list for returning language model information
|
/// Cached list for returning language model information
|
||||||
model_list: acp_thread::AgentModelList,
|
model_list: acp_thread::AgentModelList,
|
||||||
refresh_models_rx: watch::Receiver<()>,
|
refresh_models_rx: watch::Receiver<()>,
|
||||||
|
|
@ -132,10 +132,7 @@ impl LanguageModels {
|
||||||
self.refresh_models_rx.clone()
|
self.refresh_models_rx.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn model_from_id(
|
pub fn model_from_id(&self, model_id: &acp::ModelId) -> Option<Arc<dyn LanguageModel>> {
|
||||||
&self,
|
|
||||||
model_id: &acp_thread::AgentModelId,
|
|
||||||
) -> Option<Arc<dyn LanguageModel>> {
|
|
||||||
self.models.get(model_id).cloned()
|
self.models.get(model_id).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,12 +143,13 @@ impl LanguageModels {
|
||||||
acp_thread::AgentModelInfo {
|
acp_thread::AgentModelInfo {
|
||||||
id: Self::model_id(model),
|
id: Self::model_id(model),
|
||||||
name: model.name().0,
|
name: model.name().0,
|
||||||
|
description: None,
|
||||||
icon: Some(provider.icon()),
|
icon: Some(provider.icon()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_id(model: &Arc<dyn LanguageModel>) -> acp_thread::AgentModelId {
|
fn model_id(model: &Arc<dyn LanguageModel>) -> acp::ModelId {
|
||||||
acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
|
acp::ModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
|
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
|
||||||
|
|
@ -836,10 +834,15 @@ impl NativeAgentConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentModelSelector for NativeAgentConnection {
|
struct NativeAgentModelSelector {
|
||||||
|
session_id: acp::SessionId,
|
||||||
|
connection: NativeAgentConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
||||||
fn list_models(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
|
fn list_models(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
|
||||||
log::debug!("NativeAgentConnection::list_models called");
|
log::debug!("NativeAgentConnection::list_models called");
|
||||||
let list = self.0.read(cx).models.model_list.clone();
|
let list = self.connection.0.read(cx).models.model_list.clone();
|
||||||
Task::ready(if list.is_empty() {
|
Task::ready(if list.is_empty() {
|
||||||
Err(anyhow::anyhow!("No models available"))
|
Err(anyhow::anyhow!("No models available"))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -847,24 +850,24 @@ impl AgentModelSelector for NativeAgentConnection {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_model(
|
fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>> {
|
||||||
&self,
|
log::debug!(
|
||||||
session_id: acp::SessionId,
|
"Setting model for session {}: {}",
|
||||||
model_id: acp_thread::AgentModelId,
|
self.session_id,
|
||||||
cx: &mut App,
|
model_id
|
||||||
) -> Task<Result<()>> {
|
);
|
||||||
log::debug!("Setting model for session {}: {}", session_id, model_id);
|
|
||||||
let Some(thread) = self
|
let Some(thread) = self
|
||||||
|
.connection
|
||||||
.0
|
.0
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.sessions
|
.sessions
|
||||||
.get(&session_id)
|
.get(&self.session_id)
|
||||||
.map(|session| session.thread.clone())
|
.map(|session| session.thread.clone())
|
||||||
else {
|
else {
|
||||||
return Task::ready(Err(anyhow!("Session not found")));
|
return Task::ready(Err(anyhow!("Session not found")));
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(model) = self.0.read(cx).models.model_from_id(&model_id) else {
|
let Some(model) = self.connection.0.read(cx).models.model_from_id(&model_id) else {
|
||||||
return Task::ready(Err(anyhow!("Invalid model ID {}", model_id)));
|
return Task::ready(Err(anyhow!("Invalid model ID {}", model_id)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -872,33 +875,32 @@ impl AgentModelSelector for NativeAgentConnection {
|
||||||
thread.set_model(model.clone(), cx);
|
thread.set_model(model.clone(), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
update_settings_file(self.0.read(cx).fs.clone(), cx, move |settings, _cx| {
|
update_settings_file(
|
||||||
let provider = model.provider_id().0.to_string();
|
self.connection.0.read(cx).fs.clone(),
|
||||||
let model = model.id().0.to_string();
|
cx,
|
||||||
settings
|
move |settings, _cx| {
|
||||||
.agent
|
let provider = model.provider_id().0.to_string();
|
||||||
.get_or_insert_default()
|
let model = model.id().0.to_string();
|
||||||
.set_model(LanguageModelSelection {
|
settings
|
||||||
provider: provider.into(),
|
.agent
|
||||||
model,
|
.get_or_insert_default()
|
||||||
});
|
.set_model(LanguageModelSelection {
|
||||||
});
|
provider: provider.into(),
|
||||||
|
model,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selected_model(
|
fn selected_model(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelInfo>> {
|
||||||
&self,
|
|
||||||
session_id: &acp::SessionId,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Task<Result<acp_thread::AgentModelInfo>> {
|
|
||||||
let session_id = session_id.clone();
|
|
||||||
|
|
||||||
let Some(thread) = self
|
let Some(thread) = self
|
||||||
|
.connection
|
||||||
.0
|
.0
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.sessions
|
.sessions
|
||||||
.get(&session_id)
|
.get(&self.session_id)
|
||||||
.map(|session| session.thread.clone())
|
.map(|session| session.thread.clone())
|
||||||
else {
|
else {
|
||||||
return Task::ready(Err(anyhow!("Session not found")));
|
return Task::ready(Err(anyhow!("Session not found")));
|
||||||
|
|
@ -915,8 +917,8 @@ impl AgentModelSelector for NativeAgentConnection {
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn watch(&self, cx: &mut App) -> watch::Receiver<()> {
|
fn watch(&self, cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||||
self.0.read(cx).models.watch()
|
Some(self.connection.0.read(cx).models.watch())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -972,8 +974,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
|
fn model_selector(&self, session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> {
|
||||||
Some(Rc::new(self.clone()) as Rc<dyn AgentModelSelector>)
|
Some(Rc::new(NativeAgentModelSelector {
|
||||||
|
session_id: session_id.clone(),
|
||||||
|
connection: self.clone(),
|
||||||
|
}) as Rc<dyn AgentModelSelector>)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(
|
fn prompt(
|
||||||
|
|
@ -1196,9 +1201,7 @@ mod tests {
|
||||||
use crate::HistoryEntryId;
|
use crate::HistoryEntryId;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use acp_thread::{
|
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri};
|
||||||
AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo, MentionUri,
|
|
||||||
};
|
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
@ -1292,7 +1295,25 @@ mod tests {
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let models = cx.update(|cx| connection.list_models(cx)).await.unwrap();
|
// Create a thread/session
|
||||||
|
let acp_thread = cx
|
||||||
|
.update(|cx| {
|
||||||
|
Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone());
|
||||||
|
|
||||||
|
let models = cx
|
||||||
|
.update(|cx| {
|
||||||
|
connection
|
||||||
|
.model_selector(&session_id)
|
||||||
|
.unwrap()
|
||||||
|
.list_models(cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let acp_thread::AgentModelList::Grouped(models) = models else {
|
let acp_thread::AgentModelList::Grouped(models) = models else {
|
||||||
panic!("Unexpected model group");
|
panic!("Unexpected model group");
|
||||||
|
|
@ -1302,8 +1323,9 @@ mod tests {
|
||||||
IndexMap::from_iter([(
|
IndexMap::from_iter([(
|
||||||
AgentModelGroupName("Fake".into()),
|
AgentModelGroupName("Fake".into()),
|
||||||
vec![AgentModelInfo {
|
vec![AgentModelInfo {
|
||||||
id: AgentModelId("fake/fake".into()),
|
id: acp::ModelId("fake/fake".into()),
|
||||||
name: "Fake".into(),
|
name: "Fake".into(),
|
||||||
|
description: None,
|
||||||
icon: Some(ui::IconName::ZedAssistant),
|
icon: Some(ui::IconName::ZedAssistant),
|
||||||
}]
|
}]
|
||||||
)])
|
)])
|
||||||
|
|
@ -1360,8 +1382,9 @@ mod tests {
|
||||||
let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone());
|
let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone());
|
||||||
|
|
||||||
// Select a model
|
// Select a model
|
||||||
let model_id = AgentModelId("fake/fake".into());
|
let selector = connection.model_selector(&session_id).unwrap();
|
||||||
cx.update(|cx| connection.select_model(session_id.clone(), model_id.clone(), cx))
|
let model_id = acp::ModelId("fake/fake".into());
|
||||||
|
cx.update(|cx| selector.select_model(model_id.clone(), cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1850,8 +1850,18 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let connection = NativeAgentConnection(agent.clone());
|
let connection = NativeAgentConnection(agent.clone());
|
||||||
|
|
||||||
|
// Create a thread using new_thread
|
||||||
|
let connection_rc = Rc::new(connection.clone());
|
||||||
|
let acp_thread = cx
|
||||||
|
.update(|cx| connection_rc.new_thread(project, cwd, cx))
|
||||||
|
.await
|
||||||
|
.expect("new_thread should succeed");
|
||||||
|
|
||||||
|
// Get the session_id from the AcpThread
|
||||||
|
let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||||
|
|
||||||
// Test model_selector returns Some
|
// Test model_selector returns Some
|
||||||
let selector_opt = connection.model_selector();
|
let selector_opt = connection.model_selector(&session_id);
|
||||||
assert!(
|
assert!(
|
||||||
selector_opt.is_some(),
|
selector_opt.is_some(),
|
||||||
"agent2 should always support ModelSelector"
|
"agent2 should always support ModelSelector"
|
||||||
|
|
@ -1868,23 +1878,16 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||||
};
|
};
|
||||||
assert!(!listed_models.is_empty(), "should have at least one model");
|
assert!(!listed_models.is_empty(), "should have at least one model");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
listed_models[&AgentModelGroupName("Fake".into())][0].id.0,
|
listed_models[&AgentModelGroupName("Fake".into())][0]
|
||||||
|
.id
|
||||||
|
.0
|
||||||
|
.as_ref(),
|
||||||
"fake/fake"
|
"fake/fake"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a thread using new_thread
|
|
||||||
let connection_rc = Rc::new(connection.clone());
|
|
||||||
let acp_thread = cx
|
|
||||||
.update(|cx| connection_rc.new_thread(project, cwd, cx))
|
|
||||||
.await
|
|
||||||
.expect("new_thread should succeed");
|
|
||||||
|
|
||||||
// Get the session_id from the AcpThread
|
|
||||||
let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
|
|
||||||
|
|
||||||
// Test selected_model returns the default
|
// Test selected_model returns the default
|
||||||
let model = cx
|
let model = cx
|
||||||
.update(|cx| selector.selected_model(&session_id, cx))
|
.update(|cx| selector.selected_model(cx))
|
||||||
.await
|
.await
|
||||||
.expect("selected_model should succeed");
|
.expect("selected_model should succeed");
|
||||||
let model = cx
|
let model = cx
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ pub struct AcpConnection {
|
||||||
pub struct AcpSession {
|
pub struct AcpSession {
|
||||||
thread: WeakEntity<AcpThread>,
|
thread: WeakEntity<AcpThread>,
|
||||||
suppress_abort_err: bool,
|
suppress_abort_err: bool,
|
||||||
|
models: Option<Rc<RefCell<acp::SessionModelState>>>,
|
||||||
session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
|
session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,6 +265,7 @@ impl AgentConnection for AcpConnection {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
|
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
|
||||||
|
let models = response.models.map(|models| Rc::new(RefCell::new(models)));
|
||||||
|
|
||||||
if let Some(default_mode) = default_mode {
|
if let Some(default_mode) = default_mode {
|
||||||
if let Some(modes) = modes.as_ref() {
|
if let Some(modes) = modes.as_ref() {
|
||||||
|
|
@ -326,10 +328,12 @@ impl AgentConnection for AcpConnection {
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
||||||
let session = AcpSession {
|
let session = AcpSession {
|
||||||
thread: thread.downgrade(),
|
thread: thread.downgrade(),
|
||||||
suppress_abort_err: false,
|
suppress_abort_err: false,
|
||||||
session_modes: modes
|
session_modes: modes,
|
||||||
|
models,
|
||||||
};
|
};
|
||||||
sessions.borrow_mut().insert(session_id, session);
|
sessions.borrow_mut().insert(session_id, session);
|
||||||
|
|
||||||
|
|
@ -450,6 +454,27 @@ impl AgentConnection for AcpConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn model_selector(
|
||||||
|
&self,
|
||||||
|
session_id: &acp::SessionId,
|
||||||
|
) -> Option<Rc<dyn acp_thread::AgentModelSelector>> {
|
||||||
|
let sessions = self.sessions.clone();
|
||||||
|
let sessions_ref = sessions.borrow();
|
||||||
|
let Some(session) = sessions_ref.get(session_id) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(models) = session.models.as_ref() {
|
||||||
|
Some(Rc::new(AcpModelSelector::new(
|
||||||
|
session_id.clone(),
|
||||||
|
self.connection.clone(),
|
||||||
|
models.clone(),
|
||||||
|
)) as _)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -500,6 +525,82 @@ impl acp_thread::AgentSessionModes for AcpSessionModes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AcpModelSelector {
|
||||||
|
session_id: acp::SessionId,
|
||||||
|
connection: Rc<acp::ClientSideConnection>,
|
||||||
|
state: Rc<RefCell<acp::SessionModelState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcpModelSelector {
|
||||||
|
fn new(
|
||||||
|
session_id: acp::SessionId,
|
||||||
|
connection: Rc<acp::ClientSideConnection>,
|
||||||
|
state: Rc<RefCell<acp::SessionModelState>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
session_id,
|
||||||
|
connection,
|
||||||
|
state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl acp_thread::AgentModelSelector for AcpModelSelector {
|
||||||
|
fn list_models(&self, _cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
|
||||||
|
Task::ready(Ok(acp_thread::AgentModelList::Flat(
|
||||||
|
self.state
|
||||||
|
.borrow()
|
||||||
|
.available_models
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(acp_thread::AgentModelInfo::from)
|
||||||
|
.collect(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>> {
|
||||||
|
let connection = self.connection.clone();
|
||||||
|
let session_id = self.session_id.clone();
|
||||||
|
let old_model_id;
|
||||||
|
{
|
||||||
|
let mut state = self.state.borrow_mut();
|
||||||
|
old_model_id = state.current_model_id.clone();
|
||||||
|
state.current_model_id = model_id.clone();
|
||||||
|
};
|
||||||
|
let state = self.state.clone();
|
||||||
|
cx.foreground_executor().spawn(async move {
|
||||||
|
let result = connection
|
||||||
|
.set_session_model(acp::SetSessionModelRequest {
|
||||||
|
session_id,
|
||||||
|
model_id,
|
||||||
|
meta: None,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if result.is_err() {
|
||||||
|
state.borrow_mut().current_model_id = old_model_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
result?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_model(&self, _cx: &mut App) -> Task<Result<acp_thread::AgentModelInfo>> {
|
||||||
|
let state = self.state.borrow();
|
||||||
|
Task::ready(
|
||||||
|
state
|
||||||
|
.available_models
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.model_id == state.current_model_id)
|
||||||
|
.cloned()
|
||||||
|
.map(acp_thread::AgentModelInfo::from)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Model not found")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ClientDelegate {
|
struct ClientDelegate {
|
||||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||||
cx: AsyncApp,
|
cx: AsyncApp,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||||
use agent_client_protocol as acp;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::IndexMap;
|
use collections::IndexMap;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
|
|
@ -10,20 +9,19 @@ use gpui::{Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, W
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use ui::{
|
use ui::{
|
||||||
AnyElement, App, Context, IntoElement, ListItem, ListItemSpacing, SharedString, Window,
|
AnyElement, App, Context, DocumentationAside, DocumentationEdge, DocumentationSide,
|
||||||
prelude::*, rems,
|
IntoElement, ListItem, ListItemSpacing, SharedString, Window, prelude::*, rems,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
|
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
|
||||||
|
|
||||||
pub fn acp_model_selector(
|
pub fn acp_model_selector(
|
||||||
session_id: acp::SessionId,
|
|
||||||
selector: Rc<dyn AgentModelSelector>,
|
selector: Rc<dyn AgentModelSelector>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<AcpModelSelector>,
|
cx: &mut Context<AcpModelSelector>,
|
||||||
) -> AcpModelSelector {
|
) -> AcpModelSelector {
|
||||||
let delegate = AcpModelPickerDelegate::new(session_id, selector, window, cx);
|
let delegate = AcpModelPickerDelegate::new(selector, window, cx);
|
||||||
Picker::list(delegate, window, cx)
|
Picker::list(delegate, window, cx)
|
||||||
.show_scrollbar(true)
|
.show_scrollbar(true)
|
||||||
.width(rems(20.))
|
.width(rems(20.))
|
||||||
|
|
@ -36,61 +34,63 @@ enum AcpModelPickerEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AcpModelPickerDelegate {
|
pub struct AcpModelPickerDelegate {
|
||||||
session_id: acp::SessionId,
|
|
||||||
selector: Rc<dyn AgentModelSelector>,
|
selector: Rc<dyn AgentModelSelector>,
|
||||||
filtered_entries: Vec<AcpModelPickerEntry>,
|
filtered_entries: Vec<AcpModelPickerEntry>,
|
||||||
models: Option<AgentModelList>,
|
models: Option<AgentModelList>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
|
selected_description: Option<(usize, SharedString)>,
|
||||||
selected_model: Option<AgentModelInfo>,
|
selected_model: Option<AgentModelInfo>,
|
||||||
_refresh_models_task: Task<()>,
|
_refresh_models_task: Task<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AcpModelPickerDelegate {
|
impl AcpModelPickerDelegate {
|
||||||
fn new(
|
fn new(
|
||||||
session_id: acp::SessionId,
|
|
||||||
selector: Rc<dyn AgentModelSelector>,
|
selector: Rc<dyn AgentModelSelector>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<AcpModelSelector>,
|
cx: &mut Context<AcpModelSelector>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut rx = selector.watch(cx);
|
let rx = selector.watch(cx);
|
||||||
let refresh_models_task = cx.spawn_in(window, {
|
let refresh_models_task = {
|
||||||
let session_id = session_id.clone();
|
cx.spawn_in(window, {
|
||||||
async move |this, cx| {
|
async move |this, cx| {
|
||||||
async fn refresh(
|
async fn refresh(
|
||||||
this: &WeakEntity<Picker<AcpModelPickerDelegate>>,
|
this: &WeakEntity<Picker<AcpModelPickerDelegate>>,
|
||||||
session_id: &acp::SessionId,
|
cx: &mut AsyncWindowContext,
|
||||||
cx: &mut AsyncWindowContext,
|
) -> Result<()> {
|
||||||
) -> Result<()> {
|
let (models_task, selected_model_task) = this.update(cx, |this, cx| {
|
||||||
let (models_task, selected_model_task) = this.update(cx, |this, cx| {
|
(
|
||||||
(
|
this.delegate.selector.list_models(cx),
|
||||||
this.delegate.selector.list_models(cx),
|
this.delegate.selector.selected_model(cx),
|
||||||
this.delegate.selector.selected_model(session_id, cx),
|
)
|
||||||
)
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
let (models, selected_model) = futures::join!(models_task, selected_model_task);
|
let (models, selected_model) =
|
||||||
|
futures::join!(models_task, selected_model_task);
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.delegate.models = models.ok();
|
this.delegate.models = models.ok();
|
||||||
this.delegate.selected_model = selected_model.ok();
|
this.delegate.selected_model = selected_model.ok();
|
||||||
this.refresh(window, cx)
|
this.refresh(window, cx)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(&this, cx).await.log_err();
|
||||||
|
if let Some(mut rx) = rx {
|
||||||
|
while let Ok(()) = rx.recv().await {
|
||||||
|
refresh(&this, cx).await.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
refresh(&this, &session_id, cx).await.log_err();
|
};
|
||||||
while let Ok(()) = rx.recv().await {
|
|
||||||
refresh(&this, &session_id, cx).await.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
session_id,
|
|
||||||
selector,
|
selector,
|
||||||
filtered_entries: Vec::new(),
|
filtered_entries: Vec::new(),
|
||||||
models: None,
|
models: None,
|
||||||
selected_model: None,
|
selected_model: None,
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
|
selected_description: None,
|
||||||
_refresh_models_task: refresh_models_task,
|
_refresh_models_task: refresh_models_task,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +182,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||||
self.filtered_entries.get(self.selected_index)
|
self.filtered_entries.get(self.selected_index)
|
||||||
{
|
{
|
||||||
self.selector
|
self.selector
|
||||||
.select_model(self.session_id.clone(), model_info.id.clone(), cx)
|
.select_model(model_info.id.clone(), cx)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
self.selected_model = Some(model_info.clone());
|
self.selected_model = Some(model_info.clone());
|
||||||
let current_index = self.selected_index;
|
let current_index = self.selected_index;
|
||||||
|
|
@ -233,31 +233,46 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(ix)
|
div()
|
||||||
.inset(true)
|
.id(("model-picker-menu-child", ix))
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.when_some(model_info.description.clone(), |this, description| {
|
||||||
.toggle_state(selected)
|
this
|
||||||
.start_slot::<Icon>(model_info.icon.map(|icon| {
|
.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||||
Icon::new(icon)
|
if *hovered {
|
||||||
.color(model_icon_color)
|
menu.delegate.selected_description = Some((ix, description.clone()));
|
||||||
.size(IconSize::Small)
|
} else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix) {
|
||||||
}))
|
menu.delegate.selected_description = None;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}))
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
ListItem::new(ix)
|
||||||
.w_full()
|
.inset(true)
|
||||||
.pl_0p5()
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.gap_1p5()
|
.toggle_state(selected)
|
||||||
.w(px(240.))
|
.start_slot::<Icon>(model_info.icon.map(|icon| {
|
||||||
.child(Label::new(model_info.name.clone()).truncate()),
|
Icon::new(icon)
|
||||||
|
.color(model_icon_color)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.pl_0p5()
|
||||||
|
.gap_1p5()
|
||||||
|
.w(px(240.))
|
||||||
|
.child(Label::new(model_info.name.clone()).truncate()),
|
||||||
|
)
|
||||||
|
.end_slot(div().pr_3().when(is_selected, |this| {
|
||||||
|
this.child(
|
||||||
|
Icon::new(IconName::Check)
|
||||||
|
.color(Color::Accent)
|
||||||
|
.size(IconSize::Small),
|
||||||
|
)
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
.into_any_element()
|
||||||
this.child(
|
|
||||||
Icon::new(IconName::Check)
|
|
||||||
.color(Color::Accent)
|
|
||||||
.size(IconSize::Small),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
.into_any_element(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -292,6 +307,21 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||||
.into_any(),
|
.into_any(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn documentation_aside(
|
||||||
|
&self,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<ui::DocumentationAside> {
|
||||||
|
self.selected_description.as_ref().map(|(_, description)| {
|
||||||
|
let description = description.clone();
|
||||||
|
DocumentationAside::new(
|
||||||
|
DocumentationSide::Left,
|
||||||
|
DocumentationEdge::Bottom,
|
||||||
|
Rc::new(move |_| Label::new(description.clone()).into_any_element()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info_list_to_picker_entries(
|
fn info_list_to_picker_entries(
|
||||||
|
|
@ -371,6 +401,7 @@ async fn fuzzy_search(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use agent_client_protocol as acp;
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -383,8 +414,9 @@ mod tests {
|
||||||
models
|
models
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|model| acp_thread::AgentModelInfo {
|
.map(|model| acp_thread::AgentModelInfo {
|
||||||
id: acp_thread::AgentModelId(model.to_string().into()),
|
id: acp::ModelId(model.to_string().into()),
|
||||||
name: model.to_string().into(),
|
name: model.to_string().into(),
|
||||||
|
description: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use acp_thread::AgentModelSelector;
|
use acp_thread::AgentModelSelector;
|
||||||
use agent_client_protocol as acp;
|
|
||||||
use gpui::{Entity, FocusHandle};
|
use gpui::{Entity, FocusHandle};
|
||||||
use picker::popover_menu::PickerPopoverMenu;
|
use picker::popover_menu::PickerPopoverMenu;
|
||||||
use ui::{
|
use ui::{
|
||||||
|
|
@ -20,7 +19,6 @@ pub struct AcpModelSelectorPopover {
|
||||||
|
|
||||||
impl AcpModelSelectorPopover {
|
impl AcpModelSelectorPopover {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
session_id: acp::SessionId,
|
|
||||||
selector: Rc<dyn AgentModelSelector>,
|
selector: Rc<dyn AgentModelSelector>,
|
||||||
menu_handle: PopoverMenuHandle<AcpModelSelector>,
|
menu_handle: PopoverMenuHandle<AcpModelSelector>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
|
|
@ -28,7 +26,7 @@ impl AcpModelSelectorPopover {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
selector: cx.new(move |cx| acp_model_selector(session_id, selector, window, cx)),
|
selector: cx.new(move |cx| acp_model_selector(selector, window, cx)),
|
||||||
menu_handle,
|
menu_handle,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -577,23 +577,21 @@ impl AcpThreadView {
|
||||||
|
|
||||||
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||||
|
|
||||||
this.model_selector =
|
this.model_selector = thread
|
||||||
thread
|
.read(cx)
|
||||||
.read(cx)
|
.connection()
|
||||||
.connection()
|
.model_selector(thread.read(cx).session_id())
|
||||||
.model_selector()
|
.map(|selector| {
|
||||||
.map(|selector| {
|
cx.new(|cx| {
|
||||||
cx.new(|cx| {
|
AcpModelSelectorPopover::new(
|
||||||
AcpModelSelectorPopover::new(
|
selector,
|
||||||
thread.read(cx).session_id().clone(),
|
PopoverMenuHandle::default(),
|
||||||
selector,
|
this.focus_handle(cx),
|
||||||
PopoverMenuHandle::default(),
|
window,
|
||||||
this.focus_handle(cx),
|
cx,
|
||||||
window,
|
)
|
||||||
cx,
|
})
|
||||||
)
|
});
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let mode_selector = thread
|
let mode_selector = thread
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ gpui.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,12 @@ use head::Head;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{ops::Range, sync::Arc, time::Duration};
|
use std::{ops::Range, sync::Arc, time::Duration};
|
||||||
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
Color, Divider, Label, ListItem, ListItemSpacing, ScrollAxes, Scrollbars, WithScrollbar,
|
Color, Divider, DocumentationAside, DocumentationEdge, DocumentationSide, Label, ListItem,
|
||||||
prelude::*, v_flex,
|
ListItemSpacing, ScrollAxes, Scrollbars, WithScrollbar, prelude::*, utils::WithRemSize, v_flex,
|
||||||
};
|
};
|
||||||
use workspace::ModalView;
|
use workspace::{ModalView, item::Settings};
|
||||||
|
|
||||||
enum ElementContainer {
|
enum ElementContainer {
|
||||||
List(ListState),
|
List(ListState),
|
||||||
|
|
@ -222,6 +223,14 @@ pub trait PickerDelegate: Sized + 'static {
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn documentation_aside(
|
||||||
|
&self,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<DocumentationAside> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: PickerDelegate> Focusable for Picker<D> {
|
impl<D: PickerDelegate> Focusable for Picker<D> {
|
||||||
|
|
@ -781,8 +790,15 @@ impl<D: PickerDelegate> ModalView for Picker<D> {}
|
||||||
|
|
||||||
impl<D: PickerDelegate> Render for Picker<D> {
|
impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
|
||||||
|
let window_size = window.viewport_size();
|
||||||
|
let rem_size = window.rem_size();
|
||||||
|
let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
|
||||||
|
|
||||||
|
let aside = self.delegate.documentation_aside(window, cx);
|
||||||
|
|
||||||
let editor_position = self.delegate.editor_position();
|
let editor_position = self.delegate.editor_position();
|
||||||
v_flex()
|
let menu = v_flex()
|
||||||
.key_context("Picker")
|
.key_context("Picker")
|
||||||
.size_full()
|
.size_full()
|
||||||
.when_some(self.width, |el, width| el.w(width))
|
.when_some(self.width, |el, width| el.w(width))
|
||||||
|
|
@ -865,6 +881,47 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
|
Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
|
||||||
})
|
});
|
||||||
|
|
||||||
|
let Some(aside) = aside else {
|
||||||
|
return menu;
|
||||||
|
};
|
||||||
|
|
||||||
|
let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
|
||||||
|
WithRemSize::new(ui_font_size)
|
||||||
|
.occlude()
|
||||||
|
.elevation_2(cx)
|
||||||
|
.w_full()
|
||||||
|
.p_2()
|
||||||
|
.overflow_hidden()
|
||||||
|
.when(is_wide_window, |this| this.max_w_96())
|
||||||
|
.when(!is_wide_window, |this| this.max_w_48())
|
||||||
|
.child((aside.render)(cx))
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_wide_window {
|
||||||
|
div().relative().child(menu).child(
|
||||||
|
h_flex()
|
||||||
|
.absolute()
|
||||||
|
.when(aside.side == DocumentationSide::Left, |this| {
|
||||||
|
this.right_full().mr_1()
|
||||||
|
})
|
||||||
|
.when(aside.side == DocumentationSide::Right, |this| {
|
||||||
|
this.left_full().ml_1()
|
||||||
|
})
|
||||||
|
.when(aside.edge == DocumentationEdge::Top, |this| this.top_0())
|
||||||
|
.when(aside.edge == DocumentationEdge::Bottom, |this| {
|
||||||
|
this.bottom_0()
|
||||||
|
})
|
||||||
|
.child(render_aside(aside, cx)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.gap_1()
|
||||||
|
.justify_end()
|
||||||
|
.child(render_aside(aside, cx))
|
||||||
|
.child(menu)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,9 +180,9 @@ pub enum DocumentationEdge {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DocumentationAside {
|
pub struct DocumentationAside {
|
||||||
side: DocumentationSide,
|
pub side: DocumentationSide,
|
||||||
edge: DocumentationEdge,
|
pub edge: DocumentationEdge,
|
||||||
render: Rc<dyn Fn(&mut App) -> AnyElement>,
|
pub render: Rc<dyn Fn(&mut App) -> AnyElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocumentationAside {
|
impl DocumentationAside {
|
||||||
|
|
|
||||||
|
|
@ -600,10 +600,10 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti
|
||||||
winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] }
|
winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] }
|
||||||
windows-core = { version = "0.61" }
|
windows-core = { version = "0.61" }
|
||||||
windows-numerics = { version = "0.2" }
|
windows-numerics = { version = "0.2" }
|
||||||
windows-sys-4db8c43aad08e7ae = { package = "windows-sys", version = "0.60", features = ["Win32_Globalization", "Win32_System_Com", "Win32_UI_Shell"] }
|
|
||||||
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||||
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
|
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
|
||||||
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
|
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
|
||||||
|
windows-sys-d4189bed749088b6 = { package = "windows-sys", version = "0.61", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
|
||||||
|
|
||||||
[target.x86_64-pc-windows-msvc.build-dependencies]
|
[target.x86_64-pc-windows-msvc.build-dependencies]
|
||||||
codespan-reporting = { version = "0.12" }
|
codespan-reporting = { version = "0.12" }
|
||||||
|
|
@ -627,10 +627,10 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti
|
||||||
winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] }
|
winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] }
|
||||||
windows-core = { version = "0.61" }
|
windows-core = { version = "0.61" }
|
||||||
windows-numerics = { version = "0.2" }
|
windows-numerics = { version = "0.2" }
|
||||||
windows-sys-4db8c43aad08e7ae = { package = "windows-sys", version = "0.60", features = ["Win32_Globalization", "Win32_System_Com", "Win32_UI_Shell"] }
|
|
||||||
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||||
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
|
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
|
||||||
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
|
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
|
||||||
|
windows-sys-d4189bed749088b6 = { package = "windows-sys", version = "0.61", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
|
||||||
|
|
||||||
[target.x86_64-unknown-linux-musl.dependencies]
|
[target.x86_64-unknown-linux-musl.dependencies]
|
||||||
aes = { version = "0.8", default-features = false, features = ["zeroize"] }
|
aes = { version = "0.8", default-features = false, features = ["zeroize"] }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue