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` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (989) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 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
5 changes: 3 additions & 2 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,6 +285,7 @@ vision = false
model: pong?.model || "",
evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong)
current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅)
previous_version: pong?.previous_version || "", // rant 12:44:34:升级前版本(横幅 from→to)
version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7)
sessions,
open_sessions: openSessionsList(),
Expand DownExpand Up@@ -871,7 +872,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
logger.info("[gui] connManager recovery complete");
} catch (e) {
logger.warn(`[gui] post-recovery refresh failed: ${e.message}`);
Expand DownExpand Up@@ -1098,7 +1099,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
}
}, delay);
}
Expand Down
18 changes: 14 additions & 4 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,7 @@ const App = (() => {
state.model = init.model || "";
state.version = init.version || "";
state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准)
state.previousVersion = init.previous_version || ""; // rant 12:44:34:升级前版本(横幅 from→to)
state.lastKnownVersion = state.currentVersion;
state.evolutionCount = init.evolution_count ?? null;
state.lastKnownEvolutionCount = state.evolutionCount;
Expand DownExpand Up@@ -1337,14 +1338,21 @@ const App = (() => {
$("conn-banner").classList.add("hidden");
}

// ── 升级完成横幅(rant 2026-08-20T18:30:57) ─────────────
function maybeShowUpgradeBanner(currentVersion) {
// ── 升级完成横幅(rant 2026-08-20T18:30:57 + 2026-08-21T12:44:34) ─────
function maybeShowUpgradeBanner(currentVersion, previousVersion) {
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 });
if (msg) {
// rant 12:44:34:daemon 提供升级前版本 → 显示 "from → to";否则回退旧文案
if (previousVersion && previousVersion !== currentVersion) {
msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: previousVersion, to: currentVersion });
} else {
msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion });
}
}
b.classList.remove("hidden");
state.lastKnownVersion = currentVersion; // 已提示,防重复弹
}
Expand DownExpand Up@@ -1574,7 +1582,9 @@ const App = (() => {
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);
// rant 12:44:34:同时携带 previous_version → 横幅显示 from → to
if (data.previous_version) state.previousVersion = data.previous_version;
maybeShowUpgradeBanner(data.current_version || state.currentVersion, state.previousVersion);
updateModelSwitcher();
updateGrowthCard();
maybeShowEvolutionToast();
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -415,6 +415,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "切换模型失败了:{msg}",
"app.modelListFailed": "读取模型列表失败了:{msg}",
"app.upgradeBannerMsg": "EMRG 已升级到 {version},重启后生效",
"app.upgradeBannerMsgFromTo": "EMRG 已从 {from} 升级到 {to},重启后生效",
"app.upgradeRestartFailed": "重启失败:{msg}",
"app.upgradeRestartBtn": "重启生效",
"app.unknownResult": "结果未知——连接中断",
Expand DownExpand Up@@ -826,6 +827,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "Failed to switch model: {msg}",
"app.modelListFailed": "Failed to load models: {msg}",
"app.upgradeBannerMsg": "EMRG upgraded to {version} — restart to apply",
"app.upgradeBannerMsgFromTo": "EMRG upgraded from {from} to {to} — restart to apply",
"app.upgradeRestartFailed": "Restart failed: {msg}",
"app.upgradeRestartBtn": "Restart to apply",
"app.unknownResult": "Result unknown — connection lost",
Expand Down
9 changes: 6 additions & 3 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -735,27 +735,30 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现
assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查");
});

test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现
test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现;有 previous_version → 显示 from→to
const { ctx } = makeSandbox({
init: async () => ({
config_exists: true,
api_key_configured: true,
current_version: "0.2.58",
previous_version: "",
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" } });
App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.61", previous_version: "0.2.57" } });
})()`, ctx);
const visible = vm.runInContext(
'!document.getElementById("upgrade-banner").classList.contains("hidden")',
ctx
);
assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现");
const bannerText = vm.runInContext('document.getElementById("upgrade-banner-msg").textContent', ctx);
assert.ok(bannerText.includes("0.2.57") && bannerText.includes("0.2.61"), `横幅应显示 from→to,实际 ${bannerText}`);

// 负态:版本未变 → 横幅保持隐藏
const { ctx: ctx2 } = makeSandbox({});
Expand Down
18 changes: 18 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,6 +645,21 @@ def _current_installed_version(self) -> str:
except (OSError, ValueError):
return ""

def _previous_installed_version(self) -> str:
"""Pre-upgrade EMRG version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent writes the version it is
replacing into previous-version.txt before overwriting version.txt,
so the GUI banner can show "upgraded from X to Y" instead of only the
target version. Raw data only; "" when missing (dev/standalone or
first install).
"""
try:
v = (Path.home() / ".emrg" / "install" / "previous-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@@ -1421,6 +1436,9 @@ async def _process_message(
# 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据,
# 零判断逻辑(升级判断由 GUI 负责)。
"current_version": self._current_installed_version(),
# Rant 2026-08-21T12:44:34:并入升级前版本——GUI 横幅显示 "from → to"
# (升级 agent 在覆盖 version.txt 前写入 previous-version.txt)。
"previous_version": self._previous_installed_version(),
})
return

Expand Down
5 changes: 5 additions & 0 deletions emrg/server/prompts/upgrade_prompt.j2
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,11 @@ release installer" means for this version.
update or add — the target tag's pyproject.toml dependencies are the
source of truth. Operate as needed (pip --target install/uninstall/update).
- `{{ install_dir }}/version.txt` ← {{ target_tag }} (the version).
- Before overwriting `version.txt`: if it already exists with a different
content, copy its current content to `{{ install_dir }}/previous-version.txt`
(rant 2026-08-21T12:44:34 — the GUI upgrade banner shows "from X to Y").
If `version.txt` did not exist (first install), delete any stale
`previous-version.txt`.
- `{{ install_dir }}/bin/` launcher scripts (emrg/emrgd/emrg.cmd/emrgd.cmd
etc.) — sync if the target tag changed them.
- GUI as needed: whether `~/Applications/EMRG.app` is updated is YOUR call —
Expand Down
35 changes: 35 additions & 0 deletions tests/test_daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1751,3 +1751,38 @@ def test_pong_includes_current_version(tmp_path, monkeypatch):
frame = _last_frame(writer)
assert frame["type"] == "pong"
assert frame["current_version"] == "0.2.59"


