feat(maestro-tui): trim chrome + walk sidebar in visual order (NAP-289)
ci / rust (push) Successful in 8m18s

Dogfood-driven layout fixes:

- `j`/`k` now walk the same daemon-then-sessions order the sidebar emits
  (`visible_sidebar_order`), so the highlight follows what the user sees
  instead of jumping by Uuid order across daemons.
- Tab bar collapses from 3 rows of box border to 1 row of plain text +
  1 row of horizontal rule. The rule doubles as the pane's focus indicator
  (cyan top border when `focus = Pane`, default otherwise). Saves ~4 rows
  above the pane before any content shows.
- Sidebar drops the full box border in favour of a single right-edge
  vertical rule (also cyan when focused). The "daemons" header now lives
  inline as a bold first line instead of in the block title.

Visible content area grows by ~2 cols + ~4 rows on the typical layout
without losing focus / panel-boundary affordances.

Workspace clippy + 85 unit tests passing.
This commit is contained in:
2026-05-29 10:10:56 +00:00
parent f208aa441d
commit 39dddb17da
3 changed files with 42 additions and 29 deletions
+15 -14
View File
@@ -193,24 +193,12 @@ impl App {
self.state.focus = Focus::Pane;
}
Action::SidebarNext => {
let order: Vec<Uuid> = self
.catalog
.snapshot()
.sessions
.iter()
.map(|s| s.id)
.collect();
let order = visible_sidebar_order(&self.catalog.snapshot());
self.state.move_sidebar_highlight(&order, 1);
self.state.focus = Focus::Sidebar;
}
Action::SidebarPrev => {
let order: Vec<Uuid> = self
.catalog
.snapshot()
.sessions
.iter()
.map(|s| s.id)
.collect();
let order = visible_sidebar_order(&self.catalog.snapshot());
self.state.move_sidebar_highlight(&order, -1);
self.state.focus = Focus::Sidebar;
}
@@ -432,6 +420,19 @@ impl App {
}
}
/// Order the sidebar walks for `j`/`k`: each daemon's sessions, in the same
/// order [`crate::sidebar::Sidebar::render`] emits them. Keeps highlight
/// movement in visual sync with what the user sees.
fn visible_sidebar_order(snap: &crate::catalog::CatalogSnapshot) -> Vec<Uuid> {
let mut out = Vec::with_capacity(snap.sessions.len());
for d in &snap.daemons {
for s in snap.sessions.iter().filter(|s| s.daemon_id == d.daemon_id) {
out.push(s.id);
}
}
out
}
fn ws_base(http: &str) -> String {
if let Some(rest) = http.strip_prefix("http://") {
format!("ws://{rest}")
+18 -9
View File
@@ -103,26 +103,35 @@ pub fn draw(f: &mut Frame, inputs: RenderInputs<'_>) -> RegionMap {
map.sidebar = Some(sidebar_area);
}
// 1-row tab bar + 1-row horizontal rule (doubles as the pane's focus
// indicator) + flex pane area below. Saves the 4 rows the old
// `Length(3) + Borders::ALL` layout was eating before any content showed.
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)])
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(right);
let tab_area = rows[0];
let pane_area = rows[1];
let separator_area = rows[1];
let pane_area = rows[2];
inputs.tab_bar.render(f, tab_area);
map.tab_bar = Some(tab_area);
let pane_focused = inputs.state.focus == Focus::Pane;
let pane_block = Block::default()
.borders(Borders::ALL)
.border_style(focus_style(pane_focused));
let pane_inner = pane_block.inner(pane_area);
f.render_widget(pane_block, pane_area);
f.render_widget(
Block::default()
.borders(Borders::TOP)
.border_style(focus_style(pane_focused)),
separator_area,
);
if let Some(p) = inputs.pane {
p.render(f, pane_inner);
p.render(f, pane_area);
}
map.pane = Some(pane_inner);
map.pane = Some(pane_area);
// Status bar — always visible, never eclipsed by overlays so the user can
// always see the current mode + key hints.
+9 -6
View File
@@ -26,9 +26,10 @@ impl Sidebar {
self.render_with_focus(f, area, false);
}
/// Same as [`Sidebar::render`] but draws the border in the focus colour
/// when `focused`. Used by the top-level orchestrator to surface which
/// panel currently consumes navigation keys.
/// Same as [`Sidebar::render`] but tints the right-edge separator in the
/// focus colour when `focused`. Box borders eat too many rows/cols for
/// the v1 layout — a single vertical rule keeps the panel boundary
/// without stealing space.
pub fn render_with_focus(&self, f: &mut Frame, area: Rect, focused: bool) {
let border_style = if focused {
Style::default().fg(Color::Cyan)
@@ -36,13 +37,15 @@ impl Sidebar {
Style::default()
};
let block = Block::default()
.title(" daemons ")
.borders(Borders::ALL)
.borders(Borders::RIGHT)
.border_style(border_style);
let inner = block.inner(area);
f.render_widget(block, area);
let mut lines: Vec<Line> = Vec::new();
let mut lines: Vec<Line> = vec![Line::from(Span::styled(
"daemons",
Style::default().add_modifier(Modifier::BOLD),
))];
for d in &self.daemons {
lines.push(Line::from(format!("● {}", d.hostname)));
for s in self.sessions.iter().filter(|s| s.daemon_id == d.daemon_id) {