use std::sync::Arc;
use anyhow::Context as _;
use argonauts_core::Adapter;
use argonauts_core::app::{ReplayTurn, TurnReply};
use argonauts_core::cli_sessions::{
CliScan, CliSession, CliSessionRef, CliTurn, ResumeLaunch, Speaker,
};
use argonauts_core::events::run::Party;
use argonauts_core::names::{ModelRef, SessionKey};
use super::excerpt::Exchange;
use crate::commands::adapters_here;
use crate::controller::Ctx;
pub(crate) const LISTED: usize = 30;
const READ_BACK: usize = 40;
pub(super) struct Picked {
pub session: CliSession,
pub launch: ResumeLaunch,
pub turns: Vec<CliTurn>,
pub name: &'static str,
pub binary: &'static str,
}
impl Picked {
pub(super) fn worker(&self, default: &ModelRef) -> ModelRef {
if default.agent == self.session.agent_id.0 {
default.clone()
} else {
ModelRef::new(self.session.agent_id.0.clone(), None)
}
}
pub(super) fn replay(&self, worker: &ModelRef) -> Vec<ReplayTurn> {
self.turns
.iter()
.map(|turn| ReplayTurn {
from: match turn.from {
Speaker::User => Party::User,
Speaker::Agent => Party::Agent {
session: SessionKey::numbered(0),
worker: worker.clone(),
},
},
text: turn.text.clone(),
steps: turn.steps.clone(),
})
.collect()
}
pub(super) fn history(&self) -> Vec<Exchange> {
self.turns
.windows(2)
.filter(|pair| pair[0].from == Speaker::User && pair[1].from == Speaker::Agent)
.filter(|pair| !pair[1].text.is_empty())
.map(|pair| {
let reply = TurnReply::Answer {
markdown: pair[1].text.clone(),
};
Exchange::new(pair[0].text.clone(), &reply)
})
.collect()
}
}
fn scan(ctx: &Ctx) -> Option<CliScan> {
let home = ctx.env.user_home.clone()?;
Some(CliScan::new(ctx.env.repo_root.clone(), home))
}
fn adapter(ctx: &Ctx, agent_id: &str) -> Option<Arc<dyn Adapter>> {
adapters_here(Some(&ctx.env), ctx.settings.resolved())
.ok()?
.into_iter()
.find(|adapter| adapter.id() == agent_id)
}
pub(super) fn name(ctx: &Ctx, agent_id: &str) -> String {
adapter(ctx, agent_id).map_or_else(
|| agent_id.to_owned(),
|adapter| adapter.display_name().to_owned(),
)
}
pub(super) fn binary(ctx: &Ctx, agent_id: &str) -> String {
adapter(ctx, agent_id).map_or_else(
|| agent_id.to_owned(),
|adapter| adapter.binary().to_owned(),
)
}
pub(crate) async fn list(ctx: &Ctx, every_folder: bool) -> Vec<CliSession> {
let Some(mut scan) = scan(ctx) else {
return Vec::new();
};
if every_folder {
scan = scan.everywhere();
}
let adapters = adapters_here(Some(&ctx.env), ctx.settings.resolved()).unwrap_or_default();
let listed = tokio::task::spawn_blocking(move || {
let mut sessions = Vec::new();
for adapter in adapters {
match adapter.cli_sessions(&scan, LISTED) {
Ok(own) => sessions.extend(own),
Err(err) => tracing::warn!("listing {}'s own sessions: {err:#}", adapter.id()),
}
}
newest_first(&mut sessions);
sessions
})
.await;
listed.unwrap_or_else(|err| {
tracing::warn!("listing the CLIs' own sessions: {err}");
Vec::new()
})
}
fn newest_first(sessions: &mut [CliSession]) {
sessions.sort_by(|a, b| {
b.updated
.cmp(&a.updated)
.then_with(|| a.agent_id.cmp(&b.agent_id))
.then_with(|| a.id.cmp(&b.id))
});
}
pub(super) async fn pick(ctx: &Ctx, reference: &CliSessionRef) -> anyhow::Result<Picked> {
let agent_id = reference.agent_id.0.clone();
let adapter = adapter(ctx, &agent_id)
.with_context(|| format!("There's no agent CLI {agent_id}. /agents lists them."))?;
let scan = scan(ctx).with_context(|| {
format!(
"Can't find your home folder, where {} keeps its sessions.",
adapter.display_name()
)
})?;
let reference = reference.clone();
tokio::task::spawn_blocking(move || {
let session = find(adapter.as_ref(), &scan, &reference).or_else(|here| {
find(adapter.as_ref(), &scan.clone().everywhere(), &reference).map_err(|_| here)
})?;
let scan = CliScan::new(session.folder.clone(), scan.home.clone());
let launch = adapter
.resume_launch(&session)
.with_context(|| format!("{} can't carry a session on.", adapter.display_name()))?;
let turns = adapter.cli_session_turns(&scan, &session.id, READ_BACK)?;
Ok(Picked {
session,
launch,
turns,
name: adapter.display_name(),
binary: adapter.binary(),
})
})
.await
.context("reading the session")?
}
fn find(
adapter: &dyn Adapter,
scan: &CliScan,
reference: &CliSessionRef,
) -> anyhow::Result<CliSession> {
let id = reference.id.as_str();
for limit in [LISTED, usize::MAX] {
let sessions = adapter.cli_sessions(scan, limit)?;
if let Some(exact) = sessions.iter().find(|session| session.id == id) {
return Ok(exact.clone());
}
let starting: Vec<&CliSession> = sessions
.iter()
.filter(|session| session.id.starts_with(id))
.collect();
match starting.as_slice() {
[one] => return Ok((*one).clone()),
[] => {}
many => anyhow::bail!(
"{reference} could be {} sessions. Type more of its id, or pick it in /resume.",
many.len()
),
}
if sessions.len() < limit {
break;
}
}
anyhow::bail!(
"{} has no session {id} for this repo. /resume lists them.",
adapter.display_name()
)
}
pub(super) fn short_id(id: &str) -> &str {
id.get(..8).unwrap_or(id)
}