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