use anyhow::{Context, Result};
use futures::StreamExt;
use agent_client_protocol::schema::v1::SessionUpdate;
use bitrouter_sdk::acp::client::AcpClient;
use crate::chat::effects::Wire;
static SESSION_LOG: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
pub fn remember_session_log(path: std::path::PathBuf) {
let _ = SESSION_LOG.set(path);
}
fn write_session_log_tail(out: &mut impl std::io::Write) -> Result<()> {
let Some(path) = SESSION_LOG.get() else {
return Ok(());
};
let log = match std::fs::read_to_string(path) {
Ok(log) => log,
Err(e) => format!("(could not read the session log: {e})"),
};
bitrouter_tui::plain::write(
out,
&bitrouter_tui::log_tail::render(path, &log, bitrouter_tui::log_tail::TAIL_LINES),
)
.context("writing the session log tail")
}
pub(crate) fn report_failed_launch() {
let _ = write_session_log_tail(&mut std::io::stderr());
}
fn prompt_chunk(line: &str, nth: usize) -> agent_client_protocol::schema::v1::ContentChunk {
use agent_client_protocol::schema::v1::{ContentBlock, ContentChunk, MessageId, TextContent};
let mut chunk = ContentChunk::new(ContentBlock::Text(TextContent::new(line.to_string())));
chunk.message_id = Some(MessageId::from(format!("chat:prompt:{nth}")));
chunk
}
pub(crate) async fn chat_plain(
client: &AcpClient,
session_id: &str,
agent_id: &str,
recorder: Option<std::sync::Arc<bitrouter_telemetry::otel::acp::AcpSpanRecorder>>,
surface: crate::actions::session::SessionSurface<'_>,
) -> Result<()> {
use std::io::Write as _;
use futures::FutureExt as _;
use tokio::io::AsyncBufReadExt as _;
let crate::actions::session::SessionSurface {
commands,
prompt_commands,
ports,
} = surface;
let mut out = std::io::stdout();
let mut transcript = bitrouter_tui::plain::Transcript::default();
let policy = bitrouter_tui::permission::Policy::default();
let mut wire = Wire::new(client, session_id);
let mut prompts = 0_usize;
let mut updates = client.subscribe_raw_updates();
let mut permissions = client.subscribe_permissions();
let mut lines = tokio::io::BufReader::new(tokio::io::stdin()).lines();
loop {
let Some(line) = lines.next_line().await.context("reading stdin")? else {
break Ok(());
};
if line.trim().is_empty() {
continue;
}
let line = match bitrouter_tui::machine::resolve(&commands, &prompt_commands, &line) {
bitrouter_tui::machine::Resolution::Owned { action, .. } => {
let name = commands
.iter()
.find(|command| command.action == action)
.map_or(action, |command| command.name);
writeln!(out, "/{name} needs a terminal").context("writing to stdout")?;
continue;
}
bitrouter_tui::machine::Resolution::Action { action, args } => {
match ports.run(action, &args).await {
Ok(report) => out
.write_all(
&crate::output::Output::new(crate::output::Format::Human)
.render_to_vec(report.as_ref()),
)
.context("writing to stdout")?,
Err(error) => writeln!(out, "{error}").context("writing to stdout")?,
}
continue;
}
bitrouter_tui::machine::Resolution::Unavailable(reason) => {
writeln!(out, "{reason}").context("writing to stdout")?;
continue;
}
bitrouter_tui::machine::Resolution::Expand(prompt)
| bitrouter_tui::machine::Resolution::Prompt(prompt) => prompt,
};
prompts = prompts.saturating_add(1);
transcript.apply(SessionUpdate::UserMessageChunk(prompt_chunk(
&line, prompts,
)));
let started = std::time::Instant::now();
let turn = client.prompt(session_id, &line);
tokio::pin!(turn);
let outcome = loop {
tokio::select! {
update = updates.next() => if let Some(update) = update {
transcript.apply(update);
},
request = permissions.next() => if let Some(request) = request {
let (decision, prompt) = wire.answer(request, &policy).await;
writeln!(out, " permission {decision}: {}", prompt.title())
.context("writing the permission decision")?;
},
result = &mut turn => break result,
}
};
while let Some(Some(update)) = updates.next().now_or_never() {
transcript.apply(update);
}
bitrouter_tui::plain::write(&mut out, &transcript.unwritten())
.context("writing the session to stdout")?;
match outcome {
Ok(response) => {
crate::acp_cli::report_turn(
client,
agent_id,
recorder.as_ref(),
&response,
started.elapsed(),
);
writeln!(out, "[{:?}]", response.stop_reason).context("writing the stop reason")?
}
Err(e) => {
writeln!(out, "turn failed: {e}").context("writing the failure")?;
write_session_log_tail(&mut out)?;
}
}
}
}