Summary
When a user reacts with π (thumbs up) to an exec approval-pending message, the system should automatically resolve the approval as allow-always. When the user reacts with π (thumbs down), it should resolve as deny.
Motivation
Currently, approving an exec request requires typing /approve <id> allow-always β a 3-step process of:
- Finding the approval ID in the message
- Typing the full command
- Waiting for the resolution
A single tapback reaction is much faster and more natural for mobile users (especially iMessage via BlueBubbles).
Technical Approach
How tapbacks arrive (BlueBubbles)
In extensions/bluebubbles/src/monitor-processing.ts, tapbacks are already detected via parseTapbackText() and resolveTapbackContext(). A π reaction on a message produces:
rawBody = "reacted with π"
replyToId = <uuid of replied-to message>
replyToBody = <body of the message being reacted to>
Detection logic
When isTapbackMessage === true AND the replied-to message body matches /Approval required \(id ([a-f0-9]{8})\)/, extract the slug and:
π (liked) β call exec.approval.resolve with { id: slug, decision: "allow-always" }
π (disliked) β call exec.approval.resolve with { id: slug, decision: "deny" }
- Skip dispatching the tapback to the agent (return early, no session event)
Implementation sketch
// In monitor-processing.ts, after resolving tapbackParsed + replyToBody:
if (isTapbackMessage && tapbackParsed && replyToBody) {
const approvalMatch = replyToBody.match(/Approval required \(id ([a-f0-9]{8,})\)/);
if (approvalMatch) {
const approvalSlug = approvalMatch[1];
const emoji = tapbackParsed.emoji;
const decision =
emoji === "π" ? "allow-always" :
emoji === "π" ? "deny" :
null;
if (decision) {
await callGateway({
method: "exec.approval.resolve",
params: { id: approvalSlug, decision },
...
});
return; // skip agent dispatch
}
}
}
Generalization
The same pattern applies to other channels that support reactions/tapbacks (Telegram reaction events, Discord reaction events). A shared helper in src/infra/exec-approval-reaction.ts could handle channel-agnostic reactionβapproval mapping.
Acceptance Criteria
Related
src/auto-reply/reply/commands-approve.ts β text-based /approve command
src/agents/bash-tools.exec.ts β where approval slugs are generated (createApprovalSlug)
src/gateway/server-methods/exec-approval.ts β exec.approval.resolve handler
Summary
When a user reacts with π (thumbs up) to an exec approval-pending message, the system should automatically resolve the approval as
allow-always. When the user reacts with π (thumbs down), it should resolve asdeny.Motivation
Currently, approving an exec request requires typing
/approve <id> allow-alwaysβ a 3-step process of:A single tapback reaction is much faster and more natural for mobile users (especially iMessage via BlueBubbles).
Technical Approach
How tapbacks arrive (BlueBubbles)
In
extensions/bluebubbles/src/monitor-processing.ts, tapbacks are already detected viaparseTapbackText()andresolveTapbackContext(). A π reaction on a message produces:Detection logic
When
isTapbackMessage === trueAND the replied-to message body matches/Approval required \(id ([a-f0-9]{8})\)/, extract the slug and:π(liked) β callexec.approval.resolvewith{ id: slug, decision: "allow-always" }π(disliked) β callexec.approval.resolvewith{ id: slug, decision: "deny" }Implementation sketch
Generalization
The same pattern applies to other channels that support reactions/tapbacks (Telegram reaction events, Discord reaction events). A shared helper in
src/infra/exec-approval-reaction.tscould handle channel-agnostic reactionβapproval mapping.Acceptance Criteria
allow-always, no agent dispatchdeny, no agent dispatchmonitor-processing.test.ts(or similar)exec.reactions.enabled: booleanto disable if unwantedRelated
src/auto-reply/reply/commands-approve.tsβ text-based/approvecommandsrc/agents/bash-tools.exec.tsβ where approval slugs are generated (createApprovalSlug)src/gateway/server-methods/exec-approval.tsβexec.approval.resolvehandler