diff --git a/.github/pr-assets/after-quiz-runs-in-iframe.png b/.github/pr-assets/after-quiz-runs-in-iframe.png new file mode 100644 index 0000000..9187afd Binary files /dev/null and b/.github/pr-assets/after-quiz-runs-in-iframe.png differ diff --git a/.github/pr-assets/after-renders-as-page.png b/.github/pr-assets/after-renders-as-page.png new file mode 100644 index 0000000..4e2faa7 Binary files /dev/null and b/.github/pr-assets/after-renders-as-page.png differ diff --git a/.github/pr-assets/before-renders-as-source.png b/.github/pr-assets/before-renders-as-source.png new file mode 100644 index 0000000..92531d7 Binary files /dev/null and b/.github/pr-assets/before-renders-as-source.png differ diff --git a/app/agent_wiki_explorer.py b/app/agent_wiki_explorer.py index e641c88..602c94a 100644 --- a/app/agent_wiki_explorer.py +++ b/app/agent_wiki_explorer.py @@ -1,6 +1,7 @@ from __future__ import annotations import streamlit as st +import streamlit.components.v1 as components from app.utils.agent_wiki import ( WikiDocument, @@ -17,6 +18,10 @@ st.set_page_config(page_title="Agent wiki explorer", page_icon="📚") +# Viewport height for embedded HTML artifacts. Tall enough that a report's first +# screen is visible without scrolling the iframe; the iframe scrolls beyond that. +HTML_ASSET_IFRAME_HEIGHT = 900 + # Map extensions of non-markdown text documents to a syntax-highlighting language # so artifacts like `repro_app.py` render inline as code. CODE_LANGUAGE_BY_EXTENSION = { @@ -30,7 +35,6 @@ ".js": "javascript", ".ts": "typescript", ".css": "css", - ".html": "html", ".sql": "sql", } @@ -80,6 +84,22 @@ def _render_code_asset(document: WikiDocument, language: str) -> None: st.code(document_text, language=language) +def _render_html_asset(document: WikiDocument) -> None: + document_text, document_error = fetch_wiki_document_text(document["path"]) + if document_error: + st.error(document_error) + return + if document_text is None: + st.warning("The selected asset could not be loaded.") + return + st.caption(document["path"]) + # A sandboxed iframe, so the artifact's own stylesheet cannot leak into the + # app chrome and its scripts (e.g. an interactive quiz) still run. + components.html(document_text, height=HTML_ASSET_IFRAME_HEIGHT, scrolling=True) + with st.expander("View source"): + st.code(document_text, language="html") + + def _render_other_asset() -> None: st.info("This asset is not rendered inline.") @@ -132,6 +152,8 @@ def _render_other_asset() -> None: _render_markdown_document(selected_document, document_text) elif selected_document["is_image"]: _render_image_asset(selected_document) +elif selected_document["is_html"]: + _render_html_asset(selected_document) elif selected_document["extension"] in CODE_LANGUAGE_BY_EXTENSION: _render_code_asset(selected_document, CODE_LANGUAGE_BY_EXTENSION[selected_document["extension"]]) else: diff --git a/app/utils/agent_wiki.py b/app/utils/agent_wiki.py index 860cf27..c98ac55 100644 --- a/app/utils/agent_wiki.py +++ b/app/utils/agent_wiki.py @@ -31,6 +31,9 @@ TEXT_DOCUMENT_EXTENSIONS = {".md", ".markdown", ".mdx", ".txt"} MARKDOWN_EXTENSIONS = {".md", ".markdown", ".mdx"} IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"} +# Self-contained HTML artifacts (e.g. visual PR reports). Rendered in a sandboxed +# iframe rather than as source, so their own styling and scripts stay intact. +HTML_EXTENSIONS = {".html", ".htm"} ISSUES_SECTION = "issues" # Canonical filenames the agent wiki uses for runnable issue reproductions. @@ -53,6 +56,7 @@ class WikiDocument(TypedDict): source_url: str is_markdown: bool is_image: bool + is_html: bool class WikiIssueRepro(TypedDict): @@ -149,6 +153,10 @@ def is_image_path(path: str) -> bool: return _get_extension(path) in IMAGE_EXTENSIONS +def is_html_path(path: str) -> bool: + return _get_extension(path) in HTML_EXTENSIONS + + def build_wiki_raw_url(path: str) -> str: return f"{WIKI_RAW_URL_PREFIX}/{quote(path, safe='/')}" @@ -204,6 +212,7 @@ def build_wiki_documents(paths: list[str]) -> list[WikiDocument]: "source_url": build_wiki_source_url(path), "is_markdown": is_markdown_path(path), "is_image": is_image_path(path), + "is_html": is_html_path(path), } ) diff --git a/tests/test_agent_wiki.py b/tests/test_agent_wiki.py index d4310b6..c7609cd 100644 --- a/tests/test_agent_wiki.py +++ b/tests/test_agent_wiki.py @@ -48,6 +48,23 @@ def test_build_wiki_documents_includes_issue_artifacts() -> None: assert documents[1]["folder"] == "issues/12345" +def test_build_wiki_documents_flags_html_artifacts_as_html() -> None: + documents = build_wiki_documents( + [ + "pull-requests/12345/report.html", + "pull-requests/12345/notes.txt", + ] + ) + + report = documents[1] + assert report["path"] == "pull-requests/12345/report.html" + assert report["is_html"] is True + assert report["is_markdown"] is False + assert report["is_image"] is False + # Plain text artifacts keep rendering as code, not in an iframe. + assert documents[0]["is_html"] is False + + def test_get_wiki_folder_groups_issue_artifacts_by_issue_number() -> None: assert get_wiki_folder("issues/12345/repro_app.py") == "issues/12345" assert get_wiki_folder("issues/12345/nested/notes.md") == "issues/12345"