fix(tui): make current worktree selection a no-op

This commit is contained in:
Felipe Coury
2026-05-08 10:08:26 -03:00
parent 1c604c0be6
commit 700f1e4a38
5 changed files with 100 additions and 15 deletions

View File

@@ -215,6 +215,10 @@ impl App {
self.begin_switch_to_worktree_target(tui, target); self.begin_switch_to_worktree_target(tui, target);
tui.frame_requester().schedule_frame(); tui.frame_requester().schedule_frame();
} }
AppEvent::CurrentWorktreeSelected { target } => {
self.current_worktree_selected(target);
tui.frame_requester().schedule_frame();
}
AppEvent::SwitchToWorktreeAfterLoading { target } => { AppEvent::SwitchToWorktreeAfterLoading { target } => {
self.switch_to_worktree_target_after_loading(tui, app_server, target) self.switch_to_worktree_target_after_loading(tui, app_server, target)
.await; .await;

View File

@@ -183,6 +183,11 @@ impl App {
self.defer_switch_to_worktree_target(target); self.defer_switch_to_worktree_target(target);
} }
pub(super) fn current_worktree_selected(&mut self, target: String) {
self.chat_widget
.add_info_message(format!("Already in worktree {target}."), /*hint*/ None);
}
pub(super) async fn switch_to_worktree_target_after_loading( pub(super) async fn switch_to_worktree_target_after_loading(
&mut self, &mut self,
tui: &mut tui::Tui, tui: &mut tui::Tui,

View File

@@ -224,6 +224,11 @@ pub(crate) enum AppEvent {
target: String, target: String,
}, },
/// A picker row for the current worktree was selected.
CurrentWorktreeSelected {
target: String,
},
/// Continue switching into an existing worktree after the loading view has rendered. /// Continue switching into an existing worktree after the loading view has rendered.
SwitchToWorktreeAfterLoading { SwitchToWorktreeAfterLoading {
target: String, target: String,

View File

@@ -7,8 +7,8 @@ expression: "render_selection(params, 86)"
Create a worktree or fork this chat into an existing workspace. Create a worktree or fork this chat into an existing workspace.
Search worktrees Search worktrees
New worktree... Type the branch name for the new worktree. New worktree... Create a sibling worktree and start this chat there.
fcoury/demo (current) clean · cli · /repo/codex.fcoury-demo fcoury/demo (current) Already in this worktree
codex clean · app · /repo/codex.codex codex clean · app · /repo/codex.codex
main dirty · git · /repo/codex.main main dirty · git · /repo/codex.main

View File

@@ -14,6 +14,7 @@ use ratatui::widgets::WidgetRef;
use crate::app_event::AppEvent; use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender; use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::ColumnWidthMode; use crate::bottom_pane::ColumnWidthMode;
use crate::bottom_pane::SelectionAction;
use crate::bottom_pane::SelectionItem; use crate::bottom_pane::SelectionItem;
use crate::bottom_pane::SelectionRowDisplay; use crate::bottom_pane::SelectionRowDisplay;
use crate::bottom_pane::SelectionViewParams; use crate::bottom_pane::SelectionViewParams;
@@ -361,7 +362,8 @@ pub(crate) fn error_with_summary_params(summary: String, error: String) -> Selec
pub(crate) fn picker_params(entries: Vec<WorktreeInfo>, current_cwd: &Path) -> SelectionViewParams { pub(crate) fn picker_params(entries: Vec<WorktreeInfo>, current_cwd: &Path) -> SelectionViewParams {
let mut items = vec![new_worktree_item()]; let mut items = vec![new_worktree_item()];
items.extend(entries.into_iter().map(|entry| { let mut initial_selected_idx = None;
items.extend(entries.into_iter().enumerate().map(|(idx, entry)| {
let target = entry.branch.clone().unwrap_or_else(|| entry.name.clone()); let target = entry.branch.clone().unwrap_or_else(|| entry.name.clone());
let source = source_label(entry.source); let source = source_label(entry.source);
let status = if entry.dirty.is_dirty() { let status = if entry.dirty.is_dirty() {
@@ -369,7 +371,16 @@ pub(crate) fn picker_params(entries: Vec<WorktreeInfo>, current_cwd: &Path) -> S
} else { } else {
"clean" "clean"
}; };
let is_current = is_current_worktree(current_cwd, &entry);
if is_current {
initial_selected_idx = Some(idx + 1);
}
let description = format!("{status} · {source} · {}", entry.workspace_cwd.display()); let description = format!("{status} · {source} · {}", entry.workspace_cwd.display());
let selected_description = if is_current {
"Already in this worktree".to_string()
} else {
format!("Fork this chat into {}", entry.workspace_cwd.display())
};
let search_value = Some(format!( let search_value = Some(format!(
"{} {} {} {}", "{} {} {} {}",
target, target,
@@ -377,21 +388,28 @@ pub(crate) fn picker_params(entries: Vec<WorktreeInfo>, current_cwd: &Path) -> S
source, source,
entry.workspace_cwd.display() entry.workspace_cwd.display()
)); ));
SelectionItem { let target_for_action = target.clone();
name: target.clone(), let actions: Vec<SelectionAction> = if is_current {
description: Some(description), vec![Box::new(move |tx| {
selected_description: Some(format!( tx.send(AppEvent::CurrentWorktreeSelected {
"Fork this chat into {}", target: target_for_action.clone(),
entry.workspace_cwd.display()
)),
is_current: paths_match(current_cwd, &entry.workspace_cwd),
actions: vec![Box::new(move |tx| {
tx.send(AppEvent::SwitchToWorktree {
target: target.clone(),
}); });
})], })]
} else {
vec![Box::new(move |tx| {
tx.send(AppEvent::SwitchToWorktree {
target: target_for_action.clone(),
});
})]
};
SelectionItem {
name: target,
description: Some(description),
selected_description: Some(selected_description),
actions,
dismiss_on_select: true, dismiss_on_select: true,
search_value, search_value,
is_current,
..Default::default() ..Default::default()
} }
})); }));
@@ -411,6 +429,7 @@ pub(crate) fn picker_params(entries: Vec<WorktreeInfo>, current_cwd: &Path) -> S
search_placeholder: Some("Search worktrees".to_string()), search_placeholder: Some("Search worktrees".to_string()),
col_width_mode: ColumnWidthMode::AutoAllRows, col_width_mode: ColumnWidthMode::AutoAllRows,
row_display: SelectionRowDisplay::SingleLine, row_display: SelectionRowDisplay::SingleLine,
initial_selected_idx,
..Default::default() ..Default::default()
} }
} }
@@ -560,6 +579,20 @@ fn paths_match(a: &Path, b: &Path) -> bool {
a == b a == b
} }
fn is_current_worktree(current_cwd: &Path, entry: &WorktreeInfo) -> bool {
if paths_match(current_cwd, &entry.workspace_cwd) {
return true;
}
let current_cwd = current_cwd
.canonicalize()
.unwrap_or_else(|_| current_cwd.to_path_buf());
let worktree_root = entry
.worktree_git_root
.canonicalize()
.unwrap_or_else(|_| entry.worktree_git_root.clone());
current_cwd.starts_with(worktree_root)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -624,6 +657,44 @@ mod tests {
insta::assert_snapshot!("worktree_picker", render_selection(params, /*width*/ 86)); insta::assert_snapshot!("worktree_picker", render_selection(params, /*width*/ 86));
} }
#[test]
fn worktree_picker_preselects_current_worktree_from_subdirectory() {
let params = picker_params(
vec![
sample_info("fcoury/demo", WorktreeSource::Cli, /*dirty*/ false),
sample_info(
"fcoury/worktrees",
WorktreeSource::Git,
/*dirty*/ false,
),
],
Path::new("/repo/codex.fcoury-worktrees/codex-rs"),
);
assert_eq!(params.initial_selected_idx, Some(2));
}
#[test]
fn current_worktree_item_dispatches_current_selection_event() {
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let params = picker_params(
vec![sample_info(
"fcoury/worktrees",
WorktreeSource::Git,
/*dirty*/ false,
)],
Path::new("/repo/codex.fcoury-worktrees/codex-rs"),
);
(params.items[1].actions[0])(&tx);
assert!(matches!(
rx.try_recv(),
Ok(AppEvent::CurrentWorktreeSelected { target }) if target == "fcoury/worktrees"
));
}
#[test] #[test]
fn worktree_loading_snapshot() { fn worktree_loading_snapshot() {
insta::assert_snapshot!( insta::assert_snapshot!(