import { memo } from "react" import { MessageCircle } from "lucide-react" import { Markdown } from "./Markdown" import type { ReactNode } from "react" import type { ToolExecutionChunk } from "@/lib/agents/types" interface ReplyCardProps { chunk: ToolExecutionChunk } function headerLabel( isLinear: boolean, status: ToolExecutionChunk["status"] ): string { const pending = status === "in_progress" || status === "pending" if (isLinear) return pending ? "Commenting on Linear…" : "Commented on Linear" return pending ? "Replying in Slack…" : "Replied in Slack" } const SLACK_TOKEN = /<([^>]+)>/g const LINK_CLASS = "text-[color:var(--ui-accent)] underline decoration-[color:var(--ui-accent)]/50 break-words [overflow-wrap:anywhere]" type SlackTextObject = { type?: string; text?: string } type SlackBlock = { type?: string text?: SlackTextObject elements?: Array<{ type?: string; text?: SlackTextObject }> } function isSlackTextObject(value: unknown): value is SlackTextObject { return ( !!value && typeof value === "object" && typeof (value as SlackTextObject).text === "string" ) } function isSlackBlockArray(value: unknown): value is Array { return ( Array.isArray(value) && value.every((block) => !!block && typeof block === "object") ) } // Slack mrkdwn isn't standard Markdown — rewrite its link/mention syntax for display // rather than feeding it to the Markdown renderer (which mis-renders *bold* etc.). function renderSlackBody(text: string): Array { const nodes: Array = [] let lastIndex = 0 let key = 0 SLACK_TOKEN.lastIndex = 0 for ( let match = SLACK_TOKEN.exec(text); match; match = SLACK_TOKEN.exec(text) ) { if (match.index > lastIndex) nodes.push(text.slice(lastIndex, match.index)) const token = match[1] ?? "" const sep = token.indexOf("|") const target = sep === -1 ? token : token.slice(0, sep) const label = sep === -1 ? "" : token.slice(sep + 1) if (target.startsWith("@") || target.startsWith("#")) { const sigil = target[0] nodes.push( {sigil} {label || target.slice(1)} ) } else if (target.startsWith("!")) { nodes.push( @{label || target.slice(1)} ) } else { nodes.push( {label || target} ) } lastIndex = match.index + match[0].length } if (lastIndex < text.length) nodes.push(text.slice(lastIndex)) return nodes } function blocksFromOptions( message: string, options: unknown ): Array | null { if (!Array.isArray(options)) return null const cleanOptions = options.filter( (option): option is string => typeof option === "string" && option.trim().length > 0 ) if (cleanOptions.length === 0) return null return [ { type: "section", text: { type: "mrkdwn", text: message } }, { type: "actions", elements: cleanOptions.slice(0, 5).map((option) => ({ type: "button", text: { type: "plain_text", text: option }, })), }, ] } function renderSlackBlocks(blocks: Array): ReactNode { return (
{blocks.map((block, index) => { if ( (block.type === "section" || block.type === "context") && isSlackTextObject(block.text) ) { return (
{renderSlackBody(block.text.text ?? "")}
) } if (block.type === "actions" && Array.isArray(block.elements)) { return (
{block.elements.map((element, elementIndex) => { const label = isSlackTextObject(element.text) ? element.text.text : element.type || "Action" return ( {label} ) })}
) } if (block.type === "divider") { return (
) } return null })}
) } export const ReplyCard = memo(function ReplyCard({ chunk }: ReplyCardProps) { const isLinear = chunk.toolKind === "linear" const body = ((isLinear ? chunk.input?.comment_body : chunk.input?.message) as string) || "" const blocks = !isLinear ? isSlackBlockArray(chunk.input?.blocks) ? chunk.input.blocks : blocksFromOptions(body, chunk.input?.options) : null return (
{headerLabel(isLinear, chunk.status)}
{body && (
{isLinear ? ( ) : blocks ? ( renderSlackBlocks(blocks) ) : (
{renderSlackBody(body)}
)}
)}
) })