def test_pong_includes_previous_version(tmp_path, monkeypatch):
"""Pong carries previous_version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent records the pre-upgrade
version in previous-version.txt before overwriting version.txt, so the
GUI banner can show "upgraded from X to Y". Missing file → "" (dev runs
or first install), 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.61\n", encoding="utf-8")
(install / "previous-version.txt").write_text("0.2.57\n", encoding="utf-8")

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

server = _make_server()
assert server._previous_installed_version() == "0.2.57"

# Missing file → "" (no crash)
empty = tmp_path / "no-install"
monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty)
assert server._previous_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.61"
assert frame["previous_version"] == "0.2.57"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
emrg: GUI upgrade banner — show version range from→to (rant 2026-08-21T12:44:34) by argszero · Pull Request #913 · argszero/emrg · GitHub
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` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (989) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 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
5 changes: 3 additions & 2 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,6 +285,7 @@ vision = false
model: pong?.model || "",
evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong)
current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅)
previous_version: pong?.previous_version || "", // rant 12:44:34:升级前版本(横幅 from→to)
version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7)
sessions,
open_sessions: openSessionsList(),
Expand DownExpand Up@@ -871,7 +872,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
logger.info("[gui] connManager recovery complete");
} catch (e) {
logger.warn(`[gui] post-recovery refresh failed: ${e.message}`);
Expand DownExpand Up@@ -1098,7 +1099,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
}
}, delay);
}
Expand Down
18 changes: 14 additions & 4 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,7 @@ const App = (() => {
state.model = init.model || "";
state.version = init.version || "";
state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准)
state.previousVersion = init.previous_version || ""; // rant 12:44:34:升级前版本(横幅 from→to)
state.lastKnownVersion = state.currentVersion;
state.evolutionCount = init.evolution_count ?? null;
state.lastKnownEvolutionCount = state.evolutionCount;
Expand DownExpand Up@@ -1337,14 +1338,21 @@ const App = (() => {
$("conn-banner").classList.add("hidden");
}

// ── 升级完成横幅(rant 2026-08-20T18:30:57) ─────────────
function maybeShowUpgradeBanner(currentVersion) {
// ── 升级完成横幅(rant 2026-08-20T18:30:57 + 2026-08-21T12:44:34) ─────
function maybeShowUpgradeBanner(currentVersion, previousVersion) {
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 });
if (msg) {
// rant 12:44:34:daemon 提供升级前版本 → 显示 "from → to";否则回退旧文案
if (previousVersion && previousVersion !== currentVersion) {
msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: previousVersion, to: currentVersion });
} else {
msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion });
}
}
b.classList.remove("hidden");
state.lastKnownVersion = currentVersion; // 已提示,防重复弹
}
Expand DownExpand Up@@ -1574,7 +1582,9 @@ const App = (() => {
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);
// rant 12:44:34:同时携带 previous_version → 横幅显示 from → to
if (data.previous_version) state.previousVersion = data.previous_version;
maybeShowUpgradeBanner(data.current_version || state.currentVersion, state.previousVersion);
updateModelSwitcher();
updateGrowthCard();
maybeShowEvolutionToast();
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -415,6 +415,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "切换模型失败了:{msg}",
"app.modelListFailed": "读取模型列表失败了:{msg}",
"app.upgradeBannerMsg": "EMRG 已升级到 {version},重启后生效",
"app.upgradeBannerMsgFromTo": "EMRG 已从 {from} 升级到 {to},重启后生效",
"app.upgradeRestartFailed": "重启失败:{msg}",
"app.upgradeRestartBtn": "重启生效",
"app.unknownResult": "结果未知——连接中断",
Expand DownExpand Up@@ -826,6 +827,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "Failed to switch model: {msg}",
"app.modelListFailed": "Failed to load models: {msg}",
"app.upgradeBannerMsg": "EMRG upgraded to {version} — restart to apply",
"app.upgradeBannerMsgFromTo": "EMRG upgraded from {from} to {to} — restart to apply",
"app.upgradeRestartFailed": "Restart failed: {msg}",
"app.upgradeRestartBtn": "Restart to apply",
"app.unknownResult": "Result unknown — connection lost",
Expand Down
9 changes: 6 additions & 3 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -735,27 +735,30 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现
assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查");
});

test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现
test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现;有 previous_version → 显示 from→to
const { ctx } = makeSandbox({
init: async () => ({
config_exists: true,
api_key_configured: true,
current_version: "0.2.58",
previous_version: "",
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" } });
App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.61", previous_version: "0.2.57" } });
})()`, ctx);
const visible = vm.runInContext(
'!document.getElementById("upgrade-banner").classList.contains("hidden")',
ctx
);
assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现");
const bannerText = vm.runInContext('document.getElementById("upgrade-banner-msg").textContent', ctx);
assert.ok(bannerText.includes("0.2.57") && bannerText.includes("0.2.61"), `横幅应显示 from→to,实际 ${bannerText}`);

// 负态:版本未变 → 横幅保持隐藏
const { ctx: ctx2 } = makeSandbox({});
Expand Down
18 changes: 18 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,6 +645,21 @@ def _current_installed_version(self) -> str:
except (OSError, ValueError):
return ""

def _previous_installed_version(self) -> str:
"""Pre-upgrade EMRG version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent writes the version it is
replacing into previous-version.txt before overwriting version.txt,
so the GUI banner can show "upgraded from X to Y" instead of only the
target version. Raw data only; "" when missing (dev/standalone or
first install).
"""
try:
v = (Path.home() / ".emrg" / "install" / "previous-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@@ -1421,6 +1436,9 @@ async def _process_message(
# 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据,
# 零判断逻辑(升级判断由 GUI 负责)。
"current_version": self._current_installed_version(),
# Rant 2026-08-21T12:44:34:并入升级前版本——GUI 横幅显示 "from → to"
# (升级 agent 在覆盖 version.txt 前写入 previous-version.txt)。
"previous_version": self._previous_installed_version(),
})
return

Expand Down
5 changes: 5 additions & 0 deletions emrg/server/prompts/upgrade_prompt.j2
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,11 @@ release installer" means for this version.
update or add — the target tag's pyproject.toml dependencies are the
source of truth. Operate as needed (pip --target install/uninstall/update).
- `{{ install_dir }}/version.txt` ← {{ target_tag }} (the version).
- Before overwriting `version.txt`: if it already exists with a different
content, copy its current content to `{{ install_dir }}/previous-version.txt`
(rant 2026-08-21T12:44:34 — the GUI upgrade banner shows "from X to Y").
If `version.txt` did not exist (first install), delete any stale
`previous-version.txt`.
- `{{ install_dir }}/bin/` launcher scripts (emrg/emrgd/emrg.cmd/emrgd.cmd
etc.) — sync if the target tag changed them.
- GUI as needed: whether `~/Applications/EMRG.app` is updated is YOUR call —
Expand Down
35 changes: 35 additions & 0 deletions tests/test_daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1751,3 +1751,38 @@ def test_pong_includes_current_version(tmp_path, monkeypatch):
frame = _last_frame(writer)
assert frame["type"] == "pong"
assert frame["current_version"] == "0.2.59"


def test_pong_includes_previous_version(tmp_path, monkeypatch):
"""Pong carries previous_version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent records the pre-upgrade
version in previous-version.txt before overwriting version.txt, so the
GUI banner can show "upgraded from X to Y". Missing file → "" (dev runs
or first install), 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.61\n", encoding="utf-8")
(install / "previous-version.txt").write_text("0.2.57\n", encoding="utf-8")

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

server = _make_server()
assert server._previous_installed_version() == "0.2.57"

