Files
kebab/tasks/p9/p9-3-tui-ask.md
kb bc1b3147cd refactor(spec): cleanup pass over component specs
Address 8 issues found in spec audit (post PR #2):

1. §refs label: distinguish design vs report sections in p3-1 / p3-2 / p4-2 /
   p9-1 / p9-5 contract_sections (e.g., "report §11.2 Ollama" not "§11.2").
2. mock feature gate: gate MockEmbedder (p3-1) and MockLanguageModel (p4-1)
   behind `mock` cargo feature, default OFF; add CI symbol-scan as DoD item.
3. Warning type unification: p1-2 frontmatter now emits
   `kb_parse_types::Warning` (matches p1-3 / p1-4); drops crate-internal type.
4. p4-3 streaming thread: explicitly single-threaded inside RagPipeline::ask;
   collection + sink.send share the calling thread, no race. UI concurrency
   is callers responsibility (TUI worker thread pattern in p9-3).
5. p6-2 tesseract version: noted that `tesseract` 0.13 has no stable Rust
   `version()` accessor; use TessVersion FFI or shell-out + cache approach.
6. p9-* App struct extensions: introduce `kb_tui::{Library,Search,Ask,Inspect}State`
   slots in p9-1 forward-decl form; p9-2/3/4 fill bodies in their own crate
   without editing `App`. Parallel-safety contract added.
7. p3-3 cosine score: shift `(sim+1)/2` instead of clamp; preserve ranking
   signal between unrelated and opposite vectors. Clamp reserved for NaN.
8. fixtures/ root: p0-1 DoD now creates all fixture subdirs with .gitkeep so
   downstream tasks have a stable target path.
2026-04-27 23:38:13 +00:00

129 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: P9
component: kb-tui (ask pane)
task_id: p9-3
title: "TUI Ask pane: streaming answer + citation links + --explain toggle"
status: planned
depends_on: [p4-3, p9-1]
unblocks: []
contract_source: ../../docs/superpowers/specs/2026-04-27-kb-final-form-design.md
contract_sections: [§1.11.4 ask scenes, §2.3 Answer wire, §3.8 Answer]
---
# p9-3 — TUI Ask pane
## Goal
Add an Ask pane that calls `kb-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
- `kb-core`
- `kb-config`
- `kb-app`
- `kb-tui` (extends p9-1)
- `ratatui`, `crossterm`
- `tracing`
- `thiserror`
## Forbidden dependencies
- `kb-source-fs`, `kb-parse-*`, `kb-normalize`, `kb-chunk`, `kb-store-*`, `kb-embed*`, `kb-search`, `kb-llm*`, `kb-rag` (only via `kb-app`), `kb-desktop`
## Inputs
| input | type | source |
|-------|------|--------|
| `kb-app::ask(query, AskOpts)` | facade | runtime |
| keyboard events | `crossterm` | terminal |
## Outputs
| output | type | downstream |
|--------|------|------------|
| Ratatui Ask pane render | terminal | user |
| `kb-app::ask` invocation with streaming closure | facade | RAG pipeline |
## Public surface (signatures only — no new types)
```rust
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 `kb_tui::AskState` (forward-declared in p9-1). `App` is NOT edited — only `AskState` gets fields:
```rust
pub struct AskState {
pub input: String,
pub explain: bool,
pub streaming: bool,
pub partial: String,
pub answer: Option<kb_core::Answer>,
pub thread: Option<std::thread::JoinHandle<anyhow::Result<kb_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 with `path#fragment` and section label), bottom-left status (`grounded ✓/✗ model prompt_v k chunks`).
- Submission: `Enter` triggers a worker thread that calls `kb-app::ask` with `AskOpts.stream_sink: Some(tx)` (`tx: mpsc::Sender<String>`). The thread holds the `tx`, the TUI holds the matching `rx` (set on `AskState.rx`). On each render frame the TUI drains `rx.try_iter()` into `state.partial`, no blocking.
- Streaming: while `ask_streaming = true`, the Answer area shows `ask_partial` and a small "▍" cursor. When the worker finishes, `ask_answer` is populated and the citations panel switches to the final list.
- Refusal rendering:
- `grounded = false` and `refusal_reason = ScoreGate` → render the answer (which is the human-friendly "근거 부족…" message), citations show "가까운 후보".
- `grounded = false` and `refusal_reason = LlmSelfJudge` → same layout but status shows `grounded ✗ … 3 chunks searched, 0 grounded`.
- Key bindings (Ask pane):
- typing → updates `ask_input`
- `Enter` → submit (only when not currently streaming)
- `e` → toggle `ask_explain`; resubmit on next `Enter`. 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.
- All facade calls stay within `kb-app::ask` — never reach into `kb-rag` directly.
- Errors render as a popup overlay; do not crash the pane.
## Storage / wire effects
- Reads/writes via `kb-app::ask` which itself writes the `answers` row in `kb.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 `kb-app::ask` returning a canned `Answer` populates final state correctly | inline |
All tests under `cargo test -p kb-tui ask`.
## Definition of Done
- [ ] `cargo check -p kb-tui` passes
- [ ] `cargo test -p kb-tui ask` passes
- [ ] No imports outside Allowed dependencies
- [ ] Manual smoke: stream tokens visible character-by-character against a real Ollama (or `MockLanguageModel`)
- [ ] PR links design §1.11.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_recv` polled 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_timeout` or 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`.