마지막 commit. 모든 .md 안의 `kb` 단어 일괄 갱신. - 19 개 crate 이름 (`kb-core`, `kb-app`, …) → `kebab-*` (Rust 모듈 path 표기 `kb_*` → `kebab_*` 포함). - 미래 component (`kb-tui`, `kb-desktop`, `kb-asr-whisper`, `kb-ocr`, `kb-mcp`, `kb-vlm`, `kb-rerank`, `kb-vision-ocr`, `kb-index`, `kb-smoke`, `kb-architecture`) → `kebab-*` (P6+ 가 시작될 때 같은 prefix 사용). - CLI 명령 예제: `kb ingest` / `kb search` / `kb ask` / `kb init` / `kb doctor` / `kb inspect` / `kb list` / `kb eval` → `kebab <verb>`. fenced code block + 인라인 backtick 모두. - XDG paths + env vars + binary 경로 (`target/release/kb` → `target/release/kebab`) 동기화. - design doc / 최초 보고서 / SMOKE / HOTFIXES / phase epic / task spec 모든 reference 통일. - task-decomposition.md 의 `git -c user.name=kb` 는 과거 git history 기록용 author 정보라 그대로 유지 (실제 git history 의 author 는 변경 불가). - `tasks/phase-5-evaluation.md` 의 `status: planned` → `completed` 도 같이 (P5-1 + P5-2 PR 머지 후 미반영분). ## 검증 - `grep -rEn "\bkb-[a-z]|\bkb_[a-z]|\.config/kb\b|kb\.sqlite|\bKB_[A-Z]" --include="*.md"` 0 hits (task-decomposition.md 의 git author 제외). - 모든 file path reference 살아있음 (renamed file 들 모두 새 path 로 update). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.9 KiB
5.9 KiB
phase, component, task_id, title, status, depends_on, unblocks, contract_source, contract_sections
| phase | component | task_id | title | status | depends_on | unblocks | contract_source | contract_sections | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| P9 | kebab-tui (ask pane) | p9-3 | TUI Ask pane: streaming answer + citation links + --explain toggle | planned |
|
../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md |
|
p9-3 — TUI Ask pane
Goal
Add an Ask pane that calls kebab-app::ask, streams tokens into the answer area in real time, renders citation footnotes (default mode A), and toggles to --explain (mode B + retrieval trace) with a key.
Why now / why this size
Streaming UI is the only TUI piece that meaningfully differs from search/inspect. Confining it here keeps the change set focused.
Allowed dependencies
kebab-corekebab-configkebab-appkebab-tui(extends p9-1)ratatui,crosstermtracingthiserror
Forbidden dependencies
kebab-source-fs,kebab-parse-*,kebab-normalize,kebab-chunk,kebab-store-*,kebab-embed*,kebab-search,kebab-llm*,kebab-rag(only viakebab-app),kebab-desktop
Inputs
| input | type | source |
|---|---|---|
kebab-app::ask(query, AskOpts) |
facade | runtime |
| keyboard events | crossterm |
terminal |
Outputs
| output | type | downstream |
|---|---|---|
| Ratatui Ask pane render | terminal | user |
kebab-app::ask invocation with streaming closure |
facade | RAG pipeline |
Public surface (signatures only — no new types)
pub fn render_ask<B: ratatui::backend::Backend>(f: &mut ratatui::Frame, area: ratatui::layout::Rect, state: &App);
pub fn handle_key_ask(state: &mut App, key: crossterm::event::KeyEvent) -> KeyOutcome;
This task fills the body of kebab_tui::AskState (forward-declared in p9-1). App is NOT edited — only AskState gets fields:
pub struct AskState {
pub input: String,
pub explain: bool,
pub streaming: bool,
pub partial: String,
pub answer: Option<kebab_core::Answer>,
pub thread: Option<std::thread::JoinHandle<anyhow::Result<kebab_core::Answer>>>,
pub rx: Option<std::sync::mpsc::Receiver<String>>,
}
render_ask/handle_key_ask read app.ask.as_mut() exclusively. Parallel-safety contract from p9-1 holds.
Behavior contract
- Layout: top input bar (
?prompt, query text), middle answer area (rendered Markdown-light: paragraphs + inline[N]markers), bottom-right citations panel (numbered list of citations withpath#fragmentand section label), bottom-left status (grounded ✓/✗ model prompt_v k chunks). - Submission:
Entertriggers a worker thread that callskebab-app::askwithAskOpts.stream_sink: Some(tx)(tx: mpsc::Sender<String>). The thread holds thetx, the TUI holds the matchingrx(set onAskState.rx). On each render frame the TUI drainsrx.try_iter()intostate.partial, no blocking. - Streaming: while
ask_streaming = true, the Answer area showsask_partialand a small "▍" cursor. When the worker finishes,ask_answeris populated and the citations panel switches to the final list. - Refusal rendering:
grounded = falseandrefusal_reason = ScoreGate→ render the answer (which is the human-friendly "근거 부족…" message), citations show "가까운 후보".grounded = falseandrefusal_reason = LlmSelfJudge→ same layout but status showsgrounded ✗ … 3 chunks searched, 0 grounded.
- Key bindings (Ask pane):
- typing → updates
ask_input Enter→ submit (only when not currently streaming)e→ toggleask_explain; resubmit on nextEnter. While explain ON, citations panel is replaced by the per-claim breakdown (mode B in design §1.2) and a footer shows the retrieval trace summary.Esc→ switch back to Library pane (cancellation of an in-flight ask is best-effort: the worker thread continues but its final answer is dropped).j/k→ scroll the answer area when oversized.
- typing → updates
- All facade calls stay within
kebab-app::ask— never reach intokebab-ragdirectly. - Errors render as a popup overlay; do not crash the pane.
Storage / wire effects
- Reads/writes via
kebab-app::askwhich itself writes theanswersrow inkebab.sqlite. The pane has no direct DB access.
Test plan
| kind | description | fixture / data |
|---|---|---|
| unit | submission spawns worker exactly once per Enter |
inline mock |
| unit | streaming receiver accumulates tokens into ask_partial |
inline mock with 5 tokens |
| unit | toggle e flips ask_explain and re-submits on Enter |
inline |
| unit | refusal answer renders without citations panel index errors | inline |
| snapshot | rendered Ask pane mid-stream is stable | TestBackend |
| snapshot | rendered Ask pane after finished grounded answer is stable | TestBackend |
| integration | mocked kebab-app::ask returning a canned Answer populates final state correctly |
inline |
All tests under cargo test -p kebab-tui ask.
Definition of Done
cargo check -p kebab-tuipassescargo test -p kebab-tui askpasses- No imports outside Allowed dependencies
- Manual smoke: stream tokens visible character-by-character against a real Ollama (or
MockLanguageModel) - PR links design §1.1–1.4, §2.3
Out of scope
- Persistent multi-turn chat memory.
- Conversational follow-ups.
- Voice input.
- Token-by-token highlighting per claim (the per-claim mode renders after completion).
Risks / notes
mpsc::Receiver::try_recvpolled in the render loop; missing polls = stuttery streaming. Throttle the render at 30 fps and drain the channel each frame.- Worker thread join on quit must not block forever; use
join_timeoutor detach if quit signaled. - Cancellation: real cancellation of the LLM stream is provider-specific and out of scope. We accept "fire and forget" with discarded result on
Esc.