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` (225: 43 daemon_client + 19 conn-manager + 22 app-commands + 104 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` (229: 44 daemon_client + 19 conn-manager + 22 app-commands + 107 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 Down Expand Up @@ -113,7 +113,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (744) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (225: 43 daemon_client + 19 conn-manager + 22 app-commands + 104 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` (229: 44 daemon_client + 19 conn-manager + 22 app-commands + 107 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
8 changes: 8 additions & 0 deletions emrg/gui/daemon_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ const RESPONSE_TYPES = {
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)
// rant 2026-08-12T18:23:15 P2/P3:任务 + 自定义类型 CRUD(daemon.py task_create 等)
task_create: "task_result",
task_update: "task_result",
task_delete: "task_result",
task_template_list: "templates_list",
task_template_create: "template_result",
task_template_update: "template_result",
task_template_delete: "template_result",
};

class DaemonClient {
Expand Down
57 changes: 57 additions & 0 deletions emrg/gui/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,63 @@ vision = false
return frame;
});

// ── Task/template CRUD IPC (rant 2026-08-12T18:23:15 P3) ──────────
ipcMain.handle("emrg:taskCreate", async (_e, payload) => {
const { name, type, project, interval, enabled, repo, description } = payload || {};
if (typeof name !== "string" || !name.trim()) throw new Error("invalid task name");
if (typeof type !== "string" || !type.trim()) throw new Error("invalid task type");
if (typeof project !== "string" || !project.trim()) throw new Error("invalid project");
const frame = await requireConn().sendCommandAndWait("task_create", {
name: name.trim(), type: type.trim(), project: project.trim(),
interval, enabled, repo, description,
}, 8000);
if (!frame.ok && frame.error) throw new Error(frame.error);
return frame;
});

ipcMain.handle("emrg:taskUpdate", async (_e, payload) => {
const { name, ...fields } = payload || {};
if (typeof name !== "string" || !name.trim()) throw new Error("invalid task name");
const frame = await requireConn().sendCommandAndWait("task_update", { name: name.trim(), ...fields }, 8000);
if (!frame.ok && frame.error) throw new Error(frame.error);
return frame;
});

ipcMain.handle("emrg:taskDelete", async (_e, { name }) => {
if (typeof name !== "string" || !name.trim()) throw new Error("invalid task name");
const frame = await requireConn().sendCommandAndWait("task_delete", { name: name.trim() }, 8000);
if (!frame.ok && frame.error) throw new Error(frame.error);
return frame;
});

ipcMain.handle("emrg:taskTemplateList", async () => {
const frame = await requireConn().sendCommandAndWait("task_template_list", {}, 5000);
return frame.templates || [];
});

ipcMain.handle("emrg:taskTemplateCreate", async (_e, { name, prompt }) => {
if (typeof name !== "string" || !name.trim()) throw new Error("invalid template name");
if (typeof prompt !== "string" || !prompt.trim()) throw new Error("invalid template prompt");
const frame = await requireConn().sendCommandAndWait("task_template_create", { name: name.trim(), prompt }, 8000);
if (!frame.ok && frame.error) throw new Error(frame.error);
return frame;
});

ipcMain.handle("emrg:taskTemplateUpdate", async (_e, { name, prompt }) => {
if (typeof name !== "string" || !name.trim()) throw new Error("invalid template name");
if (typeof prompt !== "string" || !prompt.trim()) throw new Error("invalid template prompt");
const frame = await requireConn().sendCommandAndWait("task_template_update", { name: name.trim(), prompt }, 8000);
if (!frame.ok && frame.error) throw new Error(frame.error);
return frame;
});

ipcMain.handle("emrg:taskTemplateDelete", async (_e, { name }) => {
if (typeof name !== "string" || !name.trim()) throw new Error("invalid template name");
const frame = await requireConn().sendCommandAndWait("task_template_delete", { name: name.trim() }, 8000);
if (!frame.ok && frame.error) throw new Error(frame.error);
return frame;
});

ipcMain.handle("emrg:sendRant", async (_e, { message, project = "" } = {}) => {
// GUI / 指令 P4:/rant — 提交反馈到演化系统(daemon rant 协议,字段序与 rants.jsonl 一致)
if (typeof message !== "string" || !message.trim()) throw new Error("invalid rant message");
Expand Down
8 changes: 8 additions & 0 deletions emrg/gui/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ const api = {
removeProject: (payload) => ipcRenderer.invoke("emrg:removeProject", payload),
listTasks: () => ipcRenderer.invoke("emrg:listTasks"),
triggerTask: (payload) => ipcRenderer.invoke("emrg:triggerTask", payload),
// Task/template CRUD (rant 2026-08-12T18:23:15 P3)
taskCreate: (payload) => ipcRenderer.invoke("emrg:taskCreate", payload),
taskUpdate: (payload) => ipcRenderer.invoke("emrg:taskUpdate", payload),
taskDelete: (payload) => ipcRenderer.invoke("emrg:taskDelete", payload),
taskTemplateList: () => ipcRenderer.invoke("emrg:taskTemplateList"),
taskTemplateCreate: (payload) => ipcRenderer.invoke("emrg:taskTemplateCreate", payload),
taskTemplateUpdate: (payload) => ipcRenderer.invoke("emrg:taskTemplateUpdate", payload),
taskTemplateDelete: (payload) => ipcRenderer.invoke("emrg:taskTemplateDelete", payload),
sendRant: (payload) => ipcRenderer.invoke("emrg:sendRant", payload),
evolutionSummary: (payload) => ipcRenderer.invoke("emrg:evolutionSummary", payload),
githubStatus: () => ipcRenderer.invoke("emrg:githubStatus"),
Expand Down
69 changes: 69 additions & 0 deletions emrg/gui/renderer/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,75 @@ dialog::backdrop {
.model-form-vision input {
accent-color: var(--accent);
}

/* 定时任务管理(rant 2026-08-12T18:23:15 P3:GUI 任务/自定义类型 CRUD) */
.task-list {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 220px;
overflow-y: auto;
margin-top: 4px;
}
.task-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg-soft);
}
.task-name {
font-weight: 600;
flex-shrink: 0;
max-width: 40%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-hint {
font-size: var(--fs-aux);
color: var(--text-3);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-badge {
font-size: var(--fs-aux);
color: var(--text-3);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0 4px;
flex-shrink: 0;
}
.task-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.task-empty {
font-size: var(--fs-aux);
color: var(--text-3);
text-align: center;
padding: var(--sp-2);
}
.task-form {
margin-top: 8px;
padding: var(--sp-3);
border: 1px dashed var(--border);
border-radius: 10px;
background: var(--bg-soft);
}
.task-form select,
.task-form textarea {
width: 100%;
margin-top: 2px;
padding: 6px 10px;
font-size: var(--fs-secondary);
}

.theme-options {
display: flex;
gap: var(--sp-2);
Expand Down
58 changes: 58 additions & 0 deletions emrg/gui/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,64 @@ <h2 data-i18n="settings.title">设置</h2>
</div>
</label>
</div>
<div class="settings-group">
<div class="settings-group-title" data-i18n="settings.groupTasks">定时任务</div>
<div class="hint" style="margin-bottom:6px;" data-i18n="settings.tasksHint">管理定时任务与自定义任务类型(内置类型只读)</div>
<!-- 任务列表 -->
<div id="task-list" class="task-list"></div>
<div style="display:flex;gap:8px;margin-top:8px;">
<button type="button" id="task-add-btn" class="btn btn-ghost" style="flex:1;" data-i18n="settings.taskAdd">+ 添加任务</button>
<button type="button" id="task-template-mgr-btn" class="btn btn-ghost" style="flex:1;" data-i18n="settings.taskTemplates">自定义类型</button>
</div>
<!-- 任务表单(添加/编辑) -->
<div id="task-form" class="task-form hidden">
<label class="model-form-label" data-i18n="settings.taskName">任务名
<input type="text" id="task-form-name" maxlength="32" placeholder="daily-report" />
</label>
<div class="model-form-row">
<label class="model-form-label" data-i18n="settings.taskType">类型
<select id="task-form-type"></select>
</label>
<label class="model-form-label" data-i18n="settings.taskProject">项目
<select id="task-form-project"></select>
</label>
</div>
<div class="model-form-row">
<label class="model-form-label" data-i18n="settings.taskInterval">间隔(秒,≥60)
<input type="number" id="task-form-interval" min="60" step="1" value="1800" />
</label>
<label class="model-form-vision" style="align-self:flex-end;margin-bottom:8px;">
<input type="checkbox" id="task-form-enabled" checked /> <span data-i18n="settings.taskEnabled">启用</span>
</label>
</div>
<label class="model-form-label" data-i18n="settings.taskRepo">仓库(选填 owner/repo)
<input type="text" id="task-form-repo" maxlength="80" placeholder="acme/repo" />
</label>
<div class="model-form-footer">
<div style="display:flex;gap:8px;margin-left:auto;">
<button type="button" id="task-form-cancel" class="btn btn-ghost" style="padding:6px 12px;min-height:30px;" data-i18n="settings.cancel">取消</button>
<button type="button" id="task-form-save" class="btn btn-primary" style="padding:6px 12px;min-height:30px;" data-i18n="settings.taskSave">保存任务</button>
</div>
</div>
</div>
<!-- 自定义类型表单 -->
<div id="task-template-form" class="task-form hidden">
<label class="model-form-label" data-i18n="settings.templateName">类型名
<input type="text" id="task-template-name" maxlength="32" placeholder="report" />
</label>
<label class="model-form-label" data-i18n="settings.templatePrompt">提示词模板
<textarea id="task-template-prompt" rows="6" style="width:100%;min-height:120px;" placeholder="# {{ instance_id }} ..."></textarea>
</label>
<div class="model-form-footer">
<div style="display:flex;gap:8px;margin-left:auto;">
<button type="button" id="task-template-cancel" class="btn btn-ghost" style="padding:6px 12px;min-height:30px;" data-i18n="settings.cancel">取消</button>
<button type="button" id="task-template-save" class="btn btn-primary" style="padding:6px 12px;min-height:30px;" data-i18n="settings.templateSave">保存类型</button>
</div>
</div>
</div>
<!-- 自定义类型列表 -->
<div id="task-template-list" class="task-list hidden" style="margin-top:8px;"></div>
</div>
<div class="settings-group">
<div class="settings-group-title" data-i18n="settings.aboutTitle">关于</div>
<div class="about-box">
Expand Down
1 change: 1 addition & 0 deletions emrg/gui/renderer/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1559,6 +1559,7 @@ const App = (() => {
Dialogs.initRenameDialog();
Dialogs.initGithubSection(); // Windows GCM rant Stage 2:设置页 GitHub 连接
Dialogs.initDeviceDialog(); // Windows GCM rant Stage 2b:device flow 对话框
Dialogs.initTaskManagement(); // rant 18:23:15 P3:定时任务/自定义类型管理
initGithubBanner(); // Windows GCM rant Stage 2:演化需 GitHub 但未认证时的连接横幅
initModelSwitcher();
initModeSwitcher(); // WorkBuddy P2:Ask/Auto 工作模式
Expand Down
Loading
Loading