Skip to content

assembly code: TUI UX overhaul (modals, streaming transcript, voice mode)#242

Merged
alexkroman merged 4 commits into
mainfrom
code-tui-ux-fixes
Jun 18, 2026
Merged

assembly code: TUI UX overhaul (modals, streaming transcript, voice mode)#242
alexkroman merged 4 commits into
mainfrom
code-tui-ux-fixes

Conversation

@alexkroman

Copy link
Copy Markdown
Collaborator

Summary

Rolls the assembly code Textual TUI UX work — landed on the code-tui-ux-fixes integration branch via #240 — up to main. The branch now also merges origin/main (resolving a one-file tests/test_microphone.py overlap with #238's SIGINT-teardown guard), so this lands cleanly.

The work was driven by exercising the live TUI through Textual's SVG screenshot pilot and importing interaction patterns from langchain-ai/deepagents' code agent.

TUI fixes

  • Modal blackout fixedModalScreen { background: transparent } so approval/ask prompts dock at the bottom and leave the transcript visible above them, instead of blanking the screen.
  • Ctrl-C actually cancels — the cancel path now interrupts the in-flight turn rather than just showing a "cancelling…" note while the agent keeps running.

Mounted-widget transcript (replaces the append-only RichLog)

  • The transcript is a VerticalScroll of per-message widgets. The assistant reply streams in place token-by-token (cheap plain-text repaint), then re-renders as Markdown when finalized.
  • Collapsible tool output — a clipped preview that expands to the full content on Ctrl+O or click, so a large file dump or command output doesn't flood the view (deepagents-code's compaction pattern).
  • Compact tool-call lines (→ write_file(app.py)) and risk warnings on mutating/destructive calls.
  • Dynamic content is wrapped in rich.text.Text (no console-markup parsing) so a stray [ can't raise or inject styling.

Voice mode

  • Ctrl-V toggles voice on/off at runtime, with a status affordance.
  • Animated listening / speaking / thinking phases (a set_interval pulse + state flips) instead of a static listening bar.
  • Spoken, voice-answerable approval & ask modals — in voice mode each modal speaks its prompt and accepts a spoken reply (approve / auto-approve / reject, or a free-text answer), off the UI thread; the keyboard path always stays available. An unclear spoken reply falls back to reject (the same safe default as the keyboard), and silence never auto-rejects.

Also included

  • The earlier assembly code TUI fix (222c030): CLI-style approval, voice-mode banner, microphone robustness (stereo→mono downmix fallback for devices that won't open as mono), and unique session IDs.
  • A refactor splitting the TUI into modular files (messages.py, modals.py, voice_ui.py, tui_status.py, summarize.py, risk.py) to stay under the 500-line gate, each with its own test module.
  • The agent-cascadelive command rename, which converges with the same rename already on main (Rename agent-cascade command to live and add deepagents brain #237) — no net change to that command vs. main.

Testing

./scripts/check.sh passes end-to-end on the merge commit: 3419 tests, 99.58% branch coverage, 100% patch coverage, mutation gate (97 mutants on changed lines), no new escape hatches, and twine check clean.

🤖 Generated with Claude Code


Generated by Claude Code

alexkroman-assembly and others added 3 commits June 17, 2026 21:19
…obustness, unique sessions

- Approval prompt: replace the Textual Button row with a plain y/a/n
  keyboard-hint line, so it reads like a CLI prompt rather than chrome.
- Voice mode banner: defer the first mic open until after the splash paints
  (call_after_refresh) — opening PortAudio inline on mount raced Textual's
  initial render and left the banner blank until a resize/focus repaint.
- Mic open: redirect PortAudio's C-level stderr noise (which corrupted the
  TUI screen) via a safe-by-construction stdio.suppress_native_stderr; and on
  a mono open failure, reopen at the device's real channel count and downmix
  to mono, with a clear permission error when the device exposes 0 channels.
- Sessions: give each `assembly code` run a unique thread id instead of
  reusing a fixed "default" thread (which silently resumed prior chats);
  `--session NAME` still resumes a named one.

Gates verified pre-commit: ruff, pyright, mypy, full pytest suite, 100% patch
coverage, mutation gate. Full check.sh not run at user request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

## Summary

This PR refactors the coding-agent TUI into smaller, more maintainable
modules while keeping the main `CodeAgentApp` class intact. It also
renames the `agent-cascade` command to `live` for clarity. The changes
improve code organization without altering user-facing behavior.

## Key Changes

**TUI Refactoring:**
- **Split `tui.py`** into focused modules to stay under the 500-line
file-length gate:
- `modals.py`: `ApprovalScreen` and `AskScreen` modal dialogs with voice
support
- `messages.py`: Transcript widget classes (`UserMessage`,
`AssistantMessage`, `ToolOutput`, etc.)
- `voice_ui.py`: Voice capture/readback mechanics (`_VoiceIO` protocol,
`_VoiceLegs` mixin)
- `tui_status.py`: Pure text helpers for status line and spinner
(`_spinner_text`, `_status_text`, etc.)
- `summarize.py`: Tool activity summaries shared by TUI and Rich
fallback
  - `risk.py`: Risk heuristics for tool approval prompts

- **Extracted helper modules:**
- `agent_cascade/brain.py`: Deepagents graph builder for the live
cascade (system prompt, tool guidance, completer)
- Moved `approval_from_speech` mapping to `modals.py` for
voice-answerable approval

**Command Rename:**
- Renamed `agent-cascade` command to `live` throughout (command
registration, help text, tests, docs, templates)

**Test Reorganization:**
- Split large test files to stay under the gate:
  - `test_code_modals.py`: Modal screen tests with voice doubles
  - `test_code_messages.py`: Transcript widget rendering tests
  - `test_code_tui_voice.py`: Voice toggle and readback tests (expanded)
  - `test_code_tui_status.py`: Pure status/spinner text helpers
  - `test_code_summarize.py`: Tool summarization tests
  - `test_code_session_stream.py`: Streaming and cancellation tests
  - `test_code_risk.py`: Risk heuristic tests
  - `test_agent_cascade_brain.py`: Deepagents graph builder tests

**Installer Improvements:**
- Enhanced `install.sh` with dev mode support (`--install-method git` /
`--dev`)
- Added usage help and environment variable overrides
- Supports both release (published) and editable (development) installs

**Events & Session:**
- Added `AssistantDelta` event for per-token streaming (frozen,
hashable)
- Updated `CodeSession` to handle dual-mode streaming (`values` +
`messages`)

## Implementation Details

- **Voice modals** speak prompts and listen for spoken replies off the
UI thread (daemon threads), marshaling back via `call_from_thread`
- **Risk warnings** are pure functions (no Textual imports) so they
unit-test cleanly
- **Summarizers** clip long tool args/output to keep the transcript
scannable (mirroring deepagents-code's collapsible rows)
- **Deepagents brain** builds system prompts that advertise only
available tools, preventing the agent from narrating actions it can't
take
- All refactored modules maintain the same public APIs; `CodeAgentApp`
remains the single entry point

## Testing

- New test files cover the extracted modules with real Textual app
headless tests and pure function unit tests
- Snapshot tests updated for the `live` command rename
- CI workflow enhanced with end-to-end install.sh validation

https://claude.ai/code/session_01Ad72JciKrsz4TKG7ZY9GR6

---------

Co-authored-by: Claude <noreply@anthropic.com>
Comment thread aai_cli/core/stdio.py
devnull_fd: int | None = None
try:
saved_fd = os.dup(_STDERR_FD)
devnull_fd = os.open(os.devnull, os.O_WRONLY)
_voice_paused: bool
_last_reply: str

def _set_voice_phase(self, phase: str) -> None: ...
_last_reply: str

def _set_voice_phase(self, phase: str) -> None: ...
def _sync_input_mode(self) -> None: ...

def _set_voice_phase(self, phase: str) -> None: ...
def _sync_input_mode(self) -> None: ...
def _submit(self, text: str) -> None: ...
def _set_voice_phase(self, phase: str) -> None: ...
def _sync_input_mode(self) -> None: ...
def _submit(self, text: str) -> None: ...
def _note(self, text: str) -> None: ...
@alexkroman alexkroman enabled auto-merge June 18, 2026 17:24
The voice legs run on daemon threads that call back onto the UI thread via
call_from_thread. If the app stops (a quit, or a test's run_test block exiting)
while a leg is mid-call, that callback raises RuntimeError in the daemon thread,
which pytest's threadexception plugin escalates to a failure — surfacing as a
flaky `tests (windows, py3.12)` run on test_submit_sets_thinking_phase.

Route every leg through a guarded body that swallows the callback error once the
app is no longer running (the spoken turn is moot then) while still surfacing a
genuine failure that happens while the app is live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ad72JciKrsz4TKG7ZY9GR6
@alexkroman alexkroman added this pull request to the merge queue Jun 18, 2026
Merged via the queue into main with commit 3243ce6 Jun 18, 2026
20 checks passed
@alexkroman alexkroman deleted the code-tui-ux-fixes branch June 18, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants