Thanks for this, and especially for the IME trace and video; they make the case very clearly.
You're right, and it's a real gap. KeyDown is deliberately a raw key event: it fires for each physical key press before the platform has done any composition work. As your Chinese input method example shows, the finished character doesn't exist at that point and can't be reconstructed from the key sequence. The committed character travels a separate text input path that Objo doesn't currently surface to your code at all.
I've logged it here: https://feedback.objo.dev/feature/1721
The design follows your proposal: a new CharacterPressed() event alongside KeyDown() on the controls that have keyboard events today (TextField, TextArea, Canvas, and so on). It fires when a character is actually formed, so both dead key compositions like ¨ + e = ë and input method commits arrive as the finished text. Two refinements: the handler receives the text as a string rather than a single character, because input methods can commit several characters at once, and returning True consumes it before the control sees it, matching how KeyDown's return value already works.
It will work in both desktop and web projects. One platform note: browsers don't allow an input method's committed text to be cancelled once composition finishes, so on the web the veto will apply to ordinary typed characters while input method commits are reported after the fact. Desktop apps get the full behaviour.