diff --git a/Agent.md b/Agent.md index 158a3d9a..d76c3bce 100644 --- a/Agent.md +++ b/Agent.md @@ -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) @@ -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 diff --git a/README.en.md b/README.en.md index b782a7c6..9721964a 100644 --- a/README.en.md +++ b/README.en.md @@ -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`). diff --git a/README.md b/README.md index d00307f8..be587b6b 100644 --- a/README.md +++ b/README.md @@ -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`)。 diff --git a/emrg/gui/renderer/css/components.css b/emrg/gui/renderer/css/components.css index f6496fcd..80b4fd67 100644 --- a/emrg/gui/renderer/css/components.css +++ b/emrg/gui/renderer/css/components.css @@ -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); diff --git a/emrg/gui/renderer/js/chat.js b/emrg/gui/renderer/js/chat.js index 865f974e..ba76b447 100644 --- a/emrg/gui/renderer/js/chat.js +++ b/emrg/gui/renderer/js/chat.js @@ -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); diff --git a/emrg/gui/renderer/js/markdown.js b/emrg/gui/renderer/js/markdown.js index f8c84c2c..d02db31a 100644 --- a/emrg/gui/renderer/js/markdown.js +++ b/emrg/gui/renderer/js/markdown.js @@ -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 = ""; @@ -26,10 +27,11 @@ function codeRenderer(code, infostring, escaped) { } } const cls = `hljs language-${escapeHtml(lang || "plaintext")}`; - if (highlighted) { - return `
${highlighted}`;
- }
- return `${escaped ? code : escapeHtml(code)}`;
+ const codeHtml = highlighted
+ ? `${highlighted}`
+ : `${escaped ? code : escapeHtml(code)}`;
+ // 复制按钮:事件委托在 chat-view(CSP 禁内联 handler);code 文本经 escapeHtml 防注入
+ return `${codeHtml}