Update ACP SDK to v0.11
Seto Elkahfi committed
Jun 9, 2026 at 12:43 UTC
c7c34ff30c4e273c8388171f93a36f2622a31999
3 files changed
+676
-587
Cargo.lock
-1
index 234fd0d..412fc76 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5071,7 +5071,6 @@ version = "1.0.4"
dependencies = [
"agent-client-protocol",
"anyhow",
- "async-trait",
"crossterm 0.29.0",
"futures",
"libc",
Cargo.toml
-1
index eab6a39..e1286cb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,7 +25,6 @@ agent-client-protocol = { version = "0.11", features = ["unstable_session_fork",
onde = "1.1.2"
# Async runtime
-async-trait = "0.1"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "io-std", "io-util", "sync", "time"] }
tokio-util = { version = "0.7", features = ["compat"] }
futures = "0.3"
src/main.rs
+676
-585
index a80b2bc..c867911 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,10 +11,6 @@
//! Interactive mode is Unix-only — it needs fd redirection to keep logs out
//! of the TUI. Windows only gets ACP mode for now.
//!
-//! The model loads before the ACP `LocalSet` starts because `mistralrs` calls
-//! `block_in_place`, which panics inside `spawn_local`. Loading on a regular
-//! multi-thread worker sidesteps that.
-//!
//! On macOS the HF cache lives in the App Group container shared with the
//! siGit desktop app. See [`setup`].
//!
@@ -44,22 +40,21 @@ use std::sync::Arc;
use onde::inference::SamplingConfig;
-use agent_client_protocol::{
- Agent, AgentCapabilities, AgentSideConnection, AuthMethod, AuthMethodAgent,
- AuthenticateRequest, AuthenticateResponse, CancelNotification, Client, ContentBlock,
- ContentChunk, ForkSessionRequest, ForkSessionResponse, Implementation, InitializeRequest,
- InitializeResponse, LoadSessionRequest, LoadSessionResponse, Meta, NewSessionRequest,
- NewSessionResponse, PromptRequest, PromptResponse, ProtocolVersion, SessionCapabilities,
- SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption,
- SessionConfigValueId, SessionForkCapabilities, SessionId, SessionNotification, SessionUpdate,
+use agent_client_protocol::{Agent, ByteStreams, Client, ConnectionTo, Responder};
+use agent_client_protocol::schema::{
+ AgentCapabilities, AuthMethod, AuthMethodAgent, AuthenticateRequest, AuthenticateResponse,
+ CancelNotification, ContentBlock, ContentChunk, EmbeddedResourceResource,
+ ForkSessionRequest, ForkSessionResponse, Implementation, InitializeRequest, InitializeResponse,
+ LoadSessionRequest, LoadSessionResponse, Meta, NewSessionRequest, NewSessionResponse,
+ PromptRequest, PromptResponse, ProtocolVersion, SessionCapabilities, SessionConfigOption,
+ SessionConfigOptionCategory, SessionConfigSelectOption, SessionConfigValueId,
+ SessionForkCapabilities, SessionId, SessionNotification, SessionUpdate,
SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, StopReason, ToolCall,
ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind,
};
-use futures::future::LocalBoxFuture;
use onde::inference::{ChatEngine, GgufModelConfig, ToolDefinition, ToolResult};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
-use tokio::sync::mpsc;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
use tracing_subscriber::{EnvFilter, fmt as tracing_fmt};
@@ -267,7 +262,6 @@ fn initialize_meta() -> Meta {
struct SiGitAgent {
engine: Arc<ChatEngine>,
- notification_tx: mpsc::Sender<SessionNotification>,
/// cwd from the editor — tool calls run here, not where the process started
session_cwd: std::sync::Mutex<Option<PathBuf>>,
current_model: std::sync::Mutex<GgufModelConfig>,
@@ -288,7 +282,6 @@ struct SiGitAgent {
impl SiGitAgent {
fn new(
engine: Arc<ChatEngine>,
- notification_tx: mpsc::Sender<SessionNotification>,
initial_model: GgufModelConfig,
model_ready: Arc<AtomicBool>,
startup_model_load_started: Arc<AtomicBool>,
@@ -299,7 +292,6 @@ impl SiGitAgent {
let startup_model_id = initial_model.model_id.clone();
Self {
engine,
- notification_tx,
session_cwd: std::sync::Mutex::new(None),
current_model: std::sync::Mutex::new(initial_model),
model_ready,
@@ -372,7 +364,11 @@ impl SiGitAgent {
}
/// block until the startup model is ready, showing progress in the session.
- async fn await_model_ready(&self, session_id: &SessionId) -> agent_client_protocol::Result<()> {
+ async fn await_model_ready(
+ &self,
+ cx: &ConnectionTo<Client>,
+ session_id: &SessionId,
+ ) -> agent_client_protocol::Result<()> {
if self.model_ready.load(Ordering::Acquire) {
// already done — might be a stored error from earlier
if let Some(err) = self.model_load_error.lock().unwrap().as_ref() {
@@ -394,6 +390,7 @@ impl SiGitAgent {
};
self.send_tool_call_update(
+ cx,
session_id.clone(),
SessionUpdate::ToolCall(
ToolCall::new(tool_call_id.clone(), &title)
@@ -402,7 +399,7 @@ impl SiGitAgent {
.content(vec![format!("{}…", title).into()]),
),
)
- .await;
+ .ok();
let expected_bytes = if self.startup_needs_download {
onde::inference::models::SUPPORTED_MODEL_INFO
@@ -484,6 +481,7 @@ impl SiGitAgent {
};
self.send_tool_call_update(
+ cx,
session_id.clone(),
SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
tool_call_id.clone(),
@@ -493,13 +491,14 @@ impl SiGitAgent {
.content(vec![update_content.into()]),
)),
)
- .await;
+ .ok();
}
// done — check if it blew up
let load_error = self.model_load_error.lock().unwrap().clone();
if let Some(err) = load_error {
self.send_tool_call_update(
+ cx,
session_id.clone(),
SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
tool_call_id,
@@ -509,7 +508,7 @@ impl SiGitAgent {
.content(vec![format!("error: {err}").into()]),
)),
)
- .await;
+ .ok();
return Err(agent_client_protocol::Error::new(
-32603,
@@ -524,6 +523,7 @@ impl SiGitAgent {
};
self.send_tool_call_update(
+ cx,
session_id.clone(),
SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
tool_call_id,
@@ -532,26 +532,30 @@ impl SiGitAgent {
.status(ToolCallStatus::Completed),
)),
)
- .await;
+ .ok();
Ok(())
}
- async fn send_assistant_message(&self, session_id: SessionId, text: impl Into<String>) {
- let notification = SessionNotification::new(
+ fn send_assistant_message(
+ &self,
+ cx: &ConnectionTo<Client>,
+ session_id: SessionId,
+ text: impl Into<String>,
+ ) -> agent_client_protocol::Result<()> {
+ cx.send_notification(SessionNotification::new(
session_id,
SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from(text.into()))),
- );
- if self.notification_tx.send(notification).await.is_err() {
- log::warn!("notification channel closed");
- }
+ ))
}
- async fn send_tool_call_update(&self, session_id: SessionId, update: SessionUpdate) {
- let notification = SessionNotification::new(session_id, update);
- if self.notification_tx.send(notification).await.is_err() {
- log::warn!("notification channel closed");
- }
+ fn send_tool_call_update(
+ &self,
+ cx: &ConnectionTo<Client>,
+ session_id: SessionId,
+ update: SessionUpdate,
+ ) -> agent_client_protocol::Result<()> {
+ cx.send_notification(SessionNotification::new(session_id, update))
}
async fn switch_model_by_id(
@@ -647,439 +651,145 @@ impl SiGitAgent {
}
}
-/// config option ID for the model picker in Zed's agent panel
-const MODEL_CONFIG_ID: &str = "sigit-model";
+// ── ACP handler implementations ───────────────────────────────────────────────
-fn build_model_config_options(current_model: &GgufModelConfig) -> Vec<SessionConfigOption> {
- let items = models::build_model_picker_items();
+impl SiGitAgent {
+ async fn handle_initialize(
+ &self,
+ _req: InitializeRequest,
+ ) -> agent_client_protocol::Result<InitializeResponse> {
+ log::info!("initialize");
- let options: Vec<SessionConfigSelectOption> = items
- .iter()
- .filter(|item| item.cache_health != setup::ModelCacheHealth::Incomplete)
- .map(|item| {
- let mut desc_parts = Vec::new();
- if item.tool_calling {
- desc_parts.push("tool calling".to_string());
- }
- desc_parts.push(item.description.clone());
- if item.cache_health == setup::ModelCacheHealth::NotDownloaded {
- desc_parts.push("↓ download on select".to_string());
- }
- let description = desc_parts.join(" - ");
- let source_badge = if item.cache_health == setup::ModelCacheHealth::NotDownloaded {
- " [↓ Onde]"
- } else {
- match item.source_label.as_str() {
- "Onde" => " [◉ Onde]",
- "HuggingFace" => " [○ HuggingFace]",
- _ => "",
- }
- };
- let name = format!("{}{}", item.display_name, source_badge);
- SessionConfigSelectOption::new(
- SessionConfigValueId::new(item.config.model_id.as_str()),
- name,
+ Ok(InitializeResponse::new(ProtocolVersion::V1)
+ .agent_info(
+ Implementation::new("sigit", env!("CARGO_PKG_VERSION"))
+ .title("siGit — AI Coding Agent"),
)
- .description(description)
- })
- .collect();
+ .auth_methods(vec![AuthMethod::Agent(AuthMethodAgent::new(
+ "sigit", "siGit",
+ ))])
+ .agent_capabilities(
+ AgentCapabilities::default()
+ .load_session(true)
+ .session_capabilities(
+ SessionCapabilities::new().fork(SessionForkCapabilities::new()),
+ ),
+ )
+ .meta(initialize_meta()))
+ }
- if options.is_empty() {
- return vec![];
+ async fn handle_authenticate(
+ &self,
+ _req: AuthenticateRequest,
+ ) -> agent_client_protocol::Result<AuthenticateResponse> {
+ log::info!("authenticate");
+ Ok(AuthenticateResponse::default())
}
- let current_value = SessionConfigValueId::new(current_model.model_id.as_str());
+ async fn handle_load_session(
+ &self,
+ args: LoadSessionRequest,
+ ) -> agent_client_protocol::Result<LoadSessionResponse> {
+ log::info!(
+ "load_session: id={}, cwd={}, additional_directories={:?}",
+ args.session_id,
+ args.cwd.display(),
+ args.additional_directories
+ .iter()
+ .map(|p| p.display().to_string())
+ .collect::<Vec<_>>()
+ );
- vec![
- SessionConfigOption::select(MODEL_CONFIG_ID, "Model", current_value, options)
- .category(SessionConfigOptionCategory::Model)
- .description("Select the local LLM model for inference"),
- ]
-}
+ if let Ok(mut guard) = self.session_cwd.lock() {
+ *guard = Some(args.cwd.clone());
+ }
-/// returns `(config, max_tokens, tool_calling)` for a picker model_id, or None
-fn resolve_model_config(model_id: &str) -> Option<(GgufModelConfig, u64, bool)> {
- let items = models::build_model_picker_items();
- items
- .into_iter()
- .find(|item| {
- item.config.model_id == model_id
- && item.cache_health != setup::ModelCacheHealth::Incomplete
- })
- .map(|item| (item.config, item.max_tokens, item.tool_calling))
-}
+ // tool calls use relative paths, so we need to match the editor's cwd
+ if args.cwd.is_dir()
+ && let Err(err) = std::env::set_current_dir(&args.cwd)
+ {
+ log::warn!("could not set cwd to {}: {err}", args.cwd.display());
+ }
-#[derive(Debug, Clone)]
-enum SlashCommand {
- Help,
- Clear,
- Status,
- Models(Option<usize>),
- Exit,
- Unknown(String),
-}
+ // no session persistence, so "load" just resets
+ self.engine.clear_history().await;
-fn parse_slash(input: &str) -> Option<SlashCommand> {
- let trimmed = input.trim();
- if !trimmed.starts_with('/') {
- return None;
- }
- let mut parts = trimmed.splitn(2, char::is_whitespace);
- let command = parts.next().unwrap_or("");
- let argument = parts.next().map(str::trim);
- Some(match command {
- "/help" => SlashCommand::Help,
- "/clear" => SlashCommand::Clear,
- "/status" => SlashCommand::Status,
- "/models" => SlashCommand::Models(argument.and_then(|v| v.parse::<usize>().ok())),
- "/exit" | "/quit" | "/q" => SlashCommand::Exit,
- other => SlashCommand::Unknown(other.to_string()),
- })
-}
+ self.engine
+ .push_history(onde::inference::ChatMessage::system(format!(
+ "The user's project working directory is {}. \
+ Always use absolute paths under this directory for all file \
+ and directory operations. This is the root of the project \
+ the user has open in their editor.",
+ args.cwd.display()
+ )))
+ .await;
-fn format_models_list(current_model: &GgufModelConfig) -> String {
- let items = models::build_model_picker_items();
- if items.is_empty() {
- return "No local models found. siGit will use the platform default model.".to_string();
- }
+ let config_options = {
+ let guard = self.current_model.lock().unwrap();
+ build_model_config_options(&guard)
+ };
- let mut lines = vec!["Available models:".to_string()];
- let mut last_source: Option<&str> = None;
+ Ok(LoadSessionResponse::new().config_options(config_options))
+ }
- for (index, item) in items.iter().enumerate() {
- let source_key = match item.source_label.as_str() {
- "Onde" => "Onde",
- "HuggingFace" => "HuggingFace",
- _ => "Fallback",
- };
+ async fn handle_fork_session(
+ &self,
+ args: ForkSessionRequest,
+ ) -> agent_client_protocol::Result<ForkSessionResponse> {
+ let new_id = SessionId::new(uuid::Uuid::new_v4().to_string());
+ log::info!(
+ "fork_session: from={} new={new_id}, cwd={}, additional_directories={:?}",
+ args.session_id,
+ args.cwd.display(),
+ args.additional_directories
+ .iter()
+ .map(|p| p.display().to_string())
+ .collect::<Vec<_>>()
+ );
- if last_source != Some(source_key) {
- if last_source.is_some() {
- lines.push(String::new());
- }
- let section = match source_key {
- "Onde" => "Onde Inference",
- "HuggingFace" => "Hugging Face cache",
- _ => "Fallback",
- };
- lines.push(section.to_string());
- last_source = Some(source_key);
+ if let Ok(mut guard) = self.session_cwd.lock() {
+ *guard = Some(args.cwd.clone());
+ }
+ if args.cwd.is_dir()
+ && let Err(err) = std::env::set_current_dir(&args.cwd)
+ {
+ log::warn!("could not set cwd to {}: {err}", args.cwd.display());
}
- let number = index + 1;
- let current_badge = if item.config.model_id == current_model.model_id {
- " <- current"
- } else {
- ""
- };
- let tool_badge = if item.tool_calling {
- " tool calling"
- } else {
- ""
- };
- let health_badge = match item.cache_health {
- setup::ModelCacheHealth::Complete => "",
- setup::ModelCacheHealth::Incomplete => " ! incomplete cache",
- setup::ModelCacheHealth::NotDownloaded => " ↓ download on select",
- };
- let source = match source_key {
- "Onde" => " [Onde]",
- "HuggingFace" => " [HuggingFace]",
- _ => " [default]",
+ // no persistence, so fork == fresh session
+ self.engine.clear_history().await;
+
+ self.engine
+ .push_history(onde::inference::ChatMessage::system(format!(
+ "The user's project working directory is {}. \
+ Always use absolute paths under this directory for all file \
+ and directory operations. This is the root of the project \
+ the user has open in their editor.",
+ args.cwd.display()
+ )))
+ .await;
+
+ let config_options = {
+ let guard = self.current_model.lock().unwrap();
+ build_model_config_options(&guard)
};
- lines.push(format!(
- "{number}. {} {}{}{}{}{}",
- item.display_name, item.description, tool_badge, health_badge, current_badge, source,
- ));
+ Ok(ForkSessionResponse::new(new_id).config_options(config_options))
}
- lines.push(String::new());
- lines.push("Use /models N to switch models.".to_string());
- lines.join("\n")
-}
-
-async fn exec_slash_acp(
- agent: &SiGitAgent,
- session_id: SessionId,
- command: SlashCommand,
-) -> agent_client_protocol::Result<PromptResponse> {
- match command {
- SlashCommand::Help => {
- agent
- .send_assistant_message(
- session_id,
- "/help - show this message\n\
- /models - list available models\n\
- /models N - switch to model N\n\
- /clear - wipe conversation history\n\
- /status - show engine status\n\
- /exit - end this turn",
- )
- .await;
- }
- SlashCommand::Clear => {
- let cleared = agent.engine.clear_history().await;
- agent
- .send_assistant_message(
- session_id,
- format!("Cleared {cleared} turn(s). History is empty."),
- )
- .await;
- }
- SlashCommand::Status => {
- let info = agent.engine.info().await;
- let model = info.model_name.as_deref().unwrap_or("(none)");
- let memory = info.approx_memory.as_deref().unwrap_or("unknown");
- agent
- .send_assistant_message(
- session_id,
- format!(
- "status: {:?} model: {} memory: {} history: {} turns",
- info.status, model, memory, info.history_length,
- ),
- )
- .await;
- }
- SlashCommand::Models(None) => {
- let current_model = agent.current_model.lock().unwrap().clone();
- agent
- .send_assistant_message(session_id, format_models_list(¤t_model))
- .await;
- }
- SlashCommand::Models(Some(number)) => {
- let items = models::build_model_picker_items();
- let index = number.saturating_sub(1);
- match items.get(index).cloned() {
- None => {
- agent
- .send_assistant_message(
- session_id,
- format!("error: no model #{number} - type /models to see the list."),
- )
- .await;
- }
- Some(model) => {
- if model.cache_health == setup::ModelCacheHealth::Incomplete {
- agent
- .send_assistant_message(
- session_id,
- format!(
- "error: {} has an incomplete local cache and cannot be selected yet.",
- model.display_name
- ),
- )
- .await;
- } else if model.cache_health == setup::ModelCacheHealth::NotDownloaded {
- agent
- .send_assistant_message(
- session_id.clone(),
- format!(
- "Downloading and loading {} ({})… this may take a few minutes.",
- model.display_name, model.description
- ),
- )
- .await;
-
- match agent.switch_model_by_id(&model.config.model_id).await {
- Ok(new_config) => {
- agent.engine.clear_history().await;
- agent
- .send_assistant_message(
- session_id,
- format!(
- "✓ Downloaded and switched to {}",
- new_config.display_name
- ),
- )
- .await;
- }
- Err(err) => {
- agent
- .send_assistant_message(
- session_id,
- format!("error downloading model: {}", err.message),
- )
- .await;
- }
- }
- } else {
- agent
- .send_assistant_message(
- session_id.clone(),
- format!("Loading {}...", model.display_name),
- )
- .await;
-
- let switched = agent.switch_model_by_id(&model.config.model_id).await?;
- agent.engine.clear_history().await;
-
- agent
- .send_assistant_message(
- session_id,
- format!("Switched to {}.", switched.display_name),
- )
- .await;
- }
- }
- }
- }
- SlashCommand::Exit => {
- agent
- .send_assistant_message(
- session_id,
- "Use the panel controls to close or switch threads.",
- )
- .await;
- }
- SlashCommand::Unknown(command) => {
- agent
- .send_assistant_message(session_id, format!("unknown command: {command}"))
- .await;
- }
- }
-
- Ok(PromptResponse::new(StopReason::EndTurn))
-}
-
-#[async_trait::async_trait(?Send)]
-impl Agent for SiGitAgent {
- async fn initialize(
- &self,
- _args: InitializeRequest,
- ) -> agent_client_protocol::Result<InitializeResponse> {
- log::info!("initialize");
-
- Ok(InitializeResponse::new(ProtocolVersion::V1)
- .agent_info(
- Implementation::new("sigit", env!("CARGO_PKG_VERSION"))
- .title("siGit — AI Coding Agent"),
- )
- .auth_methods(vec![AuthMethod::Agent(AuthMethodAgent::new(
- "sigit", "siGit",
- ))])
- .agent_capabilities(
- AgentCapabilities::default()
- .load_session(true)
- .session_capabilities(
- SessionCapabilities::new().fork(SessionForkCapabilities::new()),
- ),
- )
- .meta(initialize_meta()))
- }
-
- async fn authenticate(
- &self,
- _args: AuthenticateRequest,
- ) -> agent_client_protocol::Result<AuthenticateResponse> {
- log::info!("authenticate");
- Ok(AuthenticateResponse::default())
- }
-
- async fn load_session(
- &self,
- args: LoadSessionRequest,
- ) -> agent_client_protocol::Result<LoadSessionResponse> {
- log::info!(
- "load_session: id={}, cwd={}, additional_directories={:?}",
- args.session_id,
- args.cwd.display(),
- args.additional_directories
- .iter()
- .map(|p| p.display().to_string())
- .collect::<Vec<_>>()
- );
-
- if let Ok(mut guard) = self.session_cwd.lock() {
- *guard = Some(args.cwd.clone());
- }
-
- // tool calls use relative paths, so we need to match the editor's cwd
- if args.cwd.is_dir()
- && let Err(err) = std::env::set_current_dir(&args.cwd)
- {
- log::warn!("could not set cwd to {}: {err}", args.cwd.display());
- }
-
- // no session persistence, so "load" just resets
- self.engine.clear_history().await;
-
- self.engine
- .push_history(onde::inference::ChatMessage::system(format!(
- "The user's project working directory is {}. \
- Always use absolute paths under this directory for all file \
- and directory operations. This is the root of the project \
- the user has open in their editor.",
- args.cwd.display()
- )))
- .await;
-
- let config_options = {
- let guard = self.current_model.lock().unwrap();
- build_model_config_options(&guard)
- };
-
- Ok(LoadSessionResponse::new().config_options(config_options))
- }
-
- async fn fork_session(
- &self,
- args: ForkSessionRequest,
- ) -> agent_client_protocol::Result<ForkSessionResponse> {
- let new_id = SessionId::new(uuid::Uuid::new_v4().to_string());
- log::info!(
- "fork_session: from={} new={new_id}, cwd={}, additional_directories={:?}",
- args.session_id,
- args.cwd.display(),
- args.additional_directories
- .iter()
- .map(|p| p.display().to_string())
- .collect::<Vec<_>>()
- );
-
- if let Ok(mut guard) = self.session_cwd.lock() {
- *guard = Some(args.cwd.clone());
- }
- if args.cwd.is_dir()
- && let Err(err) = std::env::set_current_dir(&args.cwd)
- {
- log::warn!("could not set cwd to {}: {err}", args.cwd.display());
- }
-
- // no persistence, so fork == fresh session
- self.engine.clear_history().await;
-
- self.engine
- .push_history(onde::inference::ChatMessage::system(format!(
- "The user's project working directory is {}. \
- Always use absolute paths under this directory for all file \
- and directory operations. This is the root of the project \
- the user has open in their editor.",
- args.cwd.display()
- )))
- .await;
-
- let config_options = {
- let guard = self.current_model.lock().unwrap();
- build_model_config_options(&guard)
- };
-
- Ok(ForkSessionResponse::new(new_id).config_options(config_options))
- }
-
- async fn new_session(
- &self,
- args: NewSessionRequest,
- ) -> agent_client_protocol::Result<NewSessionResponse> {
- let session_id = SessionId::new(uuid::Uuid::new_v4().to_string());
- log::info!(
- "new_session: id={session_id}, cwd={}, additional_directories={:?}",
- args.cwd.display(),
- args.additional_directories
- .iter()
- .map(|p| p.display().to_string())
- .collect::<Vec<_>>()
- );
+ async fn handle_new_session(
+ &self,
+ args: NewSessionRequest,
+ ) -> agent_client_protocol::Result<NewSessionResponse> {
+ let session_id = SessionId::new(uuid::Uuid::new_v4().to_string());
+ log::info!(
+ "new_session: id={session_id}, cwd={}, additional_directories={:?}",
+ args.cwd.display(),
+ args.additional_directories
+ .iter()
+ .map(|p| p.display().to_string())
+ .collect::<Vec<_>>()
+ );
if let Ok(mut guard) = self.session_cwd.lock() {
*guard = Some(args.cwd.clone());
@@ -1110,7 +820,11 @@ impl Agent for SiGitAgent {
Ok(NewSessionResponse::new(session_id).config_options(config_options))
}
- async fn prompt(&self, args: PromptRequest) -> agent_client_protocol::Result<PromptResponse> {
+ async fn handle_prompt(
+ &self,
+ cx: &ConnectionTo<Client>,
+ args: PromptRequest,
+ ) -> agent_client_protocol::Result<PromptResponse> {
let session_id = args.session_id.clone();
// log every block so we can debug @ references and file context
@@ -1131,9 +845,9 @@ impl Agent for SiGitAgent {
session_id,
i,
match &embedded.resource {
- agent_client_protocol::EmbeddedResourceResource::TextResourceContents(t) =>
+ EmbeddedResourceResource::TextResourceContents(t) =>
format!("TextResource(uri={}, {} chars)", t.uri, t.text.len()),
- agent_client_protocol::EmbeddedResourceResource::BlobResourceContents(b) =>
+ EmbeddedResourceResource::BlobResourceContents(b) =>
format!("BlobResource(uri={})", b.uri),
_ => "Unknown".to_string(),
}
@@ -1171,17 +885,13 @@ impl Agent for SiGitAgent {
ContentBlock::Resource(embedded) => {
// editor inlined the file content already
match &embedded.resource {
- agent_client_protocol::EmbeddedResourceResource::TextResourceContents(
- text_resource,
- ) => {
+ EmbeddedResourceResource::TextResourceContents(text_resource) => {
parts.push(format!(
"\n--- {} ---\n{}\n--- end {} ---",
text_resource.uri, text_resource.text, text_resource.uri
));
}
- agent_client_protocol::EmbeddedResourceResource::BlobResourceContents(
- blob,
- ) => {
+ EmbeddedResourceResource::BlobResourceContents(blob) => {
parts.push(format!("[binary resource: {}]", blob.uri));
}
_ => {
@@ -1255,7 +965,7 @@ impl Agent for SiGitAgent {
}
if let Some(command) = parse_slash(&user_text) {
- return exec_slash_acp(self, session_id, command).await;
+ return exec_slash_acp(self, cx, session_id, command).await;
}
log::info!(
@@ -1267,7 +977,7 @@ impl Agent for SiGitAgent {
// load the default ACP model lazily so initialize/session/new stay clean
// for registry validation and editor startup.
self.start_startup_model_load_if_needed();
- self.await_model_ready(&session_id).await?;
+ self.await_model_ready(cx, &session_id).await?;
// ── tool-calling loop ────────────────────────────────────────────
// send message → execute any tool calls → feed results back
@@ -1352,26 +1062,25 @@ impl Agent for SiGitAgent {
};
if !final_text.is_empty() {
- let notification = SessionNotification::new(
- session_id.clone(),
- SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from(final_text))),
- );
- if self.notification_tx.send(notification).await.is_err() {
- log::warn!("notification channel closed");
- }
+ self.send_assistant_message(cx, session_id.clone(), final_text)
+ .ok();
}
log::info!("prompt({}) complete — {} tool round(s)", session_id, round);
Ok(PromptResponse::new(StopReason::EndTurn))
}
- async fn cancel(&self, args: CancelNotification) -> agent_client_protocol::Result<()> {
+ async fn handle_cancel(
+ &self,
+ args: CancelNotification,
+ ) -> agent_client_protocol::Result<()> {
log::info!("cancel requested for session {}", args.session_id);
Ok(())
}
- async fn set_session_config_option(
+ async fn handle_set_session_config_option(
&self,
+ cx: &ConnectionTo<Client>,
args: SetSessionConfigOptionRequest,
) -> agent_client_protocol::Result<SetSessionConfigOptionResponse> {
log::info!(
@@ -1445,6 +1154,7 @@ impl Agent for SiGitAgent {
};
self.send_tool_call_update(
+ cx,
args.session_id.clone(),
SessionUpdate::ToolCall(
ToolCall::new(
@@ -1461,16 +1171,16 @@ impl Agent for SiGitAgent {
]),
),
)
- .await;
+ .ok();
// poll download progress and update the spinner in Zed
- let poller_tx = self.notification_tx.clone();
+ let cx_for_poller = cx.clone();
let poller_session = args.session_id.clone();
let poller_model_id = model_id_owned.clone();
let poller_stop = Arc::clone(&stop_flag);
let poller_tool_call_id = tool_call_id.clone();
- tokio::task::spawn_local(async move {
+ cx.spawn(async move {
const SPINNER: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let cache_path = onde::hf_cache::model_cache_path(&poller_model_id);
let mut tick: usize = 0;
@@ -1527,11 +1237,13 @@ impl Agent for SiGitAgent {
.content(vec![msg.into()]),
)),
);
- if poller_tx.send(notification).await.is_err() {
+ if cx_for_poller.send_notification(notification).is_err() {
break;
}
}
- });
+ Ok(())
+ })
+ .ok();
}
// cached models still take 10-30s to load weights; show a spinner
@@ -1543,6 +1255,7 @@ impl Agent for SiGitAgent {
.unwrap_or_else(|| model_id.to_string());
self.send_tool_call_update(
+ cx,
args.session_id.clone(),
SessionUpdate::ToolCall(
ToolCall::new(
@@ -1554,108 +1267,435 @@ impl Agent for SiGitAgent {
.content(vec![format!("Loading {cached_display_name}…").into()]),
),
)
- .await;
+ .ok();
// tick every 5s so the user knows we haven't frozen
- let spinner_tx = self.notification_tx.clone();
+ let cx_for_spinner = cx.clone();
let spinner_session = args.session_id.clone();
let spinner_name = cached_display_name.clone();
let spinner_stop = Arc::clone(&stop_flag);
let spinner_tool_call_id = tool_call_id.clone();
let load_start = std::time::Instant::now();
- tokio::task::spawn_local(async move {
- const SPINNER: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
- let mut tick: usize = 0;
- let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
- interval.tick().await; // consume the immediate first tick
+ cx.spawn(async move {
+ const SPINNER: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
+ let mut tick: usize = 0;
+ let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
+ interval.tick().await; // consume the immediate first tick
+
+ while !spinner_stop.load(Ordering::Relaxed) {
+ interval.tick().await;
+
+ if spinner_stop.load(Ordering::Relaxed) {
+ break;
+ }
+
+ let elapsed = load_start.elapsed();
+ let elapsed_str = if elapsed.as_secs() >= 60 {
+ format!("{}m {:02}s", elapsed.as_secs() / 60, elapsed.as_secs() % 60)
+ } else {
+ format!("{}s", elapsed.as_secs())
+ };
+ let frame = SPINNER[tick % SPINNER.len()];
+ tick += 1;
+
+ let msg = format!("{frame} Loading {spinner_name}… ({elapsed_str})");
+ let notification = SessionNotification::new(
+ spinner_session.clone(),
+ SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
+ spinner_tool_call_id.clone(),
+ ToolCallUpdateFields::new()
+ .status(ToolCallStatus::InProgress)
+ .content(vec![msg.into()]),
+ )),
+ );
+ if cx_for_spinner.send_notification(notification).is_err() {
+ break;
+ }
+ }
+ Ok(())
+ })
+ .ok();
+ }
+
+ let switch_result = self.switch_model_by_id(model_id).await;
+
+ stop_flag.store(true, Ordering::Relaxed);
+
+ match switch_result {
+ Ok(new_config) => {
+ let completion_title = if needs_download {
+ format!("✓ {} downloaded and loaded", new_config.display_name)
+ } else {
+ format!("✓ Switched to {}", new_config.display_name)
+ };
+ let completion_body = if needs_download {
+ format!("✓ {} downloaded and loaded.", new_config.display_name)
+ } else {
+ format!("✓ Switched to {}.", new_config.display_name)
+ };
+
+ self.send_tool_call_update(
+ cx,
+ args.session_id.clone(),
+ SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
+ tool_call_id,
+ ToolCallUpdateFields::new()
+ .title(completion_title)
+ .status(ToolCallStatus::Completed)
+ .content(vec![completion_body.into()]),
+ )),
+ )
+ .ok();
+
+ let config_options = {
+ let guard = self.current_model.lock().unwrap();
+ build_model_config_options(&guard)
+ };
+
+ log::info!("model switch complete");
+ Ok(SetSessionConfigOptionResponse::new(config_options))
+ }
+ Err(err) => {
+ self.send_tool_call_update(
+ cx,
+ args.session_id.clone(),
+ SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
+ tool_call_id,
+ ToolCallUpdateFields::new()
+ .title("Model switch failed".to_string())
+ .status(ToolCallStatus::Failed)
+ .content(vec![format!("error loading model: {}", err.message).into()]),
+ )),
+ )
+ .ok();
+
+ Err(err)
+ }
+ }
+ }
+}
+
+// ── Config option helpers ─────────────────────────────────────────────────────
+
+/// config option ID for the model picker in Zed's agent panel
+const MODEL_CONFIG_ID: &str = "sigit-model";
+
+fn build_model_config_options(current_model: &GgufModelConfig) -> Vec<SessionConfigOption> {
+ let items = models::build_model_picker_items();
+
+ let options: Vec<SessionConfigSelectOption> = items
+ .iter()
+ .filter(|item| item.cache_health != setup::ModelCacheHealth::Incomplete)
+ .map(|item| {
+ let mut desc_parts = Vec::new();
+ if item.tool_calling {
+ desc_parts.push("tool calling".to_string());
+ }
+ desc_parts.push(item.description.clone());
+ if item.cache_health == setup::ModelCacheHealth::NotDownloaded {
+ desc_parts.push("↓ download on select".to_string());
+ }
+ let description = desc_parts.join(" - ");
+ let source_badge = if item.cache_health == setup::ModelCacheHealth::NotDownloaded {
+ " [↓ Onde]"
+ } else {
+ match item.source_label.as_str() {
+ "Onde" => " [◉ Onde]",
+ "HuggingFace" => " [○ HuggingFace]",
+ _ => "",
+ }
+ };
+ let name = format!("{}{}", item.display_name, source_badge);
+ SessionConfigSelectOption::new(
+ SessionConfigValueId::new(item.config.model_id.as_str()),
+ name,
+ )
+ .description(description)
+ })
+ .collect();
+
+ if options.is_empty() {
+ return vec![];
+ }
+
+ let current_value = SessionConfigValueId::new(current_model.model_id.as_str());
+
+ vec![
+ SessionConfigOption::select(MODEL_CONFIG_ID, "Model", current_value, options)
+ .category(SessionConfigOptionCategory::Model)
+ .description("Select the local LLM model for inference"),
+ ]
+}
+
+/// returns `(config, max_tokens, tool_calling)` for a picker model_id, or None
+fn resolve_model_config(model_id: &str) -> Option<(GgufModelConfig, u64, bool)> {
+ let items = models::build_model_picker_items();
+ items
+ .into_iter()
+ .find(|item| {
+ item.config.model_id == model_id
+ && item.cache_health != setup::ModelCacheHealth::Incomplete
+ })
+ .map(|item| (item.config, item.max_tokens, item.tool_calling))
+}
+
+// ── Slash commands ────────────────────────────────────────────────────────────
+
+#[derive(Debug, Clone)]
+enum SlashCommand {
+ Help,
+ Clear,
+ Status,
+ Models(Option<usize>),
+ Exit,
+ Unknown(String),
+}
+
+fn parse_slash(input: &str) -> Option<SlashCommand> {
+ let trimmed = input.trim();
+ if !trimmed.starts_with('/') {
+ return None;
+ }
+ let mut parts = trimmed.splitn(2, char::is_whitespace);
+ let command = parts.next().unwrap_or("");
+ let argument = parts.next().map(str::trim);
+ Some(match command {
+ "/help" => SlashCommand::Help,
+ "/clear" => SlashCommand::Clear,
+ "/status" => SlashCommand::Status,
+ "/models" => SlashCommand::Models(argument.and_then(|v| v.parse::<usize>().ok())),
+ "/exit" | "/quit" | "/q" => SlashCommand::Exit,
+ other => SlashCommand::Unknown(other.to_string()),
+ })
+}
+
+fn format_models_list(current_model: &GgufModelConfig) -> String {
+ let items = models::build_model_picker_items();
+ if items.is_empty() {
+ return "No local models found. siGit will use the platform default model.".to_string();
+ }
+
+ let mut lines = vec!["Available models:".to_string()];
+ let mut last_source: Option<&str> = None;
+
+ for (index, item) in items.iter().enumerate() {
+ let source_key = match item.source_label.as_str() {
+ "Onde" => "Onde",
+ "HuggingFace" => "HuggingFace",
+ _ => "Fallback",
+ };
+
+ if last_source != Some(source_key) {
+ if last_source.is_some() {
+ lines.push(String::new());
+ }
+ let section = match source_key {
+ "Onde" => "Onde Inference",
+ "HuggingFace" => "Hugging Face cache",
+ _ => "Fallback",
+ };
+ lines.push(section.to_string());
+ last_source = Some(source_key);
+ }
+
+ let number = index + 1;
+ let current_badge = if item.config.model_id == current_model.model_id {
+ " <- current"
+ } else {
+ ""
+ };
+ let tool_badge = if item.tool_calling {
+ " tool calling"
+ } else {
+ ""
+ };
+ let health_badge = match item.cache_health {
+ setup::ModelCacheHealth::Complete => "",
+ setup::ModelCacheHealth::Incomplete => " ! incomplete cache",
+ setup::ModelCacheHealth::NotDownloaded => " ↓ download on select",
+ };
+ let source = match source_key {
+ "Onde" => " [Onde]",
+ "HuggingFace" => " [HuggingFace]",
+ _ => " [default]",
+ };
- while !spinner_stop.load(Ordering::Relaxed) {
- interval.tick().await;
+ lines.push(format!(
+ "{number}. {} {}{}{}{}{}",
+ item.display_name, item.description, tool_badge, health_badge, current_badge, source,
+ ));
+ }
- if spinner_stop.load(Ordering::Relaxed) {
- break;
- }
+ lines.push(String::new());
+ lines.push("Use /models N to switch models.".to_string());
+ lines.join("\n")
+}
- let elapsed = load_start.elapsed();
- let elapsed_str = if elapsed.as_secs() >= 60 {
- format!("{}m {:02}s", elapsed.as_secs() / 60, elapsed.as_secs() % 60)
+async fn exec_slash_acp(
+ agent: &SiGitAgent,
+ cx: &ConnectionTo<Client>,
+ session_id: SessionId,
+ command: SlashCommand,
+) -> agent_client_protocol::Result<PromptResponse> {
+ match command {
+ SlashCommand::Help => {
+ agent
+ .send_assistant_message(
+ cx,
+ session_id,
+ "/help - show this message\n\
+ /models - list available models\n\
+ /models N - switch to model N\n\
+ /clear - wipe conversation history\n\
+ /status - show engine status\n\
+ /exit - end this turn",
+ )
+ .ok();
+ }
+ SlashCommand::Clear => {
+ let cleared = agent.engine.clear_history().await;
+ agent
+ .send_assistant_message(
+ cx,
+ session_id,
+ format!("Cleared {cleared} turn(s). History is empty."),
+ )
+ .ok();
+ }
+ SlashCommand::Status => {
+ let info = agent.engine.info().await;
+ let model = info.model_name.as_deref().unwrap_or("(none)");
+ let memory = info.approx_memory.as_deref().unwrap_or("unknown");
+ agent
+ .send_assistant_message(
+ cx,
+ session_id,
+ format!(
+ "status: {:?} model: {} memory: {} history: {} turns",
+ info.status, model, memory, info.history_length,
+ ),
+ )
+ .ok();
+ }
+ SlashCommand::Models(None) => {
+ let current_model = agent.current_model.lock().unwrap().clone();
+ agent
+ .send_assistant_message(cx, session_id, format_models_list(¤t_model))
+ .ok();
+ }
+ SlashCommand::Models(Some(number)) => {
+ let items = models::build_model_picker_items();
+ let index = number.saturating_sub(1);
+ match items.get(index).cloned() {
+ None => {
+ agent
+ .send_assistant_message(
+ cx,
+ session_id,
+ format!("error: no model #{number} - type /models to see the list."),
+ )
+ .ok();
+ }
+ Some(model) => {
+ if model.cache_health == setup::ModelCacheHealth::Incomplete {
+ agent
+ .send_assistant_message(
+ cx,
+ session_id,
+ format!(
+ "error: {} has an incomplete local cache and cannot be selected yet.",
+ model.display_name
+ ),
+ )
+ .ok();
+ } else if model.cache_health == setup::ModelCacheHealth::NotDownloaded {
+ agent
+ .send_assistant_message(
+ cx,
+ session_id.clone(),
+ format!(
+ "Downloading and loading {} ({})… this may take a few minutes.",
+ model.display_name, model.description
+ ),
+ )
+ .ok();
+
+ match agent.switch_model_by_id(&model.config.model_id).await {
+ Ok(new_config) => {
+ agent.engine.clear_history().await;
+ agent
+ .send_assistant_message(
+ cx,
+ session_id,
+ format!(
+ "✓ Downloaded and switched to {}",
+ new_config.display_name
+ ),
+ )
+ .ok();
+ }
+ Err(err) => {
+ agent
+ .send_assistant_message(
+ cx,
+ session_id,
+ format!("error downloading model: {}", err.message),
+ )
+ .ok();
+ }
+ }
} else {
- format!("{}s", elapsed.as_secs())
- };
- let frame = SPINNER[tick % SPINNER.len()];
- tick += 1;
+ agent
+ .send_assistant_message(
+ cx,
+ session_id.clone(),
+ format!("Loading {}...", model.display_name),
+ )
+ .ok();
- let msg = format!("{frame} Loading {spinner_name}… ({elapsed_str})");
- let notification = SessionNotification::new(
- spinner_session.clone(),
- SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
- spinner_tool_call_id.clone(),
- ToolCallUpdateFields::new()
- .status(ToolCallStatus::InProgress)
- .content(vec![msg.into()]),
- )),
- );
- if spinner_tx.send(notification).await.is_err() {
- break;
+ let switched = agent.switch_model_by_id(&model.config.model_id).await?;
+ agent.engine.clear_history().await;
+
+ agent
+ .send_assistant_message(
+ cx,
+ session_id,
+ format!("Switched to {}.", switched.display_name),
+ )
+ .ok();
}
}
- });
+ }
}
-
- let switch_result = self.switch_model_by_id(model_id).await;
-
- stop_flag.store(true, Ordering::Relaxed);
-
- match switch_result {
- Ok(new_config) => {
- let completion_title = if needs_download {
- format!("✓ {} downloaded and loaded", new_config.display_name)
- } else {
- format!("✓ Switched to {}", new_config.display_name)
- };
- let completion_body = if needs_download {
- format!("✓ {} downloaded and loaded.", new_config.display_name)
- } else {
- format!("✓ Switched to {}.", new_config.display_name)
- };
-
- self.send_tool_call_update(
- args.session_id.clone(),
- SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
- tool_call_id,
- ToolCallUpdateFields::new()
- .title(completion_title)
- .status(ToolCallStatus::Completed)
- .content(vec![completion_body.into()]),
- )),
+ SlashCommand::Exit => {
+ agent
+ .send_assistant_message(
+ cx,
+ session_id,
+ "Use the panel controls to close or switch threads.",
)
- .await;
+ .ok();
+ }
+ SlashCommand::Unknown(command) => {
+ agent
+ .send_assistant_message(cx, session_id, format!("unknown command: {command}"))
+ .ok();
+ }
+ }
- let config_options = {
- let guard = self.current_model.lock().unwrap();
- build_model_config_options(&guard)
- };
+ Ok(PromptResponse::new(StopReason::EndTurn))
+}
- log::info!("model switch complete");
- Ok(SetSessionConfigOptionResponse::new(config_options))
- }
- Err(err) => {
- self.send_tool_call_update(
- args.session_id.clone(),
- SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
- tool_call_id,
- ToolCallUpdateFields::new()
- .title("Model switch failed".to_string())
- .status(ToolCallStatus::Failed)
- .content(vec![format!("error loading model: {}", err.message).into()]),
- )),
- )
- .await;
+// ── Request dispatch helper ───────────────────────────────────────────────────
- Err(err)
- }
- }
+fn handle_response<T: agent_client_protocol::JsonRpcResponse>(
+ responder: Responder<T>,
+ result: agent_client_protocol::Result<T>,
+) -> agent_client_protocol::Result<()> {
+ match result {
+ Ok(resp) => responder.respond(resp),
+ Err(err) => responder.respond_with_error(err),
}
}
@@ -1887,48 +1927,99 @@ async fn run_acp_server() -> anyhow::Result<()> {
let model_load_error: Arc<std::sync::Mutex<Option<String>>> =
Arc::new(std::sync::Mutex::new(None));
- let (notification_tx, mut notification_rx) = mpsc::channel::<SessionNotification>(256);
- let agent = SiGitAgent::new(
+ let state = Arc::new(SiGitAgent::new(
engine,
- notification_tx,
config,
model_ready,
startup_model_load_started,
model_load_error,
needs_download,
- );
+ ));
- // AgentSideConnection needs futures-io
let stdin = tokio::io::stdin().compat();
let stdout = tokio::io::stdout().compat_write();
-
- // ACP futures are !Send
- let local = tokio::task::LocalSet::new();
-
- local
- .run_until(async move {
- let (conn, io_task) = AgentSideConnection::new(
- agent,
- stdout,
- stdin,
- |fut: LocalBoxFuture<'static, ()>| {
- tokio::task::spawn_local(fut);
- },
- );
-
- tokio::task::spawn_local(async move {
- while let Some(notification) = notification_rx.recv().await {
- if let Err(err) = conn.session_notification(notification).await {
- log::warn!("session_notification failed: {err}");
- }
+ let transport = ByteStreams::new(stdout, stdin);
+
+ Agent
+ .builder()
+ .on_receive_request(
+ {
+ let state = Arc::clone(&state);
+ async move |req: InitializeRequest, responder, _cx: ConnectionTo<Client>| {
+ handle_response(responder, state.handle_initialize(req).await)
}
- });
-
- if let Err(err) = io_task.await {
- log::error!("ACP IO error: {err}");
- }
- })
- .await;
+ },
+ agent_client_protocol::on_receive_request!(),
+ )
+ .on_receive_request(
+ {
+ let state = Arc::clone(&state);
+ async move |req: AuthenticateRequest, responder, _cx: ConnectionTo<Client>| {
+ handle_response(responder, state.handle_authenticate(req).await)
+ }
+ },
+ agent_client_protocol::on_receive_request!(),
+ )
+ .on_receive_request(
+ {
+ let state = Arc::clone(&state);
+ async move |req: LoadSessionRequest, responder, _cx: ConnectionTo<Client>| {
+ handle_response(responder, state.handle_load_session(req).await)
+ }
+ },
+ agent_client_protocol::on_receive_request!(),
+ )
+ .on_receive_request(
+ {
+ let state = Arc::clone(&state);
+ async move |req: ForkSessionRequest, responder, _cx: ConnectionTo<Client>| {
+ handle_response(responder, state.handle_fork_session(req).await)
+ }
+ },
+ agent_client_protocol::on_receive_request!(),
+ )
+ .on_receive_request(
+ {
+ let state = Arc::clone(&state);
+ async move |req: NewSessionRequest, responder, _cx: ConnectionTo<Client>| {
+ handle_response(responder, state.handle_new_session(req).await)
+ }
+ },
+ agent_client_protocol::on_receive_request!(),
+ )
+ .on_receive_request(
+ {
+ let state = Arc::clone(&state);
+ async move |req: PromptRequest, responder, cx: ConnectionTo<Client>| {
+ handle_response(responder, state.handle_prompt(&cx, req).await)
+ }
+ },
+ agent_client_protocol::on_receive_request!(),
+ )
+ .on_receive_request(
+ {
+ let state = Arc::clone(&state);
+ async move |req: SetSessionConfigOptionRequest, responder, cx: ConnectionTo<Client>| {
+ handle_response(
+ responder,
+ state.handle_set_session_config_option(&cx, req).await,
+ )
+ }
+ },
+ agent_client_protocol::on_receive_request!(),
+ )
+ .on_receive_notification(
+ {
+ let state = Arc::clone(&state);
+ async move |notif: CancelNotification, _cx: ConnectionTo<Client>| {
+ state.handle_cancel(notif).await
+ }
+ },
+ agent_client_protocol::on_receive_notification!(),
+ )
+ .connect_to(transport)
+ .await
+ .map_err(|e| anyhow::anyhow!("ACP connection error: {e}"))?;
log::info!("siGit shutting down");
Ok(())