Files
brain-of-reese/.agents/phases/complete/153_document_viewer_context/00_phase.md
T
ducoterra 966c4b0540
Build and Push Containers / build-and-push-app (push) Successful in 2m45s
Build and Push Containers / build-and-push-db (push) Successful in 19s
phase: 153_document_viewer_context
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`.
2026-10-04 09:23:54 -04:00

7.9 KiB
Raw Blame History

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) — the back param + 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) and fullPageUrl(source, path, back = null) — back is appended as &back= + encodeURIComponent(back) ONLY when truthy (a null/absent back keeps the URL byte-identical to pre-phase: the Sources path is unchanged). The modal remembers the back of the CURRENT open (a module-level variable set at each openDocumentModal — 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 value documentUrl already uses; the sources.js L2023 call site omits it (null → no param → the viewer's /sources.html default). The no-skeleton fallback (L172) uses the same back. 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; icon aria-hidden), placed in the standalone titlebar's meta row after the mono path and in the modal's meta block. The copied string is source + "/" + path (ASSUMPTION A1 — the exact source/path form the read tool, 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.writeText in 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-fallback pattern) + 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

  1. 01_modal_back_context.md — the back param through openDocumentModal/fullPageUrl + the six call-site updates + the comment correction.
  2. 02_copy_path.md — the Copy-path control on the standalone titlebar + the modal meta, clipboard idiom + fallback field + status line.
  3. 03_unit_pins.md — the URL-contract pins (with/without back), the modal param default, the copy-path strings + fallback contract.
  4. 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 — fullPageUrl byte-identical without back, &back=%2F appended with back = "/" (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 garbage back still falls back to /sources.html — the regression guard).
  • Integration: the /api/documents/content + GET /document.html contract 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-cov in isolation (DB up: podman compose up -d db; the deterministic tests/e2e/mock_llm.py — a citation-bearing answer renders the chips).

Documentation & Consistency

  • The final pass reviews README.md and AGENTS.md in 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.md immediately 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 back param) 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 (the read-tool form)
  • uv run pytest green; the dedicated E2E green in isolation; uv run ruff check . && uv run pyright clean; app/ coverage >90%
  • one atomic commit per AGENTS.md rule 8 (--no-gpg-sign)