diff --git a/apps/macos/Sources/Moltbot/AgentEventsWindow.swift b/apps/macos/Sources/Moltbot/AgentEventsWindow.swift index 8e2f43eb1..32f4bab75 100644 --- a/apps/macos/Sources/Moltbot/AgentEventsWindow.swift +++ b/apps/macos/Sources/Moltbot/AgentEventsWindow.swift @@ -67,7 +67,7 @@ private struct EventRow: View { private var tint: Color { switch self.event.stream { - case "job": .blue + case "lifecycle": .blue case "tool": .orange case "assistant": .green default: .gray diff --git a/apps/macos/Sources/Moltbot/ControlChannel.swift b/apps/macos/Sources/Moltbot/ControlChannel.swift index 2af7c721d..093e89983 100644 --- a/apps/macos/Sources/Moltbot/ControlChannel.swift +++ b/apps/macos/Sources/Moltbot/ControlChannel.swift @@ -379,8 +379,11 @@ final class ControlChannel { let sessionKey = (event.data["sessionKey"]?.value as? String) ?? "main" switch event.stream.lowercased() { - case "job": - if let state = event.data["state"]?.value as? String { + case "lifecycle": + // Gateway emits phase: "start" | "end" | "error" + // WorkActivityStore expects state: "started" | "streaming" | (anything else = done) + if let phase = event.data["phase"]?.value as? String { + let state = phase.lowercased() == "start" ? "started" : "done" WorkActivityStore.shared.handleJob(sessionKey: sessionKey, state: state) } case "tool": diff --git a/apps/shared/MoltbotKit/Sources/MoltbotChatUI/ChatMarkdownRenderer.swift b/apps/shared/MoltbotKit/Sources/MoltbotChatUI/ChatMarkdownRenderer.swift index 713097f17..b5981c744 100644 --- a/apps/shared/MoltbotKit/Sources/MoltbotChatUI/ChatMarkdownRenderer.swift +++ b/apps/shared/MoltbotKit/Sources/MoltbotChatUI/ChatMarkdownRenderer.swift @@ -6,6 +6,43 @@ public enum ChatMarkdownVariant: String, CaseIterable, Sendable { case compact } +/// Check if the Textual package's resource bundle is available. +/// When running from a packaged app, the bundle may not be in the expected location, +/// causing a fatal error when StructuredText tries to access Bundle.module. +private let textualBundleAvailable: Bool = { + // The Textual package looks for its bundle at specific paths. + // Check if we can find it before attempting to use StructuredText. + let bundleName = "textual_Textual.bundle" + + // Check in main bundle (packaged app) + if Bundle.main.path(forResource: "textual_Textual", ofType: "bundle") != nil { + return true + } + + // Check in app's PlugIns or Frameworks directories + if let pluginsURL = Bundle.main.builtInPlugInsURL, + FileManager.default.fileExists(atPath: pluginsURL.appendingPathComponent(bundleName).path) + { + return true + } + + if let frameworksURL = Bundle.main.privateFrameworksURL, + FileManager.default.fileExists(atPath: frameworksURL.appendingPathComponent(bundleName).path) + { + return true + } + + // In development (SwiftPM), Bundle.module should work + // We can't directly test Bundle.module without triggering the crash, + // so we check for a known development path pattern + #if DEBUG + return true + #else + // In release builds, if we haven't found the bundle, assume it's not available + return false + #endif +}() + @MainActor struct ChatMarkdownRenderer: View { enum Context { @@ -22,12 +59,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, - font: self.font, - textColor: self.textColor)) + if textualBundleAvailable { + StructuredText(markdown: processed.cleaned) + .modifier(ChatMarkdownStyle( + variant: self.variant, + context: self.context, + font: self.font, + textColor: self.textColor)) + } else { + // Fallback: render as plain text when Textual bundle is unavailable + Text(processed.cleaned) + .font(self.font) + .foregroundStyle(self.textColor) + .textSelection(.enabled) + } if !processed.images.isEmpty { InlineImageList(images: processed.images)