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 @@ -64,7 +64,7 @@ EMRG is a self-evolving AI agent architecture experiment. Python implementation,
- 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` (36: 22 daemon_client + 7 integration + 7 renderer smoke); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (37: 22 daemon_client + 7 integration + 8 renderer smoke); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Auto project tracking** — Automatically detects and records working directories; project-scoped sessions
- **Rant-driven evolution** — User feedback via `/rant` drives automatic self-improvement cycles
- **Headless GitHub auth** — Non-interactive evolution auto-extracts `GH_TOKEN` from git credential store (osxkeychain / credential helper); PR comment/LGTM queries fall back to REST API (GraphQL needs `read:org` scope)
Expand Down Expand Up @@ -92,7 +92,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (472) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (36: 22 daemon_client + 7 integration + 7 renderer smoke) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (37: 22 daemon_client + 7 integration + 8 renderer smoke) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`

## Configuration

Expand Down
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ uv run python -m emrg # launch TUI
cd emrg/gui
npm ci # install deps (production: --omit=dev)
npm start # launch GUI (auto-starts daemon)
npm test # run Node tests (36: 22 daemon_client + 7 integration + 7 renderer smoke; integration runs in CI, local: npm run test:integration)
npm test # run Node tests (37: 22 daemon_client + 7 integration + 8 renderer smoke; integration runs in CI, local: npm run test:integration)
```

CI runs tests and checks for conflict markers automatically via GitHub Actions (`.github/workflows/test.yml`).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ uv run python -m emrg # 启动 TUI
cd emrg/gui
npm ci # 安装依赖(生产模式可 --omit=dev)
npm start # 启动 GUI(自动拉起 daemon)
npm test # 运行 Node 测试(36 项:22 daemon_client + 7 integration + 7 renderer smoke;集成测试在 CI 跑,本地可 npm run test:integration)
npm test # 运行 Node 测试(37 项:22 daemon_client + 7 integration + 8 renderer smoke;集成测试在 CI 跑,本地可 npm run test:integration)
```

