This commit is contained in:
Ahmed Ibrahim
2025-08-22 10:52:30 -07:00
parent d57014ec50
commit 3f4826d7fd
2 changed files with 26 additions and 254 deletions

View File

@@ -12,9 +12,6 @@ use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use crossterm::execute;
use crossterm::terminal::EnterAlternateScreen;
use crossterm::terminal::LeaveAlternateScreen;
use crossterm::terminal::supports_keyboard_enhancement;
use ratatui::layout::Rect;
use ratatui::text::Line;
@@ -28,33 +25,33 @@ use tokio::select;
use tokio::sync::mpsc::unbounded_channel;
pub(crate) struct App {
server: Arc<ConversationManager>,
app_event_tx: AppEventSender,
chat_widget: ChatWidget,
pub(crate) server: Arc<ConversationManager>,
pub(crate) app_event_tx: AppEventSender,
pub(crate) chat_widget: ChatWidget,
/// Config is stored here so we can recreate ChatWidgets as needed.
config: Config,
pub(crate) config: Config,
file_search: FileSearchManager,
pub(crate) file_search: FileSearchManager,
transcript_lines: Vec<Line<'static>>,
pub(crate) transcript_lines: Vec<Line<'static>>,
// Transcript overlay state
transcript_overlay: Option<TranscriptApp>,
pub(crate) transcript_overlay: Option<TranscriptApp>,
// If true, overlay is opened as an Esc-backtrack preview.
transcript_overlay_is_backtrack: bool,
deferred_history_lines: Vec<Line<'static>>,
transcript_saved_viewport: Option<Rect>,
pub(crate) transcript_overlay_is_backtrack: bool,
pub(crate) deferred_history_lines: Vec<Line<'static>>,
pub(crate) transcript_saved_viewport: Option<Rect>,
enhanced_keys_supported: bool,
pub(crate) enhanced_keys_supported: bool,
/// Controls the animation thread that sends CommitTick events.
commit_anim_running: Arc<AtomicBool>,
pub(crate) commit_anim_running: Arc<AtomicBool>,
// Esc-backtracking state
esc_backtrack_primed: bool,
esc_backtrack_base: Option<uuid::Uuid>,
esc_backtrack_count: usize,
pub(crate) esc_backtrack_primed: bool,
pub(crate) esc_backtrack_base: Option<uuid::Uuid>,
pub(crate) esc_backtrack_count: usize,
}
impl App {
@@ -125,73 +122,7 @@ impl App {
event: TuiEvent,
) -> Result<bool> {
if self.transcript_overlay.is_some() {
// Intercept Esc/Enter when overlay is a backtrack preview.
let mut handled = false;
if self.transcript_overlay_is_backtrack {
match event {
TuiEvent::Key(KeyEvent { code: KeyCode::Esc, kind: KeyEventKind::Press | KeyEventKind::Repeat, .. }) => {
if self.esc_backtrack_base.is_some() {
self.esc_backtrack_count = self.esc_backtrack_count.saturating_add(1);
let header_idx =
crate::backtrack_helpers::find_nth_last_user_header_index(
&self.transcript_lines,
self.esc_backtrack_count,
);
let offset = header_idx.map(|idx| {
crate::backtrack_helpers::wrapped_offset_before(
&self.transcript_lines,
idx,
tui.terminal.viewport_area.width,
)
});
let hl = crate::backtrack_helpers::highlight_range_for_nth_last_user(
&self.transcript_lines,
self.esc_backtrack_count,
);
if let Some(overlay) = &mut self.transcript_overlay {
if let Some(off) = offset { overlay.scroll_offset = off; }
overlay.set_highlight_range(hl);
}
tui.frame_requester().schedule_frame();
handled = true;
}
}
TuiEvent::Key(KeyEvent { code: KeyCode::Enter, kind: KeyEventKind::Press, .. }) => {
// Confirm the backtrack: close overlay, fork, and prefill.
let base = self.esc_backtrack_base;
let count = self.esc_backtrack_count;
self.close_transcript_overlay(tui);
if let Some(base_id) = base {
if count > 0 {
if let Err(e) = self.fork_and_render_backtrack(tui, base_id, count).await {
tracing::error!("Backtrack confirm failed: {e:#}");
}
}
}
// Reset backtrack state after confirming.
self.esc_backtrack_primed = false;
self.esc_backtrack_base = None;
self.esc_backtrack_count = 0;
handled = true;
}
_ => {}
}
}
// Forward to overlay if not handled
if !handled {
if let Some(overlay) = &mut self.transcript_overlay {
overlay.handle_event(tui, event)?;
if overlay.is_done {
self.close_transcript_overlay(tui);
if self.transcript_overlay_is_backtrack {
self.esc_backtrack_primed = false;
self.esc_backtrack_base = None;
self.esc_backtrack_count = 0;
}
}
}
}
tui.frame_requester().schedule_frame();
let _ = self.handle_backtrack_overlay_event(tui, event).await?;
} else {
match event {
TuiEvent::Key(key_event) => {
@@ -355,95 +286,20 @@ impl App {
} if self.chat_widget.composer_is_empty() => {
self.app_event_tx.send(AppEvent::ExitRequest);
}
KeyEvent {
code: KeyCode::Char('t'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
} => {
// Enter alternate screen and set viewport to full size.
let _ = execute!(tui.terminal.backend_mut(), EnterAlternateScreen);
if let Ok(size) = tui.terminal.size() {
self.transcript_saved_viewport = Some(tui.terminal.viewport_area);
tui.terminal
.set_viewport_area(Rect::new(0, 0, size.width, size.height));
let _ = tui.terminal.clear();
}
self.transcript_overlay = Some(TranscriptApp::new(self.transcript_lines.clone()));
tui.frame_requester().schedule_frame();
KeyEvent { code: KeyCode::Char('t'), modifiers: crossterm::event::KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. } => {
self.open_transcript_overlay(tui);
}
KeyEvent {
code: KeyCode::Esc,
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
} => {
// Only handle backtracking when composer is empty to avoid clobbering edits.
if self.chat_widget.composer_is_empty() {
if !self.esc_backtrack_primed {
// Arm backtracking and record base conversation.
self.esc_backtrack_primed = true;
self.esc_backtrack_count = 0;
self.esc_backtrack_base = self.chat_widget.session_id();
} else if self.transcript_overlay.is_none() {
// Open transcript overlay in backtrack preview mode and jump to the target message.
self.open_transcript_overlay(tui);
self.transcript_overlay_is_backtrack = true;
self.esc_backtrack_count = self.esc_backtrack_count.saturating_add(1);
let header_idx =
crate::backtrack_helpers::find_nth_last_user_header_index(
&self.transcript_lines,
self.esc_backtrack_count,
);
let offset = header_idx.map(|idx| {
crate::backtrack_helpers::wrapped_offset_before(
&self.transcript_lines,
idx,
tui.terminal.viewport_area.width,
)
});
let hl = crate::backtrack_helpers::highlight_range_for_nth_last_user(
&self.transcript_lines,
self.esc_backtrack_count,
);
if let Some(overlay) = &mut self.transcript_overlay {
if let Some(off) = offset { overlay.scroll_offset = off; }
overlay.set_highlight_range(hl);
}
} else if self.transcript_overlay_is_backtrack {
// Already previewing: step to the next older message.
self.esc_backtrack_count = self.esc_backtrack_count.saturating_add(1);
let header_idx =
crate::backtrack_helpers::find_nth_last_user_header_index(
&self.transcript_lines,
self.esc_backtrack_count,
);
let offset = header_idx.map(|idx| {
crate::backtrack_helpers::wrapped_offset_before(
&self.transcript_lines,
idx,
tui.terminal.viewport_area.width,
)
});
let hl = crate::backtrack_helpers::highlight_range_for_nth_last_user(
&self.transcript_lines,
self.esc_backtrack_count,
);
if let Some(overlay) = &mut self.transcript_overlay {
if let Some(off) = offset { overlay.scroll_offset = off; }
overlay.set_highlight_range(hl);
}
}
}
KeyEvent { code: KeyCode::Esc, kind: KeyEventKind::Press | KeyEventKind::Repeat, .. } => {
self.handle_backtrack_esc_key(tui);
}
// Enter confirms backtrack when primed + count > 0. Otherwise pass to widget.
KeyEvent { code: KeyCode::Enter, kind: KeyEventKind::Press, .. }
if self.esc_backtrack_primed && self.esc_backtrack_count > 0 && self.chat_widget.composer_is_empty() =>
{
if let Some(base_id) = self.esc_backtrack_base {
if let Err(e) = self.fork_and_render_backtrack(tui, base_id, self.esc_backtrack_count).await {
tracing::error!("Backtrack confirm failed: {e:#}");
}
if let Some(base_id) = self.esc_backtrack_base
&& let Err(e) = self.fork_and_render_backtrack(tui, base_id, self.esc_backtrack_count).await
{
tracing::error!("Backtrack confirm failed: {e:#}");
}
// Reset backtrack state after confirming.
self.esc_backtrack_primed = false;
@@ -462,90 +318,5 @@ impl App {
};
}
/// Re-render the full transcript into the terminal scrollback in one call.
/// Useful when switching sessions to ensure prior history remains visible.
pub(crate) fn render_transcript_once(&mut self, tui: &mut tui::Tui) {
if !self.transcript_lines.is_empty() {
tui.insert_history_lines(self.transcript_lines.clone());
}
}
fn open_transcript_overlay(&mut self, tui: &mut tui::Tui) {
// Enter alternate screen and set viewport to full size.
let _ = execute!(tui.terminal.backend_mut(), EnterAlternateScreen);
if let Ok(size) = tui.terminal.size() {
self.transcript_saved_viewport = Some(tui.terminal.viewport_area);
tui.terminal
.set_viewport_area(Rect::new(0, 0, size.width, size.height));
let _ = tui.terminal.clear();
}
self.transcript_overlay = Some(TranscriptApp::new(self.transcript_lines.clone()));
tui.frame_requester().schedule_frame();
}
fn close_transcript_overlay(&mut self, tui: &mut tui::Tui) {
// Exit alternate screen and restore viewport.
let _ = execute!(tui.terminal.backend_mut(), LeaveAlternateScreen);
if let Some(saved) = self.transcript_saved_viewport.take() {
tui.terminal.set_viewport_area(saved);
}
if !self.deferred_history_lines.is_empty() {
let lines = std::mem::take(&mut self.deferred_history_lines);
tui.insert_history_lines(lines);
}
self.transcript_overlay = None;
self.transcript_overlay_is_backtrack = false;
}
async fn fork_and_render_backtrack(
&mut self,
tui: &mut tui::Tui,
base_id: uuid::Uuid,
drop_last_messages: usize,
) -> color_eyre::eyre::Result<()> {
// Compute the text to prefill by extracting the N-th last user message
// from the UI transcript lines already rendered.
let prefill = crate::backtrack_helpers::nth_last_user_text(
&self.transcript_lines,
drop_last_messages,
);
// Fork conversation with the requested drop.
let fork = self
.server
.fork_conversation(base_id, drop_last_messages, self.config.clone())
.await?;
// Replace chat widget with one attached to the new conversation.
self.chat_widget = ChatWidget::new_from_existing(
self.config.clone(),
fork.conversation,
fork.session_configured,
tui.frame_requester(),
self.app_event_tx.clone(),
self.enhanced_keys_supported,
);
// Trim transcript to preserve only content up to the selected user message.
if let Some(cut_idx) = crate::backtrack_helpers::find_nth_last_user_header_index(
&self.transcript_lines,
drop_last_messages,
) {
self.transcript_lines.truncate(cut_idx);
} else {
self.transcript_lines.clear();
}
let _ = tui.terminal.clear();
self.render_transcript_once(tui);
// Prefill the composer with the dropped user message text, if any.
if let Some(text) = prefill {
if !text.is_empty() {
self.chat_widget.insert_str(&text);
}
}
tui.frame_requester().schedule_frame();
Ok(())
}
// (moved helper functions to backtrack_helpers.rs)
// Backtrack helpers moved to app_backtrack.rs and backtrack_helpers.rs
}

View File

@@ -24,6 +24,7 @@ use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
mod app;
mod app_backtrack;
mod app_event;
mod app_event_sender;
mod backtrack_helpers;