From d1b5b6d68a0a194a4e280e2628b039d63929b989 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Mon, 19 Oct 2026 21:56:31 +0100 Subject: [PATCH] Add replies and emoji reactions to chat messages Every message has a row under it to reply or react. Replying quotes the message above the composer, and the sent message stores reply_to_message_id (a plain id, not a foreign key, so deleting the quoted message only drops the quote). send-message accepts a quote from any session of the same conversation, so a rollover or a thread that moved on still sends; the conversation drops a picked quote when its session changes. The bot reads the quote as "[Replying to ...]" before the user's turn, file-only replies included, and relayed messages carry it too. Reactions are a fixed set (BotMessageReactions) in bot_message_reactions, one per message, emoji and user or bot. Users toggle their own through api/admin/bots/react-message, and every bot gets react_to_message for the session it answers (the user's latest message by default). Both push the fresh list as server:bot-reaction, with mine worked out for the session's user. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 1 + db/schema.ts | 81 +++++++++++++++++ db/types/db.ts | 35 +++++++- .../bot-messages-to-harness.test.ts | 67 ++++++++++++++ src/bots-queue/bot-messages-to-harness.ts | 26 +++++- src/bots-queue/run-bot-task.ts | 2 +- src/dict/bot-dict.ts | 33 +++++++ .../bot-sessions/grab-conversation-props.ts | 2 +- .../db/bots/add-bot-message-reaction.ts | 31 +++++++ .../backend/db/bots/grab-bot-messages.ts | 38 ++++++-- .../db/bots/grab-latest-user-message.ts | 21 +++++ .../db/bots/grab-messages-reactions.ts | 70 +++++++++++++++ .../backend/db/bots/grab-messages-replies.ts | 35 ++++++++ .../backend/db/bots/grab-session-message.ts | 23 +++++ .../backend/db/bots/grab-user-bot-message.ts | 32 +++++++ .../backend/db/bots/insert-user-message.ts | 15 ++++ .../db/bots/toggle-message-reaction.ts | 35 ++++++++ src/harness/tools/bot/grab-bot-chat-tools.ts | 2 +- src/harness/tools/bot/grab-bot-tools.ts | 7 ++ .../tools/bot/react-to-message-tool.ts | 88 +++++++++++++++++++ src/live-relay/relay-new-bot-messages.ts | 20 ++++- .../(data)/message-actions-context.ts | 17 ++++ .../(functions)/react-to-bot-message.ts | 18 ++++ .../[bot_id]/(functions)/send-bot-message.ts | 3 +- .../[bot_id]/(partials)/bot-message-group.tsx | 5 +- .../[bot_id]/(partials)/bot-message-line.tsx | 49 +++++++---- .../(partials)/composer-reply-preview.tsx | 49 +++++++++++ .../bots/[bot_id]/(partials)/composer.tsx | 35 +++++++- .../(partials)/message-action-button.tsx | 29 ++++++ .../[bot_id]/(partials)/message-actions.tsx | 33 +++++++ .../(partials)/message-group-block.tsx | 2 +- .../(partials)/message-reaction-chip.tsx | 33 +++++++ .../(partials)/message-reaction-option.tsx | 27 ++++++ .../(partials)/message-reaction-picker.tsx | 52 +++++++++++ .../[bot_id]/(partials)/message-reactions.tsx | 31 +++++++ .../(partials)/message-reply-quote.tsx | 41 +++++++++ .../(partials)/user-message-group.tsx | 10 ++- .../bots/[bot_id]/(partials)/user-message.tsx | 42 +++++++-- .../bots/[bot_id]/(sections)/conversation.tsx | 59 ++++++++++++- src/pages/api/admin/bots/react-message.ts | 60 +++++++++++++ src/pages/api/admin/bots/send-message.ts | 24 ++++- src/pages/api/admin/bots/session-messages.ts | 1 + src/schemas/bot-schema.ts | 26 +++++- src/types/index.ts | 37 +++++++- 44 files changed, 1289 insertions(+), 58 deletions(-) create mode 100644 src/functions/backend/db/bots/add-bot-message-reaction.ts create mode 100644 src/functions/backend/db/bots/grab-latest-user-message.ts create mode 100644 src/functions/backend/db/bots/grab-messages-reactions.ts create mode 100644 src/functions/backend/db/bots/grab-messages-replies.ts create mode 100644 src/functions/backend/db/bots/grab-session-message.ts create mode 100644 src/functions/backend/db/bots/grab-user-bot-message.ts create mode 100644 src/functions/backend/db/bots/toggle-message-reaction.ts create mode 100644 src/harness/tools/bot/react-to-message-tool.ts create mode 100644 src/pages/admin/bots/[bot_id]/(data)/message-actions-context.ts create mode 100644 src/pages/admin/bots/[bot_id]/(functions)/react-to-bot-message.ts create mode 100644 src/pages/admin/bots/[bot_id]/(partials)/composer-reply-preview.tsx create mode 100644 src/pages/admin/bots/[bot_id]/(partials)/message-action-button.tsx create mode 100644 src/pages/admin/bots/[bot_id]/(partials)/message-actions.tsx create mode 100644 src/pages/admin/bots/[bot_id]/(partials)/message-reaction-chip.tsx create mode 100644 src/pages/admin/bots/[bot_id]/(partials)/message-reaction-option.tsx create mode 100644 src/pages/admin/bots/[bot_id]/(partials)/message-reaction-picker.tsx create mode 100644 src/pages/admin/bots/[bot_id]/(partials)/message-reactions.tsx create mode 100644 src/pages/admin/bots/[bot_id]/(partials)/message-reply-quote.tsx create mode 100644 src/pages/api/admin/bots/react-message.ts diff --git a/AGENTS.md b/AGENTS.md index 9a1e061..9ac6734 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,6 +92,7 @@ Inspect live data with `bun llm-code-assistant/query-db.ts ""` - a read-onl - **Routes:** `/admin/bots` ensures the coordinator and redirects the super admin to its chat (`/admin/bots/1` on a fresh install), anyone else to their latest bot, else their first workspace, else an empty page; `/admin/bots/[bot_id]` renders the conversation (header, thread and composer, the last two held to 760px; there is no right panel); `/admin/bots/[bot_id]/models` is a worker bot's own model settings, for super admins and admins (the coordinator's redirects to its chat); `/admin/workspaces/[workspace_id]` is a workspace's dashboard (`(sections)/workspace-dashboard.tsx`: read-only picture, name, description and context, latest activity, bots with their per-workspace instructions and memory, computers; its header links into `/admin/settings/workspaces/[workspace_id]`, whose `workspace-overview.tsx` has the editable groups) and `/admin/workspaces/[workspace_id]/bots/[bot_id]` is the same `Conversation` section scoped to the workspace (`pageProps.workspace`; only the coordinator and member bots). All render in the full-bleed `src/layouts/bots/` layout (rail of conversations + user popover), which `__root.tsx` uses for every `/admin/*` page. - **Server:** `src/functions/backend/db/bots/` (`grab-bots`, `grab-bot-messages`, `grab-active-bot-task`, `grab-user-bot`, `create-bot`, `insert-user-message`). `api/admin/bots/send-message.ts` posts a user message to a bot the session user chats with (zod: `src/schemas/bot-schema.ts`), queues the bot's reply (`enqueueBotReply`) and wakes the queue (`src/bots-queue/wake-bots-queue.ts`; never import `src/bots-queue/index.ts` from a page or API route, it pulls the bunext runtime into the page bundles). `upload-attachment.ts` stores one file for the next message (private `media` row, paradigm `bot-attachment`, type from `attachmentFileKind`, documents get `text_content`, limits in `AppData.BotAttachment*`) and `send-message` takes their `media_ids` (checked to be the user's own by `grabAttachableMedia`), linked in `bot_message_media`; messages carry `attachments` (`BotMessageWithAttachments`). `mark-read.ts` clears a session's unread when a message arrives in the open conversation; `session-messages.ts` returns a session's latest messages, which the open conversation polls every `AppData.BotReplyCatchUpMs` while a reply is due (`use-reply-catch-up`), so a reply whose live push was missed (socket down) still lands without a reload (replayed as a `server:bot-message`, so the rail and unread take it too); `create-bot.ts` adds a worker (and puts it in a workspace when given one); `save-harness-settings.ts` saves a worker bot's models. - **Message rendering:** bot messages are markdown, rendered with `react-markdown` + `remark-gfm` (raw HTML never rendered) through the twui-based map in `[bot_id]/(data)/message-markdown-components.tsx` (`markdown-link`, `markdown-list`, `markdown-code-block` partials). User messages stay plain text. +- **Replies and reactions:** every message has a row under it (`(partials)/message-actions.tsx`, handlers from `BotMessageActionsContext`) to reply or react. Replying quotes the message above the composer (`composer-reply-preview.tsx`) and the sent one carries `bot_messages.reply_to_message_id` (not a foreign key: a deleted quote only drops it); `send-message` takes any message of the same conversation (the user's, same bot and workspace, any session, so a rollover or a moved-on thread still sends), and the conversation drops a picked quote when its session changes. Messages carry `reply_to` (`grabMessagesReplies`, also on relayed messages), shown by `message-reply-quote.tsx`, and the bot reads it as `[Replying to ...]` before the user's turn (`botMessagesToHarness`). Reactions are a fixed set (`BotMessageReactions` in `src/dict/bot-dict.ts`) in `bot_message_reactions` (one row per message, emoji and user, or bot); `api/admin/bots/react-message.ts` toggles the user's own on a message in their sessions (`grabUserBotMessage`, `toggleMessageReaction`), and every bot gets `react_to_message` (`src/harness/tools/bot/react-to-message-tool.ts`, chat replies and tasks: a message in the session it answers, the user's latest by default). Both push the message's fresh reactions (`grabMessagesReactions`, `mine` for the session user) to the session's user as `server:bot-reaction` (`BotMessageReactionData`), which replaces the message's own; `grabBotMessages` adds them when given `user_id`. Reactions aren't relayed between processes (production runs one). - **Attachments and thinking:** the composer uploads files as they're picked, pasted or dropped (`use-composer-attachments`), shows them as chips, and sends once none is still uploading; the thread shows images as thumbnails and other files as chips. Replies read the files of the last `BotReplyAttachmentMessages` messages with any (`load-attachment-parts.ts`: images and audio as parts, documents as their text up to `BotAttachmentMaxTextChars`, videos as a note); older ones are only named. Tasks don't get the files yet. While the user has the last word in a session (every user message queues a reply, so this needs no face and shows even while the bot works in another session) or the bot waits for a model (`BotWaitingStatusLine`) or a task for its computer (`BotComputerBusyStatusLine`), `BotThinkingRow` sits under the last message; while a task runs it reads " is working…" with the task's latest step, and a reply working through tools shows its latest step under "is thinking…" (`use-reply-progress`). A coordinator with no workers gets `CoordinatorSoloNote` instead of `CoordinatorReplyNote`, so it never claims to have handed work to a worker. Live notices come from the process that ran the job at once, and from every other process through the live relay. - **Voice:** the composer's mic (`(partials)/composer-voice-button.tsx`, `use-voice-recorder`, shown once the browser can record, disabled with a note while transcription has no model to run on for the bot there, `bot_voice` page prop, from `resolveHarnessCandidates`) dictates: the recording goes to `api/admin/bots/transcribe.ts` (`transcribeAudio` with the bot's models, after `grabUserConversation`) and the text lands in the input at the cursor, to edit and send; Escape cancels, `AppData.ComposerVoiceMaxMs` stops it. Each bot message has a play button (disabled with a note while speech has no model) (`speak-message-button.tsx`, one player per conversation via `SpeakMessageContext` / `use-speak-message`) that plays `GET api/admin/bots/speak-message.ts?message_id=` straight in an `