도그푸딩 중 발견된 두 건 (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>
도그푸딩 item 9 — TUI Ask 답변 본문이 raw `**bold**` / `# Title` /
` ```code``` ` 그대로 보여 가독성 떨어지던 문제 해소. pulldown-cmark
파싱 → ratatui Span/Line 변환.
## 핵심 변경
- **`kebab-tui::markdown::render(text, &Theme) -> Vec<Line<'static>>`**
신규. pulldown-cmark = "0.13" (이미 kebab-parse-md 가 사용 중인
버전) 위에 build.
inline:
- `**bold**` / `__bold__` → `Modifier::BOLD`
- `*italic*` / `_italic_` → `Modifier::ITALIC`
- `~~strike~~` → `Modifier::CROSSED_OUT`
- `` `code` `` → `Role::Hint` (DIM 스타일 — 터미널 호환성 위해 bg
color 보다 안전)
- `[text](url)` → `Role::CitationMarker` + `Modifier::UNDERLINED`
block:
- heading H1/H2 → `Role::Heading` (Cyan + BOLD), H3-H6 → `Role::Title`
(White + BOLD)
- bullet list `-`/`*` → `- ` + 깊이별 indent
- ordered list `1.` → 실제 번호 prefix + indent
- fenced code block ``` ``` ``` → ` ` indented + `Role::Hint`
- blockquote `>` → 좌측 `▎` bar (중첩 시 반복) + `Role::Hint`
- table `| col |` → `| col1 | col2 |` 식 줄, `|` separator 색 강조
- horizontal rule `---` → `─` × 40
- **streaming 안전성**: 매 frame 재 parse 가 spec — pulldown
토크나이저가 µs/KB 라 비용 무시. unterminated `**` (사용자가 한창
입력 중인 inline 가 닫히기 전) 은 pulldown 이 Text 로 처리 →
literal `**` 그대로 표시 (글자 누락 X).
- **`ask::push_turn_lines` 통합**: grounded 답변에서만 markdown
렌더 사용. refusal turn (`Role::Warning` override) 와 streaming
turn (`Role::Hint`) 은 raw 로 두어 role color 시그널이 markdown
스타일에 묻히지 않도록. body line 들은 ` ` indent 로 transcript
에서 답변 본문 시각 구분.
- **CLI `kebab ask` 출력은 raw markdown** — 터미널 호환성 + pipe
처리 시 안정성 위해 (ANSI escape 없이 plain text).
## 테스트 (markdown.rs 14 unit)
- empty input → 빈 라인 1 줄 (caller scroll/measure 안전)
- plain text → 단일 라인 + paragraph blank
- bold / italic / strikethrough / inline code → 해당 modifier 검증
- link → UNDERLINED 검증
- heading H1 → BOLD 텍스트 span
- bullet list `-` / numbered list `1./2.` → prefix 검증
- code fence body → 줄별 ` ` indent 보존
- blockquote → `▎` prefix
- 2x2 table → `|`-separated 줄 검증
- unterminated `**` → 글자 누락 없음 (streaming 안전성 회귀 방지)
- composite (heading + para + list + code) → 문서 순서 보존
기존 75 TUI 테스트 + 신규 14 markdown = 89 통과. clippy clean.
## 문서
- README `kebab tui` 행에 markdown 렌더 안내 + CLI 는 raw 명시
- HANDOFF: 2026-05-03 entry
- spec status planned → in_progress
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
새 crate `kebab-tui` 가 §8 facade rule 따라 `kebab-app` 만 import.
Ratatui 0.28 + crossterm 0.28 기반 shell 이 다음을 제공:
- `App` 구조체: config + focus + library + 3 Option sub-state slot
(search/ask/inspect — p9-2/3/4 가 자기 모듈에서 채우는 parallel-safety
contract). p9-1 외에 App 정의 손대지 않음.
- `Pane` enum (Library/Search/Ask/Inspect/Jobs).
- `KeyOutcome` (Continue/Quit/SwitchPane/Refresh).
- `LibraryState` + 내부 inner: docs / list_state / filter / filter_edit /
needs_refresh / loading / pending_g.
- `render_library` (Frame, area, &App) — heading/body, filter overlay
toggleable, Korean/wide-char 너비는 unicode-width 로 계산.
- `handle_key_library`: j/k/Down/Up 이동, gg/G 끝, f 필터 overlay,
/=>Search ?=>Ask Enter=>Inspect, q/Esc 종료. error overlay 가 켜
있으면 어떤 키든 dismiss.
- 필터 overlay: tags_any (CSV) + lang 두 필드, Tab cycle, Enter
apply→Refresh, Esc cancel.
- `ErrorOverlay`: anyhow chain 캡쳐 후 popup 렌더 (Clear + 빨간 border).
- 터미널 lifecycle: `TuiTerminal` 가 enter raw mode + alt screen,
Drop 이 종료 시 (panic 포함) restore — 사용자 쉘 깨지지 않게.
- 비동기 없음: facade 호출은 main thread 동기. v1 의 brief hang 수용.
CLI: `kebab tui` 서브커맨드 추가, --config 받아 App::new + run.
테스트 10건 (`tests/library.rs`, TestBackend 사용):
- 빈 library / 3-doc render / q,Esc quit / / Search 전환 / ? Ask 전환
- Enter 빈 list 무동작 / Enter Inspect 전환 / j 이동 (3-step clamp) /
f 필터 overlay → 입력 → Enter Refresh.
Test seam: `App::populate_library_for_testing` (#[doc(hidden)]) 가
`pub(crate)` inner 를 우회. spec parallel-safety contract 그대로 유지.
Spec deviation (HOTFIXES `2026-05-02 P9-1`):
- `render_library` 의 `<B: Backend>` generic 제거 — ratatui 0.28 의 Frame
이 backend-agnostic.
- `populate_library_for_testing` 추가 (test seam, 공식 API 아님).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>