Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request
- Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand)
- Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端"
- Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout)
- Unit tests `npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711)
- Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/<name>.md` → fallback
- Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`)
Expand DownExpand Up@@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)

Expand Down
6 changes: 0 additions & 6 deletions emrg/gui/renderer/css/components.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,12 +19,6 @@
background: var(--bg-soft);
color: var(--text-1);
}
/* 键盘导航聚焦(↑↓)——区别于当前会话高亮 */
.conv-item.kbd-focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--bg-soft);
}
.conv-item.active {
background: var(--accent-soft);
color: var(--accent);
Expand Down
1 change: 0 additions & 1 deletion emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,6 @@
<!-- P4 slice 2:跨项目打开的会话(浏览器 tab 效果,关闭保留数据) -->
<div class="conv-group-label open-sessions-label" id="open-sessions-label" data-i18n="sidebar.openSessions" hidden>打开的会话</div>
<nav id="open-sessions" class="open-sessions"></nav>
<nav id="conv-list"></nav>
<div class="sidebar-footer">
<span id="status-dot" class="status-dot gray" title="连接状态" data-i18n-title="sidebar.statusTitle"></span>
</div>
Expand Down
7 changes: 1 addition & 6 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,8 +94,7 @@ const App = (() => {
return;
}
if (init.sessions && init.sessions.length > 0) {
state.sessions = init.sessions;
Sidebar.render(init.sessions);
state.sessions = init.sessions; // rant 11:44:52:历史会话列表已移除,不再渲染 conv-list
// P4 slice 2:main 恢复的打开会话 + 激活会话(跳过 switchSession 的 IPC 往返)
state.openSessions = init.open_sessions || [];
Sidebar.renderOpenSessions(state.openSessions);
Expand DownExpand Up@@ -651,7 +650,6 @@ const App = (() => {
if (res.error === "session_not_found") {
Chat.addSystemMessage(_t("app.deletedSwitch"));
if (res.next_session) {
Sidebar.render(res.sessions || []);
await switchSession(res.next_session, { silent: true });
} else {
await newSession();
Expand DownExpand Up@@ -909,7 +907,6 @@ const App = (() => {
try {
const sessions = await window.emrg.listSessions();
state.sessions = sessions;
Sidebar.render(sessions);
Sidebar.renderOpenSessions(state.openSessions); // 打开会话区 title 同步刷新
renderSessionHeader(state.sessionId); // 激活会话标题栏同步刷新
} catch { /* 忽略 */ }
Expand DownExpand Up@@ -1491,7 +1488,6 @@ const App = (() => {
break;
case "sessions":
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
break;
case "open_sessions":
// P4 slice 2:main 广播打开会话列表变化 → 刷新侧边栏打开会话区
Expand DownExpand Up@@ -1539,7 +1535,6 @@ const App = (() => {
case "list_result":
if (data.type === "sessions_list") {
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
}
break;
case "command_result":
Expand Down
91 changes: 3 additions & 88 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@
*/

const Sidebar = (() => {
let sessions = [];
let openSessions = [];

/**
Expand DownExpand Up@@ -46,45 +45,9 @@ const Sidebar = (() => {
return title ? `${project}/${title}` : `${project}/${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");
nav.innerHTML = "";
if (!sessions.length) {
nav.appendChild(el("div", { class: "conv-item placeholder" }, EMRG_Copy.COPY.noSessions));
return;
}
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, { scopeNav: nav }));
// 右键菜单:重命名 / 删除(友好确认)
item.addEventListener("contextmenu", (e) => {
e.preventDefault();
App.showConvMenu(item, s.session_id, title || s.session_id);
});
nav.appendChild(item);
}
highlight(App.state.sessionId);
}

/** 高亮当前对话:指定 navEl 则仅更新该列表,否则两列表都更新(rant 2026-08-20T22:04:02 列表选中互不影响) */
/** 高亮当前会话条目(rant 11:44:52:历史会话列表已移除,仅剩打开会话区) */
function highlight(sid, navEl) {
const targets = navEl ? [navEl] : [$("conv-list"), $("open-sessions")];
const targets = navEl ? [navEl] : [$("open-sessions")];
for (const nav of targets) {
if (!nav) continue;
for (const item of nav.querySelectorAll(".conv-item")) {
Expand All@@ -93,55 +56,7 @@ const Sidebar = (() => {
}
}

// ── 键盘导航:↑↓ 切换高亮 / Enter 切换会话(与 TUI /resume 选择器一致)──
let _keyHandler = null;
let _focusIdx = -1;

function initKeyboard() {
const nav = $("conv-list");
_keyHandler = (e) => {
// 输入控件(textarea/input/select/contenteditable)内不劫持 ↑↓/Enter——
// 输入框是 <textarea id="input">,多行输入需 ↑↓ 移动光标、Shift+Enter 换行
if (e.target.closest("input, textarea, select, [contenteditable]")) return;
const items = nav.querySelectorAll(".conv-item");
if (!items.length) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (_focusIdx < 0) {
// 未聚焦:从当前会话开始
_focusIdx = items.findIndex((it) => it.classList.contains("active"));
if (_focusIdx < 0) _focusIdx = 0;
}
_focusIdx = e.key === "ArrowDown"
? (_focusIdx + 1) % items.length
: (_focusIdx - 1 + items.length) % items.length;
items.forEach((it, j) => it.classList.toggle("kbd-focus", j === _focusIdx));
items[_focusIdx].scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && _focusIdx >= 0) {
e.preventDefault();
const target = items[_focusIdx];
if (target.dataset.sid) App.switchSession(target.dataset.sid, { scopeNav: nav });
clearFocus();
} else if (e.key === "Escape") {
clearFocus();
}
};
document.addEventListener("keydown", _keyHandler);
}

function clearFocus() {
_focusIdx = -1;
for (const it of $("conv-list").querySelectorAll(".kbd-focus")) {
it.classList.remove("kbd-focus");
}
}

function init() {
if (!_keyHandler) initKeyboard();
}
init(); // 模块级绑定一次

return { render, renderOpenSessions, highlight, clearFocus };
return { renderOpenSessions, highlight };
})();

window.EMRG_Sidebar = Sidebar;
40 changes: 16 additions & 24 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,7 +123,7 @@ function makeEl(id) {
}

const ELEMENT_IDS = [
"workspace", "input", "send-btn", "stop-btn", "conv-list", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"workspace", "input", "send-btn", "stop-btn", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"open-session-dialog", "open-session-list", "open-session-title", "open-session-desc", "open-session-new", "open-session-cancel", "open-session-new-session",
"new-session-dialog", "new-session-list", "new-session-new", "new-session-cancel",
"conn-banner", "empty-state", "model-switcher", "model-switcher-label", "brand-star", "new-chat-btn", "open-chat-btn",
Expand DownExpand Up@@ -285,7 +285,7 @@ test("boot:config 缺失 → 首启引导(不拉起 daemon)", async () =>
assert.ok(els["welcome-dialog"].open, "welcome-dialog 应打开");
});

test("boot:config 就绪 → 加载会话列表", async () => {
test("boot:config 就绪 → 加载会话列表(rant 11:44:52:历史列表已移除,state 仍保留)", async () => {
const { ctx, els } = makeSandbox({
init: async () => ({
config_exists: true,
Expand All@@ -298,9 +298,9 @@ test("boot:config 就绪 → 加载会话列表", async () => {
switchSession: async () => ({}),
});
await tick();
// conv-list 应有会话项(rant 17:48:07:无分组标签,直接 project/name|id)
const items = vm.runInContext('document.getElementById("conv-list").children.length', ctx);
assert.ok(items >= 1, `conv-list 应有会话项,实际 ${items}`);
// rant 11:44:52:conv-list 不再渲染(历史会话列表移除);state.sessions 仍加载供切换/标题栏使用
const count = vm.runInContext('App.state.sessions.length', ctx);
assert.strictEqual(count, 1, `state.sessions 应有 1 条,实际 ${count}`);
});

test("流式 delta 追加 + 工具行 running→done 状态流转", async () => {
Expand DownExpand Up@@ -977,24 +977,18 @@ test("设置/首启对话框:Enter 提交键盘绑定(交互一致性)", a
assert.ok(bindBlock.includes("enterToSave"), "Enter 应通过 enterToSave 触发提交");
});

test("对话列表键盘导航:↑↓ 聚焦 / Enter 切换(与 TUI /resume 一致)", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
assert.ok(src.includes("ArrowDown"), "sidebar 应支持 ↑↓ 键盘导航");
assert.ok(src.includes("kbd-focus"), "应有键盘聚焦状态");
assert.ok(src.includes("App.switchSession"), "Enter 应切换会话");
const css = fs.readFileSync(path.join(__dirname, "..", "renderer", "css", "components.css"), "utf8");
assert.ok(css.includes(".conv-item.kbd-focus"), "键盘聚焦高亮样式应存在");
});
// rant 11:44:52:历史会话列表 + ↑↓/Enter 键盘导航已整体移除(sidebar.js 不再含 initKeyboard/clearFocus/kbd-focus)

test("对话列表键盘导航:输入控件内不劫持(e.target 守卫,textarea ↑↓/Enter 正常)", async () => {
test("rant 11:44:52: sidebar.js 已移除历史会话列表渲染与键盘导航", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
// 输入框是 <textarea id="input">,document 级常驻 keydown 必须跳过输入控件
assert.ok(src.includes("closest(\"input, textarea, select, [contenteditable]\")"), "输入控件内应跳过键盘导航");
assert.ok(src.includes("e.target.closest"), "应使用 e.target 守卫");
assert.ok(!src.includes("conv-list"), "sidebar 不应再引用 conv-list");
assert.ok(!src.includes("kbd-focus"), "sidebar 不应再有 kbd-focus 键盘聚焦");
assert.ok(!src.includes("initKeyboard"), "sidebar 不应再有 initKeyboard");
assert.ok(!src.includes("clearFocus"), "sidebar 不应再有 clearFocus");
const html = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf8");
assert.ok(!html.includes('id="conv-list"'), "index.html 不应再有 conv-list nav");
});

test("P3.2:tool_finished 只登记 write/edit 成功文件(bash 不登记)", async () => {
Expand DownExpand Up@@ -2020,7 +2014,7 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该
assert.ok(!(t2.textContent || "").includes("sess-local"), "有 title 时不显示 id");
});

test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表,另一列表保持原样", async () => {
test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表(rant 11:44:52:仅剩 open-sessions)", async () => {
const { ctx, els } = makeSandbox({});
await tick();
await vm.runInContext(
Expand All@@ -2035,11 +2029,9 @@ test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定
// 只高亮打开会话区(模拟点击打开会话列表条目)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b", document.getElementById("open-sessions"));', ctx);
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "open-sessions 条目高亮");
const convB = [...els["conv-list"].children].find((c) => c.dataset.sid === "sess-b");
assert.ok(convB && !convB.classList.contains("active"), "conv-list 中同 sid 条目不被联动高亮");
// 无 navEl 参数 → 两列表都更新(兼容初始渲染/全量刷新)
// 无 navEl 参数 → 全量刷新同样只作用于 open-sessions(conv-list 已不存在)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b");', ctx);
assert.strictEqual(convB.classList.contains("active"), true, "无 navEl 时两列表都高亮");
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "无 navEl 时 open-sessions 高亮");
});

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 sidebar — remove history session list + keyboard nav (rant 2026-08-21T11:44:52) by argszero · Pull Request #911 · 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
4 changes: 2 additions & 2 deletions Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request
- Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand)
- Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端"
- Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout)
- Unit tests `npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711)
- Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/<name>.md` → fallback
- Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`)
Expand DownExpand Up@@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)

Expand Down
6 changes: 0 additions & 6 deletions emrg/gui/renderer/css/components.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,12 +19,6 @@
background: var(--bg-soft);
color: var(--text-1);
}
/* 键盘导航聚焦(↑↓)——区别于当前会话高亮 */
.conv-item.kbd-focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--bg-soft);
}
.conv-item.active {
background: var(--accent-soft);
color: var(--accent);
Expand Down
1 change: 0 additions & 1 deletion emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,6 @@
<!-- P4 slice 2:跨项目打开的会话(浏览器 tab 效果,关闭保留数据) -->
<div class="conv-group-label open-sessions-label" id="open-sessions-label" data-i18n="sidebar.openSessions" hidden>打开的会话</div>
<nav id="open-sessions" class="open-sessions"></nav>
<nav id="conv-list"></nav>
<div class="sidebar-footer">
<span id="status-dot" class="status-dot gray" title="连接状态" data-i18n-title="sidebar.statusTitle"></span>
</div>
Expand Down
7 changes: 1 addition & 6 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,8 +94,7 @@ const App = (() => {
return;
}
if (init.sessions && init.sessions.length > 0) {
state.sessions = init.sessions;
Sidebar.render(init.sessions);
state.sessions = init.sessions; // rant 11:44:52:历史会话列表已移除,不再渲染 conv-list
// P4 slice 2:main 恢复的打开会话 + 激活会话(跳过 switchSession 的 IPC 往返)
state.openSessions = init.open_sessions || [];
Sidebar.renderOpenSessions(state.openSessions);
Expand DownExpand Up@@ -651,7 +650,6 @@ const App = (() => {
if (res.error === "session_not_found") {
Chat.addSystemMessage(_t("app.deletedSwitch"));
if (res.next_session) {
Sidebar.render(res.sessions || []);
await switchSession(res.next_session, { silent: true });
} else {
await newSession();
Expand DownExpand Up@@ -909,7 +907,6 @@ const App = (() => {
try {
const sessions = await window.emrg.listSessions();
state.sessions = sessions;
Sidebar.render(sessions);
Sidebar.renderOpenSessions(state.openSessions); // 打开会话区 title 同步刷新
renderSessionHeader(state.sessionId); // 激活会话标题栏同步刷新
} catch { /* 忽略 */ }
Expand DownExpand Up@@ -1491,7 +1488,6 @@ const App = (() => {
break;
case "sessions":
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
break;
case "open_sessions":
// P4 slice 2:main 广播打开会话列表变化 → 刷新侧边栏打开会话区
Expand DownExpand Up@@ -1539,7 +1535,6 @@ const App = (() => {
case "list_result":
if (data.type === "sessions_list") {
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
}
break;
case "command_result":
Expand Down
91 changes: 3 additions & 88 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@
*/

const Sidebar = (() => {
let sessions = [];
let openSessions = [];

/**
Expand DownExpand Up@@ -46,45 +45,9 @@ const Sidebar = (() => {
return title ? `${project}/${title}` : `${project}/${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");
nav.innerHTML = "";
if (!sessions.length) {
nav.appendChild(el("div", { class: "conv-item placeholder" }, EMRG_Copy.COPY.noSessions));
return;
}
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, { scopeNav: nav }));
// 右键菜单:重命名 / 删除(友好确认)
item.addEventListener("contextmenu", (e) => {
e.preventDefault();
App.showConvMenu(item, s.session_id, title || s.session_id);
});
nav.appendChild(item);
}
highlight(App.state.sessionId);
}

