From ed998159e0fb9ea607bb11ef2fad9efd41be8934 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Tue, 11 Aug 2026 13:25:08 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20GUI=20workspace=20panel=20P3=20?= =?UTF-8?q?=E2=80=94=20file=20browser=20tree=20+=20basic=20viewer=20(rant?= =?UTF-8?q?=202026-08-11T12:20:35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Agent.md | 4 +- README.cn.md | 2 +- README.md | 2 +- emrg/gui/renderer/css/layout.css | 88 ++++++++++++++++++ emrg/gui/renderer/index.html | 5 +- emrg/gui/renderer/js/app.js | 9 ++ emrg/gui/renderer/js/file-tree.js | 131 +++++++++++++++++++++++++++ emrg/gui/renderer/js/i18n.js | 12 +++ emrg/gui/renderer/js/result-panel.js | 72 +++++++++++++-- emrg/gui/test/app-commands.test.js | 4 +- emrg/gui/test/renderer.smoke.test.js | 108 +++++++++++++++++++++- 11 files changed, 421 insertions(+), 16 deletions(-) create mode 100644 emrg/gui/renderer/js/file-tree.js diff --git a/Agent.md b/Agent.md index 2d0abe8c..82dffa76 100644 --- a/Agent.md +++ b/Agent.md @@ -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) @@ -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 路径不受影响) diff --git a/README.cn.md b/README.cn.md index 00d74264..c2e3d06c 100644 --- a/README.cn.md +++ b/README.cn.md @@ -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) --- diff --git a/README.md b/README.md index 1aaf6680..a1a30f96 100644 --- a/README.md +++ b/README.md @@ -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) --- diff --git a/emrg/gui/renderer/css/layout.css b/emrg/gui/renderer/css/layout.css index c97c9518..ec9533db 100644 --- a/emrg/gui/renderer/css/layout.css +++ b/emrg/gui/renderer/css/layout.css @@ -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; diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html index 7b9d4ed8..2043db42 100644 --- a/emrg/gui/renderer/index.html +++ b/emrg/gui/renderer/index.html @@ -96,12 +96,14 @@
- +
文件浏览器即将推出
+ +
@@ -412,6 +414,7 @@

后台任务

+ diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js index 10e8730a..89ac7d43 100644 --- a/emrg/gui/renderer/js/app.js +++ b/emrg/gui/renderer/js/app.js @@ -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); @@ -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")); @@ -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(); @@ -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 按钮绑定 } diff --git a/emrg/gui/renderer/js/file-tree.js b/emrg/gui/renderer/js/file-tree.js new file mode 100644 index 00000000..ddb4f1ec --- /dev/null +++ b/emrg/gui/renderer/js/file-tree.js @@ -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 独立