TUI shell mode blocks Textual event loop — run_shell_command() called synchronously in on_input_submitted #8441

Open
opened 2026-04-13 18:56:44 +00:00 by HAL9000 · 1 comment
Owner

Metadata

Commit: Build: Reinforced label enforcement, and ensure implementation workers dont continue work on a mergable PR.
Branch: main

Background and Context

In src/cleveragents/tui/app.py, the on_input_submitted event handler processes shell mode (! prefix) by calling mode_router.process(text) synchronously. This call chain reaches run_shell_command() in src/cleveragents/tui/input/shell_exec.py, which uses subprocess.run(..., timeout=30). Because this runs on the Textual event loop thread, the entire UI is frozen for up to 30 seconds while a shell command executes.

Code evidence:

app.py on_input_submitted:

def on_input_submitted(self, event: InputSubmittedEvent) -> None:
    ...
    result = mode_router.process(text)   # ← synchronous, blocks event loop
    ...
    if result.mode == InputMode.SHELL:
        shell = result.shell_result
        ...

shell_exec.py run_shell_command:

proc = subprocess.run(
    command,
    shell=True,
    text=True,
    capture_output=True,
    timeout=timeout_seconds,   # up to 30 seconds blocking
)

Textual's event loop is single-threaded. Any blocking call in an event handler freezes the entire UI, including rendering, input handling, and all other widgets.

Current Behavior

When a user types !<command> and presses enter, the TUI freezes for the duration of the shell command (up to 30 seconds). During this time, no UI updates occur, no other input is processed, and the application appears hung.

Expected Behavior

Per the spec (ADR-044, M8): the TUI must remain responsive during shell execution. Shell commands should be executed in a Textual Worker thread so the event loop remains unblocked. A loading indicator should be shown while the command runs, and the result should be posted back to the UI thread via a Textual message when complete.

Acceptance Criteria

  • Shell command execution is moved to a Textual @work(thread=True) worker
  • The event loop is not blocked during shell command execution
  • A loading/pending state is shown in the conversation widget while the command runs
  • The shell result is posted back to the UI thread via a Textual message or call_from_thread
  • The 30-second timeout still applies within the worker thread
  • BDD test covers shell mode non-blocking behavior

Subtasks

  • Create a _run_shell_worker method decorated with @work(thread=True) in _TextualCleverAgentsTuiApp
  • Move run_shell_command() call into the worker
  • Show a loading state in the conversation widget when shell mode is activated
  • Post result back to UI thread using self.call_from_thread(self._on_shell_result, result)
  • Add _on_shell_result handler to update the conversation widget
  • Write BDD scenario verifying the event loop is not blocked
  • Verify existing shell mode tests pass

Definition of Done

The issue is closed when shell command execution runs in a Textual worker thread, the event loop remains responsive during execution, and all BDD tests pass on main.


Automated by CleverAgents Bot
Supervisor: Bug Hunt Pool | Agent: bug-hunt-pool-supervisor

