From 01e62198fa1727510a1d30a4bdbb9f90329da240 Mon Sep 17 00:00:00 2001 From: Omar Shahine <10343873+omarshahine@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:23:29 -0700 Subject: [PATCH 1/2] fix: render text formatting on the RPC `send` bridge path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleSend forwarded text and file to the IMCore bridge but never the attributed-text format ranges, so direct/handle sends came through plain even on macOS 15+ — only `send-rich` (and the RPC `sendRich` handler) honored formatting. The OpenClaw gateway's `message` tool uses the plain `send` method for handle targets, so its bold/italic notifications silently lost styling. Thread the format ranges through handleSend -> sendViaBridge -> the bridge sendMessage action, accepting `formatting` (the key OpenClaw emits) alongside `text_formatting`/`textFormatting` for parity with `sendRich`. Bridge transport only; AppleScript sends stay plain. Adds tests covering the threaded ranges and the no-formatting case. --- CHANGELOG.md | 3 ++ Sources/imsg/RPCServer+Handlers.swift | 18 +++++-- Tests/imsgTests/RPCServerBridgeTests.swift | 56 ++++++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5d77ad..bf99d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.11.2 - Unreleased +### Send +- fix: thread attributed-text formatting through the RPC `send` bridge path, not just `send-rich`, so direct/handle sends render **bold**/*italic*/etc. on macOS 15+. `handleSend` now forwards format ranges to the bridge, accepting `formatting` (the key OpenClaw's `message` tool emits) alongside `text_formatting`/`textFormatting`. + ## 0.11.1 - 2026-06-10 ### Read Commands diff --git a/Sources/imsg/RPCServer+Handlers.swift b/Sources/imsg/RPCServer+Handlers.swift index a462ef7..5b5417e 100644 --- a/Sources/imsg/RPCServer+Handlers.swift +++ b/Sources/imsg/RPCServer+Handlers.swift @@ -167,6 +167,12 @@ extension RPCServer { func handleSend(params: [String: Any], id: Any?) async throws { let text = stringParam(params["text"]) ?? "" let file = stringParam(params["file"]) ?? "" + // Optional attributed-text formatting (bold/italic/…, macOS 15+). Only the + // IMCore bridge transport can render it; AppleScript sends stay plain. + // Accept `text_formatting`/`textFormatting` (matching `send-rich`) plus the + // bare `formatting` key that the OpenClaw gateway emits on its `send` calls. + let textFormatting = + params["text_formatting"] ?? params["textFormatting"] ?? params["formatting"] let serviceRaw = stringParam(params["service"]) ?? "auto" guard let service = MessageService(rawValue: serviceRaw) else { throw RPCError.invalidParams("invalid service") @@ -262,7 +268,8 @@ extension RPCServer { let data = try await sendViaBridge( chatGUID: bridgeChatGUID, text: text, - file: file + file: file, + textFormatting: textFormatting ) var result: [String: Any] = ["ok": true, "transport": "bridge"] if let guid = data["messageGuid"] as? String, !guid.isEmpty { @@ -521,7 +528,8 @@ extension RPCServer { private func sendViaBridge( chatGUID: String, text: String, - file: String + file: String, + textFormatting: Any? = nil ) async throws -> [String: Any] { if !file.isEmpty { guard text.isEmpty else { @@ -533,7 +541,11 @@ extension RPCServer { ["chatGuid": chatGUID, "filePath": stagedFile, "isAudioMessage": false] ) } - return try await bridgeInvoker(.sendMessage, ["chatGuid": chatGUID, "message": text]) + var messageParams: [String: Any] = ["chatGuid": chatGUID, "message": text] + if let textFormatting { + messageParams["textFormatting"] = textFormatting + } + return try await bridgeInvoker(.sendMessage, messageParams) } } diff --git a/Tests/imsgTests/RPCServerBridgeTests.swift b/Tests/imsgTests/RPCServerBridgeTests.swift index 21e6111..8a6a24d 100644 --- a/Tests/imsgTests/RPCServerBridgeTests.swift +++ b/Tests/imsgTests/RPCServerBridgeTests.swift @@ -40,6 +40,62 @@ func rpcSendUsesBridgeWhenReadyAndExistingDirectChatResolves() async throws { #expect(result?["service"] as? String == "iMessage") } +@Test +func rpcSendThreadsTextFormattingToBridge() async throws { + let store = try CommandTestDatabase.makeStoreForRPCDirectChat() + let output = TestRPCOutput() + var capturedParams: [String: Any] = [:] + let server = RPCServer( + store: store, + verbose: false, + output: output, + sendMessage: { _ in }, + resolveSentMessage: { _, _, _, _ in nil }, + invokeBridge: { _, params in + capturedParams = params + return ["messageGuid": "bridge-guid", "chatGuid": "iMessage;-;+123", "service": "iMessage"] + }, + isBridgeReady: { true } + ) + + // The OpenClaw gateway emits format ranges under the bare `formatting` key. + let line = #""" + {"jsonrpc":"2.0","id":"3fmt","method":"send","params":{"to":"+123","text":"hello world","formatting":[{"start":0,"length":5,"styles":["bold"]}]}} + """# + await server.handleLineForTesting(line) + + let ranges = capturedParams["textFormatting"] as? [[String: Any]] + #expect(ranges?.count == 1) + #expect(ranges?.first?["start"] as? Int == 0) + #expect(ranges?.first?["length"] as? Int == 5) + #expect(ranges?.first?["styles"] as? [String] == ["bold"]) + #expect(capturedParams["message"] as? String == "hello world") +} + +@Test +func rpcSendWithoutFormattingOmitsTextFormatting() async throws { + let store = try CommandTestDatabase.makeStoreForRPCDirectChat() + let output = TestRPCOutput() + var capturedParams: [String: Any] = [:] + let server = RPCServer( + store: store, + verbose: false, + output: output, + sendMessage: { _ in }, + resolveSentMessage: { _, _, _, _ in nil }, + invokeBridge: { _, params in + capturedParams = params + return ["messageGuid": "bridge-guid", "chatGuid": "iMessage;-;+123", "service": "iMessage"] + }, + isBridgeReady: { true } + ) + + let line = #"{"jsonrpc":"2.0","id":"3nofmt","method":"send","params":{"to":"+123","text":"yo"}}"# + await server.handleLineForTesting(line) + + #expect(capturedParams["textFormatting"] == nil) +} + @Test func rpcSendAutoSMSDetectionKeepsAnyPrefixBridgeLookup() async throws { let store = try CommandTestDatabase.makeStoreForRPCDirectChat() From 282fc8fcc8a49572211434e6c9d7c3a1925a1cfe Mon Sep 17 00:00:00 2001 From: Omar Shahine <10343873+omarshahine@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:24:14 -0700 Subject: [PATCH 2/2] docs: reference #143 in changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf99d4a..9eb7e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 0.11.2 - Unreleased ### Send -- fix: thread attributed-text formatting through the RPC `send` bridge path, not just `send-rich`, so direct/handle sends render **bold**/*italic*/etc. on macOS 15+. `handleSend` now forwards format ranges to the bridge, accepting `formatting` (the key OpenClaw's `message` tool emits) alongside `text_formatting`/`textFormatting`. +- fix: thread attributed-text formatting through the RPC `send` bridge path, not just `send-rich`, so direct/handle sends render **bold**/*italic*/etc. on macOS 15+. `handleSend` now forwards format ranges to the bridge, accepting `formatting` (the key OpenClaw's `message` tool emits) alongside `text_formatting`/`textFormatting` (#143, thanks @omarshahine). ## 0.11.1 - 2026-06-10