diff --git a/Agent.md b/Agent.md index 5e2dfbfb..0bf1fe4f 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` (169: 43 daemon_client + 17 conn-manager + 22 app-commands + 52 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` (172: 43 daemon_client + 17 conn-manager + 22 app-commands + 55 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` (680) — import check: `uv run python -c "from emrg.client.app import run_client"` -GUI: `cd emrg/gui && npm test` (169: 43 daemon_client + 17 conn-manager + 22 app-commands + 52 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` (172: 43 daemon_client + 17 conn-manager + 22 app-commands + 55 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 ee14d6f3..40020a5f 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 测试(169 项:43 daemon_client + 17 conn-manager + 22 app-commands + 52 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config + 7 gui-state;集成测试在 CI 跑,本地可 npm run test:integration) +npm test # 运行 Node 测试(172 项:43 daemon_client + 17 conn-manager + 22 app-commands + 55 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 af1c210a..52783474 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 (169: 43 daemon_client + 17 conn-manager + 22 app-commands + 52 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 (172: 43 daemon_client + 17 conn-manager + 22 app-commands + 55 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/main.js b/emrg/gui/main.js index 362a3884..d1b00e04 100644 --- a/emrg/gui/main.js +++ b/emrg/gui/main.js @@ -275,9 +275,11 @@ vision = false throw new Error("invalid request_id"); // G143:renderer 预生成 id 的格式护栏 } // P2:每会话独立连接——首条消息前自动打开(新会话不 resume,daemon 隐式订阅) + // P5 slice 2:cwd 取该会话所属项目(跨项目会话用其项目路径,非全局 projectDir) + const sessionCwd = openSessions.get(sessionId)?.projectPath || projectDir; let conn = connManager?.get(sessionId); if (!conn || !conn.connected) { - conn = await openSession(sessionId, projectDir, { resume: false }); + conn = await openSession(sessionId, sessionCwd, { resume: false }); } markSessionActive(sessionId); // P4:发送 = 会话活动 → lastActive + 持久化 let rid; @@ -285,7 +287,7 @@ vision = false // G143:renderer 预生成 requestId(send 前标记自有流,消除 IPC 往返竞态窗口) // WorkBuddy P2:mode="ask" → 纯对话(daemon 不启用工具) // G65:conn.sendTask 内部标记 ownStream(每连接独立锁) - rid = conn.sendTask({ sessionId, cwd: projectDir, prompt: text, stream: true, requestId, mode }); + rid = conn.sendTask({ sessionId, cwd: sessionCwd, prompt: text, stream: true, requestId, mode }); } catch (e) { conn._releaseOwnStream(); // sendTask 抛异常(ws.send 失败)→ 释放锁,防 G65 锁泄漏 throw e; @@ -295,15 +297,17 @@ vision = false ipcMain.handle("emrg:listSessions", async () => listSessions()); - ipcMain.handle("emrg:switchSession", async (_e, { sessionId }) => { + ipcMain.handle("emrg:switchSession", async (_e, { sessionId, projectPath } = {}) => { if (!validateSessionId(sessionId)) throw new Error("invalid session_id"); // G65:自有流运行中禁止切会话(每连接独立锁,查当前激活连接) if (connManager?.get(currentSessionId)?.ownStream) throw new Error("stream in progress — cannot switch"); const prevSid = currentSessionId; // G110:切会话清空旧连接分组缓存(含 timer),防广播"幽灵"残留 connManager?.get(prevSid)?.clearGroups(); + // P5 slice 2:跨项目打开——用该项目路径 resume(非全局 projectDir) + const targetPath = projectPath || openSessions.get(sessionId)?.projectPath || projectDir; try { - await openSession(sessionId, projectDir); // 打开(新)会话连接 + resume_session 自动订阅 + await openSession(sessionId, targetPath); // 打开(新)会话连接 + resume_session 自动订阅 } catch (e) { // G106:被动删除恢复——resume error → 刷新列表 + 切最近 if (/not found|error/i.test(e.message)) { @@ -354,12 +358,14 @@ vision = false activeSid: currentSessionId, })); - ipcMain.handle("emrg:newSession", async () => { + ipcMain.handle("emrg:newSession", async (_e, { projectPath } = {}) => { // G14/G81:本地生成 session_id(无 new_session 消息) const sid = generateSessionId(); // 同步 main 侧会话状态:重连后 resume 正确会话(G41)+ 窗口标题(G109) currentSessionId = sid; win.setTitle(`EMRG — ${sid}`); + // P5 slice 2:新会话指定项目 → 先记簿记(发送时用其 cwd 建连接) + if (projectPath) touchOpenSession(sid, projectPath); // P4:新会话在首条消息前不建连接(sendMessage 自动 open)——此处仅标记激活 // 并刷新持久化(activeSid 前进;openSessions 条目随 openSession 落簿记) schedulePersistGuiState(); @@ -475,6 +481,34 @@ vision = false return { ok: true, path: p }; }); + // P5 slice 2:删除项目 = 只删 projects.yml 条目(保留磁盘数据)+ 关闭该项目已打开会话 + // 受保护项目不可删除(内置 project emrg / 内置 task emrg-task → 演化依赖,删了悬空); + // `.emrg` 是 _touch_project 历史记录**非内置 → 可删**(再次访问重新注册)。 + // 返回 { ok, removed, closed: [sids] };renderer 收到后负责切换激活会话。 + ipcMain.handle("emrg:removeProject", async (_e, { name, path: p } = {}) => { + if (typeof name !== "string" || !name.trim()) throw new Error("invalid project name"); + // 受保护项目:内置 project `emrg` + 内置 task `emrg-task` + if (name === "emrg" || name === "emrg-task") { + return { ok: false, protected: true, error: "protected system project" }; + } + // 该项目已打开的会话 → 关闭连接 + 移除 + 写盘(激活中先切相邻由 renderer 处理) + const closed = []; + for (const [sid, v] of [...openSessions.entries()]) { + if (v.projectPath === p || v.projectName === name) { + connManager?.close(sid); + openSessions.delete(sid); + closed.push(sid); + } + } + if (currentSessionId && closed.includes(currentSessionId)) { + currentSessionId = null; // 激活会话被关 → 无激活(renderer 切相邻) + } + const frame = await requireConn().sendCommandAndWait("remove_project", { name: name.trim() }, 5000); + schedulePersistGuiState(); + broadcastOpenSessions(); + return { ok: true, removed: Boolean(frame.removed), closed, name }; + }); + ipcMain.handle("emrg:listTasks", async () => { // GUI / 指令 P4:/trigger — daemon list_tasks → tasks_list const frame = await requireConn().sendCommandAndWait("list_tasks", {}, 5000); diff --git a/emrg/gui/preload.js b/emrg/gui/preload.js index 636abcf8..62b34a94 100644 --- a/emrg/gui/preload.js +++ b/emrg/gui/preload.js @@ -11,7 +11,7 @@ const api = { sendMessage: (payload) => ipcRenderer.invoke("emrg:sendMessage", payload), listSessions: () => ipcRenderer.invoke("emrg:listSessions"), switchSession: (payload) => ipcRenderer.invoke("emrg:switchSession", payload), - newSession: () => ipcRenderer.invoke("emrg:newSession"), + newSession: (payload) => ipcRenderer.invoke("emrg:newSession", payload), deleteSession: (payload) => ipcRenderer.invoke("emrg:deleteSession", payload), closeSession: (payload) => ipcRenderer.invoke("emrg:closeSession", payload), getOpenSessions: () => ipcRenderer.invoke("emrg:getOpenSessions"), @@ -27,6 +27,7 @@ const api = { listProjects: () => ipcRenderer.invoke("emrg:listProjects"), listProjectSessions: (payload) => ipcRenderer.invoke("emrg:listProjectSessions", payload), registerProject: (payload) => ipcRenderer.invoke("emrg:registerProject", payload), + removeProject: (payload) => ipcRenderer.invoke("emrg:removeProject", payload), listTasks: () => ipcRenderer.invoke("emrg:listTasks"), triggerTask: (payload) => ipcRenderer.invoke("emrg:triggerTask", payload), sendRant: (payload) => ipcRenderer.invoke("emrg:sendRant", payload), diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html index 2f7128cd..dfb90ed4 100644 --- a/emrg/gui/renderer/index.html +++ b/emrg/gui/renderer/index.html @@ -275,12 +275,26 @@
选择项目后选择要打开的会话(跨项目多开)。