fix(kebab-tui): p9-fb-22 — mid-string cursor editing + Ask follow-tail auto-scroll #96

Merged
altair823 merged 3 commits from fix/p9-fb-22-cursor-and-autoscroll into main 2026-05-04 15:41:49 +00:00
Owner

요약

도그푸딩 중 발견된 두 건의 TUI 버그 (Gitea #94, #95) 를 한 PR 로 묶어 처리.

  • #94 커서 이슈: InputBuffer 가 append-only 라 Ask/Search/Filter overlay 의 입력 중간 편집 불가. cursor 모델을 byte-position 기반으로 재구성하고 ← / → / Home / End / Delete key handler 를 세 input pane 에 wired.
  • #95 새 응답 이슈: Ask 트랜스크립트가 새 답변 도착 시 자동으로 viewport bottom 을 따라가지 않아 사용자가 매번 j 로 스크롤해야 했던 문제. AskState.follow_tail 추가 + render_answer 가 매 프레임 Paragraph::line_count(width) 로 wrapped row 수 계산해 스크롤을 bottom 에 pin.

주요 변경

InputBuffer cursor 모델 재구성

  • cursor_byte 가 새 source of truth (UTF-8 char boundary). cursor_col() 는 prefix slice 의 unicode-width 합으로 derive.
  • 신규: move_left / move_right / move_home / move_end / delete_after.
  • 기존 push_char / pop_char 는 cursor 위치에서 동작 — cursor 가 끝일 때 기존 append 동작과 동일 (backwards-compat: 30+ 기존 테스트 변경 없이 통과).

Ask follow-tail

  • AskStatederive(Default) 에서 manual Default impl 로 전환 (default follow_tail = true).
  • render_answer 가 매 프레임 Paragraph::line_count(inner.width) 로 wrapped row 수 계산해 follow-tail 시 scroll = line_count - inner_height pin. wrap-aware 이므로 viewport 너비가 바뀌어도 정확.
  • j / k: follow_tail = false 로 freeze.
  • Shift-G (Normal 모드): follow_tail = true + scroll = 0 으로 재활성화.
  • 새 submission, Ctrl-Lfollow_tail = true 재설정.

Pane key handler

  • Ask: Left / Right / Home / End / Delete mode 무관, Shift-G Normal 한정.
  • Search: 동일 5 key. Delete 만 input_dirty_at reset (cursor 이동 ≠ 쿼리 변경 → debounce 타이머 유지).
  • Library filter overlay: 동일 5 key, 활성 field (Tags / Lang) 의 buffer 에 적용.

Dep / docs

  • `kebab-tui` 의 `ratatui` dep 에 `unstable-rendered-line-info` feature 활성화 — `Paragraph::line_count` 가 ratatui 0.28 에서 unstable. ratatui 버전 bump 시 본 feature 의 안정 여부 재확인 필요 (현재 0.28.1 에 pin).
  • cheatsheet popup Search/Ask section 에 화살표 + Home/End + Delete row 추가, Ask 에 `Shift-G` row 추가.
  • README + HANDOFF + HOTFIXES + INDEX + 신규 spec `tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md` 동기.

테스트

  • 12 신규 InputBuffer unit, 6 신규 Ask integration. 기존 30+ 테스트 그대로 통과 (backwards-compat).
  • `cargo test --workspace -j 1` → 699 passed, 0 failed.
  • `cargo clippy --workspace --all-targets -- -D warnings` clean.

Closes

## 요약 도그푸딩 중 발견된 두 건의 TUI 버그 (Gitea #94, #95) 를 한 PR 로 묶어 처리. - **#94 커서 이슈**: `InputBuffer` 가 append-only 라 Ask/Search/Filter overlay 의 입력 중간 편집 불가. cursor 모델을 byte-position 기반으로 재구성하고 `← / → / Home / End / Delete` key handler 를 세 input pane 에 wired. - **#95 새 응답 이슈**: Ask 트랜스크립트가 새 답변 도착 시 자동으로 viewport bottom 을 따라가지 않아 사용자가 매번 `j` 로 스크롤해야 했던 문제. `AskState.follow_tail` 추가 + `render_answer` 가 매 프레임 `Paragraph::line_count(width)` 로 wrapped row 수 계산해 스크롤을 bottom 에 pin. ## 주요 변경 ### `InputBuffer` cursor 모델 재구성 - `cursor_byte` 가 새 source of truth (UTF-8 char boundary). `cursor_col()` 는 prefix slice 의 `unicode-width` 합으로 derive. - 신규: `move_left / move_right / move_home / move_end / delete_after`. - 기존 `push_char` / `pop_char` 는 cursor 위치에서 동작 — cursor 가 끝일 때 기존 append 동작과 동일 (backwards-compat: 30+ 기존 테스트 변경 없이 통과). ### Ask follow-tail - `AskState` 가 `derive(Default)` 에서 manual `Default` impl 로 전환 (default `follow_tail = true`). - `render_answer` 가 매 프레임 `Paragraph::line_count(inner.width)` 로 wrapped row 수 계산해 follow-tail 시 `scroll = line_count - inner_height` pin. wrap-aware 이므로 viewport 너비가 바뀌어도 정확. - `j` / `k`: `follow_tail = false` 로 freeze. - `Shift-G` (Normal 모드): `follow_tail = true` + `scroll = 0` 으로 재활성화. - 새 submission, `Ctrl-L` 도 `follow_tail = true` 재설정. ### Pane key handler - Ask: `Left / Right / Home / End / Delete` mode 무관, `Shift-G` Normal 한정. - Search: 동일 5 key. `Delete` 만 input_dirty_at reset (cursor 이동 ≠ 쿼리 변경 → debounce 타이머 유지). - Library filter overlay: 동일 5 key, 활성 field (Tags / Lang) 의 buffer 에 적용. ### Dep / docs - \`kebab-tui\` 의 \`ratatui\` dep 에 \`unstable-rendered-line-info\` feature 활성화 — \`Paragraph::line_count\` 가 ratatui 0.28 에서 unstable. ratatui 버전 bump 시 본 feature 의 안정 여부 재확인 필요 (현재 0.28.1 에 pin). - cheatsheet popup Search/Ask section 에 화살표 + Home/End + Delete row 추가, Ask 에 \`Shift-G\` row 추가. - README + HANDOFF + HOTFIXES + INDEX + 신규 spec \`tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md\` 동기. ## 테스트 - 12 신규 InputBuffer unit, 6 신규 Ask integration. 기존 30+ 테스트 그대로 통과 (backwards-compat). - \`cargo test --workspace -j 1\` → 699 passed, 0 failed. - \`cargo clippy --workspace --all-targets -- -D warnings\` clean. ## Closes - closes #94 - closes #95
altair823 added 1 commit 2026-05-04 15:30:10 +00:00
도그푸딩 중 발견된 두 건 (Gitea #94, #95) 동시 수정.

#94 — `InputBuffer` 가 append-only 라 Ask/Search/Filter overlay 에서
타이핑한 텍스트의 중간을 편집할 수 없었음. cursor 모델을 byte-position
기반으로 재구성 (cursor_col 은 prefix slice 의 unicode-width 합으로
derive). 신규 메서드: `move_left / move_right / move_home / move_end /
delete_after`. 기존 `push_char` / `pop_char` 는 cursor 위치에서 동작
(cursor 가 끝일 때 backwards-compatible). Ask / Search / Library filter
overlay 세 곳에 `← / → / Home / End / Delete` key handler 추가. Search 는
cursor 이동만으로는 input_dirty_at 을 reset 하지 않음 (커서 이동 ≠ 쿼리
변경 → debounce 타이머 유지).

#95 — Ask 트랜스크립트의 `Paragraph::scroll((s.scroll, 0))` 가 위에서
부터 카운트라, 새 답변 도착 시 `s.scroll = 0` 으로 리셋하면 viewport 가
위쪽 고정 → 트랜스크립트가 길어지면 새 응답이 시야 밖으로 밀림. `AskState`
에 `follow_tail: bool` (default true) 추가. `render_answer` 가 follow_tail
동안 매 프레임 `Paragraph::line_count(width)` 로 wrapped row 수 계산해
스크롤을 `line_count - inner_height` 에 pin. `j` / `k` 가 follow_tail 끄고
`Shift-G` 가 다시 켬. 새 submission, `Ctrl-L` 도 follow-tail 재활성화.

`kebab-tui` 의 ratatui dep 에 `unstable-rendered-line-info` feature
활성화 — `Paragraph::line_count` 가 ratatui 0.28 에서 unstable. 0.28 에
pin 되어있는 동안 안정. 향후 ratatui bump 시 본 feature 의 stable 여부
재확인 필요.

cheatsheet popup Search/Ask section 에 화살표 + Home/End + Delete row
추가, Ask 에 `Shift-G` row 추가. README + HANDOFF + HOTFIXES + INDEX 동기.

Tests: 12 신규 InputBuffer unit + 6 신규 Ask integration. 기존 699 워크
스페이스 테스트 모두 통과 (cursor 가 끝일 때 backwards-compat).

Spec: `tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md` (status `completed`).
Live deviation 기록: `tasks/HOTFIXES.md` `2026-05-04 — p9-fb-22`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-04 15:32:31 +00:00
claude-reviewer-01 left a comment
Member

회차 1 — 코드 자체의 동작은 정확하고 (workspace 699 통과, clippy clean) backwards-compat 도 잘 유지됐습니다. cursor model byte-position 재구성 + follow_tail 두 축이 깔끔하게 분리돼 있고 테스트 커버리지도 mid-string editing + Hangul wide-char + transcript overflow rendering 까지 잡혀 있어 신뢰도 높습니다.

actionable 4건 모두 nit 카테고리:

  1. crates/kebab-tui/src/input.rs:237} 닫힘과 #[cfg(test)] 사이 빈 줄이 1 → 2 로 늘었습니다 (cosmetic).
  2. tasks/HOTFIXES.md 의 신규 테스트 카운트 (12 + 5) 가 실제 카운트 (11 + 10) 와 어긋납니다. HOTFIXES 는 영속 기록이라 정정 필요.
  3. tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md 도 동일한 카운트 오류 (12 + 6).
  4. crates/kebab-tui/src/library.rs filter overlay 의 5 개 새 arm 이 동일 let buf = match edit.field { ... }; 4-line 블록을 반복. helper 로 정리할지 선택 사항 (borrow lifetime 때문에 함수 상단에서 한 번에 잡기는 어렵지만 helper fn 으로는 가능).
회차 1 — 코드 자체의 동작은 정확하고 (workspace 699 통과, clippy clean) backwards-compat 도 잘 유지됐습니다. cursor model byte-position 재구성 + follow_tail 두 축이 깔끔하게 분리돼 있고 테스트 커버리지도 mid-string editing + Hangul wide-char + transcript overflow rendering 까지 잡혀 있어 신뢰도 높습니다. actionable 4건 모두 nit 카테고리: 1. `crates/kebab-tui/src/input.rs:237` — `}` 닫힘과 `#[cfg(test)]` 사이 빈 줄이 1 → 2 로 늘었습니다 (cosmetic). 2. `tasks/HOTFIXES.md` 의 신규 테스트 카운트 (12 + 5) 가 실제 카운트 (11 + 10) 와 어긋납니다. HOTFIXES 는 영속 기록이라 정정 필요. 3. `tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md` 도 동일한 카운트 오류 (12 + 6). 4. `crates/kebab-tui/src/library.rs` filter overlay 의 5 개 새 arm 이 동일 `let buf = match edit.field { ... };` 4-line 블록을 반복. helper 로 정리할지 선택 사항 (borrow lifetime 때문에 함수 상단에서 한 번에 잡기는 어렵지만 helper fn 으로는 가능).
@@ -173,6 +234,7 @@ impl InputBuffer {
}
}

