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 acp_utils::client::{AcpEvent, LoadedSession};
12use acp_utils::notifications::McpNotification;
13use agent_client_protocol::schema::v1::{self as acp, CreateElicitationRequest, ElicitationMode, SessionId};
14use std::time::Instant;
15
16impl App {
17 #[allow(clippy::too_many_lines)]
18 pub fn on_acp_event(&mut self, event: AcpEvent) {
19 match event {
20 AcpEvent::SessionUpdate { session_id, update } => {
21 if &session_id == self.session.session_id() {
22 self.on_session_update(&update);
23 }
24 }
25 AcpEvent::PromptCompleted(stop_reason) => {
26 let status = match stop_reason {
27 acp::StopReason::Cancelled => ToolStatus::Error("cancelled".to_string()),
28 _ => ToolStatus::Success,
29 };
30 self.finish_prompt(&status);
31 }
32 AcpEvent::ContextCompaction(params) => {
33 if self.conversation.progress_indicator().accepts_activity() {
34 self.conversation.turn_mut().set_compaction_active(params.active);
35 }
36 }
37 AcpEvent::ContextCleared(_) => {
38 self.reset_conversation();
39 }
40 AcpEvent::ElicitationRequest { params, responder } => {
41 let params = *params;
42 self.close_elicitation_owner();
43 if let Some(meta) = plan_review_meta(¶ms) {
44 self.open_route(Route::PlanReview(Box::new(PlanReviewScreen::new(meta, responder))));
45 return;
46 }
47 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
50 overlay.on_elicitation_request(
51 params,
52 responder,
53 self.browser_opener.clone(),
54 self.clipboard_writer.clone(),
55 );
56 return;
57 }
58 if let Some(modal) = ElicitationModal::with_url_handlers(
59 params,
60 responder,
61 self.browser_opener.clone(),
62 self.clipboard_writer.clone(),
63 ) {
64 self.open_overlay(Overlay::Elicitation(modal));
65 }
66 }
67 AcpEvent::McpNotification(notification) => self.on_mcp_notification(¬ification),
68 AcpEvent::AuthMethodsUpdated(params) => {
69 self.session.set_auth_methods(¶ms.auth_methods);
70 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
71 overlay.update_auth_methods(¶ms.auth_methods);
72 }
73 }
74 AcpEvent::ConnectionClosed => self.on_connection_closed(),
75 AcpEvent::SessionUsage(_) => {},
76 AcpEvent::SubAgentProgress(progress) => {
77 if self.conversation.progress_indicator().accepts_activity() {
78 self.conversation.on_sub_agent_progress(&progress);
79 }
80 }
81 }
82 }
83
84 pub(super) fn abandon_workspace_move(&mut self, message: &str) {
86 self.notify(message);
87 self.session.end_workspace_move();
88 }
89
90 pub(super) fn open_session_picker(&mut self, sessions: Vec<acp::SessionInfo>) {
91 let current_id = self.session.session_id().clone();
92 let others = sessions.into_iter().filter(|session| session.session_id != current_id).collect();
93 let picker = SessionPicker::new(others, self.session.capabilities().session_preview);
94 if let Some(id) = picker.initial_preview_request() {
95 self.queue(Command::Agent(AgentCommand::SessionPreview { session_id: id }));
96 }
97 self.open_overlay(Overlay::Sessions(picker));
98 }
99
100 pub(super) fn on_loaded_session(&mut self, loaded: LoadedSession) {
101 let LoadedSession { session_id, response, replay } = loaded;
102 self.reset_turn_state();
103 for notification in replay {
104 self.on_session_update(¬ification.update);
105 }
106 self.session.set_session(session_id, response.config_options.unwrap_or_default());
107 self.return_to_conversation();
108 self.session.end_workspace_move();
109 }
110
111 pub(super) fn on_new_session(&mut self, session_id: SessionId, config_options: Vec<acp::SessionConfigOption>) {
112 self.close_elicitation_owner();
113 self.return_to_conversation();
114 let previous_selections: Vec<(String, String)> = self
115 .session
116 .config_options()
117 .iter()
118 .filter_map(|option| option.select().map(|select| (option.id.clone(), select.current_value.to_string())))
119 .collect();
120 self.session.set_session(session_id, config_options);
121 self.reset_conversation();
122 self.restore_config_selections(&previous_selections);
123 }
124
125 fn on_mcp_notification(&mut self, notification: &McpNotification) {
127 let McpNotification::ServerStatus { servers } = notification;
128 self.session.update_server_statuses(servers);
129 let servers = servers.clone();
130 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
131 overlay.update_server_statuses(servers);
132 }
133 }
134
135 fn on_connection_closed(&mut self) {
138 self.close_elicitation_owner();
139 self.return_to_conversation();
140 self.session.end_workspace_move();
141 self.commands.retain(|command| !matches!(command, Command::Terminal(TerminalCommand::RingBell)));
142 self.exit_state = ExitState::Exiting;
143 }
144
145 fn close_elicitation_owner(&mut self) {
148 match self.overlay.as_mut() {
149 Some(Overlay::Settings(overlay)) => overlay.cancel_pending_elicitation(),
150 Some(Overlay::Elicitation(_)) => self.close_overlay(),
151 _ => {}
152 }
153 }
154
155 fn on_session_update(&mut self, update: &acp::SessionUpdate) {
156 if is_agent_activity(update) && !self.conversation.progress_indicator().accepts_activity() {
157 return;
158 }
159 match update {
160 acp::SessionUpdate::UserMessageChunk(chunk) => {
161 if let Some(text) = match &chunk.content {
162 acp::ContentBlock::Text(text) => Some(text.text.clone()),
163 block => placeholder_for_content_block(block).map(str::to_string),
164 } {
165 self.conversation.append_user_content(text);
166 }
167 }
168 acp::SessionUpdate::AgentMessageChunk(chunk) => {
169 if let acp::ContentBlock::Text(text_content) = &chunk.content {
170 if !text_content.text.is_empty() {
171 self.conversation.progress_indicator_mut().response_started();
172 }
173 self.conversation.append_assistant_chunk(&text_content.text);
174 }
175 }
176 acp::SessionUpdate::AgentThoughtChunk(chunk) => {
177 if let acp::ContentBlock::Text(text_content) = &chunk.content
178 && !text_content.text.is_empty()
179 {
180 self.conversation.progress_indicator_mut().record_thought(&text_content.text);
181 }
182 }
183 acp::SessionUpdate::ToolCall(tool_call) => {
184 self.conversation.progress_indicator_mut().tool_activity();
185 self.conversation.on_tool_call(tool_call);
186 }
187 acp::SessionUpdate::ToolCallUpdate(update) => {
188 self.conversation.progress_indicator_mut().tool_activity();
189 self.conversation.on_tool_call_update(update);
190 }
191 acp::SessionUpdate::AvailableCommandsUpdate(update) => {
192 let agent_commands: Vec<_> = update
193 .available_commands
194 .iter()
195 .map(|command| CommandEntry {
196 name: command.name.clone(),
197 description: command.description.clone(),
198 has_input: command.input.is_some(),
199 hint: match &command.input {
200 Some(acp::AvailableCommandInput::Unstructured(input)) => Some(input.hint.clone()),
201 _ => None,
202 },
203 builtin: false,
204 })
205 .collect();
206 let mut all = builtin_commands(self.session.capabilities());
207 all.extend(agent_commands);
208 self.available_commands = all;
209 }
210 acp::SessionUpdate::ConfigOptionUpdate(update) => {
211 self.session.update_config_options(update.config_options.clone());
212 self.conversation.finish_current_block();
213 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
214 overlay.update_config_options(self.session.config_options());
215 }
216 }
217 acp::SessionUpdate::Plan(plan) => {
218 self.conversation.plan_tracker_mut().replace(plan.entries.clone(), Instant::now());
219 self.conversation.finish_current_block();
220 }
221 acp::SessionUpdate::UsageUpdate(usage) => {
222 self.conversation.turn_mut().set_context_usage(Some(ContextUsageDisplay {
223 used_tokens: u32::try_from(usage.used).unwrap_or(u32::MAX),
224 limit_tokens: u32::try_from(usage.size).unwrap_or(u32::MAX),
225 }));
226 }
227 _ => {
228 self.conversation.finish_current_block();
229 }
230 }
231 }
232
233 pub(super) fn finish_prompt(&mut self, terminal_status: &ToolStatus) {
234 let was_in_flight = self.waiting_for_response();
235 self.conversation.turn_mut().set_prompt_in_flight(false);
236 self.conversation.turn_mut().set_compaction_active(false);
237 self.conversation.progress_indicator_mut().prompt_finished();
238 self.conversation.finish_turn(terminal_status);
239 if was_in_flight && matches!(terminal_status, ToolStatus::Success) {
240 self.queue(Command::Terminal(TerminalCommand::RingBell));
241 }
242 }
243}
244
245fn is_agent_activity(update: &acp::SessionUpdate) -> bool {
246 matches!(
247 update,
248 acp::SessionUpdate::AgentMessageChunk(_)
249 | acp::SessionUpdate::AgentThoughtChunk(_)
250 | acp::SessionUpdate::ToolCall(_)
251 | acp::SessionUpdate::ToolCallUpdate(_)
252 )
253}
254
255pub(super) fn plan_review_meta(
256 params: &CreateElicitationRequest,
257) -> Option<utils::plan_review::PlanReviewElicitationMeta> {
258 if !matches!(params.mode, ElicitationMode::Form(_)) {
259 return None;
260 }
261 utils::plan_review::PlanReviewElicitationMeta::parse(params.meta.as_ref())
262}