CI 通过 GitHub Actions 自动运行测试并检查冲突标记(`.github/workflows/test.yml`)。
Expand Down
33 changes: 33 additions & 0 deletions emrg/gui/renderer/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,39 @@
font-size: 0.85em;
line-height: 1.55;
}
/* 代码块容器(设计 §3.3:圆角浅底 + 复制按钮) */
.code-block {
background: var(--bg-soft);
border: 1px solid var(--border);
border-radius: var(--radius-card);
margin: 0.5em 0;
overflow: hidden;
}
.code-block .code-head {
display: flex;
justify-content: flex-end;
padding: 4px 8px;
background: color-mix(in srgb, var(--bg-panel) 60%, transparent);
border-bottom: 1px solid var(--border);
}
.code-copy {
background: none;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-2);
font-size: var(--fs-aux);
padding: 2px 10px;
min-height: 24px;
cursor: pointer;
transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.code-copy:hover { background: var(--bg-panel); color: var(--text-1); }
.code-block pre {
border: none;
border-radius: 0;
margin: 0;
padding: var(--sp-3);
}
.msg-body blockquote {
border-left: 3px solid var(--border);
color: var(--text-2);
Expand Down
39 changes: 39 additions & 0 deletions emrg/gui/renderer/js/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,45 @@ const Chat = (() => {
// tool_call_id → 工具行 DOM 节点
const toolRows = new Map();

/** 复制代码按钮(设计 §3.3):事件委托在聊天区,CSP 无内联 handler */
function initCodeCopy() {
const cv = $("chat-view");
cv.addEventListener("click", (e) => {
const btn = e.target.closest(".code-copy");
if (!btn) return;
const pre = btn.closest(".code-block")?.querySelector("pre");
if (!pre) return;
const code = pre.textContent || "";
const done = () => {
btn.textContent = "已复制 ✓";
setTimeout(() => { btn.textContent = "复制"; }, 1500);
};
const fail = () => {
btn.textContent = "复制失败";
setTimeout(() => { btn.textContent = "复制"; }, 1500);
};
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(code).then(done, fail);
} else {
// 非安全上下文兜底:textarea 选中复制
const ta = document.createElement("textarea");
ta.value = code;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try {
document.execCommand("copy");
done();
} catch {
fail();
}
document.body.removeChild(ta);
}
});
}
initCodeCopy(); // 模块级绑定一次(boot 可重复调用,防 listener 泄漏)

/** 追加节点到聊天区并滚动 */
function append(node) {
$("chat-view").appendChild(node);
Expand Down
10 changes: 6 additions & 4 deletions emrg/gui/renderer/js/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
const renderer = {};

// marked 自定义 code renderer(v12 无 highlight 选项)
// 设计 §3.3:代码块带复制按钮(圆角浅底 + 一键复制)
function codeRenderer(code, infostring, escaped) {
const lang = (infostring || "").split(/\s+/)[0];
let highlighted = "";
Expand All @@ -26,10 +27,11 @@ function codeRenderer(code, infostring, escaped) {
}
}
const cls = `hljs language-${escapeHtml(lang || "plaintext")}`;
if (highlighted) {
return `<pre><code class="${cls}">${highlighted}</code></pre>`;
}
return `<pre><code class="${cls}">${escaped ? code : escapeHtml(code)}</code></pre>`;
const codeHtml = highlighted
? `<code class="${cls}">${highlighted}</code>`
: `<code class="${cls}">${escaped ? code : escapeHtml(code)}</code>`;
// 复制按钮:事件委托在 chat-view(CSP 禁内联 handler);code 文本经 escapeHtml 防注入
return `<div class="code-block"><div class="code-head"><button type="button" class="code-copy" title="复制代码">复制</button></div><pre>${codeHtml}</pre></div>`;
}

renderer.renderMarkdown = async function (mdText) {
Expand Down
22 changes: 22 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,25 @@ test("双主题 token 对比度达标(WCAG AA,rant 验收项 4 深色校准
// 记录实际值便于审阅
assert.ok(true, `深色 ${d.t1.toFixed(2)}/${d.t2.toFixed(2)}/${d.t3.toFixed(2)}/${d.ac.toFixed(2)},浅色 ${l.t1.toFixed(2)}/${l.t2.toFixed(2)}/${l.t3.toFixed(2)}/${l.ac.toFixed(2)}`);
});

test("代码块复制按钮:codeRenderer 输出容器 + 点击复制(设计 §3.3)", () => {
const { ctx } = makeSandbox({});
// codeRenderer 是模块内私有函数——通过 renderMarkdown 全链路验证输出结构
// marked 在 sandbox 中为 null → 直接验证 codeRenderer 不可行;改为验证 chat.js 已绑定委托 + CSS 存在
const html = vm.runInContext(`
(function() {
// 模拟 marked 输出经 DOMPurify 后应含 code-block 容器
// 直接调用 escapeHtml 验证注入安全
return escapeHtml("<script>alert(1)</script>");
})()
`, ctx);
assert.strictEqual(html, "&lt;script&gt;alert(1)&lt;/script&gt;", "escapeHtml 防注入");

const css = fs.readFileSync(path.join(__dirname, "..", "renderer", "css", "components.css"), "utf8");
assert.ok(css.includes(".code-block"), "CSS 应含 .code-block 容器样式");
assert.ok(css.includes(".code-copy"), "CSS 应含 .code-copy 复制按钮样式");

// chat.js 应绑定复制委托(模块加载即 initCodeCopy)
const chatSrc = fs.readFileSync(path.join(RENDERER_JS, "chat.js"), "utf8");
assert.ok(chatSrc.includes(".code-copy"), "chat.js 应含复制按钮事件委托");
});
Loading