/** 高亮当前对话:指定 navEl 则仅更新该列表,否则两列表都更新(rant 2026-08-20T22:04:02 列表选中互不影响) */
/** 高亮当前会话条目(rant 11:44:52:历史会话列表已移除,仅剩打开会话区) */
function highlight(sid, navEl) {
const targets = navEl ? [navEl] : [$("conv-list"), $("open-sessions")];
const targets = navEl ? [navEl] : [$("open-sessions")];
for (const nav of targets) {
if (!nav) continue;
for (const item of nav.querySelectorAll(".conv-item")) {
Expand All@@ -93,55 +56,7 @@ const Sidebar = (() => {
}
}

// ── 键盘导航:↑↓ 切换高亮 / Enter 切换会话(与 TUI /resume 选择器一致)──
let _keyHandler = null;
let _focusIdx = -1;

function initKeyboard() {
const nav = $("conv-list");
_keyHandler = (e) => {
// 输入控件(textarea/input/select/contenteditable)内不劫持 ↑↓/Enter——
// 输入框是 <textarea id="input">,多行输入需 ↑↓ 移动光标、Shift+Enter 换行
if (e.target.closest("input, textarea, select, [contenteditable]")) return;
const items = nav.querySelectorAll(".conv-item");
if (!items.length) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (_focusIdx < 0) {
// 未聚焦:从当前会话开始
_focusIdx = items.findIndex((it) => it.classList.contains("active"));
if (_focusIdx < 0) _focusIdx = 0;
}
_focusIdx = e.key === "ArrowDown"
? (_focusIdx + 1) % items.length
: (_focusIdx - 1 + items.length) % items.length;
items.forEach((it, j) => it.classList.toggle("kbd-focus", j === _focusIdx));
items[_focusIdx].scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && _focusIdx >= 0) {
e.preventDefault();
const target = items[_focusIdx];
if (target.dataset.sid) App.switchSession(target.dataset.sid, { scopeNav: nav });
clearFocus();
} else if (e.key === "Escape") {
clearFocus();
}
};
document.addEventListener("keydown", _keyHandler);
}

function clearFocus() {
_focusIdx = -1;
for (const it of $("conv-list").querySelectorAll(".kbd-focus")) {
it.classList.remove("kbd-focus");
}
}

function init() {
if (!_keyHandler) initKeyboard();
}
init(); // 模块级绑定一次

return { render, renderOpenSessions, highlight, clearFocus };
return { renderOpenSessions, highlight };
})();

window.EMRG_Sidebar = Sidebar;
40 changes: 16 additions & 24 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,7 +123,7 @@ function makeEl(id) {
}

const ELEMENT_IDS = [
"workspace", "input", "send-btn", "stop-btn", "conv-list", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"workspace", "input", "send-btn", "stop-btn", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"open-session-dialog", "open-session-list", "open-session-title", "open-session-desc", "open-session-new", "open-session-cancel", "open-session-new-session",
"new-session-dialog", "new-session-list", "new-session-new", "new-session-cancel",
"conn-banner", "empty-state", "model-switcher", "model-switcher-label", "brand-star", "new-chat-btn", "open-chat-btn",
Expand DownExpand Up@@ -285,7 +285,7 @@ test("boot:config 缺失 → 首启引导(不拉起 daemon)", async () =>
assert.ok(els["welcome-dialog"].open, "welcome-dialog 应打开");
});

test("boot:config 就绪 → 加载会话列表", async () => {
test("boot:config 就绪 → 加载会话列表(rant 11:44:52:历史列表已移除,state 仍保留)", async () => {
const { ctx, els } = makeSandbox({
init: async () => ({
config_exists: true,
Expand All@@ -298,9 +298,9 @@ test("boot:config 就绪 → 加载会话列表", async () => {
switchSession: async () => ({}),
});
await tick();
// conv-list 应有会话项(rant 17:48:07:无分组标签,直接 project/name|id)
const items = vm.runInContext('document.getElementById("conv-list").children.length', ctx);
assert.ok(items >= 1, `conv-list 应有会话项,实际 ${items}`);
// rant 11:44:52:conv-list 不再渲染(历史会话列表移除);state.sessions 仍加载供切换/标题栏使用
const count = vm.runInContext('App.state.sessions.length', ctx);
assert.strictEqual(count, 1, `state.sessions 应有 1 条,实际 ${count}`);
});

test("流式 delta 追加 + 工具行 running→done 状态流转", async () => {
Expand DownExpand Up@@ -977,24 +977,18 @@ test("设置/首启对话框:Enter 提交键盘绑定(交互一致性)", a
assert.ok(bindBlock.includes("enterToSave"), "Enter 应通过 enterToSave 触发提交");
});

test("对话列表键盘导航:↑↓ 聚焦 / Enter 切换(与 TUI /resume 一致)", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
assert.ok(src.includes("ArrowDown"), "sidebar 应支持 ↑↓ 键盘导航");
assert.ok(src.includes("kbd-focus"), "应有键盘聚焦状态");
assert.ok(src.includes("App.switchSession"), "Enter 应切换会话");
const css = fs.readFileSync(path.join(__dirname, "..", "renderer", "css", "components.css"), "utf8");
assert.ok(css.includes(".conv-item.kbd-focus"), "键盘聚焦高亮样式应存在");
});
// rant 11:44:52:历史会话列表 + ↑↓/Enter 键盘导航已整体移除(sidebar.js 不再含 initKeyboard/clearFocus/kbd-focus)

test("对话列表键盘导航:输入控件内不劫持(e.target 守卫,textarea ↑↓/Enter 正常)", async () => {
test("rant 11:44:52: sidebar.js 已移除历史会话列表渲染与键盘导航", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
// 输入框是 <textarea id="input">,document 级常驻 keydown 必须跳过输入控件
assert.ok(src.includes("closest(\"input, textarea, select, [contenteditable]\")"), "输入控件内应跳过键盘导航");
assert.ok(src.includes("e.target.closest"), "应使用 e.target 守卫");
assert.ok(!src.includes("conv-list"), "sidebar 不应再引用 conv-list");
assert.ok(!src.includes("kbd-focus"), "sidebar 不应再有 kbd-focus 键盘聚焦");
assert.ok(!src.includes("initKeyboard"), "sidebar 不应再有 initKeyboard");
assert.ok(!src.includes("clearFocus"), "sidebar 不应再有 clearFocus");
const html = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf8");
assert.ok(!html.includes('id="conv-list"'), "index.html 不应再有 conv-list nav");
});

test("P3.2:tool_finished 只登记 write/edit 成功文件(bash 不登记)", async () => {
Expand DownExpand Up@@ -2020,7 +2014,7 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该
assert.ok(!(t2.textContent || "").includes("sess-local"), "有 title 时不显示 id");
});

test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表,另一列表保持原样", async () => {
test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表(rant 11:44:52:仅剩 open-sessions)", async () => {
const { ctx, els } = makeSandbox({});
await tick();
await vm.runInContext(
Expand All@@ -2035,11 +2029,9 @@ test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定
// 只高亮打开会话区(模拟点击打开会话列表条目)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b", document.getElementById("open-sessions"));', ctx);
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "open-sessions 条目高亮");
const convB = [...els["conv-list"].children].find((c) => c.dataset.sid === "sess-b");
assert.ok(convB && !convB.classList.contains("active"), "conv-list 中同 sid 条目不被联动高亮");
// 无 navEl 参数 → 两列表都更新(兼容初始渲染/全量刷新)
// 无 navEl 参数 → 全量刷新同样只作用于 open-sessions(conv-list 已不存在)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b");', ctx);
assert.strictEqual(convB.classList.contains("active"), true, "无 navEl 时两列表都高亮");
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "无 navEl 时 open-sessions 高亮");
});

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 sidebar — remove history session list + keyboard nav (rant 2026-08-21T11:44:52) by argszero · Pull Request #911 · 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
4 changes: 2 additions & 2 deletions Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request
- Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand)
- Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端"
- Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout)
- Unit tests `npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711)
- Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/<name>.md` → fallback
- Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`)
Expand DownExpand Up@@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)

Expand Down
6 changes: 0 additions & 6 deletions emrg/gui/renderer/css/components.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,12 +19,6 @@
background: var(--bg-soft);
color: var(--text-1);
}
/* 键盘导航聚焦(↑↓)——区别于当前会话高亮 */
.conv-item.kbd-focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--bg-soft);
}
.conv-item.active {
background: var(--accent-soft);
color: var(--accent);
Expand Down
1 change: 0 additions & 1 deletion emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,6 @@
<!-- P4 slice 2:跨项目打开的会话(浏览器 tab 效果,关闭保留数据) -->
<div class="conv-group-label open-sessions-label" id="open-sessions-label" data-i18n="sidebar.openSessions" hidden>打开的会话</div>
<nav id="open-sessions" class="open-sessions"></nav>
<nav id="conv-list"></nav>
<div class="sidebar-footer">
<span id="status-dot" class="status-dot gray" title="连接状态" data-i18n-title="sidebar.statusTitle"></span>
</div>
Expand Down
7 changes: 1 addition & 6 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,8 +94,7 @@ const App = (() => {
return;
}
if (init.sessions && init.sessions.length > 0) {
state.sessions = init.sessions;
Sidebar.render(init.sessions);
state.sessions = init.sessions; // rant 11:44:52:历史会话列表已移除,不再渲染 conv-list
// P4 slice 2:main 恢复的打开会话 + 激活会话(跳过 switchSession 的 IPC 往返)
state.openSessions = init.open_sessions || [];
Sidebar.renderOpenSessions(state.openSessions);
Expand DownExpand Up@@ -651,7 +650,6 @@ const App = (() => {
if (res.error === "session_not_found") {
Chat.addSystemMessage(_t("app.deletedSwitch"));
if (res.next_session) {
Sidebar.render(res.sessions || []);
await switchSession(res.next_session, { silent: true });
} else {
await newSession();
Expand DownExpand Up@@ -909,7 +907,6 @@ const App = (() => {
try {
const sessions = await window.emrg.listSessions();
state.sessions = sessions;
Sidebar.render(sessions);
Sidebar.renderOpenSessions(state.openSessions); // 打开会话区 title 同步刷新
renderSessionHeader(state.sessionId); // 激活会话标题栏同步刷新
} catch { /* 忽略 */ }
Expand DownExpand Up@@ -1491,7 +1488,6 @@ const App = (() => {
break;
case "sessions":
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
break;
case "open_sessions":
// P4 slice 2:main 广播打开会话列表变化 → 刷新侧边栏打开会话区
Expand DownExpand Up@@ -1539,7 +1535,6 @@ const App = (() => {
case "list_result":
if (data.type === "sessions_list") {
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
}
break;
case "command_result":
Expand Down
91 changes: 3 additions & 88 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@
*/

const Sidebar = (() => {
let sessions = [];
let openSessions = [];

/**
Expand DownExpand Up@@ -46,45 +45,9 @@ const Sidebar = (() => {
return title ? `${project}/${title}` : `${project}/${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");
nav.innerHTML = "";
if (!sessions.length) {
nav.appendChild(el("div", { class: "conv-item placeholder" }, EMRG_Copy.COPY.noSessions));
return;
}
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, { scopeNav: nav }));
// 右键菜单:重命名 / 删除(友好确认)
item.addEventListener("contextmenu", (e) => {
e.preventDefault();
App.showConvMenu(item, s.session_id, title || s.session_id);
});
nav.appendChild(item);
}
highlight(App.state.sessionId);
}

