From 71839e0032e3b3bdb3f648ce9cffec07d07962cf Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Wed, 1 Apr 2026 21:55:14 +0000 Subject: [PATCH] feat(23-03): add messageType dispatch, ChatMessageList propagation, and chatApi handoff methods - ChatMessage: add messageType/conversationId/onHandoff props; dispatch to ChatSpecCard, ChatHandoffIndicator, ChatTaskCreatedBadge, ChatStatusUpdateBadge based on messageType - ChatMessageList: propagate messageType and conversationId to ChatMessage; add onHandoff prop - chatApi: add handoffSpec() and postStatusUpdate() methods --- ui/src/api/chat.ts | 18 +++++++++++ ui/src/components/ChatMessage.tsx | 44 +++++++++++++++++++++++++++ ui/src/components/ChatMessageList.tsx | 5 +++ 3 files changed, 67 insertions(+) diff --git a/ui/src/api/chat.ts b/ui/src/api/chat.ts index 24b195ef..70883c07 100644 --- a/ui/src/api/chat.ts +++ b/ui/src/api/chat.ts @@ -148,4 +148,22 @@ export const chatApi = { credentials: "include", }); }, + + handoffSpec( + conversationId: string, + spec: { what: string; why: string; constraints: string; success: string }, + targetRole: string = "pm", + ) { + return api.post<{ handoffMessageId: string; issues: Array<{ id: string; identifier: string; title: string }> }>( + `/conversations/${conversationId}/handoff`, + { spec, targetRole }, + ); + }, + + postStatusUpdate( + conversationId: string, + data: { agentName: string; taskId: string; taskTitle?: string; taskUrl?: string }, + ) { + return api.post<{ id: string }>(`/conversations/${conversationId}/status-update`, data); + }, }; diff --git a/ui/src/components/ChatMessage.tsx b/ui/src/components/ChatMessage.tsx index 8e21654d..ed9f6ba4 100644 --- a/ui/src/components/ChatMessage.tsx +++ b/ui/src/components/ChatMessage.tsx @@ -3,6 +3,10 @@ import { ChatMarkdownMessage } from "./ChatMarkdownMessage"; import { ChatMessageIdentityBar } from "./ChatMessageIdentityBar"; import { ChatStreamingCursor } from "./ChatStreamingCursor"; import { ChatMessageActions } from "./ChatMessageActions"; +import { ChatSpecCard } from "./ChatSpecCard"; +import { ChatHandoffIndicator } from "./ChatHandoffIndicator"; +import { ChatTaskCreatedBadge } from "./ChatTaskCreatedBadge"; +import { ChatStatusUpdateBadge } from "./ChatStatusUpdateBadge"; import { Button } from "@/components/ui/button"; import { cn } from "../lib/utils"; import type { AgentRole } from "@paperclipai/shared"; @@ -11,6 +15,8 @@ interface ChatMessageProps { id?: string; role: "user" | "assistant" | "system"; content: string; + messageType?: string | null; + conversationId?: string; agentName?: string | null; agentIcon?: string | null; agentRole?: AgentRole | null; @@ -19,12 +25,15 @@ interface ChatMessageProps { isAnyStreaming?: boolean; onEdit?: (messageId: string, newContent: string) => void; onRetry?: (messageId: string) => void; + onHandoff?: (spec: { what: string; why: string; constraints: string; success: string }) => void; } export function ChatMessage({ id, role, content, + messageType, + conversationId, agentName, agentIcon, agentRole, @@ -33,10 +42,45 @@ export function ChatMessage({ isAnyStreaming, onEdit, onRetry, + onHandoff, }: ChatMessageProps) { const [isEditing, setIsEditing] = useState(false); const [editValue, setEditValue] = useState(content); + // Dispatch to specialized system message components (Phase 23) + if (role === "system" || messageType) { + if (messageType === "spec_card") { + return ( + + ); + } + if (messageType === "handoff") { + return ; + } + if (messageType === "task_created") { + try { + const data = JSON.parse(content) as { taskId?: string; taskTitle?: string; taskUrl?: string }; + return ; + } catch { + return ; + } + } + if (messageType === "status_update") { + try { + const data = JSON.parse(content) as { agentName: string; taskId: string; taskTitle?: string; taskUrl?: string }; + return ; + } catch { + return null; + } + } + // Fall through to default system message rendering (plain markdown) + } + if (role === "user") { if (isEditing) { return ( diff --git a/ui/src/components/ChatMessageList.tsx b/ui/src/components/ChatMessageList.tsx index 086091e9..13500295 100644 --- a/ui/src/components/ChatMessageList.tsx +++ b/ui/src/components/ChatMessageList.tsx @@ -16,6 +16,7 @@ interface ChatMessageListProps { streamingAgentRole?: AgentRole | null; onEdit?: (messageId: string, newContent: string) => void; onRetry?: (messageId: string) => void; + onHandoff?: (spec: { what: string; why: string; constraints: string; success: string }) => void; agentMap?: Map; } @@ -28,6 +29,7 @@ export function ChatMessageList({ streamingAgentRole, onEdit, onRetry, + onHandoff, agentMap, }: ChatMessageListProps) { const { messages, isLoading } = useChatMessages(conversationId); @@ -143,6 +145,8 @@ export function ChatMessageList({ id={msg.id} role={msg.role as "user" | "assistant" | "system"} content={msg.content} + messageType={msg.messageType} + conversationId={conversationId} agentName={agent?.name ?? streamingAgentName} agentIcon={agent?.icon ?? streamingAgentIcon} agentRole={agent?.role ?? streamingAgentRole} @@ -151,6 +155,7 @@ export function ChatMessageList({ isAnyStreaming={isStreaming} onEdit={onEdit} onRetry={onRetry} + onHandoff={onHandoff} /> );