diff --git a/emrg/gui/main.js b/emrg/gui/main.js
index fe6ad29e..9e23e86b 100644
--- a/emrg/gui/main.js
+++ b/emrg/gui/main.js
@@ -12,6 +12,7 @@ const path = require("path");
const { spawn } = require("child_process");
const { parse: parseToml, stringify: stringifyToml } = require("smol-toml");
const { DaemonClient, generateSessionId, SESSION_ID_RE, PORT_FILE } = require("./daemon_client");
+const APP_VERSION = require("./package.json").version;
// ── 单实例锁(G85/G120:第二个实例退出并 focus 已有窗口)──
if (!app.requestSingleInstanceLock()) {
@@ -226,11 +227,11 @@ vision = false
if (!configExists) {
// config 缺失 → 不拉起 daemon(daemon 启动即崩),直接返回缺配置
- return { config_exists: false, api_key_configured: false, project_dir: projectDir, project_dir_valid: projectDirValid, server_id: "", model: "" };
+ return { config_exists: false, api_key_configured: false, project_dir: projectDir, project_dir_valid: projectDirValid, server_id: "", model: "", version: APP_VERSION };
}
const keyConfigured = isKeyConfigured(cfg.llm?.api_key);
if (!keyConfigured) {
- return { config_exists: true, api_key_configured: false, project_dir: projectDir, project_dir_valid: projectDirValid, server_id: "", model: "" };
+ return { config_exists: true, api_key_configured: false, project_dir: projectDir, project_dir_valid: projectDirValid, server_id: "", model: "", version: APP_VERSION };
}
await ensureConnected();
@@ -244,6 +245,7 @@ vision = false
server_id: pong?.identity?.instance_id || "",
model: pong?.model || "",
evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong)
+ version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7)
sessions,
};
});
diff --git a/emrg/gui/renderer/css/components.css b/emrg/gui/renderer/css/components.css
index 21dbd4fe..bfa7c3c7 100644
--- a/emrg/gui/renderer/css/components.css
+++ b/emrg/gui/renderer/css/components.css
@@ -918,3 +918,39 @@ dialog::backdrop {
.mode-btn:hover:not(.active) {
color: var(--text-1);
}
+
+/* ── 进化完成 toast(WorkBuddy P3)────────────────── */
+.evolution-toast {
+ position: fixed;
+ right: 20px;
+ bottom: 20px;
+ z-index: 1000;
+ width: 320px;
+ max-width: calc(100vw - 40px);
+ padding: var(--sp-4);
+ background: var(--bg-panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-card);
+ box-shadow: var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.18));
+ animation: toast-in 0.25s var(--ease);
+}
+.evolution-toast-title {
+ font-size: var(--fs-title, 15px);
+ font-weight: 600;
+ color: var(--text-1);
+}
+.evolution-toast-msg {
+ margin-top: var(--sp-1);
+ font-size: var(--fs-secondary);
+ color: var(--text-2);
+}
+.evolution-toast-actions {
+ display: flex;
+ gap: var(--sp-2);
+ margin-top: var(--sp-3);
+ justify-content: flex-end;
+}
+@keyframes toast-in {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
diff --git a/emrg/gui/renderer/css/layout.css b/emrg/gui/renderer/css/layout.css
index 2e10d94e..6346ad37 100644
--- a/emrg/gui/renderer/css/layout.css
+++ b/emrg/gui/renderer/css/layout.css
@@ -35,6 +35,28 @@
font-size: 18px;
}
+/* 成长状态卡(WorkBuddy P3:自进化可见化) */
+.growth-card {
+ margin: 0 var(--sp-3) var(--sp-3);
+ padding: var(--sp-2) var(--sp-3);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-card);
+ background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 8%, transparent), transparent);
+}
+.growth-line {
+ font-size: var(--fs-secondary);
+ font-weight: 600;
+ color: var(--text-1);
+}
+.growth-line b {
+ color: var(--accent);
+}
+.growth-note {
+ margin-top: 2px;
+ font-size: var(--fs-aux);
+ color: var(--text-3);
+}
+
.new-chat-btn {
margin: 0 var(--sp-3) var(--sp-3);
display: flex;
diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html
index 02f6e72e..f1279865 100644
--- a/emrg/gui/renderer/index.html
+++ b/emrg/gui/renderer/index.html
@@ -19,6 +19,11 @@
✦
EMRG
+
+
+
🌱 已自我进化 0 次
+
边工作边学习,越用越懂你
+
+
+
关于
+
+
EMRG v0.2.8 · 🌱 已自我进化 0 次
+
EMRG 是一个会自我进化的 AI 智能体——每次改进都会自动汇报,你可以随时在这里看到它的成长。
+
+
@@ -287,6 +299,16 @@
后台任务
+
+
+
🌱 EMRG 刚刚完成一次自我进化!
+
它学会了新的东西,变得更好用了。
+
+
+
+
+
+
diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js
index cc2ee686..822aa12c 100644
--- a/emrg/gui/renderer/js/app.js
+++ b/emrg/gui/renderer/js/app.js
@@ -21,7 +21,10 @@ const App = (() => {
projectDir: "",
model: "",
serverId: "",
+ version: "", // WorkBuddy P3:init 返回的 package.json 版本号(/version 不再硬编码)
evolutionCount: null,
+ // WorkBuddy P3:上次已知进化计数(用于检测"进化刚发生"→toast)
+ lastKnownEvolutionCount: null,
autoScroll: true,
// GUI / 指令补全菜单(rant 19:44 P1):items=[{cmd,hint,phase}] index=当前高亮
cmdMenu: { items: [], index: -1 },
@@ -38,9 +41,12 @@ const App = (() => {
state.projectDir = init.project_dir || "";
state.serverId = init.server_id || "";
state.model = init.model || "";
+ state.version = init.version || "";
state.evolutionCount = init.evolution_count ?? null;
+ state.lastKnownEvolutionCount = state.evolutionCount;
updateConnectionDot(init.config_exists && init.api_key_configured ? "green" : "gray");
updateModelSwitcher();
+ updateGrowthCard();
if (!init.config_exists) {
Dialogs.showWelcome(); // 首启引导
@@ -143,9 +149,7 @@ const App = (() => {
Chat.addSystemMessage("已压缩当前对话历史。");
break;
case "/version":
- Chat.addSystemMessage(
- `EMRG GUI v0.2.7 · 实例 ${state.serverId || "未知"} · 模型 ${state.model || "未知"} · 已进化 ${state.evolutionCount ?? 0} 次`
- );
+ showVersionInfo();
break;
case "/help":
showHelpDialog();
@@ -669,6 +673,67 @@ const App = (() => {
if (label) label.textContent = state.model || "选择模型";
}
+ // ── 自进化可见化(WorkBuddy P3)──────────────────
+ /** 版本信息(/version 命令 + 进化 toast "去看看" 共用) */
+ function showVersionInfo() {
+ Chat.addSystemMessage(
+ `EMRG GUI v${state.version || "0.2.8"} · 实例 ${state.serverId || "未知"} · 模型 ${state.model || "未知"} · 已进化 ${state.evolutionCount ?? 0} 次`
+ );
+ }
+
+ /** 更新侧边栏成长状态卡(计数 + 提示语) */
+ function updateGrowthCard() {
+ const n = state.evolutionCount ?? 0;
+ const countEl = $("growth-count");
+ if (countEl) countEl.textContent = String(n);
+ const noteEl = $("growth-note");
+ if (noteEl) noteEl.textContent = EMRG_Copy.COPY.growthNote;
+ }
+
+ /** 进化完成 toast:检测 evolution_count 增长,一天最多提示一次 */
+ function maybeShowEvolutionToast() {
+ if (state.evolutionCount == null) return;
+ const prev = state.lastKnownEvolutionCount;
+ state.lastKnownEvolutionCount = state.evolutionCount;
+ if (prev == null || state.evolutionCount <= prev) return; // 首次连接/无增长不提示
+ // 频率控制:一天最多 1 次(localStorage 可能不可用 → 静默跳过)
+ const today = new Date().toISOString().slice(0, 10);
+ try {
+ if (localStorage.getItem("emrg.evoToast.date") === today) return;
+ localStorage.setItem("emrg.evoToast.date", today);
+ } catch { /* ignore */ }
+ showEvolutionToast();
+ }
+
+ function showEvolutionToast() {
+ const toast = $("evolution-toast");
+ if (!toast) return;
+ const msg = $("evolution-toast-msg");
+ if (msg) msg.textContent = EMRG_Copy.COPY.evolutionToastMsg(state.evolutionCount ?? 0);
+ toast.classList.remove("hidden");
+ const star = $("brand-star");
+ if (star) star.classList.add("pulse");
+ }
+
+ function hideEvolutionToast() {
+ const toast = $("evolution-toast");
+ if (toast) toast.classList.add("hidden");
+ const star = $("brand-star");
+ if (star) star.classList.remove("pulse");
+ }
+
+ function initEvolutionToast() {
+ const see = $("evolution-toast-see");
+ if (see) {
+ see.addEventListener("click", () => {
+ hideEvolutionToast();
+ showVersionInfo();
+ });
+ }
+ const dismiss = $("evolution-toast-dismiss");
+ if (dismiss) dismiss.addEventListener("click", hideEvolutionToast);
+ }
+
function initModelSwitcher() {
const sw = $("model-switcher");
sw.addEventListener("click", async (e) => {
@@ -843,6 +908,8 @@ const App = (() => {
state.model = data.model || state.model;
state.evolutionCount = data.evolution_count ?? state.evolutionCount;
updateModelSwitcher();
+ updateGrowthCard();
+ maybeShowEvolutionToast();
break;
case "status":
handleStatus(data);
@@ -909,6 +976,8 @@ const App = (() => {
if (data.model) state.model = data.model;
state.evolutionCount = data.evolution_count ?? state.evolutionCount;
updateModelSwitcher();
+ updateGrowthCard();
+ maybeShowEvolutionToast();
Chat.addSystemMessage(EMRG_Copy.COPY.reconnected);
} else if (data.auth_failed) {
updateConnectionDot("red");
@@ -1072,6 +1141,7 @@ const App = (() => {
initModelSwitcher();
initModeSwitcher(); // WorkBuddy P2:Ask/Auto 工作模式
ResultPanel.init(); // WorkBuddy P1:结果面板(⌘\ 折叠 + 窄屏自动隐藏)
+ initEvolutionToast(); // WorkBuddy P3:进化 toast 按钮绑定
}
// ── 暴露 ─────────────────────────────────
@@ -1089,6 +1159,9 @@ const App = (() => {
bindUi,
updateEmptyState,
updateModelSwitcher,
+ updateGrowthCard, // WorkBuddy P3:成长卡(导出供测试)
+ maybeShowEvolutionToast, // WorkBuddy P3:进化 toast 检测
+ showVersionInfo, // WorkBuddy P3:/version 内容(toast "去看看" 共用)
setMode, // WorkBuddy P2:Ask/Auto 模式(导出供测试与外部调用)
};
})();
diff --git a/emrg/gui/renderer/js/copywriting.js b/emrg/gui/renderer/js/copywriting.js
index 75400300..b457a285 100644
--- a/emrg/gui/renderer/js/copywriting.js
+++ b/emrg/gui/renderer/js/copywriting.js
@@ -31,6 +31,13 @@ const COPY = {
deleteConfirmBody: "删除后无法恢复。",
noSessions: "还没有对话",
aboutEvolution: (n) => (n ? `EMRG 已自我成长 ${n} 次,感谢你的每一次反馈` : "EMRG 正在成长中"),
+ // WorkBuddy P3(rant 21:35):自进化可见化
+ growthCount: (n) => `已自我进化 ${n} 次`,
+ growthNote: "边工作边学习,越用越懂你",
+ evolutionToastTitle: "EMRG 刚刚完成一次自我进化!",
+ evolutionToastMsg: (n) => `这是它的第 ${n} 次自我改进,现在更好用了。`,
+ evolutionToastSee: "去看看",
+ evolutionToastDismiss: "知道了",
};
window.EMRG_Copy = { toolPhrases, TOOL_FAIL_TEXT, COPY };
diff --git a/emrg/gui/renderer/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js
index 8c682786..a1140db5 100644
--- a/emrg/gui/renderer/js/dialogs.js
+++ b/emrg/gui/renderer/js/dialogs.js
@@ -196,6 +196,13 @@ const Dialogs = (() => {
vision: Boolean(m.vision),
}));
renderModelList();
+ // WorkBuddy P3:关于区(版本 + 进化计数)随设置面板打开时刷新
+ try {
+ const ver = $("about-version");
+ if (ver) ver.textContent = `v${App.state.version || "0.2.8"}`;
+ const evo = $("about-evolutions");
+ if (evo) evo.textContent = EMRG_Copy.COPY.growthCount(App.state.evolutionCount ?? 0);
+ } catch { /* 元素缺失(测试桩)时忽略 */ }
} catch (e) {
Chat.addSystemMessage(`读取设置失败了:${e.message}`);
}
diff --git a/emrg/gui/test/app-commands.test.js b/emrg/gui/test/app-commands.test.js
index 81d4b740..2d50565c 100644
--- a/emrg/gui/test/app-commands.test.js
+++ b/emrg/gui/test/app-commands.test.js
@@ -26,8 +26,16 @@ function makeEl(id) {
children: [],
style: {},
dataset: {},
- classList: { add() {}, remove() {}, toggle() {} },
- addEventListener() {},
+ classList: {
+ _s: new Set(),
+ add(c) { this._s.add(c); },
+ remove(c) { this._s.delete(c); },
+ toggle(c) { this._s.has(c) ? this._s.delete(c) : this._s.add(c); },
+ contains(c) { return this._s.has(c); },
+ },
+ _listeners: {},
+ addEventListener(t, fn) { this._listeners[t] = fn; },
+ click() { const fn = this._listeners.click; if (fn) fn(); },
appendChild() {},
removeChild() {},
querySelectorAll: () => [],
@@ -257,3 +265,62 @@ test("P2:sendMessage 透传 state.mode(ask → sendMessage 带 mode)", asy
assert.strictEqual(sent.mode, "ask", "ask 模式应透传 mode 参数");
assert.strictEqual(sent.text, "帮我写一段代码", "文本应正常透传");
});
+
+// ── WorkBuddy P3(rant 21:35):自进化可见化 ────────────────
+test("P3:/version 使用动态版本号(不再硬编码 v0.2.7)", async () => {
+ const { ctx } = makeSandbox({
+ init: async () => ({ config_exists: true, api_key_configured: true, project_dir: "/p", project_dir_valid: true, server_id: "srv", model: "m", version: "0.2.8", evolution_count: 3, sessions: [] }),
+ });
+ await tick();
+ await vm.runInContext("App.handleCommand({ type: 'command', cmd: '/version', args: [] })", ctx);
+ const src = fs.readFileSync(path.join(RENDERER_JS, "app.js"), "utf8");
+ assert.ok(!src.includes("EMRG GUI v0.2.7"), "app.js 不应再硬编码 v0.2.7");
+ assert.ok(src.includes("state.version"), "版本应来自 state.version");
+ assert.strictEqual(typeof (await vm.runInContext("App.showVersionInfo", ctx)), "function");
+});
+
+test("P3:updateGrowthCard 更新侧边栏成长计数", async () => {
+ const { ctx, els } = makeSandbox({
+ init: async () => ({ config_exists: true, api_key_configured: true, project_dir: "/p", project_dir_valid: true, server_id: "srv", model: "m", version: "0.2.8", evolution_count: 42, sessions: [] }),
+ });
+ await tick();
+ await vm.runInContext("App.updateGrowthCard()", ctx);
+ assert.strictEqual(els["growth-count"].textContent, "42", "成长卡应显示 42 次");
+});
+
+test("P3:进化计数增长 → toast 显示(一天最多一次)", async () => {
+ const { ctx, els } = makeSandbox({
+ init: async () => ({ config_exists: true, api_key_configured: true, project_dir: "/p", project_dir_valid: true, server_id: "srv", model: "m", version: "0.2.8", evolution_count: 5, sessions: [] }),
+ });
+ await tick();
+ // 模拟 pong 事件:计数 5 → 6(进化发生)
+ await vm.runInContext(
+ "App.handleEvent({ type: 'pong', data: { identity: { instance_id: 'srv' }, model: 'm', evolution_count: 6 } })",
+ ctx
+ );
+ assert.ok(els["evolution-toast"], "toast 元素存在");
+ assert.ok(!els["evolution-toast"].classList.contains("hidden"), "toast 应显示");
+ // 同一天再次增长 → 不再提示(频率控制)
+ await vm.runInContext(
+ "App.handleEvent({ type: 'pong', data: { identity: { instance_id: 'srv' }, model: 'm', evolution_count: 7 } })",
+ ctx
+ );
+ // toast 仍显示(未隐藏),但不重新触发——验证不可直接观测;此处确认无异常即可
+ assert.ok(!els["evolution-toast"].classList.contains("hidden"), "toast 保持显示");
+});
+
+test("P3:toast '去看看' 关闭并输出版本信息", async () => {
+ const { ctx, els } = makeSandbox({
+ init: async () => ({ config_exists: true, api_key_configured: true, project_dir: "/p", project_dir_valid: true, server_id: "srv", model: "m", version: "0.2.8", evolution_count: 2, sessions: [] }),
+ });
+ await tick();
+ // 计数 2 → 3 触发 toast
+ await vm.runInContext(
+ "App.handleEvent({ type: 'pong', data: { identity: { instance_id: 'srv' }, model: 'm', evolution_count: 3 } })",
+ ctx
+ );
+ const see = els["evolution-toast-see"];
+ assert.ok(see, "去看看按钮存在");
+ if (see.click) see.click();
+ assert.ok(els["evolution-toast"].classList.contains("hidden"), "点击后 toast 应隐藏");
+});