From 0456b3d25c61e2593ce2bb07def9515b3441671c Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 4 Mar 2026 20:11:11 +0000 Subject: [PATCH] Fix _aser_call and sx_call list serialization: use (list ...) for data arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Data lists (dicts, strings, numbers) were wrapped in (<> ...) fragments which the client rendered as empty DocumentFragments instead of iterable arrays. This broke map/filter over cards, tag_groups, and authors in blog index and similar components. - _aser_call: data lists → (list ...), rendered content (SxExpr) → (<> ...) - sx_call: all list kwargs → (list ...) Co-Authored-By: Claude Opus 4.6 --- shared/sx/async_eval.py | 26 ++++++++++++++++++-------- shared/sx/helpers.py | 13 +++++++++++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/shared/sx/async_eval.py b/shared/sx/async_eval.py index 9c25fbf..49f4d1f 100644 --- a/shared/sx/async_eval.py +++ b/shared/sx/async_eval.py @@ -1253,15 +1253,25 @@ async def _aser_call( extra_class = val.class_name else: parts.append(f":{arg.name}") - # Plain list (e.g. from map) → wrap as fragment to - # avoid ambiguity with function application on re-parse + # Plain list → serialize for the client. + # Rendered items (SxExpr) → wrap in (<> ...) fragment. + # Data items (dicts, strings, numbers) → (list ...) + # so the client gets an iterable array, not a + # DocumentFragment that breaks map/filter. if isinstance(val, list): - items = [serialize(v) for v in val - if v is not NIL and v is not None] - parts.append( - "(<> " + " ".join(items) + ")" if items - else "nil" - ) + live = [v for v in val + if v is not NIL and v is not None] + items = [serialize(v) for v in live] + if not items: + parts.append("nil") + elif any(isinstance(v, SxExpr) for v in live): + parts.append( + "(<> " + " ".join(items) + ")" + ) + else: + parts.append( + "(list " + " ".join(items) + ")" + ) else: parts.append(serialize(val)) i += 2 diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py index 84982de..cfe5a5d 100644 --- a/shared/sx/helpers.py +++ b/shared/sx/helpers.py @@ -404,13 +404,22 @@ def sx_call(component_name: str, **kwargs: Any) -> str: Values are serialized: strings are quoted, None becomes nil, bools become true/false, numbers stay as-is. + List values use ``(list ...)`` so the client gets an iterable array + rather than a rendered fragment. """ - from .parser import serialize + from .parser import serialize, SxExpr name = component_name if component_name.startswith("~") else f"~{component_name}" parts = [name] for key, val in kwargs.items(): parts.append(f":{key.replace('_', '-')}") - parts.append(serialize(val)) + if isinstance(val, list): + items = [serialize(v) for v in val if v is not None] + if not items: + parts.append("nil") + else: + parts.append("(list " + " ".join(items) + ")") + else: + parts.append(serialize(val)) return "(" + " ".join(parts) + ")"