Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>.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/`)
Expand Down Expand Up @@ -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 路径不受影响)

Expand Down
14 changes: 14 additions & 0 deletions emrg/gui/renderer/js/dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {}); });
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ const I18N = (() => {
"settings.taskTrigger": "触发",
"settings.taskEdit": "编辑",
"settings.taskDelete": "删除",
"settings.taskOpenSession": "打开会话",
"settings.taskEmpty": "暂无任务",
"settings.taskBuiltin": "内置",
"settings.taskCustom": "自定义",
Expand Down Expand Up @@ -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",
Expand Down
32 changes: 31 additions & 1 deletion emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions emrg/server/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions tests/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading