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 number Diff line number Diff 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` (238: 44 daemon_client + 19 conn-manager + 22 app-commands + 116 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (239: 44 daemon_client + 19 conn-manager + 22 app-commands + 117 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); 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 Down Expand Up @@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (801) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (238: 44 daemon_client + 19 conn-manager + 22 app-commands + 116 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (239: 44 daemon_client + 19 conn-manager + 22 app-commands + 117 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — 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
13 changes: 13 additions & 0 deletions emrg/gui/renderer/css/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,9 @@ body.sidebar-collapsed #app {
display: block; /* 块级排布:.ft-head 行 + .ft-kids 子树纵向排列,行高自适应
(修复布局 bug:定高 flex-wrap 使下一兄弟行与展开目录首个子项
重叠,headless Chrome 像素实证——兄弟行遮挡子项) */
flex-shrink: 0; /* rant 20:58:57:.ft-row 是 .result-files(flex column)的 flex
item,默认 shrink:1 + overflow:hidden → min-height:auto 解析为 0,
flex 把整棵树压缩到容器高度,scrollHeight==clientHeight 无法滚动 */
cursor: pointer;
font-size: var(--fs-small);
color: var(--text-2);
Expand Down Expand Up @@ -622,6 +625,16 @@ body.sidebar-collapsed #app {
fill: currentColor;
}
.ft-icon path { fill: currentColor; }
/* 展开/收起 chevron(VS Code 风格 ▸/▾,rant 2026-08-13T20:58:57) */
.ft-chevron {
flex: none;
width: 14px;
height: 14px;
margin-right: 2px;
color: currentColor;
fill: currentColor;
}
.ft-chevron path { fill: currentColor; }
.ft-name {
flex: 1;
overflow: hidden;
Expand Down
15 changes: 13 additions & 2 deletions emrg/gui/renderer/js/file-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const FileTree = (() => {

// ── mono 内联 SVG 图标(16x16 viewBox,fill: currentColor 跟随文字色) ──
const ICON = {
chevronRight: '<path d="M6 3l5 5-5 5z"/>', // 折叠 ▸(目录可展开提示)
chevronDown: '<path d="M3 6l5 5 5-5z"/>', // 展开 ▾
dirClosed: '<path d="M1.5 2.5h4.1c.32 0 .62.13.84.36l1.1 1.14h6.96c.83 0 1.5.67 1.5 1.5V12.5c0 .83-.67 1.5-1.5 1.5H1.5A1.5 1.5 0 0 1 0 12.5V4c0-.83.67-1.5 1.5-1.5z"/>',
dirOpen: '<path d="M1.5 2.5h4.1c.32 0 .62.13.84.36l1.1 1.14h6.96c.83 0 1.5.67 1.5 1.5V6H2.1c-.6 0-1.13.36-1.34.92L0 12.8V4c0-.83.67-1.5 1.5-1.5z"/><path d="M14.6 6.5H2.3c-.42 0-.8.27-.94.66L0 13.5h14.5c.83 0 1.5-.67 1.5-1.5V8c0-.83-.67-1.5-1.5-1.5z"/>',
fileDefault: '<path d="M3 1h6.5l3.5 3.5V15H3V1z"/><path d="M9.5 1v3.5H13" fill="none" stroke="currentColor" stroke-width="1.5"/>',
Expand All @@ -55,11 +57,14 @@ const FileTree = (() => {
txt: "fileTxt", log: "fileTxt", csv: "fileTxt", tsv: "fileTxt",
};

function iconSvg(d) {
const svg = el("svg", { class: "ft-icon", viewBox: "0 0 16 16", width: 14, height: 14, "aria-hidden": "true" });
function iconSvg(d, cls) {
const svg = el("svg", { class: cls || "ft-icon", viewBox: "0 0 16 16", width: 14, height: 14, "aria-hidden": "true" });
svg.innerHTML = d; // 静态白名单路径,无注入风险
return svg;
}
function chevronFor(entry) {
return expanded.get(entry.path) ? ICON.chevronDown : ICON.chevronRight;
}
function iconFor(entry) {
if (entry.type === "dir") return expanded.get(entry.path) ? ICON.dirOpen : ICON.dirClosed;
const ext = String(entry.name).split(".").pop().toLowerCase();
Expand Down Expand Up @@ -123,6 +128,9 @@ const FileTree = (() => {
// 行头(图标+名称)单独包装:.ft-row 为块级容器(head + kids 纵向排布),
// 修复定高 flex-wrap 下兄弟行与展开子项重叠的布局 bug(headless Chrome 像素实证)
const head = el("div", { class: "ft-head" });
if (entry.type === "dir") {
head.appendChild(iconSvg(chevronFor(entry), "ft-chevron"));
}
head.appendChild(iconSvg(iconFor(entry)));
head.appendChild(el("span", { class: "ft-name" }, entry.name));
row.appendChild(head);
Expand Down Expand Up @@ -162,6 +170,8 @@ const FileTree = (() => {
if (row) {
const ic = row.querySelector(".ft-icon");
if (ic) ic.innerHTML = expanded.get(path) ? ICON.dirOpen : ICON.dirClosed;
const chev = row.querySelector(".ft-chevron");
if (chev) chev.innerHTML = expanded.get(path) ? ICON.chevronDown : ICON.chevronRight;
}
}

Expand Down Expand Up @@ -198,6 +208,7 @@ const FileTree = (() => {
const rootRow = el("div", { class: "ft-row ft-dir ft-root", dataset: { path: root } });
rootRow.style.paddingLeft = "8px";
const rootHead = el("div", { class: "ft-head" });
rootHead.appendChild(iconSvg(ICON.chevronDown, "ft-chevron")); // 根默认展开 → ▾
rootHead.appendChild(iconSvg(ICON.dirOpen));
rootHead.appendChild(el("span", { class: "ft-name" }, rootName));
rootRow.appendChild(rootHead);
Expand Down
31 changes: 31 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,37 @@ test("P3:点击目录行懒加载展开子项(缓存,折叠再展开不重
assert.strictEqual(calls, 2, "缓存命中不重复拉取");
});

test("P3.5:目录行 chevron 展开/收起指示(rant 20:58:57)", async () => {
const { ctx, els } = makeSandbox({
listFiles: async ({ path } = {}) => {
if (path === "/proj") return { entries: [{ name: "src", path: "/proj/src", type: "dir" }] };
return { entries: [] };
},
});
await tick();
await vm.runInContext('FileTree.setSession("s1", "/proj")', ctx);
await tick();
// 根默认展开 → chevronDown ▾
const rootRow = els["result-files"].querySelectorAll(".ft-root")[0];
const rootChev = rootRow.querySelector(".ft-chevron");
assert.ok(rootChev, "根目录行应有 chevron");
assert.ok(rootChev.innerHTML.includes("M3 6l5 5 5-5z"), "根展开态 chevronDown");
// src 目录折叠 → chevronRight ▸
const srcRow = els["result-files"].querySelectorAll(".ft-dir")[1];
const srcChev = srcRow.querySelector(".ft-chevron");
assert.ok(srcChev, "目录行应有 chevron");
assert.ok(srcChev.innerHTML.includes("M6 3l5 5-5 5z"), "折叠态 chevronRight");
// 点击展开 → 切 chevronDown
srcRow.dispatch("click", { stopPropagation() {} });
await tick();
assert.ok(srcChev.innerHTML.includes("M3 6l5 5 5-5z"), "展开后 chevronDown");
// 文件行无 chevron
const files = els["result-files"].querySelectorAll(".ft-file");
for (const f of files) {
assert.strictEqual(f.querySelector(".ft-chevron"), null, "文件行不应有 chevron");
}
});

test("P3:点击文件行 → 打开文件 Tab + 查看器加载内容", async () => {
const { ctx, els } = makeSandbox();
await tick();
Expand Down
Loading