From 3ecc1886f74cb34e1ebba5552068c1078ebd2c85 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Thu, 2 Jul 2026 07:01:06 +0200 Subject: [PATCH] feat(serve): artifact view source link opens the /source viewer at the artifact line (REQ-243, #623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The artifact detail view showed the source file but the link was a dead `href="#"` that did nothing in a plain browser — it only worked via the VSIX editor shim. It now deep-links into the built-in `/source` viewer at the artifact's definition line (hx-get + hx-push-url, scrolling to the line), while keeping the `data-source-file`/`data-source-line` attributes so the VSIX webview shim still opens the file in the editor. The VSIX webview loads no htmx, so the two runtimes don't collide: browser follows hx-get, VSIX follows the shim. The header file link uses a distinct `source-file-link` class (not the `source-ref-link` used by inline cited-source refs) so selectors that pick the first inline source ref on the detail page keep working — the Playwright aadl.spec.ts test asserts the first `a.source-ref-link` is an `.aadl` ref. Off-by-one: the `/source` viewer numbers rows 1-based, so the browser scroll target is `source_line + 1`; `data-source-line` stays 0-based for the VSIX host, which consumes it as a 0-based position. Confirmed with a live server (deep-link present, /source returns 200 for REQ-001, scroll lands on the artifact line), the full serve_integration suite (47/47), cargo clippy --all-targets -- -D warnings, and cargo fmt --check — all green. Implements: REQ-243 Verifies: REQ-243 Refs: FEAT-001 --- artifacts/requirements.yaml | 2 +- rivet-cli/src/render/artifacts.rs | 37 ++++++++++++++++++------ rivet-cli/tests/serve_integration.rs | 43 ++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index 2fc3d27..063853a 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -7623,7 +7623,7 @@ artifacts: - id: REQ-243 type: requirement title: rivet serve artifact view opens the source file at the artifact location - status: proposed + status: verified description: "In the `rivet serve` artifact view the source file is shown but clicking it does not open the file at the artifact's location, even though sources are configured — this deep-link only exists in the VSIX extension today. Wire the same source open-at-location behaviour into the dashboard. #623." provenance: created-by: ai-assisted diff --git a/rivet-cli/src/render/artifacts.rs b/rivet-cli/src/render/artifacts.rs index 334dece..4616a8f 100644 --- a/rivet-cli/src/render/artifacts.rs +++ b/rivet-cli/src/render/artifacts.rs @@ -533,24 +533,45 @@ pub(crate) fn render_artifact_detail(ctx: &RenderContext, id: &str) -> RenderRes }) }); - // Source file link (shown at top for quick access) - // Uses data-source-file + data-source-line attributes — the VS Code - // nav shim in shell.ts picks these up and opens the file at the - // exact line of the artifact definition. + // Source file link (shown at top for quick access). + // + // REQ-243 (#623): the link now navigates the dashboard to the built-in + // `/source` viewer at the artifact's definition line — previously it was a + // dead `href="#"` that did nothing in a plain browser and only worked via + // the VSIX editor shim. Two runtimes, one anchor: + // * Browser dashboard (htmx loaded): `hx-get` swaps in the `/source` + // view and the `onclick` scrolls to the artifact's line. + // * VSIX webview (no htmx): the `shell.ts` shim intercepts the + // `data-source-*` attributes and opens the file in the editor. + // Off-by-one: `/source` row anchors are 1-based, so the browser fragment is + // `source_line + 1`; `data-source-line` stays 0-based for the VSIX host. let source_link = if let Some(ref sf) = source_file { let filename = std::path::Path::new(sf) .file_name() .and_then(|n| n.to_str()) .unwrap_or(sf); + let encoded_path = urlencoding::encode(sf); let line_attr = source_line .map(|l| format!(" data-source-line=\"{l}\"")) .unwrap_or_default(); + let onclick = source_line + .map(|l| { + format!( + " onclick=\"setTimeout(function(){{var e=document.getElementById('L{}');if(e)e.scrollIntoView({{behavior:'smooth',block:'center'}})}},200)\"", + l + 1 + ) + }) + .unwrap_or_default(); + // Distinct class (`source-file-link`, not `source-ref-link`): the + // header file link and the inline cited-source refs must not share a + // selector — a Playwright test picks the first `a.source-ref-link` on + // the detail page expecting an inline `.aadl` ref (aadl.spec.ts). format!( " \ - 📄 {}", - html_escape(sf), - line_attr, - html_escape(filename), + 📄 {fn_esc}", + enc = encoded_path, + sf_esc = html_escape(sf), + fn_esc = html_escape(filename), ) } else { String::new() diff --git a/rivet-cli/tests/serve_integration.rs b/rivet-cli/tests/serve_integration.rs index fd2ddde..e552775 100644 --- a/rivet-cli/tests/serve_integration.rs +++ b/rivet-cli/tests/serve_integration.rs @@ -807,6 +807,49 @@ fn artifact_detail_has_oembed_discovery_link() { child.wait().ok(); } +/// REQ-243 (#623): the artifact detail view's source link must navigate the +/// dashboard to the built-in `/source` viewer at the artifact's definition +/// line (previously a dead `href="#"` that only worked in the VSIX editor +/// shim). It must also keep the `data-source-*` attributes so the VSIX shim +/// still opens the file in the editor. +/// +/// rivet: verifies REQ-243 +#[test] +fn artifact_detail_source_link_opens_source_view() { + let (mut child, port) = start_server(); + + let (status, body, _headers) = fetch(port, "/artifacts/REQ-001", false); + assert_eq!(status, 200); + + // Browser path: deep-links into the /source viewer via htmx (no dead #). + assert!( + body.contains("hx-get=\"/source/") && body.contains("href=\"/source/"), + "artifact detail source link must navigate to the /source viewer, \ + not a dead href=\"#\"" + ); + // The header file link uses a DISTINCT class from inline cited-source refs + // (`source-ref-link`), so selectors targeting inline refs don't catch it — + // see aadl.spec.ts, which picks the first `a.source-ref-link`. + assert!( + body.contains("class=\"source-file-link\""), + "artifact detail source link must use the distinct source-file-link class" + ); + // VSIX path: the editor shim still gets the file + line attributes. + assert!( + body.contains("data-source-file="), + "artifact detail source link must keep data-source-file for the VSIX shim" + ); + // REQ-001 lives in a source file, so its definition line resolves and the + // browser scroll target / VSIX line attribute must be present. + assert!( + body.contains("data-source-line=") && body.contains("scrollIntoView"), + "artifact detail source link must target the artifact's definition line" + ); + + child.kill().ok(); + child.wait().ok(); +} + // ── Embed resolution in documents ────────────────────────────────────── /// The documents page should not contain any embed-error spans for valid