Diff wrapping: width-aware patch summary rendering with hanging indent\n\n- Add PatchSummaryCell with custom render to compute wrapping using the actual area width\n- Refactor diff_render: expose create_diff_summary_with_width + render_patch_details_with_width\n- Use conservative width fallback when exact width unavailable\n- Keep existing DiffCell; only patch summary/details affected\n- Derive Clone+Copy for PatchEventType

This commit is contained in:
James Peter
2025-08-25 09:25:35 +10:00
parent 41b7273054
commit 03beb321d5
2 changed files with 125 additions and 39 deletions
+53 -26
View File
@@ -30,6 +30,18 @@ pub(super) fn create_diff_summary(
title: &str,
changes: &HashMap<PathBuf, FileChange>,
event_type: PatchEventType,
) -> Vec<RtLine<'static>> {
create_diff_summary_with_width(title, changes, event_type, None)
}
/// Same as `create_diff_summary` but allows specifying a target content width in columns.
/// When `width_cols` is provided, wrapping for detailed diff lines uses that width to
/// ensure hanging indents align within the caller’s render area.
pub(super) fn create_diff_summary_with_width(
title: &str,
changes: &HashMap<PathBuf, FileChange>,
event_type: PatchEventType,
width_cols: Option<usize>,
) -> Vec<RtLine<'static>> {
struct FileSummary {
display_path: String,
@@ -172,17 +184,32 @@ pub(super) fn create_diff_summary(
);
if show_details {
out.extend(render_patch_details(changes));
out.extend(render_patch_details_with_width(changes, width_cols));
}
out
}
#[allow(dead_code)]
fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'static>> {
pub(super) fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'static>> {
render_patch_details_with_width(changes, None)
}
#[allow(dead_code)]
fn render_patch_details_with_width(
changes: &HashMap<PathBuf, FileChange>,
width_cols: Option<usize>,
) -> Vec<RtLine<'static>> {
let mut out: Vec<RtLine<'static>> = Vec::new();
// Use terminal width or a reasonable fallback
let term_cols: usize = terminal::size().map(|(w, _)| w as usize).unwrap_or(120);
// Use caller-provided width or fall back to a conservative estimate based on terminal width.
// Subtract a gutter safety margin so our pre-wrapping rarely exceeds the
// actual chat content width (prevents secondary wrapping that breaks hanging indents).
let term_cols: usize = if let Some(w) = width_cols {
w as usize
} else {
let full = terminal::size().map(|(w, _)| w as usize).unwrap_or(120);
full.saturating_sub(20).max(40)
};
for (index, (path, change)) in changes.iter().enumerate() {
let is_first_file = index == 0;
@@ -197,7 +224,7 @@ fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'s
FileChange::Add { content } => {
for (i, raw) in content.lines().enumerate() {
let ln = i + 1;
out.extend(push_wrapped_diff_line(
out.extend(push_wrapped_diff_line_with_width(
ln,
DiffLineType::Insert,
raw,
@@ -209,7 +236,7 @@ fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'s
let original = std::fs::read_to_string(path).unwrap_or_default();
for (i, raw) in original.lines().enumerate() {
let ln = i + 1;
out.extend(push_wrapped_diff_line(
out.extend(push_wrapped_diff_line_with_width(
ln,
DiffLineType::Delete,
raw,
@@ -240,32 +267,32 @@ fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'s
match l {
diffy::Line::Insert(text) => {
let s = text.trim_end_matches('\n');
out.extend(push_wrapped_diff_line(
new_ln,
DiffLineType::Insert,
s,
term_cols,
));
out.extend(push_wrapped_diff_line_with_width(
new_ln,
DiffLineType::Insert,
s,
term_cols,
));
new_ln += 1;
}
diffy::Line::Delete(text) => {
let s = text.trim_end_matches('\n');
out.extend(push_wrapped_diff_line(
old_ln,
DiffLineType::Delete,
s,
term_cols,
));
out.extend(push_wrapped_diff_line_with_width(
old_ln,
DiffLineType::Delete,
s,
term_cols,
));
old_ln += 1;
}
diffy::Line::Context(text) => {
let s = text.trim_end_matches('\n');
out.extend(push_wrapped_diff_line(
new_ln,
DiffLineType::Context,
s,
term_cols,
));
out.extend(push_wrapped_diff_line_with_width(
new_ln,
DiffLineType::Context,
s,
term_cols,
));
old_ln += 1;
new_ln += 1;
}
@@ -292,7 +319,7 @@ pub(super) fn create_diff_details_only(
}
#[allow(dead_code)]
fn push_wrapped_diff_line(
fn push_wrapped_diff_line_with_width(
line_number: usize,
kind: DiffLineType,
text: &str,
@@ -545,7 +572,7 @@ mod tests {
// Call the wrapping function directly so we can precisely control the width
// Use a fixed width for testing wrapping behavior
const TEST_WRAP_WIDTH: usize = 80;
let lines = push_wrapped_diff_line(1, DiffLineType::Insert, long_line, TEST_WRAP_WIDTH);
let lines = push_wrapped_diff_line_with_width(1, DiffLineType::Insert, long_line, TEST_WRAP_WIDTH);
// Render into a small terminal to capture the visual layout
snapshot_lines(
+72 -13
View File
@@ -12,7 +12,7 @@ use codex_core::plan_tool::PlanItemArg;
use codex_core::plan_tool::StepStatus;
use codex_core::plan_tool::UpdatePlanArgs;
use codex_core::protocol::FileChange;
use crate::diff_render::create_diff_summary;
use crate::diff_render::create_diff_summary_with_width;
use codex_core::protocol::McpInvocation;
use codex_core::protocol::SessionConfiguredEvent;
use codex_core::protocol::TokenUsage;
@@ -45,6 +45,7 @@ pub(crate) struct CommandOutput {
pub(crate) stderr: String,
}
#[derive(Clone, Copy)]
pub(crate) enum PatchEventType {
ApprovalRequest,
ApplyBegin { auto_approved: bool },
@@ -2801,21 +2802,16 @@ pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlainHistoryCell {
pub(crate) fn new_patch_event(
event_type: PatchEventType,
changes: HashMap<PathBuf, FileChange>,
) -> PlainHistoryCell {
) -> PatchSummaryCell {
let title = match event_type {
PatchEventType::ApprovalRequest => "proposed patch",
PatchEventType::ApplyBegin { auto_approved: true } => "Updating...",
PatchEventType::ApplyBegin { auto_approved: false } => "Updating...",
PatchEventType::ApprovalRequest => "proposed patch".to_string(),
PatchEventType::ApplyBegin { .. } => "Updating...".to_string(),
};
let lines: Vec<Line<'static>> = create_diff_summary(title, &changes, event_type)
.into_iter()
.collect();
let kind = match title {
"proposed patch" => HistoryCellType::Patch { kind: PatchKind::Proposed },
_ => HistoryCellType::Patch { kind: PatchKind::ApplyBegin },
let kind = match event_type {
PatchEventType::ApprovalRequest => PatchKind::Proposed,
PatchEventType::ApplyBegin { .. } => PatchKind::ApplyBegin,
};
PlainHistoryCell { lines, kind }
PatchSummaryCell { title, changes, event_type, kind }
}
pub(crate) fn new_patch_apply_failure(stderr: String) -> PlainHistoryCell {
@@ -2832,6 +2828,69 @@ pub(crate) fn new_patch_apply_failure(stderr: String) -> PlainHistoryCell {
PlainHistoryCell { lines, kind: HistoryCellType::Patch { kind: PatchKind::ApplyFailure } }
}
// ==================== PatchSummaryCell ====================
// Renders patch summary + details with width-aware hanging indents so wrapped
// diff lines align under their code indentation.
pub(crate) struct PatchSummaryCell {
pub(crate) title: String,
pub(crate) changes: HashMap<PathBuf, FileChange>,
pub(crate) event_type: PatchEventType,
pub(crate) kind: PatchKind,
}
impl HistoryCell for PatchSummaryCell {
fn as_any(&self) -> &dyn std::any::Any { self }
fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
fn kind(&self) -> HistoryCellType { HistoryCellType::Patch { kind: self.kind } }
// We compute lines based on width at render time; provide a conservative
// default for non-width callers (not normally used in our pipeline).
fn display_lines(&self) -> Vec<Line<'static>> {
create_diff_summary_with_width(&self.title, &self.changes, self.event_type, Some(80))
.into_iter()
.collect()
}
fn has_custom_render(&self) -> bool { true }
fn desired_height(&self, width: u16) -> u16 {
let lines: Vec<Line<'static>> = create_diff_summary_with_width(
&self.title,
&self.changes,
self.event_type,
Some(width as usize),
)
.into_iter()
.collect();
Paragraph::new(Text::from(lines))
.wrap(Wrap { trim: false })
.line_count(width)
.try_into()
.unwrap_or(0)
}
fn custom_render_with_skip(&self, area: Rect, buf: &mut Buffer, skip_rows: u16) {
let lines: Vec<Line<'static>> = create_diff_summary_with_width(
&self.title,
&self.changes,
self.event_type,
Some(area.width as usize),
)
.into_iter()
.collect();
let text = Text::from(lines);
let bg_block = Block::default().style(Style::default().bg(crate::colors::background()));
Paragraph::new(text)
.block(bg_block)
.wrap(Wrap { trim: false })
.scroll((skip_rows, 0))
.style(Style::default().bg(crate::colors::background()))
.render(area, buf);
}
}
// new_patch_apply_success was removed in favor of in-place header mutation and type update in chatwidget
// ==================== Spacing Helper ====================