# Missing file → "" (no crash)
empty = tmp_path / "no-install"
monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty)
assert server._previous_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.61"
assert frame["previous_version"] == "0.2.57"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' emrg: GUI upgrade banner — show version range from→to (rant 2026-08-21T12:44:34) by argszero · Pull Request #913 · argszero/emrg · GitHub
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` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (989) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 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
5 changes: 3 additions & 2 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,6 +285,7 @@ vision = false
model: pong?.model || "",
evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong)
current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅)
previous_version: pong?.previous_version || "", // rant 12:44:34:升级前版本(横幅 from→to)
version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7)
sessions,
open_sessions: openSessionsList(),
Expand DownExpand Up@@ -871,7 +872,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
logger.info("[gui] connManager recovery complete");
} catch (e) {
logger.warn(`[gui] post-recovery refresh failed: ${e.message}`);
Expand DownExpand Up@@ -1098,7 +1099,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
}
}, delay);
}
Expand Down
18 changes: 14 additions & 4 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,7 @@ const App = (() => {
state.model = init.model || "";
state.version = init.version || "";
state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准)
state.previousVersion = init.previous_version || ""; // rant 12:44:34:升级前版本(横幅 from→to)
state.lastKnownVersion = state.currentVersion;
state.evolutionCount = init.evolution_count ?? null;
state.lastKnownEvolutionCount = state.evolutionCount;
Expand DownExpand Up@@ -1337,14 +1338,21 @@ const App = (() => {
$("conn-banner").classList.add("hidden");
}

// ── 升级完成横幅(rant 2026-08-20T18:30:57) ─────────────
function maybeShowUpgradeBanner(currentVersion) {
// ── 升级完成横幅(rant 2026-08-20T18:30:57 + 2026-08-21T12:44:34) ─────
function maybeShowUpgradeBanner(currentVersion, previousVersion) {
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 });
if (msg) {
// rant 12:44:34:daemon 提供升级前版本 → 显示 "from → to";否则回退旧文案
if (previousVersion && previousVersion !== currentVersion) {
msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: previousVersion, to: currentVersion });
} else {
msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion });
}
}
b.classList.remove("hidden");
state.lastKnownVersion = currentVersion; // 已提示,防重复弹
}
Expand DownExpand Up@@ -1574,7 +1582,9 @@ const App = (() => {
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);
// rant 12:44:34:同时携带 previous_version → 横幅显示 from → to
if (data.previous_version) state.previousVersion = data.previous_version;
maybeShowUpgradeBanner(data.current_version || state.currentVersion, state.previousVersion);
updateModelSwitcher();
updateGrowthCard();
maybeShowEvolutionToast();
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -415,6 +415,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "切换模型失败了:{msg}",
"app.modelListFailed": "读取模型列表失败了:{msg}",
"app.upgradeBannerMsg": "EMRG 已升级到 {version},重启后生效",
"app.upgradeBannerMsgFromTo": "EMRG 已从 {from} 升级到 {to},重启后生效",
"app.upgradeRestartFailed": "重启失败:{msg}",
"app.upgradeRestartBtn": "重启生效",
"app.unknownResult": "结果未知——连接中断",
Expand DownExpand Up@@ -826,6 +827,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "Failed to switch model: {msg}",
"app.modelListFailed": "Failed to load models: {msg}",
"app.upgradeBannerMsg": "EMRG upgraded to {version} — restart to apply",
"app.upgradeBannerMsgFromTo": "EMRG upgraded from {from} to {to} — restart to apply",
"app.upgradeRestartFailed": "Restart failed: {msg}",
"app.upgradeRestartBtn": "Restart to apply",
"app.unknownResult": "Result unknown — connection lost",
Expand Down
9 changes: 6 additions & 3 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -735,27 +735,30 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现
assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查");
});

test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现
test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现;有 previous_version → 显示 from→to
const { ctx } = makeSandbox({
init: async () => ({
config_exists: true,
api_key_configured: true,
current_version: "0.2.58",
previous_version: "",
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" } });
App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.61", previous_version: "0.2.57" } });
})()`, ctx);
const visible = vm.runInContext(
'!document.getElementById("upgrade-banner").classList.contains("hidden")',
ctx
);
assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现");
const bannerText = vm.runInContext('document.getElementById("upgrade-banner-msg").textContent', ctx);
assert.ok(bannerText.includes("0.2.57") && bannerText.includes("0.2.61"), `横幅应显示 from→to,实际 ${bannerText}`);

// 负态:版本未变 → 横幅保持隐藏
const { ctx: ctx2 } = makeSandbox({});
Expand Down
18 changes: 18 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,6 +645,21 @@ def _current_installed_version(self) -> str:
except (OSError, ValueError):
return ""

def _previous_installed_version(self) -> str:
"""Pre-upgrade EMRG version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent writes the version it is
replacing into previous-version.txt before overwriting version.txt,
so the GUI banner can show "upgraded from X to Y" instead of only the
target version. Raw data only; "" when missing (dev/standalone or
first install).
"""
try:
v = (Path.home() / ".emrg" / "install" / "previous-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@@ -1421,6 +1436,9 @@ async def _process_message(
# 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据,
# 零判断逻辑(升级判断由 GUI 负责)。
"current_version": self._current_installed_version(),
# Rant 2026-08-21T12:44:34:并入升级前版本——GUI 横幅显示 "from → to"
# (升级 agent 在覆盖 version.txt 前写入 previous-version.txt)。
"previous_version": self._previous_installed_version(),
})
return

Expand Down
5 changes: 5 additions & 0 deletions emrg/server/prompts/upgrade_prompt.j2
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,11 @@ release installer" means for this version.
update or add — the target tag's pyproject.toml dependencies are the
source of truth. Operate as needed (pip --target install/uninstall/update).
- `{{ install_dir }}/version.txt` ← {{ target_tag }} (the version).
- Before overwriting `version.txt`: if it already exists with a different
content, copy its current content to `{{ install_dir }}/previous-version.txt`
(rant 2026-08-21T12:44:34 — the GUI upgrade banner shows "from X to Y").
If `version.txt` did not exist (first install), delete any stale
`previous-version.txt`.
- `{{ install_dir }}/bin/` launcher scripts (emrg/emrgd/emrg.cmd/emrgd.cmd
etc.) — sync if the target tag changed them.
- GUI as needed: whether `~/Applications/EMRG.app` is updated is YOUR call —
Expand Down
35 changes: 35 additions & 0 deletions tests/test_daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1751,3 +1751,38 @@ def test_pong_includes_current_version(tmp_path, monkeypatch):
frame = _last_frame(writer)
assert frame["type"] == "pong"
assert frame["current_version"] == "0.2.59"


def test_pong_includes_previous_version(tmp_path, monkeypatch):
"""Pong carries previous_version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent records the pre-upgrade
version in previous-version.txt before overwriting version.txt, so the
GUI banner can show "upgraded from X to Y". Missing file → "" (dev runs
or first install), 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.61\n", encoding="utf-8")
(install / "previous-version.txt").write_text("0.2.57\n", encoding="utf-8")

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

server = _make_server()
assert server._previous_installed_version() == "0.2.57"

# Missing file → "" (no crash)
empty = tmp_path / "no-install"
monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty)
assert server._previous_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.61"
assert frame["previous_version"] == "0.2.57"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' emrg: GUI upgrade banner — show version range from→to (rant 2026-08-21T12:44:34) by argszero · Pull Request #913 · argszero/emrg · GitHub
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` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (989) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 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
5 changes: 3 additions & 2 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,6 +285,7 @@ vision = false
model: pong?.model || "",
evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong)
current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅)
previous_version: pong?.previous_version || "", // rant 12:44:34:升级前版本(横幅 from→to)
version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7)
sessions,
open_sessions: openSessionsList(),
Expand DownExpand Up@@ -871,7 +872,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
logger.info("[gui] connManager recovery complete");
} catch (e) {
logger.warn(`[gui] post-recovery refresh failed: ${e.message}`);
Expand DownExpand Up@@ -1098,7 +1099,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
}
}, delay);
}
Expand Down
18 changes: 14 additions & 4 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,7 @@ const App = (() => {
state.model = init.model || "";
state.version = init.version || "";
state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准)
state.previousVersion = init.previous_version || ""; // rant 12:44:34:升级前版本(横幅 from→to)
state.lastKnownVersion = state.currentVersion;
state.evolutionCount = init.evolution_count ?? null;
state.lastKnownEvolutionCount = state.evolutionCount;
Expand DownExpand Up@@ -1337,14 +1338,21 @@ const App = (() => {
$("conn-banner").classList.add("hidden");
}

// ── 升级完成横幅(rant 2026-08-20T18:30:57) ─────────────
function maybeShowUpgradeBanner(currentVersion) {
// ── 升级完成横幅(rant 2026-08-20T18:30:57 + 2026-08-21T12:44:34) ─────
function maybeShowUpgradeBanner(currentVersion, previousVersion) {
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 });
if (msg) {
// rant 12:44:34:daemon 提供升级前版本 → 显示 "from → to";否则回退旧文案
if (previousVersion && previousVersion !== currentVersion) {
msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: previousVersion, to: currentVersion });
} else {
msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion });
}
}
b.classList.remove("hidden");
state.lastKnownVersion = currentVersion; // 已提示,防重复弹
}
Expand DownExpand Up@@ -1574,7 +1582,9 @@ const App = (() => {
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);
// rant 12:44:34:同时携带 previous_version → 横幅显示 from → to
if (data.previous_version) state.previousVersion = data.previous_version;
maybeShowUpgradeBanner(data.current_version || state.currentVersion, state.previousVersion);
updateModelSwitcher();
updateGrowthCard();
maybeShowEvolutionToast();
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -415,6 +415,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "切换模型失败了:{msg}",
"app.modelListFailed": "读取模型列表失败了:{msg}",
"app.upgradeBannerMsg": "EMRG 已升级到 {version},重启后生效",
"app.upgradeBannerMsgFromTo": "EMRG 已从 {from} 升级到 {to},重启后生效",
"app.upgradeRestartFailed": "重启失败:{msg}",
"app.upgradeRestartBtn": "重启生效",
"app.unknownResult": "结果未知——连接中断",
Expand DownExpand Up@@ -826,6 +827,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "Failed to switch model: {msg}",
"app.modelListFailed": "Failed to load models: {msg}",
"app.upgradeBannerMsg": "EMRG upgraded to {version} — restart to apply",
"app.upgradeBannerMsgFromTo": "EMRG upgraded from {from} to {to} — restart to apply",
"app.upgradeRestartFailed": "Restart failed: {msg}",
"app.upgradeRestartBtn": "Restart to apply",
"app.unknownResult": "Result unknown — connection lost",
Expand Down
9 changes: 6 additions & 3 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -735,27 +735,30 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现
assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查");
});

test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现
test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现;有 previous_version → 显示 from→to
const { ctx } = makeSandbox({
init: async () => ({
config_exists: true,
api_key_configured: true,
current_version: "0.2.58",
previous_version: "",
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" } });
App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.61", previous_version: "0.2.57" } });
})()`, ctx);
const visible = vm.runInContext(
'!document.getElementById("upgrade-banner").classList.contains("hidden")',
ctx
);
assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现");
const bannerText = vm.runInContext('document.getElementById("upgrade-banner-msg").textContent', ctx);
assert.ok(bannerText.includes("0.2.57") && bannerText.includes("0.2.61"), `横幅应显示 from→to,实际 ${bannerText}`);

// 负态:版本未变 → 横幅保持隐藏
const { ctx: ctx2 } = makeSandbox({});
Expand Down
18 changes: 18 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,6 +645,21 @@ def _current_installed_version(self) -> str:
except (OSError, ValueError):
return ""

def _previous_installed_version(self) -> str:
"""Pre-upgrade EMRG version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent writes the version it is
replacing into previous-version.txt before overwriting version.txt,
so the GUI banner can show "upgraded from X to Y" instead of only the
target version. Raw data only; "" when missing (dev/standalone or
first install).
"""
try:
v = (Path.home() / ".emrg" / "install" / "previous-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@@ -1421,6 +1436,9 @@ async def _process_message(
# 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据,
# 零判断逻辑(升级判断由 GUI 负责)。
"current_version": self._current_installed_version(),
# Rant 2026-08-21T12:44:34:并入升级前版本——GUI 横幅显示 "from → to"
# (升级 agent 在覆盖 version.txt 前写入 previous-version.txt)。
"previous_version": self._previous_installed_version(),
})
return

Expand Down
5 changes: 5 additions & 0 deletions emrg/server/prompts/upgrade_prompt.j2
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,11 @@ release installer" means for this version.
update or add — the target tag's pyproject.toml dependencies are the
source of truth. Operate as needed (pip --target install/uninstall/update).
- `{{ install_dir }}/version.txt` ← {{ target_tag }} (the version).
- Before overwriting `version.txt`: if it already exists with a different
content, copy its current content to `{{ install_dir }}/previous-version.txt`
(rant 2026-08-21T12:44:34 — the GUI upgrade banner shows "from X to Y").
If `version.txt` did not exist (first install), delete any stale
`previous-version.txt`.
- `{{ install_dir }}/bin/` launcher scripts (emrg/emrgd/emrg.cmd/emrgd.cmd
etc.) — sync if the target tag changed them.
- GUI as needed: whether `~/Applications/EMRG.app` is updated is YOUR call —
Expand Down
35 changes: 35 additions & 0 deletions tests/test_daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1751,3 +1751,38 @@ def test_pong_includes_current_version(tmp_path, monkeypatch):
frame = _last_frame(writer)
assert frame["type"] == "pong"
assert frame["current_version"] == "0.2.59"


def test_pong_includes_previous_version(tmp_path, monkeypatch):
"""Pong carries previous_version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent records the pre-upgrade
version in previous-version.txt before overwriting version.txt, so the
GUI banner can show "upgraded from X to Y". Missing file → "" (dev runs
or first install), 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.61\n", encoding="utf-8")
(install / "previous-version.txt").write_text("0.2.57\n", encoding="utf-8")

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

server = _make_server()
assert server._previous_installed_version() == "0.2.57"

