From 6d5b55ad741ef7de2e191df50be0ee6d6ae03853 Mon Sep 17 00:00:00 2001 From: Shunsuke Hayashi Date: Wed, 21 Jan 2026 05:08:11 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20TUI=E3=82=A6=E3=82=A3=E3=82=B8=E3=82=A7?= =?UTF-8?q?=E3=83=83=E3=83=88=E3=81=AE=E8=A1=A8=E7=A4=BA=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - diff表示の修正 - historyリストの修正 - markdown表示の修正 Co-Authored-By: Claude --- crates/miyabi-tui/src/ui/widgets/diff.rs | 74 ++++++++++++++++--- .../miyabi-tui/src/ui/widgets/history_list.rs | 56 ++++++++++++-- crates/miyabi-tui/src/ui/widgets/markdown.rs | 57 +++++++++++++- 3 files changed, 169 insertions(+), 18 deletions(-) diff --git a/crates/miyabi-tui/src/ui/widgets/diff.rs b/crates/miyabi-tui/src/ui/widgets/diff.rs index 206f5cf..f3dd7e2 100644 --- a/crates/miyabi-tui/src/ui/widgets/diff.rs +++ b/crates/miyabi-tui/src/ui/widgets/diff.rs @@ -1,14 +1,70 @@ -//! Diff viewer widget placeholder. +//! Diff viewer widget for displaying file changes. //! -//! This will eventually wrap `diff_render.rs` for reuse across overlays. +//! Wraps `diff_render.rs` for reuse across overlays with scrolling support. -use ratatui::layout::Rect; -use ratatui::Frame; +use ratatui::{ + layout::Rect, + widgets::{Block, Borders, Paragraph, Wrap}, + Frame, +}; -use crate::diff_render::FileDiff; +use crate::diff_render::{DiffRender, FileDiff}; -/// Render a diff within the given area. -pub fn render_diff_widget(frame: &mut Frame, area: Rect, diff: &FileDiff) { - let _ = (frame, area, diff); - // TODO: integrate syntax highlighting and line numbers. +/// Properties for rendering a diff widget +pub struct DiffWidgetProps { + /// The file diff to display + pub diff: FileDiff, + /// Current scroll offset + pub scroll: u16, + /// Optional title for the block + pub title: Option, +} + +/// Render a diff within the given area with scrolling support. +pub fn render_diff_widget(frame: &mut Frame, area: Rect, diff: &FileDiff) { + render_diff_widget_with_scroll(frame, area, diff, 0, None); +} + +/// Render a diff with scroll offset and optional title. +pub fn render_diff_widget_with_scroll( + frame: &mut Frame, + area: Rect, + diff: &FileDiff, + scroll: u16, + title: Option<&str>, +) { + // Create a DiffRender with the single file + let mut renderer = DiffRender::new(); + renderer.files.push(diff.clone()); + + // Get rendered lines + let lines = renderer.render(); + + // Create block with optional title + let block = if let Some(t) = title { + Block::default() + .borders(Borders::ALL) + .title(t.to_string()) + } else { + Block::default() + .borders(Borders::ALL) + .title(format!("{} → {}", diff.old_path, diff.new_path)) + }; + + // Create paragraph with scroll + let paragraph = Paragraph::new(lines) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((scroll, 0)); + + frame.render_widget(paragraph, area); +} + +/// Calculate the total number of lines in a diff for scroll bounds +pub fn diff_line_count(diff: &FileDiff) -> usize { + let mut count = 1; // File header + for hunk in &diff.hunks { + count += hunk.lines.len(); + } + count } diff --git a/crates/miyabi-tui/src/ui/widgets/history_list.rs b/crates/miyabi-tui/src/ui/widgets/history_list.rs index 26d1866..e9a99c5 100644 --- a/crates/miyabi-tui/src/ui/widgets/history_list.rs +++ b/crates/miyabi-tui/src/ui/widgets/history_list.rs @@ -3,12 +3,15 @@ //! Intended to wrap `HistoryCell` rendering with consistent padding and theming. use ratatui::{ - layout::Rect, - widgets::{Block, Borders}, + layout::{Alignment, Rect}, + style::Style, + text::Line, + widgets::{Block, Borders, List, ListItem, Paragraph}, Frame, }; use crate::history_cell::HistoryCell; +use crate::ui::colors; /// Properties required to render the history list. pub struct HistoryListProps<'a> { @@ -21,10 +24,53 @@ pub struct HistoryList; impl HistoryList { pub fn render(frame: &mut Frame, area: Rect, props: HistoryListProps<'_>) { - let block = Block::default().borders(Borders::ALL); + let block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(colors::BORDER)) + .title("History"); + let inner = block.inner(area); frame.render_widget(block, area); - let _ = props; - // TODO: integrate with virtualized list + markdown rendering. + if inner.height == 0 || inner.width == 0 { + return; + } + + if props.items.is_empty() { + let empty = Paragraph::new("No history yet") + .style(Style::default().fg(colors::COMMENT)) + .alignment(Alignment::Center); + frame.render_widget(empty, inner); + return; + } + + let scroll = props.scroll as usize; + let max_lines = inner.height as usize; + let mut visible_lines: Vec = Vec::with_capacity(max_lines); + let mut line_index = 0usize; + + 'outer: for (idx, cell) in props.items.iter().enumerate() { + let mut rendered = cell.render(inner.width); + if idx + 1 < props.items.len() { + rendered.push(Line::from("")); + } + + for line in rendered { + if line_index >= scroll { + visible_lines.push(line); + if visible_lines.len() >= max_lines { + break 'outer; + } + } + line_index += 1; + } + } + + let list_items: Vec = visible_lines + .into_iter() + .map(ListItem::new) + .collect(); + + let list = List::new(list_items).style(Style::default().fg(colors::FG)); + frame.render_widget(list, inner); } } diff --git a/crates/miyabi-tui/src/ui/widgets/markdown.rs b/crates/miyabi-tui/src/ui/widgets/markdown.rs index d2ceb8f..d121a2f 100644 --- a/crates/miyabi-tui/src/ui/widgets/markdown.rs +++ b/crates/miyabi-tui/src/ui/widgets/markdown.rs @@ -3,13 +3,62 @@ //! This file is the landing zone for `markdown_stream` integration to keep //! rendering logic out of `history_cell.rs`. -use ratatui::layout::Rect; -use ratatui::Frame; +use ratatui::{ + layout::Rect, + style::{Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph, Wrap}, + Frame, +}; +use crate::markdown_render::MarkdownRenderer; use crate::markdown_stream::MarkdownStream; +use crate::ui::colors; /// Render a markdown stream into the provided frame area. pub fn render_stream(frame: &mut Frame, area: Rect, stream: &MarkdownStream) { - let _ = (frame, area, stream); - // TODO: reuse MarkdownRenderer and add code block scroll support. + let block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(colors::BORDER)) + .title("Markdown"); + + let inner_height = area.height.saturating_sub(2) as usize; + + let mut lines = if stream.is_empty() { + vec![Line::from(Span::styled( + "Waiting for response...", + Style::default().fg(colors::COMMENT), + ))] + } else { + let renderer = MarkdownRenderer::default(); + renderer.render(stream.content()) + }; + + if stream.is_streaming() { + if let Some(last) = lines.last_mut() { + last.spans.push(Span::styled( + "▌", + Style::default() + .fg(colors::FG) + .add_modifier(Modifier::SLOW_BLINK), + )); + } else { + lines.push(Line::from(Span::styled( + "▌", + Style::default() + .fg(colors::FG) + .add_modifier(Modifier::SLOW_BLINK), + ))); + } + } + + let max_offset = lines.len().saturating_sub(inner_height.max(1)); + let offset = stream.scroll_offset().min(max_offset); + + let paragraph = Paragraph::new(lines) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((offset as u16, 0)); + + frame.render_widget(paragraph, area); }