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` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 16 i18n + 7 integration + 3 commands + 7 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (257: 45 daemon_client + 19 conn-manager + 22 app-commands + 129 renderer smoke + 16 i18n + 7 integration + 3 commands + 7 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` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 16 i18n + 7 integration + 3 commands + 7 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` (257: 45 daemon_client + 19 conn-manager + 22 app-commands + 129 renderer smoke + 16 i18n + 7 integration + 3 commands + 7 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
10 changes: 10 additions & 0 deletions emrg/gui/renderer/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,16 @@ dialog::backdrop {
color: var(--text-3);
word-break: break-all;
}
/* rant 10:41:43:rant 详情 markdown —— 【】标题预处理后的 h4 层次 + 嵌套列表缩进可见 */
.rant-md h4 {
margin: 10px 0 4px;
font-size: 13px;
color: var(--text-1);
}
.rant-md li > ul,
.rant-md li > ol {
margin-left: 1em;
}
.task-actions {
display: flex;
gap: 4px;
Expand Down
27 changes: 23 additions & 4 deletions emrg/gui/renderer/js/dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1305,16 +1305,20 @@ const Dialogs = (() => {
const msg = r.message || "";
const firstLine = (msg.split("\n").find((l) => l.trim() !== "") || "").replace(/^#{1,6}\s+/, "").replace(/^[>*\-\s]+/, "");
row.appendChild(el("span", { class: "rant-col-content" }, firstLine || "—"));
// 详情展开(rant 21:36:01:完整 message 按 markdown 渲染 + 进度显示
// 详情展开/收起(rant 10:41:43:点击已展开的本行 → 收起,不再无条件重建
row.addEventListener("click", async () => {
const detail = list.querySelector(".rant-detail");
if (detail) detail.remove();
const existing = row.nextElementSibling;
const isSelfDetail = existing && existing.classList.contains("rant-detail");
// 先移除所有已展开的详情(含其他行的)
list.querySelectorAll(".rant-detail").forEach((d) => d.remove());
if (isSelfDetail) return; // 点自己已展开 → 收起
const detailRow = el("div", { class: "rant-detail", style: "padding:6px 8px;border-top:1px solid var(--border);font-size:var(--fs-secondary);" });
detailRow.appendChild(el("div", { class: "rant-meta" }, `${ts || "—"} · ${r.project || "—"} · ${stText}`));
const md = el("div", { class: "msg-body rant-md" });
try {
if (window.emrgMarkdown && window.emrgMarkdown.renderMarkdown) {
md.innerHTML = await window.emrgMarkdown.renderMarkdown(msg);
// rant 10:41:43:【】标题预处理(markdown 视觉层次;不破坏原文)
md.innerHTML = await window.emrgMarkdown.renderMarkdown(preprocessRantMarkdown(msg));
} else {
md.textContent = msg;
}
Expand All @@ -1333,6 +1337,20 @@ const Dialogs = (() => {
}
}

// rant 10:41:43:rant 详情 markdown 预处理 —— 行首 【xxx】 段标记映射为标题级
// (【任务】→####),让整篇有视觉层次;仅短行(≤60 字符)转标题,长内容行保持正文。
// 原文保留(只加 #### 前缀,不删除/改写原文)。
function preprocessRantMarkdown(text) {
if (!text) return text || "";
return String(text).split("\n").map((line) => {
const t = line.trim();
if (/^【[^】]{1,24}】/.test(t) && t.length <= 60) {
return `#### ${t}`;
}
return line;
}).join("\n");
}

