// // TaskTranscriptExporter.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Exports a task's transcript (prompts, run steps with tool calls/results, // final answers) as Markdown or PDF via NSSavePanel. The PDF path stays // deliberately simple: the Markdown text set on an off-screen NSTextView, // rendered with dataWithPDF (standard AppKit, no print dialog). // import AppKit import Foundation import UniformTypeIdentifiers @MainActor enum TaskTranscriptExporter { enum Format { case markdown case pdf } /// Presents the save panel and writes the transcript in the given format. static func presentSavePanel(for task: AgentTask, format: Format) { let panel = NSSavePanel() switch format { case .markdown: panel.allowedContentTypes = [.plainText] panel.nameFieldStringValue = "\(WorkspaceManager.slug(from: task.title)).md" case .pdf: panel.allowedContentTypes = [.pdf] panel.nameFieldStringValue = "\(WorkspaceManager.slug(from: task.title)).pdf" } let markdown = markdown(for: task) panel.begin { response in guard response == .OK, let url = panel.url else { return } Task { @MainActor in switch format { case .markdown: try? markdown.write(to: url, atomically: true, encoding: .utf8) case .pdf: if let data = pdfData(from: markdown, title: task.title) { try? data.write(to: url, options: .atomic) } } } } } // MARK: - Markdown /// The task's history (prompts, steps, answers) as Markdown. static func markdown(for task: AgentTask) -> String { var lines: [String] = ["# \(task.title)", ""] lines.append("Model: `\(task.modelID)` (\(task.providerID.displayName)) ยท Safety: \(task.safetyMode.displayName)") if let workspace = task.workspacePath { lines.append("Workspace: `\(workspace)`") } lines.append("") for message in task.messages { switch message.kind { case .user: lines.append("## ๐Ÿง‘ Prompt") lines.append(message.text) case .agentRun: lines.append("## ๐Ÿค– Run") for step in message.steps ?? [] { lines.append("### Step \(step.index)") if !step.text.isEmpty { lines.append(step.text) } for invocation in step.toolInvocations { lines.append("```") lines.append("\(invocation.call.name): \(invocation.call.argumentsJSON)") if let result = invocation.result { lines.append("โ†’ \(result.content)") } lines.append("```") } } if !message.text.isEmpty { lines.append("### Result") lines.append(message.text) } } lines.append("") } return lines.joined(separator: "\n") } // MARK: - PDF /// Renders the Markdown text into PDF data via an off-screen NSTextView. /// Headings get a heavier system font; fenced blocks stay monospaced. static func pdfData(from markdown: String, title: String) -> Data? { let attributed = attributedTranscript(from: markdown) let pageWidth: CGFloat = 612 // US Letter, points let inset: CGFloat = 48 let textView = NSTextView(frame: NSRect(x: 0, y: 0, width: pageWidth, height: 10)) textView.textContainerInset = NSSize(width: inset, height: inset) textView.isEditable = false textView.textStorage?.setAttributedString(attributed) guard let container = textView.textContainer, let manager = textView.layoutManager else { return nil } manager.ensureLayout(for: container) let used = manager.usedRect(for: container) textView.frame = NSRect(x: 0, y: 0, width: pageWidth, height: used.height + inset * 2) return textView.dataWithPDF(inside: textView.bounds) } /// Simple line-based styling: #/##/### headings, ``` code fences, body. private static func attributedTranscript(from markdown: String) -> NSAttributedString { let result = NSMutableAttributedString() let bodyFont = NSFont.systemFont(ofSize: 11) let codeFont = NSFont.monospacedSystemFont(ofSize: 9.5, weight: .regular) var inCodeFence = false for line in markdown.components(separatedBy: "\n") { var font = bodyFont var text = line if line.hasPrefix("```") { inCodeFence.toggle() continue } if inCodeFence { font = codeFont } else if line.hasPrefix("### ") { font = NSFont.systemFont(ofSize: 12, weight: .semibold) text = String(line.dropFirst(4)) } else if line.hasPrefix("## ") { font = NSFont.systemFont(ofSize: 14, weight: .semibold) text = String(line.dropFirst(3)) } else if line.hasPrefix("# ") { font = NSFont.systemFont(ofSize: 18, weight: .bold) text = String(line.dropFirst(2)) } result.append(NSAttributedString( string: text + "\n", attributes: [.font: font, .foregroundColor: NSColor.textColor] )) } return result } }