From 8153c00ba7209d999e18629b51dc36ca492de661 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Fri, 14 Aug 2026 03:11:16 +0800 Subject: [PATCH] emrg: GUI workspace panel titles + Rant 5-column list with markdown detail + i18n.apply control-preserving fix (rants 21:36:01/21:38:25/21:46:53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rant panel: workspace-view-title header + 5-column head (time/project/status/progress/content) with matching row layout; status badges get tri-state colors (done/warn/muted); detail expands with meta row + markdown-rendered message body (window.emrgMarkdown, DOMPurify same-source) + progress - Projects panel: workspace-view-title header; remove top hint row (projects.hint key deleted, actions self-explanatory + delete confirm keeps disk-data note) - Tasks panel: workspace-view-title header (tasks.title re-added after #776 removed it) - i18n.apply(): replace only the first text node instead of textContent assignment that wiped child controls (task/template/rant forms became unclickable); rants.detail + projects.hint orphans removed - renderer.smoke: P4 rant test rewritten (5-col + tri-state + real-marked detail assertion) + new source-level panel-title/i18n.apply regression test; GUI 243→244 --- Agent.md | 4 +- emrg/gui/renderer/css/components.css | 83 ++++++++++++++++++++++++++++ emrg/gui/renderer/index.html | 11 +++- emrg/gui/renderer/js/dialogs.js | 41 +++++++++----- emrg/gui/renderer/js/i18n.js | 29 ++++++++-- emrg/gui/test/renderer.smoke.test.js | 74 ++++++++++++++++++++++--- 6 files changed, 213 insertions(+), 29 deletions(-) diff --git a/Agent.md b/Agent.md index 0e345d7..a8caef3 100644 --- a/Agent.md +++ b/Agent.md @@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request - Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand) - Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端" - Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout) - - Unit tests `npm test` (243: 44 daemon_client + 19 conn-manager + 22 app-commands + 118 renderer smoke + 16 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py` + - Unit tests `npm test` (244: 44 daemon_client + 19 conn-manager + 22 app-commands + 119 renderer smoke + 16 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py` - **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711) - Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/.md` → fallback - Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`) @@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg ``` Python: `uv run pytest tests/ -v` (801) — import check: `uv run python -c "from emrg.client.app import run_client"` -GUI: `cd emrg/gui && npm test` (243: 44 daemon_client + 19 conn-manager + 22 app-commands + 118 renderer smoke + 16 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` +GUI: `cd emrg/gui && npm test` (244: 44 daemon_client + 19 conn-manager + 22 app-commands + 119 renderer smoke + 16 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响) diff --git a/emrg/gui/renderer/css/components.css b/emrg/gui/renderer/css/components.css index 0ff469f..1c55210 100644 --- a/emrg/gui/renderer/css/components.css +++ b/emrg/gui/renderer/css/components.css @@ -844,6 +844,89 @@ dialog::backdrop { padding: 0 4px; flex-shrink: 0; } +/* rant 21:36:01:Rant 面板 5 列 + 状态徽标三态配色(已完成绿 / 进行中琥珀 / 待处理灰) */ +.rant-head { + display: flex; + gap: 8px; + padding: 2px 8px 6px; + font-size: var(--fs-aux); + font-weight: 600; + color: var(--text-3); +} +.rant-head .rant-col-time, +.rant-row .rant-col-time { + flex: 0 0 150px; +} +.rant-head .rant-col-project, +.rant-row .rant-col-project { + flex: 0 0 90px; +} +.rant-head .rant-col-status, +.rant-row .rant-col-status { + flex: 0 0 76px; +} +.rant-head .rant-col-progress, +.rant-row .rant-col-progress { + flex: 1.2; + min-width: 0; +} +.rant-head .rant-col-content, +.rant-row .rant-col-content { + flex: 2; + min-width: 0; +} +.rant-head span, +.rant-row .rant-col-time, +.rant-row .rant-col-project, +.rant-row .rant-col-progress, +.rant-row .rant-col-content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.rant-row .rant-col-time, +.rant-row .rant-col-project, +.rant-row .rant-col-progress, +.rant-row .rant-col-content { + font-size: var(--fs-aux); + color: var(--text-2); +} +.task-badge.badge-done { + color: #16a34a; + border-color: #16a34a; + background: color-mix(in srgb, #16a34a 10%, transparent); +} +.task-badge.badge-warn { + color: #d97706; + border-color: #d97706; + background: color-mix(in srgb, #d97706 10%, transparent); +} +.task-badge.badge-muted { + color: var(--text-3); + border-color: var(--border); + background: transparent; +} +.rant-detail { + padding: 6px 8px; + border-top: 1px solid var(--border); + font-size: var(--fs-secondary); +} +.rant-meta { + font-size: var(--fs-aux); + color: var(--text-3); + margin-bottom: 4px; +} +.rant-md { + margin-top: 4px; +} +.rant-progress { + margin-top: 8px; + padding-top: 6px; + border-top: 1px dashed var(--border); + font-size: var(--fs-aux); + color: var(--text-3); + word-break: break-all; +} .task-actions { display: flex; gap: 4px; diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html index 35d22e9..94702db 100644 --- a/emrg/gui/renderer/index.html +++ b/emrg/gui/renderer/index.html @@ -61,7 +61,7 @@
-
查看会话 / 添加项目 / 删除(保留磁盘数据)
+

项目管理

@@ -71,6 +71,7 @@
+

任务管理

管理定时任务与自定义任务类型(内置类型只读)
@@ -130,12 +131,20 @@
+

Rant 管理

+
+ 时间 + 项目 + 状态 + 进度 + 内容 +
diff --git a/emrg/gui/renderer/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js index 0799805..829c4c4 100644 --- a/emrg/gui/renderer/js/dialogs.js +++ b/emrg/gui/renderer/js/dialogs.js @@ -1065,27 +1065,40 @@ const Dialogs = (() => { return; } for (const r of rants) { - const row = el("div", { class: "task-row" }); + // rant 21:36:01:5 列对齐(时间/项目/状态/进度/内容),与 .rant-head 列头同列宽类 + const row = el("div", { class: "task-row rant-row" }); const ts = String(r.timestamp || "").slice(0, 16).replace("T", " "); - row.appendChild(el("span", { class: "task-name" }, ts)); - // 状态徽标 + row.appendChild(el("span", { class: "rant-col-time" }, ts || "—")); + row.appendChild(el("span", { class: "rant-col-project" }, r.project || "—")); + // 状态徽标三态配色(completed 绿 / in_progress 琥珀 / pending 灰) const st = r.status || "pending"; const stText = st === "completed" ? _t("rants.statusCompleted") : st === "in_progress" ? _t("rants.statusInProgress") : _t("rants.statusPending"); - row.appendChild(el("span", { class: `task-badge ${st === "completed" ? "badge-done" : ""}` }, stText)); - // 项目 + 进度摘要 - const hints = []; - if (r.project) hints.push(r.project); - if (r.progress) hints.push(String(r.progress).slice(0, 40) + (String(r.progress).length > 40 ? "…" : "")); - row.appendChild(el("span", { class: "task-hint" }, hints.join(" · ") || "—")); - // 详情展开(完整内容 + progress) + const badgeCls = st === "completed" ? "badge-done" : st === "in_progress" ? "badge-warn" : "badge-muted"; + row.appendChild(el("span", { class: `task-badge ${badgeCls}` }, stText)); + row.appendChild(el("span", { class: "rant-col-progress" }, r.progress ? String(r.progress) : "—")); + // 内容列:message 首行摘要(去 md 标题/列表标记) const msg = r.message || ""; - row.addEventListener("click", () => { + const firstLine = (msg.split("\n").find((l) => l.trim() !== "") || "").replace(/^#{1,6}\s+/, "").replace(/^[>*\-\s]+/, ""); + row.appendChild(el("span", { class: "rant-col-content" }, firstLine || "—")); + // 详情展开(rant 21:36:01:完整 message 按 markdown 渲染 + 进度显示) + row.addEventListener("click", async () => { const detail = list.querySelector(".rant-detail"); if (detail) detail.remove(); const detailRow = el("div", { class: "rant-detail", style: "padding:6px 8px;border-top:1px solid var(--border);font-size:var(--fs-secondary);" }); - detailRow.appendChild(el("div", {}, `${_t("rants.detail")}: ${msg}`)); - if (r.progress) detailRow.appendChild(el("div", {}, `${_t("rants.statusInProgress")}: ${r.progress}`)); - else detailRow.appendChild(el("div", {}, _t("rants.noProgress"))); + detailRow.appendChild(el("div", { class: "rant-meta" }, `${ts || "—"} · ${r.project || "—"} · ${stText}`)); + const md = el("div", { class: "msg-body rant-md" }); + try { + if (window.emrgMarkdown && window.emrgMarkdown.renderMarkdown) { + md.innerHTML = await window.emrgMarkdown.renderMarkdown(msg); + } else { + md.textContent = msg; + } + } catch { + md.textContent = msg; + } + detailRow.appendChild(md); + if (r.progress) detailRow.appendChild(el("div", { class: "rant-progress" }, `${_t("rants.statusInProgress")}: ${r.progress}`)); + else detailRow.appendChild(el("div", { class: "rant-progress" }, _t("rants.noProgress"))); row.after(detailRow); }); list.appendChild(row); diff --git a/emrg/gui/renderer/js/i18n.js b/emrg/gui/renderer/js/i18n.js index 0348ded..2ace367 100644 --- a/emrg/gui/renderer/js/i18n.js +++ b/emrg/gui/renderer/js/i18n.js @@ -32,7 +32,7 @@ const I18N = (() => { "nav.settings": "设置", // 项目面板(rant 14:10:14 P5) - "projects.hint": "查看会话 / 添加项目 / 删除(保留磁盘数据)", + "projects.title": "项目管理", "projects.add": "+ 添加项目", "projects.empty": "暂无项目(点击「添加项目」注册工作目录)", "projects.added": "项目已添加:{path}", @@ -91,6 +91,7 @@ const I18N = (() => { "settings.aboutTitle": "关于", "settings.aboutDesc": "EMRG 是一个会自我进化的 AI 智能体——每次改进都会自动汇报,你可以随时在这里看到它的成长。", "settings.recentTitle": "最近改进", + "tasks.title": "任务管理", "settings.tasksHint": "管理定时任务与自定义任务类型(内置类型只读)", "settings.taskAdd": "+ 添加任务", "settings.taskTemplates": "自定义类型", @@ -241,6 +242,12 @@ const I18N = (() => { // 进化 / 任务 // Rant 面板(rant 14:10:14 P4) + "rants.title": "Rant 管理", + "rants.colTime": "时间", + "rants.colProject": "项目", + "rants.colStatus": "状态", + "rants.colProgress": "进度", + "rants.colContent": "内容", "rants.filterAll": "全部", "rants.filterPending": "待处理", "rants.filterInProgress": "进行中", @@ -258,7 +265,6 @@ const I18N = (() => { "rants.statusPending": "待处理", "rants.statusInProgress": "进行中", "rants.statusCompleted": "已完成", - "rants.detail": "完整内容", "rants.noProgress": "(无进度说明)", // / 指令 hint(commands.js 补全菜单) @@ -440,7 +446,7 @@ const I18N = (() => { "nav.settings": "Settings", // Projects panel (rant 14:10:14 P5) - "projects.hint": "View sessions / add / remove (disk data kept)", + "projects.title": "Project Management", "projects.add": "+ Add project", "projects.empty": "No projects yet (click “Add project” to register a working directory)", "projects.added": "Project added: {path}", @@ -499,6 +505,7 @@ const I18N = (() => { "settings.aboutTitle": "About", "settings.aboutDesc": "EMRG is a self-evolving AI agent — it reports every improvement, and you can watch it grow right here.", "settings.recentTitle": "Recent improvements", + "tasks.title": "Task Management", "settings.tasksHint": "Manage scheduled tasks and custom task types (builtin types are read-only)", "settings.taskAdd": "+ Add task", "settings.taskTemplates": "Custom types", @@ -649,6 +656,12 @@ const I18N = (() => { // Evolution / tasks // Rant panel (rant 14:10:14 P4) + "rants.title": "Rant Management", + "rants.colTime": "Time", + "rants.colProject": "Project", + "rants.colStatus": "Status", + "rants.colProgress": "Progress", + "rants.colContent": "Content", "rants.filterAll": "All", "rants.filterPending": "Pending", "rants.filterInProgress": "In progress", @@ -666,7 +679,6 @@ const I18N = (() => { "rants.statusPending": "Pending", "rants.statusInProgress": "In progress", "rants.statusCompleted": "Completed", - "rants.detail": "Full content", "rants.noProgress": "(no progress note)", // / command hints @@ -877,7 +889,14 @@ const I18N = (() => { if (typeof document === "undefined" || !document.querySelectorAll) return; document.querySelectorAll("[data-i18n]").forEach((node) => { const key = node.getAttribute("data-i18n"); - if (key) node.textContent = t(key); + if (!key) return; + const text = t(key); + // rant 21:46:53:只替换首个文本节点,保留 label 内的子元素控件(input/select/textarea) + // ——整体 textContent 赋值会清空含控件 label(如 )的子元素, + // 导致任务/模板表单控件消失、「添加任务」点击无响应。 + const first = node.firstChild; + if (first && first.nodeType === 3) first.nodeValue = text; + else node.textContent = text; }); document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => { const key = node.getAttribute("data-i18n-placeholder"); diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index 18762f3..ab1b9d2 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -2918,9 +2918,9 @@ test("rant 14:10:14 P5:项目面板列表(auto_evolve 徽标 + 最近活跃 assert.strictEqual(removed.name, "docs"); }); -test("rant 14:10:14 P4:rant 面板列表(状态徽标 + 筛选)+ 详情展开 + 新建提交", async () => { +test("rant 14:10:14 P4:rant 面板列表(5 列 + 状态徽标三态 + 筛选)+ 详情 markdown 渲染 + 新建提交", async () => { const rants = [ - { timestamp: "2026-08-13T14:10:14.854793", project: "emrg", status: "in_progress", progress: "P1 done", message: "GUI 重设计" }, + { timestamp: "2026-08-13T14:10:14.854793", project: "emrg", status: "in_progress", progress: "P1 done", message: "## GUI 重设计\n**加粗** 与 `code`" }, { timestamp: "2026-08-12T09:00:00", project: "", status: "completed", progress: null, message: "旧 rant" }, { timestamp: "2026-08-13T10:00:00", project: "", status: "pending", progress: null, message: "新想法" }, ]; @@ -2935,21 +2935,45 @@ test("rant 14:10:14 P4:rant 面板列表(状态徽标 + 筛选)+ 详情展 await vm.runInContext("App.openRantsPanel()", ctx); await tick(); const list = els["rant-list"]; - // 3 行 rant,全部含时间戳 + 状态徽标 + // 3 行 rant(.task-row.rant-row),状态徽标三态配色 + 5 列 const rows = list.children.filter((c) => c.className.includes("task-row")); assert.strictEqual(rows.length, 3, `rant 应渲染 3 行,实际 ${rows.length}`); const badges = rows.map((r) => r.querySelector(".task-badge") ? r.querySelector(".task-badge").textContent : ""); assert.ok(badges[0].includes("进行中"), `行0 徽标应含进行中,实际 ${badges[0]}`); assert.ok(badges[1].includes("已完成"), `行1 徽标应含已完成,实际 ${badges[1]}`); assert.ok(badges[2].includes("待处理"), `行2 徽标应含待处理,实际 ${badges[2]}`); - // 点击行 → 详情展开(完整内容 + progress) + // 三态配色类:in_progress→badge-warn / completed→badge-done / pending→badge-muted + const badgeCls = rows.map((r) => r.querySelector(".task-badge") ? r.querySelector(".task-badge").className : ""); + assert.ok(badgeCls[0].includes("badge-warn"), `行0 徽标应为 badge-warn,实际 ${badgeCls[0]}`); + assert.ok(badgeCls[1].includes("badge-done"), `行1 徽标应为 badge-done,实际 ${badgeCls[1]}`); + assert.ok(badgeCls[2].includes("badge-muted"), `行2 徽标应为 badge-muted,实际 ${badgeCls[2]}`); + // 5 列:时间/项目/状态/进度/内容(内容列去 md 标题符号后的摘要) + const timeCells = rows.map((r) => r.querySelector(".rant-col-time") ? r.querySelector(".rant-col-time").textContent : ""); + assert.ok(timeCells[0].includes("2026-08-13 14:10"), `时间列应含截断时间戳,实际 ${timeCells[0]}`); + const projCells = rows.map((r) => r.querySelector(".rant-col-project") ? r.querySelector(".rant-col-project").textContent : ""); + assert.strictEqual(projCells[0], "emrg", "项目列应显示项目名"); + assert.strictEqual(projCells[1], "—", "无项目行应显示 —"); + const contentCells = rows.map((r) => r.querySelector(".rant-col-content") ? r.querySelector(".rant-col-content").textContent : ""); + assert.ok(contentCells[0].includes("GUI 重设计"), `内容列应去 ## 后含标题摘要,实际 ${contentCells[0]}`); + assert.strictEqual(contentCells[0].includes("##"), false, "内容列摘要不应含 md 标题符号"); + const progCells = rows.map((r) => r.querySelector(".rant-col-progress") ? r.querySelector(".rant-col-progress").textContent : ""); + assert.strictEqual(progCells[0], "P1 done", "进度列应显示 progress"); + // 点击行 → 详情展开:meta 行 + markdown 渲染(注入真实 marked)+ progress + const markedReal = require(path.join(__dirname, "..", "vendor", "marked.min.js")).marked; + ctx.marked = markedReal; // 注入真实 marked 到 vm 全局(window.marked) rows[0].click(); await tick(); const detail = list.querySelector(".rant-detail"); assert.ok(detail, "点击行应展开详情"); - const detailText = (detail.children || []).map((c) => c.textContent || "").join(" "); - assert.ok(detailText.includes("GUI 重设计"), `详情应含完整 message,实际 ${detailText}`); - assert.ok(detailText.includes("P1 done"), `详情应含 progress,实际 ${detailText}`); + const metaText = (detail.querySelector(".rant-meta") || {}).textContent || ""; + assert.ok(metaText.includes("emrg"), `meta 应含项目,实际 ${metaText}`); + const detailMd = detail.querySelector(".rant-md"); + assert.ok(detailMd, "详情应有 .rant-md 容器"); + assert.ok((detailMd._html || "").includes(" { + const htmlSrc = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf-8"); + const i18nSrc = fs.readFileSync(path.join(__dirname, "..", "renderer", "js", "i18n.js"), "utf-8"); + // Rant 面板:标题 + 5 列头 + const rantSection = htmlSrc.match(/id="panel-rants"([\s\S]*?)(?:)/); + assert.ok(projSection, "index.html 应有项目面板 section"); + assert.ok(projSection[1].includes('class="workspace-view-title" data-i18n="projects.title"'), + "项目面板应有 .workspace-view-title 标题(data-i18n=projects.title)"); + assert.ok(!projSection[1].includes("projects.hint"), "项目面板不应再有顶部操作说明 hint"); + // 任务面板:标题 + const taskSection = htmlSrc.match(/id="panel-tasks"([\s\S]*?)(?:)/); + assert.ok(taskSection, "index.html 应有任务面板 section"); + assert.ok(taskSection[1].includes('class="workspace-view-title" data-i18n="tasks.title"'), + "任务面板应有 .workspace-view-title 标题(data-i18n=tasks.title)"); + // i18n:三面板标题键存在(zh/en) + for (const key of ['"rants.title"', '"projects.title"', '"tasks.title"', '"rants.colTime"', '"rants.colProject"', '"rants.colStatus"', '"rants.colProgress"', '"rants.colContent"']) { + assert.ok(i18nSrc.includes(key), `i18n.js 应定义 ${key}`); + } + // i18n.apply 修复(rant 21:46:53):不再整体 textContent 清子元素,只替换首个文本节点 + const i18nApplySrc = i18nSrc; + assert.ok(!i18nApplySrc.includes("node.textContent = t(key)"), "i18n.apply 不应整体 textContent 赋值(会清空含控件 label 子元素)"); + assert.ok(i18nApplySrc.includes("first.nodeValue = text"), "i18n.apply 应只替换首个文本节点(first.nodeValue = text)"); + // 孤儿键清理:rants.detail / projects.hint 已随本改动删除 + assert.ok(!i18nSrc.includes('"rants.detail"'), "i18n 不应再有孤儿 rants.detail"); + assert.ok(!i18nSrc.includes('"projects.hint"'), "i18n 不应再有孤儿 projects.hint"); +}); + test("rant 21:49:51 settings panel title + sidebar settings-btn removed", async () => { const htmlSrc = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf-8"); // 设置面板应有标题(对齐 Rant/项目/任务面板 .workspace-view-title)