# Missing file → "" (no crash)
empty = tmp_path / "no-install"
monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty)
assert server._previous_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.61"
assert frame["previous_version"] == "0.2.57"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' emrg: GUI upgrade banner — show version range from→to (rant 2026-08-21T12:44:34) by argszero · Pull Request #913 · argszero/emrg · GitHub
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` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (989) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 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
5 changes: 3 additions & 2 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,6 +285,7 @@ vision = false
model: pong?.model || "",
evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong)
current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅)
previous_version: pong?.previous_version || "", // rant 12:44:34:升级前版本(横幅 from→to)
version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7)
sessions,
open_sessions: openSessionsList(),
Expand DownExpand Up@@ -871,7 +872,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
logger.info("[gui] connManager recovery complete");
} catch (e) {
logger.warn(`[gui] post-recovery refresh failed: ${e.message}`);
Expand DownExpand Up@@ -1098,7 +1099,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
}
}, delay);
}
Expand Down
18 changes: 14 additions & 4 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,7 @@ const App = (() => {
state.model = init.model || "";
state.version = init.version || "";
state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准)
state.previousVersion = init.previous_version || ""; // rant 12:44:34:升级前版本(横幅 from→to)
state.lastKnownVersion = state.currentVersion;
state.evolutionCount = init.evolution_count ?? null;
state.lastKnownEvolutionCount = state.evolutionCount;
Expand DownExpand Up@@ -1337,14 +1338,21 @@ const App = (() => {
$("conn-banner").classList.add("hidden");
}

// ── 升级完成横幅(rant 2026-08-20T18:30:57) ─────────────
function maybeShowUpgradeBanner(currentVersion) {
// ── 升级完成横幅(rant 2026-08-20T18:30:57 + 2026-08-21T12:44:34) ─────
function maybeShowUpgradeBanner(currentVersion, previousVersion) {
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 });
if (msg) {
// rant 12:44:34:daemon 提供升级前版本 → 显示 "from → to";否则回退旧文案
if (previousVersion && previousVersion !== currentVersion) {
msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: previousVersion, to: currentVersion });
} else {
msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion });
}
}
b.classList.remove("hidden");
state.lastKnownVersion = currentVersion; // 已提示,防重复弹
}
Expand DownExpand Up@@ -1574,7 +1582,9 @@ const App = (() => {
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);
// rant 12:44:34:同时携带 previous_version → 横幅显示 from → to
if (data.previous_version) state.previousVersion = data.previous_version;
maybeShowUpgradeBanner(data.current_version || state.currentVersion, state.previousVersion);
updateModelSwitcher();
updateGrowthCard();
maybeShowEvolutionToast();
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -415,6 +415,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "切换模型失败了:{msg}",
"app.modelListFailed": "读取模型列表失败了:{msg}",
"app.upgradeBannerMsg": "EMRG 已升级到 {version},重启后生效",
"app.upgradeBannerMsgFromTo": "EMRG 已从 {from} 升级到 {to},重启后生效",
"app.upgradeRestartFailed": "重启失败:{msg}",
"app.upgradeRestartBtn": "重启生效",
"app.unknownResult": "结果未知——连接中断",
Expand DownExpand Up@@ -826,6 +827,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "Failed to switch model: {msg}",
"app.modelListFailed": "Failed to load models: {msg}",
"app.upgradeBannerMsg": "EMRG upgraded to {version} — restart to apply",
"app.upgradeBannerMsgFromTo": "EMRG upgraded from {from} to {to} — restart to apply",
"app.upgradeRestartFailed": "Restart failed: {msg}",
"app.upgradeRestartBtn": "Restart to apply",
"app.unknownResult": "Result unknown — connection lost",
Expand Down
9 changes: 6 additions & 3 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -735,27 +735,30 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现
assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查");
});

test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现
test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现;有 previous_version → 显示 from→to
const { ctx } = makeSandbox({
init: async () => ({
config_exists: true,
api_key_configured: true,
current_version: "0.2.58",
previous_version: "",
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" } });
App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.61", previous_version: "0.2.57" } });
})()`, ctx);
const visible = vm.runInContext(
'!document.getElementById("upgrade-banner").classList.contains("hidden")',
ctx
);
assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现");
const bannerText = vm.runInContext('document.getElementById("upgrade-banner-msg").textContent', ctx);
assert.ok(bannerText.includes("0.2.57") && bannerText.includes("0.2.61"), `横幅应显示 from→to,实际 ${bannerText}`);

// 负态:版本未变 → 横幅保持隐藏
const { ctx: ctx2 } = makeSandbox({});
Expand Down
18 changes: 18 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,6 +645,21 @@ def _current_installed_version(self) -> str:
except (OSError, ValueError):
return ""

def _previous_installed_version(self) -> str:
"""Pre-upgrade EMRG version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent writes the version it is
replacing into previous-version.txt before overwriting version.txt,
so the GUI banner can show "upgraded from X to Y" instead of only the
target version. Raw data only; "" when missing (dev/standalone or
first install).
"""
try:
v = (Path.home() / ".emrg" / "install" / "previous-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@@ -1421,6 +1436,9 @@ async def _process_message(
# 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据,
# 零判断逻辑(升级判断由 GUI 负责)。
"current_version": self._current_installed_version(),
# Rant 2026-08-21T12:44:34:并入升级前版本——GUI 横幅显示 "from → to"
# (升级 agent 在覆盖 version.txt 前写入 previous-version.txt)。
"previous_version": self._previous_installed_version(),
})
return

Expand Down
5 changes: 5 additions & 0 deletions emrg/server/prompts/upgrade_prompt.j2
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,11 @@ release installer" means for this version.
update or add — the target tag's pyproject.toml dependencies are the
source of truth. Operate as needed (pip --target install/uninstall/update).
- `{{ install_dir }}/version.txt` ← {{ target_tag }} (the version).
- Before overwriting `version.txt`: if it already exists with a different
content, copy its current content to `{{ install_dir }}/previous-version.txt`
(rant 2026-08-21T12:44:34 — the GUI upgrade banner shows "from X to Y").
If `version.txt` did not exist (first install), delete any stale
`previous-version.txt`.
- `{{ install_dir }}/bin/` launcher scripts (emrg/emrgd/emrg.cmd/emrgd.cmd
etc.) — sync if the target tag changed them.
- GUI as needed: whether `~/Applications/EMRG.app` is updated is YOUR call —
Expand Down
35 changes: 35 additions & 0 deletions tests/test_daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1751,3 +1751,38 @@ def test_pong_includes_current_version(tmp_path, monkeypatch):
frame = _last_frame(writer)
assert frame["type"] == "pong"
assert frame["current_version"] == "0.2.59"


