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: 3 additions & 3 deletions Agent.md
Original file line number Diff line number Diff 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` (183: 43 daemon_client + 19 conn-manager + 22 app-commands + 63 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 build-config + 7 gui-state); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (187: 43 daemon_client + 19 conn-manager + 22 app-commands + 67 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 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 All @@ -112,8 +112,8 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (687) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (183: 43 daemon_client + 19 conn-manager + 22 app-commands + 63 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 build-config + 7 gui-state) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
Python: `uv run pytest tests/ -v` (688) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (187: 43 daemon_client + 19 conn-manager + 22 app-commands + 67 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 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 number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ EMRG 不只是追赶——它自己追上来。

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

快速检查:`uv run pytest tests/ -v`(当前 687 项)· `cd emrg/gui && npm test`(183 项:43 daemon_client + 19 conn-manager + 22 app-commands + 63 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 build-config + 7 gui-state)
快速检查:`uv run pytest tests/ -v`(当前 688 项)· `cd emrg/gui && npm test`(187 项:43 daemon_client + 19 conn-manager + 22 app-commands + 67 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 build-config + 7 gui-state)

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,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 687 items) · `cd emrg/gui && npm test` (183: 43 daemon_client + 19 conn-manager + 22 app-commands + 63 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 build-config + 7 gui-state)
Quick checks: `uv run pytest tests/ -v` (currently 688 items) · `cd emrg/gui && npm test` (187: 43 daemon_client + 19 conn-manager + 22 app-commands + 67 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 build-config + 7 gui-state)

---

Expand Down
10 changes: 8 additions & 2 deletions emrg/gui/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -559,11 +559,17 @@ vision = false
return { authenticated: Boolean(frame.authenticated), user: frame.user || null };
});

ipcMain.handle("emrg:updateCheck", async () => {
ipcMain.handle("emrg:updateCheck", async (_e, { force } = {}) => {
// Auto update-check prompt (rant 2026-08-10T07:12:12): query daemon's
// cached latest release; display-only, no auto download/install.
// force=true (rant 2026-08-11T09:18:16): settings manual check button
// — daemon runs a fresh GitHub fetch instead of returning the cache.
try {
const frame = await requireConn().sendCommandAndWait("update_check", {}, 10000);
const frame = await requireConn().sendCommandAndWait(
"update_check",
{ force: Boolean(force) },
10000,
);
return {
current_version: frame.current_version || "",
latest_version: frame.latest_version || "",
Expand Down
2 changes: 1 addition & 1 deletion emrg/gui/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const api = {
sendRant: (payload) => ipcRenderer.invoke("emrg:sendRant", payload),
evolutionSummary: (payload) => ipcRenderer.invoke("emrg:evolutionSummary", payload),
githubStatus: () => ipcRenderer.invoke("emrg:githubStatus"),
updateCheck: () => ipcRenderer.invoke("emrg:updateCheck"),
updateCheck: (payload) => ipcRenderer.invoke("emrg:updateCheck", payload),
updateCheckPrompted: (payload) => ipcRenderer.invoke("emrg:updateCheckPrompted", payload),
githubConnect: (payload) => ipcRenderer.invoke("emrg:githubConnect", payload),
githubDisconnect: () => ipcRenderer.invoke("emrg:githubDisconnect"),
Expand Down
3 changes: 3 additions & 0 deletions emrg/gui/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ <h2 data-i18n="settings.title">设置</h2>
<div class="about-box">
<div class="about-line">EMRG <span id="about-version">v0.2.8</span> · <span id="about-evolutions">🌱 <span data-i18n="copy.growthCountPrefix">已自我进化</span> 0 <span data-i18n="copy.times">次</span></span></div>
<div class="about-line hidden" id="about-update"></div>
<div class="about-line">
<button type="button" class="btn btn-sm" id="about-update-check-btn" data-i18n="settings.checkUpdate">检查更新</button>
</div>
<div class="hint" style="margin-top:6px;" data-i18n="settings.aboutDesc">EMRG 是一个会自我进化的 AI 智能体——每次改进都会自动汇报,你可以随时在这里看到它的成长。</div>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions emrg/gui/renderer/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ const App = (() => {
// 修复:boot 成功路径必须启用输入框(此前仅 done/cancelled/disconnected/error
// 回调会调用 setComposerDisabled(false),形成"需先发消息才能启用输入框"死锁)
setComposerDisabled(false);
// rant 2026-08-11T09:18:16:启动主动更新提示——不开设置也能看到新版本
// (非阻塞系统消息,一次幂等;daemon 未就绪时静默失败)
if (window.EMRG_Dialogs?.promptUpdateAtStartup) {
Dialogs.promptUpdateAtStartup();
}
} catch (e) {
Chat.addSystemMessage(_t("app.bootFailed", { msg: e.message }));
}
Expand Down
69 changes: 56 additions & 13 deletions emrg/gui/renderer/js/dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ const Dialogs = (() => {
await refreshGithubStatus();
// Auto update-check prompt (rant 2026-08-10T07:12:12): about area shows
// a one-time non-intrusive line when a newer release exists.
// rant 2026-08-11T09:18:16:手动"检查更新"按钮(force 重新检查)
initUpdateCheckButton();
await refreshUpdateCheck();
} catch (e) {
Chat.addSystemMessage(_t("settings.readFailed", { msg: e.message }));
Expand Down Expand Up @@ -490,37 +492,75 @@ const Dialogs = (() => {

// Auto update-check prompt (rant 2026-08-10T07:12:12): display-only, one
// line in the about area, never a modal — no auto download/install.
async function refreshUpdateCheck() {
const el = $("about-update");
if (!el) return; // 元素缺失(测试桩)时忽略
// force (rant 2026-08-11T09:18:16): settings manual check button — run a
// fresh GitHub fetch instead of returning the daemon's cached result.
// ⚠️ 局部变量命名 updEl(勿用 el——会遮蔽模块级 el() 元素工厂,el("a",…)
// 抛 TypeError → catch 吞掉 → 更新行永远 hidden,正是 #602 隐藏缺陷)
async function refreshUpdateCheck({ force = false } = {}) {
const updEl = $("about-update");
if (!updEl) return; // 元素缺失(测试桩)时忽略
try {
const u = await window.emrg.updateCheck();
const u = await window.emrg.updateCheck({ force });
if (!u || !u.enabled || !u.has_update || !u.latest_version) {
el.classList.add("hidden");
el.textContent = "";
updEl.classList.add("hidden");
updEl.textContent = "";
return;
}
if (u.latest_version === u.prompted_version) {
el.classList.add("hidden");
el.textContent = "";
updEl.classList.add("hidden");
updEl.textContent = "";
return;
}
const link = el("a", {
href: "https://github.com/argszero/emrg/releases",
target: "_blank",
rel: "noopener",
}, _t("settings.updateAvailable", { latest: u.latest_version }));
el.textContent = "";
el.appendChild(link);
el.classList.remove("hidden");
updEl.textContent = "";
updEl.appendChild(link);
updEl.classList.remove("hidden");
// 幂等:同版本只提示一次
try { await window.emrg.updateCheckPrompted({ version: u.latest_version }); } catch { /* ignore */ }
} catch {
el.classList.add("hidden");
el.textContent = "";
updEl.classList.add("hidden");
updEl.textContent = "";
}
}

// 设置页"检查更新"手动按钮(rant 2026-08-11T09:18:16):点击立即强制
// 重新检查(不等 TTL 轮询),检查中显示"检查中…",完成后恢复按钮。
function initUpdateCheckButton() {
const btn = $("about-update-check-btn");
if (!btn) return; // 元素缺失(测试桩)时忽略
btn.addEventListener("click", async () => {
const original = btn.textContent;
btn.disabled = true;
btn.textContent = _t("settings.checkingUpdate");
try {
await refreshUpdateCheck({ force: true });
} finally {
btn.disabled = false;
btn.textContent = original;
}
});
}

// 启动主动更新提示(rant 2026-08-11T09:18:16):GUI 启动成功路径调用,
// 不依赖打开设置对话框——有新版本且未提示过时输出一条非阻塞系统消息
// (对齐 TUI 启动 system 行)。boot 时 daemon 可能未就绪 → 静默失败。
async function promptUpdateAtStartup() {
try {
const u = await window.emrg.updateCheck({ force: false });
if (!u || !u.enabled || !u.has_update || !u.latest_version) return;
if (u.latest_version === u.prompted_version) return;
Chat.addSystemMessage(
_t("app.updateAvailable", { latest: u.latest_version }),
);
// 幂等:同版本只提示一次
try { await window.emrg.updateCheckPrompted({ version: u.latest_version }); } catch { /* ignore */ }
} catch { /* boot-time daemon may not be ready — silent */ }
}

// ── 确认对话框(替代 confirm/alert) ────
let confirmCb = null;
function showConfirm(title, message, opts = {}) {
Expand Down Expand Up @@ -730,6 +770,9 @@ const Dialogs = (() => {
initGithubSection,
initDeviceDialog,
refreshGithubStatus,
refreshUpdateCheck,
initUpdateCheckButton,
promptUpdateAtStartup,
initOpenSessionDialog, // P5:打开会话对话框初始化
showOpenSessionDialog, // P5:两步打开会话
initNewSessionDialog, // P5 slice 2:新建会话对话框初始化
Expand Down
6 changes: 6 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ const I18N = (() => {
"settings.githubNotConnected": "未连接",
"settings.githubStatusFailed": "状态获取失败",
"settings.updateAvailable": "发现新版本 v{latest} —— 点击前往 Releases 下载(不会自动安装)",
"settings.checkUpdate": "检查更新",
"settings.checkingUpdate": "检查中…",
"app.updateAvailable": "发现新版本 v{latest} —— 点击前往 Releases 下载(不会自动安装):https://github.com/argszero/emrg/releases",
"settings.githubTokenEmpty": "请先粘贴 GitHub Personal Access Token",
"settings.githubConnecting": "连接中…",
"settings.githubConnected": "已连接 GitHub:@{user}(gh auth setup-git 已执行)",
Expand Down Expand Up @@ -419,6 +422,9 @@ const I18N = (() => {
"settings.githubNotConnected": "Not connected",
"settings.githubStatusFailed": "Failed to load status",
"settings.updateAvailable": "New version v{latest} available — click to visit Releases (no auto-install)",
"settings.checkUpdate": "Check for updates",
"settings.checkingUpdate": "Checking…",
"app.updateAvailable": "New version v{latest} available — https://github.com/argszero/emrg/releases (no auto-install)",
"settings.githubTokenEmpty": "Please paste a GitHub Personal Access Token first",
"settings.githubConnecting": "Connecting…",
"settings.githubConnected": "Connected to GitHub: @{user} (gh auth setup-git done)",
Expand Down
85 changes: 85 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const ELEMENT_IDS = [
"tasks-dialog", "tasks-list", "tasks-close",
"result-panel", "result-list", "result-toggle",
"growth-card", "growth-count", "about-recent",
"about-update", "about-update-check-btn",
"github-banner", "github-banner-msg", "github-banner-connect", "github-banner-dismiss",
];

Expand Down Expand Up @@ -1602,3 +1603,87 @@ test("B3: 发送消息清除该会话草稿;新会话从空草稿开始", asyn
const newDraft = await vm.runInContext("App.state.drafts.get('s-new')", ctx);
assert.strictEqual(newDraft, "", "new session starts with empty draft");
});

test("rant 09:18:启动主动更新提示——有新版本且未提示过 → 系统消息一次", async () => {
const { ctx } = makeSandbox({
updateCheck: async () => ({ enabled: true, has_update: true, latest_version: "0.2.99", prompted_version: "", current_version: "0.2.23" }),
updateCheckPrompted: async () => ({}),
});
await tick();
const r = await vm.runInContext(`(async function() {
let prompted = null;
window.emrg.updateCheckPrompted = async (p) => { prompted = p; };
await EMRG_Dialogs.promptUpdateAtStartup();
const texts = [];
for (let i = 0; i < $("chat-view").children.length; i++) texts.push($("chat-view").children[i].textContent);
return { n: texts.length, joined: texts.join("|"), prompted: JSON.stringify(prompted) };
})()`, ctx);
assert.ok(r.n >= 1, "应输出一条系统消息");
assert.ok(r.joined.includes("0.2.99"), `系统消息应含新版本号,实际 ${r.joined}`);
assert.ok(r.prompted.includes("0.2.99"), "应记录已提示版本(幂等)");
});

test("rant 09:18:启动提示幂等——已提示过/未启用/无更新 → 不提示", async () => {
const { ctx } = makeSandbox({
updateCheck: async () => ({ enabled: true, has_update: true, latest_version: "0.2.99", prompted_version: "0.2.99", current_version: "0.2.23" }),
});
await tick();
const r = await vm.runInContext(`(async function() {
await EMRG_Dialogs.promptUpdateAtStartup();
return { n: $("chat-view").children.length };
})()`, ctx);
assert.strictEqual(r.n, 0, "已提示过同版本 → 不得重复提示");
});

test("rant 09:18:设置页手动检查按钮——点击强制重新检查并显示检查中", async () => {
const { ctx } = makeSandbox({
updateCheck: async (payload) => ({ enabled: true, has_update: true, latest_version: "0.2.99", prompted_version: "", current_version: "0.2.23", _force: payload && payload.force }),
updateCheckPrompted: async () => ({}),
});
await tick();
const r = await vm.runInContext(`(async function() {
const calls = [];
window.emrg.updateCheck = async (payload) => {
calls.push(payload || {});
return { enabled: true, has_update: true, latest_version: "0.2.99", prompted_version: "" };
};
const btn = $("about-update-check-btn");
btn.textContent = "检查更新"; // 模拟 index.html 静态文案(沙箱不加载 HTML)
EMRG_Dialogs.initUpdateCheckButton();
btn.click();
const during = { text: btn.textContent, disabled: btn.disabled };
await new Promise((res) => setTimeout(res, 20));
return {
calls: JSON.stringify(calls),
duringText: during.text,
afterText: btn.textContent,
afterDisabled: btn.disabled,
updateShown: !$("about-update").classList.contains("hidden"),
};
})()`, ctx);
const calls = JSON.parse(r.calls);
assert.strictEqual(calls.length, 1, "点击按钮应触发一次 updateCheck");
assert.strictEqual(calls[0].force, true, "手动检查必须带 force:true(跳过 TTL 缓存)");
assert.strictEqual(r.duringText, "检查中…", "检查中按钮应显示“检查中…”");
assert.strictEqual(r.afterText, "检查更新", "完成后按钮文案恢复");
assert.strictEqual(r.afterDisabled, false, "完成后按钮恢复可用");
assert.strictEqual(r.updateShown, true, "有新版本应显示 about-update 行");
});

test("rant 09:18:refreshUpdateCheck 透传 force 参数", async () => {
const { ctx } = makeSandbox({
updateCheck: async (payload) => ({ enabled: false, has_update: false, latest_version: "", prompted_version: "" }),
});
await tick();
const r = await vm.runInContext(`(async function() {
const calls = [];
window.emrg.updateCheck = async (payload) => { calls.push(payload || {}); return { enabled: false, has_update: false, latest_version: "", prompted_version: "" }; };
await EMRG_Dialogs.refreshUpdateCheck({ force: true });
await EMRG_Dialogs.refreshUpdateCheck();
return JSON.stringify(calls);
})()`, ctx);
const calls = JSON.parse(r);
assert.strictEqual(calls.length, 2);
assert.strictEqual(calls[0].force, true, "refreshUpdateCheck({force:true}) 应透传 force");
assert.strictEqual(calls[1].force, false, "默认 refreshUpdateCheck() 不带 force");
});
14 changes: 13 additions & 1 deletion emrg/server/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -1416,10 +1416,22 @@ async def _process_message(
# latest_version (populated at startup + every TTL) plus a
# has_update flag computed against the running version. No auto
# download/install; the client shows a one-time prompt.
# force:true (rant 2026-08-11T09:18:16) — GUI manual check
# button: run a fresh GitHub fetch first instead of the cache.
import emrg
from emrg.config import load_update_config
from emrg.update_check import is_newer, load_state, parse_version
from emrg.update_check import (
is_newer,
load_state,
parse_version,
run_update_check_once,
)

if msg.get("force"):
try:
await run_update_check_once()
except Exception:
logger.debug("forced update check failed", exc_info=True)
current = getattr(emrg, "__version__", "0")
state = load_state()
latest = state.get("latest_version") or ""
Expand Down
Loading
Loading