feat: configure sidebar entry gaps

This commit is contained in:
Ogulcan Celik 2026-07-14 20:40:11 +03:00
parent b0d46fb9bc
commit 5b91dae12f
12 changed files with 230 additions and 42 deletions

View File

@ -3,9 +3,13 @@
## Unreleased
### Added
- Added independent `row_gap` settings for expanded Space and Agent sidebar entries. (#873)
- Copy mode now supports literal smart-case search with `/` and `?`, repeating with `n` and `N`, match highlighting, and tmux-style cross-line `w`/`b`/`e` word motions. (#1230)
- Added maki detection with idle, working, and blocked screen states. (#1301, thanks @tontinton)
### Changed
- Expanded Space and Agent sidebar entries now use a packed layout by default; set the corresponding `row_gap` to `1` to restore the previous spacing. (#873)
### Fixed
- Outer-terminal focus gained and lost reports now reach the focused pane when its application enables focus reporting, restoring Neovim file autoreload and other focus-aware terminal behavior. (#1337)
- Native Windows servers now detach from the terminal console that launched them, so closing WezTerm, Windows Terminal, or another host terminal no longer stops persistent pane processes. (#1329)

View File

@ -236,12 +236,14 @@ The expanded desktop sidebar renders each inner array in `rows` as one line. The
```toml
[ui.sidebar.agents]
row_gap = 0
rows = [
["state_icon", "workspace", "tab"],
["agent"],
]
[ui.sidebar.spaces]
row_gap = 0
rows = [
["state_icon", "workspace"],
["branch", "git_status"],
@ -271,6 +273,8 @@ Space rows accept these built-in tokens:
Tokens render in their configured order. Herdr normally separates adjacent values with ` · ` and uses a single space after `state_icon`. Missing values and their separators disappear; a row disappears when none of its tokens have a value. Each layout may contain at most 16 rows, with at most 16 tokens in each row.
`row_gap` controls the blank terminal rows between entries, independently for the Agent and Space panels. It defaults to `0`, which packs entries together; set it to `1` to restore the previous spacing. It does not add spacing between the content lines declared in `rows`. Consecutive indented worktree children remain packed as one Space group.
Override the complete Agent layout for a known agent under `rows_by_agent`:
```toml

View File

@ -235,12 +235,14 @@ yellow = "#f9e2af"
```toml
[ui.sidebar.agents]
row_gap = 0
rows = [
["state_icon", "workspace", "tab"],
["agent"],
]
[ui.sidebar.spaces]
row_gap = 0
rows = [
["state_icon", "workspace"],
["branch", "git_status"],
@ -270,6 +272,8 @@ Space の行では、次の組み込みトークンを使えます:
トークンは設定された順序で描画されます。Herdr は通常、隣接する値を ` · ` で区切り、`state_icon` の後には空白を 1 つ入れます。値がない場合、その値と区切りは表示されません。すべてのトークンに値がない行は表示されません。各レイアウトは最大 16 行、各行は最大 16 トークンです。
`row_gap` は、Agent パネルと Space パネルごとに、エントリ間の空白行数を指定します。デフォルトは `0` で、エントリを詰めて表示します。以前の間隔に戻すには `1` に設定します。`rows` で定義したコンテンツ行の間隔には影響しません。連続するインデントされた worktree の子は、1 つの Space グループとして詰めて表示されます。
既知のエージェントについて Agent の完全なレイアウトを上書きするには、`rows_by_agent` を使います:
```toml

View File

@ -235,12 +235,14 @@ yellow = "#f9e2af"
```toml
[ui.sidebar.agents]
row_gap = 0
rows = [
["state_icon", "workspace", "tab"],
["agent"],
]
[ui.sidebar.spaces]
row_gap = 0
rows = [
["state_icon", "workspace"],
["branch", "git_status"],
@ -270,6 +272,8 @@ Space 行支持以下内置 token
token 会按配置顺序渲染。Herdr 通常使用 ` · ` 分隔相邻值,并在 `state_icon` 后使用一个空格。缺失值及其分隔符会消失;当一行中的所有 token 都没有值时,该行会消失。每个布局最多可包含 16 行,每行最多可包含 16 个 token。
`row_gap` 分别控制 Agent 和 Space 面板中条目之间的空白终端行数。默认值为 `0`,会紧密排列条目;设为 `1` 可恢复之前的间距。它不会在 `rows` 声明的内容行之间添加间距。连续缩进的 worktree 子项仍会作为一个 Space 组紧密排列。
在 `rows_by_agent` 下为已知智能体覆盖完整的 Agent 布局:
```toml

View File

@ -668,6 +668,12 @@
"priority"
]
},
{
"key": "ui.sidebar.agents.row_gap",
"type": "integer",
"default": "0",
"description": "Blank terminal rows between expanded Agent sidebar entries. Set to 1 to restore the previous spacing."
},
{
"key": "ui.sidebar.agents.rows",
"type": "list of token rows",
@ -680,6 +686,12 @@
"default": "{}",
"description": "Complete Agent-row overrides keyed by strict canonical agent id. Agents without an override use ui.sidebar.agents.rows."
},
{
"key": "ui.sidebar.spaces.row_gap",
"type": "integer",
"default": "0",
"description": "Blank terminal rows between expanded Space sidebar entries. Set to 1 to restore the previous spacing. Consecutive indented worktree children remain packed as one group."
},
{
"key": "ui.sidebar.spaces.rows",
"type": "list of token rows",

View File

@ -448,21 +448,20 @@ impl AppState {
}
let mut row_y = body.y;
for detail in crate::ui::agent_panel_entries(self)
.into_iter()
.skip(self.agent_panel_scroll)
{
let height = crate::ui::agent_entry_height_in_body(self, &detail, body.height);
if row_y.saturating_add(height) > body.y + body.height {
let body_bottom = body.y + body.height;
let entries = crate::ui::agent_panel_entries(self);
for (index, detail) in entries.iter().enumerate().skip(self.agent_panel_scroll) {
let height = crate::ui::agent_entry_height_in_body(self, detail, body.height);
if row_y.saturating_add(height) > body_bottom {
break;
}
if row >= row_y && row < row_y.saturating_add(height) {
return Some((detail.ws_idx, detail.tab_idx, detail.pane_id));
}
row_y = row_y.saturating_add(height);
if row_y < body.y + body.height {
row_y = row_y.saturating_add(1);
}
row_y = row_y
.saturating_add(height)
.saturating_add(crate::ui::agent_entry_gap(self, index, entries.len()))
.min(body_bottom);
}
None
}
@ -721,6 +720,7 @@ mod tests {
vec![crate::config::AgentSidebarToken::Workspace],
],
);
app.state.sidebar_agents.row_gap = 1;
let detail_area = app.state.agent_panel_rect();
let metrics = crate::ui::agent_panel_scroll_metrics(&app.state, detail_area);
let body = crate::ui::agent_panel_body_rect(
@ -737,6 +737,12 @@ mod tests {
app.state.agent_detail_target_at(body.y + 3),
Some((1, 0, second_pane))
);
app.state.sidebar_agents.row_gap = 0;
assert_eq!(
app.state.agent_detail_target_at(body.y + 1),
Some((1, 0, second_pane))
);
}
#[test]
@ -1219,11 +1225,19 @@ mod tests {
Workspace::test_new("b"),
Workspace::test_new("c"),
];
app.state.sidebar_spaces.rows = vec![vec![crate::config::SpaceSidebarToken::Workspace]];
app.state.sidebar_spaces.row_gap = 0;
let active_id = app.state.workspaces[1].id.clone();
let selected_id = app.state.workspaces[2].id.clone();
app.state.active = Some(1);
app.state.selected = 2;
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
let packed_boundary_row = app.state.view.workspace_card_areas[1].rect.y;
assert_eq!(
app.state.workspace_drop_index_at_row(packed_boundary_row),
Some(2)
);
let source_row = app.state.view.workspace_card_areas[1].rect.y;
let target_row = crate::ui::workspace_drop_indicator_row(
&app.state.view.workspace_card_areas,
@ -1454,6 +1468,7 @@ mod tests {
.get_mut(&second_terminal_id)
.unwrap()
.cwd = second_repo.clone();
app.state.sidebar_spaces.row_gap = 1;
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
assert_eq!(app.state.workspace_drop_index_at_row(0), Some(0));
@ -1473,7 +1488,7 @@ mod tests {
Workspace::test_new("b"),
Workspace::test_new("c"),
];
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 24));
let cards = &app.state.view.workspace_card_areas;
let bottom_slot = crate::ui::workspace_drop_indicator_row(

View File

@ -2714,7 +2714,7 @@ mod tests {
std::fs::write(
&path,
"[ui.sidebar.agents]\nrows = [[\"state_icon\", \"$summary\"]]\n\n[ui.sidebar.agents.rows_by_agent]\nclaude = [[\"terminal_title_stripped\"]]\n\n[ui.sidebar.spaces]\nrows = [[\"workspace\", \"$jj_status\"]]\n",
"[ui.sidebar.agents]\nrows = [[\"state_icon\", \"$summary\"]]\nrow_gap = 1\n\n[ui.sidebar.agents.rows_by_agent]\nclaude = [[\"terminal_title_stripped\"]]\n\n[ui.sidebar.spaces]\nrows = [[\"workspace\", \"$jj_status\"]]\nrow_gap = 3\n",
)
.unwrap();
app.state.agent_panel_scroll = 5;
@ -2735,6 +2735,7 @@ mod tests {
crate::config::AgentSidebarToken::TerminalTitleStripped,
]]
);
assert_eq!(app.state.sidebar_agents.row_gap, 1);
assert_eq!(
app.state.sidebar_spaces.rows,
vec![vec![
@ -2742,6 +2743,7 @@ mod tests {
crate::config::SpaceSidebarToken::Custom("jj_status".into()),
]]
);
assert_eq!(app.state.sidebar_spaces.row_gap, 3);
let previous_agents = app.state.sidebar_agents.clone();
std::fs::write(

View File

@ -6,6 +6,7 @@ use crate::detect::Agent;
const MAX_SIDEBAR_ROWS: usize = 16;
const MAX_SIDEBAR_TOKENS_PER_ROW: usize = 16;
const DEFAULT_SIDEBAR_ROW_GAP: u16 = 0;
fn deserialize_sidebar_rows<'de, D, T>(deserializer: D) -> Result<Vec<Vec<T>>, D::Error>
where
@ -198,6 +199,7 @@ pub struct AgentsSidebarConfig {
pub rows: AgentSidebarRows,
#[serde(default, deserialize_with = "deserialize_rows_by_agent")]
pub rows_by_agent: BTreeMap<String, AgentSidebarRows>,
pub row_gap: u16,
}
impl AgentsSidebarConfig {
@ -220,6 +222,7 @@ impl Default for AgentsSidebarConfig {
vec![AgentSidebarToken::Agent],
],
rows_by_agent: BTreeMap::new(),
row_gap: DEFAULT_SIDEBAR_ROW_GAP,
}
}
}
@ -229,6 +232,7 @@ impl Default for AgentsSidebarConfig {
pub struct SpacesSidebarConfig {
#[serde(deserialize_with = "deserialize_sidebar_rows")]
pub rows: SpaceSidebarRows,
pub row_gap: u16,
}
impl Default for SpacesSidebarConfig {
@ -238,6 +242,7 @@ impl Default for SpacesSidebarConfig {
vec![SpaceSidebarToken::StateIcon, SpaceSidebarToken::Workspace],
vec![SpaceSidebarToken::Branch, SpaceSidebarToken::GitStatus],
],
row_gap: DEFAULT_SIDEBAR_ROW_GAP,
}
}
}
@ -268,6 +273,7 @@ mod tests {
]
);
assert!(config.agents.rows_by_agent.is_empty());
assert_eq!(config.agents.row_gap, 0);
assert_eq!(
config.spaces.rows,
vec![
@ -275,6 +281,7 @@ mod tests {
vec![SpaceSidebarToken::Branch, SpaceSidebarToken::GitStatus],
]
);
assert_eq!(config.spaces.row_gap, 0);
}
#[test]
@ -283,12 +290,14 @@ mod tests {
r#"
[ui.sidebar.agents]
rows = [["state_icon", "workspace"], ["state_text", "agent", "$summary"], ["terminal_title", "terminal_title_stripped", "$terminal_title"]]
row_gap = 1
[ui.sidebar.agents.rows_by_agent]
claude = [["terminal_title_stripped"], ["agent", "$model"]]
[ui.sidebar.spaces]
rows = [["workspace"], ["$jj_status"]]
row_gap = 3
"#,
)
.expect("sidebar token config");
@ -319,10 +328,12 @@ rows = [["workspace"], ["$jj_status"]]
],
]
);
assert_eq!(config.ui.sidebar.agents.row_gap, 1);
assert_eq!(
config.ui.sidebar.spaces.rows[1],
vec![SpaceSidebarToken::Custom("jj_status".into())]
);
assert_eq!(config.ui.sidebar.spaces.row_gap, 3);
}
#[test]

