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. sendMessageIMessage → extractMarkdownFormatRuns() 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
Context
Follow-up to #85952 (iMessage thumb approval reactions). The approval payload text comes from the SDK's
buildExecApprovalPendingReplyPayload/buildPluginApprovalPendingReplyPayload, which emit plain text.sendMessageIMessagealready runsextractMarkdownFormatRuns()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.:
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:
**ℹ️ Plugin approval required**/**⚠️ Exec approval required**).**Title:**,**Tool:**,**Plugin:**,**Agent:**,**ID:**).React with:heading and the action labels (**👍 Allow Once**,**👎 Deny**).Reply with: /approve …line so it visually reads as the manual-fallback command.Implementation sketch
The cleanest seam is the iMessage
renderblock inextensions/imessage/src/approval-native.ts:buildIMessageExecPendingPayloadbuildIMessagePluginPendingPayloadappendIMessageReactionHint/addIMessageApprovalReactionHintToTextThese 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.sendMessageIMessage→extractMarkdownFormatRuns()already handles the rest.For the forwarding (
mode: \"targets\") path, the SDK builds the payload on the gateway side and the channel send still runsextractMarkdownFormatRuns(), so the iMessage-side wrap of the rendered text inapproval-native.tscovers both forwarding and native delivery.Non-goals
Test plan
extensions/imessage/src/approval-native.test.tscase 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.extractMarkdownFormatRuns()behavior.Related