a73x

d163ae68

Render linked commits section in TUI issue detail

alex emery   2026-04-12 08:39

Mirrors the CLI renderer: --- Linked Commits --- section after
comments, with short SHA + subject + commit author + linked-by
metadata. Degrades gracefully when the commit isn't locally available.

diff --git a/src/tui/events.rs b/src/tui/events.rs
index 520a77c..7333293 100644
--- a/src/tui/events.rs
+++ b/src/tui/events.rs
@@ -21,7 +21,7 @@ pub(crate) fn run_loop(
    repo: &Repository,
) -> Result<(), Error> {
    loop {
        terminal.draw(|frame| ui(frame, app))?;
        terminal.draw(|frame| ui(frame, app, Some(repo)))?;

        if event::poll(Duration::from_millis(100))? {
            if let Event::Key(key) = event::read()? {
diff --git a/src/tui/mod.rs b/src/tui/mod.rs
index 6eea9d9..c96eaa8 100644
--- a/src/tui/mod.rs
+++ b/src/tui/mod.rs
@@ -296,7 +296,7 @@ mod tests {
    fn render_app(app: &mut App) -> Buffer {
        let backend = TestBackend::new(80, 24);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|frame| ui(frame, app)).unwrap();
        terminal.draw(|frame| ui(frame, app, None)).unwrap();
        terminal.backend().buffer().clone()
    }

@@ -960,7 +960,7 @@ mod tests {
        let mut app = make_app(3, 3);
        let backend = TestBackend::new(20, 10);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|frame| ui(frame, &mut app)).unwrap();
        terminal.draw(|frame| ui(frame, &mut app, None)).unwrap();
    }

    // ── Integration: full browse flow ────────────────────────────────────
diff --git a/src/tui/widgets.rs b/src/tui/widgets.rs
index 1f462d2..e1f2d0a 100644
--- a/src/tui/widgets.rs
+++ b/src/tui/widgets.rs
@@ -1,4 +1,4 @@
use git2::Oid;
use git2::{Oid, Repository};
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};

@@ -121,7 +121,7 @@ pub(crate) fn format_event_detail(oid: &Oid, event: &crate::event::Event) -> Str
    detail
}

pub(crate) fn ui(frame: &mut Frame, app: &mut App) {
pub(crate) fn ui(frame: &mut Frame, app: &mut App, repo: Option<&Repository>) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
@@ -139,7 +139,7 @@ pub(crate) fn ui(frame: &mut Frame, app: &mut App) {
        .split(main_area);

    render_list(frame, app, panes[0]);
    render_detail(frame, app, panes[1]);
    render_detail(frame, app, panes[1], repo);
    render_footer(frame, app, footer_area);
}

@@ -219,7 +219,7 @@ fn render_list(frame: &mut Frame, app: &mut App, area: Rect) {
    }
}

fn render_detail(frame: &mut Frame, app: &mut App, area: Rect) {
fn render_detail(frame: &mut Frame, app: &mut App, area: Rect, repo: Option<&Repository>) {
    let border_style = if app.pane == Pane::Detail {
        Style::default().fg(Color::Yellow)
    } else {
@@ -303,7 +303,7 @@ fn render_detail(frame: &mut Frame, app: &mut App, area: Rect) {
            let visible = app.visible_issues();
            let selected_idx = app.list_state.selected().unwrap_or(0);
            let content: Text = match visible.get(selected_idx) {
                Some(issue) => build_issue_detail(issue, &app.patches),
                Some(issue) => build_issue_detail(issue, &app.patches, repo),
                None => Text::raw("No matches for current filter."),
            };

@@ -342,7 +342,11 @@ fn render_detail(frame: &mut Frame, app: &mut App, area: Rect) {
    }
}

fn build_issue_detail(issue: &IssueState, patches: &[PatchState]) -> Text<'static> {
fn build_issue_detail(
    issue: &IssueState,
    patches: &[PatchState],
    repo: Option<&Repository>,
) -> Text<'static> {
    let status = issue.status.as_str();

    let mut lines: Vec<Line> = vec![
@@ -465,6 +469,43 @@ fn build_issue_detail(issue: &IssueState, patches: &[PatchState]) -> Text<'stati
        }
    }

    if !issue.linked_commits.is_empty() {
        lines.push(Line::raw(""));
        lines.push(Line::styled(
            "--- Linked Commits ---",
            Style::default()
                .fg(Color::Magenta)
                .add_modifier(Modifier::BOLD),
        ));
        for lc in &issue.linked_commits {
            let short_sha: String = lc.commit.chars().take(7).collect();
            let (subject, commit_author) = repo
                .and_then(|r| {
                    Oid::from_str(&lc.commit)
                        .ok()
                        .and_then(|oid| r.find_commit(oid).ok())
                        .map(|commit| {
                            let subject = commit.summary().unwrap_or("").to_string();
                            let author = commit.author().name().unwrap_or("unknown").to_string();
                            (subject, author)
                        })
                })
                .unwrap_or_else(|| (String::new(), String::new()));
            let line_text = if commit_author.is_empty() {
                format!(
                    "· linked {} (commit {} not in local repo) (linked by {}, {})",
                    short_sha, short_sha, lc.event_author.name, lc.event_timestamp
                )
            } else {
                format!(
                    "· linked {} \"{}\" by {} (linked by {}, {})",
                    short_sha, subject, commit_author, lc.event_author.name, lc.event_timestamp
                )
            };
            lines.push(Line::raw(line_text));
        }
    }

    Text::from(lines)
}