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` (253: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 16 i18n + 7 integration + 3 commands + 6 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 16 i18n + 7 integration + 3 commands + 7 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` (825) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (253: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 16 i18n + 7 integration + 3 commands + 6 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 16 i18n + 7 integration + 3 commands + 7 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
7 changes: 4 additions & 3 deletions emrg/gui/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ <h2 data-i18n="rename.title">重命名对话</h2>
<!-- 右键菜单(对话列表:重命名 / 删除) -->
<div id="ctx-menu" class="ctx-menu" hidden></div>

<!-- rant 2026-08-15T10:48:58: marked/dompurify/highlight MUST load BEFORE monaco loader.js — loader.js defines global AMD define/require (L1347 u.global.define=p); UMD marked sees AMD -> define branch -> window.marked never set -> chat/rant markdown degrades to plain text. UMD-first order fixes it (no AMD present -> UMD branch). -->
<script src="../vendor/marked.min.js"></script>
<script src="../vendor/dompurify.min.js"></script>
<script src="../vendor/highlight.custom.js"></script>
<script src="../vendor/monaco/vs/loader.js"></script>
<script>
// Monaco (local vendor, zero network; rant 2026-08-15T09:20:12 option A).
Expand All @@ -400,9 +404,6 @@ <h2 data-i18n="rename.title">重命名对话</h2>
}
};
</script>
<script src="../vendor/marked.min.js"></script>
<script src="../vendor/dompurify.min.js"></script>
<script src="../vendor/highlight.custom.js"></script>
<!-- 帮助对话框(/help,GUI / 指令 P1) -->
<dialog id="help-dialog">
<div class="dialog-card" style="min-width:420px;">
Expand Down
27 changes: 25 additions & 2 deletions emrg/gui/renderer/js/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,33 @@ function ensureConfigured() {
window.__emrgMarkedConfigured = true;
}

/** AMD-hijack recovery (rant 2026-08-15T10:48:58) — fallback layer:
* monaco loader 的全局 AMD define 会让 UMD marked/dompurify 走 define 分支而非挂 window;
* 顺序修复(index.html marked 先于 loader.js)后正常路径不触发,此处兜底 + 可诊断。 */
function recoverAmdModule(name) {
try {
// monaco loader 的 require 内部模块表: require.s[name] = { exports }
if (window.require && window.require.s && window.require.s[name]) {
const mod = window.require.s[name].exports;
if (mod) return mod;
}
// 部分打包器形态: define.amd.modules
if (window.define && window.define.amd && window.define.amd.modules && window.define.amd.modules[name]) {
return window.define.amd.modules[name].exports;
}
} catch { /* ignore */ }
return null;
}

