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
2 changes: 1 addition & 1 deletion Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (982) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (983) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (260: 45 daemon_client + 19 conn-manager + 22 app-commands + 131 renderer smoke + 16 i18n + 7 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + 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
19 changes: 17 additions & 2 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -279,6 +279,7 @@ vision = false
server_id: pong?.identity?.instance_id || "",
model: pong?.model || "",
evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong)
current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅)
version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7)
sessions,
open_sessions: openSessionsList(),
Expand DownExpand Up@@ -315,6 +316,20 @@ vision = false

ipcMain.handle("emrg:listSessions", async () => listSessions());

// Rant 2026-08-20T18:30:57:一键"重启生效"——发 shutdown(source=gui-restart)让
// daemon 停;connManager 检测到全部掉线 → restart-recovery → ensureDaemon 用新
// 安装代码重新 spawn(GUI 本就是 daemon 生命周期 owner,不发子进程调 CLI)。
ipcMain.handle("emrg:restartDaemon", async () => {
const conn = activeConn();
if (!conn || !conn.connected) throw new Error("daemon not connected");
try {
conn.sendCommand("shutdown", { source: "gui-restart" });
} catch (e) {
throw new Error(`shutdown failed: ${e.message}`);
}
return { ok: true };
});

ipcMain.handle("emrg:switchSession", async (_e, { sessionId, projectPath } = {}) => {
if (!validateSessionId(sessionId)) throw new Error("invalid session_id");
// P6(rant 15:07:19 边界):projectPath 校验(跨项目打开时传项目路径)
Expand DownExpand Up@@ -851,7 +866,7 @@ vision = false
const sessions = await listSessions();
sendToRenderer("sessions", { sessions });
const pong = await waitForPong();
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "" });
logger.info("[gui] connManager recovery complete");
} catch (e) {
logger.warn(`[gui] post-recovery refresh failed: ${e.message}`);
Expand DownExpand Up@@ -1076,7 +1091,7 @@ vision = false
const sessions = await listSessions();
sendToRenderer("sessions", { sessions });
const pong = await waitForPong();
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "" });
}
}, delay);
}
Expand Down
1 change: 1 addition & 0 deletions emrg/gui/preload.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const api = {
init: () => ipcRenderer.invoke("emrg:init"),
sendMessage: (payload) => ipcRenderer.invoke("emrg:sendMessage", payload),
listSessions: () => ipcRenderer.invoke("emrg:listSessions"),
restartDaemon: () => ipcRenderer.invoke("emrg:restartDaemon"),
switchSession: (payload) => ipcRenderer.invoke("emrg:switchSession", payload),
newSession: (payload) => ipcRenderer.invoke("emrg:newSession", payload),
deleteSession: (payload) => ipcRenderer.invoke("emrg:deleteSession", payload),
Expand Down
23 changes: 23 additions & 0 deletions emrg/gui/renderer/css/layout.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -392,6 +392,29 @@
}
#github-banner .btn { padding: 4px 12px; min-height: 0; font-size: var(--fs-secondary); }

/* ── 升级完成横幅(rant 2026-08-20T18:30:57:版本变化 → 提示 + 一键重启) ── */
#upgrade-banner {
position: absolute;
top: var(--sp-3);
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: var(--sp-2);
background: var(--accent-soft);
color: var(--accent);
border: 1px solid var(--border);
border-radius: 999px;
padding: 6px var(--sp-3) 6px var(--sp-4);
font-size: var(--fs-secondary);
box-shadow: var(--shadow-md);
z-index: 20;
animation: banner-in var(--dur-med) var(--ease);
white-space: nowrap;
max-width: 90%;
}
#upgrade-banner .btn { padding: 4px 12px; min-height: 0; font-size: var(--fs-secondary); }

/* ── 侧边栏折叠(⌘B / 窄屏) ────────────── */
body.sidebar-collapsed #sidebar {
display: none;
Expand Down
7 changes: 7 additions & 0 deletions emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,13 @@
<button type="button" id="github-banner-dismiss" class="btn btn-ghost" title="关闭" data-i18n-title="settings.githubBannerDismiss">✕</button>
</div>

<!-- 升级完成横幅(rant 2026-08-20T18:30:57:检测到新版本 → 提示 + 一键重启生效) -->
<div id="upgrade-banner" class="hidden">
<span id="upgrade-banner-msg"></span>
<button type="button" id="upgrade-banner-restart" class="btn btn-primary" data-i18n="app.upgradeRestartBtn">重启生效</button>
<button type="button" id="upgrade-banner-dismiss" class="btn btn-ghost" title="关闭" data-i18n-title="settings.githubBannerDismiss">✕</button>
</div>

