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` (249: 45 daemon_client + 19 conn-manager + 22 app-commands + 122 renderer smoke + 16 i18n + 7 integration + 3 commands + 6 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (250: 45 daemon_client + 19 conn-manager + 22 app-commands + 123 renderer smoke + 16 i18n + 7 integration + 3 commands + 6 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.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (825) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (249: 45 daemon_client + 19 conn-manager + 22 app-commands + 122 renderer smoke + 16 i18n + 7 integration + 3 commands + 6 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` (250: 45 daemon_client + 19 conn-manager + 22 app-commands + 123 renderer smoke + 16 i18n + 7 integration + 3 commands + 6 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
34 changes: 34 additions & 0 deletions emrg/gui/renderer/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,16 @@ dialog::backdrop {
font-size: var(--fs-secondary);
}

/* rant 09:23:10:任务 running 徽标(常驻运行的任务从源头减少误点) */
.task-running-badge {
background: var(--bg-soft);
color: var(--text-3);
font-size: 10px;
padding: 1px 6px;
border-radius: 8px;
margin-left: 4px;
}

/* rant 09:17:45:提示词编辑器 Monaco 挂载容器(长提示词 300px 起步) */
.monaco-host {
width: 100%;
Expand Down Expand Up @@ -1156,6 +1166,30 @@ dialog::backdrop {
color: var(--text-1);
}

/* ── 通用 toast(rant 2026-08-15T09:20:27:面板操作反馈全局可见) ── */
.toast {
position: fixed;
top: 16px;
right: 20px;
z-index: 1100;
max-width: calc(100vw - 40px);
padding: 10px 16px;
border-radius: var(--radius-btn);
background: var(--bg-panel);
border: 1px solid var(--border);
box-shadow: var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.18));
animation: toast-in 0.22s var(--ease);
pointer-events: none;
}
.toast-msg {
font-size: var(--fs-secondary);
color: var(--text-1);
white-space: pre-wrap;
}
.toast-success { border-left: 4px solid var(--ok, #2e9e5b); }
.toast-error { border-left: 4px solid var(--danger, #d64545); }
.toast-info { border-left: 4px solid var(--accent, #4a7dff); }

/* ── 进化完成 toast(WorkBuddy P3)────────────────── */
.evolution-toast {
position: fixed;
Expand Down
5 changes: 5 additions & 0 deletions emrg/gui/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,11 @@ <h2 data-i18n="settings.githubDeviceTitle">连接 GitHub</h2>
</dialog>

<!-- 进化对话框(/rant,GUI / 指令 P4) -->
<!-- 通用 toast(rant 2026-08-15T09:20:27:面板操作反馈全局可见,任何视图可感知) -->
<div id="toast" class="toast hidden">
<div class="toast-msg" id="toast-msg"></div>
</div>

<!-- 进化完成 toast(WorkBuddy P3:进化发生时温和提示) -->
<div id="evolution-toast" class="evolution-toast hidden">
<div class="evolution-toast-title" data-i18n="copy.evolutionToastTitle">🌱 EMRG 刚刚完成一次自我进化!</div>
Expand Down
7 changes: 7 additions & 0 deletions emrg/gui/renderer/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -474,11 +474,17 @@ const App = (() => {
const res = await window.emrg.triggerTask({ name: n });
if (res.error) {
Chat.addSystemMessage(_t("app.triggerFailed", { msg: res.error }));
showToast(_t("app.triggerFailed", { msg: res.error }), { type: "error" });
} else if (res.result === "running") {
Chat.addSystemMessage(_t("app.taskRunning", { n }));
showToast(_t("app.taskRunning", { n }), { type: "info", durationMs: 4000 });
} else {
Chat.addSystemMessage(_t("app.triggered", { n }));
showToast(_t("app.triggered", { n }), { type: "success" });
}
} catch (e) {
Chat.addSystemMessage(_t("app.triggerFailed", { msg: e.message }));
showToast(_t("app.triggerFailed", { msg: e.message }), { type: "error" });
}
}

Expand Down Expand Up @@ -1787,6 +1793,7 @@ const App = (() => {
switchView, // rant 18:55:09 v0.2:工作区视图切换(导出供测试)
switchSettingsTab, // rant 14:10:14 P2:设置面板 tab 切换(导出供测试)
openTasksPanel, // rant 14:10:14 P3:任务面板打开 + 加载(导出供测试)
doTrigger, // rant 09:23:10:/trigger 三态语义(running → info;导出供测试)
openProjectsPanel, // rant 14:10:14 P5:项目面板打开 + 加载(导出供测试)
openRantsPanel, // rant 14:10:14 P4:rant 面板打开 + 加载(导出供测试)
refreshSessions,
Expand Down
29 changes: 26 additions & 3 deletions emrg/gui/renderer/js/dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -652,22 +652,37 @@ const Dialogs = (() => {
const row = el("div", { class: "task-row" });
row.appendChild(el("span", { class: "task-name" }, t.name || "?"));
row.appendChild(el("span", { class: "task-badge" }, t.type || "evolution"));
// rant 09:23:10:running 状态徽标(演化任务 60s 一轮几乎常驻 running,从源头减少误点)
if (t.running) {
const runBadge = el("span", { class: "task-badge task-running-badge" }, _t("app.taskRunningBadge"));
row.appendChild(runBadge);
}
const cfg = (t.config && typeof t.config === "object") ? t.config : {};
const hints = [];
if (cfg.project) hints.push(cfg.project);
if (t.interval != null) hints.push(_t("app.taskInterval", { n: t.interval ?? "-" }));
if (t.enabled === false) hints.push(_t("app.taskDisabled"));
row.appendChild(el("span", { class: "task-hint" }, hints.join(" · ")));
const actions = el("span", { class: "task-actions" });
// 触发(复用 /trigger 语义)
// 触发(复用 /trigger 语义;rant 09:23:10:running 时给 info 反馈而非假成功
const trigBtn = el("button", { type: "button", class: "model-action-btn", title: _t("settings.taskTrigger") }, _t("settings.taskTrigger"));
if (t.running) trigBtn.disabled = true;
trigBtn.addEventListener("click", async () => {
try {
const res = await window.emrg.triggerTask({ name: t.name });
if (res && res.error) Chat.addSystemMessage(_t("app.triggerFailed", { msg: res.error }));
else Chat.addSystemMessage(_t("app.triggered", { n: t.name }));
if (res && res.error) {
Chat.addSystemMessage(_t("app.triggerFailed", { msg: res.error }));
showToast(_t("app.triggerFailed", { msg: res.error }), { type: "error" });
} else if (res && res.result === "running") {
Chat.addSystemMessage(_t("app.taskRunning", { n: t.name }));
showToast(_t("app.taskRunning", { n: t.name }), { type: "info", durationMs: 4000 });
} else {
Chat.addSystemMessage(_t("app.triggered", { n: t.name }));
showToast(_t("app.triggered", { n: t.name }), { type: "success" });
}
} catch (e) {
Chat.addSystemMessage(_t("app.triggerFailed", { msg: e.message }));
showToast(_t("app.triggerFailed", { msg: e.message }), { type: "error" });
}
});
actions.appendChild(trigBtn);
Expand All @@ -685,9 +700,11 @@ const Dialogs = (() => {
try {
await window.emrg.taskDelete({ name: t.name });
Chat.addSystemMessage(_t("settings.taskDeleted"));
showToast(_t("settings.taskDeleted"), { type: "success" });
await renderTaskList();
} catch (e) {
Chat.addSystemMessage(_t("app.tasksFailed", { msg: e.message }));
showToast(_t("app.tasksFailed", { msg: e.message }), { type: "error" });
}
},
});
Expand Down Expand Up @@ -765,10 +782,12 @@ const Dialogs = (() => {
if (editingTask === null) await window.emrg.taskCreate(payload);
else await window.emrg.taskUpdate(payload);
Chat.addSystemMessage(_t("settings.taskSaved"));
showToast(_t("settings.taskSaved"), { type: "success" });
closeTaskForm();
await renderTaskList();
} catch (e) {
Chat.addSystemMessage(_t("app.tasksFailed", { msg: e.message }));
showToast(_t("app.tasksFailed", { msg: e.message }), { type: "error" });
}
}

Expand Down Expand Up @@ -924,11 +943,13 @@ const Dialogs = (() => {
try {
await window.emrg.taskTemplateDelete({ name: t.name });
Chat.addSystemMessage(_t("settings.templateDeleted"));
showToast(_t("settings.templateDeleted"), { type: "success" });
await loadTaskMeta(); // 刷新类型选项(任务表单下拉)
await renderTemplateList();
} catch (e) {
// 决策点②:被任务引用的类型 daemon 拒绝删除(错误信息含任务数)
Chat.addSystemMessage(_t("settings.templateDeleteFailed", { msg: e.message }));
showToast(_t("settings.templateDeleteFailed", { msg: e.message }), { type: "error" });
}
},
});
Expand Down Expand Up @@ -1000,11 +1021,13 @@ const Dialogs = (() => {
if (editingTemplate === null) await window.emrg.taskTemplateCreate({ name, prompt });
else await window.emrg.taskTemplateUpdate({ name, prompt });
Chat.addSystemMessage(_t("settings.templateSaved"));
showToast(_t("settings.templateSaved"), { type: "success" });
closeTemplateForm();
await loadTaskMeta();
await renderTemplateList();
} catch (e) {
Chat.addSystemMessage(_t("settings.templateSaveFailed", { msg: e.message }));
showToast(_t("settings.templateSaveFailed", { msg: e.message }), { type: "error" });
}
}

Expand Down
4 changes: 4 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ const I18N = (() => {
"app.tasksFailed": "加载任务失败:{msg}",
"app.triggerFailed": "触发失败:{msg}",
"app.triggered": "已触发任务 {n}。",
"app.taskRunning": "任务 {n} 正在运行中(无法重复触发)",
"app.taskRunningBadge": "运行中",
"app.deletedSwitch": "这个对话已被删除,已帮你切到最近的对话。",
"app.switched": "已切换对话。",
"app.sessionDisconnected": "该会话连接已断开,正在自动重连…",
Expand Down Expand Up @@ -818,6 +820,8 @@ const I18N = (() => {
"app.tasksFailed": "Failed to load tasks: {msg}",
"app.triggerFailed": "Trigger failed: {msg}",
"app.triggered": "Task {n} triggered.",
"app.taskRunning": "Task {n} is running (cannot re-trigger)",
"app.taskRunningBadge": "running",
"app.deletedSwitch": "This conversation was deleted — switched to the most recent one.",
"app.switched": "Conversation switched.",
"app.sessionDisconnected": "This session's connection is lost — reconnecting automatically…",
Expand Down
20 changes: 20 additions & 0 deletions emrg/gui/renderer/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,25 @@ function applyTheme(mode) {
}
}

/**
* 通用 toast(rant 2026-08-15T09:20:27:面板操作反馈全局可见——tasks/settings
* 面板视图下聊天流不可见,Chat.addSystemMessage 的反馈=看不见的"没反应")。
* 右上角短暂显示、自动消失;type: success|error|info 决定左侧色条。
*/
let _toastTimer = null;
function showToast(message, opts = {}) {
const { type = "info", durationMs = 3000 } = opts;
const toast = document.getElementById("toast");
if (!toast) return;
const msg = document.getElementById("toast-msg");
if (msg) msg.textContent = message;
// 逐个 remove(沙箱 classList.remove 单参;DOM 语义等价)
["hidden", "toast-success", "toast-error", "toast-info"].forEach((c) => toast.classList.remove(c));
toast.classList.add("toast-" + type);
if (_toastTimer) clearTimeout(_toastTimer);
_toastTimer = setTimeout(() => toast.classList.add("hidden"), durationMs);
}

/**
* 相对时间(P6 验收补完:项目行"最近活跃"提示,消费 daemon list_projects 的
* latest_session_at)。ISO 时间串 → "刚刚 / {n} 分钟前 / {n} 小时前 / {n} 天前",
Expand Down Expand Up @@ -100,3 +119,4 @@ window.genRequestId = genRequestId;
window.groupLabel = groupLabel;
window.applyTheme = applyTheme;
window.relTime = relTime;
window.showToast = showToast;
46 changes: 46 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ const ELEMENT_IDS = [
"growth-card", "growth-count", "about-recent",
"about-update", "about-update-check-btn",
"github-banner", "github-banner-msg", "github-banner-connect", "github-banner-dismiss",
"toast", "toast-msg",
// rant 18:23:15 P3:定时任务/自定义类型管理(settings 区)
"task-list", "task-add-btn", "task-template-mgr-btn",
"task-form", "task-form-name", "task-form-type", "task-form-project",
Expand Down Expand Up @@ -2660,6 +2661,51 @@ test("P3:新增任务表单 —— 间隔 <60 客户端拒绝;≥60 提交 t
assert.strictEqual(vm.runInContext('document.getElementById("task-form").classList.contains("hidden")', ctx), true, "保存后表单收起");
});

test("rant 2026-08-15T09:20:27/09:23:10:面板操作反馈走全局 toast(任意视图可见)+ Trigger 三态语义", async () => {
const triggerCalls = [];
const { ctx, els } = makeSandbox({
listTasks: async () => [
{ name: "emrg-task", type: "evolution", running: true, interval: 60 },
{ name: "nightly", type: "custom", running: false, interval: 3600 },
],
listProjects: async () => [{ name: "emrg", path: "/p/emrg" }],
triggerTask: async (payload) => {
triggerCalls.push(payload);
if (payload.name === "emrg-task") return { name: "emrg-task", result: "running", detail: "task is currently executing" };
if (payload.name === "missing") return { error: "task 'missing' not found" };
return { name: "nightly", result: "triggered", detail: "next run moved to immediately" };
},
});
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, "两个任务各一行");
// running 徽标 + 触发按钮禁用(rant 09:23:10 从源头减少误点)
const runBadges = rows[0].querySelectorAll(".task-running-badge").length;
assert.strictEqual(runBadges, 1, "running 任务应有徽标");
const btns0 = rows[0].querySelectorAll(".model-action-btn");
const btns1 = rows[1].querySelectorAll(".model-action-btn");
assert.strictEqual(btns0[0].disabled, true, "running 任务触发按钮应禁用");
assert.strictEqual(btns1[0].disabled, false, "空闲任务触发按钮可用");
// 触发空闲任务 → triggered → success toast(面板视图下 toast 可见)
btns1[0].click();
await tick();
assert.strictEqual(triggerCalls.length, 1, "触发应调用 triggerTask");
assert.strictEqual(triggerCalls[0].name, "nightly");
assert.strictEqual(els["toast"].classList.contains("toast-success"), true, "triggered → success toast");
assert.ok(String(els["toast-msg"].textContent).includes("nightly"), "toast 消息含任务名");
// /trigger 路径:running → info toast(不再假成功)
await vm.runInContext('App.doTrigger("emrg-task")', ctx);
await tick();
assert.strictEqual(els["toast"].classList.contains("toast-info"), true, "running → info toast");
assert.ok(String(els["toast-msg"].textContent).includes("emrg-task"), "info toast 含任务名");
// 触发失败 → error toast
vm.runInContext('App.doTrigger("missing")', ctx);
await tick();
assert.strictEqual(els["toast"].classList.contains("toast-error"), true, "error → error toast");
});

test("rant 2026-08-14T15:41:52:快速点击添加任务 —— 元数据未加载完也填充下拉 + 保存成功", async () => {
let created = null;
const { ctx, els } = makeSandbox({
Expand Down
Loading