From f97229a29f5a6fd8703890c60b4c6032aaf1de8b Mon Sep 17 00:00:00 2001 From: argszero Date: Thu, 27 Aug 2026 09:01:25 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20gui=20renderer=20=E2=80=94=20task=20for?= =?UTF-8?q?m=20+=20rant=20dialog=20for=20workspace=20panels=20(Batch=205?= =?UTF-8?q?=20slice=208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Agent.md | 2 +- emrg/gui/main.js | 4 +- .../src/components/RantDialog.test.tsx | 67 ++++++ .../renderer/src/components/RantDialog.tsx | 95 +++++++++ .../renderer/src/components/Shell.test.tsx | 92 +++++++++ emrg/gui/renderer/src/components/Shell.tsx | 107 +++++++++- .../src/components/TaskFormDialog.test.tsx | 109 ++++++++++ .../src/components/TaskFormDialog.tsx | 194 ++++++++++++++++++ emrg/gui/renderer/src/lib/i18n-dicts.ts | 4 + emrg/gui/renderer/src/lib/i18n.test.ts | 6 +- emrg/gui/renderer/src/lib/workspaceView.ts | 2 +- 11 files changed, 674 insertions(+), 8 deletions(-) create mode 100644 emrg/gui/renderer/src/components/RantDialog.test.tsx create mode 100644 emrg/gui/renderer/src/components/RantDialog.tsx create mode 100644 emrg/gui/renderer/src/components/TaskFormDialog.test.tsx create mode 100644 emrg/gui/renderer/src/components/TaskFormDialog.tsx diff --git a/Agent.md b/Agent.md index 20824d7f..37342632 100644 --- a/Agent.md +++ b/Agent.md @@ -121,7 +121,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg Python: `uv run pytest tests/ -v` (1119) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (89: 45 daemon_client + 20 conn-manager + 8 integration + 6 build-config + 7 gui-state + 3 preload-api) — syntax: `node --check main.js preload.js daemon_client.js` -Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (426: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 11 markdown + 15 transcript + 7 TranscriptView + 15 history + 22 composer + 14 Composer + 12 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 29 workspaceView + 8 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 9 openSession + 6 WelcomeDialog + 8 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 12 daemonBridge + 6 DaemonBridgeProvider + 19 Shell + 15 DialogHost + 19 SettingsPanel + 4 vendorMarkdown) + `npm run build` → `renderer/dist/` +Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (441: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 11 markdown + 15 transcript + 7 TranscriptView + 15 history + 22 composer + 14 Composer + 12 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 29 workspaceView + 8 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 9 openSession + 6 WelcomeDialog + 8 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 12 daemonBridge + 6 DaemonBridgeProvider + 23 Shell + 15 DialogHost + 19 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown) + `npm run build` → `renderer/dist/` 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 路径不受影响) Git-over-https 兜底: `python scripts/sync-master-from-api.py [--repo owner/name] [--ref master]` — 受限网络下 github.com:443 不可达而 api.github.com 可达时,用 Git Data API 的 verification payload + signature 字节级重建上游 commit(含 web-flow GPG 签名 squash merge,reconstruct_commit 经 hermetic 测试验证 sha 一致)并推进本地 refs;内容对象缺失时 fail-loud 提示改用 git fetch(10+ 周期实证的恢复路径) diff --git a/emrg/gui/main.js b/emrg/gui/main.js index 0d59705a..362db329 100644 --- a/emrg/gui/main.js +++ b/emrg/gui/main.js @@ -712,13 +712,13 @@ vision = false // ── 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 || {}; + const { name, type, project, interval, enabled, repo, description, sandbox } = 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(), task_type: type.trim(), project: project.trim(), - interval, enabled, repo, description, + interval, enabled, repo, description, sandbox, }, 8000); if (!frame.ok && frame.error) throw new Error(frame.error); return frame; diff --git a/emrg/gui/renderer/src/components/RantDialog.test.tsx b/emrg/gui/renderer/src/components/RantDialog.test.tsx new file mode 100644 index 00000000..a0f6d4b3 --- /dev/null +++ b/emrg/gui/renderer/src/components/RantDialog.test.tsx @@ -0,0 +1,67 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { I18nProvider } from "../lib/i18n"; +import { RantDialog } from "./RantDialog"; +import type { ProjectRec } from "../lib/workspaceView"; + +/** + * RantDialog.test.tsx — Batch 5 slice 8:Rant 提交对话框。 + * 镜像 vanilla openRantForm/submitRantForm 语义:打开清空 + 聚焦、消息必填、 + * 项目下拉(已注册项目)、提交 payload {message, project}。 + */ + +function wrapper(node: React.ReactNode) { + return {node}; +} + +const PROJECTS: ProjectRec[] = [ + { name: "emrg", path: "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/p/emrg" }, + { name: "sci", path: "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/p/sci" }, +]; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("RantDialog", () => { + it("open:渲染消息文本域 + 项目下拉(已注册项目)", () => { + render(wrapper()); + expect(screen.getByTestId("rant-dialog")).toBeInTheDocument(); + expect((screen.getByTestId("rant-message") as HTMLTextAreaElement).value).toBe(""); + const sel = screen.getByTestId("rant-project") as HTMLSelectElement; + expect(sel.options.length).toBe(PROJECTS.length + 1); // + 空选项 + expect(sel.options[1].value).toBe("emrg"); + }); + + it("空消息 → 内联错误,不提交", () => { + const onSubmit = vi.fn(); + render(wrapper()); + fireEvent.click(screen.getByTestId("rant-submit")); + expect(screen.getByTestId("rant-error")).toHaveTextContent("required"); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it("提交 payload {message, project}(选项目)", async () => { + const onSubmit = vi.fn().mockResolvedValue(undefined); + render(wrapper()); + fireEvent.change(screen.getByTestId("rant-message"), { target: { value: "侧栏太宽了" } }); + fireEvent.change(screen.getByTestId("rant-project"), { target: { value: "emrg" } }); + fireEvent.click(screen.getByTestId("rant-submit")); + await vi.waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); + expect(onSubmit.mock.calls[0][0]).toEqual({ message: "侧栏太宽了", project: "emrg" }); + }); + + it("项目选填:留空提交 project=''", async () => { + const onSubmit = vi.fn().mockResolvedValue(undefined); + render(wrapper()); + fireEvent.change(screen.getByTestId("rant-message"), { target: { value: "hello" } }); + fireEvent.click(screen.getByTestId("rant-submit")); + await vi.waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); + expect(onSubmit.mock.calls[0][0]).toEqual({ message: "hello", project: "" }); + }); + + it("closed (open=false) → 不渲染", () => { + render(wrapper()); + expect(screen.queryByTestId("rant-dialog")).not.toBeInTheDocument(); + }); +}); diff --git a/emrg/gui/renderer/src/components/RantDialog.tsx b/emrg/gui/renderer/src/components/RantDialog.tsx new file mode 100644 index 00000000..425bad64 --- /dev/null +++ b/emrg/gui/renderer/src/components/RantDialog.tsx @@ -0,0 +1,95 @@ +import { useEffect, useRef, useState } from "react"; +import { useI18n } from "../lib/i18n"; +import type { ProjectRec } from "../lib/workspaceView"; +import { Dialog } from "./Dialog"; + +/** + * RantDialog — Rant 提交对话框(Batch 5 slice 8:工作区 Rant 面板"新建 Rant")。 + * 镜像 vanilla `openRantForm()` / `submitRantForm()`(renderer/js/dialogs.js): + * + * - 打开即清空消息、聚焦输入; + * - 项目下拉 = 已注册项目(选填,value=name||path,与 daemon rant 协议 project 字段一致); + * - 消息必填(空 → 内联错误;vanilla 用 showConfirm 提示); + * - 提交 → onSubmit({message, project})(调用方接线 sendRant + 刷新列表 + 反馈)。 + */ +export interface RantDialogProps { + open: boolean; + /** 项目下拉选项(仅已注册项目;选填) */ + projects?: ProjectRec[]; + onSubmit?: (payload: { message: string; project: string }) => void | Promise; + onDismiss?: () => void; +} + +export function RantDialog({ open, projects = [], onSubmit, onDismiss }: RantDialogProps) { + const { t } = useI18n(); + const [err, setErr] = useState(null); + const msgRef = useRef(null); + const projectRef = useRef(null); + + useEffect(() => { + if (!open) return; + setErr(null); + if (msgRef.current) msgRef.current.value = ""; + if (projectRef.current) projectRef.current.value = ""; + msgRef.current?.focus(); + }, [open]); + + const submit = async () => { + const msg = msgRef.current?.value.trim() ?? ""; + if (!msg) { + setErr(t("rants.messageRequired")); + return; + } + try { + await onSubmit?.({ message: msg, project: projectRef.current?.value ?? "" }); + } finally { + onDismiss?.(); + } + }; + + if (!open) return null; + return ( + + + + + } + > +
+ {err && ( +
+ {err} +
+ )} +