feat(tui): TUI background ingest worker + status bar (p9-fb-03) #55

Merged
altair823 merged 2 commits from feat/p9-fb-03-tui-bg into main 2026-05-02 20:46:28 +00:00
Owner

Summary

kebab tui Library 의 r 키가 kebab_app::ingest_with_config_progress 를 spawned thread 에서 호출 → run loop 가 매 frame 마다 progress channel drain → 화면 하단 status bar 갱신. blocking 하지 않음. spec PR #51 의 §2.4a / §10 + p9-fb-01 의 facade 위에 build. p9-fb-03 spec.

변경

신규 crates/kebab-tui/src/app.rs::IngestState

pub struct IngestState {
    pub rx: Receiver<IngestEvent>,
    pub counts: AggregateCounts,
    pub current_path: Option<String>,
    pub current_idx: u32,
    pub started_at: Instant,
    pub terminal_at: Option<Instant>,
    pub aborted: bool,
    pub thread: Option<JoinHandle<Result<IngestReport>>>,
    pub cancel_tx: Sender<()>, // p9-fb-04 가 wire
}

App.ingest_state: Option<IngestState> slot 추가. p9-fb-04 의 cancel wiring 을 위해 channel 만 allocate, 본 PR 에서 send X.

신규 crates/kebab-tui/src/ingest_progress.rs

  • start_ingest(app) — worker thread spawn + channel allocation.
  • drain_progress(app) — main loop tick 마다 try_recv drain.
  • apply_event(state, event) — kind 별 counter 누적 + terminal mark.
  • status_line(state) — 사람-친화 텍스트 (scanning / 진행 / ✓ 완료 / ✗ abort 4 모드).
  • ready_to_clear(state)TERMINAL_LINE_HOLD_SECS (3 초) 후 true.

Run loop wiring

  • 매 tick: drain_progress + ready_to_clear → terminal 후 3 초 경과 시 slot drop + worker join + Library refresh 큐.
  • Layout: ingest_state Some 일 때 footer 위에 status bar 1 줄 추가. 평시 영향 0.
  • Library footer hint 에 r=ingest 추가.

Status line 형식

scanning…    : `ingest: scanning… [3s]`
진행 중      : `ingest: 142/1024 (13%) notes/foo.md [0:42]`
완료         : `✓ ingest: 1024 docs (12 new, 3 updated, 1009 skipped), 421 chunks indexed in 12s`
abort        : `✗ ingest aborted at 142/1024 after 8s (new=12 updated=3 skipped=127 errors=0)`

미적용 (별 task)

  • Esc / Ctrl-C cancel signal → p9-fb-04 (IngestState.cancel_tx slot 만 본 PR 에서 정의)
  • desktop (P9-5) progress widget

Test plan

  • cargo test -p kebab-tui --lib ingest_progress — 10 PASS (apply_event 5 / status_line 4 / ready_to_clear 2 — 합 11 였으나 status_line 의 in_progress 가 앞에서 setup 한 상태 재사용해 합 10).
  • cargo test -p kebab-tui — 25+ PASS, 기존 15 회귀 0.
  • cargo clippy -p kebab-tui --all-targets -- -D warnings clean.

후속

  • 머지 후 tasks/p9/p9-fb-03-tui-ingest-background.md status in_progresscompleted 한 줄 commit.
