From f37ecc909a3fbfe9654b2df31616ff7c24bd6a6f Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 13 Aug 2026 21:07:53 +0800 Subject: [PATCH 1/2] =?UTF-8?q?emrg:=20GUI=20file=20browser=20=E2=80=94=20?= =?UTF-8?q?chevron=20indicators=20+=20fix=20tree=20scroll=20(rant=202026-0?= =?UTF-8?q?8-13T20:58:57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Agent.md | 4 ++-- emrg/gui/renderer/css/layout.css | 13 ++++++++++++ emrg/gui/renderer/js/file-tree.js | 15 ++++++++++++-- emrg/gui/test/renderer.smoke.test.js | 31 ++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/Agent.md b/Agent.md index 2c2e1d2..09d2b38 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` (237: 44 daemon_client + 19 conn-manager + 22 app-commands + 115 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py` + - Unit tests `npm test` (238: 44 daemon_client + 19 conn-manager + 22 app-commands + 116 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); 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` (237: 44 daemon_client + 19 conn-manager + 22 app-commands + 115 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` +GUI: `cd emrg/gui && npm test` (238: 44 daemon_client + 19 conn-manager + 22 app-commands + 116 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — 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/layout.css b/emrg/gui/renderer/css/layout.css index 0bcdcb7..a6005a6 100644 --- a/emrg/gui/renderer/css/layout.css +++ b/emrg/gui/renderer/css/layout.css @@ -572,6 +572,9 @@ body.sidebar-collapsed #app { display: block; /* 块级排布:.ft-head 行 + .ft-kids 子树纵向排列,行高自适应 (修复布局 bug:定高 flex-wrap 使下一兄弟行与展开目录首个子项 重叠,headless Chrome 像素实证——兄弟行遮挡子项) */ + flex-shrink: 0; /* rant 20:58:57:.ft-row 是 .result-files(flex column)的 flex + item,默认 shrink:1 + overflow:hidden → min-height:auto 解析为 0, + flex 把整棵树压缩到容器高度,scrollHeight==clientHeight 无法滚动 */ cursor: pointer; font-size: var(--fs-small); color: var(--text-2); @@ -607,6 +610,16 @@ body.sidebar-collapsed #app { fill: currentColor; } .ft-icon path { fill: currentColor; } +/* 展开/收起 chevron(VS Code 风格 ▸/▾,rant 2026-08-13T20:58:57) */ +.ft-chevron { + flex: none; + width: 14px; + height: 14px; + margin-right: 2px; + color: currentColor; + fill: currentColor; +} +.ft-chevron path { fill: currentColor; } .ft-name { flex: 1; overflow: hidden; diff --git a/emrg/gui/renderer/js/file-tree.js b/emrg/gui/renderer/js/file-tree.js index 22af51e..ff1c7a6 100644 --- a/emrg/gui/renderer/js/file-tree.js +++ b/emrg/gui/renderer/js/file-tree.js @@ -32,6 +32,8 @@ const FileTree = (() => { // ── mono 内联 SVG 图标(16x16 viewBox,fill: currentColor 跟随文字色) ── const ICON = { + chevronRight: '', // 折叠 ▸(目录可展开提示) + chevronDown: '', // 展开 ▾ dirClosed: '', dirOpen: '', fileDefault: '', @@ -55,11 +57,14 @@ const FileTree = (() => { txt: "fileTxt", log: "fileTxt", csv: "fileTxt", tsv: "fileTxt", }; - function iconSvg(d) { - const svg = el("svg", { class: "ft-icon", viewBox: "0 0 16 16", width: 14, height: 14, "aria-hidden": "true" }); + function iconSvg(d, cls) { + const svg = el("svg", { class: cls || "ft-icon", viewBox: "0 0 16 16", width: 14, height: 14, "aria-hidden": "true" }); svg.innerHTML = d; // 静态白名单路径,无注入风险 return svg; } + function chevronFor(entry) { + return expanded.get(entry.path) ? ICON.chevronDown : ICON.chevronRight; + } function iconFor(entry) { if (entry.type === "dir") return expanded.get(entry.path) ? ICON.dirOpen : ICON.dirClosed; const ext = String(entry.name).split(".").pop().toLowerCase(); @@ -123,6 +128,9 @@ const FileTree = (() => { // 行头(图标+名称)单独包装:.ft-row 为块级容器(head + kids 纵向排布), // 修复定高 flex-wrap 下兄弟行与展开子项重叠的布局 bug(headless Chrome 像素实证) const head = el("div", { class: "ft-head" }); + if (entry.type === "dir") { + head.appendChild(iconSvg(chevronFor(entry), "ft-chevron")); + } head.appendChild(iconSvg(iconFor(entry))); head.appendChild(el("span", { class: "ft-name" }, entry.name)); row.appendChild(head); @@ -162,6 +170,8 @@ const FileTree = (() => { if (row) { const ic = row.querySelector(".ft-icon"); if (ic) ic.innerHTML = expanded.get(path) ? ICON.dirOpen : ICON.dirClosed; + const chev = row.querySelector(".ft-chevron"); + if (chev) chev.innerHTML = expanded.get(path) ? ICON.chevronDown : ICON.chevronRight; } } @@ -198,6 +208,7 @@ const FileTree = (() => { const rootRow = el("div", { class: "ft-row ft-dir ft-root", dataset: { path: root } }); rootRow.style.paddingLeft = "8px"; const rootHead = el("div", { class: "ft-head" }); + rootHead.appendChild(iconSvg(ICON.chevronDown, "ft-chevron")); // 根默认展开 → ▾ rootHead.appendChild(iconSvg(ICON.dirOpen)); rootHead.appendChild(el("span", { class: "ft-name" }, rootName)); rootRow.appendChild(rootHead); diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index ebe60c3..7ccaca4 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -1183,6 +1183,37 @@ test("P3:点击目录行懒加载展开子项(缓存,折叠再展开不重 assert.strictEqual(calls, 2, "缓存命中不重复拉取"); }); +test("P3.5:目录行 chevron 展开/收起指示(rant 20:58:57)", async () => { + const { ctx, els } = makeSandbox({ + listFiles: async ({ path } = {}) => { + if (path === "/proj") return { entries: [{ name: "src", path: "/proj/src", type: "dir" }] }; + return { entries: [] }; + }, + }); + await tick(); + await vm.runInContext('FileTree.setSession("s1", "/proj")', ctx); + await tick(); + // 根默认展开 → chevronDown ▾ + const rootRow = els["result-files"].querySelectorAll(".ft-root")[0]; + const rootChev = rootRow.querySelector(".ft-chevron"); + assert.ok(rootChev, "根目录行应有 chevron"); + assert.ok(rootChev.innerHTML.includes("M3 6l5 5 5-5z"), "根展开态 chevronDown"); + // src 目录折叠 → chevronRight ▸ + const srcRow = els["result-files"].querySelectorAll(".ft-dir")[1]; + const srcChev = srcRow.querySelector(".ft-chevron"); + assert.ok(srcChev, "目录行应有 chevron"); + assert.ok(srcChev.innerHTML.includes("M6 3l5 5-5 5z"), "折叠态 chevronRight"); + // 点击展开 → 切 chevronDown + srcRow.dispatch("click", { stopPropagation() {} }); + await tick(); + assert.ok(srcChev.innerHTML.includes("M3 6l5 5 5-5z"), "展开后 chevronDown"); + // 文件行无 chevron + const files = els["result-files"].querySelectorAll(".ft-file"); + for (const f of files) { + assert.strictEqual(f.querySelector(".ft-chevron"), null, "文件行不应有 chevron"); + } +}); + test("P3:点击文件行 → 打开文件 Tab + 查看器加载内容", async () => { const { ctx, els } = makeSandbox(); await tick(); From cf32eb64544770d922c8b303779e8b8583d516f0 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 13 Aug 2026 21:16:42 +0800 Subject: [PATCH 2/2] =?UTF-8?q?emrg:=20doc=20sync=20=E2=80=94=20GUI=20test?= =?UTF-8?q?=20count=20239=20(117=20renderer=20smoke)=20after=20#768=20merg?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Agent.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Agent.md b/Agent.md index 09d2b38..8f722d6 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` (238: 44 daemon_client + 19 conn-manager + 22 app-commands + 116 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py` + - Unit tests `npm test` (239: 44 daemon_client + 19 conn-manager + 22 app-commands + 117 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); 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` (238: 44 daemon_client + 19 conn-manager + 22 app-commands + 116 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` +GUI: `cd emrg/gui && npm test` (239: 44 daemon_client + 19 conn-manager + 22 app-commands + 117 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — 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 路径不受影响)