feat: iMessage-style emoji reactions on desktop — opt-in, two-way, persistent, model-aware - #74533
Merged
Merged
Conversation
Contributor
૮ >ﻌ< ა ci reviewran on 745d138 all good! |
OutThisLife
force-pushed
the
bb/emoji-reactions
branch
2 times, most recently
from
July 30, 2026 04:48
01315bf to
ecc1b81
Compare
… tool, model context Reactions live in the existing messages.display_metadata JSON column (no new table), with iOS Tapback semantics enforced DB-side: one reaction per author per message, re-tap retracts, different emoji replaces. The desktop catches up to the reaction contract five platform adapters already ship. - SessionDB: set/get_message_reaction, latest_message_row_id (role + offset + require_text so invisible tool-call-only rows are never targeted), take_unseen_reactions (announce-exactly-once), get_message_role - message.react RPC: accepts row_id or newest_role for live messages that haven't learned their durable id yet - react_to_message tool: desktop-gated (check_fn), defaults to the user's latest visible message, messages_back for retroactive reactions - Model context rides run_message only (beside the speech-interrupted note): the persisted prompt stays clean, so no [The user reacted …] scaffolding in transcripts, and no cached prefix ever changes - Resume projection forwards row_id + reactions; _row_id is stripped from outgoing API copies next to display_metadata
…live paint One slot, Slack-style: on assistant rows the picker trigger and the landed reaction are the same far-right element, so reacting never shifts layout (empty → ☺ following the action bar's hover fade; reacted → the emoji, always visible, always full-strength). User bubbles react via right-click and show the badge beneath, in the checkpoint row's register. - Clicks paint instantly from a local nanostores overlay — no round-trip in the loop; the RPC persists behind it and rolls back visibly on rejection - Agent reactions land via the message.reaction event into an overlay keyed by DURABLE row id, so the end-of-turn resume (which regenerates renderer ids and rebuilds from in-memory history) can't clobber the paint - Full picker is frimousse behind the six-emoji quick row, fed from bundled emojibase-data served at ./emojibase by a small vite plugin (offline, no CDN), with Slack-style alternating cell tints keyed off the codepoint - Reaction picker opts out of the shared popover glass: solid surface so 15%-alpha hover tints stay readable - react_to_message tool blocks are suppressed in the transcript (like todo): the reaction appearing IS the UI; failures still render - rowId reaches rehydrated messages from both transcript shapes (gateway row_id, REST numeric id)
A third trigger kind beside @ and / — same detection, same popover, same commit path. :jo opens 😂/🤣/… fed by the bundled emojibase shortcode data (search hits shortcodes first, then tags and labels); picking inserts the emoji character as plain inline text, not a chip. Boundary-anchored with a two-char minimum so localhost:8080, timestamps, and :D never trigger it. Wired in the main composer and the edit composer's duplicated trigger loop.
CI caught ACP session restore seeing an unexpected _row_id in restored history — get_messages_as_conversation feeds more than the desktop, and changing the default shape broke the strictest consumer. Row ids are now include_row_ids=True, requested only by the gateway's resume/display projections; ACP restore, export, and inspection get the transcript in its historical shape.
…by default One lever, every surface. The renderer toggle persists locally and mirrors into display.message_reactions; the backend gates the agent's react_to_message tool (check_fn) and the model-context annotation on the same key, and the ':' composer trigger reads the store at detection time. Off means off everywhere: no ☺ slot, no right-click picker, no :shortcode: popover, no agent reactions, and the model hears nothing — while reactions already persisted keep rendering so history doesn't lose data. Also fixes the import-order lint error CI flagged in composer/index.tsx.
Slice 1 fell over on eight DB fakes with frozen get_messages_as_conversation signatures — the new opt-in kwarg is part of the method's contract now, so the fakes accept **_kwargs like the real SessionDB. Also opts the child-watch resume projection into row ids: it feeds the same _history_to_messages as the desktop resume, so reactions on a watched child session address rows the same way.
…other platforms npm install on macOS dropped all 26 @esbuild/* cross-platform entries (443 lines) — that breaks Linux/Windows installs. Restored main's lockfile and merged in only the three genuinely new entries.
OutThisLife
force-pushed
the
bb/emoji-reactions
branch
from
July 30, 2026 05:12
ecc1b81 to
745d138
Compare
kshitijk4poor
approved these changes
Jul 30, 2026
4 tasks
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…tions feat: iMessage-style emoji reactions on desktop — opt-in, two-way, persistent, model-aware
33hodl
pushed a commit
to 33hodl/hermes-agent
that referenced
this pull request
Aug 12, 2026
…tions feat: iMessage-style emoji reactions on desktop — opt-in, two-way, persistent, model-aware
prmartinow
pushed a commit
to prmartinow/hermes-agent
that referenced
this pull request
Aug 26, 2026
…tions feat: iMessage-style emoji reactions on desktop — opt-in, two-way, persistent, model-aware
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
…tions feat: iMessage-style emoji reactions on desktop — opt-in, two-way, persistent, model-aware
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings message reactions to the desktop app — the surface catching up to a contract five platform adapters already ship (photon tapbacks, Telegram setMessageReaction, Slack/Matrix/Feishu/Discord inbound). Opt-in and off by default: one toggle under Settings → Appearance drives every surface — the renderer persists it locally and mirrors it into
display.message_reactions, which also gates the agent-side tool (check_fn) and the model-context annotation. Off means off everywhere; already-persisted reactions keep rendering so history loses nothing.When enabled, it's two-way: the user tapbacks any message (right-click a sent bubble, or the ☺ slot on assistant rows), and the agent can react to the user via a desktop-gated
react_to_messagetool — quietly, like a person would, with the tool block suppressed from the transcript.Reactions persist in the existing
messages.display_metadatacolumn (no new table) with iOS Tapback semantics enforced DB-side: one reaction per author per message, re-tap retracts, different emoji replaces. The user's reactions reach the model as an annotation riding the next turn'srun_messageonly — the persisted prompt stays clean (no scaffolding in transcripts) and no already-sent message ever changes, so cached prefixes survive. Durable row ids are opt-in per consumer (include_row_ids=True, requested by the gateway's resume projections only), so ACP restore and export see the transcript in its historical shape.The UI is one Slack-style slot: the picker trigger and the landed reaction are the same far-right element, so reacting never shifts layout. Clicks paint instantly from a local overlay (no round-trip in the loop); agent reactions arrive via a
message.reactionevent keyed by durable row id, so the end-of-turn resume can't clobber the paint. The full picker is frimousse fed from bundled emojibase data — offline, no CDN — with Slack-style alternating cell tints. Riding the same data and the same toggle::shortcode:completions (:jo→ 😂) in both composers as a third trigger kind beside@and/.Test plan
messages_back,:shortcode:insert, offline picker)