## Summary `kebab tui` Library 의 `r` 키가 `kebab_app::ingest_with_config_progress` 를 spawned thread 에서 호출 → run loop 가 매 frame 마다 progress channel drain → 화면 하단 status bar 갱신. blocking 하지 않음. spec PR #51 의 §2.4a / §10 + p9-fb-01 의 facade 위에 build. p9-fb-03 spec. ## 변경 ### 신규 `crates/kebab-tui/src/app.rs::IngestState` ```rust pub struct IngestState { pub rx: Receiver<IngestEvent>, pub counts: AggregateCounts, pub current_path: Option<String>, pub current_idx: u32, pub started_at: Instant, pub terminal_at: Option<Instant>, pub aborted: bool, pub thread: Option<JoinHandle<Result<IngestReport>>>, pub cancel_tx: Sender<()>, // p9-fb-04 가 wire } ``` `App.ingest_state: Option<IngestState>` slot 추가. p9-fb-04 의 cancel wiring 을 위해 channel 만 allocate, 본 PR 에서 send X. ### 신규 `crates/kebab-tui/src/ingest_progress.rs` - `start_ingest(app)` — worker thread spawn + channel allocation. - `drain_progress(app)` — main loop tick 마다 try_recv drain. - `apply_event(state, event)` — kind 별 counter 누적 + terminal mark. - `status_line(state)` — 사람-친화 텍스트 (scanning / 진행 / ✓ 완료 / ✗ abort 4 모드). - `ready_to_clear(state)` — `TERMINAL_LINE_HOLD_SECS` (3 초) 후 true. ### Run loop wiring - 매 tick: `drain_progress` + `ready_to_clear` → terminal 후 3 초 경과 시 slot drop + worker join + Library refresh 큐. - Layout: `ingest_state` Some 일 때 footer 위에 status bar 1 줄 추가. 평시 영향 0. - Library footer hint 에 `r=ingest` 추가. ### Status line 형식 ```text scanning… : `ingest: scanning… [3s]` 진행 중 : `ingest: 142/1024 (13%) notes/foo.md [0:42]` 완료 : `✓ ingest: 1024 docs (12 new, 3 updated, 1009 skipped), 421 chunks indexed in 12s` abort : `✗ ingest aborted at 142/1024 after 8s (new=12 updated=3 skipped=127 errors=0)` ``` ## 미적용 (별 task) - `Esc` / `Ctrl-C` cancel signal → p9-fb-04 (`IngestState.cancel_tx` slot 만 본 PR 에서 정의) - desktop (P9-5) progress widget ## Test plan - [x] `cargo test -p kebab-tui --lib ingest_progress` — 10 PASS (apply_event 5 / status_line 4 / ready_to_clear 2 — 합 11 였으나 status_line 의 in_progress 가 앞에서 setup 한 상태 재사용해 합 10). - [x] `cargo test -p kebab-tui` — 25+ PASS, 기존 15 회귀 0. - [x] `cargo clippy -p kebab-tui --all-targets -- -D warnings` clean. ## 후속 - 머지 후 `tasks/p9/p9-fb-03-tui-ingest-background.md` status `in_progress` → `completed` 한 줄 commit.
altair823 added 1 commit 2026-05-02 20:43:07 +00:00
Library 의 `r` 키가 `kebab_app::ingest_with_config_progress` 를
spawned thread 에서 호출. run loop 가 매 frame 마다 progress channel
drain → 화면 하단 status bar 1 줄 갱신. blocking 하지 않음.

신규:
- crates/kebab-tui/src/app.rs: `IngestState` struct (rx + counts +
  current_path + started_at + terminal_at + aborted + thread +
  cancel_tx) + `App.ingest_state` slot + `TERMINAL_LINE_HOLD_SECS`.
- crates/kebab-tui/src/ingest_progress.rs: `start_ingest` (worker
  spawn + channel allocation), `drain_progress` (try_recv loop),
  `apply_event` (per-kind counter accumulation + Completed/Aborted
  marking), `status_line` (사람-친화 텍스트), `ready_to_clear`
  (3 초 hold).
- 키 cheatsheet: Library footer 에 `r=ingest` 추가.

Run loop:
- 매 tick `drain_progress` + `ready_to_clear` 체크 → terminal 후
  3 초 경과 시 slot drop + worker 스레드 join + Library refresh
  큐.
- Layout: ingest_state Some 일 때 footer 위에 status line 1 줄
  추가 (있을 때만, 평시 영향 0).
- status line: scanning 중 / 진행 (idx/total %, current path,
  elapsed) / 완료 (✓) / abort (✗) 4 모드.

