TUI composer: Backspace deletes the whole Thai syllable instead of one combining mark
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
- Run
codexin any terminal (reproduced in Harness, Terminal.app, iTerm2) - In the composer type
ที่— three code points:U+0E17ท +U+0E35◌ี +U+0E48◌่ - 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+0E34–U+0E3A, U+0E47–U+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.
2 Comments
I confirmed this against current
main(41ece45) and prepared a focused, tested patch locally.Root cause:
TextArea::delete_backwardalways delegates toprev_atomic_boundary, which usesGraphemeCursor. 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:
U+0E31,U+0E34..=U+0E3A, orU+0E47..=U+0E4E.Regression coverage verifies:
ที่becomesที, thenท, then empty over three Backspaces.ซ้ำstill becomesซ้on the first Backspace (spacing vowelำbehavior is unchanged).e\u{301}and a family ZWJ emoji are still removed as whole graphemes.Validation on the rebased branch:
I have the patch ready as local commit
aa69622under 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.Status update: this is fixed on
main. PR #38662 ("Delete Thai combining marks one at a time in the composer", commit 274727d37f) changeddelete_backwardto special-case Thai nonspacing marks (U+0E31,U+0E34–U+0E3A,U+0E47–U+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.)