/** 高亮当前对话:指定 navEl 则仅更新该列表,否则两列表都更新(rant 2026-08-20T22:04:02 列表选中互不影响) */
/** 高亮当前会话条目(rant 11:44:52:历史会话列表已移除,仅剩打开会话区) */
function highlight(sid, navEl) {
const targets = navEl ? [navEl] : [$("conv-list"), $("open-sessions")];
const targets = navEl ? [navEl] : [$("open-sessions")];
for (const nav of targets) {
if (!nav) continue;
for (const item of nav.querySelectorAll(".conv-item")) {
Expand All@@ -93,55 +56,7 @@ const Sidebar = (() => {
}
}

// ── 键盘导航:↑↓ 切换高亮 / Enter 切换会话(与 TUI /resume 选择器一致)──
let _keyHandler = null;
let _focusIdx = -1;

function initKeyboard() {
const nav = $("conv-list");
_keyHandler = (e) => {
// 输入控件(textarea/input/select/contenteditable)内不劫持 ↑↓/Enter——
// 输入框是 <textarea id="input">,多行输入需 ↑↓ 移动光标、Shift+Enter 换行
if (e.target.closest("input, textarea, select, [contenteditable]")) return;
const items = nav.querySelectorAll(".conv-item");
if (!items.length) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (_focusIdx < 0) {
// 未聚焦:从当前会话开始
_focusIdx = items.findIndex((it) => it.classList.contains("active"));
if (_focusIdx < 0) _focusIdx = 0;
}
_focusIdx = e.key === "ArrowDown"
? (_focusIdx + 1) % items.length
: (_focusIdx - 1 + items.length) % items.length;
items.forEach((it, j) => it.classList.toggle("kbd-focus", j === _focusIdx));
items[_focusIdx].scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && _focusIdx >= 0) {
e.preventDefault();
const target = items[_focusIdx];
if (target.dataset.sid) App.switchSession(target.dataset.sid, { scopeNav: nav });
clearFocus();
} else if (e.key === "Escape") {
clearFocus();
}
};
document.addEventListener("keydown", _keyHandler);
}

function clearFocus() {
_focusIdx = -1;
for (const it of $("conv-list").querySelectorAll(".kbd-focus")) {
it.classList.remove("kbd-focus");
}
}

function init() {
if (!_keyHandler) initKeyboard();
}
init(); // 模块级绑定一次

return { render, renderOpenSessions, highlight, clearFocus };
return { renderOpenSessions, highlight };
})();

window.EMRG_Sidebar = Sidebar;
40 changes: 16 additions & 24 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,7 +123,7 @@ function makeEl(id) {
}

const ELEMENT_IDS = [
"workspace", "input", "send-btn", "stop-btn", "conv-list", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"workspace", "input", "send-btn", "stop-btn", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"open-session-dialog", "open-session-list", "open-session-title", "open-session-desc", "open-session-new", "open-session-cancel", "open-session-new-session",
"new-session-dialog", "new-session-list", "new-session-new", "new-session-cancel",
"conn-banner", "empty-state", "model-switcher", "model-switcher-label", "brand-star", "new-chat-btn", "open-chat-btn",
Expand DownExpand Up@@ -285,7 +285,7 @@ test("boot:config 缺失 → 首启引导(不拉起 daemon)", async () =>
assert.ok(els["welcome-dialog"].open, "welcome-dialog 应打开");
});

test("boot:config 就绪 → 加载会话列表", async () => {
test("boot:config 就绪 → 加载会话列表(rant 11:44:52:历史列表已移除,state 仍保留)", async () => {
const { ctx, els } = makeSandbox({
init: async () => ({
config_exists: true,
Expand All@@ -298,9 +298,9 @@ test("boot:config 就绪 → 加载会话列表", async () => {
switchSession: async () => ({}),
});
await tick();
// conv-list 应有会话项(rant 17:48:07:无分组标签,直接 project/name|id)
const items = vm.runInContext('document.getElementById("conv-list").children.length', ctx);
assert.ok(items >= 1, `conv-list 应有会话项,实际 ${items}`);
// rant 11:44:52:conv-list 不再渲染(历史会话列表移除);state.sessions 仍加载供切换/标题栏使用
const count = vm.runInContext('App.state.sessions.length', ctx);
assert.strictEqual(count, 1, `state.sessions 应有 1 条,实际 ${count}`);
});

test("流式 delta 追加 + 工具行 running→done 状态流转", async () => {
Expand DownExpand Up@@ -977,24 +977,18 @@ test("设置/首启对话框:Enter 提交键盘绑定(交互一致性)", a
assert.ok(bindBlock.includes("enterToSave"), "Enter 应通过 enterToSave 触发提交");
});

test("对话列表键盘导航:↑↓ 聚焦 / Enter 切换(与 TUI /resume 一致)", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
assert.ok(src.includes("ArrowDown"), "sidebar 应支持 ↑↓ 键盘导航");
assert.ok(src.includes("kbd-focus"), "应有键盘聚焦状态");
assert.ok(src.includes("App.switchSession"), "Enter 应切换会话");
const css = fs.readFileSync(path.join(__dirname, "..", "renderer", "css", "components.css"), "utf8");
assert.ok(css.includes(".conv-item.kbd-focus"), "键盘聚焦高亮样式应存在");
});
// rant 11:44:52:历史会话列表 + ↑↓/Enter 键盘导航已整体移除(sidebar.js 不再含 initKeyboard/clearFocus/kbd-focus)

test("对话列表键盘导航:输入控件内不劫持(e.target 守卫,textarea ↑↓/Enter 正常)", async () => {
test("rant 11:44:52: sidebar.js 已移除历史会话列表渲染与键盘导航", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
// 输入框是 <textarea id="input">,document 级常驻 keydown 必须跳过输入控件
assert.ok(src.includes("closest(\"input, textarea, select, [contenteditable]\")"), "输入控件内应跳过键盘导航");
assert.ok(src.includes("e.target.closest"), "应使用 e.target 守卫");
assert.ok(!src.includes("conv-list"), "sidebar 不应再引用 conv-list");
assert.ok(!src.includes("kbd-focus"), "sidebar 不应再有 kbd-focus 键盘聚焦");
assert.ok(!src.includes("initKeyboard"), "sidebar 不应再有 initKeyboard");
assert.ok(!src.includes("clearFocus"), "sidebar 不应再有 clearFocus");
const html = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf8");
assert.ok(!html.includes('id="conv-list"'), "index.html 不应再有 conv-list nav");
});

test("P3.2:tool_finished 只登记 write/edit 成功文件(bash 不登记)", async () => {
Expand DownExpand Up@@ -2020,7 +2014,7 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该
assert.ok(!(t2.textContent || "").includes("sess-local"), "有 title 时不显示 id");
});

test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表,另一列表保持原样", async () => {
test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表(rant 11:44:52:仅剩 open-sessions)", async () => {
const { ctx, els } = makeSandbox({});
await tick();
await vm.runInContext(
Expand All@@ -2035,11 +2029,9 @@ test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定
// 只高亮打开会话区(模拟点击打开会话列表条目)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b", document.getElementById("open-sessions"));', ctx);
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "open-sessions 条目高亮");
const convB = [...els["conv-list"].children].find((c) => c.dataset.sid === "sess-b");
assert.ok(convB && !convB.classList.contains("active"), "conv-list 中同 sid 条目不被联动高亮");
// 无 navEl 参数 → 两列表都更新(兼容初始渲染/全量刷新)
// 无 navEl 参数 → 全量刷新同样只作用于 open-sessions(conv-list 已不存在)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b");', ctx);
assert.strictEqual(convB.classList.contains("active"), true, "无 navEl 时两列表都高亮");
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "无 navEl 时 open-sessions 高亮");
});

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 sidebar — remove history session list + keyboard nav (rant 2026-08-21T11:44:52) by argszero · Pull Request #911 · 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
4 changes: 2 additions & 2 deletions Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request
- Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand)
- Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端"
- Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout)
- Unit tests `npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711)
- Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/<name>.md` → fallback
- Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`)
Expand DownExpand Up@@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)

Expand Down
6 changes: 0 additions & 6 deletions emrg/gui/renderer/css/components.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,12 +19,6 @@
background: var(--bg-soft);
color: var(--text-1);
}
/* 键盘导航聚焦(↑↓)——区别于当前会话高亮 */
.conv-item.kbd-focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--bg-soft);
}
.conv-item.active {
background: var(--accent-soft);
color: var(--accent);
Expand Down
1 change: 0 additions & 1 deletion emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,6 @@
<!-- P4 slice 2:跨项目打开的会话(浏览器 tab 效果,关闭保留数据) -->
<div class="conv-group-label open-sessions-label" id="open-sessions-label" data-i18n="sidebar.openSessions" hidden>打开的会话</div>
<nav id="open-sessions" class="open-sessions"></nav>
<nav id="conv-list"></nav>
<div class="sidebar-footer">
<span id="status-dot" class="status-dot gray" title="连接状态" data-i18n-title="sidebar.statusTitle"></span>
</div>
Expand Down
7 changes: 1 addition & 6 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,8 +94,7 @@ const App = (() => {
return;
}
if (init.sessions && init.sessions.length > 0) {
state.sessions = init.sessions;
Sidebar.render(init.sessions);
state.sessions = init.sessions; // rant 11:44:52:历史会话列表已移除,不再渲染 conv-list
// P4 slice 2:main 恢复的打开会话 + 激活会话(跳过 switchSession 的 IPC 往返)
state.openSessions = init.open_sessions || [];
Sidebar.renderOpenSessions(state.openSessions);
Expand DownExpand Up@@ -651,7 +650,6 @@ const App = (() => {
if (res.error === "session_not_found") {
Chat.addSystemMessage(_t("app.deletedSwitch"));
if (res.next_session) {
Sidebar.render(res.sessions || []);
await switchSession(res.next_session, { silent: true });
} else {
await newSession();
Expand DownExpand Up@@ -909,7 +907,6 @@ const App = (() => {
try {
const sessions = await window.emrg.listSessions();
state.sessions = sessions;
Sidebar.render(sessions);
Sidebar.renderOpenSessions(state.openSessions); // 打开会话区 title 同步刷新
renderSessionHeader(state.sessionId); // 激活会话标题栏同步刷新
} catch { /* 忽略 */ }
Expand DownExpand Up@@ -1491,7 +1488,6 @@ const App = (() => {
break;
case "sessions":
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
break;
case "open_sessions":
// P4 slice 2:main 广播打开会话列表变化 → 刷新侧边栏打开会话区
Expand DownExpand Up@@ -1539,7 +1535,6 @@ const App = (() => {
case "list_result":
if (data.type === "sessions_list") {
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
}
break;
case "command_result":
Expand Down
91 changes: 3 additions & 88 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@
*/

const Sidebar = (() => {
let sessions = [];
let openSessions = [];

/**
Expand DownExpand Up@@ -46,45 +45,9 @@ const Sidebar = (() => {
return title ? `${project}/${title}` : `${project}/${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");
nav.innerHTML = "";
if (!sessions.length) {
nav.appendChild(el("div", { class: "conv-item placeholder" }, EMRG_Copy.COPY.noSessions));
return;
}
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, { scopeNav: nav }));
// 右键菜单:重命名 / 删除(友好确认)
item.addEventListener("contextmenu", (e) => {
e.preventDefault();
App.showConvMenu(item, s.session_id, title || s.session_id);
});
nav.appendChild(item);
}
highlight(App.state.sessionId);
}

