fix(tui): preserve hidden github pr setup items

Keep configured `github-pr` status line and terminal title entries when
`gh` is unavailable and the setup picker hides the option. This avoids
silently deleting the setting when users confirm unrelated picker edits.
This commit is contained in:
Felipe Coury
2026-04-11 14:09:39 -03:00
parent a6ea6a106d
commit 426304645a
2 changed files with 144 additions and 2 deletions

View File

@@ -173,6 +173,43 @@ fn selectable_status_line_items(github_pr_available: bool) -> Vec<StatusLineItem
items
}
fn hidden_configured_status_line_items(
status_line_items: Option<&[String]>,
github_pr_available: bool,
) -> Vec<(usize, StatusLineItem)> {
if github_pr_available {
return Vec::new();
}
let mut hidden_items = Vec::new();
for (index, id) in status_line_items.into_iter().flatten().enumerate() {
let Ok(item) = id.parse::<StatusLineItem>() else {
continue;
};
if item == StatusLineItem::GithubPr
&& !hidden_items
.iter()
.any(|(_, hidden_item)| hidden_item == &item)
{
hidden_items.push((index, item));
}
}
hidden_items
}
fn restore_hidden_status_line_items(
mut items: Vec<StatusLineItem>,
hidden_items: &[(usize, StatusLineItem)],
) -> Vec<StatusLineItem> {
for (index, item) in hidden_items {
if items.contains(item) {
continue;
}
items.insert((*index).min(items.len()), item.clone());
}
items
}
/// Runtime values used to preview the current status-line selection.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct StatusLinePreviewData {
@@ -234,6 +271,8 @@ impl StatusLineSetupView {
) -> Self {
let mut used_ids = HashSet::new();
let mut items = Vec::new();
let hidden_configured_items =
hidden_configured_status_line_items(status_line_items, github_pr_available);
if let Some(selected_items) = status_line_items.as_ref() {
for id in *selected_items {
@@ -271,12 +310,13 @@ impl StatusLineSetupView {
.items(items)
.enable_ordering()
.on_preview(move |items| preview_data.line_for_items(items))
.on_confirm(|ids, app_event| {
.on_confirm(move |ids, app_event| {
let items = ids
.iter()
.map(|id| id.parse::<StatusLineItem>())
.collect::<Result<Vec<_>, _>>()
.unwrap_or_default();
let items = restore_hidden_status_line_items(items, &hidden_configured_items);
app_event.send(AppEvent::StatusLineSetup { items });
})
.on_cancel(|app_event| {
@@ -326,6 +366,9 @@ impl Renderable for StatusLineSetupView {
mod tests {
use super::*;
use crate::app_event_sender::AppEventSender;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;
use ratatui::buffer::Buffer;
@@ -476,6 +519,35 @@ mod tests {
assert!(!rendered.contains("PR #123"));
}
#[tokio::test]
async fn confirm_preserves_hidden_github_pr_when_gh_unavailable() {
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
let mut view = StatusLineSetupView::new(
Some(&[
StatusLineItem::ModelName.to_string(),
StatusLineItem::GithubPr.to_string(),
StatusLineItem::CurrentDir.to_string(),
]),
StatusLinePreviewData::default(),
/*github_pr_available*/ false,
AppEventSender::new(tx_raw),
);
view.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let Some(AppEvent::StatusLineSetup { items }) = rx.recv().await else {
panic!("expected status line setup event");
};
assert_eq!(
items,
vec![
StatusLineItem::ModelName,
StatusLineItem::GithubPr,
StatusLineItem::CurrentDir,
]
);
}
fn render_lines(view: &StatusLineSetupView, width: u16) -> String {
let height = view.desired_height(width);
let area = Rect::new(0, 0, width, height);

View File

@@ -117,6 +117,43 @@ fn terminal_title_item_selectable(item: TerminalTitleItem, github_pr_available:
item != TerminalTitleItem::GithubPr || github_pr_available
}
fn hidden_configured_terminal_title_items(
title_items: Option<&[String]>,
github_pr_available: bool,
) -> Vec<(usize, TerminalTitleItem)> {
if github_pr_available {
return Vec::new();
}
let mut hidden_items = Vec::new();
for (index, id) in title_items.into_iter().flatten().enumerate() {
let Ok(item) = id.parse::<TerminalTitleItem>() else {
continue;
};
if item == TerminalTitleItem::GithubPr
&& !hidden_items
.iter()
.any(|(_, hidden_item)| hidden_item == &item)
{
hidden_items.push((index, item));
}
}
hidden_items
}
fn restore_hidden_terminal_title_items(
mut items: Vec<TerminalTitleItem>,
hidden_items: &[(usize, TerminalTitleItem)],
) -> Vec<TerminalTitleItem> {
for (index, item) in hidden_items {
if items.contains(item) {
continue;
}
items.insert((*index).min(items.len()), *item);
}
items
}
fn parse_terminal_title_items<T>(ids: impl Iterator<Item = T>) -> Option<Vec<TerminalTitleItem>>
where
T: AsRef<str>,
@@ -147,6 +184,8 @@ impl TerminalTitleSetupView {
github_pr_available: bool,
app_event_tx: AppEventSender,
) -> Self {
let hidden_configured_items =
hidden_configured_terminal_title_items(title_items, github_pr_available);
let selected_items = title_items
.into_iter()
.flatten()
@@ -212,10 +251,11 @@ impl TerminalTitleSetupView {
};
app_event.send(AppEvent::TerminalTitleSetupPreview { items });
})
.on_confirm(|ids, app_event| {
.on_confirm(move |ids, app_event| {
let Some(items) = parse_terminal_title_items(ids.iter().map(String::as_str)) else {
return;
};
let items = restore_hidden_terminal_title_items(items, &hidden_configured_items);
app_event.send(AppEvent::TerminalTitleSetup { items });
})
.on_cancel(|app_event| {
@@ -263,6 +303,9 @@ impl Renderable for TerminalTitleSetupView {
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;
use tokio::sync::mpsc::unbounded_channel;
@@ -321,6 +364,33 @@ mod tests {
assert!(!rendered.contains("PR #123"));
}
#[tokio::test]
async fn confirm_preserves_hidden_github_pr_when_gh_unavailable() {
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let selected = [
"project".to_string(),
"github-pr".to_string(),
"model".to_string(),
];
let mut view =
TerminalTitleSetupView::new(Some(&selected), /*github_pr_available*/ false, tx);
view.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let Some(AppEvent::TerminalTitleSetup { items }) = rx.recv().await else {
panic!("expected terminal title setup event");
};
assert_eq!(
items,
vec![
TerminalTitleItem::Project,
TerminalTitleItem::GithubPr,
TerminalTitleItem::Model,
]
);
}
#[test]
fn parse_terminal_title_items_preserves_order() {
let items =