diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js index 45a3e13..272a82f 100644 --- a/emrg/gui/renderer/js/app.js +++ b/emrg/gui/renderer/js/app.js @@ -584,7 +584,7 @@ const App = (() => { const cur = state.sessions.find((s) => s.session_id === sid) || {}; const project = sessionProjectName(sid); const title = cur.title && cur.title !== sid ? cur.title : ""; - const text = `${project}/${title}|${sid}`; + const text = title ? `${project}/${title}` : `${project}/${sid}`; // rant 22:04:57:有 name 显示 name,无 name 显示完整 id header.textContent = text; header.title = text; // 悬停完整信息 } @@ -668,7 +668,7 @@ const App = (() => { Chat.addSystemMessage(_t("app.sessionDisconnected"), sid); } updateEmptyState(); - Sidebar.highlight(sid); + Sidebar.highlight(sid, opts.scopeNav); // rant 22:04:02:点击哪列表只高亮该列表(scopeNav 缺省则两列表) setComposerDisabled(false); // 防御性:独立调用 switchSession 也确保输入框可用 } catch (e) { // P6(rant 15:07:19 上限 20):超限提示本地化(main 抛 too many open sessions) diff --git a/emrg/gui/renderer/js/sidebar.js b/emrg/gui/renderer/js/sidebar.js index 57737a6..f6446e1 100644 --- a/emrg/gui/renderer/js/sidebar.js +++ b/emrg/gui/renderer/js/sidebar.js @@ -31,7 +31,7 @@ const Sidebar = (() => { const item = el("div", { class: "conv-item open-session-item" }); item.dataset.sid = entry.sid; item.appendChild(el("span", { class: "conv-title" }, sessionLabel(entry.projectName || "", title, entry.sid))); - item.addEventListener("click", () => App.switchSession(entry.sid)); + item.addEventListener("click", () => App.switchSession(entry.sid, { scopeNav: nav })); item.addEventListener("contextmenu", (e) => { e.preventDefault(); App.showOpenSessionsMenu(item, entry); @@ -41,9 +41,9 @@ const Sidebar = (() => { highlight(App.state.sessionId); } - /** 会话条目统一格式 project/name|id(rant 2026-08-20T17:48:07 三处统一) */ + /** 会话条目统一格式:有 name 显示 project/name,无 name 显示 project/id(rant 2026-08-20T22:04:57) */ function sessionLabel(project, title, sid) { - return `${project}/${title}|${sid}`; + return title ? `${project}/${title}` : `${project}/${sid}`; } /** cwd 末段作项目名(Path(s.cwd).name 语义,兼容 \\ 与 /) */ @@ -71,7 +71,7 @@ const Sidebar = (() => { const title = s.title || ""; // G27:title 优先,无 title 则空(id 已单独显示) const project = cwdProjectName(s.cwd); item.appendChild(el("span", { class: "conv-title" }, sessionLabel(project, title, s.session_id))); - item.addEventListener("click", () => App.switchSession(s.session_id)); + item.addEventListener("click", () => App.switchSession(s.session_id, { scopeNav: nav })); // 右键菜单:重命名 / 删除(友好确认) item.addEventListener("contextmenu", (e) => { e.preventDefault(); @@ -82,9 +82,10 @@ const Sidebar = (() => { highlight(App.state.sessionId); } - /** 高亮当前对话(含打开会话区) */ - function highlight(sid) { - for (const nav of [$("conv-list"), $("open-sessions")]) { + /** 高亮当前对话:指定 navEl 则仅更新该列表,否则两列表都更新(rant 2026-08-20T22:04:02 列表选中互不影响) */ + function highlight(sid, navEl) { + const targets = navEl ? [navEl] : [$("conv-list"), $("open-sessions")]; + for (const nav of targets) { if (!nav) continue; for (const item of nav.querySelectorAll(".conv-item")) { item.classList.toggle("active", item.dataset.sid === sid); @@ -119,7 +120,7 @@ const Sidebar = (() => { } else if (e.key === "Enter" && _focusIdx >= 0) { e.preventDefault(); const target = items[_focusIdx]; - if (target.dataset.sid) App.switchSession(target.dataset.sid); + if (target.dataset.sid) App.switchSession(target.dataset.sid, { scopeNav: nav }); clearFocus(); } else if (e.key === "Escape") { clearFocus(); diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index e41cd8d..1b461f4 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -1995,7 +1995,7 @@ 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)→ 统一 project/name|id 格式", async () => { +test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该 sid)→ 有 name 显示 name、无 name 显示 id(rant 22:04:57)", async () => { const { ctx, els } = makeSandbox({}); await tick(); await vm.runInContext( @@ -2003,7 +2003,7 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该 '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 → 空 name + ' { sid: "sess-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → 显示完整 id ' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title '] } });', ctx @@ -2011,13 +2011,35 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该 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"), "id 单独显示(project/name|id)"); + assert.ok((t0.textContent || "").includes("evolution/Evolution Task"), "有 title → project/name(不含 id)"); + assert.ok(!(t0.textContent || "").includes("sess-x"), "有 title 时不显示 id"); const t1 = nav.children[1].children[0] || nav.children[1]; - assert.ok((t1.textContent || "").includes("sess-other"), "cross-project no title → id 兜底显示"); + assert.ok((t1.textContent || "").includes("mem/sess-other"), "无 title → project/完整 id"); 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"); - assert.ok((t2.textContent || "").includes("sess-local"), "当前项目条目同样带 id(project/name|id)"); + assert.ok((t2.textContent || "").includes("emrg/Local"), "当前项目条目有 title → project/name(不含 id)"); + assert.ok(!(t2.textContent || "").includes("sess-local"), "有 title 时不显示 id"); +}); + +test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表,另一列表保持原样", async () => { + const { ctx, els } = makeSandbox({}); + await tick(); + await vm.runInContext( + 'App.state.sessionId = "sess-a";' + + 'App.handleEvent({ type: "sessions", data: { sessions: [{ session_id: "sess-a", title: "Alpha", cwd: "/p/emrg" }, { session_id: "sess-b", title: "Beta", cwd: "/p/emrg" }] } });' + + 'App.handleEvent({ type: "open_sessions", data: { openSessions: [' + + ' { sid: "sess-b", projectName: "proj-b", projectPath: "/b", lastActive: "t2" },' + + ' { sid: "sess-a", projectName: "proj-a", projectPath: "/a", lastActive: "t1" }' + + '] } });', + ctx + ); + // 只高亮打开会话区(模拟点击打开会话列表条目) + await vm.runInContext('EMRG_Sidebar.highlight("sess-b", document.getElementById("open-sessions"));', ctx); + assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "open-sessions 条目高亮"); + const convB = [...els["conv-list"].children].find((c) => c.dataset.sid === "sess-b"); + assert.ok(convB && !convB.classList.contains("active"), "conv-list 中同 sid 条目不被联动高亮"); + // 无 navEl 参数 → 两列表都更新(兼容初始渲染/全量刷新) + await vm.runInContext('EMRG_Sidebar.highlight("sess-b");', ctx); + assert.strictEqual(convB.classList.contains("active"), true, "无 navEl 时两列表都高亮"); }); test("P4 s2: closeOpenSession 关闭激活会话 → 切到剩余打开会话 + 容器释放", async () => {