/** 高亮当前对话:指定 navEl 则仅更新该列表,否则两列表都更新(rant 2026-08-20T22:04:02 列表选中互不影响) */
/** 高亮当前会话条目(rant 11:44:52:历史会话列表已移除,仅剩打开会话区) */
function highlight(sid, navEl) {
const targets = navEl ? [navEl] : [$("conv-list"), $("open-sessions")];
const targets = navEl ? [navEl] : [$("open-sessions")];
for (const nav of targets) {
if (!nav) continue;
for (const item of nav.querySelectorAll(".conv-item")) {
Expand All@@ -93,55 +56,7 @@ const Sidebar = (() => {
}
}

// ── 键盘导航:↑↓ 切换高亮 / Enter 切换会话(与 TUI /resume 选择器一致)──
let _keyHandler = null;
let _focusIdx = -1;

function initKeyboard() {
const nav = $("conv-list");
_keyHandler = (e) => {
// 输入控件(textarea/input/select/contenteditable)内不劫持 ↑↓/Enter——
// 输入框是 <textarea id="input">,多行输入需 ↑↓ 移动光标、Shift+Enter 换行
if (e.target.closest("input, textarea, select, [contenteditable]")) return;
const items = nav.querySelectorAll(".conv-item");
if (!items.length) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (_focusIdx < 0) {
// 未聚焦:从当前会话开始
_focusIdx = items.findIndex((it) => it.classList.contains("active"));
if (_focusIdx < 0) _focusIdx = 0;
}
_focusIdx = e.key === "ArrowDown"
? (_focusIdx + 1) % items.length
: (_focusIdx - 1 + items.length) % items.length;
items.forEach((it, j) => it.classList.toggle("kbd-focus", j === _focusIdx));
items[_focusIdx].scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && _focusIdx >= 0) {
e.preventDefault();
const target = items[_focusIdx];
if (target.dataset.sid) App.switchSession(target.dataset.sid, { scopeNav: nav });
clearFocus();
} else if (e.key === "Escape") {
clearFocus();
}
};
document.addEventListener("keydown", _keyHandler);
}

function clearFocus() {
_focusIdx = -1;
for (const it of $("conv-list").querySelectorAll(".kbd-focus")) {
it.classList.remove("kbd-focus");
}
}

function init() {
if (!_keyHandler) initKeyboard();
}
init(); // 模块级绑定一次

return { render, renderOpenSessions, highlight, clearFocus };
return { renderOpenSessions, highlight };
})();

window.EMRG_Sidebar = Sidebar;
40 changes: 16 additions & 24 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,7 +123,7 @@ function makeEl(id) {
}

const ELEMENT_IDS = [
"workspace", "input", "send-btn", "stop-btn", "conv-list", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"workspace", "input", "send-btn", "stop-btn", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"open-session-dialog", "open-session-list", "open-session-title", "open-session-desc", "open-session-new", "open-session-cancel", "open-session-new-session",
"new-session-dialog", "new-session-list", "new-session-new", "new-session-cancel",
"conn-banner", "empty-state", "model-switcher", "model-switcher-label", "brand-star", "new-chat-btn", "open-chat-btn",
Expand DownExpand Up@@ -285,7 +285,7 @@ test("boot:config 缺失 → 首启引导(不拉起 daemon)", async () =>
assert.ok(els["welcome-dialog"].open, "welcome-dialog 应打开");
});

test("boot:config 就绪 → 加载会话列表", async () => {
test("boot:config 就绪 → 加载会话列表(rant 11:44:52:历史列表已移除,state 仍保留)", async () => {
const { ctx, els } = makeSandbox({
init: async () => ({
config_exists: true,
Expand All@@ -298,9 +298,9 @@ test("boot:config 就绪 → 加载会话列表", async () => {
switchSession: async () => ({}),
});
await tick();
// conv-list 应有会话项(rant 17:48:07:无分组标签,直接 project/name|id)
const items = vm.runInContext('document.getElementById("conv-list").children.length', ctx);
assert.ok(items >= 1, `conv-list 应有会话项,实际 ${items}`);
// rant 11:44:52:conv-list 不再渲染(历史会话列表移除);state.sessions 仍加载供切换/标题栏使用
const count = vm.runInContext('App.state.sessions.length', ctx);
assert.strictEqual(count, 1, `state.sessions 应有 1 条,实际 ${count}`);
});

test("流式 delta 追加 + 工具行 running→done 状态流转", async () => {
Expand DownExpand Up@@ -977,24 +977,18 @@ test("设置/首启对话框:Enter 提交键盘绑定(交互一致性)", a
assert.ok(bindBlock.includes("enterToSave"), "Enter 应通过 enterToSave 触发提交");
});

test("对话列表键盘导航:↑↓ 聚焦 / Enter 切换(与 TUI /resume 一致)", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
assert.ok(src.includes("ArrowDown"), "sidebar 应支持 ↑↓ 键盘导航");
assert.ok(src.includes("kbd-focus"), "应有键盘聚焦状态");
assert.ok(src.includes("App.switchSession"), "Enter 应切换会话");
const css = fs.readFileSync(path.join(__dirname, "..", "renderer", "css", "components.css"), "utf8");
assert.ok(css.includes(".conv-item.kbd-focus"), "键盘聚焦高亮样式应存在");
});
// rant 11:44:52:历史会话列表 + ↑↓/Enter 键盘导航已整体移除(sidebar.js 不再含 initKeyboard/clearFocus/kbd-focus)

test("对话列表键盘导航:输入控件内不劫持(e.target 守卫,textarea ↑↓/Enter 正常)", async () => {
test("rant 11:44:52: sidebar.js 已移除历史会话列表渲染与键盘导航", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
// 输入框是 <textarea id="input">,document 级常驻 keydown 必须跳过输入控件
assert.ok(src.includes("closest(\"input, textarea, select, [contenteditable]\")"), "输入控件内应跳过键盘导航");
assert.ok(src.includes("e.target.closest"), "应使用 e.target 守卫");
assert.ok(!src.includes("conv-list"), "sidebar 不应再引用 conv-list");
assert.ok(!src.includes("kbd-focus"), "sidebar 不应再有 kbd-focus 键盘聚焦");
assert.ok(!src.includes("initKeyboard"), "sidebar 不应再有 initKeyboard");
assert.ok(!src.includes("clearFocus"), "sidebar 不应再有 clearFocus");
const html = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf8");
assert.ok(!html.includes('id="conv-list"'), "index.html 不应再有 conv-list nav");
});

test("P3.2:tool_finished 只登记 write/edit 成功文件(bash 不登记)", async () => {
Expand DownExpand Up@@ -2020,7 +2014,7 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该
assert.ok(!(t2.textContent || "").includes("sess-local"), "有 title 时不显示 id");
});

test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表,另一列表保持原样", async () => {
test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表(rant 11:44:52:仅剩 open-sessions)", async () => {
const { ctx, els } = makeSandbox({});
await tick();
await vm.runInContext(
Expand All@@ -2035,11 +2029,9 @@ test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定
// 只高亮打开会话区(模拟点击打开会话列表条目)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b", document.getElementById("open-sessions"));', ctx);
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "open-sessions 条目高亮");
const convB = [...els["conv-list"].children].find((c) => c.dataset.sid === "sess-b");
assert.ok(convB && !convB.classList.contains("active"), "conv-list 中同 sid 条目不被联动高亮");
// 无 navEl 参数 → 两列表都更新(兼容初始渲染/全量刷新)
// 无 navEl 参数 → 全量刷新同样只作用于 open-sessions(conv-list 已不存在)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b");', ctx);
assert.strictEqual(convB.classList.contains("active"), true, "无 navEl 时两列表都高亮");
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "无 navEl 时 open-sessions 高亮");
});

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 sidebar — remove history session list + keyboard nav (rant 2026-08-21T11:44:52) by argszero · Pull Request #911 · 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
4 changes: 2 additions & 2 deletions Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request
- Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand)
- Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端"
- Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout)
- Unit tests `npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711)
- Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/<name>.md` → fallback
- Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`)
Expand DownExpand Up@@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)

Expand Down
6 changes: 0 additions & 6 deletions emrg/gui/renderer/css/components.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,12 +19,6 @@
background: var(--bg-soft);
color: var(--text-1);
}
/* 键盘导航聚焦(↑↓)——区别于当前会话高亮 */
.conv-item.kbd-focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--bg-soft);
}
.conv-item.active {
background: var(--accent-soft);
color: var(--accent);
Expand Down
1 change: 0 additions & 1 deletion emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,6 @@
<!-- P4 slice 2:跨项目打开的会话(浏览器 tab 效果,关闭保留数据) -->
<div class="conv-group-label open-sessions-label" id="open-sessions-label" data-i18n="sidebar.openSessions" hidden>打开的会话</div>
<nav id="open-sessions" class="open-sessions"></nav>
<nav id="conv-list"></nav>
<div class="sidebar-footer">
<span id="status-dot" class="status-dot gray" title="连接状态" data-i18n-title="sidebar.statusTitle"></span>
</div>
Expand Down
7 changes: 1 addition & 6 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,8 +94,7 @@ const App = (() => {
return;
}
if (init.sessions && init.sessions.length > 0) {
state.sessions = init.sessions;
Sidebar.render(init.sessions);
state.sessions = init.sessions; // rant 11:44:52:历史会话列表已移除,不再渲染 conv-list
// P4 slice 2:main 恢复的打开会话 + 激活会话(跳过 switchSession 的 IPC 往返)
state.openSessions = init.open_sessions || [];
Sidebar.renderOpenSessions(state.openSessions);
Expand DownExpand Up@@ -651,7 +650,6 @@ const App = (() => {
if (res.error === "session_not_found") {
Chat.addSystemMessage(_t("app.deletedSwitch"));
if (res.next_session) {
Sidebar.render(res.sessions || []);
await switchSession(res.next_session, { silent: true });
} else {
await newSession();
Expand DownExpand Up@@ -909,7 +907,6 @@ const App = (() => {
try {
const sessions = await window.emrg.listSessions();
state.sessions = sessions;
Sidebar.render(sessions);
Sidebar.renderOpenSessions(state.openSessions); // 打开会话区 title 同步刷新
renderSessionHeader(state.sessionId); // 激活会话标题栏同步刷新
} catch { /* 忽略 */ }
Expand DownExpand Up@@ -1491,7 +1488,6 @@ const App = (() => {
break;
case "sessions":
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
break;
case "open_sessions":
// P4 slice 2:main 广播打开会话列表变化 → 刷新侧边栏打开会话区
Expand DownExpand Up@@ -1539,7 +1535,6 @@ const App = (() => {
case "list_result":
if (data.type === "sessions_list") {
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
}
break;
case "command_result":
Expand Down
91 changes: 3 additions & 88 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@
*/

const Sidebar = (() => {
let sessions = [];
let openSessions = [];

/**
Expand DownExpand Up@@ -46,45 +45,9 @@ const Sidebar = (() => {
return title ? `${project}/${title}` : `${project}/${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");
nav.innerHTML = "";
if (!sessions.length) {
nav.appendChild(el("div", { class: "conv-item placeholder" }, EMRG_Copy.COPY.noSessions));
return;
}
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, { scopeNav: nav }));
// 右键菜单:重命名 / 删除(友好确认)
item.addEventListener("contextmenu", (e) => {
e.preventDefault();
App.showConvMenu(item, s.session_id, title || s.session_id);
});
nav.appendChild(item);
}
highlight(App.state.sessionId);
}

