Skip to main content

wisp/app/
submission.rs

1use super::{App, ForegroundOperation};
2use crate::attachment::AttachmentOutcome;
3use crate::command::{AgentCommand, Command, FilesystemCommand, PromptRejection};
4use crate::request::RequestId;
5use crate::session::session_config_view::LocalConfigView;
6use crate::surfaces::composer::Submission;
7use acp_utils::config_option_id::ConfigOptionId;
8use agent_client_protocol::schema::v2 as acp;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct QueuedPrompt {
12    pub request_id: RequestId,
13    pub submission: Submission,
14}
15
16impl App {
17    pub(super) fn submit(&mut self) {
18        if self.composer.is_empty() || !self.foreground.is_idle() {
19            return;
20        }
21
22        if self.session.workspace_access() == crate::session::WorkspaceAccess::Remote
23            && (!self.composer.selected_mentions().is_empty() || !self.composer.pending_media().is_empty())
24        {
25            self.notify("Path attachments are unavailable for remote workspaces; remove attachments before sending");
26            return;
27        }
28        let submission = self.composer.take_submission();
29        let attachments = submission.attachments();
30
31        self.foreground = ForegroundOperation::PreparingPrompt(submission);
32        if attachments.is_empty() {
33            self.finish_submission(AttachmentOutcome { blocks: Vec::new(), warnings: Vec::new() });
34        } else {
35            self.queue(Command::Filesystem(FilesystemCommand::PrepareSubmission { attachments }));
36        }
37    }
38
39    pub(super) fn finish_submission(&mut self, outcome: AttachmentOutcome) {
40        let Some(submission) = self.foreground.take_prepared_prompt() else {
41            return;
42        };
43        let media_error = self.media_support_error(&outcome.blocks);
44        if media_error.is_some() {
45            self.composer.restore_submission(submission);
46        } else {
47            let content = (!outcome.blocks.is_empty()).then_some(outcome.blocks);
48            self.start_prompt(submission, content);
49        }
50        for warning in &outcome.warnings {
51            self.notify(warning);
52        }
53        if let Some(message) = media_error {
54            self.notify(&message);
55        }
56    }
57
58    pub(super) fn finish_prompt(&mut self, request_id: RequestId, result: Result<(), PromptRejection>) {
59        let Some(index) = self.queued_prompts.iter().position(|prompt| prompt.request_id == request_id) else {
60            return;
61        };
62        let prompt = self.queued_prompts.remove(index);
63        let Err(rejection) = result else { return };
64        self.composer.restore_submission(prompt.submission);
65        if let PromptRejection::Failed(error) = rejection {
66            self.notify(&format!("Failed to send prompt: {error}"));
67        }
68    }
69
70    fn media_support_error(&self, blocks: &[acp::ContentBlock]) -> Option<String> {
71        let requires_image = blocks.iter().any(|block| matches!(block, acp::ContentBlock::Image(_)));
72        let requires_audio = blocks.iter().any(|block| matches!(block, acp::ContentBlock::Audio(_)));
73
74        if !requires_image && !requires_audio {
75            return None;
76        }
77
78        if requires_image && self.session.prompt_capabilities().image.is_none() {
79            return Some("ACP agent does not support image input.".to_string());
80        }
81        if requires_audio && self.session.prompt_capabilities().audio.is_none() {
82            return Some("ACP agent does not support audio input.".to_string());
83        }
84
85        let config = LocalConfigView::new(self.session.config_options());
86        let values = config.current_values(ConfigOptionId::Model);
87        if values.is_empty() {
88            return None;
89        }
90        let selected_meta = config.selected_model_metadata();
91
92        if selected_meta.len() != values.len() {
93            return Some("Current model selection is missing prompt capability metadata.".into());
94        }
95
96        if requires_image && selected_meta.iter().any(|meta| !meta.supports_image) {
97            return Some("Current model selection does not support image input.".to_string());
98        }
99        if requires_audio && selected_meta.iter().any(|meta| !meta.supports_audio) {
100            return Some("Current model selection does not support audio input.".to_string());
101        }
102
103        None
104    }
105
106    pub(super) fn send_prompt_search_query(&mut self, query: String) {
107        if self.composer.prompt_search().is_none() {
108            return;
109        }
110        let params = acp_utils::notifications::PromptSearchParams { query, limit: None };
111        self.queue(Command::Agent(AgentCommand::SearchPrompts(params)));
112    }
113}