From b5395e9f8385a85826bcb65f52f605190cfa5694 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 6 Aug 2026 22:08:27 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20GUI=20WorkBuddy=20P3=20=E2=80=94=20evol?= =?UTF-8?q?ution=20visibility=20(growth=20card=20+=20toast=20+=20dynamic?= =?UTF-8?q?=20/version,=20rant=2021:35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar growth card: 🌱 已自我进化 N 次 + 提示语 (evolution_count from init/pong/status) - Evolution toast: count increase → toast (🌱 刚刚完成一次自我进化 [去看看][知道了]), once per day (localStorage), brand star pulse - Settings dialog About section: version + evolution count - /version no longer hardcodes v0.2.7 — main.js passes package.json version via init - +4 GUI tests (P3): dynamic version / growth card / toast trigger / toast dismiss - GUI tests: 67 pass --- emrg/gui/main.js | 6 ++- emrg/gui/renderer/css/components.css | 36 +++++++++++++ emrg/gui/renderer/css/layout.css | 22 ++++++++ emrg/gui/renderer/index.html | 22 ++++++++ emrg/gui/renderer/js/app.js | 79 ++++++++++++++++++++++++++-- emrg/gui/renderer/js/copywriting.js | 7 +++ emrg/gui/renderer/js/dialogs.js | 7 +++ emrg/gui/test/app-commands.test.js | 71 ++++++++++++++++++++++++- 8 files changed, 243 insertions(+), 7 deletions(-) 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 @@

后台任务

+ + + 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 应隐藏"); +});