fix(tui): typing characters not visible until Enter pressed #11249

Closed
opened 2026-05-20 13:26:32 +00:00 by hamza.khyari · 0 comments
Member

Metadata

  • Commit Message: fix(tui): subclass Input to override _watch_value and eliminate layout=True per keystroke
  • Branch: bugfix/m8-tui-input-live-refresh

Background and Context

Textual's WriterThread only flushes stdout when its write queue is empty (if qsize() == 0: flush()). Every keystroke triggers Input._watch_value which sets self.virtual_size = Size(self.content_width, 1) — a Reactive(layout=True) attribute. This generates ANSI escape sequences that keep the write queue permanently non-empty during typing, so stdout is never flushed and typed characters are never rendered on screen.

When Enter is pressed, conversation.update() generates a large burst of output which eventually drains the queue, causing stdout to flush and everything to appear at once.

Root cause: Input._watch_value in Textual's built-in Input widget sets self.virtual_size on every single keystroke. virtual_size is defined as Reactive(layout=True), which calls refresh(layout=True) on every change — generating ANSI output that keeps the WriterThread write queue non-empty.

Two of the three layout=True sources per keystroke have already been eliminated:

  • _apply_symbol now skips Static.update() when the symbol is unchanged
  • CSS height: auto → height: 1 on #prompt > Input removes the auto_dimensions guard

One source remains: self.virtual_size = Size(self.content_width, 1) in Textual's own Input._watch_value.

Current Behaviour

User types characters into the TUI prompt → nothing visible on screen → press Enter → all typed characters and the LLM response appear simultaneously.

Expected Behaviour

Each typed character appears immediately on screen as it is entered, in real-time.

Acceptance Criteria

  • Characters typed in the TUI prompt appear on screen immediately as they are typed
  • The _watch_value override does not break cursor positioning, character insertion, or deletion
  • The Input.Changed event still fires correctly so mode detection ( / / / $ / ) works
  • All existing TUI BDD and integration tests continue to pass

Supporting Information

Fix: subclass textual.widgets.Input in src/cleveragents/tui/widgets/prompt.py, override _watch_value to skip self.virtual_size = Size(...) while preserving the rest of the method body (_suggestion, Changed event, _initial_value, cursor position).

Since the prompt widget is short and single-line, horizontal scrolling (which virtual_size enables) is not needed.

Relevant files:

  • src/cleveragents/tui/widgets/prompt.py_TextualPromptInput, _watch_value override
  • src/cleveragents/tui/cleveragents.tcss — already fixed height: 1

Subtasks

  • Create _PromptInput subclass of textual.widgets.Input in prompt.py that overrides _watch_value to skip virtual_size update
  • Preserve _suggestion, Changed event, _initial_value, cursor position logic in overridden method
  • Update _TextualPromptInput to use _PromptInput instead of raw _InputBase
  • Verify characters appear live in TUI when running agents tui
  • Tests (Behave): add scenario asserting that _watch_value does not set virtual_size
  • Run nox (all sessions), fix any errors

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line matches the Commit Message in Metadata exactly.
  • The commit is pushed to the branch matching the Branch in Metadata exactly.
  • The commit is submitted as a PR to master, reviewed, and merged.
## Metadata - **Commit Message**: `fix(tui): subclass Input to override _watch_value and eliminate layout=True per keystroke` - **Branch**: `bugfix/m8-tui-input-live-refresh` ## Background and Context Textual's `WriterThread` only flushes stdout when its write queue is empty (`if qsize() == 0: flush()`). Every keystroke triggers `Input._watch_value` which sets `self.virtual_size = Size(self.content_width, 1)` — a `Reactive(layout=True)` attribute. This generates ANSI escape sequences that keep the write queue permanently non-empty during typing, so stdout is never flushed and typed characters are never rendered on screen. When Enter is pressed, `conversation.update()` generates a large burst of output which eventually drains the queue, causing stdout to flush and everything to appear at once. **Root cause**: `Input._watch_value` in Textual's built-in `Input` widget sets `self.virtual_size` on every single keystroke. `virtual_size` is defined as `Reactive(layout=True)`, which calls `refresh(layout=True)` on every change — generating ANSI output that keeps the `WriterThread` write queue non-empty. Two of the three `layout=True` sources per keystroke have already been eliminated: - ✅ `_apply_symbol` now skips `Static.update()` when the symbol is unchanged - ✅ CSS `height: auto → height: 1` on `#prompt > Input` removes the `auto_dimensions` guard One source remains: `self.virtual_size = Size(self.content_width, 1)` in Textual's own `Input._watch_value`. ## Current Behaviour User types characters into the TUI prompt → nothing visible on screen → press Enter → all typed characters and the LLM response appear simultaneously. ## Expected Behaviour Each typed character appears immediately on screen as it is entered, in real-time. ## Acceptance Criteria - Characters typed in the TUI prompt appear on screen immediately as they are typed - The `_watch_value` override does not break cursor positioning, character insertion, or deletion - The `Input.Changed` event still fires correctly so mode detection (`❯` / `/` / `$` / `☰`) works - All existing TUI BDD and integration tests continue to pass ## Supporting Information Fix: subclass `textual.widgets.Input` in `src/cleveragents/tui/widgets/prompt.py`, override `_watch_value` to skip `self.virtual_size = Size(...)` while preserving the rest of the method body (`_suggestion`, `Changed` event, `_initial_value`, cursor position). Since the prompt widget is short and single-line, horizontal scrolling (which `virtual_size` enables) is not needed. Relevant files: - `src/cleveragents/tui/widgets/prompt.py` — `_TextualPromptInput`, `_watch_value` override - `src/cleveragents/tui/cleveragents.tcss` — already fixed `height: 1` ## Subtasks - [ ] Create `_PromptInput` subclass of `textual.widgets.Input` in `prompt.py` that overrides `_watch_value` to skip `virtual_size` update - [ ] Preserve `_suggestion`, `Changed` event, `_initial_value`, cursor position logic in overridden method - [ ] Update `_TextualPromptInput` to use `_PromptInput` instead of raw `_InputBase` - [ ] Verify characters appear live in TUI when running `agents tui` - [ ] Tests (Behave): add scenario asserting that `_watch_value` does not set `virtual_size` - [ ] Run `nox` (all sessions), fix any errors ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the first line matches the Commit Message in Metadata exactly. - The commit is pushed to the branch matching the Branch in Metadata exactly. - The commit is submitted as a PR to master, reviewed, and merged.
hamza.khyari added this to the v3.7.0 milestone 2026-05-20 13:26:32 +00:00
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#11249
No description provided.