feat(kebab-tui): P9-1 Ratatui shell + Library pane #42
Reference in New Issue
Block a user
Delete Branch "feat/p9-1-tui-library"
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?
요약
새 crate
kebab-tui가 Ratatui 기반 TUI shell + Library pane 을 제공. §8 facade rule 따라kebab-app만 import. p9-2/3/4 가 search/ask/inspect pane 을 같은App구조체의Option<*State>slot 으로 plug-in 하는 parallel-safety contract 확립.핵심 결정
App구조체: config + focus + library + 3 Option sub-state. p9-1 외 다른 task 가 App 정의 손대지 않음.Pane/KeyOutcomeenum: 5 pane + Continue/Quit/SwitchPane/Refresh.LibraryState: 내부LibraryStateInner가pub(crate)— docs / list_state / filter / filter_edit / needs_refresh / loading / pending_g.TuiTerminal::enter가 enable_raw_mode + AltScreen, Drop 이 종료 시 (panic 포함) restore.kebab-app::list_docs_with_config를 main thread 에서 호출, brief hang 수용. 비동기는 v1 scope 밖.unicode-width로 truncation.키 매핑 (Library pane)
j/k/Down/Up이동 ·gg맨 위 ·G맨 아래 ·f필터 ·/Search ·?Ask ·EnterInspect (선택 doc) ·q/Esc종료. error overlay 활성 시 어떤 키든 dismiss.CLI
kebab tui서브커맨드 추가.--config받아App::new(config)+run. spec DoD 의 "kebab tui launches and shows Library on a real terminal (manual smoke)" 항목.테스트 (10건,
tests/library.rs)TestBackend로 hermetic 렌더. test seam:App::populate_library_for_testing(#[doc(hidden)]).Spec deviation (HOTFIXES
2026-05-02 P9-1)render_library의<B: Backend>generic 제거 — ratatui 0.28 의Frame이 backend-agnostic. unused generic 은 clippy-D warnings거부.populate_library_for_testing테스트 seam 추가 —pub(crate) inner가 integration test 에서 안 보이므로. official UI API 아님 (#[doc(hidden)]).검증
cargo test -p kebab-tui10 passedcargo clippy --workspace --all-targets -- -D warningscleancargo build --release -p kebab-cliclean →kebab tui가--help에 노출다음 task (out of scope)
Test plan
-D warningstui서브커맨드 노출kebab tui실행 + 키 동작 확인회차 1 — 가장 큰 actionable: p9-2/3/4 미머지 시점에 / ? Enter 키가 focus 만 바꾸고 본문은 Library 그대로 라 사용자에게 거짓말. (B) 옵션 추천 — focus 바꾸되 footer 에 'not yet implemented' 안내. 그 외 nit 1건 (named-arg width specifier 코멘트). 전체 디자인 견고 — slot 패턴, Drop 가드, error overlay anyhow chain 캡쳐, §8 facade 코멘트 모두 좋음.
@@ -0,0 +11,4 @@kebab-core = { path = "../kebab-core" }kebab-config = { path = "../kebab-config" }# UI facade rule (design §8): UI crates may only touch `kebab-app`. The# search / store / embed / llm / rag layers stay invisible behind it.(칭찬) Cargo.toml 의 §8 facade rule 코멘트 ("UI facade rule (design §8): UI crates may only touch
kebab-app. The search / store / embed / llm / rag layers stay invisible behind it.") 가 미래에 누군가kebab-search또는kebab-store-sqlite를 직접 dep 추가하려고 할 때 정확한 거부 사유를 제공. spec 의 forbidden deps 절을 코드 dep 옆에 붙여둔 작은 자물쇠.@@ -0,0 +61,4 @@pub struct AskState;/// Forward-declared opaque sub-state. p9-4 fills the body.pub struct InspectState;(칭찬)
App의Option<*State>slot 패턴이 spec 의 parallel-safety contract 를 정확히 표현. p9-2 가SearchStatebody 채우면서App정의 손대지 않고 자기 모듈에서populate_search헬퍼 추가하기만 하면 됨.pub struct SearchState;opaque 가 P9-1 시점에 "이 자리는 이미 예약됨" 의 contract 를 박아둠 — p9-2/3/4 가 parallel 진행할 때 merge conflict 표면을 0 으로.@@ -0,0 +16,4 @@pub struct ErrorOverlay {pub title: String,/// Each chain link as a separate line, root-cause last.pub chain: Vec<String>,(칭찬)
ErrorOverlay가anyhow::Error를 직접 보유하지 않고Vec<String>으로 chain 캡쳐. "!Sync한 toolchain 에서 lifetime gymnastics 강요" 의 함정을 노트에 적시한 게 좋음 — 미래에 누군가Option<&anyhow::Error>로 "최적화" 시도하면 즉시 코멘트가 답.@@ -0,0 +195,4 @@.unwrap_or_else(|_| "?".to_string());let updated_short = updated.split('T').next().unwrap_or("?");format!("{title:<title_w$} {tags:<12} {updated_short:<10} {chunk_count}",(nit)
format!("{title:<title_w$} {tags:<12} ...")의title_w$는 Rust 의 named-arg width specifier 이지만 처음 보면$가 무엇인지 헷갈립니다. 미래 reader 가 같은 패턴을 다른 곳에서 쓰려고 할 때 doc 링크 한 줄 (https://doc.rust-lang.org/std/fmt/#width 의$절) 이 코멘트로 박혀 있으면 친절합니다.How to apply: 한 줄 코멘트만 추가 —
//<width$>is the named-arg width form (std::fmt §Width).. 코드 동작은 정확합니다.@@ -0,0 +47,4 @@Pane::Search | Pane::Ask | Pane::Inspect | Pane::Jobs => {handle_key_library(app, key)}};(suggestion / UX)
/?Enter 키가KeyOutcome::SwitchPane(Search/Ask/Inspect)를 emit 하면 run loop 가app.focus = p로 바꿉니다. 하지만 p9-2/3/4 는 아직 안 머지돼서Search/Ask/Inspect/Jobsarm 이 모두handle_key_library로 위임 +render_library로 placeholder 렌더. 즉 사용자가/누르면 헤더에 "Search" 가 나오는데 본문은 Library, 키 매핑은 Library — focus state 가 사실상 거짓말.Why: P9-1 단독 머지 후 사용자가
kebab tui실행하고/누르면 "왜 Library 그대로지?" 라는 의문 발생. p9-2 가 머지될 때까지 dead key 로 두는 게 더 정직.How to apply (둘 중 택일):
handle_key_library의/?Enter arm 을 일시적으로Continue로 바꾸고 footer hint 에서도 그 키들 제거. p9-2/3/4 머지 시 다시 활성화 (revert 가 단순한 한 줄)./?Enter 누르면 focus 만 바꾸고 footer 에"<pane> not yet implemented — q to return"안내 메시지 추가. 사용자에게 정직성 + 다음 task 신호.(B) 가 spec 의 키 매핑 invariant 를 그대로 유지하면서 UX 도 honest 해 추천.
@@ -0,0 +32,4 @@// propagated them; just log and let the OS recover any// remaining noise.let _ = disable_raw_mode();let _ = execute!(stdout(), LeaveAlternateScreen);(칭찬)
TuiTerminal::Drop이 raw_mode + AltScreen 모두 best-effort 복구. 특히 "Errors here would clobber a real panic if we propagated them" 코멘트가 "왜 ? 가 아니라 let _ = ?" 의 의도를 정확히 표현. terminal 을 corrupt 한 채로 user shell 에 빠지는 것보다 quiet recovery 가 정직 — color_eyre / catch_unwind 같은 추가 layer 없이도 안전하게 정리.@@ -16,1 +16,4 @@## 2026-05-02 — P9-1 TUI Library: render_library generic + test seam**Discovered**: P9-1 implementation start.(칭찬) HOTFIXES entry 가 두 deviation (
render_librarygeneric 제거, test seam 추가) 을 "왜 spec literal 그대로 못 쓰는지" + "무엇으로 대체했는지" 두 축으로 정직하게 기록. 특히<B: Backend>generic 이 ratatui 0.28 의 surface 와 어긋나 있다는 사실 ("unused generic 은 clippy-D warnings거부") 을 명시한 부분이 미래에 누군가 spec literal 따라가려고 시도할 때 즉시 답을 줌.- p9-2/3/4 미머지 시점에 / ? Enter 키로 focus 가 Search/Ask/Inspect 로 옮겨가면 헤더만 바뀌고 본문은 Library 그대로 + 키 매핑도 Library 라 사용자에게 거짓말. footer hint 가 \"Search pane not yet implemented (lands with p9-2) — q to return\" 로 전환된다. 새 stub 핸들러 `handle_key_unimplemented_pane` 가 q / Esc 만 받아 Library 로 복귀, 나머지 키는 no-op (이전 구현은 handle_key_library 로 위임해서 focus 와 다른 pane state 가 mutate 되던 절뚝거림 차단). - `format_doc_row` 의 `{title:<title_w$}` 가 std::fmt 의 named-arg width specifier — 미래 reader 가 같은 패턴 보고 헷갈리지 않도록 doc 링크 한 줄 코멘트 추가. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>회차 2 — 회차 1 지적 2건 (stub pane UX, named-arg width 코멘트) 모두 반영. 새 handle_key_unimplemented_pane 가 focus / 키 매핑 일관성 보장. 10 tests + clippy clean. 머지 가능. p9-2/3/4 가 자연스럽게 plug-in 가능.
@@ -0,0 +195,4 @@.unwrap_or_else(|_| "?".to_string());let updated_short = updated.split('T').next().unwrap_or("?");// `<width$>` is std::fmt's named-arg width form (`title_w` is the// named arg below; `$` says "use it as the padding width"). See(칭찬) std::fmt named-arg width specifier 코멘트 + 공식 docs 링크 (
#width §"Width via named parameters"). 한 줄짜리 "왜$가 의미가 있는가" 가 미래 contributor 의 고민을 절약.@@ -0,0 +97,4 @@Constraint::Length(1),]).split(f.area());render_header(f, outer[0], app);(칭찬) 새
handle_key_unimplemented_pane가 stub focus 동안 오직 q/Esc 만 받고 나머지는 no-op. 이전 구현이handle_key_library에 위임해서 사용자가 Search 로 옮긴 후 j 누르면 Library selection 이 mutate 되던 절뚝거림 차단. 코멘트 "Does NOT delegate to handle_key_library because that would let j/k/f mutate Library state while focus says otherwise — confusing UX" 가 결정 근거를 코드 옆에 박아둠 — 미래 reviewer 가 "왜 두 핸들러를 분리?" 묻지 않게.@@ -0,0 +127,4 @@Span::raw(" / "),Span::raw(pane_label),]);f.render_widget(Paragraph::new(line), area);(칭찬) footer hint 가 pane 별로 미구현 메시지 + "lands with p9-N" 명시 — 사용자에게 honest + 향후 task 신호. p9-2 머지 직후 한 줄 (
Pane::Search => "...") 만 바꾸면 자연 활성화. revert surface 가 작아 follow-up task 의 friction 0.