mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
fix(tui): make current worktree selection a no-op
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
Reference in New Issue
Block a user