From ad299bcf32a008b3eb03a962d922b0a368426190 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 20 Aug 2026 18:28:21 +0800 Subject: [PATCH] emrg: GUI session list & title bar unified format project/name|id (rant 2026-08-20T17:48:07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sidebar render(): drop 今天/昨天/更早 time grouping, sort by last-active desc, item label = project/name|id (project = cwd basename, name = title||"", id = session_id) - renderOpenSessions(): same unified format (entry.projectName + title, id always shown) - app.js renderSessionHeader(): project/name|id (was project/title(sid) or project/sid) - remove now-unused util.group* i18n keys + groupLabel() helper - tests updated: conv-list no group labels; open-session items assert project/name|id --- emrg/gui/renderer/js/app.js | 11 ++++- emrg/gui/renderer/js/i18n.js | 10 ----- emrg/gui/renderer/js/sidebar.js | 60 ++++++++++++++-------------- emrg/gui/renderer/js/utils.js | 14 ------- emrg/gui/test/i18n.test.js | 9 ----- emrg/gui/test/renderer.smoke.test.js | 13 +++--- 6 files changed, 47 insertions(+), 70 deletions(-) diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js index 53cd7ad2..77f84ca6 100644 --- a/emrg/gui/renderer/js/app.js +++ b/emrg/gui/renderer/js/app.js @@ -559,10 +559,17 @@ const App = (() => { function sessionProjectName(sid) { const os = state.openSessions.find((s) => s.sid === sid); if (os && os.projectName) return os.projectName; + // rant 17:48:07:回退用会话自身 cwd 末段(与历史列表一致),再 "home" + const cur = state.sessions.find((s) => s.session_id === sid); + if (cur && cur.cwd) { + const norm = String(cur.cwd).replace(/\\/g, "/").replace(/\/+$/, ""); + const seg = norm.split("/"); + if (seg[seg.length - 1]) return seg[seg.length - 1]; + } return "home"; } - // 会话视图顶部标题栏:项目/名称(id) 或 项目/id(有 title 时带 (id) 后缀) + // 会话视图顶部标题栏:统一 project/name|id(rant 2026-08-20T17:48:07) function renderSessionHeader(sid) { if (!sid) return; const view = [...$("workspace").children].find((c) => c.dataset?.sid === sid); @@ -575,7 +582,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 = title ? `${project}/${title}(${sid})` : `${project}/${sid}`; + const text = `${project}/${title}|${sid}`; header.textContent = text; header.title = text; // 悬停完整信息 } diff --git a/emrg/gui/renderer/js/i18n.js b/emrg/gui/renderer/js/i18n.js index ef60ac08..eae6220c 100644 --- a/emrg/gui/renderer/js/i18n.js +++ b/emrg/gui/renderer/js/i18n.js @@ -317,10 +317,6 @@ const I18N = (() => { "chat.elapsed": "耗时 {s}", "chat.expand": "展开全文", "chat.toolGroupSummary": "{count} 个工具执行 · {time}", - // 时间分组(utils.js/sidebar.js) - "util.groupToday": "今天", - "util.groupYesterday": "昨天", - "util.groupEarlier": "更早", // 结果面板(result-panel.js) // Markdown 代码块(markdown.js) "md.copyCode": "复制代码", @@ -410,7 +406,6 @@ const I18N = (() => { "app.closeSession": "❌ 关闭会话(保留数据)", "app.closeFailed": "关闭会话失败了:{msg}", "sidebar.openSessions": "打开的会话", - "sidebar.openSessionOf": "{project} / {title}", "app.noModels": "还没有配置模型", "app.goSettings": "去设置添加", "app.modelSwitchFailed": "切换模型失败了:{msg}", @@ -727,10 +722,6 @@ const I18N = (() => { "chat.elapsed": "took {s}", "chat.expand": "Expand full text", "chat.toolGroupSummary": "{count} tool calls · {time}", - // Time groups (utils.js/sidebar.js) - "util.groupToday": "Today", - "util.groupYesterday": "Yesterday", - "util.groupEarlier": "Earlier", // Result panel (result-panel.js) // Markdown code block (markdown.js) "md.copyCode": "Copy code", @@ -820,7 +811,6 @@ const I18N = (() => { "app.closeSession": "❌ Close session (keep data)", "app.closeFailed": "Failed to close session: {msg}", "sidebar.openSessions": "Open sessions", - "sidebar.openSessionOf": "{project} / {title}", "app.noModels": "No models configured", "app.goSettings": "Add in Settings", "app.modelSwitchFailed": "Failed to switch model: {msg}", diff --git a/emrg/gui/renderer/js/sidebar.js b/emrg/gui/renderer/js/sidebar.js index 9c6b4525..57737a6e 100644 --- a/emrg/gui/renderer/js/sidebar.js +++ b/emrg/gui/renderer/js/sidebar.js @@ -27,10 +27,10 @@ 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 = entry.title || cur.title || entry.sid; // entry.title 优先(跨项目),再 cur.title(当前项目),最后 sid + const title = entry.title || cur.title || ""; // entry.title 优先(跨项目),再 cur.title;id 单独显示,不降级为 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 }))); + item.appendChild(el("span", { class: "conv-title" }, sessionLabel(entry.projectName || "", title, entry.sid))); item.addEventListener("click", () => App.switchSession(entry.sid)); item.addEventListener("contextmenu", (e) => { e.preventDefault(); @@ -41,7 +41,20 @@ const Sidebar = (() => { highlight(App.state.sessionId); } - /** 渲染分组对话列表 */ + /** 会话条目统一格式 project/name|id(rant 2026-08-20T17:48:07 三处统一) */ + function sessionLabel(project, title, sid) { + return `${project}/${title}|${sid}`; + } + + /** cwd 末段作项目名(Path(s.cwd).name 语义,兼容 \\ 与 /) */ + function cwdProjectName(cwd) { + if (!cwd) return ""; + const norm = String(cwd).replace(/\\/g, "/").replace(/\/+$/, ""); + const seg = norm.split("/"); + return seg[seg.length - 1] || ""; + } + + /** 渲染会话列表(rant 17:48:07:去掉今天/昨天/更早分组,按最后活跃倒序,project/name|id) */ function render(list) { sessions = list || []; const nav = $("conv-list"); @@ -50,32 +63,21 @@ const Sidebar = (() => { nav.appendChild(el("div", { class: "conv-item placeholder" }, EMRG_Copy.COPY.noSessions)); return; } - // rant 21:19:分组标签本地化(顺序保持 今天→昨天→更早 不变) - const groups = {}; - for (const lbl of [_t("util.groupToday"), _t("util.groupYesterday"), _t("util.groupEarlier")]) { - groups[lbl] = []; - } - for (const s of sessions) { - const g = groupLabel(s.updated_at || s.created_at); - if (!groups[g]) groups[g] = []; - groups[g].push(s); - } - for (const [label, items] of Object.entries(groups)) { - if (!items.length) continue; - nav.appendChild(el("div", { class: "conv-group-label" }, label)); - for (const s of items) { - const item = el("div", { class: "conv-item" }); - item.dataset.sid = s.session_id; - const title = s.title || s.session_id; // G27:title 优先 - item.appendChild(el("span", { class: "conv-title" }, title)); - item.addEventListener("click", () => App.switchSession(s.session_id)); - // 右键菜单:重命名 / 删除(友好确认) - item.addEventListener("contextmenu", (e) => { - e.preventDefault(); - App.showConvMenu(item, s.session_id, title); - }); - nav.appendChild(item); - } + const sorted = [...sessions].sort((a, b) => + String(b.updated_at || b.created_at || "").localeCompare(String(a.updated_at || a.created_at || ""))); + for (const s of sorted) { + const item = el("div", { class: "conv-item" }); + item.dataset.sid = s.session_id; + 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("contextmenu", (e) => { + e.preventDefault(); + App.showConvMenu(item, s.session_id, title || s.session_id); + }); + nav.appendChild(item); } highlight(App.state.sessionId); } diff --git a/emrg/gui/renderer/js/utils.js b/emrg/gui/renderer/js/utils.js index 21c83e57..ee8dc994 100644 --- a/emrg/gui/renderer/js/utils.js +++ b/emrg/gui/renderer/js/utils.js @@ -42,19 +42,6 @@ function genRequestId() { }); } -/** 会话时间分组:今天 / 昨天 / 更早 */ -function groupLabel(ts) { - if (!ts) return _t("util.groupEarlier"); - const d = new Date(ts); - if (Number.isNaN(d.getTime())) return _t("util.groupEarlier"); - const now = new Date(); - const startOfDay = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); - const dayDiff = Math.round((startOfDay(now) - startOfDay(d)) / 86400000); - if (dayDiff <= 0) return _t("util.groupToday"); - if (dayDiff === 1) return _t("util.groupYesterday"); - return _t("util.groupEarlier"); -} - /** rant 21:19:i18n 取词(i18n.js 缺失时回退 key 本身) */ function _t(key, params) { try { @@ -116,7 +103,6 @@ window.$ = $; window.el = el; window.escapeHtml = escapeHtml; window.genRequestId = genRequestId; -window.groupLabel = groupLabel; window.applyTheme = applyTheme; window.relTime = relTime; window.showToast = showToast; diff --git a/emrg/gui/test/i18n.test.js b/emrg/gui/test/i18n.test.js index ff3bd984..ad594e47 100644 --- a/emrg/gui/test/i18n.test.js +++ b/emrg/gui/test/i18n.test.js @@ -145,15 +145,6 @@ test("Stage2:动态文案键(app/chat/dlg/panel)双语齐全", () => { assert.strictEqual(evalIn(zh, 'I18N.t("dlg.deleteModelBody", { name: "gpt-4o" })'), "「gpt-4o」将从可用模型里移除。"); }); -test("Stage2:时间分组标签本地化(util.group*)", () => { - const { ctx } = makeSandbox({ navigator: { language: "en-US" } }); - assert.strictEqual(evalIn(ctx, 'I18N.t("util.groupToday")'), "Today"); - assert.strictEqual(evalIn(ctx, 'I18N.t("util.groupYesterday")'), "Yesterday"); - assert.strictEqual(evalIn(ctx, 'I18N.t("util.groupEarlier")'), "Earlier"); - const { ctx: zh } = makeSandbox({ navigator: { language: "zh-CN" } }); - assert.strictEqual(evalIn(zh, 'I18N.t("util.groupToday")'), "今天"); -}); - test("Stage2:成长卡/关于区静态文案键(#501 吸收)", () => { const { ctx } = makeSandbox({ navigator: { language: "en-US" } }); assert.strictEqual(evalIn(ctx, 'I18N.t("copy.growthCountPrefix")'), "Self-evolved"); diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index c5403ae3..bcd8a746 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -296,9 +296,9 @@ test("boot:config 就绪 → 加载会话列表", async () => { switchSession: async () => ({}), }); await tick(); - // conv-list 应有分组标签 + 会话项 + // conv-list 应有会话项(rant 17:48:07:无分组标签,直接 project/name|id) const items = vm.runInContext('document.getElementById("conv-list").children.length', ctx); - assert.ok(items >= 2, `conv-list 应有分组标签+会话项,实际 ${items}`); + assert.ok(items >= 1, `conv-list 应有会话项,实际 ${items}`); }); test("流式 delta 追加 + 工具行 running→done 状态流转", async () => { @@ -1941,7 +1941,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)→ 显示 entry.title 而非 sid", async () => { +test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该 sid)→ 统一 project/name|id 格式", async () => { const { ctx, els } = makeSandbox({}); await tick(); await vm.runInContext( @@ -1949,7 +1949,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 → sid 兜底 + ' { sid: "sess-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → 空 name ' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title '] } });', ctx @@ -1958,11 +1958,12 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.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"); + assert.ok((t0.textContent || "").includes("sess-x"), "id 单独显示(project/name|id)"); 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"); + assert.ok((t1.textContent || "").includes("sess-other"), "cross-project no title → 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)"); }); test("P4 s2: closeOpenSession 关闭激活会话 → 切到剩余打开会话 + 容器释放", async () => {