spb/zyquo-agent Public MIT
The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.
Swift 94.7%
Shell 4.1%
Python 0.7%
Makefile 0.5%
1//2// TaskTranscriptExporter.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Exports a task's transcript (prompts, run steps with tool calls/results,9// final answers) as Markdown or PDF via NSSavePanel. The PDF path stays10// deliberately simple: the Markdown text set on an off-screen NSTextView,11// rendered with dataWithPDF (standard AppKit, no print dialog).12//1314import AppKit15import Foundation16import UniformTypeIdentifiers1718@MainActor19enum TaskTranscriptExporter {20 enum Format {21 case markdown22 case pdf23 }2425 /// Presents the save panel and writes the transcript in the given format.26 static func presentSavePanel(for task: AgentTask, format: Format) {27 let panel = NSSavePanel()28 switch format {29 case .markdown:30 panel.allowedContentTypes = [.plainText]31 panel.nameFieldStringValue = "\(WorkspaceManager.slug(from: task.title)).md"32 case .pdf:33 panel.allowedContentTypes = [.pdf]34 panel.nameFieldStringValue = "\(WorkspaceManager.slug(from: task.title)).pdf"35 }36 let markdown = markdown(for: task)37 panel.begin { response in38 guard response == .OK, let url = panel.url else { return }39 Task { @MainActor in40 switch format {41 case .markdown:42 try? markdown.write(to: url, atomically: true, encoding: .utf8)43 case .pdf:44 if let data = pdfData(from: markdown, title: task.title) {45 try? data.write(to: url, options: .atomic)46 }47 }48 }49 }50 }5152 // MARK: - Markdown5354 /// The task's history (prompts, steps, answers) as Markdown.55 static func markdown(for task: AgentTask) -> String {56 var lines: [String] = ["# \(task.title)", ""]57 lines.append("Model: `\(task.modelID)` (\(task.providerID.displayName)) · Safety: \(task.safetyMode.displayName)")58 if let workspace = task.workspacePath {59 lines.append("Workspace: `\(workspace)`")60 }61 lines.append("")62 for message in task.messages {63 switch message.kind {64 case .user:65 lines.append("## 🧑 Prompt")66 lines.append(message.text)67 case .agentRun:68 lines.append("## 🤖 Run")69 for step in message.steps ?? [] {70 lines.append("### Step \(step.index)")71 if !step.text.isEmpty { lines.append(step.text) }72 for invocation in step.toolInvocations {73 lines.append("```")74 lines.append("\(invocation.call.name): \(invocation.call.argumentsJSON)")75 if let result = invocation.result {76 lines.append("→ \(result.content)")77 }78 lines.append("```")79 }80 }81 if !message.text.isEmpty {82 lines.append("### Result")83 lines.append(message.text)84 }85 }86 lines.append("")87 }88 return lines.joined(separator: "\n")89 }9091 // MARK: - PDF9293 /// Renders the Markdown text into PDF data via an off-screen NSTextView.94 /// Headings get a heavier system font; fenced blocks stay monospaced.95 static func pdfData(from markdown: String, title: String) -> Data? {96 let attributed = attributedTranscript(from: markdown)97 let pageWidth: CGFloat = 612 // US Letter, points98 let inset: CGFloat = 4899 let textView = NSTextView(frame: NSRect(x: 0, y: 0, width: pageWidth, height: 10))100 textView.textContainerInset = NSSize(width: inset, height: inset)101 textView.isEditable = false102 textView.textStorage?.setAttributedString(attributed)103 guard let container = textView.textContainer, let manager = textView.layoutManager else {104 return nil105 }106 manager.ensureLayout(for: container)107 let used = manager.usedRect(for: container)108 textView.frame = NSRect(x: 0, y: 0, width: pageWidth, height: used.height + inset * 2)109 return textView.dataWithPDF(inside: textView.bounds)110 }111112 /// Simple line-based styling: #/##/### headings, ``` code fences, body.113 private static func attributedTranscript(from markdown: String) -> NSAttributedString {114 let result = NSMutableAttributedString()115 let bodyFont = NSFont.systemFont(ofSize: 11)116 let codeFont = NSFont.monospacedSystemFont(ofSize: 9.5, weight: .regular)117 var inCodeFence = false118119 for line in markdown.components(separatedBy: "\n") {120 var font = bodyFont121 var text = line122 if line.hasPrefix("```") {123 inCodeFence.toggle()124 continue125 }126 if inCodeFence {127 font = codeFont128 } else if line.hasPrefix("### ") {129 font = NSFont.systemFont(ofSize: 12, weight: .semibold)130 text = String(line.dropFirst(4))131 } else if line.hasPrefix("## ") {132 font = NSFont.systemFont(ofSize: 14, weight: .semibold)133 text = String(line.dropFirst(3))134 } else if line.hasPrefix("# ") {135 font = NSFont.systemFont(ofSize: 18, weight: .bold)136 text = String(line.dropFirst(2))137 }138 result.append(NSAttributedString(139 string: text + "\n",140 attributes: [.font: font, .foregroundColor: NSColor.textColor]141 ))142 }143 return result144 }145}146