tasks: add P9 component specs (tui x4, desktop)

This commit is contained in:
kb
2026-04-27 12:14:16 +00:00
parent 7c10b15ad7
commit f8b9f51d94
5 changed files with 613 additions and 0 deletions

View 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
View 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
View 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.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;
```
`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.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`.

View 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.

View 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`.