Compare commits

..
Author SHA1 Message Date
Miguel Palhas e6ef236f89 fix(maestro): match the web form's distinct branch-field labels in the TUI (#94)
The draft pane's new-branch gutter was labelled "br:" in both branch
modes — since #94's web fix now says "New branch name" / "Pick a
branch", mirror the same distinction here: "new:" for the branch-name
input, "pick:" for the existing-branch list (which previously had no
label at all).
2026-08-24 15:44:06 +01:00
Miguel Palhas 5747c71800 fix(maestro): distinct labels for the worktree branch field (#94)
The new-branch name input and the existing-branch picker both had a
literal <label>Branch</label>, on top of "New branch"/"Existing branch"
mode radios — reading the form gave no clue what the field wanted.
Kept the mode radios (they name the choice, not the field) and gave
each field its own label: "New branch name" for the text input,
"Pick a branch" for the select.
2026-08-24 15:42:10 +01:00
Miguel Palhas df9013ffdf merge: two-step worktree picker in the TUI draft
Keeps both #93 and #96 in the draft layout: the worktree toggle and body
stay behind the Advanced disclosure, while the worktree choice step
belongs to the directory picker (#95 rule 3) and stays visible once a
project is picked.

Closes #96
2026-08-24 15:30:02 +01:00
Miguel Palhas 39341b278c merge: collapse the worktree section by default in both clients
Closes #93
2026-08-24 15:27:38 +01:00
Miguel Palhas cd66dbf7d8 Merge remote-tracking branch 'origin/blitz/spawn-v2' into spawn-v2/93-collapse-worktree
# Conflicts:
#	web/src/spawnForm.ts
2026-08-24 15:22:16 +01:00
Miguel Palhas 6bb812f6bd feat(maestro): collapse the TUI draft's worktree section by default (#93)
Mirrors the web spawn form: worktree/branch controls sit behind a new
"Advanced options" disclosure line (bracket [+]/[-], toggled by Enter
or Space, closed by default) instead of always showing the checkbox
and branch rows. The header states whether a worktree will be created
so #91's off-by-default for a worktree row is visible without
expanding. Collapsed fields drop out of Tab order; expanding only adds
rows below the disclosure, never moving the ones above it.
2026-08-24 15:20:35 +01:00
Miguel Palhas 5de04af36a feat(maestro): collapse the spawn form worktree section by default (#93)
Closed by default behind a disclosure reusing the sidebar folder-header
affordance (catalog.ts), since most spawns take the default target and
the section was the largest block in the form. The collapsed header
states whether a worktree will be created, so #91's off-by-default for
a worktree row stays visible without expanding.

Fixes a latent [hidden] + display:flex bug for the new .wt-collapsible
element (author CSS beats the UA [hidden] rule at equal specificity);
.wt-body has the same bug pre-existing and is out of scope here.
2026-08-24 15:15:47 +01:00
5 changed files with 339 additions and 64 deletions
+1 -3
View File
@@ -1349,9 +1349,7 @@ mod tests {
for c in "crimson-otter".chars() {
app.handle_draft_input(&InputKey::char(c));
}
app.handle_draft_input(&InputKey::Tab); // → worktree toggle
app.handle_draft_input(&InputKey::Tab); // → branch mode
app.handle_draft_input(&InputKey::Tab); // → new-branch name
app.handle_draft_input(&InputKey::Tab); // → advanced disclosure (collapsed, #93)
app.handle_draft_input(&InputKey::Tab); // → submit
match app.handle_draft_input(&InputKey::Enter).as_slice() {
+204 -35
View File
@@ -80,6 +80,9 @@ pub enum DraftField {
/// apply immediately, mirroring the web form's per-click choice.
WorktreeChoice,
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.
WorktreeToggle,
/// New-vs-existing branch source; Left / Right switches.
@@ -215,6 +218,10 @@ pub struct DraftPane {
/// Worktree on for this spawn (#65). Defaults on; a failed gitroot probe
/// unticks it, like the web form.
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
/// picked rows, `/api/gitroot` for raw paths. `None` until known.
pub base_repo: Option<PathBuf>,
@@ -273,6 +280,7 @@ impl Default for DraftPane {
name_touched: false,
needs_name_refill: false,
worktree_enabled: true,
advanced_collapsed: true,
base_repo: None,
probed_cwd: None,
probe_seq: 0,
@@ -511,6 +519,21 @@ impl DraftPane {
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
/// answer instead of re-probing.
pub fn toggle_worktree(&mut self) {
@@ -673,13 +696,16 @@ impl DraftPane {
order.push(DraftField::WorktreeChoice);
}
order.push(DraftField::Name);
order.push(DraftField::WorktreeToggle);
if self.worktree_body_visible() {
order.push(DraftField::BranchMode);
order.push(match self.branch_mode {
WorktreeBranchMode::New => DraftField::BranchNew,
WorktreeBranchMode::Existing => DraftField::BranchExisting,
});
order.push(DraftField::Advanced);
if !self.advanced_collapsed {
order.push(DraftField::WorktreeToggle);
if self.worktree_body_visible() {
order.push(DraftField::BranchMode);
order.push(match self.branch_mode {
WorktreeBranchMode::New => DraftField::BranchNew,
WorktreeBranchMode::Existing => DraftField::BranchExisting,
});
}
}
order.extend([DraftField::Submit]);
order
@@ -913,6 +939,10 @@ impl DraftPane {
self.select_entry();
DraftInputResult::Consumed
}
DraftField::Advanced => {
self.toggle_advanced();
DraftInputResult::Consumed
}
DraftField::WorktreeToggle => {
self.toggle_worktree();
DraftInputResult::Consumed
@@ -924,6 +954,14 @@ impl DraftPane {
DraftInputResult::Consumed
}
},
InputKey::Char {
c: ' ',
ctrl: false,
..
} if self.focused_field == DraftField::Advanced => {
self.toggle_advanced();
DraftInputResult::Consumed
}
InputKey::Char {
c: ' ',
ctrl: false,
@@ -1020,6 +1058,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.
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 wt_choice_rows_data = if show_wt_choice {
self.worktree_choice_rows()
@@ -1029,13 +1074,13 @@ impl DraftPane {
// Up to four choices (new + worktrees) stay visible around the
// selection.
let wt_choice_capacity = wt_choice_rows_data.len().clamp(1, 4) as u16;
let show_wt_body = self.worktree_body_visible();
// Up to three branch rows stay visible around the selection; held
// branches need their reason on screen, so they aren't collapsed.
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_raw)
+ u16::from(show_wt_toggle)
+ u16::from(show_wt_choice) * wt_choice_capacity
+ u16::from(show_wt_body) * (1 + if self.branch_mode == WorktreeBranchMode::New { 1 } else { branch_rows })
+ u16::from(self.error.is_some());
@@ -1053,7 +1098,10 @@ impl DraftPane {
constraints.push(Constraint::Length(wt_choice_capacity));
}
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 {
constraints.push(Constraint::Length(1)); // branch mode
match self.branch_mode {
@@ -1268,33 +1316,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) -----------------------------------------------------
let wt_row = next_row();
let hint = if self.cwd().is_none() {
Some("choose a directory first")
} else if self.repo_missing {
Some("not inside a git repository")
} else if !self.worktree_enabled {
Some("off — spawns in the checkout itself")
} else {
None
};
let mut tiles = vec![Span::raw(format!("{:<GUTTER$}", "wt:"))];
tiles.push(Span::styled(
if self.worktree_enabled {
"[x] worktree"
if show_wt_toggle {
let wt_row = next_row();
let hint = if self.cwd().is_none() {
Some("choose a directory first")
} else if self.repo_missing {
Some("not inside a git repository")
} else if !self.worktree_enabled {
Some("off — spawns in the checkout itself")
} else {
"[ ] worktree"
},
field_style(DraftField::WorktreeToggle),
));
if let Some(hint) = hint {
None
};
let mut tiles = vec![Span::raw(format!("{:<GUTTER$}", "wt:"))];
tiles.push(Span::styled(
format!(" {hint}"),
Style::default().add_modifier(Modifier::DIM),
if self.worktree_enabled {
"[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 {
// branch source: new (created) vs existing (checked out as-is)
@@ -1322,7 +1392,7 @@ impl DraftPane {
match self.branch_mode {
WorktreeBranchMode::New => {
let row = next_row();
draw_gutter(f, row, "br:");
draw_gutter(f, row, "new:");
let col =
draw_text_field(f, &self.branch, row, field_style(DraftField::BranchNew));
if self.focused_field == DraftField::BranchNew {
@@ -1346,6 +1416,7 @@ impl DraftPane {
.unwrap_or(0)
.min(20);
let list_row = next_row();
draw_gutter(f, list_row, "pick:");
for i in start..(start + capacity).min(len.max(1)) {
let y = list_row.y + (i - start) as u16;
if y >= list_row.y + list_row.height {
@@ -2229,7 +2300,10 @@ mod tests {
#[test]
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(
vec![repo_project(
"maestro",
@@ -2443,6 +2517,7 @@ mod tests {
fn render_shows_the_worktree_section() {
let mut draft = DraftPane {
home_dir: "/home/u".into(),
advanced_collapsed: false,
..DraftPane::default()
};
draft.set_projects(
@@ -2467,10 +2542,25 @@ mod tests {
draft.base_repo = Some(PathBuf::from("/home/u/tea/maestro"));
draft.probed_cwd = Some(PathBuf::from("/home/u/tea/maestro"));
let rendered = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
for s in ["[new] [existing]", "br:", "crimson-otter"] {
for s in ["[new] [existing]", "new:", "crimson-otter"] {
assert!(rendered.contains(s), "missing {s:?}:\n{rendered}");
}
// The new-branch input and existing-branch picker must not share a
// label — matching the web form's "New branch name" / "Pick a
// branch" (#94).
draft.set_branch_mode(WorktreeBranchMode::Existing);
let rendered = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
assert!(
rendered.contains("pick:"),
"missing pick: label:\n{rendered}"
);
assert!(
!rendered.contains("br:"),
"stale ambiguous label:\n{rendered}"
);
draft.set_branch_mode(WorktreeBranchMode::New);
draft.focused_field = DraftField::WorktreeToggle;
draft.on_input(&InputKey::Enter);
let rendered = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
@@ -2481,6 +2571,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]
fn abbreviate_path_matches_the_web_helpers() {
assert_eq!(
+45 -20
View File
@@ -152,33 +152,38 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
</div>
</div>
<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">
<input type="checkbox" name="worktree_enabled" />
Create git worktree
</label>
</legend>
<p class="wt-hint" hidden></p>
<div class="wt-body" hidden>
<p class="wt-repo"></p>
<div class="wt-mode" role="radiogroup" aria-label="Branch source">
<label class="row-inline">
<input type="radio" name="worktree_branch_mode" value="new" />
New branch
<p class="wt-hint" hidden></p>
<div class="wt-body" hidden>
<p class="wt-repo"></p>
<div class="wt-mode" role="radiogroup" aria-label="Branch source">
<label class="row-inline">
<input type="radio" name="worktree_branch_mode" value="new" />
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">
New branch name
<input name="worktree_branch" type="text" placeholder="feat/foo" autocomplete="off" />
</label>
<label class="row-inline">
<input type="radio" name="worktree_branch_mode" value="existing" />
Existing branch
<label class="wt-exist-wrap" hidden>
Pick a branch
<select class="branch-select"></select>
</label>
</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>
</fieldset>
<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 cwdRaw = form.querySelector(".cwd-raw") 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 wtBody = form.querySelector(".wt-body") as HTMLElement;
const wtRepo = form.querySelector(".wt-repo") as HTMLElement;
@@ -526,9 +534,11 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
markActiveChoice();
if (choice === "new") {
wtEnabled.checked = true;
updateWtSummary();
applyProjectRepo(group.main.base_repo ?? group.main.path);
} else {
wtEnabled.checked = false;
updateWtSummary();
inferredBaseRepo = null;
wtBody.hidden = true;
wtHint.hidden = true;
@@ -661,6 +671,18 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
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) --
// The project scan already reports repo membership, so a picked project
// 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
// no inferred repo and submitting fails.
wtEnabled.checked = false;
updateWtSummary();
emit();
wtBody.hidden = true;
wtHint.hidden = false;
@@ -814,6 +837,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
branchSelect.addEventListener("change", () => emit());
wtEnabled.addEventListener("change", () => {
updateWtSummary();
emit();
if (wtEnabled.checked) {
resolveBaseRepo();
@@ -827,6 +851,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
// The toggle defaults ON (#61): the operator almost always wants a
// worktree, and every non-repo target unticks it via the gitroot probe.
wtEnabled.checked = prefill.worktreeEnabled ?? true;
updateWtSummary();
if (prefill.worktreeBranch) branchInput.value = prefill.worktreeBranch;
setBranchMode(branchMode);
+68
View File
@@ -180,6 +180,74 @@ describe("spawnForm worktree defaults and branch picker (#61)", () => {
expect(modes.at(-1)).toBe("existing");
});
it("labels the new-branch input and existing-branch picker distinctly (#94)", async () => {
const form = await mount();
pickProject(form);
await drain();
const newLabel = form.querySelector<HTMLElement>(".wt-new-wrap")!.textContent!.trim();
const existLabel = form.querySelector<HTMLElement>(".wt-exist-wrap")!.textContent!.trim();
expect(newLabel).not.toBe("Branch");
expect(existLabel).not.toBe("Branch");
expect(newLabel).not.toBe(existLabel);
expect(newLabel).toMatch(/name/i);
expect(existLabel).toMatch(/pick|choose|select/i);
});
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 () => {
const gate: { release?: () => void } = {};
urls = stubFetch({ gateBranches: (r) => (gate.release = r) });
+21 -6
View File
@@ -1284,18 +1284,33 @@ textarea {
.worktree {
border: 1px solid var(--border);
border-radius: var(--r-2);
padding: var(--sp-3);
padding: 0 var(--sp-3);
margin: 0;
display: flex;
flex-direction: column;
gap: var(--sp-3);
background: color-mix(in oklab, var(--surface-2) 50%, transparent);
}
.worktree legend {
padding: 0 6px;
color: var(--ink);
font-size: var(--fs-13);
.worktree .wt-toggle {
margin: 0 calc(var(--sp-3) * -1);
padding-left: var(--sp-3);
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 {