Make search a screen
This commit is contained in:
parent
0d126d894e
commit
2c6efc55c6
5 changed files with 204 additions and 190 deletions
|
|
@ -11,12 +11,12 @@ if TYPE_CHECKING:
|
||||||
try:
|
try:
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from textual.containers import Container
|
from textual.widgets import Footer, Header
|
||||||
from textual.widgets import Footer, Header, Input, ListItem, Static
|
|
||||||
|
|
||||||
from haiku.rag.inspector.widgets.chunk_list import ChunkList
|
from haiku.rag.inspector.widgets.chunk_list import ChunkList
|
||||||
from haiku.rag.inspector.widgets.detail_view import DetailView
|
from haiku.rag.inspector.widgets.detail_view import DetailView
|
||||||
from haiku.rag.inspector.widgets.document_list import DocumentList
|
from haiku.rag.inspector.widgets.document_list import DocumentList
|
||||||
|
from haiku.rag.inspector.widgets.search_modal import SearchModal
|
||||||
|
|
||||||
TEXTUAL_AVAILABLE = True
|
TEXTUAL_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -63,21 +63,6 @@ class InspectorApp(App): # type: ignore[misc]
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search-container {
|
|
||||||
dock: top;
|
|
||||||
height: 3;
|
|
||||||
background: $panel;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-container.visible {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-input {
|
|
||||||
width: 1fr;
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
|
|
@ -89,17 +74,10 @@ class InspectorApp(App): # type: ignore[misc]
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self.client: HaikuRAG | None = None
|
self.client: HaikuRAG | None = None
|
||||||
self.search_visible = False
|
|
||||||
self.search_active = False
|
|
||||||
# Track current selection for easy restoration
|
|
||||||
self.current_document_id: str | None = None
|
|
||||||
self.current_chunk_id: str | None = None
|
|
||||||
|
|
||||||
def compose(self) -> "ComposeResult":
|
def compose(self) -> "ComposeResult":
|
||||||
"""Compose the UI layout."""
|
"""Compose the UI layout."""
|
||||||
yield Header()
|
yield Header()
|
||||||
with Container(id="search-container"):
|
|
||||||
yield Input(placeholder="Search chunks...", id="search-input")
|
|
||||||
yield DocumentList(id="document-list")
|
yield DocumentList(id="document-list")
|
||||||
yield ChunkList(id="chunk-list")
|
yield ChunkList(id="chunk-list")
|
||||||
yield DetailView(id="detail-view")
|
yield DetailView(id="detail-view")
|
||||||
|
|
@ -108,28 +86,13 @@ class InspectorApp(App): # type: ignore[misc]
|
||||||
async def on_mount(self) -> None:
|
async def on_mount(self) -> None:
|
||||||
"""Initialize the app when mounted."""
|
"""Initialize the app when mounted."""
|
||||||
config = get_config()
|
config = get_config()
|
||||||
self.client = HaikuRAG(db_path=self.db_path, config=config, allow_create=False)
|
self.client = HaikuRAG(db_path=self.db_path, config=config, read_only=True)
|
||||||
await self.client.__aenter__()
|
await self.client.__aenter__()
|
||||||
|
|
||||||
# Load initial documents
|
# Load initial documents
|
||||||
doc_list = self.query_one(DocumentList)
|
doc_list = self.query_one(DocumentList)
|
||||||
await doc_list.load_documents(self.client)
|
await doc_list.load_documents(self.client)
|
||||||
|
|
||||||
# Select first document and load its chunks
|
|
||||||
if doc_list.documents and doc_list.list_view:
|
|
||||||
doc_list.list_view.index = 0
|
|
||||||
first_doc = doc_list.documents[0]
|
|
||||||
self.current_document_id = first_doc.id
|
|
||||||
|
|
||||||
if first_doc.id:
|
|
||||||
chunk_list = self.query_one(ChunkList)
|
|
||||||
await chunk_list.load_chunks_for_document(self.client, first_doc.id)
|
|
||||||
|
|
||||||
# Select first chunk
|
|
||||||
if chunk_list.chunks and chunk_list.list_view:
|
|
||||||
chunk_list.list_view.index = 0
|
|
||||||
self.current_chunk_id = chunk_list.chunks[0].id
|
|
||||||
|
|
||||||
# Focus the document list view
|
# Focus the document list view
|
||||||
if doc_list.list_view:
|
if doc_list.list_view:
|
||||||
doc_list.list_view.focus()
|
doc_list.list_view.focus()
|
||||||
|
|
@ -139,107 +102,50 @@ class InspectorApp(App): # type: ignore[misc]
|
||||||
if self.client:
|
if self.client:
|
||||||
await self.client.__aexit__(None, None, None)
|
await self.client.__aexit__(None, None, None)
|
||||||
|
|
||||||
|
def _select_chunk(self, chunk_list: ChunkList, chunk_id: str) -> None:
|
||||||
|
"""Helper to select a chunk after refresh."""
|
||||||
|
for idx, c in enumerate(chunk_list.chunks):
|
||||||
|
if c.id == chunk_id:
|
||||||
|
if chunk_list.list_view:
|
||||||
|
chunk_list.list_view.index = idx
|
||||||
|
chunk_list.list_view.focus()
|
||||||
|
break
|
||||||
|
|
||||||
async def action_search(self) -> None:
|
async def action_search(self) -> None:
|
||||||
"""Toggle search input visibility."""
|
"""Open search modal."""
|
||||||
search_container = self.query_one("#search-container", Container)
|
if self.client:
|
||||||
search_input = self.query_one("#search-input", Input)
|
await self.push_screen(SearchModal(self.client))
|
||||||
|
|
||||||
if self.search_visible:
|
async def on_search_modal_chunk_selected(
|
||||||
search_container.remove_class("visible")
|
self, message: SearchModal.ChunkSelected
|
||||||
search_input.value = ""
|
) -> None:
|
||||||
self.search_visible = False
|
"""Handle chunk selection from search modal."""
|
||||||
self.search_active = False
|
if not self.client:
|
||||||
|
return
|
||||||
|
|
||||||
# Restore full document list and reload chunks for selected document
|
chunk = message.chunk
|
||||||
if self.client:
|
|
||||||
|
# Navigate to the document containing this chunk
|
||||||
|
if chunk.document_id:
|
||||||
|
doc = await self.client.document_repository.get_by_id(chunk.document_id)
|
||||||
|
if doc:
|
||||||
doc_list = self.query_one(DocumentList)
|
doc_list = self.query_one(DocumentList)
|
||||||
chunk_list = self.query_one(ChunkList)
|
chunk_list = self.query_one(ChunkList)
|
||||||
|
|
||||||
# Reload all documents
|
# Find and select the document
|
||||||
await doc_list.load_documents(self.client)
|
for idx, d in enumerate(doc_list.documents):
|
||||||
|
if d.id == chunk.document_id:
|
||||||
# Restore document and chunk selection
|
if doc_list.list_view:
|
||||||
if (
|
|
||||||
self.current_document_id
|
|
||||||
and doc_list.list_view
|
|
||||||
and doc_list.documents
|
|
||||||
):
|
|
||||||
# Find and select the document
|
|
||||||
doc_found = False
|
|
||||||
for idx, doc in enumerate(doc_list.documents):
|
|
||||||
if doc.id == self.current_document_id:
|
|
||||||
doc_list.list_view.index = idx
|
doc_list.list_view.index = idx
|
||||||
doc_found = True
|
break
|
||||||
break
|
|
||||||
|
|
||||||
# Reload chunks for this document (without scores)
|
# Load chunks for this document
|
||||||
if doc_found:
|
await chunk_list.load_chunks_for_document(
|
||||||
await chunk_list.load_chunks_for_document(
|
self.client, chunk.document_id
|
||||||
self.client, self.current_document_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore chunk selection and show it in detail view
|
|
||||||
if (
|
|
||||||
self.current_chunk_id
|
|
||||||
and chunk_list.list_view
|
|
||||||
and chunk_list.chunks
|
|
||||||
):
|
|
||||||
for idx, chunk in enumerate(chunk_list.chunks):
|
|
||||||
if chunk.id == self.current_chunk_id:
|
|
||||||
chunk_list.list_view.index = idx
|
|
||||||
# Show the chunk in detail view
|
|
||||||
detail_view = self.query_one(DetailView)
|
|
||||||
await detail_view.show_chunk(chunk)
|
|
||||||
break
|
|
||||||
|
|
||||||
# Focus back to document list to show selection
|
|
||||||
doc_list.list_view.focus()
|
|
||||||
else:
|
|
||||||
search_container.add_class("visible")
|
|
||||||
search_input.focus()
|
|
||||||
self.search_visible = True
|
|
||||||
|
|
||||||
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
||||||
"""Handle search input submission."""
|
|
||||||
if event.input.id == "search-input" and self.client:
|
|
||||||
query = event.value.strip()
|
|
||||||
if query:
|
|
||||||
self.search_active = True
|
|
||||||
chunk_list = self.query_one(ChunkList)
|
|
||||||
await chunk_list.load_chunks_from_search(self.client, query)
|
|
||||||
|
|
||||||
# Get unique document IDs from the search results
|
|
||||||
doc_ids = list(
|
|
||||||
{
|
|
||||||
chunk.document_id
|
|
||||||
for chunk in chunk_list.chunks
|
|
||||||
if chunk.document_id
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update document list to show only documents with matching chunks
|
# Wait a tick for the ListView to process the new items
|
||||||
if doc_ids:
|
self.call_after_refresh(self._select_chunk, chunk_list, chunk.id)
|
||||||
doc_list = self.query_one(DocumentList)
|
|
||||||
documents = []
|
|
||||||
for doc_id in doc_ids:
|
|
||||||
doc = await self.client.document_repository.get_by_id(doc_id)
|
|
||||||
if doc:
|
|
||||||
documents.append(doc)
|
|
||||||
|
|
||||||
# Update the document list with filtered documents
|
|
||||||
doc_list.documents = documents
|
|
||||||
if doc_list.list_view:
|
|
||||||
await doc_list.list_view.clear()
|
|
||||||
for doc in documents:
|
|
||||||
title = doc.title or doc.uri or doc.id or "Untitled"
|
|
||||||
item = ListItem(Static(f"{title}"))
|
|
||||||
await doc_list.list_view.append(item)
|
|
||||||
|
|
||||||
# Select first chunk and focus the chunk list
|
|
||||||
if chunk_list.list_view:
|
|
||||||
if chunk_list.chunks:
|
|
||||||
chunk_list.list_view.index = 0
|
|
||||||
chunk_list.list_view.focus()
|
|
||||||
|
|
||||||
async def on_document_list_document_selected(
|
async def on_document_list_document_selected(
|
||||||
self, message: DocumentList.DocumentSelected
|
self, message: DocumentList.DocumentSelected
|
||||||
|
|
@ -252,15 +158,12 @@ class InspectorApp(App): # type: ignore[misc]
|
||||||
if not self.client:
|
if not self.client:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Always track current document (even during search)
|
|
||||||
self.current_document_id = message.document.id
|
|
||||||
|
|
||||||
# Show document details
|
# Show document details
|
||||||
detail_view = self.query_one(DetailView)
|
detail_view = self.query_one(DetailView)
|
||||||
await detail_view.show_document(message.document)
|
await detail_view.show_document(message.document)
|
||||||
|
|
||||||
# Load chunks for this document (but not during search - preserve search results)
|
# Load chunks for this document
|
||||||
if message.document.id and not self.search_active:
|
if message.document.id:
|
||||||
chunk_list = self.query_one(ChunkList)
|
chunk_list = self.query_one(ChunkList)
|
||||||
await chunk_list.load_chunks_for_document(self.client, message.document.id)
|
await chunk_list.load_chunks_for_document(self.client, message.document.id)
|
||||||
|
|
||||||
|
|
@ -272,17 +175,10 @@ class InspectorApp(App): # type: ignore[misc]
|
||||||
Args:
|
Args:
|
||||||
message: Message containing selected chunk
|
message: Message containing selected chunk
|
||||||
"""
|
"""
|
||||||
# Always track current chunk (even during search)
|
|
||||||
self.current_chunk_id = message.chunk.id
|
|
||||||
|
|
||||||
# Show chunk details
|
# Show chunk details
|
||||||
detail_view = self.query_one(DetailView)
|
detail_view = self.query_one(DetailView)
|
||||||
await detail_view.show_chunk(message.chunk)
|
await detail_view.show_chunk(message.chunk)
|
||||||
|
|
||||||
# Track the document this chunk belongs to
|
|
||||||
if message.chunk.document_id:
|
|
||||||
self.current_document_id = message.chunk.document_id
|
|
||||||
|
|
||||||
|
|
||||||
def run_inspector(db_path: Path | None = None) -> None:
|
def run_inspector(db_path: Path | None = None) -> None:
|
||||||
"""Run the inspector TUI.
|
"""Run the inspector TUI.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from textual import on
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.containers import VerticalScroll
|
from textual.containers import VerticalScroll
|
||||||
from textual.message import Message
|
from textual.message import Message
|
||||||
|
|
@ -53,43 +54,12 @@ class ChunkList(VerticalScroll):
|
||||||
item = ListItem(Static(f"[{chunk.order}] {first_line}"))
|
item = ListItem(Static(f"[{chunk.order}] {first_line}"))
|
||||||
await self.list_view.append(item)
|
await self.list_view.append(item)
|
||||||
|
|
||||||
async def load_chunks_from_search(
|
@on(ListView.Highlighted)
|
||||||
self, client: HaikuRAG, query: str, limit: int = 20
|
@on(ListView.Selected)
|
||||||
|
async def handle_chunk_selection(
|
||||||
|
self, event: ListView.Highlighted | ListView.Selected
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Load chunks from search results.
|
"""Handle chunk selection (arrow keys or Enter)."""
|
||||||
|
|
||||||
Args:
|
|
||||||
client: HaikuRAG client instance
|
|
||||||
query: Search query
|
|
||||||
limit: Maximum number of results
|
|
||||||
"""
|
|
||||||
if self.list_view is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
results = await client.chunk_repository.search(
|
|
||||||
query=query, limit=limit, search_type="hybrid"
|
|
||||||
)
|
|
||||||
self.chunks = [chunk for chunk, _score in results]
|
|
||||||
|
|
||||||
# Clear existing items
|
|
||||||
await self.list_view.clear()
|
|
||||||
|
|
||||||
# Add chunk items with scores
|
|
||||||
for chunk, score in results:
|
|
||||||
first_line = chunk.content.split("\n")[0]
|
|
||||||
item = ListItem(Static(f"[{score:.2f}] {first_line}"))
|
|
||||||
await self.list_view.append(item)
|
|
||||||
|
|
||||||
async def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
|
|
||||||
"""Handle chunk navigation (arrow keys)."""
|
|
||||||
if event.list_view == self.list_view and event.item is not None:
|
|
||||||
idx = event.list_view.index
|
|
||||||
if idx is not None and 0 <= idx < len(self.chunks):
|
|
||||||
chunk = self.chunks[idx]
|
|
||||||
self.post_message(self.ChunkSelected(chunk))
|
|
||||||
|
|
||||||
async def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
||||||
"""Handle chunk selection (Enter key)."""
|
|
||||||
if event.list_view == self.list_view:
|
if event.list_view == self.list_view:
|
||||||
idx = event.list_view.index
|
idx = event.list_view.index
|
||||||
if idx is not None and 0 <= idx < len(self.chunks):
|
if idx is not None and 0 <= idx < len(self.chunks):
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from haiku.rag.store.models import Chunk, Document
|
||||||
class DetailView(VerticalScroll):
|
class DetailView(VerticalScroll):
|
||||||
"""Widget for displaying detailed content of documents or chunks."""
|
"""Widget for displaying detailed content of documents or chunks."""
|
||||||
|
|
||||||
can_focus = False
|
can_focus = True
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from textual import on
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.containers import VerticalScroll
|
from textual.containers import VerticalScroll
|
||||||
from textual.message import Message
|
from textual.message import Message
|
||||||
|
|
@ -50,16 +51,12 @@ class DocumentList(VerticalScroll):
|
||||||
item = ListItem(Static(f"{title}"))
|
item = ListItem(Static(f"{title}"))
|
||||||
await self.list_view.append(item)
|
await self.list_view.append(item)
|
||||||
|
|
||||||
async def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
|
@on(ListView.Highlighted)
|
||||||
"""Handle document navigation (arrow keys)."""
|
@on(ListView.Selected)
|
||||||
if event.list_view == self.list_view and event.item is not None:
|
async def handle_document_selection(
|
||||||
idx = event.list_view.index
|
self, event: ListView.Highlighted | ListView.Selected
|
||||||
if idx is not None and 0 <= idx < len(self.documents):
|
) -> None:
|
||||||
document = self.documents[idx]
|
"""Handle document selection (arrow keys or Enter)."""
|
||||||
self.post_message(self.DocumentSelected(document))
|
|
||||||
|
|
||||||
async def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
||||||
"""Handle document selection (Enter key)."""
|
|
||||||
if event.list_view == self.list_view:
|
if event.list_view == self.list_view:
|
||||||
idx = event.list_view.index
|
idx = event.list_view.index
|
||||||
if idx is not None and 0 <= idx < len(self.documents):
|
if idx is not None and 0 <= idx < len(self.documents):
|
||||||
|
|
|
||||||
151
haiku_rag_slim/haiku/rag/inspector/widgets/search_modal.py
Normal file
151
haiku_rag_slim/haiku/rag/inspector/widgets/search_modal.py
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
from textual import on
|
||||||
|
from textual.app import ComposeResult
|
||||||
|
from textual.binding import Binding
|
||||||
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||||
|
from textual.message import Message
|
||||||
|
from textual.screen import Screen
|
||||||
|
from textual.widgets import Input, ListItem, ListView, Static
|
||||||
|
|
||||||
|
from haiku.rag.client import HaikuRAG
|
||||||
|
from haiku.rag.inspector.widgets.detail_view import DetailView
|
||||||
|
from haiku.rag.store.models import Chunk
|
||||||
|
|
||||||
|
|
||||||
|
class SearchModal(Screen):
|
||||||
|
"""Screen for searching chunks."""
|
||||||
|
|
||||||
|
BINDINGS = [
|
||||||
|
Binding("escape", "dismiss", "Close", show=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
CSS = """
|
||||||
|
SearchModal {
|
||||||
|
background: $surface;
|
||||||
|
layout: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-header {
|
||||||
|
dock: top;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-content {
|
||||||
|
height: 1fr;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-results-container {
|
||||||
|
width: 1fr;
|
||||||
|
border: solid $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-detail {
|
||||||
|
width: 2fr;
|
||||||
|
border: solid $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItem {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItem Static {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class ChunkSelected(Message):
|
||||||
|
"""Message sent when a chunk is selected from search results."""
|
||||||
|
|
||||||
|
def __init__(self, chunk: Chunk) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.chunk = chunk
|
||||||
|
|
||||||
|
def __init__(self, client: HaikuRAG):
|
||||||
|
super().__init__()
|
||||||
|
self.client = client
|
||||||
|
self.chunks: list[Chunk] = []
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Compose the search screen."""
|
||||||
|
with Vertical(id="search-header"):
|
||||||
|
yield Static("[bold]Search Chunks[/bold]")
|
||||||
|
yield Input(placeholder="Enter search query...", id="search-input")
|
||||||
|
yield Static("", id="status-label")
|
||||||
|
with Horizontal(id="search-content"):
|
||||||
|
with VerticalScroll(id="search-results-container"):
|
||||||
|
yield ListView(id="search-results")
|
||||||
|
yield DetailView(id="search-detail")
|
||||||
|
|
||||||
|
async def on_mount(self) -> None:
|
||||||
|
"""Focus the search input when mounted."""
|
||||||
|
status_label = self.query_one("#status-label", Static)
|
||||||
|
status_label.update("Type query and press Enter to search")
|
||||||
|
search_input = self.query_one("#search-input", Input)
|
||||||
|
search_input.focus()
|
||||||
|
|
||||||
|
@on(Input.Submitted, "#search-input")
|
||||||
|
async def search_submitted(self, event: Input.Submitted) -> None:
|
||||||
|
"""Handle search query submission."""
|
||||||
|
query = event.value.strip()
|
||||||
|
if query:
|
||||||
|
await self.run_search(query)
|
||||||
|
|
||||||
|
async def run_search(self, query: str) -> None:
|
||||||
|
"""Perform the search."""
|
||||||
|
status_label = self.query_one("#status-label", Static)
|
||||||
|
list_view = self.query_one("#search-results", ListView)
|
||||||
|
|
||||||
|
status_label.update("Searching...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Perform search
|
||||||
|
results = await self.client.chunk_repository.search(
|
||||||
|
query=query, limit=50, search_type="hybrid"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.chunks = [chunk for chunk, _score in results]
|
||||||
|
|
||||||
|
# Clear and populate results
|
||||||
|
await list_view.clear()
|
||||||
|
for chunk, score in results:
|
||||||
|
first_line = chunk.content.split("\n")[0]
|
||||||
|
score_str = f"{score:.2f}" if score else "N/A"
|
||||||
|
item = ListItem(Static(f"[{score_str}] {first_line}"))
|
||||||
|
await list_view.append(item)
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
status_label.update(f"Found {len(self.chunks)} results")
|
||||||
|
|
||||||
|
# Select first result, show in detail view, and focus list
|
||||||
|
if self.chunks:
|
||||||
|
list_view.index = 0
|
||||||
|
detail_view = self.query_one("#search-detail", DetailView)
|
||||||
|
await detail_view.show_chunk(self.chunks[0])
|
||||||
|
list_view.focus()
|
||||||
|
except Exception as e:
|
||||||
|
status_label.update(f"Error: {str(e)}")
|
||||||
|
|
||||||
|
async def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
|
||||||
|
"""Handle chunk navigation (arrow keys)."""
|
||||||
|
list_view = self.query_one("#search-results", ListView)
|
||||||
|
if event.list_view == list_view and event.item is not None:
|
||||||
|
idx = event.list_view.index
|
||||||
|
if idx is not None and 0 <= idx < len(self.chunks):
|
||||||
|
chunk = self.chunks[idx]
|
||||||
|
detail_view = self.query_one("#search-detail", DetailView)
|
||||||
|
await detail_view.show_chunk(chunk)
|
||||||
|
|
||||||
|
async def on_list_view_selected(self, event: ListView.Selected) -> None:
|
||||||
|
"""Handle chunk selection (Enter key)."""
|
||||||
|
list_view = self.query_one("#search-results", ListView)
|
||||||
|
if event.list_view == list_view:
|
||||||
|
idx = event.list_view.index
|
||||||
|
if idx is not None and 0 <= idx < len(self.chunks):
|
||||||
|
chunk = self.chunks[idx]
|
||||||
|
self.post_message(self.ChunkSelected(chunk))
|
||||||
|
self.app.pop_screen()
|
||||||
|
|
||||||
|
async def action_dismiss(self, result=None) -> None:
|
||||||
|
"""Close the search screen."""
|
||||||
|
self.app.pop_screen()
|
||||||
Loading…
Reference in a new issue