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}
/>
);