1use super::session::builtin_commands;
2use super::{App, ExitState, Overlay, Route};
3use crate::attachment::placeholder_for_content_block;
4use crate::command::{AgentCommand, Command, TerminalCommand};
5use crate::conversation::ContextUsageDisplay;
6use crate::conversation::tool_calls::ToolStatus;
7use crate::screens::plan_review::PlanReviewScreen;
8use crate::surfaces::modal::ElicitationModal;
9use crate::surfaces::picker::CommandEntry;
10use crate::surfaces::session_picker::SessionPicker;
11use crate::surfaces::workspace_picker::WorkspacePicker;
12use acp_utils::client::AcpEvent;
13use acp_utils::notifications::McpNotification;
14use agent_client_protocol::schema::v1::{self as acp, SessionId};
15use std::time::Instant;
16
17impl App {
18 #[allow(clippy::too_many_lines)]
19 pub fn on_acp_event(&mut self, event: AcpEvent) {
20 match event {
21 AcpEvent::SessionUpdate { session_id, update } => {
22 if let Some(passthrough) = self.session.buffer_update(&session_id, *update)
26 && &session_id == self.session.session_id()
27 {
28 self.on_session_update(&passthrough);
29 }
30 }
31 AcpEvent::PromptDone(stop_reason) => {
32 let status = match stop_reason {
33 acp::StopReason::Cancelled => ToolStatus::Error("cancelled".to_string()),
34 _ => ToolStatus::Success,
35 };
36 self.finish_prompt(&status);
37 }
38 AcpEvent::PromptError(error) => {
39 tracing::error!("Prompt error: {error}");
40 self.session.clear_loads();
41 self.session.abandon_workspace_load();
42 self.finish_prompt(&ToolStatus::Error(format!("failed: {error}")));
43 self.notify(&format!("Prompt failed: {error}"));
44 }
45 AcpEvent::ContextUsage(params) => {
46 self.conversation.turn_mut().set_context_usage(
47 params.usage.context_limit.map(|limit| ContextUsageDisplay {
48 used_tokens: params.usage.input_tokens,
49 limit_tokens: limit,
50 }),
51 );
52 }
53 AcpEvent::ContextCompaction(params) => {
54 self.conversation.turn_mut().set_compaction_active(params.active);
55 }
56 AcpEvent::ContextCleared(_) => {
57 self.reset_conversation();
58 }
59 AcpEvent::ElicitationRequest { params, responder } => {
60 self.close_elicitation_owner();
61 if let Some(meta) = plan_review_meta(¶ms) {
62 self.open_route(Route::PlanReview(Box::new(PlanReviewScreen::new(meta, responder))));
63 return;
64 }
65 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
68 overlay.on_elicitation_request(
69 params,
70 responder,
71 self.browser_opener.clone(),
72 self.clipboard_writer.clone(),
73 );
74 return;
75 }
76 self.open_overlay(Overlay::Elicitation(ElicitationModal::with_url_handlers(
77 params,
78 responder,
79 self.browser_opener.clone(),
80 self.clipboard_writer.clone(),
81 )));
82 }
83 AcpEvent::McpNotification(notification) => self.on_mcp_notification(¬ification),
84 AcpEvent::AuthMethodsUpdated(params) => {
85 self.session.set_auth_methods(¶ms.auth_methods);
86 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
87 overlay.update_auth_methods(¶ms.auth_methods);
88 }
89 }
90 AcpEvent::AuthenticateComplete { method_id } => {
91 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
92 overlay.on_authenticate_complete(&method_id);
93 }
94 }
95 AcpEvent::AuthenticateFailed { method_id, error } => {
96 tracing::warn!("Provider authentication failed for {method_id}: {error}");
97 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
98 overlay.on_authenticate_failed(&method_id);
99 }
100 }
101 AcpEvent::ConnectionClosed => self.on_connection_closed(),
102 AcpEvent::ConfigOptionUpdateFailed { error } => {
103 tracing::warn!("set_session_config_option failed: {error}");
104 self.notify(&format!("Failed to update setting: {error}"));
105 }
106 AcpEvent::SessionsListed { sessions } => self.open_session_picker(sessions),
107 AcpEvent::SessionLoaded { session_id, config_options } => {
108 self.on_session_loaded(session_id, config_options);
109 }
110 AcpEvent::NewSessionCreated { session_id, config_options } => {
111 self.on_new_session(session_id, config_options);
112 }
113 AcpEvent::SessionPreviewLoaded(preview) => {
114 if let Some(Overlay::Sessions(picker)) = self.overlay.as_mut() {
115 picker.on_preview_loaded(preview);
116 }
117 }
118 AcpEvent::SessionPreviewFailed { session_id, error } => {
119 if let Some(Overlay::Sessions(picker)) = self.overlay.as_mut() {
120 picker.on_preview_failed(&session_id, error);
121 }
122 }
123 AcpEvent::PromptSearchResults(response) => {
124 self.composer.prompt_search_on_results(response);
125 }
126 AcpEvent::PromptSearchFailed { query, error } => {
127 if let Some(picker) = self.composer.prompt_search_mut() {
128 picker.on_failed(&query, error);
129 }
130 }
131 AcpEvent::WorkspacesListed(response) => {
132 self.open_overlay(Overlay::Workspaces(WorkspacePicker::new(response.workspaces)));
133 self.session.begin_workspace_picking();
134 }
135 AcpEvent::WorkspaceMoved(response) => self.on_workspace_moved(response.new_cwd),
136 AcpEvent::WorkspaceListFailed { error } => {
137 self.abandon_workspace_move(&format!("Failed to list workspaces: {error}"));
138 }
139 AcpEvent::WorkspaceMoveFailed { error } => {
140 self.abandon_workspace_move(&format!("Workspace move failed: {error}"));
141 }
142 AcpEvent::SubAgentProgress(progress) => {
143 self.conversation.on_sub_agent_progress(&progress);
144 }
145 }
146 }
147
148 fn abandon_workspace_move(&mut self, message: &str) {
150 self.notify(message);
151 self.session.end_workspace_move();
152 }
153
154 fn open_session_picker(&mut self, sessions: Vec<acp::SessionInfo>) {
155 let current_id = self.session.session_id().clone();
156 let others = sessions.into_iter().filter(|session| session.session_id != current_id).collect();
157 let picker = SessionPicker::new(others, self.session.capabilities().session_preview);
158 if let Some(id) = picker.initial_preview_request() {
159 self.queue(Command::Agent(AgentCommand::SessionPreview { session_id: id }));
160 }
161 self.open_overlay(Overlay::Sessions(picker));
162 }
163
164 fn on_session_loaded(&mut self, session_id: SessionId, config_options: Vec<acp::SessionConfigOption>) {
168 let updates = self.session.take_buffered_updates(&session_id);
169 self.session.set_session(session_id, config_options);
170 self.reset_turn_state();
171 for update in updates {
172 self.on_session_update(&update);
173 }
174 self.return_to_conversation();
175 self.session.end_workspace_move();
176 }
177
178 fn on_new_session(&mut self, session_id: SessionId, config_options: Vec<acp::SessionConfigOption>) {
179 self.session.clear_loads();
180 self.close_elicitation_owner();
181 self.return_to_conversation();
182 let previous_selections: Vec<(String, String)> = self
183 .session
184 .config_options()
185 .iter()
186 .filter_map(|option| option.select().map(|select| (option.id.clone(), select.current_value.to_string())))
187 .collect();
188 self.session.set_session(session_id, config_options);
189 self.reset_conversation();
190 self.restore_config_selections(&previous_selections);
191 }
192
193 fn on_mcp_notification(&mut self, notification: &McpNotification) {
195 let McpNotification::ServerStatus { servers } = notification;
196 self.session.update_server_statuses(servers);
197 let servers = servers.clone();
198 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
199 overlay.update_server_statuses(servers);
200 }
201 }
202
203 fn on_connection_closed(&mut self) {
206 self.close_elicitation_owner();
207 self.return_to_conversation();
208 self.session.end_workspace_move();
209 self.session.clear_loads();
210 self.commands.retain(|command| !matches!(command, Command::Terminal(TerminalCommand::RingBell)));
211 self.exit_state = ExitState::Exiting;
212 }
213
214 fn close_elicitation_owner(&mut self) {
217 match self.overlay.as_mut() {
218 Some(Overlay::Settings(overlay)) => overlay.cancel_pending_elicitation(),
219 Some(Overlay::Elicitation(_)) => self.close_overlay(),
220 _ => {}
221 }
222 }
223
224 fn on_session_update(&mut self, update: &acp::SessionUpdate) {
225 match update {
226 acp::SessionUpdate::UserMessageChunk(chunk) => {
227 if let Some(text) = match &chunk.content {
228 acp::ContentBlock::Text(text) => Some(text.text.clone()),
229 block => placeholder_for_content_block(block).map(str::to_string),
230 } {
231 self.conversation.append_user_content(text);
232 }
233 }
234 acp::SessionUpdate::AgentMessageChunk(chunk) => {
235 if let acp::ContentBlock::Text(text_content) = &chunk.content {
236 if !text_content.text.is_empty() {
237 self.conversation.progress_indicator_mut().response_started();
238 }
239 self.conversation.append_assistant_chunk(&text_content.text);
240 }
241 }
242 acp::SessionUpdate::AgentThoughtChunk(chunk) => {
243 if let acp::ContentBlock::Text(text_content) = &chunk.content
244 && !text_content.text.is_empty()
245 {
246 self.conversation.progress_indicator_mut().record_thought(&text_content.text);
247 }
248 }
249 acp::SessionUpdate::ToolCall(tool_call) => {
250 self.conversation.progress_indicator_mut().tool_activity();
251 self.conversation.on_tool_call(tool_call);
252 }
253 acp::SessionUpdate::ToolCallUpdate(update) => {
254 self.conversation.progress_indicator_mut().tool_activity();
255 self.conversation.on_tool_call_update(update);
256 }
257 acp::SessionUpdate::AvailableCommandsUpdate(update) => {
258 let agent_commands: Vec<_> = update
259 .available_commands
260 .iter()
261 .map(|command| CommandEntry {
262 name: command.name.clone(),
263 description: command.description.clone(),
264 has_input: command.input.is_some(),
265 hint: match &command.input {
266 Some(acp::AvailableCommandInput::Unstructured(input)) => Some(input.hint.clone()),
267 _ => None,
268 },
269 builtin: false,
270 })
271 .collect();
272 let mut all = builtin_commands(self.session.capabilities());
273 all.extend(agent_commands);
274 self.available_commands = all;
275 }
276 acp::SessionUpdate::ConfigOptionUpdate(update) => {
277 self.session.update_config_options(update.config_options.clone());
278 self.conversation.finish_current_block();
279 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
280 overlay.update_config_options(self.session.config_options());
281 }
282 }
283 acp::SessionUpdate::Plan(plan) => {
284 self.conversation.plan_tracker_mut().replace(plan.entries.clone(), Instant::now());
285 self.conversation.finish_current_block();
286 }
287 _ => {
288 self.conversation.finish_current_block();
289 }
290 }
291 }
292
293 fn finish_prompt(&mut self, terminal_status: &ToolStatus) {
294 let was_in_flight = self.waiting_for_response();
295 self.conversation.turn_mut().set_prompt_in_flight(false);
296 self.conversation.turn_mut().set_compaction_active(false);
297 self.conversation.progress_indicator_mut().prompt_finished();
298 self.conversation.finish_turn(terminal_status);
299 if was_in_flight && matches!(terminal_status, ToolStatus::Success) {
300 self.queue(Command::Terminal(TerminalCommand::RingBell));
301 }
302 }
303}
304
305pub(super) fn plan_review_meta(
306 params: &acp_utils::notifications::ElicitationParams,
307) -> Option<utils::plan_review::PlanReviewElicitationMeta> {
308 match ¶ms.request {
309 acp_utils::notifications::ElicitRequestParams::FormElicitationParams { meta, .. } => {
310 utils::plan_review::PlanReviewElicitationMeta::parse(meta.as_ref().map(|meta| &**meta).map(|meta| &**meta))
311 }
312 _ => None,
313 }
314}