diff --git a/.graphlemon/runs/773a7a08-b295-4ba3-b506-196a21403d8b/code_diff.patch b/.graphlemon/runs/773a7a08-b295-4ba3-b506-196a21403d8b/code_diff.patch
new file mode 100644
index 0000000..434b343
--- /dev/null
+++ b/.graphlemon/runs/773a7a08-b295-4ba3-b506-196a21403d8b/code_diff.patch
@@ -0,0 +1,162 @@
+diff --git a/frontend/app.js b/frontend/app.js
+index 96ce34b..5a7da1d 100644
+--- a/frontend/app.js
++++ b/frontend/app.js
+@@ -90,11 +90,13 @@ const routes = {
+ "#/bugs/new": { render: renderBugFeedback, mount: mountBugFeedback },
+ };
+
+-// #/tasks/:task_id (v_task_detail) and #/skills/:skill_id/audit
+-// (v_skill_audit_log) are the only dynamic routes -- not worth a full
+-// path-matching router for two patterns.
++// #/tasks/:task_id (v_task_detail), #/skills/:skill_id/audit
++// (v_skill_audit_log), and #/artifacts/:artifact_id (v_artifact_browser,
++// pre-selecting one artifact) are the only dynamic routes -- not worth a
++// full path-matching router for three patterns.
+ const TASK_DETAIL_RE = /^#\/tasks\/([^/]+)$/;
+ const SKILL_AUDIT_RE = /^#\/skills\/([^/]+)\/audit$/;
++const ARTIFACT_DETAIL_RE = /^#\/artifacts\/([^/]+)$/;
+
+ let currentRouteCleanup = null;
+ function registerRouteCleanup(fn) {
+@@ -113,12 +115,18 @@ function render() {
+ if (!route) {
+ const taskMatch = hash.match(TASK_DETAIL_RE);
+ const skillMatch = hash.match(SKILL_AUDIT_RE);
++ const artifactMatch = hash.match(ARTIFACT_DETAIL_RE);
+ if (taskMatch) {
+ route = { render: renderTaskDetail, mount: mountTaskDetail };
+ routeArgs = [taskMatch[1]];
+ } else if (skillMatch) {
+ route = { render: renderSkillAuditLog, mount: mountSkillAuditLog };
+ routeArgs = [skillMatch[1]];
++ } else if (artifactMatch) {
++ // Reuses v_artifact_browser itself (same render/mount as "#/artifacts")
++ // with the artifact_id pre-selected -- no separate detail view.
++ route = { render: renderArtifactBrowser, mount: mountArtifactBrowser };
++ routeArgs = [artifactMatch[1]];
+ }
+ }
+ if (!route) route = { render: () => "
Not found
" + hash + "
" };
+@@ -1344,8 +1352,12 @@ const GC_CODE_ARTIFACT_TYPES = ["code_diff", "test_report"];
+ function renderArtifactSummaryList(artifacts, types, emptyText) {
+ const filtered = artifacts.filter((a) => types.includes(a.artifact_type)).slice(0, 5);
+ if (filtered.length === 0) return '' + emptyText + "
";
++ // Each item links to v_artifact_browser (#/artifacts/:artifact_id) --
++ // click handlers are wired in mountArtifactsOverviewPanel after this HTML
++ // is inserted into the DOM. This deliberately reuses the existing
++ // browser/preview view rather than building a second content display.
+ return '' + filtered.map((a) =>
+- "- " + escapeHtml(a.artifact_type) + " · " + escapeHtml(a.file_path.split("/").pop()) + ""
++ '
- ' + escapeHtml(a.artifact_type) + " · " + escapeHtml(a.file_path.split("/").pop()) + ""
+ + escapeHtml(a.validation_status) + "
"
+ ).join("") + "
";
+ }
+@@ -1397,6 +1409,15 @@ async function mountArtifactsOverviewPanel(container, projectId, { onSwitchToTas
+ li.addEventListener("click", () => onSwitchToTask(li.dataset.runTaskId));
+ }
+ }
++
++ // Doc/code list items: jump to the existing v_artifact_browser detail
++ // view (#/artifacts/:artifact_id) pre-selecting this artifact. "查看全部"
++ // links above stay pointed at unfiltered #/artifacts per the FR-5 note.
++ for (const li of container.querySelectorAll("li[data-artifact-id]")) {
++ li.addEventListener("click", () => {
++ window.location.hash = "#/artifacts/" + encodeURIComponent(li.dataset.artifactId);
++ });
++ }
+ }
+
+ function renderGraphConsole() {
+@@ -1579,14 +1600,18 @@ function renderArtifactBrowser() {
+ `;
+ }
+
+-function mountArtifactBrowser() {
++function mountArtifactBrowser(initialArtifactId) {
+ const projectId = getCurrentProjectId();
+ if (!projectId) return;
+ const listEl = document.getElementById("ab_list");
+ const previewEl = document.getElementById("ab_preview");
+ const typeFilterEl = document.getElementById("ab_type_filter");
+ const taskFilterEl = document.getElementById("ab_task_filter");
+- let selectedArtifactId = null;
++ // Seeded from the #/artifacts/:artifact_id route (v_graph_console's
++ // Planner 产出总览 list links here). refreshList()'s existing fallback
++ // below already handles an id that turns out invalid/filtered-out --
++ // falls back to the first artifact in the (possibly filtered) list.
++ let selectedArtifactId = initialArtifactId || null;
+
+ fetch("/api/tasks?project_id=" + encodeURIComponent(projectId))
+ .then((resp) => resp.json())
+diff --git a/frontend/test_graph_console_smoke.js b/frontend/test_graph_console_smoke.js
+index 55c29eb..6d79da1 100644
+--- a/frontend/test_graph_console_smoke.js
++++ b/frontend/test_graph_console_smoke.js
+@@ -12,7 +12,12 @@
+ // calls this via `.map(renderGraphVisualizerPanel)`, which passes the
+ // array index as a second argument; that must NOT be misread as an
+ // options object with enableNodeClick=true)
+-// - renderArtifactSummaryList (FR-5: type grouping/truncation/empty state)
++// - renderArtifactSummaryList (FR-5: type grouping/truncation/empty state,
++// plus 2026-08-04's click-through-to-v_artifact_browser fix: each item
++// now carries data-artifact-id)
++// - ARTIFACT_DETAIL_RE (2026-08-04: #/artifacts/:artifact_id route that
++// v_artifact_browser's own mountArtifactBrowser(initialArtifactId) uses
++// to pre-select the clicked artifact -- see render()'s route dispatch)
+ //
+ // Run: node frontend/test_graph_console_smoke.js
+
+@@ -50,7 +55,14 @@ const sandbox = {
+ vm.createContext(sandbox);
+
+ const src = fs.readFileSync(path.join(__dirname, "app.js"), "utf8");
+-new vm.Script(src, { filename: "app.js" }).runInContext(sandbox);
++// `function ...` declarations at top level become sandbox globals
++// automatically (Annex B global-object semantics), but top-level `const`
++// (e.g. ARTIFACT_DETAIL_RE) stays lexically scoped to the script and never
++// reaches the sandbox object -- explicitly re-export the one const this
++// test needs, in the same script/lexical scope, rather than changing app.js's
++// declaration style just for testability.
++const augmentedSrc = src + "\nglobalThis.ARTIFACT_DETAIL_RE = ARTIFACT_DETAIL_RE;\n";
++new vm.Script(augmentedSrc, { filename: "app.js" }).runInContext(sandbox);
+
+ // ---- computeGraphLayers: still the shared layering logic v_graph_console
+ // leans on for FR-2/3, unchanged by this SDD but worth pinning down since
+@@ -114,9 +126,9 @@ new vm.Script(src, { filename: "app.js" }).runInContext(sandbox);
+ // ---- renderArtifactSummaryList (FR-5) ----
+ {
+ const artifacts = [
+- { artifact_type: "brd", file_path: "docs/brd.md", validation_status: "passed" },
+- { artifact_type: "code_diff", file_path: "src/x.py", validation_status: "pending" },
+- { artifact_type: "sdd", file_path: "docs/sdd.md", validation_status: "passed" },
++ { artifact_id: "art-1", artifact_type: "brd", file_path: "docs/brd.md", validation_status: "passed" },
++ { artifact_id: "art-2", artifact_type: "code_diff", file_path: "src/x.py", validation_status: "pending" },
++ { artifact_id: "art-3", artifact_type: "sdd", file_path: "docs/sdd.md", validation_status: "passed" },
+ ];
+ const docsHtml = sandbox.renderArtifactSummaryList(artifacts, ["brd", "prd", "sdd", "benchmark"], "空");
+ assert.ok(docsHtml.includes("brd") && docsHtml.includes("sdd"), "docs list groups brd+sdd together");
+@@ -124,6 +136,23 @@ new vm.Script(src, { filename: "app.js" }).runInContext(sandbox);
+
+ const emptyHtml = sandbox.renderArtifactSummaryList([], ["brd"], "暂无文档类 Artifact");
+ assert.ok(emptyHtml.includes("暂无文档类 Artifact"), "empty group shows the given empty-state text");
++
++ // 2026-08-04 fix: each item must carry a real artifact_id so
++ // mountArtifactsOverviewPanel can wire a click -> #/artifacts/:id
++ // handler (see render()'s ARTIFACT_DETAIL_RE dispatch) instead of the
++ // old bare, unclickable .
++ assert.ok(docsHtml.includes('data-artifact-id="art-1"'), "brd item carries its real artifact_id");
++ assert.ok(docsHtml.includes('data-artifact-id="art-3"'), "sdd item carries its real artifact_id");
++}
++
++// ---- ARTIFACT_DETAIL_RE (2026-08-04 click-through fix) ----
++{
++ const match = "#/artifacts/abc-123".match(sandbox.ARTIFACT_DETAIL_RE);
++ assert.ok(match, "#/artifacts/:id must match ARTIFACT_DETAIL_RE");
++ assert.strictEqual(match[1], "abc-123", "captured group must be the artifact_id");
++
++ assert.ok(!"#/artifacts".match(sandbox.ARTIFACT_DETAIL_RE), "bare #/artifacts (unfiltered browser) must not match -- stays a static route");
++ assert.ok(!"#/artifacts/abc/extra".match(sandbox.ARTIFACT_DETAIL_RE), "extra path segments must not match");
+ }
+
+ console.log("frontend/test_graph_console_smoke.js: all assertions passed");