From 3e8f0f893ea1511cb9bb2998326b040e7025ccd5 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 20 Aug 2026 23:10:45 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20GUI=20task-run=20subtable=20=E2=80=94?= =?UTF-8?q?=20markdown=20work/reason=20+=20desc=20sort=20+=20click-to-expa?= =?UTF-8?q?nd=20cells=20(rant=202026-08-20T22:59:16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- emrg/gui/renderer/css/components.css | 34 +++++++++++++++++++ emrg/gui/renderer/js/dialogs.js | 51 ++++++++++++++++++++++++---- emrg/gui/test/renderer.smoke.test.js | 45 ++++++++++++++++++------ 3 files changed, 112 insertions(+), 18 deletions(-) diff --git a/emrg/gui/renderer/css/components.css b/emrg/gui/renderer/css/components.css index e39af4e..e274b4b 100644 --- a/emrg/gui/renderer/css/components.css +++ b/emrg/gui/renderer/css/components.css @@ -1081,6 +1081,40 @@ dialog::backdrop { white-space: nowrap; color: var(--text-3); } +/* rant 2026-08-20T22:59:16 #3:work/reason cell 多行截断(限制行数 + 省略号), + 点击 cell 在本条记录下方展开完整内容(Markdown 渲染),再点收起 */ +.task-run-cell { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + white-space: normal; + cursor: pointer; + line-height: 1.35; +} +.task-run-cell:hover { + color: var(--accent, var(--text-2)); +} +.task-run-expand { + flex-basis: 100%; + grid-column: 1 / -1; + font-size: var(--fs-aux); + color: var(--text-2); + background: var(--bg-soft, transparent); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 8px; + margin: 2px 0; + overflow-wrap: anywhere; +} +.task-run-expand.hidden { + display: none; +} +.task-run-expand :first-child { + margin-top: 0; +} +.task-run-expand :last-child { + margin-bottom: 0; +} .task-run-flag { display: flex; gap: 4px; diff --git a/emrg/gui/renderer/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js index 4fa1d42..a02c1f0 100644 --- a/emrg/gui/renderer/js/dialogs.js +++ b/emrg/gui/renderer/js/dialogs.js @@ -572,13 +572,21 @@ const Dialogs = (() => { // (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs // (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再 // fallback impact 机器串——空则显示 "-";无记录 → 占位文案。 - function buildTaskRunDetail(t) { + // Rant 2026-08-20T22:59:16(宿主)三处改进: + // 1) work/slowdown_reason 按 Markdown 渲染(emrgMarkdown.renderMarkdown, + // 不再纯文本 el("span", text)); + // 2) 子表按时间倒序渲染(最新在最上面;后端 recent_runs 语义——最近 5 条——不变); + // 3) work/reason cell 多行截断(CSS line-clamp)+ 点击 cell 在本条记录下方 + // 展开完整内容(Markdown 渲染),再点收起。 + async function buildTaskRunDetail(t) { const wrap = el("div", { class: "task-run-detail hidden" }); - const runs = Array.isArray(t.recent_runs) ? t.recent_runs : []; + const runs = Array.isArray(t.recent_runs) ? [...t.recent_runs] : []; if (runs.length === 0) { wrap.appendChild(el("div", { class: "task-run-empty" }, _t("app.taskRunsEmpty"))); return wrap; } + // rant 2026-08-20T22:59:16 #2:GUI 渲染前按 timestamp 倒序(最新在最上面) + runs.sort((a, b) => String(b.timestamp || "").localeCompare(String(a.timestamp || ""))); const head = el("div", { class: "task-run-head" }); head.appendChild(el("span", {}, _t("app.taskRunsColTime"))); head.appendChild(el("span", {}, _t("app.taskRunsColDone"))); @@ -589,8 +597,22 @@ const Dialogs = (() => { const row = el("div", { class: "task-run-row" }); row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp))); // rant 2026-08-20T10:58:55:字段统一 work(原 summary/done) - let done = (typeof r.work === "string" && r.work) ? r.work : ""; - row.appendChild(el("span", { class: "task-run-done" }, done || "-")); + // rant 2026-08-20T22:59:16 #1/#3:Markdown 渲染 + 点击展开完整内容 + const done = (typeof r.work === "string" && r.work) ? r.work : ""; + const doneCell = el("span", { class: "task-run-done task-run-cell" }); + const doneExpand = el("div", { class: "task-run-expand hidden" }); + if (done) { + const md = await window.emrgMarkdown.renderMarkdown(done); + doneCell.innerHTML = md; + doneExpand.innerHTML = md; + doneCell.addEventListener("click", (e) => { + if (e && typeof e.stopPropagation === "function") e.stopPropagation(); + doneExpand.classList.toggle("hidden"); + }); + } else { + doneCell.textContent = "-"; + } + row.appendChild(doneCell); const flagCell = el("span", { class: "task-run-flag" }); if (r.recommend_slowdown) { flagCell.appendChild(el("span", { class: "task-badge task-run-badge-warn" }, _t("app.taskRunThrottle"))); @@ -598,9 +620,24 @@ const Dialogs = (() => { row.appendChild(flagCell); // rant 2026-08-19T18:25:14:原因列 —— 降频判断的自然语言理由(vibe check // slowdown_reason,rant 2026-08-20T10:58:55 改名)。 - let reason = (typeof r.slowdown_reason === "string" && r.slowdown_reason) ? r.slowdown_reason : ""; - row.appendChild(el("span", { class: "task-run-reason" }, reason || "-")); + const reason = (typeof r.slowdown_reason === "string" && r.slowdown_reason) ? r.slowdown_reason : ""; + const reasonCell = el("span", { class: "task-run-reason task-run-cell" }); + const reasonExpand = el("div", { class: "task-run-expand hidden" }); + if (reason) { + const md = await window.emrgMarkdown.renderMarkdown(reason); + reasonCell.innerHTML = md; + reasonExpand.innerHTML = md; + reasonCell.addEventListener("click", (e) => { + if (e && typeof e.stopPropagation === "function") e.stopPropagation(); + reasonExpand.classList.toggle("hidden"); + }); + } else { + reasonCell.textContent = "-"; + } + row.appendChild(reasonCell); wrap.appendChild(row); + wrap.appendChild(doneExpand); + wrap.appendChild(reasonExpand); } return wrap; } @@ -714,7 +751,7 @@ const Dialogs = (() => { // rant 2026-08-18T21:32:32:点击任务卡(非按钮)→ 手风琴展开最近运行子表。 // 真实 DOM 中按钮点击通过 e.target.closest("button") 拦截(不触发展开); // 测试沙箱的 click() 无 target → 直接切换(沙箱中按钮点击不冒泡,互不影响)。 - const runDetail = buildTaskRunDetail(t); + const runDetail = await buildTaskRunDetail(t); row.appendChild(runDetail); let runDetailOpen = false; row.addEventListener("click", (e) => { diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index 1b461f4..6757eec 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -2571,16 +2571,18 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什 name: "emrg-task", type: "evolution", config: { project: "emrg" }, interval: 60, enabled: true, last_run_at: "2026-08-18T10:00:00", saturation: { heartbeat_interval: 480, heartbeat_active: true }, + // rant 2026-08-20T22:59:16 #2:后端 recent_runs 按时间升序(旧在上); + // GUI 渲染前必须按 timestamp 倒序(最新在最上面) recent_runs: [ - { timestamp: "2026-08-18T10:00:00", work: "修了双实例根因,提交 PR #854", - impact: ["cycle-ts-complete", "tools-executed=26"], - recommend_slowdown: false, tool_count: 26, slowdown_reason: "" }, - { timestamp: "2026-08-18T09:00:00", work: "", - impact: ["cycle-ts-complete", "tools-executed=3"], - recommend_slowdown: false, tool_count: 3, slowdown_reason: "" }, { timestamp: "2026-08-18T08:00:00", work: "NTE", impact: ["cycle-ts-complete"], recommend_slowdown: true, tool_count: 0, slowdown_reason: "长期无产出" }, + { timestamp: "2026-08-18T09:00:00", work: "", + impact: ["cycle-ts-complete", "tools-executed=3"], + recommend_slowdown: false, tool_count: 3, slowdown_reason: "" }, + { timestamp: "2026-08-18T10:00:00", work: "修了双实例根因,提交 PR #854", + impact: ["cycle-ts-complete", "tools-executed=26"], + recommend_slowdown: false, tool_count: 26, slowdown_reason: "" }, ], }, { name: "fresh-task", type: "sync", config: { project: "docs" }, interval: 3600, enabled: true }, @@ -2591,6 +2593,7 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什 await tick(); await vm.runInContext("App.openTasksPanel()", ctx); await tick(); + await tick(); // buildTaskRunDetail 异步(renderMarkdown)→ 多等一拍 // 初始:子表隐藏(行内含 .task-run-detail.hidden) const hiddenBefore = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-detail")).every((d) => d.classList.contains("hidden"))`, ctx); assert.strictEqual(hiddenBefore, true, "子表初始应隐藏"); @@ -2599,9 +2602,11 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什 await tick(); const detail = vm.runInContext(`document.getElementById("task-list").children[0].querySelector(".task-run-detail")`, ctx); assert.strictEqual(detail.classList.contains("hidden"), false, "点击后子表应展开"); - // 第一行 run:Agent 总结展示(自然语言 work,rant 2026-08-20T10:58:55 字段统一) - const doneTxt = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")[0].textContent`, ctx); - assert.strictEqual(doneTxt, "修了双实例根因,提交 PR #854", `子表应显示 Agent 总结,实际: ${doneTxt}`); + // rant 2026-08-20T22:59:16 #2:按 timestamp 倒序渲染 —— 最新(10:00)在最上面 + const doneHtmls = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.innerHTML)`, ctx); + assert.ok(String(doneHtmls[0]).includes("修了双实例根因,提交 PR #854"), + `最新记录(10:00)应渲染在第一行(倒序),实际: ${doneHtmls[0]}`); + assert.ok(String(doneHtmls[2]).includes("NTE"), "第三行应为 08:00(NTE)"); // 降频徽章:recommend_slowdown=true → 建议降频(idle 空转徽章已删除,rant 10:58:55) const warnCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-warn").length`, ctx); assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章"); @@ -2617,11 +2622,25 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什 const reasonTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-reason")).map((n) => n.textContent)`, ctx); assert.strictEqual(reasonTexts[0], "-", `无 slowdown_reason 应显示 "-",实际: ${reasonTexts[0]}`); assert.strictEqual(reasonTexts[1], "-", `无 slowdown_reason 应显示 "-",实际: ${reasonTexts[1]}`); - assert.strictEqual(reasonTexts[2], "长期无产出", `原因列应显示降频原因,实际: ${reasonTexts[2]}`); + const reasonHtmls = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-reason")).map((n) => n.innerHTML)`, ctx); + assert.ok(String(reasonHtmls[2]).includes("长期无产出"), `原因列应显示降频原因,实际: ${reasonHtmls[2]}`); + // rant 2026-08-20T22:59:16 #3:点击 work cell → 本条记录下方展开完整内容(Markdown),再点收起 + const expandCountBefore = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-expand:not(.hidden)").length`, ctx); + assert.strictEqual(expandCountBefore, 0, "初始所有展开块应隐藏"); + await vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")[0].click()`, ctx); + await tick(); + const expandVisible = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-expand")[0].classList.contains("hidden")`, ctx); + assert.strictEqual(expandVisible, false, "点击 work cell 后应展开完整内容"); + const expandHtml = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-expand")[0].innerHTML`, ctx); + assert.ok(String(expandHtml).includes("修了双实例根因,提交 PR #854"), "展开块应含完整 work 内容(Markdown 渲染)"); + await vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")[0].click()`, ctx); + await tick(); + const expandCollapsed = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-expand")[0].classList.contains("hidden")`, ctx); + assert.strictEqual(expandCollapsed, true, "再次点击应收起"); // rant 2026-08-19T18:25:14:一级列表不再显示"干了什么"(last_cycle_summary) const primarySummary = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-meta-summary").length`, ctx); assert.strictEqual(primarySummary, 0, `一级列表不应显示 last_cycle_summary,实际数量: ${primarySummary}`); - // 再次点击 → 折叠 + // 再次点击任务卡 → 折叠 await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx); await tick(); assert.strictEqual(vm.runInContext(`document.getElementById("task-list").children[0].querySelector(".task-run-detail").classList.contains("hidden")`, ctx), true, "再次点击应折叠"); @@ -2635,6 +2654,10 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什 assert.ok(rowRule.includes("flex-wrap: wrap"), "task-row 应含 flex-wrap: wrap(子表换行到任务卡下方)"); const detailRule = taskCss.slice(taskCss.indexOf(".task-run-detail"), taskCss.indexOf(".task-run-detail.hidden")); assert.ok(detailRule.includes("flex-basis: 100%"), "task-run-detail 应保持 flex-basis:100%(配合 flex-wrap 换行)"); + // rant 2026-08-20T22:59:16 #3:work/reason cell 多行截断(line-clamp + 省略号) + const cellRule = taskCss.slice(taskCss.indexOf(".task-run-cell"), taskCss.indexOf(".task-run-expand")); + assert.ok(cellRule.includes("-webkit-line-clamp: 2"), "task-run-cell 应含 2 行截断(line-clamp)"); + assert.ok(cellRule.includes("cursor: pointer"), "task-run-cell 应可点击(cursor: pointer)"); // rant 2026-08-20T10:34:40:主表一行 —— task-meta 不得再 flex-basis:100% // (曾强制"上次运行"独占一行 → 任务卡变 3 行),应改为弹性 auto 与 actions 同行 const metaRule = taskCss.slice(taskCss.indexOf(".task-meta {"), taskCss.indexOf(".task-meta-item"));