feat(tui): panel section headers + fix focus arrow residue#123
Merged
yishuiliunian merged 1 commit intomainfrom Apr 21, 2026
Merged
feat(tui): panel section headers + fix focus arrow residue#123yishuiliunian merged 1 commit intomainfrom
yishuiliunian merged 1 commit intomainfrom
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
━━ Title (count) ━━━━section headers when ≥2 sub-panels are visible (Agents / Tasks / Background / Scheduled), with the active one highlighted cyan.▸arrows on the previously-focused panels. Only the active panel now renders▸; other panels preservesection.focusedstate so Tab-back restores selection.PanelProvider::item_idsto takestate: &SessionStateby parameter, eliminating a lock-reentrancy deadlock risk in the render path. Addscount(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)defaultsrc/render_panel.rs(new, 137 lines) —panel_zone_height+render_panel_zonewith section headers + active-only focus scopingsrc/views/panel_header.rs(new, 52 lines) —render_section_headerwith cyan/dim variantssrc/views/mod.rs—DIM_SEPARATORshared color constant; adopted byseparator.rsandpanel_header.rssrc/providers/{agent,tasks,bg_tasks,crons}_provider.rs—title(),item_ids(app, state), zero-alloccountoverridessrc/panel_ops.rs/src/tui_loop.rs/src/render.rs— call sites updated; lock once, pass&stateinTests (3 new files, 20 tests)
panel_header_render_test.rs— header decoration / cyan-when-focused / single-panel-no-header / title+count renderingpanel_focus_visibility_test.rs— Tab-switch arrow exclusivity, Input-mode no arrows, state-preserved-across-render, single-panel-has-no-headerpanel_provider_count_test.rs— per-provider count correctness +count() == item_ids().len()drift guardTest plan
bazel build //... --config=clippy(zero warnings)bazel build //... --config=rustfmtbazel test //crates/loopal-tui:loopal-tui_test(passes, 1.1s)bazel test //...(52/52 pass).rsfiles ≤200 lines