From fed63511078846e157c6017b1e973c9ed9dcbcc5 Mon Sep 17 00:00:00 2001 From: Tim Waugh Date: Sun, 23 Nov 2025 10:39:55 +0000 Subject: [PATCH] feat(tui): add live status panel updates across all screens Add polling timers to all three phase screens to continuously update the status panel with background task progress. Previously, only the screen that started a background task would show its progress updates. Changes: - Phase 1, 2, 3: Add 0.5s polling timer that calls _update_status_panel() - Phase 1, 2, 3: Add timer cleanup in on_unmount() to prevent leaks - Phase 3: Separate status update timer from existing decision polling timer This ensures users see live progress updates (e.g., "Building page index 50%") regardless of which screen is currently mounted, improving UX for long-running background operations that span multiple screens. All 143 UI tests pass. Assisted-by: Claude Code --- src/logsqueak/tui/screens/block_selection.py | 25 +++++++++++++++ src/logsqueak/tui/screens/content_editing.py | 27 ++++++++++++++++ .../tui/screens/integration_review.py | 32 +++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/src/logsqueak/tui/screens/block_selection.py b/src/logsqueak/tui/screens/block_selection.py index 922c245..4eabd5d 100644 --- a/src/logsqueak/tui/screens/block_selection.py +++ b/src/logsqueak/tui/screens/block_selection.py @@ -105,6 +105,10 @@ def __init__( self._llm_worker: Optional[Worker] = None self._indexing_worker: Optional[Worker] = None + # Status panel update timer + from textual.timer import Timer + self._status_update_timer: Optional[Timer] = None + def _initialize_block_states_from_journals( self, journals: Dict[str, LogseqOutline] @@ -254,10 +258,18 @@ def on_mount(self) -> None: # Update bottom panel with initial block self._update_current_block() + # Start polling timer to update status panel with background task progress + self._status_update_timer = self.set_interval(0.5, self._update_status_panel) + logger.info("phase1_on_mount_finished") def on_unmount(self) -> None: """Called when screen is unmounted.""" + # Stop status update timer + if self._status_update_timer: + self._status_update_timer.stop() + self._status_update_timer = None + # Cancel any running workers if self._llm_worker: self._llm_worker.cancel() @@ -556,6 +568,19 @@ def _update_selected_count(self) -> None: ) self.selected_count = count + def _update_status_panel(self) -> None: + """Update status panel with current background task progress. + + Called periodically by timer to show live progress from all background tasks, + including those started by other screens (e.g., page_indexing from Phase 1). + """ + try: + status_panel = self.query_one(StatusPanel) + status_panel.update_status() + except Exception: + # Widget not mounted or query failed + pass + # Background tasks def start_llm_classification(self) -> None: diff --git a/src/logsqueak/tui/screens/content_editing.py b/src/logsqueak/tui/screens/content_editing.py index 91bc224..9abbf3e 100644 --- a/src/logsqueak/tui/screens/content_editing.py +++ b/src/logsqueak/tui/screens/content_editing.py @@ -158,6 +158,10 @@ def __init__( self.candidate_page_names: Dict[str, list[str]] = {} # block_id -> page names self.page_contents: Dict[str, LogseqOutline] = {} # page_name -> outline + # Status panel update timer + from textual.timer import Timer + self._status_update_timer: Optional[Timer] = None + def compose(self) -> ComposeResult: """Compose the Phase 2 screen layout.""" with Container(id="phase2-container"): @@ -227,6 +231,29 @@ async def on_mount(self) -> None: if self.auto_start_workers: self._start_background_workers() + # Start polling timer to update status panel with background task progress + self._status_update_timer = self.set_interval(0.5, self._update_status_panel) + + def on_unmount(self) -> None: + """Called when screen is unmounted.""" + # Stop status update timer + if self._status_update_timer: + self._status_update_timer.stop() + self._status_update_timer = None + + def _update_status_panel(self) -> None: + """Update status panel with current background task progress. + + Called periodically by timer to show live progress from all background tasks, + including those started by other screens (e.g., page_indexing from Phase 1). + """ + try: + status_panel = self.query_one(StatusPanel) + status_panel.update_status() + except Exception: + # Widget not mounted or query failed + pass + def _start_background_workers(self) -> None: """Start background workers for LLM rewording and RAG search.""" if self.llm_client: diff --git a/src/logsqueak/tui/screens/integration_review.py b/src/logsqueak/tui/screens/integration_review.py index d3acc6c..a02c89f 100644 --- a/src/logsqueak/tui/screens/integration_review.py +++ b/src/logsqueak/tui/screens/integration_review.py @@ -184,6 +184,10 @@ def __init__( # Store polling timer so we can stop it when complete self._polling_timer = None + # Status panel update timer (separate from decision polling) + from textual.timer import Timer + self._status_update_timer: Optional[Timer] = None + def _group_decisions_by_block(self, decisions: List[IntegrationDecision]) -> dict: """Group decisions by knowledge_block_id. @@ -291,6 +295,9 @@ def on_mount(self) -> None: decision_list = self.query_one(DecisionList) decision_list.focus() + # Start polling timer to update status panel with background task progress + self._status_update_timer = self.set_interval(0.5, self._update_status_panel) + # Decisions are generated in Phase 2 and streamed into app.integration_decisions # Check task status to determine what to do blocks_ready = len(self.decisions_ready) @@ -330,6 +337,31 @@ def on_mount(self) -> None: status_panel = self.query_one(StatusPanel) status_panel.update_status() + def on_unmount(self) -> None: + """Called when screen is unmounted.""" + # Stop status update timer + if self._status_update_timer: + self._status_update_timer.stop() + self._status_update_timer = None + + # Stop decision polling timer + if self._polling_timer: + self._polling_timer.stop() + self._polling_timer = None + + def _update_status_panel(self) -> None: + """Update status panel with current background task progress. + + Called periodically by timer to show live progress from all background tasks, + including those started by other screens (e.g., page_indexing from Phase 1). + """ + try: + status_panel = self.query_one(StatusPanel) + status_panel.update_status() + except Exception: + # Widget not mounted or query failed + pass + def watch_current_decision_index(self, old_index: int, new_index: int) -> None: """React to changes in current_decision_index.