From 958b55acf10592ca89307997e88d7ec5e70e882f Mon Sep 17 00:00:00 2001 From: graphlemon Date: Fri, 7 Aug 2026 11:02:06 +0800 Subject: [PATCH] graphlemon: code_diff artifact for task bfd3ee82-26e5-44d6-8acc-fdbeb3d78185 --- .graphlemon/runs/bfd3ee82-26e5-44d6-8acc-fdbeb3d78185/code_diff.patch | 497 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 497 insertions(+), 0 deletions(-) create mode 100644 .graphlemon/runs/bfd3ee82-26e5-44d6-8acc-fdbeb3d78185/code_diff.patch diff --git a/.graphlemon/runs/bfd3ee82-26e5-44d6-8acc-fdbeb3d78185/code_diff.patch b/.graphlemon/runs/bfd3ee82-26e5-44d6-8acc-fdbeb3d78185/code_diff.patch new file mode 100644 index 0000000..7b8be65 --- /dev/null +++ b/.graphlemon/runs/bfd3ee82-26e5-44d6-8acc-fdbeb3d78185/code_diff.patch @@ -0,0 +1,497 @@ +diff --git a/frontend/app.js b/frontend/app.js +index 1a5b3ec..f026060 100644 +--- a/frontend/app.js ++++ b/frontend/app.js +@@ -115,13 +115,36 @@ function registerRouteCleanup(fn) { + currentRouteCleanup = fn; + } + ++// U-047: `#/tasks?status=done`-style query strings on hash routes -- a new ++// capability, not a bug fix (no route previously carried a query string). ++// Hand-rolled instead of `new URLSearchParams(...)` (which mountTaskBoard's ++// own outgoing-request building already uses) because this one also has to ++// run inside frontend/test_*_smoke.js's bare `vm.createContext` sandbox, ++// which has no WHATWG URL globals -- decodeURIComponent is a plain ++// ECMAScript global and works there unmodified. ++function parseHashQuery(hash) { ++ const qIndex = hash.indexOf("?"); ++ if (qIndex === -1) return { path: hash, params: {} }; ++ const path = hash.slice(0, qIndex); ++ const params = {}; ++ for (const pair of hash.slice(qIndex + 1).split("&")) { ++ if (!pair) continue; ++ const eqIndex = pair.indexOf("="); ++ const key = eqIndex === -1 ? pair : pair.slice(0, eqIndex); ++ const value = eqIndex === -1 ? "" : pair.slice(eqIndex + 1); ++ params[decodeURIComponent(key)] = decodeURIComponent(value); ++ } ++ return { path, params }; ++} ++ + function render() { + if (currentRouteCleanup) { + currentRouteCleanup(); + currentRouteCleanup = null; + } + +- const hash = window.location.hash || "#/"; ++ const rawHash = window.location.hash || "#/"; ++ const { path: hash, params: queryParams } = parseHashQuery(rawHash); + let route = routes[hash]; + let routeArgs = []; + if (!route) { +@@ -154,16 +177,115 @@ function render() { + if (!route) route = { render: () => "

Not found

" + hash + "

" }; + + document.getElementById("app").innerHTML = route.render(...routeArgs); +- if (route.mount) route.mount(...routeArgs); +- +- for (const a of document.querySelectorAll("#nav a")) { +- a.classList.toggle("active", a.getAttribute("href") === hash); ++ // queryParams always appended after any route-specific captures -- every ++ // mount() so far only declares the params it actually reads (e.g. ++ // mountTaskBoard(queryParams)), so this trailing arg is silently ignored ++ // by the rest (plain JS behavior, not a special case per route). ++ if (route.mount) route.mount(...routeArgs, queryParams); ++ ++ // `#nav > a`, not `#nav a`: nav_project_context (U-048) nests its own ++ // links (project name, status-bucket counts) directly inside #nav -- ++ // those must never get the same "active" bolding as the fixed route ++ // links above them just because their target route happens to match. ++ for (const a of document.querySelectorAll("#nav > a")) { ++ a.classList.toggle("active", parseHashQuery(a.getAttribute("href")).path === hash); + } ++ refreshNavProjectContext(); + } + + window.addEventListener("hashchange", render); + window.addEventListener("DOMContentLoaded", render); + ++// ---- nav bar "current project" context (U-048) ---- ++// All pages share #nav (index.html), so this is wired into render() itself ++// (called once per hashchange/initial load, see above) rather than into any ++// one page's mount() -- "piggybacks on the existing hashchange listener, no ++// new event plumbing needed" per the approved SDD. Reuses ++// `GET /api/tasks?project_id=...` and reduces status counts client-side ++// (one fetch per refresh, not one per bucket) -- a dedicated aggregation ++// endpoint is a documented revisit if a single project ever reaches ++// ~1000-2000 tasks. ++ ++// 10 raw task.status values (TASK_STATUSES below, v_task_board) collapsed ++// into the 4 buckets the nav bar shows. `statuses.length === 1` buckets ++// (pending/done) link to "#/tasks?status=" -- v_task_board's status ++// filter is single-select, so multi-status buckets (running/attention) link ++// to the unfiltered "#/tasks" instead of picking one arbitrary status. ++const NAV_STATUS_BUCKETS = [ ++ { key: "pending", label: "pending", statuses: ["queued"] }, ++ { key: "running", label: "running", statuses: [ ++ "planning_brd_prd_sdd", "coding", "validating", "routed_fast_lane", "routed_full_replan", ++ ] }, ++ { key: "attention", label: "attention", statuses: [ ++ "planning_failed", "coding_failed", "blocked_pending_human", ++ ] }, ++ { key: "done", label: "done", statuses: ["done"] }, ++]; ++ ++function navBucketHref(bucket) { ++ return bucket.statuses.length === 1 ? "#/tasks?status=" + bucket.statuses[0] : "#/tasks"; ++} ++ ++function computeNavStatusBucketCounts(tasks) { ++ const statusToBucketKey = {}; ++ for (const bucket of NAV_STATUS_BUCKETS) { ++ for (const status of bucket.statuses) statusToBucketKey[status] = bucket.key; ++ } ++ const counts = {}; ++ for (const bucket of NAV_STATUS_BUCKETS) counts[bucket.key] = 0; ++ for (const task of tasks) { ++ const bucketKey = statusToBucketKey[task.status]; ++ if (bucketKey) counts[bucketKey]++; ++ } ++ return counts; ++} ++ ++function renderNavProjectContextHtml(project, counts) { ++ if (!project) return ""; ++ const countsHtml = NAV_STATUS_BUCKETS.map((bucket) => ++ '' ++ + escapeHtml(bucket.label) + " " + (counts[bucket.key] || 0) + "" ++ ).join(""); ++ return ( ++ '' + escapeHtml(project.name) + "" ++ + '' + countsHtml + "" ++ ); ++} ++ ++// Guards against out-of-order responses: if the current project is switched ++// (or this fires again before an earlier fetch resolves), a stale response ++// landing after a newer one must not overwrite the nav with old data. ++let navProjectContextRequestId = 0; ++ ++async function refreshNavProjectContext() { ++ const contextEl = document.getElementById("nav_project_context"); ++ if (!contextEl) return; ++ const projectId = getCurrentProjectId(); ++ const requestId = ++navProjectContextRequestId; ++ if (!projectId) { ++ // Requirement 1: no project selected -> render nothing, not a hint. ++ contextEl.innerHTML = ""; ++ return; ++ } ++ try { ++ const [projectResp, tasksResp] = await Promise.all([ ++ fetch("/api/projects/" + encodeURIComponent(projectId)), ++ fetch("/api/tasks?project_id=" + encodeURIComponent(projectId)), ++ ]); ++ if (requestId !== navProjectContextRequestId) return; // superseded by a newer refresh ++ if (projectResp.status !== 200) { ++ contextEl.innerHTML = ""; ++ return; ++ } ++ const project = await projectResp.json(); ++ const tasks = tasksResp.status === 200 ? await tasksResp.json() : []; ++ if (requestId !== navProjectContextRequestId) return; ++ contextEl.innerHTML = renderNavProjectContextHtml(project, computeNavStatusBucketCounts(tasks)); ++ } catch (e) { ++ if (requestId === navProjectContextRequestId) contextEl.innerHTML = ""; ++ } ++} ++ + // ---- v_project_list (2026-08-03): project list + CRUD ---- + // 02-V originally declared "不做完整多项目管理 UI(项目列表/切换)" as an + // MVP non-goal -- this page is a deliberate, explicit scope expansion +@@ -885,7 +1007,21 @@ function renderTaskBoard() { + `; + } + +-function mountTaskBoard() { ++// U-047/U-048: nav-bar status-bucket links land on #/tasks as ++// "?status=" -- this picks the status to pre-select, on initial ++// mount only (same reasoning as v_task_board's other filters: it's a ++// starting value the user can then change, not re-applied on every route ++// re-render). An unrecognized/stale status value is ignored rather than ++// left in a select the