From 547ca0d13b723c078604201cc84fe78a35dbf729 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 19 Jul 2026 18:09:22 -0700 Subject: [PATCH] [eric] app-builder: render-clean gate now covers agent-mode app builds (onboarding), not just view-builder, so a broken app never finishes 'done' --- .../manager/streaming/post_tool_hook.py | 27 +++++++++++-------- .../agents/manager/streaming/stop_hook.py | 5 ++-- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/backend/apps/agents/manager/streaming/post_tool_hook.py b/backend/apps/agents/manager/streaming/post_tool_hook.py index 68902019..26f5e0ae 100644 --- a/backend/apps/agents/manager/streaming/post_tool_hook.py +++ b/backend/apps/agents/manager/streaming/post_tool_hook.py @@ -94,23 +94,28 @@ async def post_tool_hook(ctx: HookContext, input_data: dict, tool_use_id, contex "yarn add", "yarn install", "yarn remove", )) - if session.mode == "view-builder" and (wrote_frontend_file or installed_pkg): - view_builder_dirty_sessions.add(session.id) + if wrote_frontend_file or installed_pkg: try: from backend.apps.outputs.runtime import ( manager as outputs_runtime_manager, ) - outputs_runtime_manager.reset_render_state_for_workspace(session.id) + # ANY session building an app has a preview runtime attached: the dedicated view-builder AND + # a plain agent using CreateApp (how onboarding builds its dashboard). Gate on the runtime, + # not the mode, so the Stop render-gate covers agent-mode app builds too (a plain /frontend/ + # write with no runtime, e.g. editing OpenSwarm's own source, has none and is skipped). + if outputs_runtime_manager.get(session.id) is not None: + view_builder_dirty_sessions.add(session.id) + outputs_runtime_manager.reset_render_state_for_workspace(session.id) + if installed_pkg: + # Tell the app card this turn changed deps so its turn-finish reload restarts Vite; a soft webview reload can't pick up newly installed packages. + try: + await ws_manager.send_to_session(session.id, "agent:app_deps_changed", { + "session_id": session.id, + }) + except Exception: + pass except Exception: pass - if installed_pkg: - # Tell the app card this turn changed deps so its turn-finish reload restarts Vite; a soft webview reload can't pick up newly installed packages. - try: - await ws_manager.send_to_session(session.id, "agent:app_deps_changed", { - "session_id": session.id, - }) - except Exception: - pass # Every write drains, App Builder included. This was an `elif` on the branch above, so a view-builder frontend write took that branch and skipped the drain: the one agent whose whole job is the app never saw its own vite/babel/tsc errors. if wrote_files and file_path: errs: list[str] = [] diff --git a/backend/apps/agents/manager/streaming/stop_hook.py b/backend/apps/agents/manager/streaming/stop_hook.py index 7d034d71..b6539ee4 100644 --- a/backend/apps/agents/manager/streaming/stop_hook.py +++ b/backend/apps/agents/manager/streaming/stop_hook.py @@ -26,8 +26,9 @@ async def stop_hook(ctx: HookContext, input_data: dict, tool_use_id, context) -> to render, blocks with the error so the agent fixes it, up to MAX_RETRIES then lets the stop through.""" session = ctx.session - if session.mode != "view-builder": - return {} + # Gate on the DIRTY set (any app-building session that wrote a frontend file with a runtime + # attached), not the mode: onboarding builds its dashboard in a plain agent session via CreateApp, + # which must be render-gated too, not just the dedicated view-builder. if session.id not in view_builder_dirty_sessions: return {} from backend.apps.outputs.runtime import (