From b649a7ab8de6488c1341e94c37d032c07d5b3f13 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 26 Mar 2026 19:19:47 -0700 Subject: feat: Use eye-declare for more performant and flexible AI TUI (#3343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR replaces the mess of custom rendering code in Atuin AI with [eye-declare](https://github.com/BinaryMuse/eye-declare), and updates the TUI to feel more terminal-native: output appears inline and persists in scrollback, so you can scroll up and look at previous conversations for reference. The "review" state — which used to exist between the LLM generating a response and the user either executing or following up — has been removed; just start typing to follow up with the LLM, or press `enter` at the empty input box to execute the suggested command. image --- crates/atuin-ai/src/tui/components/markdown.rs | 213 +++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 crates/atuin-ai/src/tui/components/markdown.rs (limited to 'crates/atuin-ai/src/tui/components/markdown.rs') diff --git a/crates/atuin-ai/src/tui/components/markdown.rs b/crates/atuin-ai/src/tui/components/markdown.rs new file mode 100644 index 00000000..e1551a7f --- /dev/null +++ b/crates/atuin-ai/src/tui/components/markdown.rs @@ -0,0 +1,213 @@ +//! Markdown rendering component using pulldown-cmark. +//! +//! More robust than eye-declare's built-in Markdown component: +//! uses a proper CommonMark parser rather than line-by-line regex. + +use eye_declare::Component; +use pulldown_cmark::{Event, Parser, Tag, TagEnd}; +use ratatui_core::{ + buffer::Buffer, + layout::Rect, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, + widgets::Widget, +}; +use ratatui_widgets::paragraph::{Paragraph, Wrap}; + +/// A markdown rendering component backed by pulldown-cmark. +#[derive(Default)] +pub struct Markdown { + pub source: String, +} + +impl Markdown { + pub fn new(source: impl Into) -> Self { + Self { + source: source.into(), + } + } +} + +/// Style configuration for markdown rendering. +pub struct MarkdownStyles { + pub base: Style, + pub code_inline: Style, + pub code_block: Style, + pub bold: Style, + pub italic: Style, + pub heading: Style, +} + +impl MarkdownStyles { + pub fn new() -> Self { + let base = Style::default(); + Self { + base, + code_inline: Style::default().fg(Color::Yellow), + code_block: Style::default().fg(Color::Green), + bold: base.add_modifier(Modifier::BOLD), + italic: base.add_modifier(Modifier::ITALIC), + heading: Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + } + } +} + +impl Default for MarkdownStyles { + fn default() -> Self { + Self::new() + } +} + +impl Component for Markdown { + type State = MarkdownStyles; + + fn render(&self, area: Rect, buf: &mut Buffer, state: &Self::State) { + if self.source.is_empty() || area.width == 0 || area.height == 0 { + return; + } + let text = parse_markdown(&self.source, state); + Paragraph::new(text) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn desired_height(&self, width: u16, state: &Self::State) -> u16 { + if self.source.is_empty() || width == 0 { + return 0; + } + let text = parse_markdown(&self.source, state); + Paragraph::new(text) + .wrap(Wrap { trim: false }) + .line_count(width) as u16 + } + + fn initial_state(&self) -> Option { + Some(MarkdownStyles::new()) + } +} + +/// Parse markdown source into styled ratatui Text using pulldown-cmark. +fn parse_markdown<'a>(source: &'a str, styles: &'a MarkdownStyles) -> Text<'static> { + let parser = Parser::new(source); + let mut lines: Vec>> = vec![Vec::new()]; + let mut current_line = 0; + + let mut style_stack: Vec