auto-dev-agent / ui /pipeline_visualiser.py
Siva sai Yadav
ready for HuggingFace Space deployment
8edee29
Raw
History Blame Contribute Delete
26.9 kB
"""
ui/pipeline_visualiser.py
--------------------------
Pipeline Visualiser β€” vertical card layout.
Each agent is a full card with icon, name, model badge, role description,
and an animated thinking row when active. The panel sits in the right
column and spans the full height of the page beside the left panel.
States are baked as CSS classes on every yield β€” no JS re-execution.
"""
from pipeline.state import PipelineStatus
# ── Backward-compat exports ────────────────────────────────────── #
STATUS_TO_ACTIVE_NODE: dict = {}
SUCCESS_ARROWS: set = set()
DEBUG_ARROWS: set = set()
NODE_NAME_TO_STATUS: dict[str, PipelineStatus] = {
"clarify": PipelineStatus.CLARIFYING,
"detect": PipelineStatus.DETECTING,
"plan": PipelineStatus.PLANNING,
"generate": PipelineStatus.GENERATING,
"regen": PipelineStatus.REGENERATING,
"execute": PipelineStatus.EXECUTING,
"classify_error": PipelineStatus.CLASSIFYING,
"debug_agent": PipelineStatus.DEBUGGING,
"test": PipelineStatus.TESTING,
"test_failing": PipelineStatus.TEST_FAILING,
"test_debug_agent": PipelineStatus.TEST_DEBUGGING,
"explain": PipelineStatus.EXPLAINING,
"success": PipelineStatus.SUCCESS,
"partial_success": PipelineStatus.PARTIAL_SUCCESS,
"human_loop": PipelineStatus.AWAITING_HUMAN,
}
def get_status_update_js(status=None, debug_iterations: int = 0, is_error: bool = False) -> str:
return ""
# ── Public API ─────────────────────────────────────────────────── #
def get_pipeline_html() -> str:
return _build(PipelineStatus.IDLE, 0, False, {})
def get_pipeline_html_with_update(
node_name: str,
iterations: int = 0,
is_error: bool = False,
model_assignments: dict | None = None,
) -> str:
status = NODE_NAME_TO_STATUS.get(node_name, PipelineStatus.IDLE)
return _build(status, iterations, is_error, model_assignments or {})
# ── Agent definitions ──────────────────────────────────────────── #
AGENTS = {
"clarify": {"icon": "πŸ€”", "name": "Clarity Check", "ma_key": None, "role": "Checks if task is clear enough to code"},
"detect": {"icon": "πŸ”Ž", "name": "Language Detector", "ma_key": "detector", "role": "Detects Python vs SQL from task"},
"plan": {"icon": "🧠", "name": "Planning Agent", "ma_key": "planner", "role": "Breaks task into step-by-step plan"},
"gen": {"icon": "⌨️", "name": "Code Generator", "ma_key": "generator", "role": "Writes code from the plan"},
"exec": {"icon": "⚑", "name": "Executor", "ma_key": None, "role": "Runs code in secure sandbox"},
"classify": {"icon": "πŸ”", "name": "Error Classifier", "ma_key": "classifier", "role": "Labels error type (syntax/runtime/logic)"},
"debug": {"icon": "πŸ›", "name": "Debug Agent", "ma_key": "debugger", "role": "Reflects on error & rewrites code"},
"hitl": {"icon": "πŸ™‹", "name": "Human-in-Loop", "ma_key": None, "role": "Max retries reached β€” needs your input"},
"test": {"icon": "πŸ§ͺ", "name": "Test Agent", "ma_key": "tester", "role": "Generates & runs unit tests"},
"explain": {"icon": "πŸ’‘", "name": "Explanation Agent", "ma_key": "explainer", "role": "Plain-English explanation + flowchart"},
"complete": {"icon": "βœ…", "name": "Done", "ma_key": None, "role": "Code Β· tests Β· report ready"},
}
from agents.model_router import MODEL_DISPLAY # noqa: E402
def _model_label(ma: dict, ma_key: str | None) -> str:
if not ma_key or not ma:
return ""
model_id = ma.get(ma_key, "")
return MODEL_DISPLAY.get(model_id, model_id.split("/")[-1] if model_id else "")
THINKING_MSGS = {
"clarify": "Checking task clarity...",
"detect": "Detecting language from task...",
"plan": "Analyzing task structure...",
"gen": "Writing code from plan...",
"exec": "Running in secure sandbox...",
"classify": "Diagnosing the error...",
"debug": "Reflecting & rewriting...",
"hitl": "Awaiting your input...",
"test": "Generating & running tests...",
"explain": "Building explanation + flowchart...",
"complete": "All done!",
# regen reuses the gen card β€” message is overridden in _STATE_TABLE banner
}
# ── Node state table ───────────────────────────────────────────── #
_STATE_TABLE: dict[str, dict[str, str]] = {
"idle": {},
"clarifying": {"clarify": "active"},
"needs_clarification":{"clarify": "error"},
"detecting": {"clarify":"done","detect": "active"},
"planning": {"clarify":"done","detect": "done", "plan": "active"},
"generating": {"clarify":"done","detect":"done","plan":"done","gen":"active"},
# regenerating: gen card pulses again (fresh rewrite), shows regen badge via banner
"regenerating": {"clarify":"done","detect":"done","plan":"done","gen":"active","exec":"error","classify":"done","debug":"done"},
"executing": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"active"},
"classifying": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"error","classify":"active"},
"reflecting": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"error","classify":"done","debug":"active"},
"debugging": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"error","classify":"done","debug":"active"},
"testing": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"active"},
"test_failing": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"error"},
"test_debugging": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"error","classify":"done","debug":"active"},
"polishing": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"done","explain":"active"},
"explaining": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"done","explain":"active"},
"success": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"done","explain":"done","complete":"done"},
# partial_success: code runs, but not all tests passed after exhausting test-fix budget.
# test card shown in error state so user sees which tests failed. No HITL triggered.
"partial_success": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"error","explain":"done","complete":"done"},
"awaiting_human": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"error","classify":"done","debug":"done","hitl":"active"},
"failed": {},
}
_ALL_NODES = ["clarify","detect","plan","gen","exec","classify","debug","hitl","test","explain","complete"]
STATUS_MSGS = {
"idle": ("idle", "Ready β€” submit a task to begin."),
"clarifying": ("running", "Checking if task is clear enough to proceed..."),
"needs_clarification":("warn", "Task needs clarification β€” please answer the question below."),
"detecting": ("running", "Detecting language..."),
"planning": ("running", "Planning Agent is analyzing your task..."),
"generating": ("running", "Code Generator is writing code..."),
"regenerating": ("error", "⟳ Debug loop exhausted β€” regenerating code from scratch with error context..."),
"executing": ("running", "Executor is running code in the sandbox..."),
"classifying": ("error", "Error Classifier is diagnosing the failure..."),
"reflecting": ("error", "Debug Agent is reflecting on the error..."),
"debugging": ("error", "Debug Agent is rewriting the code..."),
"testing": ("running", "Test Agent is generating & running tests..."),
"test_failing": ("error", "Tests failed β€” rewriting code to fix failing cases..."),
"test_debugging": ("error", "Debug Agent rewriting code to pass all test cases..."),
"polishing": ("running", "Explanation Agent is preparing output..."),
"explaining": ("running", "Explanation Agent is building the explanation..."),
"awaiting_human": ("warn", "⚠ Max retries reached β€” your input is needed."),
"success": ("done", "All agents completed β€” output is ready."),
"partial_success":("warn", "⚠ Code runs β€” but not all tests passed after all retry cycles. Review the Tests tab."),
"failed": ("error", "Pipeline failed."),
}
def _get_ns(status: PipelineStatus) -> dict[str, str]:
s = status.value if hasattr(status, "value") else str(status)
ns = {n: "idle" for n in _ALL_NODES}
ns.update(_STATE_TABLE.get(s, {}))
return ns
# ── CSS ────────────────────────────────────────────────────────── #
CSS = """<style>
@keyframes av-glow-blue {0%,100%{box-shadow:0 0 0 0 rgba(129,140,248,.0),0 0 12px rgba(129,140,248,.25)} 50%{box-shadow:0 0 0 3px rgba(129,140,248,.15),0 0 28px rgba(129,140,248,.5)}}
@keyframes av-glow-red {0%,100%{box-shadow:0 0 0 0 rgba(248,113,113,.0),0 0 12px rgba(248,113,113,.25)} 50%{box-shadow:0 0 0 3px rgba(248,113,113,.15),0 0 28px rgba(248,113,113,.5)}}
@keyframes av-glow-amber {0%,100%{box-shadow:0 0 0 0 rgba(251,191,36,.0), 0 0 12px rgba(251,191,36,.2)} 50%{box-shadow:0 0 0 3px rgba(251,191,36,.12), 0 0 24px rgba(251,191,36,.45)}}
@keyframes av-spin {to{transform:rotate(360deg)}}
@keyframes av-pulse {0%,100%{opacity:1}50%{opacity:.35}}
@keyframes av-slide {from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:translateY(0)}}
@keyframes av-dot1 {0%,20%,100%{opacity:.2}10%{opacity:1}}
@keyframes av-dot2 {0%,30%,100%{opacity:.2}20%{opacity:1}}
@keyframes av-dot3 {0%,40%,100%{opacity:.2}30%{opacity:1}}
@keyframes av-scan {0%{background-position:0% 0%}100%{background-position:0% 100%}}
/* ── Wrapper ── */
.av-wrap{
font-family:var(--font-sans,system-ui,-apple-system,sans-serif);
background:linear-gradient(160deg,#0d1117 0%,#0f172a 100%);
border:1px solid rgba(255,255,255,.07);
border-radius:14px;
padding:14px 14px 10px;
box-sizing:border-box;
color:#e2e8f0;
height:100%;
}
/* ── Banner ── */
.av-banner{
display:flex;align-items:center;gap:10px;
padding:10px 12px;
border-radius:10px;
margin-bottom:10px;
border:1px solid rgba(255,255,255,.06);
background:rgba(255,255,255,.03);
position:relative;overflow:hidden;
}
.av-banner.b-running{border-color:rgba(129,140,248,.3);background:rgba(129,140,248,.07);}
.av-banner.b-error {border-color:rgba(248,113,113,.3);background:rgba(248,113,113,.07);}
.av-banner.b-done {border-color:rgba(74,222,128,.3); background:rgba(74,222,128,.07);}
.av-banner.b-warn {border-color:rgba(251,191,36,.3); background:rgba(251,191,36,.07);}
.av-banner-scan{
position:absolute;inset:0;pointer-events:none;
background:linear-gradient(to bottom,transparent 0%,rgba(129,140,248,.05) 50%,transparent 100%);
background-size:100% 200%;
animation:av-scan 2s linear infinite;
}
.av-banner.b-running .av-banner-scan{display:block}
.av-banner:not(.b-running) .av-banner-scan{display:none}
.av-beacon{
width:10px;height:10px;border-radius:50%;flex-shrink:0;
background:rgba(100,100,120,.3);
}
.av-beacon.bc-run {background:#818cf8;animation:av-pulse .9s ease-in-out infinite;}
.av-beacon.bc-err {background:#f87171;animation:av-pulse .9s ease-in-out infinite;}
.av-beacon.bc-ok {background:#4ade80;}
.av-beacon.bc-warn{background:#fbbf24;animation:av-pulse .9s ease-in-out infinite;}
.av-banner-text{flex:1;font-size:11.5px;color:rgba(226,232,240,.8);line-height:1.4;}
.av-retry-badge{
font-size:10px;font-weight:700;letter-spacing:.05em;
color:#f87171;background:rgba(248,113,113,.12);
padding:2px 8px;border-radius:10px;border:1px solid rgba(248,113,113,.25);
flex-shrink:0;
}
/* ── Section label ── */
.av-section{
font-size:9px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
color:rgba(226,232,240,.25);margin:8px 0 5px 2px;
}
/* ── Agent card ── */
.av-card{
display:flex;align-items:flex-start;gap:10px;
padding:9px 11px;
border-radius:10px;
border:1px solid rgba(255,255,255,.05);
background:rgba(255,255,255,.02);
margin-bottom:5px;
transition:border-color .25s,background .25s;
position:relative;overflow:hidden;
}
.av-card.c-active{
border-color:rgba(129,140,248,.5);
background:rgba(129,140,248,.08);
animation:av-glow-blue 2s ease-in-out infinite;
}
.av-card.c-active.c-error-path{
border-color:rgba(248,113,113,.5);
background:rgba(248,113,113,.08);
animation:av-glow-red 2s ease-in-out infinite;
}
.av-card.c-done{
border-color:rgba(74,222,128,.2);
background:rgba(74,222,128,.04);
}
.av-card.c-error{
border-color:rgba(248,113,113,.35);
background:rgba(248,113,113,.06);
}
.av-card.c-idle{opacity:.45;}
/* left-edge stripe */
.av-card::before{
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
border-radius:10px 0 0 10px;background:transparent;
}
.av-card.c-active::before {background:linear-gradient(180deg,#818cf8,#a78bfa);}
.av-card.c-active.c-error-path::before{background:linear-gradient(180deg,#f87171,#fca5a5);}
.av-card.c-done::before {background:linear-gradient(180deg,#4ade80,#34d399);}
.av-card.c-error::before {background:linear-gradient(180deg,#f87171,#fca5a5);}
/* avatar */
.av-avatar{
width:34px;height:34px;border-radius:9px;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
font-size:17px;
background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.07);
}
.av-card.c-active .av-avatar{background:rgba(129,140,248,.15);border-color:rgba(129,140,248,.3);}
.av-card.c-active.c-error-path .av-avatar{background:rgba(248,113,113,.15);border-color:rgba(248,113,113,.3);}
.av-card.c-done .av-avatar{background:rgba(74,222,128,.12);border-color:rgba(74,222,128,.25);}
.av-card.c-error .av-avatar{background:rgba(248,113,113,.12);border-color:rgba(248,113,113,.25);}
/* text block */
.av-info{flex:1;min-width:0;}
.av-name-row{display:flex;align-items:center;gap:6px;margin-bottom:2px;}
.av-name{font-size:12.5px;font-weight:600;color:rgba(226,232,240,.9);}
.av-card.c-idle .av-name{color:rgba(226,232,240,.4);}
.av-model{
font-size:9.5px;font-weight:700;letter-spacing:.06em;
padding:1px 5px;border-radius:5px;
background:rgba(255,255,255,.06);color:rgba(226,232,240,.4);
border:1px solid rgba(255,255,255,.07);
}
.av-card.c-active .av-model{background:rgba(129,140,248,.18);color:rgba(165,180,252,.9);border-color:rgba(129,140,248,.3);}
.av-card.c-active.c-error-path .av-model{background:rgba(248,113,113,.18);color:rgba(252,165,165,.9);border-color:rgba(248,113,113,.3);}
.av-role{font-size:10.5px;color:rgba(148,163,184,.6);}
.av-card.c-active .av-role{color:rgba(148,163,184,.85);}
/* thinking row */
.av-thinking{
display:flex;align-items:center;gap:5px;
margin-top:5px;font-size:10.5px;color:rgba(129,140,248,.85);
animation:av-slide .25s ease;
}
.av-thinking.t-error{color:rgba(248,113,113,.85);}
.av-th-dots{display:flex;gap:2px;}
.av-th-d{width:4px;height:4px;border-radius:50%;background:rgba(129,140,248,.8);}
.t-error .av-th-d{background:rgba(248,113,113,.8);}
.av-th-d:nth-child(1){animation:av-dot1 1.2s ease-in-out infinite;}
.av-th-d:nth-child(2){animation:av-dot2 1.2s ease-in-out infinite;}
.av-th-d:nth-child(3){animation:av-dot3 1.2s ease-in-out infinite;}
/* status icon */
.av-status-icon{
width:18px;height:18px;border-radius:50%;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
font-size:10px;font-weight:700;align-self:center;
}
.av-card.c-idle .av-status-icon{background:rgba(255,255,255,.04);color:rgba(148,163,184,.25);}
.av-card.c-done .av-status-icon{background:rgba(74,222,128,.18);color:#4ade80;}
.av-card.c-error .av-status-icon{background:rgba(248,113,113,.18);color:#f87171;}
.av-spin-ring{
width:16px;height:16px;border-radius:50%;
border:2px solid rgba(129,140,248,.2);border-top-color:#818cf8;
animation:av-spin .75s linear infinite;
}
.c-error-path .av-spin-ring{border-color:rgba(248,113,113,.2);border-top-color:#f87171;}
/* ── Connector ── */
.av-conn{display:block;width:100%;margin:0 0 5px 0;text-align:center;}
.av-conn-line{
display:inline-block;
width:2px;height:14px;
background:linear-gradient(180deg,rgba(255,255,255,.07),rgba(255,255,255,.04));
border-radius:1px;
}
.av-conn-line.ln-done {background:linear-gradient(180deg,rgba(74,222,128,.3),rgba(74,222,128,.15));}
.av-conn-line.ln-error{background:linear-gradient(180deg,rgba(248,113,113,.3),rgba(248,113,113,.15));}
/* ── Debug branch box ── */
.av-debug-box{
margin:0 0 5px 0;
padding:9px 9px 7px;
border-radius:10px;
border:1px solid rgba(248,113,113,.12);
border-left:3px solid rgba(248,113,113,.15);
background:rgba(248,113,113,.03);
}
.av-debug-box.db-active{
border-color:rgba(248,113,113,.3);border-left-color:#f87171;
background:rgba(248,113,113,.07);
animation:av-glow-red 2.5s ease-in-out infinite;
}
.av-debug-label{
font-size:9px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
color:rgba(248,113,113,.35);margin-bottom:7px;
display:flex;align-items:center;gap:5px;
}
.av-debug-box.db-active .av-debug-label{color:rgba(248,113,113,.7);}
.av-debug-label-icon{
width:14px;height:14px;border-radius:50%;
background:rgba(248,113,113,.15);
display:flex;align-items:center;justify-content:center;font-size:8px;
}
/* mini agent inside debug box */
.av-mini{
display:flex;align-items:center;gap:8px;
padding:6px 8px;border-radius:8px;
border:1px solid rgba(255,255,255,.04);
background:rgba(255,255,255,.02);margin-bottom:4px;
}
.av-mini:last-child{margin-bottom:0;}
.av-mini.m-active{border-color:rgba(248,113,113,.35);background:rgba(248,113,113,.07);}
.av-mini.m-done {border-color:rgba(74,222,128,.2);background:rgba(74,222,128,.04);}
.av-mini.m-error {border-color:rgba(248,113,113,.2);}
.av-mini.m-idle {opacity:.4;}
.av-mini-avatar{
font-size:14px;width:26px;height:26px;border-radius:7px;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06);
}
.av-mini.m-active .av-mini-avatar{background:rgba(248,113,113,.12);border-color:rgba(248,113,113,.25);}
.av-mini.m-done .av-mini-avatar{background:rgba(74,222,128,.1);border-color:rgba(74,222,128,.2);}
.av-mini-info{flex:1;}
.av-mini-name{font-size:11px;font-weight:500;color:rgba(226,232,240,.85);}
.av-mini.m-idle .av-mini-name{color:rgba(226,232,240,.35);}
.av-mini-role{font-size:9.5px;color:rgba(148,163,184,.5);}
.av-mini-right{display:flex;align-items:center;gap:4px;}
.av-mini-model{
font-size:9px;font-weight:600;padding:1px 5px;border-radius:4px;
background:rgba(255,255,255,.05);color:rgba(148,163,184,.45);
}
.av-mini.m-active .av-mini-model{background:rgba(248,113,113,.15);color:rgba(252,165,165,.8);}
.av-mini-spin{
width:12px;height:12px;border-radius:50%;
border:1.5px solid rgba(248,113,113,.2);border-top-color:#f87171;
animation:av-spin .75s linear infinite;
}
.av-mini-tick {font-size:10px;color:#4ade80;}
.av-mini-cross{font-size:10px;color:#f87171;}
/* ── Legend ── */
.av-legend{
display:flex;flex-wrap:wrap;gap:10px;
margin-top:8px;padding-top:7px;
border-top:1px solid rgba(255,255,255,.06);
font-size:9.5px;color:rgba(148,163,184,.4);
}
</style>"""
# ── HTML builders ───────────────────────────────────────────────── #
def _conn(prev_state: str) -> str:
cls = "ln-done" if prev_state == "done" else ("ln-error" if prev_state == "error" else "")
return f'<div class="av-conn"><div class="av-conn-line {cls}"></div></div>'
def _agent_card(node_id: str, ns: dict, ma: dict, error_path: bool = False) -> str:
state = ns.get(node_id, "idle")
a = AGENTS[node_id]
card_cls = f"c-{state}"
if state == "active" and error_path:
card_cls += " c-error-path"
if state == "active":
status_html = '<div class="av-spin-ring"></div>'
elif state == "done":
status_html = '<div class="av-status-icon">βœ“</div>'
elif state == "error":
status_html = '<div class="av-status-icon">βœ•</div>'
else:
status_html = '<div class="av-status-icon">Β·</div>'
thinking = ""
if state == "active":
t_cls = "t-error" if error_path else ""
thinking = (
f'<div class="av-thinking {t_cls}">'
f'<div class="av-th-dots">'
f'<div class="av-th-d"></div><div class="av-th-d"></div><div class="av-th-d"></div>'
f'</div>'
f'{THINKING_MSGS.get(node_id, "Working...")}'
f'</div>'
)
label = _model_label(ma, a.get("ma_key"))
model_badge = f'<span class="av-model">{label}</span>' if label else ""
return (
f'<div class="av-card {card_cls}">'
f'<div class="av-avatar">{a["icon"]}</div>'
f'<div class="av-info">'
f'<div class="av-name-row">'
f'<span class="av-name">{a["name"]}</span>'
f'{model_badge}'
f'</div>'
f'<div class="av-role">{a["role"]}</div>'
f'{thinking}'
f'</div>'
f'{status_html}'
f'</div>'
)
def _mini_card(node_id: str, ns: dict, ma: dict) -> str:
state = ns.get(node_id, "idle")
a = AGENTS[node_id]
if state == "active":
right = '<div class="av-mini-spin"></div>'
elif state == "done":
right = '<span class="av-mini-tick">βœ“</span>'
elif state == "error":
right = '<span class="av-mini-cross">βœ•</span>'
else:
right = ""
label = _model_label(ma, a.get("ma_key"))
model_html = f'<span class="av-mini-model">{label}</span>' if label else ""
return (
f'<div class="av-mini m-{state}">'
f'<div class="av-mini-avatar">{a["icon"]}</div>'
f'<div class="av-mini-info">'
f'<div class="av-mini-name">{a["name"]}</div>'
f'<div class="av-mini-role">{a["role"]}</div>'
f'</div>'
f'<div class="av-mini-right">{model_html}{right}</div>'
f'</div>'
)
def _debug_box(ns: dict, ma: dict) -> str:
is_active = any(ns.get(n, "idle") != "idle" for n in ("classify", "debug", "hitl"))
db_cls = "db-active" if is_active else ""
return (
f'<div class="av-debug-box {db_cls}">'
f'<div class="av-debug-label">'
f'<div class="av-debug-label-icon">⚑</div>'
f'Debug loop β€” auto-retry'
f'</div>'
+ _mini_card("classify", ns, ma)
+ _mini_card("debug", ns, ma)
+ _mini_card("hitl", ns, ma)
+ '</div>'
)
# ── Main builder ─────────────────────────────────────────────────── #
def _build(status: PipelineStatus, iterations: int, is_error: bool, ma: dict) -> str:
s = status.value if hasattr(status, "value") else str(status)
ns = _get_ns(status)
kind, msg = STATUS_MSGS.get(s, ("idle", s))
bc = {"running": "bc-run", "error": "bc-err", "done": "bc-ok", "warn": "bc-warn"}.get(kind, "")
banner_cls = {"running": "b-running", "error": "b-error", "done": "b-done", "warn": "b-warn"}.get(kind, "")
retry = (
f'<span class="av-retry-badge">retry {iterations}</span>'
if iterations > 0 and s not in ("idle", "success") else ""
)
profile = ma.get("profile", "")
profile_labels = {
"reasoning": ("πŸ”’", "#818cf8", "Reasoning mode"),
"sql": ("πŸ—„οΈ", "#38bdf8", "SQL mode"),
"coding": ("⌨️", "#a78bfa", "Coding mode"),
"balanced": ("βš–οΈ", "#94a3b8", "Balanced"),
"manual": ("πŸŽ›οΈ", "#fb923c", "Manual"),
}
profile_badge = ""
if profile and profile in profile_labels:
p_icon, p_color, p_label = profile_labels[profile]
reason_tip = ma.get("reason", "")
profile_badge = (
f'<span title="{reason_tip}" style="font-size:9px;font-weight:700;'
f'padding:2px 7px;border-radius:8px;border:1px solid {p_color}44;'
f'color:{p_color};background:{p_color}18;flex-shrink:0;cursor:help;">'
f'{p_icon} {p_label}</span>'
)
debug_active = any(ns.get(n, "idle") != "idle" for n in ("classify", "debug", "hitl"))
exec_state = ns.get("exec", "idle")
html = (
CSS
+ '<div class="av-wrap">'
+ f'<div class="av-banner {banner_cls}" style="gap:8px;">'
+ f'<div class="av-banner-scan"></div>'
+ f'<div class="av-beacon {bc}"></div>'
+ f'<div class="av-banner-text">{msg}</div>'
+ f'{profile_badge}'
+ f'{retry}'
+ '</div>'
+ '<div class="av-section">Agent pipeline</div>'
+ _agent_card("clarify", ns, ma)
+ _conn(ns.get("clarify", "idle"))
+ _agent_card("detect", ns, ma)
+ _conn(ns.get("detect", "idle"))
+ _agent_card("plan", ns, ma)
+ _conn(ns.get("plan", "idle"))
+ _agent_card("gen", ns, ma)
+ _conn(ns.get("gen", "idle"))
+ _agent_card("exec", ns, ma)
+ _conn(exec_state)
)
html += _debug_box(ns, ma)
html += _conn(exec_state if not debug_active else ns.get("debug", ns.get("hitl", exec_state)))
html += (
_agent_card("test", ns, ma)
+ _conn(ns.get("test", "idle"))
+ _agent_card("explain", ns, ma)
+ _conn(ns.get("explain", "idle"))
+ _agent_card("complete", ns, ma)
+ '<div class="av-legend">'
+ '<span>Β· idle</span>'
+ '<span style="color:#818cf8">⟳ active</span>'
+ '<span style="color:#4ade80">βœ“ done</span>'
+ '<span style="color:#f87171">βœ• error</span>'
+ '<span style="color:rgba(248,113,113,.5)">⚑ debug loop</span>'
+ '</div>'
+ '</div>'
)
return html