/** `irc` — inter-agent messaging: send/wait/inbox/list ops with delivery receipts. */ import type { ReactNode } from "react"; import type { Tone } from "../parts"; import { Badge, Badges, Note, ResultText, Row } from "../parts"; import type { ToolRenderer, ToolRenderProps } from "../types"; import { detailsRecord, isRecord, normalizeWs, str, truncate } from "../util"; interface IrcReceipt { to: string; outcome: string; error?: string; } interface IrcMsg { from: string; body: string; replyTo?: string; } interface IrcPeer { id: string; kind: string; status: string; parentId?: string; unread: number; } function parseReceipts(value: unknown): IrcReceipt[] { if (!Array.isArray(value)) return []; const out: IrcReceipt[] = []; for (const item of value) { if (!isRecord(item)) continue; const to = str(item.to); const outcome = str(item.outcome); if (to === null || outcome === null) continue; out.push({ to, outcome, error: str(item.error) ?? undefined }); } return out; } function parseMsg(value: unknown): IrcMsg | null { if (!isRecord(value)) return null; const from = str(value.from); const body = str(value.body); if (from === null || body === null) return null; return { from, body, replyTo: str(value.replyTo) ?? undefined }; } function parseInbox(value: unknown): IrcMsg[] { if (!Array.isArray(value)) return []; const out: IrcMsg[] = []; for (const item of value) { const msg = parseMsg(item); if (msg) out.push(msg); } return out; } const PEER_STATUS_ORDER: Record = { running: 0, idle: 1, parked: 2 }; function parsePeers(value: unknown): IrcPeer[] { if (!Array.isArray(value)) return []; const out: IrcPeer[] = []; for (const item of value) { if (!isRecord(item)) continue; const id = str(item.id); if (id === null) continue; out.push({ id, kind: str(item.kind) ?? "?", status: str(item.status) ?? "?", parentId: str(item.parentId) ?? undefined, unread: typeof item.unread === "number" && Number.isFinite(item.unread) ? item.unread : 0, }); } return out.sort((a, b) => (PEER_STATUS_ORDER[a.status] ?? 9) - (PEER_STATUS_ORDER[b.status] ?? 9)); } /** Mirrors the TUI's outcomeColor: woken=success, revived=warning, injected=accent, failed=error. */ function outcomeTone(outcome: string): Tone | undefined { switch (outcome) { case "woken": return "ok"; case "revived": return "warn"; case "injected": return "accent"; case "failed": return "err"; default: return undefined; } } function statusTone(status: string): Tone | undefined { switch (status) { case "running": return "accent"; case "idle": return "ok"; case "parked": return undefined; default: return "err"; } } function Summary({ args, result }: ToolRenderProps): ReactNode { const op = str(args.op) ?? "?"; const d = detailsRecord(result); const opBadge = {op}; if (op === "send") { const to = str(args.to); const message = str(args.message); return ( <> {opBadge} {to && → {to}}{" "} {message && {truncate(normalizeWs(message), 80)}} ); } if (op === "wait") { const waited = d ? parseMsg(d.waited) : null; if (waited) { return ( <> {opBadge} ← {waited.from}{" "} {truncate(normalizeWs(waited.body), 80)} ); } const from = str(args.from); return ( <> {opBadge} ← {from ?? "anyone"} {d?.waited === null && ( <> {" "} timed out )} ); } if (op === "inbox") { const inbox = d ? parseInbox(d.inbox) : []; return ( <> {opBadge} {args.peek === true && peek}{" "} {d && ( {inbox.length === 0 ? "empty" : `${inbox.length} ${inbox.length === 1 ? "message" : "messages"}`} )} ); } if (op === "list") { const peers = d ? parsePeers(d.peers) : []; let unread = 0; for (const peer of peers) unread += peer.unread; return ( <> {opBadge} {d && {peers.length === 1 ? "1 peer" : `${peers.length} peers`}} {unread > 0 && ( <> {" "} {unread} unread )} ); } return opBadge; } function Body({ args, result }: ToolRenderProps): ReactNode { const op = str(args.op); const to = str(args.to); const from = str(args.from); const message = str(args.message); const d = detailsRecord(result); const receipts = parseReceipts(d?.receipts); const waited = d ? parseMsg(d.waited) : null; const timedOut = d ? d.waited === null : false; const inbox = parseInbox(d?.inbox); const peers = parsePeers(d?.peers); const structured = receipts.length > 0 || waited !== null || timedOut || inbox.length > 0 || peers.length > 0; return ( <> {message && {message}} {receipts.length > 0 && (
{receipts.map((receipt, i) => ( {receipt.outcome} {receipt.error && — {receipt.error}} ))}
)} {waited && (
{waited.body} {waited.replyTo && ( <> {" "} reply )}
)} {timedOut && No reply yet — they may answer later; check inbox or wait again.} {inbox.length > 0 && (
{inbox.map((msg, i) => ( {msg.body} {msg.replyTo && ( <> {" "} reply )} ))}
)} {peers.length > 0 && (
{peers.map(peer => ( {peer.status}{" "} {peer.parentId ? `${peer.kind} · of ${peer.parentId}` : peer.kind} {peer.unread > 0 && ( <> {" "} {peer.unread} unread )} ))}
)} {(!structured || result?.isError) && } ); } export const ircRenderer: ToolRenderer = { Summary, Body };