feat(tui): enable multi-component focus and viewport scrolling for Tab 8 and Box panels

This commit is contained in:
operator
2026-10-10 13:04:24 -04:00
parent f1252423a8
commit e6e5616de6
2 changed files with 835 additions and 107 deletions
+665 -85
View File
File diff suppressed because it is too large Load Diff
+148
View File
@@ -433,6 +433,154 @@ class TestBoxTUIHeadless(unittest.TestCase):
fleet_app._render_footer(h, w) fleet_app._render_footer(h, w)
fleet_app._render_help(h, w) fleet_app._render_help(h, w)
def test_work_pipeline_multi_component_focus_cycling_and_navigation(self):
app = muse_tui.MuseTUI(self.mock_stdscr, initial_mode="box", initial_tab="work")
app.box_tab = 7
# 1. Section cycling with Tab, Shift-Tab, l, h
self.assertEqual(app.work_focus_component, "tickets")
app._handle_key(ord('\t'))
self.assertEqual(app.work_focus_component, "prs")
app._handle_key(ord('\t'))
self.assertEqual(app.work_focus_component, "workers")
app._handle_key(ord('\t'))
self.assertEqual(app.work_focus_component, "tickets")
app._handle_key(curses.KEY_BTAB)
self.assertEqual(app.work_focus_component, "workers")
app._handle_key(curses.KEY_BTAB)
self.assertEqual(app.work_focus_component, "prs")
app._handle_key(ord('l'))
self.assertEqual(app.work_focus_component, "workers")
app._handle_key(ord('h'))
# If on workers, 'h' heals unless not on workers or cycled
app.work_focus_component = "prs"
app._handle_key(ord('h'))
self.assertEqual(app.work_focus_component, "tickets")
# 2. Navigation with j/k, PageUp/Down, Home/End
app.data.work_issues_cache = [{"number": i, "title": f"Issue {i}", "state": "open"} for i in range(10)]
app.data.work_prs_cache = [{"number": i, "title": f"PR {i}", "state": "open", "merged": False} for i in range(8)]
# On tickets
app.work_focus_component = "tickets"
app.work_tickets_sel_idx = 0
app._handle_key(ord('j'))
self.assertEqual(app.work_tickets_sel_idx, 1)
app._handle_key(curses.KEY_DOWN)
self.assertEqual(app.work_tickets_sel_idx, 2)
app._handle_key(ord('k'))
self.assertEqual(app.work_tickets_sel_idx, 1)
app._handle_key(curses.KEY_UP)
self.assertEqual(app.work_tickets_sel_idx, 0)
app._handle_key(curses.KEY_NPAGE) # PageDown (+5)
self.assertEqual(app.work_tickets_sel_idx, 5)
app._handle_key(curses.KEY_PPAGE) # PageUp (-5)
self.assertEqual(app.work_tickets_sel_idx, 0)
app._handle_key(ord('G')) # End
self.assertEqual(app.work_tickets_sel_idx, 9)
app._handle_key(ord('g')) # Home
self.assertEqual(app.work_tickets_sel_idx, 0)
# On PRs
app.work_focus_component = "prs"
app.work_prs_sel_idx = 0
app._handle_key(ord('j'))
self.assertEqual(app.work_prs_sel_idx, 1)
app._handle_key(ord('k'))
self.assertEqual(app.work_prs_sel_idx, 0)
app._handle_key(ord('G'))
self.assertEqual(app.work_prs_sel_idx, 7)
app._handle_key(ord('g'))
self.assertEqual(app.work_prs_sel_idx, 0)
# On workers
app.work_focus_component = "workers"
app.work_sel_idx = 0
app._handle_key(ord('j'))
self.assertEqual(app.work_sel_idx, 1)
app._handle_key(ord('k'))
self.assertEqual(app.work_sel_idx, 0)
app._handle_key(ord('G'))
self.assertEqual(app.work_sel_idx, 6)
app._handle_key(ord('g'))
self.assertEqual(app.work_sel_idx, 0)
def test_work_pipeline_mouse_bounds_and_section_selection(self):
app = muse_tui.MuseTUI(self.mock_stdscr, initial_mode="box", initial_tab="work")
app.box_tab = 7
app.data.work_issues_cache = [{"number": i, "title": f"Ticket {i}", "state": "open"} for i in range(12)]
app.data.work_prs_cache = [{"number": i, "title": f"PR {i}", "state": "open", "merged": False} for i in range(8)]
# Render to calculate bounds
app._render_work_view(2, 0, 30, 100)
self.assertTrue(hasattr(app, "_work_workers_bounds"))
self.assertTrue(hasattr(app, "_work_tickets_bounds"))
self.assertTrue(hasattr(app, "_work_prs_bounds"))
w_start, w_end = app._work_workers_bounds
t_start, t_end = app._work_tickets_bounds
p_start, p_end = app._work_prs_bounds
# Click inside tickets section
click_bstate = curses.BUTTON1_CLICKED
app._handle_mouse(10, t_start + 1, click_bstate)
self.assertEqual(app.work_focus_component, "tickets")
self.assertEqual(app.work_tickets_sel_idx, 1)
# Click inside PRs section
app._handle_mouse(10, p_start + 2, click_bstate)
self.assertEqual(app.work_focus_component, "prs")
self.assertEqual(app.work_prs_sel_idx, 2)
# Click inside workers section
app._handle_mouse(10, w_start, click_bstate)
self.assertEqual(app.work_focus_component, "workers")
self.assertEqual(app.work_sel_idx, 0)
# Wheel scroll down inside tickets
wheel_down = getattr(curses, "BUTTON5_PRESSED", 0x200000)
app._handle_mouse(10, t_start + 1, wheel_down)
self.assertEqual(app.work_focus_component, "tickets")
self.assertEqual(app.work_tickets_sel_idx, 2)
def test_box_tui_scrollable_tabs_approvals_tmux_dm_logs(self):
app = muse_tui.MuseTUI(self.mock_stdscr, initial_mode="box", initial_tab="approvals")
# Tab 2: Approvals scroll
app.box_tab = 2
app.data.approvals_cache = [{"node": f"agent{i}", "tool": f"tool{i}", "status": "PENDING"} for i in range(15)]
app.approvals_sel_idx = 10
app._render_approvals_view(2, 0, 10, 80)
self.assertGreater(app.approvals_scroll_start, 0)
# Tab 4: Tmux scroll
app.box_tab = 4
app.data.tmux_cache = [f"session_{i}: 1 windows" for i in range(12)]
app.tmux_sel_idx = 8
app._render_tmux_view(2, 0, 10, 80)
self.assertGreater(app.tmux_scroll_start, 0)
app._handle_key(ord('k'))
self.assertEqual(app.tmux_sel_idx, 7)
app._handle_key(ord('j'))
self.assertEqual(app.tmux_sel_idx, 8)
# Tab 5: DM Logs scroll
app.box_tab = 5
app.data.dm_logs_cache = [{"type": "sent", "agent": "opm", "to": "646", "target": "box", "ts": "2026-10-10T12:00:00", "id": f"m{i}"} for i in range(15)]
app.dm_logs_sel_idx = 12
app._render_dm_logs_view(2, 0, 10, 80)
self.assertGreater(app.dm_logs_scroll_start, 0)
app._handle_key(ord('k'))
self.assertEqual(app.dm_logs_sel_idx, 11)
app._handle_key(ord('j'))
self.assertEqual(app.dm_logs_sel_idx, 12)
app._handle_key(ord('g'))
self.assertEqual(app.dm_logs_sel_idx, 0)
app._handle_key(ord('G'))
self.assertEqual(app.dm_logs_sel_idx, 14)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()