use std::io::Write;
use std::process::Command as StdCommand;
use anyhow::Result;
use crossterm::event::{Event, KeyEvent};
use crossterm::terminal;
use crate::display::input::{InputAction, InputHandler};
use crate::display::renderer::Renderer;
use crate::event::{AppEvent, InputMode};
use crate::fork::{self, ForkConfig};
use crate::handle_inbound;
use crate::protocol::types::InboundEvent;
use crate::session::runner::{SessionConfig, SessionRunner};
use crate::session::state::{SessionState, SessionStatus};
use crate::vcr::{Io, IoEvent, VcrContext};
pub enum SessionOutcome {
Completed { result_text: String },
Interrupted,
ProcessExited,
}
struct SessionLocals {
event_buffer: Vec<AppEvent>,
pending_followups: Vec<String>,
result_text: String,
}
pub async fn run_session<W: Write>(
runner: &mut SessionRunner,
state: &mut SessionState,
renderer: &mut Renderer<W>,
input: &mut InputHandler,
io: &mut Io,
vcr: &VcrContext,
fork_config: Option<&ForkConfig>,
) -> Result<SessionOutcome> {
let mut locals = SessionLocals {
event_buffer: Vec::new(),
pending_followups: Vec::new(),
result_text: String::new(),
};
loop {
let io_event: IoEvent = vcr
.call("next_event", (), async |(): &()| io.next_event().await)
.await?;
match io_event {
IoEvent::Claude(app_event) => {
if input.is_active() && state.status == SessionStatus::Running {
locals.event_buffer.push(app_event);
} else {
let outcome = process_claude_event(
app_event,
state,
renderer,
runner,
&mut locals,
vcr,
fork_config,
)
.await?;
if let Some(outcome) = outcome {
return Ok(outcome);
}
}
}
IoEvent::Terminal(Event::Key(key_event)) => {
let action = handle_session_key_event(
&key_event,
input,
renderer,
runner,
state,
&mut locals,
vcr,
)
.await?;
match action {
LoopAction::Continue => {}
LoopAction::Return(outcome) => return Ok(outcome),
}
}
IoEvent::Terminal(_) => {}
}
}
}
enum LoopAction {
Continue,
Return(SessionOutcome),
}
enum FlushResult {
Continue,
Followup(String),
Completed(String),
ProcessExited,
}
async fn handle_session_key_event<W: Write>(
key_event: &KeyEvent,
input: &mut InputHandler,
renderer: &mut Renderer<W>,
runner: &mut SessionRunner,
state: &mut SessionState,
locals: &mut SessionLocals,
vcr: &VcrContext,
) -> Result<LoopAction> {
let action = input.handle_key(key_event);
match action {
InputAction::Activated(c) => {
renderer.begin_input_line();
renderer.write_raw(&c.to_string());
}
InputAction::Submit(text, mode) => {
let flush = flush_event_buffer(locals, state, renderer);
if let Some(action) = handle_flush_result(flush, state, renderer, runner, vcr).await? {
return Ok(action);
}
match mode {
InputMode::Steering => {
renderer.render_steering_sent(&text);
vcr.call("send_message", text, async |t: &String| {
runner.send_message(t).await
})
.await?;
}
InputMode::FollowUp => {
if state.status == SessionStatus::WaitingForInput {
renderer.render_user_message(&text);
state.suppress_next_separator = true;
vcr.call("send_message", text, async |t: &String| {
runner.send_message(t).await
})
.await?;
state.status = SessionStatus::Running;
} else {
renderer.render_followup_queued(&text);
locals.pending_followups.push(text);
}
}
}
}
InputAction::ViewMessage(ref query) => {
view_message(renderer, query);
let flush = flush_event_buffer(locals, state, renderer);
if let FlushResult::Completed(ref result_text) = flush {
return Ok(LoopAction::Return(SessionOutcome::Completed {
result_text: result_text.clone(),
}));
}
if let Some(action) = handle_flush_result(flush, state, renderer, runner, vcr).await? {
return Ok(action);
}
}
InputAction::Interrupt => {
runner.kill().await?;
return Ok(LoopAction::Return(SessionOutcome::Interrupted));
}
InputAction::EndSession => {
runner.close_input();
}
InputAction::Cancel => {
let flush = flush_event_buffer(locals, state, renderer);
if let FlushResult::Completed(ref result_text) = flush {
return Ok(LoopAction::Return(SessionOutcome::Completed {
result_text: result_text.clone(),
}));
}
if let Some(action) = handle_flush_result(flush, state, renderer, runner, vcr).await? {
return Ok(action);
}
if state.status == SessionStatus::WaitingForInput {
renderer.show_prompt();
input.activate();
}
}
InputAction::None => {}
}
Ok(LoopAction::Continue)
}
async fn process_claude_event<W: Write>(
event: AppEvent,
state: &mut SessionState,
renderer: &mut Renderer<W>,
runner: &mut SessionRunner,
locals: &mut SessionLocals,
vcr: &VcrContext,
fork_config: Option<&ForkConfig>,
) -> Result<Option<SessionOutcome>> {
match event {
AppEvent::Claude(inbound) => {
if let InboundEvent::Result(ref result) = *inbound {
locals.result_text.clone_from(&result.result);
}
let fork_tasks = if vcr.is_live() {
if let InboundEvent::Result(_) = *inbound {
fork_config.and_then(|_| fork::parse_fork_tag(&locals.result_text))
} else {
None
}
} else {
None
};
let has_pending = !locals.pending_followups.is_empty() || fork_tasks.is_some();
handle_inbound(&inbound, state, renderer, has_pending);
if let Some(tasks) = fork_tasks {
let session_id = state.session_id.clone().unwrap_or_default();
let Some(fork_cfg) = fork_config else {
unreachable!("fork_tasks set without fork_config");
};
let msg = fork::run_fork(&session_id, tasks, fork_cfg, renderer).await?;
runner.send_message(&msg).await?;
state.suppress_next_separator = true;
state.status = SessionStatus::Running;
return Ok(None);
}
if matches!(*inbound, InboundEvent::Result(_)) {
if locals.pending_followups.is_empty() {
return Ok(Some(SessionOutcome::Completed {
result_text: locals.result_text.clone(),
}));
}
let text = locals.pending_followups.remove(0);
renderer.render_followup_sent(&text);
state.suppress_next_separator = true;
vcr.call("send_message", text, async |t: &String| {
runner.send_message(t).await
})
.await?;
state.status = SessionStatus::Running;
}
}
AppEvent::ParseWarning(warning) => {
renderer.render_warning(&warning);
}
AppEvent::ProcessExit(code) => {
renderer.render_exit(code);
state.status = SessionStatus::Ended;
return Ok(Some(SessionOutcome::ProcessExited));
}
}
Ok(None)
}
fn flush_event_buffer<W: Write>(
locals: &mut SessionLocals,
state: &mut SessionState,
renderer: &mut Renderer<W>,
) -> FlushResult {
let mut result = FlushResult::Continue;
for event in locals.event_buffer.drain(..) {
match event {
AppEvent::Claude(inbound) => {
let has_pending = !locals.pending_followups.is_empty();
if let InboundEvent::Result(ref r) = *inbound {
locals.result_text.clone_from(&r.result);
}
handle_inbound(&inbound, state, renderer, has_pending);
if matches!(*inbound, InboundEvent::Result(_)) {
if has_pending {
let text = locals.pending_followups.remove(0);
result = FlushResult::Followup(text);
} else {
result = FlushResult::Completed(locals.result_text.clone());
}
}
}
AppEvent::ParseWarning(warning) => {
renderer.render_warning(&warning);
}
AppEvent::ProcessExit(code) => {
renderer.render_exit(code);
state.status = SessionStatus::Ended;
result = FlushResult::ProcessExited;
}
}
}
result
}
async fn handle_flush_result<W: Write>(
flush: FlushResult,
state: &mut SessionState,
renderer: &mut Renderer<W>,
runner: &mut SessionRunner,
vcr: &VcrContext,
) -> Result<Option<LoopAction>> {
match flush {
FlushResult::ProcessExited => Ok(Some(LoopAction::Return(SessionOutcome::ProcessExited))),
FlushResult::Followup(text) => {
renderer.render_followup_sent(&text);
state.suppress_next_separator = true;
vcr.call("send_message", text, async |t: &String| {
runner.send_message(t).await
})
.await?;
state.status = SessionStatus::Running;
Ok(None)
}
FlushResult::Completed(_) | FlushResult::Continue => Ok(None),
}
}
pub enum FollowUpAction {
Sent,
Exit,
}
pub async fn wait_for_followup<W: Write>(
input: &mut InputHandler,
renderer: &mut Renderer<W>,
runner: &mut SessionRunner,
state: &mut SessionState,
io: &mut Io,
vcr: &VcrContext,
) -> Result<FollowUpAction> {
match wait_for_text_input(input, renderer, io, vcr).await? {
Some(text) => {
state.suppress_next_separator = true;
vcr.call("send_message", text, async |t: &String| {
runner.send_message(t).await
})
.await?;
state.status = SessionStatus::Running;
Ok(FollowUpAction::Sent)
}
None => Ok(FollowUpAction::Exit),
}
}
pub async fn wait_for_user_input<W: Write>(
input: &mut InputHandler,
renderer: &mut Renderer<W>,
io: &mut Io,
vcr: &VcrContext,
) -> Result<Option<String>> {
wait_for_text_input(input, renderer, io, vcr).await
}
async fn wait_for_text_input<W: Write>(
input: &mut InputHandler,
renderer: &mut Renderer<W>,
io: &mut Io,
vcr: &VcrContext,
) -> Result<Option<String>> {
renderer.show_prompt();
input.activate();
loop {
let io_event: IoEvent = vcr
.call("next_event", (), async |(): &()| io.next_event().await)
.await?;
match io_event {
IoEvent::Terminal(Event::Key(key_event)) => {
let action = input.handle_key(&key_event);
match action {
InputAction::Submit(text, _) => {
renderer.render_user_message(&text);
return Ok(Some(text));
}
InputAction::ViewMessage(ref query) => {
view_message(renderer, query);
}
InputAction::Cancel => {
renderer.show_prompt();
input.activate();
}
InputAction::Interrupt | InputAction::EndSession => {
return Ok(None);
}
InputAction::Activated(c) => {
renderer.begin_input_line();
renderer.write_raw(&c.to_string());
}
InputAction::None => {}
}
}
IoEvent::Claude(AppEvent::ProcessExit(_)) => return Ok(None),
IoEvent::Terminal(_) | IoEvent::Claude(_) => {}
}
}
}
pub async fn spawn_session(
config: SessionConfig,
io: &mut Io,
vcr: &VcrContext,
) -> Result<SessionRunner> {
vcr.call("spawn", config, async |c: &SessionConfig| {
let tx = io.replace_event_channel();
SessionRunner::spawn(c.clone(), tx).await
})
.await
}
pub fn view_message<W: Write>(renderer: &mut Renderer<W>, query: &str) {
use crate::display::renderer::format_message;
let Some(content) = format_message(renderer.messages(), query) else {
renderer.write_raw(&format!("No message {query}\r\n"));
return;
};
terminal::disable_raw_mode().ok();
let pager = std::env::var("PAGER").unwrap_or_else(|_| "less".to_string());
let mut child = StdCommand::new(&pager)
.arg("-R") .stdin(std::process::Stdio::piped())
.spawn();
if let Ok(ref mut child) = child {
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(content.as_bytes()).ok();
}
child.stdin.take();
child.wait().ok();
}
unsafe { libc::tcflush(libc::STDIN_FILENO, libc::TCIFLUSH) };
terminal::enable_raw_mode().ok();
}