use super::{OpEntry, Origin};
const SURFACE_STDERR_LINES: usize = 3;
const SUMMARY_ARGV_MAX: usize = 100;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct OpRow {
pub ts: String,
pub argv: String,
pub cwd: String,
pub exit: i32,
pub stdout: String,
pub stderr: String,
pub origin: Origin,
}
impl OpRow {
pub fn has_output(&self) -> bool {
!self.stdout.is_empty() || !self.stderr.is_empty()
}
pub fn summary(&self) -> String {
let flat: String = self
.argv
.chars()
.map(|c| if c == '\n' || c == '\r' { ' ' } else { c })
.collect();
if flat.chars().count() <= SUMMARY_ARGV_MAX {
flat
} else {
let head: String = flat.chars().take(SUMMARY_ARGV_MAX - 1).collect();
format!("{head}…")
}
}
pub fn when(&self) -> String {
self.ts
.parse::<i64>()
.map_or_else(|_| self.ts.clone(), crate::ui_state::iso8601_extended)
}
}
impl From<&OpEntry> for OpRow {
fn from(entry: &OpEntry) -> Self {
Self {
ts: entry.ts.clone(),
argv: entry.argv.join(" "),
cwd: entry.cwd.clone(),
exit: entry.exit,
stdout: entry.stdout.clone(),
stderr: entry.stderr.clone(),
origin: entry.origin,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SurfaceFailure {
pub argv: String,
pub stderr_tail: String,
}
impl From<&OpRow> for SurfaceFailure {
fn from(row: &OpRow) -> Self {
Self {
argv: row.argv.clone(),
stderr_tail: stderr_tail(&row.stderr),
}
}
}
pub(crate) fn stderr_tail(stderr: &str) -> String {
let lines: Vec<&str> = stderr.trim_end_matches('\n').lines().collect();
let start = lines.len().saturating_sub(SURFACE_STDERR_LINES);
lines.get(start..).unwrap_or_default().join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(exit: i32, stderr: &str) -> OpEntry {
OpEntry {
ts: "TS".into(),
argv: vec!["bl".into(), "close".into(), "bl-4db6".into()],
cwd: "/proj".into(),
exit,
stdout: "out".into(),
stderr: stderr.into(),
origin: Origin::default(),
}
}
#[test]
fn op_row_carries_the_full_entry_and_joins_argv() {
let row = OpRow::from(&entry(0, ""));
assert_eq!(row.ts, "TS");
assert_eq!(row.argv, "bl close bl-4db6");
assert_eq!(row.cwd, "/proj");
assert_eq!(row.exit, 0);
assert_eq!(row.stdout, "out");
assert_eq!(row.stderr, "");
}
#[test]
fn when_renders_iso8601_and_falls_back_on_a_non_numeric_ts() {
assert_eq!(OpRow::from(&entry(0, "")).when(), "TS");
let stamped = OpEntry {
ts: "1785630266".into(),
..entry(0, "")
};
assert_eq!(OpRow::from(&stamped).when(), "2026-08-02 00:24:26Z");
}
#[test]
fn has_output_reflects_either_stream() {
assert!(OpRow::from(&entry(0, "")).has_output()); let neither = OpEntry {
stdout: String::new(),
..entry(0, "")
};
assert!(!OpRow::from(&neither).has_output());
let only_err = OpEntry {
stdout: String::new(),
..entry(1, "boom")
};
assert!(OpRow::from(&only_err).has_output());
}
#[test]
fn summary_leaves_a_short_argv_unchanged() {
assert_eq!(OpRow::from(&entry(0, "")).summary(), "bl close bl-4db6");
}
#[test]
fn summary_folds_newlines_and_elides_a_long_multiline_goal() {
let goal = format!(
"identity preamble\n{}\nball worktree preamble",
"payload segment ".repeat(30)
);
assert!(goal.chars().count() >= 500, "fixture must exceed 500 chars");
let e = OpEntry {
argv: vec![goal.clone()],
..entry(0, "")
};
let row = OpRow::from(&e);
let summary = row.summary();
assert!(!summary.contains('\n') && !summary.contains('\r'));
assert_eq!(summary.chars().count(), SUMMARY_ARGV_MAX);
assert!(summary.ends_with('…'));
assert_eq!(row.argv, goal);
}
#[test]
fn summary_elision_boundary_is_exact() {
let at_cap = "a".repeat(SUMMARY_ARGV_MAX);
let e = OpEntry {
argv: vec![at_cap.clone()],
..entry(0, "")
};
assert_eq!(OpRow::from(&e).summary(), at_cap);
let over_cap = "a".repeat(SUMMARY_ARGV_MAX + 1);
let e = OpEntry {
argv: vec![over_cap],
..entry(0, "")
};
let summary = OpRow::from(&e).summary();
assert_eq!(summary.chars().count(), SUMMARY_ARGV_MAX);
assert!(summary.ends_with('…'));
}
#[test]
fn surface_failure_carries_argv_and_stderr_tail() {
let row = OpRow::from(&entry(2, "line1\nline2"));
let f = SurfaceFailure::from(&row);
assert_eq!(f.argv, "bl close bl-4db6");
assert_eq!(f.stderr_tail, "line1\nline2");
}
#[test]
fn stderr_tail_keeps_only_the_last_lines() {
assert_eq!(stderr_tail(""), "");
assert_eq!(stderr_tail("only\n"), "only");
assert_eq!(stderr_tail("a\nb\nc\nd\ne\n"), "c\nd\ne");
}
}