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 '"; + } +@@ -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");