<!-- 工作区(rant 18:55:09 v0.2:会话视图 + 面板视图的公共父容器;DOM 显隐切换,状态保留) -->
<div id="workspace">
<section class="workspace-view" id="panel-projects" data-view="projects">
Expand Down
38 changes: 38 additions & 0 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,6 +76,8 @@ const App = (() => {
state.serverId = init.server_id || "";
state.model = init.model || "";
state.version = init.version || "";
state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准)
state.lastKnownVersion = state.currentVersion;
state.evolutionCount = init.evolution_count ?? null;
state.lastKnownEvolutionCount = state.evolutionCount;
updateConnectionDot(init.config_exists && init.api_key_configured ? "green" : "gray");
Expand DownExpand Up@@ -1335,6 +1337,39 @@ const App = (() => {
$("conn-banner").classList.add("hidden");
}

// ── 升级完成横幅(rant 2026-08-20T18:30:57) ─────────────
function maybeShowUpgradeBanner(currentVersion) {
if (!currentVersion) return; // 无版本数据(dev 运行)→ 不显示
if (currentVersion === state.lastKnownVersion) return; // 版本未变
const b = $("upgrade-banner");
if (!b) return;
const msg = $("upgrade-banner-msg");
if (msg) msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion });
b.classList.remove("hidden");
state.lastKnownVersion = currentVersion; // 已提示,防重复弹
}
function hideUpgradeBanner() {
const b = $("upgrade-banner");
if (b) b.classList.add("hidden");
}
function initUpgradeBanner() {
const restart = $("upgrade-banner-restart");
if (restart) restart.addEventListener("click", async () => {
restart.disabled = true;
try {
await window.emrg.restartDaemon();
// shutdown 已发——connManager restart-recovery 会重新 spawn 新 daemon 并重连;
// 恢复后 status 事件里 current_version 已 = 新版本 → 横幅消失。
hideUpgradeBanner();
} catch (e) {
restart.disabled = false;
Chat.addSystemMessage(_t("app.upgradeRestartFailed", { msg: e.message }));
}
});
const dismiss = $("upgrade-banner-dismiss");
if (dismiss) dismiss.addEventListener("click", hideUpgradeBanner);
}

