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:
Ben Brandt 2025-09-22 17:07:40 +02:00 committed by GitHub
parent dccbb47fbc
commit 4e6e424fd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 391 additions and 190 deletions

9
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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);

View file

@ -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();

View file

@ -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

View file

@ -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,

View file

@ -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<_>>(),

View file

@ -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,
} }

View file

@ -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)

View file

@ -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

View file

@ -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)
}
} }
} }

View file

@ -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 {

View file

@ -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"] }