use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static TABLE_SEPARATOR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^[-─]+\n?").unwrap());
static RUN_META_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"\b(ubuntu-latest|ubuntu-22\.04|ubuntu-20\.04|macos-latest|macos-14|windows-latest)\b",
)
.unwrap()
});
static JSON_BLANK_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"(?m)^\s+"[a-z_]+": null,?\n?"#).unwrap());
pub fn compress_pr_list(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = TABLE_SEPARATOR_RE.replace_all(&cleaned, "");
let kept: Vec<&str> = s
.lines()
.filter(|l| !l.trim().is_empty())
.take(30)
.collect();
kept.join("\n")
}
pub fn compress_pr_view(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().collect();
if lines.len() > 60 {
let head = &lines[..20];
let tail = &lines[lines.len() - 10..];
let omitted = lines.len() - 30;
return format!(
"{}\n... [{} more lines omitted] ...\n{}",
head.join("\n"),
omitted,
tail.join("\n")
);
}
cleaned
}
pub fn compress_pr_checks(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
let has_failure = lines.iter().any(|l| {
let t = l.to_lowercase();
t.contains("fail") || t.contains("error") || t.contains("✗") || t.contains("×")
});
if has_failure {
let failing: Vec<&str> = lines
.iter()
.copied()
.filter(|l| {
let t = l.to_lowercase();
t.contains("fail") || t.contains("error") || t.contains("✗") || t.contains("×")
})
.collect();
let pass_count = lines.len().saturating_sub(failing.len());
let mut result = failing;
if pass_count > 0 {
result.push("… (passing checks hidden)");
}
return result.join("\n");
}
lines.join("\n")
}
pub fn compress_issue_list(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = TABLE_SEPARATOR_RE.replace_all(&cleaned, "");
let kept: Vec<&str> = s
.lines()
.filter(|l| !l.trim().is_empty())
.take(30)
.collect();
kept.join("\n")
}
pub fn compress_run_list(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = RUN_META_RE.replace_all(&cleaned, "");
let sha_re = once_cell::sync::Lazy::force(&RUN_SHA_RE);
let s = sha_re.replace_all(&s, "");
let kept: Vec<&str> = s
.lines()
.filter(|l| !l.trim().is_empty())
.take(20)
.collect();
kept.join("\n")
}
pub fn compress_run_view(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().collect();
let job_lines: Vec<&str> = lines
.iter()
.copied()
.filter(|l| {
let t = l.trim();
!t.is_empty()
&& (t.contains('✓')
|| t.contains('✗')
|| t.contains('●')
|| t.contains("pass")
|| t.contains("fail")
|| t.contains("skip")
|| t.starts_with("Jobs:")
|| t.starts_with("Run ")
|| t.contains("Run ID")
|| t.contains("Branch:")
|| t.contains("Triggered")
|| t.starts_with("✓ ")
|| t.starts_with("✗ "))
})
.collect();
if job_lines.len() > 5 && job_lines.len() < lines.len() / 2 {
return job_lines.join("\n");
}
if lines.len() > 50 {
let omitted = lines.len() - 50;
return format!(
"{}\n… [{} more log lines] …",
lines[..50].join("\n"),
omitted
);
}
cleaned
}
pub fn compress_run_watch(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned
.lines()
.filter(|l| !l.trim().starts_with("Refreshing run status"))
.filter(|l| !l.trim().is_empty())
.collect();
if lines.is_empty() {
return "(run watch: no output)".to_string();
}
let start = lines.len().saturating_sub(10);
lines[start..].join("\n")
}
static RUN_SHA_RE: once_cell::sync::Lazy<regex::Regex> =
once_cell::sync::Lazy::new(|| regex::Regex::new(r"\b[0-9a-f]{7,40}\b").unwrap());
pub fn compress_api(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = JSON_BLANK_RE.replace_all(&cleaned, "");
let lines: Vec<&str> = s.lines().collect();
if lines.len() > 60 {
return format!(
"{}\n... [{} more lines — use --jq to filter] ...",
lines[..60].join("\n"),
lines.len() - 60
);
}
s.into_owned()
}
pub fn compress_repo(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().collect();
if lines.len() > 30 {
return lines[..30].join("\n");
}
cleaned
}
pub fn compress_gh(subcmd: &str, raw: &str) -> String {
let sub = subcmd.trim();
if sub.starts_with("pr list") || sub == "pr" {
return compress_pr_list(raw);
}
if sub.starts_with("pr view") {
return compress_pr_view(raw);
}
if sub.starts_with("pr checks") {
return compress_pr_checks(raw);
}
if sub.starts_with("issue list") || sub == "issue" {
return compress_issue_list(raw);
}
if sub.starts_with("run list") {
return compress_run_list(raw);
}
if sub.starts_with("run view") {
return compress_run_view(raw);
}
if sub.starts_with("run watch") {
return compress_run_watch(raw);
}
if sub.starts_with("api") {
return compress_api(raw);
}
if sub.starts_with("repo") {
return compress_repo(raw);
}
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() > 50 {
return format!(
"{}\n... [{} more lines] ...",
lines[..50].join("\n"),
lines.len() - 50
);
}
cleaned
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pr_list_keeps_entries() {
let raw =
"#42 Fix the bug OPEN user1\n#41 Update readme MERGED user2\n";
let out = compress_pr_list(raw);
assert!(out.contains("#42"), "{out}");
assert!(out.contains("#41"), "{out}");
}
#[test]
fn pr_list_truncates_at_30() {
let raw = (0..40)
.map(|i| format!("#{i} PR {i} OPEN user\n"))
.collect::<String>();
let out = compress_pr_list(raw.as_str());
assert!(!out.contains("#39"), "{out}");
}
#[test]
fn run_list_strips_runner_os() {
let raw = "completed success CI main ubuntu-latest 2m\n";
let out = compress_run_list(raw);
assert!(!out.contains("ubuntu-latest"), "{out}");
}
#[test]
fn api_truncates_large_response() {
let raw = (0..80)
.map(|i| format!(" \"field{i}\": \"value{i}\",\n"))
.collect::<String>();
let out = compress_api(&raw);
assert!(out.contains("more lines"), "{out}");
}
#[test]
fn run_list_strips_sha() {
let raw = "completed success CI main a3f9b2c 2m45s\ncompleted failure lint main deadbeef1234 1m10s\n";
let out = compress_run_list(raw);
assert!(!out.contains("a3f9b2c"), "{out}");
assert!(out.contains("success") || out.contains("failure"), "{out}");
}
#[test]
fn pr_checks_hides_passing_when_failures_exist() {
let raw = "✓ lint pass 30s\n✓ test pass 2m\n✗ deploy fail 10s\n";
let out = compress_pr_checks(raw);
assert!(out.contains("fail") || out.contains("✗"), "{out}");
assert!(out.contains("hidden") || !out.contains("✓ lint"), "{out}");
}
#[test]
fn pr_checks_passthrough_when_all_pass() {
let raw = "✓ lint pass 30s\n✓ test pass 2m\n";
let out = compress_pr_checks(raw);
assert!(out.contains("lint"), "{out}");
assert!(out.contains("test"), "{out}");
}
#[test]
fn run_watch_strips_polling_lines() {
let raw = "Refreshing run status every 3 seconds\nRefreshing run status every 3 seconds\n✓ CI completed\n";
let out = compress_run_watch(raw);
assert!(!out.contains("Refreshing"), "{out}");
assert!(out.contains("CI"), "{out}");
}
#[test]
fn run_view_keeps_job_status_lines() {
let raw = "Run ID: 123456\nBranch: main\n✓ build 2m30s\n✗ test 1m00s\nsome verbose log line\nanother log line\n";
let out = compress_run_view(raw);
assert!(out.contains("build") || out.contains("test"), "{out}");
}
}