diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift index e68c8591b..e763814e4 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift @@ -6,6 +6,23 @@ public enum ChatMarkdownVariant: String, CaseIterable, Sendable { case compact } +// MARK: - Textual Bundle Availability + +/// Checks if the Textual syntax highlighting bundle is available. +/// This must be called BEFORE any Textual types are accessed to avoid a crash +/// from SPM's generated Bundle.module accessor when the bundle is missing. +private let textualBundleAvailable: Bool = { + let bundleNames = ["textual_Textual", "Textual_Textual"] + guard let resourceURL = Bundle.main.resourceURL else { return false } + for name in bundleNames { + let bundleURL = resourceURL.appendingPathComponent("\(name).bundle") + if FileManager.default.fileExists(atPath: bundleURL.path) { + return true + } + } + return false +}() + @MainActor struct ChatMarkdownRenderer: View { enum Context { @@ -22,12 +39,20 @@ struct ChatMarkdownRenderer: View { var body: some View { let processed = ChatMarkdownPreprocessor.preprocess(markdown: self.text) VStack(alignment: .leading, spacing: 10) { - StructuredText(markdown: processed.cleaned) - .modifier(ChatMarkdownStyle( - variant: self.variant, - context: self.context, + if textualBundleAvailable { + StructuredText(markdown: processed.cleaned) + .modifier(ChatMarkdownStyle( + variant: self.variant, + context: self.context, + font: self.font, + textColor: self.textColor)) + } else { + // Fallback when Textual's resource bundle is missing (avoids crash). + FallbackMarkdownText( + text: processed.cleaned, font: self.font, - textColor: self.textColor)) + textColor: self.textColor) + } if !processed.images.isEmpty { InlineImageList(images: processed.images) @@ -88,3 +113,33 @@ private struct InlineImageList: View { } } } + +// MARK: - Fallback Markdown Rendering + +/// Fallback markdown renderer using SwiftUI's native AttributedString. +/// Used when Textual's resource bundle is missing to avoid crashes. +@MainActor +private struct FallbackMarkdownText: View { + let text: String + let font: Font + let textColor: Color + + var body: some View { + if let attributed = try? AttributedString(markdown: self.text, options: Self.markdownOptions) { + Text(attributed) + .font(self.font) + .foregroundStyle(self.textColor) + .textSelection(.enabled) + } else { + Text(self.text) + .font(self.font) + .foregroundStyle(self.textColor) + .textSelection(.enabled) + } + } + + private static let markdownOptions = AttributedString.MarkdownParsingOptions( + allowsExtendedAttributes: true, + interpretedSyntax: .inlineOnlyPreservingWhitespace, + failurePolicy: .returnPartiallyParsedIfPossible) +}