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) <noreply@anthropic.com>
This commit is contained in:
@@ -85,7 +85,7 @@ pub const NO_EXT_SENTINEL: &str = "<no-ext>";
|
||||
/// `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 {
|
||||
|
||||
@@ -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<String>` 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<kebab_app::StreamEvent>`
|
||||
/// 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<std::thread::JoinHandle<anyhow::Result<kebab_core::Answer>>>,
|
||||
/// Token receiver paired with the worker's `Sender`. Drained
|
||||
/// every render frame.
|
||||
pub rx: Option<std::sync::mpsc::Receiver<String>>,
|
||||
pub rx: Option<std::sync::mpsc::Receiver<kebab_app::StreamEvent>>,
|
||||
/// 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
|
||||
|
||||
@@ -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::<String>();
|
||||
let (tx, rx) = mpsc::channel::<kebab_app::StreamEvent>();
|
||||
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 { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user