## 2026-05-04 — 한글 IME 합성 위치 보정 시도와 revert (known issue)

### 배경

한글 합성 시 IME 후보창과 합성 글자가 `Claude Code`/`Codex`/`Gemini` 같은 React Ink 기반 TUI의 visual block cursor와 한 셀 어긋나 보이던 회귀를 두 번 시도했고, 진단 로그로 root cause가 가설과 다름이 확인되어 둘 다 revert. 합성 중 시각 어긋남은 잔존하지만 commit 후 최종 텍스트는 CLI 입력 박스에서 정상 렌더되므로 known issue로 두기로 결정. fix 가능한 방향(prompt anchor 자동 탐지 / 클라이언트 측 Hangul 컴포저)은 별도 트랙으로 검토 예정. 기록 차원에서 무엇이 안 됐는지 남겨둔다.

### 시도된 변경

#### 1. offsetParent BCR 보정 (`9b921dc`)

`positionHelperAtCursor`가 합성 글자/IME 후보창을 실제 블록 커서 셀보다 한 칸 왼쪽에 띄우는 회귀를 다음 두 가설로 잡으려 시도.

- **가설 A**: `cellW = screenRect.width / cols` 근사가 sub-pixel 오차를 만든다 (xterm.js는 `.xterm-screen` 폭을 `ceil(actualCellWidth * cols)`로 잡으므로). → `term._core._renderService.dimensions.css.cell.{width,height}`가 있으면 그 값을 우선 사용해 xterm 자체 렌더와 동일한 셀 메트릭 사용.
- **가설 B**: `helper.style.left = cursorX * cellW`가 helper textarea의 `offsetParent`인 `.xterm-helpers` 래퍼와 `.xterm-screen` origin 사이의 차이를 무시한다. → `helper.offsetParent`의 BCR과 `.xterm-screen` BCR의 좌상단 차이를 `offsetLeft/offsetTop`으로 계산해 `helper.style.left/top`에 더해 viewport 기준으로 항상 `.xterm-screen`의 col 0/row 0 = 0 정렬을 유지.

#### 2. inverse-prev-cell heuristic + .xterm-helpers 시프트 (`556890c`)

직전 fix에도 한 칸 어긋남이 잔존하자, "Claude Code/Codex/Gemini 같은 React Ink TUI는 native xterm 커서를 끄고 placeholder 또는 마지막 입력 셀을 inverse 속성으로 그려 자체 block cursor를 표현하므로 PTY의 `buffer.x`가 그 inverted 셀의 다음 셀에 위치한다"는 가설을 세움.

- `cursorX-1` 셀의 inverse 속성을 신호로 삼아 합성 동안만 `.xterm-helpers` 래퍼에 `translateX(-cellW)`를 걸어 안에 든 visible composition view + invisible textarea를 한 셀 왼쪽 = visual block cursor 위로 정렬.
- inverse가 없는 일반 CLI(bash/zsh 등)에선 transform 미적용이라 회귀 없음. `compositionend`/cleanup에서 transform 원래 값 복원해 잔존 시 누적 시프트 방지.
- 가설 검증을 위해 첫 합성 1회만 `[CLITrigger IME]` prefix로 `cursorX`, `cellW`, `prevCellInverse`, `transformApplied`를 `console.log`로 찍어 follow-up에서 제거 예정으로 남김.

#### 3. revert: 두 fix 모두 ineffective 확인 (`543586e`)

진단 로그가 root cause 가설을 부정.

- **9b921dc 무효 사유**: `.xterm-helpers ⊂ .xterm-screen` 레이아웃에서 `offsetParent` BCR과 `.xterm-screen` BCR의 좌상단 차이가 0이라 보정이 사실상 no-op.
- **556890c 무효 사유**: 가설 자체가 틀림. Claude Code는 SGR inverse로 visual cursor를 표시하지 *않고*, React Ink TUI가 PTY cursor를 화면 빈 영역(주로 우하단)에 주차시키고 자체 컴포넌트로 입력 박스를 별도 렌더하는 패턴이라 `prevCellInverse: false`와 빈 cells만 찍혀 휴리스틱 미발동.
- **`SessionTerminal.tsx`**를 `37d71e8` 시점으로 롤백: `positionHelperAtCursor`의 `_renderService` 셀 메트릭 + offsetParent 보정, `handleCompStart`의 inverse heuristic + `.xterm-helpers` transform, 진단 console.log, `restoreHelpersTransform` 모두 제거.

### 남은 known issue

- 합성 중 한글 자모/후보창이 visual cursor와 한 셀 어긋나 보임. commit 후 최종 텍스트는 CLI 입력 박스에서 정상 렌더되므로 기능 자체엔 영향 없음.
- React Ink 기반 TUI가 PTY cursor를 화면 빈 영역에 주차시키고 자체 입력 박스를 별도 렌더하는 패턴이라, xterm.js의 CompositionHelper가 보는 `buffer.x`와 사용자가 보는 visual cursor 위치가 무관함. 후속 작업은 prompt anchor 자동 탐지 또는 클라이언트 측 Hangul 컴포저로 textarea 위치를 직접 결정하는 방향.

### 수정된 주요 파일

| 파일 | 변경 내용 |
|------|----------|
| `src/client/src/components/SessionTerminal.tsx` | 9b921dc/556890c의 시도 후 543586e에서 모두 revert. 현재는 2695764의 `positionHelperAtCursor` (mount + compositionstart에 `.xterm-screen` BCR + cursorX/Y로 textarea 사전 배치) 시점으로 복귀 |

### 아키텍처 결정 / 교훈

1. **증거 없는 fix 시도는 진단 로그를 함께 박는다**: 두 시도 모두 가설만으로 코드를 박았다가 revert했지만, 9b921dc는 진단 로그가 없어 무효 검증이 어려웠고 556890c가 진단 로그를 같이 넣은 덕에 revert 결정을 빠르게 내릴 수 있었음. IME / 브라우저 이벤트 / 타이밍 버그처럼 측정이 어려운 영역은 항상 진단 로그를 1회만 찍는 once-flag와 함께 도입.
2. **xterm.js + React Ink TUI 조합의 IME 위치는 PTY cursor와 무관**: React Ink는 PTY cursor를 미관용 빈 영역에 주차하고 자체 컴포넌트로 입력 박스를 별도 렌더한다는 사실이 이번에 확정됨. 향후 IME 위치 fix는 PTY cursor 가 아닌 visual prompt anchor를 별도로 추적해야 함 (혹은 클라 측 dubeolsik composer로 textarea 위치를 직접 정함).

---
