--- phase: 22-agent-streaming plan: "03" type: execute wave: 2 depends_on: ["22-01", "22-02"] files_modified: - ui/src/components/ChatMessageActions.tsx - ui/src/components/ChatMessage.tsx - ui/src/components/ChatStopButton.tsx - ui/src/components/ChatMessage.test.tsx autonomous: true requirements: - CHAT-10 - CHAT-11 - CHAT-12 must_haves: truths: - "User can click edit pencil on a user message to enter inline edit mode" - "User can click retry on an assistant message to regenerate the response" - "Stop button appears during streaming and cancels generation on click" - "Edit/retry buttons are hidden while a stream is active" artifacts: - path: "ui/src/components/ChatMessageActions.tsx" provides: "Edit and Retry hover action buttons" exports: ["ChatMessageActions"] - path: "ui/src/components/ChatStopButton.tsx" provides: "Stop generating button" exports: ["ChatStopButton"] - path: "ui/src/components/ChatMessage.tsx" provides: "Extended ChatMessage with edit mode, retry, actions" contains: "ChatMessageActions" key_links: - from: "ui/src/components/ChatMessage.tsx" to: "ui/src/components/ChatMessageActions.tsx" via: "import ChatMessageActions" pattern: "ChatMessageActions" - from: "ui/src/components/ChatMessageActions.tsx" to: "parent callbacks" via: "onEdit, onRetry props" pattern: "onEdit|onRetry" --- Message action controls: edit button on user messages (CHAT-10), retry button on assistant messages (CHAT-11), stop generation button (CHAT-12), and inline edit mode for user messages. These are the interactive controls that work with the streaming infrastructure from Plan 01. Purpose: Give users full control over message lifecycle — edit, retry, stop. Output: ChatMessageActions, ChatStopButton components; ChatMessage with inline edit mode. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/22-agent-streaming/22-RESEARCH.md @.planning/phases/22-agent-streaming/22-UI-SPEC.md @.planning/phases/22-agent-streaming/22-01-SUMMARY.md @.planning/phases/22-agent-streaming/22-02-SUMMARY.md From ui/src/components/ChatMessage.tsx (after Plan 02): ```typescript interface ChatMessageProps { role: "user" | "assistant" | "system"; content: string; agentName?: string | null; agentIcon?: string | null; agentRole?: AgentRole | null; timestamp?: string; isStreaming?: boolean; } ``` From ui/src/hooks/useStreamingChat.ts (Plan 01): ```typescript export function useStreamingChat(conversationId: string | null): { streamingContent: string; isStreaming: boolean; startStream: (userMessage: string, agentId?: string) => void; stop: () => void; }; ``` From ui/src/api/chat.ts (Plan 01 additions): ```typescript // chatApi methods available: chatApi.postMessage(conversationId, data) chatApi.updateConversation(conversationId, data) // Plan 01 additions: chatApi.postMessageAndStream(conversationId, data, callbacks, signal) chatApi.savePartialMessage(conversationId, data) ``` From server/src/routes/chat.ts (Plan 01): ``` PATCH /conversations/:id/messages/:msgId — edit message content DELETE /conversations/:id/messages/after/:msgId — truncate messages after POST /conversations/:id/stream — SSE streaming ``` Task 1: ChatStopButton and ChatMessageActions components - ui/src/components/ChatMessage.tsx - .planning/phases/22-agent-streaming/22-UI-SPEC.md (lines 96-120 stop/edit/retry) ui/src/components/ChatStopButton.tsx, ui/src/components/ChatMessageActions.tsx **1. Create `ui/src/components/ChatStopButton.tsx`:** ```typescript import { Square } from "lucide-react"; import { Button } from "@/components/ui/button"; interface ChatStopButtonProps { onStop: () => void; } export function ChatStopButton({ onStop }: ChatStopButtonProps) { return ( Stop generating ); } ``` Per UI spec: centered, `variant="outline" size="sm"`, `Square` icon (filled via `fill-current`), label "Stop generating", `aria-label="Stop generating response"`. Container has `border-t border-border`. **2. Create `ui/src/components/ChatMessageActions.tsx`:** ```typescript import { Pencil, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; interface ChatMessageActionsProps { role: "user" | "assistant" | "system"; isStreaming?: boolean; onEdit?: () => void; onRetry?: () => void; } export function ChatMessageActions({ role, isStreaming, onEdit, onRetry }: ChatMessageActionsProps) { if (isStreaming) return null; if (role === "user" && onEdit) { return ( Edit message ); } if (role === "assistant" && onRetry) { return ( Retry response ); } return null; } ``` Per UI spec: edit Pencil at top-right of user bubble (absolute positioned, group-hover visible), retry RefreshCw below assistant message (right-aligned, group-hover visible). Both hidden during streaming (`isStreaming` check). Both 14x14px icons (`h-3.5 w-3.5`), `variant="ghost" size="icon"`, with tooltip and aria-label. pnpm --filter @paperclipai/ui exec -- tsc --noEmit 2>&1 | head -20 - grep -q "Stop generating" ui/src/components/ChatStopButton.tsx - grep -q "aria-label" ui/src/components/ChatStopButton.tsx - grep -q "ChatMessageActions" ui/src/components/ChatMessageActions.tsx - grep -q "Edit message" ui/src/components/ChatMessageActions.tsx - grep -q "Retry response" ui/src/components/ChatMessageActions.tsx - grep -q "group-hover" ui/src/components/ChatMessageActions.tsx - grep -q "isStreaming" ui/src/components/ChatMessageActions.tsx - ChatStopButton renders centered outline button with Square icon and "Stop generating" label - ChatMessageActions renders edit Pencil for user messages (absolute, group-hover) - ChatMessageActions renders retry RefreshCw for assistant messages (right-aligned, group-hover) - Both action buttons hidden when isStreaming is true - All have proper aria-labels and tooltips - TypeScript compiles clean Task 2: Extend ChatMessage with inline edit mode and wire action callbacks - ui/src/components/ChatMessage.tsx - ui/src/components/ChatMessageActions.tsx - ui/src/components/ChatStopButton.tsx - ui/src/api/chat.ts - .planning/phases/22-agent-streaming/22-UI-SPEC.md (lines 110-142 edit/retry interaction) ui/src/components/ChatMessage.tsx, ui/src/components/ChatMessage.test.tsx **1. Extend `ChatMessageProps` in `ui/src/components/ChatMessage.tsx`:** Add to the existing interface: ```typescript interface ChatMessageProps { id?: string; role: "user" | "assistant" | "system"; content: string; agentName?: string | null; agentIcon?: string | null; agentRole?: AgentRole | null; timestamp?: string; isStreaming?: boolean; isAnyStreaming?: boolean; // true when ANY message is streaming (disables edit/retry globally) onEdit?: (messageId: string, newContent: string) => void; onRetry?: (messageId: string) => void; } ``` **2. Add inline edit mode to user message branch:** ```typescript import { useState } from "react"; import { ChatMessageActions } from "./ChatMessageActions"; import { Button } from "@/components/ui/button"; // Inside ChatMessage component: const [isEditing, setIsEditing] = useState(false); const [editValue, setEditValue] = useState(content); // User message branch: if (role === "user") { if (isEditing) { return ( setEditValue(e.target.value)} className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm resize-none min-h-[40px] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" aria-label="Edit your message" rows={3} /> { setIsEditing(false); setEditValue(content); }} > Discard edit { if (id && onEdit && editValue.trim()) { onEdit(id, editValue.trim()); setIsEditing(false); } }} > Save edit ); } return ( {content} setIsEditing(true)} /> ); } ``` Per UI spec: inline textarea pre-filled with content, "Save edit" (variant="default" size="sm", disabled when empty), "Discard edit" (variant="ghost" size="sm"), `aria-label="Edit your message"`. **3. Update assistant message branch to include retry action:** ```tsx return ( {agentName && ( )} {isStreaming && } onRetry(id) : undefined} /> ); ``` **4. Replace test stubs in `ui/src/components/ChatMessage.test.tsx`:** ```typescript // @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 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"); }); ``` pnpm --filter @paperclipai/ui exec -- tsc --noEmit 2>&1 | head -20 - grep -q "isEditing" ui/src/components/ChatMessage.tsx - grep -q "Save edit" ui/src/components/ChatMessage.tsx - grep -q "Discard edit" ui/src/components/ChatMessage.tsx - grep -q "Edit your message" ui/src/components/ChatMessage.tsx - grep -q "onEdit" ui/src/components/ChatMessage.tsx - grep -q "onRetry" ui/src/components/ChatMessage.tsx - grep -q "isAnyStreaming" ui/src/components/ChatMessage.tsx - grep -q "ChatMessageActions" ui/src/components/ChatMessage.tsx - ChatMessage user messages show edit pencil on hover (group-hover) - Edit pencil click opens inline textarea with "Save edit" / "Discard edit" buttons - Save edit is disabled when textarea empty; calls onEdit(id, newContent) on click - Discard edit reverts to read-only bubble - Assistant messages show retry RefreshCw on hover; calls onRetry(id) on click - All edit/retry actions hidden when isAnyStreaming is true - TypeScript compiles clean - `pnpm --filter @paperclipai/ui exec -- tsc --noEmit` passes - `pnpm --filter @paperclipai/ui vitest run --reporter=verbose` passes - User messages have edit capability with inline textarea (CHAT-10) - Assistant messages have retry button (CHAT-11) - Stop button component ready for ChatPanel integration (CHAT-12) - All actions disabled during active streaming After completion, create `.planning/phases/22-agent-streaming/22-03-SUMMARY.md`