diff --git a/Agent.md b/Agent.md index 918bbf77..9deee6fd 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` (245: 44 daemon_client + 19 conn-manager + 22 app-commands + 120 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` (246: 44 daemon_client + 19 conn-manager + 22 app-commands + 121 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` (805) — import check: `uv run python -c "from emrg.client.app import run_client"` -GUI: `cd emrg/gui && npm test` (245: 44 daemon_client + 19 conn-manager + 22 app-commands + 120 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` (246: 44 daemon_client + 19 conn-manager + 22 app-commands + 121 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/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js index 829c4c4a..1e1808a0 100644 --- a/emrg/gui/renderer/js/dialogs.js +++ b/emrg/gui/renderer/js/dialogs.js @@ -672,7 +672,7 @@ const Dialogs = (() => { actions.appendChild(trigBtn); // 编辑(表单预填 → taskUpdate) const editBtn = el("button", { type: "button", class: "model-action-btn", title: _t("settings.taskEdit") }, _t("settings.taskEdit")); - editBtn.addEventListener("click", () => openTaskForm(t)); + editBtn.addEventListener("click", () => { openTaskForm(t).catch(() => {}); }); actions.appendChild(editBtn); // 删除(确认弹窗 → taskDelete → 热重载由 daemon apply_tasks 完成) const delBtn = el("button", { type: "button", class: "model-action-btn danger", title: _t("settings.taskDelete") }, _t("settings.taskDelete")); @@ -697,7 +697,12 @@ const Dialogs = (() => { } } - function openTaskForm(task = null) { + async function openTaskForm(task = null) { + // rant 2026-08-14T15:41:52:点"+ 添加任务"时元数据未加载完 → 下拉为空 → 保存报 invalid type + // 幂等快路径:openTasksPanel/保存刷新已加载过 → 同步填(无闪烁);未完成 → 等 IPC 返回再填 + if (taskTypes.length === 0 && taskProjects.length === 0) { + await loadTaskMeta(); + } editingTask = task ? task.name : null; $("task-form-name").value = task ? task.name : ""; $("task-form-name").disabled = Boolean(task); // 决策点:daemon 以 name 定位 → 不可改名 @@ -863,7 +868,7 @@ const Dialogs = (() => { function initTaskManagement() { const addBtn = $("task-add-btn"); - if (addBtn) addBtn.addEventListener("click", () => openTaskForm(null)); + if (addBtn) addBtn.addEventListener("click", () => { openTaskForm(null).catch(() => {}); }); const saveBtn = $("task-form-save"); if (saveBtn) saveBtn.addEventListener("click", saveTaskForm); const cancelBtn = $("task-form-cancel"); diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index e65c0c1e..628f5657 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -2660,6 +2660,48 @@ test("P3:新增任务表单 —— 间隔 <60 客户端拒绝;≥60 提交 t assert.strictEqual(vm.runInContext('document.getElementById("task-form").classList.contains("hidden")', ctx), true, "保存后表单收起"); }); +test("rant 2026-08-14T15:41:52:快速点击添加任务 —— 元数据未加载完也填充下拉 + 保存成功", async () => { + let created = null; + const { ctx, els } = makeSandbox({ + listTasks: async () => [], + // 延迟返回(50ms)→ 复现"打开面板后立即点添加"竞态(loadTaskMeta 未完成) + taskTemplateList: async () => { + await new Promise((r) => setTimeout(r, 50)); + return [ + { name: "evolution", builtin: true, template: "evolution_prompt.md" }, + { name: "custom-a", builtin: false, template: "custom-a.md", prompt: "# a" }, + ]; + }, + listProjects: async () => { + await new Promise((r) => setTimeout(r, 50)); + return [{ name: "emrg", path: "/p/emrg" }]; + }, + taskCreate: async (payload) => { created = payload; return { ok: true }; }, + }); + await tick(); + // 打开任务面板(fire-and-forget,不等 loadTaskMeta —— 复现宿主"点 ⏱ 立即点添加") + vm.runInContext("App.openTasksPanel()", ctx); + els["task-form"].classList.add("hidden"); // 镜像 index.html 初始态 + // 元数据还在加载(50ms 未到)就点"+ 添加任务" + await vm.runInContext('document.getElementById("task-add-btn").click()', ctx); + // 等 openTaskForm 内 await loadTaskMeta() 完成(50ms 延迟 + 缓冲) + await new Promise((r) => setTimeout(r, 120)); + await tick(); + // 下拉应有选项(修复前:0 个选项 → 保存报 invalid type → 界面"没反应") + const typeOpts = vm.runInContext('Array.from(document.getElementById("task-form-type").children).map((o) => o.value)', ctx); + assert.deepStrictEqual(typeOpts.sort(), ["custom-a", "evolution"]); + const projOpts = vm.runInContext('Array.from(document.getElementById("task-form-project").children).map((o) => o.value)', ctx); + assert.deepStrictEqual(projOpts, ["emrg"], "项目下拉应含已注册项目"); + // 填任务名 → 保存 → taskCreate payload type/project 非空 + vm.runInContext('document.getElementById("task-form-name").value = "quick-task"', ctx); + await vm.runInContext("EMRG_Dialogs.saveTaskForm()", ctx); + await tick(); + assert.ok(created, "快速点击保存应成功(不报 invalid type)"); + assert.ok(created.type && created.project, `type/project 非空:${JSON.stringify(created)}`); + assert.strictEqual(created.type, "evolution"); + assert.strictEqual(created.project, "emrg"); +}); + test("P3:自定义类型管理 —— 内置只读;自定义增删改走模板 CRUD", async () => { let createdTpl = null; let deletedTpl = null;