Skip to content

feat(desktop): IRC-style s/old/new/ self-correction in the composer - #3801

Open
sw-square wants to merge 3 commits into
block:mainfrom
sw-square:bumble/irc-self-correction
Open

feat(desktop): IRC-style s/old/new/ self-correction in the composer#3801
sw-square wants to merge 3 commits into
block:mainfrom
sw-square:bumble/irc-self-correction

Conversation

@sw-square

@sw-square sw-square commented Jul 30, 2026

Copy link
Copy Markdown

What

Sending a message whose entire body is a well-formed substitution command — s/hullo/hello/ — now edits your most recent message instead of sending literal text. It's a keyboard-only shorthand for the existing right-click "Edit message" gesture.

If I type hullo there!, notice the typo, and send s/u/e/, my previous message becomes hello there!.

How

Pure client-side sugar. The correction is republished through the same kind-40003 edit path a manual edit uses, so other clients (and the relay) just see a normal edit. No relay, schema, or Rust changes.

  • selfCorrection.ts (pure, unit-tested) — parses s/old/new/[flags] as literal text, never regex:
    • Delimiter is always / — the canonical IRC/sed s/old/new/ shorthand. (sed permits arbitrary delimiters only to avoid escaping the delimiter inside the pattern; our \/ escaping already covers that, so alternate delimiters would only widen the accidental-trigger surface for no gain.)
    • \-escaping of the delimiter and backslash, so s/\//\/\//g replaces every / with //.
    • trailing delimiter optional when there are no flags (s/u/e).
    • flags: g (all occurrences) and i (case-insensitive).
    • literal matchings/*/x replaces the first literal *; . is a literal dot, not a wildcard; $1/\d in the replacement are inserted verbatim.
  • selfCorrectionEdit.ts (pure, unit-tested) — resolves a command against the timeline: finds the author's most recent editable message (same eligibility as ArrowUp-to-edit — own, non-system, not pending) and rebuilds the edit through the existing imeta/emoji helpers, so attachments and custom emoji are preserved.
  • useSelfCorrectingSend — intercepts at the send boundary in ChannelPane. A valid command edits the previous message; anything that doesn't parse, or whose pattern isn't present in your last message, falls through and sends literally. Drafts that carry their own attachments are treated as ordinary sends (then s/a/b/ is a caption).

Accidental-trigger safety

Three independent guards keep ordinary prose from being eaten:

  1. The message must start s/ — prose like s3 bucket, s: notes, s|foo never qualifies.
  2. The entire body must be a well-formed command.
  3. The pattern must actually occur in your previous message — otherwise it just sends as text, never a silent edit.

Behavior notes

  • A typo-fix correction intentionally emits no new mention tags — it re-wakes nobody, and the corrected body keeps the original mention tokens so existing mentions still render. (A substitution that introduces a brand-new @mention won't notify that user; acceptable for a quick-correction affordance.)
  • Scoped to the main timeline. Thread-reply correction is a natural follow-up.

Testing

  • Unit tests across selfCorrection (grammar/apply — incl. s/u/e, s/\//\/\//g, slash-only enforcement, and a dedicated literal-not-regex proof) and selfCorrectionEdit (target selection, fallthrough, media/emoji preservation).
  • pnpm typecheck, biome, file-size guard, and the full desktop unit suite (3860 tests) pass locally.

🐝 Built by Bumble in Buzz, from a conversation in #test-swesterman.

Sending a message whose entire body is a well-formed substitution command
(e.g. `s/hullo/hello/`) now edits your most recent message instead of
sending literal text — a keyboard-only shorthand for the existing
right-click "Edit message" gesture.

It is pure client-side sugar: the correction is republished through the
same kind-40003 edit path a manual edit uses, so other clients just see a
normal edit. No relay, schema, or Rust changes.

- `selfCorrection.ts` (pure, unit-tested): parses `s<D>old<D>new<D>[flags]`
  as literal text (not regex), sed-style so any punctuation delimiter works
  (`s|a|b|`, `s#a#b#`), with `\`-escaping of the delimiter, an optional
  trailing delimiter (`s/u/e`), and `g` (global) / `i` (case-insensitive)
  flags; and applies it.
- `selfCorrectionEdit.ts` (pure, unit-tested): resolves a command against
  the timeline — finds the author's most recent editable message (same
  eligibility as ArrowUp-to-edit) and rebuilds the edit through the existing
  imeta/emoji helpers so attachments and custom emoji are preserved.
- `useSelfCorrectingSend` hook intercepts at the send boundary in
  ChannelPane: a command edits the previous message; anything that does not
  parse or match falls through and sends literally. Drafts with their own
  attachments are treated as ordinary sends.

A typo-fix correction intentionally emits no new mention tags. Scoped to the
main timeline; thread-reply correction can follow.

Signed-off-by: Sam Westerman <swesterman@squareup.com>
@sw-square
sw-square requested a review from a team as a code owner July 30, 2026 20:24
sw-square and others added 2 commits July 30, 2026 13:34
Per review feedback: arbitrary punctuation delimiters widened the
"is this a command?" gate with no functional gain. sed allows any
delimiter only to avoid escaping the delimiter inside the pattern —
which the existing `\/` backslash-escaping already covers. Fixing the
delimiter to `/` matches the canonical IRC `s/old/new/` shorthand and
shrinks the accidental-trigger surface (prose like `s: notes`, `s|foo`
is no longer mistaken for a command).

Also adds an explicit test proving the pattern and replacement are
literal text, never regex: `s/*/x` replaces the first literal `*`, `.`
is a literal dot (not a wildcard), and `$1`/`\d` in the replacement are
inserted verbatim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Sam Westerman <swesterman@squareup.com>
…path

Collapses the IRC `s/old/new/` self-correction surface without changing
behaviour (still main-composer-only, literal-not-regex, media/emoji
preserved).

- Drop the new `onEditSaveById` handler + prop threaded through
  ChannelScreen -> ChannelPane.types -> ChannelPane -> hook. Instead widen
  the existing `handleEditSave`/`onEditSave` with an optional explicit
  `eventId`, so self-correction rides the edit path already in place. Zero
  new cross-component props.
- Reuse ChannelPane's existing `findLastOwnEditable` instead of the
  duplicate `findLastOwnCorrectable`, so "what counts as editable" has one
  definition shared with ArrowUp-to-edit.
- Merge `selfCorrectionEdit.ts` into `selfCorrection.ts` as
  `buildSelfCorrectionEdit`; fold its tests into one file.
- Slim `useSelfCorrectingSend` to pure state-wiring over the tested lib.

9 files -> 6, 736 -> 595 lines. Full desktop suite green (3855 tests),
typecheck + biome + file-size/px/pubkey guards clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Sam Westerman <swesterman@squareup.com>
@sw-square

Copy link
Copy Markdown
Author

🤖 Pushed a refactor commit (12c7e5ae8) that shrinks this PR in response to feedback that it was large for the scope. No behaviour change — still main-composer-only, literal-not-regex, media/emoji preserved.

What shrank the surface (and the future-breakage risk):

  • Removed the new onEditSaveById handler + prop that threaded through ChannelScreen → ChannelPane.types → ChannelPane → useChannelPaneHandlers. Instead the existing handleEditSave/onEditSave gains an optional explicit eventId — self-correction now rides the edit path already in place. Zero new cross-component props.
  • Reuses ChannelPane's existing findLastOwnEditable instead of a byte-for-byte duplicate, so "what counts as an editable message" has one definition shared with ArrowUp-to-edit.
  • Merged selfCorrectionEdit.ts into selfCorrection.ts; folded the two test files into one.

Net: 9 files → 6, 736 → 595 lines. The remaining bulk is the parser + its tests (the escaping / optional-slash / literal-not-regex grammar); the actual wiring is now ~80 lines. Full desktop suite green (3855 tests), typecheck + biome + file-size/px/pubkey guards clean.

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