TUI composer: Backspace deletes the whole Thai syllable instead of one combining mark

Open 💬 2 comments Opened Aug 10, 2026 by twenz235

Summary

In the Codex TUI composer, pressing Backspace on Thai text deletes the entire grapheme cluster (base consonant + vowel + tone mark) in one keypress, instead of removing only the last combining mark. Thai users expect one Backspace = one code point, which is how macOS native text fields and every Thai IME behave.

Repro

  1. Run codex in any terminal (reproduced in Harness, Terminal.app, iTerm2)
  2. In the composer type ที่ — three code points: U+0E17 ท + U+0E35 ◌ี + U+0E48 ◌่
  3. Press Backspace once

Expected: ที (only the tone mark U+0E48 is removed), a second Backspace gives , a third clears the composer.

Actual: the composer is empty after a single Backspace — all three code points are gone (the placeholder hint reappears).

Automated repro

Verified with a PTY + terminal-emulator harness (pyte), so this is not a rendering artifact of any one terminal:

[codex] typed: ['› ที่']
[codex] BS1  : ['› Summarize recent commits']   # placeholder back = composer empty

Environment

  • codex-cli 0.147.0 (native arm64 binary)
  • macOS 15 (Darwin 25.5.0), Apple Silicon
  • Reproduced under TERM=xterm-256color, LANG=en_US.UTF-8

Note: is already handled correctly

Backspace on ซ้ำ (U+0E0B + U+0E49 + U+0E33) correctly leaves ซ้ — only the spacing
character is removed. That is the desired behavior and should not change; the report above is
only about combining marks that occupy no column of their own.

Notes / suggested fix

Likely a unicode-segmentation grapheme-cluster-based delete in the composer. Deleting a whole cluster is the right behavior for emoji ZWJ sequences and precomposed Latin (é), but for combining-mark scripts the platform convention is per-code-point deletion.

Suggested scope: when the trailing scalar of the cluster is a combining mark of a Thai/Lao/Khmer/Devanagari-style script (Thai: U+0E31, U+0E34U+0E3A, U+0E47U+0E4E), pop one scalar instead of the whole cluster; otherwise keep the current cluster-wise delete.

For reference, macOS AppKit behaves this way: NSTextView(string: "ที่").deleteBackward(nil) leaves U+0E17 U+0E35.

View original on GitHub ↗

2 Comments

amartejak · 17 days ago

I confirmed this against current main (41ece45) and prepared a focused, tested patch locally.

Root cause: TextArea::delete_backward always delegates to prev_atomic_boundary, which uses GraphemeCursor. That correctly keeps emoji and most composed text atomic, but it also treats a Thai base consonant plus its vowel/tone marks as one extended grapheme, so one Backspace removes the whole syllable.

The narrow fix I validated is:

  • Before the existing grapheme fallback, remove one trailing Thai combining scalar for U+0E31, U+0E34..=U+0E3A, or U+0E47..=U+0E4E.
  • Keep placeholder/attachment elements atomic by applying that exception only when the cursor is not inside an element range.
  • Leave the existing grapheme behavior unchanged for all other text.

Regression coverage verifies:

  • ที่ becomes ที, then , then empty over three Backspaces.
  • ซ้ำ still becomes ซ้ on the first Backspace (spacing vowel behavior is unchanged).
  • Decomposed Latin e\u{301} and a family ZWJ emoji are still removed as whole graphemes.
  • Existing Backspace edge/word-deletion tests continue to pass.

Validation on the rebased branch:

just test -p codex-tui delete_backward
8 tests run: 8 passed

just clippy -p codex-tui --lib
clean

I have the patch ready as local commit aa69622 under Amar Teja Kommineni. Per the repository's invitation-only contribution policy, I have not opened an unsolicited PR. If a maintainer would like this approach submitted, I am happy to open the PR immediately.

jdcodes1 · 9 days ago

Status update: this is fixed on main. PR #38662 ("Delete Thai combining marks one at a time in the composer", commit 274727d37f) changed delete_backward to special-case Thai nonspacing marks (U+0E31, U+0E34U+0E3A, U+0E47U+0E4E) so one Backspace removes one mark instead of the whole grapheme cluster, with regression tests covering your exact ที่ที → empty sequence (https://github.com/openai/codex/blob/1f41cc5d92/codex-rs/tui/src/bottom_pane/textarea.rs#L1005-L1032, tests at #L2265-L2316). The fix landed after 0.147.0, so it should arrive in the next release; your issue can likely be closed once you've verified on that build. (Note the implementation is Thai-specific by design — other scripts with the same expectation, e.g. Lao or Khmer, would need their ranges added to the same match.)