Skip to content

feat(tui): panel section headers + fix focus arrow residue#123

Merged
yishuiliunian merged 1 commit intomainfrom
worktree-swift-dancing-oasis
Apr 21, 2026
Merged

feat(tui): panel section headers + fix focus arrow residue#123
yishuiliunian merged 1 commit intomainfrom
worktree-swift-dancing-oasis

Conversation

@yishuiliunian
Copy link
Copy Markdown
Contributor

Summary

  • Multi-panel display now shows ━━ Title (count) ━━━━ section headers when ≥2 sub-panels are visible (Agents / Tasks / Background / Scheduled), with the active one highlighted cyan.
  • Fixes a latent bug: Tab-switching between panels left stale arrows on the previously-focused panels. Only the active panel now renders ; other panels preserve section.focused state so Tab-back restores selection.
  • Refactors PanelProvider::item_ids to take state: &SessionState by parameter, eliminating a lock-reentrancy deadlock risk in the render path. Adds count(app, state) with zero-alloc overrides on all 4 providers for header integer display.

Changes

Production

  • src/panel_provider.rs — trait: item_ids(app, state) + count(app, state) default
  • src/render_panel.rs (new, 137 lines) — panel_zone_height + render_panel_zone with section headers + active-only focus scoping
  • src/views/panel_header.rs (new, 52 lines) — render_section_header with cyan/dim variants
  • src/views/mod.rsDIM_SEPARATOR shared color constant; adopted by separator.rs and panel_header.rs
  • src/providers/{agent,tasks,bg_tasks,crons}_provider.rstitle(), item_ids(app, state), zero-alloc count overrides
  • src/panel_ops.rs / src/tui_loop.rs / src/render.rs — call sites updated; lock once, pass &state in

Tests (3 new files, 20 tests)

  • panel_header_render_test.rs — header decoration / cyan-when-focused / single-panel-no-header / title+count rendering
  • panel_focus_visibility_test.rs — Tab-switch arrow exclusivity, Input-mode no arrows, state-preserved-across-render, single-panel-has-no-header
  • panel_provider_count_test.rs — per-provider count correctness + count() == item_ids().len() drift guard

Test plan

  • bazel build //... --config=clippy (zero warnings)
  • bazel build //... --config=rustfmt
  • bazel test //crates/loopal-tui:loopal-tui_test (passes, 1.1s)
  • bazel test //... (52/52 pass)
  • All .rs files ≤200 lines
  • CI passes

The panel zone stacks up to 4 sub-panels (Agents / Tasks / Background
/ Scheduled) with no visual separation — users saw "#2" and "bg_2"
run together and had no signal of what Tab would target. A latent
bug also left `▸` arrows on all previously-focused panels
simultaneously after Tab switches, because the renderer
unconditionally passed each panel's remembered `section.focused` id
to its view.

Fix both in the render layer:

- Draw a `━━ Title (count) ━━━━` row above each panel when ≥2 are
  visible, acting as label + divider. Active panel highlights cyan;
  inactive uses the existing `DIM_SEPARATOR` grey (extracted to a
  shared constant).
- Scope focused ids to `FocusMode::Panel(kind)` — non-active panels
  get `None` so their `▸` indicator is hidden. `section.focused`
  state is preserved so Tab-back restores the prior selection.

While there, close a lock-reentrancy smell that could silently
deadlock:

- `PanelProvider::item_ids` now takes `state: &SessionState`
  instead of calling `app.session.lock()` internally. The renderer
  already holds the guard, so the old `AgentPanelProvider::item_ids`
  deadlocked when `render_panel_zone` called it (surfaced as a test
  timeout, not yet observed in production).
- `count(app, state)` added with zero-alloc overrides on all 4
  providers — section headers need only the integer, not a
  throwaway `Vec<String>`.
- `panel_ops` / `tui_loop` call sites updated to lock once and pass
  `&state` in.

Tests: 3 new files / 20 tests cover section header rendering, Tab
focus visibility (including single-panel-no-header), and per-provider
count vs. `item_ids().len()` drift guard.
@yishuiliunian yishuiliunian merged commit b373903 into main Apr 21, 2026
4 checks passed
@yishuiliunian yishuiliunian deleted the worktree-swift-dancing-oasis branch April 21, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant