use std::{io, path::Path, time::Instant};
use futures_util::FutureExt;
use super::{
clipboard::CopyOutcome, subagent_panel::SubagentAttachTarget, text_selection::CopyNotice, App,
};
use crate::herdr::HerdrReporter;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum AttachDestination {
HerdrPane,
Clipboard,
}
pub(super) struct PendingSubagentAttach {
target: SubagentAttachTarget,
clipboard_command: String,
handle: tokio::task::JoinHandle<io::Result<()>>,
}
pub(super) fn attach_command(run_id: &str) -> String {
format!("rho attach {run_id}")
}
pub(super) fn pane_attach_command(run_id: &str) -> String {
let executable = std::env::current_exe().ok();
pane_attach_command_for_executable(executable.as_deref(), run_id)
}
fn pane_attach_command_for_executable(executable: Option<&Path>, run_id: &str) -> String {
let executable = executable
.map(|path| path.to_string_lossy())
.unwrap_or_else(|| "rho".into());
format!(
"{} {} {}",
shell_single_quote(&executable),
shell_single_quote("attach"),
shell_single_quote(run_id)
)
}
pub(super) fn destination(herdr: &HerdrReporter) -> AttachDestination {
if herdr.is_enabled() {
AttachDestination::HerdrPane
} else {
AttachDestination::Clipboard
}
}
pub(super) fn action_hint(destination: AttachDestination) -> &'static str {
match destination {
AttachDestination::HerdrPane => "open pane",
AttachDestination::Clipboard => "copy attach",
}
}
impl App {
pub(super) fn subagent_attach_destination(&self) -> AttachDestination {
destination(&self.info.services.herdr)
}
pub(super) fn subagent_action_hint(&self) -> &'static str {
action_hint(self.subagent_attach_destination())
}
pub(super) fn activate_subagent_row(&mut self, target: &SubagentAttachTarget, now: Instant) {
let clipboard_command = attach_command(&target.run_id);
match self.subagent_attach_destination() {
AttachDestination::HerdrPane => {
if self
.pending_subagent_attaches
.iter()
.any(|pending| pending.target.run_id == target.run_id)
{
return;
}
let herdr = self.info.services.herdr.clone();
let pane_command = pane_attach_command(&target.run_id);
let handle =
tokio::spawn(async move { herdr.open_sibling_pane(&pane_command).await });
self.pending_subagent_attaches.push(PendingSubagentAttach {
target: target.clone(),
clipboard_command,
handle,
});
self.notify_status(format!(
"opening a herdr pane for {} {}",
target.agent_id, target.run_id
));
}
AttachDestination::Clipboard => {
let message = self.copy_attach_command(&clipboard_command, now);
self.notify_status(message);
}
}
}
pub(super) fn poll_pending_subagent_attaches(&mut self, now: Instant) -> bool {
let mut changed = false;
while let Some(index) = self
.pending_subagent_attaches
.iter()
.position(|pending| pending.handle.is_finished())
{
let pending = self.pending_subagent_attaches.remove(index);
let result = pending
.handle
.now_or_never()
.expect("finished subagent attach task has a result");
match result {
Ok(Ok(())) => self.notify_status(format!(
"opened a herdr pane attached to {} {}",
pending.target.agent_id, pending.target.run_id
)),
Ok(Err(error)) => {
let copy_message = self.copy_attach_command(&pending.clipboard_command, now);
self.notify_status(format!("herdr pane failed ({error}); {copy_message}"));
}
Err(error) => {
let copy_message = self.copy_attach_command(&pending.clipboard_command, now);
self.notify_status(format!("herdr pane task failed ({error}); {copy_message}"));
}
}
changed = true;
}
changed
}
pub(super) fn has_pending_subagent_attach(&self) -> bool {
!self.pending_subagent_attaches.is_empty()
}
fn copy_attach_command(&mut self, command: &str, now: Instant) -> String {
let outcome = self.clipboard.copy(command);
let message = match &outcome {
Ok(CopyOutcome::Confirmed) => format!("copied attach command: {command}"),
Ok(CopyOutcome::SentToTerminal) => {
format!("sent attach command to the terminal: {command}")
}
Err(error) => format!("copy failed ({error}); attach with: {command}"),
};
self.history
.set_copy_notice(Some(CopyNotice::from_copy_result(
outcome,
command.chars().count(),
now,
)));
message
}
}
fn shell_single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', r"'\''"))
}
#[cfg(test)]
#[path = "subagent_attach_tests.rs"]
mod tests;