View File

@ -309,6 +309,8 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
# terminal_title, and terminal_title_stripped.
# Custom values reported through pane metadata use a $name token.
# [ui.sidebar.agents]
# Blank rows between agent entries. Set to 1 to restore the previous spacing.
# row_gap = 0
# rows = [["state_icon", "workspace", "tab"], ["agent"]]
# Optional canonical agent IDs replace the default rows for matching agents.
# [ui.sidebar.agents.rows_by_agent]
@ -317,6 +319,8 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
# Expanded space rows. Built-ins are state_icon, state_text, workspace, branch, and git_status.
# Custom values reported through workspace metadata use a $name token, for example $jj_status.
# [ui.sidebar.spaces]
# Blank rows between space entries. Set to 1 to restore the previous spacing.
# row_gap = 0
# rows = [["state_icon", "workspace"], ["branch", "git_status"]]
# Accent color for highlights, borders, and navigation UI.

View File

@ -69,7 +69,7 @@ pub(crate) use self::{
SETTINGS_POPUP_WIDTH,
},
sidebar::{
agent_entry_height_in_body, agent_panel_body_rect, agent_panel_entries,
agent_entry_gap, agent_entry_height_in_body, agent_panel_body_rect, agent_panel_entries,
agent_panel_scroll_for_target, agent_panel_scroll_metrics, agent_panel_scrollbar_rect,
agent_panel_toggle_rect, collapsed_sidebar_sections, collapsed_sidebar_toggle_rect,
compute_workspace_card_areas, expanded_sidebar_sections, expanded_sidebar_toggle_rect,

View File

@ -218,11 +218,19 @@ fn workspace_row_height_in_body(
workspace_row_height(app, workspace, indented).min(body_height)
}
fn workspace_entry_gap(entries: &[WorkspaceListEntry], entry_idx: usize, indented: bool) -> u16 {
u16::from(
entry_idx + 1 < entries.len()
&& !(indented && next_entry_is_indented_workspace(entries, entry_idx)),
)
fn workspace_entry_gap(
app: &AppState,
entries: &[WorkspaceListEntry],
entry_idx: usize,
indented: bool,
) -> u16 {
if entry_idx + 1 < entries.len()
&& !(indented && next_entry_is_indented_workspace(entries, entry_idx))
{
app.sidebar_spaces.row_gap
} else {
0
}
}
fn workspace_attention_priority(state: AgentState, seen: bool) -> u8 {
@ -456,7 +464,7 @@ fn workspace_list_visible_count(app: &AppState, area: Rect, scroll: usize) -> us
};
(
workspace_row_height_in_body(app, ws, *indented, body.height),
workspace_entry_gap(&entries, entry_idx, *indented),
workspace_entry_gap(app, &entries, entry_idx, *indented),
)
}
};
@ -465,9 +473,7 @@ fn workspace_list_visible_count(app: &AppState, area: Rect, scroll: usize) -> us
}
used_rows = used_rows.saturating_add(row_height);
visible += 1;
if gap > 0 && used_rows < body.height {
used_rows = used_rows.saturating_add(1);
}
used_rows = used_rows.saturating_add(gap).min(body.height);
}
visible
}
@ -482,7 +488,7 @@ fn workspace_list_bottom_start(app: &AppState, area: Rect) -> usize {
let Some(workspace) = app.workspaces.get(*ws_idx) else {
continue;
};
let gap = workspace_entry_gap(&entries, entry_idx, *indented);
let gap = workspace_entry_gap(app, &entries, entry_idx, *indented);
let needed = workspace_row_height_in_body(app, workspace, *indented, body.height)
.saturating_add(gap);
if used_rows.saturating_add(needed) > body.height {
@ -552,6 +558,14 @@ pub(crate) fn agent_entry_height_in_body(
.min(body_height)
}
pub(crate) fn agent_entry_gap(app: &AppState, entry_idx: usize, entry_count: usize) -> u16 {
if entry_idx + 1 < entry_count {
app.sidebar_agents.row_gap
} else {
0
}
}
fn agent_panel_visible_count_from(app: &AppState, area: Rect, scroll: usize) -> usize {
let body = agent_panel_body_rect(area, false);
if body.width == 0 || body.height == 0 {
@ -560,16 +574,17 @@ fn agent_panel_visible_count_from(app: &AppState, area: Rect, scroll: usize) ->
let mut used_rows = 0u16;
let mut visible = 0usize;
for entry in agent_panel_entries(app).iter().skip(scroll) {
let entries = agent_panel_entries(app);
for (index, entry) in entries.iter().enumerate().skip(scroll) {
let height = agent_entry_height_in_body(app, entry, body.height);
if used_rows.saturating_add(height) > body.height {
break;
}
used_rows = used_rows.saturating_add(height);
visible += 1;
if used_rows < body.height {
used_rows = used_rows.saturating_add(1);
}
used_rows = used_rows
.saturating_add(agent_entry_gap(app, index, entries.len()))
.min(body.height);
}
visible
}
@ -580,7 +595,7 @@ fn agent_panel_bottom_start(app: &AppState, area: Rect) -> usize {
let mut used_rows = 0u16;
let mut start = entries.len();
for (index, entry) in entries.iter().enumerate().rev() {
let gap = u16::from(index + 1 < entries.len());
let gap = agent_entry_gap(app, index, entries.len());
let needed = agent_entry_height_in_body(app, entry, body.height).saturating_add(gap);
if used_rows.saturating_add(needed) > body.height {
break;
@ -664,7 +679,7 @@ pub(crate) fn compute_workspace_list_areas(
continue;
};
let row_height = workspace_row_height_in_body(app, ws, *indented, body.height);
let gap = workspace_entry_gap(&entries, entry_idx, *indented);
let gap = workspace_entry_gap(app, &entries, entry_idx, *indented);
if row_y.saturating_add(row_height) > body_bottom {
break;
}
@ -673,10 +688,10 @@ pub(crate) fn compute_workspace_list_areas(
rect: Rect::new(body.x, row_y, body.width, row_height),
indented: *indented,
});
row_y = row_y.saturating_add(row_height);
if gap > 0 && row_y < body_bottom {
row_y = row_y.saturating_add(1);
}
row_y = row_y
.saturating_add(row_height)
.saturating_add(gap)
.min(body_bottom);
}
}
}
@ -1298,7 +1313,7 @@ fn render_agent_detail(
let mut row_y = body.y;
let body_bottom = body.y + body.height;
for detail in details.iter().skip(app.agent_panel_scroll) {
for (index, detail) in details.iter().enumerate().skip(app.agent_panel_scroll) {
let label_color = state_label_color(detail.state, detail.seen, p);
let rows = resolved_agent_rows(app, detail);
let height = (rows.len().max(1) as u16).min(body.height);
@ -1343,10 +1358,10 @@ fn render_agent_detail(
Rect::new(body.x, row_y + row_index as u16, body.width, 1),
);
}
row_y = row_y.saturating_add(height);
if row_y < body_bottom {
row_y += 1;
}
row_y = row_y
.saturating_add(height)
.saturating_add(agent_entry_gap(app, index, details.len()))
.min(body_bottom);
}
if let Some(track) = scrollbar_rect {
@ -1445,6 +1460,36 @@ mod tests {
assert!(!second.contains("working"));
}
#[test]
fn default_agent_row_gap_packs_rendering_and_scroll_geometry() {
let mut app = crate::app::state::AppState::test_new();
app.workspaces = vec![Workspace::test_new("one"), Workspace::test_new("two")];
app.ensure_test_terminals();
for (workspace, agent) in app.workspaces.iter().zip([Agent::Pi, Agent::Claude]) {
let pane_id = workspace.tabs[0].root_pane;
let terminal_id = workspace.tabs[0].panes[&pane_id]
.attached_terminal_id
.clone();
app.terminals.get_mut(&terminal_id).unwrap().detected_agent = Some(agent);
}
app.sidebar_agents.rows = vec![vec![crate::config::AgentSidebarToken::Agent]];
assert_eq!(app.sidebar_agents.row_gap, 0);
let area = Rect::new(0, 0, 20, 5);
let metrics = agent_panel_scroll_metrics(&app, area);
let body = agent_panel_body_rect(area, false);
let mut terminal = Terminal::new(TestBackend::new(20, 5)).unwrap();
terminal
.draw(|frame| render_agent_detail(&app, &TerminalRuntimeRegistry::new(), frame, area))
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(metrics.viewport_rows, 2);
assert_eq!(metrics.max_offset_from_bottom, 0);
assert_eq!(row_text(buffer, body.y, body.width), " pi");
assert_eq!(row_text(buffer, body.y + 1, body.width), " claude");
}
#[test]
fn narrow_agent_rows_preserve_later_tab_tokens() {
let mut app = crate::app::state::AppState::test_new();
@ -2020,6 +2065,7 @@ mod tests {
workspace_with_worktree_space("main", Some("repo-key"), "/repo/herdr"),
workspace_with_worktree_space("issue", Some("repo-key"), "/repo/herdr-issue"),
];
app.sidebar_spaces.row_gap = 1;
let (cards, headers) = compute_workspace_list_areas(&app, Rect::new(0, 0, 30, 20));
@ -2031,6 +2077,87 @@ mod tests {
assert_eq!(cards[1].rect.y, cards[0].rect.y + cards[0].rect.height + 1);
}
#[test]
fn space_row_gap_preserves_compact_worktree_children() {
let mut app = AppState::test_new();
app.workspaces = vec![
workspace_with_worktree_space("main", Some("repo-key"), "/repo/herdr"),
workspace_with_worktree_space("issue", Some("repo-key"), "/repo/herdr-issue"),
workspace_with_worktree_space("review", Some("repo-key"), "/repo/herdr-review"),
Workspace::test_new("notes"),
];
app.sidebar_spaces.rows = vec![vec![crate::config::SpaceSidebarToken::Workspace]];
app.sidebar_spaces.row_gap = 2;
let (spacious, _) = compute_workspace_list_areas(&app, Rect::new(0, 0, 30, 30));
assert_eq!(
spacious[1].rect.y,
spacious[0].rect.y + spacious[0].rect.height + 2
);
assert_eq!(
spacious[2].rect.y,
spacious[1].rect.y + spacious[1].rect.height
);
assert_eq!(
spacious[3].rect.y,
spacious[2].rect.y + spacious[2].rect.height + 2
);
let spacious_metrics = workspace_list_scroll_metrics(&app, Rect::new(0, 0, 30, 7));
assert_eq!(spacious_metrics.viewport_rows, 2);
assert_eq!(spacious_metrics.max_offset_from_bottom, 2);
app.sidebar_spaces.row_gap = 0;
let (packed, _) = compute_workspace_list_areas(&app, Rect::new(0, 0, 30, 30));
assert!(packed
.windows(2)
.all(|pair| pair[1].rect.y == pair[0].rect.y + pair[0].rect.height));
let packed_metrics = workspace_list_scroll_metrics(&app, Rect::new(0, 0, 30, 7));
assert_eq!(packed_metrics.viewport_rows, 4);
assert_eq!(packed_metrics.max_offset_from_bottom, 0);
}
#[test]
fn packed_workspace_drag_indicator_overlays_an_internal_boundary() {
let mut app = AppState::test_new();
app.workspaces = vec![
Workspace::test_new("a"),
Workspace::test_new("b"),
Workspace::test_new("c"),
];
app.sidebar_spaces.rows = vec![vec![crate::config::SpaceSidebarToken::Workspace]];
app.sidebar_spaces.row_gap = 0;
let area = Rect::new(0, 0, 30, 20);
app.view.workspace_card_areas = compute_workspace_card_areas(&app, area);
let list_area = workspace_list_rect(area, app.sidebar_section_split);
let indicator_row =
workspace_drop_indicator_row(&app.view.workspace_card_areas, list_area, 2).unwrap();
assert_eq!(indicator_row, app.view.workspace_card_areas[1].rect.y);
app.drag = Some(crate::app::state::DragState {
target: crate::app::state::DragTarget::WorkspaceReorder {
source_ws_idx: 0,
insert_idx: Some(2),
},
});
let mut terminal = Terminal::new(TestBackend::new(area.width, area.height)).unwrap();
terminal
.draw(|frame| {
render_workspace_list(
&app,
&TerminalRuntimeRegistry::new(),
frame,
list_area,
false,
)
})
.unwrap();
assert_eq!(
terminal.backend().buffer()[(list_area.x, indicator_row)].symbol(),
""
);
}
#[test]
fn linked_only_worktree_members_do_not_form_parentless_group() {
let mut app = AppState::test_new();

View File

@ -153,7 +153,7 @@ mod tests {
vec![AgentSidebarToken::Custom("missing".into())],
vec![AgentSidebarToken::Agent],
],
rows_by_agent: Default::default(),
..Default::default()
};
let rows = agent_rows(&config, &entry, "working");
@ -174,7 +174,7 @@ mod tests {
AgentSidebarToken::StateText,
AgentSidebarToken::Custom("summary".into()),
]],
rows_by_agent: Default::default(),
..Default::default()
};
assert_eq!(
@ -200,7 +200,7 @@ mod tests {
AgentSidebarToken::TerminalTitleStripped,
AgentSidebarToken::Custom("terminal_title".into()),
]],
rows_by_agent: Default::default(),
..Default::default()
};
assert_eq!(
@ -217,7 +217,7 @@ mod tests {
fn known_agent_override_replaces_default_rows() {
let mut config = AgentsSidebarConfig {
rows: vec![vec![AgentSidebarToken::Workspace]],
rows_by_agent: Default::default(),
..Default::default()
};
config
.rows_by_agent
@ -265,6 +265,7 @@ mod tests {
let tokens = std::collections::HashMap::from([("jj_status".into(), "2 changes".into())]);
let config = SpacesSidebarConfig {
rows: vec![vec![SpaceSidebarToken::Custom("jj_status".into())]],
..Default::default()
};
assert_eq!(