Merge 4e860cc67d into da71eaebd2
This commit is contained in:
commit
70bf408251
@ -6,6 +6,23 @@ public enum ChatMarkdownVariant: String, CaseIterable, Sendable {
|
|||||||
case compact
|
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
|
@MainActor
|
||||||
struct ChatMarkdownRenderer: View {
|
struct ChatMarkdownRenderer: View {
|
||||||
enum Context {
|
enum Context {
|
||||||
@ -22,12 +39,20 @@ struct ChatMarkdownRenderer: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
let processed = ChatMarkdownPreprocessor.preprocess(markdown: self.text)
|
let processed = ChatMarkdownPreprocessor.preprocess(markdown: self.text)
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
StructuredText(markdown: processed.cleaned)
|
if textualBundleAvailable {
|
||||||
.modifier(ChatMarkdownStyle(
|
StructuredText(markdown: processed.cleaned)
|
||||||
variant: self.variant,
|
.modifier(ChatMarkdownStyle(
|
||||||
context: self.context,
|
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,
|
font: self.font,
|
||||||
textColor: self.textColor))
|
textColor: self.textColor)
|
||||||
|
}
|
||||||
|
|
||||||
if !processed.images.isEmpty {
|
if !processed.images.isEmpty {
|
||||||
InlineImageList(images: processed.images)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -25,11 +25,11 @@ type ParsedTtsCommand = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function parseTtsCommand(normalized: string): ParsedTtsCommand | null {
|
function parseTtsCommand(normalized: string): ParsedTtsCommand | null {
|
||||||
// Accept `/tts` and `/tts <action> [args]` as a single control surface.
|
// Accept `/tts <action> [args]` - return null for `/tts` alone to trigger inline menu.
|
||||||
if (normalized === "/tts") return { action: "status", args: "" };
|
if (normalized === "/tts") return null;
|
||||||
if (!normalized.startsWith("/tts ")) return null;
|
if (!normalized.startsWith("/tts ")) return null;
|
||||||
const rest = normalized.slice(5).trim();
|
const rest = normalized.slice(5).trim();
|
||||||
if (!rest) return { action: "status", args: "" };
|
if (!rest) return null;
|
||||||
const [action, ...tail] = rest.split(/\s+/);
|
const [action, ...tail] = rest.split(/\s+/);
|
||||||
return { action: action.toLowerCase(), args: tail.join(" ").trim() };
|
return { action: action.toLowerCase(), args: tail.join(" ").trim() };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -485,6 +485,8 @@ export const registerTelegramNativeCommands = ({
|
|||||||
// Originating context for sub-agent announce routing
|
// Originating context for sub-agent announce routing
|
||||||
OriginatingChannel: "telegram" as const,
|
OriginatingChannel: "telegram" as const,
|
||||||
OriginatingTo: `telegram:${chatId}`,
|
OriginatingTo: `telegram:${chatId}`,
|
||||||
|
// Multi-account: preserve accountId for command confirmations (e.g., /reset)
|
||||||
|
AccountId: route.accountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const disableBlockStreaming =
|
const disableBlockStreaming =
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user