Cancel wiring (p9-fb-04) 의 `IngestState.cancel_tx: Sender<()>`
slot 은 정의만 — 본 PR 에서 sender 보유, send 호출 X.

Test:
- 10 lib unit (apply_event 분기 5 / status_line 4 / ready_to_clear 2).
- 기존 15 tui test 회귀 0.

Plan 갱신:
- p9-fb-03: status `planned` → `in_progress`. 머지 후 한 줄
  commit 으로 `completed` flip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-02 20:44:46 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — 설계 결함 1건 + 가독성 nit 1건 + 칭찬 3건.

핵심 actionable:

  1. (설계 결함) cancel_tx slot 이 dead channel. start_ingest 안에서 _cancel_rx 가 drop 되므로 cancel_tx.send(()) 가 항상 Err. p9-fb-04 가 cancel 을 wire 하려면 어차피 IngestState reshape 필요 → 현 slot 의 미래 호환성 기여 0 + CLAUDE.md 의 "backward-compat shim 금지" 룰 위반 가까움. 권장: 본 PR 에서 cancel_tx field 제거, p9-fb-04 PR 에 함께 추가. 또는 IngestState.cancel_rx: Option<Receiver<()>> 도 함께 보유하는 형태로 변경.
  2. (nit) render_ingest_status 의 aborted-시 Bold 만 — p9-fb-14 (color theme) 후 갈음 가능.

총평: spec PR #51 / app facade (#52) / CLI (#53) 위에 4 번째 layer 정확히 추가. apply_event 의 facade authoritative counts replace 패턴이 drift 방지. 위 #1 의 cancel_tx 결정만 재고 후 머지.

회차 1 — 설계 결함 1건 + 가독성 nit 1건 + 칭찬 3건. 핵심 actionable: 1. **(설계 결함) `cancel_tx` slot 이 dead channel.** `start_ingest` 안에서 `_cancel_rx` 가 drop 되므로 `cancel_tx.send(())` 가 항상 `Err`. p9-fb-04 가 cancel 을 wire 하려면 어차피 `IngestState` reshape 필요 → 현 slot 의 미래 호환성 기여 0 + CLAUDE.md 의 \"backward-compat shim 금지\" 룰 위반 가까움. **권장: 본 PR 에서 `cancel_tx` field 제거, p9-fb-04 PR 에 함께 추가.** 또는 `IngestState.cancel_rx: Option<Receiver<()>>` 도 함께 보유하는 형태로 변경. 2. (nit) `render_ingest_status` 의 aborted-시 Bold 만 — p9-fb-14 (color theme) 후 갈음 가능. 총평: spec PR #51 / app facade (#52) / CLI (#53) 위에 4 번째 layer 정확히 추가. `apply_event` 의 facade authoritative counts replace 패턴이 drift 방지. 위 #1 의 cancel_tx 결정만 재고 후 머지.

(칭찬) IngestState 가 spec 본문의 field set 정확히 mirror — rx / counts / current_path / started_at 추가로 current_idx (status bar 의 idx/total 표시용) + terminal_at / aborted (final 라인 hold + 시각 구분) + thread (clear 시 join). 모든 field pub — p9-fb-04 가 cancel wire 시 외부 mutate 가능. parallel-safety contract 의 "sub-state slot" 패턴 정확.

(칭찬) `IngestState` 가 spec 본문의 field set 정확히 mirror — `rx` / `counts` / `current_path` / `started_at` 추가로 `current_idx` (status bar 의 `idx/total` 표시용) + `terminal_at` / `aborted` (final 라인 hold + 시각 구분) + `thread` (clear 시 join). 모든 field `pub` — p9-fb-04 가 cancel wire 시 외부 mutate 가능. parallel-safety contract 의 "sub-state slot" 패턴 정확.
@@ -0,0 +156,4 @@
state.counts.scanned.saturating_sub(state.counts.errors),
state.counts.scanned,
secs,
state.counts.new,

