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` (193: 43 daemon_client + 19 conn-manager + 22 app-commands + 72 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` (198: 43 daemon_client + 19 conn-manager + 22 app-commands + 77 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Auto project tracking** — Automatically detects and records working directories; project-scoped sessions
- **Rant-driven evolution** — User feedback via `/rant` drives automatic self-improvement cycles
- **Headless GitHub auth** — Non-interactive evolution auto-extracts `GH_TOKEN` from git credential store (osxkeychain / credential helper); PR comment/LGTM queries fall back to REST API (GraphQL needs `read:org` scope)
Expand DownExpand Up@@ -113,7 +113,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (694) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (193: 43 daemon_client + 19 conn-manager + 22 app-commands + 72 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` (198: 43 daemon_client + 19 conn-manager + 22 app-commands + 77 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` + 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
2 changes: 1 addition & 1 deletion README.cn.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -147,7 +147,7 @@ EMRG 不只是追赶——它自己追上来。

贡献指南、源码安装、架构、详细 FAQ → [DEVELOPMENT.md](DEVELOPMENT.md)。

快速检查:`uv run pytest tests/ -v`(当前 694 项)· `cd emrg/gui && npm test`(193 项:43 daemon_client + 19 conn-manager + 22 app-commands + 72 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state)
快速检查:`uv run pytest tests/ -v`(当前 694 项)· `cd emrg/gui && npm test`(198 项:43 daemon_client + 19 conn-manager + 22 app-commands + 77 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state)

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -147,7 +147,7 @@ They're products. EMRG is an experiment in *closing the loop* — the AI improve

Contributing, source installs, architecture, and the full FAQ → [DEVELOPMENT.md](DEVELOPMENT.md).

Quick checks: `uv run pytest tests/ -v` (currently 694 items) · `cd emrg/gui && npm test` (193: 43 daemon_client + 19 conn-manager + 22 app-commands + 72 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state)
Quick checks: `uv run pytest tests/ -v` (currently 694 items) · `cd emrg/gui && npm test` (198: 43 daemon_client + 19 conn-manager + 22 app-commands + 77 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state)

---

Expand Down
88 changes: 88 additions & 0 deletions emrg/gui/renderer/css/layout.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -463,6 +463,94 @@ body.sidebar-collapsed #app {
opacity: 0.5;
}

/* ── P3.1:文件浏览器(懒加载目录树) ── */
.result-files {
overflow-y: auto;
}
.ft-row {
display: flex;
align-items: center;
gap: var(--sp-1);
padding: 2px var(--sp-2);
cursor: pointer;
border-radius: 4px;
font-size: var(--fs-small);
color: var(--text-2);
white-space: nowrap;
overflow: hidden;
}
.ft-row:hover {
background: var(--bg-soft);
color: var(--text-1);
}
.ft-dir {
color: var(--text-1);
font-weight: 500;
}
.ft-arrow {
width: 14px;
flex: none;
text-align: center;
color: var(--text-3);
font-size: 10px;
}
.ft-kids {
margin-left: 12px;
}
.ft-kids.hidden {
display: none;
}
.ft-hint {
padding: var(--sp-2) var(--sp-3);
color: var(--text-3);
font-size: var(--fs-small);
}

/* ── P3.3:文件查看器(基础版) ── */
.result-viewer {
overflow: hidden;
}
.viewer-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-2);
padding: var(--sp-2) var(--sp-3);
border-bottom: 1px solid var(--border);
flex: none;
}
.viewer-path {
font-size: var(--fs-small);
color: var(--text-2);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.viewer-open {
flex: none;
background: none;
border: 1px solid var(--border);
color: var(--text-2);
font-size: var(--fs-small);
padding: 2px var(--sp-2);
border-radius: 6px;
cursor: pointer;
}
.viewer-open:hover {
color: var(--text-1);
background: var(--bg-soft);
}
.viewer-pre {
flex: 1;
overflow: auto;
margin: 0;
padding: var(--sp-3);
font-size: var(--fs-small);
line-height: 1.5;
white-space: pre;
color: var(--text-1);
}

.result-list {
flex: 1;
overflow-y: auto;
Expand Down
5 changes: 4 additions & 1 deletion emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,12 +96,14 @@
</div>
<!-- 打开文件 Tab 条(P3 查看器接入;框架已就绪) -->
<div id="result-tabbar" class="result-tabbar"></div>
<!-- 文件浏览 pane(P3.1 文件树接入;当前为空态占位) -->
<!-- 文件浏览 pane(P3.1 文件树,file-tree.js 渲染) -->
<div id="result-files" class="result-pane result-files">
<div class="result-empty" data-i18n="result.filesEmpty">文件浏览器即将推出</div>
</div>
<!-- 产物 pane(WorkBuddy P1 工具卡片暂保留,P3.2 改为 write/edit 文件登记) -->
<div id="result-list" class="result-list result-pane active"></div>
<!-- 打开文件查看器 pane(P3.3 基础版:readFile 文本 / 二进制提示) -->
<div id="result-viewer" class="result-pane result-viewer"></div>
</aside>
<!-- 宽度拖拽手柄:绝对定位(不进 #app flex 流,防 WebContentsView bounds x 偏移) -->
<div id="result-resizer" title="拖拽调整宽度" data-i18n-title="result.resize"></div>
Expand DownExpand Up@@ -412,6 +414,7 @@ <h2 data-i18n="tasks.title">后台任务</h2>
<script src="js/sidebar.js"></script>
<script src="js/dialogs.js"></script>
<script src="js/result-panel.js"></script>
<script src="js/file-tree.js"></script>
<script src="js/app.js"></script>
</body>
</html>
9 changes: 9 additions & 0 deletions emrg/gui/renderer/js/app.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -616,6 +616,12 @@ const App = (() => {
input.style.height = Math.min(input.scrollHeight, 150) + "px";
}

/** 会话的项目根目录(openSessions 优先,回退全局 projectDir;P3.1 文件树根) */
function projectPathFor(sid) {
const os = state.openSessions.find((s) => s.sid === sid);
return (os && os.projectPath) || state.projectDir || "";
}

async function switchSession(sid, opts = {}) { // G65:busy 即自有流进行中/发送中(IPC 往返窗口内 ownStreamRequestId 尚未赋值)
if (state.busy) {
Chat.addSystemMessage(EMRG_Copy.COPY.sessionBusy);
Expand All@@ -632,6 +638,7 @@ const App = (() => {
// P3 slice 2:激活该会话容器(状态保留,不 Chat.clear——切回继续看到原消息/流式现场)
activateSessionView(sid);
ResultPanel.switchSession(sid); // P2 框架:右栏 Tab/产物状态按 sid 隔离
FileTree.setSession(sid, projectPathFor(sid)); // P3.1:文件树根跟随会话项目
updateEmptyState();
if (res.error === "session_not_found") {
Chat.addSystemMessage(_t("app.deletedSwitch"));
Expand DownExpand Up@@ -673,6 +680,7 @@ const App = (() => {
state.drafts.set(res.session_id, ""); // 新会话无草稿
activateSessionView(state.sessionId); // P3 slice 2:新会话独立容器(空)
ResultPanel.switchSession(state.sessionId); // P2 框架:新会话右栏状态复位
FileTree.setSession(state.sessionId, projectPathFor(state.sessionId)); // P3.1:文件树根跟随
Chat.clear(state.sessionId); // 新会话从空开始(容器可能被复用)
updateEmptyState(); // 欢迎屏即反馈
await refreshSessions();
Expand DownExpand Up@@ -1474,6 +1482,7 @@ const App = (() => {
initModelSwitcher();
initModeSwitcher(); // WorkBuddy P2:Ask/Auto 工作模式
ResultPanel.init(); // WorkBuddy P1:结果面板(⌘\ 折叠 + 窄屏自动隐藏)
FileTree.setSession(state.sessionId, projectPathFor(state.sessionId)); // P3.1:文件树初始根
initEvolutionToast(); // WorkBuddy P3:进化 toast 按钮绑定
}

Expand Down
131 changes: 131 additions & 0 deletions emrg/gui/renderer/js/file-tree.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
"use strict";
/**
* file-tree.js — 工作区文件浏览器(workspace panel P3.1,rant 2026-08-11T12:20:35)
*
* Tab「文件」内容:懒加载目录树。
* - 根 = 当前激活会话 projectPath(app.js 经 FileTree.setSession(sid, path) 注入)
* - 目录行点击 → emrg.listFiles(path) 懒加载子项(已加载目录缓存,折叠不重新拉取)
* - 文件行点击 → ResultPanel.openFileTab(sid, path)(P2.2 框架 Tab + P3.3 查看器)
* - 排序(目录在前)/5000 截断/符号链接不展开/绝对路径校验由 daemon 保证(P1 #661)
* 切会话 → setSession 重设根 + 清缓存(per-session 隔离)
*/

const FileTree = (() => {
let sid = null;
let root = "";
const cache = new Map(); // path -> { loaded, loading, error, entries }

function container() { return $("result-files"); }
function t(key) { return window.EMRG_I18N ? window.EMRG_I18N.t(key) : key; }

function ensure(path) {
let st = cache.get(path);
if (!st) { st = { loaded: false, loading: false, error: false, entries: [] }; cache.set(path, st); }
return st;
}

/** 设置会话根目录(切会话调用:重设根 + 清缓存) */
function setSession(s, rootPath) {
sid = s || null;
setRoot(rootPath);
}

function setRoot(path) {
root = path || "";
cache.clear();
render();
}

/** 拉取 + 渲染目录子项(st 为 ensure(path) 的缓存态) */
async function expandDir(path, kidsEl, st) {
if (!st.loaded && !st.loading) {
st.loading = true;
kidsEl.innerHTML = "";
kidsEl.appendChild(el("div", { class: "ft-hint" }, t("result.treeLoading")));
try {
const res = await window.emrg.listFiles({ path });
st.entries = (res && res.entries) || [];
st.loaded = true;
st.error = false;
} catch {
st.entries = [];
st.loaded = true;
st.error = true;
} finally {
st.loading = false;
}
}
kidsEl.innerHTML = "";
if (st.error) {
kidsEl.appendChild(el("div", { class: "ft-hint" }, t("result.treeLoadFailed")));
return;
}
for (const e of st.entries) kidsEl.appendChild(renderEntry(e));
}

function renderEntry(entry) {
if (entry.type === "dir") {
const row = el("div", { class: "ft-row ft-dir", dataset: { path: entry.path } });
const arrow = el("span", { class: "ft-arrow" }, "▸");
row.appendChild(arrow);
row.appendChild(el("span", { class: "ft-name" }, entry.name));
const kids = el("div", { class: "ft-kids hidden" });
row.appendChild(kids);
const st = ensure(entry.path);
row.addEventListener("click", (e) => {
if (e && e.stopPropagation) e.stopPropagation();
toggleDir(entry.path, arrow, kids, st);
});
return row;
}
const row = el("div", { class: "ft-row ft-file", dataset: { path: entry.path } });
row.appendChild(el("span", { class: "ft-name" }, entry.name));
row.addEventListener("click", (e) => {
if (e && e.stopPropagation) e.stopPropagation();
openFile(entry.path);
});
return row;
}

async function toggleDir(path, arrow, kidsEl, st) {
const wasHidden = kidsEl.classList.contains("hidden");
if (wasHidden) {
await expandDir(path, kidsEl, st);
kidsEl.classList.remove("hidden");
arrow.textContent = "▾";
} else {
kidsEl.classList.add("hidden");
arrow.textContent = "▸";
}
}

function openFile(path) {
if (window.ResultPanel && window.ResultPanel.openFileTab) {
window.ResultPanel.openFileTab(sid, path);
}
}

function render() {
const c = container();
if (!c) return;
c.innerHTML = "";
if (!root) {
c.appendChild(el("div", { class: "result-empty" }, t("result.filesEmpty")));
return;
}
const rootName = String(root).split(/[\\/]/).filter(Boolean).pop() || root;
const rootRow = el("div", { class: "ft-row ft-dir ft-root", dataset: { path: root } });
rootRow.appendChild(el("span", { class: "ft-arrow" }, "▾"));
rootRow.appendChild(el("span", { class: "ft-name" }, rootName));
const kids = el("div", { class: "ft-kids" });
rootRow.appendChild(kids);
c.appendChild(rootRow);
// 根自动展开(fire-and-forget)
expandDir(root, kids, ensure(root));
}

return { setRoot, setSession };
})();

// ⚠️ 必须暴露到 window(app.js 独立 <script> 加载,模块级 const 不跨 script 共享)
window.FileTree = FileTree;
12 changes: 12 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,6 +51,12 @@ const I18N = (() => {
"result.tabArtifacts": "产物",
"result.filesEmpty": "文件浏览器即将推出",
"result.resize": "拖拽调整宽度",
"result.treeLoading": "加载中…",
"result.treeLoadFailed": "加载失败",
"result.viewerLoading": "读取文件…",
"result.viewerBinary": "二进制文件,用系统工具打开",
"result.viewerError": "无法读取文件(可能已被删除或无权限)",
"result.viewerOpen": "用系统工具打开",

// 设置
"settings.title": "设置",
Expand DownExpand Up@@ -385,6 +391,12 @@ const I18N = (() => {
"result.tabArtifacts": "Artifacts",
"result.filesEmpty": "File browser coming soon",
"result.resize": "Drag to resize",
"result.treeLoading": "Loading…",
"result.treeLoadFailed": "Failed to load",
"result.viewerLoading": "Reading file…",
"result.viewerBinary": "Binary file — open with system tool",
"result.viewerError": "Cannot read file (deleted or no permission)",
"result.viewerOpen": "Open with system tool",

// Settings
"settings.title": "Settings",
Expand Down
Loading
Loading