Replace the compile-time cfg!(target_os = "macos") jamo-width heuristic
with a runtime override (process-global AtomicU8 in pi-natives, mirrored in
the TS width engine) plus terminal-identity detection: Ghostty renders
Hangul Compatibility Jamo (U+3131..U+318E) at 2 cells, so it is forced wide;
every other terminal keeps the platform default (macOS narrow, otherwise
UAX#11), making the override a no-op outside Ghostty.
Fixes doubled/ghosted jamo during Korean IME composition on Ghostty, where
the hardware cursor landed inside the typed text and the IME candidate window
drifted from the glyph. The width is resolved synchronously before the first
paint, so no stdin/CPR probe or async cache invalidation is needed.
A runtime DSR/CPR probe that auto-detects the width on unknown terminals is
tracked in a follow-up.
- Removed configurable tab width support and the `display.tabWidth` setting across all packages.
- Deleted obsolete utility functions `getIndentation`, `getIndentationNoescape`, and `setDefaultTabWidth`.
- Standardized tab expansion logic to use a fixed `DEFAULT_TAB_WIDTH` globally.
- Cleaned up related configuration schemas, test suites, and internal API signatures to remove path-dependency.
- Centralized decoding of tmux's `extended-keys` control sequences (`csi-u` and `xterm` formats) into a shared utility.
- Updated the multi-line editor and single-line input to use this decoder, preventing escape tails (e.g., `[27;5;106~`) from leaking into the text buffer.
- Added parity to the single-line input by explicitly stripping any remaining control characters after decoding to ensure data integrity.
On kitty (Linux/Wayland) and any terminal supporting OSC 5522 enhanced
paste, the /login API-key prompt for OpenCode Zen — and any other modal
Input prompt (Perplexity OTP, GitHub Enterprise URL, manual OAuth
redirect URL, …) — silently dropped pasted content. The user could not
paste their API key; on Enter or Esc the key surfaced in the main
prompt.
Cause: InputController.#setupEnhancedPaste enables kitty's enhanced
clipboard protocol on TUI start and consumes the resulting OSC 5522
packets in an addInputListener that runs *before* focus dispatch in
tui.#handleInput. The controller's pasteText callback then routed
unconditionally to this.ctx.editor.pasteText(text) — the main
CustomEditor — even when selector-controller had cleared editorContainer
and focused a temporary Input. The text accumulated in the detached
editor and only resurfaced when the modal was dismissed.
Fix:
- TUI.getFocused() exposes the currently focused component.
- Input.pasteText(text) mirrors Editor.pasteText so any Input can absorb
a payload from a non-bracketed transport.
- InputController's enhanced-paste callback consults getFocused() and
routes text to the focused component when it exposes pasteText,
falling back to the editor only when no modal target is in focus.
- Image pastes refuse with a status message when a modal Input is
focused, instead of stuffing a binary blob into the hidden editor.
Regression tests in packages/tui/test/input.test.ts and
packages/coding-agent/test/issue-2127-repro.test.ts pin both the new
TUI/Input contract and the InputController routing source.
Fixes#2127
User-visible symptom: drag-dropping a file with Korean characters in its name (e.g., `/Users/leo/Downloads/화면 기록 2026-05-13 오후 8.55.24.mov`) into the OMP input prompt leaves the hardware cursor floating ~5–8 cells past the visible filename. Static — typing immediately after places the new character at the cursor (gap), not at the visible end of the text.
Root cause (verified by direct measurement on the user's pasted path):
- macOS Finder drag-drops file paths with Korean characters in NFD form (Conjoining Jamo, U+1100..U+11FF), not NFC (Hangul Syllables, U+AC00..U+D7A3).
- Korean syllable `화` is U+D654 in NFC: 1 char, Bun.stringWidth=2 cells.
- Same syllable in NFD is U+1112 (ᄒ) + U+116A (ᅪ): 2 chars, Bun.stringWidth=3 cells (each conjoining jamo is classified Wide=2 or Narrow=1 separately).
- Terminal emulators correctly combine the NFD sequence into a single visible syllable (2 cells) per Unicode rendering rules.
- Bun's count overcounts by 1–2 cells per syllable. For a path with 6 Korean syllables: ~9 cells overcount → cursor drifts 9 cells past the filename.
- This is the same JS/Rust/terminal width-mismatch class as the earlier Compatibility Jamo issue (U+3131..U+318E from IME composition), but in a different Unicode range — and triggered by macOS Finder, not the IME.
Fix: `Input.#handlePaste` now appends `.normalize("NFC")` to the paste-text cleaning chain. The pasted bytes are converted from NFD → NFC before being inserted into `#value`, so all downstream width arithmetic (cursor positioning, viewport scrolling, padding) sees the same canonical encoding the terminal renders.
Localized to paste-time only — does not change `visibleWidthRaw` semantics, doesn't add per-render overhead, and doesn't touch the Rust width tables. Cost: ~0.5 μs per paste (single `String.normalize("NFC")` call on the pasted content).
Tests: 2 new cases in `packages/tui/test/input.test.ts` covering (a) NFD-encoded path stored as NFC after paste, (b) cursor column matches `visibleWidth(value) + prompt-width` after pasting a Korean filename. Full tui suite: 8 pre-existing failures (overlay-scroll 4 + render-regressions 4) unchanged.
- Extracted tab width resolution from pi-natives to pi-utils with EditorConfig caching support.
- Made tabWidth parameter required in native text functions (visibleWidth, truncateToWidth, wrapTextWithAnsi, sliceWithWidth, extractSegments).
- Removed process-wide tab width state management from pi-natives text module.
- Consolidated wrapper node promotion logic in chunk classification with abbreviated string representations.
- Removed path-clean and pathdiff dependencies from pi-natives.
- Updated all consuming packages to import tab width functions from pi-utils and pass explicit tabWidth parameters.
- Migrated native bindings from TypeScript wrappers to NAPI-RS generated modules with auto-generated type definitions and runtime enums.
- Replaced chunk tree API with stateful ChunkState class supporting render, edit, and resolve operations with improved error handling.
- Converted callback signatures to error-first pattern (error, result) for shell, PTY, glob, and grep operations.
- Introduced type-safe enums for MacOSAppearance, GrepOutputMode, KeyEventType, ImageFormat, and AstMatchStrictness replacing string literals.
- Refactored chunk tree implementation with dedicated modules for edit, indent, resolve, and state management with comprehensive validation.
- Moved clipboard utilities from native bindings to coding-agent package with improved OSC 52 and Termux compatibility.
- Added checks to filter out Kitty release events (event_type 3) in both matches_key_inner and parse_key_inner functions. Added comprehensive tests for both key matching and input handling to verify release events are ignored while press and repeat events are still processed correctly.
Fixes#325