Summary
Agents reference each other's messages by the message id — the integer that store.py assigns to every message and that the MCP bridge hands back to agents (e.g. Sent to job #12 (msg_id=437), mcp_bridge.py). So an agent will say things like "see msg 437" or "re: message 437, …". But the web UI never renders that id, so as a human reading the chat I have no way to tell which message is "437." The number exists end-to-end on the agent side and is invisible on the human side.
It's already in the DOM, too — every bubble is built with el.dataset.id = msg.id in appendMessage (static/chat.js) — the UI just doesn't paint it.
Proposal
Three parts. The first two are small and self-contained (a draft PR is attached — see below); the third is a follow-up idea I'd love your read on before building.
1. Render #id in the chat bubble header.
Only on regular chat messages — the row that already shows sender / role / time (bubble-header in appendMessage). Join/leave, summaries, and proposals render differently and don't need it. The id shown is exactly msg.id, i.e. the same number agents cite, so they line up by construction.
2. A Settings toggle to show/hide it.
A "Msg numbers" control in the cog/settings panel (#settings-bar), next to Contrast — defaulting to Hide so it's opt-in and changes nothing for existing users. It mirrors the existing Contrast setting exactly: a body-class toggle (show-msg-numbers), persisted server-side through the update_settings round-trip in room_settings. The badge is always rendered in the DOM but hidden by CSS unless the body class is present, so toggling is pure presentation.
3. (Follow-up / for discussion) Make a referenced number a link that jumps to the message.
When an agent writes "msg 437," it'd be lovely to click it and scroll to that message. The infrastructure is already there:
scrollToMessage(id) (static/chat.js) already scrolls to a message and flashes a highlight — it's used today by reply-quotes and the pins panel.
- The text-render pipeline already does this kind of inline styling via
styleHashtags(renderMarkdown(...)). A linkifyMsgRefs() step in the same place would wrap matches in a clickable span calling scrollToMessage.
The catch — and the reason I'm raising it as a discussion rather than just shipping it — is that not every number is a message reference. "takes 5 minutes" or "port 8080" must not become links. Some options, roughly least → most magic:
- Require an explicit grammar. Only linkify a defined token, e.g.
msg 437, msg #437, or message 437 (regex like \bmsg(?:sage)?\s*#?\s*(\d+)\b). Prose numbers are left alone. Pairs well with documenting that convention in the README / agent system prompt so agents cite messages consistently.
- Repurpose
#NNN. A pure-numeric hashtag like #437 is currently not styled (styleHashtags requires a leading letter), so it's free to mean "message 437." Compact, but #437 is also a natural way to write other things.
- Validate against the DOM (recommended as a guard on whichever grammar). Only linkify when a message with that id actually exists on the page (
document.querySelector('.message[data-id="437"]')). Anything that doesn't resolve stays plain text. This kills almost all false positives regardless of grammar.
My instinct: explicit grammar + DOM-existence guard. Happy to follow your preference, and happy to split this into its own PR once the approach is settled — it also lightly touches the agent-facing convention, which is your call.
Implementation notes
- Setting persistence mirrors
contrast: default in room_settings (app.py), validated in the update_settings handler, broadcast back via broadcast_settings (whole room_settings dict, so it round-trips automatically), applied in applySettings / saveSettings / setupSettingsKeys (static/chat.js).
- Default off = non-breaking; no existing view changes until a user opts in. Easy to flip the default to on if you'd rather it be visible out of the box.
- Persistence choice is server-side to match the other settings in that panel; if you'd prefer a client-only (localStorage) toggle for a display-only pref, that's an easy swap.
Draft PR
I've prepared a draft PR for parts 1 + 2 (≈36 lines across app.py, static/chat.js, static/index.html, static/style.css) — happy to open it against main, or to adjust default/persistence first based on your preference. Part 3 I'd hold pending your read on the approach above.
Tested locally on v0.4.0: python -m py_compile app.py and node --check static/chat.js both pass; toggling Show/Hide flips the badges live and the setting survives a reload.
Summary
Agents reference each other's messages by the message id — the integer that
store.pyassigns to every message and that the MCP bridge hands back to agents (e.g.Sent to job #12 (msg_id=437),mcp_bridge.py). So an agent will say things like "see msg 437" or "re: message 437, …". But the web UI never renders that id, so as a human reading the chat I have no way to tell which message is "437." The number exists end-to-end on the agent side and is invisible on the human side.It's already in the DOM, too — every bubble is built with
el.dataset.id = msg.idinappendMessage(static/chat.js) — the UI just doesn't paint it.Proposal
Three parts. The first two are small and self-contained (a draft PR is attached — see below); the third is a follow-up idea I'd love your read on before building.
1. Render
#idin the chat bubble header.Only on regular chat messages — the row that already shows sender / role / time (
bubble-headerinappendMessage). Join/leave, summaries, and proposals render differently and don't need it. The id shown is exactlymsg.id, i.e. the same number agents cite, so they line up by construction.2. A Settings toggle to show/hide it.
A "Msg numbers" control in the cog/settings panel (
#settings-bar), next to Contrast — defaulting to Hide so it's opt-in and changes nothing for existing users. It mirrors the existing Contrast setting exactly: a body-class toggle (show-msg-numbers), persisted server-side through theupdate_settingsround-trip inroom_settings. The badge is always rendered in the DOM but hidden by CSS unless the body class is present, so toggling is pure presentation.3. (Follow-up / for discussion) Make a referenced number a link that jumps to the message.
When an agent writes "msg 437," it'd be lovely to click it and scroll to that message. The infrastructure is already there:
scrollToMessage(id)(static/chat.js) already scrolls to a message and flashes a highlight — it's used today by reply-quotes and the pins panel.styleHashtags(renderMarkdown(...)). AlinkifyMsgRefs()step in the same place would wrap matches in a clickable span callingscrollToMessage.The catch — and the reason I'm raising it as a discussion rather than just shipping it — is that not every number is a message reference. "takes 5 minutes" or "port 8080" must not become links. Some options, roughly least → most magic:
msg 437,msg #437, ormessage 437(regex like\bmsg(?:sage)?\s*#?\s*(\d+)\b). Prose numbers are left alone. Pairs well with documenting that convention in the README / agent system prompt so agents cite messages consistently.#NNN. A pure-numeric hashtag like#437is currently not styled (styleHashtagsrequires a leading letter), so it's free to mean "message 437." Compact, but#437is also a natural way to write other things.document.querySelector('.message[data-id="437"]')). Anything that doesn't resolve stays plain text. This kills almost all false positives regardless of grammar.My instinct: explicit grammar + DOM-existence guard. Happy to follow your preference, and happy to split this into its own PR once the approach is settled — it also lightly touches the agent-facing convention, which is your call.
Implementation notes
contrast: default inroom_settings(app.py), validated in theupdate_settingshandler, broadcast back viabroadcast_settings(wholeroom_settingsdict, so it round-trips automatically), applied inapplySettings/saveSettings/setupSettingsKeys(static/chat.js).Draft PR
I've prepared a draft PR for parts 1 + 2 (≈36 lines across
app.py,static/chat.js,static/index.html,static/style.css) — happy to open it againstmain, or to adjust default/persistence first based on your preference. Part 3 I'd hold pending your read on the approach above.Tested locally on v0.4.0:
python -m py_compile app.pyandnode --check static/chat.jsboth pass; toggling Show/Hide flips the badges live and the setting survives a reload.