Commit Graph

49 Commits

Author SHA1 Message Date
58ac62d53a feat(search): provenance 출처 필터 — [[workspace.sources]] 멀티소스 + --source/--source-type
혼합 출처 KB(위키+jira 등)에서 색인은 전부 하되 질의 시 출처로 좁히는 provenance
레버. 전역 trust 곱셈가중(weighted-RRF)은 A/B 에서 반증(θ=0.85 만으로 incident MRR
0.918→0.340 절벽, 점수 압축) — 필터가 see-saw 없는 올바른 레버.

- config [[workspace.sources]] (각 id/root/exclude/trust_level/source_type);
  단일 root 는 implicit `default` source 로 정규화. validate: id 유일·비어있지 않음.
- config schema v3→v4 (step_3_to_4, root→[[workspace.sources]] id=default 미러, 멱등)
- V014 documents.source_id 컬럼+인덱스 (additive, DEFAULT 'default', 재색인 0)
- Metadata.source_id + BodyHints trust precedence(frontmatter > source 기본값 > Primary)
- ingest: --root 미지정 시 resolved_sources() 순회 + doc 마다 source_id/trust stamp
- 검색 SearchFilters.source_type/source_id → lexical + vector 두 site (IN, OR)
- CLI kebab search --source <id> / --source-type <type> (repeatable/comma-sep)

도그푸딩(620 doc, jira400+wiki220): --source wiki 로 개념 질의 MRR 0.780→0.810,
--source jira 로 incident 0.918→0.975. trust precedence 실측(jira=secondary 기본값).

version bump 0.28.0 → 0.29.0 (신규 CLI flag + config 키 + V014 migration → minor).
follow-up: MCP search 필터 미노출 · kebab list source_id 미표시 · RAG provenance 라벨.

자세한 내용: tasks/HOTFIXES.md (2026-06-21), docs/release-notes/v0.29.0-draft.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-21 08:35:19 +00:00
e03d03cb26 test: 별칭 전용 테스트 삭제 + 영향 테스트/fixture 갱신
kebab-search/tests/lexical.rs 의 alias 채널 테스트 + insert_chunk_with_aliases
헬퍼 제거(body 회수 회귀 테스트로 대체). Chunk 리터럴 aliases: None 제거
(embedding_records_fk/idempotency/inspect). chunk 스냅샷 fixture 의 aliases
키 제거. config_migrate 는 ingest.code 앵커로, corpus_revision/search_lexical
주석은 V013 비-bump 명시로 갱신.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:37:58 +00:00
848b75c069 feat(core): Chunk.aliases 필드 (doc-side expansion)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 02:09:39 +00:00
b134ae9dd5 feat(chunk): integrate lindera korean morphological tokenizer
V009 의 tokenized_korean_text column 에 들어갈 morpheme sequence
를 lindera ko-dic 으로 분해. chunk builder pipeline 의 chunk 생성
직후 시점에서 호출 → chunk struct 의 field 에 pre-fill → store
의 put_chunks 가 단일 transaction 안에서 INSERT.

