From c8a4067166b2bd341f5c1b67bc0e6600fafe18c8 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Mon, 10 Aug 2026 18:12:14 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20GUI=20open-sessions=20sidebar=20+=20gui?= =?UTF-8?q?=5Fstate=20restore=20=E2=80=94=20P4=20slice=202=20(GUI=20multi-?= =?UTF-8?q?session=20rant=20P4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Agent.md | 4 +- README.cn.md | 2 +- README.md | 2 +- emrg/gui/main.js | 64 ++++++++++++++ emrg/gui/renderer/css/layout.css | 12 +++ emrg/gui/renderer/index.html | 3 + emrg/gui/renderer/js/app.js | 105 +++++++++++++++++++++- emrg/gui/renderer/js/i18n.js | 10 +++ emrg/gui/renderer/js/sidebar.js | 45 +++++++++- emrg/gui/test/renderer.smoke.test.js | 125 ++++++++++++++++++++++++++- 10 files changed, 357 insertions(+), 15 deletions(-) diff --git a/Agent.md b/Agent.md index bdc0ca75..0a02ef06 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` (163: 43 daemon_client + 17 conn-manager + 22 app-commands + 46 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` (167: 43 daemon_client + 17 conn-manager + 22 app-commands + 50 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` (163: 43 daemon_client + 17 conn-manager + 22 app-commands + 46 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` (167: 43 daemon_client + 17 conn-manager + 22 app-commands + 50 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 deb81441..e979c555 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 测试(163 项:43 daemon_client + 17 conn-manager + 22 app-commands + 46 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config + 7 gui-state;集成测试在 CI 跑,本地可 npm run test:integration) +npm test # 运行 Node 测试(167 项:43 daemon_client + 17 conn-manager + 22 app-commands + 50 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 d2a1cec0..e8b09cec 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 (163: 43 daemon_client + 17 conn-manager + 22 app-commands + 46 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 (167: 43 daemon_client + 17 conn-manager + 22 app-commands + 50 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 f36f64e5..edd0036f 100644 --- a/emrg/gui/main.js +++ b/emrg/gui/main.js @@ -251,6 +251,8 @@ vision = false await ensureConnected(); const pong = await waitForPong(); const sessions = await listSessions(); + // P4 slice 2:启动恢复打开会话(gui_state.json → 重开连接 + resume 重订阅) + await restoreOpenSessions(sessions); return { config_exists: true, api_key_configured: true, @@ -261,6 +263,8 @@ vision = false evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong) version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7) sessions, + open_sessions: openSessionsList(), + active_sid: currentSessionId, // P4 slice 2:恢复后的激活会话(renderer 直接采用) }; }); @@ -325,6 +329,7 @@ vision = false connManager?.close(sessionId); // P2:删除会话 → 关闭该会话连接(若打开) openSessions.delete(sessionId); // P4:删除(删数据)→ 一并移出打开会话簿记 schedulePersistGuiState(); + broadcastOpenSessions(); return { ok: true }; }); @@ -358,6 +363,7 @@ vision = false // P4:新会话在首条消息前不建连接(sendMessage 自动 open)——此处仅标记激活 // 并刷新持久化(activeSid 前进;openSessions 条目随 openSession 落簿记) schedulePersistGuiState(); + broadcastOpenSessions(); return { session_id: sid }; }); @@ -719,6 +725,7 @@ vision = false projectPath, lastActive: new Date().toISOString(), }); + broadcastOpenSessions(); } // 激活会话变化(切换/新会话/发送)→ 更新 lastActive + activeSid → 防抖写盘 @@ -727,6 +734,7 @@ vision = false openSessions.get(sid).lastActive = new Date().toISOString(); } schedulePersistGuiState(); + broadcastOpenSessions(); } function schedulePersistGuiState() { @@ -756,6 +764,61 @@ vision = false } } + // 读盘 + 清洗(启动恢复用;损坏/缺失 → 空) + function readGuiState() { + try { + const p = guiStatePath(os.homedir()); + if (!fs.existsSync(p)) return { openSessions: [], activeSid: null }; + const raw = JSON.parse(fs.readFileSync(p, "utf8")); + return { + openSessions: sanitizeOpenSessions(raw.openSessions || []), + activeSid: raw.activeSid || null, + }; + } catch (e) { + logger.warn(`[gui] gui_state.json read failed: ${e.message}`); + return { openSessions: [], activeSid: null }; + } + } + + // P4 slice 2:启动恢复——gui_state.json 中的打开会话(上限 20)逐个重开连接 + + // resume 重订阅;activeSid 失效 → 第一个有效;失效条目跳过 + 重写盘。 + async function restoreOpenSessions(sessions) { + const { openSessions: saved, activeSid } = readGuiState(); + const validSids = new Set((sessions || []).map((s) => s.session_id)); + let restored = 0; + for (const entry of saved) { + if (restored >= 20) break; // 恢复上限(sanitize 已守,双保险) + if (!validSids.has(entry.sid)) continue; // 失效条目跳过 + try { + await openSession(entry.sid, entry.projectPath); // resume 重订阅 + restored += 1; + } catch (e) { + logger.warn(`[gui] restore session ${entry.sid} failed: ${e.message}`); + openSessions.delete(entry.sid); // 失效 → 移除 + } + } + // activeSid:优先恢复原激活;失效 → 第一个有效 + if (openSessions.has(activeSid)) { + currentSessionId = activeSid; + } else if (openSessions.size > 0) { + currentSessionId = openSessionsList()[0].sid; + } + if (currentSessionId) { + win?.setTitle(`EMRG — ${currentSessionId}`); + } + schedulePersistGuiState(); // 失效条目剔除后重写盘 + broadcastOpenSessions(); + return restored; + } + + // 通知 renderer 打开会话列表变化(侧边栏数据源,slice 2) + function broadcastOpenSessions() { + sendToRenderer("open_sessions", { + openSessions: openSessionsList(), + activeSid: currentSessionId, + }); + } + // 关闭会话(保留磁盘数据):断连 + 移除 + 持久化。P4 slice 2 侧边栏"关闭"入口用。 async function closeSession(sid) { const entry = openSessions.get(sid); @@ -763,6 +826,7 @@ vision = false openSessions.delete(sid); schedulePersistGuiState(); if (currentSessionId === sid) currentSessionId = null; // 关闭激活会话 → 无激活 + broadcastOpenSessions(); return { ok: true, closed: !!entry }; } diff --git a/emrg/gui/renderer/css/layout.css b/emrg/gui/renderer/css/layout.css index 2490b483..b9d5a0c1 100644 --- a/emrg/gui/renderer/css/layout.css +++ b/emrg/gui/renderer/css/layout.css @@ -94,6 +94,18 @@ letter-spacing: 0.02em; } +/* P4 slice 2:打开会话区(跨项目 tab)——紧贴新对话按钮,视觉轻量 */ +#open-sessions { + padding-bottom: var(--sp-1); + border-bottom: 1px solid var(--border); + margin-bottom: var(--sp-1); + max-height: 40%; + overflow-y: auto; +} +.open-sessions-label { + padding-top: var(--sp-2); +} + .sidebar-footer { display: flex; align-items: center; diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html index d6341d0a..2284e9f4 100644 --- a/emrg/gui/renderer/index.html +++ b/emrg/gui/renderer/index.html @@ -25,6 +25,9 @@
边工作边学习,越用越懂你
+ + +