Files
kebab/tasks/p9/p9-5-desktop-tauri.md
altair823 f9714aa5cb docs(rename): kb → kebab — README, tasks/, docs/, design doc, report
마지막 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>
2026-05-02 04:01:55 +00:00

8.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-desktop (Tauri) p9-5 Tauri desktop app: backend commands wrapping kebab-app + multimodal source viewer planned
p9-1
p9-2
p9-3
p9-4
../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md
report §16.3 desktop (also tasks/phase-9-ui.md epic)
design §1 ask/search scenes
design §2 wire schemas v1
design §8 module boundaries

p9-5 — Tauri desktop app

Goal

Stand up a Tauri 2.x app (kebab-desktop crate as backend, kebab-desktop-frontend/ as web assets) whose Tauri commands wrap kebab-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 kebab-app; no new business logic.

Allowed dependencies

  • backend (kebab-desktop):
    • kebab-core
    • kebab-config
    • kebab-app
    • tauri = "2" + tauri-build
    • serde, serde_json
    • tracing
    • thiserror
  • frontend (kebab-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

  • kebab-source-fs, kebab-parse-*, kebab-normalize, kebab-chunk, kebab-store-*, kebab-embed*, kebab-search, kebab-llm*, kebab-rag (UI must go through kebab-app only — design §8).
  • No native PDF render backend (no pdfium, no mupdf, no poppler). PDF rendering lives entirely in the frontend (pdfjs-dist). Adding any of these would (a) bloat the bundle 100+ MB, (b) require frozen-design amendment, and (c) double the path-containment surface.

Inputs

input type source
Tauri commands invoked from frontend user clicks
kebab-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)

// Tauri command surface (one per kebab-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, raw-bytes only.
// Rendering happens 100% in the frontend (pdfjs / <img> / <audio>); backend has NO native render dependency.
#[tauri::command] fn cmd_read_markdown(path: String) -> Result<String>;       // utf-8 Markdown source
#[tauri::command] fn cmd_read_file_bytes(path: String) -> Result<Vec<u8>>;    // raw bytes for PDF / image / audio

(All commands convert internal kebab-core types to wire-schema-v1 JSON before returning.)

Behavior contract

  • Backend bootstraps tracing to a file under ~/.local/state/kebab/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). All rendering is frontend-side; backend serves raw bytes only.
    • Citation::Line { path, start, end }cmd_read_markdown(path), render with marked, scroll + highlight lines [start, end].
    • Citation::Page { path, page }cmd_read_file_bytes(path) → pass Uint8Array to pdfjs-dist (getDocument({ data })), navigate to page. No backend PDF render; no pdfium native dep.
    • Citation::Region { path, x, y, w, h }cmd_read_file_bytes(path) → blob URL → <img> + absolute-positioned overlay at (x, y, w, h).
    • Citation::Caption { path, model } → same as Region but no overlay; caption banner shows model.
    • Citation::Time { path, start_ms, end_ms }cmd_read_file_bytes(path) → blob URL → <audio src=...> seeked to start_ms / 1000, with a timeline marker spanning [start_ms, end_ms].
  • Streaming kebab 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("kebab://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 kebab-app (which reads/writes via SQLite + LanceDB).
  • Reads workspace files directly for source viewers (path-contained).
  • Writes nothing outside what kebab-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 kebab-app function and serializes via wire schema inline mocks
unit (backend) cmd_read_markdown rejects paths outside workspace tmp config
unit (backend) cmd_read_file_bytes rejects paths outside workspace incl. .., absolute path, symlink-out tmp config + traversal vectors
unit (backend) cmd_read_file_bytes returns identical bytes to std::fs::read for an in-workspace file 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 kebab-desktop. Frontend tests are bonus and not gated by this task's DoD.

Definition of Done

  • cargo check -p kebab-desktop passes
  • cargo test -p kebab-desktop passes
  • pnpm --filter kebab-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 report §16.3 (desktop epic), design §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 (~2 MB worker); lazy-load on first PDF citation. The trade-off vs a native render backend (e.g., pdfium ~150 MB binary, code-signing pain) is heavily one-sided; v1 stays on pdfjs-dist.
  • 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 kebab-rag / kebab-search / store crate appears in kebab-desktop's cargo tree.