fix: correct the Repo tab detail view's stale-fetch and refresh handling
Two bugs in the Repo tab detail view. First, RepoUpdate::Detail carried no identity for which issue or PR it was fetched for, so a slow reply filled whatever detail happened to be open: open issue #12, press Esc, open PR #7, and #12's body could render under the #7 title. Detail fetches now carry a RepoTarget (section plus number) and a reply is applied only when it matches the open view, otherwise dropped. Second, the footer advertised "r refresh" but pressing r in the detail view called refresh_repo, which reloads the lists and clears repo_detail, so the view the user was reading vanished. r now re-fetches the item on screen.
paydii committed
Jul 11, 2026 at 15:10 UTC
cecfb9379225cda7d1f1b379ef50674f6bd946ab
1 file changed
+45
-9
src/chat.rs
+45
-9
@@ -686,8 +686,20 @@ mod tui {
686
}
687
}
688
689
+ /// Identifies which list row a detail fetch belongs to, so a slow reply
690
+ /// can be matched against the currently-open detail and dropped if the
691
+ /// user has since navigated elsewhere.
692
+ #[derive(Clone, Copy, PartialEq, Eq)]
693
+ struct RepoTarget {
694
+ section: RepoSection,
695
+ number: u64,
696
+ }
697
+
698
/// The full-tab detail view opened with Enter on a list row.
699
struct RepoDetail {
700
+ /// Which issue/PR this view is showing; matched against arriving
701
+ /// `RepoUpdate::Detail` replies so a stale fetch can't fill it.
702
+ target: RepoTarget,
703
title: String,
704
/// `None` while the `get_issue` / `get_pull_request` fetch runs.
705
text: Option<String>,
@@ -699,7 +711,7 @@ mod tui {
711
enum RepoUpdate {
712
Issues(Result<Vec<RepoItem>, String>),
713
PullRequests(Result<Vec<RepoItem>, String>),
702
- Detail(String),
714
+ Detail { target: RepoTarget, text: String },
715
}
716
717
// ── App state ─────────────────────────────────────────────────────────────
@@ -1514,7 +1526,12 @@ mod tui {
1526
};
1527
1528
let number = item.number;
1529
+ let target = RepoTarget {
1530
+ section: app.repo_section,
1531
+ number,
1532
+ };
1533
app.repo_detail = Some(RepoDetail {
1534
+ target,
1535
title: format!("{label} #{number} — {}", item.title),
1536
text: None,
1537
scroll: 0,
@@ -1527,7 +1544,10 @@ mod tui {
1544
let args = serde_json::json!({ "repo": repo, "number": number }).to_string();
1545
tokio::spawn(async move {
1546
let text = crate::mcp::call_tool(&tool, &args).await;
1530
- let _ = tx.send(RepoUpdate::Detail(super::format_repo_detail(&text)));
1547
+ let _ = tx.send(RepoUpdate::Detail {
1548
+ target,
1549
+ text: super::format_repo_detail(&text),
1550
+ });
1551
});
1552
}
1553
@@ -1599,11 +1619,22 @@ mod tui {
1619
Tab::Repo => {
1620
// Detail view open: Up/Down scroll it; Esc (handled in the
1621
// event loop) closes it back to the list.
1602
- if let Some(detail) = app.repo_detail.as_mut() {
1622
+ if app.repo_detail.is_some() {
1623
match key.code {
1604
- KeyCode::Up => detail.scroll = detail.scroll.saturating_sub(1),
1605
- KeyCode::Down => detail.scroll = detail.scroll.saturating_add(1),
1606
- KeyCode::Char('r') => refresh_repo(app),
1624
+ KeyCode::Up => {
1625
+ if let Some(detail) = app.repo_detail.as_mut() {
1626
+ detail.scroll = detail.scroll.saturating_sub(1);
1627
+ }
1628
+ }
1629
+ KeyCode::Down => {
1630
+ if let Some(detail) = app.repo_detail.as_mut() {
1631
+ detail.scroll = detail.scroll.saturating_add(1);
1632
+ }
1633
+ }
1634
+ // "r refresh" (as the footer advertises) re-fetches the
1635
+ // item on screen, rather than closing the view back to
1636
+ // the list as refresh_repo would.
1637
+ KeyCode::Char('r') => open_repo_detail(app),
1638
_ => {}
1639
}
1640
return;
@@ -3505,9 +3536,14 @@ mod tui {
3536
.repo_pr_index
3537
.min(app.repo_prs.len().saturating_sub(1));
3538
}
3508
- Some(RepoUpdate::Detail(text)) => {
3509
- // Ignored if the user already closed the view.
3510
- if let Some(detail) = app.repo_detail.as_mut() {
3539
+ Some(RepoUpdate::Detail { target, text }) => {
3540
+ // Apply only to the matching open view: a reply for
3541
+ // a detail the user has since closed or navigated
3542
+ // away from is dropped, never shown under the wrong
3543
+ // title.
3544
+ if let Some(detail) = app.repo_detail.as_mut()
3545
+ && detail.target == target
3546
+ {
3547
detail.text = Some(text);
3548
}
3549
}