From a8e06ed2cdecc1edf4bcd8292ec3244759d1ec78 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 13 Aug 2026 12:52:25 +0800 Subject: [PATCH 1/2] emrg: fix GUI file-browser scroll + root collapse (rants 12:46/12:47) --- Agent.md | 4 ++-- emrg/gui/renderer/css/layout.css | 4 ++++ emrg/gui/renderer/js/file-tree.js | 9 ++++++++- emrg/gui/test/renderer.smoke.test.js | 28 ++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Agent.md b/Agent.md index f31019a..9523fab 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` (229: 44 daemon_client + 19 conn-manager + 22 app-commands + 107 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` (231: 44 daemon_client + 19 conn-manager + 22 app-commands + 109 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` (761) — import check: `uv run python -c "from emrg.client.app import run_client"` -GUI: `cd emrg/gui && npm test` (229: 44 daemon_client + 19 conn-manager + 22 app-commands + 107 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` (231: 44 daemon_client + 19 conn-manager + 22 app-commands + 109 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 566fb44..a29fed4 100644 --- a/emrg/gui/renderer/css/layout.css +++ b/emrg/gui/renderer/css/layout.css @@ -465,6 +465,10 @@ body.sidebar-collapsed #app { /* ── P3.1:文件浏览器(懒加载目录树,VS Code 风格 rant 2026-08-12T17:28:19) ── */ .result-files { + /* flex:1 + min-height:0(rant 2026-08-13T12:46:12):flex 子项默认 flex:0 1 auto 高度随内容 + 增长永不压缩进容器 → overflow-y:auto 永不触发(有滚动条槽但滚不动);与 .result-list 对齐 */ + flex: 1; + min-height: 0; overflow-y: auto; /* 滚动条 hover 显示(VS Code 行为:平时隐藏,移入文件树区域显示) */ scrollbar-width: thin; diff --git a/emrg/gui/renderer/js/file-tree.js b/emrg/gui/renderer/js/file-tree.js index e07aee2..22af51e 100644 --- a/emrg/gui/renderer/js/file-tree.js +++ b/emrg/gui/renderer/js/file-tree.js @@ -203,9 +203,16 @@ const FileTree = (() => { rootRow.appendChild(rootHead); const kids = el("div", { class: "ft-kids" }); rootRow.appendChild(kids); + // 根目录行与普通目录行一致的可展开/收起(rant 2026-08-13T12:47:18:render() 手写根行漏绑 + // click → 点击根目录无反应永远展开;toggleDir 已负责 expanded Map 持久 + 图标切换) + const st = ensure(root); + rootRow.addEventListener("click", (e) => { + if (e && e.stopPropagation) e.stopPropagation(); + toggleDir(root, kids, st, 1); + }); c.appendChild(rootRow); // 根自动展开(fire-and-forget;根子项 depth=1) - expandDir(root, kids, ensure(root), 1); + expandDir(root, kids, st, 1); } return { setRoot, setSession }; diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index bb14f82..358f1cd 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -1369,6 +1369,34 @@ test("P3.5(rant 17:28):展开态持久 + 滚动条 hover 样式(CSS 源 assert.ok(tabsIdx < tabbarIdx && tabbarIdx < toggleIdx, "tabbar 应嵌套在 result-tabs 内(同排),旧布局 toggle 在 tabbar 之前"); }); +test("P3.5(rant 2026-08-13T12:46:12):.result-files 滚动 CSS flex:1 + min-height:0", async () => { + // #result-panel 是 flex column;.result-list 有 flex:1 所以滚动正常,.result-files 缺 flex:1/min-height:0 + // → 高度随内容增长永不压缩,overflow-y:auto 永不触发(有滚动条槽但滚不动) + const css = fs.readFileSync(path.join(RENDERER_CSS, "layout.css"), "utf8").replace(/\n/g, " "); + const m = css.match(/\.result-files\s*\{([^}]*)\}/); + assert.ok(m, ".result-files 规则存在"); + assert.ok(/flex:\s*1/.test(m[1]), ".result-files 应 flex:1(对齐 .result-list)"); + assert.ok(/min-height:\s*0/.test(m[1]), ".result-files 应 min-height:0(允许压缩触发滚动)"); + assert.ok(/overflow-y:\s*auto/.test(m[1]), ".result-files 保留 overflow-y:auto"); +}); + +test("P3.5(rant 2026-08-13T12:47:18):根目录行可收起/展开", async () => { + const { ctx, els } = makeSandbox(); + await tick(); + await vm.runInContext('FileTree.setSession("s1", "/proj")', ctx); + await tick(); + const rootRow = els["result-files"].querySelectorAll(".ft-root")[0]; + assert.ok(rootRow, "根目录行存在"); + const kids = rootRow.querySelector(".ft-kids"); + assert.ok(kids, "根目录 kids 容器存在"); + assert.ok(!kids.classList.contains("hidden"), "默认展开(ft-kids 无 hidden)"); + rootRow.dispatch("click", { stopPropagation() {} }); + assert.ok(kids.classList.contains("hidden"), "点击根目录应收起"); + rootRow.dispatch("click", { stopPropagation() {} }); + await tick(); + assert.ok(!kids.classList.contains("hidden"), "再次点击应展开"); +}); + test("工具调用上限中断 → 系统提示可继续(对齐 TUI,跨项目教训)", async () => { const { ctx, els } = makeSandbox({}); await tick(); From 72f98c946f448a27827ded0748f0b7dad1214857 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 13 Aug 2026 13:01:01 +0800 Subject: [PATCH 2/2] emrg: persist emrg workspace-path repair every cycle (stale-entry follow-up) --- Agent.md | 2 +- emrg/server/scheduler.py | 8 +++++++ tests/test_scheduler.py | 51 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/Agent.md b/Agent.md index 9523fab..804f010 100644 --- a/Agent.md +++ b/Agent.md @@ -118,7 +118,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg ``` -Python: `uv run pytest tests/ -v` (761) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (762) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (231: 44 daemon_client + 19 conn-manager + 22 app-commands + 109 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/server/scheduler.py b/emrg/server/scheduler.py index 3ea2f22..1b09061 100644 --- a/emrg/server/scheduler.py +++ b/emrg/server/scheduler.py @@ -474,6 +474,14 @@ def _ensure_evolution_workspace(self) -> bool: if self._is_usable_git_repo(str(evolve_dir)): self._source_dir = str(evolve_dir) self.project_path = str(evolve_dir) + # Persist the corrected path (idempotent — writes only when the + # emrg entry differs). #716 repairs a stale emrg entry (deleted + # pytest-temp dir leaked into projects.yml) at scheduler startup + # only; this re-persists every cycle so a mid-run pollution on a + # long-running daemon self-heals within one cycle without a + # restart (list_projects/GUI pickers stay correct). + if self._project_name == "emrg": + self._ensure_project_entry() self._ensure_origin_reachable() return True logger.warning( diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index c8d93f8..41659d7 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -860,6 +860,57 @@ def test_ensure_self_evolution_task_other_entries_preserved(tmp_path): assert len(names) == 2 +def test_ensure_evolution_workspace_persists_repaired_emrg_path(tmp_path): + """Long-running daemon: a stale emrg path (deleted pytest-temp dir) is + healed in-memory to the canonical workspace AND persisted back to + projects.yml on the next cycle — not only at scheduler startup (#716 + follow-up: startup-only repair leaves a dangling entry forever when the + daemon never restarts; list_projects/GUI pickers keep showing a dead path).""" + from emrg.server import scheduler as mod + + evolve_dir = tmp_path / "evolution" / "emrg" + evolve_dir.mkdir(parents=True) + + # Stale emrg entry pointing at a path that no longer exists on disk + # (exactly the 2026-08-12 pytest-temp-leak shape). + stale = tmp_path / "gone" / "emrg" + projects_yml = tmp_path / "projects.yml" + projects_yml.write_text(yaml.safe_dump([ + {"name": "emrg", "path": str(stale), "last_active": "2026-08-12T18:44:50"}, + {"name": "other", "path": str(tmp_path / "other")}, + ])) + + fake = FakeGitRun(git_repo=True) + orig_run = mod.subprocess.run + orig_evolve = mod.EVOLUTION_CWD + orig_config = mod.config_dir + mod.subprocess.run = fake + mod.EVOLUTION_CWD = tmp_path / "evolution" + mod.config_dir = lambda: tmp_path + try: + handler = TaskHandler( + name="emrg-task", + config={"project": "emrg"}, + interval=60, + identity=InstanceIdentity(), + ) + handler._source_dir = str(stale) # stale as resolved at handler start + handler.project_path = str(stale) + ok = handler._ensure_evolution_workspace() + finally: + mod.subprocess.run = orig_run + mod.EVOLUTION_CWD = orig_evolve + mod.config_dir = orig_config + + assert ok is True + assert handler._source_dir == str(evolve_dir) # in-memory heal (pre-existing) + data = yaml.safe_load(projects_yml.read_text(encoding="utf-8")) + by_name = {e["name"]: e for e in data} + assert by_name["emrg"]["path"] == str(evolve_dir) # NEW: persisted this cycle + assert by_name["other"]["path"] == str(tmp_path / "other") # untouched + assert len(data) == 2 + + def test_ensure_evolution_workspace_dev_repo_untouched(tmp_path): """A real writable git repo (dev machine) is used as-is — no clone.""" import subprocess as real_subprocess