/** 高亮当前对话:指定 navEl 则仅更新该列表,否则两列表都更新(rant 2026-08-20T22:04:02 列表选中互不影响) */
/** 高亮当前会话条目(rant 11:44:52:历史会话列表已移除,仅剩打开会话区) */
function highlight(sid, navEl) {
const targets = navEl ? [navEl] : [$("conv-list"), $("open-sessions")];
const targets = navEl ? [navEl] : [$("open-sessions")];
for (const nav of targets) {
if (!nav) continue;
for (const item of nav.querySelectorAll(".conv-item")) {
Expand All@@ -93,55 +56,7 @@ const Sidebar = (() => {
}
}

// ── 键盘导航:↑↓ 切换高亮 / Enter 切换会话(与 TUI /resume 选择器一致)──
let _keyHandler = null;
let _focusIdx = -1;

function initKeyboard() {
const nav = $("conv-list");
_keyHandler = (e) => {
// 输入控件(textarea/input/select/contenteditable)内不劫持 ↑↓/Enter——
// 输入框是 <textarea id="input">,多行输入需 ↑↓ 移动光标、Shift+Enter 换行
if (e.target.closest("input, textarea, select, [contenteditable]")) return;
const items = nav.querySelectorAll(".conv-item");
if (!items.length) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (_focusIdx < 0) {
// 未聚焦:从当前会话开始
_focusIdx = items.findIndex((it) => it.classList.contains("active"));
if (_focusIdx < 0) _focusIdx = 0;
}
_focusIdx = e.key === "ArrowDown"
? (_focusIdx + 1) % items.length
: (_focusIdx - 1 + items.length) % items.length;
items.forEach((it, j) => it.classList.toggle("kbd-focus", j === _focusIdx));
items[_focusIdx].scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && _focusIdx >= 0) {
e.preventDefault();
const target = items[_focusIdx];
if (target.dataset.sid) App.switchSession(target.dataset.sid, { scopeNav: nav });
clearFocus();
} else if (e.key === "Escape") {
clearFocus();
}
};
document.addEventListener("keydown", _keyHandler);
}

function clearFocus() {
_focusIdx = -1;
for (const it of $("conv-list").querySelectorAll(".kbd-focus")) {
it.classList.remove("kbd-focus");
}
}

function init() {
if (!_keyHandler) initKeyboard();
}
init(); // 模块级绑定一次

return { render, renderOpenSessions, highlight, clearFocus };
return { renderOpenSessions, highlight };
})();

window.EMRG_Sidebar = Sidebar;
40 changes: 16 additions & 24 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,7 +123,7 @@ function makeEl(id) {
}

const ELEMENT_IDS = [
"workspace", "input", "send-btn", "stop-btn", "conv-list", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"workspace", "input", "send-btn", "stop-btn", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"open-session-dialog", "open-session-list", "open-session-title", "open-session-desc", "open-session-new", "open-session-cancel", "open-session-new-session",
"new-session-dialog", "new-session-list", "new-session-new", "new-session-cancel",
"conn-banner", "empty-state", "model-switcher", "model-switcher-label", "brand-star", "new-chat-btn", "open-chat-btn",
Expand DownExpand Up@@ -285,7 +285,7 @@ test("boot:config 缺失 → 首启引导(不拉起 daemon)", async () =>
assert.ok(els["welcome-dialog"].open, "welcome-dialog 应打开");
});

test("boot:config 就绪 → 加载会话列表", async () => {
test("boot:config 就绪 → 加载会话列表(rant 11:44:52:历史列表已移除,state 仍保留)", async () => {
const { ctx, els } = makeSandbox({
init: async () => ({
config_exists: true,
Expand All@@ -298,9 +298,9 @@ test("boot:config 就绪 → 加载会话列表", async () => {
switchSession: async () => ({}),
});
await tick();
// conv-list 应有会话项(rant 17:48:07:无分组标签,直接 project/name|id)
const items = vm.runInContext('document.getElementById("conv-list").children.length', ctx);
assert.ok(items >= 1, `conv-list 应有会话项,实际 ${items}`);
// rant 11:44:52:conv-list 不再渲染(历史会话列表移除);state.sessions 仍加载供切换/标题栏使用
const count = vm.runInContext('App.state.sessions.length', ctx);
assert.strictEqual(count, 1, `state.sessions 应有 1 条,实际 ${count}`);
});

test("流式 delta 追加 + 工具行 running→done 状态流转", async () => {
Expand DownExpand Up@@ -977,24 +977,18 @@ test("设置/首启对话框:Enter 提交键盘绑定(交互一致性)", a
assert.ok(bindBlock.includes("enterToSave"), "Enter 应通过 enterToSave 触发提交");
});

test("对话列表键盘导航:↑↓ 聚焦 / Enter 切换(与 TUI /resume 一致)", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
assert.ok(src.includes("ArrowDown"), "sidebar 应支持 ↑↓ 键盘导航");
assert.ok(src.includes("kbd-focus"), "应有键盘聚焦状态");
assert.ok(src.includes("App.switchSession"), "Enter 应切换会话");
const css = fs.readFileSync(path.join(__dirname, "..", "renderer", "css", "components.css"), "utf8");
assert.ok(css.includes(".conv-item.kbd-focus"), "键盘聚焦高亮样式应存在");
});
// rant 11:44:52:历史会话列表 + ↑↓/Enter 键盘导航已整体移除(sidebar.js 不再含 initKeyboard/clearFocus/kbd-focus)

test("对话列表键盘导航:输入控件内不劫持(e.target 守卫,textarea ↑↓/Enter 正常)", async () => {
test("rant 11:44:52: sidebar.js 已移除历史会话列表渲染与键盘导航", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
// 输入框是 <textarea id="input">,document 级常驻 keydown 必须跳过输入控件
assert.ok(src.includes("closest(\"input, textarea, select, [contenteditable]\")"), "输入控件内应跳过键盘导航");
assert.ok(src.includes("e.target.closest"), "应使用 e.target 守卫");
assert.ok(!src.includes("conv-list"), "sidebar 不应再引用 conv-list");
assert.ok(!src.includes("kbd-focus"), "sidebar 不应再有 kbd-focus 键盘聚焦");
assert.ok(!src.includes("initKeyboard"), "sidebar 不应再有 initKeyboard");
assert.ok(!src.includes("clearFocus"), "sidebar 不应再有 clearFocus");
const html = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf8");
assert.ok(!html.includes('id="conv-list"'), "index.html 不应再有 conv-list nav");
});

test("P3.2:tool_finished 只登记 write/edit 成功文件(bash 不登记)", async () => {
Expand DownExpand Up@@ -2020,7 +2014,7 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该
assert.ok(!(t2.textContent || "").includes("sess-local"), "有 title 时不显示 id");
});

test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表,另一列表保持原样", async () => {
test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表(rant 11:44:52:仅剩 open-sessions)", async () => {
const { ctx, els } = makeSandbox({});
await tick();
await vm.runInContext(
Expand All@@ -2035,11 +2029,9 @@ test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定
// 只高亮打开会话区(模拟点击打开会话列表条目)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b", document.getElementById("open-sessions"));', ctx);
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "open-sessions 条目高亮");
const convB = [...els["conv-list"].children].find((c) => c.dataset.sid === "sess-b");
assert.ok(convB && !convB.classList.contains("active"), "conv-list 中同 sid 条目不被联动高亮");
// 无 navEl 参数 → 两列表都更新(兼容初始渲染/全量刷新)
// 无 navEl 参数 → 全量刷新同样只作用于 open-sessions(conv-list 已不存在)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b");', ctx);
assert.strictEqual(convB.classList.contains("active"), true, "无 navEl 时两列表都高亮");
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "无 navEl 时 open-sessions 高亮");
});

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 sidebar — remove history session list + keyboard nav (rant 2026-08-21T11:44:52) by argszero · Pull Request #911 · 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
4 changes: 2 additions & 2 deletions Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request
- Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand)
- Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端"
- Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout)
- Unit tests `npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711)
- Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/<name>.md` → fallback
- Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`)
Expand DownExpand Up@@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)

Expand Down
6 changes: 0 additions & 6 deletions emrg/gui/renderer/css/components.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,12 +19,6 @@
background: var(--bg-soft);
color: var(--text-1);
}
/* 键盘导航聚焦(↑↓)——区别于当前会话高亮 */
.conv-item.kbd-focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--bg-soft);
}
.conv-item.active {
background: var(--accent-soft);
color: var(--accent);
Expand Down
1 change: 0 additions & 1 deletion emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,6 @@
<!-- P4 slice 2:跨项目打开的会话(浏览器 tab 效果,关闭保留数据) -->
<div class="conv-group-label open-sessions-label" id="open-sessions-label" data-i18n="sidebar.openSessions" hidden>打开的会话</div>
<nav id="open-sessions" class="open-sessions"></nav>
<nav id="conv-list"></nav>
<div class="sidebar-footer">
<span id="status-dot" class="status-dot gray" title="连接状态" data-i18n-title="sidebar.statusTitle"></span>
</div>
Expand Down
7 changes: 1 addition & 6 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,8 +94,7 @@ const App = (() => {
return;
}
if (init.sessions && init.sessions.length > 0) {
state.sessions = init.sessions;
Sidebar.render(init.sessions);
state.sessions = init.sessions; // rant 11:44:52:历史会话列表已移除,不再渲染 conv-list
// P4 slice 2:main 恢复的打开会话 + 激活会话(跳过 switchSession 的 IPC 往返)
state.openSessions = init.open_sessions || [];
Sidebar.renderOpenSessions(state.openSessions);
Expand DownExpand Up@@ -651,7 +650,6 @@ const App = (() => {
if (res.error === "session_not_found") {
Chat.addSystemMessage(_t("app.deletedSwitch"));
if (res.next_session) {
Sidebar.render(res.sessions || []);
await switchSession(res.next_session, { silent: true });
} else {
await newSession();
Expand DownExpand Up@@ -909,7 +907,6 @@ const App = (() => {
try {
const sessions = await window.emrg.listSessions();
state.sessions = sessions;
Sidebar.render(sessions);
Sidebar.renderOpenSessions(state.openSessions); // 打开会话区 title 同步刷新
renderSessionHeader(state.sessionId); // 激活会话标题栏同步刷新
} catch { /* 忽略 */ }
Expand DownExpand Up@@ -1491,7 +1488,6 @@ const App = (() => {
break;
case "sessions":
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
break;
case "open_sessions":
// P4 slice 2:main 广播打开会话列表变化 → 刷新侧边栏打开会话区
Expand DownExpand Up@@ -1539,7 +1535,6 @@ const App = (() => {
case "list_result":
if (data.type === "sessions_list") {
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
}
break;
case "command_result":
Expand Down
91 changes: 3 additions & 88 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@
*/

const Sidebar = (() => {
let sessions = [];
let openSessions = [];

/**
Expand DownExpand Up@@ -46,45 +45,9 @@ const Sidebar = (() => {
return title ? `${project}/${title}` : `${project}/${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");
nav.innerHTML = "";
if (!sessions.length) {
nav.appendChild(el("div", { class: "conv-item placeholder" }, EMRG_Copy.COPY.noSessions));
return;
}
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, { scopeNav: nav }));
// 右键菜单:重命名 / 删除(友好确认)
item.addEventListener("contextmenu", (e) => {
e.preventDefault();
App.showConvMenu(item, s.session_id, title || s.session_id);
});
nav.appendChild(item);
}
highlight(App.state.sessionId);
}

