From 9d991bb5f562d6d62bbdc8d60dba8fa5ab52d5b1 Mon Sep 17 00:00:00 2001 From: Clawdbot Date: Mon, 26 Jan 2026 00:15:21 +0100 Subject: [PATCH 1/2] fix(macos): menu bar activity badge not showing during agent work The gateway emits agent events with stream='lifecycle' and data.phase ('start'/'end'/'error'), but ControlChannel.swift was looking for stream='job' and data.state ('started'/'streaming'/etc). This mismatch caused the menu bar to always show 'Idle' even when the agent was actively working. Changes: - ControlChannel.swift: Handle 'lifecycle' stream instead of 'job', map phase values to the states WorkActivityStore expects - AgentEventsWindow.swift: Update stream color mapping for consistency Fixes #1932 --- apps/macos/Sources/Moltbot/AgentEventsWindow.swift | 2 +- apps/macos/Sources/Moltbot/ControlChannel.swift | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) 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": From d0eb74195ae6e17202f968c7ce2fb6dcaa393b42 Mon Sep 17 00:00:00 2001 From: Clawdbot Date: Mon, 26 Jan 2026 00:17:49 +0100 Subject: [PATCH 2/2] fix(macos): prevent crash when opening chat window The app crashes when opening the chat window because the Textual package's resource bundle (textual_Textual.bundle) isn't properly embedded in release builds. When StructuredText tries to access Bundle.module, it causes a fatal assertion failure. This fix adds a runtime check to detect if the Textual bundle is available before attempting to use StructuredText. If the bundle is missing, the chat falls back to plain text rendering instead of crashing. The bundle availability check: - Looks for textual_Textual.bundle in the main bundle - Checks PlugIns and Frameworks directories - Defaults to available in DEBUG builds (development) - Falls back to plain Text view when unavailable Fixes #1841 Related: #1640 (same root cause) --- .../MoltbotChatUI/ChatMarkdownRenderer.swift | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) 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)