Skip to content

Add chat_edit() - #11

Merged
TroyHernandez merged 2 commits into
mainfrom
edits
Aug 7, 2026
Merged

TroyHernandez merged 2 commits into
mainfrom
edits

Conversation

@TroyHernandez

Copy link
Copy Markdown
Contributor

Groundwork for a progress message in a room: post once, then keep replacing it, instead of narrating into the channel one message per tool call. corteza's room surface currently emits typing-on, a wall of prose, typing-off — there is no way to show work as it happens without this.

chat_edit(client, channel, message_id, text, markup = c("plain", "markdown"))

Matrix

Builds m.new_content plus an m.replace relation on top of mx_send()'s extra hook, so no mx.api change was needed.

The fallback body keeps the conventional * prefix. A client too old to understand edits renders the event as an ordinary message, and the asterisk is what tells a reader it is a correction rather than the bot repeating itself.

Markdown renders into both copies. Carrying it in only one shows raw markup on old clients and loses it on new ones — exactly backwards.

Encrypted rooms refuse

edits = FALSE on an e2ee client, the same bargain attachments get. An edit carries its replacement text in an ordinary event, so sending one puts it on the homeserver in the clear, in the room whose whole point is that it is not. The Megolm path cannot carry it either: crypto_ops$send() takes text, msgtype, markdown and mentions, with nowhere to put a relation.

The default throws

Stale text is worse than a visible failure. A reader cannot tell that what they are looking at is no longer true, so an edit that quietly did nothing is the one failure mode with no symptom.

Slack, loopback, IRC

Slack maps to chat.update and returns the same ts it was given; Matrix mints a new event for the edit and returns that. The contract promises "the id of the thing that happened", not that adapters agree on which thing that is — documented on the generic.

Loopback replaces in place and raises on an unknown id: a consumer editing a message it never sent has lost track of itself, and the reference adapter is where that should be loudest.

IRC has none — a line is on the wire and gone.

Verification

881/881.

What this does not do yet

The consumer. Rendering an activity trail (Ran 3 commands, read 2 files › with <details><summary>, diff stats, PR chips) is corteza-side work and lands separately.

The foundation for a progress message: post once, then keep replacing
it, instead of narrating into the channel one message per tool call.

Matrix builds m.new_content plus an m.replace relation on top of
mx_send()'s extra hook -- no mx.api change needed. The fallback body
keeps the conventional "* " prefix, because a client too old to
understand edits renders the event as an ordinary message and the
asterisk is what tells a reader it is a correction rather than the bot
repeating itself. Markdown renders into both copies; carrying it in only
one shows markup on old clients and loses it on new ones.

Encrypted rooms refuse, and capabilities report edits = FALSE on an e2ee
client to match -- the same bargain attachments get. An edit carries its
replacement text in an ordinary event, and crypto_ops$send() takes text,
msgtype, markdown and mentions with nowhere to put a relation, so the
Megolm path cannot carry one either.

The default throws rather than returning quietly. Stale text is worse
than a visible failure: a reader cannot tell that what they are looking
at is no longer true.
A room can now be shown a collapsible <details> block, which is the
whole reason chat_edit() exists -- a progress trail nobody can collapse
is a wall of text that grows.

There was no way to express one. markup = "markdown" renders through
mx_markdown_to_html(), which escapes raw HTML: correct for a
conservative subset, and a dead end for a caller that has markup the
subset cannot reach.

So `rich` carries a fragment the adapter passes through as
formatted_body, and `text` stays required as the body. That is Matrix's
own model, and it is load-bearing rather than tidy: text is what a
client too old to render the markup shows, what a push notification
carries, and what every other transport gets.

Ignored rather than refused where unsupported, on chat_typing()'s
reasoning -- the text is the message, the markup is decoration.
chat_capabilities()$rich_markup names what an adapter accepts, and is
empty on an e2ee client: the Megolm path builds its own HTML from
markdown and has nowhere to put a supplied fragment.

Placed last in the signature, after notify, so nothing calling
positionally past `text` shifts.
@TroyHernandez
TroyHernandez merged commit c10189a into main Aug 7, 2026
2 checks passed
@TroyHernandez
TroyHernandez deleted the edits branch August 7, 2026 23:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant