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) +} diff --git a/src/auto-reply/reply/commands-tts.ts b/src/auto-reply/reply/commands-tts.ts index 04b60a4e9..bba7e2b02 100644 --- a/src/auto-reply/reply/commands-tts.ts +++ b/src/auto-reply/reply/commands-tts.ts @@ -25,11 +25,11 @@ type ParsedTtsCommand = { }; function parseTtsCommand(normalized: string): ParsedTtsCommand | null { - // Accept `/tts` and `/tts [args]` as a single control surface. - if (normalized === "/tts") return { action: "status", args: "" }; + // Accept `/tts [args]` - return null for `/tts` alone to trigger inline menu. + if (normalized === "/tts") return null; if (!normalized.startsWith("/tts ")) return null; const rest = normalized.slice(5).trim(); - if (!rest) return { action: "status", args: "" }; + if (!rest) return null; const [action, ...tail] = rest.split(/\s+/); return { action: action.toLowerCase(), args: tail.join(" ").trim() }; } diff --git a/src/telegram/bot-native-commands.ts b/src/telegram/bot-native-commands.ts index cd53459e6..4b5266512 100644 --- a/src/telegram/bot-native-commands.ts +++ b/src/telegram/bot-native-commands.ts @@ -485,6 +485,8 @@ export const registerTelegramNativeCommands = ({ // Originating context for sub-agent announce routing OriginatingChannel: "telegram" as const, OriginatingTo: `telegram:${chatId}`, + // Multi-account: preserve accountId for command confirmations (e.g., /reset) + AccountId: route.accountId, }); const disableBlockStreaming =