Skip to content

feat: iMessage-style emoji reactions on desktop — opt-in, two-way, persistent, model-aware - #74533

Merged
OutThisLife merged 7 commits into
mainfrom
bb/emoji-reactions
Jul 30, 2026
Merged

OutThisLife merged 7 commits into
mainfrom
bb/emoji-reactions

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

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_message tool — quietly, like a person would, with the tool block suppressed from the transcript.

Reactions persist in the existing messages.display_metadata column (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's run_message only — 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.reaction event 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

  • 22 new suite tests (14 Python: tapback semantics, announce-once, row-id opt-in, cache-safety byte-identity; 8 TS: applyReaction transitions), all green
  • Full desktop vitest suite: 3842 passed / tsc renderer + electron: 0 errors / production build clean
  • E2E on a live gateway: react → persist → reload survives; agent reacts mid-turn → paints live and survives turn-end resume; annotation reaches the model exactly once; nothing leaks to the API wire
  • Exercised in the GUI end to end (react, retract, switch, retroactive messages_back, :shortcode: insert, offline picker)

@OutThisLife
OutThisLife requested a review from a team July 30, 2026 02:05
@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/desktop Electron desktop app (apps/desktop/*) comp/tools Tool registry, model_tools, toolsets comp/tui Terminal UI (ui-tui/ + tui_gateway/) area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have labels Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 745d138

all good!

@OutThisLife OutThisLife changed the title feat: iMessage-style emoji reactions on desktop — two-way, persistent, model-aware feat: iMessage-style emoji reactions on desktop — opt-in, two-way, persistent, model-aware Jul 30, 2026
@OutThisLife
OutThisLife force-pushed the bb/emoji-reactions branch 2 times, most recently from 01315bf to ecc1b81 Compare July 30, 2026 04:48
… 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
OutThisLife merged commit fa18306 into main Jul 30, 2026
48 checks passed
@OutThisLife
OutThisLife deleted the bb/emoji-reactions branch July 30, 2026 06:02
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/desktop Electron desktop app (apps/desktop/*) comp/tools Tool registry, model_tools, toolsets comp/tui Terminal UI (ui-tui/ + tui_gateway/) needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants