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 @@ -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` (244: 44 daemon_client + 19 conn-manager + 22 app-commands + 119 renderer smoke + 16 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (245: 44 daemon_client + 19 conn-manager + 22 app-commands + 120 renderer smoke + 16 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711)
- Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/<name>.md` → fallback
- Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`)
Expand Down Expand Up @@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (804) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (244: 44 daemon_client + 19 conn-manager + 22 app-commands + 119 renderer smoke + 16 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (245: 44 daemon_client + 19 conn-manager + 22 app-commands + 120 renderer smoke + 16 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + 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
25 changes: 22 additions & 3 deletions emrg/gui/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,12 @@ vision = false
const clean = String(title || "").trim().slice(0, 80); // 截断超长标题
if (!clean) throw new Error("empty title");
const frame = await requireConn().sendCommandAndWait("rename_session", { session_id: sessionId, cwd: projectDir, title: clean }, 5000);
// 跨项目会话重命名成功后立即同步侧边栏标题(rant 12:01:44)
const v = openSessions.get(sessionId);
if (v) {
v.title = frame.title || clean;
broadcastOpenSessions();
}
return { ok: true, title: frame.title || clean };
});

Expand Down Expand Up @@ -978,6 +984,19 @@ vision = false
lastActive: new Date().toISOString(),
});
broadcastOpenSessions();
// 跨项目标题(rant 12:01:44):异步拉该会话所在项目列表取 title——
// 找到则 v.title = s.title 并广播(失败/无 title → 保持 undefined → 侧边栏 sid 兜底)
listSessions(projectPath)
.then((sessions) => {
const v = openSessions.get(sid);
if (!v) return; // 会话已关闭/移除
const s = sessions.find((x) => x.session_id === sid);
if (s && s.title) {
v.title = s.title;
broadcastOpenSessions();
}
})
.catch(() => { /* 拉取失败保持 undefined → sid 兜底 */ });
}

// 激活会话变化(切换/新会话/发送)→ 更新 lastActive + activeSid → 防抖写盘
Expand Down Expand Up @@ -1084,7 +1103,7 @@ vision = false

function openSessionsList() {
return [...openSessions.entries()]
.map(([sid, v]) => ({ sid, projectName: v.projectName, projectPath: v.projectPath, lastActive: v.lastActive }))
.map(([sid, v]) => ({ sid, projectName: v.projectName, projectPath: v.projectPath, lastActive: v.lastActive, title: v.title }))
.sort((a, b) => String(b.lastActive || "").localeCompare(String(a.lastActive || "")));
}

Expand Down Expand Up @@ -1163,9 +1182,9 @@ vision = false
});
}

async function listSessions() {
async function listSessions(cwd = projectDir) {
try {
const frame = await requireConn().sendCommandAndWait("list_sessions", { cwd: projectDir }, 5000);
const frame = await requireConn().sendCommandAndWait("list_sessions", { cwd }, 5000);
return frame.sessions || [];
} catch (e) {
logger.warn(`[gui] list_sessions failed: ${e.message}`);
Expand Down
2 changes: 1 addition & 1 deletion emrg/gui/renderer/js/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const Sidebar = (() => {
const known = (App.state && App.state.sessions) || [];
for (const entry of openSessions) {
const cur = known.find((s) => s.session_id === entry.sid) || {};
const title = cur.title || entry.sid; // G27:title 优先
const title = entry.title || cur.title || entry.sid; // entry.title 优先(跨项目),再 cur.title(当前项目),最后 sid
const item = el("div", { class: "conv-item open-session-item" });
item.dataset.sid = entry.sid;
item.appendChild(el("span", { class: "conv-title" }, _t("sidebar.openSessionOf", { project: entry.projectName || "", title })));
Expand Down
24 changes: 24 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,30 @@ test("P4 s2: open_sessions 事件 → 渲染打开会话区(项目名/标题 +
assert.strictEqual(els["open-sessions-label"].hidden, true, "label hidden when no open sessions");
});

test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该 sid)→ 显示 entry.title 而非 sid", async () => {
const { ctx, els } = makeSandbox({});
await tick();
await vm.runInContext(
'App.state.sessionId = "sess-x";' +
'App.state.sessions = [{ session_id: "sess-local", title: "Local" }];' + // 当前项目会话;无 sess-x / sess-other
'App.handleEvent({ type: "open_sessions", data: { openSessions: [' + // main 已按 lastActive 倒序
' { sid: "sess-x", projectName: "evolution", projectPath: "/p/evolution", lastActive: "t3", title: "Evolution Task" },' + // 跨项目 + title
' { sid: "sess-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → sid 兜底
' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title
'] } });',
ctx
);
const nav = els["open-sessions"];
assert.strictEqual(nav.children.length, 3, "three open-session items rendered");
const t0 = nav.children[0].children[0] || nav.children[0];
assert.ok((t0.textContent || "").includes("Evolution Task"), "cross-project entry shows entry.title");
assert.ok(!(t0.textContent || "").includes("sess-x"), "does NOT fall back to sid when entry.title present");
const t1 = nav.children[1].children[0] || nav.children[1];
assert.ok((t1.textContent || "").includes("sess-other"), "cross-project no title + not in state.sessions → sid fallback");
const t2 = nav.children[2].children[0] || nav.children[2];
assert.ok((t2.textContent || "").includes("Local"), "current-project entry still resolves via state.sessions title");
});

test("P4 s2: closeOpenSession 关闭激活会话 → 切到剩余打开会话 + 容器释放", async () => {
let closed = null;
const { ctx, els } = makeSandbox({
Expand Down
Loading