That last `--co` (collect-only) run was my probe mistake — it shows 34% because no tests execute under collect-only. The valid coverage run was the real gate command executed earlier: `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (exit 0, all 3544 tests executed). All gates are verified. ## Phase 153 — final verification pass report **Verified (all tasks 01–04 complete in `complete/`):** - Modal `fullPageUrl(source, path, back = null)` + `openDocumentModal(…, back = null)` with module-level `currentBack`; 5 chat call sites pass `"/"`, sources.js call site unchanged (byte-identical Sources path); `documentUrl` comment now true - Copy-path control (shared `renderDocument` core, icon+text, text = accessible name) on both surfaces; per-page clipboard helpers with the fallback-field + status-line contract; `#doc-copy-announcer` live region; styles + 44px touch floor - Unit pins: `&back=%2F` / no-`back` byte-identity, `currentBack` single-writer guard, copy strings byte-pinned, phase-13 regression pins untouched **Test / lint / coverage results:** - `uv run pytest` → **3544 passed**, 0 failed (full unit + integration) - `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (>90% gate) - `uv run pytest tests/e2e/test_document_viewer_context.py -v --no-cov` → **8 passed** in 47.33s, in isolation (DB up, mock LLM) - `uv run ruff check . && uv run pyright` → clean (0 errors/warnings) **Completion criteria:** all met — (1) chat→modal→Full page: `href="/"` + "Chat"; Sources→modal→Full page: `href="/sources.html"` + "Sources" with `back=` asserted absent; (2) no-`back` URLs byte-identical to pre-phase; (3) Copy path: "Path copied." on success (clipboard readback), prefilled select-on-focus fallback field + "Path ready — copy it from the field." on rejected clipboard, both surfaces parameterized; (4) copied string exactly `docs/homelab/kubernetes.md`; (5) full suite green, E2E isolated green, lint clean, coverage 99%; (6) commit by harness. **Documentation:** README.md reviewed in full — phase-153 feature already named in the Chat section; routes, env reference, workflows, deployment all consistent — no changes needed. AGENTS.md reviewed in full — rules, gates, and quick-reference commands verified against the repo (incl. `test_smoke.py`, `.env.example`, gitignore entries) — consistent, no changes needed. **Notable:** pytest 9.1.1 quirk — a doubled `-q` (pyproject `addopts` + explicit `-q`) swallows the final "N passed" summary line while still exiting 0; the plain `uv run pytest` gate command prints it. No defects found in prior work. **Next pending phase:** `154_sync_status_freshness`.
7.9 KiB
Phase 153 — the document viewer carries its origin, and the path is copyable
Source: owner UX audit 2026-10-03 (live interactive-browser pass): "document.html's back link always says '← Sources' even when you arrived from Chat" + optimization "a 'copy path' affordance next to the path chip would speed up the copy-paste-to-terminal workflow."
Story: .agents/user_stories/document-viewer.md (phase 10, complete) + .agents/user_stories/document-back-navigation.md (phase 13, complete) — this phase extends both: the phase-13 back contract exists on the DIRECT chip link (app.js::documentUrl, L447–457) but the phase-26 same-page modal's "Full page" escape hatch (document-modal.js::fullPageUrl, L94–97) drops it, so a chat-originated document opened through the modal lands on "← Sources" in the standalone viewer.
Context: openDocumentModal(source, path, trigger = null) (document-modal.js L167) — called from the chat view (app.js L1700, L1762, L1804, L1825, L1908 — source chips, nearby-doc links, source-image figures) and from the RAG folder table (sources.js L2023). The "Full page" anchor (L202) and the no-skeleton window.open fallback (L172) both use fullPageUrl. The standalone back-link resolution (document.js, phase 13) honors a same-origin relative back param (/ prefix, not //) and falls back to /sources.html otherwise; its label matches the resolved target (Chat / Sources) — already shipped, untouched by this phase. The clipboard idiom is established (app.js L216–221, L2375–2384; history.js L297–301): navigator.clipboard.writeText in a try — a non-secure (http) homelab origin rejects the clipboard, so the failure path renders an inline fallback field (an input-like node that selects its content on focus) + a status line.
Objective
A document opened from the in-page modal carries its origin to the standalone viewer (chat → back to chat, Sources → back to Sources — the phase-13 resolution, finally fed on every path), and the owner can copy the document path (source/path) from both the modal's meta and the standalone titlebar with one click, with the established clipboard-fallback contract.
Dependencies
13_document_back_navigation(complete) — thebackparam + same-origin validation + label-match contract this phase feeds.10_story_document_viewer(complete) — the viewer's meta row (source/format chips, mono path) the Copy-path control joins.144_modal_back_button(complete) — the modal's history-entry model; this phase adds no navigation, only a URL param on the escape hatch.
Design (shared by all tasks — the executor reads this, not the chat)
- The origin param (task 01):
openDocumentModal(source, path, trigger = null, back = null)andfullPageUrl(source, path, back = null)—backis appended as&back=+encodeURIComponent(back)ONLY when truthy (a null/absentbackkeeps the URL byte-identical to pre-phase: the Sources path is unchanged). The modal remembers thebackof the CURRENT open (a module-level variable set at eachopenDocumentModal— the modal is one-at-a-time by its own contract, so a single variable is sound). The five chat-view call sites (app.js) pass"/"— chat is the shell's home route, the same valuedocumentUrlalready uses; the sources.js L2023 call site omits it (null → no param → the viewer's/sources.htmldefault). The no-skeleton fallback (L172) uses the sameback. The app.js L447–457 comment ("this URL is also what the modal's 'Full page' link points at") becomes TRUE — update the comment where it cites the contract, do not restate it. - Copy path (task 02): ONE control shape on both surfaces — an icon + "Copy path" text button (visible text,
aria-label"Copy the document path" is redundant with the text so it carries the plain text; iconaria-hidden), placed in the standalone titlebar's meta row after the mono path and in the modal's meta block. The copied string issource + "/" + path(ASSUMPTION A1 — the exactsource/pathform thereadtool, the chips, and the Sources table use; the paste target is the owner's own tooling). Clipboard contract (the share-link-fallback idiom, app.js L216–221):navigator.clipboard.writeTextin a try → success: a transient status "Path copied." (visible near the control + the page's live region; auto-clears ~4 s, the toast idiom) → failure: an inline fallback field (input-like, prefilled with the path, selects its content on focus — the.share-link-fallbackpattern) + the status line "Path ready — copy it from the field." (the owner-locked fallback wording, adapted for the path). Per-page duplication of the small helper is the established idiom (history.js L301) — document.js and document-modal.js each carry their own; no new shared module. - A11y (both tasks): the standalone page already has the landmarks/label frame (phase 10/13); the new button is a labeled control (WCAG 1.3.1/4.1.2); the status line is
role="status"; the fallback field is a labeled input-like node (the share-fallback precedent). No new color-alone state (B5).
Tasks
01_modal_back_context.md— thebackparam throughopenDocumentModal/fullPageUrl+ the six call-site updates + the comment correction.02_copy_path.md— the Copy-path control on the standalone titlebar + the modal meta, clipboard idiom + fallback field + status line.03_unit_pins.md— the URL-contract pins (with/withoutback), the modal param default, the copy-path strings + fallback contract.04_e2e_viewer_context.md— the dedicated Playwright suite: chat→modal→Full page back-to-chat, Sources→modal→Full page back-to-Sources, Copy path success + fallback.
Testing & Quality
- Unit:
tests/unit/source-level pins —fullPageUrlbyte-identical withoutback,&back=%2Fappended withback = "/"(encoded);openDocumentModal's default is null (the Sources path unchanged); the copy-path status strings pinned byte-for-byte (success + fallback); the phase-13 resolution pins (a garbagebackstill falls back to/sources.html— the regression guard). - Integration: the
/api/documents/content+GET /document.htmlcontract is untouched (no new server surface — the phase is frontend-only; the server pins are regression guards only). - Coverage: >90% on modified code (JS pinned by the source-level suites + the E2E).
- The phase is "Complete" only when the entire project test suite passes 100% (every test in the project, not just this phase's).
- E2E:
uv run pytest tests/e2e/test_document_viewer_context.py -v --no-covin isolation (DB up:podman compose up -d db; the deterministictests/e2e/mock_llm.py— a citation-bearing answer renders the chips).
Documentation & Consistency
- The final pass reviews
README.mdandAGENTS.mdin full against the project as it stands after this phase — fixing every inconsistency found, including pre-existing ones not caused by the phase. - Tasks update
README.md/AGENTS.mdimmediately when their own changes are user-visible (the Copy-path affordance is a user-visible feature — name it where the viewer is described).
Completion Criteria
- chat view → source chip → modal → "Full page": the standalone back link is
href="/"with the "Chat" label; Sources table → modal → "Full page":href="/sources.html"with the "Sources" label (E2E) - a direct Sources table chip (no
backparam) and the pre-phase URL are byte-identical — no regression on the phase-13/26 paths - Copy path on both surfaces: clipboard success reads "Path copied."; a rejected clipboard renders the prefilled fallback field + "Path ready — copy it from the field." (E2E pins both branches)
- the copied string is exactly
source/path(theread-tool form) uv run pytestgreen; the dedicated E2E green in isolation;uv run ruff check . && uv run pyrightclean;app/coverage >90%- one atomic commit per AGENTS.md rule 8 (
--no-gpg-sign)