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 emrg/gui/renderer/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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; // 悬停完整信息
}
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 9 additions & 8 deletions emrg/gui/renderer/js/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 语义,兼容 \\ 与 /) */
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
36 changes: 29 additions & 7 deletions emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1995,29 +1995,51 @@ 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(
'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 → 空 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
);
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 () => {
Expand Down
Loading