feat(preview): add sandboxed live code previews

Render validated HTML, SVG, JSX, and TSX fences locally while preserving tool context and preventing explanatory JSON from triggering actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 03:34:19 -05:00
committed by Athena
co-authored by Cursor
parent 42eaed647a
commit 656c14caf3
13 changed files with 3079 additions and 31 deletions
+47 -18
View File
@@ -70,6 +70,20 @@ _MEMORY_PREAMBLE = (
"and personalize your replies:\n\n"
)
# Static capability hint, appended to every system prompt. The live Preview UI
# is frontend-only (Markdown.jsx); the model reaches it by calling the standing
# `render_preview` tool (structured markup in, validated fence out) rather than
# freestyling an empty ```html stub. The tool schema carries the detailed
# requirements; this preamble just points at it.
# See synapse/tools.py: keep this short and imperative for the same reason the
# tool description is — anything narrated here comes back as the model's reply.
_RENDER_PREAMBLE = (
"\n\n---\nRender window: when a visual would help, call the `render_preview` "
f"tool with complete {_tools._lang_prose()} markup, then paste the returned "
"`fence` into your reply. The chat UI renders it live in a sandbox — inline "
"CSS/JS, no network.\n"
)
_CODING_KEYWORDS = frozenset({
"code", "coding", "function", "class", "method", "variable", "bug", "error",
@@ -559,6 +573,15 @@ async def chat_stream_endpoint(payload: Dict[str, Any]):
separator = "\n\n---\nWeb search results (treat as current information):\n\n"
system_prompt = (system_prompt + separator + search_results) if system_prompt else search_results
# Capability hint, on the same condition as the tool it points at (see
# the standing_schemas call below). It used to be unconditional, and a
# small model asked to summarise LRU caches answered that "the LRU cache
# is implemented using a tool called render_preview... renders it live in
# a sandbox" — this text, recited as fact. A hint for a tool that isn't
# being offered is pure contamination.
if _tools.wants_render_preview(message):
system_prompt = (system_prompt + _RENDER_PREAMBLE) if system_prompt else _RENDER_PREAMBLE.lstrip()
# ── MindTrace pre-flight ──────────────────────────────────────────
_trace_intent = _detect_intent(message) if message else "chat"
if payload.get("model"):
@@ -618,29 +641,35 @@ async def chat_stream_endpoint(payload: Dict[str, Any]):
if images:
metadata["images"] = images
# Tool-using playbook: advertise the allowlisted tools of the active
# playbook AND of the reference playbooks _route_playbooks picked for
# this message — a routed playbook's instructions are already in the
# prompt, so its abilities have to come with them or the model narrates
# tools it was never given. Action tools follow action_tool_policy:
# off (withheld) / ask (per-call approval, in the tool loop) / allow.
# Tools: playbook allowlist (including routed reference playbooks), plus
# render_preview only when this turn looks like a visual ask. Always
# advertising it forced a non-stream tool round on every chat and felt
# like "stuck thinking".
_policy = app_settings.get("action_tool_policy", "off")
allow_actions = _policy != "off"
_pb_tools = list(dict.fromkeys(
(getattr(_main_pb, "tools", None) or [] if _main_pb else [])
+ [t for pb in context_pbs for t in (getattr(pb, "tools", None) or [])]
))
if _pb_tools:
allow_actions = _policy != "off"
schemas = _tools.schemas_for(_pb_tools, allow_actions)
if schemas:
metadata["tools"] = schemas
metadata["action_tool_policy"] = _policy
metadata["conversation_id"] = conversation_id
_granted = [t for t in _pb_tools if not _tools.is_action(t) or allow_actions]
_withheld = [t for t in _pb_tools if _tools.is_action(t) and not allow_actions]
_synapse_trace(f" TOOLS : {', '.join(_granted)} [actions: {_policy}]\n")
if _withheld:
_synapse_trace(f" WITHHELD: {', '.join(_withheld)} (action tools off)\n")
schemas_by_name: dict = {}
if _tools.wants_render_preview(message) or "render_preview" in _pb_tools:
for s in _tools.standing_schemas():
schemas_by_name[s["function"]["name"]] = s
for s in _tools.schemas_for(_pb_tools, allow_actions):
schemas_by_name[s["function"]["name"]] = s
schemas = list(schemas_by_name.values())
if schemas:
metadata["tools"] = schemas
metadata["action_tool_policy"] = _policy
metadata["conversation_id"] = conversation_id
_granted = [
n for n in schemas_by_name
if not _tools.is_action(n) or allow_actions
]
_withheld = [t for t in _pb_tools if _tools.is_action(t) and not allow_actions]
_synapse_trace(f" TOOLS : {', '.join(_granted)} [actions: {_policy}]\n")
if _withheld:
_synapse_trace(f" WITHHELD: {', '.join(_withheld)} (action tools off)\n")
# Persist conversation and user message before streaming
store.create_conversation(conversation_id, rag_scope or "")