feat(tui): search pane t-key opens TracePopup (fb-37)
This commit is contained in:
@@ -387,6 +387,8 @@ pub struct App {
|
||||
pub ask: Option<AskState>,
|
||||
/// Populated by p9-4.
|
||||
pub inspect: Option<InspectState>,
|
||||
/// p9-fb-37: trace popup state, `Some` while open.
|
||||
pub trace_popup: Option<crate::trace_popup::TracePopupState>,
|
||||
/// Populated by p9-fb-03 when the user kicks off an in-shell
|
||||
/// ingest (Library `r`). Cleared by the run loop a few seconds
|
||||
/// after the run reaches a terminal event.
|
||||
@@ -461,6 +463,7 @@ impl App {
|
||||
search: None,
|
||||
ask: None,
|
||||
inspect: None,
|
||||
trace_popup: None,
|
||||
ingest_state: None,
|
||||
error_overlay: None,
|
||||
should_quit: false,
|
||||
|
||||
@@ -80,6 +80,7 @@ pub fn render_cheatsheet(f: &mut Frame, area: Rect, app: &App) {
|
||||
("Delete", "remove char at cursor"),
|
||||
("g", "open hit's citation in $EDITOR (Normal)"),
|
||||
("o", "inspect selected hit's chunk (Normal — was `i` pre-fb-21)"),
|
||||
("t", "open retrieval trace popup (Normal — p9-fb-37)"),
|
||||
("i", "Normal → Insert (toggle back to typing)"),
|
||||
("Esc", "back to Library"),
|
||||
]);
|
||||
|
||||
@@ -27,6 +27,7 @@ mod run;
|
||||
mod search;
|
||||
mod terminal;
|
||||
mod theme;
|
||||
pub mod trace_popup;
|
||||
|
||||
pub use input::{InputBuffer, display_width, place_cursor_x, truncate_to_display_width};
|
||||
pub use theme::{Palette, Role, Theme};
|
||||
|
||||
@@ -130,6 +130,21 @@ pub(crate) fn run_loop(app: &mut App) -> Result<()> {
|
||||
if event::poll(POLL_INTERVAL)? {
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
||||
// p9-fb-37: trace popup eats keys while open.
|
||||
// Sits ahead of cheatsheet + mode + pane dispatch
|
||||
// so Esc / j / k / arrows route to the popup
|
||||
// instead of leaking through to the search pane.
|
||||
if app.trace_popup.is_some() {
|
||||
let close = if let Some(popup) = app.trace_popup.as_mut() {
|
||||
crate::trace_popup::handle_key_trace_popup(popup, key)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if close {
|
||||
app.trace_popup = None;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// p9-fb-13: cheatsheet popup toggle takes
|
||||
// precedence over both mode + pane dispatch.
|
||||
// F1 toggles open/close. While visible, Esc
|
||||
@@ -255,6 +270,12 @@ fn render_root(f: &mut Frame, app: &App) {
|
||||
}
|
||||
render_status_bar(f, outer[2], app);
|
||||
render_key_hints(f, outer[3], app);
|
||||
// p9-fb-37: trace popup overlays on top of pane content but
|
||||
// below the error overlay (errors are higher-priority modal).
|
||||
if let Some(popup) = &app.trace_popup {
|
||||
let popup_area = centered_rect(80, 80, f.area());
|
||||
crate::trace_popup::render_trace_popup(f, popup_area, popup);
|
||||
}
|
||||
if let Some(err) = &app.error_overlay {
|
||||
render_error_overlay(f, f.area(), err, &app.theme);
|
||||
}
|
||||
@@ -263,6 +284,28 @@ fn render_root(f: &mut Frame, app: &App) {
|
||||
}
|
||||
}
|
||||
|
||||
/// p9-fb-37: centered sub-rect helper for the trace popup. Returns
|
||||
/// a rect of `percent_x` × `percent_y` percent of `r`, centered.
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: ratatui::layout::Rect) -> ratatui::layout::Rect {
|
||||
use ratatui::layout::{Constraint, Direction, Layout};
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(r);
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
|
||||
fn render_header(f: &mut Frame, area: Rect, app: &App) {
|
||||
let pane_label = match app.focus {
|
||||
Pane::Library => "Library",
|
||||
|
||||
@@ -209,6 +209,49 @@ pub fn handle_key_search(state: &mut App, key: KeyEvent) -> KeyOutcome {
|
||||
// pre-fb-12 SHIFT/none heuristic).
|
||||
let is_normal = state.mode == crate::app::Mode::Normal;
|
||||
|
||||
// p9-fb-37: `t` opens the trace popup. Re-runs the last submitted
|
||||
// query with SearchOpts.trace = true. Bypasses cache by going
|
||||
// through `search_with_opts_with_config` (Task 5 wires opts.trace
|
||||
// to skip the LRU cache).
|
||||
if is_normal
|
||||
&& matches!(
|
||||
(key.code, key.modifiers),
|
||||
(KeyCode::Char('t'), KeyModifiers::NONE)
|
||||
)
|
||||
{
|
||||
let (last_query, has_results) = {
|
||||
let s = state.search.as_ref().unwrap();
|
||||
(s.last_query.clone(), !s.hits.is_empty())
|
||||
};
|
||||
if !has_results {
|
||||
return KeyOutcome::Continue;
|
||||
}
|
||||
if let Some((q_text, q_mode)) = last_query {
|
||||
let q = kebab_core::SearchQuery {
|
||||
text: q_text,
|
||||
mode: q_mode,
|
||||
k: state.config.search.default_k,
|
||||
filters: kebab_core::SearchFilters::default(),
|
||||
};
|
||||
let opts = kebab_core::SearchOpts {
|
||||
trace: true,
|
||||
..Default::default()
|
||||
};
|
||||
match kebab_app::search_with_opts_with_config(state.config.clone(), q, opts) {
|
||||
Ok(resp) => {
|
||||
if let Some(t) = resp.trace {
|
||||
state.trace_popup = Some(crate::trace_popup::TracePopupState::new(t));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Silent failure — trace is debug-only; user
|
||||
// can still see search hits without it.
|
||||
}
|
||||
}
|
||||
}
|
||||
return KeyOutcome::Continue;
|
||||
}
|
||||
|
||||
// p9-fb-21: chunk-inspect rebound from `i` to `o` (vim "open").
|
||||
// The `i` key is now the universal Normal→Insert toggle (handled
|
||||
// in `mode_intercept`), so it cannot also mean "inspect chunk"
|
||||
|
||||
139
crates/kebab-tui/src/trace_popup.rs
Normal file
139
crates/kebab-tui/src/trace_popup.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
//! p9-fb-37: TUI trace popup. Opens from Search pane via `t` key
|
||||
//! when results are visible. Re-runs the current query with
|
||||
//! `SearchOpts.trace = true` and displays the lex / vec / rrf union
|
||||
//! + per-stage timing as a single scroll list.
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use kebab_core::SearchTrace;
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TracePopupState {
|
||||
pub trace: SearchTrace,
|
||||
pub scroll: u16,
|
||||
}
|
||||
|
||||
impl TracePopupState {
|
||||
pub fn new(trace: SearchTrace) -> Self {
|
||||
Self { trace, scroll: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_trace_popup(f: &mut Frame, area: Rect, state: &TracePopupState) {
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
let bold = Style::default().add_modifier(Modifier::BOLD);
|
||||
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(
|
||||
"Lexical ({} hits, {} ms)",
|
||||
state.trace.lexical.len(),
|
||||
state.trace.timing.lexical_ms,
|
||||
),
|
||||
bold,
|
||||
)));
|
||||
for c in &state.trace.lexical {
|
||||
lines.push(Line::from(format!(
|
||||
" #{:>2} score={:.4} chunk={}",
|
||||
c.rank, c.score, c.chunk_id.0
|
||||
)));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(
|
||||
"Vector ({} hits, {} ms)",
|
||||
state.trace.vector.len(),
|
||||
state.trace.timing.vector_ms,
|
||||
),
|
||||
bold,
|
||||
)));
|
||||
for c in &state.trace.vector {
|
||||
lines.push(Line::from(format!(
|
||||
" #{:>2} score={:.4} chunk={}",
|
||||
c.rank, c.score, c.chunk_id.0
|
||||
)));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(
|
||||
"RRF inputs ({} entries, {} ms fusion)",
|
||||
state.trace.rrf_inputs.len(),
|
||||
state.trace.timing.fusion_ms,
|
||||
),
|
||||
bold,
|
||||
)));
|
||||
for e in &state.trace.rrf_inputs {
|
||||
lines.push(Line::from(format!(
|
||||
" chunk={} lex={:?} vec={:?} fusion={:.4}",
|
||||
e.chunk_id.0, e.lexical_rank, e.vector_rank, e.fusion_score
|
||||
)));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!("Total: {} ms", state.trace.timing.total_ms),
|
||||
bold,
|
||||
)));
|
||||
|
||||
let block = Block::default()
|
||||
.title("Trace — Esc to close, j/k or ↑↓ to scroll")
|
||||
.borders(Borders::ALL);
|
||||
let p = Paragraph::new(lines)
|
||||
.block(block)
|
||||
.scroll((state.scroll, 0))
|
||||
.wrap(Wrap { trim: false });
|
||||
f.render_widget(p, area);
|
||||
}
|
||||
|
||||
/// Handle keys while popup is open. Returns true if the popup should close.
|
||||
pub fn handle_key_trace_popup(state: &mut TracePopupState, key: KeyEvent) -> bool {
|
||||
match key.code {
|
||||
KeyCode::Esc => true,
|
||||
KeyCode::Char('j') | KeyCode::Down => {
|
||||
state.scroll = state.scroll.saturating_add(1);
|
||||
false
|
||||
}
|
||||
KeyCode::Char('k') | KeyCode::Up => {
|
||||
state.scroll = state.scroll.saturating_sub(1);
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use kebab_core::TraceTiming;
|
||||
|
||||
fn dummy_state() -> TracePopupState {
|
||||
TracePopupState::new(SearchTrace {
|
||||
lexical: vec![],
|
||||
vector: vec![],
|
||||
rrf_inputs: vec![],
|
||||
timing: TraceTiming::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esc_closes() {
|
||||
let mut s = dummy_state();
|
||||
assert!(handle_key_trace_popup(
|
||||
&mut s,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn j_scrolls_down() {
|
||||
let mut s = dummy_state();
|
||||
assert!(!handle_key_trace_popup(
|
||||
&mut s,
|
||||
KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
|
||||
));
|
||||
assert_eq!(s.scroll, 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user