여분의 빈 줄. 기존 1줄 → 2줄로 늘었습니다. } 닫힘과 #[cfg(test)] 사이 빈 줄 한 줄로 되돌리는 게 깔끔합니다 (다른 파일에서 일관되게 1줄 사용 중).

여분의 빈 줄. 기존 1줄 → 2줄로 늘었습니다. `}` 닫힘과 `#[cfg(test)]` 사이 빈 줄 한 줄로 되돌리는 게 깔끔합니다 (다른 파일에서 일관되게 1줄 사용 중).
@@ -372,0 +411,4 @@
};
buf.delete_after();
KeyOutcome::Continue
}

Filter overlay 에 추가된 5 개 arm (Left / Right / Home / End / Delete) 이 모두 동일한 4-line let buf = match edit.field { ... }; 디스패치를 반복합니다. 5 × 4 = 20 줄이 동일 패턴.

Rust borrow checker 때문에 함수 상단에 한 번만 잡기는 어렵지만 (edit 의 mutable borrow 가 match arm 의 buf 선언 후로 묶여 KeyCode::Char(c) arm 의 self-borrow 와 겹침), fn active_buf(edit: &mut FilterEditState) -> &mut InputBuffer 같은 작은 helper 로는 줄일 수 있습니다. 이미 Backspace / Char(c) arm 도 같은 4-line 디스패치를 갖고 있어 helper 로 통일하면 7 곳 → 1 곳.

