Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd66dbf7d8 | ||
|
|
6bb812f6bd | ||
|
|
d3098a9400 | ||
|
|
5de04af36a | ||
|
|
36cb03961c | ||
|
|
5e34ce7a61 |
@@ -97,3 +97,24 @@ jobs:
|
|||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
run: pnpm -C web build
|
run: pnpm -C web build
|
||||||
|
|
||||||
|
# The flake pins pnpmDeps.hash against web/pnpm-lock.yaml (#98). A lockfile
|
||||||
|
# change invalidates the pin silently elsewhere, so the packaged build has to
|
||||||
|
# fail here too.
|
||||||
|
flake:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install nix
|
||||||
|
run: |
|
||||||
|
curl -fsSL https://nixos.org/nix/install | sh -s -- --daemon --yes
|
||||||
|
echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: build flake packages
|
||||||
|
run: |
|
||||||
|
nix build \
|
||||||
|
.#webFrontend \
|
||||||
|
.#maestro \
|
||||||
|
.#maestro-web \
|
||||||
|
--no-link
|
||||||
|
|||||||
@@ -1349,9 +1349,7 @@ mod tests {
|
|||||||
for c in "crimson-otter".chars() {
|
for c in "crimson-otter".chars() {
|
||||||
app.handle_draft_input(&InputKey::char(c));
|
app.handle_draft_input(&InputKey::char(c));
|
||||||
}
|
}
|
||||||
app.handle_draft_input(&InputKey::Tab); // → worktree toggle
|
app.handle_draft_input(&InputKey::Tab); // → advanced disclosure (collapsed, #93)
|
||||||
app.handle_draft_input(&InputKey::Tab); // → branch mode
|
|
||||||
app.handle_draft_input(&InputKey::Tab); // → new-branch name
|
|
||||||
app.handle_draft_input(&InputKey::Tab); // → submit
|
app.handle_draft_input(&InputKey::Tab); // → submit
|
||||||
|
|
||||||
match app.handle_draft_input(&InputKey::Enter).as_slice() {
|
match app.handle_draft_input(&InputKey::Enter).as_slice() {
|
||||||
|
|||||||
+184
-33
@@ -57,6 +57,9 @@ pub enum DraftField {
|
|||||||
/// Free-text path, revealed by picking `Other directory…`.
|
/// Free-text path, revealed by picking `Other directory…`.
|
||||||
CwdRaw,
|
CwdRaw,
|
||||||
Name,
|
Name,
|
||||||
|
/// Disclosure collapsing the worktree/branch controls below (#93).
|
||||||
|
/// Closed by default; Enter / Space toggles.
|
||||||
|
Advanced,
|
||||||
/// Worktree on/off for this session (#65). Enter / Space toggles.
|
/// Worktree on/off for this session (#65). Enter / Space toggles.
|
||||||
WorktreeToggle,
|
WorktreeToggle,
|
||||||
/// New-vs-existing branch source; Left / Right switches.
|
/// New-vs-existing branch source; Left / Right switches.
|
||||||
@@ -148,6 +151,10 @@ pub struct DraftPane {
|
|||||||
/// Worktree on for this spawn (#65). Defaults on; a failed gitroot probe
|
/// Worktree on for this spawn (#65). Defaults on; a failed gitroot probe
|
||||||
/// unticks it, like the web form.
|
/// unticks it, like the web form.
|
||||||
pub worktree_enabled: bool,
|
pub worktree_enabled: bool,
|
||||||
|
/// Whether the worktree/branch controls are collapsed behind the
|
||||||
|
/// "Advanced options" disclosure (#93). Closed by default — most spawns
|
||||||
|
/// take the default target.
|
||||||
|
pub advanced_collapsed: bool,
|
||||||
/// Enclosing repository of the chosen target — from the project scan for
|
/// Enclosing repository of the chosen target — from the project scan for
|
||||||
/// picked rows, `/api/gitroot` for raw paths. `None` until known.
|
/// picked rows, `/api/gitroot` for raw paths. `None` until known.
|
||||||
pub base_repo: Option<PathBuf>,
|
pub base_repo: Option<PathBuf>,
|
||||||
@@ -204,6 +211,7 @@ impl Default for DraftPane {
|
|||||||
name_touched: false,
|
name_touched: false,
|
||||||
needs_name_refill: false,
|
needs_name_refill: false,
|
||||||
worktree_enabled: true,
|
worktree_enabled: true,
|
||||||
|
advanced_collapsed: true,
|
||||||
base_repo: None,
|
base_repo: None,
|
||||||
probed_cwd: None,
|
probed_cwd: None,
|
||||||
probe_seq: 0,
|
probe_seq: 0,
|
||||||
@@ -442,6 +450,21 @@ impl DraftPane {
|
|||||||
self.set_branch_mode(next);
|
self.set_branch_mode(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enter / Space on the "Advanced options" header (#93).
|
||||||
|
pub fn toggle_advanced(&mut self) {
|
||||||
|
self.advanced_collapsed = !self.advanced_collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the collapsed header says about this spawn's default, so it stays
|
||||||
|
/// visible without expanding (#93).
|
||||||
|
fn advanced_summary(&self) -> &'static str {
|
||||||
|
if self.worktree_enabled {
|
||||||
|
"creates a worktree"
|
||||||
|
} else {
|
||||||
|
"no worktree"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Enter / Space on the toggle. Re-enabling reuses a still-valid repo
|
/// Enter / Space on the toggle. Re-enabling reuses a still-valid repo
|
||||||
/// answer instead of re-probing.
|
/// answer instead of re-probing.
|
||||||
pub fn toggle_worktree(&mut self) {
|
pub fn toggle_worktree(&mut self) {
|
||||||
@@ -602,13 +625,16 @@ impl DraftPane {
|
|||||||
order.push(DraftField::CwdRaw);
|
order.push(DraftField::CwdRaw);
|
||||||
}
|
}
|
||||||
order.push(DraftField::Name);
|
order.push(DraftField::Name);
|
||||||
order.push(DraftField::WorktreeToggle);
|
order.push(DraftField::Advanced);
|
||||||
if self.worktree_body_visible() {
|
if !self.advanced_collapsed {
|
||||||
order.push(DraftField::BranchMode);
|
order.push(DraftField::WorktreeToggle);
|
||||||
order.push(match self.branch_mode {
|
if self.worktree_body_visible() {
|
||||||
WorktreeBranchMode::New => DraftField::BranchNew,
|
order.push(DraftField::BranchMode);
|
||||||
WorktreeBranchMode::Existing => DraftField::BranchExisting,
|
order.push(match self.branch_mode {
|
||||||
});
|
WorktreeBranchMode::New => DraftField::BranchNew,
|
||||||
|
WorktreeBranchMode::Existing => DraftField::BranchExisting,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
order.extend([DraftField::Submit]);
|
order.extend([DraftField::Submit]);
|
||||||
order
|
order
|
||||||
@@ -733,6 +759,10 @@ impl DraftPane {
|
|||||||
self.select_entry();
|
self.select_entry();
|
||||||
DraftInputResult::Consumed
|
DraftInputResult::Consumed
|
||||||
}
|
}
|
||||||
|
DraftField::Advanced => {
|
||||||
|
self.toggle_advanced();
|
||||||
|
DraftInputResult::Consumed
|
||||||
|
}
|
||||||
DraftField::WorktreeToggle => {
|
DraftField::WorktreeToggle => {
|
||||||
self.toggle_worktree();
|
self.toggle_worktree();
|
||||||
DraftInputResult::Consumed
|
DraftInputResult::Consumed
|
||||||
@@ -744,6 +774,14 @@ impl DraftPane {
|
|||||||
DraftInputResult::Consumed
|
DraftInputResult::Consumed
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
InputKey::Char {
|
||||||
|
c: ' ',
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if self.focused_field == DraftField::Advanced => {
|
||||||
|
self.toggle_advanced();
|
||||||
|
DraftInputResult::Consumed
|
||||||
|
}
|
||||||
InputKey::Char {
|
InputKey::Char {
|
||||||
c: ' ',
|
c: ' ',
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
@@ -832,13 +870,18 @@ impl DraftPane {
|
|||||||
|
|
||||||
let show_custom = self.is_custom();
|
let show_custom = self.is_custom();
|
||||||
let show_raw = self.cwd_raw_visible;
|
let show_raw = self.cwd_raw_visible;
|
||||||
let show_wt_body = self.worktree_body_visible();
|
// The worktree toggle and its body stay behind the "Advanced options"
|
||||||
|
// disclosure (#93): collapsed hides both, 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();
|
||||||
// Up to three branch rows stay visible around the selection; held
|
// Up to three branch rows stay visible around the selection; held
|
||||||
// branches need their reason on screen, so they aren't collapsed.
|
// branches need their reason on screen, so they aren't collapsed.
|
||||||
let branch_rows = self.branches.len().clamp(1, 3) as u16;
|
let branch_rows = self.branches.len().clamp(1, 3) as u16;
|
||||||
let fixed: u16 = 3 /* tiles + dir + name */ + 1 /* worktree */ + 1 /* spawn */
|
let fixed: u16 = 3 /* tiles + dir + name */ + 1 /* advanced header */ + 1 /* spawn */
|
||||||
+ u16::from(show_custom)
|
+ u16::from(show_custom)
|
||||||
+ u16::from(show_raw)
|
+ u16::from(show_raw)
|
||||||
|
+ u16::from(show_wt_toggle)
|
||||||
+ u16::from(show_wt_body) * (1 + if self.branch_mode == WorktreeBranchMode::New { 1 } else { branch_rows })
|
+ u16::from(show_wt_body) * (1 + if self.branch_mode == WorktreeBranchMode::New { 1 } else { branch_rows })
|
||||||
+ u16::from(self.error.is_some());
|
+ u16::from(self.error.is_some());
|
||||||
let list_capacity = (inner.height.saturating_sub(fixed)).max(1) as usize;
|
let list_capacity = (inner.height.saturating_sub(fixed)).max(1) as usize;
|
||||||
@@ -853,7 +896,10 @@ impl DraftPane {
|
|||||||
constraints.push(Constraint::Length(1));
|
constraints.push(Constraint::Length(1));
|
||||||
}
|
}
|
||||||
constraints.push(Constraint::Length(1)); // name
|
constraints.push(Constraint::Length(1)); // name
|
||||||
constraints.push(Constraint::Length(1)); // worktree toggle
|
constraints.push(Constraint::Length(1)); // advanced header
|
||||||
|
if show_wt_toggle {
|
||||||
|
constraints.push(Constraint::Length(1)); // worktree toggle
|
||||||
|
}
|
||||||
if show_wt_body {
|
if show_wt_body {
|
||||||
constraints.push(Constraint::Length(1)); // branch mode
|
constraints.push(Constraint::Length(1)); // branch mode
|
||||||
match self.branch_mode {
|
match self.branch_mode {
|
||||||
@@ -1030,33 +1076,55 @@ impl DraftPane {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// -- 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) -----------------------------------------------------
|
// -- worktree (#65) -----------------------------------------------------
|
||||||
let wt_row = next_row();
|
if show_wt_toggle {
|
||||||
let hint = if self.cwd().is_none() {
|
let wt_row = next_row();
|
||||||
Some("choose a directory first")
|
let hint = if self.cwd().is_none() {
|
||||||
} else if self.repo_missing {
|
Some("choose a directory first")
|
||||||
Some("not inside a git repository")
|
} else if self.repo_missing {
|
||||||
} else if !self.worktree_enabled {
|
Some("not inside a git repository")
|
||||||
Some("off — spawns in the checkout itself")
|
} else if !self.worktree_enabled {
|
||||||
} else {
|
Some("off — spawns in the checkout itself")
|
||||||
None
|
|
||||||
};
|
|
||||||
let mut tiles = vec![Span::raw(format!("{:<GUTTER$}", "wt:"))];
|
|
||||||
tiles.push(Span::styled(
|
|
||||||
if self.worktree_enabled {
|
|
||||||
"[x] worktree"
|
|
||||||
} else {
|
} else {
|
||||||
"[ ] worktree"
|
None
|
||||||
},
|
};
|
||||||
field_style(DraftField::WorktreeToggle),
|
let mut tiles = vec![Span::raw(format!("{:<GUTTER$}", "wt:"))];
|
||||||
));
|
|
||||||
if let Some(hint) = hint {
|
|
||||||
tiles.push(Span::styled(
|
tiles.push(Span::styled(
|
||||||
format!(" {hint}"),
|
if self.worktree_enabled {
|
||||||
Style::default().add_modifier(Modifier::DIM),
|
"[x] worktree"
|
||||||
|
} else {
|
||||||
|
"[ ] worktree"
|
||||||
|
},
|
||||||
|
field_style(DraftField::WorktreeToggle),
|
||||||
));
|
));
|
||||||
|
if let Some(hint) = hint {
|
||||||
|
tiles.push(Span::styled(
|
||||||
|
format!(" {hint}"),
|
||||||
|
Style::default().add_modifier(Modifier::DIM),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
f.render_widget(Paragraph::new(Line::from(tiles)), wt_row);
|
||||||
}
|
}
|
||||||
f.render_widget(Paragraph::new(Line::from(tiles)), wt_row);
|
|
||||||
|
|
||||||
if show_wt_body {
|
if show_wt_body {
|
||||||
// branch source: new (created) vs existing (checked out as-is)
|
// branch source: new (created) vs existing (checked out as-is)
|
||||||
@@ -1894,7 +1962,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn held_branches_render_unavailable_with_their_holder_as_the_reason() {
|
fn held_branches_render_unavailable_with_their_holder_as_the_reason() {
|
||||||
let mut draft = DraftPane::default();
|
let mut draft = DraftPane {
|
||||||
|
advanced_collapsed: false,
|
||||||
|
..DraftPane::default()
|
||||||
|
};
|
||||||
draft.set_projects(
|
draft.set_projects(
|
||||||
vec![repo_project(
|
vec![repo_project(
|
||||||
"maestro",
|
"maestro",
|
||||||
@@ -2069,6 +2140,7 @@ mod tests {
|
|||||||
fn render_shows_the_worktree_section() {
|
fn render_shows_the_worktree_section() {
|
||||||
let mut draft = DraftPane {
|
let mut draft = DraftPane {
|
||||||
home_dir: "/home/u".into(),
|
home_dir: "/home/u".into(),
|
||||||
|
advanced_collapsed: false,
|
||||||
..DraftPane::default()
|
..DraftPane::default()
|
||||||
};
|
};
|
||||||
draft.set_projects(
|
draft.set_projects(
|
||||||
@@ -2107,6 +2179,85 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn advanced_section_is_collapsed_by_default_and_states_the_worktree_default() {
|
||||||
|
let mut draft = DraftPane {
|
||||||
|
home_dir: "/home/u".into(),
|
||||||
|
..DraftPane::default()
|
||||||
|
};
|
||||||
|
draft.set_projects(
|
||||||
|
vec![project("maestro", "/home/u/tea/maestro", 1, Some("claude"))],
|
||||||
|
Utc::now(),
|
||||||
|
);
|
||||||
|
assert!(draft.advanced_collapsed, "closed by default");
|
||||||
|
assert_eq!(
|
||||||
|
draft
|
||||||
|
.field_order()
|
||||||
|
.iter()
|
||||||
|
.filter(|f| matches!(f, DraftField::WorktreeToggle | DraftField::BranchMode))
|
||||||
|
.count(),
|
||||||
|
0,
|
||||||
|
"collapsed fields drop out of tab order"
|
||||||
|
);
|
||||||
|
|
||||||
|
let rendered = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
|
||||||
|
assert!(
|
||||||
|
rendered.contains("Advanced options") && rendered.contains("creates a worktree"),
|
||||||
|
"header states the on-by-default worktree:\n{rendered}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!rendered.contains("[x] worktree"),
|
||||||
|
"checkbox stays hidden while collapsed:\n{rendered}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn advanced_header_states_no_worktree_for_a_worktree_row_91() {
|
||||||
|
let mut draft = DraftPane::default();
|
||||||
|
let mut wt = repo_project("wt-91", "/repos/wt91", "/repos/main", 1, None);
|
||||||
|
wt.is_worktree = true;
|
||||||
|
draft.set_projects(vec![wt], Utc::now());
|
||||||
|
draft.focused_field = DraftField::DirPicker;
|
||||||
|
draft.on_input(&InputKey::Enter);
|
||||||
|
|
||||||
|
assert!(!draft.worktree_enabled);
|
||||||
|
assert!(
|
||||||
|
draft.advanced_collapsed,
|
||||||
|
"picking a row must not force it open"
|
||||||
|
);
|
||||||
|
let rendered = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
|
||||||
|
assert!(
|
||||||
|
rendered.contains("no worktree"),
|
||||||
|
"header reflects #91's off default:\n{rendered}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_on_the_advanced_header_expands_it_without_moving_fields_above() {
|
||||||
|
let mut draft = DraftPane {
|
||||||
|
home_dir: "/home/u".into(),
|
||||||
|
focused_field: DraftField::Advanced,
|
||||||
|
..DraftPane::default()
|
||||||
|
};
|
||||||
|
draft.set_projects(
|
||||||
|
vec![project("maestro", "/home/u/tea/maestro", 1, Some("claude"))],
|
||||||
|
Utc::now(),
|
||||||
|
);
|
||||||
|
let before = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
|
||||||
|
let before_head: Vec<&str> = before.lines().take(6).collect();
|
||||||
|
|
||||||
|
draft.on_input(&InputKey::Enter);
|
||||||
|
assert!(!draft.advanced_collapsed);
|
||||||
|
|
||||||
|
let after = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
|
||||||
|
let after_head: Vec<&str> = after.lines().take(6).collect();
|
||||||
|
assert_eq!(
|
||||||
|
before_head, after_head,
|
||||||
|
"expanding must not move the fields above the disclosure"
|
||||||
|
);
|
||||||
|
assert!(after.contains("[x] worktree"), "body now visible:\n{after}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn abbreviate_path_matches_the_web_helpers() {
|
fn abbreviate_path_matches_the_web_helpers() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+45
-20
@@ -152,33 +152,38 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<fieldset class="worktree">
|
<fieldset class="worktree">
|
||||||
<legend>
|
<button type="button" class="tree-folder-header wt-toggle" aria-expanded="false">
|
||||||
|
<span class="chevron">▾</span>
|
||||||
|
<span class="folder-name">Advanced options</span>
|
||||||
|
<span class="wt-summary"></span>
|
||||||
|
</button>
|
||||||
|
<div class="wt-collapsible" hidden>
|
||||||
<label class="row-inline">
|
<label class="row-inline">
|
||||||
<input type="checkbox" name="worktree_enabled" />
|
<input type="checkbox" name="worktree_enabled" />
|
||||||
Create git worktree
|
Create git worktree
|
||||||
</label>
|
</label>
|
||||||
</legend>
|
<p class="wt-hint" hidden></p>
|
||||||
<p class="wt-hint" hidden></p>
|
<div class="wt-body" hidden>
|
||||||
<div class="wt-body" hidden>
|
<p class="wt-repo"></p>
|
||||||
<p class="wt-repo"></p>
|
<div class="wt-mode" role="radiogroup" aria-label="Branch source">
|
||||||
<div class="wt-mode" role="radiogroup" aria-label="Branch source">
|
<label class="row-inline">
|
||||||
<label class="row-inline">
|
<input type="radio" name="worktree_branch_mode" value="new" />
|
||||||
<input type="radio" name="worktree_branch_mode" value="new" />
|
New branch
|
||||||
New branch
|
</label>
|
||||||
|
<label class="row-inline">
|
||||||
|
<input type="radio" name="worktree_branch_mode" value="existing" />
|
||||||
|
Existing branch
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label class="wt-new-wrap">
|
||||||
|
Branch
|
||||||
|
<input name="worktree_branch" type="text" placeholder="feat/foo" autocomplete="off" />
|
||||||
</label>
|
</label>
|
||||||
<label class="row-inline">
|
<label class="wt-exist-wrap" hidden>
|
||||||
<input type="radio" name="worktree_branch_mode" value="existing" />
|
Branch
|
||||||
Existing branch
|
<select class="branch-select"></select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="wt-new-wrap">
|
|
||||||
Branch
|
|
||||||
<input name="worktree_branch" type="text" placeholder="feat/foo" autocomplete="off" />
|
|
||||||
</label>
|
|
||||||
<label class="wt-exist-wrap" hidden>
|
|
||||||
Branch
|
|
||||||
<select class="branch-select"></select>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<p class="form-error" role="alert" aria-live="polite"></p>
|
<p class="form-error" role="alert" aria-live="polite"></p>
|
||||||
@@ -200,6 +205,9 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
|||||||
const cwdRawWrap = form.querySelector(".cwd-raw-wrap") as HTMLElement;
|
const cwdRawWrap = form.querySelector(".cwd-raw-wrap") as HTMLElement;
|
||||||
const cwdRaw = form.querySelector(".cwd-raw") as HTMLInputElement;
|
const cwdRaw = form.querySelector(".cwd-raw") as HTMLInputElement;
|
||||||
const wtEnabled = form.querySelector("input[name=worktree_enabled]") as HTMLInputElement;
|
const wtEnabled = form.querySelector("input[name=worktree_enabled]") as HTMLInputElement;
|
||||||
|
const wtToggle = form.querySelector(".wt-toggle") as HTMLButtonElement;
|
||||||
|
const wtCollapsible = form.querySelector(".wt-collapsible") as HTMLElement;
|
||||||
|
const wtSummary = form.querySelector(".wt-summary") as HTMLElement;
|
||||||
const wtHint = form.querySelector(".wt-hint") as HTMLElement;
|
const wtHint = form.querySelector(".wt-hint") as HTMLElement;
|
||||||
const wtBody = form.querySelector(".wt-body") as HTMLElement;
|
const wtBody = form.querySelector(".wt-body") as HTMLElement;
|
||||||
const wtRepo = form.querySelector(".wt-repo") as HTMLElement;
|
const wtRepo = form.querySelector(".wt-repo") as HTMLElement;
|
||||||
@@ -526,9 +534,11 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
|||||||
markActiveChoice();
|
markActiveChoice();
|
||||||
if (choice === "new") {
|
if (choice === "new") {
|
||||||
wtEnabled.checked = true;
|
wtEnabled.checked = true;
|
||||||
|
updateWtSummary();
|
||||||
applyProjectRepo(group.main.base_repo ?? group.main.path);
|
applyProjectRepo(group.main.base_repo ?? group.main.path);
|
||||||
} else {
|
} else {
|
||||||
wtEnabled.checked = false;
|
wtEnabled.checked = false;
|
||||||
|
updateWtSummary();
|
||||||
inferredBaseRepo = null;
|
inferredBaseRepo = null;
|
||||||
wtBody.hidden = true;
|
wtBody.hidden = true;
|
||||||
wtHint.hidden = true;
|
wtHint.hidden = true;
|
||||||
@@ -661,6 +671,18 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- worktree: disclosure (#93), reuses the sidebar's folder-header ---
|
||||||
|
let advancedCollapsed = true;
|
||||||
|
function setAdvancedCollapsed(collapsed: boolean): void {
|
||||||
|
advancedCollapsed = collapsed;
|
||||||
|
wtToggle.setAttribute("aria-expanded", String(!collapsed));
|
||||||
|
wtCollapsible.hidden = collapsed;
|
||||||
|
}
|
||||||
|
function updateWtSummary(): void {
|
||||||
|
wtSummary.textContent = wtEnabled.checked ? "creates a worktree" : "no worktree";
|
||||||
|
}
|
||||||
|
wtToggle.addEventListener("click", () => setAdvancedCollapsed(!advancedCollapsed));
|
||||||
|
|
||||||
// ---- worktree (branch-only; base_repo inferred via scan or /gitroot) --
|
// ---- worktree (branch-only; base_repo inferred via scan or /gitroot) --
|
||||||
// The project scan already reports repo membership, so a picked project
|
// The project scan already reports repo membership, so a picked project
|
||||||
// resolves its base repo locally (#61); only the free-text escape hatch
|
// resolves its base repo locally (#61); only the free-text escape hatch
|
||||||
@@ -741,6 +763,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
|||||||
// Publish the untick too, or the next render seeds the box checked with
|
// Publish the untick too, or the next render seeds the box checked with
|
||||||
// no inferred repo and submitting fails.
|
// no inferred repo and submitting fails.
|
||||||
wtEnabled.checked = false;
|
wtEnabled.checked = false;
|
||||||
|
updateWtSummary();
|
||||||
emit();
|
emit();
|
||||||
wtBody.hidden = true;
|
wtBody.hidden = true;
|
||||||
wtHint.hidden = false;
|
wtHint.hidden = false;
|
||||||
@@ -814,6 +837,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
|||||||
branchSelect.addEventListener("change", () => emit());
|
branchSelect.addEventListener("change", () => emit());
|
||||||
|
|
||||||
wtEnabled.addEventListener("change", () => {
|
wtEnabled.addEventListener("change", () => {
|
||||||
|
updateWtSummary();
|
||||||
emit();
|
emit();
|
||||||
if (wtEnabled.checked) {
|
if (wtEnabled.checked) {
|
||||||
resolveBaseRepo();
|
resolveBaseRepo();
|
||||||
@@ -827,6 +851,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
|||||||
// The toggle defaults ON (#61): the operator almost always wants a
|
// The toggle defaults ON (#61): the operator almost always wants a
|
||||||
// worktree, and every non-repo target unticks it via the gitroot probe.
|
// worktree, and every non-repo target unticks it via the gitroot probe.
|
||||||
wtEnabled.checked = prefill.worktreeEnabled ?? true;
|
wtEnabled.checked = prefill.worktreeEnabled ?? true;
|
||||||
|
updateWtSummary();
|
||||||
if (prefill.worktreeBranch) branchInput.value = prefill.worktreeBranch;
|
if (prefill.worktreeBranch) branchInput.value = prefill.worktreeBranch;
|
||||||
setBranchMode(branchMode);
|
setBranchMode(branchMode);
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,59 @@ describe("spawnForm worktree defaults and branch picker (#61)", () => {
|
|||||||
expect(modes.at(-1)).toBe("existing");
|
expect(modes.at(-1)).toBe("existing");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("collapses the advanced (worktree) section by default (#93)", async () => {
|
||||||
|
const form = await mount();
|
||||||
|
const toggle = form.querySelector<HTMLButtonElement>(".wt-toggle")!;
|
||||||
|
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
||||||
|
expect(form.querySelector<HTMLElement>(".wt-collapsible")!.hidden).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("states in the collapsed header that a worktree will be created by default (#93)", async () => {
|
||||||
|
const form = await mount();
|
||||||
|
expect(form.querySelector<HTMLElement>(".wt-summary")!.textContent).toMatch(/creates? a worktree/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("states in the collapsed header that no worktree will be created for a worktree row (#91, #93)", async () => {
|
||||||
|
urls = stubFetch({
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
path: "/home/user/wt-91",
|
||||||
|
name: "wt-91",
|
||||||
|
is_worktree: true,
|
||||||
|
base_repo: "/home/user/maestro",
|
||||||
|
last_activity: "2026-08-20T10:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
handle = createSpawnForm({ onSuccess() {} });
|
||||||
|
document.body.appendChild(handle.el);
|
||||||
|
for (let i = 0; i < 40; i++) await Promise.resolve();
|
||||||
|
|
||||||
|
handle.el
|
||||||
|
.querySelectorAll<HTMLButtonElement>(".project-box")
|
||||||
|
.forEach((box) => box.dataset.cwd === "/home/user/wt-91" && box.click());
|
||||||
|
await drain();
|
||||||
|
|
||||||
|
expect(handle.el.querySelector<HTMLElement>(".wt-summary")!.textContent).toMatch(/no worktree/i);
|
||||||
|
// Still collapsed — picking a project must not force it open.
|
||||||
|
expect(handle.el.querySelector<HTMLButtonElement>(".wt-toggle")!.getAttribute("aria-expanded")).toBe(
|
||||||
|
"false",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("expanding the advanced section does not reorder the fields above it (#93)", async () => {
|
||||||
|
const form = await mount();
|
||||||
|
const before = [...form.querySelectorAll(".field")];
|
||||||
|
|
||||||
|
form.querySelector<HTMLButtonElement>(".wt-toggle")!.click();
|
||||||
|
|
||||||
|
expect(form.querySelector<HTMLButtonElement>(".wt-toggle")!.getAttribute("aria-expanded")).toBe(
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
expect(form.querySelector<HTMLElement>(".wt-collapsible")!.hidden).toBe(false);
|
||||||
|
expect([...form.querySelectorAll(".field")]).toStrictEqual(before);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not repopulate a destroyed form when the branch list lands late", async () => {
|
it("does not repopulate a destroyed form when the branch list lands late", async () => {
|
||||||
const gate: { release?: () => void } = {};
|
const gate: { release?: () => void } = {};
|
||||||
urls = stubFetch({ gateBranches: (r) => (gate.release = r) });
|
urls = stubFetch({ gateBranches: (r) => (gate.release = r) });
|
||||||
|
|||||||
+21
-6
@@ -1284,18 +1284,33 @@ textarea {
|
|||||||
.worktree {
|
.worktree {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--r-2);
|
border-radius: var(--r-2);
|
||||||
padding: var(--sp-3);
|
padding: 0 var(--sp-3);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--sp-3);
|
|
||||||
background: color-mix(in oklab, var(--surface-2) 50%, transparent);
|
background: color-mix(in oklab, var(--surface-2) 50%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.worktree legend {
|
.worktree .wt-toggle {
|
||||||
padding: 0 6px;
|
margin: 0 calc(var(--sp-3) * -1);
|
||||||
color: var(--ink);
|
padding-left: var(--sp-3);
|
||||||
font-size: var(--fs-13);
|
width: calc(100% + var(--sp-3) * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wt-summary {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--fs-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wt-collapsible {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding-bottom: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wt-collapsible[hidden] {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-error {
|
.form-error {
|
||||||
|
|||||||
Reference in New Issue
Block a user