refactor(preview): simplify compile and validation paths

Load Sucrase only when a JSX/TSX preview is opened, remove the hand-written transform, and leave subjective render evaluation to the reader while retaining structural fence validation.
This commit is contained in:
2026-08-26 03:37:29 -05:00
parent 7262e7730e
commit 952ef8a0c4
10 changed files with 399 additions and 1981 deletions
+4 -113
View File
@@ -139,7 +139,6 @@ async def _normalize_to_async_generator(maybe_iterable) -> AsyncGenerator[str, N
pending_approvals: Dict[str, Dict[str, Any]] = {}
_APPROVAL_TIMEOUT = 300 # seconds; a timeout is treated as "deny all"
def _as_tool_calls(obj) -> list:
"""Normalize a parsed JSON value into Ollama-style tool_calls entries."""
if isinstance(obj, list):
@@ -208,27 +207,6 @@ def _coerce_tool_calls(msg: dict, allowed_names: set[str] | None = None) -> list
return []
_VISUAL_HINTS = _tools._RENDER_HINTS
def _render_nudge_text() -> str:
"""The one retry given to a model that ignored render_preview on a visual ask.
It arrives as a *user* turn, which means the model answers whatever it says.
Earlier wording pointed at "THIS user request" — a thing the model cannot
see — and offered "if the user's term is unclear, ask them to clarify". It
took both: two transcripts answered with "please provide the user's request
for the rendering" and nothing else. So this says only what to do next, with
no dangling reference and no escape hatch, and it names the languages the
render window actually supports rather than a hardcoded pair."""
return (
f"Use the render_preview tool now. Send complete {_tools._lang_prose()} "
f"markup drawn on a {_tools._STAGE_W}x{_tools._STAGE_H} stage, with the "
"values computed into an array and plotted point by point. Do not write "
"a ``` fence yourself."
)
def _strip_internal_turns(messages: list) -> list:
"""Flatten tool-loop messages for the final, tool-free streaming turn.
@@ -236,15 +214,13 @@ def _strip_internal_turns(messages: list) -> list:
the tools schema isn't re-sent. Their content must not go with them, though:
search/memory/document results are the reason the loop ran. Preserve those
results as an explicitly untrusted user-context turn immediately before the
real request, while dropping assistant tool-call envelopes and the synthetic
render nudge. Keeping the real request last also prevents the model from
answering the nudge or treating a tool result as the user's question."""
nudge = _render_nudge_text()
real request, while dropping assistant tool-call envelopes. Keeping the real
request last prevents the model from treating a tool result as the user's
question."""
kept = [
m for m in messages
if m.get("role") != "tool"
and not m.get("tool_calls")
and m.get("content") != nudge
]
results = [
str(m.get("content") or "")
@@ -270,35 +246,6 @@ def _strip_internal_turns(messages: list) -> list:
return kept
def _should_nudge_render(messages: list, tool_schemas: list | None) -> bool:
"""True when render_preview is available, unused, and the user asked for a visual."""
names = {
(s.get("function") or {}).get("name")
for s in (tool_schemas or [])
if isinstance(s, dict)
}
if "render_preview" not in names:
return False
for m in messages:
if m.get("role") == "assistant":
for c in (m.get("tool_calls") or []):
if (c.get("function") or {}).get("name") == "render_preview":
return False
if m.get("role") == "tool":
try:
body = _json.loads(m.get("content") or "")
if isinstance(body, dict) and ("fence" in body or "issues" in body):
return False
except Exception:
pass
user = ""
for m in reversed(messages):
if m.get("role") == "user":
user = (m.get("content") or "").lower()
break
return _tools.wants_render_preview(user)
async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, num_gpu,
conversation_id="", policy="allow"):
"""Let the model call tools before the final streamed answer.
@@ -317,8 +264,6 @@ async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, nu
# Let the UI show activity immediately — the first tool-turn is a full
# non-stream generation and can sit silent for a long time otherwise.
yield "__status__tools"
nudged_render = False
render_rejects = 0
allowed_names = {
(schema.get("function") or {}).get("name")
for schema in (tool_schemas or [])
@@ -333,11 +278,6 @@ async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, nu
break # None/error or no tool support -> fall back to plain stream
calls = _coerce_tool_calls(msg, allowed_names)
if not calls:
# One retry: small models often skip render_preview on visual asks.
if not nudged_render and _should_nudge_render(messages, tool_schemas):
nudged_render = True
messages.append({"role": "user", "content": _render_nudge_text()})
continue
break
# Normalize content-JSON tool calls into the shape later turns expect.
if not msg.get("tool_calls"):
@@ -380,50 +320,19 @@ async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, nu
continue
yield f"__status__{name}"
call_args = fn.get("arguments")
# Tell render_preview how many times it has already turned this
# model away. It withholds its scaffold on a first rejection - a
# complete, styled demo handed to a struggling model gets pasted
# rather than adapted, and then persists into the conversation as a
# template for later requests.
if name == "render_preview" and isinstance(call_args, dict):
call_args = {**call_args, "_attempt": render_rejects}
result = await _tools.dispatch(name, call_args)
messages.append({"role": "tool", "content": result})
# Cap render_preview reject loops — each retry is another full
# non-stream generation and looks like the UI is "stuck thinking".
if name == "render_preview":
try:
body = _json.loads(result)
except Exception:
body = {}
if isinstance(body, dict) and body.get("ok") is False:
render_rejects += 1
if render_rejects >= 2:
stop_after = True
elif isinstance(body, dict) and body.get("ok") is True:
if isinstance(body, dict) and body.get("ok") is True:
# Good fence in hand — let the model write the reply next.
stop_after = True
if stop_after:
break
def _last_ok_render_fence(messages: list) -> tuple[str | None, dict]:
"""Return (fence, tool_payload) from the latest successful render_preview."""
for m in reversed(messages or []):
if m.get("role") != "tool":
continue
try:
body = _json.loads(m.get("content") or "")
except Exception:
continue
if not isinstance(body, dict) or not body.get("ok"):
continue
fence = str(body.get("fence") or "").strip()
if fence.startswith("```"):
return fence, body
return None, {}
# -------------------------
# Streaming implementation
# -------------------------
@@ -471,24 +380,6 @@ async def stream_chat_response(
except Exception:
_logger.exception("tool loop failed; streaming without tools")
# If render_preview already produced a validated fence, emit it ourselves.
# Small models often "paste" a rewritten, broken copy that never runs in Preview.
forced_fence, render_meta = _last_ok_render_fence(messages)
if forced_fence:
label = render_meta.get("title") or "Interactive preview"
reply = f"{label}:\n\n{forced_fence}\n"
_logger.info(
"stream_chat_response: emitting validated render fence (chars=%d)",
len(forced_fence),
)
_synapse_trace(f"\n── TURN [{model} | render fence] {'' * 30}\n")
_synapse_trace(f"USR: {user_message}\n{'' * 50}\n")
_synapse_trace(reply.replace("\n", " ")[:500] + "\n")
step = 64
for i in range(0, len(reply), step):
yield reply[i:i + step]
return
messages = _strip_internal_turns(messages)
_logger.info("stream_chat_response: starting stream (model=%s, turns=%d, timeout=%s)", model, len(messages), timeout)