From 36510eee7b42100b9430ff83111638ff79b63212 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 17 May 2026 02:17:03 +0000 Subject: [PATCH] fix(findings): namespace per-tool button keys so multi-file render works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: uploading multiple files on the home page and clicking Run analysis blew up with StreamlitDuplicateElementKey: key='_findings_open_02_text_cleaner' when two uploaded files both had Clean Text findings. Root cause: ``render_findings_panel`` is invoked once per uploaded file from ``_home.py``, but the per-tool jump button used a filename-agnostic key: key=f"_findings_open_{tool_id}" Two files both flagging Clean Text → two buttons with identical keys → Streamlit rejects the second one. Fix: - Add ``key_namespace: str = ""`` to ``render_findings_panel``. The helper hashes it (sha1 truncated to 8 chars) and appends to every button key, so different namespaces produce different keys but the same namespace stays stable across reruns. - The home page now passes the filename: ``render_findings_panel(findings, header=f"📄 {name}", key_namespace=name)``. - The single-call site in ``upload_and_analyze_section`` (the legacy helper, only used outside the new home-page path) keeps the default empty namespace, which is fine because that path renders findings for ONE file at a time. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gui/_home.py | 6 +++++- src/gui/components/_legacy.py | 20 ++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/gui/_home.py b/src/gui/_home.py index 5f2a279..d851bc3 100644 --- a/src/gui/_home.py +++ b/src/gui/_home.py @@ -250,7 +250,11 @@ def _home_page() -> None: st.markdown(f"### 📄 {name}") st.success(t("findings.none")) else: - render_findings_panel(findings, header=f"📄 {name}") + render_findings_panel( + findings, + header=f"📄 {name}", + key_namespace=name, + ) st.divider() st.caption(t("chrome.footer")) diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index a516306..7faa282 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -1374,7 +1374,12 @@ def _tool_page_slug(tool_id: str) -> str: return _TOOL_PAGE_PATHS.get(tool_id, "") -def render_findings_panel(findings, *, header: str | None = None) -> None: +def render_findings_panel( + findings, + *, + header: str | None = None, + key_namespace: str = "", +) -> None: """Render a list of :class:`Finding` objects grouped by tool. Each tool gets a header with the count, an open-tool button, and a list @@ -1427,9 +1432,20 @@ def render_findings_panel(findings, *, header: str | None = None) -> None: # rendering blended into the page, making the per-tool # jump non-obvious. The button triggers ``st.switch_page`` # so navigation is still a soft switch (no full reload). + # + # ``key_namespace`` is hashed into the widget key so the + # home page (which calls this once PER uploaded file) + # doesn't collide on the shared tool_id — two files both + # having Clean Text findings would otherwise produce two + # buttons with the same key and Streamlit refuses. + import hashlib as _hashlib + ns = _hashlib.sha1( + (key_namespace or "").encode("utf-8"), + usedforsecurity=False, + ).hexdigest()[:8] if st.button( _t("findings.open_tool", tool=name), - key=f"_findings_open_{tool_id}", + key=f"_findings_open_{tool_id}_{ns}", type="primary", use_container_width=False, ):