Q146: the message editor stays open until the edit is saved, and says why it was not

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 00:35:28 -04:00
1 parent 8a06f2ff77
commit 02309546dc
6 files changed
+40 -9

No files matched your search

+1
View File
@@ -37,6 +37,7 @@ export const zh: Record<string, string> = {
"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": "在话题中回复",
+3 -2
View File
@@ -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({
@@ -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<void>;
onDeleteMessage: (messageId: string) => void;
onOpenFile: (fileId: string) => void;
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
+21 -5
View File
@@ -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<void>;
onDelete?: () => void;
onOpenFile?: (fileId: string) => void;
}) {
const [editing, setEditing] = useState(false);
const [saving, setSaving] = useState(false);
const [editError, setEditError] = useState<string | null>(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 (
<div className="chat-message" style={{ display: "flex", gap: 12, marginBottom: 18 }}>
@@ -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 && <div className="form-error" role="alert" style={{ marginBottom: 6 }}>{t("Not saved: {reason}", { reason: editError })}</div>}
<div className="row" style={{ gap: 6 }}>
<Button tone="primary" size="small" type="submit">{t("Save")}</Button>
<Button tone="ghost" size="small" onClick={() => setEditing(false)}>{t("Cancel")}</Button>
<Button tone="primary" size="small" type="submit" disabled={saving}>{t("Save")}</Button>
<Button tone="ghost" size="small" disabled={saving} onClick={() => { setEditing(false); setEditError(null); }}>{t("Cancel")}</Button>
</div>
</form>
) : (
+1 -1
View File
@@ -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<void>;
onDeleteMessage: (messageId: string) => void;
onOpenFile: (fileId: string) => void;
}) {
+13
View File
@@ -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();