fix(cli): render spec-required panels in agents plan rollback rich output #3279

Merged
freemo merged 1 commits from fix/plan-rollback-rich-output-panels into master 2026-04-05 17:59:07 +00:00
2 changed files with 85 additions and 9 deletions
+4 -1
View File
@@ -178,8 +178,11 @@ Feature: Plan CLI coverage round 2 remaining uncovered lines
And a mocked checkpoint service for r2cov rollback that succeeds
When I invoke r2cov plan rollback with --yes
Then the r2cov command should exit normally
And the r2cov output should contain "Rollback Summary"
And the r2cov output should contain "Changes Reverted"
And the r2cov output should contain "Impact"
And the r2cov output should contain "Post-Rollback State"
And the r2cov output should contain "Rollback complete"
And the r2cov output should contain "Restored files"
Scenario: Rollback plan succeeds in json format
Given a r2cov CLI runner
+81 -8
View File
@@ -3422,16 +3422,89 @@ def rollback_plan(
if fmt != OutputFormat.RICH.value:
console.print(format_output(data, fmt))
else:
# Rollback Summary panel
label = getattr(result, "label", None) or ""
label_line = f"\n[bold]Label:[/bold] {label}" if label else ""
console.print(
f"[green]Rollback complete.[/green]\n"
f" Plan: {plan_id}\n"
f" Checkpoint: {result.from_checkpoint_id}\n"
f" Restored files: {result.restored_files_count}\n"
f" Elapsed: {elapsed:.3f}s\n"
f" Changed paths:"
Panel(
f"[bold]Plan:[/bold] {plan_id}\n"
f"[bold]Checkpoint:[/bold] {result.from_checkpoint_id}"
f"{label_line}\n"
f"[bold]Files:[/bold] {result.restored_files_count} reverted",
title="Rollback Summary",
expand=False,
)
)
for path in result.changed_paths:
console.print(f" {path}")
# Changes Reverted table
changes_table = Table(show_header=True, expand=False)
changes_table.add_column("File", style="cyan")
changes_table.add_column("Action", style="green")
changes_reverted = getattr(result, "changes_reverted", None)
if changes_reverted and isinstance(changes_reverted, list):
for entry in changes_reverted:
if isinstance(entry, dict):
changes_table.add_row(
str(entry.get("file", "")),
str(entry.get("action", "restored")),
)
else:
changes_table.add_row(str(entry), "restored")
else:
for path in result.changed_paths:
changes_table.add_row(str(path), "restored")
console.print(Panel(changes_table, title="Changes Reverted", expand=False))
# Impact panel
child_plans_invalidated = getattr(result, "child_plans_invalidated", None)
sandbox_state = getattr(result, "sandbox", None) or (
f"restored to {result.from_checkpoint_id}"
)
decisions_after_cp = getattr(result, "decisions_after_cp", None)
tool_calls_after_cp = getattr(result, "tool_calls_after_cp", None)
impact_lines = []
if child_plans_invalidated is not None:
impact_lines.append(
f"[bold]Child Plans Invalidated:[/bold] {child_plans_invalidated}"
)
impact_lines.append(f"[bold]Sandbox:[/bold] {sandbox_state}")
if decisions_after_cp is not None:
impact_lines.append(
f"[bold]Decisions After CP:[/bold] {decisions_after_cp} discarded"
)
if tool_calls_after_cp is not None:
impact_lines.append(
f"[bold]Tool Calls After CP:[/bold] {tool_calls_after_cp} undone"
)
console.print(
Panel(
"\n".join(impact_lines),
title="Impact",
expand=False,
)
)
# Post-Rollback State panel
phase = getattr(result, "phase", None) or "execute"
state = getattr(result, "state", None) or "queued"
checkpoints_remaining = getattr(result, "checkpoints_remaining", None)
post_rollback_lines = [
f"[bold]Phase:[/bold] {phase}",
f"[bold]State:[/bold] {state}",
]
if checkpoints_remaining is not None:
post_rollback_lines.append(
f"[bold]Checkpoints Remaining:[/bold] {checkpoints_remaining}"
)
console.print(
Panel(
"\n".join(post_rollback_lines),
title="Post-Rollback State",
expand=False,
)
)
console.print("[green]\u2713 OK[/green] Rollback complete")
except BusinessRuleViolation as e:
console.print(f"[red]Rollback blocked:[/red] {e.message}")