fix(kebab-tui): p9-fb-22 — mid-string cursor editing + Ask follow-tail auto-scroll #96
Reference in New Issue
Block a user
Delete Branch "fix/p9-fb-22-cursor-and-autoscroll"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
요약
도그푸딩 중 발견된 두 건의 TUI 버그 (Gitea #94, #95) 를 한 PR 로 묶어 처리.
InputBuffer가 append-only 라 Ask/Search/Filter overlay 의 입력 중간 편집 불가. cursor 모델을 byte-position 기반으로 재구성하고← / → / Home / End / Deletekey handler 를 세 input pane 에 wired.j로 스크롤해야 했던 문제.AskState.follow_tail추가 +render_answer가 매 프레임Paragraph::line_count(width)로 wrapped row 수 계산해 스크롤을 bottom 에 pin.주요 변경
InputBuffercursor 모델 재구성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)에서 manualDefaultimpl 로 전환 (defaultfollow_tail = true).render_answer가 매 프레임Paragraph::line_count(inner.width)로 wrapped row 수 계산해 follow-tail 시scroll = line_count - inner_heightpin. wrap-aware 이므로 viewport 너비가 바뀌어도 정확.j/k:follow_tail = false로 freeze.Shift-G(Normal 모드):follow_tail = true+scroll = 0으로 재활성화.Ctrl-L도follow_tail = true재설정.Pane key handler
Left / Right / Home / End / Deletemode 무관,Shift-GNormal 한정.Delete만 input_dirty_at reset (cursor 이동 ≠ 쿼리 변경 → debounce 타이머 유지).Dep / docs
테스트
Closes
회차 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 카테고리:
crates/kebab-tui/src/input.rs:237—}닫힘과#[cfg(test)]사이 빈 줄이 1 → 2 로 늘었습니다 (cosmetic).tasks/HOTFIXES.md의 신규 테스트 카운트 (12 + 5) 가 실제 카운트 (11 + 10) 와 어긋납니다. HOTFIXES 는 영속 기록이라 정정 필요.tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md도 동일한 카운트 오류 (12 + 6).crates/kebab-tui/src/library.rsfilter 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줄 사용 중).@@ -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).
@@ -17,0 +18,4 @@**Issues**: Gitea #94 (커서 이슈) — 텍스트 입력 후 커서 이동 불가. Gitea #95 (새 응답 이슈) — 새 응답이 viewport 아래로 추가돼도 자동으로 스크롤이 따라가지 않음. 두 건 모두 사용자 도그푸딩 중 발견.**Root cause**:테스트 카운트가 실제와 어긋납니다.
cargo test -p kebab-tui결과로 확인:현재 본 항목은 "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### InputBufferspec frozen 으로 들어가면 영구 기록이라 같은 카운트 오류가 박힙니다.
12 신규→11 신규,6 신규 Ask integration→10 신규 Ask integration으로 정정 권장.나열된 테스트 이름들 자체는 정확하니 카운트 숫자만 바꾸면 됩니다.
회차 2 — 회차 1 의 4 건 모두 잘 반영됐습니다.
input.rs빈 줄 정정,library.rs의active_buf_muthelper 도입으로 7 개 arm 이 helper 한 줄로 통일된 구조 깔끔하고, render path 에서는 label/row_offset 까지 함께 픽해야 하므로 helper 적용 안 한 판단도 합리적입니다.actionable 2 건 (모두 cosmetic nit):
library.rs의active_buf_mutdoc 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.
@@ -17,0 +18,4 @@**Issues**: Gitea #94 (커서 이슈) — 텍스트 입력 후 커서 이동 불가. Gitea #95 (새 응답 이슈) — 새 응답이 viewport 아래로 추가돼도 자동으로 스크롤이 따라가지 않음. 두 건 모두 사용자 도그푸딩 중 발견.**Root cause**:회차 1 카운트 정정의 후속 — "기존 30 개" 을 "기존 38 개" 로 바꿨는데 실제는 39 개입니다.
"38" 은 어디서 나온 숫자가 아니고 (1줄 doctest 를 빼는 식의 보정 이유도 없음 — doctest 는 unit/integration 카운트와 별개) 38 → 39 로 정정 권장.
같은 정정을 spec 파일 (tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md) 의 "기존 38 개" 에도 적용 필요.
회차 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/kfreeze,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+ 기존 테스트 무수정 통과한 게 이 모델의 검증.