import { useState } from "react";
import type { ChatMessage } from "../../types/message";
import { useAvatars } from "../../context/AvatarContext";
import {
downloadTextFile,
messageMarkdownFilename,
singleAssistantMarkdown,
} from "../../utils/exportConversation";
import { renderMarkdown } from "../../utils/markdown";
import { imageToDataUrl } from "../../utils/images";
import { CompactionSummaryBlock } from "./CompactionSummaryBlock";
import { ToolCallBlock } from "./ToolCallBlock";
import { ThinkingBlock } from "./ThinkingBlock";
import { BashOutputBlock } from "./BashOutputBlock";
import styles from "./MessageList.module.css";
interface MessageBubbleProps {
message: ChatMessage;
agentGutter?: "avatar" | "spacer" | "none";
toolsExpanded?: boolean;
}
function ChatAvatar({ src, className }: { src: string; className: string }) {
const [failed, setFailed] = useState(false);
if (!src || failed) return null;
return (
setFailed(true)}
/>
);
}
function AgentSideGutter({ mode, avatarUrl }: { mode: "avatar" | "spacer"; avatarUrl: string }) {
if (mode === "spacer") {
return
;
}
return (
);
}
function AssistantActions({ content }: { content: string }) {
const [copied, setCopied] = useState(false);
const markdown = singleAssistantMarkdown(content);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(markdown);
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
} catch {
/* ignore */
}
};
const handleDownload = () => {
downloadTextFile(messageMarkdownFilename(content), markdown, "text/markdown");
};
return (
);
}
export function MessageBubble({
message,
agentGutter = "none",
toolsExpanded = false,
}: MessageBubbleProps) {
const { userAvatarUrl, agentAvatarUrl } = useAvatars();
const gutter =
agentGutter === "avatar" || agentGutter === "spacer" ? (
) : null;
if (message.role === "thinking") {
return (
);
}
if (message.role === "tool_call") {
return (
);
}
if (message.role === "bash") {
return (
);
}
if (message.role === "compaction_summary") {
return (
);
}
if (message.role === "branch_summary") {
return (
);
}
const classNames = [styles.msg];
if (message.role === "user") classNames.push(styles.user);
if (message.role === "assistant") classNames.push(styles.assistant);
if (message.role === "system") classNames.push(styles.system);
if (message.streaming) classNames.push(styles.streaming);
if (message.role === "assistant") {
const showActions = !message.streaming && message.content.trim().length > 0;
return (
);
}
if (message.role === "user") {
return (
{message.content ?
{message.content}
: null}
{message.images?.length ? (
{message.images.map((img, index) => (
})
))}
) : null}
);
}
return {message.content}
;
}