approvals: ship raw diff text instead of pre-rendered html; client classifies per-line

This commit is contained in:
müde 2026-05-17 12:30:45 +02:00
parent fb669c17c8
commit d48cee7c2d
3 changed files with 29 additions and 37 deletions

View file

@ -3,7 +3,6 @@
//! repo, plus approve/deny buttons), and the manager.
use std::convert::Infallible;
use std::fmt::Write as _;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
@ -256,8 +255,13 @@ struct ApprovalView {
kind: &'static str,
/// First 12 chars of the `commit_ref`, for `ApplyCommit` only.
sha_short: Option<String>,
/// Pre-rendered syntax-coloured diff HTML, for `ApplyCommit` only.
diff_html: Option<String>,
/// Raw unified diff text, for `ApplyCommit` only. The client splits
/// on `\n` and per-line classifies (`+` / `-` / `@@` / `--- ` / `+++ `
/// → diff-add / diff-del / diff-hunk / diff-file). Shipping raw
/// instead of pre-rendered HTML saves bytes on the wire (no
/// per-line `<span>` markup) and removes the only HTML-escape
/// surface from the snapshot.
diff: Option<String>,
/// Manager-supplied description shown on the approval card.
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
@ -639,7 +643,7 @@ async fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
agent: a.agent.clone(),
kind: "apply_commit",
sha_short: Some(sha),
diff_html: Some(render_diff_lines(&diff)),
diff: Some(diff),
description: a.description,
}
}
@ -648,7 +652,7 @@ async fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
agent: a.agent,
kind: "spawn",
sha_short: None,
diff_html: None,
diff: None,
description: a.description,
},
});
@ -1345,29 +1349,6 @@ fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<Approval> {
.collect()
}
/// Render a unified diff with per-line CSS classes so the dashboard can
/// colour adds / dels / hunk headers / context. Each line becomes a
/// `<span>` tagged by its leading character; the wrapping `<pre>` keeps
/// whitespace intact.
fn render_diff_lines(diff: &str) -> String {
let mut out = String::new();
for raw in diff.lines() {
let cls = match raw.as_bytes().first() {
// file headers (`--- a/...` / `+++ b/...`) come before any
// line starting with a single `+`/`-`. similar-rs emits them
// with the doubled prefix.
_ if raw.starts_with("--- ") => "diff-file",
_ if raw.starts_with("+++ ") => "diff-file",
Some(b'@') => "diff-hunk",
Some(b'+') => "diff-add",
Some(b'-') => "diff-del",
_ => "diff-ctx",
};
let _ = writeln!(out, "<span class=\"{cls}\">{}</span>", html_escape(raw),);
}
out
}
/// Host-side mirror of `hive_ag3nt::login::has_session`. Returns true if the
/// agent's bound `~/.claude/` dir on disk contains any regular file. The
/// dashboard reads this each render so logins driven from the agent web UI
@ -1415,8 +1396,3 @@ async fn git_diff_main_to(applied_dir: &Path, target_ref: &str) -> Result<String
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
fn html_escape(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}