Commit Graph

8 Commits

Author SHA1 Message Date
9a923474dd feat(kebab-tui): SearchState.input → InputBuffer + cursor placement
Migrates SearchState.input from String to InputBuffer so wide-char
(Hangul/CJK) keystrokes advance the terminal cursor by display columns
instead of char count. Adds cursor placement in render_input_bar via
f.set_cursor_position and a Hangul round-trip pin in tests/search.rs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 09:29:41 +00:00
765ffc97c5 feat(kebab-tui): p9-fb-12 follow-up — heuristic 제거, mode-authoritative dispatch
p9-fb-12 partial (PR #84) 의 deferred 부분 finalize. spec contract 의
\"기존 P9-3 ask 의 e/j/k input-empty heuristic 제거 — mode 로 명확히\"
완료. spec status `in_progress` → `completed`.

## 핵심 변경

- **`search::is_typing_mod`** (CTRL/ALT chord filter) 함수 삭제.
  search Char dispatch 가 `state.mode` 로 분기:
  - Normal + plain `j`/`k` → 선택 이동 (Char 이라도 Normal 이면
    navigation)
  - Insert + plain `j`/`k`/Char(c) (chord 제외) → input.push
  - Insert + CTRL/ALT chord → no-op (예약 — 향후 binding 위해)
  - Normal + 그 외 Char → no-op (no typing in Normal)
- **`search::handle_key_search` 의 `i` (chunk inspect) / `g` (editor
  jump) pre-pass** 가 `state.mode == Mode::Normal` 일 때만 fire.
  Insert 모드면 typed char (input 에 push). 기존 SHIFT-aware
  matches!() 가드는 Normal-mode 진입 가드로 흡수.
- **`ask::handle_key_ask`** 의 input-empty heuristic 삭제. e/j/k:
  - Normal + `e` → toggle explain
  - Normal + `j` → scroll down (saturating_add)
  - Normal + `k` → scroll up (saturating_sub)
  - Insert + 모든 plain Char (chord 제외) → input.push
- **테스트 fixture** (`tests/search.rs::fresh_app`,
  `tests/ask.rs::fresh_app`) 에 `app.mode = Mode::auto_for(focus)`
  추가 — run loop 의 auto-flip 동작을 테스트가 mirror.
- **기존 nav 테스트** (`j_k_move_selection_within_bounds`,
  `g_key_enqueues_pending_editor_request`, `e_toggles_explain_in_
  normal_mode`) 가 `app.mode = Mode::Normal` 명시.
- **신규 4 테스트** mode-authoritative 동작 회귀 방지:
  - search: `j_in_insert_types_does_not_move_selection`,
    `arbitrary_char_in_normal_mode_is_noop`
  - ask: `e_types_in_insert_mode_does_not_toggle_explain`,
    `jk_scroll_in_normal_mode_type_in_insert`

## 테스트

- 기존 109 + 신규 4 = 113 TUI 테스트 통과 (38 lib + 20 ask + 12
  inspect + 10 library + 6 mode + 25 search + 2 chat — search 23→25,
  ask 18→20)
- `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy --workspace --all-targets -- -D warnings` clean

## 문서

- README `kebab tui` 행: \"mode-authoritative dispatch — Search 의
  j/k/i/g, Ask 의 e/j/k 는 NORMAL 모드에서만 명령으로 동작, INSERT
  에서는 입력 문자로 typing\" 명시
- HANDOFF: 2026-05-03 follow-up entry
- spec status `in_progress` → `completed`

## HOTFIXES

p9-fb-12 partial PR (#84) 의 \"Deferred\" 항목이 본 PR 로 finalized
— HOTFIXES 새 entry 불필요 (기존 entry 가 이미 deferral 사유 + 해결
조건 명시).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:50:04 +00:00
a99f81398c review(p9-fb-08): 회차 1 nit 반영
- `SearchState.worker_thread` 필드 제거 — `JoinHandle` 을 저장만 하고
  어디서도 join 안 했음. fire_search 가 spawn 후 handle 을 즉시 drop
  하면 OS 가 thread 를 detach (search 는 pure read 라 cleanup 의무
  없음). YAGNI — ask.rs 의 thread 와 달리 cancel/observe 수요가 없는
  fire-and-forget. doc 으로 의도 명시.
- `debounce_due` 가 `pub` 으로 노출 — 새 skip 분기 (`searching && 같은
  query`) 회귀 테스트 추가:
  - `debounce_due_skips_when_in_flight_for_same_query`: 같은 input/mode
    재입력 시 spawn 안 함 (worker 누적 방지)
  - `debounce_due_fires_when_in_flight_for_different_query`: 사용자가
    in-flight 보다 빠르게 새 query 입력하면 정상 spawn (poll_worker 의
    stale guard 가 이전 결과 처리)
- `search_state_with` 헬퍼: `SearchState::default()` + field 재할당
  패턴이 clippy `field_reassign_with_default` 위반 → `#[allow(...)]`
  로 lint 무시 (테스트 helper 의 가독성 우선).

23 tests/search.rs + 35 lib + 18 ask + 12 inspect + 10 library = 98
통과. clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:55:27 +00:00
fd8597c696 feat(kebab-tui): p9-fb-08 async search worker + generation counter
도그푸딩 item 6 — TUI search 의 200ms debounce 후 동기 호출이 vector
/ hybrid 모드에서 50-200ms 동안 UI 를 freeze 시키던 문제 해소. 별
thread 에서 search 돌리고 결과 mpsc 로 받음. 사용자가 계속 타이핑하면
stale 결과 자동 폐기 (generation counter pattern, ask.rs 의 worker
패턴과 동일).

## 핵심 변경

- **`SearchState` 필드 3 개 신규**:
  - `generation: u64` — 각 spawn 마다 increment, worker 가 carry
  - `worker_thread: Option<JoinHandle<()>>`
  - `worker_rx: Option<Receiver<SearchWorkerMessage>>`
- **`SearchWorkerMessage`** (`pub enum`) — 단일 변종 `Done {
  generation, result }`. ask.rs 의 token stream 과 달리 search 는
  최종 결과만 한 번 send, 그래서 enum 으로 추후 확장 여지 둠.
- **`fire_search`** rewrite: generation+1 → debounce snapshot 갱신 →
  `std::thread::Builder::spawn` 으로 별 thread, `kebab_app::search_
  with_config(cfg, query)` 호출, channel 로 `(gen, result)` post.
  return 은 즉시 — event loop 안 막힘.
- **`poll_worker`** 신규 (`pub`, integration test 위해 노출): tick
  마다 try_recv. `gen != s.generation` 이면 stale → silently drop +
  `searching` 그대로 (newer worker 가 처리). 일치하면 hits 적용 +
  `searching=false`. Disconnect 면 worker 패닉 처리 — searching
  clear, 다음 tick 의 debounce_due 가 재 spawn.
- **`debounce_due`** 강화: `searching && last_query == 현 input/mode`
  케이스 skip — 같은 query 재 spawn 방지. 기존 dedupe 도 유지.
- **run loop** 의 `Pane::Search` 분기에 `poll_worker(app)` 한 줄
  추가 (debounce_due 호출 직전). 매 tick drain.

## 테스트 (tests/search.rs 신규 4 개)

- `poll_worker_applies_fresh_result_to_hits` — gen 일치 시 hits 적용
  + searching clear + rx drain
- `poll_worker_drops_stale_result` — gen 불일치 시 hits 비어 있음
  + searching 유지 (newer worker 기다림)
- `poll_worker_noop_when_no_rx` — 평상시 tick 에 noop, 기존 hits
  보존
- `poll_worker_handles_disconnected_channel` — 워커 panic (tx drop)
  복구 — searching clear, rx 비움

기존 17 search + 35 lib + 18 ask + 12 inspect + 10 library = 92
통과. clippy clean.

## 문서

- README `kebab tui` 행: "Search 패널은 200ms debounce 후 background
  worker, stale 결과 자동 폐기" 한 줄 추가
- HANDOFF: 2026-05-03 entry
- spec status planned → in_progress

## Out of scope

- 캐시 (p9-fb-19 별도)
- 동일 query 의 inflight worker 합치기 — 현재는 dedupe + 가장 최근
  spawn 만 살아남는 fire-and-forget. 합치는 건 mpsc multiplexing
  로직 필요해 P+ 로 미룸.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:50:56 +00:00
8c22e3792c review(p9-fb-09): 회차 1 nit 반영
- `App.pending_editor` / `force_redraw` 를 `pub(crate)` 로 좁힘.
  외부 caller (kebab-cli/desktop) 는 enqueue 할 일 없고, 잘못
  mutate 하면 \"set 후 다음 tick 에 drain\" invariant 가 깨짐.
- 외부 read access 가 필요한 경우를 위해 `App::pending_editor()` 읽기
  전용 accessor 추가 — integration test (`tests/search.rs`) 가 사용.
- `App.force_redraw` doc comment 의 \"ratchet incremented\" 표현을
  실제 type (bool flag) 에 맞게 \"when set, the next draw clears\" 로
  교체.
- `editor::tests::unspawnable_program_surfaces_program_name_in_error`
  를 `command_status_returns_not_found_for_missing_program` 으로
  rename + doc 수정 — 실제로 `with_external_program` 호출하지 않고
  `Command::status()` 만 검증한다는 점을 솔직하게 명시 (helper
  end-to-end 는 dogfooding 으로 검증).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 02:35:10 +00:00
2c10cb7e7a feat(kebab-tui): p9-fb-09 external editor return — terminal restore
Search `g` 키 (citation jump) 후 TUI 화면이 깨지는 버그 수정. 도그푸딩
item 7 — `g` 로 vim 띄우고 `:q` 후 복귀하면 이전 frame 의 잔상이 새
draw 위에 겹쳐 보였음.

## 핵심 변경

- **`kebab-tui::editor::with_external_program(&mut TuiTerminal, Command)`**
  helper 추가. suspend / spawn / restore 시퀀스를 RAII guard 로 atomic
  하게 묶어 panic 발생해도 raw mode + alt screen 복구 보장:
    1. LeaveAlternateScreen + Show cursor + disable_raw_mode
    2. Command::status() 로 child 실행
    3. enable_raw_mode + EnterAlternateScreen + Hide cursor +
       `terminal.clear()` ← 이 한 줄이 핵심 fix
- **`App.pending_editor: Option<EditorRequest>`** 추가. 키 핸들러
  (현재 `kebab-tui::search::handle_key_search` 의 `g`) 가 직접 spawn
  하는 대신 EditorRequest 를 enqueue, 실제 spawn 은 run loop 가
  `TuiTerminal` 핸들 in scope 일 때 처리.
- **`App.force_redraw: bool`** ratchet. with_external_program 종료 후
  set, run loop draw 직전 check → terminal.clear() 후 reset. editor
  외 다른 향후 use case (config reload, theme change 등) 도 같은 hook
  사용 가능.

## 가시성 정리

`with_external_program` / `jump_to_citation` 은 `pub(crate)` 로 좁혀짐
— `TuiTerminal` 자체가 module-private (raw mode + alt screen 의 안전
한 lifecycle 은 `Drop` 만 보장) 이므로 외부 caller 는 `App.pending_
editor` enqueue 패턴으로만 spawn 요청 가능. 외부 surface (`build_jump_
command`, `handle_key_search`, `render_search`) 는 그대로.

## 테스트

- `unspawnable_program_surfaces_program_name_in_error` — helper 의 spawn
  실패 경로 (ENOENT) error context 검증
- `g_key_enqueues_pending_editor_request` — `g` on hit → EditorRequest
  enqueue, citation 정보 보존
- `g_key_with_no_hits_does_not_enqueue` — empty hits → no-op
- 기존 17 개 search 테스트 + 14 lib + 18 ask + 12 inspect + 10 library
  모두 통과
- `cargo clippy -p kebab-tui --all-targets -- -D warnings` clean

## 문서

- README: `kebab tui` 행에 Search `g` 동작 + 자동 redraw 안내
- HANDOFF: 2026-05-03 머지 후 발견 entry
- spec status: `planned` → `in_progress`

후속 task (p9-fb-20 의 citation jump in TUI Ask 등) 가 같은
`pending_editor` queue + `with_external_program` helper 위에 build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 02:32:01 +00:00
0732b3ffbe review(p9-2): 회차 1 지적 반영
1. **Citation::Page 분기 fix** — `args.push(format!(\"# page {page}\"))` 가
   vim/code/cursor 에 \"두 번째 파일\" 로 해석돼 의도 외 동작 (split / new
   buffer). 마지막 push 제거, path 만 열고 `tracing::debug!` 한 줄.
   PDF 페이지 jump 는 사용자 PDF reader 책임 — `KEBAB_EDITOR_JUMP_FORMAT`
   env hook 은 P+ enhancement.
2. **j/k/g 의 SHIFT modifier 차단** — `is_typing_mod` 가 SHIFT 를 typing
   으로 취급하던 부분이 J/K/G 를 selection 키로 흡수해 \"JSON\" / \"PostgreSQL\"
   / \"Go\" 같은 대문자 검색어 깨짐. arrow 키 (Down/Up) 는 modifier 무관 유지,
   문자 키 (j/k/g) 는 `KeyModifiers::NONE` 만. SHIFT-J / SHIFT-G 회귀 테스트
   2건 추가.
3. **`format_hit_lines` 의 unused `_width` 인자 제거** — ratatui 자동
   truncate 신뢰 (Library 의 한국어 column 정렬은 별도 path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:42:49 +00:00
0490b6a126 feat(kebab-tui): P9-2 Search pane — input + dense hits + preview + editor jump
Library 의 / 키가 활성화. App.search slot 이 lazy 채워지고 (run loop 가 SwitchPane(Search) 받을 때),
debounce 200 ms 후 kebab-app::search 호출, 선택된 hit 의 chunk 를 preview pane 에 표시.
g 키로 $EDITOR (vim/nvim/code/cursor 자동 감지) 에서 citation 위치 열림.

핵심:
- SearchState 본체 (`app.rs` 의 forward decl 채움) — input / mode / hits /
  selected_hit / input_dirty_at / last_query / searching / preview.
- `src/search.rs` (신규):
  - `render_search(f, area, state)` — 3-pane layout (input bar / 결과 리스트 / preview).
    각 hit 는 §1.5 dense 4-line format (rank.score URI / heading / snippet).
  - `handle_key_search`: typing → input + dirty mark. Tab → mode 순환. Enter →
    immediate refresh. j/k → 선택 이동 + preview invalidate. g → editor jump
    (RAII raw-mode suspend). Esc → Library 복귀.
  - `build_jump_command(citation, editor_env, workspace_root)` 가 vim 류
    `+<line> path` / VS Code `code -g path:line` / cursor `cursor -g`
    자동 분기. unit test 로 잠금.
  - `jump_to_citation` 가 raw-mode + AltScreen 을 RAII 로 suspend/restore
    (panic 안전).
  - run-loop hook 4 함수: `debounce_due` / `fire_search` /
    `refresh_preview` (private to crate).
- run.rs:
  - Pane::Search arm 이 `handle_key_search` 로 dispatch + `render_search`.
  - SwitchPane(Search) 시 `app.search = Some(SearchState::default())` lazy init.
  - Idle tick 마다 debounce_due → fire_search, preview None → refresh_preview.
- 테스트 13개 (`tests/search.rs`) — Esc/typing/backspace/Tab cycle/Enter
  refresh/j-k 이동/jump cmd vim+code+args/render w/hits/empty render/no slot.

Spec deviation (HOTFIXES `2026-05-02 P9-2`):
- `render_search<B: Backend>` generic 제거 (P9-1 와 동일 사유 — ratatui 0.28
  Frame backend-agnostic).
- `jump_to_citation` 가 `workspace_root: &Path` 인자 추가. Citation.path 가
  workspace 상대 라 editor 호출 시 절대 경로 필요. spec literal 의 시그니처
  는 unimplementable.

Docs (sync rule):
- README: TUI 행 \"Library + Search 패널, ask/inspect 진행 중\" + Quick start
  의 `kebab tui` 코멘트 갱신.
- HANDOFF: 한 줄 요약 + Phase status (P9 1/5 → 2/5) + deviation 한 줄 추가.
- HOTFIXES: P9-2 entry 추가.
- tasks/p9/p9-2 status: completed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:38:17 +00:00