error highlighting and message improvements

This commit is contained in:
2026-03-31 21:15:51 -07:00
parent 5ea16d4e43
commit cbfd15ac06
5 changed files with 54 additions and 8 deletions

View File

@@ -45,6 +45,13 @@ def _is_link(value: Any) -> bool:
) )
class NodeExecutionError(Exception):
"""Wraps an error that occurred while executing a specific node."""
def __init__(self, node_id: str, original: Exception):
self.node_id = node_id
super().__init__(str(original))
class ExecutionEngine: class ExecutionEngine:
"""Synchronous (blocking) graph executor. Run inside a thread pool from async code.""" """Synchronous (blocking) graph executor. Run inside a thread pool from async code."""
@@ -99,12 +106,15 @@ class ExecutionEngine:
class_name = node_def["class_type"] class_name = node_def["class_type"]
if class_name not in NODE_CLASS_MAPPINGS: if class_name not in NODE_CLASS_MAPPINGS:
raise ValueError(f"Unknown node type: '{class_name}'") raise NodeExecutionError(node_id, ValueError(f"Unknown node type: '{class_name}'"))
cls = NODE_CLASS_MAPPINGS[class_name] cls = NODE_CLASS_MAPPINGS[class_name]
raw_inputs = node_def.get("inputs", {}) raw_inputs = node_def.get("inputs", {})
input_types = cls.INPUT_TYPES() input_types = cls.INPUT_TYPES()
inputs = self._resolve_inputs(raw_inputs, node_outputs, input_types) try:
inputs = self._resolve_inputs(raw_inputs, node_outputs, input_types)
except Exception as exc:
raise NodeExecutionError(node_id, exc) from exc
input_signature = self._build_input_signature(class_name, raw_inputs, node_output_signatures) input_signature = self._build_input_signature(class_name, raw_inputs, node_output_signatures)
cache_entry = self._get_cached_entry(node_id, class_name, input_signature) cache_entry = self._get_cached_entry(node_id, class_name, input_signature)
@@ -118,8 +128,13 @@ class ExecutionEngine:
instance = cls() instance = cls()
func = getattr(instance, cls.FUNCTION) func = getattr(instance, cls.FUNCTION)
start_time = perf_counter() start_time = perf_counter()
with active_node(node_id): try:
result = func(**inputs) with active_node(node_id):
result = func(**inputs)
except NodeExecutionError:
raise
except Exception as exc:
raise NodeExecutionError(node_id, exc) from exc
elapsed_ms = (perf_counter() - start_time) * 1000.0 elapsed_ms = (perf_counter() - start_time) * 1000.0
# Nodes must return a tuple; coerce single values just in case # Nodes must return a tuple; coerce single values just in case

View File

@@ -116,7 +116,7 @@ def create_app(
from backend.plugin_loader import load_plugins from backend.plugin_loader import load_plugins
load_plugins(plugins_dir()) load_plugins(plugins_dir())
from backend.execution import ExecutionEngine, new_prompt_id from backend.execution import ExecutionEngine, NodeExecutionError, new_prompt_id
from backend.node_registry import NODE_CLASS_MAPPINGS, get_all_node_info from backend.node_registry import NODE_CLASS_MAPPINGS, get_all_node_info
ensure_runtime_dirs(with_plugins=_plugins_on) ensure_runtime_dirs(with_plugins=_plugins_on)
@@ -522,6 +522,12 @@ def create_app(
), ),
) )
broadcast(session_id, {"type": "execution_complete", "data": {"prompt_id": prompt_id}}) broadcast(session_id, {"type": "execution_complete", "data": {"prompt_id": prompt_id}})
except NodeExecutionError as exc:
log.exception("Execution error on node %s", exc.node_id)
broadcast(session_id, {
"type": "execution_error",
"data": {"node_id": exc.node_id, "message": str(exc)},
})
except Exception as exc: except Exception as exc:
log.exception("Execution error") log.exception("Execution error")
broadcast(session_id, { broadcast(session_id, {

View File

@@ -1300,7 +1300,7 @@ function Flow() {
case 'execution_start': case 'execution_start':
setNodes((ns) => ns.map((n) => ({ setNodes((ns) => ns.map((n) => ({
...n, ...n,
data: { ...n.data, processingTimeMs: null }, data: { ...n.data, processingTimeMs: null, error: null },
}))); })));
setExecutingNodeId(null); setExecutingNodeId(null);
setStatus({ text: 'Running workflow…', level: 'info' }); setStatus({ text: 'Running workflow…', level: 'info' });
@@ -1315,7 +1315,12 @@ function Flow() {
break; break;
case 'execution_error': case 'execution_error':
setExecutingNodeId(null); setExecutingNodeId(null);
setStatus({ text: 'Error: ' + msg.data.message, level: 'error' }); if (msg.data.node_id) {
updateNodeData(msg.data.node_id, { error: msg.data.message });
}
if (!msg.data.node_id) {
setStatus({ text: 'Error: ' + msg.data.message, level: 'error' });
}
console.error('[tono] execution error', msg.data); console.error('[tono] execution error', msg.data);
break; break;
case 'preview': case 'preview':

View File

@@ -1182,7 +1182,7 @@ function CustomNode({ id, data }) {
return ( return (
<> <>
{ctx?.executingNodeId === id && <div className="node-executing-glow" aria-hidden="true" />} {ctx?.executingNodeId === id && <div className="node-executing-glow" aria-hidden="true" />}
<div className="custom-node"> <div className={`custom-node${data.error ? ' node-error' : ''}`}>
{/* Title */} {/* Title */}
<div className="node-title drag-handle" style={{ background: catColor }}> <div className="node-title drag-handle" style={{ background: catColor }}>
<div className="node-title-left"> <div className="node-title-left">
@@ -1289,6 +1289,11 @@ function CustomNode({ id, data }) {
<div className="node-warning">{data.warning}</div> <div className="node-warning">{data.warning}</div>
)} )}
{/* Error notification */}
{data.error && (
<div className="node-error-message">{data.error}</div>
)}
{scalarDisplay && !standaloneWidgets.some((w) => w.opts?.text_input) && ( {scalarDisplay && !standaloneWidgets.some((w) => w.opts?.text_input) && (
<div className="node-value-display"> <div className="node-value-display">
<div className="node-value-box"> <div className="node-value-box">

View File

@@ -797,6 +797,21 @@ html, body, #root {
border-bottom: 1px solid var(--warning-border); border-bottom: 1px solid var(--warning-border);
} }
.custom-node.node-error {
outline: 2px solid #ef4444;
outline-offset: -1px;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.35);
}
.node-error-message {
padding: 3px 10px;
font-size: 10px;
color: #fca5a5;
background: rgba(239, 68, 68, 0.12);
border-top: 1px solid rgba(239, 68, 68, 0.3);
border-bottom: 1px solid rgba(239, 68, 68, 0.3);
}
.node-value-display { .node-value-display {
padding: 8px 10px 4px; padding: 8px 10px 4px;
} }