선택 사항. 회차 2 에서 helper 로 정리해도 좋고, 이대로 acceptable as-is 로 닫아도 무방합니다 (style preference).

Filter overlay 에 추가된 5 개 arm (Left / Right / Home / End / Delete) 이 모두 동일한 4-line `let buf = match edit.field { ... };` 디스패치를 반복합니다. 5 × 4 = 20 줄이 동일 패턴. Rust borrow checker 때문에 함수 상단에 한 번만 잡기는 어렵지만 (`edit` 의 mutable borrow 가 match arm 의 `buf` 선언 후로 묶여 `KeyCode::Char(c)` arm 의 self-borrow 와 겹침), `fn active_buf(edit: &mut FilterEditState) -> &mut InputBuffer` 같은 작은 helper 로는 줄일 수 있습니다. 이미 Backspace / Char(c) arm 도 같은 4-line 디스패치를 갖고 있어 helper 로 통일하면 7 곳 → 1 곳. 선택 사항. 회차 2 에서 helper 로 정리해도 좋고, 이대로 acceptable as-is 로 닫아도 무방합니다 (style preference).
@@ -17,0 +18,4 @@
**Issues**: Gitea #94 (커서 이슈) — 텍스트 입력 후 커서 이동 불가. Gitea #95 (새 응답 이슈) — 새 응답이 viewport 아래로 추가돼도 자동으로 스크롤이 따라가지 않음. 두 건 모두 사용자 도그푸딩 중 발견.
**Root cause**:

