From 38db608bffbac238f6b906c13f17e1990024d21b Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Fri, 21 Aug 2026 17:56:49 +0800 Subject: [PATCH] emrg: add 'open session' action to GUI task rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rant 2026-08-21T17:46:12: the tasks panel has trigger/edit/delete actions per row but no way to jump straight into a task's session. - scheduler.py TaskHandler.status(): expose project (config.project), project_path (resolved) and session_id (emrg-evolution-{name}) — the task→session link. Pure in-memory fields, no I/O. - dialogs.js renderTaskList: add an 'open session' button to each task row (after trigger) that calls App.switchSession(session_id, { projectPath }) — identical effect to picking the session in the open-session dialog (appears in the left list + activates). Disabled when the task has no session_id. - i18n.js: settings.taskOpenSession (zh/en). - Tests: scheduler status test asserts the three new fields; renderer smoke test clicks the new button and verifies switchSession is called with the fixed session id + project path, and that a task without session_id gets a disabled button. - Agent.md GUI count 257 -> 258 (renderer smoke 128 -> 129). --- Agent.md | 4 ++-- emrg/gui/renderer/js/dialogs.js | 14 ++++++++++++ emrg/gui/renderer/js/i18n.js | 2 ++ emrg/gui/test/renderer.smoke.test.js | 32 +++++++++++++++++++++++++++- emrg/server/scheduler.py | 7 ++++++ tests/test_scheduler.py | 4 ++++ 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/Agent.md b/Agent.md index 648bf7e..733878b 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` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py` + - Unit tests `npm test` (258: 45 daemon_client + 19 conn-manager + 22 app-commands + 129 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 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.token; python -m emrg ``` Python: `uv run pytest tests/ -v` (1001) — import check: `uv run python -c "from emrg.client.app import run_client"` -GUI: `cd emrg/gui && npm test` (257: 45 daemon_client + 19 conn-manager + 22 app-commands + 128 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 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` (258: 45 daemon_client + 19 conn-manager + 22 app-commands + 129 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 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/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js index 72ecd90..8d708b3 100644 --- a/emrg/gui/renderer/js/dialogs.js +++ b/emrg/gui/renderer/js/dialogs.js @@ -723,6 +723,20 @@ const Dialogs = (() => { } }); actions.appendChild(trigBtn); + // 打开会话(rant 2026-08-21T17:46:12:任务行加"打开会话"按钮——直接打开该任务 + // 的固定会话 emrg-evolution-{name},效果与"打开会话"弹窗选择一致(左侧列表出现并激活)。 + // session_id/project_path 来自 daemon list_tasks(TaskHandler.status() 新增字段)。 + const sessBtn = el("button", { type: "button", class: "model-action-btn", title: _t("settings.taskOpenSession") }, _t("settings.taskOpenSession")); + if (!t.session_id) sessBtn.disabled = true; + sessBtn.addEventListener("click", async () => { + try { + await App.switchSession(t.session_id, { projectPath: t.project_path }); + } catch (e) { + Chat.addSystemMessage(_t("openSession.loadFailed", { msg: e.message })); + showToast(_t("openSession.loadFailed", { msg: e.message }), { type: "error" }); + } + }); + actions.appendChild(sessBtn); // 编辑(表单预填 → taskUpdate) const editBtn = el("button", { type: "button", class: "model-action-btn", title: _t("settings.taskEdit") }, _t("settings.taskEdit")); editBtn.addEventListener("click", () => { openTaskForm(t).catch(() => {}); }); diff --git a/emrg/gui/renderer/js/i18n.js b/emrg/gui/renderer/js/i18n.js index afb085a..e90d01c 100644 --- a/emrg/gui/renderer/js/i18n.js +++ b/emrg/gui/renderer/js/i18n.js @@ -109,6 +109,7 @@ const I18N = (() => { "settings.taskTrigger": "触发", "settings.taskEdit": "编辑", "settings.taskDelete": "删除", + "settings.taskOpenSession": "打开会话", "settings.taskEmpty": "暂无任务", "settings.taskBuiltin": "内置", "settings.taskCustom": "自定义", @@ -521,6 +522,7 @@ const I18N = (() => { "settings.taskTrigger": "Trigger", "settings.taskEdit": "Edit", "settings.taskDelete": "Delete", + "settings.taskOpenSession": "Open session", "settings.taskEmpty": "No tasks", "settings.taskBuiltin": "builtin", "settings.taskCustom": "custom", diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index cdf8e89..993b264 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -2613,7 +2613,7 @@ test("P3:设置面板打开 → 任务列表渲染(名称/类型/项目/间 await vm.runInContext(`(function() { const row = document.getElementById("task-list").children[0]; const btns = row.querySelectorAll(".model-action-btn"); - btns[1].click(); // 0=触发 1=编辑 + btns[2].click(); // 0=触发 1=打开会话 2=编辑(rant 2026-08-21T17:46:12 插入会话按钮后) })()`, ctx); assert.strictEqual(vm.runInContext('document.getElementById("task-form").classList.contains("hidden")', ctx), false, "编辑应展开表单"); assert.strictEqual(els["task-form-name"].value, "daily-report"); @@ -2840,6 +2840,36 @@ test("rant 2026-08-15T09:20:27/09:23:10:面板操作反馈走全局 toast( assert.strictEqual(els["toast"].classList.contains("toast-error"), true, "error → error toast"); }); +test("rant 2026-08-21T17:46:12:任务行'打开会话'按钮——直接打开该任务的固定会话", async () => { + const switchCalls = []; + const { ctx, els } = makeSandbox({ + listTasks: async () => [ + { name: "emrg-task", type: "evolution", running: true, interval: 60, session_id: "emrg-evolution-emrg-task", project_path: "/p/emrg", project: "emrg" }, + { name: "no-sess", type: "custom", running: false, interval: 3600 }, // 无 session_id → 按钮禁用 + ], + listProjects: async () => [{ name: "emrg", path: "/p/emrg" }], + switchSession: async (payload) => { switchCalls.push(payload); return {}; }, + }); + await tick(); + await vm.runInContext("App.openTasksPanel()", ctx); + await tick(); + const rows = vm.runInContext('Array.from(document.getElementById("task-list").children).filter((c) => c.className.includes("task-row"))', ctx); + assert.strictEqual(rows.length, 2, "两行"); + // 按钮序:0=触发 1=打开会话 2=编辑 3=删除 + const btns0 = rows[0].querySelectorAll(".model-action-btn"); + assert.strictEqual(btns0.length, 4, "操作按钮 = 触发/打开会话/编辑/删除"); + assert.ok(String(btns0[1].textContent).includes("打开会话"), "第 2 个按钮应为'打开会话'"); + assert.strictEqual(btns0[1].disabled, false, "有 session_id 的按钮可用"); + btns0[1].click(); + await tick(); + assert.strictEqual(switchCalls.length, 1, "点击应调用 switchSession"); + assert.strictEqual(switchCalls[0].sessionId, "emrg-evolution-emrg-task", "带固定会话 id"); + assert.strictEqual(switchCalls[0].projectPath, "/p/emrg", "带项目路径"); + // 无 session_id → 按钮禁用(真实 DOM disabled 不派发 click;沙箱 fake 不模拟该语义,故只断言禁用态) + const btns1 = rows[1].querySelectorAll(".model-action-btn"); + assert.strictEqual(btns1[1].disabled, true, "无 session_id 的按钮禁用"); +}); + // ── rant 2026-08-15T10:36:39:任务状态 + 下次运行倒计时 ── test("rant 10:36:39:formatCountdown 格式边界(0s/59s/60s/1h05m/负数钳制)", () => { const { ctx } = makeSandbox(); diff --git a/emrg/server/scheduler.py b/emrg/server/scheduler.py index c1cf904..5cb2ed6 100644 --- a/emrg/server/scheduler.py +++ b/emrg/server/scheduler.py @@ -581,6 +581,13 @@ def status(self) -> dict: "last_run_at": last_run_at, "recent_runs": recent_runs, "saturation": saturation, + # rant 2026-08-21T17:46:12: expose the task→session link so the GUI + # tasks panel can add an "open session" action. All in-memory — + # project name (config.project), resolved project path, and the + # fixed task session_id (emrg-evolution-{name}). + "project": self._project_name, + "project_path": self.project_path, + "session_id": self._session_id, } def _heartbeat_interval(self) -> int: diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 4bdd81f..25a7941 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -549,6 +549,10 @@ def test_evolution_handler_status_last_run_fields(): assert "heartbeat_interval" in st["saturation"] # rant 2026-08-18T21:32:32: recent_runs present, empty before any run assert st["recent_runs"] == [] + # rant 2026-08-21T17:46:12: task→session link fields (GUI open-session action) + assert st["session_id"] == "emrg-evolution-test" + assert st["project"] == "" # config={} → empty project name + assert st["project_path"] == "test" # fallback path = name # after one evolution → last-run populated from the latest log handler.evolutions.append(EvolutionLog( timestamp="2026-08-18T10:00:00",