From be431c949277a3fc3f7cde14438fa26df5d16bfb Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Wed, 1 Apr 2026 18:24:56 +0000 Subject: [PATCH] feat(22-03): extend ChatMessage with inline edit mode and action callbacks - Add id, isAnyStreaming, onEdit, onRetry props to ChatMessageProps - User messages show edit Pencil on hover via ChatMessageActions - Edit pencil opens inline textarea with Save/Discard buttons - Save edit calls onEdit(id, newContent), disabled when textarea empty - Discard edit reverts to read-only bubble - Assistant messages show retry RefreshCw via ChatMessageActions - All edit/retry actions disabled when isAnyStreaming is true - Update test stubs to reflect new prop surface --- ui/src/components/ChatMessage.test.tsx | 12 ++++- ui/src/components/ChatMessage.tsx | 68 +++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/ui/src/components/ChatMessage.test.tsx b/ui/src/components/ChatMessage.test.tsx index 2251af72..df015897 100644 --- a/ui/src/components/ChatMessage.test.tsx +++ b/ui/src/components/ChatMessage.test.tsx @@ -1,12 +1,20 @@ -import { describe, it } from "vitest"; +// @vitest-environment jsdom +import { describe, it, expect } from "vitest"; describe("ChatMessage", () => { + it("exports ChatMessage component", async () => { + const mod = await import("./ChatMessage"); + expect(mod.ChatMessage).toBeDefined(); + }); + it.todo("renders user message as right-aligned bubble with plain text"); it.todo("renders assistant message with ChatMarkdownMessage"); it.todo("renders ChatMessageIdentityBar for assistant messages when agentName is provided"); it.todo("shows edit pencil on hover for user messages"); it.todo("shows retry button on hover for assistant messages"); - it.todo("hides retry button when isStreaming is true"); + it.todo("hides retry button when isAnyStreaming is true"); it.todo("switches to inline edit textarea on pencil click"); it.todo("renders ChatStreamingCursor when isStreaming is true"); + it.todo("Save edit button disabled when edit textarea is empty"); + it.todo("Discard edit reverts to read-only bubble"); }); diff --git a/ui/src/components/ChatMessage.tsx b/ui/src/components/ChatMessage.tsx index ac5d02e2..8e21654d 100644 --- a/ui/src/components/ChatMessage.tsx +++ b/ui/src/components/ChatMessage.tsx @@ -1,10 +1,14 @@ +import { useState } from "react"; import { ChatMarkdownMessage } from "./ChatMarkdownMessage"; import { ChatMessageIdentityBar } from "./ChatMessageIdentityBar"; import { ChatStreamingCursor } from "./ChatStreamingCursor"; +import { ChatMessageActions } from "./ChatMessageActions"; +import { Button } from "@/components/ui/button"; import { cn } from "../lib/utils"; import type { AgentRole } from "@paperclipai/shared"; interface ChatMessageProps { + id?: string; role: "user" | "assistant" | "system"; content: string; agentName?: string | null; @@ -12,9 +16,13 @@ interface ChatMessageProps { agentRole?: AgentRole | null; timestamp?: string; isStreaming?: boolean; + isAnyStreaming?: boolean; + onEdit?: (messageId: string, newContent: string) => void; + onRetry?: (messageId: string) => void; } export function ChatMessage({ + id, role, content, agentName, @@ -22,20 +30,73 @@ export function ChatMessage({ agentRole, timestamp, isStreaming, + isAnyStreaming, + onEdit, + onRetry, }: ChatMessageProps) { + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(content); + if (role === "user") { + if (isEditing) { + return ( +
+
+