use super::{
App, AppStatus, BlockCache, ChatMessage, IncrementalMarkdown, MessageBlock, MessageRole,
};
use crate::acp::client::ClientEvent;
use crate::app::{mention, slash};
use agent_client_protocol::{self as acp, Agent as _};
use std::path::Path;
use std::rc::Rc;
pub(super) fn submit_input(app: &mut App) {
if matches!(app.status, AppStatus::Connecting | AppStatus::Error) {
return;
}
app.mention = None;
app.slash = None;
let text = app.input.text();
if text.trim().is_empty() {
return;
}
if slash::try_handle_submit(app, &text) {
return;
}
let _ = app.finalize_in_progress_tool_calls(acp::ToolCallStatus::Failed);
let Some(ref conn) = app.conn else { return };
let content_blocks = build_content_blocks(&text, &app.cwd_raw);
app.messages.push(ChatMessage {
role: MessageRole::User,
blocks: vec![MessageBlock::Text(
text.clone(),
BlockCache::default(),
IncrementalMarkdown::from_complete(&text),
)],
});
app.messages.push(ChatMessage { role: MessageRole::Assistant, blocks: Vec::new() });
app.input.clear();
app.status = AppStatus::Thinking;
app.viewport.engage_auto_scroll();
let conn = Rc::clone(conn);
let Some(sid) = app.session_id.clone() else {
return;
};
let tx = app.event_tx.clone();
tokio::task::spawn_local(async move {
match conn.prompt(acp::PromptRequest::new(sid, content_blocks)).await {
Ok(resp) => {
tracing::debug!("PromptResponse: stop_reason={:?}", resp.stop_reason);
let _ = tx.send(ClientEvent::TurnComplete);
}
Err(e) => {
let _ = tx.send(ClientEvent::TurnError(e.to_string()));
}
}
});
}
fn build_content_blocks(text: &str, cwd: &str) -> Vec<acp::ContentBlock> {
let spans = mention::find_mention_spans(text);
if spans.is_empty() {
return vec![acp::ContentBlock::Text(acp::TextContent::new(text))];
}
let cwd_path = Path::new(cwd);
let mut blocks: Vec<acp::ContentBlock> = Vec::new();
let mut last_end = 0;
for (start, end, ref_path) in &spans {
if *start > last_end {
let preceding = &text[last_end..*start];
if !preceding.is_empty() {
blocks.push(acp::ContentBlock::Text(acp::TextContent::new(preceding)));
}
}
let clean_path = ref_path.trim_end_matches('/');
let fs_path = cwd_path.join(clean_path);
if fs_path.is_file() {
match std::fs::read_to_string(&fs_path) {
Ok(content) => {
let uri = path_to_file_uri(&fs_path);
let mime = mime_from_extension(ref_path);
let resource_contents =
acp::TextResourceContents::new(&content, &uri).mime_type(&mime);
let embedded = acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::TextResourceContents(resource_contents),
);
blocks.push(acp::ContentBlock::Resource(embedded));
}
Err(e) => {
tracing::warn!("Failed to read @{ref_path}: {e}");
blocks
.push(acp::ContentBlock::Text(acp::TextContent::new(&text[*start..*end])));
}
}
} else if fs_path.is_dir() {
let uri = path_to_file_uri(&fs_path);
let link =
acp::ResourceLink::new(clean_path, &uri).mime_type("inode/directory".to_owned());
blocks.push(acp::ContentBlock::ResourceLink(link));
} else {
blocks.push(acp::ContentBlock::Text(acp::TextContent::new(&text[*start..*end])));
}
last_end = *end;
}
if last_end < text.len() {
let trailing = &text[last_end..];
if !trailing.is_empty() {
blocks.push(acp::ContentBlock::Text(acp::TextContent::new(trailing)));
}
}
blocks
}
fn path_to_file_uri(path: &Path) -> String {
let abs_path = path
.canonicalize()
.unwrap_or_else(|_| path.to_path_buf())
.to_string_lossy()
.replace('\\', "/");
format!("file:///{}", abs_path.trim_start_matches('/'))
}
fn mime_from_extension(path: &str) -> String {
let ext = Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();
match ext.as_str() {
"rs" => "text/x-rust",
"py" => "text/x-python",
"js" | "jsx" => "text/javascript",
"ts" | "tsx" => "text/typescript",
"json" => "application/json",
"toml" => "text/x-toml",
"yaml" | "yml" => "text/x-yaml",
"md" => "text/markdown",
"html" | "htm" => "text/html",
"css" => "text/css",
"sh" | "bash" => "text/x-shellscript",
"c" | "h" | "hpp" => "text/x-c",
"cpp" | "cc" | "cxx" => "text/x-c++",
"go" => "text/x-go",
"java" => "text/x-java",
"rb" => "text/x-ruby",
"sql" => "text/x-sql",
"xml" => "text/xml",
_ => "text/plain",
}
.to_owned()
}