Skip to content

fix(desktop): stop mention chips from swallowing the caret - #29

Merged
mattbalza merged 2 commits into
mainfrom
fix/mention-caret-trap
Aug 7, 2026
Merged

fix(desktop): stop mention chips from swallowing the caret#29
mattbalza merged 2 commits into
mainfrom
fix/mention-caret-trap

Conversation

@mattbalza

Copy link
Copy Markdown
Owner

What

Clicking just after a mention in the composer put the caret inside the handle, so the next keystroke turned @bob into @bobx.

A mention chip is an inline ProseMirror decoration over live, editable text, so its painted box doubles as a caret hit target. --inline-chip-padding-inline (0.25rem, markdown.css) paints 4px of chip past the last glyph. A click in that band is visually after the name but resolves to the document position before the trailing space.

Why not the obvious fix

The composer's contenteditable carries MESSAGE_MARKDOWN_CLASS, so it inherits .message-markdown .mention-chip { display: inline-flex; ... }. The tempting fix is to unset that. Measured against a live composer chip, it changes nothing:

candidate trailing band click 0.5–1px past the glyph
baseline (inline-flex, padding: 3px 4px 2px) 4.0px caret INSIDE the chip
display: inline; padding: 0 2px 2.0px caret INSIDE the chip
display: inline; padding: 0 4px 4.0px caret INSIDE the chip
padding-inline: 0 (either display) 0.0px past the box — caret lands in the space

The trap band is exactly the inline padding. inline and inline-flex hit-test identically.

The change

One declaration, scoped to the editable surfaces:

.rich-text-composer .tiptap {
  --inline-chip-padding-inline: 0;
}

Zeroing the variable rather than the padding keeps the agent chip's derived geometry consistent — its padding-left stays icon + gap and the ::before robot icon simply moves flush to the chip's left edge.

Rendered messages are untouched: they are not editable, so the band costs nothing there and the padded pill is the intended look.

Test plan

New E2E in desktop/tests/e2e/mentions.spec.ts, verified red on main (band = 4, expected ≤ 0.5) and green here. It asserts both halves:

  1. the structural invariant — the chip's box may not extend past its last glyph;
  2. the behaviour — a click 2px past the glyph followed by a keystroke does not weld onto the handle.

The behavioural half alone is not sufficient: it sits one pixel from the genuinely ambiguous end-of-word click that every editor resolves into the word, so the invariant is what pins the regression.

  • pnpm lint clean, tsc --noEmit clean
  • mentions.spec.ts + team-mentions.spec.ts + messaging.spec.ts + channel-composer-overflow.spec.ts: 99 passed. The one failure (messaging.spec.ts → "shows your avatar when profile avatar is set") reproduces unchanged on main and is unrelated.

Clicking just after a mention in the composer put the caret *inside* the
handle, so the next keystroke turned "@bob " into "@bobx".

The cause is that a mention chip is an inline decoration over live,
editable text, so its painted box doubles as a caret hit target.
`--inline-chip-padding-inline` (0.25rem) paints 4px of chip past the last
glyph; a click in that band is visually after the name but resolves to the
position before the trailing space.

Measured in the composer: the trap band is exactly the inline padding --
4.0px before, 0.0px after. Display mode is not involved; `inline` and
`inline-flex` hit-test identically, so this is not fixed by unsetting the
flex layout the composer inherits from `.message-markdown`.

Zeroing the variable rather than the padding keeps the agent chip's derived
geometry consistent: `padding-left` stays icon + gap and the `::before`
robot icon moves flush to the chip's left edge. Rendered messages are
untouched -- they are not editable, so the band costs nothing there and the
padded pill is the intended look.

The E2E asserts the structural invariant (the chip's box may not extend
past its last glyph) as well as the behaviour, because the behavioural half
alone is a pixel away from the genuinely ambiguous end-of-word click that
every editor resolves into the word.
… space's width

The probe clicked two pixels past the last glyph and expected the caret to
land after the trailing space. That holds only while half a space is under
two pixels wide, which is a macOS font metric — on the Linux runners the
same click resolved before the space and the test failed three times in a
row on a fix that works.

The regression is the chip's painted box extending past its last glyph, and
that measurement is font-independent, so it stays as the pin. The click now
probes three quarters into the space glyph, measured, where every platform
agrees the caret belongs after it.
@mattbalza
mattbalza merged commit 440105f into main Aug 7, 2026
23 checks passed
@mattbalza
mattbalza deleted the fix/mention-caret-trap branch August 7, 2026 16:08
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