def test_pong_includes_previous_version(tmp_path, monkeypatch):
"""Pong carries previous_version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent records the pre-upgrade
version in previous-version.txt before overwriting version.txt, so the
GUI banner can show "upgraded from X to Y". Missing file → "" (dev runs
or first install), 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.61\n", encoding="utf-8")
(install / "previous-version.txt").write_text("0.2.57\n", encoding="utf-8")

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

server = _make_server()
assert server._previous_installed_version() == "0.2.57"

# Missing file → "" (no crash)
empty = tmp_path / "no-install"
monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty)
assert server._previous_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.61"
assert frame["previous_version"] == "0.2.57"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' emrg: GUI upgrade banner — show version range from→to (rant 2026-08-21T12:44:34) by argszero · Pull Request #913 · argszero/emrg · GitHub
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` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (989) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 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
5 changes: 3 additions & 2 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,6 +285,7 @@ vision = false
model: pong?.model || "",
evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong)
current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅)
previous_version: pong?.previous_version || "", // rant 12:44:34:升级前版本(横幅 from→to)
version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7)
sessions,
open_sessions: openSessionsList(),
Expand DownExpand Up@@ -871,7 +872,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
logger.info("[gui] connManager recovery complete");
} catch (e) {
logger.warn(`[gui] post-recovery refresh failed: ${e.message}`);
Expand DownExpand Up@@ -1098,7 +1099,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
}
}, delay);
}
Expand Down
18 changes: 14 additions & 4 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,7 @@ const App = (() => {
state.model = init.model || "";
state.version = init.version || "";
state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准)
state.previousVersion = init.previous_version || ""; // rant 12:44:34:升级前版本(横幅 from→to)
state.lastKnownVersion = state.currentVersion;
state.evolutionCount = init.evolution_count ?? null;
state.lastKnownEvolutionCount = state.evolutionCount;
Expand DownExpand Up@@ -1337,14 +1338,21 @@ const App = (() => {
$("conn-banner").classList.add("hidden");
}

// ── 升级完成横幅(rant 2026-08-20T18:30:57) ─────────────
function maybeShowUpgradeBanner(currentVersion) {
// ── 升级完成横幅(rant 2026-08-20T18:30:57 + 2026-08-21T12:44:34) ─────
function maybeShowUpgradeBanner(currentVersion, previousVersion) {
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 });
if (msg) {
// rant 12:44:34:daemon 提供升级前版本 → 显示 "from → to";否则回退旧文案
if (previousVersion && previousVersion !== currentVersion) {
msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: previousVersion, to: currentVersion });
} else {
msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion });
}
}
b.classList.remove("hidden");
state.lastKnownVersion = currentVersion; // 已提示,防重复弹
}
Expand DownExpand Up@@ -1574,7 +1582,9 @@ const App = (() => {
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);
// rant 12:44:34:同时携带 previous_version → 横幅显示 from → to
if (data.previous_version) state.previousVersion = data.previous_version;
maybeShowUpgradeBanner(data.current_version || state.currentVersion, state.previousVersion);
updateModelSwitcher();
updateGrowthCard();
maybeShowEvolutionToast();
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -415,6 +415,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "切换模型失败了:{msg}",
"app.modelListFailed": "读取模型列表失败了:{msg}",
"app.upgradeBannerMsg": "EMRG 已升级到 {version},重启后生效",
"app.upgradeBannerMsgFromTo": "EMRG 已从 {from} 升级到 {to},重启后生效",
"app.upgradeRestartFailed": "重启失败:{msg}",
"app.upgradeRestartBtn": "重启生效",
"app.unknownResult": "结果未知——连接中断",
Expand DownExpand Up@@ -826,6 +827,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "Failed to switch model: {msg}",
"app.modelListFailed": "Failed to load models: {msg}",
"app.upgradeBannerMsg": "EMRG upgraded to {version} — restart to apply",
"app.upgradeBannerMsgFromTo": "EMRG upgraded from {from} to {to} — restart to apply",
"app.upgradeRestartFailed": "Restart failed: {msg}",
"app.upgradeRestartBtn": "Restart to apply",
"app.unknownResult": "Result unknown — connection lost",
Expand Down
9 changes: 6 additions & 3 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -735,27 +735,30 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现
assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查");
});

test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现
test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现;有 previous_version → 显示 from→to
const { ctx } = makeSandbox({
init: async () => ({
config_exists: true,
api_key_configured: true,
current_version: "0.2.58",
previous_version: "",
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" } });
App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.61", previous_version: "0.2.57" } });
})()`, ctx);
const visible = vm.runInContext(
'!document.getElementById("upgrade-banner").classList.contains("hidden")',
ctx
);
assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现");
const bannerText = vm.runInContext('document.getElementById("upgrade-banner-msg").textContent', ctx);
assert.ok(bannerText.includes("0.2.57") && bannerText.includes("0.2.61"), `横幅应显示 from→to,实际 ${bannerText}`);

// 负态:版本未变 → 横幅保持隐藏
const { ctx: ctx2 } = makeSandbox({});
Expand Down
18 changes: 18 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,6 +645,21 @@ def _current_installed_version(self) -> str:
except (OSError, ValueError):
return ""

def _previous_installed_version(self) -> str:
"""Pre-upgrade EMRG version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent writes the version it is
replacing into previous-version.txt before overwriting version.txt,
so the GUI banner can show "upgraded from X to Y" instead of only the
target version. Raw data only; "" when missing (dev/standalone or
first install).
"""
try:
v = (Path.home() / ".emrg" / "install" / "previous-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@@ -1421,6 +1436,9 @@ async def _process_message(
# 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据,
# 零判断逻辑(升级判断由 GUI 负责)。
"current_version": self._current_installed_version(),
# Rant 2026-08-21T12:44:34:并入升级前版本——GUI 横幅显示 "from → to"
# (升级 agent 在覆盖 version.txt 前写入 previous-version.txt)。
"previous_version": self._previous_installed_version(),
})
return

Expand Down
5 changes: 5 additions & 0 deletions emrg/server/prompts/upgrade_prompt.j2
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,11 @@ release installer" means for this version.
update or add — the target tag's pyproject.toml dependencies are the
source of truth. Operate as needed (pip --target install/uninstall/update).
- `{{ install_dir }}/version.txt` ← {{ target_tag }} (the version).
- Before overwriting `version.txt`: if it already exists with a different
content, copy its current content to `{{ install_dir }}/previous-version.txt`
(rant 2026-08-21T12:44:34 — the GUI upgrade banner shows "from X to Y").
If `version.txt` did not exist (first install), delete any stale
`previous-version.txt`.
- `{{ install_dir }}/bin/` launcher scripts (emrg/emrgd/emrg.cmd/emrgd.cmd
etc.) — sync if the target tag changed them.
- GUI as needed: whether `~/Applications/EMRG.app` is updated is YOUR call —
Expand Down
35 changes: 35 additions & 0 deletions tests/test_daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1751,3 +1751,38 @@ def test_pong_includes_current_version(tmp_path, monkeypatch):
frame = _last_frame(writer)
assert frame["type"] == "pong"
assert frame["current_version"] == "0.2.59"


def test_pong_includes_previous_version(tmp_path, monkeypatch):
"""Pong carries previous_version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent records the pre-upgrade
version in previous-version.txt before overwriting version.txt, so the
GUI banner can show "upgraded from X to Y". Missing file → "" (dev runs
or first install), 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.61\n", encoding="utf-8")
(install / "previous-version.txt").write_text("0.2.57\n", encoding="utf-8")

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

server = _make_server()
assert server._previous_installed_version() == "0.2.57"