renderer.renderMarkdown = async function (mdText) {
if (!window.marked) {
console.warn("markdown vendor missing (marked not loaded)");
return escapeHtml(mdText || "");
const amdMarked = recoverAmdModule("marked");
if (amdMarked && typeof amdMarked.parse === "function") {
window.marked = amdMarked; // 兜底取回:AMD 劫持场景仍可渲染
} else {
console.error("markdown vendor missing (marked not loaded — check index.html order: marked must load before monaco loader.js, which defines global AMD define/require)");
return escapeHtml(mdText || "");
}
}
try {
ensureConfigured();
Expand Down
28 changes: 25 additions & 3 deletions emrg/gui/test/build-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,35 @@ test("vendor ships Monaco editor (loader + editor.main js/css)", () => {
}
});

test("renderMarkdown fallback logs when marked is missing", () => {
test("renderMarkdown fallback logs when marked is missing (AMD-hijack diagnostic)", () => {
const md = fs.readFileSync(path.join(GUI_ROOT, "renderer", "js", "markdown.js"), "utf-8");
assert.match(
md,
/if \(!window\.marked\) \{[\s\S]*console\.warn\("markdown vendor missing \(marked not loaded\)"\)/,
"silent markdown degradation must be diagnostic (console.warn when marked is missing)"
/if \(!window\.marked\) \{[\s\S]*console\.error\("markdown vendor missing \(marked not loaded/,
"silent markdown degradation must be diagnostic (console.error naming the AMD-hijack cause when marked is missing)"
);
assert.match(
md,
/recoverAmdModule\("marked"\)/,
"must attempt AMD module-table recovery before degrading (rant 2026-08-15T10:48:58)"
);
});

test("index.html loads marked/dompurify/highlight BEFORE monaco loader.js (AMD-hijack order guard)", () => {
// rant 2026-08-15T10:48:58:monaco/vs/loader.js 定义全局 AMD define/require;
// UMD marked 见 AMD 走 define 分支 → window.marked 永不设置 → markdown 全退化纯文本。
// 顺序修复 = marked 等 UMD 库必须先于 loader.js 加载(无 AMD → UMD 分支挂 window)。
const html = fs.readFileSync(path.join(GUI_ROOT, "renderer", "index.html"), "utf-8");
const idxMarked = html.indexOf('src="../vendor/marked.min.js"');
const idxDompurify = html.indexOf('src="../vendor/dompurify.min.js"');
const idxHighlight = html.indexOf('src="../vendor/highlight.custom.js"');
const idxMonaco = html.indexOf('src="../vendor/monaco/vs/loader.js"');
for (const [name, idx] of [["marked", idxMarked], ["dompurify", idxDompurify], ["highlight", idxHighlight], ["monaco loader", idxMonaco]]) {
assert.ok(idx >= 0, `index.html must include ${name} script`);
}
assert.ok(idxMarked < idxMonaco, "marked.min.js must load BEFORE monaco loader.js (else AMD define hijacks UMD marked)");
assert.ok(idxDompurify < idxMonaco, "dompurify.min.js must load BEFORE monaco loader.js");
assert.ok(idxHighlight < idxMonaco, "highlight.custom.js must load BEFORE monaco loader.js");
});

test("every main.js/preload.js local require is covered by files whitelist", () => {
Expand Down
22 changes: 22 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,28 @@ test("代码块复制按钮:codeRenderer 输出容器 + 点击复制(设计
assert.ok(chatSrc.includes(".code-copy"), "chat.js 应含复制按钮事件委托");
});

test("rant 10:48:58:window.marked 缺失时从 Monaco AMD 模块表兜底取回(防退化纯文本)", async () => {
const { ctx, win } = makeSandbox({});
// 模拟 #801 Monaco 全局 AMD define 劫持后 marked 未挂 window 的场景:
// window.marked = null(沙箱默认),但 monaco require 模块表里有 marked
win.marked = undefined; // 强制缺失(沙箱默认 null → 触发兜底路径)
win.require = {
s: {
marked: { exports: { use: () => {}, parse: async () => "<p><strong>ok</strong></p>" } },
},
};
win.DOMPurify = { sanitize: (x) => x };
const html = await vm.runInContext('window.emrgMarkdown.renderMarkdown("**ok**")', ctx);
assert.ok(String(html).includes("<strong>ok</strong>"), `AMD 兜底应渲染 marked:${html}`);
assert.ok(win.marked && typeof win.marked.parse === "function", "兜底取回后 window.marked 应被赋值");
// 无 AMD 表也无 marked → 降级转义 + 可诊断错误(不崩)
const { ctx: ctx2, win: win2 } = makeSandbox({});
win2.marked = undefined;
win2.require = undefined;
const html2 = await vm.runInContext('window.emrgMarkdown.renderMarkdown("<b>x</b>")', ctx2);
assert.strictEqual(html2, "&lt;b&gt;x&lt;/b&gt;", "双缺失 → escapeHtml 降级");
});

test("模型切换器:菜单项构建 + 键盘导航 handler 注册", async () => {
const { ctx } = makeSandbox({
getSettings: async () => ({
Expand Down
Loading