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
1 change: 1 addition & 0 deletions emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@ const RESPONSE_TYPES = {
compact: "compact_result",
rewind_session: "rewind_result", // 补缺:daemon.py:955 rewind_result
read_memory: "memory_content", // 补缺:daemon.py:771/778 memory_content
evolution_summary: "evolution_summary", // WorkBuddy P3:自进化可见化
};

class DaemonClient {
Expand Down
6 changes: 6 additions & 0 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -436,6 +436,12 @@ vision = false
return { ok: true, count: frame.count ?? 0 };
});

ipcMain.handle("emrg:evolutionSummary", async (_e, { limit = 5 } = {}) => {
// GUI / 指令 P3:自进化可见化 — daemon evolution_summary(count + 最近改进)
const frame = await client.sendCommandAndWait("evolution_summary", { limit }, 5000);
return { count: frame.count ?? 0, recent: frame.recent || [] };
});

ipcMain.handle("emrg:setModel", async (_e, { model }) => {
await client.sendCommandAndWait("set_model", { model }, 5000);
return { ok: true };
Expand Down
1 change: 1 addition & 0 deletions emrg/gui/preload.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ const api = {
listTasks: () => ipcRenderer.invoke("emrg:listTasks"),
triggerTask: (payload) => ipcRenderer.invoke("emrg:triggerTask", payload),
sendRant: (payload) => ipcRenderer.invoke("emrg:sendRant", payload),
evolutionSummary: (payload) => ipcRenderer.invoke("emrg:evolutionSummary", payload),
listModels: () => ipcRenderer.invoke("emrg:listModels"),
openFile: (payload) => ipcRenderer.invoke("emrg:openFile", payload),
saveSettings: (payload) => ipcRenderer.invoke("emrg:saveSettings", payload),
Expand Down
36 changes: 36 additions & 0 deletions emrg/gui/renderer/css/components.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -954,3 +954,39 @@ dialog::backdrop {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}

/* WorkBuddy P3:设置 → 关于区 */
.about-row {
font-size: var(--fs-secondary, 13px);
color: var(--text-2);
margin-bottom: 4px;
}
.about-row b { color: var(--accent); }
.about-recent {
margin-top: 6px;
max-height: 180px;
overflow-y: auto;
}
.about-recent-title {
font-size: var(--fs-aux, 12px);
font-weight: 600;
color: var(--text-3);
margin-bottom: 4px;
}
.about-recent-item {
display: flex;
align-items: center;
gap: 8px;
font-size: var(--fs-aux, 12px);
color: var(--text-2);
padding: 4px 0;
border-bottom: 1px solid var(--border);
}
.about-recent-item:last-child { border-bottom: none; }
.about-recent-time {
font-family: var(--font-mono, monospace);
color: var(--text-3);
white-space: nowrap;
font-size: 11px;
}
}
5 changes: 5 additions & 0 deletions emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,6 +140,11 @@ <h2>设置</h2>
<div class="hint" style="margin-top:6px;">EMRG 是一个会自我进化的 AI 智能体——每次改进都会自动汇报,你可以随时在这里看到它的成长。</div>
</div>
</div>
<!-- WorkBuddy P3:最近改进列表(daemon evolution_summary) -->
<div class="settings-group">
<div class="settings-group-title">最近改进</div>
<div id="about-recent" class="about-recent"></div>
</div>
<div class="dialog-actions">
<button type="button" id="settings-cancel" class="btn btn-ghost">取消</button>
<button type="button" id="settings-save" class="btn btn-primary">保存</button>
Expand Down
34 changes: 33 additions & 1 deletion emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -734,6 +734,34 @@ const App = (() => {
if (dismiss) dismiss.addEventListener("click", hideEvolutionToast);
}

async function loadEvolutionSummary() {
const recent = $("about-recent");
if (!recent) return;
try {
const res = await window.emrg.evolutionSummary({ limit: 5 });
if (res && res.count !== undefined) {
state.evolutionCount = res.count;
updateGrowthCard();
}
const items = (res && res.recent) || [];
recent.innerHTML = "";
if (items.length === 0) {
recent.appendChild(el("div", { class: "about-recent-item" }, "还没有改进记录,输入 /rant 驱动第一次进化吧"));
return;
}
const header = el("div", { class: "about-recent-title" }, "最近改进");
recent.appendChild(header);
for (const it of items) {
const ts = String(it.timestamp || "").slice(0, 16).replace("T", " ");
const ops = (it.operations || []).join(" · ");
const row = el("div", { class: "about-recent-item" });
row.appendChild(el("span", { class: "about-recent-time" }, ts));
row.appendChild(el("span", {}, ops || "self-improvement"));
recent.appendChild(row);
}
} catch { /* 摘要加载失败静默(进化卡仍显示 count) */ }
}

function initModelSwitcher() {
const sw = $("model-switcher");
sw.addEventListener("click", async (e) => {
Expand DownExpand Up@@ -992,7 +1020,10 @@ const App = (() => {
$("send-btn").addEventListener("click", sendMessage);
$("stop-btn").addEventListener("click", () => window.emrg.cancel().catch(() => {}));
$("new-chat-btn").addEventListener("click", newSession);
$("settings-btn").addEventListener("click", Dialogs.showSettings);
$("settings-btn").addEventListener("click", () => {
loadEvolutionSummary(); // WorkBuddy P3:打开设置时加载最近改进
Dialogs.showSettings();
});
$("settings-cancel").addEventListener("click", () => $("settings-dialog").close());
$("settings-save").addEventListener("click", Dialogs.saveSettings);
$("pick-dir-btn").addEventListener("click", async () => {
Expand DownExpand Up@@ -1163,6 +1194,7 @@ const App = (() => {
maybeShowEvolutionToast, // WorkBuddy P3:进化 toast 检测
showVersionInfo, // WorkBuddy P3:/version 内容(toast "去看看" 共用)
setMode, // WorkBuddy P2:Ask/Auto 模式(导出供测试与外部调用)
loadEvolutionSummary, // WorkBuddy P3:最近改进摘要
};
})();

Expand Down
4 changes: 4 additions & 0 deletions emrg/gui/renderer/js/result-panel.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -170,3 +170,7 @@ const ResultPanel = (() => {

return { init, addToolResult, toggle, isCollapsed };
})();

// ⚠️ 必须暴露到 window:app.js 作为独立 <script> 加载,模块级 const 不跨 script 共享。
// 缺失会导致真实 GUI 打开即 ReferenceError(测试沙箱因共享 vm context 掩盖此问题)。
window.ResultPanel = ResultPanel;
50 changes: 49 additions & 1 deletion emrg/gui/test/app-commands.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,15 @@ function makeEl(id) {
_listeners: {},
addEventListener(t, fn) { this._listeners[t] = fn; },
click() { const fn = this._listeners.click; if (fn) fn(); },
appendChild() {},
appendChild(child) {
this.children.push(child);
// 近似真实 DOM:优先文本,其次子元素 innerHTML(嵌套渲染)
let frag = "";
if (child && child.text !== undefined) frag = String(child.text);
else if (child && typeof child.textContent === "string" && child.textContent.length > 0) frag = child.textContent;
else if (child && typeof child.innerHTML === "string" && child.innerHTML.length > 0) frag = child.innerHTML;
if (frag) this.innerHTML += frag;
},
removeChild() {},
querySelectorAll: () => [],
getBoundingClientRect: () => ({ left: 0, right: 100, top: 0, bottom: 100, width: 100, height: 100 }),
Expand DownExpand Up@@ -324,3 +332,43 @@ test("P3:toast '去看看' 关闭并输出版本信息", async () => {
if (see.click) see.click();
assert.ok(els["evolution-toast"].classList.contains("hidden"), "点击后 toast 应隐藏");
});

test("P3:updateGrowthCard 更新进化计数(兼容 #501 growth-count / about-evolutions id)", async () => {
const { ctx, els } = makeSandbox({});
await tick();
vm.runInContext("App.state.evolutionCount = 42; App.updateGrowthCard();", ctx);
// #501 的 id(growth-count / about-evolutions)存在时更新
assert.ok(els["growth-count"] === undefined || String(els["growth-count"].textContent) === "42", "growth-count 应更新为 42");
assert.ok(els["about-evolutions"] === undefined || els["about-evolutions"].innerHTML.includes("42"), "about-evolutions 应显示 42");
});

test("P3:loadEvolutionSummary 拉取最近改进并渲染(关于区列表)", async () => {
const { ctx, els } = makeSandbox({
evolutionSummary: async () => ({
count: 7,
recent: [
{ timestamp: "2026-08-06T20:00:00", operations: ["llm-reflection", "tool-execution"], impact: [] },
{ timestamp: "2026-08-06T19:00:00", operations: ["self-improvement"], impact: [] },
],
}),
});
await tick();
await vm.runInContext("App.loadEvolutionSummary()", ctx);
assert.ok(els["about-recent"].innerHTML.includes("最近改进"), "应渲染最近改进标题");
assert.ok(els["about-recent"].innerHTML.includes("llm-reflection"), "应显示操作摘要");
assert.ok(els["about-recent"].innerHTML.includes("20:00"), "应显示时间戳");
});

test("P3:loadEvolutionSummary 空记录显示引导文案", async () => {
const { ctx, els } = makeSandbox({
evolutionSummary: async () => ({ count: 0, recent: [] }),
});
await tick();
await vm.runInContext("App.loadEvolutionSummary()", ctx);
assert.ok(els["about-recent"].innerHTML.includes("/rant"), "空状态应提示 /rant 驱动第一次进化");
});

test("P1 回归:result-panel.js 暴露 window.ResultPanel(真实 GUI 防 ReferenceError)", async () => {
const src = fs.readFileSync(path.join(RENDERER_JS, "result-panel.js"), "utf8");
assert.ok(src.includes("window.ResultPanel"), "result-panel.js 必须暴露 window.ResultPanel(app.js 独立 script 加载需要)");
});
1 change: 1 addition & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -78,6 +78,7 @@ const ELEMENT_IDS = [
"rant-dialog", "rant-message", "rant-project", "rant-cancel", "rant-submit",
"tasks-dialog", "tasks-list", "tasks-close",
"result-panel", "result-list", "result-toggle",
"growth-card", "growth-count", "about-recent",
];

/** 构造浏览器沙箱(win 即全局对象) */
Expand Down
35 changes: 35 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -871,6 +871,41 @@ async def _process_message(
elif msg_type == "list_projects":
await self._handle_list_projects(ws)

elif msg_type == "evolution_summary":
# WorkBuddy P3 (rant 21:35): self-evolution visibility.
# Low-cost: read evolution log files (~/.emrg/logs/evolution-*.json)
# written by EvolutionHandler; return count + recent N summaries.
limit = msg.get("limit", 5)
try:
logs_dir = config_dir() / "logs"
files = sorted(
logs_dir.glob("evolution-*.json"),
key=lambda p: p.name,
reverse=True,
)[: max(1, min(int(limit), 20))]
recent = []
for f in files:
try:
data = json.loads(f.read_text(encoding="utf-8"))
recent.append({
"timestamp": data.get("timestamp", ""),
"impact": data.get("impact", []),
"operations": data.get("operations", []),
})
except (json.JSONDecodeError, OSError):
continue
await self._send(ws, {
"type": "evolution_summary",
"count": len(self.evolutions),
"recent": recent,
})
except OSError:
await self._send(ws, {
"type": "evolution_summary",
"count": len(self.evolutions),
"recent": [],
})

elif msg_type == "clear_session":
session_id = msg.get("session_id", "")
cwd = msg.get("cwd", "")
Expand Down
Loading