diff --git a/Agent.md b/Agent.md index fe54cf5..721cbb6 100644 --- a/Agent.md +++ b/Agent.md @@ -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) @@ -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 路径不受影响) diff --git a/README.cn.md b/README.cn.md index b6aedbd..724b9b7 100644 --- a/README.cn.md +++ b/README.cn.md @@ -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`)。 diff --git a/README.md b/README.md index a8575c9..666e849 100644 --- a/README.md +++ b/README.md @@ -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`). diff --git a/emrg/gui/renderer/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js index 7685a10..834ab4a 100644 --- a/emrg/gui/renderer/js/dialogs.js +++ b/emrg/gui/renderer/js/dialogs.js @@ -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); @@ -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 }); diff --git a/emrg/gui/renderer/js/i18n.js b/emrg/gui/renderer/js/i18n.js index 8122e78..ce4b84f 100644 --- a/emrg/gui/renderer/js/i18n.js +++ b/emrg/gui/renderer/js/i18n.js @@ -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": "取消", @@ -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", diff --git a/emrg/gui/renderer/js/utils.js b/emrg/gui/renderer/js/utils.js index 42f93e7..9296f76 100644 --- a/emrg/gui/renderer/js/utils.js +++ b/emrg/gui/renderer/js/utils.js @@ -73,6 +73,25 @@ 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; @@ -80,3 +99,4 @@ window.escapeHtml = escapeHtml; window.genRequestId = genRequestId; window.groupLabel = groupLabel; window.applyTheme = applyTheme; +window.relTime = relTime; diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index 6a0b22c..8f18862 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -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"); +}); diff --git a/emrg/server/evolution_prompt.md b/emrg/server/evolution_prompt.md index 0ea3f41..fe01a11 100644 --- a/emrg/server/evolution_prompt.md +++ b/emrg/server/evolution_prompt.md @@ -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` 并行扫描各项目 `/.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