import type { QaBusAttachment, QaBusMessage, QaBusSnapshotConversation, } from "openclaw/plugin-sdk/qa-channel-protocol"; import { conversationSelectionKey, findConversationBySelectionKey, messageConversationSelectionKey, threadConversationSelectionKey, } from "./ui-conversation-key.js"; import { findScenarioOutcome } from "./ui-render-scenario.js"; import { badgeHtml, esc, formatIso, formatTime } from "./ui-render-utils.js"; import type { SeedScenario, UiState } from "./ui-types.js"; function attachmentSourceUrl(attachment: QaBusAttachment): string | null { if (attachment.url?.trim()) { return attachment.url; } if (attachment.contentBase64?.trim()) { return `data:${attachment.mimeType};base64,${attachment.contentBase64}`; } return null; } function renderMessageAttachments(message: QaBusMessage): string { const attachments = message.attachments ?? []; if (attachments.length === 0) { return ""; } const items = attachments .map((attachment) => { const sourceUrl = attachmentSourceUrl(attachment); const label = attachment.fileName || attachment.altText || attachment.mimeType; if (attachment.kind === "image" && sourceUrl) { return `
${esc(attachment.altText || label)}
${esc(label)}
`; } if (attachment.kind === "video" && sourceUrl) { return `
${esc(label)}
`; } if (attachment.kind === "audio" && sourceUrl) { return `
${esc(label)}
`; } const transcript = attachment.transcript?.trim() ? `
${esc(attachment.transcript)}
` : ""; const href = sourceUrl ? ` href="${esc(sourceUrl)}" target="_blank" rel="noreferrer"` : ""; return `
${esc(label)} ${transcript}
`; }) .join(""); return `
${items}
`; } function deriveSelectedConversation(state: UiState): string | null { const first = state.snapshot?.conversations[0]; return state.selectedConversationKey ?? (first ? conversationSelectionKey(first) : null); } function deriveSelectedThread(state: UiState): string | null { return state.selectedThreadId ?? null; } function filteredMessages(state: UiState) { const messages = state.snapshot?.messages ?? []; const selectedConversationThreadIds = new Set( (state.snapshot?.threads ?? []) .filter((thread) => threadConversationSelectionKey(thread) === state.selectedConversationKey) .map((thread) => thread.id), ); return messages.filter((message) => { if ( state.selectedConversationKey && messageConversationSelectionKey(message) !== state.selectedConversationKey ) { return false; } if (state.selectedThreadId) { return message.threadId === state.selectedThreadId; } // External thread ids have no sidebar record, even when the conversation // also owns navigable threads, so keep their messages in the root view. return !message.threadId || !selectedConversationThreadIds.has(message.threadId); }); } function formatConversationLabel( conversation: QaBusSnapshotConversation, conversations: QaBusSnapshotConversation[], ): string { const label = conversation.title || conversation.id; const sidebarCollisions = conversations.filter( (candidate) => candidate !== conversation && candidate.id === conversation.id && (candidate.kind === "direct") === (conversation.kind === "direct"), ); const hasAccountCollision = sidebarCollisions.some( (candidate) => candidate.accountId !== conversation.accountId, ); const hasKindCollision = sidebarCollisions.some( (candidate) => candidate.kind !== conversation.kind, ); const disambiguators = [ ...(hasKindCollision ? [conversation.kind] : []), ...(hasAccountCollision ? [conversation.accountId] : []), ]; return disambiguators.length > 0 ? `${label} (${disambiguators.join(", ")})` : label; } export function renderChatView(state: UiState): string { const conversations = state.snapshot?.conversations ?? []; const channels = conversations.filter((c) => c.kind === "channel" || c.kind === "group"); const dms = conversations.filter((c) => c.kind === "direct"); const threads = (state.snapshot?.threads ?? []).filter( (thread) => !state.selectedConversationKey || threadConversationSelectionKey(thread) === state.selectedConversationKey, ); const selectedConv = deriveSelectedConversation(state); const selectedThread = deriveSelectedThread(state); const activeConversation = findConversationBySelectionKey(conversations, selectedConv); const messages = filteredMessages({ ...state, selectedConversationKey: selectedConv, selectedThreadId: selectedThread, }); return `
${esc(activeConversation?.title || activeConversation?.id || "No conversation")} ${activeConversation ? `${activeConversation.kind}` : ""} ${state.bootstrap?.runner.status === "running" ? 'LIVE' : ""}
${ messages.length === 0 ? '
No messages yet. Run scenarios or send a message below.
' : messages.map((m) => renderMessage(m)).join("") }
as in
`; } function messageAvatar(m: QaBusMessage): { emoji: string; bg: string; role: string } { if (m.direction === "outbound") { return { emoji: "\uD83E\uDD80", bg: "#7c6cff", role: "Claw" }; // 🦀 } return { emoji: "\uD83E\uDD9E", bg: "#d97706", role: "Clawfather" }; // 🦞 } function renderMessage(m: QaBusMessage): string { const name = m.senderName || m.senderId; const avatar = messageAvatar(m); const dirClass = m.direction === "inbound" ? "msg-direction-inbound" : "msg-direction-outbound"; const metaTags: string[] = []; if (m.threadId) { metaTags.push(`thread ${esc(m.threadId)}`); } if (m.editedAt) { metaTags.push('edited'); } if (m.deleted) { metaTags.push('deleted'); } const reactions = m.reactions.length > 0 ? `${m.reactions.map((r) => `${esc(r.emoji)}`).join("")}` : ""; return `
${avatar.emoji}
${esc(name)} ${esc(avatar.role)} ${m.direction === "inbound" ? "\u2B06" : "\u2B07"} ${formatTime(m.timestamp)}
${esc(m.text)}
${renderMessageAttachments(m)} ${metaTags.length > 0 || reactions ? `
${metaTags.join("")}${reactions}
` : ""}
`; } function recentInspectorMessages(state: UiState, limit = 18) { return (state.snapshot?.messages ?? []).slice(-limit).toReversed(); } function renderInspectorLiveMessage(message: QaBusMessage): string { const avatar = messageAvatar(message); const conversationLabel = message.conversation.title || message.conversation.id; const threadLabel = message.threadTitle || message.threadId; return `
${avatar.emoji} ${esc(message.senderName || message.senderId)} ${message.direction === "inbound" ? "inbound" : "outbound"}
${formatTime(message.timestamp)}
${esc(conversationLabel)}${threadLabel ? ` · ${esc(threadLabel)}` : ""}
${esc(message.text)}
`; } function renderInspectorLiveTranscript(state: UiState): string { const messages = recentInspectorMessages(state); const isLive = state.bootstrap?.runner.status === "running"; return ` `; } /* ===== Render: Results tab ===== */ export function renderResultsView(state: UiState): string { const scenarios = state.bootstrap?.scenarios ?? []; const selected = scenarios.find((s) => s.id === state.selectedScenarioId) ?? scenarios[0] ?? null; return `
${scenarios.length === 0 ? '
No scenarios loaded.
' : ""} ${scenarios .map((s) => { const outcome = findScenarioOutcome(state, s); const status = outcome?.status ?? "pending"; const isSelected = s.id === (selected?.id ?? null); return ` `; }) .join("")}
${selected ? renderInspector(state, selected) : '
Select a scenario
'}
`; } function renderInspector(state: UiState, scenario: SeedScenario): string { const outcome = findScenarioOutcome(state, scenario); const evidencePath = state.bootstrap?.runner.artifacts?.evidencePath ?? null; return `
${esc(scenario.title)}
${badgeHtml(outcome?.status ?? "pending")}
${ evidencePath ? `` : "" }
${esc(scenario.objective)}
Surface${esc(scenario.surface)}
Started${esc(formatIso(outcome?.startedAt))}
Finished${esc(formatIso(outcome?.finishedAt))}
Run${esc(state.scenarioRun?.kind ?? "seed only")}
Success Criteria
    ${scenario.successCriteria.map((c) => `
  • ${esc(c)}
  • `).join("")}
Observed Outcome
${ outcome ? ` ${outcome.details ? `
${esc(outcome.details)}
` : ""}
${ outcome.steps?.length ? outcome.steps .map( (step) => `
${esc(step.name)} ${badgeHtml(step.status)}
${step.details ? `
${esc(step.details)}
` : ""}
`, ) .join("") : '
No step data yet.
' }
` : '
Not executed yet — seed plan only.
' }
${ scenario.docsRefs?.length ? `
Docs
${scenario.docsRefs.map((r) => `${esc(r)}`).join("")}
` : "" } ${ scenario.codeRefs?.length ? `
Code
${scenario.codeRefs.map((r) => `${esc(r)}`).join("")}
` : "" }
${renderInspectorLiveTranscript(state)}
`; } /* ===== Render: Report tab ===== */ export function renderReportView(state: UiState): string { return `
Protocol Report
${esc(state.latestReport?.markdown ?? "Run the suite or self-check to generate a report.")}
`; } export function renderEventsView(state: UiState): string { const events = (state.snapshot?.events ?? []).slice(-60).toReversed(); return `
Event Stream ${events.length} events (newest first)
${ events.length === 0 ? '
No events yet.
' : events .map((e) => { const detail = "thread" in e ? `${e.thread.conversationId}/${e.thread.id}` : `${e.message.senderId}: ${e.message.text}`; return `
${esc(e.kind)} #${e.cursor} ${esc(detail)}
`; }) .join("") }
`; }