# Missing file → "" (no crash)
empty = tmp_path / "no-install"
monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty)
assert server._previous_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.61"
assert frame["previous_version"] == "0.2.57"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' emrg: GUI upgrade banner — show version range from→to (rant 2026-08-21T12:44:34) by argszero · Pull Request #913 · argszero/emrg · GitHub
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` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (989) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 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
5 changes: 3 additions & 2 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,6 +285,7 @@ vision = false
model: pong?.model || "",
evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong)
current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅)
previous_version: pong?.previous_version || "", // rant 12:44:34:升级前版本(横幅 from→to)
version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7)
sessions,
open_sessions: openSessionsList(),
Expand DownExpand Up@@ -871,7 +872,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
logger.info("[gui] connManager recovery complete");
} catch (e) {
logger.warn(`[gui] post-recovery refresh failed: ${e.message}`);
Expand DownExpand Up@@ -1098,7 +1099,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
}
}, delay);
}
Expand Down
18 changes: 14 additions & 4 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,7 @@ const App = (() => {
state.model = init.model || "";
state.version = init.version || "";
state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准)
state.previousVersion = init.previous_version || ""; // rant 12:44:34:升级前版本(横幅 from→to)
state.lastKnownVersion = state.currentVersion;
state.evolutionCount = init.evolution_count ?? null;
state.lastKnownEvolutionCount = state.evolutionCount;
Expand DownExpand Up@@ -1337,14 +1338,21 @@ const App = (() => {
$("conn-banner").classList.add("hidden");
}

// ── 升级完成横幅(rant 2026-08-20T18:30:57) ─────────────
function maybeShowUpgradeBanner(currentVersion) {
// ── 升级完成横幅(rant 2026-08-20T18:30:57 + 2026-08-21T12:44:34) ─────
function maybeShowUpgradeBanner(currentVersion, previousVersion) {
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 });
if (msg) {
// rant 12:44:34:daemon 提供升级前版本 → 显示 "from → to";否则回退旧文案
if (previousVersion && previousVersion !== currentVersion) {
msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: previousVersion, to: currentVersion });
} else {
msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion });
}
}
b.classList.remove("hidden");
state.lastKnownVersion = currentVersion; // 已提示,防重复弹
}
Expand DownExpand Up@@ -1574,7 +1582,9 @@ const App = (() => {
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);
// rant 12:44:34:同时携带 previous_version → 横幅显示 from → to
if (data.previous_version) state.previousVersion = data.previous_version;
maybeShowUpgradeBanner(data.current_version || state.currentVersion, state.previousVersion);
updateModelSwitcher();
updateGrowthCard();
maybeShowEvolutionToast();
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -415,6 +415,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "切换模型失败了:{msg}",
"app.modelListFailed": "读取模型列表失败了:{msg}",
"app.upgradeBannerMsg": "EMRG 已升级到 {version},重启后生效",
"app.upgradeBannerMsgFromTo": "EMRG 已从 {from} 升级到 {to},重启后生效",
"app.upgradeRestartFailed": "重启失败:{msg}",
"app.upgradeRestartBtn": "重启生效",
"app.unknownResult": "结果未知——连接中断",
Expand DownExpand Up@@ -826,6 +827,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "Failed to switch model: {msg}",
"app.modelListFailed": "Failed to load models: {msg}",
"app.upgradeBannerMsg": "EMRG upgraded to {version} — restart to apply",
"app.upgradeBannerMsgFromTo": "EMRG upgraded from {from} to {to} — restart to apply",
"app.upgradeRestartFailed": "Restart failed: {msg}",
"app.upgradeRestartBtn": "Restart to apply",
"app.unknownResult": "Result unknown — connection lost",
Expand Down
9 changes: 6 additions & 3 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -735,27 +735,30 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现
assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查");
});

test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现
test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现;有 previous_version → 显示 from→to
const { ctx } = makeSandbox({
init: async () => ({
config_exists: true,
api_key_configured: true,
current_version: "0.2.58",
previous_version: "",
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" } });
App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.61", previous_version: "0.2.57" } });
})()`, ctx);
const visible = vm.runInContext(
'!document.getElementById("upgrade-banner").classList.contains("hidden")',
ctx
);
assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现");
const bannerText = vm.runInContext('document.getElementById("upgrade-banner-msg").textContent', ctx);
assert.ok(bannerText.includes("0.2.57") && bannerText.includes("0.2.61"), `横幅应显示 from→to,实际 ${bannerText}`);

// 负态:版本未变 → 横幅保持隐藏
const { ctx: ctx2 } = makeSandbox({});
Expand Down
18 changes: 18 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,6 +645,21 @@ def _current_installed_version(self) -> str:
except (OSError, ValueError):
return ""

def _previous_installed_version(self) -> str:
"""Pre-upgrade EMRG version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent writes the version it is
replacing into previous-version.txt before overwriting version.txt,
so the GUI banner can show "upgraded from X to Y" instead of only the
target version. Raw data only; "" when missing (dev/standalone or
first install).
"""
try:
v = (Path.home() / ".emrg" / "install" / "previous-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@@ -1421,6 +1436,9 @@ async def _process_message(
# 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据,
# 零判断逻辑(升级判断由 GUI 负责)。
"current_version": self._current_installed_version(),
# Rant 2026-08-21T12:44:34:并入升级前版本——GUI 横幅显示 "from → to"
# (升级 agent 在覆盖 version.txt 前写入 previous-version.txt)。
"previous_version": self._previous_installed_version(),
})
return

Expand Down
5 changes: 5 additions & 0 deletions emrg/server/prompts/upgrade_prompt.j2
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,11 @@ release installer" means for this version.
update or add — the target tag's pyproject.toml dependencies are the
source of truth. Operate as needed (pip --target install/uninstall/update).
- `{{ install_dir }}/version.txt` ← {{ target_tag }} (the version).
- Before overwriting `version.txt`: if it already exists with a different
content, copy its current content to `{{ install_dir }}/previous-version.txt`
(rant 2026-08-21T12:44:34 — the GUI upgrade banner shows "from X to Y").
If `version.txt` did not exist (first install), delete any stale
`previous-version.txt`.
- `{{ install_dir }}/bin/` launcher scripts (emrg/emrgd/emrg.cmd/emrgd.cmd
etc.) — sync if the target tag changed them.
- GUI as needed: whether `~/Applications/EMRG.app` is updated is YOUR call —
Expand Down
35 changes: 35 additions & 0 deletions tests/test_daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1751,3 +1751,38 @@ def test_pong_includes_current_version(tmp_path, monkeypatch):
frame = _last_frame(writer)
assert frame["type"] == "pong"
assert frame["current_version"] == "0.2.59"


