Skip to content

🎨 Palette: Add persistent CLI status indicator#70

Open
Whamp wants to merge 1 commit intomainfrom
palette-ui-status-indicator-13951464115053580190
Open

🎨 Palette: Add persistent CLI status indicator#70
Whamp wants to merge 1 commit intomainfrom
palette-ui-status-indicator-13951464115053580190

Conversation

@Whamp
Copy link
Copy Markdown
Owner

@Whamp Whamp commented Feb 8, 2026

User description

This PR adds a persistent status indicator to the Chirp CLI application to provide immediate visual feedback to the user about the application's state.

Why:
Previously, the user had to rely on log messages to know if the application was recording or transcribing. A visual indicator makes the experience more intuitive and responsive.

What:

  • Modified ChirpApp in src/chirp/main.py to initialize and manage a rich.status.Status object.
  • Updated run, _start_recording, _stop_recording, and _transcribe_and_inject to update the status text and spinner style.
  • Added tests/test_ui_status.py to verify the logic.

Accessibility:

  • Uses clear text labels ("Ready", "Recording...", "Transcribing...") alongside spinners.
  • Uses distinct colors (Red for recording, Green for processing) to aid quick recognition.

PR created automatically by Jules for task 13951464115053580190 started by @Whamp


PR Type

Enhancement


Description

  • Add persistent status indicator to CLI showing application state

  • Display "Ready", "Recording...", and "Transcribing..." with distinct visual styles

  • Update status during recording, transcription, and completion phases

  • Add comprehensive test suite for status indicator state transitions


Diagram Walkthrough

flowchart LR
  A["ChirpApp Init"] -->|"Create Status"| B["Status: Ready"]
  B -->|"User presses hotkey"| C["Status: Recording"]
  C -->|"User releases hotkey"| D["Status: Transcribing"]
  D -->|"Transcription complete"| B
  D -->|"Error occurs"| B
Loading

File Walkthrough

Relevant files
Enhancement
main.py
Implement persistent status indicator with state updates 

src/chirp/main.py

  • Import Status from rich.status for UI status indicator
  • Store console instance and initialize status_indicator in __init__
  • Wrap keyboard.wait() in status context manager in run() method
  • Update status to "Recording..." with red spinner in _start_recording()
  • Update status to "Transcribing..." with green spinner in
    _stop_recording()
  • Wrap transcription logic in try-finally block to reset status to
    "Ready" after completion
+32/-17 
Tests
test_ui_status.py
Add comprehensive tests for UI status indicator                   

tests/test_ui_status.py

  • Create new test file with comprehensive test coverage for status
    indicator
  • Mock all external dependencies (ParakeetManager, AudioCapture,
    Console, etc.)
  • Test status initialization with "Ready" state
  • Test status context manager activation during run()
  • Test status updates during recording and transcription phases
  • Test status reset to "Ready" after transcription completion
  • Test status preservation when recording restarts during transcription
+128/-0 

- Implements a persistent status indicator using `rich.console.Status`.
- Visualizes "Ready" (white dots), "Recording..." (red point), and "Transcribing..." (green dots) states.
- Ensures status is reset to "Ready" after transcription, even on failure.
- Adds `tests/test_ui_status.py` to verify state transitions and mocking.

Co-authored-by: Whamp <1115485+Whamp@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@qodo-code-review
Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Sensitive data logging

Description: The new debug log line logs the full transcription content
(self.logger.debug("Transcription: %s", text)), which may expose sensitive spoken/typed
data (e.g., passwords, API keys, PII) into logs that can be collected or retained beyond
the user's control.
main.py [173-179]

Referred Code
duration = time.perf_counter() - start_time
self.logger.debug("Transcription finished in %.2fs (chars=%s)", duration, len(text))
if not text.strip():
    self.logger.info("Transcription empty; skipping paste")
    return
self.logger.debug("Transcription: %s", text)
self.text_injector.inject(text)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Sensitive data logged: The PR adds logging of the full transcription text, which can contain sensitive user
speech content and should not be written to logs.

Referred Code
self.logger.debug("Transcription finished in %.2fs (chars=%s)", duration, len(text))
if not text.strip():
    self.logger.info("Transcription empty; skipping paste")
    return
self.logger.debug("Transcription: %s", text)
self.text_injector.inject(text)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Transcription data exposure: Transcribed text is handled and then injected (text_injector.inject(text)) without any
visible safeguards or policy checks in the diff, so a human should verify this flow cannot
leak or mishandle sensitive data.

Referred Code
    text = self.parakeet.transcribe(waveform, sample_rate=16_000, language=self.config.language)
except Exception as exc:
    self.logger.exception("Transcription failed: %s", exc)
    self.audio_feedback.play_error(self.config.error_sound_path)
    return
duration = time.perf_counter() - start_time
self.logger.debug("Transcription finished in %.2fs (chars=%s)", duration, len(text))
if not text.strip():
    self.logger.info("Transcription empty; skipping paste")
    return
self.logger.debug("Transcription: %s", text)
self.text_injector.inject(text)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Avoid stale thread status resets

To prevent a race condition, add a session counter. Increment it in
_stop_recording, capture it in _transcribe_and_inject, and only reset the status
to "Ready" if the session ID matches, preventing stale threads from overwriting
the status.

src/chirp/main.py [180-182]

+# In __init__
+self._transcription_session = 0
+
+# In _stop_recording
+self._transcription_session += 1
+
+# At the top of _transcribe_and_inject
+session = self._transcription_session
+
 finally:
-    if not self._recording and self.status_indicator:
+    if session == self._transcription_session and self.status_indicator:
         self.status_indicator.update("Ready", spinner="dots", spinner_style="white")

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a critical race condition where a stale transcription thread could incorrectly overwrite the status of a new recording. The proposed solution using a session counter is an effective and robust way to prevent this concurrency issue without introducing complex locking.

Medium
General
Simplify run context usage

Simplify the run method by using contextlib.nullcontext as a fallback when
self.status_indicator is None, which eliminates the if/else block and the
duplicate self.keyboard.wait() call.

src/chirp/main.py [100-104]

-if self.status_indicator:
-    with self.status_indicator:
-        self.keyboard.wait()
-else:
+from contextlib import nullcontext
+
+with self.status_indicator or nullcontext():
     self.keyboard.wait()
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion improves code quality by removing a redundant self.keyboard.wait() call and simplifying the conditional logic. Using contextlib.nullcontext makes the code more concise and Pythonic, though it requires an additional import.

Low
Improve test assertion for clarity

Improve the assertion in test_transcription_does_not_reset_status_when_recording
by iterating through mock_status.update.call_args_list to ensure no call was
made with "Ready", making the test clearer.

tests/test_ui_status.py [112-125]

 def test_transcription_does_not_reset_status_when_recording(self):
     app = ChirpApp()
     # Simulate recording started again during transcription
     app._recording = True
 
     waveform = np.zeros(10)
     app._transcribe_and_inject(waveform)
 
     # Check that update("Ready", ...) was NOT called
-    ready_calls = [
-        call for call in self.mock_status.update.mock_calls
-        if len(call.args) > 0 and call.args[0] == "Ready"
-    ]
-    self.assertEqual(len(ready_calls), 0)
+    for call in self.mock_status.update.call_args_list:
+        self.assertNotEqual(call.args[0], "Ready", "Status should not be reset to 'Ready' while recording.")
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: The suggestion proposes a clearer and more explicit way to assert that a specific call was not made, improving the test's readability and robustness against future changes. While the existing test is functionally correct, the proposed change makes the test's intent more direct.

Low
  • More

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant