Skip to content

feat: add visual status indicators for recording and transcription#60

Open
Whamp wants to merge 1 commit intomainfrom
palette-ux-status-indicators-11290331022185833990
Open

feat: add visual status indicators for recording and transcription#60
Whamp wants to merge 1 commit intomainfrom
palette-ux-status-indicators-11290331022185833990

Conversation

@Whamp
Copy link
Copy Markdown
Owner

@Whamp Whamp commented Feb 4, 2026

User description

This PR adds visual status indicators to the Chirp CLI application. Previously, users only had log output to discern the application state. Now, a persistent spinner clearly indicates when the app is "Recording..." (red) or "Transcribing..." (green). This improves the user experience by providing immediate, non-intrusive feedback on the current operation mode. The implementation uses rich.console.Status and is thread-safe. A new test file tests/test_ui_status.py verifies the logic and ensures no regressions.


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


PR Type

Enhancement, Tests


Description

  • Add visual status indicators for recording and transcription states

  • Implement thread-safe status management using rich.console.Status

  • Add comprehensive unit tests with full mocking of dependencies

  • Document UX learnings in .jules/palette.md


Diagram Walkthrough

flowchart LR
  ChirpApp["ChirpApp initialization"]
  StatusInit["Initialize status indicator<br/>to 'Ready'"]
  Recording["Start recording<br/>update to 'Recording...'<br/>red spinner"]
  Transcribing["Stop recording<br/>update to 'Transcribing...'<br/>green spinner"]
  Complete["Transcription complete<br/>stop status indicator"]
  
  ChirpApp --> StatusInit
  StatusInit --> Recording
  Recording --> Transcribing
  Transcribing --> Complete
Loading

File Walkthrough

Relevant files
Enhancement
main.py
Add status indicator updates for recording and transcription

src/chirp/main.py

  • Store Console instance as self.console for reuse
  • Initialize self.status_indicator with "Ready" status on app startup
  • Update status to "[bold red]Recording...[/bold red]" with point
    spinner when recording starts
  • Update status to "[bold green]Transcribing...[/bold green]" with dots
    spinner when recording stops
  • Add try-finally block in _transcribe_and_inject() to stop status
    indicator after transcription completes, only if not recording again
+30/-16 
Tests
test_ui_status.py
Add comprehensive unit tests for status indicators             

tests/test_ui_status.py

  • Create new test file with comprehensive unit tests for status
    indicator functionality
  • Mock all external dependencies (ParakeetManager, AudioCapture,
    AudioFeedback, etc.) to isolate UI logic
  • Test status initialization to "Ready" state
  • Test status update to "Recording..." with correct styling and spinner
  • Test status update to "Transcribing..." with correct styling and
    spinner
  • Test status indicator stop behavior when transcription completes and
    recording is not active
  • Test status indicator does not stop if recording starts again during
    transcription
+85/-0   
Documentation
palette.md
Document UX learnings for status indicators                           

.jules/palette.md

  • Document critical UX learning about lack of visibility in background
    CLI tools
  • Record the solution of using persistent, state-aware status indicators
  • Capture the importance of non-intrusive feedback for operation mode
    awareness
+5/-0     

- Added `rich` status indicators to `ChirpApp` for "Recording..." and "Transcribing..." states.
- Implemented robust status management to handle concurrent recording/transcription states.
- Added comprehensive unit tests in `tests/test_ui_status.py` with full mocking of external dependencies.
- Created `.jules/palette.md` for UX learnings.

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 code logs the full transcription text (self.logger.debug("Transcription: %s",
text)), which can expose sensitive/PII content in application logs (especially if debug
logs are enabled or collected centrally).
main.py [172-178]

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: 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 in logs: The PR adds logging of the full transcribed text (self.logger.debug("Transcription:
%s", text)), which can contain sensitive user speech and violates secure logging
practices.

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:
Unredacted transcription handling: The PR introduces handling that outputs raw transcription content to logs without
sanitization/redaction, increasing the risk of exposing sensitive user-provided 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

Generic: Comprehensive Audit Trails

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

Status:
Missing audit context: The new logging added around transcription does not include user identity/context (if
applicable) so it is unclear whether audit-trail requirements for critical actions are
met.

Referred Code
start_time = time.perf_counter()
if waveform.size == 0:
    self.logger.warning("No audio samples captured")
    return
try:
    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
High-level
Decouple UI state from task execution

The current logic for stopping the status indicator has a race condition. To fix
this, use an atomic counter to track active transcription tasks, ensuring the
indicator only stops when all tasks are complete.

Examples:

src/chirp/main.py [158-181]
        self._executor.submit(self._transcribe_and_inject, waveform)

    def _transcribe_and_inject(self, waveform) -> None:
        try:
            start_time = time.perf_counter()
            if waveform.size == 0:
                self.logger.warning("No audio samples captured")
                return
            try:
                text = self.parakeet.transcribe(waveform, sample_rate=16_000, language=self.config.language)

 ... (clipped 14 lines)

Solution Walkthrough:

Before:

class ChirpApp:
    def _stop_recording(self):
        # ...
        self._executor.submit(self._transcribe_and_inject, waveform)

    def _transcribe_and_inject(self, waveform):
        try:
            # ... do transcription ...
        finally:
            # This check is insufficient for multiple concurrent tasks.
            # If a new recording has not started, it stops the indicator,
            # even if another transcription is still running.
            if not self._recording:
                self.status_indicator.stop()

After:

class ChirpApp:
    def __init__(self):
        # ...
        self._active_transcriptions = 0
        self._transcription_lock = threading.Lock()

    def _stop_recording(self):
        # ...
        with self._transcription_lock:
            self._active_transcriptions += 1
        self._executor.submit(self._transcribe_and_inject, waveform)

    def _transcribe_and_inject(self, waveform):
        try:
            # ... do transcription ...
        finally:
            with self._transcription_lock:
                self._active_transcriptions -= 1
                should_stop = not self._recording and self._active_transcriptions == 0
            if should_stop:
                self.status_indicator.stop()
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical race condition where concurrent transcription tasks can prematurely stop the UI status indicator, and proposes a robust solution using a counter.

High
General
Reset indicator to ready

After transcription finishes and the status indicator stops, update its text to
"Ready" to show the application is idle.

src/chirp/main.py [179-181]

 finally:
     if not self._recording:
         self.status_indicator.stop()
+        self.status_indicator.update("Ready")
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: This is a good suggestion for improving the user experience by explicitly showing a "Ready" status after transcription, rather than leaving the status line blank.

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