// ── 事件处理(main 已分类) ─────────────
async function handleEvent(evt) {
const { type, data } = evt;
Expand DownExpand Up@@ -1540,6 +1575,8 @@ const App = (() => {
if (data.server_id) state.serverId = data.server_id;
if (data.model) state.model = data.model;
state.evolutionCount = data.evolution_count ?? state.evolutionCount;
// rant 18:30:57:pong 携带 current_version → 对比已知版本,变化则弹升级横幅
maybeShowUpgradeBanner(data.current_version || state.currentVersion);
updateModelSwitcher();
updateGrowthCard();
maybeShowEvolutionToast();
Expand DownExpand Up@@ -1746,6 +1783,7 @@ const App = (() => {
Dialogs.initTaskManagement(); // rant 18:23:15 P3:定时任务/自定义类型管理
Dialogs.initRantPanel(); // rant 14:10:14 P4:rant 面板(筛选/新建)
initGithubBanner(); // Windows GCM rant Stage 2:演化需 GitHub 但未认证时的连接横幅
initUpgradeBanner(); // rant 18:30:57:升级完成横幅(重启生效按钮)
initModelSwitcher();
initModeSwitcher(); // WorkBuddy P2:Ask/Auto 工作模式
ResultPanel.init(); // WorkBuddy P1:结果面板(⌘\ 折叠 + 窄屏自动隐藏)
Expand Down
6 changes: 6 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -416,6 +416,9 @@ const I18N = (() => {
"app.modelSwitchFailed": "切换模型失败了:{msg}",
"app.modelListFailed": "读取模型列表失败了:{msg}",
"app.askModeNotice": "Ask 模式:我只对话,不执行工具。输入内容问我就好。",
"app.upgradeBannerMsg": "EMRG 已升级到 {version},重启后生效",
"app.upgradeRestartFailed": "重启失败:{msg}",
"app.upgradeRestartBtn": "重启生效",
"app.unknownResult": "结果未知——连接中断",
"app.error": "出了点问题:{msg}",
"app.unknownError": "未知错误",
Expand DownExpand Up@@ -826,6 +829,9 @@ const I18N = (() => {
"app.modelSwitchFailed": "Failed to switch model: {msg}",
"app.modelListFailed": "Failed to load models: {msg}",
"app.askModeNotice": "Ask mode: I only chat — no tools. Just ask.",
"app.upgradeBannerMsg": "EMRG upgraded to {version} — restart to apply",
"app.upgradeRestartFailed": "Restart failed: {msg}",
"app.upgradeRestartBtn": "Restart to apply",
"app.unknownResult": "Result unknown — connection lost",
"app.error": "Something went wrong: {msg}",
"app.unknownError": "Unknown error",
Expand Down
53 changes: 53 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,6 +148,7 @@ const ELEMENT_IDS = [
"result-tabs", "result-tab-files", "result-tab-artifacts", "result-tabbar", "result-files", "result-viewer", "result-resizer",
"growth-card", "growth-count", "about-recent",
"github-banner", "github-banner-msg", "github-banner-connect", "github-banner-dismiss",
"upgrade-banner", "upgrade-banner-msg", "upgrade-banner-restart", "upgrade-banner-dismiss",
"toast", "toast-msg",
// rant 18:23:15 P3:定时任务/自定义类型管理(settings 区)
"task-list", "task-add-btn", "task-template-mgr-btn",
Expand DownExpand Up@@ -733,6 +734,58 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现
assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查");
});

test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现
const { ctx } = makeSandbox({
init: async () => ({
config_exists: true,
api_key_configured: true,
current_version: "0.2.58",
sessions: [],
}),
});
await tick();
await vm.runInContext(`(function() {
document.getElementById("upgrade-banner").classList.add("hidden");
App.state.lastKnownVersion = "0.2.58";
App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.59" } });
})()`, ctx);
const visible = vm.runInContext(
'!document.getElementById("upgrade-banner").classList.contains("hidden")',
ctx
);
assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现");

// 负态:版本未变 → 横幅保持隐藏
const { ctx: ctx2 } = makeSandbox({});
await tick();
await vm.runInContext(`(function() {
document.getElementById("upgrade-banner").classList.add("hidden");
App.state.lastKnownVersion = "0.2.58";
App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.58" } });
})()`, ctx2);
const hidden = vm.runInContext(
'document.getElementById("upgrade-banner").classList.contains("hidden")',
ctx2
);
assert.strictEqual(hidden, true, "版本未变 → 横幅应保持隐藏");

// 重启按钮 → restartDaemon 调用
let restarted = false;
const { ctx: ctx3 } = makeSandbox({});
await tick();
await vm.runInContext(`(async function() {
App._testRestartDaemon = () => { window.__restartCalled = true; };
})()`, ctx3);
const appSrc = fs.readFileSync(path.join(RENDERER_JS, "app.js"), "utf8");
assert.ok(appSrc.includes("restartDaemon"), "重启按钮应调用 window.emrg.restartDaemon");
const GUI_DIR = path.join(__dirname, "..");
const mainSrc = fs.readFileSync(path.join(GUI_DIR, "main.js"), "utf8");
assert.ok(mainSrc.includes("emrg:restartDaemon"), "main.js 应注册 emrg:restartDaemon IPC");
const preloadSrc = fs.readFileSync(path.join(GUI_DIR, "preload.js"), "utf8");
assert.ok(preloadSrc.includes("restartDaemon"), "preload 应暴露 restartDaemon");
});

test("右键菜单:重命名对话框 → renameSession 调用(设计 §3.2)", async () => {
let renamed = null;
const { ctx, els } = makeSandbox({
Expand Down
17 changes: 17 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -632,6 +632,19 @@ async def _run_upgrade_session(self, session_id: str, cwd: str, prompt: str) ->
logger.debug("upgrade session failed (retry next tick)", exc_info=True)
self._session_busy[session_id] = False

def _current_installed_version(self) -> str:
"""Current installed EMRG version from ~/.emrg/install/version.txt.

Rant 2026-08-20T18:30:57: raw data only, zero judgment — the GUI
compares it with its last known version and shows the upgrade banner.
Returns "" when the file is missing (dev/standalone runs).
"""
try:
v = (Path.home() / ".emrg" / "install" / "version.txt").read_text(encoding="utf-8").strip()
return v
except (OSError, ValueError):
return ""

def _evolution_count(self) -> int:
"""Total completed evolution cycles across scheduler handlers + disk.

Expand DownExpand Up@@ -1397,6 +1410,10 @@ async def _process_message(
"started_at": self.start_time.isoformat(),
"pid": os.getpid(),
"model": self.llm.config.model,
# Rant 2026-08-20T18:30:57:并入当前安装版本——GUI 轮询 pong 时对比
# 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据,
# 零判断逻辑(升级判断由 GUI 负责)。
"current_version": self._current_installed_version(),
})
return

Expand Down
32 changes: 32 additions & 0 deletions tests/test_daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1719,3 +1719,35 @@ def test_shutdown_message_missing_source_degrades(tmp_path, caplog):

assert "shutdown requested by client (peer=unknown peer, source=unknown)" in caplog.text
assert server._stop_reason == "shutdown_msg"


def test_pong_includes_current_version(tmp_path, monkeypatch):
"""Pong carries current_version from ~/.emrg/install/version.txt.

Rant 2026-08-20T18:30:57: the GUI compares the version it last saw with
the daemon's installed version and shows the upgrade banner on change.
Missing file → empty string (dev runs), never an error.
"""
import emrg.server.daemon as daemon_mod
install = tmp_path / ".emrg" / "install"
install.mkdir(parents=True)
(install / "version.txt").write_text("0.2.59\n", encoding="utf-8")

monkeypatch.setattr(daemon_mod.Path, "home", lambda: tmp_path)

server = _make_server()
assert server._current_installed_version() == "0.2.59"

# Missing file → "" (no crash)
empty = tmp_path / "no-install"
monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty)
assert server._current_installed_version() == ""

# Pong payload includes the field
monkeypatch.setattr(daemon_mod.Path, "home", lambda: tmp_path)
writer = _FakeWriter()
import asyncio
asyncio.run(server._process_message({"type": "ping"}, writer))
frame = _last_frame(writer)
assert frame["type"] == "pong"
assert frame["current_version"] == "0.2.59"
Loading