(설계 결함 / cancel slot 사용 불가) _cancel_rxstart_ingest 안에서 즉시 drop 하면 IngestState.cancel_tx.send(()) 가 항상 Err(SendError) — channel 의 receiver 가 사라졌기 때문. p9-fb-04 가 cancel 을 wire 하려면 (a) receiver 도 worker thread 로 move 하거나 (b) IngestState 가 receiver 를 함께 보유해야 함. 현 상태로는 cancel_tx 가 슬롯이 아니라 dead channel — p9-fb-04 가 IngestState 를 reshape 해야 동작.

Why: spec 의 의도는 "slot 만 정의, p9-fb-04 가 send 만 추가". 그러나 receiver 가 같은 함수에서 dropped 면 send 결과가 noop 이라 cancel 신호 자체가 worker 에 도달 못 함. p9-fb-04 가 어차피 IngestState 손대야 한다면 본 PR 의 cancel_tx slot 은 미래 호환성 기여 0 — 차라리 p9-fb-04 까지 미루는 게 정직.

How to apply (둘 중):

  1. IngestStatepub cancel_rx: Option<Receiver<()>> 추가, start_ingest 가 둘 다 보유. p9-fb-04 가 take() 해서 worker thread 로 move.
  2. 또는 본 PR 에서 cancel_tx field 를 아예 제거하고 p9-fb-04 가 추가하는 것으로 미룸. 어차피 wire 가 함수 시그니처 (ingest_with_config_cancellable) 추가도 동반하므로 IngestState reshape 도 같이 처리.

권장: 옵션 2 — slot 만 두는 것은 backward-compat shim 패턴 (CLAUDE.md 금지) 에 가깝고, 실제로 사용도 못 함. p9-fb-04 의 PR 에 함께 추가.

(설계 결함 / cancel slot 사용 불가) `_cancel_rx` 를 `start_ingest` 안에서 즉시 drop 하면 `IngestState.cancel_tx.send(())` 가 항상 `Err(SendError)` — channel 의 receiver 가 사라졌기 때문. p9-fb-04 가 cancel 을 wire 하려면 (a) receiver 도 worker thread 로 move 하거나 (b) IngestState 가 receiver 를 함께 보유해야 함. 현 상태로는 cancel_tx 가 슬롯이 아니라 dead channel — p9-fb-04 가 `IngestState` 를 reshape 해야 동작. Why: spec 의 의도는 "slot 만 정의, p9-fb-04 가 send 만 추가". 그러나 receiver 가 같은 함수에서 dropped 면 send 결과가 noop 이라 cancel 신호 자체가 worker 에 도달 못 함. p9-fb-04 가 어차피 IngestState 손대야 한다면 본 PR 의 cancel_tx slot 은 미래 호환성 기여 0 — 차라리 p9-fb-04 까지 미루는 게 정직. How to apply (둘 중): 1. `IngestState` 에 `pub cancel_rx: Option<Receiver<()>>` 추가, `start_ingest` 가 둘 다 보유. p9-fb-04 가 `take()` 해서 worker thread 로 move. 2. 또는 본 PR 에서 `cancel_tx` field 를 아예 제거하고 p9-fb-04 가 추가하는 것으로 미룸. 어차피 wire 가 함수 시그니처 (`ingest_with_config_cancellable`) 추가도 동반하므로 IngestState reshape 도 같이 처리. 권장: 옵션 2 — slot 만 두는 것은 backward-compat shim 패턴 (CLAUDE.md 금지) 에 가깝고, 실제로 사용도 못 함. p9-fb-04 의 PR 에 함께 추가.
@@ -0,0 +193,4 @@
use kebab_app::AggregateCounts;
use kebab_core::IngestItemKind;
use std::sync::mpsc;

(칭찬) apply_eventCompleted { counts } / Aborted { counts } 분기에서 facade 의 authoritative counts 로 교체 (state.counts = counts). running totals 와 final report 사이 작은 drift (예: AssetFinished 가 chunks 보고 vs facade 의 chunks_indexed = embed_active 시에만 누적) 가 자동 정정. kebab-app::ingest_with_config_progress 의 보고와 status bar 가 항상 일치 보장.