def test_pong_includes_previous_version(tmp_path, monkeypatch):
"""Pong carries previous_version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent records the pre-upgrade
version in previous-version.txt before overwriting version.txt, so the
GUI banner can show "upgraded from X to Y". Missing file → "" (dev runs
or first install), 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.61\n", encoding="utf-8")
(install / "previous-version.txt").write_text("0.2.57\n", encoding="utf-8")

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

server = _make_server()
assert server._previous_installed_version() == "0.2.57"

# Missing file → "" (no crash)
empty = tmp_path / "no-install"
monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty)
assert server._previous_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.61"
assert frame["previous_version"] == "0.2.57"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); emrg: GUI upgrade banner — show version range from→to (rant 2026-08-21T12:44:34) by argszero · Pull Request #913 · argszero/emrg · GitHub
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` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (989) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 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
5 changes: 3 additions & 2 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,6 +285,7 @@ vision = false
model: pong?.model || "",
evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong)
current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅)
previous_version: pong?.previous_version || "", // rant 12:44:34:升级前版本(横幅 from→to)
version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7)
sessions,
open_sessions: openSessionsList(),
Expand DownExpand Up@@ -871,7 +872,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
logger.info("[gui] connManager recovery complete");
} catch (e) {
logger.warn(`[gui] post-recovery refresh failed: ${e.message}`);
Expand DownExpand Up@@ -1098,7 +1099,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, current_version: pong?.current_version || "" });
sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" });
}
}, delay);
}
Expand Down
18 changes: 14 additions & 4 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,7 @@ const App = (() => {
state.model = init.model || "";
state.version = init.version || "";
state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准)
state.previousVersion = init.previous_version || ""; // rant 12:44:34:升级前版本(横幅 from→to)
state.lastKnownVersion = state.currentVersion;
state.evolutionCount = init.evolution_count ?? null;
state.lastKnownEvolutionCount = state.evolutionCount;
Expand DownExpand Up@@ -1337,14 +1338,21 @@ const App = (() => {
$("conn-banner").classList.add("hidden");
}

// ── 升级完成横幅(rant 2026-08-20T18:30:57) ─────────────
function maybeShowUpgradeBanner(currentVersion) {
// ── 升级完成横幅(rant 2026-08-20T18:30:57 + 2026-08-21T12:44:34) ─────
function maybeShowUpgradeBanner(currentVersion, previousVersion) {
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 });
if (msg) {
// rant 12:44:34:daemon 提供升级前版本 → 显示 "from → to";否则回退旧文案
if (previousVersion && previousVersion !== currentVersion) {
msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: previousVersion, to: currentVersion });
} else {
msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion });
}
}
b.classList.remove("hidden");
state.lastKnownVersion = currentVersion; // 已提示,防重复弹
}
Expand DownExpand Up@@ -1574,7 +1582,9 @@ const App = (() => {
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);
// rant 12:44:34:同时携带 previous_version → 横幅显示 from → to
if (data.previous_version) state.previousVersion = data.previous_version;
maybeShowUpgradeBanner(data.current_version || state.currentVersion, state.previousVersion);
updateModelSwitcher();
updateGrowthCard();
maybeShowEvolutionToast();
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -415,6 +415,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "切换模型失败了:{msg}",
"app.modelListFailed": "读取模型列表失败了:{msg}",
"app.upgradeBannerMsg": "EMRG 已升级到 {version},重启后生效",
"app.upgradeBannerMsgFromTo": "EMRG 已从 {from} 升级到 {to},重启后生效",
"app.upgradeRestartFailed": "重启失败:{msg}",
"app.upgradeRestartBtn": "重启生效",
"app.unknownResult": "结果未知——连接中断",
Expand DownExpand Up@@ -826,6 +827,7 @@ const I18N = (() => {
"app.modelSwitchFailed": "Failed to switch model: {msg}",
"app.modelListFailed": "Failed to load models: {msg}",
"app.upgradeBannerMsg": "EMRG upgraded to {version} — restart to apply",
"app.upgradeBannerMsgFromTo": "EMRG upgraded from {from} to {to} — restart to apply",
"app.upgradeRestartFailed": "Restart failed: {msg}",
"app.upgradeRestartBtn": "Restart to apply",
"app.unknownResult": "Result unknown — connection lost",
Expand Down
9 changes: 6 additions & 3 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -735,27 +735,30 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现
assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查");
});

test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现
test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ 重启按钮触发 restartDaemon(正反两态)", async () => {
// 正态:status current_version 与已知版本不同 → 横幅出现;有 previous_version → 显示 from→to
const { ctx } = makeSandbox({
init: async () => ({
config_exists: true,
api_key_configured: true,
current_version: "0.2.58",
previous_version: "",
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" } });
App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.61", previous_version: "0.2.57" } });
})()`, ctx);
const visible = vm.runInContext(
'!document.getElementById("upgrade-banner").classList.contains("hidden")',
ctx
);
assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现");
const bannerText = vm.runInContext('document.getElementById("upgrade-banner-msg").textContent', ctx);
assert.ok(bannerText.includes("0.2.57") && bannerText.includes("0.2.61"), `横幅应显示 from→to,实际 ${bannerText}`);

// 负态:版本未变 → 横幅保持隐藏
const { ctx: ctx2 } = makeSandbox({});
Expand Down
18 changes: 18 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,6 +645,21 @@ def _current_installed_version(self) -> str:
except (OSError, ValueError):
return ""

def _previous_installed_version(self) -> str:
"""Pre-upgrade EMRG version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent writes the version it is
replacing into previous-version.txt before overwriting version.txt,
so the GUI banner can show "upgraded from X to Y" instead of only the
target version. Raw data only; "" when missing (dev/standalone or
first install).
"""
try:
v = (Path.home() / ".emrg" / "install" / "previous-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@@ -1421,6 +1436,9 @@ async def _process_message(
# 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据,
# 零判断逻辑(升级判断由 GUI 负责)。
"current_version": self._current_installed_version(),
# Rant 2026-08-21T12:44:34:并入升级前版本——GUI 横幅显示 "from → to"
# (升级 agent 在覆盖 version.txt 前写入 previous-version.txt)。
"previous_version": self._previous_installed_version(),
})
return

Expand Down
5 changes: 5 additions & 0 deletions emrg/server/prompts/upgrade_prompt.j2
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,11 @@ release installer" means for this version.
update or add — the target tag's pyproject.toml dependencies are the
source of truth. Operate as needed (pip --target install/uninstall/update).
- `{{ install_dir }}/version.txt` ← {{ target_tag }} (the version).
- Before overwriting `version.txt`: if it already exists with a different
content, copy its current content to `{{ install_dir }}/previous-version.txt`
(rant 2026-08-21T12:44:34 — the GUI upgrade banner shows "from X to Y").
If `version.txt` did not exist (first install), delete any stale
`previous-version.txt`.
- `{{ install_dir }}/bin/` launcher scripts (emrg/emrgd/emrg.cmd/emrgd.cmd
etc.) — sync if the target tag changed them.
- GUI as needed: whether `~/Applications/EMRG.app` is updated is YOUR call —
Expand Down
35 changes: 35 additions & 0 deletions tests/test_daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1751,3 +1751,38 @@ def test_pong_includes_current_version(tmp_path, monkeypatch):
frame = _last_frame(writer)
assert frame["type"] == "pong"
assert frame["current_version"] == "0.2.59"


def test_pong_includes_previous_version(tmp_path, monkeypatch):
"""Pong carries previous_version from ~/.emrg/install/previous-version.txt.

Rant 2026-08-21T12:44:34: the upgrade agent records the pre-upgrade
version in previous-version.txt before overwriting version.txt, so the
GUI banner can show "upgraded from X to Y". Missing file → "" (dev runs
or first install), 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.61\n", encoding="utf-8")
(install / "previous-version.txt").write_text("0.2.57\n", encoding="utf-8")

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

server = _make_server()
assert server._previous_installed_version() == "0.2.57"

# Missing file → "" (no crash)
empty = tmp_path / "no-install"
monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty)
assert server._previous_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.61"
assert frame["previous_version"] == "0.2.57"
Loading