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