From e21972327b30f3a7d7ca8654c55ac38e8b0b2d6b Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Tue, 11 Aug 2026 12:41:10 +0800 Subject: [PATCH 1/2] =?UTF-8?q?emrg:=20GUI=20workspace=20panel=20P1=20?= =?UTF-8?q?=E2=80=94=20daemon=20list=5Ffiles/read=5Ffile=20+=20IPC=20passt?= =?UTF-8?q?hrough=20(rant=202026-08-11T12:20:35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 of the multi-tab workspace panel rant: data source layer first. - daemon: list_files → files_list (dirs-first sort, 5000-entry cap + truncated, absolute-path-only, symlinks never expandable); read_file → file_content (start_line/line_limit paging, binary detection, 1MB cap) - GUI: RESPONSE_TYPES + _classify files_list whitelist, preload listFiles/ readFile, main.js IPC passthrough via requireConn - tests: +6 pytest e2e, +1 build-config preload-existence guard, RESPONSE_TYPES mapping extended; doc counts synced (pytest 688→694, GUI 187→188) --- Agent.md | 2 +- README.cn.md | 5 +- README.md | 5 +- emrg/gui/daemon_client.js | 4 +- emrg/gui/main.js | 19 +++ emrg/gui/preload.js | 2 + emrg/gui/test/build-config.test.js | 20 ++++ emrg/gui/test/daemon_client.test.js | 17 ++- emrg/server/daemon.py | 171 +++++++++++++++++++++++++++ tests/test_ws_e2e.py | 174 ++++++++++++++++++++++++++++ 10 files changed, 414 insertions(+), 5 deletions(-) diff --git a/Agent.md b/Agent.md index 3d7e93bb..c8086018 100644 --- a/Agent.md +++ b/Agent.md @@ -112,7 +112,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg ``` -Python: `uv run pytest tests/ -v` (688) — import check: `uv run python -c "from emrg.client.app import run_client"` +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` (187: 43 daemon_client + 19 conn-manager + 22 app-commands + 67 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 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 0aebf4ef..19f6bf3a 100644 --- a/README.cn.md +++ b/README.cn.md @@ -23,6 +23,9 @@ EMRG 是一个关于*自主进化*的实验:一个能帮你写代码的 AI 智能体——读文件、跑命令、做编辑——而**你的每一条吐槽 (`/rant`) 都会驱动它自我改进**。你说它哪里不好,下一个演化周期它就自己写代码修好并上线。完全开源,完全透明。 > *"EMRG 是一个自我进化的 AI 智能体架构实验。"* — [MANIFESTO](MANIFESTO.md) +> +> *人人都是产品经理——你的每一条吐槽,都是下一版的需求单。* +> *人人都是硅基生命的宿主——你运行的,是一个与你共生进化的数字生命。* --- @@ -144,7 +147,7 @@ EMRG 不只是追赶——它自己追上来。 贡献指南、源码安装、架构、详细 FAQ → [DEVELOPMENT.md](DEVELOPMENT.md)。 -快速检查:`uv run pytest tests/ -v`(当前 688 项)· `cd emrg/gui && npm test`(187 项:43 daemon_client + 19 conn-manager + 22 app-commands + 67 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 build-config + 7 gui-state) +快速检查:`uv run pytest tests/ -v`(当前 694 项)· `cd emrg/gui && npm test`(188 项:43 daemon_client + 19 conn-manager + 22 app-commands + 67 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) --- diff --git a/README.md b/README.md index ba4877fe..2dd21e0d 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ EMRG is an experiment in *autonomous self-improvement*: an AI agent that helps you code — reading files, running commands, making edits — and **every `/rant` you send drives it to improve itself**. Tell it what bothers you, and the next evolution cycle writes the fix and ships it. Fully open source, fully transparent. > *"EMRG is an experiment in self-evolving AI agent architecture."* — [MANIFESTO](MANIFESTO.md) +> +> *Everyone is a product manager — every rant is a ticket for the next release.* +> *Everyone is a host of silicon life — what you run is a digital organism that evolves with you.* --- @@ -144,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 688 items) · `cd emrg/gui && npm test` (187: 43 daemon_client + 19 conn-manager + 22 app-commands + 67 renderer smoke + 15 i18n + 7 integration + 3 commands + 4 build-config + 7 gui-state) +Quick checks: `uv run pytest tests/ -v` (currently 694 items) · `cd emrg/gui && npm test` (188: 43 daemon_client + 19 conn-manager + 22 app-commands + 67 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) --- diff --git a/emrg/gui/daemon_client.js b/emrg/gui/daemon_client.js index a0cfa721..518656d7 100644 --- a/emrg/gui/daemon_client.js +++ b/emrg/gui/daemon_client.js @@ -67,6 +67,8 @@ const RESPONSE_TYPES = { github_connect: "github_connect_result", // Windows GCM rant Stage 2:PAT 授权(daemon.py github_connect) github_disconnect: "github_disconnect_result", // Windows GCM rant Stage 2:断开(daemon.py github_disconnect) github_connect_web: "github_connect_web_result", // Stage 2b:device flow(daemon.py github_connect_web) + list_files: "files_list", // 右栏工作区面板 P1:目录树(daemon.py list_files) + read_file: "file_content", // 右栏工作区面板 P1:文件查看器(daemon.py read_file) }; class DaemonClient { @@ -599,7 +601,7 @@ class DaemonClient { this._emit("cancelled", frame); return; } - if (["sessions_list", "models_list", "history_list", "tasks_list"].includes(frame.type)) { + if (["sessions_list", "models_list", "history_list", "tasks_list", "files_list"].includes(frame.type)) { this._emit("list_result", frame); return; } diff --git a/emrg/gui/main.js b/emrg/gui/main.js index 3a44a079..145fb141 100644 --- a/emrg/gui/main.js +++ b/emrg/gui/main.js @@ -443,6 +443,25 @@ vision = false return frame.memory || { id: memoryId, content: "" }; }); + // 右栏工作区面板 P1(rant 2026-08-11T12:20:35):list_files / read_file 透传 + // 走 requireConn() = 当前会话连接天然认证;daemon list_files → files_list + ipcMain.handle("emrg:listFiles", async (_e, { path: p } = {}) => { + if (typeof p !== "string" || !p.trim()) throw new Error("invalid path"); + const frame = await requireConn().sendCommandAndWait("list_files", { path: p.trim() }, 10000); + if (frame.error) throw new Error(frame.error); + return { entries: frame.entries || [], truncated: !!frame.truncated }; + }); + + ipcMain.handle("emrg:readFile", async (_e, { path: p, startLine, lineLimit } = {}) => { + if (typeof p !== "string" || !p.trim()) throw new Error("invalid path"); + const params = { path: p.trim() }; + if (startLine !== undefined) params.start_line = startLine; + if (lineLimit !== undefined) params.line_limit = lineLimit; + const frame = await requireConn().sendCommandAndWait("read_file", params, 10000); + if (frame.error) throw new Error(frame.error); + return { content: frame.content || "", binary: !!frame.binary, truncated: !!frame.truncated, totalLines: frame.total_lines }; + }); + ipcMain.handle("emrg:listSkills", async () => { // GUI / 指令 P3:/skills — 读取技能列表(TUI 本地 load_skills 等价物,daemon 无协议) // 技能在 ~/.emrg/skills/*.md(user)与 /.emrg/skills/*.md(project) diff --git a/emrg/gui/preload.js b/emrg/gui/preload.js index fd5d9a93..ff803b68 100644 --- a/emrg/gui/preload.js +++ b/emrg/gui/preload.js @@ -23,6 +23,8 @@ const api = { rewindSession: (payload) => ipcRenderer.invoke("emrg:rewindSession", payload), listMemories: (payload) => ipcRenderer.invoke("emrg:listMemories", payload), readMemory: (payload) => ipcRenderer.invoke("emrg:readMemory", payload), + listFiles: (payload) => ipcRenderer.invoke("emrg:listFiles", payload), // 右栏工作区 P1:目录树 + readFile: (payload) => ipcRenderer.invoke("emrg:readFile", payload), // 右栏工作区 P1:文件查看器 listSkills: () => ipcRenderer.invoke("emrg:listSkills"), listProjects: () => ipcRenderer.invoke("emrg:listProjects"), listProjectSessions: (payload) => ipcRenderer.invoke("emrg:listProjectSessions", payload), diff --git a/emrg/gui/test/build-config.test.js b/emrg/gui/test/build-config.test.js index 739afe18..f71630a3 100644 --- a/emrg/gui/test/build-config.test.js +++ b/emrg/gui/test/build-config.test.js @@ -91,3 +91,23 @@ test("every main.js/preload.js local require is covered by files whitelist", () ); }); + +test("preload exposes workspace-panel APIs (listFiles/readFile)", () => { + // 右栏工作区面板 P1(rant 2026-08-11T12:20:35 P1.2):preload 桥必须暴露两个新 API, + // 否则 renderer 无法触达 daemon 的 list_files/read_file 命令。 + const preload = fs.readFileSync(path.join(GUI_ROOT, "preload.js"), "utf-8"); + for (const [api, channel] of [ + ["listFiles", "emrg:listFiles"], + ["readFile", "emrg:readFile"], + ]) { + assert.match( + preload, + new RegExp(`${api}: \\((payload)?\\) => ipcRenderer\\.invoke\\("${channel}"`), + `preload.js must expose ${api} → ${channel}` + ); + } + const main = fs.readFileSync(path.join(GUI_ROOT, "main.js"), "utf-8"); + for (const channel of ["emrg:listFiles", "emrg:readFile"]) { + assert.match(main, new RegExp(`ipcMain\\.handle\\("${channel}"`), `main.js must handle ${channel}`); + } +}); diff --git a/emrg/gui/test/daemon_client.test.js b/emrg/gui/test/daemon_client.test.js index 2fff4dd9..b504a12a 100644 --- a/emrg/gui/test/daemon_client.test.js +++ b/emrg/gui/test/daemon_client.test.js @@ -592,6 +592,7 @@ test("帧分类(G21+G58):各帧事件分发正确", async () => { send({ error: "boom" }); send({ identity: { instance_id: "i1" }, uptime_seconds: 10, evolution_count: 3, model: "m1" }); send({ type: "sessions_list", sessions: [] }); + send({ type: "files_list", path: "/tmp", entries: [] }); // 右栏工作区 P1:list_result 白名单 send({ type: "resume_result", session_id: "s1" }); send({ type: "model_set", model: "m2" }); send({ type: "session_deleted", session_id: "s1" }); @@ -599,7 +600,7 @@ test("帧分类(G21+G58):各帧事件分发正确", async () => { const types = seen.map(([t]) => t); assert.deepStrictEqual(types, [ "tool_started", "tool_finished", "message_delta", "group_cleared", "done", "cancelled", "error", "pong", - "list_result", "command_result", "command_result", "command_result", + "list_result", "list_result", "command_result", "command_result", "command_result", ]); // pong 帧数据 const pong = seen.find(([t]) => t === "pong")[1]; @@ -676,6 +677,20 @@ test("RESPONSE_TYPES 映射表与 daemon 命令名一致(修正 clear/rename/t const r8 = await p8; assert.strictEqual(r8.type, "github_connect_web_result"); assert.strictEqual(r8.code, "ABCD-1234"); + // list_files → files_list(右栏工作区面板 P1,rant 2026-08-11T12:20:35) + const p9 = client.sendCommandAndWait("list_files", { path: "/tmp" }, 2000); + await new Promise((r) => setTimeout(r, 10)); + currentMockWs.emit("message", Buffer.from(JSON.stringify({ type: "files_list", path: "/tmp", entries: [], truncated: false }))); + const r9 = await p9; + assert.strictEqual(r9.type, "files_list"); + assert.deepStrictEqual(r9.entries, []); + // read_file → file_content(右栏工作区面板 P1) + const p10 = client.sendCommandAndWait("read_file", { path: "/tmp/a.txt" }, 2000); + await new Promise((r) => setTimeout(r, 10)); + currentMockWs.emit("message", Buffer.from(JSON.stringify({ type: "file_content", path: "/tmp/a.txt", content: "hi", binary: false }))); + const r10 = await p10; + assert.strictEqual(r10.type, "file_content"); + assert.strictEqual(r10.content, "hi"); }); test("命令-响应配对超时 → reject(G93)", async () => { diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 1042727b..ec9b2aa6 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -1243,6 +1243,33 @@ async def _process_message( await self._handle_read_memory(scope, memory_id, session_id, cwd, ws) + elif msg_type == "list_files": + # GUI right-panel workspace file browser (rant 2026-08-11T12:20:35 P1.1) + path_str = msg.get("path", "") + if not path_str: + await self._send(ws, { + "type": "files_list", + "error": "list_files requires path", + }) + return + await self._handle_list_files(path_str, ws) + + elif msg_type == "read_file": + # GUI right-panel file viewer (rant 2026-08-11T12:20:35 P1.1) + path_str = msg.get("path", "") + if not path_str: + await self._send(ws, { + "type": "file_content", + "error": "read_file requires path", + }) + return + await self._handle_read_file( + path_str, + msg.get("start_line"), + msg.get("line_limit"), + ws, + ) + elif msg_type == "rant": # Store user rant/feedback for evolution analysis rant_message = msg.get("message", "").strip() @@ -2770,6 +2797,150 @@ async def _handle_resume_session( }, }) + # ── GUI workspace panel: list_files / read_file (rant 2026-08-11T12:20:35 P1.1) ── + # 单目录条目上限:超出截断 + truncated 提示(与 ReadTool 的防爆理念一致) + _MAX_LIST_ENTRIES = 5000 + # read_file 1MB 上限:更大文件引导用系统工具打开(避免 WebSocket 帧过大) + _MAX_READ_FILE_SIZE = 1 * 1024 * 1024 + # 显式 line_limit 的上限(对齐 ReadTool.MAX_LINES) + _MAX_READ_LINES = 2000 + + async def _handle_list_files(self, path_str: str, ws) -> None: + """List one directory level (workspace file browser data source). + + Returns {"type": "files_list", path, entries: [{name, path, type}], + truncated?} — fields deliberately limited to name/type/path (no + size/mtime: per-entry stat is expensive on 5000-entry directories). + """ + raw = Path(path_str).expanduser() + if not raw.is_absolute(): + await self._send(ws, { + "type": "files_list", + "error": "path must be absolute", + }) + return + path = raw.resolve() + try: + if not path.exists(): + await self._send(ws, { + "type": "files_list", + "error": f"path not found: {path}", + }) + return + if not path.is_dir(): + await self._send(ws, { + "type": "files_list", + "error": f"not a directory: {path}", + }) + return + entries = sorted( + path.iterdir(), + # 目录在前、按名排序(对齐 ReadTool);符号链接不跟随 → + # is_dir(follow_symlinks=False) 为 False → 归入 file 类不可展开 + key=lambda p: (not p.is_dir(follow_symlinks=False), p.name), + ) + result = [] + truncated = False + for e in entries[: self._MAX_LIST_ENTRIES]: + is_dir = e.is_dir(follow_symlinks=False) + result.append({ + "name": e.name, + "path": str(e), + "type": "dir" if is_dir else "file", + }) + if len(entries) > self._MAX_LIST_ENTRIES: + truncated = True + await self._send(ws, { + "type": "files_list", + "path": str(path), + "entries": result, + "truncated": truncated, + }) + except OSError as exc: + await self._send(ws, { + "type": "files_list", + "error": f"cannot list directory: {exc}", + }) + + async def _handle_read_file( + self, path_str: str, start_line, line_limit, ws + ) -> None: + """Read a text file (workspace panel viewer data source). + + Returns {"type": "file_content", path, content, truncated?, binary?, + error?}. Binary files report binary=True with empty content (image + preview is rendered via file:// URL in the renderer, no base64). + """ + raw = Path(path_str).expanduser() + if not raw.is_absolute(): + await self._send(ws, { + "type": "file_content", + "error": "path must be absolute", + }) + return + path = raw.resolve() + try: + if not path.exists(): + await self._send(ws, { + "type": "file_content", + "error": f"file not found: {path}", + }) + return + if path.is_dir(): + await self._send(ws, { + "type": "file_content", + "error": f"is a directory: {path}", + }) + return + file_size = path.stat().st_size + if file_size > self._MAX_READ_FILE_SIZE: + await self._send(ws, { + "type": "file_content", + "path": str(path), + "error": "文件过大,用系统工具打开", + }) + return + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + await self._send(ws, { + "type": "file_content", + "path": str(path), + "binary": True, + }) + return + except OSError as exc: + await self._send(ws, { + "type": "file_content", + "error": f"cannot read file: {exc}", + }) + return + + all_lines = text.split("\n") + total = len(all_lines) + try: + start = max(1, int(start_line or 1)) + except (TypeError, ValueError): + start = 1 + try: + limit = int(line_limit) if line_limit is not None else None + except (TypeError, ValueError): + limit = None + if limit is not None: + limit = min(limit, self._MAX_READ_LINES) + selected = all_lines[start - 1 : start - 1 + limit] + truncated = start - 1 + limit < total + else: + selected = all_lines[start - 1 :] + truncated = False + await self._send(ws, { + "type": "file_content", + "path": str(path), + "content": "\n".join(selected), + "truncated": truncated, + "total_lines": total, + }) + async def _handle_list_memories( self, scope: str, session_id: str, cwd: str, ws ) -> None: diff --git a/tests/test_ws_e2e.py b/tests/test_ws_e2e.py index 35e1c9e8..31e3a54f 100644 --- a/tests/test_ws_e2e.py +++ b/tests/test_ws_e2e.py @@ -953,3 +953,177 @@ async def chat_stream(messages, tools=None): finally: await cleanup() asyncio.run(_test()) + + +class TestWSWorkspacePanel: + """GUI right-panel workspace commands (rant 2026-08-11T12:20:35 P1.1). + + list_files → files_list (sorted, truncated, absolute-path-only, + symlinks not followed); read_file → file_content (paging, binary + detection, 1MB cap). + """ + + @staticmethod + async def _cmd(ws, payload): + await ws.send(json.dumps(payload)) + return json.loads(await asyncio.wait_for(ws.recv(), timeout=5)) + + def test_list_files_sorted_dirs_first(self): + async def _test(): + with tempfile.TemporaryDirectory() as tmp: + cwd = Path(tmp) + work = cwd / "work" + work.mkdir() + (work / "zdir").mkdir() + (work / "afile.txt").write_text("x", encoding="utf-8") + (work / "adir").mkdir() + (work / "bfile.py").write_text("y", encoding="utf-8") + _, _, cleanup = await _boot_server(cwd) + try: + ws = await connect_to_server() + try: + resp = await self._cmd(ws, {"type": "list_files", "path": str(work)}) + assert resp["type"] == "files_list" + assert resp.get("error") is None + # 目录在前(adir、zdir),文件在后(afile.txt、bfile.py) + names = [e["name"] for e in resp["entries"]] + types = [e["type"] for e in resp["entries"]] + assert types == ["dir", "dir", "file", "file"], types + assert names == ["adir", "zdir", "afile.txt", "bfile.py"], names + # 字段只含 name/type/path(无 size/mtime) + assert set(resp["entries"][0].keys()) == {"name", "path", "type"} + assert resp["truncated"] is False + finally: + await ws.close() + finally: + await cleanup() + asyncio.run(_test()) + + def test_list_files_relative_path_rejected(self): + async def _test(): + with tempfile.TemporaryDirectory() as tmp: + cwd = Path(tmp) + _, _, cleanup = await _boot_server(cwd) + try: + ws = await connect_to_server() + try: + resp = await self._cmd(ws, {"type": "list_files", "path": "."}) + assert resp["type"] == "files_list" + assert "error" in resp + assert "absolute" in resp["error"] + resp2 = await self._cmd(ws, {"type": "list_files", "path": str(cwd / "nope")}) + assert "error" in resp2 + finally: + await ws.close() + finally: + await cleanup() + asyncio.run(_test()) + + def test_list_files_symlink_not_followed(self): + async def _test(): + with tempfile.TemporaryDirectory() as tmp: + cwd = Path(tmp) + work = cwd / "work" + work.mkdir() + (work / "real").mkdir() + try: + (work / "link").symlink_to(work / "real", target_is_directory=True) + except (OSError, NotImplementedError): + return # 平台不支持符号链接 → 跳过 + _, _, cleanup = await _boot_server(cwd) + try: + ws = await connect_to_server() + try: + resp = await self._cmd(ws, {"type": "list_files", "path": str(work)}) + by_name = {e["name"]: e for e in resp["entries"]} + assert "real" in by_name and by_name["real"]["type"] == "dir" + assert "link" in by_name and by_name["link"]["type"] == "file", "symlink must not be expandable" + finally: + await ws.close() + finally: + await cleanup() + asyncio.run(_test()) + + def test_list_files_truncated_at_5000(self): + async def _test(): + with tempfile.TemporaryDirectory() as tmp: + cwd = Path(tmp) + work = cwd / "work" + work.mkdir() + for i in range(5100): + (work / f"f{i:05d}.txt").write_text("x", encoding="utf-8") + _, _, cleanup = await _boot_server(cwd) + try: + ws = await connect_to_server() + try: + resp = await self._cmd(ws, {"type": "list_files", "path": str(work)}) + assert resp["type"] == "files_list" + assert len(resp["entries"]) == 5000 + assert resp["truncated"] is True + finally: + await ws.close() + finally: + await cleanup() + asyncio.run(_test()) + + def test_read_file_text_and_paging(self): + async def _test(): + with tempfile.TemporaryDirectory() as tmp: + cwd = Path(tmp) + work = cwd / "work" + work.mkdir() + f = work / "doc.txt" + f.write_text("\n".join(f"line{i}" for i in range(1, 11)), encoding="utf-8") + _, _, cleanup = await _boot_server(cwd) + try: + ws = await connect_to_server() + try: + # 全文(无分页参数) + resp = await self._cmd(ws, {"type": "read_file", "path": str(f)}) + assert resp["type"] == "file_content" + assert resp["content"].split("\n")[0] == "line1" + assert resp["content"].split("\n")[-1] == "line10" + assert resp["truncated"] is False + assert resp["total_lines"] == 10 + # 分页 start_line=3, line_limit=2 + resp2 = await self._cmd(ws, { + "type": "read_file", "path": str(f), + "start_line": 3, "line_limit": 2, + }) + assert resp2["content"].split("\n") == ["line3", "line4"] + assert resp2["truncated"] is True + # 相对路径拒绝 + resp3 = await self._cmd(ws, {"type": "read_file", "path": "doc.txt"}) + assert "error" in resp3 + finally: + await ws.close() + finally: + await cleanup() + asyncio.run(_test()) + + def test_read_file_binary_and_large(self): + async def _test(): + with tempfile.TemporaryDirectory() as tmp: + cwd = Path(tmp) + work = cwd / "work" + work.mkdir() + bin_f = work / "img.png" + bin_f.write_bytes(b"\x89PNG\r\n\x1a\n\x00\x01binary") + big_f = work / "big.bin" + big_f.write_bytes(b"0" * (1024 * 1024 + 1)) + _, _, cleanup = await _boot_server(cwd) + try: + ws = await connect_to_server() + try: + resp = await self._cmd(ws, {"type": "read_file", "path": str(bin_f)}) + assert resp["type"] == "file_content" + assert resp.get("binary") is True + assert not resp.get("content") + resp2 = await self._cmd(ws, {"type": "read_file", "path": str(big_f)}) + assert "error" in resp2 + assert "系统工具" in resp2["error"] + finally: + await ws.close() + finally: + await cleanup() + asyncio.run(_test()) From de3202c05f3b784baed4d778075caef740de1d5b Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Tue, 11 Aug 2026 12:41:56 +0800 Subject: [PATCH 2/2] emrg: drop philosophy taglines from P1 branch (README rant goes in its own PR) --- README.cn.md | 3 --- README.md | 3 --- 2 files changed, 6 deletions(-) diff --git a/README.cn.md b/README.cn.md index 19f6bf3a..07fc165c 100644 --- a/README.cn.md +++ b/README.cn.md @@ -23,9 +23,6 @@ EMRG 是一个关于*自主进化*的实验:一个能帮你写代码的 AI 智能体——读文件、跑命令、做编辑——而**你的每一条吐槽 (`/rant`) 都会驱动它自我改进**。你说它哪里不好,下一个演化周期它就自己写代码修好并上线。完全开源,完全透明。 > *"EMRG 是一个自我进化的 AI 智能体架构实验。"* — [MANIFESTO](MANIFESTO.md) -> -> *人人都是产品经理——你的每一条吐槽,都是下一版的需求单。* -> *人人都是硅基生命的宿主——你运行的,是一个与你共生进化的数字生命。* --- diff --git a/README.md b/README.md index 2dd21e0d..fce9f653 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,6 @@ EMRG is an experiment in *autonomous self-improvement*: an AI agent that helps you code — reading files, running commands, making edits — and **every `/rant` you send drives it to improve itself**. Tell it what bothers you, and the next evolution cycle writes the fix and ships it. Fully open source, fully transparent. > *"EMRG is an experiment in self-evolving AI agent architecture."* — [MANIFESTO](MANIFESTO.md) -> -> *Everyone is a product manager — every rant is a ticket for the next release.* -> *Everyone is a host of silicon life — what you run is a digital organism that evolves with you.* ---