- crates/kebab-core/src/chunk.rs: Chunk struct 에
  tokenized_korean_text: Option<String> field 추가 (#[serde(default)]).
- crates/kebab-chunk/src/lib.rs: tokenize_korean_morphological()
  helper + OnceLock 캐싱 + fallback (None) 정책.
- crates/kebab-chunk/Cargo.toml: lindera features = ["embed-ko-dic"]
  추가 (DictionaryKind::KoDic 활성화에 필요).
- 모든 chunker (tier2_shared, md_heading_v1, pdf_page_v1, 9개
  code AST v1): Chunk 리터럴에 tokenized_korean_text pre-fill.
- crates/kebab-store-sqlite/src/documents.rs::put_chunks: INSERT
  SQL column list + placeholder + binding 갱신 (12번째 column).
- crates/kebab-chunk/tests/tokenize_korean.rs: 단위 테스트 2개.

lindera 3.0.7 API 정정: load_dictionary_from_kind →
load_embedded_dictionary, Token.text → Token.surface.

Spec: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md §6.2
Plan: docs/superpowers/plans/2026-05-28-v0.20.x-korean-morphological-tokenizer-plan.md (S3)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 10:22:15 +00:00
685007789a style: cargo fmt --all (round 4 ingest log feature follow-up)
Phase C4 executor 의 마지막 `fix(test): clippy + fmt fixes` commit 이
test file 부분만 fmt 적용. workspace 전체 fmt 누락 발견 → cargo fmt --all
적용. 모든 import alphabetical reorder + line wrapping 정합.

추가 untracked artifact 동시 commit:
- docs/superpowers/specs/2026-05-28-v0.20-ingest-log-spec.md (491 line, ACCEPT)
- docs/superpowers/plans/2026-05-28-v0.20-ingest-log-plan.md (616 line, ACCEPT)

workspace test: 1370 passed / 0 failed / 50 ignored, ingest_log_smoke green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 04:18:40 +00:00
7c85de065a chore: workspace-wide cleanup — clippy::pedantic baseline + auto-fix
cut PR v0.18.0 전 마지막 정리. 사용자 요청: "전체 코드베이스를 깔끔하고 알아보기 쉽게".

## Workspace lints

- `Cargo.toml` 의 `[workspace.lints.clippy]` 에 `pedantic = "warn"` (priority -1) + 의도적 allow-list 추가:
  - cast_possible_truncation / cast_possible_wrap / cast_sign_loss / cast_precision_loss — ONNX i64 / hash modular reduction 등 의도적 truncation.
  - doc_markdown / missing_errors_doc / missing_panics_doc — cosmetic doc style.
  - too_many_lines / module_name_repetitions / must_use_candidate / needless_pass_by_value / manual_let_else / items_after_statements / similar_names — informational only.
  - format_collect / match_wildcard_for_single_variants / trivially_copy_pass_by_ref / unnecessary_wraps — intentional patterns (exhaustive match, future Result variants 등).
  - default_trait_access — `Foo::default()` 가 idiomatic.
  - float_cmp — NLI / RRF score 의 explicit threshold 비교 의도.
  - struct_excessive_bools / case_sensitive_file_extension_comparisons / naive_bytecount / ignore_without_reason — domain-specific 의도.
  - format_push_string / return_self_not_must_use / match_same_arms — builder / wire-label / hot-path 패턴 보존.
  - needless_continue / used_underscore_binding / nonminimal_bool / unreadable_literal / many_single_char_names / doc_link_with_quotes / assigning_clones / collapsible_str_replace / trivial_regex / elidable_lifetime_names / range_plus_one / explicit_iter_loop / implicit_hasher / ref_option — remaining low-value style.
- 각 24 crate `Cargo.toml` 에 `[lints] workspace = true` 추가.

## Auto-fix

`cargo clippy --workspace --all-targets --fix` 적용 — 128 files changed, 552 insertions / 472 deletions. 주로:
- uninlined_format_args (~18): `format!("{}", x)` → `format!("{x}")`.
- redundant_closure_for_method_calls (~33): `.map(|x| x.foo())` → `.map(T::foo)`.
- 그 외 mechanical refactor.

## 검증

- `cargo clippy --workspace --all-targets -j 1 -- -D warnings` clean (pedantic + 모든 lint group).
- `cargo test --workspace --no-fail-fast -j 1` — **1293 tests pass + 1 pre-existing flaky fail** (`kebab-mcp::tools_call_ask_multi_hop::ask_tool_routes_multi_hop_true_to_decompose_first`, HOTFIX candidate, cleanup 무관). 회귀 0.

Wire 영향: 없음.
Behavior 영향: 없음 (mechanical refactor only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 03:01:58 +00:00
546c1564b0 feat(rag): fb-41 PR-9c-1 — core types + wire scaffolding (NLI verification)
Surface-only PR (no behavior wiring — that's PR-9c-2):
- kebab-core: RefusalReason::NliVerificationFailed + NliModelUnavailable (serde rename_all="snake_case", wire = identical strings).
- kebab-core: Answer.verification: Option<VerificationSummary> field (additive minor wire — pre-v0.18 reader 무영향).
- kebab-core: VerificationSummary { nli_score: f32, nli_threshold: f32, nli_passed: bool } struct + lib.rs 재-export.
- kebab-config: NliCfg { model, provider } + ModelsCfg.nli (default Xenova/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7).
- kebab-config: RagCfg.nli_threshold: f32 (default 0.0 = disabled, spec §2.6 single gate).
- kebab-config: env override KEBAB_MODELS_NLI_MODEL/PROVIDER + KEBAB_RAG_NLI_THRESHOLD (parse 실패 시 tracing::warn + default 유지).
- kebab-rag: RagPipeline.verifier: Option<Arc<dyn NliVerifier>> field + with_verifier builder (모두 #[allow(dead_code)] — PR-9c-2 의 step 8.5 hook 가 활성화 시 제거). RagPipeline::new signature 유지 (round-2 NEW-M1 Option B).
- kebab-rag: Cargo.toml 에 kebab-nli path 의존 추가.
- kebab-store-sqlite + kebab-tui: 두 신규 RefusalReason variant 에 대한 exhaustive match arm 추가 (snake_case label / 표시 문구).
- 모든 Answer 구축 site (rag 6 + cli/tui/eval 3 fixture) 에 verification: None 추가.
- wire schemas: answer.schema.json verification field + \$defs.VerificationSummary + refusal_reason.enum 2 추가. error.schema.json code.enum + details.description 2 추가 (forward-looking reserved).
- docs/ARCHITECTURE.md: Mermaid Adapters subgraph 의 nli 노드 + rag→nli + app→nli (forward-looking) + nli→config edges. nli→core edge 는 skip (kebab-nli/Cargo.toml direct dep 가 config 만, ARCHITECTURE 컨벤션 = direct deps only). 디렉토리 트리에 crates/kebab-nli/ 추가.

Tests: kebab-core 3 (serde rename + verification skip + struct shape) + kebab-config 6 (defaults + legacy + env + malformed env) + kebab-cli wire 5 (schema verification + enum 검증).
검증: cargo test --workspace -j 1 회귀 0 (pre-existing kebab-mcp::tools_call_ask_multi_hop flaky 1개 동일 — spec 에 명시된 known-flaky). cargo clippy --workspace --all-targets -D warnings clean.
Wire 영향: additive minor — answer.v1 의 verification optional + refusal_reason.enum 확장 + error.v1.code 확장.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:27:36 +00:00
b6756f8ce3 chore(tui): PR #173 회차 1 리뷰 반영
test `spawn_snapshot_multi_hop_into_askopts` →
`ask_state_multi_hop_field_default_false_and_round_trips` 로 rename.
이전 이름은 spawn 동작 검증을 약속했으나 본문은 단순 field
default + setter round-trip 만 검증 — name 과 실제 의도의 mismatch.
새 이름이 실제 검증 (field shape pin) 과 정확히 일치.

doc string 도 spawn 동작은 별 path (live dogfood) 로 검증된다고
명확히 표기 — test 의 책임 범위가 무엇인지 reader 가 즉시 파악.

검증
- `cargo test -p kebab-tui -j 1 --test ask` — 42 test (6 multi-hop
  포함) 모두 통과.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:29:36 +00:00
016f380428 feat(tui): fb-41 PR-6 — TUI Ask multi-hop toggle + hop trace summary
fb-41 multi-hop RAG 의 **마지막 component PR** (PR-5 머지 직후). TUI Ask
패널의 user-facing surface — F2 toggle, multi-hop badge, status panel
의 hop count summary, cheatsheet 안내. v0.18.0 cut 준비.

설계: docs/superpowers/specs/2026-05-25-p9-fb-41-multi-hop-rag-design.md
계획: docs/superpowers/plans/2026-05-25-p9-fb-41-multi-hop-rag.md (PR-6 단락)

## TUI surface

- `crates/kebab-tui/src/app.rs`:
  - `AskState.multi_hop: bool` field + Default false. 사용자 토글
    상태를 인-패널 보존, 대화 history 와 직교 — F2 flipping mid-
    conversation 도 turns 보존 (다음 turn 만 다른 pipeline 으로 route).
- `crates/kebab-tui/src/ask.rs`:
  - `handle_key_ask` 에 `(KeyCode::F(2), _) → s.multi_hop = !s.multi_hop`.
    Mode-agnostic (physical function key — Normal/Insert 양쪽 작동, typing
    ambiguity 없음). Briefing 의 candidate (F2 vs Ctrl-T) 중 F2 채택 —
    Ctrl-M 은 Enter 와 collision 이미 명시, F2 가 cleanest.
  - `spawn_ask_worker` 의 `AskOpts.multi_hop` 가 spawn 시점에 토글값
    snapshot. 이후 F2 flip 은 다음 Enter 부터 적용 (in-flight turn 무영향).
  - `render_input` 의 input pane title 에 `F2=multi-hop` binding 안내
    추가 + prompt row 에 `multi-hop` badge (Success 녹색, toggled-on 일
    때만). 사용자가 어떤 pipeline 으로 다음 query 를 보낼지 항상 가시.
  - `render_status` 의 status panel 에 `multi-hop: N hops` line 추가
    (last_answer.hops 가 Some 일 때만). forced_stop 발생 시
    `forced_stop=K` suffix — depth/pool cap tuning 단서.
- `crates/kebab-tui/src/cheatsheet.rs`:
  - Ask section 에 `F2 toggle multi-hop pipeline` entry 추가.

## 변경 없음 (의도된 deferral)

- `InspectTarget::Hop(turn_index)` variant — plan 의 PR-6 stretch goal.
  per-iter hop trace detail 을 Inspect 패널에 노출하는 기능은 별 PR
  (PR-6b 또는 v0.18 dogfood follow-up). PR-6 의 핵심 가치
  (사용자가 multi-hop pipeline 을 토글하고 결과의 hop count 를 본다)
  는 status panel 의 한 줄 summary 로 100% cover. Inspect 진입은
  multi-hop 사용자가 *드물게* 필요한 surface — v0.18 cut 부담 회피.
- prompt_template_version (`rag-multi-hop-v1`) — 그대로.
- MCP / CLI surface — PR-4 / PR-5 의 책임.

## Tests (`tests/ask.rs` 신규 6 multi-hop pins)

- `f2_toggles_multi_hop_flag_from_insert_mode`: Insert 에서 F2 toggle
  (fresh_app default mode).
- `f2_toggles_multi_hop_flag_from_normal_mode`: Normal 에서도 동일
  — mode-agnostic 회귀 핀.
- `input_pane_shows_multi_hop_badge_when_toggled_on`: 토글 on 시
  prompt row 에 `multi-hop` 등장 + title 의 `F2=multi-hop` binding
  hint 등장.
- `input_pane_omits_multi_hop_badge_when_toggled_off`: 토글 off 시
  prompt row 의 badge 부재 (title hint 는 유지 — 사용자 discoverability).
- `status_panel_summarizes_hops_when_answer_has_trace`: 3-hop trace
  (Decompose + Decide + Synthesize) → `multi-hop: 3 hops` line.
- `status_panel_omits_hops_summary_for_single_pass`: hops=None → 본문
  에 summary line 부재 (title binding hint 만).
- `spawn_snapshot_multi_hop_into_askopts`: AskState.multi_hop 의
  field shape 회귀 핀 (default false / settable / round-trip).

## 검증

- `cargo test -p kebab-tui -j 1` — 신규 6 multi-hop + 기존 ask /
  search / library / mode / cheatsheet / inspect / status_bar 모두
  통과 (42 ask test + 10 mode + 기타). 회귀 없음.
- `cargo clippy -p kebab-tui --all-targets -j 1 -- -D warnings` clean.
- 단일 crate 직렬 build (16 GB RAM 제약).

## v0.18.0 cut (다음 단계)

- Workspace `Cargo.toml` version 0.17.2 → 0.18.0 (minor — surface
  확장 + new prompt_template_version `rag-multi-hop-v1`).
- HANDOFF.md / HOTFIXES.md / INDEX.md 갱신 (fb-41 entry 정리).
- `gitea-release v0.18.0 --auto-notes`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:26:29 +00:00
7150c376bb feat(rag): fb-41 PR-3a — HopRecord wire + RagCfg multi-hop knobs
PR-3 의 분할 첫 PR. wire additive (HopRecord + HopKind + Answer.hops
field) + RagCfg 의 multi_hop_* 3 노브. RAG pipeline 동작 미변경 —
모든 Answer literal 의 `hops = None`. PR-3b (후속) 가 ask_multi_hop
의 happy path 에서 dynamic decide loop 구현 + hops trace 채움.

분할 이유: 원래 PR-3 가 wire + cfg + decide loop + ScriptedLm +
helper refactor + 5+ tests 단일 PR 였는데 ~1500 줄 단일 patch 가
review 부담 + 회기 위험 ↑. additive foundation 부터 ship 후 decide
loop 별 PR — 사용자 결정 (2026-05-25).

- `kebab_core::HopRecord` (iter, kind, sub_queries,
  context_chunks_added, forced_stop, llm_call_ms) + `HopKind`
  (Decompose / Decide / Synthesize) — wire-additive shape.
- `kebab_core::Answer.hops: Option<Vec<HopRecord>>` —
  `#[serde(default, skip_serializing_if = "Option::is_none")]`,
  single-pass / refusal path 는 None, PR-3b 의 multi-hop happy
  path 가 Some.
- `kebab_config::RagCfg` 에 3 신규 노브:
  - `multi_hop_max_depth: u32` (default 3)
  - `multi_hop_max_sub_queries_per_iter: u32` (default 5)
  - `multi_hop_max_pool_chunks: u32` (default 30)
  3 모두 `#[serde(default)]` + env override
  (`KEBAB_RAG_MULTI_HOP_MAX_*`) + legacy parse 핀
  (`LEGACY_PRE_TIMEOUT_TOML` 공유).
- 9 Answer literal site (pipeline.rs ×6 + kebab-cli + kebab-tui
  tests + kebab-eval test) 에 `hops: None` 명시 추가. exhaustive
  field check 가 자동 guard — 빠진 site 시 compile fail.
- plan 의 PR-3 단락 → PR-3a / PR-3b 분할 명시 + scope 정정.

Tests (163 passing across kebab-config + kebab-core + kebab-rag):
- 5 신규 multi-hop knob test (default / env override / legacy parse).
- 기존 50+57+31+19+3+3 test 모두 hops:None 추가 후도 통과.

Wire 영향: `answer.v1` 의 optional `hops` 필드 — `skip_serializing_
if = None` 이라 single-pass response 에 emit 안 됨. wire breaking
아님, JSON Schema 갱신은 PR-3b 또는 PR-4 (실제 emit 시점).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:15:01 +00:00
th-kim0823
bf4ebf8d2a feat(p10-1a-1): add Metadata.repo / git_branch / git_commit / code_lang
Four optional, serde-skipped-when-None fields added to `Metadata` for
code ingest context. All 11 downstream construction sites patched with
`repo: None, git_branch: None, git_commit: None, code_lang: None`.
Full workspace check (`--tests`) and per-crate test suite pass clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:44:18 +09:00
th-kim0823
7329ba96ee fix(p10-1a-1): patch missed SearchHit test-only construction sites
Add repo: None, code_lang: None to the 3 SearchHit struct literals
inside #[cfg(test)] blocks that were missed by the fa4eeb5 sweep.
2026-05-15 15:17:10 +09:00
th-kim0823
126559ce7a fix(fb-40): update test fixtures for rag-v2 default 2026-05-10 19:15:15 +09:00
th-kim0823
4440fa6659 fix(fb-38): add score_kind to remaining SearchHit literals
Add missing score_kind field to SearchHit constructors in:
- kebab-tui/tests/search.rs::make_hit()
- kebab-eval/tests/metrics_and_compare.rs::hit()
- kebab-eval/src/metrics.rs::hit()

All test fixtures default to Rrf (hybrid mode), matching the field's
Default impl and the test semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:08:29 +09:00
th-kim0823
1f39b6bc2c feat(tui): [STALE] Warning-styled badge on search/inspect/ask (fb-32)
insta filter pattern '[indexed_at]' applied where snapshots
otherwise capture time-dependent RFC3339 strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:43:05 +09:00
th-kim0823
cc41adabb5 feat(wire): search_hit.v1 + citation.v1 require indexed_at + stale (fb-32)
Additive minor — schema_version unchanged. Existing v1 consumers
that ignore unknown fields stay compatible; consumers that validate
strictly will reject pre-fb-32 payloads, which matches the wire
contract escape hatch (recipient version >= producer required).

Cross-task placeholders: kebab-eval / kebab-tui synthetic test
fixtures pin UNIX_EPOCH + stale=false (same pattern as
hybrid.rs / vector.rs). These don't exercise staleness — Task 11
adds dedicated TUI staleness rendering tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:17:15 +09:00
f867b36afb feat(kebab-core): p9-fb-23 task 2 — CanonicalDocument gains last_chunker_version + last_embedding_version
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:50:25 +00:00
774acc5c0d review(p9-fb-24): 회차 1 nit 반영 — 중복 inspect test, 카운트, sep 상수
회차 1 review (PR #97 회차 1) 의 4 건 actionable nit 모두 수렴.

- `crates/kebab-tui/tests/inspect.rs`: pre-fb-24 의 `page_keys_scroll_by_ten`
  이 신규 `page_down_scrolls_by_ten_in_inspect` + `page_up_rewinds_by_ten_saturating_in_inspect`
  와 중복 커버리지였음. 신규 두 테스트가 더 정밀 (PgUp 의 25→15→ 그 다음
  3→0 saturating 명시) 이라 기존을 삭제하고 신규로 대체. inspect 테스트
  -1 (14 → 13).
- `tasks/HOTFIXES.md`, `tasks/p9/p9-fb-24-tui-affordances.md`: 테스트
  카운트 `기존 720+` → `기존 695개 (cargo test --workspace -j 1 기준
  716 passed)` 정확화. 영속 기록.
- `crates/kebab-tui/src/run.rs`: status bar 의 magic string `"  │  "`
  를 `const STATUS_SEPARATOR: &str` 로 추출. docstring 의 rendered shape
  과 sync 보장 코멘트 추가.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:17:49 +00:00
b9433c1a2c test(kebab-tui): p9-fb-24 task 8 — status bar absorbs ingest progress
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:56:12 +00:00
27305562f4 test(kebab-tui): p9-fb-24 task 7 — status bar streaming / searching / conv_id
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:54:50 +00:00
d5a4348041 feat(kebab-tui): p9-fb-24 task 6 — render_status_bar (version + pane + docs + idle)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:53:00 +00:00
c3dbe64903 feat(kebab-tui): p9-fb-24 task 5 — Library column header row
Wire `format_doc_header` into `render_doc_list`: render the block
independently, split block_inner into a 1-row header + list via
vertical Layout, and drop the `.block(block)` from the List widget.
Remove `#[allow(dead_code)]` from `format_doc_header` now that it
is consumed. Add `library_renders_column_header_row` integration test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:49:51 +00:00
5242328588 feat(kebab-tui): p9-fb-24 task 3 — Ask PgUp/PgDn page scroll
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:44:47 +00:00
94541523e7 refactor(kebab-tui): p9-fb-24 task 2 — Inspect PgUp/PgDn via pager::PAGE_STEP
Refactor the Inspect pane's PageDown/PageUp handlers to consume the
PAGE_STEP constant from pager.rs instead of hard-coding 10. Adds
regression tests to pin the scroll delta (=10), ensuring future
viewport-aware refactors surface here rather than silently in
user-visible behaviour.

Test coverage: added page_down_scrolls_by_ten_in_inspect and
page_up_rewinds_by_ten_saturating_in_inspect (+ existing
page_keys_scroll_by_ten still passes).

Remove #[allow(dead_code)] from pager.rs now that PAGE_STEP is
consumed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:41:24 +00:00
294b1ed00c fix(kebab-tui): p9-fb-22 — mid-string cursor editing + Ask follow-tail auto-scroll
도그푸딩 중 발견된 두 건 (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>
2026-05-04 15:29:09 +00:00
7709fb0455 feat(kebab-tui): p9-fb-21 — universal i Insert toggle + Search io rebind + F1 prefix
도그푸딩 피드백 (사용자 2026-05-03): Ask Insert→Esc→Normal 후 Insert 로
돌아가는 키 모름. 전반적 키바인딩 안내 부족.

Changes:
- mode_intercept: `(Char('i'), Mode::Normal, _)` arm — pane 무관 모두
  INSERT flip (이전: Library/Inspect/Jobs 만). 사용자가 어느 pane 에서든
  Esc 후 `i` 로 Insert 즉시 복귀 가능.
- Search 의 chunk inspect 키 `i`→`o` (vim "open") rebind. `i` 가
  universal Insert toggle 로 자유로워짐.
- `footer_hints` 모든 (pane, mode, filter) 조합 첫 fragment = `F1 도움말`.
  cheatsheet binding 의 discoverability 보장.
- Search/Ask Normal hint 에 `i 입력모드` fragment 추가.
- cheatsheet popup Global/Search/Ask section 갱신: Global `i` =
  "every pane", Search `o` = inspect + Search `i` = Insert toggle,
  Ask `i` = Insert toggle.
- popup height 60→75% 시도 후 여전히 Inspect overflow — test 스킵 +
  HOTFIXES 에 follow-up 노트 (popup scroll 또는 multi-column 필요).

Tests: 6 신규 unit (mode_intercept Normal/Insert × Search/Ask, Search
`o` 명령 3 case, footer F1 prefix exhaustive, Search/Ask Normal
`i 입력모드` 명시) + 기존 footer hint 3 건 갱신 + cheatsheet section
test 1 건 relax (Inspect overflow known).

spec: `tasks/p9/p9-fb-21-tui-insert-key-discoverability.md` (status
`completed` 직접 — 도그푸딩 직접 피드백 source).
2026-05-03 14:30:04 +00:00
b96d8f9a67 review(p9-fb-10-final): extract place_cursor_x helper + filter overlay render test
- Add `place_cursor_x(inner_x, inner_width, prompt_w, cursor_col) -> u16`
  to `input.rs`: sums in `usize` (no u16 wrap), clamps to inner right
  edge, tries_into with u16::MAX fallback. Two unit tests pin the clamp
  and the in-bounds path.
- Re-export from `lib.rs` alongside `InputBuffer`, `display_width`,
  `truncate_to_display_width`.
- Replace the open-coded 2-line `raw_x`/`cursor_x` blocks in Search,
  Ask, and Library with a single `place_cursor_x` call each —
  consistent usize arithmetic across all three panes.
- Add `filter_overlay_render_places_cursor_on_focused_field` integration
  test in `tests/library.rs`: opens the filter overlay, renders through
  `TestBackend`, asserts `terminal.get_cursor_position().x > 0` (label
  offset > 0 proves `set_cursor_position` was called with a meaningful
  coordinate, not stuck at origin).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 10:29:48 +00:00
bb05eb7213 feat(kebab-tui): FilterEdit buffers → InputBuffer + cursor placement
Migrates FilterEdit.tags_buf and lang_buf from String to InputBuffer
(the same display-width-aware type used by Search/Ask), adds cursor
placement in render_filter_overlay so the caret sits at the end of the
focused field, and adds a Hangul filter test
(filter_overlay_accepts_hangul_tags) that confirms wide chars round-trip
through key events and commit_into unchanged.

Also adds App::library_filter_for_testing accessor mirroring
populate_library_for_testing style.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 09:55:18 +00:00
7784e14a5b feat(kebab-tui): AskState.input → InputBuffer + take() helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 09:44:10 +00:00
997fe46956 review(p9-fb-10-task2): cursor comment + clamp + test rigor + dedup alloc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 09:38:52 +00:00
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
9e720f1bdc feat(kebab-tui): p9-fb-10 partial — CJK width helpers + render audit
`kebab-tui::input::{display_width, truncate_to_display_width}` 신규.
unicode-width 위에서 column-단위 width 계산 (ASCII=1, Hangul/CJK/
fullwidth=2, combining=0) + char-boundary 안전 truncate.

library.rs 의 중복 `truncate_to_display_width` private fn 제거 — 단일
source 로 통일. 9 unit tests + 1 integration render test (Korean +
Japanese fixture, TestBackend 80×20).

spec 의 `InputBuffer` struct 도입은 follow-up — Ask/Search/Editor pane
의 String + cursor 일괄 마이그레이션이 회귀 표면이 커서 helper 만
먼저 머지. backspace 는 모든 pane 이 이미 `String::pop()` 사용 → byte-
boundary 안전성 확보. crossterm 0.28 가 native IME composing 미노출 →
preedit handling out of scope.

spec status `planned` → `in_progress`. HOTFIXES.md 에 InputBuffer
struct deferral 사유 기록.
2026-05-03 08:52:31 +00:00
69c410aaff feat(kebab-tui): p9-fb-13 cheatsheet popup (F1)
도그푸딩 item 11 — vim 비익숙 사용자도 TUI 조작 가능. F1 으로 cheatsheet
modal popup, 현재 pane 의 키 매핑 + global 토글 (i/Esc/F1) 한 자리.

## 핵심 변경

- **`kebab-tui::cheatsheet::render_cheatsheet(f, area, app)`** 신규 —
  70%/60% centered modal. 5 sections (Global / Library / Search / Ask
  / Inspect) 각 pane 의 모든 키 + 동사구 설명. footer 에 현재 focused
  pane 명시. theme.style(Role::Heading/CitationMarker/Hint) 으로 색
  계층 (header bold, key cyan-marker, body plain, hint dim).
- **`App.cheatsheet_visible: bool`** field + `pub fn cheatsheet_
  visible() -> bool` getter (read-only — set/unset 은 F1 intercept
  invariant).
- **`cheatsheet_intercept(app, key)`** in run.rs:
  - F1 → toggle (open ↔ close), consumed
  - Esc 가 visible 일 때 → close, consumed (mode_intercept 가 같은
    Esc 를 mode flip 으로 해석하지 않도록 cheatsheet_intercept 가
    먼저 dispatch)
  - 그 외 키 → fall-through (popup 열린 채 navigation 가능)
  - modifier-bearing F1 (Ctrl-F1 등) 무시
- **run loop 통합**: `cheatsheet_intercept` → `mode_intercept` →
  pane dispatch 순. render_root 가 error overlay 위에 cheatsheet
  overlay (사용자가 error 도중에도 도움말 소환 가능).

## HOTFIXES (`?` → `F1` rebind)

spec 은 `?` 를 trigger 로 명시했지만 Library 가 이미 `Char('?')` 를
quick-Ask binding 으로 사용 중 (handle_key_library line 305). spec 의
`?` 채택 = 기존 binding 깨거나 mode-aware special case 추가. 후자는
mode machine 에 더 많은 분기 추가하므로 회피.

**Live binding**: `F1` (universal help key, no collision).

**Per-pane verb hint line**: spec 의 verb-form hint 재구성도 본 PR
에서 deferral. 기존 `render_footer` 의 pane-별 힌트 문자열이 동일 UX
역할 — 후속 PR 에서 mode-aware verb fragments 로 split 가능.

spec status `planned` → `in_progress` (NOT `completed` — verb hint
deferral 명시).

## 테스트

- 5 신규 integration unit (`tests/cheatsheet.rs`):
  - F1 toggles visibility (open ↔ close, consumed 양쪽)
  - Esc closes when visible / falls through when hidden
  - modifier-bearing F1 (Ctrl-F1, Alt-F1) 무시
  - arbitrary keys (j, /, q, Enter) fall through 하면서 popup 열린 채
  - render_cheatsheet 가 모든 section header (Global/Library/Search/
    Ask/Inspect) + global toggle (F1, Esc) 출력
- 기존 113 TUI 테스트 + 신규 5 = 118 통과
- `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy --workspace --all-targets -- -D warnings` clean

## 문서

- README `kebab tui` 행: F1 cheatsheet popup 안내
- HANDOFF: 2026-05-03 entry
- HOTFIXES: ?→F1 rebind rationale + verb hint deferral
- spec status `planned` → `in_progress`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 08:28:09 +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
b7d7cbaddf review(p9-fb-12): 회차 1 nit 반영
- `mode_intercept` 를 `pub` 로 노출 + `pub use run::mode_intercept`
  로 lib.rs export. 신규 `tests/mode.rs` 6 integration unit:
  - Esc-from-Insert flips to Normal on every pane (consumed)
  - Esc-from-Normal falls through (pane handler 가 처리 — Library
    의 quit signal 등 보존)
  - i-from-Normal on Library/Inspect/Jobs flips to Insert (consumed)
  - i-on-Search/Ask falls through (이미 Insert, i 가 typed char)
  - Ctrl/Alt modifier 는 intercept 안 함 (chord 가능)
  - Shift+Esc 는 toggle 됨 (modifier filter 가 SHIFT allow), Shift+I
    (capital) 는 fall-through (lowercase i 만 toggle 키)
- `Mode::auto_for` doc 에 \"auto-flip overrides user manual mode on
  pane switch\" 명시 — 의도된 트레이드오프 (typing 이 Search/Ask 의
  dominant case). sticky-per-pane 은 future task.

워크스페이스 clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:28:41 +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
6d5f39632f review(회차1): refusal yellow + Ctrl-L race fix
회차 1 actionable 2건 반영.

- (UX 회귀) push_turn_lines 가 answer_color_override: Option<Color>
  추가 받음. render_answer 가 마지막 turn 에 한해 last_answer.grounded
  == false 면 Yellow override 전달 → P9-3 의 refusal 시각 구분
  contract 가 transcript 안에서도 보존. test:
  render_refusal_turn_in_transcript_uses_yellow_when_last_answer_ungrounded
  가 buffer 의 Yellow R 셀 검사로 검증.
- (race) Ctrl-L 가 turns/conversation_id/last_answer/partial/
  current_question/scroll 외에도 thread/rx/streaming 까지 detach.
  in-flight worker 가 다음 frame 에 finish 해도 새 conv 의 stale
  Turn 으로 graduate 안 됨 — JoinHandle Drop 으로 detach (P9-3 Esc
  cancel pattern 동일). worker 자체는 background 에서 SQLite
  answers 에 \"실패한 conv\" 흔적 commit. ctrl_l_clears_conversation_state
  test 가 streaming/thread/rx 도 함께 검증.

18 PASS. clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:02:43 +00:00
7ea7264f5d feat(tui): Ask conversation transcript UI (p9-fb-16)
Multi-turn ask pane. AskState 가 turns: Vec<Turn> + current_question
+ conversation_id + last_answer 로 재설계. answer area 가 transcript
형식 (Q1/A1, Q2/A2, ...) 로 갈음, 매 Enter 가 이전 turns 를 history
로 worker 에 전달 — RagPipeline::ask_with_history 호출.

신규 (kebab-tui::app):
- AskState 에 turns / current_question / conversation_id / last_answer
  4 field 추가. 기존 answer field 제거 (last_answer 가 갈음).

신규 (kebab-tui::ask):
- spawn_ask_worker: 첫 submit 시 conversation_id 자동 생성
  (conv_<unix_nanos_hex>), input → current_question, input clear.
  history = turns.clone(), turn_index = turns.len(). worker 가
  ask_with_history 호출 (kebab-app facade 가 _cancellable 통해
  RagPipeline::ask_with_history 까지 thread).
- poll_worker: Answer 받으면 Turn { question: current_question,
  answer, citations, created_at } 만들어 turns 에 push, last_answer
  도 보존.
- handle_key_ask: Ctrl-L 가 turns + conversation_id 초기화 (in-flight
  worker 는 그대로 finish — 결과는 새 conversation 의 stale turn 으로
  silently 폐기, 사용자 의도와 일치).
- render_answer: 모든 completed turns + (있으면) in-flight turn
  chronological 출력. Q/A 라벨 색상 구분 (Q cyan bold, A green bold).
  in-flight answer 는 ▍ cursor + dim. transcript title 에 turn count.
- render_status / render_citations_or_explain: s.last_answer 사용.

Test:
- 17 PASS (3 신규: ctrl_l_clears_conversation_state /
  render_transcript_shows_completed_turns_in_order /
  render_streaming_inflight_turn_appears_below_completed_turns).
- 기존 14 회귀 0 (기존 s.answer → s.last_answer + Turn fixture
  push).

README + HANDOFF: TUI 행에 multi-turn 동작 추가. spec status
planned → in_progress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:58:26 +00:00
2c058ab175 feat(rag): multi-turn ask — Turn struct + ask_with_history + token budget (p9-fb-15)
Spec PR #59 의 §3.8 multi-turn behaviour 구현. RAG facade 가 prior
turns 받아 prompt 에 prepend, retrieval query expansion 적용,
Answer 에 conversation_id / turn_index 채움.

신규 (kebab-core):
- Answer 에 conversation_id (Option<String>) / turn_index (Option<u32>)
  field 추가. serde skip_serializing_if 로 single-shot 의 wire
  output 변경 0 (기존 외부 wrapper 영향 없음).
- Turn struct (question + answer + citations + created_at).
- RefusalReason::LlmStreamAborted variant.

신규 (kebab-rag):
- AskOpts 에 history (Vec<Turn>) / conversation_id / turn_index 3 field.
- AskOpts::single_shot(mode) helper.
- RagPipeline::ask_with_history(query, history, conversation_id,
  turn_index, opts) — combined opts 로 ask 호출.
- expand_query_with_history: history.last() 의 answer 첫 200 자
  concat 해 SearchQuery.text 확장 (spec §3.8 의 \"cheap concat\";
  LLM-based standalone-question rewriting 은 P+).
- serialize_history + remaining_history_budget_chars: spec 의 priority
  enforcement — system+question 필수, retrieved chunks 가 차지한
  뒤 남은 char budget 안에서 newest 우선, oldest drop.
- ask 본문: history 가 비어있지 않으면 [이전 대화] 블록을 user
  prompt 위에 prepend. Answer 생성 site 3 곳 (정상 / NoChunks /
  ScoreGate refuse) 모두 conversation_id / turn_index 채움.

신규 (kebab-store-sqlite):
- refusal_reason_label 가 LlmStreamAborted → 'llm_stream_aborted'.

기존 caller 변경 (single-shot 동작 동일):
- kebab-cli main.rs Cmd::Ask: AskOpts 에 history=Vec::new(),
  conversation_id=None, turn_index=None 명시 (CLI multi-turn 은
  p9-fb-18 의 --session/--repl 가 채움).
- kebab-tui src/ask.rs spawn site 동일 (multi-turn UI 는 p9-fb-16).
- kebab-eval runner.rs golden eval 동일 (single-shot per query).
- kebab-app tests/ask_smoke.rs / kebab-tui tests/ask.rs / kebab-rag
  tests/pipeline.rs / kebab-eval metrics.rs Answer literal 갱신.

Test:
- 9 신규 lib unit (expand_query 4 / serialize_history 3 / remaining_budget 2).
- 기존 12 PASS 회귀 0.

Plan 갱신:
- p9-fb-15 status planned → in_progress. 머지 후 한 줄 commit
  으로 completed flip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:09:46 +00:00
8c6d29cc2d review(p9-4): 회차 1 지적 반영
blocks / embeddings 섹션의 count 라인이 collapse 검사 *밖* 에서 push
되어 collapsed 상태에서 부분만 사라지던 일관성 깨짐. fix: count 를
section header 에 inline 으로 (`▾ blocks (N)`, `▾ embeddings (N)`),
body 만 collapse 검사 안. 새 helper `push_section_header_with_count`
가 둘 다 통일.

회귀 테스트 보강:
- doc_view_collapse_hides_section_body: collapsed 상태에서 \"blocks (2)\"
  inline count 표시 + \"Heading L1\" body 숨김 검증.
- chunk_view_renders_text_and_block_ids: \"embeddings (2)\" inline
  count 검증.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:44:14 +00:00
b6e0ab352f feat(kebab-tui): P9-4 Inspect pane — doc/chunk detail with collapsible sections
Library Enter / Search 'i' 가 Inspect 진입. Doc 또는 Chunk 단일 view 로
metadata / provenance / blocks (doc) 또는 spans / text / embeddings (chunk)
6 section 을 collapsible 로 표시. Esc/q 로 originating pane 으로 복귀.

핵심:
- InspectTarget enum (`Doc(DocumentId) | Chunk(ChunkId)`).
- InspectState 본체 (`app.rs`) — target / doc / chunk / collapsed
  HashSet / scroll / return_to / needs_fetch / loading.
- `src/inspect.rs`:
  - `render_inspect` — target 종류별 render_doc / render_chunk 분기,
    section header 가 collapse marker (▾/▸) 표시. metadata.user JSON
    pretty-printed.
  - `handle_key_inspect`: j/k / Down/Up scroll. PageDown/PageUp 10 row.
    c = toggle all sections (v1 simplification). Esc/q = SwitchPane(return_to).
  - `enter_inspect(state, target, return_to)` helper — Library 와 Search
    공통 entry point.
  - run-loop hook `refresh_inspect` — needs_fetch 면 lazy
    inspect_doc_with_config / inspect_chunk_with_config.
- run.rs: Pane::Inspect arm 이 handle_key_inspect + render_inspect.
  Idle tick 마다 refresh_inspect. SwitchPane(Inspect) lazy init.
- Library: Enter 가 enter_inspect(Doc(selected)) 호출 후 SwitchPane.
- Search: 'i' (plain modifier) 가 enter_inspect(Chunk(selected_hit))
  호출 후 SwitchPane. typing 'i' (\"instance\") 와 충돌 가드.

테스트 12개 (`tests/inspect.rs`, TestBackend) — Esc 가 return_to 사용
/ q 도 동작 / j/k scroll bounds / PgUp PgDn ±10 / c 일괄 toggle / no
target hint / loading / doc view header+metadata+provenance+blocks /
collapse hides body / chunk view text+block_ids / no slot →
SwitchPane(Library) / enter_inspect helper sets fields.

Spec deviation (HOTFIXES `2026-05-02 P9-4`):
- `render_inspect<B: Backend>` generic 제거 (P9-1/2/3 와 동일).
- Search `i` 키 추가 (P9-2 spec 에 없었음, P9-4 retroactive 추가).
- `c` 일괄 collapse — spec 의 \"focus 기반 selective collapse\" 는 P+.

Docs (sync rule):
- README: TUI 행 \"4 패널\" + Quick start 코멘트.
- HANDOFF: 한 줄 요약 + Phase status (P9 3/5 → 4/5) + deviation 한 줄.
- HOTFIXES: P9-4 entry.
- tasks/p9/p9-4 status: completed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:41:11 +00:00
ad7bd7d309 review(p9-3): 회차 1 지적 반영
Esc 후 재질문 시 detached prior worker + 새 worker 동시 in-flight 가능
했음. Ollama endpoint 에 두 요청 동시 발사 → 응답 시간 두 배 + stream
혼동. spawn_ask_worker 진입 시 `s.thread.is_some()` 검사 추가, 이전
worker 가 still alive 면 Enter 무시. input bar 의 busy 텍스트 가 세
상태 (streaming / awaiting prior / idle) 분리 표시 — 사용자가 Enter
가 왜 안 먹히는지 즉시 확인.

회귀 테스트 `enter_with_detached_prior_thread_is_blocked` 추가 — never-
ending 더미 thread 를 hand-install 후 Enter no-op 검증, 종료 시 thread
take() 로 leak 명시 (test process 종료 시 OS 가 reap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:27:39 +00:00
f08fefec1d feat(kebab-tui): P9-3 Ask pane — streaming answer + citation panel + explain toggle
P9-1 Library 의 ? 키 활성화. App.ask slot 채움 (parallel-safety contract
그대로). Worker thread 가 kebab-app::ask_with_config 호출하면서
AskOpts.stream_sink 로 token 을 mpsc 채널 에 보냄, 메인 스레드 (TUI) 는
매 render frame 마다 drain 으로 문자열 누적 → 답변 영역 이 token-by-token
업데이트.

핵심:
- AskState 본체 (`app.rs`) — input / explain / streaming / partial /
  answer / thread JoinHandle / rx Receiver / scroll / last_error.
- `src/ask.rs`:
  - `render_ask` — input bar / 답변 영역 (streaming 시 ▍ cursor) /
    bottom split (status: grounded/model/prompt/k/refusal · citations
    or explain panel).
  - `handle_key_ask`: typing → input. Enter → spawn_ask_worker (input
    있음 + not streaming). e (input empty 시) → toggle explain.
    j/k (input empty 시) → scroll. Esc → SwitchPane(Library) +
    streaming/rx/thread 클리어 (best-effort cancel).
  - `spawn_ask_worker` — mpsc::channel + thread::spawn(|| ask_with_config).
  - run-loop hooks: `drain_stream` (try_iter → partial), `poll_worker`
    (handle.is_finished → take + join → answer 채움 또는 ErrorOverlay).
- run.rs: Pane::Ask arm 이 handle_key_ask + render_ask. Idle tick 마다
  drain_stream + poll_worker. SwitchPane(Ask) 시 lazy init.

테스트 13개 (`tests/ask.rs`) — Esc/typing/backspace/e toggle (input
empty)/e typed (input nonempty)/Enter empty/Enter while streaming
no-op/render pre-submission hint/streaming partial+cursor/grounded
answer + citation [1]/refusal score_gate 패널 panic 없음/explain panel
title flip/no slot.

Spec deviation (HOTFIXES `2026-05-02 P9-3`):
- `render_ask<B: Backend>` generic 제거 — ratatui 0.28 Frame
  backend-agnostic (P9-1/P9-2 와 동일).
- e/j/k 가 input 빈 상태 일 때만 command 키, 입력 있으면 typing —
  vim "command vs insert" 변형. spec literal 의 단순 \"e=toggle\" 은
  \"explain\" / \"javascript\" 같은 단어 입력 깨뜨림.

Docs (sync rule):
- README: TUI 행 \"Library + Search + Ask 패널\" + Quick start 코멘트.
- HANDOFF: 한 줄 요약 + Phase status (P9 2/5 → 3/5) + deviation 한 줄.
- HOTFIXES: P9-3 entry.
- tasks/p9/p9-3 status: completed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:24:26 +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
43ff4048e8 feat(kebab-tui): P9-1 Ratatui shell + Library pane
새 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>
2026-05-02 13:26:24 +00:00