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
11 changes: 9 additions & 2 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand All@@ -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; // 悬停完整信息
}
Expand Down
10 changes: 0 additions & 10 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": "复制代码",
Expand DownExpand Up@@ -410,7 +406,6 @@ const I18N = (() => {
"app.closeSession": "❌ 关闭会话(保留数据)",
"app.closeFailed": "关闭会话失败了:{msg}",
"sidebar.openSessions": "打开的会话",
"sidebar.openSessionOf": "{project} / {title}",
"app.noModels": "还没有配置模型",
"app.goSettings": "去设置添加",
"app.modelSwitchFailed": "切换模型失败了:{msg}",
Expand DownExpand Up@@ -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",
Expand DownExpand Up@@ -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}",
Expand Down
60 changes: 31 additions & 29 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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();
Expand All@@ -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");
Expand All@@ -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);
}
Expand Down
14 changes: 0 additions & 14 deletions emrg/gui/renderer/js/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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;
9 changes: 0 additions & 9 deletions emrg/gui/test/i18n.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand Down
13 changes: 7 additions & 6 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 () => {
Expand DownExpand Up@@ -1941,15 +1941,15 @@ 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(
'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-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → 空 name
' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title
'] } });',
ctx
Expand All@@ -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 () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
emrg: GUI session list & title bar unified format project/name|id by argszero · Pull Request #891 · argszero/emrg · GitHub
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
11 changes: 9 additions & 2 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand All@@ -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; // 悬停完整信息
}
Expand Down
10 changes: 0 additions & 10 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": "复制代码",
Expand DownExpand Up@@ -410,7 +406,6 @@ const I18N = (() => {
"app.closeSession": "❌ 关闭会话(保留数据)",
"app.closeFailed": "关闭会话失败了:{msg}",
"sidebar.openSessions": "打开的会话",
"sidebar.openSessionOf": "{project} / {title}",
"app.noModels": "还没有配置模型",
"app.goSettings": "去设置添加",
"app.modelSwitchFailed": "切换模型失败了:{msg}",
Expand DownExpand Up@@ -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",
Expand DownExpand Up@@ -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}",
Expand Down
60 changes: 31 additions & 29 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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();
Expand All@@ -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");
Expand All@@ -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);
}
Expand Down
14 changes: 0 additions & 14 deletions emrg/gui/renderer/js/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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;
9 changes: 0 additions & 9 deletions emrg/gui/test/i18n.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand Down
13 changes: 7 additions & 6 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 () => {
Expand DownExpand Up@@ -1941,15 +1941,15 @@ 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(
'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-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → 空 name
' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title
'] } });',
ctx
Expand All@@ -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 () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' emrg: GUI session list & title bar unified format project/name|id by argszero · Pull Request #891 · argszero/emrg · GitHub
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
11 changes: 9 additions & 2 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand All@@ -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; // 悬停完整信息
}
Expand Down
10 changes: 0 additions & 10 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": "复制代码",
Expand DownExpand Up@@ -410,7 +406,6 @@ const I18N = (() => {
"app.closeSession": "❌ 关闭会话(保留数据)",
"app.closeFailed": "关闭会话失败了:{msg}",
"sidebar.openSessions": "打开的会话",
"sidebar.openSessionOf": "{project} / {title}",
"app.noModels": "还没有配置模型",
"app.goSettings": "去设置添加",
"app.modelSwitchFailed": "切换模型失败了:{msg}",
Expand DownExpand Up@@ -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",
Expand DownExpand Up@@ -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}",
Expand Down
60 changes: 31 additions & 29 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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();
Expand All@@ -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");
Expand All@@ -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);
}
Expand Down
14 changes: 0 additions & 14 deletions emrg/gui/renderer/js/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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;
9 changes: 0 additions & 9 deletions emrg/gui/test/i18n.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand Down
13 changes: 7 additions & 6 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 () => {
Expand DownExpand Up@@ -1941,15 +1941,15 @@ 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(
'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-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → 空 name
' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title
'] } });',
ctx
Expand All@@ -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 () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' emrg: GUI session list & title bar unified format project/name|id by argszero · Pull Request #891 · argszero/emrg · GitHub
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
11 changes: 9 additions & 2 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand All@@ -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; // 悬停完整信息
}
Expand Down
10 changes: 0 additions & 10 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": "复制代码",
Expand DownExpand Up@@ -410,7 +406,6 @@ const I18N = (() => {
"app.closeSession": "❌ 关闭会话(保留数据)",
"app.closeFailed": "关闭会话失败了:{msg}",
"sidebar.openSessions": "打开的会话",
"sidebar.openSessionOf": "{project} / {title}",
"app.noModels": "还没有配置模型",
"app.goSettings": "去设置添加",
"app.modelSwitchFailed": "切换模型失败了:{msg}",
Expand DownExpand Up@@ -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",
Expand DownExpand Up@@ -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}",
Expand Down
60 changes: 31 additions & 29 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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();
Expand All@@ -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");
Expand All@@ -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);
}
Expand Down
14 changes: 0 additions & 14 deletions emrg/gui/renderer/js/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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;
9 changes: 0 additions & 9 deletions emrg/gui/test/i18n.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand Down
13 changes: 7 additions & 6 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 () => {
Expand DownExpand Up@@ -1941,15 +1941,15 @@ 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(
'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-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → 空 name
' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title
'] } });',
ctx
Expand All@@ -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 () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' emrg: GUI session list & title bar unified format project/name|id by argszero · Pull Request #891 · argszero/emrg · GitHub
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
11 changes: 9 additions & 2 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand All@@ -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; // 悬停完整信息
}
Expand Down
10 changes: 0 additions & 10 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": "复制代码",
Expand DownExpand Up@@ -410,7 +406,6 @@ const I18N = (() => {
"app.closeSession": "❌ 关闭会话(保留数据)",
"app.closeFailed": "关闭会话失败了:{msg}",
"sidebar.openSessions": "打开的会话",
"sidebar.openSessionOf": "{project} / {title}",
"app.noModels": "还没有配置模型",
"app.goSettings": "去设置添加",
"app.modelSwitchFailed": "切换模型失败了:{msg}",
Expand DownExpand Up@@ -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",
Expand DownExpand Up@@ -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}",
Expand Down
60 changes: 31 additions & 29 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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();
Expand All@@ -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");
Expand All@@ -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);
}
Expand Down
14 changes: 0 additions & 14 deletions emrg/gui/renderer/js/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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;
9 changes: 0 additions & 9 deletions emrg/gui/test/i18n.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand Down
13 changes: 7 additions & 6 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 () => {
Expand DownExpand Up@@ -1941,15 +1941,15 @@ 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(
'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-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → 空 name
' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title
'] } });',
ctx
Expand All@@ -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 () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' emrg: GUI session list & title bar unified format project/name|id by argszero · Pull Request #891 · argszero/emrg · GitHub
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
11 changes: 9 additions & 2 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand All@@ -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; // 悬停完整信息
}
Expand Down
10 changes: 0 additions & 10 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": "复制代码",
Expand DownExpand Up@@ -410,7 +406,6 @@ const I18N = (() => {
"app.closeSession": "❌ 关闭会话(保留数据)",
"app.closeFailed": "关闭会话失败了:{msg}",
"sidebar.openSessions": "打开的会话",
"sidebar.openSessionOf": "{project} / {title}",
"app.noModels": "还没有配置模型",
"app.goSettings": "去设置添加",
"app.modelSwitchFailed": "切换模型失败了:{msg}",
Expand DownExpand Up@@ -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",
Expand DownExpand Up@@ -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}",
Expand Down
60 changes: 31 additions & 29 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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();
Expand All@@ -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");
Expand All@@ -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);
}
Expand Down
14 changes: 0 additions & 14 deletions emrg/gui/renderer/js/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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;
9 changes: 0 additions & 9 deletions emrg/gui/test/i18n.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand Down
13 changes: 7 additions & 6 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 () => {
Expand DownExpand Up@@ -1941,15 +1941,15 @@ 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(
'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-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → 空 name
' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title
'] } });',
ctx
Expand All@@ -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 () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' emrg: GUI session list & title bar unified format project/name|id by argszero · Pull Request #891 · argszero/emrg · GitHub
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
11 changes: 9 additions & 2 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand All@@ -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; // 悬停完整信息
}
Expand Down
10 changes: 0 additions & 10 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": "复制代码",
Expand DownExpand Up@@ -410,7 +406,6 @@ const I18N = (() => {
"app.closeSession": "❌ 关闭会话(保留数据)",
"app.closeFailed": "关闭会话失败了:{msg}",
"sidebar.openSessions": "打开的会话",
"sidebar.openSessionOf": "{project} / {title}",
"app.noModels": "还没有配置模型",
"app.goSettings": "去设置添加",
"app.modelSwitchFailed": "切换模型失败了:{msg}",
Expand DownExpand Up@@ -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",
Expand DownExpand Up@@ -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}",
Expand Down
60 changes: 31 additions & 29 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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();
Expand All@@ -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");
Expand All@@ -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);
}
Expand Down
14 changes: 0 additions & 14 deletions emrg/gui/renderer/js/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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;
9 changes: 0 additions & 9 deletions emrg/gui/test/i18n.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand Down
13 changes: 7 additions & 6 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 () => {
Expand DownExpand Up@@ -1941,15 +1941,15 @@ 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(
'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-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → 空 name
' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title
'] } });',
ctx
Expand All@@ -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 () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); emrg: GUI session list & title bar unified format project/name|id by argszero · Pull Request #891 · argszero/emrg · GitHub
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
11 changes: 9 additions & 2 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand All@@ -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; // 悬停完整信息
}
Expand Down
10 changes: 0 additions & 10 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": "复制代码",
Expand DownExpand Up@@ -410,7 +406,6 @@ const I18N = (() => {
"app.closeSession": "❌ 关闭会话(保留数据)",
"app.closeFailed": "关闭会话失败了:{msg}",
"sidebar.openSessions": "打开的会话",
"sidebar.openSessionOf": "{project} / {title}",
"app.noModels": "还没有配置模型",
"app.goSettings": "去设置添加",
"app.modelSwitchFailed": "切换模型失败了:{msg}",
Expand DownExpand Up@@ -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",
Expand DownExpand Up@@ -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}",
Expand Down
60 changes: 31 additions & 29 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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();
Expand All@@ -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");
Expand All@@ -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);
}
Expand Down
14 changes: 0 additions & 14 deletions emrg/gui/renderer/js/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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;
9 changes: 0 additions & 9 deletions emrg/gui/test/i18n.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand Down
13 changes: 7 additions & 6 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 () => {
Expand DownExpand Up@@ -1941,15 +1941,15 @@ 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(
'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-other", projectName: "mem", projectPath: "/p/mem", lastActive: "t2" },' + // 跨项目无 title → 空 name
' { sid: "sess-local", projectName: "emrg", projectPath: "/p/emrg", lastActive: "t1" }' + // 当前项目 → state.sessions title
'] } });',
ctx
Expand All@@ -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 () => {
Expand Down
Loading