fix(cli): add Progress, Timing, Execution Detail, and Cost panels to agents plan status output #9461

Merged
HAL9000 merged 3 commits from fix/plan-status-missing-output-panels into master 2026-06-03 14:00:32 +00:00
3 changed files with 507 additions and 114 deletions
@@ -162,3 +162,87 @@ Feature: Plan lifecycle CLI coverage
Scenario: Plan execute shares lifecycle service instance with executor
When I run plan execute verifying lifecycle service sharing
Then the plan executor should receive the same lifecycle service instance
# ---- _print_lifecycle_plan five-panel output ----
Scenario: Plan status renders Plan Status panel with required fields
When I run plan status for a plan with all five panels
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Plan Status"
And the plan lifecycle output should contain "Processing State"
And the plan lifecycle output should contain "Projects"
And the plan lifecycle output should contain "Action"
And the plan lifecycle output should contain "Phase"
And the plan lifecycle output should contain "Attempt"
Scenario: Plan status renders Progress panel
When I run plan status for a plan with all five panels
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Progress"
And the plan lifecycle output should contain "Strategize"
And the plan lifecycle output should contain "Execute"
And the plan lifecycle output should contain "Apply"
Scenario: Plan status renders Timing panel
When I run plan status for a plan with all five panels
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Timing"
And the plan lifecycle output should contain "Started"
And the plan lifecycle output should contain "Elapsed"
And the plan lifecycle output should contain "ETA"
Scenario: Plan status renders Execution Detail panel
When I run plan status for a plan with all five panels
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Execution Detail"
And the plan lifecycle output should contain "Sandbox"
And the plan lifecycle output should contain "Tool Calls"
And the plan lifecycle output should contain "Files Modified"
And the plan lifecycle output should contain "Child Plans"
And the plan lifecycle output should contain "Checkpoints"
Scenario: Plan status renders Cost panel
When I run plan status for a plan with all five panels
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Cost"
And the plan lifecycle output should contain "Tokens Used"
And the plan lifecycle output should contain "Cost So Far"
And the plan lifecycle output should contain "Estimated"
Scenario: Plan status renders status refresh footer
When I run plan status for a plan with all five panels
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Status refreshed"
Scenario: Plan status renders Arguments when plan has arguments
When I run plan status for a plan with arguments
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Arguments"
And the plan lifecycle output should contain "coverage"
Scenario: Plan status renders Automation Profile when set
When I run plan status for a plan with automation profile
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Automation Profile"
And the plan lifecycle output should contain "trusted"
Scenario: Plan status renders Estimation Actor when set
When I run plan status for a plan with estimation actor
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Estimation Actor"
And the plan lifecycle output should contain "openai/gpt-4"
Scenario: Plan status renders Invariant Actor when set
When I run plan status for a plan with invariant actor
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Invariant Actor"
And the plan lifecycle output should contain "openai/gpt-4"
Scenario: Plan status renders phase timestamps when populated
When I run plan status for a plan with all timestamps
Then the plan lifecycle command should succeed
And the plan lifecycle output should contain "Strategize Started"
And the plan lifecycle output should contain "Strategize Completed"
And the plan lifecycle output should contain "Execute Started"
And the plan lifecycle output should contain "Execute Completed"
And the plan lifecycle output should contain "Applied At"
@@ -0,0 +1,140 @@
"""Step definitions for plan_lifecycle_cli_coverage.feature.
Covers the five-panel output of _print_lifecycle_plan():
- Plan Status panel (Processing State, Projects, Action, Phase, Attempt)
- Progress panel (Strategize, Execute, Apply)
- Timing panel (Started, Elapsed, ETA, phase timestamps)
- Execution Detail panel (Sandbox, Tool Calls, Files Modified, Child Plans,
Checkpoints)
- Cost panel (Tokens Used, Cost So Far, Estimated)
- Status refresh footer
- Optional fields: Arguments, Automation Profile, Estimation Actor,
Invariant Actor
"""
from __future__ import annotations
from datetime import datetime, timedelta
from behave import when
from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.domain.models.core.plan import (
AutomationProfileProvenance,
AutomationProfileRef,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
ProcessingState,
ProjectLink,
)
_PLAN_ULID = "01ARZ3NDEKTSV4RRFFQ69G5FBF"
def _make_five_panel_plan(
*,
arguments: dict[str, object] | None = None,
arguments_order: list[str] | None = None,
automation_profile: AutomationProfileRef | None = None,
estimation_actor: str | None = None,
invariant_actor: str | None = None,
timestamps: PlanTimestamps | None = None,
) -> Plan:
"""Build a Plan with all fields needed for five-panel output testing."""
now = datetime.now()
if timestamps is None:
timestamps = PlanTimestamps(
created_at=now - timedelta(hours=1),
updated_at=now,
)
return Plan(
identity=PlanIdentity(plan_id=_PLAN_ULID),
namespaced_name=NamespacedName.parse("local/five-panel-plan"),
action_name="local/test-action",
description="Five panel test plan",
phase=PlanPhase.EXECUTE,
processing_state=ProcessingState.PROCESSING,
project_links=[
ProjectLink(project_name="proj-a"),
ProjectLink(project_name="proj-b"),
],
arguments=dict(arguments) if arguments else {},
arguments_order=arguments_order or [],
automation_profile=automation_profile,
estimation_actor=estimation_actor,
invariant_actor=invariant_actor,
reusable=True,
read_only=False,
timestamps=timestamps,
)
@when("I run plan status for a plan with all five panels")
def step_plan_status_five_panels(context) -> None:
"""Run plan status for a plan that exercises all five output panels."""
plan = _make_five_panel_plan()
context.lifecycle_service.get_plan.return_value = plan
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
@when("I run plan status for a plan with arguments")
def step_plan_status_with_arguments(context) -> None:
"""Run plan status for a plan that has arguments."""
plan = _make_five_panel_plan(
arguments={"coverage": 80, "framework": "behave"},
arguments_order=["coverage", "framework"],
)
context.lifecycle_service.get_plan.return_value = plan
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
@when("I run plan status for a plan with automation profile")
def step_plan_status_with_automation_profile(context) -> None:
"""Run plan status for a plan that has an automation profile."""
plan = _make_five_panel_plan(
automation_profile=AutomationProfileRef(
profile_name="trusted",
provenance=AutomationProfileProvenance.PLAN,
),
)
context.lifecycle_service.get_plan.return_value = plan
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
@when("I run plan status for a plan with estimation actor")
def step_plan_status_with_estimation_actor(context) -> None:
"""Run plan status for a plan that has an estimation actor."""
plan = _make_five_panel_plan(estimation_actor="openai/gpt-4")
context.lifecycle_service.get_plan.return_value = plan
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
@when("I run plan status for a plan with invariant actor")
def step_plan_status_with_invariant_actor(context) -> None:
"""Run plan status for a plan that has an invariant actor."""
plan = _make_five_panel_plan(invariant_actor="openai/gpt-4")
context.lifecycle_service.get_plan.return_value = plan
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
@when("I run plan status for a plan with all timestamps")
def step_plan_status_with_all_timestamps(context) -> None:
"""Run plan status for a plan that has all phase timestamps populated."""
now = datetime.now()
timestamps = PlanTimestamps(
created_at=now - timedelta(hours=2),
updated_at=now,
strategize_started_at=now - timedelta(hours=1, minutes=50),
strategize_completed_at=now - timedelta(hours=1, minutes=40),
execute_started_at=now - timedelta(hours=1, minutes=30),
execute_completed_at=now - timedelta(hours=1),
applied_at=now - timedelta(minutes=30),
)
plan = _make_five_panel_plan(
timestamps=timestamps,
)
context.lifecycle_service.get_plan.return_value = plan
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
+283 -114
View File
@@ -53,7 +53,12 @@ from cleveragents.domain.models.core.error_recovery import (
ErrorCategory,
classify_error,
)
from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState
from cleveragents.domain.models.core.plan import (
ExecutionEnvPriority,
PlanPhase,
ProcessingState,
)
from cleveragents.domain.models.core.plan import Plan as LifecyclePlan
from cleveragents.infrastructure.sandbox.git_worktree import (
GitWorktreeSandbox,
)
@@ -346,8 +351,6 @@ def _execute_output_dict(
Returns:
A JSON-serialisable dict matching the spec-required envelope.
"""
from cleveragents.domain.models.core.plan import Plan as LifecyclePlan
if not isinstance(plan, LifecyclePlan):
# Legacy plan fallback — return minimal envelope
return {
@@ -1417,19 +1420,21 @@ def _get_plan_executor(
def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
"""Print v3 lifecycle plan details in a nice panel.
"""Print v3 lifecycle plan details in multiple panels.
Renders five panels per specification:
1. Plan Status - basic plan info (ID, phase, processing state, action,
projects, arguments, automation profile, actors, created/updated)
2. Progress - Strategize/Execute/Apply step progress
3. Timing - Started, Elapsed, ETA, and all phase timestamps
4. Execution Detail - Sandbox, Tool Calls, Files Modified, Child Plans,
Checkpoints
5. Cost - Tokens Used, Cost So Far, Estimated
Args:
plan: A v3 Plan object from plan.py (not plan_legacy.py)
title: Panel title
title: Panel title (used for Plan Status panel)
"""
from cleveragents.domain.models.core.plan import (
ExecutionEnvPriority,
)
from cleveragents.domain.models.core.plan import (
Plan as LifecyclePlan,
)
if not isinstance(plan, LifecyclePlan):
# Fall back to legacy plan display - this shouldn't happen
console.print(Panel(f"Plan: {plan}", title=title, expand=False))
@@ -1448,28 +1453,85 @@ def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
project_parts.append(label)
projects = ", ".join(project_parts) if project_parts else "(none)"
description_preview = plan.description[:200]
if len(plan.description) > 200:
description_preview = f"{description_preview}..."
details = (
f"[bold]ID:[/bold] {plan.identity.plan_id}\n"
f"[bold]Name:[/bold] {plan.namespaced_name}\n"
f"[bold]Action:[/bold] {plan.action_name}\n"
f"[bold]Phase:[/bold] {plan.phase.value}\n"
f"[bold]Processing State:[/bold] {state_display}\n"
f"[bold]Projects:[/bold] {projects}\n"
f"[bold]Description:[/bold]\n {description_preview}\n"
f"[bold]Strategy Actor:[/bold] {plan.strategy_actor or '(not set)'}\n"
f"[bold]Execution Actor:[/bold] {plan.execution_actor or '(not set)'}\n"
# ===== PANEL 1: Plan Status =====
plan_status_details = (
f"[cyan bold]Plan:[/cyan bold] {plan.identity.plan_id}\n"
f"[yellow bold]Phase:[/yellow bold] {plan.phase.value}\n"
f"[magenta bold]Processing State:[/magenta bold] {state_display}\n"
f"[#5599ff bold]Action:[/#5599ff bold] {plan.action_name}\n"
f"[#66cc66 bold]Projects:[/#66cc66 bold] {projects}\n"
f"[#5599ff bold]Attempt:[/#5599ff bold] {plan.identity.attempt}"
)
# Definition-of-Done evaluation summary
# Arguments (ordered)
if plan.arguments:
ordered_keys = (
plan.arguments_order
if plan.arguments_order
else sorted(plan.arguments.keys())
)
args_lines = "\n".join(
f" {key} = {plan.arguments[key]}"
for key in ordered_keys
if key in plan.arguments
)
plan_status_details += f"\n[bold]Arguments:[/bold]\n{args_lines}"
# Automation profile info
if plan.automation_profile:
plan_status_details += (
f"\n[#5599ff bold]Automation Profile:[/#5599ff bold] "
f"{plan.automation_profile.profile_name} "
f"(source: {plan.automation_profile.provenance.value})"
)
# Optional actors
if plan.strategy_actor:
plan_status_details += f"\n[bold]Strategy Actor:[/bold] {plan.strategy_actor}"
if plan.execution_actor:
plan_status_details += f"\n[bold]Execution Actor:[/bold] {plan.execution_actor}"
if plan.estimation_actor:
plan_status_details += (
f"\n[bold]Estimation Actor:[/bold] {plan.estimation_actor}"
)
if plan.invariant_actor:
plan_status_details += f"\n[bold]Invariant Actor:[/bold] {plan.invariant_actor}"
# Execution environment
if plan.execution_environment:
plan_status_details += (
f"\n[bold]Execution Environment:[/bold] {plan.execution_environment}"
)
priority_display = (
plan.execution_env_priority.value
if plan.execution_env_priority is not None
else ExecutionEnvPriority.FALLBACK.value
)
plan_status_details += (
f"\n[bold]Execution Env Priority:[/bold] {priority_display}"
)
# Timestamps: Created and Updated always shown
plan_status_details += (
f"\n[bold]Created:[/bold] {plan.timestamps.created_at}"
f"\n[bold]Updated:[/bold] {plan.timestamps.updated_at}"
)
# Description (truncated to 200 chars)
if plan.description:
description_preview = plan.description[:200]
if len(plan.description) > 200:
description_preview = f"{description_preview}..."
plan_status_details += f"\n[bold]Description:[/bold]\n {description_preview}"
# Definition-of-Done
if plan.definition_of_done:
dod_preview = plan.definition_of_done[:200]
if len(plan.definition_of_done) > 200:
dod_preview += "..."
details += f"[bold]Definition of Done:[/bold]\n {dod_preview}\n"
plan_status_details += f"\n[bold]Definition of Done:[/bold]\n {dod_preview}"
# DoD evaluation summary
if plan.validation_summary and plan.validation_summary.get("dod_evaluated"):
vs = plan.validation_summary
dod_passed = vs.get("dod_all_passed", False)
@@ -1478,102 +1540,46 @@ def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
dod_total = req_pass + req_fail
status_color = "green" if dod_passed else "red"
status_label = "PASSED" if dod_passed else "FAILED"
details += (
f"[bold]DoD Evaluation:[/bold] "
plan_status_details += (
f"\n[bold]DoD Evaluation:[/bold] "
f"[{status_color}]{status_label}[/{status_color}]"
f" ({req_pass}/{dod_total} passed"
f"{f', {req_fail} failed' if req_fail else ''})\n"
)
# Execution environment
if plan.execution_environment:
details += f"[bold]Execution Environment:[/bold] {plan.execution_environment}\n"
priority_display = (
plan.execution_env_priority.value
if plan.execution_env_priority is not None
else ExecutionEnvPriority.FALLBACK.value
)
details += f"[bold]Execution Env Priority:[/bold] {priority_display}\n"
# Optional actors
if plan.estimation_actor:
details += f"[bold]Estimation Actor:[/bold] {plan.estimation_actor}\n"
if plan.invariant_actor:
details += f"[bold]Invariant Actor:[/bold] {plan.invariant_actor}\n"
# Estimation results
# TODO: Migrate to EstimationResult.as_display_dict() to reduce duplication
if plan.estimation_result is not None:
est = plan.estimation_result
details += "[bold]Estimation:[/bold]\n"
if est.summary:
details += f" Summary: {est.summary}\n"
if est.estimated_cost_usd is not None:
details += f" Est. Cost: ${est.estimated_cost_usd:.4f}\n"
if est.estimated_tokens is not None:
details += f" Est. Tokens: {est.estimated_tokens}\n"
if est.estimated_steps is not None:
details += f" Est. Steps: {est.estimated_steps}\n"
if est.estimated_child_plans is not None:
details += f" Est. Child Plans: {est.estimated_child_plans}\n"
if est.estimated_time_seconds is not None:
details += f" Est. Time: {est.estimated_time_seconds:.1f}s\n"
if est.risk_level is not None:
details += f" Risk Level: {est.risk_level}\n"
if est.risk_factors:
details += " Risk Factors:\n"
for factor in est.risk_factors:
details += f" - {factor}\n"
# Arguments (ordered)
if plan.arguments:
details += "[bold]Arguments:[/bold]\n"
ordered_keys = (
plan.arguments_order
if plan.arguments_order
else sorted(plan.arguments.keys())
)
for key in ordered_keys:
if key in plan.arguments:
details += f" {key} = {plan.arguments[key]}\n"
# Automation profile info
if plan.automation_profile:
details += (
f"[bold]Automation Profile:[/bold] "
f"{plan.automation_profile.profile_name} "
f"(source: {plan.automation_profile.provenance.value})\n"
f"{f', {req_fail} failed' if req_fail else ''})"
)
# Invariants
if plan.invariants:
details += "[bold]Invariants:[/bold]\n"
plan_status_details += "\n[bold]Invariants:[/bold]"
for inv in plan.invariants:
details += f" [{inv.source.value}] {inv.text}\n"
plan_status_details += f"\n [{inv.source.value}] {inv.text}"
# Resume metadata
if plan.last_completed_step >= 0:
details += f"[bold]Last Completed Step:[/bold] {plan.last_completed_step}\n"
plan_status_details += (
f"\n[bold]Last Completed Step:[/bold] {plan.last_completed_step}"
)
if plan.last_checkpoint_id:
details += f"[bold]Last Checkpoint:[/bold] {plan.last_checkpoint_id}\n"
plan_status_details += (
f"\n[bold]Last Checkpoint:[/bold] {plan.last_checkpoint_id}"
)
# Multi-project changeset summaries (#199)
# Multi-project changeset summaries
if (
plan.multi_project_metadata is not None
and plan.multi_project_metadata.project_scopes
):
details += "[bold]Multi-Project Scopes:[/bold]\n"
plan_status_details += "\n[bold]Multi-Project Scopes:[/bold]"
for scope in plan.multi_project_metadata.project_scopes:
label = scope.project_name
if scope.alias:
label += f" (alias: {scope.alias})"
if scope.read_only:
label += " [read-only]"
details += f" {label}\n"
plan_status_details += f"\n {label}"
if scope.changeset_summary is not None:
cs = scope.changeset_summary
details += (
f" Changed: {cs.files_changed} "
plan_status_details += (
f"\n Changed: {cs.files_changed} "
f"Added: {cs.files_added} "
f"Deleted: {cs.files_deleted} "
f"Lines: {cs.total_lines_changed}"
@@ -1581,39 +1587,202 @@ def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
if cs.validation_passed is not None:
v_color = "green" if cs.validation_passed else "red"
v_label = "PASSED" if cs.validation_passed else "FAILED"
details += f" Validation: [{v_color}]{v_label}[/{v_color}]"
details += "\n"
plan_status_details += (
f" Validation: [{v_color}]{v_label}[/{v_color}]"
)
details += f"[bold]Terminal:[/bold] {'yes' if plan.is_terminal else 'no'}\n"
# Error message
if plan.error_message:
plan_status_details += f"\n[bold red]Error:[/bold red] {plan.error_message}"
console.print(Panel(plan_status_details, title=title, expand=False))
# ===== PANEL 2: Progress =====
# Determine status of each phase based on current phase and state
strategize_status = ""
strategize_color = "green"
execute_status = ""
execute_color = "cyan"
apply_status = ""
apply_color = "yellow"
if plan.phase == PlanPhase.ACTION:
strategize_status = ""
strategize_color = "yellow"
execute_status = ""
execute_color = "yellow"
apply_status = ""
apply_color = "yellow"
elif plan.phase == PlanPhase.STRATEGIZE:
if plan.processing_state == ProcessingState.COMPLETE:
strategize_status = ""
strategize_color = "green"
execute_status = ""
execute_color = "yellow"
else:
strategize_status = ""
strategize_color = "cyan"
execute_status = ""
execute_color = "yellow"
elif plan.phase == PlanPhase.EXECUTE:
strategize_status = ""
strategize_color = "green"
if plan.processing_state == ProcessingState.COMPLETE:
execute_status = ""
execute_color = "green"
apply_status = ""
apply_color = "yellow"
else:
execute_status = ""
execute_color = "cyan"
apply_status = ""
apply_color = "yellow"
elif plan.phase == PlanPhase.APPLY:
strategize_status = ""
strategize_color = "green"
execute_status = ""
execute_color = "green"
applied_or_constrained = plan.processing_state in (
ProcessingState.APPLIED,
ProcessingState.CONSTRAINED,
)
if applied_or_constrained:
apply_status = ""
apply_color = "green"
else:
apply_status = ""
apply_color = "cyan"
progress_details = (
f"[{strategize_color}]{strategize_status}[/{strategize_color}] Strategize\n"
f"[{execute_color}]{execute_status}[/{execute_color}] Execute\n"
f"[{apply_color}]{apply_status}[/{apply_color}] Apply"
)
console.print(Panel(progress_details, title="Progress", expand=False))
# ===== PANEL 3: Timing =====
# Calculate timing information
started_time = "N/A"
elapsed_time = "N/A"
eta_time = "N/A"
# Timestamps
details += f"[bold]Created:[/bold] {plan.timestamps.created_at}\n"
details += f"[bold]Updated:[/bold] {plan.timestamps.updated_at}"
if plan.timestamps.strategize_started_at:
details += (
started_time = plan.timestamps.strategize_started_at.strftime("%H:%M:%S")
# Calculate elapsed time
now = datetime.now(plan.timestamps.strategize_started_at.tzinfo)
elapsed_seconds = (now - plan.timestamps.strategize_started_at).total_seconds()
hours = int(elapsed_seconds // 3600)
minutes = int((elapsed_seconds % 3600) // 60)
seconds = int(elapsed_seconds % 60)
elapsed_time = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
# Calculate ETA using estimation result when available
if (
plan.estimation_result is not None
and plan.estimation_result.estimated_time_seconds is not None
):
estimated_total_seconds = plan.estimation_result.estimated_time_seconds
remaining_seconds = max(0, estimated_total_seconds - elapsed_seconds)
eta_hours = int(remaining_seconds // 3600)
eta_minutes = int((remaining_seconds % 3600) // 60)
eta_secs = int(remaining_seconds % 60)
eta_time = f"{eta_hours:02d}:{eta_minutes:02d}:{eta_secs:02d}"
else:
eta_time = "N/A"
timing_details = (
f"[#66cc66 bold]Started:[/#66cc66 bold] {started_time}\n"
f"[yellow bold]Elapsed:[/yellow bold] {elapsed_time}\n"
f"[#66cc66 bold]ETA:[/#66cc66 bold] {eta_time}"
)
# Optional phase timestamps
if plan.timestamps.strategize_started_at:
timing_details += (
f"\n[bold]Strategize Started:[/bold] "
f"{plan.timestamps.strategize_started_at}"
)
if plan.timestamps.strategize_completed_at:
details += (
timing_details += (
f"\n[bold]Strategize Completed:[/bold] "
f"{plan.timestamps.strategize_completed_at}"
)
if plan.timestamps.execute_started_at:
details += (
timing_details += (
f"\n[bold]Execute Started:[/bold] {plan.timestamps.execute_started_at}"
)
if plan.timestamps.execute_completed_at:
details += (
timing_details += (
f"\n[bold]Execute Completed:[/bold] {plan.timestamps.execute_completed_at}"
)
if plan.timestamps.applied_at:
details += f"\n[bold]Applied At:[/bold] {plan.timestamps.applied_at}"
timing_details += f"\n[bold]Applied At:[/bold] {plan.timestamps.applied_at}"
if plan.error_message:
details += f"\n[bold red]Error:[/bold red] {plan.error_message}"
console.print(Panel(timing_details, title="Timing", expand=False))
console.print(Panel(details, title=title, expand=False))
# ===== PANEL 4: Execution Detail =====
# Extract execution details
sandbox_strategy = "N/A"
if plan.sandbox_refs:
# Note: sandbox_refs are internal ULID references; the spec example
# shows a human-readable strategy name (e.g. 'git_worktree'). N/A
# would be more accurate but raw refs are retained for diagnostic use.
sandbox_strategy = ", ".join(plan.sandbox_refs[:3])
if len(plan.sandbox_refs) > 3:
sandbox_strategy += f" (+{len(plan.sandbox_refs) - 3} more)"
# tool_calls is not tracked in CostMetadata; display N/A
tool_calls_display = "N/A"
# files_modified is not directly available; display N/A
files_modified_display = "N/A"
child_plans = "0/0 complete"
if plan.subplan_statuses:
completed = sum(
1 for sp in plan.subplan_statuses if sp.completed_at is not None
)
child_plans = f"{completed}/{len(plan.subplan_statuses)} complete"
# Checkpoint count approximation: we only know whether at least one
# checkpoint exists (via last_checkpoint_id). A more accurate count
# would require an additional query; displayed as 'N/A' would be
# consistent with tool_calls and files_modified, but the binary signal
# (0 vs 1) is intentional for now.
checkpoints = 0
if plan.last_checkpoint_id:
checkpoints = 1 # At least one checkpoint exists
execution_details = (
f"[#5599ff bold]Sandbox:[/#5599ff bold] {sandbox_strategy}\n"
f"[#5599ff bold]Tool Calls:[/#5599ff bold] {tool_calls_display}\n"
f"[#5599ff bold]Files Modified:[/#5599ff bold] {files_modified_display}\n"
f"[#5599ff bold]Child Plans:[/#5599ff bold] {child_plans}\n"
f"[#5599ff bold]Checkpoints:[/#5599ff bold] {checkpoints} created"
)
console.print(Panel(execution_details, title="Execution Detail", expand=False))
# ===== PANEL 5: Cost =====
tokens_used = 0
cost_so_far = 0.0
estimated_cost = 0.0
if plan.cost_metadata:
tokens_used = plan.cost_metadata.total_tokens or 0
cost_so_far = plan.cost_metadata.total_cost or 0.0
if plan.estimation_result and plan.estimation_result.estimated_cost_usd:
estimated_cost = plan.estimation_result.estimated_cost_usd
cost_details = (
f"[#5599ff bold]Tokens Used:[/#5599ff bold] {tokens_used:,}\n"
f"[yellow bold]Cost So Far:[/yellow bold] ${cost_so_far:.3f}\n"
f"[#5599ff bold]Estimated:[/#5599ff bold] ${estimated_cost:.3f}"
)
console.print(Panel(cost_details, title="Cost", expand=False))
# ===== Footer =====
console.print("[#66cc66 bold]✓ OK[/#66cc66 bold] Status refreshed")
@app.command("use")