From f554122b074956ffafc38972130cf433976c67b2 Mon Sep 17 00:00:00 2001 From: gilesb Date: Mon, 12 Jan 2026 00:23:12 +0000 Subject: [PATCH] Replace plan JSON with colored S-expression display - Add plan_to_sexp() to convert plan to S-expression format - Syntax highlighting for S-expressions: - Pink: special forms (plan, recipe, def, ->) - Blue: primitives (source, effect, sequence, etc.) - Purple: keywords (:input, :name, etc.) - Green: strings - Yellow: parentheses - Gray: comments Co-Authored-By: Claude Opus 4.5 --- app/routers/runs.py | 57 ++++++++++++++++++++++++++++++++++ app/templates/runs/detail.html | 34 +++++++++++++++++--- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/app/routers/runs.py b/app/routers/runs.py index 1e4e219..643c691 100644 --- a/app/routers/runs.py +++ b/app/routers/runs.py @@ -27,6 +27,59 @@ from ..services.run_service import RunService router = APIRouter() logger = logging.getLogger(__name__) + +def plan_to_sexp(plan: dict, recipe_name: str = None) -> str: + """Convert a plan to S-expression format for display.""" + if not plan or not plan.get("steps"): + return ";; No plan available" + + lines = [] + lines.append(f'(plan "{recipe_name or "unknown"}"') + + # Group nodes by type for cleaner output + steps = plan.get("steps", []) + + for step in steps: + step_id = step.get("id", "?") + step_type = step.get("type", "EFFECT") + inputs = step.get("inputs", []) + config = step.get("config", {}) + + # Build the step S-expression + if step_type == "SOURCE": + if config.get("input"): + # Variable input + input_name = config.get("name", config.get("input", "input")) + lines.append(f' (source :input "{input_name}")') + elif config.get("asset"): + # Fixed asset + lines.append(f' (source {config.get("asset", step_id)})') + else: + lines.append(f' (source {step_id})') + elif step_type == "EFFECT": + effect_name = config.get("effect", step_id) + if inputs: + inp_str = " ".join(inputs) + lines.append(f' (-> {inp_str} (effect {effect_name}))') + else: + lines.append(f' (effect {effect_name})') + elif step_type == "SEQUENCE": + if inputs: + inp_str = " ".join(inputs) + lines.append(f' (sequence {inp_str})') + else: + lines.append(f' (sequence)') + else: + # Generic node + if inputs: + inp_str = " ".join(inputs) + lines.append(f' ({step_type.lower()} {inp_str})') + else: + lines.append(f' ({step_type.lower()} {step_id})') + + lines.append(')') + return "\n".join(lines) + RUNS_KEY_PREFIX = "artdag:run:" @@ -258,6 +311,9 @@ async def get_run( } }) + # Generate S-expression representation of the plan + plan_sexp = plan_to_sexp(plan, run.get("recipe_name")) + templates = get_templates(request) return render(templates, "runs/detail.html", request, run=run, @@ -266,6 +322,7 @@ async def get_run( run_inputs=run_inputs, dag_elements=dag_elements, output_media_type=output_media_type, + plan_sexp=plan_sexp, active_tab="runs", ) diff --git a/app/templates/runs/detail.html b/app/templates/runs/detail.html index cd9c32f..b2eb06a 100644 --- a/app/templates/runs/detail.html +++ b/app/templates/runs/detail.html @@ -156,15 +156,41 @@ {% endfor %} - -
+ +
- Show Plan JSON + Plan (S-expression)
-
{{ plan | tojson(indent=2) }}
+
{{ plan_sexp }}
+ + + {% else %}

No plan available for this run.

{% endif %}