From f4cded781b634a6ade034979c287da0543dbb65c Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 27 Apr 2026 15:09:25 -0700 Subject: [PATCH] [eric] App Builder fixes: preview loads (auth-token threaded into iframe URL + sub-resource paths), agent keeps running when you switch tabs (session+workspace persisted on the app), Settings default model/thinking now flow into App + Skill Builder drafts --- .gitattributes | 1 + backend/apps/outputs/models.py | 9 ++ backend/apps/outputs/outputs.py | 59 +++++++++- backend/main.py | 5 +- .../src/app/pages/Skills/SkillBuilderChat.tsx | 47 +++++++- frontend/src/app/pages/Views/ViewEditor.tsx | 110 +++++++++++++++++- frontend/src/app/pages/Views/ViewPreview.tsx | 21 +++- frontend/src/shared/state/agentsSlice.ts | 14 ++- frontend/src/shared/state/outputsSlice.ts | 4 + 9 files changed, 253 insertions(+), 17 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..0282f448 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +backend/mcp-bundles/** linguist-vendored diff --git a/backend/apps/outputs/models.py b/backend/apps/outputs/models.py index 79e1b922..e25658a6 100644 --- a/backend/apps/outputs/models.py +++ b/backend/apps/outputs/models.py @@ -27,6 +27,11 @@ class Output(BaseModel): permission: str = "ask" auto_run_config: Optional[AutoRunConfig] = None thumbnail: Optional[str] = None + # Linkage so reopening the App Builder reattaches to the in-progress session + # and reuses the same on-disk workspace folder instead of seeding a fresh one + # (which would orphan the running agent + lose chat history on every navigate). + session_id: Optional[str] = None + workspace_id: Optional[str] = None created_at: str = Field(default_factory=lambda: datetime.now().isoformat()) updated_at: str = Field(default_factory=lambda: datetime.now().isoformat()) @@ -71,6 +76,8 @@ class OutputCreate(BaseModel): files: dict[str, str] = Field(default_factory=dict) auto_run_config: Optional[dict[str, Any]] = None thumbnail: Optional[str] = None + session_id: Optional[str] = None + workspace_id: Optional[str] = None @model_validator(mode="before") @classmethod @@ -101,6 +108,8 @@ class OutputUpdate(BaseModel): permission: Optional[str] = None auto_run_config: Optional[dict[str, Any]] = None thumbnail: Optional[str] = None + session_id: Optional[str] = None + workspace_id: Optional[str] = None @model_validator(mode="before") @classmethod diff --git a/backend/apps/outputs/outputs.py b/backend/apps/outputs/outputs.py index eea536ad..4f82d622 100644 --- a/backend/apps/outputs/outputs.py +++ b/backend/apps/outputs/outputs.py @@ -1,5 +1,6 @@ import json import os +import re import logging import mimetypes import base64 @@ -7,6 +8,7 @@ from datetime import datetime from contextlib import asynccontextmanager from fastapi import HTTPException, Query from fastapi.responses import Response +from backend.auth import get_auth_token from jsonschema import validate as schema_validate, ValidationError as SchemaValidationError from backend.config.Apps import SubApp from backend.apps.outputs.models import ( @@ -79,6 +81,53 @@ def _inject_data_into_html(html: str, input_json: str = "{}", result_json: str = return f"{injection}\n{html}" +# URL schemes / prefixes that must NOT have ?token= appended. These are either +# external (CDNs, mailto) or non-network references that the auth middleware +# never sees. Anything else is treated as a same-origin relative URL pointing +# at our /api/outputs/.../serve/ subtree, which DOES need the token. +_ABSOLUTE_URL_PREFIXES = ( + "http://", "https://", "//", "data:", "blob:", + "mailto:", "tel:", "javascript:", "about:", "#", +) + +_HREF_SRC_ATTR_RE = re.compile( + r"""(\s(?:href|src))\s*=\s*(["'])([^"']+)\2""", + re.IGNORECASE, +) + + +def _inject_token_into_relative_urls(html: str, token: str) -> str: + """Append `?token=` to every relative href/src in the served HTML. + + Browsers strip the parent iframe URL's query string before resolving + relative `` / `