From 02309546dc387d3f03298cc70fff66237a190e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 00:35:28 -0400 Subject: [PATCH] Q146: the message editor stays open until the edit is saved, and says why it was not Co-Authored-By: Claude Opus 5.5 --- src/renderer/src/i18n/zh.ts | 1 + src/renderer/src/pages/ChatPage.tsx | 5 ++-- .../src/pages/chat/ChatConversation.tsx | 2 +- .../src/pages/chat/ChatMessageRow.tsx | 26 +++++++++++++++---- src/renderer/src/pages/chat/ChatTimeline.tsx | 2 +- tests/e2e/ceo-chat.spec.ts | 13 ++++++++++ 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index 1ba4821d..0c83bd27 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -37,6 +37,7 @@ export const zh: Record = { "Waiting on you, or resumes on its own at {time}.": "等待你处理,或于 {time} 自动恢复。", "Waiting on you.": "等待你处理。", "Not sent: {reason}": "未发送:{reason}", + "Not saved: {reason}": "未保存:{reason}", "Remove {name}": "移除 {name}", "Not attached: {reason}": "未能附加:{reason}", "Reply in thread": "在话题中回复", diff --git a/src/renderer/src/pages/ChatPage.tsx b/src/renderer/src/pages/ChatPage.tsx index 494753f5..5525ed01 100644 --- a/src/renderer/src/pages/ChatPage.tsx +++ b/src/renderer/src/pages/ChatPage.tsx @@ -180,14 +180,15 @@ export function ChatPage({ snapshot, actions }: PageProps) { decidedByEmployeeId: employee.id, })); - const editChatMessage = (messageId: string, bodyMd: string) => - void actions.run(() => window.agentCompany.editMessage({ + const editChatMessage = async (messageId: string, bodyMd: string) => { + await actions.run(() => window.agentCompany.editMessage({ orgId, messageId, actorType: "employee", actorId: employee.id, bodyMd, })); + }; const deleteChatMessage = (messageId: string) => void actions.run(() => window.agentCompany.deleteMessage({ diff --git a/src/renderer/src/pages/chat/ChatConversation.tsx b/src/renderer/src/pages/chat/ChatConversation.tsx index 8d669809..b4714ad6 100644 --- a/src/renderer/src/pages/chat/ChatConversation.tsx +++ b/src/renderer/src/pages/chat/ChatConversation.tsx @@ -69,7 +69,7 @@ export function ChatConversation({ onAttach: () => void; onApprove: (approvalId: string) => void; onDecline: (approvalId: string) => void; - onEditMessage: (messageId: string, bodyMd: string) => void; + onEditMessage: (messageId: string, bodyMd: string) => Promise; onDeleteMessage: (messageId: string) => void; onOpenFile: (fileId: string) => void; onSubmit: (event: FormEvent) => void; diff --git a/src/renderer/src/pages/chat/ChatMessageRow.tsx b/src/renderer/src/pages/chat/ChatMessageRow.tsx index 034e2698..a735732e 100644 --- a/src/renderer/src/pages/chat/ChatMessageRow.tsx +++ b/src/renderer/src/pages/chat/ChatMessageRow.tsx @@ -41,11 +41,14 @@ export function ChatMessageRow({ /** Where each agent's reply to this message stands; a finished reply shows nothing. */ replyStatuses?: ChatReplyStatus[]; onOpenThread?: () => void; - onEdit?: (bodyMd: string) => void; + /** Saves an edit; the editor stays open until it is saved. */ + onEdit?: (bodyMd: string) => Promise; onDelete?: () => void; onOpenFile?: (fileId: string) => void; }) { const [editing, setEditing] = useState(false); + const [saving, setSaving] = useState(false); + const [editError, setEditError] = useState(null); const name = senderName(message, context); const bot = message.senderType === "agent"; const deleted = Boolean(message.deletedAt); @@ -57,8 +60,19 @@ export function ChatMessageRow({ event.preventDefault(); const next = new FormData(event.currentTarget).get("body"); const value = typeof next === "string" ? next.trim() : ""; - if (value && onEdit) onEdit(value); - setEditing(false); + if (!value || !onEdit || saving) return; + setSaving(true); + setEditError(null); + // The editor closes only once the edit is saved; a refusal keeps the text and says why. + onEdit(value).then(() => { + setSaving(false); + setEditing(false); + }).catch((reason: unknown) => { + const message = reason instanceof Error ? reason.message : String(reason); + console.error(`[chat] edit failed: ${message}`); + setSaving(false); + setEditError(message); + }); }; return (
@@ -102,12 +116,14 @@ export function ChatMessageRow({ defaultValue={message.bodyMd} autoFocus aria-label={t("Edit message")} + disabled={saving} className="input" style={{ minHeight: 60, marginBottom: 6 }} /> + {editError &&
{t("Not saved: {reason}", { reason: editError })}
}
- - + +
) : ( diff --git a/src/renderer/src/pages/chat/ChatTimeline.tsx b/src/renderer/src/pages/chat/ChatTimeline.tsx index 6b3fbe59..40b9963c 100644 --- a/src/renderer/src/pages/chat/ChatTimeline.tsx +++ b/src/renderer/src/pages/chat/ChatTimeline.tsx @@ -68,7 +68,7 @@ export function ChatTimeline({ onOpenThread: (threadId: string) => void; onApprove: (approvalId: string) => void; onDecline: (approvalId: string) => void; - onEditMessage: (messageId: string, bodyMd: string) => void; + onEditMessage: (messageId: string, bodyMd: string) => Promise; onDeleteMessage: (messageId: string) => void; onOpenFile: (fileId: string) => void; }) { diff --git a/tests/e2e/ceo-chat.spec.ts b/tests/e2e/ceo-chat.spec.ts index 456ef585..0e8a4210 100644 --- a/tests/e2e/ceo-chat.spec.ts +++ b/tests/e2e/ceo-chat.spec.ts @@ -111,6 +111,19 @@ test("the pinned CEO conversation is the Messages front door", async () => { await expect(composer).toHaveValue(""); await expect(page.getByRole("alert")).toHaveCount(0); + // An edit the backend refuses keeps the editor open with the text and says why. + const own = chatLog.locator(".chat-message", { hasText: "Second thought: what is blocked?" }); + await own.getByRole("button", { name: "Edit message" }).click(); + const editor = own.getByRole("textbox", { name: "Edit message" }); + await editor.fill("/status everything"); + await own.getByRole("button", { name: "Save" }).click(); + await expect(own.getByRole("alert")).toContainText("Not saved: Slash command messages are immutable."); + await expect(editor).toHaveValue("/status everything"); + await editor.fill("Second thought: what is blocked right now?"); + await own.getByRole("button", { name: "Save" }).click(); + await expect(chatLog.getByText("Second thought: what is blocked right now?")).toBeVisible(); + await expect(chatLog.getByRole("textbox", { name: "Edit message" })).toHaveCount(0); + // Each conversation keeps its own unsent draft. await composer.fill("Draft for the CEO only"); await page.locator(".convo-item", { hasText: "general" }).click();