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);
tui.frame_requester().schedule_frame();
}
AppEvent::CurrentWorktreeSelected { target } => {
self.current_worktree_selected(target);
tui.frame_requester().schedule_frame();
}
AppEvent::SwitchToWorktreeAfterLoading { target } => {
self.switch_to_worktree_target_after_loading(tui, app_server, target)
.await;

View File

@@ -183,6 +183,11 @@ impl App {
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(
&mut self,
tui: &mut tui::Tui,

View File

@@ -224,6 +224,11 @@ pub(crate) enum AppEvent {
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.
SwitchToWorktreeAfterLoading {
target: String,

View File

@@ -7,8 +7,8 @@ expression: "render_selection(params, 86)"
Create a worktree or fork this chat into an existing workspace.
Search worktrees
New worktree... Type the branch name for the new worktree.
fcoury/demo (current) clean · cli · /repo/codex.fcoury-demo
New worktree... Create a sibling worktree and start this chat there.
fcoury/demo (current) Already in this worktree
codex clean · app · /repo/codex.codex
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_sender::AppEventSender;
use crate::bottom_pane::ColumnWidthMode;
use crate::bottom_pane::SelectionAction;
use crate::bottom_pane::SelectionItem;
use crate::bottom_pane::SelectionRowDisplay;
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 {
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 source = source_label(entry.source);
let status = if entry.dirty.is_dirty() {
@@ -369,7 +371,16 @@ pub(crate) fn picker_params(entries: Vec<WorktreeInfo>, current_cwd: &Path) -> S
} else {
"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 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!(
"{} {} {} {}",
target,
@@ -377,21 +388,28 @@ pub(crate) fn picker_params(entries: Vec<WorktreeInfo>, current_cwd: &Path) -> S
source,
entry.workspace_cwd.display()
));
SelectionItem {
name: target.clone(),
description: Some(description),
selected_description: Some(format!(
"Fork this chat into {}",
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(),
let target_for_action = target.clone();
let actions: Vec<SelectionAction> = if is_current {
vec![Box::new(move |tx| {
tx.send(AppEvent::CurrentWorktreeSelected {
target: target_for_action.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,
search_value,
is_current,
..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()),
col_width_mode: ColumnWidthMode::AutoAllRows,
row_display: SelectionRowDisplay::SingleLine,
initial_selected_idx,
..Default::default()
}
}
@@ -560,6 +579,20 @@ fn paths_match(a: &Path, b: &Path) -> bool {
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)]
mod tests {
use super::*;
@@ -624,6 +657,44 @@ mod tests {
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]
fn worktree_loading_snapshot() {
insta::assert_snapshot!(