(칭찬) `apply_event` 의 `Completed { counts }` / `Aborted { counts }` 분기에서 facade 의 authoritative counts 로 교체 (`state.counts = counts`). running totals 와 final report 사이 작은 drift (예: AssetFinished 가 chunks 보고 vs facade 의 `chunks_indexed = embed_active 시에만 누적`) 가 자동 정정. `kebab-app::ingest_with_config_progress` 의 보고와 status bar 가 항상 일치 보장.
@@ -32,0 +38,4 @@
.ingest_state
.as_ref()
.map(crate::ingest_progress::ready_to_clear)
.unwrap_or(false);

(칭찬) clear_now 분기가 take()thread.take() + join() + Library needs_refresh 큐 — terminal event 후 3 초 hold 동안 사용자가 final 라인 읽고, slot drop 시 worker 정리 + 다음 idle tick 에서 Library 가 재로드 → 새 doc 자동 surface. 사용자 명시 action 없이 신선한 view 보장.

(칭찬) `clear_now` 분기가 `take()` 후 `thread.take()` + `join()` + Library `needs_refresh` 큐 — terminal event 후 3 초 hold 동안 사용자가 final 라인 읽고, slot drop 시 worker 정리 + 다음 idle tick 에서 Library 가 재로드 → 새 doc 자동 surface. 사용자 명시 action 없이 신선한 view 보장.

(가독성 nit) render_ingest_statusSpan::styledModifier::BOLD 만 적용 (aborted 시) — color theme (p9-fb-14) 도입 전이라 이번 PR 의 visual 차이는 미미. 다만 ✓ / ✗ 첫 글자만으로도 시각 구분 가능하니 OK. 후속 (p9-fb-14) 에서 Theme::warning / Theme::error 로 갈음 가능 — coupling 0.

(가독성 nit) `render_ingest_status` 가 `Span::styled` 에 `Modifier::BOLD` 만 적용 (aborted 시) — color theme (p9-fb-14) 도입 전이라 이번 PR 의 visual 차이는 미미. 다만 ✓ / ✗ 첫 글자만으로도 시각 구분 가능하니 OK. 후속 (p9-fb-14) 에서 `Theme::warning` / `Theme::error` 로 갈음 가능 — coupling 0.
altair823 added 1 commit 2026-05-02 20:46:15 +00:00
회차 1 의 설계 결함 지적 반영. 원래 IngestState 에 cancel_tx:
Sender<()> 만 두고 receiver 는 start_ingest 안에서 즉시 drop —
실제 send() 호출 시 항상 Err(SendError) 인 dead channel 이 됨.
\"slot 만 정의\" 의도였으나 실용 가치 0 + CLAUDE.md 의 backward-
compat shim 금지 룰 위반.

수정:
- IngestState 에서 cancel_tx field 제거.
- start_ingest 의 cancel channel allocation 제거.
- doc comment 갱신 — p9-fb-04 가 (cancel_tx, cancel_rx) pair 동시
  추가 + receiver 를 worker thread 로 move 하는 형태로 reshape 한다고
  명시.
- test fresh_state helper 도 cancel_tx 인자 제거.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-02 20:46:23 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 의 설계 결함 (cancel_tx dead channel) 정확히 반영. cancel_tx field + channel allocation 제거 + doc 갱신 (p9-fb-04 가 reshape). 추가 actionable 0. APPROVE.

회차 2 — 회차 1 의 설계 결함 (cancel_tx dead channel) 정확히 반영. cancel_tx field + channel allocation 제거 + doc 갱신 (p9-fb-04 가 reshape). 추가 actionable 0. APPROVE.
altair823 merged commit 3177ba01a4 into main 2026-05-02 20:46:28 +00:00
altair823 deleted branch feat/p9-fb-03-tui-bg 2026-05-02 20:46:29 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#55