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
4 changes: 2 additions & 2 deletions Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ EMRG is a self-evolving AI agent architecture experiment. Python implementation,
- 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` (176: 43 daemon_client + 19 conn-manager + 22 app-commands + 57 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config + 7 gui-state); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (178: 43 daemon_client + 19 conn-manager + 22 app-commands + 59 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 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 Down Expand Up @@ -94,7 +94,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

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

Expand Down
2 changes: 1 addition & 1 deletion README.cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ uv run python -m emrg # 启动 TUI
cd emrg/gui
npm ci # 安装依赖(生产模式可 --omit=dev)
npm start # 启动 GUI(自动拉起 daemon)
npm test # 运行 Node 测试(176 项:43 daemon_client + 19 conn-manager + 22 app-commands + 57 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config + 7 gui-state;集成测试在 CI 跑,本地可 npm run test:integration)
npm test # 运行 Node 测试(178 项:43 daemon_client + 19 conn-manager + 22 app-commands + 59 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config + 7 gui-state;集成测试在 CI 跑,本地可 npm run test:integration)
```

CI 通过 GitHub Actions 自动运行测试并检查冲突标记(`.github/workflows/test.yml`)。
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ uv run python -m emrg # launch TUI
cd emrg/gui
npm ci # install deps (production: --omit=dev)
npm start # launch GUI (auto-starts daemon)
npm test # run Node tests (176: 43 daemon_client + 19 conn-manager + 22 app-commands + 57 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config + 7 gui-state; integration runs in CI, local: npm run test:integration)
npm test # run Node tests (178: 43 daemon_client + 19 conn-manager + 22 app-commands + 59 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config + 7 gui-state; integration runs in CI, local: npm run test:integration)
```

CI runs tests and checks for conflict markers automatically via GitHub Actions (`.github/workflows/test.yml`).
Expand Down
6 changes: 6 additions & 0 deletions emrg/gui/renderer/js/dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,9 @@ const Dialogs = (() => {
const hint = el("span", { class: "help-hint" }, p.path || "");
pick.appendChild(name);
pick.appendChild(hint);
// P6 验收补完:项目行显示"最近活跃"相对时间(消费 daemon latest_session_at)
const act = relTime(p.latest_session_at);
if (act) pick.appendChild(el("span", { class: "help-hint" }, act));
// P5 slice 2:点击打开会话(带项目路径 → resume 用该项目 cwd)
pick.addEventListener("click", () => showProjectSessions(p));
row.appendChild(pick);
Expand Down Expand Up @@ -674,6 +677,9 @@ const Dialogs = (() => {
const hint = el("span", { class: "help-hint" }, p.path || "");
row.appendChild(name);
row.appendChild(hint);
// P6 验收补完:项目行显示"最近活跃"相对时间
const act = relTime(p.latest_session_at);
if (act) row.appendChild(el("span", { class: "help-hint" }, act));
row.addEventListener("click", async () => {
$("new-session-dialog").close();
await App.newSession({ projectPath: p.path });
Expand Down
8 changes: 8 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ const I18N = (() => {
"deleteProject.protectedBody": "「{name}」是系统项目,不可删除。",
"deleteProject.removed": "项目已删除:{name}",
"deleteProject.failed": "删除项目失败:{msg}",
"relTime.justNow": "刚刚",
"relTime.minutesAgo": "{n} 分钟前",
"relTime.hoursAgo": "{n} 小时前",
"relTime.daysAgo": "{n} 天前",
"rewind.title": "回退到历史消息点",
"rewind.desc": "选择要保留到的消息点,之后的对话将被移除。",
"rewind.cancel": "取消",
Expand Down Expand Up @@ -477,6 +481,10 @@ const I18N = (() => {
"deleteProject.protectedBody": "\"{name}\" is a system project and cannot be deleted.",
"deleteProject.removed": "Project removed: {name}",
"deleteProject.failed": "Failed to delete project: {msg}",
"relTime.justNow": "just now",
"relTime.minutesAgo": "{n} min ago",
"relTime.hoursAgo": "{n} h ago",
"relTime.daysAgo": "{n} d ago",
"rewind.title": "Rewind to a history point",
"rewind.desc": "Choose the message point to keep — later messages will be removed.",
"rewind.cancel": "Cancel",
Expand Down
20 changes: 20 additions & 0 deletions emrg/gui/renderer/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,30 @@ function applyTheme(mode) {
}
}

/**
* 相对时间(P6 验收补完:项目行"最近活跃"提示,消费 daemon list_projects 的
* latest_session_at)。ISO 时间串 → "刚刚 / {n} 分钟前 / {n} 小时前 / {n} 天前",
* 走 i18n(zh/en);缺失/非法输入返回空串(调用点自行隐藏)。
*/
function relTime(iso) {
if (!iso) return "";
const then = new Date(iso).getTime();
if (Number.isNaN(then)) return "";
const diffMin = Math.floor((Date.now() - then) / 60000);
const t = (window.EMRG_I18N && window.EMRG_I18N.t) || ((k) => k);
if (diffMin < 1) return t("relTime.justNow");
if (diffMin < 60) return t("relTime.minutesAgo", { n: diffMin });
const hrs = Math.floor(diffMin / 60);
if (hrs < 24) return t("relTime.hoursAgo", { n: hrs });
const days = Math.floor(hrs / 24);
return t("relTime.daysAgo", { n: days });
}

// 显式挂载到 window(多 script 顺序加载共享全局,显式挂载更健壮)
window.$ = $;
window.el = el;
window.escapeHtml = escapeHtml;
window.genRequestId = genRequestId;
window.groupLabel = groupLabel;
window.applyTheme = applyTheme;
window.relTime = relTime;
45 changes: 45 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1497,3 +1497,48 @@ test("P6: model_set 多连接重复广播幂等 — 状态一致、无副作用
await vm.runInContext('App.handleEvent({ type: "command_result", sid: "conn-c", data: { type: "model_set", model: "m2" } });', ctx);
assert.strictEqual(ctx.App.state.model, "m2", "still idempotent after third broadcast");
});

// ── P6 验收补完(rant 15:07:19):relTime 相对时间助手 + 项目行最近活跃提示 ──

test("P6: relTime 相对时间 — 刚刚/分钟/小时/天 + 无效输入空串", async () => {
const { ctx } = makeSandbox({});
await tick();
const out = vm.runInContext(`(function(){
const now = Date.now();
return {
justNow: relTime(new Date(now).toISOString()),
mins: relTime(new Date(now - 5 * 60000).toISOString()),
hrs: relTime(new Date(now - 3 * 3600000).toISOString()),
days: relTime(new Date(now - 2 * 86400000).toISOString()),
none: relTime(""),
bad: relTime("not-a-date"),
};
})()`, ctx);
assert.strictEqual(out.justNow, "刚刚", "just now");
assert.strictEqual(out.mins, "5 分钟前", "5 minutes ago");
assert.strictEqual(out.hrs, "3 小时前", "3 hours ago");
assert.strictEqual(out.days, "2 天前", "2 days ago");
assert.strictEqual(out.none, "", "empty input → empty");
assert.strictEqual(out.bad, "", "invalid input → empty");
});

test("P6: 打开会话弹窗项目行显示最近活跃提示(latest_session_at)", async () => {
const projects = [
{ name: "mem", path: "/p/mem", latest_session_at: new Date(Date.now() - 5 * 60000).toISOString() },
{ name: "emrg", path: "/p/emrg" }, // 无 latest_session_at → 不显示
];
const { ctx, els } = makeSandbox({ listProjects: async () => projects });
await tick();
await vm.runInContext("EMRG_Dialogs.showOpenSessionDialog()", ctx);
await tick();
const rows = els["open-session-list"].children;
assert.strictEqual(rows.length, 2, "two project rows");
// 行 0 = div[pick, del];pick 子节点含 name/path/act hint(relTime 5 分钟前)
const pick0 = rows[0].children[0];
const texts0 = [...pick0.children].map((c) => c.textContent || "");
assert.ok(texts0.includes("5 分钟前"), "recent activity hint shown, got: " + texts0.join(","));
// 行 1 无 latest_session_at → 无活动提示
const pick1 = rows[1].children[0];
const texts1 = [...pick1.children].map((c) => c.textContent || "");
assert.ok(!texts1.some((t) => t.includes("前") || t.includes("ago")), "no activity hint when absent");
});
1 change: 1 addition & 0 deletions emrg/server/evolution_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ When reading rants, follow these rules:
> - GUI new-session dialog + delete-project protected guard (#642 GUI 多会话 rant 2026-08-10T15:07:19 P5 slice 2:**新建会话弹窗** `showNewSessionDialog`(listProjects 活跃序 → 点选即 `App.newSession({projectPath})`;底部"+ 新建项目…" → pickProjectDir → registerProject → 同路径新建)+ index.html `#new-session-dialog` + 打开弹窗顶部"+ 新建会话…"入口;**删除项目**:打开弹窗项目行右侧删除按钮 → 受保护守卫(内置 project `emrg` / 内置 task `emrg-task` 提示"系统项目不可删除",不调 API;`.emrg` 非内置可删)→ 确认弹窗(数据保留可恢复)→ main.js `emrg:removeProject` IPC(关闭该项目已打开会话连接 + 移出簿记 + 写盘 + 激活被关 → renderer 切相邻/新建 + 广播 open_sessions)→ daemon `remove_project`(P1 已备);**slice-1 补洞**:switchSession/sendMessage 带 per-session projectPath(跨项目 resume 用项目 cwd 非全局 projectDir);newSession 接受 projectPath(首条消息前即记簿记);i18n zh/en 11 条;+3 测试 renderer.smoke 52→55(新建会话选项目 / 新建项目→注册→新建 / 删除项目受保护+普通确认);GUI 169→172 三文档同步;680 pytest 全绿;P5 完成,P6 收尾待续) ✅
> - GUI multi-session P6 finalize (#643 GUI 多会话 rant 2026-08-10T15:07:19 收尾边界:①**关闭在忙连接先 cancel 再 close**——ConnManager.close() 检测 `conn.ownStream && conn.ws` 先发 cancel(fire-and-forget 吞断连异常,不阻塞同步 close 语义),防流式半途断线留脏状态;②**上限 20 超限提示不自动关**——switchSession 显式打开新会话时 `openSessions.size >= DEFAULT_CAP && !openSessions.has(sid)` → 抛 "too many open sessions (20) — close some first"(已打开 sid 复用不拦;sendMessage/newSession 创建路径不拦);renderer 识别该错误 → 本地化 `app.tooManyOpenSessions` zh/en(不再漏英文原始错误);③**projectPath 校验**——switchSession/newSession IPC 收 projectPath 时校验 string 非空;+3 测试(conn-manager 17→19:忙 close 发 cancel / 空闲 close 不 cancel;renderer.smoke 55→56:超限本地化提示);GUI 172→175 三文档同步;680 pytest 全绿;P1-P6 全部完成,验收清单剩余宿主实测项) ✅
> - GUI multi-session acceptance completion (#644 GUI 多会话 rant 2026-08-10T15:07:19 验收补完两件:①**项目按最新会话活跃倒序**——daemon `_handle_list_projects` 并行扫描各项目 `<path>/.emrg/sessions/*/meta.json` 取最大 created_at(asyncio.gather),projects 响应带 `latest_session_at` 并倒序(此前 projects.yml 按 path 排序、list_projects 按文件序返回,打开/新建会话弹窗"按最近活跃"的验收项实际未满足);GUI 单连接无法并发 list_sessions(DaemonClient `_pending` 按 respType 键控互相覆盖)→ daemon 侧聚合的正确架构;/rant 项目列表同步受益(最近活跃置顶);②**model_set 多连接重复广播幂等测试**——renderer.smoke 新增两/三连接同值广播:state.model 不变、无新系统消息、model-switcher-label 一致(验收"model_set 多连接重复收无副作用");+2 测试(pytest 680→681、renderer.smoke 55→56)GUI 172→173、三文档同步;681 pytest 全绿;P1-P6 验收项仅剩宿主实测类) ✅
> - GUI project-row recent-activity hints (#646 GUI 多会话 rant 2026-08-10T15:07:19 验收体验补完:打开/新建会话弹窗项目行显示"最近活跃"相对时间——utils.js 新增 `relTime(iso)`(ISO → "刚刚/N 分钟前/N 小时前/N 天前",走 i18n zh/en,缺失/非法输入返回空串);dialogs.js showOpenSessionDialog + showNewSessionDialog 项目行消费 daemon `latest_session_at`(#644 新增字段)追加 hint span(无字段不显示);i18n zh/en 4 键(relTime.*);+2 测试 renderer.smoke 57→59(relTime 六态:刚刚/分钟/小时/天/空/非法 + 弹窗行活动提示/无字段不显示);GUI 176→178 三文档同步;681 pytest 全绿;"按最近活跃排序"从隐式排序变为可见提示,宿主可感知排序依据) ✅

#### 2.2 Latest GitHub code changes

Expand Down
Loading