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` (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`
- Unit tests `npm test` (241: 44 daemon_client + 19 conn-manager + 22 app-commands + 117 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 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 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` (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`
GUI: `cd emrg/gui && npm test` (241: 44 daemon_client + 19 conn-manager + 22 app-commands + 117 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 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
54 changes: 49 additions & 5 deletions emrg/gui/renderer/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
}
.msg.user {
align-self: flex-end;
/* rant 21:28:49:flex 容器中 auto margin 优先于 align-self——.session-view > * 的
margin-left/right:auto(居中 760px)盖掉了 align-self:flex-end → 用户气泡被居中
而非右对齐。margin-left:auto + margin-right:0(特异性 0,2,0 覆盖 0,1,0)恢复右对齐。 */
margin-left: auto;
margin-right: 0;
background: var(--accent-soft);
color: var(--text-1);
border-radius: var(--radius-bubble);
Expand Down Expand Up @@ -217,9 +222,9 @@
display: flex;
align-items: center;
gap: var(--sp-2);
font-size: var(--fs-secondary);
font-size: var(--fs-small);
color: var(--text-2);
padding: 4px 2px;
padding: 2px 2px;
border-radius: 8px;
cursor: pointer;
user-select: none;
Expand All @@ -232,9 +237,9 @@
}
.tool-row .tool-spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid var(--amber-soft);
width: 12px;
height: 12px;
border: 1.5px solid var(--amber-soft);
border-top-color: var(--amber);
border-radius: 50%;
animation: spin 0.8s linear infinite;
Expand Down Expand Up @@ -286,6 +291,44 @@
margin: 0 0 6px 2px;
}

/* rant 21:28:49:工具耗时(成功时 label 后 · 3.2s) */
.tool-time {
font-size: var(--fs-aux);
color: var(--text-3);
margin-left: 4px;
}

/* ── 连续工具合并组(方案 A:⌄ N 个工具执行 · Ts) ─────── */
.tool-group {
display: flex;
flex-direction: column;
}
.tool-group-bar {
display: flex;
align-items: center;
gap: var(--sp-1);
font-size: var(--fs-small);
color: var(--text-3);
padding: 2px 4px;
border-radius: 6px;
cursor: pointer;
user-select: none;
transition: background-color var(--dur-fast) var(--ease);
}
.tool-group-bar:hover {
background: var(--bg-soft);
}
.tool-group-bar .tool-group-chev {
font-size: 10px;
transition: transform var(--dur-med) var(--ease);
}
.tool-group.collapsed .tool-group-rows {
display: none;
}
.tool-group:not(.collapsed) .tool-group-bar .tool-group-chev {
transform: rotate(180deg);
}

/* ── 空状态欢迎屏 ─────────────────────── */
#empty-state {
flex: 1;
Expand Down Expand Up @@ -812,6 +855,7 @@ dialog::backdrop {
text-align: center;
padding: var(--sp-2);
}

