diff --git a/haiku_rag_slim/haiku/rag/chat/widgets/document_filter_modal.py b/haiku_rag_slim/haiku/rag/chat/widgets/document_filter_modal.py index 372e92e3..d065b9bb 100644 --- a/haiku_rag_slim/haiku/rag/chat/widgets/document_filter_modal.py +++ b/haiku_rag_slim/haiku/rag/chat/widgets/document_filter_modal.py @@ -18,8 +18,6 @@ class DocumentCheckbox(Checkbox): def __init__(self, label: str, doc_id: str, *, value: bool) -> None: super().__init__(label, value=value, classes="doc-checkbox") self.doc_id = doc_id - # `label` is a reactive Text; narrowing the page wants the plain string. - self.search_text = label def _labelled(docs) -> list[tuple[str, str]]: @@ -147,7 +145,10 @@ class DocumentFilterModal(ModalScreen): def compose(self) -> ComposeResult: with Vertical(id="filter-container"): yield Static("[bold]Filter Documents[/bold]", id="filter-header") - yield Input(placeholder="Search documents...", id="filter-search") + yield Input( + placeholder="Search documents; press Enter to search...", + id="filter-search", + ) with VerticalScroll(id="filter-list"): yield Static("Loading...", id="loading-indicator") yield Static("", id="filter-footer") @@ -170,7 +171,7 @@ class DocumentFilterModal(ModalScreen): is showing and every selection stays reachable. """ if search is not None: - self._search = search + self._search = search.strip() self._page = 0 self._listing_selected = False @@ -223,11 +224,8 @@ class DocumentFilterModal(ModalScreen): return max(1, -(-self._matching // DOCUMENT_PAGE)) def _update_footer(self) -> None: - """Report the selection, and where in the listing this page sits. - - Counted from the checkboxes on screen, so narrowing the page as the user - types reports what they can actually see. - """ + """Report the selection, where in the listing this page sits, and + whether the search box holds a term the listing was not built from.""" footer = self.query_one("#filter-footer", Static) count = len(self._selected) if count == 0: @@ -236,17 +234,13 @@ class DocumentFilterModal(ModalScreen): state = f"[bold]{count}[/bold] document(s) selected" if self._listing_selected: state += " [dim]— listing the selected[/dim]" - visible = sum(1 for box in self.query(DocumentCheckbox) if box.display) if self._pages > 1: state += ( f" [dim]— page {self._page + 1} of {self._pages}" f" ({self._matching} total)[/dim]" ) - elif self._matching > visible: - state += ( - f" [dim]— showing {visible} of {self._matching};" - " type and press enter to search[/dim]" - ) + if self.query_one("#filter-search", Input).value.strip() != self._search: + state += " [dim]— press enter to search[/dim]" footer.update(state) async def on_checkbox_changed(self, event: Checkbox.Changed) -> None: @@ -275,14 +269,7 @@ class DocumentFilterModal(ModalScreen): await self._load_documents(event.value) def on_input_changed(self, event: Input.Changed) -> None: - """Narrow the page already shown, for feedback while typing.""" - search_term = event.value.lower().strip() - filter_list = self.query_one("#filter-list", VerticalScroll) - - for checkbox in filter_list.query(DocumentCheckbox): - checkbox.display = ( - search_term == "" or search_term in checkbox.search_text.lower() - ) + """Prompt when the search term has not been submitted.""" self._update_footer() async def _turn_to(self, page: int) -> None: diff --git a/tests/chat/test_chat_app.py b/tests/chat/test_chat_app.py index 06a90bd8..e2120c02 100644 --- a/tests/chat/test_chat_app.py +++ b/tests/chat/test_chat_app.py @@ -945,10 +945,22 @@ class TestKeepingSelectionsReachable: assert "No documents match" in str(empty.content) @pytest.mark.asyncio - async def test_narrowing_as_you_type_updates_the_count(self, temp_db_path: Path): - from textual.widgets import Input, Static + async def test_typing_without_submitting_leaves_the_listing_alone( + self, temp_db_path: Path + ): + """The listing is what the last submitted search asked for, and it pages. - from haiku.rag.chat.widgets.document_filter_modal import DocumentFilterModal + Narrowing it as the user types would hide rows from the page the search + landed on while the rest of the matches stayed a page away, so the term + applies on enter and says so until then. + """ + from textual.widgets import Button, Input, Static + + from haiku.rag.chat.widgets.document_filter_modal import ( + DOCUMENT_PAGE, + DocumentCheckbox, + DocumentFilterModal, + ) from haiku.rag.store.models.document import Document client = AsyncMock() @@ -958,7 +970,7 @@ class TestKeepingSelectionsReachable: Document(id="id-one", content="", title="Capital region"), Document(id="id-two", content="", title="Nobel laureates"), ] - client.count_documents.return_value = 9 + client.count_documents.return_value = DOCUMENT_PAGE * 2 modal = DocumentFilterModal(client=client) app, _ = _make_app(temp_db_path, client) @@ -970,12 +982,32 @@ class TestKeepingSelectionsReachable: await app.push_screen(modal) await pilot.pause() footer = modal.query_one("#filter-footer", Static) - assert "showing 2 of 9" in str(footer.content) + assert "press enter to search" not in str(footer.content) - modal.on_input_changed(Input.Changed(Input(), "nobel")) + search = modal.query_one("#filter-search", Input) + search.value = "nobel" + modal.on_input_changed(Input.Changed(search, "nobel")) + + assert "press enter to search" in str(footer.content) + assert all(box.display for box in modal.query(DocumentCheckbox)) + + await modal.on_button_pressed( + Button.Pressed(modal.query_one("#next-btn", Button)) + ) await pilot.pause() - assert "showing 1 of 9" in str(footer.content) + paged = client.list_documents.await_args.kwargs + assert paged["offset"] == DOCUMENT_PAGE + assert paged["filter"] is None + assert "press enter to search" in str(footer.content) + + await modal.on_input_submitted(Input.Submitted(search, search.value)) + await pilot.pause() + + submitted = client.list_documents.await_args.kwargs + assert submitted["offset"] == 0 + assert "nobel" in submitted["filter"] + assert "press enter to search" not in str(footer.content) class TestDocumentSearchFilter: