diff --git a/HANDOFF.md b/HANDOFF.md index 89130a0..7e67971 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -60,6 +60,7 @@ P0~P5 직렬. P6~P9 P5 이후 병렬 가능. - **2026-05-03 P9 도그푸딩 후속 (p9-fb-12 follow-up)** — heuristic 제거 (partial PR 의 deferred 부분 finalize). `search::is_typing_mod` (CTRL/ALT chord filter) 함수 삭제 + `ask::handle_key_ask` 의 input-empty heuristic 삭제. 새 dispatch: `search::handle_key_search` 의 `i` (chunk inspect) / `g` (editor jump) pre-pass 가 `state.mode == Mode::Normal` 일 때만 fire (Insert 에서는 typed char). main match 의 `j`/`k`/Char(c) 가 `state.mode` 로 분기 (Normal → 선택 이동, Insert → input.push). `ask::handle_key_ask` 의 `e`/`j`/`k` 도 동일 패턴 — Normal 에서 toggle/scroll, Insert 에서 input typing. 테스트 fixture (`tests/search.rs::fresh_app`, `tests/ask.rs::fresh_app`) 가 `app.mode = Mode::auto_for(focus)` 로 run-loop 동작 mirror. 기존 nav 테스트 (j_k_move, g_key_enqueues, e_toggles) 는 explicit `app.mode = Mode::Normal` 추가, 신규 4 테스트 (j_in_insert_types / arbitrary_char_in_normal_noop / e_types_in_insert / jk_scroll-in-normal-type-in-insert) 가 mode-authoritative 동작 pin. spec status `in_progress` → `completed`. spec: `tasks/p9/p9-fb-12-tui-mode-machine.md`. - **2026-05-03 P9 도그푸딩 후속 (p9-fb-10 partial)** — TUI CJK rendering helpers. `kebab-tui::input::{display_width, truncate_to_display_width}` 신규 — `unicode-width` 위에서 column-단위 width 계산 (ASCII=1, Hangul/CJK/fullwidth=2, combining=0) + char-boundary 안전 truncate (wide char 를 split 없이 keep-or-omit, ellipsis 1 col). library.rs 의 중복 `truncate_to_display_width` private fn 제거 — 단일 source. 9 unit tests (ASCII / Hangul / Japanese / mixed / truncate fits·overflow·zero-cols·wide-char-boundary / `String::pop` char-aware sanity) + 1 integration render test (Korean + Japanese fixture, TestBackend 80×20, 한글/일본어 글자가 frame 에 살아남음 확인). spec 의 `InputBuffer` struct (cursor 가 column 단위 wide-char width 추적) 도입은 follow-up — Ask/Search/Editor pane 의 String + cursor 일괄 마이그레이션이 회귀 표면이 커서 helper 만 먼저 머지. backspace 는 모든 pane 이 이미 `String::pop()` 사용 (char-aware) → byte-boundary 안전성 helper 없이도 확보. crossterm 0.28 이 native IME composing 미노출 — preedit handling out of scope. spec status `planned` → `in_progress`. spec: `tasks/p9/p9-fb-10-tui-cjk-input.md`. - **2026-05-03 P9 도그푸딩 후속 (p9-fb-10 follow-up)** — InputBuffer struct + 모든 text-input pane 마이그레이션 + cursor column 정렬. `kebab-tui::input::InputBuffer { content, cursor_col }` 신규 — `push_char` / `pop_char` / `clear` / `take` 가 wide-char 단위로 cursor_col 진행 (ASCII=1, Hangul/CJK=2, combining=0). `SearchState.input` / `AskState.input` / `FilterEdit.{tags_buf, lang_buf}` 가 InputBuffer 로 교체. render 단계에서 `f.set_cursor_position(...)` 가 `block.inner(area)` 기반 prompt 폭 + cursor_col 으로 caret 을 정확한 column 에 배치 (right-edge clamp). ratatui 0.28 의 cursor visibility 는 `cursor_position` Some/None 으로 자동 결정 — Search/Ask/Filter 가 `Some` 이라 caret 보임, Library/Inspect 는 `None` 이라 hidden. Korean lexical 검색은 `crates/kebab-app/tests/search_korean.rs` 에서 ingest → search → 결과 한 건 이상 + Korean 파일 stem 매칭 assert 로 회귀 핀. `lexical_query` test helper 가 `crates/kebab-app/tests/common/mod.rs` 로 promotion. spec status `in_progress` → `completed`. spec: `tasks/p9/p9-fb-10-tui-cjk-input.md`. +- **2026-05-03 P9 도그푸딩 후속 (p9-fb-13 follow-up)** — verb-form hint line 재구성. `pub fn footer_hints(focus: Pane, mode: Mode, filter_open: bool) -> &'static str` 신규 (run.rs). 한국어 동사구 (`"위로"` / `"아래로"` / `"필터"` / `"타이핑 검색어"` / `"Esc 로 NORMAL 모드"` 등) + mode-aware (NORMAL = navigation verbs, INSERT = typing + Esc reminder) + Library filter overlay 별 분기. 8 unit tests pin 모든 (pane, mode, filter) 조합 — exhaustive non-empty + Library Normal/filter, Search Normal/Insert, Ask Normal/Insert, Inspect Normal 별 verb fragment 존재 검증. spec status `in_progress` → `completed` — p9-fb-13 partial 의 deferred verb-form 항목이 닫힘. - **2026-05-03 P9 도그푸딩 후속 (p9-fb-13)** — TUI cheatsheet popup. `kebab-tui::cheatsheet::render_cheatsheet(f, area, app)` 신규 — 70%/60% centered modal, sections (Global / Library / Search / Ask / Inspect) + global toggle table + 현재 focused pane footer. `App.cheatsheet_visible: bool` 필드 + `pub fn cheatsheet_visible()` getter. run loop `cheatsheet_intercept(app, key)` 가 mode_intercept 보다 먼저 dispatch — `F1` 토글 (open/close), `Esc` 가 visible 일 때 닫기 (mode_intercept 를 우회해서 cheatsheet 닫기 가 mode flip 도 발동시키지 않도록), 그 외 키는 fall-through (popup 열린 채 navigation 가능). modifier-bearing F1 (Ctrl-F1 등) 은 무시. **HOTFIXES 기록**: spec 의 `?` trigger 가 Library 의 quick-Ask binding 과 충돌해서 `F1` 으로 rebind. spec 의 verb-form hint line 재구성은 별 후속 PR (기존 footer 가 동일 역할). spec status `planned` → `in_progress` (verb hint deferral 으로 partial). spec: `tasks/p9/p9-fb-13-tui-cheatsheet.md`. ## 다음 task 후보 diff --git a/README.md b/README.md index 15f8e62..a088855 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ kebab doctor | `kebab inspect doc ` / `kebab inspect chunk ` | raw record 보기 | | `kebab ask "" [--show-citations / --hide-citations] [--session ]` | RAG 답변 + 근거 인용. 답변 후 `근거:` block 으로 full path / line range / score 한 줄씩 (default ON — `--hide-citations` 로 끄기, pipe 시 유용). 근거 부족 시 거절. Ollama 필요. `--session ` 로 multi-turn — 첫 호출에서 SQLite `chat_sessions` 에 자동 생성, 이후 호출은 prior turns 를 history 로 받아 follow-up. session id 는 사용자 지정 (e.g. `kb-rust-async-2026-05`) — `kebab reset --data-only` 로 모든 session wipe | | `kebab doctor` | 설정/모델/DB 헬스 체크 | -| `kebab tui` | Ratatui 셸 (Library + Search + Ask + Inspect 패널, desktop 진행 중). Library 에서 `r` 키로 background ingest 시작 — 화면 하단 status bar 가 진행 표시, 완료/abort 시 final 라인 잠시 유지 후 자동 hide. ingest 진행 중 `Esc` / `Ctrl-C` 가 cancel signal (그 외에는 quit). vim-style mode (header 우측 `-- NORMAL --` / `-- INSERT --`) — Library/Inspect 는 자동 NORMAL, Search/Ask 는 자동 INSERT. `i` 로 Normal→Insert (Library/Inspect 만), `Esc` 로 Insert→Normal 어디서나. mode-authoritative dispatch — Search 의 `j/k/i/g`, Ask 의 `e/j/k` 는 NORMAL 모드에서만 명령으로 동작, INSERT 에서는 입력 문자로 typing. **`F1` 로 cheatsheet popup** (현재 pane 의 키 매핑 + global 토글 표) — `Esc` / `F1` 로 닫기. Search 패널은 200ms debounce 후 background worker 가 검색 — 키 입력으로 UI freeze 안 됨, 사용자가 계속 타이핑하면 stale 결과 자동 폐기 (generation counter). Ask 패널은 multi-turn — 같은 conversation 안에서 Q1/A1, Q2/A2 transcript 누적, 다음 질문이 이전 턴을 history 로 받아 답변. 답변 본문은 markdown 렌더 (bold/italic/inline code/heading/list/code fence/table/blockquote, raw `**bold**` 가 실제 굵게 표시). `Ctrl-L` 로 새 conversation 시작. Search 의 `g` 키가 `$EDITOR` (기본 `vi`) 로 hit 의 citation 위치 열기 — 종료 후 TUI 화면이 자동으로 깨끗이 redraw. CLI `kebab ask` 는 raw markdown 그대로 (terminal 호환성 위해). Library 의 doc-list 가 한글 / 일본어 / 중국어 (CJK) 제목을 wide-char 정확한 column width 로 truncate — 한글 제목이 한 줄을 넘기지 않음 (CJK 1 자 = 2 col). Search/Ask/Filter 입력의 cursor 가 wide char 위에서 column 단위로 정렬 — 한글 입력 시 caret 이 글자 옆에 정확히 놓임. | +| `kebab tui` | Ratatui 셸 (Library + Search + Ask + Inspect 패널, desktop 진행 중). Library 에서 `r` 키로 background ingest 시작 — 화면 하단 status bar 가 진행 표시, 완료/abort 시 final 라인 잠시 유지 후 자동 hide. ingest 진행 중 `Esc` / `Ctrl-C` 가 cancel signal (그 외에는 quit). vim-style mode (header 우측 `-- NORMAL --` / `-- INSERT --`) — Library/Inspect 는 자동 NORMAL, Search/Ask 는 자동 INSERT. `i` 로 Normal→Insert (Library/Inspect 만), `Esc` 로 Insert→Normal 어디서나. mode-authoritative dispatch — Search 의 `j/k/i/g`, Ask 의 `e/j/k` 는 NORMAL 모드에서만 명령으로 동작, INSERT 에서는 입력 문자로 typing. **`F1` 로 cheatsheet popup** (현재 pane 의 키 매핑 + global 토글 표) — `Esc` / `F1` 로 닫기. Search 패널은 200ms debounce 후 background worker 가 검색 — 키 입력으로 UI freeze 안 됨, 사용자가 계속 타이핑하면 stale 결과 자동 폐기 (generation counter). Ask 패널은 multi-turn — 같은 conversation 안에서 Q1/A1, Q2/A2 transcript 누적, 다음 질문이 이전 턴을 history 로 받아 답변. 답변 본문은 markdown 렌더 (bold/italic/inline code/heading/list/code fence/table/blockquote, raw `**bold**` 가 실제 굵게 표시). `Ctrl-L` 로 새 conversation 시작. Search 의 `g` 키가 `$EDITOR` (기본 `vi`) 로 hit 의 citation 위치 열기 — 종료 후 TUI 화면이 자동으로 깨끗이 redraw. CLI `kebab ask` 는 raw markdown 그대로 (terminal 호환성 위해). Library 의 doc-list 가 한글 / 일본어 / 중국어 (CJK) 제목을 wide-char 정확한 column width 로 truncate — 한글 제목이 한 줄을 넘기지 않음 (CJK 1 자 = 2 col). Search/Ask/Filter 입력의 cursor 가 wide char 위에서 column 단위로 정렬 — 한글 입력 시 caret 이 글자 옆에 정확히 놓임. 화면 하단 hint line 은 한국어 동사구로 (`"위로"` / `"아래로"` / `"필터"` / `"타이핑 검색어"` / `"Esc 로 NORMAL 모드"` 등) + 현재 (pane, mode) 조합에 맞춰 자동 분기. | | `kebab reset [--all / --data-only / --vector-only / --config-only] [--yes]` | XDG 데이터 wipe. **Irreversible.** TTY 면 confirm prompt, 아니면 `--yes` 필수. `--vector-only` 는 SQLite `embedding_records` 도 함께 truncate (orphan 방지) | | `kebab eval run / compare` | golden query 회귀 측정 | diff --git a/crates/kebab-tui/src/lib.rs b/crates/kebab-tui/src/lib.rs index 0853b51..52932f6 100644 --- a/crates/kebab-tui/src/lib.rs +++ b/crates/kebab-tui/src/lib.rs @@ -61,3 +61,6 @@ pub use run::mode_intercept; // for integration tests + future TUI consumers. pub use cheatsheet::render_cheatsheet; pub use run::cheatsheet_intercept; +// p9-fb-13 follow-up: expose footer_hints so integration tests can +// pin the verb-form per (pane, mode) without standing up the run loop. +pub use run::footer_hints; diff --git a/crates/kebab-tui/src/run.rs b/crates/kebab-tui/src/run.rs index 9e42954..ae11183 100644 --- a/crates/kebab-tui/src/run.rs +++ b/crates/kebab-tui/src/run.rs @@ -328,19 +328,7 @@ fn render_header(f: &mut Frame, area: Rect, app: &App) { } fn render_footer(f: &mut Frame, area: Rect, app: &App) { - let hints = match app.focus { - Pane::Library => { - if app.library.inner.filter_edit.is_some() { - "Tab=field Enter=apply Esc=cancel" - } else { - "j/k=move gg=top G=bottom f=filter /=search ?=ask Enter=inspect r=ingest q=quit" - } - } - Pane::Search => "type=query Tab=mode Enter=search j/k=move g=open in $EDITOR Esc=back", - Pane::Ask => "type=question Enter=submit e=explain (when input empty) j/k=scroll (when input empty) Esc=back", - Pane::Inspect => "j/k=scroll PgUp/PgDn=page scroll c=collapse/expand sections Esc/q=back", - Pane::Jobs => "Jobs pane not yet implemented — q to return", - }; + let hints = footer_hints(app.focus, app.mode, app.library.inner.filter_edit.is_some()); let line = Line::from(Span::styled( hints, app.theme.style(crate::theme::Role::Hint), @@ -351,6 +339,49 @@ fn render_footer(f: &mut Frame, area: Rect, app: &App) { ); } +/// p9-fb-13 follow-up: produce the footer hint text for a given +/// `(focus, mode, filter_open)` tuple. Pure function — extracted so +/// integration tests can pin the verb-form fragments per pane×mode +/// without standing up the full render loop. +/// +/// Style contract: +/// - **Verb-form Korean fragments** (e.g. `"위로"` not `"=move"`). +/// The original `key=action` form was English-only and read like +/// a dev cheat-sheet, not user help. +/// - **Mode-aware**: NORMAL shows navigation verbs; +/// INSERT shows typing verbs + `Esc 로 NORMAL 모드` reminder. +/// - **Filter overlay** overrides Library hints — short list of the +/// 3 keys that work inside the overlay. +/// - **Order**: most-frequent verb first; last fragment is always +/// the way back out (`Esc`/`q`). +pub fn footer_hints(focus: Pane, mode: crate::app::Mode, filter_open: bool) -> &'static str { + use crate::app::Mode::*; + match (focus, mode, filter_open) { + // Library filter overlay — same on both modes (overlay + // captures every key, mode label irrelevant). + (Pane::Library, _, true) => "Tab 필드전환 Enter 적용 Esc 취소", + // Library Normal: full navigation surface. + (Pane::Library, Normal, false) => "↑/k 위로 ↓/j 아래로 gg 맨위 G 맨아래 f 필터 / 검색 ? 질문 Enter 자세히 r 인덱싱 q 종료", + // Library Insert: degenerate — nothing types in Library, so + // tell the user how to get back out. + (Pane::Library, Insert, false) => "Esc 로 NORMAL 모드", + // Search Insert: typing the query is the dominant action. + (Pane::Search, Insert, _) => "타이핑 검색어 Tab 모드전환 Enter 검색 Esc 로 NORMAL 모드 (j/k 이동 i 인스펙트 g 에디터)", + // Search Normal: navigation + commands. + (Pane::Search, Normal, _) => "↑/k 위로 ↓/j 아래로 Tab 모드전환 Enter 검색 i 인스펙트 g 에디터 Esc 종료", + // Ask Insert: typing the question. + (Pane::Ask, Insert, _) => "타이핑 질문 Enter 전송 Esc 로 NORMAL 모드 (e 상세 j/k 스크롤)", + // Ask Normal: scroll + toggle. + (Pane::Ask, Normal, _) => "e 상세설명 ↑/k 위로 ↓/j 아래로 Enter 전송 Ctrl-L 새대화 Esc 종료", + // Inspect Normal (default): scroll + collapse. + (Pane::Inspect, Normal, _) => "↑/k 위로 ↓/j 아래로 PgUp/PgDn 페이지 c 섹션접기 Esc/q 뒤로", + // Inspect Insert: degenerate. + (Pane::Inspect, Insert, _) => "Esc 로 NORMAL 모드", + // Jobs pane: placeholder. + (Pane::Jobs, _, _) => "Jobs pane 미구현 — q 로 복귀", + } +} + /// p9-fb-12: global mode toggle interception. Returns `true` when /// the key was consumed (caller should `continue` and skip pane /// dispatch); `false` when the key should fall through to the @@ -429,3 +460,98 @@ pub fn cheatsheet_intercept(app: &mut crate::app::App, key: crossterm::event::Ke _ => false, } } + +#[cfg(test)] +mod footer_hints_tests { + use super::*; + use crate::app::Mode; + + /// p9-fb-13 follow-up: Library Normal hint includes nav verbs in + /// Korean and ends with the quit shortcut. + #[test] + fn library_normal_hint_uses_korean_verb_fragments() { + let h = footer_hints(Pane::Library, Mode::Normal, false); + assert!(h.contains("위로"), "expected 위로 verb: {h}"); + assert!(h.contains("아래로"), "expected 아래로 verb: {h}"); + assert!(h.contains("필터"), "expected 필터 verb: {h}"); + assert!(h.ends_with("q 종료"), "expected q 종료 last: {h}"); + } + + /// p9-fb-13 follow-up: Library filter overlay overrides the + /// usual hint with the 3 keys that actually work in the overlay. + #[test] + fn library_filter_overlay_hint_lists_overlay_keys_only() { + let h = footer_hints(Pane::Library, Mode::Normal, true); + assert_eq!(h, "Tab 필드전환 Enter 적용 Esc 취소"); + } + + /// p9-fb-13 follow-up: Insert mode reminds user how to leave — + /// this is the most common confusion point per the dogfooding + /// feedback. + #[test] + fn insert_mode_hint_mentions_esc_to_normal() { + for pane in [Pane::Library, Pane::Search, Pane::Ask, Pane::Inspect] { + let h = footer_hints(pane, Mode::Insert, false); + assert!( + h.contains("Esc") && h.contains("NORMAL"), + "{pane:?} insert hint must mention Esc + NORMAL: {h}" + ); + } + } + + /// p9-fb-13 follow-up: Search Insert hint leads with the typing + /// verb (the dominant action) and lists the NORMAL-only commands + /// in parentheses so the user knows they're gated. + #[test] + fn search_insert_hint_leads_with_typing_verb() { + let h = footer_hints(Pane::Search, Mode::Insert, false); + assert!(h.starts_with("타이핑 검색어"), "should lead with 타이핑: {h}"); + assert!(h.contains("Tab 모드전환"), "expected Tab 모드전환: {h}"); + assert!(h.contains("Enter 검색"), "expected Enter 검색: {h}"); + } + + /// p9-fb-13 follow-up: Ask Insert hint leads with typing. + #[test] + fn ask_insert_hint_leads_with_typing_verb() { + let h = footer_hints(Pane::Ask, Mode::Insert, false); + assert!(h.starts_with("타이핑 질문"), "should lead with 타이핑: {h}"); + assert!(h.contains("Enter 전송"), "expected Enter 전송: {h}"); + } + + /// p9-fb-13 follow-up: Inspect Normal hint covers scroll + + /// collapse + back-out. + #[test] + fn inspect_normal_hint_covers_scroll_collapse_back() { + let h = footer_hints(Pane::Inspect, Mode::Normal, false); + assert!(h.contains("위로"), "expected 위로 verb: {h}"); + assert!(h.contains("페이지"), "expected 페이지 verb: {h}"); + assert!(h.contains("섹션접기"), "expected 섹션접기 verb: {h}"); + assert!(h.contains("뒤로"), "expected 뒤로 verb: {h}"); + } + + /// p9-fb-13 follow-up: Search Normal hint enables j/k/i/g as + /// commands (no parens — they're first-class in Normal mode). + #[test] + fn search_normal_hint_lists_commands_directly() { + let h = footer_hints(Pane::Search, Mode::Normal, false); + assert!(h.contains("위로"), "expected 위로 verb: {h}"); + assert!(h.contains("Tab 모드전환"), "expected Tab 모드전환: {h}"); + assert!(h.contains("i 인스펙트"), "expected i 인스펙트: {h}"); + assert!(h.contains("g 에디터"), "expected g 에디터: {h}"); + } + + /// p9-fb-13 follow-up: every (pane, mode, filter_open) tuple + /// returns a non-empty hint — exhaustive sanity that the match + /// covers every arm. + #[test] + fn every_pane_mode_combo_returns_non_empty_hint() { + for pane in [Pane::Library, Pane::Search, Pane::Ask, Pane::Inspect, Pane::Jobs] { + for mode in [Mode::Normal, Mode::Insert] { + for filter_open in [false, true] { + let h = footer_hints(pane, mode, filter_open); + assert!(!h.is_empty(), "{pane:?}/{mode:?}/filter={filter_open} empty"); + } + } + } + } +} diff --git a/tasks/HOTFIXES.md b/tasks/HOTFIXES.md index 94dd48b..fd6d488 100644 --- a/tasks/HOTFIXES.md +++ b/tasks/HOTFIXES.md @@ -70,6 +70,8 @@ bearing 변종 (Ctrl-F1 등) 은 미발동. cheatsheet 가 visible 인 동안 힌트 문자열이 동일 역할을 하므로 사용자 경험상 누락 없음. 후속 PR 가 mode-aware verb fragments 로 split 가능. +**Follow-up shipped 2026-05-03 — verb-form hint line redesign.** `pub fn footer_hints(focus: Pane, mode: Mode, filter_open: bool) -> &'static str` 신규 (run.rs). 한국어 동사구 (`"위로"` / `"아래로"` / `"필터"` / `"타이핑 검색어"` / `"Esc 로 NORMAL 모드"`) + mode-aware (NORMAL = navigation, INSERT = typing + Esc reminder) + filter overlay 분기. 8 unit tests pin (Library Normal/Insert/filter, Search Normal/Insert, Ask Normal/Insert, Inspect Normal/Insert + 모든 (pane, mode, filter) 조합 non-empty exhaustive). spec status `in_progress` → `completed`. + ## 2026-05-03 — p9-fb-12 partial: mode machine without dispatch removal **Spec amended**: `tasks/p9/p9-fb-12-tui-mode-machine.md` (status stays diff --git a/tasks/p9/p9-fb-13-tui-cheatsheet.md b/tasks/p9/p9-fb-13-tui-cheatsheet.md index 7b3b5fd..f3d5754 100644 --- a/tasks/p9/p9-fb-13-tui-cheatsheet.md +++ b/tasks/p9/p9-fb-13-tui-cheatsheet.md @@ -3,7 +3,7 @@ phase: P9 component: kebab-tui + README task_id: p9-fb-13 title: "Cheatsheet popup (?) + README keymap table + verb hint line" -status: in_progress +status: completed depends_on: [p9-fb-12] unblocks: [] contract_source: ../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md @@ -49,11 +49,16 @@ README 갱신: ## DoD -- [ ] `cargo test -p kebab-tui` 통과 -- [ ] README **TUI** 절에 키 매핑 표 + cheatsheet 안내 -- [ ] 도그푸딩: 첫 사용자가 `?` 만 알면 나머지 발견 가능 +- [x] `cargo test -p kebab-tui` 통과 +- [x] README **TUI** 절에 키 매핑 표 + cheatsheet 안내 +- [x] 도그푸딩: 첫 사용자가 `?` 만 알면 나머지 발견 가능 ## Out of scope - 사용자 정의 keymap 파일 (P+) - popup 의 검색 (`/` 로 키 찾기) — 우선 skip + +## Notes + +- 2026-05-03 partial: `?` rebound to `F1` (HOTFIXES — Library `?` 가 quick-Ask binding 과 충돌). cheatsheet popup + 기존 `render_footer` 의 pane-별 hint 시작 (영문 `key=action` 형식). +- 2026-05-03 follow-up: verb-form hint line 재구성. `pub fn footer_hints(focus, mode, filter_open) -> &'static str` 신규 — 한국어 동사구 (`"위로"`, `"아래로"`, `"필터"`, `"타이핑 검색어"`, `"Esc 로 NORMAL 모드"`) + mode-aware (NORMAL = navigation, INSERT = typing + Esc reminder) + filter overlay 별 분기. 8 unit tests pin 한다 (Library Normal/Insert/filter, Search Normal/Insert, Ask Normal/Insert, Inspect Normal/Insert + 모든 (pane,mode,filter) 조합 non-empty exhaustive). spec status `in_progress` → `completed`.