a73x

035c56d2

Fix issue checkout and add status messages in TUI

a73x   2026-03-21 08:18

- o on issue now checks out the closing commit when no linked patch
  exists (falls back from linked patch head → closed_by OID)
- g and o show a yellow status bar message when there's nothing to
  follow/checkout, instead of silently doing nothing
- Status message clears on next keypress

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git a/src/tui.rs b/src/tui.rs
index 88eb978..d0f92f2 100644
--- a/src/tui.rs
+++ b/src/tui.rs
@@ -41,6 +41,7 @@ struct App {
    pane: Pane,
    mode: ViewMode,
    show_all: bool,
    status_msg: Option<String>,
}

impl App {
@@ -59,6 +60,7 @@ impl App {
            pane: Pane::ItemList,
            mode: ViewMode::Details,
            show_all: false,
            status_msg: None,
        }
    }

@@ -132,7 +134,7 @@ impl App {
        self.pane = Pane::ItemList;
    }

    fn follow_link(&mut self) {
    fn follow_link(&mut self) -> bool {
        match self.tab {
            Tab::Issues => {
                // From an issue, jump to the first patch that fixes it
@@ -140,21 +142,20 @@ impl App {
                if let Some(idx) = self.list_state.selected() {
                    if let Some(issue) = visible.get(idx) {
                        let issue_id = issue.id.clone();
                        // Find first patch with fixes == this issue
                        let target = self
                            .patches
                            .iter()
                            .enumerate()
                            .find(|(_, p)| p.fixes.as_deref() == Some(&issue_id));
                        if let Some((patch_idx, _)) = target {
                            // Need to find index in visible patches
                            let visible_patches = self.visible_patches();
                            let visible_idx = visible_patches
                                .iter()
                                .position(|p| p.fixes.as_deref() == Some(&issue_id));
                            // If the patch isn't visible (filtered), toggle show_all
                            if visible_idx.is_none() && !self.show_all {
                                self.show_all = true;
                            if !self.show_all {
                                let visible_patches = self.visible_patches();
                                if !visible_patches
                                    .iter()
                                    .any(|p| p.fixes.as_deref() == Some(&issue_id))
                                {
                                    self.show_all = true;
                                }
                            }
                            let visible_patches = self.visible_patches();
                            if let Some(vi) = visible_patches
@@ -165,10 +166,12 @@ impl App {
                                self.list_state.select(Some(vi));
                                self.scroll = 0;
                                self.mode = ViewMode::Details;
                                return true;
                            }
                        }
                    }
                }
                return false;
            }
            Tab::Patches => {
                // From a patch, jump to the linked issue (fixes field)
@@ -177,11 +180,11 @@ impl App {
                    if let Some(patch) = visible.get(idx) {
                        if let Some(ref fixes_id) = patch.fixes {
                            let fixes_id = fixes_id.clone();
                            let visible_issues = self.visible_issues();
                            let visible_idx =
                                visible_issues.iter().position(|i| i.id == fixes_id);
                            if visible_idx.is_none() && !self.show_all {
                                self.show_all = true;
                            if !self.show_all {
                                let visible_issues = self.visible_issues();
                                if !visible_issues.iter().any(|i| i.id == fixes_id) {
                                    self.show_all = true;
                                }
                            }
                            let visible_issues = self.visible_issues();
                            if let Some(vi) =
@@ -191,10 +194,12 @@ impl App {
                                self.list_state.select(Some(vi));
                                self.scroll = 0;
                                self.mode = ViewMode::Details;
                                return true;
                            }
                        }
                    }
                }
                return false;
            }
        }
    }
@@ -267,6 +272,7 @@ fn run_loop(

        if event::poll(Duration::from_millis(100))? {
            if let Event::Key(key) = event::read()? {
                app.status_msg = None; // clear status on any keypress
                match key.code {
                    KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
                    KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
@@ -312,8 +318,9 @@ fn run_loop(
                            .select(if count > 0 { Some(0) } else { None });
                    }
                    KeyCode::Char('g') => {
                        // Follow cross-references between issues and patches
                        app.follow_link();
                        if !app.follow_link() {
                            app.status_msg = Some("No linked item to follow".to_string());
                        }
                    }
                    KeyCode::Char('o') => {
                        // Check out the relevant commit for local browsing
@@ -326,16 +333,19 @@ fn run_loop(
                                    .map(|p| p.head_commit.clone())
                            }
                            Tab::Issues => {
                                // Find linked patch's head commit
                                // Find linked patch's head commit, or fall back to closing commit
                                let visible = app.visible_issues();
                                app.list_state
                                    .selected()
                                    .and_then(|idx| visible.get(idx))
                                    .and_then(|issue| {
                                        // Try linked patch first
                                        app.patches
                                            .iter()
                                            .find(|p| p.fixes.as_deref() == Some(&issue.id))
                                            .map(|p| p.head_commit.clone())
                                            // Fall back to closing commit
                                            .or_else(|| issue.closed_by.map(|oid| oid.to_string()))
                                    })
                            }
                        };
@@ -359,6 +369,9 @@ fn run_loop(
                                }
                            }
                            return Ok(());
                        } else {
                            app.status_msg =
                                Some("No linked patch to check out".to_string());
                        }
                    }
                    KeyCode::Char('r') => {
@@ -969,10 +982,19 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
    } else {
        "a:show all"
    };
    let text = format!(
        " 1:issues  2:patches  j/k:navigate  Tab:pane  {}{}  g:follow  o:checkout  r:refresh  q:quit",
        filter_hint, mode_hint
    );
    let para = Paragraph::new(text).style(Style::default().bg(Color::DarkGray).fg(Color::White));
    let text = if let Some(ref msg) = app.status_msg {
        format!(" {}", msg)
    } else {
        format!(
            " 1:issues  2:patches  j/k:navigate  Tab:pane  {}{}  g:follow  o:checkout  r:refresh  q:quit",
            filter_hint, mode_hint
        )
    };
    let style = if app.status_msg.is_some() {
        Style::default().bg(Color::Yellow).fg(Color::Black)
    } else {
        Style::default().bg(Color::DarkGray).fg(Color::White)
    };
    let para = Paragraph::new(text).style(style);
    frame.render_widget(para, area);
}