2.5.0 #10

Merged
rune merged 41 commits from 2.5.0 into main 2026-08-03 08:33:23 +02:00
Showing only changes of commit 7f5374dce0 - Show all commits
+42 -12
View File
@@ -76,7 +76,8 @@ enum ConversationExportService {
font-size: 14px; color: #1a1a1a; background: #ffffff; max-width: 820px; margin: 40px auto; \
padding: 0 24px; line-height: 1.5; }
h1 { font-size: 22px; border-bottom: 1px solid #ddd; padding-bottom: 12px; }
.message { margin: 20px 0; padding: 12px 16px; border-radius: 8px; border-left: 4px solid transparent; }
.message { margin: 20px 0; padding: 12px 16px; border-radius: 8px; border-left: 4px solid transparent; \
break-inside: avoid; page-break-inside: avoid; }
.message.user { background: #f5f7fa; border-left-color: #4a90d9; }
.message.assistant { background: #fafafa; border-left-color: #8a8a8a; }
.role { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; \
@@ -115,16 +116,22 @@ enum ConversationExportService {
// MARK: - PDF
enum PDFError: LocalizedError {
case generationFailed
var errorDescription: String? { "Failed to generate PDF" }
}
/// Uses WKWebView's real print pipeline (NSPrintOperation), not createPDF(). Confirmed
/// empirically by comparing against a normal reference PDF: createPDF() never paginates
/// it captures the webview's frame verbatim, or auto-grows to ONE continuous page matching
/// full content height for overflow (a single page many thousands of points tall for a long
/// conversation). That's a fundamentally different, non-standard document shape from any
/// normal PDF, which is what actually made ordinarily-sized text read as "huge" there was
/// no page of a familiar size to judge it against. Printing through NSPrintOperation with a
/// real US Letter paper size gives genuine multi-page pagination, matching what any other
/// app's "export/print to PDF" produces.
static func pdfData(name: String, messages: [Message]) async throws -> Data {
let htmlString = html(name: name, messages: messages)
// Confirmed empirically (not guessed): with WKPDFConfiguration.rect left at its
// default, createPDF captures exactly the webview's frame size verbatim for content
// that fits within it, and auto-grows to a single tall page matching the full
// scrollable content when it overflows it does not paginate. Manually resizing the
// frame to match content height beforehand (a previous attempt at this) actively
// breaks that: the captured page ends up NOT matching the resized frame at all,
// producing a small, oddly-proportioned page that made ordinary-sized text look
// enormous relative to it. So: set a sane starting frame and leave it alone.
let webView = WKWebView(frame: NSRect(x: 0, y: 0, width: 850, height: 1100))
webView.pageZoom = 1.0
// PDF content is baked at export time, so it can't respond to prefers-color-scheme
@@ -134,11 +141,34 @@ enum ConversationExportService {
webView.navigationDelegate = delegate
try await delegate.load(htmlString, in: webView)
return try await withCheckedThrowingContinuation { continuation in
webView.createPDF(configuration: WKPDFConfiguration()) { result in
continuation.resume(with: result)
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".pdf")
defer { try? FileManager.default.removeItem(at: tempURL) }
let printInfo = NSPrintInfo()
printInfo.paperSize = NSSize(width: 612, height: 792) // US Letter
printInfo.topMargin = 36
printInfo.bottomMargin = 36
printInfo.leftMargin = 36
printInfo.rightMargin = 36
printInfo.horizontalPagination = .fit
printInfo.jobDisposition = .save
printInfo.dictionary()[NSPrintInfo.AttributeKey.jobSavingURL] = tempURL
let printOp = webView.printOperation(with: printInfo)
printOp.showsPrintPanel = false
printOp.showsProgressPanel = false
// NSPrintOperation.run() blocks synchronously, so it's dispatched off the main actor
// to avoid freezing the UI while a long conversation paginates.
let success = await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
DispatchQueue.global(qos: .userInitiated).async {
continuation.resume(returning: printOp.run())
}
}
guard success, let data = try? Data(contentsOf: tempURL) else {
throw PDFError.generationFailed
}
return data
}
private final class PDFLoadDelegate: NSObject, WKNavigationDelegate {