테스트 카운트가 실제와 어긋납니다. cargo test -p kebab-tui 결과로 확인:

  • input.rs unit: pre-fb-22 18 → post-fb-22 29 → 신규 11
  • tests/ask.rs integration: pre-fb-22 21 → post-fb-22 31 → 신규 10

현재 본 항목은 "12 신규 InputBuffer unit + 5 신규 Ask integration" 으로 명시. 회차 2 에서 11 신규 InputBuffer unit + 10 신규 Ask integration 으로 바로잡고, 이어진 "기존 30 개" 도 "기존 38 개" (18+21-1, +1 은 ignored doctest) 정도로 조정하는 게 정확합니다. HOTFIXES 는 영속 기록이라 정확한 카운트가 의미 있습니다.

테스트 카운트가 실제와 어긋납니다. `cargo test -p kebab-tui` 결과로 확인: - input.rs unit: pre-fb-22 18 → post-fb-22 29 → **신규 11** - tests/ask.rs integration: pre-fb-22 21 → post-fb-22 31 → **신규 10** 현재 본 항목은 "12 신규 InputBuffer unit + 5 신규 Ask integration" 으로 명시. 회차 2 에서 `11 신규 InputBuffer unit + 10 신규 Ask integration` 으로 바로잡고, 이어진 "기존 30 개" 도 "기존 38 개" (18+21-1, +1 은 ignored doctest) 정도로 조정하는 게 정확합니다. HOTFIXES 는 영속 기록이라 정확한 카운트가 의미 있습니다.
@@ -0,0 +38,4 @@
## Behavior contract
### InputBuffer

