use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgentCli {
Claude,
Codex,
Gemini,
}
impl AgentCli {
pub fn as_str(self) -> &'static str {
match self {
AgentCli::Claude => "claude",
AgentCli::Codex => "codex",
AgentCli::Gemini => "gemini",
}
}
pub fn label(self) -> &'static str {
match self {
AgentCli::Claude => "Claude",
AgentCli::Codex => "Codex",
AgentCli::Gemini => "Gemini",
}
}
pub fn cycle(self) -> Self {
match self {
AgentCli::Claude => AgentCli::Codex,
AgentCli::Codex => AgentCli::Gemini,
AgentCli::Gemini => AgentCli::Claude,
}
}
}
#[derive(Clone, Debug)]
pub struct TermCell {
pub ch: String,
pub fg: [u8; 3],
pub bg: [u8; 3],
pub bold: bool,
}
impl Default for TermCell {
fn default() -> Self {
Self {
ch: " ".to_string(),
fg: [204, 204, 204],
bg: [0, 0, 0],
bold: false,
}
}
}
#[derive(Clone, Debug)]
pub struct TermGrid {
pub cells: Vec<Vec<TermCell>>,
pub cols: u16,
pub rows: u16,
pub cursor_row: u16,
pub cursor_col: u16,
pub cursor_visible: bool,
pub buddy: Option<BuddyArt>,
}
#[derive(Clone, Debug)]
pub struct BuddyArt {
pub rows: Vec<Vec<TermCell>>,
pub width: u16,
pub anchor_col: u16,
pub anchor_row: u16,
}
impl TermGrid {
pub fn empty(cols: u16, rows: u16) -> Self {
Self {
cells: vec![vec![TermCell::default(); cols as usize]; rows as usize],
cols,
rows,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
buddy: None,
}
}
pub fn detect_and_extract_buddy(&mut self) {
let cols = self.cols as usize;
let rows = self.rows as usize;
if cols < 12 || rows < 3 {
return;
}
let scan_width: usize = 30.min(cols / 2);
let gap: usize = 2;
let min_rows: usize = 2;
let scan_start = cols.saturating_sub(scan_width);
let mut chosen: Option<(usize, usize, usize)> = None;
for lc in scan_start..cols.saturating_sub(3) {
if lc < gap { continue; }
let mut left_blank = true;
for gc in (lc - gap)..lc {
for r in 0..rows {
if !is_blank_cell(&self.cells[r][gc]) {
left_blank = false;
break;
}
}
if !left_blank { break; }
}
if !left_blank { continue; }
let mut top: Option<usize> = None;
let mut bot: Option<usize> = None;
let mut total_filled = 0usize;
for r in 0..rows {
let row_has_content = (lc..cols).any(|c| !is_blank_cell(&self.cells[r][c]));
if row_has_content {
if top.is_none() { top = Some(r); }
bot = Some(r);
total_filled += (lc..cols).filter(|&c| !is_blank_cell(&self.cells[r][c])).count();
}
}
let (Some(top), Some(bot)) = (top, bot) else { continue; };
let span = bot - top + 1;
if span < min_rows { continue; }
if total_filled < 4 { continue; }
if span > 8 { continue; }
chosen = Some((lc, top, bot));
break;
}
let Some((lc, top, bot)) = chosen else { return; };
let width = (cols - lc) as u16;
let mut art_rows: Vec<Vec<TermCell>> = Vec::with_capacity(bot - top + 1);
for r in top..=bot {
let mut row_cells = Vec::with_capacity(width as usize);
for c in lc..cols {
row_cells.push(self.cells[r][c].clone());
self.cells[r][c] = TermCell::default();
}
art_rows.push(row_cells);
}
self.buddy = Some(BuddyArt {
rows: art_rows,
width,
anchor_col: lc as u16,
anchor_row: top as u16,
});
}
}
fn is_blank_cell(cell: &TermCell) -> bool {
cell.ch.is_empty() || cell.ch.chars().all(|c| c == ' ' || c == '\u{a0}')
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ChatRole {
User,
Assistant,
Tool,
Thinking,
Error,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: ChatRole,
pub content: String,
pub tool_name: Option<String>,
}
#[derive(Clone, Debug)]
pub enum AgentSnapshotMode {
Pty(TermGrid),
Chat(Vec<ChatMessage>),
Idle,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum LiveStatus {
#[default]
Idle,
Thinking,
RunningTool {
name: String,
done: u32,
},
}
#[derive(Clone, Debug)]
pub struct AgentRenderSnapshot {
pub mode: AgentSnapshotMode,
pub session_active: bool,
pub live_status: LiveStatus,
}