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` (242: 44 daemon_client + 19 conn-manager + 22 app-commands + 118 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`
- Unit tests `npm test` (243: 44 daemon_client + 19 conn-manager + 22 app-commands + 118 renderer smoke + 16 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` (242: 44 daemon_client + 19 conn-manager + 22 app-commands + 118 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`
GUI: `cd emrg/gui && npm test` (243: 44 daemon_client + 19 conn-manager + 22 app-commands + 118 renderer smoke + 16 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
30 changes: 0 additions & 30 deletions emrg/gui/renderer/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const I18N = (() => {
"projects.empty": "暂无项目(点击「添加项目」注册工作目录)",
"projects.added": "项目已添加:{path}",
"projects.addFailed": "添加项目失败:{msg}",
"projects.sessions": "会话",
"projects.autoEvolve": "演化",
"projects.viewSessions": "查看会话",
"projects.noSessions": "该项目暂无会话",
Expand All @@ -56,15 +55,12 @@ const I18N = (() => {
"composer.modelLoading": "加载中…",
"composer.switchModel": "切换模型",
"composer.modeTitle": "工作模式:Ask 只对话 / Auto 自动执行",
"composer.modeAsk": "Ask",
"composer.modeAuto": "Auto",
"composer.placeholder": "发消息给 EMRG… (Enter 发送 / Shift+Enter 换行)",
"composer.send": "发送",
"composer.stop": "停止回复",
"composer.hint": "EMRG 可能会犯错,请核对重要信息",

// 结果面板
"result.title": "产物",
"result.collapse": "折叠/展开 (⌘\\)",
"result.empty": "还没有产物,对话中生成的文件会出现在这里",
"result.tabFiles": "文件",
Expand Down Expand Up @@ -95,7 +91,6 @@ const I18N = (() => {
"settings.aboutTitle": "关于",
"settings.aboutDesc": "EMRG 是一个会自我进化的 AI 智能体——每次改进都会自动汇报,你可以随时在这里看到它的成长。",
"settings.recentTitle": "最近改进",
"settings.groupTasks": "定时任务",
"settings.tasksHint": "管理定时任务与自定义任务类型(内置类型只读)",
"settings.taskAdd": "+ 添加任务",
"settings.taskTemplates": "自定义类型",
Expand Down Expand Up @@ -168,7 +163,6 @@ const I18N = (() => {
"settings.installFailed": "安装启动失败:{msg}",
"app.updateReady": "新版本 v{latest} 已下载 —— 设置 → 关于 → 点击安装",
"app.updateAvailable": "发现新版本 v{latest} —— 点击前往 Releases 下载(不会自动安装):https://github.com/argszero/emrg/releases",
"settings.githubTokenEmpty": "请先粘贴 GitHub Personal Access Token",
"settings.githubConnecting": "连接中…",
"settings.githubConnected": "已连接 GitHub:@{user}(gh auth setup-git 已执行)",
"settings.githubConnectFailed": "GitHub 连接失败:{msg}",
Expand Down Expand Up @@ -266,8 +260,6 @@ const I18N = (() => {
"rants.statusCompleted": "已完成",
"rants.detail": "完整内容",
"rants.noProgress": "(无进度说明)",
"tasks.title": "后台任务",
"tasks.desc": "点击任务立即触发一次运行。",

// / 指令 hint(commands.js 补全菜单)
"cmd.clear.hint": "清空当前对话",
Expand Down Expand Up @@ -341,8 +333,6 @@ const I18N = (() => {
"util.groupYesterday": "昨天",
"util.groupEarlier": "更早",
// 结果面板(result-panel.js)
"panel.openFailed": "无法打开文件",
"panel.copied": "✓ 已复制",
// Markdown 代码块(markdown.js)
"md.copyCode": "复制代码",
// 对话框动态文案(dialogs.js)
Expand Down Expand Up @@ -382,13 +372,11 @@ const I18N = (() => {
"app.compacted": "已压缩当前对话历史。",
"app.imagePaste": "请直接粘贴图片到输入框(Ctrl+V / ⌘V)。",
"app.cmdFailed": "指令 {cmd} 执行失败:{msg}",
"app.helpNoSessions": "还没有对话,输入内容即可开始。",
"app.unnamed": "(未命名)",
"app.current": "当前",
"app.noHistory": "没有可回退的历史消息。",
"app.historyFailed": "加载历史失败:{msg}",
"app.historyLoadMore": "↑ 加载更早消息",
"app.historyLoading": "加载中…",
"app.historyNoMore": "没有更多历史",
"app.rewound": "已回退到消息点 #{index},移除了 {n} 条记录。",
"app.rewindFailed": "回退失败:{msg}",
Expand All @@ -399,12 +387,10 @@ const I18N = (() => {
"app.memFailed": "加载记忆失败:{msg}",
"app.noSkills": "还没有加载技能。",
"app.skillsFailed": "加载技能失败:{msg}",
"app.globalAll": "(全局 — 所有项目)",
"app.rantEmpty": "写点内容再提交吧。",
"app.rantReceived": "✓ 收到!EMRG 会据此进化。",
"app.rantCount": "(已累计 {n} 条反馈)",
"app.rantFailed": "提交失败了:{msg}",
"app.noTasks": "没有可触发的任务。",
"app.taskDisabled": "已停用",
"app.taskInterval": "间隔 {n}s",
"app.tasksFailed": "加载任务失败:{msg}",
Expand All @@ -422,7 +408,6 @@ const I18N = (() => {
"app.closeSession": "❌ 关闭会话(保留数据)",
"app.closeFailed": "关闭会话失败了:{msg}",
"sidebar.openSessions": "打开的会话",
"sidebar.noOpenSessions": "暂无打开的会话",
"sidebar.openSessionOf": "{project} / {title}",
"app.noModels": "还没有配置模型",
"app.goSettings": "去设置添加",
Expand Down Expand Up @@ -460,7 +445,6 @@ const I18N = (() => {
"projects.empty": "No projects yet (click “Add project” to register a working directory)",
"projects.added": "Project added: {path}",
"projects.addFailed": "Failed to add project: {msg}",
"projects.sessions": "Sessions",
"projects.autoEvolve": "Evolve",
"projects.viewSessions": "Sessions",
"projects.noSessions": "No sessions for this project yet",
Expand All @@ -479,15 +463,12 @@ const I18N = (() => {
"composer.modelLoading": "Loading…",
"composer.switchModel": "Switch model",
"composer.modeTitle": "Work mode: Ask (chat only) / Auto (run tools)",
"composer.modeAsk": "Ask",
"composer.modeAuto": "Auto",
"composer.placeholder": "Message EMRG… (Enter to send / Shift+Enter for new line)",
"composer.send": "Send",
"composer.stop": "Stop reply",
"composer.hint": "EMRG may make mistakes — please verify important information",

// Result panel
"result.title": "Artifacts",
"result.collapse": "Collapse/expand (⌘\\)",
"result.empty": "No artifacts yet — files generated in the conversation will appear here",
"result.tabFiles": "Files",
Expand Down Expand Up @@ -518,7 +499,6 @@ const I18N = (() => {
"settings.aboutTitle": "About",
"settings.aboutDesc": "EMRG is a self-evolving AI agent — it reports every improvement, and you can watch it grow right here.",
"settings.recentTitle": "Recent improvements",
"settings.groupTasks": "Scheduled tasks",
"settings.tasksHint": "Manage scheduled tasks and custom task types (builtin types are read-only)",
"settings.taskAdd": "+ Add task",
"settings.taskTemplates": "Custom types",
Expand Down Expand Up @@ -591,7 +571,6 @@ const I18N = (() => {
"settings.installFailed": "Failed to launch installer: {msg}",
"app.updateReady": "New version v{latest} downloaded — install it from Settings → About",
"app.updateAvailable": "New version v{latest} available — https://github.com/argszero/emrg/releases (no auto-install)",
"settings.githubTokenEmpty": "Please paste a GitHub Personal Access Token first",
"settings.githubConnecting": "Connecting…",
"settings.githubConnected": "Connected to GitHub: @{user} (gh auth setup-git done)",
"settings.githubConnectFailed": "GitHub connect failed: {msg}",
Expand Down Expand Up @@ -689,8 +668,6 @@ const I18N = (() => {
"rants.statusCompleted": "Completed",
"rants.detail": "Full content",
"rants.noProgress": "(no progress note)",
"tasks.title": "Background tasks",
"tasks.desc": "Click a task to trigger it immediately.",

// / command hints
"cmd.clear.hint": "Clear current conversation",
Expand Down Expand Up @@ -764,8 +741,6 @@ const I18N = (() => {
"util.groupYesterday": "Yesterday",
"util.groupEarlier": "Earlier",
// Result panel (result-panel.js)
"panel.openFailed": "Could not open file",
"panel.copied": "✓ Copied",
// Markdown code block (markdown.js)
"md.copyCode": "Copy code",
// Dialog dynamic copy (dialogs.js)
Expand Down Expand Up @@ -805,13 +780,11 @@ const I18N = (() => {
"app.compacted": "Conversation history compacted.",
"app.imagePaste": "Paste the image into the input (Ctrl+V / ⌘V).",
"app.cmdFailed": "Command {cmd} failed: {msg}",
"app.helpNoSessions": "No conversations yet — start typing.",
"app.unnamed": "(untitled)",
"app.current": "current",
"app.noHistory": "No history points to rewind to.",
"app.historyFailed": "Failed to load history: {msg}",
"app.historyLoadMore": "↑ Load earlier messages",
"app.historyLoading": "Loading…",
"app.historyNoMore": "No more history",
"app.rewound": "Rewound to message point #{index}, removed {n} records.",
"app.rewindFailed": "Rewind failed: {msg}",
Expand All @@ -822,12 +795,10 @@ const I18N = (() => {
"app.memFailed": "Failed to load memories: {msg}",
"app.noSkills": "No skills loaded yet.",
"app.skillsFailed": "Failed to load skills: {msg}",
"app.globalAll": "(Global — all projects)",
"app.rantEmpty": "Write something first.",
"app.rantReceived": "✓ Got it! EMRG will evolve accordingly.",
"app.rantCount": "({n} feedback items so far)",
"app.rantFailed": "Submission failed: {msg}",
"app.noTasks": "No tasks to trigger.",
"app.taskDisabled": "Disabled",
"app.taskInterval": "every {n}s",
"app.tasksFailed": "Failed to load tasks: {msg}",
Expand All @@ -845,7 +816,6 @@ const I18N = (() => {
"app.closeSession": "❌ Close session (keep data)",
"app.closeFailed": "Failed to close session: {msg}",
"sidebar.openSessions": "Open sessions",
"sidebar.noOpenSessions": "No open sessions",
"sidebar.openSessionOf": "{project} / {title}",
"app.noModels": "No models configured",
"app.goSettings": "Add in Settings",
Expand Down
58 changes: 58 additions & 0 deletions emrg/gui/test/i18n.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,61 @@ test("Stage3b:最近改进列表文案双语(#502 evolution_summary 侧)",
const { ctx: zh } = makeSandbox({ navigator: { language: "zh-CN" } });
assert.strictEqual(evalIn(zh, 'I18N.t("app.recentImprovements")'), "最近改进");
});

// ── Orphan-key guard(cycle 20260813-223026:#771 ❌ 教训固化)────────
// #755 清理了 8 组孤儿键、#771 评审 ❌ 检出 7 个孤儿 i18n keys + 12 个未用 CSS 类——
// 两个方向都缺自动守卫:①词典里定义了但全库无引用的孤儿键(删除功能后残留)
// ②标记/代码里引用了但词典未定义的键(拼写错误会静默回退显示原始 key)。
// 本测试扫描 index.html data-i18n* 属性 + renderer JS 全部取词点,双向校验。
test("i18n 守卫:词典键无孤儿(定义了必须被引用)且引用键无缺失(引用必须已定义)", () => {
const html = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf8");
const { ctx } = makeSandbox({ navigator: { language: "zh-CN" } });
const dictKeys = new Set(evalIn(ctx, "Object.keys(I18N.DICTS.zh)"));
assert.ok(dictKeys.size > 60, `词典应有键(实际 ${dictKeys.size})`);

// 1) 收集引用:index.html data-i18n / data-i18n-placeholder / data-i18n-title
const used = new Set();
const attrRe = /data-i18n(?:-placeholder|-title)?="([^"]+)"/g;
let m;
while ((m = attrRe.exec(html)) !== null) used.add(m[1]);

// 2) 收集引用:renderer JS 全部取词调用(跳过 i18n.js 词典本身)。
// 取 t-call 整体跨度(含三元前缀 _t(cond ? "A" : "B")),再取其中所有字面键;
// 模板串 t(`tool.${base}.doing`) → 静态段拼 glob(tool.*.doing)。
const globs = [];
const callRe = /(?:_t|I18N\.t|EMRG_I18N\.t|\bt)\([^)]*\)/g;
for (const f of fs.readdirSync(path.join(__dirname, "..", "renderer", "js"))) {
if (!f.endsWith(".js") || f === "i18n.js") continue;
const src = fs.readFileSync(path.join(__dirname, "..", "renderer", "js", f), "utf8");
let cm;
while ((cm = callRe.exec(src)) !== null) {
const body = cm[0];
if (body.includes("`")) {
const tplRe = /`([^`]*)`/;
const tpl = (tplRe.exec(body) || [])[1] || "";
if (tpl.includes("${")) globs.push(tpl.replace(/\$\{[^}]+\}/g, "*"));
else if (tpl) used.add(tpl);
continue;
}
const keyRe = /"([a-z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)+)"/g;
let km;
while ((km = keyRe.exec(body)) !== null) used.add(km[1]);
}
// 对象属性值引用(命令注册表 hint: "cmd.clear.hint" 形态)
const propRe = /\b(?:hint|title|key|label|placeholder|text|msg):\s*"([a-z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)+)"/g;
let pm;
while ((pm = propRe.exec(src)) !== null) used.add(pm[1]);
}
// glob → 正则:转义特殊字符,* → .*
const globRes = globs.map((g) => new RegExp("^" + g.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"));

// 3) 方向 A:词典键必须被引用(孤儿检测)
const orphans = [...dictKeys].filter(
(k) => !used.has(k) && !globRes.some((re) => re.test(k))
);
assert.strictEqual(orphans.length, 0, `孤儿 i18n 键(词典定义但无引用,应删除):\n ${orphans.join("\n ")}`);

// 4) 方向 B:引用键必须已在词典定义(拼写错误检测)
const missing = [...used].filter((k) => !dictKeys.has(k));
assert.strictEqual(missing.length, 0, `引用了但词典未定义的 i18n 键(拼写错误会静默回退):\n ${missing.join("\n ")}`);
});
Loading