code_diff.patch
8.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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: () => "<h1>Not found</h1><p>" + hash + "</p>" };
@@ -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 '<div class="hint">' + emptyText + "</div>";
+ // 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 '<ul class="simple-list">' + filtered.map((a) =>
- "<li><span>" + escapeHtml(a.artifact_type) + " · " + escapeHtml(a.file_path.split("/").pop()) + "</span><span>"
+ '<li data-artifact-id="' + a.artifact_id + '" style="cursor:pointer;"><span>' + escapeHtml(a.artifact_type) + " · " + escapeHtml(a.file_path.split("/").pop()) + "</span><span>"
+ escapeHtml(a.validation_status) + "</span></li>"
).join("") + "</ul>";
}
@@ -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 <li>.
+ 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");