diff --git a/src/hqt/tui/app.py b/src/hqt/tui/app.py index 45e6402..b136da7 100644 --- a/src/hqt/tui/app.py +++ b/src/hqt/tui/app.py @@ -74,6 +74,7 @@ class HqtApp(App): Binding("l", "focus_sessions", "Sessions"), Binding("x", "archive_session", "Archive"), Binding("A", "toggle_archived", "Show Archived"), + Binding("u", "unarchive_session", "Restore"), Binding("d", "delete_session", "Delete Session"), Binding("enter", "attach_session", "Attach", priority=True), Binding("r", "rename_session", "Rename"), @@ -349,6 +350,12 @@ class HqtApp(App): # the focused dialog widget. if action == "attach_session" and isinstance(self.screen, ModalScreen): return False + # Archive applies only to active sessions; restore only to archived ones. + # Hide the inapplicable one so the footer never advertises it. + if action == "archive_session" and self._show_archived: + return None + if action == "unarchive_session" and not self._show_archived: + return None return True def action_attach_session(self) -> None: @@ -454,6 +461,17 @@ class HqtApp(App): self._run_service_worker(_do()) + def action_unarchive_session(self) -> None: + sid = self.query_one(SessionList).get_selected_session_id() + if not sid: + return + + async def _do() -> None: + await self._sessions().unarchive_session(sid) + await self._refresh_sessions() + + self._run_service_worker(_do()) + def action_toggle_archived(self) -> None: self._show_archived = not self._show_archived self.query_one(SessionList).set_mode(archived=self._show_archived) diff --git a/tests/test_tui.py b/tests/test_tui.py index 3052016..5f86a9d 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -2127,3 +2127,71 @@ async def test_toggle_archived_switches_list_and_header(): assert app._show_archived is False mock_service.list_sessions.assert_awaited_with(proj_id, archived=False) assert str(header.render()) == "Sessions" + + +@pytest.mark.asyncio +async def test_unarchive_binding_exists(): + bindings = {b.key: b for b in HqtApp.BINDINGS} + assert bindings["u"].action == "unarchive_session" + + +@pytest.mark.asyncio +async def test_unarchive_action_calls_service_and_refreshes(): + app = HqtApp() + async with app.run_test(size=(120, 40)) as pilot: + from hqt.db.models import Project, Harness, Session + from hqt.tui.widgets.session_list import SessionList + + seed_db = app._db_factory() + proj = Project(name="restore-proj", path="/tmp/restore-proj") + seed_db.add(proj) + seed_db.flush() + harness = seed_db.query(Harness).first() + sess = Session( + project_id=proj.id, + harness_id=harness.id, + tmux_session_name="hqt-restore", + archived=True, + ) + seed_db.add(sess) + seed_db.commit() + proj_id = proj.id + sess_id = sess.id + seed_db.close() + + app._selected_project_id = proj_id + app._show_archived = True + await app._refresh_sessions() + await pilot.pause() + + sl = app.query_one(SessionList) + sl.query_one("#session-list").focus() + await pilot.press("down") + await pilot.pause() + assert sl.get_selected_session_id() == sess_id + + mock_service = MagicMock() + mock_service.unarchive_session = AsyncMock() + mock_service.list_sessions = AsyncMock(return_value=[]) + mock_service.sync_window_labels = AsyncMock(return_value=[]) + app._session_service = mock_service + + await app.run_action("unarchive_session") + await pilot.pause() + + mock_service.unarchive_session.assert_awaited_once_with(sess_id) + mock_service.list_sessions.assert_awaited() + + +@pytest.mark.asyncio +async def test_archive_restore_actions_gated_by_view(): + app = HqtApp() + async with app.run_test(size=(120, 40)): + # Active view (default): archive offered, restore hidden. + assert app.check_action("archive_session", ()) is True + assert app.check_action("unarchive_session", ()) is None + + # Archived view: restore offered, archive hidden. + app._show_archived = True + assert app.check_action("archive_session", ()) is None + assert app.check_action("unarchive_session", ()) is True