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 numberDiff line numberDiff 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` (198: 43 daemon_client + 19 conn-manager + 22 app-commands + 77 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (200: 43 daemon_client + 19 conn-manager + 22 app-commands + 79 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); 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 DownExpand Up@@ -113,7 +113,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (694) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (198: 43 daemon_client + 19 conn-manager + 22 app-commands + 77 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (200: 43 daemon_client + 19 conn-manager + 22 app-commands + 79 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` + 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
2 changes: 1 addition & 1 deletion README.cn.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -147,7 +147,7 @@ EMRG 不只是追赶——它自己追上来。

贡献指南、源码安装、架构、详细 FAQ → [DEVELOPMENT.md](DEVELOPMENT.md)。

快速检查:`uv run pytest tests/ -v`(当前 694 项)· `cd emrg/gui && npm test`(198 项:43 daemon_client + 19 conn-manager + 22 app-commands + 77 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state)
快速检查:`uv run pytest tests/ -v`(当前 694 项)· `cd emrg/gui && npm test`(200 项:43 daemon_client + 19 conn-manager + 22 app-commands + 79 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state)

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -147,7 +147,7 @@ They're products. EMRG is an experiment in *closing the loop* — the AI improve

Contributing, source installs, architecture, and the full FAQ → [DEVELOPMENT.md](DEVELOPMENT.md).

Quick checks: `uv run pytest tests/ -v` (currently 694 items) · `cd emrg/gui && npm test` (198: 43 daemon_client + 19 conn-manager + 22 app-commands + 77 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state)
Quick checks: `uv run pytest tests/ -v` (currently 694 items) · `cd emrg/gui && npm test` (200: 43 daemon_client + 19 conn-manager + 22 app-commands + 79 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state)

---

Expand Down
24 changes: 19 additions & 5 deletions emrg/gui/renderer/js/result-panel.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,12 +250,13 @@ const ResultPanel = (() => {
vp.appendChild(pre);
}

/** 切换会话 → Tab 状态按 sid 隔离(缺口 5) */
/** 切换会话 → Tab/产物状态按 sid 隔离(缺口 5) */
function switchSession(sid) {
currentSid = sid || null;
stateFor(currentSid);
renderTabbar();
activateTab(stateFor(currentSid).active);
renderArtifacts(); // 产物 pane 按当前会话桶恢复(后台 tool_finished 只入桶不渲染)
}

// ── 产物登记(P1 卡片渲染保留;P3.2 改 write/edit 文件登记) ──
Expand All@@ -270,14 +271,27 @@ const ResultPanel = (() => {
/** 登记一个产物条目(tool_finished 事件;sid = 事件桥会话,P3.2 消费) */
function addToolResult(data, sid) {
const arr = artifactsFor(sid || currentSid);
arr.unshift({
const record = {
tool_name: data.tool_name || "tool",
content: String(data.content || ""),
error: !!data.error,
elapsed: data.elapsed,
});
};
arr.unshift(record);
if (arr.length > 100) arr.pop();
renderCard(data);
// 后台会话:只入桶不渲染(防污染激活会话产物 pane;切回时由 renderArtifacts 恢复)
if ((sid || null) !== currentSid) return;
renderCard(record);
}

/** 按当前会话桶重渲染产物 pane(switchSession/init 时从桶恢复 DOM,镜像 renderTabbar 模式) */
function renderArtifacts() {
const list = listEl();
if (!list) return;
list.innerHTML = "";
const arr = artifactsFor(currentSid);
if (arr.length === 0) { renderEmpty(); return; }
for (const rec of arr.slice(0, MAX_ITEMS)) renderCard(rec);
}

function renderCard(data) {
Expand DownExpand Up@@ -410,7 +424,7 @@ const ResultPanel = (() => {
window.addEventListener("resize", updateResizerPos);
renderTabbar();
activateTab(stateFor(currentSid).active);
renderEmpty();
renderArtifacts();
}

return { init, addToolResult, toggle, isCollapsed, switchSession, openFileTab, closeFileTab, activateTab, getWidth, setWidth };
Expand Down
39 changes: 39 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1036,6 +1036,45 @@ test("P2 框架:per-session Tab 状态隔离(切会话各显各的)", asyn
assert.ok(els["result-tabbar"].children[0].classList.contains("active"), "s1 的激活 Tab 应保留");
});

test("P2 框架:后台会话 tool_finished 只入桶不渲染(防污染激活 pane)", async () => {
const { ctx } = makeSandbox();
await tick();
await vm.runInContext('ResultPanel.switchSession("s1")', ctx);
// 后台会话 s2 的 tool_finished → 只入桶不渲染
await vm.runInContext('ResultPanel.addToolResult({ tool_name: "write", content: "Wrote file: /bg.txt", elapsed: 0.2 }, "s2")', ctx);
assert.strictEqual(vm.runInContext('document.getElementById("result-list").children.length', ctx), 0, "后台会话产物不得渲染到激活 pane");
// 激活会话 s1 的 tool_finished → 渲染
await vm.runInContext('ResultPanel.addToolResult({ tool_name: "write", content: "Wrote file: /active.txt", elapsed: 0.3 }, "s1")', ctx);
assert.strictEqual(vm.runInContext('document.getElementById("result-list").children.length', ctx), 1, "激活会话产物应渲染");
});

test("P2 框架:switchSession 按 sid 重渲染产物 pane(桶→DOM 恢复)", async () => {
const { ctx } = makeSandbox();
await tick();
// 两会话各自登记(初始非激活 → 只入桶)
await vm.runInContext('ResultPanel.addToolResult({ tool_name: "bash", content: "s1-out", elapsed: 0.1 }, "s1")', ctx);
await vm.runInContext('ResultPanel.addToolResult({ tool_name: "bash", content: "s2-out", elapsed: 0.2 }, "s2")', ctx);
// 递归收集 list 文本(mock 元素 textContent 是属性,不入 innerHTML)
const collectText = `(function(){
function collect(node) {
let out = node.textContent || "";
for (const c of node.children || []) out += collect(c);
return out;
}
return collect(document.getElementById("result-list"));
})()`;
// 切到 s2 → pane 只显示 s2 的记录
await vm.runInContext('ResultPanel.switchSession("s2")', ctx);
const text2 = vm.runInContext(collectText, ctx);
assert.ok(String(text2).includes("s2-out"), "s2 pane 应显示 s2 产物");
assert.ok(!String(text2).includes("s1-out"), "s2 pane 不得显示 s1 产物");
// 切回 s1 → pane 只显示 s1 的记录(s2 卡片不残留)
await vm.runInContext('ResultPanel.switchSession("s1")', ctx);
const text1 = vm.runInContext(collectText, ctx);
assert.ok(String(text1).includes("s1-out"), "s1 pane 应显示 s1 产物");
assert.ok(!String(text1).includes("s2-out"), "s1 pane 不得显示 s2 产物");
});

test("P3:文件树渲染根 + 根自动展开懒加载", async () => {
const { ctx, els } = makeSandbox();
await tick();
Expand Down
Loading