use std::collections::HashMap;
use crate::risk::Risk;
use crate::tui::event::{DiffData, PermOption};
use agent_client_protocol::schema::v1::StopReason;
use bitrouter_substrate::translate::UsageCost;
use super::diff::{Line, diff_lines, parse_rendered_diff};
use super::{SCROLLBACK_CAP, TICKS_PER_SEC, fmt_elapsed};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TailKind {
Message,
Thought,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingView {
pub title: String,
pub diff: Option<DiffData>,
pub options: Vec<PermOption>,
pub risk: Risk,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Autonomy {
#[default]
Manual,
Assisted,
Auto,
}
impl Autonomy {
pub(super) fn next(self) -> Self {
match self {
Autonomy::Manual => Autonomy::Assisted,
Autonomy::Assisted => Autonomy::Auto,
Autonomy::Auto => Autonomy::Manual,
}
}
pub fn label(self) -> &'static str {
match self {
Autonomy::Manual => "manual",
Autonomy::Assisted => "assisted",
Autonomy::Auto => "auto",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PaneKind {
#[default]
Monitor,
Pty,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Ownership {
#[default]
Human,
Orchestrator,
}
#[derive(Debug, Clone)]
pub struct PaneState {
pub record_id: String,
pub agent_id: String,
pub harness: String,
pub lines: Vec<Line>,
pub pending: Option<PendingView>,
pub exited: bool,
pub attention: bool,
pub done: bool,
pub(super) since: u64,
pub(super) last_bucket: u8,
pub autonomy: Autonomy,
pub pending_seq: u64,
pub scroll: Option<usize>,
pub viewport: usize,
pub usage: Option<(u64, u64)>,
pub cost: Option<UsageCost>,
pub tail: Option<(TailKind, String)>,
code_lang: Option<String>,
tool_diffs: HashMap<String, String>,
pub turn_active: bool,
pub last_stop: Option<StopReason>,
pub port: Option<u16>,
pub review: Option<(u64, u64, u64)>,
pub check_retries: u8,
pub kind: PaneKind,
pub owner: Ownership,
pub model: Option<String>,
}
impl PaneState {
pub fn new(record_id: String, agent_id: String) -> Self {
Self {
record_id,
agent_id,
harness: String::new(),
lines: Vec::new(),
pending: None,
exited: false,
attention: false,
done: false,
since: 0,
last_bucket: u8::MAX,
autonomy: Autonomy::default(),
pending_seq: 0,
scroll: None,
viewport: 0,
usage: None,
cost: None,
tail: None,
code_lang: None,
tool_diffs: HashMap::new(),
turn_active: false,
last_stop: None,
port: None,
review: None,
check_retries: 0,
kind: PaneKind::default(),
owner: Ownership::default(),
model: None,
}
}
pub fn is_session(&self) -> bool {
self.kind == PaneKind::Pty && !self.record_id.starts_with("attach:")
}
pub(super) fn push(&mut self, line: Line) {
self.lines.push(line);
if self.lines.len() > SCROLLBACK_CAP {
let overflow = self.lines.len() - SCROLLBACK_CAP;
self.lines.drain(0..overflow);
if let Some(s) = &mut self.scroll {
*s = s.saturating_sub(overflow);
}
}
}
pub(super) fn push_external(&mut self, line: Line) {
self.flush_tail();
self.push(line);
}
pub(super) fn flush_tail(&mut self) {
if let Some((kind, buf)) = self.tail.take()
&& !buf.is_empty()
{
self.commit_stream_line(kind, buf);
}
}
pub(super) fn stream(&mut self, kind: TailKind, text: &str) {
if let Some((k, _)) = &self.tail
&& *k != kind
{
self.flush_tail();
}
let mut buf = match self.tail.take() {
Some((_, b)) => b,
None => String::new(),
};
buf.push_str(text);
while let Some(pos) = buf.find('\n') {
let line: String = buf.drain(..=pos).collect();
let line = line
.trim_end_matches('\n')
.trim_end_matches('\r')
.to_string();
self.commit_stream_line(kind, line);
}
if !buf.is_empty() {
self.tail = Some((kind, buf));
}
}
fn commit_stream_line(&mut self, kind: TailKind, line: String) {
match kind {
TailKind::Thought => self.push(Line::Thought(line)),
TailKind::Message => {
let trimmed = line.trim();
if trimmed.starts_with("```") {
match self.code_lang.take() {
Some(_) => {} None => {
self.code_lang =
Some(trimmed.trim_start_matches('`').trim().to_string());
}
}
self.push(Line::Message(line));
} else if let Some(lang) = &self.code_lang {
let lang = lang.clone();
self.push(Line::Code { text: line, lang });
} else {
self.push(Line::Message(line));
}
}
}
}
pub(super) fn push_tool_diff(&mut self, id: &str, raw: &str) {
if self.tool_diffs.get(id).is_some_and(|prev| prev == raw) {
return;
}
self.tool_diffs.insert(id.to_string(), raw.to_string());
match parse_rendered_diff(raw) {
Some(data) => {
self.flush_tail();
for line in diff_lines(&data) {
self.push(line);
}
}
None => {
self.flush_tail();
for l in raw.lines() {
self.push(Line::Message(l.to_string()));
}
}
}
}
pub(super) fn scroll_page_up(&mut self) {
let page = self.viewport.max(1);
let tail_start = self.lines.len().saturating_sub(page);
let start_now = self.scroll.unwrap_or(tail_start);
self.scroll = Some(start_now.saturating_sub(page));
}
pub(super) fn scroll_page_down(&mut self) {
let page = self.viewport.max(1);
if let Some(s) = self.scroll {
let next = s + page;
if next + page >= self.lines.len() {
self.scroll = None; } else {
self.scroll = Some(next);
}
}
}
pub(super) fn bucket(&self) -> u8 {
if self.pending.is_some() {
0 } else if self.review.is_some() && !self.exited {
1 } else if self.attention {
2 } else if self.done && !self.exited {
3 } else if !self.exited && self.turn_active {
4 } else if !self.exited {
5 } else {
6 }
}
pub fn elapsed_label(&self, tick: u64) -> Option<String> {
if self.exited {
return None;
}
let watched = self.pending.is_some()
|| self.review.is_some()
|| self.attention
|| self.done
|| self.turn_active;
if !watched {
return None;
}
Some(fmt_elapsed(tick.saturating_sub(self.since) / TICKS_PER_SEC))
}
}