import { LitElement, html, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import * as Diff from "diff"; import type { MessageParam, TextBlockParam, ContentBlock, ContentBlockParam, Message, ToolUnion, } from "@anthropic-ai/sdk/resources/messages"; import { SimpleConversation, EnhancedMessageParam } from "../../../src/shared-conversation-processor"; import { markdownToHtml } from "../utils/markdown"; @customElement("simple-conversation-view") export class SimpleConversationView extends LitElement { @property({ type: Array }) conversations: SimpleConversation[] = []; // Disable shadow DOM to use global CSS createRenderRoot() { return this; } private handleToggle( e: Event, options: { type?: "content" | "write" | "custom"; targetSelector?: string; toggleSelector?: string; customHandler?: (element: HTMLElement, isHidden: boolean) => void; } = {}, ) { const { type = "content", targetSelector, toggleSelector, customHandler } = options; const currentElement = e.currentTarget as HTMLElement; if (type === "write") { const fullContent = currentElement.previousElementSibling as HTMLElement; if (fullContent) { const isExpanded = !fullContent.classList.contains("hidden"); if (isExpanded) { fullContent.classList.add("hidden"); currentElement.classList.remove("hidden"); } else { fullContent.classList.remove("hidden"); currentElement.classList.add("hidden"); } } return; } // Default content toggle behavior const target = targetSelector ? (currentElement.querySelector(targetSelector) as HTMLElement) : (currentElement.nextElementSibling as HTMLElement); const toggle = toggleSelector ? (currentElement.querySelector(toggleSelector) as HTMLElement) : (currentElement.querySelector("span:first-child") as HTMLElement); if (target) { const isHidden = target.classList.contains("hidden"); if (customHandler) { customHandler(target, isHidden); } else { target.classList.toggle("hidden", !isHidden); if (toggle) { toggle.textContent = isHidden ? "[-]" : "[+]"; } } } } private toggleContent(e: Event) { this.handleToggle(e); } private toggleWriteContent(e: Event) { this.handleToggle(e, { type: "write" }); } private formatContent(content: string | ContentBlockParam[], toolResults?: Record): TemplateResult { if (typeof content === "string") { return this.formatStringContent(content); } if (Array.isArray(content)) { return html` ${content.map((block) => { if (block.type === "text") { return this.formatStringContent(block.text); } else if (block.type === "thinking") { const thinkingBlock = block as any; return html`
Thinking
${unsafeHTML(markdownToHtml(thinkingBlock.thinking || ""))}
`; } else if (block.type === "tool_result") { // Skip standalone tool_result blocks - they will be paired with tool_use return html``; } else if (block.type === "tool_use") { const toolUse = block as any; const toolResult = toolResults?.[toolUse.id]; if (block.name === "TodoWrite" || block.name === "Edit" || block.name === "MultiEdit") { return this.renderToolContainer(block, toolResult); } if (block.name === "Write") { const customContent = html`
${this.renderWritePreview(block)}
`; return html`
${this.getToolDisplayName(block)}
${customContent} ${toolResult ? this.renderToolResult(toolResult, block) : ""}
`; } return this.renderToolContainer(block, toolResult, { isCollapsible: true, isCollapsed: true, isClickToExpand: true, }); } return html`
${JSON.stringify(block, null, 2)}
`; })} `; } return html`
${JSON.stringify(content, null, 2)}
`; } private formatStringContent(content: string): TemplateResult { // Check for system reminder blocks (handling both raw and HTML-escaped delimiters) const systemReminderRegexEscaped = /<system-reminder>([\s\S]*?)<\/system-reminder>/g; const systemReminderRegexRaw = /([\s\S]*?)<\/system-reminder>/g; const systemReminders: string[] = []; let match; // Extract all system reminder blocks (escaped) while ((match = systemReminderRegexEscaped.exec(content)) !== null) { systemReminders.push(match[1].trim()); } // Extract all system reminder blocks (raw) while ((match = systemReminderRegexRaw.exec(content)) !== null) { systemReminders.push(match[1].trim()); } // Remove system reminder blocks from main content let mainContent = content.replace(systemReminderRegexEscaped, "").replace(systemReminderRegexRaw, "").trim(); return html` ${mainContent ? html`
${unsafeHTML(markdownToHtml(mainContent))}
` : ""} ${systemReminders.length > 0 ? this.renderCollapsibleSection( "System Reminder", html`
${systemReminders.map( (reminder, index) => html`
${systemReminders.length > 1 ? html`
Reminder ${index + 1}:
` : ""}
${unsafeHTML(markdownToHtml(reminder))}
`, )}
`, { titleClasses: "text-vs-muted", containerClasses: "mt-4 mb-4", count: systemReminders.length > 1 ? systemReminders.length : undefined, }, ) : ""} `; } private formatSystem(system: string | TextBlockParam[] | undefined): string { if (!system) return ""; if (typeof system === "string") { return markdownToHtml(system); } if (Array.isArray(system)) { const textContent = system .map((block) => { if (block.type === "text") { return block.text; } return JSON.stringify(block, null, 2); }) .join("\n"); return markdownToHtml(textContent); } return JSON.stringify(system, null, 2); } private formatResponseContent(response: Message): TemplateResult { if (!response) return html``; if (response.content && Array.isArray(response.content)) { return html` ${response.content.map((block) => { if (block.type === "text") { return html`
${unsafeHTML(markdownToHtml(block.text))}
`; } else if (block.type === "thinking") { const thinkingBlock = block as any; return html`
Thinking
${unsafeHTML(markdownToHtml(thinkingBlock.thinking || ""))}
`; } else if (block.type === "tool_use") { if (block.name === "TodoWrite") { return html`
${this.getToolDisplayName(block)}
${this.renderToolUseContent(block)}
`; } return html`
[+] ${this.getToolDisplayName(block)}
`; } return html`
${JSON.stringify(block, null, 2)}
`; })} `; } return html`
${JSON.stringify(response, null, 2)}
`; } private formatSingleParam(toolName: string, paramValue: string | undefined, paramName: string = ""): TemplateResult { return paramValue ? html`${toolName}(${this.unescapeHtml(paramValue)})` : html`${toolName}`; } private formatMultiParam(toolName: string, params: string[]): TemplateResult { return params.length > 0 ? html`${toolName}(${params.join(", ")})` : html`${toolName}`; } private unescapeHtml(str: string): string { const div = document.createElement("div"); div.innerHTML = str; return div.textContent || div.innerText || ""; } private wrapInScrollable(content: TemplateResult | string, usePreFormatting: boolean = true): TemplateResult { if (usePreFormatting && typeof content === "string") { return html`
${content}
`; } return html`
${content}
`; } private renderCollapsibleSection( title: string, content: TemplateResult, options: { isExpanded?: boolean; titleClasses?: string; containerClasses?: string; count?: number; } = {}, ): TemplateResult { const { isExpanded = false, titleClasses = "", containerClasses = "", count } = options; const expandedClass = isExpanded ? "" : "hidden"; const toggleSymbol = isExpanded ? "[-]" : "[+]"; const displayTitle = count !== undefined ? `${title} (${count})` : title; return html`
${toggleSymbol} ${displayTitle}
${content}
`; } private getToolDisplayName(toolUse: any, toolResult?: any): TemplateResult { const toolName = toolUse.name; const input = toolUse.input; switch (toolName) { case "Read": return this.formatSingleParam(toolName, input?.file_path); case "Bash": return this.formatSingleParam(toolName, input?.command); case "Write": return this.formatSingleParam(toolName, input?.file_path); case "Glob": if (input?.pattern) { const params = [this.unescapeHtml(input.pattern)]; if (input?.path) params.push(this.unescapeHtml(input.path)); return this.formatMultiParam(toolName, params); } return html`${toolName}`; case "Grep": if (input?.pattern) { const params = [this.unescapeHtml(input.pattern)]; if (input?.include) params.push(this.unescapeHtml(input.include)); if (input?.path) params.push(this.unescapeHtml(input.path)); return this.formatMultiParam(toolName, params); } return html`${toolName}`; case "LS": if (input?.path) { const params = [this.unescapeHtml(input.path)]; if (input?.ignore) { const ignoreStr = input.ignore.map((p: string) => this.unescapeHtml(p)).join(", "); params.push(`ignore: ${ignoreStr}`); } return this.formatMultiParam(toolName, params); } return html`${toolName}`; case "Edit": return input?.file_path ? this.formatSingleParam(toolName, this.unescapeHtml(input.file_path).split("/").pop()) : html`${toolName}`; case "MultiEdit": if (input?.file_path) { const fileName = this.unescapeHtml(input.file_path).split("/").pop() || input.file_path; const editCount = input?.edits ? input.edits.length : 0; return this.formatMultiParam(toolName, [fileName, `${editCount} edits`]); } return html`${toolName}`; case "NotebookRead": return input?.notebook_path ? this.formatSingleParam(toolName, this.unescapeHtml(input.notebook_path).split("/").pop()) : html`${toolName}`; case "NotebookEdit": if (input?.notebook_path && input?.cell_number !== undefined) { const fileName = this.unescapeHtml(input.notebook_path).split("/").pop(); const cellNum = input.cell_number; const mode = input?.edit_mode || "replace"; return this.formatMultiParam(toolName, [fileName, `cell ${cellNum}`, mode]); } return html`${toolName}`; case "WebFetch": return input?.url ? this.formatSingleParam(toolName, input.url) : html`${toolName}`; case "WebSearch": return input?.query ? this.formatSingleParam(toolName, input.query) : html`${toolName}`; default: return html`${toolName}`; } } private renderToolUseContent(toolUse: any): TemplateResult { const toolName = toolUse.name; const input = toolUse.input; if (toolName === "TodoWrite" && input?.todos) { const todos = input.todos; return this.wrapInScrollable( html`${todos.map((todo: any) => { const statusClass = todo.status === "completed" ? "line-through text-vs-text" : todo.status === "in_progress" ? "text-green-400" : "text-vs-muted"; return html`
• ${todo.content}
`; })}`, false, ); } if (toolName === "NotebookEdit" && input?.new_source) { const content = input.new_source; return this.wrapInScrollable(content); } if (toolName === "Write" && input?.content) { const content = input.content; return this.wrapInScrollable(content); } if (toolName === "MultiEdit" && input?.edits) { const edits = input.edits; return this.wrapInScrollable( html`${edits.map((edit: any, index: number) => { const oldStr = edit.old_string; const newStr = edit.new_string; const diffLines = this.renderDiff(oldStr, newStr); return html`
Edit ${index + 1}:
${diffLines}
`; })}`, false, ); } if (toolName === "Edit" && input?.old_string && input?.new_string) { const oldStr = input.old_string; const newStr = input.new_string; const diffLines = this.renderDiff(oldStr, newStr); return this.wrapInScrollable(html`${diffLines}`, false); } if (toolName === "WebFetch" && input?.url && input?.prompt) { return this.wrapInScrollable( html`
URL:
${input.url}
Prompt:
${input.prompt}
`, false, ); } if (toolName === "WebSearch" && input?.query) { const params = []; params.push(html`
Query:
${input.query}
`); if (input?.allowed_domains) { params.push(html`
Allowed Domains:
${input.allowed_domains.join(", ")}
`); } if (input?.blocked_domains) { params.push(html`
Blocked Domains:
${input.blocked_domains.join(", ")}
`); } return this.wrapInScrollable(html`${params}`, false); } // Default: show JSON parameters return this.wrapInScrollable(JSON.stringify(input, null, 2)); } private renderToolResult(toolResult: any, toolUse?: any): TemplateResult { return html`
[+] Tool Result ${toolResult?.is_error ? "❌" : "✅"}
${toolUse ? html`
[+] Raw Tool Call
` : ""}
`; } private renderToolContainer( toolUse: any, toolResult?: any, options: { isCollapsible?: boolean; isCollapsed?: boolean; isClickToExpand?: boolean; customContent?: TemplateResult; } = {}, ): TemplateResult { const { isCollapsible = false, isCollapsed = false, isClickToExpand = false, customContent } = options; const contentDiv = customContent || html`${this.renderToolUseContent(toolUse)}`; const headerClasses = isClickToExpand ? "text-vs-type px-4 break-all cursor-pointer hover:text-white transition-colors" : "text-vs-type px-4 break-all"; const contentClasses = isCollapsed ? "bg-vs-bg-secondary mx-4 p-4 text-vs-text hidden" : "bg-vs-bg-secondary mx-4 p-4 text-vs-text"; return html`
${isCollapsible ? html`[${isCollapsed ? "+" : "-"}]` : ""} ${this.getToolDisplayName(toolUse)}
${contentDiv}
${toolResult ? this.renderToolResult(toolResult, toolUse) : ""}
`; } private renderWritePreview(toolUse: any): TemplateResult { const input = toolUse.input; if (!input?.content) { return html`
No content
`; } const content = input.content; const lines = content.split("\n"); const preview = lines.slice(0, 10); const hasMore = lines.length > 10; return html` ${this.wrapInScrollable(preview.join("\n"))} ${hasMore ? html`
... ${lines.length - 10} more lines (click to expand)
` : ""} `; } private renderDiff(oldStr: string, newStr: string): TemplateResult[] { const diff = Diff.diffLines(oldStr, newStr); const diffLines = []; for (const part of diff) { const lines = part.value.split("\n"); // Remove empty last line from split if it exists if (lines[lines.length - 1] === "") { lines.pop(); } for (const line of lines) { if (part.added) { diffLines.push(html`
+ ${line}
`); } else if (part.removed) { diffLines.push(html`
- ${line}
`); } else { diffLines.push(html`
  ${line}
`); } } } return diffLines; } private hasTools(conversation: SimpleConversation): boolean { return !!(conversation.finalPair.request.tools && conversation.finalPair.request.tools.length > 0); } private renderTools(tools: ToolUnion[]): TemplateResult { return html` ${tools.map((tool) => { if ("name" in tool && tool.name) { const description = ("description" in tool && tool.description) || "No description"; return this.renderCollapsibleSection( tool.name, html`
${unsafeHTML(markdownToHtml(description))}
${"input_schema" in tool && tool.input_schema && typeof tool.input_schema === "object" ? (() => { const schema = tool.input_schema as any; if (schema.properties) { return html`
Parameters:
${Object.entries(schema.properties).map(([paramName, paramDef]) => { const def = paramDef as any; const required = schema.required?.includes(paramName) ? " (required)" : ""; const type = def.type ? ` [${def.type}]` : ""; const desc = def.description ? ` - ${def.description}` : ""; return html`
${paramName} ${type}${required}${desc}
`; })} `; } return html``; })() : html``} `, { titleClasses: "text-vs-type font-bold", containerClasses: "mb-4", isExpanded: true, }, ); } return html`
${JSON.stringify(tool, null, 2)}
`; })} `; } private renderConversationContent(conversation: SimpleConversation): TemplateResult { return html` ${conversation.system ? this.renderCollapsibleSection( "System Prompt", html`
${unsafeHTML(this.formatSystem(conversation.system))}
`, { titleClasses: "text-vs-function", containerClasses: "px-4 mt-4", }, ) : ""} ${this.hasTools(conversation) ? this.renderCollapsibleSection( "Tools", html`
${this.renderTools(conversation.finalPair.request.tools || [])}
`, { titleClasses: "text-vs-type", containerClasses: "px-4", count: conversation.finalPair.request.tools?.length || 0, }, ) : ""}
${conversation.messages .filter((message) => !(message as EnhancedMessageParam).hide) .map( (message, msgIndex) => html`
${message.role}
${this.formatContent(message.content, (message as EnhancedMessageParam).toolResults)}
`, )}
assistant
${this.formatResponseContent(conversation.response)}
`; } render() { if (this.conversations.length === 0) { return html`
No conversations found.
`; } return html`
${this.conversations.map( (conversation) => html`
${conversation.compacted ? html`
[+] Compacted (click to view details)
` : html`
${Array.from(conversation.models).join(", ")}
${new Date(conversation.metadata.startTime).toLocaleString()} ${conversation.messages.length + 1} messages
${this.renderConversationContent(conversation)} `}
`, )}
`; } }