.task-form {
margin-top: 8px;
padding: var(--sp-3);
Expand Down
5 changes: 3 additions & 2 deletions emrg/gui/renderer/css/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,10 @@
overflow-y: auto;
display: none;
flex-direction: column;
gap: var(--sp-5);
gap: var(--sp-3);
padding: var(--sp-5) var(--sp-5) var(--sp-4);
scroll-behavior: smooth;
/* rant 21:28:49:去掉 smooth——JS scrollTop=scrollHeight 也走平滑动画,工具行快速
连续追加时滚不到底 → 最新内容不可见。JS 跳转全部即时;用户手动滚动不受影响。 */
}
.session-view.active {
display: flex;
Expand Down
90 changes: 89 additions & 1 deletion emrg/gui/renderer/js/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,33 @@ const Chat = (() => {
}
}

/** rant 21:28:49:工具合并组 —— 组内工具行容器 + 状态 */
function updateToolGroup(group, sid) {
const rowsEl = group.querySelector(".tool-group-rows");
const bar = group.querySelector(".tool-group-bar");
if (!rowsEl || !bar) return;
const rows = rowsEl.children.filter((c) => c.classList && c.classList.contains("tool-row"));
const running = rows.some((r) => r.classList.contains("running"));
const userExpanded = group.dataset.userExpanded === "1";
if (rows.length >= 2 && !running) {
// ≥2 行全部完成 → bar 显示 + 摘要(数量 + 总耗时);非手动展开则收起
const total = rows.reduce((acc, r) => acc + (parseFloat(r.dataset.elapsed) || 0), 0);
const summary = bar.querySelector(".tool-group-summary");
if (summary) {
summary.textContent = EMRG_Copy._t("chat.toolGroupSummary", {
count: rows.length,
time: total.toFixed(1) + "s",
});
}
bar.classList.remove("hidden");
group.classList.toggle("collapsed", !userExpanded);
} else {
// 组内仅 1 行 或 存在 running 行 → bar 隐藏、rows 展开(进行中工具实时可见)
bar.classList.add("hidden");
group.classList.remove("collapsed");
}
}

/** 工具友好状态行(进行中 → 完成/失败,默认折叠,点开展示原始输出) */
function handleToolStart(data, sid) {
const { groupNodes, toolRows } = st(sid);
Expand Down Expand Up @@ -300,7 +327,59 @@ const Chat = (() => {
row.classList.toggle("expanded", !out.classList.contains("hidden"));
}
});
append(row, sid);

// rant 21:28:49:连续工具合并 —— 会话容器最后一个子节点判定
// 1) 前一工具已完成(.tool-row:not(.running))→ 新建 .tool-group,把旧行移入 rows,新行也入 rows
// 2) 已是 .tool-group → 新行直接入其 rows(组收起时新工具 start 自动展开显示 running)
// 3) 其他(文本/用户消息/无节点)→ 独立 .tool-row(现状不变;文本穿插不合并)
const cv = chatContainer(sid);
const last = cv.children.length ? cv.children[cv.children.length - 1] : null;
let appendedToGroup = false;
if (last && last.classList) {
if (last.classList.contains("tool-row") && !last.classList.contains("running")) {
const rowsEl = el("div", { class: "tool-group-rows" });
const bar = el("div", { class: "tool-group-bar hidden" });
bar.appendChild(el("span", { class: "tool-group-chev" }, "⌄"));
bar.appendChild(el("span", { class: "tool-group-summary" }, ""));
const grp = el("div", { class: "tool-group" });
grp.appendChild(bar);
grp.appendChild(rowsEl);
// bar 点击 → 展开/收起 rows + chevron 旋转 + 组标 user-expanded(手动展开后不再自动收起)
bar.addEventListener("click", () => {
const wasCollapsed = grp.classList.contains("collapsed");
if (wasCollapsed) {
grp.classList.remove("collapsed");
grp.dataset.userExpanded = "1";
} else {
grp.classList.add("collapsed");
delete grp.dataset.userExpanded; // 手动收起后新工具完成仍自动收起(rule 5)
}
});
// 移旧行入组(真实 DOM appendChild 即移动;mock 需先 remove 防残留)
if (last.parentNode) last.remove();
rowsEl.appendChild(last);
rowsEl.appendChild(row);
cv.appendChild(grp);
appendedToGroup = true;
} else if (last.classList.contains("tool-group")) {
const rowsEl = last.querySelector(".tool-group-rows");
if (rowsEl) {
rowsEl.appendChild(row);
last.classList.remove("collapsed"); // 组收起后新工具 start → 自动展开显示 running 行
appendedToGroup = true;
}
}
}
if (appendedToGroup) {
scrollToBottom(sid);
App.updateEmptyState?.();
const rowsEl = row.parentNode;
if (rowsEl && rowsEl.classList.contains("tool-group-rows")) {
updateToolGroup(rowsEl.parentNode, sid);
}
} else {
append(row, sid);
}
toolRows.set(data.tool_call_id, row);
}

Expand All @@ -327,6 +406,10 @@ const Chat = (() => {
const elapsed = data.elapsed !== undefined ? `${data.elapsed.toFixed(1)}s` : "";
if (elapsed && ok) {
row.title = EMRG_Copy._t("chat.elapsed", { s: elapsed });
// rant 21:28:49:工具耗时可见 —— label 后 · 3.2s(失败不加)
const time = el("span", { class: "tool-time" }, `· ${elapsed}`);
if (label) label.after(time);
row.dataset.elapsed = String(data.elapsed); // 供合并组摘要求和
}
// G91/G131:content 截断 2000 字符 + 展开全文
const content = data.content || "";
Expand All @@ -344,6 +427,11 @@ const Chat = (() => {
}
}
scrollToBottom(sid);
// rant 21:28:49:该行在合并组内 → 更新组摘要(数量 + 总耗时)与收起状态
const rowsEl = row.parentNode;
if (rowsEl && rowsEl.classList && rowsEl.classList.contains("tool-group-rows")) {
updateToolGroup(rowsEl.parentNode, sid);
}
}

return {
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ const I18N = (() => {
"chat.maxRoundsHint": "⚠️ 这次任务因为工具调用次数上限中断了,结果可能不完整——输入“继续”我可以接着做。",
"chat.elapsed": "耗时 {s}",
"chat.expand": "展开全文",
"chat.toolGroupSummary": "{count} 个工具执行 · {time}",
// 时间分组(utils.js/sidebar.js)
"util.groupToday": "今天",
"util.groupYesterday": "昨天",
Expand Down Expand Up @@ -759,6 +760,7 @@ const I18N = (() => {
"chat.maxRoundsHint": "⚠️ This task hit the tool-call limit and may be incomplete — type \"continue\" and I'll keep going.",
"chat.elapsed": "took {s}",
"chat.expand": "Expand full text",
"chat.toolGroupSummary": "{count} tool calls · {time}",
// Time groups (utils.js/sidebar.js)
"util.groupToday": "Today",
"util.groupYesterday": "Yesterday",
Expand Down
Loading
Loading