/** 高亮当前对话:指定 navEl 则仅更新该列表,否则两列表都更新(rant 2026-08-20T22:04:02 列表选中互不影响) */
/** 高亮当前会话条目(rant 11:44:52:历史会话列表已移除,仅剩打开会话区) */
function highlight(sid, navEl) {
const targets = navEl ? [navEl] : [$("conv-list"), $("open-sessions")];
const targets = navEl ? [navEl] : [$("open-sessions")];
for (const nav of targets) {
if (!nav) continue;
for (const item of nav.querySelectorAll(".conv-item")) {
Expand All@@ -93,55 +56,7 @@ const Sidebar = (() => {
}
}

// ── 键盘导航:↑↓ 切换高亮 / Enter 切换会话(与 TUI /resume 选择器一致)──
let _keyHandler = null;
let _focusIdx = -1;

function initKeyboard() {
const nav = $("conv-list");
_keyHandler = (e) => {
// 输入控件(textarea/input/select/contenteditable)内不劫持 ↑↓/Enter——
// 输入框是 <textarea id="input">,多行输入需 ↑↓ 移动光标、Shift+Enter 换行
if (e.target.closest("input, textarea, select, [contenteditable]")) return;
const items = nav.querySelectorAll(".conv-item");
if (!items.length) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (_focusIdx < 0) {
// 未聚焦:从当前会话开始
_focusIdx = items.findIndex((it) => it.classList.contains("active"));
if (_focusIdx < 0) _focusIdx = 0;
}
_focusIdx = e.key === "ArrowDown"
? (_focusIdx + 1) % items.length
: (_focusIdx - 1 + items.length) % items.length;
items.forEach((it, j) => it.classList.toggle("kbd-focus", j === _focusIdx));
items[_focusIdx].scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && _focusIdx >= 0) {
e.preventDefault();
const target = items[_focusIdx];
if (target.dataset.sid) App.switchSession(target.dataset.sid, { scopeNav: nav });
clearFocus();
} else if (e.key === "Escape") {
clearFocus();
}
};
document.addEventListener("keydown", _keyHandler);
}

function clearFocus() {
_focusIdx = -1;
for (const it of $("conv-list").querySelectorAll(".kbd-focus")) {
it.classList.remove("kbd-focus");
}
}

function init() {
if (!_keyHandler) initKeyboard();
}
init(); // 模块级绑定一次

return { render, renderOpenSessions, highlight, clearFocus };
return { renderOpenSessions, highlight };
})();

window.EMRG_Sidebar = Sidebar;
40 changes: 16 additions & 24 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,7 +123,7 @@ function makeEl(id) {
}

const ELEMENT_IDS = [
"workspace", "input", "send-btn", "stop-btn", "conv-list", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"workspace", "input", "send-btn", "stop-btn", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"open-session-dialog", "open-session-list", "open-session-title", "open-session-desc", "open-session-new", "open-session-cancel", "open-session-new-session",
"new-session-dialog", "new-session-list", "new-session-new", "new-session-cancel",
"conn-banner", "empty-state", "model-switcher", "model-switcher-label", "brand-star", "new-chat-btn", "open-chat-btn",
Expand DownExpand Up@@ -285,7 +285,7 @@ test("boot:config 缺失 → 首启引导(不拉起 daemon)", async () =>
assert.ok(els["welcome-dialog"].open, "welcome-dialog 应打开");
});

test("boot:config 就绪 → 加载会话列表", async () => {
test("boot:config 就绪 → 加载会话列表(rant 11:44:52:历史列表已移除,state 仍保留)", async () => {
const { ctx, els } = makeSandbox({
init: async () => ({
config_exists: true,
Expand All@@ -298,9 +298,9 @@ test("boot:config 就绪 → 加载会话列表", async () => {
switchSession: async () => ({}),
});
await tick();
// conv-list 应有会话项(rant 17:48:07:无分组标签,直接 project/name|id)
const items = vm.runInContext('document.getElementById("conv-list").children.length', ctx);
assert.ok(items >= 1, `conv-list 应有会话项,实际 ${items}`);
// rant 11:44:52:conv-list 不再渲染(历史会话列表移除);state.sessions 仍加载供切换/标题栏使用
const count = vm.runInContext('App.state.sessions.length', ctx);
assert.strictEqual(count, 1, `state.sessions 应有 1 条,实际 ${count}`);
});

test("流式 delta 追加 + 工具行 running→done 状态流转", async () => {
Expand DownExpand Up@@ -977,24 +977,18 @@ test("设置/首启对话框:Enter 提交键盘绑定(交互一致性)", a
assert.ok(bindBlock.includes("enterToSave"), "Enter 应通过 enterToSave 触发提交");
});

test("对话列表键盘导航:↑↓ 聚焦 / Enter 切换(与 TUI /resume 一致)", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
assert.ok(src.includes("ArrowDown"), "sidebar 应支持 ↑↓ 键盘导航");
assert.ok(src.includes("kbd-focus"), "应有键盘聚焦状态");
assert.ok(src.includes("App.switchSession"), "Enter 应切换会话");
const css = fs.readFileSync(path.join(__dirname, "..", "renderer", "css", "components.css"), "utf8");
assert.ok(css.includes(".conv-item.kbd-focus"), "键盘聚焦高亮样式应存在");
});
// rant 11:44:52:历史会话列表 + ↑↓/Enter 键盘导航已整体移除(sidebar.js 不再含 initKeyboard/clearFocus/kbd-focus)

test("对话列表键盘导航:输入控件内不劫持(e.target 守卫,textarea ↑↓/Enter 正常)", async () => {
test("rant 11:44:52: sidebar.js 已移除历史会话列表渲染与键盘导航", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
// 输入框是 <textarea id="input">,document 级常驻 keydown 必须跳过输入控件
assert.ok(src.includes("closest(\"input, textarea, select, [contenteditable]\")"), "输入控件内应跳过键盘导航");
assert.ok(src.includes("e.target.closest"), "应使用 e.target 守卫");
assert.ok(!src.includes("conv-list"), "sidebar 不应再引用 conv-list");
assert.ok(!src.includes("kbd-focus"), "sidebar 不应再有 kbd-focus 键盘聚焦");
assert.ok(!src.includes("initKeyboard"), "sidebar 不应再有 initKeyboard");
assert.ok(!src.includes("clearFocus"), "sidebar 不应再有 clearFocus");
const html = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf8");
assert.ok(!html.includes('id="conv-list"'), "index.html 不应再有 conv-list nav");
});

test("P3.2:tool_finished 只登记 write/edit 成功文件(bash 不登记)", async () => {
Expand DownExpand Up@@ -2020,7 +2014,7 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该
assert.ok(!(t2.textContent || "").includes("sess-local"), "有 title 时不显示 id");
});

test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表,另一列表保持原样", async () => {
test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表(rant 11:44:52:仅剩 open-sessions)", async () => {
const { ctx, els } = makeSandbox({});
await tick();
await vm.runInContext(
Expand All@@ -2035,11 +2029,9 @@ test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定
// 只高亮打开会话区(模拟点击打开会话列表条目)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b", document.getElementById("open-sessions"));', ctx);
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "open-sessions 条目高亮");
const convB = [...els["conv-list"].children].find((c) => c.dataset.sid === "sess-b");
assert.ok(convB && !convB.classList.contains("active"), "conv-list 中同 sid 条目不被联动高亮");
// 无 navEl 参数 → 两列表都更新(兼容初始渲染/全量刷新)
// 无 navEl 参数 → 全量刷新同样只作用于 open-sessions(conv-list 已不存在)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b");', ctx);
assert.strictEqual(convB.classList.contains("active"), true, "无 navEl 时两列表都高亮");
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "无 navEl 时 open-sessions 高亮");
});

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 sidebar — remove history session list + keyboard nav (rant 2026-08-21T11:44:52) by argszero · Pull Request #911 · 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
4 changes: 2 additions & 2 deletions Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request
- Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand)
- Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端"
- Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout)
- Unit tests `npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711)
- Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/<name>.md` → fallback
- Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`)
Expand DownExpand Up@@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)

Expand Down
6 changes: 0 additions & 6 deletions emrg/gui/renderer/css/components.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,12 +19,6 @@
background: var(--bg-soft);
color: var(--text-1);
}
/* 键盘导航聚焦(↑↓)——区别于当前会话高亮 */
.conv-item.kbd-focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--bg-soft);
}
.conv-item.active {
background: var(--accent-soft);
color: var(--accent);
Expand Down
1 change: 0 additions & 1 deletion emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,6 @@
<!-- P4 slice 2:跨项目打开的会话(浏览器 tab 效果,关闭保留数据) -->
<div class="conv-group-label open-sessions-label" id="open-sessions-label" data-i18n="sidebar.openSessions" hidden>打开的会话</div>
<nav id="open-sessions" class="open-sessions"></nav>
<nav id="conv-list"></nav>
<div class="sidebar-footer">
<span id="status-dot" class="status-dot gray" title="连接状态" data-i18n-title="sidebar.statusTitle"></span>
</div>
Expand Down
7 changes: 1 addition & 6 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,8 +94,7 @@ const App = (() => {
return;
}
if (init.sessions && init.sessions.length > 0) {
state.sessions = init.sessions;
Sidebar.render(init.sessions);
state.sessions = init.sessions; // rant 11:44:52:历史会话列表已移除,不再渲染 conv-list
// P4 slice 2:main 恢复的打开会话 + 激活会话(跳过 switchSession 的 IPC 往返)
state.openSessions = init.open_sessions || [];
Sidebar.renderOpenSessions(state.openSessions);
Expand DownExpand Up@@ -651,7 +650,6 @@ const App = (() => {
if (res.error === "session_not_found") {
Chat.addSystemMessage(_t("app.deletedSwitch"));
if (res.next_session) {
Sidebar.render(res.sessions || []);
await switchSession(res.next_session, { silent: true });
} else {
await newSession();
Expand DownExpand Up@@ -909,7 +907,6 @@ const App = (() => {
try {
const sessions = await window.emrg.listSessions();
state.sessions = sessions;
Sidebar.render(sessions);
Sidebar.renderOpenSessions(state.openSessions); // 打开会话区 title 同步刷新
renderSessionHeader(state.sessionId); // 激活会话标题栏同步刷新
} catch { /* 忽略 */ }
Expand DownExpand Up@@ -1491,7 +1488,6 @@ const App = (() => {
break;
case "sessions":
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
break;
case "open_sessions":
// P4 slice 2:main 广播打开会话列表变化 → 刷新侧边栏打开会话区
Expand DownExpand Up@@ -1539,7 +1535,6 @@ const App = (() => {
case "list_result":
if (data.type === "sessions_list") {
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
}
break;
case "command_result":
Expand Down
91 changes: 3 additions & 88 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@
*/

const Sidebar = (() => {
let sessions = [];
let openSessions = [];

/**
Expand DownExpand Up@@ -46,45 +45,9 @@ const Sidebar = (() => {
return title ? `${project}/${title}` : `${project}/${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");
nav.innerHTML = "";
if (!sessions.length) {
nav.appendChild(el("div", { class: "conv-item placeholder" }, EMRG_Copy.COPY.noSessions));
return;
}
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, { scopeNav: nav }));
// 右键菜单:重命名 / 删除(友好确认)
item.addEventListener("contextmenu", (e) => {
e.preventDefault();
App.showConvMenu(item, s.session_id, title || s.session_id);
});
nav.appendChild(item);
}
highlight(App.state.sessionId);
}

