Compare commits

..
1 Commits
Author SHA1 Message Date
Miguel PalhasandClaude Opus 5 d032036542 feat(maestro): move worktree choice behind advanced options
ci / flake (push) Successful in 5m12s
ci / rust (push) Successful in 2m41s
ci / web (push) Successful in 35s
The worktree step ("+ new" plus the repository's existing worktrees) now
lives inside the collapsed "Advanced options" section in both the web form
and the TUI draft pane, so a plain spawn is command, project, name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 08:42:00 +01:00
3 changed files with 84 additions and 59 deletions
+70 -55
View File
@@ -699,12 +699,13 @@ impl DraftPane {
order.push(DraftField::DirPicker);
if self.cwd_raw_visible {
order.push(DraftField::CwdRaw);
} else if self.selected_group_key.is_some() {
order.push(DraftField::WorktreeChoice);
}
order.push(DraftField::Name);
order.push(DraftField::Advanced);
if !self.advanced_collapsed {
if !self.cwd_raw_visible && self.selected_group_key.is_some() {
order.push(DraftField::WorktreeChoice);
}
order.push(DraftField::WorktreeToggle);
if self.worktree_body_visible() {
order.push(DraftField::BranchMode);
@@ -1082,14 +1083,13 @@ impl DraftPane {
let show_custom = self.is_custom();
let show_raw = self.cwd_raw_visible;
// The worktree toggle and its body stay behind the "Advanced options"
// disclosure (#93): collapsed hides both, regardless of what they'd
// otherwise show. The worktree *choice* step is not part of that —
// it belongs to the directory picker (#95 rule 3), so it stays
// visible once a project is picked.
// The worktree choice step, the worktree toggle and its body all stay
// behind the "Advanced options" disclosure (#93): collapsed hides
// them, regardless of what they'd otherwise show.
let show_wt_toggle = !self.advanced_collapsed;
let show_wt_body = show_wt_toggle && self.worktree_body_visible();
let show_wt_choice = self.selected_group_key.is_some() && !show_raw;
let show_wt_choice =
!self.advanced_collapsed && self.selected_group_key.is_some() && !show_raw;
let wt_choice_rows_data = if show_wt_choice {
self.worktree_choice_rows()
} else {
@@ -1118,11 +1118,12 @@ impl DraftPane {
constraints.push(Constraint::Length(list_capacity as u16));
if show_raw {
constraints.push(Constraint::Length(1));
} else if show_wt_choice {
constraints.push(Constraint::Length(wt_choice_capacity));
}
constraints.push(Constraint::Length(1)); // name
constraints.push(Constraint::Length(1)); // advanced header
if show_wt_choice {
constraints.push(Constraint::Length(wt_choice_capacity));
}
if show_wt_toggle {
constraints.push(Constraint::Length(1)); // worktree toggle
}
@@ -1268,7 +1269,52 @@ impl DraftPane {
if self.focused_field == DraftField::CwdRaw {
cursor = Some((col, row.y));
}
} else if show_wt_choice {
}
// -- name + reroll ---------------------------------------------------------
let name_row = next_row();
draw_gutter(f, name_row, "name:");
let col = draw_text_field(f, &self.name, name_row, field_style(DraftField::Name));
if self.focused_field == DraftField::Name {
cursor = Some((col, name_row.y));
}
// Dim reroll hint, tucked after the typed name without covering it.
let hint_offset = (GUTTER as u16 + self.name.value().chars().count() as u16 + 1)
.min(name_row.width.saturating_sub(10));
f.render_widget(
Paragraph::new(Line::from(Span::styled(
"^R reroll",
Style::default().add_modifier(Modifier::DIM),
))),
Rect {
x: name_row.x + hint_offset,
y: name_row.y,
width: name_row.width.saturating_sub(hint_offset),
height: 1,
},
);
// -- advanced disclosure (#93) ------------------------------------------
let advanced_row = next_row();
f.render_widget(
Paragraph::new(Line::from(vec![
Span::raw(format!("{:<GUTTER$}", "adv:")),
Span::styled(
format!(
"[{}] Advanced options",
if self.advanced_collapsed { "+" } else { "-" }
),
field_style(DraftField::Advanced),
),
Span::styled(
format!(" — {}", self.advanced_summary()),
Style::default().add_modifier(Modifier::DIM),
),
])),
advanced_row,
);
if show_wt_choice {
// Step two (#96 rule 3): "+ new" first, then the repository's
// worktrees newest-first.
let choice_rows = wt_choice_rows_data;
@@ -1317,49 +1363,6 @@ impl DraftPane {
}
}
// -- name + reroll ---------------------------------------------------------
let name_row = next_row();
draw_gutter(f, name_row, "name:");
let col = draw_text_field(f, &self.name, name_row, field_style(DraftField::Name));
if self.focused_field == DraftField::Name {
cursor = Some((col, name_row.y));
}
// Dim reroll hint, tucked after the typed name without covering it.
let hint_offset = (GUTTER as u16 + self.name.value().chars().count() as u16 + 1)
.min(name_row.width.saturating_sub(10));
f.render_widget(
Paragraph::new(Line::from(Span::styled(
"^R reroll",
Style::default().add_modifier(Modifier::DIM),
))),
Rect {
x: name_row.x + hint_offset,
y: name_row.y,
width: name_row.width.saturating_sub(hint_offset),
height: 1,
},
);
// -- advanced disclosure (#93) ------------------------------------------
let advanced_row = next_row();
f.render_widget(
Paragraph::new(Line::from(vec![
Span::raw(format!("{:<GUTTER$}", "adv:")),
Span::styled(
format!(
"[{}] Advanced options",
if self.advanced_collapsed { "+" } else { "-" }
),
field_style(DraftField::Advanced),
),
Span::styled(
format!(" — {}", self.advanced_summary()),
Style::default().add_modifier(Modifier::DIM),
),
])),
advanced_row,
);
// -- worktree (#65) -----------------------------------------------------
if show_wt_toggle {
let wt_row = next_row();
@@ -2207,9 +2210,14 @@ mod tests {
assert_eq!(draft.worktree_choice, WorktreeChoice::New);
assert_eq!(draft.picked_cwd.as_deref(), Some(Path::new("/repos/main")));
assert!(draft.worktree_enabled, "\"+ new\" means create-worktree on");
assert!(
!draft.field_order().contains(&DraftField::WorktreeChoice),
"step two lives behind the collapsed \"Advanced options\" disclosure"
);
draft.toggle_advanced();
assert!(
draft.field_order().contains(&DraftField::WorktreeChoice),
"step two is now reachable"
"step two is reachable once advanced is open"
);
assert!(
draft.take_gitroot_probe(probe_due()).is_none(),
@@ -2622,6 +2630,13 @@ mod tests {
);
draft.focused_field = DraftField::DirPicker;
draft.on_input(&InputKey::Enter); // picks the "main" box
let collapsed = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
assert!(
!collapsed.contains("+ new"),
"step two hides with \"Advanced options\" closed:\n{collapsed}"
);
draft.toggle_advanced();
let rendered = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
assert!(
+4 -4
View File
@@ -140,10 +140,6 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
placeholder="/home/user/project" inputmode="text" autocomplete="off" />
</div>
</div>
<div class="field wt-step" hidden>
<span class="field-label">Worktree</span>
<div class="picker wt-choices"></div>
</div>
<div class="field">
<span class="field-label">Name</span>
<div class="name-row">
@@ -158,6 +154,10 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
<span class="wt-summary"></span>
</button>
<div class="wt-collapsible" hidden>
<div class="field wt-step" hidden>
<span class="field-label">Worktree</span>
<div class="picker wt-choices"></div>
</div>
<label class="row-inline">
<input type="checkbox" name="worktree_enabled" />
Create git worktree
+10
View File
@@ -108,6 +108,16 @@ describe("spawnForm worktree step (#95)", () => {
expect(step(form).hidden).toBe(false);
});
it("keeps the worktree step inside the collapsed Advanced options section", async () => {
const form = await mount([MAESTRO, WT_A]);
box(form, MAESTRO.path).click();
await drain();
const collapsible = form.querySelector<HTMLElement>(".wt-collapsible")!;
expect(collapsible.contains(step(form))).toBe(true);
expect(collapsible.hidden).toBe(true);
});
it("lists '+ new' first and selected by default, then the project's worktrees", async () => {
const form = await mount([MAESTRO, WT_A, WT_B]);
box(form, MAESTRO.path).click();