feat: configure sidebar entry gaps
This commit is contained in:
parent
b0d46fb9bc
commit
5b91dae12f
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
Loading…
Reference in New Issue