/** 高亮当前对话:指定 navEl 则仅更新该列表,否则两列表都更新(rant 2026-08-20T22:04:02 列表选中互不影响) */
/** 高亮当前会话条目(rant 11:44:52:历史会话列表已移除,仅剩打开会话区) */
function highlight(sid, navEl) {
const targets = navEl ? [navEl] : [$("conv-list"), $("open-sessions")];
const targets = navEl ? [navEl] : [$("open-sessions")];
for (const nav of targets) {
if (!nav) continue;
for (const item of nav.querySelectorAll(".conv-item")) {
Expand All@@ -93,55 +56,7 @@ const Sidebar = (() => {
}
}

// ── 键盘导航:↑↓ 切换高亮 / Enter 切换会话(与 TUI /resume 选择器一致)──
let _keyHandler = null;
let _focusIdx = -1;

function initKeyboard() {
const nav = $("conv-list");
_keyHandler = (e) => {
// 输入控件(textarea/input/select/contenteditable)内不劫持 ↑↓/Enter——
// 输入框是 <textarea id="input">,多行输入需 ↑↓ 移动光标、Shift+Enter 换行
if (e.target.closest("input, textarea, select, [contenteditable]")) return;
const items = nav.querySelectorAll(".conv-item");
if (!items.length) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (_focusIdx < 0) {
// 未聚焦:从当前会话开始
_focusIdx = items.findIndex((it) => it.classList.contains("active"));
if (_focusIdx < 0) _focusIdx = 0;
}
_focusIdx = e.key === "ArrowDown"
? (_focusIdx + 1) % items.length
: (_focusIdx - 1 + items.length) % items.length;
items.forEach((it, j) => it.classList.toggle("kbd-focus", j === _focusIdx));
items[_focusIdx].scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && _focusIdx >= 0) {
e.preventDefault();
const target = items[_focusIdx];
if (target.dataset.sid) App.switchSession(target.dataset.sid, { scopeNav: nav });
clearFocus();
} else if (e.key === "Escape") {
clearFocus();
}
};
document.addEventListener("keydown", _keyHandler);
}

function clearFocus() {
_focusIdx = -1;
for (const it of $("conv-list").querySelectorAll(".kbd-focus")) {
it.classList.remove("kbd-focus");
}
}

function init() {
if (!_keyHandler) initKeyboard();
}
init(); // 模块级绑定一次

return { render, renderOpenSessions, highlight, clearFocus };
return { renderOpenSessions, highlight };
})();

window.EMRG_Sidebar = Sidebar;
40 changes: 16 additions & 24 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,7 +123,7 @@ function makeEl(id) {
}

const ELEMENT_IDS = [
"workspace", "input", "send-btn", "stop-btn", "conv-list", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"workspace", "input", "send-btn", "stop-btn", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"open-session-dialog", "open-session-list", "open-session-title", "open-session-desc", "open-session-new", "open-session-cancel", "open-session-new-session",
"new-session-dialog", "new-session-list", "new-session-new", "new-session-cancel",
"conn-banner", "empty-state", "model-switcher", "model-switcher-label", "brand-star", "new-chat-btn", "open-chat-btn",
Expand DownExpand Up@@ -285,7 +285,7 @@ test("boot:config 缺失 → 首启引导(不拉起 daemon)", async () =>
assert.ok(els["welcome-dialog"].open, "welcome-dialog 应打开");
});

test("boot:config 就绪 → 加载会话列表", async () => {
test("boot:config 就绪 → 加载会话列表(rant 11:44:52:历史列表已移除,state 仍保留)", async () => {
const { ctx, els } = makeSandbox({
init: async () => ({
config_exists: true,
Expand All@@ -298,9 +298,9 @@ test("boot:config 就绪 → 加载会话列表", async () => {
switchSession: async () => ({}),
});
await tick();
// conv-list 应有会话项(rant 17:48:07:无分组标签,直接 project/name|id)
const items = vm.runInContext('document.getElementById("conv-list").children.length', ctx);
assert.ok(items >= 1, `conv-list 应有会话项,实际 ${items}`);
// rant 11:44:52:conv-list 不再渲染(历史会话列表移除);state.sessions 仍加载供切换/标题栏使用
const count = vm.runInContext('App.state.sessions.length', ctx);
assert.strictEqual(count, 1, `state.sessions 应有 1 条,实际 ${count}`);
});

test("流式 delta 追加 + 工具行 running→done 状态流转", async () => {
Expand DownExpand Up@@ -977,24 +977,18 @@ test("设置/首启对话框:Enter 提交键盘绑定(交互一致性)", a
assert.ok(bindBlock.includes("enterToSave"), "Enter 应通过 enterToSave 触发提交");
});

test("对话列表键盘导航:↑↓ 聚焦 / Enter 切换(与 TUI /resume 一致)", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
assert.ok(src.includes("ArrowDown"), "sidebar 应支持 ↑↓ 键盘导航");
assert.ok(src.includes("kbd-focus"), "应有键盘聚焦状态");
assert.ok(src.includes("App.switchSession"), "Enter 应切换会话");
const css = fs.readFileSync(path.join(__dirname, "..", "renderer", "css", "components.css"), "utf8");
assert.ok(css.includes(".conv-item.kbd-focus"), "键盘聚焦高亮样式应存在");
});
// rant 11:44:52:历史会话列表 + ↑↓/Enter 键盘导航已整体移除(sidebar.js 不再含 initKeyboard/clearFocus/kbd-focus)

test("对话列表键盘导航:输入控件内不劫持(e.target 守卫,textarea ↑↓/Enter 正常)", async () => {
test("rant 11:44:52: sidebar.js 已移除历史会话列表渲染与键盘导航", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
// 输入框是 <textarea id="input">,document 级常驻 keydown 必须跳过输入控件
assert.ok(src.includes("closest(\"input, textarea, select, [contenteditable]\")"), "输入控件内应跳过键盘导航");
assert.ok(src.includes("e.target.closest"), "应使用 e.target 守卫");
assert.ok(!src.includes("conv-list"), "sidebar 不应再引用 conv-list");
assert.ok(!src.includes("kbd-focus"), "sidebar 不应再有 kbd-focus 键盘聚焦");
assert.ok(!src.includes("initKeyboard"), "sidebar 不应再有 initKeyboard");
assert.ok(!src.includes("clearFocus"), "sidebar 不应再有 clearFocus");
const html = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf8");
assert.ok(!html.includes('id="conv-list"'), "index.html 不应再有 conv-list nav");
});

test("P3.2:tool_finished 只登记 write/edit 成功文件(bash 不登记)", async () => {
Expand DownExpand Up@@ -2020,7 +2014,7 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该
assert.ok(!(t2.textContent || "").includes("sess-local"), "有 title 时不显示 id");
});

test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表,另一列表保持原样", async () => {
test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表(rant 11:44:52:仅剩 open-sessions)", async () => {
const { ctx, els } = makeSandbox({});
await tick();
await vm.runInContext(
Expand All@@ -2035,11 +2029,9 @@ test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定
// 只高亮打开会话区(模拟点击打开会话列表条目)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b", document.getElementById("open-sessions"));', ctx);
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "open-sessions 条目高亮");
const convB = [...els["conv-list"].children].find((c) => c.dataset.sid === "sess-b");
assert.ok(convB && !convB.classList.contains("active"), "conv-list 中同 sid 条目不被联动高亮");
// 无 navEl 参数 → 两列表都更新(兼容初始渲染/全量刷新)
// 无 navEl 参数 → 全量刷新同样只作用于 open-sessions(conv-list 已不存在)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b");', ctx);
assert.strictEqual(convB.classList.contains("active"), true, "无 navEl 时两列表都高亮");
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "无 navEl 时 open-sessions 高亮");
});

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 sidebar — remove history session list + keyboard nav (rant 2026-08-21T11:44:52) by argszero · Pull Request #911 · 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
4 changes: 2 additions & 2 deletions Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request
- Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand)
- Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端"
- Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout)
- Unit tests `npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711)
- Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/<name>.md` → fallback
- Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`)
Expand DownExpand Up@@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)

Expand Down
6 changes: 0 additions & 6 deletions emrg/gui/renderer/css/components.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,12 +19,6 @@
background: var(--bg-soft);
color: var(--text-1);
}
/* 键盘导航聚焦(↑↓)——区别于当前会话高亮 */
.conv-item.kbd-focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--bg-soft);
}
.conv-item.active {
background: var(--accent-soft);
color: var(--accent);
Expand Down
1 change: 0 additions & 1 deletion emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,6 @@
<!-- P4 slice 2:跨项目打开的会话(浏览器 tab 效果,关闭保留数据) -->
<div class="conv-group-label open-sessions-label" id="open-sessions-label" data-i18n="sidebar.openSessions" hidden>打开的会话</div>
<nav id="open-sessions" class="open-sessions"></nav>
<nav id="conv-list"></nav>
<div class="sidebar-footer">
<span id="status-dot" class="status-dot gray" title="连接状态" data-i18n-title="sidebar.statusTitle"></span>
</div>
Expand Down
7 changes: 1 addition & 6 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,8 +94,7 @@ const App = (() => {
return;
}
if (init.sessions && init.sessions.length > 0) {
state.sessions = init.sessions;
Sidebar.render(init.sessions);
state.sessions = init.sessions; // rant 11:44:52:历史会话列表已移除,不再渲染 conv-list
// P4 slice 2:main 恢复的打开会话 + 激活会话(跳过 switchSession 的 IPC 往返)
state.openSessions = init.open_sessions || [];
Sidebar.renderOpenSessions(state.openSessions);
Expand DownExpand Up@@ -651,7 +650,6 @@ const App = (() => {
if (res.error === "session_not_found") {
Chat.addSystemMessage(_t("app.deletedSwitch"));
if (res.next_session) {
Sidebar.render(res.sessions || []);
await switchSession(res.next_session, { silent: true });
} else {
await newSession();
Expand DownExpand Up@@ -909,7 +907,6 @@ const App = (() => {
try {
const sessions = await window.emrg.listSessions();
state.sessions = sessions;
Sidebar.render(sessions);
Sidebar.renderOpenSessions(state.openSessions); // 打开会话区 title 同步刷新
renderSessionHeader(state.sessionId); // 激活会话标题栏同步刷新
} catch { /* 忽略 */ }
Expand DownExpand Up@@ -1491,7 +1488,6 @@ const App = (() => {
break;
case "sessions":
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
break;
case "open_sessions":
// P4 slice 2:main 广播打开会话列表变化 → 刷新侧边栏打开会话区
Expand DownExpand Up@@ -1539,7 +1535,6 @@ const App = (() => {
case "list_result":
if (data.type === "sessions_list") {
state.sessions = data.sessions || [];
Sidebar.render(data.sessions || []);
}
break;
case "command_result":
Expand Down
91 changes: 3 additions & 88 deletions emrg/gui/renderer/js/sidebar.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@
*/

const Sidebar = (() => {
let sessions = [];
let openSessions = [];

/**
Expand DownExpand Up@@ -46,45 +45,9 @@ const Sidebar = (() => {
return title ? `${project}/${title}` : `${project}/${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");
nav.innerHTML = "";
if (!sessions.length) {
nav.appendChild(el("div", { class: "conv-item placeholder" }, EMRG_Copy.COPY.noSessions));
return;
}
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, { scopeNav: nav }));
// 右键菜单:重命名 / 删除(友好确认)
item.addEventListener("contextmenu", (e) => {
e.preventDefault();
App.showConvMenu(item, s.session_id, title || s.session_id);
});
nav.appendChild(item);
}
highlight(App.state.sessionId);
}