function openRantForm() {
const form = $("rant-form");
if (!form) return;
Expand Down Expand Up @@ -1564,6 +1582,7 @@ const Dialogs = (() => {
showProjectSessionsInPanel, // rant 14:10:14 P5:项目面板内嵌会话列表(测试复用)
initRantPanel, // rant 14:10:14 P4:rant 面板初始化
renderRantList, // rant 14:10:14 P4:rant 列表渲染(测试/刷新复用)
preprocessRantMarkdown, // rant 10:41:43:rant 详情 markdown 【】标题预处理(测试复用)
setRantFilter, // rant 14:10:14 P4:rant 状态筛选(测试复用)
openRantForm, // rant 14:10:14 P4:新建 rant 表单(测试复用)
submitRantForm, // rant 14:10:14 P4:rant 表单提交(测试复用)
Expand Down
57 changes: 57 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ function makeEl(id) {
if (i >= 0) this.parentNode.children.splice(i + 1, 0, c);
else this.parentNode.children.push(c);
},
get nextElementSibling() {
// 忠实 DOM:下一兄弟节点(rant 10:41:43 点击收起依赖 row.nextElementSibling)
if (!this.parentNode) return null;
const i = this.parentNode.children.indexOf(this);
if (i < 0) return null;
return this.parentNode.children[i + 1] || null;
},
remove() {
// 真实脱离父节点(chat.js handleToolEnd 移除 .tool-spinner)
if (this.parentNode) {
Expand Down Expand Up @@ -3270,6 +3277,56 @@ test("rant 14:10:14 P4:rant 面板列表(5 列 + 状态徽标三态 + 筛选
assert.strictEqual(sent.message, "希望支持 X");
});

test("rant 10:41:43:preprocessRantMarkdown —— 【】段标记映射标题 + 原文保留 + 长行不转", () => {
const { ctx } = makeSandbox();
const pp = (s) => vm.runInContext(`EMRG_Dialogs.preprocessRantMarkdown(${JSON.stringify(s)})`, ctx);
// 短行 【xxx】 → #### 标题(原文保留,仅加前缀)
const out1 = pp("【任务】\n宿主要求:显示任务状态。\n【修改方案】\n加倒计时。");
assert.ok(out1.includes("#### 【任务】"), `【任务】应转 h4 标题:${out1}`);
assert.ok(out1.includes("#### 【修改方案】"), `【修改方案】应转 h4:${out1}`);
assert.ok(out1.includes("宿主要求:显示任务状态。"), "正文行保持原文");
// 长行(>60 字符,如带完整设计的 rant 标题)→ 不转标题,保持正文
const longLine = "【GUI 任务管理:显示任务当前状态 + 下次运行倒计时——完整设计如下,照抄实施,勿需再读文档】";
const out2 = pp(longLine);
assert.strictEqual(out2, longLine, "长行不应转标题");
// 空/非字符串安全
assert.strictEqual(pp(""), "", "空串安全");
assert.strictEqual(pp(null), "", "null 安全");
});

test("rant 10:41:43:rant 详情点击收起 —— 再点已展开行 → 收起;点其他行 → 换展开", async () => {
const rants = [
{ timestamp: "2026-08-15T10:00:00", project: "emrg", status: "pending", progress: null, message: "【任务】\n第一条" },
{ timestamp: "2026-08-15T11:00:00", project: "emrg", status: "pending", progress: null, message: "【任务】\n第二条" },
];
const { ctx, els } = makeSandbox({
listRants: async () => rants,
listProjects: async () => [],
});
await tick();
await vm.runInContext("App.openRantsPanel()", ctx);
await tick();
const list = els["rant-list"];
const rows = list.children.filter((c) => c.className.includes("task-row"));
assert.strictEqual(rows.length, 2);
// 点行0 → 展开
rows[0].click();
await tick();
assert.ok(list.querySelector(".rant-detail"), "点行0应展开详情");
// 再点行0 → 收起(修复前:删+建 → 永远展开)
rows[0].click();
await tick();
assert.strictEqual(list.querySelector(".rant-detail"), null, "再点已展开行应收起");
// 点行0 → 展开;点行1 → 旧的收起、行1 展开(行为保持)
rows[0].click();
await tick();
assert.ok(list.querySelector(".rant-detail"), "行0展开");
rows[1].click();
await tick();
const details = list.children.filter((c) => c.className.includes("rant-detail"));
assert.strictEqual(details.length, 1, "切换查看不同 rant → 旧的收起新的展开(仅 1 个详情)");
});

test("rant 21:36:01/21:38:25/21:46:53:三面板标题 + Rant 列头 + 项目 hint 移除 + i18n.apply 保留控件", async () => {
const htmlSrc = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf-8");
const i18nSrc = fs.readFileSync(path.join(__dirname, "..", "renderer", "js", "i18n.js"), "utf-8");
Expand Down
Loading