Files
kebab/docs/components/ui/README.md
th-kim0823 af8c162e09 docs(components): per-group contributor reference (12 그룹)
docs/components/<group>/README.md 12 페이지 + 인덱스 작성. 각 그룹
페이지가 구성 crate 표 + 구조 mermaid + data flow mermaid + 주요
type/trait/함수 시그니처 + 외부 의존 + 핵심 결정 (HOTFIXES + spec
의 "왜" 통합) + 관련 spec/HOTFIXES 링크. 인덱스가 그룹 wiring
다이어그램 + 진입 가이드 보유.

ARCHITECTURE.md 의 ASCII crate 의존 그래프를 mermaid flowchart 로
교체 (등가 정보, Gitea/GitHub 자동 렌더). docs/components/ 진입
링크 추가.

이 layer 는 contributor 향 — 사용자 향 grand picture 는 README.md
의 logical-architecture diagram 그대로 유지. 진척도는 HANDOFF.md,
per-task spec 은 tasks/INDEX.md 가 기존대로 source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:05:32 +09:00

184 lines
11 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.
# UI
> 사용자 surface — 두 binary, 둘 다 `kebab-app` facade 만 사용. CLI 가 1:1 subcommand → app fn 매핑, TUI 가 4 패널 (Library / Search / Ask / Inspect) ratatui shell.
## 구성 crate
| Crate | 역할 |
|-------|------|
| `kebab-cli` | binary `kebab`. clap subcommand → `kebab-app` fn 1:1. wire schema v1 envelope (`--json`). progress bar (indicatif). Ctrl-C cancel handler. exit codes. |
| `kebab-tui` | binary `kebab tui` (그리고 라이브러리). ratatui + crossterm. 4 패널 + cheatsheet popup + error overlay + Vim-style mode machine. async search worker + background ingest worker + external editor jump. |
## 구조 (TUI 위주, CLI 는 단순)
```mermaid
classDiagram
class App {
config: Config
sqlite: Arc~SqliteStore~
kebab_app: app::App
focus: Pane
mode: Mode
theme: Theme
library: LibraryState
search: SearchState
ask: AskState
inspect: InspectState
ingest: Option~IngestState~
cheatsheet_visible: bool
error_overlay: Option~ErrorOverlay~
pending_editor: Option~EditorRequest~
search_cache: ...
}
class Pane {
<<enum>>
Library
Search
Ask
Inspect
Jobs
}
class Mode {
<<enum>>
Normal
Insert
+auto_for(pane) Mode
+label() &str
}
class LibraryState
class SearchState {
input: InputBuffer
generation: u64
worker_thread: Option~JoinHandle~
worker_rx: Option~Receiver~
}
class AskState {
input: InputBuffer
turns: Vec~Turn~
conversation_id: Option~String~
last_answer: Option~Answer~
}
class InspectState
class IngestState {
cancel: Arc~AtomicBool~
rx: Receiver~IngestEvent~
partial_counts
}
class Theme {
+from_name("dark"|"light")
+style(Role) Style
}
class InputBuffer {
content: String
cursor_col: usize
push_char/pop_char/clear/take
}
App --> Pane : focus
App --> Mode
App --> Theme
App --> LibraryState
App --> SearchState
App --> AskState
App --> InspectState
App --> IngestState
SearchState --> InputBuffer
AskState --> InputBuffer
```
## Data flow — 전체 ratatui run loop
```mermaid
flowchart LR
Start["kebab tui<br/>main"]
Setup["enable_raw_mode<br/>+ EnterAlternateScreen<br/>+ Hide cursor"]
Loop["run loop"]
Tick["매 tick"]
Drain["drain progress channel<br/>(ingest worker)"]
Poll["search worker poll<br/>(generation match?)"]
Render["render pane (focus)<br/>+ overlay (error/cheatsheet)"]
Event["crossterm event"]
CIntercept["cheatsheet_intercept<br/>(F1 toggle, Esc close)"]
MIntercept["mode_intercept<br/>(i/Esc Normal↔Insert)"]
Dispatch["pane dispatch<br/>(library/search/ask/inspect)"]
EditorReq["EditorRequest enqueue"]
EditorSpawn["with_external_program<br/>(suspend → spawn → restore)"]
Restore["disable_raw_mode + force_redraw"]
Start --> Setup --> Loop
Loop --> Tick --> Drain --> Poll --> Render
Render --> Event
Event --> CIntercept --> MIntercept --> Dispatch
Dispatch --> EditorReq --> EditorSpawn -.RAII guard.-> Restore -.-> Render
Loop -.quit.-> Setup
```
## 주요 type / trait / 함수
**`kebab-cli`** (`main.rs`):
- clap `Cli { config: Option<PathBuf>, verbose, debug, json, command: Cmd }``--config` 가 모든 subcommand 에 전역.
- subcommand 별 호출: `init``init_workspace`, `ingest``ingest_with_config_cancellable`, `search``search_with_config`, `ask``ask_with_config` 또는 `ask_with_session_with_config`, `list` / `inspect` / `doctor` / `tui` / `reset` → 대응 `*_with_config`.
- `progress.rs``indicatif::ProgressBar` (TTY 시) / non-TTY 한 줄씩 / `--json` line-delimited stdout.
- `cancel.rs``ctrlc` SIGINT handler. 1회 → cancel signal, 2회 → hard exit 130.
- `wire.rs``*.v1` envelope wrap (`ingest_report.v1`, `search_hit.v1`, `answer.v1`, `doctor.v1` 등). `DoctorReport` 만 자체 schema_version, 다른 도메인 type 은 cli 에서 wrap.
- exit code: 0 (ok) / 1 (anyhow chain) / 2 (clap usage) / 3 (no-hit signal) / 4 (refusal signal) / 5 (doctor unhealthy) — design §10.
**`kebab-tui`** (`lib.rs` re-export):
- `App { config, sqlite, kebab_app, focus, mode, theme, library, search, ask, inspect, ingest, cheatsheet_visible, error_overlay, pending_editor, search_cache, ... }` — single-threaded shell state.
- `Pane` enum (Library / Search / Ask / Inspect / Jobs), `Mode` enum (Normal / Insert) + `Mode::auto_for(pane)` (Library/Inspect/Jobs → Normal, Search/Ask → Insert).
- `InputBuffer { content, cursor_col }` — wide-char-aware (한글 = 2 col, ASCII = 1 col, combining = 0). `push_char` / `pop_char` / `clear` / `take`.
- `Theme { from_name, style(Role) }` — 16 Role × 2 palette (dark/light). 모든 pane 의 inline `Style::default().fg(...)``theme.style(Role::X)` 격리.
- `render_*(f, area, app, ...)` per pane — `f: &mut Frame<'_>` (ratatui 0.28 backend-agnostic). cursor caret 은 caret-필요 pane (Search / Ask / Filter) 가 `set_cursor_position(...)` 호출.
- `handle_key_*(app, key) -> KeyOutcome` per pane — Mode-authoritative dispatch (p9-fb-12 follow-up): Normal 에서 nav/command 키, Insert 에서 typing.
- `enter_inspect(app, target: InspectTarget, return_to: Pane)` (p9-fb-04) — Library `Enter` (Doc) / Search `o` (Chunk).
- `with_external_program(&mut TuiTerminal, Command)` (p9-fb-09) — RAII guard 가 atomic suspend/restore.
- `cheatsheet_intercept(app, key)` (p9-fb-13) — F1 toggle, mode_intercept 보다 먼저 dispatch.
- `start_ingest / drain_progress / cancel_running_ingest / ready_to_clear / status_line` (p9-fb-03) — Library `r` 가 spawned thread + channel + Esc cancel.
- `markdown::render(text, &Theme) -> Vec<Line<'static>>` (p9-fb-11) — pulldown-cmark 위 inline + block + table + code + heading.
- `footer_hints(focus, mode, filter_open) -> &'static str` (p9-fb-13 follow-up) — 한국어 동사구 + mode-aware. 첫 fragment 항상 `F1 도움말`.
## 외부 의존
- `kebab-cli``kebab-app` (only), `clap`, `indicatif`, `ctrlc`, `serde_json`, `anyhow`. **forbidden** 직접 import: `kebab-store-*` / `kebab-llm-*` / `kebab-search` / `kebab-rag`.
- `kebab-tui``kebab-app` (only) + `kebab-config` + `kebab-core` + `kebab-store-sqlite` (직접 import — App 의 sqlite handle 공유 위함, ChatSessionRepo 호출), `ratatui` (0.28), `crossterm` (0.28), `pulldown-cmark` (0.13), `unicode-width`, `lru`, `anyhow`, `time`.
- 외부 서비스: 없음 (facade 가 가져옴).
## 핵심 결정
- **UI binary 가 facade 만 → swap 가능**.
**왜**: future MCP server / HTTP wrapper 가 같은 contract 위에 build. `kebab-cli``--json` envelope 으로 wire schema v1 표면 = 외부 통합 (Claude Code skill 등) 이 binary 한 줄 spawn 으로 끝.
- **`kebab-app::App` 한 번 open → CLI subcommand 처리 후 drop**.
**왜**: per-invocation cold start 단순. 장수 caller (kebab-tui session) 만 retain → memoized embedder/vector/llm 이득.
- **TUI = single-threaded run loop + 외부 worker thread**.
**왜**: ratatui idiomatic. 매 tick render. search / ingest 처럼 50-200ms+ 작업은 별 thread + channel post 로 freeze 회피. main thread 가 매 tick channel drain + apply.
- **search worker = generation counter + stale drop** (p9-fb-08).
**왜**: 사용자가 빠르게 타이핑 시 매 keystroke 가 worker spawn. 이전 worker 의 결과가 늦게 도착해도 generation mismatch → silent drop. UI 항상 최신 query 의 결과만 보임.
- **Vim-style Mode machine** (p9-fb-12).
**왜**: 텍스트 입력 (`e`/`j`/`k`/`i`) 와 command 키 (`e`=explain, `j`=down, `k`=up, `i`=inspect) 충돌. 도그푸딩에서 "explain" / "javascript" 같은 단어 입력이 mode 안 가지고 깨짐. Normal/Insert 명시. Search/Ask 는 자동 Insert (입력 위주), Library/Inspect/Jobs 는 자동 Normal (nav 위주). `i` 가 universal Normal→Insert toggle (p9-fb-21), Search 의 chunk inspect 는 `i``o` rebind (vim "open").
- **CJK column-aware InputBuffer** (p9-fb-10).
**왜**: ratatui frame 의 `set_cursor_position(...)` 가 column 단위. byte/char 인덱스 시 한글 1 글자가 1 col 만 차지하는 것처럼 cursor 가 박힘. `unicode-width` 위 wide-char 단위 cursor_col 추적 → caret 정확. backspace 는 모든 pane 이 `String::pop()` 으로 char-aware 이미 안전.
- **Theme module** (p9-fb-14).
**왜**: dark/light palette swap 가 inline `fg(Color::*)` 흩어져 있으면 한 군데 빼먹음. 16 Role × 2 Palette exhaustive match → unknown role compile-time fail. config typo 시 dark fallback (panic 안 함).
- **External editor jump = RAII guard** (p9-fb-09).
**왜**: ratatui alt-screen + raw mode 가 spawn 사이 깨지면 화면 손상. suspend (LeaveAlt + Show cursor + disable_raw) → spawn → restore (enable_raw + EnterAlt + Hide + clear) 시퀀스를 RAII 로 묶음. 키 핸들러는 enqueue 만, run loop 가 `TuiTerminal` handle 들고 spawn — handle ownership 분리.
- **Multi-turn Ask UI** (p9-fb-16).
**왜**: 도그푸딩에서 "이 문서 더 자세히" 같은 follow-up 질문이 standalone single-shot 으로 처리되어 retrieval 부정확. answer area 가 transcript (Q1/A1, Q2/A2, ...). 매 Enter 가 prior turns 를 history 로 worker 에 전달. `Ctrl-L` 로 conversation 초기화.
- **F1 cheatsheet popup** (p9-fb-13).
**왜**: 도그푸딩에서 keybinding discoverability 문제. 단축키 도움말 없으면 사용자가 매번 README 검색. spec 의 `?` trigger 가 Library 의 quick-Ask 와 충돌해서 `F1` rebind. mode_intercept 보다 먼저 dispatch — popup 이 mode flip 발동 안 시킴.
## 관련 spec / HOTFIXES
- frozen 설계 §1 (UX scenes), §2.4a (ingest progress wire), §3.7 (SearchHit / DocSummary), §3.8 (Answer / Turn), §8 (boundary), §10 (errors / exit codes): [`docs/superpowers/specs/2026-04-27-kebab-final-form-design.md`](../../superpowers/specs/2026-04-27-kebab-final-form-design.md)
- task spec:
- CLI: [`tasks/p0/p0-1-skeleton.md`](../../../tasks/p0/p0-1-skeleton.md) (초기) + 모든 phase 의 wiring task.
- TUI: [`tasks/p9/p9-1-tui-library.md`](../../../tasks/p9/p9-1-tui-library.md), [`tasks/p9/p9-2-tui-search.md`](../../../tasks/p9/p9-2-tui-search.md), [`tasks/p9/p9-3-tui-ask.md`](../../../tasks/p9/p9-3-tui-ask.md), [`tasks/p9/p9-4-tui-inspect.md`](../../../tasks/p9/p9-4-tui-inspect.md)
- 도그푸딩 후속 (p9-fb-01..21): [`tasks/p9/`](../../../tasks/p9/)
- HOTFIXES (P9-1 Backend generic 제거, P9-2 workspace_root 인자, P9-3 e/j/k 텍스트 충돌 → mode machine, p9-fb-* 도그푸딩 후속): [`tasks/HOTFIXES.md`](../../../tasks/HOTFIXES.md)