Skip to content

iMessage: use attributed-body formatting for approval prompts (bold headers and labels) #85954

@omarshahine

Description

@omarshahine

Context

Follow-up to #85952 (iMessage thumb approval reactions). The approval payload text comes from the SDK's buildExecApprovalPendingReplyPayload / buildPluginApprovalPendingReplyPayload, which emit plain text. sendMessageIMessage already runs extractMarkdownFormatRuns() over outbound text and ships native attributed-body runs to recipients on macOS 15+ / iOS 18+, so any **bold** / __italic__ / ~~strike~~ / <u>underline</u> markers in the payload render natively on iMessage.

Today the approval prompts render as plain text on iMessage, e.g.:

ℹ️ Plugin approval required
Title: Fastmail: get_email
Description: Read email body (untrusted content)
Tool: fastmail_get_email
Plugin: fastmail-cli
Agent: lobster-mail
ID: plugin:8d5075f3-a972-4190-97d9-8b3c1b921b06

React with:

👍 Allow Once
👎 Deny

Expires in: 120s
Reply with: /approve plugin:8d5075f3-… allow-once|allow-always|deny

Proposal

Layer iMessage-specific markdown around the payload returned by the SDK so the header line and field labels render bold / underlined on recent Apple clients, while keeping the SDK output channel-agnostic.

Candidate enhancement points:

  • Bold the header line (**ℹ️ Plugin approval required** / **⚠️ Exec approval required**).
  • Bold the field labels (**Title:**, **Tool:**, **Plugin:**, **Agent:**, **ID:**).
  • Bold the React with: heading and the action labels (**👍 Allow Once**, **👎 Deny**).
  • Possibly underline the Reply with: /approve … line so it visually reads as the manual-fallback command.

Implementation sketch

The cleanest seam is the iMessage render block in extensions/imessage/src/approval-native.ts:

  • buildIMessageExecPendingPayload
  • buildIMessagePluginPendingPayload
  • appendIMessageReactionHint / addIMessageApprovalReactionHintToText

These currently call the SDK payload builders, run replaceApprovalIdPlaceholder, then append/insert the plain-text reaction hint. Wrap the targeted lines in **…** after the SDK call but before the channel send. sendMessageIMessageextractMarkdownFormatRuns() already handles the rest.

For the forwarding (mode: \"targets\") path, the SDK builds the payload on the gateway side and the channel send still runs extractMarkdownFormatRuns(), so the iMessage-side wrap of the rendered text in approval-native.ts covers both forwarding and native delivery.

Non-goals

  • Inline interactive buttons (iMessage has no equivalent UI).
  • Changing the SDK's payload format — keep markdown out of the canonical text so other channels (Slack, Discord, etc.) aren't affected.
  • Link cards or rich previews.

Test plan

  • Add an extensions/imessage/src/approval-native.test.ts case asserting the iMessage exec/plugin payloads contain **ID:** (or whichever label is targeted) so a future SDK rendering change doesn't silently drop the formatting.
  • Live verification: trigger a plugin approval to an iMessage approver on macOS 15+ / iOS 18+, confirm the header / labels render bold; trigger one to an older-OS recipient (or via SMS bridge) and confirm the markers strip cleanly to plain text per the existing extractMarkdownFormatRuns() behavior.

Related

Metadata

Metadata

Assignees

Labels

maintainerMaintainer-authored PR

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions