From e5c99f5b80760815a97006ded329a655ec15e317 Mon Sep 17 00:00:00 2001
From: th-kim0823
Date: Sat, 9 May 2026 14:57:46 +0900
Subject: [PATCH] feat(tui): adapt ask worker to StreamEvent sink (fb-33)
Worker channel now carries kebab_app::StreamEvent. drain_stream
matches on Token { delta }; RetrievalDone and Final are ignored
(citations render from last_answer, Final is redundant with
worker join). app::AskState.rx type widened to match.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
crates/kebab-app/src/lib.rs | 2 +-
crates/kebab-tui/src/app.rs | 11 +++++++----
crates/kebab-tui/src/ask.rs | 16 +++++++++++++---
3 files changed, 21 insertions(+), 8 deletions(-)
diff --git a/crates/kebab-app/src/lib.rs b/crates/kebab-app/src/lib.rs
index 602fdaf..960442b 100644
--- a/crates/kebab-app/src/lib.rs
+++ b/crates/kebab-app/src/lib.rs
@@ -85,7 +85,7 @@ pub const NO_EXT_SENTINEL: &str = "";
/// `use kebab_app::AskOpts` keeps working without churn. The struct gained
/// a `stream_sink` field in P4-3; non-streaming callers (kb-cli today)
/// pass `stream_sink: None`.
-pub use kebab_rag::AskOpts;
+pub use kebab_rag::{AskOpts, StreamEvent};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DoctorReport {
diff --git a/crates/kebab-tui/src/app.rs b/crates/kebab-tui/src/app.rs
index 44ed73c..1d53d0c 100644
--- a/crates/kebab-tui/src/app.rs
+++ b/crates/kebab-tui/src/app.rs
@@ -186,9 +186,12 @@ impl Default for SearchState {
/// Ask pane state — owned by p9-3, extended by p9-fb-16 for
/// multi-turn conversation transcript.
///
-/// The worker thread (`thread`) owns the `mpsc::Sender` that
-/// `kebab-app::ask` writes tokens into. The pane keeps the matching
-/// `rx` and drains it once per render frame (no blocking).
+/// The worker thread (`thread`) owns the `mpsc::Sender`
+/// that `kebab-app::ask` writes events into. The pane keeps the matching
+/// `rx` and drains it once per render frame (no blocking). Only the
+/// `Token { delta }` variant is consumed for the streaming transcript;
+/// `RetrievalDone` and `Final` are ignored (citations render from
+/// `last_answer` after the worker join).
///
/// p9-fb-16: completed `Turn`s accumulate in `turns`; the worker
/// passes a snapshot of `turns` as `history` to
@@ -214,7 +217,7 @@ pub struct AskState {
pub thread: Option>>,
/// Token receiver paired with the worker's `Sender`. Drained
/// every render frame.
- pub rx: Option>,
+ pub rx: Option>,
/// Vertical scroll offset for the transcript area when content
/// exceeds the viewport. Only consulted when `follow_tail` is
/// false; otherwise the renderer overrides this with the
diff --git a/crates/kebab-tui/src/ask.rs b/crates/kebab-tui/src/ask.rs
index dd20917..854a325 100644
--- a/crates/kebab-tui/src/ask.rs
+++ b/crates/kebab-tui/src/ask.rs
@@ -483,7 +483,7 @@ pub fn handle_key_ask(state: &mut App, key: KeyEvent) -> KeyOutcome {
}
fn spawn_ask_worker(state: &mut App) {
- let (tx, rx) = mpsc::channel::();
+ let (tx, rx) = mpsc::channel::();
let cfg = state.config.clone();
let s = state.ask.as_mut().unwrap();
// p9-fb-10: take() consumes the input in one step (no clone +
@@ -542,8 +542,18 @@ fn make_conversation_id() -> String {
pub(crate) fn drain_stream(state: &mut App) {
let Some(s) = state.ask.as_mut() else { return };
if let Some(rx) = &s.rx {
- for tok in rx.try_iter() {
- s.partial.push_str(&tok);
+ for ev in rx.try_iter() {
+ match ev {
+ kebab_app::StreamEvent::Token { delta, .. } => {
+ s.partial.push_str(&delta);
+ }
+ // p9-fb-33: TUI ignores RetrievalDone (citation
+ // panel renders after completion via `last_answer`)
+ // and Final (the worker thread's join already
+ // delivers the canonical Answer in poll_worker).
+ kebab_app::StreamEvent::RetrievalDone { .. }
+ | kebab_app::StreamEvent::Final { .. } => {}
+ }
}
}
}