1use super::session::builtin_commands;
2use super::{App, ExitState, ForegroundOperation, Overlay, PromptPhase, Route};
3use crate::command::{AgentCommand, Command, GitReviewCommand, TerminalCommand};
4use crate::conversation::tool_calls::ToolStatus;
5use crate::conversation::{ContextUsageDisplay, MessageRole};
6use crate::screens::artifact_review::ArtifactReviewScreen;
7use crate::surfaces::modal::ElicitationModal;
8use crate::surfaces::picker::CommandEntry;
9use crate::surfaces::session_picker::SessionPicker;
10use acp_utils::client::AcpEvent;
11use acp_utils::notifications::McpNotification;
12use agent_client_protocol::schema::MaybeUndefined;
13use agent_client_protocol::schema::v2::{
14 self as acp, CreateElicitationRequest, ElicitationMode, SessionId, SessionUpdate, StateUpdate,
15};
16use std::time::Instant;
17use utils::artifact_review::ArtifactReviewElicitationMeta;
18
19impl App {
20 #[allow(clippy::too_many_lines)]
21 pub fn on_acp_event(&mut self, event: AcpEvent) {
22 match event {
23 AcpEvent::SessionUpdate(notification) => {
24 if ¬ification.session_id == self.session.session_id()
25 || matches!(self.foreground, ForegroundOperation::CreatingSession { .. })
26 {
27 self.on_session_update(¬ification.update);
28 }
29 }
30 AcpEvent::ContextCleared(_) => {
31 self.reset_conversation();
32 }
33 AcpEvent::ElicitationRequest { params, responder } => {
34 let params = *params;
35 self.close_elicitation_owner();
36 if let Some(meta) = artifact_review_meta(¶ms) {
37 self.open_route(Route::ArtifactReview(Box::new(ArtifactReviewScreen::new(meta, responder))));
38 return;
39 }
40 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
43 overlay.on_elicitation_request(
44 params,
45 responder,
46 self.browser_opener.clone(),
47 self.clipboard_writer.clone(),
48 );
49 return;
50 }
51 if let Some(modal) = ElicitationModal::with_url_handlers(
52 params,
53 responder,
54 self.browser_opener.clone(),
55 self.clipboard_writer.clone(),
56 ) {
57 self.open_overlay(Overlay::Elicitation(modal));
58 }
59 }
60 AcpEvent::McpNotification(notification) => self.on_mcp_notification(¬ification),
61 AcpEvent::GitDiffEvent(params) => {
62 self.queue(Command::GitReview(GitReviewCommand::Forward(params.event)));
63 }
64 AcpEvent::AuthMethodsUpdated(params) => {
65 self.session.set_auth_methods(¶ms.auth_methods);
66 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
67 overlay.update_auth_methods(¶ms.auth_methods);
68 }
69 }
70 AcpEvent::ConnectionClosed => self.on_connection_closed(),
71 AcpEvent::SubAgentProgress(progress) => {
72 if self.conversation.progress_indicator().accepts_activity() {
73 self.conversation.on_sub_agent_progress(&progress);
74 }
75 }
76 }
77 }
78
79 pub(super) fn abandon_workspace_move(&mut self, message: &str) {
81 self.notify(message);
82 self.foreground = ForegroundOperation::Idle;
83 }
84
85 pub(super) fn open_session_picker(&mut self, sessions: Vec<acp::SessionInfo>) {
86 let current_id = self.session.session_id().clone();
87 let others = sessions.into_iter().filter(|session| session.session_id != current_id).collect();
88 let picker = SessionPicker::new(others, self.session.capabilities().session_preview);
89 if let Some(id) = picker.initial_preview_request() {
90 self.queue(Command::Agent(AgentCommand::SessionPreview { session_id: id }));
91 }
92 self.open_overlay(Overlay::Sessions(picker));
93 }
94
95 pub(super) fn on_resumed_session(&mut self, session_id: &SessionId, response: acp::ResumeSessionResponse) {
96 match &self.foreground {
97 ForegroundOperation::ResumingSession { session_id: expected, cwd }
98 | ForegroundOperation::LoadingWorkspaceSession { session_id: expected, cwd }
99 if expected == session_id =>
100 {
101 if self.session.working_dir() != cwd {
102 let cwd = cwd.clone();
103 self.session.set_working_dir(cwd.clone());
104 self.resolve_workspace(cwd);
105 }
106 }
107 _ => return,
108 }
109 self.session.update_config_options(response.config_options);
110 if matches!(self.foreground, ForegroundOperation::LoadingWorkspaceSession { .. }) {
111 self.notify(&format!("Moved to {}", self.session.working_dir().display()));
112 }
113 self.return_to_conversation();
114 self.foreground = ForegroundOperation::Idle;
115 }
116
117 pub(super) fn on_new_session(&mut self, session_id: SessionId, config_options: Vec<acp::SessionConfigOption>) {
118 if !matches!(self.foreground, ForegroundOperation::Idle | ForegroundOperation::CreatingSession { .. }) {
119 return;
120 }
121 let previous_selections = match std::mem::take(&mut self.foreground) {
122 ForegroundOperation::CreatingSession { previous_selections } => previous_selections,
123 _ => Vec::new(),
124 };
125 self.close_elicitation_owner();
126 self.return_to_conversation();
127 self.session.set_session(session_id, config_options);
128 self.restore_config_selections(&previous_selections);
129 }
130
131 fn on_mcp_notification(&mut self, notification: &McpNotification) {
133 let McpNotification::ServerStatus { servers } = notification;
134 self.session.update_server_statuses(servers);
135 let servers = servers.clone();
136 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
137 overlay.update_server_statuses(servers);
138 }
139 }
140
141 fn on_connection_closed(&mut self) {
144 self.close_elicitation_owner();
145 self.return_to_conversation();
146 self.foreground = ForegroundOperation::Idle;
147 self.commands.retain(|command| !matches!(command, Command::Terminal(TerminalCommand::RingBell)));
148 self.exit_state = ExitState::ConnectionLost;
149 }
150
151 fn close_elicitation_owner(&mut self) {
154 match self.overlay.as_mut() {
155 Some(Overlay::Settings(overlay)) => overlay.cancel_pending_elicitation(),
156 Some(Overlay::Elicitation(_)) => self.close_overlay(),
157 _ => {}
158 }
159 }
160
161 fn on_session_update(&mut self, update: &SessionUpdate) {
162 if matches!(update, SessionUpdate::StateUpdate(StateUpdate::Running(_))) && self.foreground.is_idle() {
163 self.foreground = ForegroundOperation::Prompt(PromptPhase::Running);
164 self.conversation.progress_indicator_mut().prompt_started();
165 }
166 if self.waiting_for_response() {
167 self.observe_activity(update);
168 }
169 match update {
170 SessionUpdate::CompactionUpdate(update) => {
171 if self.conversation.progress_indicator().accepts_activity() {
172 self.conversation.turn_mut().apply_compaction(update);
173 }
174 }
175 SessionUpdate::StateUpdate(StateUpdate::Idle(idle)) if self.waiting_for_response() => {
176 let status = match idle.stop_reason {
177 Some(acp::StopReason::Cancelled) => ToolStatus::Error("cancelled".to_string()),
178 _ => ToolStatus::Success,
179 };
180 self.finish_prompt(&status);
181 }
182 SessionUpdate::UserMessage(message) => {
183 self.conversation.upsert_message(MessageRole::User, message.message_id.clone(), &message.content);
184 }
185 SessionUpdate::AgentMessage(message) => {
186 self.conversation.upsert_message(MessageRole::Assistant, message.message_id.clone(), &message.content);
187 }
188 SessionUpdate::UserMessageChunk(chunk) => {
189 self.conversation.append_message_chunk(MessageRole::User, chunk);
190 }
191 SessionUpdate::AgentMessageChunk(chunk) => {
192 self.conversation.append_message_chunk(MessageRole::Assistant, chunk);
193 }
194 SessionUpdate::ToolCallContentChunk(chunk) => {
195 self.conversation.on_tool_call_content_chunk(chunk);
196 }
197 SessionUpdate::ToolCallUpdate(update) => {
198 self.conversation.on_tool_call_update(update);
199 }
200 SessionUpdate::AvailableCommandsUpdate(update) => {
201 let agent_commands: Vec<_> = update
202 .available_commands
203 .iter()
204 .map(|command| CommandEntry {
205 name: command.name.clone(),
206 description: command.description.clone(),
207 has_input: command.input.is_some(),
208 hint: match &command.input {
209 Some(acp::AvailableCommandInput::Text(input)) => Some(input.hint.clone()),
210 _ => None,
211 },
212 builtin: false,
213 })
214 .collect();
215 let mut all = builtin_commands(self.session.capabilities());
216 all.extend(agent_commands);
217 self.available_commands = all;
218 }
219 SessionUpdate::ConfigOptionUpdate(update) => {
220 self.session.update_config_options(update.config_options.clone());
221 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
222 overlay.update_config_options(self.session.config_options());
223 }
224 }
225 SessionUpdate::PlanUpdate(plan) => {
226 self.conversation.plan_tracker_mut().apply_update(plan, Instant::now());
227 }
228 SessionUpdate::UsageUpdate(usage) => {
229 self.conversation.turn_mut().set_context_usage(Some(ContextUsageDisplay {
230 used_tokens: u32::try_from(usage.used).unwrap_or(u32::MAX),
231 limit_tokens: u32::try_from(usage.size).unwrap_or(u32::MAX),
232 }));
233 }
234 _ => {}
235 }
236 }
237
238 fn observe_activity(&mut self, update: &SessionUpdate) {
239 let indicator = self.conversation.progress_indicator_mut();
240 match update {
241 SessionUpdate::AgentMessageChunk(_) | SessionUpdate::StateUpdate(StateUpdate::Running(_)) => {
242 indicator.response_started();
243 }
244 SessionUpdate::StateUpdate(StateUpdate::RequiresAction(_)) => indicator.requires_action(),
245 SessionUpdate::ToolCallUpdate(_) => indicator.tool_activity(),
246 SessionUpdate::AgentThoughtChunk(chunk) => {
247 if let acp::ContentBlock::Text(text) = &chunk.content
248 && !text.text.is_empty()
249 {
250 indicator.record_thought(&chunk.message_id, &text.text);
251 }
252 }
253 SessionUpdate::AgentThought(message) => match &message.content {
254 MaybeUndefined::Undefined => {}
255 MaybeUndefined::Null => indicator.replace_thought(&message.message_id, ""),
256 MaybeUndefined::Value(blocks) => {
257 let text = acp_utils::content::map_content_blocks_to_text(blocks.clone());
258 indicator.replace_thought(&message.message_id, &text);
259 }
260 },
261 _ => {}
262 }
263 }
264
265 pub(super) fn finish_prompt(&mut self, terminal_status: &ToolStatus) {
266 let was_in_flight = self.waiting_for_response();
267 self.foreground.finish_prompt();
268 self.conversation.turn_mut().clear_compactions();
269 self.conversation.progress_indicator_mut().prompt_finished();
270 self.conversation.finish_turn(terminal_status);
271 if was_in_flight && matches!(terminal_status, ToolStatus::Success) {
272 self.queue(Command::Terminal(TerminalCommand::RingBell));
273 }
274 }
275}
276
277pub(super) fn artifact_review_meta(params: &CreateElicitationRequest) -> Option<ArtifactReviewElicitationMeta> {
278 if !matches!(params.mode, ElicitationMode::Form(_)) {
279 return None;
280 }
281 ArtifactReviewElicitationMeta::parse(params.meta.as_ref())
282}