tasks: add P9 component specs (tui x4, desktop)
This commit is contained in:
124
tasks/p9/p9-1-tui-library.md
Normal file
124
tasks/p9/p9-1-tui-library.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
phase: P9
|
||||
component: kb-tui (library view)
|
||||
task_id: p9-1
|
||||
title: "Ratatui library list view + tag filter"
|
||||
status: planned
|
||||
depends_on: [p1-6]
|
||||
unblocks: [p9-2, p9-3, p9-4]
|
||||
contract_source: ../../docs/superpowers/specs/2026-04-27-kb-final-form-design.md
|
||||
contract_sections: [§16.2 TUI epic (tasks/phase-9-ui.md), §3.7 SearchHit, §1 UX scenes for shared key bindings]
|
||||
---
|
||||
|
||||
# p9-1 — TUI library view
|
||||
|
||||
## Goal
|
||||
|
||||
Stand up a Ratatui app skeleton with a "Library" pane: list documents, filter by tag/lang, navigate. Establishes the global app loop, key dispatch, and `kb-app` integration point that the search/ask/inspect panes (p9-2..p9-4) extend.
|
||||
|
||||
## Why now / why this size
|
||||
|
||||
Library is the cheapest screen and the natural anchor for the TUI shell. Subsequent panes plug into the same dispatch / shared state.
|
||||
|
||||
## Allowed dependencies
|
||||
|
||||
- `kb-core`
|
||||
- `kb-config`
|
||||
- `kb-app` (facade — the only crate this binary touches besides `kb-core`/`kb-config`)
|
||||
- `ratatui = "0.28"`
|
||||
- `crossterm`
|
||||
- `tracing`
|
||||
- `thiserror`
|
||||
|
||||
## Forbidden dependencies
|
||||
|
||||
- `kb-source-fs`, `kb-parse-*`, `kb-normalize`, `kb-chunk`, `kb-store-*`, `kb-embed*`, `kb-search`, `kb-llm*`, `kb-rag` (UI must go through `kb-app` only — this is the design §8 boundary)
|
||||
|
||||
## Inputs
|
||||
|
||||
| input | type | source |
|
||||
|-------|------|--------|
|
||||
| `kb-app::list_docs(filter)` | facade call | runtime |
|
||||
| keyboard events | `crossterm` | terminal |
|
||||
| `kb-config::Config` | runtime | env / file |
|
||||
|
||||
## Outputs
|
||||
|
||||
| output | type | downstream |
|
||||
|--------|------|------------|
|
||||
| Ratatui frame | terminal render | user |
|
||||
| App state (selected doc, filter, focus) | in-memory | next-pane handoff |
|
||||
|
||||
## Public surface (signatures only — no new types)
|
||||
|
||||
```rust
|
||||
pub struct App { /* state: docs, filter, selection, focus pane */ }
|
||||
|
||||
impl App {
|
||||
pub fn new(config: kb_config::Config) -> anyhow::Result<Self>;
|
||||
pub fn run(&mut self) -> anyhow::Result<()>; // blocking loop until quit
|
||||
}
|
||||
|
||||
pub enum Pane { Library, Search, Ask, Inspect, Jobs }
|
||||
|
||||
pub fn render_library<B: ratatui::backend::Backend>(f: &mut ratatui::Frame, area: ratatui::layout::Rect, state: &App);
|
||||
|
||||
pub fn handle_key_library(state: &mut App, key: crossterm::event::KeyEvent) -> KeyOutcome;
|
||||
|
||||
pub enum KeyOutcome { Continue, Quit, SwitchPane(Pane), Refresh }
|
||||
```
|
||||
|
||||
## Behavior contract
|
||||
|
||||
- Layout: header (1 line, breadcrumb / pane label) + body (full) + footer (key hints).
|
||||
- Library body: scrollable list of `DocSummary` with columns `[title] [tag list] [updated_at] [chunk_count]`.
|
||||
- Filter bar (toggled by `f`): edit `tags_any` and `lang` fields; pressing `Enter` re-runs `list_docs`.
|
||||
- Key bindings (Library pane only):
|
||||
- `j` / `k` or arrow keys → move selection down/up
|
||||
- `g g` → top, `G` → bottom
|
||||
- `f` → toggle filter
|
||||
- `/` → switch to Search pane (p9-2)
|
||||
- `?` → switch to Ask pane (p9-3)
|
||||
- `Enter` → switch to Inspect pane (p9-4) on selected doc
|
||||
- `q` or `Esc` → quit
|
||||
- All facade calls run on the main thread (no async). For long calls, render a "loading…" state and call from a worker thread; bridge via `mpsc::channel` (this task may keep things synchronous and accept brief UI hangs for v1).
|
||||
- Logging: `tracing` initialized to a file under `~/.local/state/kb/logs/`; never to stdout/stderr (so the TUI is not corrupted).
|
||||
- Error rendering: a popup overlay shows `error: {msg}\nhint: {hint}` from `anyhow::Error` chain; press any key to dismiss.
|
||||
|
||||
## Storage / wire effects
|
||||
|
||||
- Reads: `kb-app::list_docs` only.
|
||||
- Writes: none.
|
||||
|
||||
## Test plan
|
||||
|
||||
| kind | description | fixture / data |
|
||||
|------|-------------|----------------|
|
||||
| unit | `handle_key_library` arrow-down increments selection within bounds | inline state |
|
||||
| unit | filter `f` opens edit overlay; `Enter` triggers refresh | inline |
|
||||
| snapshot | rendered library with 3 docs + filter open produces stable frame buffer (use `ratatui::backend::TestBackend`) | inline |
|
||||
| unit | error popup renders without panic on injected `anyhow::Error` | inline |
|
||||
| integration | mocked `kb-app::list_docs` returning N docs renders all rows | inline |
|
||||
|
||||
All tests under `cargo test -p kb-tui library`.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] `cargo check -p kb-tui` passes
|
||||
- [ ] `cargo test -p kb-tui library` passes
|
||||
- [ ] No imports outside `kb-core`, `kb-config`, `kb-app`
|
||||
- [ ] `kb tui` (or `kb` if TUI is the default) launches and shows Library on a real terminal (manual smoke)
|
||||
- [ ] PR links design §8 module boundary, §16.2 epic
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Search pane (p9-2), Ask pane (p9-3), Inspect pane (p9-4), Jobs pane.
|
||||
- Mouse support (P+).
|
||||
- Theme / color customization (P+).
|
||||
- Cross-platform installation packaging (separate concern).
|
||||
|
||||
## Risks / notes
|
||||
|
||||
- Ratatui re-renders on every event; large doc lists can be slow. Use `ListState` and only render visible rows.
|
||||
- crossterm raw-mode cleanup must run on panic (`color_eyre` or manual `disable_raw_mode` in `Drop`); a corrupted terminal after a crash is a UX disaster.
|
||||
- Korean text rendering width: use `unicode-width` and account for wide characters when computing column widths.
|
||||
117
tasks/p9/p9-2-tui-search.md
Normal file
117
tasks/p9/p9-2-tui-search.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
phase: P9
|
||||
component: kb-tui (search pane)
|
||||
task_id: p9-2
|
||||
title: "TUI Search pane: input + result list + preview + editor jump"
|
||||
status: planned
|
||||
depends_on: [p2-2, p3-4, p9-1]
|
||||
unblocks: []
|
||||
contract_source: ../../docs/superpowers/specs/2026-04-27-kb-final-form-design.md
|
||||
contract_sections: [§1.5/1.6 search output, §3.7 SearchHit, §0 Q3 citation]
|
||||
---
|
||||
|
||||
# p9-2 — TUI Search pane
|
||||
|
||||
## Goal
|
||||
|
||||
Add a Search pane to the TUI that drives `kb-app::search`, renders dense results (rank+score / path#frag / heading / snippet), and supports `g` (editor jump to citation) for the selected hit.
|
||||
|
||||
## Why now / why this size
|
||||
|
||||
Search is the most-used surface. Confining it to one pane leverages the App skeleton from p9-1 without rebuilding key dispatch.
|
||||
|
||||
## 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`, `kb-desktop`
|
||||
|
||||
## Inputs
|
||||
|
||||
| input | type | source |
|
||||
|-------|------|--------|
|
||||
| `kb-app::search(query)` | facade | runtime |
|
||||
| keyboard events | `crossterm` | terminal |
|
||||
| selected hit's citation | `kb_core::Citation` | App state |
|
||||
|
||||
## Outputs
|
||||
|
||||
| output | type | downstream |
|
||||
|--------|------|------------|
|
||||
| Ratatui frame for Search pane | render | user |
|
||||
| External editor process spawn | `std::process::Command` | OS |
|
||||
|
||||
## Public surface (signatures only — no new types)
|
||||
|
||||
```rust
|
||||
pub fn render_search<B: ratatui::backend::Backend>(f: &mut ratatui::Frame, area: ratatui::layout::Rect, state: &App);
|
||||
pub fn handle_key_search(state: &mut App, key: crossterm::event::KeyEvent) -> KeyOutcome;
|
||||
pub fn jump_to_citation(citation: &kb_core::Citation, editor_env: &str /* $EDITOR */) -> anyhow::Result<()>;
|
||||
```
|
||||
|
||||
`App` (from p9-1) is extended with: `search_input: String`, `search_mode: SearchMode`, `hits: Vec<SearchHit>`, `selected_hit: usize`.
|
||||
|
||||
## Behavior contract
|
||||
|
||||
- Layout: top input bar (search query + mode badge `[hybrid|lexical|vector]`), middle result list (one hit per 4 lines per design §1.5 dense format), bottom preview pane (full chunk text fetched lazily via `kb-app::inspect_chunk`).
|
||||
- Key bindings (Search pane):
|
||||
- typing → updates `search_input`; debounced (200 ms) re-search
|
||||
- `Tab` → cycles `search_mode` Lexical → Vector → Hybrid → Lexical
|
||||
- `Enter` → forces re-search immediately
|
||||
- `j` / `k` or arrow keys → move selected hit
|
||||
- `g` → call `jump_to_citation(&hits[selected].citation, &env::var("EDITOR").unwrap_or_else(|_| "vi".into()))`
|
||||
- `Esc` → switch back to Library pane
|
||||
- `jump_to_citation`:
|
||||
- For `Citation::Line { path, start, .. }`: spawn `editor +<start> <workspace_root>/<path>`. Common editors `vim`/`nvim`/`vi`/`emacs`/`hx` accept `+N`. Fallback: `code -g <path>:<start>` if `$EDITOR` contains "code".
|
||||
- For other citation kinds: open the file in `$EDITOR` without line jump (best effort).
|
||||
- Use `std::process::Command::status()` blocking; suspend the TUI (`disable_raw_mode`) before launch and restore on return.
|
||||
- The search call runs synchronously; for hybrid mode that may take seconds, render a centered "searching…" overlay until complete.
|
||||
- All search results rendered must conform to design §1.5 dense format (4 lines: `<rank>. <score> <path#frag>` / `<section_label>` / `<snippet line 1>` / `<snippet line 2>`).
|
||||
- Errors → popup overlay (consistent with p9-1).
|
||||
- Stable terminal restoration on panic and process exit.
|
||||
|
||||
## Storage / wire effects
|
||||
|
||||
- Reads only. No DB writes.
|
||||
- Spawns external editor process; that process can mutate user files. The TUI does not interfere.
|
||||
|
||||
## Test plan
|
||||
|
||||
| kind | description | fixture / data |
|
||||
|------|-------------|----------------|
|
||||
| unit | typing into search_input triggers re-search after debounce | inline timer mock |
|
||||
| unit | `Tab` cycles mode through 3 values back to Lexical | inline |
|
||||
| unit | `j` / `k` move selection within bounds | inline |
|
||||
| unit | `jump_to_citation` for `Line` builds `+<line> <path>` command (assert via mocked Command runner) | inline |
|
||||
| snapshot | rendered Search pane with 3 hits + preview stable | TestBackend |
|
||||
| integration | mocked `kb-app::search` returning fixture hits drives render | inline |
|
||||
|
||||
All tests under `cargo test -p kb-tui search`.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] `cargo check -p kb-tui` passes
|
||||
- [ ] `cargo test -p kb-tui search` passes
|
||||
- [ ] `g` keybinding launches `$EDITOR` with correct `+<line>` argument (manual smoke against vim)
|
||||
- [ ] No imports outside Allowed dependencies
|
||||
- [ ] PR links design §1.5/1.6, §3.7
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Inline citation render of LLM answers (Ask pane = p9-3).
|
||||
- Full `--explain` retrieval trace (mention but defer to a future toggle).
|
||||
- Mouse selection.
|
||||
|
||||
## Risks / notes
|
||||
|
||||
- Suspending and restoring crossterm raw mode around the editor spawn is finicky; code defensively (RAII guard).
|
||||
- Different editors take different jump syntaxes. Provide an env override `KB_EDITOR_JUMP_FORMAT="vim"` for users on exotic editors.
|
||||
- Long snippet text wrap: clamp to viewport width and ellipsize per design §1.5 (`…` already in dense template).
|
||||
114
tasks/p9/p9-3-tui-ask.md
Normal file
114
tasks/p9/p9-3-tui-ask.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
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.1–1.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;
|
||||
```
|
||||
|
||||
`App` extended with: `ask_input: String`, `ask_explain: bool`, `ask_streaming: bool`, `ask_partial: String`, `ask_answer: Option<kb_core::Answer>`, `ask_thread: Option<std::thread::JoinHandle<anyhow::Result<kb_core::Answer>>>`, `ask_rx: Option<std::sync::mpsc::Receiver<String>>`.
|
||||
|
||||
## 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`. The thread receives a `mpsc::Sender<String>` it forwards each token through (closure plugged into `AskOpts.print_stream`). The TUI reads from the receiver and appends to `ask_partial`.
|
||||
- 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.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_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`.
|
||||
118
tasks/p9/p9-4-tui-inspect.md
Normal file
118
tasks/p9/p9-4-tui-inspect.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
phase: P9
|
||||
component: kb-tui (inspect pane)
|
||||
task_id: p9-4
|
||||
title: "TUI Inspect pane: document & chunk detail render"
|
||||
status: planned
|
||||
depends_on: [p1-6, p9-1]
|
||||
unblocks: []
|
||||
contract_source: ../../docs/superpowers/specs/2026-04-27-kb-final-form-design.md
|
||||
contract_sections: [§1 inspect output, §3.5 Chunk, §2.5 DocSummary, §2.6 ChunkInspection]
|
||||
---
|
||||
|
||||
# p9-4 — TUI Inspect pane
|
||||
|
||||
## Goal
|
||||
|
||||
Render document and chunk inspection views (matching the wire schemas `doc_summary.v1` and `chunk_inspection.v1`) with collapsible sections for `metadata`, `provenance`, `blocks` (doc) and `embeddings` (chunk).
|
||||
|
||||
## Why now / why this size
|
||||
|
||||
Inspect is read-only and has no external interactions; smallest possible pane. Useful for debugging chunker output and citation provenance during P5+ tuning.
|
||||
|
||||
## 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::inspect_doc(id)` | facade | runtime |
|
||||
| `kb-app::inspect_chunk(id)` | facade | runtime |
|
||||
| keyboard events | `crossterm` | terminal |
|
||||
|
||||
## Outputs
|
||||
|
||||
| output | type | downstream |
|
||||
|--------|------|------------|
|
||||
| Ratatui Inspect pane render | terminal | user |
|
||||
|
||||
## Public surface (signatures only — no new types)
|
||||
|
||||
```rust
|
||||
pub enum InspectTarget { Doc(kb_core::DocumentId), Chunk(kb_core::ChunkId) }
|
||||
|
||||
pub fn render_inspect<B: ratatui::backend::Backend>(f: &mut ratatui::Frame, area: ratatui::layout::Rect, state: &App);
|
||||
pub fn handle_key_inspect(state: &mut App, key: crossterm::event::KeyEvent) -> KeyOutcome;
|
||||
```
|
||||
|
||||
`App` extended with: `inspect_target: Option<InspectTarget>`, `inspect_doc: Option<kb_core::CanonicalDocument>`, `inspect_chunk: Option<kb_core::Chunk>`, `inspect_collapsed: HashSet<&'static str>` (sections collapsed), `inspect_scroll: u16`.
|
||||
|
||||
## Behavior contract
|
||||
|
||||
- Switching to Inspect from Library passes `Doc(selected.doc_id)`. From Search pressing `i` (new key on Search pane) passes `Chunk(selected_hit.chunk_id)`.
|
||||
- Doc view layout (top to bottom):
|
||||
1. Header (title, doc_path, doc_id, lang, source_type, trust_level)
|
||||
2. Metadata (aliases / tags / timestamps / `metadata.user` JSON pretty-printed)
|
||||
3. Provenance (events list)
|
||||
4. Blocks (count + first-N preview; on `b` toggle to full list paginated)
|
||||
- Chunk view layout:
|
||||
1. Header (chunk_id, doc_id, doc_path, heading_path, chunker_version)
|
||||
2. Source spans (rendered as W3C fragment URIs per design §0 Q3)
|
||||
3. Text (chunk full text in a scrollable area)
|
||||
4. Embeddings (model_id, dims, embedding_id list — empty if none yet)
|
||||
- Key bindings:
|
||||
- `j` / `k` → scroll
|
||||
- `c` → collapse / expand currently focused section (focus is implicit by current scroll position; v1 may simplify by toggling all sections)
|
||||
- `Esc` → return to previous pane (Library or Search)
|
||||
- `Enter` → no-op (Inspect is terminal — no editor jump here; users use Search pane for jump)
|
||||
- Loading: while `kb-app::inspect_doc` or `inspect_chunk` runs, show "loading…". On error, popup with hint.
|
||||
- Renders must conform to wire schemas `doc_summary.v1` (subset for header) and `chunk_inspection.v1`.
|
||||
|
||||
## Storage / wire effects
|
||||
|
||||
- Reads only.
|
||||
|
||||
## Test plan
|
||||
|
||||
| kind | description | fixture / data |
|
||||
|------|-------------|----------------|
|
||||
| unit | switching to InspectTarget::Doc triggers `kb-app::inspect_doc` once | inline mock |
|
||||
| unit | scroll bounded by content height | inline |
|
||||
| unit | collapse toggle via `c` flips state | inline |
|
||||
| snapshot | doc-view rendered for fixture stable | TestBackend + fixture |
|
||||
| snapshot | chunk-view rendered for fixture stable | TestBackend + fixture |
|
||||
|
||||
All tests under `cargo test -p kb-tui inspect`.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] `cargo check -p kb-tui` passes
|
||||
- [ ] `cargo test -p kb-tui inspect` passes
|
||||
- [ ] No imports outside Allowed dependencies
|
||||
- [ ] Manual smoke: inspect a doc with multiple chunks, scroll, return to library
|
||||
- [ ] PR links design §3.5, §2.5, §2.6
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Editing documents.
|
||||
- Re-ingestion buttons.
|
||||
- Embedding inspection beyond listing model identity.
|
||||
- Side-by-side diff with previous doc version.
|
||||
|
||||
## Risks / notes
|
||||
|
||||
- Long chunk text (~10 KB) rendering can be slow if re-rendered every frame; cache wrapped lines and re-wrap only on resize.
|
||||
- Pretty-printing `metadata.user` as JSON: prefer `serde_json::to_string_pretty`. Indentation = 2 spaces.
|
||||
- Korean text in metadata: ensure `unicode-width`-aware wrapping.
|
||||
140
tasks/p9/p9-5-desktop-tauri.md
Normal file
140
tasks/p9/p9-5-desktop-tauri.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
phase: P9
|
||||
component: kb-desktop (Tauri)
|
||||
task_id: p9-5
|
||||
title: "Tauri desktop app: backend commands wrapping kb-app + multimodal source viewer"
|
||||
status: planned
|
||||
depends_on: [p9-1, p9-2, p9-3, p9-4]
|
||||
unblocks: []
|
||||
contract_source: ../../docs/superpowers/specs/2026-04-27-kb-final-form-design.md
|
||||
contract_sections: [§16.3 desktop epic (tasks/phase-9-ui.md), §1 ask/search scenes, §2 wire schemas v1, §8 module boundaries]
|
||||
---
|
||||
|
||||
# p9-5 — Tauri desktop app
|
||||
|
||||
## Goal
|
||||
|
||||
Stand up a Tauri 2.x app (`kb-desktop` crate as backend, `kb-desktop-frontend/` as web assets) whose Tauri commands wrap `kb-app` 1:1. The frontend renders multimodal source viewers (Markdown render, PDF page viewer, image viewer with region overlay, audio player with seek). Citation clicks route to the appropriate viewer.
|
||||
|
||||
## Why now / why this size
|
||||
|
||||
Last task. Combines all backend phases into a single user-facing surface. Strict policy: backend commands are thin wrappers over `kb-app`; no new business logic.
|
||||
|
||||
## Allowed dependencies
|
||||
|
||||
- backend (`kb-desktop`):
|
||||
- `kb-core`
|
||||
- `kb-config`
|
||||
- `kb-app`
|
||||
- `tauri = "2"` + `tauri-build`
|
||||
- `serde`, `serde_json`
|
||||
- `tracing`
|
||||
- `thiserror`
|
||||
- frontend (`kb-desktop-frontend/`): vanilla TypeScript + Vite (default; user may swap to Svelte/Solid in a follow-up).
|
||||
- PDF rendering: `pdfjs-dist`
|
||||
- Markdown rendering: `marked` + `dompurify`
|
||||
- Audio: HTML `<audio>` with custom segment overlay
|
||||
- Image: HTML `<img>` with absolute-positioned bounding box overlay
|
||||
|
||||
## Forbidden dependencies
|
||||
|
||||
- `kb-source-fs`, `kb-parse-*`, `kb-normalize`, `kb-chunk`, `kb-store-*`, `kb-embed*`, `kb-search`, `kb-llm*`, `kb-rag` (UI must go through `kb-app` only — design §8)
|
||||
|
||||
## Inputs
|
||||
|
||||
| input | type | source |
|
||||
|-------|------|--------|
|
||||
| Tauri commands | invoked from frontend | user clicks |
|
||||
| `kb-config::Config` | runtime | env / file |
|
||||
| user file system (read-only) | for source viewers | OS |
|
||||
|
||||
## Outputs
|
||||
|
||||
| output | type | downstream |
|
||||
|--------|------|------------|
|
||||
| Tauri app bundle (macOS dmg, Linux AppImage, Windows msi) | distribution | user |
|
||||
| Tauri commands return wire-schema-v1 JSON | IPC | frontend |
|
||||
|
||||
## Public surface (signatures only — no new types)
|
||||
|
||||
```rust
|
||||
// Tauri command surface (one per kb-app facade method, plus source viewers)
|
||||
#[tauri::command] fn cmd_init(force: bool) -> Result<()>;
|
||||
#[tauri::command] fn cmd_ingest(scope_json: serde_json::Value, summary_only: bool) -> Result<serde_json::Value /* IngestReportWireV1 */>;
|
||||
#[tauri::command] fn cmd_list_docs(filter_json: serde_json::Value) -> Result<Vec<serde_json::Value /* DocSummaryWireV1 */>>;
|
||||
#[tauri::command] fn cmd_inspect_doc(id: String) -> Result<serde_json::Value /* CanonicalDocument as wire */>;
|
||||
#[tauri::command] fn cmd_inspect_chunk(id: String) -> Result<serde_json::Value /* ChunkInspectionWireV1 */>;
|
||||
#[tauri::command] fn cmd_search(query_json: serde_json::Value) -> Result<Vec<serde_json::Value /* SearchHitWireV1 */>>;
|
||||
#[tauri::command] fn cmd_ask(query: String, opts_json: serde_json::Value) -> Result<serde_json::Value /* AnswerWireV1 */>;
|
||||
#[tauri::command] fn cmd_doctor() -> Result<serde_json::Value /* DoctorReportWireV1 */>;
|
||||
|
||||
// Source viewers — file IO restricted to workspace_root
|
||||
#[tauri::command] fn cmd_read_markdown(path: String) -> Result<String>;
|
||||
#[tauri::command] fn cmd_read_pdf_page(path: String, page: u32) -> Result<Vec<u8> /* PNG bytes rendered via pdfium or backend pre-render */>;
|
||||
#[tauri::command] fn cmd_read_image(path: String) -> Result<Vec<u8>>;
|
||||
#[tauri::command] fn cmd_read_audio(path: String) -> Result<Vec<u8>>;
|
||||
```
|
||||
|
||||
(All commands convert internal `kb-core` types to wire-schema-v1 JSON before returning.)
|
||||
|
||||
## Behavior contract
|
||||
|
||||
- Backend bootstraps `tracing` to a file under `~/.local/state/kb/logs/` and a Tauri plugin loads/saves window state.
|
||||
- Every Tauri command performs **path containment** for source viewers: resolves `path` against `config.workspace.root`, rejects (`anyhow::Error`) any path outside.
|
||||
- Layout (frontend): left = Library + Search + Ask tabs; right = Source viewer keyed by current citation.
|
||||
- Citation routing in the frontend (clicks on `[N]` markers or hit rows):
|
||||
- `Citation::Line { path, start, end }` → load Markdown via `cmd_read_markdown`, render with `marked`, scroll + highlight lines `[start, end]`.
|
||||
- `Citation::Page { path, page }` → render the PDF page via `pdfjs-dist`, scroll to page.
|
||||
- `Citation::Region { path, x, y, w, h }` → load image, overlay a translucent box at `(x, y, w, h)`.
|
||||
- `Citation::Caption { path, model }` → image viewer with caption banner (no overlay).
|
||||
- `Citation::Time { path, start_ms, end_ms }` → audio element `<audio>` seeked to `start_ms / 1000`, with a vertical timeline marker spanning `[start_ms, end_ms]`.
|
||||
- Streaming `kb ask`: backend command `cmd_ask` returns the buffered Answer (per §0 Q5: pipe/JSON mode buffers). For real-time streaming in the desktop, expose a separate `cmd_ask_stream` event channel via Tauri's `Window::emit("kb://ask-token", payload)`. (Implementation can be deferred to a follow-up; v1 of the desktop accepts buffered.)
|
||||
- All backend errors mapped to a `String` message with structure `{ "error": msg, "hint": Option<msg> }`.
|
||||
- Frontend respects light/dark per OS theme (Tauri supplies the API).
|
||||
- No telemetry. No automatic update channel for v1 (manual download).
|
||||
|
||||
## Storage / wire effects
|
||||
|
||||
- Reads via `kb-app` (which reads/writes via SQLite + LanceDB).
|
||||
- Reads workspace files directly for source viewers (path-contained).
|
||||
- Writes nothing outside what `kb-app` writes.
|
||||
- Wire JSON between backend and frontend uses schema v1 strictly. The frontend MUST validate `schema_version` strings on every IPC return and warn (or upgrade-gate) when `v1 != current`.
|
||||
|
||||
## Test plan
|
||||
|
||||
| kind | description | fixture / data |
|
||||
|------|-------------|----------------|
|
||||
| unit (backend) | each command wraps the corresponding `kb-app` function and serializes via wire schema | inline mocks |
|
||||
| unit (backend) | `cmd_read_markdown` rejects paths outside workspace | tmp config |
|
||||
| unit (backend) | citation route in deserialized wire JSON resolves to expected viewer kind (string match) | inline |
|
||||
| smoke (frontend, optional in this task) | Vitest test that mounts the Library tab, calls a mocked `cmd_list_docs`, renders 1 row | minimal |
|
||||
| manual | full-stack smoke against a real ingested workspace (Markdown + 1 PDF + 1 image + 1 audio); each citation jumps correctly | manual checklist |
|
||||
|
||||
Backend tests under `cargo test -p kb-desktop`. Frontend tests are bonus and not gated by this task's DoD.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] `cargo check -p kb-desktop` passes
|
||||
- [ ] `cargo test -p kb-desktop` passes
|
||||
- [ ] `pnpm --filter kb-desktop-frontend build` produces a static asset bundle Tauri can package
|
||||
- [ ] `tauri build` produces an unsigned dmg on macOS in CI (signed/notarized are out of scope)
|
||||
- [ ] Each Tauri command returns wire-schema-v1 JSON; frontend asserts `schema_version`
|
||||
- [ ] No imports outside Allowed dependencies (backend)
|
||||
- [ ] PR links design §16.3 epic, §1, §2 wire schemas, §8
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Code signing & notarization.
|
||||
- Auto-update channel.
|
||||
- Multi-window UI.
|
||||
- Drag-and-drop ingestion (P+).
|
||||
- Workspace selection UI for multi-workspace (multi-workspace itself is out of scope per design §0).
|
||||
- Streaming `ask` event channel (deferred; buffered v1 acceptable).
|
||||
|
||||
## Risks / notes
|
||||
|
||||
- Tauri 2 frontend stack churn: lock pinned versions in `package.json` and `tauri.conf.json` to avoid CI drift.
|
||||
- Path containment is the desktop's most security-sensitive surface; tests must include path traversal vectors (`..`, symlinks, absolute paths).
|
||||
- PDF rendering via `pdfjs-dist` is heavy; lazy-load on first PDF citation.
|
||||
- Audio formats vary; rely on the browser engine's HTML audio decoder (WebKit on macOS supports `.m4a`, `.mp3`; mileage varies on `.flac`/`.ogg`).
|
||||
- Wide Tauri command surface tempts business-logic creep; CI must enforce that no `kb-rag` / `kb-search` / store crate appears in `kb-desktop`'s `cargo tree`.
|
||||
Reference in New Issue
Block a user