/** 高亮当前对话:指定 navEl 则仅更新该列表,否则两列表都更新(rant 2026-08-20T22:04:02 列表选中互不影响) */
/** 高亮当前会话条目(rant 11:44:52:历史会话列表已移除,仅剩打开会话区) */
function highlight(sid, navEl) {
const targets = navEl ? [navEl] : [$("conv-list"), $("open-sessions")];
const targets = navEl ? [navEl] : [$("open-sessions")];
for (const nav of targets) {
if (!nav) continue;
for (const item of nav.querySelectorAll(".conv-item")) {
Expand All@@ -93,55 +56,7 @@ const Sidebar = (() => {
}
}

// ── 键盘导航:↑↓ 切换高亮 / Enter 切换会话(与 TUI /resume 选择器一致)──
let _keyHandler = null;
let _focusIdx = -1;

function initKeyboard() {
const nav = $("conv-list");
_keyHandler = (e) => {
// 输入控件(textarea/input/select/contenteditable)内不劫持 ↑↓/Enter——
// 输入框是 <textarea id="input">,多行输入需 ↑↓ 移动光标、Shift+Enter 换行
if (e.target.closest("input, textarea, select, [contenteditable]")) return;
const items = nav.querySelectorAll(".conv-item");
if (!items.length) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (_focusIdx < 0) {
// 未聚焦:从当前会话开始
_focusIdx = items.findIndex((it) => it.classList.contains("active"));
if (_focusIdx < 0) _focusIdx = 0;
}
_focusIdx = e.key === "ArrowDown"
? (_focusIdx + 1) % items.length
: (_focusIdx - 1 + items.length) % items.length;
items.forEach((it, j) => it.classList.toggle("kbd-focus", j === _focusIdx));
items[_focusIdx].scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && _focusIdx >= 0) {
e.preventDefault();
const target = items[_focusIdx];
if (target.dataset.sid) App.switchSession(target.dataset.sid, { scopeNav: nav });
clearFocus();
} else if (e.key === "Escape") {
clearFocus();
}
};
document.addEventListener("keydown", _keyHandler);
}

function clearFocus() {
_focusIdx = -1;
for (const it of $("conv-list").querySelectorAll(".kbd-focus")) {
it.classList.remove("kbd-focus");
}
}

function init() {
if (!_keyHandler) initKeyboard();
}
init(); // 模块级绑定一次

return { render, renderOpenSessions, highlight, clearFocus };
return { renderOpenSessions, highlight };
})();

window.EMRG_Sidebar = Sidebar;
40 changes: 16 additions & 24 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,7 +123,7 @@ function makeEl(id) {
}

const ELEMENT_IDS = [
"workspace", "input", "send-btn", "stop-btn", "conv-list", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"workspace", "input", "send-btn", "stop-btn", "open-sessions", "open-sessions-label", "status-dot", "settings-btn",
"open-session-dialog", "open-session-list", "open-session-title", "open-session-desc", "open-session-new", "open-session-cancel", "open-session-new-session",
"new-session-dialog", "new-session-list", "new-session-new", "new-session-cancel",
"conn-banner", "empty-state", "model-switcher", "model-switcher-label", "brand-star", "new-chat-btn", "open-chat-btn",
Expand DownExpand Up@@ -285,7 +285,7 @@ test("boot:config 缺失 → 首启引导(不拉起 daemon)", async () =>
assert.ok(els["welcome-dialog"].open, "welcome-dialog 应打开");
});

test("boot:config 就绪 → 加载会话列表", async () => {
test("boot:config 就绪 → 加载会话列表(rant 11:44:52:历史列表已移除,state 仍保留)", async () => {
const { ctx, els } = makeSandbox({
init: async () => ({
config_exists: true,
Expand All@@ -298,9 +298,9 @@ test("boot:config 就绪 → 加载会话列表", async () => {
switchSession: async () => ({}),
});
await tick();
// conv-list 应有会话项(rant 17:48:07:无分组标签,直接 project/name|id)
const items = vm.runInContext('document.getElementById("conv-list").children.length', ctx);
assert.ok(items >= 1, `conv-list 应有会话项,实际 ${items}`);
// rant 11:44:52:conv-list 不再渲染(历史会话列表移除);state.sessions 仍加载供切换/标题栏使用
const count = vm.runInContext('App.state.sessions.length', ctx);
assert.strictEqual(count, 1, `state.sessions 应有 1 条,实际 ${count}`);
});

test("流式 delta 追加 + 工具行 running→done 状态流转", async () => {
Expand DownExpand Up@@ -977,24 +977,18 @@ test("设置/首启对话框:Enter 提交键盘绑定(交互一致性)", a
assert.ok(bindBlock.includes("enterToSave"), "Enter 应通过 enterToSave 触发提交");
});

test("对话列表键盘导航:↑↓ 聚焦 / Enter 切换(与 TUI /resume 一致)", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
assert.ok(src.includes("ArrowDown"), "sidebar 应支持 ↑↓ 键盘导航");
assert.ok(src.includes("kbd-focus"), "应有键盘聚焦状态");
assert.ok(src.includes("App.switchSession"), "Enter 应切换会话");
const css = fs.readFileSync(path.join(__dirname, "..", "renderer", "css", "components.css"), "utf8");
assert.ok(css.includes(".conv-item.kbd-focus"), "键盘聚焦高亮样式应存在");
});
// rant 11:44:52:历史会话列表 + ↑↓/Enter 键盘导航已整体移除(sidebar.js 不再含 initKeyboard/clearFocus/kbd-focus)

test("对话列表键盘导航:输入控件内不劫持(e.target 守卫,textarea ↑↓/Enter 正常)", async () => {
test("rant 11:44:52: sidebar.js 已移除历史会话列表渲染与键盘导航", async () => {
const { ctx } = makeSandbox({});
await tick();
const src = fs.readFileSync(path.join(RENDERER_JS, "sidebar.js"), "utf8");
// 输入框是 <textarea id="input">,document 级常驻 keydown 必须跳过输入控件
assert.ok(src.includes("closest(\"input, textarea, select, [contenteditable]\")"), "输入控件内应跳过键盘导航");
assert.ok(src.includes("e.target.closest"), "应使用 e.target 守卫");
assert.ok(!src.includes("conv-list"), "sidebar 不应再引用 conv-list");
assert.ok(!src.includes("kbd-focus"), "sidebar 不应再有 kbd-focus 键盘聚焦");
assert.ok(!src.includes("initKeyboard"), "sidebar 不应再有 initKeyboard");
assert.ok(!src.includes("clearFocus"), "sidebar 不应再有 clearFocus");
const html = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf8");
assert.ok(!html.includes('id="conv-list"'), "index.html 不应再有 conv-list nav");
});

test("P3.2:tool_finished 只登记 write/edit 成功文件(bash 不登记)", async () => {
Expand DownExpand Up@@ -2020,7 +2014,7 @@ test("P4 s2: 跨项目打开会话(entry.title 优先,state.sessions 无该
assert.ok(!(t2.textContent || "").includes("sess-local"), "有 title 时不显示 id");
});

test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表,另一列表保持原样", async () => {
test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定列表(rant 11:44:52:仅剩 open-sessions)", async () => {
const { ctx, els } = makeSandbox({});
await tick();
await vm.runInContext(
Expand All@@ -2035,11 +2029,9 @@ test("rant 22:04:02: highlight(sid, navEl) 列表作用域 — 只高亮指定
// 只高亮打开会话区(模拟点击打开会话列表条目)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b", document.getElementById("open-sessions"));', ctx);
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "open-sessions 条目高亮");
const convB = [...els["conv-list"].children].find((c) => c.dataset.sid === "sess-b");
assert.ok(convB && !convB.classList.contains("active"), "conv-list 中同 sid 条目不被联动高亮");
// 无 navEl 参数 → 两列表都更新(兼容初始渲染/全量刷新)
// 无 navEl 参数 → 全量刷新同样只作用于 open-sessions(conv-list 已不存在)
await vm.runInContext('EMRG_Sidebar.highlight("sess-b");', ctx);
assert.strictEqual(convB.classList.contains("active"), true, "无 navEl 时两列表都高亮");
assert.strictEqual(els["open-sessions"].children[0].classList.contains("active"), true, "无 navEl 时 open-sessions 高亮");
});

test("P4 s2: closeOpenSession 关闭激活会话 → 切到剩余打开会话 + 容器释放", async () => {
Expand Down
Loading