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
6 changes: 4 additions & 2 deletions emrg/gui/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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();
Expand All @@ -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,
};
});
Expand Down
36 changes: 36 additions & 0 deletions emrg/gui/renderer/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
}
22 changes: 22 additions & 0 deletions emrg/gui/renderer/css/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions emrg/gui/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
<span class="brand-star" id="brand-star">✦</span>
<span>EMRG</span>
</div>
<!-- 成长状态卡(WorkBuddy P3:自进化可见化) -->
<div class="growth-card" id="growth-card" title="EMRG 每次自我改进都会自动汇报">
<div class="growth-line">🌱 已自我进化 <b id="growth-count">0</b> 次</div>
<div class="growth-note" id="growth-note">边工作边学习,越用越懂你</div>
</div>
<button id="new-chat-btn" class="new-chat-btn" title="新对话 (⌘N)">+ 新对话</button>
<nav id="conv-list"></nav>
<div class="sidebar-footer">
Expand Down Expand Up @@ -128,6 +133,13 @@ <h2>设置</h2>
</div>
</label>
</div>
<div class="settings-group">
<div class="settings-group-title">关于</div>
<div class="about-box">
<div class="about-line">EMRG <span id="about-version">v0.2.8</span> · <span id="about-evolutions">🌱 已自我进化 0 次</span></div>
<div class="hint" style="margin-top:6px;">EMRG 是一个会自我进化的 AI 智能体——每次改进都会自动汇报,你可以随时在这里看到它的成长。</div>
</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 Expand Up @@ -287,6 +299,16 @@ <h2>后台任务</h2>
</div>
</dialog>

<!-- 进化完成 toast(WorkBuddy P3:进化发生时温和提示) -->
<div id="evolution-toast" class="evolution-toast hidden">
<div class="evolution-toast-title">🌱 EMRG 刚刚完成一次自我进化!</div>
<div class="evolution-toast-msg" id="evolution-toast-msg">它学会了新的东西,变得更好用了。</div>
<div class="evolution-toast-actions">
<button type="button" id="evolution-toast-see" class="btn btn-primary">去看看</button>
<button type="button" id="evolution-toast-dismiss" class="btn btn-ghost">知道了</button>
</div>
</div>

<script src="js/utils.js"></script>
<script src="js/commands.js"></script>
<script src="js/markdown.js"></script>
Expand Down
79 changes: 76 additions & 3 deletions emrg/gui/renderer/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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(); // 首启引导
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -1072,6 +1141,7 @@ const App = (() => {
initModelSwitcher();
initModeSwitcher(); // WorkBuddy P2:Ask/Auto 工作模式
ResultPanel.init(); // WorkBuddy P1:结果面板(⌘\ 折叠 + 窄屏自动隐藏)
initEvolutionToast(); // WorkBuddy P3:进化 toast 按钮绑定
}

// ── 暴露 ─────────────────────────────────
Expand All @@ -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 模式(导出供测试与外部调用)
};
})();
Expand Down
7 changes: 7 additions & 0 deletions emrg/gui/renderer/js/copywriting.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
7 changes: 7 additions & 0 deletions emrg/gui/renderer/js/dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down
71 changes: 69 additions & 2 deletions emrg/gui/test/app-commands.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => [],
Expand Down Expand Up @@ -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 应隐藏");
});
Loading