Say what went wrong on a node in one line

The bubble opened with the loader's own frame — `exec(compile(...))`
inside `_load_function` — before anything about the user's code, and
then said all of it twice, because a load failure arrives both as the
node's status and as a node_error issue carrying the same text. It is
now `Type: message (line N)`, deduplicated, with the line taken from the
node's own file. Full tracebacks still go to the server log.

Long messages grow the bubble instead of stretching it, and scroll only
past 240px. The tooltip arrow is gone: it read as a rotated square notch
under the bubble, and no better anywhere else it appeared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkmeRiyeYmVZqJVwuyHq9o
This commit is contained in:
Melvin Strobl
2026-08-15 21:39:59 +02:00
co-authored by Claude Opus 5
parent 3b72d17ca7
commit 2bc9f684e8
3 changed files with 26 additions and 9 deletions
+19 -5
View File
@@ -351,7 +351,7 @@ class FlowController:
except Exception as exc:
logger.warning("Node '%s' failed to load: %s", node_id, exc)
entry.status = NodeStatus.ERROR
entry.error = _short_traceback(exc)
entry.error = _short_error(exc)
return entry
# -------------------------------------------------------------------------
@@ -405,7 +405,21 @@ def _bound(specs: list[MessageSpec]) -> list[MessageSpec]:
return [spec for spec in specs if spec.name]
def _short_traceback(exc: Exception) -> str:
"""The last frames of a failure, which is what a node author needs."""
lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
return "".join(lines[-3:]).strip()
def _short_error(exc: Exception) -> str:
"""One line a node author can act on: what broke, and where in their code.
The frames in between belong to the loader rather than to the node, so only
the line in the node's own source is kept. The full traceback stays in the
server log.
"""
if isinstance(exc, SyntaxError):
# Its own message already names the compiled file, which is noise here.
return f"{type(exc).__name__}: {exc.msg} (line {exc.lineno})"
frames = [
frame
for frame in traceback.extract_tb(exc.__traceback__)
if frame.filename.startswith("<node ")
]
where = f" (line {frames[-1].lineno})" if frames else ""
return f"{type(exc).__name__}: {exc}{where}"
+6 -2
View File
@@ -83,9 +83,13 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
// Whatever is wrong — it failed to load, it failed to run, or the graph
// around it does not add up — is the same red dot with the same explanation.
const problem = [live?.status === "error" ? live.error : null, issueText]
// A load failure arrives twice, as node status and as a validation issue, so
// identical lines collapse into one.
const lines = [live?.status === "error" ? live.error : null, issueText]
.filter(Boolean)
.join("\n")
.split("\n")
const problem = [...new Set(lines)].join("\n")
const status = problem ? "error" : live?.status
const style = STATUS_STYLES[status as keyof typeof STATUS_STYLES]
@@ -126,7 +130,7 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
aria-label={style.label}
/>
</TooltipTrigger>
<TooltipContent className="max-w-xs whitespace-pre-line">
<TooltipContent className="max-h-60 max-w-xs overflow-y-auto whitespace-pre-line break-words">
{problem || style.label}
</TooltipContent>
</Tooltip>
+1 -2
View File
@@ -34,7 +34,7 @@ function TooltipTrigger({
function TooltipContent({
className,
sideOffset = 0,
sideOffset = 4,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
@@ -50,7 +50,6 @@ function TooltipContent({
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)