Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request
- Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand)
- Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端"
- Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout)
- Unit tests `npm test` (218: 43 daemon_client + 19 conn-manager + 22 app-commands + 97 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (221: 43 daemon_client + 19 conn-manager + 22 app-commands + 100 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Auto project tracking** — Automatically detects and records working directories; project-scoped sessions
- **Rant-driven evolution** — User feedback via `/rant` drives automatic self-improvement cycles
- **Headless GitHub auth** — Non-interactive evolution auto-extracts `GH_TOKEN` from git credential store (osxkeychain / credential helper); PR comment/LGTM queries fall back to REST API (GraphQL needs `read:org` scope)
Expand All @@ -112,8 +112,8 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (705) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (218: 43 daemon_client + 19 conn-manager + 22 app-commands + 97 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
Python: `uv run pytest tests/ -v` (729) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (221: 43 daemon_client + 19 conn-manager + 22 app-commands + 100 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)

Expand Down
26 changes: 17 additions & 9 deletions emrg/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,21 @@ class LlmConfig:

@dataclass
class UpdateConfig:
"""Auto update-check settings (rant 2026-08-10T07:12:12).

check: master switch — when false, the daemon never queries GitHub.
ttl_hours: how often to re-check the latest release (default 24h).
Prompting is always display-only (no auto download/install).
"""Auto update-check settings (rants 2026-08-10T07:12:12, 2026-08-12T12:10:12).

check: master switch — when false, the daemon never queries GitHub
(checking, downloading and prompting are all disabled).
ttl_hours: how often to re-check the latest release (default 1h;
rant 2026-08-12T12:10:12: host 指示 24h → 1h).
auto_download: when a newer version exists, download the current
platform's installer into ~/.emrg/updates/ in the background
(stream + Range resume + SHA256 verify). Installation is ALWAYS
user-initiated — never auto-installed.
"""

check: bool = True
ttl_hours: int = 24
ttl_hours: int = 1
auto_download: bool = True


@dataclass
Expand Down Expand Up @@ -109,7 +115,8 @@ def load_config() -> EmrgConfig:
update_data = data.get("update", {})
update = UpdateConfig(
check=update_data.get("check", True),
ttl_hours=update_data.get("ttl_hours", 24),
ttl_hours=update_data.get("ttl_hours", 1),
auto_download=update_data.get("auto_download", True),
)

return EmrgConfig(llm=llm, update=update)
Expand All @@ -120,7 +127,7 @@ def load_update_config() -> UpdateConfig:

The daemon is constructed with just LlmConfig; this helper lets it read
the update-check switch without parsing the full config. Missing config
file or missing section → defaults (check=True, ttl=24h).
file or missing section → defaults (check=True, ttl=1h, auto_download=True).
"""
cfg_path = config_path()
if not cfg_path.exists():
Expand All @@ -132,7 +139,8 @@ def load_update_config() -> UpdateConfig:
update_data = data.get("update", {})
return UpdateConfig(
check=update_data.get("check", True),
ttl_hours=update_data.get("ttl_hours", 24),
ttl_hours=update_data.get("ttl_hours", 1),
auto_download=update_data.get("auto_download", True),
)


Expand Down
46 changes: 42 additions & 4 deletions emrg/gui/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -636,10 +636,11 @@ vision = false
});

ipcMain.handle("emrg:updateCheck", async (_e, { force } = {}) => {
// Auto update-check prompt (rant 2026-08-10T07:12:12): query daemon's
// cached latest release; display-only, no auto download/install.
// force=true (rant 2026-08-11T09:18:16): settings manual check button
// — daemon runs a fresh GitHub fetch instead of returning the cache.
// Auto update-check (rant 2026-08-10T07:12:12): query daemon's cached
// latest release; force=true (rant 2026-08-11T09:18:16): settings
// manual check button — daemon runs a fresh GitHub fetch.
// rant 2026-08-12T12:10:12: response also carries the auto-download
// state (downloaded_version/path/sha) for the "ready to install" UI.
try {
const frame = await requireConn().sendCommandAndWait(
"update_check",
Expand All @@ -651,13 +652,50 @@ vision = false
latest_version: frame.latest_version || "",
has_update: Boolean(frame.has_update),
prompted_version: frame.prompted_version || "",
downloaded_version: frame.downloaded_version || "",
downloaded_path: frame.downloaded_path || "",
downloaded_sha: frame.downloaded_sha || "",
enabled: Boolean(frame.enabled),
};
} catch {
return { has_update: false, enabled: false, latest_version: "", current_version: "" };
}
});

ipcMain.handle("emrg:updateInstall", async (_e, { path: p, version } = {}) => {
// Auto-update one-click install (rant 2026-08-12T12:10:12): the daemon
// already downloaded + SHA256-verified the installer into
// ~/.emrg/updates/. The user clicked "install" — launch the installer,
// then quit EMRG (the installer stops any remaining EMRG processes
// itself — stop-emrg.cmd / pkg install logic). Install is ALWAYS
// user-initiated; the download itself was automatic.
try {
if (!p || typeof p !== "string") return { ok: false, error: "missing path" };
if (!fs.existsSync(p)) return { ok: false, error: "downloaded installer not found" };
const platform = process.platform;
if (platform === "win32") {
// PrivilegesRequired=lowest → no UAC prompt for the exe itself.
const child = spawn(p, [], { detached: true, stdio: "ignore", windowsHide: true });
child.unref();
} else if (platform === "darwin") {
// `open <pkg>` mounts + launches the Installer.app flow.
const child = spawn("open", [p], { detached: true, stdio: "ignore" });
child.unref();
} else if (platform === "linux") {
fs.chmodSync(p, 0o755);
const child = spawn(p, [], { detached: true, stdio: "ignore" });
child.unref();
} else {
return { ok: false, error: `unsupported platform: ${platform}` };
}
// Let the IPC reply flush, then close the GUI — the installer takes over.
setTimeout(() => { try { app.quit(); } catch { /* ignore */ } }, 500);
return { ok: true, version: String(version || "") };
} catch (err) {
return { ok: false, error: String((err && err.message) || err) };
}
});

ipcMain.handle("emrg:updateCheckPrompted", async (_e, { version }) => {
// Idempotency (rant 07:12:12 §4): record that the GUI showed the prompt
// for this version — same version never re-prompted.
Expand Down
1 change: 1 addition & 0 deletions emrg/gui/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const api = {
githubStatus: () => ipcRenderer.invoke("emrg:githubStatus"),
updateCheck: (payload) => ipcRenderer.invoke("emrg:updateCheck", payload),
updateCheckPrompted: (payload) => ipcRenderer.invoke("emrg:updateCheckPrompted", payload),
updateInstall: (payload) => ipcRenderer.invoke("emrg:updateInstall", payload),
githubConnect: (payload) => ipcRenderer.invoke("emrg:githubConnect", payload),
githubDisconnect: () => ipcRenderer.invoke("emrg:githubDisconnect"),
githubConnectWeb: () => ipcRenderer.invoke("emrg:githubConnectWeb"),
Expand Down
7 changes: 7 additions & 0 deletions emrg/gui/renderer/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,13 @@ const App = (() => {
}
}
break;
case "update_downloaded":
// rant 2026-08-12T12:10:12:daemon 后台自动下载 + 校验完新安装包 →
// 非阻塞提示"已就绪,点击安装"(设置 → 关于显示安装按钮)
Chat.addSystemMessage(
_t("app.updateReady", { latest: data.downloaded_version || "" }),
);
break;
case "group_cleared":
Chat.groupNodes.delete(data.requestId);
break;
Expand Down
87 changes: 69 additions & 18 deletions emrg/gui/renderer/js/dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -491,42 +491,82 @@ const Dialogs = (() => {
}

// Auto update-check prompt (rant 2026-08-10T07:12:12): display-only, one
// line in the about area, never a modal — no auto download/install.
// line in the about area, never a modal.
// force (rant 2026-08-11T09:18:16): settings manual check button — run a
// fresh GitHub fetch instead of returning the daemon's cached result.
// rant 2026-08-12T12:10:12: when the daemon already auto-downloaded a
// verified installer (downloaded_version), show a one-click install button
// instead of the plain Releases link.
// ⚠️ 局部变量命名 updEl(勿用 el——会遮蔽模块级 el() 元素工厂,el("a",…)
// 抛 TypeError → catch 吞掉 → 更新行永远 hidden,正是 #602 隐藏缺陷)
async function refreshUpdateCheck({ force = false } = {}) {
const updEl = $("about-update");
if (!updEl) return; // 元素缺失(测试桩)时忽略
try {
const u = await window.emrg.updateCheck({ force });
if (!u || !u.enabled || !u.has_update || !u.latest_version) {
if (!u || !u.enabled) {
updEl.classList.add("hidden");
updEl.textContent = "";
return;
}
if (u.latest_version === u.prompted_version) {
updEl.classList.add("hidden");
updEl.textContent = "";
return;
}
const link = el("a", {
href: "https://github.com/argszero/emrg/releases",
target: "_blank",
rel: "noopener",
}, _t("settings.updateAvailable", { latest: u.latest_version }));
updEl.textContent = "";
updEl.appendChild(link);
updEl.classList.remove("hidden");
// 幂等:同版本只提示一次
try { await window.emrg.updateCheckPrompted({ version: u.latest_version }); } catch { /* ignore */ }
let shown = false;
// ① 已自动下载 + 校验通过 → 一键安装按钮(rant 2026-08-12T12:10:12)
if (u.downloaded_version && u.downloaded_version !== u.current_version) {
const btn = el("button", {
type: "button",
class: "btn btn-sm btn-primary",
}, _t("settings.updateReady", { latest: u.downloaded_version }));
btn.addEventListener("click", () => showUpdateInstallConfirm(u));
updEl.appendChild(btn);
shown = true;
}
// ② 有新版但未下载 → Releases 链接(幂等:同版本只提示一次)
if (!shown && u.has_update && u.latest_version && u.latest_version !== u.prompted_version) {
const link = el("a", {
href: "https://github.com/argszero/emrg/releases",
target: "_blank",
rel: "noopener",
}, _t("settings.updateAvailable", { latest: u.latest_version }));
updEl.appendChild(link);
shown = true;
try { await window.emrg.updateCheckPrompted({ version: u.latest_version }); } catch { /* ignore */ }
}
updEl.classList.toggle("hidden", !shown);
} catch {
updEl.classList.add("hidden");
updEl.textContent = "";
}
}

// Auto-update one-click install (rant 2026-08-12T12:10:12): the daemon
// downloaded + SHA256-verified the installer; the user clicks install →
// confirm (SmartScreen hint on Windows) → launch installer + quit EMRG
// (the installer stops any remaining EMRG processes itself).
function showUpdateInstallConfirm(u) {
const isWin = /win/i.test((navigator.platform || "") + (navigator.userAgent || ""));
showConfirm(
_t("settings.installTitle"),
_t(isWin ? "settings.installConfirmWin" : "settings.installConfirm", { latest: u.downloaded_version || "" }),
{
okText: _t("settings.installNow"),
danger: false,
onOk: async () => {
try {
const res = await window.emrg.updateInstall({ path: u.downloaded_path, version: u.downloaded_version });
if (res && res.ok) {
Chat.addSystemMessage(_t("settings.installStarted", { latest: u.downloaded_version || "" }));
} else {
Chat.addSystemMessage(_t("settings.installFailed", { msg: (res && res.error) || "" }));
}
} catch {
Chat.addSystemMessage(_t("settings.installFailed", { msg: "" }));
}
},
},
);
}

// 设置页"检查更新"手动按钮(rant 2026-08-11T09:18:16):点击立即强制
// 重新检查(不等 TTL 轮询),检查中显示"检查中…",完成后恢复按钮。
function initUpdateCheckButton() {
Expand All @@ -547,11 +587,21 @@ const Dialogs = (() => {

// 启动主动更新提示(rant 2026-08-11T09:18:16):GUI 启动成功路径调用,
// 不依赖打开设置对话框——有新版本且未提示过时输出一条非阻塞系统消息
// (对齐 TUI 启动 system 行)。boot 时 daemon 可能未就绪 → 静默失败。
// (对齐 TUI 启动 system 行)。rant 2026-08-12T12:10:12:若 daemon 已
// 自动下载好安装包,则提示"已就绪,点击安装"。boot 时 daemon 可能未
// 就绪 → 静默失败。
async function promptUpdateAtStartup() {
try {
const u = await window.emrg.updateCheck({ force: false });
if (!u || !u.enabled || !u.has_update || !u.latest_version) return;
if (!u || !u.enabled) return;
// 已自动下载 + 校验通过 → "已就绪,点击安装"(设置 → 关于)
if (u.downloaded_version && u.downloaded_version !== u.current_version) {
Chat.addSystemMessage(
_t("app.updateReady", { latest: u.downloaded_version }),
);
return;
}
if (!u.has_update || !u.latest_version) return;
if (u.latest_version === u.prompted_version) return;
Chat.addSystemMessage(
_t("app.updateAvailable", { latest: u.latest_version }),
Expand Down Expand Up @@ -773,6 +823,7 @@ const Dialogs = (() => {
refreshUpdateCheck,
initUpdateCheckButton,
promptUpdateAtStartup,
showUpdateInstallConfirm, // rant 12:10:12:已下载安装包 → 一键安装确认
initOpenSessionDialog, // P5:打开会话对话框初始化
showOpenSessionDialog, // P5:两步打开会话
initNewSessionDialog, // P5 slice 2:新建会话对话框初始化
Expand Down
16 changes: 16 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ const I18N = (() => {
"settings.updateAvailable": "发现新版本 v{latest} —— 点击前往 Releases 下载(不会自动安装)",
"settings.checkUpdate": "检查更新",
"settings.checkingUpdate": "检查中…",
"settings.updateReady": "新版本 v{latest} 已就绪 —— 点击安装",
"settings.installNow": "点击安装",
"settings.installTitle": "安装新版本",
"settings.installConfirm": "安装 v{latest}?EMRG 将自动退出,由安装器接管。",
"settings.installConfirmWin": "安装 v{latest}?EMRG 将自动退出。若出现 SmartScreen 提示,点击「更多信息 → 仍要运行」。",
"settings.installStarted": "正在启动安装 v{latest}…",
"settings.installFailed": "安装启动失败:{msg}",
"app.updateReady": "新版本 v{latest} 已下载 —— 设置 → 关于 → 点击安装",
"app.updateAvailable": "发现新版本 v{latest} —— 点击前往 Releases 下载(不会自动安装):https://github.com/argszero/emrg/releases",
"settings.githubTokenEmpty": "请先粘贴 GitHub Personal Access Token",
"settings.githubConnecting": "连接中…",
Expand Down Expand Up @@ -449,6 +457,14 @@ const I18N = (() => {
"settings.updateAvailable": "New version v{latest} available — click to visit Releases (no auto-install)",
"settings.checkUpdate": "Check for updates",
"settings.checkingUpdate": "Checking…",
"settings.updateReady": "New version v{latest} ready — click to install",
"settings.installNow": "Install now",
"settings.installTitle": "Install new version",
"settings.installConfirm": "Install v{latest}? EMRG will quit and the installer takes over.",
"settings.installConfirmWin": "Install v{latest}? EMRG will quit. If SmartScreen appears, click 'More info → Run anyway'.",
"settings.installStarted": "Launching installer for v{latest}…",
"settings.installFailed": "Failed to launch installer: {msg}",
"app.updateReady": "New version v{latest} downloaded — install it from Settings → About",
"app.updateAvailable": "New version v{latest} available — https://github.com/argszero/emrg/releases (no auto-install)",
"settings.githubTokenEmpty": "Please paste a GitHub Personal Access Token first",
"settings.githubConnecting": "Connecting…",
Expand Down
Loading
Loading