spec frozen 으로 들어가면 영구 기록이라 같은 카운트 오류가 박힙니다. 12 신규11 신규, 6 신규 Ask integration10 신규 Ask integration 으로 정정 권장.

나열된 테스트 이름들 자체는 정확하니 카운트 숫자만 바꾸면 됩니다.

spec frozen 으로 들어가면 영구 기록이라 같은 카운트 오류가 박힙니다. `12 신규` → `11 신규`, `6 신규 Ask integration` → `10 신규 Ask integration` 으로 정정 권장. 나열된 테스트 이름들 자체는 정확하니 카운트 숫자만 바꾸면 됩니다.
altair823 added 1 commit 2026-05-04 15:34:33 +00:00
회차 1 review (PR #96 회차 1) 의 4 건 actionable nit 모두 수렴.

- `crates/kebab-tui/src/input.rs`: `impl InputBuffer { ... }` 닫힘과
  `#[cfg(test)]` 사이의 잉여 빈 줄 1 개 제거 (1 → 2 → 1).
- `tasks/HOTFIXES.md`, `tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md`:
  신규 테스트 카운트 정정 — 12 → 11 (InputBuffer unit), 5/6 → 10
  (Ask integration), 30 → 38 (기존 backwards-compat 통과 카운트).
  영속 기록이라 정확한 숫자가 의미 있음.
- `crates/kebab-tui/src/library.rs`: `FilterEdit::active_buf_mut(&mut self)
  -> &mut InputBuffer` helper 추가, filter overlay 의 7 개 key arm
  (Backspace + Left/Right/Home/End/Delete + Char) 이 모두 helper 한 줄로
  통일. 동일 `match edit.field { ... }` 디스패치 7번 반복 → 1 곳.

코드/문서 수렴. 카운트는 `cargo test -p kebab-tui` 으로 재확인:
input.rs unit 18 → 29 (+11), tests/ask.rs 21 → 31 (+10).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-04 15:36:21 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 의 4 건 모두 잘 반영됐습니다. input.rs 빈 줄 정정, library.rsactive_buf_mut helper 도입으로 7 개 arm 이 helper 한 줄로 통일된 구조 깔끔하고, render path 에서는 label/row_offset 까지 함께 픽해야 하므로 helper 적용 안 한 판단도 합리적입니다.

actionable 2 건 (모두 cosmetic nit):

  1. HOTFIXES + spec 의 "기존 38 개" 가 실제 39 개 (input.rs 18 + tests/ask.rs 21) 와 1 차이.
  2. library.rsactive_buf_mut doc comment 가 "3-line dispatch" 인데 실제 dispatch 는 4-line 또는 2-arm.

회차 3 에서 두 건 정정하고 APPROVE 받으면 머지 가능 상태.

회차 2 — 회차 1 의 4 건 모두 잘 반영됐습니다. `input.rs` 빈 줄 정정, `library.rs` 의 `active_buf_mut` helper 도입으로 7 개 arm 이 helper 한 줄로 통일된 구조 깔끔하고, render path 에서는 label/row_offset 까지 함께 픽해야 하므로 helper 적용 안 한 판단도 합리적입니다. actionable 2 건 (모두 cosmetic nit): 1. HOTFIXES + spec 의 "기존 38 개" 가 실제 39 개 (input.rs 18 + tests/ask.rs 21) 와 1 차이. 2. `library.rs` 의 `active_buf_mut` doc comment 가 "3-line dispatch" 인데 실제 dispatch 는 4-line 또는 2-arm. 회차 3 에서 두 건 정정하고 APPROVE 받으면 머지 가능 상태.
@@ -77,0 +78,4 @@
/// the `match edit.field` pick so the key-handler arms (Backspace
/// / arrows / Delete / typed Char) don't each re-spell the same
/// 3-line dispatch.
fn active_buf_mut(&mut self) -> &mut crate::input::InputBuffer {

doc comment 의 "3-line dispatch" 가 실제 수와 안 맞습니다. 원래 코드는 4 줄 (let buf = match edit.field { + 2 arms + };) 또는 2 arms 였는데 "3-line" 은 어느 쪽도 아님.

"4-line" 또는 "2-arm" 으로 정정하면 정확. cosmetic nit.

doc comment 의 "3-line dispatch" 가 실제 수와 안 맞습니다. 원래 코드는 4 줄 (`let buf = match edit.field {` + 2 arms + `};`) 또는 2 arms 였는데 "3-line" 은 어느 쪽도 아님. "4-line" 또는 "2-arm" 으로 정정하면 정확. cosmetic nit.
@@ -17,0 +18,4 @@
**Issues**: Gitea #94 (커서 이슈) — 텍스트 입력 후 커서 이동 불가. Gitea #95 (새 응답 이슈) — 새 응답이 viewport 아래로 추가돼도 자동으로 스크롤이 따라가지 않음. 두 건 모두 사용자 도그푸딩 중 발견.
**Root cause**:

회차 1 카운트 정정의 후속 — "기존 30 개" 을 "기존 38 개" 로 바꿨는데 실제는 39 개입니다.

  • input.rs unit 테스트: 현재 29 = 신규 11 + 기존 18
  • tests/ask.rs 통합: 현재 31 = 신규 10 + 기존 21
  • 합 = 39

"38" 은 어디서 나온 숫자가 아니고 (1줄 doctest 를 빼는 식의 보정 이유도 없음 — doctest 는 unit/integration 카운트와 별개) 38 → 39 로 정정 권장.

같은 정정을 spec 파일 (tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md) 의 "기존 38 개" 에도 적용 필요.

회차 1 카운트 정정의 후속 — "기존 30 개" 을 "기존 38 개" 로 바꿨는데 실제는 39 개입니다. - input.rs unit 테스트: 현재 29 = 신규 11 + 기존 **18** - tests/ask.rs 통합: 현재 31 = 신규 10 + 기존 **21** - 합 = **39** "38" 은 어디서 나온 숫자가 아니고 (1줄 doctest 를 빼는 식의 보정 이유도 없음 — doctest 는 unit/integration 카운트와 별개) 38 → 39 로 정정 권장. 같은 정정을 spec 파일 (tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md) 의 "기존 38 개" 에도 적용 필요.
altair823 added 1 commit 2026-05-04 15:36:53 +00:00
회차 2 review (PR #96 회차 2) 의 2 건 cosmetic nit 모두 수렴.

- `tasks/HOTFIXES.md`, `tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md`:
  \"기존 38 개\" → \"기존 39 개 (input.rs unit 18 + tests/ask.rs 21)\"
  로 정확 카운트 + 출처 명시.
- `crates/kebab-tui/src/library.rs`: `active_buf_mut` doc comment 의
  \"3-line dispatch\" → \"2-arm dispatch\" (실제 dispatch 가 2 arm 이라
  가장 정확한 표현).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-04 15:37:24 +00:00
claude-reviewer-01 left a comment
Member

회차 3 — 회차 1 의 4 건 + 회차 2 의 2 건 모두 수렴. 카운트는 input.rs 18 + tests/ask.rs 21 = 39 로 정확하고 doc comment 의 dispatch 표현도 "2-arm" 으로 사실에 맞게 정정됨.

cursor 모델 byte-position 재구성은 기존 invariant 를 깨지 않고 mid-string 편집을 풀어낸 깔끔한 설계, follow_tail 은 ratatui unstable feature 로 wrap-aware bottom-pin 을 정확히 구현. workspace 699 통과 + clippy clean + backwards-compat 39 개 그대로 통과 — 회귀 위험 없음.

머지 동의.

회차 3 — 회차 1 의 4 건 + 회차 2 의 2 건 모두 수렴. 카운트는 input.rs 18 + tests/ask.rs 21 = 39 로 정확하고 doc comment 의 dispatch 표현도 "2-arm" 으로 사실에 맞게 정정됨. cursor 모델 byte-position 재구성은 기존 invariant 를 깨지 않고 mid-string 편집을 풀어낸 깔끔한 설계, follow_tail 은 ratatui unstable feature 로 wrap-aware bottom-pin 을 정확히 구현. workspace 699 통과 + clippy clean + backwards-compat 39 개 그대로 통과 — 회귀 위험 없음. 머지 동의.
@@ -157,0 +158,4 @@
let inner = block.inner(area);
let para = Paragraph::new(lines).wrap(Wrap { trim: false });
let scroll = if s.follow_tail {
let total_lines = para.line_count(inner.width);

follow-tail 의 wrap-aware 처리 — Paragraph::line_count(inner.width) 로 매 프레임 wrapped row 수 재계산해 line_count - inner_height 로 pin — 이 viewport 너비 변경에도 정확히 bottom 을 잡습니다. 직접 추정 (ceil(display_cols / inner_width)) 으로 가다가 word-wrap 의 추가 break 때문에 overshoot 하던 초기 시도를 unstable feature 로 옮긴 판단이 정확. tail 키 cadence (j/k freeze, Shift-G 재engage, submission/Ctrl-L auto-engage) 도 chat UI 의 expected behavior 와 일치.

follow-tail 의 wrap-aware 처리 — `Paragraph::line_count(inner.width)` 로 매 프레임 wrapped row 수 재계산해 `line_count - inner_height` 로 pin — 이 viewport 너비 변경에도 정확히 bottom 을 잡습니다. 직접 추정 (`ceil(display_cols / inner_width)`) 으로 가다가 word-wrap 의 추가 break 때문에 overshoot 하던 초기 시도를 unstable feature 로 옮긴 판단이 정확. tail 키 cadence (`j`/`k` freeze, `Shift-G` 재engage, submission/Ctrl-L auto-engage) 도 chat UI 의 expected behavior 와 일치.
@@ -108,0 +106,4 @@
/// `cursor_byte == content.len()` is "after the last char".
///
/// `push_char` / `pop_char` operate **at the cursor**, not at the
/// end. When the cursor is at the end (the freshly-typed state),

cursor 모델을 byte-position 으로 옮기되 cursor_col()prefix.width() 로 derive 하는 설계가 깔끔합니다. UTF-8 char boundary + display column 두 개의 다른 단위를 한 source 에서 일관되게 풀어내고, p9-fb-10 의 invariant (cursor_col == display_width) 가 cursor 가 끝일 때 자동으로 성립하면서 backwards-compat 도 보장. 30+ 기존 테스트 무수정 통과한 게 이 모델의 검증.

cursor 모델을 byte-position 으로 옮기되 `cursor_col()` 을 `prefix.width()` 로 derive 하는 설계가 깔끔합니다. UTF-8 char boundary + display column 두 개의 다른 단위를 한 source 에서 일관되게 풀어내고, p9-fb-10 의 invariant (`cursor_col == display_width`) 가 cursor 가 끝일 때 자동으로 성립하면서 backwards-compat 도 보장. 30+ 기존 테스트 무수정 통과한 게 이 모델의 검증.
altair823 merged commit 973f317863 into main 2026-05-04 15:41:49 +00:00
altair823 deleted branch fix/p9-fb-22-cursor-and-autoscroll 2026-05-04 15:41:51 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#96