## Metadata **Commit:** `Build: Reinforced label enforcement, and ensure implementation workers dont continue work on a mergable PR.` **Branch:** `main` ## Background and Context In `src/cleveragents/tui/app.py`, the `on_input_submitted` event handler processes shell mode (`!` prefix) by calling `mode_router.process(text)` synchronously. This call chain reaches `run_shell_command()` in `src/cleveragents/tui/input/shell_exec.py`, which uses `subprocess.run(..., timeout=30)`. Because this runs on the Textual event loop thread, the entire UI is frozen for up to 30 seconds while a shell command executes. **Code evidence:** `app.py` `on_input_submitted`: ```python def on_input_submitted(self, event: InputSubmittedEvent) -> None: ... result = mode_router.process(text) # ← synchronous, blocks event loop ... if result.mode == InputMode.SHELL: shell = result.shell_result ... ``` `shell_exec.py` `run_shell_command`: ```python proc = subprocess.run( command, shell=True, text=True, capture_output=True, timeout=timeout_seconds, # up to 30 seconds blocking ) ``` Textual's event loop is single-threaded. Any blocking call in an event handler freezes the entire UI, including rendering, input handling, and all other widgets. ## Current Behavior When a user types `!<command>` and presses enter, the TUI freezes for the duration of the shell command (up to 30 seconds). During this time, no UI updates occur, no other input is processed, and the application appears hung. ## Expected Behavior Per the spec (ADR-044, M8): the TUI must remain responsive during shell execution. Shell commands should be executed in a Textual `Worker` thread so the event loop remains unblocked. A loading indicator should be shown while the command runs, and the result should be posted back to the UI thread via a Textual message when complete. ## Acceptance Criteria - [ ] Shell command execution is moved to a Textual `@work(thread=True)` worker - [ ] The event loop is not blocked during shell command execution - [ ] A loading/pending state is shown in the conversation widget while the command runs - [ ] The shell result is posted back to the UI thread via a Textual message or `call_from_thread` - [ ] The 30-second timeout still applies within the worker thread - [ ] BDD test covers shell mode non-blocking behavior ## Subtasks - [ ] Create a `_run_shell_worker` method decorated with `@work(thread=True)` in `_TextualCleverAgentsTuiApp` - [ ] Move `run_shell_command()` call into the worker - [ ] Show a loading state in the conversation widget when shell mode is activated - [ ] Post result back to UI thread using `self.call_from_thread(self._on_shell_result, result)` - [ ] Add `_on_shell_result` handler to update the conversation widget - [ ] Write BDD scenario verifying the event loop is not blocked - [ ] Verify existing shell mode tests pass ## Definition of Done The issue is closed when shell command execution runs in a Textual worker thread, the event loop remains responsive during execution, and all BDD tests pass on `main`. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunt Pool | Agent: bug-hunt-pool-supervisor
HAL9000 added this to the v3.7.0 milestone 2026-04-13 19:13:00 +00:00
Author
Owner

[AUTO-OWNR-6] Triage Decision

Status: Verified

MoSCoW: Must Have
Priority: High

Rationale: Blocking the Textual event loop with a synchronous subprocess.run(..., timeout=30) call in on_input_submitted is a critical TUI correctness bug. Textual's event loop is single-threaded; any blocking call freezes the entire UI for up to 30 seconds, making the application appear hung. Per ADR-044 and the M8 spec, the TUI must remain responsive during shell execution. This is a Must Have fix — the TUI cannot be considered functional if shell mode renders it unresponsive.

Next Steps: Move shell command execution into a Textual @work(thread=True) worker (_run_shell_worker). Show a loading/pending state in the conversation widget while the command runs. Post the result back to the UI thread via call_from_thread(self._on_shell_result, result). The 30-second timeout should remain in effect within the worker. BDD scenario verifying non-blocking behavior required before close.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

## [AUTO-OWNR-6] Triage Decision **Status**: ✅ Verified **MoSCoW**: Must Have **Priority**: High **Rationale**: Blocking the Textual event loop with a synchronous `subprocess.run(..., timeout=30)` call in `on_input_submitted` is a critical TUI correctness bug. Textual's event loop is single-threaded; any blocking call freezes the entire UI for up to 30 seconds, making the application appear hung. Per ADR-044 and the M8 spec, the TUI must remain responsive during shell execution. This is a Must Have fix — the TUI cannot be considered functional if shell mode renders it unresponsive. **Next Steps**: Move shell command execution into a Textual `@work(thread=True)` worker (`_run_shell_worker`). Show a loading/pending state in the conversation widget while the command runs. Post the result back to the UI thread via `call_from_thread(self._on_shell_result, result)`. The 30-second timeout should remain in effect within the worker. BDD scenario verifying non-blocking behavior required before close. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#8441
No description provided.