From 651bad3b47e063ca9add184d4fe101d05ebff30e Mon Sep 17 00:00:00 2001 From: Glucksberg Date: Sun, 25 Jan 2026 19:42:06 +0000 Subject: [PATCH 1/3] fix(tts): generate audio when block streaming drops final reply When block streaming succeeds, final replies are dropped but TTS was only applied to final replies. Fix by accumulating block text during streaming and generating TTS-only audio after streaming completes. Also: - Change truncate vs skip behavior when summary OFF (now truncates) - Align TTS limits with Telegram max (4096 chars) - Improve /tts command help messages with examples - Add newline separator between accumulated blocks --- src/auto-reply/reply/commands-tts.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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() }; } From 96a156ab8734486a8a36892efda9226184453a8d Mon Sep 17 00:00:00 2001 From: Glucksberg Date: Mon, 26 Jan 2026 04:34:59 +0000 Subject: [PATCH 2/3] fix(macos): prevent crash when Textual syntax highlighting bundle is missing Add graceful fallback to SwiftUI's native AttributedString markdown rendering when the Textual resource bundle is not found. This prevents the app from crashing with an NSBundle.module assertion failure when code blocks are rendered without the prism-bundle.js resource being properly embedded. Fixes #2002 --- .../OpenClawChatUI/ChatMarkdownRenderer.swift | 65 +++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) 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) +} From 4e860cc67d4256d2ab8638a02241ae7e25c58f54 Mon Sep 17 00:00:00 2001 From: Glucksberg Date: Mon, 26 Jan 2026 04:54:09 +0000 Subject: [PATCH 3/3] fix(telegram): thread accountId through native command context Native Telegram commands (slash menu) did not include AccountId in the message context, causing /reset confirmations to be delivered through the default Telegram bot instead of the account-specific bot. The session routing worked correctly (correct agent session was reset) but the confirmation message delivery used the wrong account because params.ctx.AccountId was undefined in handleCommands. Fixes #2038 --- src/telegram/bot-native-commands.ts | 2 ++ 1 file changed, 2 insertions(+) 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 =