import { memo, type ReactElement } from 'react';
import type {
ACPToolCall,
Message,
PermissionRequest,
TodoItem,
} from '../adapters/types';
import type { WebShellAssistantTurnFooterRenderInfo } from '../customization';
import { useI18n } from '../i18n';
import { ErrorBoundary } from './ErrorBoundary';
import { MessageTimestamp } from './MessageTimestamp';
import { UserMessage } from './messages/UserMessage';
import {
AssistantMessage,
ThinkingMessage,
type SessionContentGenerator,
} from './messages/AssistantMessage';
import { SystemMessage } from './messages/SystemMessage';
import { ToolGroup } from './messages/ToolGroup';
import { PlanMessage } from './messages/PlanMessage';
import { BtwMessage } from './messages/BtwMessage';
import { UserShellMessage } from './messages/UserShellMessage';
import { InsightProgress } from './InsightProgress';
import { InsightReady } from './InsightReady';
interface MessageItemProps {
message: Message;
pendingApproval?: PermissionRequest | null;
/** Run /context detail, exactly like typing it (context-usage panels). */
onShowContextDetail?: () => void;
workspaceCwd?: string;
isLatest?: boolean;
showRetryHint?: boolean;
onRetryClick?: () => void;
onBranchSession?: () => void;
showAssistantActions?: boolean;
showAssistantBranch?: boolean;
isLocateFlashing?: boolean;
assistantTurnFooterInfo?: WebShellAssistantTurnFooterRenderInfo;
generateContent?: SessionContentGenerator;
}
export const MessageItem = memo(function MessageItem({
message,
pendingApproval,
onShowContextDetail,
workspaceCwd,
isLatest = false,
showRetryHint = false,
onRetryClick,
onBranchSession,
showAssistantActions = false,
showAssistantBranch = false,
isLocateFlashing = false,
assistantTurnFooterInfo,
generateContent,
}: MessageItemProps) {
const { t } = useI18n();
const body = ((): ReactElement | null => {
switch (message.role) {
case 'user':
return (
);
case 'assistant':
return (
);
case 'thinking':
return (
);
case 'tool_group':
return (
);
case 'plan':
return (
);
case 'system':
return (
);
case 'user_shell':
return (
);
case 'btw':
return (
);
case 'insight_progress':
return (
);
case 'insight_ready':
return ;
case 'insight_error':
return (
{message.error}
);
default:
return null;
}
})();
if (body === null) return null;
// Isolate each message's render: a throw in Markdown/KaTeX/Mermaid/a tool
// panel degrades to an inline notice rather than white-screening the whole
// (embeddable) transcript. `resetKeys={[message]}` lets a streamed/edited/
// retried update recover on its own; a stable broken message stays on the
// fallback without looping.
const safeBody = (
}
>
{body}
);
// Re-enable text selection on every message row so users can long-press /
// drag-select reply text. The blanket `html * { user-select: none }` in
// standalone.css disables selection on UI chrome (native-app feel); this
// attribute opts the message subtree back in, including descendants
// (Markdown body, code blocks, tool panels, sub-messages).
//
// `display: contents` keeps this wrapper out of layout: several parents
// (e.g. MessageTimestamp's chat row) are flex containers whose items used
// to be the message body itself. A plain div here becomes the flex item
// instead and shrinks to its content width, squeezing user chat bubbles
// (whose max-width: 80% then resolves against the shrunken wrapper) so
// even short messages wrap mid-word. The user-select re-enable rule
// matches `[data-user-selectable] *`, so the boxless wrapper does not
// affect it.
const selectableSafeBody = (
{safeBody}
);
if (message.role === 'assistant') {
if (showAssistantActions) {
return selectableSafeBody;
}
return (
{selectableSafeBody}
);
}
// The cancellation marker is a right-aligned, full-width turn-terminal row;
// a hover timestamp would overlap its text, so skip the MessageTimestamp
// wrapper. The data-user-selectable div is still applied for consistency.
if (message.role === 'system' && message.source === 'prompt_cancelled') {
return selectableSafeBody;
}
return (
{selectableSafeBody}
);
}, areMessageItemPropsEqual);
// Aligns with the message it replaces: user messages are right-aligned bubbles,
// so the notice sits on the right too and still reads as that user turn's prompt
// (a left-aligned notice would look like it belongs to the previous turn's
// output). Assistant and other rows are left-aligned, matching their layout.
function MessageRenderError({ align }: { align: 'start' | 'end' }) {
const { t } = useI18n();
return (