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
This commit is contained in:
Nexus Dev 2026-04-01 21:55:14 +00:00
parent 7feacb121f
commit 78dbfbc26b
3 changed files with 67 additions and 0 deletions

View file

@ -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);
},
};

View file

@ -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 (
<ChatSpecCard
content={content}
messageId={id}
conversationId={conversationId}
onHandoff={onHandoff}
/>
);
}
if (messageType === "handoff") {
return <ChatHandoffIndicator content={content} />;
}
if (messageType === "task_created") {
try {
const data = JSON.parse(content) as { taskId?: string; taskTitle?: string; taskUrl?: string };
return <ChatTaskCreatedBadge taskId={data.taskId} taskTitle={data.taskTitle} taskUrl={data.taskUrl} />;
} catch {
return <ChatTaskCreatedBadge />;
}
}
if (messageType === "status_update") {
try {
const data = JSON.parse(content) as { agentName: string; taskId: string; taskTitle?: string; taskUrl?: string };
return <ChatStatusUpdateBadge agentName={data.agentName} taskId={data.taskId} taskTitle={data.taskTitle} taskUrl={data.taskUrl} />;
} catch {
return null;
}
}
// Fall through to default system message rendering (plain markdown)
}
if (role === "user") {
if (isEditing) {
return (

View file

@ -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<string, { name: string; icon: string | null; role: AgentRole | null }>;
}
@ -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}
/>
</div>
);