1use super::session::builtin_commands;
2use super::{App, ExitState, ForegroundOperation, Overlay, Route};
3use crate::command::{AgentCommand, Command, GitReviewCommand, TerminalCommand};
4use crate::screens::artifact_review::ArtifactReviewScreen;
5use crate::surfaces::modal::ElicitationModal;
6use crate::surfaces::picker::CommandEntry;
7use crate::surfaces::session_picker::SessionPicker;
8use acp_utils::client::AcpEvent;
9use acp_utils::conversation::TurnFinished;
10use acp_utils::notifications::McpNotification;
11use agent_client_protocol::schema::v2::{self as acp, CreateElicitationRequest, ElicitationMode, SessionId, SessionUpdate};
12use utils::artifact_review::ArtifactReviewElicitationMeta;
13
14impl App {
15 #[allow(clippy::too_many_lines)]
16 pub fn on_acp_event(&mut self, event: AcpEvent) {
17 let current = self.is_current(&event);
18 if current
19 && let Some(TurnFinished { stop_reason }) = self.conversation.apply_event(&event)
20 && stop_reason != Some(acp::StopReason::Cancelled)
21 {
22 self.queue(Command::Terminal(TerminalCommand::RingBell));
23 }
24 match event {
25 AcpEvent::SessionUpdate(notification) if current => self.on_session_update(¬ification.update),
26 AcpEvent::ContextCleared(_) if current => self.foreground.drop_prepared_prompt(),
27 AcpEvent::ElicitationRequest { params, responder } => {
28 let params = *params;
29 self.close_elicitation_owner();
30 if let Some(meta) = artifact_review_meta(¶ms) {
31 self.open_route(Route::ArtifactReview(Box::new(ArtifactReviewScreen::new(meta, responder))));
32 return;
33 }
34 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
37 overlay.on_elicitation_request(
38 params,
39 responder,
40 self.browser_opener.clone(),
41 self.clipboard_writer.clone(),
42 );
43 return;
44 }
45 if let Some(modal) = ElicitationModal::with_url_handlers(
46 params,
47 responder,
48 self.browser_opener.clone(),
49 self.clipboard_writer.clone(),
50 ) {
51 self.open_overlay(Overlay::Elicitation(modal));
52 }
53 }
54 AcpEvent::McpNotification(notification) => self.on_mcp_notification(¬ification),
55 AcpEvent::GitDiffEvent(params) => {
56 self.queue(Command::GitReview(GitReviewCommand::Forward(params.event)));
57 }
58 AcpEvent::AuthMethodsUpdated(params) => {
59 self.session.set_auth_methods(¶ms.auth_methods);
60 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
61 overlay.update_auth_methods(¶ms.auth_methods);
62 }
63 }
64 AcpEvent::ConnectionClosed => self.on_connection_closed(),
65 AcpEvent::SessionUpdate(_) | AcpEvent::ContextCleared(_) | AcpEvent::SubAgentProgress(_) => {}
66 }
67 }
68
69 pub(super) fn abandon_workspace_move(&mut self, message: &str) {
71 self.notify(message);
72 self.foreground = ForegroundOperation::Idle;
73 }
74
75 pub(super) fn open_session_picker(&mut self, sessions: Vec<acp::SessionInfo>) {
76 let current_id = self.session.session_id().clone();
77 let others = sessions.into_iter().filter(|session| session.session_id != current_id).collect();
78 let picker = SessionPicker::new(others, self.session.capabilities().session_preview);
79 if let Some(id) = picker.initial_preview_request() {
80 self.queue(Command::Agent(AgentCommand::SessionPreview { session_id: id }));
81 }
82 self.open_overlay(Overlay::Sessions(picker));
83 }
84
85 pub(super) fn on_resumed_session(&mut self, session_id: &SessionId, response: acp::ResumeSessionResponse) {
86 match &self.foreground {
87 ForegroundOperation::ResumingSession { session_id: expected, cwd }
88 | ForegroundOperation::LoadingWorkspaceSession { session_id: expected, cwd }
89 if expected == session_id =>
90 {
91 if self.session.working_dir() != cwd {
92 let cwd = cwd.clone();
93 self.session.set_working_dir(cwd.clone());
94 self.resolve_workspace(cwd);
95 }
96 }
97 _ => return,
98 }
99 self.session.update_config_options(response.config_options);
100 if matches!(self.foreground, ForegroundOperation::LoadingWorkspaceSession { .. }) {
101 self.notify(&format!("Moved to {}", self.session.working_dir().display()));
102 }
103 self.return_to_conversation();
104 self.foreground = ForegroundOperation::Idle;
105 }
106
107 pub(super) fn on_new_session(&mut self, session_id: SessionId, config_options: Vec<acp::SessionConfigOption>) {
108 if !matches!(self.foreground, ForegroundOperation::CreatingSession { .. })
109 && !self.can_start_foreground_operation()
110 {
111 return;
112 }
113 let previous_selections = match std::mem::take(&mut self.foreground) {
114 ForegroundOperation::CreatingSession { previous_selections } => previous_selections,
115 _ => Vec::new(),
116 };
117 self.close_elicitation_owner();
118 self.return_to_conversation();
119 self.session.set_session(session_id, config_options);
120 self.restore_config_selections(&previous_selections);
121 }
122
123 fn on_mcp_notification(&mut self, notification: &McpNotification) {
125 let McpNotification::ServerStatus { servers } = notification;
126 self.session.update_server_statuses(servers);
127 let servers = servers.clone();
128 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
129 overlay.update_server_statuses(servers);
130 }
131 }
132
133 fn on_connection_closed(&mut self) {
136 self.close_elicitation_owner();
137 self.return_to_conversation();
138 self.foreground = ForegroundOperation::Idle;
139 self.commands.retain(|command| !matches!(command, Command::Terminal(TerminalCommand::RingBell)));
140 self.exit_state = ExitState::ConnectionLost;
141 }
142
143 fn close_elicitation_owner(&mut self) {
146 match self.overlay.as_mut() {
147 Some(Overlay::Settings(overlay)) => overlay.cancel_pending_elicitation(),
148 Some(Overlay::Elicitation(_)) => self.close_overlay(),
149 _ => {}
150 }
151 }
152
153 fn is_current(&self, event: &AcpEvent) -> bool {
156 event.session_id().is_none_or(|session_id| session_id == self.session.session_id())
157 || matches!(self.foreground, ForegroundOperation::CreatingSession { .. })
158 }
159
160 fn on_session_update(&mut self, update: &SessionUpdate) {
161 match update {
162 SessionUpdate::AvailableCommandsUpdate(update) => {
163 let agent_commands: Vec<_> = update
164 .available_commands
165 .iter()
166 .map(|command| CommandEntry {
167 name: command.name.clone(),
168 description: command.description.clone(),
169 has_input: command.input.is_some(),
170 hint: match &command.input {
171 Some(acp::AvailableCommandInput::Text(input)) => Some(input.hint.clone()),
172 _ => None,
173 },
174 builtin: false,
175 })
176 .collect();
177 let mut all = builtin_commands(self.session.capabilities());
178 all.extend(agent_commands);
179 self.available_commands = all;
180 }
181 SessionUpdate::ConfigOptionUpdate(update) => {
182 self.session.update_config_options(update.config_options.clone());
183 if let Some(Overlay::Settings(overlay)) = self.overlay.as_mut() {
184 overlay.update_config_options(self.session.config_options());
185 }
186 }
187 _ => {}
188 }
189 }
190}
191
192pub(super) fn artifact_review_meta(params: &CreateElicitationRequest) -> Option<ArtifactReviewElicitationMeta> {
193 if !matches!(params.mode, ElicitationMode::Form(_)) {
194 return None;
195 }
196 ArtifactReviewElicitationMeta::parse(params.meta.as_ref())
197}