diff --git a/static/js/document.js b/static/js/document.js index 0d0aa64..5283b36 100644 --- a/static/js/document.js +++ b/static/js/document.js @@ -152,6 +152,8 @@ import * as Modals from './modalManager.js'; addDocToTabs, syncDocIndicator: _syncDocIndicator, }); + _maybeOpenDocFromHash(); + window.addEventListener('hashchange', _maybeOpenDocFromHash); } /** Update overflow-doc-btn accent indicator, toolbar indicator, and session list icon */ @@ -5811,16 +5813,31 @@ import * as Modals from './modalManager.js'; } try { const res = await fetch(`${API_BASE}/api/document/${docId}`); - if (!res.ok) throw new Error('Not found'); + if (!res.ok) throw new Error(res.status === 404 ? 'Not found' : `HTTP ${res.status}`); const doc = await res.json(); addDocToTabs(doc, doc.session_id); _ensureDocPaneMounted(); switchToDoc(doc.id); } catch (e) { console.error('Failed to load document:', e); + if (uiModule) { + const msg = e.message === 'Not found' + ? 'Document not found — try opening it from the Library.' + : 'Could not open document.'; + uiModule.showError(msg); + } } } + // Deep-link: #document- opens that document on load / URL-bar nav. + // Clicks on in-chat document anchors are handled separately (they call + // preventDefault, so they don't change the hash); this covers refresh + // and pasted/typed document URLs, which previously did nothing. + function _maybeOpenDocFromHash() { + const m = (window.location.hash || '').match(/^#document-(.+)$/); + if (m) loadDocument(m[1]); + } + /** Open panel and ensure a document exists, creating a session if needed */ export async function ensureDocPanel() { let sessionId = _lastSessionId diff --git a/tests/test_document_deeplink.py b/tests/test_document_deeplink.py new file mode 100644 index 0000000..8d73372 --- /dev/null +++ b/tests/test_document_deeplink.py @@ -0,0 +1,33 @@ +"""Regression guards for in-chat document deep-links (#document-). + +The frontend module is browser-coupled (window/fetch/document) so there's +no JS unit harness for it — these pin the source-level invariants that the +404-silent-failure fix depends on. See issue #560. +""" + +from pathlib import Path + +_REPO = Path(__file__).resolve().parents[1] + + +def test_chat_document_links_use_the_document_id(): + """The list/open tool must anchor to the real document id, not a slug — + a slug 404s against the UUID-keyed /api/document/ route.""" + src = (_REPO / "src" / "tool_implementations.py").read_text(encoding="utf-8") + assert "(#document-{d.id})" in src + assert "(#document-{doc.id})" in src + + +def test_document_deeplink_handled_on_hashchange_and_load(): + """#document- in the URL must open the doc on refresh / URL-bar nav, + not just on click.""" + js = (_REPO / "static" / "js" / "document.js").read_text(encoding="utf-8") + assert "addEventListener('hashchange', _maybeOpenDocFromHash)" in js + assert "#document-" in js + + +def test_failed_document_load_surfaces_user_error(): + """A missing/failed document must tell the user, not fail silently.""" + js = (_REPO / "static" / "js" / "document.js").read_text(encoding="utf-8") + assert "uiModule.showError" in js + assert "Document not found" in js