Files
nucleic/Sources/NucleicCore/ConversationExport.swift
T
abkslmandnucleic a3364665bc Add Usage Metrics
Nucleic-Session: B4F51091-BE3C-42D3-BCDD-8E89E83543D0
Co-authored-by: Nucleic <[email protected]>
2026-07-09 01:44:59 -07:00

111 lines
5.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Renders a full session — metadata, backend/debug fields, the human-readable
/// conversation, and the raw event JSONL — into a single Markdown document for
/// sharing as a reference.
public enum ConversationExport {
public static func markdown(session: Session, project: Project?, events: [AgentEvent]) -> String {
var out = "# Nucleic session export\n\n"
out += "## Session\n"
out += line("Title", session.title)
out += line("ID", session.id.rawValue)
out += line("Status", session.status.displayName)
out += line("Backend", session.backend.rawValue)
out += line("Backend session id", session.backendSessionID ?? "—")
out += line("Model", session.model ?? "default")
out += line("Effort", session.effort ?? "default")
out += line("Branch", session.branch ?? "—")
out += line("Worktree", session.worktreePath ?? "—")
out += line("Base SHA", session.baseSHA ?? "—")
if let diff = session.diffStat {
out += line("Diff", "\(diff.filesChanged) files +\(diff.added) \(diff.removed)")
}
out += line("Last seq", "\(session.lastSeq)")
out += line("Transcript path", session.transcriptPath)
out += line("Created", iso(session.createdAt))
out += line("Updated", iso(session.updatedAt))
if let project {
out += "\n## Project\n"
out += line("Name", project.name)
out += line("Root", project.rootPath)
out += line("Default branch", project.defaultBranch.value)
}
out += "\n## Conversation\n\n"
for event in events { out += human(event) }
out += "\n## Raw events (JSONL)\n\nFull normalized event stream for debugging:\n\n```jsonl\n"
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
encoder.dateEncodingStrategy = .iso8601
for event in events {
if let data = try? encoder.encode(event) {
out += String(decoding: data, as: UTF8.self) + "\n"
}
}
out += "```\n"
return out
}
/// A filesystem-safe default filename for the export.
public static func suggestedFileName(for session: Session) -> String {
let slug = Slug.sanitize(session.title, fallback: "session")
return "nucleic-\(slug)-\(session.id.short).md"
}
// MARK: - rendering
private static func line(_ key: String, _ value: String) -> String { "- **\(key):** \(value)\n" }
private static func human(_ event: AgentEvent) -> String {
let seq = "`#\(event.seq)`"
switch event.kind {
case .sessionStarted(let started):
return "\(seq) **session started** — model \(started.model)\n\n"
case .userText(let chunk):
return "\(seq) **User:** \(chunk.text)\n\n"
case .assistantText(let chunk) where !chunk.isPartial:
return "\(seq) **Assistant:** \(chunk.text)\n\n"
case .assistantText, .thinking, .toolCallInputDelta:
return ""
case .toolCallCompleted(let call):
return "\(seq) **Tool** `\(call.name)` — \(call.input.canonicalString())\n\n"
case .toolCallStarted:
return ""
case .toolResult(let result):
let body = result.content.canonicalString()
return "\(seq) **Result**\(result.isError ? " (error)" : ""): \(body.prefix(2000))\n\n"
case .fileChange(let change):
return "\(seq) **File \(change.kind.rawValue):** \(change.path)\n\n"
case .approvalRequested(let request):
return "\(seq) **Approval requested:** \(request.title) [\(request.risk.rawValue)]\n\n"
case .approvalResolved(let resolved):
return "\(seq) **Approval \(resolved.decidedBy):** resolved\n\n"
case .usage(let usage):
return "\(seq) _usage in=\(usage.inputTokens ?? 0) out=\(usage.outputTokens ?? 0)_\n\n"
case .rateLimit(let rl):
return "\(seq) _rate limit \(rl.rateLimitType ?? "?") \(rl.status ?? "")_\n\n"
case .codexUsage(let usage):
let peak = usage.peakUtilization.map { "\(Int($0.rounded()))%" } ?? "?"
return "\(seq) _codex usage peak \(peak)_\n\n"
case .turnCompleted:
return "\(seq) _— turn completed —_\n\n"
case .runFinished(let finished):
return "\(seq) _— run \(finished.outcome.rawValue) —_\n\n"
case .error(let error):
return "\(seq) ⚠️ **\(error.recoverable ? "recoverable" : "fatal")**: \(error.message)\n\n"
case .note(let note):
return "\(seq) _\(note.text)_\n\n"
case .raw(let raw):
return "\(seq) `raw[\(event.nativeType ?? "?")]` \(raw.native.canonicalString().prefix(500))\n\n"
}
}
private static func iso(_ date: Date) -> String {
let formatter = ISO8601DateFormatter()
return formatter.string(from: date)
}
}