This commit is contained in:
Glucksberg 2026-01-30 14:48:39 +00:00 committed by GitHub
commit 70bf408251
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 65 additions and 8 deletions

View File

@ -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)
}

View File

@ -25,11 +25,11 @@ type ParsedTtsCommand = {
};
function parseTtsCommand(normalized: string): ParsedTtsCommand | null {
// Accept `/tts` and `/tts <action> [args]` as a single control surface.
if (normalized === "/tts") return { action: "status", args: "" };
// Accept `/tts <action> [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() };
}

View File

@ -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 =